For agentic workers: 按
tasks.md顺序逐项执行;每个实现任务先补失败测试,再写最小实现并运行对应验证。不得跳过状态机 CAS、外部调用前持久化 intent 或最终git diff检查。
Goal: 为 payType="3" 接入 LINE Pay Online API v4,提供门店凭证版本管理、追加式支付尝试、异步确认、主动补单、自动全额退款和平台运维能力。
Architecture: ruoyi-system 只承载实体、MyBatis Mapper 和持久化状态机;ruoyi-admin 承载 LINE HTTP/HMAC、订单编排、回跳页面、定时任务和 Controller。支付、确认、退款均采用“本地先提交 intent/claim → 外部 HTTP → 独立本地 CAS 落事实”,不依赖一个跨网络事务。支付尝试为订单 1:N 且历史永不覆盖,稳定键为 paymentId、line_order_id、transaction_id 和可空唯一 active_dd_id。
Tech Stack: Java 21、Spring Boot、MyBatis/MyBatis-Plus、MySQL、Apache HttpClient 4、fastjson2、Redisson、Vue 2、Element UI、JUnit 5/Mockito。
LINE Pay 普通付款省略未被当前公开契约保证的 payType/confirmUrlType/appPackageName,仅显式 capture=true 并使用默认浏览器回跳。Request 成功只代表已创建付款请求;回跳快速登记后返回内嵌安全中间页,Confirm 由可恢复服务异步完成。任务每分钟按行租约扫描,先 Retrieve,再按需 Check;只有 Check 0110 且本地订单仍可付款时才能 Confirm。支付经严格匹配的 Retrieve/Confirm 证实后,支付行永久保持 PAID,退款事实只写退款表。
Language/Version: Java 21;Vue 2
Primary Dependencies: Spring Boot、MyBatis、MyBatis-Plus、Apache HttpClient 4.5.14、fastjson2、Redisson、Element UI
Storage: MySQL;新增 4 张 LINE 专用表,不执行在线迁移
Testing: Maven Surefire + JUnit 5/Mockito;前端 ESLint/production build
Target Platform: Windows 开发;Spring Boot 多实例部署;LINE Pay Sandbox/Production
Project Type: 多模块后端 + 独立平台 Vue 项目
Performance Goals: App 查询只做本地数据库读取;定时任务单轮最多 20 笔并有时间预算;关键扫描走 (status,next_reconcile_at,id)
Constraints: 不修改 OMG 表结构或迁移 OMG 日志;不修改废弃 ZaloPay/PayController/TestTask/OrderAppeal 业务;Secret 明文存储和平台详情回显为用户已接受方案;支付/退款副作用未知时不得盲重试
Scale/Scope: 门店级凭证;单门店子订单;每订单多次历史尝试但最多一条阻断性尝试;首版只支持全额退款
.specify/memory/constitution.md 尚未定义项目条款,因此以下仓库规则作为强制门禁:
ruoyi-admin -> ruoyi-system;LINE HTTP 集成只放 ruoyi-admin。@RequestBody/@RequestParam/@RequestHeader/@PathVariable,不新增 Map 入参或 Bean Validation。updatesql/sql.md,不直接执行数据库变更。specs/019-line-pay/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── tasks.md
├── contracts/
│ └── api.md
└── checklists/
└── requirements.md
foodie_server/
├── ruoyi-system/src/main/java/com/ruoyi/system/
│ ├── domain/{PosStoreLinePay,PosOrderLinePayment,PosOrderLineRefund,PaymentGatewayLog}.java
│ ├── domain/dto/{StoreLinePayCredentialDto,StoreLinePayToggleDto}.java
│ ├── domain/vo/PosStoreLinePayVo.java
│ ├── mapper/*Line*.java
│ └── service/{I*,impl/*}.java
├── ruoyi-system/src/main/resources/mapper/chanting/*Line*.xml
├── ruoyi-admin/src/main/java/com/ruoyi/app/
│ ├── utils/linepay/{LinePayProperties,LinePaySigner,LinePayHttpTransport,ApacheLinePayHttpTransport,LinePayClient}.java
│ ├── pay/{LinePayController,LinePayService,LinePayGatewayAuditService,PaymentCreateGuardService}.java
│ ├── pay/dto/*LinePay*.java
│ ├── mendian/PosStoreLinePayController.java
│ └── task/LinePayReconcileTask.java
├── ruoyi-admin/src/test/java/com/ruoyi/app/{utils/linepay,pay,task}/*Test.java
├── ruoyi-system/src/test/java/com/ruoyi/system/service/impl/*Line*Test.java
└── updatesql/sql.md
foodie-admin-vue/
└── src/
├── api/chanting/storeLinePay.js
├── api/system/order.js
├── views/mendian/storePayment/{index.vue,components/LinePayTab.vue}
├── views/system/order/index.vue
└── api/language/language.{zh_CN,zh_TW,en_US,vi}.js
ruoyi-system)IPosStoreLinePayService 负责版本号分配、验证成功后的当前版本原子切换、启停和列表/详情。IPosOrderLinePaymentService 负责创建 REQUESTING、稳定键查询、App 选择规则、CAS 状态推进、活跃键释放、扫描和行租约。IPosOrderLineRefundService 负责 UNIQUE(payment_id) 的 insert-if-absent、CAS claim、扫描和退款事实。IPaymentGatewayLogService 只追加审计数据;日志不是状态机事实,日志写入失败不回滚资金事实。PosOrderMapper 新增条件更新:只有未付款、合法订单态且当前 pay_type 与目标渠道一致时,才允许领取支付渠道或更新支付 URL。ruoyi-admin)LinePaySigner 严格按 channelSecret + URI + body/query + nonce 计算 Base64 HMAC-SHA256;GET 使用最终原始 query 字符串,POST 使用最终 UTF-8 JSON 字节。LinePayClient 暴露 request/check/confirm/retrieve/refund/verifyCredential;交易号始终为字符串。端点超时分别按 Request 10 秒、Check/Retrieve/Refund 20 秒、Confirm 40 秒配置。LinePayHttpTransport 隔离实际 Apache HttpClient,便于不联网测试精确 method/URI/body/header。LinePayGatewayAuditService 记录 correlationId、方向、action、HTTP/LINE 结果和 payload;调用方捕获日志异常后继续处理资金事实。ruoyi-admin)PaymentCreateGuardService 使用共享 pay:create:<ddId> Redisson watchdog 锁和订单条件更新,LINE 与 OMG create 都经过同一门禁;OMG 仅做这处最小代码调整。LinePayService.create 在锁内校验用户、订单、单门店、金额、当前凭证和历史 payType=3 归属,然后复用活跃尝试或先提交新 REQUESTING。Request HTTP 在事务外执行,结果以 CAS 落为 WAITING_AUTH 或 REQUEST_UNKNOWN。LinePayService.onConfirmRedirect 只按 line_order_id 登记并尝试把行推进到可处理状态,不同步等待 Confirm;Controller 返回安全 HTML。LinePayService.reconcilePayment 始终优先 Retrieve;无付款事实才 Check。副作用调用前先 CAS claim,超时进入 UNKNOWN,后续只读恢复。LinePayService.applyPaidFact 在一笔本地事务内写 PAID、锁定订单并更新 pay_status=1;订单已取消则 insert-if-absent 创建退款 intent。LinePayService.requestFullRefund 只针对 PAID 行,省略 refundAmount;成功后退款表为 REFUNDED 且订单 pay_status=2,支付表保持 PAID。LinePayReconcileTask 使用进程级 watchdog 锁减少重复扫描,但每行仍通过 lease_owner/lease_until/version 领取;失败一笔不终止整批。/pay/line/create、/query 使用 {ddId} DTO 和现有 @Auth + @Anonymous + token header 组合;confirm/cancel 仅为匿名 GET 回跳。| 场景 | 本地处理 | 后续动作 |
|---|---|---|
| Request 超时/响应丢失 | REQUEST_UNKNOWN,保留活跃键 |
Retrieve by line_order_id,必要时 Check |
Check 0000 |
保持等待 | 退避后再查 |
Check 0110 |
本地门禁通过后 CAS CONFIRMING |
Confirm;超时转 CONFIRM_UNKNOWN |
Check 0121/0122 |
先 Retrieve 排除已付款 | 明确未付款才终止并释放活跃键 |
Check 0123 |
不直接判已付 | Retrieve 验证唯一 PAYMENT + CAPTURE |
| Confirm 非明确结果/超时 | CONFIRM_UNKNOWN |
只读 Retrieve/Check,不盲 Confirm |
| 取消后迟到付款 | 支付仍写 PAID |
唯一退款 intent + 自动全退 |
| Refund 超时/未知 | UNKNOWN |
Retrieve 原支付并检查 refundList |
| 到达恢复截止仍未知 | MANUAL_REVIEW |
停止自动副作用,平台人工处理 |
mvn -pl ruoyi-system -am -Dtest='*Line*Test' -Dsurefire.failIfNoSpecifiedTests=false testmvn -pl ruoyi-admin -am -Dtest='*LinePay*Test,OrderLifecycleServiceTest,OmgPayControllerTest' -Dsurefire.failIfNoSpecifiedTests=false testmvn -pl ruoyi-admin -am -DskipTests packagefoodie-admin-vue 运行 npm run lint 和 npm run build:prod;若仓库已有无关 lint 错误,记录并对变更文件运行 ESLint。git diff --check、git status --short、逐文件核对换行/编码、确认未触碰废弃代码和用户已有 OMG SQL。| Decision | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| 订单 1:N 支付尝试 + 可空唯一活跃键 | 保留每次网关 orderId/transactionId/凭证与未知状态,避免迟到回跳污染新支付 | 覆盖单行会丢失恢复和退款所需键值 |
| 不可变凭证版本 + payment.credential_id | 轮换后历史交易仍能找到原渠道身份 | 一店一行覆盖会让旧交易失去凭证归属 |
| 支付与退款分表 | PAID 是资金事实,退款是另一事实与状态机 |
在支付状态写 REFUNDED 会掩盖曾经扣款 |
| durable intent + 外部调用 + CAS | 数据库事务无法覆盖 LINE 网络副作用 | 单个 @Transactional 无法修复响应丢失和本地提交失败 |