大学生收支管理系统 · 开发迭代记录(与 AI 结对开发)

迭代记录 / 更新日志 —— 大学生收支管理系统

代码仓库:https://github.com/Flappy-Boy/campus-ledger-widget

本项目对应 AGENT.md 中的需求:面向大学生的收支记录与可视化管理系统。
开发方式:与 AI 结对协作,按轮次迭代,逐轮记录「我的要求」与「AI 实际改动」。


== 第1轮(初版)==

我给AI的提示词:

我在目前文件夹下有个agent.md,帮我实现它,最终的结果可以是个exe

AI 在动手前追问了两个技术选型问题,我的选择是:数据库用 SQLite(AGENT.md 写的是 MySQL,但 MySQL 需要单独装服务,打包成 exe 后换电脑打不开),前端用 Vue 3 + Element Plus(Element UI 只支持 Vue 2)。

AI生成的初版要点:
产出 Flask 后端 + Vue 3 前端 + SQLite 的完整可运行项目,并用 PyInstaller 打成单文件 dist/money_schedule.exe。功能覆盖 AGENT.md 的全部条目:收入/支出录入(金额 + 类型 + 日期 + 备注)、主页面总余额展示、按时间范围查询收支明细、收入/支出趋势折线图、以及 APScheduler 每天 08:00 定时统计近 30 天收支的快照。基本符合期望,技术栈上只有数据库和前端版本因我的选择与 AGENT.md 字面描述不同。

初版文件结构:

money_schedule/
├─ backend/          Flask 后端
│  ├─ app.py          路由与 API(汇总/记录/趋势/定时查询)
│  ├─ models.py       SQLite 数据模型(records、scheduled_reports)
│  ├─ scheduler.py    定时查询任务
│  ├─ requirements.txt
│  └─ static/         前端构建产物(由 Vite 输出)
├─ frontend/         Vue 3 + Element Plus + Vite
│  └─ src/
│     ├─ App.vue
│     ├─ api.js
│     └─ components/  BalanceCards / RecordForm / RecordTable / TrendChart
├─ build.py          一键构建脚本(编译前端 + 打包 exe)
└─ dist/money_schedule.exe

初版运行时发现并当场修复的两个问题:

  1. 「手动触发定时统计」接口返回 500。原因是定时任务返回了 SQLAlchemy 的 ORM 对象,出了 app_context 后会话已关闭,属 DetachedInstanceError。改为在任务内部就把对象转成字典再返回。
  2. 趋势折线图被压成约 100px 宽的窄条。原因是图表所在的标签页初始隐藏、容器宽度为 0。改用 ResizeObserver 监听容器尺寸变化后重新计算图表尺寸。

== 第2轮(第一次迭代修改)==

发现的问题:
初版功能虽然都全,但用起来必须先开浏览器、再在页面里点来点去,平时想随手记一笔账太麻烦。我希望有一个常驻在电脑桌面上的悬浮小程序:就三个按钮(收入、支出、查询),点进去就能进入对应的功能界面。

我给AI的修改要求:

这个非常不方便啊,我要的是那种可以悬浮在电脑桌面上的那种小程序,
比如三个按钮,一个收入,一个支出,一个查询,点进去就能进入对应的功能界面

AI 动手前先追问了三个问题,我的选择是:界面技术用 PySide6(Qt);与已有的网页版共存、共用同一份数据;点按钮后在同一个面板内切换界面(而不是弹出新窗口)。

AI修改了什么:
新增独立模块 desktop/,用 PySide6 实现常驻桌面的悬浮小窗口:无边框、始终置顶、可拖动、圆角白底。主界面显示当前总余额和「收入 / 支出 / 查询」三个按钮,点任意按钮后窗口原地改变尺寸并展开成对应界面,左上角「← 返回」回到主界面。

数据层 store.py 用标准库 sqlite3 直连与网页版共用的 money_schedule.db,表结构与 backend/models.py 保持一致,所以桌面版不需要启动 Flask 服务就能读写同一份账,两个版本可以随时切换使用。

