# LINE Pay 前端接入文档(客户 App) > 适用对象:客户 App(uni-app)前端开发人员 > 对应后端:foodie_server `019-line-pay`,`LinePayController`(`/pay/line/**`) > 后端契约权威:本目录 `contracts/api.md`、`spec.md` > 最后更新:2026-08-13 本文档只覆盖**客户 App 需要对接的部分**:发起支付、跳转 LINE 收银台、接收 App Scheme 回跳、查询最终结果。门店凭证配置(平台管理端)详见 `contracts/api.md` 第 3 节,不在本文档范围。 --- ## 1. 概述 LINE Pay 直连是项目餐饮订单的线上支付渠道之一,订单字段 `pos_order.pay_type = "3"` 即表示该笔订单使用 LINE Pay。 **本期约束(前端必须知道):** - 仅支持**单门店**餐饮订单;多门店父单无法发起 LINE Pay(后端会拒绝)。 - 币种固定 **TWD**,金额为整数,全部由**服务端订单决定**,前端不传金额。 - 仅支持**全额退款**,且退款由后端在订单取消时自动处理,**前端没有独立的退款接口**。 - LINE Pay 不提供同步成功回调。**支付结果永远以 `/pay/line/query` 的返回为准**,绝不能信任 Scheme 参数或 LINE 收银台页面判断是否付款成功。 --- ## 2. 整体流程 ```mermaid sequenceDiagram participant App as 客户 App participant Server as foodie_server participant Line as LINE 收银台 App->>Server: ① 下单(订单 payType=3, LINE Pay) App->>Server: ② POST /pay/line/create {ddId} Server-->>App: 返回 paymentUrl + 状态 WAITING_AUTH App->>Line: ③ 在 WebView / 外部浏览器打开 paymentUrl Note over Line: 用户完成 LINE 登录授权 Line->>Server: ④ LINE 回跳 /pay/line/confirm(后端处理) Server-->>Line: 返回中间页 HTML(快速返回,不同步 Confirm) Note over Server: 后端异步 Confirm + 请款 Server->>App: ⑤ 中间页唤起 Scheme
com.twanmsdyh.app://payment/result?orderId=ddId App->>Server: ⑥ POST /pay/line/query {ddId}(带 token) Server-->>App: 返回最终状态(PAID / 退款等) Note over App: 若仍处理中,轮询 query 直到终态 ``` **一句话流程:** 下单选 LINE Pay → 调 `create` 拿收银台链接 → 打开让用户授权 → App 被 Scheme 唤回 → 调 `query` 拿最终结果。 --- ## 3. 前置条件 1. **门店已开通 LINE Pay 并启用**:订单所属门店在平台管理端配置了有效 Channel 凭证且处于启用状态。否则 `create` 会返回业务错误。 2. **订单 `payType = "3"`**:下单时必须把支付方式指定为 LINE Pay。后端 `create` 会校验当前 `payType`,**不允许 create 时临时切换渠道**。 3. **订单可支付**:未支付、未取消、未完成。堂食订单(`state=1`)也可支付。 4. **用户已登录**:`create` / `query` 都需要请求头携带登录 `token`。 5. **App 已注册自定义 Scheme**:见第 5 节。 --- ## 4. 接口说明 所有接口走项目统一 `AjaxResult` 外壳:`{ code, msg, data }`。`code === 200` 为成功,其余为业务错误,`msg` 为国际化错误文案(直接展示即可)。 > **重要:`transactionId` 是 19 位长数字,必须按字符串处理。** 用 JS `Number` / 算术运算会导致精度丢失。JSON 解析、存储、比较全程保持字符串。 ### 4.1 创建支付 `POST /pay/line/create` 向 LINE 发起支付请求,返回 LINE 收银台地址。 **请求** - Header:`token: <登录 token>`(必填) - Body(JSON): ```json { "ddId": "202608120001" } ``` | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `ddId` | string | 是 | 业务订单号 `pos_order.dd_id` | **成功响应 `data`** ```json { "ddId": "202608120001", "paymentId": 42, "lineOrderId": "LP2026081200010001", "transactionId": "2026081200000000001", "paymentUrl": "https://sandbox-web-pay.line.me/...", "status": "WAITING_AUTH", "reusedAttempt": false } ``` | 字段 | 类型 | 说明 | |---|---|---| | `ddId` | string | 回显订单号 | | `paymentId` | number | 后端支付流水主键,排查问题时提供 | | `lineOrderId` | string | 后端生成的 LINE 订单号(**不是 ddId**),前端一般不用 | | `transactionId` | string \| null | LINE 19 位交易号,**字符串**,可能为空(尚未生成时) | | `paymentUrl` | string | **LINE 收银台地址,前端需在浏览器/WebView 打开它** | | `status` | string | 支付状态(见第 6 节),正常为 `WAITING_AUTH` | | `reusedAttempt` | boolean | `true` 表示本次复用了已有支付尝试(见下方说明) | **关于 `reusedAttempt`(重复点击处理):** 后端保证同一订单**任意时刻最多一条活跃 LINE 支付尝试**。前端重复点击「去支付」时: - 若上一笔仍在等待授权 / 处理中,后端**复用原尝试**,返回**同一个 `paymentUrl`**,`reusedAttempt = true` —— 前端直接用返回的 `paymentUrl` 跳转即可,**不要当成错误**。 - 若上一笔已被 LINE 明确判为取消 / 过期 / 失败,后端才会新建尝试,`reusedAttempt = false`。 - 若已支付,返回的 `status` 为 `PAID`。 **业务错误(`code !== 200`)** `msg` 已国际化,典型场景:订单不存在 / 非本人订单 / `payType` 不是 3 / 多门店父单 / 订单不可支付 / 门店未开通或未启用 / 已有处理中尝试需稍后重试。前端直接展示 `msg`,无需自己映射。 --- ### 4.2 查询支付 `POST /pay/line/query` 查询订单的支付/退款最终状态。**该接口只读本地数据库,不会同步去调 LINE,响应很快。** **请求** - Header:`token: <登录 token>`(必填) - Body(JSON): ```json { "ddId": "202608120001" } ``` **成功响应 `data`** ```json { "ddId": "202608120001", "paymentId": 42, "orderPayStatus": 1, "paymentStatus": "PAID", "refundStatus": null, "transactionId": "2026081200000000001", "updatedAt": "2026-08-12T12:34:56+08:00" } ``` | 字段 | 类型 | 说明 | |---|---|---| | `ddId` | string | 回显订单号 | | `paymentId` | number | 当前选中的支付流水主键 | | `orderPayStatus` | number | **订单支付状态**,见下表 | | `paymentStatus` | string | LINE 支付流水状态,见第 6 节 | | `refundStatus` | string \| null | 退款状态,未退款为 `null`,见第 6 节 | | `transactionId` | string \| null | LINE 交易号,**字符串** | | `updatedAt` | string | 最近更新时间(ISO-8601) | **`orderPayStatus` 枚举(前端主要依据这个判定结果)** | 值 | 含义 | 前端处理 | |---|---|---| | `0` | 未支付 | 支付尚未完成(看 `paymentStatus` 判断是否还在处理中) | | `1` | 已支付 | 支付成功,展示成功页 | | `2` | 已退款 | 已全额退款(取消订单后触发) | --- ## 5. App Scheme 回跳 LINE 收银台授权完成后,LINE 会回跳后端 `/pay/line/confirm`,后端返回一个**中间页**。该中间页会尝试唤起 App,并显示「支付结果确认中,请回 App 查看」的提示和「打开 App」按钮。 **前端必须注册并处理以下固定 Scheme:** ```text com.twanmsdyh.app://payment/result?orderId= ``` **处理规则(强制):** 1. App 被 Scheme 唤起后,**读取本地登录 token**,带上 `ddId = orderId 参数` 调用 `POST /pay/line/query`。 2. **绝不能把 Scheme 到达当作支付成功。** Scheme 参数可被伪造,`orderId` 只是用于回查的订单号,不含任何支付结果。 3. `query` 返回 `orderPayStatus === 1` 才算真正成功。 4. 若 `query` 返回仍在处理中(见第 7 节轮询),进入轮询,直到拿到终态或超时提示。 > **Scheme 注册 + 自动唤起 + 兜底按钮,需在 iOS / Android 真机验收**(Sandbox 无法模拟 LINE App 内置浏览器和 App 自动唤起行为)。在真机接入前,中间页本身已能安全显示提示并保留「打开 App」按钮。 --- ## 6. 状态枚举 ### 6.1 `paymentStatus`(LINE 支付流水状态) | 状态 | 含义 | 是否终态 | |---|---|---| | `WAITING_AUTH` | 等待用户在 LINE 授权 | 否 | | `READY_CONFIRM` | 已授权,待后端确认 | 否 | | `CONFIRMING` | 后端确认中 | 否 | | `PAID` | **已支付** | 是 | | `CANCELLED_OR_EXPIRED` | 用户取消或授权过期 | 是 | | `FAILED` | 支付失败 | 是 | | `MANUAL_REVIEW` | 进入人工核对(未知结果超时) | 是(需联系平台处理) | | `REQUESTING` / `REQUEST_UNKNOWN` | 请求中 / 请求结果未知 | 否(后端会恢复) | | `CONFIRM_UNKNOWN` | 确认结果未知 | 否(后端会恢复) | > 前端通常**只需重点识别 `PAID` 和非终态**。复杂的中间态由后端定时任务自动恢复,前端遇到非终态按第 7 节轮询即可。 ### 6.2 `refundStatus`(退款状态) | 状态 | 含义 | |---|---| | `null` | 无退款 | | `CREATED` / `PROCESSING` | 退款处理中 | | `REFUNDED` | **已全额退款** | | `FAILED` | 退款失败 | | `MANUAL_REVIEW` | 退款进入人工核对 | --- ## 7. 接入步骤清单 按顺序完成即可: 1. **下单接口**:确保下单时能把订单 `payType` 设为 `"3"`(LINE Pay)。 2. **支付按钮**:用户点击「LINE Pay 支付」时,调用 `POST /pay/line/create`,拿到 `paymentUrl`。 3. **打开收银台**:用 WebView(或外部浏览器)打开 `paymentUrl`。 - 若返回 `reusedAttempt === true`,正常使用同一个 `paymentUrl`,无需提示。 - 若返回 `status === "PAID"`,直接进成功流程,不必再跳转。 4. **注册 Scheme**:在 App 配置中注册 `com.twanmsdyh.app://payment/result`,回调里取出 `orderId`。 5. **回跳查询**:Scheme 回调触发后,带 token 调 `POST /pay/line/query`。 6. **结果判定**: - `orderPayStatus === 1` → 支付成功。 - `orderPayStatus === 2` → 已退款(取消订单场景)。 - `paymentStatus` 为 `PAID` 但订单处理中 → 走轮询。 7. **轮询**(见下):若结果尚未终态,按固定间隔重试 `query`。 8. **真机验收**:上线前在 iOS / Android 真机完成端到端支付 + Scheme 唤回验证。 ### 轮询策略建议 `/pay/line/confirm` 是**异步确认 + 请款**的,最长可能需要数十秒。Scheme 唤回后第一次 `query` 可能仍是 `WAITING_AUTH` / `CONFIRMING`,需轮询: ```text 首次 query 后,若未终态: - 每隔 3 秒查一次 - 最多查 10 次(约 30 秒) - 仍未终态 → 提示「支付结果确认中,请稍后在订单列表查看」,不要报错 ``` > 即使 App 被杀或 Scheme 丢失也没关系:后端有独立定时任务会主动向 LINE 确认/查询并落账,用户下次打开订单时调 `query` 仍能拿到最终结果。 --- ## 8. 代码示例(参考) ```javascript // 1. 创建支付 async function startLinePay(ddId, token) { const res = await request({ url: '/pay/line/create', method: 'POST', header: { token }, data: { ddId } }); if (res.code !== 200) { // res.msg 已是国际化文案,直接提示 uni.showToast({ title: res.msg, icon: 'none' }); return; } const { paymentUrl, status, reusedAttempt } = res.data; // 已支付,无需跳转 if (status === 'PAID') { goOrderResult(ddId); return; } // 打开 LINE 收银台(reusedAttempt=true 也用同一个 url) // #ifdef APP-PLUS plus.runtime.openURL(paymentUrl); // 或用 webview // #endif } // 2. Scheme 回调里(query 的入口) // App scheme: com.twanmsdyh.app://payment/result?orderId= function onPaymentScheme(orderId) { pollLinePayQuery(orderId, getToken()); } // 3. 轮询查询最终结果 async function pollLinePayQuery(ddId, token) { const MAX = 10, INTERVAL = 3000; for (let i = 0; i < MAX; i++) { const res = await request({ url: '/pay/line/query', method: 'POST', header: { token }, data: { ddId } }); if (res.code !== 200) { uni.showToast({ title: res.msg, icon: 'none' }); return; } const { orderPayStatus, paymentStatus } = res.data; if (orderPayStatus === 1) { // 已支付 goOrderResult(ddId); return; } if (orderPayStatus === 2) { // 已退款 goOrderResult(ddId); return; } // 终态失败(CANCELLED_OR_EXPIRED / FAILED)也结束 if (['CANCELLED_OR_EXPIRED', 'FAILED'].includes(paymentStatus)) { uni.showToast({ title: '支付未完成', icon: 'none' }); return; } // 否则继续等待 await sleep(INTERVAL); } // 超时:不报错,引导用户稍后回订单列表查看 uni.showToast({ title: '支付结果确认中,请稍后查看订单', icon: 'none' }); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } ``` --- ## 9. 注意事项 / FAQ **Q1:能不能用 Scheme 到达判断支付成功?** 不能。Scheme 只是「回到 App」的信号,参数可伪造且不含结果。**一律以 `query` 的 `orderPayStatus === 1` 为准。** **Q2:`transactionId` 为什么是字符串?** 它是 LINE 的 19 位交易号,超过 JS 安全整数范围(`2^53`),用 `Number` 会丢精度。解析 JSON、传参、比较全部保持字符串。 **Q3:用户重复点击「去支付」会重复扣款吗?** 不会。后端保证同订单最多一条活跃尝试,重复点击返回同一 `paymentUrl`(`reusedAttempt=true`)。 **Q4:用户授权完关了页面、或没装 App、或 Scheme 没唤起怎么办?** 不影响结果。后端定时任务会主动向 LINE 确认并落账。用户重新打开订单页调 `query` 即可拿到最终状态。前端轮询超时后**不要报错**,提示「结果确认中」即可。 **Q5:为什么 `create` 报错「订单支付方式不符」?** `create` 要求订单当前 `payType` 必须是 `"3"`。请检查下单接口是否正确设置了 LINE Pay。 **Q6:需要前端处理退款吗?** 不需要。退款是全额退款,由后端在用户/商家**取消订单**时自动发起。前端通过 `query` 的 `orderPayStatus === 2` 或 `refundStatus === "REFUNDED"` 感知即可。 **Q7:金额前端传吗?** 不传。金额、币种(TWD)全部由服务端订单决定,前端只传 `ddId`。 **Q8:`MANUAL_REVIEW` 怎么处理?** 支付/退款结果未知且超过后端追踪期限会进入人工核对。前端展示「支付结果确认中,请联系客服」即可,不要允许用户重新发起(此时仍占用订单支付位)。 --- ## 10. 接口速查 | 接口 | 方法 | 路径 | Header | Body | |---|---|---|---|---| | 创建支付 | POST | `/pay/line/create` | `token` | `{ "ddId": "..." }` | | 查询支付 | POST | `/pay/line/query` | `token` | `{ "ddId": "..."` } | | LINE 回跳(后端) | GET | `/pay/line/confirm` | — | LINE 调用,前端不直接对接 | | App Scheme | — | `com.twanmsdyh.app://payment/result?orderId=` | — | 中间页唤起 | > 域名以实际部署为准(生产公网回跳域名如 `https://foodieapi.waimai-paotui.com`),由后端配置,前端只需用相对路径 + 网关统一前缀。