DDD支付模块

工作中对接了招商银行模块,但是回调过程中需要考虑很多问题,这里小计一下

网络不可靠!可能出现:

你的服务器临时过载(GC、Full GC)
数据库连接池满
防火墙拦截
代码 bug 导致 500
机房网络抖动
系统必须支持 幂等处理!

1.首先回调不保证,重复发送回调,需要超过3min后定时主动查询状态,需要幂等处理,改成支付成功/退款关闭订单
首先回调默认是含有重试的5s → 10s → 30s → 1m → 5m → 10m → 30m,招商平台发起回调,如果没有相应是会重复发起,所以考虑网络抖动等情况
需要保证幂等性,我使用的是,out_trade_order+状态,如果非待支付状态,直接return

if (order.getStatus() == PAID || order.getStatus() == CLOSED) {
    return "success"; // 已处理,直接 ACK
}
// 如果是其他异常状态(如 PROCESSING),应记录告警!
if (order.getStatus() != WAITING_PAY) {
    log.warn("订单状态异常,orderId: {}, status: {}", orderId, order.getStatus());
    return "success"; // 仍返回 success 避免重试
}

每次创建订单的时候,塞入当前时间+30min=end_time是最大支付时间,只要超过最大支付时间 使用job扫描 待支付订单查询状态是关闭订单还是支付成功状态

2.向下对接下游系统,比如支付系统收到回调,需要向下调用物业系统发送订单成功,怎么保证事务?
首先下游也要考虑幂等,下游宕机,超时怎么办?考虑首先是告警机制+退款补偿
考虑性能,可以使用线程池 异步发送http/rpc请求下游订单 或者 mq消息发送(可靠消息最终一致性)
如果使用mq消息,建议将更改订单状态+mq本地消息表放在同一个事务内,保证向下传递链路完整性, 并且发MQ消息,由消费者重试

// 阶段1:事务内写 DB + 消息表
@Transactional
public void prepareMessage(String orderId) {
    updateOrderToPaid(orderId);
    mqTask.insert(new Message(orderId, "CREATE"));

	try{
		sendMQ()
		mqTask.update("SEND")
	}catch{
		mqTask.update("FAIL")
		//可增加告警mq是否出现问题
		//不抛出异常
	}
}

//这块可以重构
// 阶段2:独立线程/Job 扫描消息表并发 MQ(成功后改消息状态)
@Schedule(3000)
  private Map<String, Integer> execNotifyJob(List<NotifyTaskEntity> notifyTaskEntityList) throws Exception {
        int successCount = 0, errorCount = 0, retryCount = 0;
        for (NotifyTaskEntity notifyTask : notifyTaskEntityList) {
            // 回调处理 success 成功,error 失败
            String response = port.groupBuyNotify(notifyTask);

            // 更新状态判断&变更数据库表回调任务状态
            if (NotifyTaskHTTPEnumVO.SUCCESS.getCode().equals(response)) {
                int updateCount = repository.updateNotifyTaskStatusSuccess(notifyTask);
                if (1 == updateCount) {
                    successCount += 1;
                }
            } else if (NotifyTaskHTTPEnumVO.ERROR.getCode().equals(response)) {
                if (notifyTask.getNotifyCount() > 4) {
                    int updateCount = repository.updateNotifyTaskStatusError(notifyTask);
                    if (1 == updateCount) {
                        errorCount += 1;
                    }
                } else {
                    int updateCount = repository.updateNotifyTaskStatusRetry(notifyTask);
                    if (1 == updateCount) {
                        retryCount += 1;
                    }
                }
            }
        }

        Map<String, Integer> resultMap = new HashMap<>();
        resultMap.put("waitCount", notifyTaskEntityList.size());
        resultMap.put("successCount", successCount);
        resultMap.put("errorCount", errorCount);
        resultMap.put("retryCount", retryCount);

        return resultMap;
    }

本地消息任务表可以由Job重试发送,但是注意Rabbitmq本地重试消息会在内存内,而且会丢失
建议开始死信队列,并且告警机制,外加补偿机制!

@Configuration
public class RabbitMQConfig {

    // 死信交换机
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlx.exchange");
    }

    // 死信队列
    @Bean
    public Queue dlqQueue() {
        return QueueBuilder.durable("dlq.queue").build();
    }

    // 主队列:绑定死信
    @Bean
    public Queue mainQueue() {
        return QueueBuilder.durable("order.queue")
            .withArgument("x-dead-letter-exchange", "dlx.exchange")
            .withArgument("x-dead-letter-routing-key", "dlq")
            .build();
    }

    @Bean
    public Binding dlqBinding() {
        return BindingBuilder.bind(dlqQueue()).to(dlxExchange()).with("dlq");
    }
}

@RabbitListener(queues = "order.queue")
public void handleOrder(String message, Channel channel, Message msg) throws IOException {
    try {
        processOrder(message);
        channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        // 拒绝消息,不重新入队 → 进入死信队列
        channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, false);
    }
}

