feat(cleaner-history): record missing orders with production ID tracking
Record ALL input orders in history, including resolution failures (not_found) and ERP query misses (erp_not_found). Add ProductionId column to track original 总排号 input. Add 总排号 column and new status styles to the history UI. Fix empty result caching that prevented retry on transient query failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,8 @@ import type {
|
|||||||
ExportResultItem,
|
ExportResultItem,
|
||||||
ExportResultResponse
|
ExportResultResponse
|
||||||
} from '../../types/cleaner.types'
|
} from '../../types/cleaner.types'
|
||||||
import type { InsertMaterialDetailInput } from '../../types/cleaner-history.types'
|
import type { InsertMaterialDetailInput, InsertOrderInput } from '../../types/cleaner-history.types'
|
||||||
|
import type { OrderMapping } from '../../types/order-resolver.types'
|
||||||
|
|
||||||
const log = createLogger('CleanerApplicationService')
|
const log = createLogger('CleanerApplicationService')
|
||||||
|
|
||||||
@@ -72,16 +73,16 @@ export class CleanerApplicationService {
|
|||||||
log.warn('Resolution warnings', { warnings })
|
log.warn('Resolution warnings', { warnings })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validOrderNumbers.length === 0) {
|
// Build order inputs from ALL mappings (including resolution failures)
|
||||||
throw new ValidationError(
|
const orderInputs = this.buildOrderInputs(mappings)
|
||||||
'没有有效的生产订单号可处理。请检查输入的格式或数据库连接。',
|
|
||||||
'VAL_INVALID_INPUT'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Resolved order numbers', { count: validOrderNumbers.length })
|
log.info('Resolved order numbers', {
|
||||||
|
total: mappings.length,
|
||||||
|
resolved: validOrderNumbers.length,
|
||||||
|
failed: mappings.length - validOrderNumbers.length
|
||||||
|
})
|
||||||
|
|
||||||
// Insert execution record and order records into database
|
// Insert execution record and ALL order records into database
|
||||||
const currentUser = SessionManager.getInstance().getUserInfo()
|
const currentUser = SessionManager.getInstance().getUserInfo()
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
await historyDao.insertExecution({
|
await historyDao.insertExecution({
|
||||||
@@ -90,14 +91,41 @@ export class CleanerApplicationService {
|
|||||||
userId: currentUser.id,
|
userId: currentUser.id,
|
||||||
username: currentUser.username,
|
username: currentUser.username,
|
||||||
isDryRun: input.dryRun ?? false,
|
isDryRun: input.dryRun ?? false,
|
||||||
totalOrders: validOrderNumbers.length,
|
totalOrders: orderInputs.length,
|
||||||
appVersion
|
appVersion
|
||||||
})
|
})
|
||||||
await historyDao.insertOrderRecords(
|
await historyDao.insertOrderRecords(batchId, 1, orderInputs)
|
||||||
batchId,
|
}
|
||||||
1,
|
|
||||||
validOrderNumbers.map((order) => ({ orderNumber: order }))
|
// If no valid order numbers, update execution to failed and return early
|
||||||
)
|
if (validOrderNumbers.length === 0) {
|
||||||
|
if (currentUser) {
|
||||||
|
await historyDao.updateExecutionStatus(
|
||||||
|
batchId,
|
||||||
|
1,
|
||||||
|
'failed',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
new Date(),
|
||||||
|
warnings.join('\n') || '没有有效的生产订单号可处理'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const emptyResult: CleanerResult = {
|
||||||
|
ordersProcessed: 0,
|
||||||
|
materialsDeleted: 0,
|
||||||
|
materialsSkipped: 0,
|
||||||
|
errors: [...warnings],
|
||||||
|
details: [],
|
||||||
|
retriedOrders: 0,
|
||||||
|
successfulRetries: 0,
|
||||||
|
materialsFailed: 0,
|
||||||
|
uncertainDeletions: 0
|
||||||
|
}
|
||||||
|
await this.recordCleanupAudit(0, input, emptyResult)
|
||||||
|
return emptyResult
|
||||||
}
|
}
|
||||||
|
|
||||||
authService = new ErpAuthService({
|
authService = new ErpAuthService({
|
||||||
@@ -203,14 +231,10 @@ export class CleanerApplicationService {
|
|||||||
userId: currentUser.id,
|
userId: currentUser.id,
|
||||||
username: currentUser.username,
|
username: currentUser.username,
|
||||||
isDryRun: input.dryRun ?? false,
|
isDryRun: input.dryRun ?? false,
|
||||||
totalOrders: validOrderNumbers.length,
|
totalOrders: orderInputs.length,
|
||||||
appVersion
|
appVersion
|
||||||
})
|
})
|
||||||
await historyDao.insertOrderRecords(
|
await historyDao.insertOrderRecords(batchId, 2, orderInputs)
|
||||||
batchId,
|
|
||||||
2,
|
|
||||||
validOrderNumbers.map((order) => ({ orderNumber: order }))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleaner = new CleanerService(authService)
|
cleaner = new CleanerService(authService)
|
||||||
@@ -378,6 +402,38 @@ export class CleanerApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build order inputs from ALL mappings, including resolution failures.
|
||||||
|
* Deduplicates by orderNumber for resolved mappings, includes all failed mappings.
|
||||||
|
*/
|
||||||
|
private buildOrderInputs(mappings: OrderMapping[]): InsertOrderInput[] {
|
||||||
|
const inputs: InsertOrderInput[] = []
|
||||||
|
const seenOrderNumbers = new Set<string>()
|
||||||
|
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (mapping.resolved && mapping.orderNumber) {
|
||||||
|
// Deduplicate resolved mappings by order number
|
||||||
|
if (!seenOrderNumbers.has(mapping.orderNumber)) {
|
||||||
|
seenOrderNumbers.add(mapping.orderNumber)
|
||||||
|
inputs.push({
|
||||||
|
orderNumber: mapping.orderNumber,
|
||||||
|
productionId: mapping.productionId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Resolution failure: use original input as orderNumber identifier
|
||||||
|
inputs.push({
|
||||||
|
orderNumber: mapping.input,
|
||||||
|
productionId: mapping.productionId,
|
||||||
|
initialStatus: 'not_found',
|
||||||
|
errorMessage: mapping.error || '未在数据库中找到对应的订单号'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
private async recordCleanupAudit(
|
private async recordCleanupAudit(
|
||||||
orderCount: number,
|
orderCount: number,
|
||||||
input: CleanerInput,
|
input: CleanerInput,
|
||||||
@@ -428,7 +484,11 @@ export class CleanerApplicationService {
|
|||||||
batchId,
|
batchId,
|
||||||
attemptNumber,
|
attemptNumber,
|
||||||
detail.orderNumber,
|
detail.orderNumber,
|
||||||
detail.errors.length > 0 ? 'failed' : 'success',
|
detail.notFound
|
||||||
|
? 'erp_not_found'
|
||||||
|
: detail.errors.length > 0
|
||||||
|
? 'failed'
|
||||||
|
: 'success',
|
||||||
detail.materialsDeleted,
|
detail.materialsDeleted,
|
||||||
detail.materialsSkipped,
|
detail.materialsSkipped,
|
||||||
detail.materialsFailed,
|
detail.materialsFailed,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const CLEANER_ORDER_CONFIG = {
|
|||||||
BATCH_ID: 'BatchId',
|
BATCH_ID: 'BatchId',
|
||||||
ATTEMPT_NUMBER: 'AttemptNumber',
|
ATTEMPT_NUMBER: 'AttemptNumber',
|
||||||
ORDER_NUMBER: 'OrderNumber',
|
ORDER_NUMBER: 'OrderNumber',
|
||||||
|
PRODUCTION_ID: 'ProductionId',
|
||||||
STATUS: 'Status',
|
STATUS: 'Status',
|
||||||
MATERIALS_DELETED: 'MaterialsDeleted',
|
MATERIALS_DELETED: 'MaterialsDeleted',
|
||||||
MATERIALS_SKIPPED: 'MaterialsSkipped',
|
MATERIALS_SKIPPED: 'MaterialsSkipped',
|
||||||
@@ -324,18 +325,26 @@ export class CleanerOperationHistoryDAO {
|
|||||||
|
|
||||||
for (const order of orders) {
|
for (const order of orders) {
|
||||||
try {
|
try {
|
||||||
|
const status = order.initialStatus || 'pending'
|
||||||
const sqlString = `
|
const sqlString = `
|
||||||
INSERT INTO ${tableName}
|
INSERT INTO ${tableName}
|
||||||
(BatchId, AttemptNumber, OrderNumber, Status,
|
(BatchId, AttemptNumber, OrderNumber, ProductionId, Status,
|
||||||
MaterialsDeleted, MaterialsSkipped, MaterialsFailed,
|
MaterialsDeleted, MaterialsSkipped, MaterialsFailed,
|
||||||
UncertainDeletions, RetryCount, RetrySuccess, ErrorMessage)
|
UncertainDeletions, RetryCount, RetrySuccess, ErrorMessage)
|
||||||
VALUES
|
VALUES
|
||||||
(${dialect.param(0)}, ${dialect.param(1)}, ${dialect.param(2)}, 'pending',
|
(${dialect.param(0)}, ${dialect.param(1)}, ${dialect.param(2)}, ${dialect.param(3)}, ${dialect.param(4)},
|
||||||
0, 0, 0, 0, 0, 0, NULL)
|
0, 0, 0, 0, 0, 0, ${dialect.param(5)})
|
||||||
`
|
`
|
||||||
await trackDuration(
|
await trackDuration(
|
||||||
async () =>
|
async () =>
|
||||||
await dbService.query(sqlString, [batchId, attemptNumber, order.orderNumber]),
|
await dbService.query(sqlString, [
|
||||||
|
batchId,
|
||||||
|
attemptNumber,
|
||||||
|
order.orderNumber,
|
||||||
|
order.productionId || null,
|
||||||
|
status,
|
||||||
|
order.errorMessage || null
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
operationName: 'CleanerOperationHistoryDAO.insertOrderRecords',
|
operationName: 'CleanerOperationHistoryDAO.insertOrderRecords',
|
||||||
context: { tableName, operationType: 'INSERT', batchId, attemptNumber }
|
context: { tableName, operationType: 'INSERT', batchId, attemptNumber }
|
||||||
@@ -730,7 +739,7 @@ export class CleanerOperationHistoryDAO {
|
|||||||
// Query orders
|
// Query orders
|
||||||
const orderSql = `
|
const orderSql = `
|
||||||
SELECT
|
SELECT
|
||||||
ID, BatchId, AttemptNumber, OrderNumber, Status,
|
ID, BatchId, AttemptNumber, OrderNumber, ProductionId, Status,
|
||||||
MaterialsDeleted, MaterialsSkipped, MaterialsFailed,
|
MaterialsDeleted, MaterialsSkipped, MaterialsFailed,
|
||||||
UncertainDeletions, RetryCount, RetrySuccess, ErrorMessage
|
UncertainDeletions, RetryCount, RetrySuccess, ErrorMessage
|
||||||
FROM ${orderTable}
|
FROM ${orderTable}
|
||||||
@@ -778,6 +787,7 @@ export class CleanerOperationHistoryDAO {
|
|||||||
batchId: row.BatchId as string,
|
batchId: row.BatchId as string,
|
||||||
attemptNumber: row.AttemptNumber as number,
|
attemptNumber: row.AttemptNumber as number,
|
||||||
orderNumber: row.OrderNumber as string,
|
orderNumber: row.OrderNumber as string,
|
||||||
|
productionId: (row.ProductionId as string) || null,
|
||||||
status: row.Status as string,
|
status: row.Status as string,
|
||||||
materialsDeleted: row.MaterialsDeleted as number,
|
materialsDeleted: row.MaterialsDeleted as number,
|
||||||
materialsSkipped: row.MaterialsSkipped as number,
|
materialsSkipped: row.MaterialsSkipped as number,
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ export class CleanerService {
|
|||||||
for (const missingOrder of missingOrders) {
|
for (const missingOrder of missingOrders) {
|
||||||
const missingMessage = '订单未出现在查询结果中'
|
const missingMessage = '订单未出现在查询结果中'
|
||||||
result.errors.push(`Order ${missingOrder}: ${missingMessage}`)
|
result.errors.push(`Order ${missingOrder}: ${missingMessage}`)
|
||||||
result.details.push(this.createErrorDetail(missingOrder, missingMessage))
|
result.details.push(this.createErrorDetail(missingOrder, missingMessage, true))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1115,7 +1115,7 @@ export class CleanerService {
|
|||||||
return /^SC\d{14}$/.test(value)
|
return /^SC\d{14}$/.test(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private createErrorDetail(orderNumber: string, message: string): OrderCleanDetail {
|
private createErrorDetail(orderNumber: string, message: string, notFound: boolean = false): OrderCleanDetail {
|
||||||
return {
|
return {
|
||||||
orderNumber,
|
orderNumber,
|
||||||
materialsDeleted: 0,
|
materialsDeleted: 0,
|
||||||
@@ -1129,7 +1129,8 @@ export class CleanerService {
|
|||||||
retrySuccess: false,
|
retrySuccess: false,
|
||||||
materialsFailed: 0,
|
materialsFailed: 0,
|
||||||
failedMaterials: [],
|
failedMaterials: [],
|
||||||
uncertainDeletions: 0
|
uncertainDeletions: 0,
|
||||||
|
notFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Migration Script: Add ProductionId column to CleanerOrderHistory table
|
||||||
|
*
|
||||||
|
* This script adds the ProductionId column to the ERPAuto.CleanerOrderHistory
|
||||||
|
* table for tracking the original production ID (总排号) input.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx tsx src/main/services/user/migration/add-productionid-to-cleaner-order.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ConfigManager } from '../../config/config-manager'
|
||||||
|
import { MySqlService } from '../../database/mysql'
|
||||||
|
import { SqlServerService } from '../../database/sql-server'
|
||||||
|
import { PostgreSqlService } from '../../database/postgresql'
|
||||||
|
import { createLogger } from '../../logger'
|
||||||
|
|
||||||
|
const log = createLogger('Migration')
|
||||||
|
|
||||||
|
async function runMySQLMigration(): Promise<void> {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
await configManager.initialize()
|
||||||
|
const dbConfig = configManager.getConfig().database.mysql
|
||||||
|
|
||||||
|
const service = new MySqlService({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.connect()
|
||||||
|
console.log('Connected to MySQL')
|
||||||
|
|
||||||
|
// Check if column already exists
|
||||||
|
const check = await service.query(
|
||||||
|
`SELECT COUNT(*) as cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'CleanerOrderHistory' AND COLUMN_NAME = 'ProductionId'`
|
||||||
|
)
|
||||||
|
if ((check.rows[0]?.cnt as number) > 0) {
|
||||||
|
console.log('Column ProductionId already exists, skipping.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.query(
|
||||||
|
`ALTER TABLE CleanerOrderHistory ADD COLUMN ProductionId VARCHAR(50) NULL`
|
||||||
|
)
|
||||||
|
console.log('Added ProductionId column to CleanerOrderHistory.')
|
||||||
|
} finally {
|
||||||
|
if (service.isConnected()) await service.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSqlServerMigration(): Promise<void> {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
await configManager.initialize()
|
||||||
|
const dbConfig = configManager.getConfig().database.sqlserver
|
||||||
|
|
||||||
|
const service = new SqlServerService({
|
||||||
|
server: dbConfig.server,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database,
|
||||||
|
options: { trustServerCertificate: dbConfig.trustServerCertificate }
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.connect()
|
||||||
|
console.log('Connected to SQL Server')
|
||||||
|
|
||||||
|
// Check if column already exists
|
||||||
|
const check = await service.query(
|
||||||
|
`SELECT COUNT(*) as cnt FROM sys.columns WHERE OBJECT_ID = OBJECT_ID('ERPAuto.CleanerOrderHistory') AND name = 'ProductionId'`
|
||||||
|
)
|
||||||
|
if ((check.rows[0]?.cnt as number) > 0) {
|
||||||
|
console.log('Column ProductionId already exists, skipping.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.query(
|
||||||
|
`ALTER TABLE [ERPAuto].[CleanerOrderHistory] ADD ProductionId NVARCHAR(50) NULL`
|
||||||
|
)
|
||||||
|
console.log('Added ProductionId column to CleanerOrderHistory.')
|
||||||
|
} finally {
|
||||||
|
if (service.isConnected()) await service.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPostgreSqlMigration(): Promise<void> {
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
await configManager.initialize()
|
||||||
|
const dbConfig = configManager.getConfig().database.postgresql
|
||||||
|
|
||||||
|
const service = new PostgreSqlService({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await service.connect()
|
||||||
|
console.log('Connected to PostgreSQL')
|
||||||
|
|
||||||
|
await service.query(
|
||||||
|
`ALTER TABLE "ERPAuto"."CleanerOrderHistory" ADD COLUMN IF NOT EXISTS "ProductionId" VARCHAR(50) NULL`
|
||||||
|
)
|
||||||
|
console.log('Added ProductionId column to CleanerOrderHistory.')
|
||||||
|
} finally {
|
||||||
|
if (service.isConnected()) await service.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
console.log('Migration: Add ProductionId to CleanerOrderHistory')
|
||||||
|
|
||||||
|
const configManager = ConfigManager.getInstance()
|
||||||
|
await configManager.initialize()
|
||||||
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
|
console.log(`Database type: ${dbType}`)
|
||||||
|
|
||||||
|
if (dbType === 'mysql') {
|
||||||
|
await runMySQLMigration()
|
||||||
|
} else if (dbType === 'sqlserver') {
|
||||||
|
await runSqlServerMigration()
|
||||||
|
} else if (dbType === 'postgresql') {
|
||||||
|
await runPostgreSqlMigration()
|
||||||
|
} else {
|
||||||
|
console.error(`Unsupported database type: ${dbType}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done.')
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Migration failed:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -31,6 +31,7 @@ export interface CleanerOrderRecord {
|
|||||||
batchId: string
|
batchId: string
|
||||||
attemptNumber: number
|
attemptNumber: number
|
||||||
orderNumber: string
|
orderNumber: string
|
||||||
|
productionId: string | null
|
||||||
status: string
|
status: string
|
||||||
materialsDeleted: number
|
materialsDeleted: number
|
||||||
materialsSkipped: number
|
materialsSkipped: number
|
||||||
@@ -87,6 +88,9 @@ export interface InsertCleanerExecutionInput {
|
|||||||
/** 插入订单记录的输入 */
|
/** 插入订单记录的输入 */
|
||||||
export interface InsertOrderInput {
|
export interface InsertOrderInput {
|
||||||
orderNumber: string
|
orderNumber: string
|
||||||
|
productionId?: string
|
||||||
|
initialStatus?: string
|
||||||
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 插入物料明细的输入 */
|
/** 插入物料明细的输入 */
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export interface OrderCleanDetail {
|
|||||||
materialsFailed: number
|
materialsFailed: number
|
||||||
failedMaterials: FailedMaterial[]
|
failedMaterials: FailedMaterial[]
|
||||||
uncertainDeletions: number
|
uncertainDeletions: number
|
||||||
|
// Missing order flag: true when order not found in ERP query results
|
||||||
|
notFound?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DeletionOutcome {
|
export enum DeletionOutcome {
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ const statusStyles: Record<string, string> = {
|
|||||||
partial: 'bg-amber-100 text-amber-700',
|
partial: 'bg-amber-100 text-amber-700',
|
||||||
failed: 'bg-red-100 text-red-700',
|
failed: 'bg-red-100 text-red-700',
|
||||||
crashed: 'bg-red-100 text-red-700',
|
crashed: 'bg-red-100 text-red-700',
|
||||||
pending: 'bg-gray-100 text-gray-700'
|
pending: 'bg-gray-100 text-gray-700',
|
||||||
|
not_found: 'bg-orange-100 text-orange-700',
|
||||||
|
erp_not_found: 'bg-orange-100 text-orange-700'
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
@@ -75,7 +77,9 @@ const statusLabels: Record<string, string> = {
|
|||||||
partial: '部分成功',
|
partial: '部分成功',
|
||||||
failed: '失败',
|
failed: '失败',
|
||||||
crashed: '崩溃',
|
crashed: '崩溃',
|
||||||
pending: '进行中'
|
pending: '进行中',
|
||||||
|
not_found: '未找到',
|
||||||
|
erp_not_found: 'ERP不存在'
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusIcons: Record<string, React.ReactNode> = {
|
const statusIcons: Record<string, React.ReactNode> = {
|
||||||
@@ -83,7 +87,9 @@ const statusIcons: Record<string, React.ReactNode> = {
|
|||||||
partial: <Clock size={16} className="text-amber-600" />,
|
partial: <Clock size={16} className="text-amber-600" />,
|
||||||
failed: <XCircle size={16} className="text-red-600" />,
|
failed: <XCircle size={16} className="text-red-600" />,
|
||||||
crashed: <XCircle size={16} className="text-red-600" />,
|
crashed: <XCircle size={16} className="text-red-600" />,
|
||||||
pending: <Clock size={16} className="text-gray-500" />
|
pending: <Clock size={16} className="text-gray-500" />,
|
||||||
|
not_found: <XCircle size={16} className="text-orange-600" />,
|
||||||
|
erp_not_found: <XCircle size={16} className="text-orange-600" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDateTime = (dateStr: string | Date | null | undefined): string => {
|
const formatDateTime = (dateStr: string | Date | null | undefined): string => {
|
||||||
@@ -429,6 +435,9 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600 w-8" />
|
<th className="px-4 py-2 text-left font-medium text-gray-600 w-8" />
|
||||||
|
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
||||||
|
总排号
|
||||||
|
</th>
|
||||||
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
<th className="px-4 py-2 text-left font-medium text-gray-600">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
订单号
|
订单号
|
||||||
@@ -493,7 +502,10 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-gray-900 font-mono text-xs">
|
<td className="px-4 py-2 text-gray-900 font-mono text-xs">
|
||||||
{order.orderNumber}
|
{order.productionId || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-gray-900 font-mono text-xs">
|
||||||
|
{order.status === 'not_found' ? '-' : order.orderNumber}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<span
|
<span
|
||||||
@@ -549,7 +561,7 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
{/* Material details */}
|
{/* Material details */}
|
||||||
{isOrderExpanded && (
|
{isOrderExpanded && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9} className="bg-gray-50/50 px-8 py-3">
|
<td colSpan={10} className="bg-gray-50/50 px-8 py-3">
|
||||||
{isLoadingMaterials ? (
|
{isLoadingMaterials ? (
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
加载物料详情...
|
加载物料详情...
|
||||||
@@ -709,6 +721,8 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
}
|
}
|
||||||
}, [logger])
|
}, [logger])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
void fetchBatches()
|
void fetchBatches()
|
||||||
@@ -826,6 +840,7 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="pt-4 border-t border-gray-200 flex justify-end">
|
<div className="pt-4 border-t border-gray-200 flex justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export interface CleanerHistoryOrderRecord {
|
|||||||
batchId: string
|
batchId: string
|
||||||
attemptNumber: number
|
attemptNumber: number
|
||||||
orderNumber: string
|
orderNumber: string
|
||||||
|
productionId: string | null
|
||||||
status: string
|
status: string
|
||||||
materialsDeleted: number
|
materialsDeleted: number
|
||||||
materialsSkipped: number
|
materialsSkipped: number
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ describe('CleanerApplicationService', () => {
|
|||||||
).rejects.toBeInstanceOf(DatabaseQueryError)
|
).rejects.toBeInstanceOf(DatabaseQueryError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject with ValidationError when no valid order numbers are provided', async () => {
|
it('should return empty result when no valid order numbers are provided', async () => {
|
||||||
;(service as any).getErpConfig = vi
|
;(service as any).getErpConfig = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ url: 'u', username: 'x', password: 'p' })
|
.mockResolvedValue({ url: 'u', username: 'x', password: 'p' })
|
||||||
@@ -250,9 +250,13 @@ describe('CleanerApplicationService', () => {
|
|||||||
disconnect: vi.fn().mockResolvedValue(undefined)
|
disconnect: vi.fn().mockResolvedValue(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
const result = await service.runCleaner(
|
||||||
service.runCleaner({ send: vi.fn() } as any, makeInput({ orderNumbers: [] }))
|
{ send: vi.fn() } as any,
|
||||||
).rejects.toThrow('没有有效的生产订单号可处理')
|
makeInput({ orderNumbers: [] })
|
||||||
|
)
|
||||||
|
expect(result.ordersProcessed).toBe(0)
|
||||||
|
expect(result.materialsDeleted).toBe(0)
|
||||||
|
expect(result.details).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should process orders containing empty strings without crashing', async () => {
|
it('should process orders containing empty strings without crashing', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user