# 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= 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`:存在相同或重复请求的可能,不直接判失败,转只读状态查询;任何未识别 Pay/Check 返回码也按未知结果处理,不能按失败释放订单。 - 网络超时、响应丢失、无法解析响应、`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`,记录真实交易号和 `captured_amount`,并在同一事务内以实际扣款额创建唯一全额退款意图;不得执行正常订单已支付/履约副作用,也不得允许再次扫码,退款结果仍不明确时进入人工核对。 ## 9. 并发和幂等 - 同一订单的 Online 和 Offline 尝试共用现有订单级支付创建锁和 `active_dd_id` 唯一门禁。 - 已有任一模式的活跃尝试时,不发起另一个模式的新请求。 - 重复提交 Offline 接口时,若已经存在 Offline 活跃尝试,只返回其当前状态;新传入的 `oneTimeKey` 不发送、不记录。 - 创建尝试发生唯一键冲突时直接复用数据库中的活跃尝试;所有状态 CAS 失败后重读数据库状态,不以过期内存对象继续网关副作用。 - 只有 `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` 固定替换为 ``。 - 网关 `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 完成真实端到端验证;若缺少商户权限或凭证,明确记录为未执行项。