brainstorm.md 11 KB

LINE Pay Sandbox 接入头脑风暴记录

日期:2026-08-11
状态:暂停,待继续设计评审
目标:以 spec-kit 流程接入 LINE Pay Online API v4 Sandbox,并在设计批准后依次完成 specify -> plan -> tasks -> implement

本文只记录已确认决策和待评审设计,不代表实现已获完整批准。不得在文档、源码、配置文件或日志中写入真实 Channel Secret。

1. 已确认的业务决策

议题 决策
与现有 OMG 的关系 方案 A:LINE Pay 与 OMG 并存,不替换 OMG
支付方式编号 暂定 payType=8,最终在规格阶段核对现有枚举后固化
API 版本 Online API v4,Request、Check、Confirm、Retrieve、Refund 全部统一使用 /v4
环境 首版接入 LINE Pay Sandbox
凭证模式 商家独立凭证,每个门店使用自己的 Channel ID / Channel Secret
凭证归属 与现有 OMG 一致,按 pos_store 一店一套;订单通过 PosOrder.mdId 定位凭证
凭证维护者 平台管理员;商家端不录入、不查看 Channel Secret
付款确认 confirmUrlType=CLIENT
请款方式 Confirm 时自动请款,不实现授权/请款分离、Capture 或 Void
退款范围 首版只支持全额退款
币种与金额 固定 TWD,整数金额;金额仅取服务端订单,Request/Confirm/Refund 必须一致
实现范围 foodie_server 后端 + foodie-admin-vue 平台管理前端;用户端只交付 API 契约
Sandbox 凭证 已具备;后续通过管理页面安全录入,不在本文记录
公网后端地址 https://foodieapi.waimai-paotui.com
用户端结果地址 暂留空占位,后续补充;不得硬编码虚假地址

2. LINE Pay 回跳地址

LINE Pay 配置使用以下两个后端 HTTPS 地址:

confirmUrl: https://foodieapi.waimai-paotui.com/pay/line/confirm
cancelUrl:  https://foodieapi.waimai-paotui.com/pay/line/cancel

它们不是用户端最终页面:

  • confirmUrl 只表示用户已完成 LINE Pay 认证,后端仍须调用 Confirm 或查询 API,不能直接把订单标为已支付。
  • cancelUrl 只结束本次支付尝试,不直接取消外卖订单。
  • 后端处理完成后应 302 跳至固定配置的用户端结果地址;该地址尚未确定。
  • 结果地址为空时,不进行开放重定向,也不接受请求参数提供跳转目标;实现应返回安全的中性提示页。

项目已有 App Scheme com.twanmsdyh.app,曾提议以后使用统一入口:

com.twanmsdyh.app://payment/result?status=<success|failed|cancelled>&orderId=<orderId>

该路径尚未由用户端确认,当前仅作为候选,不得直接视为有效契约。

3. 官方文档核验结论

3.1 版本与 Sandbox

  • 台湾新接入优先使用 Online API v4。v4 于 2025-11 增加 paymentProviderTSP / EPI)。
  • 官方基础付款指南仍含 v3 示例,不能因此混用 v3;实现路径必须全部为 /v4
  • Sandbox 主机:https://sandbox-api-pay.line.me
  • Production 主机:https://api-pay.line.me,本阶段不启用。
  • Sandbox Online 支付只能验证 Web 收银台,必须使用 info.paymentUrl.web;不能以 Sandbox 验证 App payment URL。
  • Sandbox 的 paymentProvider 固定为 TSP,无法模拟 EPI;EPI 响应兼容必须列为生产前独立验收项。

3.2 v4 核心 API

POST /v4/payments/request
GET  /v4/payments/requests/{transactionId}/check
POST /v4/payments/{transactionId}/confirm
GET  /v4/payments
POST /v4/payments/{transactionId}/refund

建议最短 Read timeout:Request 10 秒、Check/Retrieve/Refund 20 秒、Confirm 40 秒。HTTP 200 不代表业务成功,必须判断 returnCode

3.3 HMAC 签名

请求头:

Content-Type: application/json
X-LINE-ChannelId
X-LINE-Authorization
X-LINE-Authorization-Nonce

签名使用 HMAC-SHA256,key 为 Channel Secret,结果 Base64:

GET:  channelSecret + apiPath + queryString + nonce
POST: channelSecret + apiPath + exactRequestBody + nonce

关键约束:

  • POST JSON 必须只序列化一次;用于签名的字符串与实际发送内容必须完全一致。
  • GET query 的参数顺序、重复参数、编码和值必须与实际 URL 完全一致。
  • apiPath 包含准确的 /v4 路径和路径参数,不包含 scheme 或 host。
  • nonce 使用 UUID v4;同一次请求的签名和请求头必须使用同一值。
  • 必须为 POST 精确 JSON、GET query、空 body 和非 ASCII 内容编写签名契约测试。

3.4 交易事实与幂等

  • confirmUrl 是无签名的浏览器 GET 回跳,不是可信支付成功通知。
  • LINE Pay 未提供可直接作为最终支付事实的签名 webhook。
  • 本地必须先校验 orderId + transactionId + 门店 + 金额 + 币种,再由服务器 Confirm/查询取证。
  • LINE Pay orderId 必须全局唯一;每次新的支付尝试使用独立 LINE orderId,不能直接复用外卖订单号。
  • transactionId 为 19 位数字,全链路按字符串存储和返回,避免 JavaScript 精度丢失。
  • 重复回跳、用户刷新、乱序回跳和并发 Confirm 只能有一个执行者,其余返回已有结果。
  • Confirm/Refund 超时、1198 或临时错误后先调用 Retrieve 对账,不盲目重复有副作用的请求。
  • cancelUrl 或 Check 0121 不能覆盖已通过 Confirm/Retrieve 证实的支付终态。

