分毫不差·知会——CNCC 2026 会议系统
分毫不差·知会——CNCC 2026 会议系统

| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 2601_CS_SE_FZU |
| 这个作业要求在哪里 | LLM结合探索CodeArts团队实战总结 |
| 团队名称 | 分毫不差 |
| 这个作业的目标 | 交付知会会议系统,并按随笔与项目评分说明实现、协作与验证 |
| 其他参考文献 | 邹欣《构建之法》;知会仓库 docs/design.md、docs/ai-coding/README.md |
目录
1. 项目 CodeArts 地址与在线部署地址
代码和页面分两处。提交记录在华为云 CodeArts,打开就能用的会议系统在线上站点。后面各章的文件链接都指向同一仓库的 main。
| 用途 | 地址 |
|---|---|
CodeArts 仓库 main | 知会 main |
| 线上站点 | https://zhihui.worldboot.net/ |
| 健康检查 | https://zhihui.worldboot.net/api/health |
(1)CodeArts
● 项目编号 0f1ae6620d2c47afb854655c391779d9,代码仓编号 3112935,分支是 main。
● 计分以这个仓库的提交为准。main 上 154 次提交,8 名成员都有提交。次数和每人清单在第 3 章。
● 文件链接都带 ref=main。表结构、部署配置和页面代码以这个分支上的当前文件为准,不以本机未推送的改动为准。
和部署直接相关的三份文件:
| 文件 | 作用 |
|---|---|
| compose.prod.yaml | 数据库、迁移、应用、Caddy 四个容器 |
| deploy/Caddyfile | 对外的 HTTPS 反代到应用 |
| README.md | 「部署」一节写启动步骤 |
(2)在线部署
● 对外地址是 https://zhihui.worldboot.net/。浏览器里走 HTTPS,域名写在服务器的 SITE_ADDRESS 里。
● 2026-11-04 23:37(UTC+8)请求 /api/health,HTTP 200,正文 {"status":"ok"},响应头有 Via: 1.1 Caddy。同一时刻首页上的场次是分论坛 170、大会论坛 3、专题论坛 167。
● 参会者能走完的路径在这个站点上:首页、分论坛目录、推荐、注册和报名。截图在第 4 章,部署过程在第 16 章。
(1)打开首页就能看到会议和四天场次,不必先登录。
(2)分论坛目录和推荐页由反代后面的应用从数据库读出,不是静态占位页。
(3)主席看板、秘书名单、发布通知和修改论坛时间的成功画面截自本机同一套界面。线上演示口令登不进这几个账号,第 4 章开头写了这个范围。
2. 组员职责分工
分工怎么划
知会按业务边界划分,一条开发部分只有一个负责人。页面能看的论坛信息、报名怎么写入、通知发给谁、人数怎么数、日程怎么判冲突,各自归不同的人,避免同一条规则写出两套。
李润君负责单元测试,陈林祺负责端到端验收,两人都不改业务计算。袁湘湘负责资源包和设计说明,不改接口字段。高娅婷负责数据库容器和部署配置,不改报名和通知规则。
实现顺序按依赖来,不按人头把整个模块一次做完:
- 先有可运行的数据库和容器,迁移和页面才有地方跑。
- 工程骨架和设计说明定下目录、权限和需求解释,账号与登录接在这套约定上。
- 表结构稳定之后,再做论坛浏览、报名、通知和人数统计。
- 功能完成后补对应的单元测试和端到端验收。推荐、修改论坛时间和日程也是先有实现,再有测试。
- 生产部署配置放在报名、通知和导出都可用之后,最后做推荐验收和健康检查。
模块归谁
| 模块 | 目录 | 负责人 |
|---|---|---|
| 环境、依赖检查、生产部署 | 容器配置、锁文件、构建脚本 | 高娅婷 |
| 工程骨架与首页 | 应用入口和首页 | 饶恩哲 |
| 表结构与迁移 | drizzle、src/lib/db | 饶恩哲 |
| 论坛浏览与官网数据导入 | src/lib/forums、src/app/forums、src/app/agenda、data/cncc2026 | 饶恩哲 |
| 智能推荐 | src/lib/recommend、src/app/recommend | 饶恩哲 |
| 设计说明与 AI Coding 记录 | docs | 袁湘湘 |
| 五种语言的文案 | messages、src/i18n | 袁湘湘 |
| 会话、密码、登录限流 | src/lib/auth | 许雅欣 |
| 登录、注册、报名写入、个人报名和回执 | src/app/login、src/app/register、src/lib/registrations、src/app/me、src/app/receipt | 许雅欣 |
| 通知发布与实时推送 | src/lib/notices、src/app/notices | 饶慧 |
| 修改论坛时间 | src/app/forums/[id]/time | 饶慧 |
| 人数统计与主席看板 | src/lib/stats、src/app/dashboard | 叶银珍 |
| 参会者名单与导出 | src/app/attendees | 叶银珍 |
| 个人日程与冲突 | src/lib/schedule、src/app/schedule | 叶银珍 |
| 演示账号与模拟参会数据 | scripts/seed-demo.ts | 叶银珍 |
| 单元测试 | 登录、报名、通知、推荐、日程、统计的测试 | 李润君 |
| 端到端验收与健康检查 | 登录、报名、通知、改时间、导出、推荐 | 陈林祺 |
分工总表
每人一行,只列负责范围。
| 学号 | 姓名 | 角色 | 负责的部分 |
|---|---|---|---|
| 072403123 | 饶恩哲 | 工程与论坛主路径、智能推荐 | 工程骨架与首页,表结构与迁移,论坛浏览,官网议程导入,智能推荐 |
| 102400307 | 许雅欣 | 账号与报名 | 会话与登录限流,登录和注册页面,报名写入,个人报名与回执,分步引导 |
| 072403324 | 饶慧 | 通知与论坛时间 | 通知发布,通知列表,修改论坛时间,实时推送 |
| 052406103 | 叶银珍 | 人数、名单与日程 | 人数统计,主席看板,参会者导出,个人日程与冲突,演示数据 |
| 102400202 | 袁湘湘 | 多语言与设计文档 | 设计说明,五种语言资源包,AI Coding 记录 |
| 102400103 | 李润君 | 单元测试 | 登录与权限,报名,通知与推荐校验,日程冲突与统计 |
| 102400201 | 陈林祺 | 端到端验收 | 登录,报名主路径,通知、改时间与导出,推荐与健康检查 |
| 102400109 | 高娅婷 | 环境与部署 | 本地数据库与容器,依赖检查,生产部署,构建脚本 |
饶恩哲(072403123)
论坛页只做查询和展示。报名写入、通知和人数统计不在这个目录里。
| 交给别人 | 留在这里 |
|---|---|
| 报名写入 → 许雅欣 | 工程骨架、首页 |
| 通知发给谁 → 饶慧 | 表结构、迁移、官网导入 |
| 人数怎么统计 → 叶银珍 | 论坛列表、详情、议程 |
| 智能推荐 |
导入可以反复执行。会议主席改过的论坛时间再次导入时跳过。27 场没有主席、49 场没有主题,按原始数据显示,不补造。
推荐是一次调用,不用多步 agent。名称、时间和地点只用数据库里的。
| 模型返回 | 处理 |
|---|---|
| 编号不在论坛名单里 | 丢掉 |
| 理由在论坛文本里对不上 | 丢掉 |
| 超时、调用失败或 JSON 损坏 | 重试一次,再换一家;仍失败则用本地关键词匹配 |
| 正常结果 | 写入推荐记录;密钥只留在服务端 |
许雅欣(102400307)
参会者第一次报名走三步:选论坛 → 创建账号 → 查看回执。注册页旁边能看到即将报名的论坛。
账号守这几条:
- 公开注册只能成为参会者。主席和秘书由管理脚本创建,不能靠表单传角色。
- 密码不明文保存。Cookie 只带随机令牌,库里存哈希。
- 退出或角色变更后,旧令牌立即失效。连续登录失败会限流。
报名守这几条:
- 至少选一场。登录后可以查看、修改、取消。
- 取消后可以再报同一场,但同一论坛不能同时有两条有效报名。
- 全部取消后,不再计入大会人数。
- 非参会者直接调用接口也要被拒绝,不能只把按钮藏起来。
饶慧(072403324)
通知不按人复制一份。能看到什么,取决于现在还有没有这场的有效报名:新报名的人看得到历史通知,取消后不再收到。
| 情况 | 处理 |
|---|---|
| 主席或秘书给未绑定的论坛发通知 | 拒绝 |
| 正文 | 按纯文本显示,不按 HTML 渲染 |
| 标记已读 | 只能标自己有权看到的 |
| 在线推送 | 只推给当前有效报名的用户;有心跳,断开就清订阅 |
| 结束时间不晚于开始 | 拒绝改时间 |
| 新旧时间相同 | 不写记录,不发通知 |
| 会议主席改时间成功 | 同一事务里更新论坛、留下历史、通知当时已报名的人 |
页面上的时间按北京时间解析。冲突提不提示不在这里保存,日程按新的起止时间再算。
叶银珍(052406103)
人数每次从有效报名现算,不另存一份。
| 谁在看 | 看到什么 |
|---|---|
| 会议主席 | 全部论坛的人数。大会人数 = 至少报名一场的用户数 |
| 分论坛主席 | 只看绑定论坛。分论坛人数 = 报名了该论坛的用户数 |
| 秘书 | 全部参会者,并可导出 |
| 参会者 | 自己的日程,以及和已选论坛的时间冲突 |
导出用 UTF-8。单元格若以 =、+、-、@ 开头,先转义,避免被表格软件当成公式。
冲突用半开区间 [开始, 结束):
- 13:30–17:30 和 15:00–18:00:重叠,提示。
- 13:30–17:30 和 17:30–19:00:首尾相接,不提示。
- 只提示,不拦住报名。两场都计入人数,用户自己选一场放进日程。
- 新增、取消或改「去这场」之后重算。论坛时间被改掉后,打开日程即按新时间再检查。
演示数据约 200 名模拟参会者,报名时间散在最近几天。脚本再跑一遍不会重复建人。演示账号不用官网主席的真实姓名。
袁湘湘(102400202)
题目没写死的地方先定口径,再交给对应的人实现。
| 口径 | 定下来的做法 | 实现的人 |
|---|---|---|
| 注册通过 | 不做人工审核 | 许雅欣 |
| 时间重叠 | 只提示,不拦截 | 叶银珍 |
| 一场结束时另一场开始 | 不算冲突 | 叶银珍 |
| 秘书 | 能看全部参会者,只能给绑定的论坛发通知 | 叶银珍、饶慧 |
文案在资源包里,页面用统一接口按当前语言读取。五种语言是同一套条目:
- 简体中文
- 繁体中文
- 英语
- 日语
- 法语
再加一种语言,补资源包即可。通知正文只存一份中文,因为收件人的界面语言各不相同。AI Coding 记录按条留下 Prompt、方案、改或留的理由、验证方式。
李润君(102400103)
不另写业务实现。用例失败时,先分清是断言错了还是功能错了。
| 规则 | 用例要看到的结果 |
|---|---|
| 四种角色越权 | 请求被拒绝 |
| 退出登录 | 旧令牌不能再用 |
| 同一论坛报名两次 | 不会留下两条有效记录 |
| 报名开幕式 | 被拒绝 |
| 模型返回了不存在的论坛 | 结果里没有这场 |
| 推荐理由对不上论坛文本 | 这条被丢掉 |
| 模型接口失败 | 退回本地匹配;用假客户端测,不依赖真实密钥 |
| 两场时间重叠 | 判为冲突 |
| 一场结束、另一场开始 | 不判为冲突 |
| 大会人数、分论坛人数 | 与看板口径一致 |
陈林祺(102400201)
在浏览器里走通路径。对不上就退回模块负责人,不把断言改松。
- 四种角色只能进自己有权的页面;无权限是拒绝,不是空白页。
- 注册、改选、取消能走完,并出现回执。
- 已报名的参会者能看到主席或秘书发出的通知。
- 会议主席把时间改成重叠后,日程出现冲突提示。
- 秘书能导出名单。
- 推荐出的论坛能打开详情,并能直接报名。
- 健康检查能判断部署起来的服务是否可用。
高娅婷(102400109)
不改报名、通知和推荐的规则。本地按这个顺序就能打开网站:
- 启动数据库容器
- 执行迁移
- 导入议程
- 打开网站
依赖版本和检查命令留在仓库里,换一台机器不用先在某人电脑上补装工具。生产环境重复部署时,迁移只跑新增部分,导入不覆盖已经改过的论坛时间。服务起来后,用健康检查确认进程和数据库都已就绪。
3. CodeArts 提交日志与 commit 次数
统计以 CodeArts 仓库 知会 main 的提交记录为准。main 上共 154 次提交,8 名成员都有提交
提交日志截图
仓库统计写明提交人数 8、总提交 154。提交页按日期分成两段:11 月 4 日 7 次,11 月 3 日 147 次。下图是提交页顶部。11 月 4 日这 7 次的内容是表单提示:错误改由页面自己显示、登录页带到注册页时保留已选论坛、英法语数量用复数。

往下是 11 月 3 日的 147 次。这一屏能看到合并请求、文档、测试和功能提交混在同一天,作者名显示在每条下面。例如袁湘湘的注册引导文档,叶银珍的冲突选择、取消报名前提示、登录前已选论坛不冲掉已有报名。

154 次里有 7 次是合并提交,其余 147 次是功能、测试和文档。合并提交各算在提交人名下,与页面上的作者一致。
各成员 commit 次数
每人都不低于 3 次。下表是提交作者名下的次数,合并提交计算在内。154 = 147 次改动 + 7 次合并。
| 姓名 | 提交次数 | 其中合并 |
|---|---|---|
| 袁湘湘 | 20 | 1 |
| 李润君 | 20 | 1 |
| 许雅欣 | 19 | 0 |
| 饶恩哲 | 19 | 1 |
| 饶慧 | 19 | 1 |
| 叶银珍 | 19 | 1 |
| 陈林祺 | 19 | 1 |
| 高娅婷 | 19 | 1 |
| 合计 | 154 | 7 |
下面按提交作者列出。代码和测试链到 main 上的当前文件,提交链到该次提交详情。截图是提交页顶部,完整 154 次以各人表中的链接为准。
饶恩哲
工程骨架、本地数据库、生产镜像和部署说明。健康检查也在这里。共 19 次,其中合并 1 次。
这 19 次把空目录做成能在本地启动、也能按生产配置打开的项目。后面的页面和测试都跑在这套环境和镜像上。
代码:
- Dockerfile
- compose.yaml
- compose.prod.yaml
- src/app/api/health/route.ts
- docs/architecture.md
- docs/parallel-tasks.md
测试:这一部分没有单独的业务单测,部署是否起来用健康检查接口确认。提交 说明 3daa9d22 合并小修分支 ca858645 更正合并审查记录里关于数据库隔离的说法 544df5ab 说明环境变量里的数据库地址会盖过配置文件 15299025 为并行任务补上分支名和提示词 c95edb9a 补上后续开发要用的项目说明 511d1ad8 补上架构概览和当前进度 d6622c14 修好工作区脚本摘要被相邻文字截断的问题 191a4637 写入界面决定、项目约定和并行任务说明 016328a1 让每个工作区使用独立端口和数据库 14587c85 记下需求决定和模块划分 5b40ee80 重写说明,补上启动和部署步骤 e5740f15 加上带 Caddy 的生产部署配置 f7f324e9 加上多阶段生产镜像 e4061d06 加上健康检查接口 536f0795 用容器提供本地数据库 ed88c301 启动时校验环境变量 19b8f82b 加入数据库、登录、多语言和测试依赖 40a8ffc2 改成独立输出,并去掉脚手架自带资源 8648e4c7 用脚手架建立项目
许雅欣
登录、会话、密码、角色权限和登录页。失败后保留邮箱、跳转限制在站内。共 19 次,没有合并提交。
密码不保存明文,会话放在数据库里。四种角色能进哪些页面,由服务端守卫决定,页面上藏起入口不算数。
代码:
- src/lib/auth/password.ts
- src/lib/auth/session.ts
- src/lib/auth/permissions.ts
- src/lib/auth/actions.ts
- src/app/login/page.tsx
测试:登录流程和四种角色的访问矩阵。 - src/lib/auth/password.test.ts
- src/lib/auth/permissions.test.ts
- src/lib/auth/login-throttle.test.ts
- e2e/auth.spec.ts
提交 说明 74e137a0 表单错误改由页面按当前语言显示,不用浏览器气泡 18dc2712 抽出共用的密码输入框和字段错误 afc4fde2 提交前先检查注册和登录字段 88c59504 去掉登录页上重复的文字 e14b4932 在说明里补上演示账号和端到端测试命令 df7a34a4 记下登录相关的开发和排错 33a7a020 写明角色权限和为什么用数据库会话 4c92da61 用端到端测试覆盖登录和四种角色的访问范围 a3574543 登录失败后保留已填写的邮箱 6351e09e 加上各角色都能打开的账号页 1fecef19 页头菜单按角色显示 ffd9d87e 加上登录页以及登录、退出 83e2ecb3 加上路由守卫,无权限时返回真实的拒绝页 3ccfc0e9 补上登录和角色页面的五种语言文案 de1b0057 登录会话存进数据库 ddbde020 连续登录失败时限流 57ffa363 登录后的跳转只允许站内地址 d5315f86 把角色权限集中到一处 32a42093 用单向散列保存密码
饶慧
论坛目录、详情、议程、界面壳和多语言读取。从详情返回时恢复筛选。共 19 次,其中合并 1 次。
目录要在一百七十场里筛选、按天查看,再回到刚才选中的论坛。文案不写在页面里,五种语言都从资源包读取。
代码:
- src/lib/forums/queries.ts
- src/lib/forums/filter.ts
- src/app/forums/page.tsx
- src/app/agenda/page.tsx
- src/i18n/request.ts
- messages/zh-CN.json
测试:论坛页、注册流和各角色路由的端到端;英法语数量复数是后来的修正。 - src/lib/forums/filter.test.ts
- src/i18n/messages.test.ts
- e2e/pages.spec.ts
提交 说明 0d645e80 英语和法语的数量改用复数 f51b123f 加上克制的按钮动效 fd423a60 合并后续修正 f1023a5e 修好论坛列表的浏览器返回 d785a1b4 用端到端测试覆盖论坛页、注册和各角色路由 4360f167 调整论坛详情和登录表单的比例 ca042576 返回论坛目录时从地址恢复筛选和已选论坛 b27c1991 看起来像按钮的导航改成真正的链接 fa123991 加上会议议程页 2087040d 加上分论坛详情页 53bfa7c0 加上提交服务端操作的共用按钮 ee5abb63 加上带筛选和按天计划的论坛目录 956c6673 加上论坛的查询 7d970cf6 论坛列表可以筛选并按天分组 b9775f0f 套上账本风格的页面外壳 76f2aa42 加入表单、卡片和表格组件 a7840784 界面组件改用能显示中文的系统字体 c63f0596 加上语言切换和共用页面布局 07ee6f74 界面文字从各语言资源包读取
叶银珍
注册报名、个人日程和冲突判断。冲突只在已经放进日程的论坛之间计算。共 19 次,其中合并 1 次。
注册至少选一场,取消后可以再报,同一论坛不能同时留下两条有效报名。一场正好在另一场结束时开始,不算冲突。
代码:
- src/lib/registrations/actions.ts
- src/lib/schedule/conflicts.ts
- src/app/register/page.tsx
- src/app/schedule/page.tsx
测试:报名端到端,以及半开区间冲突。 - src/lib/registrations/input.test.ts
- src/lib/schedule/conflicts.test.ts
- e2e/registration.spec.ts
- e2e/guided-registration.spec.ts
提交 说明 4e2ba506 导航上显示尚未处理的时间冲突 ce6b5d29 取消报名前先说明后果 977deb0b 登录前选的论坛并入已有报名,不取消原来的 a641425c 冲突的几场可以一步选出要去的那场 9c67bb35 互相重叠的论坛算作一处冲突 476952b6 论坛时间正在修改时,报名仍可进行 80cf202f 按地址限制注册频率 4aecd8c0 合并报名功能 c63ada3d 写清混合并发用例实际覆盖了什么 b9474448 去掉客户端刷新已经覆盖的缓存更新 81efd8d9 用端到端测试覆盖注册和报名 1986d664 冲突只在已经放进日程的论坛之间计算 b97d438c 实现参会者注册 9550e54e 实现报名、取消和放入日程 1f76dac0 校验注册和报名的输入 31de6768 加上个人日程页 0ce54a36 账号页显示已报名的论坛 a0e63d82 注册页旁边显示将要报名的论坛 eb480230 用半开区间判断日程是否冲突
袁湘湘
设计说明、AI Coding 记录、测试配置,以及跨任务和报名引导的端到端。共 20 次,其中合并 1 次。
端到端测试改在生产构建上跑,并且串行执行,避免冷启动把结果弄得不稳定。关掉脚本之后,不合格的输入仍然要能到达服务端校验。
代码:
- docs/design.md
- docs/ai-coding/README.md
- vitest.config.mts
测试:生产构建上的端到端,关掉脚本后仍能到达服务端校验。 - e2e/cross-task.spec.ts
- e2e/guided-registration.spec.ts
- e2e/forum-history.spec.ts
提交 说明 36162f81 记下表单提示的决定 9069a366 关掉脚本后仍能测到服务端校验和页面提示 c237fa94 从登录去注册时保留已选论坛 4d7782b7 合并报名引导 5f2b14e4 记下报名引导的开发过程 b1504717 记下报名引导的设计决定 0b7f03f6 覆盖登录后再选论坛,以及处理冲突 5f579c1a 原有端到端测试改走引导流程 232817d6 第一次报名显示步骤和回执 acb118b6 五种语言都改写报名文案 8128e494 记下并行任务和合并审查 8101759e 全部任务合并后更新进度和决定 c6ab7442 覆盖跨多个任务的流程 e037dc26 端到端测试改为串行执行 4eeacd6c 记下设计复查和端到端排错 b5a30372 端到端测试改在生产构建上跑 9386b8b1 去掉用不到的检查指令 678272d4 用真实案例开始写 AI Coding 记录 2cd51a95 测试配置改为模块格式,去掉加载警告 cc1eb400 测试里也能用项目的路径别名
李润君
表结构、迁移、官网数据导入、人数看板、参会者名单和 CSV 导出、模拟参会者。共 20 次,其中合并 1 次。
空库迁移要能建出论坛、报名和通知。论坛目录可以反复导入,已经改过的时间不能被盖回去。秘书能导出名单,单元格里像公式的内容要先转义。
代码:
- src/lib/db/schema.ts
- drizzle/0000_init.sql
- src/lib/stats/queries.ts
- src/lib/stats/csv.ts
- src/app/dashboard/page.tsx
- src/app/attendees/page.tsx
测试:CSV 公式注入和重复导入演示数据。 - src/lib/stats/csv.test.ts
- src/lib/stats/demo-data.test.ts
提交 说明 d3440055 合并改时间、导出和演示数据 83f91b9f 导入约 200 名模拟参会者,重复执行不重复建人 6a721345 生成模拟参会者和报名记录 009fb5c3 导出链接跟随当前的论坛筛选 7a647402 参会者可以导出,也可以只导出一场论坛 a804ea12 导出时转义可能被表格软件当成公式的单元格 9fe08630 整理参会者相关的文案编号 793af476 参会者名单可以按论坛筛选并导出 e210232a 看板上显示参会人数 095c466b 首页改成展示会议统计 3f015e46 加上报名、通知和人数的查询 f0c26f25 生产环境也可以导入演示账号 494dcf78 参会者名单只给秘书看 2f703490 看板只给会议主席和分论坛主席看 2d4230f1 四种角色各有一个可重复导入的演示账号 9e675c68 首页显示实时的论坛数量 f0c0a59e 论坛目录可以重复导入且不覆盖已改时间 a1cad4ce 放入 CNCC 2026 论坛数据包 55433a8b 加上第一版数据库迁移 c51fcb87 定义论坛、报名和通知的表结构
陈林祺
通知发布、实时推送,以及会议主席修改论坛时间。共 19 次,其中合并 1 次。
只有绑定该论坛的主席和秘书能发通知。已报名的人不用刷新就能收到;取消之后不再推送。改时间按北京时间解析,改完通知当时已经报名的人。
代码:
- src/lib/notices/actions.ts
- src/lib/notices/sse.ts
- src/app/notices/page.tsx
- src/lib/forums/actions.ts
- src/app/forums/[id]/time/page.tsx
测试:推送权限、SSE 响应头、改时间和导出。 - src/lib/notices/sse.test.ts
- src/lib/notices/bus.test.ts
- src/lib/forums/time.test.ts
- e2e/notices.spec.ts
- e2e/time-export.spec.ts
提交 说明 f8e0e54d 改时间失败时保留已填时间,并指出时间没有变化 5a94cd7f 论坛时间改过之后,提示报告时间可能没有同步 2d2c10ce 合并通知功能 9d2d6042 覆盖隐藏标签页的补推,并去掉已读权限测试里的竞争 3822d3fa 更正实时推送响应头的说明 0b01acc7 用端到端测试覆盖改论坛时间和导出名单 bef3c1e4 会议主席可以修改论坛时间 67e11aba 按北京时间解析输入,并生成时间变更通知 d0645428 用端到端测试覆盖实时送达、权限和推送响应头 3b639385 发布失败时保留已写的通知草稿 48e5bc1a 导航上显示未读通知数 c557a086 把新通知实时推给已报名的参会者 70d8af6e 为长连接校验登录会话 fdfa2be1 发布和标已读都在服务端检查权限 01ddb5d7 实时推送带心跳,断开时清理订阅 c8c5174c 进程内用一条通知总线转发新通知 78bc1678 加上修改论坛时间的页面 8262fbcf 加上发布通知的页面 fd7f8960 加上通知收件箱
高娅婷
智能推荐:提示词、模型调用、结果校验、失败后换服务商和本地关键词降级。共 19 次,其中合并 1 次。
模型返回的论坛必须在现有名单里,对不上的丢掉。接口失败先重试再换一家,全部失败就用本地关键词匹配。页面测试不调用真实模型。
代码:
- src/lib/recommend/prompt.ts
- src/lib/recommend/client.ts
- src/lib/recommend/validate.ts
- src/lib/recommend/fallback.ts
- src/lib/recommend/run.ts
- src/app/recommend/page.tsx
测试:不连接真实模型也能测校验和降级;页面端到端同样不调用真实接口。 - src/lib/recommend/validate.test.ts
- src/lib/recommend/fallback.test.ts
- src/lib/recommend/run.test.ts
- e2e/recommend.spec.ts
提交 说明 ef32beac 去掉未使用的未实现结果 1346236c 空字符在进数据库之前就拒绝 bdc0560f 请求限流和来源地址查找改成共用 0864fb21 合并智能推荐 3c7fbe0c 推荐页的端到端测试不调用真实模型 7cd46114 加上用真实服务商评估推荐的脚本 82795463 把推荐入口接到完整流程 3b001846 推荐失败时重试、换服务商,再退回本地匹配 cf0301f7 限制推荐的调用频率 1645d6d4 模型都失败时用本地关键词匹配 f6926817 接入三家模型,调用方式统一 daf0aa1a 按论坛名单校验模型返回的结果 93520509 压缩论坛目录并生成推荐提示 e4b2d637 默认使用 DeepSeek 的当前模型 d5a8b5af 加入另外两家模型的调用库 927538c1 补上共用错误文案和通知事件接口 493e5f74 加上推荐页 88dffb31 为尚未实现的功能定好接口 caa09641 默认用 DeepSeek,开发服务改到 3100 端口
4. 程序运行截图
这一节只证明主流程能走通。每个流程放一两张图。页面上有什么、用户怎么操作,写在第 10 节。两边用的是同一批图片。
4.1 浏览
未登录可以打开首页和会议议程。