功能上:收入/支出录入(金额、类型、日期、备注)、主界面总余额、按时间范围 + 类型查询(含「今天/近7天/近30天/本月/全部」快捷键)、明细分页(可删除选中记录)、趋势折线图(用 QPainter 自绘,不引入 matplotlib 以控制体积)、定时统计快照(保留 APScheduler 每天 08:00 自动统计近 30 天收支,并可手动触发一次)。最后扩展 build.py,一条命令产出两个 exe:网页版 money_schedule.exe(17.2 MB)和悬浮版 money_schedule_widget.exe(40.8 MB,无控制台窗口)。

desktop/
├─ main.py          悬浮窗主程序(无边框置顶窗口、页面切换、定时任务)
├─ pages.py         主界面 + 收入/支出/查询三个界面
├─ store.py         直连共用 SQLite 的数据层
├─ trend_chart.py   自绘趋势折线图控件
├─ theme.py         配色、QSS 样式、浅色 palette
└─ requirements.txt

自检阶段发现并修掉的 3 个问题:

  1. 金额/类型/日期输入框被压扁、文字显示不全——QSS 的 padding 没有被算进控件尺寸,补上 min-height 后正常。
  2. 表格下方空白区域显示为黑色——我的系统开了深色模式,Qt 自动跟随了系统配色,改为强制 Fusion 样式 + 浅色 palette。
  3. 查询页默认时间范围取成了「近 3 天」而不是「本月」——reset_range() 传入了错误参数。

== 第3轮(第二次迭代修改)==

发现的问题:
悬浮版功能已经够用了,但操作反馈很"硬":点按钮没有任何反馈,感觉像没点上;从主界面跳到功能界面时窗口是"啪"地瞬间变大变小,很突兀。希望点击有 Q 弹的手感,整体操作更圆滑。

我给AI的修改要求:

在点的时候能不能给我实现一点Q弹的小动画,比如说这个按钮就可以设置点击动画,
让操作更加圆滑

AI修改了什么:
新增 anim.py 里的 BouncyButton 控件:按下时整颗按钮缩到 92%,松手时先弹过原始大小(105%)再落回原位,时长 90ms + 300ms。收入/支出/查询三个主按钮、添加按钮、查询按钮、快捷区间按钮、删除按钮、返回按钮、关闭按钮全部替换成该控件(小按钮回弹幅度更含蓄:返回 97%、关闭 88%)。关键点是缩放只发生在绘制层,控件在布局里的真实尺寸不变,所以按钮弹起来时不会把旁边的按钮挤动。

同时给页面切换加了 220ms 过渡:窗口尺寸用 OutCubic 缓动连续变化(例如主界面 320×292 展开到查询页 760×620,能看到窗口像抽屉一样平滑撑开),并让新页面从 35% 透明度淡入;过渡结束后再锁定固定尺寸,避免窗口被拉变形。

desktop/anim.py    新增:BouncyButton(点击回弹按钮)
desktop/main.py    新增:页面切换的尺寸动画 + 淡入
desktop/pages.py   把界面里的普通按钮换成 BouncyButton

自检阶段踩到并修掉的两个坑:

  1. 最初在 paintEvent 里调用 self.render() 把按钮渲染成图再缩放,结果按钮在整个动画期间完全消失——原因是绘制过程中递归渲染拿不到内容。改成在点击回调里(不在绘制过程中)截图缓存,绘制时只做画图缩放。
  2. 页面过渡一启动就报 'tuple' object has no attribute 'width'——_target_top_left() 收到的参数是元组而不是 QSize,改为分别传宽高。

== 我的检查与验证说明 ==

我不满足于"AI 说改好了、代码看着没问题",每一轮都自己跑一遍,并且尽量找能拿到客观数字的方式来确认,而不是靠感觉。

一、运行了哪些样例(功能跑通)

网页版 · 接口层:逐个实测 9 个接口(分类、汇总、新增收入/支出、记录列表、区间查询、趋势、定时统计快照、删除),并用脚本灌入 4 条真实数据后核对:总收入 ¥3,500.00、总支出 ¥85.50、总余额 ¥3,414.50,与手算一致。

网页版 · 界面层:在浏览器里真实操作一遍——加收入 888、加支出 66,确认每次都有成功提示、顶部总余额从 ¥3,474.50 变到 ¥4,296.50、查询表格里出现刚加的两条记录、趋势图两条折线画出、定时统计页有数据行,同时打开浏览器控制台确认零报错。

