2026秋软件工程 · 现场编程作业:个人记账本
2026秋软件工程 · 现场编程作业:个人记账本
| 项目 | 内容 |
|---|---|
| 作业所属课程 | 202601SoftwareEngineering班级博客 |
| 作业要求 | 2026秋软件工程现场编程作业 |
| 作业的目标 | 在 3 小时内,以团队形式完成一个个人记账本 Web 应用:用户可以记录日常收支(日期、金额、类别、备注),查看最近的账目,并按日期、类别、金额等维度进行收支统计;要求多人协作、模块化分工、可现场演示,并发布 Release 与撰写团队博客。 |
| 团队名称 | 啊对对队 |
| 团队成员名字-学号 | 李嘉星-102401128、林诗航-102401129、张炜鑫-102401127、李莹莹-102401104、谢玲洁-102401101、王清源-102401134 |
一、组员职责分工
| 成员 | 角色 | 负责模块 | 主要产出 |
|---|---|---|---|
| 李嘉星 | 组长 | app.py 路由层 + 项目统筹 |
Flask 路由骨架、接口约定、仓库管理、Release 发布、演示主讲 |
| 林诗航 | 后端 | storage.py 数据存储 |
JSON 读写、数据校验、自动备份、CSV 导出 |
| 张炜鑫 | 后端 | record.py 记账业务 |
新增/删除/修改记账、金额归一化 |
| 李莹莹 | 后端 | report.py 报表业务 |
最近N笔、多维统计、本月小结、月度环比 |
| 谢玲洁 | 前端 | index.html + main.js |
记录页:表单、记录列表、小结卡片、增删交互 |
| 王清源 | 前端 | stats.html + stats.js |
统计页:环形图/柱状图/折线图、筛选联动、测试记录表 |
各成员按模块分工、独立提交。团队主要使用功能分支和 Pull Request 集成代码,仓库初始化与部分提交直接进入 main。
二、程序运行环境
- Python 3.12(开发环境,建议使用同一版本)
- 依赖:仅
flask(pip install -r requirements.txt) - 浏览器:Chrome / Edge 均可
- 无需外网:Bootstrap 与 Chart.js 已本地化到
static/vendor/
pip install -r requirements.txt
python seed.py # 可选:生成12条演示数据
python app.py # 访问 http://127.0.0.1:5000
三、软件运行截图与演示视频

主页:顶部为本月支出/收入/结余与预算进度条,左侧记账表单,右侧最近记录列表。

记一笔"食堂 26 元"后,弹窗提示成功,最近记录、本月支出、结余同时更新。

金额输入 -5 时,浏览器原生校验直接拦截(提交后端还有第二道校验兜底)。

统计页:环形图(支出/收入对比)、每日收支柱状图、近30天趋势折线图,三图等高。

类型切换为「收入」后,环形图显示收入类别占比,折线图只显示收入,柱状图仍保留每日收入与支出的对比。

本月已花 349.20 元、超出 300 元预算时,进度条变红并提示"剁手前先冷静一下"。

导出的 CSV 用 Excel 打开,中文显示正常(utf-8-sig 编码)。

