Win7 兼容打包说明¶
维护者用。目标:产出一台 Windows 10 打包机即可、在 Win7 / Win8 / 8.1 / 10 / 11(含 32 位老机器)上双击运行的单文件 exe。 最后更新:2026-09-16(修复打包版 JS 全 404 的回归;产物随连接复用重打)
一、结论与产物¶
项目现在按目标平台输出到两个互不干扰的目录(旧 dist/ 保留不动,其 quiz.db 全程只读借用):
| 目录 | 面向机器 | 构建环境 | exe 位数 | 大小(2026-09-16 实测) |
|---|---|---|---|---|
win7/局域网答题.exe |
Win7 / Win8 / 8.1 / 10 / 11 全系列,32/64 位通吃 | Python 3.8.10 x86 + PyInstaller 5.13.2 | 32 位 | 16,551,413 字节(约 15.8 MB) |
win10/局域网答题.exe |
64 位 Win8.1 / 10 / 11 | Python 3.12 + PyInstaller 6.x | 64 位 | 43,839,720 字节(约 41.8 MB) |
给学校机房发 win7/ 里的包即可(32 位 exe 在 64 位系统上照常运行,覆盖面最广)。
每个目录内另有 一键关闭服务.bat;exe 首次运行会自动在同目录生成 quiz.db、端口.txt、访问地址.txt、运行日志.txt、uploads/。
为什么必须 3.8 + PyInstaller 5.13.2(三道硬门槛)¶
- Python 运行时:3.8 是最后一个支持 Win7/8 的官方版本。3.9+ 要求 Win8.1+,3.12 的
python3.dll直接调用 Win10 API,在 Win7 上必报"无法定位程序输入点于 KERNEL32.dll / api-ms-win-core-path-l1-1-0.dll 丢失"。 - PyInstaller bootloader:6.x 官方仅支持 Win8+;5.13.2 是支持 Win7 的最后版本。
- 应用依赖:新版 fastapi/pydantic/uvicorn/websockets 已放弃 3.8,必须锁版本(见
requirements-py38.txt)。
二、打包环境一次性搭建(仅打包机需要,老师电脑不需要)¶
1. 安装 Python 3.8.10(32 位)¶
- 32 位安装包(推荐,通吃 32/64 位目标机): https://www.python.org/ftp/python/3.8.10/python-3.8.10.exe
- 64 位安装包(仅当确定目标机全是 64 位时选用): https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe
安装建议:可取消"Add to PATH"(不影响系统里的 3.12),记住安装路径即可,默认:
C:\Users\<用户名>\AppData\Local\Programs\Python\Python38-32\python.exe
2. 创建专用虚拟环境并安装锁定依赖¶
在项目根目录打开 PowerShell:
# 创建 3.8 虚拟环境(以后打包只认它,与系统 3.12 互不影响)
& "$env:LOCALAPPDATA\Programs\Python\Python38-32\python.exe" -m venv venv38
# 安装全部依赖(锁版清单 + PyInstaller 5.13.2 + 加密/压缩工具;国内用清华源)
.\venv38\Scripts\python.exe -m pip install -r requirements-py38.txt `
pyinstaller==5.13.2 pycryptodome rjsmin -i https://pypi.tuna.tsinghua.edu.cn/simple
版本组合(已验证,勿随意升级):
| 包 | 版本 | 上限原因 |
|---|---|---|
| fastapi | 0.110.0 | 新版拒绝 3.8 安装 |
| pydantic | 2.6.4(pydantic-core 2.16.3) | 2.11 起放弃 3.8 |
| starlette | 0.36.3 | 与 fastapi 匹配 |
| uvicorn | 0.29.0 | 0.40+ 要求 3.10 |
| websockets | 12.0 | 13+ 要求 3.9 |
| python-dotenv | 1.0.1 | 1.1 起要求 3.9 |
| PyInstaller | 5.13.2 | 6.x bootloader 不支持 Win7 |
三、日常打包命令¶
# Win7 兼容包(自动识别当前是 3.8,产物 → win7\)
.\venv38\Scripts\python.exe exepack\build.py
# Win10 现代包(用系统 3.12,产物 → win10\;build.py 会自动切换到 PyInstaller 6.x)
py -3.12 exepack\build.py
build.py按解释器版本自动选择目标与 PyInstaller:3.8 → win7/5.13.2,3.9+ → win10/6.x; 也可用--target win7|win10强制(用 3.12 强行打 win7 会被直接拒绝并提示)。- 其他参数照旧:
--console(调试)、--name 自定义名、--no-minify、--mangle。 - 打包约 1~3 分钟,结束会打印目标平台、Python 版本、位数、PyInstaller 版本。
- 打包前若旧 exe 还在运行会明确报"文件被占用",先双击对应目录的
一键关闭服务.bat再打包。
种子数据库规则(不会覆盖任何已有数据)¶
烤进 exe 的初始 quiz.db 按以下顺序只读查找:
win7\quiz.db(或win10\quiz.db,平台专用种子,需手工放)dist\quiz.db(旧的已清理种子,仅借用,不改动 dist)- 根目录
quiz.db(开发库兜底)
exe 在新电脑首次运行时,仅当同目录没有 quiz.db 才释放内置种子;已有的绝不覆盖。
2026-09-16 实操提醒:
win7\quiz.db若存在会被优先当种子,但它可能是某次开发/测试直接跑出来的库(含真实账号、场次、作答明细)。发学校机房的包必须用干净种子(如dist\quiz.db:仅 3 个演示账号、0 场次)。本次重打前已删除混入测试数据的win7\quiz.db,让 build.py 回退到dist\quiz.db。要长期固定干净种子,就把清理过的库放到win7\quiz.db/win10\quiz.db。
四、为兼容做过的代码改动(均为跨版本安全写法)¶
qbank.py:输入模型注解int | None/list[...]改为Optional[int]/List[...](PEP 604/585 在 3.8 不支持),3.8~3.12 均合法。exepack/entry.py:Starlette 0.36 的app.routes是只读 property,改为对app.router.routes原地切片替换(新版同样支持)。exepack/build.py:新增--target与 PyInstaller 版本自动切换;补全uvicorn.loops等父包 hidden-import(否则 5.13.2 下报PyiFrozenImporter cannot handle module 'uvicorn.loops');产物分win7/、win10/,构建缓存也分开。exepack/build.py(Win7 模式额外走自动生成的 spec):剔除从打包机带入的新版ucrtbase.dll与api-ms-win-*。在 Win10 机器上打包时 PyInstaller 会收进新版 UCRT,它依赖api-ms-win-core-sysinfo-l1-2-0等 Win8+ API set,Win7 加载即弹"丢失 dll"(点确定后回退系统 UCRT 又能跑,表现为"每次启动弹一次窗")。剔除后统一用目标机系统 UCRT(KB2999226),构建后已验证包内无坏依赖。
2026-09-15 性能优化与瘦身改动(详见第五章)¶
dbcore.py:SQLite 每个连接启用 WAL +synchronous=NORMAL+temp_store=MEMORY+ 4MB 页缓存 + 64MB mmap + 30s busy_timeout,全部包在 try/except 中,老环境/只读介质自动回退默认值;新增checkpoint()。dbcore.py(2026-09-16 连接复用):SQLite 连接改为线程内复用(每线程首次使用时建连并缓存,退出事务只提交/回滚不关闭),消除每查询 ~0.9ms 的建连+PRAGMA 固定开销;MySQL 仍走连接池。exe 内 uvicorn 同为多线程模型,行为与源码运行一致。exepack/svc.py:「关闭服务」退出进程前调用dbcore.checkpoint()(TRUNCATE),保证老师随后拷贝/合并quiz.db不丢 WAL 尾部数据。exepack/build.py:① win7/win10 统一走自动生成的 spec(命令行--exclude-module挡不住 hook 强收的二进制);② spec 中按路径过滤Crypto/PublicKey/*.pyd(椭圆曲线,约 0.7MB,AES-GCM 链路不依赖),EXCLUDES 增补unittest/pydoc_data/lib2to3与Crypto.{Signature,IO,Protocol};③ 显式禁用 UPX(老 CPU 启动解压更慢、内存峰值更高、杀软对壳敏感)。
2026-09-16 修复:打包版所有页面 JS 404(严重回归)¶
现象:exe 起服务正常、页面 HTML 能打开,但所有自有 JS 请求 /static/*.js 返回 404,页面白屏无数据(开发环境源码运行不受影响,故极易漏测)。
根因:2026-09-15 静态资源优化把模板脚本引用统一改成了 Jinja 写法 src="{{ sv('/static/xx.js') }}"(加版本号),而打包预处理 exepack/prepare.py 的改写正则 SRC_RE 只认字面 src="/static/xx.js"。于是自有 JS 照常加密移出 static 目录,模板里的引用却没被改写成 /j/——两头一错开就全 404。
修复(exepack/prepare.py):SRC_RE 同时匹配三种写法(字面路径、?v= 查询串、{{ sv('…') }} 表达式);改写输出吃掉整个 <script …> 开标签、只补开标签,消除旧版偶发的 </script></script> 双闭合瑕疵。{{ sv() }} 用于 CSS 的引用不受影响(CSS 不加密,运行时正常渲染)。
打包后必验(本次已做,列在第七章清单):
1. 扫描 build/exe_assets/templates/*.html:src="/static/*.js" 残留必须为 0(vendor 除外);
2. 起 exe 后按 js/manifest.json 逐个请求 /j/<name>,19/19 应 200 且 content-type 为 javascript。
五、低配机性能优化(2026-09-15,面向机房老电脑)¶
1. SQLite 引擎调优(收益最大的一项)¶
改动前连接零调优,全是 SQLite 默认值:synchronous=FULL(每次提交都 fsync 刷盘)+ journal_mode=delete(回滚日志,读与写互锁)。在机械盘、多人同时交卷的机房场景下,表现为提交卡顿、偶发 "database is locked"。
dbcore._raw_connect() 现在对每个 SQLite 连接设置:
| PRAGMA | 取值 | 作用 |
|---|---|---|
journal_mode |
WAL | 读写不互锁:学生交卷与老师后台查成绩可并发;崩溃安全;设置一次即持久化在库文件头 |
synchronous |
NORMAL | WAL 下仅检查点才 fsync,不再每条提交强制刷盘,机械盘写入明显变快 |
temp_store |
MEMORY | ORDER BY/GROUP BY 临时结果放内存,低配机减少磁盘抖动 |
cache_size |
4 MB(默认 2MB) | 减少磁盘读;按需分配页,短连接不会凭空吃满 4MB |
mmap_size |
64 MB | 按需映射,32 位进程地址空间也安全;老系统不支持时自动忽略 |
busy_timeout |
30000 ms | 与 connect(timeout=30) 双保险,遇锁等待而非立刻报错 |
工程兜底:
- 每条 PRAGMA 单独 try/except,任何一条不被支持(精简系统、只读介质、网络盘)都静默回退,不影响启动。
- 「关闭服务」按钮退出前执行 PRAGMA wal_checkpoint(TRUNCATE) 合并 WAL,老师直接拷贝/用 db_merge_gui.py 合并 quiz.db 时数据完整。
- 程序的数据库连接是短连接,每次关闭时 SQLite 也会自动检查点,正常退出后通常没有残留 -wal 文件。
- 注意:不要在服务运行中只复制 quiz.db 一个文件;如需热拷贝,把同目录 quiz.db-wal、quiz.db-shm 一起拷走,或先点「关闭服务」。
2. 打包瘦身与启动提速¶
- 剔除 pycryptodome 的非对称加密二进制
Crypto/PublicKey/*.pyd(_ec_ws.pyd单个 717KB)。前端 JS 加密只用Crypto.Cipher.AES(AES-256-GCM,见exepack/jsenc.py),已用「启动后请求加密 JS 接口」实测解密下发正常。 - 排除
unittest / pydoc_data / lib2to3 / Crypto.Signature / Crypto.IO / Crypto.Protocol等运行时不用的模块。 - 禁用 UPX 壳:onefile 本来要把 35MB 解包到临时目录,UPX 压缩的二进制在老 CPU 上启动时还要在内存里再解压一次(启动慢、内存峰值高),且机房 360 等杀软对 UPX 壳的实时扫描/误报更积极。用约 0.2~1MB 的体积换启动速度与兼容性。
- 包内
static/uploads的题目配图(约 14MB)是种子库内置题真实引用的资源,不可删除(已逐一核对引用关系,缺失即裂图)。
3. 硬件要求(优化后的实际建议)¶
| 配置 | 说明 |
|---|---|
| 系统 | Win7 SP1(32/64 位)~ Win11;Win7 需 KB2999226 或 vc_redist.x86 |
| CPU | 任意双核 1.6GHz 以上即可;首次启动主要耗时在 onefile 解包 |
| 内存 | 空闲 300~400MB 足够(32 位进程用户态上限 2GB,实测占用远低于此) |
| 磁盘 | 程序目录可写、剩余 100MB 以上(exe + 解包临时目录 + 数据库/图片) |
| 网络 | 局域网百兆够用;首次运行放行防火墙「专用网络」 |
低配机进一步提速的运维建议:把 exe 放到本地硬盘而非 U 盘/网络共享运行(onefile 每次启动要解包);机器装了机械盘又装了 360 的,可把 exe 所在目录加入杀软信任区。
六、目标机(Win7 电脑)部署要求¶
1. 系统补丁(必查)¶
- 必须 Windows 7 SP1(计算机→右键属性可见)。
- 运行库推荐最省事的做法:直接安装 Visual C++ 2015–2022 可再发行组件包 (x86)(内含 UCRT 全套,Win7 SP1 支持,最新 14.x 仍可装): https://aka.ms/vs/17/release/vc_redist.x86.exe
- 手工补丁路线(等价):KB2999226(Universal C Runtime),Python 3.8 运行依赖: https://support.microsoft.com/help/2999226
- 安装较新的 MSU 或 vc_redist 前通常还要先装:KB4474419(SHA-2 支持)、KB4490628(服务堆栈更新); 个别精简版系统另需 KB2533623。
- 补丁顺序建议:SP1 → KB4474419 → KB4490628 →(KB2999226 或直接装 vc_redist.x86),装完重启。
- 机房批量部署建议:把 vc_redist.x86.exe 随答题程序一起分发,先静默安装(
vc_redist.x86.exe /install /quiet /norestart)再放 exe,可一次性消除所有 api-ms-win-crt / UCRT 类弹窗。
2. 浏览器(最大的坑,必须处理)¶
程序双击后调用系统默认浏览器。Win7 自带 IE 无法使用后台页面(白屏/报错),必须装现代浏览器并设为默认:
- Firefox ESR 115(首选,官方保留全版本离线安装包):
https://ftp.mozilla.org/pub/firefox/releases/ → 进入最高版本号的
115.x.xesr/目录 →win32/zh-CN/(32 位系统)或win64/zh-CN/(64 位系统)下载 Setup exe。 - Chrome 109(109.0.5414.120 是支持 Win7/8 的最后版本):可在 Chrome 企业版历史版本或可信镜像下载离线 MSI。
- Edge 109(109.0.1518.140 为最后版本):微软已不公开归档,拿到离线包后统一分发。
- 机房常见的 360 极速 / QQ 浏览器(Chromium 内核较新)一般也能用;若页面异常,一律以 Firefox ESR 115 复测。
建议机房老师机和学生机提前统一分发安装(可离线),并在"默认程序"里设为默认浏览器。
3. 分发与网络¶
- 只需把
win7\局域网答题.exe(连同可选的一键关闭服务.bat)拷到老师机任意目录,无需装 Python。 - 首次运行弹出 Windows 防火墙提示:勾选"专用网络"并允许,否则学生机连不上。
- 学生机与老师机需在同一局域网网段,用
访问地址.txt里的http://老师机IP:8000访问。
七、交付前验证清单(找一台真实 Win7 机器)¶
- 双击 exe,3~10 秒后自动打开后台;同目录生成了
运行日志.txt等文件。 - 日志里没有"警告/错误/Traceback";用 Firefox/Chrome 打开后台能正常登录、渲染。
- F12 → Network 刷新各页:所有
.js均 200(走/j/…),无/static/*.js的 404(2026-09-16 回归项,见第四章)。 - 题库增删改、批量操作(分类/标签/可见性)、组卷、批量导入(.xlsx)、图片上传各点一遍。
- 学生机浏览器能打开答题页并提交。
- 主持台"关闭服务"后进程退出、端口释放(或双击
一键关闭服务.bat)。
八、故障排查¶
| 现象 | 原因 / 处理 |
|---|---|
| 双击闪退、无反应 | 打开同目录 运行日志.txt 看堆栈;无日志多半是被杀软拦截 |
| 弹窗"无法定位程序输入点…KERNEL32.dll / api-ms-win-core-path" | 用错包(拿到了 win10/旧 dist 的 3.12 exe),或系统非 SP1 / 缺 KB2999226 |
| 弹窗"丢失 api-ms-win-core-sysinfo-l1-2-0.dll"(点确定后页面还能开) | 跑的是 2026-09-14 修复前的旧包(16.5 MB / 17,347,147 字节)。先双击 一键关闭服务.bat 杀掉占着 8000 端口的旧进程(否则新 exe 只会打开浏览器、不重启服务端,让人误以为已更新),再用新版覆盖:最新为 2026-09-16 版 16,551,413 字节(文件名 win7\局域网答题.exe;2026-09-15 性能优化版 16,535,026 字节同样可用)。换新包后仍弹窗,则是系统缺 UCRT,装 vc_redist.x86.exe(见下) |
页面能开但白屏、无数据,F12 里 /static/*.js 全 404 |
跑的是 2026-09-16 修复前的包(16,535,026 字节及更早)。该版把模板脚本引用改成 {{ sv() }} 后打包漏改写,自有 JS 加密移出却没换成 /j/。用 2026-09-16 版(16,551,413 字节)覆盖即可,详见第四章 |
同目录出现 quiz.db-wal / quiz.db-shm 文件 |
WAL 模式正常产物,不是垃圾文件:-wal 里是最新事务,-shm 是共享内存索引。点「关闭服务」正常退出后会自动合并回 quiz.db;拷贝/合并数据库时请先关闭服务,或三个文件一起拷 |
| 多人同时交卷偶发卡顿或 "database is locked" | 2026-09-15 版已用 WAL + 30 秒 busy_timeout 解决;若仍出现,确认跑的是新包(见上行字节数),且 quiz.db 在本地可写磁盘而非 U 盘/网络共享 |
| 弹窗"丢失 api-ms-win-crt-runtime/ucrtbase/vcruntime140 等" | 系统缺 UCRT/VC 运行库。安装 VC++ 2015–2022 可再发行组件包 (x86):https://aka.ms/vs/17/release/vc_redist.x86.exe (装前可能需先打 KB4474419),效果等同于 KB2999226 且更省事 |
| 后台白屏、提示浏览器版本过低 | 还在用 IE,安装并设默认 Firefox ESR 115 / Chrome 109 |
| 端口 8000 被占启动不了 | 双击 一键关闭服务.bat,或记事本改同目录 端口.txt |
| 学生机打不开页面 | 防火墙未放行、不在同一网段、老师机 IP 变了(以 访问地址.txt 为准) |
| 0xc000007b 等启动错误 | 位数问题;win7 包为 32 位,一般无此问题,勿用 64 位包跑 32 位系统 |
九、自动升级更新(2026-09-16 新增)¶
老师机上的 exe 启动后会在后台检查云服务器上的版本清单,发现新版只在主持台顶部弹一条黄色提示条;老师点「立即升级」才开始下载,下载完成点「重启应用」才替换 exe 并重启。不点就永远不更新,也不会在考试中途自动重启。
1. 原理与数据安全性¶
- 清单文件
latest.json记录最新版本号与各平台下载地址、MD5、大小;程序把内置版本号(打包时烤进 exe 的version.txt)与之比较,latest > cur才提示。 - 下载落地到 exe 同目录
update/局域网答题-<版本>.exe,逐块校验 MD5,不符即删除并提示重试。 - 「重启应用」时程序生成
update_apply.bat(GBK+CRLF):等旧进程释放文件锁 →copy覆盖 → 拉起新版 → 自删。quiz.db/uploads//端口.txt/访问地址.txt都在 exe 同目录且不会被覆盖,替换只换程序本体,题库、成绩、配置全部保留。 - 源码运行模式(
python main.py)不替换自身,接口会提示「请手动更新代码」。
2. 服务器侧:上传什么¶
打包成功后项目根会生成 dist_update/ 目录,里面就是发版要上传的全部内容:
dist_update/
├── latest.json 版本清单(win7 / win10 两条目,分别打包时自动合并)
├── quiz-update-win7.exe Win7 兼容包副本(文件名用纯 ASCII,避免图床/CDN 中文名问题)
└── quiz-update-win10.exe Win10 现代包副本
当前已配置的更新地址为 https://img.sakaay.com/p/img/quiz/(build.py 的 UPDATE_BASE 与 updatecore.py 的 DEFAULT_MANIFEST_URL 已写入)。注意 img.sakaay.com 图床规则:不带 /p/ 是查看页(返回 HTML),带 /p/ 才是文件直链,程序下载必须走 /p/ 路径。若以后换服务器,在项目根放 update_base.txt(一行 URL)即可覆盖,无需改代码。
3. 更新地址怎么配(三选一,优先级从高到低)¶
- 老师机 exe 同目录放「更新地址.txt」:内容写
latest.json的完整 URL 一行即可。适合不同学校指向不同服务器,或临时换源,改完重启程序生效、无需重新打包。 - 项目根放
update_base.txt:内容写更新目录 URL(如https://你的域名/quiz-update,不带/latest.json)。打包时build.py用它生成latest.json里各 exe 的下载地址前缀。 - 改
build.py的UPDATE_BASE常量 /updatecore.py的DEFAULT_MANIFEST_URL常量:写死进代码。
⚠ 三者都还是占位符
https://REPLACE_ME/...时,打包会打印提醒;运行时后台检查会静默失败(连不上不弹任何窗、不打扰老师),只是永远不会提示升级。发版前务必至少配好第 2 或第 3 项。
4. 发版流程(每次更新程序走一遍)¶
- 改代码 → 把项目根
version.txt改成新版本号(如2026.09.17.1,四段数字、逐位递增,程序按元组比较大小)。 - 分别打两个包(串行,共享
build/exe_assets): 两次打包会把各自产物累积进同一个dist_update/latest.json(win7 条目 + win10 条目,版本号取最后一次)。 - 把
dist_update/整个目录上传到更新服务器覆盖同名文件。 - 老师机下次启动(或 24 小时后台复查)即收到提示条;也可在主持台手动刷新页面立即触发检查。
5. 提示条交互(主持台顶部,admin_update.js)¶
| 状态 | 文案 | 右侧按钮 |
|---|---|---|
| 发现新版 | 发现新版本 v…(当前 v…)+ 清单 notes | 立即升级 |
| 下载中 | 正在下载新版本 v…(大小):百分比 | 无 |
| 下载完成 | 新版本 v… 已下载完成,重启后立即生效(数据不受影响) | 重启应用 |
| 出错 | 具体原因(校验失败 / 下载失败) | 重试 |
点右上角 × 关闭后,本次运行内不再提示(sessionStorage 记录);下次启动若仍是新版会再次提示。
6. 接口与鉴权¶
/api/update/check(GET 检查)、/api/update/start(POST 开始下载)、/api/update/status(GET 进度)、/api/update/apply(POST 替换重启)。四个接口都挂在管理面守卫 ADMIN_API_PREFIX 内、且各自 _staff() 二次校验——未登录 / 学生账号一律 401,学生无法触发下载或重启。