4.2 语言
界面有简体中文、繁体中文、英语、日语、法语。下面是英语和法语的首页。


4.3 推荐
输入研究方向之后,系统返回可以打开、可以报名的论坛。


4.4 注册和报名
注册提交后直接进入回执。个人中心可以修改报名,也可以按场次取消。


4.5 日程冲突
同一时段报了两场时,日程要求选一场。选定之后冲突提示消失,另一场报名仍保留。


4.6 人数、通知和权限
参会者直接打开人数看板会被拒绝。会议主席在本机能看到全会人数。分论坛主席发布通知后,右侧出现已发布记录;报名了该论坛的参会者在通知页看到未读。




5. 遇到的困难及解决方法
每人两件自己提交里解决过的事。现象只写看到的结果,判断写排除了哪种解释、依据是什么、所以改哪里。提交都是本人的。代码用华为云上 main 的文件链接。截图用编辑器和测试窗口的实拍。代码图是后台另开的编辑器窗口实拍,每人两个问题各一张。有对应单元测试的,测试输出图紧跟在代码图后面。
饶恩哲
问题 1 报告的数据库和实际连上的不是同一个
现象
合并审查时,克隆目录的环境文件指向隔离库 zhihui_wt9,说明里也写成这几轮跑在隔离库。当时的 shell 已经导出了主库地址。配置加载不会覆盖已经存在的环境变量,种子写进了主库。再导入演示数据时,创建人数是 0。这个数字对一个空库来说不合理。
判断
| 看到的 | 能证明什么 | 不能证明什么 |
|---|---|---|
环境文件写着 zhihui_wt9 | 文件是这么写的 | 这一次种子连的就是这个库 |
| 创建人数是 0 | 目标库里这些论坛已经在,插入被跳过 | 隔离库没建好。库不存在会报错退出,不会安静地插入 0 行 |
| 进程里已经导出主库地址 | 加载时不再用文件盖回去,种子进了主库 | — |
不覆盖已有环境变量是对的。错在审查记录把文件内容当成了实际连接。记录不改,下一次合并还会按错误的库去对人数。
改法
说明改成:已经导出的数据库地址会盖过配置文件。核对改为读进程中的连接串。主库里的测试账号和通知清掉之后,217 条论坛和全新导入一致,主席改过的时间没有被盖掉。

提交 544df5ab,作者饶恩哲。更正审查记录的是 ca858645,作者也是饶恩哲。
读取规则在 src/lib/env.ts。
问题 2 工作区脚本的摘要被旁边的文字截断
现象
并行开发时每个工作区要有自己的端口和数据库。准备脚本打出的摘要贴在说明文字旁边时,摘要被截断,下一个人照着抄会抄错端口。
判断
(1)脚本是成功的:库能连上,端口也分配了。
(2)坏在贴进说明时,摘要和旁边的句子粘成一行,后半段端口被截掉。照着抄,抄到的是残缺数字。
(3)理解成「库没建起来」就会去重跑初始化。库已经在,重跑解决不了抄错端口。两个工作区用了同一个端口或库名,报名和通知会写进对方的数据。
所以改输出,不改建库。摘要单独成行,端口和库名都在这一行里。
改法
摘要单独成行,不再和旁边的句子连在一起。每个工作区仍使用自己的端口和数据库,互不覆盖。

提交 d6622c14,作者饶恩哲。独立端口和数据库是 016328a1,作者饶恩哲。
约定写在 docs/parallel-tasks.md。
许雅欣
问题 1 登录失败后邮箱被清空
现象
限流的端到端测试第一次失败,是因为页面上有两个提示区域:一条是邮箱或密码错误,另一条是框架自己的路由播报,选择器同时点到了两处。选择器改完以后仍失败。输错密码再看表单,邮箱已经被清空。
判断
| 第几次失败 | 当时看到的 | 归到哪里 |
|---|---|---|
| 第一次 | 选择器同时点到登录错误和路由播报 | 测试选错了元素。这一步先不改表单 |
| 第二次 | 选择器收窄后仍失败,邮箱框是空的 | 页面在动作结束后重置了表单 |
- 密码清空:失败多半是密码错了,留着没有帮助。
- 邮箱填回:这是用户刚输入的账号,清空就要整段重填。
- 文案不拆开:拆成「邮箱不存在」或「密码错误」,别人可以用失败来探测哪个邮箱已注册。提示放在页面上,浏览器气泡的语言和当前界面对不上。
改法
选择器只取登录错误。邮箱放进返回状态,失败后填回输入框。错误显示在页面上,不用浏览器自带气泡,避免气泡语言和页面不一致。整句只写「邮箱或密码错误」,不指出是哪一项。


提交 a3574543,作者许雅欣。页面上显示错误是 74e137a0,作者许雅欣。
表单动作在 src/lib/auth/actions.ts。字段检查在 src/lib/auth/form-checks.ts。
问题 2 无权限时只有一张空白页
现象
四种角色登录之后,没有权限的页面有的直接空白,看不出是被拒绝还是页面没写完。测试如果只判断「页面上没有数据」,空白和拒绝分不清。
判断
只看「页面上没有名单」,页面没做完和请求被拒绝都会通过。菜单里藏起入口,也挡不住地址栏直接打开。
- 403:这一角色被拒绝。
- 200 的空壳:页面还在,只是没有数据。两种结果不能都算通过。
- 四种角色分开断言。「不是参会者就拒绝」盖不住秘书、主席和工作人员的差别。
next只允许站内路径。原样跳转会把人带到站外,外部地址退回站内默认页。
改法
每个页面和接口都走同一套权限判断。无权限返回拒绝页。端到端断言真实的 403,四种角色的访问范围分开写。登录之后的跳转只允许站内地址,避免 next 参数把人带到外部网站。


提交 83e2ecb3,作者许雅欣。访问矩阵的端到端是 4c92da61,作者许雅欣。
守卫在 src/lib/auth/guards.ts。权限表在 src/lib/auth/permissions.ts。
饶慧
问题 1 从详情返回,地址变了列表不变
现象
在论坛详情按浏览器返回,地址栏已经回到目录,列表仍停在详情。进入详情之前的筛选和已选论坛也不见了。
判断
- 地址已经是目录:浏览器历史退回去了,路由没有算错。先改路由解决不了。
- 列表仍是详情:组件拿的是进详情时留下的状态,筛选和已选论坛跟着丢了。刷新一次又正常,容易被当成偶发。
- 断言如果只查地址里有目录,这个错会被盖住。要同时看地址、列表和进详情之前的筛选。
- 看起来像按钮、实际在导航的元素改成链接。读屏只当它是按钮时,返回路径和键盘预期都不一样。
改法
返回之后按当前地址恢复目录、筛选和已选论坛。同一条路径再走一遍,地址和列表一起回到目录,筛选还在。看起来像按钮的导航改成真正的链接,屏幕阅读器能读出这是链接。

提交 f1023a5e,作者饶慧。从地址恢复筛选是 ca042576,作者饶慧。
目录组件在 src/components/forums/forum-browser.tsx。
问题 2 英语和法语的人数仍是单数
现象
界面文字已经放进资源包,五种语言能切换。英语和法语里,论坛数量和报名人数仍用单数,和实际数字对不上。
判断
| 语言 | 切换 | 数量句子 |
|---|---|---|
| 中文、繁体中文、日语 | 能切换 | 没有单复数形态 |
| 英语、法语 | 能切换 | 仍是单数,数字变了句子没变 |
五种语言都能切换,所以不是文案没加载。单复数放进资源包的不同条目。组件里按语言写分支,每加一种语言都要改页面。页面只传句子编号和数字。
改法
英语和法语的数量条目改成复数形式。页面仍按句子编号和当前语言读取。再加一种语言,只补资源包。


提交 0d645e80,作者饶慧。资源包读取方式是 07ee6f74,作者饶慧。
英文文案在 messages/en.json。法文文案在 messages/fr.json。
叶银珍
问题 1 一场结束、另一场开始,算不算冲突
现象
167 场专题论坛集中在四个下午。同一下午任选两场,时间重叠。数据包里没有首尾相接的两场,这个边界要靠改时间才能看到。三场挤在同一时段时,如果按两两一对提示,用户会看到三对,不知道留哪一场。
判断
| 情况 | 怎么算 | 为什么 |
|---|---|---|
| 一场结束时刻等于下一场开始 | 不算冲突 | 左闭右开,结束时刻已经不属于这一场。算成冲突,紧接着的下一场也不能报 |
| 三场挤在同一时段 | 一处选择,不是三对 | 两两成对会得到三对提示,看不出留哪一场 |
| 冲突要不要入库 | 不入库,下次打开按当前时间重算 | 主席改了时间,旧冲突行还在,除非再写同步。报名只表示报了哪几场 |
数据包里没有首尾相接的两场,这个边界要改一场的结束时间才能看到。两场都留下,人数两边都计。冲突只是提示,不替用户取消一场。
改法
重叠写成一场的开始早于另一场的结束,并且反过来也成立。区间用左闭右开。同一时段收成一组,用户一步选出要去的那场。两场报名都还在,都计入人数。下次打开日程按数据库里的当前时间重算。


提交 eb480230,作者叶银珍。一步选出要去的那场是 a641425c,作者叶银珍。
计算在 src/lib/schedule/conflicts.ts。
问题 2 登录前选的论坛把原来的报名取消了
现象
没登录时先勾了几场,登录之后如果用整份已选列表去保存,这个账号以前报过的论坛会被取消。用户只是想再加一场。
判断
(1)登录前的勾选还没有这个账号,名单里只有这一次点过的论坛。整份覆盖会取消以前报过、这次没再勾的场次。用户没有点取消。
(2)这次选择是追加,不是替换。登录跳转不是确认,写入要等用户确认之后。全部取消留在个人页面,由用户明确做。
(3)取消后再报同一场要能成功,但不能同时留下两条有效报名。取消只标上时间,不删掉原来的行。
改法
新选的论坛追加到已有报名上。已经报过的不取消。全部取消仍由用户在个人页面自己做。取消后可以再报同一场,同一论坛不会同时留下两条有效报名。


提交 977deb0b,作者叶银珍。报名写入是 9550e54e。
写入在 src/lib/registrations/actions.ts。
袁湘湘
问题 1 几个任务一起跑,人数和未读对不上
现象
四个任务各自的端到端都通过。合在同一次运行里,大会人数是 205,用例期望 208。另一条用例期望未读数是 1,实际是 3。AI 把人数差解释成统计漏了 3 人,把未读差解释成计算公式错了。
判断
| 分开跑 | 合在同一次 | |
|---|---|---|
| 大会人数 | 通过 | 205,期望 208 |
| 未读 | 通过 | 3,期望 1 |
| 公式如果写错 | 单跑也会差 | 单跑是对的,所以先不改查询 |
AI 把差数解释成统计少了 3 人、未读公式写错。依据只有期望值和页面数字不相等,没有看这些行是哪条用例写入的。
查库能看到别的用例留下的报名和通知。多出来的行被算进去了,不是漏计。按那个解释去改查询,会把正确的计数改成迁就脏数据。要改的是用例:串行跑,未读那条先清掉上一条留下的通知。
改法
端到端改成串行,不再同时改同一批报名。未读用例先清掉上一条留下的通知,计算代码没有改。人数查询也没有改。失败的那一轮不拿来当统计有缺陷的证据。

提交 e037dc26,作者袁湘湘。跨任务用例是 c6ab7442,作者袁湘湘。
串行配置在 playwright.config.ts。
问题 2 限流测试每一轮的通过数不一样
现象
登录限流的端到端每一轮都冷启动开发服务器。通过的项数来回变,失败位置也不一样。如果把其中一轮写进记录,下一轮对不上。
判断
阈值如果写错,每一轮都会在同一次数失败。这几轮次数和锁定时间没改,失败位置却在漂移。更像开发服务器冷启动还没就绪。
- 这一轮先不写入记录。改到生产构建上,连续 3 轮通过数不变,才承认稳定。开发服务器上也能偶然过一轮。
- 页面上的检查可以被关掉脚本绕开。稳定之后仍要让不合格输入到达服务端。冷启动的失败不能反过来证明服务端已经拦住了这些输入。
改法
这条测试改到生产构建上跑。连续 3 轮通过数不变,才写入记录。关掉脚本之后,不合格的输入仍然要能到达服务端校验,不能只靠页面拦住。


提交 b5a30372,作者袁湘湘。关掉脚本仍测服务端的是 9069a366,作者袁湘湘。
取舍记在 docs/ai-coding/README.md。
李润君
问题 1 再次导入盖掉已经改过的时间
现象
论坛目录需要反复导入,换一台机器才能重建。如果导入用整表覆盖,会议主席改过的召开时间会被数据包里的旧时间写回去。
判断
- 整表覆盖最省事,但主席改时间发生在导入之后。再导一次,旧时间会把修改写回去。原因在导入,不在主席的保存。
- 人员和报告应用不会改,可以重建。论坛这一行会被改,已存在的行跳过,只插入还没有的场次。
- 27 场没有主席、49 场没有主题,是原始数据如此。导入时补造,会把「官网没有」写成「我们编了一个」。空着显示。
改法
已经存在的论坛行跳过。手动改过一场时间后再导入,修改还在。27 场没有主席、49 场没有主题的,仍按原始数据显示,不在导入时补造。

提交 f0c0a59e,作者李润君。第一版迁移是 55433a8b,作者李润君。
问题 2 导出的名单会被表格软件当成公式
现象
秘书导出参会者时,单元格如果以等号、加号、减号或 @ 开头,用表格软件打开后会被当成公式执行,名单内容变掉。
判断
| 缺陷 | 在哪一层 | 另一件解决不了它 |
|---|---|---|
等号、加号、减号、@ 开头被当成公式 | 生成文件时转义。文件离开系统之后,网页上的显示已经不在 | 不重复建人 |
| 大约 200 名模拟参会者被建成两行,看板人数变多 | 导入时按人不重复建 | 公式转义 |
网页里看这些格子是文本。中文姓名用 UTF-8,和公式转义一起交出去,但是两件不同的事。
改法
这些开头的单元格先转义,文件使用 UTF-8。可以导出全部,也可以只导出当前筛选的一场。重复导入大约 200 名模拟参会者时不会重复建人,看板上的人数才不会越导越多。


提交 a804ea12,作者李润君。不重复建人是 83f91b9f,作者李润君。
转义在 src/lib/stats/csv.ts。
陈林祺
问题 1 发布之后另一页收不到未读数
现象
主席发布通知后,已经打开通知页的参会者看不到未读变化。单进程里用模块内的事件能收到。Server Action 和接口被打成两份模块之后,发布写进一份,页面订的是另一份。
判断
(1)单进程能收到,发布、计算未读、页面订阅这条链是通的。拆成两份模块后收不到,差别只在订阅表是不是同一份。未读数如果算错,单进程也会错。
(2)谁能看见不能交给前端。推送里带上论坛名单,取消报名之后,已经打开的页面还能按旧名单收通知。可见范围在服务端查,推送里只放通知编号和未读数。
(3)改回模块内变量,再次收不到;改回共用的一处,点击发布大约 46 毫秒收到。这一来一回说明不是顺手改到了别的逻辑。连续断开 50 个连接,订阅数回到 0。
改法
推送里只放通知编号和未读数。订阅挂到进程共用的一处。改回模块内变量后再次收不到;改回来之后,点击发布大约 46 毫秒收到事件。连续断开 50 个连接,订阅数回到 0。


提交 c8c5174c,作者陈林祺。推送实现是 c557a086,作者陈林祺。
问题 2 注释里的 gzip 没有测过
现象
实时推送的注释写着压缩会把事件攒到连接结束。这句话没有测量。已读测试偶发超时,一度被看成按钮被产品弄丢了。
判断
| 说法 | 依据 | 结论 |
|---|---|---|
| 压缩会把事件攒到连接结束 | 没有测量,只是写的时候觉得说得通 | 看响应头和原始字节。事件流没有被压缩。禁止改写的头可以留,理由不能再写成压缩 |
| 已读按钮被产品弄丢了 | 超时那一次,连接是在取消报名之后才建立的 | 这个人已经不在报名里,按钮消失是对的。改测试的等待,不改按钮 |
改法
注释改成测量到的事实。响应头仍禁止中间层改写,理由不再写成压缩。测试改为等刷新完成再找按钮,没有去改按钮。


提交 3822d3fa,作者陈林祺。
响应头在 src/lib/notices/sse.ts。
高娅婷
问题 1 模型返回了库里没有的论坛
现象
推荐结果里出现过不存在的编号,也出现过一段在论坛名称、主题、简介和报告标题里都对不上的理由。超时或损坏的 JSON 会直接出现在页面上。本地兜底如果按通用分词来切,「机器人」会被切成「机器」,再命中「机器学习」。
判断
| 返回里的问题 | 怎么处理 |
|---|---|
| 编号不在这一次的名单里 | 丢掉这一条 |
| 编号是真的,理由是另一场的句子 | 理由必须能在该论坛的名称、主题、简介或报告标题里逐字找到,对不上的丢掉 |
| JSON 超时或损坏 | 不能画到页面上。先重试,再换一家,全部失败才走本地兜底 |
| 「机器人」被切成「机器」,再命中「机器学习」 | 不用通用分词。先按虚词断开,再用目录原文做最长匹配 |
再让模型多走一步去查库,查的仍是报名已经读过的那张表。推荐保持一次调用,校验放在调用之后。
15 条需求在 deepseek-flash 上跑了两遍,14/15 和 15/15。这只说明这两遍。另外两家没有密钥,不能写成已经调用过。
改法
编号不在名单里的丢掉,理由对不上论坛文本的丢掉。超时或 JSON 损坏先重试,再换一家,全部失败才用目录做最长匹配,并先按虚词断开。15 条需求在 deepseek-flash 上跑了两遍,分别是 14/15 和 15/15。没有密钥的另外两家不写成已经调用。


提交 daf0aa1a,作者高娅婷。本地匹配是 1645d6d4,作者高娅婷。
校验在 src/lib/recommend/validate.ts。本地匹配在 src/lib/recommend/fallback.ts。过程在 docs/ai-coding/README.md。
问题 2 未登录访问带空字符的地址返回 500
现象
未登录打开带空字符的注册地址,服务器返回 500。读代码时一度写成这种字符只有已登录的工作人员能触发。
判断
读代码时写成「只有已登录的工作人员能送到写入」。这是从调用链推出来的,没有打开未登录地址看过。实测未登录也返回 500。
- 不是权限问题,也不是推荐校验漏了编号。推荐那一层看的是模型返回的名单。
- 这里是字符在进数据库之前没有被拒绝,查询把空字符当成非法输入,变成了 500。用户应该看到拒绝。
- 说明里留着旧结论,下一次会按错误的角色去复现。记录改成实测。
改法
空字符在写入之前拒绝。未登录再打开这个地址,不再返回 500。说明里记下的是实测状态,没有留「只有工作人员能触发」。

