7 Commits

Author SHA1 Message Date
Misaka_Company
1ffa0650a6 1.14.1 2026-04-28 12:17:15 +08:00
Misaka_Company
72dba32a52 docs: add release notes for version 1.14.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 12:17:11 +08:00
Misaka_Company
98865f5d7e fix(cleaner): preserve production IDs in operation history
The 总排号 field was always empty because getCleanerData() resolved
production IDs to order numbers before passing them to the cleaner,
losing the original inputs. Now originalInputs are carried through
the full chain so the resolver can properly set productionId.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 11:57:47 +08:00
Misaka_Company
5ff99cdd0f refactor: use dot notation for table name config (schema.table instead of schema_table)
Replace underscore-based table name splitting with dot-based splitting
to match the standard schema.tablename format, removing MySQL compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 11:10:03 +08:00
Misaka
664f26d63f fix(cleaner): guard missing session refresh config 2026-04-24 19:11:21 +08:00
Misaka_Company
e39fc87869 1.14.0 2026-04-24 15:50:33 +08:00
Misaka_Company
4b071e4331 docs: add release notes for version 1.14.0 2026-04-24 15:50:29 +08:00
16 changed files with 118 additions and 77 deletions

View File

@@ -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}"`
}
```

View File

@@ -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=生产订单号
```

16
docs/releases/1.14.0.md Normal file
View File

@@ -0,0 +1,16 @@
# 1.14.0
## 核心功能
- 清理任务新增长批次自动重建会话机制,在大批量订单处理中可按批次边界自动重开浏览器并继续执行,降低长时间运行中断的概率。
- 会话重建阈值支持配置,可结合批次大小灵活调整重建时机,避免在批次处理中途打断流程。
## 问题修复
- 修复无头模式下大批量清理任务更容易在后半程停滞的问题,提升真实数据处理场景下的完成率。
- 优化清理流程在登录态异常场景下的识别能力,减少详情页长时间等待后才暴露问题的情况。
## 改进
- 新增 ERP 页面状态诊断日志,可区分登录页、首页、查询页、详情页等关键页面状态,方便快速定位异常发生位置。
- 补充详情页打开、页面跳转、弹窗创建和并发等待等关键日志,排查间歇性问题时可以更快还原现场。

10
docs/releases/1.14.1.md Normal file
View File

@@ -0,0 +1,10 @@
# 1.14.1
## 问题修复
- 修复清理任务操作历史中「总排号」始终为空的问题,原始生产编号现在能正确保留并显示在历史记录中。
- 修复会话重建配置缺失时清理任务可能异常中断的问题,提升配置不完整场景下的运行稳定性。
## 改进
- 表名配置统一使用 `schema.tablename` 标准点分写法,与数据库标准格式保持一致。

View File

@@ -180,7 +180,7 @@ validation:
# 订单号解析配置
orderResolution:
tableName: 'productionContractData_26 年压力表合同数据'
tableName: 'ERPAuto.vw_productionContractData'
productionIdField: '总排号'
orderNumberField: '生产订单号'
```

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "erpauto",
"version": "1.13.0",
"version": "1.14.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "erpauto",
"version": "1.13.0",
"version": "1.14.1",
"hasInstallScript": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.929.0",

View File

@@ -1,6 +1,6 @@
{
"name": "erpauto",
"version": "1.13.0",
"version": "1.14.1",
"description": "An Electron application with React and TypeScript",
"main": "./out/main/index.js",
"author": "example.com",

View File

@@ -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.

View File

@@ -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,36 +58,29 @@ 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)
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 `[${schema}].[${actualTableName}]`
}
return `"${schema}"."${actualTableName}"`
}
// If no underscore found, default schema
// No dot found — use default schema
if (this.dbService.type === 'sqlserver') {
return `[dbo].[${tableName}]`
}
return `"public"."${tableName}"`
}
return tableName
}
/**
* Check if input matches productionID pattern

View File

@@ -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') {

View File

@@ -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) {

View File

@@ -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)
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 `"${schema}"."${tableName}"`
}
// No dot found — use default schema
if (dbType === 'sqlserver') {
return `[dbo].[${mysqlTableName}]`
return `[dbo].[${dottedTableName}]`
}
return `"public"."${mysqlTableName}"`
}
return mysqlTableName
return `"public"."${dottedTableName}"`
}

View File

@@ -13,6 +13,7 @@ export interface CleanerProgress {
export interface CleanerInput {
orderNumbers: string[]
originalInputs?: string[]
materialCodes: string[]
dryRun: boolean
headless?: boolean

View File

@@ -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,

View File

@@ -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())

View File

@@ -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"')