缓存抢购货物
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
package com.rj.dto;
|
||||
|
||||
import com.rj.entity.LbGoods;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 单次批量抢单会话内,保证同一租户的同一货品 ID 最多被一个账号尝试抢购。
|
||||
@@ -12,13 +16,36 @@ public class LbRushBuyGoodsCoordinator {
|
||||
|
||||
private final ConcurrentHashMap<String, String> claimedBy = new ConcurrentHashMap<>();
|
||||
|
||||
/** 同批次内按租户缓存可抢货品列表,避免多账号重复查库。 */
|
||||
private final ConcurrentHashMap<String, List<LbGoods>> rushBuyGoodsByTenant = new ConcurrentHashMap<>();
|
||||
|
||||
/** 任一线程收到「活动未开始」后置位,本批次内其他账号不再发起抢购。 */
|
||||
private volatile boolean activityNotStarted;
|
||||
|
||||
/** 任一线程收到「活动已结束」后置位,本批次内其他账号不再发起抢购。 */
|
||||
private volatile boolean activityEnded;
|
||||
|
||||
private static String key(String tenantId, Object goodsId) {
|
||||
return tenantId.trim() + ":" + goodsId;
|
||||
}
|
||||
|
||||
public List<LbGoods> peekRushBuyGoodsList(String tenantId) {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return rushBuyGoodsByTenant.get(tenantId.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 同租户在本批次内仅加载一次可抢货品列表,后续账号直接复用缓存。
|
||||
*/
|
||||
public List<LbGoods> resolveRushBuyGoodsList(String tenantId, Supplier<List<LbGoods>> loader) {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
return List.copyOf(loader.get());
|
||||
}
|
||||
return rushBuyGoodsByTenant.computeIfAbsent(tenantId.trim(), k -> List.copyOf(loader.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} 表示当前账号取得该货品的独占权;{@code false} 表示已被其他账号占用
|
||||
*/
|
||||
@@ -64,6 +91,14 @@ public class LbRushBuyGoodsCoordinator {
|
||||
return activityNotStarted;
|
||||
}
|
||||
|
||||
public void markActivityEnded() {
|
||||
activityEnded = true;
|
||||
}
|
||||
|
||||
public boolean isActivityEnded() {
|
||||
return activityEnded;
|
||||
}
|
||||
|
||||
public void release(String tenantId, Object goodsId, String accountId) {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()
|
||||
|| goodsId == null || accountId == null || accountId.trim().isEmpty()) {
|
||||
|
||||
@@ -46,6 +46,9 @@ public class HxrAdminBuyService {
|
||||
/** 外部系统返回此文案时表示抢购活动尚未开始。 */
|
||||
public static final String MSG_ACTIVITY_NOT_STARTED = "活动未开始";
|
||||
|
||||
/** 外部系统返回此文案时表示抢购活动已结束。 */
|
||||
public static final String MSG_ACTIVITY_ENDED = "活动已结束";
|
||||
|
||||
/** 外部系统返回此文案时表示该货品已被他人抢订,同批次其他账号不应再尝试。 */
|
||||
public static final String MSG_ORDER_ALREADY_GRABBED = "此订单已被抢订";
|
||||
|
||||
@@ -164,6 +167,10 @@ public class HxrAdminBuyService {
|
||||
log.warn("hxr /api/order/buy 活动未开始,调用方应停止继续抢购 id={} sellerId={}",
|
||||
goodsId, sellerId);
|
||||
}
|
||||
if (msg != null && msg.contains(MSG_ACTIVITY_ENDED)) {
|
||||
log.warn("hxr /api/order/buy 活动已结束,调用方应停止继续抢购 id={} sellerId={}",
|
||||
goodsId, sellerId);
|
||||
}
|
||||
if (MSG_DAILY_LIMIT_EXCEEDED.equals(msg)) {
|
||||
log.warn("hxr /api/order/buy 当日可抢订单数已满,停止该账号继续抢购 id={} sellerId={} tokenPrefix={}",
|
||||
goodsId, sellerId, abbreviate(resolvedToken, 8));
|
||||
@@ -213,6 +220,11 @@ public class HxrAdminBuyService {
|
||||
public boolean activityNotStarted() {
|
||||
return apiMsg != null && apiMsg.contains(MSG_ACTIVITY_NOT_STARTED);
|
||||
}
|
||||
|
||||
/** msg 包含「活动已结束」时,调用方应立刻停止该账号及本批次后续抢购。 */
|
||||
public boolean activityEnded() {
|
||||
return apiMsg != null && apiMsg.contains(MSG_ACTIVITY_ENDED);
|
||||
}
|
||||
}
|
||||
|
||||
private static String randomNoncestr() {
|
||||
|
||||
@@ -6,13 +6,19 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.rj.config.HxrAdminProperties;
|
||||
import com.rj.dto.hxr.HxrGoodsApiContext;
|
||||
import com.rj.dto.hxr.HxrLbGoodsSelectResponse;
|
||||
import com.rj.dto.hxr.HxrUserLoginApiContext;
|
||||
import com.rj.entity.LbBuyerShopping;
|
||||
import com.rj.mapper.LbBuyerShoppingMapper;
|
||||
import com.rj.tenant.TenantContextHolder;
|
||||
import com.rj.util.HxrGoodsSignUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -35,6 +41,9 @@ public class HxrAdminGoodsService {
|
||||
|
||||
public static final int GOODS_PAGE_SIZE = 20;
|
||||
|
||||
/** 与 {@link com.rj.service.impl.LbBuyerShoppingServiceImpl} 模拟登录默认密码一致。 */
|
||||
private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456";
|
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private static final String USER_AGENT =
|
||||
@@ -50,6 +59,9 @@ public class HxrAdminGoodsService {
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
private final HxrAdminProperties properties;
|
||||
private final HxrAdminUserLoginService hxrAdminUserLoginService;
|
||||
private final ILbThirdIntegrationConfigService lbThirdIntegrationConfigService;
|
||||
private final LbBuyerShoppingMapper lbBuyerShoppingMapper;
|
||||
|
||||
/**
|
||||
* 分页拉取货品列表(使用 {@code hxr.admin} 配置文件,兼容旧调用)。
|
||||
@@ -103,6 +115,92 @@ public class HxrAdminGoodsService {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
GoodsPageFetchResult firstAttempt = executeGoodsPageRequest(page, ctx);
|
||||
if (firstAttempt.success()) {
|
||||
return Optional.of(firstAttempt.response());
|
||||
}
|
||||
if (!firstAttempt.loginRequired()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<String> refreshedToken = refreshTokenFromBuyerShopping();
|
||||
if (refreshedToken.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
HxrGoodsApiContext refreshedCtx = withToken(ctx, refreshedToken.get());
|
||||
GoodsPageFetchResult retryAttempt = executeGoodsPageRequest(page, refreshedCtx);
|
||||
if (retryAttempt.success()) {
|
||||
log.info("hxr /api/order/goods token 失效后已通过买方手机号模拟登录并重试成功 page={}", page);
|
||||
return Optional.of(retryAttempt.response());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* token 失效时:从 {@code lb_buyer_shopping} 取当前租户 1 条记录的买家手机号模拟登录,解析 {@code data.userinfo.token}。
|
||||
*/
|
||||
private Optional<String> refreshTokenFromBuyerShopping() {
|
||||
String tenantId = TenantContextHolder.getTenantId();
|
||||
if (!StringUtils.hasText(tenantId)) {
|
||||
log.warn("hxr /api/order/goods token 失效,但当前线程无 tenantId,无法从 lb_buyer_shopping 模拟登录");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LbBuyerShopping> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(LbBuyerShopping::getTenantId, tenantId.trim())
|
||||
.isNotNull(LbBuyerShopping::getBuyerMobile)
|
||||
.ne(LbBuyerShopping::getBuyerMobile, "")
|
||||
.orderByDesc(LbBuyerShopping::getBuyTime)
|
||||
.last("LIMIT 1");
|
||||
LbBuyerShopping sample = lbBuyerShoppingMapper.selectOne(queryWrapper);
|
||||
if (sample == null || !StringUtils.hasText(sample.getBuyerMobile())) {
|
||||
log.warn("hxr /api/order/goods token 失效,tenantId={} 在 lb_buyer_shopping 中未找到可用买家手机号",
|
||||
tenantId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String buyerMobile = sample.getBuyerMobile().trim();
|
||||
Optional<HxrUserLoginApiContext> loginCtxOpt =
|
||||
lbThirdIntegrationConfigService.resolveUserLoginApiContext(tenantId.trim());
|
||||
if (loginCtxOpt.isEmpty()) {
|
||||
log.warn("hxr /api/order/goods token 失效,tenantId={} 未找到登录 API 配置,buyerMobile={}",
|
||||
tenantId, buyerMobile);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
HxrAdminUserLoginService.LoginApiResult loginResult = hxrAdminUserLoginService.login(
|
||||
buyerMobile,
|
||||
DEFAULT_SIMULATE_LOGIN_PASSWORD,
|
||||
loginCtxOpt.get());
|
||||
if (!loginResult.success()) {
|
||||
log.warn("hxr /api/order/goods 买方模拟登录失败 tenantId={} buyerMobile={} apiCode={} apiMsg={}",
|
||||
tenantId, buyerMobile, loginResult.apiCode(), loginResult.apiMsg());
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String token = HxrAdminUserLoginService.extractToken(loginResult.parsed());
|
||||
if (!StringUtils.hasText(token)) {
|
||||
log.warn("hxr /api/order/goods 买方模拟登录成功但响应无 token tenantId={} buyerMobile={}",
|
||||
tenantId, buyerMobile);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
log.info("hxr /api/order/goods 已通过 lb_buyer_shopping 买家手机号模拟登录获取新 token tenantId={} buyerMobile={} tokenPrefix={}",
|
||||
tenantId, buyerMobile, abbreviate(token.trim(), 8));
|
||||
return Optional.of(token.trim());
|
||||
} catch (Exception e) {
|
||||
log.error("hxr /api/order/goods 买方模拟登录异常 tenantId={} buyerMobile={}", tenantId, buyerMobile, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private GoodsPageFetchResult executeGoodsPageRequest(int page, HxrGoodsApiContext ctx) throws Exception {
|
||||
String resolvedToken = ctx.token();
|
||||
String appStr = ctx.appStr().trim();
|
||||
String goodsApiBaseUrl = ctx.goodsApiBaseUrl();
|
||||
|
||||
int pageLimit = ctx.pageLimit() > 0 ? ctx.pageLimit() : GOODS_PAGE_SIZE;
|
||||
int safePage = Math.max(1, page);
|
||||
String uri = UriComponentsBuilder.fromUriString(goodsApiBaseUrl)
|
||||
@@ -119,7 +217,7 @@ public class HxrAdminGoodsService {
|
||||
signParams.put("limit", pageLimit);
|
||||
signParams.put("timestamp", timestamp);
|
||||
signParams.put("noncestr", noncestr);
|
||||
String sign = HxrGoodsSignUtil.computeSign(signParams, appStr.trim());
|
||||
String sign = HxrGoodsSignUtil.computeSign(signParams, appStr);
|
||||
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(60))
|
||||
@@ -146,32 +244,55 @@ public class HxrAdminGoodsService {
|
||||
if (status < 200 || status >= 300) {
|
||||
log.warn("hxr /api/order/goods HTTP {} page={} bodyPrefix={}",
|
||||
status, safePage, abbreviate(response.body(), 400));
|
||||
return Optional.empty();
|
||||
return GoodsPageFetchResult.failure(false);
|
||||
}
|
||||
|
||||
String bodyText = response.body();
|
||||
if (bodyText == null || bodyText.isBlank()) {
|
||||
log.warn("hxr /api/order/goods empty body page={}", safePage);
|
||||
return Optional.empty();
|
||||
return GoodsPageFetchResult.failure(false);
|
||||
}
|
||||
|
||||
JsonNode root = JSON.readTree(bodyText);
|
||||
int code = root.path("code").asInt(-1);
|
||||
String msg = root.path("msg").asText("");
|
||||
if (code != 0) {
|
||||
boolean loginRequired = code == 401 || HxrAdminBuyService.MSG_LOGIN_REQUIRED.equals(msg);
|
||||
log.warn("hxr /api/order/goods api code={} msg={} page={} bodyPrefix={}",
|
||||
code, msg, safePage, abbreviate(bodyText, 400));
|
||||
return Optional.empty();
|
||||
return GoodsPageFetchResult.failure(loginRequired);
|
||||
}
|
||||
|
||||
JsonNode dataNode = root.get("data");
|
||||
if (isErrorPayload(dataNode)) {
|
||||
log.warn("hxr /api/order/goods 返回异常 data={} page={} msg={}", dataNode, safePage, msg);
|
||||
return Optional.empty();
|
||||
return GoodsPageFetchResult.failure(false);
|
||||
}
|
||||
|
||||
HxrLbGoodsSelectResponse body = JSON.treeToValue(root, HxrLbGoodsSelectResponse.class);
|
||||
return Optional.of(body);
|
||||
return GoodsPageFetchResult.success(body);
|
||||
}
|
||||
|
||||
private static HxrGoodsApiContext withToken(HxrGoodsApiContext ctx, String token) {
|
||||
return new HxrGoodsApiContext(
|
||||
ctx.goodsApiBaseUrl(),
|
||||
ctx.buyApiUrl(),
|
||||
ctx.pageLimit(),
|
||||
ctx.origin(),
|
||||
ctx.referer(),
|
||||
token,
|
||||
ctx.appStr());
|
||||
}
|
||||
|
||||
private record GoodsPageFetchResult(boolean success, HxrLbGoodsSelectResponse response, boolean loginRequired) {
|
||||
|
||||
static GoodsPageFetchResult success(HxrLbGoodsSelectResponse response) {
|
||||
return new GoodsPageFetchResult(true, response, false);
|
||||
}
|
||||
|
||||
static GoodsPageFetchResult failure(boolean loginRequired) {
|
||||
return new GoodsPageFetchResult(false, null, loginRequired);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从完整 URL 中去掉 query,供配置文件回退路径使用。 */
|
||||
|
||||
@@ -522,6 +522,12 @@ public class LbBuyAccountServiceImpl
|
||||
private static String formatRushBuyAccountLabel(String accountId, LbBuyAccount account) {
|
||||
StringBuilder sb = new StringBuilder("抢单账号");
|
||||
if (account != null) {
|
||||
if (account.getTenantId() != null && !account.getTenantId().trim().isEmpty()) {
|
||||
sb.append(" tenantId=").append(account.getTenantId().trim());
|
||||
}
|
||||
if (account.getTenantName() != null && !account.getTenantName().trim().isEmpty()) {
|
||||
sb.append(" tenantName=").append(account.getTenantName().trim());
|
||||
}
|
||||
if (account.getLoginAccount() != null && !account.getLoginAccount().trim().isEmpty()) {
|
||||
sb.append(" loginAccount=").append(account.getLoginAccount().trim());
|
||||
}
|
||||
@@ -649,7 +655,8 @@ public class LbBuyAccountServiceImpl
|
||||
}
|
||||
|
||||
private static String formatRushBuySuccessDetail(Map<?, ?> detail) {
|
||||
return "货品ID=" + detail.get("id")
|
||||
return formatRushBuyDetailPrefix(detail)
|
||||
+ "货品ID=" + detail.get("id")
|
||||
+ ",金额=" + detail.get("totalMoney");
|
||||
}
|
||||
|
||||
@@ -659,12 +666,21 @@ public class LbBuyAccountServiceImpl
|
||||
String reason = apiCode != null
|
||||
? "接口返回码=" + apiCode + (apiMsg != null ? "," + apiMsg : "")
|
||||
: (apiMsg != null ? apiMsg.toString() : "未知原因");
|
||||
return "货品ID=" + detail.get("id")
|
||||
return formatRushBuyDetailPrefix(detail)
|
||||
+ "货品ID=" + detail.get("id")
|
||||
+ ",金额=" + detail.get("totalMoney")
|
||||
+ ",原因=" + reason
|
||||
+ ",返回信息=" + (apiMsg != null ? apiMsg : "无");
|
||||
}
|
||||
|
||||
private static String formatRushBuyDetailPrefix(Map<?, ?> detail) {
|
||||
Object tenantId = detail.get("tenantId");
|
||||
if (tenantId == null || tenantId.toString().trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "tenantId=" + tenantId.toString().trim() + ",";
|
||||
}
|
||||
|
||||
private static String truncateRushBuyResult(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
|
||||
@@ -22,6 +22,8 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
@@ -558,15 +560,8 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
String previousTenantId = TenantContextHolder.getTenantId();
|
||||
TenantContextHolder.setTenantId(tid);
|
||||
try {
|
||||
LocalDateTime updatedAfter = LocalDateTime.now().minusDays(1);
|
||||
LambdaQueryWrapper<LbGoods> w = new LambdaQueryWrapper<>();
|
||||
w.eq(LbGoods::getTenantId, tid);
|
||||
w.isNotNull(LbGoods::getSellerId);
|
||||
w.gt(LbGoods::getTotalMoney, RUSH_BUY_MIN_TOTAL_MONEY);
|
||||
w.le(LbGoods::getTotalMoney, RUSH_BUY_MAX_TOTAL_MONEY);
|
||||
w.ge(LbGoods::getUpdatedAt, updatedAfter);
|
||||
w.orderByDesc(LbGoods::getTotalMoney);
|
||||
List<LbGoods> goodsList = this.list(w);
|
||||
log.info("查询-待抢购-货物列表......");
|
||||
List<LbGoods> goodsList = resolveRushBuyGoodsList(tid, accountTag, goodsCoordinator);//
|
||||
if (goodsList.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "lb_goods 中无可抢购货品(金额需大于25000且不超过38000,且更新时间需在24小时内)");
|
||||
@@ -587,6 +582,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
boolean stoppedByDailyLimit = false;
|
||||
boolean stoppedByLoginRequired = false;
|
||||
boolean stoppedByActivityNotStarted = false;
|
||||
boolean stoppedByActivityEnded = false;
|
||||
boolean tokenRefreshAttempted = false;
|
||||
boolean tokenRefreshed = false;
|
||||
boolean restartAfterTokenRefresh;
|
||||
@@ -598,6 +594,10 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
stoppedByActivityNotStarted = true;
|
||||
break;
|
||||
}
|
||||
if (goodsCoordinator != null && goodsCoordinator.isActivityEnded()) {
|
||||
stoppedByActivityEnded = true;
|
||||
break;
|
||||
}
|
||||
if (successCount >= maxBuyCount) {
|
||||
stoppedByMax = true;
|
||||
break;
|
||||
@@ -614,6 +614,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
&& !goodsCoordinator.tryAcquire(tid, goods.getId(), accountId)) {
|
||||
skippedCount++;
|
||||
Map<String, Object> skippedItem = new LinkedHashMap<>();
|
||||
skippedItem.put("tenantId", tid);
|
||||
skippedItem.put("id", goods.getId());
|
||||
skippedItem.put("sellerId", goods.getSellerId());
|
||||
skippedItem.put("title", goods.getTitle());
|
||||
@@ -635,6 +636,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
|
||||
attemptCount++;
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("tenantId", tid);
|
||||
item.put("id", goods.getId());
|
||||
item.put("sellerId", goods.getSellerId());
|
||||
item.put("title", goods.getTitle());
|
||||
@@ -690,6 +692,18 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (buyResult.activityEnded()) {
|
||||
stoppedByActivityEnded = true;
|
||||
if (goodsCoordinator != null) {
|
||||
goodsCoordinator.markActivityEnded();
|
||||
}
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("活动已结束,停止继续抢购");
|
||||
} else {
|
||||
log.info("{} 活动已结束,停止继续抢购", accountTag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (buyResult.loginRequired()) {
|
||||
if (tokenRefreshContext != null && !tokenRefreshAttempted) {
|
||||
if (accountTag.isEmpty()) {
|
||||
@@ -752,11 +766,17 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
? HxrAdminBuyService.MSG_ACTIVITY_NOT_STARTED
|
||||
+ ",已停止抢购;成功 " + successCount + " 笔,失败 " + failCount + " 笔"
|
||||
: HxrAdminBuyService.MSG_ACTIVITY_NOT_STARTED);
|
||||
} else if (stoppedByActivityEnded) {
|
||||
result.put("message", successCount > 0
|
||||
? HxrAdminBuyService.MSG_ACTIVITY_ENDED
|
||||
+ ",已停止抢购;成功 " + successCount + " 笔,失败 " + failCount + " 笔"
|
||||
: HxrAdminBuyService.MSG_ACTIVITY_ENDED);
|
||||
} else {
|
||||
result.put("message", successCount > 0
|
||||
? "抢购完成,成功 " + successCount + " 笔,失败 " + failCount + " 笔"
|
||||
: "抢购未成功");
|
||||
}
|
||||
result.put("tenantId", tid);
|
||||
result.put("successCount", successCount);
|
||||
result.put("failCount", failCount);
|
||||
result.put("skippedCount", skippedCount);
|
||||
@@ -767,6 +787,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
result.put("stoppedByDailyLimit", stoppedByDailyLimit);
|
||||
result.put("stoppedByLoginRequired", stoppedByLoginRequired);
|
||||
result.put("stoppedByActivityNotStarted", stoppedByActivityNotStarted);
|
||||
result.put("stoppedByActivityEnded", stoppedByActivityEnded);
|
||||
result.put("tokenRefreshed", tokenRefreshed);
|
||||
result.put("details", details);
|
||||
if (accountTag.isEmpty()) {
|
||||
@@ -801,6 +822,63 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
return rushBuyAccountLabel.trim();
|
||||
}
|
||||
|
||||
private List<LbGoods> resolveRushBuyGoodsList(
|
||||
String tid, String accountTag, LbRushBuyGoodsCoordinator goodsCoordinator) {
|
||||
if (goodsCoordinator == null) {
|
||||
return loadRushBuyGoodsList(tid, accountTag);
|
||||
}
|
||||
List<LbGoods> cached = goodsCoordinator.peekRushBuyGoodsList(tid);
|
||||
if (cached != null) {
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("rushBuy 复用同批次已缓存的可抢货品,tenantId={},结果数量={}", tid, cached.size());
|
||||
} else {
|
||||
log.info("{} rushBuy 复用同批次已缓存的可抢货品,tenantId={},结果数量={}",
|
||||
accountTag, tid, cached.size());
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
return goodsCoordinator.resolveRushBuyGoodsList(tid, () -> loadRushBuyGoodsList(tid, accountTag));
|
||||
}
|
||||
|
||||
private List<LbGoods> loadRushBuyGoodsList(String tid, String accountTag) {
|
||||
LocalDateTime updatedAfter = resolveRushBuyUpdatedAfter();
|
||||
LambdaQueryWrapper<LbGoods> w = new LambdaQueryWrapper<>();
|
||||
w.isNotNull(LbGoods::getSellerId);
|
||||
w.gt(LbGoods::getTotalMoney, RUSH_BUY_MIN_TOTAL_MONEY);
|
||||
w.le(LbGoods::getTotalMoney, RUSH_BUY_MAX_TOTAL_MONEY);
|
||||
w.ge(LbGoods::getUpdatedAt, updatedAfter);
|
||||
w.orderByDesc(LbGoods::getTotalMoney);
|
||||
long goodsQueryStartedAt = System.nanoTime();
|
||||
List<LbGoods> goodsList = this.list(w);
|
||||
long goodsQueryElapsedMs = (System.nanoTime() - goodsQueryStartedAt) / 1_000_000L;
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("rushBuy 查询可抢货品耗时 {} ms,tenantId={},updatedAfter={},结果数量={}",
|
||||
goodsQueryElapsedMs, tid, updatedAfter, goodsList.size());
|
||||
} else {
|
||||
log.info("{} rushBuy 查询可抢货品耗时 {} ms,tenantId={},updatedAfter={},结果数量={}",
|
||||
accountTag, goodsQueryElapsedMs, tid, updatedAfter, goodsList.size());
|
||||
}
|
||||
return goodsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 周二至周五:取当前时间往前 24 小时;周六、周日、周一:取最近一个周五 00:00:00。
|
||||
*/
|
||||
private static LocalDateTime resolveRushBuyUpdatedAfter() {
|
||||
LocalDate today = LocalDate.now();
|
||||
DayOfWeek dow = today.getDayOfWeek();
|
||||
if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY || dow == DayOfWeek.MONDAY) {
|
||||
int daysToLastFriday = switch (dow) {
|
||||
case SATURDAY -> 1;
|
||||
case SUNDAY -> 2;
|
||||
case MONDAY -> 3;
|
||||
default -> throw new IllegalStateException("unreachable");
|
||||
};
|
||||
return today.minusDays(daysToLastFriday).atStartOfDay();
|
||||
}
|
||||
return LocalDateTime.now().minusDays(1);
|
||||
}
|
||||
|
||||
private static HxrGoodsApiContext withToken(HxrGoodsApiContext ctx, String token) {
|
||||
return new HxrGoodsApiContext(
|
||||
ctx.goodsApiBaseUrl(),
|
||||
|
||||
Reference in New Issue
Block a user