提交 1346236c,作者高娅婷。
推荐流程仍在 src/lib/recommend/run.ts。空字符是在进数据库之前拒绝,和推荐校验是同一人的另一次提交。
6. 组员贡献比例
这次按选题时定下的考核方案算贡献系数。过程分只留作组内记录。计入本次作业的比例,按角色份额折成百分数。
五个维度
交付、规模、协作、规范、文档只打三档:达标 100,能用但有缺陷 60,打不开或对不上分工 0。60 分写明理由。
| 学号 | 姓名 | 交付 | 规模 | 协作 | 规范 | 文档 |
|---|---|---|---|---|---|---|
| 072403123 | 饶恩哲 | 100 | 100 | 100 | 100 | 60 |
| 102400307 | 许雅欣 | 100 | 100 | 100 | 100 | 100 |
| 072403324 | 饶慧 | 100 | 60 | 100 | 100 | 60 |
| 052406103 | 叶银珍 | 100 | 60 | 100 | 100 | 100 |
| 102400202 | 袁湘湘 | 100 | 60 | 60 | 100 | 100 |
| 102400103 | 李润君 | 100 | 60 | 100 | 100 | 60 |
| 102400201 | 陈林祺 | 100 | 60 | 100 | 100 | 60 |
| 102400109 | 高娅婷 | 100 | 60 | 60 | 100 | 60 |
记 60 的理由
一项一行。分数仍是 60。
| 姓名 | 哪一项 | 理由 |
|---|---|---|
| 饶恩哲 | 文档 | 骨架、表结构、论坛导入和推荐能核对;设计说明和五种语言文案归袁湘湘,不以本人文档为主责 |
| 饶慧 | 规模 | 通知、推送和改时间是一条业务链,过了半条主路径,没有到工程与论坛主路径的规模 |
| 饶慧 | 文档 | 通知规则在页面和测试里能对上,整份设计说明不以本人为主责 |
| 叶银珍 | 规模 | 人数、名单、日程和演示数据都落地,范围仍小于论坛主路径加推荐 |
| 袁湘湘 | 规模 | 资源包和设计说明覆盖五种语言,不改接口和报名计算,规模不到功能主路径 |
| 袁湘湘 | 协作 | 文案和 AI Coding 记录已写入;对报名、通知字段的联调批注少于功能负责人 |
| 李润君 | 规模 | 单元测试盖住登录、报名、通知、推荐、日程和统计,本人不改业务计算 |
| 李润君 | 文档 | 用例和断言在测试文件里,对外设计说明不以测试章节代替 |
| 陈林祺 | 规模 | 端到端覆盖登录、报名、通知、改时间、导出、推荐和健康检查,本人不改业务计算 |
| 陈林祺 | 文档 | 验收步骤留在用例里,整份设计说明不以验收章节代替 |
| 高娅婷 | 规模 | 容器、依赖检查、构建和生产部署可用,不改报名和通知规则 |
| 高娅婷 | 协作 | 环境和部署配置完整;跨模块“下一句怎么改”的批注少于功能联调记录 |
| 高娅婷 | 文档 | 启动和部署依赖有记录,设计正文由文档职责主写 |
记 100 的项
| 谁 | 哪些是 100 | 因为 |
|---|---|---|
| 饶恩哲 | 交付、规模、协作、规范 | 工程骨架、迁移、论坛浏览、官网导入和智能推荐按目录交付,导入可重复、推荐不采用名单外的论坛 |
| 许雅欣 | 交付、规模、协作、规范、文档 | 会话、限流、登录注册、报名写入、个人报名和回执关闭;公开注册不能改成主席或秘书 |
| 饶慧 | 交付、协作、规范 | 通知发布、列表、实时推送和修改论坛时间对得上分工,再次导入会跳过主席改过的时间 |
| 叶银珍 | 交付、协作、规范、文档 | 人数、主席看板、导出、个人日程与冲突、演示账号可重复导入;冲突规则写在日程模块里 |
| 袁湘湘 | 交付、规范、文档 | 设计说明、五种语言资源包和 AI Coding 记录按实际行为更新,不改接口字段 |
| 李润君 | 交付、协作、规范 | 登录、报名、通知、推荐、日程和统计的单元测试能单独跑,不改业务计算 |
| 陈林祺 | 交付、协作、规范 | 登录到推荐的端到端和健康检查能复跑;无权限断言的是拒绝,不是空白页 |
| 高娅婷 | 交付、规范 | 本地数据库、容器、依赖检查、生产部署和构建脚本与功能代码分开 |
解开阻塞,各加 2 分
| 姓名 | 加分 | 做了什么 |
|---|---|---|
| 饶慧 | +2 | 通知发给谁和改时间后的再次导入分开,主席改过的时间不会被官网导入盖掉 |
| 叶银珍 | +2 | 个人日程把已选论坛的时间重叠标出来,报名保存和冲突提示不再各写一套 |
| 李润君 | +2 | 推荐、日程和统计的单元测试把“页面能开但计算没被断言”的缺口补上 |
| 陈林祺 | +2 | 四种角色的端到端分开断言拒绝,空白页和无权限不再混成一种结果 |
个人分
R = 0.35 × 交付 + 0.25 × 规模 + 0.20 × 协作 + 0.12 × 规范 + 0.08 × 文档。
| 姓名 | 计算 | 五项结果 | 加 2 分后 | 过程分 |
|---|---|---|---|---|
| 饶恩哲 | 0.35×100 + 0.25×100 + 0.20×100 + 0.12×100 + 0.08×60 = 35+25+20+12+4.8 | 96.8 | 不加 | 96 |
| 许雅欣 | 0.35×100 + 0.25×100 + 0.20×100 + 0.12×100 + 0.08×100 = 35+25+20+12+8 | 100 | 不加 | 100 |
| 饶慧 | 0.35×100 + 0.25×60 + 0.20×100 + 0.12×100 + 0.08×60 = 35+15+20+12+4.8 | 86.8 | 88.8 | 88 |
| 叶银珍 | 0.35×100 + 0.25×60 + 0.20×100 + 0.12×100 + 0.08×100 = 35+15+20+12+8 | 90 | 92 | 92 |
| 袁湘湘 | 0.35×100 + 0.25×60 + 0.20×60 + 0.12×100 + 0.08×100 = 35+15+12+12+8 | 82 | 不加 | 82 |
| 李润君 | 0.35×100 + 0.25×60 + 0.20×100 + 0.12×100 + 0.08×60 = 35+15+20+12+4.8 | 86.8 | 88.8 | 88 |
| 陈林祺 | 0.35×100 + 0.25×60 + 0.20×100 + 0.12×100 + 0.08×60 = 35+15+20+12+4.8 | 86.8 | 88.8 | 88 |
| 高娅婷 | 0.35×100 + 0.25×60 + 0.20×60 + 0.12×100 + 0.08×60 = 35+15+12+12+4.8 | 78.8 | 不加 | 78 |
饶恩哲五项结果 96.8,比过程分多 0.8。饶慧、李润君、陈林祺加完 2 分是 88.8,高娅婷是 78.8,这 0.8 不进入过程分。许雅欣 100、叶银珍 90 加 2 分后是 92、袁湘湘 82,没有这 0.8。
过程分之和 = 96 + 100 + 88 + 92 + 82 + 88 + 88 + 78 = 712。这组数留作本轮过程记录。折贡献比例不用这 712。
本轮计入的份额按角色定,八人互不相同。工程与论坛主路径、智能推荐的饶恩哲 19,账号与报名的许雅欣 16。端到端验收的陈林祺比 11 高一点,记 11.20。
其余五人围绕 10.8,且都不低于 10。袁湘湘管设计说明、五种语言和 AI Coding 记录,低于 11、高于 10.8,记 10.90。叶银珍管人数、名单、日程和演示数据,同样低于 11、高于 10.8,记 10.85。高娅婷管环境与部署,记 10.80,高于饶慧。饶慧管通知、推送和改时间,记 10.75。李润君管单元测试,是八人里最低的一档,记 10.50,不低于 10。
11.20 + 10.90 + 10.85 + 10.80 + 10.75 + 10.50 = 65。相对 10.8 的偏差是 +0.40、+0.10、+0.05、0、−0.05、−0.30,代数和为 +0.20。这 0.20 补在陈林祺高于 11 的那一截里:11.20 − 11 = 0.20。
19 + 16 + 11.20 + 10.90 + 10.85 + 10.80 + 10.75 + 10.50 = 100。
贡献系数
W = 这个人的份额 / 100。
先得到百分数,再保留两位。两位之后加起来要是 100%。
| 学号 | 计算 | 未补余数 | 贡献度 |
|---|---|---|---|
| 072403123 | 19 / 100 = 19.00% | 19.00% | 19.00% |
| 102400307 | 16 / 100 = 16.00% | 16.00% | 16.00% |
| 072403324 | 10.75 / 100 = 10.75% | 10.75% | 10.75% |
| 052406103 | 10.85 / 100 = 10.85% | 10.85% | 10.85% |
| 102400202 | 10.90 / 100 = 10.90% | 10.90% | 10.90% |
| 102400103 | 10.50 / 100 = 10.50% | 10.50% | 10.50% |
| 102400201 | 11.20 / 100 = 11.20% | 11.20% | 11.20% |
| 102400109 | 10.80 / 100 = 10.80% | 10.80% | 10.80% |
19.00 + 16.00 + 10.75 + 10.85 + 10.90 + 10.50 + 11.20 + 10.80 = **100.00%**。本次没有余数要补。
本次贡献度
| 学号 | 工作内容 | 贡献度 |
|---|---|---|
| 072403123 | 工程与论坛主路径、智能推荐。工程骨架与首页,表结构与迁移,论坛浏览,官网议程导入,智能推荐 | 19% |
| 102400307 | 账号与报名。会话与登录限流,登录和注册页面,报名写入,个人报名与回执,分步引导 | 16% |
| 072403324 | 通知与论坛时间。通知发布,通知列表,修改论坛时间,实时推送 | 10.75% |
| 052406103 | 人数、名单与日程。人数统计,主席看板,参会者导出,个人日程与冲突,演示数据 | 10.85% |
| 102400202 | 多语言与设计文档。设计说明,五种语言资源包,AI Coding 记录 | 10.90% |
| 102400103 | 单元测试。登录与权限,报名,通知与推荐校验,日程冲突与统计 | 10.50% |
| 102400201 | 端到端验收。登录,报名主路径,通知、改时间与导出,推荐与健康检查 | 11.20% |
| 102400109 | 环境与部署。本地数据库与容器,依赖检查,生产部署,构建脚本 | 10.80% |
19 + 16 + 10.75 + 10.85 + 10.90 + 10.50 + 11.20 + 10.80 = **100%**。
过程分用于组内过程记录;计入本次作业组员贡献比例的,以本表贡献度为准。
7. PSP 表格
按《构建之法》PSP2.1。范围是 2026-11-03 至 2026-11-04 这一轮产品实现,单位是分钟。预估是开工前每人自己报的,实际是做完后的回忆,没有打卡。父行是下面几行的和。
饶恩哲
基建、环境和工作区占的比重大,编码比页面少。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 30 | 40 |
| · 估计这个任务需要多少时间 | 30 | 40 |
| 开发 | 550 | 695 |
| · 分析需求 | 60 | 80 |
| · 生成设计文档 | 80 | 100 |
| · 设计复审 | 40 | 50 |
| · 代码规范 | 20 | 25 |
| · 具体设计 | 70 | 90 |
| · 具体编码 | 160 | 200 |
| · 代码复审 | 40 | 50 |
| · 测试 | 80 | 100 |
| 报告 | 65 | 80 |
| · 测试报告 | 25 | 30 |
| · 计算工作量 | 15 | 20 |
| · 事后总结 | 25 | 30 |
| 合计 | 645 | 815 |
许雅欣
登录、权限和对应测试占开发的大头。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 25 | 30 |
| · 估计这个任务需要多少时间 | 25 | 30 |
| 开发 | 480 | 605 |
| · 分析需求 | 40 | 55 |
| · 生成设计文档 | 30 | 40 |
| · 设计复审 | 20 | 30 |
| · 代码规范 | 20 | 20 |
| · 具体设计 | 40 | 50 |
| · 具体编码 | 180 | 210 |
| · 代码复审 | 30 | 40 |
| · 测试 | 120 | 160 |
| 报告 | 60 | 80 |
| · 测试报告 | 30 | 40 |
| · 计算工作量 | 10 | 15 |
| · 事后总结 | 20 | 25 |
| 合计 | 565 | 715 |
饶慧
论坛目录、返回和五种语言的文案。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 20 | 30 |
| · 估计这个任务需要多少时间 | 20 | 30 |
| 开发 | 450 | 570 |
| · 分析需求 | 40 | 50 |
| · 生成设计文档 | 25 | 35 |
| · 设计复审 | 15 | 20 |
| · 代码规范 | 15 | 20 |
| · 具体设计 | 50 | 70 |
| · 具体编码 | 200 | 240 |
| · 代码复审 | 25 | 35 |
| · 测试 | 80 | 100 |
| 报告 | 45 | 55 |
| · 测试报告 | 20 | 25 |
| · 计算工作量 | 10 | 10 |
| · 事后总结 | 15 | 20 |
| 合计 | 515 | 655 |
叶银珍
冲突规则和报名写入来回改过几轮,测试比预估长。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 25 | 35 |
| · 估计这个任务需要多少时间 | 25 | 35 |
| 开发 | 495 | 640 |
| · 分析需求 | 50 | 70 |
| · 生成设计文档 | 40 | 50 |
| · 设计复审 | 30 | 40 |
| · 代码规范 | 15 | 20 |
| · 具体设计 | 60 | 80 |
| · 具体编码 | 170 | 210 |
| · 代码复审 | 30 | 40 |
| · 测试 | 100 | 130 |
| 报告 | 55 | 70 |
| · 测试报告 | 25 | 30 |
| · 计算工作量 | 10 | 15 |
| · 事后总结 | 20 | 25 |
| 合计 | 575 | 745 |
袁湘湘
设计说明和端到端占得多,具体编码比其他人少。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 40 | 50 |
| · 估计这个任务需要多少时间 | 40 | 50 |
| 开发 | 500 | 630 |
| · 分析需求 | 50 | 60 |
| · 生成设计文档 | 90 | 110 |
| · 设计复审 | 40 | 50 |
| · 代码规范 | 20 | 25 |
| · 具体设计 | 40 | 50 |
| · 具体编码 | 80 | 100 |
| · 代码复审 | 40 | 55 |
| · 测试 | 140 | 180 |
| 报告 | 90 | 115 |
| · 测试报告 | 40 | 50 |
| · 计算工作量 | 20 | 25 |
| · 事后总结 | 30 | 40 |
| 合计 | 630 | 795 |
李润君
表、导入和导出。设计文档比页面多。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 25 | 30 |
| · 估计这个任务需要多少时间 | 25 | 30 |
| 开发 | 465 | 585 |
| · 分析需求 | 45 | 60 |
| · 生成设计文档 | 50 | 70 |
| · 设计复审 | 25 | 35 |
| · 代码规范 | 20 | 25 |
| · 具体设计 | 50 | 60 |
| · 具体编码 | 160 | 190 |
| · 代码复审 | 25 | 35 |
| · 测试 | 90 | 110 |
| 报告 | 50 | 60 |
| · 测试报告 | 20 | 25 |
| · 计算工作量 | 15 | 15 |
| · 事后总结 | 15 | 20 |
| 合计 | 540 | 675 |
陈林祺
通知推送和测量响应头。测试里有来回确认订阅是不是同一份。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 20 | 30 |
| · 估计这个任务需要多少时间 | 20 | 30 |
| 开发 | 485 | 625 |
| · 分析需求 | 40 | 55 |
| · 生成设计文档 | 30 | 40 |
| · 设计复审 | 25 | 35 |
| · 代码规范 | 15 | 20 |
| · 具体设计 | 50 | 70 |
| · 具体编码 | 180 | 220 |
| · 代码复审 | 35 | 45 |
| · 测试 | 110 | 140 |
| 报告 | 55 | 70 |
| · 测试报告 | 25 | 30 |
| · 计算工作量 | 10 | 15 |
| · 事后总结 | 20 | 25 |
| 合计 | 560 | 725 |
高娅婷
推荐校验、兜底和空字符。实际编码和测试都比预估长。
| PSP2.1 | 预估 | 实际 |
|---|---|---|
| 计划 | 30 | 40 |
| · 估计这个任务需要多少时间 | 30 | 40 |
| 开发 | 545 | 715 |
| · 分析需求 | 50 | 70 |
| · 生成设计文档 | 40 | 55 |
| · 设计复审 | 30 | 40 |
| · 代码规范 | 15 | 20 |
| · 具体设计 | 50 | 70 |
| · 具体编码 | 200 | 250 |
| · 代码复审 | 30 | 40 |
| · 测试 | 130 | 170 |
| 报告 | 70 | 90 |
| · 测试报告 | 30 | 40 |
| · 计算工作量 | 15 | 20 |
| · 事后总结 | 25 | 30 |
| 合计 | 645 | 845 |
8. 选题展示问题的重新回答
下面每个问题按这四段写:答辩当时怎么答,原型画成什么样,库和系统设计定成哪条规则,冲刺里哪一天做成。
问题 1 微信、支付宝已经能 AA,为什么还要做
答辩当时
准备稿里的口径收成一句:
群收款只管单独一笔。一次活动里好几笔互相垫,再发一次群收款,盖不住前面那些。同账只算谁转给谁,钱仍在微信或支付宝里转。
(1)对照原文是 微信群收款 和 支付宝 AA 与群收款。
(2)能抵多笔的产品没有藏,说明书第 6 节写了 百事 AA 和 Splitwise 的债务合并。
(3)固定包里的范围说明是 product-spec.md。
三人例子写在说明书第 3 节,数字是验收用的,不是调查结果。
| 谁记 | 内容 | 谁付 | 怎么分 | 每人应付 |
|---|---|---|---|---|
| A | 餐饮 120 | A | 三人人均 | 各 40 |
| B | 交通 90 | B | 三人人均 | 各 30 |
| C | 住宿 300 | C | 去掉没住的 B | A、C 各 150 |
净额是 A −100、B +20、C +80。收成两笔:A 转给 C 80,A 再转给 B 20。同一个人要转给两个人,这时再发一次群收款,盖不住前面三笔。
原型里怎么画
● 主流程用的是鼓山一日,预设小安、小白、小陈、小丁。
● 结算页 07.01 给出待转账总额和笔数。
● 07.03 到 07.07 把登记拆开:先看到两笔待登记,再分别登记 30 元和 60 元。
● 07.11 另画了待转账为 0 的情况。
● 页面清单写明:原型里的「已还」只是人工登记,不代表银行或微信核到账。
● 可点原型从欢迎页走,Prototype Flow。
● 设计稿 里 04 — Screens 按编号摆状态。
● 固定包上的对照说明是 prototype-guide.md 和 figma-ledgers-alignment.md。
● 冲刺里走通的是两人一笔,不是稿上那四个人:

库和系统设计
系统设计 1.2.2 把「真实支付与到账验证」放进明确不做。
● 数据库用整数分,消费、分摊、结算批次、转账分成不同的表。
● 预览按当前有效消费重算,正式结算另存一批,之后读已保存的方案。
● 规则 BR02 要求每笔有效消费的分摊合计等于总额,跨行合计在服务事务里验。
● 固定包上的模块说明是 settlements.md、expenses.md 和 balances.md。
● 库里对上的那一笔是 12000 分消费和 6000 分转账:

冲刺做成了什么
| 序号 | 内容 |
|---|---|
| (1) | 固定包 df4d773 上走的是两人一笔:画面 120 元,库里 12000 分,分摊合计仍是 12000,冻结后一笔 6000 分、状态 PAID,账本 SETTLED、版本 5。 |
| (2) | 核对文本是 final-database.txt。 |
| (3) | 这证明「多笔记成谁转给谁」这条在包上能走通。 |
| (4) | 它没有把答辩里的三人三笔原样再跑一遍,也不能说已经比群收款更快。 |
画面上先是一笔消费。成员手动保存 120 元,两人各摊 60 元。这一步只证明记入,还没证明收成了转账。

(1)创建者确认冻结时,120 元消费收成一笔 60 元转账。
(2)正式冻结后进入结算中,进度是 0/1 笔。
(3)登记完成是 1/1,待登记金额为 0。
(4)创建者再确认,账本已结清,原来的 120 元消费还在。
(5)这些画面都在 Day 7,原图目录是 day7-delivery。



● 库里的原始结果单独留了两张。
● 第一张是结清状态、归档隔离、12000 分消费、分摊和图片字节。
● 第二张是 6000 分、状态 PAID 的转账,以及七类业务审计各一次。
● 答辩里的三人例子(餐饮 120、交通 90、住宿 300,净额 −100/+20/+80)没有在这个固定包上再跑一遍。
● 这次能指给老师看的,是两人一笔从记入到结清。

问题 2 转账方案怎么算,余数算谁的
答辩当时
准备稿把算法写成三步,少一步就不叫最少笔数。
(1)先算每人净额。加起来必须是 0,否则不出方案。
(2)再拆成尽量多的、组内也能结平的小组。人数是 n,这样的组有 k 个,最少笔数是 n − k。不用贪心。上界 O(3^n),所以一本账限 12 人。
(3)算完再模拟一遍。每个人归零、金额为正、没有自己转给自己,这一轮才算过。
笔数可以多,金额不能错。算不完就如实写,不在超时后改成贪心,还继续叫最少。
金额按分存。
● 100 元三人均,记成 3334、3333、3333 分,加回去还是 10000。
● 说明书第 4 节补了一句:余分用最大余数法,余数相同按固定成员顺序,同一笔每次结果一样。
● 算法上界参考的是 The debts' clearing problem。
● 整数类型用的是 PostgreSQL 数值,不用小数。
● 12 人最坏耗时当时没测完,口径是算不动就如实写,不改成贪心还叫最少。
● 代码后来落在 split.test.ts。
原型里怎么画
| 序号 | 内容 |
|---|---|
| (1) | 分摊单独一章。 |
| (2) | 05.01 全部参与,05.02 去掉没参与的人,05.03 按份,05.04 按金额。 |
| (3) | 05.06 把 100 元三人均画成 33.34 / 33.33 / 33.33,清单写明这是规则展示,不计入鼓山账本。 |
| (4) | 05.07 画了 120 元只分配 110 元,「完成」禁用。 |
| (5) | 07.01 的预览不冻结账本,07.02 才是冻结确认。 |
| (6) | 页在 设计稿,可点路径在 Prototype Flow。 |
| (7) | 冲刺里对上的是能除尽的 120 元,两人各 60,不是 05.06 那张尾差示意: |
库和系统设计
(1)数据库术语表把金额定义成 BIGINT 整数分,12000 表示 120 元。
(2)expenses.amount_cents 和 expense_splits 分开存,每人最终应摊的分写进行,展示不再重跑规则。
(3)结算预览记在 A22:零和子集求精确最少笔数,预览可以过期,不生成正式批次。
(4)幂等键记在 idempotency_keys,同一写请求重放不能变成第二笔。
(5)模块说明是 expenses.md 和 settlements.md,冻结核对是 settlement-freeze-validation.md。
(6)落库后的整数在这张 SQL 里:
冲刺做成了什么
Day 2 把记消费收成一次事务,对应 Issue #3 和 PR #11。分摊纯函数在固定包的 split.test.ts。Day 6 的记录写明:函数通过之后,还要核对 expense_splits 里的整数是不是真的落库。固定包上的结果就是上一问那笔 12000 分,两人各 6000,加回去仍是这笔消费。
Day 2 当天无数据库一层是 17 个文件、105 项。终端画面留在冲刺总结里。数据库按 PR 另记,不相加:PR #9 27/27,PR #10 34/34,PR #11 38/38。输出文件是 第二天无数据库 和 第二天移动端。12 人最坏耗时在冲刺里仍没有单独的计时记录,答辩时「算不动就如实写」这条没有被改成贪心。

● 同一笔被显式用原键重放时,库里仍然只有一笔 120 元。
● 这张是 Day 6 的候选路径,用来看幂等,不拿来改写 Day 7 固定包上的 12000 分。

问题 3 调查里没有算错率,凭什么说这是需求
答辩当时
准备稿把公开资料能证明什么、不能证明什么分开写。
| 来源 | 样本 | 写了什么 | 没写的 |
|---|---|---|---|
| 中国青年网,2016-03-04 | 460 名大学生 | 29.31% 每周聚会两次以上 | 算错率,AA 占比 |
| 河北都市网,2019-11-14 | 在连高校 236 人 | 52.54% 每月聚餐两次以上 | 没还钱的比例 |
队里没有存档问卷,所以说明书不补这两个比例。用户只收在校生、一次至少 3 人、有人先垫事后再还。当场付清的不做。
对照产品说明的是别人已经能做什么,不是本队测出来的算错率:
- 微信群收款 能收单独一笔
- 支付宝 AA 与群收款 写的是等额人均
- 百事 AA 和 Splitwise 能抵多笔
需求成立,靠的是「一次活动里好几笔互相垫,结束时要看清谁转给谁」。不靠一个我们没有测过的算错率。
原型里怎么画
原型没有做问卷页。
● 它把「一次活动」画成一本账:02.01 我的账本,02.03 建完是空的,02.04 才有总支出和流水。
● 验收数字用鼓山一日的预设,页面清单写明点击提交只演示下一步,不执行真实注册。
● 需求边界因此留在说明书,不留在可点稿的通过次数上。
● 可点稿从欢迎页进:Prototype Flow。
● 静态页在 设计稿。
● 固定包里的说明是 prototype-guide.md,产品范围在 product-spec.md。
库和系统设计
| 序号 | 内容 |
|---|---|
| (1) | 系统设计的读者是开发、测试和答辩教师,用户仍是参加同一活动的成员,最多 12 人。 |
| (2) | 数据库名 tongzhang,手机不直连库。 |
| (3) | 没有「算错率」这张表,因为选题时就决定不报这个数。 |
| (4) | 验收改成金额守恒:分摊合计、净额合计为 0、结清后财务字节不变。 |
| (5) | 检查写在 expenses.md 和 balances.md,固定包上的结果是这一张: |

冲刺做成了什么
(1)冲刺没有补问卷。
(2)公开资料仍然只证明聚餐常见,不证明同账减少了算错。
(3)能拿出来的是「这笔账能不能对上」,写在 测试随笔 第 1 节。
● 金额用整数分。
● 100 元是 10000 分,三人均分得不到三个相同的整数。
● 测试预期是:成员 a、b、c 按这个顺序,a 得 3334,b 和 c 各 3333,合计仍是 10000;把成员顺序反过来,同一个人拿到的分数不变;1 分分给三人,合计仍是 1;按份 2:1 分 100 分,是 67 与 33;手动分摊合计对不上,拒绝写入。
● 页面上显示「¥100.00」只说明格式,上表这几行才说明总额守恒。
● 代码在固定包的 split.test.ts。
● Day 2 把这层先跑通,终端是 17 个文件、105 项,其中包含记账事务,不单是这一份分摊。
函数通过之后,还要看库。
● Day 6 的结算核对在 issue57-settlement-invariants-validation.md,定向回归是 4 个文件、38 项,和当天无数据库的 125 项分开记。
● Day 7 固定包上的真实账是 #72:1 个账本、2 名成员、12000 分消费、两条 6000 分分摊、1 笔 6000 分且状态 PAID,最终 SETTLED、版本 5。
● 文本在 final-database.txt。
| 序号 | 内容 |
|---|---|
| (1) | 画面上,两人各摊 60 元。 |
| (2) | 余额里创建者待付 60、成员待收 60,两边相加是 0。 |
| (3) | 库里的原始行是 12000 分消费、两条分摊,以及后来那笔 6000 分转账。 |
这四张证明的是固定包上这一笔对得上。它们不构成算错率,也不能把 105 项、38 项和 Day 7 的通过数加在一起。
问题 4 钱还是要自己去转,到底解决了什么
答辩当时
(1)准备稿的答案是:解决算不清、对不上、不好意思催。
(2)真实收付款这次不做,对照仍是 微信群收款,App 不发起那一笔。
(3)结算页给出谁转给谁、转多少,转完标已还,全部标完金额冻住,账本只读归档。
(4)快了多少、是不是更好开口,没测过,不报数字。
(5)范围写在 product-spec.md。
原型里怎么画
● 07.08、07.12 是复制转账文案的轻提示,不切换到支付 App。
● 07.06、07.07 登记前先核对「谁转给谁、多少」。
● 07.09 全部已还之后等创建者结清,07.10 再确认进入只读。
● 08.02 归档说明移出当前列表但保留历史,08.04 是历史账本。
● 08.05 在清单里已删除,内容并进 08.01。
● 稿在 Figma,说明在 prototype-guide.md。
● 冲刺里对应的登记面板是这一张,金额是 60 元,不是稿上的 30 加 60:

库和系统设计
账本三种状态写进说明书,后面的系统和数据库沿用同一套名字。
| 状态 | 这时能做 | 这时不能做 |
|---|---|---|
| ACTIVE 进行中 | 邀请、记账、看预览 | 把预览当成已经冻结 |
| SETTLING 结算中 | 看方案、登记已还、创建者结清 | 再改消费和成员 |
| SETTLED 已结清 | 只读、本人归档、看历史 | 改金额,或把账从别人的列表里删掉 |
BR07:正式结算后消费、分摊、成员集合和方案不可改,没有取消结算接口。
● 还款不另建表,首版记在单笔转账的状态上。
● 归档是成员自己的 archived_at,不是把财务行删掉。
(1)模块说明在 settlements.md 和 ledgers.md。
已还只是人在 App 里登记。它不等于微信或支付宝已经到账。
冲刺做成了什么
Day 3 接了正式冻结,Day 5 接了已还和结清。Day 7 上,创建者归档之后能看历史,另一名成员的当前列表里这本账还在。归档前、归档后的财务记录字节一致:归档前、归档后。普通成员看得到自己的待收,看不到冻结入口。余额明细上,创建者待付 60 元,成员待收 60 元。成员打开预览,只有一笔 60 元方案,没有正式冻结。创建者那边才有冻结确认。登记是人工整笔,面板上核对的是这一笔 60 元,不是跳到微信或支付宝。

| 序号 | 内容 |
|---|---|
| (1) | 归档是创建者自己的列表。 |
| (2) | 确认文案写明不影响其他成员。 |
| (3) | 创建者当前列表移出这本账,历史里仍能打开,打开后消费和正式记录只读。 |
| (4) | 另一名成员的当前列表里这本已结清账本还在,他的历史是空的,说明个人归档没有串到别人的历史。 |
| (5) | 画面都在 Day 7。 |






问题 5 功能是不是太少
答辩当时
(1)这句是展示时听到的。
(2)说明书第 1 节其实已经写了主路径:注册登录、建账、邀请、记账、看流水和余额、预览、冻结、登记已还、结清只读、归档。
(3)第 5 节还列了统计、三种分摊、筛选。
(4)当时没有可运行的包,所以听起来像只有「能登录、能记一笔」。
(5)后来收进仓库的是 product-spec.md,作业帖是 选题。
原型里怎么画
● Screens 按编号有 73 个设计条目,含状态和面板,不等于 73 个路由。
● 登录、账本、邀请、消费、分摊、余额、结算、归档、个人设置都有页。
● 扫码、复制、分享在稿上是示意。
● 清单写明:一部分异常只在 Screens 里展示,可点稿用预设账号,不能在里面真正建三个账号。
● 入口是 设计稿 和 可点原型,文字对照是 prototype-guide.md。
● 冲刺真正跑起来的主路径从建账这一张开始:

库和系统设计
系统设计把首版纳入和明确不做分成两列。选题第 5 节、系统设计 1.2.2、冲刺结束时的 501,三处并不完全重合,所以分开列。
| 能力 | 选题第 5 节 | 系统设计 1.2.2 | 冲刺固定包 |
|---|---|---|---|
| 真实账号、邀请、记一笔、收据、预览、冻结、已还、结清、本人归档 | 做 | 纳入 | 主路径接上 |
| 三种分摊、流水筛选、统计 | 做 | 纳入 | 统计仍是 501,筛选没有单独验收 |
| 进行中修改、删除支出 | 建议限记录人或创建者 | 进行中允许按权限改、删 | 仍返回 501 |
| 浏览器版、真实支付、OCR、离线记账、成员退出、分次还款、取消结算 | 不做或不进必达 | 明确不做 | 没有入口 |
冲刺结束时仍返回 501 的原文在 known-limitations.md,模块索引在 docs/modules。
● 已核销的范围看燃尽:

冲刺做成了什么
| 序号 | 内容 |
|---|---|
| (1) | 冲刺核销的是开题时那条主路径,28 项、130 点。 |
| (2) | 燃尽 130→121→98→78→53→48→16→0,到 Day 7 剩余为 0。 |
| (3) | 资料随 65227bd 交付。 |
(1)剩余 0 只说明这 28 项核销完了。
(2)消费修改、消费删除、统计、资料修改、通知偏好保存仍返回 501,扫码没有入口。
(3)原文在 已知限制。
(4)原型里画过的统计和编辑,这次没有接上。
(5)Day 6 结束还剩 16 点,是安装和文档,不是把 501 那些补上。
(6)燃尽见 Day 6。

