# LINE Pay 直连支付 Implementation Plan > **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。 --- ## Summary LINE Pay 普通付款省略未被当前公开契约保证的 `payType/confirmUrlType/appPackageName`,仅显式 `capture=true` 并使用默认浏览器回跳。Request 成功只代表已创建付款请求;回跳快速登记后返回内嵌安全中间页,Confirm 由可恢复服务异步完成。任务每分钟按行租约扫描,先 Retrieve,再按需 Check;只有 Check `0110` 且本地订单仍可付款时才能 Confirm。支付经严格匹配的 Retrieve/Confirm 证实后,支付行永久保持 `PAID`,退款事实只写退款表。 ## Technical Context **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**: 门店级凭证;单门店子订单;每订单多次历史尝试但最多一条阻断性尝试;首版只支持全额退款 ## Constitution Check `.specify/memory/constitution.md` 尚未定义项目条款,因此以下仓库规则作为强制门禁: - [x] 依赖方向保持 `ruoyi-admin -> ruoyi-system`;LINE HTTP 集成只放 `ruoyi-admin`。 - [x] Controller 使用明确 DTO、`@RequestBody/@RequestParam/@RequestHeader/@PathVariable`,不新增 Map 入参或 Bean Validation。 - [x] 所有 DDL/权限 SQL 只追加到 `updatesql/sql.md`,不直接执行数据库变更。 - [x] 不修改废弃支付与任务代码;仅在当前有效订单入口接入支付/退款门禁。 - [x] 平台新增文本四语同步,保留 Vue 文件 CRLF。 - [x] 修复/功能均先测试再实现,交付前编译、定向测试、前端校验和 diff 审核。 ## Project Structure ### Documentation ```text specs/019-line-pay/ ├── spec.md ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── tasks.md ├── contracts/ │ └── api.md └── checklists/ └── requirements.md ``` ### Source Code ```text 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 ``` ## Component Design ### Persistence boundary (`ruoyi-system`) - `IPosStoreLinePayService` 负责版本号分配、验证成功后的当前版本原子切换、启停和列表/详情。 - `IPosOrderLinePaymentService` 负责创建 `REQUESTING`、稳定键查询、App 选择规则、CAS 状态推进、活跃键释放、扫描和行租约。 - `IPosOrderLineRefundService` 负责 `UNIQUE(payment_id)` 的 insert-if-absent、CAS claim、扫描和退款事实。 - `IPaymentGatewayLogService` 只追加审计数据;日志不是状态机事实,日志写入失败不回滚资金事实。 - `PosOrderMapper` 新增条件更新:只有未付款、合法订单态且当前 `pay_type` 与目标渠道一致时,才允许领取支付渠道或更新支付 URL。 ### LINE gateway boundary (`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;调用方捕获日志异常后继续处理资金事实。 ### Orchestration boundary (`ruoyi-admin`) - `PaymentCreateGuardService` 使用共享 `pay:create:` 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` 领取;失败一笔不终止整批。 ### Platform and App boundary - `/pay/line/create`、`/query` 使用 `{ddId}` DTO 和现有 `@Auth + @Anonymous + token header` 组合;confirm/cancel 仅为匿名 GET 回跳。 - HTML 页面由 Controller 自包含返回,不依赖单独服务器部署;固定 App Scheme,不接受客户端 redirect 参数,并设置 no-store/no-referrer/CSP/frame 限制。 - 平台凭证页可查看当前版本详情、验证并切换新版本、启停;普通列表不携带 Secret,拥有详情权限的页面按用户决定可回显。 - 订单平台页显示 LINE 支付/退款状态,并提供人工查询与全额退款;资金动作使用独立权限。 ## Error and Recovery Policy | 场景 | 本地处理 | 后续动作 | |---|---|---| | 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` | 停止自动副作用,平台人工处理 | ## Implementation Order 1. 先落实 SQL 模型、实体/Mapper/Service 和状态 CAS 测试。 2. 实现 HMAC、HTTP 契约和凭证验证测试,再接凭证管理接口。 3. 实现 create/query、回跳页面和追加式尝试选择规则。 4. 实现 Retrieve/Check/Confirm 恢复、支付事实事务和取消竞态。 5. 实现全额退款与定时任务,再接两个真实取消入口和接单门禁。 6. 最小修改 OMG create 共享渠道门禁。 7. 完成平台前端、四语和权限 SQL,最后做全栈验证。 ## Verification Gates - `mvn -pl ruoyi-system -am -Dtest='*Line*Test' -Dsurefire.failIfNoSpecifiedTests=false test` - `mvn -pl ruoyi-admin -am -Dtest='*LinePay*Test,OrderLifecycleServiceTest,OmgPayControllerTest' -Dsurefire.failIfNoSpecifiedTests=false test` - `mvn -pl ruoyi-admin -am -DskipTests package` - 在 `foodie-admin-vue` 运行 `npm run lint` 和 `npm run build:prod`;若仓库已有无关 lint 错误,记录并对变更文件运行 ESLint。 - `git diff --check`、`git status --short`、逐文件核对换行/编码、确认未触碰废弃代码和用户已有 OMG SQL。 ## Complexity Tracking | Decision | Why Needed | Simpler Alternative Rejected Because | |---|---|---| | 订单 1:N 支付尝试 + 可空唯一活跃键 | 保留每次网关 orderId/transactionId/凭证与未知状态,避免迟到回跳污染新支付 | 覆盖单行会丢失恢复和退款所需键值 | | 不可变凭证版本 + payment.credential_id | 轮换后历史交易仍能找到原渠道身份 | 一店一行覆盖会让旧交易失去凭证归属 | | 支付与退款分表 | `PAID` 是资金事实,退款是另一事实与状态机 | 在支付状态写 REFUNDED 会掩盖曾经扣款 | | durable intent + 外部调用 + CAS | 数据库事务无法覆盖 LINE 网络副作用 | 单个 `@Transactional` 无法修复响应丢失和本地提交失败 |