fix(db): complete PostgreSQL integration in validation and cleaner services
OrderNumberResolver, validation, and cleaner services had incomplete PostgreSQL support - they only handled SQL Server and MySQL, causing PostgreSQL to fall through to MySQL code paths with invalid syntax (backticks, ? placeholders) and missing schema.table name splitting. Changes: - Add PostgreSQL SQL generation ($N params, double-quoted identifiers) in OrderNumberResolver, validation-application-service, production-input-service, and validation-database - Add PostgreSQL to database factory functions in validation-database and cleaner-application-service - Add UPPER, LOWER, and 40+ common SQL functions to SQL_KEYWORDS to prevent prepareSql() from quoting them as identifiers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import type { WebContents } from 'electron'
|
import type { WebContents } from 'electron'
|
||||||
import type { MySqlService } from '../database/mysql'
|
import type { IDatabaseService } from '../../types/database.types'
|
||||||
import type { SqlServerService } from '../database/sql-server'
|
|
||||||
import { ErpAuthService } from '../erp/erp-auth'
|
import { ErpAuthService } from '../erp/erp-auth'
|
||||||
import { CleanerService } from '../erp/cleaner'
|
import { CleanerService } from '../erp/cleaner'
|
||||||
import { OrderNumberResolver } from '../erp/order-resolver'
|
import { OrderNumberResolver } from '../erp/order-resolver'
|
||||||
import { MySqlService as MySqlServiceImpl } from '../database/mysql'
|
import { MySqlService as MySqlServiceImpl } from '../database/mysql'
|
||||||
import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server'
|
import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server'
|
||||||
|
import { PostgreSqlService as PostgreSqlServiceImpl } from '../database/postgresql'
|
||||||
import { ConfigManager } from '../config/config-manager'
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { ResultExporter } from '../excel/result-exporter'
|
import { ResultExporter } from '../excel/result-exporter'
|
||||||
import { CleanerReportGenerator } from '../report/cleaner-report-generator'
|
import { CleanerReportGenerator } from '../report/cleaner-report-generator'
|
||||||
@@ -26,13 +26,11 @@ import type {
|
|||||||
|
|
||||||
const log = createLogger('CleanerApplicationService')
|
const log = createLogger('CleanerApplicationService')
|
||||||
|
|
||||||
type DatabaseService = MySqlService | SqlServerService
|
|
||||||
|
|
||||||
export class CleanerApplicationService {
|
export class CleanerApplicationService {
|
||||||
async runCleaner(eventSender: WebContents, input: CleanerInput): Promise<CleanerResult> {
|
async runCleaner(eventSender: WebContents, input: CleanerInput): Promise<CleanerResult> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
let authService: ErpAuthService | null = null
|
let authService: ErpAuthService | null = null
|
||||||
let dbService: DatabaseService | null = null
|
let dbService: IDatabaseService | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info('Fetching ERP configuration from database...')
|
log.info('Fetching ERP configuration from database...')
|
||||||
@@ -46,7 +44,7 @@ export class CleanerApplicationService {
|
|||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
log.info(
|
log.info(
|
||||||
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...`
|
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : dbType === 'postgresql' ? 'PostgreSQL' : 'MySQL'} for order resolution...`
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -201,7 +199,7 @@ export class CleanerApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDatabaseService(): Promise<DatabaseService> {
|
private async getDatabaseService(): Promise<IDatabaseService> {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const config = configManager.getConfig()
|
const config = configManager.getConfig()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
@@ -223,6 +221,19 @@ export class CleanerApplicationService {
|
|||||||
return sqlServerService
|
return sqlServerService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbType === 'postgresql') {
|
||||||
|
const dbConfig = config.database.postgresql
|
||||||
|
const pgService = new PostgreSqlServiceImpl({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
await pgService.connect()
|
||||||
|
return pgService
|
||||||
|
}
|
||||||
|
|
||||||
const dbConfig = config.database.mysql
|
const dbConfig = config.database.mysql
|
||||||
const mysqlService = new MySqlServiceImpl({
|
const mysqlService = new MySqlServiceImpl({
|
||||||
host: dbConfig.host,
|
host: dbConfig.host,
|
||||||
|
|||||||
@@ -238,6 +238,53 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'FIRST',
|
'FIRST',
|
||||||
'LAST',
|
'LAST',
|
||||||
|
|
||||||
|
// ==================== Scalar & String Functions ====================
|
||||||
|
'UPPER',
|
||||||
|
'LOWER',
|
||||||
|
'TRIM',
|
||||||
|
'LTRIM',
|
||||||
|
'RTRIM',
|
||||||
|
'BTRIM',
|
||||||
|
'SUBSTRING',
|
||||||
|
'CONCAT',
|
||||||
|
'LENGTH',
|
||||||
|
'CHAR_LENGTH',
|
||||||
|
'CHARACTER_LENGTH',
|
||||||
|
'REPLACE',
|
||||||
|
'POSITION',
|
||||||
|
'OVERLAY',
|
||||||
|
'LPAD',
|
||||||
|
'RPAD',
|
||||||
|
'REPEAT',
|
||||||
|
'REVERSE',
|
||||||
|
'SPLIT_PART',
|
||||||
|
'INITCAP',
|
||||||
|
'NORMALIZE',
|
||||||
|
'CHR',
|
||||||
|
'ASCII',
|
||||||
|
'FORMAT',
|
||||||
|
|
||||||
|
// ==================== Numeric Functions ====================
|
||||||
|
'ABS',
|
||||||
|
'CEIL',
|
||||||
|
'CEILING',
|
||||||
|
'FLOOR',
|
||||||
|
'ROUND',
|
||||||
|
'POWER',
|
||||||
|
'SQRT',
|
||||||
|
'MOD',
|
||||||
|
'SIGN',
|
||||||
|
'TRUNC',
|
||||||
|
|
||||||
|
// ==================== Date/Time Functions ====================
|
||||||
|
'EXTRACT',
|
||||||
|
'DATE_TRUNC',
|
||||||
|
'TO_CHAR',
|
||||||
|
'TO_DATE',
|
||||||
|
'TO_TIMESTAMP',
|
||||||
|
'TO_NUMBER',
|
||||||
|
'AGE',
|
||||||
|
|
||||||
// ==================== Pattern Matching ====================
|
// ==================== Pattern Matching ====================
|
||||||
'BETWEEN',
|
'BETWEEN',
|
||||||
'LIKE',
|
'LIKE',
|
||||||
|
|||||||
@@ -58,23 +58,34 @@ export class OrderNumberResolver {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get table name based on database type
|
* Get table name based on database type
|
||||||
* Converts MySQL schema_tablename format to SQL Server [schema].[tablename] format
|
* Converts schema_tablename format to database-specific quoting:
|
||||||
* e.g., productionContractData_26年压力表合同数据 -> [productionContractData].[26年压力表合同数据]
|
* - SQL Server: [schema].[tablename]
|
||||||
* dbo_MaterialsToBeDeleted -> [dbo].[MaterialsToBeDeleted]
|
* - PostgreSQL: "schema"."tablename"
|
||||||
|
* - MySQL: schema_tablename (as-is)
|
||||||
|
* e.g., productionContractData_26年压力表合同数据 ->
|
||||||
|
* SQL Server: [productionContractData].[26年压力表合同数据]
|
||||||
|
* PostgreSQL: "productionContractData"."26年压力表合同数据"
|
||||||
|
* MySQL: productionContractData_26年压力表合同数据
|
||||||
*/
|
*/
|
||||||
private getTableName(tableName: string): string {
|
private getTableName(tableName: string): string {
|
||||||
if (this.dbService.type === 'sqlserver') {
|
if (this.dbService.type === 'sqlserver' || this.dbService.type === 'postgresql') {
|
||||||
// Find the FIRST underscore to split schema and table name
|
// Find the FIRST underscore to split schema and table name
|
||||||
// This handles patterns like: schema_tablename
|
// This handles patterns like: schema_tablename
|
||||||
const firstUnderscoreIndex = tableName.indexOf('_')
|
const firstUnderscoreIndex = tableName.indexOf('_')
|
||||||
if (firstUnderscoreIndex > 0) {
|
if (firstUnderscoreIndex > 0) {
|
||||||
const schema = tableName.substring(0, firstUnderscoreIndex)
|
const schema = tableName.substring(0, firstUnderscoreIndex)
|
||||||
const actualTableName = tableName.substring(firstUnderscoreIndex + 1)
|
const actualTableName = tableName.substring(firstUnderscoreIndex + 1)
|
||||||
|
if (this.dbService.type === 'sqlserver') {
|
||||||
return `[${schema}].[${actualTableName}]`
|
return `[${schema}].[${actualTableName}]`
|
||||||
}
|
}
|
||||||
// If no underscore found, default to dbo schema
|
return `"${schema}"."${actualTableName}"`
|
||||||
|
}
|
||||||
|
// If no underscore found, default schema
|
||||||
|
if (this.dbService.type === 'sqlserver') {
|
||||||
return `[dbo].[${tableName}]`
|
return `[dbo].[${tableName}]`
|
||||||
}
|
}
|
||||||
|
return `"public"."${tableName}"`
|
||||||
|
}
|
||||||
return tableName
|
return tableName
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +118,11 @@ export class OrderNumberResolver {
|
|||||||
// 使用 COLLATE 指定不区分大小写的排序规则
|
// 使用 COLLATE 指定不区分大小写的排序规则
|
||||||
sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS = @p0`
|
sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS = @p0`
|
||||||
params = [productionId]
|
params = [productionId]
|
||||||
|
} else if (this.dbService.type === 'postgresql') {
|
||||||
|
// PostgreSQL: 使用双引号保护中文标识符,UPPER 实现不区分大小写
|
||||||
|
// prepareSql() 会保留已双引号包裹的标识符
|
||||||
|
sql = `SELECT "${dbConfig.FIELD_ORDER_NUMBER}" FROM "${tableName}" WHERE UPPER("${dbConfig.FIELD_PRODUCTION_ID}") = UPPER($1) LIMIT 1`
|
||||||
|
params = [productionId]
|
||||||
} else {
|
} else {
|
||||||
// MySQL 默认不区分大小写,但显式使用 UPPER 确保一致性
|
// MySQL 默认不区分大小写,但显式使用 UPPER 确保一致性
|
||||||
sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE UPPER(\`${dbConfig.FIELD_PRODUCTION_ID}\`) = UPPER(?) LIMIT 1`
|
sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE UPPER(\`${dbConfig.FIELD_PRODUCTION_ID}\`) = UPPER(?) LIMIT 1`
|
||||||
@@ -155,6 +171,10 @@ export class OrderNumberResolver {
|
|||||||
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
||||||
// 使用 COLLATE 指定不区分大小写的排序规则
|
// 使用 COLLATE 指定不区分大小写的排序规则
|
||||||
sql = `SELECT DISTINCT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS IN (${placeholders})`
|
sql = `SELECT DISTINCT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS IN (${placeholders})`
|
||||||
|
} else if (this.dbService.type === 'postgresql') {
|
||||||
|
// PostgreSQL: 使用双引号保护中文标识符,UPPER 实现不区分大小写
|
||||||
|
const pgPlaceholders = uniqueProductionIds.map((_, i) => `UPPER($${i + 1})`).join(', ')
|
||||||
|
sql = `SELECT DISTINCT "${dbConfig.FIELD_PRODUCTION_ID}", "${dbConfig.FIELD_ORDER_NUMBER}" FROM "${tableName}" WHERE UPPER("${dbConfig.FIELD_PRODUCTION_ID}") IN (${pgPlaceholders})`
|
||||||
} else {
|
} else {
|
||||||
const idPlaceholders = uniqueProductionIds.map(() => 'UPPER(?)').join(', ')
|
const idPlaceholders = uniqueProductionIds.map(() => 'UPPER(?)').join(', ')
|
||||||
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function getSourceNumbersFromInputs(
|
|||||||
const productionIds: string[] = []
|
const productionIds: string[] = []
|
||||||
const orderNumbers: string[] = []
|
const orderNumbers: string[] = []
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const isSqlServer = configManager.getDatabaseType() === 'sqlserver'
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
for (const item of inputs) {
|
for (const item of inputs) {
|
||||||
const type = identifyInputType(item)
|
const type = identifyInputType(item)
|
||||||
@@ -44,7 +44,7 @@ export async function getSourceNumbersFromInputs(
|
|||||||
const contractTableName = getValidationTableName('productionContractData_26年压力表合同数据')
|
const contractTableName = getValidationTableName('productionContractData_26年压力表合同数据')
|
||||||
const batchSize = 2000
|
const batchSize = 2000
|
||||||
|
|
||||||
if (isSqlServer) {
|
if (dbType === 'sqlserver') {
|
||||||
const sql = await import('mssql')
|
const sql = await import('mssql')
|
||||||
const allOrderNumbers: string[] = []
|
const allOrderNumbers: string[] = []
|
||||||
|
|
||||||
@@ -71,6 +71,22 @@ export async function getSourceNumbersFromInputs(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderNumbers.push(...allOrderNumbers)
|
||||||
|
} else if (dbType === 'postgresql') {
|
||||||
|
const allOrderNumbers: string[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < productionIds.length; i += batchSize) {
|
||||||
|
const batch = productionIds.slice(i, i + batchSize)
|
||||||
|
const placeholders = batch.map((_, idx) => `$${idx + 1}`).join(',')
|
||||||
|
const contractSql = `
|
||||||
|
SELECT DISTINCT "生产订单号"
|
||||||
|
FROM ${contractTableName}
|
||||||
|
WHERE "总排号" IN (${placeholders})
|
||||||
|
`
|
||||||
|
const contractResult = await dbService.query(contractSql, batch)
|
||||||
|
allOrderNumbers.push(...contractResult.rows.map((row) => row.生产订单号 as string))
|
||||||
|
}
|
||||||
|
|
||||||
orderNumbers.push(...allOrderNumbers)
|
orderNumbers.push(...allOrderNumbers)
|
||||||
} else {
|
} else {
|
||||||
const allOrderNumbers: string[] = []
|
const allOrderNumbers: string[] = []
|
||||||
|
|||||||
@@ -463,6 +463,18 @@ export class ValidationApplicationService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbService.type === 'postgresql') {
|
||||||
|
return dbService.query(
|
||||||
|
`
|
||||||
|
SELECT "MaterialName", "Specification", "Model"
|
||||||
|
FROM ${detailTableName}
|
||||||
|
WHERE "MaterialCode" = $1
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[materialCode]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return dbService.query(
|
return dbService.query(
|
||||||
`
|
`
|
||||||
SELECT MaterialName, Specification, Model
|
SELECT MaterialName, Specification, Model
|
||||||
@@ -521,6 +533,24 @@ export class ValidationApplicationService {
|
|||||||
return materialCodes
|
return materialCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbService.type === 'postgresql') {
|
||||||
|
const result = await dbService.query(
|
||||||
|
`
|
||||||
|
SELECT "MaterialCode"
|
||||||
|
FROM ${markedTableName}
|
||||||
|
WHERE "ManagerName" = $1 AND "MaterialCode" IS NOT NULL
|
||||||
|
`,
|
||||||
|
[username]
|
||||||
|
)
|
||||||
|
const materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
|
||||||
|
log.info(`Regular user: got ${materialCodes.length} materials`, {
|
||||||
|
userId: username,
|
||||||
|
isAdmin: false,
|
||||||
|
materialCount: materialCodes.length
|
||||||
|
})
|
||||||
|
return materialCodes
|
||||||
|
}
|
||||||
|
|
||||||
const result = await dbService.query(
|
const result = await dbService.query(
|
||||||
`
|
`
|
||||||
SELECT MaterialCode
|
SELECT MaterialCode
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ConfigManager } from '../config/config-manager'
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { MySqlService } from '../database/mysql'
|
import { MySqlService } from '../database/mysql'
|
||||||
import { SqlServerService } from '../database/sql-server'
|
import { SqlServerService } from '../database/sql-server'
|
||||||
|
import { PostgreSqlService } from '../database/postgresql'
|
||||||
|
|
||||||
export type ValidationDatabaseService = MySqlService | SqlServerService
|
export type ValidationDatabaseService = MySqlService | SqlServerService | PostgreSqlService
|
||||||
|
|
||||||
export async function createValidationDatabaseService(): Promise<ValidationDatabaseService> {
|
export async function createValidationDatabaseService(): Promise<ValidationDatabaseService> {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
@@ -26,6 +27,19 @@ export async function createValidationDatabaseService(): Promise<ValidationDatab
|
|||||||
return sqlServerService
|
return sqlServerService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbType === 'postgresql') {
|
||||||
|
const dbConfig = config.database.postgresql
|
||||||
|
const pgService = new PostgreSqlService({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
await pgService.connect()
|
||||||
|
return pgService
|
||||||
|
}
|
||||||
|
|
||||||
const dbConfig = config.database.mysql
|
const dbConfig = config.database.mysql
|
||||||
const mysqlService = new MySqlService({
|
const mysqlService = new MySqlService({
|
||||||
host: dbConfig.host,
|
host: dbConfig.host,
|
||||||
@@ -42,15 +56,21 @@ export function getValidationTableName(mysqlTableName: string): string {
|
|||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
if (dbType === 'sqlserver') {
|
if (dbType === 'sqlserver' || dbType === 'postgresql') {
|
||||||
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
||||||
if (firstUnderscoreIndex > 0) {
|
if (firstUnderscoreIndex > 0) {
|
||||||
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
||||||
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
||||||
|
if (dbType === 'sqlserver') {
|
||||||
return `[${schema}].[${tableName}]`
|
return `[${schema}].[${tableName}]`
|
||||||
}
|
}
|
||||||
|
return `"${schema}"."${tableName}"`
|
||||||
|
}
|
||||||
|
if (dbType === 'sqlserver') {
|
||||||
return `[dbo].[${mysqlTableName}]`
|
return `[dbo].[${mysqlTableName}]`
|
||||||
}
|
}
|
||||||
|
return `"public"."${mysqlTableName}"`
|
||||||
|
}
|
||||||
|
|
||||||
return mysqlTableName
|
return mysqlTableName
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user