适用对象:客户 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 节,不在本文档范围。
LINE Pay 直连是项目餐饮订单的线上支付渠道之一,订单字段 pos_order.pay_type = "3" 即表示该笔订单使用 LINE Pay。
本期约束(前端必须知道):
/pay/line/query 的返回为准,绝不能信任 Scheme 参数或 LINE 收银台页面判断是否付款成功。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<br/>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 拿最终结果。
create 会返回业务错误。payType = "3":下单时必须把支付方式指定为 LINE Pay。后端 create 会校验当前 payType,不允许 create 时临时切换渠道。state=1)也可支付。create / query 都需要请求头携带登录 token。所有接口走项目统一 AjaxResult 外壳:{ code, msg, data }。code === 200 为成功,其余为业务错误,msg 为国际化错误文案(直接展示即可)。
重要:
transactionId是 19 位长数字,必须按字符串处理。 用 JSNumber/ 算术运算会导致精度丢失。JSON 解析、存储、比较全程保持字符串。
POST /pay/line/create向 LINE 发起支付请求,返回 LINE 收银台地址。
请求
token: <登录 token>(必填)Body(JSON):
{
"ddId": "202608120001"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
ddId |
string | 是 | 业务订单号 pos_order.dd_id |
成功响应 data
{
"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 跳转即可,不要当成错误。reusedAttempt = false。status 为 PAID。业务错误(code !== 200)
msg 已国际化,典型场景:订单不存在 / 非本人订单 / payType 不是 3 / 多门店父单 / 订单不可支付 / 门店未开通或未启用 / 已有处理中尝试需稍后重试。前端直接展示 msg,无需自己映射。
POST /pay/line/query查询订单的支付/退款最终状态。该接口只读本地数据库,不会同步去调 LINE,响应很快。
请求
token: <登录 token>(必填)Body(JSON):
{
"ddId": "202608120001"
}
成功响应 data
{
"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 |
已退款 | 已全额退款(取消订单后触发) |
LINE 收银台授权完成后,LINE 会回跳后端 /pay/line/confirm,后端返回一个中间页。该中间页会尝试唤起 App,并显示「支付结果确认中,请回 App 查看」的提示和「打开 App」按钮。
前端必须注册并处理以下固定 Scheme:
com.twanmsdyh.app://payment/result?orderId=<URL编码后的 ddId>
处理规则(强制):
ddId = orderId 参数 调用 POST /pay/line/query。orderId 只是用于回查的订单号,不含任何支付结果。query 返回 orderPayStatus === 1 才算真正成功。query 返回仍在处理中(见第 7 节轮询),进入轮询,直到拿到终态或超时提示。Scheme 注册 + 自动唤起 + 兜底按钮,需在 iOS / Android 真机验收(Sandbox 无法模拟 LINE App 内置浏览器和 App 自动唤起行为)。在真机接入前,中间页本身已能安全显示提示并保留「打开 App」按钮。
paymentStatus(LINE 支付流水状态)| 状态 | 含义 | 是否终态 |
|---|---|---|
WAITING_AUTH |
等待用户在 LINE 授权 | 否 |
READY_CONFIRM |
已授权,待后端确认 | 否 |
CONFIRMING |
后端确认中 | 否 |
PAID |
已支付 | 是 |
CANCELLED_OR_EXPIRED |
用户取消或授权过期 | 是 |
FAILED |
支付失败 | 是 |
MANUAL_REVIEW |
进入人工核对(未知结果超时) | 是(需联系平台处理) |
REQUESTING / REQUEST_UNKNOWN |
请求中 / 请求结果未知 | 否(后端会恢复) |
CONFIRM_UNKNOWN |
确认结果未知 | 否(后端会恢复) |
前端通常只需重点识别
PAID和非终态。复杂的中间态由后端定时任务自动恢复,前端遇到非终态按第 7 节轮询即可。
refundStatus(退款状态)| 状态 | 含义 |
|---|---|
null |
无退款 |
CREATED / PROCESSING |
退款处理中 |
REFUNDED |
已全额退款 |
FAILED |
退款失败 |
MANUAL_REVIEW |
退款进入人工核对 |
按顺序完成即可:
payType 设为 "3"(LINE Pay)。POST /pay/line/create,拿到 paymentUrl。paymentUrl。
reusedAttempt === true,正常使用同一个 paymentUrl,无需提示。status === "PAID",直接进成功流程,不必再跳转。com.twanmsdyh.app://payment/result,回调里取出 orderId。POST /pay/line/query。orderPayStatus === 1 → 支付成功。orderPayStatus === 2 → 已退款(取消订单场景)。paymentStatus 为 PAID 但订单处理中 → 走轮询。query。/pay/line/confirm 是异步确认 + 请款的,最长可能需要数十秒。Scheme 唤回后第一次 query 可能仍是 WAITING_AUTH / CONFIRMING,需轮询:
首次 query 后,若未终态:
- 每隔 3 秒查一次
- 最多查 10 次(约 30 秒)
- 仍未终态 → 提示「支付结果确认中,请稍后在订单列表查看」,不要报错
即使 App 被杀或 Scheme 丢失也没关系:后端有独立定时任务会主动向 LINE 确认/查询并落账,用户下次打开订单时调
query仍能拿到最终结果。
// 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=<ddId>
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)); }
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 怎么处理?
支付/退款结果未知且超过后端追踪期限会进入人工核对。前端展示「支付结果确认中,请联系客服」即可,不要允许用户重新发起(此时仍占用订单支付位)。
| 接口 | 方法 | 路径 | 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=<ddId> |
— | 中间页唤起 |
域名以实际部署为准(生产公网回跳域名如
https://foodieapi.waimai-paotui.com),由后端配置,前端只需用相对路径 + 网关统一前缀。