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 从运行包启动
- 从 Release 页面 下载并完整解压运行包。
- 安装 Python 并加入 PATH;首次运行
scripts\setup.bat安装后端依赖,需要联网。 - 运行
scripts\start.bat,浏览器访问http://127.0.0.1:5000。 - 记账数据保存在运行目录的
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 执行。
六、三个实现亮点
- 金额精度贯穿接口、存储和统计。 表单提交字符串,后端 Decimal 转为整数分,汇总也使用整数分,避免多笔小额记录累加产生浮点尾差。
- 列表与统计共用筛选规则。 查询和统计都使用 RecordFilter 与 filter_records,条件语义统一;列表分页与全量统计各自处理,便于验证两边结果一致。
- 本地数据保存可验证。 写入锁和临时文件替换减少覆盖与半写风险;损坏文件保留;验收通过真实进程重启证明记录来自文件而不是页面内存。
七、重要事件与经验
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 检查和演示串联后,我更明白数据可信、空态清楚和可复现交付比单纯展示更重要。

浙公网安备 33010602011771号