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) { return this.prisma.operationLog.create({ data: { userId, action, resource, resourceId, detail: detail as Prisma.InputJsonValue, }, }); } }