From 05d856309fe8c915e6aac4a9ad8e8b0f9e979631 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 2 Mar 2026 11:10:59 +0800 Subject: [PATCH] feat: add SQL Server support and refactor database layer for multi-database compatibility - Add SqlServerService integration alongside existing MySQL support - Refactor DAOs (DiscreteMaterialPlanDAO, MaterialsToBeDeletedDAO, BipUsersDAO) to support both MySQL and SQL Server - Update validation handler to dynamically select database service based on DB_TYPE environment variable - Add connection pooling and proper connection management for SQL Server - Update package-lock.json dependency peer flags Co-Authored-By: Claude Sonnet 4.5 --- package-lock.json | 33 +- src/main/ipc/validation-handler.ts | 345 +++++++------ .../database/discrete-material-plan-dao.ts | 443 +++++++++++----- .../database/materials-to-be-deleted-dao.ts | 472 +++++++++++++----- src/main/services/database/sql-server.ts | 6 +- src/main/services/user/bip-users-dao.ts | 381 ++++++++++---- 6 files changed, 1207 insertions(+), 473 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2c3a2bd..3c36757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,6 +99,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -160,6 +161,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -351,6 +353,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1099,7 +1102,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1121,7 +1123,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1138,7 +1139,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1153,7 +1153,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -3008,6 +3007,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3030,6 +3030,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3132,6 +3133,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -3565,6 +3567,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3597,6 +3600,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4329,6 +4333,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4984,8 +4989,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -5334,6 +5338,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -5562,6 +5567,7 @@ "integrity": "sha512-Rz5QvP1pTqoU1DPRrG3EeX2oWBtS3uRmd6Z/wzZsb2e/iIUsrT+XcBaAhFr4FW48gDc8uP2wYVyY5Aamha/5Zg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -5750,7 +5756,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -5771,7 +5776,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6099,6 +6103,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6159,6 +6164,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9870,6 +9876,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9951,6 +9958,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9974,7 +9982,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -9992,7 +9999,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -10013,6 +10019,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10145,6 +10152,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11380,7 +11388,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -11668,6 +11675,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11902,6 +11910,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12448,6 +12457,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -12845,6 +12855,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index bfd19f9..fdd46fc 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -9,6 +9,7 @@ import { ipcMain } from 'electron' import { MySqlService } from '../services/database/mysql' +import { SqlServerService } from '../services/database/sql-server' import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao' import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao' import type { @@ -49,19 +50,53 @@ export function clearSharedProductionIds(): void { } /** - * Get MySQL service for validation operations + * Get database service for validation operations (MySQL or SQL Server) */ -async function getValidationMySqlService(): Promise { - const mysqlService = new MySqlService({ - 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 || '' - }) +async function getValidationDatabaseService(): Promise { + const dbType = process.env.DB_TYPE?.toLowerCase() - await mysqlService.connect() - return mysqlService + if (dbType === 'sqlserver' || dbType === 'mssql') { + const sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + await sqlServerService.connect() + return sqlServerService + } else { + const mysqlService = new MySqlService({ + 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 || '' + }) + await mysqlService.connect() + return mysqlService + } +} + +/** + * Get table name based on database type + */ +function getTableName(mysqlTableName: string): string { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + // Convert MySQL table name to SQL Server format + // e.g., dbo_MaterialsToBeDeleted -> [dbo].[MaterialsToBeDeleted] + if (mysqlTableName.startsWith('dbo_')) { + const tableName = mysqlTableName.substring(4) + return `[dbo].[${tableName}]` + } + return `[dbo].[${mysqlTableName}]` + } + return mysqlTableName } /** @@ -96,10 +131,12 @@ function identifyInputType(input: string): 'production_id' | 'order_number' | 'u */ async function getSourceNumbersFromInputs( inputs: string[], - mysqlService: MySqlService + dbService: MySqlService | SqlServerService ): Promise { const productionIds: string[] = [] const orderNumbers: string[] = [] + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' for (const item of inputs) { const type = identifyInputType(item) @@ -114,17 +151,38 @@ async function getSourceNumbersFromInputs( // Table name in MySQL: productionContractData_26年压力表合同数据 // Column name: 生产订单号 (SourceNumber) if (productionIds.length > 0) { - const placeholders = productionIds.map(() => '?').join(',') - const contractSql = ` - SELECT DISTINCT 生产订单号 - FROM productionContractData_26年压力表合同数据 - WHERE 总排号 IN (${placeholders}) - ` - const contractResult = await mysqlService.query(contractSql, productionIds) - const dbOrderNumbers = contractResult.rows.map( - row => row.生产订单号 as string - ) - orderNumbers.push(...dbOrderNumbers) + if (isSqlServer) { + const sql = require('mssql') + const placeholders = productionIds.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + productionIds.forEach((id, idx) => { + params[`p${idx}`] = { value: id, type: sql.NVarChar } + }) + + const contractSql = ` + SELECT DISTINCT 生产订单号 + FROM productionContractData_26年压力表合同数据 + WHERE 总排号 IN (${placeholders}) + ` + const contractResult = await (dbService as SqlServerService).queryWithParams(contractSql, params) + const dbOrderNumbers = contractResult.rows.map( + row => row.生产订单号 as string + ) + orderNumbers.push(...dbOrderNumbers) + } else { + const placeholders = productionIds.map(() => '?').join(',') + const contractSql = ` + SELECT DISTINCT 生产订单号 + FROM productionContractData_26年压力表合同数据 + WHERE 总排号 IN (${placeholders}) + ` + const contractResult = await (dbService as MySqlService).query(contractSql, productionIds) + const dbOrderNumbers = contractResult.rows.map( + row => row.生产订单号 as string + ) + orderNumbers.push(...dbOrderNumbers) + } } // Deduplicate @@ -146,13 +204,15 @@ export function registerValidationHandlers(): void { _event, request: ValidationRequest ): Promise => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null try { console.log('[Validation] Starting validation:', request) - // Connect to MySQL - mysqlService = await getValidationMySqlService() + // Connect to database + dbService = await getValidationDatabaseService() + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' let sourceNumbers: string[] | null = null @@ -175,7 +235,7 @@ export function registerValidationHandlers(): void { } } - sourceNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService) + sourceNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) console.log( `[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs` ) @@ -185,7 +245,7 @@ export function registerValidationHandlers(): void { console.log( `[Validation] Read ${inputs.length} inputs from file` ) - sourceNumbers = await getSourceNumbersFromInputs(inputs, mysqlService) + sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService) console.log( `[Validation] Got ${sourceNumbers.length} source numbers` ) @@ -194,8 +254,6 @@ export function registerValidationHandlers(): void { // Get material records from DiscreteMaterialPlanData const materialDao = new DiscreteMaterialPlanDAO() - // Inject the mysqlService instance - ;(materialDao as any).mysqlService = mysqlService let materialRecords: any[] = [] @@ -222,24 +280,32 @@ export function registerValidationHandlers(): void { } // Get type keywords from MaterialsTypeToBeDeleted + const typeKeywordTableName = getTableName('dbo_MaterialsTypeToBeDeleted') const typeKeywordSql = ` SELECT MaterialName, ManagerName - FROM dbo_MaterialsTypeToBeDeleted + FROM ${typeKeywordTableName} WHERE MaterialName IS NOT NULL ` - const typeKeywordResult = await mysqlService.query(typeKeywordSql) + const typeKeywordResult = isSqlServer + ? await (dbService as SqlServerService).query(typeKeywordSql) + : await (dbService as MySqlService).query(typeKeywordSql) + const typeKeywords = typeKeywordResult.rows.map(row => ({ materialName: row.MaterialName as string, managerName: row.ManagerName as string })) // Get marked material codes from MaterialsToBeDeleted + const markedTableName = getTableName('dbo_MaterialsToBeDeleted') const markedSql = ` SELECT MaterialCode, ManagerName - FROM dbo_MaterialsToBeDeleted + FROM ${markedTableName} WHERE MaterialCode IS NOT NULL AND ManagerName IS NOT NULL ` - const markedResult = await mysqlService.query(markedSql) + const markedResult = isSqlServer + ? await (dbService as SqlServerService).query(markedSql) + : await (dbService as MySqlService).query(markedSql) + const markedCodesDict = new Map() for (const row of markedResult.rows) { markedCodesDict.set( @@ -306,11 +372,11 @@ export function registerValidationHandlers(): void { error: `Validation failed: ${message}` } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[Validation] Error disconnecting MySQL:', closeError) + console.warn('[Validation] Error disconnecting database:', closeError) } } } @@ -328,13 +394,8 @@ export function registerValidationHandlers(): void { _event, request: MaterialUpsertBatchRequest ): Promise => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const stats = await dao.upsertBatch(request.materials) return { @@ -348,14 +409,6 @@ export function registerValidationHandlers(): void { success: false, error: `Upsert failed: ${message}` } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -369,13 +422,8 @@ export function registerValidationHandlers(): void { _event, request: MaterialDeleteRequest ): Promise => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const count = await dao.deleteByMaterialCodes(request.materialCodes) return { @@ -389,14 +437,6 @@ export function registerValidationHandlers(): void { success: false, error: `Delete failed: ${message}` } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -407,26 +447,13 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'materials:getManagers', async (_event): Promise<{ managers: string[] }> => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const managers = await dao.getManagers() return { managers } } catch (error) { console.error('[Materials] Get managers error:', error) return { managers: [] } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -440,13 +467,14 @@ export function registerValidationHandlers(): void { _event, managerName: string ): Promise<{ materials: MaterialRecordSummary[] }> => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null try { - mysqlService = await getValidationMySqlService() - const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService + dbService = await getValidationDatabaseService() + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' + const dao = new MaterialsToBeDeletedDAO() const materials = await dao.getMaterialsByManager(managerName) // Get material codes set for quick lookup @@ -454,17 +482,32 @@ export function registerValidationHandlers(): void { // Enrich with material details from DiscreteMaterialPlanData const enrichedMaterials: MaterialRecordSummary[] = [] + const detailTableName = getTableName('dbo_DiscreteMaterialPlanData') for (const mat of materials) { - const detailSql = ` - SELECT MaterialName, Specification, Model - FROM dbo_DiscreteMaterialPlanData - WHERE MaterialCode = ? - LIMIT 1 - ` - const detailResult = await mysqlService.query(detailSql, [ - mat.materialCode - ]) + let detailResult: any + + if (isSqlServer) { + const sql = require('mssql') + const detailSql = ` + SELECT TOP 1 MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = @materialCode + ` + detailResult = await (dbService as SqlServerService).queryWithParams(detailSql, { + materialCode: { value: mat.materialCode, type: sql.NVarChar } + }) + } else { + const detailSql = ` + SELECT MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = ? + LIMIT 1 + ` + detailResult = await (dbService as MySqlService).query(detailSql, [ + mat.materialCode + ]) + } enrichedMaterials.push({ materialCode: mat.materialCode, @@ -490,11 +533,11 @@ export function registerValidationHandlers(): void { console.error('[Materials] Get by manager error:', error) return { materials: [] } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) + console.warn('[Materials] Error disconnecting database:', closeError) } } } @@ -507,28 +550,44 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'materials:getAll', async (_event): Promise<{ materials: MaterialRecordSummary[] }> => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null try { - mysqlService = await getValidationMySqlService() - const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService + dbService = await getValidationDatabaseService() + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' + const dao = new MaterialsToBeDeletedDAO() const materials = await dao.getAllRecords() const markedCodes = await dao.getAllMaterialCodes() const enrichedMaterials: MaterialRecordSummary[] = [] + const detailTableName = getTableName('dbo_DiscreteMaterialPlanData') for (const mat of materials) { - const detailSql = ` - SELECT MaterialName, Specification, Model - FROM dbo_DiscreteMaterialPlanData - WHERE MaterialCode = ? - LIMIT 1 - ` - const detailResult = await mysqlService.query(detailSql, [ - mat.materialCode - ]) + let detailResult: any + + if (isSqlServer) { + const sql = require('mssql') + const detailSql = ` + SELECT TOP 1 MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = @materialCode + ` + detailResult = await (dbService as SqlServerService).queryWithParams(detailSql, { + materialCode: { value: mat.materialCode, type: sql.NVarChar } + }) + } else { + const detailSql = ` + SELECT MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = ? + LIMIT 1 + ` + detailResult = await (dbService as MySqlService).query(detailSql, [ + mat.materialCode + ]) + } enrichedMaterials.push({ materialCode: mat.materialCode, @@ -554,11 +613,11 @@ export function registerValidationHandlers(): void { console.error('[Materials] Get all error:', error) return { materials: [] } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) + console.warn('[Materials] Error disconnecting database:', closeError) } } } @@ -571,26 +630,13 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'materials:getStatistics', async (_event): Promise<{ stats: any }> => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const stats = await dao.getStatistics() return { stats } } catch (error) { console.error('[Materials] Get statistics error:', error) return { stats: null } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -628,7 +674,7 @@ export function registerValidationHandlers(): void { materialCodes?: string[] error?: string }> => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance() try { @@ -643,11 +689,13 @@ export function registerValidationHandlers(): void { const isAdmin = userInfo.userType === 'Admin' const username = userInfo.username + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`) - // Connect to MySQL - mysqlService = await getValidationMySqlService() + // Connect to database + dbService = await getValidationDatabaseService() // 1. Get order numbers from shared Production IDs const sharedIds = getSharedProductionIds() @@ -655,36 +703,55 @@ export function registerValidationHandlers(): void { if (sharedIds.length > 0) { console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`) - orderNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService) + orderNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`) } // 2. Get material codes from MaterialsToBeDeleted table let materialCodes: string[] = [] + const markedTableName = getTableName('dbo_MaterialsToBeDeleted') if (isAdmin) { // Admin sees all materials const allCodesSql = ` SELECT MaterialCode - FROM dbo_MaterialsToBeDeleted + FROM ${markedTableName} WHERE MaterialCode IS NOT NULL ` - const result = await mysqlService.query(allCodesSql) + const result = isSqlServer + ? await (dbService as SqlServerService).query(allCodesSql) + : await (dbService as MySqlService).query(allCodesSql) + materialCodes = result.rows .map(row => row.MaterialCode as string) .filter(Boolean) console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`) } else { // Regular users only see their own materials - const userMaterialsSql = ` - SELECT MaterialCode - FROM dbo_MaterialsToBeDeleted - WHERE ManagerName = ? AND MaterialCode IS NOT NULL - ` - const result = await mysqlService.query(userMaterialsSql, [username]) - materialCodes = result.rows - .map(row => row.MaterialCode as string) - .filter(Boolean) + if (isSqlServer) { + const sql = require('mssql') + const userMaterialsSql = ` + SELECT MaterialCode + FROM ${markedTableName} + WHERE ManagerName = @username AND MaterialCode IS NOT NULL + ` + const result = await (dbService as SqlServerService).queryWithParams(userMaterialsSql, { + username: { value: username, type: sql.NVarChar } + }) + materialCodes = result.rows + .map(row => row.MaterialCode as string) + .filter(Boolean) + } else { + const userMaterialsSql = ` + SELECT MaterialCode + FROM ${markedTableName} + WHERE ManagerName = ? AND MaterialCode IS NOT NULL + ` + const result = await (dbService as MySqlService).query(userMaterialsSql, [username]) + materialCodes = result.rows + .map(row => row.MaterialCode as string) + .filter(Boolean) + } console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`) } @@ -701,11 +768,11 @@ export function registerValidationHandlers(): void { error: `获取清理数据失败:${message}` } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[CleanerData] Error disconnecting MySQL:', closeError) + console.warn('[CleanerData] Error disconnecting database:', closeError) } } } diff --git a/src/main/services/database/discrete-material-plan-dao.ts b/src/main/services/database/discrete-material-plan-dao.ts index 6581945..5ac26b2 100644 --- a/src/main/services/database/discrete-material-plan-dao.ts +++ b/src/main/services/database/discrete-material-plan-dao.ts @@ -9,6 +9,8 @@ */ import { MySqlService } from './mysql' +import { SqlServerService } from './sql-server' +import sql from 'mssql' /** * Material plan record interface @@ -50,6 +52,7 @@ export interface MaterialPlanRecord { * Configuration for DiscreteMaterialPlanData table */ export const DISCRETE_MATERIAL_PLAN_CONFIG = { + TABLE_NAME_SQLSERVER: '[dbo].[DiscreteMaterialPlanData]', TABLE_NAME_MYSQL: 'dbo_DiscreteMaterialPlanData', COLUMNS: { ID: 'ID', @@ -90,25 +93,69 @@ export const DISCRETE_MATERIAL_PLAN_CONFIG = { */ export class DiscreteMaterialPlanDAO { private mysqlService: MySqlService | null = null + private sqlServerService: SqlServerService | null = null + private dbType: 'mysql' | 'sqlserver' = 'mysql' /** - * Get MySQL service instance + * Constructor - determine database type from environment */ - private async getMySqlService(): Promise { - if (this.mysqlService && this.mysqlService.isConnected()) { + constructor() { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + this.dbType = 'sqlserver' + } else { + this.dbType = 'mysql' + } + } + + /** + * Get the appropriate table name based on database type + */ + private getTableName(): string { + return this.dbType === 'sqlserver' + ? DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_SQLSERVER + : DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL + } + + /** + * Get database service instance (MySQL or SQL Server) + */ + private async getDatabaseService(): Promise { + if (this.dbType === 'sqlserver') { + if (this.sqlServerService && this.sqlServerService.isConnected()) { + return this.sqlServerService + } + + this.sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + + await this.sqlServerService.connect() + return this.sqlServerService + } else { + if (this.mysqlService && this.mysqlService.isConnected()) { + return this.mysqlService + } + + this.mysqlService = new MySqlService({ + 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 || '' + }) + + await this.mysqlService.connect() return this.mysqlService } - - this.mysqlService = new MySqlService({ - 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 || '' - }) - - await this.mysqlService.connect() - return this.mysqlService } // ==================== QUERY ALL ==================== @@ -119,11 +166,15 @@ export class DiscreteMaterialPlanDAO { */ async queryAll(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `SELECT * FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}` + const sql = `SELECT * FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rows } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query all error:', error) @@ -139,7 +190,8 @@ export class DiscreteMaterialPlanDAO { */ async queryAllDistinctByMaterialCode(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` WITH RankedRecords AS ( @@ -149,7 +201,7 @@ export class DiscreteMaterialPlanDAO { PARTITION BY MaterialCode ORDER BY CreateDate ASC, SequenceNumber ASC ) AS rn - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ) SELECT @@ -163,7 +215,10 @@ export class DiscreteMaterialPlanDAO { WHERE rn = 1 ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error) @@ -184,22 +239,42 @@ export class DiscreteMaterialPlanDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const batchSize = 2000 const allResults: any[] = [] for (let i = 0; i < sourceNumbers.length; i += batchSize) { const batch = sourceNumbers.slice(i, i + batchSize) - const placeholders = batch.map(() => '?').join(',') - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber IN (${placeholders}) - ` + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} - const result = await mysqlService.query(sql, batch) - allResults.push(...result.rows) + batch.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + allResults.push(...result.rows) + } else { + const placeholders = batch.map(() => '?').join(',') + + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sql, batch) + allResults.push(...result.rows) + } } return allResults @@ -222,39 +297,76 @@ export class DiscreteMaterialPlanDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const batchSize = 2000 const allResults: any[] = [] for (let i = 0; i < sourceNumbers.length; i += batchSize) { const batch = sourceNumbers.slice(i, i + batchSize) - const placeholders = batch.map(() => '?').join(',') - const sql = ` - WITH RankedRecords AS ( + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + batch.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + WITH RankedRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialCode IS NOT NULL + ) SELECT - *, - ROW_NUMBER() OVER ( - PARTITION BY MaterialCode - ORDER BY CreateDate ASC, SequenceNumber ASC - ) AS rn - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber IN (${placeholders}) - AND MaterialCode IS NOT NULL - ) - SELECT - Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, - ProductCode, ProductName, ProductUnit, ProductPlanQuantity, - UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, - SequenceNumber, MaterialCode, MaterialName, Specification, Model, - DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, - Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion - FROM RankedRecords - WHERE rn = 1 - ` + Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, + ProductCode, ProductName, ProductUnit, ProductPlanQuantity, + UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, + SequenceNumber, MaterialCode, MaterialName, Specification, Model, + DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, + Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion + FROM RankedRecords + WHERE rn = 1 + ` - const result = await mysqlService.query(sql, batch) - allResults.push(...result.rows) + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + allResults.push(...result.rows) + } else { + const placeholders = batch.map(() => '?').join(',') + + const sql = ` + WITH RankedRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialCode IS NOT NULL + ) + SELECT + Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, + ProductCode, ProductName, ProductUnit, ProductPlanQuantity, + UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, + SequenceNumber, MaterialCode, MaterialName, Specification, Model, + DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, + Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion + FROM RankedRecords + WHERE rn = 1 + ` + + const result = await (dbService as MySqlService).query(sql, batch) + allResults.push(...result.rows) + } } return allResults @@ -271,16 +383,32 @@ export class DiscreteMaterialPlanDAO { */ async queryBySourceNumber(sourceNumber: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber = @sourceNumber + ` - const result = await mysqlService.query(sql, [sourceNumber]) - return result.rows + const result = await (dbService as SqlServerService).queryWithParams(sql, { + sourceNumber: { value: sourceNumber, type: sql.NVarChar } + }) + + return result.rows + } else { + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber = ? + ` + + const result = await (dbService as MySqlService).query(sql, [sourceNumber]) + + return result.rows + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error) return [] @@ -296,16 +424,32 @@ export class DiscreteMaterialPlanDAO { */ async queryByPlanNumber(planNumber: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE PlanNumber = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber = @planNumber + ` - const result = await mysqlService.query(sql, [planNumber]) - return result.rows + const result = await (dbService as SqlServerService).queryWithParams(sql, { + planNumber: { value: planNumber, type: sql.NVarChar } + }) + + return result.rows + } else { + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber = ? + ` + + const result = await (dbService as MySqlService).query(sql, [planNumber]) + + return result.rows + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error) return [] @@ -323,17 +467,37 @@ export class DiscreteMaterialPlanDAO { } try { - const mysqlService = await this.getMySqlService() - const placeholders = planNumbers.map(() => '?').join(',') + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE PlanNumber IN (${placeholders}) - ` + if (this.dbType === 'sqlserver') { + const placeholders = planNumbers.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} - const result = await mysqlService.query(sql, planNumbers) - return result.rows + planNumbers.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + return result.rows + } else { + const placeholders = planNumbers.map(() => '?').join(',') + + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sql, planNumbers) + return result.rows + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error) return [] @@ -348,11 +512,15 @@ export class DiscreteMaterialPlanDAO { */ async countAll(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `SELECT COUNT(*) as count FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}` + const sql = `SELECT COUNT(*) as count FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { console.error('[DiscreteMaterialPlanDAO] Count all error:', error) @@ -367,16 +535,32 @@ export class DiscreteMaterialPlanDAO { */ async countByPlanNumber(planNumber: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT COUNT(*) as count - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE PlanNumber = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE PlanNumber = @planNumber + ` - const result = await mysqlService.query(sql, [planNumber]) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + planNumber: { value: planNumber, type: sql.NVarChar } + }) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE PlanNumber = ? + ` + + const result = await (dbService as MySqlService).query(sql, [planNumber]) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error) return 0 @@ -390,32 +574,59 @@ export class DiscreteMaterialPlanDAO { */ async getUniqueMaterialNames(sourceNumbers?: string[]): Promise { try { - const mysqlService = await this.getMySqlService() - - let sql: string - let params: any[] = [] + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() if (sourceNumbers && sourceNumbers.length > 0) { - const placeholders = sourceNumbers.map(() => '?').join(',') - sql = ` - SELECT DISTINCT MaterialName - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber IN (${placeholders}) - AND MaterialName IS NOT NULL - ` - params = sourceNumbers + if (this.dbType === 'sqlserver') { + const placeholders = sourceNumbers.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + sourceNumbers.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + SELECT DISTINCT MaterialName + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialName IS NOT NULL + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + return result.rows + .map(row => row.MaterialName as string) + .filter(Boolean) + } else { + const placeholders = sourceNumbers.map(() => '?').join(',') + + const sql = ` + SELECT DISTINCT MaterialName + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialName IS NOT NULL + ` + + const result = await (dbService as MySqlService).query(sql, sourceNumbers) + return result.rows + .map(row => row.MaterialName as string) + .filter(Boolean) + } } else { - sql = ` + const sql = ` SELECT DISTINCT MaterialName - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialName IS NOT NULL ` - } - const result = await mysqlService.query(sql, params) - return result.rows - .map(row => row.MaterialName as string) - .filter(Boolean) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + + return result.rows + .map(row => row.MaterialName as string) + .filter(Boolean) + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error) return [] @@ -428,7 +639,8 @@ export class DiscreteMaterialPlanDAO { */ async getStatistics(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT @@ -437,10 +649,13 @@ export class DiscreteMaterialPlanDAO { COUNT(DISTINCT SourceNumber) as uniqueOrders, MIN(CreateDate) as earliestRecord, MAX(CreateDate) as latestRecord - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows.length > 0 ? result.rows[0] : {} } catch (error) { console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error) @@ -456,5 +671,9 @@ export class DiscreteMaterialPlanDAO { await this.mysqlService.disconnect() this.mysqlService = null } + if (this.sqlServerService) { + await this.sqlServerService.disconnect() + this.sqlServerService = null + } } } diff --git a/src/main/services/database/materials-to-be-deleted-dao.ts b/src/main/services/database/materials-to-be-deleted-dao.ts index 520aabf..509fd9b 100644 --- a/src/main/services/database/materials-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-to-be-deleted-dao.ts @@ -9,6 +9,8 @@ */ import { MySqlService } from './mysql' +import { SqlServerService } from './sql-server' +import sql from 'mssql' /** * Material record interface @@ -41,6 +43,7 @@ export interface MaterialStats { * Configuration for MaterialsToBeDeleted table */ export const MATERIALS_TO_BE_DELETED_CONFIG = { + TABLE_NAME_SQLSERVER: '[dbo].[MaterialsToBeDeleted]', TABLE_NAME_MYSQL: 'dbo_MaterialsToBeDeleted', COLUMNS: { ID: 'ID', @@ -54,25 +57,69 @@ export const MATERIALS_TO_BE_DELETED_CONFIG = { */ export class MaterialsToBeDeletedDAO { private mysqlService: MySqlService | null = null + private sqlServerService: SqlServerService | null = null + private dbType: 'mysql' | 'sqlserver' = 'mysql' /** - * Get MySQL service instance + * Constructor - determine database type from environment */ - private async getMySqlService(): Promise { - if (this.mysqlService && this.mysqlService.isConnected()) { + constructor() { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + this.dbType = 'sqlserver' + } else { + this.dbType = 'mysql' + } + } + + /** + * Get the appropriate table name based on database type + */ + private getTableName(): string { + return this.dbType === 'sqlserver' + ? MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_SQLSERVER + : MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL + } + + /** + * Get database service instance (MySQL or SQL Server) + */ + private async getDatabaseService(): Promise { + if (this.dbType === 'sqlserver') { + if (this.sqlServerService && this.sqlServerService.isConnected()) { + return this.sqlServerService + } + + this.sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + + await this.sqlServerService.connect() + return this.sqlServerService + } else { + if (this.mysqlService && this.mysqlService.isConnected()) { + return this.mysqlService + } + + this.mysqlService = new MySqlService({ + 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 || '' + }) + + await this.mysqlService.connect() return this.mysqlService } - - this.mysqlService = new MySqlService({ - 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 || '' - }) - - await this.mysqlService.connect() - return this.mysqlService } // ==================== UPSERT (MERGE) ==================== @@ -90,15 +137,34 @@ export class MaterialsToBeDeletedDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() + const manager = managerName?.trim() || null - const sql = ` - INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) - ` + if (this.dbType === 'sqlserver') { + const sql = ` + MERGE ${tableName} AS target + USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + ON target.MaterialCode = source.MaterialCode + WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName + WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); + ` + + await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar }, + managerName: { value: manager, type: sql.NVarChar } + }) + } else { + const sql = ` + INSERT INTO ${tableName} (MaterialCode, ManagerName) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) + ` + + await (dbService as MySqlService).query(sql, [code, manager]) + } - await mysqlService.query(sql, [materialCode.trim(), managerName?.trim() || null]) return true } catch (error) { console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error) @@ -123,7 +189,8 @@ export class MaterialsToBeDeletedDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() for (const material of materials) { const materialCode = material.materialCode?.trim() @@ -135,13 +202,29 @@ export class MaterialsToBeDeletedDAO { } try { - const sql = ` - INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) - ` + if (this.dbType === 'sqlserver') { + const sql = ` + MERGE ${tableName} AS target + USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + ON target.MaterialCode = source.MaterialCode + WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName + WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); + ` + + await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: materialCode, type: sql.NVarChar }, + managerName: { value: managerName || null, type: sql.NVarChar } + }) + } else { + const sql = ` + INSERT INTO ${tableName} (MaterialCode, ManagerName) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) + ` + + await (dbService as MySqlService).query(sql, [materialCode, managerName || null]) + } - await mysqlService.query(sql, [materialCode, managerName || null]) stats.success++ } catch (error) { console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error) @@ -164,15 +247,19 @@ export class MaterialsToBeDeletedDAO { */ async getAllMaterialCodes(): Promise> { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT MaterialCode - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return new Set(result.rows.map(row => row.MaterialCode as string).filter(Boolean)) } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error) @@ -186,16 +273,20 @@ export class MaterialsToBeDeletedDAO { */ async getAllRecords(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT ID, MaterialCode, ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ORDER BY ManagerName, MaterialCode ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows.map(row => ({ id: row.ID as number, materialCode: row.MaterialCode as string, @@ -214,21 +305,42 @@ export class MaterialsToBeDeletedDAO { */ async getMaterialsByManager(managerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT ID, MaterialCode, ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE ManagerName = ? AND MaterialCode IS NOT NULL - ORDER BY MaterialCode - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE ManagerName = @managerName AND MaterialCode IS NOT NULL + ORDER BY MaterialCode + ` - const result = await mysqlService.query(sql, [managerName]) - return result.rows.map(row => ({ - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string - })) + const result = await (dbService as SqlServerService).queryWithParams(sql, { + managerName: { value: managerName, type: sql.NVarChar } + }) + + return result.rows.map(row => ({ + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + })) + } else { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE ManagerName = ? AND MaterialCode IS NOT NULL + ORDER BY MaterialCode + ` + + const result = await (dbService as MySqlService).query(sql, [managerName]) + + return result.rows.map(row => ({ + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + })) + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error) return [] @@ -241,16 +353,20 @@ export class MaterialsToBeDeletedDAO { */ async getManagers(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT DISTINCT ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE ManagerName IS NOT NULL ORDER BY ManagerName ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows.map(row => row.ManagerName as string).filter(Boolean) } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get managers error:', error) @@ -265,24 +381,50 @@ export class MaterialsToBeDeletedDAO { */ async getRecordByMaterialCode(materialCode: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() - const sql = ` - SELECT ID, MaterialCode, ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE MaterialCode = @materialCode + ` - const result = await mysqlService.query(sql, [materialCode.trim()]) - if (result.rows.length === 0) { - return null - } + const result = await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar } + }) - const row = result.rows[0] - return { - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string + if (result.rows.length === 0) { + return null + } + + const row = result.rows[0] + return { + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + } + } else { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE MaterialCode = ? + ` + + const result = await (dbService as MySqlService).query(sql, [code]) + + if (result.rows.length === 0) { + return null + } + + const row = result.rows[0] + return { + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + } } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error) @@ -299,15 +441,31 @@ export class MaterialsToBeDeletedDAO { */ async deleteByMaterialCode(materialCode: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() - const sql = ` - DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode = @materialCode + ` - const result = await mysqlService.query(sql, [materialCode.trim()]) - return result.rowCount > 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar } + }) + + return result.rowCount > 0 + } else { + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode = ? + ` + + const result = await (dbService as MySqlService).query(sql, [code]) + + return result.rowCount > 0 + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error) return false @@ -321,15 +479,30 @@ export class MaterialsToBeDeletedDAO { */ async deleteByManager(managerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE ManagerName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + DELETE FROM ${tableName} + WHERE ManagerName = @managerName + ` - const result = await mysqlService.query(sql, [managerName]) - return result.rowCount + const result = await (dbService as SqlServerService).queryWithParams(sql, { + managerName: { value: managerName, type: sql.NVarChar } + }) + + return result.rowCount + } else { + const sql = ` + DELETE FROM ${tableName} + WHERE ManagerName = ? + ` + + const result = await (dbService as MySqlService).query(sql, [managerName]) + + return result.rowCount + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error) return 0 @@ -342,11 +515,15 @@ export class MaterialsToBeDeletedDAO { */ async deleteAllMaterials(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}` + const sql = `DELETE FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rowCount } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error) @@ -368,19 +545,38 @@ export class MaterialsToBeDeletedDAO { const batchSize = 1000 try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() for (let i = 0; i < materialCodes.length; i += batchSize) { const batch = materialCodes.slice(i, i + batchSize) - const placeholders = batch.map(() => '?').join(',') - const sql = ` - DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode IN (${placeholders}) - ` + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} - const result = await mysqlService.query(sql, batch) - totalDeleted += result.rowCount + batch.forEach((code, idx) => { + params[`p${idx}`] = { value: code.trim(), type: sql.NVarChar } + }) + + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + totalDeleted += result.rowCount + } else { + const placeholders = batch.map(() => '?').join(',') + + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sql, batch) + totalDeleted += result.rowCount + } } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error) @@ -398,16 +594,33 @@ export class MaterialsToBeDeletedDAO { */ async materialExists(materialCode: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() - const sql = ` - SELECT COUNT(*) as count - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE MaterialCode = @materialCode + ` - const result = await mysqlService.query(sql, [materialCode.trim()]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar } + }) + + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE MaterialCode = ? + ` + + const result = await (dbService as MySqlService).query(sql, [code]) + + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Material exists error:', error) return false @@ -420,11 +633,15 @@ export class MaterialsToBeDeletedDAO { */ async countAll(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `SELECT COUNT(*) as count FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}` + const sql = `SELECT COUNT(*) as count FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { console.error('[MaterialsToBeDeletedDAO] Count all error:', error) @@ -439,16 +656,32 @@ export class MaterialsToBeDeletedDAO { */ async countByManager(managerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT COUNT(*) as count - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE ManagerName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE ManagerName = @managerName + ` - const result = await mysqlService.query(sql, [managerName]) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + managerName: { value: managerName, type: sql.NVarChar } + }) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE ManagerName = ? + ` + + const result = await (dbService as MySqlService).query(sql, [managerName]) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error) return 0 @@ -461,30 +694,37 @@ export class MaterialsToBeDeletedDAO { */ async getStatistics(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() // Get total and unique managers const statsSql = ` SELECT COUNT(*) as totalMaterials, COUNT(DISTINCT ManagerName) as uniqueManagers - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ` - const statsResult = await mysqlService.query(statsSql) + const statsResult = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(statsSql) + : await (dbService as MySqlService).query(statsSql) + const stats = statsResult.rows[0] || {} // Get materials per manager const managerSql = ` SELECT ManagerName, COUNT(*) as count - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE ManagerName IS NOT NULL GROUP BY ManagerName ORDER BY count DESC ` - const managerResult = await mysqlService.query(managerSql) + const managerResult = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(managerSql) + : await (dbService as MySqlService).query(managerSql) + const materialsPerManager = managerResult.rows.map(row => ({ [row.ManagerName as string]: row.count as number })) @@ -512,5 +752,9 @@ export class MaterialsToBeDeletedDAO { await this.mysqlService.disconnect() this.mysqlService = null } + if (this.sqlServerService) { + await this.sqlServerService.disconnect() + this.sqlServerService = null + } } } diff --git a/src/main/services/database/sql-server.ts b/src/main/services/database/sql-server.ts index 2222a21..a2936a3 100644 --- a/src/main/services/database/sql-server.ts +++ b/src/main/services/database/sql-server.ts @@ -98,8 +98,9 @@ export class SqlServerService { const result = await request.query(sqlString) // Convert recordset to array of objects - const columns = result.recordset.columns?.map((col) => col.name) || [] const rows = result.recordset as Record[] + // Extract column names from the first row if available + const columns = rows.length > 0 ? Object.keys(rows[0]) : [] return { rows, @@ -137,8 +138,9 @@ export class SqlServerService { const result = await request.query(sqlString) // Convert recordset to array of objects - const columns = result.recordset.columns?.map((col) => col.name) || [] const rows = result.recordset as Record[] + // Extract column names from the first row if available + const columns = rows.length > 0 ? Object.keys(rows[0]) : [] return { rows, diff --git a/src/main/services/user/bip-users-dao.ts b/src/main/services/user/bip-users-dao.ts index 1a281e7..ea3ec3d 100644 --- a/src/main/services/user/bip-users-dao.ts +++ b/src/main/services/user/bip-users-dao.ts @@ -9,6 +9,8 @@ */ import { MySqlService } from '../database/mysql' +import { SqlServerService } from '../database/sql-server' +import sql from 'mssql' import type { UserInfo } from '../../types/user.types' /** @@ -35,25 +37,69 @@ export const BIP_USERS_CONFIG = { */ export class BIPUsersDAO { private mysqlService: MySqlService | null = null + private sqlServerService: SqlServerService | null = null + private dbType: 'mysql' | 'sqlserver' = 'mysql' /** - * Get MySQL service instance + * Constructor - determine database type from environment */ - private async getMySqlService(): Promise { - if (this.mysqlService && this.mysqlService.isConnected()) { + constructor() { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + this.dbType = 'sqlserver' + } else { + this.dbType = 'mysql' + } + } + + /** + * Get the appropriate table name based on database type + */ + private getTableName(): string { + return this.dbType === 'sqlserver' + ? BIP_USERS_CONFIG.TABLE_NAME_SQLSERVER + : BIP_USERS_CONFIG.TABLE_NAME_MYSQL + } + + /** + * Get database service instance (MySQL or SQL Server) + */ + private async getDatabaseService(): Promise { + if (this.dbType === 'sqlserver') { + if (this.sqlServerService && this.sqlServerService.isConnected()) { + return this.sqlServerService + } + + this.sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + + await this.sqlServerService.connect() + return this.sqlServerService + } else { + if (this.mysqlService && this.mysqlService.isConnected()) { + return this.mysqlService + } + + this.mysqlService = new MySqlService({ + 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 || '' + }) + + await this.mysqlService.connect() return this.mysqlService } - - this.mysqlService = new MySqlService({ - 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 || '' - }) - - await this.mysqlService.connect() - return this.mysqlService } /** @@ -64,25 +110,49 @@ export class BIPUsersDAO { */ async authenticate(username: string, password: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT ID, UserName, UserType - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE UserName = ? AND Password = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE UserName = @username AND Password = @password + ` - const result = await mysqlService.query(sql, [username, password]) + const result = await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar }, + password: { value: password, type: sql.NVarChar } + }) - if (result.rows.length > 0) { - const row = result.rows[0] - return { - id: row.ID as number, - username: row.UserName as string, - userType: row.UserType as 'Admin' | 'User' | 'Guest' + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } } + return null + } else { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE UserName = ? AND Password = ? + ` + + const result = await (dbService as MySqlService).query(sql, [username, password]) + + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } + } + return null } - return null } catch (error) { console.error('[BIPUsersDAO] Authenticate error:', error) return null @@ -96,25 +166,48 @@ export class BIPUsersDAO { */ async authenticateByComputerName(computerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT ID, UserName, UserType - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE ComputerNmae = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE ComputerNmae = @computerName + ` - const result = await mysqlService.query(sql, [computerName]) + const result = await (dbService as SqlServerService).queryWithParams(sql, { + computerName: { value: computerName, type: sql.NVarChar } + }) - if (result.rows.length > 0) { - const row = result.rows[0] - return { - id: row.ID as number, - username: row.UserName as string, - userType: row.UserType as 'Admin' | 'User' | 'Guest' + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } } + return null + } else { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE ComputerNmae = ? + ` + + const result = await (dbService as MySqlService).query(sql, [computerName]) + + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } + } + return null } - return null } catch (error) { console.error('[BIPUsersDAO] Authenticate by computer name error:', error) return null @@ -127,15 +220,18 @@ export class BIPUsersDAO { */ async getAllUsers(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT ID, UserName, UserType, CreateTime - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} ORDER BY UserName ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) return result.rows.map(row => ({ id: row.ID as number, @@ -164,29 +260,63 @@ export class BIPUsersDAO { computerName: string = '' ): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - let sql: string - let params: any[] + if (this.dbType === 'sqlserver') { + let sql: string + let params: Record - if (computerName) { - sql = ` - INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - (UserName, Password, UserType, ComputerNmae) - VALUES (?, ?, ?, ?) - ` - params = [username, password, userType, computerName] + if (computerName) { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType, ComputerNmae) + VALUES (@username, @password, @userType, @computerName) + ` + params = { + username: { value: username, type: sql.NVarChar }, + password: { value: password, type: sql.NVarChar }, + userType: { value: userType, type: sql.NVarChar }, + computerName: { value: computerName, type: sql.NVarChar } + } + } else { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType) + VALUES (@username, @password, @userType) + ` + params = { + username: { value: username, type: sql.NVarChar }, + password: { value: password, type: sql.NVarChar }, + userType: { value: userType, type: sql.NVarChar } + } + } + + await (dbService as SqlServerService).queryWithParams(sql, params) + return true } else { - sql = ` - INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - (UserName, Password, UserType) - VALUES (?, ?, ?) - ` - params = [username, password, userType] - } + let sql: string + let params: any[] - await mysqlService.query(sql, params) - return true + if (computerName) { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType, ComputerNmae) + VALUES (?, ?, ?, ?) + ` + params = [username, password, userType, computerName] + } else { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType) + VALUES (?, ?, ?) + ` + params = [username, password, userType] + } + + await (dbService as MySqlService).query(sql, params) + return true + } } catch (error) { console.error('[BIPUsersDAO] Create user error:', error) return false @@ -201,16 +331,31 @@ export class BIPUsersDAO { */ async updateUserType(username: string, userType: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - SET UserType = ? - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + UPDATE ${tableName} + SET UserType = @userType + WHERE UserName = @username + ` - await mysqlService.query(sql, [userType, username]) - return true + await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar }, + userType: { value: userType, type: sql.NVarChar } + }) + return true + } else { + const sql = ` + UPDATE ${tableName} + SET UserType = ? + WHERE UserName = ? + ` + + await (dbService as MySqlService).query(sql, [userType, username]) + return true + } } catch (error) { console.error('[BIPUsersDAO] Update user type error:', error) return false @@ -225,16 +370,31 @@ export class BIPUsersDAO { */ async updatePassword(username: string, newPassword: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - SET Password = ? - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + UPDATE ${tableName} + SET Password = @newPassword + WHERE UserName = @username + ` - await mysqlService.query(sql, [newPassword, username]) - return true + await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar }, + newPassword: { value: newPassword, type: sql.NVarChar } + }) + return true + } else { + const sql = ` + UPDATE ${tableName} + SET Password = ? + WHERE UserName = ? + ` + + await (dbService as MySqlService).query(sql, [newPassword, username]) + return true + } } catch (error) { console.error('[BIPUsersDAO] Update password error:', error) return false @@ -248,15 +408,28 @@ export class BIPUsersDAO { */ async deleteUser(username: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - DELETE FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + DELETE FROM ${tableName} + WHERE UserName = @username + ` - await mysqlService.query(sql, [username]) - return true + await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar } + }) + return true + } else { + const sql = ` + DELETE FROM ${tableName} + WHERE UserName = ? + ` + + await (dbService as MySqlService).query(sql, [username]) + return true + } } catch (error) { console.error('[BIPUsersDAO] Delete user error:', error) return false @@ -270,16 +443,30 @@ export class BIPUsersDAO { */ async userExists(username: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT COUNT(*) as count - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE UserName = @username + ` - const result = await mysqlService.query(sql, [username]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar } + }) + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE UserName = ? + ` + + const result = await (dbService as MySqlService).query(sql, [username]) + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } } catch (error) { console.error('[BIPUsersDAO] User exists error:', error) return false @@ -294,5 +481,9 @@ export class BIPUsersDAO { await this.mysqlService.disconnect() this.mysqlService = null } + if (this.sqlServerService) { + await this.sqlServerService.disconnect() + this.sqlServerService = null + } } }