fix: restore cleaner history in postgresql
This commit is contained in:
@@ -122,6 +122,15 @@ export class CleanerOperationHistoryDAO {
|
||||
return this.getDialect().quoteTableName('ERPAuto', 'CleanerMaterialDetail')
|
||||
}
|
||||
|
||||
private getIsDryRunAggregateSql(): string {
|
||||
const dialect = this.getDialect()
|
||||
if (dialect.dbType === 'postgresql') {
|
||||
return `MAX(CASE WHEN e.IsDryRun THEN 1 ELSE 0 END)`
|
||||
}
|
||||
|
||||
return `MAX(CAST(e.IsDryRun AS INT))`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database service instance using DatabaseFactory
|
||||
*/
|
||||
@@ -619,9 +628,9 @@ export class CleanerOperationHistoryDAO {
|
||||
MAX(CASE WHEN e.AttemptNumber = latest.max_attempt THEN e.OrdersProcessed ELSE 0 END) as OrdersProcessed,
|
||||
MAX(CASE WHEN e.AttemptNumber = latest.max_attempt THEN e.TotalMaterialsDeleted ELSE 0 END) as TotalMaterialsDeleted,
|
||||
MAX(CASE WHEN e.AttemptNumber = latest.max_attempt THEN e.TotalMaterialsFailed ELSE 0 END) as TotalMaterialsFailed,
|
||||
MAX(CAST(e.IsDryRun AS INT)) as IsDryRun,
|
||||
ISNULL(SUM(CASE WHEN o.Status = 'success' THEN 1 ELSE 0 END), 0) as SuccessCount,
|
||||
ISNULL(SUM(CASE WHEN o.Status = 'failed' THEN 1 ELSE 0 END), 0) as FailedCount
|
||||
${this.getIsDryRunAggregateSql()} as IsDryRun,
|
||||
COALESCE(SUM(CASE WHEN o.Status = 'success' THEN 1 ELSE 0 END), 0) as SuccessCount,
|
||||
COALESCE(SUM(CASE WHEN o.Status = 'failed' THEN 1 ELSE 0 END), 0) as FailedCount
|
||||
FROM ${execTable} e
|
||||
INNER JOIN (
|
||||
SELECT BatchId, MAX(AttemptNumber) as max_attempt
|
||||
@@ -906,27 +915,13 @@ export class CleanerOperationHistoryDAO {
|
||||
const materialTable = this.getMaterialTableName()
|
||||
const dialect = this.getDialect()
|
||||
|
||||
// Check if batch exists
|
||||
const checkSql = `
|
||||
SELECT TOP 1 UserId
|
||||
FROM ${execTable}
|
||||
WHERE BatchId = ${dialect.param(0)}
|
||||
`
|
||||
|
||||
const checkResult = await trackDuration(
|
||||
async () => await dbService.query(checkSql, [batchId]),
|
||||
{
|
||||
operationName: 'CleanerOperationHistoryDAO.deleteBatch.check',
|
||||
context: { operationType: 'SELECT', batchId }
|
||||
}
|
||||
)
|
||||
|
||||
if (checkResult.result.rows.length === 0) {
|
||||
const details = await this.getBatchDetails(batchId)
|
||||
if (details.executions.length === 0) {
|
||||
return { success: false, error: '批次不存在' }
|
||||
}
|
||||
|
||||
// Permission check: non-admin can only delete own batches
|
||||
const batchUserId = checkResult.result.rows[0].UserId as number
|
||||
const batchUserId = details.executions[0].userId
|
||||
if (!isAdmin && batchUserId !== requestingUserId) {
|
||||
return { success: false, error: '没有权限删除此批次' }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Admin users see all users' records, regular users see only their own.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Modal } from './ui/Modal'
|
||||
import { useLogger } from '../hooks/useLogger'
|
||||
import {
|
||||
@@ -28,6 +28,11 @@ import type {
|
||||
CleanerHistoryOrderRecord,
|
||||
CleanerHistoryMaterialRecord
|
||||
} from '../hooks/cleaner/types'
|
||||
import {
|
||||
canStartHistoryLoad,
|
||||
getNextHistoryLoadState,
|
||||
type HistoryLoadState
|
||||
} from './cleaner-history-load-state'
|
||||
|
||||
// The preload API returns Date for time fields, but IPC serialization converts them to strings.
|
||||
// Use a local type that accommodates both to satisfy TypeScript.
|
||||
@@ -145,46 +150,53 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
() => new Map()
|
||||
)
|
||||
const [loadingMaterials, setLoadingMaterials] = useState<Set<string>>(() => new Set())
|
||||
const [materialLoadStates, setMaterialLoadStates] = useState<Map<string, HistoryLoadState>>(
|
||||
() => new Map()
|
||||
)
|
||||
const [detailsLoadState, setDetailsLoadState] = useState<HistoryLoadState>('idle')
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const detailsLoadedRef = useRef(false)
|
||||
const loadedMaterialsRef = useRef<Set<string>>(new Set())
|
||||
const logger = useLogger('BatchItem')
|
||||
|
||||
// Fetch batch details when first expanded
|
||||
useEffect(() => {
|
||||
if (!isExpanded || detailsLoadedRef.current) return
|
||||
detailsLoadedRef.current = true
|
||||
const fetchDetails = useCallback(async () => {
|
||||
if (!canStartHistoryLoad(detailsLoadState)) return
|
||||
|
||||
const fetchDetails = async () => {
|
||||
try {
|
||||
const result = await window.electron.cleaner.getHistoryBatchDetails(batch.batchId)
|
||||
if (result.success && result.data) {
|
||||
setExecutions(result.data.executions)
|
||||
setOrders(result.data.orders)
|
||||
setDetailsLoadState((prev) => getNextHistoryLoadState(prev, 'start'))
|
||||
try {
|
||||
const result = await window.electron.cleaner.getHistoryBatchDetails(batch.batchId)
|
||||
if (result.success && result.data) {
|
||||
setExecutions(result.data.executions)
|
||||
setOrders(result.data.orders)
|
||||
|
||||
const execs = result.data.executions
|
||||
if (execs.length > 0) {
|
||||
setCurrentAttempt(Math.max(...execs.map((e) => e.attemptNumber)))
|
||||
}
|
||||
const execs = result.data.executions
|
||||
if (execs.length > 0) {
|
||||
setCurrentAttempt(Math.max(...execs.map((e) => e.attemptNumber)))
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to fetch batch details', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
batchId: batch.batchId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void fetchDetails()
|
||||
}, [isExpanded, batch.batchId, logger])
|
||||
setDetailsLoadState('success')
|
||||
} else {
|
||||
setDetailsLoadState('error')
|
||||
}
|
||||
} catch (err) {
|
||||
setDetailsLoadState('error')
|
||||
logger.error('Failed to fetch batch details', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
batchId: batch.batchId
|
||||
})
|
||||
}
|
||||
}, [batch.batchId, detailsLoadState, logger])
|
||||
|
||||
const fetchMaterials = useCallback(
|
||||
async (attemptNumber: number, orderNumber: string) => {
|
||||
const cacheKey = `${attemptNumber}:${orderNumber}`
|
||||
if (loadedMaterialsRef.current.has(cacheKey)) return
|
||||
loadedMaterialsRef.current.add(cacheKey)
|
||||
const loadState = materialLoadStates.get(cacheKey) ?? 'idle'
|
||||
if (!canStartHistoryLoad(loadState)) return
|
||||
|
||||
setMaterialLoadStates((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cacheKey, getNextHistoryLoadState(prev.get(cacheKey) ?? 'idle', 'start'))
|
||||
return next
|
||||
})
|
||||
setLoadingMaterials((prev) => new Set(prev).add(cacheKey))
|
||||
try {
|
||||
const result = await window.electron.cleaner.getHistoryMaterialDetails(
|
||||
@@ -194,8 +206,24 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
)
|
||||
if (result.success && result.data) {
|
||||
setOrderMaterials((prev) => new Map(prev).set(cacheKey, result.data!))
|
||||
setMaterialLoadStates((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cacheKey, 'success')
|
||||
return next
|
||||
})
|
||||
} else {
|
||||
setMaterialLoadStates((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cacheKey, 'error')
|
||||
return next
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
setMaterialLoadStates((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cacheKey, 'error')
|
||||
return next
|
||||
})
|
||||
logger.error('Failed to fetch material details', {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
batchId: batch.batchId,
|
||||
@@ -210,9 +238,17 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
[batch.batchId, logger]
|
||||
[batch.batchId, logger, materialLoadStates]
|
||||
)
|
||||
|
||||
const toggleBatchExpansion = () => {
|
||||
const nextExpanded = !isExpanded
|
||||
setIsExpanded(nextExpanded)
|
||||
if (nextExpanded) {
|
||||
void fetchDetails()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleOrderExpansion = (attemptNumber: number, orderNumber: string) => {
|
||||
const cacheKey = `${attemptNumber}:${orderNumber}`
|
||||
const isCurrentlyExpanded = expandedOrders.has(cacheKey)
|
||||
@@ -281,7 +317,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
className={`flex items-center justify-between p-4 cursor-pointer transition-colors ${
|
||||
isExpanded ? 'bg-gray-50' : 'hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => setIsExpanded((prev) => !prev)}
|
||||
onClick={toggleBatchExpansion}
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<button className="p-1 hover:bg-gray-200 rounded">
|
||||
@@ -415,7 +451,13 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
)}
|
||||
|
||||
{/* Order table */}
|
||||
{filteredOrders.length > 0 ? (
|
||||
{detailsLoadState === 'loading' && executions.length === 0 && orders.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-sm text-gray-500">加载详情中...</div>
|
||||
) : detailsLoadState === 'error' && executions.length === 0 && orders.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-sm text-red-600">
|
||||
加载详情失败,请折叠后重新展开重试
|
||||
</div>
|
||||
) : filteredOrders.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
@@ -452,6 +494,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
const isOrderExpanded = expandedOrders.has(orderKey)
|
||||
const materials = orderMaterials.get(orderKey) || []
|
||||
const isLoadingMaterials = loadingMaterials.has(orderKey)
|
||||
const materialLoadState = materialLoadStates.get(orderKey) ?? 'idle'
|
||||
|
||||
return (
|
||||
<React.Fragment key={orderKey}>
|
||||
@@ -531,6 +574,10 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
||||
<td colSpan={11} className="bg-gray-50/50 px-8 py-3">
|
||||
{isLoadingMaterials ? (
|
||||
<div className="text-xs text-gray-500">加载物料详情...</div>
|
||||
) : materialLoadState === 'error' ? (
|
||||
<div className="text-xs text-red-600">
|
||||
加载物料详情失败,请折叠后重新展开重试
|
||||
</div>
|
||||
) : materials.length > 0 ? (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
|
||||
19
src/renderer/src/components/cleaner-history-load-state.ts
Normal file
19
src/renderer/src/components/cleaner-history-load-state.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type HistoryLoadState = 'idle' | 'loading' | 'success' | 'error'
|
||||
|
||||
export const canStartHistoryLoad = (state: HistoryLoadState): boolean =>
|
||||
state === 'idle' || state === 'error'
|
||||
|
||||
export const getNextHistoryLoadState = (
|
||||
currentState: HistoryLoadState,
|
||||
event: 'start' | 'success' | 'error'
|
||||
): HistoryLoadState => {
|
||||
if (event === 'start') {
|
||||
return canStartHistoryLoad(currentState) ? 'loading' : currentState
|
||||
}
|
||||
|
||||
if (event === 'success') {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
return 'error'
|
||||
}
|
||||
@@ -167,17 +167,19 @@ const CleanerPage: React.FC = () => {
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<CleanerOperationHistoryModal
|
||||
isOpen={showHistoryModal}
|
||||
onClose={() => setShowHistoryModal(false)}
|
||||
user={
|
||||
currentUsername
|
||||
? { username: currentUsername, userType: isAdmin ? 'Admin' : 'User' }
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
{showHistoryModal ? (
|
||||
<Suspense fallback={null}>
|
||||
<CleanerOperationHistoryModal
|
||||
isOpen={showHistoryModal}
|
||||
onClose={() => setShowHistoryModal(false)}
|
||||
user={
|
||||
currentUsername
|
||||
? { username: currentUsername, userType: isAdmin ? 'Admin' : 'User' }
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{confirmDialog && <ConfirmDialog {...confirmDialog} />}
|
||||
|
||||
29
tests/unit/cleaner-history-load-state.test.ts
Normal file
29
tests/unit/cleaner-history-load-state.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
canStartHistoryLoad,
|
||||
getNextHistoryLoadState
|
||||
} from '../../src/renderer/src/components/cleaner-history-load-state'
|
||||
|
||||
describe('cleaner history load state helpers', () => {
|
||||
it('allows initial and failed loads to retry', () => {
|
||||
expect(canStartHistoryLoad('idle')).toBe(true)
|
||||
expect(canStartHistoryLoad('error')).toBe(true)
|
||||
})
|
||||
|
||||
it('prevents duplicate requests after loading starts or succeeds', () => {
|
||||
expect(canStartHistoryLoad('loading')).toBe(false)
|
||||
expect(canStartHistoryLoad('success')).toBe(false)
|
||||
})
|
||||
|
||||
it('retries after an error but keeps successful loads cached', () => {
|
||||
const failedState = getNextHistoryLoadState('loading', 'error')
|
||||
const retryState = getNextHistoryLoadState(failedState, 'start')
|
||||
const successState = getNextHistoryLoadState(retryState, 'success')
|
||||
const blockedState = getNextHistoryLoadState(successState, 'start')
|
||||
|
||||
expect(failedState).toBe('error')
|
||||
expect(retryState).toBe('loading')
|
||||
expect(successState).toBe('success')
|
||||
expect(blockedState).toBe('success')
|
||||
})
|
||||
})
|
||||
107
tests/unit/cleaner-page-history-lazy.test.tsx
Normal file
107
tests/unit/cleaner-page-history-lazy.test.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
|
||||
const { mockHistoryModalModuleLoad } = vi.hoisted(() => ({
|
||||
mockHistoryModalModuleLoad: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/hooks/useCleaner', () => ({
|
||||
useCleaner: () => ({
|
||||
isAdmin: false,
|
||||
currentUsername: 'tester',
|
||||
dryRun: false,
|
||||
setDryRun: vi.fn(),
|
||||
valMode: 'database_full',
|
||||
setValMode: vi.fn(),
|
||||
validationResults: [],
|
||||
selectedItems: new Set<string>(),
|
||||
setSelectedItems: vi.fn(),
|
||||
setHiddenItems: vi.fn(),
|
||||
managers: [],
|
||||
selectedManagers: [],
|
||||
setSelectedManagers: vi.fn(),
|
||||
isRunning: false,
|
||||
isExecuting: false,
|
||||
isValidationRunning: false,
|
||||
isExporting: false,
|
||||
isTypeDialogOpen: false,
|
||||
setIsTypeDialogOpen: vi.fn(),
|
||||
headless: false,
|
||||
setHeadless: vi.fn(),
|
||||
processConcurrency: 1,
|
||||
updateProcessConcurrency: vi.fn(),
|
||||
showSettingsMenu: false,
|
||||
setShowSettingsMenu: vi.fn(),
|
||||
filteredResults: [],
|
||||
isReportDialogOpen: false,
|
||||
setIsReportDialogOpen: vi.fn(),
|
||||
reportData: null,
|
||||
editingCell: null,
|
||||
editValue: '',
|
||||
setEditValue: vi.fn(),
|
||||
inputRef: { current: null },
|
||||
startEdit: vi.fn(),
|
||||
saveEdit: vi.fn(),
|
||||
cancelEdit: vi.fn(),
|
||||
handleAssignManagerOnSelect: vi.fn(),
|
||||
progress: null,
|
||||
startTime: null,
|
||||
resetStartTime: vi.fn(),
|
||||
handleValidation: vi.fn(),
|
||||
handleCheckboxToggle: vi.fn(),
|
||||
handleConfirmDeletion: vi.fn(),
|
||||
handleExecuteDeletion: vi.fn(),
|
||||
handleExportResults: vi.fn(),
|
||||
confirmDialog: null
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/cleaner/CleanerExecutionBar', () => ({
|
||||
CleanerExecutionBar: () => React.createElement('div', null, 'execution-bar')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/cleaner/CleanerResultsTable', () => ({
|
||||
CleanerResultsTable: () => React.createElement('div', null, 'results-table')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/cleaner/CleanerSidebar', () => ({
|
||||
CleanerSidebar: () => React.createElement('aside', null, 'sidebar')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/cleaner/CleanerToolbar', () => ({
|
||||
CleanerToolbar: () => React.createElement('div', null, 'toolbar')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/ui/ConfirmDialog', () => ({
|
||||
ConfirmDialog: () => React.createElement('div', null, 'confirm-dialog')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/MaterialTypeManagementDialog', () => ({
|
||||
default: () => React.createElement('div', null, 'type-dialog')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/ExecutionReportDialog', () => ({
|
||||
default: () => React.createElement('div', null, 'report-dialog')
|
||||
}))
|
||||
|
||||
vi.mock('../../src/renderer/src/components/CleanerOperationHistoryModal', () => {
|
||||
mockHistoryModalModuleLoad()
|
||||
return {
|
||||
default: () => React.createElement('div', null, 'history-dialog')
|
||||
}
|
||||
})
|
||||
|
||||
import CleanerPage from '../../src/renderer/src/pages/CleanerPage'
|
||||
|
||||
describe('CleanerPage history modal lazy loading', () => {
|
||||
beforeEach(() => {
|
||||
mockHistoryModalModuleLoad.mockClear()
|
||||
})
|
||||
|
||||
it('does not load the history modal module on the initial render', () => {
|
||||
renderToStaticMarkup(React.createElement(CleanerPage))
|
||||
|
||||
expect(mockHistoryModalModuleLoad).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const queryMock = vi.fn()
|
||||
const createMock = vi.fn()
|
||||
const trackDurationMock = vi.fn(async (fn: () => Promise<unknown>) => ({ result: await fn() }))
|
||||
|
||||
vi.mock('../../../../src/main/services/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn()
|
||||
}),
|
||||
getRequestId: () => 'test-request-id',
|
||||
trackDuration: trackDurationMock
|
||||
}))
|
||||
|
||||
vi.mock('../../../../src/main/services/database/index', () => ({
|
||||
create: createMock
|
||||
}))
|
||||
|
||||
describe('CleanerOperationHistoryDAO (PostgreSQL compatibility)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
createMock.mockResolvedValue({
|
||||
type: 'postgresql',
|
||||
isConnected: () => true,
|
||||
query: queryMock,
|
||||
disconnect: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
it('uses PostgreSQL-compatible aggregation in getBatches', async () => {
|
||||
queryMock.mockResolvedValue({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 0
|
||||
})
|
||||
|
||||
const { CleanerOperationHistoryDAO } = await import(
|
||||
'../../../../src/main/services/database/cleaner-operation-history-dao'
|
||||
)
|
||||
const dao = new CleanerOperationHistoryDAO()
|
||||
|
||||
await dao.getBatches(undefined, { limit: 10 })
|
||||
|
||||
expect(queryMock).toHaveBeenCalledTimes(1)
|
||||
const sql = queryMock.mock.calls[0][0] as string
|
||||
|
||||
expect(sql).toContain('COALESCE(SUM(CASE WHEN o.Status = \'success\' THEN 1 ELSE 0 END), 0)')
|
||||
expect(sql).toContain('COALESCE(SUM(CASE WHEN o.Status = \'failed\' THEN 1 ELSE 0 END), 0)')
|
||||
expect(sql).toContain('MAX(CASE WHEN e.IsDryRun THEN 1 ELSE 0 END) as IsDryRun')
|
||||
expect(sql).not.toContain('ISNULL(')
|
||||
})
|
||||
|
||||
it('avoids SQL Server TOP syntax when checking delete permissions', async () => {
|
||||
queryMock
|
||||
.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{
|
||||
ID: 1,
|
||||
BatchId: 'batch-1',
|
||||
AttemptNumber: 1,
|
||||
UserId: 7,
|
||||
Username: 'tester',
|
||||
OperationTime: new Date('2026-04-14T10:00:00.000Z'),
|
||||
EndTime: null,
|
||||
Status: 'success',
|
||||
IsDryRun: false,
|
||||
TotalOrders: 1,
|
||||
OrdersProcessed: 1,
|
||||
TotalMaterialsDeleted: 1,
|
||||
TotalMaterialsSkipped: 0,
|
||||
TotalMaterialsFailed: 0,
|
||||
TotalUncertainDeletions: 0,
|
||||
ErrorMessage: null,
|
||||
AppVersion: '1.12.3'
|
||||
}
|
||||
],
|
||||
columns: [],
|
||||
rowCount: 1
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 0
|
||||
})
|
||||
.mockResolvedValue({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
const { CleanerOperationHistoryDAO } = await import(
|
||||
'../../../../src/main/services/database/cleaner-operation-history-dao'
|
||||
)
|
||||
const dao = new CleanerOperationHistoryDAO()
|
||||
|
||||
const result = await dao.deleteBatch('batch-1', 7, false)
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
const executedSql = queryMock.mock.calls.map(([sql]) => sql as string).join('\n')
|
||||
expect(executedSql).not.toContain('TOP 1')
|
||||
expect(executedSql).toContain('FROM "ERPAuto"."CleanerExecution"')
|
||||
expect(executedSql).toContain('DELETE FROM "ERPAuto"."CleanerMaterialDetail"')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user