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

86 lines
3.2 KiB
TypeScript
Raw Normal View History

import db from '../config/database';
import { logger } from '../utils/logger';
import { DecisionExplainabilityEngine } from '../core/ai/DecisionExplainabilityEngine';
export interface SupplierRiskProfile {
supplierId: string;
name: string;
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
notices: string[];
lastScanAt: Date;
}
/**
* [BIZ_OPS_157] (/) (Supplier Risk Radar)
* @description cf_suppliers defect_rate
*/
export class SupplierRiskRadar {
/**
*
*/
static async scanRisks(tenantId: string): Promise<SupplierRiskProfile[]> {
logger.info(`[SupplierRisk] Scanning risks for tenant: ${tenantId}`);
try {
// 1. 获取该租户下绩效异常或有风险记录的供应商 (Zero-Mock Policy)
const highRiskSuppliers = await db('cf_suppliers')
.where('tenant_id', tenantId)
.where(function() {
this.where('risk_level', '!=', 'LOW')
.orWhere('defect_rate', '>', 0.05) // 质量异常
.orWhere('rating_score', '<', 60); // 评分极低
})
.select('id', 'name', 'risk_level', 'legal_notices', 'defect_rate', 'rating_score', 'updated_at');
const profiles: SupplierRiskProfile[] = [];
for (const supplier of highRiskSuppliers) {
const notices = supplier.legal_notices ? supplier.legal_notices.split('|') : [];
let riskLevel: SupplierRiskProfile['riskLevel'] = supplier.risk_level as any;
// 2. 联动分析:如果质量极差且有法务风险,升级为 CRITICAL
if (supplier.defect_rate > 0.1 && riskLevel === 'HIGH') {
riskLevel = 'CRITICAL';
}
if (riskLevel === 'CRITICAL' || riskLevel === 'HIGH') {
const reason = `Supplier ${supplier.name} identified as ${riskLevel} risk. ` +
`Quality Defect Rate: ${(supplier.defect_rate * 100).toFixed(2)}%. ` +
`Legal/Business Notices: ${notices.length} found.`;
// 3. [UX_XAI_01] 记录决策证据链
await DecisionExplainabilityEngine.logDecision({
tenantId,
module: 'SUPPLIER_RISK',
resourceId: supplier.id,
decisionType: 'SUPPLIER_REPLACEMENT_SUGGESTION',
causalChain: reason,
factors: [
{ name: 'DefectRate', value: supplier.defect_rate, weight: 0.5, impact: 'NEGATIVE' },
{ name: 'LegalRiskLevel', value: supplier.risk_level, weight: 0.5, impact: 'NEGATIVE' }
],
traceId: `risk-radar-${Date.now()}`
});
}
profiles.push({
supplierId: supplier.id,
name: supplier.name,
riskLevel,
notices,
lastScanAt: supplier.updated_at
});
}
return profiles;
} catch (err: any) {
logger.error(`[SupplierRisk][FATAL] Scan failed: ${err.message}`);
throw {
category: 'Logic Conflict',
rootCause: 'Failed to access cf_suppliers for risk radar',
mitigation: 'Check cf_suppliers schema and connectivity'
};
}
}
}