零第三方依赖:用 Python 标准库 + SQLite 写一个分析型记账 PWA

注意:全文使用ai生成,项目为vibe coding项目,发布于github

摘要:市面上的记账应用大多停留在「记录」,分析靠人眼。本文分享我用 Python 标准库(http.server + sqlite3)从零实现的一个分析型记账系统:以星型模型组织数据,支持多维聚合、下钻筛选、渠道余额联动与 PWA 离线使用。全文无任何第三方依赖,python script/server.py 即可运行。

一、背景:从「记录工具」到「分析工具」

我之前的记账工具定位是「记录」——记完就完了,分析靠自己翻流水。问题是:记录不是目的,看清钱花在哪才是。

所以新版本的设计目标是:让分析自动发生。围绕三件事重构:

  1. 数据怎么组织——用星型模型(事实表 + 维度表),而不是一张大宽表;
  2. 分析怎么做——多维聚合 + 下钻,而不是固定报表;
  3. 怎么让人愿意记——记账路径要短,顺手就能记。

技术选型上刻意「保守」:后端只用 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 数据建模 个人项目 零依赖
原创声明:✅ 原创

posted @ 2026-10-03 00:11  RonF02  阅读(4)  评论(0)    收藏  举报