import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; export function validateChannelWord(value: unknown, editing = false) { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new BadRequestException('规则参数无效'); const data = value as Record; if (Object.keys(data).some((key) => !['channelId', 'word', 'status', 'remark', 'version'].includes(key))) throw new BadRequestException('包含不支持的字段'); if (typeof data.channelId !== 'string' || !data.channelId.trim() || data.channelId.length > 160) throw new BadRequestException('请选择通道'); if (typeof data.word !== 'string' || !data.word.trim() || data.word.trim().length > 200) throw new BadRequestException('敏感词需为1~200个字符'); if (typeof data.status !== 'string' || !['active', 'inactive'].includes(data.status)) throw new BadRequestException('状态无效'); if (data.remark !== undefined && (typeof data.remark !== 'string' || data.remark.length > 500)) throw new BadRequestException('备注最多500个字符'); if (editing && (!Number.isSafeInteger(data.version) || Number(data.version) < 1)) throw new BadRequestException('请提供规则版本'); return { channelId: data.channelId.trim(), word: data.word.trim(), status: data.status as string, remark: (data.remark as string | undefined) ?? '', version: editing ? Number(data.version) : undefined, }; } @Injectable() export class ChannelSensitiveWordsService { constructor(private readonly prisma: PrismaService) {} async authorize(userId?: string) { if ( !userId || !(await this.prisma.user.findFirst({ where: { id: userId, status: 'active', deletedAt: null, roles: { some: { role: { code: 'platform_admin' } } } }, select: { id: true }, })) ) throw new ForbiddenException('无敏感词管理权限'); } async list(userId: string | undefined, query: Record) { await this.authorize(userId); const page = Number(query.page ?? 1), pageSize = Number(query.pageSize ?? 25); if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new BadRequestException('分页参数无效'); if (query.status && !['all', 'active', 'inactive'].includes(query.status)) throw new BadRequestException('状态无效'); if (query.keyword && (typeof query.keyword !== 'string' || query.keyword.length > 200)) throw new BadRequestException('搜索词过长'); if (query.channelId && typeof query.channelId !== 'string') throw new BadRequestException('通道参数无效'); const where: Prisma.ChannelSensitiveWordWhereInput = { status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' }, channelId: query.channelId || undefined, word: query.keyword?.trim() ? { contains: query.keyword.trim() } : undefined, }; const [items, total] = await this.prisma.$transaction([ this.prisma.channelSensitiveWord.findMany({ where, include: { channel: { select: { id: true, name: true, status: true } } }, orderBy: [{ updatedAt: 'desc' }, { id: 'asc' }], skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.channelSensitiveWord.count({ where }), ]); return { items, total, page, pageSize }; } async save(userId: string | undefined, value: unknown, id?: string) { await this.authorize(userId); const data = validateChannelWord(value, Boolean(id)); try { return await this.prisma.$transaction(async (tx) => { if ( !(await tx.smsChannel.findFirst({ where: { id: data.channelId, status: { not: 'deleted' } }, select: { id: true }, })) ) throw new BadRequestException('通道不存在或已删除'); const current = id ? await tx.channelSensitiveWord.findUnique({ where: { id } }) : await tx.channelSensitiveWord.findUnique({ where: { channelId_word: { channelId: data.channelId, word: data.word } }, }); if (id && (!current || current.status === 'deleted')) throw new NotFoundException('规则不存在或已删除'); if (!id && current && current.status !== 'deleted') throw new ConflictException('该通道已配置相同敏感词'); const fields = { channelId: data.channelId, word: data.word, status: data.status, remark: data.remark, updatedBy: userId!, }; let saved; if (current) { const result = await tx.channelSensitiveWord.updateMany({ where: { id: current.id, version: id ? data.version : current.version }, data: { ...fields, version: { increment: 1 } }, }); if (result.count !== 1) throw new ConflictException('规则已被修改,请刷新后重试'); saved = await tx.channelSensitiveWord.findUniqueOrThrow({ where: { id: current.id } }); } else saved = await tx.channelSensitiveWord.create({ data: { ...fields, createdBy: userId! } }); await tx.operationLog.create({ data: { userId, action: current?.status === 'deleted' ? 'channel_sensitive_word.restore' : id ? 'channel_sensitive_word.update' : 'channel_sensitive_word.create', resource: 'channel_sensitive_word', resourceId: saved.id, detail: JSON.parse(JSON.stringify({ before: current, after: saved })), }, }); return saved; }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') throw new ConflictException('该通道已配置相同敏感词'); throw error; } } async remove(userId: string | undefined, id: string, version: unknown) { await this.authorize(userId); if (!Number.isSafeInteger(version) || Number(version) < 1) throw new BadRequestException('请提供规则版本'); return this.prisma.$transaction(async (tx) => { const before = await tx.channelSensitiveWord.findUnique({ where: { id } }); if (!before || before.status === 'deleted') throw new NotFoundException('规则不存在或已删除'); const result = await tx.channelSensitiveWord.updateMany({ where: { id, version: Number(version) }, data: { status: 'deleted', version: { increment: 1 }, updatedBy: userId! }, }); if (!result.count) throw new ConflictException('规则已被修改,请刷新后重试'); const after = await tx.channelSensitiveWord.findUniqueOrThrow({ where: { id } }); await tx.operationLog.create({ data: { userId, action: 'channel_sensitive_word.delete', resource: 'channel_sensitive_word', resourceId: id, detail: JSON.parse(JSON.stringify({ before, after })), }, }); return { deleted: true }; }); } }