Bläddra i källkod

docs: 沉淀领域术语表与扫码支付/票机/部署盒子方案

- 新增 CONTEXT.md 领域术语表(票机/小票/订单/查单/放行车辆等)
- 新增 docs/adr/0001 支付渠道抽象+微信先行、0002 到账确认以查单为主
- 新增 doc/扫码支付出场设计.md、doc/票机对接规划.md、doc/部署盒子方案.md
- 收录中央缴费机对接文档与项目问题分析文档
lq 10 timmar sedan
förälder
incheckning
242d1e7094

+ 64 - 0
CONTEXT.md

@@ -0,0 +1,64 @@
+# 智慧道闸(Smart Parking)
+
+停车场收费系统的领域语言。系统管理停车场的进出场、计费与收款;车辆身份由车牌与 RFID 识别,费用通过数字票结算。
+
+## Language
+
+**数字票**:
+停车会话的电子凭证,由票号(ticket_no)唯一标识,承载入场、计费、支付与出场状态。所有进场方式(车牌识别、RFID、人工、票机)都生成数字票。
+_Avoid_: 电子券、订单
+
+**票机**:
+安装在入口车道的热敏打印机(CSN / EMD807,79.5mm)。按下按钮后打印入场小票,是数字票的物理载体发放设备。串口与 USB 两种连接方式都必须支持。
+_Avoid_: 打印机(泛指时)、出票机
+
+**小票**:
+票机打印的纸质入场凭证,票面含票号二维码。车主在出口出示小票,扫码后进入支付流程。
+_Avoid_: 凭证、回执
+
+**车牌识别**:
+相机自动识别车牌号码并产生进出场事件的车辆识别方式。本系统入场与出场的主要识别手段。
+_Avoid_: LPR(口语可用)、车牌匹配
+
+**扫码**:
+在出口用扫码设备读取小票上的票号二维码,把纸质小票关联到数字票的动作。仅指读票,见"扫码支付"。
+_Avoid_: 扫票、识别
+
+**扫码支付**:
+车主用支付 App(微信/支付宝)扫描订单二维码完成付款的动作。方向与"扫码"相反:是车主扫系统,不是系统扫票。
+_Avoid_: 扫码(指付款时)、支付扫码
+
+**订单**:
+扫码小票后生成的一次待收款记录,锁定当前应付金额,是订单二维码背后的业务实体。支付渠道确认到账后订单关闭。
+_Avoid_: 账单、交易
+
+**订单二维码**:
+编码订单的动态收款二维码,供车主用手机扫码支付。区别于静态收款码。
+
+**支付渠道**:
+外部的收款服务提供方(如微信支付、支付宝、银行 KHQR)。订单通过某个支付渠道完成收款,渠道确认到账后订单关闭。
+_Avoid_: 支付方式(指 cash/pos 等现场收款手段时)
+
+**查单**:
+系统主动调用支付渠道的订单查询接口确认支付结果的方式。系统部署在内网、无公网回调入口,查单是确认到账的主要手段。
+_Avoid_: 回调确认(指主要手段时)
+
+**放行车辆**:
+月卡、白名单等免缴费直接开闸的车辆。非放行车辆一律走"识别 → 计费 → 屏显 → 支付 → 开闸"。
+_Avoid_: 免费车(歧义:免费时段车辆)
+
+**支付入口**:
+完成收款的三种渠道分类:柜台(counter)、小票(ticket,扫码小票自助支付)、中央缴费机(central_payment_machine)。
+_Avoid_: 支付方式(指 cash/pos 等手段时)
+
+**自助终端**:
+安装在出口车道的立式无人值守设备:集成二维码扫描器、IC 卡读卡区、HELP 对讲按钮、屏幕,可选出票口。车主在此扫码小票、查看订单二维码并完成支付。
+_Avoid_: 缴费机(指中央缴费机时)、kiosk
+
+**按钮盒**:
+入口车道触发票机出票的物理按钮装置。第一版以模拟触发代替实物。
+_Avoid_: 取票按钮
+
+**无人岗亭**:
+出口无值守人员的运营形态,车主自助完成扫码、支付与开闸,系统需自行处理失败与异常。
+_Avoid_: 自助模式、脱机

+ 662 - 0
doc/中央缴费机对接.md