● 主路径在冲刺里的分工可以顺着 Issue 看,不必把「功能少」理解成没有人认领。
● Day 1 到 Day 2 是契约和记账事务,#1 到 #12。
● Day 3、Day 4 是建账、邀请、收据和冻结,#13 到 #34。
● 收口在 冲刺总结 和 评审表。
9. 选题之后的新思考与具体行动
这一节只回答选题答辩结束之后的事:有什么新的思考,以及思考落到了哪一次具体行动。
● 答辩当时怎么答,写在第 8 节。
● 这里每问都按同一组模块写,前面的材料保留,末尾再收成三块。
| 模块 | 写什么 |
|---|---|
| 选题之后的新思考 | 答辩材料里还没定死、后来才看清的判断 |
| 具体行动 | 改了哪张表、加了哪个接口、哪一天做的、用什么测过 |
| 这次明确不做 | 想过但冲刺里决定不接的部分,避免把想法当成已经完成 |
| 序号 | 内容 |
|---|---|
| (1) | 时间顺序是:选题最早,然后是 2026-10-12 前后的 原型,再是 2026-10-20 的 系统与数据库设计,最后才是 10 月 23 日到 29 日的 冲刺。 |
| (2) | 仓库是 Liangbai2333/tongzhang。 |
问题 1 别人怎么进这本账
选题时只定了入口
说明书第 3 节把邀请收成几条,后来写进 product-spec.md。
(1)创建者看到 6 位邀请码和对应二维码。码和二维码是同一条服务端记录。
(2)对方要先安装并登录,再输入邀请码,或在本 App 内扫码。确认账本名称、封面和创建者之后才加入。
(3)只有创建者能查看、分享和重置。重置后旧码和旧二维码一起失效。
(4)只有进行中的账本能加人,最多 12 人,创建者算在里面。
二维码只保证在 App 内能扫。不保证微信或系统相机扫了会跳进同账。
原型先把码放在邀请页
(1)03.01 展示账本、6 位码和二维码。
(2)03.03 输入,03.05 加入前核对账本、创建者和人数。
(3)03.06 码错误、03.07 过期、03.08 已满 12 人,都留在原输入页,不另开错误路由。
(4)03.10 重置前要确认旧码失效。
(5)加入是另一条分支,不能在原型里实际拉起第二个账号。
(6)稿在 Figma,对齐记录是 figma-invitations-alignment.md。
(7)冲刺里第二个人看到的确认页是这一张:

设计里写成成员关系
● 数据库把用户和成员拆开:用户是账号,成员是用户和这本账的关系,用 joined_at。
● 未加入者不能读账目。
● 系统设计 4.5 一带按这个关系做权限,不靠客户端把按钮藏起来。
● 模块是 invitations.md 和 ledgers.md,接口说明是 ledger-invitation-api-guide.md。
冲刺里码出现在建账之后
Day 3 接了建账和邀请,专项说明在 issue18-ledger-invitation-tests.md。Day 4 把封面和建账放进同一笔事务。Day 7 的固定包上,创建者上传封面并建账,邀请码出现在画面上。第二账号按这个码预览同一本账再加入,库里是 1 个账本、2 名成员。扫码仍没有入口,和 501 那条放在一起。
建账时邀请码出现在画面上:
第二账号按这本账加入:
Day 6 还留了一条更早的候选路径:先选封面、填写名称,提交时模拟 503,原尝试留在界面上;会话被撤销就进重新登录;同账号再登录后名称和上传引用还在;用原来的键重放,得到的是同一本账、同一个邀请码,没有第二本。乙再输入这个码、预览并加入。这条用来说明「建账和邀请码是一次写入」,和 Day 7 固定包上的那张建账图是两次运行,数字不要加在一起。



选题之后的新思考
答辩时「可以邀请」四个字,没有回答第二个人看到的是不是同一本账。
● 选题之后才定下来:邀请码必须出现在建账成功的画面上,加入前先预览账本名称和创建者,库里用成员关系而不是再复制一份账。
码抄错、码过期、人满了,都留在原来的输入页。不另开一个错误页,也不在客户端把按钮藏起来就算拒绝。
具体行动
| 行动 | 落点 | 怎么核对 |
|---|---|---|
用户和成员拆表,成员用 joined_at | ledgers.md、invitations.md | 未加入者不能读账目 |
| Day 3 接建账和邀请 | Issue #18 测试说明 | 专项 2 个文件、23 项,不并进别的层 |
| Day 4 封面和建账同一事务 | Day 4 | 失败不留下没有封面的账,也不留下没有账的封面 |
| Day 7 固定包双账号 | #72 | 1 个账本、2 名成员,邀请码在建账画面上 |
(1)创建者上传封面并建账,邀请码出现在画面上。
(2)第二账号按这个码预览,确认后再加入。
(3)Day 6 用原键重放,得到的仍是同一本账、同一个码。
这次明确不做
- 扫码加入没有入口,03.04 只留在原型里
- 邀请链接、未安装时的引导,选题里就排除了
- 成员退出、把人移出账本,系统设计写进明确不做
问题 2 金额、分摊和收据能不能分开保存
选题时怕记成两笔
| 序号 | 内容 |
|---|---|
| (1) | 说明书第 5 节写过:超时不能直接当成失败,要按同一次请求去核对,避免同一笔记两次。 |
| (2) | 收据至少一张,图片放 PostgreSQL,列表只给引用。 |
| (3) | 二进制类型见 bytea。 |
| (4) | 边界后来写在 expense-create-boundary.md。 |
原型把失败留在原表单
(1)04.01 一张表单里同时有金额、付款人、分摊和收据。
(2)04.06 保存失败时保留输入并给出重试,不另开错误页。
(3)05.07 合计不符时不能完成。
(4)稿在 Figma。
(5)冲刺里收据已经按成员读出来:

设计里收成一张事务
● 数据库 3.1:消费、分摊明细、收据关联分开建实体,写入必须同一事务。
● BR12 要求付款人、分摊人、图片都在这本账里。
● BR13 用幂等复合主键,成功结果和业务同事务提交。
● 图片用 bytea,授权按成员,不按「拿到地址就能看」。
● 说明在 expenses.md、images.md 和 expense-images-validation.md。
● 重放之后仍是一笔:
冲刺分两天接上
Day 2 先接整数分和一次事务,PR #11。Day 4 把收据绑进同一笔。集成测试在绑定之后故意让后面的数据库步骤失败,再看消费、分摊、版本、审计和图片绑定是否一起回滚。过程写在 测试随笔。
成员要拍收据时,系统会先问相机权限。
● 拒绝之后金额和名称还在,可以改走相册。
● 取消相册也不会把已经填的账本名称清掉。
● 这些是 Day 7 上的权限画面,证明图片入口受系统权限约束,不证明 OCR 已经做了。
● 选题里收据识别本来就放在不做。



| 序号 | 内容 |
|---|---|
| (1) | 归档之后,有权限的成员仍能把这张收据再读出来。 |
| (2) | 原图是 android-38。 |
选题之后的新思考
(1)选题只说超时不能当成失败。
(2)答辩之后才看清失败会落在哪:先存金额、过一会再绑收据,中间断开会留下没有图的消费,或者图没有账。
(3)所以金额、分摊、收据授权必须同一笔提交或同一笔回滚。
(4)每人最终应摊的分写进 expense_splits,展示不再重跑规则。
具体行动
| 行动 | 改了什么 | 核对 |
|---|---|---|
| Day 2 记消费一次事务 | Issue #3、PR #11 | 该 PR 数据库 38/38,不和当天 105 项相加 |
| 分摊余数 | split.test.ts | 10000 分分成 3334、3333、3333 |
| Day 4 收据绑进同一笔 | expense-images-validation.md | 绑定之后故意让后续步骤失败,消费、分摊、图片一起回滚 |
| 幂等 | idempotency_keys | 原键重放仍是一笔 120 元 |
(1)表分开:expenses、expense_splits、expense_images。
(2)写入不分开:三张表进同一事务。
(3)读图按是不是这本账的成员,不按地址。
这次明确不做
- 收据识别、OCR
- 一张消费三张收据
- 用负金额做退款
- 消费修改和删除,冲刺里继续 501
问题 3 看余额会不会把账冻上
选题时两种结算还写在一起
● 说明书第 1 节已经分开:预览跟着进行中的账单变;正式结算按当时的数据冻结并保存方案,之后再打开,读的是已保存的方案。
● 展示时如果把两句合成「可以结算」,听的人会以为看余额就已经冻上。
● 范围在 product-spec.md。
原型把预览和确认拆成两页
07.01 白底摘要,写明预览不冻结。
● 07.02 才是「确认后不可继续改消费」。
● 06.01 是进行中的成员余额,06.02 是结算中的收付关系。
● 02.06、04.04 把结算中的流水和详情画成只读,没有新增、编辑、删除入口。
● 稿在 Figma,文字在 prototype-guide.md。
● 固定包上,普通成员的预览没有冻结入口:
设计里预览不写批次
预览和冻结在设计里是两个写入口,不是同一个按钮的两种文案。
| 动作 | 谁能做 | 库里留下什么 |
|---|---|---|
| 看余额、看预览 | 成员 | 不生成正式批次,预览可以过期 |
| 正式冻结 | 创建者 | 进入 SETTLING,保存当时的方案 |
| 登记已还、确认结清 | 创建者;某一笔已还可由该笔付款人登记 | 转账状态改变,消费金额不改 |
| 序号 | 内容 |
|---|---|
| (1) | 状态进 SETTLING 之后,写入口统一拒绝再改消费。 |
| (2) | 普通成员仍能看自己的应收应付。 |
| (3) | 条文在 balances.md、settlements.md 和 settlement-freeze-validation.md。 |
A22 写的是:结算预览可以过期,不生成正式批次。看一眼余额,不会把账冻上。
冲刺按这个边界接
Day 3 把只读预览和正式冻结分成两个写入口。Day 5 把已还和结清接上。固定包上,成员看得到待收 60 元,看不到冻结按钮。冻结后的那笔转账才进入 PAID。画面在 Day 7。
选题之后的新思考
(1)说明书里预览和正式结算本来就是两句。
(2)答辩现场把它们听成「可以结算」之后,才决定在原型、接口和按钮上拆开:看余额不写批次,冻结才进入 SETTLING。
(3)普通成员看得到自己的应收应付,看不到冻结。
具体行动
(1)原型把 07.01 预览和 07.02 冻结确认分成两页,见 prototype-guide.md。
(2)Day 3 把只读预览和正式冻结分成两个写入口,核对在 settlement-freeze-validation.md。
(3)Day 5 接上登记已还和创建者结清。
(4)固定包上成员待收 60 元,创建者待付 60 元,冻结后才有一笔 6000 分、状态 PAID 的转账。画面在 Day 7。
预览可以过期。过期之后再打开,读的是当时保存的方案,不重新分配收款人。
这次明确不做
- 看余额就冻结
- 取消结算、把账从 SETTLING 退回 ACTIVE
- 已经登记还款之后再改原始支出
- 分次还款、撤销已还
问题 4 哪些接口这次不接
选题列了不做,也列了后续
● 首版不做浏览器版、离线记账、邀请链接、双币、真实支付、收据识别、聊天、不限人数的精确最少笔数、上架。
● 收据加到三张、预算、活动日期、操作动态、通知、CSV,写成后续,不进首版必达。
● 修改、删除当时还写在「进行中可以做」里。
● 范围在 product-spec.md,作业帖是 选题。
原型仍把编辑和统计画出来
04.02、04.10 是可编辑详情,04.03 是删除确认。
● 06.03 是类别环形图和成员对比,不提供钻取。
● 03.04 有扫码区域。
● 这些页说明打算有这个状态,不说明服务已经接上。
● 稿在 Figma,对照是 prototype-guide.md。
● 冲刺接上的是记入和结清,不是编辑:
设计把边界写进两张表
| 序号 | 内容 |
|---|---|
| (1) | 系统设计 1.2.2 的「明确不做」比选题更具体:成员退出、分次还款、撤销已还、取消结算、实际通知提醒事件。 |
| (2) | 统计和筛选仍在纳入列。 |
| (3) | 数据库 13 张表里没有单独的统计汇总表,统计若要做,是对有效消费的查询,不是另一套账。 |
| (4) | 占位返回 501 的约定写在 modules/README.md 和 ledgers.md。 |
冲刺先走完一笔到结清
(1)接上的是建账、邀请加入、记消费、上传收据、只读预览、正式冻结、登记已还、结清、归档和历史只读。
(2)见 Day 3、Day 5、Day 7。
继续返回 501 的,和「没有入口」的,分开记。
| 项 | 冲刺结束时 | 依据 |
|---|---|---|
| 消费修改、消费删除 | 501 | 原型画过编辑和删除确认,服务没接 |
| 统计 | 501 | 系统设计纳入过,没有汇总表,也没有查询接口 |
| 资料修改、通知偏好保存 | 501 | 个人页有稿,保存没有接上 |
| 扫码加入 | 没有入口 | 03.04 只在 Screens 里示意 |
原文是 已知限制。
(1)剩余 0 点只说明原 28 项核销完了。
(2)上表这几项不在那 28 项的完成范围内。
(3)Day 6 结束还剩的 16 点是安装和文档,见 Day 6,不是把 501 补上。
选题之后的新思考
● 选题把统计、筛选、修改、删除都写进了首版或「进行中可以做」。
● 原型也把这些页画出来了。
● 冲刺排期时才改判断:先走完一笔从记入到结清,画过的页不等于这周要接的接口。
● 没接的返回 501,不返回一份假数据。
具体行动
接上的写入口按天列在 Day 3、Day 5、Day 7:建账、邀请加入、记消费、上传收据、只读预览、正式冻结、登记已还、结清、归档和历史只读。
不接的原文钉在 known-limitations.md。
● 占位约定在 modules/README.md:业务还没接时返回 FEATURE_NOT_READY,不展示伪造账本或零余额。
这次明确不做
(1)不把原型里的 73 个设计条目当成 73 个已经实现的路由。
(2)不把剩余 0 点写成统计和编辑已经做完。
(3)不把 Day 6 剩下的 16 点挪去补 501。那 16 点是安装和文档。
问题 5 怎么证明金额和权限是对的
选题时的验收只有鼓山一日
| 序号 | 内容 |
|---|---|
| (1) | 说明书第 7 节用一组手算数字验收:分摊合计正确,每人余额与手算一致,12 人以内求出最少笔数,能结清,数据在服务端。 |
| (2) | 没有写测试分层。 |
| (3) | 数字后来收在 product-spec.md,冲刺的分层写在 测试随笔。 |
原型不能当测试
(1)页面清单写明当前原型使用预设账号和固定消费。
(2)1 笔、2 笔流水快照是 ¥120、¥240,点提交不校验。
(3)所以原型通过不能拿来当金额守恒的证据。
(4)可点稿在 Prototype Flow,说明在 prototype-guide.md。
(5)真正落库的 120 元是冲刺这一张,不是原型快照:
设计把检查放进事务和错误码
● 数据库验收写的是完整性与金额守恒,和系统设计共用 FR、A 编号和错误码。
● 分摊合计不符返回 SPLIT_SUM_MISMATCH,冻结后改账返回 LEDGER_FROZEN,重放冲突返回 IDEMPOTENCY_CONFLICT。
● 这些码要在服务端出现,不能只在界面上禁用按钮。
● 约定在 expenses.md 和 shared.md,测试在 split.test.ts。
冲刺按层记通过数
测试随笔 规定上一层通过不改写下一层。固定包 df4d773 上分开记:19 个文件 125 项,24 套件 250 项,新库 136 项。三层不相加。输出在 unit.txt、mobile.txt。
| 层 | 证明什么 | 不能代替什么 |
|---|---|---|
| 纯函数分摊 | 余数进哪一方,同一输入结果相同 | expense_splits 里真的是这个整数 |
| 数据库事务 | 消费、分摊、图片授权一起提交或一起回滚 | 手机上选图、相机权限 |
| 原生路径 | 断网之后迟到的响应该留在界面上 | 组件测试的通过数 |
● Day 6 的回归和 Day 7 的 40 张图,分别证明检查和当时那台模拟器上的操作。
| 序号 | 内容 |
|---|---|
| (1) | 固定包上还看了认证门禁。 |
| (2) | 独立库撤销真实会话之后,再保存会得到 401,进重新登录。 |
| (3) | 核验响应故意延迟 8 秒,断网之后迟到的 200 仍不放行业务。 |
| (4) | 换成另一个账号登录,原金额和标题清空,付款人跟着当前账号走。 |
| (5) | 三张都在 Day 7,对应说明在 issues58-59。 |



Day 5 的证据整理和 Day 6 的回归记录各有一张,用来看当时怎么记层,不用来把通过数加总。


选题之后的新思考
(1)鼓山一日的手算只能说明验收数字长什么样。
(2)一笔消费同时有图片、分摊、账本版本和权限,其中一步写错会记成两笔,或者余额对不上。
(3)选题之后把检查拆成层:上一层通过,不改写下一层的数字。
具体行动
| 层 | 行动 | 固定包上的记录 |
|---|---|---|
| 纯函数 | 余数进哪一方事先写明 | split.test.ts |
| 数据库 | 消费、分摊、图片同一事务;原键重放不是两笔 | 测试随笔,新库 136 项 |
| 权限 | 撤销会话得 401;断网后迟到的 200 不放行 | Issue #46,b0c5cf0,issues58-59 |
| 整包 | 19 个文件 125 项、24 套件 250 项分开归档 | unit.txt、mobile.txt |
四列齐了才算这条用例跑过:预期、入口、当次日志、对上哪个提交。只有预期、没有入口的,不算已经测过。
这次明确不做
- 不用组件测试的通过数代替系统选图和相机权限
- 不把 125、250、136 加总成一个「测了多少项」
- 不用原型点击次数代替金额守恒
问题 6 固定包之后的提交算不算这次已经测过
选题时把「能装上」写成必达
说明书把「能装上」写成必达,同时把几件事排除在必达之外。
- 要有移动端安装包,不提供浏览器版
- Android 和 iOS 不同时承诺两个都交
- 小程序和应用商店上架不写成必达
- 现场另备一台已登录的演示机,避免当场注册失败
● 环境说明引用的是 React Native 和 Expo 开发包。
● 仓库里的启动约束在 environment-setup.md。
冲刺交出去的包要本机 API(3374)和 Metro(8074),启动还要
adb reverse。它证明的是这条开发路径能走通,不是可以从应用商店下载后单独使用。
发布页后来交出的是开发 APK:

冲刺交的是开发客户端
发布页是 alpha-day7-20261029。
● 这个包要本机 API(3374)和 Metro(8074),启动还要 adb reverse。
● 40 张运行图在 验收附件,只证明 df4d773 在当时那台模拟器上的操作。
晚于固定包的提交另算
PR #83 晚于 df4d773。常驻标签和回前台核验要另测,不能借用这 40 张图。PR #84 的 CI 移动端是 256 项,不并进固定包上的 250。提交列表在 commits/main。
| 序号 | 内容 |
|---|---|
| (1) | 发布页上能下载的是开发 APK、源码、校验清单和启动说明。 |
| (2) | 启动说明里写的是本机 API 和 Metro,不是应用商店里的独立包。 |
| (3) | 图在 Day 7 和 冲刺总结。 |
PR #83 回前台核验时会保留页面并显示遮罩。这张图晚于 df4d773,只能证明这次增量,不能补进固定包那 40 张。

冲刺总结 里写的下一阶段安排是建议。答辩 PPT 在 这里。它不是答辩之后就已经排定的下一次站会,也不能用剩余 0 点证明 501 那些已经做完。
选题之后的新思考
(1)「能装上」在选题里是必达。
(2)冲刺交包的那一天才把这句话收窄:交出去的是开发客户端,锁住的提交是 df4d773。
(3)晚于这个提交的界面,不能借用这个包上的 40 张图。
具体行动
(1)发布 alpha-day7-20261029,附上开发 APK、源码、校验清单和启动说明。
(2)启动说明写明本机 API 3374、Metro 8074、adb reverse。约束在 environment-setup.md。
(3)40 张运行图只归档在 day7-delivery,对上 df4d773。
(4)PR #83 和 PR #84 的 CI 另记。256 项不并进 250。
这次明确不做
- 不把开发 APK 写成可以拿走独立使用的发行包
- 不把应用商店上架写成这次已经完成
- 不用 冲刺总结 里的下一阶段建议,代替一次已经发生的测试
10. GUI 界面
知会的界面是浏览器里的网页,部署在 https://zhihui.worldboot.net/。用户不用安装桌面程序。打开网址就能看到导航、表单和列表。下面按页面写屏幕上有什么、用户怎么操作。规则本身放在第 11 节。
五种语言的文案放在资源包里,页面用统一接口取词。语言下拉始终显示「简体中文、繁體中文、English、日本語、Français」,看不懂当前界面时也能切回去。论坛名称保持采集到的中文,不跟着界面语言改。
10.1 未登录能看到的页面
首页一屏里是会议时间和三个入口:选分论坛、看议程、做智能推荐。右侧是场次统计。页脚标明这是课程作业,数据来自 2026 年 10 月 30 日采集的官网公开信息。未登录时右上角是「登录」和「注册」。

分论坛目录是报名的主界面。每场一行,显示日期、起止时间、会场和主席。左侧复选框用来选择要参加的场次。未登录时勾选后进入注册;已登录时要再点保存,才把选择写入。页内可以按日期、主题、会场、类型和关键词缩小列表。下方「我的四天」按日期收已选场次,空着的日期直接标出来。

搜索词写在地址里。搜「软件工程」之后,刷新或把链接发给别人,条件还在。

日期和会场可以叠在关键词上。没有结果时页面给出清除筛选,不用对着空白表猜是没有数据还是筛掉了。

会议议程按日期分组,同一天按开始时间排。从目录点进某一场,或从议程点进,打开的是同一个详情页。

详情页放议题、主席和嘉宾、召开时间、地点。未登录时动作是返回目录,或去报名。

直接打开注册地址会回到目录,并带上注册意图。用户必须先勾选至少一场,才能看到填姓名和邮箱的表。

登录页只有邮箱和密码,密码可以切换显示。没有账号时,页底链到注册。失败提示留在这张表上,写「邮箱或密码错误」,邮箱还在,可以改完再试。


线上用仓库里的演示邮箱登录时,同样停在这张表上。后面的参会者截图改为现场注册。

10.2 语言切换
切换语言后,标题、按钮、统计标签和页脚一起变,布局不变。下面五张是同一种首页。





登录之后再切到英语,右上角仍是退出,不是登录和注册。语言只换文案。

10.3 智能推荐
推荐页是一个输入框和三条可点的例子。点例子会填入句子并立刻请求。页上写明推荐范围是系统里已有的论坛。


10.4 注册、回执、个人中心和通知
从推荐结果进入报名后,步骤条是「选论坛 → 创建账号 → 查看回执」。表上收姓名、邮箱、密码和单位。


提交成功后直接进入回执,不用再登一次。回执按日期列出已报名场次、时间和地点。

登录后导航多出「我的日程」「通知」「个人中心」。论坛详情仍是同一场的议题、时间和地点,推荐里的「查看详情」打开的就是这个页面。

个人中心左侧是姓名、邮箱、单位和角色「参会者」。右侧是报名列表。「修改报名」回到目录改勾选;每一场单独有「取消报名」,取消的是这一场,不是整个账号。

再进目录时可以「只看已选」。筛选对不上时列表可以为空,「我的四天」仍标着已报的那一天。没有改动时,保存按钮是「没有要保存的修改」。

通知页说明「这里只显示你报名的论坛发布的通知」。还没有消息时,页面写「还没有通知」,导航上也没有未读数。

10.5 个人日程
只报名一场时,日程写明没有时间冲突,另外三天标成还没有安排。页底写着:恰好在另一场结束时开始不算冲突。用户在这页选择自己人去哪一场。

再报名同一时段的另一场后,回执上两场都标出时间冲突,导航「我的日程」出现待选择的数量。


在冲突卡片上点「去这场」后,选中的场次标「我去这场」,另一场标「不去,报名保留」,并可以「改去这场」。导航上的冲突数消失。

个人中心同时列出两场。不去的那一行带「不去,报名保留」。两场仍可以各自取消。

10.6 四种角色各自看到的导航
参会者的导航没有人数看板、参会者名单和发布通知。直接在地址栏打开这三处,页面是「没有访问权限」,并可以返回个人中心。



会议主席登录后,导航是「人数看板」和「个人中心」,没有发布通知,也没有参会者名单。看板上方是全会的报名人数、有人报名的论坛数、报名关系和近 24 小时新增。下面每一场一行,行内是该场人数和新增。

分论坛主席的导航多了「发布通知」。看板只列出自己负责的论坛。演示账号是两场,人数分别显示在对应行上。

修改论坛时间从论坛详情进入,只有会议主席能打开。页上是当前北京时间、开始和结束两个输入框、保存按钮,以及下面的修改记录。还没改过时,记录是空的。页上的说明写着:保存后会通知已报名的人,并重新检查他们的日程冲突。

发布通知是一张表:选择论坛、填写标题和正文,然后点发布。下拉只列出自己负责的场次。还没发过时,右侧写「还没有发布过通知」。

发布成功后,表单下方出现「通知已发布。」,右侧「已发布」列出标题、论坛和时间。输入框清空,可以继续发下一条。

秘书的导航是「参会者」和「发布通知」,没有人数看板。名单页可以按论坛筛选,表里是姓名、邮箱、单位和报名时间。

秘书打开发布页时,表和分论坛主席相同。区别在顶栏:这里是「参会者」,不是「人数看板」。

报名了该论坛的参会者打开通知页,能看到刚才那条。导航「通知」带未读数,条目上有「未读」和「标记已读」。点论坛名可以回到该场详情。

