Knowledge note
正在加载知识笔记
正在加载知识笔记
Knowledge note
适用对象:刚接触本项目、对 Java 分层和 DDD 还不熟悉的开发者。
本文不是重新讲一遍全部业务,而是提供一张“源码导航地图”:看到一个类时,知道它是什么、为什么存在、上游是谁、下游去哪里,以及应该按照什么顺序阅读。
配套阅读:根目录的
CORE_FLOW_GUIDE.md更偏端到端业务流程;本文更偏domain/trade内部的包结构、对象职责和代码跳转顺序。
trade 不只是“创建订单”,它同时包含四条业务线:
lock:活动校验、用户限次、团队库存预占、创建团队主单和用户明细单。settlement:接收支付成功结果、校验订单与支付时间、推进支付和成团进度、创建通知任务。refund:识别未支付、已支付、已成团场景,选择不同策略回滚订单、团队计数和库存。task:执行本地通知任务,通过 HTTP 或 MQ 通知下游,并处理重试状态。它还组合了 DDD 分层、聚合、责任链、策略、Redis 库存、MySQL 本地消息表、MQ/HTTP 通知和定时补偿。因此学习时不要从目录第一行一路往下读,应该先选一条业务线,再按照:
入口 -> 领域服务 -> 责任链 -> 聚合 -> 仓储接口 -> 仓储实现
纵向阅读。
flowchart TD
C["MarketTradeController"] --> L["ITradeLockOrderService"]
C --> S["ITradeSettlementOrderService"]
C --> R["ITradeRefundOrderService"]
L --> LC["锁单责任链"]
LC --> LA["GroupBuyOrderAggregate"]
S --> SC["结算责任链"]
SC --> SA["GroupBuyTeamSettlementAggregate"]
R --> RC["退款责任链"]
RC --> RS["退款策略"]
RS --> RA["GroupBuyRefundAggregate"]
L --> REPO["ITradeRepository"]
S --> REPO
RS --> REPO
T["TradeTaskService"] --> REPO
T --> PORT["ITradePort"]
REPO --> RI["TradeRepository"]
RI --> DB["MySQL"]
RI --> REDIS["Redis"]
RI --> DCC["DCC"]
PORT --> PI["TradePort"]
PI --> HTTP["HTTP 回调"]
PI --> MQ["RabbitMQ"]
JOB["GroupBuyNotifyJob"] --> T
RJOB["TimeoutRefundJob"] --> R
MQ --> LISTENER["RefundSuccessTopicListener"]
LISTENER --> R
先记住四句话:
service 负责组织一次领域操作。filter 负责一小段校验、数据准备或流程节点工作。aggregate 把一次写操作需要的完整业务数据打包给仓储。repository/port 是 domain 向外访问数据库、Redis、MQ、HTTP 的抽象边界。交易域根目录:
group-buy-market-domain/src/main/java/cn/bugstack/domain/trade
├── adapter
│ ├── repository/ITradeRepository.java
│ └── port/ITradePort.java
├── model
│ ├── aggregate
│ ├── entity
│ └── valobj
└── service
├── ITradeLockOrderService.java
├── ITradeSettlementOrderService.java
├── ITradeRefundOrderService.java
├── ITradeTaskService.java
├── lock
├── settlement
├── refund
└── task
| 目录 | 初学者翻译 | 主要内容 | 阅读建议 |
|---|---|---|---|
adapter/repository | 领域对持久化能力的要求 | 查单、锁单、结算、退款、通知任务、库存 | 先看方法名,后看实现 |
adapter/port | 领域对外部系统能力的要求 | HTTP/MQ 通知 | 和 Repository 对比着看 |
model/aggregate | 一次完整写操作的数据包 | 锁单、结算、退款聚合 | 重点看包含哪些对象 |
model/entity | 业务数据模型集合 | 业务实体、命令、上下文结果 | 不要只凭 Entity 后缀判断 |
model/valobj | 值、状态、配置和场景枚举 | 进度、通知配置、退款类型 | 重点看枚举如何参与分支 |
service/lock | 正向创建交易 | 锁单服务和三节点责任链 | 第一条推荐学习链路 |
service/settlement | 支付成功后的正向推进 | 结算服务和四节点责任链 | 第二条推荐学习链路 |
service/refund | 逆向交易 | 退款责任链和三种策略 | 最后重点学习 |
service/task | 可靠通知执行 | 查询任务、发送、更新状态 | 用于理解最终一致性 |
当前项目使用了 DDD 分层、聚合、实体、值对象、仓储和端口等概念,但它不是所有实体都包含丰富行为的“纯富领域模型”。不少 *Entity 更接近命令、上下文或结果数据载体。学习时按实际职责分类,比按后缀分类更准确。
| 类 | 代表什么 | 关键字段 |
|---|---|---|
UserEntity | 当前交易用户 | userId |
GroupBuyActivityEntity | 交易域需要的活动快照 | 活动状态、时间、限次、目标人数、有效时长 |
GroupBuyTeamEntity | 拼团队伍当前快照 | 团队号、目标数、完成数、锁单数、状态、有效期 |
MarketPayOrderEntity | 用户营销支付订单视图 | 团队号、订单号、三段价格、交易状态 |
PayActivityEntity | 锁单时采用的活动信息 | 活动 ID、名称、目标人数、有效期、团队号 |
PayDiscountEntity | 锁单时采用的商品与优惠结果 | 来源、渠道、商品、价格、外部单号、通知配置 |
TradePaySuccessEntity | 外部支付成功事实 | 来源、渠道、用户、外部单号、支付时间 |
TradeRefundOrderEntity | 退款要操作的订单定位信息 | 用户、团队、活动、内部订单号、外部单号 |
NotifyTaskEntity | 待执行的外部通知任务 | 通知类型、MQ、URL、次数、JSON 参数、UUID |
| 类 | 真正角色 | 谁创建它 | 交给谁 |
|---|---|---|---|
TradeLockRuleCommandEntity | 锁单责任链输入 | TradeLockOrderService | tradeRuleFilter |
TradeSettlementRuleCommandEntity | 结算责任链输入 | TradeSettlementOrderService | tradeSettlementRuleFilter |
TradeRefundCommandEntity | 退款用例命令 | Controller 或超时任务 | TradeRefundOrderService |
命令对象回答的是:“这一次流程要求系统做什么?”
| 类 | 真正角色 | 包含什么 |
|---|---|---|
TradeLockRuleFilterBackEntity | 锁单链输出 | 用户已参与次数、库存恢复 Key |
TradeSettlementRuleFilterBackEntity | 结算链输出 | 完整团队快照和通知配置 |
TradePaySettlementEntity | 结算服务对上层的结果 | 用户、团队、活动、外部单号 |
TradeRefundBehaviorEntity | 退款服务对上层的结果 | 用户、订单、团队、SUCCESS/REPEAT |
结果对象回答的是:“这次流程完成后,上层需要知道什么?”
| 类 | 用途 |
|---|---|
GroupBuyProgressVO | 团队目标数、完成数、锁单数的组合快照 |
NotifyConfigVO | HTTP/MQ 通知配置 |
NotifyTypeEnumVO | 通知方式:HTTP 或 MQ |
TradeOrderStatusEnumVO | 用户订单状态:CREATE/COMPLETE/CLOSE |
RefundTypeEnumVO | 根据团队状态和订单状态识别退款场景,并给出策略 Bean 名 |
TaskNotifyCategoryEnumVO | 通知任务业务分类:结算、未支付退款、已支付未成团退款、已支付已成团退款 |
TeamRefundSuccess | 退款成功消息的数据载体,供消费端恢复库存 |
聚合在本项目中可以先理解为:交给仓储执行一次一致性写操作的业务参数包。
它不等于数据库表,也不等于 Controller DTO。它组合了某次领域动作需要同时参考的数据。
GroupBuyOrderAggregate:锁单聚合GroupBuyOrderAggregate
├── UserEntity 谁在下单
├── PayActivityEntity 参加哪个活动/团队
├── PayDiscountEntity 买什么、按什么价格、如何通知
└── userTakeOrderCount 用户这是第几次参与
交给 ITradeRepository.lockMarketPayOrder(...)。仓储会把它拆成团队主单 group_buy_order 和用户明细 group_buy_order_list 等持久化动作。
GroupBuyTeamSettlementAggregate:结算聚合GroupBuyTeamSettlementAggregate
├── UserEntity 哪个用户完成支付
├── GroupBuyTeamEntity 当前团队状态和进度
└── TradePaySuccessEntity 外部支付成功事实
交给 ITradeRepository.settlementMarketPayOrder(...)。仓储据此更新用户明细、团队完成数和团队状态,并在需要时写入通知任务。
GroupBuyRefundAggregate:退款聚合GroupBuyRefundAggregate
├── TradeRefundOrderEntity 退哪一笔订单
├── GroupBuyProgressVO 锁单数/完成数如何变化
└── GroupBuyOrderEnumVO 团队应更新到什么状态
它提供三个静态构造方法:
buildUnpaid2RefundAggregate(..., -1):只减少锁单数。buildPaid2RefundAggregate(..., -1, -1):减少锁单数和完成数。buildPaidTeam2RefundAggregate(..., -1, -1, status):减少两个计数,并更新成团后的团队状态。这里的 -1 是“增量更新量”,不是把数据库字段直接设置为 -1。
| 业务线 | 入口服务 | 核心模式 | 聚合 | 仓储核心方法 | 最终效果 |
|---|---|---|---|---|---|
| 锁单 | TradeLockOrderService | 责任链 | GroupBuyOrderAggregate | lockMarketPayOrder | 创建待支付订单并占名额 |
| 结算 | TradeSettlementOrderService | 责任链 | GroupBuyTeamSettlementAggregate | settlementMarketPayOrder | 订单支付完成,推进团队进度 |
| 退款 | TradeRefundOrderService | 责任链 + 策略 | GroupBuyRefundAggregate | 三种退款方法 | 关闭订单并回滚计数/库存 |
| 通知 | TradeTaskService | 本地消息任务 + 端口适配 | NotifyTaskEntity | 查询和更新任务状态 | HTTP/MQ 通知与重试 |
MarketTradeController.lockMarketPayOrder
-> ITradeLockOrderService
-> TradeLockOrderService.lockMarketPayOrder
-> TradeLockRuleFilterFactory
-> ActivityUsabilityRuleFilter
-> UserTakeLimitRuleFilter
-> TeamStockOccupyRuleFilter
-> GroupBuyOrderAggregate
-> ITradeRepository.lockMarketPayOrder
-> TradeRepository.lockMarketPayOrder
MarketTradeController 在进入交易域锁单服务前还做了几件事:
userId + outTradeNo 查询已有订单;如果已经存在 CREATE 状态订单,直接返回原订单,减少重复锁单。lockCount == targetCount 时提前拦截。UserEntity、PayActivityEntity、PayDiscountEntity 调用交易域。因此不能说“所有锁单规则都在 TradeLockOrderService”。参数校验、已有订单快速返回和试算发生在 Controller 编排阶段;交易域内部再执行关键业务规则链。
输入 TradeLockRuleCommandEntity
├── userId
├── activityId
└── teamId
上下文 TradeLockRuleFilterFactory.DynamicContext
├── groupBuyActivity
└── userTakeOrderCount
输出 TradeLockRuleFilterBackEntity
├── userTakeOrderCount
└── recoveryTeamStockKey
三者不要混淆:
| 顺序 | 节点 | 输入依赖 | 做什么 | 写入/返回 |
|---|---|---|---|---|
| 1 | ActivityUsabilityRuleFilter | activityId | 查活动,校验 EFFECTIVE 和当前时间位于活动起止时间内 | 将活动放入 Context |
| 2 | UserTakeLimitRuleFilter | 活动、用户 | 查询用户在活动下的订单数,和 takeLimitCount 比较 | 将次数放入 Context |
| 3 | TeamStockOccupyRuleFilter | 团队号、活动目标和有效时长 | 加入已有团时占用 Redis 团队库存;新开团直接跳过 | 返回次数和恢复 Key |
责任链顺序有业务含义:先用基础合法性规则快速失败,再做用户计数,最后才占团队库存。
TeamStockOccupyRuleFilter 会检查 teamId:
teamId 为空:表示新开团,没有既有团队名额可以预占,节点直接返回。teamId 非空:表示加入已有团队,通过 repository.occupyTeamStock(...) 抢占一个名额。Redis Key 由 TradeLockRuleFilterFactory 生成:
库存 Key:group_buy_market_team_stock_key_{activityId}_{teamId}
恢复 Key:group_buy_market_team_stock_key_{activityId}_{teamId}_recovery
TradeLockOrderService.lockMarketPayOrder(...) 的职责可以压缩成三步:
GroupBuyOrderAggregate。repository.lockMarketPayOrder(...) 落库。如果第 3 步异常,服务会调用:
repository.recoveryTeamStock(recoveryTeamStockKey, validTime);
这表示前面 Redis 已成功占位,但数据库锁单失败时,要主动补回名额。
基础设施实现位于:
group-buy-market-infrastructure/.../adapter/repository/TradeRepository.java
| PO/表 | 粒度 | 锁单时的作用 |
|---|---|---|
GroupBuyOrder / group_buy_order | 团队维度 | 新开团时创建团队主单;参团时维护团队锁单数 |
GroupBuyOrderList / group_buy_order_list | 用户参与维度 | 创建用户的待支付明细订单 |
一个 GroupBuyOrderAggregate 会被仓储拆解为多张表的写操作,领域对象和 PO 并非一一对应。
锁单前,Controller 先做外部单号幂等检查、团队是否已满检查和营销试算;进入交易域后,锁单责任链依次校验活动有效性、用户参与上限,并在加入已有团队时通过 Redis 预占名额。规则通过后,服务把用户、活动、优惠和参与次数组装成锁单聚合,由仓储创建团队主单和用户明细单;如果数据库落单失败,会用责任链返回的恢复 Key 补回 Redis 库存。
MarketTradeController.settlementMarketPayOrder
-> ITradeSettlementOrderService
-> TradeSettlementOrderService.settlementMarketPayOrder
-> TradeSettlementRuleFilterFactory
-> SCRuleFilter
-> OutTradeNoRuleFilter
-> SettableRuleFilter
-> EndRuleFilter
-> GroupBuyTeamSettlementAggregate
-> ITradeRepository.settlementMarketPayOrder
-> TradeRepository.settlementMarketPayOrder
-> TradeTaskService.execNotifyJob
输入 TradeSettlementRuleCommandEntity
├── source
├── channel
├── userId
├── outTradeNo
└── outTradeTime
上下文 TradeSettlementRuleFilterFactory.DynamicContext
├── marketPayOrderEntity
└── groupBuyTeamEntity
输出 TradeSettlementRuleFilterBackEntity
└── 团队完整快照 + notifyConfigVO
| 顺序 | 节点 | 核心问题 | 关键动作 |
|---|---|---|---|
| 1 | SCRuleFilter | 这个来源和渠道是否被禁用? | 调用 isSCBlackIntercept,底层读取 DCC 黑名单 |
| 2 | OutTradeNoRuleFilter | 订单是否存在、是否可继续结算? | 按用户和外部单号查订单;不存在或已 CLOSE 时拒绝 |
| 3 | SettableRuleFilter | 支付发生时间是否仍在团队有效期内? | 查团队,要求 outTradeTime 严格早于 validEndTime |
| 4 | EndRuleFilter | 如何把链路结果交回服务? | 将 Context 中的团队映射为 FilterBack |
EndRuleFilter 把“前面节点只负责校验和填上下文”与“最终统一构造输出”分开,使每个节点职责更单一。
TradeSettlementOrderService:
TradePaySuccessEntity 转成责任链 Command。GroupBuyTeamEntity。GroupBuyTeamSettlementAggregate。NotifyTaskEntity,在线程池中立即尝试执行一次通知。TradeRepository.settlementMarketPayOrder(...) 才负责具体的数据更新和通知任务落库。可以理解为:
不是每次单个用户支付都一定需要通知下游。仓储根据团队进度和通知配置判断是否创建任务;需要通知时,会将 notify_task 与核心业务状态一起持久化,再返回领域层。
领域服务随后异步调用 tradeTaskService.execNotifyJob(notifyTaskEntity),相当于“落库后立即投递一次”;即使即时投递失败,GroupBuyNotifyJob 仍可扫描本地任务继续补偿。
这是一种本地消息表思路:业务状态和待发送任务先可靠保存,再通过即时触发 + 定时补偿逐步完成外部通知。
支付成功后,结算服务先执行责任链:检查来源渠道黑名单、按外部单号加载订单、拒绝关闭订单,并校验实际支付时间早于团队截止时间。校验通过后,把付款用户、团队快照和支付成功事实组装为结算聚合,由仓储在事务中更新用户订单和团队进度;达到通知条件时同时写入本地通知任务,再异步投递,失败任务由定时任务补偿。
退款不能统一执行同一套 SQL,因为要同时看两个状态:
CREATE / COMPLETE / CLOSE。PROGRESS / COMPLETE / FAIL / COMPLETE_FAIL。不同组合需要减少不同计数、更新不同团队状态,并决定是否恢复 Redis 名额。因此项目使用:
MarketTradeController.refundMarketPayOrder
-> ITradeRefundOrderService
-> TradeRefundOrderService.refundOrder
-> TradeRefundRuleFilterFactory
-> DataNodeFilter
-> UniqueRefundNodeFilter
-> RefundOrderNodeFilter
-> RefundTypeEnumVO
-> IRefundOrderStrategy
-> Unpaid2RefundStrategy / Paid2RefundStrategy / PaidTeam2RefundStrategy
-> GroupBuyRefundAggregate
-> ITradeRepository 对应退款方法
库存异步恢复还要继续读:
退款策略写通知任务
-> TradeTaskService / TradePort 发布 MQ
-> RefundSuccessTopicListener
-> TradeRefundOrderService.restoreTeamLockStock
-> 退款策略.reverseStock
-> ITradeRepository.refund2AddRecovery
输入 TradeRefundCommandEntity
├── userId
├── outTradeNo
├── source
└── channel
上下文 TradeRefundRuleFilterFactory.DynamicContext
├── marketPayOrderEntity
└── groupBuyTeamEntity
输出 TradeRefundBehaviorEntity
├── userId
├── orderId
├── teamId
└── SUCCESS / REPEAT
| 顺序 | 节点 | 职责 | 结果 |
|---|---|---|---|
| 1 | DataNodeFilter | 按用户和外部单号查询用户订单,再按 teamId 查询团队 | 将两个实体写入 Context |
| 2 | UniqueRefundNodeFilter | 判断订单是否已经 CLOSE | 已关闭直接返回 REPEAT,不再进入策略 |
| 3 | RefundOrderNodeFilter | 根据团队状态 + 订单状态选择退款类型和策略 | 执行策略,返回 SUCCESS |
UniqueRefundNodeFilter 是接口层幂等体验的一部分:重复请求不会再次执行回滚,而是返回“重复退款”的业务行为结果。
生产环境仍应保留数据库条件更新、唯一约束等最终防线,不能只依赖一次前置查询防并发。
RefundTypeEnumVO 是退款路由器| 团队状态 | 用户订单状态 | 退款类型 | 策略 Bean |
|---|---|---|---|
PROGRESS | CREATE | UNPAID_UNLOCK | unpaid2RefundStrategy |
PROGRESS | COMPLETE | PAID_UNFORMED | paid2RefundStrategy |
COMPLETE 或 COMPLETE_FAIL | COMPLETE | PAID_FORMED | paidTeam2RefundStrategy |
RefundTypeEnumVO.getRefundStrategy(...) 遍历枚举并调用每个枚举值自己的 matches(...)。枚举中还保存 Spring Bean 名,RefundOrderNodeFilter 再从下面这张 Map 取得对应策略:
Map<String, IRefundOrderStrategy> refundOrderStrategyMap
这张 Map 由 Spring 自动注入,Key 是策略 Bean 名。
| 策略 | 当前场景 | 聚合中的计数变化 | 团队状态变化 | 退款后恢复 Redis 名额 |
|---|---|---|---|---|
Unpaid2RefundStrategy | 未支付、未成团 | lockCount - 1 | 由仓储按当前实现处理 | 是 |
Paid2RefundStrategy | 已支付、未成团 | lockCount - 1、completeCount - 1 | 仍属于未成团逆向 | 是 |
PaidTeam2RefundStrategy | 已支付、已成团 | 两个计数都减 1 | 当前完成数为 1 时转 FAIL,否则转 COMPLETE_FAIL | 否 |
为什么已成团退款不恢复 Redis 团队名额?当前代码的业务解释是:团队已经结束,不再恢复成可继续参团的进行中队伍。
退款策略执行 repository.unpaid2Refund/paid2Refund/paidTeam2Refund 时,负责数据库订单、团队计数、状态和通知任务。
需要恢复团队库存的场景,不是在主事务里直接改 Redis,而是发送退款成功消息。RefundSuccessTopicListener 消费后调用:
restoreTeamLockStock
-> 根据消息 type 找到相同策略
-> strategy.reverseStock(...)
-> repository.refund2AddRecovery(recoveryKey, orderId)
这样 MySQL 核心状态变化与 Redis 恢复通过消息解耦,整体采用最终一致性。要注意:消息可靠投递、重复消费幂等、失败补偿仍是生产系统需要重点验证的部分。
TimeoutRefundJob 调用:
tradeRefundOrderService.queryTimeoutUnpaidOrderList();
拿到超时未支付明细后,为每笔订单构造 TradeRefundCommandEntity,仍然进入同一个 refundOrder(...) 主流程。这样手工退款与定时超时退款复用同一套责任链、幂等判断和策略逻辑。
退款入口先通过责任链按外部单号加载订单和团队,发现订单已关闭就直接返回重复退款,实现接口幂等;否则根据团队状态和用户订单状态,由
RefundTypeEnumVO路由到未支付未成团、已支付未成团或已支付已成团三种策略。策略分别构造退款聚合,由仓储事务更新订单、团队计数和通知任务;需要恢复名额的场景再通过退款成功 MQ,由监听器按相同策略执行 Redis 库存恢复。
ITradeTaskService
-> TradeTaskService
-> ITradeRepository.queryUnExecutedNotifyTaskList
-> ITradePort.groupBuyNotify
-> TradePort
-> EventPublisher 或 GroupBuyNotifyService
-> ITradeRepository.updateNotifyTaskStatusXxx
-> GroupBuyNotifyJob
| 抽象 | 回答的问题 | 当前实现 |
|---|---|---|
ITradeRepository | 交易域的数据和基础设施状态如何查询/保存? | TradeRepository,操作 MySQL、Redis、DCC |
ITradePort | 交易域如何通知外部世界? | TradePort,选择 HTTP 或 MQ |
简单记忆:
当前 ITradeRepository 也承载了一部分 Redis 和 DCC 能力,属于本项目的工程取舍,不必强行说成教科书式纯仓储。
TradeTaskService 的三种执行入口| 方法 | 用途 |
|---|---|
execNotifyJob() | 全量查询待执行任务,供定时任务扫描 |
execNotifyJob(teamId) | 只执行指定团队任务,便于定向补偿 |
execNotifyJob(notifyTaskEntity) | 立即执行刚创建的一条任务 |
执行每条任务时:
port.groupBuyNotify(notifyTask)。SUCCESS:更新任务为成功。ERROR 且次数未超限:更新为重试状态。ERROR 且 notifyCount > 4:更新为最终失败。TradePort 如何发送TradePort 先按任务 lockKey() 获取 Redis 分布式锁,避免多个服务实例同时执行同一任务。
拿到锁后按 notifyType 分流:
HTTP:调用 GroupBuyNotifyService.groupBuyNotify(...)。MQ:调用 EventPublisher.publish(routingKey, parameterJson)。没有抢到锁时返回 NULL,不是直接判定发送失败。
如果数据库事务提交成功后直接发 MQ,进程可能在两者之间宕机,造成“业务已成功,但消息永久丢失”。当前项目把待通知内容写入 notify_task,然后:
事务内:更新业务数据 + 写 notify_task
事务后:立即尝试通知
失败后:GroupBuyNotifyJob 定时扫描重试
这不等于绝对的“恰好一次”。更准确的说法是:依靠任务状态、重试、分布式锁和消费端幂等实现至少一次投递语义下的最终一致性。
| 对比项 | 责任链 | 策略模式 |
|---|---|---|
| 核心问题 | 一次请求要依次经过哪些步骤? | 同一目标有多种算法时选哪一个? |
| 当前项目 | 锁单链、结算链、退款链 | 三种退款策略 |
| 调用数量 | 通常顺序执行多个节点 | 通常选择一个策略执行 |
| 扩展方式 | 增加节点并调整装配顺序 | 新增策略并增加匹配规则 |
| 数据传递 | Command + DynamicContext + FilterBack | 统一策略接口 + 具体聚合 |
在退款流程中两者一起使用:
责任链负责:加载 -> 幂等 -> 路由
策略负责:具体怎么退
面试中可以说,这样避免一个退款方法里出现大量交叉的 if/else,并让数据准备、重复检查和具体回滚逻辑各自独立。
DynamicContext 是一次责任链调用期间的临时共享上下文,不是数据库实体,也不应该跨请求共享。
以结算为例:
SCRuleFilter
暂时不写 Context
↓
OutTradeNoRuleFilter
查询订单并 setMarketPayOrderEntity
↓
SettableRuleFilter
复用订单查询 teamId,再查询团队并 setGroupBuyTeamEntity
↓
EndRuleFilter
读取团队,构造最终返回值
好处:
风险:
SettableRuleFilter 默认前一个节点已经放入订单。阅读时要为每个节点标记两件事:它从 Context 读取什么,它向 Context 写入什么。
| Domain 对象 | 基础设施 PO/数据来源 | 说明 |
|---|---|---|
GroupBuyActivityEntity | GroupBuyActivity | 只映射交易规则需要的活动字段 |
GroupBuyTeamEntity | GroupBuyOrder | 团队主单的领域视图 |
MarketPayOrderEntity | GroupBuyOrderList | 用户参与明细的支付视图 |
NotifyTaskEntity | NotifyTask | 通知任务的领域视图 |
GroupBuyOrderAggregate | 多个 PO | 会拆解到团队主单和用户明细 |
GroupBuyTeamSettlementAggregate | 多表更新 + 通知任务 | 表达一次结算事务输入 |
GroupBuyRefundAggregate | 用户明细 + 团队主单 + 通知任务 | 表达一次退款事务输入 |
为什么不直接让 domain 使用 PO?
stateDiagram-v2
[*] --> CREATE: 锁单成功
CREATE --> COMPLETE: 支付结算成功
CREATE --> CLOSE: 未支付退款/超时关闭
COMPLETE --> CLOSE: 已支付退款
| 状态 | 含义 | 典型入口 |
|---|---|---|
CREATE | 已锁单、待支付 | lockMarketPayOrder |
COMPLETE | 支付完成 | settlementMarketPayOrder |
CLOSE | 已关闭/已退款 | 三种退款仓储方法 |
stateDiagram-v2
[*] --> PROGRESS: 创建团队
PROGRESS --> COMPLETE: 完成人数达到目标
PROGRESS --> FAIL: 逆向后团队失败
COMPLETE --> COMPLETE_FAIL: 成团后发生部分退款
COMPLETE --> FAIL: 仅剩一笔完成单并退款
COMPLETE_FAIL --> COMPLETE_FAIL: 后续已成团订单退款
状态变化的最终 SQL 条件和并发控制要继续查看 TradeRepository 与对应 Mapper,不要只从枚举推断所有细节。
| 类 | 一句话职责 |
|---|---|
ITradeLockOrderService | 对上提供查待支付订单、查团队进度、锁单能力 |
TradeLockOrderService | 执行锁单链、组装聚合、调用仓储并处理落库失败补偿 |
TradeLockRuleFilterFactory | 装配锁单责任链并定义共享上下文和库存 Key |
ITradeSettlementOrderService | 对上提供支付结算能力 |
TradeSettlementOrderService | 执行结算链、组装结算聚合、触发通知 |
TradeSettlementRuleFilterFactory | 装配四节点结算责任链并定义上下文 |
ITradeRefundOrderService | 对上提供退款、库存恢复、超时订单查询能力 |
TradeRefundOrderService | 退款入口编排,并按消息类型重新找到策略恢复库存 |
TradeRefundRuleFilterFactory | 装配退款责任链并定义订单/团队上下文 |
IRefundOrderStrategy | 统一约束退款动作和库存恢复动作 |
AbstractRefundOrderStrategy | 提供仓储、消息发送和恢复库存公共逻辑 |
ITradeTaskService | 定义全量、指定团队、指定任务三种通知执行方式 |
TradeTaskService | 发送通知并更新成功、重试或失败状态 |
| 类 | 类型 | 一句话职责 |
|---|---|---|
ActivityUsabilityRuleFilter | 锁单节点 | 校验活动状态和活动时间 |
UserTakeLimitRuleFilter | 锁单节点 | 校验用户活动参与上限 |
TeamStockOccupyRuleFilter | 锁单节点 | 参团时抢占 Redis 名额 |
SCRuleFilter | 结算节点 | 按来源和渠道执行 DCC 黑名单拦截 |
OutTradeNoRuleFilter | 结算节点 | 加载用户订单并拒绝无效或关闭订单 |
SettableRuleFilter | 结算节点 | 校验支付时间没有超过团队有效期 |
EndRuleFilter | 结算节点 | 将团队 Context 转换为结算链输出 |
DataNodeFilter | 退款节点 | 加载订单和团队数据 |
UniqueRefundNodeFilter | 退款节点 | 对已关闭订单返回重复退款结果 |
RefundOrderNodeFilter | 退款节点 | 识别退款类型并执行具体策略 |
Unpaid2RefundStrategy | 退款策略 | 未支付未成团退款,回滚锁单数,后续恢复名额 |
Paid2RefundStrategy | 退款策略 | 已支付未成团退款,回滚两个计数,后续恢复名额 |
PaidTeam2RefundStrategy | 退款策略 | 已支付已成团退款,调整团队状态,不重新开放名额 |
阅读:
ITrade*Service 接口。ITradeRepository 和 ITradePort,只看方法名。第一轮目标:能画出锁单、结算、退款、通知四条主线。
暂时跳过:
TradeRepository 中的 SQL 细节;为每条责任链画一张表:
| 节点 | 从哪里读 | 向哪里写 |
|---|---|---|
示例:OutTradeNoRuleFilter | Command 中的用户和外部单号 | Context 中的订单实体 |
第二轮目标:看到 next(request, context) 时,知道下一个节点能获得什么数据。
重点查看 TradeRepository 的这些方法:
lockMarketPayOrder
settlementMarketPayOrder
occupyTeamStock
recoveryTeamStock
unpaid2Refund
paid2Refund
paidTeam2Refund
refund2AddRecovery
queryUnExecutedNotifyTaskList
updateNotifyTaskStatusSuccess/Error/Retry
第三轮目标:把 Domain 语义对应到具体表、Redis Key、事务和消息任务。
MarketTradeController.lockMarketPayOrderTradeLockOrderService.lockMarketPayOrderapplyTradeRepository.occupyTeamStockTradeRepository.lockMarketPayOrder观察:outTradeNo、teamId、DynamicContext、recoveryTeamStockKey、userTakeOrderCount。
TradeSettlementOrderService.settlementMarketPayOrderapplyTradeRepository.settlementMarketPayOrderTradeTaskService.execNotifyJobTradePort.groupBuyNotify观察:订单状态、outTradeTime、团队 validEndTime、三个进度字段、返回的 NotifyTaskEntity。
TradeRefundOrderService.refundOrderapplyRefundTypeEnumVO.getRefundStrategyrefundOrderRefundSuccessTopicListenerreverseStock观察:团队状态、订单状态、命中的退款类型、策略 Bean 名、聚合中的 -1 增量、退款消息 type。
lockCount 和 completeCount 有什么区别lockCount:已锁住团队名额的订单数量,包括待支付占位。completeCount:已经完成支付结算的订单数量。targetCount:团队成团目标人数。orderId 和 outTradeNo 有什么区别orderId:本系统生成的用户参与订单标识。outTradeNo:外部业务或支付侧传入的交易单号,也是接口幂等查询的重要条件。这是当前代码的组织方式,不代表只能这样设计。两者都服务于幂等,但落点不同:锁单入口先快速返回已有订单;退款把重复判断作为逆向流程的明确节点。生产设计时可以进一步统一用例层边界和并发防线。
FilterBack:告诉服务“校验完成后得到了什么”
Aggregate:告诉仓储“这次完整业务动作要写什么”
refundOrder 又有 reverseStockrefundOrder:主退款流程,修改数据库业务状态并创建通知任务。reverseStock:退款成功消息到达后,对需要的场景恢复 Redis 团队名额。TradeTaskService 有线程池但主要循环是同步的当前类的核心扫描方法按列表逐条执行;线程池主要由结算服务等上游用于异步触发单条通知。阅读代码时应以实际调用位置为准,不要只看到字段就宣称任务服务内部做了并行批处理。
以下内容是学习和面试中的改进方向,不代表当前项目已经全部实现。
| 关注点 | 当前实现可观察到的做法 | 生产级可继续优化 |
|---|---|---|
| 锁单幂等 | 先按外部单号查询,数据库侧处理重复键异常 | 明确唯一索引、幂等记录和重复请求返回协议 |
| 团队并发 | Redis 库存占用 + 数据库条件更新/事务 | Lua 原子脚本、版本号、压测验证边界并发 |
| 结算幂等 | 查询订单状态并通过仓储更新 | SQL 条件更新 CREATE -> COMPLETE,记录支付事件唯一键 |
| 退款幂等 | CLOSE 前置判断,仓储事务更新 | 退款流水唯一号、状态机 CAS、消费端去重表 |
| 消息一致性 | 本地通知任务 + 即时发送 + 定时补偿 | Outbox 标准化、监控告警、死信队列、人工重放平台 |
| 缓存恢复 | MQ 消费后按订单恢复 | 幂等 Lua、恢复日志、对账任务 |
| 责任链上下文 | 节点共享可变 Context | 增加必填断言、节点契约测试、缩小上下文字段 |
| 策略路由 | 枚举保存 Spring Bean 名 | 启动期校验映射完整性,避免字符串配置错误 |
| 可观测性 | 日志和任务统计 | TraceId、业务指标、团队维度审计轨迹 |
面试表达建议:先说清楚当前代码,再说“如果上生产,我会补充……”。不要把建议方案说成项目已经实现的功能。
trade子域负责拼团交易生命周期,主要分成锁单、支付结算、退款逆向和通知任务四部分。锁单和结算使用责任链拆分规则,退款在责任链基础上再用策略模式处理三种状态组合。每次核心写操作会组装聚合对象,通过ITradeRepository交给基础设施层落到 MySQL、Redis 和本地消息表;外部 HTTP/MQ 通知则通过ITradePort隔离。整体重点是订单状态、团队进度、库存占位和消息通知之间的最终一致性。
一个交易动作往往不是只改一张表。比如锁单同时涉及用户、活动、优惠、团队和用户参与次数;结算同时涉及支付事实和团队当前进度。项目用聚合把一次业务写操作所需的完整上下文打包,再交给仓储在事务中拆解落库,这比让领域服务直接操作多个 DAO 更能保持业务边界清晰。
退款先用责任链加载订单和团队并做重复退款判断,再用团队状态和订单状态识别退款类型,通过 Spring 注入的策略 Map 选择具体策略。未支付未成团只回滚锁单计数,已支付未成团同时回滚完成数,已支付已成团还要调整团队状态。数据库退款完成后写通知任务,再由 MQ 消费端按策略决定是否恢复 Redis 团队名额。
Command、DynamicContext、FilterBack 的区别。SUCCESS 和 REPEAT 退款结果的区别。ITradeRepository 与 ITradePort。如果时间有限,只按下面的顺序读:
第一天:四个 Service 接口和实现
第二天:三个 Factory,记住每条责任链的节点顺序
第三天:三个 Aggregate 和所有 Command/FilterBack
第四天:三种退款策略与 RefundTypeEnumVO
第五天:ITradeRepository -> TradeRepository 核心方法
第六天:TradeTaskService -> ITradePort -> TradePort -> 三个 Trigger
第七天:自己画图并按面试模板口述,不看源码复盘
最终要形成的认识不是“记住很多类名”,而是:
外部请求或任务触发
-> 领域服务编排
-> 责任链准备和校验
-> 必要时选择策略
-> 聚合表达一次业务变更
-> Repository/Port 跨越领域边界
-> MySQL、Redis、MQ、HTTP 完成落地
掌握这条骨架后,再遇到任何一个 trade 类,都能先判断它位于哪一层、属于哪条业务线、负责输入、校验、编排、落库、通知中的哪一步。
知识笔记会随着实践和认知变化持续更新,不代表最终结论。