2026秋软件工程现场编程作业(我说的都队)

我说的都队|2026 秋软件工程现场编程:个人记账本

项目 内容
作业所属课程 202601 软件工程(福州大学)
作业要求 2026秋软件工程现场编程作业
作业目标 协作开发个人记账本,完成收支录入、本地文件持久化、记录查询和筛选统计,保留各成员实际开发与提交记录。
团队名称 我说的都队
项目仓库 personal_account_book
运行版本 v1.0.0 最终验收版
团队成员 102401334王翔鹏 102401333周玘晖 102401330高一鹏 102401331林明亮 102401233郑志銮 102401225王圣杰 102401234黄俊凡 102400412龚巍斌 102401309叶凯乐 102401227林子涵 102401212沈雷

一、项目实现概述

我们实现了一个在本机浏览器中使用的个人记账本。用户可以选择收入或支出,填写日期、金额、类别和备注;保存后,记录写入后端的 JSON 文件,最近记录列表随之更新。明细页支持组合筛选与分页,统计页展示当前条件下的收入、支出、结余和类别支出。

本次开发先落实基础模块和接口,再在整合阶段完善页面交互、错误反馈、测试和运行包。我们优先保证“录入 → 保存 → 查询 → 统计 → 重启仍可读取”的完整流程。

当前实现包括新增收支、最近10条、明细分页、日期/类型/类别/金额范围筛选、收支统计与类别条形图。应用按单机、单用户、单进程使用设计。

二、分工与协作记录

姓名 学号 负责模块 具体工作 主要协作记录
叶凯乐 102401309 后端框架、应用装配与发布 create_app、路由注册、错误响应、静态资源与启动脚本 PR #12
林明亮 102401331 本地文件存储 JSON 初始化、读取、原子追加、锁和损坏文件保护 PR #7
周玘晖 102401333 记账写入 POST /api/records、UUID 与创建时间生成、持久化成功后返回 PR #13
高一鹏 102401330 查询筛选 日期/类型/类别/金额组合筛选、稳定排序、分页和 total PR #6
王圣杰 102401225 统计计算 收入、支出、结余、条数及类别汇总,复用统一筛选 PR #2
黄俊凡 102401234 PM、类别与校验 接口协调、类别定义、金额/日期/筛选/分页校验、验收与材料整理 PR #1
郑志銮 102401233 前端框架与整合 Vue/Vite、三页导航、API 客户端、公共样式与刷新协调 PR #8
王翔鹏 102401334 记账表单 收支录入、动态类别、防重复提交、字段错误与保存反馈 PR #10
龚巍斌 102400412 记录展示 可复用表格、最近10条、加载/空态/重试及请求竞态处理 PR #5
沈雷 102401212 筛选与明细、QA 筛选面板、分页、总条数、错误反馈和最新请求保护 PR #11
林子涵 102401227 统计展示、演示协作 统计卡片、筛选集成、类别条形图与空态 PR #9

表中的链接对应各成员负责模块的 PR,可进一步查看代码文件和提交记录。模块提交与合入主分支分开进行,整合时检查公开接口、数据单位和依赖是否就绪。

提交署名说明: 郑志銮的 PR #8 由账号 Zzl299 发起,但原始提交误用了仓库所有者的 noreply 邮箱,导致 GitHub 自动将该 commit 关联到 cht-hyp。原始提交和 PR 作者记录均保留,后续通过 PR #16 补充了说明并使用本人关联邮箱提交。成员贡献以模块、PR、提交内容与实际讲解共同核对。

三、运行环境与使用方法

3.1 本次验收环境

项目 实际版本/方式
操作系统 Windows,系统构建 22631
Python / Flask Python 3.11.9 / Flask 3.1.2
后端测试 pytest 9.1.0
Node.js / npm Node.js 24.19.0 / npm 11.17.0,用于前端开发与构建
Vue / Vite Vue 3.5.45 / Vite 7.3.7,安装以 package-lock.json 为准
数据保存 UTF-8 JSON;金额保存为整数分
页面与接口 Flask 同时提供构建后的 web 页面及 /api 接口

3.2 从运行包启动

  1. 从 Release 页面 下载并完整解压运行包。
  2. 安装 Python 并加入 PATH;首次运行 scripts\setup.bat 安装后端依赖,需要联网。
  3. 运行 scripts\start.bat,浏览器访问 http://127.0.0.1:5000。
  4. 记账数据保存在运行目录的 data/records.json,首次启动自动初始化。备份时关闭服务后复制 data 目录。

