feat: support productionID input for order number resolution
- Add OrderNumberResolver service to auto-recognize productionID and 生产订单号 formats - Integrate MySQL database lookup for productionID to 生产订单号 conversion - Update OrderNumberInput component with format statistics display - Modify Extractor and Cleaner to resolve order numbers before processing Database configuration: - Table: productionContractData_26 年压力表合同数据 - Fields: 总排号 (productionID), 生产订单号 (production order number) Supported formats: - productionID: 2 digits + 1 letter + serial number (e.g., 26B742) - 生产订单号:SC + 14 digits (e.g., SC70202601040109)
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||||
import { CleanerService } from '../services/erp/cleaner'
|
import { CleanerService } from '../services/erp/cleaner'
|
||||||
|
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||||
|
import { MySqlService } from '../services/database/mysql'
|
||||||
import type { CleanerInput, CleanerResult } from '../types/cleaner.types'
|
import type { CleanerInput, CleanerResult } from '../types/cleaner.types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +16,7 @@ export function registerCleanerHandlers(): void {
|
|||||||
input: CleanerInput
|
input: CleanerInput
|
||||||
): Promise<{ success: boolean; data?: CleanerResult; error?: string }> => {
|
): Promise<{ success: boolean; data?: CleanerResult; error?: string }> => {
|
||||||
let authService: ErpAuthService | null = null
|
let authService: ErpAuthService | null = null
|
||||||
|
let mysqlService: MySqlService | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check environment variables
|
// Check environment variables
|
||||||
@@ -32,6 +35,36 @@ export function registerCleanerHandlers(): void {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve order numbers (convert productionIDs to 生产订单号)
|
||||||
|
const mysqlConfig = {
|
||||||
|
host: process.env.DB_MYSQL_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
||||||
|
user: process.env.DB_USERNAME || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Cleaner] Connecting to MySQL for order resolution...')
|
||||||
|
mysqlService = new MySqlService(mysqlConfig)
|
||||||
|
await mysqlService.connect()
|
||||||
|
|
||||||
|
const resolver = new OrderNumberResolver(mysqlService)
|
||||||
|
const mappings = await resolver.resolve(input.orderNumbers)
|
||||||
|
|
||||||
|
// Get valid order numbers and warnings
|
||||||
|
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
|
||||||
|
const warnings = resolver.getWarnings(mappings)
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
console.warn('[Cleaner] Resolution warnings:', warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validOrderNumbers.length === 0) {
|
||||||
|
throw new Error('没有有效的生产订单号可处理。请检查输入的格式或数据库连接。')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Cleaner] Resolved order numbers:', validOrderNumbers)
|
||||||
|
|
||||||
// Create auth service and login
|
// Create auth service and login
|
||||||
authService = new ErpAuthService({
|
authService = new ErpAuthService({
|
||||||
url: erpUrl,
|
url: erpUrl,
|
||||||
@@ -44,12 +77,24 @@ export function registerCleanerHandlers(): void {
|
|||||||
await authService.login()
|
await authService.login()
|
||||||
console.log('[Cleaner] Login successful')
|
console.log('[Cleaner] Login successful')
|
||||||
|
|
||||||
// Create cleaner service and run cleaning
|
// Create cleaner service and run cleaning with resolved order numbers
|
||||||
const cleaner = new CleanerService(authService)
|
const cleaner = new CleanerService(authService)
|
||||||
console.log('[Cleaner] Starting cleaning:', input)
|
|
||||||
const result = await cleaner.clean(input)
|
const modifiedInput: CleanerInput = {
|
||||||
|
...input,
|
||||||
|
orderNumbers: validOrderNumbers,
|
||||||
|
onProgress: input.onProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Cleaner] Starting cleaning:', modifiedInput)
|
||||||
|
const result = await cleaner.clean(modifiedInput)
|
||||||
console.log('[Cleaner] Cleaning completed:', result)
|
console.log('[Cleaner] Cleaning completed:', result)
|
||||||
|
|
||||||
|
// Add warnings to result errors if any
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
result.errors = [...warnings, ...result.errors]
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, data: result }
|
return { success: true, data: result }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
@@ -66,6 +111,16 @@ export function registerCleanerHandlers(): void {
|
|||||||
console.warn('[Cleaner] Error closing browser:', closeError)
|
console.warn('[Cleaner] Error closing browser:', closeError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up: disconnect MySQL
|
||||||
|
if (mysqlService) {
|
||||||
|
try {
|
||||||
|
await mysqlService.disconnect()
|
||||||
|
console.log('[Cleaner] MySQL disconnected')
|
||||||
|
} catch (closeError) {
|
||||||
|
console.warn('[Cleaner] Error disconnecting MySQL:', closeError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||||
import { ExtractorService } from '../services/erp/extractor'
|
import { ExtractorService } from '../services/erp/extractor'
|
||||||
|
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||||
|
import { MySqlService } from '../services/database/mysql'
|
||||||
import type { ExtractorInput, ExtractorResult } from '../types/extractor.types'
|
import type { ExtractorInput, ExtractorResult } from '../types/extractor.types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +16,7 @@ export function registerExtractorHandlers(): void {
|
|||||||
input: ExtractorInput
|
input: ExtractorInput
|
||||||
): Promise<{ success: boolean; data?: ExtractorResult; error?: string }> => {
|
): Promise<{ success: boolean; data?: ExtractorResult; error?: string }> => {
|
||||||
let authService: ErpAuthService | null = null
|
let authService: ErpAuthService | null = null
|
||||||
|
let mysqlService: MySqlService | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check environment variables
|
// Check environment variables
|
||||||
@@ -32,6 +35,36 @@ export function registerExtractorHandlers(): void {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve order numbers (convert productionIDs to 生产订单号)
|
||||||
|
const mysqlConfig = {
|
||||||
|
host: process.env.DB_MYSQL_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
||||||
|
user: process.env.DB_USERNAME || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Extractor] Connecting to MySQL for order resolution...')
|
||||||
|
mysqlService = new MySqlService(mysqlConfig)
|
||||||
|
await mysqlService.connect()
|
||||||
|
|
||||||
|
const resolver = new OrderNumberResolver(mysqlService)
|
||||||
|
const mappings = await resolver.resolve(input.orderNumbers)
|
||||||
|
|
||||||
|
// Get valid order numbers and warnings
|
||||||
|
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
|
||||||
|
const warnings = resolver.getWarnings(mappings)
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
console.warn('[Extractor] Resolution warnings:', warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validOrderNumbers.length === 0) {
|
||||||
|
throw new Error('没有有效的生产订单号可处理。请检查输入的格式或数据库连接。')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Extractor] Resolved order numbers:', validOrderNumbers)
|
||||||
|
|
||||||
// Create auth service and login
|
// Create auth service and login
|
||||||
authService = new ErpAuthService({
|
authService = new ErpAuthService({
|
||||||
url: erpUrl,
|
url: erpUrl,
|
||||||
@@ -44,10 +77,22 @@ export function registerExtractorHandlers(): void {
|
|||||||
await authService.login()
|
await authService.login()
|
||||||
console.log('[Extractor] Login successful')
|
console.log('[Extractor] Login successful')
|
||||||
|
|
||||||
// Create extractor service and run extraction
|
// Create extractor service and run extraction with resolved order numbers
|
||||||
const extractor = new ExtractorService(authService)
|
const extractor = new ExtractorService(authService)
|
||||||
console.log('[Extractor] Starting extraction for orders:', input.orderNumbers)
|
console.log('[Extractor] Starting extraction for orders:', validOrderNumbers)
|
||||||
const result = await extractor.extract(input)
|
|
||||||
|
const modifiedInput: ExtractorInput = {
|
||||||
|
...input,
|
||||||
|
orderNumbers: validOrderNumbers
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await extractor.extract(modifiedInput)
|
||||||
|
|
||||||
|
// Add warnings to result errors if any
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
result.errors = [...warnings, ...result.errors]
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[Extractor] Extraction completed:', result)
|
console.log('[Extractor] Extraction completed:', result)
|
||||||
|
|
||||||
return { success: true, data: result }
|
return { success: true, data: result }
|
||||||
@@ -66,6 +111,16 @@ export function registerExtractorHandlers(): void {
|
|||||||
console.warn('[Extractor] Error closing browser:', closeError)
|
console.warn('[Extractor] Error closing browser:', closeError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up: disconnect MySQL
|
||||||
|
if (mysqlService) {
|
||||||
|
try {
|
||||||
|
await mysqlService.disconnect()
|
||||||
|
console.log('[Extractor] MySQL disconnected')
|
||||||
|
} catch (closeError) {
|
||||||
|
console.warn('[Extractor] Error disconnecting MySQL:', closeError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { registerFileHandlers } from './file-handler'
|
|||||||
import { registerExtractorHandlers } from './extractor-handler'
|
import { registerExtractorHandlers } from './extractor-handler'
|
||||||
import { registerCleanerHandlers } from './cleaner-handler'
|
import { registerCleanerHandlers } from './cleaner-handler'
|
||||||
import { registerDatabaseHandlers } from './database-handler'
|
import { registerDatabaseHandlers } from './database-handler'
|
||||||
|
import { registerResolverHandlers } from './resolver-handler'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all IPC handlers
|
* Register all IPC handlers
|
||||||
@@ -16,4 +17,5 @@ export function registerIpcHandlers(): void {
|
|||||||
registerExtractorHandlers()
|
registerExtractorHandlers()
|
||||||
registerCleanerHandlers()
|
registerCleanerHandlers()
|
||||||
registerDatabaseHandlers()
|
registerDatabaseHandlers()
|
||||||
|
registerResolverHandlers()
|
||||||
}
|
}
|
||||||
|
|||||||
140
src/main/ipc/resolver-handler.ts
Normal file
140
src/main/ipc/resolver-handler.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* IPC handlers for Order Number Resolver
|
||||||
|
*
|
||||||
|
* Provides APIs for the renderer process to:
|
||||||
|
* - Resolve productionIDs and 生产订单号 to production order numbers
|
||||||
|
* - Validate input formats
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ipcMain } from 'electron'
|
||||||
|
import { MySqlService } from '../services/database/mysql'
|
||||||
|
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||||
|
import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolver input from renderer
|
||||||
|
*/
|
||||||
|
export interface ResolverInput {
|
||||||
|
/** List of order numbers/productionIDs to resolve */
|
||||||
|
inputs: string[]
|
||||||
|
/** MySQL configuration (optional, uses default if not provided) */
|
||||||
|
mysqlConfig?: {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
user: string
|
||||||
|
password: string
|
||||||
|
database: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolver response to renderer
|
||||||
|
*/
|
||||||
|
export interface ResolverResponse {
|
||||||
|
/** Whether the resolution was successful */
|
||||||
|
success: boolean
|
||||||
|
/** Resolved order mappings */
|
||||||
|
mappings?: OrderMapping[]
|
||||||
|
/** Valid production order numbers ready for use */
|
||||||
|
validOrderNumbers?: string[]
|
||||||
|
/** Warning messages for invalid inputs */
|
||||||
|
warnings?: string[]
|
||||||
|
/** Resolution statistics */
|
||||||
|
stats?: ResolutionStats
|
||||||
|
/** Error message if failed */
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register IPC handlers for order number resolver
|
||||||
|
*/
|
||||||
|
export function registerResolverHandlers(): void {
|
||||||
|
/**
|
||||||
|
* Resolve order numbers
|
||||||
|
* Converts productionIDs and 生产订单号 to production order numbers
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'resolver:resolve',
|
||||||
|
async (_event, input: ResolverInput): Promise<ResolverResponse> => {
|
||||||
|
let mysqlService: MySqlService | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use provided config or environment variables
|
||||||
|
const mysqlConfig = input.mysqlConfig || {
|
||||||
|
host: process.env.DB_MYSQL_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
|
||||||
|
user: process.env.DB_USERNAME || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_NAME || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MySQL service
|
||||||
|
mysqlService = new MySqlService(mysqlConfig)
|
||||||
|
await mysqlService.connect()
|
||||||
|
|
||||||
|
// Create resolver and resolve inputs
|
||||||
|
const resolver = new OrderNumberResolver(mysqlService)
|
||||||
|
const mappings = await resolver.resolve(input.inputs)
|
||||||
|
|
||||||
|
// Get valid order numbers and warnings
|
||||||
|
const validOrderNumbers = resolver.getValidOrderNumbers(mappings)
|
||||||
|
const warnings = resolver.getWarnings(mappings)
|
||||||
|
const stats = resolver.getStats(mappings)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
mappings,
|
||||||
|
validOrderNumbers,
|
||||||
|
warnings,
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `解析失败:${message}`
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up MySQL connection
|
||||||
|
if (mysqlService) {
|
||||||
|
try {
|
||||||
|
await mysqlService.disconnect()
|
||||||
|
} catch (closeError) {
|
||||||
|
console.warn('[Resolver] Error disconnecting MySQL:', closeError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input format only (without database lookup)
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'resolver:validateFormat',
|
||||||
|
async (_event, inputs: string[]): Promise<{
|
||||||
|
success: boolean
|
||||||
|
results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>
|
||||||
|
error?: string
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const resolver = new OrderNumberResolver({
|
||||||
|
isConnected: () => false
|
||||||
|
} as MySqlService)
|
||||||
|
|
||||||
|
const results = inputs.map(input => ({
|
||||||
|
input,
|
||||||
|
type: resolver.recognizeType(input)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { success: true, results }
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `验证失败:${message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
363
src/main/services/erp/order-resolver.ts
Normal file
363
src/main/services/erp/order-resolver.ts
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
/**
|
||||||
|
* Order Number Resolver Service
|
||||||
|
*
|
||||||
|
* Automatically recognizes productionID and 生产订单号 (production order number),
|
||||||
|
* and converts them via MySQL database lookup.
|
||||||
|
*
|
||||||
|
* - productionID format: 2 digits + 1 letter + serial number (e.g., "22A1", "22A1234")
|
||||||
|
* - 生产订单号 format: SC + 14 digits (e.g., "SC70202602120085")
|
||||||
|
*
|
||||||
|
* Database table: productionContractData_26年压力表合同数据
|
||||||
|
* Fields: 总排号 (productionID), 生产订单号 (production order number)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MySqlService } from '../database/mysql'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order mapping result
|
||||||
|
*/
|
||||||
|
export interface OrderMapping {
|
||||||
|
/** Original input from user */
|
||||||
|
input: string
|
||||||
|
/** Recognized productionID (if input matches productionID pattern) */
|
||||||
|
productionId?: string
|
||||||
|
/** Final production order number to use */
|
||||||
|
orderNumber?: string
|
||||||
|
/** Whether this mapping is valid */
|
||||||
|
isValid: boolean
|
||||||
|
/** Error or warning message */
|
||||||
|
error?: string
|
||||||
|
/** Input type */
|
||||||
|
inputType: 'productionId' | 'orderNumber' | 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolution statistics
|
||||||
|
*/
|
||||||
|
export interface ResolutionStats {
|
||||||
|
totalInputs: number
|
||||||
|
recognizedAsProductionId: number
|
||||||
|
recognizedAsOrderNumber: number
|
||||||
|
unknownFormat: number
|
||||||
|
successfullyResolved: number
|
||||||
|
failedToResolve: number
|
||||||
|
notFoundInDatabase: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expression patterns
|
||||||
|
*/
|
||||||
|
export const ORDER_PATTERNS = {
|
||||||
|
/** productionID: 2 digits + 1 letter + serial number (1+) */
|
||||||
|
PRODUCTION_ID: /^\d{2}[A-Za-z]\d+$/,
|
||||||
|
/** 生产订单号:SC + 14 digits */
|
||||||
|
ORDER_NUMBER: /^SC\d{14}$/
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database table and field names
|
||||||
|
* Can be overridden via environment variables:
|
||||||
|
* - DB_TABLE_NAME: Table name (default: 'productionContractData_26年压力表合同数据')
|
||||||
|
* - DB_FIELD_PRODUCTION_ID: Field name for productionID (default: '总排号')
|
||||||
|
* - DB_FIELD_ORDER_NUMBER: Field name for order number (default: '生产订单号')
|
||||||
|
*/
|
||||||
|
export const DB_CONFIG = {
|
||||||
|
TABLE_NAME: process.env.DB_TABLE_NAME || 'productionContractData_26年压力表合同数据',
|
||||||
|
FIELD_PRODUCTION_ID: process.env.DB_FIELD_PRODUCTION_ID || '总排号',
|
||||||
|
FIELD_ORDER_NUMBER: process.env.DB_FIELD_ORDER_NUMBER || '生产订单号'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Number Resolver Service
|
||||||
|
*/
|
||||||
|
export class OrderNumberResolver {
|
||||||
|
private mysqlService: MySqlService
|
||||||
|
|
||||||
|
constructor(mysqlService: MySqlService) {
|
||||||
|
this.mysqlService = mysqlService
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognize the type of an input string
|
||||||
|
* @param input - The input string to recognize
|
||||||
|
* @returns The recognized type
|
||||||
|
*/
|
||||||
|
recognizeType(input: string): 'productionId' | 'orderNumber' | 'unknown' {
|
||||||
|
const trimmed = input.trim()
|
||||||
|
|
||||||
|
if (ORDER_PATTERNS.ORDER_NUMBER.test(trimmed)) {
|
||||||
|
return 'orderNumber'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ORDER_PATTERNS.PRODUCTION_ID.test(trimmed)) {
|
||||||
|
return 'productionId'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a list of inputs to production order numbers
|
||||||
|
* @param inputs - List of input strings (can be productionID or 生产订单号)
|
||||||
|
* @returns List of order mappings
|
||||||
|
*/
|
||||||
|
async resolve(inputs: string[]): Promise<OrderMapping[]> {
|
||||||
|
const mappings: OrderMapping[] = []
|
||||||
|
const stats: ResolutionStats = {
|
||||||
|
totalInputs: inputs.length,
|
||||||
|
recognizedAsProductionId: 0,
|
||||||
|
recognizedAsOrderNumber: 0,
|
||||||
|
unknownFormat: 0,
|
||||||
|
successfullyResolved: 0,
|
||||||
|
failedToResolve: 0,
|
||||||
|
notFoundInDatabase: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// First pass: recognize types and categorize
|
||||||
|
const productionIds: string[] = []
|
||||||
|
const orderNumbers: string[] = []
|
||||||
|
|
||||||
|
for (const input of inputs) {
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (!trimmed) continue
|
||||||
|
|
||||||
|
const type = this.recognizeType(trimmed)
|
||||||
|
|
||||||
|
const baseMapping: OrderMapping = {
|
||||||
|
input: trimmed,
|
||||||
|
isValid: false,
|
||||||
|
inputType: type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'productionId') {
|
||||||
|
stats.recognizedAsProductionId++
|
||||||
|
productionIds.push(trimmed)
|
||||||
|
baseMapping.productionId = trimmed
|
||||||
|
} else if (type === 'orderNumber') {
|
||||||
|
stats.recognizedAsOrderNumber++
|
||||||
|
orderNumbers.push(trimmed)
|
||||||
|
baseMapping.orderNumber = trimmed
|
||||||
|
baseMapping.isValid = true // Order numbers are valid by format
|
||||||
|
stats.successfullyResolved++
|
||||||
|
} else {
|
||||||
|
stats.unknownFormat++
|
||||||
|
baseMapping.error = `无法识别的格式:${trimmed}`
|
||||||
|
mappings.push(baseMapping)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings.push(baseMapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query database for productionIDs
|
||||||
|
if (productionIds.length > 0) {
|
||||||
|
const productionIdMappings = await this.resolveProductionIds(productionIds)
|
||||||
|
|
||||||
|
// Update mappings with database results
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (mapping.inputType === 'productionId') {
|
||||||
|
const dbResult = productionIdMappings.find(m => m.input === mapping.input)
|
||||||
|
if (dbResult) {
|
||||||
|
mapping.orderNumber = dbResult.orderNumber
|
||||||
|
mapping.isValid = dbResult.isValid
|
||||||
|
mapping.error = dbResult.error
|
||||||
|
|
||||||
|
if (dbResult.isValid) {
|
||||||
|
stats.successfullyResolved++
|
||||||
|
} else {
|
||||||
|
stats.failedToResolve++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify order numbers exist in database (optional validation)
|
||||||
|
// This can be skipped if you want to allow any SC+14digits format
|
||||||
|
// For now, we'll verify them against the database
|
||||||
|
if (orderNumbers.length > 0) {
|
||||||
|
const verifiedOrderNumbers = new Set(await this.verifyOrderNumbers(orderNumbers))
|
||||||
|
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (mapping.inputType === 'orderNumber' && mapping.orderNumber) {
|
||||||
|
if (!verifiedOrderNumbers.has(mapping.orderNumber)) {
|
||||||
|
mapping.isValid = false
|
||||||
|
mapping.error = `生产订单号不存在于数据库中:${mapping.orderNumber}`
|
||||||
|
stats.notFoundInDatabase++
|
||||||
|
stats.successfullyResolved--
|
||||||
|
stats.failedToResolve++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve productionIDs to production order numbers via database lookup
|
||||||
|
* @param productionIds - List of productionIDs to resolve
|
||||||
|
* @returns List of order mappings
|
||||||
|
*/
|
||||||
|
private async resolveProductionIds(productionIds: string[]): Promise<OrderMapping[]> {
|
||||||
|
const mappings: OrderMapping[] = []
|
||||||
|
|
||||||
|
if (!this.mysqlService.isConnected()) {
|
||||||
|
// Database not connected, return all as failed
|
||||||
|
for (const pid of productionIds) {
|
||||||
|
mappings.push({
|
||||||
|
input: pid,
|
||||||
|
productionId: pid,
|
||||||
|
isValid: false,
|
||||||
|
error: '数据库未连接,无法查询生产订单号',
|
||||||
|
inputType: 'productionId'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build query with placeholders
|
||||||
|
const placeholders = productionIds.map(() => '?').join(', ')
|
||||||
|
const query = `
|
||||||
|
SELECT \`${DB_CONFIG.FIELD_PRODUCTION_ID}\`, \`${DB_CONFIG.FIELD_ORDER_NUMBER}\`
|
||||||
|
FROM \`${DB_CONFIG.TABLE_NAME}\`
|
||||||
|
WHERE \`${DB_CONFIG.FIELD_PRODUCTION_ID}\` IN (${placeholders})
|
||||||
|
`
|
||||||
|
|
||||||
|
const result = await this.mysqlService.query(query, productionIds)
|
||||||
|
|
||||||
|
// Create a map for quick lookup
|
||||||
|
const resultMap = new Map<string, string>()
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const prodId = row[DB_CONFIG.FIELD_PRODUCTION_ID] as string
|
||||||
|
const orderNum = row[DB_CONFIG.FIELD_ORDER_NUMBER] as string
|
||||||
|
if (prodId && orderNum) {
|
||||||
|
resultMap.set(prodId, orderNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build mappings
|
||||||
|
for (const pid of productionIds) {
|
||||||
|
const orderNumber = resultMap.get(pid)
|
||||||
|
|
||||||
|
if (orderNumber) {
|
||||||
|
mappings.push({
|
||||||
|
input: pid,
|
||||||
|
productionId: pid,
|
||||||
|
orderNumber,
|
||||||
|
isValid: true,
|
||||||
|
inputType: 'productionId'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mappings.push({
|
||||||
|
input: pid,
|
||||||
|
productionId: pid,
|
||||||
|
isValid: false,
|
||||||
|
error: `数据库中未找到生产 ID:${pid}`,
|
||||||
|
inputType: 'productionId'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '未知数据库错误'
|
||||||
|
|
||||||
|
// Return all as failed with error
|
||||||
|
return productionIds.map(pid => ({
|
||||||
|
input: pid,
|
||||||
|
productionId: pid,
|
||||||
|
isValid: false,
|
||||||
|
error: `数据库查询失败:${message}`,
|
||||||
|
inputType: 'productionId'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that order numbers exist in the database
|
||||||
|
* @param orderNumbers - List of order numbers to verify
|
||||||
|
* @returns List of valid order numbers
|
||||||
|
*/
|
||||||
|
private async verifyOrderNumbers(orderNumbers: string[]): Promise<string[]> {
|
||||||
|
if (!this.mysqlService.isConnected()) {
|
||||||
|
return orderNumbers // Skip verification if not connected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const placeholders = orderNumbers.map(() => '?').join(', ')
|
||||||
|
const query = `
|
||||||
|
SELECT \`${DB_CONFIG.FIELD_ORDER_NUMBER}\`
|
||||||
|
FROM \`${DB_CONFIG.TABLE_NAME}\`
|
||||||
|
WHERE \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` IN (${placeholders})
|
||||||
|
`
|
||||||
|
|
||||||
|
const result = await this.mysqlService.query(query, orderNumbers)
|
||||||
|
return result.rows.map(row => row[DB_CONFIG.FIELD_ORDER_NUMBER] as string)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[OrderResolver] Failed to verify order numbers:', error)
|
||||||
|
return orderNumbers // Skip verification on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get resolution statistics
|
||||||
|
* @param mappings - List of order mappings
|
||||||
|
* @returns Resolution statistics
|
||||||
|
*/
|
||||||
|
getStats(mappings: OrderMapping[]): ResolutionStats {
|
||||||
|
const stats: ResolutionStats = {
|
||||||
|
totalInputs: mappings.length,
|
||||||
|
recognizedAsProductionId: 0,
|
||||||
|
recognizedAsOrderNumber: 0,
|
||||||
|
unknownFormat: 0,
|
||||||
|
successfullyResolved: 0,
|
||||||
|
failedToResolve: 0,
|
||||||
|
notFoundInDatabase: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (mapping.inputType === 'productionId') {
|
||||||
|
stats.recognizedAsProductionId++
|
||||||
|
} else if (mapping.inputType === 'orderNumber') {
|
||||||
|
stats.recognizedAsOrderNumber++
|
||||||
|
} else {
|
||||||
|
stats.unknownFormat++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.isValid) {
|
||||||
|
stats.successfullyResolved++
|
||||||
|
} else if (mapping.error?.includes('不存在于数据库中')) {
|
||||||
|
stats.notFoundInDatabase++
|
||||||
|
stats.failedToResolve++
|
||||||
|
} else if (mapping.error) {
|
||||||
|
stats.failedToResolve++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract valid order numbers from mappings
|
||||||
|
* @param mappings - List of order mappings
|
||||||
|
* @returns List of valid production order numbers
|
||||||
|
*/
|
||||||
|
getValidOrderNumbers(mappings: OrderMapping[]): string[] {
|
||||||
|
return mappings
|
||||||
|
.filter(m => m.isValid && m.orderNumber)
|
||||||
|
.map(m => m.orderNumber!)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract warnings/errors from mappings
|
||||||
|
* @param mappings - List of order mappings
|
||||||
|
* @returns List of warning messages
|
||||||
|
*/
|
||||||
|
getWarnings(mappings: OrderMapping[]): string[] {
|
||||||
|
return mappings
|
||||||
|
.filter(m => !m.isValid && m.error)
|
||||||
|
.map(m => m.error!)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/preload/index.d.ts
vendored
22
src/preload/index.d.ts
vendored
@@ -1,4 +1,25 @@
|
|||||||
import type { FileAPI, ExtractorAPI, CleanerAPI, DatabaseAPI } from '../main/types/ipc-api.types'
|
import type { FileAPI, ExtractorAPI, CleanerAPI, DatabaseAPI } from '../main/types/ipc-api.types'
|
||||||
|
import type { ResolverInput, ResolverResponse } from '../main/ipc/resolver-handler'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order number resolver API
|
||||||
|
*/
|
||||||
|
export interface ResolverAPI {
|
||||||
|
/**
|
||||||
|
* Resolve productionIDs and 生产订单号 to production order numbers
|
||||||
|
* @param input - Resolver input with list of inputs
|
||||||
|
*/
|
||||||
|
resolve: (input: ResolverInput) => Promise<ResolverResponse>
|
||||||
|
/**
|
||||||
|
* Validate input format only (without database lookup)
|
||||||
|
* @param inputs - List of inputs to validate
|
||||||
|
*/
|
||||||
|
validateFormat: (inputs: string[]) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -22,6 +43,7 @@ declare global {
|
|||||||
extractor: ExtractorAPI
|
extractor: ExtractorAPI
|
||||||
cleaner: CleanerAPI
|
cleaner: CleanerAPI
|
||||||
database: DatabaseAPI
|
database: DatabaseAPI
|
||||||
|
resolver: ResolverAPI
|
||||||
}
|
}
|
||||||
api: unknown
|
api: unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { electronAPI } from '@electron-toolkit/preload'
|
|||||||
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
|
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
|
||||||
import type { ExtractorInput } from '../main/types/extractor.types'
|
import type { ExtractorInput } from '../main/types/extractor.types'
|
||||||
import type { CleanerInput } from '../main/types/cleaner.types'
|
import type { CleanerInput } from '../main/types/cleaner.types'
|
||||||
|
import type { ResolverInput } from '../main/ipc/resolver-handler'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
@@ -25,6 +26,12 @@ const api = {
|
|||||||
runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input)
|
runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Order number resolver
|
||||||
|
resolver: {
|
||||||
|
resolve: (input: ResolverInput) => ipcRenderer.invoke('resolver:resolve', input),
|
||||||
|
validateFormat: (inputs: string[]) => ipcRenderer.invoke('resolver:validateFormat', inputs)
|
||||||
|
},
|
||||||
|
|
||||||
// Database service
|
// Database service
|
||||||
database: {
|
database: {
|
||||||
connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config),
|
connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config),
|
||||||
|
|||||||
@@ -5,24 +5,78 @@ interface OrderNumberInputProps {
|
|||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
label?: string
|
label?: string
|
||||||
|
enableFormatStats?: boolean // Whether to show format statistics
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormatStats {
|
||||||
|
productionIdCount: number
|
||||||
|
orderNumberCount: number
|
||||||
|
unknownCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular expression patterns for order number recognition
|
||||||
|
const ORDER_PATTERNS = {
|
||||||
|
// productionID: 2 digits + 1 letter + serial number (1+)
|
||||||
|
PRODUCTION_ID: /^\d{2}[A-Za-z]\d+$/,
|
||||||
|
// 生产订单号:SC + 14 digits
|
||||||
|
ORDER_NUMBER: /^SC\d{14}$/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OrderNumberInput - A textarea component for entering line-separated order numbers
|
* OrderNumberInput - A textarea component for entering line-separated order numbers
|
||||||
* Displays a count of valid order numbers entered
|
* Supports automatic recognition of productionID and 生产订单号 formats
|
||||||
*/
|
*/
|
||||||
export const OrderNumberInput: React.FC<OrderNumberInputProps> = ({
|
export const OrderNumberInput: React.FC<OrderNumberInputProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = '请输入订单号,每行一个',
|
placeholder = '请输入订单号,每行一个\n支持两种格式:\n- productionID: 22A1, 22A123\n- 生产订单号:SC70202602120085',
|
||||||
label = '订单号列表'
|
label = '订单号列表',
|
||||||
|
enableFormatStats = true
|
||||||
}) => {
|
}) => {
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = useState(0)
|
||||||
|
const [stats, setStats] = useState<FormatStats>({
|
||||||
|
productionIdCount: 0,
|
||||||
|
orderNumberCount: 0,
|
||||||
|
unknownCount: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const recognizeType = (input: string): 'productionId' | 'orderNumber' | 'unknown' => {
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (!trimmed) return 'unknown'
|
||||||
|
|
||||||
|
if (ORDER_PATTERNS.ORDER_NUMBER.test(trimmed)) {
|
||||||
|
return 'orderNumber'
|
||||||
|
}
|
||||||
|
if (ORDER_PATTERNS.PRODUCTION_ID.test(trimmed)) {
|
||||||
|
return 'productionId'
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Count non-empty lines
|
// Count non-empty lines and categorize by format
|
||||||
const lines = value.split('\n').filter((line) => line.trim().length > 0)
|
const lines = value.split('\n').filter((line) => line.trim().length > 0)
|
||||||
setCount(lines.length)
|
setCount(lines.length)
|
||||||
|
|
||||||
|
// Calculate format statistics
|
||||||
|
const newStats: FormatStats = {
|
||||||
|
productionIdCount: 0,
|
||||||
|
orderNumberCount: 0,
|
||||||
|
unknownCount: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const type = recognizeType(line)
|
||||||
|
if (type === 'productionId') {
|
||||||
|
newStats.productionIdCount++
|
||||||
|
} else if (type === 'orderNumber') {
|
||||||
|
newStats.orderNumberCount++
|
||||||
|
} else {
|
||||||
|
newStats.unknownCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStats(newStats)
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
@@ -33,7 +87,18 @@ export const OrderNumberInput: React.FC<OrderNumberInputProps> = ({
|
|||||||
<div className="order-number-input">
|
<div className="order-number-input">
|
||||||
<div className="input-header">
|
<div className="input-header">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<span className="count-badge">{count} 个订单号</span>
|
<div className="stats-wrapper">
|
||||||
|
<span className="count-badge">{count} 个</span>
|
||||||
|
{enableFormatStats && stats.productionIdCount > 0 && (
|
||||||
|
<span className="stat-badge production-id">{stats.productionIdCount} 总排号</span>
|
||||||
|
)}
|
||||||
|
{enableFormatStats && stats.orderNumberCount > 0 && (
|
||||||
|
<span className="stat-badge order-number">{stats.orderNumberCount} 订单号</span>
|
||||||
|
)}
|
||||||
|
{enableFormatStats && stats.unknownCount > 0 && (
|
||||||
|
<span className="stat-badge unknown">{stats.unknownCount} 未知格式</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={value}
|
value={value}
|
||||||
@@ -57,6 +122,11 @@ export const OrderNumberInput: React.FC<OrderNumberInputProps> = ({
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
.stats-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.count-badge {
|
.count-badge {
|
||||||
background: #e6f7ff;
|
background: #e6f7ff;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
@@ -65,6 +135,25 @@ export const OrderNumberInput: React.FC<OrderNumberInputProps> = ({
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.stat-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.stat-badge.production-id {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.stat-badge.order-number {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
.stat-badge.unknown {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
.order-textarea {
|
.order-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user