@chenchaolong/plugin-trade-compliance-workbench 1.0.119 → 1.0.121

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.
@@ -11,7 +11,7 @@ import { join } from 'node:path';
11
11
  import { finished } from 'node:stream/promises';
12
12
  import { createHash, randomUUID } from 'node:crypto';
13
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
- import { CATALOG_REVIEW_BATCH_LIMIT } from './constants.js';
14
+ import { CATALOG_REVIEW_BATCH_LIMIT, CATALOG_REVIEW_LEASE_MS, CATALOG_TRANSLATION_REVIEW_LEASE_MS } from './constants.js';
15
15
  import { CatalogTypes, ClauseTypes, CustomerProfitRateStatuses, DomainError, DomainErrorCodes, RefundValidationMethods, RiskLevels, SalesPaymentStatuses, SanctionHitStatuses, StalledCatalogImportDecisions } from './types.js';
16
16
  import { deriveSalesLine, assertReceivedAmount } from './domain/finance.js';
17
17
  import { parseCustomerContractNo, parsePurchaseContractNo } from './domain/contract-number.js';
@@ -699,6 +699,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
699
699
  task ??= await taskRepo.findOne({ where: { ...activeScope(scope), id: taskId } });
700
700
  if (!task)
701
701
  throw new DomainError(DomainErrorCodes.NOT_FOUND, '解析任务不存在');
702
+ task = await this.recoverExpiredCatalogReviewLeases(scope, task) ?? task;
702
703
  const publicTask = (await this.publicImportTaskDtos([task]))[0];
703
704
  if (!task.resultEntityId)
704
705
  return { task: publicTask };
@@ -1379,7 +1380,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1379
1380
  if (blocks.length === 0)
1380
1381
  return null;
1381
1382
  const leaseToken = randomUUID();
