新系统模拟抢单
This commit is contained in:
@@ -1,17 +1,41 @@
|
||||
package com.rj.controller;
|
||||
|
||||
import com.rj.dto.LbBuyAccountRushBuyContext;
|
||||
import com.rj.dto.LbBuyAccountRushBuyRequest;
|
||||
import com.rj.dto.LbRushBuyGoodsCoordinator;
|
||||
import com.rj.dto.hxr.HxrUserLoginApiContext;
|
||||
import com.rj.entity.LbBuyAccount;
|
||||
import com.rj.entity.LbThirdIntegrationConfig;
|
||||
import com.rj.entity.LbUser;
|
||||
import com.rj.service.HxrAdminUserLoginService;
|
||||
import com.rj.service.HxrAdminUserService;
|
||||
import com.rj.service.ILbBuyAccountService;
|
||||
import com.rj.service.ILbGoodsService;
|
||||
import com.rj.service.ILbThirdIntegrationConfigService;
|
||||
import com.rj.service.ILbUserService;
|
||||
import com.rj.tenant.TenantContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/lbBuyAccount")
|
||||
@Tag(name = "LB抢单账号配置", description = "lb_buy_account 增删改查与分页")
|
||||
@@ -20,6 +44,21 @@ public class LbBuyAccountController {
|
||||
@Autowired
|
||||
private ILbBuyAccountService lbBuyAccountService;
|
||||
|
||||
@Autowired
|
||||
private ILbUserService lbUserService;
|
||||
|
||||
@Autowired
|
||||
private HxrAdminUserService hxrAdminUserService;
|
||||
|
||||
@Autowired
|
||||
private HxrAdminUserLoginService hxrAdminUserLoginService;
|
||||
|
||||
@Autowired
|
||||
private ILbGoodsService lbGoodsService;
|
||||
|
||||
@Autowired
|
||||
private ILbThirdIntegrationConfigService lbThirdIntegrationConfigService;
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增")
|
||||
public ResponseEntity<Map<String, Object>> add(
|
||||
@@ -106,4 +145,285 @@ public class LbBuyAccountController {
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/simulate-rush-buy_all")
|
||||
@Operation(
|
||||
summary = "模拟指定租户所有用户抢单",
|
||||
description =
|
||||
"根据前端传入的租户 id,从 lb_user 表读取该租户所有用户,根据 sign_type 模拟登录"
|
||||
+ "(V2 用 doV2SimulateLogin POST /trade-app/api/app/login;"
|
||||
+ "V1 用 HxrAdminUserLoginService.login POST /api/user/login);"
|
||||
+ "第二步:根据登录后解析到的 token 调用 rushBuy 从第三方抢单")
|
||||
public ResponseEntity<Map<String, Object>> simulateRushBuy(
|
||||
@Parameter(description = "租户ID(必填)", required = true)
|
||||
@RequestParam String tenantId,
|
||||
@Parameter(description = "货品开始时间(可选),格式 yyyy-MM-dd HH:mm:ss")
|
||||
@RequestParam(required = false) String goodsBeginTime) {
|
||||
|
||||
log.info("模拟租户用户抢单开始 tenantId={} goodsBeginTime={}",
|
||||
tenantId, goodsBeginTime);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<Map<String, Object>> userResults = Collections.synchronizedList(new ArrayList<>());
|
||||
AtomicInteger totalUsers = new AtomicInteger(0);
|
||||
AtomicInteger loginSuccess = new AtomicInteger(0);
|
||||
AtomicInteger loginFail = new AtomicInteger(0);
|
||||
AtomicInteger rushBuySuccess = new AtomicInteger(0);
|
||||
AtomicInteger rushBuyFail = new AtomicInteger(0);
|
||||
// 方法进入时间(仅作为 catch 异常时的兜底计时起点)
|
||||
long methodEnteredAt = System.currentTimeMillis();
|
||||
// 总耗时起点:由第一个真正开始执行(模拟登录)的用户线程记录,0 表示还没有用户开始
|
||||
// 总耗时定义:第一个用户开始 → 最后一个用户抢单结束(墙钟时间,并发实际占用时长)
|
||||
AtomicLong firstUserStartAt = new AtomicLong(0L);
|
||||
|
||||
try {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "tenantId不能为空");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
final String tid = tenantId.trim();
|
||||
log.info("当前租户 tenantId={}", tid);
|
||||
|
||||
// 查询当前租户的第三方集成配置
|
||||
LbThirdIntegrationConfig config = lbThirdIntegrationConfigService.lambdaQuery()
|
||||
.eq(LbThirdIntegrationConfig::getTenantId, tid)
|
||||
.one();
|
||||
if (config == null) {
|
||||
result.put("success", false);
|
||||
result.put("message", "未找到当前租户的第三方集成配置(lb_third_integration_config)");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
if (config.getEnabled() == null || config.getEnabled() != 1) {
|
||||
result.put("success", false);
|
||||
result.put("message", "当前租户的第三方集成配置未启用");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
String signType = config.getSignType();
|
||||
String origin = config.getGoodsApiOrigin();
|
||||
boolean isV2 = "v2".equalsIgnoreCase(signType);
|
||||
log.info("当前租户配置 signType={} origin={} isV2={}", signType, origin, isV2);
|
||||
|
||||
// 查询当前租户的所有 lb_user(租户上下文已由请求拦截器设置)
|
||||
List<LbUser> users = lbUserService.list();
|
||||
if (users == null || users.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "当前租户的 lb_user 表为空");
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// 过滤出有效用户
|
||||
List<LbUser> validUsers = new ArrayList<>();
|
||||
for (LbUser user : users) {
|
||||
if (user == null || user.getId() == null) continue;
|
||||
if (user.getMobile() == null || user.getMobile().trim().isEmpty()) continue;
|
||||
if (user.getPassword() == null || user.getPassword().trim().isEmpty()) continue;
|
||||
if (user.getNickname() != null && user.getNickname().contains("**")) continue;
|
||||
validUsers.add(user);
|
||||
}
|
||||
totalUsers.set(validUsers.size());
|
||||
log.info("租户 {} 共 {} 个有效用户,开始多线程模拟登录+抢单", tenantId, validUsers.size());
|
||||
|
||||
LbRushBuyGoodsCoordinator coordinator = new LbRushBuyGoodsCoordinator();
|
||||
|
||||
// 每个用户一个线程并发抢单
|
||||
int threadCount = Math.min(validUsers.size(), 50);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
log.info("批次抢单提交 {} 个用户到线程池(线程池大小={}),等待第一个用户开始计时",
|
||||
validUsers.size(), threadCount);
|
||||
|
||||
for (LbUser user : validUsers) {
|
||||
final LbUser u = user;
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
// 第一个真正开始执行(模拟登录)的线程记录总耗时起点,后续线程不再覆盖
|
||||
long nowMs = System.currentTimeMillis();
|
||||
if (firstUserStartAt.compareAndSet(0L, nowMs)) {
|
||||
log.info("总耗时起点:第一个用户[{}]开始模拟登录,时间戳={}", u.getMobile(), nowMs);
|
||||
}
|
||||
String userLabel = "用户[" + u.getMobile() + "]";
|
||||
Map<String, Object> userResult = new LinkedHashMap<>();
|
||||
userResult.put("userId", u.getId());
|
||||
userResult.put("mobile", u.getMobile());
|
||||
userResult.put("tenantId", tid);
|
||||
long userStartedAt = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// ====== Step 1: 模拟登录 ======
|
||||
String token = null;
|
||||
long loginStartedAt = System.currentTimeMillis();
|
||||
|
||||
if (isV2) {
|
||||
log.info("V2 模拟登录 userId={} mobile={} origin={}",
|
||||
u.getId(), u.getMobile(), origin);
|
||||
Optional<String> tok = hxrAdminUserService.doV2SimulateLogin(
|
||||
u.getMobile().trim(),
|
||||
u.getPassword().trim(),
|
||||
origin);
|
||||
if (tok.isPresent()) {
|
||||
token = tok.get();
|
||||
loginSuccess.incrementAndGet();
|
||||
log.info("V2 模拟登录成功 userId={} mobile={} tokenPrefix={}",
|
||||
u.getId(), u.getMobile(),
|
||||
token.length() > 8 ? token.substring(0, 8) : token);
|
||||
} else {
|
||||
loginFail.incrementAndGet();
|
||||
userResult.put("success", false);
|
||||
userResult.put("message", "V2模拟登录失败");
|
||||
userResults.add(userResult);
|
||||
log.warn("V2 模拟登录失败 userId={} mobile={}", u.getId(), u.getMobile());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// V1 登录
|
||||
Optional<HxrUserLoginApiContext> loginCtxOpt =
|
||||
lbThirdIntegrationConfigService.resolveUserLoginApiContext(tid);
|
||||
if (loginCtxOpt.isEmpty()) {
|
||||
loginFail.incrementAndGet();
|
||||
userResult.put("success", false);
|
||||
userResult.put("message", "V1登录配置解析失败");
|
||||
userResults.add(userResult);
|
||||
log.warn("V1 登录配置解析失败 tid={}", tid);
|
||||
return;
|
||||
}
|
||||
log.info("V1 模拟登录 userId={} mobile={}", u.getId(), u.getMobile());
|
||||
HxrAdminUserLoginService.LoginApiResult loginResult =
|
||||
hxrAdminUserLoginService.login(
|
||||
u.getMobile().trim(),
|
||||
u.getPassword().trim(),
|
||||
loginCtxOpt.get());
|
||||
if (loginResult.success()) {
|
||||
token = HxrAdminUserLoginService.extractToken(loginResult.parsed());
|
||||
if (token != null && !token.trim().isEmpty()) {
|
||||
token = token.trim();
|
||||
loginSuccess.incrementAndGet();
|
||||
log.info("V1 模拟登录成功 userId={} mobile={} tokenPrefix={}",
|
||||
u.getId(), u.getMobile(),
|
||||
token.length() > 8 ? token.substring(0, 8) : token);
|
||||
} else {
|
||||
loginFail.incrementAndGet();
|
||||
userResult.put("success", false);
|
||||
userResult.put("message", "V1登录成功但响应无token");
|
||||
userResults.add(userResult);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
loginFail.incrementAndGet();
|
||||
userResult.put("success", false);
|
||||
userResult.put("message", "V1登录失败: code=" + loginResult.apiCode() + " msg=" + loginResult.apiMsg());
|
||||
userResults.add(userResult);
|
||||
log.warn("V1 登录失败 userId={} mobile={} code={} msg={}",
|
||||
u.getId(), u.getMobile(), loginResult.apiCode(), loginResult.apiMsg());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
long loginElapsedMs = System.currentTimeMillis() - loginStartedAt;
|
||||
userResult.put("loginElapsedMs", loginElapsedMs);
|
||||
|
||||
// ====== Step 2: 用 token 抢单,maxBuyCount 从 lb_user.max_order 读取 ======
|
||||
// V2 场景下,每人的最大抢单量扩大 3 倍;V1 保持原值
|
||||
int baseMaxOrder = (u.getMaxOrder() != null && u.getMaxOrder() > 0)
|
||||
? u.getMaxOrder() : 1;
|
||||
int maxOrder = isV2 ? baseMaxOrder * 3 : baseMaxOrder;
|
||||
|
||||
LbBuyAccountRushBuyContext refreshCtx = new LbBuyAccountRushBuyContext(
|
||||
String.valueOf(u.getId()),
|
||||
u.getMobile().trim(),
|
||||
u.getPassword().trim());
|
||||
|
||||
log.info("开始抢单 userId={} mobile={} tid={} maxOrder={}{} 登录耗时={} ms",
|
||||
u.getId(), u.getMobile(), tid, maxOrder,
|
||||
isV2 ? "(V2 已扩大3倍,原始 max_order=" + baseMaxOrder + ")" : "",
|
||||
loginElapsedMs);
|
||||
|
||||
long rushStartedAt = System.currentTimeMillis();
|
||||
Map<String, Object> rushBuyResult = lbGoodsService.rushBuy(
|
||||
tid,
|
||||
token,
|
||||
maxOrder,
|
||||
userLabel,
|
||||
refreshCtx,
|
||||
coordinator,
|
||||
String.valueOf(u.getId()),
|
||||
null,
|
||||
goodsBeginTime);
|
||||
long rushElapsedMs = System.currentTimeMillis() - rushStartedAt;
|
||||
|
||||
Boolean rushSuccess = rushBuyResult.get("success") instanceof Boolean
|
||||
? (Boolean) rushBuyResult.get("success") : null;
|
||||
if (Boolean.TRUE.equals(rushSuccess)) {
|
||||
rushBuySuccess.incrementAndGet();
|
||||
} else {
|
||||
rushBuyFail.incrementAndGet();
|
||||
}
|
||||
long userElapsedMs = System.currentTimeMillis() - userStartedAt;
|
||||
userResult.put("success", rushSuccess);
|
||||
userResult.put("rushBuyResult", rushBuyResult);
|
||||
userResult.put("rushElapsedMs", rushElapsedMs);
|
||||
userResult.put("totalElapsedMs", userElapsedMs);
|
||||
userResults.add(userResult);
|
||||
log.info("{} 抢单结束 success={} 抢单耗时={} ms,用户总耗时={} ms ({} s)",
|
||||
userLabel, rushSuccess, rushElapsedMs, userElapsedMs,
|
||||
String.format("%.2f", userElapsedMs / 1000.0));
|
||||
|
||||
} catch (Exception e) {
|
||||
loginFail.incrementAndGet();
|
||||
long userElapsedMs = System.currentTimeMillis() - userStartedAt;
|
||||
userResult.put("success", false);
|
||||
userResult.put("message", "异常: " + e.getMessage());
|
||||
userResult.put("totalElapsedMs", userElapsedMs);
|
||||
userResults.add(userResult);
|
||||
log.error("模拟抢单异常 userId={} mobile={} 耗时={} ms",
|
||||
u.getId(), u.getMobile(), userElapsedMs, e);
|
||||
}
|
||||
}, executor);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
// 等待所有线程完成:join 返回的这一刻,就是最后一个用户抢单结束的时间点
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
executor.shutdown();
|
||||
long batchEndAt = System.currentTimeMillis();
|
||||
log.info("总耗时终点:最后一个用户抢单结束,时间戳={}", batchEndAt);
|
||||
// 总耗时 = 最后一个用户结束时间 - 第一个用户开始时间(墙钟时间)
|
||||
long batchStartRef = firstUserStartAt.get() > 0 ? firstUserStartAt.get() : methodEnteredAt;
|
||||
long batchElapsedMs = batchEndAt - batchStartRef;
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", "模拟抢单完成,总用户=" + totalUsers.get()
|
||||
+ " 登录成功=" + loginSuccess.get() + " 登录失败=" + loginFail.get()
|
||||
+ " 抢单成功=" + rushBuySuccess.get() + " 抢单失败=" + rushBuyFail.get()
|
||||
+ " 总耗时=" + batchElapsedMs + " ms (" + String.format("%.2f", batchElapsedMs / 1000.0) + " s)");
|
||||
result.put("totalUsers", totalUsers.get());
|
||||
result.put("loginSuccess", loginSuccess.get());
|
||||
result.put("loginFail", loginFail.get());
|
||||
result.put("rushBuySuccess", rushBuySuccess.get());
|
||||
result.put("rushBuyFail", rushBuyFail.get());
|
||||
result.put("elapsedMs", batchElapsedMs);
|
||||
result.put("userResults", userResults);
|
||||
log.info("模拟当前租户用户抢单完成 tid={} totalUsers={} loginSuccess={} loginFail={} rushBuySuccess={} rushBuyFail={} 总耗时={} ms ({} s)(第一个用户开始→最后一个用户结束)",
|
||||
tid, totalUsers.get(), loginSuccess.get(), loginFail.get(),
|
||||
rushBuySuccess.get(), rushBuyFail.get(), batchElapsedMs,
|
||||
String.format("%.2f", batchElapsedMs / 1000.0));
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
long nowMs = System.currentTimeMillis();
|
||||
long batchStartRef = firstUserStartAt.get() > 0 ? firstUserStartAt.get() : methodEnteredAt;
|
||||
long batchElapsedMs = nowMs - batchStartRef;
|
||||
log.error("模拟当前租户用户抢单异常,总耗时={} ms(第一个用户开始→异常发生)", batchElapsedMs, e);
|
||||
result.put("success", false);
|
||||
result.put("message", "异常: " + e.getMessage());
|
||||
result.put("elapsedMs", batchElapsedMs);
|
||||
result.put("totalUsers", totalUsers.get());
|
||||
result.put("loginSuccess", loginSuccess.get());
|
||||
result.put("loginFail", loginFail.get());
|
||||
result.put("rushBuySuccess", rushBuySuccess.get());
|
||||
result.put("rushBuyFail", rushBuyFail.get());
|
||||
result.put("userResults", userResults);
|
||||
return ResponseEntity.internalServerError().body(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.rj.config.HxrAdminProperties;
|
||||
import com.rj.dto.hxr.HxrGoodsApiContext;
|
||||
import com.rj.util.HxrGoodsSignUtil;
|
||||
import com.rj.util.HxrdSignUtil;
|
||||
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.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
@@ -19,6 +24,7 @@ import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* 调用 hxrd {@code POST /api/order/buy} 抢购(请求头 {@code token} + {@code S/T/N} 鉴权)。
|
||||
@@ -52,6 +58,11 @@ public class HxrAdminBuyService {
|
||||
/** 外部系统返回此文案时表示该货品已被他人抢订,同批次其他账号不应再尝试。 */
|
||||
public static final String MSG_ORDER_ALREADY_GRABBED = "此订单已被抢订";
|
||||
|
||||
/** V2 使用的 User-Agent(与 HxrAdminGoodsService V2 请求一致)。 */
|
||||
private static final String V2_USER_AGENT =
|
||||
"Mozilla/5.0 (Linux; Android 13; PEYM00 Build/TP1A.220905.001) AppleWebKit/537.36 "
|
||||
+ "(KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36";
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper()
|
||||
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
|
||||
|
||||
@@ -195,25 +206,274 @@ public class HxrAdminBuyService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// V2 抢购:POST {origin}/trade-app/api/app/sale/flash-sale?goodsId=&buyerId=
|
||||
// 鉴权:Authorization: Bearer {jwt} + Cookie: ELADMIN-TOKEN + X-Sign-N/T/S
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* V2 抢购(flash-sale)。
|
||||
*
|
||||
* @param goodsId 货品 ID
|
||||
* @param buyerId 买家 ID(即 lb_user.id)
|
||||
* @param ctx 运行时配置(含 origin/token/signType 等)
|
||||
* @return 抢购结果
|
||||
*/
|
||||
public BuyApiResult buyV2(long goodsId, long buyerId, HxrGoodsApiContext ctx) throws Exception {
|
||||
if (ctx == null) {
|
||||
return BuyApiResult.failure(-1, "抢购 API 配置为空");
|
||||
}
|
||||
String resolvedToken = ctx.token();
|
||||
if (resolvedToken == null || resolvedToken.isBlank()) {
|
||||
return BuyApiResult.failure(-1, "未提供 token");
|
||||
}
|
||||
String baseDomain = ctx.origin();
|
||||
if (baseDomain == null || baseDomain.isBlank()) {
|
||||
return BuyApiResult.failure(-1, "未配置 origin");
|
||||
}
|
||||
|
||||
// 1) 拼 URL
|
||||
String trimmedDomain = baseDomain.trim();
|
||||
while (trimmedDomain.endsWith("/")) {
|
||||
trimmedDomain = trimmedDomain.substring(0, trimmedDomain.length() - 1);
|
||||
}
|
||||
String uri = UriComponentsBuilder.fromUriString(trimmedDomain)
|
||||
.replacePath("/trade-app/api/app/sale/flash-sale")
|
||||
.replaceQueryParam("goodsId", goodsId)
|
||||
.replaceQueryParam("buyerId", buyerId)
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
// 2) 签名三 Header
|
||||
HxrdSignUtil.SignTriplet trip = HxrdSignUtil.sign();
|
||||
|
||||
// 3) Cookie(ELADMIN-TOKEN 空格编码为 %20)
|
||||
String cookieHeader = "sidebarStatus=0; ELADMIN-TOKEN="
|
||||
+ encodeCookieValue("Bearer " + resolvedToken.trim());
|
||||
|
||||
log.info("v2 flash-sale 请求 goodsId={} buyerId={} uri={} tokenPrefix={} cookiePrefix={} n={} t={}",
|
||||
goodsId, buyerId, uri, abbreviate(resolvedToken, 8),
|
||||
abbreviate(cookieHeader, 40), trip.nHeader(), trip.tHeader());
|
||||
|
||||
// 4) 发请求
|
||||
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, text/plain, */*")
|
||||
.header("Accept-Encoding", "gzip, deflate")
|
||||
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
.header("Origin", trimmedDomain)
|
||||
.header("Referer", trimmedDomain + "/")
|
||||
.header("User-Agent", V2_USER_AGENT)
|
||||
.header("Sec-Ch-Ua", "\"Chromium\";v=\"152\", \"Not?A_Brand\";v=\"24\", \"Microsoft Edge\";v=\"152\"")
|
||||
.header("Sec-Ch-Ua-Mobile", "?1")
|
||||
.header("Sec-Ch-Ua-Platform", "\"Android\"")
|
||||
.header("Sec-Fetch-Dest", "empty")
|
||||
.header("Sec-Fetch-Mode", "cors")
|
||||
.header("Sec-Fetch-Site", "same-origin")
|
||||
.header("Authorization", "Bearer " + resolvedToken.trim())
|
||||
.header("Cookie", cookieHeader)
|
||||
.header("X-Sign-N", trip.nHeader())
|
||||
.header("X-Sign-T", trip.tHeader())
|
||||
.header("X-Sign-S", trip.sHeader())
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
|
||||
HttpResponse<byte[]> resp = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||
int status = resp.statusCode();
|
||||
byte[] bodyBytes = resp.body();
|
||||
String contentEncoding = resp.headers().firstValue("Content-Encoding").orElse("");
|
||||
byte[] decompressed = decompressGzipIfNeeded(bodyBytes, contentEncoding);
|
||||
String bodyText = decompressed == null ? "" : new String(decompressed, StandardCharsets.UTF_8);
|
||||
|
||||
if (bodyText == null || bodyText.isBlank()) {
|
||||
if (status < 200 || status >= 300) {
|
||||
log.warn("v2 flash-sale HTTP {} goodsId={} buyerId={} 空响应", status, goodsId, buyerId);
|
||||
return BuyApiResult.failure(status, "HTTP " + status);
|
||||
}
|
||||
log.warn("v2 flash-sale empty body goodsId={} buyerId={}", goodsId, buyerId);
|
||||
return BuyApiResult.failure(-1, "响应为空");
|
||||
}
|
||||
|
||||
// 5) 解析响应(HTTP 400 也可能带业务 JSON,如 {"success":false,"message":"商品已售出"})
|
||||
// 注意:V2 消息字段名是 message(不是 msg),状态字段可能是 code 或 status;
|
||||
// 服务端返回的中文是 backslash-uXXXX 形式的 ASCII 转义,Jackson 解析后自动转成汉字。
|
||||
JsonNode root = null;
|
||||
try {
|
||||
root = JSON.readTree(bodyText);
|
||||
} catch (Exception parseEx) {
|
||||
log.warn("v2 flash-sale 响应非JSON HTTP={} goodsId={} buyerId={} bodyPrefix={}",
|
||||
status, goodsId, buyerId, abbreviate(unescapeUnicode(bodyText), 400));
|
||||
return BuyApiResult.failure(status, "HTTP " + status);
|
||||
}
|
||||
|
||||
boolean ok = false;
|
||||
int code = -1;
|
||||
// 消息字段:优先 message,兜底 msg
|
||||
JsonNode msgNode = root.get("message");
|
||||
if (msgNode == null || msgNode.isNull()) {
|
||||
msgNode = root.get("msg");
|
||||
}
|
||||
String msg = msgNode != null && !msgNode.isNull() ? msgNode.asText("") : "";
|
||||
|
||||
// 状态码字段:优先 code,兜底 status(HTTP 状态码)
|
||||
JsonNode codeNode = root.get("code");
|
||||
if (codeNode != null && !codeNode.isNull() && codeNode.isNumber()) {
|
||||
code = codeNode.asInt(-1);
|
||||
} else {
|
||||
JsonNode statusNode = root.get("status");
|
||||
if (statusNode != null && !statusNode.isNull() && statusNode.isNumber()) {
|
||||
code = statusNode.asInt(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// 优先看 success 字段
|
||||
JsonNode successNode = root.get("success");
|
||||
if (successNode != null && !successNode.isNull()) {
|
||||
if (successNode.isBoolean()) {
|
||||
ok = successNode.booleanValue();
|
||||
} else if (successNode.isNumber()) {
|
||||
ok = successNode.intValue() == 1;
|
||||
} else {
|
||||
String t = successNode.asText("");
|
||||
ok = "true".equalsIgnoreCase(t) || "1".equals(t);
|
||||
}
|
||||
}
|
||||
// 再看 code 字段
|
||||
if (code != -1 && !ok) {
|
||||
ok = (code == 0 || code == 200);
|
||||
}
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
// HTTP 层失败,但 body 里可能有业务消息(如 400 + "商品已售出"),带上解码后的中文消息
|
||||
log.warn("v2 flash-sale HTTP {} goodsId={} buyerId={} message=[{}] bodyPrefix={}",
|
||||
status, goodsId, buyerId, msg, abbreviate(unescapeUnicode(bodyText), 400));
|
||||
String resultMsg = msg.isEmpty() ? ("HTTP " + status) : msg;
|
||||
return new BuyApiResult(false, code == -1 ? status : code, resultMsg);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
log.warn("v2 flash-sale 失败 code={} message=[{}] goodsId={} buyerId={} bodyPrefix={}",
|
||||
code, msg, goodsId, buyerId, abbreviate(unescapeUnicode(bodyText), 400));
|
||||
if (msg.contains(MSG_ACTIVITY_NOT_STARTED)) {
|
||||
log.warn("v2 flash-sale 活动未开始 goodsId={} buyerId={}", goodsId, buyerId);
|
||||
}
|
||||
if (msg.contains(MSG_ACTIVITY_ENDED)) {
|
||||
log.warn("v2 flash-sale 活动已结束 goodsId={} buyerId={}", goodsId, buyerId);
|
||||
}
|
||||
if (msg.contains(MSG_DAILY_LIMIT_EXCEEDED)) {
|
||||
log.warn("v2 flash-sale 当日可抢订单数已满 goodsId={} buyerId={}", goodsId, buyerId);
|
||||
}
|
||||
if (code == 401 || msg.contains(MSG_LOGIN_REQUIRED)) {
|
||||
log.warn("v2 flash-sale token 失效 goodsId={} buyerId={}", goodsId, buyerId);
|
||||
}
|
||||
} else {
|
||||
log.info("v2 flash-sale 成功 goodsId={} buyerId={}", goodsId, buyerId);
|
||||
}
|
||||
return new BuyApiResult(ok, code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 原始文本中的 backslash-uXXXX 转义解码为汉字(仅用于日志展示,不影响业务解析)。
|
||||
* 服务端返回 {"message":"转义形式的商品已售出"},日志里直接打印会显示 unicode 编码,
|
||||
* 解码后显示为 {"message":"商品已售出"}。
|
||||
*/
|
||||
private static String unescapeUnicode(String s) {
|
||||
if (s == null || s.indexOf('\\') < 0) {
|
||||
return s;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(s.length());
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\\' && i + 5 < s.length() && s.charAt(i + 1) == 'u') {
|
||||
try {
|
||||
int hex = Integer.parseInt(s.substring(i + 2, i + 6), 16);
|
||||
sb.append((char) hex);
|
||||
i += 5;
|
||||
continue;
|
||||
} catch (NumberFormatException ignore) {
|
||||
// 不是合法的 4 位十六进制转义,按原样输出
|
||||
}
|
||||
}
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** gzip 解压(V2 响应可能是 gzip 压缩的)。 */
|
||||
private static byte[] decompressGzipIfNeeded(byte[] body, String contentEncoding) {
|
||||
if (body == null || body.length == 0) {
|
||||
return body;
|
||||
}
|
||||
if (contentEncoding == null || !contentEncoding.toLowerCase().contains("gzip")) {
|
||||
return body;
|
||||
}
|
||||
try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(body))) {
|
||||
return gis.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
log.warn("gzip 解压失败,返回原始 body", e);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie 值 percent-encode:空格编码为 %20(不是 URLEncoder 的 '+')。
|
||||
* 与 HxrAdminUserService.encodeCookieValue 一致,保证 Cookie 行为与浏览器相同。
|
||||
*/
|
||||
private static String encodeCookieValue(String value) {
|
||||
if (value == null) return "";
|
||||
StringBuilder sb = new StringBuilder(value.length() + 16);
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
|| (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
sb.append(c);
|
||||
} else if (c == ' ') {
|
||||
sb.append("%20");
|
||||
} else {
|
||||
for (byte b : String.valueOf(c).getBytes(StandardCharsets.UTF_8)) {
|
||||
sb.append(String.format("%%%02X", b));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public record BuyApiResult(boolean success, int apiCode, String apiMsg) {
|
||||
|
||||
public static BuyApiResult failure(int code, String msg) {
|
||||
return new BuyApiResult(false, code, msg);
|
||||
}
|
||||
|
||||
/** {@code code=1} 且 msg 为「已超出当天可抢订单数」时,调用方应停止该账号后续抢购。 */
|
||||
/** msg 包含「已超出当天可抢订单数」时,调用方应停止该账号后续抢购。 */
|
||||
public boolean dailyLimitExceeded() {
|
||||
return apiCode == 1 && MSG_DAILY_LIMIT_EXCEEDED.equals(apiMsg);
|
||||
return apiMsg != null && apiMsg.contains(MSG_DAILY_LIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
/** {@code code=401} 或 msg 为「请登录」时,调用方应停止该账号后续抢购。 */
|
||||
/** {@code code=401} 或 msg 包含「请登录」时,调用方应停止该账号后续抢购。 */
|
||||
public boolean loginRequired() {
|
||||
return apiCode == 401 || MSG_LOGIN_REQUIRED.equals(apiMsg);
|
||||
return apiCode == 401 || (apiMsg != null && apiMsg.contains(MSG_LOGIN_REQUIRED));
|
||||
}
|
||||
|
||||
/** msg 包含「此订单已被抢订」时,同租户该货品在批量抢单中应标记为不可再抢。 */
|
||||
/**
|
||||
* msg 表示货品已被抢订/售出时,同租户该货品在批量抢单中应标记为不可再抢。
|
||||
* 兼容 V1「此订单已被抢订」与 V2「商品已售出」「已抢光」「已售罄」等文案。
|
||||
*/
|
||||
public boolean orderAlreadyGrabbed() {
|
||||
return apiMsg != null && apiMsg.contains(MSG_ORDER_ALREADY_GRABBED);
|
||||
if (apiMsg == null) {
|
||||
return false;
|
||||
}
|
||||
return apiMsg.contains(MSG_ORDER_ALREADY_GRABBED)
|
||||
|| apiMsg.contains("已售出")
|
||||
|| apiMsg.contains("已抢光")
|
||||
|| apiMsg.contains("已售罄")
|
||||
|| apiMsg.contains("已被抢");
|
||||
}
|
||||
|
||||
/** msg 包含「活动未开始」时,调用方应立刻停止该账号及本批次后续抢购。 */
|
||||
|
||||
@@ -13,10 +13,10 @@ import com.rj.dto.hxr.HxrGoodsApiContext;
|
||||
import com.rj.dto.hxr.HxrLbGoodsPageData;
|
||||
import com.rj.dto.hxr.HxrLbGoodsSelectResponse;
|
||||
import com.rj.dto.hxr.HxrUserLoginApiContext;
|
||||
import com.rj.entity.LbBuyerShopping;
|
||||
import com.rj.entity.LbGoods;
|
||||
import com.rj.entity.LbThirdIntegrationConfig;
|
||||
import com.rj.mapper.LbBuyerShoppingMapper;
|
||||
import com.rj.entity.LbUser;
|
||||
import com.rj.mapper.LbUserMapper;
|
||||
import com.rj.tenant.TenantContextHolder;
|
||||
import com.rj.util.HxrGoodsSignUtil;
|
||||
import com.rj.util.HxrdSignUtil;
|
||||
@@ -95,7 +95,7 @@ public class HxrAdminGoodsService {
|
||||
private final HxrAdminProperties properties;
|
||||
private final HxrAdminUserLoginService hxrAdminUserLoginService;
|
||||
private final ILbThirdIntegrationConfigService lbThirdIntegrationConfigService;
|
||||
private final LbBuyerShoppingMapper lbBuyerShoppingMapper;
|
||||
private final LbUserMapper lbUserMapper;
|
||||
|
||||
/**
|
||||
* 分页拉取货品列表(使用 {@code hxr.admin} 配置文件,兼容旧调用)。
|
||||
@@ -178,7 +178,7 @@ public class HxrAdminGoodsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* token 失效时:从 {@code lb_buyer_shopping} 取当前租户多条记录的买家手机号,
|
||||
* token 失效时:从 {@code lb_user} 取当前租户多条记录的买家手机号,
|
||||
* 逐个尝试模拟登录,解析 token。
|
||||
* - sign_type == v2: 直接 POST /trade-app/api/app/login JSON {phone,password} 取 Bearer JWT(去掉前缀)
|
||||
* - sign_type == v1 / null:沿用旧 HxrAdminUserLoginService 登录
|
||||
@@ -186,27 +186,27 @@ public class HxrAdminGoodsService {
|
||||
private Optional<String> refreshTokenFromBuyerShopping(HxrGoodsApiContext ctx) {
|
||||
String tenantId = TenantContextHolder.getTenantId();
|
||||
if (!StringUtils.hasText(tenantId)) {
|
||||
log.warn("hxr /api/order/goods token 失效,但当前线程无 tenantId,无法从 lb_buyer_shopping 模拟登录");
|
||||
log.warn("hxr /api/order/goods token 失效,但当前线程无 tenantId,无法从 lb_user 模拟登录");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LbBuyerShopping> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(LbBuyerShopping::getTenantId, tenantId.trim())
|
||||
.isNotNull(LbBuyerShopping::getBuyerMobile)
|
||||
.ne(LbBuyerShopping::getBuyerMobile, "")
|
||||
.orderByDesc(LbBuyerShopping::getBuyTime)
|
||||
LambdaQueryWrapper<LbUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(LbUser::getTenantId, tenantId.trim())
|
||||
// 昵称为 NULL 不排除;昵称非 NULL 时不能包含 **
|
||||
.and(w -> w.isNull(LbUser::getNickname).or().notLike(LbUser::getNickname, "**"))
|
||||
.orderByDesc(LbUser::getJoinTime)
|
||||
.last("LIMIT 20");
|
||||
List<LbBuyerShopping> samples = lbBuyerShoppingMapper.selectList(queryWrapper);
|
||||
List<LbUser> samples = lbUserMapper.selectList(queryWrapper);
|
||||
if (samples == null || samples.isEmpty()) {
|
||||
log.warn("hxr /api/order/goods token 失效,tenantId={} 在 lb_buyer_shopping 中未找到可用买家手机号",
|
||||
log.warn("hxr /api/order/goods token 失效,tenantId={} 在 lb_user 中未找到可用买家手机号",
|
||||
tenantId);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Set<String> seenMobiles = new LinkedHashSet<>();
|
||||
List<String> candidateMobiles = new ArrayList<>();
|
||||
for (LbBuyerShopping sample : samples) {
|
||||
String mobile = sample.getBuyerMobile();
|
||||
for (LbUser sample : samples) {
|
||||
String mobile = sample.getMobile();
|
||||
if (!StringUtils.hasText(mobile)) {
|
||||
continue;
|
||||
}
|
||||
@@ -216,7 +216,7 @@ public class HxrAdminGoodsService {
|
||||
}
|
||||
}
|
||||
if (candidateMobiles.isEmpty()) {
|
||||
log.warn("hxr /api/order/goods token 失效,tenantId={} 在 lb_buyer_shopping 中候选手机号去重后为空",
|
||||
log.warn("hxr /api/order/goods token 失效,tenantId={} 在 lb_user 中候选手机号去重后为空",
|
||||
tenantId);
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -278,7 +278,7 @@ public class HxrAdminGoodsService {
|
||||
}
|
||||
|
||||
boolean persisted = persistGoodsApiToken(tenantId.trim(), trimmedToken);
|
||||
log.info("hxr goods 已通过 lb_buyer_shopping 买家手机号模拟登录获取新 token signType={} tenantId={} buyerMobile={} 序号={}/{} tokenPrefix={} persisted={}",
|
||||
log.info("hxr goods 已通过 lb_user 买家手机号模拟登录获取新 token signType={} tenantId={} buyerMobile={} 序号={}/{} tokenPrefix={} persisted={}",
|
||||
v2 ? "v2" : "v1", tenantId, buyerMobile, i + 1, candidateMobiles.size(), abbreviate(trimmedToken, 8), persisted);
|
||||
return Optional.of(trimmedToken);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -248,13 +248,11 @@ public class HxrAdminUserService {
|
||||
/**
|
||||
* ===== V2 分支:sign_type == v2(功能:「获取用户」列表/分页查询) =====
|
||||
*
|
||||
* <p>按功能判断是否模拟登录(不看域名):
|
||||
* <p><b>本功能不做模拟登录,永远使用数据库 {@code lb_third_integration_config} 中配置的 token</b>
|
||||
* ({@code ctx.token()},即 goods_api_token):
|
||||
* <ul>
|
||||
* <li>本功能是「获取用户」(fetchUserSelectPage = 列表/分页拉取)+ V2版本
|
||||
* → <b>需要按 v2 逻辑模拟登录</b>:
|
||||
* 配置 token 为空 → 先 root/123456 调 /trade-app/api/app/login 拿 JWT;
|
||||
* 首次请求返回 401/403/500 → 刷新 token 后再重试一次。</li>
|
||||
* <li>「获取用户信息(单用户详情)」属于另一功能,另有独立实现,V2 下不做模拟登录(不在本方法内)。</li>
|
||||
* <li>数据库 token 为空 → 直接跳过(返回 empty),不会调用 /trade-app/api/app/login;</li>
|
||||
* <li>请求返回 401/403 等鉴权失败 → 不自动重登、不重试,仅记录日志并返回 empty,需人工在数据库更新 token;</li>
|
||||
* <li>V1 分支完全不变(fetchUserSelectPageV1 不受影响)。</li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -290,20 +288,15 @@ public class HxrAdminUserService {
|
||||
pureToken == null ? "(null)" : abbreviate(pureToken, 24),
|
||||
pureToken == null ? "(null)" : (pureToken.length() > 24 ? pureToken.substring(pureToken.length() - 24) : pureToken));
|
||||
|
||||
// ======== 按功能模拟登录(本功能=获取用户列表+V2 → 要模拟登录) ========
|
||||
boolean simulatedLoginDone = false;
|
||||
// ======== 本功能不模拟登录:永远使用数据库 lb_third_integration_config 配置的 token(ctx.token()) ========
|
||||
// token 缺失直接跳过,不调用 /trade-app/api/app/login;鉴权失败也不自动重登,需人工更新数据库 token。
|
||||
if (!bearerPresent) {
|
||||
log.info("v2 fetchUserSelectPageV2 token 为空,开始模拟登录 user={} domain={}", V2_LOGIN_USERNAME, trimmedDomain);
|
||||
Optional<String> tok = doV2SimulateLogin(V2_LOGIN_USERNAME, V2_LOGIN_PASSWORD, trimmedDomain);
|
||||
if (tok.isPresent()) {
|
||||
pureToken = tok.get();
|
||||
bearerPresent = true;
|
||||
simulatedLoginDone = true;
|
||||
log.info("v2 fetchUserSelectPageV2 模拟登录 OK token(len={}) prefix={}", pureToken.length(), abbreviate(pureToken, 24));
|
||||
} else {
|
||||
log.warn("[EMPTY_REASON:SIM_LOGIN_FAIL_NO_TOKEN] v2 fetchUserSelectPageV2 模拟登录失败(无初始token且登录拿不到) user={}", V2_LOGIN_USERNAME);
|
||||
}
|
||||
log.warn("[EMPTY_REASON:DB_TOKEN_EMPTY] v2 fetchUserSelectPageV2 数据库 token 为空,不模拟登录,直接跳过 domain={}",
|
||||
trimmedDomain);
|
||||
return Optional.empty();
|
||||
}
|
||||
log.info("v2 fetchUserSelectPageV2 使用数据库 token(不模拟登录) domain={} token(len={}) prefix={}",
|
||||
trimmedDomain, pureToken.length(), abbreviate(pureToken, 24));
|
||||
|
||||
// ==================== Bug Fix 2:Cookie 空格编码用 %20 而不是 URLEncoder 的 + ====================
|
||||
// Java URLEncoder.encode 会把空格变成 '+'(表单 application/x-www-form-urlencoded 规范),
|
||||
@@ -613,7 +606,8 @@ public class HxrAdminUserService {
|
||||
if (m == null) m = textOr(userNode.get("bankPhone"), null);
|
||||
u.setMobile(m);
|
||||
}
|
||||
if (u.getPassword() == null) u.setPassword(textOr(userNode.get("password"), null));
|
||||
// 密码统一为 123456(第三方返回的是 BCrypt 哈希,后续模拟登录需要明文密码)
|
||||
u.setPassword("123456");
|
||||
if (u.getSex() == null) u.setSex(textOr(userNode.get("sex"), null));
|
||||
if (u.getAvatar() == null) u.setAvatar(textOr(userNode.get("avatar"), null));
|
||||
if (u.getInvite() == null) u.setInvite(textOr(userNode.get("invite"), textOr(userNode.get("inviteCode"), null)));
|
||||
@@ -1141,11 +1135,6 @@ public class HxrAdminUserService {
|
||||
return form.toString();
|
||||
}
|
||||
|
||||
/** v2 模拟登录默认用户名(仅移动端 hxrdm 域名无 token/401 时使用)。 */
|
||||
private static final String V2_LOGIN_USERNAME = "root";
|
||||
/** v2 模拟登录默认密码。 */
|
||||
private static final String V2_LOGIN_PASSWORD = "123456";
|
||||
|
||||
/**
|
||||
* v2 模拟登录:POST JSON {phone,password} → /trade-app/api/app/login。
|
||||
* 成功返回格式:{ success:true, token:"Bearer eyJ..." };去掉 "Bearer " 前缀后返回。
|
||||
@@ -1154,7 +1143,7 @@ public class HxrAdminUserService {
|
||||
* @param password 登录密码
|
||||
* @param baseDomain 站点 Origin;为空无法请求,返回 empty
|
||||
*/
|
||||
private Optional<String> doV2SimulateLogin(String phone, String password, String baseDomain) throws Exception {
|
||||
public Optional<String> doV2SimulateLogin(String phone, String password, String baseDomain) throws Exception {
|
||||
if (!StringUtils.hasText(phone) || !StringUtils.hasText(password)) {
|
||||
log.warn("[SIM_LOGIN:EMPTY_CREDS] v2 模拟登录凭据为空 phone={} pwdEmpty={}", phone, !StringUtils.hasText(password));
|
||||
return Optional.empty();
|
||||
|
||||
@@ -533,6 +533,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
String goodsBeginTime) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
String accountTag = formatRushBuyAccountLogTag(rushBuyAccountLabel);
|
||||
long rushBuyStartedAt = System.currentTimeMillis();
|
||||
try {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
@@ -593,6 +594,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
boolean tokenRefreshAttempted = false;
|
||||
boolean tokenRefreshed = false;
|
||||
boolean restartAfterTokenRefresh;
|
||||
boolean isV2 = "v2".equalsIgnoreCase(apiContext.signType());
|
||||
|
||||
do {
|
||||
restartAfterTokenRefresh = false;
|
||||
@@ -617,7 +619,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
if (goods.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
if(goods.getPrice().intValue() >maxGrabAmount){
|
||||
if (maxGrabAmount != null && goods.getPrice().intValue() > maxGrabAmount) {
|
||||
log.info("跳过货品 {},因为超过最大抢单金额:{}", goods.getId(), goods.getPrice().intValue());
|
||||
continue;
|
||||
}
|
||||
@@ -638,10 +640,13 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
: "该货品已被其他账号占用,我自动跳过";
|
||||
skippedItem.put("message", skipReason);
|
||||
details.add(skippedItem);
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("跳过货品 {},{}", goods.getId(), skipReason);
|
||||
} else {
|
||||
log.info("{} 跳过货品 {},{}", accountTag, goods.getId(), skipReason);
|
||||
// V2 全部抢单场景下用户数×货品数会产生海量跳过日志,静默不输出;V1 保持原日志
|
||||
if (!isV2) {
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("跳过货品 {},{}", goods.getId(), skipReason);
|
||||
} else {
|
||||
log.info("{} 跳过货品 {},{}", accountTag, goods.getId(), skipReason);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -658,8 +663,25 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
} else {
|
||||
log.info("{} 正在抢购货品:{}", accountTag, item);
|
||||
}
|
||||
HxrAdminBuyService.BuyApiResult buyResult =
|
||||
hxrAdminBuyService.buy(goods.getId(), goods.getSellerId(), apiContext);
|
||||
HxrAdminBuyService.BuyApiResult buyResult;
|
||||
try {
|
||||
if (isV2) {
|
||||
// V2 抢购:GET /trade-app/api/app/sale/flash-sale?goodsId=&buyerId=
|
||||
long buyerId = Long.parseLong(accountId);
|
||||
buyResult = hxrAdminBuyService.buyV2(goods.getId(), buyerId, apiContext);
|
||||
} else {
|
||||
// V1 抢购:POST /api/order/buy
|
||||
buyResult = hxrAdminBuyService.buy(goods.getId(), goods.getSellerId(), apiContext);
|
||||
}
|
||||
} catch (Exception itemEx) {
|
||||
log.error("{} 抢单货品 {} 异常: {}", accountTag, goods.getId(), itemEx.getMessage(), itemEx);
|
||||
failCount++;
|
||||
item.put("apiCode", -1);
|
||||
item.put("message", "抢单异常: " + itemEx.getMessage());
|
||||
item.put("success", false);
|
||||
details.add(item);
|
||||
continue;
|
||||
}
|
||||
item.put("apiCode", buyResult.apiCode());
|
||||
item.put("message", buyResult.apiMsg());
|
||||
item.put("success", buyResult.success());
|
||||
@@ -667,8 +689,14 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
|
||||
if (buyResult.success()) {
|
||||
successCount++;
|
||||
successTotalAmount += goods.getTotalMoney().intValue();
|
||||
Thread.sleep(3000);
|
||||
if (goods.getTotalMoney() != null) {
|
||||
successTotalAmount += goods.getTotalMoney().intValue();
|
||||
}
|
||||
// V2 抢到后立即标记为不可抢,防止其他用户重复尝试同一货品
|
||||
if (isV2 && goodsCoordinator != null) {
|
||||
goodsCoordinator.markUnavailable(tid, goods.getId());
|
||||
}
|
||||
Thread.sleep(isV2 ? 200 : 3000);
|
||||
} else {
|
||||
failCount++;
|
||||
if (buyResult.orderAlreadyGrabbed()) {
|
||||
@@ -820,10 +848,16 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
result.put("stoppedByActivityEnded", stoppedByActivityEnded);
|
||||
result.put("tokenRefreshed", tokenRefreshed);
|
||||
result.put("details", details);
|
||||
long rushBuyElapsedMs = System.currentTimeMillis() - rushBuyStartedAt;
|
||||
result.put("elapsedMs", rushBuyElapsedMs);
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("message={},抢购结束,success={}", result.get("message"),successCount > 0);
|
||||
log.info("message={},抢购结束,success={}, 耗时={} ms ({} s)",
|
||||
result.get("message"), successCount > 0, rushBuyElapsedMs,
|
||||
String.format("%.2f", rushBuyElapsedMs / 1000.0));
|
||||
} else {
|
||||
log.info("message={}, {} 抢购结束,success={}", result.get("message"), accountTag, successCount > 0);
|
||||
log.info("message={}, {} 抢购结束,success={}, 耗时={} ms ({} s)",
|
||||
result.get("message"), accountTag, successCount > 0, rushBuyElapsedMs,
|
||||
String.format("%.2f", rushBuyElapsedMs / 1000.0));
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
@@ -834,10 +868,12 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
long rushBuyElapsedMs = System.currentTimeMillis() - rushBuyStartedAt;
|
||||
result.put("elapsedMs", rushBuyElapsedMs);
|
||||
if (accountTag.isEmpty()) {
|
||||
log.error("rushBuy failed", e);
|
||||
log.error("rushBuy failed, 耗时={} ms", rushBuyElapsedMs, e);
|
||||
} else {
|
||||
log.error("{} rushBuy failed", accountTag, e);
|
||||
log.error("{} rushBuy failed, 耗时={} ms", accountTag, rushBuyElapsedMs, e);
|
||||
}
|
||||
result.put("success", false);
|
||||
result.put("message", "抢购异常:" + e.getMessage());
|
||||
|
||||
Reference in New Issue
Block a user