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

627 lines
21 KiB
TypeScript
Raw Normal View History

import { logger } from '../utils/logger';
import BlacklistDatabaseService from './BlacklistDatabaseService';
import BlacklistService from './BlacklistService';
export interface BuyerBehavior {
buyer_id: string;
platform: string;
platform_buyer_id: string;
order_count: number;
return_rate: number;
chargeback_rate: number;
complaint_rate: number;
refund_rate: number;
average_order_value: number;
purchase_frequency: number;
review_score: number;
suspicious_behavior: boolean;
abnormal_activity: boolean;
location_mismatch: boolean;
payment_issues: number;
account_age_days: number;
device_changes: number;
ip_changes: number;
velocity_score: number;
risk_indicators: string[];
last_purchase_date: Date;
first_purchase_date: Date;
}
export interface RiskAssessmentResult {
buyer_id: string;
platform: string;
platform_buyer_id: string;
risk_score: number;
risk_level: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
risk_factors: string[];
recommendations: string[];
assessment_date: Date;
is_blacklisted: boolean;
blacklist_reasons?: string[];
confidence_score: number;
}
export interface RiskRule {
id: string;
name: string;
description: string;
condition: string;
weight: number;
threshold: number;
action: 'ALERT' | 'BLOCK' | 'MONITOR';
enabled: boolean;
created_by: string;
created_at: Date;
updated_at: Date;
}
export interface RiskStatistics {
total_assessments: number;
high_risk_count: number;
medium_risk_count: number;
low_risk_count: number;
critical_risk_count: number;
blacklist_conversion_rate: number;
false_positives: number;
false_negatives: number;
by_platform: Record<string, number>;
by_risk_level: Record<string, number>;
average_risk_score: number;
risk_trend: {
date: string;
average_score: number;
}[];
}
export interface RiskAssessmentRequest {
tenant_id: string;
shop_id: string;
task_id?: string;
trace_id: string;
buyer_behavior: BuyerBehavior;
include_blacklist_check: boolean;
assessment_reason: string;
}
export default class RiskAssessmentService {
private static instance: RiskAssessmentService;
private blacklistService: BlacklistService;
static getInstance(): RiskAssessmentService {
if (!RiskAssessmentService.instance) {
RiskAssessmentService.instance = new RiskAssessmentService();
}
return RiskAssessmentService.instance;
}
constructor() {
this.blacklistService = BlacklistService.getInstance();
}
async initTables(): Promise<void> {
// 初始化风险规则表
const hasRiskRulesTable = await db.schema.hasTable('cf_risk_rule');
if (!hasRiskRulesTable) {
logger.info('[RiskAssessmentService] Creating cf_risk_rule table...');
await db.schema.createTable('cf_risk_rule', (table) => {
table.string('id', 36).primary();
table.string('name', 255).notNullable();
table.text('description').notNullable();
table.text('condition').notNullable();
table.decimal('weight', 5, 2).notNullable();
table.decimal('threshold', 5, 2).notNullable();
table.enum('action', ['ALERT', 'BLOCK', 'MONITOR']).notNullable();
table.boolean('enabled').notNullable().defaultTo(true);
table.string('created_by', 64).notNullable();
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
});
}
// 初始化风险评估记录表
const hasRiskAssessmentsTable = await db.schema.hasTable('cf_risk_assessment');
if (!hasRiskAssessmentsTable) {
logger.info('[RiskAssessmentService] Creating cf_risk_assessment table...');
await db.schema.createTable('cf_risk_assessment', (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 64).notNullable().index();
table.string('shop_id', 64).notNullable();
table.string('task_id', 36);
table.string('trace_id', 64).notNullable();
table.string('buyer_id', 64).notNullable();
table.string('platform', 64).notNullable().index();
table.string('platform_buyer_id', 64).notNullable().index();
table.decimal('risk_score', 5, 2).notNullable().index();
table.enum('risk_level', ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).notNullable().index();
table.json('risk_factors').notNullable();
table.json('recommendations').notNullable();
table.boolean('is_blacklisted').notNullable();
table.json('blacklist_reasons');
table.decimal('confidence_score', 5, 2).notNullable();
table.text('assessment_reason').notNullable();
table.json('buyer_behavior').notNullable();
table.datetime('assessment_date').notNullable().defaultTo(db.fn.now());
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
}
}
async assessRisk(request: RiskAssessmentRequest): Promise<RiskAssessmentResult> {
try {
logger.info(`[RiskAssessmentService] Assessing risk for buyer: ${request.buyer_behavior.platform_buyer_id}, platform=${request.buyer_behavior.platform}, traceId=${request.trace_id}`);
// 计算基础风险分数
let riskScore = this.calculateBaseRiskScore(request.buyer_behavior);
// 应用风险规则
const { adjustedScore, riskFactors } = await this.applyRiskRules(request.buyer_behavior, riskScore);
riskScore = adjustedScore;
// 检查黑名单状态
let isBlacklisted = false;
let blacklistReasons: string[] = [];
if (request.include_blacklist_check) {
const blacklistCheck = await this.blacklistService.checkBlacklist(
request.buyer_behavior.platform,
request.buyer_behavior.platform_buyer_id,
request.tenant_id
);
isBlacklisted = blacklistCheck.is_blacklisted;
blacklistReasons = blacklistCheck.reasons || [];
// 如果在黑名单中,提高风险分数
if (isBlacklisted) {
riskScore = Math.min(100, riskScore + 30);
riskFactors.push('Buyer is in blacklist');
}
}
// 确定风险等级
const riskLevel = this.calculateRiskLevel(riskScore);
// 生成建议
const recommendations = this.generateRecommendations(riskLevel, riskFactors, isBlacklisted);
// 计算置信度分数
const confidenceScore = this.calculateConfidenceScore(request.buyer_behavior, riskFactors.length);
// 保存评估记录
await this.saveAssessmentRecord({
...request,
risk_score: riskScore,
risk_level: riskLevel,
risk_factors: riskFactors,
recommendations: recommendations,
is_blacklisted: isBlacklisted,
blacklist_reasons: blacklistReasons,
confidence_score: confidenceScore
});
const result: RiskAssessmentResult = {
buyer_id: request.buyer_behavior.buyer_id,
platform: request.buyer_behavior.platform,
platform_buyer_id: request.buyer_behavior.platform_buyer_id,
risk_score: riskScore,
risk_level: riskLevel,
risk_factors: riskFactors,
recommendations: recommendations,
assessment_date: new Date(),
is_blacklisted: isBlacklisted,
blacklist_reasons: blacklistReasons,
confidence_score: confidenceScore
};
logger.info(`[RiskAssessmentService] Risk assessment completed: score=${riskScore}, level=${riskLevel}, confidence=${confidenceScore}, traceId=${request.trace_id}`);
return result;
} catch (error: any) {
logger.error(`[RiskAssessmentService] Failed to assess risk: ${error.message}, traceId=${request.trace_id}`);
throw error;
}
}
private calculateBaseRiskScore(behavior: BuyerBehavior): number {
let score = 0;
// 退货率 (0-20分)
score += Math.min(20, behavior.return_rate * 20);
// 退款率 (0-20分)
score += Math.min(20, behavior.refund_rate * 20);
// 拒付率 (0-25分)
score += Math.min(25, behavior.chargeback_rate * 25);
// 投诉率 (0-15分)
score += Math.min(15, behavior.complaint_rate * 15);
// 可疑行为 (0-10分)
if (behavior.suspicious_behavior) score += 10;
// 异常活动 (0-10分)
if (behavior.abnormal_activity) score += 10;
// 位置不匹配 (0-8分)
if (behavior.location_mismatch) score += 8;
// 支付问题 (0-5分 per issue)
score += Math.min(15, behavior.payment_issues * 5);
// 设备变更 (0-5分 per change)
score += Math.min(10, behavior.device_changes * 2);
// IP变更 (0-5分 per change)
score += Math.min(10, behavior.ip_changes * 2);
// 速度评分 (0-10分)
score += Math.min(10, behavior.velocity_score);
// 账户年龄 (新账户风险更高)
if (behavior.account_age_days < 30) score += 10;
else if (behavior.account_age_days < 90) score += 5;
return Math.min(100, score);
}
private async applyRiskRules(behavior: BuyerBehavior, baseScore: number): Promise<{ adjustedScore: number; riskFactors: string[] }> {
const riskFactors: string[] = [];
let adjustedScore = baseScore;
// 获取启用的风险规则
const rules = await db('cf_risk_rule').where({ enabled: true });
for (const rule of rules) {
try {
// 简单的规则评估逻辑
if (this.evaluateRule(rule, behavior)) {
adjustedScore = Math.min(100, adjustedScore + rule.weight);
riskFactors.push(rule.name);
}
} catch (error) {
logger.warn(`[RiskAssessmentService] Failed to evaluate rule ${rule.name}: ${error}`);
}
}
return { adjustedScore, riskFactors };
}
private evaluateRule(rule: any, behavior: BuyerBehavior): boolean {
// 简单的规则评估实现
// 实际应用中可能需要更复杂的规则引擎
switch (rule.name) {
case 'High Return Rate':
return behavior.return_rate > 0.3;
case 'Chargeback History':
return behavior.chargeback_rate > 0;
case 'Suspicious Activity':
return behavior.suspicious_behavior;
case 'New Account':
return behavior.account_age_days < 30;
case 'Multiple Device Changes':
return behavior.device_changes > 3;
case 'Multiple IP Changes':
return behavior.ip_changes > 3;
default:
return false;
}
}
private calculateRiskLevel(score: number): 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' {
if (score >= 80) return 'CRITICAL';
if (score >= 60) return 'HIGH';
if (score >= 30) return 'MEDIUM';
return 'LOW';
}
private generateRecommendations(riskLevel: string, riskFactors: string[], isBlacklisted: boolean): string[] {
const recommendations: string[] = [];
switch (riskLevel) {
case 'CRITICAL':
recommendations.push('Block buyer immediately');
recommendations.push('Add to blacklist');
recommendations.push('Review all past transactions');
recommendations.push('Consider legal action if fraud is suspected');
break;
case 'HIGH':
recommendations.push('Place order on hold for manual review');
recommendations.push('Request additional verification');
recommendations.push('Limit order amount');
recommendations.push('Monitor future transactions closely');
break;
case 'MEDIUM':
recommendations.push('Monitor transaction closely');
recommendations.push('Set lower order limits');
recommendations.push('Require signature for delivery');
break;
case 'LOW':
recommendations.push('Process order normally');
recommendations.push('Continue monitoring');
break;
}
if (isBlacklisted) {
recommendations.push('Review blacklist entry details');
recommendations.push('Consider permanent block if repeat offender');
}
if (riskFactors.includes('High Return Rate')) {
recommendations.push('Consider restocking fees');
recommendations.push('Review return policy compliance');
}
if (riskFactors.includes('Chargeback History')) {
recommendations.push('Use secure payment methods');
recommendations.push('Keep detailed transaction records');
}
return recommendations;
}
private calculateConfidenceScore(behavior: BuyerBehavior, riskFactorCount: number): number {
let confidence = 50; // 基础置信度
// 数据完整性提高置信度
if (behavior.order_count > 0) confidence += 10;
if (behavior.account_age_days > 30) confidence += 10;
if (riskFactorCount > 0) confidence += 15;
if (behavior.review_score > 0) confidence += 5;
// 数据不足降低置信度
if (behavior.order_count === 0) confidence -= 20;
if (behavior.account_age_days < 7) confidence -= 15;
return Math.max(10, Math.min(95, confidence));
}
private async saveAssessmentRecord(data: any): Promise<void> {
const id = uuidv4();
await db('cf_risk_assessment').insert({
id,
tenant_id: data.tenant_id,
shop_id: data.shop_id,
task_id: data.task_id,
trace_id: data.trace_id,
buyer_id: data.buyer_behavior.buyer_id,
platform: data.buyer_behavior.platform,
platform_buyer_id: data.buyer_behavior.platform_buyer_id,
risk_score: data.risk_score,
risk_level: data.risk_level,
risk_factors: data.risk_factors,
recommendations: data.recommendations,
is_blacklisted: data.is_blacklisted,
blacklist_reasons: data.blacklist_reasons,
confidence_score: data.confidence_score,
assessment_reason: data.assessment_reason,
buyer_behavior: data.buyer_behavior,
assessment_date: new Date(),
created_at: new Date()
});
}
async getRiskAssessments(tenantId: string, filter?: {
platform?: string;
risk_level?: string;
start_date?: Date;
end_date?: Date;
limit?: number;
offset?: number;
}): Promise<{
assessments: any[];
total: number;
}> {
try {
let query = db('cf_risk_assessment').where({ tenant_id: tenantId });
if (filter) {
if (filter.platform) {
query = query.where({ platform: filter.platform });
}
if (filter.risk_level) {
query = query.where({ risk_level: filter.risk_level });
}
if (filter.start_date) {
query = query.where('assessment_date', '>=', filter.start_date);
}
if (filter.end_date) {
query = query.where('assessment_date', '<=', filter.end_date);
}
}
const total = await query.count('id as count').first();
const assessments = await query
.orderBy('assessment_date', 'desc')
.limit(filter?.limit || 100)
.offset(filter?.offset || 0);
return {
assessments,
total: total ? parseInt(total.count as string) : 0
};
} catch (error: any) {
logger.error(`[RiskAssessmentService] Failed to get risk assessments: ${error.message}`);
return { assessments: [], total: 0 };
}
}
async getRiskStatistics(tenantId: string, days: number = 30): Promise<RiskStatistics> {
try {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const totalAssessments = await db('cf_risk_assessment')
.where({ tenant_id: tenantId })
.where('assessment_date', '>=', startDate)
.count('id as count')
.first();
const highRiskCount = await db('cf_risk_assessment')
.where({ tenant_id: tenantId, risk_level: 'HIGH' })
.where('assessment_date', '>=', startDate)
.count('id as count')
.first();
const mediumRiskCount = await db('cf_risk_assessment')
.where({ tenant_id: tenantId, risk_level: 'MEDIUM' })
.where('assessment_date', '>=', startDate)
.count('id as count')
.first();
const lowRiskCount = await db('cf_risk_assessment')
.where({ tenant_id: tenantId, risk_level: 'LOW' })
.where('assessment_date', '>=', startDate)
.count('id as count')
.first();
const criticalRiskCount = await db('cf_risk_assessment')
.where({ tenant_id: tenantId, risk_level: 'CRITICAL' })
.where('assessment_date', '>=', startDate)
.count('id as count')
.first();
const blacklistConversion = await db('cf_risk_assessment')
.where({ tenant_id: tenantId, is_blacklisted: true })
.where('assessment_date', '>=', startDate)
.count('id as count')
.first();
const byPlatform = await db('cf_risk_assessment')
.where({ tenant_id: tenantId })
.where('assessment_date', '>=', startDate)
.select('platform', db.raw('count(*) as count'))
.groupBy('platform');
const byRiskLevel = await db('cf_risk_assessment')
.where({ tenant_id: tenantId })
.where('assessment_date', '>=', startDate)
.select('risk_level', db.raw('count(*) as count'))
.groupBy('risk_level');
const averageScore = await db('cf_risk_assessment')
.where({ tenant_id: tenantId })
.where('assessment_date', '>=', startDate)
.avg('risk_score as avg_score')
.first();
// 生成风险趋势数据
const riskTrend = [];
for (let i = days - 1; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const dayStart = new Date(date);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(date);
dayEnd.setHours(23, 59, 59, 999);
const dayAverage = await db('cf_risk_assessment')
.where({ tenant_id: tenantId })
.whereBetween('assessment_date', [dayStart, dayEnd])
.avg('risk_score as avg_score')
.first();
riskTrend.push({
date: dateStr,
average_score: dayAverage?.avg_score ? parseFloat(dayAverage.avg_score as string) : 0
});
}
const total = totalAssessments ? parseInt(totalAssessments.count as string) : 0;
const blacklistCount = blacklistConversion ? parseInt(blacklistConversion.count as string) : 0;
return {
total_assessments: total,
high_risk_count: highRiskCount ? parseInt(highRiskCount.count as string) : 0,
medium_risk_count: mediumRiskCount ? parseInt(mediumRiskCount.count as string) : 0,
low_risk_count: lowRiskCount ? parseInt(lowRiskCount.count as string) : 0,
critical_risk_count: criticalRiskCount ? parseInt(criticalRiskCount.count as string) : 0,
blacklist_conversion_rate: total > 0 ? (blacklistCount / total) * 100 : 0,
false_positives: 0, // 需要额外逻辑计算
false_negatives: 0, // 需要额外逻辑计算
by_platform: byPlatform.reduce((acc, item) => {
acc[item.platform] = parseInt(item.count as string);
return acc;
}, {} as Record<string, number>),
by_risk_level: byRiskLevel.reduce((acc, item) => {
acc[item.risk_level] = parseInt(item.count as string);
return acc;
}, {} as Record<string, number>),
average_risk_score: averageScore?.avg_score ? parseFloat(averageScore.avg_score as string) : 0,
risk_trend: riskTrend
};
} catch (error: any) {
logger.error(`[RiskAssessmentService] Failed to get risk statistics: ${error.message}`);
return {
total_assessments: 0,
high_risk_count: 0,
medium_risk_count: 0,
low_risk_count: 0,
critical_risk_count: 0,
blacklist_conversion_rate: 0,
false_positives: 0,
false_negatives: 0,
by_platform: {},
by_risk_level: {},
average_risk_score: 0,
risk_trend: []
};
}
}
async createRiskRule(rule: Omit<RiskRule, 'id' | 'created_at' | 'updated_at'>): Promise<RiskRule> {
try {
const id = uuidv4();
const now = new Date();
const newRule: RiskRule = {
...rule,
id,
created_at: now,
updated_at: now
};
await db('cf_risk_rule').insert(newRule);
logger.info(`[RiskAssessmentService] Risk rule created: id=${id}, name=${rule.name}`);
return newRule;
} catch (error: any) {
logger.error(`[RiskAssessmentService] Failed to create risk rule: ${error.message}`);
throw error;
}
}
async updateRiskRule(id: string, updates: Partial<RiskRule>): Promise<boolean> {
try {
const result = await db('cf_risk_rule')
.where({ id })
.update({
...updates,
updated_at: new Date()
});
if (result > 0) {
logger.info(`[RiskAssessmentService] Risk rule updated: id=${id}`);
}
return result > 0;
} catch (error: any) {
logger.error(`[RiskAssessmentService] Failed to update risk rule: ${error.message}`);
return false;
}
}
async getRiskRules(): Promise<RiskRule[]> {
try {
const rules = await db('cf_risk_rule').where({ enabled: true }).orderBy('weight', 'desc');
return rules as RiskRule[];
} catch (error: any) {
logger.error(`[RiskAssessmentService] Failed to get risk rules: ${error.message}`);
return [];
}
}
}
// 导入必要的依赖
import db from '../config/database';
import { v4 as uuidv4 } from 'uuid';