refactor: move ipc orchestration into application services

This commit is contained in:
Misaka
2026-03-21 10:40:36 +08:00
parent 546d005c19
commit 26c05f3726
4 changed files with 551 additions and 515 deletions

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

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