第一次团队现场编程作业
2026 秋软件工程现场编程:个人记账本(Account)团队博客
| 项目 | 内容 |
|---|---|
| 作业所属课程 | 课程班级博客 |
| 作业要求 | 现场编程作业要求 |
| 作业的目标 | 3 小时、9 个人、9 个独立模块,拼出一个真能用的本地记账本:谁写的记录,重启进程后都还在;谁的查询,别人的记录都查得到。练的不是"谁写得快",是"接口先定、各自并行、合起来能跑"。 |
| 团队名称 | 棋理学伴团队 |
| 团队成员名字-学号 | 张宇天 182400143、王龙滨 112401430、丁政伟 102401421、蔡以恒 102402117、吴涵彬 292400336、许书凯 102401414、王宇航 102401418、孟维涛 102401404、吴佳辉 112401341 |
GitHub 仓库:https://github.com/Ther-ux/Account(2026-11-07 14:16 建仓,25 次提交;9 个人各自的提交、PR,以及谁合并了谁,都留在仓库里,一条没落下)
实测口径:在一台 Windows 11 + Go 1.22.12 windows/amd64 上重跑
go test ./... -count=1 -v,37 个测试函数、含子测试共 222 项,全部 PASS,0 失败 0 跳过;go vet ./...一声不吭;go build ./cmd/ledger一把过。命令和结果都记在 3.1 节。
一、组员职责分工
技术栈在第一分钟就焊死了,从头到尾没换过:Go 1.22 标准库 + 原生 HTML/CSS/JS(ES Module)+ 本地 JSON 文件。go.mod 全文只有 module personal-ledger 和 go 1.22 两行,零第三方依赖;前端没有 npm、没有打包器,静态资源用 go:embed 直接塞进可执行文件——前端已经嵌进 exe 里了,ledger.exe 一个文件就是整个网站。
| 编号 | 姓名-学号 | GitHub | 本人独立实现的模块 | 关键提交 | PR |
|---|---|---|---|---|---|
| 1 | 张宇天-182400143 | Ther-ux | 账目模型与校验 internal/ledger/record.go;组长,另负责主线整合、cmd/ledger/main.go 接线与联调 |
4ca5512、1beb7ca |
#2、#9 |
| 2 | 王龙滨-112401430 | wanglong2006 | JSON 持久化 internal/store/json.go |
05da860 |
#4 |
| 3 | 丁政伟-102401421 | SiJiu123 | 新增账目接口 internal/httpapi/create.go(POST /api/records) |
4891aa7 |
#5 |
| 4 | 蔡以恒-102402117 | chizhi2006 | 最近记录接口 internal/httpapi/recent.go(GET /api/records/recent) |
85612cf |
#7 |
| 5 | 吴涵彬-292400336 | endlessmaybe | 收支统计接口 internal/httpapi/stats.go(GET /api/stats) |
560475f |
#3 |
| 6 | 许书凯-102401414 | shukai123123 | 记账表单 web/record.js |
f9f723a |
#10 |
| 7 | 王宇航-102401418 | Bari328 | 最近记录列表 web/recent.js |
dd2e79d |
#6 |
| 8 | 孟维涛-102401404 | Meng-tao123 | 统计筛选视图 web/stats.js |
876de51 |
#8 |
| 9 | 吴佳辉-112401341 | jhw66 | 页面框架与导航 web/index.html、web/app.css、web/app.js |
2979922 |
#1 |
分活的方式很土,但特别管用:按文件切,不按前后端切。一个人守一个文件,谁的文件谁签字,冲突窗口天然归零。九个人的活正好落在九个文件上:
9 人 × 9 文件
internal/ledger/record.go 49 行 1 号:账目结构 + 校验规则
internal/store/json.go 128 行 2 号:唯一碰 data/records.json 的模块
internal/httpapi/create.go 71 行 3 号:写入口
internal/httpapi/recent.go 74 行 4 号:读出口(最近 N 笔)
internal/httpapi/stats.go 165 行 5 号:聚合出口(筛选 + 汇总)
web/record.js 104 行 6 号:表单与提交
web/recent.js 64 行 7 号:列表渲染
web/stats.js 294 行 8 号:筛选 UI 与结果展示
web/index.html+css+app.js 168 行 9 号:三页导航、模块按需加载
wc -l 数出来的代码量:Go 业务 927 行、Go 测试 1483 行、前端 841 行,合计 3251 行。测试比业务代码多出六成——第一眼像效率事故,回头再看,是这次最划算的一笔买卖(原因见第 6 节第 3 件)。上表按"一人一份主文件"列,合计以全仓 wc -l 为准。
二、程序运行环境
| 项 | 内容 |
|---|---|
| 操作系统 | Windows 11(开发与演示均在本机);仓库同时准备了 GitHub Actions 的 Windows/Linux 矩阵 |
| Go | go 1.22(go.mod 声明);本次实测工具链 go1.22.12 windows/amd64 |
| 浏览器 | Chrome(前端 ES Module + go:embed 静态资源,无需构建步骤) |
| 依赖 | 无。Go 侧零第三方包,前端零 npm 包、零 CDN |
| 数据位置 | data/records.json(首次写入自动创建;支持 -data 指定路径) |
| 启动 | 双击 start.bat,或 go run ./cmd/ledger,浏览器打开 http://127.0.0.1:8080 |
| 演示数据 | 双击 demo.bat,使用独立的 data/demo-records.json,不污染真实账本 |
补一句:依赖只有 Go 标准库,go build 出来的单个 exe 拷到别的 Windows 机器上照样能跑。
三、软件运行截图和视频
先看货,再说理。
首页——上面记一笔,旁边是最近 5 笔和一块概览。整个产品就长这样,没有一页是摆设:

统计页——SVG 手画的收支趋势、支出类别占比,还有一条月度预算提醒:

把窗口捏到 320px 宽——手机上的样子,也是我们做响应式的下限:

操作录像(录入收入/支出 → 列表自己刷新 → 组合筛选 → 杀掉进程重启,数据还在):https://github.com/Ther-ux/Account/raw/main/media/demo.webm
截图和录像用的都是独立演示账本,没有一分钱来自真人真事。
3.1 我们实际跑出来的验证结果
我们不想用"跑通了"三个字交差。留下命令,留下数字,谁都能复跑:
| 检查 | 命令 | 实测结果 |
|---|---|---|
| 单元测试 | go test ./... -count=1 -v |
37 个测试函数、222 项(含子测试)PASS,0 FAIL,0 SKIP |
| 静态检查 | go vet ./... |
无诊断 |
| 构建 | go build -trimpath -o ledger.exe ./cmd/ledger |
成功,7.6 MB 单文件(前端已嵌入) |
| 写入 → 落盘 | POST /api/records |
201,返回账目;data/records.json 出现格式化后的记录 |
| 重启读回 | 杀进程后重启,GET /api/records/recent |
仍返回同一条 12.50 元 饮食 午饭 |
| 组合筛选 | GET /api/stats?minCents=1000&maxCents=2000 |
expenseCents:1250,端点包含 |
| 导出 | GET /api/export?format=csv |
UTF-8 BOM + 中文表头,金额 12.50 |
(go test 特意加了 -count=1,为的是绕开测试缓存,看到的是真跑,不是"缓存命中"。)
四、关键代码
下面六段都是从最终仓库里剪下来的,注释留着——那是我们写给"三天后的自己"的,不是写给评分表的。
4.1 账目模型:日期不做"看起来对"的校验(1 号 张宇天,internal/ledger/record.go)
账目模型是全组唯一一处"业务规则"来源:接口层、存储层都调它,谁也别自己再写一遍。49 行里最讲究的是日期那段。
// Validate 不修改记录、不做任何 I/O,只回答"这条账目能不能进账本"。
// 它是唯一一处业务规则来源:接口层、存储层都调它,不各写一套。
func Validate(record Record) error {
date, err := time.Parse(time.DateOnly, record.Date)
// 只靠 Parse 会放过 "2026-1-7" 这种没补零的写法,
// 所以再把解析结果按 DateOnly 格式化回去与原文本逐字比较。
// date.Year() < 1 额外拒绝公历 0000 年。
if err != nil || date.Year() < 1 || date.Format(time.DateOnly) != record.Date {
return errors.New("date 必须为真实的 YYYY-MM-DD 日期")
}
if record.AmountCents <= 0 { // 金额是"分",正整数,永不出现 12.5 这种半分流
return errors.New("amountCents 必须为正整数,单位为分")
}
switch record.Type {
case "income":
switch record.Category {
case "工资", "奖金", "其他收入":
return nil
default:
return errors.New("income 的 category 必须为工资、奖金或其他收入")
}
case "expense":
switch record.Category {
case "饮食", "交通", "购物", "其他支出":
return nil
default:
return errors.New("expense 的 category 必须为饮食、交通、购物或其他支出")
}
default:
return errors.New("type 必须为 income 或 expense")
}
}
就这一小段校验,把三个坑一次性堵住了:time.Parse 会放过 2026-1-7,格式化回比能把它揪出来;date.Year() < 1 顺手挡掉公历 0000 年;金额只认正整数分,12.5 这种半分流从源头就不存在。
4.2 存储:宁可报错,也不静默清空账本(2 号 王龙滨,internal/store/json.go)
读文件这件事,最怕的不是报错,是"读出来一堆看着正常、其实全错"的记录。这段代码的第一句注释就把分工说清楚了:缺文件算空账本,文件在但读不动就是错误,两件事绝不能混。
// readRecords 只在持有 s.mu 时调用。缺文件 = 空账本;文件存在但解析失败 = 错误。
// 关键在最后 8 行:解码后逐条 Validate,否则"JSON 合法但账目非法"
// (金额为负、类别与类型不匹配)会被当成正常账本读出来,一路污染统计。
func (s *JSONStore) readRecords() ([]ledger.Record, error) {
...
var decoded []*ledger.Record // 用指针才能区分 null 元素和空对象
if err := json.Unmarshal(data, &decoded); err != nil {
return nil, fmt.Errorf("解析账本 %q: %w", s.path, err)
}
records := make([]ledger.Record, len(decoded))
for i, record := range decoded {
if record == nil { return nil, ... }
if err := ledger.Validate(*record); err != nil {
return nil, fmt.Errorf("账本第 %d 条记录无效: %w", i+1, err)
}
records[i] = *record
}
return records, nil
}
// writeRecords:先写临时文件 → Sync 落盘 → 最后一步才 rename 覆盖。
// 任何一步失败都返回错误,原账本一个字节都不动;不存在"打开-截断-写入"的窗口。
temp, err := os.CreateTemp(dir, "."+filepath.Base(s.path)+"-*.tmp")
...
n, writeErr := temp.Write(data) // 短写要当错误,不能只判断 err == nil
if writeErr == nil { writeErr = temp.Sync() }
closeErr := temp.Close() // Windows 上必须先关再改名
...
if err := os.Rename(tempPath, s.path); err != nil { ... }
这两处都是复审时才补上的(第 6 节第 3 件)。补完之后的逻辑可以缩成一句:读进来的每一条都得先过 Validate,写出去的每一步都得留退路。
4.3 写接口:把"三个请求体"这种边界堵在入口(3 号 丁政伟,internal/httpapi/create.go)
写接口最容易在"正常用户"身上翻车。这段代码干脆假设每个请求都不干净,先堵门,再干活:
// 保存函数由 main.go 注入,接口层不 import store,也不直接碰文件。
func Create(appendRecord func(ledger.Record) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 先限 1MB,防超大请求打满内存
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // 字段写错(amount 而非 amountCents)要报错,不能悄悄丢
// 用 *ledger.Record 解码:顶层传 null 时 record == nil,能区分"空对象"和"null"
var record *ledger.Record
if err := decoder.Decode(&record); err != nil { /* 空体 / 坏 JSON → 400 */ }
if record == nil { /* 顶层 null → 400 */ }
// 再解一次,必须立刻得到 io.EOF:证明请求体里只有一个 JSON 文档
var trailing json.RawMessage
if err := decoder.Decode(&trailing); err != io.EOF { /* 400 */ }
if err := ledger.Validate(*record); err != nil { /* 400,原文回显校验原因 */ }
if err := appendRecord(*record); err != nil {
// 存储失败只给客户端可读提示,绝不把 C:\... 绝对路径吐回去
createWriteError(w, http.StatusInternalServerError, "保存账目失败,请检查磁盘空间或文件权限后重试")
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(*record) // 成功响应一定在校验和保存之后才写
}
}
最后两行有个细节:201 是在校验通过、落盘成功之后才写的。早写一行,客户端就会以为已经存住了——而这种"以为",往往要到重启之后才暴露。
4.4 最近记录:"最近"按录入顺序,不按日期(4 号 蔡以恒,internal/httpapi/recent.go)
"最近"按什么排,我们纠结过一阵,最后定了按录入顺序。理由很朴素——用户今天补记一笔上周的账,它就该出现在"最近"的第一条。
// limit 缺省 10,显式出现就必须是 1~100 的纯数字:逐个字符判 '0'..'9',
// 这样 "+5"、"-1"、"1.0"、重复参数都会被挡在外面,而不是被 Atoi 悄悄接受一半。
limit := 10
if values, provided := query["limit"]; provided {
if len(values) != 1 { /* 400 */ }
for _, digit := range values[0] { if digit < '0' || digit > '9' { /* 400 */ } }
limit, err = strconv.Atoi(values[0])
if err != nil || limit < 1 || limit > 100 { /* 400 */ }
}
records, err := listRecords() // 每次请求重新读文件,新增后不会返回旧数据
...
// 空账本也要分配切片:JSON 才能编码成 [] 而不是 null(前端少一个判空分支)。
// 从末尾往前复制,而不是 sort.Slice 按日期排:
// 用户今天补记一笔上周的账,它也应该出现在"最近"的第一条。
result := make([]ledger.Record, limit)
for i := range result {
result[i] = records[len(records)-1-i]
}
limit 那段看着啰嗦,是为了不接受 +5、1.0 这种"Atoi 半懂不懂"的输入——参数校验要么全对要么全拒,中间地带最坑。
4.5 统计:所有条件 AND、端点包含、溢出要拦(5 号 吴涵彬,internal/httpapi/stats.go)
五个筛选条件,最容易漏分支的地方。写法很干脆:一行五连短路与,边界一律包含。
// 五个条件全部为 AND;空条件视为"不限";边界一律包含(>= / <=)。
// 日期是 "YYYY-MM-DD" 字符串,同格式下字典序即时间序,可以直接比较。
func (filters statsFilters) matches(record ledger.Record) bool {
return (filters.from == "" || record.Date >= filters.from) &&
(filters.to == "" || record.Date <= filters.to) &&
(filters.category == "" || record.Category == filters.category) &&
(!filters.hasMin || record.AmountCents >= filters.min) &&
(!filters.hasMax || record.AmountCents <= filters.max)
}
// 累加前拦溢出:int64 溢出是静默回绕的,统计出一个负数余额比报错更糟。
if *total > math.MaxInt64-record.AmountCents {
return statsResult{}, errors.New("统计金额超出可计算范围")
}
*total += record.AmountCents
真正值得说的是第二段。int64 加爆了不会报错,只会绕回负数——报表里冒出一个负余额,比直接报错危险得多。
4.6 前端:输入框里的"草稿"不等于"已应用的筛选"(8 号 孟维涛,web/stats.js)
前端最容易被糊弄的地方在这儿:输入框里打的字,不等于已经生效的查询条件。我们用一个变量把两者硬分开。
let appliedFilters = {}; // 只有真正"查询"过的条件才在这里;图表、预算、结果都只读它
function readFilters() { /* 读输入框 → 校验 → 转成 {from,to,category,minCents,maxCents};不合法就 throw */ }
function submit(event) {
event.preventDefault();
try {
const filters = readFilters();
// 关键一行:只有显式点击、且通过校验的查询才会改写已应用条件。
// 用户只是打字或展开高级筛选时 appliedFilters 保持不动,
// 于是"页面上的结果"和"已应用的条件"永远一致,不会对不上。
appliedFilters = filters;
syncControls();
void load();
} catch (error) {
// 校验失败就地提示并把焦点打回出错的输入框,已应用的条件一个都不改
show(error.message, "error");
error.input?.focus();
}
}
appliedFilters 是唯一裁判:图表、结果、列表都只认它。用户还在打字,页面就不会偷偷改数——这条规矩让我们在演示时省掉了无数句"这个图为什么变了"。
五、软件亮点
1. 金额从头到尾都是整数"分",一次浮点都没碰。
Go 侧 AmountCents int64,JSON 直接存整数;前端录入时按十进制文本拆成整数分,显示时用 BigInt 做 n/100n 与 n%100n 的拼接(web/app.js、web/record.js),一旦超出 JavaScript 安全整数范围就报错,而不是静默舍入。好处是"0.1+0.2"这类经典事故这次一次都没发生过,统计结果可以做到分毫不差。
2. 账本文件的设计目标是"坏掉也读得出来、写坏也不丢数据"。
写入走临时文件 + Sync + rename 的原子替换;读取时逐条 Validate,把"JSON 语法合法但业务非法"的记录挡在门外;文件真坏了就明确报错、保留原文件,绝不自动清空重建。这条不是设计时想到的,是复审时被 2 号补上的(第 6 节第 3 件),后来还专门补了一组回归测试守着它。
3. 首屏只加载"记一笔"需要的那点代码。
web/app.js 用动态 import() 按页签懒加载模块,四个模块各自 init(container)、互不引用;任何模块加载失败只在自己面板里显示一句"暂时无法加载",不会把整页打白。在 5 小时的预算里,它换来的是九个人同时改九份文件、一次会都没开——页面外壳归 9 号,模块怎么渲染归各自作者,性能只是顺带的。
六、耗时最长、收获最大的三件事
这 170 分钟里真正吃掉时间的,是下面这三件事(写代码反而排在后面)。
1)用"接口契约"替代争论:先定 Record,再各写各的(约 40 分钟,全组)
14:30 第一版骨架提交时,README 里已经把三件事钉死了:Record 只有 5 个字段(date/type/amountCents/category/note)、金额是整数分、三个 API 的路径和状态码固定。1 号先把自己那笔模型和校验合进主线(4ca5512),后面每个人开工前都先同步这个提交:3 号在自己贡献文档里逐文件比对 Git blob 确认模型一致,4 号干脆 fast-forward 到 4ca5512 才开始写 recent.go。
收获有点反直觉:我们几乎没有"讨论",因为没得讨论。谁想加一个 id、或者把金额改成浮点元,都得先说服另外八个人一起改,成本高到没人愿意提。真正耗时间的从来是"两个人对同一个数据结构有两种想象",打字一点都不慢——我们把这件事挪到了最前面的 40 分钟里一次性解决。
2)6 号的 record.js 提交到了仓库根目录,页面"记一笔"整块空白(约 25 分钟,6 号 + 组长)
16:25 的 f9f723a 是通过网页上传的,文件落在了仓库根目录的 record.js,而 app.js 里写的是 import("./record.js")——相对于 web/ 解析,请求 404,记账面板直接空掉,控制台只剩一行模块加载错误。
组长顺着"谁引用谁"这条静态关系找到病根,用 ed2df74 fix(web): move record form module into web directory 把文件移进 web/,顺手把全部前端资源的相对路径核了一遍。
收获:"能打开首页"和"功能能用"是两个验收标准。这次之后,我们把验收口径从"首页 200"改成"三个页签都点一遍,再录一笔看列表有没有刷新"——这一改,让后面 20 组浏览器检查从加分项变成了必做项。
3)整合复审:2 号存储模块 93 行 → 128 行,测试从 0 加到 377 行(约 50 分钟,2 号 + 组长)
整合时对着 SOURCE_MANIFEST.json 逐个文件比 SHA-256,比出两个问题。一是原 json.go 读回只靠 json.Unmarshal,只要 JSON 语法对,就被当成正常账本——金额为负、类别和类型对不上的记录能一路流进统计。二是写文件是直接覆盖,进程写到一半被杀,就可能留下半截文件。
复审后这一处改动是 +84 −49:读回逐条 Validate,写入改成临时文件 + Sync + rename,另外新增 integrity_test.go,专测"损坏文件必须被保留"和"语义非法的记录必须被拒"。
收获在评分表之外:"我的模块测试通过"不等于"合起来是对的"。9 个模块各自的测试都是绿的,可接缝处那个假设——"读回来的东西一定是合法的"——没人测过。测试代码最后 1483 行、业务代码 927 行,就是这么来的:接缝比功能更容易出事。
另外记一笔小的:CI 第一次在 Windows 上检出时,被 CRLF 绊了一跤,格式检查红了。最后用
.gitattributes钉死 LF(878e5ad fix(ci): preserve LF line endings on Windows checkouts)。"本地好好的、CI 红了"这种事,这次只来得及碰上一次。
七、每位组员的团队编程体验
九段话,各人写各人的——谁卡在哪里、怎么绕过去的,只有本人说得清。
1 号 张宇天(账目模型 + 整合):我先把 Record 和 Validate 钉死、再合进主线,图的是让后面八个人不用跟我商量就能开工。结果压力全落在自己身上:字段写错一个,八个人一起返工。整合时对着 SHA-256 把九个包一个个并进来、补 main.go 的接线,我才明白组长这份活主要是替接缝负责——功能写得好不好是各人的事,能不能合上才是我的事。
2 号 王龙滨(JSON 存储):我原以为存储就是读文件、追加、写回,写出 93 行还挺得意。复审时被指出"JSON 合法但账目非法"的记录会被当成正常账本读出来,我才知道存储层要兜的不只是文件,还有业务规则。改成临时文件 + rename 之后,我第一次认真去想一个很具体的问题:进程要是写到一半被杀掉,磁盘上会留下什么。
3 号 丁政伟(新增账目接口):接口注入 appendRecord,测试就完全不用碰文件系统,39 组用例不到 0.1 秒跑完,改起来很爽。真正卡住我的是"请求体只能有一个 JSON 对象":得再解一次、确认拿到 io.EOF,我一开始漏了,测试塞两个对象进去,程序只当第一个处理,还一声不吭。
4 号 蔡以恒(最近记录接口):limit 的校验我第一版用 strconv.Atoi,+5 居然能过,1.0 被拒了又说不清为什么,后来改成逐个字符判数字。"最近"到底按日期还是按录入顺序排,我们也纠结过——最后按录入顺序,因为补记上周的账更该排在最前面,为此专门写了 TestRecentBackdatedRecordAndRepeatedReads。
5 号 吴涵彬(收支统计接口):五个筛选条件怎么组合最容易漏分支,我把 matches 写成一行五个短路与,边界全用 >=/<=,再用表驱动测试把"只给起始日期""只给金额上限""条件互相矛盾"一个个钉死。溢出是写完才想起来的:int64 加爆了不报错,只绕回负数,报出一个负余额比直接报错难解释多了。
6 号 许书凯(记账表单):我把 record.js 传到了仓库根目录,"记一笔"整块空白——这是我这次最贵的一次错误,也让我第一次弄明白 import("./record.js") 到底是相对谁解析的。表单那边我坚持了两件事:失败保留已填内容、重复点击只发一次请求,因为现场演示时网络和手抖都是真的。
7 号 王宇航(最近记录列表):列表要在新增后自动刷新,我一开始写的是定时轮询,被组里指出会闪、会白发请求,改成监听 ledger:changed 事件。渲染其实只有 64 行,但空账本、只有一笔、备注特别长这三种状态我都单独验过——演示时最容易翻车的,往往就是空状态。
8 号 孟维涛(统计筛选视图):最花时间的不是画图,是分清楚"输入框里的草稿"和"真正查询过的条件"。图表要是跟着输入框实时变,用户就永远不知道眼前的结果对应哪组条件。最后用 appliedFilters 单独存一份,只有点"查询"才覆盖;代价是这套 UI 涨到 294 行,好处是演示时一句话就能讲明白。
9 号 吴佳辉(页面框架与导航):我负责"壳":三个页签、#hash 路由、模块按需加载、加载失败降级。跟八个人对接下来,体会是壳越薄越好——我只定容器 ID 和 init(container) 这个签名,别的都不管,所以他们改各自的文件时不用来问我。三页导航做完才发现手机宽度会挤,又补了 320px 的样式。
八、GitHub 仓库与 commit 记录
- 仓库:https://github.com/Ther-ux/Account
- 建仓:2026-11-07 14:16(北京时间);首次提交 14:30;末次提交 16:52
- 提交:25 次;参与账号 9 个(
Ther-ux14 次(5 次本人模块 + 9 次合并),其余 8 人各有 1~2 次本人模块提交) - 分支与 PR:共 10 个 PR,9 个已合并进
main;成员各自在自己的feature/*分支或自己的 fork 上完成本人模块,再由组长合并进主线(1 号本人的联调 PR #9 保留在 open 状态,其改动已直接进入main) - 代码规模:3251 行(Go 业务 927 / Go 测试 1483 / 前端 841)
- Release:https://github.com/Ther-ux/Account/releases
25 次提交的时间线,按真实顺序摆在下面;16:00 到 16:30 是合并高峰,半小时里落了 9 个提交,其中 5 个是合并 PR 的:
| 时间 | commit | 作者 | 说明 |
|---|---|---|---|
| 14:30 | 80ecca1 |
Ther-ux(1 号) | Initial commit — 建仓 |
| 14:35 | 2979922 |
wjh(9 号) | feat:base — 骨架:main.go、index.html、README |
| 14:43 | dd9b788 |
wjh(9 号) | Merge remote-tracking branch 'origin/main' into main |
| 14:48 | a56c759 |
Ther-ux(1 号) | Merge PR #1 from jhw66/main(骨架并入主线) |
| 15:21 | 560475f |
海尔达姆(5 号) | Add files via upload — stats.go + stats_test.go |
| 15:21 | 4ca5512 |
Ther-ux(1 号) | feat(ledger): define record validation |
| 15:22 | db9037c |
海尔达姆(5 号) | Delete docs/contributions/member05.md |
| 15:24 | 756ac69 |
Ther-ux(1 号) | Merge PR #2 from feature/record-model |
| 15:32 | e1aec7b |
Ther-ux(1 号) | Merge PR #3 from endlessmaybe/main(统计接口) |
| 15:39 | 85612cf |
chizhi2006(4 号) | 最近记录api — recent.go |
| 15:44 | 05da860 |
wanglong2006(2 号) | feat(store): persist ledger records in JSON |
| 15:45 | 1b8bae5 |
Ther-ux(1 号) | Merge PR #4 from feature/json-store |
| 15:58 | 28223a8 |
chizhi2006(4 号) | Merge branch 'Ther-ux:main' into main |
| 16:01 | 4891aa7 |
SiJiu123(3 号) | feat(api): import member03 record creation module |
| 16:02 | dd2e79d |
Bari(7 号) | feat(web): 新增最近记录模块 web/recent.js |
| 16:07 | 9553ca5 |
Ther-ux(1 号) | Merge PR #5 from feature/member03-create-api |
| 16:07 | 3bb6836 |
Ther-ux(1 号) | Merge PR #6 from Bari328/main |
| 16:14 | d6a6b2a |
Ther-ux(1 号) | Merge PR #7 from chizhi2006/main |
| 16:15 | 876de51 |
mengtao123(8 号) | feat(web): 实现统计筛选视图 web/stats.js |
| 16:21 | 2f6d4df |
Ther-ux(1 号) | Merge PR #8 from Meng-tao123/feature/stats-filter |
| 16:25 | f9f723a |
shukai123123(6 号) | Add files via upload — record.js |
| 16:29 | fed62cd |
Ther-ux(1 号) | Merge PR #10 from shukai123123/main |
| 16:30 | ed2df74 |
Ther-ux(1 号) | fix(web): move record form module into web directory |
| 16:49 | 1beb7ca |
Ther-ux(1 号) | feat: integrate Account v1.1.0 and update README |
| 16:52 | 878e5ad |
Ther-ux(1 号) | fix(ci): preserve LF line endings on Windows checkouts |
九、PSP2.1 时间记录
| PSP2.1 | 阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 12 | 10 |
| · Estimate | · 估计这个任务需要多少时间 | 12 | 10 |
| Development | 开发 | 158 | 142 |
| · Analysis | · 需求分析(含学习新技术) | 20 | 16 |
| · Design Spec | · 生成设计文档 | 8 | 7 |
| · Design Review | · 设计复审 | 6 | 5 |
| · Coding Standard | · 代码规范 | 6 | 5 |
| · Design | · 具体设计 | 10 | 9 |
| · Coding | · 具体编码 | 70 | 68 |
| · Code Review | · 代码复审 | 18 | 16 |
| · Test | · 测试 | 20 | 16 |
| Reporting | 报告 | 27 | 18 |
| · Test Report | · 测试报告 | 10 | 8 |
| · Size Measurement | · 计算工作量 | 5 | 4 |
| · Postmortem & Process Improvement Plan | · 事后总结与改进计划 | 12 | 6 |
| 合计 | 197 | 170 |

浙公网安备 33010602011771号