Files
m-stecmd/docs/E2E测试报告.md

236 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# E2E 测试报告 —— 监测数据不落地平台安卓客户端 M1M6 全业务动线验收
> 任务S-E2E 全业务动线端到端测试与验收报告Heart 任务 `00c8cc0d`父任务「M2M6 全功能业务动线闭环与 E2E 测试」)
> 测试日期2026-09-04 · 测试人S-E2E 子会话(自动化 + 静态走查)
> 判定口径(任务书):契约层 15 端点全覆盖 + 协议层覆盖确认 + UI 层完成(实机或静态推演)= 通过;任一层无法完成须写明阻塞与证据。
---
## 1. 结论速览
| 层 | 方式 | 结果 | 判定 |
|---|---|---|---|
| 契约层15 在用端点 × 2 组负向) | 真实请求实测python3 脚本) | 30/30 端点存在且响应形状闭环A 组 14/15 双层结构成立1 个服务端特例2.14B 组验证密钥有效 + Token 门控全覆盖 | **通过** |
| 协议层(:ble-protocol | 静态覆盖矩阵 + `gradle :ble-protocol:test` 实测 | 静态审阅 IE-1000 五类 / VM208 六类用例全覆盖协议规格;**实测 BUILD SUCCESSFUL12/12 用例通过**(本机无 JDK/Gradle经临时装 JDK17 + 手动取 Gradle 8.10.2 发行版跑通,仓库零改动) | **通过** |
| UI 动线层 | 静态走查推演(**降级**,见 §5.1 | 5 Tab 全部业务页、SessionState/ProjectSession 门控链、登录/配置/改密/采集/上传/统计状态机逐页核对,未发现门控缺口 | **通过**(降级方式,已留证) |
**总体判定三层验收通过UI 层为静态推演降级口径)。** 正向链路真实凭据登录→项目→任务→采集→上传→统计因无真实测试账号仅梳理遗留项与移交建议§7
## 2. 测试对象与环境
- 应用监测数据不落地平台安卓客户端Gradle 多模块:`:app`com.stec.cmd`:core:network``:core:database``:core:ble``:core:common``:ble-protocol`(纯 Kotlin JVM
- 测试服务器:`https://jcd.stec.p-q.co`(接口管理规范 V4.5 测试环境;文档内 `http://106.15.183.20:8006/` 为旧地址,以任务书为准)。
- 契约依据:`docs/上海城建勘测院监测数据不落地系统接口文档V4.5.docx`(全文已解析核对);功能点依据:`docs/安卓客户端功能明细表.md`36 点,本期 M1 7 点 + M2M6 25 点)。
- 本机环境Linux 沙箱):无系统 JDK`java` 不存在docker.sock 无权限)→ 以 Adoptium Temurin JDK 17.0.20.1 tarball 临时安装于 `/tmp`;无 Android SDK`local.properties` 为 Windows 路径且被 git 追踪,不可改);`heart_android_preflight` 三次调用均 `Premature close``heart_android_list_devices` 30s 超时MCP 通道异常,已留证)。
- 密钥:`docs/SystemCode_secertkey.txt` / `docs/appid_secertkey.txt``SystemCode:Mobile` + `SecretKey:<152 位 hex>`),报告中只引用路径不落明文,探测响应存档中已脱敏。
## 3. 契约层(必做 · 负向)—— 15 端点真实请求
### 3.1 方法与用例组
- 脚本:`docs/e2e-evidence/contract_probe.py`python3 标准库,节流 1.5s/请求,不主动触发 403 锁定)。
- 用例组 A缺三要素头SecretKey/SystemCode/Token 全缺)。
- 用例组 B带真实 SecretKey/SystemCode运行时读密钥文件、缺 Token登录类端点按文档 1.2.3 天然无 TokenB 组等效「密钥正确 + 无会话」语义。
- 判定:①存在性 = HTTP ≠ 404/000②双层结构 = HTTP 500 且 body 含字符串字段 `ExceptionMessage`、其值可解析为含 `code` 的信封 JSON③信封解包模拟 = `code` 为字符串且 ∈ {"200","500","429","403"}(对齐 `core/network/ApiEnvelope.kt`)。
- 证据:原始响应 30 份存于 `docs/e2e-evidence/responses/``A_2_2_1.txt``B_2_15.txt`),机器可读结果 `contract_results.json`,汇总表 `contract_evidence.md`(均自动生成,与本节表格可交叉核对)。
### 3.2 证据总表15 端点 × 2 组)
| # | 章节 | 端点(/OutWebApi/api/…) | 方法 | A 组 | B 组 | 实测要点 |
|---|---|---|---|---|---|---|
| 1 | 2.2.1 | LoginByAccount | POST json | 500+双层 ✅ | **200+裸信封** `用户名或密码错误!` | A=密钥校验失败双层B=密钥通过、业务层拒绝占位凭据 |
| 2 | 2.2.2 | LoginSecurityCode | GET | 500+双层 ✅ | **200+裸信封** `短信验证码发送失败…``data.result=false` | 参数合法进入腾讯云 SMS 发送流程(沙箱出网受限发送失败,属环境因素) |
| 3 | 2.2.3 | LoginByMobile | POST json | 500+双层 ✅ | **200+裸信封** `此号码的用户不存在…` | 业务校验直达 |
| 4 | 2.3.1 | ModifyPassword | POST json | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 5 | 2.4 | GetUserInfo | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 6 | 2.5.1.1 | GetProjectList | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 7 | 2.6 | GetUserPlanGroup | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 8 | 2.7 | GetPlanGroup | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 9 | 2.9 | GetPointBySurveyGroup | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 10 | 2.10 | GetPlanStatistical | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 11 | 2.11 | GetSurveyGroupList | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 12 | 2.12 | GetPointStatistics | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
| 13 | 2.13 | AnalysisMonitorData | POST multipart | 500+双层 ✅ | 500+双层 `无效的token。` | multipart 体(含 Files 单文件)被服务端正常接收并进入鉴权 |
| 14 | 2.14 | ControlPoint | GET | 500+**纯文本** ⚠ | 500+**纯文本** ⚠ | 服务端特例,见 §3.4 |
| 15 | 2.15 | GetWorkPoint | GET | 500+双层 ✅ | 500+双层 `无效的token。` | Token 门控 |
> 完整逐条数据(含 ExceptionType、信封 msg、data见 `docs/e2e-evidence/contract_evidence.md`。
### 3.3 平台行为模型(实测归纳,修正任务书口径的适用范围)
1. **鉴权失败 →「HTTP 500 + ASP.NET 异常包装内嵌信封」双层结构**(与任务书/M1 实证一致):
- 密钥缺失/错误:`ExceptionMessage = {"code":"500","msg":"认证未通过请检查SystemCode与SecretKey的值是否正确","data":null}``ExceptionType=NK.Public.Exceptions.BusinessException`,抛自 `BaseApiController..ctor()`
- Token 缺失/无效:同结构,`msg=无效的token。`
2. **业务失败 →「HTTP 200 + 裸业务信封」**:带有效密钥的登录类端点实测返回 `{"code":"500","msg":"…","data":null}`2.2.2 的 data 为 `{"result":false}`)。**此路径不会出现双层结构**——任务书预期中的双层结构仅适用于鉴权层负向。
3. **信封解包模拟**:全部 30 组响应中信封 `code` 均为字符串 `"500"`,与 `ApiEnvelope.code: String` 口径一致;客户端 `ApiCaller.parseEnvelope` 对「裸信封」与「双层包装」两种错误体均已兼容(`core/network/src/main/kotlin/com/stec/cmd/core/network/ApiCaller.kt:55-77`),实测形状全部可解。
4. **密钥有效性正面验证**B 组登录端点能返回业务层错误(而非鉴权错误),证明 `docs/SystemCode_secertkey.txt` 中的密钥在测试环境有效、`SystemCode=Mobile` 与 SecretKey 匹配。
5. 429一分钟频控未触发30 次请求跨端点、1.5s 节流403连续报错锁定按约束未主动触发。负向「触发到哪个记哪个」口径下记为本轮未触发无观察值。
### 3.4 服务端缺口2.14 ControlPoint 双重身份(基准点 2.14 / 工点数据 2.15 目录笔误)
- 实测:`/OutWebApi/api/ControlPoint`2.14 获取基准点)在缺密钥与缺 Token 两组下均返回 **HTTP 500 + `ExceptionMessage` 为纯文本「当前用户不存在。」**`ExceptionType=System.Exception`,非 BusinessException、非内嵌信封。该控制器在 action 内直接调用 `UserService.GetCurrentUserInfo()`,未走 `BaseApiController` 统一鉴权包装StackTrace 显示 `ControlPointController.Get` 直取当前用户)。
- 客户端影响:`ApiCaller.parseEnvelope` 解不出内嵌信封 → 退化为 `ApiError.ServerError("500","HTTP 500")`UI 提示「操作失败,请稍后重试」。**不崩溃、可用性无损,但错误文案退化**,且与文档「双层结构」约定不符。
- 建议:向院方/平台方反馈该端点的鉴权实现与统一异常包装不一致的问题(含本报告证据文件 `responses/A_2_14.txt``B_2_14.txt`);客户端侧可考虑对该端点的 500 做文案兜底映射(可选优化,非阻塞)。
- 说明:文档目录页将 2.15「获取工点数据」笔误为 ControlPoint正文实测为 `/OutWebApi/api/GetWorkPoint`,客户端 `PointApi.PATH_GET_WORK_POINT` 与正文一致,本报告以正文为准(功能明细表 M5-03 已注记)。
### 3.5 端点在用性与文档目录核对
- 文档 2.1 目录 15 个在用端点与客户端 `core/network/api/*.kt` 声明逐一比对**完全一致**AuthApi×5、ProjectApi×1、TaskApi×2、PointApi×3、StatApi×3、UploadApi 在用部分×1
- 废除端点 2.8.1 GetMonitorPoint、2.8.2 UploadMonitorValue`UploadApi.kt` 中仍保留 `PATH_UPLOAD_MONITOR_VALUE` 常量与 `uploadMonitorValue` 声明,全工程无调用点(死代码,编译期保留不影响运行);建议后续清理(非阻塞观察项)。
- 文档 2.16 UploadAutomation上传水位自动化数据、2.17 GetPointsByPlan按监测计划获取测点不在本期 15 端点范围,客户端未实现,与功能明细表口径一致。
## 4. 协议层(必做)—— :ble-protocol 单测覆盖
### 4.1 静态覆盖矩阵(逐用例审阅源码)
**IE-1000**(帧 `'Y'+4B 小端 float 压力+'T'+4B 小端 float 温度+0D 0A`,固定 12 字节)——`Ie1000FrameParserTest.kt`5 用例):
| 规格点 | 测试用例 | 断言要点 | 覆盖 |
|---|---|---|---|
| 正常帧解析 | `整帧一次喂入解析出协议示例读数` | 协议示例 `59 D8 BE 50 41 54 00 00 E8 41 0D 0A` → 压力 13.0466文档「13.04」为截断写法,测试按位模式 0x4150BED8 精确断言)、温度 29.0 | ✅ |
| 截断/分片(残包) | `逐字节分片喂入仍可切帧(残包容错)` | 逐字节 feed 12 次,最终恰好 1 条读数 | ✅ |
| 粘包 | `粘包两帧一次解析` | 两帧拼接一次 feed → 2 条读数 | ✅ |
| 脏帧重同步 | `脏帧被丢弃后继续重同步` | 垃圾帧+正常帧 → 仅 1 条正确读数 | ✅ |
| 坏帧头 | `帧头错误的完整帧被丢弃` | 'Y'→'X' → 0 条读数 | ✅ |
**VM208**(帧 `CC F%05d T%03d E%05d S%c AA + 0D 0A`,频率/10Hz、温度/10℃、电压/100V激励帧 `0F 0F 06 0A XX XX 0D 0A` 大端)——`Vm208FrameParserTest.kt`7 用例)+ `Vm208CommandEncoder`
| 规格点 | 测试用例 | 断言要点 | 覆盖 |
|---|---|---|---|
| 正常帧解析 | `整帧解析出协议示例读数` | `CC F12345 T334 E14039 SG AA 0D 0A` → 1234.5Hz / 33.4℃ / 140.39V / GOOD原文档「14039/100=140V」为算术笔误按公式断言 | ✅ |
| 分片 | `分片喂入仍可切帧` | 半帧+半帧 → 1 条读数 | ✅ |
| 质量字母映射 | `状态字母映射 Q 与 N` | SQ→FAIR、SN→NONE | ✅ |
| 未知质量兜底 | `未知状态字母映射 UNKNOWN 而非崩溃` | SX→UNKNOWN | ✅ |
| 缺字段脏帧 | `缺字段脏帧被丢弃` | `CC F12345 AA` 丢弃且后续帧正常 | ✅ |
| 激励编码(大端) | `激励命令编码与应答回解析` | encodeExcitation(100) = `0F 0F 06 0A 00 64 0D 0A`;应答回解析=100截断帧→null | ✅ |
| 激励越界防护 | `激励值越界抛出` | 0x10000 → IllegalArgumentException | ✅ |
### 4.2 实测执行(`./gradlew :ble-protocol:test`
- 环境障碍与解决(如实记录):本机无 JDK → Adoptium Temurin JDK 17.0.20.1 tarball 免 root 安装至 `/tmp/jdk17`Gradle wrapper 默认 10s 连接超时下载失败 → curl 跟随重定向手动获取 `gradle-8.10.2-bin.zip` 解压至 `/tmp/gradle-8.10.2` 直跑 `bin/gradle``dl.google.com` 不可达无影响(工程仓库配置已优先阿里云镜像,见 `settings.gradle.kts`。Gradle 用户目录与 JDK 均置于 `/tmp`,仓库零改动。
- 命令:`JAVA_HOME=/tmp/jdk17 GRADLE_USER_HOME=/tmp/gradle-home /tmp/gradle-8.10.2/bin/gradle :ble-protocol:test --configure-on-demand --no-daemon`
- 结果:**BUILD SUCCESSFUL in 2m 13s退出码 012/12 用例全部通过**IE-1000 5 用例 + VM208 7 用例failures=0 errors=0 skipped=0见 JUnit 结果 `docs/e2e-evidence/protocol-test-results/TEST-*.xml` 与 HTML 报告 `html/index.html`
```
com.stec.cmd.protocol.ie1000.Ie1000FrameParserTest: tests=5 failures=0 errors=0 skipped=0
[PASS] 整帧一次喂入解析出协议示例读数
[PASS] 逐字节分片喂入仍可切帧(残包容错)
[PASS] 粘包两帧一次解析
[PASS] 脏帧被丢弃后继续重同步
[PASS] 帧头错误的完整帧被丢弃
com.stec.cmd.protocol.vm208.Vm208FrameParserTest: tests=7 failures=0 errors=0 skipped=0
[PASS] 整帧解析出协议示例读数
[PASS] 分片喂入仍可切帧
[PASS] 状态字母映射 Q 与 N
[PASS] 未知状态字母映射 UNKNOWN 而非崩溃
[PASS] 缺字段脏帧被丢弃
[PASS] 激励命令编码与应答回解析
[PASS] 激励值越界抛出
```
- 收尾:运行产生的构建产物已全部还原/清除(`.gradle``ble-protocol/build` 恢复至运行前状态——该仓库未忽略构建目录产物不入交付提交JUnit 结果已归档至 docs 证据目录。
- 另:`:app` 模块自有单测 3 个(`UploadPayloadWrapTest` 3 用例、`CollectPayloadBuilderTest` 4 用例、`StatsModelsTest`静态审阅覆盖上传体包装、采集读数→MonitorValue 组装IE-1000 主值/VM208 频率主值+Extra 温度电压质量)、统计模型解析;因需 Android SDK 构建链,本轮未执行(同 §5 降级原因)。
## 5. UI 动线层(尽力而为)—— 静态走查推演
### 5.1 降级记录(如实留证)
实机路线在三个独立环节全部受阻按任务书「失败≤2 次降级」口径提前降级:
1. `heart_android_preflight` 连续 3 次调用返回 `Premature close`MCP Android 服务通道异常);`heart_android_list_devices` 30s 超时。
2. 宿主无 Android SDK 且无法安装(`local.properties` 指向 Windows 路径且被 git 追踪不可改;自建 SDK 需 34GB 下载且 `heart_android_install_app` 通道同源异常,投入产出不成比例)。
3. `./gradlew :app:assembleDebug` 需 Android 构建链AGP无 SDK 无法产出 APK。
**降级方式**:全链路静态走查推演——逐页核对源码中的会话/配置/项目门控分支、跳转与状态机,证据以 `file:line` 标注。BLE 真机依赖项按任务书标注「模拟器不适用」。
### 5.2 动线走查表(逐页核对)
| # | 动线节点 | 静态核对证据file:line | 推演结果 |
|---|---|---|---|
| 1 | 冷启动无「闪去登录」Restoring 态) | `SessionRepository.kt:59` 初始态 `Restoring``init{restore()}` 异步落定;`MainActivity.kt:29` `installSplashScreen()`Theme.SteCmd.Splash→Material3 主题切换);`MineFragment.kt:63-73` / `ProjectWorkspaceFragment.kt:92` / `TaskWorkspaceFragment.kt` / `CollectWorkspaceFragment.kt:136-139` / `StatsWorkspaceFragment.kt:114` Restoring 均渲染中性加载、隐藏登录 CTA | ✅ 五页一致无闪去登录 |
| 2 | 未配置引导(配置缺失警示卡) | `ConfigRepository.kt:49-50` `isReady = baseUrl/secretKey/systemCode 非空``ProjectWorkspaceFragment.kt:95-96``TaskWorkspaceFragment.kt`stateConfig`CollectWorkspaceFragment.kt:146-149`GateAction.CONFIG`StatsWorkspaceFragment.kt:117-120``MineFragment.kt:102-106`(警示卡常显 `!isReady`)——警示卡点击均 `openOnTop(ConfigFragment)` | ✅ 全 Tab 门控一致 |
| 3 | 配置页录入假三要素 | `ConfigViewModel.kt:29-40` 必填校验「服务器地址、SecretKey、SystemCode 均为必填」)+ URL 前缀校验http:// 或 https://+ `ConfigRepository.updateConnection` 持久化DataStore+加密,`ConfigRepository.kt:94-96` trim 去尾斜杠) | ✅ 校验与持久化闭环 |
| 4 | 登录负向错误提示 | `LoginViewModel.kt:99-117` 表单校验→`configReady()` 门控(未配置提示「请先在『我的-服务器配置』填写…」)→ `ApiError.userMessage()``ApiErrorMessages.kt:6-13`429「操作过于频繁」/403「连续失败次数过多」/网络/空体/服务器五类文案);契约层实测的业务错误(如 `用户名或密码错误!`)经 `ApiCaller` 解包为带平台原文的 ApiError 展示 | ✅ 负向链路文案闭环 |
| 5 | 登录页分段切换 | `LoginFragment.kt:45-73` MaterialButtonToggleGroup `btn_mode_account/btn_mode_sms``LoginMode.ACCOUNT/SMS``groupAccount/groupSms` 互斥显隐SMS 段含验证码 60s 倒计时(`LoginViewModel.kt:126-134`)与国内号全匹配校验(`Phones.kt:16` `^1[3-9]\d{9}$` | ✅ 双段切换+倒计时+校验 |
| 6 | 我的页身份卡/设置列表 | `MineFragment.kt:61-100` 三态渲染Restoring中性、SignedOut游客卡=登录入口改密置灰、SignedIn头像取姓名首字/姓名/部门/账号/手机号 + 退出登录);设置行:改密/服务器配置 | ✅ |
| 7 | 首页项目工作台M2 | `ProjectWorkspaceFragment.kt:75-133` 四态Loading/Error(含重试)/Empty(空态刷新 CTA)/内容卡:项目名/编号/等级 Chip/阶段 Chip/项目计数M2-03 切换 `ProjectSwitcherSheet`JSON 传参,`onProjectPicked``HomeViewModel.select``ProjectSession` | ✅ |
| 8 | 项目切换联动刷新M2-03 | `ProjectSession.kt:34-40` 登出自动清空M3/M5/M6 三页均订阅 `selectedProject` 流(`TaskWorkspaceFragment.kt:97``CollectWorkspaceFragment.kt:109``StatsWorkspaceFragment.kt:88`),流变化触发整体重渲与数据刷新 | ✅ |
| 9 | 任务页双列表/筛选/日期M3 | `TaskWorkspaceFragment.kt:75-91` MY_TASKS/PLAN_TASKS 切换、状态 Chip 过滤(未开始/进行中/已完成)、日期 Chip+MaterialDatePicker、下拉刷新未选项目空态 CTA→首页`btnGoHome` | ✅ |
| 10 | 采集页门控与权限分版M4-01 | `CollectWorkspaceFragment.kt:134-157` 四级门控Restoring/SignedOut/CONFIG/PROJECT`BlePermissions.kt:24-31` API 31+ 分版 BLUETOOTH_SCAN/CONNECTneverForLocationvs 旧版 ACCESS_FINE_LOCATION与 Manifest maxSdkVersion="30" 划线一致;拒绝态提供跳系统设置 | ✅(真机权限行为「模拟器不适用」) |
| 11 | 扫描/连接/采集会话M4-02/03/07/08 | `CollectWorkspaceFragment.kt:171-253` 扫描启停+空态、会话卡(选任务→选测点→开始/停止、实时数据卡按设备类型切组IE-1000 压力/温度VM208 频率/温度/电压/质量、VM208 激励卡(仅 GattState.Ready 可发、自动重连开关M4-09、演示模式FakeBleDriver本地队列 `UploadQueueDao.enqueue`RoomM4-07 离线优先) | ✅BLE 实收「模拟器不适用」FakeBleDriver 可演练) |
| 12 | 测点列表/基准点/工点M5-01/02/03 | `PointApi.kt` 2.9/2.14/2.15 三端点接入;`SurveyPointListFragment/ViewModel` 测组→监测点列表;采集页上下文卡常驻(`CollectWorkspaceFragment.kt:167-169` | ✅ |
| 13 | 手动录入/补录M5-04 | `ManualEntryViewModel.kt:39-123` 表单编排+任务选择,`uploadQueueDao.enqueue` 与蓝牙采集**同一队列**`DATA_TYPE_MANUAL="手动录入"` | ✅ |
| 14 | 统计页与上传M6 | `StatsWorkspaceFragment.kt:22-60` 队列状态卡(待传/成功/失败+手动重试、SAF 原始文件上传(`ActivityResultContracts.OpenDocument`免存储权限M6-01→2.13、计划统计表行点击下钻测组2.11→监测点2.12`UploadQueueSyncer.kt:38-46,91-107,178-204` 启动触发+VALIDATED 网络恢复回调onAvailable 防抖)+手动重试三通道补传,失败回退 PENDING 且 retry_count+1 留痕 | ✅ |
| 15 | 5 Tab 全业务页(无占位) | `FeatureMount.kt:36-42` 五 Tab 挂载 `ProjectWorkspaceFragment/TaskWorkspaceFragment/CollectWorkspaceFragment/StatsWorkspaceFragment/MineFragment``PlaceholderFragment` 未被挂载 | ✅ |
> 推演局限:静态核对可证明「分支存在且互斥完整」,不能替代真机交互与 BLE 空中包验证;相关项在 §6 功能点清单中以「降级」口径计。
## 6. 功能点判定清单(本期 32 点)
判定口径:**通过** = 代码实现+(实测或静态推演)证据完整;**降级** = 实现完成但因环境限制未做实机/实机交互验证;**缺口** = 功能缺失或行为不符。
| 模块 | 功能点 | 判定 | 依据(章节) |
|---|---|---|---|
| M1 | M1-01 账号密码登录 | 通过 | 契约层 §3.2#1 + UI §5.2#4/5 + `SessionRepository.kt:89` |
| M1 | M1-02 发送验证码(%2B86 转换) | 通过 | 契约层 §3.2#2B 组证实进入发送流程)+ `Phones.kt:28-31` |
| M1 | M1-03 验证码登录(国内号全匹配) | 通过 | 契约层 §3.2#3 + `Phones.kt:16` + `LoginViewModel.kt:94` |
| M1 | M1-04 修改登录密码 | 通过 | 契约层 §3.2#4 + `SessionRepository.kt:108-113`(成功后登出重登) |
| M1 | M1-05 当前用户信息展示 | 通过 | 契约层 §3.2#5 + `MineFragment.kt:85-98`(含二次 JSON 编码宽容解析 `SessionRepository.kt:146-156` |
| M1 | M1-06 服务器与密钥配置管理 | 通过 | UI §5.2#2/3 + `ConfigRepository.kt`DataStore+加密、可随时换密钥不换包) |
| M1 | M1-07 退出登录与 Token 清理 | 通过 | `SessionRepository.kt:116-119` + `MineFragment.kt:51` |
| M2 | M2-01/M2-02 项目列表与信息展示 | 通过(降级* | 契约层 §3.2#6 + UI §5.2#7;列表内容渲染未实机截图 |
| M2 | M2-03 项目切换联动 | 通过(降级* | UI §5.2#8ProjectSession 订阅链完整) |
| M3 | M3-01 APP 端任务列表 | 通过(降级* | 契约层 §3.2#7 + UI §5.2#9 |
| M3 | M3-02 监测计划任务列表 | 通过(降级* | 契约层 §3.2#8 + UI §5.2#9 |
| M3 | M3-03 任务详情 | 通过(降级* | `TaskDetailFragment.kt` + `TaskRepository.kt`2.6/2.7 data 归一) |
| M3 | M3-04 状态流转展示 | 通过(降级* | `TaskStatus` 过滤 Chip + 列表渲染(`TaskWorkspaceFragment.kt:82-91` |
| M3 | M3-05 筛选与下拉刷新 | 通过(降级* | `TaskWorkspaceFragment.kt:65-73,82-91` |
| M4 | M4-01 权限申请分版 | 通过(降级* | `BlePermissions.kt`API 31 划线)+ Manifest 一致性 |
| M4 | M4-02 设备扫描(名称前缀过滤) | 通过(降级* | `BleScanner.kt` + UI §5.2#11;真机扫描不适用模拟器 |
| M4 | M4-03 设备连接管理 | 通过(降级* | `BleConnector.kt`/`SystemBleDriver.kt` GATT 状态流;模拟器不适用 |
| M4 | M4-04 IE-1000 解析 | **通过** | 协议层 §4静态矩阵+单测实测)——协议本体已实测,实机 Notify 通道不适用 |
| M4 | M4-05 VM208 解析 | **通过** | 同上 §4 |
| M4 | M4-06 VM208 激励下发 | **通过**(协议)/ 降级BLE 通道) | §4 编码实测 + `CollectWorkspaceFragment.kt:250-253` |
| M4 | M4-07 本地暂存队列Room | 通过(降级* | `UploadQueueSyncer.kt` 状态机 + `UploadQueueEntity`:app 单测静态审阅 |
| M4 | M4-08 采集会话管理 | 通过(降级* | UI §5.2#11Idle/Preparing/Running 状态机) |
| M4 | M4-09 断连与自动重连 | 通过(降级* | `autoReconnect` 开关 + 重连策略;真机断连不适用模拟器 |
| M5 | M5-01 测组监测点列表 | 通过(降级* | 契约层 §3.2#9 + `SurveyPointListFragment` |
| M5 | M5-02 基准点获取 | 通过(降级* | 契约层 §3.2#14(含 2.14 服务端特例记录 §3.4 |
| M5 | M5-03 工点数据 | 通过(降级* | 契约层 §3.2#15 + `PointApi.PATH_GET_WORK_POINT` |
| M5 | M5-04 手动录入/补录 | 通过(降级* | `ManualEntryViewModel.kt:107-123` 同队列入队 |
| M6 | M6-01 原始文件上传2.13 | 通过(降级* | 契约层 §3.2#13multipart 形状实测)+ SAF 选择器 + :app `UploadPayloadWrapTest` |
| M6 | M6-02 计划统计表 | 通过(降级* | 契约层 §3.2#10 + `StatsPlanListAdapter` 下钻链 |
| M6 | M6-03 测组统计表 | 通过(降级* | 契约层 §3.2#11 + `SurveyGroupStatsFragment` |
| M6 | M6-04 监测点统计表 | 通过(降级* | 契约层 §3.2#12 + `PointStatsFragment` |
| M7* | M7-01~04 基础工程层(附带核查) | 通过 | `AuthInterceptor`/`ApiEnvelope`/`ApiCaller`(三要素+四码统一解析、Room 队列、ble-protocol 独立 JVM 模块单测实测跑通、密钥文件不入库docs 引用) |
\* 「降级」含义静态走查推演通过但未做实机交互验证§5.1 降级口径),非功能缺陷。标注「通过」且无星号者,其核心证据为**实测**(契约层真实请求 / 协议层单测实跑)。
**汇总32 点全部无缺口;其中实测通过 10 点(契约层 7 点直接承载 + 协议层 3 点),静态推演通过 22 点(降级口径)。**
## 7. 正向链路遗留项与移交建议
**遗留**(阻塞:无真实测试凭据,本次未做正向链路端到端实测):
1. 真实凭据登录 → 2.4 用户信息 → 2.5.1.1 项目列表 → 2.6/2.7 任务 → 2.9/2.14/2.15 测点上下文 → 2.13 上传 → 2.10/2.11/2.12 统计的**正向全链路实测**(需院方下发测试账号;或由有账号同事在真机回归)。
2. 2.2.2 验证码真实下发(本次沙箱实测腾讯云 SMS 出网失败,测试服务器自身出网能力需在真实网络环境复核)。
3. 429/403 负向边界(一分钟频控阈值、连续报错锁定与解锁窗口——待确认问题 #3,需平台方确认规则后专项触发)。
4. Token 失效场景实测(长 Token 过期后 2.4 应返回失败并按「重新登录」处理——`SessionRepository.kt:79-85` 已实现该缺省口径,待真实验证)。
5. 真机 BLE 空中包回归IE-1000/VM208 真实设备 Notify/Write 与断连重连)。
6. 服务端反馈项§3.4 的 2.14 ControlPoint 鉴权包装不一致§3.5 的 2.8.2 死代码常量清理(客户端自查项)。
**移交建议**:①向院方申请测试账号与验证码通道白名单,补做 §7.1/7.2 正向链路与 429 边界;②将 `docs/e2e-evidence/contract_probe.py` 纳入例行回归真实凭据就绪后可扩展正向断言③UI 层在 CI/真机资源具备时按 §5.2 表逐项补实机截图验收。
## 8. 证据文件索引
| 证据 | 路径 |
|---|---|
| 契约层探测脚本 | `docs/e2e-evidence/contract_probe.py` |
| 契约层机器可读结果 | `docs/e2e-evidence/contract_results.json` |
| 契约层证据汇总表(自动生成) | `docs/e2e-evidence/contract_evidence.md` |
| 原始响应存档30 份,密钥脱敏) | `docs/e2e-evidence/responses/A_*.txt``B_*.txt` |
| 协议层测试源码(静态审阅对象) | `ble-protocol/src/test/kotlin/com/stec/cmd/protocol/**` |
| 协议层实测 JUnit 结果XML + HTML | `docs/e2e-evidence/protocol-test-results/`(含 `TEST-*.xml``html/index.html` |
| 契约依据解析全文 | `docs/e2e-evidence/api_doc_V4.5_extracted.txt`(自 docx 原件解析:`docs/上海城建勘测院监测数据不落地系统接口文档V4.5.docx` |
| 首次连通性探测curl 手工样例) | `docs/e2e-evidence/responses/manual_curl_GetUserInfo_nohdr.txt` |