feat: complete cmpp platform phases 0-5

This commit is contained in:
hectorzhao
2026-07-01 13:22:04 +08:00
parent 824a8b334f
commit ee926fea04
86 changed files with 10490 additions and 14 deletions
+292
View File
@@ -0,0 +1,292 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { randomBytes, createHash } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';
export interface CreateSmsApplicationDto {
tenantId: string;
name: string;
scene?: string;
callbackUrl?: string;
dailyLimit?: number;
maxPhonesPerTask?: number;
templateMismatchMode?: string;
ipAllowlist?: string[];
}
export interface CreateSmsSignatureDto {
tenantId: string;
applicationId?: string;
name: string;
purpose?: string;
drainageInfo?: Record<string, unknown>;
}
export interface CreateSignatureMaterialDto {
signatureId: string;
fileObjectId?: string;
materialType: string;
title: string;
description?: string;
}
export interface CreateSmsTemplateDto {
tenantId: string;
applicationId: string;
signatureId?: string;
name: string;
content: string;
category?: string;
variables?: Array<{ name: string; example?: string; required?: boolean }>;
}
export interface ReviewDto {
reviewerId?: string;
reason?: string;
}
@Injectable()
export class SmsConfigService {
constructor(private readonly prisma: PrismaService) {}
listApplications(tenantId?: string) {
return this.prisma.smsApplication.findMany({
where: tenantId ? { tenantId } : undefined,
include: { ipAllowlist: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
}
createApplication(data: CreateSmsApplicationDto) {
const secret = randomBytes(24).toString('hex');
return this.prisma.smsApplication.create({
data: {
tenantId: data.tenantId,
name: data.name,
scene: data.scene,
callbackUrl: data.callbackUrl,
secretHash: hashSecret(secret),
dailyLimit: data.dailyLimit,
maxPhonesPerTask: data.maxPhonesPerTask ?? 1000000,
templateMismatchMode: data.templateMismatchMode ?? 'reject',
ipAllowlist: {
create: (data.ipAllowlist ?? []).map((ipCidr) => ({ ipCidr })),
},
},
include: { ipAllowlist: true },
});
}
listSignatures(tenantId?: string) {
return this.prisma.smsSignature.findMany({
where: tenantId ? { tenantId } : undefined,
include: { materials: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
}
createSignature(data: CreateSmsSignatureDto) {
return this.prisma.smsSignature.create({
data: {
tenantId: data.tenantId,
applicationId: data.applicationId,
name: data.name,
purpose: data.purpose,
drainageInfo: data.drainageInfo as Prisma.InputJsonValue | undefined,
},
});
}
createSignatureMaterial(data: CreateSignatureMaterialDto) {
return this.prisma.signatureMaterial.create({
data: {
signatureId: data.signatureId,
fileObjectId: data.fileObjectId,
materialType: data.materialType,
title: data.title,
description: data.description,
},
});
}
async submitSignature(signatureId: string) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature) {
throw new NotFoundException('Signature not found');
}
const updated = await this.prisma.smsSignature.update({
where: { id: signatureId },
data: { auditStatus: 'pending', rejectReason: null },
});
await this.createAuditRecord({
tenantId: signature.tenantId,
targetType: 'sms_signature',
targetId: signatureId,
action: 'submit',
statusBefore: signature.auditStatus,
statusAfter: 'pending',
});
return updated;
}
listTemplates(tenantId?: string) {
return this.prisma.smsTemplate.findMany({
where: tenantId ? { tenantId } : undefined,
include: { variables: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
}
createTemplate(data: CreateSmsTemplateDto) {
return this.prisma.smsTemplate.create({
data: {
tenantId: data.tenantId,
applicationId: data.applicationId,
signatureId: data.signatureId,
name: data.name,
content: data.content,
category: data.category,
billingUnits: estimateBillingUnits(data.content),
variables: {
create: (data.variables ?? inferTemplateVariables(data.content)).map((variable: TemplateVariableInput) => ({
name: variable.name,
example: variable.example,
required: variable.required ?? true,
})),
},
},
include: { variables: true },
});
}
async submitTemplate(templateId: string) {
const template = await this.prisma.smsTemplate.findUnique({ where: { id: templateId } });
if (!template) {
throw new NotFoundException('Template not found');
}
const updated = await this.prisma.smsTemplate.update({
where: { id: templateId },
data: { auditStatus: 'pending', rejectReason: null },
});
await this.createAuditRecord({
tenantId: template.tenantId,
targetType: 'sms_template',
targetId: templateId,
action: 'submit',
statusBefore: template.auditStatus,
statusAfter: 'pending',
});
return updated;
}
listAuditRecords(targetType?: string, targetId?: string) {
return this.prisma.auditRecord.findMany({
where: {
targetType,
targetId,
},
orderBy: { createdAt: 'desc' },
take: 100,
});
}
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);
}
private 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 updated = await 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: data.reviewerId,
});
return updated;
}
private 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 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: data.reviewerId,
});
return updated;
}
private createAuditRecord(data: Prisma.AuditRecordUncheckedCreateInput) {
return this.prisma.auditRecord.create({ data });
}
}
interface TemplateVariableInput {
name: string;
example?: string;
required?: boolean;
}
function hashSecret(secret: string) {
return createHash('sha256').update(secret).digest('hex');
}
function estimateBillingUnits(content: string) {
const length = [...content].length;
if (length <= 70) {
return 1;
}
return Math.ceil(length / 67);
}
function inferTemplateVariables(content: string): TemplateVariableInput[] {
const matches = content.match(/\$\{[a-zA-Z0-9_]+\}/g) ?? [];
return [...new Set(matches)].map((match) => ({ name: match.slice(2, -1), required: true }));
}