|
@@ -0,0 +1,256 @@
|
|
|
|
|
+# LINE Pay Offline API v4 商家扫码支付设计
|
|
|
|
|
+
|
|
|
|
|
+**状态**:已确认
|
|
|
|
|
+
|
|
|
|
|
+**确认日期**:2026-08-18
|
|
|
|
|
+
|
|
|
|
|
+**所属规格**:`019-line-pay`
|
|
|
|
|
+**实现范围**:仅 `foodie_server` 后端
|
|
|
|
|
+
|
|
|
|
|
+## 1. 目标和已确认边界
|
|
|
|
|
+
|
|
|
|
|
+在现有 LINE Pay Online API v4 支付不变的前提下,增加实体门店“商家扫描客户 LINE Pay 我的条码(My Code)”支付能力。
|
|
|
|
|
+
|
|
|
|
|
+- `payType="3"` 继续统一表示 LINE Pay,不为扫码支付新增支付渠道编号。
|
|
|
|
|
+- Online 与 Offline 通过支付尝试的 `payment_mode` 区分并长期并存。
|
|
|
|
|
+- 只有 `/system/orderShOprate/createOrder` 创建的新商家订单可以发起 Offline 扫码支付。
|
|
|
|
|
+- 商家订单只允许一个门店包;后端仍须校验,不能信任客户端传入的 `shId/mdId`。
|
|
|
|
|
+- 商家端是 uni-app。本期不修改 App、`foodie-store` 或 `foodie-admin-vue`,只定义后端接口契约。
|
|
|
|
|
+- App 只负责扫码并把 `oneTimeKey` 传给后端;金额、币种、商品、门店和 LINE `orderId` 均由后端生成或读取。
|
|
|
|
|
+- 台湾使用默认自动请款。本期不实现分开请款、Capture 或 Void。
|
|
|
|
|
+- 继续使用现有门店级 `pos_store_line_pay` 凭证及其已经批准的保存策略,本增量不变更 Channel Secret 的存储和回显规则。
|
|
|
|
|
+
|
|
|
|
|
+## 2. 官方文档核对结论
|
|
|
|
|
+
|
|
|
|
|
+本设计以 2026-08-18 可访问的 LINE Pay 官方文档为准:
|
|
|
|
|
+
|
|
|
|
|
+- [实体付款总览](https://developers-pay.line.me/zh/offline)
|
|
|
|
|
+- [提前准备](https://developers-pay.line.me/zh/offline/prerequisites)
|
|
|
|
|
+- [执行付款](https://developers-pay.line.me/zh/offline/implement-payment)
|
|
|
|
|
+- [授权与请款分开](https://developers-pay.line.me/zh/offline/implement-capture-separated-payment)
|
|
|
|
|
+- [查询付款明细](https://developers-pay.line.me/zh/offline/retrieve-payment-details)
|
|
|
|
|
+- [退款处理](https://developers-pay.line.me/zh/offline/handle-refund)
|
|
|
|
|
+- [LINE POINTS 处理](https://developers-pay.line.me/zh/offline/handle-point-payment-separately)
|
|
|
|
|
+- [Offline API v4 总览与错误码](https://developers-pay.line.me/zh/offline-api-v4)
|
|
|
|
|
+- [付款请求](https://developers-pay.line.me/zh/offline-api-v4/request-payment)
|
|
|
|
|
+- [查询付款状态](https://developers-pay.line.me/zh/offline-api-v4/check-payment-status)
|
|
|
|
|
+- [查询授权信息](https://developers-pay.line.me/zh/offline-api-v4/retrieve-confirmation-information)
|
|
|
|
|
+- [请款](https://developers-pay.line.me/zh/offline-api-v4/capture)
|
|
|
|
|
+- [取消授权](https://developers-pay.line.me/zh/offline-api-v4/void)
|
|
|
|
|
+- [查询付款明细 API](https://developers-pay.line.me/zh/offline-api-v4/retrieve-payment-details)
|
|
|
|
|
+- [退款 API](https://developers-pay.line.me/zh/offline-api-v4/refund)
|
|
|
|
|
+- [Sandbox](https://developers-pay.line.me/zh/sandbox)
|
|
|
|
|
+- [FAQ](https://developers-pay.line.me/zh/faq)
|
|
|
|
|
+- [API 变更日志](https://developers-pay.line.me/zh/api-change-log)
|
|
|
|
|
+- [专业术语](https://developers-pay.line.me/zh/glossary)
|
|
|
|
|
+
|
|
|
|
|
+直接影响实现的约束如下:
|
|
|
|
|
+
|
|
|
|
|
+1. Offline API v4 使用与 Online API v4 相同的 HMAC-SHA256 请求认证;v4 不需要服务器 IP allowlist。
|
|
|
|
|
+2. 台湾 `oneTimeKey` 长度为 18,生成后五分钟内有效。
|
|
|
|
|
+3. `POST /v4/payments/oneTimeKeys/pay` 的 Read Timeout 至少为 40 秒。
|
|
|
|
|
+4. 付款请求超时或没有收到响应时,使用 `GET /v4/payments/orders/{orderId}/check` 查询;该接口 Read Timeout 至少为 20 秒,禁止直接重复付款请求。
|
|
|
|
|
+5. 状态查询的 `info.status` 为 `AUTH_READY`、`COMPLETE`、`CANCEL` 或 `FAIL`。
|
|
|
|
|
+6. `AUTH_READY` 以及结果码 `1169` 表明客户仍可能需要在 LINE Pay 选择付款方式并完成密码认证,扫码本身不等于支付成功。
|
|
|
|
|
+7. 台湾默认自动请款;只有事先向 LINE Pay 申请并配置分开请款时才使用 Capture/Void。
|
|
|
|
|
+8. 成功后必须校验 `info.payInfo[].amount` 合计与请求金额一致;不一致必须退款。
|
|
|
|
|
+9. Offline 全额退款使用 `POST /v4/payments/orders/{orderId}/refund`,省略 `refundAmount`。
|
|
|
|
|
+10. API 业务结果通过 `returnCode` 判断,不能把 HTTP 200 当成成功。
|
|
|
|
|
+11. `transactionId` 是 19 位整数,沿用现有字符串存储和 JSON 字符串输出。
|
|
|
|
|
+12. v4 返回的 `paymentProvider` 可能为 `TSP` 或 `EPI`,沿用现有字段保存原值。
|
|
|
|
|
+13. Sandbox 不能使用真实 LINE App 生成的 My Code,必须使用官方 Sandbox My Code 生成器;Sandbox 不能测试 iPASS Money 条码。
|
|
|
|
|
+14. `X-LINE-MerchantDeviceProfileId` 和 `X-LINE-MerchantDeviceType` 为成对出现的选填请求头,本期不接收或发送设备信息。
|
|
|
|
|
+
|
|
|
|
|
+## 3. 方案选择
|
|
|
|
|
+
|
|
|
|
|
+采用“扩展现有支付尝试”方案:在 `pos_order_line_payment` 增加模式字段,复用现有凭证、审计、支付事实、退款事实、行租约和恢复任务。
|
|
|
|
|
+
|
|
|
|
|
+未采用以下方案:
|
|
|
|
|
+
|
|
|
|
|
+- 新建 Offline 专用支付表:隔离清晰,但会复制凭证、状态机、退款、审计和恢复逻辑。
|
|
|
|
|
+- 不增加模式字段:改动更少,但查询、退款和恢复无法可靠判断应调用 Online 还是 Offline 端点。
|
|
|
|
|
+
|
|
|
|
|
+## 4. 数据模型
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 `pos_order.order_source`
|
|
|
|
|
+
|
|
|
|
|
+新增 `order_source VARCHAR(16) NOT NULL DEFAULT 'USER'`:
|
|
|
|
|
+
|
|
|
|
|
+- `USER`:用户端或历史订单,不允许 Offline 扫码支付。
|
|
|
|
|
+- `MERCHANT`:由商家下单接口创建,允许在通过其他门禁后发起 Offline 扫码支付。
|
|
|
|
|
+
|
|
|
|
|
+`/system/orderShOprate/createOrder` 必须显式写入 `MERCHANT`。默认值使历史订单保持不可扫码,不做历史来源推断或回填。
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 `pos_order_line_payment.payment_mode`
|
|
|
|
|
+
|
|
|
|
|
+新增 `payment_mode VARCHAR(16) NOT NULL DEFAULT 'ONLINE'`:
|
|
|
|
|
+
|
|
|
|
|
+- `ONLINE`:现有 Online API v4 流水。
|
|
|
|
|
+- `OFFLINE`:本次新增的 My Code 扫码流水。
|
|
|
|
|
+
|
|
|
|
|
+所有现有行通过默认值保持 `ONLINE`。新建 Online/Offline 尝试时必须显式写入模式。退款、查询和恢复按模式选择端点,不根据是否存在 `paymentUrl` 等可变字段猜测。
|
|
|
|
|
+
|
|
|
|
|
+不保存 `oneTimeKey`,也不新增设备信息字段。
|
|
|
|
|
+
|
|
|
|
|
+## 5. 商家订单门禁
|
|
|
|
|
+
|
|
|
|
|
+商家下单接口在创建订单前必须一次性完成以下校验:
|
|
|
|
|
+
|
|
|
|
|
+1. token 对应有效商家用户;不能仅凭客户端调用了商家 URL 判断身份。
|
|
|
|
|
+2. `items` 非空且恰好一个门店包,所有门店字段一致。
|
|
|
|
|
+3. 根据项目既有商家归属规则校验 `shId/mdId`,禁止商家替其他门店创建可扫码订单。
|
|
|
|
|
+4. 商品门店、订单金额和 `paymentMethod` 通过现有业务规则。
|
|
|
|
|
+5. 创建出的 `PosOrder` 显式写入 `orderSource=MERCHANT`。
|
|
|
|
|
+
|
|
|
|
|
+扫码接口还必须验证:订单来源为 `MERCHANT`、属于当前商家、`payType="3"`、未支付、未取消、未完成、金额大于零、只有一个子订单/门店,并且门店存在已启用 LINE Pay 凭证。
|
|
|
|
|
+
|
|
|
|
|
+## 6. 后端接口契约
|
|
|
|
|
+
|
|
|
|
|
+### 6.1 发起扫码付款
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+POST /system/orderShOprate/linePay/offline/pay
|
|
|
|
|
+Header: token
|
|
|
|
|
+Content-Type: application/json
|
|
|
|
|
+
|
|
|
|
|
+{
|
|
|
|
|
+ "ddId": "Foodie 业务订单号",
|
|
|
|
|
+ "oneTimeKey": "台湾 18 位 My Code"
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- Controller 使用明确 DTO、`@RequestBody` 和 `@RequestHeader String token`,不使用 Map 或 Bean Validation。
|
|
|
|
|
+- `ddId` 用于定位 Foodie 订单;提交给 LINE 的 `orderId` 是后端为本次支付尝试生成的永久唯一 `line_order_id`,两者不得混用。
|
|
|
|
|
+- `oneTimeKey` 只允许本次方法调用链在内存中使用,接口返回前不持久化。
|
|
|
|
|
+
|
|
|
|
|
+### 6.2 查询本地支付状态
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+GET /system/orderShOprate/linePay/offline/status?ddId=<Foodie业务订单号>
|
|
|
|
|
+Header: token
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+查询接口先验证商家订单归属,再返回稳定的本地结果:
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+AUTH_REQUIRED 客户需在 LINE Pay 完成认证
|
|
|
|
|
+PROCESSING 网关结果确认中,禁止重新扫码扣款
|
|
|
|
|
+PAID 已核实支付成功
|
|
|
|
|
+CANCELLED 客户取消
|
|
|
|
|
+FAILED 明确失败,需要客户生成新的 My Code
|
|
|
|
|
+MANUAL_REVIEW 结果长期不明确,需要人工核对
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+响应不返回 `oneTimeKey`、Channel Secret、HMAC、网关原始请求体或内部异常。
|
|
|
|
|
+
|
|
|
|
|
+## 7. Offline 付款请求
|
|
|
|
|
+
|
|
|
|
|
+`LinePayClient` 增加独立方法,不能复用 Online `request/check/refund` 的 URI:
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+POST /v4/payments/oneTimeKeys/pay
|
|
|
|
|
+GET /v4/payments/orders/{lineOrderId}/check
|
|
|
|
|
+POST /v4/payments/orders/{lineOrderId}/refund
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+首版付款请求仅发送自动请款所需字段,不发送 redirect URL、Capture/Void 或设备请求头:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "amount": 100,
|
|
|
|
|
+ "currency": "TWD",
|
|
|
|
|
+ "oneTimeKey": "<仅发送给LINE,不记录>",
|
|
|
|
|
+ "orderId": "后端生成的lineOrderId",
|
|
|
|
|
+ "packages": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": "单门店包标识",
|
|
|
|
|
+ "amount": 100,
|
|
|
|
|
+ "products": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "服务端生成的订单商品摘要",
|
|
|
|
|
+ "quantity": 1,
|
|
|
|
|
+ "price": 100
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`amount` 必须等于 package 金额合计;package 金额必须等于 product 的 `quantity × price` 合计。签名使用实际发送的同一份 UTF-8 JSON 字符串。
|
|
|
|
|
+
|
|
|
|
|
+## 8. 状态机和错误处理
|
|
|
|
|
+
|
|
|
|
|
+Offline 流水复用现有基础状态:
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+REQUESTING
|
|
|
|
|
+REQUEST_UNKNOWN
|
|
|
|
|
+WAITING_AUTH
|
|
|
|
|
+PAID
|
|
|
|
|
+CANCELLED_OR_EXPIRED
|
|
|
|
|
+FAILED
|
|
|
|
|
+AMOUNT_MISMATCH
|
|
|
|
|
+MANUAL_REVIEW
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- `0000`:核对 `orderId`、`transactionId`、`payInfo` 金额合计和 `paymentProvider`;全部一致后落 `PAID` 并且只执行一次订单支付事实与后续副作用。币种固定取本地请求事实 `TWD`,不要求付款响应返回该字段。
|
|
|
|
|
+- `1145`、`1169`:进入 `WAITING_AUTH`,本地返回 `AUTH_REQUIRED/PROCESSING`,后续按 `lineOrderId` 查询。
|
|
|
|
|
+- `1152`、`1172`、`1198`:存在相同或重复请求的可能,不直接判失败,转只读状态查询。
|
|
|
|
|
+- 网络超时、响应丢失、无法解析响应、`1199`、`190X`、`9000`:进入 `REQUEST_UNKNOWN`,禁止重新调用付款接口,只能状态查询。
|
|
|
|
|
+- `1133`:My Code 无效或过期,明确失败并提示重新生成 My Code。
|
|
|
|
|
+- 余额不足、用户/信用卡不可用、金额/限额等明确业务拒绝:进入 `FAILED`。
|
|
|
|
|
+- 查询 `AUTH_READY`:保持 `WAITING_AUTH`。
|
|
|
|
|
+- 查询 `COMPLETE`:核对交易号、订单号和 `payInfo` 金额合计后落支付事实;查询响应未返回的币种继续以本地请求事实 `TWD` 为准。
|
|
|
|
|
+- 查询 `CANCEL`:进入 `CANCELLED_OR_EXPIRED` 并释放活跃尝试。
|
|
|
|
|
+- 查询 `FAIL`:进入 `FAILED` 并释放活跃尝试。
|
|
|
|
|
+- 查询暂时 `1150` 或未取得终态:在恢复截止时间前继续只读查询;到期进入 `MANUAL_REVIEW`,不自动释放订单占用。
|
|
|
|
|
+
|
|
|
|
|
+若 `payInfo` 合计与订单金额不一致,支付行进入 `AMOUNT_MISMATCH`,记录真实交易号并立即创建唯一全额退款意图;不得执行正常订单已支付/履约副作用,也不得允许再次扫码,退款结果仍不明确时进入人工核对。
|
|
|
|
|
+
|
|
|
|
|
+## 9. 并发和幂等
|
|
|
|
|
+
|
|
|
|
|
+- 同一订单的 Online 和 Offline 尝试共用现有订单级支付创建锁和 `active_dd_id` 唯一门禁。
|
|
|
|
|
+- 已有任一模式的活跃尝试时,不发起另一个模式的新请求。
|
|
|
|
|
+- 重复提交 Offline 接口时,若已经存在 Offline 活跃尝试,只返回其当前状态;新传入的 `oneTimeKey` 不发送、不记录。
|
|
|
|
|
+- 只有 `CANCELLED_OR_EXPIRED` 或明确 `FAILED` 释放后,才能使用客户新生成的 My Code 创建新尝试。
|
|
|
|
|
+- `PAID`、`REQUEST_UNKNOWN`、`WAITING_AUTH`、`AMOUNT_MISMATCH` 和 `MANUAL_REVIEW` 持续阻断新支付。
|
|
|
|
|
+- 支付事实、退款意图、订单 `payStatus` 和通知/结算副作用均使用现有 CAS/唯一约束保证至多一次。
|
|
|
|
|
+
|
|
|
|
|
+## 10. 退款和取消订单
|
|
|
|
|
+
|
|
|
|
|
+- Online 流水继续按 `transactionId` 调用 `/v4/payments/{transactionId}/refund`。
|
|
|
|
|
+- Offline 流水按 `lineOrderId` 调用 `/v4/payments/orders/{lineOrderId}/refund`。
|
|
|
|
|
+- 两种模式的全额退款请求均省略 `refundAmount`,但 URI 必须根据 `payment_mode` 分派。
|
|
|
|
|
+- 退款响应超时或结果未知时只调用 Retrieve 检查 `refundList`,不得盲目重复 Refund。
|
|
|
|
|
+- 订单取消与迟到支付竞态继续复用现有“记录真实付款后创建唯一退款意图”规则。
|
|
|
|
|
+
|
|
|
|
|
+## 11. 日志和敏感数据
|
|
|
|
|
+
|
|
|
|
|
+- `oneTimeKey` 不得进入数据库、普通日志、异常文本、Controller 响应或 `payment_gateway_log.payload`。
|
|
|
|
|
+- Offline REQUEST 审计日志只保存脱敏后的业务摘要;若记录 JSON,`oneTimeKey` 固定替换为 `<redacted>`。
|
|
|
|
|
+- 网关 `returnCode`、`returnMessage`、`lineOrderId`、字符串 `transactionId`、`paymentProvider` 和耗时可以记录。
|
|
|
|
|
+- 本增量不改变现有规格已经批准的 Channel Secret 保存和权限回显策略;真实 Secret 仍不得写入源码、测试夹具或支付交互日志。
|
|
|
|
|
+
|
|
|
|
|
+## 12. Sandbox 和生产前置条件
|
|
|
|
|
+
|
|
|
|
|
+- Sandbox 使用 `https://sandbox-api-pay.line.me` 和官方台湾 My Code 生成器。
|
|
|
|
|
+- 自动化测试不得把真实 LINE App My Code 用于 Sandbox,也不把 Sandbox iPASS Money 作为验收项。
|
|
|
|
|
+- EPI 测试使用官方变更日志列出的 My Code preset,并验证 `paymentProvider` 原值。
|
|
|
|
|
+- 生产启用前必须确认对应 LINE Pay 商户已取得 Channel ID/Secret 且具备 Offline API 商户权限;只有固定收款码、无法取得 API credentials 的商户不满足接入条件。
|
|
|
|
|
+
|
|
|
|
|
+## 13. 验证范围
|
|
|
|
|
+
|
|
|
|
|
+书面规格审批后,实施计划至少覆盖:
|
|
|
|
|
+
|
|
|
|
|
+1. 商家下单身份、归属、单门店和订单来源测试。
|
|
|
|
|
+2. Entity、Mapper、SQL 默认值及 Online 历史数据兼容测试。
|
|
|
|
|
+3. Offline HMAC 精确请求体、40 秒付款超时和 20 秒查询/退款超时测试。
|
|
|
|
|
+4. `oneTimeKey` 18 位校验、禁止落库和全链路日志脱敏测试。
|
|
|
|
|
+5. `0000/1145/1169/1133/重复请求/未知结果` 映射测试。
|
|
|
|
|
+6. `AUTH_READY/COMPLETE/CANCEL/FAIL` 状态查询测试。
|
|
|
|
|
+7. 超时后只查询、不重复付款请求的测试。
|
|
|
|
|
+8. 金额合计一致、金额不一致自动退款且不触发履约的测试。
|
|
|
|
|
+9. Online/Offline 活跃尝试互斥和重复扫码并发测试。
|
|
|
|
|
+10. Offline 按 `lineOrderId` 退款、Online 仍按 `transactionId` 退款的回归测试。
|
|
|
|
|
+11. 使用官方 Sandbox My Code 完成真实端到端验证;若缺少商户权限或凭证,明确记录为未执行项。
|