运行包已包含前端构建资源,使用者不需要 Node.js。开发者从源码运行时,先执行 npm --prefix frontend ci 和 npm --prefix frontend run build,将 dist 内容放到 web,再启动后端。

四、页面截图与功能演示

以下截图和视频由真实 Chrome 访问 Flask 应用生成,使用独立演示账本,不修改日常账本。它们展示软件功能;现场向助教演示的情况应以现场实际记录为准。

4.1 主界面与新增收支

主界面与空账本

页面分为“记一笔”“明细”“统计”三个入口。录入时,类别随收入/支出类型变化;金额保留字符串提交,后端完成权威校验。

新增工资收入

新增餐饮支出

4.2 最近记录与明细筛选

三笔真实保存后的最近记录

保存成功后通过刷新标记重新查询最近记录。记录顺序由后端统一决定,表格只展示接口返回的数据。

明细列表

金额范围筛选

本例设置最低5元、最高20元,得到餐饮20元和交通5元两笔支出。日期和金额范围均包含边界;多个条件按 AND 组合。查询或重置时回到第一页,翻页继续使用已生效的条件。

4.3 统计与输入反馈

全量收支与类别汇总

工资收入1000元、餐饮支出20元、交通支出5元,对应总收入1000元、总支出25元、结余975元、记录数3。

筛选餐饮后的统计

筛选餐饮后,收入为0元、支出20元、结余-20元、记录数1。这里的结余指当前匹配记录的收入减支出。

三位小数金额被拒绝

输入1.234元会得到校验提示,记录不会写入。后端也会拒绝非法日期、金额、类别、客户端伪造的服务端字段等输入。

后端进程重启后读取原记录

4.4 软件运行视频

查看软件演示视频

演示顺序:空账本 → 新增工资收入 → 新增餐饮支出 → 非法金额提示 → 最近记录 → 金额筛选 → 全量与类别统计 → 停止并重新启动后端进程 → 原记录与统计恢复。

五、架构、接口与关键代码

5.1 模块边界

Vue 页面与组件
       ↓
统一 API 客户端(请求、解包、错误转换)
       ↓
Flask 路由(协议、参数、状态码)
       ↓
业务模块(写入 / 统一筛选 / 排序分页 / 统计)
       ↓
JSON 存储(锁、读取、原子写入)
接口 作用
GET /api/health 服务健康检查
GET /api/categories 获取收入、支出类别
POST /api/records 新增并保存记录,成功返回201
GET /api/records 组合筛选、排序及分页
GET /api/statistics 对全部匹配记录统计,不接受分页参数

成功响应统一使用 data,错误响应使用 error.code / message / fields。前端在字段错误时定位输入,在网络错误时提示连接问题,避免把错误误显示成空账本。

以下代码从验收版本中节选,省略函数说明字符串;完整上下文可通过每段源码链接查看。

5.2 金额字符串精确转为整数分

def _cents(value, field, allow_zero=False):
    value = _text(value, field)
    if len(value) > 32 or not re.fullmatch(r'[0-9]+(?:\.[0-9]{1,2})?', value):
        _fail(field, '金额必须为普通十进制字符串,最多两位小数')
    amount = Decimal(value)
    if amount > MAX_AMOUNT or amount < 0 or (amount == 0 and not allow_zero):
        _fail(field, '金额超出允许范围')
    return int(amount * 100)

查看源码。金额先做十进制格式校验,再使用 Decimal 转换并检查范围;存储和统计使用整数分。新增金额要求大于零,筛选边界允许零。

5.3 保存成功后才返回记录

def create_record(payload: dict[str, Any], store: Any) -> dict[str, Any]:
    normalized = validate_record_payload(payload)

    record: dict[str, Any] = {
        "id": str(uuid.uuid4()),
        "date": normalized["date"],  # 校验层已收敛为 YYYY-MM-DD 字符串
        "type": normalized["type"],
        "amount_cents": normalized["amount_cents"],  # 整数分,不是 "20.00"
        "category": normalized["category"],
        "note": normalized.get("note", ""),
        "created_at": _utc_now_iso_millis(),
    }

    # 只有 append_record 成功返回后,才把结果交出去;失败时调用方拿不到记录
    return store.append_record(record)

查看源码。业务先调用统一校验,再生成 UUID 和 UTC 创建时间。返回值来自存储层,只有实际保存成功,路由才返回201;校验失败不会触碰存储。

5.4 本地文件的原子替换

