| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- 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.entity.VirtualGoodsConfig;
- 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.SysConfigService;
- import com.etotem.cfc.service.VirtualGoodsConfigService;
- 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;
- import com.etotem.cfc.util.SortUtil;
- import com.etotem.cfc.config.DatabaseInitializer;
- /**
- * 虚拟支付超时兜底任务(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;
- @Resource
- private SysConfigService sysConfigService;
- @Resource
- private VirtualGoodsConfigService virtualGoodsConfigService;
- @Value("${virtual-pay.payment-timeout-minutes:30}")
- private int paymentTimeoutMinutes;
- @Scheduled(fixedRate = 60000) // 每分钟执行一次
- public void handleTimeoutOrders() {
- if (!DatabaseInitializer.isMigrationsComplete()) {
- log.info("数据库迁移尚未完成,跳过本轮虚拟支付超时检查");
- return;
- }
- 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);
- SortUtil.applySort(assessmentWrapper);
- List<AssessmentOrder> assessmentOrders = assessmentOrderMapper.selectList(assessmentWrapper);
- LambdaQueryWrapper<PaymentOrder> paymentWrapper = new LambdaQueryWrapper<PaymentOrder>()
- .eq(PaymentOrder::getStatus, "pending")
- .lt(PaymentOrder::getCreatedAt, timeoutTime);
- SortUtil.applySort(paymentWrapper);
- List<PaymentOrder> paymentOrders = paymentOrderMapper.selectList(paymentWrapper);
- LambdaQueryWrapper<MemberSubscriptionOrder> subWrapper = new LambdaQueryWrapper<MemberSubscriptionOrder>()
- .eq(MemberSubscriptionOrder::getStatus, "pending")
- .lt(MemberSubscriptionOrder::getCreatedAt, timeoutTime);
- SortUtil.applySort(subWrapper);
- 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 == VirtualPayService.STATUS_ORDER_NOT_FOUND
- || 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;
- }
- /**
- * 判定订单是否属于"虚拟支付通道"(即下单时走 xpay,后续需要 xpay query_order 兜底)。
- * 判据与 MembershipService.createOrder 降级逻辑同源:开关开启 且 该 levelCode
- * 在 virtual_goods_config 中有启用的 MEMBERSHIP 道具。不满足则必然走了普通支付
- * (v3-notify 通道),不应被 xpay 查单任务处理。
- */
- boolean isVirtualPaymentOrder(PaymentOrder order) {
- if (order == null || order.getLevelCode() == null) {
- return false;
- }
- String switchVal = sysConfigService.getValue("member_pay_virtual_enabled");
- boolean virtualEnabled = !"0".equals(switchVal);
- if (!virtualEnabled) {
- return false;
- }
- VirtualGoodsConfig cfg = virtualGoodsConfigService.getEnabled("MEMBERSHIP", order.getLevelCode());
- return cfg != null;
- }
- /** ORD 前缀(会员订单)单笔处理,返回是否处理(解锁/关单) */
- private boolean handlePaymentOrder(PaymentOrder order) {
- // 误杀防护(2026-10-22 订单48 事件):普通支付降级订单在 xpay 侧不存在,
- // queryOrder 必然返回 STATUS_ORDER_NOT_FOUND 并会被误置 cancelled。
- // 仅虚拟支付通道订单(开关开启 + 有道具配置)才允许 xpay 查单,与普通支付通道
- // 订单(走 v3-notify)的查询/关单路径解耦。
- if (!isVirtualPaymentOrder(order)) {
- log.info("虚拟支付超时兜底: 非虚拟支付渠道会员订单, 跳过不查单不关单: orderNo={}, levelCode={}",
- order.getOrderNo(), order.getLevelCode());
- return false;
- }
- // family_id=NULL 的订单(下单接口未校验历史脏数据):无法向微信查单,直接置 cancelled
- if (order.getFamilyId() == null) {
- order.setStatus("cancelled");
- order.setUpdatedAt(new Date());
- paymentOrderMapper.updateById(order);
- log.warn("虚拟支付超时兜底: 会员订单无family_id, 置cancelled: orderNo={}", order.getOrderNo());
- return true;
- }
- 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 == VirtualPayService.STATUS_ORDER_NOT_FOUND
- || 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 == VirtualPayService.STATUS_ORDER_NOT_FOUND
- || 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) {
- Long adminUserId = membershipService.resolveFamilyAdminUserId(familyId);
- if (adminUserId == null) {
- return null;
- }
- User buyer = userMapper.selectById(adminUserId);
- if (buyer == null || buyer.getOpenid() == null || buyer.getOpenid().isEmpty()) {
- return null;
- }
- return buyer.getOpenid();
- }
- }
|