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 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-03-02 11:10:59 +08:00
parent bbdd1e18f8
commit 05d856309f
6 changed files with 1207 additions and 473 deletions

View File

@@ -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<MySqlService> {
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<MySqlService | SqlServerService> {
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<any[]> {
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<any[]> {
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<string, { value: string; type: sql.ISqlType }> = {}
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<string, { value: string; type: sql.ISqlType }> = {}
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<any[]> {
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<any[]> {
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<string, { value: string; type: sql.ISqlType }> = {}
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<number> {
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<number> {
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<string[]> {
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<string, { value: string; type: sql.ISqlType }> = {}
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<any> {
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
}
}
}