Files
makemd/server/src/services/SupplierService.ts

245 lines
9.4 KiB
TypeScript
Raw Normal View History

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()
});
}
}