|
|
@@ -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`。
|