暗色模式:导航栏、卡片、按钮全部适配,偏好自动记忆。
四、软件实现的关键代码
1. 分层架构:路由不写业务、业务不碰文件
浏览器(fetch) → app.py 路由层 → record/report 业务层 → storage 存储层 → data/*.json
前后端使用统一的账目记录格式:
{"id":1, "date":"2026-11-07", "type":"expense", "amount":22.5, "category":"饮食", "note":"食堂午饭"}
2. 数据校验前置(storage.py)
新增或修改账目时,先校验日期、类型、金额、类别和备注,再写入文件:
def validate(data):
# 校验输入结构,再检查各个字段。
if not isinstance(data, dict):
return False, "数据格式不正确"
try:
datetime.datetime.strptime(data.get("date", ""), "%Y-%m-%d")
except (ValueError, TypeError):
return False, "日期格式应为 YYYY-MM-DD"
if data.get("type") not in ("expense", "income"):
return False, "类型只能是 expense 或 income"
try:
if float(data.get("amount", 0)) <= 0:
return False, "金额必须大于0"
except (ValueError, TypeError):
return False, "金额必须是数字"
if data.get("category") not in CATEGORIES:
return False, "类别不合法,必须是:" + "、".join(CATEGORIES)
note = data.get("note", "") or ""
if not isinstance(note, str) or len(note) > NOTE_MAX_LEN:
return False, "备注不能超过%d字" % NOTE_MAX_LEN
return True, ""
3. 写入前自动备份(storage.py)
def save_records(records):
# 写入前先把旧文件备份为 records.bak,防止写坏文件导致全部记录丢失
os.makedirs(DATA_DIR, exist_ok=True)
if os.path.exists(DATA_FILE):
shutil.copy(DATA_FILE, BAK_FILE)
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2) # 中文不乱码
4. 记账:校验→自增id→落盘(record.py)
def add_record(data):
ok, msg = storage.validate(data) # 1.先校验,防脏数据
if not ok:
return False, msg
records = storage.load_records() # 2.读
data["amount"] = round(float(data["amount"]), 2) # 3.金额归一化两位小数
data["note"] = (data.get("note") or "").strip()
data["id"] = storage.next_id(records) # 4.自增id
records.append(data)
storage.save_records(records) # 5.写回
return True, "记账成功"
5. 多维统计:先过滤、后聚合(report.py)
def stats(args):
# 按日期、类别、类型和金额区间过滤后,再汇总。
rows = _filter(storage.load_records(), args)
total = round(sum(r["amount"] for r in rows), 2)
group = args.get("group", "category")
groups = defaultdict(lambda: [0.0, 0])
for r in rows:
key = r["category"] if group == "category" else r["date"]
groups[key][0] += r["amount"]
groups[key][1] += 1
# 每组返回金额和笔数,按金额降序排列。
items = [{"key": k, "total": round(v[0], 2), "count": v[1]}
for k, v in sorted(groups.items(), key=lambda x: -x[1][0])]
return {"total": total, "count": len(rows), "items": items}
6. 记账成功后刷新列表和收支小结(main.js)
提交成功后,前端重新读取最近记录与本月收支小结,列表、支出和结余随之更新:
if (json.ok) {
form.reset();
form.date.value = new Date().toISOString().slice(0, 10);
loadRecent(); // 刷新最近记录列表
loadSummary(); // 刷新支出、收入和结余
}
alert(json.msg); // 后端校验失败的原因直接提示给用户
7. 图表等高(Chart.js)
原生 canvas 尺寸不同导致图表一高一矮,用"固定高度容器 + 关闭宽高比锁定"解决:
charts.pie = new Chart(document.getElementById("pie"), {
type: "doughnut",
data: { labels: data.items.map(x => x.key),
datasets: [{ data: data.items.map(x => x.total) }] },
options: { maintainAspectRatio: false } // 填满 chart-box 容器,三图等高
});
五、亮点
亮点①:三图联动可视化
统计页提供环形图(类别占比)、每日收支对比柱状图(红支出/绿收入)、近30天趋势折线图(缺失日期自动补零,曲线连续),日期和金额区间影响三张图;类型筛选改变环形图与折线图的展示范围,柱状图保留收支对照。折线图复用后端按日汇总的结果,并在前端为缺失日期补零。

类型选"全部"时,环形图自动切换为支出/收入两大块对比;选单一类型则显示该类型的类别构成。
亮点②:数据安全闭环——自动备份 + 校验前置
写入 records.json 前会把上一版复制为 records.bak,出现写入问题时可尝试从备份恢复。新增和修改账目时,也会先校验输入再写入。
亮点③:CSV 导出防乱码
导出采用 utf-8-sig 编码(带 BOM 头),Excel 双击打开中文不乱码——这能帮助 Excel 识别 UTF-8 编码,避免某些打开方式下出现中文乱码。
更多细节设计
月度预算进度条(80% 黄 / 超支红)、月度环比(含跨年边界)、全站暗色模式(偏好持久化)、前端依赖本地化(断网可演示)、备注渲染 XSS 转义、删除二次确认。
六、耗时较长、收获较大的三件事
事件一:处理 report.py 的版本冲突
经过:开发中后端出现了两份 report.py——集成时遇到了同名文件冲突。核对文件内容和提交署名后,团队关闭了原 PR,改用成员账号提交的版本。
收获:①接口和文件归属必须在开工前分配清楚,否则合并期一定撞车;②核对提交署名和 PR 记录,有助于确认每个模块的来源和贡献。
事件二:演示环境断网,CDN 依赖差点翻车
经过:界面调试时发现页面"裸奔"——Bootstrap 样式全没加载。排查发现 Bootstrap/Chart.js 走的 jsdelivr CDN 在我们网络环境下直接连接失败。这意味着现场演示如果网络不好,整个页面会当场翻车。
收获:①演示类项目必须做最坏情况假设,我们把两个库(约 438KB)下载进仓库 static/vendor/,改为本地引用,从此断网也能完整演示;②第三方 CDN 是不可控依赖,关键场合要本地化。这个问题从发现到修复 20 分钟,却可能是影响演示分数最大的一次修复。
事件三:Excel 打开 CSV 的"乱码"与 ########
经过:导出 CSV 后用 Excel 打开,先遇到中文全乱码(普通 UTF-8 无 BOM),改用 utf-8-sig 解决;随后又发现部分日期列显示 ########,排查确认并非数据损坏,而是 Excel 把 ISO 日期自动转换成了日期类型后列宽不足。期间还讨论了是否要把日期强制写成文本格式,权衡后保留标准日期格式,方便用户在 Excel 里继续排序筛选。
收获:①编码问题要理解原理(BOM 头的作用)而不是死记;②"显示异常"不等于"数据损坏",先确认数据完整性再动手改;③导出文件是给别人用的,要站在使用者的软件(Excel)角度考虑格式。
七、每个组员的团队编程体验
李嘉星(组长):这次最大的体会是"组长的工作不在写代码"。写路由骨架只花了半小时,但定接口、分模块、盯进度、处理合并冲突、打 Release 占了我大半时间。3 小时的项目里,前期 15 分钟把接口约定清楚,是全组能并行开发的前提。
林诗航:我负责存储层,一开始觉得就是"读写 JSON"很简单,真正做起来才发现要考虑文件不存在、损坏、并发写、备份等各种边界。把校验和备份都收进 storage 之后,别的同学不用关心这些细节。好的模块设计就是让用的人省心。
张炜鑫:我的记账模块依赖 storage,一开始等得有点急,后来按约定先写了 mock 数据顶着开发,storage 一合并就无缝接上。这次还替全组处理了一次合并冲突和提交署名问题,深刻体会到:规范不是限制效率,而是保护每个人的劳动成果。
李莹莹:统计模块的需求看着模糊,实际动手才知道要把"先过滤、后聚合"想清楚,还要和前端约定返回格式。中间我的文件和别人的版本撞了车,处理冲突让我把 git 的合并机制真正搞懂了一次。
谢玲洁:前端最大的收获是学会了"和后端对着接口干活":接口约定好了,我连后端没写完都可以用假数据先把页面搭起来。另外遇到页面样式全丢的问题,排查后发现是 CDN 加载失败——解决问题的过程比问题本身收获大。
王清源:三张图表让我把 Chart.js 弄明白了,尤其是"图表一高一矮"和"数据断点"这两个坑:前者靠固定高度容器解决,后者靠日期补零解决。负责测试也让我意识到:写完功能只是开始,把用例过一遍才是完成。
八、SP 参考
九、GitHub 仓库与 Commit 记录
- 仓库地址:OooneCloud/account-book
- Release 版本:v1.2
- Commit 记录:main 分支提交历史
- Pull Request 记录:全部 PR
截至 v1.2,main 的历史包含 28 个提交,其中 17 个为非合并提交、11 个为合并提交。仓库共 12 个 PR(11 个合并、1 个关闭)。六位成员的主要提交如下:
| 成员 | 主要提交 |
|---|---|
| 李嘉星(2022260505) | [core] 添加 Flask 骨架和路由 |
| 林诗航(lin634) | [storage] 实现JSON存储模块:读写/数据校验/自动备份/CSV导出 |
| 张炜鑫(OooneCloud) | [record] 实现记账模块:新增/修改/删除与字段校验;[core] 初始化;发布 v1.0/v1.1/v1.2 |
| 李莹莹(astronomy-yy) | [report] 实现查看与统计模块:最近N笔/多维统计/本月小结 |
| 谢玲洁(xxxs111) | [ui] 记账主页:布局/全站样式/接口对接 |
| 王清源(2-qing) | [ui] 统计页图表;[test] 功能测试记录表;[docs] README |

main 分支提交记录截图,展示六位成员的模块提交。

Pull Request 记录截图:12 个 PR 中,11 个已合并、1 个已关闭。

浙公网安备 33010602011771号