Jelajahi Sumber

文档:细化 OMG 双支付入口实施计划

补充服务端文件职责、精确接口签名、测试源码步骤、安全审查、统一验证命令与 App 仓库边界。
qmj 1 Minggu lalu
induk
melakukan
4490595046
1 mengubah file dengan 269 tambahan dan 1 penghapusan
  1. 269 1
      specs/020-omg-payment-rebuild/plan.md

+ 269 - 1
specs/020-omg-payment-rebuild/plan.md

@@ -1,6 +1,6 @@
 # OMG AIO 创建支付与付款结果回调重建 Implementation Plan
 
-> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` for inline implementation or `subagent-driven-development` only when the user explicitly requests subagents. Execute [tasks.md](tasks.md) task-by-task; every production behavior must follow Red → Green → Refactor.
+> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` for inline implementation or `subagent-driven-development` only when the user explicitly requests subagents. Execute [tasks.md](tasks.md) task-by-task; steps use checkbox syntax for tracking, and every production behavior must follow Red → Green → Refactor.
 
 **Goal:** 从零实现测试环境 `POST /pay/omg/create` 和 `POST /pay/omg/notify`,按门店独立凭证创建官方 AIO 表单,并以凭证快照可信、幂等地同步最终付款结果。
 
@@ -1223,6 +1223,274 @@ mvn -pl ruoyi-admin -am -DskipTests package
 - OMG stage:分别验证信用卡和 Apple Pay 直接流程;页面不得出现超商快付、AFTEE 或其他渠道选择项。
 - 按项目 OMG 延后验证约束,测试源码与生产代码在同一实施批次完成,Maven、构建和 stage 验收等待全部 OMG 计划功能调整完成后统一执行。
 
