本文基于本人在负责项目订单模块进行分析。
目标是尝试分析如果用 DDD 重构,在保留现有单体架构的前提下,是否能够解决订单模型职责混杂、状态流转分散,以及渠道回调幂等边界不清的问题。
一、问题不是“订单表字段多”,而是变化没有边界
在多数业务系统中,订单都会逐渐变复杂:客户资料、地址、产品、配送、来源渠道、外部订单号、审核资料、状态日志、回调结果都会围绕它出现。复杂本身并不可怕;真正危险的是,这些信息因为“都和订单有关”而不断被塞进同一个实体、同一张表、同一个更新接口。
最初设计的 订单 正处在这个阶段:实体本身约有 50 个字段,并同时承载客户、地址、组织归属、外呼、隐私号、营销、审核和履约状态。与此同时,代码中还有 12 个以 Order 开头的关联模型,覆盖订单项、物流、图片、视频、通话、历史、状态日志、来源和公共抢单等能力。
这不是简单的“表设计不规范”,而是一个更值得讨论的信号:订单边界正在被多个业务上下文共同使用,但系统还没有明确地表达这些上下文。
本文讨论三个问题:
- 当前订单模型里混在一起的职责究竟有哪些?
- 为什么状态校验、锁和日志都存在,状态一致性仍然存在风险?
- 是否应该引入 DDD;如果应该,如何避免一次高风险重构?
二、从代码还原当前订单模型
当前订单主实体不是一个纯粹的“交易订单”,而是多个业务阶段的共享载体。把字段按变化原因重新分类,可以得到下面这张图。
各位大佬如果觉得设计的垃圾,请轻点喷,喷的同时给出一些意见,本人半个科班出身

围绕主表的子模型又包含了更多不同性质的数据:
| 现有模型 | 当前承载的主要信息 | 更接近的业务含义 |
|---|---|---|
OrderItem |
产品、直播间、物流单号、运营商意向单号、运营商订单号、首充时间 | 商品快照 + 渠道履约信息 |
OrderExpress |
物流原始数据 | 物流跟踪 |
OrderImage / OrderVideo |
合规图片、视频和存储元数据 | 资料与合规 |
OrderCall |
通话记录 | 客户触达 |
OrderHistory |
来源、类型、人员和状态快照 | 变更历史 |
OrderStatusLog |
前后状态、操作者、备注 | 状态审计 |
SysTelecomPushLog / 查单日志 |
外部请求与结果 | 渠道执行审计 |
这张表格揭示了一个关键事实:系统已经在用多个实体表达不同概念,只是这些概念尚未以清晰的领域边界组织起来。
2.1 一张订单表中混入了四类变化
从变更频率和责任归属看,至少可以分出四类数据:
- 订单事实:客户是谁、收货地址是什么、订单来自哪里、何时创建。
- 履约过程:谁负责配送、是否接单、是否改约、何时完成。
- 渠道执行:向哪个运营商提交、渠道单号是什么、渠道返回什么状态、是否需要重试。
- 资料与审核:身份证识别、通话录音、合规图片、订单视频。
它们都围绕订单,但变化原因不同:修改地址不应影响渠道重试;补一张合规图不应触发配送状态迁移;运营商回调不应直接理解为客户履约完成。
如果这些差异只靠 Service 名称和开发者经验维持,系统规模一大,修改一个字段时就很难判断应该更新主表、订单项、渠道日志,还是资料表。
三、当前状态机制:已有保护,但规则没有收口
当前订单状态使用整数 :待分配、待外呼、待配送、配送中、失败、改约、成功、未接电话和客户号码异常。它覆盖了从外呼到配送再到终态的主线。
我在维护系统过程中并非没有治理,以下是我做的一些设计与维护:
- 人工更新订单时会先读取当前状态,并通过
订单Validator做角色化的流转校验。 - 抢单和自动分派等场景使用
WHERE status = 待分配的条件更新,避免两个配送员同时抢到同一订单。 - 状态日志通过
@LogOrderStatus切面记录操作者、原状态和动作信息。 - 外部渠道查单结果应用器会识别本地成功/失败终态,避免常规查单重复推进状态。
- 部分渠道推单使用分布式锁,降低重复提交风险。
这些措施解决了很多实际问题,但它们分布在 Controller、Service、任务、回调和切面中。不同入口拥有不同保护强度,导致系统的真实状态规则不是一张表,而是一组分散的代码约定。

