From 3a30694684a0824b5a8d5e5f5ce5df5416df7c6f Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Fri, 17 Apr 2026 11:05:17 +0800 Subject: [PATCH] feat(cleaner-history): add searchBatches DAO method Co-Authored-By: Claude Opus 4.6 --- .../database/cleaner-operation-history-dao.ts | 168 +++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/src/main/services/database/cleaner-operation-history-dao.ts b/src/main/services/database/cleaner-operation-history-dao.ts index da2fc19..e2f4ca7 100644 --- a/src/main/services/database/cleaner-operation-history-dao.ts +++ b/src/main/services/database/cleaner-operation-history-dao.ts @@ -18,7 +18,10 @@ import type { InsertCleanerExecutionInput, InsertOrderInput, InsertMaterialDetailInput, - GetCleanerBatchesOptions + GetCleanerBatchesOptions, + SearchCleanerHistoryOptions, + CleanerSearchBatchResult, + CleanerHistorySearchResult } from '../../types/cleaner-history.types' const log = createLogger('CleanerOperationHistoryDAO') @@ -980,6 +983,169 @@ export class CleanerOperationHistoryDAO { } } + // ==================== QUERY: SEARCH BATCHES ==================== + + /** + * Full-level search across batches, orders, and materials. + * Uses a UNION ALL query to find matching BatchIds, then fetches + * full nested data for each matched batch. + * + * @param userId - Optional user ID for filtering + * @param options - Search options (query string, usernames filter, result limit) + * @returns Search result with matched batches and total count + */ + async searchBatches( + userId: number | undefined, + options: SearchCleanerHistoryOptions + ): Promise { + const emptyResult: CleanerHistorySearchResult = { batches: [], totalMatches: 0 } + + try { + const dbService = await this.getDatabaseService() + const execTable = this.getExecutionTableName() + const orderTable = this.getOrderTableName() + const materialTable = this.getMaterialTableName() + const dialect = this.getDialect() + + const { query, limit = 20 } = options + const safeLimit = Math.floor(limit) + const likePattern = `%${query}%` + + // Build UNION ALL to find distinct BatchIds matching the query + // Each subquery searches a different table's columns + let paramIndex = 0 + const p = () => dialect.param(paramIndex++) + + const unionSql = ` + SELECT DISTINCT BatchId FROM ( + SELECT BatchId FROM ${execTable} + WHERE BatchId LIKE ${p()} + OR Username LIKE ${p()} + OR Status LIKE ${p()} + UNION ALL + SELECT BatchId FROM ${orderTable} + WHERE OrderNumber LIKE ${p()} + OR ProductionId LIKE ${p()} + UNION ALL + SELECT BatchId FROM ${materialTable} + WHERE MaterialCode LIKE ${p()} + OR MaterialName LIKE ${p()} + ) AS matched + WHERE BatchId IN ( + SELECT BatchId FROM ${execTable} + WHERE 1=1 + ${userId !== undefined ? `AND UserId = ${p()}` : ''} + ${userId === undefined && options.usernames && options.usernames.length > 0 ? `AND Username IN (${dialect.params(options.usernames.length)})` : ''} + ) + ` + + const unionParams: (string | number)[] = [ + likePattern, likePattern, likePattern, // exec table: BatchId, Username, Status + likePattern, likePattern, // order table: OrderNumber, ProductionId + likePattern, likePattern // material table: MaterialCode, MaterialName + ] + + // User filtering params + if (userId !== undefined) { + unionParams.push(userId) + } else if (options.usernames && options.usernames.length > 0) { + unionParams.push(...options.usernames) + } + + const { result } = await trackDuration( + async () => await dbService.query(unionSql, unionParams), + { + operationName: 'CleanerOperationHistoryDAO.searchBatches.union', + context: { operationType: 'SELECT', query } + } + ) + + const matchedBatchIds: string[] = result.rows.map((row) => row.BatchId as string) + const totalMatches = matchedBatchIds.length + const limitedBatchIds = matchedBatchIds.slice(0, safeLimit) + + if (limitedBatchIds.length === 0) { + return { batches: [], totalMatches: 0 } + } + + // Fetch full nested data for each matched batch + const batches: CleanerSearchBatchResult[] = [] + + for (const batchId of limitedBatchIds) { + try { + const details = await this.getBatchDetails(batchId) + + if (details.executions.length === 0) { + continue + } + + // Derive batch stats from execution records + const latestExec = details.executions.reduce((a, b) => + a.attemptNumber > b.attemptNumber ? a : b + ) + + const batch: CleanerBatchStats = { + batchId, + userId: latestExec.userId, + username: latestExec.username, + operationTime: latestExec.operationTime.toISOString(), + status: latestExec.status, + totalAttempts: details.executions.length, + totalOrders: latestExec.totalOrders, + ordersProcessed: latestExec.ordersProcessed, + totalMaterialsDeleted: latestExec.totalMaterialsDeleted, + totalMaterialsFailed: latestExec.totalMaterialsFailed, + successCount: details.orders.filter((o) => o.status === 'success').length, + failedCount: details.orders.filter((o) => o.status === 'failed').length, + isDryRun: latestExec.isDryRun + } + + // Fetch materials for each order + const ordersWithMaterials = await Promise.all( + details.orders.map(async (order) => { + const materials = await this.getMaterialDetails( + batchId, + order.attemptNumber, + order.orderNumber + ) + return { order, materials } + }) + ) + + batches.push({ + batch, + executions: details.executions, + orders: ordersWithMaterials + }) + } catch (error) { + log.error('Error fetching batch data for search result', { + operationType: 'SELECT', + batchId, + error: error instanceof Error ? error.message : String(error) + }) + } + } + + log.info('Search batches completed', { + operationType: 'SELECT', + requestId: getRequestId(), + query, + totalMatches, + returnedBatches: batches.length + }) + + return { batches, totalMatches } + } catch (error) { + log.error('Search batches error', { + operationType: 'SELECT', + requestId: getRequestId(), + query: options.query, + error: error instanceof Error ? error.message : String(error) + }) + return emptyResult + } + } + // ==================== UTILITIES ==================== /**