+### 可执行范围与文件职责
+
+本计划的可执行批次只覆盖当前 `foodie_server` 仓库。已在 `E:\QtwCode` 查找用户端源码;候选仓库 `CTE/cte_share` 与 `CTE/food-pdy` 都没有规格指定的 `pages/OrderList/buy/omgCheckout.vue`,也没有当前 OMG create/retry 对接代码,因此不得猜测其中任一仓库就是生产 App。App 修改必须在用户确认实际仓库后单独形成精确计划。
+
+服务端批次不修改 Entity、Mapper、数据库表或 `updatesql/sql.md`。文件职责如下:
+
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentMethod.java`:定义 App 白名单枚举、严格解析规则与 OMG 单渠道映射。
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentMethodTest.java`:锁定大小写、空白和未知值拒绝规则。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentRequest.java`:只增加 `paymentMethod` 字段及访问器。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgRetryPaymentRequest.java`:只增加 `paymentMethod` 字段及访问器。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentErrorCode.java`:增加稳定错误 `PAYMENT_METHOD_INVALID`。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java`:token 验证后解析白名单枚举,并传递给 create/retry Service。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateService.java`:让首次创建和精确替换都携带已验证枚举。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentRetryService.java`:在查询或写入前拒绝空枚举,并把重新选择的渠道传给新表单。
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java`:按枚举生成互斥的信用卡或 Apple Pay 字段并签名。
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java`、`OmgPaymentControllerTest.java`、`OmgPaymentRetryControllerTest.java`、`OmgPaymentCreateServiceTest.java`、`OmgPaymentRetryServiceTest.java`:覆盖 DTO 契约、无副作用拒绝、参数贯穿和签名字段集合。
+- Modify: `ruoyi-admin/src/main/resources/i18n/messages.properties`、`messages_zh_CN.properties`、`messages_zh_TW.properties`、`messages_en_US.properties`、`messages_vi.properties`:提供同一错误 key 的五套文案。
+- Modify: `specs/020-omg-payment-rebuild/tasks.md`:实现提交后只更新 T105/T106,统一验证和 App 任务保持未完成。
+
+### Task 1: create/retry 支付渠道白名单服务端批次
+
+**Interfaces:**
+
+- Consumes: create/retry JSON `{orderId, paymentMethod}`,其中 `paymentMethod` 只能是精确字符串 `CREDIT` 或 `APPLE_PAY`。
+- Produces: `OmgPaymentMethod.require(String)`;`OmgPaymentCreateService#create(Long, String, OmgPaymentMethod)`;`OmgPaymentCreateService#replaceActiveForRetry(Long, String, String, OmgPaymentMethod)`;`OmgPaymentRetryService#retry(Long, String, OmgPaymentMethod)`;`OmgPaymentFormFactory#create(String, Integer, String, String, String, String, OmgPaymentMethod)`。
+- Security invariant: 客户端不能提交 `ChoosePayment`、`UnionPay`、`IgnorePayment` 或其他 OMG 原始字段;非法 `paymentMethod` 在 query、订单锁、尝试创建或替换前失败。
+
+- [ ] **Step 1: 先写枚举、表单和 Controller 失败测试源码,但按 OMG 延后验证规则暂不运行**
+
+`OmgPaymentMethodTest` 必须包含以下精确断言:
+
+```java
+assertEquals(OmgPaymentMethod.CREDIT, OmgPaymentMethod.require("CREDIT"));
+assertEquals(OmgPaymentMethod.APPLE_PAY, OmgPaymentMethod.require("APPLE_PAY"));
+for (String invalid : new String[]{"", "credit", "Credit", "ALL", " CREDIT "}) {
+    OmgPaymentBusinessException error = assertThrows(
+            OmgPaymentBusinessException.class,
+            () -> OmgPaymentMethod.require(invalid));
+    assertEquals(OmgPaymentErrorCode.PAYMENT_METHOD_INVALID, error.getCode());
+}
+assertEquals(OmgPaymentErrorCode.PAYMENT_METHOD_INVALID,
+        assertThrows(OmgPaymentBusinessException.class,
+                () -> OmgPaymentMethod.require(null)).getCode());
+```
+
+`OmgPaymentFormFactoryTest` 将旧的 `ALL + IgnorePayment` 用例拆成两个用例:
+
+```java
+OmgPaymentForm credit = factory.create("DD-1", 100, "1000031", "KEY", "IV",
+        "OMG12345678901234567", OmgPaymentMethod.CREDIT);
+assertEquals("Credit", credit.fields().get("ChoosePayment"));
+assertEquals("2", credit.fields().get("UnionPay"));
+assertFalse(credit.fields().containsKey("IgnorePayment"));
+
+OmgPaymentForm applePay = factory.create("DD-1", 100, "1000031", "KEY", "IV",
+        "OMG12345678901234567", OmgPaymentMethod.APPLE_PAY);
+assertEquals("ApplePay", applePay.fields().get("ChoosePayment"));
+assertFalse(applePay.fields().containsKey("UnionPay"));
+assertFalse(applePay.fields().containsKey("IgnorePayment"));
+```
+
+同时捕获 signer 入参:信用卡签名前字段数为 14,包含 `ChoosePayment=Credit`、`UnionPay=2`;Apple Pay 签名前字段数为 13,包含 `ChoosePayment=ApplePay` 且不含 `UnionPay/IgnorePayment/CheckMacValue`。
+
+Controller 测试把 DTO 字段集合断言改为:
+
+```java
+assertEquals(Set.of("orderId", "paymentMethod"),
+        Arrays.stream(OmgCreatePaymentRequest.class.getDeclaredFields())
+                .map(Field::getName).collect(Collectors.toSet()));
+```
+
+create/retry 成功用例分别设置 `CREDIT`、`APPLE_PAY` 并验证 Service 收到领域枚举;缺失、`ALL`、`Credit` 和带空白值返回 `PAYMENT_METHOD_INVALID`,且 `verifyNoInteractions(createService/retryService)`。
+
+- [ ] **Step 2: 写 Service 参数贯穿与无副作用测试源码,但暂不运行**
+
+将现有 create Service 测试调用统一增加 `OmgPaymentMethod.CREDIT`,并验证工厂参数:
+
+```java
+verify(formFactory).create(eq("DD-1"), eq(100), eq("1000031"), eq("KEY"), eq("IV"),
+        anyString(), eq(OmgPaymentMethod.CREDIT));
+```
+
+新增 null 防御用例,要求在任何依赖交互前失败:
+
+```java
+OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+        () -> service.create(5L, "DD-1", null));
+assertEquals(PAYMENT_METHOD_INVALID, error.getCode());
+verifyNoInteractions(attempts, credentials, generator, formFactory);
+```
+
+retry Service 的 `FAILED` 与可替换 `UNPAID` 用例分别验证:
+
+```java
+verify(createService).create(5L, "DD-1", OmgPaymentMethod.APPLE_PAY);
+verify(createService).replaceActiveForRetry(5L, "DD-1", "OMGOLD",
+        OmgPaymentMethod.CREDIT);
+```
+
+retry 的 null 防御必须在 `queryService.query(...)` 前返回 `PAYMENT_METHOD_INVALID`,并断言 `verifyNoInteractions(queryService, createService)`。原有 `PAID/UNKNOWN/paymentType 非空/并发变化` 断言保持不变,只补充枚举参数。
+
+- [ ] **Step 3: 实现严格枚举、DTO 字段、稳定错误和五语言文案**
+
+`OmgPaymentMethod.java` 使用以下完整领域边界:
+
+```java
+package com.ruoyi.app.omgpay;
+
+/** Maps the App payment-method allowlist to one OMG hosted payment channel. */
+public enum OmgPaymentMethod {
+    CREDIT("Credit", true),
+    APPLE_PAY("ApplePay", false);
+
+    private final String choosePayment;
+    private final boolean unionPayDisabled;
+
+    OmgPaymentMethod(String choosePayment, boolean unionPayDisabled) {
+        this.choosePayment = choosePayment;
+        this.unionPayDisabled = unionPayDisabled;
+    }
+
+    public String getChoosePayment() {
+        return choosePayment;
+    }
+
+    public boolean isUnionPayDisabled() {
+        return unionPayDisabled;
+    }
+
+    public static OmgPaymentMethod require(String value) {
+        try {
+            return value == null ? invalid() : valueOf(value);
+        } catch (IllegalArgumentException error) {
+            return invalid();
+        }
+    }
+
+    private static OmgPaymentMethod invalid() {
+        throw new OmgPaymentBusinessException(OmgPaymentErrorCode.PAYMENT_METHOD_INVALID);
+    }
+}
+```
+
+两个请求 DTO 都只增加:
+
+```java
+private String paymentMethod;
+
+public String getPaymentMethod() {
+    return paymentMethod;
+}
+
+public void setPaymentMethod(String paymentMethod) {
+    this.paymentMethod = paymentMethod;
+}
+```
+
+错误枚举增加 `PAYMENT_METHOD_INVALID("omg.pay.payment.method.invalid")`。五个 properties 文件使用同一 key:
+
+```properties
+# messages.properties / messages_zh_CN.properties
+omg.pay.payment.method.invalid=请选择信用卡或 Apple Pay
+# messages_zh_TW.properties
+omg.pay.payment.method.invalid=請選擇信用卡或 Apple Pay
+# messages_en_US.properties
+omg.pay.payment.method.invalid=Please select Credit Card or Apple Pay
+# messages_vi.properties
+omg.pay.payment.method.invalid=Vui lòng chọn Thẻ tín dụng hoặc Apple Pay
+```
+
+- [ ] **Step 4: 让 Controller、create/retry Service 和表单工厂只传递领域枚举**
+
+Controller 在 token 解析成功后执行白名单解析,再调用 Service:
+
+```java
+String paymentMethodValue = request == null ? null : request.getPaymentMethod();
+safeUserId = tokenUserResolver.requireUserId(token);
+OmgPaymentMethod paymentMethod = OmgPaymentMethod.require(paymentMethodValue);
+OmgPaymentCreateOutcome outcome = createService.create(safeUserId, orderId, paymentMethod);
+```
+
+retry 同样调用 `retryService.retry(safeUserId, orderId, paymentMethod)`。不得把原始 `paymentMethodValue` 写入日志或传入表单工厂。
+
+create Service 在 `create(...)` 和 `replaceActiveForRetry(...)` 的第一条业务判断调用统一的 `requirePaymentMethod(paymentMethod)`,然后把枚举传入 `createWithBoundedTradeNumberRetries(...)`:
+
+```java
+private static void requirePaymentMethod(OmgPaymentMethod paymentMethod) {
+    if (paymentMethod == null) {
+        throw business(PAYMENT_METHOD_INVALID);
+    }
+}
+```
+
+retry Service 在调用 query 前执行同样的 null 判断并抛出 `new OmgPaymentBusinessException(PAYMENT_METHOD_INVALID)`,再在 `FAILED`、可替换 `UNPAID` 两条路径传递原枚举。
+
+表单工厂签名追加 `OmgPaymentMethod paymentMethod`,删除旧固定字段并改为:
+
+```java
+fields.put("ChoosePayment", paymentMethod.getChoosePayment());
+if (paymentMethod.isUnionPayDisabled()) {
+    fields.put("UnionPay", "2");
+}
+```
+
+`IgnorePayment` 和 `ChoosePayment=ALL` 不得出现在生产代码。签名器继续接收表单实际字段的不可变副本,现有字段安全清洗、URL 校验和响应不可变性保持不变。
+
+- [ ] **Step 5: 做一次统一静态审查,不运行 Maven、编译或测试**
+
+```powershell
+rg -n 'ChoosePayment|IgnorePayment|UnionPay|paymentMethod|PAYMENT_METHOD_INVALID' `
+  ruoyi-admin/src/main/java/com/ruoyi/app/omgpay `
+  ruoyi-admin/src/test/java/com/ruoyi/app/omgpay `
+  ruoyi-admin/src/main/resources/i18n
+rg -n 'omg.pay.payment.method.invalid' `
+  ruoyi-admin/src/main/resources/i18n/messages.properties `
+  ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties `
+  ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties `
+  ruoyi-admin/src/main/resources/i18n/messages_en_US.properties `
+  ruoyi-admin/src/main/resources/i18n/messages_vi.properties
+git diff --check
+```
+
+人工确认:生产代码没有 `ChoosePayment=ALL` 或 `IgnorePayment`;只有信用卡分支包含 `UnionPay=2`;客户端原始字符串只在 Controller 白名单解析边界出现;日志、错误响应和测试失败信息不包含 token、HashKey、HashIV、CheckMacValue 或完整表单;无 Entity、Mapper、SQL 或数据库文件变化。
+
+- [ ] **Step 6: 更新任务状态、核对暂存范围并形成一个中文实现提交**
+
+仅在生产代码、测试源码、五语言 i18n 和文档静态审查完成后,把 T105/T106 标记为完成;T107/T108/T109 保持未完成。使用 PowerShell 数组暂存本批次明确文件,运行 `git diff --cached --name-only` 和 `git diff --cached --check`,确认不包含 `.claude/homunculus/*` 或其他用户脏文件,然后提交:
+
+```powershell
+git commit -m "实现 OMG 信用卡与 Apple Pay 双入口"
+```
+
+提交后状态只能报告为“已提交”,不能报告“已验证”。
+
+### Task 2: 全部 OMG 功能完成后的统一验证
+
+**Interfaces:**
+
+- Consumes: Task 1 的独立实现提交与项目现有 OMG 全部功能。
+- Produces: 定向测试、完整 OMG 回归、模块构建和 stage 双渠道人工证据。
+
+- [ ] **Step 1: 使用 JDK 21 运行渠道定向测试**
+
+```powershell
+$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
+$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
+mvn -pl ruoyi-admin -am "-Dtest=com.ruoyi.app.omgpay.OmgPaymentMethodTest,com.ruoyi.app.omgpay.OmgPaymentFormFactoryTest,com.ruoyi.app.omgpay.OmgPaymentControllerTest,com.ruoyi.app.omgpay.OmgPaymentRetryControllerTest,com.ruoyi.app.omgpay.OmgPaymentCreateServiceTest,com.ruoyi.app.omgpay.OmgPaymentRetryServiceTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
+```
+
+Expected: Maven 退出码 0,六个指定测试类全部通过;信用卡签名前 14 个字段,Apple Pay 签名前 13 个字段。
+
+- [ ] **Step 2: 运行 OMG 回归与模块构建**
+
+```powershell
+mvn -pl ruoyi-admin -am "-Dtest=com.ruoyi.app.omgpay.*Test,com.ruoyi.system.omgpay.*Test" "-Dsurefire.failIfNoSpecifiedTests=false" test
+mvn -pl ruoyi-admin -am -DskipTests package
+```
+
+Expected: 两条命令退出码均为 0,构建输出 `BUILD SUCCESS`。
+
+- [ ] **Step 3: 执行 OMG stage 双渠道验收**
+
+按 [quickstart.md](quickstart.md) 分别发送 `paymentMethod=CREDIT` 与 `paymentMethod=APPLE_PAY`。确认信用卡表单只含 `ChoosePayment=Credit + UnionPay=2`,Apple Pay 表单只含 `ChoosePayment=ApplePay`;两个页面均不出现超商快付、AFTEE 或其他渠道选择。Apple Pay 若因门店未开通或设备不支持而失败,记录为外部环境限制,不把信用卡成功当作 Apple Pay 验收证据。
+
+- [ ] **Step 4: 更新 T108/T109 并提交验证记录**
+
+只有上述自动化、构建和人工证据全部检查后,才能把功能状态报告为“已验证”。若 App 仍未接入,服务端最多报告为“已验证”,完整用户功能仍不得宣称完成。
+
 ## 2026-08-14 retry 查询稀疏响应修复计划
 
 **Goal:** 修复用户连续点击支付时,retry 查询到已签名但省略非核心字段的 OMG 响应而错误返回 `PAYMENT_QUERY_FAILED`。