feat: polish reporting templates and shared controls

This commit is contained in:
hectorzhao
2026-07-15 16:31:49 +08:00
parent 28fb8e9038
commit a7a4e8d9f6
21 changed files with 488 additions and 119 deletions
+41 -7
View File
@@ -95,7 +95,8 @@ export interface CreateSmsTemplateOptions {
initialAuditStatus?: string;
}
export type UpdateSmsTemplateDto = Partial<Omit<CreateSmsTemplateDto, 'tenantId'>> & {
export type UpdateSmsTemplateDto = Partial<Omit<CreateSmsTemplateDto, 'tenantId' | 'signatureId'>> & {
signatureId?: string | null;
auditStatus?: string;
};
@@ -1135,7 +1136,12 @@ export class SmsConfigService {
});
}
createTemplate(data: CreateSmsTemplateDto, options: CreateSmsTemplateOptions = {}) {
async createTemplate(data: CreateSmsTemplateDto, options: CreateSmsTemplateOptions = {}) {
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 the template tenant');
}
await this.validateTemplateSignature(data.signatureId, data.tenantId, data.applicationId, data.content);
return this.prisma.smsTemplate.create({
data: {
tenantId: data.tenantId,
@@ -1169,11 +1175,13 @@ export class SmsConfigService {
throw new BadRequestException('applicationId does not belong to the template tenant');
}
}
if (data.signatureId) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: data.signatureId }, select: { tenantId: true } });
if (!signature || signature.tenantId !== template.tenantId) {
throw new BadRequestException('signatureId does not belong to the template tenant');
}
if (data.signatureId !== undefined || data.applicationId !== undefined || data.content !== undefined) {
await this.validateTemplateSignature(
data.signatureId === undefined ? template.signatureId : data.signatureId,
template.tenantId,
data.applicationId ?? template.applicationId,
data.content ?? template.content,
);
}
const variables = data.variables ?? (data.content ? inferTemplateVariables(data.content) : undefined);
return this.prisma.$transaction(async (tx) => {
@@ -1208,6 +1216,7 @@ export class SmsConfigService {
if (!template) {
throw new NotFoundException('Template not found');
}
await this.validateTemplateSignature(template.signatureId, template.tenantId, template.applicationId, template.content);
const updated = await this.prisma.smsTemplate.update({
where: { id: templateId },
@@ -1224,6 +1233,26 @@ export class SmsConfigService {
return updated;
}
private async validateTemplateSignature(signatureId: string | null | undefined, tenantId: string, applicationId: string, content: string) {
if (!signatureId) {
throw new BadRequestException('短信模板必须选择短信签名');
}
const signature = await this.prisma.smsSignature.findUnique({
where: { id: signatureId },
select: { tenantId: true, applicationId: true, name: true },
});
if (!signature || signature.tenantId !== tenantId) {
throw new BadRequestException('signatureId does not belong to the template tenant');
}
if (signature.applicationId && signature.applicationId !== applicationId) {
throw new BadRequestException('signatureId does not belong to the template application');
}
const signaturePrefix = normalizeSmsSignature(signature.name);
if (!signaturePrefix || !content.startsWith(signaturePrefix)) {
throw new BadRequestException(`模板内容必须以所选短信签名 ${signaturePrefix || signature.name} 开头`);
}
}
listAuditRecords(targetType?: string, targetId?: string) {
return this.prisma.auditRecord.findMany({
where: {
@@ -1435,6 +1464,11 @@ function inferTemplateVariables(content: string): TemplateVariableInput[] {
return [...new Set(matches)].map((match) => ({ name: match.slice(2, -1), required: true }));
}
function normalizeSmsSignature(name: string) {
const innerName = name.trim().replace(/^[【\[]+|[】\]]+$/g, '').trim();
return innerName ? `${innerName}` : '';
}
function startOfToday() {
const date = new Date();
date.setHours(0, 0, 0, 0);