|
@@ -852,8 +852,17 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
return errorData;
|
|
return errorData;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 清理该用户旧的进行中支付分订单(DB中的 + 同设备Redis追踪的)
|
|
|
|
|
- cancelStalePayScoreOrders(userId, deviceId);
|
|
|
|
|
|
|
+ // 清理该用户旧的进行中支付分订单(分级:自动补扣 > 取消未确认 > 清理超时孤儿 > Redis追踪)
|
|
|
|
|
+ int remainingActive = cancelStalePayScoreOrdersAndCount(userId, deviceId);
|
|
|
|
|
+
|
|
|
|
|
+ // 微信支付分限制:同一实名用户进行中订单不能超过 3 笔
|
|
|
|
|
+ if (remainingActive >= 3) {
|
|
|
|
|
+ log.warn("[支付分服务] 用户 {} 仍有 {} 笔未完结支付分订单,已达上限,拒绝创建新订单", userId, remainingActive);
|
|
|
|
|
+ Map<String, Object> limitData = new HashMap<>();
|
|
|
|
|
+ limitData.put("success", false);
|
|
|
|
|
+ limitData.put("errorMsg", "同一实名身份下进行中订单过多(已达" + remainingActive + "笔),请先完成或取消未支付订单后再试");
|
|
|
|
|
+ return limitData;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 如果没有传 openId,从用户表中获取
|
|
// 如果没有传 openId,从用户表中获取
|
|
|
if (openId == null || openId.isEmpty()) {
|
|
if (openId == null || openId.isEmpty()) {
|
|
@@ -1104,11 +1113,46 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 清理用户旧的进行中支付分订单
|
|
|
|
|
- * 生产环境下回调正常运转后,此方法只在用户多次快速扫码(测试/异常)时触发
|
|
|
|
|
|
|
+ * 清理用户旧的进行中支付分订单,确保不超过微信支付分 3 笔限额。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 分级策略(按优先级从高到低):
|
|
|
|
|
+ * 1. 自动补扣:有金额(>0) 且 创建超过 1 分钟的订单 → 直接扣款,不损失资金
|
|
|
|
|
+ * 2. 取消未确认:CREATED 状态(用户从未授权) → 安全取消
|
|
|
|
|
+ * 3. 取消超时孤儿:DOING/USER_PAYING + 金额为空 + 超过 30 分钟 → 异常单清理
|
|
|
|
|
+ * 4. Redis 追踪清理:同设备旧的预创建订单
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 清理后用户剩余的未完结支付分订单数量
|
|
|
*/
|
|
*/
|
|
|
- private void cancelStalePayScoreOrders(Long userId, String deviceId) {
|
|
|
|
|
- // 1. 取消 CREATED 状态的孤儿订单(创建了但用户从未授权使用)
|
|
|
|
|
|
|
+ private int cancelStalePayScoreOrdersAndCount(Long userId, String deviceId) {
|
|
|
|
|
+ // 1. 自动补扣:有金额的待支付订单(>1min 避免与 ORDER 回调竞态)
|
|
|
|
|
+ try {
|
|
|
|
|
+ java.time.LocalDateTime autoCompleteThreshold = java.time.LocalDateTime.now().minusMinutes(1);
|
|
|
|
|
+ List<Order> readyOrders = orderService.lambdaQuery()
|
|
|
|
|
+ .eq(Order::getUserId, userId)
|
|
|
|
|
+ .eq(Order::getPayChannel, PaymentChannel.WECHAT_PAYSCORE.getCode())
|
|
|
|
|
+ .in(Order::getPayScoreState, Arrays.asList(
|
|
|
|
|
+ PayScoreState.DOING.getCode(),
|
|
|
|
|
+ PayScoreState.USER_PAYING.getCode()))
|
|
|
|
|
+ .isNotNull(Order::getPayScoreOrderId)
|
|
|
|
|
+ .gt(Order::getTotalAmount, BigDecimal.ZERO)
|
|
|
|
|
+ .le(Order::getCreateTime, autoCompleteThreshold)
|
|
|
|
|
+ .list();
|
|
|
|
|
+
|
|
|
|
|
+ if (readyOrders != null && !readyOrders.isEmpty()) {
|
|
|
|
|
+ log.info("[支付分服务] 自动补扣用户 {} 的 {} 笔待支付订单", userId, readyOrders.size());
|
|
|
|
|
+ for (Order order : readyOrders) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ orderService.completeOrderWithPayScore(order.getId(), order.getTotalAmount());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[支付分服务] 自动补扣失败 - orderId: {}, orderNo: {}", order.getId(), order.getOrderNo(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[支付分服务] 自动补扣查询失败 - userId: {}", userId, e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 取消 CREATED 状态的孤儿订单(用户从未授权使用,不会损失资金)
|
|
|
try {
|
|
try {
|
|
|
List<Order> inProgressOrders = orderService.lambdaQuery()
|
|
List<Order> inProgressOrders = orderService.lambdaQuery()
|
|
|
.eq(Order::getUserId, userId)
|
|
.eq(Order::getUserId, userId)
|
|
@@ -1124,12 +1168,11 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("[支付分服务] 清理DB进行中订单失败 - userId: {}", userId, e);
|
|
|
|
|
|
|
+ log.error("[支付分服务] 清理CREATED订单失败 - userId: {}", userId, e);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 1.5 取消 DOING/USER_PAYING 状态但金额为空且超过30分钟的孤儿订单
|
|
|
|
|
- // 仅清理因回调签名失败等异常造成的遗留订单,正常等待 ORDER 回调的订单不受影响
|
|
|
|
|
- // 30分钟阈值确保覆盖 ORDER 回调的最大重试窗口,不会误取消正常进行中的订单
|
|
|
|
|
|
|
+ // 3. 取消超时孤儿:DOING/USER_PAYING + 金额为空 + 超过 30 分钟
|
|
|
|
|
+ // 仅清理回调签名失败等异常造成的遗留订单,正常等待 ORDER 回调的不受影响
|
|
|
try {
|
|
try {
|
|
|
java.time.LocalDateTime staleThreshold = java.time.LocalDateTime.now().minusMinutes(30);
|
|
java.time.LocalDateTime staleThreshold = java.time.LocalDateTime.now().minusMinutes(30);
|
|
|
List<Order> stuckOrders = orderService.lambdaQuery()
|
|
List<Order> stuckOrders = orderService.lambdaQuery()
|
|
@@ -1139,22 +1182,21 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
PayScoreState.DOING.getCode(),
|
|
PayScoreState.DOING.getCode(),
|
|
|
PayScoreState.USER_PAYING.getCode()))
|
|
PayScoreState.USER_PAYING.getCode()))
|
|
|
.isNotNull(Order::getPayScoreOrderId)
|
|
.isNotNull(Order::getPayScoreOrderId)
|
|
|
- .and(w -> w.isNull(Order::getTotalAmount).or().eq(Order::getTotalAmount, BigDecimal.ZERO))
|
|
|
|
|
|
|
+ .and(w -> w.isNull(Order::getTotalAmount).or().le(Order::getTotalAmount, BigDecimal.ZERO))
|
|
|
.le(Order::getCreateTime, staleThreshold)
|
|
.le(Order::getCreateTime, staleThreshold)
|
|
|
.list();
|
|
.list();
|
|
|
|
|
|
|
|
if (stuckOrders != null && !stuckOrders.isEmpty()) {
|
|
if (stuckOrders != null && !stuckOrders.isEmpty()) {
|
|
|
- log.info("[支付分服务] 清理用户 {} 的 {} 笔已确认但金额为空超时订单(>30min)", userId, stuckOrders.size());
|
|
|
|
|
|
|
+ log.info("[支付分服务] 清理用户 {} 的 {} 笔超时孤儿订单(>30min)", userId, stuckOrders.size());
|
|
|
for (Order order : stuckOrders) {
|
|
for (Order order : stuckOrders) {
|
|
|
cancelPayScoreOrderByOutOrderNo(order.getPayScoreOrderId(), order.getId());
|
|
cancelPayScoreOrderByOutOrderNo(order.getPayScoreOrderId(), order.getId());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("[支付分服务] 清理金额为空订单失败 - userId: {}", userId, e);
|
|
|
|
|
|
|
+ log.error("[支付分服务] 清理超时孤儿订单失败 - userId: {}", userId, e);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 取消 Redis 追踪的预创建订单(之前未完成购物流程留下的)
|
|
|
|
|
- // 必须检查关联的本地订单状态,避免误取消已确认/正在扣款的订单
|
|
|
|
|
|
|
+ // 4. 取消 Redis 追踪的预创建订单(之前未完成购物流程留下的)
|
|
|
if (stringRedisTemplate != null) {
|
|
if (stringRedisTemplate != null) {
|
|
|
try {
|
|
try {
|
|
|
String trackingKey = REDIS_TRACKING_KEY + userId;
|
|
String trackingKey = REDIS_TRACKING_KEY + userId;
|
|
@@ -1176,6 +1218,35 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
log.error("[支付分服务] 清理Redis追踪订单失败 - userId: {}", userId, e);
|
|
log.error("[支付分服务] 清理Redis追踪订单失败 - userId: {}", userId, e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 统计清理后剩余的未完结订单数
|
|
|
|
|
+ return countActivePayScoreOrders(userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 统计用户当前未完结的支付分订单数量(DB 维度)
|
|
|
|
|
+ */
|
|
|
|
|
+ private int countActivePayScoreOrders(Long userId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Long count = orderService.lambdaQuery()
|
|
|
|
|
+ .eq(Order::getUserId, userId)
|
|
|
|
|
+ .eq(Order::getPayChannel, PaymentChannel.WECHAT_PAYSCORE.getCode())
|
|
|
|
|
+ .in(Order::getPayScoreState, Arrays.asList(
|
|
|
|
|
+ PayScoreState.CREATED.getCode(),
|
|
|
|
|
+ PayScoreState.DOING.getCode(),
|
|
|
|
|
+ PayScoreState.USER_PAYING.getCode()))
|
|
|
|
|
+ .isNotNull(Order::getPayScoreOrderId)
|
|
|
|
|
+ .count();
|
|
|
|
|
+ return count != null ? count.intValue() : 0;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[支付分服务] 统计未完结订单失败 - userId: {}", userId, e);
|
|
|
|
|
+ return 99; // 保守值,宁可拒绝也不要超标
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** @deprecated 由 cancelStalePayScoreOrdersAndCount 替代 */
|
|
|
|
|
+ private void cancelStalePayScoreOrders(Long userId, String deviceId) {
|
|
|
|
|
+ cancelStalePayScoreOrdersAndCount(userId, deviceId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void cancelPayScoreOrderByOutOrderNo(String outOrderNo, Long localOrderId) {
|
|
private void cancelPayScoreOrderByOutOrderNo(String outOrderNo, Long localOrderId) {
|