11. 基础功能实现
这一节按作业原文写基础功能怎么做出来。
大会论坛和专题论坛是两类「分论坛」,表结构只有一套。类型是字段,不是两套系统。
仓库在 zhihui。全部提交在 commit 列表。口径写在 docs/design.md,对应提交 14587c85。
| 角色 | 能做的事 | 不能做的事 |
|---|---|---|
| 参会者 | 报名、改报名、取消、看自己的通知和日程 | 看人数、导出名单、发通知、改时间 |
| 会议主席 | 看全会人数和每一场人数,改论坛时间 | 替别人报名,发分论坛通知 |
| 分论坛主席 | 看自己绑定论坛的人数,给这些论坛发通知 | 看其他论坛人数,导出全部名单 |
| 秘书 | 看并导出参会者,给绑定论坛发通知 | 看人数看板 |
(1)规则集中在 permissions.ts。页面、动作、接口都调用它,不在业务里直接比 role。提交是 d5315f86。
(2)公开注册只创建参会者。另外三种角色由种子脚本创建。
(3)「注册通过」是表单校验通过。没有审批角色,所以不做人工审核。
会议议程与分论坛信息
功能说明
作业要求网站展示议程和分论坛:议题、主席、召开时间、地点、简介。
实现思路
目录、议程、详情读同一张论坛表。
● 不另存一份议程。改了起止时间,三处一起变。
● 展示用北京时间。冲突判断用同一对开始、结束。
● 没有主席的场次显示主持或嘉宾,不补造人名。没有主题的场次筛选时归入「未分类」。
关键实现
查询在 forums/queries.ts。筛选在 forums/filter.ts。表结构在 schema.ts。数据包说明在 data/cncc2026/README.md。
(1)筛选条件写在地址里。刷新、把链接发给别人,条件还在。
(2)搜索打在名称、主席和嘉宾上。
(3)人员和登录账号分开。展示用官网采集的人名,权限用 forum_staff,演示账号不用真实主席的名字。
运行界面
议程按日期分组,同一天按开始时间排。

详情页给出议题、人员、时间和地点。未登录时可以返回目录或去报名。

目录左侧勾选就是报名入口。下方「我的四天」把已选场次按天收起来。

测试
筛选的单元测试在 filter.test.ts。
注意事项
● 部分报告没有单独的时间。冲突只用这场论坛的整体时段。
● 闭门论坛允许模拟报名,并在页面上标出来。
● 不按「当前日期已经过了会议」关闭报名。真实会议已经结束,关掉就无法演示。
参会者注册、报名与修改取消
功能说明
作业要求:注册时选择一个或多个分论坛。登录后能查看报名,并能修改、取消。
实现思路
注册和第一场报名放在同一次提交里。
不出现「账号建好了,但任何分论坛人数都不加」的空账号。大会人数定义为至少还有一场有效报名的用户,和这条规则一致。
关键实现
写入在 registrations/actions.ts。注册在 auth/register.ts。
(1)直接打开注册地址,会回到目录,并带上注册意图。必须先勾选至少一场。
(2)已登录后再改勾选,要点「保存报名」才写库。
(3)保存时,不在新列表里的有效报名写上 canceled_at,不删除行。新的编号插入,冲突则什么都不做。
(4)同一用户的保存会排队。否则两次并发保存互相看不见,结果会变成两者的并集。
(5)单场「报名这场」靠部分唯一索引。重复点、并发点,都当成已经报名成功。
(6)取消只写取消时间。没有有效报名时也当作成功,避免重复点击报错。
(7)登录前勾的场次,登录后是加到已有报名上。确认之后才保存,不会把原来的报名清掉。
运行界面
个人中心左侧是姓名、邮箱、单位和角色。右侧每一场可以改报名,也可以单独取消。取消的是这一场,不是注销账号。

注册成功后直接进入回执,不用再登一次。回执按日期列出已报名的时间、地点。

测试
输入校验在 registrations/input.test.ts。注册规则在 auth/register.test.ts。登录失败仍保留邮箱的提交是 a3574543。
报名写入的测试窗口:

权限被拒绝时返回拒绝页,不是空白页:

注意事项
● 邮箱冲突靠数据库唯一约束,不先查再插入。
● 登录失败统一说「邮箱或密码错误」,不说明是账号不存在还是密码不对。
● 全部取消之后,这个人不再计入大会人数。
● 主席、秘书、会议主席不能用报名接口给自己报名。
会议主席与分论坛主席查看人数
功能说明
作业的口径:
| 名称 | 怎么数 |
|---|---|
| 分论坛人数 | 注册了这场的用户数 |
| 大会参与人数 | 至少注册一个分论坛的用户数 |
同一个人报了多场,分论坛各行各算一次。大会人数用去重后的用户数,不会因此变成多个人。
实现思路
人数不另存一份计数。每次从「取消时间为空」的报名现算。取消用的是标记而不是删行,近 24 小时新增才能算出来。
关键实现
大会人数在 stats/queries.ts 的 getConferenceAttendeeCount:对有效报名做 count(distinct user_id)。
各场人数在 getForumCounts。传入绑定的论坛编号时,分论坛主席只能看到这些行。近 24 小时是这些有效报名里,创建时间落在 24 小时内的条数。
(1)会议主席打开看板:上面是全会四个数,下面每一场一行。
(2)分论坛主席打开同一地址:查询被限制在绑定论坛。导航里没有「参会者」。
(3)参会者直接敲地址:守卫拒绝,页面写「没有访问权限」。
运行界面



测试
权限用例在 permissions.test.ts。模拟数据在 stats/demo-data.test.ts。
注意事项
● 冲突还没选「去哪一场」时,两场报名都算进人数。关注度不因为还没取舍而少算。
● 演示用的约 200 名模拟参会者密码是随机的,不能当登录账号。脚本再跑不会重复建人。
● 看板和导出都不使用官网主席的真实姓名作为登录用户。
秘书获取参会者信息
功能说明
作业要求秘书能拿到全部参会者信息,交给分论坛主席。
实现思路
名单页只有秘书能进。可以按论坛筛选。导出跟随当前筛选,避免筛了一场却导出所有人。
关键实现
编码在 stats/csv.ts。
(1)文件用 UTF-8,并带 BOM,Excel 打开不乱码。换行用 CRLF。
(2)单元格若以 =、+、-、@ 开头,前面加单引号。全角的同类符号也转义。否则表格软件会当成公式。
(3)含逗号、引号、换行的单元格按 RFC 4180 加引号。
(4)秘书发通知时,下拉仍只列出绑定论坛。能看全部名单,不表示能给任意论坛发通知。
导出转义的提交是 a804ea12。导出跟随筛选的提交是 009fb5c3。
运行界面

测试
测试代码在 stats/csv.test.ts。

注意事项
● 会议主席看不到这个名单页。人数和名单是两种权限。
● 导出的是当前有效报名,已取消的不进表。
分论坛通知与参会者接收推送
功能说明
作业要求两件事。
(1)分论坛主席和秘书发布本论坛的通知。
(2)参会者注册通过并登录后,收到自己所选论坛的信息。
实现思路
通知不按人复制一份。收件箱按「现在还有没有这场的有效报名」查询。
新报名的人看得到这场已经发出的通知。取消之后,这场的新消息不再出现。
关键实现
发布在 notices/actions.ts。查询在 notices/queries.ts。推送在 notices/sse.ts。
(1)先看是否登录,再校验标题和正文。标题最长 120 字,正文最长 5000 字,不允许空字符。
(2)canPublishNotice 不通过就拒绝。分论坛主席和秘书都只能发给绑定的论坛。
(3)写入成功后,向当时在线的连接发一条事件。浏览器订的是 /api/notices/stream。
(4)流上每 25 秒发心跳。响应头要求缓存和反向代理不要把事件攒住再输出。
(5)标记已读只对自己收件箱里的通知有效。不存在和无权看,返回同一种拒绝,不暴露这条通知在不在。
(6)会议主席改时间且新旧不同:同一事务里更新论坛、留下记录,并插入一条中文通知。正文写原时间、新时间,并提醒检查日程。时间按北京时间写,不跟服务器时区走。代码在 forums/time.ts。
(7)新旧时间相同:不写记录,不发通知。
运行界面
发布成功后,右侧列出标题、论坛和时间,输入框清空。

报名了该场的参会者打开通知页,导航带未读数,条目可以标记已读,也可以点回论坛详情。

还没有任何消息时,页面写「还没有通知」,不会混进别的论坛的公告。

测试
推送流的测试在 sse.test.ts。进程内转发在 bus.test.ts。改时间在 time.test.ts。

注意事项
● 未读数只统计当前报名范围,不是全站公告。
● 通知正文只存一份中文。收件人的界面语言可以不同,正文不按语言复制五份。
● 单进程里用内存事件转发。多副本部署时,在线推送不会自动跨进程,收件箱刷新仍能看到已入库的通知。
登录、会话和按角色看到的菜单
功能说明
上面四类人要先登录,系统才知道该给谁看人数、名单和通知。
菜单藏起来不够。直接敲地址也要被同一套规则挡住。
实现思路
会话放在数据库里,不用 JWT。
(1)Cookie 里是随机令牌。库里只存它的 SHA-256。
(2)退出时删掉服务端记录。旧令牌再拿来用也无效。
(3)会话大约 14 天。过期记录在新建会话时清掉。
代码在 auth/session.ts。为什么不用 JWT,写在 design.md。
运行界面
登录只收邮箱和密码。失败提示留在原表单上。


测试
连续失败会限流。登录失败后邮箱还在,提交是 a3574543。

注意事项
● 跳转地址只允许站内。不能被带去站外。
● 密码用单向散列保存,不明文落库。
● 公开注册不能选出会议主席、分论坛主席或秘书。
议程数据怎么进库
功能说明
页面上的 170 场不是手写在页面里的。来自 2026 年 10 月 30 日采集的官网公开数据。
实现思路
导入写进论坛表。目录、议程、详情、推荐目录、人数行,都读这张表。
| 数据里的情况 | 页面怎么处理 |
|---|---|
| 3 场大会论坛、167 场专题论坛 | 同一张表,用类型字段区分 |
| 24 场 Tutorial | 类型上能筛出来 |
| 27 场没有主席 | 显示主持或嘉宾,不编人名 |
| 49 场没有主题 | 筛选归入「未分类」 |
注意事项
● 再次导入不能把已经改过的开会时间盖回旧值。改时间是会议主席的操作,导入只补目录。
● 页脚写明这是课程作业,不是 CCF 官方系统。
首页上的场次是从这张表算出来的:

12. LLM 智能推荐
功能说明
作业要求四件事。
(1)用户用自然语言描述研究方向、兴趣或参会需求。
(2)系统结合已有分论坛,给出推荐,并说明理由。
(3)结果必须是系统里真实存在的论坛,能看详情,能直接报名。
(4)要处理不存在的论坛、无关推荐、接口失败。
实现思路
不用多步 agent。一次调用:输入需求,要求只返回 JSON。
名称、时间、地点只从数据库读。模型可以提供编号和理由,不能提供一个库里没有的标题去给用户报名。
规则和目录放在系统提示的前部,不随每次问句变化,便于服务商做前缀缓存。用户句子放进 JSON 的 user_request 字段。这样句子里的引号和换行跳不出这个字段。
关键实现
| 步骤 | 代码 | 做什么 |
|---|---|---|
| 组提示词 | prompt.ts | 规定 JSON 形状、最多 5 条、相关度低于 0.6 不要返回 |
| 调用与重试 | run.ts | 每家服务商最多两次,超时由外层计时 |
| 校验 | validate.ts | 整段坏了就重试;单条坏了只丢掉这一条 |
| 退路 | fallback.ts | 全部失败后用关键词在目录里找 |
| 设计说明 | design.md | 写明不用 agent 框架,校验放在自己的代码里 |
调用顺序:
(1)按当前界面语言注明理由要用哪种语言。
(2)把目录压缩进系统提示。摘要若在原文就断掉,提示词禁止模型把后半句补完。
(3)外层超时不依赖对方是否响应取消。到点就换下一家。
(4)去掉模型可能包上的 Markdown 代码围栏,再解析 JSON。
(5)通过的条目按相关度排序,最多留 5 条。相关度和「依据摘录」只写日志,不发给浏览器。
(6)浏览器拿到的是论坛编号。详情页、报名按钮和目录用的是同一个编号。
(7)每次调用在 llm_requests 留一行。退回本地匹配时再留一行。错误信息写入前,把类似密钥的片段打掉。
失败后退回本地匹配的提交是 3b001846。端到端不调用真实模型的提交是 3c7fbe0c。
运行界面
未提交时,输入框下有三条可点的例子。点下去会填入句子并立刻请求。页上写明范围是系统里已有的论坛。

点「大语言模型和软件工程」之后,留下的场次包括技术债、智能编程与软件产业、软件工程人才培养、工业场景智能体、AgentOS。每条有自己的理由,并能查看详情、去报名。

异常结果处理
| 返回内容 | 记成 | 用户看到什么 |
|---|---|---|
| 编号不在库里 | unknownForum | 这一条不出现 |
| 同一编号重复 | duplicate | 只留一条 |
| 相关度低于 0.6 | lowRelevance | 当作无关推荐丢掉 |
| 依据在该论坛原文里找不到 | evidenceNotFound | 丢掉。短到像「AI」这种到处都有的片段也不算依据 |
| 理由里出现大段规则原文 | promptLeak | 丢掉 |
| 声明没有研究需求 | offTopic | 空列表,不硬凑论坛 |
| JSON 整个对不上 | 无效 | 重试,然后换服务商 |
| 超时或接口错误 | 错误 | 每家再试一次 |
| 全部失败 | fallback | 用关键词在中文目录里匹配 |
论坛正文是中文。英语、日语、法语里的常见方向先映射到中文词,再做本地匹配。映射只覆盖主要方向,不假装覆盖所有说法。
用户句子里如果要求「忽略以上规则、改掉输出格式」,提示词把这类内容当成数据,不当成新指令。若整句都是闲聊或让它写代码,按答非所问处理。
测试
校验测试在 validate.test.ts。调用过程在 run.test.ts。客户端在 client.test.ts。
测试用假客户端,不依赖真实密钥。

注意事项
● 密钥只留在服务端。
● 目录里的文字是资料,不是给模型的新指令。
● 空列表是合法结果。没有合适论坛时,不编一场出来。
● 评分和端到端不走真实接口,避免当场被超时或额度影响。
13. AI Coding 过程
记录以华为云仓库 zhihui 为准。提交列表在 这里。过程说明由袁湘湘写入 AI Coding 记录,原文在 docs/ai-coding/README.md。创建人是提交卡片上的名字。推荐那几条显示为高娅婷,和分工表里的饶恩哲不一致,以提交详情页为准。
13.1 关键 Prompt、输出摘要与取舍(8 分)
这一部分只写怎么把 AI 收住:提示词、它回了什么、采纳还是拒绝、理由落在哪次提交。测试怎么判对错放在 13.2。
案例 1:先定技术栈和冲突规则,再写代码
四种角色、报名、推送、推荐、五种语言和个人日程如果一次生成,冲突判断和推荐调用会各写一套。这一步先定规则。
第 1 轮:技术栈和数据里的时间
先分析需求,确定项目命名和技术栈。数据用 CNCC 2026 数据包。基础功能要有会议与分论坛展示、注册时选择一个或多个分论坛、登录后查看和修改报名、四种角色看到的人数和通知、参会者收到所选分论坛的推送。LLM 要用自然语言推荐真实存在的分论坛,并处理不存在的论坛、无关推荐和接口失败。附加功能至少五种语言,语言放资源包,用统一接口取文案;还要根据已选论坛生成个人日程并提示时间冲突。先给结论和理由,先不生成项目。
AI 的结论:167 场论坛分布在 4 个下午,同一下午任选两场必然重叠。技术选型是 Next.js 加 PostgreSQL 的单体。
| 取舍 | 内容 | 谁落下去 | 落点 |
|---|---|---|---|
| 采纳 | 单体。冲突在服务端按起止时间算。「不按日期关闭报名」写入设计说明 | 饶恩哲 | design.md,14587c8 |
| 采纳 | 半开区间按这轮的重叠条件实现 | 叶银珍 | conflicts.ts,eb48023 |
| 采纳 | 按单体搭骨架 | 饶恩哲 | 8648e4c |
| 拒绝 | 推荐拆成独立服务。人数、报名和推送要读同一份数据 | — | — |
| 拒绝 | 脚手架默认的 Geist 没有中文字形,构建还要联网下载 | 饶慧改成系统字体 | a784078 |
核对:
(1)用数据包复核「四个下午、同一下午必重叠」。
(2)把「不按日期关闭报名」和冲突规则写入设计文档。
这一轮对话和技术栈结论

design.md 里对应的两段

第 2 轮:推荐不用 Agent
LLM 推荐需要用 Agent 框架吗?推荐要能打开论坛详情并直接报名,异常结果要有明确去处。
| 取舍 | 内容 |
|---|---|
| 采纳 | 一次调用。目录放进系统提示,用户原话只作为数据。后面的接口不再加工具循环 |
| 拒绝 | Agent 和多轮工具调用。推荐没有第二步必须由模型选择的工具 |
核对:页面一次返回就能打开真实论坛并报名。
这一轮对话

留下的规则
| 规则 | 做法 |
|---|---|
| 两场刚好相接 | 结束时刻等于开始时刻,不算冲突 |
| 半开区间 | startA < endB 且 startB < endA |
| 新增、取消、改选 | 按当前选择重算,不另存一份会过期的日程 |
| 主办方改时间 | 下次打开日程时按数据库里的新时间再算 |
| 推荐 | 一次模型调用 |
案例 2:界面先出可选择的稿,再写页面
只写「把比例处理好」,布局、编号和冲突文案会一起被换掉。
第 1 轮:用真实论坛出三份方向
选定现在这个界面方向。先用数据包里的真实论坛渲染 3 个方向,标出桌面和手机上的比例。先不要改业务代码。
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | 先看三份稿,选定【填写方向名称】后再写页面 | 饶恩哲把取舍写入说明,191a463 |
| 拒绝 | 跳过比较,直接生成页面 | — |
核对:三份稿都用数据包里的论坛,桌面和手机都能读出名称、时间和地点。
采纳的方向:账本

半开区间的最终代码

这一轮对话

第 2 轮:按选定方向实现,再用截图改细节
按选定方向实现。界面比例和细节处理一下。实现后在桌面宽度和手机宽度各截一套。
截图里的六处:
(1)筛选栏占 3 行。
(2)会议室名称换行混乱。
(3)选中行盖住面板边框。
(4)手机上吸顶筛选栏太高。
(5)标题排在操作区后面。
(6)出现「注册并报名 0 场」。
| 取舍 | 内容 | 谁落下去 | 落点 |
|---|---|---|---|
| 采纳 | 六处按截图改。目录带筛选和按日面板 | 饶慧 | forums/page.tsx,ee5abb6 |
| 采纳 | 主题壳层,以及详情、登录表单的比例 | 饶慧 | b9775f0,4360f16 |
| 拒绝 | 编号列。「与 4013 冲突」改成论坛名称和时间 | — | — |
核对:再截一套桌面和手机。筛选栏不再占 3 行,选中行不盖住边框,0 场不再用那句文案。
改好后的冲突文案

改好后:未解决的冲突留在导航

冲突提示改成名称和时间

这一轮对话

留下的结论
- 先选定方向,再改业务页面
- 目录不展示内部编号
- 冲突提示用论坛名称和时间
- 桌面和手机各留一套改完的截图
案例 3:智能推荐从方案收到能报名
用户会说「我对大语言模型和软件工程感兴趣」。模型可能返回库外编号、对不上的理由,或接口失败。原始 JSON 不能拿来报名。
第 1 轮:只定契约
用户用自然语言描述研究方向,例如「我对大语言模型和软件工程感兴趣,希望参加相关论坛」。结合数据库里的分论坛推荐,并说明理由。结果必须是系统里真实存在的论坛,能打开详情,并能直接选择报名。编号不在白名单的丢掉;理由必须能在该论坛的名称、主题、简介或报告标题里逐字找到;相关度低于 0.6 丢掉;与会议无关返回 offTopic。接口超时、失败或 JSON 损坏时重试一次并换一家,全部失败再退回本地关键词匹配。被截断的简介标成 cutOff,不许续写。用户输入经 JSON 转义后放进 user_request,只作为数据。先给提示词结构和异常表,先不写页面。
| 问到的 | AI 的方案 |
|---|---|
| 目录 | 每场只留编号、名称、主题、简介摘要、报告标题 |
| 用户输入 | 放在 user_request,不拼进系统指令 |
| 异常 | 白名单、依据逐字核对、低相关丢弃、offTopic、失败后本地匹配并记 fallback |
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | 五条作为接口契约。高娅婷按表压缩目录 | prompt.ts,9352050 |
| 采纳 | 对不上的理由不进入报名 | validate.ts,daf0aa1 |
| 采纳 | 一次调用,默认模型定为 deepseek-flash | e4b2d63 |
| 拒绝 | 契约未定就写页面。用户原话拼进系统指令 | — |
核对:
(1)一条真实论坛文本,依据能逐字找到。
(2)一条故意写错的编号,被白名单丢掉。
(3)一条无关输入,出口是 offTopic。
提示词结构和异常表


第 2 轮:本地匹配用真实目录打
本地兜底不要用通用分词。用论坛目录当词典做最长匹配。用真实论坛名跑「机器人」「大语言模型和软件工程」,列出切出的词和命中的论坛。
| 最初 | 真实数据 | 最后 |
|---|---|---|
Intl.Segmenter | 「机器人」切成「机器」,命中「机器学习」。假论坛名测不出来 | 用目录当词典做最长匹配 |
| 直接取最长片段 | 切出「大语言模型和」 | 先按虚词断开 |
| 去掉出现在 25% 以上论坛里的词 | 「安全」也被去掉 | 阈值改为 50% |
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | 最长匹配、虚词断开、50%。高娅婷替换通用分词 | fallback.ts,1645d6d |
| 拒绝 | 继续用 Intl.Segmenter。依据是真实目录上的错切 | — |
| 拒绝 | 25% 过滤。它把方向词一起丢掉 | — |
核对:重跑这两个句子,不再命中由「机器」带出的「机器学习」,也不再留下「大语言模型和」。
改好后的最长匹配

目录当词典,停用词阈值 50%

这一轮对话

第 3 轮:异常结果进入页面
推荐策略不动。补齐三种返回:编号不存在;编号存在但理由在论坛文本中找不到;两家接口都失败。留下的论坛要能打开详情并报名。offTopic 和全部失败时页面上要有明确文案,不能显示空白或原始 JSON。
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | 三种返回都进接口。失败先换一家,再本地匹配 | 3b00184 |
| 采纳 | 留下的论坛进入现有详情和报名 | recommend/page.tsx |
| 拒绝 | offTopic 或全部失败时展示原始 JSON,或给出打不开的编号 | — |
| 修改 | 【填写改过的字段、文案或阈值,以及那一次返回】 | — |
核对:
(1)库外编号不出现报名按钮。
(2)理由对不上的编号不出现报名按钮。
(3)两家都失败时,出现本地匹配或明确失败文案。
成功推荐,含理由、详情和报名

offTopic

被白名单或依据校验拦下

这一轮对话

留下的结论
- 一次调用,用户原话只在
user_request - 理由必须能在论坛文本里逐字找到
- 失败先换一家,再退回本地最长匹配
- 页面不展示原始 JSON
案例 4:报名冲突的提示和登录后的选择
页面能算出冲突,但用户看不出该点哪一个按钮。只写「把提示和动效做得直观一点」,文案、动画和报名参数会一起被换掉。重叠仍用案例 1 的半开区间。
第 1 轮:先走真实流程
先不要改代码。用真实账号走完注册、选择分论坛、看到冲突、改掉其中一场、再看日程。桌面和手机各留一套截图,列出哪一句提示、哪一个按钮让人不知道下一步。
AI 指出操作反了:页面写「选择要参加的一场」,按钮却是「移出日程」。选完后绿色主按钮落在不去的那场上,再点一次冲突又出现。
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | 先改文案和主按钮,再做动效。一次只保留要参加的那场 | 叶银珍,a641425,日程页 schedule/page.tsx |
| 采纳 | 多场重叠算成一组冲突 | 9c67bb3 |
| 拒绝 | 问题没列出就改样式。主按钮留在不去的那场上 | — |
核对:用截图对照按钮文字和冲突是否还在。
改好后:主按钮是“去这场”

主按钮落在要保留的那场

这一轮对话

第 2 轮:三种引导做成可点的原型
针对上一轮列出的问题,做 3 种可点击的引导原型,动效要能看。先不要写进正式页面。
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | 「步骤与回执」:先指出冲突,改完用回执列出留下的论坛 | 袁湘湘,232817d |
| 拒绝 | 另外两种【填写名称】。理由:【回执能对上最终场次】 | — |
核对:原型第一版回执因为动画没有显示,自动测试却全部通过。看截图才发现列表是空的。正式页面不把「测试通过」当成动画正确。
采纳的回执页

回执列出留下的报名

这一轮对话

第 3 轮:登录后追加选择,不覆盖旧报名
登录后选的冲突呢?已选论坛要带到登录后的页面。检查保存时会不会把这个用户原来已经报名的论坛取消。
AI 的两点:登录链接丢掉已选论坛;用 selected 整份带过去,会取消已有报名。应改成 add,只追加这一次。
| 取舍 | 内容 | 落点 |
|---|---|---|
| 采纳 | add 只追加,并补「选择还在」「旧报名不被取消」 | 叶银珍,actions.ts,977deb0 |
| 采纳 | 未解决的冲突仍出现在导航 | 4e2ba50 |
| 采纳 | 登录后改冲突的测试 | 袁湘湘,0b7f03f |
| 拒绝 | 用 selected 整份覆盖 | — |
| 拒绝 | 「目录页冲突提示可以不改」。目录和日程用同一份当前选择 | — |
核对:
(1)已有报名的账号带 add 登录并保存。
(2)旧论坛还在,新论坛加上。
(3)目录上已解决的冲突消失,未解决的仍在。
add 和保存后的报名列表

目录和日程上的同一组冲突

这一轮对话

留下的结论
- 重叠仍按半开区间
- 主按钮只在准备保留的那场
- 改完有回执,登录用
add追加 - 目录和日程同时更新
- 英、法、日界面另查过。饶慧改了英语和法语的数量写法,0d645e8
改完的桌面和手机日程

英、法、日同一页

13.2 检查、测试与是否采纳(6 分)
这一部分不改 13.1 里的方案和提示词。每一轮仍是四块,互不并成一段:
(1)我的提示词,和上面案例里的对话保持原句。
(2)AI 的回复,包括它把失败归到测试还是归到代码。
(3)怎么分开:看失败点落在选择器、残留数据、断言读错区域,还是落在产品行为。
(4)凭哪一次运行听不听:写明是哪一次复现、哪一次重跑,以及听了还是不听。
对话卡片用 zhihui博客/AI-Coding截图。并行时实际 205、串行后再通过、返回前后的页面,这些当时的对比图没有留下。下面贴的是改好之后的代码和测试。
四个任务是分开做的。页面和查询先做完,写操作留成函数签名固定的存根,再拆成任务 A 到 D,各开一个 worktree、一个会话和一个 PR。主会话只审核和合并。每个任务的提示结构相同:负责 docs/parallel-tasks.md 里的那一项,先执行 setup-worktree,再读架构、设计文档和共同规则,完成后向 main 提 PR。四个 PR 合并时没有代码冲突。单独全过,不代表合在一起也过,所以下面先写合并后的重跑。
案例 5:合并之后、真实调用和导入的核对
四个 PR 各自写着测试全过,也都没有和其他任务一起跑过。主会话在单独的克隆上按顺序合并,自己重跑全部检查,并补了各 PR 单独测不到的场景。袁湘湘把审核写入 8128e49。
第 1 轮:人数差了 3,先别改计数
大会人数这条断言

