# 异常处置闭环设计 > 更新日期:2026-08-12 > 状态说明:~~本文为设计文档~~ **已于 2026-08-12 按本文实现完毕**(后端模块、自动埋点、指令流水、权限种子、前端页面、自动化测试均落地,`go test ./internal/service/... ./internal/modules/... ./internal/initialize` 与前端 `npm run build` 通过)。文中标注(预留)的项为后续扩展点,不在当前范围;标注(建议包含)的设备指令流水已实现。 > 代码位置:设计基于 2026-08-12 当前代码(`internal/service/parking/passage.go`、`internal/service/parking/gate.go`、`internal/modules/` 等)。 ## 1. 现状与问题 ### 1.1 异常只存在于 HTTP 响应 - 统一通行 `HandlePassage` 中,业务落库成功但开闸失败时返回 `result + err`(`passage.go:172-176` 入场、`passage.go:231-235` 出场),`GateStatus="failed"`、`BusinessCompleted=true`。该"业务已完成、设备动作失败"的半完成状态**不落库**,重启后无从追踪。 - 票机打印失败(`printer` 模块)同样只写在响应里(`PrintStatus="failed"`),无持久化任务、重试、重打和作废机制(重打机制属 P0 打印任务持久化,本文不覆盖,但打印失败事件可先挂入异常表,见 7.4)。 ### 1.2 无任何异常实体 `vehicle_record`、`payment_record`、`shift_record`、`printer`、`camera`、`uhf_reader` 均无异常标记、重试状态或补偿状态字段。`frontend/src/view/report/sheet.vue` 是纯静态占位页(无数据绑定、无 API 调用),菜单 27"异常报表"已存在并授权 618/888/9527(`internal/initialize/seed.go:135`)。 ### 1.3 设备状态三套并存且不一致 | 状态来源 | 位置 | 问题 | | --- | --- | --- | | DB `uhf_reader.status` | 读协程 `ReadData` 出错置 offline、重连成功置 online | 只覆盖 UHF 读卡器,与内存态脱节 | | 内存 `DeviceManager` / `ConnManager` | `GetGateRuntimeStatus`(`gate.go:104-114`) | 仪表盘设备状态据此统计,重启即失效 | | `LastOnlineTime` | `internal/dao/uhf_reader.go` | 死字段,全项目无人写入 | 模拟道闸(`config.yaml` 的 `gate-simulator: true`)下所有设备 `IsGateConnected` 恒真,设备真实掉线无法被现有链路感知。 ### 1.4 人工抬杆无业务审计 `POST /parking/gate/open|close`(`internal/api/v1/parking/gate.go`)仅调用 `OpenGateByDeviceCode`,不关联会话、不记录操作原因。OperationRecord 中间件只记录原始 HTTP 请求,无法表达"为什么抬杆、抬杆对应哪辆车/哪个会话"。 ### 1.5 可复用的现成机制 | 机制 | 位置 | 复用方式 | | --- | --- | --- | | 数字票事件日志 | `digital_ticket.event_log`(JSON 事件数组,状态机每次转换追加) | 异常表采用同样的 `event_log` 字段记录状态流转时间线 | | 操作审计中间件 | `internal/middleware/operation.go`,PrivateGroup 全量挂载 | API 层的处置请求自动留痕,异常表无需重复记录请求体 | | 状态机 CAS 更新 | `digital-ticket` 模块 `UpdateStateIfCurrentTx` 模式 | 异常状态流转按当前状态条件更新,防并发覆盖 | | 模块化结构 | `internal/modules/payment|shift`(api.go + service/ + repository/) | 新模块完全仿照此结构 | | 权限幂等种子 | `internal/initialize/operation_permission_seed.go`(sys_apis + casbin_rule) | 新增 API 权限仿照 `EnsureOperationPermissions` 实现 | | 前端蓝本 | `view/report/paymentRecord.vue`(统计行 + 筛选 + 表格 + 分页) | 异常处置页面直接复用该结构 | ## 2. 总体设计 ### 2.1 核心概念:异常事件即工单 一次"需要被记录并可能被人工介入"的事件,事件本身与处置过程合一,落在同一张表: - **自动产生**:业务/设备链路失败时由埋点自动落库(如开闸失败、设备离线)。 - **人工产生**:操作员在页面主动上报(如无牌车人工处理、丢票)。 - **可处置**:记录经历 `pending → processing → resolved → closed` 状态流转,处置信息(方式、金额、备注、处理人)随记录保存。 - **可审计**:`event_log` 保存每次流转的事件时间线;API 请求由 OperationRecord 中间件留痕;人工放行与资金相关处置有独立权限。 ### 2.2 设计原则 1. **best-effort 埋点**:自动埋点失败只记日志,绝不影响业务主流程的返回值与事务结果。 2. **状态机 + CAS**:状态流转使用条件更新(仿数字票),并发时以行级条件保证只有一次生效。 3. **增量接入**:埋点是在既有链路上的纯增量调用,不改变现有业务返回值和事务边界。 4. **权限分级**:查看/上报面向全部角色;处置(状态流转)面向管理员;资金类处置仅最高管理员。 5. **事件与工单不拆分**:v1 不建"事件表 + 工单表"两套,避免双写一致性问题;同一异常类型的去重由业务规则控制(见 5.4)。 ### 2.3 模块位置与边界 新增模块 `internal/modules/incident/`,表模型 `internal/dao/incident.go`: ```text internal/modules/incident/ ├─ api.go # HTTP Handler + SetupIncidentRouter ├─ service/ │ ├─ service.go # 领域校验、状态机、埋点入口、统计 │ └─ service_test.go └─ repository/ └─ repo.go # 分页查询、条件更新、去重查询 ``` 模块边界:`incident` 只记录异常事实与处置过程,**不修改**会话、数字票、支付流水和道闸状态;需要联动修正业务数据时(如强制免费后补结算),由调用方在业务事务内先完成修正、再调用埋点记录,异常表不参与业务事务。 ## 3. 数据模型 ### 3.1 异常事件表 `incident_record` ```go type IncidentRecord struct { global.GVA_MODEL IncidentNo string // 唯一编号,如 INC20260812-0001 Category string // 异常分类(见 3.2 枚举) Source string // 来源:passage/manual/device/payment/printer/system Level string // 等级:info/warning/critical Status string // 状态:pending/processing/resolved/closed VehicleRecordID uint // 关联停车会话(可选) TicketNo string // 关联数字票(可选) PlateNumber string // 车牌(可选) RFIDTag string // RFID(可选) ParkingLotID uint // 停车场(可选) ChannelID uint // 通道(可选) ChannelCode string // 通道编码快照 DeviceCode string // 设备编码(可选) OperatorID uint // 上报人/创建人 Description string // 异常描述(人读) Detail string // JSON 上下文(请求参数、错误信息、设备状态等) HandlerID uint // 处理人 HandledAt *time.Time // 处理时间 HandleType string // 处置方式(见 3.3) HandleRemark string // 处置备注(必填) ForceFreeAmount float64 // 强制免费金额(资金类处置时记录) EventLog string // JSON 事件时间线(同 digital_ticket.event_log) } ``` 字段说明: - `incident_no` 建立唯一索引,生成规则 `INC + yyyyMMdd + 当日序号`(SQLite 下单事务内 `count + 1` 生成,冲突时重试,避免自增回绕)。 - `category + device_code + status` 建立联合索引,供去重查询和页面筛选。 - `Detail` 存 JSON 字符串(如失败请求、错误原文),大小受控(截断至 1000 字符),不存敏感字段(密码、支付卡号)。 - 资金相关:`force_free_amount` 仅 `force_free` 处置时写入;其余处置方式必须为 0。 ### 3.2 异常分类枚举 `Category` | 值 | 中文 | 默认等级 | v1 埋点源 | | --- | --- | --- | --- | | `gate_failed` | 开闸失败 | critical | 统一通行(7.1) | | `duplicate_entry` | 重复入场 | warning | 统一通行(7.1) | | `blacklist` | 黑名单拦截 | warning | 统一通行(7.1) | | `lot_full` | 满位拒绝 | info | 统一通行(7.1) | | `no_entry_exit` | 无入场记录出场 | critical | 统一通行(7.1) | | `manual_raise` | 人工抬杆/关闸 | warning | 人工抬杆 API(7.2) | | `device_offline` | 设备离线 | warning | UHF 状态转换(7.3) | | `print_failed` | 打印失败 | warning | (预留,打印任务持久化落地后接入) | | `payment_failed` | 支付失败 | warning | (预留,支付订单层落地后接入) | | `payment_uncertain` | 支付状态不确定 | critical | (预留,外部支付回调接入后接入) | ### 3.3 处置方式枚举 `HandleType` | 值 | 中文 | 说明 | | --- | --- | --- | | `manual_gate` | 人工抬杆放行 | 对应 manual_raise 类异常,需备注原因 | | `force_free` | 强制免费 | 资金类,必须填金额,仅最高管理员 | | `reset` | 状态重置/修正 | 修正会话、票或余位数据后关闭 | | `device_repaired` | 设备修复 | 设备恢复后由系统自动或人工确认 | | `reprint` | 重新打印 | 预留,配合打印任务持久化 | | `ignore` | 误报忽略 | 需备注误报原因 | | `other` | 其他 | 需备注 | ### 3.4 指令流水表 `device_command_log`(建议包含,可裁剪) 配合人工抬杆审计与开闸失败追踪,记录每次道闸开/关命令: ```go type DeviceCommandLog struct { global.GVA_MODEL DeviceCode string // 设备编码 DeviceName string // 设备名称快照 Action string // open / close Source string // passage(通行链路)/ manual(人工操作)/ test OperatorID uint // 操作员(manual 时必填) SessionID uint // 关联停车会话(可选) IncidentID uint // 关联异常事件(可选,开闸失败时关联) Result string // success / failed / timeout ErrorMessage string // 失败原因 DurationMs int64 // 耗时 } ``` - 统一埋点在 `OpenGateByDeviceCode` / `CloseGateByDeviceCode`(`gate.go:117-144`)封装处,所有开闸/关闸(通行、人工、测试)都经过此处,无需改动各调用点。 - 开闸失败时:指令流水写 `failed`,同时异常埋点生成 `gate_failed`,二者通过 `incident_id` 关联。 - 裁剪此项不影响异常事件主链路,仅损失"命令级"审计粒度。 ## 4. 状态机 ### 4.1 状态与流转 ```text pending ──> processing ──> resolved ──> closed ↑ │ └──────── reopen ─────────┘ ``` | 流转 | 约束 | | --- | --- | | pending → processing | 记录开始处理,写入 `handler_id` | | pending/processing → resolved | 必须携带 `handle_type` + `handle_remark`;`force_free` 必须带 `force_free_amount ≥ 0`;写入 `handled_at` | | resolved → closed | 关闭工单,可带补充备注 | | closed → pending(reopen) | 重新打开,可带备注 | 禁止其他流转(如 pending → closed 直接关闭、resolved 回退 processing)。所有流转由 `TransitionIncident` 统一入口执行,更新采用条件语句 `UPDATE ... SET status=? WHERE id=? AND status=?`,受影响行数为 0 时返回"状态已变更"错误(CAS,仿数字票 `UpdateStateIfCurrentTx`)。 ### 4.2 处置动作与状态的映射 - 自动埋点创建的异常默认 `pending`。 - `manual_raise` 人工抬杆:创建时即为 `resolved` + `handle_type=manual_gate`(原因即备注),因为动作已完成、仅需留痕;如需复核可人工 reopen。 - `device_offline`:设备重连时,系统将同设备未关闭(pending/processing)的离线记录批量置为 `resolved` + `device_repaired`,仍保留人工确认入口。 - 资金类处置(`force_free`)在服务层校验角色权限(见 6.2)。 ## 5. 服务层设计(`internal/modules/incident/service/`) ### 5.1 方法清单 | 方法 | 用途 | | --- | --- | | `RecordIncident(ctx RecordIncidentRequest)` | 自动埋点入口,best-effort:内部错误只记日志并返回 nil,不抛给业务调用方 | | `CreateIncident(req)` | 人工上报(API 层调用),返回完整记录 | | `ListIncidents(q)` | 分页 + 多条件筛选(状态/分类/等级/来源/车牌/票号/停车场/设备/日期范围/上报人) | | `GetIncident(id)` | 详情(含 event_log 解析后的时间线) | | `TransitionIncident(id, req)` | 状态流转 + 处置信息,CAS 更新 | | `GetIncidentStats()` | 统计:按状态计数(待处理/处理中/已解决/已关闭)、今日新增、资金类待处理数量 | | `ResolveDeviceOffline(deviceCode)` | 设备重连时自动关闭离线异常(7.3 调用) | ### 5.2 `RecordIncident` 的入参 ```go type RecordIncidentRequest struct { Category string Source string Level string // 缺省按分类默认等级 VehicleRecordID uint TicketNo string PlateNumber string RFIDTag string ParkingLotID uint ChannelID uint ChannelCode string DeviceCode string OperatorID uint Description string Detail interface{} // 序列化为 JSON 存入 Detail } ``` ### 5.3 查询 `repository` 采用既有三段式:`db.Count(&total)` → 默认值兜底(page=1、page_size=10)→ `Order("created_at DESC").Offset((page-1)*pageSize).Limit(pageSize).Scan`,返回 `(list, total, err)`;API 层包 `response.PageResult`。列表关联 `sys_users` 取上报人/处理人昵称(仿 shift repository 的 `operator_name`)。 ### 5.4 去重规则 | 分类 | 去重规则 | | --- | --- | | `device_offline` | 同设备存在 pending/processing 的离线记录时不重复生成 | | `gate_failed`/`duplicate_entry` 等通行类 | 同 `device_code + plate_number/rfid + category` 在 5 分钟内不重复生成(防抖复用 `passage.go` 的 3 秒防抖窗口之上再加事件级窗口) | | 其他 | 不做自动去重,人工判断 | ### 5.5 事务边界 - 异常表写入**不参与**业务事务:业务事务提交成功后调用 `RecordIncident`(独立写)。 - 特例:人工抬杆/关闸 API 中,命令执行与异常落库可以共用一次简单事务。命令失败时仍生成记录(见 7.2),此时分类为 `gate_failed`、状态 `pending`,备注写明人工操作失败原因,供复核。 - `TransitionIncident` 单条更新,无需跨表事务(event_log 追加与状态更新在同一 `UPDATE` 内拼 JSON 完成)。 ## 6. HTTP API 与权限 ### 6.1 路由(前缀 `/incident`) 注册于 `internal/initialize/router.go` 的 PrivateGroup(自动挂 JWT + Casbin + OperationRecord): | 方法 | 路径 | 说明 | 角色 | | --- | --- | --- | --- | | GET | `/incident/list` | 分页筛选列表 | 618/888/9527 | | GET | `/incident/:id` | 详情 | 618/888/9527 | | POST | `/incident` | 人工上报 | 618/888/9527 | | POST | `/incident/:id/transition` | 状态流转+处置 | 888/9527;`force_free` 仅 888 | | GET | `/incident/stats` | 状态/今日统计 | 618/888/9527 | 角色约定(沿用现有种子):618=操作员,888=管理员,9527=超级管理员。`force_free` 资金类处置在服务层通过 `utils.GetUserID` + 角色判断二次校验(Casbin 只到路径粒度,资金类需在业务层校验权限,防止低权限角色直接调用)。 ### 6.2 权限种子 仿 `operation_permission_seed.go` 新增 `EnsureIncidentPermissions()`: - `sys_apis` 按 `path+method` 幂等插入上表 5 个 API; - `casbin_rule` 给 618/888/9527 插 list/detail/create/stats 规则;给 888/9527 插 transition 规则; - 挂到 `SeedSystemData` 新库/旧库两个分支末尾(与 `EnsureOperationPermissions` 同位置,seed.go:23-29 与 533-538)。 ### 6.3 菜单与页面 - 复用菜单 27"异常报表"(`view/report/sheet.vue`,已授权 618/888/9527),**不新增菜单**,仅重写页面。 - 若需将菜单标题改为"异常处置",需在种子中增加"更新已存在菜单标题"逻辑(现有种子只插不更);v1 建议保留原标题以省去迁移,文档标题在页面内体现。 ## 7. 自动埋点设计(v1) ### 7.1 统一通行失败(`internal/service/parking/passage.go`) | 埋点位置 | 触发条件 | 分类 | 等级 | | --- | --- | --- | --- | | `handleEntry`/`handleExit` 开闸分支(passage.go:172、231) | `OpenGateByDeviceCode` 返回错误 | `gate_failed` | critical | | `handleEntry` 前(passage.go:130-132) | `passageStatus[identifier]` 已在场内 | `duplicate_entry` | warning | | `HandlePassage` 黑名单检查(passage.go:92-95) | `isBlack == true` | `blacklist` | warning | | `handleEntry` 内 `VehicleEntry` 返回 `ErrParkingLotFull` | 满位拒绝 | `lot_full` | info | | `handleExit` 内 `ExitConfirm` 返回"无入场记录"类错误 | 无会话/丢票 | `no_entry_exit` | critical | 实现要点: - 错误识别:`errors.Is(err, parkingSessionService.ErrSessionNotFound)` / `ErrSessionAlreadyOpen` / `ErrParkingLotFull` 判定分类(哨兵错误已存在,见 parking-session/service.go:20-29);`ExitConfirm` 错误中"无入场记录"需按 `errors.Is` 或错误消息兜底判定。 - 开闸失败场景:在 `passage.go` 返回 `result+err` **之前**调用 `RecordIncident`(此时会话 ID/票号已在 `result` 中),并同时写指令流水(7.5)。 - 埋点不改变现有返回值:`RecordIncident` 内部吞错,调用前后业务代码零改动语义。 ### 7.2 人工抬杆/关闸(`internal/api/v1/parking/gate.go`) - `POST /parking/gate/open|close` 请求体增加**可选** `reason` 字段(向后兼容,缺省为空)。 - 命令执行后调用 `RecordIncident`:分类 `manual_raise`、来源 `manual`、状态直接 `resolved`、`handle_type=manual_gate`、`handle_remark=reason`,`operator_id` 从 JWT 取。 - **命令失败分支**:不生成 `manual_raise`(动作未完成),改生成分类 `gate_failed`、来源 `manual`、状态 `pending` 的待处置记录,`handle_remark` 写明人工操作失败原因与设备状态,等待管理员复核(可能需现场处理或换设备重试)。 - 关联上下文:若请求体带 `plate_number`/`session_id`(可选字段)则一并写入;不带也可(纯设备操作留痕)。 - 同时写指令流水(action=open/close、source=manual、result=success/failed)。 ### 7.3 设备离线(`internal/service/uhf/reader.go`、`conn_manager.go`) - 埋点位置:读协程 `ReadData` 出错置 `status=offline` 的分支(reader.go:251-261)与 `conn_manager.go:72` 连接断开检测处。 - 触发条件:**状态从在线转为离线**(避免重连抖动重复生成);配合 5.4 去重规则(同设备未关闭不重复生成)。 - 分类 `device_offline`、来源 `device`、等级 `warning`,`device_code`/`parking_lot_id` 从设备记录取。 - 重连成功(置 `status=online` 分支):调用 `ResolveDeviceOffline(deviceCode)`,将同设备 pending/processing 的离线记录批量置 `resolved` + `device_repaired`,备注"设备恢复在线"。 - 离线事件同时**不动**内存 `DeviceManager` 与 `GetGateRuntimeStatus`,仅补 DB 事实与异常记录;三套状态一致性问题另立任务,不在本设计内解决(记录到 `doc/功能缺失.md` 3.6 追踪)。 ### 7.4 扩展点(非 v1) - **打印失败**:待打印任务持久化(P0)落地后,在打印服务失败分支调用 `RecordIncident`(分类 `print_failed`),并支持 `reprint` 处置方式重打。 - **支付失败/不确定**:待支付订单层(P0)落地后,在订单超时、回调失败、对账差异处埋点(分类 `payment_failed`/`payment_uncertain`);POS/缴费机终端超时时直接生成 `payment_uncertain` 工单引导人工核对(呼应 `doc/收费流程.md` 第 5.5 条)。 ### 7.5 指令流水埋点(建议包含) 统一在 `OpenGateByDeviceCode`/`CloseGateByDeviceCode` 内包一层: ```go start := time.Now() err := controller.OpenGate(deviceCode, validTime) logCommand(DeviceCommandLog{DeviceCode, Action: "open", Source: 来源, Result: success/failed, ErrorMessage, DurationMs}) ``` - 通行链路调用(passage.go)时 `source=passage`,人工 API 时 `source=manual`(由调用方经参数传入,封装函数加一个 `source`/`operator` 可选参数,缺省 `passage`)。 - 开闸失败时 `incident_id` 关联 `gate_failed` 记录(先建异常、后写流水,拿到 ID 回填)。 - 该表只读不参与业务判断,写失败同样 best-effort 记日志。 ## 8. 前端页面设计(`view/report/sheet.vue` 重写) 结构完全复用 `paymentRecord.vue` 蓝本: 1. **统计行**(`el-statistic` × 4):待处理、处理中、今日新增、资金类待处理(金额合计,供管理层关注)。 2. **筛选表单**(`el-form inline`):状态 `el-select`、分类 `el-select`、等级 `el-select`、车牌 `el-input`、票号 `el-input`、停车场 `el-select`(数据源 `obtainPullOverList`,同 revenue.vue)、日期范围 `el-date-picker`、搜索/重置按钮。 3. **表格**(`el-table` + `el-table-column`):编号、分类(tag 颜色区分)、等级(tag:info 蓝 / warning 橙 / critical 红)、状态(tag:待处理 warning / 处理中 primary / 已解决 success / 已关闭 info)、车牌、停车场、通道/设备、描述(`show-overflow-tooltip`)、上报时间、处理人、操作(详情/处理)。 4. **分页**:`el-pagination`(page/page_size/total,同 paymentRecord.vue)。 5. **详情抽屉**(`el-drawer`):完整字段 + `event_log` 时间线(`el-timeline`)+ 处置表单(目标状态、处置方式 `el-select`、强制免费金额 `el-input-number`(仅 force_free 显示)、备注 `el-input textarea`)。处置按钮按角色控制:618 只读,888/9527 可处置,`force_free` 仅 888 可提交(前端隐藏 + 后端强制校验双保险)。 6. **常量映射**:分类/等级/状态/处置方式 → 中文名 + tag 类型,集中定义在页面顶部常量对象(同 paymentRecord.vue 的 `methodName` 兜底模式)。 新增 `frontend/src/api/incident.js`(仿 `shift.js` 极简模式): ```js import service from '@/utils/request' export const getIncidentList = (params) => service({ url: '/incident/list', method: 'get', params }) export const getIncidentDetail = (id) => service({ url: `/incident/${id}`, method: 'get' }) export const createIncident = (data) => service({ url: '/incident', method: 'post', data }) export const transitionIncident = (id, data) => service({ url: `/incident/${id}/transition`, method: 'post', data }) export const getIncidentStats = () => service({ url: '/incident/stats', method: 'get' }) ``` ## 9. 数据库迁移与兼容 - `RegisterTables`(`internal/initialize/gorm.go`)的 AutoMigrate 列表追加 `dao.IncidentRecord{}`(及 `dao.DeviceCommandLog{}`,若采纳指令流水)。AutoMigrate 只建新表,不改旧表,无历史数据兼容问题。 - 埋点为纯增量调用:`passage.go`、`gate.go`、`reader.go` 的现有返回值与事务边界不变;人工抬杆 API 的 `reason` 为可选参数,旧客户端不受影响。 - 异常记录软删除(GVA_MODEL 自带 DeletedAt),列表默认排除已删除;已删除记录不在统计口径内。 ## 10. 测试设计(实施阶段执行) 沿用现有测试模式(`glebarez/sqlite` 内存库 + `global.GVA_DB`,见 `parking-session/service/service_test.go`): | 测试组 | 场景 | | --- | --- | | 编号生成 | 当日序号递增、跨日重置、并发生成不重复 | | 状态机 | 合法流转全路径、非法流转拒绝(pending→closed 等)、resolved 缺 handle_type/remark 拒绝、force_free 缺金额/负金额拒绝 | | CAS 并发 | 两条并发流转同一记录,仅一条成功 | | best-effort | 模拟 DB 故障(注入错误连接)时 `RecordIncident` 返回 nil 不阻塞业务 | | 去重 | 同设备离线不重复生成、同通行标识时间窗内不重复 | | 统计 | 状态计数/今日新增/资金类金额合计 | | 埋点集成 | 注入失败 `GateController` 使 `OpenGateByDeviceCode` 失败 → 生成 `gate_failed` + 指令流水;人工抬杆带/不带 reason 均生成记录;离线→重连自动关闭 | | 路由安全 | 参照 `router_security_test.go`:匿名/低权限角色访问 `transition` 被拒 | ## 11. 实施顺序与验收标准 ### 11.1 实施顺序 1. 模型:`dao.IncidentRecord`(+ `dao.DeviceCommandLog`)+ `RegisterTables` 注册。 2. 服务层:repository 分页/去重/CAS + service 六方法 + 状态机 + 编号生成。 3. 埋点:passage.go 五类埋点 + 人工抬杆 reason 参数 + reader.go 离线/重连埋点 + 指令流水封装。 4. 权限种子:`EnsureIncidentPermissions()` + `SeedSystemData` 挂载。 5. 前端:`api/incident.js` + `view/report/sheet.vue` 重写。 6. 测试:第 10 节全部用例 + `npm run build` 前端构建验证。 ### 11.2 验收标准(对应 `doc/功能缺失.md` 2.2) - 异常自动落库,包含来源、停车会话、车辆、通道、设备和操作员(开闸失败、重复入场、黑名单、满位、无入场出场、人工抬杆、设备离线七类 v1 场景实测通过)。 - 支持待处理、处理中、已解决、已关闭四状态流转,处理备注必填,流转留痕(event_log 可查)。 - 人工抬杆与资金类(强制免费)处置有权限控制与完整审计(角色校验 + 操作日志 + 金额记录)。 - 页面:筛选、分页、统计、详情时间线、处置表单可用;低权限角色看不到处置入口。 - 自动化测试覆盖状态机、并发、去重、埋点集成与权限拒绝,全部通过。 ## 12. 与相关文档的衔接 | 文档 | 衔接点 | | --- | --- | | `doc/功能缺失.md` | 本文对应 2.2 节 P0 项;实施完成后从缺失清单移除,并同步 2.3(打印补偿)与 3.6(设备模型)的追踪 | | `doc/项目进度.md` | 实施完成后在"待办与优先级"中更新异常处置闭环状态 | | `doc/代码规划.md` | 模块结构与边界遵循第 2、3 节约定;`incident` 加入模块清单 | | `doc/收费流程.md` | 支付失败/不确定埋点在支付订单层落地时按第 5.5 条"进入人工核对"原则实现 |