From a976bf525a1271f1c42eeb55df56d59b50b8499d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E7=8C=AB?= Date: Thu, 3 Sep 2026 13:48:09 +0800 Subject: [PATCH] chore(merge): Update documentation files and file index - Update Android client architecture documentation - Update Android client feature specification - Update Android client development plan - Refresh file index metadata --- .heart/index/files-v1.json | 2 +- docs/安卓客户端主框架架构说明.md | 138 +++++++++++++++++++++++++++++++ docs/安卓客户端功能明细表.md | 133 +++++++++++++++++++++++++++++ docs/安卓客户端开发计划表.md | 67 +++++++++++++++ 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 docs/安卓客户端主框架架构说明.md create mode 100644 docs/安卓客户端功能明细表.md create mode 100644 docs/安卓客户端开发计划表.md diff --git a/.heart/index/files-v1.json b/.heart/index/files-v1.json index 5d26b2f..04fa11c 100644 --- a/.heart/index/files-v1.json +++ b/.heart/index/files-v1.json @@ -1 +1 @@ -{"schemaVersion":1,"projectId":"941ffde0-1f80-49fb-9974-75c4166efaff","localPath":"/home/system/Heart/m-stecmd","builtAt":1788411038210,"files":[{"relPath":"gradlew","mtimeMs":1788410186991.1487,"size":8784},{"relPath":"settings.gradle.kts","mtimeMs":1788410186991.1487,"size":839},{"relPath":"gradlew.bat","mtimeMs":1788410186991.1487,"size":2894},{"relPath":"gradle/wrapper/gradle-wrapper.properties","mtimeMs":1788410186991.1487,"size":253},{"relPath":"gradle/wrapper/gradle-wrapper.jar","mtimeMs":1788410186990.7554,"size":43583},{"relPath":"gradle/libs.versions.toml","mtimeMs":1788410186990.202,"size":4479},{"relPath":"gradle.properties","mtimeMs":1788410186989.9387,"size":341},{"relPath":"docs/安卓客户端开发计划表.md","mtimeMs":1788410186989.9387,"size":3749},{"relPath":"docs/安卓客户端主框架架构说明.md","mtimeMs":1788410186989.9387,"size":7254},{"relPath":"docs/安卓客户端功能明细表.md","mtimeMs":1788410186989.9387,"size":7358},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/ApiError.kt","mtimeMs":1788410186989.9387,"size":1902},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/AuthInterceptor.kt","mtimeMs":1788410186989.9387,"size":1198},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/NetworkConfig.kt","mtimeMs":1788410186989.9387,"size":1164},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/NetworkModule.kt","mtimeMs":1788410186989.9387,"size":2109},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/ApiEnvelope.kt","mtimeMs":1788410186989.7983,"size":1387},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/UploadQueueDao.kt","mtimeMs":1788410186989.351,"size":1970},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/DatabaseModule.kt","mtimeMs":1788410186989.351,"size":831},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/UploadQueueEntity.kt","mtimeMs":1788410186989.351,"size":2131},{"relPath":"core/network/build.gradle.kts","mtimeMs":1788410186989.351,"size":1257},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/AppDatabase.kt","mtimeMs":1788410186989.2122,"size":704},{"relPath":"core/database/build.gradle.kts","mtimeMs":1788410186988.8174,"size":824},{"relPath":"core/common/src/main/kotlin/com/stec/cmd/core/common/AppResult.kt","mtimeMs":1788410186988.8174,"size":1629},{"relPath":"core/common/src/main/kotlin/com/stec/cmd/core/common/UiState.kt","mtimeMs":1788410186988.8174,"size":796},{"relPath":"core/common/src/main/kotlin/com/stec/cmd/core/common/AppLog.kt","mtimeMs":1788410186988.666,"size":1184},{"relPath":"core/common/build.gradle.kts","mtimeMs":1788410186988.18,"size":660},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/BleUuids.kt","mtimeMs":1788410186988.18,"size":1506},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/BleScanner.kt","mtimeMs":1788410186988.18,"size":3253},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/DeviceType.kt","mtimeMs":1788410186988.18,"size":728},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/BleConnector.kt","mtimeMs":1788410186987.9937,"size":7457},{"relPath":"core/ble/build.gradle.kts","mtimeMs":1788410186987.7659,"size":781},{"relPath":"build.gradle.kts","mtimeMs":1788410186987.4011,"size":440},{"relPath":"ble-protocol/src/test/kotlin/com/stec/cmd/protocol/vm208/Vm208FrameParserTest.kt","mtimeMs":1788410186987.4011,"size":3049},{"relPath":"ble-protocol/src/test/kotlin/com/stec/cmd/protocol/ie1000/Ie1000FrameParserTest.kt","mtimeMs":1788410186987.4011,"size":2017},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/vm208/Vm208Reading.kt","mtimeMs":1788410186987.1267,"size":1009},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/vm208/Vm208FrameParser.kt","mtimeMs":1788410186987.1267,"size":4342},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/vm208/Vm208CommandEncoder.kt","mtimeMs":1788410186986.948,"size":1614},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/ie1000/Ie1000Reading.kt","mtimeMs":1788410186986.948,"size":404},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/ie1000/Ie1000FrameParser.kt","mtimeMs":1788410186986.7795,"size":3046},{"relPath":"ble-protocol/build.gradle.kts","mtimeMs":1788410186986.2803,"size":307},{"relPath":"app/src/main/res/xml/network_security_config.xml","mtimeMs":1788410186986.2803,"size":698},{"relPath":"app/src/main/res/values/strings.xml","mtimeMs":1788410186986.2803,"size":465},{"relPath":"app/src/main/res/values/themes.xml","mtimeMs":1788410186986.2803,"size":1107},{"relPath":"app/src/main/res/values/colors.xml","mtimeMs":1788410186986.1257,"size":378},{"relPath":"app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml","mtimeMs":1788410186986.1257,"size":273},{"relPath":"app/src/main/res/menu/bottom_nav_menu.xml","mtimeMs":1788410186985.819,"size":795},{"relPath":"app/src/main/res/layout/fragment_placeholder.xml","mtimeMs":1788410186985.819,"size":1354},{"relPath":"app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml","mtimeMs":1788410186985.819,"size":273},{"relPath":"app/src/main/res/drawable/ic_nav_collect.xml","mtimeMs":1788410186985.3345,"size":557},{"relPath":"app/src/main/res/drawable/ic_nav_task.xml","mtimeMs":1788410186985.3345,"size":373},{"relPath":"app/src/main/res/drawable/ic_nav_stats.xml","mtimeMs":1788410186985.3345,"size":375},{"relPath":"app/src/main/res/drawable/ic_nav_home.xml","mtimeMs":1788410186985.3345,"size":355},{"relPath":"app/src/main/res/drawable/ic_launcher_foreground.xml","mtimeMs":1788410186985.3345,"size":604},{"relPath":"app/src/main/res/drawable/ic_nav_mine.xml","mtimeMs":1788410186985.3345,"size":434},{"relPath":"app/src/main/res/layout/activity_main.xml","mtimeMs":1788410186985.3345,"size":846},{"relPath":"app/src/main/res/drawable/ic_launcher_background.xml","mtimeMs":1788410186985.2454,"size":344},{"relPath":"app/src/main/kotlin/com/stec/cmd/config/ConfigRepository.kt","mtimeMs":1788410186985.1157,"size":4764},{"relPath":"app/src/main/kotlin/com/stec/cmd/StatsFragment.kt","mtimeMs":1788410186984.4575,"size":214},{"relPath":"app/src/main/kotlin/com/stec/cmd/FeatureMount.kt","mtimeMs":1788410186984.4575,"size":1762},{"relPath":"app/src/main/kotlin/com/stec/cmd/SteCmdApplication.kt","mtimeMs":1788410186984.4575,"size":977},{"relPath":"app/src/main/kotlin/com/stec/cmd/MainActivity.kt","mtimeMs":1788410186984.4575,"size":2145},{"relPath":"app/src/main/kotlin/com/stec/cmd/MineFragment.kt","mtimeMs":1788410186984.4575,"size":205},{"relPath":"app/src/main/kotlin/com/stec/cmd/config/AppModule.kt","mtimeMs":1788410186984.4575,"size":1423},{"relPath":"app/src/main/kotlin/com/stec/cmd/PlaceholderFragment.kt","mtimeMs":1788410186984.4575,"size":1325},{"relPath":"app/src/main/kotlin/com/stec/cmd/HomeFragment.kt","mtimeMs":1788410186984.4575,"size":201},{"relPath":"app/src/main/kotlin/com/stec/cmd/TaskFragment.kt","mtimeMs":1788410186984.4575,"size":198},{"relPath":"app/src/main/kotlin/com/stec/cmd/CollectFragment.kt","mtimeMs":1788410186984.328,"size":213},{"relPath":"app/src/main/AndroidManifest.xml","mtimeMs":1788410186984.1328,"size":2271},{"relPath":"app/proguard-rules.pro","mtimeMs":1788410186984.0251,"size":432},{"relPath":"README.md","mtimeMs":1788410186983.202,"size":2837},{"relPath":"app/build.gradle.kts","mtimeMs":1788410186983.202,"size":2070},{"relPath":"docs/API-V4.5.docx","mtimeMs":1788409042297.758,"size":589881},{"relPath":"docs/appid_secertkey.txt","mtimeMs":1788399378820.7002,"size":172},{"relPath":"docs/SystemCode_secertkey.txt","mtimeMs":1788399363441.691,"size":172},{"relPath":"docs/BLE-IE1000.txt","mtimeMs":1788399277710.64,"size":1651}]} \ No newline at end of file +{"schemaVersion":1,"projectId":"941ffde0-1f80-49fb-9974-75c4166efaff","localPath":"/home/system/Heart/m-stecmd","builtAt":1788412889827,"files":[{"relPath":"gradle/wrapper/gradle-wrapper.properties","mtimeMs":1788410186991.1487,"size":253},{"relPath":"gradlew.bat","mtimeMs":1788410186991.1487,"size":2894},{"relPath":"gradlew","mtimeMs":1788410186991.1487,"size":8784},{"relPath":"settings.gradle.kts","mtimeMs":1788410186991.1487,"size":839},{"relPath":"gradle/wrapper/gradle-wrapper.jar","mtimeMs":1788410186990.7554,"size":43583},{"relPath":"gradle/libs.versions.toml","mtimeMs":1788410186990.202,"size":4479},{"relPath":"gradle.properties","mtimeMs":1788410186989.9387,"size":341},{"relPath":"docs/安卓客户端功能明细表.md","mtimeMs":1788410186989.9387,"size":7358},{"relPath":"docs/安卓客户端主框架架构说明.md","mtimeMs":1788410186989.9387,"size":7254},{"relPath":"docs/安卓客户端开发计划表.md","mtimeMs":1788410186989.9387,"size":3749},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/NetworkModule.kt","mtimeMs":1788410186989.9387,"size":2109},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/NetworkConfig.kt","mtimeMs":1788410186989.9387,"size":1164},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/ApiError.kt","mtimeMs":1788410186989.9387,"size":1902},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/AuthInterceptor.kt","mtimeMs":1788410186989.9387,"size":1198},{"relPath":"core/network/src/main/kotlin/com/stec/cmd/core/network/ApiEnvelope.kt","mtimeMs":1788410186989.7983,"size":1387},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/DatabaseModule.kt","mtimeMs":1788410186989.351,"size":831},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/UploadQueueDao.kt","mtimeMs":1788410186989.351,"size":1970},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/UploadQueueEntity.kt","mtimeMs":1788410186989.351,"size":2131},{"relPath":"core/network/build.gradle.kts","mtimeMs":1788410186989.351,"size":1257},{"relPath":"core/database/src/main/kotlin/com/stec/cmd/core/database/AppDatabase.kt","mtimeMs":1788410186989.2122,"size":704},{"relPath":"core/database/build.gradle.kts","mtimeMs":1788410186988.8174,"size":824},{"relPath":"core/common/src/main/kotlin/com/stec/cmd/core/common/UiState.kt","mtimeMs":1788410186988.8174,"size":796},{"relPath":"core/common/src/main/kotlin/com/stec/cmd/core/common/AppResult.kt","mtimeMs":1788410186988.8174,"size":1629},{"relPath":"core/common/src/main/kotlin/com/stec/cmd/core/common/AppLog.kt","mtimeMs":1788410186988.666,"size":1184},{"relPath":"core/common/build.gradle.kts","mtimeMs":1788410186988.18,"size":660},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/BleScanner.kt","mtimeMs":1788410186988.18,"size":3253},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/DeviceType.kt","mtimeMs":1788410186988.18,"size":728},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/BleUuids.kt","mtimeMs":1788410186988.18,"size":1506},{"relPath":"core/ble/src/main/kotlin/com/stec/cmd/core/ble/BleConnector.kt","mtimeMs":1788410186987.9937,"size":7457},{"relPath":"core/ble/build.gradle.kts","mtimeMs":1788410186987.7659,"size":781},{"relPath":"build.gradle.kts","mtimeMs":1788410186987.4011,"size":440},{"relPath":"ble-protocol/src/test/kotlin/com/stec/cmd/protocol/vm208/Vm208FrameParserTest.kt","mtimeMs":1788410186987.4011,"size":3049},{"relPath":"ble-protocol/src/test/kotlin/com/stec/cmd/protocol/ie1000/Ie1000FrameParserTest.kt","mtimeMs":1788410186987.4011,"size":2017},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/vm208/Vm208Reading.kt","mtimeMs":1788410186987.1267,"size":1009},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/vm208/Vm208FrameParser.kt","mtimeMs":1788410186987.1267,"size":4342},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/ie1000/Ie1000Reading.kt","mtimeMs":1788410186986.948,"size":404},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/vm208/Vm208CommandEncoder.kt","mtimeMs":1788410186986.948,"size":1614},{"relPath":"ble-protocol/src/main/kotlin/com/stec/cmd/protocol/ie1000/Ie1000FrameParser.kt","mtimeMs":1788410186986.7795,"size":3046},{"relPath":"ble-protocol/build.gradle.kts","mtimeMs":1788410186986.2803,"size":307},{"relPath":"app/src/main/res/xml/network_security_config.xml","mtimeMs":1788410186986.2803,"size":698},{"relPath":"app/src/main/res/values/strings.xml","mtimeMs":1788410186986.2803,"size":465},{"relPath":"app/src/main/res/values/themes.xml","mtimeMs":1788410186986.2803,"size":1107},{"relPath":"app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml","mtimeMs":1788410186986.1257,"size":273},{"relPath":"app/src/main/res/values/colors.xml","mtimeMs":1788410186986.1257,"size":378},{"relPath":"app/src/main/res/layout/fragment_placeholder.xml","mtimeMs":1788410186985.819,"size":1354},{"relPath":"app/src/main/res/menu/bottom_nav_menu.xml","mtimeMs":1788410186985.819,"size":795},{"relPath":"app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml","mtimeMs":1788410186985.819,"size":273},{"relPath":"app/src/main/res/layout/activity_main.xml","mtimeMs":1788410186985.3345,"size":846},{"relPath":"app/src/main/res/drawable/ic_launcher_foreground.xml","mtimeMs":1788410186985.3345,"size":604},{"relPath":"app/src/main/res/drawable/ic_nav_mine.xml","mtimeMs":1788410186985.3345,"size":434},{"relPath":"app/src/main/res/drawable/ic_nav_home.xml","mtimeMs":1788410186985.3345,"size":355},{"relPath":"app/src/main/res/drawable/ic_nav_task.xml","mtimeMs":1788410186985.3345,"size":373},{"relPath":"app/src/main/res/drawable/ic_nav_collect.xml","mtimeMs":1788410186985.3345,"size":557},{"relPath":"app/src/main/res/drawable/ic_nav_stats.xml","mtimeMs":1788410186985.3345,"size":375},{"relPath":"app/src/main/res/drawable/ic_launcher_background.xml","mtimeMs":1788410186985.2454,"size":344},{"relPath":"app/src/main/kotlin/com/stec/cmd/config/ConfigRepository.kt","mtimeMs":1788410186985.1157,"size":4764},{"relPath":"app/src/main/kotlin/com/stec/cmd/FeatureMount.kt","mtimeMs":1788410186984.4575,"size":1762},{"relPath":"app/src/main/kotlin/com/stec/cmd/MainActivity.kt","mtimeMs":1788410186984.4575,"size":2145},{"relPath":"app/src/main/kotlin/com/stec/cmd/MineFragment.kt","mtimeMs":1788410186984.4575,"size":205},{"relPath":"app/src/main/kotlin/com/stec/cmd/StatsFragment.kt","mtimeMs":1788410186984.4575,"size":214},{"relPath":"app/src/main/kotlin/com/stec/cmd/PlaceholderFragment.kt","mtimeMs":1788410186984.4575,"size":1325},{"relPath":"app/src/main/kotlin/com/stec/cmd/SteCmdApplication.kt","mtimeMs":1788410186984.4575,"size":977},{"relPath":"app/src/main/kotlin/com/stec/cmd/TaskFragment.kt","mtimeMs":1788410186984.4575,"size":198},{"relPath":"app/src/main/kotlin/com/stec/cmd/HomeFragment.kt","mtimeMs":1788410186984.4575,"size":201},{"relPath":"app/src/main/kotlin/com/stec/cmd/config/AppModule.kt","mtimeMs":1788410186984.4575,"size":1423},{"relPath":"app/src/main/kotlin/com/stec/cmd/CollectFragment.kt","mtimeMs":1788410186984.328,"size":213},{"relPath":"app/src/main/AndroidManifest.xml","mtimeMs":1788410186984.1328,"size":2271},{"relPath":"app/proguard-rules.pro","mtimeMs":1788410186984.0251,"size":432},{"relPath":"README.md","mtimeMs":1788410186983.202,"size":2837},{"relPath":"app/build.gradle.kts","mtimeMs":1788410186983.202,"size":2070},{"relPath":"docs/API-V4.5.docx","mtimeMs":1788409042297.758,"size":589881},{"relPath":"docs/appid_secertkey.txt","mtimeMs":1788399378820.7002,"size":172},{"relPath":"docs/SystemCode_secertkey.txt","mtimeMs":1788399363441.691,"size":172},{"relPath":"docs/BLE-IE1000.txt","mtimeMs":1788399277710.64,"size":1651}]} \ No newline at end of file diff --git a/docs/安卓客户端主框架架构说明.md b/docs/安卓客户端主框架架构说明.md new file mode 100644 index 0000000..ee998bb --- /dev/null +++ b/docs/安卓客户端主框架架构说明.md @@ -0,0 +1,138 @@ +# 安卓客户端主框架架构说明 + +> S0 工程骨架交付(2026-09-03)。本文档说明主框架分层、模块职责、关键约定与业务挂载方法,是 S1–S4 功能开发的挂载依据。 + +## 1. 架构总览 + +```mermaid +flowchart TD + APP[":app 业务壳
导航 · 主题 · 权限 · 配置注入
M1–M6 页面挂载点"] + subgraph CORE["core 基础层"] + NET[":core:network
鉴权三要素 · 统一解析"] + DB[":core:database
Room · 上传队列"] + BLE[":core:ble
扫描 · GATT 封装"] + COMMON[":core:common
AppResult · UiState · 日志"] + end + PROTO[":ble-protocol
IE-1000 / VM208 帧解析
纯 Kotlin JVM · 零依赖"] + + APP --> NET + APP --> DB + APP --> BLE + APP --> PROTO + NET --> COMMON + DB --> COMMON + BLE --> COMMON + BLE -. "Notify 字节流" .-> PROTO +``` + +**依赖规则**(静态校验脚本强制核查): + +| 规则 | 说明 | +|---|---| +| app → core:* + ble-protocol | 业务壳是唯一允许依赖全部模块的层 | +| core:{network,database,ble} → core:common | core 模块横向互不依赖 | +| ble-protocol 零依赖 | 纯 Kotlin JVM,无 Android / 第三方框架依赖 | +| 业务代码禁止绕过 core 层 | S1+ feature 只允许经 core 能力访问网络/存储/蓝牙 | + +## 2. 模块职责与关键类 + +| 模块 | 职责 | 关键类 | +|---|---|---| +| `:app` | 单 Activity 导航壳、5 Tab、权限、配置注入 | `SteCmdApplication` `MainActivity` `FeatureMount` `config/ConfigRepository` | +| `:core:common` | 统一结果/视图状态/日志门面 | `AppResult` `UiState` `AppLog` | +| `:core:network` | 平台 HTTP 框架:鉴权三要素 + 返回码统一解析 | `NetworkConfig` `TokenProvider` `AuthInterceptor` `ApiEnvelope` `ApiError` `NetworkModule` | +| `:core:database` | Room 离线优先存储 + 上传队列参考实现 | `AppDatabase` `UploadQueueEntity` `UploadQueueDao` `DatabaseModule` | +| `:core:ble` | BLE 扫描(名称前缀过滤)与 GATT 连接骨架 | `BleScanner` `BleConnector` `BleSession` `BleUuids` `DeviceType` | +| `:ble-protocol` | 两类设备协议帧解析/编码(独立单测) | `Ie1000FrameParser` `Vm208FrameParser` `Vm208CommandEncoder` `Ie1000Reading` `Vm208Reading` | + +## 3. 关键约定 + +### 3.1 鉴权三要素流(接口文档 1.2) + +``` +ConfigRepository(DataStore+内存快照, app 壳实现 NetworkConfig/TokenProvider) + └→ AuthInterceptor(core:network): 请求头注入 SecretKey / SystemCode / Token + · Token 为空跳过(登录/验证码接口天然不携带) + · 密钥值不进代码库(M7-04),运行期经 S1 配置页写入 DataStore +``` + +Hilt 绑定:`config/AppModule.kt` 中 `@Binds` 将 `ConfigRepository` 绑定为 `NetworkConfig` 与 `TokenProvider`,补全 `NetworkModule` 依赖图。 + +### 3.2 返回码统一解析(接口文档 1.1) + +| code | 语义 | 异常类型 | +|---|---|---| +| "200" | 成功 | —(`ApiEnvelope.bodyOrThrow()` 返回 data) | +| "500" | 业务失败 | `ApiError.ServerError` | +| "429" | 一分钟内同条数据频繁请求超限 | `ApiError.TooManyRequests` | +| "403" | 同条数据连续报错超限(平台锁定) | `ApiError.Locked` | +| 传输层失败 | 超时/断网 | `ApiError.NetworkError` | + +code 为**字符串**类型;S1+ 业务层只捕获 `ApiError` 并按子类提示。 + +### 3.3 协议解析流(BLE Notify → 上传队列) + +``` +BleSession.notifyBytes(ByteArray 流) + └→ Ie1000FrameParser / Vm208FrameParser.feed(bytes) ← 内部缓冲,天然处理粘包/残包 + └→ Ie1000Reading / Vm208Reading + └→ 组装上传 JSON → UploadQueueDao.enqueue() ← 离线优先,网络可用后补传 +``` + +- IE-1000:`'Y' + 4B float(压力,小端) + 'T' + 4B float(温度) + 0D0A`,12 字节定长; +- VM208:`CC F原值 T原值 E原值 S质量 AA + 0D0A`,缩放 /10 /10 /100; +- VM208 激励命令:`0F 0F 06 0A XX XX 0D 0A`(大端 16 位),`Vm208CommandEncoder`。 +- ⚠ VM208 服务 UUID 原文档仅 31 位十六进制,`BleUuids` 按末位补 0 占位,**S2 真机联调时用 nRF Connect 校准**。 + +### 3.4 网络安全白名单 + +`app/src/main/res/xml/network_security_config.xml`:默认全域禁明文;仅接口文档 1.2.4 测试 IP `106.15.183.20` 开放 cleartext(联调用,发布前如生产全 HTTPS 可移除该 domain-config)。 + +## 4. 业务挂载指南(S1 挂载步骤) + +挂载机制:`FeatureMount.mounts` 注册表声明每个 Tab 的 Fragment 类;导航壳只认注册表,业务页替换注册项即可。 + +以 S1 首页(M2 项目列表)为例: + +1. **建 Fragment**:`app/src/main/kotlin/com/stec/cmd/feature/home/HomeFragment.kt`,`@AndroidEntryPoint` + ViewBinding(或迁移至团队选定的 UI 方案); +2. **建 ViewModel**:`HomeViewModel`,注入 S1 新建的 Retrofit Service(声明在 core:network 或独立 feature 模块); +3. **换注册项**:`FeatureMount.mounts` 中 `Tab.HOME` 的 `fragmentClass` 从 `HomeFragment::class.java`(占位)改为业务 Fragment; +4. **加接口**:Retrofit Service 方法返回 `ApiEnvelope`,调用侧 `bodyOrThrow()` 拿业务数据、捕获 `ApiError`; +5. **跑校验**:`./gradlew :app:assembleDebug`,并确认静态校验脚本(见 §6)通过(menu↔FeatureMount id 对应关系不受影响)。 + +M1(我的)额外步骤:配置页 UI 读写 `ConfigRepository.updateConnection()/updateToken()`(M1-06)。 + +## 5. 目录结构对照 + +``` +SteCmdAndroid/ +├─ app/ # 业务壳(本表 §2 :app) +├─ core/ +│ ├─ common/ # AppResult / UiState / AppLog +│ ├─ network/ # AuthInterceptor / ApiEnvelope / ApiError / NetworkModule +│ ├─ database/ # AppDatabase / UploadQueue 实体与 DAO / DatabaseModule +│ └─ ble/ # BleScanner / BleConnector / BleSession / BleUuids / DeviceType +├─ ble-protocol/ # 协议解析(src/main + src/test 冒烟单测) +├─ gradle/ +│ └─ libs.versions.toml # 版本目录:全工程唯一版本事实源 +├─ docs/ # 规划文档、协议原文、本说明 +├─ settings.gradle.kts # 6 模块注册 +└─ build.gradle.kts # 根:插件版本声明(apply false) +``` + +## 6. 环境与验证 + +| 场景 | 命令 / 操作 | +|---|---| +| 首次打开 | Android Studio(JDK 17)打开工程根目录,等待 Gradle Sync | +| 编译 | `./gradlew :app:assembleDebug` | +| 协议单测 | `./gradlew :ble-protocol:test` | +| 静态校验(本机已通过 198 项) | `python3 /tmp/verify_core.py && python3 /tmp/verify_app.py`(脚本随会话生成) | +| 真机联调 | 配置页写入三要素 + 测试地址后,先验证登录(M1-01) | + +## 7. 已知遗留(S1 前处理) + +1. Gradle Sync / 编译 / 单测**尚未在真实环境执行**(交付环境无 JDK/Android SDK); +2. VM208 服务 UUID 需真机校准(§3.3); +3. splash 与导航图标为几何占位,S6 发布前替换正式视觉; +4. 密钥(SecretKey/SystemCode)首次运行需经配置页写入;协议文档附带密钥文件仅用于联调,不得入库。 diff --git a/docs/安卓客户端功能明细表.md b/docs/安卓客户端功能明细表.md new file mode 100644 index 0000000..dadac6f --- /dev/null +++ b/docs/安卓客户端功能明细表.md @@ -0,0 +1,133 @@ +# 安卓客户端功能明细表 + +> 2026-09-03 重建版:原文档随工作树清理丢失,按任务记录与接口解析稿(V4.5)复刻。接口清单以解析稿为准(15 个在用 + 2 个废除子接口)。 + +## 文档属性 + +| 属性 | 值 | +|---|---| +| 项目 | 上海城建勘测院 · 监测数据不落地平台安卓客户端 | +| 依据 | 接口管理规范 V4.5、BLE-IE1000/VM208 蓝牙协议说明 | +| 优先级定义 | P0=应用可发布的最小闭环;P1=重要;P2=增强 | +| 功能点合计 | 36(P0×23 / P1×11 / P2×2) | + +## 模块总览 + +| 模块 | 名称 | 功能点数 | P0 | P1 | P2 | +|---|---|---|---|---|---| +| M1 | 我的(登录与配置) | 7 | 5 | 1 | 1 | +| M2 | 首页(项目工作台) | 3 | 2 | 1 | 0 | +| M3 | 任务管理 | 5 | 3 | 2 | 0 | +| M4 | 采集·蓝牙 | 9 | 7 | 2 | 0 | +| M5 | 采集·测点数据 | 4 | 0 | 3 | 1 | +| M6 | 统计与数据上传 | 4 | 2 | 2 | 0 | +| M7 | 基础工程层 | 4 | 4 | 0 | 0 | +| 合计 | | 36 | 23 | 11 | 2 | + +## M1 我的(登录与配置) + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M1-01 | 账号密码登录 | 2.2.1 | P0 | 请求头带 SecretKey/SystemCode,成功取得 Token | +| M1-02 | 获取短信验证码 | 2.2.2 | P0 | 手机号 +86 需转 %2B86 | +| M1-03 | 验证码登录 | 2.2.3 | P0 | 国内手机号全匹配校验 | +| M1-04 | 修改登录密码 | 2.3.1 | P1 | 登录态下操作 | +| M1-05 | 当前用户信息展示 | 2.4 | P0 | 头像/姓名/角色等 | +| M1-06 | 服务器与密钥配置管理 | 无(本地) | P0 | baseUrl/SecretKey/SystemCode/Token 持久化(DataStore+加密),M7-04 落地入口 | +| M1-07 | 退出登录与 Token 清理 | 无(本地) | P2 | 清除本地 Token 与缓存 | + +## M2 首页(项目工作台) + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M2-01 | 当前用户项目列表 | 2.5.1.1 | P0 | 外业人员可参与多项目 | +| M2-02 | 项目基本信息展示 | 2.5.1.1(data) | P0 | 项目名称/编号/状态 | +| M2-03 | 项目切换 | 2.5.1.1(本地选中态) | P1 | 切换后任务/统计跟随刷新 | + +## M3 任务管理 + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M3-01 | APP 端任务列表 | 2.6 | P0 | 外业人员当日/近期任务 | +| M3-02 | 项目监测计划任务列表 | 2.7 | P0 | 按监测计划组织 | +| M3-03 | 任务详情 | 2.6/2.7(data) | P0 | 测组、测点、计划周期等 | +| M3-04 | 任务状态流转展示 | 2.6/2.7(data) | P1 | 未开始/进行中/已完成 | +| M3-05 | 任务筛选与下拉刷新 | 2.6/2.7 | P1 | 按状态/时间过滤 | + +## M4 采集·蓝牙 + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M4-01 | 蓝牙与定位权限申请 | 无(系统) | P0 | Android 12L 分版(BLUETOOTH_SCAN/CONNECT vs 旧版+定位) | +| M4-02 | 设备扫描 | 无(BLE) | P0 | 按名称前缀过滤:IE-1000、VM208 | +| M4-03 | 设备连接管理 | 无(BLE GATT) | P0 | 两设备各有服务/写/Notify UUID;连接状态流 | +| M4-04 | IE-1000 数据接收解析 | 无(BLE Notify) | P0 | 帧 `'Y'+4B float+'T'+4B float+0D 0A`(压力/温度) | +| M4-05 | VM208 数据接收解析 | 无(BLE Notify) | P0 | 帧 `CC F%05d T%03d E%05d S%c AA+0D 0A`;频率/10Hz、温度/10℃、电压/100V | +| M4-06 | VM208 激励命令下发 | 无(BLE Write) | P0 | `0F 0F 06 0A XX XX 0D 0A`,XX XX 大端 16 位 | +| M4-07 | 采集数据本地暂存 | 无(Room) | P0 | 先落本地队列,网络可用后上传(离线优先) | +| M4-08 | 采集会话管理 | 无(本地) | P1 | 开始/停止采集、绑定任务与测点 | +| M4-09 | 断连与异常处理 | 无(BLE) | P1 | 断连提示、自动重连策略 | + +## M5 采集·测点数据 + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M5-01 | 测组监测点列表 | 2.9 | P1 | 按测组拉取监测点 | +| M5-02 | 基准点获取 | 2.14 | P1 | 水平位移等计算的基准 | +| M5-03 | 工点数据 | 2.15 | P1 | 工点/测项结构(目录页 2.15 笔误 ControlPoint,以正文为准) | +| M5-04 | 手动录入/补录 | 2.13(配合) | P2 | 无法蓝牙采集时人工录入后走同一上传队列 | + +## M6 统计与数据上传 + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M6-01 | 原始文件上传与解析 | 2.13 | P0 | 数据解析(原始文件上传),采集文件的兜底通道 | +| M6-02 | 项目监测计划统计表 | 2.10 | P0 | 项目维度完成度 | +| M6-03 | 测组统计表 | 2.11 | P1 | 指定监测计划的测组统计 | +| M6-04 | 监测点统计表 | 2.12 | P1 | 测点维度历史数据 | + +## M7 基础工程层 + +| 编号 | 功能点 | 对应 API | 优先级 | 说明 | +|---|---|---|---|---| +| M7-01 | 网络框架 | 全部 | P0 | 鉴权三要素拦截器 + 200/500/429/403 统一解析(core:network) | +| M7-02 | 本地存储框架 | 无 | P0 | Room 队列 + 加密配置存储(core:database + DataStore) | +| M7-03 | 协议解析独立模块 | 无 | P0 | 纯 Kotlin JVM,脱离真机可单测(ble-protocol) | +| M7-04 | 密钥配置管理 | 无 | P0 | SecretKey/SystemCode 不进代码库,可随时更换(.gitignore + ConfigRepository) | + +## 第 3 节 待确认问题(8 项) + +| # | 问题 | 缺省处理 | +|---|---|---| +| 1 | SecretKey/SystemCode 的分发与更换流程 | 做成配置项,支持不换包更换 | +| 2 | Token 有效期与刷新机制 | 无刷新接口,过期按 401/500 重新登录 | +| 3 | 429/403 的解锁规则与窗口 | 按接口文档语义提示用户稍后重试 | +| 4 | 任务状态是否允许 APP 端上报变更 | S0 只读展示,不上报 | +| 5 | 2.13 原始文件上传的文件格式与大小限制 | 按采集仪原始文件直传 | +| 6 | 监测值上传是否走 2.8(已废除)替代接口 | 以 2.13 + 待定接口为准,待院方确认 | +| 7 | 蓝牙后台采集是否需要 | 默认前台采集 | +| 8 | 多项目并行采集的合并口径 | 单项目单会话 | + +## 附录 A 接口覆盖对照 + +| 章节 | 接口名称 | 状态 | 承载模块 | +|---|---|---|---| +| 2.2.1 | 账号密码登录 | 在用 | M1-01 | +| 2.2.2 | 发送验证码 | 在用 | M1-02 | +| 2.2.3 | 手机验证码登录 | 在用 | M1-03 | +| 2.3.1 | 修改登录密码 | 在用 | M1-04 | +| 2.4 | 获取当前用户信息 | 在用 | M1-05 | +| 2.5.1.1 | 获取当前用户项目列表 | 在用 | M2-01/02/03 | +| 2.6 | 获取 APP 端任务列表 | 在用 | M3-01/03/04/05 | +| 2.7 | 获取项目监测计划任务列表 | 在用 | M3-02/03/04/05 | +| 2.8.1 | 获取监测计划 | 废除 | 不实现 | +| 2.8.2 | 上传本次监测值 | 废除 | 不实现(上传通道待院方确认,见待确认 #6) | +| 2.9 | 获取测组包含的监测点列表 | 在用 | M5-01 | +| 2.10 | 获取项目监测计划统计表 | 在用 | M6-02 | +| 2.11 | 获取指定监测计划的测组统计表 | 在用 | M6-03 | +| 2.12 | 监测点统计表 | 在用 | M6-04 | +| 2.13 | 数据解析(原始文件上传) | 在用 | M6-01、M5-04 | +| 2.14 | 获取基准点 | 在用 | M5-02 | +| 2.15 | 获取工点数据 | 在用 | M5-03 | + +> 返回状态值约定(接口文档 1.1):200 成功;500 失败;429 同一条数据一分钟内频繁请求超限;403 同一条数据连续报错超限。 diff --git a/docs/安卓客户端开发计划表.md b/docs/安卓客户端开发计划表.md new file mode 100644 index 0000000..e7c34a5 --- /dev/null +++ b/docs/安卓客户端开发计划表.md @@ -0,0 +1,67 @@ +# 安卓客户端开发计划表 + +> 2026-09-03 重建版:原文档随工作树清理丢失,按任务记录复刻。工作量与阶段划分保持原口径。 + +## 技术基线 + +| 项 | 选型 | +|---|---| +| 语言 | Kotlin 2.0.21 | +| 架构 | MVVM + Hilt 2.52 + Coroutines 1.9.0 | +| 网络 | Retrofit 2.11.0 + OkHttp 4.12.0 + kotlinx.serialization 1.7.3 | +| 本地存储 | Room 2.6.1(离线优先)+ DataStore 1.1.1 + EncryptedSharedPreferences | +| 蓝牙 | Android 原生 BLE;协议解析器独立为纯 Kotlin JVM 模块(ble-protocol) | +| 构建 | AGP 8.7.3 / Gradle 8.10.2 / JDK 17 / minSdk 26 / targetSdk 34 | +| 测试环境 | https://jcd.stec.p-q.co(白名单,见 network_security_config) | + +## 阶段与迭代计划(6 阶段,12 周) + +| 阶段 | 内容 | 工作量 | 周期 | 依赖 | +|---|---|---|---|---| +| S0 工程骨架 | Gradle 多模块、core 基础层、ble-protocol、app 壳(导航/主题/权限/配置) | 4 人日 | 0.5 周 | — | +| S1 登录与首页 | M1 全部 + M2 + M7 配置注入 + 修改密码 | 22 人日 | 2 周 | S0 | +| S2 任务与采集框架 | M3 全部 + M4 骨架(权限/扫描/连接) | 24 人日 | 2 周 | S1 | +| S3 蓝牙采集深化 | M4 解析/下发/暂存/会话 + M5 测点数据 | 28 人日 | 3 周 | S2 | +| S4 统计与上传 | M6 全部 + 离线队列上传调度 | 12 人日 | 2 周 | S3 | +| S5 联调与测试 | 接口联调、真机回归、性能与弱网测试 | 20 人日 | 2 周 | S4 | +| S6 发布准备 | 签名、混淆、应用商店/院内分发、文档 | 4 人日 | 0.5 周 | S5 | + +- 迭代工作量合计:4+22+24+28+12+4 = **94 人日**(其中开发 86 + S0/S6 管理 8) +- 测试 20 人日(S5)+ 管理发布 8 人日 → **总计约 114 人日** + +## 模块 × 工作量矩阵(开发 86 人日) + +| 模块 | M1 我的 | M2 首页 | M3 任务 | M4 采集·蓝牙 | M5 测点 | M6 统计 | M7 基础层 | 合计 | +|---|---|---|---|---|---|---|---|---| +| 人日 | 12 | 6 | 10 | 28 | 12 | 8 | 10 | 86 | + +## 里程碑(MS1–MS6) + +| 里程碑 | 时点 | 验收标准 | +|---|---|---| +| MS1 工程可构建 | S0 末(第 0.5 周) | Android Studio 打开即 Sync,:app:assembleDebug 通过,协议单测绿 | +| MS2 登录闭环 | S1 末(第 2.5 周) | 三种登录方式 + Token 管理 + 项目列表可用 | +| MS3 任务可用 | S2 末(第 4.5 周) | 任务列表/详情完整,BLE 扫描连接真机通过 | +| MS4 采集闭环 | S3 末(第 7.5 周) | 真机采集→解析→本地暂存全链路跑通 | +| MS5 数据闭环 | S4 末(第 9.5 周) | 采集数据上传成功,弱网队列补传可用 | +| MS6 发布 | S6 末(第 12 周) | 签名包通过验收测试,交付文档齐全 | + +## 风险与应对(8 项) + +| # | 风险 | 应对 | +|---|---|---| +| 1 | SecretKey/SystemCode 更换频繁 | 全配置化,支持不换包更换(M7-04) | +| 2 | 429/403 限流误伤外业作业 | 队列退避重试 + 明确用户提示 | +| 3 | BLE 各厂商兼容性差异 | 主流机型真机矩阵测试;协议层独立单测降低解析回归成本 | +| 4 | 弱网/断网场景数据丢失 | Room 离线队列先落盘,网络恢复自动补传 | +| 5 | 接口变更(V4.5 之后) | kotlinx.serialization 显式注解,字段演进留默认值 | +| 6 | 2.8 废除后的上传通道未定 | 预留 UploadQueue 抽象,通道可替换 | +| 7 | 真机依赖与外业时间窗 | S0–S2 期间提前申请设备与测试账号 | +| 8 | 人员并行度 | M4(28 人日)为关键路径,S3 阶段优先投入 | + +## 交付物清单 + +- 可发布 APK(签名、混淆配置齐备) +- 源码工程(本仓库)+ 架构说明文档 +- 测试报告(S5 产出) +- 《安卓客户端功能明细表》(接口覆盖对照)