跳转至

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运行日志.txtuploads/

为什么必须 3.8 + PyInstaller 5.13.2(三道硬门槛)

  1. 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 丢失"。
  2. PyInstaller bootloader:6.x 官方仅支持 Win8+;5.13.2 是支持 Win7 的最后版本
  3. 应用依赖:新版 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 按以下顺序只读查找:

  1. win7\quiz.db(或 win10\quiz.db,平台专用种子,需手工放)
  2. dist\quiz.db(旧的已清理种子,仅借用,不改动 dist
  3. 根目录 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.dllapi-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/lib2to3Crypto.{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/*.htmlsrc="/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-walquiz.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 机器)

  1. 双击 exe,3~10 秒后自动打开后台;同目录生成了 运行日志.txt 等文件。
  2. 日志里没有"警告/错误/Traceback";用 Firefox/Chrome 打开后台能正常登录、渲染。
  3. F12 → Network 刷新各页:所有 .js 均 200(走 /j/…),无 /static/*.js 的 404(2026-09-16 回归项,见第四章)。
  4. 题库增删改、批量操作(分类/标签/可见性)、组卷、批量导入(.xlsx)、图片上传各点一遍。
  5. 学生机浏览器能打开答题页并提交。
  6. 主持台"关闭服务"后进程退出、端口释放(或双击 一键关闭服务.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. 更新地址怎么配(三选一,优先级从高到低)

  1. 老师机 exe 同目录放「更新地址.txt」:内容写 latest.json 的完整 URL 一行即可。适合不同学校指向不同服务器,或临时换源,改完重启程序生效、无需重新打包。
  2. 项目根放 update_base.txt:内容写更新目录 URL(如 https://你的域名/quiz-update,不带 /latest.json)。打包时 build.py 用它生成 latest.json 里各 exe 的下载地址前缀。
  3. build.pyUPDATE_BASE 常量 / updatecore.pyDEFAULT_MANIFEST_URL 常量:写死进代码。

⚠ 三者都还是占位符 https://REPLACE_ME/... 时,打包会打印提醒;运行时后台检查会静默失败(连不上不弹任何窗、不打扰老师),只是永远不会提示升级。发版前务必至少配好第 2 或第 3 项。

4. 发版流程(每次更新程序走一遍)

  1. 改代码 → 把项目根 version.txt 改成新版本号(如 2026.09.17.1,四段数字、逐位递增,程序按元组比较大小)。
  2. 分别打两个包(串行,共享 build/exe_assets):
    venv38\Scripts\python.exe exepack\build.py --target win7
    python exepack\build.py --target win10
    
    两次打包会把各自产物累积进同一个 dist_update/latest.json(win7 条目 + win10 条目,版本号取最后一次)。
  3. dist_update/ 整个目录上传到更新服务器覆盖同名文件。
  4. 老师机下次启动(或 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,学生无法触发下载或重启。