”海底小纵队” 现场编程作业
作业基本信息
| 项目 | 内容 |
|---|---|
| 作业所属课程 | 软件工程(福州大学 2026 春) |
| 作业要求 | 个人记账本作业要求 |
| 作业的目标 | 以团队协作方式(GitHub 分支管理、Pull Request 复审、持续集成、版本发布)完成一个功能完整的个人记账本 Web 应用,实践软件工程从需求、设计到编码、测试、集成的完整流程,并让每位成员独立负责一个模块 |
| 团队名称 | 海底小纵队 |
| 团队成员(名字-学号) | 李铭媛-102401108、陈博淇-102401411、陈宝荣-102401409、冯安娜-102401402、陈媛-102401307、陈婉凤-102401406、洪雅琳-102401202、王怡婷-102401405、李思淇-052401304、马英慧-102401604、林郑嘉同-102301228 |
组员职责分工
| 姓名 | 学号 | 角色 | 独立负责模块 | 主要文件 |
|---|---|---|---|---|
| 李铭媛 | 102401108 | 组长 / 架构 | 项目集成与数据导出 | app.py、export.py、requirements.txt |
| 陈博淇 | 102401411 | 后端 | 数据存储层 | storage.py、data/records.json |
| 陈宝荣 | 102401409 | 后端 | 记账录入 | record_add.py、录入页模板 |
| 冯安娜 | 102401402 | 后端 | 记录查看 | record_list.py、列表页模板 |
| 陈媛 | 102401307 | 后端 | 收支统计 | stats.py、统计页模板 |
| 陈婉凤 | 102401406 | 后端 | 分类管理 | category.py、分类页模板 |
| 洪雅琳 | 102401202 | 后端 | 筛选与搜索 | query.py |
| 王怡婷 | 102401405 | 前端 | 用户界面与全局样式 | templates/base.html、static/css/、static/js/ui.js |
| 李思淇 | 052401304 | 前端 | 图表可视化 | static/js/charts.js |
| 马英慧 | 102401604 | 测试 | 测试与质量保障 | tests/ |
| 林郑嘉同 | 102301228 | 文档 / 运维 | 文档、博客与 CI | README.md、docs/、.github/ |
程序运行环境
| 项目 | 环境 |
|---|---|
| 操作系统 | Windows 11(跨平台,Linux / macOS 均可) |
| 语言 / 运行时 | Python 3.12 |
| Web 框架 | Flask(Blueprint 模块化) |
| 前端 | 原生 HTML / CSS / JavaScript + Chart.js(本地 vendor) |
| 数据存储 | 本地 JSON 文件(data/records.json、data/categories.json) |
| 测试 | unittest + pytest |
| 持续集成 | GitHub Actions(ubuntu-latest + Python 3.12) |
启动方式:
pip install -r requirements.txt
python app.py
# 浏览器访问 http://127.0.0.1:5000
软件运行截图和视频





