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