TTeamDesk 学习手册

src/server/services/ticket-events.ts

此代码来自构建时的仓库快照。修改源码后重新构建即可更新。

ts
import type {
  Prisma,
  PrismaClient,
  MemberRole,
  TicketEventKind,
  TicketStatus,
} from '@/generated/prisma/client';
import { requireTicketAccess } from './access';

type EventInput = {
  teamId: string;
  ticketId: string;
  version: number;
  kind: TicketEventKind;
  actorUserId: string;
  actorRole: MemberRole;
  fromStatus?: TicketStatus;
  toStatus?: TicketStatus;
  fromAssigneeName?: string;
  toAssigneeName?: string;
  reason?: string;
  replyId?: string;
};
export async function recordTicketEvent(
  tx: Prisma.TransactionClient,
  input: EventInput,
) {
  const actor = await tx.user.findUniqueOrThrow({
    where: { id: input.actorUserId },
    select: { name: true },
  });
  // 只接受事务客户端,调用方必须把业务写入和记录写入放在同一事务。
  // 记录失败直接抛出,数据库会撤销先前修改;不能 catch 后忽略,否则历史会“缺一笔”。
  // 操作者从会话和成员关系取得,姓名保存快照,不接受客户端伪造身份或变更前的值。
  await tx.ticketEvent.create({ data: { ...input, actorName: actor.name } });
}
export function createTicketEventService(
  db: PrismaClient,
  userId: string,
  teamId: string,
) {
  return {
    async list(input: { id: string; page: number; pageSize: number }) {
      return db.$transaction(
        async (tx) => {
          await requireTicketAccess(tx, userId, teamId, input.id);
          // 先验证工单权限,再按团队+工单分页;总数也不能泄漏其他客户/团队的历史。
          const where = { teamId, ticketId: input.id };
          const items = await tx.ticketEvent.findMany({
            where,
            orderBy: { version: 'desc' },
            skip: (input.page - 1) * input.pageSize,
            take: input.pageSize,
            select: {
              id: true,
              version: true,
              kind: true,
              actorName: true,
              actorRole: true,
              fromStatus: true,
              toStatus: true,
              fromAssigneeName: true,
              toAssigneeName: true,
              reason: true,
              createdAt: true,
            },
          });
          const total = await tx.ticketEvent.count({ where });
          return {
            items: items.map((event) => ({
              ...event,
              createdAt: event.createdAt.toISOString(),
            })),
            total,
          };
        },
        { isolationLevel: 'RepeatableRead' },
      );
    },
  };
}

搜索教程

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

阅读设置

外观
正文字号

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

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