零第三方依赖:用 Python 标准库 + SQLite 写一个分析型记账 PWA
注意:全文使用ai生成,项目为vibe coding项目,发布于github
摘要:市面上的记账应用大多停留在「记录」,分析靠人眼。本文分享我用 Python 标准库(
http.server+sqlite3)从零实现的一个分析型记账系统:以星型模型组织数据,支持多维聚合、下钻筛选、渠道余额联动与 PWA 离线使用。全文无任何第三方依赖,python script/server.py即可运行。
一、背景:从「记录工具」到「分析工具」
我之前的记账工具定位是「记录」——记完就完了,分析靠自己翻流水。问题是:记录不是目的,看清钱花在哪才是。
所以新版本的设计目标是:让分析自动发生。围绕三件事重构:
- 数据怎么组织——用星型模型(事实表 + 维度表),而不是一张大宽表;
- 分析怎么做——多维聚合 + 下钻,而不是固定报表;
- 怎么让人愿意记——记账路径要短,顺手就能记。
技术选型上刻意「保守」:后端只用 Python 标准库,零第三方依赖。好处是部署极简(装个 Python 就能跑),也不会有依赖地狱。
二、整体架构
后端 Python 标准库:http.server + sqlite3(无任何 pip 依赖)
前端 原生 HTML / CSS / JavaScript + PWA(离线可用)
存储 accounts.db(账户库) + {uid}.db(每用户独立业务库)
部署 python script/server.py / docker compose up -d
多用户数据隔离采用「账户库 + 按用户分库」:
data/accounts.db只存账号与会话;data/{uid}.db存该用户的业务数据(星型模型)。
这样每个用户的数据物理隔离,备份、迁移、删除都只需要操作一个文件。
三、数据建模:星型模型
核心是一张事实表 + 三张维度表:
| 表 | 角色 | 关键字段 |
|---|---|---|
records |
事实表 | date / time / amount / category_id / motive_id / channel_id / note |
dim_category |
分类维度 | parent_id 自关联,level 1/2/3 |
dim_motive |
消费场景维度 | 独自 / 社交 / 为他人(固定三条) |
dim_channel |
渠道维度 | name / balance |
budgets |
预算 | UNIQUE(user_id, category_id, month) |
两个我觉得值得说的设计决策:
1)时间特征不落库,查询时实时派生。 周末/工作日、时段、月初月末这些都能从 date/time 算出来,没必要冗余存储:
def _period(hour):
if hour is None:
return None
if hour < 6: return "凌晨"
if hour < 12: return "上午"
if hour < 18: return "下午"
return "晚上"
def _month_part(day):
if day <= 10: return "月初"
if day <= 20: return "月中"
return "月末"
2)消费场景(motive)设计成三标签互斥穷尽。 每笔支出只记「当时有谁」这一客观事实,用户不需要做心理分析,降低记账负担。
四、核心实现
1. 多维筛选:同一维度支持多值复选
报表接口 /api/report/aggregate 支持 category_id / motive / channel / is_weekend / period / month_part 任意组合,且同一维度可传多个值。实现上是把单值和列表统一成 set 再匹配:
def _value_set(v):
if v is None:
return None
vals = v if isinstance(v, (list, tuple)) else [v]
vals = [x for x in vals if x not in (None, "")]
return set(vals) if vals else None
前端则用 URLSearchParams.append 把多值拼进 query,后端 _value_set 天然兼容单值/多值。
2. 记账联动渠道余额
记一笔时,同步更新对应渠道的余额——支出为负即减、收入为正即加:
conn.execute(
"INSERT INTO records (uuid,user_id,date,time,amount,category_id,motive_id,channel_id,note,created_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?)",
(rid, uid, date, t, amount, category_id, motive_id, channel_id, note, now))
if channel_id is not None:
conn.execute("UPDATE dim_channel SET balance = balance + ? WHERE id=?",
(amount, channel_id))
同时校验分类必须是叶子节点,避免脏数据:
if cat["level"] == 2:
if conn.execute("SELECT 1 FROM dim_category WHERE parent_id=?", (category_id,)).fetchone():
return None, "该二级分类下还有子分类,请选择最细一级"
elif cat["level"] != 3:
return None, "请选择叶子子分类"
3. 「记住上一次渠道」
无需额外状态表,直接查最近一条记录的渠道即可:
last = conn.execute(
"SELECT channel_id FROM records WHERE channel_id IS NOT NULL "
"ORDER BY created_at DESC, date DESC LIMIT 1").fetchone()
4. 下钻与定位
「钱去哪了」支持下钻:category_id 为空按一级场景聚合,点一级看子项,点二级看商家;钻到叶子(无子分类)时逐条列出具体消费,点击某条即可滚动定位到「记录明细」对应行并高亮。这是整个项目里最实用的功能。
五、踩坑记录
这部分是我觉得最值得分享的,都是真实踩过的坑:
1)http.server 默认 HTTP/1.0 导致连接风暴。 并发请求时连接无法复用,端口被大量占用甚至耗尽。解决:强制使用 HTTP/1.1 + keep-alive 复用连接。
2)PWA cache-first 让改动不生效。 静态资源走 cache-first 时,改了 CSS/JS 用户端还是旧的。解决:缓存版本号(spendlog-v1 → v2)强制更新,首页走 network-first。
3)Excel 导出文件损坏。 workbook.xml 缺少 <sheets> 包裹 <sheet> 标签会导致文件打不开。解决:严格按 OOXML 结构生成。
4)z-index 管理不当,错误提示被遮罩挡住。 关键反馈(如登录错误)被弹层盖住,用户完全看不到。UI 层级需要统一规划。
六、部署
最简方式(只需 Python):
python script/server.py --port 8090
或 Docker:
docker compose up -d
手机与电脑同一 WiFi 下,浏览器访问 http://<内网IP>:8090,「添加到主屏幕」即可获得类 App 体验,离线也能用。
七、小结与规划
这个项目验证了一件事:不用第三方框架,也能做出一个体验完整的分析型 Web 应用。星型模型让分析变得自然,零依赖让部署变得轻松,PWA 让它在手机上像原生 App。
后续计划:
- 更丰富的报表可视化;
- 数据导入导出的健壮性增强;
- 多设备同步方案。
分类:Python / Web开发
标签:Python SQLite 架构设计 PWA 数据建模 个人项目 零依赖
原创声明:✅ 原创
浙公网安备 33010602011771号