3.1 一个 status 承担了多条生命周期
这里我在后面Review代码的时候发现了一个被忽视的问题是:当前订单状态字段同时表达了至少三种不同的流程。这是我在后续维护上给我造成了许多困扰
| 生命周期 | 典型状态或动作 | 真正关心的问题 |
|---|---|---|
| 客户触达 | 待外呼、未接电话、号码异常 | 客户是否可联系、是否需要再次触达 |
| 人员分派 | 待分配、待配送、抢单、分派 | 谁拥有履约责任、是否允许抢单 |
| 配送履约 | 配送中、成功、失败、改约 | 服务是否实际完成、是否需要改约或结束 |
| 渠道处理 | 推单成功、意向单、回调、查单 | 外部系统是否接受、是否开卡、是否可重试 |
这些流程有关联,但并不等价。例如:
- 渠道“已受理”通常只能说明外部单已创建,不必然代表本地订单已经“配送中”。
- 客户“未接电话”是触达结果,未必等价于履约失败。
- “改约”可能是履约过程中的暂停状态,也可能是一个需要重新安排的子流程。
- 渠道开卡成功和配送成功在某些业务中一致,在另一些业务中却是两个不同事实。
当所有语义都压缩进一个整数状态时,后续只能不断添加例外:某个角色能否回退、某个渠道能否覆盖、某个任务在什么状态下运行。例外越多,状态机越像一套难以完整描述的权限规则,而不是领域规则。
3.2 现有更新方式仍有并发窗口
人工更新入口会先读取订单、校验状态,再按订单 ID 执行更新。这能避免很多非法操作,但如果两个请求在同一时刻读到相同旧状态,它们都可能通过校验;除非最终 SQL 再带上旧状态或版本号,否则后写入者仍可能覆盖先写入者。
渠道路径的风险更明显:运营商推单确认成功后,会将订单直接更新为“配送中”。这条路径本身合理,但如果此时订单已被人工处理为失败、成功或改约,延迟到达的渠道结果可能反向覆盖本地更晚的业务事实。
外部渠道查单结果应用器已经加入“终态不再更新”的意图,这是非常好的方向;但要把它升级为真正的并发保障,最终更新 SQL 也应携带状态条件或乐观锁版本,而不是只根据读取时的对象判断。
乐观锁机制我在初学的时候时常作为八股文进行背诵和理解,到我真实实践的时候总觉有必要。
3.3 状态日志不能替代状态机
状态日志解决的是“发生过什么”,状态机解决的是“什么可以发生”。
我设计的初衷是因为一件比较头疼的事情,就是订单状态流转无法溯源,导致定责和恢复困难,于是我设计了状态流转表,记录每次多方人员操作订单的记录,最初就是这么想的,没想到后续这个状态流转日志在业务线上帮了我解决很多运营上的问题,是我比较惊喜的一个设计和方案。在这个表设计后 因为运营人员频繁错误的操作,导致订单被回收,批量误操作,导致订单状态错乱,这个时候我们就通过这张表配合日志TraceId定位进行了批量的恢复,但是在恢复数据这种场景依旧没有形成一个好的解决方案,还是使用状态日志 写SQL方式回写,这也是我想优化的。
还有一点是,AOP切面日志对人工接口很有效,但渠道回调、定时任务、批处理或底层 Wrapper 直接更新不一定会经过同一个注解入口。日志可以作为审计补偿,却不应该成为状态规则的唯一承载方式。
四、幂等性:锁只是其中一层
我自己看过很多文章,自己也有实践,我实践过程中发现订单领域经常把“加锁”和“幂等”混为一谈。
锁只解决同一时刻的并发竞争,幂等还要解决重复请求、超时重试、消息重放、回调重复和跨天补偿。
对于我负责的系统,在研发和维护过程中迭代了至少存在四层幂等需求:
- 创建幂等:相同外部订单号不能重复创建本地订单。
- 渠道提交幂等:同一订单向同一渠道不能因为重试产生多张外部单。
- 回调消费幂等:同一渠道回调重复到达时,只能产生一次业务效果。
- 状态迁移幂等:已经成功或失败的订单,不应被相同结果再次推进,更不应被过期结果回退。

