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

三、软件运行截图与演示视频

下载软件运行演示(MP4,约 1 分钟)

主页整体

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

记账成功后三处联动

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

金额非法拦截

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

统计页三图全景

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

筛选联动

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

预算超支

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

CSV导出

导出的 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 记录

截至 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

commit记录

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

PR列表

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

posted @ 2026-11-07 22:11  Jasonxdd  阅读(10)  评论(0)    收藏  举报