扫码支付超时时间设置设计

1. 目标

在“支付设置”中新增“扫码支付超时时间”配置项,单位为秒,默认值为 30 秒,可设置范围为 5~120 秒。

该配置只约束扫码支付的整个 HTTP 请求,不改变刷卡支付、刷脸支付以及其他业务接口当前使用的全局网络超时。

2. 已确认的产品规则

  1. 新安装、旧版本升级且本地尚无配置时,扫码支付超时时间均为 30 秒。
  2. 管理员可以在“支付设置”中修改超时时间。
  3. 允许输入 5~120 之间的整数,单位固定为秒。
  4. 空值、非整数和越界值不得保存,界面需要给出明确提示。
  5. 设置保存后从下一笔扫码支付开始生效;进行中的支付不受设置变化影响。
  6. 只有扫码支付使用该配置,刷卡、刷脸和其他网络请求继续沿用当前全局配置。
  7. 扫码支付到达整体超时时间后,继续走现有支付失败回调、提示框和恢复识别流程,不新增自动重试。

3. 当前实现与问题

3.1 当前超时是全局配置

CptApplication 创建的全局 OkHttpClient 将连接、读取和写入超时统一设为 5 秒。RetrofitClient 的普通接口全部复用该客户端,因此目前无法针对扫码支付单独调整。

全局客户端没有设置 callTimeout。连接、写入和读取阶段分别受 5 秒限制,但整个请求没有统一的总时限,实际等待可能超过 5 秒。

3.2 当前扫码支付实际走 HTTP

支付方式编号中,扫码支付为 2。PaymentManager 在普通 HTTP 模式下直接调用 HTTP 支付;在 MQTT 模式下,paymentType == 2 也被强制路由到 HTTP。

因此本次只需要为扫码支付 HTTP 请求建立专用超时,不修改 MQTT 的 5 秒支付结果倒计时。当前“刷卡支付不能走 mqtt”的代码注释与条件不一致,实施时应改为描述真实行为,避免继续误导维护者,但不改变现有支付路由。

3.3 扫码枪收码时间不是支付超时

UsbHidScannerHelper 的 300 毫秒延迟用于判断扫码枪是否完成输入。它发生在支付请求之前,不属于本次配置范围,保持不变。

4. 方案选择

采用“扫码支付专用 Retrofit/OkHttp 客户端 + RxJava 整体超时”方案:从现有全局客户端克隆配置,只覆盖扫码支付需要的连接、读取和写入超时,为专用 Retrofit 实例建立独立缓存,并使用 RxJava timeout 限制整笔请求的总时长。

不采用以下方案:

5. 设置项设计

5.1 配置常量

在 Constants 中新增有明确业务含义的常量:

SCAN_PAYMENT_TIMEOUT_SECONDS
SCAN_PAYMENT_TIMEOUT_SECONDS_DEFAULT = 30
SCAN_PAYMENT_TIMEOUT_SECONDS_MIN = 5
SCAN_PAYMENT_TIMEOUT_SECONDS_MAX = 120

秒数只在常量中定义,不在页面、网络层或测试中重复写入魔法数字。

5.2 配置校验策略

新增纯逻辑的扫码支付超时策略,职责仅包括:

策略类不直接访问界面或发起网络请求,使边界规则可以通过本地单元测试覆盖。

5.3 支付设置界面

在现有“支付方式”区域下新增一行:

扫码支付超时时间       30 秒

交互规则:

  1. 页面加载时读取本地值,经超时策略规范化后显示。
  2. 点击设置行打开现有 InputValueDialogFragment。
  3. 为该弹窗增加可选的数字输入模式;字符串输入场景保持现有行为,不受影响。
  4. 弹窗标题为“扫码支付超时时间”,提示为“请输入 5~120 秒”。
  5. 输入合法整数后保存到 PreferenceUtils,关闭弹窗并立即刷新显示。
  6. 输入为空、不是整数或越界时,弹窗保持打开并显示对应提示。
  7. 关闭“扫码支付”支付方式时仍保留该超时配置,重新启用后继续使用管理员上次保存的值。

6. 网络层设计

6.1 专用客户端

RetrofitClient 增加扫码支付专用客户端创建入口。创建过程必须从 CptApplication.getOkHttpClient().newBuilder() 开始,以继承当前项目已有的拦截器、证书、缓存、重定向等公共网络能力。

