2026秋软件工程现场编程作业
| 项目 | 内容 |
|---|---|
| 作业所属课程 | 202601 软件工程 |
| 作业要求 | 2026秋软件工程现场编程作业 |
| 作业目标 | Note 是一个本地运行的网页版个人记账本,支持收入与支出录入、账单筛选、收支统计和图表展示。账单保存在本地 JSON 文件中,关闭程序后重新启动仍可读取。 |
| 团队名称 | 看懂每一步 |
| 团队成员学号—姓名 | 102401434包学丰 102401436冯玄 102401111郑瀚 102401615秦文贵 102401622吴圣洲 102401435乔震宇 102301225刘运昆 162501110蒋书瑞 102301230林为峰 102401629王元豪 |
1.组员职责分工
| 成员 | 学号 | GitHub | 负责内容 | 主要文件 |
|---|---|---|---|---|
| 包学丰(组长) | 102401434 | tw1l1ghtcc | 数据存储、公共异常、应用入口、模块整合及项目文档 | app.py、storage.py、errors.py、requirements.txt、.gitignore、tests/test_storage_app.py、README.md |
| 冯玄 | 102401436 | otto2-5-8 | 新增与删除账单、业务校验、记账测试及演示脚本 | records.py、tests/test_records.py、tests/support.py、scripts/demo_records.py |
| 秦文贵 | 102401615 | vg666-NiKo | 查询参数校验、组合筛选、排序与列表接口 | queries.py、tests/test_queries.py |
| 乔震宇 | 102401435 | 1530017982 | 收支汇总、分类与月度统计、统计接口 | statistics_service.py、tests/test_statistics_service.py |
| 吴圣洲 | 102401622 | drkalerq | 页面框架、录入表单、收支切换、金额换算与录入交互 | templates/index.html、static/css/main.css、static/js/entry.js |
| 郑瀚 | 102401111 | Kerical | 账单列表、筛选与删除交互、统计看板、图表及请求封装 | static/js/list.js、static/js/dashboard.js、static/js/api.js、static/css/views.css |
| 刘运昆 | 102301225 | kun123567 | 代码检查,博客编写,截图 |
2.程序运行环境
- Python 3.10或以上版本,可使用 Python 3.12。
- Git(使用 Git 克隆、更新代码时需要)。
- Chrome、Edge 等支持现代 JavaScript 的浏览器。
3.软件运行截图
4.软件实现的关键代码
原子化保存账单(storage.py)
def save_records(records: list[dict]) -> None:"""校验后保存;调用方须用 records_lock 包住完整的读取、修改、保存过程。"""with records_lock:temporary_path = Nonetry:_validate_records(records)# 即使直接调用保存,也不能覆盖已损坏的原文件。if DATA_FILE.exists():_read_existing()DATA_FILE.parent.mkdir(parents=True, exist_ok=True)# 临时文件与正式文件位于同一目录,写完整后再替换。with tempfile.NamedTemporaryFile(mode="w",delete=False,dir=DATA_FILE.parent,encoding="utf-8",) as stream:temporary_path = Path(stream.name)json.dump(records, stream, ensure_ascii=False, indent=2)stream.flush()os.fsync(stream.fileno())os.replace(temporary_path, DATA_FILE)except (OSError, ValueError, TypeError, UnicodeError) as exc:if temporary_path is not None and temporary_path.exists():temporary_path.unlink(missing_ok=True)raise _storage_error() from exc
这段代码的关键在于 os.replace(temporary_path, DATA_FILE):临时文件与正式文件同目录,写完整、fsync 落盘后再原子替换,正式文件在任何时刻要么是旧的完整版本,要么是新的完整版本。records_lock 则配合业务层的“读—改—写”流程,防止并发请求互相覆盖。
金额“元 → 分”换算(static/js/entry.js)
* “元”文本 → 整数“分”。* 按字符串拆分整数位与小数位,不用 parseFloat(text) * 100,* 避免浮点乘法带来的精度误差(如 19.99 → 1998)。* @returns {{cents: number} | {error: string}}*/
function toCents(text) {const raw = String(text == null ? "" : text).trim();if (raw === "") {return { error: "请填写金额" };}// 只允许整数或最多两位小数if (!/^\d+(\.\d{1,2})?$/.test(raw)) {return { error: "金额只能包含数字,最多两位小数,例如 12.50" };}const [yuanPart, fractionPart = ""] = raw.split(".");const cents =Number(yuanPart) * 100 + Number((fractionPart + "00").slice(0, 2));if (!Number.isSafeInteger(cents) || cents < 1 || cents > MAX_AMOUNT_CENTS) {return { error: "金额需在 0.01 元到 9,999,999.99 元之间" };}return { cents };
}
这个函数是“用户输入”与“后端存储”之间的桥梁。它刻意避开了 parseFloat,因为 19.99 这类金额在二进制浮点中无法精确表示,直接乘 100 会得到 1998 而非 1999。字符串拆分整数位与小数位是记账场景中处理金额精度的标准做法,同时返回值统一为 { cents } 或 { error },调用方无需再处理异常。
5.软件的3项亮点
项目亮点
亮点一:金额精度的端到端保障
记账应用最怕"对不上账",而浮点数精度是这一问题的常见根源。本项目在前端录入和后端存储两端分别设防,确保金额始终以精确的整数"分"流通。
- 前端防浮点误差:
static/js/entry.js中的toCents函数刻意绕开parseFloat(text) * 100,因为19.99 * 100在浮点运算中会得到1998而非1999。它改为按字符串拆分整数位和小数位来换算,并用正则^\d+(\.\d{1,2})?$限定"整数或最多两位小数"。 - 后端防类型混淆:
storage.py强制金额字段为整数,并用type(record["amount"]) is not int排除True被误当作1分的情况,同时限定在1到999999999分的合法范围内。
一个防浮点误差,一个防类型混淆,两端共同守住了金额的精确性。
亮点二:本地存储的"不丢数据"设计
用本地 JSON 文件存账单,最大的风险是写入过程中断导致文件损坏,或并发请求互相覆盖。storage.py 的 save_records 用一套组合拳规避了这些问题:
- 原子替换:先写临时文件,
flush()并os.fsync()确保真正落盘后,再用os.replace原子地替换正式文件。正式文件在任何时刻要么是旧的完整版本,要么是新的完整版本,不会出现只写了一半的损坏文件。 - 防御性校验:保存前先调用
_read_existing()检查原文件是否已损坏;若原文件有问题,宁可报错也不覆盖,保留用户仅存的可用数据。 - 可重入锁:使用
RLock而非普通Lock,因为业务层需要以"读—改—写"的完整流程嵌套获取同一把锁,避免并发请求之间的写覆盖。
这套设计让一个单进程的本地存储也具备了接近数据库级别的数据安全性。
亮点三:前后端校验口径的刻意对齐
本项目没有把校验完全推给后端,而是让前端在提交前做一套与后端完全一致的轻量校验,既提升体验,也减少无效请求。
- 职责说明明确:
entry.js开篇即写明"提交前做与后端一致的轻量校验,失败保留已填内容并展示原因"。 - 字符计数对齐:前端用
codePointLength按 Unicode 码点计数,与后端 Python 的len()口径保持一致,避免中文备注长度判定出现分歧。 - 日期校验更真实:用
isRealDate检查"真实存在的日期"(含闰年、月末天数),而非只匹配格式。 - 规避时区陷阱:
localToday手动拼接年月日,刻意不用toISOString,防止 UTC 时区偏移把"今天"变成"昨天"。 - 错误语义统一:后端
app.py将所有AppError和框架级HTTPException映射为前端可识别的{error, code}结构,前端无需根据 HTTP 状态码猜测错误含义。
前后端在校验规则和错误语义上的这种对齐,减少了很多"前端放行、后端拒绝"的割裂感。
6.在编码、争论、复审等活动中花费时间较长,较大收获的事件
Note 软件开发耗时事件与核心收获复盘
事件一:推翻浮点运算,确立“元/分”字符串换算方案
- 涉及活动:方案争论、编码实现
- 背景与耗时原因:记账应用对金额精度的要求极高。最初部分成员尝试使用常规的
parseFloat(text) * 100进行“元”到“分”的转换,但在测试中发现19.99 * 100会因 JavaScript 浮点数精度问题得到1998而非1999。团队因此花费了较多时间进行争论,比较了四舍五入、使用第三方精度库以及字符串拆分等多种方案。 - 最终决策与收获:最终团队决定摒弃任何浮点乘法,在
static/js/entry.js中手写了toCents函数,通过字符串拆分整数位与小数位来实现精确换算[reference:0]。同时,后端storage.py严格要求金额必须为整数分,并利用type(record["amount"]) is not int排除了布尔值被当作1分的隐患[reference:1]。这一争论让大家深刻认识到金融级数据处理的严谨性,确立了“端到端整数化”的防错机制。
事件二:重构本地 JSON 存储,应对“写坏文件”与并发隐患
- 涉及活动:代码复审、底层架构重构
- 背景与耗时原因:项目采用本地 JSON 文件存储数据,在早期的代码复审中,团队发现直接使用
json.dump覆盖写入存在致命风险:如果在写入一半时程序崩溃或断电,整个账单文件将损坏,导致用户数据全部丢失。此外,多个并发请求同时写入时容易发生覆盖。 - 最终决策与收获:为了解决该问题,团队耗费大量时间重新设计了
storage.py的存储流程。引入了“同目录临时文件 +os.fsync()+os.replace”的原子替换机制,确保正式文件在任何时刻要么是旧的完整版本,要么是新的完整版本[reference:2]。同时引入threading.RLock,配合业务层的“读—改—写”流程进行并发保护[reference:3]。这段经历让团队深刻理解了操作系统层面的文件安全与原子操作的重要性。
事件三:前后端校验口径对齐与异常体系统一
- 涉及活动:接口联调、设计争论
- 背景与耗时原因:在联调阶段,前端录入了带有表情符号的备注或处于特殊时区的日期,经常遇到“前端放行、后端拒绝”的割裂感。例如,JavaScript 的
toISOString可能因 UTC 偏移导致“今天”变成“昨天”;普通length与 Python 的len()对 Unicode 字符的计数不一致。 - 最终决策与收获:团队通过反复沟通,决定在前端
entry.js中提前实现一套与后端完全一致的轻量校验:使用codePointLength按码点计数[reference:4];手写localToday拼接年月日以规避时区问题[reference:5]。在后端,通过app.py的统一错误处理,将所有AppError和框架级HTTPException映射为前端可识别的{error, code}结构[reference:6]。这次对齐工作虽然繁琐,但极大地提升了用户体验,并减少了无效的网络请求。
事件四:模块化整合与接口约定的“阵痛”
- 涉及活动:团队协作、代码集成
- 背景与耗时原因:项目由成员分工完成,初期大家各自开发顺畅,但在最终整合时却遭遇了巨大阻力。正如组长包学丰在编程体验中记录的:“起初以为分别完成代码最后合并就能运行,联调时才发现,函数名、参数和错误格式稍有不同,就可能让模块无法对接”[reference:7]。
- 最终决策与收获:为解决整合问题,团队花费大量时间进行开会协调与接口重构,严格定义了
records.py、queries.py等模块间的蓝图(Blueprint)注册方式和数据返回格式。这次阵痛让大家深刻体会到“提前约定接口”的重要性,不仅提升了代码的规范性,也锻炼了跨角色沟通与阅读他人代码的能力[reference:8]。
7.每个组员的团队编程体验
团队成员编程体验汇总
包学丰(102401434)—— 存储、公共异常、入口与整合
核心工作 在本次团队项目中,我主要负责底层的数据存储模块、公共异常处理、应用入口编写以及最后的代码模块整合工作。作为组长,除了完成自身的业务代码,我还需要时刻关注组员的提交进度,协调公共文件的冲突,确保最终合并到主分支的代码能够顺利运行。
遇到的挑战与踩坑 刚开始时,我以为团队合作就是把大家各自负责的代码像拼图一样拼在一起。但在真正进行联调时,我才深刻体会到这其中的痛点。各个模块之间如果没有提前商量好接口,最后合并简直就是一场灾难。比如前端传过来的参数名和后端接收的不一致,或者不同模块抛出的异常格式不同,导致前端无法统一处理。除此之外,在处理并发请求和文件写入时,我们遇到了本地 JSON 文件写一半崩溃导致数据损坏的隐患。
收获与反思 通过这次项目,我深刻理解了“提前约定接口”和“契约先行”到底有多重要。我们后来统一了 Flask 蓝图(Blueprint)的注册方式,规范了数据返回格式,整个过程虽然痛苦,但极大地提升了代码的健壮性。我不仅摸清了从 Flask 接收请求到调用业务代码,再到返回结果的完整全链路,还学会了如何用 RLock 锁和 os.replace 的原子操作来保证数据的安全。团队合作不仅是完成自己的部分,更需要去读懂他人的代码,把问题沟通清楚。
冯玄(102401436)—— 新增、删除与业务校验
核心工作 我主要负责 records.py 中账单的新增和删除逻辑,以及五项核心的业务校验规则。由于记账应用的特殊性,我们需要在前后端统一将金额转换为整数“分”来存储,并在新增账单时完成“读取现有数据 → 追加新数据 → 保存到文件”的完整加锁流程。
遇到的挑战与踩坑 在编写业务校验时,我遇到了一个非常隐蔽的 Bug。在 Python 中,bool 类型其实是 int 的子类(True == 1,False == 0)。当用户恶意或错误地传入 amount=true 时,原本的校验逻辑会把它当成数字 1 分并通过入库,甚至返回 201 创建成功的状态码。这让我意识到,不能仅仅依赖于简单的 isinstance 检查。
收获与反思 我的测试观念发生了转变:从最初的“本地测试跑通全绿就行”,变成了“必须拉取最新主干,跑通整体应用才算数”。我记住了写盘必须用临时文件加 os.replace 原子替换的规范,防止数据损坏。在技术上,这让我明白语义陷阱必须用严密的单元测试来固定。模块之间的配合往往比单点代码的编写更考验设计的严谨性。
秦文贵(102401615)—— 查询、筛选与排序
核心工作 我主要负责 queries.py 模块,实现了账单的复杂查询逻辑。核心流程是“先筛选,再排序,最后截取”,并且这个查询模块被设计为可复用的,后续的统计模块也能直接调用这部分逻辑来获取数据,保证了筛选口径的一致性。
遇到的挑战与踩坑 在开发过程中,我遇到了一些环境与协作上的小插曲。比如在提交 README 文件时,因为没有及时拉取最新代码,导致了版本冲突。另外在本地配置 Flask 环境时,遇到了网络中断,起初被一大堆下载警告吓到,后来才学会如何区分普通的网络警告与最终的安装执行结果。
收获与反思 多人协作意味着必须持续关注主仓库的变化,修改自己的本地副本不代表团队的版本已经更新。在测试策略上,我体会到了隔离测试与联调验证的区别:隔离测试主要检查模块内部的规则是否正确,而联调验证则是检查模块与模块之间能否严丝合缝地配合。这两者缺一不可,只有都做好了,整个系统才能顺畅运转。
乔震宇(102401435)—— 收支统计与汇总
核心工作 我负责的是 statistics_service.py,主要实现总收入、总支出、结余计算,以及按分类和按月的汇总统计。对外通过 GET /api/stats 接口提供数据。
遇到的挑战与踩坑 在开发统计功能时,我差点犯了一个大错。我最初想图省事,直接复用列表查询的默认逻辑。但随后发现,账单列表在前端展示时,为了避免加载过慢,默认只显示前 10 条(有截断)。如果统计模块直接复用了这个带有截断限制的结果,那么算出来的总金额将完全是错误的。我必须强制传入 allow_limit=False 来拒绝截断,确保获取全量数据。
收获与反思 单个函数计算正确,绝不代表系统就能正常运行。你还需要仔细检查参数传递、返回的字段名是否与前端约定的一致,以及各种错误处理是否到位。在团队开发中,提前把金额单位、筛选范围以及接口格式协商清楚,能够极大减少后期对接时的频繁修改。这让我对“接口即契约”有了切身的体会。
吴圣洲(102401622)—— 页面框架与录入界面
核心工作 我负责前端页面的骨架搭建、CSS 样式编写以及记账录入界面的交互。为了实现模块解耦,我固定了 9 个核心容器的 ID,并设计了 records-changed 自定义事件,让录入、列表、统计模块能够通过事件驱动的方式互相通信,而不是直接调用彼此的函数。
遇到的挑战与踩坑 前端开发遇到了不少细节坑。最典型的是金额换算,原本想用 parseFloat(text) * 100,结果出现了 19.99 * 100 = 1998 的浮点精度问题;另外用 toISOString 获取今天的日期时,受 UTC 时区偏移影响,到了晚上“今天”的账单日期可能就变成“昨天”了。在网络异常测试时,我也意识到不能简单地提示“未保存成功”,因为实际上请求可能已经送达后端。
收获与反思 我发现在协作中,契约写得越具体,后期的沟通成本就越低。前端虽然做了一层轻量校验,但它只能用来拦截用户的明显错误,最终的合法性判定必须、也一定要放在后端进行。在验证功能时,必须要用具体的数字说话,比如金额必须精确到分,而不是停留在“页面能打开、按钮能点击”的表面。
郑瀚(102401111)—— 列表、筛选交互、统计看板与图表
核心工作 我主要负责前端账单列表的渲染逻辑、筛选面板的交互,以及统计看板的构建和 Chart.js 图表的接入。在录入或修改数据后,我负责监听 records-changed 事件来触发列表和统计看板的自动刷新,从而保持前端各模块数据的联动。
遇到的挑战与踩坑 在接入图表时,我遇到了容器高度塌陷的问题,导致图表在页面初次加载时渲染异常或者挤压变形。另外,筛选面板的交互逻辑与列表渲染紧密相关,我需要确保筛选条件(比如只选支出)与统计看板的数据完全一致,否则用户会看到列表里是 5 笔支出,而图表却把收入也算进去的尴尬情况。
收获与反思 这次前端开发让我体会到,组件之间通过自定义事件和固定容器 ID 进行协作,比直接去调用对方模块的刷新函数要稳定得多,因为这是一种低耦合的设计模式。图表展示不仅仅是把图画出来,更关键的是要确保底层的数据来源与列表的筛选规则完美契合。在今后的项目中,我还会继续坚持“契约先行”的理念,这样在联调和集成时能有效避免大量的返工。
刘运昆(102301225)—— 检查代码,博客
核心工作 在本次项目中,我主要负责两大部分工作:一是作为代码审查者(Code Reviewer),负责检查各位组员提交的代码;二是负责团队博客的撰写,将项目开发过程中的技术细节、踩坑经验与架构设计整理成文,对外输出。
遇到的挑战与踩坑 代码审查工作远比想象中耗时。我需要快速理解大家编写的不同模块(从前端 JS 到后端 Python),并检查边界条件。比如在审查前端代码时,我要特别注意 parseFloat 可能引发的精度陷阱;在审查后端时,要留意 Python 中 bool 作为 int 子类可能导致的类型校验漏洞;还要检查各个接口的返回值格式是否满足统一标准。撰写博客时,最大的挑战是如何把类似 os.replace 原子写盘、RLock 防并发写覆盖这样生涩的技术点,转化为通俗易懂的团队经验分享,这需要反复斟酌语言。
收获与反思 通过“检查代码”和“写博客”,我被迫跳出了“只写自己那一亩三分地代码”的局限,获得了更宏观的系统视角。检查别人的代码让我见识到了不同的编码习惯,也让我更深刻地理解了接口对齐和防御性编程的必要性。而撰写博客则是对整个项目知识的重新梳理和沉淀。我发现,代码审查是保障软件质量的最后一道防线,而一篇好的技术博客不仅是团队成果的展示,更是对自身逻辑表达能力的一次绝佳锻炼。技术交付,不仅包含能跑的代码,也包含能沉淀下来的知识。
8.GitHub 仓库地址和 commit 记录。
GitHub 仓库地址:https://github.com/tw1l1ghtcc/Note commit 记录:https://github.com/tw1l1ghtcc/Note/commits/main/