下午茶拼单系统—7. RabbitMQ回调、退单补偿与部署复盘
前言
最后一篇写收口:支付之后拼团成功怎么回调交易域,用户退单怎么反向通知营销域,部署时 MySQL、Redis、RabbitMQ、应用之间怎么组织。相比前面几篇,这一篇更像项目上线前的检查单。
简历里写到 RabbitMQ 实现异步事件处理、设计 HTTP + MQ 双通道回调与消息持久化重试机制,这不是为了堆技术,而是因为支付和拼团天然跨系统。跨系统就要接受一个事实:调用可能失败,消息可能重复,状态一定要能幂等推进。
HTTP与MQ双通道
营销域组队成功后,可以通过 HTTP 回调交易域,也可以发 MQ。HTTP 简单直接,适合低延迟;MQ 解耦更好,适合削峰和失败重试。项目里交易域同时保留 group_buy_notify 接口和 TeamSuccessTopicListener,便于根据配置切换通道。
@RequestMapping(value = "group_buy_notify", method = RequestMethod.POST)
public String groupBuyNotify(@RequestBody NotifyRequestDTO requestDTO) {
try {
orderService.changeOrderMarketSettlement(requestDTO.getOutTradeNoList());
return "success";
} catch (Exception e) {
return "error";
}
}
HTTP 回调一定要返回明确结果,营销域才能判断是否重试。交易域处理时要按 orderId 列表幂等更新,不能因为重复回调就把状态推进两次。
组队成功监听
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "${spring.rabbitmq.config.consumer.topic_team_success.queue}"),
exchange = @Exchange(value = "${spring.rabbitmq.config.consumer.topic_team_success.exchange}", type = ExchangeTypes.TOPIC),
key = "${spring.rabbitmq.config.consumer.topic_team_success.routing_key}"
)
)
public void listener(String message) {
try {
NotifyRequestDTO requestDTO = JSON.parseObject(message, NotifyRequestDTO.class);
orderService.changeOrderMarketSettlement(requestDTO.getOutTradeNoList());
} catch (Exception e) {
throw e;
}
}
监听器里捕获异常后重新抛出,是为了让 RabbitMQ 的重试或死信机制接管,而不是把失败消息吞掉。消息消费端最怕“日志里报错但消息已经 ack”,那样状态就只能人工补。
退单链路
退单分两层:交易侧先校验订单是否存在、是否属于当前用户、是否已经关闭;如果是营销订单,先通知营销域退拼团;然后根据订单状态决定是否需要支付宝退款。CREATE 和 PAY_WAIT 状态还没真正支付,可以直接关闭;PAY_SUCCESS 或 DEAL_DONE 才需要走退款。
public boolean refundMarketOrder(String userId, String orderId) {
OrderEntity orderEntity = repository.queryOrderByUserIdAndOrderId(userId, orderId);
if (null == orderEntity) return false;
String status = orderEntity.getOrderStatusVO().getCode();
if (OrderStatusVO.CLOSE.getCode().equals(status)) {
return false;
}
port.refundMarketPayOrder(userId, orderId);
if (OrderStatusVO.CREATE.getCode().equals(status)
|| OrderStatusVO.PAY_WAIT.getCode().equals(status)) {
return repository.refundOrder(userId, orderId);
}
return repository.refundMarketOrder(userId, orderId);
}
这段逻辑的重点是先校验所有权和状态,再调用外部域。退款类接口必须天然幂等:同一 orderId 重复请求,最多只会把订单保持在已退单状态,而不能重复释放库存或重复退款。
部署与环境
本地开发时我把 MySQL、Redis、RabbitMQ 放到 docker-compose 里,应用分 dev、test、prod 配置。营销域端口 8091,交易域单独启动,通过 Retrofit2 调用营销 API。部署脚本里要先拉起基础设施,再启动应用,最后检查健康接口和日志。
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: group_buy_market
redis:
image: redis:7
ports:
- "16379:6379"
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
真正上线前还要把日志级别、连接池、线程池、Redis 连接池和 MQ 重试策略检查一遍。下午茶拼单这种活动流量会集中在午后和傍晚,峰值不一定大到离谱,但会很尖。
复盘
这 8 篇文章把项目从架构、规则树、优惠策略、并发加载、Redis 标签、DCC、交易锁单、MQ 回调串了一遍。回到简历,它不再只是“Spring Boot + MySQL + Redis + RabbitMQ”,而是一套能解释业务复杂度的后端项目。
如果后续继续完善,我会优先补三块:第一,营销域锁单和结算的完整幂等表;第二,DCC 配置后台和变更审计;第三,基于压测数据调整线程池、连接池和 MQ 消费并发。项目经历真正有说服力的地方,往往就在这些复盘和取舍里。





