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);
}
}
消息轨迹 记录每条消息的发送/消费时间、重试次数(便于排查)
引申一个数据库操作与消息 事务一致性 注解实现
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface LocalTaskMessage {
/**
* 实体属性名称
*/
String entityAttributeName() default "";
}
@Slf4j
@Component
@Aspect
public class LocalTaskMessageAop {
private final ILocalTaskMessageHandleService handleService;
private final TransactionTemplate transactionTemplate;
public LocalTaskMessageAop(ILocalTaskMessageHandleService handleService,
TransactionTemplate transactionTemplate) {
this.handleService = handleService;
this.transactionTemplate = transactionTemplate;
}
@Pointcut("@annotation(cn.bugstack.wrench.local.task.message.LocalTaskMessage)")
public void aopPoint() {
}
@Around("aopPoint() && @annotation(localTaskMessage)")
public Object notify(ProceedingJoinPoint joinPoint, LocalTaskMessage localTaskMessage) throws Throwable {
String signature = joinPoint.getSignature().toShortString();
String entityAttributeName = localTaskMessage.entityAttributeName();
// 判断当前是否已有事务;有则共用,无则统一开启并包裹目标方法与切面逻辑
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
if (active) {
try {
Object result = joinPoint.proceed();
TaskMessageEntityCommand command = resolveCommand(joinPoint, entityAttributeName);
if (command != null) {
log.info("LocalTaskMessageAop 提取到命令对象(同一事务): 方法={} 路径={} 命令={}", signature, entityAttributeName, command);
handleService.acceptTaskMessage(command);
} else {
log.warn("LocalTaskMessageAop 未能提取命令对象(同一事务): 方法={} 路径={}", signature, entityAttributeName);
}
return result;
} catch (Throwable e) {
log.error("LocalTaskMessageAop 处理失败: 方法={} 路径={} 错误={}", signature, entityAttributeName, e.getMessage(), e);
throw e;
}
}
// 无事务时,开启新事务并将方法执行与切面处理统一包裹
try {
return transactionTemplate.execute(status -> {
try {
Object result = joinPoint.proceed();
TaskMessageEntityCommand command = resolveCommand(joinPoint, entityAttributeName);
if (command != null) {
log.info("LocalTaskMessageAop 提取到命令对象(新开事务): 方法={} 路径={} 命令={}", signature, entityAttributeName, command);
handleService.acceptTaskMessage(command);
} else {
log.warn("LocalTaskMessageAop 未能提取命令对象(新开事务): 方法={} 路径={}", signature, entityAttributeName);
}
return result;
} catch (Throwable t) {
log.error("LocalTaskMessageAop 事务内处理失败: 方法={} 路径={} 错误={}", signature, entityAttributeName, t.getMessage(), t);
status.setRollbackOnly();
throw new RuntimeException(t);
}
});
} catch (RuntimeException e) {
if (e.getCause() != null) throw e.getCause();
throw e;
}
}
/**
* 根据 entityAttributeName 从方法入参中解析出 TaskMessageEntityCommand
* 支持两种用法:
* 1) "command" 直接为方法参数名,且参数类型为 TaskMessageEntityCommand
* 2) "xxx.command" 首段为方法参数名,后续为对象属性路径,例如 xxx.getCommand()
* 当 entityAttributeName 为空时,回退为从参数列表中直接寻找第一个 TaskMessageEntityCommand 类型的对象。
*/
private TaskMessageEntityCommand resolveCommand(ProceedingJoinPoint joinPoint, String entityAttributeName) {
Object[] args = joinPoint.getArgs();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 如果未配置路径,直接从参数中找第一个 TaskMessageEntityCommand
if (entityAttributeName == null || entityAttributeName.trim().isEmpty()) {
for (Object arg : args) {
if (arg instanceof TaskMessageEntityCommand) {
return (TaskMessageEntityCommand) arg;
}
}
return null;
}
String[] path = entityAttributeName.split("\\.");
String paramName = path[0];
// 通过参数名定位入参对象;优先使用反射参数名,其次回退到类型匹配
Object root = findArgumentByNameOrType(method, args, paramName);
if (root == null) {
return null;
}
// 如果仅有首段且类型就是命令,直接返回
if (path.length == 1) {
return (root instanceof TaskMessageEntityCommand) ? (TaskMessageEntityCommand) root : null;
}
// 继续沿路径下钻对象属性
Object current = root;
for (int i = 1; i < path.length; i++) {
if (current == null) return null;
current = extractProperty(current, path[i]);
}
return (current instanceof TaskMessageEntityCommand) ? (TaskMessageEntityCommand) current : null;
}
/**
* 根据参数名或类型在方法入参中查找对应对象。
*/
private Object findArgumentByNameOrType(Method method, Object[] args, String paramName) {
// 先尝试使用反射拿到参数名(JDK8+ 需 -parameters 或带调试信息)
java.lang.reflect.Parameter[] parameters = method.getParameters();
if (parameters.length == args.length) {
for (int i = 0; i < parameters.length; i++) {
if (Objects.equals(parameters[i].getName(), paramName)) {
return args[i];
}
}
}
// 如果参数名无法匹配,尝试按类型回退(避免误匹配,仅在 paramName 看起来像类型时使用)
if ("command".equals(paramName)) {
for (Object arg : args) {
if (arg instanceof TaskMessageEntityCommand) {
return arg;
}
}
}
return null;
}
/**
* 从对象中按属性名提取属性值,优先调用 getter,然后尝试直接访问字段。
*/
private Object extractProperty(Object target, String propertyName) {
Class<?> clazz = target.getClass();
String getterName = "get" + capitalize(propertyName);
try {
Method getter = clazz.getMethod(getterName);
return getter.invoke(target);
} catch (Exception ignore) {
// ignore and fallback to field
}
try {
Field field = clazz.getDeclaredField(propertyName);
field.setAccessible(true);
return field.get(target);
} catch (Exception e) {
return null;
}
}
private String capitalize(String s) {
if (s == null || s.isEmpty()) return s;
char first = s.charAt(0);
if (Character.isUpperCase(first)) return s;
return Character.toUpperCase(first) + s.substring(1);
}
}
注意EnableTransactionManagement的Order是LOWEST_PRECEDENCE
使用LocalTaskMessage 不要外加 Transaction注解
Aop判断是该线程是否有事务,注意执行顺序!

浙公网安备 33010602011771号