@chenchaolong/plugin-trade-compliance-workbench 1.0.18 → 1.0.20
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/dist/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -1
- package/dist/lib/catalog-import/catalog-import.pipeline.js +2 -1
- package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -1
- package/dist/lib/catalog-import/confidence.js +2 -2
- package/dist/lib/catalog-import/confidence.js.map +1 -1
- package/dist/lib/catalog-import/contracts.d.ts +5 -2
- package/dist/lib/catalog-import/contracts.d.ts.map +1 -1
- package/dist/lib/catalog-import/outcome.d.ts +19 -0
- package/dist/lib/catalog-import/outcome.d.ts.map +1 -0
- package/dist/lib/catalog-import/outcome.js +18 -0
- package/dist/lib/catalog-import/outcome.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet-structure.d.ts +15 -0
- package/dist/lib/catalog-import/spreadsheet-structure.d.ts.map +1 -0
- package/dist/lib/catalog-import/spreadsheet-structure.js +44 -0
- package/dist/lib/catalog-import/spreadsheet-structure.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet.parser.d.ts.map +1 -1
- package/dist/lib/catalog-import/spreadsheet.parser.js +71 -0
- package/dist/lib/catalog-import/spreadsheet.parser.js.map +1 -1
- package/dist/lib/domain/contract-number.js +2 -2
- package/dist/lib/domain/contract-number.js.map +1 -1
- package/dist/lib/entities/catalog.entity.d.ts +5 -1
- package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
- package/dist/lib/entities/catalog.entity.js +16 -0
- package/dist/lib/entities/catalog.entity.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
- package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
- package/dist/lib/remote-ui/app-source.js +208 -67
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/components/forms.js +1 -1
- package/dist/lib/remote-ui/components/forms.js.map +1 -1
- package/dist/lib/remote-ui/components/task-status.d.ts +2 -1
- package/dist/lib/remote-ui/components/task-status.d.ts.map +1 -1
- package/dist/lib/remote-ui/components/task-status.js +5 -4
- package/dist/lib/remote-ui/components/task-status.js.map +1 -1
- package/dist/lib/remote-ui/contracts.d.ts +3 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/import-task-status.d.ts +10 -0
- package/dist/lib/remote-ui/import-task-status.d.ts.map +1 -0
- package/dist/lib/remote-ui/import-task-status.js +236 -0
- package/dist/lib/remote-ui/import-task-status.js.map +1 -0
- package/dist/lib/remote-ui/model.d.ts.map +1 -1
- package/dist/lib/remote-ui/model.js +2 -1
- package/dist/lib/remote-ui/model.js.map +1 -1
- package/dist/lib/remote-ui/pages/analytics-prototype.css +307 -0
- package/dist/lib/remote-ui/pages/catalogs.css +681 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts +9 -3
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +36 -7
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.css +259 -0
- package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/contracts.js +36 -13
- package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
- package/dist/lib/remote-ui/pages/files-prototype.css +547 -0
- package/dist/lib/remote-ui/pages/files.d.ts +3 -2
- package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/files.js +15 -15
- package/dist/lib/remote-ui/pages/files.js.map +1 -1
- package/dist/lib/remote-ui/pages/procurement-contracts.css +939 -0
- package/dist/lib/remote-ui/pages/procurement.js +1 -1
- package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales-files.css +512 -0
- package/dist/lib/remote-ui/pages/sales.js +2 -2
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/pages/settings.css +178 -0
- package/dist/lib/remote-ui/pages/settings.js +1 -1
- package/dist/lib/remote-ui/pages/settings.js.map +1 -1
- package/dist/lib/remote-ui/polling.d.ts +1 -1
- package/dist/lib/remote-ui/polling.d.ts.map +1 -1
- package/dist/lib/remote-ui/polling.js +4 -2
- package/dist/lib/remote-ui/polling.js.map +1 -1
- package/dist/lib/remote-ui/purchase-review-adapter.d.ts +4 -0
- package/dist/lib/remote-ui/purchase-review-adapter.d.ts.map +1 -0
- package/dist/lib/remote-ui/purchase-review-adapter.js +78 -0
- package/dist/lib/remote-ui/purchase-review-adapter.js.map +1 -0
- package/dist/lib/remote-ui/styles.css +588 -0
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +19 -7
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/types.d.ts +12 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +24 -0
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench-view.provider.js +19 -4
- package/dist/lib/workbench-view.provider.js.map +1 -1
- package/dist/lib/workbench.plugin.d.ts.map +1 -1
- package/dist/lib/workbench.plugin.js +1 -2
- package/dist/lib/workbench.plugin.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +7 -1
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +276 -75
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +4 -1
|
@@ -24,16 +24,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
26
|
import { classifyCandidate } from './catalog-import/confidence.js';
|
|
27
|
+
import { catalogTerminalOutcome } from './catalog-import/outcome.js';
|
|
27
28
|
const DEFAULT_IMPORT_ISSUE_EXPORT_MAX_ROWS = 50_000;
|
|
28
29
|
const HARD_IMPORT_ISSUE_EXPORT_MAX_ROWS = 100_000;
|
|
29
30
|
const CUSTOMER_REANALYSIS_TIMEOUT_MS = 20 * 60 * 1000;
|
|
30
|
-
const
|
|
31
|
+
const DEFAULT_ANALYTICS_EXPORT_MAX_ROWS = 100_000;
|
|
31
32
|
const CONTROLLED_CATALOG_TRANSLATION_CONCURRENCY = 4;
|
|
32
33
|
const CONTROLLED_CATALOG_TRANSLATION_TIMEOUT_MS = 60_000;
|
|
33
34
|
const CONTROLLED_CATALOG_PROCESSING_CLAIM_LEASE_MS = 90_000;
|
|
34
35
|
const CONTROLLED_CATALOG_PROCESSING_HEARTBEAT_MS = 30_000;
|
|
35
36
|
const CONTROLLED_CATALOG_PROCESSING_CLAIM_INITIAL_POLL_MS = 250;
|
|
36
37
|
const CONTROLLED_CATALOG_PROCESSING_CLAIM_MAX_POLL_MS = 1_000;
|
|
38
|
+
const SKIPPED_REASON_MESSAGES = {
|
|
39
|
+
SECTION_OR_NOTE_SKIPPED: '章节或说明行已跳过',
|
|
40
|
+
INVALID_HS_CODE: 'HS 编码格式无效',
|
|
41
|
+
EMPTY_PRODUCT_DESCRIPTION: '商品描述为空',
|
|
42
|
+
STRUCTURAL_MAPPING_AMBIGUOUS: '表格列结构无法可靠判断'
|
|
43
|
+
};
|
|
37
44
|
const RUNNING_IMPORT_TASK_STATUSES = ['EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'];
|
|
38
45
|
const RECOVERABLE_IMPORT_TASK_STATUSES = ['QUEUED', 'EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'];
|
|
39
46
|
const RECOVERABLE_REANALYSIS_STATUSES = ['PENDING', 'RUNNING'];
|
|
@@ -716,7 +723,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
716
723
|
const batch = await batchRepo.save(batchRepo.create({
|
|
717
724
|
...auditScope(scope), catalogType, sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName,
|
|
718
725
|
importMode: input.replaceBatchId ? 'REPLACE_BATCH' : 'APPEND', replacedBatchId: input.replaceBatchId ?? null,
|
|
719
|
-
totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0,
|
|
726
|
+
totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0, skippedCount: 0,
|
|
727
|
+
inferenceMetadata: input.inferenceMetadata ?? null,
|
|
720
728
|
importTaskId: task.id, riskRecalculatedAt: null
|
|
721
729
|
}));
|
|
722
730
|
batchId = batch.id;
|
|
@@ -736,14 +744,15 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
736
744
|
const batch = await batchRepo.save(batchRepo.create({
|
|
737
745
|
...auditScope(scope), sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName,
|
|
738
746
|
importMode: input.replaceBatchId ? 'REPLACE_BATCH' : 'APPEND', replacedBatchId: input.replaceBatchId ?? null,
|
|
739
|
-
totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0,
|
|
747
|
+
totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0, skippedCount: 0,
|
|
748
|
+
inferenceMetadata: input.inferenceMetadata ?? null, importTaskId: task.id
|
|
740
749
|
}));
|
|
741
750
|
batchId = batch.id;
|
|
742
751
|
}
|
|
743
752
|
task.status = 'PERSISTING';
|
|
744
753
|
task.resultEntityId = batchId;
|
|
745
754
|
task.completedAt = null;
|
|
746
|
-
task.progress = initialFastCatalogProgress(input.totalCandidates);
|
|
755
|
+
task.progress = initialFastCatalogProgress(input.totalCandidates, input.inferenceMetadata);
|
|
747
756
|
await taskRepo.save(task);
|
|
748
757
|
return { committedCursor: 0, batchId };
|
|
749
758
|
});
|
|
@@ -771,23 +780,29 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
771
780
|
const batchRepo = manager.getRepository(ControlledCatalogBatch);
|
|
772
781
|
const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
|
|
773
782
|
await this.appendControlledCatalogRows(manager, scope, batch, controlledRows, localizedRows);
|
|
783
|
+
await this.appendCatalogSkippedSources(manager.getRepository(ControlledCatalogImportIssue), scope, batch, input.skipped);
|
|
774
784
|
await batchRepo.save(batch);
|
|
775
785
|
task.progress.successCount = batch.successCount;
|
|
776
786
|
task.progress.duplicateCount = batch.duplicateCount;
|
|
777
787
|
task.progress.invalidCount = batch.invalidCount;
|
|
788
|
+
task.progress.skippedCount = batch.skippedCount;
|
|
778
789
|
}
|
|
779
790
|
else {
|
|
780
791
|
const batch = await this.appendFastSanctionCandidates(manager, scope, task.resultEntityId, input.candidates);
|
|
792
|
+
await this.appendCatalogSkippedSources(manager.getRepository(SanctionCatalogImportIssue), scope, batch, input.skipped);
|
|
793
|
+
await manager.getRepository(SanctionCatalogBatch).save(batch);
|
|
781
794
|
task.progress.successCount = batch.successCount;
|
|
782
795
|
task.progress.duplicateCount = batch.duplicateCount;
|
|
783
796
|
task.progress.invalidCount = batch.invalidCount;
|
|
797
|
+
task.progress.skippedCount = batch.skippedCount;
|
|
784
798
|
}
|
|
799
|
+
let insertedReviewCount = 0;
|
|
785
800
|
if (input.reviewBlocks.length) {
|
|
786
801
|
const reviewRepo = manager.getRepository(CatalogReviewBlock);
|
|
787
802
|
for (const block of input.reviewBlocks) {
|
|
788
803
|
const existing = await reviewRepo.findOne({ where: {
|
|
789
804
|
...activeScope(scope), importTaskId: task.id, ordinal: block.ordinal,
|
|
790
|
-
sourceLocation: block.sourceLocation
|
|
805
|
+
sourceLocation: block.sourceLocation
|
|
791
806
|
} });
|
|
792
807
|
if (existing)
|
|
793
808
|
continue;
|
|
@@ -798,11 +813,12 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
798
813
|
payload: block.payload, reasons: block.reasons,
|
|
799
814
|
leaseToken: null, leaseExpiresAt: null, resolvedAt: null
|
|
800
815
|
}));
|
|
816
|
+
insertedReviewCount += 1;
|
|
801
817
|
}
|
|
802
818
|
}
|
|
803
819
|
task.progress.committedCursor = input.endCursor;
|
|
804
820
|
task.progress.processedCount = input.endCursor + input.reviewBlocks.length + input.skipped.length;
|
|
805
|
-
task.progress.reviewPendingCount +=
|
|
821
|
+
task.progress.reviewPendingCount += insertedReviewCount;
|
|
806
822
|
task.progress.currentBatch = input.endCursor === 0 ? 0 : Math.ceil(input.endCursor / 100);
|
|
807
823
|
task.progress.progressVersion += 1;
|
|
808
824
|
task.progress.lastProgressAt = new Date().toISOString();
|
|
@@ -820,18 +836,42 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
820
836
|
if (!current.progress || current.progress.mode !== 'FAST_PIPELINE') {
|
|
821
837
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '快速目录导入进度不存在');
|
|
822
838
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
839
|
+
if (current.progress.reviewPendingCount > 0) {
|
|
840
|
+
current.status = 'AWAITING_REVIEW';
|
|
841
|
+
current.progress.stage = 'REVIEWING';
|
|
842
|
+
current.progress.observerStatus = 'PENDING';
|
|
843
|
+
current.completedAt = null;
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
const counts = await this.catalogBatchCounts(manager, scope, current);
|
|
847
|
+
const outcome = catalogTerminalOutcome(counts);
|
|
848
|
+
current.progress.successCount = counts.successCount;
|
|
849
|
+
current.progress.duplicateCount = counts.duplicateCount;
|
|
850
|
+
current.progress.invalidCount = counts.invalidCount;
|
|
851
|
+
current.progress.skippedCount = counts.skippedCount;
|
|
852
|
+
current.progress.outcome = outcome.outcome;
|
|
853
|
+
if (outcome.status === 'FAILED') {
|
|
854
|
+
await this.applyImportTaskFailure(manager, scope, current, 'FAILED', outcome.errorCode, outcome.errorMessage);
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
current.status = 'SUCCEEDED';
|
|
858
|
+
current.progress.stage = 'SUCCEEDED';
|
|
859
|
+
current.progress.observerStatus = 'COMPLETED';
|
|
860
|
+
current.errorCode = null;
|
|
861
|
+
current.errorMessage = null;
|
|
862
|
+
current.completedAt = new Date();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
826
865
|
current.progress.progressVersion += 1;
|
|
827
866
|
current.progress.lastProgressAt = new Date().toISOString();
|
|
828
|
-
current.completedAt = new Date();
|
|
829
867
|
return repo.save(current);
|
|
830
868
|
});
|
|
831
|
-
if (task.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
869
|
+
if (task.status === 'SUCCEEDED' && (task.progress?.successCount ?? 0) > 0) {
|
|
870
|
+
if (task.documentType === 'CONTROLLED_CATALOG')
|
|
871
|
+
await this.recalculateProductRisks(scope, 'CATALOG_CHANGED', task.resultEntityId ?? undefined);
|
|
872
|
+
else
|
|
873
|
+
await this.recalculateCompanyRisks(scope, 'CATALOG_CHANGED');
|
|
874
|
+
}
|
|
835
875
|
return task;
|
|
836
876
|
}
|
|
837
877
|
async waitImportProgress(scope, taskId, afterProgressVersion = 0, waitMs = 10_000) {
|
|
@@ -914,12 +954,14 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
914
954
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '复核块租约无效或已过期');
|
|
915
955
|
}
|
|
916
956
|
const acceptedCandidates = [];
|
|
957
|
+
const rejectedBlocks = [];
|
|
917
958
|
let resolved = 0;
|
|
918
959
|
let rejected = 0;
|
|
919
960
|
for (const item of items) {
|
|
920
961
|
const block = blocks.find(value => value.id === item.blockId);
|
|
921
962
|
if (item.decision === 'REJECT') {
|
|
922
963
|
block.status = 'REJECTED';
|
|
964
|
+
rejectedBlocks.push(block);
|
|
923
965
|
rejected += 1;
|
|
924
966
|
}
|
|
925
967
|
else {
|
|
@@ -942,12 +984,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
942
984
|
const batchRepo = manager.getRepository(ControlledCatalogBatch);
|
|
943
985
|
const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
|
|
944
986
|
const rows = acceptedCandidates.flatMap(candidateToControlledRows);
|
|
945
|
-
const localized =
|
|
946
|
-
productName: String(row.productName ?? '').trim(),
|
|
947
|
-
controlDescription: optionalString(row.controlDescription),
|
|
948
|
-
originalProductName: null, originalControlDescription: null,
|
|
949
|
-
translationSource: row.translationSource ?? null
|
|
950
|
-
}));
|
|
987
|
+
const localized = await this.localizeControlledCatalogRows(rows);
|
|
951
988
|
await this.appendControlledCatalogRows(manager, scope, batch, rows, localized);
|
|
952
989
|
await batchRepo.save(batch);
|
|
953
990
|
task.progress.successCount = batch.successCount;
|
|
@@ -960,6 +997,13 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
960
997
|
task.progress.duplicateCount = batch.duplicateCount;
|
|
961
998
|
task.progress.invalidCount = batch.invalidCount;
|
|
962
999
|
}
|
|
1000
|
+
const rejectedBatch = await this.appendRejectedCatalogReviews(manager, scope, task, rejectedBlocks);
|
|
1001
|
+
if (rejectedBatch) {
|
|
1002
|
+
task.progress.successCount = rejectedBatch.successCount;
|
|
1003
|
+
task.progress.duplicateCount = rejectedBatch.duplicateCount;
|
|
1004
|
+
task.progress.invalidCount = rejectedBatch.invalidCount;
|
|
1005
|
+
task.progress.skippedCount = rejectedBatch.skippedCount;
|
|
1006
|
+
}
|
|
963
1007
|
await reviewRepo.save(blocks);
|
|
964
1008
|
task.progress.reviewResolvedCount += resolved;
|
|
965
1009
|
task.progress.reviewRejectedCount += rejected;
|
|
@@ -967,10 +1011,24 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
967
1011
|
task.progress.progressVersion += 1;
|
|
968
1012
|
task.progress.lastProgressAt = new Date().toISOString();
|
|
969
1013
|
if (task.progress.reviewPendingCount === 0) {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
task.progress.
|
|
973
|
-
task.
|
|
1014
|
+
const counts = await this.catalogBatchCounts(manager, scope, task);
|
|
1015
|
+
const outcome = catalogTerminalOutcome(counts);
|
|
1016
|
+
task.progress.successCount = counts.successCount;
|
|
1017
|
+
task.progress.duplicateCount = counts.duplicateCount;
|
|
1018
|
+
task.progress.invalidCount = counts.invalidCount;
|
|
1019
|
+
task.progress.skippedCount = counts.skippedCount;
|
|
1020
|
+
task.progress.outcome = outcome.outcome;
|
|
1021
|
+
if (outcome.status === 'FAILED') {
|
|
1022
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', outcome.errorCode, outcome.errorMessage);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
task.status = 'SUCCEEDED';
|
|
1026
|
+
task.progress.stage = 'SUCCEEDED';
|
|
1027
|
+
task.progress.observerStatus = 'COMPLETED';
|
|
1028
|
+
task.errorCode = null;
|
|
1029
|
+
task.errorMessage = null;
|
|
1030
|
+
task.completedAt = new Date();
|
|
1031
|
+
}
|
|
974
1032
|
}
|
|
975
1033
|
else {
|
|
976
1034
|
task.status = 'AWAITING_REVIEW';
|
|
@@ -1039,6 +1097,148 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1039
1097
|
}
|
|
1040
1098
|
return batchRepo.save(batch);
|
|
1041
1099
|
}
|
|
1100
|
+
async appendCatalogSkippedSources(repository, scope, batch, skippedSources) {
|
|
1101
|
+
for (const skipped of skippedSources) {
|
|
1102
|
+
const parsedPayload = { skipReason: skipped.reason, sourceLocation: skipped.sourceLocation };
|
|
1103
|
+
const existing = await repository.findOne({ where: {
|
|
1104
|
+
...activeScope(scope), batchId: batch.id, issueType: 'INVALID', parsedPayload
|
|
1105
|
+
} });
|
|
1106
|
+
if (existing)
|
|
1107
|
+
continue;
|
|
1108
|
+
batch.totalCount += 1;
|
|
1109
|
+
batch.invalidCount += 1;
|
|
1110
|
+
batch.skippedCount += 1;
|
|
1111
|
+
await repository.save(repository.create({
|
|
1112
|
+
...createdScope(scope), batchId: batch.id, issueType: 'INVALID', sourceSequence: batch.totalCount,
|
|
1113
|
+
sourceLocation: skipped.sourceLocation || null, sourceContent: skipped.sourceContent || null,
|
|
1114
|
+
parsedPayload, message: SKIPPED_REASON_MESSAGES[skipped.reason] ?? '无法入库的来源行已跳过'
|
|
1115
|
+
}));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async appendRejectedCatalogReviews(manager, scope, task, blocks) {
|
|
1119
|
+
if (blocks.length === 0 || !task.resultEntityId)
|
|
1120
|
+
return null;
|
|
1121
|
+
const controlled = task.documentType === 'CONTROLLED_CATALOG';
|
|
1122
|
+
const batchRepo = controlled
|
|
1123
|
+
? manager.getRepository(ControlledCatalogBatch)
|
|
1124
|
+
: manager.getRepository(SanctionCatalogBatch);
|
|
1125
|
+
const issueRepo = controlled
|
|
1126
|
+
? manager.getRepository(ControlledCatalogImportIssue)
|
|
1127
|
+
: manager.getRepository(SanctionCatalogImportIssue);
|
|
1128
|
+
const batch = await required(batchRepo.findOne({ where: {
|
|
1129
|
+
...activeScope(scope), id: task.resultEntityId
|
|
1130
|
+
} }), controlled ? '管控目录批次不存在' : '制裁目录批次不存在');
|
|
1131
|
+
for (const block of blocks) {
|
|
1132
|
+
const parsedPayload = { reviewBlockId: block.id, reviewDecision: 'REJECT' };
|
|
1133
|
+
const existing = await issueRepo.findOne({ where: {
|
|
1134
|
+
...activeScope(scope), batchId: batch.id, issueType: 'INVALID', parsedPayload
|
|
1135
|
+
} });
|
|
1136
|
+
if (existing)
|
|
1137
|
+
continue;
|
|
1138
|
+
batch.totalCount += 1;
|
|
1139
|
+
batch.invalidCount += 1;
|
|
1140
|
+
batch.skippedCount += 1;
|
|
1141
|
+
await issueRepo.save(issueRepo.create({
|
|
1142
|
+
...createdScope(scope), batchId: batch.id, issueType: 'INVALID', sourceSequence: batch.totalCount,
|
|
1143
|
+
sourceLocation: block.sourceLocation, sourceContent: block.sourceContent,
|
|
1144
|
+
parsedPayload, message: '审核未通过,来源行已跳过'
|
|
1145
|
+
}));
|
|
1146
|
+
}
|
|
1147
|
+
return batchRepo.save(batch);
|
|
1148
|
+
}
|
|
1149
|
+
async catalogBatchCounts(manager, scope, task) {
|
|
1150
|
+
if (!task.resultEntityId)
|
|
1151
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '目录导入批次不存在');
|
|
1152
|
+
const batch = task.documentType === 'CONTROLLED_CATALOG'
|
|
1153
|
+
? await required(manager.getRepository(ControlledCatalogBatch).findOne({
|
|
1154
|
+
where: { ...activeScope(scope), id: task.resultEntityId }
|
|
1155
|
+
}), '管控目录批次不存在')
|
|
1156
|
+
: await required(manager.getRepository(SanctionCatalogBatch).findOne({
|
|
1157
|
+
where: { ...activeScope(scope), id: task.resultEntityId }
|
|
1158
|
+
}), '制裁目录批次不存在');
|
|
1159
|
+
return {
|
|
1160
|
+
successCount: batch.successCount,
|
|
1161
|
+
duplicateCount: batch.duplicateCount,
|
|
1162
|
+
invalidCount: batch.invalidCount,
|
|
1163
|
+
skippedCount: batch.skippedCount ?? 0
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async rollbackFailedCatalogReplacement(manager, scope, task) {
|
|
1167
|
+
if (!task.resultEntityId || (task.documentType !== 'CONTROLLED_CATALOG' && task.documentType !== 'SANCTION_CATALOG'))
|
|
1168
|
+
return;
|
|
1169
|
+
const now = new Date();
|
|
1170
|
+
if (task.documentType === 'CONTROLLED_CATALOG') {
|
|
1171
|
+
const batchRepo = manager.getRepository(ControlledCatalogBatch);
|
|
1172
|
+
const recordRepo = manager.getRepository(ControlledGoodsRecord);
|
|
1173
|
+
const current = await batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } });
|
|
1174
|
+
if (!current?.replacedBatchId)
|
|
1175
|
+
return;
|
|
1176
|
+
const replaced = await batchRepo.findOne({ where: { ...activeScope(scope), id: current.replacedBatchId } });
|
|
1177
|
+
if (!replaced || replaced.deleteOperation !== 'CATALOG_REPLACE')
|
|
1178
|
+
return;
|
|
1179
|
+
current.deletedAt = now;
|
|
1180
|
+
current.deletedById = scope.userId ?? null;
|
|
1181
|
+
current.deleteOperation = 'IMPORT_FAILED';
|
|
1182
|
+
current.updatedById = scope.userId ?? null;
|
|
1183
|
+
await batchRepo.save(current);
|
|
1184
|
+
await recordRepo.update({
|
|
1185
|
+
...activeScope(scope), batchId: current.id, deletedAt: IsNull()
|
|
1186
|
+
}, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'IMPORT_FAILED', updatedById: scope.userId ?? null });
|
|
1187
|
+
replaced.deletedAt = null;
|
|
1188
|
+
replaced.deletedById = null;
|
|
1189
|
+
replaced.deleteOperation = null;
|
|
1190
|
+
replaced.updatedById = scope.userId ?? null;
|
|
1191
|
+
await batchRepo.save(replaced);
|
|
1192
|
+
await recordRepo.update({
|
|
1193
|
+
...activeScope(scope), batchId: replaced.id, deleteOperation: 'CATALOG_REPLACE'
|
|
1194
|
+
}, { deletedAt: null, deletedById: null, deleteOperation: null, updatedById: scope.userId ?? null });
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const batchRepo = manager.getRepository(SanctionCatalogBatch);
|
|
1198
|
+
const recordRepo = manager.getRepository(SanctionedCompanyRecord);
|
|
1199
|
+
const aliasRepo = manager.getRepository(SanctionedCompanyAlias);
|
|
1200
|
+
const current = await batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } });
|
|
1201
|
+
if (!current?.replacedBatchId)
|
|
1202
|
+
return;
|
|
1203
|
+
const replaced = await batchRepo.findOne({ where: { ...activeScope(scope), id: current.replacedBatchId } });
|
|
1204
|
+
if (!replaced || replaced.deleteOperation !== 'SANCTION_CATALOG_REPLACE')
|
|
1205
|
+
return;
|
|
1206
|
+
current.deletedAt = now;
|
|
1207
|
+
current.deletedById = scope.userId ?? null;
|
|
1208
|
+
current.deleteOperation = 'IMPORT_FAILED';
|
|
1209
|
+
current.updatedById = scope.userId ?? null;
|
|
1210
|
+
await batchRepo.save(current);
|
|
1211
|
+
const failedRecords = await recordRepo.find({
|
|
1212
|
+
where: { ...activeScope(scope), batchId: current.id, deletedAt: IsNull() }, select: { id: true }
|
|
1213
|
+
});
|
|
1214
|
+
if (failedRecords.length > 0) {
|
|
1215
|
+
await aliasRepo.update({
|
|
1216
|
+
...activeScope(scope), sanctionedCompanyId: In(failedRecords.map(record => record.id)), deletedAt: IsNull()
|
|
1217
|
+
}, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'IMPORT_FAILED', updatedById: scope.userId ?? null });
|
|
1218
|
+
}
|
|
1219
|
+
await recordRepo.update({
|
|
1220
|
+
...activeScope(scope), batchId: current.id, deletedAt: IsNull()
|
|
1221
|
+
}, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'IMPORT_FAILED', updatedById: scope.userId ?? null });
|
|
1222
|
+
replaced.deletedAt = null;
|
|
1223
|
+
replaced.deletedById = null;
|
|
1224
|
+
replaced.deleteOperation = null;
|
|
1225
|
+
replaced.updatedById = scope.userId ?? null;
|
|
1226
|
+
await batchRepo.save(replaced);
|
|
1227
|
+
await recordRepo.update({
|
|
1228
|
+
...activeScope(scope), batchId: replaced.id, deleteOperation: 'SANCTION_CATALOG_REPLACE'
|
|
1229
|
+
}, { deletedAt: null, deletedById: null, deleteOperation: null, updatedById: scope.userId ?? null });
|
|
1230
|
+
}
|
|
1231
|
+
async applyImportTaskFailure(manager, scope, task, status, errorCode, errorMessage, completedAt = new Date()) {
|
|
1232
|
+
task.status = status;
|
|
1233
|
+
task.errorCode = errorCode;
|
|
1234
|
+
task.errorMessage = errorMessage;
|
|
1235
|
+
task.completedAt = completedAt;
|
|
1236
|
+
if (task.progress?.mode === 'FAST_PIPELINE') {
|
|
1237
|
+
task.progress.stage = 'FAILED';
|
|
1238
|
+
task.progress.observerStatus = 'FAILED';
|
|
1239
|
+
await this.rollbackFailedCatalogReplacement(manager, scope, task);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1042
1242
|
async createImportTask(scope, input) {
|
|
1043
1243
|
const repo = this.dataSource.getRepository(ImportTask);
|
|
1044
1244
|
const targetAssistantId = requiredId(input.targetAssistantId, '目标智能体标识');
|
|
@@ -1075,10 +1275,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1075
1275
|
if (!task)
|
|
1076
1276
|
return null;
|
|
1077
1277
|
if (!task.targetAssistantId) {
|
|
1078
|
-
|
|
1079
|
-
task.errorCode = 'TARGET_ASSISTANT_MISSING';
|
|
1080
|
-
task.errorMessage = '历史解析任务缺少目标智能体,无法自动恢复';
|
|
1081
|
-
task.completedAt = new Date();
|
|
1278
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'TARGET_ASSISTANT_MISSING', '历史解析任务缺少目标智能体,无法自动恢复');
|
|
1082
1279
|
await repo.save(task);
|
|
1083
1280
|
return null;
|
|
1084
1281
|
}
|
|
@@ -1102,10 +1299,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1102
1299
|
if (!task)
|
|
1103
1300
|
return null;
|
|
1104
1301
|
if (!task.targetAssistantId) {
|
|
1105
|
-
|
|
1106
|
-
task.errorCode = 'TARGET_ASSISTANT_MISSING';
|
|
1107
|
-
task.errorMessage = '历史解析任务缺少目标智能体,无法自动恢复';
|
|
1108
|
-
task.completedAt = new Date();
|
|
1302
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'TARGET_ASSISTANT_MISSING', '历史解析任务缺少目标智能体,无法自动恢复');
|
|
1109
1303
|
await repo.save(task);
|
|
1110
1304
|
return null;
|
|
1111
1305
|
}
|
|
@@ -1181,10 +1375,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1181
1375
|
const task = await repo.findOne({ where: { ...activeScope(snapshot), id: snapshot.id }, lock: { mode: 'pessimistic_write' } });
|
|
1182
1376
|
if (!task || task.targetAssistantId || !RECOVERABLE_IMPORT_TASK_STATUSES.includes(task.status))
|
|
1183
1377
|
return;
|
|
1184
|
-
|
|
1185
|
-
task.errorCode = 'TARGET_ASSISTANT_MISSING';
|
|
1186
|
-
task.errorMessage = '历史解析任务缺少目标智能体,无法自动恢复';
|
|
1187
|
-
task.completedAt = new Date();
|
|
1378
|
+
await this.applyImportTaskFailure(manager, snapshot, task, 'FAILED', 'TARGET_ASSISTANT_MISSING', '历史解析任务缺少目标智能体,无法自动恢复');
|
|
1188
1379
|
await repo.save(task);
|
|
1189
1380
|
});
|
|
1190
1381
|
}
|
|
@@ -1213,10 +1404,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1213
1404
|
return task;
|
|
1214
1405
|
if (task.claimAttemptToken !== attemptToken)
|
|
1215
1406
|
return task;
|
|
1216
|
-
|
|
1217
|
-
task.errorCode = 'AI_PARSING_TIMEOUT';
|
|
1218
|
-
task.errorMessage = '解析任务超过 20 分钟未完成';
|
|
1219
|
-
task.completedAt = new Date();
|
|
1407
|
+
await this.applyImportTaskFailure(manager, scope, task, 'TIMED_OUT', 'AI_PARSING_TIMEOUT', '解析任务超过 20 分钟未完成');
|
|
1220
1408
|
return repo.save(task);
|
|
1221
1409
|
});
|
|
1222
1410
|
}
|
|
@@ -1328,10 +1516,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1328
1516
|
return task;
|
|
1329
1517
|
if (task.status === 'FAILED' || task.status === 'TIMED_OUT')
|
|
1330
1518
|
return task;
|
|
1331
|
-
|
|
1332
|
-
task.errorCode = 'AGENT_HANDOFF_RESULT_MISSING';
|
|
1333
|
-
task.errorMessage = '智能体已结束,但解析工具未保存业务结果';
|
|
1334
|
-
task.completedAt = new Date();
|
|
1519
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'AGENT_HANDOFF_RESULT_MISSING', '智能体已结束,但解析工具未保存业务结果');
|
|
1335
1520
|
return repo.save(task);
|
|
1336
1521
|
});
|
|
1337
1522
|
}
|
|
@@ -1355,17 +1540,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1355
1540
|
if (input.kind === 'complete' &&
|
|
1356
1541
|
task.status !== 'AWAITING_REVIEW' && task.status !== 'SUCCEEDED' &&
|
|
1357
1542
|
task.status !== 'FAILED' && task.status !== 'TIMED_OUT') {
|
|
1358
|
-
|
|
1359
|
-
task.errorCode = 'AGENT_HANDOFF_RESULT_MISSING';
|
|
1360
|
-
task.errorMessage = '智能体已结束,但解析工具未保存业务结果';
|
|
1361
|
-
task.completedAt = new Date();
|
|
1543
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'AGENT_HANDOFF_RESULT_MISSING', '智能体已结束,但解析工具未保存业务结果');
|
|
1362
1544
|
}
|
|
1363
1545
|
else if (input.kind === 'error' &&
|
|
1364
1546
|
!TERMINAL_IMPORT_TASK_STATUSES.includes(task.status)) {
|
|
1365
|
-
|
|
1366
|
-
task.errorCode = requiredId(input.errorCode, '错误代码');
|
|
1367
|
-
task.errorMessage = requiredId(input.errorMessage, '错误信息');
|
|
1368
|
-
task.completedAt = new Date();
|
|
1547
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', requiredId(input.errorCode, '错误代码'), requiredId(input.errorMessage, '错误信息'));
|
|
1369
1548
|
}
|
|
1370
1549
|
await repo.save(task);
|
|
1371
1550
|
return { accepted: true, sequence: input.sequence };
|
|
@@ -1377,10 +1556,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1377
1556
|
const task = await required(repo.findOne({ where: { ...activeScope(scope), id }, lock: { mode: 'pessimistic_write' } }), '解析任务不存在');
|
|
1378
1557
|
if (TERMINAL_IMPORT_TASK_STATUSES.includes(task.status))
|
|
1379
1558
|
return task;
|
|
1380
|
-
|
|
1381
|
-
task.errorCode = failure.errorCode;
|
|
1382
|
-
task.errorMessage = failure.errorMessage;
|
|
1383
|
-
task.completedAt = new Date();
|
|
1559
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', failure.errorCode, failure.errorMessage);
|
|
1384
1560
|
return repo.save(task);
|
|
1385
1561
|
});
|
|
1386
1562
|
}
|
|
@@ -1397,10 +1573,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1397
1573
|
return task;
|
|
1398
1574
|
if (!snapshotMissingRecoveryMetadataStillMissing(snapshot, task))
|
|
1399
1575
|
return task;
|
|
1400
|
-
|
|
1401
|
-
task.errorCode = failure.errorCode;
|
|
1402
|
-
task.errorMessage = failure.errorMessage;
|
|
1403
|
-
task.completedAt = new Date();
|
|
1576
|
+
await this.applyImportTaskFailure(manager, scope, task, 'FAILED', failure.errorCode, failure.errorMessage);
|
|
1404
1577
|
return repo.save(task);
|
|
1405
1578
|
});
|
|
1406
1579
|
}
|
|
@@ -2470,10 +2643,33 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2470
2643
|
.orWhere("COALESCE(row.generationSnapshot #>> '{request,fileNumber}', '') ILIKE :generatedFileName")), { generatedFileName: `%${String(fileName).trim()}%` });
|
|
2471
2644
|
}
|
|
2472
2645
|
if (hasQueryValue(customerName))
|
|
2473
|
-
qb.andWhere(
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2646
|
+
qb.andWhere(`(
|
|
2647
|
+
COALESCE(row.generationSnapshot #>> '{customer,name}', '') ILIKE :generatedFileCustomerName
|
|
2648
|
+
OR EXISTS (
|
|
2649
|
+
SELECT 1
|
|
2650
|
+
FROM plugin_trade_compliance_customer_contract legacy_contract_filter
|
|
2651
|
+
INNER JOIN plugin_trade_compliance_customer legacy_customer_filter
|
|
2652
|
+
ON legacy_customer_filter.id = legacy_contract_filter."customerId"
|
|
2653
|
+
AND legacy_customer_filter."tenantId" = legacy_contract_filter."tenantId"
|
|
2654
|
+
AND legacy_customer_filter."organizationId" = legacy_contract_filter."organizationId"
|
|
2655
|
+
AND legacy_customer_filter."deletedAt" IS NULL
|
|
2656
|
+
WHERE legacy_contract_filter.id = row."customerContractId"
|
|
2657
|
+
AND legacy_contract_filter."tenantId" = row."tenantId"
|
|
2658
|
+
AND legacy_contract_filter."organizationId" = row."organizationId"
|
|
2659
|
+
AND legacy_contract_filter."deletedAt" IS NULL
|
|
2660
|
+
AND legacy_customer_filter.name ILIKE :generatedFileCustomerName
|
|
2661
|
+
)
|
|
2662
|
+
)`, { generatedFileCustomerName: `%${String(customerName).trim()}%` });
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
const snapshotNames = new Map(page.items.map(item => [item.id, generatedFileCustomerName(item.generationSnapshot)]));
|
|
2666
|
+
const fallbackContractIds = [...new Set(page.items.filter(item => !snapshotNames.get(item.id)).map(item => item.customerContractId).filter((id) => Boolean(id)))];
|
|
2667
|
+
const contracts = fallbackContractIds.length ? await this.dataSource.getRepository(CustomerContract).find({ where: { ...activeScope(scope), id: In(fallbackContractIds), deletedAt: IsNull() } }) : [];
|
|
2668
|
+
const customerIds = [...new Set(contracts.map(contract => contract.customerId).filter((id) => Boolean(id)))];
|
|
2669
|
+
const customers = customerIds.length ? await this.dataSource.getRepository(Customer).find({ where: { ...activeScope(scope), id: In(customerIds), deletedAt: IsNull() } }) : [];
|
|
2670
|
+
const contractCustomerIds = new Map(contracts.map(contract => [contract.id, contract.customerId]));
|
|
2671
|
+
const customerNames = new Map(customers.map(customer => [customer.id, customer.name]));
|
|
2672
|
+
return { ...page, items: page.items.map(item => ({ ...item, customerName: snapshotNames.get(item.id) ?? customerNames.get(contractCustomerIds.get(item.customerContractId) ?? '') ?? null })) };
|
|
2477
2673
|
}
|
|
2478
2674
|
async listMonthlyRates(scope, query = {}) {
|
|
2479
2675
|
const page = Math.max(1, Math.trunc(Number(query.page)) || 1);
|
|
@@ -2695,8 +2891,9 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2695
2891
|
const statement = analyticsAggregationStatement(scope, query, currentYear, 'PAGE');
|
|
2696
2892
|
const rawRows = await source.query(statement.sql, statement.parameters);
|
|
2697
2893
|
const total = Number(rawRows[0]?.total ?? rawRows.length);
|
|
2698
|
-
|
|
2699
|
-
|
|
2894
|
+
const maxRows = analyticsExportMaxRows();
|
|
2895
|
+
if (total > maxRows)
|
|
2896
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `分析分组最多支持 ${maxRows} 条`, { total });
|
|
2700
2897
|
const pageCount = Math.max(1, Math.ceil(total / query.pageSize));
|
|
2701
2898
|
const page = Math.min(query.page, pageCount);
|
|
2702
2899
|
const pageStart = (page - 1) * query.pageSize + 1;
|
|
@@ -2724,11 +2921,12 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2724
2921
|
sheet.getRow(1).font = { bold: true };
|
|
2725
2922
|
sheet.getRow(1).commit();
|
|
2726
2923
|
const statement = analyticsAggregationStatement(scope, query, shanghaiCurrentYear(), 'EXPORT');
|
|
2924
|
+
const maxRows = analyticsExportMaxRows();
|
|
2727
2925
|
let count = 0;
|
|
2728
2926
|
await this.withAnalyticsSnapshot(statement, async (raw) => {
|
|
2729
2927
|
count += 1;
|
|
2730
|
-
if (count >
|
|
2731
|
-
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `分析导出最多支持 ${
|
|
2928
|
+
if (count > maxRows)
|
|
2929
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `分析导出最多支持 ${maxRows} 条明细`, { total: count });
|
|
2732
2930
|
const row = analyticsDetailFromRaw(raw, query);
|
|
2733
2931
|
sheet.addRow(query.dimension === 'PRODUCT'
|
|
2734
2932
|
? [row.label, row.orderCount, row.productQuantity, row.amountCny]
|
|
@@ -3083,10 +3281,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3083
3281
|
const lockedStartedAt = locked.startedAt ?? locked.queuedAt ?? locked.createdAt;
|
|
3084
3282
|
if (!lockedStartedAt || !importTaskTimedOut(lockedStartedAt, now))
|
|
3085
3283
|
return;
|
|
3086
|
-
|
|
3087
|
-
locked.errorCode = DomainErrorCodes.IMPORT_TIMED_OUT;
|
|
3088
|
-
locked.errorMessage = '文件解析超过 20 分钟,请重新上传';
|
|
3089
|
-
locked.completedAt = now;
|
|
3284
|
+
await this.applyImportTaskFailure(manager, scope, locked, 'TIMED_OUT', DomainErrorCodes.IMPORT_TIMED_OUT, '文件解析超过 20 分钟,请重新上传', now);
|
|
3090
3285
|
locked.machineElapsedMs = now.getTime() - lockedStartedAt.getTime();
|
|
3091
3286
|
await lockedRepo.save(locked);
|
|
3092
3287
|
});
|
|
@@ -3327,6 +3522,7 @@ function reanalysisClauseInput(value) {
|
|
|
3327
3522
|
return { clauseType: clause.clauseType, extractionStatus: clause.extractionStatus, sourceText: optionalText('sourceText'), reviewedText: optionalText('reviewedText'), sourceLocation: optionalText('sourceLocation'), sourceContent: optionalText('sourceContent') };
|
|
3328
3523
|
}
|
|
3329
3524
|
function importIssueExportLimit() { const configured = Number(process.env.TRADE_COMPLIANCE_IMPORT_ISSUE_EXPORT_MAX_ROWS); return Math.min(HARD_IMPORT_ISSUE_EXPORT_MAX_ROWS, Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : DEFAULT_IMPORT_ISSUE_EXPORT_MAX_ROWS); }
|
|
3525
|
+
function analyticsExportMaxRows() { const configured = Number(process.env.TRADE_COMPLIANCE_ANALYTICS_EXPORT_MAX_ROWS); return Number.isInteger(configured) && configured > 0 ? configured : DEFAULT_ANALYTICS_EXPORT_MAX_ROWS; }
|
|
3330
3526
|
function hasQueryValue(value) { return value !== undefined && value !== null && String(value).trim() !== ''; }
|
|
3331
3527
|
function generatedFileCustomerName(snapshot) {
|
|
3332
3528
|
const customer = snapshot?.customer;
|
|
@@ -3523,7 +3719,7 @@ function optionalString(value) {
|
|
|
3523
3719
|
const normalized = String(value ?? '').trim();
|
|
3524
3720
|
return normalized || null;
|
|
3525
3721
|
}
|
|
3526
|
-
function initialFastCatalogProgress(totalCandidates) {
|
|
3722
|
+
function initialFastCatalogProgress(totalCandidates, inferenceMetadata) {
|
|
3527
3723
|
return {
|
|
3528
3724
|
version: 1,
|
|
3529
3725
|
mode: 'FAST_PIPELINE',
|
|
@@ -3533,6 +3729,8 @@ function initialFastCatalogProgress(totalCandidates) {
|
|
|
3533
3729
|
successCount: 0,
|
|
3534
3730
|
duplicateCount: 0,
|
|
3535
3731
|
invalidCount: 0,
|
|
3732
|
+
skippedCount: 0,
|
|
3733
|
+
inferenceMetadata,
|
|
3536
3734
|
currentBatch: 0,
|
|
3537
3735
|
totalBatches: totalCandidates > 0 ? Math.ceil(totalCandidates / 100) : 0,
|
|
3538
3736
|
committedCursor: 0,
|
|
@@ -3551,6 +3749,7 @@ function candidateToControlledRows(candidate) {
|
|
|
3551
3749
|
: [undefined];
|
|
3552
3750
|
return hsCodes.map(hsCode => ({
|
|
3553
3751
|
productName: optionalString(candidate.values.productName) ?? undefined,
|
|
3752
|
+
originalProductName: optionalString(candidate.values.originalProductName) ?? undefined,
|
|
3554
3753
|
hsCode: typeof hsCode === 'string' ? hsCode : undefined,
|
|
3555
3754
|
controlDescription: optionalString(candidate.values.controlDescription) ?? undefined,
|
|
3556
3755
|
controlCode: optionalString(candidate.values.controlCode) ?? undefined,
|
|
@@ -3558,7 +3757,9 @@ function candidateToControlledRows(candidate) {
|
|
|
3558
3757
|
sourceOrdinal: optionalString(candidate.values.sourceOrdinal) ?? String(candidate.ordinal),
|
|
3559
3758
|
sourceLocation: evidence?.sourceLocation,
|
|
3560
3759
|
sourceContent: evidence?.sourceContent,
|
|
3561
|
-
translationSource:
|
|
3760
|
+
translationSource: candidate.values.translationRequired || (!candidate.values.productName && candidate.values.originalProductName)
|
|
3761
|
+
? 'AGENT_TRANSLATED'
|
|
3762
|
+
: 'SOURCE_CHINESE'
|
|
3562
3763
|
}));
|
|
3563
3764
|
}
|
|
3564
3765
|
function candidateAliases(candidate) {
|
|
@@ -3713,7 +3914,7 @@ function analyticsAggregationStatement(scope, query, currentYear, purpose) {
|
|
|
3713
3914
|
FROM sales_orders orders GROUP BY orders."customerId"
|
|
3714
3915
|
)`;
|
|
3715
3916
|
}
|
|
3716
|
-
const bounded = `${sourceSql}, bounded AS (SELECT * FROM grouped ORDER BY "amountCny" DESC, key ASC LIMIT ${
|
|
3917
|
+
const bounded = `${sourceSql}, bounded AS (SELECT * FROM grouped ORDER BY "amountCny" DESC, key ASC LIMIT ${analyticsExportMaxRows() + 1})`;
|
|
3717
3918
|
if (purpose === 'EXPORT')
|
|
3718
3919
|
return { sql: `WITH ${bounded} SELECT * FROM bounded ORDER BY "amountCny" DESC, key ASC`, parameters };
|
|
3719
3920
|
const metricColumns = query.dimension === 'PRODUCT'
|