feat(order-resolver): add deduplication logic for production order number resolution

- 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 个总排号)
This commit is contained in:
Misaka_Company
2026-03-16 16:02:52 +08:00
parent cccc4e4c8c
commit ad8b3deb00
3 changed files with 126 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "erpauto", "name": "erpauto",
"version": "1.0.0", "version": "1.0.1",
"description": "An Electron application with React and TypeScript", "description": "An Electron application with React and TypeScript",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "example.com", "author": "example.com",

View File

@@ -125,6 +125,9 @@ export function registerExtractorHandlers(): void {
const validOrderNumbers = resolver.getValidOrderNumbers(mappings) const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
const warnings = resolver.getWarnings(mappings) const warnings = resolver.getWarnings(mappings)
// Get deduplication report for detailed logging
const dedupReport = resolver.getDeduplicationReport(mappings)
if (warnings.length > 0) { if (warnings.length > 0) {
log.warn('Resolution warnings', { warnings }) log.warn('Resolution warnings', { warnings })
} }
@@ -137,7 +140,23 @@ export function registerExtractorHandlers(): void {
} }
log.info('Resolved order numbers', { count: validOrderNumbers.length }) 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 // Create auth service and login
authService = new ErpAuthService({ authService = new ErpAuthService({

View File

@@ -169,16 +169,21 @@ export class OrderNumberResolver {
return new Map() return new Map()
} }
// P1: Deduplicate input productionIds to avoid redundant queries
const uniqueProductionIds = [...new Set(productionIds)]
// Use parameterized query to prevent SQL injection // Use parameterized query to prevent SQL injection
const placeholders = productionIds.map((_, i) => `@p${i}`).join(', ') const placeholders = uniqueProductionIds.map((_, i) => `@p${i}`).join(', ')
const params = productionIds const params = uniqueProductionIds
let sql: string let sql: string
if (this.dbService.type === 'sqlserver') { 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 { } else {
const idPlaceholders = productionIds.map(() => '?').join(', ') const idPlaceholders = uniqueProductionIds.map(() => '?').join(', ')
sql = `SELECT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` IN (${idPlaceholders})` // 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) const result = await this.dbService.query(sql, params)
@@ -205,11 +210,47 @@ export class OrderNumberResolver {
/** /**
* Resolve order numbers from mixed input * 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<OrderMapping[]> { async resolve(inputs: string[]): Promise<OrderMapping[]> {
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<string, string>()
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) { 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 } const mapping: OrderMapping = { input, resolved: false }
if (this.isOrderNumber(input)) { if (this.isOrderNumber(input)) {
@@ -217,35 +258,35 @@ export class OrderNumberResolver {
mapping.orderNumber = input mapping.orderNumber = input
mapping.resolved = true mapping.resolved = true
} else if (this.isProductionId(input)) { } else if (this.isProductionId(input)) {
// Is a productionID, need to lookup // Is a productionID, lookup from batch mappings
mapping.productionId = input mapping.productionId = input
try { const orderNumber = mappings.get(input)
const orderNumber = await this.mapProductionIdToOrderNumber(input) if (orderNumber) {
if (orderNumber) { mapping.orderNumber = orderNumber
mapping.orderNumber = orderNumber mapping.resolved = true
mapping.resolved = true } else {
} else { mapping.error = '未在数据库中找到对应的订单号'
mapping.error = '未在数据库中找到对应的订单号'
}
} catch (error) {
mapping.error = error instanceof Error ? error.message : '数据库查询失败'
log.warn('Failed to resolve productionID', { productionId: input, error })
} }
} else { } else {
mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式' mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式'
} }
mappings.push(mapping) results.push(mapping)
} }
return mappings return results
} }
/** /**
* Get valid order numbers from mappings * Get valid order numbers from mappings
* P2: Returns deduplicated order numbers
*/ */
getValidOrderNumbers(mappings: OrderMapping[]): string[] { 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 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<string, string[]>
summary: string
} {
// Group productionIDs by their resolved order number
const orderNumberGroups = new Map<string, string[]>()
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
}
}
} }