From 6e249db5a1f27215c8997b7ae8cb664bbdd34fdf Mon Sep 17 00:00:00 2001 From: cst61 Date: Sun, 7 Jun 2026 16:20:25 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=8A=A2=E8=B4=AD=E8=B4=A7?= =?UTF-8?q?=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rj/dto/LbRushBuyGoodsCoordinator.java | 35 +++++ .../com/rj/service/HxrAdminBuyService.java | 12 ++ .../com/rj/service/HxrAdminGoodsService.java | 133 +++++++++++++++++- .../service/impl/LbBuyAccountServiceImpl.java | 20 ++- .../rj/service/impl/LbGoodsServiceImpl.java | 96 +++++++++++-- 5 files changed, 279 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/rj/dto/LbRushBuyGoodsCoordinator.java b/src/main/java/com/rj/dto/LbRushBuyGoodsCoordinator.java index 96e4bc6..7d35ed5 100644 --- a/src/main/java/com/rj/dto/LbRushBuyGoodsCoordinator.java +++ b/src/main/java/com/rj/dto/LbRushBuyGoodsCoordinator.java @@ -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 claimedBy = new ConcurrentHashMap<>(); + /** 同批次内按租户缓存可抢货品列表,避免多账号重复查库。 */ + private final ConcurrentHashMap> 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 peekRushBuyGoodsList(String tenantId) { + if (tenantId == null || tenantId.trim().isEmpty()) { + return null; + } + return rushBuyGoodsByTenant.get(tenantId.trim()); + } + + /** + * 同租户在本批次内仅加载一次可抢货品列表,后续账号直接复用缓存。 + */ + public List resolveRushBuyGoodsList(String tenantId, Supplier> 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()) { diff --git a/src/main/java/com/rj/service/HxrAdminBuyService.java b/src/main/java/com/rj/service/HxrAdminBuyService.java index e94f24b..6f7a602 100644 --- a/src/main/java/com/rj/service/HxrAdminBuyService.java +++ b/src/main/java/com/rj/service/HxrAdminBuyService.java @@ -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() { diff --git a/src/main/java/com/rj/service/HxrAdminGoodsService.java b/src/main/java/com/rj/service/HxrAdminGoodsService.java index 1c4370b..52c2a2e 100644 --- a/src/main/java/com/rj/service/HxrAdminGoodsService.java +++ b/src/main/java/com/rj/service/HxrAdminGoodsService.java @@ -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 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 refreshTokenFromBuyerShopping() { + String tenantId = TenantContextHolder.getTenantId(); + if (!StringUtils.hasText(tenantId)) { + log.warn("hxr /api/order/goods token 失效,但当前线程无 tenantId,无法从 lb_buyer_shopping 模拟登录"); + return Optional.empty(); + } + + LambdaQueryWrapper 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 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,供配置文件回退路径使用。 */ diff --git a/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java b/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java index 3082f7c..bffb83d 100644 --- a/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java @@ -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; diff --git a/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java b/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java index 0b07a6d..ec82077 100644 --- a/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java @@ -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 impl String previousTenantId = TenantContextHolder.getTenantId(); TenantContextHolder.setTenantId(tid); try { - LocalDateTime updatedAfter = LocalDateTime.now().minusDays(1); - LambdaQueryWrapper 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 goodsList = this.list(w); + log.info("查询-待抢购-货物列表......"); + List 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 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 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 impl && !goodsCoordinator.tryAcquire(tid, goods.getId(), accountId)) { skippedCount++; Map 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 impl attemptCount++; Map 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 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 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 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 impl return rushBuyAccountLabel.trim(); } + private List resolveRushBuyGoodsList( + String tid, String accountTag, LbRushBuyGoodsCoordinator goodsCoordinator) { + if (goodsCoordinator == null) { + return loadRushBuyGoodsList(tid, accountTag); + } + List 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 loadRushBuyGoodsList(String tid, String accountTag) { + LocalDateTime updatedAfter = resolveRushBuyUpdatedAfter(); + LambdaQueryWrapper 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 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(),