import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import type { ReviewDto, StatusChangeDto } from './sms-config.contracts'; import { SmsApplicationLifecycleService } from './application-lifecycle.service'; import { SmsReportValidationService } from './report-validation.service'; import { writeUniqueSignature } from './signature-uniqueness'; /** R3 domain service. Kept framework-agnostic and composed behind SmsConfigService. */ export class SmsAuditService { constructor( private readonly prisma: PrismaService, private readonly lifecycle: SmsApplicationLifecycleService, private readonly reportValidation: SmsReportValidationService, ) {} listAuditRecords(targetType?: string, targetId?: string) { return this.prisma.auditRecord.findMany({ where: { targetType, targetId, }, include: { reviewer: { select: { id: true, username: true, displayName: true } }, }, orderBy: { createdAt: 'desc' }, }); } approveSignature(signatureId: string, data: ReviewDto) { return this.reviewSignature(signatureId, 'approved', 'approve', data); } rejectSignature(signatureId: string, data: ReviewDto) { return this.reviewSignature(signatureId, 'rejected', 'reject', data); } approveTemplate(templateId: string, data: ReviewDto) { return this.reviewTemplate(templateId, 'approved', 'approve', data); } rejectTemplate(templateId: string, data: ReviewDto) { return this.reviewTemplate(templateId, 'rejected', 'reject', data); } async changeSignatureStatus(signatureId: string, data: StatusChangeDto, tenantId?: string) { const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } }); if (!signature || (tenantId && signature.tenantId !== tenantId)) { throw new NotFoundException('Signature not found'); } const status = data.status ?? 'deleted'; const updated = await writeUniqueSignature(this.prisma, { ...signature, auditStatus: status }, () => this.prisma.smsSignature.update({ where: { id: signatureId }, data: { auditStatus: status } }), ); await this.lifecycle.writeOperationLog( signature.tenantId, data.operatorId, `sms_signature.${status}`, 'sms_signature', signatureId, { statusBefore: signature.auditStatus, statusAfter: status, reason: data.reason, }, ); return updated; } async changeTemplateStatus(templateId: string, data: StatusChangeDto, tenantId?: string) { const template = await this.prisma.smsTemplate.findUnique({ where: { id: templateId } }); if (!template || (tenantId && template.tenantId !== tenantId)) { throw new NotFoundException('Template not found'); } const status = data.status ?? 'deleted'; const updated = await this.prisma.smsTemplate.update({ where: { id: templateId }, data: { auditStatus: status } }); await this.lifecycle.writeOperationLog( template.tenantId, data.operatorId, `sms_template.${status}`, 'sms_template', templateId, { statusBefore: template.auditStatus, statusAfter: status, reason: data.reason, }, ); return updated; } async reviewSignature(signatureId: string, statusAfter: string, action: string, data: ReviewDto) { const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } }); if (!signature) { throw new NotFoundException('Signature not found'); } const reviewerId = await this.resolveReviewerId(data.reviewerId); const updated = await writeUniqueSignature(this.prisma, { ...signature, auditStatus: statusAfter }, () => this.prisma.smsSignature.update({ where: { id: signatureId }, data: { auditStatus: statusAfter, rejectReason: statusAfter === 'rejected' ? data.reason : null, }, }), ); await this.createAuditRecord({ tenantId: signature.tenantId, targetType: 'sms_signature', targetId: signatureId, action, statusBefore: signature.auditStatus, statusAfter, reason: data.reason, reviewerId, }); return updated; } async reviewDrainageInfo(itemId: string, statusAfter: string, action: string, data: ReviewDto) { const item = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId } }); if (!item) throw new NotFoundException('Drainage info not found'); if (!['pending', 'rejected'].includes(item.auditStatus)) { throw new BadRequestException('只有待审核或已驳回的引流信息可以审核'); } if (statusAfter === 'rejected' && !data.reason?.trim()) { throw new BadRequestException('驳回引流信息时必须填写原因'); } const reviewerId = await this.resolveReviewerId(data.reviewerId); const updated = await this.prisma.smsDrainageInfo.update({ where: { id: itemId }, data: { auditStatus: statusAfter, rejectReason: statusAfter === 'rejected' ? data.reason?.trim() : null, reviewedAt: new Date(), }, include: { tenant: true, signature: true, application: true }, }); await this.createAuditRecord({ tenantId: item.tenantId, targetType: 'sms_drainage_info', targetId: itemId, action, statusBefore: item.auditStatus, statusAfter, reason: data.reason, reviewerId, }); if (statusAfter === 'approved') await this.reportValidation.activateDrainageReporting(itemId); else await this.reportValidation.suspendDrainageReporting(itemId, data.reason?.trim() || '引流信息运营审核驳回'); return updated; } async reviewTemplate(templateId: string, statusAfter: string, action: string, data: ReviewDto) { const template = await this.prisma.smsTemplate.findUnique({ where: { id: templateId } }); if (!template) { throw new NotFoundException('Template not found'); } const reviewerId = await this.resolveReviewerId(data.reviewerId); const updated = await this.prisma.smsTemplate.update({ where: { id: templateId }, data: { auditStatus: statusAfter, rejectReason: statusAfter === 'rejected' ? data.reason : null, }, }); await this.createAuditRecord({ tenantId: template.tenantId, targetType: 'sms_template', targetId: templateId, action, statusBefore: template.auditStatus, statusAfter, reason: data.reason, reviewerId, }); return updated; } async resolveReviewerId(reviewerId?: string) { if (!reviewerId) { return undefined; } const reviewer = await this.prisma.user.findUnique({ where: { id: reviewerId }, select: { id: true } }); if (!reviewer) { throw new BadRequestException('reviewerId does not reference an existing user'); } return reviewerId; } createAuditRecord(data: Prisma.AuditRecordUncheckedCreateInput) { return this.prisma.auditRecord.create({ data }); } }