src/server/services/ticket-events.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' },
);
},
};
}