245 lines
9.4 KiB
TypeScript
245 lines
9.4 KiB
TypeScript
|
|
import db from '../config/database';
|
|||
|
|
import { logger } from '../utils/logger';
|
|||
|
|
|
|||
|
|
export interface SupplierPerformance {
|
|||
|
|
id: string;
|
|||
|
|
tenantId: string;
|
|||
|
|
name: string;
|
|||
|
|
ratingScore: number;
|
|||
|
|
avgDeliveryDays: number;
|
|||
|
|
defectRate: number;
|
|||
|
|
performanceHistory: any;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [BIZ_TRADE_04] 供应商风险评级与自动优选服务
|
|||
|
|
* @description 建立供应商画像,基于交期、质量、价格波动自动优选最优货源
|
|||
|
|
*/
|
|||
|
|
export class SupplierService {
|
|||
|
|
private static readonly TABLE_NAME = 'cf_suppliers';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化数据库表
|
|||
|
|
*/
|
|||
|
|
static async initTable() {
|
|||
|
|
const hasTable = await db.schema.hasTable(this.TABLE_NAME);
|
|||
|
|
if (!hasTable) {
|
|||
|
|
console.log(`📦 Creating ${this.TABLE_NAME} table...`);
|
|||
|
|
await db.schema.createTable(this.TABLE_NAME, (table) => {
|
|||
|
|
table.string('id', 64).primary();
|
|||
|
|
table.string('tenant_id', 64).notNullable().index(); // [CORE_SEC_45] 租户隔离
|
|||
|
|
table.string('name', 128).notNullable();
|
|||
|
|
table.decimal('rating_score', 10, 2).defaultTo(80.00);
|
|||
|
|
table.decimal('avg_delivery_days', 10, 2).defaultTo(5.00);
|
|||
|
|
table.decimal('defect_rate', 10, 4).defaultTo(0.0000);
|
|||
|
|
table.decimal('price_stability', 10, 2).defaultTo(0.95); // [BIZ_SUP_20] 价格稳定性 (0-1)
|
|||
|
|
table.decimal('avg_response_hours', 10, 2).defaultTo(2.00); // [BIZ_SUP_20] 响应速度 (小时)
|
|||
|
|
table.boolean('isSourceFactory').defaultTo(false);
|
|||
|
|
table.string('risk_level', 20).defaultTo('LOW'); // [BIZ_OPS_157] 经营风险级别
|
|||
|
|
table.text('legal_notices').nullable(); // [BIZ_OPS_157] 法务/工商风险详情
|
|||
|
|
table.json('performance_history').nullable();
|
|||
|
|
table.timestamps(true, true);
|
|||
|
|
|
|||
|
|
// 增加复合索引:租户 + 工厂属性
|
|||
|
|
table.index(['tenant_id', 'isSourceFactory'], 'idx_supplier_type');
|
|||
|
|
});
|
|||
|
|
console.log(`✅ Table ${this.TABLE_NAME} created`);
|
|||
|
|
} else {
|
|||
|
|
// [BIZ_OPS_157] 确保风险分析所需的列存在
|
|||
|
|
const hasRisk = await db.schema.hasColumn(this.TABLE_NAME, 'risk_level');
|
|||
|
|
if (!hasRisk) {
|
|||
|
|
await db.schema.table(this.TABLE_NAME, (table) => {
|
|||
|
|
table.string('risk_level', 20).defaultTo('LOW');
|
|||
|
|
table.text('legal_notices').nullable();
|
|||
|
|
});
|
|||
|
|
logger.info(`✅ Table ${this.TABLE_NAME} updated with risk columns`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 记录供应商
|
|||
|
|
*/
|
|||
|
|
static async registerSupplier(supplier: SupplierPerformance): Promise<void> {
|
|||
|
|
logger.info(`[Supplier] Registering supplier: ${supplier.name} for Tenant: ${supplier.tenantId}`);
|
|||
|
|
|
|||
|
|
await db(this.TABLE_NAME).insert({
|
|||
|
|
id: supplier.id,
|
|||
|
|
tenant_id: supplier.tenantId,
|
|||
|
|
name: supplier.name,
|
|||
|
|
rating_score: supplier.ratingScore,
|
|||
|
|
avg_delivery_days: supplier.avgDeliveryDays,
|
|||
|
|
defect_rate: supplier.defectRate,
|
|||
|
|
performance_history: JSON.stringify(supplier.performanceHistory || {}),
|
|||
|
|
created_at: new Date(),
|
|||
|
|
updated_at: new Date()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新供应商绩效数据 (由采购单 PO 完成后调用)
|
|||
|
|
*/
|
|||
|
|
static async updatePerformance(supplierId: string, stats: { deliveryDays: number; qualityResult: 'PASSED' | 'FAILED' }): Promise<void> {
|
|||
|
|
const supplier = await db(this.TABLE_NAME).where({ id: supplierId }).first();
|
|||
|
|
if (!supplier) return;
|
|||
|
|
|
|||
|
|
const history = typeof supplier.performance_history === 'string' ? JSON.parse(supplier.performance_history) : supplier.performance_history;
|
|||
|
|
history.deliveries = (history.deliveries || 0) + 1;
|
|||
|
|
history.total_days = (history.total_days || 0) + stats.deliveryDays;
|
|||
|
|
if (stats.qualityResult === 'FAILED') {
|
|||
|
|
history.defects = (history.defects || 0) + 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const newAvgDelivery = history.total_days / history.deliveries;
|
|||
|
|
const newDefectRate = (history.defects || 0) / history.deliveries;
|
|||
|
|
|
|||
|
|
// 综合评分公式: (1 - 破损率) * 70% + (1 / 交期) * 30% (简化版)
|
|||
|
|
const newScore = (1 - newDefectRate) * 70 + (1 / Math.max(newAvgDelivery, 1)) * 30;
|
|||
|
|
|
|||
|
|
await db(this.TABLE_NAME).where({ id: supplierId }).update({
|
|||
|
|
avg_delivery_days: newAvgDelivery,
|
|||
|
|
defect_rate: newDefectRate,
|
|||
|
|
rating_score: newScore,
|
|||
|
|
performance_history: JSON.stringify(history),
|
|||
|
|
updated_at: new Date()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [BIZ_SUP_20] 实时性能指标采集 (Performance Telemetry)
|
|||
|
|
* @description 模拟从订单履约、客服沟通中自动提取供应商表现数据
|
|||
|
|
*/
|
|||
|
|
static async collectRealTimeMetrics(supplierId: string, tenantId: string) {
|
|||
|
|
logger.info(`[TrustScore] Collecting real-time metrics for Supplier: ${supplierId}`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 1. 获取该供应商近期的履约数据
|
|||
|
|
const stats = await db('cf_orders')
|
|||
|
|
.where({ supplier_id: supplierId, tenant_id: tenantId })
|
|||
|
|
.orderBy('created_at', 'desc')
|
|||
|
|
.limit(20)
|
|||
|
|
.select('logistics_cost', 'status', 'created_at', 'updated_at');
|
|||
|
|
|
|||
|
|
if (stats.length === 0) return;
|
|||
|
|
|
|||
|
|
// 2. 计算平均履约时效 (模拟逻辑)
|
|||
|
|
const avgDelivery = stats.reduce((acc, curr) => {
|
|||
|
|
const days = (curr.updated_at.getTime() - curr.created_at.getTime()) / (1000 * 3600 * 24);
|
|||
|
|
return acc + days;
|
|||
|
|
}, 0) / stats.length;
|
|||
|
|
|
|||
|
|
// 3. 计算价格稳定性 (价格标准差,模拟)
|
|||
|
|
const priceStability = 0.98 - (Math.random() * 0.1);
|
|||
|
|
|
|||
|
|
// 4. 更新供应商主表
|
|||
|
|
await db(this.TABLE_NAME).where({ id: supplierId }).update({
|
|||
|
|
avg_delivery_days: Number(avgDelivery.toFixed(2)),
|
|||
|
|
price_stability: priceStability,
|
|||
|
|
updated_at: new Date()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
logger.info(`[TrustScore] Metrics updated for ${supplierId}: Delivery=${avgDelivery.toFixed(1)}d, Stability=${priceStability.toFixed(2)}`);
|
|||
|
|
} catch (err: any) {
|
|||
|
|
// [CORE_DIAG_01] Agent 异常自省
|
|||
|
|
logger.error(`[TrustScore][WARN] Metrics collection failed: ${err.message}`);
|
|||
|
|
throw {
|
|||
|
|
category: 'Context Missing',
|
|||
|
|
rootCause: 'Insufficient order data for statistical analysis',
|
|||
|
|
mitigation: 'Wait for more orders or use industry benchmark defaults'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [BIZ_SUP_20] 供应商全链路信用与质量评分模型
|
|||
|
|
* @description 基于多维指标 (交期、质量、响应速度、价格稳定性) 自动计算信用分
|
|||
|
|
*/
|
|||
|
|
static async calculateSupplierScore(supplierId: string): Promise<number> {
|
|||
|
|
const supplier = await db(this.TABLE_NAME).where({ id: supplierId }).first();
|
|||
|
|
if (!supplier) return 0;
|
|||
|
|
|
|||
|
|
// 1. 交期维度 (权重 30%)
|
|||
|
|
const deliveryScore = Math.max(0, 100 - (supplier.avg_delivery_days * 5));
|
|||
|
|
|
|||
|
|
// 2. 质量维度 (权重 30%)
|
|||
|
|
const qualityScore = (1 - supplier.defect_rate) * 100;
|
|||
|
|
|
|||
|
|
// 3. 响应速度 (权重 20%)
|
|||
|
|
const responseScore = Math.max(0, 100 - (supplier.avg_response_hours * 5)); // 1小时 95, 2小时 90...
|
|||
|
|
|
|||
|
|
// 4. 价格稳定性 (权重 20%)
|
|||
|
|
const stabilityScore = supplier.price_stability * 100;
|
|||
|
|
|
|||
|
|
// 综合加权总分
|
|||
|
|
const finalScore = (deliveryScore * 0.3) + (qualityScore * 0.3) + (responseScore * 0.2) + (stabilityScore * 0.2);
|
|||
|
|
|
|||
|
|
await db(this.TABLE_NAME).where({ id: supplierId }).update({
|
|||
|
|
rating_score: finalScore,
|
|||
|
|
updated_at: new Date()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return finalScore;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [BIZ_SUP_20] AGI 驱动的供应商信用报告 (TrustReport)
|
|||
|
|
* @description 生成供应商信用深度分析,用于采购路由决策支持
|
|||
|
|
*/
|
|||
|
|
static async getSupplierTrustReport(supplierId: string): Promise<any> {
|
|||
|
|
const supplier = await db(this.TABLE_NAME).where({ id: supplierId }).first();
|
|||
|
|
if (!supplier) throw new Error('Supplier not found');
|
|||
|
|
|
|||
|
|
const score = await this.calculateSupplierScore(supplierId);
|
|||
|
|
|
|||
|
|
// 模拟 AGI 叙事生成 (Narrative Engine)
|
|||
|
|
const riskLevel = score > 90 ? 'LOW' : score > 70 ? 'MEDIUM' : 'HIGH';
|
|||
|
|
const narrative = `Supplier ${supplier.name} has a TrustScore of ${score.toFixed(2)}. ` +
|
|||
|
|
`Delivery performance is ${supplier.avg_delivery_days <= 3 ? 'EXCELLENT' : 'STABLE'}. ` +
|
|||
|
|
`Quality defect rate is ${(supplier.defect_rate * 100).toFixed(2)}%. ` +
|
|||
|
|
`Response time averages ${supplier.avg_response_hours} hours. ` +
|
|||
|
|
`Recommended Action: ${riskLevel === 'LOW' ? 'Whitelisted for Auto-PO' : 'Requires Human Review'}.`;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
supplierId: supplier.id,
|
|||
|
|
name: supplier.name,
|
|||
|
|
trustScore: score,
|
|||
|
|
riskLevel,
|
|||
|
|
narrative,
|
|||
|
|
metrics: {
|
|||
|
|
deliveryDays: supplier.avg_delivery_days,
|
|||
|
|
defectRate: supplier.defect_rate,
|
|||
|
|
responseHours: supplier.avg_response_hours,
|
|||
|
|
priceStability: supplier.price_stability
|
|||
|
|
},
|
|||
|
|
isFactory: supplier.isSourceFactory
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [BIZ_SUP_15] 推荐最优供应商
|
|||
|
|
*/
|
|||
|
|
static async recommendBestSupplier(productId: string, tenantId: string): Promise<string | null> {
|
|||
|
|
logger.info(`[Supplier] Recommending best supplier for Product: ${productId}, Tenant: ${tenantId}`);
|
|||
|
|
|
|||
|
|
const suppliers = await db(this.TABLE_NAME)
|
|||
|
|
.where({ tenant_id: tenantId })
|
|||
|
|
.orderBy('rating_score', 'desc')
|
|||
|
|
.limit(1);
|
|||
|
|
|
|||
|
|
return suppliers.length > 0 ? suppliers[0].id : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* [BIZ_AI_16-EXT] 更新供应商状态或执行切换建议
|
|||
|
|
*/
|
|||
|
|
static async updateSupplierStatus(supplierId: string, status: string, notes?: string): Promise<void> {
|
|||
|
|
logger.info(`[Supplier] Updating status for ${supplierId} to ${status}. Notes: ${notes}`);
|
|||
|
|
|
|||
|
|
await db(this.TABLE_NAME).where({ id: supplierId }).update({
|
|||
|
|
risk_level: status === 'BLOCK' ? 'HIGH' : 'LOW',
|
|||
|
|
legal_notices: notes,
|
|||
|
|
updated_at: new Date()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|