缓存抢购货物

This commit is contained in:
2026-06-07 16:20:25 +08:00
parent dd0cf8d74d
commit 6e249db5a1
5 changed files with 279 additions and 17 deletions

View File

@@ -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供配置文件回退路径使用。 */