## 项目定名 - 目录 wb_usage_portal → workbuddy-portal - Python 包 wb_usage → workbuddy_portal(含 session cookie 名) - 界面品牌统一为 WorkBuddy Portal;项目标识收敛到 config 单一来源 ## 容器化 - Dockerfile:多阶段构建,依赖层与源码解耦;非 root(uid 1000);内置健康检查 - docker-compose.yml:单服务 + 绑定挂载 data/logs + 日志轮转 + TZ - docker/entrypoint.sh:幂等初始化 → exec serve(LF 行尾,已由 .gitattributes 锁定) - docker/healthcheck.py:纯标准库探活 /login(slim 镜像无 curl) - .dockerignore / .env.example;数据目录可用 WB_DATA_DIR 等环境变量覆盖 ## 文档 - docs/USER-GUIDE.md 用户使用手册(含 9 张真实界面截图) - docs/DEPLOYMENT.md 部署运维(Docker / 裸机 / 反代 / 备份 / 推 Gitea 注册表) - docs/ARCHITECTURE.md 架构与设计说明(含已知坑与红线、验证体系) - docs/API.md 接口参考(路径 / 参数 / 返回结构 / 错误码) - docs/FAQ.md 常见问题;docs/CHANGELOG.md 变更日志 ## 修复缺陷(8) 1. /records/export 必然 500:生成器在请求上下文销毁后才迭代,改用自建连接 2. 大屏页图表全白:相对路径把 echarts.min.js 解析成 /vendor/... → 404 3. /users 500:路由已注册但模板缺失 4. 明细页日期筛选失效:视图传 f.frm、模板读 f.from 5. 配置页维护按钮全死:调用了不存在的 WBU.bindMaint() 6. 审计只能看最近 40 条:LIMIT 写死 7. 明细页多跑一条无用 SELECT:day_list() 取了没人用 8. 登录页锁定阈值未从配置注入 ## 安全加固 - 新增 safe_next():拒绝 //evil.com 等协议相对 URL 的开放重定向 - 缺 CSRF 的写请求统一 400 - 默认开启云端 HTTPS 证书校验(ssl_verify=1);Cookie 是账号凭证 - 登录失败计数表加上限与 TTL - /logout 拆分为 POST(执行) + GET(仅提示),防 <img src=/logout> 静默退出 - settings 内部簿记键 slot:* 读写两侧过滤,不再从 /api/settings 泄漏 ## 内部质量与工具 - 设置项写时校验 + 读时兜底,杜绝「一个手滑的数字让采集整个跑不起来」 - 全局 ValueError → 400:手写 query string 不再暴露 500 页面 - CSV 导出改 csv.writer 流式写入(原手工拼串,字段含逗号会串列) - bundle 明细加 20000 上限并回传 recordsTotal/recordsTruncated,不静默丢数据 - tools/smoke.py 离线回归 99 项;tools/check_live.py 真实 HTTP 56 项 - tools/shots.py Playwright 逐页截图 + JS 报错收集 ## 验证 - compileall 通过;smoke 99/99;对容器实例 check_live 56/56;截图 0 JS 报错 - 容器内采集实测成功(trigger=startup 补跑:新增 11 条)
10 KiB
常见问题(FAQ)
按「现象」分类。每条都给出原因和动作,不用从头排查。
一、部署
Q:docker compose up 报端口被占用
Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:8848
改 .env 里的 WB_PORT,比如 WB_PORT=18848,然后 docker compose up -d。
容器内始终监听 8848,只改宿主映射即可。
Q:容器起来了但局域网访问不了
.env的WB_BIND是不是被设成了127.0.0.1(那只允许本机);- 服务器防火墙有没有放行该端口;
docker compose ps的PORTS是不是0.0.0.0:8848->8848/tcp。
Q:容器 unhealthy 但 Up
docker compose exec portal python /app/docker/healthcheck.py
健康检查打的是 /login(唯一免登录页),拿到 200 才算健康。
若失败,看 docker compose logs 有没有 Python traceback。
Q:unable to open database file
Linux 宿主机上宿主目录属主与容器内 uid 1000 不一致:
sudo chown -R 1000:1000 ./data ./logs
docker compose restart
Q:database is locked / disk I/O error
| 报错 | 原因 |
|---|---|
database is locked |
有另一个写者(另一个容器实例?宿主机上同时在跑 manage.py collect?)。等它结束——文件锁会串行化,但 SQLite 层面的写冲突仍会短暂报错 |
disk I/O error |
挂载的文件系统不支持 SQLite 需要的锁语义(某些 NFS / 网络盘)。改用本地盘或 Docker 命名卷 |
Q:exec format error 或 no such file or directory(关于 entrypoint.sh)
docker/entrypoint.sh 被 CRLF 污染了。仓库 .gitattributes 已强制 *.sh 为 LF;
若手工传过文件:
python -c "p='docker/entrypoint.sh';d=open(p,'rb').read();open(p,'wb').write(d.replace(b'\r\n',b'\n'))"
Q:能不能跑多个副本做高可用
不要。 三个理由:SQLite 是单写者;调度线程在 Web 进程内;文件锁只在本机有效。
真要跑多副本,除第一份外全部设 WB_DISABLE_SCHEDULER=1,但写冲突依然存在。
这个服务的正确扩展方式是「升级到更强的单机」,不是横向加副本。
二、采集
Q:采集报 cookie_expired / unauthorized
Cookie 过期。重新获取(见 用户手册 3.2), 填进「配置管理 → 凭证」,保存后按区间补采。
Cookie 通常是浏览器会话级,关掉浏览器可能就失效。从已登录浏览器复制时勾选「保持登录」。
Q:采集成功但「新增 0 条」
大概率正常。看那一次的 抓取 条数:
| 抓取 | 新增 | 判断 |
|---|---|---|
| > 0 | 0 | 云端返回的都是库里已有的(断点回退窗口重叠)——正常 |
| 0 | 0 | 该时段云端确实没有记录——正常 |
| > 0 | > 0 | 正常采集到新数据 |
Q:返回 409 busy
已有采集在跑。文件锁 data/collect.lock 保证同时只有一个采集。
到「任务管理 → 运行历史」看它是否还在 running,等结束再操作。
Q:TLS / SSLError
企业代理或自签证书场景。两种处理:
- 推荐:把企业根证书装进系统信任链;
- 临时:「配置管理」把
ssl_verify设为0。
Cookie 就是账号凭证,关掉证书校验等于把它暴露在中间人面前。只在受控内网临时用。
Q:日志里出现「云端时间比本地早」告警
云端记录的 cloud_ts 比本地 ts 早超过 drift_tolerance_minutes(默认 5 分钟)。
影响不大,但可能意味着:
- 服务端时钟不准 → 校时;
- 云端写入有延迟 → 适当调大
rewind_minutes(比如 5)。
Q:想让采集更频繁 / 更稀疏
「任务管理 → 每日时刻」改成任意逗号分隔的时刻,如 08:30,12:30,18:00,22:00。
保存即生效,不用重启。时刻按本地时区解释。
三、数据与日期
Q:日期差一天 / 跨日数据落到相邻日期
时区问题。所有日期按服务端本地时区计算。
docker compose exec portal date # 期望:CST / +0800
docker compose exec portal python -c "from workbuddy_portal import db; print(db.now_str())"
若不是,检查 .env 的 TZ=Asia/Shanghai,然后 docker compose up -d 重建
(TZ 是环境变量,restart 不生效)。
Q:导出的 CSV 在 Excel 里乱码
本系统导出的文件带 UTF-8 BOM,双击不会乱码。若乱码,先确认你打开的是 从「导出 CSV」拿到的文件,而不是用记事本另存过的版本。
Q:大屏的「单笔 TOP」为什么切了日期区间也不变
设计如此。 top 是全局的:如果跟着窗口变,排名会随筛选跳动,反而看不出长期最贵的那几条。
想要窗口内的排行,用「数据明细」按积分降序 + 日期筛选。
Q:大屏的 daily(日历/趋势)为什么不受区间影响
也是设计如此:daily 是全量(约 200 B/天),日历与日期轴需要完整日期序列。
真正跟随窗口的是 dims / totals / 明细表。
Q:明细表提示「只显示最近 N 条」
大屏下发的明细有上限(recordsCap = 20000)。超过时只发最新 N 条并置
recordsTruncated=true,页面据此提示。完整数据请到「数据明细」页筛选或导出。
四、界面
Q:页面能开但图表全白
Ctrl+F5强刷清缓存;- 开浏览器控制台看有没有资源 404;
- 「日志管理 → 应用日志」看有没有异常栈。
历史上有过
/dashboard下相对路径把echarts.min.js解析成/vendor/...导致 整页全白的问题,已在tools/smoke.py里加了「页面所有src/href资源逐个断言 200」防回归。
Q:某块样式突然失效 / 文字发虚
多半是类名撞了全局样式。本项目约定:新组件用带前缀的独有类名
(如 .calcell .cbar,而不是含糊的 .bar)。
smoke.py 里有「页面 class ∩ app.css 选择器」差集断言,跑一遍就能发现异常类名。
Q:登录后跳转目标丢了
历史 bug,已修。现在 next 参数在 GET/POST 两条路径上都正确回填。
注意开放重定向防护会拒绝站外目标://evil.com、/\evil.com、https://evil.com
一律回落到 /——这是预期行为。
五、账号与权限
Q:忘记管理员密码
# 裸机
python manage.py passwd admin 新密码
# Docker
docker compose exec portal python manage.py passwd admin 新密码
不传新密码时会用默认的 admin123——别这么干。
Q:怎么给同事开只读账号
「用户管理 → 新建账号」,不要勾「管理员」。
普通用户能看所有页面、能导出、能触发采集,但看不到「用户管理」且访问 /users 返回 403。
Q:不小心把自己降级 / 删掉自己了
做不到。服务端有三条护栏:不能取消自己的管理员身份、不能删除自己、至少保留一个账号。
Q:所有人被踢下线了
SECRET_KEY 变了。它存在 data/instance.json。这个文件丢了/被删了就会重新生成,
所有会话失效(数据不受影响)。恢复办法:从备份里找回 instance.json,或让大家重新登录。
六、运维
Q:备份怎么做最稳
# 1) 先 checkpoint,把 WAL 落进主库
docker compose exec portal python -c "
from workbuddy_portal import db
db.connect().execute('PRAGMA wal_checkpoint(TRUNCATE)')"
# 2) 拷走
cp data/usage.sqlite ~/backup/usage-$(date +%F).sqlite
或者整体 tar 掉 data/(含 -wal / -shm)——别把新库配旧 WAL 用,那会损坏数据。
Q:数据库文件越来越大
docker compose exec portal python manage.py vacuum
或在「配置管理 → 维护动作 → 整理数据库」点一下。
作用是 wal_checkpoint(TRUNCATE) + VACUUM,回收删除后的空闲页并压缩 WAL。
Q:升级会不会丢数据
不会。数据在宿主机的 data/(绑定挂载),docker compose up -d --build 只重建容器。
但升级前依然要备份:schema.sql 用 CREATE TABLE IF NOT EXISTS,
加表加索引安全,改列需要手工迁移。
Q:日志在哪、怎么滚动
| 位置 | 内容 |
|---|---|
logs/app.log(挂载到宿主机) |
应用日志,滚动 2 MB × 3 |
docker compose logs |
容器 stdout(entrypoint + waitress) |
| 页面「日志管理」 | 采集逐行日志 + 应用日志尾部 + 操作审计 |
Q:想改采集的接口地址(走镜像/代理)
「配置管理」里改 api_base 与 api_path。改了之后记得同步确认 Cookie 是该域下的有效凭证。
七、开发
Q:改完代码怎么验证
五层,前两层必须跑绿:
python tools/smoke.py # 离线回归 99 项
python manage.py serve --port 8849 --no-scheduler # 另开终端
python tools/check_live.py --base http://127.0.0.1:8849 # 真实 HTTP 56 项
python tools/shots.py --base http://127.0.0.1:8849 --full # 界面截图 + JS 报错
详见 架构说明 · 验证体系。
Q:ModuleNotFoundError: No module named 'flask'
选错解释器了。依赖装在项目的 venv 或托管环境里:
python -c "import flask, sys; print(sys.executable, flask.__version__)"
报这个错说明当前 python 不是装了依赖的那个。
Q:改模板后页面没变
本项目 TEMPLATES_AUTO_RELOAD=True,模板改动通常立即生效。
若是静态资源(CSS/JS)被浏览器缓存,Ctrl+F5。
Q:写了个自定义接口结果 500,但日志只看到异常栈
先看是不是流式响应(Response(gen()) / stream_with_context):
Flask 在返回 app_iter 之后就关掉了请求上下文里的连接,
生成器里若复用 db.get_db() 会报 Cannot operate on a closed database。
正确做法是在生成器内部 db.connect() 自建连接并 finally 关闭。
详见 架构说明 · 已知坑。