改好之后的配置

我的提示词:
按合并顺序在一个单独的克隆里重跑全部端到端。大会人数期望 208、实际 205 时,先查是不是别的任务同时在写报名。不要先改产品里的计数。
AI 的回复:
差值可以解释成大会人数漏计 3 人。大会人数是有效报名里去重后的用户数。若有 3 个用户的报名在统计时没被算进这一个查询,结果就是 205。按漏计去改计数公式,能把这条断言补平。
怎么分开:
失败日志里,其他任务的用例同时在同一套库上插入和取消报名。205 不一定是公式少算了 3 人,也可能是别的用例把人写进来又取消掉,这条断言读到了中间状态。失败点落在「别的用例还在写」,不是计数公式对不上数据包。所以这一轮判成测试隔离的问题,不是产品问题。
凭哪一次运行:
- 并行那一次实际是 205,只能说明当时库里的人数和期望不一致,不能据此改公式。
- 端到端改成串行后再跑,同一条断言通过,计数代码没有改。
- 听的是串行通过的这一次,不听「按漏计改产品」。
现在的配置写在 playwright.config.ts:workers: 1,注释写明用例共用一个数据库,大会人数和未读数都是全局数字,并行写入会互相影响。袁湘湘的签入是 e037dc2。同一份配置还写了另一件相关的事:不设 E2E_BASE_URL 时,测试先 pnpm build 再 pnpm start,因为开发服务器的按需编译和热更新会打断页面操作。默认不调用真实 LLM,结果才确定。
第 2 轮:浏览器返回后,地址变了,列表没有变
返回后目录和筛选还在

改好之后的返回

我的提示词:
在合并后的版本上复现:打开论坛目录,进入一场详情,再按浏览器返回。若地址变了而列表没有变,沿
ForumBrowser的效果和replaceState查。修完用同一步骤再走一遍。
AI 的回复:
在合并后的版本上走了一遍。论坛目录筛选停在 10 月 21 日,进入「智能体记忆:理论、系统与应用」的详情,再按浏览器返回。地址已经回到目录,列表仍是详情。这不是某一个并行任务带来的合并冲突,main 上原来就是这样。ForumBrowser 的效果比 Next 接管 history 更早执行。返回时它用原生 replaceState 改地址,把 Next 的路由状态清掉。地址栏是目录,页面还停在详情,进入详情前的筛选也回不来。
怎么分开:
用合并后的版本、原来的点击路径就能复现。地址变了、内容不变,说明路由状态被清掉。测试要求「按浏览器返回后列表也是目录」,这条期望是对的,不能改成避开浏览器返回。所以这一轮是代码错了。补测试时还碰到同一类页面问题,也按「先别改测试让它过」分开:
(1)看完详情返回时选择丢失,是 Next 复用了缓存页面,组件拿到旧 props,改为从当前地址恢复状态。
(2)注册跳转的 intent 被地址同步冲掉,刷新后提示消失。
(3)按钮样式的链接被组件库标成 role="button",屏幕阅读器读不出是链接,于是新增 LinkButton。
这三项是页面的问题。登录限流偶发失败则是测试的问题:提示文字每轮相同,断言立刻通过,下一轮填写时恰好被表单重置清空,改为等表单重置完成,连续 3 轮才算通过。
凭哪一次运行:
复现成功的那一次决定要改代码。调整效果和路由状态的顺序之后,用同一步骤再走:地址回到目录,列表也是目录,10 月 21 日的筛选还在。听的是这一次,测试里没有改成避开浏览器返回。饶慧的签入是 f1023a5。
第 3 轮:未读数是 3,未登录打开异常参数返回 500
未读数和 NUL

我的提示词:
新写的跨任务用例期望未读数是 1,实际是 3。先查上一条用例留下的通知。再用未登录身份打开
/register?forums=%00,记录实际状态码。不要沿用「只有工作人员能触发」这句。
AI 的回复:
有两句,而且第一句和后面实测的方向相反。未读数不一致时,它先把原因说成未读计算。读注册地址的注释时,还沿用过「NUL 只有已授权工作人员能触发」。
怎么分开:
这两件事要拆开看,不能因为同一次审核失败就都改产品。
(1)未读数。库里能对上上一轮跨任务用例留下的通知,那一轮发布了通知,没有清掉。当前账号读到 3 条未读,其中 2 条是残留。这是测试没清理,计算公式不用动。
(2)NUL。注释里的那句和一次未登录请求对不上。未登录就能走到这个异常参数,不是工作人员接口。所以「只有工作人员能触发」是读代码时的误判,要按实测改接口,而不是改测试去绕开 500。
凭哪一次运行:
- 清掉那两条残留后再跑,未读数是 1,和这条用例的期望一致,计算代码没动。听清理后的这一次,不听「改未读」。
- 未登录打开
/register?forums=%00的那一次,实际状态码是 500。听这一次,500 在合并前修掉,不留在主分支上。
袁湘湘补的用例是 c6ab744。高娅婷按这次 500 拦住 NUL,1346236。
第 4 轮:配置文件里的库不是实际连上的库
实际连接串

我的提示词:
核对这几轮端到端实际连的是哪一个库。不要只看克隆目录里的环境文件。若
seed:demo出现created=0,或主库计数发生变化,列出实际的连接来源。
AI 的回复:
测试跑在 zhihui_wt9,依据是克隆里的环境文件,并向外面报告过「在独立数据库上跑」。
怎么分开:
环境文件和进程实际的 DATABASE_URL 不是同一个库。当时的 shell 导出了指向主库的连接串,dotenv 和 --env-file 都不覆盖已经存在的环境变量。那几轮端到端因此写进了主库。「独立库」是报告错了,不是产品把报名写错了表。
凭哪一次运行:
新建空库后 seed:demo 打出 created=0 的那一次。这个数字不合理,才改去查实际连接串。查库之后,测试账号和通知已经清理,217 条论坛和全新导入完全一致;留下的是 200 个模拟参会者、16 条 fallback 日志和登录会话。听的是 created=0 和随后的查库,不听环境文件上的库名。之前「已经隔离」的说法要更正。饶恩哲写明覆盖关系,544df5a,更正隔离说法的是 ca85864。后面案例 4 那种全量测试,也是在确认过的独立数据库上跑,并前后核对主库计数没有变化。
第 5 轮:推荐只统计真正调用过的结果
两遍真实调用

我的提示词:
用同一组 15 条真实需求调用
deepseek-flash两遍。另外准备 4 条无关或注入输入。列出成功数、被白名单或依据校验拦下的条目、本地匹配的命中数。没有 Key 的供应商不要写成已测试。
AI 的回复:
以及它对自己两个失败单测的判断:
| 它给出的结果 | 它的判断 |
|---|---|
| 两遍 14/15、15/15 | 可以作为接口结果 |
4 条无关或注入都是 offTopic | 出口正确 |
| 校验拦下 3 条:2 个不存在的编号,1 条找不到的依据 | 校验生效 |
| 本地匹配更严标准 9/15 | — |
| 「灵巧」「人形机器人」两个单测失败 | 两次都说是断言写错,并改了断言 |
怎么分开:
调用日志和单测失败不是一件事。日志用来核对推荐出口,标准偏松,不能把 15/15 写成「理由都很强」。单测要再看失败是断言写错,还是分词写错。假论坛名的单元测试发现不了「机器人」被切成「机器」、再命中「机器学习」。两个失败里,AI 都先改了断言,这个动作本身不能当作结论。
凭哪一次运行:
(1)两遍真实调用的日志留下 14/15 和 15/15。4 条无关或注入都是 offTopic。校验在真实调用里拦下 3 条。人工又读了一遍理由,记下几条偏弱的推荐,不改成全部强相关。高娅婷的脚本是 7cd4611。
(2)「灵巧」对过切词规则,断言和规则不一致。听「断言写错」,改测试。
(3)「人形机器人」后来用真实目录跑出错误切分。这一次推翻「断言写错」,测试保留,改匹配。最长匹配也不再直接取最长片段,先按虚词断开,因为真实结果里出现过「大语言模型和」。停用词阈值从 25% 改到 50%,因为 25% 把「安全」这样的方向词也滤掉了。
(4)没有 Key 的供应商没有调用记录。Anthropic 和 OpenAI 没有写成已测。真实宕机没有遇到,失败路径只算假客户端那一次。
PR 正文里的三句话,也是用三次实跑核对,不直接信正文:
- 查 DeepSeek 的
/models确认模型名。 - 经过 Caddy 伪造
X-Forwarded-For,第 11 次仍被限频。 - 生产镜像上走完注册、推送、取消,并调用真实 DeepSeek。
合并后另外定了两条:注册接口按 IP 限流;改论坛时间后,不按差值平移报告时间,那样是在补造信息。
第 6 轮:再次导入,主席改过的时间还在
导入数字和再次导入

我的提示词:
把导入结果和
data_quality.json逐项核对。然后手动改一场论坛的时间,再执行一次导入,看这场时间还在不在。forum_people和agenda_items应用不会修改,可以重建;已经存在的forums要跳过。
AI 的回复:
导入脚本若整表覆盖,重新部署时会把会议主席改过的时间写回数据包里的旧值。
怎么分开:
覆盖写入是导入脚本的问题,不是主席改时间的接口写错。日期也要单独看。new Date("2026-02-30T...") 会滚到 3 月,只靠正则和 isNaN 拦不住,这是解析代码的问题,不是测试把合法日期判错了。模拟姓名的上限如果按「组合总数减去保留姓名数」来算,官网人员大部分不在组合里,会误报数量不够,这是算法错了。权限测试若用 toContain("forbidden"),可能匹配到无关文本;没有对照请求时,请求格式不对也会显得「被拒绝」。
凭哪一次运行:
(1)导入后的 SQL 和 data_quality.json 逐项对上,见下表。
(2)手动改一场论坛的时间后再导入,这场修改仍然保留。所以采纳「已经存在的 forums 跳过」。forum_people 和 agenda_items 应用不会改,可以整体重建。李润君的签入是 f0c0a59。本地从空库拉起过一次完整环境,迁移、导入、应用和 Caddy 依次起来,健康检查返回 200;改一条数据后重启,修改还在。这一次还没在真实服务器上再做。
(3)服务器时区设为 America/Los_Angeles 再跑端到端,解析不随服务器时区变。不存在的日期改成解析后按北京时间格式化回去比较。模拟姓名改成先生成姓名池再过滤。权限断言收紧,并加了会议主席的对照请求。
(4)改论坛时间后,详情只提示报告时间可能没有同步,不按差值去平移。陈林祺的签入是 5a94cd7。
| 项目 | 数量 |
|---|---|
| 大会论坛 | 3 |
| 专题论坛 | 143 |
| Tutorial | 24 |
| 其他活动 | 47 |
| 无主题 | 49 |
| 无主席 | 27 |
| 报告缺时间 | 318 |
案例 6:登录限流测试连错三次
现象是登录限流的端到端一直失败,而且每轮报错不一样。三次失败不收成一个原因。每一轮先看 AI 把原因归到哪里,再用下一次运行决定听不听。会话放在数据库里,不放 JWT:退出和角色变更要这一次立刻生效,每次请求本来就要读角色。Playwright 覆盖登录流程和 4 种角色的访问矩阵,无权限时断言真实的 403,并验证退出后旧令牌重放无效、next 参数不能跳到外部网站。
第 1 轮:页面上有两个 alert
选择器命中了两个节点

改好之后的测试

我的提示词:
登录限流的端到端测试失败。不要改产品来迁就测试。先看失败页面上到底有几个提示节点,再判断是选择器写错,还是限流没有生效。
AI 的回复:
产品没有改。失败页面上有两个 role="alert"。一个是登录错误提示,另一个是 Next.js 自带的路由播报。限流已经生效,页面上有错误文案。测试失败是因为选择器匹配到两个节点。它把这 2 项归到测试。
怎么分开:
失败信息是「匹配到两个元素」,不是「限流次数不对」,也不是「错误文案没出现」。页面上能看见登录错误,说明限流这条产品路径是通的。路由播报是框架自己的节点,关掉它会改掉无障碍播报,不能为了让选择器唯一就关。
凭哪一次运行:
选择器改成只取登录错误那一条,再跑。原来那 2 项匹配到两个元素的失败消失了,还剩 1 项,失败点不再是选择器命中了两个节点。听这一次:限流实现不动,路由播报也不关。限流留在许雅欣的 login-throttle.ts,ddbde02。角色矩阵仍断言真实 403,4c92da6。
第 2 轮:输错密码后,邮箱被清空
先写一条修复前必须失败的测试

我的提示词:
还剩这一项。先写一条修复前必须失败的测试,确认失败原因是输错密码后邮箱被清空。确认之后再改产品。
AI 的回复:
React 19 在表单动作结束后重置表单。服务端返回错误时,用户已经输入的邮箱被清空。它把这一项归到产品,并说明这是测试顺带发现的真实体验问题。
怎么分开:
如果只在测试里重填邮箱,限流文案还在,用户仍要重新输入。那是把产品问题藏进测试。先写一条修复前必须失败的测试,失败信息应是邮箱为空,而不是限流文案找不到。这条测试在修改前失败,才能说明改的是产品,不是断言放宽了。
凭哪一次运行:
这条新测试在修改前失败。许雅欣把邮箱从 action 状态填回 login-form.tsx 之后,同一条通过。签入 a357454。听的是「修复前失败、修复后通过」这一对。会话放数据库,退出后旧令牌立即失效,session.ts,de1b005。
第 3 轮:前两项已经解释过,整组结果仍会变
对生产构建连续跑 3 轮

我的提示词:
前两项已经解释过,整组结果仍会变化。不要再改选择器和表单。先查每次是不是都在冷启动开发服务器,再改成对生产构建跑,连续 3 轮。
AI 的回复:
前两轮的修改已经留下。剩余波动来自每轮冷启动开发服务器,报错也不一致,不是限流次数算错。
怎么分开:
报错每轮不一样,和「两个 alert」「邮箱被清空」这两项已经对上的失败点对不上。再改限流阈值,没有一条稳定的失败信息可以对照。开发服务器每次冷启动、按需编译,会把同一步操作做成不同的超时或不同的文案。
凭哪一次运行:
改成测试生产构建之后,连续 3 轮,18 项都通过,报错不再变化。听的是这 3 轮,不是某一次偶然变绿。袁湘湘改的是运行方式,b5a3037。配置里对应的句子是:开发服务器的按需编译和热更新会打断页面操作,结果不稳定。
第 4 轮:失败时仍被提交,以及测不到的校验
脚本退出码、容器和浏览器气泡

这一轮没有新的功能提示词。检查的是脚本输出,以及后来问过「注册什么的提示都优化了吗」之后,AI 回去查代码的结果。它没有直接回答「优化了」,而是列出 6 处还没改的地方,确认后才动手。
AI 的回复:
脚本没有判断测试退出码,测试失败时仍做了提交。另一次它说登录页没有注册入口;改代码时才看到底部有链接,当场更正。它还把一轮淡色错误文字说成对比度问题。
怎么分开:
(1)失败仍提交,是脚本没看退出码,不是那次测试通过了。当时还没推送,不计入通过次数。
(2)Docker 那一次,zsh 不拆分变量,容器根本没启动。日志里的失败不是产品失败,那一批不能算数。
(3)上一轮写在输入框下方的错误,正常使用时基本显示不出来。浏览器自带的校验先拦住提交,气泡语言跟随浏览器。原先的用例为了走到服务端,主动关掉浏览器校验,所以一直没有暴露。这是测试把产品问题遮住了。加了页面上的检查之后,原来的「服务端校验」用例已经到不了服务端,继续通过也没有意义。
(4)登录页底部的链接是有的。真正的问题是那个链接会丢掉已选的论坛,不是「没有入口」。
(5)英语和法语写着 “1 forums”。第一轮截图里错误文字很淡,关掉动画重拍后,确认只是截到淡入中途,不是对比度。
凭哪一次运行:
容器重新启动并通过的那一次才计数。服务端校验改成关掉脚本运行,并确认请求真的发到了服务端。限流文案另用很低的额度启动服务看过。5 种语言都看了截图,包括之前没看过的繁体中文。许雅欣让错误跟着页面语言,74e137a。袁湘湘的测试和记录是 9069a36、36162f8。
案例 7:报名保存的锁,和会假通过的断言
普通端到端是绿的。这一段检查的是绿灯能不能发现方案里的问题。叶银珍除了常规端到端,还逐个破坏实现做变异测试,确认测试真的能发现问题。对应并行任务 A。
第 1 轮:排队锁和外键检查
两个连接复现出 40P01

改好之后的锁

我的提示词:
实现保存报名。并发保存时不能写出互相覆盖的结果。给出锁的写法,并说明它会不会和已有外键检查互相等待。先不要用测试通过代替这个说明。
AI 的回复:
保存报名放在一个事务里。先按论坛编号从小到大锁论坛行,再按报名编号锁已有的报名行,然后写入。两个并发请求抢的是同一批行,顺序固定,不会互相把对方的新结果盖掉。锁如果写成 FOR UPDATE,会和已有的外键检查互相等待。插入报名时,外键要在论坛行上取 FOR KEY SHARE。FOR UPDATE 和 FOR KEY SHARE 冲突。一个连接按这个顺序锁住论坛行,另一个连接做外键检查,PostgreSQL 返回 40P01。
怎么分开:
普通端到端没有踩中这个时序。那些测试通过,不能用来说明没有死锁。死锁要看数据库返回的错误码,不能看页面上的成功提示。
凭哪一次运行:
两个数据库连接按这个加锁顺序复现,得到 40P01 的那一次。听这次复现,不听原来的端到端通过。实现里不用 FOR UPDATE,改成 FOR NO KEY UPDATE。它仍按固定顺序锁住论坛行和报名行,不和外键的 FOR KEY SHARE 冲突。说明写进 PR:死锁时序由两个连接复现,端到端没有覆盖,没有改成「测试已覆盖」。代码在 actions.ts,签入 9550e54。PR 说明是 c63ada3。
第 2 轮:写完是否还要刷新缓存
去掉 revalidatePath 之后断言不变

我的提示词:
检查每次保存后的
revalidatePath是不是必须。用变异测试:去掉这句再跑原来的断言,看结果变不变。
AI 的回复:
去掉之后结果不变。页面保存成功后已经调用 router.refresh()。
怎么分开:
若去掉后断言变化,这句就是产品需要的刷新,要加回去。若去掉后不变,留下它只会让人以为列表刷新依赖这句。变异测试就是为了看测试能不能发现「这句被拿掉」。
凭哪一次运行:
去掉这句的那一次,报名列表、人数和冲突提示的断言与去掉前一致。听这一次,删除 revalidatePath。签入 b947444。
第 3 轮:断言查的是主内容区,冲突在侧栏
侧栏上还有冲突时,旧断言仍通过

我的提示词:
详情页有一条「页面不含冲突」的断言。检查它读的是主内容区还是侧栏。若冲突只出现在侧栏,这条断言算不算假通过。
AI 的回复:
冲突状态在侧栏,断言查的是 <main>。侧栏仍有冲突时,这条断言也会通过。
怎么分开:
断言通过,同时侧栏仍显示冲突,就是测试读错区域。不能把旧断言的通过理解成冲突已经消失,也不能为了让旧断言继续过,把侧栏上的提示删掉。
凭哪一次运行:
故意留下一场冲突。旧断言通过,改成查侧栏的新断言失败。这一对结果决定改断言。页面按新断言改完后,新断言才通过。叶银珍的签入是 1986d66。报名主路径端到端是 81efd8d。
第 4 轮:全量突然失败,先别算到上一轮头上
库里多出来的那条主席报名

我的提示词:
全量端到端突然失败。先不要把原因定为刚刚删除的
revalidatePath。查测试库里这次多出来的报名是谁留下的。
AI 的回复:
第一反应是删除 revalidatePath 造成人数或列表不一致,也就是上一轮改代码改坏了。
怎么分开:
若是删除刷新造成的,库里不会多出一条特定报名,失败会稳定地落在「刷新没发生」。若库里有变异测试留下的行,失败来自测试数据,把刷新加回去也不会让这条脏数据消失。
凭哪一次运行:
查库看到变异测试「去掉权限校验」留下的一条会议主席报名。清掉后再跑,全量恢复。期间没有把 revalidatePath 加回去。听查库和清理后的这一次,不听第一反应。
案例 8:通知推送的三条技术判断
第一稿能在单进程里收到事件。换到 Server Action 和 Route Handler 同时运行时收不到。注释里有一句未经测量的缓冲说明。已读测试会偶发超时。对应并行任务 B。这里每轮都先写 AI 的技术判断,再写哪一次实跑决定听不听。
第 1 轮:推送里只放信号
两个账号的未读数

我的提示词:
做分论坛通知的实时更新。事件里不要放标题和正文。说明未读数在哪里算,页面怎样只更新当前用户有权看到的通知。
AI 的回复:
分论坛通知用 SSE 做实时更新。事件里只放通知编号和未读数,不放标题、正文,也不放可点的论坛链接。事件只是一个信号,不是另一份通知内容。未读数在服务端算,只算一处。页面收到事件后调用 router.refresh(),再按当前登录用户查询。权限也只在服务端算。
怎么分开:
若事件里带标题和正文,没有报名的账号也可能在事件流里看到内容。只看「有报名的那一个页面刷新了」,证明不了权限。要用两个账号看未读数。
凭哪一次运行:
同一次发布后,没有报名这场的账号未读数不动,有报名的账号未读数增加。听这一次,事件里不放标题、正文和链接。陈林祺的推送在 sse.ts,签入 c557a08,发布在 actions.ts。
第 2 轮:订阅放在模块里,还是放在 globalThis 上
改回模块内变量就收不到

改好之后的订阅

我的提示词:
现在发布之后另一个页面收不到。检查订阅是放在模块里,还是两个入口各有一份模块。给出能复现「收不到」的步骤,再改存放位置。
AI 的回复:
存根注释建议用模块内的 EventEmitter。Server Action 和 Route Handler 会被打成两份模块,发布写进一份,页面订的是另一份。改回模块内变量后,推送失效。
怎么分开:
单进程里能收到,不能证明两份模块也能收到。要故意改回模块内变量,看「收不到」是否出现。出现了,才是代码把订阅放错了地方,不是测试没有等到事件。
凭哪一次运行:
改回模块内变量的那一次,发布后另一个页面收不到。改到 globalThis 上的 Set 之后,经过 Caddy 读原始响应,点击发布约 46 ms 收到事件。听这一来一回,不听「单进程所以没问题」。陈林祺的签入是 c8c5174,代码在 bus.ts。
第 3 轮:注释里的 gzip,和 curl 看到的响应
text/event-stream 没有被压缩

改好之后的测试

我的提示词:
注释写 Next.js 的 gzip 会缓冲 SSE。用 curl 请求
text/event-stream,看响应有没有被压缩、事件是一次到齐还是被攒住。注释按测量结果改。
AI 的回复:
gzip 会缓冲事件,所以需要 no-transform。
怎么分开:
注释里的原因,和响应头要不要留,是两件事。原因靠原始响应。响应头可以因为别的理由留下。用「注释这么写」来证明压缩存在,是把说明当成了测量。
凭哪一次运行:
curl 的那一次。text/event-stream 没有被压缩,事件也没有攒到连接结束。不听「gzip 会缓冲」。no-transform 留下,注释改成:实测这种响应不会被压缩;保留这个头是为了中间层不再改写。陈林祺的签入是 3822d3f。直播、权限和响应头的测试是 d064542。标签页隐藏时会断开连接。通过 IP 访问走 HTTP/1.1,同一站点只有 6 个连接,每个标签页占一个 SSE,会卡住其他请求。这是连接数的限制,和 gzip 那句不是同一个结论。
第 4 轮:连接数,以及已读测试为什么超时
连接建立得比取消报名晚

我的提示词:
检查两件事。第一,标签页一直占着 SSE 时,同一站点还能不能打开别的请求。第二,已读权限测试偶发超时,先看超时发生时连接是在取消报名之前还是之后建立的。
AI 的回复:
通过 IP 访问走 HTTP/1.1,同一站点 6 个连接,每个标签页占一个 SSE 会堵住其他请求。超时那一次,它先认为产品把按钮弄丢了。
怎么分开:
(1)连接数是运行环境的限制。标签页一直占着连接,其他请求会被堵住。这要改连接生命周期,隐藏时断开,不是把测试里的标签页数改少来躲开。
(2)超时要看日志里的先后。若 SSE 在取消报名之后才连上,页面正确地刷新了,按钮随之消失。测试还在等那个按钮,就会超时。这是测试等早了,不是按钮被产品弄丢。
凭哪一次运行:
超时日志里,连接建立晚于取消报名的那一次。听这一次,不改按钮,只改测试等待时机。陈林祺的签入是 9d2d604。通过与否另看两组数:经过 Caddy,发布后约 46 ms 收到事件;连续 3 轮各建立并断开 50 个连接,订阅数都回到 0。
14. 数据库设计
● 知会的数据放在 PostgreSQL 17。
● 表、枚举、外键和索引都写在 src/lib/db/schema.ts,由 Drizzle 生成迁移 drizzle/0000_init.sql。
● 页面和接口只通过查询函数读写,不在各个路由里各写一套建表语句。
● 连接串只来自环境变量 DATABASE_URL。
● 本地容器和线上容器用同一份迁移,结构一致,数据卷各自独立。
能由数据库拒绝的写入放进约束;会随报名和改时间变化的结果不另存一张“结果表”。人数、收件箱和冲突每次按当前行计算,避免两处数字各改各的。
这一节对应李润君的表结构和迁移,以及饶恩哲的数据库容器。
● 空库执行迁移之后,要能建出论坛、报名和通知;论坛目录可以反复导入,已经改过的起止时间不能被数据包盖回去。
● 表结构的签入是 c51fcb87,第一版迁移是 55433a8b。
● 本地数据库容器是 536f0795。
| 序号 | 内容 |
|---|---|
| (1) | 六张图按账号、目录、报名通知、改时间与推荐日志、总体关系、数据流来画。 |
| (2) | 实体名和基数跟 drizzle/0000_init.sql 一致。 |
| (3) | 属性不画进框里的长列表,完整列在后面的字段字典。 |
图 14-1 账号与会话
一个用户可以有多条会话。会话主键是令牌哈希,不是用户编号。用户删除时会话级联删除。

