✨ 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:
@@ -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",
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user