@@ -0,0 +1,662 @@
+# 停车场管理系统对接中央缴费机技术文档
+
+------
+
+## 1. 概述
+
+本文档定义了停车场管理系统(SmartParking)与中央缴费机(如 pof-parking-payment-machine)之间的对接规范,涵盖通信架构、接口定义、业务流程及错误码说明。目标是使功能开发者能够依据本文档独立完成缴费机与停车管理系统的集成开发。
+
+------
+
+## 2. 通信架构
+
+### 2.1 网络通信方式
+
+中央缴费机采用 **TCP/IP** 网络通信方式接入系统 [4],即缴费机与后端服务器通过局域网或互联网建立网络连接。
+
+> ⚠️ **重要**:对接 **不是** 通过串口串联方式,**不是** MQTT 长连接,而是基于 HTTP/HTTPS 的 RESTful API 请求-响应模式。每次业务操作均为独立的 HTTP 请求 [2][7][16]。
+
+### 2.2 协议选型
+
+实际对接中存在两种协议形式,取决于缴费机型号与系统版本:
+
+| 协议                          | 传输层 | 数据格式 | 适用场景                                                   |
+| ----------------------------- | ------ | -------- | ---------------------------------------------------------- |
+| **SmartParking 标准集成协议** | HTTPS  | JSON     | 外部客户平台(通用缴费机)与 SmartParking 系统集成 [2][17] |
+| **POF 缴费机专用协议**        | HTTP   | JSON     | 睿泊科技 POF 缴费机与停车系统后端对接 [7]                  |
+
+下文分别详述两套协议的接口定义。
+
+------
+
+## 3. SmartParking 标准集成协议(HTTPS RESTful API)
+
+该协议用于外部客户平台(如中央缴费机)与 SmartParking 系统的标准化集成 [2][16][17]。
+
+### 3.1 用户认证
+
+**接口**: `POST /sp/login`
+**用途**: 获取 `accessToken`,用于后续所有接口调用的权限验证 [2]
+
+**请求参数**:
+
+| 参数     | 类型   | 必填 | 说明                      |
+| -------- | ------ | ---- | ------------------------- |
+| userName | String | 是   | 用户名                    |
+| passWord | String | 是   | 32位大写 MD5 加密后的密码 |
+
+**请求示例**:
+
+```json
+{
+  "userName": "paystation001",
+  "passWord": "E10ADC3949BA59ABBE56E057F20F883E"
+}
+```
+
+**成功返回**:
+
+```json
+{
+  "accessToken": "xxxxxxxxxxxx",
+  "sessionId": "yyyyyyyyyyyy",
+  "expireInSeconds": 1800
+}
+```
+
+**字段说明**:
+
+- `accessToken`: 访问令牌,**有效期 30 分钟**,后续接口调用时需使用此值作为会话凭证 [2]
+- `sessionId`: 会话 ID
+- `expireInSeconds`: 令牌有效时长(秒)
+
+------
+
+### 3.2 按车牌查询费用
+
+**接口**: `POST /sp/charge/payCost`
+**用途**: 实时计算当前车辆的停车费用,供缴费机完成预支付 [2][17]
+
+**请求参数**:
+
+| 参数           | 类型   | 必填 | 说明                      |
+| -------------- | ------ | ---- | ------------------------- |
+| sessionId      | String | 是   | 登录返回的 accessToken    |
+| carNum         | String | 是   | 车牌号                    |
+| parkingLotCode | String | 是   | 停车场 ID(默认 0001)[2] |
+
+**成功返回**:
+
+```json
+{
+  "carAccessCurId": "车辆业务ID",
+  "freeOutTime": 15,
+  "inParkTime": 1714000000,
+  "receivableMoney": 12.00,
+  "payedMoney": 0.00,
+  "favorableAmount": 0.00,
+  "ticketNum": "TICKET001"
+}
+```
+
+**字段说明**:
+
+| 字段            | 类型    | 说明                                                |
+| --------------- | ------- | --------------------------------------------------- |
+| carAccessCurId  | String  | 车辆业务 ID,车辆入场后生成,一次进出场业务唯一 [2] |
+| freeOutTime     | Int     | 支付后免费出场分钟数 [2]                            |
+| inParkTime      | Long    | 入场时间戳 [2]                                      |
+| receivableMoney | Decimal | 应收金额(消费金额)[2]                             |
+| payedMoney      | Decimal | 已付金额 [2]                                        |
+| favorableAmount | Decimal | 优惠金额 [2]                                        |
+| ticketNum       | String  | 票据号 [2]                                          |
+
+------
+
+### 3.3 支付成功通知
+
+**接口**: `POST /sp/charge/notify`
+**用途**: 客户平台将支付账单插入 SmartParking,通知支付成功并触发放行 [2][17]
+
+**请求参数**:
+
+| 参数           | 类型    | 必填 | 说明                                                         |
+| -------------- | ------- | ---- | ------------------------------------------------------------ |
+| sessionId      | String  | 是   | 登录返回的 accessToken                                       |
+| carAccessCurId | String  | 是   | 车辆业务 ID(查询费用时返回)[2]                             |
+| carNum         | String  | 是   | 车牌号或 UUID(无牌车)[2]                                   |
+| consumeMoney   | Decimal | 是   | 消费金额 [2]                                                 |
+| consumeTimeStr | String  | 是   | 支付时间 [2]                                                 |
+| feeAmount      | Decimal | 是   | 实际支付金额 [2]                                             |
+| feeType        | Int     | 是   | 支付方式类型:1-现金,9-支付宝,10-微信支付,16-微信公众号支付 [2] |
+| orderStatus    | String  | 是   | 订单状态 [2]                                                 |
+| parkingLotCode | String  | 是   | 停车场 ID [2]                                                |
+| serialNumber   | String  | 是   | 订单支付流水号(**需保证唯一**)[2]                          |
+| ticketNum      | String  | 否   | 票据号 [2]                                                   |
+
+**说明**: [2]
+
+- 支付成功后 SmartParking 自动查询并放行(未配置出口确认规则时)
+- 若 `serialNumber` 重复,返回错误码 `0021`(流水号已存在)
+
+------
+
+### 3.4 全局错误码
+
+| 错误码 | 说明                              |
+| ------ | --------------------------------- |
+| 0000   | 成功                              |
+| 0001   | 失败(具体错误见 errMsg 字段)[2] |
+| 0004   | 登录密码错误 [2]                  |
+| 0006   | 认证失败,请重新登录 [2]          |
+| 0007   | 请求参数不存在 [2]                |
+| 0017   | 支付金额不足 [2]                  |
+| 0021   | 流水号已存在(重复通知)[2]       |
+
+------
+
+## 4. POF 缴费机专用协议(HTTP POST JSON)
+
+该协议定义 POF 停车缴费机与停车系统后端之间的完整接口规范 [7]。
+
+### 4.1 通用说明
+
+- **接口类型**: HTTP POST
+- **编码**: UTF-8
+- **数据格式**: JSON
+- **调用失败统一返回**: `{"Result":0, "Data":{}, "ReMark":"失败原因"}` [7]
+- **成功返回**: `{"Result":1, ...}`
+- **认证机制**: 除登录接口外,所有业务接口均需携带 `Token` 参数 [1]
+
+### 4.2 操作员登录
+
+**接口**: `POST /api/login`
+**用途**: 操作员登录,获取 Token,用于后续接口鉴权 [7]
+
+**请求参数**:
+
+| 参数     | 类型   | 必填 | 说明     |
+| -------- | ------ | ---- | -------- |
+| UserName | String | 是   | 用户名   |
+| Password | String | 是   | 登录密码,当前必须传明文 UTF-8 字符串;不支持 MD5 或 bcrypt 哈希 |
+
+**请求示例**:
+
+```json
+{"UserName":"admin","Password":"admin"}
+```
+
+**成功返回**:
+
+```json
+{"Result":1,"Token":"123456"}
+```
+
+------
+
+### 4.3 查询车辆费用
+
+**接口**: `POST /api/queryprice`
+**用途**: 按车牌查询车辆停车费用 [7]
+
+**请求参数**:
+
+| 参数           | 类型   | 必填 | 说明                         |
+| -------------- | ------ | ---- | ---------------------------- |
+| Token          | String | 是   | 认证令牌                     |
+| Data.CarNo     | String | 是   | 查询车牌                     |
+| Data.QueryTime | String | 是   | 请求时间 yyyy-MM-dd HH:mm:ss |
+
+**成功返回**:
+
+```json
+{
+  "Result": 1,
+  "Data": {
+    "OrderId": "123456",
+    "CarNo": "B88888",
+    "InTime": "2023-04-25 12:00:01",
+    "OutTime": "2023-04-26 09:03:23",
+    "StopTime": "1小时20分钟",
+    "Price": "12",
+    "CardNo": "00001",
+    "Remark": "收费标准0001",
+    "InPic": ""
+  }
+}
+```
+
+**字段说明**: [7]
+
+| 字段     | 说明                                    |
+| -------- | --------------------------------------- |
+| OrderId  | 订单号(后续所有操作的核心关联标识)[1] |
+| CarNo    | 车牌号                                  |
+| InTime   | 入场时间                                |
+| OutTime  | 出场时间                                |
+| StopTime | 停放时长                                |
+| Price    | 应收金额                                |
+| CardNo   | 卡号(月租/会员卡)                     |
+| Remark   | 计费规则说明                            |
+| InPic    | 入场图片                                |
+
+------
+
+### 4.4 支付成功通知
+
+**接口**: `POST /api/paysuccess`
+**用途**: 缴费机完成支付后通知后端 [7]
+
+**请求参数**:
+
+| 参数         | 类型   | 必填 | 说明                                       |
+| ------------ | ------ | ---- | ------------------------------------------ |
+| Token        | String | 是   | 认证令牌                                   |
+| Data.OrderId | String | 是   | 订单号                                     |
+| Data.CarNo   | String | 是   | 车牌                                       |
+| Data.Money   | String | 是   | 缴费金额                                   |
+| Data.PayTime | String | 是   | 支付时间 yyyy-MM-dd HH:mm:ss               |
+| Data.PayType | num    | 是   | 支付方式:**0-现金,1-刷卡** [1][7]        |
+| Data.PayDev  | num    | 是   | 支付设备:**1-岗亭,2-POF,3-平板** [1][7] |
+| Data.DevId   | String | 是   | 设备标识(如 "PoF-01")[1]                 |
+| Data.ReMark  | String | 是   | 备注                                       |
+
+------
+
+### 4.5 心跳检测
+
+**接口**: `POST /api/heartbeat`
+**用途**: 心跳检测,返回服务器时间 [1]
+
+**请求参数**: Token(认证令牌)
+
+**返回**: 服务器系统时间。
+
+------
+
+### 4.6 推送优惠券
+
+**接口**: `POST /api/ticket`
+**用途**: 向订单推送优惠券 [7]
+
+**请求参数**:
+
+| 参数            | 类型   | 必填 | 说明                                               |
+| --------------- | ------ | ---- | -------------------------------------------------- |
+| Token           | String | 是   | 认证令牌                                           |
+| Data.OrderId    | String | 是   | 订单号                                             |
+| Data.CarNo      | String | 是   | 车牌                                               |
+| Data.TicketNo   | String | 是   | 优惠券编号                                         |
+| Data.TicketType | num    | 是   | 优惠券类型:**0-减免金额,1-减免时长,2-全免** [1] |
+| Data.Money      | String | 是   | 减免金额                                           |
+| Data.Minutes    | num    | 是   | 减免时间(分钟)                                   |
+| Data.OperTime   | String | 是   | 操作时间                                           |
+| Data.ShopName   | String | 是   | 商户名称或卡号                                     |
+| Data.ReMark     | String | 是   | 备注                                               |
+
+------
+
+### 4.7 投币明细上报
+
+**接口**: `POST /api/paylist`
+**用途**: 上报缴费机投币/找零明细 [7]
+
+**请求参数**:
+
+| 参数              | 类型     | 必填 | 说明                   |
+| ----------------- | -------- | ---- | ---------------------- |
+| Token             | String   | 是   | 认证令牌               |
+| Data.OrderId      | String   | 是   | 订单号                 |
+| Data.VoucherNo    | String   | 是   | 凭证编号               |
+| Data.DevIp        | String   | 是   | 设备 IP                |
+| Data.EventDate    | Datetime | 是   | 发生时间               |
+| Data.InOutType    | num      | 是   | **0-接收,1-找零** [7] |
+| Data.CoinCashType | num      | 是   | **0-硬币,1-纸币** [7] |
+| Data.MValue       | String   | 是   | 面值                   |
+| Data.Num          | Int      | 是   | 数量                   |
+
+------
+
+### 4.8 找零失败记录
+
+**接口**: `POST /api/payfail`
+**用途**: 记录现金支付找零失败事件 [7]
+
+**请求参数**:
+
+| 参数           | 类型     | 必填 | 说明                                              |
+| -------------- | -------- | ---- | ------------------------------------------------- |
+| Token          | String   | 是   | 认证令牌                                          |
+| Data.OrderId   | String   | 是   | 订单号                                            |
+| Data.CarNo     | String   | 是   | 凭证编号(字段名与备注可能不符,需与后端确认)[7] |
+| Data.Intime    | String   | 是   | 入场时间                                          |
+| Data.Change1   | decimal  | 是   | 应收金额                                          |
+| Data.Change2   | decimal  | 是   | 实收金额                                          |
+| Data.Change3   | decimal  | 是   | 应找零金额                                        |
+| Data.Change4   | decimal  | 是   | 实际找零金额                                      |
+| Data.EventTime | Datetime | 是   | 找零时间                                          |
+
+------
+
+### 4.9 设备状态上报
+
+**接口**: `POST /api/devstatus`
+**用途**: 上报缴费机设备运行状态 [7]
+
+**请求参数**:
+
+| 参数                      | 类型   | 必填 | 说明                                           |
+| ------------------------- | ------ | ---- | ---------------------------------------------- |
+| Token                     | String | 是   | 认证令牌                                       |
+| Data.Printer              | string | 是   | **0-余量充足,1-余量不足,2-无可用打印纸** [7] |
+| Data.Temperature          | string | 是   | 设备温度(摄氏度)                             |
+| Data.Payment.CurrencyInfo | array  | 是   | 货币类型/面值/数量                             |
+| Data.Payment.Online       | bool   | 是   | POS 在线状态                                   |
+
+------
+
+### 4.10 交班登录
+
+**接口**: `POST /api/pof-login`
+**用途**: 操作员交班身份验证 [7][19]
+
+**请求参数**: 与操作员登录接口相同(UserName + Password),返回的 Token 用于交班场景 [7]。
+
+------
+
+### 4.11 查询收费总额
+
+**接口**: `POST /api/pof-querymoney`
+**用途**: 查询指定设备在指定时间范围内的收费总额,用于交班对账 [7][19]
+
+**请求参数**:
+
+| 参数           | 类型   | 必填 | 说明                    |
+| -------------- | ------ | ---- | ----------------------- |
+| Token          | String | 是   | 认证令牌                |
+| Data.DevId     | String | 是   | 设备标识(如 "PoF-01") |
+| Data.QueryTime | String | 是   | 请求时间                |
+
+**成功返回**:
+
+```json
+{"Result":1,"Data":{"TotalMoney":"12","Remark":"收费标准0001"}}
+```
+
+------
+
+### 4.12 待完善接口
+
+| 接口                 | 现状       | 说明                                                    |
+| -------------------- | ---------- | ------------------------------------------------------- |
+| `/api/paystatus`     | 已实现     | 按 `OrderId` 查询数字票和支付状态;具体兼容字段见 4.13 |
+| `/api/checkpassword` | 参数已知   | 密码验证(`Data.Password`),但具体用途待确认 [7]       |
+
+### 4.13 支付状态查询(第二阶段已实现)
+
+**接口**: `POST /api/paystatus`
+**用途**: 支付通知超时、网络异常或缴费机重启后,查询订单在管理端的最终支付状态。该接口只读数据库,不会重复扣款或新增支付记录。
+
+**请求示例**:
+
+```json
+{"Token":"登录返回的Token","Data":{"OrderId":"数字票号","CarNo":"湘A12345","DevId":"设备标识"}}
+```
+
+`OrderId` 必填;`CarNo` 和 `DevId` 用于日志及一致性核对,可选。系统仍以 Token 绑定的设备身份认证,并校验设备所属停车场权限。
+
+**成功返回**:
+
+```json
+{"Result":1,"Data":{"OrderId":"...","CarNo":"湘A12345","State":"paid","PayStatus":"paid","PaymentStatus":"paid","Fee":"12.00","Money":"12.00","PaidAmount":"12.00","PayTime":"2026-09-04 12:00:00"},"ReMark":"success"}
+```
+
+`PayStatus` 为 `unpaid` 或 `paid`;`State` 还可能为 `pending_payment`、`exited` 等数字票生命周期状态。订单不存在、车牌不匹配、设备无权访问或 Token 无效时返回 `Result=0` 及失败原因。
+
+> ⚠️ 上述接口在实际对接时需与后端开发确认完整定义。 [1][7]
+
+------
+
+## 5. 对接业务流程
+
+### 5.1 正常支付流程
+
+缴费机从查询费用到完成支付的完整流程如下 [9]:
+
+```
+┌─────────┐      ┌──────────────┐      ┌─────────┐
+│  缴费机   │      │  停车系统后端  │      │  用户    │
+└────┬────┘      └──────┬───────┘      └────┬────┘
+     │  1. 用户输入车牌    │                   │
+     │──────────────────→│                   │
+     │  2. /api/login     │                   │
+     │──────────────────→│ 返回 Token        │
+     │←──────────────────│                   │
+     │  3. /api/queryprice│                   │
+     │  (或 /sp/charge/   │                   │
+     │   payCost)         │                   │
+     │──────────────────→│ 返回车辆信息+费用   │
+     │←──────────────────│                   │
+     │  4. 用户支付        │                   │
+     │                   │                   │←─── 现金/刷卡/扫码
+     │  5. /api/paysuccess│                   │
+     │  (或 /sp/charge/   │                   │
+     │   notify)          │                   │
+     │──────────────────→│ 记录支付+放行       │
+     │←──────────────────│ 返回成功           │
+     │  6. 打印小票        │                   │
+```
+
+**流程步骤说明**: [9]
+
+1. 用户在缴费机输入车牌号
+2. 缴费机调用登录接口获取 Token(若 Token 未过期可复用)
+3. 调用查询费用接口获取订单信息(订单号、入场时间、应收金额等)
+4. 用户完成支付(现金、刷卡或扫码)
+5. 缴费机调用支付成功通知接口,携带订单号、金额、支付方式、设备标识等
+6. 后端确认支付成功后标记订单已缴费;车辆到出口后由既有出口确认流程校验缴费状态并放行
+7. 缴费机打印小票(可选步骤,调用优惠券接口进行减免)[9]
+
+### 5.2 优惠券抵扣流程(可选)
+
+在支付完成前或完成后,可调用优惠券接口(`/api/ticket`)应用减免 [9]。
+
+### 5.3 找零失败处理
+
+现金支付产生找零时,若找零失败,通过 `/api/payfail` 上报记录,便于后续人工处理 [9]。
+
+### 5.4 交班对账流程
+
+```
+┌─────────┐      ┌──────────────┐
+│  缴费机   │      │  停车系统后端  │
+└────┬────┘      └──────┬───────┘
+     │  1. /api/pof-login│
+     │──────────────────→│ 交班登录验证
+     │←──────────────────│ 返回 Token
+     │  2. /api/pof-querymoney│
+     │──────────────────→│ 查询收费总额
+     │←──────────────────│ 返回 TotalMoney
+     │  3. 核对金额,完成交班│
+```
+
+**流程说明**: [19]
+
+1. 接班操作员调用交班登录接口进行身份验证,获取交班会话 Token
+2. 查询当前设备累计收费总额(携带 DevId)
+3. 双方核对金额完成交接
+4. 系统记录交班日志
+
+------
+
+## 6. 关键字段速查表
+
+| 字段                    | 所属协议 | 说明                                               |
+| ----------------------- | -------- | -------------------------------------------------- |
+| Token / accessToken     | 两套协议 | 认证令牌,接口鉴权凭证 [2][7]                      |
+| OrderId                 | POF 协议 | 订单号,核心关联标识 [1]                           |
+| carAccessCurId          | 标准协议 | 车辆业务 ID [2]                                    |
+| CarNo / carNum          | 两套协议 | 车牌号(无牌车用 UUID)[2][7]                      |
+| Price / receivableMoney | 两套协议 | 应收金额 [2][7]                                    |
+| Money / feeAmount       | 两套协议 | 实付金额 [2][7]                                    |
+| PayType                 | POF 协议 | 支付方式:0-现金,1-刷卡 [1]                       |
+| feeType                 | 标准协议 | 支付方式:1-现金,9-支付宝,10-微信,16-公众号 [2] |
+| PayDev                  | POF 协议 | 支付设备:1-岗亭,2-POF,3-平板 [1]                |
+| DevId                   | POF 协议 | 设备标识(如 "PoF-01")[1]                         |
+| serialNumber            | 标准协议 | 支付流水号(唯一)[2]                              |
+| TicketType              | POF 协议 | 优惠券类型:0-减金额,1-减时长,2-全免 [1]         |
+
+------
+
+## 7. 异常与边界处理建议
+
+### 7.1 支付金额不足
+
+标准协议中,若实际支付金额低于应收金额,系统返回错误码 `0017`(支付金额不足)[2]。测试用例显示系统允许部分支付(生成支付记录但不视为完全支付)[6],缴费机应根据业务需求决定是否接受部分支付。
+
+### 7.2 重复通知
+
+若支付通知的流水号已存在,返回错误码 `0021`。缴费机收到该错误码时应视为已通知成功,避免重复处理 [2]。
+
+### 7.3 Token 过期
+
+标准协议中 accessToken 有效期 30 分钟 [2]。收到 `0006`(认证失败)时应重新调用登录接口获取新 Token。
+
+### 7.4 网络异常
+
+- 缴费机侧应实现请求超时重试机制
+- 所有接口为 HTTP 请求-响应模式,无长连接维护 [2][7]
+- 对于离线场景(如断网),建议增加本地缓存和补传机制(PoF 刷卡支付场景尤其重要)
+
+### 7.5 待确认事项
+
+实际对接时需与后端确认以下问题: [1][7]
+
+1. `/api/devstatus` 的完整请求/响应定义缺失;`/api/paystatus` 已按本项目兼容格式实现
+2. `/api/paylist` 的 `OrderId` 参数逻辑(投币明细通常与设备会话相关,而非订单)
+3. `/api/payfail` 的 `CarNo` 参数备注为"凭证编号",是否为笔误
+4. `/api/checkpassword` 的具体业务用途
+
+------
+
+## 8. 开发建议
+
+1. **优先确定协议版本**:确认缴费机型号和所对接的后端系统版本,选择对应的协议(标准集成协议或 POF 专用协议)
+2. **模块化封装**:按 微型三层架构 将缴费机对接封装为独立模块,便于维护和扩展
+3. **幂等处理**:支付通知接口必须实现幂等,处理重复调用场景 [2]
+4. **日志记录**:完整记录请求/响应日志,便于排查对账问题
+5. **测试覆盖**:参照 [智慧停车系统 — 测试用例报告](wikilink:智慧停车系统 — 测试用例报告) 中部分支付、重复入场、无收费配置兜底等场景设计测试用例 [6]
+
+------
+
+## 9. 本项目 POF 实现约定与模拟桩联调
+
+### 9.1 已实现范围
+
+本项目已按 POF 专用协议实现以下公开 HTTP POST 接口:
+
+| 接口 | 实现状态 | 约定 |
+| --- | --- | --- |
+| `/api/login` | 已实现 | 使用中央缴费机设备账号登录,返回 30 分钟有效的设备 Token |
+| `/api/queryprice` | 已实现 | 按当前在场会话实时计费,返回数字票号作为 `OrderId` |
+| `/api/paysuccess` | 已实现 | 写入支付记录、更新数字票和在场车辆为已支付;不会主动开闸或关闭停车会话 |
+| `/api/heartbeat` | 已实现 | 返回服务器时间 |
+
+接口统一挂载在后端服务的公开 `/api` 路径,并在每个业务接口内校验 POF 设备 Token;不会使用后台 Web 用户的 JWT,也不会使用 `admin/123456`。
+
+### 9.2 订单、支付和出口规则
+
+1. `OrderId` 固定映射为 `digital_ticket.ticket_no`,是本项目 POF 支付的唯一订单标识。
+2. `/api/queryprice` 只允许查询仍在场、数字票状态为 `pending_payment` 且未支付的车辆。费用以服务端当前时间实时计算。
+3. `/api/paysuccess` 必须携带 `PayDev=2`。`DevId` 作为协议和审计字段记录,但不参与阻断式认证;系统以 Token 绑定的设备身份为准。`PayType=0` 映射 `cash`(展示名“现金支付”),`PayType=1` 映射 `pos`;若管理员停用了 POS 支付方式,刷卡通知会被拒绝,需先在支付方式配置中启用。
+4. 支付通知前会重新实时算费;`Money` 与当前应收金额不一致时,返回“金额已变化,请重新查询”。
+5. 缴费成功只标记订单和停车会话已支付。车辆在出口由既有出口流程确认离场和放行,以保留“缴费后免费离场时限”的统一校验。
+6. 厂商协议没有唯一支付流水号。当前按 `OrderId + 支付方式 + 应收金额` 实现订单级幂等:相同通知重试成功但不会产生第二条收费记录;数据不一致的重复通知失败。所有请求(Token 已脱敏)写入 `central_payment_request_log` 供联调与对账。
+
+### 9.3 查询时间兼容
+
+厂商资料的 `/api/queryprice` 参数表使用 `Data.QueryTime`,示例却使用 `Data.InTime`。当前服务同时接收这两个字段,但不将其作为入场时间或计费时间,均以系统已记录的入场时间和服务器当前时间为准。
+
+### 9.4 模拟桩
+
+启动应用一次后,系统会创建仅用于本地联调的模拟设备:
+
+| 项目 | 值 |
+| --- | --- |
+| 设备 ID | `POF-SIM-01` |
+| 用户名 | `admin` |
+| 密码 | `admin` |
+| 支付入口 | `central_payment_machine` |
+
+> 安全提示:上述凭据仅限本机或隔离测试网。真机安装前必须禁用或删除该模拟设备,并为每台真机创建独立设备 ID、账号和密码。
+
+先在系统中准备一条未支付的在场车辆记录,再从 `lc_garage` 目录运行:
+
+```powershell
+go run ./cmd/pof-simulator -base-url http://127.0.0.1:8888 -plate 粤A12345
+```
+
+模拟桩会依次调用 `login -> queryprice -> paysuccess -> heartbeat`,并打印每个接口原始 JSON 响应。仅验证登录、查询和心跳时使用:
+
+```powershell
+go run ./cmd/pof-simulator -base-url http://127.0.0.1:8888 -plate 粤A12345 -no-pay
+```
+
+生产或局域网部署时,缴费机访问的是后端监听地址,例如 `http://<服务器IP>:8888/api/login`;不是桌面前端 WebView 内的代理地址。实际端口以 `config.yaml` 的 `system.addr` 为准。
+
+### 9.5 联调日志与登录密码格式
+
+中央缴费机请求进入管理端后,后端运行日志会依次输出“收到请求”和“处理成功/失败”两类记录。登录请求包含以下不敏感联调字段:
+
+- `source_ip`:缴费机来源 IP;
+- `username`:提交的设备账号;
+- `password_format`:密码格式识别结果;
+- `password_length`:密码字符长度。
+
+系统不会记录登录密码明文、密码哈希或设备 Token。`Password` 当前必须按 JSON 字符串传输明文,例如 `"Password":"admin"`;日志中的 `plain-text_expected` 表示格式符合当前协议。出现 `md5-32-hex_unsupported` 或 `bcrypt-hash_unsupported` 时,说明缴费机传入了摘要/哈希值,当前服务会按密码错误处理,设备应改为发送明文密码。
+
+处理结果同时写入 SQLite 的 `central_payment_request_log` 表,包含 `endpoint`、`source_ip`、`device_id`、`result`、`remark` 和已脱敏的 `request_body`,可用于重启后的联调追溯。
+
+应用使用默认配置时,运行日志按日期写入 `log\YYYY-MM-DD\info.log`(收到请求、处理成功)和 `log\YYYY-MM-DD\warn.log`(解析或鉴权失败)。实时观察登录联调日志可执行:
+
+```powershell
+Get-Content (Join-Path .\log (Join-Path (Get-Date -Format 'yyyy-MM-dd') 'info.log')) -Wait | Select-String '中央缴费机'
+```
+
+若登录失败,再查看同日期的 `warn.log`。通过项目管理脚本以 `split` 模式启动时,标准输出还会同步写入 `.run\backend.stdout.log`。
+
+代码库还提供不依赖真实数据库或硬件的 HTTP 联调回归测试。它以内存 SQLite 建立一条在场订单,并按模拟桩顺序验证 `login -> queryprice -> paysuccess -> paystatus -> 重试 paysuccess -> heartbeat`,确保状态查询可确认支付结果且重复支付通知不会产生第二笔支付记录:
+
+```powershell
+$env:GOCACHE = (Resolve-Path '.gocache').Path
+go test ./internal/modules/central-payment/... -run TestPOFSimulatorHTTPFlow -count=1
+```
+
+### 9.5 厂商待确认项
+
+以下内容未在当前实现范围内,需在真机到位前向厂商确认后再扩展:
+
+1. `/api/checkpassword` 仅给出 `Data.Password`,没有业务语义,当前未实现。
+2. `/api/ticket` 的优惠券时机、抵扣规则与本系统收费规则尚未定义,当前未实现。
+3. `/api/paylist` 投币/找零明细的 `VoucherNo` 唯一性、补传重试和对账口径未定义,当前未实现。
+4. `/api/payfail` 的 `CarNo` 字段说明写为“凭证编号”,字段语义冲突,当前未实现。
+5. `/api/devstatus` 中 `Payment.CurrencyInfo` 的数据结构和设备故障处置规则未定义,当前未实现。
+6. POF 支付成功通知没有唯一外部流水号。建议厂商在 `Data` 中补充稳定且唯一的 `VoucherNo` 或 `SerialNo`,以支持跨订单、跨重试窗口的精确幂等和财务对账。
+7. 厂商样例中存在中文引号、缺少逗号等非合法 JSON 的写法;设备必须发送合法 UTF-8 JSON,服务端不会对非 JSON 文本进行猜测性修复。
+8. 厂商资料明确列出 `/api/pof-login` 和 `/api/pof-querymoney`,但没有定义交班时段/班次边界,也没有说明 `QueryTime` 是查询时点还是统计区间。当前未实现,待现场交班和对账口径确认后扩展。
+
+------
+
+## 10. 参考资料
+
+| 编号 | 资料                                  |
+| ---- | ------------------------------------- |
+| [1]  | 停车系统缴费接口说明补全polan(4)      |
+| [2]  | 自助缴费机集成网络协议(英文版 2025) |
+| [4]  | POF 停车缴费机硬件规格                |
+| [6]  | 智慧停车系统 — 测试用例报告           |
+| [7]  | 缴费机 API 接口文档                   |
+| [9]  | 缴费机支付流程                        |
+| [16] | 自助缴费机集成协议                    |
+| [17] | SmartParking 集成协议                 |
+| [19] | 交班对账机制                          |

