- 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
229 lines
7.2 KiB
C#
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;
|
|
}
|