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; required?: boolean; status?: string; description?: string; } export interface DictionaryStatusDto { status?: string; operatorId?: string; reason?: string; } export interface DictionaryListQuery { tenantId?: string; applicationId?: string; keyword?: string; status?: 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 }); } 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, 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; } listDrainageFields() { return this.prisma.drainageField.findMany({ orderBy: { createdAt: 'desc' } }); } createDrainageField(data: CreateDrainageFieldDto) { return this.prisma.drainageField.create({ data: { code: data.code, name: data.name, fieldType: data.fieldType, required: data.required ?? false, status: data.status ?? 'active', description: data.description, }, }); } 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, }, }); } }