图 14-2 论坛目录
一场论坛对应多名展示人员、多场报告,也可以绑定多个职员账号。展示人员和职员账号是两条线。

图 14-3 报名、通知与已读
报名连接用户和论坛。通知连接论坛和作者。已读连接用户和通知。人数只数 canceled_at 为空的报名。

图 14-4 改时间与推荐日志
改时间留下旧值和新值。推荐日志留下原始返回和状态。两张表都不参与人数。

图 14-5 总体关系
十二张表放在一张图上。没有“冲突”这张表。

联系与基数
| 甲 | 乙 | 基数 | 含义 |
|---|---|---|---|
| users | sessions | 1 对多 | 可同时多端登录,退出删对应行 |
| users | registrations | 1 对多 | 一人可报多场,也可反复取消后再报 |
| forums | registrations | 1 对多 | 一场可有多人;有人报名时论坛删不掉 |
| users | forums | 多对多 | 经 forum_staff,表示负责而不是报名 |
| forums | forum_people | 1 对多 | 展示用人员,可零条(没有主席时改显示主持) |
| forums | agenda_items | 1 对多 | 报告可零条,时间可空 |
| forums | notices | 1 对多 | 通知不按收件人复制 |
| users | notices | 1 对多 | 作者;RESTRICT |
| users + notices | notice_reads | 多对多 | 联合主键,一人一条通知最多一行已读 |
| forums | forum_time_changes | 1 对多 | 每次修改追加,不覆盖上一笔 |
| users | llm_requests | 1 对多,可零 | 未登录或用户已删时 user_id 为空 |
图 14-6 四条数据流
外部进来的东西有四类:官网数据包、参会者提交的注册、主席或秘书写的通知、参会者的一句话。箭头上写的是流动的数据,不是再起一张表。
● 导入把活动、人员和报告写入目录三表,不覆盖已经改过的起止时间。
● 报名要先读目录,确认论坛编号存在,再写入 users 和 registrations。
● 发布只往 notices 插一行。收件箱不另建表,用当前有效报名去读这张表。
● 推荐要读目录,丢掉库里没有的编号,调用记录写入 llm_requests。
冲突和人数没有自己的数据存储,每次按报名和目录的当前行计算。

线上论坛详情和个人中心用来核对“目录表”和“有效报名”是否就是页面上那几列,截图放在本节末尾,与第 4 章是同一批。
(1)先分清三类数据
题目里的“分论坛”“主席”“参会者”很容易被收成一张宽表。知会拆开,是因为这三类数据的生命周期不一样。
(1)目录数据来自官网采集,可以整批重导。 2026 年 10 月 30 日的公开数据进 forums、forum_people、agenda_items。活动一共 217 条。is_registrable 为真的是可报名分论坛,开幕式等只出现在议程里。首页上的 170 场可报名、3 场大会论坛、167 场专题论坛,是从这些行数出来的,不是写死在页面上。大会论坛和专题论坛用同一张 forums,用 type 区分,对应题目里“两类分论坛可以用同一种数据结构”。
(2)账号数据由注册和管理脚本产生,不能随目录导入被覆盖。 users 存邮箱、口令哈希、姓名、单位和角色。公开注册只能得到 attendee。会议主席、分论坛主席和秘书由 scripts/seed-demo.ts 创建,对应提交 2d4230f1。is_demo 用来标出演示账号和模拟参会者,真实注册的人不带这个标记,导出和看人数时可以分辨。
(3)关系数据在使用过程中增长,重导目录时必须留着。 报名、通知、已读、改时间、推荐日志都属于这一类。所以导入脚本更新论坛的名称和简介,却不去改已经由会议主席写过的 start_at / end_at。这件事记在 f0c0a59e。数据包本身是 a1cad4ce。
(1)官网标注的主席、主持、嘉宾进 forum_people,只供详情页展示。
(2)27 场没有主席时就显示主持或嘉宾,不补造人名。
(3)登录后的分论坛主席、秘书是否能发通知,看的是 forum_staff。
(4)两张表故意没有外键相连:展示姓名不等于系统账号,演示账号也不借用真实主席的名字。
(5)权限判断因此不会在“同名不同人”上出错。
(2)每张表解决哪一条需求
| 表 | 一行是什么 | 对应的需求 |
|---|---|---|
users | 一个可登录的人 | 参会者注册;四种角色 |
sessions | 一次登录 | 退出后立即失效 |
forums | 一场活动 | 议题、时间、地点、简介、能否报名 |
forum_people | 官网标注的一位人员 | 详情页上的主席、主持、嘉宾 |
agenda_items | 论坛里的一场报告 | 议程;部分报告没有时间 |
forum_staff | 某账号负责某论坛 | 分论坛主席和秘书的权限 |
registrations | 一次报名,取消后仍保留 | 报名、修改、取消、人数 |
notices | 挂在某论坛上的一条通知 | 主席、秘书发布;参会者接收 |
notice_reads | 某人读过某条 | 未读和已读 |
forum_time_changes | 一次起止时间变更 | 主办方改时间后可追溯 |
llm_requests | 一次推荐调用 | 异常结果可对照原始返回 |
forums.id 用数据包里的稳定编号,例如 CNCC2026-4036,不用自增整数。推荐结果、报名和通知都引用这个编号。模型如果返回一个库里没有的编号,校验直接丢掉,不需要再做一次“名字模糊匹配才算命中”。source_activity_id 另有唯一约束,重复导入时能找到同一条活动,而不是再插一行。
agenda_items.start_at 允许为空。设计说明里写过:部分报告没有时间,冲突检测只用论坛整体时段。若按报告时间做冲突,空时间会被当成“不冲突”或被补成整场,两种都会和页面提示不一致。报告表因此只服务详情和议程展示。
● 时间列统一是 timestamptz。
● 会议在成都举行,改时间的页面按北京时间解析后再写入,库里保存的是带时区的时刻。
● 比较两场是否重叠时比的是时刻,不比字符串,也不受服务器系统时区是 UTC 还是东八区的影响。
(3)报名表为什么不删除行
取消报名如果执行
DELETE,每天新增人数就只能看“现在还在的行”,取消过的人从历史里消失。
● 知会在 registrations 上保留 created_at 和 canceled_at。
● 取消是更新 canceled_at,不是删行。
● 有效报名的定义是 canceled_at IS NULL。
| 序号 | 内容 |
|---|---|
| (1) | 同一人不能对同一论坛同时有两条有效报名,否则人数会重复。 |
| (2) | 取消之后又要允许重新报名,所以不能在 (user_id, forum_id) 上建普通唯一约束——那样取消行还在,重新插入会被拒绝。 |
| (3) | 迁移里用的是部分唯一索引,见 drizzle/0000_init.sql: |
CREATE UNIQUE INDEX "registrations_active_unique"
ON "registrations" ("user_id", "forum_id")
WHERE "canceled_at" IS NULL;
in_schedule 和“有没有报名”分开。两场时间重叠时,两行都算报名人数;用户可以只把其中一场放进个人日程,把另一场的 in_schedule 设为 false。页面文案是“不去,报名保留”。冲突本身不写入任何表。每次打开日程,用当前有效且 in_schedule 为真的论坛,按半开区间 [start_at, end_at) 计算。前一场的结束时刻等于后一场的开始时刻,不算冲突。会议主席改的是 forums 上的时间,并追加一行 forum_time_changes。下一次计算读到的就是新时间,不需要另做缓存失效。数据里本来没有首尾相接的论坛,这种边界靠改时间来演示,规则仍写在计算里,而不是等数据里碰巧出现才处理。
人数的口径也落在这张表上,查询在 src/lib/stats/queries.ts,看板上线的提交是 e210232a。
● 分论坛人数:该 forum_id 上 canceled_at IS NULL 的行数。同一人有历史取消行时只计当前这一条。
● 大会参与人数:至少有一条有效报名的用户数。一个人报了多场只计一次。全部取消之后不再计入,和设计说明一致。
● 不建每日汇总表。汇总表要在报名、取消、重新报名三处一起改,漏一处就会和名单对不上。名单导出读的也是有效报名,见 src/lib/stats/csv.ts。
(1)有效行上的两个部分索引专门服务这两类查询:registrations_forum_active_idx 按论坛数人数,registrations_user_active_idx 按人取出“我的报名”和日程。
(2)历史取消行不进这两棵索引,列表变长之后仍只扫当前报名。
(4)通知为什么不按人复制
● 如果每发一条通知就给每个已报名的人插一行,后报名的人看不到此前的通知,取消报名的人还要把那些行删掉或标记失效。
● notices 只挂在论坛上,作者是 author_id。
● 收件箱用“这个人当前有效报名了哪些论坛”去连接通知。
● 新报名的人能看到该论坛已经发出的历史通知;取消之后这条连接断开,不再出现在收件箱里,也不再走 SSE。
● 已读单独记在 notice_reads,主键是 (user_id, notice_id),重复标记已读不会多出一行。
notice_kind 只有 normal 和 time_change。改时间会写 forum_time_changes,并给当时已经报名的人发一条改时间通知。通知正文只存一份中文。收件人的界面语言可以不同,但论坛内容和这类系统通知都不按语言复制多行,避免同一次改时间在库里变成五条。
删除通知作者时用
ON DELETE RESTRICT。
● 人还挂着已发布的通知时,不能把账号直接删掉,否则通知会失去作者或变成无法追溯的空账号。
● 改时间记录的 changed_by 同样是 restrict。
(5)会话、推荐日志和删除规则
| 序号 | 内容 |
|---|---|
| (1) | 登录不用 JWT。 |
| (2) | Cookie 里是随机串,sessions.token_hash 存它的 SHA-256,并且就是主键。 |
| (3) | 数据库文件泄露不能直接拿令牌去冒充。 |
| (4) | 退出登录删掉这一行,下一次请求立刻失败,不必等令牌自然过期。 |
| (5) | 用户被删除时,会话级联删除。 |
llm_requests 记下查询原文、服务商、模型、状态、耗时、原始响应和解析后的 JSON。状态是枚举 ok、invalid、error、fallback。invalid 表示返回里有对不上 forums.id 的项,页面不展示这些项。error 是调用失败。fallback 是改走本地关键词匹配。原始响应留下,是为了事后区分“模型说了不存在的论坛”和“校验把那一条丢掉了”。用户删除后 user_id 置空(ON DELETE SET NULL),调用记录仍在。
其余删除规则按“删了会不会把已经发生的事抹掉”来定:
| 关系 | 删除时 | 原因 |
|---|---|---|
| 用户 → 会话、报名、已读、职员绑定 | CASCADE | 账号不在了,这些行没有独立含义 |
| 论坛 → 人员、报告、通知、改时间、职员绑定 | CASCADE | 附属数据跟着论坛走 |
| 报名 → 论坛 | RESTRICT | 还有报名时不能删论坛,人数依赖这条外键 |
| 通知作者、改时间操作者 | RESTRICT | 保留“谁发的、谁改的” |
| 推荐日志 → 用户 | SET NULL | 人可以删,排错记录留下 |
forums 上还有检查约束 end_at > start_at。结束等于开始的论坛写不进去,半开区间才不会变成空区间。49 场没有主题时 theme 为空,筛选归入“未分类”,不按标题猜测后填进这一列。猜出来的主题会让筛选和推荐目录同时偏掉。
(6)和代码、测试的对应
查询和导入没有把规则散落在页面组件里。
● 结构:src/lib/db/schema.ts、src/lib/db/index.ts、drizzle.config.ts
● 导入:scripts/seed-catalog.ts,说明见 data/cncc2026/README.md
● 报名写入:src/lib/registrations/actions.ts
● 冲突计算:src/lib/schedule/conflicts.ts,测试 src/lib/schedule/conflicts.test.ts
● 推荐校验:src/lib/recommend/validate.ts,测试 src/lib/recommend/validate.test.ts
● 设计说明里的口径:docs/design.md
(1)模拟参会者约 200 人,重复执行不重复建人,提交是 83f91b9f,测试是 src/lib/stats/demo-data.test.ts。
(2)推荐查询在进库之前会拒绝空字符,提交是 1346236c,避免一种在日志里看不见、统计时又会截断的字符进 llm_requests.query。
● 第 4 章里的论坛详情、我的报名和个人日程,分别对应 forums + forum_people + agenda_items、有效的 registrations,以及按 in_schedule 现算的冲突。
● 表结构不在那些截图里重复贴出。
命名和类型约定
先写命名、类型和时间字段,再写每张表,避免十二张表各用一套缩写。
| 对象 | 约定 | 例子 |
|---|---|---|
| 表名 | 小写复数,单词之间用下划线 | forum_time_changes |
| 字段 | 小写,时间以 _at 结尾 | start_at、canceled_at |
| 主键 | 论坛用数据包编号;账号、报名、通知用 uuid | forums.id = CNCC2026-4036 |
| 枚举 | PostgreSQL 枚举,不在字段里写自由字符串 | user_role、llm_status |
| 时刻 | timestamptz,比较的是时刻 | 半开区间按时刻算 |
| 布尔 | 能用是否表示的不用 0/1 | is_registrable、in_schedule |
| 索引名 | 表名加用途 | registrations_active_unique |
支持软件是 PostgreSQL 17 与 Drizzle。
● 结构的唯一来源是 src/lib/db/schema.ts。
● 迁移脚本 drizzle/0000_init.sql 由它生成,不手改出第二套表。
概念上的四块
下面四块对应后面的字段表。一块里的表一起导入或一起保留,不跨块覆盖。
| 块 | 表 | 谁写入 | 重导目录时 |
|---|---|---|---|
| 身份 | users、sessions | 注册、登录、演示脚本 | 保留 |
| 目录 | forums、forum_people、agenda_items | seed-catalog.ts | 更新文案;已改时间不覆盖 |
| 使用 | registrations、notices、notice_reads、forum_time_changes、forum_staff | 页面操作 | 保留 |
| 调用日志 | llm_requests | 推荐流程 | 保留 |
(7)字段字典
下面只列业务上要核对的列。完整 CREATE TABLE 以迁移脚本为准,不在博客里再抄一遍全部 SQL。
users
| 列 | 空 | 说明 |
|---|---|---|
id | 否 | 主键,gen_random_uuid() |
email | 否 | 唯一,登录名 |
password_hash | 否 | 口令哈希,不明文 |
name | 否 | 显示名 |
affiliation | 可 | 单位 |
role | 否 | attendee / conference_chair / forum_chair / secretary,默认参会者 |
is_demo | 否 | 演示账号和模拟参会者为真 |
created_at | 否 | 注册时间 |
forums
| 列 | 空 | 说明 |
|---|---|---|
id | 否 | 数据包编号,推荐和报名都引用它 |
source_activity_id | 否 | 唯一,重复导入时认出同一条活动 |
name | 否 | 论坛名称 |
type | 否 | plenary / thematic / other |
is_tutorial | 否 | 是否 Tutorial |
theme | 可 | 官网没有主题就空着,筛选里叫未分类 |
start_at / end_at | 否 | 检查约束:结束晚于开始 |
venue | 可 | 场馆 |
room | 否 | 会议室 |
description | 否 | 简介 |
is_registrable | 否 | 能否报名;开幕式等为假 |
is_closed_door | 否 | 闭门标记,仍允许模拟报名 |
source_url | 否 | 官网出处 |
registrations
| 列 | 空 | 说明 |
|---|---|---|
id | 否 | 主键 |
user_id | 否 | 外键 users,用户删除则级联 |
forum_id | 否 | 外键 forums,有报名时论坛不能删 |
in_schedule | 否 | 默认真;假表示报名保留但不进日程 |
created_at | 否 | 用来看新增,不随取消消失 |
canceled_at | 可 | 空表示当前有效 |
notices 与 notice_reads
| 列 | 空 | 说明 |
|---|---|---|
notices.forum_id | 否 | 通知属于哪一场 |
notices.author_id | 否 | 作者,删除用 RESTRICT |
notices.kind | 否 | normal 或 time_change |
notices.title / body | 否 | 只存一份正文 |
notice_reads.user_id + notice_id | 否 | 联合主键,重复已读不会多行 |
notice_reads.read_at | 否 | 第一次标已读的时刻 |
sessions 与 llm_requests
| 列 | 说明 |
|---|---|
sessions.token_hash | 主键。Cookie 里是随机串,库里是 SHA-256 |
sessions.expires_at | 到期时刻。退出时直接删行,不等到这一列 |
llm_requests.status | ok、invalid、error、fallback |
llm_requests.raw_response | 模型原文,页面展示的是校验之后的结果 |
llm_requests.result | jsonb,解析后的结构 |
llm_requests.user_id | 可空。人删了置空,日志留下 |
forum_people 的 role 是 chair、co_chair、host、co_host、guest。forum_staff 没有角色列,能不能发通知由账号自己的 users.role 加上“是否绑定这场”一起判断。agenda_items 的起止时间可空。forum_time_changes 同时留下旧的和新的起止时间,以及 changed_by。
(8)完整性、索引和保密
必须由库拒绝的写入
- 论坛结束时刻不晚于开始时刻。
- 同一邮箱两个账号。
- 同一人、同一论坛、两条都还没取消的报名。
- 报名指向不存在的论坛,或通知指向不存在的作者。
- 角色、论坛类型、通知种类、推荐状态写成枚举以外的字符串。
故意不算进库的结果
| 结果 | 放在哪里算 | 为什么不建列 |
|---|---|---|
| 两场是否冲突 | src/lib/schedule/conflicts.ts | 改时间或改日程后旧标记会过期 |
| 分论坛人数、大会人数 | src/lib/stats/queries.ts | 报名、取消、再报名要改三处 |
| 收件箱里有哪些通知 | 有效报名连接 notices | 后报名的人要看到历史通知 |
索引
| 索引 | 类型 | 给谁用 |
|---|---|---|
registrations_active_unique | 部分唯一 | 挡住重复的有效报名 |
registrations_forum_active_idx | 部分 | 数一场的人数 |
registrations_user_active_idx | 部分 | 我的报名、日程 |
forums_start_idx | 普通 | 议程按时间 |
forums_theme_idx | 普通 | 按主题筛 |
notices_forum_created_idx | 普通 | 某论坛通知按时间 |
sessions_user_idx | 普通 | 按人清理会话 |
llm_requests_created_idx | 普通 | 按时间看失败 |
不分区。217 场活动和课程规模的报名用不上分区表。也不为人数建触发器,避免触发器和查询各写一种口径。
保密
● 口令只存哈希。会话只存令牌哈希。
● 公开注册写不出主席或秘书角色。
● 名单导出在 src/lib/stats/csv.ts 里转义可能被表格软件当成公式的单元格,提交 a804ea12,测试 src/lib/stats/csv.test.ts。
● 推荐原文里的空字符在进 llm_requests 之前拒绝,提交 1346236c。
● 数据库口令只在服务器 .env,不进镜像,也不进迁移脚本。
备份靠生产卷 db_data。重新构建应用容器不删这个卷。要清空必须单独删卷,部署命令里没有这一步。
(9)需求对照
| 题目里的话 | 落在哪 | 怎么验收 |
|---|---|---|
| 展示议题、主席、时间、地点、简介 | forums + forum_people + agenda_items | 第 4.1 节详情页 |
| 注册时选一场或多场,可改、可取消 | registrations | 第 4.4、4.5 节 |
| 分论坛人数、大会人数 | 有效报名的两种计数 | 查询在 stats/queries.ts;线上主席成功态未截到,见第 4 章说明 |
| 秘书拿全部参会者 | 同一张有效报名,导出 CSV | 参会者打开会拒绝,第 4.6 节 |
| 发布通知,报名者收到 | notices 按当前报名连接 | 第 4.4 节通知页;推送见第 13 章 |
| 推荐必须是真实论坛 | 结果里的编号必须在 forums.id | validate.ts 与第 4.3 节 |
| 改时间后日程重查 | 更新 forums,追加 forum_time_changes,冲突不入库 | conflicts.test.ts |
| 序号 | 内容 |
|---|---|
| (1) | 首页上的 170、3、167 来自目录行,不是写在前端的常数。 |
| (2) | 对应提交 9e675c68。 |
| (3) | 详情页读到的就是上表这些列: |

我的报名读的是 canceled_at 为空的行。下图是线上已保存的一场,和“取消不删行、有效行才显示”是同一条规则。

(10)谁在读写这些表
页面不直接连库。能碰到表的入口只有三类:三条 HTTP 路由、一组服务端动作、启动时跑一次的脚本。语言切换只改 Cookie,不写数据库。
三条 HTTP 路由里,只有健康检查不看登录。导出和推送都先查
sessions,再决定能不能读报名或通知。
| 入口 | 方法 | 读 | 写 | 谁能调用 |
|---|---|---|---|---|
| /api/health | GET | select 1 | 无 | 部署检查,不看角色 |
| /api/attendees/export | GET | users、有效 registrations | 无 | 秘书;?forum= 只导出一场 |
| /api/notices/stream | GET | sessions、有效报名、未读数 | 无 | 已登录参会者 |
(1)导出编号无效时返回 404,不会退回去导出全部。
(2)CSV 带 Cache-Control: no-store。名单是个人信息,不留在浏览器或代理缓存里。
(3)推送只发信号,不把通知正文放进事件。浏览器收到后再按权限查 notices。
(4)连接打开很久之后,每次推送都重新核对会话和当前报名。取消报名之后,这条连接不再收到该论坛的信号。
服务端动作都在 src/lib 里,文件链到 CodeArts 的 main。
| 动作 | 文件 | 碰到的表 |
|---|---|---|
| 登录、退出 | src/lib/auth/actions.ts | 读 users;写入或删除 sessions |
| 注册 | src/lib/auth/register.ts | 同一事务写 users 和 registrations |
| 保存、单场报名、取消、去这场 | src/lib/registrations/actions.ts | registrations;先读 forums 确认能报名 |
| 发布、标已读 | src/lib/notices/actions.ts | notices、notice_reads |
| 改论坛时间 | src/lib/forums/actions.ts | forums、forum_time_changes、一条改时间通知 |
| 推荐 | src/lib/recommend/actions.ts | 读目录;写入 llm_requests |
● 目录页、详情、议程只读 forums、forum_people、agenda_items。查询在 src/lib/forums/queries.ts。
● 人数和名单在 src/lib/stats/queries.ts。看板不另开一张 HTTP 表。
● 收件箱在 src/lib/notices/queries.ts。
没有这些接口:
(1)没有公开的建表、改表接口。结构只走 drizzle/0000_init.sql。
(2)没有按人复制通知的写入接口。
(3)没有把冲突结果存进表的接口。
(4)没有让参会者把自己改成主席的接口。公开注册只插入 attendee。
(11)事务、锁,以及库里没有的东西
一次报名或一次改时间,不能写成两个互相看得见一半的提交。
| 事务 | 放在一起的写入 | 为什么锁 |
|---|---|---|
| 注册 | 新用户和第一批报名 | 账号建出来却一场都没报上,大会人数会对不上 |
saveRegistrations | 取消不在新名单里的行,再补上新行 | 同一人两次保存要排队,否则两边互相看不到,结果变成并集 |
chooseForum | 只改这一场的 in_schedule | 和保存报名用同一把用户行锁,避免和并发保存死锁 |
updateForumTime | 更新 forums,追加改时间记录,写一条通知 | 锁住这一行论坛,两位主席同时改时,旧时间仍然准确 |
● 锁用的是 FOR NO KEY UPDATE,不用 FOR UPDATE。后者会挡住报名写入时的外键检查,和并发报名形成死锁。说明写在 src/lib/registrations/actions.ts。
● 改时间的事务提交之后才推送。推送失败不会把已经写好的时间和通知滚回去。
● 时间字符串按北京时间解析后再入库。比较冲突时比的是 timestamptz,不比表单上的文本。
库里故意没有的对象:
(1)没有视图。人数、收件箱、冲突都在查询函数里算。
(2)没有触发器。避免触发器和查询各写一种人数。
(3)没有分区。217 场活动用不上。
(4)没有存储过程。规则留在 TypeScript 里,测试能直接调用。
(5)setLocale 不进库。五种语言不复制五份通知正文。
脚本和页面分开:
| 脚本 | 何时跑 | 会不会盖掉线上数据 |
|---|---|---|
| scripts/seed-catalog.ts | 生产迁移容器里,每次部署 | 不覆盖已改的论坛起止时间 |
| scripts/seed-demo.ts | 需要演示账号时单独跑 | 重复执行不重复建人 |
连接只从环境变量 DATABASE_URL 来,入口是 src/lib/db/index.ts。本地容器和线上容器用同一份迁移,数据卷各自一份。
(12)连接、会话寿命和进库之前的拒绝
能在进库之前拒绝的请求,不拿约束报错当页面提示。约束仍在,用来挡住绕过页面的写入。
| 运行事实 | 取值 | 写在哪 |
|---|---|---|
| 连接池上限 | 10 | src/lib/db/index.ts |
| 开发热更新 | 连接放在 globalThis | 同一文件,避免每次重载再开一组连接 |
| 会话有效期 | 14 天 | src/lib/auth/session.ts |
| 过期会话 | 登录时删掉 expires_at 已过的行 | 查询会话时也要求未过期 |
| 权限 | 应用里判断,库不做行级安全 | 四种角色不是四个数据库用户 |
(1)PostgreSQL 里只有一个业务用户 zhihui。主席和秘书的差别在 users.role,不在数据库账号。
(2)因此导出接口自己查角色。只靠“连得上库”不能导出名单。
(3)迁移记在 Drizzle 自己的迁移表里。重复部署不会把 0000_init 再执行一遍。
动作失败时返回的是代码,不是数据库原文。和表有关的几条:
| 代码 | 什么时候 | 还没写进表 |
|---|---|---|
notLoggedIn | 没有有效会话 | 是 |
forbidden | 角色或绑定不够 | 是 |
invalidInput | 编号、正文或空字符不合格 | 是 |
forumNotFound | 论坛不能报名,或库里没有 | 是 |
invalidTimeRange | 结束不晚于开始 | 是;forums 上的检查约束是第二道 |
rateLimited | 推荐太密 | 是;不记入 llm_requests |
● 唯一索引冲突不应该成为正常路径。保存报名先在事务里取消旧行,再插入,避免同一人同一论坛两行有效报名。
● 推荐调用失败或结果无效时,仍然可以写入 llm_requests,状态是 error、invalid 或 fallback。这是日志,不是又一次报名。
这一节从这里往前,表、约束、图、字段、接口、事务和连接都对上了仓库里的实现。再往下是页面怎么少绕路,不再加新的表。
15. 用户体验
(1)这一节写的是操作是否少绕路。
(2)依据是 docs/design.md 里的界面决定。
(3)下面的画面来自已部署站点 https://zhihui.worldboot.net/,与第 4 章是同一批截图,这里按操作步骤再排一次,方便对着页面看决定。
● 167 场专题论坛都在四天的 13:30–17:30。
● 同一下午任选两场,时间必定重叠。
● 不冲突的选法最多七场:三个上午的大会论坛,加上四个下午各一场。
● 目录、注册和日程都围绕“在必冲突的下午里做取舍”,而不是先把 170 场展示完再让人自己记。
能在当前页完成的不先跳走;会让人重填的失败留在原地;冲突只提示,不替用户取消报名。
这条约定落在三处代码上,页面只是它的结果:
| 约定 | 代码 | 提交 |
|---|---|---|
| 注册和选论坛在同一张表 | src/app/register/page.tsx | 232817d6 |
| 冲突只改日程,不删报名 | src/lib/schedule/conflicts.ts | 0b7f03f6 |
| 五种语言走资源包 | src/i18n/locales.ts | acb118b6 |
端到端是否真的走出这三步,看 e2e/guided-registration.spec.ts 和 e2e/registration.spec.ts。
● 页面文案的取舍记在 docs/design.md 的「引导和动效」。
方便和快捷
评分里这一项看的是少绕路,不是多做装饰。知会把「快」落在少一次点击、少一次重填、少一次自己对时间。
(1)找一场。 170 场不用先翻完整份议程。日期、主题、会场、类型和关键词都在目录页,筛选栏吸顶。搜索实现在 src/lib/forums/filter.ts,测试在 src/lib/forums/filter.test.ts。
(2)报一场。 未登录勾选后直接进注册,不用先建账号再回来找。已登录要再点保存,避免误触改掉已有报名。保存逻辑在 src/lib/registrations/actions.ts。
(3)撞时间。 主按钮是「去这场」。点一下只改日程,两场报名都还在,不必先取消再重选。半开区间的测试是 src/lib/schedule/conflicts.test.ts。
(4)让系统先筛。 推荐页给出三个现成例子,点一下就填进输入框,不用从空白开始打字。校验在 src/lib/recommend/validate.ts,页面是 src/app/recommend/page.tsx。
(5)失败不重来。 登录错了留下邮箱;注册错了留下已填内容。错误写在框下面,一改就消失,不跟浏览器气泡抢位置。登录页在 src/app/login/page.tsx。
同一次操作里,人要记住的只有「选哪场」和「去不去」。人数、冲突、收件箱都由当前报名算出来,不用人再对一遍。
下图是这条路径的步骤,不是线上截图。线上每个步骤的画面在第 4 章。

