plan.md 15 KB

Implementation Plan: 平台订单状态受控调整

Branch: test(沿用当前分支,不创建新分支) | Date: 2026-08-10 | Spec: spec.md

Input: Feature specification from /specs/018-order-status-admin/spec.md

Summary

平台订单管理改为使用状态专用接口,不再把完整 PosOrder 对象提交给通用编辑接口。修改弹窗先读取服务端计算的状态上下文与允许操作,再通过旧状态快照执行 CAS 更新:订单状态和配送状态只允许合法前向流转;线下支付通过确认收款/确认退款维护;OMG 支付通过现有查询补单和真实退款流水维护。所有成功的人工操作同步记录管理员、前后状态和原因。

本功能同时修复两个已经确认的状态不变量缺口:骑手送达必须同步 deliveryStatus=3state=3sdTime 并执行幂等完成结算;OMG 退款成功必须同步 payStatus=2afterSaleStatus=3state=4。不新增数据库表或字段。

Technical Context

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_orderpos_order_logpos_order_omg_paymentpos_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 个管理端状态接口;无数据库迁移

Constitution Check

GATE: Phase 0 前检查;Phase 1 设计完成后复查。

项目 .specify/memory/constitution.md 仍是未初始化的占位模板,无法提供可执行条款。本功能以根目录 CLAUDE.md、管理后台 CLAUDE.md、006 订单状态规格和 016 OMG 支付规格作为实际质量门槛。

Gate 结论 处理
使用当前有效订单/支付代码 PASS 只修改 PosOrderControllerPosOrderQsOprateControllerOmgPayController 及其现有服务;不接入废弃支付通道
支付与退款真实性 PASS OMG 只允许回调/查询核销和真实退款;线下操作必须显式确认并审计
输入最小化与权限 PASS 新接口使用专用 DTO、@Validsystem:order:edit,不接收金额/用户/商品等字段
并发与幂等 PASS 订单使用四状态旧快照 CAS;OMG 继续使用支付流水 CAS;完成账单只在成功进入完成态后执行
数据库变更规范 PASS 无表结构变更,不修改 updatesql/sql.md
前端国际化 PASS 新增及本页触及的状态文案同步简中、繁中、英文、越南文
测试优先 PASS 先写策略/服务/OMG 回归测试,再实现接口和页面
变更范围 PASS 不创建分支,不修改与订单状态无关的功能

Phase 1 复查:设计未引入新的数据库、支付渠道、权限体系或跨模块反向依赖,所有 Gate 继续通过。

Project Structure

Documentation (this feature)

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 生成,本阶段不创建。

Source Code (repository root)

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 重构。

Phase 0: Research Outcome

详细结论见 research.md。核心决策如下:

  1. 增加 GET /system/order/{id}/status-context,由后端返回当前快照和允许操作,前端不自行复制完整状态机。
  2. 本地状态更新使用 state + deliveryStatus + payStatus + afterSaleStatus 旧值组成的无模式变更 CAS;更新行数为 0 时要求刷新。
  3. 配送/完成、线下收款和线下退款集中到 OrderLifecycleService,同步写 OrderLogHelper.logSync
  4. OMG 补单继续复用 reconcileByQuery 的金额、交易号和流水 CAS;管理员不能直接提交 payStatus=1
  5. OMG 退款继续先将支付流水从已支付 CAS 为退款中,再调用网关;成功后统一完成订单/售后状态,超时则保留退款中等待核对;人工待办经管理员核实后使用同一流水 CAS 完成。
  6. payType="1" 是本期唯一可人工确认的线下支付方式;payType="7" 是 OMG;历史在线类型不得按线下订单处理。
  7. 不新增审计表;原因限制 1~200 字,结构化前后值写入现有 512 字订单日志内容。

Phase 1: Design

1. 状态上下文与能力判定

状态弹窗不再调用通用订单详情作为可提交表单。新上下文接口返回:

  • 订单标识、类型、支付类型、当前四状态、送达时间、是否已有骑手;
  • 允许的下一订单状态和配送状态;
  • 是否允许确认线下收款、线下退款、OMG 补单、OMG 退款;
  • OMG 流水是否退款中或等待人工退款。

能力字段只是界面提示,所有写接口仍重新读取订单并执行同样校验,不能依赖前端隐藏按钮实现安全控制。

2. 专用请求模型

所有写请求携带:

  • expectedState
  • expectedDeliveryStatus(允许为 null);
  • expectedPayStatus
  • expectedAfterSaleStatus
  • reason(去首尾空格后 1~200 字)。

订单/配送修改额外携带可空的 targetStatetargetDeliveryStatus,至少一个目标值发生变化。支付操作不接收目标支付状态,目标由服务端动作固定决定。

3. 状态策略

  • 订单普通流转:0→1→2→3;取消仅 0/1→4
  • 已支付订单不能通过普通状态接口取消;必须走对应退款动作。
  • state=3/4payStatus=2afterSaleStatus>0 禁止普通状态调整。
  • 外送完成要求 payStatus=1deliveryStatus=3;配送改为 3 时同步完成。
  • 自取/堂食完成要求 payStatus=1,配送状态始终为 null
  • 配送普通流转:null→0→1→2→3null→0 仅在外送且 state=2;进入 1/2 要求已有骑手,本期不提供指派骑手。
  • 相同目标视为幂等无变化,不重复生成日志、账单或推送。

完整矩阵见 data-model.md

4. 原子更新和完成副作用

OrderLifecycleService 先根据数据库最新值校验请求快照,再使用 MyBatis-Plus 条件更新:

WHERE id = :id
  AND state = :expectedState
  AND pay_status = :expectedPayStatus
  AND after_sale_status = :expectedAfterSaleStatus
  AND delivery_status <=> :expectedDeliveryStatus

Java 实现对可空配送状态分别使用 isNulleq,不拼接 SQL。只有 CAS 成功进入 state=3 的调用才生成商家/骑手账单;账单继续使用现有按订单检查。状态、账单和同步审计日志处于同一 Spring 事务。

骑手送达改调用同一完成方法,从而补齐 state=3 和完成账单,同时保留现有到付用户账单处理。原有骑手锁保留,CAS 作为支付回调、定时任务和其他节点并发时的最终保护。

5. 支付和退款

线下

  • 确认收款:仅 payType="1"payStatus=0、非取消/退款订单;CAS 为 payStatus=1
  • 确认退款:仅 payType="1"payStatus=1state!=3afterSaleStatus=0;二次确认后 CAS 为 payStatus=2, afterSaleStatus=3, state=4,并复用现有积分返还幂等能力。

OMG

  • 补单:管理端先校验快照和 payType="7",再调用 reconcileByQuery(ddId, "admin");支付流水和订单核销仍由现有 CAS 决定。
  • 退款:已完成订单先拒绝。信用卡退款成功后同步三个业务状态;不支持 API 的支付方式只记录一次人工待办,订单仍保持已支付;管理员在 OMG 后台完成退款后,可基于该待办二次确认,流水通过 1→4→3 CAS 后同步三个业务状态;网关超时保留 OMG 流水退款中并禁止重复发起。
  • 外部 HTTP 调用不包在长事务中。网关成功后业务状态同步失败时记录高优先级同步日志,后续只允许补偿状态,禁止再次退款。

6. 旧接口加固

PUT /system/order 删除旧状态值 7、12 和 kefuState→state=5 分支。请求只要包含 statedeliveryStatuspayStatusafterSaleStatussdTime 即拒绝,并提示使用状态专用接口,防止旧页面或构造请求绕过规则。其他历史非状态编辑能力保持原样。

7. 管理后台

  • 修改弹窗只保存状态上下文、目标值、旧快照和原因,不再把完整订单对象回传。
  • 订单/配送下拉框只展示服务端返回的允许值;终态只读。
  • 支付状态始终只读,根据能力显示“确认线下收款”“查询 OMG 支付”“确认线下退款”“发起 OMG 退款”“确认 OMG 人工退款完成”等按钮。
  • 退款使用二次确认;提交期间按钮 loading,阻止重复点击;成功后重新读取详情和列表。
  • 将本页订单、配送、支付、售后状态文案统一改为四语言 $t(),避免新弹窗出现中文硬编码。

8. API Contract

接口、请求和响应示例见 contracts/admin-order-status-api.md。所有写接口复用 system:order:edit 权限,并使用 @RepeatSubmit 限制短时间重复操作。

Implementation Sequence

  1. 先编写状态策略、CAS、完成副作用和 Controller 合约测试,确认失败。
  2. 实现 DTO、状态上下文和 OrderLifecycleService
  3. 增加管理端状态/线下支付接口,并加固旧 PUT /system/order
  4. 让骑手送达复用统一完成逻辑,补充状态和账单回归测试。
  5. 扩展 OMG 管理补单/退款结果处理,补齐退款后三状态一致性和人工待办幂等。
  6. 修改管理后台 API、弹窗、操作按钮和四语言文案。
  7. 执行后端测试、编译、前端 lint/build 及 quickstart.md 手工场景。

Verification Strategy

Automated

  • 状态策略单元测试覆盖全部合法/非法转移、类型限制、售后和终态。
  • 服务测试验证 CAS 条件、CAS 失败无副作用、完成只结算一次、原因日志内容。
  • Controller 测试验证权限注解、参数校验和额外业务字段无法进入请求 DTO。
  • OMG 测试覆盖补单金额不符、回调/补单幂等、退款成功、人工待办、超时未知和重复退款。
  • 执行 mvn -pl ruoyi-admin -am test 与前端 lint/build。

Manual

  • 外送 2/2/1/0 → 3/3/1/0(订单/配送/支付/售后)并检查送达时间、账单、日志。
  • 自取/堂食线下收款后完成。
  • OMG 漏回调补单;OMG 信用卡退款;延期支付人工退款待办。
  • 旧快照并发提交、非法回退、已完成退款、自取修改配送、空原因全部拒绝。
  • 四种语言检查无原始 key 和中文泄漏。

Rollout and Rollback

  • 后端状态接口与前端页面同批发布;先部署后端可保证旧前端仍能读取订单,但旧状态提交会收到明确拒绝。
  • 无数据库迁移,回滚只需回退后端和前端代码。
  • 发布后重点观察:CAS 冲突、OMG 人工待办、退款结果未知、退款成功但业务状态同步失败日志。

Risks

风险 缓解措施
OMG 网关成功但本地状态失败 流水先保留真实退款状态,记录高优先级日志,仅做本地补偿,不重复退款
完成路径并发重复结算 状态 CAS 成功者才执行完成副作用,现有账单按订单幂等检查兜底
管理员页面陈旧覆盖回调/骑手结果 四状态快照 CAS,冲突时强制刷新
历史在线支付被误当线下 线下动作只允许 payType="1",不是简单判断 !=7
无独立审计字段 使用同步订单日志,原因上限 200 字,内容包含四状态前后值
Constitution 未初始化 在本计划显式列出并执行 CLAUDE.md 与既有规格门槛

Complexity Tracking

无 Constitution 违规需要豁免。新增状态上下文接口和生命周期服务是隔离完整实体更新、统一状态不变量所需的最小结构。