开始界面
图 1 开始界面:发光标题、飘动箭头、玩法卡片、实时演示棋盘与音效开关

AIGC标识 第一次团队现场编程作业

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 笔和一块概览。整个产品就长这样,没有一页是摆设:

01_首页

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

02_统计页

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

03_手机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-ux 14 次(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
posted @ 2026-11-07 19:57  Bari  阅读(25)  评论(0)    收藏  举报
悬停被挡的箭头
图 2 悬停被挡:红色路径 + 阻挡者橙色框
悬停可飞出的箭头
图 3 悬停可飞出:绿色路径延伸到棋盘外