1382
- const leaseExpiresAt = new Date(Date.now() + 10 * 60_000);
1383
+ const leaseExpiresAt = new Date(Date.now() + CATALOG_TRANSLATION_REVIEW_LEASE_MS);
1383
1384
  for (const block of blocks) {
1384
1385
  block.status = 'LEASED';
1385
1386
  block.leaseToken = leaseToken;
@@ -1407,12 +1408,45 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1407
1408
  async countCatalogReviewBlocks(scope, taskId) {
1408
1409
  return this.dataSource.getRepository(CatalogReviewBlock).count({ where: { ...activeScope(scope), importTaskId: taskId, status: In(['PENDING', 'LEASED']) } });
1409
1410
  }
1411
+ async recoverExpiredCatalogReviewLeases(scope, task) {
1412
+ if ((task.documentType !== 'CONTROLLED_CATALOG' && task.documentType !== 'SANCTION_CATALOG') ||
1413
+ task.status !== 'AWAITING_REVIEW' || !task.progress)
1414
+ return null;
1415
+ return this.dataSource.transaction(async (manager) => {
1416
+ const taskRepo = manager.getRepository(ImportTask);
1417
+ const locked = await taskRepo.findOne({
1418
+ where: { ...activeScope(scope), id: task.id, deletedAt: IsNull() },
1419
+ lock: { mode: 'pessimistic_write' }
1420
+ });
1421
+ if (!locked || locked.status !== 'AWAITING_REVIEW')
1422
+ return null;
1423
+ await this.recoverExpiredCatalogReviewLeasesInTransaction(manager, scope, locked);
1424
+ return locked;
1425
+ });
1426
+ }
1427
+ async recoverExpiredCatalogReviewLeasesInTransaction(manager, scope, task) {
1428
+ if (task.documentType !== 'CONTROLLED_CATALOG' && task.documentType !== 'SANCTION_CATALOG')
1429
+ return task.progress?.reviewPendingCount ?? 0;
1430
+ const repo = manager.getRepository(CatalogReviewBlock);
1431
+ await repo.update({
1432
+ ...activeScope(scope), importTaskId: task.id, status: 'LEASED', leaseExpiresAt: LessThanOrEqual(new Date())
1433
+ }, { status: 'PENDING', leaseToken: null, leaseExpiresAt: null });
1434
+ const previousPendingCount = task.progress?.reviewPendingCount;
1435
+ const pendingCount = await this.reconcileCatalogReviewPendingCount(repo, scope, task);
1436
+ if (task.progress && previousPendingCount !== pendingCount) {
1437
+ task.progress.progressVersion += 1;
1438
+ task.progress.lastProgressAt = new Date().toISOString();
1439
+ await manager.getRepository(ImportTask).save(task);
1440
+ }
1441
+ return pendingCount;
1442
+ }
1410
1443
  async waitImportProgress(scope, taskId, afterProgressVersion = 0, waitMs = 10_000) {
1411
1444
  const deadline = Date.now() + Math.max(0, Math.min(waitMs, 10_000));
1412
1445
  while (true) {
1413
- const task = await required(this.dataSource.getRepository(ImportTask).findOne({
1446
+ let task = await required(this.dataSource.getRepository(ImportTask).findOne({
1414
1447
  where: { ...activeScope(scope), id: taskId }
1415
1448
  }), '解析任务不存在');
1449
+ task = await this.recoverExpiredCatalogReviewLeases(scope, task) ?? task;
1416
1450
  const version = task.progress?.progressVersion ?? 0;
1417
1451
  if (version > afterProgressVersion || TERMINAL_IMPORT_TASK_STATUSES.includes(task.status)) {
1418
1452
  return { task: publicImportTaskDto(task) };
@@ -1433,16 +1467,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1433
1467
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '仅目录任务包含复核块');
1434
1468
  }
1435
1469
  const repo = manager.getRepository(CatalogReviewBlock);
1436
- await repo.update({
1437
- ...activeScope(scope), importTaskId: taskId, status: 'LEASED', leaseExpiresAt: LessThanOrEqual(new Date())
1438
- }, { status: 'PENDING', leaseToken: null, leaseExpiresAt: null });
1439
- const previousPendingCount = task.progress?.reviewPendingCount;
1440
- const pendingCount = await this.reconcileCatalogReviewPendingCount(repo, scope, task);
1441
- if (task.progress && previousPendingCount !== pendingCount) {
1442
- task.progress.progressVersion += 1;
1443
- task.progress.lastProgressAt = new Date().toISOString();
1444
- await manager.getRepository(ImportTask).save(task);
1445
- }
1470
+ await this.recoverExpiredCatalogReviewLeasesInTransaction(manager, scope, task);
1446
1471
  const blocks = await repo.createQueryBuilder('block')
1447
1472
  .setLock('pessimistic_write')
1448
1473
  .setOnLocked('skip_locked')
@@ -1453,10 +1478,34 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1453
1478
  .addOrderBy('block.id', 'ASC')
1454
1479
  .limit(limit)
1455
1480
  .getMany();
1456
- if (blocks.length === 0)
1457
- return { leaseToken: null, items: [] };
1481
+ if (blocks.length === 0) {
1482
+ const leased = await repo.createQueryBuilder('block')
1483
+ .setLock('pessimistic_write')
1484
+ .setOnLocked('skip_locked')
1485
+ .where('block."tenantId" = :tenantId AND block."organizationId" = :organizationId AND block."importTaskId" = :taskId AND block.status = :status', {
1486
+ ...activeScope(scope), taskId, status: 'LEASED'
1487
+ })
1488
+ .andWhere('block."leaseExpiresAt" IS NOT NULL')
1489
+ .orderBy('block."leaseExpiresAt"', 'ASC')
1490
+ .addOrderBy('block.ordinal', 'ASC')
1491
+ .addOrderBy('block.id', 'ASC')
1492
+ .limit(1)
1493
+ .getOne();
1494
+ if (!leased)
1495
+ return { leaseToken: null, items: [] };
1496
+ const leasedUntil = leased.leaseExpiresAt instanceof Date ? leased.leaseExpiresAt : null;
1497
+ const leaseExpiresAt = leasedUntil?.toISOString() ?? null;
1498
+ const retryAfterMs = leasedUntil ? Math.max(1000, leasedUntil.getTime() - Date.now()) : 5000;
1499
+ return {
1500
+ leaseToken: null,
1501
+ items: [],
1502
+ leaseExpiresAt,
1503
+ retryAfterMs,
1504
+ instruction: '当前待复核块仍被占用,请等待 retryAfterMs 后再调用 nextReviewBatch。'
1505
+ };
1506
+ }
1458
1507
  const leaseToken = randomUUID();
1459
- const leaseExpiresAt = new Date(Date.now() + 5 * 60_000);
1508
+ const leaseExpiresAt = new Date(Date.now() + CATALOG_REVIEW_LEASE_MS);
1460
1509
  for (const block of blocks) {
1461
1510
  block.status = 'LEASED';
1462
1511
  block.leaseToken = leaseToken;