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

13 · 工单分配:把处理责任交给合适的人#

前两阶段已经能创建工单,并限制谁可以读写。现在客服小陈进入工作台,想先看未分配工单,把其中一张交给自己,再与客户沟通。第三阶段围绕这条协作流程展开。

本章先讲分配;后面继续讲回复、重试、草稿和 URL 状态。对应功能提交为 1236559,正文仍按当前仓库阅读,遇到操作记录和提醒等后续逻辑时会标出边界,无需切换旧提交。

1. 先分清“选择负责人”与“保存负责人”#

数据 放在哪里 什么时候真正改变
当前负责人 PostgreSQL,Query 缓存在浏览器 服务端分配成功
下拉框尚未保存的选择 Assignment 组件 state 用户选择、清空或取消
“分配给我”的列表条件 URL,交给 nuqs 用户修改筛选
未发出的回复 Zustand 草稿 用户编辑,还没有发送

同一个页面里的状态可以分属不同层。下拉框选中了小陈,不代表数据库已经更新;保存成功后失效查询,才让列表和详情跟上服务端结果。

Assignment 中的 selected 有三个含义:

  • undefined:用户尚未作出新选择,显示服务器当前负责人。
  • null:用户明确选择取消分配。
  • 字符串:用户选择了一条新的成员关系。

这里不能随便用 if (!selected) 把 undefined 和 null 混为一谈,否则“取消负责人”可能无法提交。

2. 为什么负责人引用 Membership,而不是 User#

查看 schema.prisma 与 第三份迁移,工单新增 assigneeMembershipId,关联的是 Membership。

用户表达“是谁”,成员关系表达“此人在这个团队中的资格”。小陈可能同时属于两个团队,在一个团队是客服、另一个是客户。分配必须指向他有处理资格的那条团队关系。

前端请求叫 assigneeId,当前它的值实际上是 Membership.id,不是用户 ID。候选接口返回 { id, userId, user: { name } }:下拉选项用 id 提交,用姓名展示,用 userId 判断“我”。不要被相似字段名绕进去。

数据库外键保证该成员关系存在,但当前这条负责人外键本身没有表达“成员关系和工单必须同团队”。同团队与非客户角色条件仍由服务检查,不能只依赖外键。

3. 候选人接口为什么只返回本团队工作人员#

collaboration service 的 assignees:

  1. 确认调用人是当前团队成员。
  2. 客户调用时返回 FORBIDDEN。
  3. 只查当前团队内 ADMIN 或 AGENT 的成员关系。
  4. 返回有限字段作为候选列表。

候选人下拉减少误选,不能代替保存时验证。候选接口响应之后,对方可能已经被移除或降为客户;浏览器也可以提交一个不在选项中的 ID。

4. 保存分配经过哪些检查#

router.ts 的 ticket.assign 使用 assignTicketSchema,验证工单 UUID、可空负责人 UUID、非负整数版本,并合并团队输入。

createCollaborationService(db, userId, teamId).assign(input) 在事务中:

  1. 获取团队行锁,与成员变更等相关写流程协调。
  2. requireTicketAccess 检查团队成员与目标工单可见性。
  3. 拒绝客户修改分配,拒绝已关闭工单。
  4. 验证请求携带的 version。
  5. 若负责人没有变化,直接成功返回。
  6. 非空目标必须是本团队管理员或客服。
  7. 按工单、团队和版本条件更新,递增版本,并写操作记录。

第 5 步位于版本检查之后。因此旧页面即使选择“和现在一样的人”,也可能先收到冲突;只有版本仍有效的无变化请求才是成功的空操作,不产生假版本或假操作记录。

5. 最终写入为什么仍带团队与版本#

ts
// 源码节选:src/server/services/collaboration.ts
const result = await tx.ticket.updateMany({
  where: { id: input.id, teamId, version: input.version },
  data: {
    assigneeMembershipId: input.assigneeId,
    version: { increment: 1 },
  },
});

返回 count 为 0 就不能当成功。团队条件守住资源范围,版本条件守住编辑依据;前置验证用于给出明确业务错误,最终条件更新用于守住写入时的事实。

团队锁让候选人资格检查与成员变更协调;事务保证工单分配及当前操作记录一起提交。它们没有把浏览器缓存同步也变成数据库事务,所以成功后仍要刷新查询。

拓展阅读分配是否意味着只有负责人才能操作?

当前不意味着。分配表达处理责任,第二阶段的管理员 / 客服权限仍允许他们处理本团队工单。若产品改成“仅负责人可处理”,还要改变服务端授权和测试,不能只在页面把别人名字旁的按钮禁用。

6. 前端成功、冲突与“分配给我”#

成功时组件把 selected 设回 undefined,让列表、详情和当前操作记录查询失效,再提示成功。遇到 CONFLICT 则丢弃旧选择并让详情失效,等待新事实,不自动拿新版本重发。

“分配给我”按钮当前只把下拉框选择设成自己的 Membership.id,仍需点击保存分配。这与列表筛选 assignment=mine 是两种不同操作:前者准备修改业务数据,后者只改变读取条件。

负责人被移除或降为客户时,成员服务 在同一事务中清空其分配并递增工单版本,恢复客服角色也不会自动把过去的负责人全部分回去。

7. 本章验收与练习#

管理员和客服可将未关闭工单分配给本团队工作人员,再清空负责人;客户和其他团队成员不能成为负责人。用旧版本提交应得到冲突。关闭后不能改分配,历史负责人信息仍可查看。

对应自动验证在 collaboration.integration.test.ts:覆盖非法候选、客户与跨团队访问、旧版本、取消分配,以及成员降级和移除后的解除分配。

练习:候选项包含 userId 和 id,请求应该传哪个?

动手练习查看参考答案

参考答案:传候选记录的 id,也就是成员关系 ID。它让服务端在同一团队范围验证处理资格;userId 主要用于识别账号和显示“我”,不能替代这个字段。

完整链路是:下拉选择 → tRPC 校验登录和输入 → 服务检查调用资格、工单与候选关系 → 事务条件更新 PostgreSQL → 返回结果 → 刷新列表和详情。

把知识变成自己的理解

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

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

搜索教程

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

阅读设置

外观
正文字号

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

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