这套模型的重点不是多建几张表,而是明确每层的唯一键:
| 场景 | 建议唯一键 |
|---|---|
| 外部订单导入 | 来源 + 外部订单号 |
| 渠道提单 | 订单号 + 渠道码 + 场景码 |
| 渠道回调 | 渠道码+ eventId;无 eventId 时使用外部单号 + 状态 + 时间窗口 |
| 状态更新 | orderId + version 或 orderId + expectedStatus |
| 批量任务行处理 | taskId + rowNo 或业务幂等键 |
五、DDD 是否适合这个系统
为什么我想要引入DDD一方面是我觉得系统维护到后面有点混沌,随着AI Coding的时代到来,都用上了智能体编码, 实体很混乱,想着引入DDD是否能解决问题
学习研究结论是:适合引入 DDD 的建模思想,但不适合现在就按“完整 DDD + 微服务”推倒重来。
当然我相信大佬们可以把这套系统可以重构成DDD,但是我对于DDD缺乏实践层面的经验和技术,只是初步分析,对于DDD我也是停留在纸上谈兵的阶段
AI的意见是系统当前最需要的是把“领域规则”从杂糅的更新逻辑中提炼出来,而不是引入更多抽象层。
最现实的方式是:继续使用 Spring Boot、MyBatis、现有数据库和 Controller,但按业务边界组织模块,并让关键状态规则只有一个入口。
5.1 建议的限界上下文

