From 17fbd7d251d0dfaf6fd6212acdba094a3cb5cfb6 Mon Sep 17 00:00:00 2001 From: Misaka Date: Tue, 31 Mar 2026 19:42:44 +0800 Subject: [PATCH] fix(extractor): resolve MySQL LIMIT placeholder error in operation history query MySQL binary protocol prepared statements (connection.execute()) do not support ? placeholders in LIMIT/OFFSET clauses, causing "Incorrect arguments to mysqld_stmt_execute". Embed validated integer values directly for MySQL while keeping parameterized queries for SQL Server. Also apply React best practices to ExtractorOperationHistoryModal: - Hoist formatDateTime to module level - Wrap async handlers with useCallback for stable effect dependencies - Import shared types instead of duplicating definitions - Use ternary for conditional rendering Co-Authored-By: Claude Opus 4.6 --- .../extractor-operation-history-dao.ts | 35 +++--- .../ExtractorOperationHistoryModal.tsx | 101 +++++++----------- src/renderer/src/pages/ExtractorPage.tsx | 4 +- 3 files changed, 60 insertions(+), 80 deletions(-) diff --git a/src/main/services/database/extractor-operation-history-dao.ts b/src/main/services/database/extractor-operation-history-dao.ts index 25fb680..40c1901 100644 --- a/src/main/services/database/extractor-operation-history-dao.ts +++ b/src/main/services/database/extractor-operation-history-dao.ts @@ -288,31 +288,30 @@ export class ExtractorOperationHistoryDAO { ` if (options?.limit) { - // Add pagination - track current param count before adding new params - const offsetIndex = params.length - const limitIndex = params.length + 1 - - if (options.offset !== undefined) { - params.push(options.offset) - } - params.push(options.limit) + const safeLimit = Math.floor(options.limit) + const safeOffset = options.offset !== undefined ? Math.floor(options.offset) : undefined if (isSqlServer) { - if (options.offset !== undefined) { - sqlString += ` OFFSET @p${offsetIndex} ROWS FETCH NEXT @p${limitIndex} ROWS ONLY` + // SQL Server: use parameterized OFFSET/FETCH + const offsetIndex = params.length + if (safeOffset !== undefined) { + params.push(safeOffset) + } + params.push(safeLimit) + + if (safeOffset !== undefined) { + sqlString += ` OFFSET @p${offsetIndex} ROWS FETCH NEXT @p${offsetIndex + 1} ROWS ONLY` } else { - // When no offset, use 0 for offset and next index for limit sqlString += ` OFFSET 0 ROWS FETCH NEXT @p${offsetIndex} ROWS ONLY` } } else { - if (options.offset !== undefined) { - sqlString += ` LIMIT ?` - // For MySQL with offset, we need to modify the query - // Replace LIMIT with OFFSET LIMIT - const parts = sqlString.split(' LIMIT ?') - sqlString = parts[0] + ` OFFSET ? LIMIT ?` + (parts[1] || '') + // MySQL: embed validated integer values directly. + // connection.execute() uses binary protocol prepared statements, + // which do not reliably support ? placeholders in LIMIT/OFFSET clauses. + if (safeOffset !== undefined) { + sqlString += ` LIMIT ${safeLimit} OFFSET ${safeOffset}` } else { - sqlString += ` LIMIT ?` + sqlString += ` LIMIT ${safeLimit}` } } } diff --git a/src/renderer/src/components/ExtractorOperationHistoryModal.tsx b/src/renderer/src/components/ExtractorOperationHistoryModal.tsx index ec21af9..fd3bf9e 100644 --- a/src/renderer/src/components/ExtractorOperationHistoryModal.tsx +++ b/src/renderer/src/components/ExtractorOperationHistoryModal.tsx @@ -5,7 +5,7 @@ * Admin users see all users' records, regular users see only their own. */ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { Modal } from './ui/Modal' import { RefreshCw, @@ -17,32 +17,10 @@ import { Clock } from 'lucide-react' import type { UserInfo } from './UserSelectionDialog' - -// Local type definitions matching the backend types -interface BatchStats { - batchId: string - userId: number - username: string - operationTime: string - status: string - totalOrders: number - totalRecords: number - successCount: number - failedCount: number -} - -interface OperationHistoryRecord { - id?: number - batchId: string - userId: number - username: string - productionId: string | null - orderNumber: string - operationTime: Date - status: string - recordCount: number | null - errorMessage: string | null -} +import type { + BatchStats, + OperationHistoryRecord +} from '../../../main/types/operation-history.types' interface ExtractorOperationHistoryModalProps { isOpen: boolean @@ -71,6 +49,17 @@ const statusIcons: Record = { pending: } +const formatDateTime = (dateStr: string) => { + const date = new Date(dateStr) + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) +} + export const ExtractorOperationHistoryModal: React.FC = ({ isOpen, onClose, @@ -85,14 +74,7 @@ export const ExtractorOperationHistoryModal: React.FC { - if (isOpen) { - void fetchBatches() - } - }, [isOpen]) - - const fetchBatches = async () => { + const fetchBatches = useCallback(async () => { setLoading(true) setError(null) try { @@ -107,23 +89,33 @@ export const ExtractorOperationHistoryModal: React.FC { - // If already loaded, don't fetch again - if (batchDetails.has(batchId)) { - return - } - - try { - const result = await window.electron.operationHistory.getBatchDetails(batchId) - if (result.success && result.data) { - setBatchDetails((prev) => new Map(prev).set(batchId, result.data!)) + const fetchBatchDetails = useCallback( + async (batchId: string) => { + // If already loaded, don't fetch again + if (batchDetails.has(batchId)) { + return } - } catch (err) { - console.error('Failed to fetch batch details:', err) + + try { + const result = await window.electron.operationHistory.getBatchDetails(batchId) + if (result.success && result.data) { + setBatchDetails((prev) => new Map(prev).set(batchId, result.data!)) + } + } catch (err) { + console.error('Failed to fetch batch details:', err) + } + }, + [batchDetails] + ) + + // Fetch batches when modal opens + useEffect(() => { + if (isOpen) { + void fetchBatches() } - } + }, [isOpen, fetchBatches]) const toggleBatchExpansion = (batchId: string) => { setExpandedBatches((prev) => { @@ -175,17 +167,6 @@ export const ExtractorOperationHistoryModal: React.FC { - const date = new Date(dateStr) - return date.toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) - } - if (!isOpen) return null return ( diff --git a/src/renderer/src/pages/ExtractorPage.tsx b/src/renderer/src/pages/ExtractorPage.tsx index 5373d30..195d813 100644 --- a/src/renderer/src/pages/ExtractorPage.tsx +++ b/src/renderer/src/pages/ExtractorPage.tsx @@ -90,13 +90,13 @@ const ExtractorPage: React.FC = () => { - {showHistoryModal && ( + {showHistoryModal ? ( setShowHistoryModal(false)} user={user} /> - )} + ) : null} {!isRunning && isComplete && (