diff --git a/audit/BUSINESS_FLOW_TEST_REPORT.md b/audit/BUSINESS_FLOW_TEST_REPORT.md
index b86ece5..28e115a 100644
--- a/audit/BUSINESS_FLOW_TEST_REPORT.md
+++ b/audit/BUSINESS_FLOW_TEST_REPORT.md
@@ -1,219 +1,65 @@
-# 全项目业务流程测试报告
+# 全项目流程审计与检查报告
-测试日期:2026-07-31
-测试分支:`main`
-测试环境:仓库当前远程开发 PostgreSQL / Redis、Mock 验证码、Mock 支付边界
+审计日期:2026-08-03
+
+依据:`checking/03-系统与业务覆盖矩阵.md`、`checking/06-业务流程测试.md`
## 1. 结论
-本轮门禁通过:
+本轮是代码、契约和本地门禁审计,不是远程环境端到端验收。BF-01~BF-15 中没有任何流程具备“全链路通过”证据:4 项为部分具备,11 项因实现缺口或环境缺口阻塞。旧报告中的远程数据库、浏览器和 Flutter 成功结论不得沿用。
-- 核心成功路径全部通过。
-- 本轮发现的 P0:0 个。
-- 本轮发现并修复的 P1:5 类。
-- 修复后平台、气站、配送三套后台的 91 个鉴权及资源列表 HTTP 检查全部通过。
-- 用户注册、服务关系、商城、钱包和提现闭环的 56 个流程断言全部通过。
-- 三套 Web 已用真实浏览器完成登录和关键页面回归。
-- 两个 Flutter 客户端的静态分析及现有测试全部通过。
+状态定义:
-当前没有阻止提交和推送的未修复 P0/P1。
+- `部分具备`:存在主要实现和局部自动化证据,但缺远程依赖、外部沙箱或跨端 E2E。
+- `阻塞`:关键业务步骤不存在,或必要环境/安全凭据未提供。
-## 2. 测试范围与数据隔离
+## 2. BF-01~BF-15 检查矩阵
-测试以 `BFT20260731...` 为唯一前缀创建气站、配送点、后台账号、用户、地址、商品、钱包、银行卡、商城订单和提现申请。未使用或修改既有业务主数据。
+| 流程 | 状态 | 本轮证据 | 主要缺口 |
+| --- | --- | --- | --- |
+| BF-01 邀请注册与服务关系 | 阻塞 | 管理端与 API 可构建 | 无本轮远程 DB/Redis、二维码安全及跨端 E2E。 |
+| BF-02 设备绑定与远程关阀 | 阻塞 | 内部命令/Outbox/Gateway/协议代码存在 | 无设备绑定、用户归属鉴权和公开远控入口。 |
+| BF-03 高风险遥测与整改 | 阻塞 | 上行消息可持久化 | 无规则事件、自动关阀、通知、整改复检及开阀拦截闭环。 |
+| BF-04 共享、群控与报修 | 阻塞 | 无充分证据 | 共享权限、群控结果聚合及撤权服务端校验未形成闭环。 |
+| BF-05 下单、支付与任务 | 部分具备 | 支付回调一致性已修复并有单测 | 无远程事务、支付沙箱、商户/appid 显式校验和跨端任务 E2E。 |
+| BF-06 超时关单与迟到回调 | 部分具备 | Worker 调度与 API 关单代码可构建 | Worker 无测试;无渠道超时、竞争和重启实测。 |
+| BF-07 订单项退款入钱包 | 部分具备 | 退款模型/逻辑可静态检查 | 无远程并发、审核隔离、钱包守恒和渠道对账证据。 |
+| BF-08 气瓶配送与回收 | 阻塞 | 三套后台可构建 | 无库存并发、轨迹、扫码取证、押金及结算 E2E。 |
+| BF-09 人员准入与离线补传 | 阻塞 | App 代码存在 | Flutter 未验证;服务端强制前置和离线完整性无全链路证据。 |
+| BF-10 安装/维修闭环 | 阻塞 | 需求与页面资源存在 | 缺服务端顺序状态机和强制证据集成测试。 |
+| BF-11 钱包提现审核 | 部分具备 | 资金逻辑已有历史单测,后端门禁通过 | 本轮未连接远程 DB/Redis,未执行并发、重复审批和打款回执 E2E。 |
+| BF-12 组织隔离与轨迹访问 | 阻塞 | 鉴权框架和后台资源存在 | 无多组织远程数据、分页/导出/缓存串租户及敏感访问审计 E2E。 |
+| BF-13 IoT 上行与 ACK | 阻塞 | 协议、Gateway、Topic 身份单测通过 | ACK 包号复用风险;无 MQTT Broker/真设备联调。 |
+| BF-14 IoT Outbox 恢复 | 阻塞 | Outbox/Worker 投递代码存在 | 无双 Worker、崩溃窗口、重复投递、积压和死信测试。 |
+| BF-15 审计与灾备 | 阻塞 | 部分事实有审计字段 | 未执行 PostgreSQL WAL 恢复、Redis 故障恢复和 RPO/RTO 测量。 |
-测试结束后:
+## 3. AC-01~AC-25 汇总
-- 已驳回仍处于待处理状态的并发测试提现,恢复其预扣余额。
-- 已归档本轮创建的气站、配送点、后台账号、用户、地址、服务关系、商品和分类等可变主数据。
-- 钱包流水、提现申请、审核记录等不可变资金与审计证据按约定保留,并可通过测试前缀追踪。
-- Redis 验证码使用短 TTL,未修改既有键;本轮测试键会自动过期。
+| 状态 | AC |
+| --- | --- |
+| 局部代码证据 | AC-02、AC-07、AC-08、AC-24、AC-25 |
+| 关键实现或 E2E 阻塞 | AC-01、AC-03~AC-06、AC-09~AC-23 |
-## 3. 核心流程结果
+“局部代码证据”不代表验收通过。AC-02/AC-25 仍受 ACK 关联和 Broker 联调阻塞;AC-07 仍受支付沙箱和商户身份检查阻塞;AC-08 仍受远程并发验证阻塞;AC-24 仍需数据库 Schema 全量核查。
-### 3.1 鉴权与数据域
+## 4. 本轮实际验证
-- 平台 root 正确密码登录成功,错误密码被拒绝。
-- 未携带令牌访问受保护接口被拒绝。
-- 气站管理员、配送管理员分别登录成功。
-- 气站令牌访问平台后台、用户端接口均被拒绝。
-- 用户完成验证码注册、密码登录、资料及地址查询。
-- 用户注册时正确建立气站和配送点服务关系。
+| 范围 | 结果 |
+| --- | --- |
+| API | `go test ./...`、`go vet ./...`、build 通过 |
+| Worker | test/vet/build 通过,但无测试文件 |
+| IoT Gateway | test/vet/build 通过 |
+| IoT Server | test/vet/build 通过,含 Topic 身份回归测试 |
+| platform_admin | type、contract、build 通过;lint 有 111 warnings/12 infos |
+| gas_admin、delivery_admin | type、lint、build 命令通过;存在非阻断 lint 告警 |
+| site | build 通过,4/4 托管测试通过 |
+| Flutter Apps | 命令启动超时,未取得结果 |
+| 远程 PostgreSQL/Redis | API YAML 已恢复远程配置,但本轮未执行集成流程 |
-### 3.2 商城与幂等
+## 5. 远程执行前置清单
-- 创建并启用商城分类和商品。
-- 用户按服务端价格创建 80 元订单。
-- 相同 `request_no` 重复创建只返回同一订单,未重复扣减库存。
-- 钱包支付成功后订单进入已支付状态。
-- 已支付订单重复支付被拒绝。
-
-### 3.3 钱包资金闭环
-
-以人工充值 100 元且可提现为起点:
-
-1. 充值后:`balance=10000`、`withdrawal_balance=10000`。
-2. 重复充值请求保持幂等,未重复入账。
-3. 商城消费 80 元后:`balance=2000`、`withdrawal_balance=2000`。
-4. 再申请提现 100 元被拒绝,余额保持不变。
-5. 申请提现 10 元后立即预扣:两类余额均从 2000 降至 1000。
-6. 重复提交同一提现申请未重复预扣。
-7. 平台驳回后两类余额均恢复至 2000。
-8. 重复驳回未重复返还,余额没有凭空增加。
-9. 未审核提现直接完成被拒绝。
-10. 审核通过并完成提现 10 元后:`balance=1000`、`withdrawal_balance=1000`。
-11. 相同交易号重复完成保持幂等。
-12. 两个并发 15 元提现请求面对 20 元余额时仅一个成功,最终余额为 5 元,没有透支。
-
-以上结果确认此前的重复支出、驳回增发和完成不扣总余额问题已闭环。
-
-### 3.4 全资源 HTTP 扫描
-
-修复后独立执行 91 个回归检查,结果 `91/91`:
-
-- 平台总后台:47 个资源列表。
-- 气站后台:20 个资源列表。
-- 配送后台:18 个资源列表。
-- 另含三端登录、平台错误密码和未认证访问检查。
-
-所有接口均返回预期业务码,没有 404、500、跨端越权或 SQL 错误。
-
-## 4. 发现并修复的问题
-
-### P1-1 远程库迁移被历史非空数据阻断
-
-`gasorder_track_point.received_at` 新增为非空字段时,GORM 直接执行 `ADD NOT NULL`,已有轨迹行导致迁移失败。
-
-修复:
-
-- 迁移前先添加可空字段。
-- 使用既有 `occurred_at`,再退化到 `created_at` / 当前时间回填。
-- 回填完成后再设置非空。
-- PostgreSQL 和 MySQL 分别使用对应 SQL。
-- 已在远程开发库实际迁移成功。
-
-### P1-2 联表资源查询的 `status` 字段歧义
-
-气站后台的配送账号、人员资质等联表资源使用裸 `status <> 3`,PostgreSQL 返回 `column reference "status" is ambiguous`。
-
-修复:
-
-- `ActiveRecords` 统一使用 GORM 当前表限定 `"
"."status"`。
-- 气站配送账号查询抽取明确的数据域查询函数。
-- 增加联表 SQL 回归测试。
-- 修复后气站和配送全部资源列表通过。
-
-### P1-3 配送钱包充值页面契约存在但 GET 路由缺失
-
-配送前端声明 `/wallet_recharge` 资源页,后端仅有 POST 充值动作,列表页请求 GET 时直接 404。
-
-修复:
-
-- 增加配送点范围内的充值流水列表和详情接口。
-- 仅返回当前配送点钱包中 `delivery_admin_recharge` 类型流水。
-- 同步配送前端生成契约。
-- 增加路由回归断言。
-
-### P1-4 气站、配送后台登录令牌写入与读取使用不同键
-
-登录 Store 将令牌写入 `token`,HTTP 客户端却读取 `gas_admin_token` / `delivery_admin_token`。表现为提示“登录成功”后资料请求 401,并退回登录页。
-
-修复:
-
-- HTTP 客户端统一调用各端 `getToken()`。
-- 气站端固定使用 `gas_admin_token`。
-- 配送端固定使用 `delivery_admin_token`。
-- 浏览器回归确认两端均能进入运营首页。
-
-### P1-5 隐藏关联页被前端权限守卫错误送到 404
-
-`hideInMenu` 的资质、地址、关联明细页复用已授权 `menuCode`,但权限守卫仍强制要求路由名存在于服务端菜单树。
-
-修复:
-
-- 三套后台统一允许“服务端菜单路由存在”或“当前路由 `menuCode` 已授权”。
-- 后端 JWT、角色和数据域校验保持不变。
-- 浏览器回归确认气站人员资质隐藏页可访问。
-
-## 5. Web 浏览器验证
-
-使用本地真实前端和远程开发 API:
-
-- 平台后台 `5173`:root 登录、运营仪表盘数据、提现列表加载通过。
-- 气站后台 `5175`:隔离气站管理员登录、首页数据、人员资质隐藏页加载通过。
-- 配送后台 `5176`:隔离配送管理员登录、首页、钱包充值页加载通过。
-- 关键页面控制台无 error / warning。
-
-## 6. Flutter 客户端
-
-### 用户 App
-
-- `flutter analyze`:通过,无问题。
-- `flutter test`:2 项通过。
-
-### 服务 App
-
-- `flutter analyze`:通过,无问题。
-- `flutter test`:3 项通过。
-
-设备级运行未覆盖,原因:
-
-- 两个工程当前只包含 Android / iOS 目录,没有 macOS / Web 平台。
-- 本机没有 Android SDK。
-- Flutter 报两个工程未配置为可构建的 iOS 应用。
-- 当前可见设备只有 macOS 和 Chrome,无法承载现有工程。
-
-因此客户端结论限定为“静态分析和自动化测试通过”,不声明真机/模拟器运行通过。
-
-## 7. Worker、IoT 与外部边界
-
-- Worker:`go test ./...`、构建通过;当前为 Mock 边界。
-- IoT:`go test ./...`、构建通过;未连接真实 MQTT Broker 或设备。
-- 未连接真实短信、支付渠道和 MQTT。
-- 验证码与支付只验证当前 Mock 实现的状态机、幂等和资金记账。
-
-## 8. 实际执行的回归命令
-
-```bash
-cd backend/api
-go test ./...
-go vet ./...
-go build ./cmd/main/main.go
-
-cd backend/worker
-go test ./...
-go build ./cmd/main/main.go
-
-cd backend/iot
-go test ./...
-go build ./cmd/main/main.go
-
-cd frontend/platform_admin
-pnpm type:check
-pnpm lint
-pnpm contract:check
-pnpm build
-
-cd frontend/gas_admin
-pnpm type:check
-pnpm lint
-pnpm contract:check
-pnpm build
-
-cd frontend/delivery_admin
-pnpm contract:sync
-pnpm type:check
-pnpm lint
-pnpm contract:check
-pnpm build
-
-cd apps/user_app
-flutter analyze
-flutter test
-
-cd apps/service_app
-flutter analyze
-flutter test
-```
-
-补充说明:三套前端 lint 命令返回成功,但仍报告工程模板既有的未使用代码、单词组件名和 Node import 风格 warnings;本轮未批量改写这些非业务告警。
+1. 确认 YAML 配置指向隔离库,并为 Redis 使用独立 DB/前缀。
+2. 限制配置文件访问权限,不在命令、报告或日志中记录连接值。
+3. 明确测试数据前缀和保留策略;禁止清库和删除资金/审计事实。
+4. 启动支付沙箱、MQTT 模拟器和四后端进程,逐项记录 UI/API、数据库、Outbox、回执和审计证据。
+5. 先解决 P0/P1 实现缺口,再执行完整 BF 门禁;否则测试结果只能是预期阻塞。
diff --git a/audit/CODE_AUDIT_REPORT.md b/audit/CODE_AUDIT_REPORT.md
index 2a2f6f5..31d9b50 100644
--- a/audit/CODE_AUDIT_REPORT.md
+++ b/audit/CODE_AUDIT_REPORT.md
@@ -1,640 +1,72 @@
# 全项目代码审计报告
-审计日期:2026-07-31
+审计日期:2026-08-03
-审计对象:`apps/*`、`backend/*`、`frontend/*`
+审计范围:`apps/*`、`frontend/*`、`backend/{api,worker,iot-gateway,iot-server}`
-审计方式:只读代码审计、静态检查、单元测试和本地构建
+依据:`checking/*`、`docs/01`~`docs/12`、仓库实现与测试结果
-审计基线:仅以实际代码、路由、数据模型、客户端调用和测试为依据,未将 `docs/*` 作为需求或验收依据
+## 1. 发布结论
-## 1. 结论
+当前版本**不满足生产发布条件**。本轮确认并修复 4 类高风险实现问题,但设备绑定/安全整改等核心业务尚未形成可执行闭环,远程 PostgreSQL/Redis 集成、真实支付沙箱、MQTT Broker 级联调和灾备恢复尚无本轮证据。
-当前代码**不具备生产发布条件**。
+风险统计(未修复项):P0 2 项、P1 6 项、P2 4 项。代码修复不等于对应跨端流程已验收。
-本次确认:
+## 2. 本轮已修复
-- P0:2 项
-- P1:7 项
-- P2:8 项
-- P3:3 项
+| 编号 | 等级 | 问题 | 修复与验证 |
+| --- | --- | --- | --- |
+| FIX-02 | P0 | 通用迁移会直接删除旧支付/退款表,破坏资金及审计事实 | 改为检测旧表并中止迁移,要求专用保留迁移;新增 sqlmock 回归测试。 |
+| FIX-03 | P1 | 支付回调可能在目标业务记录不存在时仍把支付单标记成功 | 业务状态推进强制恰好更新一行;补充 0 行、1 行和数据库错误测试;重复回调校验渠道流水号。 |
+| FIX-04 | P1 | IoT Server 信任帧内设备号,可利用他人 Topic 伪造 ACK | 设备身份取自已认证 Topic;帧内设备号不一致时不转发解码 ACK;补充 Topic 单元测试。 |
+| FIX-05 | P2 | Worker/IoT Server 使用无明确超时的 HTTP 客户端;内部令牌普通比较 | 增加 12/15 秒超时;支付 Worker 令牌改为常量时间比较。 |
-最先需要阻断发布的事项是:
+## 3. 未修复发布阻断项
-1. 仓库历史中存在明文的外部数据库、Redis 等敏感配置;固定验证码又可直接进入注册、登录和重置密码流程。
-2. 钱包的 `balance` 与 `withdrawal_balance` 记账口径不闭合,可产生重复支出、拒绝提现后余额凭空增加、提现完成后总余额不减少等资金错误。
-3. Worker、IoT 和上传存储仍明确是 Mock,实际异步与设备链路不存在。
-4. 工作人员“作业前置检查”仅用于展示,业务接口没有服务端强制执行。
+### P0
-因此,测试和构建全部通过并不能推导出业务可上线;现有测试主要验证路由、模型和少量纯函数,没有覆盖上述关键资金、鉴权、幂等和跨端流程。
+1. **核心设备安全闭环未实现。** 当前 API 只有内部 IoT 命令、Outbox 和设备消息路由,没有面向用户的设备绑定/归属鉴权、远控动作、高风险开阀拦截、安全事件、整改和复检闭环。AC-01~AC-06、AC-12、AC-15、AC-22 不能验收。
+2. **真实远程依赖及资金链路无本轮证据。** API 已按要求恢复 YAML 中的远程 PostgreSQL/Redis 配置,但本轮没有执行远程数据库事务、Redis 并发/TTL、支付宝/微信沙箱回调及退款对账。BF-05~BF-07、BF-11 不能作为通过。
-### 等级定义
+### P1
-| 等级 | 含义 |
-|---|---|
-| P0 | 可直接造成账户接管、资金损失、核心数据破坏或必须立即处置的密钥泄露 |
-| P1 | 核心流程不可用、可绕过关键业务控制或上线即产生严重错误 |
-| P2 | 在异常、并发、重试或特定数据下产生错误,或形成明显维护/兼容风险 |
-| P3 | 低风险冗余、局部质量或性能问题 |
+1. IoT ACK 仅按 `device_id + uint16 packet_number + 活跃状态` 批量更新;包号重启后可复用,且没有约束同设备同包号仅一个活跃命令,存在误确认风险。
+2. Worker 没有自动化测试,关单扫描、Outbox 重试、双 Worker 认领、进程重启和坏响应均无回归保护。
+3. 微信回调依赖 SDK 验签解密,但业务代码未显式比对回调商户号、appid 和币种与本地配置/支付单一致。
+4. IoT Server TLS、设备认证和 Topic ACL 未以生产模式启动门禁强制;Broker 级伪造、越权订阅和断线重连未验证。
+5. 服务人员准入、离线补传强制字段、安装/维修/安检完成条件尚无完整服务端闭环证据。
+6. PostgreSQL 全量加 WAL 恢复、Redis 故障恢复及实际 RPO/RTO 未演练。
-置信度“高”表示从当前代码可以完整证明;“中”表示代码证据明确,但删除字段或迁移前仍需核对线上数据及外部消费者。
+### P2
-## 2. P0:发布阻断
+1. 支付创建采用“先查询再插入”,并发相同幂等请求可能向渠道重复建单后在本地唯一键失败;需数据库预留事实或冲突回读设计。
+2. IoT Outbox 没有明确死信上限和人工恢复状态;持续失败会无限重试。
+3. 三套管理端 lint 仍有约百条告警,规则目前不会阻断质量门禁。
+4. Flutter 工具在本机启动超时,两个 App 的 analyze/test 未得到本轮结果。
-### P0-01 仓库内存在敏感连接配置,固定验证码可形成账户接管链路
+## 4. 模块审计结论
-置信度:高
+| 模块 | 结论 | 主要证据/缺口 |
+| --- | --- | --- |
+| API | 有条件通过静态门禁 | 测试、vet、build 通过;支付新增回归通过;核心设备/安全/服务闭环缺失。 |
+| Worker | 构建通过、业务门禁不通过 | test/vet/build 通过,但无测试文件,远程依赖与故障恢复未执行。 |
+| IoT Gateway | 局部门禁通过 | test/vet/build 通过;缺真实 Gateway→IoT Server 故障注入。 |
+| IoT Server | 局部门禁通过 | 协议及 Topic 测试、vet、build 通过;缺 Broker ACL/TLS/真设备验证。 |
+| platform_admin | 构建通过 | type、contract、build 通过;lint 111 warnings/12 infos。 |
+| gas_admin / delivery_admin | 构建通过 | type、lint、build 命令通过;lint 仍有大量非阻断告警。 |
+| site | 通过本地站点门禁 | 先 build 后执行 4 个托管路由/打包测试,全部通过。 |
+| user_app / service_app | 未验证 | Flutter 命令超时;不能沿用历史结果。 |
-**证据**
+## 5. 敏感配置与迁移要求
-- `backend/api/etc/platform_dev.yaml:7` 包含公网 PostgreSQL 连接地址、账号和明文密码。
-- `backend/api/etc/platform_dev.yaml:10` 包含公网 Redis 地址和明文凭据。
-- `backend/api/etc/platform_dev.yaml:12,18,24` 分别包含固定 JWT 密钥/固定验证码/字段加密密钥性质的配置。
-- 该文件由 Git 跟踪,且至少出现在多个历史提交中,单纯修改当前文件不能消除历史泄露。
-- `backend/api/internal/logic/common/client_auth.go:60-67` 将全局固定验证码写入 Redis。
-- `backend/api/internal/logic/common/client_auth.go:73-85` 使用该固定值完成验证和消费。
-- 用户与工作人员的注册、验证码登录、重置密码、支付密码设置均复用该验证能力。
+- API 按当前项目约定从 YAML 读取远程连接;该文件含敏感配置,应限制仓库和主机访问,并检查 Git 历史、CI 日志及共享记录。
+- 报告、命令输出和日志不得记录远程连接值。
+- 旧资金表迁移需单独方案,至少包含数据盘点、不可变备份、字段映射、对账、回滚和审计批准。
-**影响**
+## 6. 建议修复顺序
-- 外部数据库和 Redis 可能被直接访问、篡改或拖库。
-- 已知固定验证码的人可为目标手机号申请新的 `request_identity`,随后尝试注册、验证码登录或重置密码。
-- 字段加密密钥一旦与密文数据同时泄露,敏感字段的静态加密失去保护。
-
-**根因**
-
-- 开发配置被当作可提交配置管理。
-- Mock 验证码能力直接复用了真实身份流程,没有环境级硬隔离。
-
-**最小整改**
-
-1. 立即轮换数据库、Redis、JWT、字段加密等所有已提交凭据;先吊销旧值,再更新部署。
-2. 核查相关服务的访问日志、异常登录、密码重置和数据导出记录。
-3. 将敏感值迁移到环境变量或密钥管理系统,仓库只保留无效示例。
-4. 清理 Git 历史中的敏感内容,并要求所有已有克隆重新同步。
-5. Mock 验证码只允许在不可访问生产数据的本地环境启动;生产启动时发现 Mock 开关或固定码应直接失败。
-
-**迁移风险**
-
-- 字段加密密钥轮换需要双密钥读取或批量重加密方案,不能直接替换后让历史数据失效。
-
-**复测**
-
-- 对仓库当前树和完整 Git 历史执行密钥扫描。
-- 在非本地环境验证 Mock 验证码无法启动。
-- 使用旧密钥、旧数据库凭据和旧 Redis 凭据验证均已失效。
-- 覆盖目标手机号的验证码登录和重置密码攻击用例。
-
-### P0-02 钱包双余额账本不闭合,可重复支出并制造余额
-
-置信度:高
-
-**证据**
-
-1. 平台可提现充值同时增加两列:
- - `backend/api/internal/logic/platform/wallet/wallet.go:232-239`
-2. 商城支付只扣 `balance`,不扣 `withdrawal_balance`:
- - `backend/api/internal/logic/client/user/shop.go:167-189`
-3. 用户提现申请只扣 `withdrawal_balance`,不扣 `balance`:
- - `backend/api/internal/logic/common/client_wallet.go:372-398`
-4. 提现完成只更新申请状态和外部交易号,不扣 `balance`:
- - `backend/api/internal/logic/platform/wallet/wallet.go:336-366`
-5. 配送后台创建提现申请时不预扣 `withdrawal_balance`,仅在查询时减去待处理申请:
- - `backend/api/internal/logic/delivery/finance.go:145-188`
-6. 平台驳回任何提现申请都会把申请金额加回 `withdrawal_balance`:
- - `backend/api/internal/logic/platform/wallet/wallet.go:313-323`
-
-**可复现场景**
-
-- 钱包充值 100 元且标记可提现后:`balance=100`、`withdrawal_balance=100`。
-- 商城消费 80 元后:`balance=20`、`withdrawal_balance=100`。
-- 再申请提现 100 元会通过,形成 80 元消费加 100 元提现。
-- 配送后台申请提现时未预扣;若平台驳回,现有代码仍加回金额,可将 100 元可提现余额变成 200 元。
-- 提现最终完成也不会减少总余额,余额和实际资金继续背离。
-
-**影响**
-
-- 直接资金损失。
-- 钱包余额、可提现余额、提现申请和流水无法对账。
-- 客户可在正常 API 流程内触发,不需要数据库权限。
-
-**根因**
-
-- `withdrawal_balance` 是 `balance` 的可提现子集,但各交易没有在同一事务内维护该不变量。
-- 用户提现和配送后台提现采用了两套互不兼容的预扣策略。
-- 提现没有完整的冻结额、解冻、出账和不可变流水模型。
-
-**最小整改**
-
-1. 立即关闭充值、余额支付和提现写入口,先冻结风险窗口。
-2. 明确统一不变量,例如 `0 <= available_withdrawable <= available_balance`,并增加“冻结余额/冻结可提现余额”。
-3. 支付、提现申请、驳回、完成必须在事务和行锁内同时更新余额、冻结额、流水。
-4. 合并用户与配送后台提现逻辑,禁止各自实现不同扣减策略。
-5. 对历史钱包、流水、提现、商城订单做全量对账和差异修复。
-
-**迁移风险**
-
-- 不能只改代码;历史余额已经可能不可信。
-- 修复前需以外部支付记录、订单、提现回执和不可变流水重建余额,避免把现有错误余额作为初始事实。
-
-**复测**
-
-- 覆盖充值→支付→提现、提现→驳回、提现→完成、重复请求、并发支付/提现。
-- 对每一步断言余额、可提现余额、冻结额和流水守恒。
-- 增加基于随机交易序列的账本不变量测试。
-
-## 3. P1:核心流程和安全控制
-
-### P1-01 Worker 与 IoT 仅为阻塞式 Mock 进程
-
-置信度:高
-
-**证据**
-
-- `backend/worker/cmd/main/main.go:14-20` 只初始化后等待退出信号,并明确输出 Redis Streams consumer 未启用。
-- `backend/iot/cmd/main/main.go:14-20` 只初始化后等待退出信号,并明确输出 MQTT Broker 未连接。
-- 两模块合计约 150 行 Go 代码,没有消费者、重试、Outbox 投递、MQTT 会话、命令回执或业务测试。
-
-**影响**
-
-- 异步通知、对账、超时处理等依赖 Worker 的流程不会执行。
-- 设备遥测、远程命令和回执链路不存在。
-- 进程可以成功构建和启动,但只会制造“服务在线”的假象。
-
-**最小整改**
-
-- 发布清单中明确排除这两个能力,或在发布前实现真实适配、健康检查、失败重试、幂等和可观测性。
-- 健康检查必须区分“进程存活”和“已连接 Redis Streams/MQTT”。
-
-### P1-02 关闭 Mock 后验证码功能全部失效,代码中没有真实发送适配器
-
-置信度:高
-
-**证据**
-
-- `backend/api/internal/logic/common/client_auth.go:60-63` 无条件保存 `MockVerificationCode`,没有生成随机码或调用短信渠道。
-- `backend/api/internal/logic/common/client_auth.go:73-76` 在 `MockVerificationEnabled=false` 时直接拒绝所有验证码。
-- 未发现短信发送接口、供应商适配器或发送结果处理。
-
-**影响**
-
-- 为安全而关闭 Mock 后,验证码登录、注册、密码重置和支付密码相关流程全部不可用。
-
-**最小整改**
-
-- 建立真实验证码生成、散列保存、发送、频率限制、失败处理和审计链路。
-- Mock 与真实实现应通过依赖注入隔离,禁止在业务函数内用全局开关混用。
-
-### P1-03 工作人员作业预检可以绕过
-
-置信度:高
-
-**证据**
-
-- `backend/api/internal/logic/client/staff/auth.go:67-108` 计算机构归属、资质有效性、在岗状态和 `can_work`。
-- `backend/api/internal/logic/client/staff/auth.go:20-52` 的登录实现只校验账户状态、角色和密码,注释所称“资质有效”未被执行。
-- `backend/api/internal/logic/client/staff/work.go:72-85` 及 `backend/api/internal/logic/client/staff/delivery.go:55-230` 的开始、轨迹、到达、异常、恢复、签收等接口没有校验同一套预检条件。
-- `apps/service_app/lib/app/router.dart:12-98` 只按是否有 Token 路由;可直接进入 `/work` 或 `/tasks/:identity`,没有强制 `can_work`。
-
-**影响**
-
-- 离岗、资质过期或缺少机构归属的人员仍可绕过页面直接调用作业 API。
-
-**最小整改**
-
-- 将作业资格校验提取为服务端中间件/领域守卫,挂载在所有会改变工单、配送、轨迹、证据的接口上。
-- 客户端预检仅负责展示,不作为可信控制。
-
-### P1-04 三套支付事实互相割裂,真实支付不会进入看板支付统计
-
-置信度:高
-
-**证据**
-
-- 重叠模型:
- - `backend/api/internal/models/fin_payment.go:8-16`
- - `backend/api/internal/models/wallet_payment.go:5-20`
- - `backend/api/internal/models/gasorder_payment.go:5-12`
-- 除 Mock seed 外,未发现业务代码创建 `FinPayment`、`WalletPayment` 或 `GasorderPayment`。
-- 商城实际支付只更新 `EcOrder` 并创建 `WalletRecord`:
- - `backend/api/internal/logic/client/user/shop.go:180-191`
-- 平台支付金额和渠道统计读取 `WalletPayment`:
- - `backend/api/internal/logic/platform/dashboard/statistics.go:106-132`
-- 气体订单的金额调整又以 `GasorderPayment` 是否存在作为“已支付”判断:
- - `backend/api/internal/logic/delivery/order.go:369-375`
-
-**影响**
-
-- 真实商城支付成功后,看板支付金额仍可能为零。
-- 支付页面、财务支付、钱包支付、气体订单支付显示不同事实。
-- 气体订单代码具备“已支付后禁止改价”判断,但当前流程没有形成对应支付记录。
-
-**最小整改**
-
-- 选定唯一支付主事实和订单支付关联模型。
-- 所有支付渠道在同一事务/事件链路写入统一支付事实和钱包流水。
-- 看板、财务、气站、配送后台统一读取同一事实或受控聚合。
-
-**迁移风险**
-
-- 三张表不能直接删;需先核对线上数据和外部消费者,建立字段映射与去重规则。
-
-### P1-05 用户 App 的商城流程在“请前往订单页支付”后中断
-
-置信度:高
-
-**证据**
-
-- 下单成功明确提示前往订单页支付:
- - `apps/user_app/lib/ui/features/shop/shop_page.dart:59-70`
-- `ClientRepository` 只有创建和列表方法,没有调用后端已有的支付、取消、确认收货接口:
- - `apps/user_app/lib/data/repositories/client_repository.dart:43-117`
-- 订单页只是三个只读列表:
- - `apps/user_app/lib/ui/features/orders/orders_page.dart:7-47`
-- 后端实际提供 `/shop/orders/:identity/pay`、`cancel` 和 `confirm-receipt`:
- - `backend/api/internal/routers/client.go:47-51`
-
-**影响**
-
-- 用户可以下单并占用库存,但不能在客户端完成支付、取消或确认收货。
-
-**最小整改**
-
-- 增加订单详情及基于服务端状态的支付/取消/确认动作。
-- 支付请求必须持久化并复用幂等号,不能每次点击生成新值。
-
-### P1-06 冻结或停用的钱包仍可在客户端执行资金操作
-
-置信度:高
-
-**证据**
-
-- 平台允许把钱包改为启用、停用或冻结:
- - `backend/api/internal/logic/platform/wallet/wallet.go:191-200`
-- 客户端 `ensureWallet` 对已存在钱包直接返回,不检查状态:
- - `backend/api/internal/logic/common/client_wallet.go:41-57`
-- 商城支付和用户提现查询钱包时也未限制 `status`:
- - `backend/api/internal/logic/client/user/shop.go:167-177`
- - `backend/api/internal/logic/common/client_wallet.go:366-398`
-
-**影响**
-
-- 风控冻结不能阻止支付和提现。
-
-**最小整改**
-
-- 所有资金写操作在事务内按 `status=enable` 锁定钱包;冻结后禁止新交易,仅允许受控冲正/退款。
-
-### P1-07 上传能力仍是本地 Mock,且只按扩展名判定文件类型
-
-置信度:高
-
-**证据**
-
-- `backend/api/internal/logic/upload/upload.go:36-37,90-95` 明确为本地 Mock 存储。
-- `backend/api/internal/logic/upload/upload.go:45-49` 只检查文件名扩展名和大小。
-- `backend/api/internal/logic/upload/upload.go:79-83` 将客户端声明的 Content-Type 原样返回,没有校验文件签名或实际 MIME。
-
-**影响**
-
-- 伪装成图片/PDF/视频的任意内容可进入存储。
-- 单机本地目录无法支持多实例、一致备份、受控下载或恶意文件隔离。
-
-**最小整改**
-
-- 校验魔数和解码结果,重编码图片,对视频/PDF进行独立扫描。
-- 使用私有对象存储、短期授权访问、病毒扫描、审计和生命周期策略。
-
-## 4. P2:一致性、重试与维护风险
-
-### P2-01 多处“幂等”只处理唯一键冲突,没有验证请求归属和载荷
-
-置信度:高
-
-**证据**
-
-- 签收回执按全局 `request_no` 返回任意既有确认记录,没有校验订单和当前工作人员:
- - `backend/api/internal/logic/client/staff/delivery.go:226-231`
-- 商城支付成功后使用相同 `request_no` 重试,会先因订单状态不再是待支付而失败,无法返回原结果:
- - `backend/api/internal/logic/client/user/shop.go:161-195`
-- 创建工单遇到重复 `request_no` 直接返回数据库错误:
- - `backend/api/internal/logic/client/user/address_ticket.go:98-109`
-- 打卡重复请求返回时,响应中的 `work_status` 根据本次请求重新计算,而不是根据既有记录:
- - `backend/api/internal/logic/client/staff/work.go:46-69`
-
-**影响**
-
-- 网络超时后的安全重试可能变成失败、误报成功,甚至返回另一订单的结果。
-
-**最小整改**
-
-- 幂等记录至少绑定:主体、资源、动作、请求载荷摘要和最终响应。
-- 相同幂等键但载荷不同必须返回稳定冲突错误;相同载荷返回已保存结果。
-
-### P2-02 服务 App 保存了签收幂等号,但提交时重新生成
-
-置信度:高
-
-**证据**
-
-- 草稿保存 `request_no`:
- - `apps/service_app/lib/ui/features/work/work_detail_page.dart:114-123`
-- 页面没有把该值传给仓库:
- - `apps/service_app/lib/ui/features/work/work_detail_page.dart:124-136`
-- 仓库在每次提交时重新生成 UUID:
- - `apps/service_app/lib/data/repositories/service_repository.dart:92-108`
-
-**影响**
-
-- 上传或请求响应丢失后,用户重试会使用新幂等键,可能重复形成签收动作。
-
-**最小整改**
-
-- `submitDeliveryReceipt` 必须接收并复用草稿中的 `requestNo`;删除草稿前保留服务端最终结果。
-
-### P2-03 商城订单的组织字段在真实创建流程中永远为零
-
-置信度:高
-
-**证据**
-
-- `EcOrder` 声明 `gas_station_id` 和 `delivery_point_id`:
- - `backend/api/internal/models/ec_order.go:15-16`
-- 真实下单创建 `EcOrder` 时未赋值:
- - `backend/api/internal/logic/client/user/shop.go:54-59`
-- 仅 Mock seed 为这两个字段赋值。
-
-**影响**
-
-- 按气站/配送点统计、权限范围、履约分派或结算会得到空组织。
-
-**最小整改**
-
-- 若商城订单必须归属机构,应在服务端从服务关系或商品归属中确定并写入快照。
-- 若业务确实为平台统一商城,应迁移后移除这两个误导字段及相关索引/展示。
-
-### P2-04 三个 Web 管理端以复制方式维护,且配送端包含整段不可达配置
-
-置信度:高
-
-**证据**
-
-- 三个 `src + scripts` 各约 1.9 万行。
-- 平台端与气站端有 80 个逐字节相同文件,约 18,280 行;平台端与配送端有 79 个相同文件,约 18,199 行。
-- 三端的 `CrudListPage.vue` 均为 1,175 行且内容相同。
-- `frontend/delivery_admin/src/api/resources.ts:287-384` 复制了完整平台资源定义。
-- `frontend/delivery_admin/src/api/resources.ts:386-426` 定义 `gasOverrides`,但导出只使用 `deliveryOverrides`:
- - `frontend/delivery_admin/src/api/resources.ts:428-470`
-
-**影响**
-
-- 一个通用缺陷需要在三处同步修复,极易发生漂移。
-- 配送包携带与本端无关的大量资源、字段和动作配置。
-
-**最小整改**
-
-- 将 API 客户端、会话、通用 CRUD、字段渲染、权限和契约检查提取到工作区共享包。
-- 各管理端只维护入口、主题和本端资源覆盖。
-- 直接删除前先用引用检查确认不可达;当前 `gasOverrides` 已可由导出链证明不可达。
-
-### P2-05 数据库自增 `id` 被作为公共 API 字段并在三端展示
-
-置信度:高
-
-**证据**
-
-- 公共实体把内部主键序列化为 `id`:
- - `backend/api/internal/models/entity.go:10-16`
-- 公共资源响应显式保留记录自身 `id`:
- - `backend/api/internal/logic/common/resource.go:450-486`
-- 三端通用列表固定显示 `ID` 列:
- - `frontend/platform_admin/src/views/shared/CrudListPage.vue:23-26`
-
-**影响**
-
-- `id` 与 `identity` 成为两个公开身份字段,增加前端误用和外部耦合。
-- 连续主键还会泄露记录规模和创建顺序。
-
-**最小整改**
-
-- 数据库 `id` 本身不是冗余字段,不应删除;应从 HTTP 响应和前端移除。
-- 迁移前检查外部调用方是否仍使用 `id`,提供兼容窗口。
-
-### P2-06 两个 Flutter API 客户端缺少超时和统一的 401 会话失效处理
-
-置信度:高
-
-**证据**
-
-- `apps/user_app/lib/data/services/api_client.dart:47-76`
-- `apps/service_app/lib/data/services/api_client.dart:41-80`
-- 请求直接等待 `_client.send`/上传结果,没有连接、读取或总超时。
-- 401 只抛异常,没有清理安全存储中的 Token。
-- 两个路由都仅以“Token 字符串非空”判断已登录:
- - `apps/user_app/lib/app/router.dart:14-21`
- - `apps/service_app/lib/app/router.dart:12-19`
-
-**影响**
-
-- 弱网下页面可能长期挂起。
-- Token 过期或被撤销后,用户会停留在已登录路由并持续收到请求错误。
-
-**最小整改**
-
-- 建立共享客户端层,统一超时、有限重试、取消、401 清会话和重新登录。
-- 资金写操作只能依赖幂等键重试,不能盲目重放。
-
-### P2-07 前后端契约检查只比较资源名、模式和路由字符串
-
-置信度:高
-
-**证据**
-
-- `frontend/platform_admin/scripts/check-backend-contract.mjs:12-37` 通过正则读取 `define(name, mode)`,再检查资源路径和路由源码是否包含字符串。
-- 未校验 HTTP 方法、动作路径、请求字段、必填项、枚举、响应字段和错误码。
-- 气站、配送端脚本采用同类实现。
-
-**影响**
-
-- `contract:check` 通过不能发现支付动作缺失、字段漂移或错误 HTTP 方法。
-
-**最小整改**
-
-- 使用结构化 OpenAPI/Schema 生成客户端与字段类型。
-- 至少对每个动作校验方法、路径、请求/响应结构,并增加真实路由契约测试。
-
-### P2-08 钱包创建存在并发竞态,银行卡敏感字段加密错误被忽略
-
-置信度:高
-
-**证据**
-
-- `ensureWallet` 采用先查后创建,唯一键冲突时不回查已有钱包:
- - `backend/api/internal/logic/common/client_wallet.go:41-57`
-- 首次并发请求时,一个请求可能收到数据库唯一约束错误。
-- 绑卡时只处理卡号加密错误,身份证和手机号加密错误被丢弃:
- - `backend/api/internal/logic/common/client_wallet.go:287-300`
-
-**影响**
-
-- 新用户并发访问钱包时出现偶发失败。
-- 加密异常可能生成空密文但仍写入银行卡记录,造成不可恢复的数据缺失。
-
-**最小整改**
-
-- 使用 `ON CONFLICT DO NOTHING` 后回查,或在事务中锁定所有者。
-- 每个敏感字段的加密错误都必须中止事务。
-
-## 5. P3:已证实或疑似冗余
-
-### P3-01 钱包第三方账号字段只有 Mock 数据写入
-
-置信度:中
-
-**证据**
-
-- `backend/api/internal/models/wallet_basic.go:11-14` 定义支付宝、微信账号及姓名四个字段。
-- 生产业务中未发现写入入口;仅 seed 使用。
-- Web 资源仍展示这些字段。
-
-**判断**
-
-- 当前属于“疑似冗余/未完成字段”,不能仅凭静态引用直接删除。
-
-**建议**
-
-- 核对线上非空率、导出消费者和未来渠道设计;确认不用后再做带回滚方案的迁移。
-
-### P3-02 Web 与状态 Store 保留多组本端不可达能力和模板字段
-
-置信度:中
-
-**证据**
-
-- 气站/配送端的通用平台 API 保留角色创建、角色菜单、平台账号等调用,但本端路由和资源未暴露这些页面。
-- 三端用户 Store 保留 `job`、`organization`、`location`、`email`、`introduction`、`personalWebsite` 等模板字段,实际登录资料只填充账号、名称、头像、角色和菜单。
-
-**建议**
-
-- 先用 TypeScript 引用分析和运行时埋点确认不可达,再删除本端无关方法和状态字段。
-- 共享 Store 只保留跨端最小会话模型,本端扩展单独声明。
-
-### P3-03 离线草稿读取重复解析同一加密文件
-
-置信度:高
-
-**证据**
-
-- `apps/service_app/lib/data/offline/encrypted_draft_store.dart:69-77` 先读取并 `jsonDecode` 外层载荷,随后再次读取文件并在 `_decrypt` 内再次解析。
-
-**影响**
-
-- 每次读草稿多一次文件读取和 JSON 解析,且前后两次读取理论上可能看到不同内容。
-
-**最小整改**
-
-- 读取一次字符串,完成结构校验后把同一值传给解密函数。
-
-## 6. 字段与表冗余结论
-
-| 对象 | 结论 | 依据 | 处理方式 |
-|---|---|---|---|
-| 数据库 `id` | 数据库内部必需,公共 API/UI 冗余 | API 同时提供 `identity`,UI仍显示 `id` | 保留数据库列;移除公开序列化和 UI 展示 |
-| `FinPayment` / `WalletPayment` / `GasorderPayment` | 已证实事实重叠,但不能直接删表 | 实际支付不写三表,看板却读其中一表 | 先确定唯一支付事实并迁移、对账 |
-| `EcOrder.GasStationID/DeliveryPointID` | 当前生产流程未填充 | 仅 seed 写入 | 明确归属规则后补写,或迁移删除 |
-| `WalletBasic` 支付宝/微信四字段 | 疑似冗余 | 仅 seed 写入 | 查线上非空率及外部消费者后决定 |
-| 配送端 `gasOverrides` | 已证实代码冗余 | 定义后未进入导出链 | 可删除,并用静态检查防回归 |
-| Web 会话模板字段 | 疑似冗余 | 未进入实际资料映射 | 引用和运行时确认后删除 |
-
-没有将“只在响应、展示或 seed 中出现”的字段直接判定为可删。数据库删字段、删表前必须补充:
-
-1. 线上非空率和取值分布;
-2. API 网关/访问日志中的字段消费者;
-3. 报表、导出、脚本和第三方集成引用;
-4. 双写/回填/回滚方案。
-
-## 7. 测试、静态检查与构建结果
-
-已实际执行:
-
-```text
-backend/api:
- go test ./... 通过
- go vet ./... 通过
- go build ./cmd/main/main.go 通过
-
-backend/worker:
- go test ./... 通过(无测试文件)
- go build ./cmd/main/main.go 通过
-
-backend/iot:
- go test ./... 通过(无测试文件)
- go build ./cmd/main/main.go 通过
-
-apps/user_app:
- flutter analyze 通过
- flutter test 通过(2 个测试)
-
-apps/service_app:
- flutter analyze 通过
- flutter test 通过(3 个测试)
-
-frontend/platform_admin:
- pnpm type:check 通过
- pnpm lint 退出码 0;112 warnings / 12 infos
- pnpm contract:check 通过(47 个资源)
- pnpm build 通过
-
-frontend/gas_admin:
- pnpm type:check 通过
- pnpm lint 退出码 0;105 warnings / 12 infos
- pnpm contract:check 通过(20 个资源)
- pnpm build 通过
-
-frontend/delivery_admin:
- pnpm type:check 通过
- pnpm lint 退出码 0;106 warnings / 12 infos
- pnpm contract:check 通过(18 个资源)
- pnpm build 通过
-```
-
-说明:
-
-- Web lint 的相当一部分告警来自工具无法识别 Vue 模板对 `script setup` 变量的使用,不能全部视为真实死代码;本报告仅列出能够从导出/调用链证明的冗余。
-- 后端虽有若干测试文件,但没有覆盖用户钱包支付、用户提现、配送提现、验证码完整流程、工作人员预检强制执行等高风险路径。
-- 两个 Flutter App 的测试主要覆盖模型和登录页面,不覆盖商城支付、离线签收重试、401 会话失效或完整作业流程。
-
-## 8. 建议整改顺序
-
-### 立即处置
-
-1. 关闭相关公网凭据并轮换全部已泄露密钥。
-2. 关闭生产资金写入口,审计历史余额和提现。
-3. 禁止生产环境启用固定验证码和 Mock 支付。
-
-### 第一阶段:资金与身份
-
-1. 重建钱包不变量、冻结额、统一流水和提现状态机。
-2. 合并支付事实,完成历史数据对账。
-3. 接入真实验证码并增加限流、审计和攻击测试。
-4. 强制服务端工作人员作业资格守卫。
-
-### 第二阶段:闭环与可靠性
-
-1. 完成用户 App 支付/取消/收货闭环。
-2. 修复所有幂等键的归属和载荷校验。
-3. 修复服务 App 草稿幂等号传递、401 会话和网络超时。
-4. 上线真实 Worker、IoT、对象存储及健康检查。
-
-### 第三阶段:去重与契约
-
-1. 抽取三个 Web 端共享包,删除不可达配置。
-2. 用结构化契约替换正则和源码字符串检查。
-3. 在确认线上数据和消费者后迁移疑似冗余字段/表。
-
-## 9. 审计限制
-
-- 未连接公网数据库、Redis、短信、支付、MQTT 或对象存储,未验证仓库中凭据是否仍有效。
-- 未启动依赖外部数据库的完整 E2E 流程,结论来自当前代码可证明的控制流、数据写入和客户端调用链。
-- 未使用 `docs/*` 推导功能缺口或业务要求。
-- 未修改任何业务源代码;只新增本报告。
+1. 确认 YAML 中的远程凭据仅指向隔离测试环境,并限制文件访问权限。
+2. 实现设备绑定、归属鉴权、安全事件、自动关阀、整改复检和开阀拦截。
+3. 为 ACK 增加持久化包号分配/命令相关标识及唯一活跃约束。
+4. 补齐 Worker、支付并发、Broker ACL/TLS 和跨进程故障恢复测试。
+5. 完成 BF-01~BF-15 的远程集成、真浏览器/Flutter E2E、支付沙箱和灾备演练后再评估发布。
diff --git a/backend/api/cmd/cli/main.go b/backend/api/cmd/cli/main.go
index 3100b03..b518d03 100644
--- a/backend/api/cmd/cli/main.go
+++ b/backend/api/cmd/cli/main.go
@@ -196,7 +196,7 @@ func migrateDatabase() error {
if err := prepareAdditiveMigrations(migrationDatabase, driver); err != nil {
return err
}
- if err := resetLegacyPaymentSchema(migrationDatabase, driver); err != nil {
+ if err := rejectLegacyPaymentSchema(migrationDatabase); err != nil {
return err
}
const legacyPhoneIndex = "idx_platform_account_phone"
@@ -216,25 +216,12 @@ func migrateDatabase() error {
return initdb.New(databaseService)
}
-// resetLegacyPaymentSchema 执行经业务明确授权的开发期破坏性支付模型重置,不迁移旧支付或退款历史。
-func resetLegacyPaymentSchema(databaseService *gorm.DB, driver string) error {
- statements := []string{}
- if driver == "postgres" {
- statements = append(statements,
- `DROP TABLE IF EXISTS "wallet_refund" CASCADE`,
- `DROP TABLE IF EXISTS "wallet_payment" CASCADE`,
- `DROP TABLE IF EXISTS "gasorder_payment" CASCADE`,
- )
- } else {
- statements = append(statements,
- `DROP TABLE IF EXISTS wallet_refund`,
- `DROP TABLE IF EXISTS wallet_payment`,
- `DROP TABLE IF EXISTS gasorder_payment`,
- )
- }
- for _, statement := range statements {
- if err := databaseService.Exec(statement).Error; err != nil {
- return fmt.Errorf("reset legacy payment schema: %w", err)
+// rejectLegacyPaymentSchema 阻止通用迁移物理删除历史资金表。
+// 旧支付事实必须通过单独评审的数据迁移保留和对账,不能由 migrate 命令静默重置。
+func rejectLegacyPaymentSchema(databaseService *gorm.DB) error {
+ for _, table := range []string{"wallet_refund", "wallet_payment", "gasorder_payment"} {
+ if databaseService.Migrator().HasTable(table) {
+ return fmt.Errorf("检测到历史资金表 %s:请先执行经财务与审计批准的保留式迁移", table)
}
}
return nil
diff --git a/backend/api/cmd/cli/main_test.go b/backend/api/cmd/cli/main_test.go
index a30b6f8..f71062b 100644
--- a/backend/api/cmd/cli/main_test.go
+++ b/backend/api/cmd/cli/main_test.go
@@ -1,18 +1,31 @@
package main
import (
+ "regexp"
"testing"
- "gorm.io/driver/sqlite"
+ "github.com/DATA-DOG/go-sqlmock"
+ "gorm.io/driver/postgres"
"gorm.io/gorm"
)
func TestPrepareAdditiveMigrationsSkipsMissingLegacyTable(t *testing.T) {
- databaseService, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
+ sqlDatabase, mock, err := sqlmock.New()
if err != nil {
- t.Fatalf("open sqlite database: %v", err)
+ t.Fatalf("open sql mock: %v", err)
+ }
+ defer sqlDatabase.Close()
+ mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM information_schema.tables WHERE table_schema = CURRENT_SCHEMA() AND table_name = $1 AND table_type = $2")).
+ WithArgs("gasorder_track_point", "BASE TABLE").
+ WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
+ databaseService, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
+ if err != nil {
+ t.Fatalf("open postgres mock: %v", err)
}
if err := prepareAdditiveMigrations(databaseService, "postgres"); err != nil {
t.Fatalf("missing legacy table should not require a backfill: %v", err)
}
+ if err := mock.ExpectationsWereMet(); err != nil {
+ t.Fatalf("unexpected migration query: %v", err)
+ }
}
diff --git a/backend/api/internal/config/config.go b/backend/api/internal/config/config.go
index c529839..5394e32 100644
--- a/backend/api/internal/config/config.go
+++ b/backend/api/internal/config/config.go
@@ -90,6 +90,9 @@ func New(srvKey string) {
Spec.BindIP = conf.CheckIP(Spec.BindIP)
Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port)
conf.NotNil(Spec.Service, Spec.Cache)
+ if Spec.Databases == nil || len(Spec.Databases.Source) == 0 {
+ panic("Databases configuration is required")
+ }
if Spec.Global.ManualRechargeMaxAmount <= 0 {
panic("Global.ManualRechargeMaxAmount must be greater than zero")
}
diff --git a/backend/api/internal/logic/iot/iot.go b/backend/api/internal/logic/iot/iot.go
index 64132dd..5397591 100644
--- a/backend/api/internal/logic/iot/iot.go
+++ b/backend/api/internal/logic/iot/iot.go
@@ -4,6 +4,7 @@ package iot
import (
"crypto/subtle"
"encoding/json"
+ "errors"
"net/http"
"strings"
"time"
@@ -187,7 +188,14 @@ func SaveDeviceMessage(ctx *gin.Context) {
if frame.Control&0x04 != 0 {
status, errorCode = "failed", "DEVICE_REPORTED_FAILURE"
}
- return tx.Model(&models.IotCommand{}).Where("device_id = ? AND packet_number = ? AND command_status IN ?", envelope.DeviceID, frame.PacketNumber, []string{"dispatched", "pending_confirmation"}).Updates(map[string]any{"command_status": status, "error_code": errorCode, "acknowledged_at": received, "updated_at": received}).Error
+ result := tx.Model(&models.IotCommand{}).Where("device_id = ? AND packet_number = ? AND command_status IN ?", envelope.DeviceID, frame.PacketNumber, []string{"dispatched", "pending_confirmation"}).Updates(map[string]any{"command_status": status, "error_code": errorCode, "acknowledged_at": received, "updated_at": received})
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected > 1 {
+ return errors.New("ambiguous device acknowledgement")
+ }
+ return nil
})
if err != nil {
ctx.JSON(500, gin.H{"code": "IOT_MESSAGE_SAVE_FAILED"})
diff --git a/backend/api/internal/logic/payment/callback.go b/backend/api/internal/logic/payment/callback.go
index 10ca1ee..35b8d57 100644
--- a/backend/api/internal/logic/payment/callback.go
+++ b/backend/api/internal/logic/payment/callback.go
@@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"strconv"
+ "strings"
"time"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
@@ -80,12 +81,18 @@ func complete(paymentNo, tradeNo string, amount int64, channel, callbackDigest s
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("payment_no = ?", paymentNo).First(&order).Error; err != nil {
return err
}
- if order.PaymentStatus == StatusPaid {
- return nil
- }
if order.Channel != channel || order.Amount != amount {
return errors.New("payment identity or amount mismatch")
}
+ if strings.TrimSpace(tradeNo) == "" {
+ return errors.New("channel trade number is required")
+ }
+ if order.PaymentStatus == StatusPaid {
+ if order.ChannelTradeNo != tradeNo {
+ return errors.New("duplicate callback trade number mismatch")
+ }
+ return nil
+ }
if time.Now().After(order.ExpiresAt) {
return tx.Model(&order).Updates(map[string]any{"payment_status": 50, "channel_trade_no": tradeNo, "callback_digest": callbackDigest, "failure_code": "PAID_AFTER_EXPIRED"}).Error
}
@@ -95,9 +102,10 @@ func complete(paymentNo, tradeNo string, amount int64, channel, callbackDigest s
}
switch order.BusinessType {
case "ec_order":
- return tx.Model(&models.EcOrder{}).Where("identity = ? AND order_status = ?", order.BusinessIdentity, 16).Updates(map[string]any{"order_status": 18, "paid_at": &now}).Error
+ result := tx.Model(&models.EcOrder{}).Where("identity = ? AND order_status = ?", order.BusinessIdentity, 16).Updates(map[string]any{"order_status": 18, "paid_at": &now})
+ return requireSingleBusinessUpdate(result)
case "gasorder":
- return tx.Model(&models.GasorderBasic{}).Where("identity = ? AND order_status IN ?", order.BusinessIdentity, []int{16, 18}).Update("order_status", 35).Error
+ return requireSingleBusinessUpdate(tx.Model(&models.GasorderBasic{}).Where("identity = ? AND order_status IN ?", order.BusinessIdentity, []int{16, 18}).Update("order_status", 35))
case "recharge":
return completeRecharge(tx, order, now)
}
@@ -105,6 +113,16 @@ func complete(paymentNo, tradeNo string, amount int64, channel, callbackDigest s
})
}
+func requireSingleBusinessUpdate(result *gorm.DB) error {
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected != 1 {
+ return errors.New("payment business state conflict")
+ }
+ return nil
+}
+
func completeRecharge(tx *gorm.DB, payment models.PaymentOrder, now time.Time) error {
var recharge models.WalletRechargeOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND recharge_status = ?", payment.BusinessIdentity, 10).First(&recharge).Error; err != nil {
diff --git a/backend/api/internal/logic/payment/callback_test.go b/backend/api/internal/logic/payment/callback_test.go
new file mode 100644
index 0000000..2adb3f0
--- /dev/null
+++ b/backend/api/internal/logic/payment/callback_test.go
@@ -0,0 +1,21 @@
+package payment
+
+import (
+ "errors"
+ "testing"
+
+ "gorm.io/gorm"
+)
+
+func TestRequireSingleBusinessUpdate(t *testing.T) {
+ if err := requireSingleBusinessUpdate(&gorm.DB{RowsAffected: 1}); err != nil {
+ t.Fatalf("single state update rejected: %v", err)
+ }
+ if err := requireSingleBusinessUpdate(&gorm.DB{}); err == nil {
+ t.Fatal("zero-row state update must be rejected")
+ }
+ expected := errors.New("database unavailable")
+ if err := requireSingleBusinessUpdate(&gorm.DB{Error: expected}); !errors.Is(err, expected) {
+ t.Fatalf("database error lost: %v", err)
+ }
+}
diff --git a/backend/api/internal/logic/payment/close.go b/backend/api/internal/logic/payment/close.go
index 52a8155..7f195f3 100644
--- a/backend/api/internal/logic/payment/close.go
+++ b/backend/api/internal/logic/payment/close.go
@@ -2,6 +2,7 @@ package payment
import (
"context"
+ "crypto/subtle"
"fmt"
"net/http"
"time"
@@ -55,7 +56,9 @@ func closeChannelOrder(ctx context.Context, order models.PaymentOrder) error {
// CloseExpiredHandler 只接受 Worker 共享凭证,不暴露为平台用户动作。
func CloseExpiredHandler(ctx *gin.Context) {
- if config.Spec.Payment.InternalServiceToken == "" || ctx.GetHeader("X-Heqi-Worker-Token") != config.Spec.Payment.InternalServiceToken {
+ expected := config.Spec.Payment.InternalServiceToken
+ actual := ctx.GetHeader("X-Heqi-Worker-Token")
+ if expected == "" || len(actual) != len(expected) || subtle.ConstantTimeCompare([]byte(actual), []byte(expected)) != 1 {
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
diff --git a/backend/iot-server/internal/service/service.go b/backend/iot-server/internal/service/service.go
index 56af9b4..40073d0 100644
--- a/backend/iot-server/internal/service/service.go
+++ b/backend/iot-server/internal/service/service.go
@@ -32,6 +32,7 @@ type Service struct {
packet atomic.Uint32
http *http.Server
ready atomic.Bool
+ client *http.Client
}
type Command struct {
Identity string `json:"identity"`
@@ -97,7 +98,7 @@ func New(cfg config.Config) (*Service, error) {
if err = broker.AddListener(listeners.NewTCP(listenerConfig)); err != nil {
return nil, fmt.Errorf("配置 MQTT 监听器: %w", err)
}
- return &Service{cfg: cfg, keys: keys, broker: broker}, nil
+ return &Service{cfg: cfg, keys: keys, broker: broker, client: &http.Client{Timeout: 12 * time.Second}}, nil
}
func makeTLSConfig(cfg config.Config) (*tls.Config, error) {
@@ -222,10 +223,14 @@ func (s *Service) command(w http.ResponseWriter, r *http.Request) {
func (s *Service) onMessage(_ *mqtt.Client, _ packets.Subscription, message packets.Packet) {
raw := append([]byte(nil), message.Payload...)
- envelope := Envelope{Type: "device_message", Topic: message.TopicName, ReceivedAt: time.Now().UTC().Format(time.RFC3339Nano), PayloadHex: hex.EncodeToString(raw)}
+ expectedDeviceID, topicOK := deviceIDFromTopic(s.cfg, message.TopicName)
+ envelope := Envelope{Type: "device_message", Topic: message.TopicName, DeviceID: expectedDeviceID, ReceivedAt: time.Now().UTC().Format(time.RFC3339Nano), PayloadHex: hex.EncodeToString(raw)}
if frame, err := protocol.Decode(raw, s.keys); err == nil {
deviceID := hex.EncodeToString(frame.DeviceID[:])
- envelope.DeviceID = deviceID
+ if !topicOK || deviceID != expectedDeviceID {
+ s.forwardEnvelope(envelope)
+ return
+ }
main := byte(0)
if len(frame.Payload) > 0 {
main = frame.Payload[0]
@@ -238,6 +243,10 @@ func (s *Service) onMessage(_ *mqtt.Client, _ packets.Subscription, message pack
}
envelope.Frame = decoded
}
+ s.forwardEnvelope(envelope)
+}
+
+func (s *Service) forwardEnvelope(envelope Envelope) {
data, _ := json.Marshal(envelope)
request, err := http.NewRequest(http.MethodPost, s.cfg.HTTP.CallbackURL, bytes.NewReader(data))
if err != nil {
@@ -245,12 +254,38 @@ func (s *Service) onMessage(_ *mqtt.Client, _ packets.Subscription, message pack
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Heqi-Iot-Token", s.cfg.HTTP.InternalToken)
- response, err := http.DefaultClient.Do(request)
+ response, err := s.client.Do(request)
if err == nil {
_ = response.Body.Close()
}
}
+func deviceIDFromTopic(cfg config.Config, topic string) (string, bool) {
+ for _, pattern := range []string{cfg.MQTT.UpTopic, cfg.MQTT.AckTopic} {
+ parts := strings.Split(pattern, "/")
+ values := strings.Split(topic, "/")
+ if len(parts) != len(values) {
+ continue
+ }
+ deviceID := ""
+ matched := true
+ for index := range parts {
+ if parts[index] == "+" {
+ deviceID = values[index]
+ continue
+ }
+ if parts[index] != values[index] {
+ matched = false
+ break
+ }
+ }
+ if matched && len(deviceID) == 16 {
+ return strings.ToLower(deviceID), true
+ }
+ }
+ return "", false
+}
+
func decodeDeviceID(value string) ([8]byte, error) {
var result [8]byte
decoded, err := hex.DecodeString(value)
diff --git a/backend/iot-server/internal/service/service_test.go b/backend/iot-server/internal/service/service_test.go
new file mode 100644
index 0000000..3a4c708
--- /dev/null
+++ b/backend/iot-server/internal/service/service_test.go
@@ -0,0 +1,22 @@
+package service
+
+import (
+ "testing"
+
+ "git.apinb.com/heqiapp/platforms/backend/iot-server/internal/config"
+)
+
+func TestDeviceIDFromTopic(t *testing.T) {
+ cfg := config.Config{MQTT: config.MQTT{UpTopic: "devices/+/up", AckTopic: "devices/+/ack"}}
+ for _, topic := range []string{"devices/0000000000000001/up", "devices/0000000000000001/ack"} {
+ identity, ok := deviceIDFromTopic(cfg, topic)
+ if !ok || identity != "0000000000000001" {
+ t.Fatalf("topic %s: identity=%q ok=%v", topic, identity, ok)
+ }
+ }
+ for _, topic := range []string{"devices/0001/up", "devices/0000000000000001/down", "other/0000000000000001/up"} {
+ if identity, ok := deviceIDFromTopic(cfg, topic); ok || identity != "" {
+ t.Fatalf("invalid topic %s accepted as %q", topic, identity)
+ }
+ }
+}
diff --git a/backend/worker/cmd/main/main.go b/backend/worker/cmd/main/main.go
index a0d8e4d..94abdb2 100644
--- a/backend/worker/cmd/main/main.go
+++ b/backend/worker/cmd/main/main.go
@@ -31,6 +31,7 @@ func main() {
func closeExpiredPayments(ctx context.Context) {
ticker := time.NewTicker(time.Duration(config.Spec.PaymentAPI.IntervalSeconds) * time.Second)
defer ticker.Stop()
+ client := &http.Client{Timeout: 15 * time.Second}
for {
select {
case <-ctx.Done():
@@ -41,7 +42,7 @@ func closeExpiredPayments(ctx context.Context) {
continue
}
request.Header.Set("X-Heqi-Worker-Token", config.Spec.PaymentAPI.Token)
- response, err := http.DefaultClient.Do(request)
+ response, err := client.Do(request)
if err == nil {
_ = response.Body.Close()
}
diff --git a/backend/worker/internal/config/config.go b/backend/worker/internal/config/config.go
index eb15533..63405a0 100644
--- a/backend/worker/internal/config/config.go
+++ b/backend/worker/internal/config/config.go
@@ -4,6 +4,8 @@ package config
import (
"git.apinb.com/bsm-sdk/core/conf"
"net"
+ "os"
+ "strings"
)
var Spec SrvConfig
@@ -30,10 +32,28 @@ type IoTAPIConfig struct {
func New(srvKey string) {
conf.New(srvKey, &Spec)
+ if databaseDSN := strings.TrimSpace(os.Getenv("HEQI_DATABASE_DSN")); databaseDSN != "" {
+ if Spec.Databases == nil {
+ panic("Databases configuration is required")
+ }
+ Spec.Databases.Source = []string{databaseDSN}
+ }
+ if redisURL := strings.TrimSpace(os.Getenv("HEQI_REDIS_URL")); redisURL != "" {
+ Spec.Cache = redisURL
+ }
+ if token := strings.TrimSpace(os.Getenv("HEQI_PAYMENT_INTERNAL_TOKEN")); token != "" {
+ Spec.PaymentAPI.Token = token
+ }
+ if token := strings.TrimSpace(os.Getenv("HEQI_IOT_INTERNAL_TOKEN")); token != "" {
+ Spec.IoTAPI.Token = token
+ }
Spec.Port = conf.CheckPort(Spec.Port)
Spec.BindIP = conf.CheckIP(Spec.BindIP)
Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port)
conf.NotNil(Spec.Service, Spec.Cache)
+ if Spec.Databases == nil || len(Spec.Databases.Source) == 0 {
+ panic("Databases configuration is required")
+ }
if Spec.PaymentAPI.BaseURL == "" || Spec.PaymentAPI.Token == "" || Spec.PaymentAPI.IntervalSeconds <= 0 {
panic("PaymentAPI configuration is required")
}
diff --git a/checking/02-总体测试计划.md b/checking/02-总体测试计划.md
index d14439c..42b866c 100644
--- a/checking/02-总体测试计划.md
+++ b/checking/02-总体测试计划.md
@@ -2,7 +2,7 @@
## 1. 目标
-本计划用于验证当前已实现的两类 App、三个业务管理端、官网以及 API/Worker/IoT 三个后端进程,确保:
+本计划用于验证当前已实现的两类 App、三个业务管理端、官网以及 API、Worker、IoT Gateway、IoT Server 四个后端进程,确保:
- 业务主责与数据边界正确,没有通过其他终端越权改写事实。
- 设备控制、安全事件、订单履约、资金和审计形成可追踪闭环。
@@ -22,10 +22,11 @@
| Web | `frontend/delivery_admin` | 配送点人员、用户、地址、订单、资金、报表和组织隔离 |
| Web | `frontend/site` | 官网内容、导航、响应式、静态资源、可访问性、SEO 基础、构建与托管 Worker |
| 后端 | `backend/api` | Gin API/BFF、JWT、权限、同步事务、状态动作、金额、幂等、审计、上传和数据库事实 |
-| 后端 | `backend/worker` | 配置、启动、Mock 循环、优雅退出;未来 Outbox/Streams 的投递、重试、死信和积压 |
-| 后端 | `backend/iot-gateway`、`backend/iot-server` | 配置、启动、无状态转发、协议编解码、MQTT TLS、身份、遥测、回执和断线重连 |
+| 后端 | `backend/worker` | 支付超时关单、IoT Outbox 认领与投递、重试触发、内部令牌、超时和优雅退出 |
+| 后端 | `backend/iot-gateway` | 无状态命令转发、上行回调转发、内部鉴权、过期拦截、请求体限制和上游故障映射 |
+| 后端 | `backend/iot-server` | Mochi MQTT Broker、每设备认证与 Topic ACL、V1.8 帧/AES/LRC8、上下行、回执和遥测解析 |
-不修改 `sample/`。真实支付、短信、地图、对象存储、电子合同、MQTT 厂商接入等,以受控沙箱或 Mock 验证;生产联调另设上线前检查点。
+真实支付、短信、地图、对象存储、电子合同和 MQTT 厂商硬件等,以受控沙箱或模拟器验证;生产联调另设上线前检查点。
## 3. 测试分层
@@ -35,10 +36,10 @@
| L1 单元测试 | 状态机、金额、权限、幂等、序列化、ViewModel、领域规则 | 每次提交;关键规则 100% 分支覆盖 | 函数/规则 |
| L2 组件/接口测试 | Router+Logic+DB、Vue/Flutter 组件、上传边界、错误码 | 每次合并请求 | 组件/API |
| L3 契约测试 | 后端路由、资源契约、请求响应 Schema、客户端 claim | 每次接口变更 | 契约消费者/提供者 |
-| L4 集成测试 | API+PostgreSQL+Redis/Mock、Worker、IoT、对象存储替身 | 每日或候选版 | 服务/依赖 |
+| L4 集成测试 | API+PostgreSQL+Redis、Worker、Gateway、MQTT Server、设备模拟器、对象存储替身 | 每日或候选版 | 服务/依赖 |
| L5 跨端 E2E | App/Web 经真实 API 完成跨角色流程 | 每日冒烟、候选版全回归 | 业务链路 |
| L6 专项测试 | 安全、性能、稳定性、弱网、兼容性、可访问性、灾备 | 候选版 | 系统能力 |
-| L7 UAT/发布验证 | AC-01~AC-24、运营/财务/安全签字、生产冒烟 | 发布前后 | 验收口径 |
+| L7 UAT/发布验证 | AC-01~AC-25、运营/财务/安全签字、生产冒烟 | 发布前后 | 验收口径 |
## 4. 核心测试设计原则
@@ -78,6 +79,8 @@
- 金额均为最小货币单位整数,覆盖 0、边界值、超额、并发与溢出。
- 充值、支付、退款、提现预扣/驳回/打款、结算、对账的事务一致性和不可变流水。
- 回调签名、金额、订单、商户身份、重复/乱序回调和伪造回调。
+- 统一 `payment_order` 创建、客户端参数最小化、超时关单、过期后支付进入异常待确认。
+- 用户退款按订单项和数量校验退款窗口、履约状态与累计金额;审核通过后同事务入钱包并写不可变流水。
- 始终满足 `0 <= 可提现余额 <= 总余额`,申请人不能篡改审核和打款状态。
### 5.5 内容、上传与隐私
@@ -102,10 +105,18 @@
- 敏感字段加密、日志/崩溃报告/缓存/剪贴板/截图风险,Release 包配置和调试入口。
- MQTT 设备身份、TLS、主题 ACL、伪造遥测和命令回执在真实接入后专项验证。
+### IoT 协议与链路
+
+- API 创建命令与 Outbox 必须同事务;相同幂等键返回同一命令,不产生第二条待投递事实。
+- Worker 使用 `SKIP LOCKED` 认领 Outbox,经 Gateway 投递至 IoT Server;失败按状态与可用时间重试,卡在 processing 的记录可被重新认领。
+- Gateway 不保存业务事实;校验内部令牌、命令过期时间、动作和请求体大小,并正确映射上游拒绝或不可用。
+- IoT Server 验证每设备凭证和 Topic ACL;V1.8 帧覆盖边界、长度、大端序、AES-128、LRC8、BCD 时间、实时数据块和阀门动作编码。
+- ACK 依据设备号、包序号和控制位更新命令;无 ACK 保持待确认,重复 ACK 不产生重复命令或业务动作。
+
### 可靠性与恢复
-- API、Worker、IoT 重启;数据库/Redis/对象存储/MQTT 短时不可用;网络分区与时钟偏移。
-- Outbox、消费幂等、重试、死信和积压只在持久化消息契约落地后作为上线硬门禁。
+- API、Worker、IoT Gateway、IoT Server 重启;数据库/Redis/对象存储/MQTT 短时不可用;网络分区与时钟偏移。
+- 当前 IoT Outbox 的认领、processing 超时回收、失败重试和积压必须测试;死信上限与 Redis Streams 唤醒尚未落地,作为上线待决项记录。
- 备份恢复演练验证核心数据完整性,并记录实测 RPO/RTO;目标值需业务确认。
### 兼容性与可访问性
@@ -127,14 +138,14 @@
- 测试负责人:版本范围、计划、排期、风险、日报和发布建议。
- 开发负责人:单元/接口测试、可测试性、数据脚本、日志和缺陷修复。
-- 产品/业务:确认正常与异常口径,签署 AC-01~AC-24。
+- 产品/业务:确认正常与异常口径,签署 AC-01~AC-25。
- 安全/财务/运维:分别签署高风险设备与隐私、资金、性能/备份/灾备结果。
- 每轮必备产物:版本清单、环境清单、用例集、自动化报告、专项报告、缺陷清单、复测证据、残余风险和签字记录。
## 9. 完成标准
-- AC-01~AC-24 均有用例、执行结果和证据,没有“未覆盖但默认通过”。
+- AC-01~AC-25 均有用例、执行结果和证据,没有“未覆盖但默认通过”。
- P0/P1 为 0;P2 延期均有责任人、截止时间、影响和风险接受人。
- 所有受影响模块的静态检查、单元、契约和构建通过;关键链路 E2E 通过。
- 设备、安全、订单、支付、提现、检查、整改、敏感访问的审计可按 `request_id`/`identity` 串联。
-- 安全、性能、弱网/离线和恢复专项完成;未具备真实依赖的项目明确标记“受限通过”,不得写成已完成生产验收。
+- 安全、性能、弱网/离线和恢复专项完成;IoT 模拟器、真实设备、TLS/证书与厂商联调分层记录,模拟通过不得写成真实设备验收通过。
diff --git a/checking/03-系统与业务覆盖矩阵.md b/checking/03-系统与业务覆盖矩阵.md
index e8facc5..f06cd6f 100644
--- a/checking/03-系统与业务覆盖矩阵.md
+++ b/checking/03-系统与业务覆盖矩阵.md
@@ -5,8 +5,9 @@
| 流程 | 参与端/进程 | 必测断言 |
| --- | --- | --- |
| 用户注册与服务归属 | user_app → API → gas/delivery admin | 邀请范围/时效/次数/地址校验;已有账号不重复创建;归因可审计 |
-| 设备绑定与远控 | user_app/platform_admin → API → IoT → 设备 Mock | 归属鉴权、幂等键、命令 ID、回执、超时待确认、审计;高风险开阀拦截 |
-| 告警整改闭环 | IoT → API/Worker → user_app/service_app/platform_admin | 事件分级、自动关阀、通知、SLA、派单、证据、复检、关闭与全链路审计 |
+| 设备绑定与远控 | user_app/platform_admin → API → Outbox → Worker → Gateway → IoT Server → MQTT 设备 | 归属鉴权、事务、幂等键、命令 ID、包序号、回执、超时待确认、审计;高风险开阀拦截 |
+| 告警整改闭环 | MQTT 设备 → IoT Server → Gateway → API/Worker → user_app/service_app/platform_admin | 原始帧、设备/接收时间、事件分级、自动关阀、通知、SLA、派单、证据、复检、关闭与审计 |
+| 渠道支付与退款 | user_app → API → 支付渠道 → API → platform_admin → 钱包 | 支付单幂等、验签、金额/商户、超时关单、重复/迟到回调、按项退款、审核与不可变流水 |
| 商城订单履约 | user_app → API → gas_admin/delivery_admin → service_app | 整数金额、库存/合同规则、幂等支付、任务拆分、归属、轨迹、签收、状态聚合 |
| 安装/维修/安检 | admin → API → service_app → API | 准入、上班、区域、强制证据、异常/恢复、离线补传、服务端完成校验 |
| 钱包与提现 | user_app/service_app/admin → API | 支付密码、并发余额、不可变流水、预扣/退回/打款、审核隔离、审计 |
@@ -22,11 +23,12 @@
| gas_admin | 资源页、合同/订单动作、菜单与数据范围 | gas 资源 JSON 与后端一致 | 配送点、人员、用户、合同、订单、财务、工单 | 跨气站隔离、并发动作、导出审计 |
| delivery_admin | 资源页、订单/人员动作、菜单与数据范围 | delivery 资源 JSON 与后端一致 | 人员、用户、地址、配送、钱包、结算 | 跨配送点隔离、轨迹隐私、并发动作 |
| site | 页面组件、托管 Worker | 静态资源/路由/缓存头 | 导航、CTA、错误页 | 响应式、Safari、可访问性、SEO、性能 |
-| API | Logic、状态机、金额、幂等、权限、模型约束 | Router+DB、错误码、Schema、上传 | 所有跨端业务流 | 安全、并发、性能、故障注入、审计 |
-| Worker | 事件处理器(落地后) | Outbox/Streams 契约(落地后) | 告警/通知/对账/超时 | 重试、死信、积压、重启、至少一次投递 |
-| IoT | 协议解析/校验(落地后) | MQTT topic/payload/回执契约(落地后) | 遥测→告警、命令→回执 | TLS、ACL、断线重连、伪造、乱序、容量 |
+| API | Logic、状态机、支付/退款、金额、幂等、权限、IoT 命令/Outbox/上行模型 | Router+DB、错误码、Schema、上传、内部接口 | 所有跨端业务流 | 安全、并发、性能、故障注入、审计 |
+| Worker | 支付关单调度、IoT Outbox 轮询与投递 | API/Gateway 内部接口及令牌 | 过期支付、命令可靠投递 | 重试、积压、重启、重复认领、上游超时 |
+| IoT Gateway | 命令/上行 HTTP 转发 | Worker/API/IoT Server 内部契约 | 命令下行与设备消息上行 | 无状态、令牌、过期、体积限制、上游故障 |
+| IoT Server | V1.8 帧、AES/LRC8、阀门命令、实时数据解析 | MQTT Topic/ACL 与内部 HTTP 命令契约 | 遥测→持久化、命令→回执 | TLS、设备认证、断线重连、伪造、乱序、容量 |
-## 3. AC-01~AC-24 追踪矩阵
+## 3. AC-01~AC-25 追踪矩阵
| AC | 场景摘要 | 主测模块 | 关键负向/异常覆盖 |
| --- | --- | --- | --- |
@@ -54,6 +56,7 @@
| AC-22 | 设备共享/紧急联系人 | user_app/API/Worker | 权限撤销后缓存、成员越权控制、联系人未授权 |
| AC-23 | 二维码/轨迹隐私 | apps/API/admin | 二维码泄密、精确轨迹直链、跨订单访问、无审计导出 |
| AC-24 | 钱包实体命名一致性 | DB/Go/Flutter/Vue/契约 | 同义表/模型/Schema、内部 ID 暴露、迁移兼容错误 |
+| AC-25 | MQTT 设备命令与协议回执 | API/Worker/Gateway/IoT Server/设备模拟器 | 事务中断、重复认领、Gateway 超时、命令过期、ACL 越权、坏帧、ACK 丢失/重复/伪造 |
## 4. 数据与接口不变量
@@ -63,4 +66,3 @@
- 状态只能经专用动作接口流转,非法跳转返回稳定错误码。
- 客户端隐藏按钮、菜单或本地缓存不作为安全断言,必须直接调用 API 验证服务端拒绝。
- 所有关键写入验证幂等、事务原子性、审计完整性及失败后的可查询状态。
-
diff --git a/checking/04-环境数据与依赖计划.md b/checking/04-环境数据与依赖计划.md
index 079a6ef..98cb294 100644
--- a/checking/04-环境数据与依赖计划.md
+++ b/checking/04-环境数据与依赖计划.md
@@ -6,7 +6,7 @@
| --- | --- | --- | --- |
| Local | 单元、组件、快速接口调试 | 自动生成、可重置 | 内存/本地 Mock |
| CI | 静态、单元、契约、构建 | 每任务独立数据库 | 容器化替身,禁止共享状态 |
-| Integration | API+DB+Redis+Worker+IoT 集成 | 固定基线+场景数据 | 沙箱或可控 Mock,支持故障注入 |
+| Integration | API+DB+Redis+Worker+Gateway+MQTT Server 集成 | 固定基线+场景数据 | 支付沙箱、设备模拟器和故障注入 |
| Staging | 全端 E2E、专项、UAT | 脱敏仿真数据 | 尽量接真实沙箱、设备模拟器 |
| Production | 发布后只读/低风险冒烟 | 生产数据 | 禁止造资金与高风险设备动作,除非有演练审批 |
@@ -17,7 +17,10 @@
- PostgreSQL:每轮可重置的独立库;启用约束、事务、时区和并发测试;保留慢查询与锁等待观测。
- Redis:验证码、限流、支付密码锁定及未来事件流;提供清空命名空间、断连、延迟和重启能力。
- API:默认基线 `http://localhost:12426`;按平台、气站、配送点、用户、服务人员分别生成令牌。
-- Worker/IoT:当前执行启动、配置、健康、日志和优雅退出验证;真实消息/协议接入后扩充专用环境。
+- Worker:配置支付扫描与 IoT 轮询间隔、API/Gateway 地址和内部令牌;支持加速时钟、上游超时与重启测试。
+- IoT Gateway:独立端口、API/IoT Server 上游地址和内部令牌;测试中确认不持久化业务事实。
+- IoT Server:独立 HTTP/MQTT 端口、Mochi Broker、V1.8 AES 密钥环、逐设备 Client ID/设备号/凭证、Topic ACL;集成环境使用测试证书启用 MQTT TLS。
+- 设备模拟器:至少两台合法设备和一台攻击设备,支持 `up/ack/down`、断线重连、重复包、乱序包、坏 LRC、错误 AES key、ACK 成功/失败/丢失。
- 对象存储替身:支持上传、下载授权、过期、拒绝、超时、损坏和病毒扫描结果模拟。
- 时间控制:支持验证码过期、SLA、合同、证照、订单、提现及幂等窗口的时间推进。
@@ -51,7 +54,7 @@
| 依赖 | 必备模拟能力 |
| --- | --- |
-| MQTT/设备 | 在线/离线、命令 ACK/拒绝/超时、重复回执、伪造设备、遥测峰值 |
+| MQTT/设备 | 每设备认证、Topic ACL、在线/离线、命令 ACK/拒绝/超时、重复回执、伪造设备、坏帧、遥测峰值 |
| 支付 | 签名正确/错误、金额/订单/商户不符、重复/乱序回调、查询补偿 |
| 短信/推送 | 成功、失败、延迟、重复、限流、部分接收人失败 |
| 地图/定位 | 正常、低精度、权限拒绝、漂移、区域外、无位置、后台受限 |
@@ -68,4 +71,3 @@
## 7. 待确认项
以下内容在得到正式口径前只测试“可配置与安全默认值”,不自行设定政策:安全阈值与开阀审批、SLO/RPO/RTO 数值、目标设备规模和上报频率、支付/合同/地图厂商、证据留存期、支持的 OS/浏览器最低版本、视频合规规则。
-
diff --git a/checking/05-执行批次与质量门禁.md b/checking/05-执行批次与质量门禁.md
index f489f43..d67a745 100644
--- a/checking/05-执行批次与质量门禁.md
+++ b/checking/05-执行批次与质量门禁.md
@@ -8,7 +8,7 @@
| B1 快速门禁 | 静态、单元、契约、构建 | 每次提交/合并请求 | 尽早阻断低层错误 |
| B2 API 回归 | 鉴权、CRUD、状态动作、金额、幂等、审计、上传 | 每日 | 后端事实层稳定 |
| B3 单端回归 | 两 App、三个管理端、官网 | 每日/候选版 | 单端功能和兼容性 |
-| B4 跨端 E2E | 7 条主流程、AC-01~AC-24 | 候选版 | 业务闭环可验收 |
+| B4 跨端 E2E | `06-业务流程测试.md`、AC-01~AC-25 | 候选版 | 业务闭环可验收 |
| B5 专项 | 安全、性能、弱网、稳定性、灾备、可访问性 | 候选版 | 非功能风险可接受 |
| B6 UAT | 产品、运营、安全、财务、运维签署 | 发布前 | 业务与上线决策完成 |
| B7 发布验证 | 部署校验、迁移、低风险冒烟、监控和回滚 | 发布时 | 生产可用且可回退 |
@@ -80,9 +80,9 @@ cd backend/iot-server && go test ./... && go vet ./... && go build ./cmd/main/ma
### 候选版
- 全模块构建通过,B2/B3 全回归完成。
-- 7 条跨端主流程和 AC-01~AC-24 全部执行。
+- `06-业务流程测试.md` 中 P0/P1 流程和 AC-01~AC-25 全部执行。
- P0/P1 为 0;性能、安全、弱网和恢复专项已出报告。
-- Worker/IoT 的 Mock 限制在报告中明确,未完成真实联调不伪装成通过。
+- IoT 模拟器、真实设备、TLS/证书和厂商联调的通过层级在报告中明确,不混写结论。
### 生产发布
@@ -92,13 +92,13 @@ cd backend/iot-server && go test ./... && go vet ./... && go build ./cmd/main/ma
## 5. 冒烟清单
-1. API/Worker/IoT 启动、健康、配置加载和无敏感日志。
+1. API、Worker、IoT Gateway、IoT Server 启动、健康、配置加载和无敏感日志。
2. 五类客户端登录及 client claim 隔离。
3. 三个管理端菜单加载、核心列表、详情和一个低风险写操作。
4. user_app 读取首页/商品/订单,service_app 读取准入/任务。
5. 一次幂等写入重复提交,事实不重复。
6. 一次跨组织 identity 访问,被稳定错误码拒绝。
-7. 一次设备命令 Mock 回执和一次超时待确认。
+7. 一次完整 IoT 命令下行与 MQTT 回执,以及一次 ACK 丢失后的待确认。
8. 一次订单到任务的跨端可见性校验。
9. 一次敏感字段脱敏和审计查询。
10. 官网首页、主要导航、静态资源与错误页。
@@ -108,7 +108,7 @@ cd backend/iot-server && go test ./... && go vet ./... && go build ./cmd/main/ma
| 优先级 | 建设项 | 原因 |
| --- | --- | --- |
| P0 | API 鉴权/组织隔离/状态机/金额/幂等集成测试 | 当前最高业务与安全风险 |
-| P0 | AC-02、03、04、07、08、09、21 跨端回归 | 设备、安全、资金、隔离、离线核心链路 |
+| P0 | AC-02、03、04、07、08、09、21、25 跨端回归 | 设备、安全、资金、隔离、离线和 IoT 可靠性核心链路 |
| P1 | App Repository/ViewModel/离线草稿及 Web 权限组件测试 | 当前端侧自动化覆盖较薄 |
| P1 | 三管理端契约检查统一 CI、浏览器冒烟 | 防止路由/菜单/资源漂移 |
| P1 | 上传、日志脱敏、依赖与密钥扫描 | 隐私与供应链风险 |
@@ -119,7 +119,17 @@ cd backend/iot-server && go test ./... && go vet ./... && go build ./cmd/main/ma
- 计划/执行/通过/失败/阻塞/未执行用例数及覆盖率。
- 按模块和 P0~P3 的新增、遗留、关闭、重开缺陷。
-- AC-01~AC-24 状态,关键链路通过率。
+- AC-01~AC-25 状态,关键链路通过率。
- 自动化通过率、偶发失败率、覆盖率变化和执行时长。
- API 性能分位数/错误率、资源峰值、安全发现、恢复实测值。
- 环境限制、未测范围、残余风险、责任人和下一步日期。
+
+## 8. 当前自动化缺口基线
+
+本次盘点只用于安排优先级,不代表功能缺陷结论:
+
+- `backend/iot-gateway` 已有命令元数据转发、上行转发和无本地状态测试,但缺 Handler 鉴权、过期、体积限制、上游错误映射和优雅退出覆盖。
+- `backend/iot-server` 已有配置、加密帧往返、篡改拒绝、阀门命令和实时传感器解析测试,但缺 Broker 级设备认证、Topic ACL、真实 MQTT 上下行、ACK、断线重连和 TLS 测试。
+- `backend/worker` 当前没有 `_test.go`,支付扫描、Outbox 轮询、HTTP 状态分支、超时、坏响应、重试和停止行为应列为 P0 自动化补齐项。
+- `backend/api` 的新增支付/退款和 IoT Logic 尚缺直接数据库事务及路由安全测试;优先覆盖并发回调、迟到回调、退款金额守恒、命令/Outbox 原子性、重复认领和 ACK 状态更新。
+- 跨四进程的设备模拟 E2E 和支付沙箱 E2E 尚未形成持续集成作业,应在候选版验收前落地或以人工证据明确替代。
diff --git a/checking/06-业务流程测试.md b/checking/06-业务流程测试.md
new file mode 100644
index 0000000..491e9bf
--- /dev/null
+++ b/checking/06-业务流程测试.md
@@ -0,0 +1,173 @@
+# 业务流程测试
+
+本文件把业务流程拆成可执行的跨端测试。每个流程都必须记录 UI/API 结果、数据库事实、Outbox/消息、审计和最终用户可见状态。详细单条用例使用 `templates/测试用例模板.md` 展开。
+
+## 1. 执行规则
+
+- P0 流程每个候选版本全量执行;P1 流程每日自动化冒烟、候选版全量执行;P2 在专项或 UAT 执行。
+- 每个流程至少覆盖成功、服务端拒绝、重复请求、并发、依赖超时、进程重启、越权和恢复。
+- 所有标识使用 `identity`;数据库 `id` 仅用于测试内部关联断言,不出现在客户端请求响应中。
+- 时间断言同时检查业务发生时间、设备/客户端采集时间、服务端接收时间和更新时间,不用补传时间覆盖原始时间。
+- 金额均以最小货币单位整数断言;状态必须通过动作接口推进。
+
+## 2. 流程总表
+
+| 流程 ID | 优先级 | 流程 | 主要参与者 | 关联验收 |
+| --- | --- | --- | --- | --- |
+| BF-01 | P0 | 邀请注册与服务关系 | gas/delivery admin、user_app、API | AC-20、AC-23 |
+| BF-02 | P0 | 设备绑定与单设备远程关阀 | user_app、API、Worker、Gateway、IoT Server、设备 | AC-01、AC-02、AC-25 |
+| BF-03 | P0 | 高风险遥测自动关阀与整改 | 设备、四后端进程、service_app、admins、user_app | AC-03~AC-06、AC-16 |
+| BF-04 | P1 | 设备共享、群控与报修 | user_app、API、IoT 链路、admin | AC-12、AC-22 |
+| BF-05 | P0 | 商城下单、渠道支付与任务生成 | user_app、API、支付沙箱、admins、service_app | AC-07 |
+| BF-06 | P0 | 支付超时关单与迟到回调 | user_app、API、Worker、支付沙箱 | AC-07 |
+| BF-07 | P0 | 订单项退款审核入钱包 | user_app、API、platform_admin | AC-07、AC-14 |
+| BF-08 | P1 | 气瓶预约、配送、随瓶安检与回收 | user_app、gas/delivery admin、service_app、API | AC-11、AC-13、AC-18 |
+| BF-09 | P0 | 服务人员准入与离线补传 | service_app、admins、API | AC-16、AC-19、AC-21 |
+| BF-10 | P1 | 安装/维修闭环 | service_app、API、admins、user_app | AC-06、AC-17 |
+| BF-11 | P0 | 钱包提现审核 | apps、API、platform_admin | AC-08、AC-19、AC-24 |
+| BF-12 | P0 | 组织隔离与敏感轨迹访问 | admins、apps、API | AC-09、AC-11、AC-23 |
+| BF-13 | P1 | IoT 上行报文持久化与命令 ACK | 设备、IoT Server、Gateway、API | AC-02、AC-03、AC-25 |
+| BF-14 | P1 | IoT Outbox 故障恢复 | API、Worker、Gateway、IoT Server | AC-02、AC-25 |
+| BF-15 | P2 | 审计、备份恢复与链路追踪 | 全系统、DB、运维 | AC-10 |
+
+## 3. BF-01 邀请注册与服务关系
+
+前置:气站 A、配送点 A/B;未注册手机号 U1、已有用户 U2;有效、过期、停用、次数耗尽、区域外五种邀请。
+
+主流程:
+
+1. 气站 A 创建有服务区域、有效期、次数和活动来源的邀请二维码。
+2. U1 扫码,API 解析签名令牌并校验状态、范围、地址、风控和登录状态。
+3. U1 完成验证码、协议同意和地址确认,创建全局账号及服务关系。
+4. gas_admin 查询转化记录,user_app 查询当前服务归属。
+
+断言:二维码不含用户信息、管理员身份、长期令牌或 API 密钥;验证码一次消费;用户只创建一次;服务关系、来源、扫码和确认审计完整。
+
+异常:U2 重复扫码不重复建档;跨区、过期、停用、超次数被稳定错误码拒绝;从 A 变更至 B 必须明确确认,失败不得覆盖原关系;并发两次确认只有一个确定结果。
+
+## 4. BF-02 设备绑定与远程关阀
+
+前置:未绑定在线设备 D1、已绑定设备 D2、用户 U1/U2;四后端进程和 MQTT 设备模拟器启动。
+
+主流程:
+
+1. U1 扫码绑定 D1,API 校验设备存在、未占用、地址和用户授权。
+2. U1 发起关阀,服务端完成对象归属、安全规则与二次确认校验。
+3. API 在同一事务创建 `iot_command` 和 `iot_outbox`,返回命令 identity,不返回执行成功。
+4. Worker 认领 Outbox,经 Gateway 向 IoT Server 下发;IoT Server 编码 V1.8 阀门命令并发布 `devices/{deviceId}/down`。
+5. 模拟器返回 ACK;IoT Server 解码并经 Gateway 回调 API,命令变为成功。
+6. user_app 查询到最终结果,审计可串联请求、命令、Outbox、包序号、回执和操作者。
+
+异常:D2/U2 越权、设备离线、相同幂等键、相同幂等键不同参数、Gateway 不可用、命令已过期、ACK 丢失、失败 ACK、伪造设备 ACK、重复 ACK。无回执必须保持“待确认”,不能显示成功。
+
+## 5. BF-03 高风险遥测自动关阀与整改
+
+前置:规则阈值、责任人和测试 SLA 已配置;设备 D1 在线;服务人员具备/不具备准入条件两组。
+
+主流程:
+
+1. D1 发布达到高风险阈值的加密 V1.8 实时数据帧。
+2. IoT Server 验证设备身份、Topic、边界、长度、LRC8、AES 和帧内容,保留原始 payload。
+3. Gateway 转发上行,API 保存设备时间、服务端接收时间、原始帧和解码结果。
+4. 系统创建安全事件、SLA、自动关阀命令、责任任务和通知。
+5. 合格安检员接单、上门检查、提交分类证据和用户签名;不合格进入整改。
+6. 整改完成后复检;通过后关闭事件并满足开阀恢复条件。
+
+断言:事件、命令、通知、任务、证据、整改、复检和审计互相关联;普通用户在未整改时开阀被拒绝;自动关阀 ACK 丢失时事件仍存在且命令待确认。
+
+异常:重复/乱序遥测不重复建同一事件;坏帧不得进入业务规则;通知部分失败可重试;不合格人员不能接单;缺照片、签字或围栏不允许完成;复检失败继续整改;SLA 超时只升级一次。
+
+## 6. BF-04 设备共享、群控与报修
+
+1. 所有者分别授予成员“查看”和“控制”权限,验证能力互斥。
+2. 对含正常、离线、高风险锁定设备的分组执行关阀和开阀。
+3. 校验逐设备命令 identity 与结果;高风险开阀被单独拦截,不因其他设备成功而覆盖。
+4. 成员权限撤销后,旧页面缓存和旧令牌直接调用控制 API 仍被拒绝。
+5. 一键报修提交类型、描述、地址、定位和照片,形成工单及访问审计。
+
+## 7. BF-05 商城下单、渠道支付与任务生成
+
+前置:可售商品、库存、地址、预约时段、支付宝/微信沙箱;一个需要配送、一个需要安装的订单。
+
+主流程:创建订单并冻结库存 → 创建幂等 `payment_order` → 客户端获得最小化调起参数 → 渠道成功回调 → 验签并校验 appid/商户、支付单号、金额、币种和状态 → 数据库事务标记支付成功并推进业务订单 → 生成配送/安装任务 → 对应人员可见。
+
+异常:客户端声称支付成功不推进订单;错误签名、appid、商户、金额、渠道或支付单号被拒绝;重复回调不重复扣款、建任务或写流水;并发回调只有一次状态推进;同请求号不同金额/业务 identity 产生幂等冲突;日志只保留回调摘要。
+
+## 8. BF-06 支付超时关单与迟到回调
+
+1. 创建即将过期的支付宝和微信支付单,订单保持待支付。
+2. Worker 使用内部令牌调用关单接口,API 查询过期待支付单并逐渠道关闭。
+3. 仅渠道关单成功且数据库条件更新成功的记录变为 closed;单笔失败留待下轮。
+4. 重复扫描不重复关单或改变已支付订单。
+5. 对已过期支付单模拟成功回调,支付单进入“过期后支付异常”,业务订单不得直接进入已支付/履约。
+
+异常:错误 Worker 令牌、渠道超时、API 重启、Worker 重启、扫描和回调竞争、刚好到期边界、100 条批次边界。
+
+## 9. BF-07 订单项退款审核入钱包
+
+前置:用户本人已支付且未履约订单,含多个订单项;配置退款窗口;用户钱包存在。
+
+主流程:用户选择订单项和数量提交退款 → API 校验本人、支付成功、窗口、履约状态、数量和累计退款金额 → 创建待审核退款及明细 → 平台财务审核 → 同一事务锁定钱包、增加余额、完成退款并写不可变流水 → 用户查询到账。
+
+异常:他人订单、窗口过期、已履约、项目不存在、数量为 0/超量、累计超实付、重复请求、并发两笔超额、驳回无原因、申请人自审、重复审批。任一数据库步骤失败必须整体回滚,不得只加余额或只改退款状态。
+
+## 10. BF-08 气瓶预约、配送、随瓶安检与回收
+
+主流程:选择配送公司、规格、数量和时段 → 冻结库存与费用 → 支付 → 气站分派配送点/人员 → 配送员准入校验 → 出发和轨迹上报 → 到达围栏校验 → 气瓶扫码/蓝牙校验 → 随瓶安检和取证 → 收款确认/签收 → 空瓶回收 → 同步更新气瓶、库存、押金、订单和结算。
+
+异常:库存并发不足、时段失效、人员区域外、非履约定位、错误/重复气瓶码、证据不足、金额差异、回收损坏/编码不符、离线乱序补传、用户查看其他订单轨迹。
+
+## 11. BF-09 服务人员准入与离线补传
+
+1. 自主注册仅生成待审核账户;分别制造资质过期、培训未过、未上班、区域外、无授权设备。
+2. 验证任一条件不满足都不能开始任务,直接调用 API 同样拒绝。
+3. 合格人员开始任务后断网,采集定位、照片元数据、签名和扫描结果并本地加密。
+4. 重启 App 后恢复草稿;联网按原始采集时间和幂等键补传。
+5. 服务端去重、乱序校正、完整性/哈希和任务状态校验,记录接收时间与补传标记。
+
+异常:重复补传、篡改哈希、未来时间、任务已关闭、部分上传成功、权限中途撤销、本地明文检索、非任务期间后台定位。
+
+## 12. BF-10 安装/维修闭环
+
+按“使用条件→备料→安装→测试→前期安检→设备激活→完成→收款”顺序执行新装;维修保存前中后证据、材料、价格快照、应收/实收和签字。测试失败、环境不合格、收费差异或强制证据缺失时,UI 与直接 API 均不能完成;整改/复检通过后才可继续。
+
+## 13. BF-11 钱包提现审核
+
+1. 绑定银行卡并验证只返回末四位;设置/校验独立六位支付密码和失败锁定。
+2. 申请提现,在同一事务锁定钱包、预扣总余额和可提现余额、创建申请和流水。
+3. 驳回只返还该申请实际预扣的两类余额;完成打款只确认外部结果,不再次扣款。
+4. 全程验证 `0 <= 可提现余额 <= 总余额`。
+
+异常:余额不足、并发申请、冻结钱包、错误/锁定支付密码、重复审批、驳回无原因、申请人篡改、历史未预扣提现兼容分支、整数溢出。
+
+## 14. BF-12 组织隔离与敏感轨迹访问
+
+使用结构相同的气站 A/B、配送点 A/B、用户 U1/U2 数据,逐项验证列表、详情、搜索、分页、关联资源、导出和直接猜测 identity。用户只看本人简化轨迹;精确回放/导出要求审批、水印、用途和审计;非履约位置不可访问。缓存命中、批量接口和错误响应不得泄露目标是否存在或敏感字段。
+
+## 15. BF-13 IoT 上行报文与命令 ACK
+
+协议矩阵:无加密/key 1、最短/最大允许帧、错误起止符、声明长度错误、LRC 错误、AES key 不存在、密文非 16 字节倍数、BCD 时间、实时传感器/事件/IO/参数块、未知子标识、截断块。
+
+链路断言:设备只能发布自己的 `up/ack` 并订阅自己的 `down`;Gateway 与 API 均校验内部令牌;上行原始十六进制小写保存;ACK 只有设备号、包序号和控制位匹配时更新命令;设备失败位写稳定错误码;伪造/重复 ACK 不改变其他命令。
+
+## 16. BF-14 IoT Outbox 故障恢复
+
+1. 在 API 事务中注入 Outbox 写失败,验证命令也不落库;命令写失败同理。
+2. 两个 Worker 并发认领,`SKIP LOCKED` 保证一条记录单次 processing。
+3. Gateway 连接失败、返回非 2xx、响应 JSON 损坏分别记录失败并安排重试。
+4. processing 超过一分钟可重新认领;退避按尝试次数增长且上限一分钟。
+5. Worker 在投递成功后、回写 API 前崩溃,恢复后允许重复投递但不重复业务命令事实;设备/Server 侧以命令 identity 和幂等键识别。
+6. 观察 pending/retry/processing 积压、最老年龄和失败码;未实现死信上限时在报告中标记风险。
+
+## 17. BF-15 审计、备份恢复与链路追踪
+
+选取设备命令、安全事件、支付回调、退款、提现和敏感轨迹导出六类事实,验证可用 `request_id`、业务 identity、命令 identity、支付单号串联日志、审计、数据库和外部摘要。隔离环境执行 PostgreSQL 全量加 WAL 恢复,校验记录数、关键哈希、状态不变量、资金守恒和审计链;记录实测 RPO/RTO,不以目标值代替实测值。
+
+## 18. 自动化落地顺序
+
+1. API 数据库集成测试:支付创建/回调/关单/退款、IoT 命令/Outbox/上行回执。
+2. Worker 使用 `httptest` 覆盖 204、401、500、超时、坏 JSON、重复认领和优雅退出。
+3. Gateway 补充 Handler 级鉴权、过期、体积、上游错误和上行拒绝测试。
+4. IoT Server 增加 Broker 级每设备认证/ACL、MQTT 上下行和断线重连测试。
+5. 使用设备模拟器和支付沙箱建立 BF-02、BF-05、BF-06、BF-13、BF-14 的 CI 集成作业。
+6. 使用 Flutter/Web E2E 覆盖 BF-01、BF-03、BF-07~BF-12 的跨角色流程。
diff --git a/checking/README.md b/checking/README.md
index 72ea557..ebbb675 100644
--- a/checking/README.md
+++ b/checking/README.md
@@ -8,9 +8,10 @@
| --- | --- |
| [01.md](01.md) | 用户已有的平台后台补充需求原文,作为测试输入保留,不作为完整测试计划 |
| [02-总体测试计划.md](02-总体测试计划.md) | 范围、策略、分层、阶段、角色、风险和完成标准 |
-| [03-系统与业务覆盖矩阵.md](03-系统与业务覆盖矩阵.md) | 各端、后端进程、跨端流程和 AC-01~AC-24 的追踪关系 |
+| [03-系统与业务覆盖矩阵.md](03-系统与业务覆盖矩阵.md) | 各端、后端进程、跨端流程和 AC-01~AC-25 的追踪关系 |
| [04-环境数据与依赖计划.md](04-环境数据与依赖计划.md) | 测试环境、账号、数据、Mock、第三方和设备模拟要求 |
| [05-执行批次与质量门禁.md](05-执行批次与质量门禁.md) | 冒烟、回归、专项、发布验收的执行顺序和准入准出条件 |
+| [06-业务流程测试.md](06-业务流程测试.md) | 跨 App、Web、API、Worker、IoT 和数据库的可执行业务流程测试 |
| [templates/测试用例模板.md](templates/测试用例模板.md) | 单条测试用例的统一记录格式 |
| [templates/测试报告模板.md](templates/测试报告模板.md) | 每轮测试结果、缺陷和发布建议模板 |
@@ -24,6 +25,6 @@
## 计划边界
-- `backend/worker` 已投递 IoT Outbox;无状态 `backend/iot-gateway` 与内嵌 Mochi MQTT Broker 的 `backend/iot-server` 已落地系统接口和 MQTT 协议边界。真实设备证书和厂商硬件仍需在联调环境验收。
+- `backend/worker` 已承担支付超时扫描和 IoT Outbox 投递;无状态 `backend/iot-gateway` 与内嵌 Mochi MQTT Broker 的 `backend/iot-server` 已落地系统接口和 MQTT V1.8 协议边界。真实设备证书、生产 TLS 和厂商硬件仍需在联调环境验收。
- `frontend/site` 是官网,按展示、响应式、可访问性、链接、构建和托管 Worker 测试;不把它当成业务事实写入端。
- 本计划不把尚未实现或文档中“待确认”的政策当作通过标准。此类项进入阻塞/待决清单,由产品、安全、财务或法务确认后再固化用例。
diff --git a/checking/templates/测试报告模板.md b/checking/templates/测试报告模板.md
index 942581c..18c2953 100644
--- a/checking/templates/测试报告模板.md
+++ b/checking/templates/测试报告模板.md
@@ -22,7 +22,7 @@
| 单端/E2E | | | | | | |
| 安全/性能/弱网/恢复 | | | | | | |
-## 4. AC-01~AC-24
+## 4. AC-01~AC-25
| AC | 结果 | 证据 | 缺陷/限制 |
| --- | --- | --- | --- |
diff --git a/docs/11-数据接口与安全.md b/docs/11-数据接口与安全.md
index 996f3fb..17052ef 100644
--- a/docs/11-数据接口与安全.md
+++ b/docs/11-数据接口与安全.md
@@ -105,6 +105,13 @@
- 对象存储启用版本/生命周期策略,合同和安全证据按合规期限留存;备份不得绕开数据加密和访问控制。
- 关键服务多实例部署,MQTT、数据库、消息队列和对象存储须有明确高可用方案和故障演练计划。
+### 5.1 远程依赖与迁移安全
+
+- 当前 API 按项目部署约定仅从对应 YAML 读取 PostgreSQL、Redis、字段加密密钥及支付/IoT 内部令牌,不接受环境变量覆盖。配置文件必须采用严格访问权限并由部署流程受控分发;不得出现在日志、测试报告或公开构建产物中。
+- 远程 Redis 应使用 TLS;PostgreSQL 应启用服务端证书校验,并由部署环境限制来源网络。开发 Mock 验证码和 Mock 支付不得在生产配置启用。
+- 资金、退款、支付及审计表不得由通用迁移命令直接删除。发现旧表时迁移必须中止,先完成数据盘点、备份、映射、双写或兼容读取方案,经财务和审计确认后执行专用迁移。
+- 远程集成测试使用隔离数据库、独立 Redis DB/命名空间和测试前缀;不得清空共享实例,也不得物理删除资金、安全事件或审计事实作为清理手段。
+
## 6. 移动 Client API 安全实施约定
- 用户端和工作人员端分别使用 `user_app`、`service_app` JWT client claim;服务端逐请求校验 client、账户启用状态、岗位、组织和对象归属。
diff --git a/frontend/site/package.json b/frontend/site/package.json
index 61228dd..aa1771d 100644
--- a/frontend/site/package.json
+++ b/frontend/site/package.json
@@ -7,7 +7,7 @@
"dev": "vite",
"build": "vite build && node scripts/prepare-sites-build.mjs",
"preview": "vite preview",
- "test:sites": "node --test tests/sites-worker.test.mjs"
+ "test:sites": "npm run build && node --test tests/sites-worker.test.mjs"
},
"dependencies": {
"@fontsource-variable/noto-sans-sc": "^5.3.0",