diff --git a/src/main/services/cleaner/cleaner-application-service.ts b/src/main/services/cleaner/cleaner-application-service.ts index b09d504..d27cea9 100644 --- a/src/main/services/cleaner/cleaner-application-service.ts +++ b/src/main/services/cleaner/cleaner-application-service.ts @@ -23,7 +23,8 @@ import type { ExportResultItem, ExportResultResponse } from '../../types/cleaner.types' -import type { InsertMaterialDetailInput } from '../../types/cleaner-history.types' +import type { InsertMaterialDetailInput, InsertOrderInput } from '../../types/cleaner-history.types' +import type { OrderMapping } from '../../types/order-resolver.types' const log = createLogger('CleanerApplicationService') @@ -72,16 +73,16 @@ export class CleanerApplicationService { log.warn('Resolution warnings', { warnings }) } - if (validOrderNumbers.length === 0) { - throw new ValidationError( - '没有有效的生产订单号可处理。请检查输入的格式或数据库连接。', - 'VAL_INVALID_INPUT' - ) - } + // Build order inputs from ALL mappings (including resolution failures) + const orderInputs = this.buildOrderInputs(mappings) - log.info('Resolved order numbers', { count: validOrderNumbers.length }) + log.info('Resolved order numbers', { + total: mappings.length, + resolved: validOrderNumbers.length, + failed: mappings.length - validOrderNumbers.length + }) - // Insert execution record and order records into database + // Insert execution record and ALL order records into database const currentUser = SessionManager.getInstance().getUserInfo() if (currentUser) { await historyDao.insertExecution({ @@ -90,14 +91,41 @@ export class CleanerApplicationService { userId: currentUser.id, username: currentUser.username, isDryRun: input.dryRun ?? false, - totalOrders: validOrderNumbers.length, + totalOrders: orderInputs.length, appVersion }) - await historyDao.insertOrderRecords( - batchId, - 1, - validOrderNumbers.map((order) => ({ orderNumber: order })) - ) + await historyDao.insertOrderRecords(batchId, 1, orderInputs) + } + + // If no valid order numbers, update execution to failed and return early + if (validOrderNumbers.length === 0) { + if (currentUser) { + await historyDao.updateExecutionStatus( + batchId, + 1, + 'failed', + 0, + 0, + 0, + 0, + 0, + new Date(), + warnings.join('\n') || '没有有效的生产订单号可处理' + ) + } + const emptyResult: CleanerResult = { + ordersProcessed: 0, + materialsDeleted: 0, + materialsSkipped: 0, + errors: [...warnings], + details: [], + retriedOrders: 0, + successfulRetries: 0, + materialsFailed: 0, + uncertainDeletions: 0 + } + await this.recordCleanupAudit(0, input, emptyResult) + return emptyResult } authService = new ErpAuthService({ @@ -203,14 +231,10 @@ export class CleanerApplicationService { userId: currentUser.id, username: currentUser.username, isDryRun: input.dryRun ?? false, - totalOrders: validOrderNumbers.length, + totalOrders: orderInputs.length, appVersion }) - await historyDao.insertOrderRecords( - batchId, - 2, - validOrderNumbers.map((order) => ({ orderNumber: order })) - ) + await historyDao.insertOrderRecords(batchId, 2, orderInputs) } cleaner = new CleanerService(authService) @@ -378,6 +402,38 @@ export class CleanerApplicationService { } } + /** + * Build order inputs from ALL mappings, including resolution failures. + * Deduplicates by orderNumber for resolved mappings, includes all failed mappings. + */ + private buildOrderInputs(mappings: OrderMapping[]): InsertOrderInput[] { + const inputs: InsertOrderInput[] = [] + const seenOrderNumbers = new Set() + + for (const mapping of mappings) { + if (mapping.resolved && mapping.orderNumber) { + // Deduplicate resolved mappings by order number + if (!seenOrderNumbers.has(mapping.orderNumber)) { + seenOrderNumbers.add(mapping.orderNumber) + inputs.push({ + orderNumber: mapping.orderNumber, + productionId: mapping.productionId + }) + } + } else { + // Resolution failure: use original input as orderNumber identifier + inputs.push({ + orderNumber: mapping.input, + productionId: mapping.productionId, + initialStatus: 'not_found', + errorMessage: mapping.error || '未在数据库中找到对应的订单号' + }) + } + } + + return inputs + } + private async recordCleanupAudit( orderCount: number, input: CleanerInput, @@ -428,7 +484,11 @@ export class CleanerApplicationService { batchId, attemptNumber, detail.orderNumber, - detail.errors.length > 0 ? 'failed' : 'success', + detail.notFound + ? 'erp_not_found' + : detail.errors.length > 0 + ? 'failed' + : 'success', detail.materialsDeleted, detail.materialsSkipped, detail.materialsFailed, diff --git a/src/main/services/database/cleaner-operation-history-dao.ts b/src/main/services/database/cleaner-operation-history-dao.ts index 4134aa1..4e105e0 100644 --- a/src/main/services/database/cleaner-operation-history-dao.ts +++ b/src/main/services/database/cleaner-operation-history-dao.ts @@ -65,6 +65,7 @@ export const CLEANER_ORDER_CONFIG = { BATCH_ID: 'BatchId', ATTEMPT_NUMBER: 'AttemptNumber', ORDER_NUMBER: 'OrderNumber', + PRODUCTION_ID: 'ProductionId', STATUS: 'Status', MATERIALS_DELETED: 'MaterialsDeleted', MATERIALS_SKIPPED: 'MaterialsSkipped', @@ -324,18 +325,26 @@ export class CleanerOperationHistoryDAO { for (const order of orders) { try { + const status = order.initialStatus || 'pending' const sqlString = ` INSERT INTO ${tableName} - (BatchId, AttemptNumber, OrderNumber, Status, + (BatchId, AttemptNumber, OrderNumber, ProductionId, Status, MaterialsDeleted, MaterialsSkipped, MaterialsFailed, UncertainDeletions, RetryCount, RetrySuccess, ErrorMessage) VALUES - (${dialect.param(0)}, ${dialect.param(1)}, ${dialect.param(2)}, 'pending', - 0, 0, 0, 0, 0, 0, NULL) + (${dialect.param(0)}, ${dialect.param(1)}, ${dialect.param(2)}, ${dialect.param(3)}, ${dialect.param(4)}, + 0, 0, 0, 0, 0, 0, ${dialect.param(5)}) ` await trackDuration( async () => - await dbService.query(sqlString, [batchId, attemptNumber, order.orderNumber]), + await dbService.query(sqlString, [ + batchId, + attemptNumber, + order.orderNumber, + order.productionId || null, + status, + order.errorMessage || null + ]), { operationName: 'CleanerOperationHistoryDAO.insertOrderRecords', context: { tableName, operationType: 'INSERT', batchId, attemptNumber } @@ -730,7 +739,7 @@ export class CleanerOperationHistoryDAO { // Query orders const orderSql = ` SELECT - ID, BatchId, AttemptNumber, OrderNumber, Status, + ID, BatchId, AttemptNumber, OrderNumber, ProductionId, Status, MaterialsDeleted, MaterialsSkipped, MaterialsFailed, UncertainDeletions, RetryCount, RetrySuccess, ErrorMessage FROM ${orderTable} @@ -778,6 +787,7 @@ export class CleanerOperationHistoryDAO { batchId: row.BatchId as string, attemptNumber: row.AttemptNumber as number, orderNumber: row.OrderNumber as string, + productionId: (row.ProductionId as string) || null, status: row.Status as string, materialsDeleted: row.MaterialsDeleted as number, materialsSkipped: row.MaterialsSkipped as number, diff --git a/src/main/services/erp/cleaner.ts b/src/main/services/erp/cleaner.ts index 493ecb6..15aa603 100644 --- a/src/main/services/erp/cleaner.ts +++ b/src/main/services/erp/cleaner.ts @@ -307,7 +307,7 @@ export class CleanerService { for (const missingOrder of missingOrders) { const missingMessage = '订单未出现在查询结果中' result.errors.push(`Order ${missingOrder}: ${missingMessage}`) - result.details.push(this.createErrorDetail(missingOrder, missingMessage)) + result.details.push(this.createErrorDetail(missingOrder, missingMessage, true)) } }, { @@ -1115,7 +1115,7 @@ export class CleanerService { return /^SC\d{14}$/.test(value) } - private createErrorDetail(orderNumber: string, message: string): OrderCleanDetail { + private createErrorDetail(orderNumber: string, message: string, notFound: boolean = false): OrderCleanDetail { return { orderNumber, materialsDeleted: 0, @@ -1129,7 +1129,8 @@ export class CleanerService { retrySuccess: false, materialsFailed: 0, failedMaterials: [], - uncertainDeletions: 0 + uncertainDeletions: 0, + notFound } } diff --git a/src/main/services/user/migration/add-productionid-to-cleaner-order.ts b/src/main/services/user/migration/add-productionid-to-cleaner-order.ts new file mode 100644 index 0000000..8317746 --- /dev/null +++ b/src/main/services/user/migration/add-productionid-to-cleaner-order.ts @@ -0,0 +1,142 @@ +/** + * Migration Script: Add ProductionId column to CleanerOrderHistory table + * + * This script adds the ProductionId column to the ERPAuto.CleanerOrderHistory + * table for tracking the original production ID (总排号) input. + * + * Usage: + * npx tsx src/main/services/user/migration/add-productionid-to-cleaner-order.ts + */ + +import { ConfigManager } from '../../config/config-manager' +import { MySqlService } from '../../database/mysql' +import { SqlServerService } from '../../database/sql-server' +import { PostgreSqlService } from '../../database/postgresql' +import { createLogger } from '../../logger' + +const log = createLogger('Migration') + +async function runMySQLMigration(): Promise { + const configManager = ConfigManager.getInstance() + await configManager.initialize() + const dbConfig = configManager.getConfig().database.mysql + + const service = new MySqlService({ + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database + }) + + try { + await service.connect() + console.log('Connected to MySQL') + + // Check if column already exists + const check = await service.query( + `SELECT COUNT(*) as cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'CleanerOrderHistory' AND COLUMN_NAME = 'ProductionId'` + ) + if ((check.rows[0]?.cnt as number) > 0) { + console.log('Column ProductionId already exists, skipping.') + return + } + + await service.query( + `ALTER TABLE CleanerOrderHistory ADD COLUMN ProductionId VARCHAR(50) NULL` + ) + console.log('Added ProductionId column to CleanerOrderHistory.') + } finally { + if (service.isConnected()) await service.disconnect() + } +} + +async function runSqlServerMigration(): Promise { + const configManager = ConfigManager.getInstance() + await configManager.initialize() + const dbConfig = configManager.getConfig().database.sqlserver + + const service = new SqlServerService({ + server: dbConfig.server, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, + options: { trustServerCertificate: dbConfig.trustServerCertificate } + }) + + try { + await service.connect() + console.log('Connected to SQL Server') + + // Check if column already exists + const check = await service.query( + `SELECT COUNT(*) as cnt FROM sys.columns WHERE OBJECT_ID = OBJECT_ID('ERPAuto.CleanerOrderHistory') AND name = 'ProductionId'` + ) + if ((check.rows[0]?.cnt as number) > 0) { + console.log('Column ProductionId already exists, skipping.') + return + } + + await service.query( + `ALTER TABLE [ERPAuto].[CleanerOrderHistory] ADD ProductionId NVARCHAR(50) NULL` + ) + console.log('Added ProductionId column to CleanerOrderHistory.') + } finally { + if (service.isConnected()) await service.disconnect() + } +} + +async function runPostgreSqlMigration(): Promise { + const configManager = ConfigManager.getInstance() + await configManager.initialize() + const dbConfig = configManager.getConfig().database.postgresql + + const service = new PostgreSqlService({ + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database + }) + + try { + await service.connect() + console.log('Connected to PostgreSQL') + + await service.query( + `ALTER TABLE "ERPAuto"."CleanerOrderHistory" ADD COLUMN IF NOT EXISTS "ProductionId" VARCHAR(50) NULL` + ) + console.log('Added ProductionId column to CleanerOrderHistory.') + } finally { + if (service.isConnected()) await service.disconnect() + } +} + +async function main(): Promise { + console.log('Migration: Add ProductionId to CleanerOrderHistory') + + const configManager = ConfigManager.getInstance() + await configManager.initialize() + const dbType = configManager.getDatabaseType() + + console.log(`Database type: ${dbType}`) + + if (dbType === 'mysql') { + await runMySQLMigration() + } else if (dbType === 'sqlserver') { + await runSqlServerMigration() + } else if (dbType === 'postgresql') { + await runPostgreSqlMigration() + } else { + console.error(`Unsupported database type: ${dbType}`) + process.exit(1) + } + + console.log('Done.') +} + +main().catch((err) => { + console.error('Migration failed:', err) + process.exit(1) +}) diff --git a/src/main/types/cleaner-history.types.ts b/src/main/types/cleaner-history.types.ts index f295d81..2d0e2f5 100644 --- a/src/main/types/cleaner-history.types.ts +++ b/src/main/types/cleaner-history.types.ts @@ -31,6 +31,7 @@ export interface CleanerOrderRecord { batchId: string attemptNumber: number orderNumber: string + productionId: string | null status: string materialsDeleted: number materialsSkipped: number @@ -87,6 +88,9 @@ export interface InsertCleanerExecutionInput { /** 插入订单记录的输入 */ export interface InsertOrderInput { orderNumber: string + productionId?: string + initialStatus?: string + errorMessage?: string } /** 插入物料明细的输入 */ diff --git a/src/main/types/cleaner.types.ts b/src/main/types/cleaner.types.ts index 4633319..6e99de0 100644 --- a/src/main/types/cleaner.types.ts +++ b/src/main/types/cleaner.types.ts @@ -73,6 +73,8 @@ export interface OrderCleanDetail { materialsFailed: number failedMaterials: FailedMaterial[] uncertainDeletions: number + // Missing order flag: true when order not found in ERP query results + notFound?: boolean } export enum DeletionOutcome { diff --git a/src/renderer/src/components/CleanerOperationHistoryModal.tsx b/src/renderer/src/components/CleanerOperationHistoryModal.tsx index 4327d29..79c91a5 100644 --- a/src/renderer/src/components/CleanerOperationHistoryModal.tsx +++ b/src/renderer/src/components/CleanerOperationHistoryModal.tsx @@ -67,7 +67,9 @@ const statusStyles: Record = { partial: 'bg-amber-100 text-amber-700', failed: 'bg-red-100 text-red-700', crashed: 'bg-red-100 text-red-700', - pending: 'bg-gray-100 text-gray-700' + pending: 'bg-gray-100 text-gray-700', + not_found: 'bg-orange-100 text-orange-700', + erp_not_found: 'bg-orange-100 text-orange-700' } const statusLabels: Record = { @@ -75,7 +77,9 @@ const statusLabels: Record = { partial: '部分成功', failed: '失败', crashed: '崩溃', - pending: '进行中' + pending: '进行中', + not_found: '未找到', + erp_not_found: 'ERP不存在' } const statusIcons: Record = { @@ -83,7 +87,9 @@ const statusIcons: Record = { partial: , failed: , crashed: , - pending: + pending: , + not_found: , + erp_not_found: } const formatDateTime = (dateStr: string | Date | null | undefined): string => { @@ -429,6 +435,9 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => { + + 总排号 +
订单号 @@ -493,7 +502,10 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => { )} - {order.orderNumber} + {order.productionId || '-'} + + + {order.status === 'not_found' ? '-' : order.orderNumber} { {/* Material details */} {isOrderExpanded && ( - + {isLoadingMaterials ? (
加载物料详情... @@ -709,6 +721,8 @@ export const CleanerOperationHistoryModal: React.FC { if (isOpen) { void fetchBatches() @@ -826,6 +840,7 @@ export const CleanerOperationHistoryModal: React.FC + {/* Footer */} {/* Footer */}