+ 109 - 0
doc/扫码支付出场设计.md

@@ -0,0 +1,109 @@
+# 扫码支付出场设计
+
+> 状态:**设计定稿,待实施**(2026-09-09 经 grill-with-docs 访谈收口)
+> 前置条件:① 微信支付商户资质(mchid/APIv3 密钥/证书,小额真实交易测试);② 出口屏硬件采购(现场现状:无任何屏幕)
+> 术语:见根目录 `CONTEXT.md`;架构决策:`docs/adr/0001`、`docs/adr/0002`
+
+## 1. 背景与范围
+
+车辆通过**车牌识别**进出场;出口无人值守,非放行车辆一律走"识别 → 计费 → 屏显 → 支付 → 开闸"。车主在出口屏上用微信/支付宝**扫码支付**(车主扫系统屏上的订单二维码),到账后系统自动开闸。
+
+**本期做**:出口自助支付全链路(订单、渠道抽象、微信 Native、查单确认、放行联动、出口屏 kiosk 页面)。
+**本期不做**:票机/小票链路(继续搁置,见 `doc/票机对接规划.md`)、支付宝实现(渠道接口预留)、语音播报、对讲接入、纸质缴费凭证、IC 卡。
+
+## 2. 现状盘点(代码事实)
+
+| 能力 | 现状 |
+|------|------|
+| LPR 出场识别 | 已自动化:`CameraEvent`(MQTT)→ `HandlePassage.handleExit` → `ExitConfirm` 自动匹配会话;**已支付票自动核销并自动开闸**;临时车有费用时拒绝放行(需岗亭处理)——这是要被本功能替换的人工环节 |
+| 放行车辆 | 月卡/白名单/RFID 免密放行已有(`trigger="rfid"`、`free` 方式) |
+| 缴费后免费离场 | `FeeConfig.FreeExitMinutes`(默认 15 分钟),`paid_at` 起算;超时补差逻辑在 `buildExitPreview`/`ExitConfirm`。**本功能"即时支付"路径天然不触发超时**,该机制继续保护"别处付费开到出口"的老路径 |
+| 在线支付 | **不存在**。`ticket_qr` 只是打印票号的预留(InputMode=external,默认停用),无任何支付网关代码 |
+| 支付成功联动 | 中央缴费机 `/api/paysuccess` 回调刻意**不开闸**(靠车开到出口再被识别);本功能车已停在出口,必须回调/查单后主动结算+开闸——与先例的关键分叉 |
+| 出口屏/播报 | 全仓无任何屏幕、语音集成;`no_entry_exit`、`gate_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_ticket` 的 `Transition`。
+- 幂等:同一会话存在未过期 `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.md`、`docs/adr/0002-query-first-payment-confirmation.md`
+- `doc/票机对接规划.md` —— 本设计的姊妹规划(入口小票链路,暂缓)
+- `doc/中央缴费机对接.md`、`doc/收费流程.md`、`doc/设备接入层设计.md`

