请购成功的版本
This commit is contained in:
@@ -139,7 +139,7 @@ public class LbGoodsController {
|
||||
@Operation(
|
||||
summary = "抢购货品",
|
||||
description =
|
||||
"从 lb_goods 按 total_money 从大到小选取金额小于39000的货品,调用 hxrd POST /api/order/buy(body: id、seller_id);"
|
||||
"从 lb_goods 选取金额大于29000且24小时内更新的货品,调用 hxrd POST /api/order/buy(body: id、seller_id);"
|
||||
+ "成功笔数达到 maxBuyCount 后停止,且循环抢购总次数不超过 maxBuyCount 的5倍;"
|
||||
+ "token、URL、appStr 等从 lb_third_integration_config 按 tenantId 读取;"
|
||||
+ "token 可选,传入时覆盖库中 goodsApiToken")
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
package com.rj.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.rj.entity.LbBuyAccount;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Mapper
|
||||
public interface LbBuyAccountMapper extends BaseMapper<LbBuyAccount> {
|
||||
|
||||
/**
|
||||
* 按主键更新抢单结果与时间;跳过多租户拦截,避免批量抢单 worker 线程无租户上下文导致更新 0 行。
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Update("""
|
||||
UPDATE lb_buy_account
|
||||
SET rush_buy_result = #{rushBuyResult},
|
||||
last_rush_buy_time = #{lastRushBuyTime},
|
||||
update_time = #{updateTime}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
int updateRushBuyOutcome(@Param("id") String id,
|
||||
@Param("rushBuyResult") String rushBuyResult,
|
||||
@Param("lastRushBuyTime") LocalDateTime lastRushBuyTime,
|
||||
@Param("updateTime") LocalDateTime updateTime);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,23 @@ package com.rj.scheduler;
|
||||
|
||||
import com.rj.config.AppConfig;
|
||||
import com.rj.config.HxrAdminProperties;
|
||||
import com.rj.entity.LbThirdIntegrationConfig;
|
||||
import com.rj.entity.LbGoods;
|
||||
import com.rj.service.HxrAdminOrderSelectService;
|
||||
import com.rj.service.ILbGoodsService;
|
||||
import com.rj.service.ILbOrderRowService;
|
||||
import com.rj.service.ILbThirdIntegrationConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.DayOfWeek;
|
||||
@@ -29,6 +38,9 @@ public class LBAdminPullScheduler {
|
||||
|
||||
private static final long FIFTY_MINUTES_MS = 30L * 60L * 1000L;
|
||||
private static final long UnPay_MINUTES_MS = 20L * 60L * 1000L;
|
||||
private static final long GOODS_PULL_INTERVAL_MS = 1000L;
|
||||
private static final LocalTime GOODS_PULL_WINDOW_START = LocalTime.of(9, 59);
|
||||
private static final LocalTime GOODS_PULL_WINDOW_END = LocalTime.of(22, 3, 59);
|
||||
|
||||
private static final DateTimeFormatter BUY_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@@ -36,8 +48,16 @@ public class LBAdminPullScheduler {
|
||||
private final HxrAdminProperties hxrAdminProperties;
|
||||
private final HxrAdminOrderSelectService hxrAdminOrderSelectService;
|
||||
private final ILbOrderRowService lbOrderRowService;
|
||||
private final ILbThirdIntegrationConfigService lbThirdIntegrationConfigService;
|
||||
private final ILbGoodsService lbGoodsService;
|
||||
private final Clock clock;
|
||||
|
||||
/** 防止货品拉取上一轮未完成时重复执行。 */
|
||||
private final AtomicBoolean goodsPullRunning = new AtomicBoolean(false);
|
||||
|
||||
/** 最近一次调度拉取的货品:租户 ID -> 货品列表(线程安全)。 */
|
||||
private final ConcurrentHashMap<String, List<LbGoods>> tenantGoodsCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Scheduled(
|
||||
fixedRate = FIFTY_MINUTES_MS,
|
||||
initialDelayString = "${hxr.admin.pull-initial-delay-ms:0}")
|
||||
@@ -147,5 +167,134 @@ public class LBAdminPullScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取抢购的物品
|
||||
* 货品同步:每 1 秒触发一次,仅在工作日 9:59~20:03(应用时区)窗口内真正执行;
|
||||
* 若上一轮尚未结束则跳过,避免重复并发执行。
|
||||
*/
|
||||
// @Scheduled(
|
||||
// fixedRate = GOODS_PULL_INTERVAL_MS,
|
||||
// initialDelayString = "${hxr.admin.pull-initial-delay-ms:0}")
|
||||
public void pullGoodsFromThirdParty() {
|
||||
try {
|
||||
|
||||
ZoneId zone = ZoneId.of(appConfig.getTimezone());
|
||||
if (!isWithinGoodsPullWindow(zone)) {
|
||||
log.debug(
|
||||
"当前不在货品拉取窗口(周一至周五 {}~{}),跳过",
|
||||
GOODS_PULL_WINDOW_START,
|
||||
GOODS_PULL_WINDOW_END);
|
||||
return;
|
||||
}
|
||||
if (!goodsPullRunning.compareAndSet(false, true)) {
|
||||
log.debug("货品拉取正在运行中,跳过重复执行");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
syncEnabledGoodsConfigs(zone);
|
||||
} finally {
|
||||
goodsPullRunning.set(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
goodsPullRunning.set(false);
|
||||
log.error("货品拉取调度失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWithinGoodsPullWindow(ZoneId zone) {
|
||||
ZonedDateTime nowZdt = ZonedDateTime.now(clock.withZone(zone));
|
||||
DayOfWeek dow = nowZdt.getDayOfWeek();
|
||||
if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) {
|
||||
return false;
|
||||
}
|
||||
LocalTime now = nowZdt.toLocalTime();
|
||||
return !now.isBefore(GOODS_PULL_WINDOW_START) && !now.isAfter(GOODS_PULL_WINDOW_END);
|
||||
}
|
||||
|
||||
private void syncEnabledGoodsConfigs(ZoneId zone) {
|
||||
List<LbThirdIntegrationConfig> configs = lbThirdIntegrationConfigService.lambdaQuery()
|
||||
.eq(LbThirdIntegrationConfig::getEnabled, 1)
|
||||
.list();
|
||||
if (configs == null || configs.isEmpty()) {
|
||||
log.debug("无启用的第三方集成配置,跳过货品拉取");
|
||||
return;
|
||||
}
|
||||
if (!isWithinGoodsPullWindow(zone)) {
|
||||
log.debug("货品拉取窗口已结束,跳过本次调度");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("货品拉取开始,启用租户数={}", configs.size());
|
||||
ConcurrentHashMap<String, List<LbGoods>> runGoodsMap = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<String, Map<String, Object>> runResultMap = new ConcurrentHashMap<>();
|
||||
AtomicInteger skippedCount = new AtomicInteger(0);
|
||||
|
||||
List<CompletableFuture<Void>> futures = configs.stream()
|
||||
.map(config -> CompletableFuture.runAsync(() -> {
|
||||
String tenantId = config.getTenantId();
|
||||
if (tenantId == null || tenantId.isBlank()) {
|
||||
log.warn("集成配置 id={} 缺少 tenantId,跳过", config.getId());
|
||||
skippedCount.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
String tid = tenantId.trim();
|
||||
Map<String, Object> fetchResult = lbGoodsService.fetchGoodsFromHxrInMemory(tid, null);
|
||||
runResultMap.put(tid, fetchResult);
|
||||
if (Boolean.TRUE.equals(fetchResult.get("success"))) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<LbGoods> goods = (List<LbGoods>) fetchResult.get("data");
|
||||
runGoodsMap.put(tid, goods != null ? goods : List.of());
|
||||
} else {
|
||||
runGoodsMap.put(tid, List.of());
|
||||
}
|
||||
}))
|
||||
.toList();
|
||||
|
||||
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
|
||||
|
||||
tenantGoodsCache.clear();
|
||||
tenantGoodsCache.putAll(runGoodsMap);
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
int totalGoods = 0;
|
||||
StringBuilder detail = new StringBuilder();
|
||||
for (Map.Entry<String, Map<String, Object>> entry : runResultMap.entrySet()) {
|
||||
String tid = entry.getKey();
|
||||
Map<String, Object> fetchResult = entry.getValue();
|
||||
boolean success = Boolean.TRUE.equals(fetchResult.get("success"));
|
||||
int fetched = fetchResult.get("fetched") instanceof Number n ? n.intValue() : 0;
|
||||
if (success) {
|
||||
successCount++;
|
||||
totalGoods += fetched;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
detail.append(System.lineSeparator())
|
||||
.append(" tenantId=")
|
||||
.append(tid)
|
||||
.append(" success=")
|
||||
.append(success)
|
||||
.append(" fetched=")
|
||||
.append(fetched)
|
||||
.append(" message=")
|
||||
.append(fetchResult.get("message"));
|
||||
}
|
||||
|
||||
log.info(
|
||||
"货品拉取调度完成 配置租户数={} 跳过={} 成功={} 失败={} 内存租户数={} 总货品数={}{}",
|
||||
configs.size(),
|
||||
skippedCount.get(),
|
||||
successCount,
|
||||
failCount,
|
||||
tenantGoodsCache.size(),
|
||||
totalGoods,
|
||||
detail);
|
||||
}
|
||||
|
||||
/** 获取最近一次调度写入内存的租户货品映射(只读视图)。 */
|
||||
public Map<String, List<LbGoods>> getTenantGoodsCache() {
|
||||
return Map.copyOf(tenantGoodsCache);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,10 +37,22 @@ public interface ILbGoodsService extends IService<LbGoods> {
|
||||
Map<String, Object> syncFromHxrGoods(String tenantId, String token);
|
||||
|
||||
/**
|
||||
* 按 {@code total_money} 从大到小选取金额小于39000的 {@code lb_goods},调用 hxrd {@code /api/order/buy} 抢购;
|
||||
* 分页拉取第三方 {@code /api/order/goods},仅组装为内存对象列表,不写入 {@code lb_goods}。
|
||||
*
|
||||
* @param token 可选;非空时作为请求头 token,否则使用集成配置中的 goodsApiToken
|
||||
*/
|
||||
Map<String, Object> fetchGoodsFromHxrInMemory(String tenantId, String token);
|
||||
|
||||
/**
|
||||
* 选取金额大于29000且24小时内更新的 {@code lb_goods},调用 hxrd {@code /api/order/buy} 抢购;
|
||||
* 成功笔数达到 {@code maxBuyCount} 后停止,且循环抢购总次数不超过 {@code maxBuyCount} 的5倍。
|
||||
*
|
||||
* @param token 可选;非空时作为请求头 token,否则使用 {@code lb_third_integration_config} 中的 goodsApiToken
|
||||
* @param rushBuyAccountLabel 可选;批量账号抢单时传入,用于日志标识当前抢单账号
|
||||
*/
|
||||
Map<String, Object> rushBuy(String tenantId, String token, Integer maxBuyCount);
|
||||
default Map<String, Object> rushBuy(String tenantId, String token, Integer maxBuyCount) {
|
||||
return rushBuy(tenantId, token, maxBuyCount, null);
|
||||
}
|
||||
|
||||
Map<String, Object> rushBuy(String tenantId, String token, Integer maxBuyCount, String rushBuyAccountLabel);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -226,75 +231,96 @@ public class LbBuyAccountServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> accountResults = new ArrayList<>();
|
||||
int successAccounts = 0;
|
||||
int failAccounts = 0;
|
||||
int accountCount = normalizedIds.size();
|
||||
AtomicInteger threadSeq = new AtomicInteger(0);
|
||||
ExecutorService batchExecutor = Executors.newFixedThreadPool(accountCount, r -> {
|
||||
Thread t = new Thread(r, "lb-rush-buy-" + threadSeq.incrementAndGet());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
log.info("批量抢单启动,账号数={},独立线程数={}", accountCount, accountCount);
|
||||
|
||||
for (String id : normalizedIds) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("accountId", id);
|
||||
try {
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
CountDownLatch readyLatch = new CountDownLatch(accountCount);
|
||||
CountDownLatch doneLatch = new CountDownLatch(accountCount);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object>[] resultArray = new Map[accountCount];
|
||||
|
||||
LbBuyAccount account = accountMap.get(id);
|
||||
if (account == null) {
|
||||
item.put("success", false);
|
||||
item.put("message", "抢单账号不存在");
|
||||
accountResults.add(item);
|
||||
failAccounts++;
|
||||
continue;
|
||||
for (int i = 0; i < accountCount; i++) {
|
||||
final int index = i;
|
||||
String id = normalizedIds.get(i);
|
||||
LbBuyAccount account = accountMap.get(id);
|
||||
batchExecutor.submit(() -> {
|
||||
readyLatch.countDown();
|
||||
try {
|
||||
startLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
resultArray[index] = buildRushBuyTaskErrorItem(id, account, "抢单被中断");
|
||||
persistRushBuyOutcomeFromItem(account, resultArray[index]);
|
||||
doneLatch.countDown();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resultArray[index] = processSingleAccountRushBuy(id, account);
|
||||
} catch (Exception e) {
|
||||
log.error("抢单账号任务执行异常,accountId={},loginAccount={}",
|
||||
id, account != null ? account.getLoginAccount() : null, e);
|
||||
resultArray[index] = buildRushBuyTaskErrorItem(id, account, e.getMessage());
|
||||
persistRushBuyOutcomeFromItem(account, resultArray[index]);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
item.put("loginAccount", account.getLoginAccount());
|
||||
item.put("tenantId", account.getTenantId());
|
||||
item.put("nickname", account.getNickname());
|
||||
if (!readyLatch.await(30, TimeUnit.SECONDS)) {
|
||||
result.put("success", false);
|
||||
result.put("message", "抢单线程就绪超时");
|
||||
return result;
|
||||
}
|
||||
log.info("全部 {} 个抢单线程已就绪,同时启动", accountCount);
|
||||
startLatch.countDown();
|
||||
|
||||
String resultMessage;
|
||||
Boolean rushSuccess = false;
|
||||
LocalDateTime rushBuyTime = LocalDateTime.now();
|
||||
if (!doneLatch.await(120, TimeUnit.SECONDS)) {
|
||||
result.put("success", false);
|
||||
result.put("message", "批量抢单执行超时");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (account.getEnabled() != null && account.getEnabled() == 0) {
|
||||
resultMessage = formatPreRushBuyFailure("账号未启用");
|
||||
} else if (account.getTenantId() == null || account.getTenantId().trim().isEmpty()) {
|
||||
resultMessage = formatPreRushBuyFailure("tenantId不能为空");
|
||||
} else if (account.getTokenFront() == null || account.getTokenFront().trim().isEmpty()) {
|
||||
resultMessage = formatPreRushBuyFailure("token_front不能为空");
|
||||
} else {
|
||||
Integer maxGrabCount = account.getMaxGrabCount();
|
||||
if (maxGrabCount == null || maxGrabCount <= 0) {
|
||||
resultMessage = formatPreRushBuyFailure("maxGrabCount必须大于0");
|
||||
List<Map<String, Object>> accountResults = new ArrayList<>(accountCount);
|
||||
int successAccounts = 0;
|
||||
int failAccounts = 0;
|
||||
for (Map<String, Object> item : resultArray) {
|
||||
accountResults.add(item);
|
||||
if (Boolean.TRUE.equals(item.get("success"))) {
|
||||
successAccounts++;
|
||||
} else {
|
||||
Map<String, Object> rushBuyResult = lbGoodsService.rushBuy(
|
||||
account.getTenantId().trim(),
|
||||
account.getTokenFront().trim(),
|
||||
maxGrabCount);
|
||||
item.put("rushBuyResult", rushBuyResult);
|
||||
rushSuccess = rushBuyResult.get("success") instanceof Boolean
|
||||
? (Boolean) rushBuyResult.get("success")
|
||||
: null;
|
||||
resultMessage = formatRushBuyResultMessage(rushBuyResult);
|
||||
failAccounts++;
|
||||
}
|
||||
}
|
||||
|
||||
item.put("success", Boolean.TRUE.equals(rushSuccess));
|
||||
item.put("message", resultMessage);
|
||||
accountResults.add(item);
|
||||
persistRushBuyOutcome(account.getId(), resultMessage, rushBuyTime);
|
||||
|
||||
if (Boolean.TRUE.equals(rushSuccess)) {
|
||||
successAccounts++;
|
||||
} else {
|
||||
failAccounts++;
|
||||
result.put("success", successAccounts > 0);
|
||||
result.put("message", successAccounts > 0
|
||||
? "批量抢购完成,成功账号 " + successAccounts + " 个,失败 " + failAccounts + " 个"
|
||||
: "批量抢购未成功");
|
||||
result.put("totalAccounts", accountCount);
|
||||
result.put("successAccounts", successAccounts);
|
||||
result.put("failAccounts", failAccounts);
|
||||
result.put("accountResults", accountResults);
|
||||
return result;
|
||||
} finally {
|
||||
batchExecutor.shutdown();
|
||||
try {
|
||||
if (!batchExecutor.awaitTermination(120, TimeUnit.SECONDS)) {
|
||||
batchExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
batchExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
result.put("success", successAccounts > 0);
|
||||
result.put("message", successAccounts > 0
|
||||
? "批量抢购完成,成功账号 " + successAccounts + " 个,失败 " + failAccounts + " 个"
|
||||
: "批量抢购未成功");
|
||||
result.put("totalAccounts", normalizedIds.size());
|
||||
result.put("successAccounts", successAccounts);
|
||||
result.put("failAccounts", failAccounts);
|
||||
result.put("accountResults", accountResults);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("抢单账号批量抢购异常", e);
|
||||
result.put("success", false);
|
||||
@@ -303,6 +329,88 @@ public class LbBuyAccountServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> buildRushBuyTaskErrorItem(String accountId, LbBuyAccount account, String reason) {
|
||||
String accountLabel = formatRushBuyAccountLabel(accountId, account);
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("accountId", accountId);
|
||||
if (account != null) {
|
||||
item.put("loginAccount", account.getLoginAccount());
|
||||
item.put("tenantId", account.getTenantId());
|
||||
item.put("nickname", account.getNickname());
|
||||
}
|
||||
item.put("success", false);
|
||||
item.put("message", attachAccountLabel(accountLabel, "抢单任务异常:" + reason));
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单账号抢单逻辑,供线程池并行调用。
|
||||
*/
|
||||
private Map<String, Object> processSingleAccountRushBuy(String id, LbBuyAccount account) {
|
||||
String accountLabel = formatRushBuyAccountLabel(id, account);
|
||||
log.info("抢单任务开始,{},thread={}", accountLabel, Thread.currentThread().getName());
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("accountId", id);
|
||||
|
||||
if (account == null) {
|
||||
String message = attachAccountLabel(accountLabel, formatPreRushBuyFailure("抢单账号不存在"));
|
||||
item.put("success", false);
|
||||
item.put("message", message);
|
||||
log.info("抢单任务结束,{},success=false,message={}", accountLabel, message);
|
||||
return item;
|
||||
}
|
||||
|
||||
item.put("loginAccount", account.getLoginAccount());
|
||||
item.put("tenantId", account.getTenantId());
|
||||
item.put("nickname", account.getNickname());
|
||||
|
||||
String resultMessage;
|
||||
Boolean rushSuccess = false;
|
||||
|
||||
if (account.getEnabled() != null && account.getEnabled() == 0) {
|
||||
resultMessage = attachAccountLabel(accountLabel, formatPreRushBuyFailure("账号未启用"));
|
||||
} else if (account.getTenantId() == null || account.getTenantId().trim().isEmpty()) {
|
||||
resultMessage = attachAccountLabel(accountLabel, formatPreRushBuyFailure("tenantId不能为空"));
|
||||
} else if (account.getTokenFront() == null || account.getTokenFront().trim().isEmpty()) {
|
||||
resultMessage = attachAccountLabel(accountLabel, formatPreRushBuyFailure("token_front不能为空"));
|
||||
} else {
|
||||
Integer maxGrabCount = account.getMaxGrabCount();
|
||||
if (maxGrabCount == null || maxGrabCount <= 0) {
|
||||
resultMessage = attachAccountLabel(accountLabel, formatPreRushBuyFailure("maxGrabCount必须大于0"));
|
||||
} else {
|
||||
Map<String, Object> rushBuyResult = lbGoodsService.rushBuy(
|
||||
account.getTenantId().trim(),
|
||||
account.getTokenFront().trim(),
|
||||
maxGrabCount,
|
||||
accountLabel);
|
||||
item.put("rushBuyResult", rushBuyResult);
|
||||
rushSuccess = rushBuyResult.get("success") instanceof Boolean
|
||||
? (Boolean) rushBuyResult.get("success")
|
||||
: null;
|
||||
resultMessage = attachAccountLabel(
|
||||
accountLabel, formatRushBuyResultMessage(rushBuyResult));
|
||||
}
|
||||
}
|
||||
|
||||
item.put("success", Boolean.TRUE.equals(rushSuccess));
|
||||
item.put("message", resultMessage);
|
||||
log.info("抢单任务结束,{},success={},message={}", accountLabel, item.get("success"), resultMessage);
|
||||
persistRushBuyOutcome(account.getId(), accountLabel, resultMessage);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void persistRushBuyOutcomeFromItem(LbBuyAccount account, Map<String, Object> item) {
|
||||
if (account == null || account.getId() == null || item == null) {
|
||||
return;
|
||||
}
|
||||
String accountLabel = formatRushBuyAccountLabel(account.getId(), account);
|
||||
Object message = item.get("message");
|
||||
persistRushBuyOutcome(
|
||||
account.getId(),
|
||||
accountLabel,
|
||||
message != null ? message.toString() : "未知抢单结果");
|
||||
}
|
||||
|
||||
private Map<String, Object> validateRequiredForAdd(LbBuyAccount entity) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) {
|
||||
@@ -382,28 +490,55 @@ public class LbBuyAccountServiceImpl
|
||||
/**
|
||||
* 将本次抢单结果与抢单时间写入 lb_buy_account(rush_buy_result、last_rush_buy_time)。
|
||||
*/
|
||||
private void persistRushBuyOutcome(String accountId, String resultMessage, LocalDateTime rushBuyTime) {
|
||||
private void persistRushBuyOutcome(String accountId, String accountLabel, String resultMessage) {
|
||||
if (accountId == null || accountId.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String storedResult = truncateRushBuyResult(
|
||||
resultMessage != null && !resultMessage.isEmpty() ? resultMessage : "未知抢单结果");
|
||||
LocalDateTime time = rushBuyTime != null ? rushBuyTime : LocalDateTime.now();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
try {
|
||||
boolean ok = this.lambdaUpdate()
|
||||
.eq(LbBuyAccount::getId, accountId.trim())
|
||||
.set(LbBuyAccount::getRushBuyResult, storedResult)
|
||||
.set(LbBuyAccount::getLastRushBuyTime, time)
|
||||
.set(LbBuyAccount::getUpdateTime, LocalDateTime.now())
|
||||
.update();
|
||||
if (!ok) {
|
||||
log.warn("抢单结果落库未更新任何行,accountId={}", accountId);
|
||||
int rows = this.baseMapper.updateRushBuyOutcome(
|
||||
accountId.trim(), storedResult, now, now);
|
||||
if (rows <= 0) {
|
||||
log.warn("抢单结果落库未更新任何行,{},accountId={},lastRushBuyTime={}",
|
||||
accountLabel, accountId, now);
|
||||
} else {
|
||||
log.info("抢单结果已落库,{},lastRushBuyTime={},rushBuyResult={}",
|
||||
accountLabel, now, storedResult);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("抢单结果落库异常,accountId={}", accountId, e);
|
||||
log.error("抢单结果落库异常,{},accountId={},lastRushBuyTime={}",
|
||||
accountLabel, accountId, now, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatRushBuyAccountLabel(String accountId, LbBuyAccount account) {
|
||||
StringBuilder sb = new StringBuilder("抢单账号");
|
||||
if (account != null) {
|
||||
if (account.getLoginAccount() != null && !account.getLoginAccount().trim().isEmpty()) {
|
||||
sb.append(" loginAccount=").append(account.getLoginAccount().trim());
|
||||
}
|
||||
if (account.getNickname() != null && !account.getNickname().trim().isEmpty()) {
|
||||
sb.append(" nickname=").append(account.getNickname().trim());
|
||||
}
|
||||
}
|
||||
if (accountId != null && !accountId.trim().isEmpty()) {
|
||||
sb.append(" accountId=").append(accountId.trim());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String attachAccountLabel(String accountLabel, String message) {
|
||||
if (accountLabel == null || accountLabel.isEmpty()) {
|
||||
return message;
|
||||
}
|
||||
if (message == null || message.isEmpty()) {
|
||||
return accountLabel;
|
||||
}
|
||||
return accountLabel + " | " + message;
|
||||
}
|
||||
|
||||
private static String formatPreRushBuyFailure(String reason) {
|
||||
return "【失败】原因:" + reason;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.util.Optional;
|
||||
@RequiredArgsConstructor
|
||||
public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> implements ILbGoodsService {
|
||||
|
||||
private static final BigDecimal RUSH_BUY_MAX_TOTAL_MONEY = new BigDecimal("39000");
|
||||
private static final BigDecimal RUSH_BUY_MIN_TOTAL_MONEY = new BigDecimal("29000");
|
||||
|
||||
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@@ -375,6 +375,120 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> fetchGoodsFromHxrInMemory(String tenantId, String token) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "tenantId不能为空");
|
||||
result.put("data", List.of());
|
||||
result.put("fetched", 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
String tid = tenantId.trim();
|
||||
Optional<HxrGoodsApiContext> apiContextOpt =
|
||||
lbThirdIntegrationConfigService.resolveGoodsApiContext(tid, token);
|
||||
if (apiContextOpt.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message",
|
||||
"未找到该租户的第三方集成配置,或配置未启用、URL/凭证不完整(请检查 lb_third_integration_config)");
|
||||
result.put("data", List.of());
|
||||
result.put("fetched", 0);
|
||||
return result;
|
||||
}
|
||||
HxrGoodsApiContext apiContext = apiContextOpt.get();
|
||||
|
||||
String previousTenantId = TenantContextHolder.getTenantId();
|
||||
TenantContextHolder.setTenantId(tid);
|
||||
try {
|
||||
HxrLbGoodsPageData firstPageData = null;
|
||||
int page = 1;
|
||||
List<LbGoods> allGoods = new ArrayList<>();
|
||||
while (true) {
|
||||
Optional<HxrLbGoodsSelectResponse> opt =
|
||||
hxrAdminGoodsService.fetchGoodsPage(page, apiContext);
|
||||
if (opt.isEmpty()) {
|
||||
if (page == 1) {
|
||||
result.put("success", false);
|
||||
result.put("message",
|
||||
"未拉取到货品(请检查 lb_third_integration_config 中 token、appStr、URL 或网络)");
|
||||
result.put("data", List.of());
|
||||
result.put("fetched", 0);
|
||||
return result;
|
||||
}
|
||||
result.put("success", false);
|
||||
result.put("message", "第 " + page + " 页拉取失败,已成功拉取前 "
|
||||
+ (page - 1) + " 页共 " + allGoods.size() + " 条");
|
||||
result.put("data", List.copyOf(allGoods));
|
||||
result.put("fetched", allGoods.size());
|
||||
if (firstPageData != null) {
|
||||
result.put("lastPage", firstPageData.lastPage());
|
||||
}
|
||||
result.put("pages", page - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
HxrLbGoodsSelectResponse body = opt.get();
|
||||
HxrLbGoodsPageData pageData = body.data();
|
||||
if (firstPageData == null && pageData != null) {
|
||||
firstPageData = pageData;
|
||||
}
|
||||
List<LbGoods> rows = pageData != null ? pageData.list() : null;
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (LbGoods row : rows) {
|
||||
if (row.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
row.setTenantId(tid);
|
||||
row.setCurrentPage(page);
|
||||
applyDefaults(row);
|
||||
if (row.getCreatedAt() == null) {
|
||||
row.setCreatedAt(now);
|
||||
}
|
||||
if (row.getUpdatedAt() == null) {
|
||||
row.setUpdatedAt(now);
|
||||
}
|
||||
allGoods.add(row);
|
||||
}
|
||||
|
||||
boolean hasMore = pageData != null && pageData.hasmore();
|
||||
int lastPage = pageData != null ? pageData.lastPage() : page;
|
||||
if (!hasMore || page >= lastPage) {
|
||||
break;
|
||||
}
|
||||
page++;
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", allGoods.isEmpty() ? "接口成功,无货品数据" : "拉取完成");
|
||||
result.put("data", List.copyOf(allGoods));
|
||||
result.put("fetched", allGoods.size());
|
||||
result.put("lastPage", firstPageData != null ? firstPageData.lastPage() : page);
|
||||
result.put("pages", page);
|
||||
return result;
|
||||
} finally {
|
||||
if (previousTenantId != null) {
|
||||
TenantContextHolder.setTenantId(previousTenantId);
|
||||
} else {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("message", "拉取异常:" + e.getMessage());
|
||||
result.put("data", List.of());
|
||||
result.put("fetched", 0);
|
||||
log.error("fetchGoodsFromHxrInMemory failed", e);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private int upsertBatch(List<LbGoods> entities) {
|
||||
if (entities == null || entities.isEmpty()) {
|
||||
return 0;
|
||||
@@ -399,8 +513,9 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> rushBuy(String tenantId, String token, Integer maxBuyCount) {
|
||||
public Map<String, Object> rushBuy(String tenantId, String token, Integer maxBuyCount, String rushBuyAccountLabel) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
String accountTag = formatRushBuyAccountLogTag(rushBuyAccountLabel);
|
||||
try {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
@@ -430,22 +545,24 @@ 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.lt(LbGoods::getTotalMoney, RUSH_BUY_MAX_TOTAL_MONEY);
|
||||
w.orderByDesc(LbGoods::getTotalMoney).orderByDesc(LbGoods::getId);
|
||||
w.gt(LbGoods::getTotalMoney, RUSH_BUY_MIN_TOTAL_MONEY);
|
||||
w.ge(LbGoods::getUpdatedAt, updatedAfter);
|
||||
w.orderByDesc(LbGoods::getUpdatedAt);
|
||||
List<LbGoods> goodsList = this.list(w);
|
||||
if (goodsList.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "lb_goods 中无可抢购货品(金额需小于39000)");
|
||||
result.put("message", "lb_goods 中无可抢购货品(金额需大于29000,且更新时间需在24小时内)");
|
||||
result.put("successCount", 0);
|
||||
result.put("failCount", 0);
|
||||
result.put("details", List.of());
|
||||
return result;
|
||||
}
|
||||
|
||||
int maxAttempts = maxBuyCount * 2;
|
||||
int maxAttempts = maxBuyCount * 100;
|
||||
List<Map<String, Object>> details = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
@@ -472,7 +589,11 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
item.put("sellerId", goods.getSellerId());
|
||||
item.put("title", goods.getTitle());
|
||||
item.put("totalMoney", goods.getTotalMoney());
|
||||
log.info("正在抢购货品是:{}",item.toString());
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("正在抢购货品:{}", item);
|
||||
} else {
|
||||
log.info("{} 正在抢购货品:{}", accountTag, item);
|
||||
}
|
||||
HxrAdminBuyService.BuyApiResult buyResult =
|
||||
hxrAdminBuyService.buy(goods.getId(), goods.getSellerId(), apiContext);
|
||||
item.put("apiCode", buyResult.apiCode());
|
||||
@@ -498,6 +619,11 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
result.put("stoppedByMax", stoppedByMax);
|
||||
result.put("stoppedByMaxAttempts", stoppedByMaxAttempts);
|
||||
result.put("details", details);
|
||||
if (accountTag.isEmpty()) {
|
||||
log.info("抢购结束,success={},message={}", successCount > 0, result.get("message"));
|
||||
} else {
|
||||
log.info("{} 抢购结束,success={},message={}", accountTag, successCount > 0, result.get("message"));
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
if (previousTenantId != null) {
|
||||
@@ -507,13 +633,24 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("rushBuy failed", e);
|
||||
if (accountTag.isEmpty()) {
|
||||
log.error("rushBuy failed", e);
|
||||
} else {
|
||||
log.error("{} rushBuy failed", accountTag, e);
|
||||
}
|
||||
result.put("success", false);
|
||||
result.put("message", "抢购异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatRushBuyAccountLogTag(String rushBuyAccountLabel) {
|
||||
if (rushBuyAccountLabel == null || rushBuyAccountLabel.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return rushBuyAccountLabel.trim();
|
||||
}
|
||||
|
||||
private static void applyDefaults(LbGoods entity) {
|
||||
if (entity.getPrice() == null) {
|
||||
entity.setPrice(BigDecimal.ZERO);
|
||||
|
||||
Reference in New Issue
Block a user