refactor: abstract database layer to support MySQL and SQL Server

- Add IDatabaseService interface with unified query(transaction) methods
- Create DatabaseFactory for centralized database service creation
- Modify MySqlService and SqlServerService to implement IDatabaseService
- Unify SqlServerService.query() to accept array params (internally converts to @p0, @p1...)
- Refactor OrderNumberResolver to use IDatabaseService
- Refactor DiscreteMaterialPlanDAO to use DatabaseFactory
- Refactor MaterialsToBeDeletedDAO to use DatabaseFactory
- Update IPC handlers to use DatabaseFactory.create()
- Add database.types.ts with shared type definitions

This enables switching between MySQL and SQL Server via DB_TYPE env variable.

Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-03-03 20:18:20 +08:00
parent 6698d82d6b
commit 0040ca7521
10 changed files with 624 additions and 702 deletions

View File

@@ -214,7 +214,18 @@ export function registerDatabaseHandlers(): void {
}
log.debug('Executing SQL Server query', { windowId, sql: sqlString.substring(0, 100) })
return await service.query(sqlString, params)
// Use queryWithParams for named parameters, or query for no params
if (params && Object.keys(params).length > 0) {
// Convert to the format expected by queryWithParams
const typedParams: Record<string, { value: unknown }> = {}
for (const [key, value] of Object.entries(params)) {
typedParams[key] = { value }
}
return await service.queryWithParams(sqlString, typedParams)
} else {
return await service.query(sqlString)
}
} catch (error) {
const message = error instanceof Error ? error.message : 'SQL Server query failed'
log.error('SQL Server query failed', { error: message })

View File

@@ -2,7 +2,7 @@ import { ipcMain } from 'electron'
import { ErpAuthService } from '../services/erp/erp-auth'
import { ExtractorService } from '../services/erp/extractor'
import { OrderNumberResolver } from '../services/erp/order-resolver'
import { MySqlService } from '../services/database/mysql'
import { create, type IDatabaseService } from '../services/database'
import { createLogger } from '../services/logger'
import { withErrorHandling, type IpcResult } from './index'
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
@@ -19,7 +19,7 @@ export function registerExtractorHandlers(): void {
async (_event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
return withErrorHandling(async () => {
let authService: ErpAuthService | null = null
let mysqlService: MySqlService | null = null
let dbService: IDatabaseService | null = null
try {
// Check environment variables
@@ -39,29 +39,20 @@ export function registerExtractorHandlers(): void {
)
}
// Resolve order numbers (convert productionIDs to 生产订单号)
const mysqlConfig = {
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
}
log.info('Connecting to MySQL for order resolution...')
mysqlService = new MySqlService(mysqlConfig)
// Create database service using factory
log.info('Connecting to database for order resolution...')
try {
await mysqlService.connect()
dbService = await create()
} catch (error) {
throw new DatabaseQueryError(
'MySQL 连接失败',
'数据库连接失败',
'DB_CONNECTION_FAILED',
error instanceof Error ? error : undefined
)
}
const resolver = new OrderNumberResolver(mysqlService)
// Resolve order numbers (convert productionIDs to 生产订单号)
const resolver = new OrderNumberResolver(dbService)
const mappings = await resolver.resolve(input.orderNumbers)
// Get valid order numbers and warnings
@@ -136,13 +127,13 @@ export function registerExtractorHandlers(): void {
}
}
// Clean up: disconnect MySQL
if (mysqlService) {
// Clean up: disconnect database
if (dbService) {
try {
await mysqlService.disconnect()
log.debug('MySQL disconnected')
await dbService.disconnect()
log.debug('Database disconnected')
} catch (closeError) {
log.warn('Error disconnecting MySQL', {
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}

View File

@@ -7,10 +7,9 @@
*/
import { ipcMain } from 'electron'
import { MySqlService } from '../services/database/mysql'
import { create, type IDatabaseService } from '../services/database'
import { OrderNumberResolver } from '../services/erp/order-resolver'
import { createLogger } from '../services/logger'
import { DatabaseQueryError } from '../types/errors'
import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver'
const log = createLogger('ResolverHandler')
@@ -21,14 +20,6 @@ const log = createLogger('ResolverHandler')
export interface ResolverInput {
/** List of order numbers/productionIDs to resolve */
inputs: string[]
/** MySQL configuration (optional, uses default if not provided) */
mysqlConfig?: {
host: string
port: number
user: string
password: string
database: string
}
}
/**
@@ -60,25 +51,15 @@ export function registerResolverHandlers(): void {
ipcMain.handle(
'resolver:resolve',
async (_event, input: ResolverInput): Promise<ResolverResponse> => {
let mysqlService: MySqlService | null = null
let dbService: IDatabaseService | null = null
try {
// Use provided config or environment variables
const mysqlConfig = input.mysqlConfig || {
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
}
// Create MySQL service
log.info('Connecting to MySQL for resolution', { inputCount: input.inputs.length })
mysqlService = new MySqlService(mysqlConfig)
await mysqlService.connect()
// Create database service using factory
log.info('Connecting to database for resolution', { inputCount: input.inputs.length })
dbService = await create()
// Create resolver and resolve inputs
const resolver = new OrderNumberResolver(mysqlService)
const resolver = new OrderNumberResolver(dbService)
const mappings = await resolver.resolve(input.inputs)
// Get valid order numbers and warnings
@@ -107,13 +88,13 @@ export function registerResolverHandlers(): void {
error: `解析失败:${message}`
}
} finally {
// Clean up MySQL connection
if (mysqlService) {
// Clean up database connection
if (dbService) {
try {
await mysqlService.disconnect()
log.debug('MySQL disconnected')
await dbService.disconnect()
log.debug('Database disconnected')
} catch (closeError) {
log.warn('Error disconnecting MySQL', {
log.warn('Error disconnecting database', {
error: closeError instanceof Error ? closeError.message : String(closeError)
})
}
@@ -136,9 +117,11 @@ export function registerResolverHandlers(): void {
error?: string
}> => {
try {
// Create a mock resolver without database connection
const resolver = new OrderNumberResolver({
isConnected: () => false
} as MySqlService)
isConnected: () => false,
type: 'mysql'
} as IDatabaseService)
const results = inputs.map((input) => ({
input,