4. 对抗复核必须覆盖的风险

实现与测试至少要回答以下问题:

  1. 所有 API 是否统一使用 v4,是否完整保存 v4 的 paymentProvider
  2. 签名 JSON/Query 是否与实际发送内容逐字节一致。
  3. orderId 是否永久唯一,transactionId 是否始终按字符串处理。
  4. Request、Confirm、订单和退款金额是否全部来自同一服务端事实。
  5. 是否错误地把 HTTP 200、confirmUrl 到达或 cancelUrl 到达视为最终支付状态。
  6. Confirm/Refund 已被 LINE 执行但响应丢失时,是否能通过 Retrieve 恢复最终事实。
  7. Sandbox 无法测试 EPI 和 App payment URL 时,是否在上线清单中单独保留生产验收。
  8. 用户取消订单与迟到的支付成功发生竞态时,是否先记录真实支付,再自动全额退款或进入人工核对,而不是丢弃付款事实。
  9. 重复回跳、重复退款、定时对账和人工操作之间是否使用数据库条件更新/锁保证幂等。
  10. 日志、接口响应和平台页面是否始终隐藏 Channel Secret、HMAC 原文及敏感 token。

5. 已比较的实现路线

方案 1:独立 LINE Pay 模块(已批准)

新建 LINE Pay 凭证、支付和退款流水。只在订单取消、退款和后台查询处增加很薄的支付方式分派,不重构 OMG。

优点:边界清楚、对现有 OMG 风险较低,并能完整处理幂等、对账和审计。

方案 2:统一支付框架(未采用)

建立通用 PaymentProvider 接口,并同时迁移 OMG。长期结构整洁,但超出本次范围,会扩大已运行 OMG 的回归风险。

方案 3:Controller 直接接入(拒绝)

不建独立支付/退款账本,只把交易号写入订单。无法可靠处理回跳重复、网络超时、退款恢复和审计,不满足支付安全要求。

6. 已批准的架构边界

6.1 ruoyi-system

负责纯数据库能力,不引入 LINE Pay HTTP 客户端或 com.ruoyi.app.*

  • pos_store_line_pay:每个 pos_store 一套加密凭证。
  • pos_order_line_payment:每次 Request/Confirm 的独立支付流水。
  • pos_order_line_refund:每次全额退款操作的独立流水。
  • 相应 Entity、Mapper XML、Service。

6.2 ruoyi-admin

  • LinePayClient:v4 HTTP、精确 JSON/Query 签名、超时及结果码解析。
  • LinePayService:创建、Confirm、查询、全额退款和状态机。
  • LinePayController:用户支付接口及匿名 CLIENT 回跳接口。
  • PosStoreLinePayController:平台管理员维护门店凭证和启用状态。
  • LinePayReconcileTask:恢复漏回跳、Confirm 未知和 Refund 未知状态。
  • 订单取消入口按 payType 分派至 OMG 或 LINE Pay;不引用废弃支付 Controller。

预定用户支付接口:

POST /pay/line/create
GET  /pay/line/confirm
GET  /pay/line/cancel
POST /pay/line/query
POST /pay/line/refund
  • create/query/refund 使用明确 DTO;需要登录的接口通过 @RequestHeader String token 获取 token。
  • confirm/cancel 使用显式 @RequestParam,允许匿名 GET,并按重复、乱序请求设计。
  • Sandbox 的创建响应只向调用方提供 paymentUrl.web

6.3 foodie-admin-vue

新增“LINE Pay 门店管理”,交互风格参考现有 OMG 页面,但数据和状态完全独立:

  • 平台管理员分页查看门店开通/启用状态。
  • 录入或轮换 Channel ID / Channel Secret。
  • Secret 只允许写入,不允许读取回显;详情仅返回 hasSecret 等脱敏状态。
  • 启停门店 LINE Pay。
  • 新增用户可见文本必须同步 zh/tw/en/vi 四个 i18n 文件。

7. 安全设计方向(待详细评审)

  • Channel Secret 使用 AES-256-GCM 加密落库,保存随机 nonce、密文和版本;主密钥只从服务端环境变量读取。
  • Channel ID 可查询但默认脱敏展示;Channel Secret 永不回显。
  • 不把 Secret、签名原文、Authorization、paymentAccessToken 写入日志。
  • 凭证更新与启用分开;是否加入无扣款的在线探测仍需在详细设计中确定,不能依赖未被 LINE 官方保证的响应语义。
  • 外部回跳只能使用服务端固定配置的结果地址,禁止请求参数控制 302 目标。
  • 所有数据库迁移 SQL 只写入 updatesql/sql.md,不直接执行。

8. 明天继续的设计评审顺序

  1. 数据表字段、索引、状态枚举和凭证加密/轮换。
  2. Request -> CLIENT 回跳 -> Confirm -> PAID 的事务边界与幂等。
  3. Confirm/Refund 超时、迟到成功、取消竞态和定时对账。
  4. Controller 契约、平台管理 API 与 foodie-admin-vue 页面。
  5. 四语 i18n、错误映射、日志脱敏与权限。
  6. Sandbox 自动化测试、真实凭证联调及生产前 EPI/App 验收。
  7. 全部设计获批后创建正式 spec.md,自审并等待批准,再生成 plan.mdtasks.md,最后按 TDD 实现。

9. 官方资料