fix: enforce application limits and signature format

This commit is contained in:
hectorzhao
2026-07-22 16:29:22 +08:00
parent cd085d2712
commit a09036c67b
20 changed files with 479 additions and 79 deletions
+27 -5
View File
@@ -323,6 +323,7 @@ describe('SmsConfigService', () => {
interfaceEnabled: false,
interfaceType: 'cmpp20',
queuePriority: 'priority',
dailyLimit: 100000,
downstreamReceiptRetryEnabled: true,
downstreamUplinkRetryEnabled: true,
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },
@@ -955,20 +956,41 @@ describe('SmsConfigService', () => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await service.createSignature({ tenantId: 'tenant-1', name: '运营新建签名' }, { initialAuditStatus: 'approved' });
await service.createSignature({ tenantId: 'tenant-1', name: '运营新建签名' }, { initialAuditStatus: 'approved' });
expect(prisma.smsSignature.create).toHaveBeenCalledWith({
data: expect.objectContaining({ auditStatus: 'approved', name: '运营新建签名' }),
data: expect.objectContaining({ auditStatus: 'approved', name: '运营新建签名' }),
});
expect(prisma.auditRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: 'admin_create_approved', statusAfter: 'approved' }) });
});
it.each(['未带括号', '[英文括号]', '【【重复括号】】', '【 】'])(
'rejects a signature name without exactly one complete Chinese black bracket pair: %s',
async (name) => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await expect(service.createSignature({ tenantId: 'tenant-1', name }))
.rejects.toThrow('短信签名必须包含完整中文黑括号,例如:【某某科技】');
expect(prisma.smsSignature.create).not.toHaveBeenCalled();
},
);
it('rejects editing a signature to a name without complete Chinese black brackets', async () => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await expect(service.updateSignature('sig-1', { name: '编辑无括号' }))
.rejects.toThrow('短信签名必须包含完整中文黑括号,例如:【某某科技】');
expect(prisma.smsSignature.update).not.toHaveBeenCalled();
});
it('updates enterprise signature drainage info through the admin API path', async () => {
const prisma = createPrismaMock();
const service = new SmsConfigService(prisma as never);
await expect(service.updateSignature('sig-1', {
name: '签名B',
name: '签名B',
auditStatus: 'approved',
drainageInfo: {
carrierStatus: { mobile: 'approved', unicom: 'approved', telecom: 'approved' },
@@ -976,14 +998,14 @@ describe('SmsConfigService', () => {
},
})).resolves.toEqual(expect.objectContaining({
id: 'sig-1',
name: '签名B',
name: '签名B',
auditStatus: 'approved',
}));
expect(prisma.smsSignature.update).toHaveBeenCalledWith({
where: { id: 'sig-1' },
data: expect.objectContaining({
name: '签名B',
name: '签名B',
auditStatus: 'approved',
drainageInfo: expect.objectContaining({
carrierStatus: expect.objectContaining({ mobile: 'approved' }),
+18 -6
View File
@@ -385,7 +385,7 @@ export class SmsConfigService {
interfaceType,
cmppMaxConnections: getPositiveInteger(data.cmppMaxConnections, 1, 'cmppMaxConnections'),
cmppWindowSize: getPositiveInteger(data.cmppWindowSize, 16, 'cmppWindowSize'),
dailyLimit: data.dailyLimit,
dailyLimit: getPositiveInteger(data.dailyLimit, 100000, 'dailyLimit'),
customerUnitPrice: data.customerUnitPrice ?? 0,
queuePriority,
maxPhonesPerTask: data.maxPhonesPerTask ?? 1000000,
@@ -452,7 +452,7 @@ export class SmsConfigService {
interfaceType,
cmppMaxConnections: data.cmppMaxConnections === undefined ? undefined : getPositiveInteger(data.cmppMaxConnections, 1, 'cmppMaxConnections'),
cmppWindowSize: data.cmppWindowSize === undefined ? undefined : getPositiveInteger(data.cmppWindowSize, 16, 'cmppWindowSize'),
dailyLimit: data.dailyLimit,
dailyLimit: data.dailyLimit === undefined ? undefined : getPositiveInteger(data.dailyLimit, 100000, 'dailyLimit'),
customerUnitPrice: data.customerUnitPrice,
queuePriority,
maxPhonesPerTask: data.maxPhonesPerTask,
@@ -799,6 +799,7 @@ export class SmsConfigService {
}));
return {
...signature,
name: normalizeSmsSignature(signature.name),
drainageInfo: { ...legacyPayload, links: drainageLinks },
reportTargets: (() => {
const channels = routes.filter((route) => route.applicationId === signature.applicationId && route.group).flatMap((route) => route.group!.items.map((item) => item.channel)).filter((channel) => channel.status !== 'deleted');
@@ -894,7 +895,7 @@ export class SmsConfigService {
id: signature.id,
tenantId: signature.tenantId,
applicationId: signature.applicationId,
name: signature.name,
name: normalizeSmsSignature(signature.name),
purpose: signature.purpose,
auditStatus: signature.auditStatus,
reportStatus: signature.reportStatus,
@@ -977,11 +978,12 @@ export class SmsConfigService {
async createSignature(data: CreateSmsSignatureDto, options: CreateSmsSignatureOptions = {}) {
await this.validateSignatureReportValues(data.applicationId, data.drainageInfo);
const drainageInfo = await this.withReportRequirementSnapshot(data.applicationId, data.drainageInfo);
const name = validateCompleteSmsSignature(data.name);
const signature = await this.prisma.smsSignature.create({
data: {
tenantId: data.tenantId,
applicationId: data.applicationId,
name: data.name,
name,
purpose: data.purpose,
auditStatus: options.initialAuditStatus,
drainageInfo: drainageInfo as Prisma.InputJsonValue | undefined,
@@ -1011,8 +1013,9 @@ export class SmsConfigService {
const drainageInfo = data.drainageInfo
? await this.withReportRequirementSnapshot(applicationId, data.drainageInfo)
: undefined;
const name = data.name === undefined ? undefined : validateCompleteSmsSignature(data.name);
const materialChanged = (data.applicationId !== undefined && data.applicationId !== signature.applicationId)
|| (data.name !== undefined && normalizeSmsSignature(data.name) !== normalizeSmsSignature(signature.name))
|| (name !== undefined && name !== normalizeSmsSignature(signature.name))
|| (data.purpose !== undefined && data.purpose !== signature.purpose)
|| (data.drainageInfo !== undefined && JSON.stringify(data.drainageInfo) !== JSON.stringify(signature.drainageInfo ?? null));
const auditStatus = materialChanged && signature.auditStatus === 'approved' ? 'pending' : data.auditStatus;
@@ -1020,7 +1023,7 @@ export class SmsConfigService {
where: { id: signatureId },
data: {
applicationId: data.applicationId,
name: data.name,
name,
purpose: data.purpose,
auditStatus,
rejectReason: auditStatus === 'pending' ? null : undefined,
@@ -1728,6 +1731,15 @@ function normalizeSmsSignature(name: string) {
return innerName ? `${innerName}` : '';
}
function validateCompleteSmsSignature(name: string) {
const value = name.trim();
const match = value.match(/^【([^【】]+)】$/);
if (!match || match[1] !== match[1].trim()) {
throw new BadRequestException('短信签名必须包含完整中文黑括号,例如:【某某科技】');
}
return value;
}
function startOfToday() {
const date = new Date();
date.setHours(0, 0, 0, 0);