|
@@ -0,0 +1,1069 @@
|
|
|
|
|
+# 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.
|
|
|
|
|
+
|
|
|
|
|
+**Goal:** 从零实现测试环境 `POST /pay/omg/create`,按门店独立凭证生成官方 AIO 签名表单,以新尝试表阻止重复支付入口,并彻底退役旧 OMG 支付/回调/补单/退款运行时。
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** `ruoyi-system/com.ruoyi.system.omgpay` 只负责新尝试表、锁定订单快照和 MyBatis 持久化;`ruoyi-admin/com.ruoyi.app.omgpay` 负责 token 用户解析、业务校验、交易号、官方检查码、表单组装、事务编排、Controller 与安全日志。创建流程不调用 OMG HTTP:事务内 `SELECT pos_order ... FOR UPDATE` 后检查活跃尝试,生成并落库 `CREATED`,提交后由 Controller 返回表单。旧 `pos_store_omg` 凭证查询保持不变,其余旧 OMG 支付代码不得被新流程引用。
|
|
|
|
|
+
|
|
|
|
|
+**Tech Stack:** Java 21、Spring Boot 3、MyBatis/MyBatis-Plus、MySQL、JUnit 5、Mockito、SLF4J/Logback、Maven Surefire。
|
|
|
|
|
+
|
|
|
|
|
+## Global Constraints
|
|
|
|
|
+
|
|
|
|
|
+- OMG AIO 官方技术文件 V1.5.3 是支付协议唯一外部事实来源;不得从旧代码或 `specs/016-omg-payment` 复制行为。
|
|
|
|
|
+- 只实现创建支付订单;回调、取号通知、查询、补单、退款、关账和正式环境全部不实现。
|
|
|
|
|
+- 新代码只能位于 `com.ruoyi.app.omgpay`、`com.ruoyi.system.omgpay` 及对应资源/测试目录。
|
|
|
|
|
+- `ruoyi-admin -> ruoyi-system`;`ruoyi-system` 禁止导入 `com.ruoyi.app.*`。
|
|
|
|
|
+- 每个门店独立使用 `pos_store_omg` 中已启用的 `MerchantID / HashKey / HashIV`;不修改可信凭证存储与管理代码。
|
|
|
|
|
+- 新创建入口固定 `POST /pay/omg/create`;Controller 使用 `@RequestHeader String token`、显式 `@RequestBody` DTO,禁止 Map 入参和 Bean Validation。
|
|
|
|
|
+- 客户端请求只含 `orderId`;金额、门店、网关、ReturnURL、说明、支付方式和签名全部由服务端产生。
|
|
|
|
|
+- `ChoosePayment=ALL`、`NeedExtraPaidInfo=Y`;所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
|
|
|
|
|
+- 后续回调必须对全部实际返回字段验签,包含额外字段与空值字段;本阶段只保留规格约束,不注册回调处理器。
|
|
|
|
|
+- 网关固定 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`,不得配置正式环境回退。
|
|
|
|
|
+- 同一 `ddId` 最多一条 `CREATED`;存在时返回 `PAYMENT_ATTEMPT_EXISTS`,不重放旧表单、不生成新 MerchantTradeNo。
|
|
|
|
|
+- DDL 只追加到 `updatesql/sql.md`,绝不连接数据库执行。
|
|
|
|
|
+- 所有业务错误使用 `MessageUtils.message(...)`;新增 key 同步 default、`zh_CN`、`zh_TW`、`en_US`、`vi` 五个 properties 文件。
|
|
|
|
|
+- 日志不得包含 token、HashKey、HashIV、完整 CheckMacValue、完整表单、DTO 或凭证对象;非预期异常必须以 ERROR 记录异常对象和堆栈。
|
|
|
|
|
+- 新公开类型、检查码编码、事务行锁与唯一约束必须有必要注释;禁止显然代码噪声注释。
|
|
|
|
|
+- JDK 21 只在当前命令环境设置 `JAVA_HOME`/`PATH`;不修改全局配置。
|
|
|
|
|
+- 工作区已有未提交修改。执行前逐文件读取现有 diff;不 reset、不覆盖、不提交与本规格无关的修改。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Summary
|
|
|
|
|
+
|
|
|
|
|
+### Create transaction
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+HTTP token + {orderId}
|
|
|
|
|
+ -> OmgPaymentController resolves user
|
|
|
|
|
+ -> OmgPaymentCreateService @Transactional
|
|
|
|
|
+ -> SELECT pos_order by dd_id FOR UPDATE
|
|
|
|
|
+ -> validate owner/single-store/state/payStatus/payType/amount
|
|
|
|
|
+ -> SELECT active pos_order_omg_attempt
|
|
|
|
|
+ -> read trusted enabled store credential
|
|
|
|
|
+ -> generate 20-char MerchantTradeNo
|
|
|
|
|
+ -> build all AIO fields
|
|
|
|
|
+ -> sign every actual non-CheckMacValue field
|
|
|
|
|
+ -> INSERT CREATED attempt
|
|
|
|
|
+ -> transaction commits
|
|
|
|
|
+ -> log safe success context
|
|
|
|
|
+ -> AjaxResult.success({status,gatewayUrl,formFields})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Duplicate create
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+second request waits on the same pos_order row lock
|
|
|
|
|
+ -> first transaction commits
|
|
|
|
|
+ -> second request sees active_dd_id
|
|
|
|
|
+ -> PAYMENT_ATTEMPT_EXISTS
|
|
|
|
|
+ -> no generator call, no form replay, no second insert
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Failure contract
|
|
|
|
|
+
|
|
|
|
|
+Business failures throw `OmgPaymentBusinessException` carrying `OmgPaymentErrorCode` and an i18n key. Controller logs the stable code and safe context, then returns `AjaxResult.error(localizedMessage, new OmgPaymentErrorResponse(code.name()))`. Unexpected exceptions are logged with the throwable and mapped to `PAYMENT_CREATION_FAILED`; internal message, SQL and stack are never returned.
|
|
|
|
|
+
|
|
|
|
|
+## Technical Context
|
|
|
|
|
+
|
|
|
|
|
+**Language/Version**: Java 21
|
|
|
|
|
+**Primary Dependencies**: Spring Boot 3.3.5、MyBatis/MyBatis-Plus、Hutool、SLF4J、JUnit 5/Mockito
|
|
|
|
|
+**Storage**: MySQL InnoDB;新增 `pos_order_omg_attempt`,复用 `pos_order` 与 `pos_store_omg`
|
|
|
|
|
+**Testing**: Maven Surefire;纯单元测试 + Mapper/DDL 合约测试 + 手动 stage/并发验收
|
|
|
|
|
+**Target Platform**: Windows 开发,Spring Boot 服务部署,多实例安全由数据库锁/唯一键保证
|
|
|
|
|
+**Project Type**: 多模块后端 web service
|
|
|
|
|
+**Performance Goals**: 创建流程无网关 HTTP;正常路径固定次数 DB 访问,目标 <1 秒
|
|
|
|
|
+**Constraints**: stage-only、整数 TWD、MerchantTradeNo ≤20 ASCII 英数字、无旧表依赖、无敏感日志
|
|
|
|
|
+**Scale/Scope**: 用户自己的单门店餐饮订单;一个创建端点、一张新尝试表
|
|
|
|
|
+
|
|
|
|
|
+**Branch/Date/Spec**: `test` | 2026-08-13 | [spec.md](spec.md)
|
|
|
|
|
+
|
|
|
|
|
+## Constitution Check
|
|
|
|
|
+
|
|
|
|
|
+`.specify/memory/constitution.md` 仍是占位模板,因此以根目录 `AGENTS.md` 与已批准规格为门禁:
|
|
|
|
|
+
|
|
|
|
|
+- [x] Controller 显式 header/body DTO,无 Map 入参、无 Bean Validation。
|
|
|
|
|
+- [x] admin→system 依赖方向保持;system 只含 domain/mapper/service。
|
|
|
|
|
+- [x] DDL 只写 `updatesql/sql.md`。
|
|
|
|
|
+- [x] 支付、输入、凭证、日志通过 security-review 门禁。
|
|
|
|
|
+- [x] 新表使用 InnoDB、参数化 MyBatis、生成列唯一键和 `FOR UPDATE`。
|
|
|
|
|
+- [x] TDD,定向测试后再实现。
|
|
|
|
|
+- [x] 只保留 `pos_store_omg` 可信能力,旧支付/退款表运行时引用清零。
|
|
|
|
|
+- [x] 不实现回调、查询、退款或生产环境。
|
|
|
|
|
+
|
|
|
|
|
+结论:无须复杂性豁免。
|
|
|
|
|
+
|
|
|
|
|
+## Project Structure
|
|
|
|
|
+
|
|
|
|
|
+### Documentation
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+specs/020-omg-payment-rebuild/
|
|
|
|
|
+├── spec.md
|
|
|
|
|
+├── plan.md
|
|
|
|
|
+├── research.md
|
|
|
|
|
+├── data-model.md
|
|
|
|
|
+├── quickstart.md
|
|
|
|
|
+├── tasks.md
|
|
|
|
|
+└── contracts/
|
|
|
|
|
+ └── api.md
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### New source files
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/omgpay/
|
|
|
|
|
+├── domain/
|
|
|
|
|
+│ ├── OmgPaymentAttempt.java
|
|
|
|
|
+│ └── OmgPaymentOrderSnapshot.java
|
|
|
|
|
+├── mapper/
|
|
|
|
|
+│ └── OmgPaymentAttemptMapper.java
|
|
|
|
|
+└── service/
|
|
|
|
|
+ ├── IOmgPaymentAttemptService.java
|
|
|
|
|
+ └── impl/
|
|
|
|
|
+ └── OmgPaymentAttemptServiceImpl.java
|
|
|
|
|
+
|
|
|
|
|
+ruoyi-system/src/main/resources/mapper/omgpay/
|
|
|
|
|
+└── OmgPaymentAttemptMapper.xml
|
|
|
|
|
+
|
|
|
|
|
+ruoyi-system/src/test/java/com/ruoyi/system/omgpay/
|
|
|
|
|
+├── mapper/OmgPaymentAttemptMapperContractTest.java
|
|
|
|
|
+└── service/OmgPaymentAttemptServiceTest.java
|
|
|
|
|
+
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/
|
|
|
|
|
+├── OmgPaymentController.java
|
|
|
|
|
+├── OmgPaymentCreateService.java
|
|
|
|
|
+├── OmgPaymentCreateOutcome.java
|
|
|
|
|
+├── OmgPaymentProperties.java
|
|
|
|
|
+├── OmgPaymentTokenUserResolver.java
|
|
|
|
|
+├── OmgPaymentForm.java
|
|
|
|
|
+├── OmgPaymentFormFactory.java
|
|
|
|
|
+├── OmgCheckMacSigner.java
|
|
|
|
|
+├── OmgMerchantTradeNoGenerator.java
|
|
|
|
|
+├── OmgPaymentBusinessException.java
|
|
|
|
|
+├── OmgPaymentErrorCode.java
|
|
|
|
|
+└── dto/
|
|
|
|
|
+ ├── OmgCreatePaymentRequest.java
|
|
|
|
|
+ ├── OmgCreatePaymentResponse.java
|
|
|
|
|
+ └── OmgPaymentErrorResponse.java
|
|
|
|
|
+
|
|
|
|
|
+ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/
|
|
|
|
|
+├── OmgCheckMacSignerTest.java
|
|
|
|
|
+├── OmgMerchantTradeNoGeneratorTest.java
|
|
|
|
|
+├── OmgPaymentFormFactoryTest.java
|
|
|
|
|
+├── OmgPaymentCreateServiceTest.java
|
|
|
|
|
+├── OmgPaymentControllerTest.java
|
|
|
|
|
+└── OmgLegacyRetirementTest.java
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Modified files
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+ruoyi-admin/src/main/resources/application.yml
|
|
|
|
|
+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
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java
|
|
|
|
|
+ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderLifecycleServiceTest.java
|
|
|
|
|
+updatesql/sql.md
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`UserOrderController.java`、`OrderLifecycleService.java` 和 `OrderLifecycleServiceTest.java` 当前已有用户未提交改动;只删除明确的旧 OMG 调用/依赖,并在最终 diff 中确认其余变化完整保留。
|
|
|
|
|
+
|
|
|
|
|
+After old-table retirement, `OrderLifecycleService` constructors become:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public OrderLifecycleService(IPosOrderService posOrderService,
|
|
|
|
|
+ OrderService billingService,
|
|
|
|
|
+ OrderLogHelper orderLogHelper,
|
|
|
|
|
+ IUserWalletService userWalletService,
|
|
|
|
|
+ IPointsTransactionService pointsTransactionService) {
|
|
|
|
|
+ this(posOrderService, billingService, orderLogHelper, userWalletService,
|
|
|
|
|
+ pointsTransactionService, null, null);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Autowired
|
|
|
|
|
+public OrderLifecycleService(IPosOrderService posOrderService,
|
|
|
|
|
+ OrderService billingService,
|
|
|
|
|
+ OrderLogHelper orderLogHelper,
|
|
|
|
|
+ IUserWalletService userWalletService,
|
|
|
|
|
+ IPointsTransactionService pointsTransactionService,
|
|
|
|
|
+ IPosOrderLinePaymentService linePaymentService,
|
|
|
|
|
+ IPosOrderLineRefundService lineRefundService) { ... }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+All updated tests use one of these signatures; no OMG payment/refund Service argument remains.
|
|
|
|
|
+
|
|
|
|
|
+### Retired old files
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgCallbackRequest.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgOrderRequest.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgRefundOutcome.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/task/OmgReconcileTask.java
|
|
|
|
|
+ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgQueryThrottle.java
|
|
|
|
|
+ruoyi-admin/src/test/java/com/ruoyi/app/pay/OmgPayControllerTest.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgPaymentService.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgRefundService.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImpl.java
|
|
|
|
|
+ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImpl.java
|
|
|
|
|
+ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgPaymentMapper.xml
|
|
|
|
|
+ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgRefundMapper.xml
|
|
|
|
|
+ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImplTest.java
|
|
|
|
|
+ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImplTest.java
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+保留 `com.ruoyi.app.utils.omg.OmgPay/OmgPayConfig/OmgCheckMacValue`,因为现有可信门店凭证管理 Controller 仍使用它们做录入时验证;新 `omgpay` 包通过测试保证对这些旧类零引用。
|
|
|
|
|
+
|
|
|
|
|
+## Component Interfaces
|
|
|
|
|
+
|
|
|
|
|
+### Persistence
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public interface OmgPaymentAttemptMapper {
|
|
|
|
|
+ OmgPaymentOrderSnapshot selectOrderForUpdate(@Param("ddId") String ddId);
|
|
|
|
|
+ OmgPaymentAttempt selectActiveByDdId(@Param("ddId") String ddId);
|
|
|
|
|
+ OmgPaymentAttempt selectByMerchantTradeNo(@Param("merchantTradeNo") String merchantTradeNo);
|
|
|
|
|
+ int insertCreated(OmgPaymentAttempt attempt);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public interface IOmgPaymentAttemptService {
|
|
|
|
|
+ OmgPaymentOrderSnapshot lockOrder(String ddId);
|
|
|
|
|
+ OmgPaymentAttempt getActiveByDdId(String ddId);
|
|
|
|
|
+ OmgPaymentAttempt getByMerchantTradeNo(String merchantTradeNo);
|
|
|
|
|
+ OmgPaymentAttempt createCreated(String ddId, String merchantTradeNo,
|
|
|
|
|
+ Long storeId, String merchantId, Integer amount);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Signing and form
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Component
|
|
|
|
|
+public final class OmgCheckMacSigner {
|
|
|
|
|
+ public String sign(Map<String, String> fields, String hashKey, String hashIv);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Component
|
|
|
|
|
+public final class OmgMerchantTradeNoGenerator {
|
|
|
|
|
+ public String generate(); // "OMG" + 17 uppercase alphanumeric characters
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Component
|
|
|
|
|
+public final class OmgPaymentFormFactory {
|
|
|
|
|
+ public static final String STAGE_GATEWAY_URL =
|
|
|
|
|
+ "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5";
|
|
|
|
|
+
|
|
|
|
|
+ public OmgPaymentForm create(String orderId, Integer amount,
|
|
|
|
|
+ String merchantId, String hashKey, String hashIv,
|
|
|
|
|
+ String merchantTradeNo);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public record OmgPaymentForm(String gatewayUrl, Map<String, String> fields) {}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+The form factory creates a new ordered map containing exactly these fields before signature:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+fields.put("MerchantID", merchantId);
|
|
|
|
|
+fields.put("MerchantTradeNo", merchantTradeNo);
|
|
|
|
|
+fields.put("MerchantTradeDate", taipeiNow);
|
|
|
|
|
+fields.put("PaymentType", "aio");
|
|
|
|
|
+fields.put("TotalAmount", String.valueOf(amount));
|
|
|
|
|
+fields.put("TradeDesc", "Foodie order " + safeOrderId);
|
|
|
|
|
+fields.put("ItemName", "Order " + safeOrderId);
|
|
|
|
|
+fields.put("ReturnURL", properties.requireSafeReturnUrl());
|
|
|
|
|
+fields.put("ChoosePayment", "ALL");
|
|
|
|
|
+fields.put("EncryptType", "1");
|
|
|
|
|
+fields.put("InvoiceMark", "N");
|
|
|
|
|
+fields.put("NeedExtraPaidInfo", "Y");
|
|
|
|
|
+fields.put("ExpireDate", "1");
|
|
|
|
|
+fields.put("StoreExpireDate", "30");
|
|
|
|
|
+fields.put("BarcodeATMExpireDate", "1");
|
|
|
|
|
+fields.put("CheckMacValue", signer.sign(fields, hashKey, hashIv));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Orchestration
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+public OmgPaymentCreateOutcome create(Long userId, String orderId);
|
|
|
|
|
+
|
|
|
|
|
+public record OmgPaymentCreateOutcome(
|
|
|
|
|
+ OmgCreatePaymentResponse response,
|
|
|
|
|
+ Long attemptId,
|
|
|
|
|
+ String orderId,
|
|
|
|
|
+ Long userId,
|
|
|
|
|
+ Long storeId,
|
|
|
|
|
+ Integer amount,
|
|
|
|
|
+ String maskedMerchantTradeNo) {}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### API
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Anonymous
|
|
|
|
|
+@Auth
|
|
|
|
|
+@PostMapping("/create")
|
|
|
|
|
+public AjaxResult create(@RequestHeader String token,
|
|
|
|
|
+ @RequestBody(required = false) OmgCreatePaymentRequest request);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Do not add `@RepeatSubmit`: repeated clicks must reach the transactional duplicate check and return the stable `PAYMENT_ATTEMPT_EXISTS` result rather than a generic throttle response. This endpoint performs no external HTTP and is bounded by authentication, ownership validation and fixed DB work.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: New attempt persistence and DDL
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Create: `ruoyi-system/src/main/java/com/ruoyi/system/omgpay/domain/OmgPaymentAttempt.java`
|
|
|
|
|
+- Create: `ruoyi-system/src/main/java/com/ruoyi/system/omgpay/domain/OmgPaymentOrderSnapshot.java`
|
|
|
|
|
+- Create: `ruoyi-system/src/main/java/com/ruoyi/system/omgpay/mapper/OmgPaymentAttemptMapper.java`
|
|
|
|
|
+- Create: `ruoyi-system/src/main/java/com/ruoyi/system/omgpay/service/IOmgPaymentAttemptService.java`
|
|
|
|
|
+- Create: `ruoyi-system/src/main/java/com/ruoyi/system/omgpay/service/impl/OmgPaymentAttemptServiceImpl.java`
|
|
|
|
|
+- Create: `ruoyi-system/src/main/resources/mapper/omgpay/OmgPaymentAttemptMapper.xml`
|
|
|
|
|
+- Create: `ruoyi-system/src/test/java/com/ruoyi/system/omgpay/mapper/OmgPaymentAttemptMapperContractTest.java`
|
|
|
|
|
+- Create: `ruoyi-system/src/test/java/com/ruoyi/system/omgpay/service/OmgPaymentAttemptServiceTest.java`
|
|
|
|
|
+- Modify: `updatesql/sql.md`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+
|
|
|
|
|
+- Consumes: `pos_order` and no old OMG payment/refund table.
|
|
|
|
|
+- Produces: the persistence interfaces listed under Component Interfaces.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Write failing service validation tests**
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Test
|
|
|
|
|
+void createCreatedRejectsInvalidSnapshotsBeforeInsert() {
|
|
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
|
|
+ () -> service.createCreated("DD-1", "OMG123", 10L, "M1", 0));
|
|
|
|
|
+ verifyNoInteractions(mapper);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Test
|
|
|
|
|
+void createCreatedInsertsOnlyCreatedFacts() {
|
|
|
|
|
+ when(mapper.insertCreated(any())).thenAnswer(invocation -> {
|
|
|
|
|
+ OmgPaymentAttempt row = invocation.getArgument(0);
|
|
|
|
|
+ row.setId(7L);
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ });
|
|
|
|
|
+ OmgPaymentAttempt row = service.createCreated("DD-1", "OMG123", 10L, "M1", 100);
|
|
|
|
|
+ assertEquals(0, row.getAttemptStatus());
|
|
|
|
|
+ assertEquals(7L, row.getId());
|
|
|
|
|
+ assertNull(row.getActiveDdId()); // generated by DB, never inserted by Java
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Write failing Mapper/DDL contract tests**
|
|
|
|
|
+
|
|
|
|
|
+Load the XML and `updatesql/sql.md` as UTF-8 strings and assert:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+assertTrue(xml.contains("FROM pos_order"));
|
|
|
|
|
+assertTrue(xml.contains("FOR UPDATE"));
|
|
|
|
|
+assertTrue(xml.contains("FROM pos_order_omg_attempt"));
|
|
|
|
|
+assertFalse(xml.contains("pos_order_omg_payment"));
|
|
|
|
|
+assertFalse(xml.contains("pos_order_omg_refund"));
|
|
|
|
|
+assertTrue(sql.contains("UNIQUE KEY uk_omg_attempt_trade_no"));
|
|
|
|
|
+assertTrue(sql.contains("UNIQUE KEY uk_omg_attempt_active_dd"));
|
|
|
|
|
+assertTrue(sql.contains("IF(attempt_status = 0, dd_id, NULL)"));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Run tests and observe RED**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
|
|
|
|
|
+$env:Path="$env:JAVA_HOME\bin;$env:Path"
|
|
|
|
|
+mvn -pl ruoyi-system -am -Dtest='OmgPaymentAttemptServiceTest,OmgPaymentAttemptMapperContractTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: compilation fails because new types/resources do not exist.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Add DDL exactly as specified in `data-model.md`**
|
|
|
|
|
+
|
|
|
|
|
+Append the dated fenced SQL block. Do not run it. Keep `dd_id` utf8mb4 to match `pos_order`; keep gateway identifiers `ascii_bin`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Implement domain, Mapper XML and service**
|
|
|
|
|
+
|
|
|
|
|
+The XML must use parameter binding and explicit columns:
|
|
|
|
|
+
|
|
|
|
|
+```xml
|
|
|
|
|
+<select id="selectOrderForUpdate"
|
|
|
|
|
+ resultType="com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot">
|
|
|
|
|
+ SELECT id, dd_id AS ddId, parent_dd_id AS parentDdId, md_id AS storeId,
|
|
|
|
|
+ user_id AS userId, amount, state, pay_status AS payStatus, pay_type AS payType
|
|
|
|
|
+ FROM pos_order WHERE dd_id = #{ddId} LIMIT 1 FOR UPDATE
|
|
|
|
|
+</select>
|
|
|
|
|
+
|
|
|
|
|
+<insert id="insertCreated" useGeneratedKeys="true" keyProperty="id">
|
|
|
|
|
+ INSERT INTO pos_order_omg_attempt
|
|
|
|
|
+ (dd_id, merchant_trade_no, store_id, merchant_id, amount,
|
|
|
|
|
+ attempt_status, create_time, update_time)
|
|
|
|
|
+ VALUES
|
|
|
|
|
+ (#{ddId}, #{merchantTradeNo}, #{storeId}, #{merchantId}, #{amount},
|
|
|
|
|
+ #{attemptStatus}, #{createTime}, #{updateTime})
|
|
|
|
|
+</insert>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Comment why `FOR UPDATE` serializes endpoint calls before MerchantTradeNo generation and why `active_dd_id` is omitted from INSERT.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Run GREEN verification**
|
|
|
|
|
+
|
|
|
|
|
+Run the command from Step 3. Expected: both tests PASS.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 7: Commit only Task 1 files**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git add -- ruoyi-system/src/main/java/com/ruoyi/system/omgpay ruoyi-system/src/main/resources/mapper/omgpay ruoyi-system/src/test/java/com/ruoyi/system/omgpay updatesql/sql.md
|
|
|
|
|
+git commit -m "feat: add OMG payment attempt persistence"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: Official CheckMacValue signer
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgCheckMacSigner.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgCheckMacSignerTest.java`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+
|
|
|
|
|
+- Consumes: arbitrary internal `Map<String,String>` plus HashKey/HashIV.
|
|
|
|
|
+- Produces: `String sign(Map<String,String>, String, String)`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Write official-vector failing test**
|
|
|
|
|
+
|
|
|
|
|
+Use the exact official appendix parameters and public sample key/IV, then assert:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+assertEquals("AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266",
|
|
|
|
|
+ signer.sign(fields, "5294y06JbISpM5x9", "v77hoKGq4kWxNNIS"));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Write completeness and validation failing tests**
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+assertNotEquals(signer.sign(fields, key, iv),
|
|
|
|
|
+ signer.sign(withExtraField(fields, "NeedExtraPaidInfo", "Y"), key, iv));
|
|
|
|
|
+assertNotEquals(signer.sign(fields, key, iv),
|
|
|
|
|
+ signer.sign(withExtraField(fields, "EmptyExtra", ""), key, iv));
|
|
|
|
|
+assertThrows(IllegalArgumentException.class,
|
|
|
|
|
+ () -> signer.sign(Map.of("CheckMacValue", "caller-value"), key, iv));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Also prove input map is unchanged and output matches `[0-9A-F]{64}`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Run RED**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgCheckMacSignerTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: new signer class missing.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Implement the minimal signer**
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public String sign(Map<String, String> fields, String hashKey, String hashIv) {
|
|
|
|
|
+ requireSecrets(hashKey, hashIv);
|
|
|
|
|
+ TreeMap<String, String> sorted = validatedCopy(fields);
|
|
|
|
|
+ String query = sorted.entrySet().stream()
|
|
|
|
|
+ .map(e -> e.getKey() + "=" + e.getValue())
|
|
|
|
|
+ .collect(Collectors.joining("&"));
|
|
|
|
|
+ String raw = "HashKey=" + hashKey + "&" + query + "&HashIV=" + hashIv;
|
|
|
|
|
+ String encoded = dotNetUrlEncode(raw).toLowerCase(Locale.ROOT);
|
|
|
|
|
+ return HexFormat.of().withUpperCase().formatHex(sha256(encoded));
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`validatedCopy` retains empty strings and rejects only null keys/values and `CheckMacValue`. Add a focused comment explaining the official .NET URL conversion; do not log raw or encoded strings.
|
|
|
|
|
+
|
|
|
|
|
+Use these exact conversion operations after `URLEncoder.encode(raw, UTF_8)` and before lowercasing:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+return encoded.replace("%2D", "-")
|
|
|
|
|
+ .replace("%5F", "_")
|
|
|
|
|
+ .replace("%2E", ".")
|
|
|
|
|
+ .replace("%21", "!")
|
|
|
|
|
+ .replace("%2A", "*")
|
|
|
|
|
+ .replace("%28", "(")
|
|
|
|
|
+ .replace("%29", ")");
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Although Java already leaves some of these characters unchanged, retaining the complete official table in one method makes the protocol rule auditable.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Run GREEN**
|
|
|
|
|
+
|
|
|
|
|
+Run Step 3. Expected: all signer tests PASS with official vector.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git add -- ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgCheckMacSigner.java ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgCheckMacSignerTest.java
|
|
|
|
|
+git commit -m "feat: implement official OMG check code signer"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: Stage-only form factory and trade number
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentProperties.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentForm.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGenerator.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGeneratorTest.java`
|
|
|
|
|
+- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+
|
|
|
|
|
+- Consumes: order snapshot values, credential strings and the signer from Task 2.
|
|
|
|
|
+- Produces: exact stage URL and immutable ordered form field map.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Write failing generator tests**
|
|
|
|
|
+
|
|
|
|
|
+Generate 1,000 values and assert each is unique in the sample, length 20, starts `OMG`, and matches `[A-Z0-9]{20}`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Write failing form contract tests**
|
|
|
|
|
+
|
|
|
|
|
+With a fixed Clock at `2026-08-13T07:30:23Z`, assert Taipei date `2026/08/13 15:30:23`, exact gateway, exact 16-key set, fixed field values and that the signer receives exactly the 15 non-check-code fields.
|
|
|
|
|
+
|
|
|
|
|
+Also assert invalid ReturnURL cases are rejected: HTTP, wrong path, query, fragment, user-info and relative URL.
|
|
|
|
|
+
|
|
|
|
|
+Use order IDs containing `<script>`, `#`, `|` and non-ASCII characters to prove `TradeDesc`/`ItemName` contain no HTML or OMG item separator, use only the sanitized ASCII reference, and remain within the official length limits.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Run RED**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgMerchantTradeNoGeneratorTest,OmgPaymentFormFactoryTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Implement properties, generator and form factory**
|
|
|
|
|
+
|
|
|
|
|
+`OmgPaymentProperties` is `@Component @ConfigurationProperties(prefix="omgpay")` with only `returnUrl`. `requireSafeReturnUrl()` parses `URI` and enforces the exact safety rules.
|
|
|
|
|
+
|
|
|
|
|
+`OmgMerchantTradeNoGenerator` uses `SecureRandom` and alphabet `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+StringBuilder value = new StringBuilder("OMG");
|
|
|
|
|
+while (value.length() < 20) {
|
|
|
|
|
+ value.append(ALPHABET.charAt(random.nextInt(ALPHABET.length())));
|
|
|
|
|
+}
|
|
|
|
|
+return value.toString();
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`OmgPaymentFormFactory` uses a defensive unmodifiable copy for response fields and does not expose secrets.
|
|
|
|
|
+
|
|
|
|
|
+The order reference used in `TradeDesc` and `ItemName` is derived only after a real order row is found. It keeps ASCII letters and digits, drops all other characters, falls back to `ORDER` when empty, and is truncated to 64 characters. Therefore neither field contains HTML or OMG item separators; `ItemName` remains below its 120 ASCII-character display limit.
|
|
|
|
|
+
|
|
|
|
|
+Use constructor injection without a global Clock bean:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public OmgPaymentFormFactory(OmgPaymentProperties properties, OmgCheckMacSigner signer) {
|
|
|
|
|
+ this(properties, signer, Clock.systemUTC());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+OmgPaymentFormFactory(OmgPaymentProperties properties,
|
|
|
|
|
+ OmgCheckMacSigner signer,
|
|
|
|
|
+ Clock clock) {
|
|
|
|
|
+ this.properties = properties;
|
|
|
|
|
+ this.signer = signer;
|
|
|
|
|
+ this.clock = clock;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Tests in the same package use the second constructor with `Clock.fixed(...)`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Add isolated configuration**
|
|
|
|
|
+
|
|
|
|
|
+Append:
|
|
|
|
|
+
|
|
|
|
|
+```yaml
|
|
|
|
|
+omgpay:
|
|
|
|
|
+ return-url: https://foodieapi.waimai-paotui.com/pay/omg/notify
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Do not read `omg.base-url`, `omg.order-result-url` or `omg.payment-info-url` from the new package.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Run GREEN and commit**
|
|
|
|
|
+
|
|
|
|
|
+Run Step 3, then:
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git add -- ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentProperties.java ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentForm.java ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGenerator.java ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGeneratorTest.java ruoyi-admin/src/main/resources/application.yml
|
|
|
|
|
+git commit -m "feat: build stage-only OMG checkout forms"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 4: Transactional create orchestration
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentErrorCode.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentBusinessException.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateOutcome.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateService.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentResponse.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentCreateServiceTest.java`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+
|
|
|
|
|
+- Consumes: `IOmgPaymentAttemptService`, trusted `IPosStoreOmgService`, form factory and generator.
|
|
|
|
|
+- Produces: `OmgPaymentCreateOutcome create(Long userId, String orderId)`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Write failing validation tests**
|
|
|
|
|
+
|
|
|
|
|
+One parameterized test matrix covers null order, other owner, `parentDdId != ddId`, null store, state 3/4, payStatus nonzero, payType other than `"2"`, nonpositive amount and missing credential. Assert exact `OmgPaymentErrorCode`, no insert and no form generation.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Write failing duplicate tests**
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+when(attempts.lockOrder("DD-1")).thenReturn(payableOrder());
|
|
|
|
|
+when(attempts.getActiveByDdId("DD-1")).thenReturn(existingAttempt());
|
|
|
|
|
+
|
|
|
|
|
+OmgPaymentBusinessException error = assertThrows(
|
|
|
|
|
+ OmgPaymentBusinessException.class,
|
|
|
|
|
+ () -> service.create(5L, "DD-1"));
|
|
|
|
|
+assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
|
|
|
|
|
+verifyNoInteractions(generator, formFactory, credentialService);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Write failing success and collision tests**
|
|
|
|
|
+
|
|
|
|
|
+Success asserts call order: lock → active lookup → credential → generator → form → insert. Outcome contains response plus safe metadata; response contains no attempt ID or secrets.
|
|
|
|
|
+
|
|
|
|
|
+For `DuplicateKeyException`, test:
|
|
|
|
|
+
|
|
|
|
|
+- active row now exists → `PAYMENT_ATTEMPT_EXISTS` and no retry;
|
|
|
|
|
+- trade number exists but no active row → generate a new number, rebuild form and retry, at most 3 attempts;
|
|
|
|
|
+- unclassified/third collision → `PAYMENT_CREATION_FAILED`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Run RED**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgPaymentCreateServiceTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Implement the transactional service**
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+public OmgPaymentCreateOutcome create(Long userId, String orderId) {
|
|
|
|
|
+ OmgPaymentOrderSnapshot order = attempts.lockOrder(orderId);
|
|
|
|
|
+ validateOrder(userId, orderId, order);
|
|
|
|
|
+ if (attempts.getActiveByDdId(orderId) != null) {
|
|
|
|
|
+ throw business(PAYMENT_ATTEMPT_EXISTS);
|
|
|
|
|
+ }
|
|
|
|
|
+ PosStoreOmg credential = credentials.getEnabledCredential(order.getStoreId());
|
|
|
|
|
+ validateCredential(credential);
|
|
|
|
|
+ return createWithBoundedTradeNumberRetries(order, credential);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Validate before using any field. Keep the exact active check before generator invocation. Catch only `DuplicateKeyException`; never catch and downgrade arbitrary runtime exceptions inside the transaction.
|
|
|
|
|
+
|
|
|
|
|
+Define the enum mapping explicitly:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+AUTH_REQUIRED("omg.pay.auth.required"),
|
|
|
|
|
+ORDER_REQUIRED("omg.pay.order.required"),
|
|
|
|
|
+ORDER_NOT_AVAILABLE("omg.pay.order.not.available"),
|
|
|
|
|
+MULTI_STORE_ORDER_NOT_SUPPORTED("omg.pay.multi.store.unsupported"),
|
|
|
|
|
+ORDER_STATE_NOT_PAYABLE("omg.pay.order.state.not.payable"),
|
|
|
|
|
+ORDER_ALREADY_PAID("omg.pay.order.already.paid"),
|
|
|
|
|
+ORDER_AMOUNT_INVALID("omg.pay.order.amount.invalid"),
|
|
|
|
|
+PAYMENT_TYPE_INVALID("omg.pay.payment.type.invalid"),
|
|
|
|
|
+STORE_CREDENTIAL_UNAVAILABLE("omg.pay.credential.unavailable"),
|
|
|
|
|
+PAYMENT_ATTEMPT_EXISTS("omg.pay.attempt.exists"),
|
|
|
|
|
+PAYMENT_CONFIGURATION_INVALID("omg.pay.configuration.invalid"),
|
|
|
|
|
+PAYMENT_CREATION_FAILED("omg.pay.creation.failed");
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`OmgPaymentBusinessException` carries the enum and optional safe `storeId`. Failures after a real order is locked populate `storeId`; pre-order failures leave it null. Convert invalid ReturnURL/form configuration to `PAYMENT_CONFIGURATION_INVALID` without exposing its raw exception message.
|
|
|
|
|
+
|
|
|
|
|
+Validate `merchantId` against `[A-Za-z0-9]{1,10}` and require nonblank HashKey/HashIV before form creation. After order and credential validation, emit one safe INFO event:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+log.info("OMG payment validation passed orderId={}, userId={}, storeId={}",
|
|
|
|
|
+ order.getDdId(), userId, order.getStoreId());
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Do not log the credential object or any individual secret. The Controller's success log runs only after this transactional method returns through the Spring proxy, so that event represents a committed attempt rather than an uncommitted insert.
|
|
|
|
|
+
|
|
|
|
|
+Mask the trade number for logs as the first five characters, `***`, and the final four characters. Values shorter than ten characters become `***`; the full value remains only in the authorized success form.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Run GREEN and check package isolation**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgPaymentCreateServiceTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+rg -n "com\.ruoyi\.app\.pay\.OmgPayController|com\.ruoyi\.app\.utils\.omg|IPosOrderOmgPaymentService|IPosOrderOmgRefundService" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay ruoyi-system/src/main/java/com/ruoyi/system/omgpay
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: tests PASS; `rg` produces no matches.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 7: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git add -- ruoyi-admin/src/main/java/com/ruoyi/app/omgpay ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentCreateServiceTest.java
|
|
|
|
|
+git commit -m "feat: orchestrate safe OMG payment creation"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 5: Controller, i18n and diagnostic logs
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentTokenUserResolver.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentRequest.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgPaymentErrorResponse.java`
|
|
|
|
|
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentControllerTest.java`
|
|
|
|
|
+- Modify: five `ruoyi-admin/src/main/resources/i18n/messages*.properties` files
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+
|
|
|
|
|
+- Consumes: create service from Task 4 and project `JwtUtil`/`@Auth`.
|
|
|
|
|
+- Produces: public `POST /pay/omg/create`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Write failing Controller contract tests**
|
|
|
|
|
+
|
|
|
|
|
+Reflection assertions:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+assertEquals("/pay/omg", controllerMapping);
|
|
|
|
|
+assertEquals("/create", postMapping);
|
|
|
|
|
+assertTrue(hasRequestHeaderNamedToken);
|
|
|
|
|
+assertTrue(hasExplicitRequestBody);
|
|
|
|
|
+assertEquals(Set.of("orderId"), declaredDtoProperties);
|
|
|
|
|
+assertFalse(usesMapRequestParameter);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Behavior assertions cover missing body/orderId, resolver failure, each business exception, success, and unexpected exception. Confirm error `data.status` and success `data` shape.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Write failing log-capture tests**
|
|
|
|
|
+
|
|
|
|
|
+Attach a Logback `ListAppender` and assert:
|
|
|
|
|
+
|
|
|
|
|
+- start INFO has orderId/userId;
|
|
|
|
|
+- success INFO has attemptId/storeId/amount/status/masked MTN;
|
|
|
|
|
+- business WARN has stable code;
|
|
|
|
|
+- unexpected ERROR contains the exact Throwable;
|
|
|
|
|
+- concatenated messages do not contain token, test HashKey/HashIV, 64-character CheckMacValue or full form field dump.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Run RED**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgPaymentControllerTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Add i18n keys in all five bundles**
|
|
|
|
|
+
|
|
|
|
|
+Add keys matching every `OmgPaymentErrorCode.messageKey`. Use these exact key sets and translations:
|
|
|
|
|
+
|
|
|
|
|
+```properties
|
|
|
|
|
+# messages.properties and messages_zh_CN.properties
|
|
|
|
|
+omg.pay.auth.required=请先登录
|
|
|
|
|
+omg.pay.order.required=订单号不能为空
|
|
|
|
|
+omg.pay.order.not.available=订单不存在或无权操作
|
|
|
|
|
+omg.pay.multi.store.unsupported=多门店订单暂不支持 OMG 支付
|
|
|
|
|
+omg.pay.order.state.not.payable=当前订单状态不可支付
|
|
|
|
|
+omg.pay.order.already.paid=订单已支付或支付状态不可用
|
|
|
|
|
+omg.pay.order.amount.invalid=订单金额异常
|
|
|
|
|
+omg.pay.payment.type.invalid=订单支付方式不是 OMG
|
|
|
|
|
+omg.pay.credential.unavailable=该门店暂未启用 OMG 支付
|
|
|
|
|
+omg.pay.attempt.exists=该订单已有待处理的支付尝试
|
|
|
|
|
+omg.pay.configuration.invalid=OMG 支付配置无效
|
|
|
|
|
+omg.pay.creation.failed=OMG 支付创建失败,请稍后重试
|
|
|
|
|
+
|
|
|
|
|
+# messages_zh_TW.properties
|
|
|
|
|
+omg.pay.auth.required=請先登入
|
|
|
|
|
+omg.pay.order.required=訂單號不能為空
|
|
|
|
|
+omg.pay.order.not.available=訂單不存在或無權操作
|
|
|
|
|
+omg.pay.multi.store.unsupported=多門店訂單暫不支援 OMG 支付
|
|
|
|
|
+omg.pay.order.state.not.payable=目前訂單狀態不可支付
|
|
|
|
|
+omg.pay.order.already.paid=訂單已付款或付款狀態不可用
|
|
|
|
|
+omg.pay.order.amount.invalid=訂單金額異常
|
|
|
|
|
+omg.pay.payment.type.invalid=訂單付款方式不是 OMG
|
|
|
|
|
+omg.pay.credential.unavailable=此門店尚未啟用 OMG 支付
|
|
|
|
|
+omg.pay.attempt.exists=此訂單已有待處理的支付嘗試
|
|
|
|
|
+omg.pay.configuration.invalid=OMG 支付設定無效
|
|
|
|
|
+omg.pay.creation.failed=OMG 支付建立失敗,請稍後再試
|
|
|
|
|
+
|
|
|
|
|
+# messages_en_US.properties
|
|
|
|
|
+omg.pay.auth.required=Please sign in first
|
|
|
|
|
+omg.pay.order.required=The order number is required
|
|
|
|
|
+omg.pay.order.not.available=The order does not exist or is not available to this user
|
|
|
|
|
+omg.pay.multi.store.unsupported=OMG Pay does not support multi-store orders yet
|
|
|
|
|
+omg.pay.order.state.not.payable=The current order state cannot be paid
|
|
|
|
|
+omg.pay.order.already.paid=The order is already paid or its payment state is unavailable
|
|
|
|
|
+omg.pay.order.amount.invalid=The order amount is invalid
|
|
|
|
|
+omg.pay.payment.type.invalid=The order payment method is not OMG Pay
|
|
|
|
|
+omg.pay.credential.unavailable=OMG Pay is not enabled for this store
|
|
|
|
|
+omg.pay.attempt.exists=This order already has a pending payment attempt
|
|
|
|
|
+omg.pay.configuration.invalid=The OMG Pay configuration is invalid
|
|
|
|
|
+omg.pay.creation.failed=The OMG Pay checkout could not be created; please try again later
|
|
|
|
|
+
|
|
|
|
|
+# messages_vi.properties
|
|
|
|
|
+omg.pay.auth.required=Vui lòng đăng nhập trước
|
|
|
|
|
+omg.pay.order.required=Vui lòng nhập mã đơn hàng
|
|
|
|
|
+omg.pay.order.not.available=Đơn hàng không tồn tại hoặc người dùng không có quyền truy cập
|
|
|
|
|
+omg.pay.multi.store.unsupported=OMG Pay chưa hỗ trợ đơn hàng từ nhiều cửa hàng
|
|
|
|
|
+omg.pay.order.state.not.payable=Trạng thái đơn hàng hiện tại không thể thanh toán
|
|
|
|
|
+omg.pay.order.already.paid=Đơn hàng đã được thanh toán hoặc trạng thái thanh toán không khả dụng
|
|
|
|
|
+omg.pay.order.amount.invalid=Số tiền đơn hàng không hợp lệ
|
|
|
|
|
+omg.pay.payment.type.invalid=Phương thức thanh toán của đơn hàng không phải OMG Pay
|
|
|
|
|
+omg.pay.credential.unavailable=Cửa hàng này chưa bật OMG Pay
|
|
|
|
|
+omg.pay.attempt.exists=Đơn hàng này đã có một lần thanh toán đang chờ xử lý
|
|
|
|
|
+omg.pay.configuration.invalid=Cấu hình OMG Pay không hợp lệ
|
|
|
|
|
+omg.pay.creation.failed=Không thể tạo trang thanh toán OMG Pay; vui lòng thử lại sau
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Keep identical key sets across default/zh_CN/zh_TW/en_US/vi and localized values in each file.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Implement resolver and Controller**
|
|
|
|
|
+
|
|
|
|
|
+The Controller does not log request/response objects:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+try {
|
|
|
|
|
+ Long userId = tokenUserResolver.requireUserId(token);
|
|
|
|
|
+ log.info("OMG payment create started orderId={}, userId={}", orderId, userId);
|
|
|
|
|
+ OmgPaymentCreateOutcome outcome = createService.create(userId, orderId);
|
|
|
|
|
+ log.info("OMG payment create succeeded orderId={}, userId={}, storeId={}, "
|
|
|
|
|
+ + "attemptId={}, amount={}, status=CREATED, merchantTradeNo={}",
|
|
|
|
|
+ outcome.orderId(), outcome.userId(), outcome.storeId(),
|
|
|
|
|
+ outcome.attemptId(), outcome.amount(), outcome.maskedMerchantTradeNo());
|
|
|
|
|
+ return AjaxResult.success(outcome.response());
|
|
|
|
|
+} catch (OmgPaymentBusinessException error) {
|
|
|
|
|
+ log.warn("OMG payment create rejected orderId={}, userId={}, storeId={}, code={}",
|
|
|
|
|
+ safeOrderId, safeUserId, error.getStoreId(), error.getCode());
|
|
|
|
|
+ return AjaxResult.error(MessageUtils.message(error.getMessageKey()),
|
|
|
|
|
+ new OmgPaymentErrorResponse(error.getCode().name()));
|
|
|
|
|
+} catch (Exception error) {
|
|
|
|
|
+ log.error("OMG payment create failed orderId={}, userId={}", safeOrderId, safeUserId, error);
|
|
|
|
|
+ return AjaxResult.error(MessageUtils.message("omg.pay.creation.failed"),
|
|
|
|
|
+ new OmgPaymentErrorResponse("PAYMENT_CREATION_FAILED"));
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+The project `@Auth` remains the first invalid-token gate. Resolver defense never logs token and maps missing/invalid user ID to `AUTH_REQUIRED` when reached.
|
|
|
|
|
+
|
|
|
|
|
+Before logging, sanitize the untrusted request order ID to at most 64 characters, retaining only ASCII letters, digits, `-` and `_`; use `"<empty>"` when nothing remains. This prevents newline/control-character log injection and unbounded log entries. Add a test with `"DD-1\r\nforged=true"` and assert no CR/LF appears in any captured log message.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Run GREEN plus i18n key parity check**
|
|
|
|
|
+
|
|
|
|
|
+Run Step 3. Then compare all `omg.pay.*` keys across the five bundles; expected identical key lists.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 7: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git add -- ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentTokenUserResolver.java ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentRequest.java ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgPaymentErrorResponse.java ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentControllerTest.java ruoyi-admin/src/main/resources/i18n
|
|
|
|
|
+git commit -m "feat: expose logged OMG payment creation endpoint"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 6: Retire all old payment-table runtime paths
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgLegacyRetirementTest.java`
|
|
|
|
|
+- Modify: `UserOrderController.java`, `PosOrderShOprateController.java`, `PosOrderController.java`, `OrderLifecycleService.java`, `OrderLifecycleServiceTest.java`
|
|
|
|
|
+- Delete: files listed under “Retired old files”
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+
|
|
|
|
|
+- Consumes: new `OmgPaymentController`; keeps `OrderLifecycleService.PAY_TYPE_OMG="2"` only for channel progression gate.
|
|
|
|
|
+- Produces: one `/pay/omg/create` mapping and zero old callback/query/refund/table runtime references.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Capture existing dirty diffs before editing**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git diff -- ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderLifecycleServiceTest.java ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java ruoyi-admin/src/test/java/com/ruoyi/app/pay/OmgPayControllerTest.java
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Record that unrelated LINE/status changes remain visible after this task. Do not use checkout/reset.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Write failing retirement test**
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+assertThrows(ClassNotFoundException.class,
|
|
|
|
|
+ () -> Class.forName("com.ruoyi.app.pay.OmgPayController"));
|
|
|
|
|
+assertThrows(ClassNotFoundException.class,
|
|
|
|
|
+ () -> Class.forName("com.ruoyi.app.task.OmgReconcileTask"));
|
|
|
|
|
+assertThrows(ClassNotFoundException.class,
|
|
|
|
|
+ () -> Class.forName("com.ruoyi.system.domain.PosOrderOmgPayment"));
|
|
|
|
|
+assertNotNull(OmgPaymentController.class.getAnnotation(RestController.class));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Also inspect `OmgPaymentController#create` annotations and assert the only exposed method under `/pay/omg` is `/create`; no `notify`, `query`, `paymentInfo`, `return` or `refund` handler exists in the new class.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Run RED**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgLegacyRetirementTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: old classes still load.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Remove old Controller/task/DTO/table persistence sources**
|
|
|
|
|
+
|
|
|
|
|
+Delete only the explicit retired file list. Preserve `PosStoreOmg*` and old `OmgPay/OmgPayConfig/OmgCheckMacValue` used by credential management.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Remove reachable old calls surgically**
|
|
|
|
|
+
|
|
|
|
|
+- `UserOrderController.cancelOrder`: delete only the old OMG refund block after cancellation; leave other cancellation and LINE behavior intact.
|
|
|
|
|
+- `PosOrderShOprateController.cancelOrder`: delete only the old OMG refund block.
|
|
|
|
|
+- `PosOrderController`: remove old Controller/DTO injection, the three OMG reconcile/refund/manual-confirm endpoints, and the now-unused `refundData` helper.
|
|
|
|
|
+- `OrderLifecycleService`: remove old table imports, constructor dependencies, `validateOmgReconcile`, `validateOmgRefund`, `finalizeOmgRefund`, `finalizeSystemOmgRefund`, old buildContext table queries and manual-refund helpers. Keep `PAY_TYPE_OMG="2"` and the unpaid-online-order progression gate.
|
|
|
|
|
+- `OrderLifecycleServiceTest`: remove only old payment/refund service mocks and tests; update constructors while preserving LINE Pay and current order lifecycle tests.
|
|
|
|
|
+- `application.yml`: under legacy `omg`, retain only `base-url` required by trusted `PosStoreOmgController` credential verification; remove old `return-url`, `order-result-url`, `payment-info-url`, `client-redirect-url`, `create` and `reconcile` configuration. Keep the new `omgpay.return-url` added in Task 3.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Run GREEN and prove zero runtime references**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgLegacyRetirementTest,OrderLifecycleServiceTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+rg -n --glob '*.java' --glob '*.xml' "OmgPayController|OmgReconcileTask|IPosOrderOmgPaymentService|IPosOrderOmgRefundService|PosOrderOmgPayment|PosOrderOmgRefund|pos_order_omg_payment|pos_order_omg_refund" ruoyi-admin/src/main ruoyi-system/src/main
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: tests PASS; `rg` returns no matches. Historical SQL and old specs are excluded deliberately.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 7: Recheck preservation and commit**
|
|
|
|
|
+
|
|
|
|
|
+Review `git diff` for the three initially dirty files and verify unrelated hunks remain. Then stage only task files and commit:
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git add -- ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgCallbackRequest.java ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgOrderRequest.java ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgRefundOutcome.java ruoyi-admin/src/main/java/com/ruoyi/app/task/OmgReconcileTask.java ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgQueryThrottle.java ruoyi-admin/src/test/java/com/ruoyi/app/pay/OmgPayControllerTest.java ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgLegacyRetirementTest.java ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgPaymentService.java ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgRefundService.java ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImpl.java ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImpl.java ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgPaymentMapper.xml ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgRefundMapper.xml ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImplTest.java ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImplTest.java ruoyi-admin/src/main/resources/application.yml
|
|
|
|
|
+git add -p -- ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderLifecycleServiceTest.java
|
|
|
|
|
+git commit -m "refactor: retire legacy OMG payment runtime"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+For `git add -p`, accept only old OMG retirement hunks and reject pre-existing LINE/order-status hunks. Before commit, use `git diff --cached --name-only` and `git diff --cached`; never include unrelated pre-staged content.
|
|
|
|
|
+
|
|
|
|
|
+### Task 7: Full verification and stage acceptance handoff
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+
|
|
|
|
|
+- Modify if results require: only new OMG files or directly affected retirement files.
|
|
|
|
|
+- Verify: all files in this feature.
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:** complete feature acceptance.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: Run all new and affected tests**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
|
|
|
|
|
+$env:Path="$env:JAVA_HOME\bin;$env:Path"
|
|
|
|
|
+mvn -pl ruoyi-system -am -Dtest='OmgPaymentAttemptServiceTest,OmgPaymentAttemptMapperContractTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+mvn -pl ruoyi-admin -am -Dtest='OmgCheckMacSignerTest,OmgMerchantTradeNoGeneratorTest,OmgPaymentFormFactoryTest,OmgPaymentCreateServiceTest,OmgPaymentControllerTest,OmgLegacyRetirementTest,OrderLifecycleServiceTest,LinePayCancellationRaceTest' -Dsurefire.failIfNoSpecifiedTests=false test
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: exit code 0.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Build admin and dependencies**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+mvn -pl ruoyi-admin -am -DskipTests package
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Expected: `BUILD SUCCESS` on JDK 21.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Run static safety checks**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+rg -n "payment\.funpoint\.com\.tw|PlatformID|PaymentInfoURL|OrderResultURL|ClientRedirectURL|ClientBackURL" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
|
|
|
|
|
+rg -n "com\.ruoyi\.app\.pay\.OmgPayController|com\.ruoyi\.app\.utils\.omg|pos_order_omg_payment|pos_order_omg_refund" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay ruoyi-system/src/main/java/com/ruoyi/system/omgpay
|
|
|
|
|
+rg -n "HashKey|HashIV|CheckMacValue|formFields|token" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Interpret results manually: stage URL and required form names are expected; production URL, prohibited fields, old imports and logging of values are not.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Check comments and log statements**
|
|
|
|
|
+
|
|
|
|
|
+Review each new public type and non-obvious signer/transaction/unique-key branch. Confirm necessary “why” comments exist and trivial assignment comments do not. Inspect every `log.*` argument and verify no object serialization or sensitive values.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 5: Check diff, encoding and line endings**
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git diff --check
|
|
|
|
|
+git status --short
|
|
|
|
|
+git diff --stat
|
|
|
|
|
+git diff --name-only
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Compare against the pre-task dirty file list. No unrelated file may be staged or rewritten; no whole-file line-ending churn.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 6: Manual DB/stage validation when developer applies DDL**
|
|
|
|
|
+
|
|
|
|
|
+Follow [quickstart.md](quickstart.md): first creation, current-page form POST, repeated request, two-request concurrency, negative cases and log review. Do not treat payment/callback as acceptance.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 7: Final verification commit if needed**
|
|
|
|
|
+
|
|
|
|
|
+If verification required small fixes, rerun Steps 1–5 and commit only those fixes:
|
|
|
|
|
+
|
|
|
|
|
+```powershell
|
|
|
|
|
+git commit -m "test: verify OMG payment creation rebuild"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Do not create an empty commit.
|
|
|
|
|
+
|
|
|
|
|
+## Verification Gates
|
|
|
|
|
+
|
|
|
|
|
+1. Official signature vector equals the published SHA-256 CheckMacValue.
|
|
|
|
|
+2. All 15 actual pre-sign fields, including extra/expiry fields, are signer inputs; empty fields are retained generically.
|
|
|
|
|
+3. Success form has exactly 16 fields and the exact stage URL.
|
|
|
|
|
+4. Order is locked before active lookup and generator call.
|
|
|
|
|
+5. Repeat and normal concurrent endpoint requests produce no second MerchantTradeNo.
|
|
|
|
|
+6. New table unique keys exist in documented DDL; DDL was not executed.
|
|
|
|
|
+7. No new code references old OMG Controller, old signer/tools or old payment/refund services.
|
|
|
|
|
+8. No reachable main-source SQL references old payment/refund tables.
|
|
|
|
|
+9. `pos_store_omg` and credential management remain unchanged.
|
|
|
|
|
+10. Old `/pay/omg/notify/query/paymentInfo/return/refund` routes and scheduled task are absent.
|
|
|
|
|
+11. Logs have required safe context and unexpected exception stack, with zero secret/full-form leakage.
|
|
|
|
|
+12. JDK 21 tests and module build exit 0.
|
|
|
|
|
+
|
|
|
|
|
+## Spec Coverage Self-Review
|
|
|
|
|
+
|
|
|
|
|
+| Spec requirements | Implemented/verified by |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| FR-001–FR-003 | Tasks 1, 4 and package-isolation scans |
|
|
|
|
|
+| FR-004–FR-005 | Task 5 Controller reflection/DTO tests |
|
|
|
|
|
+| FR-006–FR-007 | Task 4 validation matrix and server-side amount assertions |
|
|
|
|
|
+| FR-008–FR-009 | Task 3 stage constant and trade-number tests |
|
|
|
|
|
+| FR-010–FR-011 | Tasks 1 and 4 row lock, active unique key and duplicate tests |
|
|
|
|
|
+| FR-012–FR-016 | Task 3 exact field-set, text, timezone and ReturnURL tests |
|
|
|
|
|
+| FR-017–FR-019 | Task 2 official vector, all-field mutation and empty-value tests |
|
|
|
|
|
+| FR-020–FR-021 | Tasks 3 and 5 response contract; `quickstart.md` client POST acceptance |
|
|
|
|
|
+| FR-022 | Task 1 DDL/Mapper contract tests |
|
|
|
|
|
+| FR-023–FR-024 | Task 5 log/error tests and five-bundle key parity |
|
|
|
|
|
+| FR-025–FR-027 | Task 6 class absence, route absence and main-source zero-match scans |
|
|
|
|
|
+| FR-028 | Task 1 SQL-only DDL registration |
|
|
|
|
|
+| FR-029 | Tasks 3, 6 and 7 stage-only/no-callback gates |
|
|
|
|
|
+| FR-030–FR-031 | Tasks 5 and 7 comment/log reviews and automated log assertions |
|
|
|
|
|
+
|
|
|
|
|
+Self-review result: all 31 functional requirements have an implementation task and a verification point; no placeholders remain; interface names and signatures are consistent across tasks.
|
|
|
|
|
+
|
|
|
|
|
+## Risks and Controls
|
|
|
|
|
+
|
|
|
|
|
+| Risk | Control |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| Duplicate payable entries | `FOR UPDATE` on order before generator + `UNIQUE(active_dd_id)` |
|
|
|
|
|
+| MerchantTradeNo collision | `UNIQUE(merchant_trade_no)` + bounded three-attempt regeneration |
|
|
|
|
|
+| Old table deletion breaks active paths | remove Controller/task/admin/cancel/lifecycle references; main-source zero-match scan |
|
|
|
|
|
+| Credentials leak | response whitelist, no credential DTO logging, safe log tests |
|
|
|
|
|
+| Signature omits new fields | signer accepts full actual map; completeness mutation tests |
|
|
|
|
|
+| Production accidentally enabled | gateway constant is exact stage URL; no production property |
|
|
|
|
|
+| Old callback handles new notification | old Controller deleted; route absence test |
|
|
|
|
|
+| User dirty work overwritten | pre-edit diff capture, surgical patching, staged-file audit |
|
|
|
|
|
+
|
|
|
|
|
+## Complexity Tracking
|
|
|
|
|
+
|
|
|
|
|
+No constitution violations. The separate form factory, signer, generator and persistence service each hold one security-sensitive responsibility and are directly unit testable; none is a generic multi-provider abstraction.
|