def _write_all(self, records: list[dict]) -> None:
    payload = json.dumps(records, ensure_ascii=False, indent=2) + "\n"
    self._path.parent.mkdir(parents=True, exist_ok=True)

    temp_path: Path | None = None
    try:
        handle_fd, temp_name = tempfile.mkstemp(
            prefix=".records-", suffix=".tmp", dir=str(self._path.parent)
        )
        temp_path = Path(temp_name)
        with os.fdopen(handle_fd, "w", encoding="utf-8", newline="\n") as handle:
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temp_path, self._path)
        temp_path = None
    except OSError as error:
        raise StorageError(f"数据文件写入失败,原文件未改动:{error}") from error
    finally:
        if temp_path is not None:
            try:
                temp_path.unlink()
            except OSError:  # pragma: no cover - 清理失败不影响主流程
                pass

查看源码。存储层在整个“读—追加—写回”过程中持有锁;正式写入先落到同目录临时文件,完成 flush/fsync 后再 replace。读取发现损坏文件会报错并保留原内容,不能当作空账本覆盖。

5.5 先筛选、再排序分页

def query_records(
    records: Sequence[dict],
    filters: RecordFilter,
    *,
    limit: int,
    offset: int,
) -> dict:
    if limit < 1 or limit > 100:
        raise ValueError("limit 必须在 1~100 之间")
    if offset < 0:
        raise ValueError("offset 必须 >= 0")

    matched = filter_records(records, filters)
    ordered = _sort_records(matched)
    total = len(ordered)
    items = ordered[offset : offset + limit]
    return {"items": items, "total": total, "limit": limit, "offset": offset}

查看源码。filter_records 是不读文件、不分页的纯函数。查询接口先得到全部匹配记录,再稳定排序、计算 total 和切片。统计模块复用同一个筛选函数,因此分页不会截断统计范围。

统计计算继续使用整数分,分别累加 income 和 expense;类别汇总仅统计匹配支出,并按金额降序、同金额按类别名称排序。统计源码

5.6 保存通知与前端刷新

RecordForm:持久化成功,发出 saved(record)
    → EntryView 转发
    → App 增加 refreshKey
    → RecentRecords 重新请求

保存期间,表单锁住重复提交,并发出 saving 状态限制导航切换,防止请求尚未完成时表单被卸载。列表与统计请求使用序号保护,旧响应不能覆盖新结果;备注按文本展示,不将输入当作 HTML 执行。

六、三个实现亮点

  1. 金额精度贯穿接口、存储和统计。 表单提交字符串,后端 Decimal 转为整数分,汇总也使用整数分,避免多笔小额记录累加产生浮点尾差。
  2. 列表与统计共用筛选规则。 查询和统计都使用 RecordFilter 与 filter_records,条件语义统一;列表分页与全量统计各自处理,便于验证两边结果一致。
  3. 本地数据保存可验证。 写入锁和临时文件替换减少覆盖与半写风险;损坏文件保留;验收通过真实进程重启证明记录来自文件而不是页面内存。

七、重要事件与经验

7.1 替身测试与真实验收的距离

问题: 测试时由于队员进度不统一,常常需要做替身测试。统计 PR #2 的模块测试能够通过,但当时真实查询服务尚未合入,直接调用统计函数仍会因缺少 record_query 而失败。

处理: 复审时运行测试之外,又直接调用真实业务函数,确认缺的是依赖而非汇总算法;先将 PR 保留为草稿。存储和查询模块提交后,再组合真实存储、查询及统计,验证类别、日期、金额和分页口径,随后合并。

收获: 隔离测试能验证本模块计算,真实联调才能验证依赖连接。验收需要同时记录“模块通过”和“组合通过”,不能直接把使用替身的结果当作结果。PR #2 · 查询 PR #6 · 存储 PR #7

7.2 前端占位组件与正式交付发生冲突

问题: 前端框架为了验证导航与刷新,带有明确标注的占位组件;9号记录展示随后交付了同名正式组件,合并时出现两个文件的 add/add 冲突。

处理: 对照职责和公开 Props,保留9号正式表格、最近记录及竞态保护逻辑,把它们接入7号框架,并同步交接文档。再跑组件测试、前端构建以及真实客户端到后端的 HTTP 检查。

收获: 占位代码应标明归属和替换条件,整合人必须核对语义,不能只以“冲突标记已经消失”作为合并依据。前端框架 PR #8 · 记录展示 PR #5

