feat: add channel report batch briefs
This commit is contained in:
@@ -111,16 +111,71 @@ export class ReportBatchGenerationService {
|
||||
async getBatch(batchId: string) {
|
||||
const batch = await this.prisma.reportMaterialBatch.findUnique({
|
||||
where: { id: batchId },
|
||||
include: { exportFiles: true, items: true },
|
||||
include: { exportFiles: { include: { items: true } }, items: true },
|
||||
});
|
||||
if (!batch) throw new NotFoundException('报备批次不存在');
|
||||
const tasks = await this.collectBatchTasks(batchId);
|
||||
const successCount = tasks.filter((task) => task.status === 'approved').length;
|
||||
const channelIds = batch.exportFiles
|
||||
.map((file) => file.channelId)
|
||||
.filter((id): id is string => Boolean(id));
|
||||
const channels = channelIds.length
|
||||
? await this.prisma.smsChannel.findMany({
|
||||
where: { id: { in: channelIds } },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
: [];
|
||||
const channelNameById = new Map(channels.map((channel) => [channel.id, channel.name]));
|
||||
const batchItemById = new Map(batch.items.map((item) => [item.id, item]));
|
||||
const dateParts = new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
})
|
||||
.formatToParts(batch.createdAt)
|
||||
.reduce<Record<string, string>>((parts, part) => ({ ...parts, [part.type]: part.value }), {});
|
||||
const batchDate = `${dateParts.year}年${dateParts.month}月${dateParts.day}日`;
|
||||
const briefs = batch.exportFiles
|
||||
.filter((file): file is typeof file & { channelId: string } => Boolean(file.channelId))
|
||||
.map((file) => {
|
||||
const lines = [...file.items]
|
||||
.sort((left, right) => left.rowNumber - right.rowNumber)
|
||||
.flatMap((entry, index) => {
|
||||
const batchItem = batchItemById.get(entry.batchItemId);
|
||||
if (!batchItem) return [];
|
||||
const snapshot = jsonRecord(batchItem.snapshot);
|
||||
const signature = jsonRecord(snapshot.signature);
|
||||
const rawSignatureName = String(signature.name ?? '').trim();
|
||||
const signatureName = rawSignatureName.replace(/^【|】$/g, '');
|
||||
const smsContents = jsonRecord(snapshot.briefSmsContentByChannel);
|
||||
const smsContent = String(smsContents[file.channelId] ?? '');
|
||||
if (batchItem.reportType === 'drainage') {
|
||||
const drainage = jsonRecord(snapshot.drainage);
|
||||
const drainageValue = String(drainage.url ?? drainage.siteName ?? '').trim();
|
||||
return [`${index + 1}.引流信息:${drainageValue},短信内容:${smsContent};`];
|
||||
}
|
||||
return [`${index + 1}.短信签名:【${signatureName}】,短信内容:${smsContent};`];
|
||||
});
|
||||
return {
|
||||
channelId: file.channelId,
|
||||
channelName: channelNameById.get(file.channelId) ?? file.channelId,
|
||||
fileId: file.id,
|
||||
fileName: file.fileName,
|
||||
itemCount: lines.length,
|
||||
content: [
|
||||
`尊敬的供应商您好,今天是${batchDate},辛苦报备以下签名或引流信息:`,
|
||||
...lines,
|
||||
`报备材料在表格里,报备批次号${batch.batchNo}。`,
|
||||
].join('\n'),
|
||||
};
|
||||
});
|
||||
return {
|
||||
...batch,
|
||||
reportTotal: tasks.length,
|
||||
successCount,
|
||||
successRate: tasks.length ? successCount / tasks.length : 0,
|
||||
briefs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -305,6 +360,7 @@ export class ReportBatchGenerationService {
|
||||
}
|
||||
}
|
||||
const exportedFiles = [];
|
||||
const briefSmsContentByItem = new Map<string, Record<string, string>>();
|
||||
const incomplete = new Set<string>(
|
||||
prepared.filter((item) => item.channels.length === 0).map((item) => item.batchItem.id),
|
||||
);
|
||||
@@ -312,9 +368,25 @@ export class ReportBatchGenerationService {
|
||||
for (const [channelId, items] of channelMap) {
|
||||
const result = await this.channelExport.exportChannelBatch(batch.id, channelId, items);
|
||||
exportedFiles.push(result.file);
|
||||
for (const entry of result.briefItems) {
|
||||
const current = briefSmsContentByItem.get(entry.batchItemId) ?? {};
|
||||
current[channelId] = entry.smsContent;
|
||||
briefSmsContentByItem.set(entry.batchItemId, current);
|
||||
}
|
||||
failedTargetCount += result.incompleteBatchItemIds.length;
|
||||
for (const itemId of result.incompleteBatchItemIds) incomplete.add(itemId);
|
||||
}
|
||||
for (const item of prepared) {
|
||||
await this.prisma.reportMaterialBatchItem.update({
|
||||
where: { id: item.batchItem.id },
|
||||
data: {
|
||||
snapshot: {
|
||||
...jsonRecord(item.snapshot),
|
||||
briefSmsContentByChannel: briefSmsContentByItem.get(item.batchItem.id) ?? {},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const item of prepared) {
|
||||
if (incomplete.has(item.batchItem.id) || item.channels.length === 0) continue;
|
||||
if (item.reportType === 'signature')
|
||||
|
||||
@@ -73,6 +73,7 @@ export class ReportChannelExportService {
|
||||
const reportTypes = [...new Set(items.map((item) => item.reportType))];
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const fileRows: Array<{ item: (typeof items)[number]; taskId: string; rowNumber: number }> = [];
|
||||
const briefItems: Array<{ batchItemId: string; smsContent: string }> = [];
|
||||
const incompleteBatchItemIds: string[] = [];
|
||||
let totalRows = 0;
|
||||
for (const reportType of reportTypes) {
|
||||
@@ -94,6 +95,9 @@ export class ReportChannelExportService {
|
||||
const values = fields.map(
|
||||
(field) => resolveExportValue(item.snapshot, field.code, field.name) ?? field.defaultValue ?? '',
|
||||
);
|
||||
const smsContentIndex = fields.findIndex((field) => field.name.trim() === '短信内容');
|
||||
const smsContentValue = smsContentIndex >= 0 ? values[smsContentIndex] : '';
|
||||
const smsContent = isFileRef(smsContentValue) ? '' : String(smsContentValue ?? '');
|
||||
const missing = fields.filter((field, index) => field.required && !hasValue(values[index]));
|
||||
const missingReason =
|
||||
fields.length === 0
|
||||
@@ -186,6 +190,7 @@ export class ReportChannelExportService {
|
||||
}
|
||||
row.height = targetHeight;
|
||||
fileRows.push({ item, taskId: task.id, rowNumber: row.number });
|
||||
briefItems.push({ batchItemId: item.batchItem.id, smsContent });
|
||||
for (const entry of tasks)
|
||||
await this.recordTask(entry.task.id, channelId, entry.existingTask?.status, 'exporting');
|
||||
}
|
||||
@@ -219,7 +224,7 @@ export class ReportChannelExportService {
|
||||
rowNumber: entry.rowNumber,
|
||||
})),
|
||||
});
|
||||
return { file: { ...file, fileObject: uploaded }, incompleteBatchItemIds };
|
||||
return { file: { ...file, fileObject: uploaded }, incompleteBatchItemIds, briefItems };
|
||||
}
|
||||
|
||||
async getSingleMaterialDetail(data: SingleReportMaterialDto) {
|
||||
|
||||
@@ -213,6 +213,7 @@ describe('ReportMaterialsService', () => {
|
||||
reportMaterialBatchItem: {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
create: jest.fn().mockImplementation(() => Promise.resolve({ id: `batch-item-${++batchItemSequence}` })),
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
smsChannel: {
|
||||
findUnique: jest
|
||||
@@ -245,6 +246,28 @@ describe('ReportMaterialsService', () => {
|
||||
transform: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
code: 'sms_content_primary',
|
||||
name: '短信内容',
|
||||
exportName: '短信内容一',
|
||||
required: false,
|
||||
columnWidth: 24,
|
||||
imageWidth: 120,
|
||||
imageHeight: 80,
|
||||
transform: null,
|
||||
defaultValue: '第一条短信内容',
|
||||
},
|
||||
{
|
||||
code: 'sms_content_secondary',
|
||||
name: '短信内容',
|
||||
exportName: '短信内容二',
|
||||
required: false,
|
||||
columnWidth: 24,
|
||||
imageWidth: 120,
|
||||
imageHeight: 80,
|
||||
transform: null,
|
||||
defaultValue: '第二条短信内容',
|
||||
},
|
||||
]),
|
||||
},
|
||||
channelSignatureReportTask: {
|
||||
@@ -301,6 +324,18 @@ describe('ReportMaterialsService', () => {
|
||||
where: { id: 'signature-1' },
|
||||
data: { pendingReport: false },
|
||||
});
|
||||
expect(prisma.reportMaterialBatchItem.update).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.reportMaterialBatchItem.update).toHaveBeenCalledWith({
|
||||
where: { id: 'batch-item-1' },
|
||||
data: {
|
||||
snapshot: expect.objectContaining({
|
||||
briefSmsContentByChannel: {
|
||||
'channel-a': '第一条短信内容',
|
||||
'channel-b': '第一条短信内容',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(uploadedWorkbooks).toHaveLength(2);
|
||||
for (const buffer of uploadedWorkbooks) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
@@ -585,6 +620,71 @@ describe('ReportMaterialsService', () => {
|
||||
expect(prisma.reportMaterialBatch.findMany).toHaveBeenCalledWith(expect.objectContaining({ skip: 10, take: 10 }));
|
||||
});
|
||||
|
||||
it('returns one copyable brief per channel from the immutable batch snapshot', async () => {
|
||||
const batch = {
|
||||
id: 'batch-brief-1',
|
||||
batchNo: 'RB20260902090000TEST',
|
||||
createdAt: new Date('2026-09-02T01:00:00.000Z'),
|
||||
items: [
|
||||
{
|
||||
id: 'batch-item-signature',
|
||||
signatureId: 'signature-1',
|
||||
drainageItemId: null,
|
||||
reportType: 'signature',
|
||||
materialVersion: 1,
|
||||
snapshot: {
|
||||
signature: { name: '【测试签名】' },
|
||||
briefSmsContentByChannel: { 'channel-1': '【测试签名】您本次提交的验证码1234' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'batch-item-drainage',
|
||||
signatureId: 'signature-1',
|
||||
drainageItemId: 'drainage-1',
|
||||
reportType: 'drainage',
|
||||
materialVersion: 1,
|
||||
snapshot: {
|
||||
signature: { name: '测试签名' },
|
||||
drainage: { url: 'https://example.com' },
|
||||
briefSmsContentByChannel: { 'channel-1': '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
exportFiles: [
|
||||
{
|
||||
id: 'file-1',
|
||||
channelId: 'channel-1',
|
||||
fileName: '通道一.xlsx',
|
||||
items: [
|
||||
{ batchItemId: 'batch-item-signature', rowNumber: 2 },
|
||||
{ batchItemId: 'batch-item-drainage', rowNumber: 3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const prisma = {
|
||||
reportMaterialBatch: { findUnique: jest.fn().mockResolvedValue(batch) },
|
||||
channelSignatureReportTask: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
smsChannel: { findMany: jest.fn().mockResolvedValue([{ id: 'channel-1', name: '通道一' }]) },
|
||||
};
|
||||
const service = new ReportMaterialsService(prisma as never, {} as never, {} as never);
|
||||
|
||||
const result = await service.getBatch('batch-brief-1');
|
||||
|
||||
expect(result.briefs).toEqual([
|
||||
expect.objectContaining({
|
||||
channelId: 'channel-1',
|
||||
channelName: '通道一',
|
||||
itemCount: 2,
|
||||
content:
|
||||
'尊敬的供应商您好,今天是2026年9月2日,辛苦报备以下签名或引流信息:\n' +
|
||||
'1.短信签名:【测试签名】,短信内容:【测试签名】您本次提交的验证码1234;\n' +
|
||||
'2.引流信息:https://example.com,短信内容:;\n' +
|
||||
'报备材料在表格里,报备批次号RB20260902090000TEST。',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects malformed preflight items as a readable 400 before Prisma is called', async () => {
|
||||
const prisma = { smsSignature: { findUnique: jest.fn() } };
|
||||
const service = new ReportMaterialsService(prisma as never, {} as never, {} as never);
|
||||
|
||||
@@ -72,8 +72,8 @@ export class AdminSmsConfigController {
|
||||
}
|
||||
|
||||
@Get('enterprise-signatures')
|
||||
listSignatures(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('status') status?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('signatureKeyword') signatureKeyword?: string, @Query('drainageKeyword') drainageKeyword?: string, @Query('signatureSort') signatureSort?: 'asc' | 'desc', @Query('submittedAtFrom') submittedAtFrom?: string, @Query('submittedAtTo') submittedAtTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
||||
const query = { tenantId, keyword, status, enterpriseKeyword, applicationKeyword, signatureKeyword, drainageKeyword, signatureSort, submittedAtFrom, submittedAtTo, page: Number(page), pageSize: Number(pageSize) };
|
||||
listSignatures(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('status') status?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('signatureKeyword') signatureKeyword?: string, @Query('drainageKeyword') drainageKeyword?: string, @Query('submittedAtFrom') submittedAtFrom?: string, @Query('submittedAtTo') submittedAtTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
||||
const query = { tenantId, keyword, status, enterpriseKeyword, applicationKeyword, signatureKeyword, drainageKeyword, submittedAtFrom, submittedAtTo, page: Number(page), pageSize: Number(pageSize) };
|
||||
return page || pageSize ? this.smsConfig.listSignaturesPage(query) : this.smsConfig.listSignatures(query);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,8 +74,6 @@ export class SmsSignatureService {
|
||||
) {}
|
||||
async listSignatures(queryOrTenantId?: string | SignatureListQuery, summaryOnly = false) {
|
||||
const query = typeof queryOrTenantId === 'string' ? { tenantId: queryOrTenantId } : (queryOrTenantId ?? {});
|
||||
const signatureSort =
|
||||
query.signatureSort === 'asc' || query.signatureSort === 'desc' ? query.signatureSort : undefined;
|
||||
const signatures = await this.prisma.smsSignature.findMany({
|
||||
where: {
|
||||
id: query.signatureId,
|
||||
@@ -159,7 +157,7 @@ export class SmsSignatureService {
|
||||
select: { reportType: true, materialVersion: true, snapshot: true },
|
||||
},
|
||||
},
|
||||
orderBy: signatureSort ? [{ name: signatureSort }, { id: 'asc' }] : { createdAt: 'desc' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
...(query.page && query.pageSize
|
||||
? {
|
||||
skip: (query.page - 1) * query.pageSize,
|
||||
@@ -442,11 +440,97 @@ export class SmsSignatureService {
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
const [items, total] = await Promise.all([
|
||||
const [items, total, pendingReportDetailTotal] = await Promise.all([
|
||||
this.listSignatures({ ...query, page, pageSize }, true),
|
||||
this.prisma.smsSignature.count({ where }),
|
||||
this.countPendingReportDetails(where),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
return { items, total, page, pageSize, pendingReportDetailTotal };
|
||||
}
|
||||
|
||||
private async countPendingReportDetails(where: Prisma.SmsSignatureWhereInput) {
|
||||
const signatures = await this.prisma.smsSignature.findMany({
|
||||
where: { AND: [where, { auditStatus: 'approved', pendingReport: true }] },
|
||||
select: {
|
||||
id: true,
|
||||
applicationId: true,
|
||||
materialVersion: true,
|
||||
application: { select: { status: true } },
|
||||
reportTasks: {
|
||||
where: { reportType: 'signature' },
|
||||
select: { channelId: true, carrier: true, status: true, approvalScope: true },
|
||||
},
|
||||
reportBatchItems: {
|
||||
where: { reportType: 'signature', batch: { status: { in: ['completed', 'partial_failed'] } } },
|
||||
select: { materialVersion: true, snapshot: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
const applicationIds = [
|
||||
...new Set(signatures.map((signature) => signature.applicationId).filter((id): id is string => Boolean(id))),
|
||||
];
|
||||
const routes = applicationIds.length
|
||||
? await this.prisma.channelRouteRule.findMany({
|
||||
where: { applicationId: { in: applicationIds }, status: 'active' },
|
||||
select: {
|
||||
applicationId: true,
|
||||
group: {
|
||||
select: {
|
||||
status: true,
|
||||
items: {
|
||||
select: {
|
||||
channel: { select: { id: true, carrier: true, carriers: true, status: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: [];
|
||||
let total = 0;
|
||||
for (const signature of signatures) {
|
||||
if (!signature.applicationId || signature.application?.status !== 'active') continue;
|
||||
const generatedTargets = new Set<string>();
|
||||
for (const item of signature.reportBatchItems.filter(
|
||||
(entry) => entry.materialVersion === signature.materialVersion,
|
||||
)) {
|
||||
const businessKeys = isRecord(item.snapshot) ? item.snapshot.businessKeys : undefined;
|
||||
if (!Array.isArray(businessKeys)) continue;
|
||||
for (const value of businessKeys) {
|
||||
const match = typeof value === 'string' ? value.match(/:channel:([^:]+):carrier:([^:]+)$/) : null;
|
||||
if (!match) continue;
|
||||
for (const carrier of match[2].split(',').map((entry) => entry.trim()).filter(Boolean))
|
||||
generatedTargets.add(`${match[1]}:${carrier}`);
|
||||
}
|
||||
}
|
||||
const channels = [
|
||||
...new Map(
|
||||
routes
|
||||
.filter((route) => route.applicationId === signature.applicationId && route.group?.status === 'active')
|
||||
.flatMap((route) => route.group!.items.map((item) => item.channel))
|
||||
.filter((channel) => channel.status === 'active')
|
||||
.map((channel) => [channel.id, channel]),
|
||||
).values(),
|
||||
];
|
||||
for (const channel of channels) {
|
||||
for (const carrier of normalizeChannelCarriers(channel.carriers, channel.carrier)) {
|
||||
const task =
|
||||
signature.reportTasks.find(
|
||||
(candidate) => candidate.channelId === channel.id && candidate.carrier === carrier,
|
||||
) ??
|
||||
signature.reportTasks.find(
|
||||
(candidate) =>
|
||||
candidate.channelId === channel.id &&
|
||||
candidate.carrier === null &&
|
||||
candidate.approvalScope === 'legacy_channel',
|
||||
);
|
||||
if (task?.status === 'abandoned') continue;
|
||||
if (generatedTargets.has(`${channel.id}:${carrier}`) || generatedTargets.has(`${channel.id}:legacy`)) continue;
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
async getSignature(id: string) {
|
||||
|
||||
@@ -144,7 +144,6 @@ export interface SignatureListQuery {
|
||||
applicationKeyword?: string;
|
||||
signatureKeyword?: string;
|
||||
drainageKeyword?: string;
|
||||
signatureSort?: 'asc' | 'desc';
|
||||
submittedAtFrom?: string;
|
||||
submittedAtTo?: string;
|
||||
page?: number;
|
||||
|
||||
@@ -781,7 +781,7 @@ describe('SmsConfigService', () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.listSignatures({ enterpriseKeyword: '租户', applicationKeyword: '应用', signatureKeyword: '签名', drainageKeyword: '官网', signatureSort: 'asc' })).resolves.toEqual([
|
||||
await expect(service.listSignatures({ enterpriseKeyword: '租户', applicationKeyword: '应用', signatureKeyword: '签名', drainageKeyword: '官网' })).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'sig-1',
|
||||
tenant: expect.objectContaining({ name: '租户A' }),
|
||||
@@ -804,7 +804,7 @@ describe('SmsConfigService', () => {
|
||||
reportTasks: expect.objectContaining({ select: expect.any(Object) }),
|
||||
reportBatchItems: expect.any(Object),
|
||||
}),
|
||||
orderBy: [{ name: 'asc' }, { id: 'asc' }],
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -1223,6 +1223,7 @@ describe('SmsConfigService', () => {
|
||||
const result = await service.listSignaturesPage({ page: 1, pageSize: 10 });
|
||||
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.pendingReportDetailTotal).toBe(0);
|
||||
expect(result.items[0]).toEqual(expect.objectContaining({ id: 'sig-1', name: '【签名A】' }));
|
||||
expect(result.items[0]).not.toHaveProperty('materials');
|
||||
expect(result.items[0]).not.toHaveProperty('reportTasks');
|
||||
@@ -1230,6 +1231,56 @@ describe('SmsConfigService', () => {
|
||||
expect(result.items[0]).not.toHaveProperty('drainageReportTargets');
|
||||
});
|
||||
|
||||
it('returns the filtered total of signature channel-carrier details still awaiting batch generation', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.smsSignature.findMany.mockImplementation(({ where }) => {
|
||||
if (where?.AND) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 'sig-pending',
|
||||
applicationId: 'app-1',
|
||||
materialVersion: 2,
|
||||
application: { status: 'active' },
|
||||
reportTasks: [
|
||||
{ channelId: 'channel-1', carrier: 'mobile', status: 'abandoned', approvalScope: 'carrier_specific' },
|
||||
],
|
||||
reportBatchItems: [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
prisma.channelRouteRule.findMany.mockResolvedValue([
|
||||
{
|
||||
applicationId: 'app-1',
|
||||
group: {
|
||||
status: 'active',
|
||||
items: [
|
||||
{
|
||||
channel: {
|
||||
id: 'channel-1',
|
||||
status: 'active',
|
||||
carrier: 'mobile',
|
||||
carriers: ['mobile', 'unicom'],
|
||||
reportFields: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
] as never);
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
const result = await service.listSignaturesPage({ signatureKeyword: '测试', page: 1, pageSize: 10 });
|
||||
|
||||
expect(result.pendingReportDetailTotal).toBe(1);
|
||||
expect(prisma.smsSignature.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { AND: [expect.objectContaining({ name: { contains: '测试' } }), { auditStatus: 'approved', pendingReport: true }] },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'【带 空格】',
|
||||
' 【外部空格】',
|
||||
|
||||
Reference in New Issue
Block a user