# Implementation Plan: 平台订单状态受控调整 **Branch**: `test`(沿用当前分支,不创建新分支) | **Date**: 2026-08-10 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/018-order-status-admin/spec.md` ## Summary 平台订单管理改为使用状态专用接口,不再把完整 `PosOrder` 对象提交给通用编辑接口。修改弹窗先读取服务端计算的状态上下文与允许操作,再通过旧状态快照执行 CAS 更新:订单状态和配送状态只允许合法前向流转;线下支付通过确认收款/确认退款维护;OMG 支付通过现有查询补单和真实退款流水维护。所有成功的人工操作同步记录管理员、前后状态和原因。 本功能同时修复两个已经确认的状态不变量缺口:骑手送达必须同步 `deliveryStatus=3`、`state=3`、`sdTime` 并执行幂等完成结算;OMG 退款成功必须同步 `payStatus=2`、`afterSaleStatus=3`、`state=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_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 个管理端状态接口;无数据库迁移 ## Constitution Check *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 继续通过。 ## Project Structure ### Documentation (this feature) ```text 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) ```text 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](./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 字)。 订单/配送修改额外携带可空的 `targetState`、`targetDeliveryStatus`,至少一个目标值发生变化。支付操作不接收目标支付状态,目标由服务端动作固定决定。 ### 3. 状态策略 - 订单普通流转:`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](./data-model.md)。 ### 4. 原子更新和完成副作用 `OrderLifecycleService` 先根据数据库最新值校验请求快照,再使用 MyBatis-Plus 条件更新: ```text 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 作为支付回调、定时任务和其他节点并发时的最终保护。 ### 5. 支付和退款 #### 线下 - 确认收款:仅 `payType="1"`、`payStatus=0`、非取消/退款订单;CAS 为 `payStatus=1`。 - 确认退款:仅 `payType="1"`、`payStatus=1`、`state!=3`、`afterSaleStatus=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` 分支。请求只要包含 `state`、`deliveryStatus`、`payStatus`、`afterSaleStatus` 或 `sdTime` 即拒绝,并提示使用状态专用接口,防止旧页面或构造请求绕过规则。其他历史非状态编辑能力保持原样。 ### 7. 管理后台 - 修改弹窗只保存状态上下文、目标值、旧快照和原因,不再把完整订单对象回传。 - 订单/配送下拉框只展示服务端返回的允许值;终态只读。 - 支付状态始终只读,根据能力显示“确认线下收款”“查询 OMG 支付”“确认线下退款”“发起 OMG 退款”“确认 OMG 人工退款完成”等按钮。 - 退款使用二次确认;提交期间按钮 loading,阻止重复点击;成功后重新读取详情和列表。 - 将本页订单、配送、支付、售后状态文案统一改为四语言 `$t()`,避免新弹窗出现中文硬编码。 ### 8. API Contract 接口、请求和响应示例见 [contracts/admin-order-status-api.md](./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](./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 违规需要豁免。新增状态上下文接口和生命周期服务是隔离完整实体更新、统一状态不变量所需的最小结构。