06 · 验证、排错与练习:证明业务真的跑通#
“页面能打开”和“业务正确”不是一回事。本章分别检查成功路径、失败边界和并发行为,并把错误定位到具体一层。以下是供你本地执行的学习验收,不表示教程编写过程中已经启动数据库或完成这些运行测试。
1. 按一条故事完成手动验收#
先用当前仓库的注册账号创建团队,成为管理员。依次操作:
| 操作 | 预期 | 验证的知识点 |
|---|---|---|
| 空白标题、空描述提交 | 被拒绝,有字段提示 | 输入校验 |
| 创建“无法登录” | 提示成功并打开详情 | 前端到数据库写入 |
| 刷新详情 | 内容仍存在 | 数据持久化 |
| 回列表,切换状态筛选 | 总数与行数据匹配 | 条件与分页 |
| 待处理 → 处理中 | 成功,版本递增 | 合法路径 |
| 处理中 → 已解决,不填说明 | 当前版本拒绝 | 依赖业务状态的校验 |
| 填说明后解决,再填新的说明重新打开 | 成功,显示新状态 | 可恢复状态与说明规则 |
| 填写关闭说明后关闭工单 | 成功,之后无可用状态操作 | 终态规则 |
| 访问格式错误的 ID | 明确错误 | UUID 输入校验 |
| 访问合法但不存在的 UUID | 未找到提示 | 资源不存在处理 |
| 旧版本修改同一工单 | 冲突并读取最新状态 | 并发控制 |
最后用 Prisma Studio 对照标题、状态、版本和团队归属。当前版本还会有操作记录和提醒任务,这是后续阶段接入留下的数据,不代表第一阶段必须掌握它们的完整实现。
2. 自动检查各自能证明什么#
pnpm lint
pnpm typecheck
pnpm format:check
pnpm testESLint 检查代码规则,TypeScript 检查类型,Prettier 检查格式,Vitest 执行行为测试。任何单项通过都不能代替其他验证,尤其类型正确不意味着权限正确。
默认测试中,未提供 TEST_DATABASE_URL / TEST_REDIS_URL 的集成测试会跳过。输出全绿时也要看通过与跳过的数量,不能据此宣称数据库链路已验证。
先读两个测试入口#
- tests/ticket.test.ts:拒绝空白输入、非法优先级与异常分页。无须运行数据库,适合理解边界条件。
- tests/tickets.integration.test.ts:使用真实数据库、
appRouter.createCaller和明确用户上下文,覆盖当前工单、身份与团队规则,包括版本冲突。
状态路径和操作记录的更多测试在 ticket-events.integration.test.ts。这些测试按业务约定列合法路径,而非直接读取 allowedTransitions 生成预期值,避免实现写错后测试也跟着错。
两个文件的证据不能混写:tickets 用例明确检查并发修改只有一次成功、另一项 CONFLICT;它末尾对关闭后修改只断言抛错,没有固定断言是哪种错误。完整 16 组路径及状态说明的精确检查在记录测试中,读法见第 21 章。
createCaller 经过 tRPC procedure、输入校验与服务层,但不经过浏览器网络和 Next.js 的 HTTP Route Handler。因此它不能单独证明 Cookie 传输、Origin 检查、页面消息都正常;这些要由相应认证测试、HTTP 或浏览器验收补足。
3. 运行真实数据库测试#
使用独立的本地测试数据库,并先对它应用仓库迁移。以下示例通过现有 PostgreSQL 容器创建 teamdesk_test,仅首次创建时执行 createdb:
pnpm db:up
# 仅首次创建;已有 teamdesk_test 时跳过下一行
docker compose exec -T postgres createdb -U teamdesk teamdesk_test
DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test pnpm db:migrate
pnpm db:generate
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test pnpm exec vitest run tests/tickets.integration.test.ts tests/ticket-events.integration.test.ts如果数据库已存在,跳过创建即可。上面的临时环境变量仅影响该条命令,不会改写 .env。这两组测试聚焦当前工单与状态规则,不要求 Redis 队列在线;当前创建逻辑仍会在 PostgreSQL 中保存提醒任务。
学习第一阶段不要求立即执行所有后台任务测试。如果要验证当前项目的全部集成能力,暂停开发 Worker,启动 Redis 后执行:
TEST_DATABASE_URL=postgresql://teamdesk:teamdesk@localhost:5433/teamdesk_test \
TEST_REDIS_URL=redis://127.0.0.1:6380 pnpm test:integration测试仅清理自己创建的数据,队列测试使用随机前缀;仍应明确指向测试环境。Worker 消费哪个库取决于它自己的 DATABASE_URL,临时设置 TEST_DATABASE_URL 不会让它自动切库。不要启动连接测试库的常驻 Worker;与测试库完全分离的开发 Worker 不会因这个测试命令就读到测试待办。没有连接环境时,如实记录“集成测试未执行”,不要把跳过写成通过。
4. 按数据流排查,不从头盲改#
先问“失败最早出现在哪层”,再查下一层:
| 现象 | 优先检查 | 原因 |
|---|---|---|
| Docker 无法连接 | Docker Desktop 是否启动 | CLI 存在不代表引擎在运行 |
| 数据库连接失败 | 容器健康、5433 端口、DATABASE_URL |
宿主机连接地址与容器端口不同 |
| 报表或列不存在 | pnpm db:migrate 是否对当前库成功 |
生成 Client 不会创建表 |
| Prisma 字段或类型过期 | pnpm db:generate,重启开发进程 |
模型、生成文件、旧进程可能不一致 |
| POST 被来源检查拒绝 | 地址栏 Origin 与 BETTER_AUTH_URL |
localhost 与 127.0.0.1 不相同 |
| 已登录却不能操作 | 会话 Cookie、当前团队、成员角色 | 登录与业务授权是两次判断 |
| 提交后没有请求 | Form 字段提示、safeParse 结果 |
可能在浏览器就已拒绝 |
| 请求有响应但创建失败 | tRPC 错误内容、字段错误 | HTTP 返回不等于业务成功 |
| 数据库更新了,页面仍旧 | mutation 成功回调、query key、失效逻辑 | 服务端写入不会自动更改客户端缓存 |
看起来没变化却 CONFLICT |
详情版本与数据库版本 | 分配、回复等当前能力也可能推进版本 |
| 默认测试通过但未查库 | Vitest 的 skipped 与环境变量 | 集成测试受显式连接开关控制 |
| 提醒 / 导出不执行 | Worker、Redis、队列前缀 | 属于后台任务链路,不是基础建表失败 |
浏览器 Network 适合确认请求是否发出、输入与响应;服务端日志适合定位内部异常;Prisma Studio 适合确认最终数据。不要在浏览器日志输出连接串或密钥。
5. 三个跟源码走的练习#
练习 A:画出创建工单的字段来源#
不改代码,列出创建请求和数据库新增记录的字段,逐个标注来自表单、会话、团队上下文还是默认值。
动手练习查看参考答案
参考答案:标题、描述、优先级来自表单;团队 ID 来自选择后还要服务端验权;创建者来自已验证会话;状态与版本使用初始默认值;ID、编号与时间由 Prisma / 数据库机制产生;当前截止时间由服务端根据优先级计算。
检验点:浏览器不能通过传一个 creatorId 就替别人创建,也不能凭借真实的 teamId 自动获得权限。
练习 B:解释筛选后为什么回第一页#
先翻到后面的页码,再筛选只剩少量工单的状态。找到 setFilters({ status: ..., page: 1 }),解释去掉 page: 1 后会发生什么。
动手练习查看参考答案
参考答案:旧页码可能超出新结果集范围,接口返回空页,但总数非零。重置页码属于交互语义,不是数据库丢了记录。即使正确重置,其他用户并发改变结果集时,仍可能需要处理越界页的体验。
练习 C:用旧 version 描述一次冲突#
找到集成测试中的 CONFLICT 断言,按“首次创建 → 两次修改同时携带初始版本 → 一次成功、另一次冲突 → 读取最终状态”复述场景。源码使用 Promise.allSettled 并发发起请求,不预设哪一个先成功。
动手练习查看参考答案
参考答案:先取得写入机会的请求把 v0 变成 v1,另一个仍携带 v0,因此被拒绝;测试断言恰好一个成功,另一个返回 CONFLICT。若只比较标题或 updatedAt,容易把显示精度、时钟与业务版本混淆;整数版本让编辑依据更明确。
6. 再做两个扩展思考#
如果要加“已取消”,是不是只加一个 Select 选项?
需要先确定它是终态还是可恢复状态,再协调数据库枚举与迁移、共享 schema、标签、合法路径、服务校验、UI 和测试;当前还要考虑操作说明、提醒与操作记录。需求是跨层合同,不是局部视觉变更。
如果创建请求超时,应该直接自动重发吗?
不一定。请求可能已经写入数据库,只是响应丢失。自动重发可能重复创建。可靠重试通常需要为一次业务意图分配稳定请求标识,并由服务端唯一约束与事务保证幂等;单纯禁用按钮做不到这一点。当前回复接口已有相关实践,可在第三阶段继续学习。
7. 第一阶段完成标准#
你不需要背会每个库的 API,但应能独立解释以下链路,并用页面、Network、数据库和测试各找一份证据:
前端收集输入
→ 接口运行时校验与会话 / 团队权限
→ 服务层决定允许的业务行为
→ Prisma 与 PostgreSQL 完成读取、约束和事务写入
→ 返回明确数据或错误
→ 前端刷新相关查询并反馈给用户接着进入 第 07 章:登录认证,系统学习登录、成员角色和团队隔离。本阶段已保留并使用这些保护,但学习重心先放在工单业务与数据库之间的完整闭环。