7.3 保存过程中切换页面,会丢失完成通知

问题: 表单保存请求尚未结束时若切换导航,使用 v-if 的页面可能被卸载,保存后的通知链路需要额外保护。

处理: 最终验收修复增加 saving 事件,由表单经 EntryView 转发给 App;保存期间禁用导航,完成后恢复。回归用例检查表单仍挂载、成功通知到达、列表与统计能更新,同时补充异常分页响应应显示错误的验证。

收获: 按钮防连点只覆盖表单内部,还需要考虑页面生命周期与异步请求的关系。最终验收 PR #14

7.4 开发目录能运行,解压包仍可能安装失败

问题: 本地 ZIP 复核中,批处理脚本的换行格式导致 Windows cmd 解析异常;另一次安装检查中,脚本返回成功,但新环境仍无法导入 Flask。

处理: 包内批处理统一为 CRLF,安装步骤检查依赖实际可导入后才报告成功。打包器每次从当前后端和新构建的前端取文件,排除个人账本、虚拟环境、node_modules 和测试替身;解压到独立目录后重新安装、启动、录入并重启验证。

收获: Release 验收应从实际交付 ZIP 开始,不能只在开发目录里启动一次。相关打包修正与回归用例记录在 PR #15;PR #15仍待合并,现有 Release 附件已独立下载并通过运行验收。

八、验证结果与交付记录

验证范围 实际结果
最终应用源码(PR #14)后端测试 153 passed
最终应用源码前端组件与集成检查 15 passed
公共格式化检查 20/20 passed
Vite 生产构建 通过
真实 Chrome 功能检查 收支录入、最近记录刷新、明细筛选、统计、非法金额提示通过
新后端进程读取同一账本 保留3条记录,收入1000元、支出25元、结余975元
本地运行包解压验证 安装依赖、页面与静态资源、真实写入、筛选和重启读取通过
打包工作分支增加2项回归用例后 后端155 passed;与正式应用源码的153项分别记录
GitHub Release 发布包独立验收 重新下载后,安装、启动、主要功能与重启持久化检查通过

九、成员团队编程体验

叶凯乐(102401309)

干完这票得来场高考放松一下。

林明亮(102401331)

协作开发让我体会到模块依赖与合并顺序至关重要。

周玘晖(102401333)

在组长的带领下和 AI 的帮助下,克服了许多困难,完成了自己的任务。

高一鹏(102401330)

编写查询筛选模块时,我认识到参数校验和稳定排序对分页接口十分关键。

王圣杰(102401225)

这次团队编程中我学习到了如何和组员分工协作、沟通需求,遇到 bug 时一起排查定位,也懂得了合理规划开发流程,明白一个稳定的项目离不开规范的代码和及时的同步。

黄俊凡(102401234)

作为 PM,我在负责类别与校验模块的同时协调接口、合并和验收,这次最直接的体会是,要把每个人的代码整合成可交付的软件,必须及时同步进度、明确模块边界,并用真实联调和解压运行检查结果。

郑志銮(102401233)

我负责前端框架:按 README 立契约,搭好 Vue/Vite 骨架与 API 客户端,用 Mock 让全组并行;联调时我遇到中文编码、Node 环境缺失和构建路径 404 这几类问题,才明白验证只能落在真实环境。

王翔鹏(102401334)

我负责记账表单,最大的收获是学会按契约和其他模块对接:类别由页面注入、金额保持字符串、保存成功后只发一个事件。联调遇到接口和推送问题时和组员一起定位,才真正理解了及时同步和统一规范对团队项目有多重要。

龚巍斌(102400412)

今天我负责了记账明细和最近记录的前端展示,参与整体代码审查、问题修复和界面主题优化,加深了对团队协作和项目维护的理解,学会了从整体角度审查和优化项目,认识到软件开发不仅要实现功能,还要关注稳定性、用户体验和后续维护。

沈雷(102401212)

这次我负责筛选明细和 QA,在修复分页、竞态和异常状态并完成真实联调的过程中,体会到模块边界、请求契约和持续测试比单纯写完功能更能保证团队项目稳定落地。

林子涵(102401227)

我负责统计展示与 DevOps/演示串联。这次最深体会是统计数字必须与列表一致、类别汇总要对上总支出;参与 Release 检查和演示串联后,我更明白数据可信、空态清楚和可复现交付比单纯展示更重要。

十、参考与材料链接

posted @ 2026-11-07 19:57  JFanH  阅读(12)  评论(0)    收藏  举报