diff --git a/src/main/services/config/config-manager.ts b/src/main/services/config/config-manager.ts index e21d722..a0cac1e 100644 --- a/src/main/services/config/config-manager.ts +++ b/src/main/services/config/config-manager.ts @@ -20,7 +20,7 @@ import { dirname } from 'path' import { app } from 'electron' import yaml from 'js-yaml' import { z } from 'zod' -import { createLogger, applyLoggingConfig } from '../logger' +import { createLogger, applyLoggingConfig, trackDuration } from '../logger' import { applyAuditConfig } from '../logger/audit-logger' import { fullConfigSchema, @@ -141,14 +141,22 @@ export class ConfigManager { // 开发环境:配置文件放在项目根目录,方便编辑和调试 this.configPath = path.resolve(__dirname, '../../config.yaml') this.backupPath = path.resolve(__dirname, '../../config.yaml.backup') - log.info('Running in development mode', { configPath: this.configPath }) + log.info('Running in development mode', { + configPath: this.configPath, + isDev: true, + environment: process.env.NODE_ENV || 'not-set' + }) } else { // 生产环境(包括安装版和便携版):配置文件放在用户数据目录 // Windows: C:\Users\\AppData\Roaming\erpauto\config.yaml // 这样配置会在应用升级时保留,且不会暴露在应用目录中 this.configPath = path.join(app.getPath('userData'), 'config.yaml') this.backupPath = path.join(app.getPath('userData'), 'config.yaml.backup') - log.info('Running in production mode', { configPath: this.configPath }) + log.info('Running in production mode', { + configPath: this.configPath, + isDev: false, + userDataPath: app.getPath('userData') + }) } this.initialized = true @@ -168,12 +176,18 @@ export class ConfigManager { */ public async initialize(): Promise { if (!fs.existsSync(this.configPath)) { - log.info('Config file not found, creating default config.yaml') + log.info('Config file not found, creating default config.yaml', { + configPath: this.configPath + }) await this.saveConfig(DEFAULT_CONFIG) this.config = DEFAULT_CONFIG // Apply logging configuration from default config applyLoggingConfig(DEFAULT_CONFIG.logging) applyAuditConfig(DEFAULT_CONFIG.logging.auditRetention) + log.info('Default configuration created and applied', { + configPath: this.configPath, + logLevel: DEFAULT_CONFIG.logging.level + }) return } @@ -196,14 +210,26 @@ export class ConfigManager { applyLoggingConfig(validated.logging) applyAuditConfig(validated.logging.auditRetention) - log.info('Configuration loaded and validated successfully') + log.info('Configuration loaded and validated successfully', { + configPath: this.configPath, + logLevel: validated.logging.level, + auditRetention: validated.logging.auditRetention, + appRetention: validated.logging.appRetention, + isDev: process.env.NODE_ENV === 'development' || !(app?.isPackaged ?? false) + }) } catch (error) { if (error instanceof z.ZodError) { const messages = error.issues.map(formatZodIssue) - log.error('Configuration validation failed', { errors: messages }) - throw new Error(`配置文件验证失败:\n${messages.join('\n')}`) + log.error('Configuration validation failed', { + configPath: this.configPath, + errors: messages + }) + throw new Error(`配置文件验证失败:\n${messages.join('\n')}`) } - log.error('Failed to load configuration', { error }) + log.error('Failed to load configuration', { + configPath: this.configPath, + error + }) throw error } } @@ -216,6 +242,7 @@ export class ConfigManager { // 备份现有配置 if (fs.existsSync(this.configPath)) { fs.copyFileSync(this.configPath, this.backupPath) + log.debug('Config backup created', { backupPath: this.backupPath }) } // 转换为 YAML @@ -230,13 +257,21 @@ export class ConfigManager { fs.writeFileSync(this.configPath, content, 'utf-8') this.config = config - log.info('Configuration saved successfully') + log.info('Configuration saved successfully', { + configPath: this.configPath, + logLevel: config.logging.level, + auditRetention: config.logging.auditRetention + }) return true } catch (error) { - log.error('Failed to save configuration', { error }) + log.error('Failed to save configuration', { + configPath: this.configPath, + error + }) // 恢复备份 if (fs.existsSync(this.backupPath)) { fs.copyFileSync(this.backupPath, this.configPath) + log.warn('Configuration restored from backup', { backupPath: this.backupPath }) } return false } @@ -295,6 +330,11 @@ export class ConfigManager { await this.loadConfig() } + log.info('Updating configuration', { + configPath: this.configPath, + updateKeys: Object.keys(updates) + }) + // 深合并 const merged = this.deepMerge(this.config!, updates) @@ -306,12 +346,24 @@ export class ConfigManager { return { success: false, error: '保存配置失败' } } + log.info('Configuration update completed', { + configPath: this.configPath, + updatedKeys: Object.keys(updates) + }) return { success: true } } catch (error) { if (error instanceof z.ZodError) { const messages = error.issues.map(formatZodIssue) - return { success: false, error: `配置验证失败:\n${messages.join('\n')}` } + log.error('Configuration update validation failed', { + configPath: this.configPath, + errors: messages + }) + return { success: false, error: `配置验证失败:\n${messages.join('\n')}` } } + log.error('Failed to update configuration', { + configPath: this.configPath, + error + }) return { success: false, error: error instanceof Error ? error.message : '未知错误' } } } diff --git a/src/main/services/database/discrete-material-plan-dao.ts b/src/main/services/database/discrete-material-plan-dao.ts index f912d01..19025bb 100644 --- a/src/main/services/database/discrete-material-plan-dao.ts +++ b/src/main/services/database/discrete-material-plan-dao.ts @@ -9,7 +9,7 @@ */ import { create, type IDatabaseService } from './index' -import { createLogger } from '../logger' +import { createLogger, run, getRequestId, trackDuration } from '../logger' const log = createLogger('DiscreteMaterialPlanDAO') @@ -138,11 +138,17 @@ export class DiscreteMaterialPlanDAO { const tableName = this.getTableName() const sqlString = `SELECT * FROM ${tableName}` - const result = await dbService.query(sqlString) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'DiscreteMaterialPlanDAO.queryAll', + context: { tableName, operationType: 'SELECT' } + }) - return result.rows + return result.result.rows } catch (error) { log.error('Query all error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -182,10 +188,16 @@ export class DiscreteMaterialPlanDAO { WHERE rn = 1 ` - const result = await dbService.query(sqlString) - return result.rows + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'DiscreteMaterialPlanDAO.queryAllDistinctByMaterialCode', + context: { tableName: this.getTableName(), operationType: 'SELECT' } + }) + return result.result.rows } catch (error) { log.error('Query all distinct by material code error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -221,13 +233,25 @@ export class DiscreteMaterialPlanDAO { WHERE SourceNumber IN (${placeholders}) ` - const result = await dbService.query(sqlString, batch) - allResults.push(...result.rows) + const result = await trackDuration(async () => await dbService.query(sqlString, batch), { + operationName: 'DiscreteMaterialPlanDAO.queryBySourceNumbers', + context: { + tableName, + operationType: 'SELECT', + batchNumber: Math.floor(i / batchSize) + 1, + batchSize: batch.length + } + }) + allResults.push(...result.result.rows) } return allResults } catch (error) { log.error('Query by source numbers error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), + recordCount: sourceNumbers.length, error: error instanceof Error ? error.message : String(error) }) return [] @@ -280,13 +304,25 @@ export class DiscreteMaterialPlanDAO { WHERE rn = 1 ` - const result = await dbService.query(sqlString, batch) - allResults.push(...result.rows) + const result = await trackDuration(async () => await dbService.query(sqlString, batch), { + operationName: 'DiscreteMaterialPlanDAO.queryBySourceNumbersDistinct', + context: { + tableName, + operationType: 'SELECT', + batchNumber: Math.floor(i / batchSize) + 1, + batchSize: batch.length + } + }) + allResults.push(...result.result.rows) } return allResults } catch (error) { log.error('Query by source numbers distinct error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), + recordCount: sourceNumbers.length, error: error instanceof Error ? error.message : String(error) }) return [] @@ -311,10 +347,19 @@ export class DiscreteMaterialPlanDAO { WHERE SourceNumber = ${placeholder} ` - const result = await dbService.query(sqlString, [sourceNumber]) - return result.rows + const result = await trackDuration( + async () => await dbService.query(sqlString, [sourceNumber]), + { + operationName: 'DiscreteMaterialPlanDAO.queryBySourceNumber', + context: { tableName, operationType: 'SELECT' } + } + ) + return result.result.rows } catch (error) { log.error('Query by source number error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -341,10 +386,19 @@ export class DiscreteMaterialPlanDAO { WHERE PlanNumber = ${placeholder} ` - const result = await dbService.query(sqlString, [planNumber]) - return result.rows + const result = await trackDuration( + async () => await dbService.query(sqlString, [planNumber]), + { + operationName: 'DiscreteMaterialPlanDAO.queryByPlanNumber', + context: { tableName, operationType: 'SELECT' } + } + ) + return result.result.rows } catch (error) { log.error('Query by plan number error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -378,13 +432,25 @@ export class DiscreteMaterialPlanDAO { WHERE PlanNumber IN (${placeholders}) ` - const result = await dbService.query(sqlString, batch) - allResults.push(...result.rows) + const result = await trackDuration(async () => await dbService.query(sqlString, batch), { + operationName: 'DiscreteMaterialPlanDAO.queryByPlanNumbers', + context: { + tableName, + operationType: 'SELECT', + batchNumber: Math.floor(i / batchSize) + 1, + batchSize: batch.length + } + }) + allResults.push(...result.result.rows) } return allResults } catch (error) { log.error('Query by plan numbers error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), + recordCount: planNumbers.length, error: error instanceof Error ? error.message : String(error) }) return [] @@ -404,18 +470,31 @@ export class DiscreteMaterialPlanDAO { return 0 } + const batchId = getRequestId() || `delete-${Date.now()}` + let totalDeleted = 0 + try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const isSqlServer = dbService.type === 'sqlserver' const batchSize = 2000 - let totalDeleted = 0 // Get unique source numbers const uniqueSourceNumbers = [...new Set(sourceNumbers.filter(Boolean))] + const totalBatches = Math.ceil(uniqueSourceNumbers.length / batchSize) + + log.info('Starting batch delete operation', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: batchId, + totalRecords: uniqueSourceNumbers.length, + batchSize, + totalBatches + }) for (let i = 0; i < uniqueSourceNumbers.length; i += batchSize) { const batch = uniqueSourceNumbers.slice(i, i + batchSize) + const batchNumber = Math.floor(i / batchSize) + 1 const placeholders = this.buildPlaceholders(batch.length, isSqlServer) const sqlString = ` @@ -423,16 +502,32 @@ export class DiscreteMaterialPlanDAO { WHERE SourceNumber IN (${placeholders}) ` - const result = await dbService.query(sqlString, batch) - totalDeleted += result.rowCount || 0 + const result = await trackDuration(async () => await dbService.query(sqlString, batch), { + operationName: 'DiscreteMaterialPlanDAO.deleteBySourceNumbers', + context: { + tableName, + operationType: 'DELETE', + batchId, + batchNumber, + totalBatches, + batchSize: batch.length + } + }) + const deletedCount = result.result.rowCount || 0 + totalDeleted += deletedCount log.debug('Deleted batch', { - batch: i / batchSize + 1, - count: result.rowCount + batch: batchNumber, + totalBatches, + count: deletedCount, + batchId }) } log.info('Deleted records by source numbers', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: batchId, totalDeleted, sourceNumberCount: uniqueSourceNumbers.length }) @@ -440,6 +535,11 @@ export class DiscreteMaterialPlanDAO { return totalDeleted } catch (error) { log.error('Delete by source numbers error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: batchId, + totalDeleted, + recordCount: sourceNumbers.length, error: error instanceof Error ? error.message : String(error) }) throw error @@ -459,11 +559,13 @@ export class DiscreteMaterialPlanDAO { return 0 } + const batchId = getRequestId() || `insert-${Date.now()}` + let totalInserted = 0 + try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const isSqlServer = dbService.type === 'sqlserver' - let totalInserted = 0 // SQL Server has a limit of 2100 parameters per query // Each record has 28 columns, so max rows per batch = 2100 / 28 = 75 @@ -473,35 +575,61 @@ export class DiscreteMaterialPlanDAO { const effectiveBatchSize = isSqlServer ? Math.min(batchSize, Math.floor(sqlServerMaxParams / columnsPerRow)) : batchSize + const totalBatches = Math.ceil(records.length / effectiveBatchSize) - log.info('Batch insert parameters', { + log.info('Batch insert started', { + tableName, + operationType: 'INSERT', + requestId: batchId, isSqlServer, dbType: dbService.type, columnsPerRow, effectiveBatchSize, - totalRecords: records.length + totalRecords: records.length, + totalBatches }) // Process in batches for (let i = 0; i < records.length; i += effectiveBatchSize) { const batch = records.slice(i, i + effectiveBatchSize) - const inserted = await this.insertBatch(dbService, tableName, batch, isSqlServer) + const batchNumber = Math.floor(i / effectiveBatchSize) + 1 + + const inserted = await this.insertBatchWithTracking( + dbService, + tableName, + batch, + isSqlServer, + batchId, + batchNumber, + totalBatches + ) totalInserted += inserted log.debug('Inserted batch', { - batch: Math.floor(i / effectiveBatchSize) + 1, - count: inserted + batch: batchNumber, + totalBatches, + count: inserted, + batchId }) } log.info('Batch insert completed', { + tableName, + operationType: 'INSERT', + requestId: batchId, totalInserted, - batchSize: effectiveBatchSize + batchSize: effectiveBatchSize, + totalBatches }) return totalInserted } catch (error) { log.error('Batch insert error', { + tableName: this.getTableName(), + operationType: 'INSERT', + requestId: batchId, + totalInserted, + recordCount: records.length, error: error instanceof Error ? error.message : String(error) }) throw error @@ -509,13 +637,16 @@ export class DiscreteMaterialPlanDAO { } /** - * Insert a single batch of records + * Insert a single batch of records with tracking */ - private async insertBatch( + private async insertBatchWithTracking( dbService: IDatabaseService, tableName: string, records: MaterialPlanRecord[], - isSqlServer: boolean + isSqlServer: boolean, + batchId: string, + batchNumber: number, + totalBatches: number ): Promise { if (records.length === 0) { return 0 @@ -567,8 +698,30 @@ export class DiscreteMaterialPlanDAO { VALUES ${rowPlaceholders.join(', ')} ` - const result = await dbService.query(sqlString, values) - return result.rowCount || records.length + const result = await trackDuration(async () => await dbService.query(sqlString, values), { + operationName: 'DiscreteMaterialPlanDAO.insertBatch', + context: { + tableName, + operationType: 'INSERT', + batchId, + batchNumber, + totalBatches, + recordCount: records.length + } + }) + return result.result.rowCount || records.length + } + + /** + * Insert a single batch of records (legacy method - kept for compatibility) + */ + private async insertBatch( + dbService: IDatabaseService, + tableName: string, + records: MaterialPlanRecord[], + isSqlServer: boolean + ): Promise { + return this.insertBatchWithTracking(dbService, tableName, records, isSqlServer, 'unknown', 1, 1) } /** @@ -660,11 +813,17 @@ export class DiscreteMaterialPlanDAO { const tableName = this.getTableName() const sqlString = `SELECT COUNT(*) as count FROM ${tableName}` - const result = await dbService.query(sqlString) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'DiscreteMaterialPlanDAO.countAll', + context: { tableName, operationType: 'SELECT' } + }) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0 } catch (error) { log.error('Count all error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 @@ -689,10 +848,19 @@ export class DiscreteMaterialPlanDAO { WHERE PlanNumber = ${placeholder} ` - const result = await dbService.query(sqlString, [planNumber]) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await trackDuration( + async () => await dbService.query(sqlString, [planNumber]), + { + operationName: 'DiscreteMaterialPlanDAO.countByPlanNumber', + context: { tableName, operationType: 'SELECT' } + } + ) + return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0 } catch (error) { log.error('Count by plan number error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 @@ -725,8 +893,18 @@ export class DiscreteMaterialPlanDAO { AND MaterialName IS NOT NULL ` - const result = await dbService.query(sqlString, batch) - allNames.push(...result.rows.map((row) => row.MaterialName as string).filter(Boolean)) + const result = await trackDuration(async () => await dbService.query(sqlString, batch), { + operationName: 'DiscreteMaterialPlanDAO.getUniqueMaterialNames', + context: { + tableName, + operationType: 'SELECT', + batchNumber: Math.floor(i / batchSize) + 1, + batchSize: batch.length + } + }) + allNames.push( + ...result.result.rows.map((row) => row.MaterialName as string).filter(Boolean) + ) } return allNames @@ -737,11 +915,18 @@ export class DiscreteMaterialPlanDAO { WHERE MaterialName IS NOT NULL ` - const result = await dbService.query(sqlString) - return result.rows.map((row) => row.MaterialName as string).filter(Boolean) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'DiscreteMaterialPlanDAO.getUniqueMaterialNames', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.map((row) => row.MaterialName as string).filter(Boolean) } } catch (error) { log.error('Get unique material names error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), + recordCount: sourceNumbers?.length || 0, error: error instanceof Error ? error.message : String(error) }) return [] @@ -767,10 +952,16 @@ export class DiscreteMaterialPlanDAO { FROM ${tableName} ` - const result = await dbService.query(sqlString) - return result.rows.length > 0 ? result.rows[0] : {} + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'DiscreteMaterialPlanDAO.getStatistics', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.length > 0 ? result.result.rows[0] : {} } catch (error) { log.error('Get statistics error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return {} diff --git a/src/main/services/database/extractor-operation-history-dao.ts b/src/main/services/database/extractor-operation-history-dao.ts index a19df05..20957b6 100644 --- a/src/main/services/database/extractor-operation-history-dao.ts +++ b/src/main/services/database/extractor-operation-history-dao.ts @@ -10,7 +10,7 @@ */ import { create, type IDatabaseService } from './index' -import { createLogger } from '../logger' +import { createLogger, run, getRequestId, trackDuration } from '../logger' import type { OperationHistoryRecord, BatchStats, @@ -106,15 +106,31 @@ export class ExtractorOperationHistoryDAO { records: InsertBatchRecordInput[] ): Promise { if (!records || records.length === 0) { - log.warn('No records to insert') + log.warn('No records to insert', { + batchId, + tableName: this.getTableName(), + requestId: getRequestId() + }) return false } + const requestId = getRequestId() || `insert-${Date.now()}` + try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const isSqlServer = dbService.type === 'sqlserver' + log.info('Batch records insertion started', { + tableName, + operationType: 'INSERT', + requestId, + batchId, + userId, + username, + recordCount: records.length + }) + for (const record of records) { try { if (isSqlServer) { @@ -124,13 +140,20 @@ export class ExtractorOperationHistoryDAO { VALUES (@p0, @p1, @p2, @p3, @p4, GETDATE(), 'pending') ` - await dbService.query(sqlString, [ - batchId, - userId, - username, - record.productionId || null, - record.orderNumber - ]) + await trackDuration( + async () => + await dbService.query(sqlString, [ + batchId, + userId, + username, + record.productionId || null, + record.orderNumber + ]), + { + operationName: 'ExtractorOperationHistoryDAO.insertBatchRecords', + context: { tableName, operationType: 'INSERT', batchId } + } + ) } else { const sqlString = ` INSERT INTO ${tableName} @@ -138,16 +161,26 @@ export class ExtractorOperationHistoryDAO { VALUES (?, ?, ?, ?, ?, NOW(), 'pending') ` - await dbService.query(sqlString, [ - batchId, - userId, - username, - record.productionId || null, - record.orderNumber - ]) + await trackDuration( + async () => + await dbService.query(sqlString, [ + batchId, + userId, + username, + record.productionId || null, + record.orderNumber + ]), + { + operationName: 'ExtractorOperationHistoryDAO.insertBatchRecords', + context: { tableName, operationType: 'INSERT', batchId } + } + ) } } catch (error) { log.error('Error inserting individual record', { + tableName, + operationType: 'INSERT', + requestId, batchId, orderNumber: record.orderNumber, error: error instanceof Error ? error.message : String(error) @@ -155,10 +188,21 @@ export class ExtractorOperationHistoryDAO { } } - log.info('Batch records inserted', { batchId, count: records.length }) + log.info('Batch records inserted', { + tableName, + operationType: 'INSERT', + requestId, + batchId, + count: records.length + }) return true } catch (error) { log.error('Insert batch records error', { + tableName: this.getTableName(), + operationType: 'INSERT', + requestId, + batchId, + recordCount: records.length, error: error instanceof Error ? error.message : String(error) }) return false @@ -186,12 +230,24 @@ export class ExtractorOperationHistoryDAO { ` const params = [status, batchId] - await dbService.query(sqlString, params) + const result = await trackDuration(async () => await dbService.query(sqlString, params), { + operationName: 'ExtractorOperationHistoryDAO.updateBatchStatus', + context: { tableName, operationType: 'UPDATE', batchId } + }) - log.info('Batch status updated', { batchId, status }) + log.info('Batch status updated', { + tableName, + operationType: 'UPDATE', + requestId: getRequestId(), + batchId, + status + }) return { success: true, updatedCount: 1 } } catch (error) { log.error('Update batch status error', { + tableName: this.getTableName(), + operationType: 'UPDATE', + requestId: getRequestId(), batchId, error: error instanceof Error ? error.message : String(error) }) @@ -244,11 +300,17 @@ export class ExtractorOperationHistoryDAO { params = [status, errorMessage || null, batchId, orderNumber] } - await dbService.query(sqlString, params) + await trackDuration(async () => await dbService.query(sqlString, params), { + operationName: 'ExtractorOperationHistoryDAO.updateRecordStatus', + context: { tableName, operationType: 'UPDATE', batchId } + }) return true } catch (error) { log.error('Update record status error', { + tableName: this.getTableName(), + operationType: 'UPDATE', + requestId: getRequestId(), batchId, orderNumber, error: error instanceof Error ? error.message : String(error) @@ -291,7 +353,6 @@ export class ExtractorOperationHistoryDAO { sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? ` params.push(userId) } else if (options?.usernames && options.usernames.length > 0) { - // Admin user filtering by multiple usernames using IN clause const placeholders = this.buildPlaceholders(options.usernames.length, isSqlServer) sqlString += ` WHERE Username IN (${placeholders}) ` params.push(...options.usernames) @@ -307,7 +368,6 @@ export class ExtractorOperationHistoryDAO { const safeOffset = options.offset !== undefined ? Math.floor(options.offset) : undefined if (isSqlServer) { - // SQL Server: use parameterized OFFSET/FETCH const offsetIndex = params.length if (safeOffset !== undefined) { params.push(safeOffset) @@ -320,9 +380,6 @@ export class ExtractorOperationHistoryDAO { sqlString += ` OFFSET 0 ROWS FETCH NEXT @p${offsetIndex} ROWS ONLY` } } else { - // MySQL: embed validated integer values directly. - // connection.execute() uses binary protocol prepared statements, - // which do not reliably support ? placeholders in LIMIT/OFFSET clauses. if (safeOffset !== undefined) { sqlString += ` LIMIT ${safeLimit} OFFSET ${safeOffset}` } else { @@ -331,9 +388,12 @@ export class ExtractorOperationHistoryDAO { } } - const result = await dbService.query(sqlString, params) + const result = await trackDuration(async () => await dbService.query(sqlString, params), { + operationName: 'ExtractorOperationHistoryDAO.getBatches', + context: { tableName, operationType: 'SELECT', userId } + }) - return result.rows.map((row) => ({ + return result.result.rows.map((row) => ({ batchId: row.BatchId as string, userId: row.UserId as number, username: row.Username as string, @@ -346,6 +406,10 @@ export class ExtractorOperationHistoryDAO { })) } catch (error) { log.error('Get batches error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), + userId, error: error instanceof Error ? error.message : String(error) }) return [] @@ -381,9 +445,12 @@ export class ExtractorOperationHistoryDAO { ORDER BY ID ` - const result = await dbService.query(sqlString, [batchId]) + const result = await trackDuration(async () => await dbService.query(sqlString, [batchId]), { + operationName: 'ExtractorOperationHistoryDAO.getBatchDetails', + context: { tableName, operationType: 'SELECT', batchId } + }) - return result.rows.map((row) => ({ + return result.result.rows.map((row) => ({ id: row.ID as number, batchId: row.BatchId as string, userId: row.UserId as number, @@ -397,6 +464,9 @@ export class ExtractorOperationHistoryDAO { })) } catch (error) { log.error('Get batch details error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), batchId, error: error instanceof Error ? error.message : String(error) }) @@ -432,13 +502,16 @@ export class ExtractorOperationHistoryDAO { GROUP BY BatchId, UserId, Username ` - const result = await dbService.query(sqlString, [batchId]) + const result = await trackDuration(async () => await dbService.query(sqlString, [batchId]), { + operationName: 'ExtractorOperationHistoryDAO.getBatchStats', + context: { tableName, operationType: 'SELECT', batchId } + }) - if (result.rows.length === 0) { + if (result.result.rows.length === 0) { return null } - const row = result.rows[0] + const row = result.result.rows[0] return { batchId: row.BatchId as string, userId: row.UserId as number, @@ -452,6 +525,9 @@ export class ExtractorOperationHistoryDAO { } } catch (error) { log.error('Get batch stats error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), batchId, error: error instanceof Error ? error.message : String(error) }) @@ -473,6 +549,8 @@ export class ExtractorOperationHistoryDAO { requestingUserId: number, isAdmin: boolean ): Promise<{ success: boolean; error?: string }> { + const requestId = getRequestId() || `delete-${Date.now()}` + try { const dbService = await this.getDatabaseService() const tableName = this.getTableName() @@ -497,12 +575,24 @@ export class ExtractorOperationHistoryDAO { WHERE BatchId = ${placeholder} ` - const result = await dbService.query(sqlString, [batchId]) + const result = await trackDuration(async () => await dbService.query(sqlString, [batchId]), { + operationName: 'ExtractorOperationHistoryDAO.deleteBatch', + context: { tableName, operationType: 'DELETE', batchId, requestingUserId } + }) - log.info('Batch deleted', { batchId, rowCount: result.rowCount }) + log.info('Batch deleted', { + tableName, + operationType: 'DELETE', + requestId, + batchId, + rowCount: result.result.rowCount + }) return { success: true } } catch (error) { log.error('Delete batch error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId, batchId, error: error instanceof Error ? error.message : String(error) }) @@ -530,10 +620,16 @@ export class ExtractorOperationHistoryDAO { WHERE UserId = ${placeholder} ` - const result = await dbService.query(sqlString, [userId]) - return result.rowCount + const result = await trackDuration(async () => await dbService.query(sqlString, [userId]), { + operationName: 'ExtractorOperationHistoryDAO.deleteByUser', + context: { tableName, operationType: 'DELETE', userId } + }) + return result.result.rowCount } catch (error) { log.error('Delete by user error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: getRequestId(), userId, error: error instanceof Error ? error.message : String(error) }) @@ -561,10 +657,16 @@ export class ExtractorOperationHistoryDAO { WHERE BatchId = ${placeholder} ` - const result = await dbService.query(sqlString, [batchId]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await trackDuration(async () => await dbService.query(sqlString, [batchId]), { + operationName: 'ExtractorOperationHistoryDAO.batchExists', + context: { tableName, operationType: 'SELECT', batchId } + }) + return result.result.rows.length > 0 && (result.result.rows[0].count as number) > 0 } catch (error) { log.error('Batch exists error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), batchId, error: error instanceof Error ? error.message : String(error) }) @@ -595,16 +697,21 @@ export class ExtractorOperationHistoryDAO { sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? ` params.push(userId) } else if (usernames && usernames.length > 0) { - // Admin user filtering by multiple usernames using IN clause const placeholders = this.buildPlaceholders(usernames.length, isSqlServer) sqlString += ` WHERE Username IN (${placeholders}) ` params.push(...usernames) } - const result = await dbService.query(sqlString, params) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await trackDuration(async () => await dbService.query(sqlString, params), { + operationName: 'ExtractorOperationHistoryDAO.countBatches', + context: { tableName, operationType: 'SELECT', userId } + }) + return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0 } catch (error) { log.error('Count batches error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 diff --git a/src/main/services/database/materials-to-be-deleted-dao.ts b/src/main/services/database/materials-to-be-deleted-dao.ts index c5825c6..22f8f99 100644 --- a/src/main/services/database/materials-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-to-be-deleted-dao.ts @@ -9,7 +9,7 @@ */ import { create, type IDatabaseService } from './index' -import { createLogger } from '../logger' +import { createLogger, run, getRequestId, trackDuration } from '../logger' const log = createLogger('MaterialsToBeDeletedDAO') @@ -100,7 +100,11 @@ export class MaterialsToBeDeletedDAO { */ async upsertMaterial(materialCode: string, managerName: string): Promise { if (!materialCode || !materialCode.trim()) { - log.error('MaterialCode cannot be empty') + log.error('MaterialCode cannot be empty', { + tableName: this.getTableName(), + operationType: 'UPSERT', + requestId: getRequestId() + }) return false } @@ -112,7 +116,6 @@ export class MaterialsToBeDeletedDAO { const isSqlServer = dbService.type === 'sqlserver' if (isSqlServer) { - // SQL Server MERGE statement const sqlString = ` MERGE ${tableName} AS target USING (VALUES (@p0, @p1)) AS source (MaterialCode, ManagerName) @@ -121,21 +124,30 @@ export class MaterialsToBeDeletedDAO { WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); ` - await dbService.query(sqlString, [code, manager]) + await trackDuration(async () => await dbService.query(sqlString, [code, manager]), { + operationName: 'MaterialsToBeDeletedDAO.upsertMaterial', + context: { tableName, operationType: 'MERGE' } + }) } else { - // MySQL ON DUPLICATE KEY UPDATE const sqlString = ` INSERT INTO ${tableName} (MaterialCode, ManagerName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ` - await dbService.query(sqlString, [code, manager]) + await trackDuration(async () => await dbService.query(sqlString, [code, manager]), { + operationName: 'MaterialsToBeDeletedDAO.upsertMaterial', + context: { tableName, operationType: 'INSERT' } + }) } return true } catch (error) { log.error('Upsert material error', { + tableName: this.getTableName(), + operationType: 'UPSERT', + requestId: getRequestId(), + materialCode: materialCode.trim(), error: error instanceof Error ? error.message : String(error) }) return false @@ -154,6 +166,7 @@ export class MaterialsToBeDeletedDAO { return { total: 0, success: 0, failed: 0 } } + const batchId = getRequestId() || `upsert-${Date.now()}` const stats: UpsertStats = { total: materials.length, success: 0, @@ -165,6 +178,14 @@ export class MaterialsToBeDeletedDAO { const tableName = this.getTableName() const isSqlServer = dbService.type === 'sqlserver' + log.info('Batch upsert started', { + tableName, + operationType: 'UPSERT', + requestId: batchId, + totalRecords: materials.length, + dbType: dbService.type + }) + for (const material of materials) { const materialCode = material.materialCode?.trim() const managerName = material.managerName?.trim() || '' @@ -176,7 +197,6 @@ export class MaterialsToBeDeletedDAO { try { if (isSqlServer) { - // SQL Server MERGE statement const sqlString = ` MERGE ${tableName} AS target USING (VALUES (@p0, @p1)) AS source (MaterialCode, ManagerName) @@ -185,29 +205,56 @@ export class MaterialsToBeDeletedDAO { WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); ` - await dbService.query(sqlString, [materialCode, managerName || null]) + await trackDuration( + async () => await dbService.query(sqlString, [materialCode, managerName || null]), + { + operationName: 'MaterialsToBeDeletedDAO.upsertBatch', + context: { tableName, operationType: 'MERGE', batchId } + } + ) } else { - // MySQL ON DUPLICATE KEY UPDATE const sqlString = ` INSERT INTO ${tableName} (MaterialCode, ManagerName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ` - await dbService.query(sqlString, [materialCode, managerName || null]) + await trackDuration( + async () => await dbService.query(sqlString, [materialCode, managerName || null]), + { + operationName: 'MaterialsToBeDeletedDAO.upsertBatch', + context: { tableName, operationType: 'INSERT', batchId } + } + ) } stats.success++ } catch (error) { log.error('Error upserting material', { + tableName, + operationType: 'UPSERT', + requestId: batchId, materialCode, error: error instanceof Error ? error.message : String(error) }) stats.failed++ } } + + log.info('Batch upsert completed', { + tableName, + operationType: 'UPSERT', + requestId: batchId, + success: stats.success, + failed: stats.failed, + total: stats.total + }) } catch (error) { log.error('Batch upsert error', { + tableName: this.getTableName(), + operationType: 'UPSERT', + requestId: batchId, + totalRecords: materials.length, error: error instanceof Error ? error.message : String(error) }) stats.failed = stats.total - stats.success @@ -279,10 +326,16 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode IS NOT NULL ` - const result = await dbService.query(sqlString) - return new Set(result.rows.map((row) => row.MaterialCode as string).filter(Boolean)) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsToBeDeletedDAO.getAllMaterialCodes', + context: { tableName, operationType: 'SELECT' } + }) + return new Set(result.result.rows.map((row) => row.MaterialCode as string).filter(Boolean)) } catch (error) { log.error('Get all material codes error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return new Set() @@ -305,14 +358,20 @@ export class MaterialsToBeDeletedDAO { ORDER BY ManagerName, MaterialCode ` - const result = await dbService.query(sqlString) - return result.rows.map((row) => ({ + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsToBeDeletedDAO.getAllRecords', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.map((row) => ({ id: row.ID as number, materialCode: row.MaterialCode as string, managerName: row.ManagerName as string })) } catch (error) { log.error('Get all records error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -338,14 +397,23 @@ export class MaterialsToBeDeletedDAO { ORDER BY MaterialCode ` - const result = await dbService.query(sqlString, [managerName]) - return result.rows.map((row) => ({ + const result = await trackDuration( + async () => await dbService.query(sqlString, [managerName]), + { + operationName: 'MaterialsToBeDeletedDAO.getMaterialsByManager', + context: { tableName, operationType: 'SELECT' } + } + ) + return result.result.rows.map((row) => ({ id: row.ID as number, materialCode: row.MaterialCode as string, managerName: row.ManagerName as string })) } catch (error) { log.error('Get materials by manager error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -368,10 +436,16 @@ export class MaterialsToBeDeletedDAO { ORDER BY ManagerName ` - const result = await dbService.query(sqlString) - return result.rows.map((row) => row.ManagerName as string).filter(Boolean) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsToBeDeletedDAO.getManagers', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.map((row) => row.ManagerName as string).filter(Boolean) } catch (error) { log.error('Get managers error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -397,13 +471,16 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode = ${placeholder} ` - const result = await dbService.query(sqlString, [code]) + const result = await trackDuration(async () => await dbService.query(sqlString, [code]), { + operationName: 'MaterialsToBeDeletedDAO.getRecordByMaterialCode', + context: { tableName, operationType: 'SELECT' } + }) - if (result.rows.length === 0) { + if (result.result.rows.length === 0) { return null } - const row = result.rows[0] + const row = result.result.rows[0] return { id: row.ID as number, materialCode: row.MaterialCode as string, @@ -411,6 +488,9 @@ export class MaterialsToBeDeletedDAO { } } catch (error) { log.error('Get record by material code error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return null @@ -437,10 +517,16 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode = ${placeholder} ` - const result = await dbService.query(sqlString, [code]) - return result.rowCount > 0 + const result = await trackDuration(async () => await dbService.query(sqlString, [code]), { + operationName: 'MaterialsToBeDeletedDAO.deleteByMaterialCode', + context: { tableName, operationType: 'DELETE' } + }) + return result.result.rowCount > 0 } catch (error) { log.error('Delete by material code error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return false @@ -464,10 +550,19 @@ export class MaterialsToBeDeletedDAO { WHERE ManagerName = ${placeholder} ` - const result = await dbService.query(sqlString, [managerName]) - return result.rowCount + const result = await trackDuration( + async () => await dbService.query(sqlString, [managerName]), + { + operationName: 'MaterialsToBeDeletedDAO.deleteByManager', + context: { tableName, operationType: 'DELETE' } + } + ) + return result.result.rowCount } catch (error) { log.error('Delete by manager error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 @@ -484,10 +579,16 @@ export class MaterialsToBeDeletedDAO { const tableName = this.getTableName() const sqlString = `DELETE FROM ${tableName}` - const result = await dbService.query(sqlString) - return result.rowCount + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsToBeDeletedDAO.deleteAllMaterials', + context: { tableName, operationType: 'DELETE' } + }) + return result.result.rowCount } catch (error) { log.error('Delete all materials error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 @@ -504,6 +605,7 @@ export class MaterialsToBeDeletedDAO { return 0 } + const batchId = getRequestId() || `delete-${Date.now()}` let totalDeleted = 0 const batchSize = 1000 @@ -511,9 +613,20 @@ export class MaterialsToBeDeletedDAO { const dbService = await this.getDatabaseService() const tableName = this.getTableName() const isSqlServer = dbService.type === 'sqlserver' + const totalBatches = Math.ceil(materialCodes.length / batchSize) + + log.info('Batch delete started', { + tableName, + operationType: 'DELETE', + requestId: batchId, + totalRecords: materialCodes.length, + batchSize, + totalBatches + }) for (let i = 0; i < materialCodes.length; i += batchSize) { const batch = materialCodes.slice(i, i + batchSize) + const batchNumber = Math.floor(i / batchSize) + 1 const placeholders = this.buildPlaceholders(batch.length, isSqlServer) const sqlString = ` @@ -521,14 +634,41 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode IN (${placeholders}) ` - const result = await dbService.query( - sqlString, - batch.map((c) => c.trim()) + const result = await trackDuration( + async () => + await dbService.query( + sqlString, + batch.map((c) => c.trim()) + ), + { + operationName: 'MaterialsToBeDeletedDAO.deleteByMaterialCodes', + context: { + tableName, + operationType: 'DELETE', + batchId, + batchNumber, + totalBatches, + batchSize: batch.length + } + } ) - totalDeleted += result.rowCount + totalDeleted += result.result.rowCount } + + log.info('Batch delete completed', { + tableName, + operationType: 'DELETE', + requestId: batchId, + totalDeleted, + totalRecords: materialCodes.length + }) } catch (error) { log.error('Delete by material codes error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: batchId, + totalDeleted, + recordCount: materialCodes.length, error: error instanceof Error ? error.message : String(error) }) } @@ -557,10 +697,16 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode = ${placeholder} ` - const result = await dbService.query(sqlString, [code]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await trackDuration(async () => await dbService.query(sqlString, [code]), { + operationName: 'MaterialsToBeDeletedDAO.materialExists', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.length > 0 && (result.result.rows[0].count as number) > 0 } catch (error) { log.error('Material exists error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return false @@ -577,11 +723,17 @@ export class MaterialsToBeDeletedDAO { const tableName = this.getTableName() const sqlString = `SELECT COUNT(*) as count FROM ${tableName}` - const result = await dbService.query(sqlString) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsToBeDeletedDAO.countAll', + context: { tableName, operationType: 'SELECT' } + }) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0 } catch (error) { log.error('Count all error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 @@ -606,10 +758,19 @@ export class MaterialsToBeDeletedDAO { WHERE ManagerName = ${placeholder} ` - const result = await dbService.query(sqlString, [managerName]) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await trackDuration( + async () => await dbService.query(sqlString, [managerName]), + { + operationName: 'MaterialsToBeDeletedDAO.countByManager', + context: { tableName, operationType: 'SELECT' } + } + ) + return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0 } catch (error) { log.error('Count by manager error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return 0 @@ -634,8 +795,11 @@ export class MaterialsToBeDeletedDAO { WHERE MaterialCode IS NOT NULL ` - const statsResult = await dbService.query(statsSql) - const stats = statsResult.rows[0] || {} + const statsResult = await trackDuration(async () => await dbService.query(statsSql), { + operationName: 'MaterialsToBeDeletedDAO.getStatistics', + context: { tableName, operationType: 'SELECT' } + }) + const stats = statsResult.result.rows[0] || {} // Get materials per manager const managerSql = ` @@ -646,8 +810,11 @@ export class MaterialsToBeDeletedDAO { ORDER BY count DESC ` - const managerResult = await dbService.query(managerSql) - const materialsPerManager = managerResult.rows.map((row) => ({ + const managerResult = await trackDuration(async () => await dbService.query(managerSql), { + operationName: 'MaterialsToBeDeletedDAO.getStatistics.managers', + context: { tableName, operationType: 'SELECT' } + }) + const materialsPerManager = managerResult.result.rows.map((row) => ({ [row.ManagerName as string]: row.count as number })) @@ -658,6 +825,9 @@ export class MaterialsToBeDeletedDAO { } } catch (error) { log.error('Get statistics error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return { diff --git a/src/main/services/database/materials-type-to-be-deleted-dao.ts b/src/main/services/database/materials-type-to-be-deleted-dao.ts index 0e68b1c..69ce458 100644 --- a/src/main/services/database/materials-type-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-type-to-be-deleted-dao.ts @@ -6,7 +6,7 @@ */ import { create, type IDatabaseService } from './index' -import { createLogger } from '../logger' +import { createLogger, run, getRequestId, trackDuration } from '../logger' const log = createLogger('MaterialsTypeToBeDeletedDAO') @@ -87,14 +87,20 @@ export class MaterialsTypeToBeDeletedDAO { ORDER BY ManagerName, MaterialName ` - const result = await dbService.query(sqlString) - return result.rows.map((row) => ({ + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsTypeToBeDeletedDAO.getAllMaterials', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.map((row) => ({ id: row.ID as number, materialName: row.MaterialName as string, managerName: row.ManagerName as string })) } catch (error) { log.error('Get all materials error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -120,14 +126,23 @@ export class MaterialsTypeToBeDeletedDAO { ORDER BY MaterialName ` - const result = await dbService.query(sqlString, [managerName]) - return result.rows.map((row) => ({ + const result = await trackDuration( + async () => await dbService.query(sqlString, [managerName]), + { + operationName: 'MaterialsTypeToBeDeletedDAO.getMaterialsByManager', + context: { tableName, operationType: 'SELECT' } + } + ) + return result.result.rows.map((row) => ({ id: row.ID as number, materialName: row.MaterialName as string, managerName: row.ManagerName as string })) } catch (error) { log.error('Get materials by manager error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -150,10 +165,16 @@ export class MaterialsTypeToBeDeletedDAO { ORDER BY ManagerName ` - const result = await dbService.query(sqlString) - return result.rows.map((row) => row.ManagerName as string).filter(Boolean) + const result = await trackDuration(async () => await dbService.query(sqlString), { + operationName: 'MaterialsTypeToBeDeletedDAO.getManagers', + context: { tableName, operationType: 'SELECT' } + }) + return result.result.rows.map((row) => row.ManagerName as string).filter(Boolean) } catch (error) { log.error('Get managers error', { + tableName: this.getTableName(), + operationType: 'SELECT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return [] @@ -170,7 +191,11 @@ export class MaterialsTypeToBeDeletedDAO { */ async upsertMaterial(materialName: string, managerName: string): Promise { if (!materialName || !materialName.trim()) { - log.error('MaterialName cannot be empty') + log.error('MaterialName cannot be empty', { + tableName: this.getTableName(), + operationType: 'UPSERT', + requestId: getRequestId() + }) return false } @@ -182,7 +207,6 @@ export class MaterialsTypeToBeDeletedDAO { const isSqlServer = dbService.type === 'sqlserver' if (isSqlServer) { - // SQL Server MERGE statement const sqlString = ` MERGE ${tableName} AS target USING (VALUES (@p0, @p1)) AS source (MaterialName, ManagerName) @@ -191,21 +215,29 @@ export class MaterialsTypeToBeDeletedDAO { WHEN NOT MATCHED THEN INSERT (MaterialName, ManagerName) VALUES (source.MaterialName, source.ManagerName); ` - await dbService.query(sqlString, [name, manager]) + await trackDuration(async () => await dbService.query(sqlString, [name, manager]), { + operationName: 'MaterialsTypeToBeDeletedDAO.upsertMaterial', + context: { tableName, operationType: 'MERGE' } + }) } else { - // MySQL ON DUPLICATE KEY UPDATE const sqlString = ` INSERT INTO ${tableName} (MaterialName, ManagerName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ` - await dbService.query(sqlString, [name, manager]) + await trackDuration(async () => await dbService.query(sqlString, [name, manager]), { + operationName: 'MaterialsTypeToBeDeletedDAO.upsertMaterial', + context: { tableName, operationType: 'INSERT' } + }) } return true } catch (error) { log.error('Upsert material error', { + tableName: this.getTableName(), + operationType: 'UPSERT', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return false @@ -247,10 +279,16 @@ export class MaterialsTypeToBeDeletedDAO { params = [name] } - const result = await dbService.query(sqlString, params) - return result.rowCount > 0 + const result = await trackDuration(async () => await dbService.query(sqlString, params), { + operationName: 'MaterialsTypeToBeDeletedDAO.deleteMaterial', + context: { tableName, operationType: 'DELETE' } + }) + return result.result.rowCount > 0 } catch (error) { log.error('Delete material error', { + tableName: this.getTableName(), + operationType: 'DELETE', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return false @@ -284,29 +322,46 @@ export class MaterialsTypeToBeDeletedDAO { SET MaterialName = @p0, ManagerName = @p1 WHERE MaterialName = @p2 AND ManagerName = @p3 ` - const result = await dbService.query(sqlString, [ - newName.trim(), - newManager.trim(), - oldName.trim(), - oldManager.trim() - ]) - return result.rowCount > 0 + const result = await trackDuration( + async () => + await dbService.query(sqlString, [ + newName.trim(), + newManager.trim(), + oldName.trim(), + oldManager.trim() + ]), + { + operationName: 'MaterialsTypeToBeDeletedDAO.updateMaterial', + context: { tableName, operationType: 'UPDATE' } + } + ) + return result.result.rowCount > 0 } else { const sqlString = ` UPDATE ${tableName} SET MaterialName = ?, ManagerName = ? WHERE MaterialName = ? AND ManagerName = ? ` - const result = await dbService.query(sqlString, [ - newName.trim(), - newManager.trim(), - oldName.trim(), - oldManager.trim() - ]) - return result.rowCount > 0 + const result = await trackDuration( + async () => + await dbService.query(sqlString, [ + newName.trim(), + newManager.trim(), + oldName.trim(), + oldManager.trim() + ]), + { + operationName: 'MaterialsTypeToBeDeletedDAO.updateMaterial', + context: { tableName, operationType: 'UPDATE' } + } + ) + return result.result.rowCount > 0 } } catch (error) { log.error('Update material error', { + tableName: this.getTableName(), + operationType: 'UPDATE', + requestId: getRequestId(), error: error instanceof Error ? error.message : String(error) }) return false @@ -323,9 +378,24 @@ export class MaterialsTypeToBeDeletedDAO { async upsertBatch( request: MaterialTypeBatchRequest ): Promise<{ total: number; success: number; failed: number }> { + const batchId = getRequestId() || `batch-${Date.now()}` const stats = { total: 0, success: 0, failed: 0 } try { + const tableName = this.getTableName() + const totalOperations = + request.toInsert.length + request.toUpdate.length + request.toDelete.length + + log.info('Batch upsert started', { + tableName, + operationType: 'BATCH', + requestId: batchId, + totalOperations, + inserts: request.toInsert.length, + updates: request.toUpdate.length, + deletes: request.toDelete.length + }) + // Process inserts for (const record of request.toInsert) { stats.total++ @@ -355,9 +425,24 @@ export class MaterialsTypeToBeDeletedDAO { else stats.failed++ } + log.info('Batch upsert completed', { + tableName, + operationType: 'BATCH', + requestId: batchId, + success: stats.success, + failed: stats.failed, + total: stats.total + }) + return stats } catch (error) { log.error('Batch upsert error', { + tableName: this.getTableName(), + operationType: 'BATCH', + requestId: batchId, + total: stats.total, + success: stats.success, + failed: stats.failed, error: error instanceof Error ? error.message : String(error) }) return stats diff --git a/src/main/services/rustfs/rustfs-service.ts b/src/main/services/rustfs/rustfs-service.ts index a9a556e..68ca4e1 100644 --- a/src/main/services/rustfs/rustfs-service.ts +++ b/src/main/services/rustfs/rustfs-service.ts @@ -15,7 +15,7 @@ import { type GetObjectCommandInput, type DeleteObjectCommandInput } from '@aws-sdk/client-s3' -import { createLogger } from '../logger' +import { createLogger, run, trackDuration } from '../logger' import type { RustfsConfig } from '../../types/config.schema' import * as fs from 'fs' import * as path from 'path' @@ -77,6 +77,7 @@ export class RustfsService { try { // Validate configuration if (!this.config.enabled) { + log.warn('RustFS upload skipped - disabled in config', { filePath, key }) return { success: false, key, @@ -86,6 +87,7 @@ export class RustfsService { // Check if file exists if (!fs.existsSync(filePath)) { + log.warn('RustFS upload skipped - file not found', { filePath, key }) return { success: false, key, @@ -103,7 +105,9 @@ export class RustfsService { filePath, key, contentType: mimeType, - size: fileContent.length + fileSize: fileContent.length, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) const input: PutObjectCommandInput = { @@ -116,9 +120,12 @@ export class RustfsService { const command = new PutObjectCommand(input) const response = await this.client.send(command) - log.info('File uploaded successfully', { + log.info('File uploaded successfully to RustFS', { key, - etag: response.ETag + fileSize: fileContent.length, + etag: response.ETag, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -131,7 +138,9 @@ export class RustfsService { log.error('Failed to upload file to RustFS', { filePath, key, - error: errorMessage + error: errorMessage, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -151,6 +160,10 @@ export class RustfsService { async uploadString(content: string, key: string, contentType?: string): Promise { try { if (!this.config.enabled) { + log.warn('RustFS string upload skipped - disabled in config', { + key, + endpoint: this.config.endpoint + }) return { success: false, key, @@ -163,7 +176,9 @@ export class RustfsService { log.info('Uploading string content to RustFS', { key, contentType: mimeType, - size: content.length + fileSize: content.length, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) const input: PutObjectCommandInput = { @@ -176,9 +191,12 @@ export class RustfsService { const command = new PutObjectCommand(input) const response = await this.client.send(command) - log.info('String content uploaded successfully', { + log.info('String content uploaded successfully to RustFS', { key, - etag: response.ETag + fileSize: content.length, + etag: response.ETag, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -190,7 +208,9 @@ export class RustfsService { const errorMessage = error instanceof Error ? error.message : 'Unknown upload error' log.error('Failed to upload string to RustFS', { key, - error: errorMessage + error: errorMessage, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -208,6 +228,10 @@ export class RustfsService { async downloadFile(key: string): Promise { try { if (!this.config.enabled) { + log.warn('RustFS download skipped - disabled in config', { + key, + endpoint: this.config.endpoint + }) return { success: false, content: Buffer.alloc(0), @@ -215,7 +239,11 @@ export class RustfsService { } } - log.info('Downloading file from RustFS', { key }) + log.info('Downloading file from RustFS', { + key, + endpoint: this.config.endpoint, + bucket: this.config.bucket + }) const input: GetObjectCommandInput = { Bucket: this.config.bucket, @@ -232,9 +260,11 @@ export class RustfsService { const content = Buffer.concat(chunks) - log.info('File downloaded successfully', { + log.info('File downloaded successfully from RustFS', { key, - size: content.length + fileSize: content.length, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -245,7 +275,9 @@ export class RustfsService { const errorMessage = error instanceof Error ? error.message : 'Unknown download error' log.error('Failed to download file from RustFS', { key, - error: errorMessage + error: errorMessage, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -263,13 +295,21 @@ export class RustfsService { async deleteFile(key: string): Promise<{ success: boolean; error?: string }> { try { if (!this.config.enabled) { + log.warn('RustFS delete skipped - disabled in config', { + key, + endpoint: this.config.endpoint + }) return { success: false, error: 'RustFS is not enabled in configuration' } } - log.info('Deleting file from RustFS', { key }) + log.info('Deleting file from RustFS', { + key, + endpoint: this.config.endpoint, + bucket: this.config.bucket + }) const input: DeleteObjectCommandInput = { Bucket: this.config.bucket, @@ -279,7 +319,11 @@ export class RustfsService { const command = new DeleteObjectCommand(input) await this.client.send(command) - log.info('File deleted successfully', { key }) + log.info('File deleted successfully from RustFS', { + key, + endpoint: this.config.endpoint, + bucket: this.config.bucket + }) return { success: true @@ -288,7 +332,9 @@ export class RustfsService { const errorMessage = error instanceof Error ? error.message : 'Unknown delete error' log.error('Failed to delete file from RustFS', { key, - error: errorMessage + error: errorMessage, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { @@ -341,7 +387,8 @@ export class RustfsService { try { log.info('Testing RustFS connection', { endpoint: this.config.endpoint, - bucket: this.config.bucket + bucket: this.config.bucket, + region: this.config.region }) // Try to list objects in the bucket (head bucket operation) @@ -354,7 +401,10 @@ export class RustfsService { const command = new ListObjectsV2Command(input) await this.client.send(command) - log.info('RustFS connection test successful') + log.info('RustFS connection test successful', { + endpoint: this.config.endpoint, + bucket: this.config.bucket + }) return { success: true, @@ -363,7 +413,9 @@ export class RustfsService { } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown connection error' log.error('RustFS connection test failed', { - error: errorMessage + error: errorMessage, + endpoint: this.config.endpoint, + bucket: this.config.bucket }) return { diff --git a/src/main/services/update/update-service.ts b/src/main/services/update/update-service.ts index 4bee8b5..9d2bb34 100644 --- a/src/main/services/update/update-service.ts +++ b/src/main/services/update/update-service.ts @@ -1,6 +1,6 @@ import * as fs from 'fs' import { ConfigManager } from '../config/config-manager' -import { createLogger } from '../logger' +import { createLogger, run, trackDuration } from '../logger' import type { UpdateConfig } from '../../types/config.schema' import type { UserType } from '../../types/user.types' import type { @@ -67,7 +67,10 @@ export class UpdateService { enabled, supported: supportState.supported, currentVersion: this.status.currentVersion, - currentChannel: this.status.currentChannel + currentChannel: this.status.currentChannel, + endpoint: this.config?.endpoint, + bucket: this.config?.bucket, + checkIntervalMinutes: this.config?.checkIntervalMinutes }) this.initialized = true @@ -88,17 +91,35 @@ export class UpdateService { public async getChangelog(release: DownloadReleaseRequest): Promise { this.ensureInitialized() if (!this.status.enabled || !this.storageClient) { + log.warn('Changelog request rejected - auto update disabled', { + version: release.version, + channel: release.channel + }) throw new Error('自动更新不可用') } const cacheKey = `${release.channel}:${release.version}` const cached = this.changelogCache.get(cacheKey) if (cached) { + log.debug('Changelog returned from cache', { + version: release.version, + channel: release.channel + }) return cached } + log.info('Fetching changelog from storage', { + version: release.version, + channel: release.channel, + changelogKey: release.changelogKey + }) const markdown = await this.storageClient.readText(release.changelogKey) this.changelogCache.set(cacheKey, markdown) + log.info('Changelog fetched successfully', { + version: release.version, + channel: release.channel, + cacheSize: this.changelogCache.size + }) return markdown } @@ -107,6 +128,10 @@ export class UpdateService { this.status.currentUserType = userType if (!this.status.enabled || !userType) { + log.info('Update service context cleared', { + userType, + reason: this.status.enabled ? 'user logged out' : 'auto-update disabled' + }) this.clearPolling() this.catalog = { stable: [], preview: [] } this.publishStatus({ @@ -124,10 +149,18 @@ export class UpdateService { return } + log.info('Update service user context set', { + userType, + enabled: this.status.enabled, + currentVersion: this.status.currentVersion + }) + // 启动异步更新检查,不阻塞登录流程 void this.checkForUpdates().catch((error) => { + const message = error instanceof Error ? error.message : String(error) log.warn('Async update check failed', { - error: error instanceof Error ? error.message : String(error) + userType, + error: message }) }) this.startPolling() @@ -136,6 +169,11 @@ export class UpdateService { public async checkForUpdates(): Promise { this.ensureInitialized() if (!this.status.enabled || !this.storageClient || !this.catalogService) { + log.debug('Update check skipped - service not enabled or not initialized', { + enabled: this.status.enabled, + hasStorageClient: !!this.storageClient, + hasCatalogService: !!this.catalogService + }) return this.getStatus() } @@ -147,6 +185,12 @@ export class UpdateService { try { const currentUserType = this.status.currentUserType + log.info('Checking for updates', { + userType: currentUserType, + currentVersion: this.status.currentVersion, + currentChannel: this.status.currentChannel + }) + this.catalog = await this.catalogService.loadCatalog(currentUserType) if (currentUserType === 'User') { @@ -154,10 +198,19 @@ export class UpdateService { this.publishStatus(nextStatus) if (nextStatus.phase === 'available' && nextStatus.recommendedRelease) { + const release = nextStatus.recommendedRelease + log.info('Update available for user', { + version: release.version, + channel: release.channel + }) // 异步下载,不阻塞更新检查流程 - void this.downloadRelease(nextStatus.recommendedRelease).catch((error) => { + void this.downloadRelease(release).catch((error) => { const message = error instanceof Error ? error.message : '下载更新失败' - log.warn('Async update download failed', { error: message }) + log.warn('Async update download failed', { + version: release.version, + channel: release.channel, + error: message + }) this.publishStatus({ phase: 'error', error: message, @@ -166,6 +219,10 @@ export class UpdateService { }) } } else if (currentUserType === 'Admin') { + log.info('Update check completed for admin', { + stableReleases: this.catalog.stable.length, + previewReleases: this.catalog.preview.length + }) this.publishStatus(this.catalogService.resolveAdminStatus(this.status, this.catalog)) } else { this.publishStatus({ @@ -176,7 +233,10 @@ export class UpdateService { } } catch (error) { const message = error instanceof Error ? error.message : '检查更新失败' - log.error('Failed to check for updates', { error: message }) + log.error('Failed to check for updates', { + userType: this.status.currentUserType, + error: message + }) this.publishStatus({ phase: 'error', error: message, @@ -190,9 +250,19 @@ export class UpdateService { public async downloadRelease(request: DownloadReleaseRequest): Promise { this.ensureInitialized() if (!this.status.enabled || !this.storageClient) { + log.warn('Download request rejected - auto update disabled', { + version: request.version, + channel: request.channel + }) throw new Error('自动更新不可用') } + log.info('Starting update download', { + version: request.version, + channel: request.channel, + artifactKey: request.artifactKey + }) + this.publishStatus({ phase: 'downloading', progress: 0, @@ -208,10 +278,22 @@ export class UpdateService { const hash = await this.installer.calculateSha256(downloadPath) if (hash.toLowerCase() !== request.sha256.toLowerCase()) { + log.error('Update package hash mismatch', { + version: request.version, + channel: request.channel, + expectedHash: request.sha256, + actualHash: hash + }) await fs.promises.rm(downloadPath, { force: true }) throw new Error('更新包校验失败,文件哈希不匹配') } + log.info('Update download completed and verified', { + version: request.version, + channel: request.channel, + downloadPath + }) + this.publishStatus({ phase: 'downloaded', progress: 100, @@ -233,9 +315,19 @@ export class UpdateService { const downloaded = this.status.downloadedRelease if (!this.status.enabled || !downloaded) { + log.warn('Install request rejected - no update package available', { + enabled: this.status.enabled, + hasDownloadedRelease: !!downloaded + }) throw new Error('没有可安装的更新包') } + log.info('Installing update package', { + version: downloaded.version, + channel: downloaded.channel, + localPath: downloaded.localPath + }) + this.publishStatus({ phase: 'installing', latestVersion: downloaded.version, @@ -245,6 +337,10 @@ export class UpdateService { }) await this.installer.installDownloadedRelease(downloaded) + log.info('Update installation completed', { + version: downloaded.version, + channel: downloaded.channel + }) } private ensureInitialized(): void { @@ -264,14 +360,23 @@ export class UpdateService { private startPolling(): void { this.clearPolling() if (!this.config) { + log.warn('Polling not started - no update configuration') return } + log.info('Update polling started', { + intervalMinutes: this.config.checkIntervalMinutes, + endpoint: this.config.endpoint, + bucket: this.config.bucket + }) + this.intervalHandle = setInterval( () => { this.checkForUpdates().catch((error) => { + const message = error instanceof Error ? error.message : String(error) log.warn('Periodic update check failed', { - error: error instanceof Error ? error.message : String(error) + channel: this.status.currentChannel, + error: message }) }) }, diff --git a/src/main/services/validation/validation-application-service.ts b/src/main/services/validation/validation-application-service.ts index 4fb0fdc..1e6b683 100644 --- a/src/main/services/validation/validation-application-service.ts +++ b/src/main/services/validation/validation-application-service.ts @@ -1,7 +1,7 @@ import { DiscreteMaterialPlanDAO } from '../database/discrete-material-plan-dao' import { MaterialsToBeDeletedDAO } from '../database/materials-to-be-deleted-dao' import { SqlServerService } from '../database/sql-server' -import { createLogger } from '../logger' +import { createLogger, withRequestContext, trackDuration, getRequestId } from '../logger' import type { MaterialRecordSummary, ValidationRequest, @@ -30,109 +30,202 @@ export class ValidationApplicationService { userInfo: UserInfo, senderId: number ): Promise { - let dbService: ValidationDatabaseService | null = null + return withRequestContext( + async () => { + const requestId = getRequestId() + let dbService: ValidationDatabaseService | null = null - try { - const isAdmin = userInfo.userType === 'Admin' - const username = userInfo.username + try { + const isAdmin = userInfo.userType === 'Admin' + const username = userInfo.username - log.info('Starting validation', { mode: request.mode, user: username, isAdmin }) + log.info('Starting validation workflow', { + mode: request.mode, + useSharedProductionIds: request.useSharedProductionIds, + userId: userInfo.id, + username, + isAdmin, + requestId + }) - dbService = await createValidationDatabaseService() + // Track data query duration + const dataQueryResult = await trackDuration( + async () => { + dbService = await createValidationDatabaseService() - let sourceNumbers: string[] | null = null + let sourceNumbers: string[] | null = null - if (request.mode === 'database_filtered') { - if (request.useSharedProductionIds) { - const sharedIds = sharedProductionIdsStore.get(senderId) - log.info(`Using ${sharedIds.length} shared Production IDs`) + if (request.mode === 'database_filtered') { + if (request.useSharedProductionIds) { + const sharedIds = sharedProductionIdsStore.get(senderId) + log.info(`Using ${sharedIds.length} shared Production IDs`, { + userId: userInfo.id, + mode: request.mode, + useSharedProductionIds: true + }) - if (sharedIds.length === 0) { - return this.emptyFailure( - '没有可用的共享 Production ID。请在数据提取页面输入 Production ID。' - ) + if (sharedIds.length === 0) { + return { + sourceNumbers: null, + failure: this.emptyFailure( + '没有可用的共享 Production ID。请在数据提取页面输入 Production ID。' + ) + } + } + + sourceNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) + log.info( + `Got ${sourceNumbers.length} source numbers from shared Production IDs`, + { + userId: userInfo.id, + sourceCount: sourceNumbers.length + } + ) + + if (sourceNumbers.length === 0) { + return { + sourceNumbers: null, + failure: this.emptyFailure( + '共享的 Production ID 没有找到对应的订单数据。请确保在数据提取页面输入了有效的 Production ID 并成功获取了订单数据。' + ) + } + } + } else if (request.productionIdFile) { + const inputs = readProductionIds(request.productionIdFile) + log.info(`Read ${inputs.length} inputs from file`, { + userId: userInfo.id, + fileMode: !request.useSharedProductionIds + }) + + sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService) + log.info(`Got ${sourceNumbers.length} source numbers`, { + userId: userInfo.id, + sourceCount: sourceNumbers.length + }) + + if (sourceNumbers.length === 0) { + return { + sourceNumbers: null, + failure: this.emptyFailure( + '文件中的 Production ID 没有找到对应的订单数据。请检查 Production ID 是否正确,或确保数据库中有对应的订单数据。' + ) + } + } + } + } + + const materialDao = new DiscreteMaterialPlanDAO() + let materialRecords: any[] = [] + + if (request.mode === 'database_full') { + materialRecords = await materialDao.queryAllDistinctByMaterialCode() + } else if (sourceNumbers && sourceNumbers.length > 0) { + materialRecords = await materialDao.queryBySourceNumbersDistinct(sourceNumbers) + } + + if (materialRecords.length === 0) { + return { + sourceNumbers: null, + failure: this.emptyFailure( + '未找到物料记录。请检查数据库中是否有对应订单的物料数据。' + ) + } + } + + return { sourceNumbers, materialRecords, failure: null } + }, + { + operationName: 'data-query', + message: 'Data query phase', + context: { mode: request.mode, userId: userInfo.id } + } + ) + + // Check for failure + if (dataQueryResult.result.failure) { + return dataQueryResult.result.failure } - sourceNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) - log.info(`Got ${sourceNumbers.length} source numbers from shared Production IDs`) + // Track validation duration + const validationResult = await trackDuration( + async () => { + const typeKeywords = await this.loadTypeKeywords(dbService!) + const markedCodes = await this.loadMarkedCodes(dbService!) + const results = this.buildValidationResults( + (dataQueryResult.result as any).materialRecords, + typeKeywords, + markedCodes, + { isAdmin, username } + ) - if (sourceNumbers.length === 0) { - return this.emptyFailure( - '共享的 Production ID 没有找到对应的订单数据。请确保在数据提取页面输入了有效的 Production ID 并成功获取了订单数据。' - ) + const markedCount = results.filter((result) => result.isMarkedForDeletion).length + const matchedCount = results.filter((result) => result.managerName).length + + log.info('Validation completed', { + totalRecords: results.length, + matchedCount, + markedCount, + userId: userInfo.id, + mode: request.mode + }) + + return { + success: true, + results, + stats: { + totalRecords: results.length, + matchedCount, + markedCount + } + } + }, + { + operationName: 'validation-processing', + message: 'Validation processing phase', + context: { mode: request.mode, userId: userInfo.id } + } + ) + + return validationResult.result + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('Validation workflow failed', { + error: message, + mode: request.mode, + useSharedProductionIds: request.useSharedProductionIds, + userId: userInfo.id, + username: userInfo.username, + requestId + }) + return { + success: false, + error: `Validation failed: ${message}` } - } else if (request.productionIdFile) { - const inputs = readProductionIds(request.productionIdFile) - log.info(`Read ${inputs.length} inputs from file`) - - sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService) - log.info(`Got ${sourceNumbers.length} source numbers`) - - if (sourceNumbers.length === 0) { - return this.emptyFailure( - '文件中的 Production ID 没有找到对应的订单数据。请检查 Production ID 是否正确,或确保数据库中有对应的订单数据。' - ) + } finally { + if (dbService) { + await this.disconnectQuietly(dbService) } } - } - - const materialDao = new DiscreteMaterialPlanDAO() - let materialRecords: any[] = [] - - if (request.mode === 'database_full') { - materialRecords = await materialDao.queryAllDistinctByMaterialCode() - } else if (sourceNumbers && sourceNumbers.length > 0) { - materialRecords = await materialDao.queryBySourceNumbersDistinct(sourceNumbers) - } - - if (materialRecords.length === 0) { - return this.emptyFailure('未找到物料记录。请检查数据库中是否有对应订单的物料数据。') - } - - const typeKeywords = await this.loadTypeKeywords(dbService) - const markedCodes = await this.loadMarkedCodes(dbService) - const results = this.buildValidationResults(materialRecords, typeKeywords, markedCodes, { - isAdmin, - username - }) - - const markedCount = results.filter((result) => result.isMarkedForDeletion).length - const matchedCount = results.filter((result) => result.managerName).length - - return { - success: true, - results, - stats: { - totalRecords: results.length, - matchedCount, - markedCount - } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('Validation error', { error: message }) - return { - success: false, - error: `Validation failed: ${message}` - } - } finally { - if (dbService) { - await this.disconnectQuietly(dbService) - } - } + }, + { userId: userInfo.id.toString(), operation: 'validate' } + ) } async getMaterialsByManager(managerName: string): Promise { + log.info(`Getting materials by manager: ${managerName}`) const dao = new MaterialsToBeDeletedDAO() const materials = await dao.getMaterialsByManager(managerName) const markedCodes = await dao.getAllMaterialCodes() + log.info(`Found ${materials.length} materials for manager: ${managerName}`) return this.enrichMaterials(materials, markedCodes) } async getAllMaterials(): Promise { + log.info('Getting all materials') const dao = new MaterialsToBeDeletedDAO() const materials = await dao.getAllRecords() const markedCodes = await dao.getAllMaterialCodes() + log.info(`Found ${materials.length} total materials`) return this.enrichMaterials(materials, markedCodes) } @@ -145,43 +238,70 @@ export class ValidationApplicationService { materialCodes?: string[] error?: string }> { - let dbService: ValidationDatabaseService | null = null + return withRequestContext( + async () => { + const requestId = getRequestId() + let dbService: ValidationDatabaseService | null = null - try { - const isAdmin = userInfo.userType === 'Admin' - const username = userInfo.username - log.info(`User: ${username}, isAdmin: ${isAdmin}`) + try { + const isAdmin = userInfo.userType === 'Admin' + const username = userInfo.username + log.info('Getting cleaner data', { + userId: userInfo.id, + username, + isAdmin, + requestId + }) - dbService = await createValidationDatabaseService() + dbService = await createValidationDatabaseService() - const sharedIds = sharedProductionIdsStore.get(senderId) - let orderNumbers: string[] = [] + const sharedIds = sharedProductionIdsStore.get(senderId) + let orderNumbers: string[] = [] - if (sharedIds.length > 0) { - log.info(`Using ${sharedIds.length} shared Production IDs`) - orderNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) - log.info(`Got ${orderNumbers.length} order numbers`) - } + if (sharedIds.length > 0) { + log.info(`Using ${sharedIds.length} shared Production IDs`, { + userId: userInfo.id, + sharedCount: sharedIds.length + }) + orderNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) + log.info(`Got ${orderNumbers.length} order numbers`, { + userId: userInfo.id, + orderCount: orderNumbers.length + }) + } - const materialCodes = await this.loadMaterialCodesForCleaner(dbService, username, isAdmin) + const materialCodes = await this.loadMaterialCodesForCleaner(dbService, username, isAdmin) + log.info('Cleaner data retrieved', { + userId: userInfo.id, + orderCount: orderNumbers.length, + materialCodeCount: materialCodes.length + }) - return { - success: true, - orderNumbers, - materialCodes - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - log.error('CleanerData error', { error: message }) - return { - success: false, - error: `获取清理数据失败:${message}` - } - } finally { - if (dbService) { - await this.disconnectQuietly(dbService) - } - } + return { + success: true, + orderNumbers, + materialCodes + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + log.error('CleanerData error', { + error: message, + userId: userInfo.id, + username: userInfo.username, + requestId + }) + return { + success: false, + error: `获取清理数据失败:${message}` + } + } finally { + if (dbService) { + await this.disconnectQuietly(dbService) + } + } + }, + { userId: userInfo.id.toString(), operation: 'getCleanerData' } + ) } private emptyFailure(error: string): ValidationResponse { @@ -291,6 +411,8 @@ export class ValidationApplicationService { const detailTableName = getValidationTableName('dbo_DiscreteMaterialPlanData') const enrichedMaterials: MaterialRecordSummary[] = [] + log.info(`Enriching ${materials.length} materials with details`) + for (const material of materials) { const detailResult = await this.queryMaterialDetail( dbService, @@ -309,6 +431,11 @@ export class ValidationApplicationService { }) } + log.info(`Material enrichment completed`, { + totalMaterials: materials.length, + enrichedCount: enrichedMaterials.length + }) + return enrichedMaterials } finally { if (dbService) { @@ -363,7 +490,11 @@ export class ValidationApplicationService { ` ) const materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean) - log.info(`Admin user: got ${materialCodes.length} materials`) + log.info(`Admin user: got ${materialCodes.length} materials`, { + userId: username, + isAdmin: true, + materialCount: materialCodes.length + }) return materialCodes } @@ -382,7 +513,11 @@ export class ValidationApplicationService { const materialCodes = result.rows .map((row: Record) => row.MaterialCode as string) .filter(Boolean) - log.info(`Regular user: got ${materialCodes.length} materials`) + log.info(`Regular user: got ${materialCodes.length} materials`, { + userId: username, + isAdmin: false, + materialCount: materialCodes.length + }) return materialCodes } @@ -395,7 +530,11 @@ export class ValidationApplicationService { [username] ) const materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean) - log.info(`Regular user: got ${materialCodes.length} materials`) + log.info(`Regular user: got ${materialCodes.length} materials`, { + userId: username, + isAdmin: false, + materialCount: materialCodes.length + }) return materialCodes }