软件实现的关键代码
记账录入(record_add.py)
@record_add_bp.route('/add', methods=['GET', 'POST'])
def add():
if request.method == 'POST':
# 1. 接收表单数据(前端提交后,后端一律不信任、重新校验)
record_type = request.form.get('type') # 收支类型:income / expense
date_str = request.form.get('date') # 日期字符串
amount_str = request.form.get('amount') # 金额字符串
category = request.form.get('category') # 类别名
account = request.form.get('account') # 账户
note = request.form.get('note', '').strip()
# 2. 后端严格校验
if not all([record_type, date_str, amount_str, category, account]):
flash('提交失败:收支类型、日期、金额、类别、账户均为必填项!', 'error')
return redirect(url_for('record_add.add'))
amount = float(amount_str)
if not math.isfinite(amount) or amount <= 0:
# math.isfinite 拦截 NaN / Infinity,防止异常金额入库
flash('提交失败:金额无效,必须是大于 0 的有效数字!', 'error')
return redirect(url_for('record_add.add'))
datetime.date.fromisoformat(date_str) # 日期必须是严格的 YYYY-MM-DD,否则抛错
# 3. 构造记录并交给存储层保存
new_record = {"type": record_type, "date": date_str,
"amount": round(amount, 2), "category": category,
"account": account, "note": note}
from storage import add_record
add_record(new_record)
flash('记账成功!', 'success')
return redirect(url_for('record_add.add'))
说明(实现思路):核心是把所有输入都在后端重新校验(必填、类型、金额有限性、日期严格格式),任何一项不合法就用 flash 提示并跳回原页,全部通过后才组装记录、交给存储层落盘。这样把「校验」和「存储」分离,校验规则只在录入这一处维护,存储层不必再关心表单逻辑。
多条件组合筛选(query.py)
def _matches(record, cond):
"""判断一条记录是否满足全部筛选条件(条件之间是「且」的关系)。"""
# 关键字:切词后每个词都要命中(检索 ID / 类别 / 备注 / 账户)
if cond["keyword_tokens"]:
haystack = _search_text(record)
if not all(token in haystack for token in cond["keyword_tokens"]):
return False
# 收支类型:income / expense
if cond["type"] is not None and _record_type(record) != cond["type"]:
return False
# 日期区间(含起止当天)
if cond["start_date"] is not None or cond["end_date"] is not None:
day = _record_date(record)
if day is None:
return False
if cond["start_date"] is not None and day < cond["start_date"]:
return False
if cond["end_date"] is not None and day > cond["end_date"]:
return False
# 金额区间(含边界)
if cond["min_amount"] is not None or cond["max_amount"] is not None:
amount = _record_amount(record)
if cond["min_amount"] is not None and amount < cond["min_amount"]:
return False
if cond["max_amount"] is not None and amount > cond["max_amount"]:
return False
return True # 所有条件都通过才算命中
说明(实现思路):核心是「条件归一化 + 单一匹配函数」。先把用户各种写法(中英文、逗号分隔等)统一成标准条件对象,再用这一个匹配函数逐条判断,各条件之间是「且」,任意一条不满足就排除。这样每新增一个筛选维度,只需加一个判断块,不影响其它逻辑。
自定义统计范围(stats.py)
def filter_records(records, date_from="", date_to="", category="",
rtype="", amount_min=None, amount_max=None):
"""按条件过滤记录;任一条件为空表示不限,返回新列表、不改原数据。"""
# 统一归一化成字符串并去空白,避免 None / 空格造成误判
date_from = str(date_from or "").strip()
date_to = str(date_to or "").strip()
category = str(category or "").strip()
rtype = str(rtype or "").strip()
result = []
for rec in records:
date = str(rec.get("date", ""))
if date_from and date < date_from: # 起始日期(含当天)
continue
if date_to and date > date_to: # 截止日期(含当天)
continue
if category and rec.get("category", "") != category:
continue
if rtype and rec.get("type") != rtype: # income / expense
continue
amount = _to_decimal(rec.get("amount", 0))
if amount_min is not None and amount < _to_decimal(amount_min):
continue
if amount_max is not None and amount > _to_decimal(amount_max):
continue
result.append(rec)
return result
说明(实现思路):核心是「过滤一次、处处复用」。统计页无论切日期区间、选类别还是限定收支类型,都先经过这一个 filter_records 得到同一份筛选结果,再把它同时交给汇总(总收入 / 总支出 / 结余)、分类聚合(各类占比)和图表数据,所以改一个条件,顶部汇总、分类表和三张图会一起刷新,不会出现「表格是全部数据、图是筛选数据」的不一致。条件为空一律视为不限,调用方不必为每个维度传默认值。
软件亮点
- 类别管理:支持默认分类与自定义增删改,分类可配置颜色与图标,支出类可设月度预算;删除被记录引用的分类时自动迁移到同类型兜底分类,改名时同步更新所有引用它的记录,任何操作都不会产生「孤儿数据」。
- 自定义统计范围:统计页支持按日期区间、收支类型、类别、金额区间任意组合限定统计范围,顶部汇总、分类占比和三张图表基于同一份筛选结果联动刷新,不用导出数据手工统计。
- 智能组合筛选与搜索:列表页支持关键字 + 多条件任意组合检索,关键字匹配 ID / 类别 / 备注 / 账户,并刻意排除金额和日期,避免「搜 2 命中一片」「搜 2026 全量」这类反直觉的结果。
耗时较长、收获较大的事件
事件一:图表模块「孤立」——不报错却整页不显示
问题:李思淇负责的图表模块本身质量很高——charts.js 写了 540 行,Chart.js 已本地化到 vendor/,能自动扫描 canvas 渲染。但集成时发现 templates/partials/charts.html 和 static/js/charts.js 是「孤立」的:stats.html 既没有 include 这个 partial,也没有 <script> 引入 charts.js。三个图表容器没有任何页面引用——合并后不报错,但图表整个不显示。
解决:在 stats.html 的 endblock 前补一行 {% include 'partials/charts.html' %}。同时 stats.html 与 main 上陈媛的提交(2a59b72)改了同一个文件,合并时以 main 的 E 版本为底、只叠加 partial 引用,不做整文件覆盖。
收获:模块写完 ≠ 集成完成。「不报错」恰恰是最隐蔽的失败——功能缺失不会抛出任何异常。集成验证必须有人用浏览器真实点一遍,不能只看测试是否全绿。
事件二:前后端「各说各话」,两个页面一打开就崩
问题:王怡婷(H)写的页面模板质量很高(布局继承、公共样式、UI 规范文档都很齐全),但她假设的「页面该用哪些数据、叫什么名字」和陈媛、陈宝荣后端实际给出的对不上。最典型的一处:统计页模板去找「总收入」这个字段,后端返回的字段却是另一个名字,模板读不到就报错;录入页模板提交后要跳转到一个「编辑页」,可后端根本没实现这个页面地址。结果统计页和录入页一打开就是 500(服务器内部错误),而且这类错误在合并前光看代码很难发现。
更麻烦的是,接口双方各有各的理解,谁都不算「错」,谁也不会主动让步,僵持下去只会一直拖着没法集成。
解决:由组长李铭媛拍板「后端迁就模板」——因为模板功能更全、还预留了编辑功能。后端按模板期望的字段名和页面地址来调整,入口再补上漏注册的显示过滤器和页面路由。同时把「每个页面到底传哪些数据、叫什么名字」写成一份《页面变量契约》文档,作为唯一权威标准,并纳入集成验收范围。回归时用自动化脚本把每个页面逐个打开一遍,全部能正常显示才算通过。
收获:接口契约必须提前定、写进文档,分歧必须有人拍板。模块各自写得再好,只要双方对「数据叫什么、页面地址是什么」没有白纸黑字的约定,联调就是「猜谜」;而一旦出现分歧,靠「自觉对齐」解决不了,需要明确的决策机制 + 一份可执行的契约文档。
事件三:同一个人改同一个文件,合并时差点互相覆盖
问题:统计页模板被两个人同时改了——李思淇在自己的分支里给统计页加图表,陈媛也刚刚在主线(main)上推了「统计页模板对齐契约变量」的修改,两处改动落在同一个文件上。合并时如果直接整个覆盖,就会丢掉对方的工作。
解决:合并时以主线(main)上陈媛的版本为底,只把李思淇的那一行图表引用叠加上去,不做整文件覆盖;并逐项核对双方改动不冲突后再合并,最后由测试同学用浏览器实际看三个图表能不能渲染出来,通过才算完成。
收获:多人协作时,同一文件的冲突不能「谁后合入谁覆盖谁」。正确的做法是以主线为底、只叠加自己的改动、逐处核对,再让集成验证兜底。否则辛苦写的功能会在一次粗暴合并里被静默吞掉。
每个组员的团队编程体验
- 李铭媛(组长 / 集成):我本次最大的收获,是真正体会到了系统架构设计的重要性。前后端的衔接如果没有事先定义清楚,联调阶段就会暴露出各种问题。因此在动工之前,必须先想清楚:要实现什么、怎么实现、各模块之间如何衔接,以及如何通过模块组合最终拼成一个完整的产品。只有把这些梳理清楚,才能在最后交付一个功能完整、性能良好的产品。
- 陈博淇(存储):本次团队编程实践中,我们小组使用 Python Flask 开发了一个个人记账系统,我主要负责 JSON 数据存储模块,包括收支记录的增删改查、数据校验、异常处理和文件读写等功能。在完成基本功能后,我还对代码进行了精简,减少了重复逻辑,并通过测试检查修改前后的功能是否一致。 开发过程中,我学习了使用 Git 和 GitHub 进行分支管理、代码提交和 Pull Request 合并,也遇到了身份认证失败、本地与远程分支不同步等问题。在解决这些问题的过程中,我对 Git 的使用和团队协作流程有了更具体的认识。 团队联调时,我发现不同模块之间还存在数据读取方式不统一的情况,部分模块没有使用约定好的存储接口。这让我意识到,除了完成自己负责的功能,还需要考虑模块之间的配合,提前约定好接口和数据格式,减少后续集成时出现的问题。 通过这次实践,我积累了一些实际项目开发经验,也发现自己在代码设计、Git 使用和多人协作方面还有需要继续学习的地方。
- 陈宝荣(录入):这次项目中我负责录入模块的后端逻辑与校验。跨模块的接口对接让我体会到了团队规范与解耦的便利;而测试阶段查出的边界值漏洞(如 NaN 注入),则帮我补足了代码健壮性上的盲区。能在团队里专注打磨后端逻辑,体会大家互相“兜底”的默契,是一次非常踏实且宝贵的实战体验。
- 冯安娜(查看):在这次软件工程团队作业中,我感受到了和个人全栈编程不同的经历,每个人都在积极地推进度、找bug,最终完成的时刻成就感很强!
- 陈媛(统计):在本次团队协同开发任务里,我负责统计模块的开发与缺陷修复,收获很多。之前独立写代码时很少在意模块之间的约定,这次深刻体会到统一接口规范的重要性:直接读写本地 JSON 文件虽然写起来省事,但容易造成并发问题、数据不一致,也会让模块之间高度耦合。在修复分类读取错误、重复货币符号、绕过存储接口这些问题的过程中,我学会了遵循团队设计契约,调用统一封装好的接口获取数据,而不是自己直读文件。同时代码评审也帮我发现很多考虑不周的细节,让我明白团队开发不只是实现功能,还要保证代码可维护、和其他成员的模块兼容。
- 陈婉凤(分类):本次团队编程让我体会到模块化协作的价值。我实现了分类 CRUD、图标、预算配置,完成改名联动记录、删除自动迁移兜底分类的逻辑。经过多轮代码评审,持续补齐校验与数据安全漏洞。对接统一存储接口的过程,让我理解了模块开发的规范:不直接操作数据文件,依靠公共接口交互,保障整个项目数据一致。
- 洪雅琳(筛选):在团队集体编程中,最开始的分工以及沟通很重要,虽然我们提前定好了各接口和数据类型,但在后续编程中还是会出现接口没对齐的情况,其实中间就差一个确认,深刻教我要提高沟通能力,有时候闷头干的都是无效工。并且由于是集体编程,只有在全部人都完成时才能看到页面真实情况,这也大幅提高了我们的管理时间能力。
- 王怡婷(UI):这次项目我负责页面布局和视觉样式,主要是 base.html、stats.html、categories.html、记录列表和表单页,以及 static/css/ 下的基础、布局和组件样式。结果这次实验,我深感接口统一才是页面可用的前提,也体会到协作中约定先行,能少走不少弯路。
- 李思淇(图表):这次 11 人团队项目做的是 Flask 个人记账本,我负责前端图表模块,用 Chart.js 实现收支占比环形图、分类柱状图和月度趋势图。起初我以为任务就是“把图画出来”,真正做下来才发现,图表能不能用,更多取决于数据契约和页面集成。这些经历让我意识到,团队开发里“写完代码”只是第一步,更重要的是让别人知道怎么接、需要什么数据、放在哪里。这次最大的收获是:前端模块不是孤立的代码,而是系统链路的一环。交付时不能只说“代码 push 了”,还要说清楚数据契约、接入位置和依赖。以后我会更早对齐接口、附最小接入示例,并用真实数据联调。从“画三张图”到“接进一条链路”,是我这次团队编程最大的成长。
- 马英慧(测试):最大的收获是体验到了分工协作的完整流程:认领模块、独立开发、issue 提 bug、修完闭环、演示验收。每个人只负责一小块,合起来却是一个能跑的完整网站。收获颇多。
- 林郑嘉同(文档 / CI):我不写核心代码,却占了「上帝视角」——要把 11 个人各写的模块,整理成别人一看就懂的 README 和契约文档,才明白「讲清楚」比「写出来」更难。最触动的是,我搭的 CI 冒烟测试真在集成阶段抓到了页面端点错误,自动化的价值,亲手抓到一次 bug 才真正信服。
GitHub 仓库地址和 commit 记录
- 仓库地址:https://github.com/Limy-YY/personal-expense-tracker
- commit 记录:https://github.com/Limy-YY/personal-expense-tracker/commits/main/
测试岗成员的主要贡献体现在 Issue 中——测试岗负责在开发过程中提出问题、缺陷和优化建议,并以 Issue 的形式提交至仓库的 Issues 板块,由开发成员跟进修复后在 commit 中体现相应改动。

浙公网安备 33010602011771号