158 lines
7.2 KiB
TypeScript
158 lines
7.2 KiB
TypeScript
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<string, unknown>;
|
||
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<string, string | undefined>) {
|
||
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 };
|
||
});
|
||
}
|
||
}
|