fix: 发卡异步激活竞态修复,伙伴卡支持重试激活

- consume() 异步激活改为事务提交后(afterCommit)触发,避免新设备未提交时激活报"设备不存在"
- 激活重试任务覆盖无订单号的伙伴代销卡
- 移除废弃的 start.sh
This commit is contained in:
zhaogd
2026-09-26 14:28:08 +08:00
parent 03e5f6fbf2
commit d9546840d3
4 changed files with 50 additions and 66 deletions

View File

@@ -24,6 +24,7 @@ import io.as.modules.device.dto.DeviceOrgContext;
import io.as.modules.device.entity.DeviceEntity;
import io.as.modules.device.service.DeviceResolveService;
import io.as.modules.openapi.sign.ApiDeviceSignService;
import io.as.modules.openapi.service.PartnerCardPoolService;
import io.as.modules.jst.service.JushitingApiService;
import io.as.modules.jst.service.JstActivateCardResult;
import io.as.modules.payment.service.WechatPayService;
@@ -89,6 +90,9 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
@Autowired
private SysOrgService sysOrgService;
@Autowired
private PartnerCardPoolService partnerCardPoolService;
@Value("${renren.daycard.order-timeout-minutes:15}")
private int orderTimeoutMinutes;
@@ -740,20 +744,21 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
int retried = 0;
for (DayCardPoolEntity pool : failedPools) {
try {
// 查询关联订单
if (StringUtils.isBlank(pool.getOrderNo())) {
continue;
}
if (StringUtils.isNotBlank(pool.getOrderNo())) {
// 订单支付的卡:查询关联订单后走订单激活
DayCardOrderEntity order = getByOrderNo(pool.getOrderNo());
if (order == null) {
continue;
}
// 重置激活状态为 PENDING
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_PENDING, null, null);
// 通过 selfProxy 调用确保 @Async 生效
selfProxy.asyncActivateCard(order);
} else if (pool.getDeviceId() != null) {
// 伙伴代销消耗的卡(无订单):走伙伴激活重试
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_PENDING, null, null);
partnerCardPoolService.retryActivate(pool.getId(), pool.getDeviceId());
}
retried++;
} catch (Exception e) {
logger.error("重试激活天卡失败: poolId={}, orderNo={}", pool.getId(), pool.getOrderNo(), e);

View File

@@ -85,4 +85,12 @@ public interface PartnerCardPoolService extends IService<PartnerCardPoolEntity>
* @return 实际回退数量
*/
int revert(String partnerId, int quantity);
/**
* 重试激活伙伴消耗的天卡(无订单场景,供激活失败补偿任务调用)
*
* @param poolId 天卡池ID
* @param deviceId 设备ID
*/
void retryActivate(Long poolId, Long deviceId);
}

View File

@@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.*;
@@ -243,9 +245,19 @@ public class PartnerCardPoolServiceImpl extends ServiceImpl<PartnerCardPoolDao,
this.updateById(pcp);
// 7. 异步激活天卡(调橘视厅)
// 注意:必须等事务提交后再触发,否则异步任务读不到未提交的设备/天卡记录(曾导致"设备不存在"激活失败)
final Long poolId = pcp.getPoolId();
final Long deviceId = device.getId();
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
activateExecutor.execute(() -> activateCardAsync(poolId, deviceId));
}
});
} else {
activateExecutor.execute(() -> activateCardAsync(poolId, deviceId));
}
// 8. 返回天卡信息(异步激活中,客户端通过checkBenefits轮询确认)
// thirdCardNo不暴露给第三方,App通过init+checkBenefits自行获取播放凭证
@@ -290,6 +302,17 @@ public class PartnerCardPoolServiceImpl extends ServiceImpl<PartnerCardPoolDao,
return cards.size();
}
// ==================== 重试激活 ====================
@Override
public void retryActivate(Long poolId, Long deviceId) {
if (poolId == null || deviceId == null) {
return;
}
logger.info("重试激活伙伴天卡: poolId={}, deviceId={}", poolId, deviceId);
activateExecutor.execute(() -> activateCardAsync(poolId, deviceId));
}
/** 按设备标识查找已有设备,找不到返回新实体(标识优先级:mac > wifiMac > deviceUid > deviceSn) */
private DeviceEntity findOrCreateDevice(String mac, String wifiMac, String deviceUid, String deviceSn) {
DeviceEntity existing = null;

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env bash
# ===========================================================
# start.sh — daycard-openapi 后端一键启动脚本
# 项目: Spring Boot 2.6.6 / Java 17 / 端口 8084
#
# 用法:
# ./start.sh # clean 后启动(推荐)
# ./start.sh --no-clean # 跳过 clean,加速重复启动
# ./start.sh -Dspring-boot.run.jvmArguments="-Dserver.port=8090"
# # 透传任意 maven / spring-boot 参数
# ===========================================================
set -euo pipefail
cd "$(dirname "$0")"
# ---- 1. 选定 Java 17(找不到则回退 Java 8)----
JDK17_PREF="/Users/zhaoguodong/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home"
if [[ -d "$JDK17_PREF" ]]; then
export JAVA_HOME="$JDK17_PREF"
elif JDK17_AUTO="$(/usr/libexec/java_home -v 17 2>/dev/null)"; then
export JAVA_HOME="$JDK17_AUTO"
elif JDK8_AUTO="$(/usr/libexec/java_home -v 1.8 2>/dev/null)"; then
export JAVA_HOME="$JDK8_AUTO"
echo "⚠️ 未找到 Java 17,回退到 Java 8" >&2
else
echo "❌ 找不到 Java 17 或 Java 8,请安装 JDK 或修改本脚本里的 JAVA_HOME" >&2
exit 1
fi
echo "==> JAVA_HOME=$JAVA_HOME"
"$JAVA_HOME/bin/java" -version
# ---- 2. 解析参数 ----
DO_CLEAN=1
PASSTHRU=()
for arg in "$@"; do
case "$arg" in
--no-clean) DO_CLEAN=0 ;;
--clean) DO_CLEAN=1 ;;
*) PASSTHRU+=("$arg") ;;
esac
done
# ---- 3. 启动 ----
# clean 可清掉 target 中残留的 stale class(曾出现 AsApplication 2.class
# 导致“找到两个 main class”无法启动的问题)。
GOAL="spring-boot:run"
if [[ "$DO_CLEAN" -eq 1 ]]; then
GOAL="clean spring-boot:run"
echo "==> mvn clean spring-boot:run"
else
echo "==> mvn spring-boot:run"
fi
mvn $GOAL "${PASSTHRU[@]+"${PASSTHRU[@]}"}"