TTeamDesk 学习手册
本章目录 展开导航 ↓

02 · 架构与第一阶段地图:一张工单经过哪些层#

第一阶段不是把四个按钮拼出来,而是让一次操作具备完整输入、规则、存储、结果和失败反馈。本章先建立地图,之后每章沿地图走一条业务路径。

1. 先用用户语言定义范围#

一个客户遇到问题,填写标题、描述和优先级;系统保存工单,列表能找到它,点进去能看到详情;客服按允许的路径推进状态。

用户动作 接口 服务方法 最终结果
提交问题 ticket.create create 数据库产生工单,页面打开详情
查看工作台 ticket.list list 返回本页数据与匹配总数
打开某一张工单 ticket.detail detail 返回工单或明确的错误
推进处理进度 ticket.updateStatus updateStatus 合法状态写入,版本递增,页面刷新

没有删除接口不妨碍这个阶段闭环:业务中的“关闭”是保留工单并结束处理,不等于删除数据库记录。

2. 请求沿这张地图移动#

text
浏览器:Ant Design Form / Table
  ↓ useMutation / useQuery + tRPC options
src/trpc/client.tsx:HTTP 客户端与查询缓存
  ↓ HTTP /api/trpc(httpBatchLink 可以合并请求)
src/app/api/trpc/[trpc]/route.ts:HTTP 适配、会话、上下文
  ↓
src/server/api/trpc.ts:登录检查、错误格式
  ↓
src/server/api/router.ts:选择接口、校验输入、转交服务
  ↓
src/server/services/ticket.ts:权限、业务规则、事务、结果转换
  ↓ Prisma Client → PostgreSQL
返回:JSON → 查询缓存 / 变更回调 → React 界面

入口中的会话验证先建立身份;进入 procedure 后还要经过登录中间件、输入解析,再运行接口处理函数。各层任一检查失败都会中断后续步骤。

前端可以把这理解为“组件 → 请求封装 → controller → service → 数据访问”。类比只帮助定位职责:tRPC 提供类型推导和调用约定,但底层仍然有 HTTP、序列化、网络失败和服务端执行。

3. 目录按职责拆开有什么价值#

位置 职责 阅读入口
src/app 路由和页面壳、HTTP 入口 首页、详情页
src/features/tickets 工单 UI、表单、查询与交互 workspace.tsx
src/shared 前后端都能使用的规则和类型 ticket.ts
src/trpc 浏览器侧 tRPC 和 Query Provider client.tsx
src/server/api 接口定义与通用保护 router.ts、trpc.ts
src/server/services 用例、资源授权和数据库操作 ticket.ts
src/server/db.ts 连接池与 Prisma 实例 db.ts
prisma 模型与迁移历史 schema

共享 schema 不依赖数据库,所以浏览器可以安全使用它。服务层接收 db 而不是自己在每个方法里创建连接,测试也能注入明确的数据库实例。当前服务层使用 TRPCError,仍与 tRPC 有一定耦合;如果以后要复用到 CLI,可再考虑独立业务错误类型,不必在第一阶段先搭建复杂抽象。

4. Server Component 与 Client Component#

本项目 page.tsx、layout.tsx 默认是 Server Component;需要表单事件、Hooks、浏览器缓存的工作台标注 'use client'。

服务端页面可以渲染客户端工作台。详情页面的一个关键步骤是:

tsx
// 源码节选:src/app/tickets/[id]/page.tsx
async function Detail({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <TicketDetail id={id} />;
}

当前版本的 params 是 Promise,需要先读取。外层 Suspense 提供等待时的页面占位。不要直接套用旧教程的同步 params.id。Suspense 也不会自动接管所有 TanStack Query 状态;工作台仍显式显示 loading、error 和 empty。

'use client' 声明客户端模块边界,不代表组件首次只能在浏览器渲染。它允许交互相关代码进入客户端构建;数据库模块不能跟着进入这条导入链。db.ts 的 import 'server-only' 帮助阻止误用。

客户端这一行很重要:

ts
import type { AppRouter } from '@/server/api/router';

import type 只参与编译期类型检查,运行时代码会被移除,所以没有把 Prisma 和服务端业务打包进浏览器。类型路径指向服务端文件,与运行时导入该文件是两件事。

5. Provider 负责提供运行环境#

layout.tsx 与 providers.tsx 共同提供:

  • AntdRegistry:收集并注入 antd 首屏样式。
  • StyleProvider layer、ConfigProvider:样式层级、中文语言包、主题。
  • antd App:供 App.useApp() 获取消息等上下文能力。
  • ApiProvider:tRPC 客户端与 TanStack Query 缓存。
  • NuqsAdapter、TeamWorkspace:当前版本的 URL 状态与团队/登录上下文。

Provider 像给组件插上公共电源;它不自动替组件保存工单。只有真正调用接口并成功写库,工单才持久化。globals.css 声明 CSS layer 顺序,配合 antd 的 layer 让 Tailwind 和组件样式按明确层级协作;遇到样式覆盖问题,应检查层级与选择器,而非不断加 !important。

6. 第一阶段的数据模型#

先关注最初迁移中的九个核心字段,参见 第一份 SQL:

字段 含义 设计原因
id UUID 主键 程序稳定识别记录,详情 URL 使用它
number 自增、唯一编号 页面展示 #0001,便于沟通
title 最多 100 字符的标题列 短文本,列表快速识别问题
description TEXT 描述 保存较长问题正文
priority 优先级枚举 限定为低、普通、高、紧急
status 状态枚举,初始 OPEN 明确处理阶段
version 初始 0 的整数 检测旧页面修改
createdAt 创建时间 排序、展示
updatedAt 更新时间 反映修改发生时间

当前模型还带团队、创建者、负责人、操作记录与提醒关系。理解核心字段后,再回到这些扩展,不要把最初 SQL 覆盖到当前 schema。

拓展阅读UUID、编号和授权。

UUID 不容易猜测,但“难猜”不是权限。当前查询仍限定团队,客户还限定创建者。number 当前是全表自增,团队内出现不连续编号正常;数据库序列也可能因事务回滚产生空缺,它不能用作记录总数。

拓展阅读ORM 与数据库分别保证什么?

Prisma 将模型变成类型安全的访问方法;PostgreSQL 真正保存数据并执行约束。TypeScript 类型不会阻止外部 HTTP 客户端发送错误数据;数据库枚举只限定状态值,并不知道 OPEN 能不能直接变成 RESOLVED。合法状态路径仍须由业务服务检查。

7. 用四个问题检查理解#

  1. 改标题长度限制,为什么要检查共享 schema、表单和数据库列?因为它们分别约束运行时请求、用户体验和存储能力。
  2. 想查某条工单,应在哪里检查团队?当前服务层的数据库过滤条件中必须带上;不能只靠页面隐藏入口。
  3. 为什么 router 很薄?它负责接口边界,复杂业务集中到 service,便于阅读和测试。
  4. 为什么不在浏览器直接调用 Prisma?连接凭据与可信规则属于服务端,浏览器无法成为数据库授权边界。

下一章从“提交一张工单”开始,把这张地图走一遍。

把知识变成自己的理解

完成本章练习,再继续下一站。

TEAMDESK HANDBOOK · 从一张工单,走进全栈开发

搜索教程

输入关键词,搜索全部章节。

阅读设置

外观
正文字号

从一张工单出发,读懂数据如何流转。

设置与进度仅保存在当前浏览器。