feat(cleaner-history): add searchBatches DAO method

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-04-17 11:05:17 +08:00
parent c61d62fd98
commit 3a30694684

View File

@@ -18,7 +18,10 @@ import type {
InsertCleanerExecutionInput, InsertCleanerExecutionInput,
InsertOrderInput, InsertOrderInput,
InsertMaterialDetailInput, InsertMaterialDetailInput,
GetCleanerBatchesOptions GetCleanerBatchesOptions,
SearchCleanerHistoryOptions,
CleanerSearchBatchResult,
CleanerHistorySearchResult
} from '../../types/cleaner-history.types' } from '../../types/cleaner-history.types'
const log = createLogger('CleanerOperationHistoryDAO') 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<CleanerHistorySearchResult> {
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 ==================== // ==================== UTILITIES ====================
/** /**