Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ffa0650a6 | ||
|
|
72dba32a52 | ||
|
|
98865f5d7e | ||
|
|
5ff99cdd0f | ||
|
|
664f26d63f |
@@ -188,21 +188,26 @@ flowchart LR
|
||||
**表名转换逻辑**:
|
||||
|
||||
```typescript
|
||||
// MySQL: dbo_MaterialsToBeDeleted
|
||||
// 输入格式: dbo.MaterialsToBeDeleted
|
||||
// SQL Server: [dbo].[MaterialsToBeDeleted]
|
||||
function getTableName(mysqlTableName: string): string {
|
||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
||||
if (dbType === 'sqlserver' || dbType === 'mssql') {
|
||||
// 找到第一个下划线分割schema和表名
|
||||
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
||||
if (firstUnderscoreIndex > 0) {
|
||||
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
||||
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
||||
// PostgreSQL: "dbo"."MaterialsToBeDeleted"
|
||||
function getValidationTableName(dottedTableName: string): string {
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const dbType = configManager.getDatabaseType()
|
||||
|
||||
const dotIndex = dottedTableName.indexOf('.')
|
||||
if (dotIndex > 0) {
|
||||
const schema = dottedTableName.substring(0, dotIndex)
|
||||
const tableName = dottedTableName.substring(dotIndex + 1)
|
||||
if (dbType === 'sqlserver') {
|
||||
return `[${schema}].[${tableName}]`
|
||||
}
|
||||
return `[dbo].[${mysqlTableName}]`
|
||||
return `"${schema}"."${tableName}"`
|
||||
}
|
||||
return mysqlTableName
|
||||
if (dbType === 'sqlserver') {
|
||||
return `[dbo].[${dottedTableName}]`
|
||||
}
|
||||
return `"public"."${dottedTableName}"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ graph LR
|
||||
### 10.2 默认配置示例
|
||||
|
||||
```env
|
||||
DB_TABLE_NAME=productionContractData_26 年压力表合同数据
|
||||
DB_TABLE_NAME=ERPAuto.vw_productionContractData
|
||||
DB_FIELD_PRODUCTION_ID=总排号
|
||||
DB_FIELD_ORDER_NUMBER=生产订单号
|
||||
```
|
||||
|
||||
10
docs/releases/1.14.1.md
Normal file
10
docs/releases/1.14.1.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 1.14.1
|
||||
|
||||
## 问题修复
|
||||
|
||||
- 修复清理任务操作历史中「总排号」始终为空的问题,原始生产编号现在能正确保留并显示在历史记录中。
|
||||
- 修复会话重建配置缺失时清理任务可能异常中断的问题,提升配置不完整场景下的运行稳定性。
|
||||
|
||||
## 改进
|
||||
|
||||
- 表名配置统一使用 `schema.tablename` 标准点分写法,与数据库标准格式保持一致。
|
||||
@@ -180,7 +180,7 @@ validation:
|
||||
|
||||
# 订单号解析配置
|
||||
orderResolution:
|
||||
tableName: 'productionContractData_26 年压力表合同数据'
|
||||
tableName: 'ERPAuto.vw_productionContractData'
|
||||
productionIdField: '总排号'
|
||||
orderNumberField: '生产订单号'
|
||||
```
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "erpauto",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "erpauto",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.929.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "erpauto",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "An Electron application with React and TypeScript",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { InsertMaterialDetailInput, InsertOrderInput } from '../../types/cl
|
||||
import type { OrderMapping } from '../../types/order-resolver.types'
|
||||
|
||||
const log = createLogger('CleanerApplicationService')
|
||||
const DEFAULT_SESSION_REFRESH_ORDER_THRESHOLD = 160
|
||||
|
||||
export class CleanerApplicationService {
|
||||
async runCleaner(
|
||||
@@ -65,7 +66,8 @@ export class CleanerApplicationService {
|
||||
}
|
||||
|
||||
const resolver = new OrderNumberResolver(dbService)
|
||||
const mappings = await resolver.resolve(input.orderNumbers)
|
||||
const inputsToResolve = input.originalInputs?.length ? input.originalInputs : input.orderNumbers
|
||||
const mappings = await resolver.resolve(inputsToResolve)
|
||||
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
|
||||
const warnings = resolver.getWarnings(mappings)
|
||||
|
||||
@@ -148,9 +150,8 @@ export class CleanerApplicationService {
|
||||
log.info('Login successful')
|
||||
|
||||
const totalOrders = validOrderNumbers.length
|
||||
const cleanerConfig = configManager.getConfig().cleaner
|
||||
const effectiveSessionRefreshOrderThreshold =
|
||||
input.sessionRefreshOrderThreshold ?? cleanerConfig.sessionRefreshOrderThreshold
|
||||
this.resolveSessionRefreshOrderThreshold(input, configManager)
|
||||
this.sendProgress(eventSender, 'ERP 登录成功', (1 / (1 + totalOrders)) * 100, {
|
||||
phase: 'login',
|
||||
currentOrderIndex: 0,
|
||||
@@ -455,9 +456,10 @@ export class CleanerApplicationService {
|
||||
: result.errors.length > 0
|
||||
? AuditStatus.FAILURE
|
||||
: AuditStatus.SUCCESS
|
||||
const cleanerConfig = ConfigManager.getInstance().getConfig().cleaner
|
||||
const effectiveSessionRefreshOrderThreshold =
|
||||
input.sessionRefreshOrderThreshold ?? cleanerConfig.sessionRefreshOrderThreshold
|
||||
const effectiveSessionRefreshOrderThreshold = this.resolveSessionRefreshOrderThreshold(
|
||||
input,
|
||||
ConfigManager.getInstance()
|
||||
)
|
||||
|
||||
logAuditWithCurrentUser(AuditAction.CLEAN, 'MATERIAL_PLAN', status, {
|
||||
orderCount,
|
||||
@@ -471,6 +473,17 @@ export class CleanerApplicationService {
|
||||
})
|
||||
}
|
||||
|
||||
private resolveSessionRefreshOrderThreshold(
|
||||
input: CleanerInput,
|
||||
configManager: Pick<ConfigManager, 'getConfig'>
|
||||
): number {
|
||||
return (
|
||||
input.sessionRefreshOrderThreshold ??
|
||||
configManager.getConfig().cleaner?.sessionRefreshOrderThreshold ??
|
||||
DEFAULT_SESSION_REFRESH_ORDER_THRESHOLD
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save attempt results to database: update order statuses, insert material details,
|
||||
* and update execution status.
|
||||
|
||||
@@ -40,7 +40,7 @@ export function getDbConfig() {
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const config = configManager.getConfig()
|
||||
return {
|
||||
TABLE_NAME: config.orderResolution.tableName || 'productionContractData_26 年压力表合同数据',
|
||||
TABLE_NAME: config.orderResolution.tableName || 'ERPAuto.vw_productionContractData',
|
||||
FIELD_PRODUCTION_ID: config.orderResolution.productionIdField || '总排号',
|
||||
FIELD_ORDER_NUMBER: config.orderResolution.orderNumberField || '生产订单号'
|
||||
}
|
||||
@@ -58,35 +58,28 @@ export class OrderNumberResolver {
|
||||
|
||||
/**
|
||||
* Get table name based on database type
|
||||
* Converts schema_tablename format to database-specific quoting:
|
||||
* 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年压力表合同数据
|
||||
* e.g., ERPAuto.vw_productionContractData ->
|
||||
* SQL Server: [ERPAuto].[vw_productionContractData]
|
||||
* PostgreSQL: "ERPAuto"."vw_productionContractData"
|
||||
*/
|
||||
private getTableName(tableName: string): string {
|
||||
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)
|
||||
if (this.dbService.type === 'sqlserver') {
|
||||
return `[${schema}].[${actualTableName}]`
|
||||
}
|
||||
return `"${schema}"."${actualTableName}"`
|
||||
}
|
||||
// If no underscore found, default schema
|
||||
const dotIndex = tableName.indexOf('.')
|
||||
if (dotIndex > 0) {
|
||||
const schema = tableName.substring(0, dotIndex)
|
||||
const actualTableName = tableName.substring(dotIndex + 1)
|
||||
if (this.dbService.type === 'sqlserver') {
|
||||
return `[dbo].[${tableName}]`
|
||||
return `[${schema}].[${actualTableName}]`
|
||||
}
|
||||
return `"public"."${tableName}"`
|
||||
return `"${schema}"."${actualTableName}"`
|
||||
}
|
||||
return tableName
|
||||
// No dot found — use default schema
|
||||
if (this.dbService.type === 'sqlserver') {
|
||||
return `[dbo].[${tableName}]`
|
||||
}
|
||||
return `"public"."${tableName}"`
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function getSourceNumbersFromInputs(
|
||||
}
|
||||
|
||||
if (productionIds.length > 0) {
|
||||
const contractTableName = getValidationTableName('productionContractData_26年压力表合同数据')
|
||||
const contractTableName = getValidationTableName('ERPAuto.vw_productionContractData')
|
||||
const batchSize = 2000
|
||||
|
||||
if (dbType === 'sqlserver') {
|
||||
|
||||
@@ -236,6 +236,7 @@ export class ValidationApplicationService {
|
||||
): Promise<{
|
||||
success: boolean
|
||||
orderNumbers?: string[]
|
||||
originalInputs?: string[]
|
||||
materialCodes?: string[]
|
||||
error?: string
|
||||
}> {
|
||||
@@ -288,6 +289,7 @@ export class ValidationApplicationService {
|
||||
return {
|
||||
success: true,
|
||||
orderNumbers,
|
||||
originalInputs: sharedIds,
|
||||
materialCodes
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -325,7 +327,7 @@ export class ValidationApplicationService {
|
||||
}
|
||||
|
||||
private async loadTypeKeywords(dbService: ValidationDatabaseService): Promise<TypeKeyword[]> {
|
||||
const typeKeywordTableName = getValidationTableName('dbo_MaterialsTypeToBeDeleted')
|
||||
const typeKeywordTableName = getValidationTableName('dbo.MaterialsTypeToBeDeleted')
|
||||
const sql = `
|
||||
SELECT MaterialName, ManagerName
|
||||
FROM ${typeKeywordTableName}
|
||||
@@ -341,7 +343,7 @@ export class ValidationApplicationService {
|
||||
private async loadMarkedCodes(
|
||||
dbService: ValidationDatabaseService
|
||||
): Promise<Map<string, string>> {
|
||||
const markedTableName = getValidationTableName('dbo_MaterialsToBeDeleted')
|
||||
const markedTableName = getValidationTableName('dbo.MaterialsToBeDeleted')
|
||||
const sql = `
|
||||
SELECT MaterialCode, ManagerName
|
||||
FROM ${markedTableName}
|
||||
@@ -416,7 +418,7 @@ export class ValidationApplicationService {
|
||||
|
||||
try {
|
||||
dbService = await createValidationDatabaseService()
|
||||
const detailTableName = getValidationTableName('dbo_DiscreteMaterialPlanData')
|
||||
const detailTableName = getValidationTableName('dbo.DiscreteMaterialPlanData')
|
||||
const enrichedMaterials: MaterialRecordSummary[] = []
|
||||
|
||||
log.info(`Enriching ${materials.length} materials with details`)
|
||||
@@ -501,7 +503,7 @@ export class ValidationApplicationService {
|
||||
selectedManagers: string[],
|
||||
orderNumbers: string[]
|
||||
): Promise<string[]> {
|
||||
const markedTableName = getValidationTableName('dbo_MaterialsToBeDeleted')
|
||||
const markedTableName = getValidationTableName('dbo.MaterialsToBeDeleted')
|
||||
|
||||
// Admin with selected managers: filter MaterialsToBeDeleted by ManagerName IN (selectedManagers)
|
||||
if (isAdmin && selectedManagers && selectedManagers.length > 0) {
|
||||
|
||||
@@ -52,25 +52,22 @@ export async function createValidationDatabaseService(): Promise<ValidationDatab
|
||||
return mysqlService
|
||||
}
|
||||
|
||||
export function getValidationTableName(mysqlTableName: string): string {
|
||||
export function getValidationTableName(dottedTableName: string): string {
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const dbType = configManager.getDatabaseType()
|
||||
|
||||
if (dbType === 'sqlserver' || dbType === 'postgresql') {
|
||||
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
||||
if (firstUnderscoreIndex > 0) {
|
||||
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
||||
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
||||
if (dbType === 'sqlserver') {
|
||||
return `[${schema}].[${tableName}]`
|
||||
}
|
||||
return `"${schema}"."${tableName}"`
|
||||
}
|
||||
const dotIndex = dottedTableName.indexOf('.')
|
||||
if (dotIndex > 0) {
|
||||
const schema = dottedTableName.substring(0, dotIndex)
|
||||
const tableName = dottedTableName.substring(dotIndex + 1)
|
||||
if (dbType === 'sqlserver') {
|
||||
return `[dbo].[${mysqlTableName}]`
|
||||
return `[${schema}].[${tableName}]`
|
||||
}
|
||||
return `"public"."${mysqlTableName}"`
|
||||
return `"${schema}"."${tableName}"`
|
||||
}
|
||||
|
||||
return mysqlTableName
|
||||
// No dot found — use default schema
|
||||
if (dbType === 'sqlserver') {
|
||||
return `[dbo].[${dottedTableName}]`
|
||||
}
|
||||
return `"public"."${dottedTableName}"`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface CleanerProgress {
|
||||
|
||||
export interface CleanerInput {
|
||||
orderNumbers: string[]
|
||||
originalInputs?: string[]
|
||||
materialCodes: string[]
|
||||
dryRun: boolean
|
||||
headless?: boolean
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { CleanerExportItem, MaterialBatchChange } from './helpers'
|
||||
interface CleanerDataPayload {
|
||||
success?: boolean
|
||||
orderNumbers?: string[]
|
||||
originalInputs?: string[]
|
||||
materialCodes?: string[]
|
||||
}
|
||||
|
||||
@@ -147,6 +148,7 @@ export async function runCleanerExecution(params: {
|
||||
|
||||
const response = await window.electron.cleaner.runCleaner({
|
||||
orderNumbers: orderNumberList,
|
||||
originalInputs: cleanerData?.originalInputs,
|
||||
materialCodes: materialCodeList,
|
||||
dryRun: params.dryRun,
|
||||
headless: params.headless,
|
||||
|
||||
@@ -279,6 +279,14 @@ describe('CleanerApplicationService', () => {
|
||||
expect(result.ordersProcessed).toBe(1)
|
||||
})
|
||||
|
||||
it('should fall back to default session refresh threshold when cleaner config is missing', async () => {
|
||||
const eventSender: any = { send: vi.fn() }
|
||||
|
||||
await service.runCleaner(eventSender, makeInput())
|
||||
|
||||
expect(lastCleanerInput?.sessionRefreshOrderThreshold).toBe(160)
|
||||
})
|
||||
|
||||
it('should close ERP browser on success', async () => {
|
||||
await service.runCleaner({ send: vi.fn() } as any, makeInput())
|
||||
|
||||
|
||||
@@ -125,31 +125,25 @@ describe('ValidationDatabaseService', () => {
|
||||
})
|
||||
|
||||
describe('getValidationTableName', () => {
|
||||
it('returns table name unchanged for mysql', async () => {
|
||||
currentDbType = 'mysql'
|
||||
const mod = await import('../../../../src/main/services/validation/validation-database')
|
||||
expect(mod.getValidationTableName('MaterialsToBeDeleted')).toBe('MaterialsToBeDeleted')
|
||||
})
|
||||
|
||||
it('converts schema_table to [schema].[table] for sqlserver', async () => {
|
||||
it('converts schema.table to [schema].[table] for sqlserver', async () => {
|
||||
currentDbType = 'sqlserver'
|
||||
const mod = await import('../../../../src/main/services/validation/validation-database')
|
||||
expect(mod.getValidationTableName('dbo_Materials')).toBe('[dbo].[Materials]')
|
||||
expect(mod.getValidationTableName('dbo.Materials')).toBe('[dbo].[Materials]')
|
||||
})
|
||||
|
||||
it('wraps nameless table in [dbo].[name] for sqlserver', async () => {
|
||||
it('wraps dotless table in [dbo].[name] for sqlserver', async () => {
|
||||
currentDbType = 'sqlserver'
|
||||
const mod = await import('../../../../src/main/services/validation/validation-database')
|
||||
expect(mod.getValidationTableName('Materials')).toBe('[dbo].[Materials]')
|
||||
})
|
||||
|
||||
it('converts schema_table to "schema"."table" for postgresql', async () => {
|
||||
it('converts schema.table to "schema"."table" for postgresql', async () => {
|
||||
currentDbType = 'postgresql'
|
||||
const mod = await import('../../../../src/main/services/validation/validation-database')
|
||||
expect(mod.getValidationTableName('public_Materials')).toBe('"public"."Materials"')
|
||||
expect(mod.getValidationTableName('public.Materials')).toBe('"public"."Materials"')
|
||||
})
|
||||
|
||||
it('wraps nameless table in "public"."name" for postgresql', async () => {
|
||||
it('wraps dotless table in "public"."name" for postgresql', async () => {
|
||||
currentDbType = 'postgresql'
|
||||
const mod = await import('../../../../src/main/services/validation/validation-database')
|
||||
expect(mod.getValidationTableName('Materials')).toBe('"public"."Materials"')
|
||||
|
||||
Reference in New Issue
Block a user