Feature Branch: 019-line-pay
Created: 2026-08-12
Updated: 2026-08-18(增加商家扫描客户 My Code 的 Offline API v4 后端能力)
Status: Approved — 2026-08-12;Offline 增量已确认 — 2026-08-18
Input: 在餐饮订单中新增 LINE Pay 直连支付,使用 payType="3",支持门店级凭证、支付确认、全额退款、主动状态查询、平台管理和 App 回跳。
本规格取代
brainstorm.md中尚未确认或后来已变更的内容。若两者冲突,以本规格为准。
1145、1169、AUTH_READY 和 WAITING_AUTH 返回 AUTH_REQUIRED;超时、响应丢失、重复请求待核实及 REQUEST_UNKNOWN 返回 PROCESSING。paymentProvider 应如何校验? → A: 必须非空并原值保存;TSP、EPI 是已知值,其他非空值也允许;缺失或空值进入 MANUAL_REVIEW。| 议题 | 最终决策 |
|---|---|
| 支付渠道编号 | pos_order.pay_type="3" 表示 LINE Pay 直连;不再表示 ZaloPay |
| 与 OMG 的关系 | LINE Pay 与 OMG 并存,不替换 OMG |
| OMG 数据范围 | pos_store_omg、pos_order_omg_payment、pos_order_omg_refund、OMG 的 ipn_log 写入及既有回调原文全部保持现状 |
| LINE Pay 日志 | 新建公共命名的 payment_gateway_log,首版只写 LINE Pay;不迁移、不复制 OMG 日志 |
| API 与环境 | LINE Pay Online API v4;首版接 Sandbox,生产地址仅预留服务端配置 |
| 凭证粒度 | 每个 pos_store 独立 Channel ID / Channel Secret;订单按 PosOrder.mdId 选择凭证 |
| 凭证验证 | 保存时使用无扣款 Retrieve 探测;探测通过后保存为新版本并自动启用,失败时保留旧版本 |
| Secret 策略 | 按已批准风险,Channel Secret 首版明文保存;平台管理员详情页/查询接口允许回显;不设“日志、异常、响应必须隐藏 Secret/HMAC”的验收限制 |
| 确认与请款 | 使用普通支付默认浏览器回跳并快速返回安全提示页,Confirm 自动请款;不实现授权/请款分离、Capture 或 Void 操作 |
| 退款 | 仅全额退款;用户不直接调用独立退款接口,用户/商家取消订单后自动退款;平台管理员可人工全额退款 |
| 主动查询 | 新建独立定时任务主动 Check/Retrieve/Confirm 并更新支付、退款及订单状态;不使用废弃的 TestTask |
| App 回跳 | 固定 com.twanmsdyh.app://payment/result?orderId=<ddId>;App 打开后必须调用查询接口取得最终状态,不信任 Scheme 参数判断支付结果 |
| 安全提示页 | 由 Spring Controller 直接返回自包含 HTML,不是独立 Vue 页面,也不需要另行部署静态站点 |
| 金额与币种 | 固定 TWD、整数金额;所有金额只取服务端订单事实 |
| 首版订单范围 | 只支持单门店餐饮订单;多门店父单不得发起 LINE Pay |
本期明确接受以下风险,不将其作为阻断条件:
真实 Secret 仍不得写入本规格、源码常量或测试夹具。接口继续受平台权限控制;列表接口不批量返回 Secret,详情接口才按权限返回,以免无意义扩大数据量和暴露面。
foodie_server:LINE Pay v4 客户端、凭证、支付、Confirm、查询、全额退款、状态机、定时恢复、订单联动、平台管理接口和中间提示页。foodie-admin-vue:把现有 LINE Pay“即将上线”占位页替换为门店凭证管理页,并在订单管理中提供 LINE Pay 查询与全额退款操作。updatesql/sql.md,不直接执行。pay_type="3" 订单。任何 LINE Pay 查询、退款或补偿必须同时找到匹配的 LINE Pay 流水,不能只看 payType=3。用户对一笔单门店、未支付、可支付的餐饮订单发起 LINE Pay,系统返回 Sandbox Web 收银台地址。用户完成 LINE 认证后立即看到“支付结果确认中”的安全提示页,页面尝试打开 App;后台异步 Confirm 并由 App 查询最终状态。
Independent Test:从待支付订单发起 Sandbox 支付,完成认证后回到服务端中间页,任务完成 Confirm,订单最终变为已支付。
Acceptance Scenarios:
paymentUrl.web、字符串交易号和查询所需标识。confirmUrl,Then 服务端持久化回跳事实并快速返回中间页,不同步等待最长 40 秒的 Confirm。平台管理员录入门店 Channel ID / Channel Secret。服务端先使用该组未落库凭证查询随机不存在的 LINE orderId;通过后创建不可变凭证版本并自动设为当前启用版本。
Independent Test:正确凭证返回预期探测码并自动启用;错误或网络未知不覆盖当前版本。
Acceptance Scenarios:
1150,或返回结构有效的 0000,系统标记认证探测通过、保存新版本并自动启用。1104、1105、1106 或明确鉴权失败,When 管理员保存,Then 拒绝新版本,当前版本保持不变。9000 或结果未知,When 管理员保存,Then 返回“验证结果未知”,不保存、不切换、不停用旧凭证。credential_id;若旧版本明确鉴权失败,只允许同门店、同环境、同 Channel ID 的当前版本先经严格 Retrieve 证明交易归属,再用于本轮恢复,绝不尝试不同 Channel 的凭证。LINE Pay 没有专用的凭证校验接口。上述
1150/0000仅表示该签名与 Channel 组合被网关接受,不证明门店归属、TWD/Online 权限或真实扣款能力。自动启用是本项目已批准的业务策略;真实支付能力仍须 Sandbox 端到端验收。
即使用户关闭页面、回跳丢失、Confirm/Refund 响应丢失或服务重启,独立任务仍能主动查询 LINE Pay 并恢复最终资金事实。
Independent Test:模拟丢回跳、Confirm 超时和 Refund 超时,任务通过 Check/Retrieve 恢复且不重复扣款/退款。
Acceptance Scenarios:
0000,When 任务处理,Then 保持等待,不标记支付成功。0110 且订单仍可支付,When 任务取得行级执行权,Then 自动调用一次 Confirm。0110 但订单已取消,When 任务处理,Then 不 Confirm、不主动扣款,继续查询直至网关给出终态或进入人工核对。0121,When Retrieve 未发现已支付事实,Then 标记取消或过期并释放该订单的 LINE 活跃尝试。0122,When Retrieve 未发现已支付事实,Then 标记失败并允许新的支付尝试。0123,When 任务处理,Then 必须再 Retrieve 核实 Capture 后才标记已支付。用户或有权商家通过现有真实取消入口取消订单。若 LINE Pay 已支付,系统创建唯一全额退款意图并调用 Refund;若支付成功迟到,则先记录真实支付,再自动创建全额退款意图。
Independent Test:分别重放“支付先成功再取消”和“取消先成功、支付结果后到”两种时序,最终只发生一次全额退款。
Acceptance Scenarios:
payStatus 才更新为已退款,并执行既有退款后的订单/积分联动。平台管理员可查看门店开通状态、维护凭证、启停新支付、查看 LINE Pay 状态,并对未知交易人工执行“查询核实”或对可退款交易执行“全额退款”。
Independent Test:以不同权限账号访问列表、Secret 详情、保存、启停、订单查询和退款,权限与状态门禁正确。
0110。payType=3 订单与新 LINE Pay 订单共存。state=1,以及多门店父单包含不同 mdId。ruoyi-system只承载实体、Mapper XML、Service 和数据库条件更新,不依赖 LINE Pay HTTP 客户端,不导入 com.ruoyi.app.*:
pos_store_line_pay:门店凭证的不可变版本。pos_order_line_payment:每次真正发起的新 LINE Request 各保存一条支付尝试;历史尝试永不覆盖。pos_order_line_refund:每笔已付款交易最多一条全额退款状态。payment_gateway_log:LINE Pay 原始交互与结果码的追加日志。ruoyi-adminLinePayClient:v4 HTTP、精确签名、超时和 DTO 解析。LinePayService:凭证探测、创建、回跳登记、Confirm、Retrieve、Check、退款及状态机。LinePayController:App 创建/查询接口和匿名回跳页。PosStoreLinePayController:平台门店凭证管理。LinePayReconcileTask:独立主动查询与恢复任务;只调用 Service,不调用 Controller。foodie-admin-vuesrc/views/mendian/storePayment/index.vue 保留 OMG Tab,把 LINE Pay 占位内容替换为独立 LinePayTab。src/api/chanting/storeLinePay.js。src/api/language/language.zh_CN.jssrc/api/language/language.zh_TW.jssrc/api/language/language.en_US.jssrc/api/language/language.vi.jsstoreLinePay 嵌套对象中,使用有意义的英文驼峰名称。pos_store_line_pay每次验证通过生成新行,旧行不覆盖、不删除,以便历史交易继续使用原凭证。
关键字段:
| 字段 | 约束与含义 |
|---|---|
id |
主键,同时作为支付流水的 credential_id |
store_id |
pos_store.id |
credential_version |
门店内递增版本;唯一 (store_id, credential_version) |
channel_id |
LINE Channel ID |
channel_secret |
按已批准策略明文保存 |
verify_status |
AUTH_PROBE_VERIFIED |
verify_return_code/message |
最近保存探测结果摘要 |
verified_time |
探测通过时间 |
current_store_id |
当前版本时等于 store_id,历史版本为 NULL;唯一索引保证每店只有一个当前版本 |
is_enabled |
当前版本是否允许发起新支付;禁用不影响旧交易查询/退款 |
| 并发与审计字段 | version/create_by/create_time/update_by/update_time |
验证通过后的版本切换在一个数据库事务内完成:旧版本 current_store_id=NULL,is_enabled=0,新版本 current_store_id=store_id,is_enabled=1。并发管理员切换使用版本 CAS;失败者重新读取当前版本。
pos_order_line_payment只保存支付状态事实和恢复调度字段,不保存每次网关 returnCode 或整段原始响应。
该表与订单是 1:N:同一 dd_id 可以有多条历史支付尝试,但任意时刻最多只有一条当前活跃尝试。用户对仍在等待或处理中的尝试重复点击支付时复用原记录;只有原尝试已被 LINE 明确判定取消、过期或失败后,重新支付才插入新记录。旧记录及其 line_order_id、transaction_id、credential_id 永不被新尝试覆盖。
关键字段:
| 字段 | 约束与含义 |
|---|---|
id |
主键 |
dd_id |
pos_order.dd_id,字符串业务订单号;普通索引、允许重复,不设唯一约束 |
line_order_id |
Request 前生成并提交,VARCHAR(100)、ASCII/BINARY 比较,非空永久唯一,实际值不超过 100 字符 |
transaction_id |
LINE 19 位交易号,VARCHAR/Java String/JS String,可空唯一,禁止 Long/Number |
credential_id |
不可变引用 pos_store_line_pay.id |
store_id |
发起时门店快照 |
amount/currency |
TWD 整数金额和 TWD |
payment_url |
Sandbox 的 paymentUrl.web |
payment_provider |
保存 LINE 原始返回值;NULL 在业务展示时可解释为 TSP,但数据库不伪造原值 |
status |
下述支付状态 |
active_dd_id |
阻断新尝试时等于 dd_id,明确取消/过期/失败后为 NULL;唯一索引保证每订单仅一条阻断性 LINE 流水 |
version |
行级 CAS 版本 |
| 恢复字段 | next_reconcile_at/reconcile_deadline/reconcile_count/lease_owner/lease_until/status_changed_at |
| 事实时间 | auth_completed_time/pay_time/create_time/update_time |
支付状态:
REQUESTING
REQUEST_UNKNOWN
WAITING_AUTH
READY_CONFIRM
AUTH_DONE_ORDER_CANCELLED
CONFIRMING
CONFIRM_UNKNOWN
PAID
CANCELLED_OR_EXPIRED
FAILED
MANUAL_REVIEW
PAID、所有 UNKNOWN 和 MANUAL_REVIEW 继续占用 active_dd_id;只有网关明确 0121、0122 或明确无副作用失败才能释放。支付一旦 PAID 永不改写为退款状态。
支付行只允许按照状态机 CAS 推进状态和补充本次尝试的网关结果;不得把另一支付尝试的交易号、凭证或支付地址写入旧行,也不得通过覆盖旧行实现“重新支付”。
禁止使用
UNIQUE(dd_id,is_active)并把历史行写成0,因为这会导致同一订单只能保存一条历史失效记录。DDL 使用可空active_dd_id唯一索引或等价生成列。
pos_order_line_refund退款独立保存,不污染支付状态。
关键字段:id、payment_id(唯一,保证全额退款只创建一次)、refund_transaction_id(字符串、可空唯一)、amount、status、version、恢复调度/租约字段、refund_time/create_time/update_time。
退款状态:
CREATED
PROCESSING
UNKNOWN
RETRY_WAIT
REFUNDED
FAILED
MANUAL_REVIEW
UNKNOWN 不得直接再次 Refund。仅官方明确可重试的 1900、1902、1999 可进入受控 RETRY_WAIT;其他明确失败进入 FAILED 或人工核对。
payment_gateway_log该表首版只写 provider=LINE_PAY。它记录“发生过什么”,不作为支付/退款状态机事实;日志写入失败不得回滚已经确认的资金事实。
关键字段:
id, provider, correlation_id,
store_id, credential_id, payment_id, refund_id, dd_id,
gateway_order_id, transaction_id,
action, direction, source,
http_status, return_code, return_message,
success, duration_ms, payload, create_time
action 包含 CREDENTIAL_VERIFY/REQUEST/CHECK/CONFIRM/RETRIEVE/REFUND/REDIRECT_CONFIRM/REDIRECT_CANCEL。direction 区分请求、响应和本地事件;同次 HTTP 请求/响应共用 correlation_id。0000/0110/0121/0122/0123/1150 等原始码记录在本表;支付/退款表只保存规范状态和调度字段。(payment_id,create_time)、(refund_id,create_time)、(dd_id,create_time)、(store_id,create_time)、(gateway_order_id,create_time)、(transaction_id,create_time)、correlation_id。POST /v4/payments/request
GET /v4/payments/requests/{transactionId}/check
POST /v4/payments/{transactionId}/confirm
GET /v4/payments
POST /v4/payments/{transactionId}/refund
https://sandbox-api-pay.line.me,不可由请求参数控制。payType="3" 只属于本项目,绝不发送给 LINE;普通支付省略 LINE 的 options.payment.payType。options.payment.capture=true。Request 发送官方公开契约中的回跳字段;普通支付省略未被当前 v4 公共页面明确保证的 payType、confirmUrlType 和 appPackageName,使用默认浏览器回跳:
{
"redirectUrls": {
"confirmUrl": "https://foodieapi.waimai-paotui.com/pay/line/confirm",
"cancelUrl": "https://foodieapi.waimai-paotui.com/pay/line/cancel"
}
}
Request 的 amount 必须等于 packages 金额之和,每个 package 金额必须等于 products 的 price × quantity 之和。首版使用一个服务端生成的 package/product,金额等于订单应收整数金额。
Refund 为全额退款时省略 refundAmount;Refund 请求没有 currency 字段。
请求头:
Content-Type: application/json
X-LINE-ChannelId
X-LINE-Authorization
X-LINE-Authorization-Nonce
GET: channelSecret + apiPath + exactQueryString + nonce
POST: channelSecret + apiPath + exactRequestBody + nonce
使用 Channel Secret 作为 HMAC-SHA256 key,再 Base64。POST JSON 只序列化一次并将同一字节串用于签名和发送;GET 的参数顺序、编码和值必须与最终 URL 完全一致;同一次请求的签名和请求头使用同一 UUID v4 nonce。
建议读取超时下限:Request 10 秒、Check/Retrieve/Refund 20 秒、Confirm 40 秒。HTTP 200 不表示业务成功,必须解释 returnCode。
returnCode=0000 只表示查询成功,不等于已付款。必须在 info[] 中找到唯一匹配本地 line_order_id + transaction_id + currency 的记录,并确认:
transactionType=PAYMENTpayInfo[].amount 合计与本地金额一致;Online v4 当前公开 Retrieve 契约未保证 payStatus 字段,因此不依赖该字段credential_id 相符AUTHORIZATION 不是已结算。空数组、多条歧义或字段不匹配全部进入未知/人工核对。退款事实同时检查 refundList,不能只根据原 PAYMENT 仍为 CAPTURE 判断“未退款”。
ddId,不混用 pos_order.id、父单号和 LINE orderId。pos_order;多门店父单直接返回不支持 LINE Pay。state=1 仍可支付;金额为正整数 TWD。mdId 取得当前启用凭证版本。payType=3;OMG create 必须要求订单当前 payType=7。create 不允许临时切换支付渠道。pay:create:<ddId> Redisson 分布式锁内重新读取订单,并以 pay_status=0、非终态、pay_type=目标渠道 作为数据库条件门禁。锁获取失败直接返回处理中,不降级为无锁创建。现有 OMG create 只做该最小校验/锁调整,不改 OMG 表或日志。REQUESTING + line_order_id + credential_id + active_dd_id,再调用外部 Request。transaction_id/payment_url 并进入 WAITING_AUTH。超时、响应丢失或落库失败进入 REQUEST_UNKNOWN,由 Retrieve 按 line_order_id 恢复。重复创建规则:
WAITING_AUTH:查询现有尝试后返回同一有效 paymentUrl。READY_CONFIRM/CONFIRMING/UNKNOWN:返回“处理中”,不创建新尝试。PAID:直接返回已支付。CANCELLED_OR_EXPIRED/FAILED:旧行保留并释放 active_dd_id;重新支付插入新行并生成新的永久唯一 line_order_id,不更新或覆盖旧行。LINE 自动在 confirm URL 追加 orderId(即本地 line_order_id)和 transactionId。cancel URL 的两项参数可能缺失。两个 GET 都无签名、可伪造,不是最终资金事实。
/confirm:匹配本地流水并 CAS 登记 READY_CONFIRM,将 next_reconcile_at 提前,然后快速返回 HTML;不在浏览器请求内同步调用 Confirm。/cancel:只记本地事件并触发尽快 Check,不直接标记取消,不取消外卖订单。页面从已匹配的本地流水取得 ddId,构造固定 Scheme:
com.twanmsdyh.app://payment/result?orderId=<URL-encoded ddId>
页面加载后尝试一次 Scheme,并提供手动“打开 App”按钮;无法唤起时页面继续保留安全提示。
固定 Scheme 不接受请求参数指定跳转目标,避免开放重定向。
HTML/JS/URL 参数正确转义,内联脚本使用每响应 CSP nonce;响应使用 text/html;charset=UTF-8、Cache-Control: no-store、Referrer-Policy: no-referrer、X-Content-Type-Options: nosniff 和限制性 CSP/frame-ancestors 'none'。
执行者先以 CAS 和行租约把状态变为 CONFIRMING,再调用外部 Confirm:
active_dd_id、订单状态、门店、credential_id、line_order_id、transaction_id、金额和币种。AUTH_DONE_ORDER_CANCELLED 并等待网关过期终态。0000 后验证响应并在本地事实事务中把支付置为 PAID。1198、1199、9000 或本地落库未知时置为 CONFIRM_UNKNOWN,只能 Retrieve 恢复。pos_order.pay_type=3,pay_status=1,并只触发一次现有履约/推送副作用。堂食初始 state=1 也允许核销。state=4,仍先记录 PAID 和订单已付款事实,并在同一事务插入唯一退款意图;不触发履约。商家接单必须阻止真实 LINE 未付款订单进入履约。真实 LINE 订单禁止走旧 /setorderuzt,只能使用当前用户、商家、骑手专用订单操作入口;历史 payType=3 且没有 LINE 流水的订单不按新 LINE 订单处理。
恢复时优先 Retrieve;未发现已支付事实才使用 Check:
| Check code | 本地动作 |
|---|---|
0000 |
保持等待认证 |
0110 |
标记可 Confirm;订单仍可支付才自动 Confirm |
0121 |
Retrieve 未发现付款后标记 CANCELLED_OR_EXPIRED |
0122 |
Retrieve 未发现付款后标记 FAILED |
0123 |
调 Retrieve;只有证实 PAYMENT/CAPTURE 才标记 PAID |
任何 Check 结果都不能覆盖已由 Retrieve/Confirm 确认的 PAID。
userType/shId/mdId 规则验证订单归属。insert-if-absent(payment_id) 创建 CREATED 退款;若尚未支付,迟到支付落账事务负责补建。PROCESSING,先 Retrieve 排除已经退款,再调用一次省略 refundAmount 的全额 Refund。0000 且返回 refundTransactionId 后置 REFUNDED,再更新订单 payStatus=2 和既有退款后状态/积分。UNKNOWN;通过原 PAYMENT 的 refundList 或 refundTransactionId Retrieve 恢复,禁止直接重发 Refund。FAILED 退款意图的订单,确保“完成订单”和“开始退款”最多只有一方成功占位,已经退款的订单也不能再完成。PROCESSING 恢复为 UNKNOWN 时必须初始化未知截止时间;截止前每次无结论 Retrieve 必须以版本 CAS 后移 next_reconcile_at,截止时的最后一次 Retrieve 不再重排,而是以当前版本可靠落入 MANUAL_REVIEW,避免旧未知记录长期占据批次队首或永久占用退款意图。REFUNDED 是不可降级终态;通用人工核对 CAS 永远不得覆盖它。只有 Retrieve 已严格证实存在部分或歧义退款证据时,专用证据 CAS 才可将 FAILED 等非退款终态升级为 MANUAL_REVIEW,阻止订单在已出现退款事实时继续履约;Retrieve 严格证实全额退款时,另一专用证据 CAS 可将 FAILED 升级为 REFUNDED,但不得覆盖既有 MANUAL_REVIEW/REFUNDED。查询不依赖覆盖旧记录,而是按查询目的使用稳定键:
ddId 查询:先读取订单资金状态。订单 payStatus=1 时选择唯一一条尚未证实全额退款的 PAID 支付行;payStatus=2 时返回已证实全额退款的支付/退款事实;未支付时查询唯一的 active_dd_id=ddId;没有活跃行时返回 create_time,id 倒序的最近一条终态尝试。若出现多条尚未全额退款的 PAID,则不自动任选,返回人工核对状态。line_order_id 精确定位;同时带有 transactionId 时还必须与同一行匹配。历史回跳只处理历史行,不得更新当前活跃行。line_order_id/transaction_id/credential_id 查询 LINE。PAID payment_id 创建或读取唯一退款行,不按“最近一条支付”猜测。dd_id 查询全部尝试并按 create_time,id 倒序展示。若已释放的历史尝试后来被 Retrieve 证实实际支付成功,系统必须在该历史行记录真实 PAID。订单尚未支付时,以该事实核销订单并阻止其他尝试 Confirm;订单已由另一尝试支付时,把迟到的重复付款转入自动全额退款或人工核对,不能覆盖任一支付行。
LinePayReconcileTask 位于 ruoyi-admin/src/main/java/com/ruoyi/app/task,默认每 60 秒启动一轮:
next_reconcile_at <= now 的非终态支付/退款,默认批量上限 20,并为支付、取消补偿和退款保留独立处理机会;支付行领取租约时同步后移 next_reconcile_at,避免慢响应记录反复占据队首,一笔失败不影响其他记录。lease_owner/lease_until/version CAS claim。即使全局锁失效或人工操作并发,也只有一个副作用执行者。(status,next_reconcile_at,id);按状态退避并递增 reconcile_count。MANUAL_REVIEW 并保留 active_dd_id。MANUAL_REVIEW,绝不释放活跃键或自动重试副作用。以上周期、批量和期限使用服务端配置,可调但不得由客户端请求控制。
POST /pay/line/create
POST /pay/line/query
GET /pay/line/confirm
GET /pay/line/cancel
create/query:@Anonymous + @Auth + @RequestHeader String token + @RequestBody 明确 DTO;请求字段为 ddId。confirm:@Anonymous,显式必填 @RequestParam String orderId、transactionId。cancel:@Anonymous,显式 @RequestParam(required=false) 接收可能缺失的 orderId、transactionId。创建响应包含:ddId、paymentId、lineOrderId、字符串 transactionId、paymentUrl、规范支付状态、reusedAttempt。查询响应按 7.6 的规则包含所选 paymentId、订单支付状态、LINE 规范支付状态、退款状态和更新时间;不以 Scheme 或回跳参数作为结果。
门店管理至少提供列表、详情、保存并验证凭证、启停当前版本。订单管理至少提供查询核实和全额退款。
权限拆分:
chanting:storePayment:list
chanting:storeLinePay:list
chanting:storeLinePay:query
chanting:storeLinePay:saveCredentials
chanting:storeLinePay:toggleEnable
system:order:linePaymentReconcile
system:order:lineRefund
现有支付管理菜单入口从仅 OMG 权限调整为公共 storePayment:list,避免只有 LINE 权限时进不了页面。所有 SQL 只写入 updatesql/sql.md。
payType="3" 定义为 LINE Pay 直连,并更新 PosOrder、OrderPositionInfo 等仍被有效链路引用的注释;废弃 ZaloPay 实体、Service、Controller、配置和历史字段语义不做增量修改,LINE Pay 使用独立配置前缀和独立流水识别。mdId 使用当前已启用且探测通过的门店凭证版本,金额固定取服务端订单的整数 TWD。payType=3 发送为 LINE API 的 options.payment.payType。line_order_id 和支付意图;同一订单允许多条历史支付尝试但最多一条活跃尝试。重复点击 MUST 复用活跃行,只有旧尝试被明确终止后才插入新行,任何重新支付 MUST NOT 覆盖旧行。0000/0110/0121/0122/0123 映射推进状态。PAYMENT/CAPTURE 后记录已支付;Check、HTTP 200 和回跳到达均不是最终资金事实。refundAmount;退款未知时 MUST Retrieve,禁止盲目重复 Refund,只有证实退款后才更新订单已退款状态。credential_id;新凭证探测失败或未知不得覆盖当前版本,旧交易继续使用原版本。credential_id。payment_gateway_log,原始结果码不塞入支付/退款业务表;该日志首版 MUST NOT 接管或迁移 OMG 日志。$t() 并同步简中、繁中、英文、越南文四份实际语言文件,key 使用 storeLinePay 下有意义的英文驼峰名称。ipn_log 和既有原始回调结构不变;只允许为防跨渠道并发,对 OMG create 增加相同订单级锁和渠道一致性门禁。line_order_id/transaction_id、任务按 payment_id、退款按已付 payment_id 精确处理,MUST NOT 仅凭历史 payType=3 或“最近一条”猜测资金记录。updatesql/sql.md,不得由实现过程直接执行。ddId 调查询接口取得最终支付/退款状态,不得信任 URL 参数得出支付结果。payType=3 只有在存在 pos_order_line_payment 且标识匹配时才允许 LINE 查询或退款。transactionId 在 Java/JSON/前端全程保持字符串。refundAmount。state=0 和堂食 state=1 均可正确核销;未付 LINE 订单不得接单。payType=3 不触发 LINE 查询或退款。shId/mdId 范围订单;平台各 LINE 权限独立生效。$t()。paymentUrl.web,完成 Web 收银台认证。0000/0110/0121/0122/0123 映射和 Retrieve 二次核实。Sandbox 官方不支持 App payment URL,也不能模拟 EPI;以下必须在生产启用前单独完成:
/payment/result,打开后带 token 查询服务端最终状态。paymentProvider 的 TSP/EPI 兼容。/system/orderShOprate/createOrder 产生的 MERCHANT 单门店订单;用户端、历史、跨商家和多门店订单在调用 LINE 前全部拒绝。lineOrderId Check;重复请求、任务并发和新 My Code 均不会触发第二次 Pay。oneTimeKey 不出现在数据库、响应、异常、普通日志和网关审计中;金额不一致不履约、只产生一个全额退款意图,且原订单永久关闭支付。pos_order.dd_id 是 App 与支付接口使用的业务订单号;LINE 的 orderId 始终指独立 line_order_id,两者不可混用。https://foodieapi.waimai-paotui.com 已具备有效 HTTPS/TLS。本节扩展现有 Online API v4 功能。详细设计、官方文档核对结论、状态映射和测试边界见 offline-merchant-scan-design.md。本节与前文冲突时,仅在 Offline 商家扫码支付范围内以本节为准;现有 Online 流程保持不变。
foodie_server 后端;商家端为 uni-app,本期不修改 App、foodie-store 或 foodie-admin-vue。payType="3" MUST 继续表示 LINE Pay;Online 与 Offline MUST 通过 pos_order_line_payment.payment_mode 的 ONLINE/OFFLINE 区分并存。/system/orderShOprate/createOrder 创建且持久化为 order_source="MERCHANT" 的新订单可以发起 Offline 扫码支付;历史订单及用户端订单默认 USER,不得推断或回填来源。items 仅包含一个门店包;普通/夜市商家 MUST 同时校验 shId=userId 与目标 PosStore.userId=userId,禁止本人 shId 组合其他商家的 mdId。扫码支付时 MUST 再次校验来源、归属、单门店、订单状态、payType、金额和门店凭证。ddId 和台湾 18 位 oneTimeKey;金额、币种、商品、门店、凭证及 LINE orderId MUST 取自服务端事实。POST /v4/payments/oneTimeKeys/pay 并使用台湾默认自动请款;本期 MUST NOT 实现分开请款、Capture、Void、redirect URL 或设备请求头。lineOrderId 调用 GET /v4/payments/orders/{orderId}/check,MUST NOT 重复提交付款请求或重用 oneTimeKey。只有明确无资金副作用的 Pay 拒绝码或 Check CANCEL/FAIL 才能释放活跃键。1145、1169、AUTH_READY 和本地 WAITING_AUTH 映射为 AUTH_REQUIRED;将付款超时、响应丢失、重复请求待核实和本地 REQUEST_UNKNOWN 映射为 PROCESSING;将 COMPLETE/CANCEL/FAIL 分别映射为 PAID/CANCELLED/FAILED。AUTH_REQUIRED 和 PROCESSING 均 MUST 阻止再次扫码;1169 表示客户仍需选择付款方式并完成认证,不得把扫码动作直接视为支付成功。returnCode="0000" 或状态查询 COMPLETE 且订单号、交易号、payInfo 金额合计和 paymentProvider 通过核对后,系统才能写入支付事实;paymentProvider MUST 非空并原值保存,TSP、EPI 是已知值但其他非空值同样允许,缺失或空值 MUST 进入 MANUAL_REVIEW 且不得触发正常履约。币种固定使用本地请求事实 TWD,不得要求付款/状态响应返回未公开保证的币种字段。payInfo 合计与订单金额不一致,系统 MUST 持久化真实交易和 LINE 实际扣款额,并在同一事务内以实际扣款额创建唯一全额退款意图;阻止正常履约,未知退款结果继续只读核实。无论退款最终成功、失败或进入人工核对,原订单都永久关闭支付,不允许再次扫码,商家必须创建新订单。oneTimeKey。所有状态 CAS 失败后 MUST 重读持久化状态,不得用过期对象继续副作用。POST /v4/payments/orders/{lineOrderId}/refund;Online 退款继续使用现有按 transactionId 的端点,分派依据只能是持久化的 payment_mode。oneTimeKey MUST NOT 落库、写入普通日志、异常、响应或网关审计原文;Offline REQUEST 审计只能保存移除该字段或替换为 <redacted> 的摘要。updatesql/sql.md,不得由实现或测试直接执行。POST /system/orderShOprate/linePay/offline/pay
Header: token
Body: { "ddId": "...", "oneTimeKey": "..." }
GET /system/orderShOprate/linePay/offline/status?ddId=...
Header: token
接口返回的规范状态为 AUTH_REQUIRED/PROCESSING/PAID/CANCELLED/FAILED/MANUAL_REVIEW。Controller 必须使用明确 DTO、@RequestBody、@RequestParam 和 @RequestHeader String token,不得使用 Map 入参或 Bean Validation。
MERCHANT 订单,When 提交有效 My Code,Then 后端生成唯一 lineOrderId、创建 OFFLINE 尝试并按服务端金额请求 LINE Pay。1145、1169、AUTH_READY 或本地状态为 WAITING_AUTH,When 商家查询,Then 返回 AUTH_REQUIRED、首次使用 30 分钟认证截止且后续不顺延,并阻止第二次扫码;付款超时、响应丢失、未识别返回码、重复请求待核实或本地状态为 REQUEST_UNKNOWN 时返回 PROCESSING 并同样阻止第二次扫码。COMPLETE、订单号和交易号匹配、payInfo 金额合计一致且 paymentProvider 非空,When 状态落库,Then 原值保存 paymentProvider,并且订单、结算和通知副作用至多执行一次;paymentProvider 缺失或为空时进入 MANUAL_REVIEW 且不触发正常履约。lineOrderId 查询,不再次请求付款。CANCEL/FAIL,When 状态落库,Then 释放活跃尝试,客户必须生成新的 My Code 才能再次支付。lineOrderId 调用 Offline Refund;Online 回归仍使用 transactionId。oneTimeKey。