364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
import { BadRequestException, Injectable } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
export interface CreatePhoneSegmentDto {
|
|
prefix: string;
|
|
carrier: string;
|
|
province?: string;
|
|
city?: string;
|
|
}
|
|
|
|
export interface PhoneSegmentListQuery {
|
|
keyword?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
export interface CreatePhoneCarrierRuleDto {
|
|
carrier: string;
|
|
pattern: string;
|
|
priority?: number;
|
|
status?: string;
|
|
remark?: string;
|
|
}
|
|
|
|
export interface PageQuery {
|
|
keyword?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
export interface CreateSensitiveWordDto {
|
|
word: string;
|
|
level?: string;
|
|
status?: string;
|
|
}
|
|
|
|
export interface CreateBlacklistDto {
|
|
tenantId?: string;
|
|
applicationId?: string;
|
|
phoneNumber: string;
|
|
reason?: string;
|
|
status?: string;
|
|
operatorId?: string;
|
|
}
|
|
|
|
export interface CreateDrainageFieldDto {
|
|
code: string;
|
|
name: string;
|
|
fieldType: 'string' | 'image' | 'file';
|
|
required?: boolean;
|
|
status?: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface CreateCommonReportFieldDto {
|
|
drainageFieldId: string;
|
|
reportType: 'signature' | 'drainage';
|
|
required?: boolean;
|
|
sortOrder?: number;
|
|
}
|
|
|
|
export interface DictionaryStatusDto {
|
|
status?: string;
|
|
operatorId?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
export interface DictionaryListQuery {
|
|
tenantId?: string;
|
|
applicationId?: string;
|
|
keyword?: string;
|
|
status?: string;
|
|
enterpriseKeyword?: string;
|
|
applicationKeyword?: string;
|
|
phoneNumber?: string;
|
|
reasonKeyword?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class DictionariesService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async listPhoneSegments(query: PhoneSegmentListQuery = {}) {
|
|
const page = Math.max(1, Number(query.page ?? 1));
|
|
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize ?? 25)));
|
|
const keyword = query.keyword?.trim();
|
|
const where = keyword ? {
|
|
OR: [
|
|
{ prefix: { startsWith: keyword } },
|
|
{ carrier: { contains: keyword } },
|
|
{ province: { contains: keyword } },
|
|
{ city: { contains: keyword } },
|
|
],
|
|
} : undefined;
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.phoneSegment.findMany({ where, orderBy: { prefix: 'asc' }, skip: (page - 1) * pageSize, take: pageSize }),
|
|
this.prisma.phoneSegment.count({ where }),
|
|
]);
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
createPhoneSegment(data: CreatePhoneSegmentDto) {
|
|
return this.prisma.phoneSegment.create({ data });
|
|
}
|
|
|
|
deletePhoneSegment(id: string) {
|
|
return this.prisma.phoneSegment.delete({ where: { id } });
|
|
}
|
|
|
|
async listPhoneCarrierRules(query: PageQuery = {}) {
|
|
const page = Math.max(1, Number(query.page ?? 1));
|
|
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize ?? 25)));
|
|
const where = query.keyword?.trim() ? {
|
|
OR: [
|
|
{ carrier: { contains: query.keyword.trim() } },
|
|
{ pattern: { contains: query.keyword.trim() } },
|
|
{ remark: { contains: query.keyword.trim() } },
|
|
],
|
|
} : undefined;
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.phoneCarrierRule.findMany({ where, orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }], skip: (page - 1) * pageSize, take: pageSize }),
|
|
this.prisma.phoneCarrierRule.count({ where }),
|
|
]);
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
createPhoneCarrierRule(data: CreatePhoneCarrierRuleDto) {
|
|
if (!data.carrier || !data.pattern) {
|
|
throw new BadRequestException('carrier and pattern are required');
|
|
}
|
|
try {
|
|
new RegExp(data.pattern);
|
|
} catch {
|
|
throw new BadRequestException('pattern must be a valid regular expression');
|
|
}
|
|
return this.prisma.phoneCarrierRule.create({
|
|
data: {
|
|
carrier: data.carrier,
|
|
pattern: data.pattern,
|
|
priority: data.priority ?? 100,
|
|
status: data.status ?? 'active',
|
|
remark: data.remark,
|
|
},
|
|
});
|
|
}
|
|
|
|
listSensitiveWords(query: DictionaryListQuery = {}) {
|
|
return this.prisma.sensitiveWord.findMany({
|
|
where: {
|
|
status: query.status && query.status !== 'all' ? query.status : undefined,
|
|
OR: query.keyword ? [
|
|
{ word: { contains: query.keyword } },
|
|
{ level: { contains: query.keyword } },
|
|
] : undefined,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createSensitiveWord(data: CreateSensitiveWordDto) {
|
|
const created = await this.prisma.sensitiveWord.create({
|
|
data: {
|
|
word: data.word,
|
|
level: data.level ?? 'block',
|
|
status: data.status ?? 'active',
|
|
},
|
|
});
|
|
await this.writeOperationLog(undefined, 'sensitive_word.create', 'sensitive_word', created.id, { word: data.word });
|
|
return created;
|
|
}
|
|
|
|
async changeSensitiveWordStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status ?? 'active';
|
|
const updated = await this.prisma.sensitiveWord.update({ where: { id }, data: { status } });
|
|
await this.writeOperationLog(data.operatorId, `sensitive_word.${status}`, 'sensitive_word', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
listGlobalBlacklist(query: DictionaryListQuery = {}) {
|
|
return this.prisma.globalBlacklist.findMany({
|
|
where: {
|
|
status: query.status && query.status !== 'all' ? query.status : undefined,
|
|
OR: query.keyword ? [
|
|
{ phoneNumber: { contains: query.keyword } },
|
|
{ reason: { contains: query.keyword } },
|
|
] : undefined,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createGlobalBlacklist(data: CreateBlacklistDto) {
|
|
const created = await this.prisma.globalBlacklist.create({
|
|
data: {
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
status: data.status ?? 'active',
|
|
},
|
|
});
|
|
await this.writeOperationLog(data.operatorId, 'global_blacklist.create', 'global_blacklist', created.id, {
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
});
|
|
return created;
|
|
}
|
|
|
|
async changeGlobalBlacklistStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status ?? 'active';
|
|
const updated = await this.prisma.globalBlacklist.update({ where: { id }, data: { status } });
|
|
await this.writeOperationLog(data.operatorId, `global_blacklist.${status}`, 'global_blacklist', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
listEnterpriseBlacklist(query: DictionaryListQuery = {}) {
|
|
return this.prisma.enterpriseBlacklist.findMany({
|
|
where: {
|
|
tenantId: query.tenantId,
|
|
applicationId: query.applicationId,
|
|
status: query.status && query.status !== 'all' ? query.status : undefined,
|
|
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
|
|
application: query.applicationKeyword ? { name: { contains: query.applicationKeyword } } : undefined,
|
|
phoneNumber: query.phoneNumber ? { contains: query.phoneNumber } : undefined,
|
|
reason: query.reasonKeyword ? { contains: query.reasonKeyword } : undefined,
|
|
OR: query.keyword ? [
|
|
{ phoneNumber: { contains: query.keyword } },
|
|
{ reason: { contains: query.keyword } },
|
|
{ tenant: { name: { contains: query.keyword } } },
|
|
{ application: { name: { contains: query.keyword } } },
|
|
] : undefined,
|
|
},
|
|
include: { tenant: true, application: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createEnterpriseBlacklist(data: CreateBlacklistDto) {
|
|
if (!data.tenantId || !data.applicationId) {
|
|
throw new BadRequestException('tenantId and applicationId are required for application blacklist');
|
|
}
|
|
const application = await this.prisma.smsApplication.findUnique({
|
|
where: { id: data.applicationId },
|
|
select: { tenantId: true },
|
|
});
|
|
if (!application || application.tenantId !== data.tenantId) {
|
|
throw new BadRequestException('applicationId does not belong to tenantId');
|
|
}
|
|
const createData: Prisma.EnterpriseBlacklistUncheckedCreateInput = {
|
|
tenantId: data.tenantId,
|
|
applicationId: data.applicationId,
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
status: data.status ?? 'active',
|
|
};
|
|
const created = await this.prisma.enterpriseBlacklist.create({ data: createData });
|
|
await this.writeOperationLog(data.operatorId, 'enterprise_blacklist.create', 'enterprise_blacklist', created.id, {
|
|
tenantId: data.tenantId,
|
|
applicationId: data.applicationId,
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
});
|
|
return created;
|
|
}
|
|
|
|
async changeEnterpriseBlacklistStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status ?? 'active';
|
|
const updated = await this.prisma.enterpriseBlacklist.update({ where: { id }, data: { status } });
|
|
await this.writeOperationLog(data.operatorId, `enterprise_blacklist.${status}`, 'enterprise_blacklist', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
async listDrainageFields() {
|
|
const fields = await this.prisma.drainageField.findMany({
|
|
include: { _count: { select: { channelReportFields: true, commonReportFields: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return fields.map(({ _count, ...field }) => ({
|
|
...field,
|
|
usageCount: _count.channelReportFields,
|
|
commonUsageCount: _count.commonReportFields,
|
|
}));
|
|
}
|
|
|
|
createDrainageField(data: CreateDrainageFieldDto) {
|
|
const code = data.code?.trim();
|
|
if (!code || !/^[A-Za-z0-9]+$/.test(code)) {
|
|
throw new BadRequestException('code must contain only Arabic numerals and English letters');
|
|
}
|
|
if (!['string', 'image', 'file'].includes(data.fieldType)) {
|
|
throw new BadRequestException('fieldType must be string, image or file');
|
|
}
|
|
return this.prisma.drainageField.create({
|
|
data: {
|
|
code,
|
|
name: data.name,
|
|
fieldType: data.fieldType,
|
|
required: data.required ?? false,
|
|
status: data.status ?? 'active',
|
|
description: data.description,
|
|
},
|
|
});
|
|
}
|
|
|
|
async deleteDrainageField(id: string) {
|
|
const [usageCount, commonUsageCount] = await Promise.all([
|
|
this.prisma.channelReportField.count({ where: { drainageFieldId: id } }),
|
|
this.prisma.commonReportField.count({ where: { drainageFieldId: id } }),
|
|
]);
|
|
if (usageCount > 0 || commonUsageCount > 0) {
|
|
throw new BadRequestException(`该字段已被 ${usageCount} 个通道和 ${commonUsageCount} 个通用配置使用,不能删除`);
|
|
}
|
|
return this.prisma.drainageField.delete({ where: { id } });
|
|
}
|
|
|
|
listCommonReportFields() {
|
|
return this.prisma.commonReportField.findMany({
|
|
include: { drainageField: true },
|
|
orderBy: [{ reportType: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }],
|
|
});
|
|
}
|
|
|
|
async createCommonReportField(data: CreateCommonReportFieldDto) {
|
|
if (data.reportType !== 'signature' && data.reportType !== 'drainage') {
|
|
throw new BadRequestException('reportType must be signature or drainage');
|
|
}
|
|
const field = await this.prisma.drainageField.findUnique({ where: { id: data.drainageFieldId } });
|
|
if (!field || field.status !== 'active') {
|
|
throw new BadRequestException('报备字段库字段不存在或已停用');
|
|
}
|
|
const existing = await this.prisma.commonReportField.findUnique({
|
|
where: { drainageFieldId_reportType: { drainageFieldId: field.id, reportType: data.reportType } },
|
|
});
|
|
if (existing) {
|
|
throw new BadRequestException('该字段已配置为对应类型的通用字段');
|
|
}
|
|
return this.prisma.commonReportField.create({
|
|
data: {
|
|
drainageFieldId: field.id,
|
|
reportType: data.reportType,
|
|
required: data.required ?? field.required,
|
|
sortOrder: data.sortOrder ?? 100,
|
|
status: 'active',
|
|
},
|
|
include: { drainageField: true },
|
|
});
|
|
}
|
|
|
|
deleteCommonReportField(id: string) {
|
|
return this.prisma.commonReportField.delete({ where: { id } });
|
|
}
|
|
|
|
private writeOperationLog(userId: string | undefined, action: string, resource: string, resourceId: string, detail: Record<string, unknown>) {
|
|
return this.prisma.operationLog.create({
|
|
data: {
|
|
userId,
|
|
action,
|
|
resource,
|
|
resourceId,
|
|
detail: detail as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
}
|
|
}
|