@RabbitListener(queues = "dlq.queue")
public void handleDlqMessage(String message) {
    log.error("死信消息,请人工处理: {}", message);
    // 1. 发企业微信/钉钉告警
    // 2. 写入监控数据库
    // 3. 可选:尝试自动重试 N 次(谨慎!)
}

@XxlJob("compensateUnsettledGroupOrders")
public void compensateUnsettledGroupOrders() {
    // 查询:已支付但未回调订单(超过5分钟)
    List<OrderEntity> orders = orderRepository.findPaidButUnsettledGroupOrders(
        System.currentTimeMillis() - 5 * 60 * 1000
    );
    
    for (OrderEntity order : orders) {
        try {
            settlementService.doSettlement(order.getOrderId(), order.getPayTime());
            log.info("补偿结算成功: {}", order.getOrderId());
        } catch (Exception e) {
            log.error("补偿结算失败: {}", order.getOrderId(), e);
            // 可记录到告警系统
        }
    }
}

对账系统 每日跑批:比对银行账单 vs 本地订单,自动修复不一致
全链路追踪 在回调入口打 TraceID,贯穿 MQ、RPC、DB
熔断降级 下游物业系统不可用时,自动跳过并告警(避免阻塞主流程)


第 2 步:配置熔断策略(application.yml)
yaml
编辑
resilience4j:
  circuitbreaker:
    instances:
      propertyService:               # 熔断器名称(对应 @CircuitBreaker(name="propertyService"))
        failure-rate-threshold: 50   # 错误率 >50% 触发熔断
        minimum-number-of-calls: 5   # 至少5次调用才计算错误率
        wait-duration-in-open-state: 30s  # 熔断后30秒进入半开状态
        permitted-number-of-calls-in-half-open-state: 3  # 半开时允许3次试探
        automatic-transition-from-open-to-half-open-enabled: true
  timelimiter:
    instances:
      propertyService:
        timeout-duration: 2s         # 超过2秒视为失败
第 3 步:封装物业系统调用(带熔断)
java
编辑
@Service
public class PropertyServiceClient {

    @Autowired
    private RestTemplate restTemplate;

    // 🔥 核心:使用 @CircuitBreaker 注解
    @CircuitBreaker(name = "propertyService", fallbackMethod = "notifyPropertyFallback")
    @TimeLimiter(name = "propertyService") // 超时控制
    public CompletableFuture<Void> notifyProperty(String orderId) {
        return CompletableFuture.runAsync(() -> {
            // 模拟 HTTP 调用物业系统
            restTemplate.postForObject(
                "http://property-system/api/order-paid",
                Map.of("orderId", orderId),
                Void.class
            );
        });
    }

    // ⚠️ fallback 方法:熔断时执行
    public CompletableFuture<Void> notifyPropertyFallback(String orderId, Exception ex) {
        // 1. 记录告警日志
        log.error("【熔断触发】物业系统不可用,跳过通知!orderId: {}, 原因: {}", orderId, ex.getMessage());

        // 2. 发送告警(钉钉/企业微信/邮件)
        alertService.sendAlert("物业系统熔断", "订单 " + orderId + " 通知失败,请检查!");

        // 3. 可选:写入补偿任务表(后续 Job 重试)
        compensationTaskService.addTask(orderId, TaskType.NOTIFY_PROPERTY);

        // 4. 返回成功(不抛异常,避免影响主流程)
        return CompletableFuture.completedFuture(null);
    }
}

或者使用

@Service
public class PropertyServiceClient {

    @Autowired
    private RestTemplate restTemplate;

    // 🔥 核心:定义 Sentinel 资源
    @SentinelResource(
        value = "notifyProperty",               // 资源名(必须唯一)
        blockHandler = "notifyPropertyBlock",   // 触发熔断/限流时的 fallback
        exceptionsToIgnore = { BusinessException.class } // 业务异常不计入熔断
    )
    public void notifyProperty(String orderId) {
        // 模拟调用物业系统(可能超时或抛异常)
        restTemplate.postForObject(
            "http://property-system/api/order-paid",
            Map.of("orderId", orderId),
            Void.class
        );
    }

    // ⚠️ BlockHandler:必须和原方法签名一致(多一个 BlockException 参数)
    public void notifyPropertyBlock(String orderId, BlockException ex) {
        log.warn("【Sentinel 熔断】物业系统不可用,跳过通知!orderId: {}", orderId, ex);

        // 发告警
        alertService.sendAlert("物业通知熔断", "订单 " + orderId + " 被 Sentinel 熔断");

        // 可选:加入补偿任务
        compensationTaskService.addTask(orderId, TaskType.NOTIFY_PROPERTY);
    }
}

消息轨迹 记录每条消息的发送/消费时间、重试次数(便于排查)

posted @ 2025-11-30 16:16  8023渡劫  阅读(2)  评论(0)    收藏  举报