fengling-risk-control/Fengling.RiskControl.Client/Evaluation/RiskEvaluator.cs
Sam 293209b1dc feat: add Fengling.RiskControl.Client SDK
- Implement RedisCounterService for rate limiting
- Implement RuleLoader with timer refresh
- Implement RiskEvaluator for local rule evaluation
- Implement SamplingService for CAP events
- Implement CapEventPublisher for async event publishing
- Implement FailoverStrategy for Redis failure handling
- Add configuration classes and DI extensions
- Add unit tests (9 tests)
- Add NuGet publishing script
2026-02-06 00:16:53 +08:00

229 lines
7.2 KiB
C#

using Fengling.RiskControl.Domain.Aggregates.RiskRules;
using Fengling.RiskControl.Configuration;
using Fengling.RiskControl.Counter;
using Fengling.RiskControl.Rules;
using Microsoft.Extensions.Logging;
namespace Fengling.RiskControl.Evaluation;
public interface IRiskEvaluator
{
Task<RiskEvaluationResult> EvaluateAsync(RiskEvaluationRequest request);
Task<bool> IsAllowedAsync(RiskEvaluationRequest request);
}
public class RiskEvaluator : IRiskEvaluator
{
private readonly IRuleLoader _ruleLoader;
private readonly IRiskCounterService _counterService;
private readonly RiskControlClientOptions _options;
private readonly ILogger<RiskEvaluator> _logger;
public RiskEvaluator(
IRuleLoader ruleLoader,
IRiskCounterService counterService,
RiskControlClientOptions options,
ILogger<RiskEvaluator> logger)
{
_ruleLoader = ruleLoader;
_counterService = counterService;
_options = options;
_logger = logger;
}
public async Task<RiskEvaluationResult> EvaluateAsync(RiskEvaluationRequest request)
{
var rules = await _ruleLoader.GetActiveRulesAsync();
var factors = new List<RiskFactorResult>();
var totalScore = 0;
foreach (var rule in rules)
{
var factor = await EvaluateRuleAsync(rule, request);
if (factor != null)
{
factors.Add(factor);
totalScore += factor.Points;
}
}
var riskLevel = DetermineRiskLevel(totalScore);
var recommendedAction = DetermineAction(riskLevel);
var blocked = recommendedAction == RiskRuleAction.Block;
return new RiskEvaluationResult
{
TotalScore = totalScore,
RiskLevel = riskLevel,
RecommendedAction = recommendedAction,
Blocked = blocked,
Message = blocked ? "操作被风险控制系统拒绝" : "操作已通过风险评估",
Factors = factors,
EvaluatedAt = DateTime.UtcNow
};
}
public Task<bool> IsAllowedAsync(RiskEvaluationRequest request)
{
return EvaluateAsync(request).ContinueWith(t => !t.Result.Blocked);
}
private async Task<RiskFactorResult?> EvaluateRuleAsync(RiskRule rule, RiskEvaluationRequest request)
{
return rule.RuleType switch
{
RiskRuleType.FrequencyLimit => await EvaluateFrequencyLimitAsync(rule, request),
RiskRuleType.AmountLimit => EvaluateAmountLimit(rule, request),
RiskRuleType.Blacklist => EvaluateBlacklist(rule, request),
RiskRuleType.DeviceFingerprint => EvaluateDeviceFingerprint(rule, request),
RiskRuleType.VelocityCheck => EvaluateVelocityCheck(rule, request),
_ => null
};
}
private async Task<RiskFactorResult?> EvaluateFrequencyLimitAsync(RiskRule rule, RiskEvaluationRequest request)
{
var config = rule.GetConfig<FrequencyLimitConfig>();
if (config == null)
{
_logger.LogWarning("Rule {RuleId} has invalid FrequencyLimitConfig", rule.Id);
return null;
}
var metricKey = $"{request.EventType.ToLower()}_count";
var currentCount = await _counterService.GetValueAsync(request.MemberId, metricKey);
var limit = config.MaxCount;
if (currentCount >= limit)
{
return new RiskFactorResult
{
RuleName = rule.Name,
RuleType = rule.RuleType.ToString(),
Points = rule.Priority,
Detail = $"已超过{request.EventType}次数限制({currentCount}/{limit})"
};
}
await _counterService.IncrementAsync(request.MemberId, metricKey);
return null;
}
private RiskFactorResult? EvaluateAmountLimit(RiskRule rule, RiskEvaluationRequest request)
{
if (!request.Amount.HasValue)
return null;
var config = rule.GetConfig<AmountLimitConfig>();
if (config == null)
return null;
var metricKey = $"{request.EventType.ToLower()}_amount";
var currentAmount = _counterService.GetValueAsync(request.MemberId, metricKey).GetAwaiter().GetResult();
if (currentAmount + request.Amount.Value > config.MaxAmount)
{
return new RiskFactorResult
{
RuleName = rule.Name,
RuleType = rule.RuleType.ToString(),
Points = rule.Priority,
Detail = $"已超过{request.EventType}金额限制({currentAmount + request.Amount.Value}/{config.MaxAmount})"
};
}
_counterService.IncrementAsync(request.MemberId, metricKey, request.Amount.Value).GetAwaiter().GetResult();
return null;
}
private RiskFactorResult? EvaluateBlacklist(RiskRule rule, RiskEvaluationRequest request)
{
var config = rule.GetConfig<BlacklistConfig>();
if (config == null)
return null;
var memberValues = _counterService.GetAllValuesAsync(request.MemberId).GetAwaiter().GetResult();
if (memberValues.TryGetValue("is_blacklisted", out var isBlacklisted) && isBlacklisted == 1)
{
return new RiskFactorResult
{
RuleName = rule.Name,
RuleType = rule.RuleType.ToString(),
Points = rule.Priority,
Detail = "用户已被加入黑名单"
};
}
return null;
}
private RiskFactorResult? EvaluateDeviceFingerprint(RiskRule rule, RiskEvaluationRequest request)
{
if (string.IsNullOrEmpty(request.DeviceFingerprint))
return null;
var config = rule.GetConfig<DeviceFingerprintConfig>();
if (config == null)
return null;
return null;
}
private RiskFactorResult? EvaluateVelocityCheck(RiskRule rule, RiskEvaluationRequest request)
{
var config = rule.GetConfig<VelocityCheckConfig>();
if (config == null)
return null;
return null;
}
private RiskLevel DetermineRiskLevel(int totalScore)
{
if (totalScore >= _options.Evaluation.HighRiskThreshold)
return RiskLevel.High;
if (totalScore >= _options.Evaluation.MediumRiskThreshold)
return RiskLevel.Medium;
return RiskLevel.Low;
}
private RiskRuleAction DetermineAction(RiskLevel riskLevel)
{
return riskLevel switch
{
RiskLevel.High => RiskRuleAction.Block,
RiskLevel.Medium => RiskRuleAction.RequireVerification,
_ => RiskRuleAction.Allow
};
}
}
public class FrequencyLimitConfig
{
public int MaxCount { get; set; } = 10;
public string TimeWindow { get; set; } = "Day";
}
public class AmountLimitConfig
{
public int MaxAmount { get; set; } = 1000;
}
public class BlacklistConfig
{
public List<string> BlockedMembers { get; set; } = new();
}
public class DeviceFingerprintConfig
{
public int MaxAccountsPerDevice { get; set; } = 3;
}
public class VelocityCheckConfig
{
public int MaxRequestsPerMinute { get; set; } = 100;
}