|
|
@@ -55,6 +55,8 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
|
|
|
private static final String REDIS_KEY_PREFIX = "payscore:preauth:";
|
|
|
private static final long REDIS_TTL_MINUTES = 10;
|
|
|
+ private static final String REDIS_TRACKING_KEY = "payscore:preauth:tracking:";
|
|
|
+ private static final long REDIS_TRACKING_TTL_HOURS = 24;
|
|
|
|
|
|
private static final BigDecimal DEFAULT_RISK_FUND_AMOUNT = new BigDecimal("99.00");
|
|
|
private static final String DEFAULT_RISK_FUND_NAME = "DEPOSIT";
|
|
|
@@ -743,8 +745,8 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
return errorData;
|
|
|
}
|
|
|
|
|
|
- // 清理用户旧的进行中支付分订单,避免微信返回"进行中订单过多"
|
|
|
- cancelUserInProgressPayScoreOrders(userId);
|
|
|
+ // 取消用户旧的进行中支付分订单(DB + Redis追踪的 + 暴力清理)
|
|
|
+ cancelUserInProgressPayScoreOrders(userId, deviceId);
|
|
|
|
|
|
// 如果没有传 openId,从用户表中获取
|
|
|
if (openId == null || openId.isEmpty()) {
|
|
|
@@ -758,10 +760,22 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
openId = user.getOpenid();
|
|
|
}
|
|
|
|
|
|
- // 生成商户服务订单号(不创建本地订单,由 AI 识别回调统一创建)
|
|
|
+ // 尝试创建,失败时如果是"进行中订单过多"则暴力清理后重试
|
|
|
+ Map<String, Object> result = doCreatePayScoreOrder(userId, deviceId, openId);
|
|
|
+ if (!Boolean.TRUE.equals(result.get("success"))) {
|
|
|
+ String errorMsg = (String) result.get("errorMsg");
|
|
|
+ if (errorMsg != null && (errorMsg.contains("进行中") || errorMsg.contains("过多"))) {
|
|
|
+ log.warn("[支付分服务] 首次创建失败(进行中订单过多),尝试暴力清理 - userId: {}", userId);
|
|
|
+ bruteForceCancelRecentOrders(deviceId);
|
|
|
+ result = doCreatePayScoreOrder(userId, deviceId, openId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> doCreatePayScoreOrder(Long userId, String deviceId, String openId) {
|
|
|
String outOrderNo = generatePreAuthOutOrderNo(deviceId);
|
|
|
|
|
|
- // 创建微信支付分服务订单
|
|
|
PayScoreCreateRequest request = new PayScoreCreateRequest();
|
|
|
request.setOutOrderNo(outOrderNo);
|
|
|
request.setServiceIntroduction("智能零售服务");
|
|
|
@@ -774,18 +788,9 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
PayScoreResult result = payScoreStrategy.createServiceOrder(request);
|
|
|
|
|
|
if (result.isSuccess()) {
|
|
|
- // 将支付分信息存入 Redis,供 AI 识别回调创建订单时关联
|
|
|
- if (stringRedisTemplate != null) {
|
|
|
- String redisKey = REDIS_KEY_PREFIX + deviceId + ":" + userId;
|
|
|
- JSONObject json = new JSONObject();
|
|
|
- json.put("payScoreOrderId", result.getOutOrderNo());
|
|
|
- json.put("payScoreState", result.getState());
|
|
|
- if (wxPayConfig != null) {
|
|
|
- json.put("serviceId", wxPayConfig.getServiceId());
|
|
|
- }
|
|
|
- stringRedisTemplate.opsForValue().set(redisKey, json.toJSONString(), REDIS_TTL_MINUTES, TimeUnit.MINUTES);
|
|
|
- log.info("[支付分服务] 支付分信息已存入Redis - key: {}, ttl: {}min", redisKey, REDIS_TTL_MINUTES);
|
|
|
- }
|
|
|
+ trackPreAuthOrder(userId, outOrderNo);
|
|
|
+ storePreAuthToRedis(deviceId, userId, result);
|
|
|
+ untrackPreAuthOrdersByDevice(userId, deviceId, outOrderNo);
|
|
|
|
|
|
Map<String, Object> data = new HashMap<>();
|
|
|
data.put("success", true);
|
|
|
@@ -805,6 +810,21 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private void storePreAuthToRedis(String deviceId, Long userId, PayScoreResult result) {
|
|
|
+ if (stringRedisTemplate == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String redisKey = REDIS_KEY_PREFIX + deviceId + ":" + userId;
|
|
|
+ JSONObject json = new JSONObject();
|
|
|
+ json.put("payScoreOrderId", result.getOutOrderNo());
|
|
|
+ json.put("payScoreState", result.getState());
|
|
|
+ if (wxPayConfig != null) {
|
|
|
+ json.put("serviceId", wxPayConfig.getServiceId());
|
|
|
+ }
|
|
|
+ stringRedisTemplate.opsForValue().set(redisKey, json.toJSONString(), REDIS_TTL_MINUTES, TimeUnit.MINUTES);
|
|
|
+ log.info("[支付分服务] 支付分信息已存入Redis - key: {}, ttl: {}min", redisKey, REDIS_TTL_MINUTES);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 从 Redis 获取预创建的支付分信息并关联到订单
|
|
|
* 由 HahaCallbackServiceImpl 在创建订单后调用
|
|
|
@@ -836,6 +856,8 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
|
|
|
// 删除 Redis 记录,防止重复使用
|
|
|
stringRedisTemplate.delete(redisKey);
|
|
|
+ // 从追踪 SET 中移除,订单已正常关联不再需要清理
|
|
|
+ stringRedisTemplate.opsForSet().remove(REDIS_TRACKING_KEY + order.getUserId(), payScoreOrderId);
|
|
|
log.info("[支付分服务] 预授权信息已关联到订单 - orderId: {}, payScoreOrderId: {}", order.getId(), payScoreOrderId);
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
@@ -844,10 +866,10 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 取消用户所有进行中的支付分订单
|
|
|
- * 在创建新支付分订单前调用,避免微信返回"同一实名身份下进行中的订单过多"
|
|
|
+ * 取消用户所有进行中的支付分订单(DB订单 + Redis追踪的预创建订单 + 暴力扫描)
|
|
|
*/
|
|
|
- private void cancelUserInProgressPayScoreOrders(Long userId) {
|
|
|
+ private void cancelUserInProgressPayScoreOrders(Long userId, String deviceId) {
|
|
|
+ // 1. 取消 DB 中的进行中支付分订单
|
|
|
try {
|
|
|
List<Order> inProgressOrders = orderService.lambdaQuery()
|
|
|
.eq(Order::getUserId, userId)
|
|
|
@@ -857,34 +879,150 @@ public class PayScoreServiceImpl implements PayScoreService {
|
|
|
.isNotNull(Order::getPayScoreOrderId)
|
|
|
.list();
|
|
|
|
|
|
- if (inProgressOrders == null || inProgressOrders.isEmpty()) {
|
|
|
- return;
|
|
|
+ if (inProgressOrders != null && !inProgressOrders.isEmpty()) {
|
|
|
+ log.info("[支付分服务] 发现用户 {} 有 {} 笔DB中的进行中支付分订单,开始清理",
|
|
|
+ userId, inProgressOrders.size());
|
|
|
+ for (Order order : inProgressOrders) {
|
|
|
+ cancelSingleByOutOrderNo(order.getPayScoreOrderId(), order.getId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[支付分服务] 清理DB进行中订单失败 - userId: {}", userId, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 取消 Redis 追踪 SET 中记录的预创建订单
|
|
|
+ if (stringRedisTemplate != null) {
|
|
|
+ try {
|
|
|
+ String trackingKey = REDIS_TRACKING_KEY + userId;
|
|
|
+ java.util.Set<String> trackedOrders = stringRedisTemplate.opsForSet().members(trackingKey);
|
|
|
+ if (trackedOrders != null && !trackedOrders.isEmpty()) {
|
|
|
+ log.info("[支付分服务] 发现用户 {} 有 {} 笔Redis追踪的预创建订单,开始清理",
|
|
|
+ userId, trackedOrders.size());
|
|
|
+ for (String outOrderNo : trackedOrders) {
|
|
|
+ if (outOrderNo != null && !outOrderNo.isEmpty()) {
|
|
|
+ cancelSingleByOutOrderNo(outOrderNo, null);
|
|
|
+ stringRedisTemplate.opsForSet().remove(trackingKey, outOrderNo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[支付分服务] 清理Redis追踪订单失败 - userId: {}", userId, e);
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 暴力扫描:尝试取消当前设备最近一段时间内可能存在的孤儿订单
|
|
|
+ bruteForceCancelRecentOrders(deviceId);
|
|
|
+ }
|
|
|
|
|
|
- log.info("[支付分服务] 发现用户 {} 有 {} 笔进行中的支付分订单,开始清理",
|
|
|
- userId, inProgressOrders.size());
|
|
|
-
|
|
|
- int cancelledCount = 0;
|
|
|
- for (Order order : inProgressOrders) {
|
|
|
- try {
|
|
|
- PayScoreResult cancelResult = cancelPayScoreOrder(order.getId(),
|
|
|
- "自动取消:创建新订单前清理旧订单");
|
|
|
- if (cancelResult.isSuccess()) {
|
|
|
- cancelledCount++;
|
|
|
- } else {
|
|
|
- log.warn("[支付分服务] 取消旧订单失败 - orderId: {}, payScoreOrderId: {}, error: {}",
|
|
|
- order.getId(), order.getPayScoreOrderId(), cancelResult.getErrorMsg());
|
|
|
+ /**
|
|
|
+ * 通过 outOrderNo 调用微信API取消单个支付分订单
|
|
|
+ */
|
|
|
+ private void cancelSingleByOutOrderNo(String outOrderNo, Long localOrderId) {
|
|
|
+ try {
|
|
|
+ PayScoreCancelRequest cancelReq = new PayScoreCancelRequest();
|
|
|
+ cancelReq.setOutOrderNo(outOrderNo);
|
|
|
+ cancelReq.setReason("自动取消:创建新订单前清理旧订单");
|
|
|
+ PayScoreResult cancelResult = payScoreStrategy.cancelServiceOrder(cancelReq);
|
|
|
+
|
|
|
+ if (cancelResult.isSuccess()) {
|
|
|
+ log.info("[支付分服务] 取消成功 - outOrderNo: {}, localOrderId: {}", outOrderNo, localOrderId);
|
|
|
+ // 如果有本地订单,同步更新状态
|
|
|
+ if (localOrderId != null) {
|
|
|
+ Order order = orderService.getById(localOrderId);
|
|
|
+ if (order != null) {
|
|
|
+ order.setPayScoreState(PayScoreState.REVOKED.getCode());
|
|
|
+ orderService.updateById(order);
|
|
|
}
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("[支付分服务] 取消旧订单异常 - orderId: {}, payScoreOrderId: {}",
|
|
|
- order.getId(), order.getPayScoreOrderId(), e);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // ORDER_NOT_EXIST 表示微信侧已不存在,忽略
|
|
|
+ if (cancelResult.getErrorMsg() != null && cancelResult.getErrorMsg().contains("ORDER_NOT_EXIST")) {
|
|
|
+ log.debug("[支付分服务] 订单在微信侧已不存在 - outOrderNo: {}", outOrderNo);
|
|
|
+ } else {
|
|
|
+ log.debug("[支付分服务] 取消失败 - outOrderNo: {}, error: {}",
|
|
|
+ outOrderNo, cancelResult.getErrorMsg());
|
|
|
}
|
|
|
}
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("[支付分服务] 取消异常 - outOrderNo: {}, error: {}", outOrderNo, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- log.info("[支付分服务] 用户 {} 旧订单清理完成 - 成功: {}/{}", userId, cancelledCount, inProgressOrders.size());
|
|
|
+ /**
|
|
|
+ * 暴力扫描并取消最近一段时间内的预创建订单
|
|
|
+ * 当微信返回"进行中订单过多"时,尝试用不同时间戳构造 outOrderNo 并取消
|
|
|
+ */
|
|
|
+ private void bruteForceCancelRecentOrders(String deviceId) {
|
|
|
+ if (payScoreStrategy == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ long nowSec = System.currentTimeMillis() / 1000;
|
|
|
+ String safeDeviceId = deviceId.replaceAll("[^A-Za-z0-9]", "");
|
|
|
+ String suffix = safeDeviceId.length() > 10 ? safeDeviceId.substring(safeDeviceId.length() - 10) : safeDeviceId;
|
|
|
|
|
|
+ // 扫描过去60分钟内,每30秒一个时间点(最多120次尝试)
|
|
|
+ int cancelledCount = 0;
|
|
|
+ for (int i = 0; i < 120; i++) {
|
|
|
+ long ts = nowSec - (i * 30L);
|
|
|
+ String outOrderNo = "PS" + ts + suffix;
|
|
|
+ try {
|
|
|
+ PayScoreCancelRequest cancelReq = new PayScoreCancelRequest();
|
|
|
+ cancelReq.setOutOrderNo(outOrderNo);
|
|
|
+ cancelReq.setReason("自动清理孤儿订单");
|
|
|
+ PayScoreResult cancelResult = payScoreStrategy.cancelServiceOrder(cancelReq);
|
|
|
+ if (cancelResult.isSuccess()) {
|
|
|
+ cancelledCount++;
|
|
|
+ if (cancelledCount <= 3) {
|
|
|
+ log.info("[支付分服务] 暴力清理成功 - outOrderNo: {}", outOrderNo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 忽略单次取消异常,继续尝试
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (cancelledCount > 0) {
|
|
|
+ log.info("[支付分服务] 暴力清理完成 - 成功取消 {} 笔孤儿订单", cancelledCount);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将预创建的 outOrderNo 记录到 Redis SET 中,防止成为孤儿订单
|
|
|
+ */
|
|
|
+ private void trackPreAuthOrder(Long userId, String outOrderNo) {
|
|
|
+ if (stringRedisTemplate == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String trackingKey = REDIS_TRACKING_KEY + userId;
|
|
|
+ stringRedisTemplate.opsForSet().add(trackingKey, outOrderNo);
|
|
|
+ stringRedisTemplate.expire(trackingKey, REDIS_TRACKING_TTL_HOURS, TimeUnit.HOURS);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("[支付分服务] 追踪订单失败 - outOrderNo: {}", outOrderNo, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清除同一设备旧的追踪记录(同一设备的新订单会替代旧订单)
|
|
|
+ */
|
|
|
+ private void untrackPreAuthOrdersByDevice(Long userId, String deviceId, String currentOutOrderNo) {
|
|
|
+ if (stringRedisTemplate == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String safeDeviceId = deviceId.replaceAll("[^A-Za-z0-9]", "");
|
|
|
+ String suffix = safeDeviceId.length() > 10 ? safeDeviceId.substring(safeDeviceId.length() - 10) : safeDeviceId;
|
|
|
+ String trackingKey = REDIS_TRACKING_KEY + userId;
|
|
|
+ java.util.Set<String> tracked = stringRedisTemplate.opsForSet().members(trackingKey);
|
|
|
+ if (tracked != null) {
|
|
|
+ for (String outOrderNo : tracked) {
|
|
|
+ if (outOrderNo != null && outOrderNo.endsWith(suffix) && !outOrderNo.equals(currentOutOrderNo)) {
|
|
|
+ cancelSingleByOutOrderNo(outOrderNo, null);
|
|
|
+ stringRedisTemplate.opsForSet().remove(trackingKey, outOrderNo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
} catch (Exception e) {
|
|
|
- log.error("[支付分服务] 清理用户 {} 进行中订单失败", userId, e);
|
|
|
+ log.warn("[支付分服务] 清理设备旧追踪失败 - deviceId: {}", deviceId, e);
|
|
|
}
|
|
|
}
|
|
|
}
|