Files
smartDriveEE/src/main/java/com/rj/service/HxrAdminGoodsService.java
2026-06-07 16:20:25 +08:00

349 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.rj.service;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
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;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
/**
* 调用 hxrd {@code /api/order/goods} 货品列表(请求头 {@code token} + {@code S/T/N} 鉴权)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
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 =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0";
private static final String HEADER_TOKEN = "Token";
private static final ObjectMapper JSON = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.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} 配置文件,兼容旧调用)。
*
* @param page 页码,从 1 开始
* @param token 可选;非空时作为请求头 token否则使用 {@code hxr.admin.goods-api-token}
*/
public Optional<HxrLbGoodsSelectResponse> fetchGoodsPage(int page, String token) throws Exception {
String resolvedToken = resolveToken(token);
if (resolvedToken == null) {
log.warn("未提供 token 且 hxr.admin 未配置 goods-api-token跳过 /api/order/goods");
return Optional.empty();
}
String appStr = properties.getGoodsApiAppStr();
if (appStr == null || appStr.isBlank()) {
log.warn("hxr.admin 未配置 goods-api-app-str跳过 /api/order/goods");
return Optional.empty();
}
HxrGoodsApiContext ctx = new HxrGoodsApiContext(
stripQuery(properties.getGoodsApiUrl()),
properties.getBuyApiUrl(),
GOODS_PAGE_SIZE,
properties.getGoodsApiOrigin(),
properties.getGoodsApiReferer(),
resolvedToken,
appStr.trim());
return fetchGoodsPage(page, ctx);
}
/**
* 分页拉取货品列表(使用租户 {@code lb_third_integration_config} 解析出的运行时配置)。
*/
public Optional<HxrLbGoodsSelectResponse> fetchGoodsPage(int page, HxrGoodsApiContext ctx) throws Exception {
if (ctx == null) {
log.warn("货品 API 配置为空,跳过 /api/order/goods");
return Optional.empty();
}
String resolvedToken = ctx.token();
if (resolvedToken == null || resolvedToken.isBlank()) {
log.warn("未提供 token跳过 /api/order/goods");
return Optional.empty();
}
String appStr = ctx.appStr();
if (appStr == null || appStr.isBlank()) {
log.warn("未配置 goodsApiAppStr跳过 /api/order/goods");
return Optional.empty();
}
String goodsApiBaseUrl = ctx.goodsApiBaseUrl();
if (goodsApiBaseUrl == null || goodsApiBaseUrl.isBlank()) {
log.warn("未配置 goodsApiBaseUrl跳过 /api/order/goods");
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)
.replaceQueryParam("page", safePage)
.replaceQueryParam("limit", pageLimit)
.build()
.encode()
.toUriString();
long timestamp = System.currentTimeMillis() / 1000;
String noncestr = randomNoncestr();
Map<String, Object> signParams = new LinkedHashMap<>();
signParams.put("page", safePage);
signParams.put("limit", pageLimit);
signParams.put("timestamp", timestamp);
signParams.put("noncestr", noncestr);
String sign = HxrGoodsSignUtil.computeSign(signParams, appStr);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(60))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.timeout(Duration.ofSeconds(120))
.header("Accept", "application/json,*/*")
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
.header("Origin", ctx.origin())
.header("Referer", ctx.referer())
.header("User-Agent", USER_AGENT)
.header(HEADER_TOKEN, resolvedToken)
.header("S", sign)
.header("T", String.valueOf(timestamp))
.header("N", noncestr)
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status < 200 || status >= 300) {
log.warn("hxr /api/order/goods HTTP {} page={} bodyPrefix={}",
status, safePage, abbreviate(response.body(), 400));
return GoodsPageFetchResult.failure(false);
}
String bodyText = response.body();
if (bodyText == null || bodyText.isBlank()) {
log.warn("hxr /api/order/goods empty body page={}", safePage);
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 GoodsPageFetchResult.failure(loginRequired);
}
JsonNode dataNode = root.get("data");
if (isErrorPayload(dataNode)) {
log.warn("hxr /api/order/goods 返回异常 data={} page={} msg={}", dataNode, safePage, msg);
return GoodsPageFetchResult.failure(false);
}
HxrLbGoodsSelectResponse body = JSON.treeToValue(root, HxrLbGoodsSelectResponse.class);
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供配置文件回退路径使用。 */
private static String stripQuery(String url) {
if (url == null || url.isBlank()) {
return url;
}
int q = url.indexOf('?');
return q >= 0 ? url.substring(0, q) : url;
}
/** 优先使用入参 token否则回退到配置 {@code goods-api-token}。 */
private String resolveToken(String token) {
if (token != null && !token.isBlank()) {
return token.trim();
}
String configured = properties.getGoodsApiToken();
if (configured != null && !configured.isBlank()) {
return configured.trim();
}
return null;
}
/** 与前端 {@code Math.random().toString(36).slice(-5)} 接近的 5 位随机串。 */
private static String randomNoncestr() {
String base36 = Long.toUnsignedString(Math.abs(RANDOM.nextLong()), 36);
if (base36.length() >= 5) {
return base36.substring(base36.length() - 5);
}
StringBuilder sb = new StringBuilder(base36);
while (sb.length() < 5) {
sb.append(Integer.toString(RANDOM.nextInt(36), 36));
}
return sb.toString();
}
private static boolean isErrorPayload(JsonNode dataNode) {
if (dataNode == null || dataNode.isNull()) {
return false;
}
if (!dataNode.isArray() || dataNode.isEmpty()) {
return false;
}
return dataNode.get(0).isTextual() || dataNode.get(0).isNumber();
}
private static String abbreviate(String s, int maxLen) {
if (s == null) {
return "";
}
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
}
}