# LINE Pay Online / Offline Implementation Quickstart ## 1. Prerequisites - JDK 21: `C:\Users\qmj\.jdks\graalvm-jdk-21.0.7` - MySQL schema changes from `updatesql/sql.md` applied manually by the developer - Redis/Redisson available - LINE Pay Sandbox Channel ID/Secret - 对应 LINE Pay 商户已开通 Offline API 权限;固定收款码但没有 API credentials 的商户不能联调 - 官方台湾 Sandbox My Code 生成器;不得使用真实 LINE App 生成的 My Code - Public HTTPS API base URL reachable by LINE redirect - Platform frontend: `E:\QtwCode\foodie\foodie-admin-vue` Do not execute schema migration from Codex. Do not use the deprecated `TestTask`, `ZaloPayController`, `PayController`, or `OrderAppealController` flows. ## 2. Runtime configuration `ruoyi-admin/src/main/resources/application.yml` contains non-secret defaults: ```yaml line-pay: environment: sandbox base-url: https://sandbox-api-pay.line.me confirm-url: https://api.example.com/pay/line/confirm cancel-url: https://api.example.com/pay/line/cancel reconcile: fixed-delay-ms: 60000 initial-delay-ms: 60000 batch-size: 20 row-lease-seconds: 120 round-budget-seconds: 45 auth-deadline-minutes: 30 unknown-deadline-hours: 24 ``` Channel credentials are saved per store through the platform API; they are not placed in application.yml. ## 3. Local verification Use JDK 21 only in the current PowerShell process: ```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='*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 ``` 现有 Online 平台前端回归(仅当实际修改 `foodie-admin-vue` 时执行): ```powershell Set-Location E:\QtwCode\foodie\foodie-admin-vue npm run lint npm run build:prod ``` Repository checks: ```powershell Set-Location E:\QtwCode\foodie\foodie_server git diff --check git status --short ``` Offline 增量的定向验证命令在全部后端功能、测试源码和 SQL 调整完成后统一执行: ```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='*Line*Test' -Dsurefire.failIfNoSpecifiedTests=false test mvn -pl ruoyi-admin -am -Dtest='*LinePayOffline*Test,PosOrderShOprateControllerTest,*LinePay*Test,OrderLifecycleServiceTest,OmgPaymentControllerTest' -Dsurefire.failIfNoSpecifiedTests=false test mvn -pl ruoyi-admin -am -DskipTests package ``` ## 4. Sandbox acceptance flow 1. Platform opens Store Payment → LINE Pay. 2. Enter store Channel ID/Secret; verify the detail shows a new current version and enabled state. 3. Create one single-store order with `payType=3`. 4. Call `/pay/line/create`; assert payment row is `WAITING_AUTH` and response uses `paymentUrl.web`. 5. Repeat create before completion; assert the same `paymentId/lineOrderId` is returned with `reusedAttempt=true`. 6. Complete Sandbox web authorization and allow confirm redirect. 7. Confirm page returns quickly and App query eventually reports `PAID`. 8. Query DB read-only and verify payment remains `PAID`, order `pay_status=1`, and gateway logs contain REQUEST/CHECK/CONFIRM/RETRIEVE events. 9. Cancel a paid order; verify exactly one refund row is created, eventually `REFUNDED`, while payment remains `PAID` and order becomes `pay_status=2`. 10. Create a second attempt only after the first is explicitly cancelled/expired/failed; verify both rows remain and only the second has `active_dd_id`. ## 5. Offline Sandbox acceptance flow 1. 使用商家 token 调 `/system/orderShOprate/createOrder` 创建 `paymentMethod="3"` 的单门店订单。 2. 只读检查子订单 `order_source=MERCHANT`;用户端订单和历史订单保持 `USER`。 3. 从官方台湾 Sandbox My Code 生成器取得 18 位 `oneTimeKey`,调用 `/system/orderShOprate/linePay/offline/pay`。 4. 检查支付行 `payment_mode=OFFLINE`,且数据库、应用日志、异常和 `payment_gateway_log.payload` 均不存在原始 `oneTimeKey`。 5. Pay 返回 `1145/1169` 或 Check 返回 `AUTH_READY` 时,状态接口返回 `AUTH_REQUIRED`;重复提交新的 My Code 只返回旧尝试,不再次调用 Pay。 6. Pay 超时、响应不明确或返回未识别结果码时,状态接口返回 `PROCESSING`,任务只按 `lineOrderId` 调 Offline Check;Check 未识别结果码同样不得释放活跃键。 7. Check 返回 `COMPLETE` 且订单号、交易号、金额合计、非空 `paymentProvider` 匹配时,状态最终为 `PAID`,订单结算和通知只执行一次。 8. 使用错误商家、本人 `shId` 搭配他店 `mdId`、用户端/历史订单、多门店订单、非 `payType=3` 订单或无效 My Code 请求,确认在调用 LINE 前拒绝。 9. 模拟金额不一致,确认 `captured_amount` 保存 LINE 实际扣款额、不触发正常履约、只创建一条以实际扣款额为金额的全额退款意图,并且原订单保持禁止再次支付。 10. 对已支付 Offline 订单执行取消/退款,确认 URI 使用 `lineOrderId`;对 Online 订单回归确认仍使用 `transactionId`。 ## 6. Required concurrency and security tests - Two simultaneous create calls for the same LINE order return one attempt. - LINE create and OMG create for the same order cannot both claim the payment channel. - confirm redirect, scheduled task and admin reconcile cannot Confirm twice. - user cancellation before late Capture still records PAID and creates one refund intent. - store cancellation and late Capture produce the same result and enforce store ownership. - credential rotation during a waiting/paid transaction does not change its `credential_id`; old transaction remains queryable/refundable. - UNKNOWN and MANUAL_REVIEW never release `active_dd_id` or directly repeat side effects. - Online 与 Offline 同时发起时只允许一个模式创建活跃尝试。 - 两个 Offline Pay 请求并发时只向 LINE 发送一次,后到请求的 `oneTimeKey` 不记录、不发送。 - `AUTH_REQUIRED`、`PROCESSING`、`AMOUNT_MISMATCH` 和缺失 `paymentProvider` 的 `MANUAL_REVIEW` 均阻止重新扫码。 - 普通商家和夜市商家按 `shId=userId` 鉴权,其他摊位商家按 `mdId=InfoUser.storeId` 鉴权。 - Controller、审计服务和异常分支的测试均断言原始 `oneTimeKey` 不可见。 ## 7. Production acceptance items Sandbox cannot prove every App handoff behavior. Before production enablement, verify on real iOS and Android devices: - LINE payment browser returns to the intermediate page and the fixed Scheme handoff works. - `com.twanmsdyh.app://payment/result` is registered and handled. - auto-open works where allowed; the manual button works in LINE WebView and external browsers. - App always calls `/query` and never trusts the deep-link as success. - current production credentials complete an end-to-end TWD payment and full refund. - 业务方确认该 Channel 已开通 Offline API,并用真实客户 My Code 验证 `AUTH_READY/COMPLETE/CANCEL/FAIL` 收敛。 - 验证 `paymentProvider` 的 `TSP/EPI` 原值;若生产返回其他非空值仍保存并允许完成,缺失或空值进入人工核对。