From f51cae0f6ff61881e5734879602585e247d4af62 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sun, 5 Apr 2026 13:46:37 +0800 Subject: [PATCH] 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 --- .../cleaner/cleaner-application-service.ts | 25 +++++++--- src/main/services/database/postgresql.ts | 47 +++++++++++++++++++ src/main/services/erp/order-resolver.ts | 34 +++++++++++--- .../validation/production-input-service.ts | 20 +++++++- .../validation-application-service.ts | 30 ++++++++++++ .../validation/validation-database.ts | 28 +++++++++-- 6 files changed, 164 insertions(+), 20 deletions(-) diff --git a/src/main/services/cleaner/cleaner-application-service.ts b/src/main/services/cleaner/cleaner-application-service.ts index 96fce84..59de3a3 100644 --- a/src/main/services/cleaner/cleaner-application-service.ts +++ b/src/main/services/cleaner/cleaner-application-service.ts @@ -1,11 +1,11 @@ import type { WebContents } from 'electron' -import type { MySqlService } from '../database/mysql' -import type { SqlServerService } from '../database/sql-server' +import type { IDatabaseService } from '../../types/database.types' import { ErpAuthService } from '../erp/erp-auth' import { CleanerService } from '../erp/cleaner' import { OrderNumberResolver } from '../erp/order-resolver' import { MySqlService as MySqlServiceImpl } from '../database/mysql' import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server' +import { PostgreSqlService as PostgreSqlServiceImpl } from '../database/postgresql' import { ConfigManager } from '../config/config-manager' import { ResultExporter } from '../excel/result-exporter' import { CleanerReportGenerator } from '../report/cleaner-report-generator' @@ -26,13 +26,11 @@ import type { const log = createLogger('CleanerApplicationService') -type DatabaseService = MySqlService | SqlServerService - export class CleanerApplicationService { async runCleaner(eventSender: WebContents, input: CleanerInput): Promise { const startTime = Date.now() let authService: ErpAuthService | null = null - let dbService: DatabaseService | null = null + let dbService: IDatabaseService | null = null try { log.info('Fetching ERP configuration from database...') @@ -46,7 +44,7 @@ export class CleanerApplicationService { const configManager = ConfigManager.getInstance() const dbType = configManager.getDatabaseType() 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 { @@ -201,7 +199,7 @@ export class CleanerApplicationService { } } - private async getDatabaseService(): Promise { + private async getDatabaseService(): Promise { const configManager = ConfigManager.getInstance() const config = configManager.getConfig() const dbType = configManager.getDatabaseType() @@ -223,6 +221,19 @@ export class CleanerApplicationService { 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 mysqlService = new MySqlServiceImpl({ host: dbConfig.host, diff --git a/src/main/services/database/postgresql.ts b/src/main/services/database/postgresql.ts index cf5aa4a..829785d 100644 --- a/src/main/services/database/postgresql.ts +++ b/src/main/services/database/postgresql.ts @@ -238,6 +238,53 @@ const SQL_KEYWORDS = new Set([ 'FIRST', '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 ==================== 'BETWEEN', 'LIKE', diff --git a/src/main/services/erp/order-resolver.ts b/src/main/services/erp/order-resolver.ts index a9caa97..eb7e8a2 100644 --- a/src/main/services/erp/order-resolver.ts +++ b/src/main/services/erp/order-resolver.ts @@ -58,22 +58,33 @@ export class OrderNumberResolver { /** * Get table name based on database type - * Converts MySQL schema_tablename format to SQL Server [schema].[tablename] format - * e.g., productionContractData_26年压力表合同数据 -> [productionContractData].[26年压力表合同数据] - * dbo_MaterialsToBeDeleted -> [dbo].[MaterialsToBeDeleted] + * Converts schema_tablename format to database-specific quoting: + * - SQL Server: [schema].[tablename] + * - 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 { - if (this.dbService.type === 'sqlserver') { + if (this.dbService.type === 'sqlserver' || this.dbService.type === 'postgresql') { // Find the FIRST underscore to split schema and table name // This handles patterns like: schema_tablename const firstUnderscoreIndex = tableName.indexOf('_') if (firstUnderscoreIndex > 0) { const schema = tableName.substring(0, firstUnderscoreIndex) const actualTableName = tableName.substring(firstUnderscoreIndex + 1) - return `[${schema}].[${actualTableName}]` + if (this.dbService.type === 'sqlserver') { + return `[${schema}].[${actualTableName}]` + } + return `"${schema}"."${actualTableName}"` } - // If no underscore found, default to dbo schema - return `[dbo].[${tableName}]` + // If no underscore found, default schema + if (this.dbService.type === 'sqlserver') { + return `[dbo].[${tableName}]` + } + return `"public"."${tableName}"` } return tableName } @@ -107,6 +118,11 @@ export class OrderNumberResolver { // 使用 COLLATE 指定不区分大小写的排序规则 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] + } 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 { // MySQL 默认不区分大小写,但显式使用 UPPER 确保一致性 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 // 使用 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})` + } 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 { const idPlaceholders = uniqueProductionIds.map(() => 'UPPER(?)').join(', ') // P0: Use DISTINCT to prevent duplicates from one-to-many relationships diff --git a/src/main/services/validation/production-input-service.ts b/src/main/services/validation/production-input-service.ts index 734f41a..7c54b36 100644 --- a/src/main/services/validation/production-input-service.ts +++ b/src/main/services/validation/production-input-service.ts @@ -29,7 +29,7 @@ export async function getSourceNumbersFromInputs( const productionIds: string[] = [] const orderNumbers: string[] = [] const configManager = ConfigManager.getInstance() - const isSqlServer = configManager.getDatabaseType() === 'sqlserver' + const dbType = configManager.getDatabaseType() for (const item of inputs) { const type = identifyInputType(item) @@ -44,7 +44,7 @@ export async function getSourceNumbersFromInputs( const contractTableName = getValidationTableName('productionContractData_26年压力表合同数据') const batchSize = 2000 - if (isSqlServer) { + if (dbType === 'sqlserver') { const sql = await import('mssql') 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) } else { const allOrderNumbers: string[] = [] diff --git a/src/main/services/validation/validation-application-service.ts b/src/main/services/validation/validation-application-service.ts index 1e6b683..99e8e3a 100644 --- a/src/main/services/validation/validation-application-service.ts +++ b/src/main/services/validation/validation-application-service.ts @@ -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( ` SELECT MaterialName, Specification, Model @@ -521,6 +533,24 @@ export class ValidationApplicationService { 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( ` SELECT MaterialCode diff --git a/src/main/services/validation/validation-database.ts b/src/main/services/validation/validation-database.ts index 24cddf1..0916d8c 100644 --- a/src/main/services/validation/validation-database.ts +++ b/src/main/services/validation/validation-database.ts @@ -1,8 +1,9 @@ import { ConfigManager } from '../config/config-manager' import { MySqlService } from '../database/mysql' 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 { const configManager = ConfigManager.getInstance() @@ -26,6 +27,19 @@ export async function createValidationDatabaseService(): Promise 0) { const schema = mysqlTableName.substring(0, firstUnderscoreIndex) const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1) - return `[${schema}].[${tableName}]` + if (dbType === 'sqlserver') { + return `[${schema}].[${tableName}]` + } + return `"${schema}"."${tableName}"` } - return `[dbo].[${mysqlTableName}]` + if (dbType === 'sqlserver') { + return `[dbo].[${mysqlTableName}]` + } + return `"public"."${mysqlTableName}"` } return mysqlTableName