feat: polish reporting templates and shared controls
This commit is contained in:
@@ -79,7 +79,7 @@ function createPrismaMock() {
|
||||
drainageItems: [],
|
||||
reportTasks: [],
|
||||
}]),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', auditStatus: 'pending' }),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '签名A', auditStatus: 'pending' }),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'sig-new', tenantId: 'tenant-1', ...data })),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'sig-1', tenantId: 'tenant-1', ...data })),
|
||||
},
|
||||
@@ -121,7 +121,7 @@ function createPrismaMock() {
|
||||
signature: { id: 'sig-1', name: '签名A' },
|
||||
variables: [{ name: 'name', required: true }],
|
||||
}]),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'tpl-1', tenantId: 'tenant-1', auditStatus: 'pending' }),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'tpl-1', tenantId: 'tenant-1', applicationId: 'app-1', signatureId: 'sig-1', content: '【签名A】您好${name}', auditStatus: 'pending' }),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'tpl-1', tenantId: 'tenant-1', ...data })),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'tpl-new', ...data })),
|
||||
},
|
||||
@@ -832,7 +832,7 @@ describe('SmsConfigService', () => {
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '运营添加模板',
|
||||
content: '您的验证码为${code}',
|
||||
content: '【签名A】您的验证码为${code}',
|
||||
variables: [{ name: 'code', example: '123456', required: true }],
|
||||
}, { initialAuditStatus: 'approved' })).resolves.toEqual(expect.objectContaining({ id: 'tpl-new' }));
|
||||
|
||||
@@ -842,6 +842,7 @@ describe('SmsConfigService', () => {
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '运营添加模板',
|
||||
content: '【签名A】您的验证码为${code}',
|
||||
auditStatus: 'approved',
|
||||
variables: {
|
||||
create: [{ name: 'code', example: '123456', required: true }],
|
||||
@@ -868,7 +869,7 @@ describe('SmsConfigService', () => {
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '模板B',
|
||||
content: '验证码${code}',
|
||||
content: '【签名A】验证码${code}',
|
||||
variables: [{ name: 'code', example: '123456', required: true }],
|
||||
})).resolves.toEqual(expect.objectContaining({ id: 'tpl-1', name: '模板B' }));
|
||||
|
||||
@@ -879,7 +880,7 @@ describe('SmsConfigService', () => {
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '模板B',
|
||||
content: '验证码${code}',
|
||||
content: '【签名A】验证码${code}',
|
||||
variables: {
|
||||
create: [{ name: 'code', example: '123456', required: true }],
|
||||
},
|
||||
@@ -887,4 +888,26 @@ describe('SmsConfigService', () => {
|
||||
include: { variables: true, application: true, tenant: true, signature: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('requires the selected signature at the start of template content', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.createTemplate({
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
name: '缺少签名模板',
|
||||
content: '您的验证码为${code}',
|
||||
})).rejects.toThrow('短信模板必须选择短信签名');
|
||||
|
||||
await expect(service.createTemplate({
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '签名不匹配模板',
|
||||
content: '【其他签名】您的验证码为${code}',
|
||||
})).rejects.toThrow('模板内容必须以所选短信签名 【签名A】 开头');
|
||||
|
||||
expect(prisma.smsTemplate.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user