feat: filter SMS routes by channel sensitive words
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user