feat: unify drainage targets and carrier status UI

This commit is contained in:
hectorzhao
2026-08-28 17:24:01 +08:00
parent 9ac41e9308
commit c68ac7a3db
25 changed files with 265 additions and 65 deletions
+16 -7
View File
@@ -11,6 +11,16 @@ import { SmsReportValidationService } from './report-validation.service';
import { SmsAuditService } from './audit.service';
import { shanghaiDateRange } from '../common/shanghai-date-range';
const DRAINAGE_TARGET_PATTERN = /^(?:https?:\/\/\S+|(?:\+?86[\s-]?)?1(?:[\s-]?\d){10}|(?:\+?86[\s-]?)?(?:\(?0\d{2,3}\)?[\s-]?)?\d{7,8}(?:[\s-]?(?:转|ext\.?)?[\s-]?\d{1,6})?)$/i;
function normalizeDrainageTarget(value?: string) {
const target = value?.trim() ?? '';
if (!target || !DRAINAGE_TARGET_PATTERN.test(target)) {
throw new BadRequestException('引流信息必须是 http/https URL、手机号码或固定电话号码');
}
return target;
}
/** R3 domain service. Kept framework-agnostic and composed behind SmsConfigService. */
export class SmsDrainageService {
constructor(private readonly prisma: PrismaService, private readonly reportValidation: SmsReportValidationService, private readonly audit: SmsAuditService) {}
@@ -70,7 +80,7 @@ export class SmsDrainageService {
if (!signature) throw new NotFoundException('Signature not found');
if (tenantId && signature.tenantId !== tenantId) throw new NotFoundException('Signature not found');
if (signature.auditStatus !== 'approved') throw new BadRequestException('签名审核通过后才能新增引流信息');
if (!data.siteName?.trim() || !data.url?.trim()) throw new BadRequestException('siteName and url are required');
const target = normalizeDrainageTarget(data.url);
await this.reportValidation.validateDrainageReportValues(signature.applicationId ?? undefined, data.reportValues);
const auditStatus = options.initialAuditStatus ?? 'pending';
const item = await this.prisma.smsDrainageInfo.create({
@@ -78,8 +88,8 @@ export class SmsDrainageService {
tenantId: signature.tenantId,
signatureId,
applicationId: signature.applicationId,
siteName: data.siteName.trim(),
url: data.url.trim(),
siteName: target,
url: target,
remark: data.remark,
reportValues: data.reportValues as Prisma.InputJsonValue | undefined,
auditStatus,
@@ -104,8 +114,7 @@ export class SmsDrainageService {
if (!current) throw new NotFoundException('Drainage info not found');
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
if (current.auditStatus === 'deleted') throw new BadRequestException('已删除的引流信息不能修改');
if (data.siteName !== undefined && !data.siteName.trim()) throw new BadRequestException('siteName is required');
if (data.url !== undefined && !data.url.trim()) throw new BadRequestException('url is required');
const target = data.url === undefined ? undefined : normalizeDrainageTarget(data.url);
const applicationId = current.signature.applicationId ?? current.applicationId ?? undefined;
await this.reportValidation.validateDrainageReportValues(applicationId, data.reportValues ?? (isRecord(current.reportValues) ? current.reportValues : {}));
const auditStatus = options.initialAuditStatus ?? 'pending';
@@ -113,8 +122,8 @@ export class SmsDrainageService {
where: { id: itemId },
data: {
applicationId,
siteName: data.siteName?.trim(),
url: data.url?.trim(),
siteName: target,
url: target,
remark: data.remark,
reportValues: data.reportValues as Prisma.InputJsonValue | undefined,
auditStatus,
+1 -1
View File
@@ -54,7 +54,7 @@ export type UpdateSmsSignatureDto = Partial<Omit<CreateSmsSignatureDto, 'tenantI
};
export interface CreateSmsDrainageInfoDto {
siteName: string;
siteName?: string;
url: string;
remark?: string;
reportValues?: Record<string, unknown>;
@@ -967,7 +967,7 @@ describe('SmsConfigService', () => {
}]);
const service = new SmsConfigService(prisma as never);
await expect(service.createDrainageInfo('sig-1', { siteName: '官网', url: 'https://example.com', reportValues: {} }, {}, 'tenant-1'))
await expect(service.createDrainageInfo('sig-1', { url: 'https://example.com', reportValues: {} }, {}, 'tenant-1'))
.rejects.toThrow('引流信息缺少必填报备资料:网站主体');
expect(prisma.smsDrainageInfo.create).not.toHaveBeenCalled();
});
@@ -1014,9 +1014,12 @@ describe('SmsConfigService', () => {
prisma.channelRouteRule.findMany.mockResolvedValue([]);
const service = new SmsConfigService(prisma as never);
await expect(service.createDrainageInfo('sig-1', { siteName: '官网', url: 'https://example.com', reportValues: {} }, {}, 'tenant-1'))
await expect(service.createDrainageInfo('sig-1', { url: '13800138000', reportValues: {} }, {}, 'tenant-1'))
.resolves.toEqual(expect.objectContaining({ id: 'drainage-1', auditStatus: 'pending' }));
expect(prisma.smsDrainageInfo.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ siteName: '13800138000', url: '13800138000' }),
}));
expect(prisma.auditRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ targetType: 'sms_drainage_info', action: 'submit', statusAfter: 'pending' }) });
expect(prisma.channelSignatureReportTask.create).not.toHaveBeenCalled();
});