|
@@ -0,0 +1,232 @@
|
|
|
|
|
+package com.etotem.cfc.task;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.etotem.cfc.entity.AssessmentOrder;
|
|
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
|
|
+import com.etotem.cfc.entity.MemberSubscription;
|
|
|
|
|
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
|
|
|
|
|
+import com.etotem.cfc.entity.PaymentOrder;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.AssessmentOrderMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.MemberSubscriptionOrderMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.PaymentOrderMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.AssessmentOrderService;
|
|
|
|
|
+import com.etotem.cfc.service.MemberSubscriptionService;
|
|
|
|
|
+import com.etotem.cfc.service.MembershipService;
|
|
|
|
|
+import com.etotem.cfc.service.VirtualPayService;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.Calendar;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 虚拟支付超时兜底任务(spec §7:success 回调丢失防丢单)。
|
|
|
|
|
+ * 每分钟扫描三张订单表(测评/会员/订阅)status=pending 且创建超过 N 分钟的订单:
|
|
|
|
|
+ * 1. 调 VirtualPayService.queryOrder 向微信确认实际支付状态(兜底发货推送丢失)
|
|
|
|
|
+ * 2. 已支付 → 触发对应解锁逻辑(幂等,与发货推送同路径)
|
|
|
|
|
+ * 3. 明确未支付/已关闭 → 置 cancelled
|
|
|
|
|
+ * 4. 查询失败/无法解析 openid → 跳过本次(防误杀已支付订单),下轮再查
|
|
|
|
|
+ */
|
|
|
|
|
+@Component
|
|
|
|
|
+public class VirtualPayTimeoutTask {
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(VirtualPayTimeoutTask.class);
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private VirtualPayService virtualPayService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AssessmentOrderService assessmentOrderService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MembershipService membershipService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MemberSubscriptionService memberSubscriptionService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AssessmentOrderMapper assessmentOrderMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private PaymentOrderMapper paymentOrderMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MemberSubscriptionOrderMapper memberSubscriptionOrderMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private FamilyMapper familyMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${virtual-pay.payment-timeout-minutes:30}")
|
|
|
|
|
+ private int paymentTimeoutMinutes;
|
|
|
|
|
+
|
|
|
|
|
+ @Scheduled(fixedRate = 60000) // 每分钟执行一次
|
|
|
|
|
+ public void handleTimeoutOrders() {
|
|
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
|
|
+ cal.add(Calendar.MINUTE, -paymentTimeoutMinutes);
|
|
|
|
|
+ Date timeoutTime = cal.getTime();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 扫描三张表(status=pending 且创建超过 N 分钟)
|
|
|
|
|
+ LambdaQueryWrapper<AssessmentOrder> assessmentWrapper = new LambdaQueryWrapper<AssessmentOrder>()
|
|
|
|
|
+ .eq(AssessmentOrder::getStatus, "pending")
|
|
|
|
|
+ .lt(AssessmentOrder::getCreatedAt, timeoutTime);
|
|
|
|
|
+ List<AssessmentOrder> assessmentOrders = assessmentOrderMapper.selectList(assessmentWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<PaymentOrder> paymentWrapper = new LambdaQueryWrapper<PaymentOrder>()
|
|
|
|
|
+ .eq(PaymentOrder::getStatus, "pending")
|
|
|
|
|
+ .lt(PaymentOrder::getCreatedAt, timeoutTime);
|
|
|
|
|
+ List<PaymentOrder> paymentOrders = paymentOrderMapper.selectList(paymentWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<MemberSubscriptionOrder> subWrapper = new LambdaQueryWrapper<MemberSubscriptionOrder>()
|
|
|
|
|
+ .eq(MemberSubscriptionOrder::getStatus, "pending")
|
|
|
|
|
+ .lt(MemberSubscriptionOrder::getCreatedAt, timeoutTime);
|
|
|
|
|
+ List<MemberSubscriptionOrder> subOrders = memberSubscriptionOrderMapper.selectList(subWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 每单处理:解析 openid → queryOrder → 已支付解锁 / 未支付关单 / 失败跳过
|
|
|
|
|
+ int assessmentHandled = 0;
|
|
|
|
|
+ int membershipHandled = 0;
|
|
|
|
|
+ int subscriptionHandled = 0;
|
|
|
|
|
+ for (AssessmentOrder order : assessmentOrders) {
|
|
|
|
|
+ if (handleAssessmentOrder(order)) {
|
|
|
|
|
+ assessmentHandled++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for (PaymentOrder order : paymentOrders) {
|
|
|
|
|
+ if (handlePaymentOrder(order)) {
|
|
|
|
|
+ membershipHandled++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for (MemberSubscriptionOrder order : subOrders) {
|
|
|
|
|
+ if (handleSubscriptionOrder(order)) {
|
|
|
|
|
+ subscriptionHandled++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 汇总日志
|
|
|
|
|
+ log.info("虚拟支付超时兜底扫描完成: 测评={}/{}, 会员={}/{}, 订阅={}/{}",
|
|
|
|
|
+ assessmentHandled, assessmentOrders.size(),
|
|
|
|
|
+ membershipHandled, paymentOrders.size(),
|
|
|
|
|
+ subscriptionHandled, subOrders.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** A 前缀(测评订单)单笔处理,返回是否处理(解锁/关单) */
|
|
|
|
|
+ private boolean handleAssessmentOrder(AssessmentOrder order) {
|
|
|
|
|
+ // openid = order.getUserId() → User.getOpenid()(A 单买家即下单用户)
|
|
|
|
|
+ User user = userMapper.selectById(order.getUserId());
|
|
|
|
|
+ if (user == null || user.getOpenid() == null || user.getOpenid().isEmpty()) {
|
|
|
|
|
+ log.warn("虚拟支付超时兜底: 测评订单openid无法解析, 跳过不关单: orderNo={}", order.getOrderNo());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder(order.getOrderNo(), user.getOpenid());
|
|
|
|
|
+ if (result == null) {
|
|
|
|
|
+ log.warn("虚拟支付超时兜底: 测评订单queryOrder失败, 跳过不关单: orderNo={}", order.getOrderNo());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.paid) {
|
|
|
|
|
+ // 幂等:已 paid/refunded 返回 true,防重复佣金
|
|
|
|
|
+ boolean ok = assessmentOrderService.handleVirtualPaySuccess(order.getOrderNo(), result.wxpayOrderId);
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 测评订单已支付, 触发解锁: orderNo={}, ok={}", order.getOrderNo(), ok);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.status == 0 || result.status == 1 || result.status == 6) {
|
|
|
|
|
+ // 明确未支付/已关闭 → 置 cancelled
|
|
|
|
|
+ order.setStatus("cancelled");
|
|
|
|
|
+ order.setUpdatedAt(new Date());
|
|
|
|
|
+ assessmentOrderMapper.updateById(order);
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 测评订单未支付, 置cancelled: orderNo={}, status={}",
|
|
|
|
|
+ order.getOrderNo(), result.status);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ // status∈{5,7,8,9,10} 退款相关 → 跳过,不关单
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 测评订单退款相关状态, 跳过: orderNo={}, status={}", order.getOrderNo(), result.status);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** ORD 前缀(会员订单)单笔处理,返回是否处理(解锁/关单) */
|
|
|
|
|
+ private boolean handlePaymentOrder(PaymentOrder order) {
|
|
|
|
|
+ String openid = resolveFamilyCreatorOpenid(order.getFamilyId());
|
|
|
|
|
+ if (openid == null) {
|
|
|
|
|
+ log.warn("虚拟支付超时兜底: 会员订单openid无法解析, 跳过不关单: orderNo={}", order.getOrderNo());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder(order.getOrderNo(), openid);
|
|
|
|
|
+ if (result == null) {
|
|
|
|
|
+ log.warn("虚拟支付超时兜底: 会员订单queryOrder失败, 跳过不关单: orderNo={}", order.getOrderNo());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.paid) {
|
|
|
|
|
+ // 幂等:paid 返回 true;trial 直接 true
|
|
|
|
|
+ boolean ok = membershipService.processPaymentCallback(order.getOrderNo(), result.wxpayOrderId, "virtual");
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 会员订单已支付, 触发开通: orderNo={}, ok={}", order.getOrderNo(), ok);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.status == 0 || result.status == 1 || result.status == 6) {
|
|
|
|
|
+ order.setStatus("cancelled");
|
|
|
|
|
+ order.setUpdatedAt(new Date());
|
|
|
|
|
+ paymentOrderMapper.updateById(order);
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 会员订单未支付, 置cancelled: orderNo={}, status={}",
|
|
|
|
|
+ order.getOrderNo(), result.status);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ // status∈{5,7,8,9,10} 退款相关 → 跳过,不关单
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 会员订单退款相关状态, 跳过: orderNo={}, status={}", order.getOrderNo(), result.status);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** SUB 前缀(订阅订单)单笔处理,返回是否处理(解锁/关单) */
|
|
|
|
|
+ private boolean handleSubscriptionOrder(MemberSubscriptionOrder order) {
|
|
|
|
|
+ String openid = resolveFamilyCreatorOpenid(order.getFamilyId());
|
|
|
|
|
+ if (openid == null) {
|
|
|
|
|
+ log.warn("虚拟支付超时兜底: 订阅订单openid无法解析, 跳过不关单: orderNo={}", order.getOrderNo());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder(order.getOrderNo(), openid);
|
|
|
|
|
+ if (result == null) {
|
|
|
|
|
+ log.warn("虚拟支付超时兜底: 订阅订单queryOrder失败, 跳过不关单: orderNo={}", order.getOrderNo());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.paid) {
|
|
|
|
|
+ // 幂等:已 paid/refunded return null
|
|
|
|
|
+ MemberSubscription activated = memberSubscriptionService.activateSubscription(order.getFamilyId(),
|
|
|
|
|
+ order.getLevel(), order.getAmount(), "virtual", result.wxpayOrderId, order.getOrderNo());
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 订阅订单已支付, 触发激活: orderNo={}, activated={}",
|
|
|
|
|
+ order.getOrderNo(), activated != null);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.status == 0 || result.status == 1 || result.status == 6) {
|
|
|
|
|
+ order.setStatus("cancelled");
|
|
|
|
|
+ memberSubscriptionOrderMapper.updateById(order);
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 订阅订单未支付, 置cancelled: orderNo={}, status={}",
|
|
|
|
|
+ order.getOrderNo(), result.status);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ // status∈{5,7,8,9,10} 退款相关 → 跳过,不关单
|
|
|
|
|
+ log.info("虚拟支付超时兜底: 订阅订单退款相关状态, 跳过: orderNo={}, status={}", order.getOrderNo(), result.status);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析家庭创建者(管理员)的 openid(ORD/SUB 单范式,同 MembershipService.processPaymentCallback)。
|
|
|
|
|
+ * 家庭或创建者缺失 → 返回 null(调用方不得关单)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String resolveFamilyCreatorOpenid(Long familyId) {
|
|
|
|
|
+ Family family = familyMapper.selectById(familyId);
|
|
|
|
|
+ if (family == null || family.getCreatorId() == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ User buyer = userMapper.selectById(family.getCreatorId());
|
|
|
|
|
+ if (buyer == null || buyer.getOpenid() == null || buyer.getOpenid().isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return buyer.getOpenid();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|