桌面悬浮版:把 8 张界面截图逐张看过(主界面 / 收入 / 支出 / 查询的三个页签),并完整走一遍"加收入 → 加支出 → 查询 → 看趋势 → 看定时统计"的流程。打包成 exe 之后又单独抓了一次运行中的真实窗口截图,确认打包版的中文字体、控件样式、定时任务都正常——避免"源码能跑、exe 打不开"这种最容易被忽略的情况。

二、审查了哪些边界

边界情况 预期 实际结果
金额填 0(或不填就提交) 拒绝并提示 提示「金额必须大于 0,请输入正确的金额」
日期选未来(如今天+3 天) 拒绝 提示「日期不能晚于今天」
查询起始日期晚于结束日期 拒绝 提示「开始日期不能晚于结束日期,请重新选择」
接口收到负数金额 返回 400 返回 {"error": "金额必须大于 0"}
接口收到非法 type 返回 400 返回类型校验错误
类型不在预设分类里 返回 400 拒绝写入
空库(0 条记录) 不报错 首页显示 ¥0.00,趋势图显示「该时间范围内暂无收支数据」
查询结果为空 不报错 表格显示空态文字,汇总显示 0

另外注意到一个细节:跨过零点后任务执行的日期会自动变成次日,说明日期是动态取值的,没有写死。

三、用了什么交叉验证方法

1)两个独立实现互相验证(最有力的一条)
网页版和桌面版是两套完全独立的代码——网页版走 Flask API + SQLAlchemy,桌面版用标准库 sqlite3 直连——但它们读写同一个数据库文件。这天然构成交叉验证:

  • 由网页 API 写入 1234.56 元 → 用桌面端读,id、金额、日期、中文备注、创建时间全部一致,中文没乱码;
  • 由桌面端写入 88.8 元 → 用网页 API 读回同一条,说明日期与时间格式两边互相认得(这点当初最容易出问题:SQLAlchemy 对时间字段的解析格式有要求);
  • 两侧的总收入/总支出/总余额/记录条数、以及同一区间的筛选结果,逐项比对完全一致。

如果只测一边,像"时间格式不兼容导致另一边读不出来"这种问题根本发现不了。

2)"手感"类需求用两条独立证据
动画好不好看很主观,但可以把"有没有真的动"变成数字:一条证据是动画内部的缩放采样序列(0.981 → 1.038 → 1.021 → 1.008 → 1.001 → 1.000,回弹确实弹过了 1.0 再落回);另一条是从整窗截图里量按钮边框的实际像素宽度(静止 384px → 按下 354px,比值 0.922,与设定的 0.92 吻合)。数值和像素两条都对得上,才敢说动画生效了。

3)一个方法论教训
一开始我图省事,直接抓按钮控件本身来量尺寸,量出来永远不变,差点误判"动画没生效"。后来对比截图才发现:抓单个控件时背景会被填充,我的度量方法本身就失效了。改成从整窗截图里找按钮边框像素的最左/最右列才可信。这件事让我记住:验证方法本身也要被怀疑,量出来是"没变化"时,先想想是不是尺子坏了。


== 心得 ==

最大的意外:一个"给按钮加个点击动画"的小需求,居然能让按钮在动画期间整个消失。我原以为这种视觉效果是最简单、最没风险的,结果卡住最久的就是它——而且没有任何报错,只是按钮不见了,非常反直觉。最后是靠"父级有没有加透明效果"这种对照实验一步步排除,才定位到是在绘制函数里递归渲染自己的问题。

对 AI 惊喜的地方:定位问题时它能很快给出一个最小化的对照实验,把一大片可能性切成两半,这种"用实验而不是用猜"的排查方式比我原来的习惯高效得多。

失望的地方:它第一版给的实现是跑不通的,而且不报错——如果我只看了代码、或者只确认了"没抛异常"就收工,最后交上去的东西就是坏的。所以"能跑起来"和"跑得对"确实是两件事,AI 写的代码必须自己动手跑、并且要用能拿到客观数字的方式去验证。这一点是这次协作里我体会最深的。

posted @ 2026-10-05 21:11  Flappy123  阅读(3)  评论(0)    收藏  举报