# 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`：存在相同或重复请求的可能，不直接判失败，转只读状态查询；任何未识别 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` 固定替换为 `<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 完成真实端到端验证；若缺少商户权限或凭证，明确记录为未执行项。
