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 <noreply@anthropic.com>
This commit is contained in:
@@ -288,31 +288,30 @@ export class ExtractorOperationHistoryDAO {
|
|||||||
`
|
`
|
||||||
|
|
||||||
if (options?.limit) {
|
if (options?.limit) {
|
||||||
// Add pagination - track current param count before adding new params
|
const safeLimit = Math.floor(options.limit)
|
||||||
const offsetIndex = params.length
|
const safeOffset = options.offset !== undefined ? Math.floor(options.offset) : undefined
|
||||||
const limitIndex = params.length + 1
|
|
||||||
|
|
||||||
if (options.offset !== undefined) {
|
|
||||||
params.push(options.offset)
|
|
||||||
}
|
|
||||||
params.push(options.limit)
|
|
||||||
|
|
||||||
if (isSqlServer) {
|
if (isSqlServer) {
|
||||||
if (options.offset !== undefined) {
|
// SQL Server: use parameterized OFFSET/FETCH
|
||||||
sqlString += ` OFFSET @p${offsetIndex} ROWS FETCH NEXT @p${limitIndex} ROWS ONLY`
|
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 {
|
} else {
|
||||||
// When no offset, use 0 for offset and next index for limit
|
|
||||||
sqlString += ` OFFSET 0 ROWS FETCH NEXT @p${offsetIndex} ROWS ONLY`
|
sqlString += ` OFFSET 0 ROWS FETCH NEXT @p${offsetIndex} ROWS ONLY`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (options.offset !== undefined) {
|
// MySQL: embed validated integer values directly.
|
||||||
sqlString += ` LIMIT ?`
|
// connection.execute() uses binary protocol prepared statements,
|
||||||
// For MySQL with offset, we need to modify the query
|
// which do not reliably support ? placeholders in LIMIT/OFFSET clauses.
|
||||||
// Replace LIMIT with OFFSET LIMIT
|
if (safeOffset !== undefined) {
|
||||||
const parts = sqlString.split(' LIMIT ?')
|
sqlString += ` LIMIT ${safeLimit} OFFSET ${safeOffset}`
|
||||||
sqlString = parts[0] + ` OFFSET ? LIMIT ?` + (parts[1] || '')
|
|
||||||
} else {
|
} else {
|
||||||
sqlString += ` LIMIT ?`
|
sqlString += ` LIMIT ${safeLimit}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Admin users see all users' records, regular users see only their own.
|
* 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 { Modal } from './ui/Modal'
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -17,32 +17,10 @@ import {
|
|||||||
Clock
|
Clock
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { UserInfo } from './UserSelectionDialog'
|
import type { UserInfo } from './UserSelectionDialog'
|
||||||
|
import type {
|
||||||
// Local type definitions matching the backend types
|
BatchStats,
|
||||||
interface BatchStats {
|
OperationHistoryRecord
|
||||||
batchId: string
|
} from '../../../main/types/operation-history.types'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtractorOperationHistoryModalProps {
|
interface ExtractorOperationHistoryModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -71,6 +49,17 @@ const statusIcons: Record<string, React.ReactNode> = {
|
|||||||
pending: <Clock size={16} className="text-gray-500" />
|
pending: <Clock size={16} className="text-gray-500" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<ExtractorOperationHistoryModalProps> = ({
|
export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -85,14 +74,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
|
|
||||||
const isAdmin = user?.userType === 'Admin'
|
const isAdmin = user?.userType === 'Admin'
|
||||||
|
|
||||||
// Fetch batches when modal opens
|
const fetchBatches = useCallback(async () => {
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
void fetchBatches()
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
const fetchBatches = async () => {
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
@@ -107,23 +89,33 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const fetchBatchDetails = async (batchId: string) => {
|
const fetchBatchDetails = useCallback(
|
||||||
// If already loaded, don't fetch again
|
async (batchId: string) => {
|
||||||
if (batchDetails.has(batchId)) {
|
// If already loaded, don't fetch again
|
||||||
return
|
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!))
|
|
||||||
}
|
}
|
||||||
} 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) => {
|
const toggleBatchExpansion = (batchId: string) => {
|
||||||
setExpandedBatches((prev) => {
|
setExpandedBatches((prev) => {
|
||||||
@@ -175,17 +167,6 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ const ExtractorPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showHistoryModal && (
|
{showHistoryModal ? (
|
||||||
<ExtractorOperationHistoryModal
|
<ExtractorOperationHistoryModal
|
||||||
isOpen={showHistoryModal}
|
isOpen={showHistoryModal}
|
||||||
onClose={() => setShowHistoryModal(false)}
|
onClose={() => setShowHistoryModal(false)}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{!isRunning && isComplete && (
|
{!isRunning && isComplete && (
|
||||||
<div className="bg-green-50 rounded-xl p-8 flex items-center justify-center gap-4 shadow-md">
|
<div className="bg-green-50 rounded-xl p-8 flex items-center justify-center gap-4 shadow-md">
|
||||||
|
|||||||
Reference in New Issue
Block a user