release: harden Android network security

This commit is contained in:
key
2026-07-29 01:13:18 +08:00
parent 9ea5d79a62
commit ba68fc8326
16 changed files with 198 additions and 29 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
## Unreleased
- **2026-07-28 至 2026-07-29,8 月 1 日 Android 受控内测候选包与法务链路,当前总体 No-Go**:补齐登录页协议默认不勾选与法务地址失败关闭、App-Plus 原生隐私配置生成/校验、APK 包名/版本/API/签名/哈希/包内资源一致性/敏感值门禁、DCloud 云证书自定义基座和独立测试包流程;《服务协议》《隐私政策》静态页已部署到 `/legal/`,发布前整站备份为 `/opt/wygj/backups/www-20260728224710`。客户端法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码也不再以源码常量进入安装包。当前独立 APK `com.yincheng.wygj 0.1.6 (106)` 的 `targetSdkVersion=35`,SHA-256 为 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914`;vivo V2443A(Android 15)已完成覆盖安装和冷启动,本机 `Medium_Phone` Android 15/API 35 虚拟机已完成最新包干净安装和冷启动,真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃。移动端 191 项测试、类型检查、H5/App 构建、管理端构建、P0 后端 39 项定向测试及 `ruoyi-aihr` 696 项全量测试通过;跨平台源码契约测试已统一换行符后复跑全绿。本次评估时工作区尚未提交,生产后端未包含本批 P0 回退语义,用户隐私选择、权限反例、认证态核心业务、正式内容和完整发布预检仍未完成,故 Go/No-Go 为 **No-Go**;本次除法务静态页外未发布后端、H5 业务资源或数据库。
- **2026-07-28 至 2026-07-29,8 月 1 日 Android 受控内测候选包与法务链路,当前总体 No-Go**:补齐登录页协议默认不勾选与法务地址失败关闭、App-Plus 原生隐私配置生成/校验、APK 包名/版本/API/签名/哈希/包内资源一致性/敏感值门禁、DCloud 云证书自定义基座和独立测试包流程;《服务协议》《隐私政策》静态页已部署到 `/legal/`,发布前整站备份为 `/opt/wygj/backups/www-20260728224710`。客户端法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码也不再以源码常量进入安装包。当前独立 APK `com.yincheng.wygj 0.1.8 (108)` 的 `targetSdkVersion=35`,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`;新增 Android 原生网络安全配置并经 APK 反编译确认 `usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均已进入安装包。本机 `Medium_Phone` Android 15/API 35 虚拟机已完成最新包干净安装和冷启动,真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃;`0.1.6` 已在 vivo V2443A(Android 15)完成覆盖安装和冷启动。移动端 193 项测试、类型检查和 App 构建预检通过;前一冻结批次的 H5/管理端构建及 `ruoyi-aihr` 696 项全量测试通过。生产后端仍未包含本批 P0 回退语义,用户隐私选择、权限反例、认证态核心业务、正式内容和完整发布预检仍未完成,故 Go/No-Go 为 **No-Go**;本次除法务静态页外未发布后端、H5 业务资源或数据库。
- **2026-07-26,阿里云百炼 ASR 生产热修复**:`POST /api/ai/asr` 新增 `dashscope/qianwen + qwen3-asr-flash` 的 `/compatible-mode/v1/chat/completions` + Base64 `input_audio` 分支,原 SiliconFlow `/audio/transcriptions` multipart 路径继续兼容,ASR 超时放宽至 30 秒。定向 Maven 测试 10 项通过;后端 JAR(SHA-256 `46c99cff75d21f8536a71c3c058b6437e99dbf9ff4bae3542b44471f3f34ad84`)已发布,生产 ASR 已切换至 `dashscope/qwen3-asr-flash`,旧 `siliconflow/TeleAI/TeleSpeechASR` 已停用。生产接口获得正确转写与受控 `ossId`,Android HBuilderX 标准基座“按住说话”请求成功,线上日志无 403/ASR 异常。回滚目录为 `/opt/wygj/backups/asr-hotfix-20260726210122`;本次未发布静态资源、未变更 schema,也不代表完整包一致性或原生 App 全项验收。
- **2026-07-26,移动端编译链升级与 uni-ui 解析修复,未发布**:本机 HBuilderX GUI 已升级为 `5.22.2026071707`,CLI 为 `5.22.2026072503-alpha`;`mobile-uni` 的 App、App-Plus、H5 与 Vite 编译包统一到对应的 `3.0.0-alpha-5020220260725001` 批次,消除 5.15/5.21 混用产生的两套 `uni-cli-shared` 状态隔离,使登录页 `<uni-forms>`、`<uni-easyinput>` 等组件重新由 `pages.json` easycom 规则正常解析。H5 与 App 均以 `Compiler version: 5.22(vue3)` 构建通过;浏览器和 vivo V2443A(Android 15)真机均确认手机号、验证码标签及输入框正常。HBuilder 5.22 标准基座 `versionName=15.22` 已自动更新并成功运行,测试手机号配合固定验证码登录成功,可进入员工首页并切换“问”页,未出现编译器/SDK 版本不匹配弹窗或组件解析告警。升级后仍须重建并安装 5.22 自定义运行基座完成原生权限全清单;`verify:app-release` 当前因缺少含正式法务 HTTPS 链接的 `androidPrivacy.json` 按预期阻断。本项未发布生产 H5,也未云打包 APK/IPA,手机上的 `com.yincheng.wygj` 正式包未被覆盖。
- **2026-07-26,全网 AI 本地库修复,未发布**:补齐 `reset-dev-db.sh` 遗漏的全网 AI 基表、数据库密钥和千问字段三项正式迁移,并增加迁移顺序及幂等回归,避免重置本地库后再次缺失 `aihr_web_search_provider`;当前本地库为 137 张表,管理端代理下的提供方列表接口验证通过。
+1 -1
View File
@@ -11,7 +11,7 @@
- 品牌:管理端与员工端统一显示“帮道”,继续使用现有红色品牌图标。
- 基础链路:租户列表、验证码、`000000 / admin / admin123` 加密登录已验证。
- 管理端:除 AI 面试、训练、案例、SOP 和资料处理外,已包含大喇叭发布、单文件提炼与多岗位解读、五渠道直通反馈收件箱、内容/任务运营、问题榜运营、知识空间、全网 AI 配置和组织权限;员工个人训练、主管复盘、团队难题、成长证据与训练录音仅在认证 APP 按项目范围访问。
- 用户侧:`mobile-uni/` 承载员工、候选人和主管三端;“问”已改为数字师傅 Agent,文字、ASR 转写和图片/视频统一先做意图规划,再按服务端权限调用知识、训练、待办、记忆、媒体或全网工具,寒暄和不完整表达不再机械落入 RAG。现有能力还包括手机号登录、30 分钟短期多轮、原文件/视频受控交付、大喇叭公司文件摘要/下载/多岗位解读/追问、五渠道直通反馈、“记一下”确认卡、三级字号、训练/题目反馈、问题榜与积分、岗位考试、案例、候选面试/资料、主管复盘/派发及学员模式。Agent 代码已随 2026-07-25 完整包部署;2026-07-29 当前候选包为 `0.1.6 (106)`,已通过 API 35、法务链接、包内资源一致性和敏感值门禁,并在 Android 15 真机完成覆盖安装、在本机 Android 15/API 35 虚拟机完成干净安装和冷启动。最新包已真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃;用户亲自执行的隐私选择、权限正反例、认证态核心业务全回归、生产依赖匹配和发布签认仍未完成,当前 Go/No-Go 仍为 No-Go。旧 `mobile/` 只用于历史回溯。
- 用户侧:`mobile-uni/` 承载员工、候选人和主管三端;“问”已改为数字师傅 Agent,文字、ASR 转写和图片/视频统一先做意图规划,再按服务端权限调用知识、训练、待办、记忆、媒体或全网工具,寒暄和不完整表达不再机械落入 RAG。现有能力还包括手机号登录、30 分钟短期多轮、原文件/视频受控交付、大喇叭公司文件摘要/下载/多岗位解读/追问、五渠道直通反馈、“记一下”确认卡、三级字号、训练/题目反馈、问题榜与积分、岗位考试、案例、候选面试/资料、主管复盘/派发及学员模式。Agent 代码已随 2026-07-25 完整包部署;2026-07-29 当前候选包为 `0.1.8 (108)`,已通过 API 35、法务链接、包内资源一致性、敏感值、禁止明文 HTTP 和网络安全资源门禁,并在本机 Android 15/API 35 虚拟机完成干净安装和冷启动。最新包已真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃;`0.1.6` 已在 Android 15 真机完成覆盖安装。用户亲自执行的隐私选择、权限正反例、认证态核心业务全回归、生产依赖匹配和发布签认仍未完成,当前 Go/No-Go 仍为 No-Go。旧 `mobile/` 只用于历史回溯。
- 英雄路径:AI 面试(真模型出题/评分)、三角色对练(真 LLM 客户回复+评分,ASR/TTS 配置后支持语音)、案例沉淀(真实语音上传+ASR 转写+模型整理)均为真模型优先、未配置或失败回退本地兜底;生产短音频 ASR 当前为阿里云百炼 `qwen3-asr-flash`,SiliconFlow audio 接口保留兼容。SOP 知识库已接中文关键词 + MySQL Fulltext + Qdrant 混合召回 + rerank 重排,支持 txt/md/PDF/Word/Excel/PPT/图片/视频上传解析入库(图片走 vision OCR,视频走 ffmpeg 抽音轨转写+关键帧描述),资料处理中心支持 ZIP 异步展开后逐文件入队;模型配置页可查看向量库维度/点数并一键重建。
- 资料处理:已新增解析任务状态页,按 `aihr_knowledge_attach` 和 `aihr_knowledge_fragment` 展示等待解析、解析中、已完成、失败、片段数和向量化状态;任务列表按当前筛选结果前端分页(10/20/50 条,筛选或刷新回到第一页)。页面只保留“批量导入”,通过异步队列处理多文件或 ZIP,不提供浏览器目录选择和服务端目录导入入口。后端目录导入接口仅保留为运维/调试能力。
- 组织人员:`aihr_org_snapshot` 本地 reset 保留演示 seed;生产通过 `POST /api/aihr/org/sync` 同步外部组织。2026-07-25 记录的生产快照为 3417 人、2938 人在职且均有姓名、3392 个手机号映射、175 名在职主管和 20 名项目经理;姓名通过稳定 `employee_id` 定向补齐,未执行全量覆盖。当前上游 `employee_number` 与既有主体不一致且手机号大量脱敏,修复契约前禁止强制全量同步;人数属于时点事实,发布或验收前应重新查询。
+3 -1
View File
@@ -24,6 +24,8 @@
同日,本机 Android Studio `Medium_Phone` 虚拟机完成 Android 15/API 35 启动,物理分辨率 `1080×2400`、密度 `420`。在虚拟机此前未安装本应用的前提下,`0.1.6 (106)` 首次安装成功并从系统 Launcher 冷启动,`am start -W` 记录总耗时约 `6.4s`;最新包真实显示原生《服务协议和隐私政策》弹窗,四项相机、录音、图片和视频权限均为 `granted=false`,crash 缓冲区为空。截图和 UI 层级保存在忽略目录 `output/aug1-rc/android-emulator/`;验收未代替用户点击同意或拒绝。虚拟机证据用于补充干净安装和 API 35 权限基线,不能替代真实设备的媒体、WebRTC、弱网和系统浏览器交互。
同日,候选包继续升级为 `0.1.8 (108)`,大小 `33,921,705` 字节,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`。新增的项目根原生清单与 `network_security_config.xml` 已进入实际云打包输入,193 项移动端测试、类型检查、App 构建及联网法务预检通过;APK 反编译确认包内资源与当前编译产物一致、`usesCleartextTraffic=false`、`networkSecurityConfig` 引用、系统证书信任配置、签名和敏感值扫描均通过。虚拟机卸载旧实例后全新安装,Launcher 冷启动总耗时 `2.904s`,最新原生隐私弹窗真实显示,四项敏感权限均为 `granted=false`,crash 缓冲区为空。仍未代替用户点击拒绝或同意。
## 已完成的 Android 基座证据
- [x] 自定义基座包名、版本、API 35、单签名、v2 签名和 SHA-256 可重复校验。
@@ -33,7 +35,7 @@
- [x] 独立 DCloud 云证书内测 APK 的包内法务配置、API 35、单签名、v2 签名和 SHA-256 可重复校验。
- [x] 清除本 App 测试数据后首次启动先显示原生隐私弹窗,服务协议与隐私政策链接均可在系统浏览器打开。
- [x] `0.1.6 (106)` 当前候选包完成同签名覆盖安装、冷启动和启动权限复核,包内资源与当前 App-Plus 编译产物一致。
- [x] `0.1.6 (106)` 在本机 Android 15/API 35 虚拟机完成干净安装和冷启动,最新包原生隐私弹窗真实显示,四项敏感权限保持未授权且未见崩溃。
- [x] `0.1.8 (108)` 在本机 Android 15/API 35 虚拟机完成卸载重装和冷启动,最新包原生隐私弹窗真实显示,四项敏感权限保持未授权、网络安全配置已进入 APK 且未见崩溃。
## Android(最低 Android 10 + 一台当前主流机型)
+6 -5
View File
@@ -42,13 +42,14 @@ npm --prefix mobile-uni run build:app
npm --prefix mobile-uni run verify:app-release
```
`configure:app-privacy` 会同时校验 HTTPS、公网域名、两个地址互不相同,并从模板生成被 Git 忽略的 `mobile-uni/src/androidPrivacy.json`;`build:app` 再将其复制到 `dist/build/app/androidPrivacy.json`。这两个位置都纳入门禁,避免 npm/Vite CLI 项目把配置误写到工程根目录而产生假绿。`verify:app-release` 还会确认:
`configure:app-privacy` 会同时校验 HTTPS、公网域名、两个地址互不相同,并从模板生成被 Git 忽略的 `mobile-uni/src/androidPrivacy.json`;`build:app` 再将其复制到 `dist/build/app/androidPrivacy.json`,并把项目根的 `AndroidManifest.xml` 与 `nativeResources/android/res/xml/network_security_config.xml` 同步到云打包输入目录。源码和编译产物都纳入门禁,避免 npm/Vite CLI 项目把原生配置留在源码但未进入安装包而产生假绿。`verify:app-release` 还会确认:
- 首次与二次隐私弹窗使用同一组地址;
- 原生弹窗地址与本次 App 编译使用的 `VITE_APP_TERMS_URL`、`VITE_APP_PRIVACY_URL` 完全一致;
- 两个页面可通过公网 HTTPS 访问并包含可识别的协议正文;
- `VITE_DEV_SMS_HINT_ENABLED` 与 `VITE_DEV_SMS_HINT_VALUE` 均未进入发布环境;
- 编译产物不含手机号、固定测试验证码、私钥或常见云端/API 令牌格式。
- Android 清单与网络安全配置均禁止明文 HTTP,并只信任系统证书。
登录页默认不勾选协议。协议地址缺失或不合规时,页面允许查看但禁止发送验证码和登录;测试验证码快捷提示只有开发环境同时显式设置 `VITE_DEV_SMS_HINT_ENABLED=true` 和本机 `VITE_DEV_SMS_HINT_VALUE` 才显示。源码不再内置固定验证码值。
@@ -64,23 +65,23 @@ HBuilderX 5.22 当前实测中,自定义调试基座不支持安心打包,
```powershell
& '.\mobile-uni\scripts\verify-android-apk.ps1' `
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.6-dcloud-test.apk' `
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.8-dcloud-test.apk' `
-BuildKind dcloud-test
```
APK 门禁还会比较包内与当前 `dist/build/app/app-service.js` 的 SHA-256,避免用同版本号的旧包冒充当前源码,并在反编译结果中复查敏感文件、手机号、固定测试验证码、私钥和常见令牌格式。
APK 门禁还会比较包内与当前 `dist/build/app/app-service.js` 的 SHA-256,避免用同版本号的旧包冒充当前源码,并在反编译结果中复查敏感文件、手机号、固定测试验证码、私钥和常见令牌格式;`dcloud-test` 与 `release` 模式都拒绝 `usesCleartextTraffic=true`。
完成代码评审并提交冻结后,从仓库根目录执行统一 RC 证据脚本。默认拒绝脏工作区;`-AllowDirtyRehearsal -SkipBuild` 只能用于验证脚本和 APK,生成的证据会明确标记为不可发布:
```powershell
.\scripts\prepare-aug1-rc.ps1 `
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.6-dcloud-test.apk' `
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.8-dcloud-test.apk' `
-BuildKind dcloud-test
```
脚本会复跑移动端测试/类型检查/H5/App 构建、管理端构建、后端定向测试和打包,验证 APK 后在被 Git 忽略的 `output/aug1-rc/` 生成标准文件名、哈希及 JSON/Markdown 证据。它不执行生产同步、数据库迁移、服务重启或用户隐私选择。
2026-07-28 最终自定义基座大小为 `21,158,193` 字节,SHA-256 为 `82D3352FF96A86F5DB4AC0C71DCB0ED6CEC98F2A785F6EAED2FE8D4FC0C5F704`。2026-07-29 当前独立内测候选包为 `bangdao-0.1.6-dcloud-test.apk`:包名 `com.yincheng.wygj`、版本 `0.1.6 (106)`、`targetSdkVersion=35`,大小 `33,921,279` 字节,SHA-256 为 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914`,签名证书 SHA-256 为 `AB06ECC0DE544DE73C385F3F0690B22966EBC12887FB8BEE1830CDBEFDEBEF5D`。APK 反编译门禁已确认包内业务资源等于当前 App-Plus 编译产物,法务配置和敏感值扫描通过;同签名覆盖安装和冷启动成功,启动后敏感权限保持未授权且未见 `FATAL EXCEPTION`。前一候选包的干净启动已显示原生 template 隐私弹窗,两个链接均通过系统浏览器打开线上正文。该包使用 DCloud 云端证书,且 DCloud 运行壳仍声明允许明文流量,只能作为 8 月 1 日受控内测包;企业正式 release 门禁仍会拒绝明文流量和非企业签名。
2026-07-28 最终自定义基座大小为 `21,158,193` 字节,SHA-256 为 `82D3352FF96A86F5DB4AC0C71DCB0ED6CEC98F2A785F6EAED2FE8D4FC0C5F704`。2026-07-29 当前独立内测候选包为 `bangdao-0.1.8-dcloud-test.apk`:包名 `com.yincheng.wygj`、版本 `0.1.8 (108)`、`targetSdkVersion=35`,大小 `33,921,705` 字节,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`,签名证书 SHA-256 为 `AB06ECC0DE544DE73C385F3F0690B22966EBC12887FB8BEE1830CDBEFDEBEF5D`。APK 反编译门禁已确认包内业务资源等于当前 App-Plus 编译产物,法务配置、敏感值扫描、`usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均通过;本机 Android 15/API 35 虚拟机完成卸载重装、冷启动和权限基线复核,总启动耗时约 `2.9s`,四项敏感权限保持未授权且 crash 缓冲区为空。该包仍使用 DCloud 云端测试证书,只能作为 8 月 1 日受控内测包;企业正式 release 还必须使用签认后的企业证书摘要。
企业正式签名 APK 还必须用发布负责人保管的证书摘要执行 release 模式核验;该模式会额外拒绝 `debuggable=true`、明文 HTTP 和签名摘要不一致:
@@ -14,17 +14,17 @@
## 已通过
| 检查项 | 2026-07-28 结果 |
| 检查项 | 当前结果 |
|---|---|
| 仓库同步 | 本地 `main` 与 `origin/main` 均为 `9ae5f750`,前后差异 `0/0` |
| 移动端自动化 | 191 项单元测试通过;TypeScript 类型检查通过 |
| 移动端自动化 | 193 项单元测试通过;TypeScript 类型检查通过 |
| 构建 | `mobile-uni` H5、App-Plus、管理端生产构建通过;后端 `ruoyi-admin.jar` 重建通过 |
| P0 后端语义 | `AihrSopSeedServiceTest` 39 项通过;`ruoyi-aihr` 696 项全量测试通过,0 失败、0 错误 |
| App 资源门禁 | 正式法务 URL、原生隐私配置、API 35、图标和启动图检查通过 |
| 独立 APK | `com.yincheng.wygj`,`0.1.6 (106)`,`targetSdkVersion=35`,v2 签名;SHA-256 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914` |
| 独立 APK | `com.yincheng.wygj`,`0.1.8 (108)`,`targetSdkVersion=35`,v2 签名,明文 HTTP 关闭且网络安全资源已进入 APK;SHA-256 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878` |
| APK 内容门禁 | 包内业务资源等于当前 App-Plus 编译产物;法务链接、手机号、固定测试验证码、私钥和常见令牌扫描通过 |
| 真机安装启动 | vivo V2443A(Android 15)完成 `0.1.6` 覆盖安装和冷启动;相机、录音、媒体权限保持未授权,未见 `FATAL EXCEPTION` |
| 虚拟机干净安装 | 本机 `Medium_Phone` Android 15/API 35 完成 `0.1.6` 首次安装和冷启动;最新包真实显示原生隐私弹窗,四项敏感权限未授权,crash 缓冲区为空 |
| 虚拟机干净安装 | 本机 `Medium_Phone` Android 15/API 35 完成 `0.1.8` 卸载重装和冷启动;最新包真实显示原生隐私弹窗,四项敏感权限未授权,crash 缓冲区为空 |
| 首次隐私弹窗 | 最新包已在虚拟机干净启动显示原生弹窗;前一候选包的两个 HTTPS 链接已在 vivo 系统浏览器打开,仍需用户亲自执行拒绝/同意反例 |
| 法务静态页 | `https://peilian.njzhmj.top/legal/terms.html` 与 `https://peilian.njzhmj.top/legal/privacy.html` 已部署并可访问;发布前站点备份为 `/opt/wygj/backups/www-20260728224710` |
@@ -35,7 +35,7 @@
3. **原生人工反例未完成。** 覆盖安装和冷启动已通过;仍缺用户亲自执行的同意/拒绝、拒绝后退出、同意后登录、短信失败与重新登录、麦克风允许/拒绝、媒体取消、前后台切换和弱网验证。
4. **核心业务真机闭环未完成。** 仍缺认证账号下完整“查/练/问”、五个直通渠道、五个正式对练场景、ASR/TTS、附件上传下载和越权反例。
5. **内容与业务验收不足。** 当前仅收到 1 份生活顾问访谈,距离计划中的 24 份访谈、30 道人工审核政策题、5 个正式场景及五渠道真实处理人配置均无完成证据。
6. **发布与回滚未签认。** 尚无四个发布单元的版本矩阵、负责人签字、完整预检结果和回滚演练记录。当前 APK 使用 DCloud 测试证书且运行壳允许明文流量,只能用于受控内测;企业签名和公司法务正式签认仍是正式发布门槛。
6. **发布与回滚未签认。** 本地已有 APK/后端最小版本矩阵、构建证据和回滚边界,但尚无负责人签字、完整预检通过和回滚演练记录。当前 APK 已关闭明文流量但仍使用 DCloud 测试证书,只能用于受控内测;企业签名和公司法务正式签认仍是正式发布门槛。
## 转为 Go 的最短路径
@@ -1,6 +1,6 @@
# 帮道 8 月 1 日 Android 安装包测试版任务计划
> 文档版本:v1.9
> 文档版本:v2.0
>
> 制定日期:2026-07-27
>
@@ -86,16 +86,16 @@
### 3.2 RC 自动化门禁快照(2026-07-29)
本节记录当前工作区自动化和测试签名自定义基座结果,不代表企业签名 RC APK 或完整真机验收完成。
本节记录当前工作区自动化、测试签名自定义基座和独立内测 APK 结果,不代表企业签名 RC APK 或完整真机验收完成。
| 门禁 | 结果 | 说明 |
|---|---|---|
| `mobile-uni` 单元测试 | 通过 | 191/191;测试过程有本机 WebSocket 端口占用告警,但无失败 |
| `mobile-uni` 单元测试 | 通过 | 193/193;测试过程有本机 WebSocket 端口占用告警,但无失败 |
| `mobile-uni` TypeScript 检查 | 通过 | `npm --prefix mobile-uni run typecheck` |
| H5 资源构建 | 通过 | 仅证明 H5 资源可编译 |
| App-Plus 资源构建 | 通过 | 仅生成 HBuilderX 出包资源,不等同于 APK |
| App 原生配置、图标与启动图校验 | 通过 | 固定包名、语义版本、API 35、双 ABI、启动权限策略、Camera/Record 模块、22 个图标和 14 个启动图 |
| 自定义基座 APK 结构与签名校验 | 通过 | `verify:android-apk` 已核对包名、版本、`targetSdkVersion`、单签名、v2 签名和 SHA-256 |
| App 原生配置、图标与启动图校验 | 通过 | 固定包名、语义版本、API 35、双 ABI、启动权限策略、Camera/Record 模块、禁止明文 HTTP、网络安全资源、22 个图标和 14 个启动图 |
| 独立内测 APK 结构与签名校验 | 通过 | `verify-android-apk.ps1 -BuildKind dcloud-test` 已核对包名、版本、`targetSdkVersion`、网络安全配置、包内业务资源、单签名、v2 签名和 SHA-256 |
| `390×844` 未登录首屏与入口 | 通过 | 今日主操作、练、问、问制度快捷项、大喇叭和直通车登录门禁均在首屏可见;练、问已实际点击可达,无控制台错误 |
| 登录后核心业务路径 | 未执行 | 当前没有可用的本地 APP 登录主体和后端会话;不得用伪造手机号或未认证预览冒充通过 |
| 管理端生产构建 | 通过 | 存在大 chunk 告警,不阻断本轮构建 |
@@ -122,7 +122,8 @@
- 2026-07-29 当前候选包升级为 `0.1.6 (106)`,位于 `mobile-uni/dist/debug/bangdao-0.1.6-dcloud-test.apk`,大小 `33,921,279` 字节,SHA-256 为 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914`。登录页法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码不再以源码常量进入包内;APK 反编译门禁已确认包内业务资源等于当前编译产物,并通过法务链接、签名和敏感值扫描。
- `0.1.6 (106)` 已在同一设备完成保留数据的覆盖安装和 Launcher 冷启动;相机、录音和媒体权限保持未授权,未见 `FATAL EXCEPTION`。覆盖安装保留了前一版本的隐私状态,因此最新包的拒绝/同意反例仍须由用户亲自执行。
- 本机 Android Studio `Medium_Phone` 虚拟机已启动为 Android 15/API 35(`1080×2400`、密度 `420`)。在此前未安装本应用的虚拟机上,`0.1.6 (106)` 首次安装和 Launcher 冷启动成功,最新包真实显示原生隐私弹窗,四项相机、录音、图片和视频权限均未授予,crash 缓冲区为空。虚拟机只补充干净安装和 API 35 权限基线,不替代真机媒体、WebRTC、弱网与系统浏览器验收;拒绝/同意仍由用户亲自执行。
- 自定义基座和独立内测 APK 均使用 DCloud 云端测试证书,不是企业正式签名 APK;独立内测包的 DCloud 运行壳仍声明允许明文流量,因此只可用于 8 月 1 日受控内测。公司法务签认、企业签名、用户实际同意/拒绝、登录后权限反例仍是正式发布阻断。
- `0.1.8 (108)` 进一步把 Android 原生清单和 `network_security_config.xml` 纳入 App 构建链及 APK 反编译门禁。独立内测 APK 大小 `33,921,705` 字节,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`;APK 反编译确认 `usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均已进入安装包,本机虚拟机卸载旧实例后全新安装并在 `2.904s` 内冷启动,最新隐私弹窗真实显示,四项敏感权限未授予且 crash 缓冲区为空。
- 自定义基座和独立内测 APK 均使用 DCloud 云端测试证书,不是企业正式签名 APK;当前 `0.1.8` 已关闭明文流量并内置网络安全配置,但仍只可用于 8 月 1 日受控内测。公司法务签认、企业签名、用户实际同意/拒绝、登录后权限反例仍是正式发布阻断。
## 4. P0 工作分解
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.yincheng.wygj">
<application
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false"
tools:replace="android:networkSecurityConfig,android:usesCleartextTraffic" />
</manifest>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "aihr-mobile-uni",
"version": "0.1.6",
"version": "0.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aihr-mobile-uni",
"version": "0.1.6",
"version": "0.1.8",
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-5020220260725001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001",
+2 -2
View File
@@ -1,14 +1,14 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "aihr-mobile-uni",
"version": "0.1.6",
"version": "0.1.8",
"private": true,
"type": "module",
"scripts": {
"dev": "uni -p h5",
"dev:h5": "uni -p h5",
"build:h5": "uni build -p h5",
"build:app": "uni build -p app",
"build:app": "uni build -p app && node scripts/sync-app-native-resources.mjs",
"generate:app-assets": "sh scripts/generate-app-assets.sh",
"configure:app-privacy": "node scripts/configure-android-privacy.mjs",
"verify:app-assets": "node scripts/verify-app-assets.mjs",
@@ -0,0 +1,24 @@
import { cpSync, existsSync, mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
const projectRoot = resolve(import.meta.dirname, '..');
const mappings = [
[
resolve(projectRoot, 'AndroidManifest.xml'),
resolve(projectRoot, 'dist/build/app/AndroidManifest.xml')
],
[
resolve(projectRoot, 'nativeResources/android/res/xml/network_security_config.xml'),
resolve(projectRoot, 'dist/build/app/nativeResources/android/res/xml/network_security_config.xml')
]
];
for (const [source, destination] of mappings) {
if (!existsSync(source)) {
throw new Error(`Missing required native App resource: ${source}`);
}
mkdirSync(dirname(destination), { recursive: true });
cpSync(source, destination);
}
console.log('Android native manifest and network security resources synchronized.');
+29 -3
View File
@@ -205,19 +205,45 @@ try {
$application = $decodedManifest.manifest.application
$debuggable = $application.GetAttribute('debuggable', $androidNamespace) -eq 'true'
$usesCleartextTraffic = $application.GetAttribute('usesCleartextTraffic', $androidNamespace) -eq 'true'
$networkSecurityConfig = $application.GetAttribute('networkSecurityConfig', $androidNamespace)
if ($BuildKind -eq 'custom-base' -and -not $debuggable) {
throw 'Custom base APK must remain debuggable.'
}
if ($BuildKind -eq 'dcloud-test' -and $debuggable) {
throw 'DCloud internal test APK must not be debuggable.'
}
if ($BuildKind -in @('dcloud-test', 'release') -and $usesCleartextTraffic) {
throw "$BuildKind APK must not allow cleartext HTTP traffic."
}
if ($BuildKind -in @('dcloud-test', 'release')) {
if ($networkSecurityConfig -ne '@xml/network_security_config') {
throw "$BuildKind APK must reference @xml/network_security_config."
}
$decodedNetworkSecurityPath = Join-Path $decodePath 'res\xml\network_security_config.xml'
if (-not (Test-Path -LiteralPath $decodedNetworkSecurityPath)) {
throw "$BuildKind APK is missing res/xml/network_security_config.xml."
}
$decodedNetworkSecurity = Get-Content -Raw -Encoding UTF8 -LiteralPath $decodedNetworkSecurityPath
if ($decodedNetworkSecurity -notmatch 'cleartextTrafficPermitted=\"false\"') {
throw "$BuildKind APK network security config must reject cleartext traffic."
}
if ($decodedNetworkSecurity -match 'cleartextTrafficPermitted=\"true\"') {
throw "$BuildKind APK network security config must not contain a cleartext exception."
}
if ($decodedNetworkSecurity -notmatch '<certificates src=\"system\"\s*/>') {
throw "$BuildKind APK network security config must trust system certificates only."
}
if ($decodedNetworkSecurity -match '<certificates src=\"(?!system\")[^\"]+\"') {
throw "$BuildKind APK network security config must not trust user or bundled certificates."
}
if ($decodedNetworkSecurity -match '<debug-overrides\b') {
throw "$BuildKind APK network security config must not contain debug trust overrides."
}
}
if ($BuildKind -eq 'release') {
if ($debuggable) {
throw 'Release APK must not be debuggable.'
}
if ($usesCleartextTraffic) {
throw 'Release APK must not allow cleartext HTTP traffic.'
}
if ([string]::IsNullOrWhiteSpace($ExpectedSignerSha256)) {
throw 'Release verification requires -ExpectedSignerSha256 for the enterprise signing certificate.'
}
+59 -1
View File
@@ -15,6 +15,16 @@ let releaseLegalUrls;
const fail = (message) => failures.push(message);
const compiledAppRoot = resolve(projectRoot, 'dist/build/app');
const sourceAndroidManifestPath = resolve(projectRoot, 'AndroidManifest.xml');
const compiledAndroidManifestPath = resolve(compiledAppRoot, 'AndroidManifest.xml');
const sourceNetworkSecurityPath = resolve(
projectRoot,
'nativeResources/android/res/xml/network_security_config.xml'
);
const compiledNetworkSecurityPath = resolve(
compiledAppRoot,
'nativeResources/android/res/xml/network_security_config.xml'
);
const releaseTextExtensions = new Set(['.css', '.html', '.js', '.json', '.txt', '.xml']);
const releaseSensitiveFilePattern = /(^|[\\/])(?:\.env(?:\.|$)|id_rsa$)|\.(?:jks|keystore|p12|pfx|pem)$/i;
const releaseSensitiveContentPatterns = [
@@ -126,7 +136,7 @@ if (app?.ssl?.untrustedca !== 'refuse') {
fail('app-plus.ssl.untrustedca must remain refuse');
}
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.versionName || ''))) {
fail('versionName must use semantic numeric form such as 0.1.6');
fail('versionName must use semantic numeric form such as 0.1.8');
}
if (!/^[1-9]\d*$/.test(String(manifest.versionCode || ''))) {
fail('versionCode must be a positive integer');
@@ -137,6 +147,9 @@ if (android?.packagename !== 'com.yincheng.wygj') {
if (android?.targetSdkVersion !== 35) {
fail('Android targetSdkVersion must be 35 for the August 1 test package');
}
if (android?.usesCleartextTraffic !== false) {
fail('Android manifest configuration must explicitly disable cleartext traffic');
}
const expectedAndroidAbis = ['arm64-v8a', 'armeabi-v7a'];
const actualAndroidAbis = Array.isArray(android?.abiFilters)
? [...android.abiFilters].sort()
@@ -162,6 +175,46 @@ for (const permission of ['android.permission.RECORD_AUDIO', 'android.permission
}
}
const verifyAndroidNetworkSecurity = (manifestPath, networkSecurityPath, label) => {
if (!existsSync(manifestPath)) {
fail(`${label} is missing AndroidManifest.xml`);
return;
}
if (!existsSync(networkSecurityPath)) {
fail(`${label} is missing network_security_config.xml`);
return;
}
const androidManifest = readFileSync(manifestPath, 'utf8');
const networkSecurity = readFileSync(networkSecurityPath, 'utf8');
if (!/android:usesCleartextTraffic\s*=\s*["']false["']/.test(androidManifest)) {
fail(`${label} AndroidManifest.xml must disable cleartext traffic`);
}
if (!/android:networkSecurityConfig\s*=\s*["']@xml\/network_security_config["']/.test(androidManifest)) {
fail(`${label} AndroidManifest.xml must reference network_security_config`);
}
if (!/cleartextTrafficPermitted\s*=\s*["']false["']/.test(networkSecurity)) {
fail(`${label} network security config must reject cleartext traffic`);
}
if (/cleartextTrafficPermitted\s*=\s*["']true["']/.test(networkSecurity)) {
fail(`${label} network security config must not contain a cleartext exception`);
}
if (!/<certificates\s+src\s*=\s*["']system["']\s*\/>/.test(networkSecurity)) {
fail(`${label} network security config must trust system certificates only`);
}
if (/<certificates\s+src\s*=\s*["'](?!system["'])[^"']+["']/.test(networkSecurity)) {
fail(`${label} network security config must not trust user or bundled certificates`);
}
if (/<debug-overrides\b/.test(networkSecurity)) {
fail(`${label} network security config must not contain debug trust overrides`);
}
};
verifyAndroidNetworkSecurity(
sourceAndroidManifestPath,
sourceNetworkSecurityPath,
'source App'
);
const androidIcons = [
['hdpi', 72], ['xhdpi', 96], ['xxhdpi', 144], ['xxxhdpi', 192]
];
@@ -251,6 +304,11 @@ if (requirePrivacy) {
fail(`compiled App androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
}
}
verifyAndroidNetworkSecurity(
compiledAndroidManifestPath,
compiledNetworkSecurityPath,
'compiled App'
);
scanCompiledAppForSensitiveValues();
}
+3 -2
View File
@@ -3,8 +3,8 @@
"appid" : "__UNI__B36D8BE",
"description" : "帮道员工端",
"vueVersion" : "3",
"versionName" : "0.1.6",
"versionCode" : "106",
"versionName" : "0.1.8",
"versionCode" : "108",
"uniStatistics" : {
"enable" : false
},
@@ -89,6 +89,7 @@
"android" : {
"packagename" : "com.yincheng.wygj",
"targetSdkVersion" : 35,
"usesCleartextTraffic" : false,
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
"permissionExternalStorage" : {
"request" : "none"
+38
View File
@@ -42,6 +42,38 @@ test('Android 自定义基座显式面向 Android 15 权限模型', async () =>
assert.equal(android?.packagename, 'com.yincheng.wygj');
assert.equal(android?.targetSdkVersion, 35, '不得回退到 DCloud 默认的 targetSdkVersion 28 兼容模式');
assert.equal(android?.usesCleartextTraffic, false, '发布配置不得允许明文 HTTP');
});
test('Android 原生网络安全配置拒绝明文流量且只信任系统证书', async () => {
const androidManifest = await readFile(
new URL('../AndroidManifest.xml', import.meta.url),
'utf8'
);
const networkSecurity = await readFile(
new URL('../nativeResources/android/res/xml/network_security_config.xml', import.meta.url),
'utf8'
);
assert.match(androidManifest, /android:usesCleartextTraffic="false"/);
assert.match(androidManifest, /android:networkSecurityConfig="@xml\/network_security_config"/);
assert.match(networkSecurity, /cleartextTrafficPermitted="false"/);
assert.match(networkSecurity, /<certificates src="system" \/>/);
assert.doesNotMatch(networkSecurity, /cleartextTrafficPermitted="true"/);
assert.doesNotMatch(networkSecurity, /<certificates src="user"/);
assert.doesNotMatch(networkSecurity, /<debug-overrides\b/);
});
test('App 构建链将 Android 原生网络安全资源同步到云打包输入目录', async () => {
const pkg = await readJson('../package.json');
const syncScript = await readFile(
new URL('../scripts/sync-app-native-resources.mjs', import.meta.url),
'utf8'
);
assert.match(pkg.scripts?.['build:app'] || '', /sync-app-native-resources\.mjs/);
assert.match(syncScript, /dist\/build\/app\/AndroidManifest\.xml/);
assert.match(syncScript, /dist\/build\/app\/nativeResources\/android\/res\/xml\/network_security_config\.xml/);
});
test('Android 启动阶段不主动索取设备信息或外部存储权限', async () => {
@@ -146,6 +178,12 @@ test('APK 门禁区分自定义调试基座与内置业务资源的测试包', a
assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/);
assert.match(verifier, /fixed test SMS code/);
assert.match(verifier, /'mobile number'\s*=/);
assert.match(verifier, /dcloud-test.*release.*usesCleartextTraffic/s);
assert.match(verifier, /res\\xml\\network_security_config\.xml/);
assert.match(verifier, /network security config must trust system certificates only/);
assert.match(verifier, /must not contain a cleartext exception/);
assert.match(verifier, /must not trust user or bundled certificates/);
assert.match(verifier, /must not contain debug trust overrides/);
});
test('8 月 1 日 RC 脚本默认拒绝脏工作区且记录完整构建证据', async () => {