refactor: move ipc orchestration into application services
This commit is contained in:
@@ -11,9 +11,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { SessionManager } from '../services/user/session-manager'
|
|
||||||
import { createLogger } from '../services/logger'
|
|
||||||
import { logAudit } from '../services/logger/audit-logger'
|
|
||||||
import type { UserInfo } from '../types/user.types'
|
import type { UserInfo } from '../types/user.types'
|
||||||
import type {
|
import type {
|
||||||
CurrentUserResponse,
|
CurrentUserResponse,
|
||||||
@@ -23,212 +20,59 @@ import type {
|
|||||||
UserSelectionResponse
|
UserSelectionResponse
|
||||||
} from '../types/auth-ipc.types'
|
} from '../types/auth-ipc.types'
|
||||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||||
import { ValidationError } from '../types/errors'
|
|
||||||
import { withErrorHandling, type IpcResult } from './index'
|
import { withErrorHandling, type IpcResult } from './index'
|
||||||
import { UpdateService } from '../services/update/update-service'
|
import { AuthApplicationService } from '../services/auth/auth-application-service'
|
||||||
|
|
||||||
const log = createLogger('AuthHandler')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register IPC handlers for user authentication
|
* Register IPC handlers for user authentication
|
||||||
*/
|
*/
|
||||||
export function registerAuthHandlers(): void {
|
export function registerAuthHandlers(): void {
|
||||||
const sessionManager = SessionManager.getInstance()
|
const authService = new AuthApplicationService()
|
||||||
const updateService = UpdateService.getInstance()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get computer name
|
|
||||||
*/
|
|
||||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise<IpcResult<string>> => {
|
ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise<IpcResult<string>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => authService.getComputerName(), 'auth:getComputerName')
|
||||||
const os = await import('os')
|
|
||||||
return os.hostname()
|
|
||||||
}, 'auth:getComputerName')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Silent login by computer name
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.AUTH_SILENT_LOGIN,
|
IPC_CHANNELS.AUTH_SILENT_LOGIN,
|
||||||
async (): Promise<IpcResult<SilentLoginResponse>> => {
|
async (): Promise<IpcResult<SilentLoginResponse>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => authService.silentLogin(), 'auth:silentLogin')
|
||||||
log.info('Attempting silent login')
|
|
||||||
const success = await sessionManager.loginByComputerName()
|
|
||||||
const userInfo = sessionManager.getUserInfo()
|
|
||||||
|
|
||||||
if (success && userInfo) {
|
|
||||||
await updateService.setUserContext(userInfo.userType)
|
|
||||||
|
|
||||||
// Check if admin needs user selection
|
|
||||||
const requiresUserSelection = userInfo.userType === 'Admin'
|
|
||||||
|
|
||||||
log.info('Silent login successful', {
|
|
||||||
username: userInfo.username,
|
|
||||||
userType: userInfo.userType,
|
|
||||||
requiresUserSelection
|
|
||||||
})
|
|
||||||
|
|
||||||
// Audit log: LOGIN success (non-blocking)
|
|
||||||
const os = await import('os')
|
|
||||||
logAudit('LOGIN', String(userInfo.id), {
|
|
||||||
username: userInfo.username,
|
|
||||||
computerName: os.hostname(),
|
|
||||||
resource: 'ERP_SYSTEM',
|
|
||||||
status: 'success',
|
|
||||||
metadata: { loginType: 'silent', userType: userInfo.userType }
|
|
||||||
}).catch((err) => log.warn('Failed to write audit log', { err }))
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
userInfo,
|
|
||||||
requiresUserSelection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateService.setUserContext(null)
|
|
||||||
throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT')
|
|
||||||
}, 'auth:silentLogin')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Login with username and password
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.AUTH_LOGIN,
|
IPC_CHANNELS.AUTH_LOGIN,
|
||||||
async (_event, request: LoginRequest): Promise<IpcResult<LoginResponse>> => {
|
async (_event, request: LoginRequest): Promise<IpcResult<LoginResponse>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(
|
||||||
const { username, password } = request
|
async () => authService.login(request.username, request.password),
|
||||||
|
'auth:login'
|
||||||
if (!username || !password) {
|
)
|
||||||
log.warn('Login attempt with missing credentials')
|
|
||||||
throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED')
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Login attempt', { username })
|
|
||||||
const success = await sessionManager.login(username, password)
|
|
||||||
const userInfo = sessionManager.getUserInfo()
|
|
||||||
|
|
||||||
if (success && userInfo) {
|
|
||||||
log.info('Login successful', { username, userType: userInfo.userType })
|
|
||||||
await updateService.setUserContext(userInfo.userType)
|
|
||||||
|
|
||||||
// Audit log: LOGIN success (non-blocking)
|
|
||||||
const os = await import('os')
|
|
||||||
logAudit('LOGIN', String(userInfo.id), {
|
|
||||||
username: userInfo.username,
|
|
||||||
computerName: os.hostname(),
|
|
||||||
resource: 'ERP_SYSTEM',
|
|
||||||
status: 'success',
|
|
||||||
metadata: { loginType: 'credentials', userType: userInfo.userType }
|
|
||||||
}).catch((err) => log.warn('Failed to write audit log', { err }))
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
userInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit log: LOGIN failure (non-blocking)
|
|
||||||
const os = await import('os')
|
|
||||||
logAudit('LOGIN', '0', {
|
|
||||||
username,
|
|
||||||
computerName: os.hostname(),
|
|
||||||
resource: 'ERP_SYSTEM',
|
|
||||||
status: 'failure',
|
|
||||||
metadata: { loginType: 'credentials', reason: 'invalid_credentials' }
|
|
||||||
}).catch((err) => log.warn('Failed to write audit log', { err }))
|
|
||||||
|
|
||||||
log.warn('Login failed - invalid credentials', { username })
|
|
||||||
await updateService.setUserContext(null)
|
|
||||||
throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT')
|
|
||||||
}, 'auth:login')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout
|
|
||||||
*/
|
|
||||||
ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise<IpcResult<void>> => {
|
ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise<IpcResult<void>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => authService.logout(), 'auth:logout')
|
||||||
const userInfo = sessionManager.getUserInfo()
|
|
||||||
log.info('User logout', { username: userInfo?.username })
|
|
||||||
|
|
||||||
// Audit log: LOGOUT (non-blocking)
|
|
||||||
if (userInfo) {
|
|
||||||
const os = await import('os')
|
|
||||||
logAudit('LOGOUT', String(userInfo.id), {
|
|
||||||
username: userInfo.username,
|
|
||||||
computerName: os.hostname(),
|
|
||||||
resource: 'ERP_SYSTEM',
|
|
||||||
status: 'success',
|
|
||||||
metadata: { userType: userInfo.userType }
|
|
||||||
}).catch((err) => log.warn('Failed to write audit log', { err }))
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionManager.logout()
|
|
||||||
await updateService.setUserContext(null)
|
|
||||||
}, 'auth:logout')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current user
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.AUTH_GET_CURRENT_USER,
|
IPC_CHANNELS.AUTH_GET_CURRENT_USER,
|
||||||
async (): Promise<IpcResult<CurrentUserResponse>> => {
|
async (): Promise<IpcResult<CurrentUserResponse>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => authService.getCurrentUser(), 'auth:getCurrentUser')
|
||||||
const isAuthenticated = sessionManager.isAuthenticated()
|
|
||||||
const userInfo = sessionManager.getUserInfo()
|
|
||||||
return {
|
|
||||||
isAuthenticated,
|
|
||||||
userInfo: userInfo ?? undefined
|
|
||||||
}
|
|
||||||
}, 'auth:getCurrentUser')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all users (for admin user selection)
|
|
||||||
*/
|
|
||||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_ALL_USERS, async (): Promise<IpcResult<UserInfo[]>> => {
|
ipcMain.handle(IPC_CHANNELS.AUTH_GET_ALL_USERS, async (): Promise<IpcResult<UserInfo[]>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => authService.getAllUsers(), 'auth:getAllUsers')
|
||||||
log.debug('Fetching all users for admin selection')
|
|
||||||
return await sessionManager.getAllUsers()
|
|
||||||
}, 'auth:getAllUsers')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch user (admin only)
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.AUTH_SWITCH_USER,
|
IPC_CHANNELS.AUTH_SWITCH_USER,
|
||||||
async (_event, userInfo: UserInfo): Promise<IpcResult<UserSelectionResponse>> => {
|
async (_event, userInfo: UserInfo): Promise<IpcResult<UserSelectionResponse>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => authService.switchUser(userInfo), 'auth:switchUser')
|
||||||
log.info('User switch attempt', { targetUser: userInfo.username })
|
|
||||||
const success = sessionManager.switchUser(userInfo)
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
const newUser = sessionManager.getUserInfo()
|
|
||||||
log.info('User switch successful', { newUsername: newUser?.username })
|
|
||||||
await updateService.setUserContext(newUser?.userType ?? null)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
userInfo: newUser ?? undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn('User switch failed')
|
|
||||||
throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT')
|
|
||||||
}, 'auth:switchUser')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if current user is admin
|
|
||||||
*/
|
|
||||||
ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise<IpcResult<boolean>> => {
|
ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise<IpcResult<boolean>> => {
|
||||||
return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin')
|
return withErrorHandling(async () => authService.isAdmin(), 'auth:isAdmin')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,368 +1,34 @@
|
|||||||
import { ipcMain, type WebContents } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
|
||||||
import { CleanerService } from '../services/erp/cleaner'
|
|
||||||
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
|
||||||
import { MySqlService } from '../services/database/mysql'
|
|
||||||
import { SqlServerService } from '../services/database/sql-server'
|
|
||||||
import { ConfigManager } from '../services/config/config-manager'
|
|
||||||
import { ResultExporter } from '../services/excel/result-exporter'
|
|
||||||
import { CleanerReportGenerator } from '../services/report/cleaner-report-generator'
|
|
||||||
import { RustfsService } from '../services/rustfs'
|
|
||||||
import { SessionManager } from '../services/user/session-manager'
|
|
||||||
import { createLogger } from '../services/logger'
|
|
||||||
import { logAudit } from '../services/logger/audit-logger'
|
|
||||||
import { withErrorHandling, type IpcResult } from './index'
|
import { withErrorHandling, type IpcResult } from './index'
|
||||||
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
|
||||||
import type {
|
import type {
|
||||||
CleanerInput,
|
CleanerInput,
|
||||||
CleanerResult,
|
CleanerResult,
|
||||||
CleanerProgress,
|
|
||||||
ExportResultItem,
|
ExportResultItem,
|
||||||
ExportResultResponse
|
ExportResultResponse
|
||||||
} from '../types/cleaner.types'
|
} from '../types/cleaner.types'
|
||||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
|
||||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||||
|
import { CleanerApplicationService } from '../services/cleaner/cleaner-application-service'
|
||||||
const log = createLogger('CleanerHandler')
|
|
||||||
|
|
||||||
function sendProgress(
|
|
||||||
sender: WebContents,
|
|
||||||
message: string,
|
|
||||||
progress: number,
|
|
||||||
extra?: Partial<CleanerProgress>
|
|
||||||
): void {
|
|
||||||
try {
|
|
||||||
const progressData: CleanerProgress = {
|
|
||||||
message,
|
|
||||||
progress,
|
|
||||||
currentOrderIndex: extra?.currentOrderIndex ?? 0,
|
|
||||||
totalOrders: extra?.totalOrders ?? 0,
|
|
||||||
currentMaterialIndex: extra?.currentMaterialIndex ?? 0,
|
|
||||||
totalMaterialsInOrder: extra?.totalMaterialsInOrder ?? 0,
|
|
||||||
currentOrderNumber: extra?.currentOrderNumber,
|
|
||||||
phase: extra?.phase ?? 'processing'
|
|
||||||
}
|
|
||||||
sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData)
|
|
||||||
} catch (error) {
|
|
||||||
log.warn('Failed to send progress event', { error })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
|
||||||
const configManager = ConfigManager.getInstance()
|
|
||||||
const config = configManager.getConfig()
|
|
||||||
const dbType = configManager.getDatabaseType()
|
|
||||||
|
|
||||||
if (dbType === 'sqlserver') {
|
|
||||||
const dbConfig = config.database.sqlserver
|
|
||||||
const sqlServerService = new SqlServerService({
|
|
||||||
server: dbConfig.server,
|
|
||||||
port: dbConfig.port,
|
|
||||||
user: dbConfig.username,
|
|
||||||
password: dbConfig.password,
|
|
||||||
database: dbConfig.database,
|
|
||||||
options: {
|
|
||||||
encrypt: false,
|
|
||||||
trustServerCertificate: dbConfig.trustServerCertificate
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await sqlServerService.connect()
|
|
||||||
return sqlServerService
|
|
||||||
} else {
|
|
||||||
const dbConfig = config.database.mysql
|
|
||||||
const mysqlService = new MySqlService({
|
|
||||||
host: dbConfig.host,
|
|
||||||
port: dbConfig.port,
|
|
||||||
user: dbConfig.username,
|
|
||||||
password: dbConfig.password,
|
|
||||||
database: dbConfig.database
|
|
||||||
})
|
|
||||||
await mysqlService.connect()
|
|
||||||
return mysqlService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get ERP configuration for current user
|
|
||||||
* URL is from config.yaml (fixed infrastructure)
|
|
||||||
* Username and password are from user's database config
|
|
||||||
*/
|
|
||||||
async function getErpConfig(): Promise<{
|
|
||||||
url: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
}> {
|
|
||||||
// Get ERP URL from config.yaml (fixed for all users)
|
|
||||||
const configManager = ConfigManager.getInstance()
|
|
||||||
const globalConfig = configManager.getConfig()
|
|
||||||
const erpUrl = globalConfig.erp.url
|
|
||||||
|
|
||||||
// Get username and password from user's database config
|
|
||||||
const erpConfigService = UserErpConfigService.getInstance()
|
|
||||||
const userConfig = await erpConfigService.getCurrentUserErpConfig()
|
|
||||||
|
|
||||||
if (!userConfig || !userConfig.username || !userConfig.password) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'ERP 配置不完整。请在设置中配置 ERP 用户名和密码',
|
|
||||||
'VAL_MISSING_REQUIRED'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: erpUrl,
|
|
||||||
username: userConfig.username,
|
|
||||||
password: userConfig.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerCleanerHandlers(): void {
|
export function registerCleanerHandlers(): void {
|
||||||
|
const cleanerService = new CleanerApplicationService()
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.CLEANER_RUN,
|
IPC_CHANNELS.CLEANER_RUN,
|
||||||
async (event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
|
async (event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
|
||||||
const sender = event.sender
|
return withErrorHandling(
|
||||||
const startTime = Date.now()
|
async () => cleanerService.runCleaner(event.sender, input),
|
||||||
|
'cleaner:run'
|
||||||
return withErrorHandling(async () => {
|
)
|
||||||
let authService: ErpAuthService | null = null
|
|
||||||
let dbService: MySqlService | SqlServerService | null = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get ERP configuration from database for current user
|
|
||||||
log.info('Fetching ERP configuration from database...')
|
|
||||||
const erpConfig = await getErpConfig()
|
|
||||||
|
|
||||||
log.info('ERP config retrieved', {
|
|
||||||
url: erpConfig.url ? 'configured' : 'EMPTY',
|
|
||||||
username: erpConfig.username ? 'configured' : 'EMPTY'
|
|
||||||
})
|
|
||||||
|
|
||||||
const configManager = ConfigManager.getInstance()
|
|
||||||
const dbType = configManager.getDatabaseType()
|
|
||||||
log.info(
|
|
||||||
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...`
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
dbService = await getDatabaseService()
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseQueryError(
|
|
||||||
'数据库连接失败',
|
|
||||||
'DB_CONNECTION_FAILED',
|
|
||||||
error instanceof Error ? error : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolver = new OrderNumberResolver(dbService)
|
|
||||||
const mappings = await resolver.resolve(input.orderNumbers)
|
|
||||||
|
|
||||||
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
|
|
||||||
const warnings = resolver.getWarnings(mappings)
|
|
||||||
|
|
||||||
if (warnings.length > 0) {
|
|
||||||
log.warn('Resolution warnings', { warnings })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validOrderNumbers.length === 0) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'没有有效的生产订单号可处理。请检查输入的格式或数据库连接。',
|
|
||||||
'VAL_INVALID_INPUT'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Resolved order numbers', { count: validOrderNumbers.length })
|
|
||||||
|
|
||||||
authService = new ErpAuthService({
|
|
||||||
url: erpConfig.url,
|
|
||||||
username: erpConfig.username,
|
|
||||||
password: erpConfig.password,
|
|
||||||
headless: input.headless ?? true
|
|
||||||
})
|
|
||||||
|
|
||||||
log.info('Logging in to ERP...')
|
|
||||||
try {
|
|
||||||
await authService.login()
|
|
||||||
} catch (error) {
|
|
||||||
throw new ErpConnectionError(
|
|
||||||
'ERP 登录失败',
|
|
||||||
'ERP_LOGIN_FAILED',
|
|
||||||
error instanceof Error ? error : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
log.info('Login successful')
|
|
||||||
|
|
||||||
// Send login complete progress
|
|
||||||
const totalOrders = validOrderNumbers.length
|
|
||||||
const loginProgress = (1 / (1 + totalOrders)) * 100
|
|
||||||
sendProgress(sender, 'ERP 登录成功', loginProgress, {
|
|
||||||
phase: 'login',
|
|
||||||
currentOrderIndex: 0,
|
|
||||||
totalOrders,
|
|
||||||
currentMaterialIndex: 0,
|
|
||||||
totalMaterialsInOrder: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const cleaner = new CleanerService(authService)
|
|
||||||
|
|
||||||
const modifiedInput: CleanerInput = {
|
|
||||||
...input,
|
|
||||||
orderNumbers: validOrderNumbers,
|
|
||||||
onProgress: (message, progress, extra) => {
|
|
||||||
sendProgress(sender, message, progress ?? 0, extra)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Starting cleaning', {
|
|
||||||
orderCount: validOrderNumbers.length,
|
|
||||||
queryBatchSize: input.queryBatchSize ?? 100,
|
|
||||||
processConcurrency: input.processConcurrency ?? 1
|
|
||||||
})
|
|
||||||
const result = await cleaner.clean(modifiedInput)
|
|
||||||
|
|
||||||
if (warnings.length > 0) {
|
|
||||||
result.errors = [...warnings, ...result.errors]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send completion progress
|
|
||||||
sendProgress(sender, '清理完成', 100, {
|
|
||||||
phase: 'complete',
|
|
||||||
currentOrderIndex: totalOrders,
|
|
||||||
totalOrders,
|
|
||||||
currentMaterialIndex: 0,
|
|
||||||
totalMaterialsInOrder: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
log.info('Cleaning completed', {
|
|
||||||
processedCount: result.ordersProcessed,
|
|
||||||
errorCount: result.errors.length
|
|
||||||
})
|
|
||||||
|
|
||||||
// Audit log: CLEAN (non-blocking)
|
|
||||||
const os = await import('os')
|
|
||||||
const currentUser = SessionManager.getInstance().getUserInfo()
|
|
||||||
if (currentUser) {
|
|
||||||
const status: 'success' | 'failure' | 'partial' =
|
|
||||||
result.errors.length > 0 && result.materialsDeleted > 0
|
|
||||||
? 'partial'
|
|
||||||
: result.errors.length > 0
|
|
||||||
? 'failure'
|
|
||||||
: 'success'
|
|
||||||
logAudit('CLEAN', String(currentUser.id), {
|
|
||||||
username: currentUser.username,
|
|
||||||
computerName: os.hostname(),
|
|
||||||
resource: 'MATERIAL_PLAN',
|
|
||||||
status,
|
|
||||||
metadata: {
|
|
||||||
orderCount: validOrderNumbers.length,
|
|
||||||
dryRun: input.dryRun ?? false,
|
|
||||||
queryBatchSize: input.queryBatchSize ?? 100,
|
|
||||||
processConcurrency: input.processConcurrency ?? 1,
|
|
||||||
materialsDeleted: result.materialsDeleted,
|
|
||||||
materialsSkipped: result.materialsSkipped,
|
|
||||||
errorCount: result.errors.length
|
|
||||||
}
|
|
||||||
}).catch((err) => log.warn('Failed to write audit log', { err }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate report and upload to RustFS (silent, user unaware)
|
|
||||||
try {
|
|
||||||
const endTime = Date.now()
|
|
||||||
const currentUser = SessionManager.getInstance().getUserInfo()
|
|
||||||
const username = currentUser?.username ?? 'unknown'
|
|
||||||
|
|
||||||
const reportGenerator = new CleanerReportGenerator()
|
|
||||||
const reportPath = await reportGenerator.generateReport(result, {
|
|
||||||
dryRun: input.dryRun ?? false,
|
|
||||||
username,
|
|
||||||
startTime,
|
|
||||||
endTime
|
|
||||||
})
|
|
||||||
log.info('Report generated', { path: reportPath })
|
|
||||||
|
|
||||||
// Upload to RustFS if enabled
|
|
||||||
const configManager = ConfigManager.getInstance()
|
|
||||||
const config = configManager.getConfig()
|
|
||||||
|
|
||||||
if (config.rustfs?.enabled && config.rustfs.endpoint) {
|
|
||||||
try {
|
|
||||||
const rustfs = new RustfsService({ config: config.rustfs })
|
|
||||||
const reportFileName = reportPath.split(/[\\/]/).pop() || 'report.md'
|
|
||||||
const storageKey = rustfs.generateReportKey(reportFileName, username)
|
|
||||||
|
|
||||||
log.info('Uploading report to RustFS', {
|
|
||||||
localPath: reportPath,
|
|
||||||
storageKey
|
|
||||||
})
|
|
||||||
|
|
||||||
const uploadResult = await rustfs.uploadFile(
|
|
||||||
reportPath,
|
|
||||||
storageKey,
|
|
||||||
'text/markdown; charset=utf-8'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (uploadResult.success) {
|
|
||||||
log.info('Report uploaded to RustFS successfully', {
|
|
||||||
key: storageKey,
|
|
||||||
etag: uploadResult.etag
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log.warn('Failed to upload report to RustFS', {
|
|
||||||
error: uploadResult.error,
|
|
||||||
key: storageKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (rustfsError) {
|
|
||||||
log.error('RustFS upload failed', {
|
|
||||||
error: rustfsError instanceof Error ? rustfsError.message : String(rustfsError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug('RustFS is not enabled, skipping upload')
|
|
||||||
}
|
|
||||||
} catch (reportError) {
|
|
||||||
log.warn('Failed to generate report', {
|
|
||||||
error: reportError instanceof Error ? reportError.message : String(reportError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
if (authService) {
|
|
||||||
try {
|
|
||||||
await authService.close()
|
|
||||||
log.debug('Browser closed')
|
|
||||||
} catch (closeError) {
|
|
||||||
log.warn('Error closing browser', {
|
|
||||||
error: closeError instanceof Error ? closeError.message : String(closeError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbService) {
|
|
||||||
try {
|
|
||||||
await dbService.disconnect()
|
|
||||||
log.debug('Database disconnected')
|
|
||||||
} catch (closeError) {
|
|
||||||
log.warn('Error disconnecting database', {
|
|
||||||
error: closeError instanceof Error ? closeError.message : String(closeError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 'cleaner:run')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.CLEANER_EXPORT_RESULTS,
|
IPC_CHANNELS.CLEANER_EXPORT_RESULTS,
|
||||||
async (_event, items: ExportResultItem[]): Promise<IpcResult<ExportResultResponse>> => {
|
async (_event, items: ExportResultItem[]): Promise<IpcResult<ExportResultResponse>> => {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(
|
||||||
log.info('Exporting validation results', { count: items.length })
|
async () => cleanerService.exportResults(items),
|
||||||
if (!items || items.length === 0) {
|
'cleaner:exportResults'
|
||||||
throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT')
|
)
|
||||||
}
|
|
||||||
const exporter = new ResultExporter()
|
|
||||||
return await exporter.exportValidationResults(items)
|
|
||||||
}, 'cleaner:exportResults')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
167
src/main/services/auth/auth-application-service.ts
Normal file
167
src/main/services/auth/auth-application-service.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { hostname } from 'os'
|
||||||
|
import { SessionManager } from '../user/session-manager'
|
||||||
|
import { UpdateService } from '../update/update-service'
|
||||||
|
import { createLogger } from '../logger'
|
||||||
|
import { logAudit } from '../logger/audit-logger'
|
||||||
|
import { ValidationError } from '../../types/errors'
|
||||||
|
import type { UserInfo } from '../../types/user.types'
|
||||||
|
import type {
|
||||||
|
CurrentUserResponse,
|
||||||
|
LoginResponse,
|
||||||
|
SilentLoginResponse,
|
||||||
|
UserSelectionResponse
|
||||||
|
} from '../../types/auth-ipc.types'
|
||||||
|
|
||||||
|
const log = createLogger('AuthApplicationService')
|
||||||
|
|
||||||
|
export class AuthApplicationService {
|
||||||
|
constructor(
|
||||||
|
private readonly sessionManager: SessionManager = SessionManager.getInstance(),
|
||||||
|
private readonly updateService: UpdateService = UpdateService.getInstance()
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getComputerName(): Promise<string> {
|
||||||
|
return hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
async silentLogin(): Promise<SilentLoginResponse> {
|
||||||
|
log.info('Attempting silent login')
|
||||||
|
const success = await this.sessionManager.loginByComputerName()
|
||||||
|
const userInfo = this.sessionManager.getUserInfo()
|
||||||
|
|
||||||
|
if (!success || !userInfo) {
|
||||||
|
await this.updateService.setUserContext(null)
|
||||||
|
throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateService.setUserContext(userInfo.userType)
|
||||||
|
|
||||||
|
const requiresUserSelection = userInfo.userType === 'Admin'
|
||||||
|
log.info('Silent login successful', {
|
||||||
|
username: userInfo.username,
|
||||||
|
userType: userInfo.userType,
|
||||||
|
requiresUserSelection
|
||||||
|
})
|
||||||
|
|
||||||
|
this.writeAuditLog('LOGIN', String(userInfo.id), {
|
||||||
|
username: userInfo.username,
|
||||||
|
computerName: hostname(),
|
||||||
|
resource: 'ERP_SYSTEM',
|
||||||
|
status: 'success',
|
||||||
|
metadata: { loginType: 'silent', userType: userInfo.userType }
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userInfo,
|
||||||
|
requiresUserSelection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username: string, password: string): Promise<LoginResponse> {
|
||||||
|
if (!username || !password) {
|
||||||
|
log.warn('Login attempt with missing credentials')
|
||||||
|
throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED')
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Login attempt', { username })
|
||||||
|
const success = await this.sessionManager.login(username, password)
|
||||||
|
const userInfo = this.sessionManager.getUserInfo()
|
||||||
|
|
||||||
|
if (!success || !userInfo) {
|
||||||
|
this.writeAuditLog('LOGIN', '0', {
|
||||||
|
username,
|
||||||
|
computerName: hostname(),
|
||||||
|
resource: 'ERP_SYSTEM',
|
||||||
|
status: 'failure',
|
||||||
|
metadata: { loginType: 'credentials', reason: 'invalid_credentials' }
|
||||||
|
})
|
||||||
|
|
||||||
|
log.warn('Login failed - invalid credentials', { username })
|
||||||
|
await this.updateService.setUserContext(null)
|
||||||
|
throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT')
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Login successful', { username, userType: userInfo.userType })
|
||||||
|
await this.updateService.setUserContext(userInfo.userType)
|
||||||
|
|
||||||
|
this.writeAuditLog('LOGIN', String(userInfo.id), {
|
||||||
|
username: userInfo.username,
|
||||||
|
computerName: hostname(),
|
||||||
|
resource: 'ERP_SYSTEM',
|
||||||
|
status: 'success',
|
||||||
|
metadata: { loginType: 'credentials', userType: userInfo.userType }
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
const userInfo = this.sessionManager.getUserInfo()
|
||||||
|
log.info('User logout', { username: userInfo?.username })
|
||||||
|
|
||||||
|
if (userInfo) {
|
||||||
|
this.writeAuditLog('LOGOUT', String(userInfo.id), {
|
||||||
|
username: userInfo.username,
|
||||||
|
computerName: hostname(),
|
||||||
|
resource: 'ERP_SYSTEM',
|
||||||
|
status: 'success',
|
||||||
|
metadata: { userType: userInfo.userType }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessionManager.logout()
|
||||||
|
await this.updateService.setUserContext(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser(): CurrentUserResponse {
|
||||||
|
const isAuthenticated = this.sessionManager.isAuthenticated()
|
||||||
|
const userInfo = this.sessionManager.getUserInfo()
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated,
|
||||||
|
userInfo: userInfo ?? undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllUsers(): Promise<UserInfo[]> {
|
||||||
|
log.debug('Fetching all users for admin selection')
|
||||||
|
return this.sessionManager.getAllUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchUser(userInfo: UserInfo): Promise<UserSelectionResponse> {
|
||||||
|
log.info('User switch attempt', { targetUser: userInfo.username })
|
||||||
|
const success = this.sessionManager.switchUser(userInfo)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
log.warn('User switch failed')
|
||||||
|
throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = this.sessionManager.getUserInfo()
|
||||||
|
log.info('User switch successful', { newUsername: newUser?.username })
|
||||||
|
await this.updateService.setUserContext(newUser?.userType ?? null)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userInfo: newUser ?? undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin(): boolean {
|
||||||
|
return this.sessionManager.isAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeAuditLog(
|
||||||
|
action: 'LOGIN' | 'LOGOUT',
|
||||||
|
actorId: string,
|
||||||
|
payload: Parameters<typeof logAudit>[2]
|
||||||
|
): void {
|
||||||
|
logAudit(action, actorId, payload).catch((err) =>
|
||||||
|
log.warn('Failed to write audit log', { err })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
359
src/main/services/cleaner/cleaner-application-service.ts
Normal file
359
src/main/services/cleaner/cleaner-application-service.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import type { WebContents } from 'electron'
|
||||||
|
import type { MySqlService } from '../database/mysql'
|
||||||
|
import type { SqlServerService } from '../database/sql-server'
|
||||||
|
import { ErpAuthService } from '../erp/erp-auth'
|
||||||
|
import { CleanerService } from '../erp/cleaner'
|
||||||
|
import { OrderNumberResolver } from '../erp/order-resolver'
|
||||||
|
import { MySqlService as MySqlServiceImpl } from '../database/mysql'
|
||||||
|
import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server'
|
||||||
|
import { ConfigManager } from '../config/config-manager'
|
||||||
|
import { ResultExporter } from '../excel/result-exporter'
|
||||||
|
import { CleanerReportGenerator } from '../report/cleaner-report-generator'
|
||||||
|
import { RustfsService } from '../rustfs'
|
||||||
|
import { SessionManager } from '../user/session-manager'
|
||||||
|
import { UserErpConfigService } from '../user/user-erp-config-service'
|
||||||
|
import { createLogger } from '../logger'
|
||||||
|
import { logAudit } from '../logger/audit-logger'
|
||||||
|
import { IPC_CHANNELS } from '../../../shared/ipc-channels'
|
||||||
|
import { DatabaseQueryError, ErpConnectionError, ValidationError } from '../../types/errors'
|
||||||
|
import type {
|
||||||
|
CleanerInput,
|
||||||
|
CleanerProgress,
|
||||||
|
CleanerResult,
|
||||||
|
ExportResultItem,
|
||||||
|
ExportResultResponse
|
||||||
|
} from '../../types/cleaner.types'
|
||||||
|
|
||||||
|
const log = createLogger('CleanerApplicationService')
|
||||||
|
|
||||||
|
type DatabaseService = MySqlService | SqlServerService
|
||||||
|
|
||||||
|
export class CleanerApplicationService {
|
||||||
|
async runCleaner(eventSender: WebContents, input: CleanerInput): Promise<CleanerResult> {
|
||||||
|
const startTime = Date.now()
|
||||||
|
let authService: ErpAuthService | null = null
|
||||||
|
let dbService: DatabaseService | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info('Fetching ERP configuration from database...')
|
||||||
|
const erpConfig = await this.getErpConfig()
|
||||||
|
|
||||||
|
log.info('ERP config retrieved', {
|
||||||
|
url: erpConfig.url ? 'configured' : 'EMPTY',
|
||||||
|
username: erpConfig.username ? 'configured' : 'EMPTY'
|
||||||
|
})
|
||||||
|
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const dbType = configManager.getDatabaseType()
|
||||||
|
log.info(
|
||||||
|
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
dbService = await this.getDatabaseService()
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseQueryError(
|
||||||
|
'数据库连接失败',
|
||||||
|
'DB_CONNECTION_FAILED',
|
||||||
|
error instanceof Error ? error : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = new OrderNumberResolver(dbService)
|
||||||
|
const mappings = await resolver.resolve(input.orderNumbers)
|
||||||
|
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
|
||||||
|
const warnings = resolver.getWarnings(mappings)
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
log.warn('Resolution warnings', { warnings })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validOrderNumbers.length === 0) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'没有有效的生产订单号可处理。请检查输入的格式或数据库连接。',
|
||||||
|
'VAL_INVALID_INPUT'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Resolved order numbers', { count: validOrderNumbers.length })
|
||||||
|
|
||||||
|
authService = new ErpAuthService({
|
||||||
|
url: erpConfig.url,
|
||||||
|
username: erpConfig.username,
|
||||||
|
password: erpConfig.password,
|
||||||
|
headless: input.headless ?? true
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info('Logging in to ERP...')
|
||||||
|
try {
|
||||||
|
await authService.login()
|
||||||
|
} catch (error) {
|
||||||
|
throw new ErpConnectionError(
|
||||||
|
'ERP 登录失败',
|
||||||
|
'ERP_LOGIN_FAILED',
|
||||||
|
error instanceof Error ? error : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
log.info('Login successful')
|
||||||
|
|
||||||
|
const totalOrders = validOrderNumbers.length
|
||||||
|
this.sendProgress(eventSender, 'ERP 登录成功', (1 / (1 + totalOrders)) * 100, {
|
||||||
|
phase: 'login',
|
||||||
|
currentOrderIndex: 0,
|
||||||
|
totalOrders,
|
||||||
|
currentMaterialIndex: 0,
|
||||||
|
totalMaterialsInOrder: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const cleaner = new CleanerService(authService)
|
||||||
|
const modifiedInput: CleanerInput = {
|
||||||
|
...input,
|
||||||
|
orderNumbers: validOrderNumbers,
|
||||||
|
onProgress: (message, progress, extra) => {
|
||||||
|
this.sendProgress(eventSender, message, progress ?? 0, extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Starting cleaning', {
|
||||||
|
orderCount: validOrderNumbers.length,
|
||||||
|
queryBatchSize: input.queryBatchSize ?? 100,
|
||||||
|
processConcurrency: input.processConcurrency ?? 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await cleaner.clean(modifiedInput)
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
result.errors = [...warnings, ...result.errors]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendProgress(eventSender, '清理完成', 100, {
|
||||||
|
phase: 'complete',
|
||||||
|
currentOrderIndex: totalOrders,
|
||||||
|
totalOrders,
|
||||||
|
currentMaterialIndex: 0,
|
||||||
|
totalMaterialsInOrder: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info('Cleaning completed', {
|
||||||
|
processedCount: result.ordersProcessed,
|
||||||
|
errorCount: result.errors.length
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.recordCleanupAudit(validOrderNumbers.length, input, result)
|
||||||
|
await this.generateAndUploadReport(input, result, startTime)
|
||||||
|
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
if (authService) {
|
||||||
|
try {
|
||||||
|
await authService.close()
|
||||||
|
log.debug('Browser closed')
|
||||||
|
} catch (closeError) {
|
||||||
|
log.warn('Error closing browser', {
|
||||||
|
error: closeError instanceof Error ? closeError.message : String(closeError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbService) {
|
||||||
|
try {
|
||||||
|
await dbService.disconnect()
|
||||||
|
log.debug('Database disconnected')
|
||||||
|
} catch (closeError) {
|
||||||
|
log.warn('Error disconnecting database', {
|
||||||
|
error: closeError instanceof Error ? closeError.message : String(closeError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportResults(items: ExportResultItem[]): Promise<ExportResultResponse> {
|
||||||
|
log.info('Exporting validation results', { count: items.length })
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT')
|
||||||
|
}
|
||||||
|
|
||||||
|
const exporter = new ResultExporter()
|
||||||
|
return exporter.exportValidationResults(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendProgress(
|
||||||
|
sender: WebContents,
|
||||||
|
message: string,
|
||||||
|
progress: number,
|
||||||
|
extra?: Partial<CleanerProgress>
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const progressData: CleanerProgress = {
|
||||||
|
message,
|
||||||
|
progress,
|
||||||
|
currentOrderIndex: extra?.currentOrderIndex ?? 0,
|
||||||
|
totalOrders: extra?.totalOrders ?? 0,
|
||||||
|
currentMaterialIndex: extra?.currentMaterialIndex ?? 0,
|
||||||
|
totalMaterialsInOrder: extra?.totalMaterialsInOrder ?? 0,
|
||||||
|
currentOrderNumber: extra?.currentOrderNumber,
|
||||||
|
phase: extra?.phase ?? 'processing'
|
||||||
|
}
|
||||||
|
sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData)
|
||||||
|
} catch (error) {
|
||||||
|
log.warn('Failed to send progress event', { error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDatabaseService(): Promise<DatabaseService> {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const config = configManager.getConfig()
|
||||||
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
|
if (dbType === 'sqlserver') {
|
||||||
|
const dbConfig = config.database.sqlserver
|
||||||
|
const sqlServerService = new SqlServerServiceImpl({
|
||||||
|
server: dbConfig.server,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database,
|
||||||
|
options: {
|
||||||
|
encrypt: false,
|
||||||
|
trustServerCertificate: dbConfig.trustServerCertificate
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await sqlServerService.connect()
|
||||||
|
return sqlServerService
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbConfig = config.database.mysql
|
||||||
|
const mysqlService = new MySqlServiceImpl({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
await mysqlService.connect()
|
||||||
|
return mysqlService
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getErpConfig(): Promise<{ url: string; username: string; password: string }> {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const globalConfig = configManager.getConfig()
|
||||||
|
const erpUrl = globalConfig.erp.url
|
||||||
|
|
||||||
|
const erpConfigService = UserErpConfigService.getInstance()
|
||||||
|
const userConfig = await erpConfigService.getCurrentUserErpConfig()
|
||||||
|
|
||||||
|
if (!userConfig || !userConfig.username || !userConfig.password) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'ERP 配置不完整。请在设置中配置 ERP 用户名和密码',
|
||||||
|
'VAL_MISSING_REQUIRED'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: erpUrl,
|
||||||
|
username: userConfig.username,
|
||||||
|
password: userConfig.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async recordCleanupAudit(
|
||||||
|
orderCount: number,
|
||||||
|
input: CleanerInput,
|
||||||
|
result: CleanerResult
|
||||||
|
): Promise<void> {
|
||||||
|
const currentUser = SessionManager.getInstance().getUserInfo()
|
||||||
|
if (!currentUser) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const status: 'success' | 'failure' | 'partial' =
|
||||||
|
result.errors.length > 0 && result.materialsDeleted > 0
|
||||||
|
? 'partial'
|
||||||
|
: result.errors.length > 0
|
||||||
|
? 'failure'
|
||||||
|
: 'success'
|
||||||
|
|
||||||
|
await logAudit('CLEAN', String(currentUser.id), {
|
||||||
|
username: currentUser.username,
|
||||||
|
computerName: (await import('os')).hostname(),
|
||||||
|
resource: 'MATERIAL_PLAN',
|
||||||
|
status,
|
||||||
|
metadata: {
|
||||||
|
orderCount,
|
||||||
|
dryRun: input.dryRun ?? false,
|
||||||
|
queryBatchSize: input.queryBatchSize ?? 100,
|
||||||
|
processConcurrency: input.processConcurrency ?? 1,
|
||||||
|
materialsDeleted: result.materialsDeleted,
|
||||||
|
materialsSkipped: result.materialsSkipped,
|
||||||
|
errorCount: result.errors.length
|
||||||
|
}
|
||||||
|
}).catch((err) => log.warn('Failed to write audit log', { err }))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateAndUploadReport(
|
||||||
|
input: CleanerInput,
|
||||||
|
result: CleanerResult,
|
||||||
|
startTime: number
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const endTime = Date.now()
|
||||||
|
const currentUser = SessionManager.getInstance().getUserInfo()
|
||||||
|
const username = currentUser?.username ?? 'unknown'
|
||||||
|
|
||||||
|
const reportGenerator = new CleanerReportGenerator()
|
||||||
|
const reportPath = await reportGenerator.generateReport(result, {
|
||||||
|
dryRun: input.dryRun ?? false,
|
||||||
|
username,
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
})
|
||||||
|
log.info('Report generated', { path: reportPath })
|
||||||
|
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
const config = configManager.getConfig()
|
||||||
|
|
||||||
|
if (!config.rustfs?.enabled || !config.rustfs.endpoint) {
|
||||||
|
log.debug('RustFS is not enabled, skipping upload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rustfs = new RustfsService({ config: config.rustfs })
|
||||||
|
const reportFileName = reportPath.split(/[\\/]/).pop() || 'report.md'
|
||||||
|
const storageKey = rustfs.generateReportKey(reportFileName, username)
|
||||||
|
|
||||||
|
log.info('Uploading report to RustFS', {
|
||||||
|
localPath: reportPath,
|
||||||
|
storageKey
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadResult = await rustfs.uploadFile(
|
||||||
|
reportPath,
|
||||||
|
storageKey,
|
||||||
|
'text/markdown; charset=utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (uploadResult.success) {
|
||||||
|
log.info('Report uploaded to RustFS successfully', {
|
||||||
|
key: storageKey,
|
||||||
|
etag: uploadResult.etag
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log.warn('Failed to upload report to RustFS', {
|
||||||
|
error: uploadResult.error,
|
||||||
|
key: storageKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (rustfsError) {
|
||||||
|
log.error('RustFS upload failed', {
|
||||||
|
error: rustfsError instanceof Error ? rustfsError.message : String(rustfsError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (reportError) {
|
||||||
|
log.warn('Failed to generate report', {
|
||||||
|
error: reportError instanceof Error ? reportError.message : String(reportError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user