/** * Order Number Resolver Service * * Automatically recognizes productionID and 生产订单号 (production order number), * and converts them via database lookup. * * - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234") * - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085") * * Database table and field names are loaded from config.yaml */ import type { IDatabaseService } from '../database' import { ConfigManager } from '../config/config-manager' import { createLogger } from '../logger' import type { OrderMapping, OrderNumberType, ResolutionStats } from '../../types/order-resolver.types' const log = createLogger('OrderResolver') /** * ProductionID pattern: 2 digits + 1 letter + 1-6 digits * Examples: 22A1, 22A123, 26B10617 */ const PRODUCTION_ID_PATTERN = /^\d{2}[A-Z]\d{1,6}$/i /** * Production order number pattern: SC + 14 digits */ const ORDER_NUMBER_PATTERN = /^SC\d{14}$/i /** * Database table and field names * Loaded from config.yaml via ConfigManager */ export function getDbConfig() { const configManager = ConfigManager.getInstance() const config = configManager.getConfig() return { TABLE_NAME: config.orderResolution.tableName || 'productionContractData_26 年压力表合同数据', FIELD_PRODUCTION_ID: config.orderResolution.productionIdField || '总排号', FIELD_ORDER_NUMBER: config.orderResolution.orderNumberField || '生产订单号' } } /** * Order Number Resolver Service */ export class OrderNumberResolver { private dbService: IDatabaseService constructor(dbService: IDatabaseService) { this.dbService = dbService } /** * Get table name based on database type * Converts MySQL schema_tablename format to SQL Server [schema].[tablename] format * e.g., productionContractData_26年压力表合同数据 -> [productionContractData].[26年压力表合同数据] * dbo_MaterialsToBeDeleted -> [dbo].[MaterialsToBeDeleted] */ private getTableName(tableName: string): string { if (this.dbService.type === 'sqlserver') { // Find the FIRST underscore to split schema and table name // This handles patterns like: schema_tablename const firstUnderscoreIndex = tableName.indexOf('_') if (firstUnderscoreIndex > 0) { const schema = tableName.substring(0, firstUnderscoreIndex) const actualTableName = tableName.substring(firstUnderscoreIndex + 1) return `[${schema}].[${actualTableName}]` } // If no underscore found, default to dbo schema return `[dbo].[${tableName}]` } return tableName } /** * Check if input matches productionID pattern */ isProductionId(input: string): boolean { return PRODUCTION_ID_PATTERN.test(input) } /** * Check if input matches order number pattern */ isOrderNumber(input: string): boolean { return ORDER_NUMBER_PATTERN.test(input) } /** * Map productionID to order number via database lookup */ async mapProductionIdToOrderNumber(productionId: string): Promise { try { const dbConfig = getDbConfig() const tableName = this.getTableName(dbConfig.TABLE_NAME) let sql: string let params: any[] if (this.dbService.type === 'sqlserver') { // 使用 COLLATE 指定不区分大小写的排序规则 sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS = @p0` params = [productionId] } else { // MySQL 默认不区分大小写,但显式使用 UPPER 确保一致性 sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE UPPER(\`${dbConfig.FIELD_PRODUCTION_ID}\`) = UPPER(?) LIMIT 1` params = [productionId] } const result = await this.dbService.query(sql, params) if (result.rows.length > 0) { const orderNumber = result.rows[0][Object.keys(result.rows[0])[0]] as string return orderNumber || null } return null } catch (error) { const message = error instanceof Error ? error.message : '未知数据库错误' log.error('Failed to map productionID to order number', { productionId, error: message }) throw error } } /** * Map multiple productionIds to order numbers */ async mapProductionIdsToOrderNumbers(productionIds: string[]): Promise> { try { const dbConfig = getDbConfig() const tableName = this.getTableName(dbConfig.TABLE_NAME) if (productionIds.length === 0) { 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 = uniqueProductionIds.map((_, i) => `@p${i}`).join(', ') const params = uniqueProductionIds let sql: string if (this.dbService.type === 'sqlserver') { // P0: Use DISTINCT to prevent duplicates from one-to-many relationships // 使用 COLLATE 指定不区分大小写的排序规则 sql = `SELECT DISTINCT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS IN (${placeholders})` } else { const idPlaceholders = uniqueProductionIds.map(() => 'UPPER(?)').join(', ') // P0: Use DISTINCT to prevent duplicates from one-to-many relationships // MySQL: 使用 UPPER 确保不区分大小写 sql = `SELECT DISTINCT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE UPPER(\`${dbConfig.FIELD_PRODUCTION_ID}\`) IN (${idPlaceholders})` } const result = await this.dbService.query(sql, params) const mappings = new Map() for (const row of result.rows) { const keys = Object.keys(row) const prodId = row[keys[0]] as string const orderNum = row[keys[1]] as string if (prodId && orderNum) { mappings.set(prodId, orderNum) } } return mappings } catch (error) { const message = error instanceof Error ? error.message : '未知数据库错误' log.error('Failed to map productionIds to order numbers', { error: message }) throw error } } /** * 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 { // 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 // 使用小写 key 存储映射,以支持忽略大小写查找 const mappings = new Map() if (productionIds.length > 0) { const batchMappings = await this.mapProductionIdsToOrderNumbers(productionIds) batchMappings.forEach((orderNum, prodId) => { mappings.set(prodId.toLowerCase(), 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)) { // Already an order number mapping.orderNumber = input mapping.resolved = true } else if (this.isProductionId(input)) { // Is a productionID, lookup from batch mappings (使用小写查找以忽略大小写) mapping.productionId = input const orderNumber = mappings.get(input.toLowerCase()) if (orderNumber) { mapping.orderNumber = orderNumber mapping.resolved = true } else { mapping.error = '未在数据库中找到对应的订单号' } } else { mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式' } results.push(mapping) } return results } /** * Get valid order numbers from mappings * P2: Returns deduplicated order numbers */ getValidOrderNumbers(mappings: OrderMapping[]): string[] { const validNumbers = mappings .filter((m) => m.resolved && m.orderNumber) .map((m) => m.orderNumber!) // P2: Deduplicate before returning return [...new Set(validNumbers)] } /** * Get warnings from failed mappings */ getWarnings(mappings: OrderMapping[]): string[] { return mappings.filter((m) => !m.resolved && m.error).map((m) => `${m.input}: ${m.error}`) } /** * Recognize the type of input */ recognizeType(input: string): OrderNumberType { if (this.isOrderNumber(input)) return 'orderNumber' if (this.isProductionId(input)) return 'productionId' return 'unknown' } /** * Get resolution statistics */ getStats(mappings: OrderMapping[]): ResolutionStats { const stats: ResolutionStats = { totalInputs: mappings.length, validOrderNumbers: 0, validProductionIds: 0, resolvedCount: 0, failedCount: 0, unknownFormat: 0 } for (const mapping of mappings) { if (mapping.resolved) { stats.resolvedCount++ if (mapping.orderNumber && !mapping.productionId) { stats.validOrderNumbers++ } else if (mapping.productionId) { stats.validProductionIds++ } } else { stats.failedCount++ if (!mapping.productionId && !mapping.orderNumber) { stats.unknownFormat++ } } } 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 } } }