From 6f49596467a939471a6a773f9dfc1484fba9cdba Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Tue, 14 Apr 2026 10:08:15 +0800 Subject: [PATCH] feat(cleaner-history): record missing orders with production ID tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record ALL input orders in history, including resolution failures (not_found) and ERP query misses (erp_not_found). Add ProductionId column to track original 总排号 input. Add 总排号 column and new status styles to the history UI. Fix empty result caching that prevented retry on transient query failures. Co-Authored-By: Claude Opus 4.6 --- .../cleaner/cleaner-application-service.ts | 104 ++++++++++--- .../database/cleaner-operation-history-dao.ts | 20 ++- src/main/services/erp/cleaner.ts | 7 +- .../add-productionid-to-cleaner-order.ts | 142 ++++++++++++++++++ src/main/types/cleaner-history.types.ts | 4 + src/main/types/cleaner.types.ts | 2 + .../CleanerOperationHistoryModal.tsx | 25 ++- src/renderer/src/hooks/cleaner/types.ts | 1 + .../cleaner-application-service.test.ts | 12 +- 9 files changed, 278 insertions(+), 39 deletions(-) create mode 100644 src/main/services/user/migration/add-productionid-to-cleaner-order.ts 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 */}