三种故障场景的超时、对账与手动回退方案 — 更新于 2026-03-09
POS Tip Adjust 是 CodePay Intent Mode 的后付小费流程:Web 前端发起请求 → 后端通过 WebSocket 推送到 POS 设备 → POS 设备调用 CodePay SDK 修改授权金额 → 回调后端确认。
但 POS 设备是一个不可靠的外部系统,存在三种故障场景:
| 场景 | 触发条件 | tip_adjust_status | 处理方式 |
|---|---|---|---|
| POS 报失败 | POS 主动回报 success=false | failed |
前端显示 Retry POS / Enter Manually |
| POS 超时无回调 | 60s 内无任何 POS 响应 | timeout |
先查 CodePay API 自动对账,失败则显示手动录入 |
| POS 成功但回调丢失 | 60s 超时后 CodePay 查到成功 | completed (自动) |
自动对账,无需人工干预 |
sequenceDiagram
participant Web as Web 前端
participant BE as 后端
participant POS as POS 设备
participant CP as CodePay
Web->>BE: requestTipAdjust(txId, $10)
BE->>BE: status = 'pending'
BE->>BE: scheduleTimeout(60s)
BE->>POS: WebSocket: TIP_ADJUST_REQUEST
POS->>CP: Adjust tip via SDK
CP-->>POS: Success
POS->>BE: POST /pos-tip-adjust-confirm (success=true)
BE->>BE: cancelTimeout()
BE->>BE: confirmTipAdjust() / status = 'completed'
BE-->>Web: WebSocket: TIP_ADJUST_COMPLETED
Web->>Web: 刷新交易列表
POS 终端主动回报操作失败(刷卡被拒、通信错误等)。这是最简单的场景 — POS 给了明确的失败信号。
sequenceDiagram
participant Web as Web 前端
participant BE as 后端
participant POS as POS 设备
BE->>POS: TIP_ADJUST_REQUEST
POS-->>BE: confirm(success=false, error="Card declined")
BE->>BE: cancelTimeout()
BE->>BE: status = 'failed'
BE-->>Web: TIP_ADJUST_FAILED
Web->>Web: 显示 "POS Failed" + 两个按钮
用户看到:
submitTipEntry API 以 pos_fallback 方法直接写入数据库POS 设备没有任何响应(网络断开、设备死机、App crash 等)。后端 60 秒超时触发后,先尝试 CodePay Query Order API 自动对账。
sequenceDiagram
participant Web as Web 前端
participant BE as 后端
participant POS as POS 设备
participant CP as CodePay API
BE->>POS: TIP_ADJUST_REQUEST
Note over POS: 无响应...
Note over BE: 60 秒超时
BE->>BE: handleTipAdjustTimeout()
BE->>CP: POST /api/entry/orderquery
CP-->>BE: found: false
BE->>BE: status = 'timeout'
BE-->>Web: TIP_ADJUST_TIMEOUT
Web->>Web: 显示 "POS Timed Out" + 两个按钮
用户看到:
POS 设备实际完成了小费调整,但回调消息因网络抖动丢失。这是最重要的场景 — 自动对账,用户完全无感知。
sequenceDiagram
participant Web as Web 前端
participant BE as 后端
participant POS as POS 设备
participant CP as CodePay API
BE->>POS: TIP_ADJUST_REQUEST
POS->>CP: Adjust tip ($10)
CP-->>POS: Success
Note over POS,BE: 回调丢失 X
Note over BE: 60 秒超时
BE->>BE: handleTipAdjustTimeout()
BE->>CP: POST /api/entry/orderquery
CP-->>BE: found: true, tipAmount: $10
BE->>BE: confirmTipAdjust(自动对账)
BE->>BE: status = 'completed'
BE-->>Web: TIP_ADJUST_COMPLETED
Web->>Web: 刷新交易列表(小费已自动更新)
stateDiagram-v2
[*] --> pending : requestTipAdjust()
pending --> completed : POS confirm(success=true)
pending --> failed : POS confirm(success=false)
pending --> timeout : 60s 超时 + CodePay 无结果
pending --> completed : 60s 超时 + CodePay 有结果(自动对账)
failed --> pending : Retry POS
timeout --> pending : Retry POS
failed --> completed : Enter Manually (pos_fallback)
timeout --> completed : Enter Manually (pos_fallback)
复用现有基础设施:pos-payment-timeout-scheduler.js 是 067-pos-payment-intent 中为支付请求超时建立的调度器,基于 setTimeout + Map 实现。Tip Adjust 直接复用,使用 tip_adjust:{transactionId} 作为 key。
| 时机 | 操作 | 位置 |
|---|---|---|
| 发送到 POS 后 | scheduleTimeout(60s, handleTipAdjustTimeout) |
requestTipAdjust() 末尾 |
| POS 回调到达 | cancelTimeout() |
pos-device-routes.js + confirmTipAdjust() 开头(双重取消) |
| 60s 超时触发 | handleTipAdjustTimeout() |
查 CodePay → 自动对账 或 设 timeout |
POS 卡交易的 tip_entry_method 是 card_post_payment,正常情况下不允许通过手动小费工作流编辑(EDITABLE_TIP_METHODS guard)。
新增 pos_fallback 方法绕过这个 guard:
// tip-entry-service.js
const EDITABLE_TIP_METHODS = new Set([null, 'cash_post_payment', 'manual_adjustment', 'pos_fallback']);
// saveTipEntry 中的 guard
const isPosFallback = normalizedInput.tipEntryMethod === 'pos_fallback';
if (!isPosFallback && !EDITABLE_TIP_METHODS.has(transaction.current_tip_entry_method)) {
throw errors.badRequest('Card-recorded tips cannot be edited from the cash tip workflow');
}
pos_fallback 会被记录在 transaction_tip_history 的 tip_entry_method 列中,便于审计追踪哪些小费是通过 POS 故障回退录入的。
| 机制 | 作用 | 位置 |
|---|---|---|
双重 cancelTimeout |
路由层 + 服务层都取消超时,防止 POS 回调后超时仍触发 | pos-device-routes.js + confirmTipAdjust() |
status = 'pending' 前置检查 |
超时 handler 只处理仍在 pending 的交易,已 completed/failed 的跳过 | handleTipAdjustTimeout() |
| 乐观锁 UPDATE | WHERE tip_adjust_status = $3 防止并发写入 |
handleTipAdjustTimeout() |
FOR UPDATE OF t 行锁 |
confirmTipAdjust() 用行锁防止并发对账 |
confirmTipAdjust() 内的 SELECT |
tipAdjustableStatuses |
全额退款的交易不显示 fallback UI | TransactionTipEditor.tsx |
isSaveLoading + manualMode |
前端防止双击提交和按钮状态混乱 | TransactionTipEditor.tsx |
如果 handleTipAdjustTimeout 正在查询 CodePay 时,POS 回调同时到达:
confirmTipAdjust 获取行锁,status → completedconfirmTipAdjust 发现 status 已是 completed,抛出 "Tip has already been adjusted"结果:安全。先到的写入生效,后到的被幂等检查拒绝。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
CODEPAY_ACCESS_TOKEN |
空(跳过查询) | CodePay API 认证 Token,未配置时超时直接降级为手动录入 |
CODEPAY_QUERY_URL |
https://api.codepay.us/api/entry/orderquery |
CodePay Query Order API 地址 |
| 常量 | 值 | 位置 |
|---|---|---|
PosConfig.TIP_ADJUST_TIMEOUT_SECONDS |
60 | backend/constants/pos.js |
CODEPAY_ACCESS_TOKEN 未配置,场景三(自动对账)不会工作 — 所有超时都会直接降级为手动录入。生产环境必须配置此 Token。