扫码支付出场设计.md 8.3 KB

扫码支付出场设计

状态:设计定稿,待实施(2026-09-09 经 grill-with-docs 访谈收口) 前置条件:① 微信支付商户资质(mchid/APIv3 密钥/证书,小额真实交易测试);② 出口屏硬件采购(现场现状:无任何屏幕) 术语:见根目录 CONTEXT.md;架构决策:docs/adr/0001docs/adr/0002

1. 背景与范围

车辆通过车牌识别进出场;出口无人值守,非放行车辆一律走"识别 → 计费 → 屏显 → 支付 → 开闸"。车主在出口屏上用微信/支付宝扫码支付(车主扫系统屏上的订单二维码),到账后系统自动开闸。

本期做:出口自助支付全链路(订单、渠道抽象、微信 Native、查单确认、放行联动、出口屏 kiosk 页面)。 本期不做:票机/小票链路(继续搁置,见 doc/票机对接规划.md)、支付宝实现(渠道接口预留)、语音播报、对讲接入、纸质缴费凭证、IC 卡。

2. 现状盘点(代码事实)

能力 现状
LPR 出场识别 已自动化:CameraEvent(MQTT)→ HandlePassage.handleExitExitConfirm 自动匹配会话;已支付票自动核销并自动开闸;临时车有费用时拒绝放行(需岗亭处理)——这是要被本功能替换的人工环节
放行车辆 月卡/白名单/RFID 免密放行已有(trigger="rfid"free 方式)
缴费后免费离场 FeeConfig.FreeExitMinutes(默认 15 分钟),paid_at 起算;超时补差逻辑在 buildExitPreview/ExitConfirm本功能"即时支付"路径天然不触发超时,该机制继续保护"别处付费开到出口"的老路径
在线支付 不存在ticket_qr 只是打印票号的预留(InputMode=external,默认停用),无任何支付网关代码
支付成功联动 中央缴费机 /api/paysuccess 回调刻意不开闸(靠车开到出口再被识别);本功能车已停在出口,必须回调/查单后主动结算+开闸——与先例的关键分叉
出口屏/播报 全仓无任何屏幕、语音集成;no_entry_exitgate_failed 异常落库已有
道闸 MQTT 道闸 + 模拟道闸双通道(GateController),人工兜底 POST /parking/gate/open(审计 manual_raise

3. 端到端流程

车到出口 → LPR 识别(channel.Direction=out)→ HandlePassage:
  ├─ 放行车辆 → 直接开闸(现状逻辑,不进支付)
  ├─ 无入场记录 → 落 no_entry_exit + 屏显"无入场记录,请联系管理员",保持关闸
  ├─ 黑名单 → 落事件 + 屏显"请联系管理员",保持关闸
  └─ 临停车(有费用)→ 事件拒绝放行(现状),出口屏感知后进入支付流程:
        屏显费用 + 创建订单(锁定金额,TTL 5 分钟)→ 渲染订单二维码(微信 Native code_url)
        → 车主扫码支付 → 查单确认到账(主要手段,见 ADR-0002)
        → 订单 paid、payment_record 落库(entry=exit_kiosk, method=wechat)
        → ExitConfirm 结算出场(paid_at=now,免费窗口自此起算)
        → 开闸(来源=支付放行)→ 屏显"支付成功,一路平安"
        超时未支付 → 订单过期 → 屏幕重新计费刷新(复用会话,不重复建单)

出口屏感知"车来了":kiosk 页面每 3 秒轮询本通道最近出场识别 + 费用预览(v1 轮询,量大再升级推送)。

4. 组件设计

4.1 订单模块(新增 internal/modules/order/

  • 实体:order{order_no, ticket_no/session_id, plate, amount, currency, channel, channel_txn_id, state, expire_at, event_log}
  • 状态机:pending → paid(查单确认)/ pending → expired(TTL 5 分钟),CAS 原子转移,风格对齐 digital_ticketTransition
  • 幂等:同一会话存在未过期 pending 订单则复用;支付成功只结算、开闸一次。
  • 金额锁定:订单创建时按 ExitPreview 结果定格;过期后重新计费。

4.2 支付渠道抽象(新增 internal/modules/payment/channel/

  • 接口:CreateOrder(order) (codeURL, err)QueryOrder(order) (status, err)HandleNotify(payload)(挂点,暂不启用——ADR-0002)。
  • 实现 v1:微信 Native(code_url 渲染成订单二维码)。接口必须以查单为一等能力。

4.3 到账确认与放行联动

  • 页面轮询(2s)驱动即时查单;服务端定时任务(30s)对未关闭订单兜底查单。
  • 确认到账后单事务:订单转 paid + payment_record 落库(entry=exit_kiosk、method=wechat,方式按渠道拆——ADR-0001)→ ExitConfirm 结算 → OpenGateWithContext 开闸,审计来源标记支付放行(区别于 manual_raise)。
  • 开闸失败:落 gate_failed,屏显"支付成功,请联系管理员放行";已支付状态保留,人工补开闸不重复收费(免费窗口内)。

4.4 出口屏 kiosk 页面(前端新增)

  • 大字自助页面:等待识别 → 费用 + 订单二维码 → 轮询订单状态 → 成功/失败/过期提示。
  • 复用现有 Vue 技术栈与鉴权(页面持设备 token);硬件为新增工控屏/平板,跑浏览器全屏。
  • 新增支付入口配置 exit_kiosk(出口自助),启用方式 wechat(支付宝后续)。

5. 异常与边界处置

场景 处置
无入场记录 关闸 + 屏显提示 + no_entry_exit 落库(已有)
黑名单车 关闸 + 屏显"请联系管理员" + 落事件,绝不自动放行
扫码后不支付/不扫 屏幕保持费用页 + 倒计时提示;订单过期自动刷新重新计费;闸保持关闭;落滞留记录
查单超时/渠道不可用 屏显"网络异常,请稍候",订单保持 pending,恢复后继续;超 5 分钟过期重生成
重复识别(车挪动再触发 LPR) 命中同一会话复用未过期订单,不重复建单
开闸指令失败 gate_failed + 屏显"请联系管理员";已支付保留,人工补开不重复收费
放行车辆 直接开闸,不生成订单

6. 已定决策汇总(访谈结论)

# 决策 结论
1 交互形态 屏显动态订单二维码,车主扫码支付(现场无屏,需新装
2 运营形态 无人自助
3 支付渠道 渠道抽象 + 微信先行(ADR-0001),测试用小额真实交易
4 到账确认 查单为主(页面轮询 + 定时任务),回调暂不建设(ADR-0002,系统部署在内网主机)
5 放行时序 到账即自动结算 + 开闸,不依赖相机二次识别;回调幂等
6 订单模型 金额锁定 + TTL 5 分钟过期刷新 + 同会话复用未过期订单
7 支付方式建模 入口 exit_kiosk + 方式按渠道拆(wechat/alipay
8 兜底范围 屏幕感知车辆用轮询;无入场记录/黑名单关闸落事件;滞留不自动放行
9 范围边界 只做 LPR + 扫码支付出场;票机 M1 继续搁置

7. 外部前置条件(实施前必须落实)

  1. 微信商户资质:mchid、APIv3 密钥/证书、绑定 AppID——尚缺,为第一关键路径(渠道接口可先按 mock 实现,联调前必须到位)。
  2. 币种确认:系统计费币种与微信收款币种(CNY)的关系、跨境资质——影响订单的 currency 字段与金额换算规则,实施前确认。
  3. 出口屏硬件:工控屏或 HDMI 平板(跑浏览器 kiosk),采购与安装。

8. 实施分期

  • M1 后端核心链路:订单模块 + 渠道抽象 + 微信实现(mock 可切换)+ 查单任务 + 放行联动 + exit_kiosk 入口种子;用调试页面验证全链路。
  • M2 出口屏上线:kiosk 页面 + 现场部署联调(真屏、真闸、真相机)。
  • M3 增强:服务端对账报表、支付宝渠道、语音播报、订单看板。

9. 相关文档

  • CONTEXT.md —— 术语表(车牌识别、扫码支付、订单、查单、放行车辆等)
  • docs/adr/0001-payment-channel-abstraction.mddocs/adr/0002-query-first-payment-confirmation.md
  • doc/票机对接规划.md —— 本设计的姊妹规划(入口小票链路,暂缓)
  • doc/中央缴费机对接.mddoc/收费流程.mddoc/设备接入层设计.md