app-integration.md 14 KB

LINE Pay 前端接入文档(客户 App)

适用对象:客户 App(uni-app)前端开发人员 对应后端:foodie_server 019-line-pay,LinePayController(/pay/line/**) 后端契约权威:本目录 contracts/api.mdspec.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. 整体流程

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 拿最终结果。


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):

    {
    "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 跳转即可,不要当成错误
  • 若上一笔已被 LINE 明确判为取消 / 过期 / 失败,后端才会新建尝试,reusedAttempt = false
  • 若已支付,返回的 statusPAID

业务错误(code !== 200)

msg 已国际化,典型场景:订单不存在 / 非本人订单 / payType 不是 3 / 多门店父单 / 订单不可支付 / 门店未开通或未启用 / 已有处理中尝试需稍后重试。前端直接展示 msg,无需自己映射。


4.2 查询支付 POST /pay/line/query

查询订单的支付/退款最终状态。该接口只读本地数据库,不会同步去调 LINE,响应很快。

请求

  • Header: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 已退款 已全额退款(取消订单后触发)

5. App Scheme 回跳

LINE 收银台授权完成后,LINE 会回跳后端 /pay/line/confirm,后端返回一个中间页。该中间页会尝试唤起 App,并显示「支付结果确认中,请回 App 查看」的提示和「打开 App」按钮。

前端必须注册并处理以下固定 Scheme:

com.twanmsdyh.app://payment/result?orderId=<URL编码后的 ddId>

处理规则(强制):

  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 → 已退款(取消订单场景)。
    • paymentStatusPAID 但订单处理中 → 走轮询。
  7. 轮询(见下):若结果尚未终态,按固定间隔重试 query
  8. 真机验收:上线前在 iOS / Android 真机完成端到端支付 + Scheme 唤回验证。

轮询策略建议

/pay/line/confirm异步确认 + 请款的,最长可能需要数十秒。Scheme 唤回后第一次 query 可能仍是 WAITING_AUTH / CONFIRMING,需轮询:

首次 query 后,若未终态:
  - 每隔 3 秒查一次
  - 最多查 10 次(约 30 秒)
  - 仍未终态 → 提示「支付结果确认中,请稍后在订单列表查看」,不要报错

即使 App 被杀或 Scheme 丢失也没关系:后端有独立定时任务会主动向 LINE 确认/查询并落账,用户下次打开订单时调 query 仍能拿到最终结果。


8. 代码示例(参考)

// 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)); }

9. 注意事项 / FAQ

Q1:能不能用 Scheme 到达判断支付成功? 不能。Scheme 只是「回到 App」的信号,参数可伪造且不含结果。一律以 queryorderPayStatus === 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:需要前端处理退款吗? 不需要。退款是全额退款,由后端在用户/商家取消订单时自动发起。前端通过 queryorderPayStatus === 2refundStatus === "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=<ddId> 中间页唤起

域名以实际部署为准(生产公网回跳域名如 https://foodieapi.waimai-paotui.com),由后端配置,前端只需用相对路径 + 网关统一前缀。