专用客户端将以下三项统一设为管理员配置值:

RetrofitRequest 在扫码支付的 Observable 上增加相同秒数的 RxJava timeout,作为从订阅到响应结束的最终上限。达到上限时 RxJava 取消订阅,Retrofit 的 RxJava 适配层随之取消底层 HTTP Call,确保“30 秒”表示整笔扫码支付最多等待 30 秒,而不是每个网络阶段分别等待 30 秒。

6.2 Retrofit 实例缓存

扫码支付 Retrofit 实例按以下两个维度缓存:

baseUrl + timeoutSeconds

当服务地址或超时配置变化时重新创建;未变化时继续复用,避免每笔支付重复创建连接资源。该缓存与现有普通 Retrofit 实例分离,不能覆盖或替换普通业务客户端。

6.3 请求路由

RetrofitRequest 新增语义明确的扫码支付入口,例如 orderScanPayment(...)。该入口接收已经规范化的秒数,使用扫码支付专用 API 实例,并在 Observable 上应用相同秒数的 timeout 操作符。

PaymentManager 在一笔支付开始时执行:

paymentType == 扫码支付
  -> 读取扫码支付超时时间
  -> 规范化为 5~120 秒,缺失或异常则使用 30 秒
  -> 调用扫码支付专用 HTTP 入口

其他支付方式
  -> 保持现有 HTTP/MQTT 路由和超时

配置值在请求开始时读取一次并固定到该笔请求,避免管理员在支付进行中修改设置导致请求语义变化。

7. 超时及异常处理

扫码支付触发整体超时后:

  1. RxJava timeout 结束订阅并取消底层 HTTP Call,把 TimeoutException 交给现有失败链路。
  2. PaymentManager 关闭“支付中”状态。
  3. 继续调用现有 onPaymentFailed()。
  4. 菜品识别页或支付测试页展示现有支付失败弹窗。
  5. 用户关闭结果弹窗后,页面恢复现有识别和支付输入状态。

ExceptionHandle 需要将 java.util.concurrent.TimeoutException 与现有 SocketTimeoutException 一并映射为“请求超时”,避免整体超时被显示成“未知错误”。

本次不增加客户端自动重试,防止付款码在服务端已经受理但客户端未及时收到响应时产生重复扣款风险。

应保留足够的定位日志,但日志不得新增完整付款码输出。超时日志至少包含支付请求 ID、支付方式、配置的超时秒数以及异常类型。

8. 影响范围

计划修改业务仓库中的以下文件或模块:

明确不修改:

9. 测试设计

9.1 单元测试

至少覆盖:

  1. 本地无配置时返回 30 秒。
  2. 5 秒和 120 秒均可保存并生效。
  3. 4 秒、121 秒、负数及异常历史值回退到 30 秒。
  4. 空值和非整数不能从设置弹窗保存。
  5. 扫码支付专用客户端的连接、读取和写入超时均等于配置值。
  6. 扫码支付 Observable 使用配置值作为整体超时,超时后取消底层请求。
  7. TimeoutException 被映射为“请求超时”。
  8. 修改超时时间后专用 Retrofit 缓存更新,普通 Retrofit 实例不变。
  9. paymentType == 2 使用扫码支付专用请求入口,其他支付方式维持原路由。

9.2 手工验收

  1. 清除应用数据后进入支付设置,显示“30 秒”。
  2. 从旧版本覆盖安装后进入支付设置,未设置过时显示“30 秒”。
  3. 分别输入空值、文字、4、121,确认无法保存且提示正确。
  4. 分别保存 5、30、120,退出设置页并重新进入,确认值持久化。
  5. 模拟支付接口超过设置时间不返回,确认到时结束等待并显示支付失败。
  6. 修改配置后发起下一笔扫码支付,确认使用新值。
  7. 同时验证刷卡、刷脸和一个普通接口仍使用原有网络配置。
  8. 连续执行多笔扫码支付,确认没有重复回调、客户端持续创建或线程泄漏。

9.3 工程验证

实现后至少执行:

./gradlew :app:testDebugUnitTest
./gradlew :app:assembleDebug
git diff --check

10. 验收标准

11. 实施边界

本文档仅确认设计,不包含业务代码修改。设计经用户复核后,再生成详细实施计划;实施计划通过后才进入代码修改和验证阶段。