refactor: migrate configuration to YAML-based system

This commit is contained in:
test
2026-03-07 11:30:09 +08:00
parent 48f1f51d76
commit c13be9e19a
27 changed files with 1716 additions and 1638 deletions

View File

@@ -7,14 +7,12 @@
* - 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)
* Database table and field names are loaded from config.yaml
*/
import type { IDatabaseService } from '../database'
import { SqlServerService } from '../database/sql-server'
import { ConfigManager } from '../config/config-manager'
import { createLogger } from '../logger'
import sql from 'mssql'
const log = createLogger('OrderResolver')
@@ -28,49 +26,52 @@ export interface OrderMapping {
productionId?: string
/** Final production order number to use */
orderNumber?: string
/** Whether this mapping is valid */
isValid: boolean
/** Error or warning message */
/** Whether the order number was successfully resolved */
resolved: boolean
/** Error message if resolution failed */
error?: string
/** Input type */
inputType: 'productionId' | 'orderNumber' | 'unknown'
}
/**
* Order number type recognition result
*/
export type OrderNumberType = 'productionId' | 'orderNumber' | 'unknown'
/**
* Resolution statistics
*/
export interface ResolutionStats {
totalInputs: number
recognizedAsProductionId: number
recognizedAsOrderNumber: number
validOrderNumbers: number
validProductionIds: number
resolvedCount: number
failedCount: number
unknownFormat: number
successfullyResolved: number
failedToResolve: number
notFoundInDatabase: number
}
/**
* Regular expression patterns
* ProductionID pattern: 2 digits + 1 letter + 1-4 digits
*/
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
const PRODUCTION_ID_PATTERN = /^\d{2}[A-Z]\d{1,4}$/i
/**
* Production order number pattern: SC + 14 digits
*/
const ORDER_NUMBER_PATTERN = /^SC\d{14}$/i
/**
* 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: '生产订单号')
* Loaded from config.yaml via ConfigManager
*/
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
export function getDbConfig() {
const configManager = ConfigManager.getInstance()
const config = configManager.getConfig()
return {
TABLE_NAME: config.orderResolution.tableName || 'productionContractData_26 年压力表合同数据',
FIELD_PRODUCTION_ID: config.orderResolution.productionIdField || '总排号',
FIELD_ORDER_NUMBER: config.orderResolution.orderNumberField || '生产订单号'
}
}
/**
* Order Number Resolver Service
@@ -84,364 +85,201 @@ export class OrderNumberResolver {
/**
* Get table name based on database type
* Converts MySQL schema_tablename format to SQL Server [schema].[tablename] format
* e.g., productionContractData_26年压力表合同数据 -> [productionContractData].[26年压力表合同数据]
*/
private getTableName(mysqlTableName: string): string {
private getTableName(tableName: string): string {
if (this.dbService.type === 'sqlserver') {
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
if (firstUnderscoreIndex > 0) {
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
return `[${schema}].[${tableName}]`
}
return `[dbo].[${mysqlTableName}]`
return `[dbo].[${tableName}]`
}
return mysqlTableName
return tableName
}
/**
* Recognize the type of an input string
* @param input - The input string to recognize
* @returns The recognized type
* Check if input matches productionID pattern
*/
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'
isProductionId(input: string): boolean {
return PRODUCTION_ID_PATTERN.test(input)
}
/**
* Resolve a list of inputs to production order numbers
* @param inputs - List of input strings (can be productionID or 生产订单号)
* @returns List of order mappings
* Check if input matches order number pattern
*/
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
isOrderNumber(input: string): boolean {
return ORDER_NUMBER_PATTERN.test(input)
}
/**
* Resolve productionIDs to production order numbers via database lookup
* @param productionIds - List of productionIDs to resolve
* @returns List of order mappings
* Map productionID to order number via database lookup
*/
private async resolveProductionIds(productionIds: string[]): Promise<OrderMapping[]> {
const mappings: OrderMapping[] = []
if (!this.dbService.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
}
async mapProductionIdToOrderNumber(productionId: string): Promise<string | null> {
try {
const isSqlServer = this.dbService.type === 'sqlserver'
const tableName = this.getTableName(DB_CONFIG.TABLE_NAME)
const dbConfig = getDbConfig()
const tableName = this.getTableName(dbConfig.TABLE_NAME)
log.debug('Resolving production IDs', {
count: productionIds.length,
dbType: this.dbService.type
})
let sql: string
let params: any[]
let result
if (isSqlServer) {
// Use queryWithParams for SQL Server with explicit parameter types
const placeholders = productionIds.map((_, idx) => `@p${idx}`).join(', ')
const params: Record<
string,
{
value: string
type: sql.ISqlType | sql.ISqlTypeFactoryWithLength | sql.ISqlTypeWithLength
}
> = {}
productionIds.forEach((id, idx) => {
params[`p${idx}`] = { value: id, type: sql.NVarChar(255) }
})
const query = `
SELECT ${DB_CONFIG.FIELD_PRODUCTION_ID}, ${DB_CONFIG.FIELD_ORDER_NUMBER}
FROM ${tableName}
WHERE ${DB_CONFIG.FIELD_PRODUCTION_ID} IN (${placeholders})
`
result = await (this.dbService as SqlServerService).queryWithParams(query, params)
if (this.dbService.type === 'sqlserver') {
sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] = @p0`
params = [productionId]
} else {
// Use standard query for MySQL
const placeholders = productionIds.map(() => '?').join(', ')
const query = `
SELECT \`${DB_CONFIG.FIELD_PRODUCTION_ID}\`, \`${DB_CONFIG.FIELD_ORDER_NUMBER}\`
FROM ${tableName}
WHERE \`${DB_CONFIG.FIELD_PRODUCTION_ID}\` IN (${placeholders})
`
result = await this.dbService.query(query, productionIds)
sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` = ? LIMIT 1`
params = [productionId]
}
// Create a map for quick lookup (use lowercase key for case-insensitive matching)
const resultMap = new Map<string, string>()
const result = await this.dbService.query(sql, params)
if (result.rows.length > 0) {
const orderNumber = result.rows[0][Object.keys(result.rows[0])[0]] as string
return orderNumber || null
}
return null
} catch (error) {
const message = error instanceof Error ? error.message : '未知数据库错误'
log.error('Failed to map productionID to order number', {
productionId,
error: message
})
throw error
}
}
/**
* Map multiple productionIds to order numbers
*/
async mapProductionIdsToOrderNumbers(productionIds: string[]): Promise<Map<string, string>> {
try {
const dbConfig = getDbConfig()
const tableName = this.getTableName(dbConfig.TABLE_NAME)
if (productionIds.length === 0) {
return new Map()
}
// Use parameterized query to prevent SQL injection
const placeholders = productionIds.map((_, i) => `@p${i}`).join(', ')
const params = productionIds
let sql: string
if (this.dbService.type === 'sqlserver') {
sql = `SELECT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] IN (${placeholders})`
} else {
const idPlaceholders = productionIds.map(() => '?').join(', ')
sql = `SELECT \`${dbConfig.FIELD_PRODUCTION_ID}\`, \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE \`${dbConfig.FIELD_PRODUCTION_ID}\` IN (${idPlaceholders})`
}
const result = await this.dbService.query(sql, params)
const mappings = 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
const keys = Object.keys(row)
const prodId = row[keys[0]] as string
const orderNum = row[keys[1]] as string
if (prodId && orderNum) {
// Store with lowercase key for case-insensitive matching
resultMap.set(prodId.toLowerCase(), orderNum)
}
}
// Build mappings
for (const pid of productionIds) {
// Use lowercase for case-insensitive lookup
const orderNumber = resultMap.get(pid.toLowerCase())
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'
})
mappings.set(prodId, orderNum)
}
}
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'
}))
log.error('Failed to map productionIds to order numbers', {
error: message
})
throw error
}
}
/**
* Verify that order numbers exist in the database
* @param orderNumbers - List of order numbers to verify
* @returns List of valid order numbers
* Resolve order numbers from mixed input
*/
private async verifyOrderNumbers(orderNumbers: string[]): Promise<string[]> {
if (!this.dbService.isConnected()) {
return orderNumbers // Skip verification if not connected
}
async resolve(inputs: string[]): Promise<OrderMapping[]> {
const mappings: OrderMapping[] = []
try {
const isSqlServer = this.dbService.type === 'sqlserver'
const tableName = this.getTableName(DB_CONFIG.TABLE_NAME)
for (const input of inputs) {
const mapping: OrderMapping = { input, resolved: false }
let result
if (isSqlServer) {
// Use queryWithParams for SQL Server with explicit parameter types
const placeholders = orderNumbers.map((_, idx) => `@p${idx}`).join(', ')
const params: Record<
string,
{
value: string
type: sql.ISqlType | sql.ISqlTypeFactoryWithLength | sql.ISqlTypeWithLength
if (this.isOrderNumber(input)) {
// Already an order number
mapping.orderNumber = input
mapping.resolved = true
} else if (this.isProductionId(input)) {
// Is a productionID, need to lookup
mapping.productionId = input
try {
const orderNumber = await this.mapProductionIdToOrderNumber(input)
if (orderNumber) {
mapping.orderNumber = orderNumber
mapping.resolved = true
} else {
mapping.error = '未在数据库中找到对应的订单号'
}
> = {}
orderNumbers.forEach((id, idx) => {
params[`p${idx}`] = { value: id, type: sql.NVarChar(255) }
})
const query = `
SELECT ${DB_CONFIG.FIELD_ORDER_NUMBER}
FROM ${tableName}
WHERE ${DB_CONFIG.FIELD_ORDER_NUMBER} IN (${placeholders})
`
result = await (this.dbService as SqlServerService).queryWithParams(query, params)
} catch (error) {
mapping.error = error instanceof Error ? error.message : '数据库查询失败'
log.warn('Failed to resolve productionID', { productionId: input, error })
}
} else {
// Use standard query for MySQL
const placeholders = orderNumbers.map(() => '?').join(', ')
const query = `
SELECT \`${DB_CONFIG.FIELD_ORDER_NUMBER}\`
FROM ${tableName}
WHERE \`${DB_CONFIG.FIELD_ORDER_NUMBER}\` IN (${placeholders})
`
result = await this.dbService.query(query, orderNumbers)
mapping.error = '格式不识别:既不是有效的生产订单号也不是总排号格式'
}
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
mappings.push(mapping)
}
return mappings
}
/**
* Get valid order numbers from mappings
*/
getValidOrderNumbers(mappings: OrderMapping[]): string[] {
return mappings.filter((m) => m.resolved && m.orderNumber).map((m) => m.orderNumber!)
}
/**
* Get warnings from failed mappings
*/
getWarnings(mappings: OrderMapping[]): string[] {
return mappings.filter((m) => !m.resolved && m.error).map((m) => `${m.input}: ${m.error}`)
}
/**
* Recognize the type of input
*/
recognizeType(input: string): OrderNumberType {
if (this.isOrderNumber(input)) return 'orderNumber'
if (this.isProductionId(input)) return 'productionId'
return 'unknown'
}
/**
* 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
validOrderNumbers: 0,
validProductionIds: 0,
resolvedCount: 0,
failedCount: 0,
unknownFormat: 0
}
for (const mapping of mappings) {
if (mapping.inputType === 'productionId') {
stats.recognizedAsProductionId++
} else if (mapping.inputType === 'orderNumber') {
stats.recognizedAsOrderNumber++
if (mapping.resolved) {
stats.resolvedCount++
if (mapping.orderNumber && !mapping.productionId) {
stats.validOrderNumbers++
} else if (mapping.productionId) {
stats.validProductionIds++
}
} else {
stats.unknownFormat++
}
if (mapping.isValid) {
stats.successfullyResolved++
} else if (mapping.error?.includes('不存在于数据库中')) {
stats.notFoundInDatabase++
stats.failedToResolve++
} else if (mapping.error) {
stats.failedToResolve++
stats.failedCount++
if (!mapping.productionId && !mapping.orderNumber) {
stats.unknownFormat++
}
}
}
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!)
}
}