## 现象
容器跑着跑着页面全部 500,应用日志:
sqlite3.OperationalError: unable to open database file
(db.py:33, conn.execute("PRAGMA journal_mode=WAL"))
## 根因(已最小复现)
数据原本用绑定挂载(./data:/app/data)。Windows + Docker Desktop 的绑定挂载走 9p
(mount 里是 type 9p, aname=drvfs;path=C:\)。9p 本身支持 WAL(新建库能开 WAL),
但**宿主的 Windows 进程打开过这个 WAL 库之后**,容器侧缓存的 -shm 映射就失效,
下一次连接无法重建共享内存文件 → 打不开数据库,且**不会自愈**。
复现:
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
docker compose exec portal python -c "..." # OK, 1665 条
python manage.py stats # 宿主侧纯读一次
docker compose exec portal python -c "..." # ERR unable to open database file
# 只有 docker compose restart portal 才恢复
## 处理
- docker-compose.yml 改用命名卷 wb_data / wb_logs(容器独占 /app/data)
- 新增 docker-compose.hostdir.yml 叠加层:需要宿主目录时用
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
(注明**只建议 Linux**;Linux 的 bind mount 与容器同一文件系统,无此问题)
- 数据迁移:docker run --rm -v workbuddy-portal_wb_data:/to -v "$PWD/data":/from:ro \
alpine:3.20 sh -c 'cp -a /from/. /to/'
- 文档同步:DEPLOYMENT 2.4/5.4/第六节全部改为命名卷 + 备份恢复用 docker run;
新增第九节「Windows 绑定挂载的坑」(含复现步骤);FAQ、USER-GUIDE、README、CHANGELOG 同步
## 验证
- 宿主跑 manage.py stats 与 smoke.py 之后,容器侧仍能正常读写(此前会立刻失效)
- 容器实例 check_live 56/56;离线 smoke 99/99;hostdir 叠加层 config 校验通过
7.8 KiB
7.8 KiB
变更日志
本项目遵循 语义化版本:主版本.次版本.修订号。
- 主版本:不兼容的变更(数据库迁移需手工介入、接口契约变化)
- 次版本:向后兼容的功能新增
- 修订号:向后兼容的缺陷修复
[1.1.0] — 2026-09-14
主题:项目定名 workbuddy-portal · 容器化 · 文档体系
新增
- Docker 化:多阶段
Dockerfile(依赖层与运行层分离,改业务代码不触发重装依赖)、docker-compose.yml(单服务、数据放 Docker 命名卷、健康检查、日志轮转)、docker-compose.hostdir.yml(可选叠加层:把数据/日志放到宿主机目录,仅建议 Linux)、docker/entrypoint.sh(幂等初始化 → exec 交接)、docker/healthcheck.py(纯标准库)、.dockerignore、.env.example - 容器环境变量:
WB_HOSTWB_PORTWB_DATA_DIRWB_LOG_DIRWB_DBWB_ADMIN_USERWB_ADMIN_PASSWORDWB_DISABLE_SCHEDULERWB_IMPORT_CREDSWB_IMPORT_XLSX - 文档体系
docs/: 用户使用手册(含 9 张界面截图)、 部署与运维指南(含推镜像到 Gitea 注册表的完整流程)、 架构与设计说明、 接口参考、 常见问题 .gitattributes:强制*.sh/Dockerfile/ 各类源码为 LF (带 CRLF 的.sh在容器里会报exec format error,极难定位)
变更
- 项目定名:
wb_usage_portal→workbuddy-portal; Python 包wb_usage→workbuddy_portal;会话 cookiewb_usage_sid→workbuddy_portal_sid(升级后需要重新登录) - 界面品牌统一为 WorkBuddy Portal(此前为「WorkBuddy 用量门户」)
- 项目标识收敛到
config.PROJECT_NAME/PROJECT_TITLE/PROJECT_DESC单一来源, 模板通过app_name等上下文变量引用,不再多处硬编码 - 数据 / 日志目录支持环境变量覆盖(
WB_DATA_DIR/WB_LOG_DIR/WB_DB) - 版本号 1.0.0 → 1.1.0
- 大屏页标题改为「WorkBuddy Portal · 积分消耗大屏」
修复
| # | 症状 | 根因 |
|---|---|---|
| 1 | /records/export 必然 500 |
生成器在请求上下文销毁后才被迭代,复用 db.get_db() 撞「数据库已关闭」。改为生成器内自建连接 |
| 2 | 大屏页图表全白 | /dashboard 无尾斜杠,src="vendor/echarts.min.js" 被解析成 /vendor/… → 404 |
| 3 | /users 500 |
路由已注册但 users.html 不存在 |
| 4 | 明细页日期筛选失效 | 视图传 f.frm、模板读 f.from;导出链接拼 ?frm= 而接口只认 from |
| 5 | 配置页 3 个维护按钮全死 | 模板调 WBU.bindMaint(),app.js 里没有该函数 |
| 6 | 审计只能看最近 40 条 | LIMIT 40 写死 |
| 7 | 明细页多跑一条无用 SELECT |
day_list() 取了没人用 |
| 8 | 登录页锁定阈值写死 | 未从配置注入 |
#2 值得单独一提:前两层测试都没抓到(离线断言只看状态码 +
<html>, 真实 HTTP 只看状态码),是加上 Playwright 截图后才发现的。 据此给tools/smoke.py补了「页面所有src/href资源引用逐个断言 200」一节。
另外在容器实测中发现并修掉一个部署期才暴露的问题:
| 症状 | 根因 | 处理 |
|---|---|---|
容器跑着跑着页面全 500,日志里 sqlite3.OperationalError: unable to open database file |
数据原本用绑定挂载;Windows + Docker Desktop 走 9p,宿主的 Windows 进程只要访问过这个 WAL 库(纯读也会触发),容器侧下一次连接就重建不了 -shm,且不会自愈 |
改为 Docker 命名卷(容器独占数据目录);需要宿主目录时叠加 docker-compose.hostdir.yml(仅建议 Linux) |
最小复现:容器正常 → 宿主跑一次 manage.py stats → 容器立刻打不开库、且重启前不再恢复。
已写进 DEPLOYMENT.md 第九节。
安全
- 新增
security.safe_next():登录跳转的next拒绝//evil.com(协议相对 URL)、/\evil.com、绝对地址与含 CR/LF 的值 - 缺 CSRF 的写请求统一 400(此前部分路径漏检)
- 默认开启云端 HTTPS 证书校验(
ssl_verify=1)。Cookie 是账号凭证,不该裸奔 - 登录失败计数表加上限(4096 个 IP)与 TTL(1 小时),防内存被大量来源 IP 撑爆
/logout拆分为 POST(执行)+ GET(仅提示),防<img src="/logout">静默退出settings的内部簿记键(slot:*)读写两侧都过滤,不再从/api/settings泄漏
内部质量
- 设置项写时校验 + 读时兜底:
config.normalize_setting()(范围 + 单位,非法值 400 并列出全部错误)+ 采集路径全面改用db.get_int(),杜绝「一个手滑的数字让采集整个跑不起来」 - 全局
ValueError→ 400 处理器:手写 query string 不再能把 500 页面暴露出去 - 日期归一化
norm_day()/norm_window()(容忍2026/09/08、带时间、起止写反) - CSV 导出改用
csv.writer流式写入(此前手工拼字符串,model/client含逗号会串列) bundle明细加下限(BUNDLE_RECORDS_CAP = 20000),并返回recordsTotal/recordsCap/recordsTruncated,不静默丢数据- 轮询日志尾部改用
collections.deque(maxlen=n),不再把整个文件读进内存 - 修掉一条非法 CSS 声明
font: 13px/1.5 inherit(简写里inherit不能当字族, 整条被浏览器丢弃,输入框一直用默认字体)
工具
- 新增
tools/smoke.py:离线回归 99 项断言(全页面只读渲染 + 模板残留 + 历史缺陷防回归 ①~⑭ + 静态资源逐个 200 + CSV 列 + 页面 class ↔app.css选择器对账), 不需要先起服务 tools/check_live.py扩充到 56 项断言,新增用户管理 / 审计 / 流式导出 / 开放重定向 / CSRF 四节- 新增
tools/shots.py:Playwright 登录后逐页截图 + 收集 console / pageerror (内建可执行文件探测,规避驱动与本机浏览器版本错位)
[1.0.0] — 2026-09-14
主题:从「脚本 + CSV + 静态大屏」演化为独立可部署的门户
新增
- 独立 Flask 应用:
create_app工厂 +views/api双蓝图 - SQLite(WAL)作为唯一数据正本,主键去重、断点续采、聚合全部下推 SQL
- 进程内调度线程(20 秒轮询 + 槽位去重 + 启动补跑),不再依赖外部计划任务
- 单写者文件锁
data/collect.lock(含 30 分钟僵尸锁抢占) - 登录鉴权(
pbkdf2:sha256:200000)、全站 CSRF、同 IP 失败限速 - 后台页面:概览 / 数据明细 / 任务管理 / 配置管理 / 日志管理
- 独立 ECharts 交互大屏
/dashboard(离线自带 echarts,不依赖 CDN) - CLI:
initservecollectmigrate-csvimport-xlsximport-credsfill-promptexport-csvstatsstatuspasswdvacuum - 从旧版 CSV / 官网 xlsx / 编辑器设置导入的迁移通道
- 从 VSCode / Cursor / Trae 的
settings.json接管 Cookie 与 User-Agent
变更
- 数据正本从 CSV 改为 SQLite;CSV 降级为导出物(
data/exports/) - 采集从外部脚本改入 Web 进程;删除全部外部自动化与计划任务
- 运行期配置(Cookie、调度、采集参数)从文件搬进数据库,由后台页面维护
版本对照
| 版本 | 数据正本 | 调度 | 部署 | 鉴权 |
|---|---|---|---|---|
| 1.1.0 | SQLite(WAL) | 进程内 | Docker Compose / 裸机 | 登录 + CSRF + 角色 |
| 1.0.0 | SQLite(WAL) | 进程内 | 裸机 | 登录 + CSRF |
| < 1.0 | CSV 文件 | 外部计划任务 | 脚本 | 无(仅局域网) |