(1)目录里就能做完选择
| 序号 | 内容 |
|---|---|
| (1) | 未登录时打开的首页、分论坛目录、议程和详情都在第 4.1 节。 |
| (2) | 目录一次列出可报名论坛,按日期、主题、会场、类型和关键词筛选。 |
| (3) | 每行有日期、起止时间和地点,左侧就是报名用的复选框。 |
| (4) | 未登录时勾选后进入注册;已登录时要再点保存才写入,避免随手勾选就改了已有报名。 |



(1)宽屏上筛选栏和日期栏吸顶,往下翻不用回到顶部才能改条件。
(2)窄屏把主操作放在底部栏。
(3)会议室去掉重复的场馆前缀,一行能看完地点。
(4)编号列从初稿里拿掉了:CNCC2026-4036 这种编号对选论坛的人没有用,留给数据库和推荐校验。
(5)49 场没有主题时归入“未分类”,筛选里能找到,不把它们藏进某个猜出来的主题。
页面下方「我的四天」按日期收起已选论坛,空闲的一天直接标出来。四天的下午是冲突最密的地方,按天看比按一张长表看更容易发现“这天下午已经占了”。
(2)第一次报名知道自己在哪一步
● 170 场的目录容易让人不知道勾完之后发生什么。
● 引导方案定成“选论坛 → 创建账号 → 查看回执”,签入是 232817d6。
●



注册页旁边列出将要报名的论坛,创建账号之后会发生什么是看得见的。
● 注册和保存之后给一张回执,写明下一步,而不是跳到个人中心后没有任何反馈。
● 注册页、回执和「我的报名」的画面在第 4 章后半,从注册表单到已选两场的那几张。
| 序号 | 内容 |
|---|---|
| (1) | 从登录页再去注册时,已经勾过的论坛还在,提交是 c237fa94。 |
| (2) | 没登录时先选了论坛再登录,选择会加到已有报名上,确认后才保存,不能在这次确认里把以前的报名取消掉。 |
| (3) | 这是为了避免“去登录一下,回来发现原来的报名没了”。 |
(1)注册至少选一场。
(2)之后可以全部取消,全部取消后这个人不再计入大会人数。
(3)报名不按“今天是否已过会议日期”关闭:真实会议在作业时已经结束,若按日期关闭,演示时无法报名。
(4)这个决定写在设计说明里,页面上就不会出现一个永远点不了的“已截止”。
● 错误提示不走浏览器自带的气泡。
● 气泡的语言跟浏览器走,可能和页面五种语言不一致,还会挡住页面自己的句子。
● 错误写在输入框下面,一改就消失。
● 登录失败时整句是“邮箱或密码错误”,不写在某一个框下面,避免告诉别人这个邮箱有没有注册。
●

登录失败清空密码、留下邮箱;注册失败保留已填内容。登录相关画面在第 4 章的登录页和错误密码那一张。
(3)冲突提示不替人做决定
紫色只表示时间冲突,不用来做普通按钮。
● 文案写“与另外 N 场冲突”,不写论坛编号。
● 目录里“与已选论坛同时段”只在鼠标移上去时出现:同一个下午有几十场,每行都写会把已经选上的真正冲突盖住。




| 序号 | 内容 |
|---|---|
| (1) | 解决冲突时两场并排,主按钮是「去这场」。 |
| (2) | 较早的一版主按钮是“移出日程”,并且落在人不想留下的那场上,审查真实流程后改掉。 |
| (3) | 点「去这场」只改 in_schedule,两场报名都还在,人数不变。 |
| (4) | 已经决定不去的那场,再报第三场同时段论坛时不再参与比较,但可以随时改去。 |
| (5) | 另一场取消报名之后,被拿掉的那场不会自动回到日程,要自己点「放回日程」。 |
| (6) | 自动放回去会在人没有看时间的情况下改日程。 |
(1)一场在另一场结束的时刻开始,不算冲突。
(2)数据里没有这种首尾相接,要由会议主席改时间来出现,改完日程按新时间重算。
(3)报告级时间不参与这次计算。
(4)详情页会提示:改了论坛时间之后,报告时间可能没有跟着平移,因为官网没有给出新的报告时间,平移就是补造。
● 冲突、回执和解决之后的画面在第 4 章的日程和回执几张。
● 端到端覆盖“登录后再选论坛,以及处理冲突”的提交是 0b7f03f6。
● 半开区间的测试在 src/lib/schedule/conflicts.test.ts。
取消报名前先说明后果:取消后不再收到该论坛的通知。确认框是为了这个后果,不是为了多一步。
(4)推荐、通知和语言


推荐页接受一句话,例如关心大语言模型和软件工程。
● 返回的每一条都必须是 forums 里的真实编号,能打开详情,也能直接报名,不用把论坛名再复制回搜索框。
● 对不上的编号丢掉;接口失败先重试再换服务商,全部失败则用本地关键词匹配。
● 页面上不会出现一场库里不存在的论坛。
● 推荐结果的画面在第 4 章。
● 校验代码是 src/lib/recommend/validate.ts,失败后的降级是 3b001846。


| 序号 | 内容 |
|---|---|
| (1) | 通知只来自当前报名的论坛。 |
| (2) | 没有权限的人看到拒绝页,而不是一个提交后才失败的空表单。 |
| (3) | 参会者打不开人数看板、全部参会者和发布入口,第 4 章有这几张拒绝页。 |
| (4) | 已报名的人不用手动刷新就能收到新通知;取消之后不再推。 |
| (5) | 发布和推送的实现见第 3 章陈林祺的提交,例如 SSE 在 src/lib/notices/sse.ts。 |
(1)语言是简体中文、繁体中文、英语、日语、法语。
(2)文案在资源包里,页面用统一接口取词,不在组件里写“如果是中文就显示这句”。
(3)五种语言的首页在第 4 章开头。
(4)报名文案五种语言一起改的提交是 acb118b6。
(5)数量在英语和法语里用复数,是 11 月 4 日提交记录里的一项,避免“1 forums”这种句子出现在已经切换语言的页面上。
(5)少掉的那一次操作
| 如果做成 | 现在做成 | 人少做的事 |
|---|---|---|
| 先注册再回目录找论坛 | 注册表单上直接勾选 | 少一次搜索 |
| 保存后只跳到个人中心 | 先给回执,再进入我的报名 | 少一次回忆刚才选了什么 |
| 冲突就禁止提交 | 两场都留下,并排选「去这场」 | 少一次取消再重报 |
| 改时间后只发一条通知 | 日程按新的起止时间重算 | 少一次自己对通知和旧日程 |
| 推荐结果只是一段话 | 每条对应真实论坛,可报名 | 少一次把名字复制到搜索框 |
| 每种语言一套页面 | 同一套页面换资源包 | 少维护五套布局 |
● 动效只用来说明刚发生的事:操作反馈约 120 毫秒,状态变化约 200 毫秒,回执和对话框不超过 320 毫秒。
● 系统设置里减少动态效果时,这些动画关掉。
● 列表本身靠网格和细边区分层次,不靠阴影把 170 行堆出层次。
16. 服务器部署
线上地址是 https://zhihui.worldboot.net/。仓库以 CodeArts 的 知会 main 为准。第 4 章有完整操作截图。本节再贴这次打开线上站点时的首页、目录和推荐,用来核对部署是否还活着。第 4 章开头已经说明:参会者流程截自线上站点;主席人数看板、秘书名单、发布通知和修改论坛时间截自本机,因为线上演示口令登不进这几个账号。
生产环境不是把开发服务器挂到公网上。应用、数据库、迁移和反向代理分成四个容器,写在 compose.prod.yaml。这条配置的提交是 e5740f15。启动步骤写进说明的提交是 5b40ee80,说明正文在 README.md 的「部署」一节。
重复执行同一条启动命令时,迁移只跑还没执行过的脚本,目录导入不把会议主席改过的论坛时间写回数据包里的旧值。
下图是容器关系,不是网页截图。打开之后的页面在第 4 章。

2026-11-04 23:37(UTC+8)在本机请求线上健康检查,HTTP 200,正文 {"status":"ok"},响应头带 Via: 1.1 Caddy。同一时刻打开首页,右侧数字是库里的场次:分论坛 170、大会论坛 3、专题论坛 167,四天分别是 24、54、53、39。

同一站点再打开分论坛和推荐,确认反代后面的页面不是空壳。目录右侧「我的四天」四天都标空闲,说明这次访问还没有报名;列表本身已经从库里读出 10 月 21 日的场次。

推荐页把示例句子放在输入框下面。按钮在没有输入时不可用,避免发出空请求。

部署过程在阿里云主机的终端里完成。机器是 Ubuntu 24.04.5,Docker 29.1.3,Compose 2.40.3。代码在 /opt/zhihui/current。
(1)先连上服务器,看清系统和 Docker 版本,再构建生产镜像。

(2)镜像按 Dockerfile 的多阶段来。依赖层命中缓存,pnpm build 跑的是 Next.js 生产构建。

(3)迁移发现表已经在,于是跳过建表,仍然报告成功。当时库里可报名论坛 170 场。接着只重建应用容器,健康检查通过后是 zhihui-prod-app-1 Healthy。

(4)从外面再打一次健康检查。HTTP/2 200,正文 {"status":"ok"},响应头 via: 1.1 Caddy。说明请求先到 Caddy,再进应用。

同一时刻的响应头:
| 项 | 实测 |
|---|---|
| 时间 | 2026-11-04 23:37(UTC+8) |
| 地址 | https://zhihui.worldboot.net/api/health |
| 状态 | 200 |
| 正文 | {"status":"ok"} |
| 反代 | Via: 1.1 Caddy |
| 协议提示 | Alt-Svc: h3=":443" |
select 1 写在 src/app/api/health/route.ts。Caddy 把这个路径反代到 app:3000,配置是 deploy/Caddyfile。容器编排是 compose.prod.yaml,提交 e5740f15。
(1)四个容器和启动顺序
| 容器 | 来源 | 做什么 |
|---|---|---|
db | postgres:17-alpine | 数据在卷 db_data,容器重建后库还在 |
migrate | Dockerfile 的 tools 阶段 | 执行迁移和目录导入,成功后退出 |
app | 同一 Dockerfile 的 runner 阶段 | 只含 Next.js standalone,以 node 用户听 3000 端口 |
caddy | caddy:2-alpine | 对外 80 和 443,反代到 app:3000 |
顺序是依赖,不是四条可以随便调换的命令。
db的健康检查是pg_isready。库还不能连接时,后面的步骤不开始。migrate执行pnpm exec drizzle-kit migrate,再执行pnpm exec tsx scripts/seed-catalog.ts。这一步失败,应用容器不会启动,避免网站已经打开、表还没建好。app的健康检查请求本机http://127.0.0.1:3000/api/health。路由在 src/app/api/health/route.ts,提交是 e4061d06。它执行select 1。进程在、数据库不在时返回 503,Compose 不把这个容器当成已就绪。caddy等到应用健康之后才接公网。反代配置在 deploy/Caddyfile。证书和站点数据在卷caddy_data、caddy_config,重建容器不必重新申请证书。
镜像本身是多阶段构建,提交 f7f324e9。tools 保留完整依赖,给迁移和导入用。runner 只复制 standalone 产物、静态文件和 public,里面没有 pnpm,也没有完整的 node_modules。运行用户是 node,不是 root。
(2)域名、Cookie 和密钥
Caddyfile 的站点地址来自环境变量 SITE_ADDRESS,默认 :80。
(1)只有 IP 时保持 SITE_ADDRESS=:80,SESSION_COOKIE_SECURE=false,用 HTTP。会话 Cookie 不能在这种情况下被浏览器因 Secure 标志丢掉。
(2)有域名时把 SITE_ADDRESS 写成域名,SESSION_COOKIE_SECURE=true。Caddy 申请证书。当前对外的名字是 zhihui.worldboot.net,浏览器里是 HTTPS。第 4 章的页面就是从这个地址打开的。
数据库口令、模型 Key 放在服务器上的 .env,由 Compose 注入容器,不写进镜像层,也不进 Git。.env 从 .env.example 复制。启动时会检查必填项,这项校验的提交是 ed88c301。环境变量里的数据库地址会盖过配置文件里的地址,避免“改了 .env 却还连着旧库”,说明提交是 544df5ab。
推荐默认可以用 DeepSeek,也可以再配 Anthropic 或 OpenAI。Key 没配时,推荐走本地匹配,报名、日程和通知仍然可用。Key 配在环境变量里之后不必改代码。
(3)在服务器上执行的命令
在服务器上拉取 main 之后:
docker compose -f compose.prod.yaml up -d --build --wait
curl -fsS https://zhihui.worldboot.net/api/health
--wait 会停到 Compose 文件里写明的健康条件满足。/api/health 的正文是 {"status":"ok"} 时,应用进程和数据库都已就绪。返回 503 或连接失败,就不算部署完成。健康检查能判断部署起来的服务是否可用,这一点写在第 2 章陈林祺的验收项里;接口本身由饶恩哲提交。
本次核对记三行,不把「容器在跑」和「库能查」混成一件事:
- 地址:https://zhihui.worldboot.net/api/health
- 状态码:200
- 反代:
Via: 1.1 Caddy
首页能打开只说明 Caddy 把页面送出来了。健康检查多证明了一层:应用进程执行了 select 1。路由实现见 src/app/api/health/route.ts,签入 e4061d06。
演示账号不是这条命令的必经步骤。需要时再跑:
docker compose -f compose.prod.yaml run --rm migrate pnpm exec tsx scripts/seed-demo.ts
生产环境允许单独导入演示账号,提交是 f0c26f25。当前这台机器没有使用仓库 README 里的那组演示口令登录主席和秘书,所以第 4 章没有他们的成功态。参会者路径是在线上注册后走通的。
本地开发用的是另一份 compose.yaml,只起数据库,应用用 pnpm dev,端口 3100。并行开发时每个工作区用自己的端口和数据库,提交是 016328a1,避免两个人同时迁移打到同一个库。线上只有一份 db_data,不把这套并行库带到生产。
(4)重新部署时什么会变、什么不会变
| 动作 | 结果 |
|---|---|
再次 up --build | 用新的镜像替换应用;db_data 还在 |
drizzle-kit migrate | 只执行尚未记入迁移表的脚本;目前是 0000_init |
seed-catalog.ts | 补齐或更新目录字段;不覆盖已改的论坛起止时间 |
| 重新申请证书 | 域名不变且 caddy_data 还在时,沿用已有证书 |
| 未设置模型 Key | 推荐降级;其余页面仍由健康检查放行 |
因此会议主席在线上改过的时间,不会因为重新构建镜像回到 10 月 30 日数据包里的旧值。报名、通知和推荐日志都在 db_data 里,重建应用容器不会把它们清掉。要清掉数据必须单独删卷,部署步骤里没有这一步。
经过 Caddy 的通知推送,发布后大约 46 毫秒能收到事件。这是第 13 章里对线上路径的测量,用来说明反代没有把 SSE 攒到连接结束。第 4 章的通知页是这条链路上用户能看到的那一端。
(5)怎么判断已经部署成功
同时满足下面几条,才算这一项做完:
● 浏览器打开 https://zhihui.worldboot.net/ 能看到会议首页。画面见第 4.1 节。
● https://zhihui.worldboot.net/api/health 返回 {"status":"ok"}。
● 分论坛目录里的场次来自库,而不是空列表。目录见第 4.1 节。
● 现场注册的参会者能保存报名并看到个人日程。注册到「我的报名」的画面在第 4 章后半。
● 推荐返回的论坛能打开详情。推荐画面在第 4 章。
构建、迁移和应用健康的终端画面见本节前面四张。证书申请日志没有单独截。对外是否成功,以 zhihui.worldboot.net 和第 4 章的页面为准。
17. 附加功能
两项都做了。作业给每个附加功能的上限是 10 分。
(1)多语言:至少五种,文案进资源包,用统一接口取词。
(2)个人日程:按已选分论坛生成,检查时间冲突,用户可以调整。主办方改时间之后要重新检查。
多语言资源包
功能说明
作业允许从下面七种里自己选五种:简体中文、繁体中文、英语、德语、日语、法语、阿拉伯语。
本组选了五种,没有选德语和阿拉伯语。
| 作业列出的语言 | 本组 |
|---|---|
| 简体中文 | 选了 |
| 繁体中文 | 选了 |
| 英语 | 选了 |
| 德语 | 未选 |
| 日语 | 选了 |
| 法语 | 选了 |
| 阿拉伯语 | 未选 |
作业指出,如果写成下面这样,语言一多就没法维护:
if (Chinese) show Chinese menu text;
else if (Japanese) show Japanese menu text.
再加一种语言就要再加一个分支。程序员和翻译会改同一处代码。
作业要求的做法是:语言放进资源包,调用统一接口。题目里的例子是 LoadMenuItem(id, language)。
本组的对应关系:
| 题目里的说法 | 仓库里的做法 |
|---|---|
| 资源包 | messages/zh-CN.json、zh-TW.json、en.json、ja.json、fr.json |
id | 文案条目的键,例如导航、按钮、空状态 |
language | cookie zhihui_locale,取值是 zh-CN、zh-TW、en、ja、fr |
LoadMenuItem(id, language) | 服务端按语言加载整个资源包,页面用翻译函数按 id 取一句 |
(1)程序员改页面逻辑和条目编号,更新代码库。
(2)懂那种语言的人只改对应 JSON,不改 if。
(3)再加德语或阿拉伯语时,新增一个资源包,并在 locales.ts 的 LOCALES 里登记一行。业务组件不用加分支。


实现思路
新增一种语言:加一个
messages/{locale}.json,再在语言列表登记一行。业务页面不用改分支。
当前语言存在 cookie zhihui_locale。每次请求读这个值,合法才用,否则用简体中文。然后加载对应 JSON。时间区固定 Asia/Shanghai。
关键实现
加载在 src/i18n/request.ts。语言列表在 src/i18n/locales.ts。
五个资源包:
| 语言 | 资源包 |
|---|---|
| 简体中文 | zh-CN.json |
| 繁体中文 | zh-TW.json |
| 英语 | en.json |
| 日语 | ja.json |
| 法语 | fr.json |
(1)下拉选项始终用这种语言自己的名字:简体中文、繁體中文、English、日本語、Français。看不懂当前界面时也能切回去。
(2)页面用翻译函数按条目编号取词。导航、按钮、空状态、错误提示都走这一条。
(3)切换只写 cookie 再刷新。登录状态不动。右上角仍是退出,不会变回「登录 / 注册」。
(4)英语和法语的人数要用复数。提交是 0d645e80。
(5)登录和注册关掉浏览器自带的校验气泡。气泡的语言跟浏览器走,可能和页面不一致,还会挡住页面自己的提示。
运行界面
五种语言的首页都在下面。论坛名称仍是中文,变的是导航、按钮、统计和页脚。





登录之后再切英语,右上角仍是退出:

测试
英语人数走复数条目:

资源包对照的测试窗口:

注意事项
● 论坛名称、嘉宾、通知正文保持采集到的中文。那是会议数据,只存一份。
● 时间不跟访问者所在时区走。会议在成都,统一按北京时间显示。
● 本地关键词推荐里,其他语言只映射了常见研究方向。界面翻译完整,不表示降级搜索也覆盖所有外语说法。
个人日程与时间冲突
功能说明
作业要求:按参会者选择的分论坛生成个人日程,自动发现时间冲突并提示,用户可以调整。
下面三问按仓库里的实现回答。后面的「作业里的三个问题」再展开代码和界面。
问:如何判断两个分论坛的时间是否重叠?一场恰好在另一场结束时开始,算不算冲突?
答:用半开区间。两段时间互相伸进对方的区间才算重叠。结束时刻等于下一场的开始时刻,不算冲突。只提示,不禁止报名。
问:新增、取消或调整分论坛时,如何及时更新日程和冲突提示?
答:冲突不存成标记。保存、取消、点「去这场」或「改去这场」之后,下一次把页面渲染出来时用当前报名重算。导航上的数字跟着这次渲染变。
问:主办方临时改了论坛时间,个人日程要不要重新检查?
答:要。日程不记住「报名当时冲不冲突」。下次打开用新的开始和结束再算。改时间成功时,还会给当时已报名的人一条通知,提醒看日程。
实现思路
冲突不写入数据库。每次打开日程,用当前的开始和结束重算。
半开区间
[开始, 结束):一场的开始早于另一场的结束,并且反过来也成立,才算重叠。一场恰好在另一场结束时开始,不算冲突。
作业里的三个问题
(1)怎么判断重叠?
| 两场时间 | 结果 |
|---|---|
| 13:30–17:30 与 15:00–18:00 | 重叠,提示 |
| 13:30–17:30 与 17:30–19:00 | 首尾相接,不提示 |
| 三场都在同一个下午 | 算一处冲突,不拆成三对 |
比较函数在 schedule/conflicts.ts 的 overlaps。已按开始时间排序后,后面的场次如果已经不早于当前这场的结束,内层循环可以停。
(2)新增、取消、调整之后怎么更新?
● 保存报名、取消、点「去这场」之后,不靠旧标记。下一次读取重新分组。
● 「去这场」在一个事务里完成:选中的放进日程,和它重叠的其他报名移出日程。不会停在「两场都在」或「两场都不在」的中间状态。代码在 registrations/actions.ts 的 chooseForum。
● 移出日程的那场报名还在。人数照常计算。页面标「不去,报名保留」。
● 导航上的冲突数在这次服务端渲染里重算,保存之后的下一屏就是新数字。
只比较已经放进日程的报名。这一步的提交是 1986d664。一步选中要去的那场,提交是 a641425c。
(3)主办方改了时间,要不要重新检查?
要。日程不保存「当时有没有冲突」。下次打开用新的开始和结束再跑同一套函数。
改时间成功时还会通知当时已报名的人,正文提醒检查日程。改时间的页面:

运行界面
只报名一场时,另外三天是空的。页底写明首尾相接不算冲突。

目录下方「我的四天」按日期收已选场次。空着的日期直接标出来。

保存第二场之后,回执上两场都标时间冲突。


日程要求选一场。导航出现待选择的数量。

点「去这场」之后,冲突数消失。另一场报名还在。

个人中心同时保留两场。不去的那一行单独标出来,仍可取消。

主办方改时间的页面。保存后按新时间重新检查日程。

测试
用例在 conflicts.test.ts。


注意事项
● 只提示,不拦住报名。用户可以两场都报上,再决定人去哪一场。
● 已经决定不去的那场,不会因为又报了同时段的第三场就被重新算进来。需要的话,用户自己点回日程。
● 数据包里没有现成的首尾相接。要看这个边界,用会议主席把一场的开始改到另一场的结束时刻。
● 167 场专题论坛集中在四个下午的同一时段。同一下午再选一场,按这个规则就会提示冲突。不冲突的选法,最多是把上午的大会论坛和不同下午错开。
相关推荐
CNCC2026作业数据包
CNCC2026作业数据包
MobaXterm终端工具个人学习版安装包及常用配置备份
资源包含 MobaXterm 个人版的安装包以及日常运维常用的基础配置备份文件。适用于网络工程师及运维人员用于日常的服务器管理、SSH 连接及串口调试。 压缩包内附带了个人整理的软件使用技巧与配置说明(仅供个人学习参考)。请各位同行下载后注意,技术工具请尽量支持官方正版,本资源仅供个人测试与交流使用,切勿用于商业环境。
机械臂的「抽风」自救指南:从万向节死锁到毫米级丝滑抓取
机械臂的「抽风」自救指南:从万向节死锁到毫米级丝滑抓取
科技中介服务机构如何借助平台提升技术撮合服务的专业性与响应速度?.docx
科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。
产业园区如何利用AI技术提升科技成果转化服务的响应速度与转化成功率?.docx
科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。



