feat: 增加 Client Windows 打包基础 (#92)

This commit is contained in:
chengma
2026-08-10 11:59:13 +08:00
parent 3bfaf44799
commit 99252aecce
14 changed files with 470 additions and 22 deletions
+15 -2
View File
@@ -111,7 +111,7 @@ client/data/
└── artifacts/ 失败截图和控件树 XML
```
跑源码时它就在 `client\data\`;将来打包成 exe 后,它在 `launcher.exe` 旁边。
跑源码时它就在 `client\data\`;打包成 exe 后,它在根目录 `Launcher.exe` 旁边。
想看里面的数据,装一个免费工具 **DB Browser for SQLite**,用它打开 `client.db` 即可。表结构和每个字段什么意思,见 [03 数据模型](03-data-model.md)。
@@ -148,7 +148,20 @@ Remove-Item Env:QT_QPA_PLATFORM
第 2 条打印出 `OK` 就算通过。完整的验证要求见 [client/AGENTS.md](../../client/AGENTS.md) §验证。
## 10. 接下来读什么
## 10. 打包 Windows 便携版
在 `client` 目录双击 `build_client.bat`,或者从 PowerShell 执行:
```powershell
cd D:\chengma\cmautobuy\client
.\build_client.bat
```
第一次会在 `.build-venv/` 安装固定版本依赖,完成后产物位于 `release/`。发布时
使用完整便携压缩包;`autobuy——manifest.json` 和更新压缩包留给版本发布与后续
更新功能使用。不要把本机已有的 `data/` 塞进发布包。
## 11. 接下来读什么
不用一次读完全部文档。按你要做的事挑:
+22 -12
View File
@@ -183,25 +183,35 @@ one-file 每次启动都要解压到临时目录,启动慢,出错几乎没
```text
CMAutoBuy/ 整个文件夹拷到任何机器都能用
├── launcher.exe 双击这个启动
├── app/ 运行时依赖,用户不要动
│ ├── python310.dll
│ ├── PyQt5/Qt5/
│ │ ├── bin/ Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll …
│ │ └── plugins/platforms/qwindows.dll
│ ├── qfluentwidgets/ qss 样式、字体、图标资源
│ ├── adbutils/binaries/ adb.exe
│ └── …
├── Launcher.exe 双击这个启动;它只负责启动主程序
├── app/ 主程序和运行时依赖,升级时整体替换
│ ├── CMAutoBuy.exe
│ ├── version.txt
│ └── dependencies/
│ ├── python310.dll
│ ├── PyQt5/Qt5/
│ │ ├── bin/ Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll …
│ │ └── plugins/platforms/qwindows.dll
│ ├── qfluentwidgets/ qss 样式、字体、图标资源
│ ├── adbutils/binaries/adb.exe
│ └── …
└── data/ 本地数据,升级时保留(见 [03 数据模型](03-data-model.md) §2.1)
├── client.db
├── logs/
└── artifacts/
```
`app/` 这个名字来自 PyInstaller 的 `--contents-directory app` 参数(默认叫 `_internal`)。
主程序使用 PyInstaller one-dir,内部依赖目录通过 `--contents-directory dependencies`
固定命名。根目录的 `Launcher.exe` 是独立轻量启动器,为后续“关闭主程序后再替换
`app/`”保留边界;当前版本不联网、不下载,也不自动更新。
**升级方式:** 删掉 `launcher.exe` 和 `app/`,换成新版本,**`data/` 原样不动**。
数据库靠 `PRAGMA user_version` 自动迁移,见 [03](03-data-model.md) §2.3。
**当前升级方式:** 关闭程序后只替换 `app/`,**`Launcher.exe` 和 `data/` 原样不动**。
数据库靠 `PRAGMA user_version` 自动迁移,见 [03](03-data-model.md) §2.3。完整便携包
用于首次安装;更新包只包含 `app/`,不得包含 `data/`。
每次构建在 `client/release/` 生成更新包、完整便携包和
`autobuy——manifest.json`。清单至少记录版本、文件名、字节大小和 SHA256;SHA256
用于发现下载损坏,不等同于发布者身份认证。联网更新和清单签名必须另建工单。
**已知风险点**(打包工单必须逐项验证):
+7 -5
View File
@@ -32,7 +32,7 @@ data/
| 什么情况 | `data/` 在哪 |
|---|---|
| 打包成 exe 后 | `launcher.exe` 旁边(见 [01 需求](01-requirements.md) §8.1 的目录结构) |
| 打包成 exe 后 | 根目录 `Launcher.exe` 旁边(见 [01 需求](01-requirements.md) §8.1 的目录结构) |
| 直接跑源码 | `client/data/`(已在 `.gitignore` 里,不会被提交) |
这叫**便携模式**:整个程序文件夹拷到哪都能用,出问题把文件夹打包发出来就能复现。
@@ -57,11 +57,13 @@ from pathlib import Path
def data_dir() -> Path:
"""可写数据目录:数据库、日志、截图都放这儿。
打包后 = launcher.exe 旁边的 data/
打包后 = 根目录 Launcher.exe 旁边的 data/
跑源码 = client/data/
"""
if getattr(sys, "frozen", False): # frozen=True 说明是打包后的 exe
base = Path(sys.executable).parent / "data"
executable_dir = Path(sys.executable).resolve().parent
install_root = executable_dir.parent if executable_dir.name.casefold() == "app" else executable_dir
base = install_root / "data"
else:
base = Path(__file__).resolve().parents[1] / "data"
base.mkdir(parents=True, exist_ok=True)
@@ -71,7 +73,7 @@ def data_dir() -> Path:
def app_dir() -> Path:
"""只读资源目录:图标、内置 qss 之类,**不要往这里写东西**。
打包后 = app/ 目录(PyInstaller 解包位置)
打包后 = app/dependencies/(PyInstaller 的只读依赖和收集资源目录)
跑源码 = client/
"""
if getattr(sys, "frozen", False):
@@ -106,7 +108,7 @@ PRAGMA busy_timeout = 5000;
使用 `PRAGMA user_version` 管理顺序迁移。数据库升级必须支持从所有已发布版本迁移,不得在启动时直接删除旧库重建。
这条在打包后尤其重要:升级 = 换掉 `launcher.exe` 和 `app/`,`data/` 原样保留,
这条在打包后尤其重要:升级 = 换掉 `app/`,`Launcher.exe` 和 `data/` 原样保留,
所以新版本必须能读旧数据库。
## 3. `pdd_tasks`
+3 -2
View File
@@ -204,8 +204,9 @@ Artifact 写入前应脱敏,数据库只保存引用。保留周期由设置
| 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 | 升级不丢数据(仅打包版本) | 换掉 `launcher.exe` 和 `app/`,`data/` 保留,启动后任务、日志、设置都还在 | 开发者 |
| 6 | Windows 干净环境启动测试通过 | **未打包版本**:找一台没装过本项目的机器,照 [00 上手指南](00-getting-started.md) 从头走一遍。**已打包版本**:把整个文件夹拷到一台**没装 Python** 的机器上双击 `Launcher.exe` | 开发者 |
| 6b | 升级不丢数据(仅打包版本) | 关闭程序并只换掉 `app/`,保留 `Launcher.exe` 和 `data/`;启动后任务、日志、设置都还在 | 开发者 |
| 6c | 发布清单与产物一致 | `autobuy——manifest.json` 中的版本、文件名、字节大小和 SHA256 与实际压缩包一致,更新包中没有 `data/` | 开发者 |
| 7 | 日志和产物无敏感信息 | 翻一遍日志和 `artifacts/`,确认没有 token、Cookie、密码、收货人信息 | 开发者 |
| 8 | 开源和商业许可证已确认 | 新增依赖的许可证是否允许本项目的使用方式 | **项目负责人**(不是开发者自己判断) |
| 9 | 真实下单版本额外满足采购安全门禁 | 逐条核对 §3 的 8 项 | **项目负责人 + 操作人员共同确认** |