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:
Misaka
2026-03-31 20:42:22 +08:00
parent 17fbd7d251
commit 6e04f21b10
4 changed files with 66 additions and 42 deletions

View File

@@ -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 })
} }

View File

@@ -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) {

View File

@@ -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}` }
} }
} }

View File

@@ -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 {