| 上下文 | 应负责什么 | 不应继续负责什么 |
|---|---|---|
| 订单 | 客户快照、收货地址、来源、订单基本事实 | 保存所有渠道单号和渠道状态 |
| 履约 | 分派、抢单、配送、改约、完成/失败 | 组装第三方渠道请求 |
| 渠道集成 | 提单、查单、回调、重试、渠道订单状态 | 直接绕过订单规则覆盖主状态 |
| 产品与资源 | 产品、套餐、库存/现量、首充映射 | 决定配送员归属 |
| 资料与合规 | 图片、视频、OCR、录音和审核结论 | 改写订单履约状态 |
| 组织与权限 | 代理商、成员、数据范围 | 持有订单生命周期规则 |
5.2 聚合如何划分
其实说简单点就是 继续拆分上下文,实体,但是这样会让系统复杂度更高了,梳理起来很头疼
AI建议是不要把所有表都塞入一个超大聚合。更适合的拆法是:
- Order(订单聚合):订单基本事实、当前业务状态、状态版本号;只允许通过领域方法改变核心状态。
- Fulfillment(履约聚合或履约模块):配送员归属、接单、改约、物流和完成结果。
- ChannelOrder(渠道履约单):渠道代码、外部订单号/意向单号、渠道状态、请求幂等键、最后回调时间、失败原因、重试信息。
- ComplianceAsset(合规资料):图片、视频、OCR/录音结果,与订单关联但独立演进。
其中最优先的新模型是 ChannelOrder。它可以逐步承接目前散落在 Order、OrderItem、推单日志和查单日志里的外部系统状态,而不要求一次迁移所有老字段。
六、目标状态机:从“设置整数”转向“执行命令”
状态机的目标不是禁止所有回退,而是把回退、人工修正、渠道同步都变成显式业务动作。
后台运营/管理员可以有修正权限,但它不应等价于可以随意 setStatus。
TypeError: Cannot read properties of undefined (reading 'x')
目前代码是没有的,还是依赖更新方法,以至于膨胀到一个方法几百行,过错过错
在代码层,关键不再是暴露一个可写入任意字段的 updateStatus,而是建立清晰命令:
order.assignTo(agent, operator);
order.acceptBy(agent, operator);
order.startDelivery(operator);
order.complete(completionEvidence, operator);
order.fail(failureReason, operator);
order.reschedule(appointment, operator);
每个方法都负责:
- 校验当前状态是否允许迁移。
- 校验操作者和归属关系。
- 更新状态、业务时间和必要快照。
- 产生
OrderStatusChanged领域事件。 - 通过版本号或旧状态条件保证并发下只有一个迁移成功。
渠道回调不再直接执行 setStatus(配送中),而是先更新 ChannelOrder,再根据渠道状态与本地履约状态的映射,发起明确命令,例如 confirmChannelAccepted 或 completeByChannel。
七、渐进式迁移:不推倒重来
DDD 重构最危险的方式,是先画出完美模型,再要求一次替换所有表和接口。对正在运行的订单系统,这种首先是很危险的,其次做完了需要做大量的回归测试验证,目前团队人数有限,很难做到,更可靠的路线是增量迁移。
阶段一:先统一语言和状态规则
- 建立状态迁移矩阵:每个状态允许哪些命令、哪些角色、哪些前置条件。
- 将状态常量升级为枚举或值对象,统一状态文本、终态判断和可迁移判断。
- 为人工修改、抢单、分派、渠道回调、定时同步补充状态机测试。
阶段二:建立统一状态迁移服务
- 保留旧接口,但将核心状态修改逐步委托给
OrderTransitionService。 - 更新 SQL 增加
version或expectedStatus条件。 - 所有成功迁移统一发布领域事件,日志从“接口注解附带能力”转为“迁移结果必然产物”。
阶段三:新增渠道履约单,不急于删除旧字段
- 新建
channel_order,保存渠道、外部单号、渠道状态、请求幂等键、最后结果和版本。 - 新渠道优先使用新表;运营商、外部渠道等老渠道先双写并校验一致性。
- 逐步把订单项中渠道专属单号迁移为只读兼容字段。
阶段四:把查询与写入分开演进
- 写模型遵守领域边界:订单、履约、渠道单各自更新。
- 列表和详情可以继续通过 MyBatis 聚合查询,必要时建立读模型或视图。
- 不为了“纯 DDD”牺牲现有报表和运营查询效率。
阶段五:补齐可靠事件与回调基础设施
- 渠道回调引入收件箱去重记录。
- 订单状态事件使用 Outbox 或可靠任务投递。
- 重试基于渠道履约单状态,而不是前端重复点击或临时日志判断。
八、结语:DDD 的价值是把不确定性关进边界里
这个订单系统当前最宝贵的地方,是已经积累了大量真实业务规则:分派保护、角色校验、渠道锁、状态日志、查单补偿、合规资料和资源同步。问题不在于这些规则不存在,而在于它们分散在不同技术入口中。
因此,DDD 在这里的价值不是制造更多类和接口,而是回答四个必须清楚的问题:
- 这次变化属于订单、履约、渠道还是合规?
- 谁可以触发这次变化,前置状态是什么?
- 重复消息和并发请求到来时,哪个结果应该生效?
- 变化发生后,日志、资源、通知和统计如何可靠地跟随?
当这些问题都由清晰的模型、命令、状态机和幂等键表达时,订单系统才会从“能不断加功能”走向“可以持续演进”。