Files
cmautobuy/docs/client/06-quality-security.md
T

341 lines
21 KiB
Markdown
Raw Normal View History

# 06 Client 质量、安全与测试基线
- 文档状态:基线草案,待质量评审
- 适用范围:Client 源码、配置、数据库、自动化和发布产物
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
## 1. 质量目标
- 同一台 Client 不重复执行或重复提交同一个采购任务;崩溃重启后也不重复下单。
- 网络和 Admin 故障不丢失已完成结果。
- 设备或 PDD 页面异常可诊断,不以错误坐标继续执行高风险操作。
- 所有阻塞工作离开 Qt 主线程,窗口在长任务期间保持可用。
- 代码、接口、数据库和诊断产物不泄露凭据及不必要的个人数据。
## 2. 测试分层
### 2.1 单元测试
必须覆盖:
- 任务状态转换和非法转换;
- 最终确认页订单总价、数量、订单总价上限和规格校验;
- Admin DTO 与 `pdd_data` JSON 验证;
- SQLite Repository、迁移和默认排序;
- Outbox 幂等、退避和崩溃恢复;
- 销量、评价和价格文字解析;
- 控件树坐标解析、规格匹配和选中状态判断;
- 订单候选匹配规则。
PDD 解析测试优先使用脱敏的 XML 固件,不要求每次连接真实设备。
### 2.2 集成测试
使用临时 SQLite 和 Mock Admin 覆盖:
- 原子领取和重复领取;
- 提交一个 Admin 侧已取消的任务,仍被接受并标记 `succeeded`;
- 采集/采购任务分派;
- 结果先落库再进入 Outbox;
- Admin 超时后重试相同幂等键;
- 程序在提交前、下单阶段和提交后异常退出;
- 窗口关闭后后台结果不会更新已销毁界面。
### 2.3 界面测试
- Python 语法编译;
- 离屏实例化主窗口;
- 表格模型排序、筛选和增量加载;
- 开始/停止按钮防重复;
- Ctrl+F、Enter 和 Tab 顺序;
- 加载、空、错误、离线、运行和人工处理状态;
- 浅色、深色、高对比度和 100%–200% 缩放。
### 2.4 设备测试
真实设备测试必须显式标记,不应混入默认快速测试:
- PDD 首页、商品页、规格面板和订单列表识别;
- 不同商品规格数量、滚动方式和缺货状态;
- 登录失效、验证码、网络慢、页面变化和弹窗;
- 采集完整性;
- 采购演练及订单核对。
### 2.5 契约和端到端测试
- Mock Gateway 与 Http Gateway 对同一接口测试集通过。
- Admin 提供可测试环境后执行版本、错误和幂等契约测试,含"已取消任务的结果仍被接受"。
- 端到端测试从任务领取开始,以 Admin 确认结果和本地状态一致结束。
## 3. 采购安全门禁
Admin 新建采购任务固定为真实下单(不支付),Client 不再提供手工 live 授权。Client 身份有效、已选择 Android 设备且真实采购 Adapter 就绪时自动声明 live;领取并执行后仍必须满足下列安全门禁:
1. Admin 原子领取和结果幂等已经通过联合测试。
2. Outbox 断网和重启恢复测试通过。
3. 商品编号、标题、规格、数量、库存和价格保护校验通过。
4. 最终下单前后均有持久化步骤标记。
5. 进程在不可逆阶段退出后只执行订单核对,不会重新下单。
2026-08-18 09:55:32 +08:00
6. 批量重新采购永久拒绝历史任一次执行带不可逆标记的任务再次下单;`reconcile_manual_review` 只允许恢复原运行继续只读核单,不创建新运行、不调用采购 Adapter,核单不确定时不启动下一条。
7. 订单匹配能识别唯一候选;多个候选进入人工处理。
8. 历史演练模式和真实模式在代表性商品与设备上通过自动化验收。
9. 真实设备首单按独立验收工单执行,并明确停止在支付前。
Client 不保存或读取 `purchase.live_*` 手工授权设置。`purchase_mode` 只表达本次注册或领取时的运行就绪能力:身份、设备或真实采购 Adapter 任一缺失都声明 `dry_run`,全部就绪才自动声明 `live`。演练 Adapter 不提供提交方法,live Adapter 只提供一次性提交方法,两者都不提供付款或取消订单方法。开发者不能把自动化测试通过当成真机验收完成。
2026-08-10 00:27:52 +08:00
### 3.1 当前自动化安全检查
- 关键手机动作前已持久化 `current_step`。
- 无不可逆标记的中断会先关闭旧运行,新运行使用新 `attempt_id`。
- 有不可逆标记的中断只调用只读核对,采购 Adapter 调用次数为 0。
- 只读核单要求非空订单编号、有效下单时间和未付款状态;时间位于本地
`order_submitted_at` 前后 5 分钟且候选唯一时才生成成功 Outbox。
- 商品和确认快照不依赖订单页重复展示;无候选、多候选、订单号或时间缺失、
超出时间窗口或非未付款状态只转人工,不产生采购成功结果。
2026-08-10 00:27:52 +08:00
- 已落库 Outbox 只补交,不重跑手机流程。
- 停止、设备断开、验证码、登录失效和结果不确定都保留稳定错误和诊断。
- `execution_mode=live` 只允许采购任务,且 Client 不得自行升级 Admin 下发的模式。
- 最终点击前重新读取页面并核对规格、数量、价格、库存和唯一提交目标。
- `irreversible_action_at` 必须在点击前以事务提交;点击最多一次,返回或抛错后均只进入核单。
- 下单点击完成或结果不确定后,任务先保存为 `reconcile_purchase`。持续自动获取
等待 5 秒后自动开始下一轮;Dispatcher 必须优先执行当前任务的只读核单,不能
重新打开商品、重新下单或先领取其他任务。5 秒只用于页面跳转缓冲,不代表下单成功。
- 正式 uiautomator2 采购 Adapter 只点击商品页采购入口和目标规格,
演练的 `enter_confirmation` 与 `stop_before_submit` 均为只读检查。
- 单元测试必须断言演练没有最终点击、live 只有一次最终点击,且模糊目标不会点击。
2026-08-10 00:27:52 +08:00
上述只证明本地安全边界;真实设备联调仍须按独立验收工单记录结果,且不得进入付款操作。
## 4. 自动化防护
- 点击前必须确认包名、页面类型、目标语义和坐标边界。
- 高风险点击应在最新控件树上重新定位,不使用长时间缓存的坐标。
2026-08-10 19:31:21 +08:00
- 采购普通状态读取先使用最新控件树确认 PDD 页面;控件树归属不明确时才查询
当前前台应用,避免在部分真机上重复等待十秒以上。最终下单点击前仍必须单独
查询前台应用。性能日志分别记录状态读取、颜色选择和尺码选择耗时。
- 规格选择后读取最新控件树并验证选中状态。
- 运行时规格解析请求必须在网络发送前写入独立 SQLite 记录;只调用一次专用命令,
不轮询 Admin。只有 `matched` 响应的哈希、候选编号、原文和 options 都是本次候选的
逐字副本时才继续。响应返回后重新完整遍历真机候选并复算哈希,页面、颜色或候选
有任何变化都在可逆阶段停止。
- 解析记录不能覆盖任务原始规格。成功结果同时保留原始规格、实际采用规格和
`resolution_id`;解析失败不自动重新采购,已有不可逆标记时永远不调用规格解析。
- 设置采购数量时先读取当前值;数量相同不聚焦输入框,小差值优先使用加减按钮。
只有输入框兜底路径确认输入法已经显示时才允许按一次返回键;输入法关闭后必须
重新核对规格、数量、价格和唯一提交目标,规格面板丢失时立即停止。
- 最终下单前重新读取实际价格、数量和规格。
- 页面出现验证码、登录、支付、权限请求或未知模态层时停止并转人工处理。
- uiautomator2 连接由单一工作线程独占,任务间清理临时状态。
- 所有循环具有超时、最大滑动次数和可取消检查。
### 4.1 商品深链页面识别
采集和采购共用同一个页面分类器。分类同时参考当前包名和控件树中的 PDD 包名,
避免部分 Android 系统错误报告前台包名。首页必须同时出现已选中的“首页”以及
“聊天”“个人中心”等底部导航特征,并且不能出现商品购买入口,不能只凭单个文字判断。
打开商品链接后若连续读取到稳定首页,Client 只重新打开同一链接一次;第二次仍为
稳定首页时,以 `PDD_GOODS_UNAVAILABLE` 结束当前任务并上报失败,自动获取可以继续
处理下一条任务。加载、网络异常、浏览器、登录、验证码、风控和未知页面不得归为
商品失效。打开前后的商品页控件树签名相同则视为旧页面,不能据此执行规格选择或
下单;采购进入不可逆阶段前仍须按商品编号等业务字段再次确认目标身份。
商品页和规格面板的页面轮询不得在每一轮依赖 `app_current()`:部分设备的前台查询会阻塞十秒以上,
还可能把已经显示 PDD 的页面报告为系统设置。连接时可以读取一次前台状态;后续
商品页和规格面板判断优先使用最新控件树中的包名与组合语义。单次设备调用即使跨过
截止时间,也要先分析刚返回的最新控件树,再决定成功或超时。规格面板超时必须记录
最后页面类型、控件树读取次数和脱敏 XML 引用,不保存商品、账号或收货相关原文。
2026-08-18 10:11:08 +08:00
只读核单遇到多个微信的 Android 应用选择器时,分别读取节点的 `text` 和
`content-desc`,并同时校验白名单标题、微信候选、前台 Resolver 包和 Activity。
选择器可能与底层 PDD 节点同时出现在控件树中,因此必须先判断选择器;证据完整时
最多按一次返回键,绝不选择微信或点击支付。证据不完整或一次返回后仍存在时转人工。
> **运营提醒(不是代码规则):** Admin 超时重派可能导致同一任务被两台 Client 各下一单,
> 产生重复的未付款订单。人工审核时取消多余订单即可。但未付款订单长期堆积可能触发平台风控,
> 需要操作人员留意,不要放着不管。
>
> 将来若要在程序里防这个,正确的加固点是**最终下单前的最后一次校验**(本节已要求重新读取价格、
> 数量和规格,顺带确认任务有效性即可),**不要**改回周期性回查 Admin 状态。
## 5. 错误分类
建议使用稳定错误代码:
| 分类 | 示例 |
|---|---|
| `ADMIN_*` | 认证失败、超时、请求无效、幂等冲突 |
| `DEVICE_*` | 设备离线、ADB 失败、应用未启动 |
| `PDD_PAGE_*` | 页面超时、未知页面、登录失效、验证码 |
| `PDD_DATA_*` | 标题缺失、规格不完整、价格解析失败 |
| `PURCHASE_*` | 规格缺货、价格超限、下单状态不确定 |
| `ORDER_*` | 未找到订单、多个候选、详情不匹配 |
| `DB_*` | 迁移失败、写入失败、数据库锁超时 |
| `OUTBOX_*` | 提交失败、幂等冲突、结果被拒绝 |
错误反馈必须说明发生了什么、已保留什么以及下一步是什么。原始异常堆栈仅写入诊断日志,不直接展示给普通用户。
## 6. 日志与诊断
日志至少包含:
- 时间、级别、模块和稳定事件名;
- `client_id`、`remote_task_id`、`attempt_id` 和请求编号;
- 当前步骤、持续时间、结果和稳定错误代码;
- Admin 响应状态,但不记录认证头和完整敏感正文。
建议事件:
- `task_claim_started/succeeded/empty/failed`
- `task_claimed`
- `task_step_changed`
- `task_result_persisted`
- `outbox_send_started/succeeded/failed`
- `purchase_irreversible_step_entered`
- `order_reconcile_completed`
### 6.1 领取到商品页性能日志
正式 Client 把任务领取到商品页就绪的白名单耗时写入
`data/logs/task_performance.jsonl`。每行是一个 JSON 对象,只允许下面四个字段:
| 字段 | 含义 |
|---|---|
| `task_id` | Admin 分配的稳定任务编号 |
| `operation` | 稳定阶段名,例如 `admin_claim_request`、`uiautomator2_connect` |
| `duration_ms` | 单调时钟计算的非负毫秒值 |
| `result` | `succeeded`、`failed` 或 `already_foreground` 等结果分类 |
阶段至少包含 Admin 领取、本地保存、ADB 检查、uiautomator2 连接、首次应用状态、
PDD 启动/等待、打开链接、首次控件树、商品页就绪和 `end_to_end_total`。
`goods_page_ready` 内部包含首次及后续控件树读取,`end_to_end_total` 又包含所有阶段,
所以阶段耗时不能直接相加后与总耗时比较;它们是有意重叠的嵌套区间。
性能日志不得增加 URL、设备号、控件树、异常正文、凭据或个人信息。日志按 2 MiB
轮转并保留 3 个旧文件。需要比较真机冷/热启动时,从 `client/` 执行:
```powershell
C:/Python310/python.exe tools/measure_goods_open.py <设备号> <商品链接> --runs 5
```
测量工具只启动或停止 PDD、打开商品页和读取控件树,不点击规格、下单或付款;
输出只包含模式、阶段名、中位数和 P95。遇到登录或安全验证会立即停止。
失败 Artifact 目录建议:
```text
artifacts/<remote_task_id>/<attempt_id>/
├── metadata.json
├── hierarchy.xml
├── screenshot.png
└── steps.jsonl
```
Artifact 写入前应脱敏,数据库只保存引用。保留周期由设置控制,删除不得影响任务结果和审计字段。
## 7. 凭据与隐私
- Admin 必须使用 HTTPS;生产环境不得允许静默忽略证书错误。
- Token 优先保存到 Windows 凭据管理器或由安全环境注入。
- **`data/` 目录是明文的,且设计上就是"整个拷走",凭据绝不能写进去**,见 [03](03-data-model.md) §2.1。
- 设置页只能显示认证状态或掩码值。
- 日志、数据库、截图、XML、Gitea 工单和 `docs/task` 都不得包含 Token、Cookie、密码或支付信息。
- 只保存完成任务所需的订单编号和时间,不采集无关收货人、地址、电话等个人数据。
- 不实现验证码、风控或平台安全机制绕过。
## 8. 数据可靠性
- 数据库迁移前创建可恢复备份或采用可回滚迁移步骤。
- 所有可写文件只能落在 `data/` 下,路径一律通过 `data_dir()` 取,见 [03 数据模型](03-data-model.md) §2.2。
- 每个任务状态变化和执行记录在同一事务内保持一致。
- `result_pending` 任务必须存在对应结果和 Outbox 记录。
- Outbox 发送成功前不得删除结果正文。
- 迟到的 Admin 响应不得覆盖更新的本地执行状态。
- 数据库损坏、磁盘写满和只读目录必须产生可操作错误,不能继续下单。
### 8.1 在线更新安全
- 一般清单和更新包只允许 HTTPS;按已确认部署方案,只有固定发布主机的默认端口
临时允许 HTTP Basic Authentication,任何其他 HTTP 地址仍拒绝。
- URL 不得包含账号密码、查询参数或片段。URL 和账号可写 SQLite;密码只能写入
Windows 凭据管理器,不得进入源码、Git、SQLite、日志、异常文本、工单或文档。
- 不同协议、主机或端口使用不同的系统凭据目标;修改服务器地址后必须为新服务器
重新输入密码,不能把旧服务器密码自动发送到新地址。
- HTTP Basic Authentication 不能防止同网段监听或中间人攻击;主机白名单只限制
Client 误连范围,不能替代 TLS,发布服务器具备条件后应迁移到 HTTPS。
- 更新包必须与最终清单 URL 同源,并同时校验清单声明的字节大小和 SHA256。
- 清单、压缩包和解压后总大小均有限制;ZIP 只能包含安全的 `app/` 内容,拒绝路径
穿越、绝对路径、反斜杠路径和符号链接。
- 下载和解压只写 `data/update/`;`client.db`、日志、Artifact 和其他设置不得进入
替换范围。
- Launcher 发现主程序仍在运行时不得替换目录;替换失败必须恢复旧 `app`,新版本
未通过健康检查时恢复 `app.old`。
- SHA256 不证明发布者身份。当前发布服务器整体失陷不在保护范围内,正式扩大分发
前应另行评估签名清单和代码签名。
## 9. 性能和响应性
- 表格使用模型/视图和增量加载,不为每个单元格创建 QWidget。
- 搜索、排序和分页使用索引支持的 SQLite 查询。
- 高频进度信号需要节流,避免每次 XML 节点变化都刷新界面。
- 大型 XML 解析、截图和文件写入位于工作线程。
- 目标基线:普通本地搜索在典型数据量下保持即时反馈,任务运行时界面可持续操作。
## 10. 发布门禁
每个版本至少满足下面全部条件。**"谁负责"这一列是为了让你知道哪些不用自己扛**——不是你负责的项,去找对应的人,不要自己拍板放行。
| # | 门禁项 | 怎么验证 | 谁负责 |
|---|---|---|---|
| 1 | 依赖版本已固定,且能重复安装 | `client/requirements.txt` 里每一行都带 `==`;在干净环境执行 `pip install -r requirements.txt` 能装成功 | 开发者 |
| 2 | 全部默认单元测试和集成测试通过 | 跑测试命令,不允许有跳过而没说明的用例 | 开发者 |
| 3 | UI 离屏冒烟测试通过 | 见 [client/AGENTS.md](../../client/AGENTS.md) §验证 第 2 条 | 开发者 |
| 4 | 数据库从上一发布版本迁移成功 | 拿上一版本的 `client.db` 副本启动新版本,数据不丢 | 开发者 |
| 5 | Mock Admin 契约测试通过 | Mock 和 HTTP 两个实现跑同一套测试 | 开发者 |
| 6 | Windows 干净环境启动测试通过 | **未打包版本**:找一台没装过本项目的机器,照 [00 上手指南](00-getting-started.md) 从头走一遍。**已打包版本**:把整个文件夹拷到一台**没装 Python** 的机器上双击 `Launcher.exe` | 开发者 |
| 6b | 升级不丢数据(仅打包版本) | 关闭程序并只换掉 `app/`,保留 `Launcher.exe` 和 `data/`;启动后任务、日志、设置都还在 | 开发者 |
| 6c | 发布清单与产物一致 | `autobuy_manifest.json` 与对应版本化清单内容相同;其中版本、文件名、字节大小和 SHA256 与实际压缩包一致,更新包中没有 `data/` | 开发者 |
| 6d | 在线更新和回退通过 | 使用上一版本目录检查新版本、下载、下次启动替换、健康标记、文件占用失败和自动回退;确认数据库未被覆盖 | 开发者 |
| 7 | 日志和产物无敏感信息 | 翻一遍日志和 `artifacts/`,确认没有 token、Cookie、密码、收货人信息 | 开发者 |
| 8 | 开源和商业许可证已确认 | 新增依赖的许可证是否允许本项目的使用方式 | **项目负责人**(不是开发者自己判断) |
| 9 | 真实下单版本额外满足采购安全门禁 | 逐条核对 §3 的 8 项 | **项目负责人 + 操作人员共同确认** |
第 8、9 项开发者**不要自己判断验收通过**。新增依赖时把包名和许可证类型报给项目负责人;真实设备下单验收必须由项目负责人和实际操作的人共同确认,见 §3。
### 10.1 采购运行时规格解析联合验收
Admin 和 Client 共同使用仓库根目录的
`testdata/contracts/purchase_spec_resolution_v1.json`。样例只允许虚构任务、商品和规格,
不得写入真实订单、账号、地址、手机号、设备号或凭据。
Client 联合验收必须确认:
- Mock Admin 与真实 HTTP Gateway 对同一请求生成相同正文、幂等键、回执和错误分类;
- 规则命中、AI 命中、无法判断、拒绝和超时均按约定保存结果,不静默改用错误规格;
- 请求发出前先保存规格快照,超时后不自动重复请求;
- 收到结果后重新读取商品页,页面变化或候选消失时停止;
- 选中规格后再次核对商品、规格、数量和价格,再写入不可逆标记并且只提交一次;
- 不可逆标记存在时,重启或异常恢复只允许核对订单,不能再次下单。
从 `client/` 目录执行共享契约测试:
```powershell
C:/Python310/python.exe -m pytest -q test/test_purchase_spec_resolution_contract_vectors.py
```
真实设备验收必须由项目负责人先指定测试商品、账号、Client、Android 设备和价格上限,并由
操作人员全程观察。每次验收最多产生一个未付款订单,不进入付款页面;一旦写入不可逆标记,
任何超时、断网、程序退出或页面异常都只核对订单,不重新下单。
## 11. 任务完成定义
单元任务只有同时满足以下条件才算完成:
1. 工单验收标准逐项通过。
2. 代码、迁移、测试和必要文档同步完成。
3. 没有静默跳过的测试或未说明的真实设备假设。
4. 变更不泄露敏感信息,也不扩大自动化权限。
5. Gitea 工单更新最终结果和提交哈希。
6. 完成记录归档到 `docs/task`。