@chenchaolong/plugin-trade-compliance-workbench 1.0.50 → 1.0.52

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.
Files changed (65) hide show
  1. package/dist/lib/analytics-export.d.ts +7 -0
  2. package/dist/lib/analytics-export.d.ts.map +1 -0
  3. package/dist/lib/analytics-export.js +445 -0
  4. package/dist/lib/analytics-export.js.map +1 -0
  5. package/dist/lib/analytics-query.d.ts +43 -0
  6. package/dist/lib/analytics-query.d.ts.map +1 -0
  7. package/dist/lib/analytics-query.js +710 -0
  8. package/dist/lib/analytics-query.js.map +1 -0
  9. package/dist/lib/constants.d.ts +1 -0
  10. package/dist/lib/constants.d.ts.map +1 -1
  11. package/dist/lib/constants.js +1 -0
  12. package/dist/lib/constants.js.map +1 -1
  13. package/dist/lib/import-jobs/import-command.builder.js +2 -2
  14. package/dist/lib/import-jobs/import-command.builder.js.map +1 -1
  15. package/dist/lib/import-jobs/import-job-callback.processor.d.ts.map +1 -1
  16. package/dist/lib/import-jobs/import-job-callback.processor.js +2 -0
  17. package/dist/lib/import-jobs/import-job-callback.processor.js.map +1 -1
  18. package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
  19. package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
  20. package/dist/lib/remote-components/trade-compliance-workbench/editor-source.mjs +17 -1
  21. package/dist/lib/remote-components/trade-compliance-workbench/editor.js +100 -100
  22. package/dist/lib/remote-ui/app-source.js +11 -3
  23. package/dist/lib/remote-ui/app-source.js.map +1 -1
  24. package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
  25. package/dist/lib/remote-ui/contract-import.js +3 -1
  26. package/dist/lib/remote-ui/contract-import.js.map +1 -1
  27. package/dist/lib/remote-ui/contracts.d.ts +10 -0
  28. package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
  29. package/dist/lib/remote-ui/pages/analytics-dashboard.js +5 -5
  30. package/dist/lib/remote-ui/pages/analytics-dashboard.js.map +1 -1
  31. package/dist/lib/remote-ui/pages/analytics-detail-list.js +1 -1
  32. package/dist/lib/remote-ui/pages/analytics-detail-list.js.map +1 -1
  33. package/dist/lib/remote-ui/pages/analytics-format.js +4 -4
  34. package/dist/lib/remote-ui/pages/analytics-format.js.map +1 -1
  35. package/dist/lib/remote-ui/pages/catalog-import-history.d.ts +18 -0
  36. package/dist/lib/remote-ui/pages/catalog-import-history.d.ts.map +1 -0
  37. package/dist/lib/remote-ui/pages/catalog-import-history.js +38 -0
  38. package/dist/lib/remote-ui/pages/catalog-import-history.js.map +1 -0
  39. package/dist/lib/remote-ui/pages/catalogs.css +110 -0
  40. package/dist/lib/remote-ui/pages/catalogs.d.ts +7 -4
  41. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  42. package/dist/lib/remote-ui/pages/catalogs.js +47 -12
  43. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  44. package/dist/lib/remote-ui/pages/sales.d.ts +4 -1
  45. package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
  46. package/dist/lib/remote-ui/pages/sales.js +9 -4
  47. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  48. package/dist/lib/remote-ui/styles.css +1 -1
  49. package/dist/lib/sales-file.service.d.ts.map +1 -1
  50. package/dist/lib/sales-file.service.js +32 -0
  51. package/dist/lib/sales-file.service.js.map +1 -1
  52. package/dist/lib/types.d.ts +67 -0
  53. package/dist/lib/types.d.ts.map +1 -1
  54. package/dist/lib/types.js.map +1 -1
  55. package/dist/lib/workbench-view.provider.d.ts +7 -0
  56. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  57. package/dist/lib/workbench-view.provider.js +5 -1
  58. package/dist/lib/workbench-view.provider.js.map +1 -1
  59. package/dist/lib/workbench.middleware.js +3 -3
  60. package/dist/lib/workbench.middleware.js.map +1 -1
  61. package/dist/lib/workbench.service.d.ts +29 -7
  62. package/dist/lib/workbench.service.d.ts.map +1 -1
  63. package/dist/lib/workbench.service.js +97 -466
  64. package/dist/lib/workbench.service.js.map +1 -1
  65. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { __decorate, __metadata, __param } from "tslib";
2
- import { Injectable, Optional } from '@nestjs/common';
2
+ import { Inject, Injectable, Optional } from '@nestjs/common';
3
3
  import { InjectDataSource } from '@nestjs/typeorm';
