@chenchaolong/plugin-trade-compliance-workbench 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/lib/catalog-import/catalog-document.parser.d.ts +18 -0
- package/dist/lib/catalog-import/catalog-document.parser.d.ts.map +1 -0
- package/dist/lib/catalog-import/catalog-document.parser.js +36 -0
- package/dist/lib/catalog-import/catalog-document.parser.js.map +1 -0
- package/dist/lib/catalog-import/catalog-import.pipeline.d.ts +30 -0
- package/dist/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -0
- package/dist/lib/catalog-import/catalog-import.pipeline.js +54 -0
- package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -0
- package/dist/lib/catalog-import/confidence.d.ts +3 -0
- package/dist/lib/catalog-import/confidence.d.ts.map +1 -0
- package/dist/lib/catalog-import/confidence.js +52 -0
- package/dist/lib/catalog-import/confidence.js.map +1 -0
- package/dist/lib/catalog-import/contracts.d.ts +50 -0
- package/dist/lib/catalog-import/contracts.d.ts.map +1 -0
- package/dist/lib/catalog-import/contracts.js +2 -0
- package/dist/lib/catalog-import/contracts.js.map +1 -0
- package/dist/lib/catalog-import/docx.parser.d.ts +6 -0
- package/dist/lib/catalog-import/docx.parser.d.ts.map +1 -0
- package/dist/lib/catalog-import/docx.parser.js +130 -0
- package/dist/lib/catalog-import/docx.parser.js.map +1 -0
- package/dist/lib/catalog-import/header-mapping.d.ts +4 -0
- package/dist/lib/catalog-import/header-mapping.d.ts.map +1 -0
- package/dist/lib/catalog-import/header-mapping.js +34 -0
- package/dist/lib/catalog-import/header-mapping.js.map +1 -0
- package/dist/lib/catalog-import/pdf-layout.parser.d.ts +15 -0
- package/dist/lib/catalog-import/pdf-layout.parser.d.ts.map +1 -0
- package/dist/lib/catalog-import/pdf-layout.parser.js +133 -0
- package/dist/lib/catalog-import/pdf-layout.parser.js.map +1 -0
- package/dist/lib/catalog-import/pdf-table.normalizer.d.ts +4 -0
- package/dist/lib/catalog-import/pdf-table.normalizer.d.ts.map +1 -0
- package/dist/lib/catalog-import/pdf-table.normalizer.js +22 -0
- package/dist/lib/catalog-import/pdf-table.normalizer.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet.parser.d.ts +7 -0
- package/dist/lib/catalog-import/spreadsheet.parser.d.ts.map +1 -0
- package/dist/lib/catalog-import/spreadsheet.parser.js +118 -0
- package/dist/lib/catalog-import/spreadsheet.parser.js.map +1 -0
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +3 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/entities/catalog.entity.d.ts +25 -1
- package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
- package/dist/lib/entities/catalog.entity.js +94 -0
- package/dist/lib/entities/catalog.entity.js.map +1 -1
- package/dist/lib/entities/index.d.ts +2 -2
- package/dist/lib/entities/index.d.ts.map +1 -1
- package/dist/lib/entities/index.js +3 -2
- package/dist/lib/entities/index.js.map +1 -1
- package/dist/lib/entities/shared.entity.d.ts +3 -1
- package/dist/lib/entities/shared.entity.d.ts.map +1 -1
- package/dist/lib/entities/shared.entity.js +8 -0
- package/dist/lib/entities/shared.entity.js.map +1 -1
- package/dist/lib/import-jobs/import-command.builder.d.ts +4 -0
- package/dist/lib/import-jobs/import-command.builder.d.ts.map +1 -1
- package/dist/lib/import-jobs/import-command.builder.js +11 -0
- package/dist/lib/import-jobs/import-command.builder.js.map +1 -1
- package/dist/lib/import-jobs/import-job-callback.processor.d.ts.map +1 -1
- package/dist/lib/import-jobs/import-job-callback.processor.js +22 -0
- package/dist/lib/import-jobs/import-job-callback.processor.js.map +1 -1
- package/dist/lib/import-jobs/import-job.processor.d.ts.map +1 -1
- package/dist/lib/import-jobs/import-job.processor.js +32 -1
- package/dist/lib/import-jobs/import-job.processor.js.map +1 -1
- package/dist/lib/public-dto.d.ts +1 -1
- package/dist/lib/public-dto.d.ts.map +1 -1
- package/dist/lib/public-dto.js +2 -0
- package/dist/lib/public-dto.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
- package/dist/lib/remote-ui/app-source.js +12 -0
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/contracts.d.ts +9 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/runtime-validation.d.ts +1 -1
- package/dist/lib/types.d.ts +26 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +3 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +7 -0
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench-view.provider.js +1 -0
- package/dist/lib/workbench-view.provider.js.map +1 -1
- package/dist/lib/workbench.middleware.d.ts +89 -6
- package/dist/lib/workbench.middleware.d.ts.map +1 -1
- package/dist/lib/workbench.middleware.js +3 -0
- package/dist/lib/workbench.middleware.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +56 -1
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +463 -7
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { __decorate, __metadata, __param } from "tslib";
|
|
2
2
|
import { Injectable, Optional } from '@nestjs/common';
|
|
3
3
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
4
|
-
import { Brackets, DataSource, In, IsNull, Not } from 'typeorm';
|
|
4
|
+
import { Brackets, DataSource, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
|
|
5
5
|
import { Decimal } from 'decimal.js';
|
|
6
6
|
import ExcelJS from 'exceljs';
|
|
7
7
|
import { createWriteStream } from 'node:fs';
|
|
@@ -10,7 +10,7 @@ import { tmpdir } from 'node:os';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { finished } from 'node:stream/promises';
|
|
12
12
|
import { randomUUID } from 'node:crypto';
|
|
13
|
-
import { BankAccountSettings, CompanySanctionMatch, CompanySettings, ControlledCatalogBatch, ControlledCatalogImportIssue, ControlledGoodsRecord, Customer, CustomerContract, CustomerContractAnalysisRun, CustomerContractClause, CustomerContractClauseAnalysis, DerivedSalesOrderManualData, GeneratedSalesFile, ImportTask, ManagedAsset, MonthlySafeExchangeRate, ProductComplianceMatch, ProfitRuleSettings, PurchaseOrder, PurchaseOrderLine, SanctionCatalogBatch, SanctionCatalogImportIssue, SanctionedCompanyRecord, SettingsHistory, Supplier } from './entities/index.js';
|
|
13
|
+
import { BankAccountSettings, CatalogReviewBlock, CompanySanctionMatch, CompanySettings, ControlledCatalogBatch, ControlledCatalogImportIssue, ControlledGoodsRecord, Customer, CustomerContract, CustomerContractAnalysisRun, CustomerContractClause, CustomerContractClauseAnalysis, DerivedSalesOrderManualData, GeneratedSalesFile, ImportTask, ManagedAsset, MonthlySafeExchangeRate, ProductComplianceMatch, ProfitRuleSettings, PurchaseOrder, PurchaseOrderLine, SanctionCatalogBatch, SanctionCatalogImportIssue, SanctionedCompanyAlias, SanctionedCompanyRecord, SettingsHistory, Supplier } from './entities/index.js';
|
|
14
14
|
import { CatalogTypes, ClauseTypes, CustomerProfitRateStatuses, DomainError, DomainErrorCodes, RefundValidationMethods, SalesPaymentStatuses, SanctionHitStatuses } from './types.js';
|
|
15
15
|
import { deriveSalesLine, assertReceivedAmount } from './domain/finance.js';
|
|
16
16
|
import { parseCustomerContractNo, parsePurchaseContractNo } from './domain/contract-number.js';
|
|
@@ -23,6 +23,7 @@ import { TradeComplianceFileStorage } from './adapters/file-storage.js';
|
|
|
23
23
|
import { TranslationProvider } from './adapters/translation.provider.js';
|
|
24
24
|
import { parseBankAccountType, parseExchangeRateInput, parseExpectedRevision, validateBankSettingsInput, validateProfitSettingsInput, validatePurchaseLineDiscriminators, validateTaxRates } from './runtime-validation.js';
|
|
25
25
|
import { publicCustomerContractReanalysisDto, publicImportTaskDto } from './public-dto.js';
|
|
26
|
+
import { classifyCandidate } from './catalog-import/confidence.js';
|
|
26
27
|
const DEFAULT_IMPORT_ISSUE_EXPORT_MAX_ROWS = 50_000;
|
|
27
28
|
const HARD_IMPORT_ISSUE_EXPORT_MAX_ROWS = 100_000;
|
|
28
29
|
const CUSTOMER_REANALYSIS_TIMEOUT_MS = 20 * 60 * 1000;
|
|
@@ -442,7 +443,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
442
443
|
await issueRepo.save(issueRepo.create({ ...createdScope(scope), batchId: batch.id, issueType: 'DUPLICATE', sourceSequence: batch.totalCount, sourceLocation: row.sourceLocation ?? null, sourceContent: row.sourceContent ?? null, parsedPayload: row, message: '目录中已存在相同商品名称和海关编码' }));
|
|
443
444
|
continue;
|
|
444
445
|
}
|
|
445
|
-
await recordRepo.save(recordRepo.create({ ...auditScope(scope), batchId: batch.id, sourceType: 'FILE_IMPORT', catalogType: batch.catalogType, productName, normalizedProductName: productName, hsCode: row.hsCode ?? null, normalizedHsCode, controlDescription: localized.controlDescription, originalProductName: localized.originalProductName, originalControlDescription: localized.originalControlDescription, translationSource: localized.translationSource, sourceLocation: row.sourceLocation ?? null, sourceContent: row.sourceContent ?? null }));
|
|
446
|
+
await recordRepo.save(recordRepo.create({ ...auditScope(scope), batchId: batch.id, sourceType: 'FILE_IMPORT', catalogType: batch.catalogType, productName, normalizedProductName: productName, hsCode: row.hsCode ?? null, normalizedHsCode, controlDescription: localized.controlDescription, originalProductName: localized.originalProductName, originalControlDescription: localized.originalControlDescription, translationSource: localized.translationSource, controlCode: row.controlCode ?? null, sourceSection: row.sourceSection ?? null, sourceOrdinal: row.sourceOrdinal ?? null, sourceLocation: row.sourceLocation ?? null, sourceContent: row.sourceContent ?? null }));
|
|
446
447
|
batch.successCount += 1;
|
|
447
448
|
batch.riskRecalculatedAt = null;
|
|
448
449
|
}
|
|
@@ -681,6 +682,363 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
681
682
|
await this.recalculateCompanyRisks(scope, 'CATALOG_CHANGED');
|
|
682
683
|
return batch;
|
|
683
684
|
}
|
|
685
|
+
async beginFastCatalogImport(scope, input) {
|
|
686
|
+
return this.dataSource.transaction(async (manager) => {
|
|
687
|
+
const taskRepo = manager.getRepository(ImportTask);
|
|
688
|
+
const task = await required(taskRepo.findOne({
|
|
689
|
+
where: { ...activeScope(scope), id: input.taskId },
|
|
690
|
+
lock: { mode: 'pessimistic_write' }
|
|
691
|
+
}), '解析任务不存在');
|
|
692
|
+
if (task.documentType !== 'CONTROLLED_CATALOG' && task.documentType !== 'SANCTION_CATALOG') {
|
|
693
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '仅目录任务支持快速入库');
|
|
694
|
+
}
|
|
695
|
+
if (task.progress?.mode === 'FAST_PIPELINE' && task.resultEntityId) {
|
|
696
|
+
return { committedCursor: task.progress.committedCursor, batchId: task.resultEntityId };
|
|
697
|
+
}
|
|
698
|
+
assertImportTaskResultWritable(task);
|
|
699
|
+
const now = new Date();
|
|
700
|
+
let batchId;
|
|
701
|
+
if (task.documentType === 'CONTROLLED_CATALOG') {
|
|
702
|
+
const catalogType = input.catalogType;
|
|
703
|
+
if (!catalogType)
|
|
704
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '管控目录缺少目录类型');
|
|
705
|
+
const batchRepo = manager.getRepository(ControlledCatalogBatch);
|
|
706
|
+
const recordRepo = manager.getRepository(ControlledGoodsRecord);
|
|
707
|
+
if (input.replaceBatchId) {
|
|
708
|
+
const old = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: input.replaceBatchId, deletedAt: IsNull() } }), '被覆盖目录批次不存在');
|
|
709
|
+
old.deletedAt = now;
|
|
710
|
+
old.deletedById = scope.userId ?? null;
|
|
711
|
+
old.deleteOperation = 'CATALOG_REPLACE';
|
|
712
|
+
old.updatedById = scope.userId ?? null;
|
|
713
|
+
await batchRepo.save(old);
|
|
714
|
+
await recordRepo.update({ ...activeScope(scope), batchId: old.id, deletedAt: IsNull() }, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'CATALOG_REPLACE' });
|
|
715
|
+
}
|
|
716
|
+
const batch = await batchRepo.save(batchRepo.create({
|
|
717
|
+
...auditScope(scope), catalogType, sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName,
|
|
718
|
+
importMode: input.replaceBatchId ? 'REPLACE_BATCH' : 'APPEND', replacedBatchId: input.replaceBatchId ?? null,
|
|
719
|
+
totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0,
|
|
720
|
+
importTaskId: task.id, riskRecalculatedAt: null
|
|
721
|
+
}));
|
|
722
|
+
batchId = batch.id;
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const batchRepo = manager.getRepository(SanctionCatalogBatch);
|
|
726
|
+
const recordRepo = manager.getRepository(SanctionedCompanyRecord);
|
|
727
|
+
if (input.replaceBatchId) {
|
|
728
|
+
const old = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: input.replaceBatchId, deletedAt: IsNull() } }), '被覆盖制裁目录批次不存在');
|
|
729
|
+
old.deletedAt = now;
|
|
730
|
+
old.deletedById = scope.userId ?? null;
|
|
731
|
+
old.deleteOperation = 'SANCTION_CATALOG_REPLACE';
|
|
732
|
+
old.updatedById = scope.userId ?? null;
|
|
733
|
+
await batchRepo.save(old);
|
|
734
|
+
await recordRepo.update({ ...activeScope(scope), batchId: old.id, deletedAt: IsNull() }, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'SANCTION_CATALOG_REPLACE' });
|
|
735
|
+
}
|
|
736
|
+
const batch = await batchRepo.save(batchRepo.create({
|
|
737
|
+
...auditScope(scope), sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName,
|
|
738
|
+
importMode: input.replaceBatchId ? 'REPLACE_BATCH' : 'APPEND', replacedBatchId: input.replaceBatchId ?? null,
|
|
739
|
+
totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0, importTaskId: task.id
|
|
740
|
+
}));
|
|
741
|
+
batchId = batch.id;
|
|
742
|
+
}
|
|
743
|
+
task.status = 'PERSISTING';
|
|
744
|
+
task.resultEntityId = batchId;
|
|
745
|
+
task.completedAt = null;
|
|
746
|
+
task.progress = initialFastCatalogProgress(input.totalCandidates);
|
|
747
|
+
await taskRepo.save(task);
|
|
748
|
+
return { committedCursor: 0, batchId };
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
async appendFastCatalogBatch(scope, input) {
|
|
752
|
+
const controlledRows = input.candidates.flatMap(candidate => candidate.kind === 'CONTROLLED'
|
|
753
|
+
? candidateToControlledRows(candidate)
|
|
754
|
+
: []);
|
|
755
|
+
const localizedRows = controlledRows.length ? await this.localizeControlledCatalogRows(controlledRows) : [];
|
|
756
|
+
return this.dataSource.transaction(async (manager) => {
|
|
757
|
+
const taskRepo = manager.getRepository(ImportTask);
|
|
758
|
+
const task = await required(taskRepo.findOne({
|
|
759
|
+
where: { ...activeScope(scope), id: input.taskId },
|
|
760
|
+
lock: { mode: 'pessimistic_write' }
|
|
761
|
+
}), '解析任务不存在');
|
|
762
|
+
if (!task.progress || task.progress.mode !== 'FAST_PIPELINE' || !task.resultEntityId) {
|
|
763
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '快速目录导入尚未初始化');
|
|
764
|
+
}
|
|
765
|
+
if (task.progress.committedCursor >= input.endCursor && input.endCursor > input.startCursor)
|
|
766
|
+
return task.progress;
|
|
767
|
+
if (task.progress.committedCursor !== input.startCursor) {
|
|
768
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '快速目录导入游标不一致');
|
|
769
|
+
}
|
|
770
|
+
if (task.documentType === 'CONTROLLED_CATALOG') {
|
|
771
|
+
const batchRepo = manager.getRepository(ControlledCatalogBatch);
|
|
772
|
+
const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
|
|
773
|
+
await this.appendControlledCatalogRows(manager, scope, batch, controlledRows, localizedRows);
|
|
774
|
+
await batchRepo.save(batch);
|
|
775
|
+
task.progress.successCount = batch.successCount;
|
|
776
|
+
task.progress.duplicateCount = batch.duplicateCount;
|
|
777
|
+
task.progress.invalidCount = batch.invalidCount;
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
const batch = await this.appendFastSanctionCandidates(manager, scope, task.resultEntityId, input.candidates);
|
|
781
|
+
task.progress.successCount = batch.successCount;
|
|
782
|
+
task.progress.duplicateCount = batch.duplicateCount;
|
|
783
|
+
task.progress.invalidCount = batch.invalidCount;
|
|
784
|
+
}
|
|
785
|
+
if (input.reviewBlocks.length) {
|
|
786
|
+
const reviewRepo = manager.getRepository(CatalogReviewBlock);
|
|
787
|
+
for (const block of input.reviewBlocks) {
|
|
788
|
+
const existing = await reviewRepo.findOne({ where: {
|
|
789
|
+
...activeScope(scope), importTaskId: task.id, ordinal: block.ordinal,
|
|
790
|
+
sourceLocation: block.sourceLocation, status: 'PENDING'
|
|
791
|
+
} });
|
|
792
|
+
if (existing)
|
|
793
|
+
continue;
|
|
794
|
+
await reviewRepo.save(reviewRepo.create({
|
|
795
|
+
...createdScope(scope), importTaskId: task.id, documentType: task.documentType,
|
|
796
|
+
confidence: block.confidence, status: 'PENDING', ordinal: block.ordinal,
|
|
797
|
+
sourceLocation: block.sourceLocation || null, sourceContent: block.sourceContent || null,
|
|
798
|
+
payload: block.payload, reasons: block.reasons,
|
|
799
|
+
leaseToken: null, leaseExpiresAt: null, resolvedAt: null
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
task.progress.committedCursor = input.endCursor;
|
|
804
|
+
task.progress.processedCount = input.endCursor + input.reviewBlocks.length + input.skipped.length;
|
|
805
|
+
task.progress.reviewPendingCount += input.reviewBlocks.length;
|
|
806
|
+
task.progress.currentBatch = input.endCursor === 0 ? 0 : Math.ceil(input.endCursor / 100);
|
|
807
|
+
task.progress.progressVersion += 1;
|
|
808
|
+
task.progress.lastProgressAt = new Date().toISOString();
|
|
809
|
+
task.progress.stage = input.reviewBlocks.length ? 'REVIEWING' : 'SAVING';
|
|
810
|
+
await taskRepo.save(task);
|
|
811
|
+
return task.progress;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async finalizeFastCatalogImport(scope, taskId) {
|
|
815
|
+
const task = await this.dataSource.transaction(async (manager) => {
|
|
816
|
+
const repo = manager.getRepository(ImportTask);
|
|
817
|
+
const current = await required(repo.findOne({
|
|
818
|
+
where: { ...activeScope(scope), id: taskId }, lock: { mode: 'pessimistic_write' }
|
|
819
|
+
}), '解析任务不存在');
|
|
820
|
+
if (!current.progress || current.progress.mode !== 'FAST_PIPELINE') {
|
|
821
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '快速目录导入进度不存在');
|
|
822
|
+
}
|
|
823
|
+
current.status = current.progress.reviewPendingCount > 0 ? 'AWAITING_REVIEW' : 'SUCCEEDED';
|
|
824
|
+
current.progress.stage = current.progress.reviewPendingCount > 0 ? 'REVIEWING' : 'SUCCEEDED';
|
|
825
|
+
current.progress.observerStatus = current.progress.reviewPendingCount > 0 ? 'PENDING' : 'COMPLETED';
|
|
826
|
+
current.progress.progressVersion += 1;
|
|
827
|
+
current.progress.lastProgressAt = new Date().toISOString();
|
|
828
|
+
current.completedAt = new Date();
|
|
829
|
+
return repo.save(current);
|
|
830
|
+
});
|
|
831
|
+
if (task.documentType === 'CONTROLLED_CATALOG')
|
|
832
|
+
await this.recalculateProductRisks(scope, 'CATALOG_CHANGED', task.resultEntityId ?? undefined);
|
|
833
|
+
else
|
|
834
|
+
await this.recalculateCompanyRisks(scope, 'CATALOG_CHANGED');
|
|
835
|
+
return task;
|
|
836
|
+
}
|
|
837
|
+
async waitImportProgress(scope, taskId, afterProgressVersion = 0, waitMs = 10_000) {
|
|
838
|
+
const deadline = Date.now() + Math.max(0, Math.min(waitMs, 10_000));
|
|
839
|
+
while (true) {
|
|
840
|
+
const task = await required(this.dataSource.getRepository(ImportTask).findOne({
|
|
841
|
+
where: { ...activeScope(scope), id: taskId }
|
|
842
|
+
}), '解析任务不存在');
|
|
843
|
+
const version = task.progress?.progressVersion ?? 0;
|
|
844
|
+
if (version > afterProgressVersion || TERMINAL_IMPORT_TASK_STATUSES.includes(task.status)) {
|
|
845
|
+
return { task: publicImportTaskDto(task) };
|
|
846
|
+
}
|
|
847
|
+
const remaining = deadline - Date.now();
|
|
848
|
+
if (remaining <= 0)
|
|
849
|
+
return { task: publicImportTaskDto(task) };
|
|
850
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(250, remaining)));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async nextCatalogReviewBatch(scope, taskId, requestedLimit = 20) {
|
|
854
|
+
const limit = Math.max(1, Math.min(requestedLimit, 20));
|
|
855
|
+
return this.dataSource.transaction(async (manager) => {
|
|
856
|
+
const task = await required(manager.getRepository(ImportTask).findOne({
|
|
857
|
+
where: { ...activeScope(scope), id: taskId }, lock: { mode: 'pessimistic_write' }
|
|
858
|
+
}), '解析任务不存在');
|
|
859
|
+
if (task.documentType !== 'CONTROLLED_CATALOG' && task.documentType !== 'SANCTION_CATALOG') {
|
|
860
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '仅目录任务包含复核块');
|
|
861
|
+
}
|
|
862
|
+
const repo = manager.getRepository(CatalogReviewBlock);
|
|
863
|
+
await repo.update({
|
|
864
|
+
...activeScope(scope), importTaskId: taskId, status: 'LEASED', leaseExpiresAt: LessThanOrEqual(new Date())
|
|
865
|
+
}, { status: 'PENDING', leaseToken: null, leaseExpiresAt: null });
|
|
866
|
+
const blocks = await repo.createQueryBuilder('block')
|
|
867
|
+
.setLock('pessimistic_write')
|
|
868
|
+
.setOnLocked('skip_locked')
|
|
869
|
+
.where('block."tenantId" = :tenantId AND block."organizationId" = :organizationId AND block."importTaskId" = :taskId AND block.status = :status', {
|
|
870
|
+
...activeScope(scope), taskId, status: 'PENDING'
|
|
871
|
+
})
|
|
872
|
+
.orderBy('block.ordinal', 'ASC')
|
|
873
|
+
.addOrderBy('block.id', 'ASC')
|
|
874
|
+
.limit(limit)
|
|
875
|
+
.getMany();
|
|
876
|
+
if (blocks.length === 0)
|
|
877
|
+
return { leaseToken: null, items: [] };
|
|
878
|
+
const leaseToken = randomUUID();
|
|
879
|
+
const leaseExpiresAt = new Date(Date.now() + 5 * 60_000);
|
|
880
|
+
for (const block of blocks) {
|
|
881
|
+
block.status = 'LEASED';
|
|
882
|
+
block.leaseToken = leaseToken;
|
|
883
|
+
block.leaseExpiresAt = leaseExpiresAt;
|
|
884
|
+
}
|
|
885
|
+
await repo.save(blocks);
|
|
886
|
+
return {
|
|
887
|
+
leaseToken,
|
|
888
|
+
leaseExpiresAt: leaseExpiresAt.toISOString(),
|
|
889
|
+
items: blocks.map(block => ({
|
|
890
|
+
blockId: block.id, ordinal: block.ordinal, confidence: block.confidence,
|
|
891
|
+
sourceLocation: block.sourceLocation, sourceContent: block.sourceContent,
|
|
892
|
+
payload: block.payload, reasons: block.reasons
|
|
893
|
+
}))
|
|
894
|
+
};
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
async submitCatalogReviewBatch(scope, taskId, leaseToken, items) {
|
|
898
|
+
if (!leaseToken.trim() || items.length === 0 || items.length > 20) {
|
|
899
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '复核提交参数无效');
|
|
900
|
+
}
|
|
901
|
+
return this.dataSource.transaction(async (manager) => {
|
|
902
|
+
const taskRepo = manager.getRepository(ImportTask);
|
|
903
|
+
const task = await required(taskRepo.findOne({
|
|
904
|
+
where: { ...activeScope(scope), id: taskId }, lock: { mode: 'pessimistic_write' }
|
|
905
|
+
}), '解析任务不存在');
|
|
906
|
+
if (!task.progress || !task.resultEntityId)
|
|
907
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '目录复核任务状态无效');
|
|
908
|
+
const reviewRepo = manager.getRepository(CatalogReviewBlock);
|
|
909
|
+
const blocks = await reviewRepo.find({ where: {
|
|
910
|
+
...activeScope(scope), importTaskId: taskId, id: In(items.map(item => item.blockId)),
|
|
911
|
+
status: 'LEASED', leaseToken
|
|
912
|
+
} });
|
|
913
|
+
if (blocks.length !== items.length || blocks.some(block => !block.leaseExpiresAt || block.leaseExpiresAt.getTime() <= Date.now())) {
|
|
914
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '复核块租约无效或已过期');
|
|
915
|
+
}
|
|
916
|
+
const acceptedCandidates = [];
|
|
917
|
+
let resolved = 0;
|
|
918
|
+
let rejected = 0;
|
|
919
|
+
for (const item of items) {
|
|
920
|
+
const block = blocks.find(value => value.id === item.blockId);
|
|
921
|
+
if (item.decision === 'REJECT') {
|
|
922
|
+
block.status = 'REJECTED';
|
|
923
|
+
rejected += 1;
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
const payload = item.payload ?? block.payload;
|
|
927
|
+
const candidate = reviewCandidate(task.documentType, block, payload);
|
|
928
|
+
const decision = classifyCandidate(candidate);
|
|
929
|
+
if (decision.confidence === 'LOW') {
|
|
930
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `复核结果未通过代码校验:${decision.reasons.join(',')}`);
|
|
931
|
+
}
|
|
932
|
+
acceptedCandidates.push(candidate);
|
|
933
|
+
block.payload = payload;
|
|
934
|
+
block.status = 'RESOLVED';
|
|
935
|
+
resolved += 1;
|
|
936
|
+
}
|
|
937
|
+
block.resolvedAt = new Date();
|
|
938
|
+
block.leaseToken = null;
|
|
939
|
+
block.leaseExpiresAt = null;
|
|
940
|
+
}
|
|
941
|
+
if (task.documentType === 'CONTROLLED_CATALOG' && acceptedCandidates.length) {
|
|
942
|
+
const batchRepo = manager.getRepository(ControlledCatalogBatch);
|
|
943
|
+
const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
|
|
944
|
+
const rows = acceptedCandidates.flatMap(candidateToControlledRows);
|
|
945
|
+
const localized = rows.map(row => ({
|
|
946
|
+
productName: String(row.productName ?? '').trim(),
|
|
947
|
+
controlDescription: optionalString(row.controlDescription),
|
|
948
|
+
originalProductName: null, originalControlDescription: null,
|
|
949
|
+
translationSource: row.translationSource ?? null
|
|
950
|
+
}));
|
|
951
|
+
await this.appendControlledCatalogRows(manager, scope, batch, rows, localized);
|
|
952
|
+
await batchRepo.save(batch);
|
|
953
|
+
task.progress.successCount = batch.successCount;
|
|
954
|
+
task.progress.duplicateCount = batch.duplicateCount;
|
|
955
|
+
task.progress.invalidCount = batch.invalidCount;
|
|
956
|
+
}
|
|
957
|
+
if (task.documentType === 'SANCTION_CATALOG' && acceptedCandidates.length) {
|
|
958
|
+
const batch = await this.appendFastSanctionCandidates(manager, scope, task.resultEntityId, acceptedCandidates);
|
|
959
|
+
task.progress.successCount = batch.successCount;
|
|
960
|
+
task.progress.duplicateCount = batch.duplicateCount;
|
|
961
|
+
task.progress.invalidCount = batch.invalidCount;
|
|
962
|
+
}
|
|
963
|
+
await reviewRepo.save(blocks);
|
|
964
|
+
task.progress.reviewResolvedCount += resolved;
|
|
965
|
+
task.progress.reviewRejectedCount += rejected;
|
|
966
|
+
task.progress.reviewPendingCount = Math.max(0, task.progress.reviewPendingCount - resolved - rejected);
|
|
967
|
+
task.progress.progressVersion += 1;
|
|
968
|
+
task.progress.lastProgressAt = new Date().toISOString();
|
|
969
|
+
if (task.progress.reviewPendingCount === 0) {
|
|
970
|
+
task.status = 'SUCCEEDED';
|
|
971
|
+
task.progress.stage = 'SUCCEEDED';
|
|
972
|
+
task.progress.observerStatus = 'COMPLETED';
|
|
973
|
+
task.completedAt = new Date();
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
task.status = 'AWAITING_REVIEW';
|
|
977
|
+
task.progress.stage = 'REVIEWING';
|
|
978
|
+
}
|
|
979
|
+
await taskRepo.save(task);
|
|
980
|
+
return { resolved, rejected, remaining: task.progress.reviewPendingCount, task: publicImportTaskDto(task) };
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
async setImportObserverConversation(scope, taskId, conversationId) {
|
|
984
|
+
const normalizedConversationId = requiredId(conversationId, '观察会话标识');
|
|
985
|
+
return this.dataSource.transaction(async (manager) => {
|
|
986
|
+
const repo = manager.getRepository(ImportTask);
|
|
987
|
+
const task = await required(repo.findOne({
|
|
988
|
+
where: { ...activeScope(scope), id: taskId }, lock: { mode: 'pessimistic_write' }
|
|
989
|
+
}), '解析任务不存在');
|
|
990
|
+
if (task.observerConversationId && task.observerConversationId !== normalizedConversationId) {
|
|
991
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '观察会话标识不一致');
|
|
992
|
+
}
|
|
993
|
+
task.observerConversationId = normalizedConversationId;
|
|
994
|
+
if (task.progress)
|
|
995
|
+
task.progress.observerStatus = 'ACTIVE';
|
|
996
|
+
return repo.save(task);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
async appendFastSanctionCandidates(manager, scope, batchId, candidates) {
|
|
1000
|
+
const batchRepo = manager.getRepository(SanctionCatalogBatch);
|
|
1001
|
+
const recordRepo = manager.getRepository(SanctionedCompanyRecord);
|
|
1002
|
+
const aliasRepo = manager.getRepository(SanctionedCompanyAlias);
|
|
1003
|
+
const issueRepo = manager.getRepository(SanctionCatalogImportIssue);
|
|
1004
|
+
const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: batchId } }), '制裁目录批次不存在');
|
|
1005
|
+
for (const candidate of candidates.filter(value => value.kind === 'SANCTION')) {
|
|
1006
|
+
const companyName = String(candidate.values.companyName ?? '').trim();
|
|
1007
|
+
const sourceLocation = candidate.evidence[0]?.sourceLocation ?? null;
|
|
1008
|
+
const sourceContent = candidate.evidence[0]?.sourceContent ?? null;
|
|
1009
|
+
if (!companyName)
|
|
1010
|
+
continue;
|
|
1011
|
+
const normalizedCompanyName = normalizeExactName(companyName);
|
|
1012
|
+
const sanctionListSource = optionalString(candidate.values.sanctionListSource);
|
|
1013
|
+
const normalizedSanctionListSource = normalizeSanctionSource(sanctionListSource);
|
|
1014
|
+
const duplicate = await recordRepo.findOne({ where: { ...activeScope(scope), normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() } });
|
|
1015
|
+
batch.totalCount += 1;
|
|
1016
|
+
if (duplicate) {
|
|
1017
|
+
batch.duplicateCount += 1;
|
|
1018
|
+
await issueRepo.save(issueRepo.create({
|
|
1019
|
+
...createdScope(scope), batchId: batch.id, issueType: 'DUPLICATE', sourceSequence: batch.totalCount,
|
|
1020
|
+
sourceLocation, sourceContent, parsedPayload: candidate.values,
|
|
1021
|
+
message: '制裁目录中已存在相同公司和名单来源'
|
|
1022
|
+
}));
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
const record = await recordRepo.save(recordRepo.create({
|
|
1026
|
+
...auditScope(scope), batchId: batch.id, sourceType: 'FILE_IMPORT', companyName, normalizedCompanyName,
|
|
1027
|
+
countryOrRegion: optionalString(candidate.values.countryOrRegion), sanctionListSource,
|
|
1028
|
+
normalizedSanctionListSource, sanctionReason: optionalString(candidate.values.sanctionReason),
|
|
1029
|
+
effectiveDate: optionalString(candidate.values.effectiveDate), remarks: optionalString(candidate.values.remarks),
|
|
1030
|
+
sourceLocation, sourceContent
|
|
1031
|
+
}));
|
|
1032
|
+
for (const alias of candidateAliases(candidate)) {
|
|
1033
|
+
await aliasRepo.save(aliasRepo.create({
|
|
1034
|
+
...auditScope(scope), sanctionedCompanyId: record.id, alias: alias.alias,
|
|
1035
|
+
normalizedAlias: normalizeExactName(alias.alias), aliasType: alias.aliasType, sourceLocation
|
|
1036
|
+
}));
|
|
1037
|
+
}
|
|
1038
|
+
batch.successCount += 1;
|
|
1039
|
+
}
|
|
1040
|
+
return batchRepo.save(batch);
|
|
1041
|
+
}
|
|
684
1042
|
async createImportTask(scope, input) {
|
|
685
1043
|
const repo = this.dataSource.getRepository(ImportTask);
|
|
686
1044
|
const targetAssistantId = requiredId(input.targetAssistantId, '目标智能体标识');
|
|
@@ -692,6 +1050,17 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
692
1050
|
order: { queuedAt: 'ASC', id: 'ASC' }
|
|
693
1051
|
});
|
|
694
1052
|
}
|
|
1053
|
+
async findNextFastCatalogImportCandidate(scope) {
|
|
1054
|
+
const tasks = await this.dataSource.getRepository(ImportTask).find({
|
|
1055
|
+
where: {
|
|
1056
|
+
...activeScope(scope),
|
|
1057
|
+
documentType: In(['CONTROLLED_CATALOG', 'SANCTION_CATALOG']),
|
|
1058
|
+
status: In(['EXTRACTING', 'VALIDATING', 'PERSISTING'])
|
|
1059
|
+
},
|
|
1060
|
+
order: { startedAt: 'ASC', id: 'ASC' }
|
|
1061
|
+
});
|
|
1062
|
+
return tasks.find(task => task.progress?.mode === 'FAST_PIPELINE') ?? null;
|
|
1063
|
+
}
|
|
695
1064
|
async claimQueuedImportTaskById(scope, id, claimAttemptToken) {
|
|
696
1065
|
const attemptToken = requiredId(claimAttemptToken, '领取尝试标识');
|
|
697
1066
|
return this.dataSource.transaction(async (manager) => {
|
|
@@ -2453,18 +2822,28 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2453
2822
|
const customerRepo = manager.getRepository(Customer);
|
|
2454
2823
|
const now = new Date();
|
|
2455
2824
|
await repo.update({ ...activeScope(scope), deletedAt: IsNull() }, { deletedAt: now, invalidationReason: reason });
|
|
2456
|
-
const [suppliers, customers, sanctions] = await Promise.all([
|
|
2825
|
+
const [suppliers, customers, sanctions, aliases] = await Promise.all([
|
|
2457
2826
|
supplierRepo.find({ where: { ...activeScope(scope), deletedAt: IsNull() } }),
|
|
2458
2827
|
customerRepo.find({ where: { ...activeScope(scope), deletedAt: IsNull() } }),
|
|
2459
|
-
manager.getRepository(SanctionedCompanyRecord).find({ where: { ...activeScope(scope), deletedAt: IsNull() } })
|
|
2828
|
+
manager.getRepository(SanctionedCompanyRecord).find({ where: { ...activeScope(scope), deletedAt: IsNull() } }),
|
|
2829
|
+
manager.getRepository(SanctionedCompanyAlias).find({ where: { ...activeScope(scope), deletedAt: IsNull() } })
|
|
2460
2830
|
]);
|
|
2831
|
+
const sanctionsById = new Map(sanctions.map(item => [item.id, item]));
|
|
2832
|
+
const sanctionsByNormalizedName = new Map();
|
|
2833
|
+
for (const item of sanctions)
|
|
2834
|
+
appendMapValue(sanctionsByNormalizedName, item.normalizedCompanyName, item);
|
|
2835
|
+
for (const alias of aliases) {
|
|
2836
|
+
const item = sanctionsById.get(alias.sanctionedCompanyId);
|
|
2837
|
+
if (item)
|
|
2838
|
+
appendMapValue(sanctionsByNormalizedName, alias.normalizedAlias, item);
|
|
2839
|
+
}
|
|
2461
2840
|
for (const supplier of suppliers) {
|
|
2462
|
-
for (const item of
|
|
2841
|
+
for (const item of sanctionsByNormalizedName.get(supplier.normalizedName) ?? [])
|
|
2463
2842
|
await repo.save(repo.create({ ...createdScope(scope), subjectType: 'SUPPLIER', supplierId: supplier.id, customerId: null, sanctionedCompanyId: item.id, matchedNormalizedName: supplier.normalizedName, checkedAt: now, deletedAt: null, invalidationReason: null }));
|
|
2464
2843
|
await supplierRepo.update({ ...activeScope(scope), id: supplier.id, deletedAt: IsNull() }, { complianceCheckedAt: now, updatedById: scope.userId ?? null, updatedAt: now });
|
|
2465
2844
|
}
|
|
2466
2845
|
for (const customer of customers) {
|
|
2467
|
-
for (const item of
|
|
2846
|
+
for (const item of sanctionsByNormalizedName.get(customer.normalizedName) ?? [])
|
|
2468
2847
|
await repo.save(repo.create({ ...createdScope(scope), subjectType: 'CUSTOMER', supplierId: null, customerId: customer.id, sanctionedCompanyId: item.id, matchedNormalizedName: customer.normalizedName, checkedAt: now, deletedAt: null, invalidationReason: null }));
|
|
2469
2848
|
await customerRepo.update({ ...activeScope(scope), id: customer.id, deletedAt: IsNull() }, { complianceCheckStatus: 'CHECKED', complianceCheckedAt: now, updatedById: scope.userId ?? null, updatedAt: now });
|
|
2470
2849
|
}
|
|
@@ -3113,6 +3492,83 @@ function snapshotMissingRecoveryMetadataStillMissing(snapshot, current) {
|
|
|
3113
3492
|
}
|
|
3114
3493
|
function requiredId(value, label) { const id = String(value ?? '').trim(); if (!id)
|
|
3115
3494
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `缺少${label}`); return id; }
|
|
3495
|
+
function optionalString(value) {
|
|
3496
|
+
const normalized = String(value ?? '').trim();
|
|
3497
|
+
return normalized || null;
|
|
3498
|
+
}
|
|
3499
|
+
function initialFastCatalogProgress(totalCandidates) {
|
|
3500
|
+
return {
|
|
3501
|
+
version: 1,
|
|
3502
|
+
mode: 'FAST_PIPELINE',
|
|
3503
|
+
stage: 'SAVING',
|
|
3504
|
+
totalCandidates,
|
|
3505
|
+
processedCount: 0,
|
|
3506
|
+
successCount: 0,
|
|
3507
|
+
duplicateCount: 0,
|
|
3508
|
+
invalidCount: 0,
|
|
3509
|
+
currentBatch: 0,
|
|
3510
|
+
totalBatches: totalCandidates > 0 ? Math.ceil(totalCandidates / 100) : 0,
|
|
3511
|
+
committedCursor: 0,
|
|
3512
|
+
progressVersion: 1,
|
|
3513
|
+
lastProgressAt: new Date().toISOString(),
|
|
3514
|
+
observerStatus: 'PENDING',
|
|
3515
|
+
reviewPendingCount: 0,
|
|
3516
|
+
reviewResolvedCount: 0,
|
|
3517
|
+
reviewRejectedCount: 0
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
function candidateToControlledRows(candidate) {
|
|
3521
|
+
const evidence = candidate.evidence[0];
|
|
3522
|
+
const hsCodes = Array.isArray(candidate.values.hsCodes) && candidate.values.hsCodes.length
|
|
3523
|
+
? candidate.values.hsCodes
|
|
3524
|
+
: [undefined];
|
|
3525
|
+
return hsCodes.map(hsCode => ({
|
|
3526
|
+
productName: optionalString(candidate.values.productName) ?? undefined,
|
|
3527
|
+
hsCode: typeof hsCode === 'string' ? hsCode : undefined,
|
|
3528
|
+
controlDescription: optionalString(candidate.values.controlDescription) ?? undefined,
|
|
3529
|
+
controlCode: optionalString(candidate.values.controlCode) ?? undefined,
|
|
3530
|
+
sourceSection: optionalString(candidate.values.sourceSection) ?? undefined,
|
|
3531
|
+
sourceOrdinal: optionalString(candidate.values.sourceOrdinal) ?? String(candidate.ordinal),
|
|
3532
|
+
sourceLocation: evidence?.sourceLocation,
|
|
3533
|
+
sourceContent: evidence?.sourceContent,
|
|
3534
|
+
translationSource: 'SOURCE_CHINESE'
|
|
3535
|
+
}));
|
|
3536
|
+
}
|
|
3537
|
+
function candidateAliases(candidate) {
|
|
3538
|
+
if (!Array.isArray(candidate.values.aliases))
|
|
3539
|
+
return [];
|
|
3540
|
+
return candidate.values.aliases.flatMap(value => {
|
|
3541
|
+
if (!value || typeof value !== 'object')
|
|
3542
|
+
return [];
|
|
3543
|
+
const alias = optionalString(value.alias);
|
|
3544
|
+
const aliasType = value.aliasType;
|
|
3545
|
+
return alias && (aliasType === 'AKA' || aliasType === 'LOCAL_LANGUAGE') ? [{ alias, aliasType }] : [];
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
function reviewCandidate(documentType, block, payload) {
|
|
3549
|
+
const values = { ...payload };
|
|
3550
|
+
if (!Array.isArray(values.hsCodes) && typeof values.hsCode === 'string')
|
|
3551
|
+
values.hsCodes = [values.hsCode];
|
|
3552
|
+
return {
|
|
3553
|
+
kind: documentType === 'CONTROLLED_CATALOG' ? 'CONTROLLED' : 'SANCTION',
|
|
3554
|
+
ordinal: block.ordinal,
|
|
3555
|
+
values,
|
|
3556
|
+
evidence: block.sourceLocation && block.sourceContent
|
|
3557
|
+
? [{ sourceLocation: block.sourceLocation, sourceContent: block.sourceContent }]
|
|
3558
|
+
: [],
|
|
3559
|
+
signals: ['STRUCTURED_SOURCE', 'HEADER_MAPPED', 'RECORD_START_EXPLICIT', 'COLUMN_LAYOUT_STABLE'],
|
|
3560
|
+
warnings: block.reasons
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
function appendMapValue(map, key, value) {
|
|
3564
|
+
const values = map.get(key);
|
|
3565
|
+
if (values) {
|
|
3566
|
+
if (!values.includes(value))
|
|
3567
|
+
values.push(value);
|
|
3568
|
+
}
|
|
3569
|
+
else
|
|
3570
|
+
map.set(key, [value]);
|
|
3571
|
+
}
|
|
3116
3572
|
function assertCompleteClauseAnalyses(analyses) {
|
|
3117
3573
|
if (analyses.length !== ClauseTypes.length)
|
|
3118
3574
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '重新分析结果必须完整包含七类关键条款');
|