+ 88 - 0
doc/票机对接规划.md

@@ -0,0 +1,88 @@
+# 票机对接规划
+
+> 状态:**规划存档,暂缓实施**(2026-09-09 评估复杂度后决定推迟)
+> 产出方式:grill-with-docs 设计访谈;领域术语见仓库根目录 `CONTEXT.md`
+> 重启本项目时:先回答[第 6 节 待定决策](#6-待定决策重启时先答),再动工
+>
+> **更新 2026-09-09**:原 M2(出口自助支付)已拆出并演化为独立设计——**LPR + 扫码支付出场**,见 `doc/扫码支付出场设计.md`(车辆识别由"扫小票"改为"车牌识别",订单/渠道/查单/放行联动设计以该文档为准)。本规划仅保留 M1(入场小票闭环)及后续票机增强。
+
+## 1. 背景与目标
+
+票机是**支付的一种入口**(对应系统现有支付入口分类 `ticket`)。目标流程:
+
+```
+入口:车主触发按钮盒 → 票机打印小票(含 ticket_no 二维码)→ 开闸入场
+出口:车主在自助终端扫码小票 → 系统生成订单 + 订单二维码(动态、带金额)
+     → 车主手机扫码支付(微信支付)→ 回调确认到账 → 自动开闸出场
+```
+
+范围选定为 **C:两条链路都做**——
+- **A. 入场小票闭环**:触发端 + 打印机管理前端 + 失败处理(后端打印链路已有 80%)
+- **B. 出口自助支付**:扫码小票 → 动态订单二维码 → 支付 → 自动出场(全新开发,本期核心)
+
+## 2. 现状盘点(代码事实,2026-09-09)
+
+| 模块 | 现状 |
+|------|------|
+| `internal/modules/printer/` | 已对接真实票机:CSN USB DLL(`CsnPrinterLibs.dll`)+ 串口 ESC/POS(EMD807 79.5mm)双通道;票面含票号二维码;`POST /ticket-machine/button` 已写好但**全仓库无调用方**(触发端缺失) |
+| 前端 | **无任何打印机管理/补打 UI**;incident 字典已预留 `print_failed`/`reprint` 事件类型 |
+| 数字票 `digital-ticket` | 状态机 `created→pending_payment→paid→exited`,票号即二维码内容;票机出的票就是数字票的物理载体 |
+| 支付 `payment` | 支付成功**无任何打印/出场钩子**;`ticket_qr` 支付方式目前展示**静态收款码 + 人工确认**,动态订单二维码不存在 |
+| 中央缴费机 `central-payment` | 独立设备(POF 协议、自带打印、`/api/paysuccess` 回调),与出口自助终端是**两类设备,勿混淆** |
+| 相关设计文档 | `docs/superpowers/specs/2026-07-30-ticket-printer-design.md`、`plans/2026-07-30-ticket-printer-plan.md`、`plans/2026-07-31-usb-ticket-photo-layout-plan.md` |
+
+## 3. 已定决策
+
+| # | 决策点 | 结论 |
+|---|--------|------|
+| 1 | 范围 | A + B 都要,入口闭环先行 |
+| 2 | 票机硬件 | CSN / EMD807 79.5mm 热敏机,已在现场;**串口与 USB 都要支持**(沿用现有双通道) |
+| 3 | 运营形态 | 按**无人岗亭**设计,失败处理与自助兜底是一等公民 |
+| 4 | 入口触发 | 按钮盒;**v1 用模拟触发**,不接实物 |
+| 5 | 出口硬件 | 立式自助终端(参考照片:集成二维码扫描器、IC 读卡区、HELP 对讲按钮、可选出票口);出口车道另有 LPR 摄像机 + LED 屏 |
+| 6 | 支付渠道 | **微信支付先行**,先打通测试环节;渠道侧保留抽象以便后续接 KHQR/银行(对应 ADR 提案 0001,未定稿) |
+| 7 | 缴费凭证 | 第一版**不打**纸质凭证,出口不装打印机;数字票页面为唯一凭证 |
+| 8 | 入口打印失败 | 自动重试 2 次 → 仍失败**照常开闸** + 记 `print_failed` 事件 + 出口按车牌兜底 |
+
+## 4. 核心流程设计(目标态)
+
+**入场**:按钮盒触发 → `POST /ticket-machine/button` → 校验通道/绑定设备 → `HandlePassage` 建会话+数字票 → 打印小票(重试策略见决策 8)→ MQTT 开闸。
+
+**出口**:车主把小票对准自助终端扫码器 → 终端(后端服务页面)按 ticket_no 解析数字票 → 计费 → **创建订单**(锁定金额)→ 生成订单二维码(微信 Native `code_url`)渲染上屏 → 车主扫码支付 → 微信回调(幂等)→ 订单已支付、数字票转 paid → 自动开闸 → 屏显成功;超时未支付回到初始屏。
+
+## 5. 领域术语
+
+见 `CONTEXT.md`(票机、小票、数字票、扫码、订单、订单二维码、支付渠道、支付入口、自助终端、按钮盒、无人岗亭)。**后续讨论与代码命名以此为准。**
+
+## 6. 待定决策(重启时先答)
+
+| # | 决策点 | 选项与推荐 |
+|---|--------|-----------|
+| 1 | 自助终端内部形态 | (a) 标准外设(USB HID 扫描器 + HDMI 屏)自装浏览器 kiosk ✅推荐;(b) 厂商 SDK/协议对接。需确认:设备是否已选型、内部系统、出票口是否为打印机、"Tap the card" IC 读卡 v1 不做是否成立 |
+| 2 | 微信商户资料 | 直连/服务商?`mchid`、APIv3 密钥/证书、AppID 有无?收款币种(计费 USD/KHR vs 微信 CNY 的跨境问题)?沙箱 or 小额实付 |
+| 3 | 道闸控制权 | (a) 归我们系统(MQTT 道闸底座)✅推荐;(b) 厂商终端驱动、我们调它的接口 |
+| 4 | 丢票兜底范围 | 推荐 v1 做"按车牌找回会话"(出口已有 LPR 硬件);HELP 对讲 v1 只留物理按键不接系统 |
+| 5 | 模拟触发落点 | 推荐调试接口 + 前端"模拟取票"按钮都要;真实按钮盒通信方式(以太网直连 POST / 边缘盒子 IO)待选型 |
+| 6 | ADR-0001 | 订单实体 + 支付渠道接口 + 微信先行。提案状态:**待批准**,批准后写入 `docs/adr/0001-payment-channel-abstraction.md` |
+
+## 7. 分期实施建议
+
+- **M1 入场小票闭环**(工作量小,收益立现):模拟触发入口、打印机管理页(状态/测试页/补打)、打印失败重试 + incident 落库、票机-通道绑定展示。
+- **M2 出口自助支付**(本期核心):自助终端 kiosk 页面、订单模型与状态机(pending→paid→expired,幂等)、微信 Native 下单 + 回调 + 对账、回调自动开闸、车牌找回兜底、超时回初始屏。
+- **M3 增强**:真实按钮盒接入、HELP 对讲、缴费凭证打印(硬件已预留出票口)、IC 卡读卡、多支付渠道(KHQR/银行直连)。
+
+## 8. 风险与依赖
+
+1. **微信跨境收款**:境外停车场用微信收款需跨境商户资质,币种与汇率结算规则要先和渠道方确认——这是 M2 的最大外部依赖。
+2. **自助终端选型**:若采购整机带厂商封闭系统,软件主动权旁落;选型时坚持标准外设(USB HID + HDMI)。
+3. **回调可靠性**:支付回调可能丢失/迟到 → 必须幂等 + 主动查单兜底 + 对账;"到账即开闸"的体验依赖回调时延。
+4. **金额锁定**:订单创建时刻锁定费用,与"缴费后免费离场时限"的交互要定义清楚(超时后补差 or 重新计费)。
+5. **无人值守故障面**:缺纸/离线的远程可观测性(仪表盘状态 + incident)在 M1 就要有,不能等 M3。
+
+## 9. 相关文档
+
+- `CONTEXT.md` —— 领域术语表
+- `docs/superpowers/specs/2026-07-30-ticket-printer-design.md` —— 票机打印原始设计
+- `docs/superpowers/plans/2026-07-31-usb-ticket-photo-layout-plan.md` —— USB 票面排版
+- `doc/中央缴费机对接.md` —— POF 协议(另一类设备)
+- `doc/设备接入层设计.md`、`doc/收费流程.md`、`doc/进出场流程详解.md`

+ 115 - 0
doc/部署盒子方案.md

@@ -0,0 +1,115 @@
+# 部署盒子方案(现场主机选型)
+
+> 状态:**选型定稿,待采购**(2026-09-09 经 grill 访谈收口)
+> 背景:管理系统部署有两种情况——① 直接部署在用户的 Windows 电脑;② 提供预装系统的盒子设备现场部署。本方案针对 ② 的设备选型,同时给出两种情况的差异清单。
+> 术语:见根目录 `CONTEXT.md`
+
+## 1. 已定决策
+
+| # | 决策点 | 结论 |
+|---|--------|------|
+| 1 | 功能边界 | 盒子为"停车场现场主机",一次到位:出口扫码支付链路 + FFmpeg 转码 + 票机打印(CSN DLL)+ 操作员工作台桌面,管理端所有内容都部署上去 |
+| 2 | 操作系统 | **Windows 锁定**——票机 CSN USB DLL(`CsnPrinterLibs.dll`)与 winspool 打印为 Windows 专用;核心链路(纯 Go + 无 CGO SQLite + 内嵌 MQTT)本就跨平台,选 Windows 是零移植成本 |
+| 3 | 部署环境 | 岗亭室内(有空调、有人);因需外接操作员显示器跑管理端页面 |
+| 4 | 试点节奏 | 首批 1~2 台自己上门装,跑通后做整盘镜像批量复制 |
+| 5 | 远程运维 | **ToDesk 预装**(开机自启、免确认),用户侧与内部远程均走它 |
+| 6 | 预算口径 | 单台 **¥2000~2500**(品牌机含税含质保);首批 2 台 ¥4000~5000(不含操作员显示器 ¥400~600、出口屏 ¥800~1500) |
+
+## 2. 选型清单
+
+**机型**:Windows 无风扇工控机(岗亭室内版,不需要宽温)
+
+| 项 | 规格 | 说明 |
+|---|---|---|
+| CPU | Intel N100(四核) | Go 后端 + FFmpeg 多路转码 + 操作员桌面余量充足 |
+| 内存 | 16G DDR4 | 8G 够用,16G 留桌面余量 |
+| 硬盘 | 512G NVMe SSD | 系统 + 数据库 + 转码缓存 |
+| 网口 | **双千兆** | 网口1 接停车场设备网段(相机/读卡器/出口屏);网口2 接路由器外网(微信查单出站 + ToDesk)——设备网与外网隔离 |
+| 显示 | HDMI ×2 | 操作员显示器(管理端工作台)+ 备用 |
+| 串口 | COM ×1~2(RS232/485) | 串口 UHF 读卡器、串口票机预留 |
+| USB | ≥4 | CSN 票机、扫码枪、维护 U 盘 |
+| 散热/供电 | 无风扇 + 来电自启(BIOS:AC Power Loss → Power On) | 7×24 不吸尘,断电无人值守恢复 |
+
+**价格带**:白牌方案商整机 ¥1500~2200;品牌(研华、华北工控等)¥2800~3800 含三年质保。**首批建议买品牌**。
+
+### 2.1 网关与网络拓扑(双网口角色)
+
+盒子兼任**设备网段的网关与唯一出入口**:
+
+```
+互联网/物业路由器 ──> 盒子[网口2: 外网口]     (微信查单出站、ToDesk 远程)
+                        盒子[网口1: 设备网口] 192.168.60.1 ──> 设备交换机
+                                                                ├─ LPR 相机
+                                                                ├─ 出口屏(安卓)
+                                                                ├─ UHF 读卡器(TCP)
+                                                                ├─ 道闸(MQTT)
+                                                                └─ 中央缴费机
+```
+
+- **设备网段默认与外网隔离**:设备之间本地互通即可,无出网需求;出网的只有盒子自身(支付查单、远程运维)。现场拓扑收敛为"光猫 → 盒子 → 所有设备",省一台路由器。
+- **NAT 转发默认关闭**,需要设备临时出网(如相机固件升级)时开启,用完关闭:
+  ```powershell
+  New-NetIPAddress -InterfaceAlias "设备网" -IPAddress 192.168.60.1 -PrefixLength 24
+  # 临时出网(默认不执行):
+  Set-NetIPInterface -InterfaceAlias "设备网","外网" -Forwarding Enabled
+  New-NetNat -Name ParkingNat -InternalIPInterfaceAddressPrefix 192.168.60.0/24
+  ```
+- 设备网段 **IP 静态分配**(Windows 桌面版无 DHCP 服务角色,不为此装第三方 DHCP),IP 分配表见现场部署 Checklist。
+- **单点风险低**:盒子宕机时设备网段二层仍然互通,出口屏 ↔ 后端的支付链路不受影响,仅损失远程运维与查单出网。
+- 上行接入方式:盒子外网口接**物业路由器/光猫的 LAN 口**(作为上网客户端);不做 PPPoE 拨号,不接管物业宽带。
+
+**UPS**:默认不配。支付到账确认是查单制(`docs/adr/0002`),断电重启后订单仍在、定时任务自动补确认,不丢单;现场断电频繁再补 ¥200~400 小 UPS。
+
+## 3. 系统与镜像
+
+- **系统**:Windows 11 IoT Enterprise LTSC(无商店、无强制功能更新)。停车现场禁止系统半夜自动重启更新;用专业版替代时必须手动禁用自动更新。
+- **镜像制作**(支持"先 a 后 b"的批量复制):
+  1. 系统 + 驱动 + FFmpeg(固定目录,config.yaml 指向)
+  2. 后端 `smart-parking-backend.exe` 注册为 **NSSM 服务**(崩溃自动拉起、开机自启)
+  3. 前端 `npm run build` 静态产物由后端托管(复用 `scripts/publish-release.ps1` 打包产物)
+  4. ToDesk 开机自启;SQLite 备份计划任务(每 30 分钟拷 db/wal/shm 到 `D:\backup`,保留 7 天)
+  5. 整盘镜像存 U 盘(傲梅/DiskGenius),现场换机 ≈ 30 分钟恢复
+- **每台唯一化**(现场部署时改):设备编码、`config.yaml` 通道/设备绑定、出口屏页面地址、ToDesk 设备授权。
+
+## 4. 两种部署情况差异清单
+
+| 项 | 情况 1:用户 Windows PC | 情况 2:部署盒子 |
+|---|---|---|
+| 开机与性能 | 依赖用户电脑与开机习惯 | 7×24 常开,BIOS 来电自启 |
+| 环境 | 杀毒软件可能拦截端口/进程,需放行清单 | 镜像固定环境,无此问题 |
+| 数据备份 | 责任在用户(交付清单写明) | 计划任务自动备份 `D:\backup` |
+| 系统更新 | 必须手动禁用自动更新 | LTSC 默认无强制更新 |
+| 网络隔离 | 单网段 | 双网口:设备网 / 外网隔离 |
+| 票机/打印 | 即插即用(Windows) | 即插即用(Windows) |
+| 交付 | 手工安装 + 验收单 | 镜像恢复 + 每台唯一化 + 验收单 |
+
+## 5. 现场部署 Checklist(每台)
+
+- [ ] BIOS:来电自启(AC Power Loss → Power On)
+- [ ] 双网口接线:设备网段 / 外网网段分离,分别记录 IP
+- [ ] 设备网关角色配置:盒子设备网口静态 IP(如 192.168.60.1/24,见 2.1 节),NAT 转发保持关闭
+- [ ] **设备网段 IP 分配表**(静态分配,逐台登记):
+  | 设备 | IP | 备注 |
+  |---|---|---|
+  | 盒子设备网口 | 192.168.60.1 | 网关 |
+  | LPR 出口相机 | 192.168.60.11 | |
+  | 出口屏(安卓) | 192.168.60.21 | |
+  | UHF 读卡器 | 192.168.60.31 | TCP 模式 |
+  | 道闸控制器 | 192.168.60.41 | MQTT |
+- [ ] 静态 IP 或路由器 DHCP 绑定(后端、出口屏地址都要固定)
+- [ ] 后端服务(NSSM)启动并自启验证;`/health` 200
+- [ ] FFmpeg 路径与 `conversion-mode` 核对;`gate-simulator: false`、`debug-routes: false`(生产必查)
+- [ ] 通道/读卡器/道闸/出口屏设备绑定与 `config.yaml` 一致
+- [ ] ToDesk 自启 + 免确认;记录设备 ID/密码到运维台账
+- [ ] SQLite 备份计划任务验证(手动触发一次,检查 `D:\backup`)
+- [ ] 票机 USB/串口连通(上票机阶段);出口屏浏览器自启指向 kiosk 页面
+- [ ] 出口屏联调开关关闭确认:`exit-kiosk` 的 mock 渠道切回 `wechat`
+- [ ] 微信支付商户配置(`wechat-pay` 段)填写并小额真实交易验证
+- [ ] 整盘镜像更新归档(部署完成后的最终态存 U 盘)
+
+## 6. 相关文档
+
+- `doc/扫码支付出口设计.md` —— 出口自助支付设计
+- `doc/票机对接规划.md` —— 票机链路(CSN DLL 为 Windows 依赖的来源)
+- `docs/adr/0001`、`docs/adr/0002` —— 支付渠道抽象与查单为主
+- `scripts/publish-release.ps1` —— 发布打包脚本(镜像内容来源)

+ 209 - 0
doc/项目问题分析与修复方案.md

@@ -0,0 +1,209 @@
+# 智慧停车项目问题分析与修复方案
+
+> 分析日期:2026-09-07  
+> 分析范围:`lc_garage` 实际源码、配置、前端构建、项目文档与 Git 历史  
+> 结论性质:代码与配置静态审查 + 可执行构建验证,不替代真实闸机、打印机、摄像头和支付终端的现场验收。
+
+## 1. 结论摘要
+
+项目已经完成从旧的 GIN-VUE-ADMIN 服务端形态向 Wails 单进程桌面应用的主要迁移,并具备停车场配置、停车会话、数字票、支付流水、月卡、交接班、报表、打印和异常处置等基础模块。当前代码也已经出现 `internal/modules` 领域模块化结构,说明迁移正在持续进行。
+
+距离可稳定交付仍有三类问题:
+
+1. **生产安全边界不够明确**:上传、Swagger、监控和中央缴费机入口的公开性需要逐项确认;默认账号、敏感配置和调试能力仍应纳入发布安全策略。
+2. **设备和资金业务闭环不足**:真实支付、道闸反馈、打印失败补偿、摄像头识别、LED 余位屏、备份恢复和对账仍有明显缺口。
+3. **文档和工程入口存在漂移**:根目录 `AGENTS.md`、旧版 `PROJECT.md`、`lc_garage/README.md`、`doc/PROJECT.md` 和 `doc/功能缺失.md` 的架构、数据库和完成状态描述不完全一致。
+
+## 2. 已验证现状
+
+### 2.1 结构与入口
+
+- 实际应用目录为 `lc_garage/`,根目录是项目文档工作区。
+- Wails 入口为 `lc_garage/main.go`,通过 `//go:embed all:frontend/dist` 嵌入前端,并将 `/api/*` 反向代理到 Gin `:8888`。
+- 后端实际代码位于 `lc_garage/internal/`,不存在文档中曾描述的 `lc_garage/server/` 主目录。
+- 前端同时存在 `frontend/` 和 `web/` 两套目录;Wails 配置明确使用 `frontend`,`web` 更像历史遗留或兼容目录。
+- 新模块已实际存在:`parking-session`、`digital-ticket`、`payment`、`monthly`、`shift`、`report`、`printer`、`incident`、`central-payment`、`deviceprovisioning` 等。
+
+### 2.2 验证结果
+
+- `frontend`: `npm run build` 成功,Vite 生成生产包;构建有 Sass 弃用提示及两个超过 500 KB 的 chunk 警告。
+- `go test ./...`: 当前环境失败在 Go 构建缓存目录 `C:\Users\longc\AppData\Local\go-build` 的访问权限,输出为 `Access is denied`,不能视为代码测试失败。
+- 项目进度文档声称的模块级测试命令应使用项目可写的 `GOCACHE`,并在 CI 中固定执行,避免环境权限造成误判。
+
+## 3. 问题清单
+
+### P0:上线前必须处理
+
+#### P0-1 公开路由和敏感数据访问边界需要收紧
+
+证据:`internal/initialize/router.go:70-92`。
+
+- `Router.StaticFS(global.GVA_CONFIG.Local.StorePath, ...)` 直接提供本地上传目录;车牌抓拍、头像等文件可通过可预测路径访问。
+- Swagger 在 `:swagger/*any` 直接注册,未看到生产环境开关。
+- `dashboardRouter.InitDashboardRouter(PublicGroup)` 将监控接口放在公开组;CPU、内存等运行信息不应默认匿名暴露。
+- `PublicGroup.POST("/device-images/upload", deviceimage.Upload)` 公开注册,上传接口应明确鉴权、文件类型、大小、路径和访问权限。
+- 中央缴费机公开接口是设备对接需要,但必须只允许设备令牌、来源网络或签名请求,不能按普通公开 API 处理。
+
+修复方案:
+
+1. 将上传改为受 JWT/Casbin 保护的下载接口,或使用短时签名 URL;禁止目录遍历和原始文件名落盘。
+2. 增加 `system.expose-swagger`、`system.expose-monitor` 等显式配置,生产默认关闭。
+3. 设备图片上传和中央缴费机接口分别使用设备认证中间件;记录设备 ID、请求摘要和失败原因。
+4. 为公开路由增加路由安全测试:匿名访问必须只允许健康检查和明确的设备协议接口。
+
+#### P0-2 默认凭据和密钥管理不适合生产
+
+证据:`internal/initialize/seed.go` 包含固定 bcrypt 密码和 `README.md:32-35` 公开写出 `admin/123456`;`config.yaml` 还保留对象存储示例密钥字段。
+
+修复方案:
+
+- 首次启动进入初始化向导,强制设置管理员密码;发布包不再预置可直接登录的通用密码。
+- 将 JWT secret、设备 token、摄像头密码和第三方支付密钥移出仓库,使用系统凭据存储或受保护配置文件。
+- 对历史数据库提供管理员密码轮换迁移;启动时检测默认密码并阻止生产模式继续运行。
+
+#### P0-3 SQLite 并发写、备份和恢复策略缺失
+
+证据:`internal/initialize/gorm_sqlite.go:47-53` 只设置连接池,没有设置 WAL、`busy_timeout` 或备份策略;`config.yaml:185-199` 允许 `max-open-conns: 100`。
+
+风险:设备事件、支付、操作日志和定时任务并发写入时可能出现 `database is locked`;进程或磁盘损坏会导致本地运营数据丢失。
+
+修复方案:
+
+1. SQLite 初始化后执行 `PRAGMA journal_mode=WAL`、`PRAGMA busy_timeout=5000`、合理的 `synchronous` 设置,并将写并发控制在可验证范围。
+2. 增加在线一致性备份或安全快照,备份文件校验、保留周期和恢复演练。
+3. 对支付、停车会话、月卡和交接班增加恢复后校验任务,发现跨表不一致时生成异常事件。
+4. 增加并发写压力测试和断电恢复测试,测试不依赖用户目录下不可写的 Go 缓存。
+
+#### P0-4 服务启动失败处理不可靠
+
+证据:`internal/core/server.go:34-40` 调用 `s.ListenAndServe().Error()`;`internal/initialize/gorm.go:77-84` 在迁移失败时使用 `os.Exit(0)`。
+
+风险:监听失败可能触发 nil error 解引用;数据库迁移失败却以成功码退出,Windows 启动器和发布脚本无法正确识别失败。
+
+修复方案:
+
+- 保存 `err := s.ListenAndServe()`,对 `http.ErrServerClosed` 单独处理,其余错误记录并以失败状态退出或通知 Wails 主进程。
+- 将 `os.Exit(0)` 改为错误返回/受控退出码 `1`,并在启动错误日志中写明迁移步骤和恢复建议。
+- 启动前执行端口、数据库路径和配置值校验,失败时不要启动前端窗口。
+
+#### P0-5 真实资金交易尚未形成可审计闭环
+
+证据:`doc/功能缺失.md` 和 `internal/modules/payment` 的现状说明;当前 `payment_record` 主要记录本地业务结果,POS、扫码和中央缴费机仍需真实设备或网关确认。
+
+修复方案:
+
+- 建立支付订单状态机:待支付、处理中、成功、失败、不确定、已退款、已撤销。
+- 为支付记录增加渠道订单号、幂等键、回调时间、原始渠道状态和退款/冲正关联。
+- 外部支付只能在可信回调、主动查询或设备签名结果确认后标记成功;重复回调必须幂等。
+- 交接班和收入报表按支付入口、支付方式、操作员、班次、退款和差异统一对账。
+
+### P1:应在近期完成
+
+#### P1-1 道闸和读卡器状态模型仍然混杂
+
+现状记录见 `doc/功能缺失.md:117-139`。读卡器、闸机控制器、连接管理器和运行时状态存在多套来源;串口/TCP 的 ACK、分片、超时、重连和落杆反馈还需要现场验证。
+
+修复方案:拆分设备类型与通道拓扑,统一 `DeviceRuntime` 状态源;所有开闸/关闸命令落 `device_command_log`,校验 ACK、超时和重试,增加地感、防砸、落杆确认和人工兜底测试。
+
+#### P1-2 模拟道闸必须具备不可误用的运行保护
+
+`config.yaml:210` 当前为 `gate-simulator: false`,默认值正确;但仍应在启动时显示运行模式,并在生产构建或生产配置中拒绝开启模拟模式。模拟模式下的“开闸成功”不能被当作真实硬件确认。
+
+修复方案:增加 `environment=production` 与 `gate-simulator` 互斥校验;模拟模式所有指令标记 `simulated=true`,前端和审计记录明确显示。
+
+#### P1-3 打印失败没有持久化补偿任务
+
+现状:票机打印仍以同步硬件调用为主,失败重试、重打、作废和异常处置尚未形成完整任务模型。
+
+修复方案:新增打印任务表和状态机,先创建任务再执行硬件动作;记录短写、串口错误、设备离线和重试次数,允许受权限控制的重打/作废,并与数字票和停车会话建立补偿规则。
+
+#### P1-4 月卡、交接班和收入报表的财务口径仍不完整
+
+现状:月卡退款缺少完整退款流水和审批;自然月/季/年、并发办理、人工白名单保护需要继续完善;交接班和报表还需要覆盖所有支付方式、退款和差异。
+
+修复方案:
+
+- 月卡办理、续费、退款、白名单变更和支付流水使用同一事务及幂等键。
+- 用日历月/季度/年度计算有效期,明确过期、冻结、转车和退款规则。
+- 交接班增加唯一 active 约束、收款前当班校验、管理者复核和锁定机制。
+- 报表提供明细追溯、导出、打印和支付记录对账。
+
+#### P1-5 数字票过期任务和跨表对账不足
+
+`doc/功能缺失.md` 指出 `ExpireAt` 自动执行和状态不一致检测仍不完整。应明确待支付票、已支付未离场票的过期规则,并由定时任务执行状态迁移和异常告警。
+
+### P2:产品能力和工程质量提升
+
+- 摄像头 RTSP/SDK、OCR 置信度、防重复识别和通道绑定。
+- LED 余位屏协议、推送、离线重试和恢复同步。
+- 远程设备诊断、设备告警中心和现场运维审计。
+- 历史会话、日志和抓拍图片归档清理;上传文件容量配额。
+- 短信/邮件验证从模拟实现升级为真实供应商适配,并增加频率限制。
+- 通道事件从内存队列升级为持久化队列,支持重启恢复。
+- 前端 bundle 拆分,处理 ECharts 约 1 MB chunk;清理 Sass `@import`/legacy API 弃用警告。
+- 增加 API、JWT、Casbin、操作日志脱敏、登录登出、越权和配置校验测试。
+
+## 4. 文档与结构问题
+
+### 4.1 架构描述互相冲突
+
+- 根目录 `PROJECT.md` 仍描述 `server/` 独立后端、MySQL 和旧 Electron 结构,与实际 Wails + SQLite 代码不符。
+- `AGENTS.md`、`lc_garage/README.md` 和 `doc/PROJECT.md` 对结构的描述不一致:前者较旧,后两者基本反映当前布局。
+- `doc/功能缺失.md` 中部分安全问题已在提交 `8cca42f` 修复,例如注册角色层级校验、操作日志脱敏、模拟闸机告警,但文档仍把它们写成当前漏洞。
+- `doc/项目进度.md` 更新日期早于最近代码提交,完成状态与后续新增的中央缴费机、设备接入模块没有完全同步。
+
+修复方案:
+
+1. 以 `lc_garage/doc/PROJECT.md` 作为唯一项目说明,以 `lc_garage/README.md` 作为快速开始文档。
+2. 将根目录 `PROJECT.md` 标记为历史文档或删除,避免新成员误用 MySQL/server 启动方式。
+3. 给 `doc/功能缺失.md` 增加“已修复/仍存在”分栏、代码版本和验证命令;删除已完成项的当前时态描述。
+4. 每次领域功能合并时同步更新 `项目进度.md`、流程文档和测试记录,并在 CI 检查文档中的路径是否存在。
+
+### 4.2 双前端目录需要明确归属
+
+Wails 使用 `frontend`,但 `web` 仍保留完整 Vue 源码和 Vite 配置。双目录会导致修复只落在一处、构建结果与开发结果不一致。
+
+修复方案:短期在 `README` 和 `wails.json` 中明确 `frontend` 为唯一构建源;中期归档或删除 `web`,若必须兼容则用脚本从唯一源生成,不允许手工双写。
+
+## 5. 前端已确认缺陷
+
+### 5.1 报表异常处理引用未定义变量
+
+`frontend/src/view/report/enter.vue:222-224` 的异常处理调用 `t.value.getListFail`,文件中没有对应的 `t` 定义。接口失败时会再次触发 `ReferenceError`,原始错误信息被覆盖。
+
+修复方案:改为已有的 `$t`/语言包访问方式,或定义统一的 `useI18n` 实例;补充请求失败分支测试。
+
+### 5.2 登出失败时本地凭据清理策略不统一
+
+`frontend/src/pinia/modules/user.js:91-98` 只有黑名单接口返回成功才清理 token;而 `frontend/src/utils/request.js:147-162` 的 401 处理依赖用户确认。
+
+修复方案:登出无论服务端结果如何都清理本地 token;401 统一立即清理并跳转登录,弹窗只用于提示,不应让失效凭据继续留存。
+
+## 6. 推荐实施顺序
+
+| 顺序 | 优先级 | 工作包 | 完成标准 |
+| ---: | :---: | --- | --- |
+| 1 | P0 | 路由、上传、Swagger、监控和设备接口安全边界 | 匿名访问测试通过;上传不可猜测读取;生产 Swagger/监控默认关闭 |
+| 2 | P0 | 默认凭据、密钥和配置校验 | 首次启动强制改密;仓库不含可用生产密钥;非法端口/路径拒绝启动 |
+| 3 | P0 | SQLite WAL、锁等待、备份恢复 | 并发写压测通过;备份可校验;恢复演练有记录 |
+| 4 | P0 | 启动失败和进程退出语义 | 迁移/监听失败返回非零并写清晰诊断,不出现 nil error panic |
+| 5 | P0 | 支付订单、回调幂等和财务对账 | 成功/失败/不确定/退款/冲正可追溯,重复通知不重复收费 |
+| 6 | P1 | 道闸、读卡器、打印机现场闭环 | ACK、超时、重试、离线、落杆和打印补偿均有集成测试 |
+| 7 | P1 | 月卡、交接班、报表和数字票过期 | 事务、并发、退款、班次和跨表对账规则明确并覆盖测试 |
+| 8 | P1 | 摄像头、设备告警和 LED | 设备状态、事件、恢复和余位推送可在现场验收 |
+| 9 | P2 | 文档收敛、双前端归档和前端性能 | 唯一开发入口,文档路径与状态可由 CI 校验 |
+
+## 7. 验收与持续检查
+
+发布前至少执行:
+
+```powershell
+cd D:\lq\Smart Parking\lc_garage
+$env:GOCACHE = "D:\lq\Smart Parking\lc_garage\build\gocache-ci"
+go test ./... -count=1
+
+cd frontend
+npm run build
+```
+
+另外需要单独保留以下验收记录:真实闸机开/关及断网、真实打印机短写和重打、摄像头重复识别、POS/中央缴费机回调幂等、SQLite 备份恢复、管理员和操作员越权测试。只有代码、权限、自动化测试和现场流程同时通过,相关功能才应标记为“已完成”。

+ 18 - 0
docs/adr/0001-payment-channel-abstraction.md

@@ -0,0 +1,18 @@
+---
+status: accepted
+date: 2026-09-09
+---
+
+# 0001 - 支付渠道抽象 + 微信支付先行
+
+出口自助支付需要在线收款,可选路径有:微信直连、支付宝直连、聚合支付商。我们决定在订单实体之下引入**支付渠道接口**(下单、查单、通知挂点),第一个实现为微信 Native 扫码支付,测试阶段以小额真实交易打通;支付宝后续作为第二个实现,不在本期。
+
+## Considered Options
+
+- 聚合商一次接入微信+支付宝:少一套代码,但引入第三方依赖与分账成本,且现场已有微信测试条件。
+- 直连两套并行开发:工作量翻倍,支付宝场景(境外车主)暂未验证需求。
+
+## Consequences
+
+- 支付流水按渠道拆支付方式(`wechat` / `alipay`),日对账直接可用;渠道细节(预支付单号、查单结果)只存在订单上,不污染流水语义。
+- 渠道接口必须把"查单"作为一等能力(原因见 ADR-0002),不能只做下单+回调。

+ 18 - 0
docs/adr/0002-query-first-payment-confirmation.md

@@ -0,0 +1,18 @@
+---
+status: accepted
+date: 2026-09-09
+---
+
+# 0002 - 到账确认以主动查单为主(无公网回调)
+
+系统部署在现场电脑主机(内网),没有公网 HTTPS 入口,微信支付的支付通知(回调)无法直达。我们决定:**确认到账以主动查单为主**——出口屏页面每 2 秒轮询订单状态触发即时查单,服务端定时任务对未关闭订单兜底查单;回调端点暂不建设,渠道接口仅保留通知挂点,将来有公网入口时再启用为加速器。
+
+## Considered Options
+
+- 内网穿透/公网中转暴露回调端点:引入额外的公网组件与安全面,现场部署条件不可控。
+- 回调为主、查单兜底:在本部署形态下回调根本不可达,不成立。
+
+## Consequences
+
+- 到账确认延迟 = 查单间隔(秒级),可接受:车主就停在屏幕前,页面轮询驱动体验。
+- 查单是放行的权威依据,必须幂等:同一订单多次查得"已支付"只结算、开闸一次。