4
4
  import { Brackets, DataSource, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
5
5
  import { Decimal } from 'decimal.js';
@@ -11,7 +11,8 @@ import { join } from 'node:path';
11
11
  import { finished } from 'node:stream/promises';
12
12
  import { 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 { AnalyticsComplianceCategoryKeys, CatalogTypes, ClauseTypes, CustomerProfitRateStatuses, DomainError, DomainErrorCodes, RefundValidationMethods, SalesPaymentStatuses, SanctionHitStatuses } from './types.js';
14
+ import { CATALOG_REVIEW_BATCH_LIMIT } from './constants.js';
15
+ import { CatalogTypes, ClauseTypes, CustomerProfitRateStatuses, DomainError, DomainErrorCodes, RefundValidationMethods, SalesPaymentStatuses, SanctionHitStatuses } from './types.js';
15
16
  import { deriveSalesLine, assertReceivedAmount } from './domain/finance.js';
16
17
  import { parseCustomerContractNo, parsePurchaseContractNo } from './domain/contract-number.js';
17
18
  import { isProductNameMatch, normalizeExactName, normalizeForProductMatch, normalizeHsCode, normalizeSanctionSource } from './domain/normalization.js';
@@ -25,11 +26,23 @@ import { parseBankAccountType, parseExchangeRateInput, parseExpectedRevision, va
25
26
  import { publicCustomerContractReanalysisDto, publicImportTaskDto } from './public-dto.js';
26
27
  import { classifyCandidate } from './catalog-import/confidence.js';
27
28
  import { catalogTerminalOutcome } from './catalog-import/outcome.js';
29
+ import { analyticsBusinessDetailFromRaw, analyticsDashboardFromRaw, analyticsDashboardStatement, analyticsDetailStatement, analyticsDetailTotalFromRaw, analyticsExportComplianceFromRaw, analyticsExportCustomersFromRaw, analyticsExportFundRisksFromRaw, analyticsExportSalesFileReviewsFromRaw, analyticsExportStatements, analyticsExportSuppliersFromRaw, resolveAnalyticsPeriod } from './analytics-query.js';
30
+ import { analyticsExportMaxRows, renderAnalyticsExportWorkbook } from './analytics-export.js';
31
+ import { formatBeijingDateTime } from './remote-ui/date-time.js';
28
32
  const DEFAULT_IMPORT_ISSUE_EXPORT_MAX_ROWS = 50_000;
29
33
  const HARD_IMPORT_ISSUE_EXPORT_MAX_ROWS = 100_000;
30
34
  const CUSTOMER_REANALYSIS_TIMEOUT_MS = 20 * 60 * 1000;
31
35
  const CONTROLLED_CATALOG_TRANSLATION_CONCURRENCY = 4;
32
36
  const CONTROLLED_CATALOG_TRANSLATION_TIMEOUT_MS = 60_000;
37
+ const ANALYTICS_EXPORT_FILE_OPERATIONS = Symbol('ANALYTICS_EXPORT_FILE_OPERATIONS');
38
+ const defaultAnalyticsExportFileOperations = {
39
+ async createWorkspace(fileName) {
40
+ const directory = await mkdtemp(join(tmpdir(), 'tcw-analytics-'));
41
+ return { directory, filePath: join(directory, fileName) };
42
+ },
43
+ readWorkspaceFile: readFile,
44
+ async removeWorkspace(directory) { await rm(directory, { recursive: true, force: true }); }
45
+ };
33
46
  const CONTROLLED_CATALOG_PROCESSING_CLAIM_LEASE_MS = 90_000;
34
47
  const CONTROLLED_CATALOG_PROCESSING_HEARTBEAT_MS = 30_000;
35
48
  const CONTROLLED_CATALOG_PROCESSING_CLAIM_INITIAL_POLL_MS = 250;
@@ -80,10 +93,11 @@ function sanctionedCompanyDto(item) {
80
93
  return dto;
81
94
  }
82
95
  let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
83
- constructor(dataSource, storage, translation) {
96
+ constructor(dataSource, storage, translation, analyticsExportFiles = defaultAnalyticsExportFileOperations) {
84
97
  this.dataSource = dataSource;
85
98
  this.storage = storage;
86
99
  this.translation = translation;
100
+ this.analyticsExportFiles = analyticsExportFiles;
87
101
  }
88
102
  async bootstrapSettings(scope) {
89
103
  const profitRepo = this.dataSource.getRepository(ProfitRuleSettings);
@@ -279,6 +293,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
279
293
  }
280
294
  async listControlledBatches(scope, query = {}) { return this.pagedQuery(ControlledCatalogBatch, scope, query, ['sourceFileName'], { catalogType: query.parameters?.catalogType }); }
281
295
  async listSanctionBatches(scope, query = {}) { return this.pagedQuery(SanctionCatalogBatch, scope, query, ['sourceFileName']); }
296
+ async listControlledBatchHistory(scope, query = {}) { return this.pagedBatchHistoryQuery(ControlledCatalogBatch, scope, query, { catalogType: query.parameters?.catalogType }); }
297
+ async listSanctionBatchHistory(scope, query = {}) { return this.pagedBatchHistoryQuery(SanctionCatalogBatch, scope, query); }
282
298
  async saveControlledGoods(scope, input) {
283
299
  const repo = this.dataSource.getRepository(ControlledGoodsRecord);
284
300
  const productName = input.productName.trim();
@@ -973,8 +989,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
973
989
  await new Promise(resolve => setTimeout(resolve, Math.min(250, remaining)));
974
990
  }
975
991
  }
976
- async nextCatalogReviewBatch(scope, taskId, requestedLimit = 20) {
977
- const limit = Math.max(1, Math.min(requestedLimit, 20));
992
+ async nextCatalogReviewBatch(scope, taskId, requestedLimit = CATALOG_REVIEW_BATCH_LIMIT) {
993
+ const limit = Math.max(1, Math.min(requestedLimit, CATALOG_REVIEW_BATCH_LIMIT));
978
994
  return this.dataSource.transaction(async (manager) => {
979
995
  const task = await required(manager.getRepository(ImportTask).findOne({
980
996
  where: { ...activeScope(scope), id: taskId }, lock: { mode: 'pessimistic_write' }
@@ -1018,7 +1034,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1018
1034
  });
1019
1035
  }
1020
1036
  async submitCatalogReviewBatch(scope, taskId, leaseToken, items) {
1021
- if (!leaseToken.trim() || items.length === 0 || items.length > 20) {
1037
+ if (!leaseToken.trim() || items.length === 0 || items.length > CATALOG_REVIEW_BATCH_LIMIT) {
1022
1038
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '复核提交参数无效');
1023
1039
  }
1024
1040
  return this.dataSource.transaction(async (manager) => {
@@ -1048,7 +1064,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1048
1064
  rejected += 1;
1049
1065
  }
1050
1066
  else {
1051
- const payload = item.payload ?? block.payload;
1067
+ const payload = item.payload ? { ...block.payload, ...item.payload } : block.payload;
1052
1068
  const candidate = reviewCandidate(task.documentType, block, payload);
1053
1069
  const decision = classifyCandidate(candidate);
1054
1070
  if (decision.confidence === 'LOW') {
@@ -3012,7 +3028,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
3012
3028
  async analyticsDetails(scope, query, source = this.dataSource) {
3013
3029
  const statement = analyticsDetailStatement(scope, query, shanghaiCurrentYear());
3014
3030
  const rows = await source.query(statement.sql, statement.parameters);
3015
- const total = numberValue(rows[0]?.total);
3031
+ const total = analyticsDetailTotalFromRaw(rows[0]?.total);
3016
3032
  return {
3017
3033
  items: rows.filter(row => row.id != null).map(analyticsBusinessDetailFromRaw),
3018
3034
  total,
@@ -3023,28 +3039,39 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
3023
3039
  }
3024
3040
  async exportAnalytics(scope, query) {
3025
3041
  const title = '数据分析驾驶舱';
3026
- const directory = await mkdtemp(join(tmpdir(), 'tcw-analytics-'));
3027
- const filePath = join(directory, `${title}.xlsx`);
3042
+ const generatedAt = new Date();
3043
+ const currentYear = shanghaiCurrentYear(generatedAt);
3044
+ const period = resolveAnalyticsPeriod(query, currentYear);
3045
+ const maxRows = analyticsExportMaxRows();
3046
+ const timestamp = formatBeijingDateTime(generatedAt).replace(/[-:]/g, '').replace(' ', '_');
3047
+ const fileName = `${title}_${timestamp}.xlsx`;
3048
+ const workspace = await this.analyticsExportFiles.createWorkspace(fileName);
3028
3049
  try {
3029
- const { dashboard } = await this.analytics(scope, query);
3030
- await writeWorkbookFile(filePath, async (workbook) => {
3031
- const summary = workbook.addWorksheet('业务汇总');
3032
- summary.columns = [{ header: '指标', width: 28 }, { header: '数值', width: 24 }];
3033
- summary.getRow(1).font = { bold: true };
3034
- summary.getRow(1).commit();
3035
- for (const [label, value] of analyticsKpiExportRows(dashboard))
3036
- summary.addRow([label, value]).commit();
3037
- const compliance = workbook.addWorksheet('合规分类');
3038
- compliance.columns = [{ header: '分类', width: 28 }, { header: '数量', width: 16 }];
3039
- compliance.getRow(1).font = { bold: true };
3040
- compliance.getRow(1).commit();
3041
- for (const item of dashboard.riskDistribution)
3042
- compliance.addRow([item.label, item.count]).commit();
3050
+ const data = await this.dataSource.transaction('REPEATABLE READ', async (manager) => {
3051
+ const dashboardStatement = analyticsDashboardStatement(scope, query, currentYear);
3052
+ const statements = analyticsExportStatements(scope, query, currentYear, maxRows);
3053
+ const dashboardRows = await manager.query(dashboardStatement.sql, dashboardStatement.parameters);
3054
+ const summary = analyticsDashboardFromRaw(dashboardRows[0]);
3055
+ const fundRiskRows = analyticsExportRowsWithinLimit('资金风险', await manager.query(statements.fundRisks.sql, statements.fundRisks.parameters), maxRows);
3056
+ const fundRisks = analyticsExportFundRisksFromRaw(fundRiskRows);
3057
+ const complianceRows = analyticsExportRowsWithinLimit('合规事项', await manager.query(statements.compliance.sql, statements.compliance.parameters), maxRows);
3058
+ const complianceItems = analyticsExportComplianceFromRaw(complianceRows);
3059
+ const customerRows = analyticsExportRowsWithinLimit('客户经营', await manager.query(statements.customers.sql, statements.customers.parameters), maxRows);
3060
+ const customers = analyticsExportCustomersFromRaw(customerRows);
3061
+ const supplierRows = analyticsExportRowsWithinLimit('供应商风险', await manager.query(statements.suppliers.sql, statements.suppliers.parameters), maxRows);
3062
+ const suppliers = analyticsExportSuppliersFromRaw(supplierRows);
3063
+ const salesFileReviewRows = analyticsExportRowsWithinLimit('文件复核', await manager.query(statements.salesFileReviews.sql, statements.salesFileReviews.parameters), maxRows);
3064
+ const salesFileReviews = analyticsExportSalesFileReviewsFromRaw(salesFileReviewRows);
3065
+ return {
3066
+ organizationId: scope.organizationId, period, generatedAt: generatedAt.toISOString(), summary,
3067
+ fundRisks, complianceItems, customers, suppliers, salesFileReviews
3068
+ };
3043
3069
  });
3044
- return { fileName: `${title}.xlsx`, buffer: await readFile(filePath) };
3070
+ await writeWorkbookFile(workspace.filePath, workbook => renderAnalyticsExportWorkbook(workbook, data));
3071
+ return { fileName, buffer: await this.analyticsExportFiles.readWorkspaceFile(workspace.filePath) };
3045
3072
  }
3046
3073
  finally {
3047
- await rm(directory, { recursive: true, force: true });
3074
+ await this.analyticsExportFiles.removeWorkspace(workspace.directory);
3048
3075
  }
3049
3076
  }
3050
3077
  async recalculateProductRisks(scope, reason, controlledBatchId) {
@@ -3157,8 +3184,12 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
3157
3184
  const pageSize = Math.min(200, Math.max(1, Number(query.pageSize ?? 20)));
3158
3185
  const qb = this.dataSource.getRepository(entity).createQueryBuilder('row')
3159
3186
  .leftJoin(batchEntity, 'batch', 'batch.id = row.batchId AND batch.tenantId = :tenantId AND batch.organizationId = :organizationId', activeScope(scope))
3160
- .where('row.tenantId = :tenantId AND row.organizationId = :organizationId', activeScope(scope))
3161
- .andWhere('row.deletedAt IS NULL');
3187
+ .where('row.tenantId = :tenantId AND row.organizationId = :organizationId', activeScope(scope));
3188
+ const catalogBatchId = String(query.parameters?.batchId ?? '').trim();
3189
+ if (catalogBatchId)
3190
+ qb.andWhere('row.batchId = :catalogBatchId', { catalogBatchId });
3191
+ else
3192
+ qb.andWhere('row.deletedAt IS NULL');
3162
3193
  for (const [key, value] of Object.entries(filters.exact ?? {}))
3163
3194
  if (hasQueryValue(value))
3164
3195
  qb.andWhere(`row.${key} = :exact_${key}`, { [`exact_${key}`]: value });
@@ -3180,6 +3211,21 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
3180
3211
  const fileNames = new Map(batches.map(batch => [batch.id, batch.sourceFileName]));
3181
3212
  return { items: items.map(item => ({ ...item, sourceFileName: item.batchId ? fileNames.get(item.batchId) : undefined })), total, page, pageSize };
3182
3213
  }
3214
+ async pagedBatchHistoryQuery(entity, scope, query, exactFilters = {}) {
3215
+ const page = Math.max(1, Number(query.page ?? 1));
3216
+ const pageSize = Math.min(200, Math.max(1, Number(query.pageSize ?? 20)));
3217
+ const qb = this.dataSource.getRepository(entity).createQueryBuilder('row')
3218
+ .where('row.tenantId = :tenantId AND row.organizationId = :organizationId', activeScope(scope));
3219
+ const search = String(query.search ?? '').trim();
3220
+ if (search)
3221
+ qb.andWhere('CAST(row.sourceFileName AS TEXT) ILIKE :search', { search: `%${search}%` });
3222
+ for (const [key, value] of Object.entries(exactFilters)) {
3223
+ if (value !== undefined && value !== null && value !== '')
3224
+ qb.andWhere(`row.${key} = :filter_${key}`, { [`filter_${key}`]: value });
3225
+ }
3226
+ const [items, total] = await qb.orderBy('row.updatedAt', 'DESC').addOrderBy('row.id', 'DESC').skip((page - 1) * pageSize).take(pageSize).getManyAndCount();
3227
+ return { items, total, page, pageSize };
3228
+ }
3183
3229
  async upsertSupplier(manager, scope, input) {
3184
3230
  const repo = manager.getRepository(Supplier);
3185
3231
  const normalizedName = normalizeExactName(input.name);
@@ -3389,9 +3435,11 @@ TradeComplianceWorkbenchService = __decorate([
3389
3435
  __param(0, InjectDataSource()),
3390
3436
  __param(1, Optional()),
3391
3437
  __param(2, Optional()),
3438
+ __param(3, Optional()),
3439
+ __param(3, Inject(ANALYTICS_EXPORT_FILE_OPERATIONS)),
3392
3440
  __metadata("design:paramtypes", [DataSource,
3393
3441
  TradeComplianceFileStorage,
3394
- TranslationProvider])
3442
+ TranslationProvider, Object])
3395
3443
  ], TradeComplianceWorkbenchService);
3396
3444
  export { TradeComplianceWorkbenchService };
3397
3445
  function activeScope(scope) { return { tenantId: scope.tenantId, organizationId: scope.organizationId }; }
@@ -3838,6 +3886,11 @@ function candidateToControlledRows(candidate) {
3838
3886
  const hsCodes = Array.isArray(candidate.values.hsCodes) && candidate.values.hsCodes.length
3839
3887
  ? candidate.values.hsCodes
3840
3888
  : [undefined];
3889
+ const translationSource = candidate.values.translationSource === 'AGENT_TRANSLATED' || candidate.values.translationSource === 'SOURCE_CHINESE'
3890
+ ? candidate.values.translationSource
3891
+ : candidate.values.translationRequired || (!candidate.values.productName && candidate.values.originalProductName)
3892
+ ? 'AGENT_TRANSLATED'
3893
+ : 'SOURCE_CHINESE';
3841
3894
  return hsCodes.map(hsCode => ({
3842
3895
  productName: optionalString(candidate.values.productName) ?? undefined,
3843
3896
  originalProductName: optionalString(candidate.values.originalProductName) ?? undefined,
@@ -3848,9 +3901,7 @@ function candidateToControlledRows(candidate) {
3848
3901
  sourceOrdinal: optionalString(candidate.values.sourceOrdinal) ?? String(candidate.ordinal),
3849
3902
  sourceLocation: evidence?.sourceLocation,
3850
3903
  sourceContent: evidence?.sourceContent,
3851
- translationSource: candidate.values.translationRequired || (!candidate.values.productName && candidate.values.originalProductName)
3852
- ? 'AGENT_TRANSLATED'
3853
- : 'SOURCE_CHINESE'
3904
+ translationSource
3854
3905
  }));
3855
3906
  }
3856
3907
  function candidateAliases(candidate) {
@@ -3907,439 +3958,19 @@ function fuzzyMatches(value, filter) { return !filter || String(value ?? '').tri
3907
3958
  function shanghaiCurrentYear(now = new Date()) {
3908
3959
  return Number(new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Shanghai', year: 'numeric' }).format(now));
3909
3960
  }
3910
- function currentYearDateRange(currentYear, requested) {
3911
- const yearFrom = `${currentYear}-01-01`;
3912
- const yearTo = `${currentYear}-12-31`;
3913
- return { from: requested?.from ?? yearFrom, to: requested?.to ?? yearTo };
3914
- }
3915
- function analyticsDashboardStatement(scope, query, currentYear) {
3916
- const parameters = [];
3917
- const parameter = (value) => { parameters.push(value); return `$${parameters.length}`; };
3918
- const tenant = parameter(scope.tenantId);
3919
- const organization = parameter(scope.organizationId);
3920
- const range = currentYearDateRange(currentYear, query.filters.orderDate);
3921
- const from = parameter(range.from);
3922
- const to = parameter(range.to);
3923
- return { sql: `SELECT ${analyticsDashboardJsonSql(tenant, organization, from, to)} AS "dashboardJson"`, parameters };
3924
- }
3925
- function analyticsDashboardJsonSql(tenant, organization, from, to) {
3926
- const complianceDetails = analyticsComplianceDetailSql(tenant, organization, from, to);
3927
- const categoryCount = (key) => `(SELECT COUNT(*) FROM compliance_details WHERE "categoryKey" = '${key}')`;
3928
- const categoryAmount = (key) => `(SELECT COALESCE(SUM("amountCny"), 0) FROM compliance_details WHERE "categoryKey" = '${key}')`;
3929
- const customerContractReviewCount = categoryCount('customer-contract-review');
3930
- const purchaseContractReviewCount = categoryCount('purchase-contract-review');
3931
- const productRiskCount = categoryCount('product-risk-review');
3932
- const sanctionHitCount = categoryCount('sanction-review');
3933
- const salesFileReviewCount = categoryCount('sales-file-review');
3934
- const complianceTodoCount = `(${customerContractReviewCount} + ${purchaseContractReviewCount} + ${productRiskCount} + ${sanctionHitCount} + ${salesFileReviewCount})`;
3935
- const monthlyTrend = `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
3936
- 'month', monthly.month_no,
3937
- 'purchaseAmountCny', monthly."purchaseAmountCny",
3938
- 'salesAmountCny', monthly."salesAmountCny",
3939
- 'estimatedGrossProfitCny', GREATEST(monthly."salesAmountCny" - monthly."purchaseAmountCny", 0),
3940
- 'receivedCny', monthly."receivedCny"
3941
- ) ORDER BY monthly.month_no), '[]'::jsonb)
3942
- FROM (
3943
- SELECT month_no,
3944
- COALESCE(purchase_month."purchaseAmountCny", 0) AS "purchaseAmountCny",
3945
- COALESCE(sales_month."salesAmountCny", 0) AS "salesAmountCny",
3946
- COALESCE(sales_month."receivedCny", 0) AS "receivedCny"
3947
- FROM generate_series(1, 12) AS month_no
3948
- LEFT JOIN (
3949
- SELECT EXTRACT(MONTH FROM purchase."orderDate")::int AS month_no, SUM(purchase."contractAmountCny") AS "purchaseAmountCny"
3950
- FROM plugin_trade_compliance_purchase_order purchase
3951
- WHERE purchase."tenantId" = ${tenant} AND purchase."organizationId" = ${organization} AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
3952
- GROUP BY EXTRACT(MONTH FROM purchase."orderDate")::int
3953
- ) purchase_month USING (month_no)
3954
- LEFT JOIN (
3955
- SELECT EXTRACT(MONTH FROM purchase."orderDate")::int AS month_no,
3956
- SUM(ROUND(ROUND(line."unitPriceTaxIncluded" * (1 + COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")), 0) * line.quantity, 0)) AS "salesAmountCny",
3957
- SUM(COALESCE(manual."receivedAmountCny", 0)) AS "receivedCny"
3958
- FROM plugin_trade_compliance_customer_contract contract
3959
- INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
3960
- INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
3961
- LEFT JOIN plugin_trade_compliance_derived_sales_manual manual ON manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL
3962
- INNER JOIN plugin_trade_compliance_purchase_order_line line ON line."purchaseOrderId" = purchase.id AND line."tenantId" = purchase."tenantId" AND line."organizationId" = purchase."organizationId" AND line."deletedAt" IS NULL
3963
- WHERE contract."tenantId" = ${tenant} AND contract."organizationId" = ${organization} AND contract."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
3964
- AND (manual."pricingProfitRateOverride" IS NOT NULL OR (customer."pricingProfitRateStatus" = 'CONFIGURED' AND customer."defaultPricingProfitRate" IS NOT NULL))
3965
- GROUP BY EXTRACT(MONTH FROM purchase."orderDate")::int
3966
- ) sales_month USING (month_no)
3967
- ) monthly)`;
3968
- return `(WITH compliance_details AS (${complianceDetails}), purchase_scope AS (
3969
- SELECT purchase.id, purchase."supplierId", supplier.name AS "supplierName", purchase."contractNo", purchase."orderDate",
3970
- purchase."contractAmountCny" AS "purchaseTotalCny", purchase."paidAmountCny" AS "paidCny",
3971
- GREATEST(purchase."contractAmountCny" - purchase."paidAmountCny", 0) AS "unpaidCny",
3972
- purchase."refundedAmountCny" AS "refundedCny",
3973
- GREATEST((SELECT COALESCE(SUM(refund_line."lineAmountTaxIncluded" / (1 + COALESCE(refund_line."vatRate", 0)) * COALESCE(refund_line."refundRate", 0)), 0) FROM plugin_trade_compliance_purchase_order_line refund_line WHERE refund_line."purchaseOrderId" = purchase.id AND refund_line."tenantId" = purchase."tenantId" AND refund_line."organizationId" = purchase."organizationId" AND refund_line."deletedAt" IS NULL) - purchase."refundedAmountCny", 0) AS "unrefundedCny"
3974
- FROM plugin_trade_compliance_purchase_order purchase
3975
- INNER JOIN plugin_trade_compliance_supplier supplier ON supplier.id = purchase."supplierId" AND supplier."tenantId" = purchase."tenantId" AND supplier."organizationId" = purchase."organizationId" AND supplier."deletedAt" IS NULL
3976
- WHERE purchase."tenantId" = ${tenant} AND purchase."organizationId" = ${organization} AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
3977
- ), sales_scope AS (
3978
- SELECT raw.*, GREATEST(raw."salesTotalCny" - raw."receivedCny", 0) AS "unreceivedCny", GREATEST(raw."salesTotalCny" - raw."purchaseTotalCny", 0) AS "estimatedGrossProfitCny"
3979
- FROM (
3980
- SELECT contract.id AS "customerContractId", purchase.id AS "purchaseOrderId", customer.id AS "customerId", customer.name AS "customerName",
3981
- COALESCE((SELECT sales_file."salesOrderNoSnapshot" FROM plugin_trade_compliance_generated_sales_file sales_file WHERE sales_file."customerContractId" = contract.id AND sales_file."purchaseOrderId" = purchase.id AND sales_file."tenantId" = contract."tenantId" AND sales_file."organizationId" = contract."organizationId" AND sales_file."deletedAt" IS NULL ORDER BY sales_file."generatedAt" DESC LIMIT 1), contract."contractNo") AS "salesOrderNo", purchase."orderDate",
3982
- purchase."contractAmountCny" AS "purchaseTotalCny",
3983
- (SELECT COALESCE(SUM(ROUND(ROUND(sales_line."unitPriceTaxIncluded" * (1 + COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")), 0) * sales_line.quantity, 0)), 0) FROM plugin_trade_compliance_purchase_order_line sales_line WHERE sales_line."purchaseOrderId" = purchase.id AND sales_line."tenantId" = purchase."tenantId" AND sales_line."organizationId" = purchase."organizationId" AND sales_line."deletedAt" IS NULL) AS "salesTotalCny",
3984
- COALESCE(manual."receivedAmountCny", 0) AS "receivedCny"
3985
- FROM plugin_trade_compliance_customer_contract contract
3986
- INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
3987
- INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
3988
- LEFT JOIN plugin_trade_compliance_derived_sales_manual manual ON manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL
3989
- WHERE contract."tenantId" = ${tenant} AND contract."organizationId" = ${organization} AND contract."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
3990
- AND (manual."pricingProfitRateOverride" IS NOT NULL OR (customer."pricingProfitRateStatus" = 'CONFIGURED' AND customer."defaultPricingProfitRate" IS NOT NULL))
3991
- ) raw
3992
- ), global_purchase AS (
3993
- SELECT COALESCE(SUM("purchaseTotalCny"), 0) AS "purchaseTotalCny", COALESCE(SUM("paidCny"), 0) AS "paidCny", COALESCE(SUM("unpaidCny"), 0) AS "unpaidCny", COALESCE(SUM("refundedCny"), 0) AS "refundedCny", COALESCE(SUM("unrefundedCny"), 0) AS "unrefundedCny"
3994
- FROM purchase_scope
3995
- ), global_sales AS (
3996
- SELECT COALESCE(SUM("salesTotalCny"), 0) AS "salesTotalCny", COALESCE(SUM("receivedCny"), 0) AS "receivedCny", COALESCE(SUM("unreceivedCny"), 0) AS "unreceivedCny", COALESCE(SUM("estimatedGrossProfitCny"), 0) AS "estimatedGrossProfitCny"
3997
- FROM sales_scope
3998
- ), customer_rollup AS (
3999
- SELECT "customerId", MIN("customerName") AS "customerName", SUM("salesTotalCny") AS "salesAmountCny", SUM("estimatedGrossProfitCny") AS "estimatedGrossProfitCny", SUM("unreceivedCny") AS "unreceivedCny", COUNT(*)::bigint AS "orderCount", MAX("orderDate") AS "latestOrderDate"
4000
- FROM sales_scope GROUP BY "customerId"
4001
- ), customer_value_top AS (
4002
- SELECT * FROM customer_rollup ORDER BY "salesAmountCny" DESC LIMIT 5
4003
- ), receivable_top AS (
4004
- SELECT * FROM customer_rollup WHERE "unreceivedCny" > 0 ORDER BY "unreceivedCny" DESC LIMIT 5
4005
- ), supplier_top AS (
4006
- SELECT purchase_scope."supplierId", MIN(purchase_scope."supplierName") AS "supplierName", SUM(purchase_scope."unpaidCny") AS "unpaidCny", COUNT(DISTINCT purchase_scope.id)::bigint AS "purchaseOrderCount",
4007
- (SELECT COUNT(DISTINCT product_match.id) FROM plugin_trade_compliance_purchase_order risk_purchase
4008
- INNER JOIN plugin_trade_compliance_purchase_order_line risk_line ON risk_line."purchaseOrderId" = risk_purchase.id AND risk_line."tenantId" = risk_purchase."tenantId" AND risk_line."organizationId" = risk_purchase."organizationId" AND risk_line."deletedAt" IS NULL
4009
- INNER JOIN plugin_trade_compliance_product_match product_match ON product_match."purchaseOrderLineId" = risk_line.id AND product_match."tenantId" = risk_line."tenantId" AND product_match."organizationId" = risk_line."organizationId" AND product_match."deletedAt" IS NULL
4010
- INNER JOIN plugin_trade_compliance_controlled_goods goods ON goods.id = product_match."controlledGoodsId" AND goods."tenantId" = product_match."tenantId" AND goods."organizationId" = product_match."organizationId" AND goods."deletedAt" IS NULL
4011
- WHERE risk_purchase."tenantId" = ${tenant} AND risk_purchase."organizationId" = ${organization} AND risk_purchase."deletedAt" IS NULL AND risk_purchase."supplierId" = purchase_scope."supplierId" AND risk_purchase."orderDate" BETWEEN ${from} AND ${to})::bigint AS "productRiskHitCount",
4012
- (SELECT COUNT(DISTINCT sanction_match.id) FROM plugin_trade_compliance_company_sanction_match sanction_match
4013
- INNER JOIN plugin_trade_compliance_sanctioned_company company ON company.id = sanction_match."sanctionedCompanyId" AND company."tenantId" = sanction_match."tenantId" AND company."organizationId" = sanction_match."organizationId" AND company."deletedAt" IS NULL
4014
- WHERE sanction_match."tenantId" = ${tenant} AND sanction_match."organizationId" = ${organization} AND sanction_match."deletedAt" IS NULL AND sanction_match."subjectType" = 'SUPPLIER' AND sanction_match."supplierId" = purchase_scope."supplierId")::bigint AS "sanctionHitCount"
4015
- FROM purchase_scope
4016
- GROUP BY purchase_scope."supplierId" ORDER BY SUM(purchase_scope."unpaidCny") DESC LIMIT 5
4017
- )
4018
- SELECT jsonb_build_object(
4019
- 'kpis', jsonb_build_object(
4020
- 'purchaseTotalCny', (SELECT "purchaseTotalCny" FROM global_purchase), 'salesTotalCny', (SELECT "salesTotalCny" FROM global_sales),
4021
- 'receivedCny', (SELECT "receivedCny" FROM global_sales), 'unreceivedCny', (SELECT "unreceivedCny" FROM global_sales), 'paidCny', (SELECT "paidCny" FROM global_purchase), 'unpaidCny', (SELECT "unpaidCny" FROM global_purchase),
4022
- 'refundedCny', (SELECT "refundedCny" FROM global_purchase), 'unrefundedCny', (SELECT "unrefundedCny" FROM global_purchase),
4023
- 'estimatedGrossProfitCny', (SELECT "estimatedGrossProfitCny" FROM global_sales),
4024
- 'estimatedGrossProfitRate', CASE WHEN (SELECT "salesTotalCny" FROM global_sales) > 0 THEN (SELECT "estimatedGrossProfitCny" FROM global_sales) / (SELECT "salesTotalCny" FROM global_sales) ELSE 0 END,
4025
- 'complianceTodoCount', ${complianceTodoCount},
4026
- 'riskExposureCny', (SELECT COALESCE(SUM("amountCny"), 0) FROM compliance_details)
4027
- ),
4028
- 'monthlyTrend', ${monthlyTrend},
4029
- 'todos', jsonb_build_array(
4030
- jsonb_build_object('key', 'customer-contract-review', 'label', '客户合同待审核', 'count', ${customerContractReviewCount}, 'amountCny', ${categoryAmount('customer-contract-review')}, 'priority', 'high', 'description', 'AI 解析完成但未人工确认的客户合同'),
4031
- jsonb_build_object('key', 'purchase-contract-review', 'label', '采购合同待审核', 'count', ${purchaseContractReviewCount}, 'amountCny', ${categoryAmount('purchase-contract-review')}, 'priority', 'high', 'description', '上传完成但未审核入库的采购合同'),
4032
- jsonb_build_object('key', 'product-risk-review', 'label', '商品风险待确认', 'count', ${productRiskCount}, 'amountCny', ${categoryAmount('product-risk-review')}, 'priority', 'medium', 'description', '命中管控商品目录的商品明细'),
4033
- jsonb_build_object('key', 'sanction-review', 'label', '制裁公司待复核', 'count', ${sanctionHitCount}, 'amountCny', ${categoryAmount('sanction-review')}, 'priority', 'high', 'description', '客户或供应商命中公司制裁目录'),
4034
- jsonb_build_object('key', 'sales-file-review', 'label', '销售文件待复核', 'count', ${salesFileReviewCount}, 'amountCny', ${categoryAmount('sales-file-review')}, 'priority', 'medium', 'description', '生成时记录了真实业务异常的销售文件')
4035
- ),
4036
- 'riskDistribution', jsonb_build_array(
4037
- jsonb_build_object('key', 'customer-contract-review', 'label', '客户合同待审核', 'count', ${customerContractReviewCount}, 'amountCny', ${categoryAmount('customer-contract-review')}),
4038
- jsonb_build_object('key', 'purchase-contract-review', 'label', '采购合同待审核', 'count', ${purchaseContractReviewCount}, 'amountCny', ${categoryAmount('purchase-contract-review')}),
4039
- jsonb_build_object('key', 'product-risk-review', 'label', '商品风险待确认', 'count', ${productRiskCount}, 'amountCny', ${categoryAmount('product-risk-review')}),
4040
- jsonb_build_object('key', 'sanction-review', 'label', '制裁公司待复核', 'count', ${sanctionHitCount}, 'amountCny', ${categoryAmount('sanction-review')}),
4041
- jsonb_build_object('key', 'sales-file-review', 'label', '销售文件待复核', 'count', ${salesFileReviewCount}, 'amountCny', ${categoryAmount('sales-file-review')})
4042
- ),
4043
- 'customerValueTop5', (SELECT COALESCE(jsonb_agg(jsonb_build_object('customerId', customer_value_top."customerId", 'customerName', customer_value_top."customerName", 'salesAmountCny', customer_value_top."salesAmountCny", 'estimatedGrossProfitCny', customer_value_top."estimatedGrossProfitCny", 'estimatedGrossProfitRate', CASE WHEN customer_value_top."salesAmountCny" > 0 THEN customer_value_top."estimatedGrossProfitCny" / customer_value_top."salesAmountCny" ELSE 0 END, 'unreceivedCny', customer_value_top."unreceivedCny", 'riskStatus', CASE WHEN customer_value_top."unreceivedCny" >= 100000 THEN 'HIGH' WHEN customer_value_top."unreceivedCny" > 0 THEN 'MEDIUM' ELSE 'LOW' END) ORDER BY customer_value_top."salesAmountCny" DESC), '[]'::jsonb) FROM customer_value_top),
4044
- 'receivableRiskTop5', (SELECT COALESCE(jsonb_agg(jsonb_build_object('customerId', receivable_top."customerId", 'customerName', receivable_top."customerName", 'unreceivedCny', receivable_top."unreceivedCny", 'orderCount', receivable_top."orderCount", 'latestOrderDate', receivable_top."latestOrderDate") ORDER BY receivable_top."unreceivedCny" DESC), '[]'::jsonb) FROM receivable_top),
4045
- 'supplierRiskTop5', (SELECT COALESCE(jsonb_agg(jsonb_build_object('supplierId', supplier_top."supplierId", 'supplierName', supplier_top."supplierName", 'unpaidCny', supplier_top."unpaidCny", 'sanctionHitCount', supplier_top."sanctionHitCount", 'productRiskHitCount', supplier_top."productRiskHitCount", 'purchaseOrderCount', supplier_top."purchaseOrderCount") ORDER BY supplier_top."unpaidCny" DESC), '[]'::jsonb) FROM supplier_top WHERE supplier_top."unpaidCny" > 0 OR supplier_top."sanctionHitCount" > 0 OR supplier_top."productRiskHitCount" > 0),
4046
- 'salesFileReviews', (SELECT COALESCE(jsonb_agg(jsonb_build_object('fileId', file.id, 'fileName', file."fileName", 'fileType', file."fileType", 'salesOrderNo', file."salesOrderNoSnapshot", 'reason', file.reason, 'generatedAt', file."generatedAt") ORDER BY file."generatedAt" DESC), '[]'::jsonb) FROM (SELECT source.*, (SELECT COALESCE(NULLIF(warning->>'message', ''), NULLIF(warning->>'reason', ''), NULLIF(warning->>'code', '')) FROM jsonb_array_elements(source."generationWarnings") warning WHERE COALESCE(NULLIF(warning->>'message', ''), NULLIF(warning->>'reason', ''), NULLIF(warning->>'code', '')) IS NOT NULL LIMIT 1) AS reason FROM plugin_trade_compliance_generated_sales_file source WHERE source."tenantId" = ${tenant} AND source."organizationId" = ${organization} AND source."deletedAt" IS NULL AND source."reviewedAt" IS NULL AND source."generatedAt"::date BETWEEN ${from} AND ${to} AND jsonb_typeof(source."generationWarnings") = 'array' AND jsonb_array_length(source."generationWarnings") > 0 AND EXISTS (SELECT 1 FROM jsonb_array_elements(source."generationWarnings") warning WHERE COALESCE(NULLIF(warning->>'message', ''), NULLIF(warning->>'reason', ''), NULLIF(warning->>'code', '')) IS NOT NULL) ORDER BY source."generatedAt" DESC LIMIT 5) file),
4047
- 'riskOrders', (SELECT COALESCE(jsonb_agg(risk_item.item), '[]'::jsonb) FROM (SELECT jsonb_build_object('orderKey', 'sales:' || sales_scope."purchaseOrderId"::text, 'orderNo', sales_scope."salesOrderNo", 'partyName', sales_scope."customerName", 'riskType', '未收款风险', 'riskAmountCny', sales_scope."unreceivedCny", 'status', '待跟进') AS item, sales_scope."unreceivedCny" AS amount FROM sales_scope WHERE sales_scope."unreceivedCny" > 0 UNION ALL SELECT jsonb_build_object('orderKey', 'purchase:' || purchase_scope.id::text, 'orderNo', purchase_scope."contractNo", 'partyName', purchase_scope."supplierName", 'riskType', '未付款风险', 'riskAmountCny', purchase_scope."unpaidCny", 'status', '待跟进') AS item, purchase_scope."unpaidCny" AS amount FROM purchase_scope WHERE purchase_scope."unpaidCny" > 0 ORDER BY amount DESC LIMIT 5) risk_item),
4048
- 'dataSources', jsonb_build_array(
4049
- jsonb_build_object('label', '采购订单', 'description', '采购金额、付款记录、退税记录'),
4050
- jsonb_build_object('label', '采购商品明细', 'description', '采购商品金额、商品风险命中'),
4051
- jsonb_build_object('label', '客户合同审核结果', 'description', '客户合同风险、待审核合同'),
4052
- jsonb_build_object('label', '公司制裁目录匹配结果', 'description', '客户/供应商制裁命中'),
4053
- jsonb_build_object('label', '付款记录', 'description', '已付款、未付款金额'),
4054
- jsonb_build_object('label', '销售订单', 'description', '销售金额、收款记录、客户合同关联'),
4055
- jsonb_build_object('label', '销售商品明细', 'description', '销售商品金额、管控商品命中'),
4056
- jsonb_build_object('label', '管控商品目录匹配结果', 'description', '商品风险命中与处理状态'),
4057
- jsonb_build_object('label', '收款记录', 'description', '已收款、未收款金额'),
4058
- jsonb_build_object('label', '退税记录', 'description', '已退税、未退税金额'),
4059
- jsonb_build_object('label', '销售文件生成与审核记录', 'description', '销售单据生成、复核状态与异常原因'),
4060
- jsonb_build_object('label', '系统设置中的币种与汇率规则', 'description', '币种、月度汇率与金额折算规则')
4061
- )
4062
- ))`;
4063
- }
4064
- function analyticsDetailStatement(scope, query, currentYear) {
4065
- const parameters = [];
4066
- const parameter = (value) => { parameters.push(value); return `$${parameters.length}`; };
4067
- const tenant = parameter(scope.tenantId);
4068
- const organization = parameter(scope.organizationId);
4069
- const range = currentYearDateRange(currentYear, query.filters.orderDate);
4070
- const from = parameter(range.from);
4071
- const to = parameter(range.to);
4072
- const source = query.kind === 'COMPLIANCE_TODO' || query.kind === 'COMPLIANCE_RISK'
4073
- ? analyticsComplianceDetailSql(tenant, organization, from, to)
4074
- : query.kind === 'SALES_FILE_REVIEW'
4075
- ? analyticsSalesFileReviewDetailSql(tenant, organization, from, to)
4076
- : analyticsRiskOrderDetailSql(tenant, organization, from, to);
4077
- const category = query.categoryKey ? `WHERE details."categoryKey" = ${parameter(query.categoryKey)}` : '';
4078
- const offset = parameter((query.page - 1) * query.pageSize);
4079
- const limit = parameter(query.pageSize);
4080
- return {
4081
- sql: `WITH details AS (${source}), filtered AS (SELECT * FROM details ${category}), counted AS (SELECT COUNT(*) AS total FROM filtered), paged AS (SELECT * FROM filtered ORDER BY "businessDate" DESC NULLS LAST, id DESC OFFSET ${offset} LIMIT ${limit}) SELECT paged.*, counted.total FROM counted LEFT JOIN paged ON TRUE ORDER BY paged."businessDate" DESC NULLS LAST, paged.id DESC`,
4082
- parameters
4083
- };
4084
- }
4085
- function analyticsComplianceDetailSql(tenant, organization, from, to) {
4086
- const salesFileRows = analyticsSalesFileReviewDetailSql(tenant, organization, from, to);
4087
- return `
4088
- SELECT task.id::text AS id, 'customer-contract-review' AS "categoryKey", '客户合同待审核' AS "categoryLabel",
4089
- task."sourceFileName" AS "referenceNo", task."sourceFileName" AS "subjectName", task."createdAt"::text AS "businessDate",
4090
- NULL::text AS reason, NULL::numeric AS "amountCny", '待审核' AS status,
4091
- jsonb_build_object('kind', 'CUSTOMER_CONTRACT_REVIEW', 'label', '审核', 'id', task.id, 'importTaskId', task.id) AS action
4092
- FROM plugin_trade_compliance_import_task task
4093
- WHERE task."tenantId" = ${tenant} AND task."organizationId" = ${organization} AND task."deletedAt" IS NULL AND task."documentType" = 'CUSTOMER_CONTRACT' AND task.status = 'AWAITING_REVIEW' AND task."createdAt"::date BETWEEN ${from} AND ${to}
4094
- UNION ALL
4095
- SELECT task.id::text, 'purchase-contract-review', '采购合同待审核', task."sourceFileName", task."sourceFileName", task."createdAt"::text,
4096
- NULL::text, NULL::numeric, '待审核', jsonb_build_object('kind', 'PURCHASE_CONTRACT_REVIEW', 'label', '审核', 'id', task.id, 'importTaskId', task.id)
4097
- FROM plugin_trade_compliance_import_task task
4098
- WHERE task."tenantId" = ${tenant} AND task."organizationId" = ${organization} AND task."deletedAt" IS NULL AND task."documentType" = 'PURCHASE_CONTRACT' AND task.status = 'AWAITING_REVIEW' AND task."createdAt"::date BETWEEN ${from} AND ${to}
4099
- UNION ALL
4100
- SELECT match.id::text, 'product-risk-review', '商品风险待确认', purchase."contractNo", line."productName", purchase."orderDate"::text,
4101
- COALESCE(goods."controlDescription", goods."productName"), line."lineAmountTaxIncluded", '待确认',
4102
- jsonb_build_object('kind', 'PURCHASE_ORDER_VIEW', 'label', '查看', 'id', match.id, 'purchaseOrderId', purchase.id, 'supplierId', purchase."supplierId")
4103
- FROM plugin_trade_compliance_product_match match
4104
- INNER JOIN plugin_trade_compliance_purchase_order_line line ON line.id = match."purchaseOrderLineId" AND line."tenantId" = match."tenantId" AND line."organizationId" = match."organizationId" AND line."deletedAt" IS NULL
4105
- INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase.id = line."purchaseOrderId" AND purchase."tenantId" = match."tenantId" AND purchase."organizationId" = match."organizationId" AND purchase."deletedAt" IS NULL
4106
- INNER JOIN plugin_trade_compliance_controlled_goods goods ON goods.id = match."controlledGoodsId" AND goods."tenantId" = match."tenantId" AND goods."organizationId" = match."organizationId" AND goods."deletedAt" IS NULL
4107
- WHERE match."tenantId" = ${tenant} AND match."organizationId" = ${organization} AND match."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
4108
- UNION ALL
4109
- SELECT ('supplier:' || match.id::text || ':' || purchase.id::text), 'sanction-review', '制裁公司待复核', purchase."contractNo", supplier.name, purchase."orderDate"::text,
4110
- company."sanctionReason", purchase."contractAmountCny", '待复核', jsonb_build_object('kind', 'PURCHASE_ORDER_VIEW', 'label', '查看', 'id', match.id, 'purchaseOrderId', purchase.id, 'supplierId', supplier.id)
4111
- FROM plugin_trade_compliance_company_sanction_match match
4112
- INNER JOIN plugin_trade_compliance_supplier supplier ON supplier.id = match."supplierId" AND supplier."tenantId" = match."tenantId" AND supplier."organizationId" = match."organizationId" AND supplier."deletedAt" IS NULL
4113
- INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."supplierId" = supplier.id AND purchase."tenantId" = match."tenantId" AND purchase."organizationId" = match."organizationId" AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
4114
- INNER JOIN plugin_trade_compliance_sanctioned_company company ON company.id = match."sanctionedCompanyId" AND company."tenantId" = match."tenantId" AND company."organizationId" = match."organizationId" AND company."deletedAt" IS NULL
4115
- WHERE match."tenantId" = ${tenant} AND match."organizationId" = ${organization} AND match."deletedAt" IS NULL AND match."subjectType" = 'SUPPLIER'
4116
- UNION ALL
4117
- SELECT ('customer:' || match.id::text || ':' || purchase.id::text), 'sanction-review', '制裁公司待复核', COALESCE(file."salesOrderNoSnapshot", contract."contractNo"), customer.name, purchase."orderDate"::text,
4118
- company."sanctionReason", contract."contractAmountReviewCny", '待复核', jsonb_build_object('kind', 'SALES_ORDER_VIEW', 'label', '查看', 'id', match.id, 'customerId', customer.id, 'customerContractId', contract.id, 'purchaseOrderId', purchase.id)
4119
- FROM plugin_trade_compliance_company_sanction_match match
4120
- INNER JOIN plugin_trade_compliance_customer customer ON customer.id = match."customerId" AND customer."tenantId" = match."tenantId" AND customer."organizationId" = match."organizationId" AND customer."deletedAt" IS NULL
4121
- INNER JOIN plugin_trade_compliance_customer_contract contract ON contract."customerId" = customer.id AND contract."tenantId" = match."tenantId" AND contract."organizationId" = match."organizationId" AND contract."deletedAt" IS NULL
4122
- INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X' AND purchase."orderDate" BETWEEN ${from} AND ${to}
4123
- INNER JOIN plugin_trade_compliance_sanctioned_company company ON company.id = match."sanctionedCompanyId" AND company."tenantId" = match."tenantId" AND company."organizationId" = match."organizationId" AND company."deletedAt" IS NULL
4124
- LEFT JOIN LATERAL (SELECT generated."salesOrderNoSnapshot" FROM plugin_trade_compliance_generated_sales_file generated WHERE generated."customerContractId" = contract.id AND generated."purchaseOrderId" = purchase.id AND generated."tenantId" = contract."tenantId" AND generated."organizationId" = contract."organizationId" AND generated."deletedAt" IS NULL ORDER BY generated."generatedAt" DESC LIMIT 1) file ON TRUE
4125
- WHERE match."tenantId" = ${tenant} AND match."organizationId" = ${organization} AND match."deletedAt" IS NULL AND match."subjectType" = 'CUSTOMER'
4126
- UNION ALL ${salesFileRows}`;
4127
- }
4128
- function analyticsSalesFileReviewDetailSql(tenant, organization, from, to) {
4129
- return `SELECT file.id::text AS id, 'sales-file-review' AS "categoryKey", '销售文件待复核' AS "categoryLabel", file."salesOrderNoSnapshot" AS "referenceNo",
4130
- file."fileName" AS "subjectName", file."generatedAt"::text AS "businessDate",
4131
- (SELECT COALESCE(NULLIF(warning->>'message', ''), NULLIF(warning->>'reason', ''), NULLIF(warning->>'code', '')) FROM jsonb_array_elements(file."generationWarnings") warning WHERE COALESCE(NULLIF(warning->>'message', ''), NULLIF(warning->>'reason', ''), NULLIF(warning->>'code', '')) IS NOT NULL LIMIT 1) AS reason,
4132
- NULL::numeric AS "amountCny", '待复核' AS status,
4133
- jsonb_build_object('kind', 'SALES_FILE_REVIEW', 'label', '复核', 'id', file.id, 'customerContractId', file."customerContractId", 'purchaseOrderId', file."purchaseOrderId", 'salesOrderNoSnapshot', file."salesOrderNoSnapshot", 'fileType', file."fileType", 'fileName', file."fileName", 'storageFileId', file."storageFileId", 'editVersion', file."editVersion", 'generatedAt', file."generatedAt") AS action
4134
- FROM plugin_trade_compliance_generated_sales_file file
4135
- WHERE file."tenantId" = ${tenant} AND file."organizationId" = ${organization} AND file."deletedAt" IS NULL AND file."reviewedAt" IS NULL AND file."generatedAt"::date BETWEEN ${from} AND ${to}
4136
- AND jsonb_typeof(file."generationWarnings") = 'array' AND jsonb_array_length(file."generationWarnings") > 0
4137
- AND EXISTS (SELECT 1 FROM jsonb_array_elements(file."generationWarnings") warning WHERE COALESCE(NULLIF(warning->>'message', ''), NULLIF(warning->>'reason', ''), NULLIF(warning->>'code', '')) IS NOT NULL)`;
4138
- }
4139
- function analyticsRiskOrderDetailSql(tenant, organization, from, to) {
4140
- return `SELECT ('purchase:' || purchase.id::text) AS id, 'risk-order' AS "categoryKey", '未付款订单' AS "categoryLabel", purchase."contractNo" AS "referenceNo", supplier.name AS "subjectName", purchase."orderDate"::text AS "businessDate", '未付款' AS reason,
4141
- GREATEST(purchase."contractAmountCny" - purchase."paidAmountCny", 0) AS "amountCny", '待跟进' AS status,
4142
- jsonb_build_object('kind', 'PURCHASE_ORDER_VIEW', 'label', '跟进', 'id', purchase.id, 'purchaseOrderId', purchase.id, 'supplierId', supplier.id) AS action
4143
- FROM plugin_trade_compliance_purchase_order purchase
4144
- INNER JOIN plugin_trade_compliance_supplier supplier ON supplier.id = purchase."supplierId" AND supplier."tenantId" = purchase."tenantId" AND supplier."organizationId" = purchase."organizationId" AND supplier."deletedAt" IS NULL
4145
- WHERE purchase."tenantId" = ${tenant} AND purchase."organizationId" = ${organization} AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to} AND GREATEST(purchase."contractAmountCny" - purchase."paidAmountCny", 0) > 0
4146
- UNION ALL
4147
- SELECT ('sales:' || contract.id::text || ':' || purchase.id::text), 'risk-order', '未收款订单', COALESCE(file."salesOrderNoSnapshot", contract."contractNo"), customer.name, purchase."orderDate"::text, '未收款',
4148
- GREATEST(sales_amount."salesTotalCny" - COALESCE(manual."receivedAmountCny", 0), 0) AS "amountCny", '待跟进',
4149
- jsonb_build_object('kind', 'SALES_ORDER_VIEW', 'label', '跟进', 'id', contract.id::text || ':' || purchase.id::text, 'customerId', customer.id, 'customerContractId', contract.id, 'purchaseOrderId', purchase.id)
4150
- FROM plugin_trade_compliance_customer_contract contract
4151
- INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
4152
- INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
4153
- LEFT JOIN plugin_trade_compliance_derived_sales_manual manual ON manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL
4154
- CROSS JOIN LATERAL (SELECT COALESCE(SUM(ROUND(ROUND(line."unitPriceTaxIncluded" * (1 + COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")), 0) * line.quantity, 0)), 0) AS "salesTotalCny" FROM plugin_trade_compliance_purchase_order_line line WHERE line."purchaseOrderId" = purchase.id AND line."tenantId" = purchase."tenantId" AND line."organizationId" = purchase."organizationId" AND line."deletedAt" IS NULL) sales_amount
4155
- LEFT JOIN LATERAL (SELECT generated."salesOrderNoSnapshot" FROM plugin_trade_compliance_generated_sales_file generated WHERE generated."customerContractId" = contract.id AND generated."purchaseOrderId" = purchase.id AND generated."tenantId" = contract."tenantId" AND generated."organizationId" = contract."organizationId" AND generated."deletedAt" IS NULL ORDER BY generated."generatedAt" DESC LIMIT 1) file ON TRUE
4156
- WHERE contract."tenantId" = ${tenant} AND contract."organizationId" = ${organization} AND contract."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
4157
- AND (manual."pricingProfitRateOverride" IS NOT NULL OR (customer."pricingProfitRateStatus" = 'CONFIGURED' AND customer."defaultPricingProfitRate" IS NOT NULL))
4158
- AND GREATEST(sales_amount."salesTotalCny" - COALESCE(manual."receivedAmountCny", 0), 0) > 0`;
4159
- }
4160
- function analyticsBusinessDetailFromRaw(row) {
4161
- const action = typeof row.action === 'string' ? safeJsonParse(row.action) : row.action;
4162
- return {
4163
- id: stringValue(row.id), categoryKey: analyticsBusinessCategoryKeyValue(row.categoryKey), categoryLabel: stringValue(row.categoryLabel), referenceNo: stringValue(row.referenceNo),
4164
- ...(row.subjectName == null ? {} : { subjectName: stringValue(row.subjectName) }),
4165
- ...(row.businessDate == null ? {} : { businessDate: stringValue(row.businessDate) }),
4166
- ...(row.reason == null ? {} : { reason: stringValue(row.reason) }),
4167
- ...(row.amountCny == null ? {} : { amountCny: moneyString(row.amountCny) }),
4168
- status: stringValue(row.status), action: analyticsDetailActionValue(action)
4169
- };
4170
- }
4171
- function analyticsDetailActionValue(value) {
4172
- if (!value || typeof value !== 'object' || Array.isArray(value))
4173
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细操作无效');
4174
- const action = value;
4175
- const id = requiredId(action.id, '分析明细操作标识');
4176
- if (action.kind === 'CUSTOMER_CONTRACT_REVIEW')
4177
- return { kind: action.kind, label: '审核', id, importTaskId: requiredId(action.importTaskId, '客户合同任务标识') };
4178
- if (action.kind === 'PURCHASE_CONTRACT_REVIEW')
4179
- return { kind: action.kind, label: '审核', id, importTaskId: requiredId(action.importTaskId, '采购合同任务标识') };
4180
- if (action.kind === 'PURCHASE_ORDER_VIEW')
4181
- return { kind: action.kind, label: analyticsDetailActionLabel(action.label), id, purchaseOrderId: requiredId(action.purchaseOrderId, '采购订单标识'), supplierId: requiredId(action.supplierId, '供应商标识') };
4182
- if (action.kind === 'SALES_ORDER_VIEW')
4183
- return { kind: action.kind, label: analyticsDetailActionLabel(action.label), id, customerId: requiredId(action.customerId, '客户标识'), customerContractId: requiredId(action.customerContractId, '客户合同标识'), purchaseOrderId: requiredId(action.purchaseOrderId, '采购订单标识') };
4184
- if (action.kind === 'SALES_FILE_REVIEW') {
4185
- const fileType = ['PI', 'CI', 'PL', 'CUSTOMS_DECLARATION'].find(item => item === action.fileType);
4186
- const editVersion = Number(action.editVersion);
4187
- if (!fileType || !Number.isInteger(editVersion) || editVersion < 0)
4188
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '销售文件复核操作无效');
4189
- return { kind: action.kind, label: '复核', id, customerContractId: requiredId(action.customerContractId, '客户合同标识'), purchaseOrderId: requiredId(action.purchaseOrderId, '采购订单标识'), salesOrderNoSnapshot: requiredId(action.salesOrderNoSnapshot, '销售订单编号'), fileType, fileName: requiredId(action.fileName, '文件名'), storageFileId: requiredId(action.storageFileId, '存储文件标识'), editVersion, generatedAt: requiredId(action.generatedAt, '生成时间') };
4190
- }
4191
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细操作类型无效', { kind: action.kind });
4192
- }
4193
- function analyticsDetailActionLabel(value) {
4194
- if (value === '查看' || value === '跟进')
4195
- return value;
4196
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细操作名称无效', { value });
4197
- }
4198
- function analyticsKpiExportRows(dashboard) {
4199
- return [
4200
- ['采购总额 CNY', dashboard.kpis.purchaseTotalCny], ['销售总额 CNY', dashboard.kpis.salesTotalCny], ['已收款 CNY', dashboard.kpis.receivedCny],
4201
- ['未收款 CNY', dashboard.kpis.unreceivedCny], ['已付款 CNY', dashboard.kpis.paidCny], ['未付款 CNY', dashboard.kpis.unpaidCny],
4202
- ['已退税 CNY', dashboard.kpis.refundedCny], ['未退税 CNY', dashboard.kpis.unrefundedCny], ['预计毛利 CNY', dashboard.kpis.estimatedGrossProfitCny],
4203
- ['合规待办', dashboard.kpis.complianceTodoCount], ['风险敞口 CNY', dashboard.kpis.riskExposureCny]
4204
- ];
4205
- }
4206
- function analyticsDashboardFromRaw(row) {
4207
- const parsed = parseAnalyticsDashboard(row?.dashboardJson);
4208
- if (parsed)
4209
- return parsed;
4210
- return {
4211
- kpis: {
4212
- purchaseTotalCny: '0.00', salesTotalCny: '0.00', receivedCny: '0.00', unreceivedCny: '0.00',
4213
- paidCny: '0.00', unpaidCny: '0.00', refundedCny: '0.00', unrefundedCny: '0.00',
4214
- estimatedGrossProfitCny: '0.00',
4215
- estimatedGrossProfitRate: '0.00000000',
4216
- complianceTodoCount: 0,
4217
- riskExposureCny: '0.00'
4218
- },
4219
- monthlyTrend: [],
4220
- todos: [],
4221
- riskDistribution: [],
4222
- customerValueTop5: [],
4223
- receivableRiskTop5: [],
4224
- supplierRiskTop5: [],
4225
- salesFileReviews: [],
4226
- riskOrders: [],
4227
- dataSources: []
4228
- };
4229
- }
4230
- function parseAnalyticsDashboard(value) {
4231
- const source = typeof value === 'string' ? safeJsonParse(value) : value;
4232
- if (!source || typeof source !== 'object')
4233
- return null;
4234
- const dashboard = source;
4235
- if (!dashboard.kpis || typeof dashboard.kpis !== 'object')
4236
- return null;
4237
- const rawTodos = arrayOf(dashboard.todos);
4238
- const riskDistribution = arrayOf(dashboard.riskDistribution).flatMap(item => {
4239
- const key = complianceCategoryKeyValue(item.key);
4240
- return key ? [{ key, label: stringValue(item.label), count: numberValue(item.count), amountCny: item.amountCny == null ? undefined : moneyString(item.amountCny) }] : [];
4241
- });
4242
- const todos = riskDistribution.map(risk => {
4243
- const todo = rawTodos.find(item => item.key === risk.key);
4244
- return { ...risk, priority: priorityValue(todo?.priority), description: stringValue(todo?.description) };
4245
- });
4246
- return {
4247
- kpis: {
4248
- purchaseTotalCny: moneyString(dashboard.kpis.purchaseTotalCny),
4249
- salesTotalCny: moneyString(dashboard.kpis.salesTotalCny),
4250
- receivedCny: moneyString(dashboard.kpis.receivedCny),
4251
- unreceivedCny: moneyString(dashboard.kpis.unreceivedCny),
4252
- paidCny: moneyString(dashboard.kpis.paidCny),
4253
- unpaidCny: moneyString(dashboard.kpis.unpaidCny),
4254
- refundedCny: moneyString(dashboard.kpis.refundedCny),
4255
- unrefundedCny: moneyString(dashboard.kpis.unrefundedCny),
4256
- estimatedGrossProfitCny: moneyString(dashboard.kpis.estimatedGrossProfitCny),
4257
- estimatedGrossProfitRate: decimalString(dashboard.kpis.estimatedGrossProfitRate),
4258
- complianceTodoCount: riskDistribution.reduce((sum, item) => sum + item.count, 0),
4259
- riskExposureCny: moneyString(dashboard.kpis.riskExposureCny)
4260
- },
4261
- monthlyTrend: arrayOf(dashboard.monthlyTrend).map(item => ({
4262
- month: numberValue(item.month),
4263
- purchaseAmountCny: moneyString(item.purchaseAmountCny),
4264
- salesAmountCny: moneyString(item.salesAmountCny),
4265
- estimatedGrossProfitCny: moneyString(item.estimatedGrossProfitCny),
4266
- receivedCny: moneyString(item.receivedCny)
4267
- })),
4268
- todos,
4269
- riskDistribution,
4270
- customerValueTop5: arrayOf(dashboard.customerValueTop5).map(item => ({
4271
- customerId: stringValue(item.customerId),
4272
- customerName: stringValue(item.customerName),
4273
- salesAmountCny: moneyString(item.salesAmountCny),
4274
- estimatedGrossProfitCny: moneyString(item.estimatedGrossProfitCny),
4275
- estimatedGrossProfitRate: decimalString(item.estimatedGrossProfitRate),
4276
- unreceivedCny: moneyString(item.unreceivedCny),
4277
- riskStatus: riskStatusValue(item.riskStatus)
4278
- })),
4279
- receivableRiskTop5: arrayOf(dashboard.receivableRiskTop5).map(item => ({
4280
- customerId: stringValue(item.customerId),
4281
- customerName: stringValue(item.customerName),
4282
- unreceivedCny: moneyString(item.unreceivedCny),
4283
- orderCount: numberValue(item.orderCount),
4284
- latestOrderDate: item.latestOrderDate == null ? null : stringValue(item.latestOrderDate)
4285
- })),
4286
- supplierRiskTop5: arrayOf(dashboard.supplierRiskTop5).map(item => ({
4287
- supplierId: stringValue(item.supplierId),
4288
- supplierName: stringValue(item.supplierName),
4289
- unpaidCny: moneyString(item.unpaidCny),
4290
- sanctionHitCount: numberValue(item.sanctionHitCount),
4291
- productRiskHitCount: numberValue(item.productRiskHitCount),
4292
- purchaseOrderCount: numberValue(item.purchaseOrderCount)
4293
- })),
4294
- salesFileReviews: arrayOf(dashboard.salesFileReviews).map(item => ({
4295
- fileId: stringValue(item.fileId),
4296
- fileName: stringValue(item.fileName),
4297
- fileType: stringValue(item.fileType),
4298
- salesOrderNo: stringValue(item.salesOrderNo),
4299
- reason: stringValue(item.reason),
4300
- generatedAt: stringValue(item.generatedAt)
4301
- })),
4302
- riskOrders: arrayOf(dashboard.riskOrders).map(item => ({
4303
- orderKey: stringValue(item.orderKey),
4304
- orderNo: stringValue(item.orderNo),
4305
- partyName: stringValue(item.partyName),
4306
- riskType: stringValue(item.riskType),
4307
- riskAmountCny: moneyString(item.riskAmountCny),
4308
- status: stringValue(item.status)
4309
- })),
4310
- dataSources: arrayOf(dashboard.dataSources).map(item => ({ label: stringValue(item.label), description: stringValue(item.description) }))
4311
- };
4312
- }
4313
- function safeJsonParse(value) { try {
4314
- return JSON.parse(value);
4315
- }
4316
- catch {
4317
- return null;
4318
- } }
4319
- function arrayOf(value) { return Array.isArray(value) ? value.filter((item) => Boolean(item) && typeof item === 'object') : []; }
4320
- function complianceCategoryKeyValue(value) { return AnalyticsComplianceCategoryKeys.find(key => key === value); }
4321
- function analyticsBusinessCategoryKeyValue(value) {
4322
- const complianceKey = complianceCategoryKeyValue(value);
4323
- if (complianceKey)
4324
- return complianceKey;
4325
- if (value === 'risk-order')
4326
- return value;
4327
- throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细业务分类无效', { value });
4328
- }
4329
- function stringValue(value) { return value == null ? '' : String(value); }
4330
- function numberValue(value) { const number = Number(value); return Number.isFinite(number) ? number : 0; }
4331
- function priorityValue(value) { return value === 'high' || value === 'medium' || value === 'low' ? value : 'medium'; }
4332
- function riskStatusValue(value) { return value === 'HIGH' || value === 'MEDIUM' || value === 'LOW' ? value : 'LOW'; }
4333
- function moneyString(value) { try {
4334
- return new Decimal(value == null ? 0 : value).toFixed(2);
4335
- }
4336
- catch {
4337
- return '0.00';
4338
- } }
4339
- function decimalString(value) { try {
4340
- return new Decimal(value == null ? 0 : value).toString();
3961
+ function analyticsExportRowsWithinLimit(sheetName, rows, maxRows) {
3962
+ if (rows.length === 0)
3963
+ return rows;
3964
+ const first = rows[0];
3965
+ const value = first && typeof first === 'object' && !Array.isArray(first) ? first.__totalCount : undefined;
3966
+ const text = typeof value === 'string' ? value : typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : '';
3967
+ if (!/^(0|[1-9]\d*)$/.test(text))
3968
+ throw new Error(`${sheetName}工作表总行数无效`);
3969
+ const totalCount = BigInt(text);
3970
+ if (totalCount < BigInt(rows.length))
3971
+ throw new Error(`${sheetName}工作表总行数无效`);
3972
+ if (totalCount > BigInt(maxRows))
3973
+ throw new Error(`${sheetName}工作表数据行数 ${text} 超过上限 ${maxRows}`);
3974
+ return rows;
4341
3975
  }
4342
- catch {
4343
- return '0';
4344
- } }
4345
3976
  //# sourceMappingURL=workbench.service.js.map