11 KiB
架构设计
本文讲"怎么把技术栈搭起来":系统结构、职责划分、数据模型、技术难点、开发顺序。 具体用了哪些框架 / 库 / 工具链,见 技术栈;清单、软件包、许可证等协议字段见 协议合约。
一、系统结构
用户
|
v
SoftBox.exe(Gio 桌面客户端,现代版 / Win7 遗留版)
|
├─> core 业务核心(清单、下载、安装、更新、授权、存储)
| |
| ├─> HTTPS:签名 Catalog 清单 + ZIP 软件包(对象存储 / CDN)
| └─> 本地文件:SoftBoxData(apps / data / licenses / cache / downloads / staging / backups / logs)
|
├─> platform/windows(进程、版本、图标、机器指纹、托盘、目录选择)
|
└─> SoftBoxUpdater.exe(盒子自更新助手)
真实组件:
- 客户端 UI:Gio,入口
app-modern/cmd/softbox与app-win7/cmd/softbox。 - 业务核心:
core/(独立 go.mod,Go 1.20 兼容,无 UI、无 Windows 强依赖)。 - 外部服务:仅静态的签名清单与软件包下载(HTTPS);无自建在线 API、无账号服务。
- 存储:JSON 文件 + 原子替换;无数据库。
二、分层与职责划分
domain 纯模型、状态和规则
↓
application 用例、任务调度、事件总线
↓
infrastructure 网络、存储、清单、下载、安装、授权
↓
platform/windows 进程、版本、图标、机器信息、更新助手
↓
ui/gio Gio 控件、布局和交互
↓
cmd/softbox 依赖装配和程序入口
硬约束:
- domain 不依赖 Gio、SQLite 或 Windows API。
- application 只依赖接口(下载、存储、时间、进程、系统信息均注入)。
- infrastructure 不操作 UI。
- platform/windows 通过接口暴露能力,并提供非 Windows stub,保证 core 可在 CI 无头测试;仅 Win10 存在的 API 必须动态加载、失败降级,不进 EXE 导入表。
- ui/gio 只提交用例并消费事件;Layout 中不读磁盘、不访问网络、不算哈希。
- cmd 负责现代版和 Win7 版的具体装配。
UI 固定交互模式:
- 每帧先 drain 点击事件 → 2. 向 application runtime 提交任务 → 3. 保存 RequestID 与软件 ID → 4. 后台 goroutine 执行 → 5. runtime 发布 application.Event → 6. UI 的 ApplyEvent 更新 ViewModel → 7.
Window.Invalidate请求重绘。
控件状态按软件 ID保存,不按列表序号;列表用惰性 layout.List;图标走内存 + 磁盘缓存。
三、仓库目录结构
soft_quay/
├─ core/ # 共享业务核心(go.mod,Go 1.20 兼容)
│ ├─ domain/
│ ├─ application/
│ ├─ catalog/ # 清单获取、验签、缓存、过滤
│ ├─ downloader/ # 下载队列、断点续传、任务持久化
│ ├─ installer/ # ZIP 校验、安全解压、staging/backup/回滚
│ ├─ licensing/ # Ed25519 许可证验证、machine_hash
│ ├─ storage/ # JSON 原子读写、目录布局
│ └─ updater/ # 盒子自更新编排
├─ app-modern/ # 现代版(go.mod,Go 1.25 + Gio v0.10.1)
│ ├─ cmd/softbox/
│ ├─ ui/gio/
│ └─ platform/windows/
├─ app-win7/ # Win7 遗留版(go.mod,Go 1.20 + Gio v0.6.0)
│ ├─ cmd/softbox/
│ ├─ ui/gio/
│ └─ platform/windows/
├─ schemas/ # manifest / app.json / files.json / license JSON Schema
├─ scripts/ # 构建、打包、签名、校验脚本
├─ testdata/ # 测试用清单、ZIP、许可证样例(全部为假数据)
├─ docs/ # 本文档集
└─ go.work
依赖方向:app-modern → core ← app-win7,不得反向。
四、数据模型
4.1 远端只读数据(签名后消费)
| 数据 | 来源 | 说明 |
|---|---|---|
| Catalog 清单 | manifest-modern.json / manifest-win7.json(HTTPS) |
软件列表、版本、packages(url/size/sha256)、Ed25519 签名;字段见 api.md |
| ZIP 软件包 | 清单中的 packages[arch].url |
内含 app.json + files.json + payload/;协议见 api.md |
| 撤销名单 | 签名的 revocation 列表 | 网络失败时使用缓存并保留宽限期 |
关键事实:
- 软件主键是永久稳定的
id(小写英文/数字/短横线),不用名称;下架用status,不用名称前缀。 - 清单验签失败时拒绝,回退到最后一次验证成功的缓存,绝不接受未验证的新内容。
- Catalog 正式加载顺序为 HTTPS 获取 → 验签 → 严格字段/Schema/channel 校验 → 缓存替换 → 目标过滤;签名正确但结构或目标通道不匹配的远端内容不能挤掉最后可消费缓存。
- 清单解析拒绝未知字段、重复字段/软件 ID、尾随 JSON、非整数数字、非 HTTPS URL 与 architectures/packages 映射不一致;签名域为移除顶层
signature后的受限规范 JSON,细节见 api.md。 - channel 分
modern/win7,更新器必须校验 channel + min_os,禁止交叉升级。 category提供稳定单分类,tags用于搜索和多标签展示;hidden 项从远端目录隐藏,deprecated 与不兼容项保留可见原因但不提供安装包操作。
4.2 本地动态数据(JSON + 原子写入)
安装模式根目录 %LOCALAPPDATA%/OwnSoftBox/(便携模式为程序目录 .softbox/,V2):
├─ app/ # 盒子自身程序
├─ apps/<id>/ # current/ staging/ backup/ installed-app.json
├─ data/<id>/ # 子软件用户数据(更新永不覆盖)
├─ licenses/ # 许可证(更新永不覆盖)
├─ cache/ # 清单缓存、图标缓存
├─ downloads/ # 下载临时文件 + 任务元数据
├─ staging/ # 盒子自更新暂存
├─ backups/ # 盒子自更新备份
└─ logs/
软件本地状态机(domain 层枚举):
not_installed → queued → downloading → verifying → extracting → installing → installed,
另有 update_available、running、failed、rollback_pending、incompatible。
每次状态写入使用临时文件 + 原子替换;崩溃后可识别 staging、backup 和未完成事务。
T-202 将本地识别拆为两层:
core/storage:严格读写installed-app.jsonv1,读取主文件或中断遗留 backup,并只读检测安装 transaction/journal 是否存在。core/domain:纯 SemVer 2.0.0 比较与ResolveAppStatus,按“恢复事务 → 活跃操作 → 运行 → 不兼容 → 是否安装 → 是否有更新”推导单一状态。
磁盘扫描结果必须在进入 Gio Layout 前准备好;UI 不直接读取 installed-app.json。完整字段见 api.md,Schema 为 schemas/installed-app.schema.json。
4.3 事件模型
后台任务只发布事件(DownloadStarted / DownloadProgress / DownloadPaused / DownloadCompleted / DownloadFailed 等),UI 按 RequestID 和软件 ID 回填,见 api.md 事件合约。
五、关键安全流程
安装/更新一款软件的强制顺序:
读取已签名 Catalog → 选择 OS/架构匹配的 Package → 下载到 downloads
→ 校验 size 与 SHA-256 → 安全读取 app.json → 比对 ID/版本/通道/系统/架构
→ 检查 ZIP 路径与解压上限 → 解压 payload 到 staging → 校验 entry_exe
→ 确认目标软件已退出 → current 改名 backup → staging 原子切换为 current
→ 健康检查 → 成功延迟清理 backup / 失败恢复 backup
必须防止:绝对路径、../ 穿越、符号链接逃逸、写入其他软件目录、覆盖 data 与 licenses、运行中强替换 EXE、未验证包被执行、解压数量/体积/压缩比无上限、包内自动执行脚本。
Phase 1 ZIP 原型采用“两阶段解压”:先完整预检中央目录、协议顶层、路径、类型、重复项、entrypoint 与资源上限,全部通过后才创建新的 staging 并只写 payload/;任一复制/CRC 失败删除本次 staging。原型默认限制见 api.md,T-302 正式整合时复核。
Phase 1 原子切换原型把 install-transaction.json 与目录现实共同作为恢复依据。阶段写入顺序为 prepared → current_backed_up → staging_activated → committed,健康失败写 rollback_required;崩溃恢复不自动信任未健康检查的新 current,而是恢复旧 backup 或撤销首次安装。日志结构见 api.md。
该原型已覆盖进程在关键持久化步骤之间退出的恢复;真实断电时的目录项落盘顺序、杀毒软件/文件锁干扰仍需 T-302/T-601 在 Windows VM/真机做故障注入,当前结论不替代硬件级断电验证。
盒子自更新由独立 SoftBoxUpdater.exe 完成(传入 PID、暂存目录、目标目录;等待退出→备份→切换→启动新版→失败恢复)。
授权:平台层采集多个稳定硬件标识 → 清洗生成 machine_hash(不保存原始序列号/MAC)→ 服务端 Ed25519 私钥签发许可证 → 客户端内置公钥离线验签;许可证与程序文件、用户配置分开保存;子软件必须独立再次验证,不能只信盒子。
六、关键技术难点
| 难点 | 说明 | 应对 |
|---|---|---|
| 清单签名与缓存回退 | 验签、缓存、拒绝逻辑错了等于远程代码执行 | Phase 1 先做最小原型 + 恶意样例测试(testdata 伪造清单) |
| ZIP 安全解压 | 路径穿越/符号链接/zip bomb | 独立模块 + 攻击样例表驱动测试 |
| 原子切换与回滚 | Windows 文件锁、断电恢复 | staging/backup 状态机 + 崩溃恢复测试;更新前确认进程退出 |
| 双 Gio 版本 API 差异 | v0.6.0 与 v0.10.1 控件 API 不同 | 共享 ViewModel 与交互语义,Gio 适配层各自实现 |
| Win7 API 兼容 | 新 API 进导入表则 Win7 无法启动 | 动态加载 + 降级;Toolhelp32/GetFileVersionInfo 等 Win7 已有 API 优先 |
| 断点续传与任务恢复 | 重启后恢复未完成任务 | 任务元数据持久化 + HTTP Range;超时指数退避 |
| 机器指纹稳定性 | 硬件变动导致正版误判 | 多标识加权 + 容错 + 换绑申诉流程 |
高风险模块先做最小原型(Phase 1),不要等整个系统搭完才验证。
七、推荐开发顺序
- 工程骨架:core / app-modern / app-win7 + go.work + 空 Gio 窗口 + 平台 stub + CI 双目标编译。
- 最高风险原型:签名清单验签与缓存回退、ZIP 安全解压、staging/backup 原子切换。
- 核心用户流程:清单 → 列表/搜索 → 下载队列 → 安装 → 启动。
- 更新与自更新:子软件更新、SoftBoxUpdater、健康检查与回滚。
- 授权:机器指纹、许可证验证、试用/换绑/导入。
- Win7 加固与发布:API 降级、真机矩阵、双通道发布流水线。
八、架构纪律
- 业务事实和协议变化必须同步更新本文与 api.md。
- 不在代码里发明文档没有的接口、字段和状态。
- 清单缓存、用户数据、许可证、下载临时文件分目录存放,不混在一个模型里。
- 高风险模块先单独验证,再接入完整页面或流程。
- core 的 Go 1.20 兼容性由 CI 用 Go 1.20 工具链编译检查,不靠口头约定。