From ad8b3deb0031e327acaf060d6403b3760fefead4 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 16 Mar 2026 16:02:52 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(order-resolver):=20add=20dedup?= =?UTF-8?q?lication=20logic=20for=20production=20order=20number=20resoluti?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add P0: DISTINCT in SQL query to prevent database duplicates - Add P1: Input layer deduplication to avoid redundant queries - Add P2: Return layer deduplication in getValidOrderNumbers() - Optimize resolve() to use batch query instead of loop queries - Add getDeduplicationReport() for human-readable mapping summary - Improve extraction logs to show deduplication statistics - Only log merged mappings (multiple productionIDs → one order number) - Remove duplicate marking as error (normal business scenario) - Bump version to 1.0.1 Example log output: 输入 5 个总排号 → 解析为 2 个唯一订单号(3 个重复已合并) 重复合并详情: SC70202603120085 ← 26B12214、26B12213、26B12212 (共 3 个总排号) SC70202603120131 ← 26B12125、26B12126 (共 2 个总排号) --- package.json | 2 +- src/main/ipc/extractor-handler.ts | 21 +++- src/main/services/erp/order-resolver.ts | 126 ++++++++++++++++++++---- 3 files changed, 126 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 13c96a4..0bc7195 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "erpauto", - "version": "1.0.0", + "version": "1.0.1", "description": "An Electron application with React and TypeScript", "main": "./out/main/index.js", "author": "example.com", diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index d250fa5..ddf1975 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -125,6 +125,9 @@ export function registerExtractorHandlers(): void { const validOrderNumbers = resolver.getValidOrderNumbers(mappings) const warnings = resolver.getWarnings(mappings) + // Get deduplication report for detailed logging + const dedupReport = resolver.getDeduplicationReport(mappings) + if (warnings.length > 0) { log.warn('Resolution warnings', { warnings }) } @@ -137,7 +140,23 @@ export function registerExtractorHandlers(): void { } log.info('Resolved order numbers', { count: validOrderNumbers.length }) - sendLog(sender, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`) + + // Log deduplication summary + sendLog(sender, 'info', dedupReport.summary) + + // Log only merged mappings (where multiple productionIDs map to the same order number) + if (dedupReport.inputCount > dedupReport.uniqueOrderNumbersCount) { + sendLog(sender, 'info', '重复合并详情:') + dedupReport.orderNumberGroups.forEach((productionIds, orderNumber) => { + if (productionIds.length > 1) { + sendLog( + sender, + 'info', + ` ${orderNumber} ← ${productionIds.join('、')} (共 ${productionIds.length} 个总排号)` + ) + } + }) + } // Create auth service and login authService = new ErpAuthService({ diff --git a/src/main/services/erp/order-resolver.ts b/src/main/services/erp/order-resolver.ts index 4aee7aa..fbc5196 100644 --- a/src/main/services/erp/order-resolver.ts +++ b/src/main/services/erp/order-resolver.ts @@ -169,16 +169,21 @@ export class OrderNumberResolver { return new Map() } + // P1: Deduplicate input productionIds to avoid redundant queries + const uniqueProductionIds = [...new Set(productionIds)] + // Use parameterized query to prevent SQL injection - const placeholders = productionIds.map((_, i) => `@p${i}`).join(', ') - const params = productionIds + const placeholders = uniqueProductionIds.map((_, i) => `@p${i}`).join(', ') + const params = uniqueProductionIds let sql: string if (this.dbService.type === 'sqlserver') { - sql = `SELECT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] IN (${placeholders})` + // P0: Use DISTINCT to prevent duplicates from one-to-many relationships + sql = `SELECT DISTINCT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] IN (${placeholders})` } else { - const idPlaceholders = productionIds.map(() => '?').join(', ') - sql = `SELECT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` IN (${idPlaceholders})` + const idPlaceholders = uniqueProductionIds.map(() => '?').join(', ') + // P0: Use DISTINCT to prevent duplicates from one-to-many relationships + sql = `SELECT DISTINCT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` IN (${idPlaceholders})` } const result = await this.dbService.query(sql, params) @@ -205,11 +210,47 @@ export class OrderNumberResolver { /** * Resolve order numbers from mixed input + * + * Optimized for batch processing with deduplication: + * - Multiple productionIDs mapping to the same order number are treated as valid (not errors) + * - Returns all mappings with duplicate tracking */ async resolve(inputs: string[]): Promise { - const mappings: OrderMapping[] = [] + // P1: Deduplicate inputs at the input layer to avoid redundant queries + const uniqueInputs = [...new Set(inputs)] + + // Separate productionIds and order numbers + const productionIds: string[] = [] + const orderNumbers: string[] = [] + + for (const input of uniqueInputs) { + if (this.isOrderNumber(input)) { + orderNumbers.push(input) + } else if (this.isProductionId(input)) { + productionIds.push(input) + } + } + + // Batch query productionId to order number mappings + const mappings = new Map() + if (productionIds.length > 0) { + const batchMappings = await this.mapProductionIdsToOrderNumbers(productionIds) + batchMappings.forEach((orderNum, prodId) => { + mappings.set(prodId, orderNum) + }) + } + + // Build results while preserving original input order + // Note: Multiple productionIDs mapping to the same order number is VALID (not an error) + const results: OrderMapping[] = [] for (const input of inputs) { + // Skip if this exact input was already processed + const alreadyProcessed = results.some((r) => r.input === input) + if (alreadyProcessed) { + continue + } + const mapping: OrderMapping = { input, resolved: false } if (this.isOrderNumber(input)) { @@ -217,35 +258,35 @@ export class OrderNumberResolver { mapping.orderNumber = input mapping.resolved = true } else if (this.isProductionId(input)) { - // Is a productionID, need to lookup + // Is a productionID, lookup from batch mappings mapping.productionId = input - try { - const orderNumber = await this.mapProductionIdToOrderNumber(input) - if (orderNumber) { - mapping.orderNumber = orderNumber - mapping.resolved = true - } else { - mapping.error = '未在数据库中找到对应的订单号' - } - } catch (error) { - mapping.error = error instanceof Error ? error.message : '数据库查询失败' - log.warn('Failed to resolve productionID', { productionId: input, error }) + const orderNumber = mappings.get(input) + if (orderNumber) { + mapping.orderNumber = orderNumber + mapping.resolved = true + } else { + mapping.error = '未在数据库中找到对应的订单号' } } else { mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式' } - mappings.push(mapping) + results.push(mapping) } - return mappings + return results } /** * Get valid order numbers from mappings + * P2: Returns deduplicated order numbers */ getValidOrderNumbers(mappings: OrderMapping[]): string[] { - return mappings.filter((m) => m.resolved && m.orderNumber).map((m) => m.orderNumber!) + const validNumbers = mappings + .filter((m) => m.resolved && m.orderNumber) + .map((m) => m.orderNumber!) + // P2: Deduplicate before returning + return [...new Set(validNumbers)] } /** @@ -295,4 +336,47 @@ export class OrderNumberResolver { return stats } + + /** + * Get deduplication summary for logging + * Returns a human-readable report showing: + * - Input count + * - Unique order numbers count + * - Mapping details (which productionIDs map to which order numbers) + */ + getDeduplicationReport(mappings: OrderMapping[]): { + inputCount: number + uniqueOrderNumbersCount: number + orderNumberGroups: Map + summary: string + } { + // Group productionIDs by their resolved order number + const orderNumberGroups = new Map() + + for (const mapping of mappings) { + if (mapping.resolved && mapping.orderNumber) { + const existing = orderNumberGroups.get(mapping.orderNumber) || [] + existing.push(mapping.input) + orderNumberGroups.set(mapping.orderNumber, existing) + } + } + + const inputCount = mappings.length + const uniqueOrderNumbersCount = orderNumberGroups.size + + // Build summary string + let summary = `输入 ${inputCount} 个总排号 → 解析为 ${uniqueOrderNumbersCount} 个唯一订单号` + + if (inputCount > uniqueOrderNumbersCount) { + const duplicateCount = inputCount - uniqueOrderNumbersCount + summary += `(${duplicateCount} 个重复已合并)` + } + + return { + inputCount, + uniqueOrderNumbersCount, + orderNumberGroups, + summary + } + } }