Branch: test(沿用当前分支,不创建新分支) | Date: 2026-08-10 | Spec: spec.md
Input: Feature specification from /specs/018-order-status-admin/spec.md
平台订单管理改为使用状态专用接口,不再把完整 PosOrder 对象提交给通用编辑接口。修改弹窗先读取服务端计算的状态上下文与允许操作,再通过旧状态快照执行 CAS 更新:订单状态和配送状态只允许合法前向流转;线下支付通过确认收款/确认退款维护;OMG 支付通过现有查询补单和真实退款流水维护。所有成功的人工操作同步记录管理员、前后状态和原因。
本功能同时修复两个已经确认的状态不变量缺口:骑手送达必须同步 deliveryStatus=3、state=3、sdTime 并执行幂等完成结算;OMG 退款成功必须同步 payStatus=2、afterSaleStatus=3、state=4。不新增数据库表或字段。
Language/Version: Java 21;JavaScript(Vue 2.6)
Primary Dependencies: Spring Boot 3.3.5、RuoYi-Vue 3.8.5、Spring Security、MyBatis-Plus/MyBatis XML、Redisson、Vue 2.6、Element UI 2.15、Axios 0.24、Vue I18n 8
Storage: MySQL;复用 pos_order、pos_order_log、pos_order_omg_payment、pos_order_omg_refund,无 DDL 变更
Testing: JUnit 5、Mockito、Spring Boot Test;前端 ESLint、生产构建和人工界面验证
Target Platform: RuoYi Java Web 服务及桌面浏览器管理后台
Project Type: 后端多模块 Web 服务 + 独立 Vue 管理后台
Performance Goals: 本地状态操作只读取单笔订单并执行单行条件更新;OMG 查询/退款不增加额外网关调用;订单列表查询不新增关联查询
Constraints: 在线支付状态必须以 OMG 事实为准;禁止完整实体批量赋值;外部支付请求不得持有长数据库事务;状态/账单/日志必须幂等;四语言文案齐全;不创建分支
Scale/Scope: 1 个订单管理页面、1 个现有订单 Controller、2 条现有履约/支付链路、7 个管理端状态接口;无数据库迁移
GATE: Phase 0 前检查;Phase 1 设计完成后复查。
项目 .specify/memory/constitution.md 仍是未初始化的占位模板,无法提供可执行条款。本功能以根目录 CLAUDE.md、管理后台 CLAUDE.md、006 订单状态规格和 016 OMG 支付规格作为实际质量门槛。
| Gate | 结论 | 处理 |
|---|---|---|
| 使用当前有效订单/支付代码 | PASS | 只修改 PosOrderController、PosOrderQsOprateController、OmgPayController 及其现有服务;不接入废弃支付通道 |
| 支付与退款真实性 | PASS | OMG 只允许回调/查询核销和真实退款;线下操作必须显式确认并审计 |
| 输入最小化与权限 | PASS | 新接口使用专用 DTO、@Valid、system:order:edit,不接收金额/用户/商品等字段 |
| 并发与幂等 | PASS | 订单使用四状态旧快照 CAS;OMG 继续使用支付流水 CAS;完成账单只在成功进入完成态后执行 |
| 数据库变更规范 | PASS | 无表结构变更,不修改 updatesql/sql.md |
| 前端国际化 | PASS | 新增及本页触及的状态文案同步简中、繁中、英文、越南文 |
| 测试优先 | PASS | 先写策略/服务/OMG 回归测试,再实现接口和页面 |
| 变更范围 | PASS | 不创建分支,不修改与订单状态无关的功能 |
Phase 1 复查:设计未引入新的数据库、支付渠道、权限体系或跨模块反向依赖,所有 Gate 继续通过。
specs/018-order-status-admin/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
└── contracts/
└── admin-order-status-api.md
tasks.md 由下一阶段 /speckit.tasks 生成,本阶段不创建。
foodie_server/
├── ruoyi-admin/src/main/java/com/ruoyi/app/order/
│ ├── PosOrderController.java # 管理端状态接口、旧 PUT 接口加固
│ ├── PosOrderQsOprateController.java # 骑手送达改走统一完成逻辑
│ ├── OrderLifecycleService.java # 新增:状态策略、CAS、完成/线下支付/退款
│ └── dto/
│ ├── AdminOrderActionRequest.java # 新增:旧状态快照 + 原因
│ ├── AdminOrderStatusUpdateRequest.java # 新增:目标订单/配送状态
│ └── AdminOrderStatusContext.java # 新增:当前状态与允许操作
├── ruoyi-admin/src/main/java/com/ruoyi/app/pay/
│ ├── OmgPayController.java # 复用补单/退款核心,补齐退款状态不变量
│ └── dto/
│ └── OmgRefundOutcome.java # 新增:成功/人工待办/结果未知/失败
├── ruoyi-admin/src/test/java/com/ruoyi/app/order/
│ ├── OrderLifecycleServiceTest.java # 新增
│ └── PosOrderAdminStatusControllerTest.java # 新增
└── ruoyi-admin/src/test/java/com/ruoyi/app/pay/
└── OmgPayControllerTest.java # 扩展退款与补单回归
foodie-admin-vue/
└── src/
├── api/system/order.js # 新增状态上下文和动作 API
├── views/system/order/index.vue # 受控修改弹窗与支付动作
└── api/language/
├── language.zh_CN.js
├── language.zh_TW.js
├── language.en_US.js
└── language.vi.js
Structure Decision: 状态不变量和本地事务放在 OrderLifecycleService,Controller 仅做权限、参数绑定与响应转换。OMG 不复制网关逻辑,继续复用现有 OmgPayController.reconcileByQuery 和退款流水服务;本期用明确结果对象替代管理员侧解析错误字符串,不进行整套 OMG Controller 重构。
详细结论见 research.md。核心决策如下:
GET /system/order/{id}/status-context,由后端返回当前快照和允许操作,前端不自行复制完整状态机。state + deliveryStatus + payStatus + afterSaleStatus 旧值组成的无模式变更 CAS;更新行数为 0 时要求刷新。OrderLifecycleService,同步写 OrderLogHelper.logSync。reconcileByQuery 的金额、交易号和流水 CAS;管理员不能直接提交 payStatus=1。payType="1" 是本期唯一可人工确认的线下支付方式;payType="7" 是 OMG;历史在线类型不得按线下订单处理。状态弹窗不再调用通用订单详情作为可提交表单。新上下文接口返回:
能力字段只是界面提示,所有写接口仍重新读取订单并执行同样校验,不能依赖前端隐藏按钮实现安全控制。
所有写请求携带:
expectedState;expectedDeliveryStatus(允许为 null);expectedPayStatus;expectedAfterSaleStatus;reason(去首尾空格后 1~200 字)。订单/配送修改额外携带可空的 targetState、targetDeliveryStatus,至少一个目标值发生变化。支付操作不接收目标支付状态,目标由服务端动作固定决定。
0→1→2→3;取消仅 0/1→4。state=3/4、payStatus=2 或 afterSaleStatus>0 禁止普通状态调整。payStatus=1 且 deliveryStatus=3;配送改为 3 时同步完成。payStatus=1,配送状态始终为 null。null→0→1→2→3;null→0 仅在外送且 state=2;进入 1/2 要求已有骑手,本期不提供指派骑手。完整矩阵见 data-model.md。
OrderLifecycleService 先根据数据库最新值校验请求快照,再使用 MyBatis-Plus 条件更新:
WHERE id = :id
AND state = :expectedState
AND pay_status = :expectedPayStatus
AND after_sale_status = :expectedAfterSaleStatus
AND delivery_status <=> :expectedDeliveryStatus
Java 实现对可空配送状态分别使用 isNull 或 eq,不拼接 SQL。只有 CAS 成功进入 state=3 的调用才生成商家/骑手账单;账单继续使用现有按订单检查。状态、账单和同步审计日志处于同一 Spring 事务。
骑手送达改调用同一完成方法,从而补齐 state=3 和完成账单,同时保留现有到付用户账单处理。原有骑手锁保留,CAS 作为支付回调、定时任务和其他节点并发时的最终保护。
payType="1"、payStatus=0、非取消/退款订单;CAS 为 payStatus=1。payType="1"、payStatus=1、state!=3、afterSaleStatus=0;二次确认后 CAS 为 payStatus=2, afterSaleStatus=3, state=4,并复用现有积分返还幂等能力。payType="7",再调用 reconcileByQuery(ddId, "admin");支付流水和订单核销仍由现有 CAS 决定。1→4→3 CAS 后同步三个业务状态;网关超时保留 OMG 流水退款中并禁止重复发起。PUT /system/order 删除旧状态值 7、12 和 kefuState→state=5 分支。请求只要包含 state、deliveryStatus、payStatus、afterSaleStatus 或 sdTime 即拒绝,并提示使用状态专用接口,防止旧页面或构造请求绕过规则。其他历史非状态编辑能力保持原样。
$t(),避免新弹窗出现中文硬编码。接口、请求和响应示例见 contracts/admin-order-status-api.md。所有写接口复用 system:order:edit 权限,并使用 @RepeatSubmit 限制短时间重复操作。
OrderLifecycleService。PUT /system/order。mvn -pl ruoyi-admin -am test 与前端 lint/build。2/2/1/0 → 3/3/1/0(订单/配送/支付/售后)并检查送达时间、账单、日志。| 风险 | 缓解措施 |
|---|---|
| OMG 网关成功但本地状态失败 | 流水先保留真实退款状态,记录高优先级日志,仅做本地补偿,不重复退款 |
| 完成路径并发重复结算 | 状态 CAS 成功者才执行完成副作用,现有账单按订单幂等检查兜底 |
| 管理员页面陈旧覆盖回调/骑手结果 | 四状态快照 CAS,冲突时强制刷新 |
| 历史在线支付被误当线下 | 线下动作只允许 payType="1",不是简单判断 !=7 |
| 无独立审计字段 | 使用同步订单日志,原因上限 200 字,内容包含四状态前后值 |
| Constitution 未初始化 | 在本计划显式列出并执行 CLAUDE.md 与既有规格门槛 |
无 Constitution 违规需要豁免。新增状态上下文接口和生命周期服务是隔离完整实体更新、统一状态不变量所需的最小结构。