Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cbe9eef12 | ||
|
|
5b310d944b | ||
|
|
6e04f21b10 | ||
|
|
17fbd7d251 |
6
docs/releases/1.7.1.md
Normal file
6
docs/releases/1.7.1.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# 1.7.1
|
||||
|
||||
## 问题修复
|
||||
|
||||
- 修复 MySQL 数据库下操作历史查询报错问题。
|
||||
- 优化历史记录数据结构,支持按订单统计记录数量。
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "erpauto",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "erpauto",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.929.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "erpauto",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"description": "An Electron application with React and TypeScript",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
|
||||
@@ -256,7 +256,14 @@ export function registerExtractorHandlers(): void {
|
||||
: result.errors.length > 0
|
||||
? 'failed'
|
||||
: 'success'
|
||||
await historyDao.updateBatchStatus(batchId, status, result.recordCount)
|
||||
|
||||
// Write per-order record counts
|
||||
for (const { orderNumber, recordCount } of result.orderRecordCounts) {
|
||||
await historyDao.updateRecordStatus(batchId, orderNumber, status, undefined, recordCount)
|
||||
}
|
||||
|
||||
// Update batch status without recordCount (per-order counts are set individually)
|
||||
await historyDao.updateBatchStatus(batchId, status)
|
||||
log.info('Operation history batch status updated', { batchId, status })
|
||||
}
|
||||
|
||||
|
||||
@@ -160,43 +160,27 @@ export class ExtractorOperationHistoryDAO {
|
||||
* Update the status of all records in a batch
|
||||
* @param batchId - Batch identifier
|
||||
* @param status - New status (success, failed, partial)
|
||||
* @param recordCount - Total record count for the batch
|
||||
* @returns Update result
|
||||
*/
|
||||
async updateBatchStatus(
|
||||
batchId: string,
|
||||
status: string,
|
||||
recordCount: number | null
|
||||
status: string
|
||||
): Promise<UpdateBatchStatusResult> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const isSqlServer = dbService.type === 'sqlserver'
|
||||
|
||||
const placeholder = isSqlServer ? '@p0' : '?'
|
||||
let sqlString: string
|
||||
let params: (string | number | null)[]
|
||||
|
||||
if (recordCount !== null) {
|
||||
sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||
RecordCount = ${isSqlServer ? '@p1' : '?'}
|
||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
||||
`
|
||||
params = isSqlServer ? [status, recordCount, batchId] : [status, recordCount, batchId]
|
||||
} else {
|
||||
sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET Status = ${placeholder}
|
||||
WHERE BatchId = ${isSqlServer ? '@p1' : '?'}
|
||||
`
|
||||
params = isSqlServer ? [status, batchId] : [status, batchId]
|
||||
}
|
||||
const sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET Status = ${isSqlServer ? '@p0' : '?'}
|
||||
WHERE BatchId = ${isSqlServer ? '@p1' : '?'}
|
||||
`
|
||||
const params = [status, batchId]
|
||||
|
||||
await dbService.query(sqlString, params)
|
||||
|
||||
log.info('Batch status updated', { batchId, status, recordCount })
|
||||
log.info('Batch status updated', { batchId, status })
|
||||
return { success: true, updatedCount: 1 }
|
||||
} catch (error) {
|
||||
log.error('Update batch status error', {
|
||||
@@ -208,33 +192,51 @@ export class ExtractorOperationHistoryDAO {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single record's status and error message
|
||||
* Update a single record's status, error message, and optional record count
|
||||
* @param batchId - Batch identifier
|
||||
* @param orderNumber - Order number
|
||||
* @param status - New status
|
||||
* @param errorMessage - Optional error message
|
||||
* @param recordCount - Optional per-order record count
|
||||
* @returns True if successful
|
||||
*/
|
||||
async updateRecordStatus(
|
||||
batchId: string,
|
||||
orderNumber: string,
|
||||
status: string,
|
||||
errorMessage?: string
|
||||
errorMessage?: string,
|
||||
recordCount?: number
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const isSqlServer = dbService.type === 'sqlserver'
|
||||
|
||||
const sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||
ErrorMessage = ${isSqlServer ? '@p1' : '?'}
|
||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
||||
AND OrderNumber = ${isSqlServer ? '@p3' : '?'}
|
||||
`
|
||||
let sqlString: string
|
||||
let params: (string | number | null)[]
|
||||
|
||||
await dbService.query(sqlString, [status, errorMessage || null, batchId, orderNumber])
|
||||
if (recordCount !== undefined) {
|
||||
sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||
ErrorMessage = ${isSqlServer ? '@p1' : '?'},
|
||||
RecordCount = ${isSqlServer ? '@p2' : '?'}
|
||||
WHERE BatchId = ${isSqlServer ? '@p3' : '?'}
|
||||
AND OrderNumber = ${isSqlServer ? '@p4' : '?'}
|
||||
`
|
||||
params = [status, errorMessage || null, recordCount, batchId, orderNumber]
|
||||
} else {
|
||||
sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET Status = ${isSqlServer ? '@p0' : '?'},
|
||||
ErrorMessage = ${isSqlServer ? '@p1' : '?'}
|
||||
WHERE BatchId = ${isSqlServer ? '@p2' : '?'}
|
||||
AND OrderNumber = ${isSqlServer ? '@p3' : '?'}
|
||||
`
|
||||
params = [status, errorMessage || null, batchId, orderNumber]
|
||||
}
|
||||
|
||||
await dbService.query(sqlString, params)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -288,31 +290,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}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ export class ExtractorService {
|
||||
downloadedFiles: [],
|
||||
mergedFile: null,
|
||||
recordCount: 0,
|
||||
errors: []
|
||||
errors: [],
|
||||
orderRecordCounts: []
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -79,6 +80,7 @@ export class ExtractorService {
|
||||
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
||||
result.mergedFile = mergeResult.mergedFile
|
||||
result.recordCount = mergeResult.recordCount
|
||||
result.orderRecordCounts = mergeResult.orderRecordCounts
|
||||
|
||||
// Add merge error to result if any
|
||||
if (mergeResult.error) {
|
||||
@@ -123,9 +125,14 @@ export class ExtractorService {
|
||||
*/
|
||||
private async mergeFiles(
|
||||
filePaths: string[]
|
||||
): Promise<{ mergedFile: string | null; recordCount: number; error?: string }> {
|
||||
): Promise<{
|
||||
mergedFile: string | null
|
||||
recordCount: number
|
||||
error?: string
|
||||
orderRecordCounts: Array<{ orderNumber: string; recordCount: number }>
|
||||
}> {
|
||||
if (filePaths.length === 0) {
|
||||
return { mergedFile: null, recordCount: 0 }
|
||||
return { mergedFile: null, recordCount: 0, orderRecordCounts: [] }
|
||||
}
|
||||
|
||||
log.info('Starting merge', { fileCount: filePaths.length })
|
||||
@@ -154,15 +161,21 @@ export class ExtractorService {
|
||||
|
||||
// Calculate total record count (total material rows)
|
||||
let recordCount = 0
|
||||
const orderRecordCounts: Array<{ orderNumber: string; recordCount: number }> = []
|
||||
for (const order of allOrders) {
|
||||
recordCount += order.materials.length
|
||||
const count = order.materials.length
|
||||
recordCount += count
|
||||
orderRecordCounts.push({
|
||||
orderNumber: order.orderInfo.productionOrder || '',
|
||||
recordCount: count
|
||||
})
|
||||
}
|
||||
|
||||
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
|
||||
|
||||
if (recordCount === 0) {
|
||||
log.warn('No records found in any downloaded files')
|
||||
return { mergedFile: null, recordCount: 0 }
|
||||
return { mergedFile: null, recordCount: 0, orderRecordCounts }
|
||||
}
|
||||
|
||||
// Generate output filename with timestamp
|
||||
@@ -178,13 +191,13 @@ export class ExtractorService {
|
||||
log.info('Saving merged file', { outputPath })
|
||||
await this.saveMergedOrders(allOrders, outputPath)
|
||||
log.info('Merged file saved successfully', { recordCount })
|
||||
return { mergedFile: outputPath, recordCount }
|
||||
return { mergedFile: outputPath, recordCount, orderRecordCounts }
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? error.stack : ''
|
||||
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
|
||||
// Return parsed record count and error info even if save fails
|
||||
return { mergedFile: null, recordCount, error: `保存合并文件失败:${errorMsg}` }
|
||||
return { mergedFile: null, recordCount, orderRecordCounts, error: `保存合并文件失败:${errorMsg}` }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface ExtractorResult {
|
||||
errors: string[]
|
||||
/** Database import result (only populated if mergedFile was created) */
|
||||
importResult?: ImportResult
|
||||
/** Per-order material row counts */
|
||||
orderRecordCounts: Array<{ orderNumber: string; recordCount: number }>
|
||||
}
|
||||
|
||||
export interface OrderInfo {
|
||||
|
||||
@@ -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<string, React.ReactNode> = {
|
||||
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> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -85,14 +74,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
|
||||
const isAdmin = user?.userType === 'Admin'
|
||||
|
||||
// Fetch batches when modal opens
|
||||
useEffect(() => {
|
||||
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<ExtractorOperationHistoryM
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchBatchDetails = async (batchId: string) => {
|
||||
// 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<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
|
||||
|
||||
return (
|
||||
|
||||
@@ -90,13 +90,13 @@ const ExtractorPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showHistoryModal && (
|
||||
{showHistoryModal ? (
|
||||
<ExtractorOperationHistoryModal
|
||||
isOpen={showHistoryModal}
|
||||
onClose={() => setShowHistoryModal(false)}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{!isRunning && isComplete && (
|
||||
<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