fix(extractor): track per-order RecordCount in operation history
Previously updateBatchStatus wrote the batch-level total recordCount to every row, causing the detail view to show misleading identical counts. Now mergeFiles collects per-order material counts, the handler writes each order's count individually via updateRecordStatus, and batch aggregation uses SUM instead of MAX for accurate totals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -256,7 +256,14 @@ export function registerExtractorHandlers(): void {
|
|||||||
: result.errors.length > 0
|
: result.errors.length > 0
|
||||||
? 'failed'
|
? 'failed'
|
||||||
: 'success'
|
: 'success'
|
||||||
await historyDao.updateBatchStatus(batchId, status, result.recordCount)
|
|
||||||
|
// Write per-order record counts
|
||||||
|
for (const { orderNumber, recordCount } of result.orderRecordCounts) {
|
||||||
|
await historyDao.updateRecordStatus(batchId, orderNumber, status, undefined, recordCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update batch status without recordCount (per-order counts are set individually)
|
||||||
|
await historyDao.updateBatchStatus(batchId, status)
|
||||||
log.info('Operation history batch status updated', { batchId, status })
|
log.info('Operation history batch status updated', { batchId, status })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,43 +160,27 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
* Update the status of all records in a batch
|
* Update the status of all records in a batch
|
||||||
* @param batchId - Batch identifier
|
* @param batchId - Batch identifier
|
||||||
* @param status - New status (success, failed, partial)
|
* @param status - New status (success, failed, partial)
|
||||||
* @param recordCount - Total record count for the batch
|
|
||||||
* @returns Update result
|
* @returns Update result
|
||||||
*/
|
*/
|
||||||
async updateBatchStatus(
|
async updateBatchStatus(
|
||||||
batchId: string,
|
batchId: string,
|
||||||
status: string,
|
status: string
|
||||||
recordCount: number | null
|
|
||||||
): Promise<UpdateBatchStatusResult> {
|
): Promise<UpdateBatchStatusResult> {
|
||||||
try {
|
try {
|
||||||
const dbService = await this.getDatabaseService()
|
const dbService = await this.getDatabaseService()
|
||||||
const tableName = this.getTableName()
|
const tableName = this.getTableName()
|
||||||
const isSqlServer = dbService.type === 'sqlserver'
|
const isSqlServer = dbService.type === 'sqlserver'
|
||||||
|
|
||||||
const placeholder = isSqlServer ? '@p0' : '?'
|
const sqlString = `
|
||||||
let sqlString: string
|
UPDATE ${tableName}
|
||||||
let params: (string | number | null)[]
|
SET Status = ${isSqlServer ? '@p0' : '?'}
|
||||||
|
WHERE BatchId = ${isSqlServer ? '@p1' : '?'}
|
||||||
if (recordCount !== null) {
|
`
|
||||||
sqlString = `
|
const params = [status, batchId]
|
||||||
UPDATE ${tableName}
|
|
||||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
|
||||||
RecordCount = ${isSqlServer ? '@p1' : '?'}
|
|
||||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
|
||||||
`
|
|
||||||
params = isSqlServer ? [status, recordCount, batchId] : [status, recordCount, batchId]
|
|
||||||
} else {
|
|
||||||
sqlString = `
|
|
||||||
UPDATE ${tableName}
|
|
||||||
SET Status = ${placeholder}
|
|
||||||
WHERE BatchId = ${isSqlServer ? '@p1' : '?'}
|
|
||||||
`
|
|
||||||
params = isSqlServer ? [status, batchId] : [status, batchId]
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbService.query(sqlString, params)
|
await dbService.query(sqlString, params)
|
||||||
|
|
||||||
log.info('Batch status updated', { batchId, status, recordCount })
|
log.info('Batch status updated', { batchId, status })
|
||||||
return { success: true, updatedCount: 1 }
|
return { success: true, updatedCount: 1 }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Update batch status error', {
|
log.error('Update batch status error', {
|
||||||
@@ -208,33 +192,51 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a single record's status and error message
|
* Update a single record's status, error message, and optional record count
|
||||||
* @param batchId - Batch identifier
|
* @param batchId - Batch identifier
|
||||||
* @param orderNumber - Order number
|
* @param orderNumber - Order number
|
||||||
* @param status - New status
|
* @param status - New status
|
||||||
* @param errorMessage - Optional error message
|
* @param errorMessage - Optional error message
|
||||||
|
* @param recordCount - Optional per-order record count
|
||||||
* @returns True if successful
|
* @returns True if successful
|
||||||
*/
|
*/
|
||||||
async updateRecordStatus(
|
async updateRecordStatus(
|
||||||
batchId: string,
|
batchId: string,
|
||||||
orderNumber: string,
|
orderNumber: string,
|
||||||
status: string,
|
status: string,
|
||||||
errorMessage?: string
|
errorMessage?: string,
|
||||||
|
recordCount?: number
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const dbService = await this.getDatabaseService()
|
const dbService = await this.getDatabaseService()
|
||||||
const tableName = this.getTableName()
|
const tableName = this.getTableName()
|
||||||
const isSqlServer = dbService.type === 'sqlserver'
|
const isSqlServer = dbService.type === 'sqlserver'
|
||||||
|
|
||||||
const sqlString = `
|
let sqlString: string
|
||||||
UPDATE ${tableName}
|
let params: (string | number | null)[]
|
||||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
|
||||||
ErrorMessage = ${isSqlServer ? '@p1' : '?'}
|
|
||||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
|
||||||
AND OrderNumber = ${isSqlServer ? '@p3' : '?'}
|
|
||||||
`
|
|
||||||
|
|
||||||
await dbService.query(sqlString, [status, errorMessage || null, batchId, orderNumber])
|
if (recordCount !== undefined) {
|
||||||
|
sqlString = `
|
||||||
|
UPDATE ${tableName}
|
||||||
|
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||||
|
ErrorMessage = ${isSqlServer ? '@p1' : '?'},
|
||||||
|
RecordCount = ${isSqlServer ? '@p2' : '?'}
|
||||||
|
WHERE BatchId = ${isSqlServer ? '@p3' : '?'}
|
||||||
|
AND OrderNumber = ${isSqlServer ? '@p4' : '?'}
|
||||||
|
`
|
||||||
|
params = [status, errorMessage || null, recordCount, batchId, orderNumber]
|
||||||
|
} else {
|
||||||
|
sqlString = `
|
||||||
|
UPDATE ${tableName}
|
||||||
|
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||||
|
ErrorMessage = ${isSqlServer ? '@p1' : '?'}
|
||||||
|
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
||||||
|
AND OrderNumber = ${isSqlServer ? '@p3' : '?'}
|
||||||
|
`
|
||||||
|
params = [status, errorMessage || null, batchId, orderNumber]
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbService.query(sqlString, params)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ export class ExtractorService {
|
|||||||
downloadedFiles: [],
|
downloadedFiles: [],
|
||||||
mergedFile: null,
|
mergedFile: null,
|
||||||
recordCount: 0,
|
recordCount: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
|
orderRecordCounts: []
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -79,6 +80,7 @@ export class ExtractorService {
|
|||||||
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
||||||
result.mergedFile = mergeResult.mergedFile
|
result.mergedFile = mergeResult.mergedFile
|
||||||
result.recordCount = mergeResult.recordCount
|
result.recordCount = mergeResult.recordCount
|
||||||
|
result.orderRecordCounts = mergeResult.orderRecordCounts
|
||||||
|
|
||||||
// Add merge error to result if any
|
// Add merge error to result if any
|
||||||
if (mergeResult.error) {
|
if (mergeResult.error) {
|
||||||
@@ -123,9 +125,14 @@ export class ExtractorService {
|
|||||||
*/
|
*/
|
||||||
private async mergeFiles(
|
private async mergeFiles(
|
||||||
filePaths: string[]
|
filePaths: string[]
|
||||||
): Promise<{ mergedFile: string | null; recordCount: number; error?: string }> {
|
): Promise<{
|
||||||
|
mergedFile: string | null
|
||||||
|
recordCount: number
|
||||||
|
error?: string
|
||||||
|
orderRecordCounts: Array<{ orderNumber: string; recordCount: number }>
|
||||||
|
}> {
|
||||||
if (filePaths.length === 0) {
|
if (filePaths.length === 0) {
|
||||||
return { mergedFile: null, recordCount: 0 }
|
return { mergedFile: null, recordCount: 0, orderRecordCounts: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Starting merge', { fileCount: filePaths.length })
|
log.info('Starting merge', { fileCount: filePaths.length })
|
||||||
@@ -154,15 +161,21 @@ export class ExtractorService {
|
|||||||
|
|
||||||
// Calculate total record count (total material rows)
|
// Calculate total record count (total material rows)
|
||||||
let recordCount = 0
|
let recordCount = 0
|
||||||
|
const orderRecordCounts: Array<{ orderNumber: string; recordCount: number }> = []
|
||||||
for (const order of allOrders) {
|
for (const order of allOrders) {
|
||||||
recordCount += order.materials.length
|
const count = order.materials.length
|
||||||
|
recordCount += count
|
||||||
|
orderRecordCounts.push({
|
||||||
|
orderNumber: order.orderInfo.productionOrder || '',
|
||||||
|
recordCount: count
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
|
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
|
||||||
|
|
||||||
if (recordCount === 0) {
|
if (recordCount === 0) {
|
||||||
log.warn('No records found in any downloaded files')
|
log.warn('No records found in any downloaded files')
|
||||||
return { mergedFile: null, recordCount: 0 }
|
return { mergedFile: null, recordCount: 0, orderRecordCounts }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate output filename with timestamp
|
// Generate output filename with timestamp
|
||||||
@@ -178,13 +191,13 @@ export class ExtractorService {
|
|||||||
log.info('Saving merged file', { outputPath })
|
log.info('Saving merged file', { outputPath })
|
||||||
await this.saveMergedOrders(allOrders, outputPath)
|
await this.saveMergedOrders(allOrders, outputPath)
|
||||||
log.info('Merged file saved successfully', { recordCount })
|
log.info('Merged file saved successfully', { recordCount })
|
||||||
return { mergedFile: outputPath, recordCount }
|
return { mergedFile: outputPath, recordCount, orderRecordCounts }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||||
const errorStack = error instanceof Error ? error.stack : ''
|
const errorStack = error instanceof Error ? error.stack : ''
|
||||||
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
|
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
|
||||||
// Return parsed record count and error info even if save fails
|
// Return parsed record count and error info even if save fails
|
||||||
return { mergedFile: null, recordCount, error: `保存合并文件失败:${errorMsg}` }
|
return { mergedFile: null, recordCount, orderRecordCounts, error: `保存合并文件失败:${errorMsg}` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export interface ExtractorResult {
|
|||||||
errors: string[]
|
errors: string[]
|
||||||
/** Database import result (only populated if mergedFile was created) */
|
/** Database import result (only populated if mergedFile was created) */
|
||||||
importResult?: ImportResult
|
importResult?: ImportResult
|
||||||
|
/** Per-order material row counts */
|
||||||
|
orderRecordCounts: Array<{ orderNumber: string; recordCount: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderInfo {
|
export interface OrderInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user