import db from '../config/database'; import { logger } from '../utils/logger'; export interface LogisticsOption { carrierId: string; carrierName: string; serviceType: 'STANDARD' | 'EXPRESS' | 'ECONOMY' | 'AIR_FREIGHT' | 'SEA_FREIGHT'; estimatedDays: number; baseCost: number; reliability: number; trackingSupported: boolean; } export interface LogisticsStrategy { orderId: string; destination: string; recommendedOption: LogisticsOption; alternatives: LogisticsOption[]; strategyReason: string; } export interface ChannelSelection { orderId: string; channelId: string; channelName: string; carrierId: string; estimatedDelivery: Date; cost: number; } export interface FreightCalculation { productId: string; quantity: number; destination: string; weight: number; volume: number; freightCost: number; breakdown: { baseFreight: number; fuelSurcharge: number; remoteAreaFee: number; insuranceFee: number; }; } export interface LogisticsStrategyInput { orderId: string; destination: string; tenantId: string; shopId?: string; taskId?: string; traceId: string; businessType: 'TOC' | 'TOB'; } /** * [BE-LOG001] 物流策略引擎 * [BE-LOG002] 渠道选择算法 * [BE-LOG003] 运费计算接口 * * @description 物流策略与渠道选择核心服务 */ export class LogisticsService { private static readonly CARRIERS = [ { id: 'DHL', name: 'DHL Express', type: 'EXPRESS', baseRate: 15, reliability: 0.95 }, { id: 'FEDEX', name: 'FedEx', type: 'EXPRESS', baseRate: 14, reliability: 0.93 }, { id: 'UPS', name: 'UPS', type: 'STANDARD', baseRate: 12, reliability: 0.92 }, { id: 'SF', name: 'SF Express', type: 'EXPRESS', baseRate: 10, reliability: 0.90 }, { id: 'EMS', name: 'EMS', type: 'STANDARD', baseRate: 8, reliability: 0.85 }, { id: 'SEA_ECONOMY', name: 'Sea Freight Economy', type: 'SEA_FREIGHT', baseRate: 3, reliability: 0.80 }, ]; /** * [BE-LOG001] 物流策略引擎 * 根据订单ID和目的地推荐最优物流方案 */ static async getLogisticsStrategy(input: LogisticsStrategyInput): Promise { const { orderId, destination, tenantId, shopId, taskId, traceId, businessType } = input; logger.info(`[LogisticsService] Getting logistics strategy - orderId: ${orderId}, destination: ${destination}, tenantId: ${tenantId}, traceId: ${traceId}`); try { const order = await this.getOrderInfo(orderId, tenantId); const options = await this.evaluateLogisticsOptions(destination, order); const recommendedOption = this.selectOptimalOption(options, businessType); const alternatives = options.filter(o => o.carrierId !== recommendedOption.carrierId).slice(0, 3); const strategyReason = this.generateStrategyReason(recommendedOption, businessType); const strategy: LogisticsStrategy = { orderId, destination, recommendedOption, alternatives, strategyReason, }; await this.logStrategyDecision(tenantId, orderId, strategy, traceId); return strategy; } catch (error: any) { logger.error(`[LogisticsService] Failed to get logistics strategy - orderId: ${orderId}, error: ${error.message}`); throw error; } } /** * [BE-LOG002] 渠道选择算法 * 根据订单和物流方案选择最优渠道 */ static async selectChannel( orderId: string, logisticsOption: LogisticsOption, options: { tenantId: string; shopId?: string; taskId?: string; traceId: string; } ): Promise { const { tenantId, traceId } = options; logger.info(`[LogisticsService] Selecting channel - orderId: ${orderId}, carrierId: ${logisticsOption.carrierId}, tenantId: ${tenantId}, traceId: ${traceId}`); try { const channel = await this.findOptimalChannel(logisticsOption, tenantId); const estimatedDelivery = this.calculateEstimatedDelivery(logisticsOption.estimatedDays); const selection: ChannelSelection = { orderId, channelId: channel.id, channelName: channel.name, carrierId: logisticsOption.carrierId, estimatedDelivery, cost: logisticsOption.baseCost, }; await db('cf_logistics_channel_selections').insert({ tenant_id: tenantId, order_id: orderId, channel_id: channel.id, carrier_id: logisticsOption.carrierId, estimated_delivery: estimatedDelivery, cost: logisticsOption.baseCost, trace_id: traceId, created_at: new Date(), }); logger.info(`[LogisticsService] Channel selected - orderId: ${orderId}, channelId: ${channel.id}`); return selection; } catch (error: any) { logger.error(`[LogisticsService] Failed to select channel - orderId: ${orderId}, error: ${error.message}`); throw error; } } /** * [BE-LOG003] 运费计算接口 * 根据商品ID、数量和目的地计算运费 */ static async calculateFreight( productId: string, quantity: number, destination: string, options: { tenantId: string; shopId?: string; taskId?: string; traceId: string; } ): Promise { const { tenantId, traceId } = options; logger.info(`[LogisticsService] Calculating freight - productId: ${productId}, quantity: ${quantity}, destination: ${destination}, tenantId: ${tenantId}, traceId: ${traceId}`); try { const product = await this.getProductInfo(productId, tenantId); const weight = (product.weight || 0.5) * quantity; const volume = (product.volume || 0.001) * quantity; const baseFreight = this.calculateBaseFreight(weight, volume, destination); const fuelSurcharge = baseFreight * 0.15; const remoteAreaFee = this.getRemoteAreaFee(destination); const insuranceFee = baseFreight * 0.02; const freightCost = Number((baseFreight + fuelSurcharge + remoteAreaFee + insuranceFee).toFixed(2)); const calculation: FreightCalculation = { productId, quantity, destination, weight, volume, freightCost, breakdown: { baseFreight: Number(baseFreight.toFixed(2)), fuelSurcharge: Number(fuelSurcharge.toFixed(2)), remoteAreaFee: Number(remoteAreaFee.toFixed(2)), insuranceFee: Number(insuranceFee.toFixed(2)), }, }; await db('cf_freight_calculations').insert({ tenant_id: tenantId, product_id: productId, quantity, destination, weight, volume, freight_cost: freightCost, breakdown: JSON.stringify(calculation.breakdown), trace_id: traceId, created_at: new Date(), }); logger.info(`[LogisticsService] Freight calculated - productId: ${productId}, freightCost: ${freightCost}`); return calculation; } catch (error: any) { logger.error(`[LogisticsService] Failed to calculate freight - productId: ${productId}, error: ${error.message}`); throw error; } } private static async getOrderInfo(orderId: string, tenantId: string): Promise { const order = await db('cf_consumer_orders') .where({ id: orderId, tenant_id: tenantId }) .first(); return order || { id: orderId, weight: 1, value: 100, priority: 'NORMAL' }; } private static async getProductInfo(productId: string, tenantId: string): Promise { const product = await db('cf_product') .where({ id: productId, tenant_id: tenantId }) .first(); return product || { id: productId, weight: 0.5, volume: 0.001 }; } private static async evaluateLogisticsOptions(destination: string, order: any): Promise { const isRemote = this.isRemoteDestination(destination); const isHighValue = (order.value || 100) > 500; const options: LogisticsOption[] = this.CARRIERS.map(carrier => { let estimatedDays = this.getEstimatedDays(carrier.type, destination); let baseCost = carrier.baseRate * (order.weight || 1); if (isRemote) { baseCost *= 1.3; estimatedDays += 2; } if (isHighValue && carrier.type !== 'EXPRESS') { estimatedDays += 1; } return { carrierId: carrier.id, carrierName: carrier.name, serviceType: carrier.type as any, estimatedDays, baseCost: Number(baseCost.toFixed(2)), reliability: carrier.reliability, trackingSupported: carrier.type !== 'SEA_FREIGHT', }; }); return options; } private static selectOptimalOption(options: LogisticsOption[], businessType: string): LogisticsOption { const scoredOptions = options.map(option => { let score = 0; score += option.reliability * 40; score += (1 / option.estimatedDays) * 30; score += (1 / option.baseCost) * 20; if (option.trackingSupported) { score += 10; } if (businessType === 'TOB') { score += option.reliability * 10; } return { ...option, score }; }); scoredOptions.sort((a, b) => b.score - a.score); return scoredOptions[0]; } private static generateStrategyReason(option: LogisticsOption, businessType: string): string { return `Recommended ${option.carrierName} (${option.serviceType}) for ${businessType} order. ` + `Estimated delivery: ${option.estimatedDays} days. ` + `Reliability: ${(option.reliability * 100).toFixed(0)}%. ` + `Cost: $${option.baseCost}.`; } private static async logStrategyDecision( tenantId: string, orderId: string, strategy: LogisticsStrategy, traceId: string ): Promise { try { await db('cf_logistics_strategy_decisions').insert({ tenant_id: tenantId, order_id: orderId, recommended_carrier: strategy.recommendedOption.carrierId, recommended_cost: strategy.recommendedOption.baseCost, estimated_days: strategy.recommendedOption.estimatedDays, strategy_reason: strategy.strategyReason, alternatives: JSON.stringify(strategy.alternatives), trace_id: traceId, created_at: new Date(), }); } catch (error: any) { logger.warn(`[LogisticsService] Failed to log strategy decision: ${error.message}`); } } private static async findOptimalChannel(option: LogisticsOption, tenantId: string): Promise<{ id: string; name: string }> { const channel = await db('cf_logistics_channels') .where({ tenant_id: tenantId, carrier_id: option.carrierId, status: 'ACTIVE' }) .first(); if (channel) { return { id: channel.id, name: channel.name }; } return { id: `CH-${option.carrierId}-${Date.now()}`, name: `${option.carrierName} Channel`, }; } private static calculateEstimatedDelivery(days: number): Date { const delivery = new Date(); delivery.setDate(delivery.getDate() + days); return delivery; } private static calculateBaseFreight(weight: number, volume: number, destination: string): number { const volumetricWeight = volume * 5000; const chargeableWeight = Math.max(weight, volumetricWeight); let ratePerKg = 5; if (destination.startsWith('US') || destination.startsWith('CA')) { ratePerKg = 8; } else if (destination.startsWith('EU') || destination.startsWith('UK')) { ratePerKg = 7; } else if (destination.startsWith('AU') || destination.startsWith('NZ')) { ratePerKg = 6; } return chargeableWeight * ratePerKg; } private static getRemoteAreaFee(destination: string): number { const remoteAreas = ['AK', 'HI', 'PR', 'GU', 'VI', 'NT', 'YT', 'NU']; const isRemote = remoteAreas.some(area => destination.includes(area)); return isRemote ? 15 : 0; } private static isRemoteDestination(destination: string): boolean { const remotePatterns = ['AK', 'HI', 'PR', 'GU', 'VI', 'NT', 'YT', 'NU', 'REMOTE']; return remotePatterns.some(pattern => destination.toUpperCase().includes(pattern)); } private static getEstimatedDays(serviceType: string, destination: string): number { const baseDays: Record = { 'EXPRESS': 3, 'STANDARD': 7, 'ECONOMY': 14, 'AIR_FREIGHT': 5, 'SEA_FREIGHT': 30, }; let days = baseDays[serviceType] || 7; if (destination.startsWith('US') || destination.startsWith('CA')) { days += 2; } else if (destination.startsWith('EU') || destination.startsWith('UK')) { days += 3; } else if (destination.startsWith('AU') || destination.startsWith('NZ')) { days += 4; } return days; } static async initTables(): Promise { logger.info('[LogisticsService] Initializing logistics tables'); try { if (!(await db.schema.hasTable('cf_logistics_strategy_decisions'))) { await db.schema.createTable('cf_logistics_strategy_decisions', (table) => { table.string('id').primary().defaultTo(db.raw('uuid()')); table.string('tenant_id').notNullable().index(); table.string('order_id').notNullable().index(); table.string('recommended_carrier').notNullable(); table.decimal('recommended_cost', 10, 2).notNullable(); table.integer('estimated_days').notNullable(); table.text('strategy_reason'); table.json('alternatives'); table.string('trace_id'); table.timestamp('created_at').defaultTo(db.fn.now()); }); logger.info('[LogisticsService] Table cf_logistics_strategy_decisions created'); } if (!(await db.schema.hasTable('cf_logistics_channel_selections'))) { await db.schema.createTable('cf_logistics_channel_selections', (table) => { table.string('id').primary().defaultTo(db.raw('uuid()')); table.string('tenant_id').notNullable().index(); table.string('order_id').notNullable().index(); table.string('channel_id').notNullable(); table.string('channel_name'); table.string('carrier_id').notNullable(); table.date('estimated_delivery'); table.decimal('cost', 10, 2).notNullable(); table.string('trace_id'); table.timestamp('created_at').defaultTo(db.fn.now()); }); logger.info('[LogisticsService] Table cf_logistics_channel_selections created'); } if (!(await db.schema.hasTable('cf_freight_calculations'))) { await db.schema.createTable('cf_freight_calculations', (table) => { table.string('id').primary().defaultTo(db.raw('uuid()')); table.string('tenant_id').notNullable().index(); table.string('product_id').notNullable().index(); table.integer('quantity').notNullable(); table.string('destination').notNullable(); table.decimal('weight', 10, 3).notNullable(); table.decimal('volume', 10, 6).notNullable(); table.decimal('freight_cost', 10, 2).notNullable(); table.json('breakdown'); table.string('trace_id'); table.timestamp('created_at').defaultTo(db.fn.now()); }); logger.info('[LogisticsService] Table cf_freight_calculations created'); } if (!(await db.schema.hasTable('cf_logistics_channels'))) { await db.schema.createTable('cf_logistics_channels', (table) => { table.string('id').primary(); table.string('tenant_id').notNullable().index(); table.string('name').notNullable(); table.string('carrier_id').notNullable().index(); table.enum('status', ['ACTIVE', 'INACTIVE', 'SUSPENDED']).notNullable().defaultTo('ACTIVE'); table.string('trace_id'); table.timestamp('created_at').defaultTo(db.fn.now()); table.timestamp('updated_at').defaultTo(db.fn.now()); }); logger.info('[LogisticsService] Table cf_logistics_channels created'); } logger.info('[LogisticsService] Logistics tables initialized successfully'); } catch (error: any) { logger.error(`[LogisticsService] Failed to initialize logistics tables - error: ${error.message}`); throw error; } } }