fix: 修复报备导入映射与审核跳转

This commit is contained in:
hectorzhao
2026-09-05 00:20:33 +08:00
parent 65082959c0
commit 1a7a7245a6
12 changed files with 879 additions and 159 deletions
@@ -18,6 +18,7 @@ import {
hasValue,
normalizeImageExtension,
imageContentType,
duplicateCoreMappingKind,
} from './report-materials.helpers';
import { ReportImportParserService } from './import-parser.service';
import { compatibleImages, loadCompatibleWorkbook } from './workbook-compatibility';
@@ -36,6 +37,17 @@ export class ReportImportReviewService {
if (!batch) throw new NotFoundException('导入批次不存在');
if (batch.status !== 'analyzed') throw new ConflictException('该导入批次已提交审核,不能重复导入');
if (!data.mappings?.length) throw new BadRequestException('请至少配置一个导入字段映射');
const duplicateCoreKind = duplicateCoreMappingKind(data.mappings);
if (duplicateCoreKind) {
const label = {
signatureName: '短信签名',
purpose: '签名用途/依据',
siteName: '站点名称',
url: '引流 URL 或号码',
remark: '备注',
}[duplicateCoreKind];
throw new BadRequestException(`目标字段“${label}”只能映射一个源列`);
}
if (data.profile)
await this.importParser.saveImportProfile({
...data.profile,
@@ -0,0 +1,90 @@
import { ReportMaterialsService } from './report-materials.service';
import { duplicateCoreMappingKind, signatureCoreMapping } from './report-materials.helpers';
describe('report material import mappings', () => {
it('distinguishes signature category or purpose columns from the signature name', () => {
expect(signatureCoreMapping('签名/签名')).toMatchObject({
kind: 'signatureName',
code: 'signature_name',
});
expect(signatureCoreMapping('签名用途/签名用途')).toMatchObject({
kind: 'purpose',
code: 'purpose',
});
expect(signatureCoreMapping('签名类别1营业执照2商标3APP/签名类别')).toMatchObject({
kind: 'purpose',
code: 'purpose',
});
});
it('rejects duplicate core targets without blocking distinct dynamic fields', () => {
expect(
duplicateCoreMappingKind([
{
sourceHeader: '签名',
sourceColumnIndex: 1,
targetFieldCode: 'signature_name',
targetKind: 'signatureName',
fieldType: 'string',
},
{
sourceHeader: '签名类别',
sourceColumnIndex: 2,
targetFieldCode: 'signature_name',
targetKind: 'signatureName',
fieldType: 'string',
},
]),
).toBe('signatureName');
expect(
duplicateCoreMappingKind([
{
sourceHeader: '正面',
sourceColumnIndex: 1,
targetFieldCode: 'identity',
targetKind: 'dynamic',
fieldType: 'image',
},
{
sourceHeader: '反面',
sourceColumnIndex: 2,
targetFieldCode: 'identity',
targetKind: 'dynamic',
fieldType: 'image',
},
]),
).toBeUndefined();
});
it('rejects duplicate core mappings before loading the workbook', async () => {
const prisma = {
reportMaterialImportBatch: {
findUnique: jest.fn().mockResolvedValue({ id: 'batch-duplicate', status: 'analyzed' }),
},
};
const files = { getDownload: jest.fn() };
const service = new ReportMaterialsService(prisma as never, files as never, {} as never);
await expect(
service.commitImport('batch-duplicate', {
mappings: [
{
sourceHeader: '签名',
sourceColumnIndex: 1,
targetFieldCode: 'signature_name',
targetKind: 'signatureName',
fieldType: 'string',
},
{
sourceHeader: '签名类别',
sourceColumnIndex: 2,
targetFieldCode: 'signature_name',
targetKind: 'signatureName',
fieldType: 'string',
},
],
}),
).rejects.toThrow('目标字段“短信签名”只能映射一个源列');
expect(files.getDownload).not.toHaveBeenCalled();
});
});
@@ -100,6 +100,16 @@ export function suggestMappings(
});
}
export function duplicateCoreMappingKind(mappings: ImportMapping[]) {
const seen = new Set<ImportMapping['targetKind']>();
for (const mapping of mappings) {
if (mapping.targetKind === 'dynamic') continue;
if (seen.has(mapping.targetKind)) return mapping.targetKind;
seen.add(mapping.targetKind);
}
return undefined;
}
export function remapProfileColumns(
profileColumns: ImportMapping[],
sourceColumns: Array<{
@@ -137,8 +147,8 @@ export function remapProfileColumns(
export function signatureCoreMapping(
header: string,
): { code: string; kind: ImportMapping['targetKind']; required?: boolean } | undefined {
if (/签名(?:用途|依据|类别|类型)|用途/.test(header)) return { code: 'purpose', kind: 'purpose' };
if (/短信签名|签名名称|签名/.test(header)) return { code: 'signature_name', kind: 'signatureName', required: true };
if (/用途|签名依据/.test(header)) return { code: 'purpose', kind: 'purpose' };
return undefined;
}