Compare commits
No commits in common. "2ce392ab3323a4dfb29a28b07d31fef8e8eb94c5" and "d6f5c00554f85863d2f12728372a7de481b218a4" have entirely different histories.
2ce392ab33
...
d6f5c00554
@ -1,35 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NetCorePalVersion>3.2.1</NetCorePalVersion>
|
|
||||||
<FastEndpointsVersion>7.1.1</FastEndpointsVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
|
|
||||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
|
||||||
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" Version="$(NetCorePalVersion)" />
|
|
||||||
<PackageVersion Include="MediatR" Version="12.5.0" />
|
|
||||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.1" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
|
||||||
<PackageVersion Include="StackExchange.Redis" Version="2.7.33" />
|
|
||||||
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
|
|
||||||
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
|
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
|
||||||
<PackageVersion Include="DotNetCore.CAP" Version="8.4.1" />
|
|
||||||
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
|
|
||||||
<PackageVersion Include="NetCorePal.Extensions.Domain.Abstractions" Version="$(NetCorePalVersion)" />
|
|
||||||
<PackageVersion Include="NetCorePal.Extensions.Primitives" Version="$(NetCorePalVersion)" />
|
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
|
||||||
<PackageVersion Include="FluentAssertions" Version="6.12.2" />
|
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
|
|
||||||
<PackageVersion Include="NSubstitute" Version="5.3.0" />
|
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MediatR" />
|
||||||
|
<PackageReference Include="FluentValidation.AspNetCore" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Fengling.RiskControl.Domain\Fengling.RiskControl.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\Fengling.RiskControl.Infrastructure\Fengling.RiskControl.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@ -1,30 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Configuration;
|
|
||||||
|
|
||||||
public static class RiskControlClientExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddRiskControlClient(
|
|
||||||
this IServiceCollection services,
|
|
||||||
Action<RiskControlClientOptions> configureOptions)
|
|
||||||
{
|
|
||||||
services.Configure(configureOptions);
|
|
||||||
services.AddSingleton<IValidateOptions<RiskControlClientOptions>, RiskControlClientOptionsValidator>();
|
|
||||||
services.AddSingleton<RiskControlClientOptions>(sp =>
|
|
||||||
sp.GetRequiredService<IOptions<RiskControlClientOptions>>().Value);
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskControlClientOptionsValidator : IValidateOptions<RiskControlClientOptions>
|
|
||||||
{
|
|
||||||
public ValidateOptionsResult Validate(string? name, RiskControlClientOptions options)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(options.Redis.ConnectionString))
|
|
||||||
{
|
|
||||||
return ValidateOptionsResult.Fail("Redis.ConnectionString is required");
|
|
||||||
}
|
|
||||||
return ValidateOptionsResult.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
namespace Fengling.RiskControl.Configuration;
|
|
||||||
|
|
||||||
public class RiskControlClientOptions
|
|
||||||
{
|
|
||||||
public RedisOptions Redis { get; set; } = new();
|
|
||||||
public EvaluationOptions Evaluation { get; set; } = new();
|
|
||||||
public CapOptions Cap { get; set; } = new();
|
|
||||||
public SamplingOptions Sampling { get; set; } = new();
|
|
||||||
public FailoverOptions RedisFailover { get; set; } = new();
|
|
||||||
public ReconciliationOptions Reconciliation { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RedisOptions
|
|
||||||
{
|
|
||||||
public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542";
|
|
||||||
public string KeyPrefix { get; set; } = "fengling:risk:";
|
|
||||||
public int DefaultTtlSeconds { get; set; } = 7200;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EvaluationOptions
|
|
||||||
{
|
|
||||||
public int RuleRefreshIntervalSeconds { get; set; } = 30;
|
|
||||||
public int HighRiskThreshold { get; set; } = 70;
|
|
||||||
public int MediumRiskThreshold { get; set; } = 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CapOptions
|
|
||||||
{
|
|
||||||
public bool PublisherEnabled { get; set; } = true;
|
|
||||||
public string ConnectionName { get; set; } = "Fengling.RiskControl";
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SamplingOptions
|
|
||||||
{
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
public double DefaultRate { get; set; } = 0.01;
|
|
||||||
public List<SamplingRule> Rules { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SamplingRule
|
|
||||||
{
|
|
||||||
public string Condition { get; set; } = string.Empty;
|
|
||||||
public double Rate { get; set; } = 0.01;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FailoverOptions
|
|
||||||
{
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
public int QuickFailThresholdSeconds { get; set; } = 5;
|
|
||||||
public int DenyNewUsersThresholdSeconds { get; set; } = 30;
|
|
||||||
public string Strategy { get; set; } = "DenyNewUsers";
|
|
||||||
public bool FallbackToRulesOnly { get; set; } = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ReconciliationOptions
|
|
||||||
{
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
public string Schedule { get; set; } = "0 4 * * *";
|
|
||||||
public int Threshold { get; set; } = 5;
|
|
||||||
public int BatchSize { get; set; } = 1000;
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
|
||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Counter;
|
|
||||||
|
|
||||||
public interface IRiskCounterService
|
|
||||||
{
|
|
||||||
Task<long> IncrementAsync(string memberId, string metric, int value = 1);
|
|
||||||
Task<int> GetValueAsync(string memberId, string metric);
|
|
||||||
Task<Dictionary<string, int>> GetAllValuesAsync(string memberId);
|
|
||||||
Task<bool> SetValueAsync(string memberId, string metric, int value);
|
|
||||||
Task RefreshTtlAsync(string memberId);
|
|
||||||
Task<bool> ExistsAsync(string memberId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RedisCounterService : IRiskCounterService
|
|
||||||
{
|
|
||||||
private readonly IConnectionMultiplexer _redis;
|
|
||||||
private readonly RiskControlClientOptions _options;
|
|
||||||
private readonly ILogger<RedisCounterService> _logger;
|
|
||||||
|
|
||||||
private string MemberKey(string memberId) => $"{_options.Redis.KeyPrefix}{memberId}";
|
|
||||||
|
|
||||||
public RedisCounterService(
|
|
||||||
IConnectionMultiplexer redis,
|
|
||||||
RiskControlClientOptions options,
|
|
||||||
ILogger<RedisCounterService> logger)
|
|
||||||
{
|
|
||||||
_redis = redis;
|
|
||||||
_options = options;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<long> IncrementAsync(string memberId, string metric, int value = 1)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = MemberKey(memberId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await db.HashIncrementAsync(key, metric, value);
|
|
||||||
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Redis increment failed for member {MemberId}, metric {Metric}", memberId, metric);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetValueAsync(string memberId, string metric)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = MemberKey(memberId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = await db.HashGetAsync(key, metric);
|
|
||||||
return value.HasValue ? int.Parse(value) : 0;
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Redis get failed for member {MemberId}, metric {Metric}", memberId, metric);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, int>> GetAllValuesAsync(string memberId)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = MemberKey(memberId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var entries = await db.HashGetAllAsync(key);
|
|
||||||
return entries
|
|
||||||
.Where(e => e.Name != "_ttl")
|
|
||||||
.ToDictionary(
|
|
||||||
e => e.Name.ToString(),
|
|
||||||
e => int.Parse(e.Value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Redis get all failed for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SetValueAsync(string memberId, string metric, int value)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = MemberKey(memberId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await db.HashSetAsync(key, metric, value);
|
|
||||||
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Redis set failed for member {MemberId}, metric {Metric}", memberId, metric);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshTtlAsync(string memberId)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = MemberKey(memberId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds));
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Redis TTL refresh failed for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ExistsAsync(string memberId)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = MemberKey(memberId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await db.KeyExistsAsync(key);
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Redis exists check failed for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Evaluation;
|
|
||||||
|
|
||||||
public class RiskEvaluationRequest
|
|
||||||
{
|
|
||||||
public string MemberId { get; set; } = string.Empty;
|
|
||||||
public string EventType { get; set; } = string.Empty;
|
|
||||||
public int? Amount { get; set; }
|
|
||||||
public string? DeviceFingerprint { get; set; }
|
|
||||||
public string? IpAddress { get; set; }
|
|
||||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskEvaluationResult
|
|
||||||
{
|
|
||||||
public int TotalScore { get; set; }
|
|
||||||
public RiskLevel RiskLevel { get; set; }
|
|
||||||
public RiskRuleAction RecommendedAction { get; set; }
|
|
||||||
public bool Blocked { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public List<RiskFactorResult> Factors { get; set; } = new();
|
|
||||||
public DateTime EvaluatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskFactorResult
|
|
||||||
{
|
|
||||||
public string RuleName { get; set; } = string.Empty;
|
|
||||||
public string RuleType { get; set; } = string.Empty;
|
|
||||||
public int Points { get; set; }
|
|
||||||
public string? Detail { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum RiskLevel
|
|
||||||
{
|
|
||||||
Low = 0,
|
|
||||||
Medium = 1,
|
|
||||||
High = 2
|
|
||||||
}
|
|
||||||
@ -1,228 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
using DotNetCore.CAP;
|
|
||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Fengling.RiskControl.Evaluation;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Events;
|
|
||||||
|
|
||||||
public interface IRiskEventPublisher
|
|
||||||
{
|
|
||||||
Task PublishRiskAssessmentAsync(RiskEvaluationRequest request, RiskEvaluationResult result);
|
|
||||||
Task PublishRiskAlertAsync(RiskEvaluationRequest request, RiskEvaluationResult result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CapEventPublisher : IRiskEventPublisher
|
|
||||||
{
|
|
||||||
private readonly ICapPublisher _capPublisher;
|
|
||||||
private readonly RiskControlClientOptions _options;
|
|
||||||
private readonly ILogger<CapEventPublisher> _logger;
|
|
||||||
|
|
||||||
public CapEventPublisher(
|
|
||||||
ICapPublisher capPublisher,
|
|
||||||
RiskControlClientOptions options,
|
|
||||||
ILogger<CapEventPublisher> logger)
|
|
||||||
{
|
|
||||||
_capPublisher = capPublisher;
|
|
||||||
_options = options;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PublishRiskAssessmentAsync(RiskEvaluationRequest request, RiskEvaluationResult result)
|
|
||||||
{
|
|
||||||
if (!_options.Cap.PublisherEnabled)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("CAP publisher is disabled, skipping event publish");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var @event = new RiskAssessmentEvent
|
|
||||||
{
|
|
||||||
MemberId = request.MemberId,
|
|
||||||
EventType = request.EventType,
|
|
||||||
Amount = request.Amount,
|
|
||||||
DeviceFingerprint = request.DeviceFingerprint,
|
|
||||||
IpAddress = request.IpAddress,
|
|
||||||
TotalScore = result.TotalScore,
|
|
||||||
RiskLevel = result.RiskLevel.ToString(),
|
|
||||||
Blocked = result.Blocked,
|
|
||||||
Factors = result.Factors,
|
|
||||||
EvaluatedAt = result.EvaluatedAt
|
|
||||||
};
|
|
||||||
|
|
||||||
await _capPublisher.PublishAsync("fengling.risk.assessed", @event);
|
|
||||||
_logger.LogDebug("Published RiskAssessmentEvent for member {MemberId}", request.MemberId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to publish RiskAssessmentEvent for member {MemberId}", request.MemberId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PublishRiskAlertAsync(RiskEvaluationRequest request, RiskEvaluationResult result)
|
|
||||||
{
|
|
||||||
if (!_options.Cap.PublisherEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var @event = new RiskAlertEvent
|
|
||||||
{
|
|
||||||
MemberId = request.MemberId,
|
|
||||||
EventType = request.EventType,
|
|
||||||
RiskLevel = result.RiskLevel.ToString(),
|
|
||||||
TotalScore = result.TotalScore,
|
|
||||||
Blocked = result.Blocked,
|
|
||||||
AlertTime = DateTime.UtcNow,
|
|
||||||
Priority = result.RiskLevel switch
|
|
||||||
{
|
|
||||||
Evaluation.RiskLevel.High => "P0",
|
|
||||||
Evaluation.RiskLevel.Medium => "P1",
|
|
||||||
_ => "P2"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await _capPublisher.PublishAsync("fengling.risk.alert", @event);
|
|
||||||
_logger.LogWarning("Published RiskAlertEvent for member {MemberId}, level={Level}",
|
|
||||||
request.MemberId, result.RiskLevel);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to publish RiskAlertEvent for member {MemberId}", request.MemberId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskAssessmentEvent
|
|
||||||
{
|
|
||||||
public string MemberId { get; set; } = string.Empty;
|
|
||||||
public string EventType { get; set; } = string.Empty;
|
|
||||||
public int? Amount { get; set; }
|
|
||||||
public string? DeviceFingerprint { get; set; }
|
|
||||||
public string? IpAddress { get; set; }
|
|
||||||
public int TotalScore { get; set; }
|
|
||||||
public string RiskLevel { get; set; } = string.Empty;
|
|
||||||
public bool Blocked { get; set; }
|
|
||||||
public List<RiskFactorResult> Factors { get; set; } = new();
|
|
||||||
public DateTime EvaluatedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskAlertEvent
|
|
||||||
{
|
|
||||||
public string MemberId { get; set; } = string.Empty;
|
|
||||||
public string EventType { get; set; } = string.Empty;
|
|
||||||
public string RiskLevel { get; set; } = string.Empty;
|
|
||||||
public int TotalScore { get; set; }
|
|
||||||
public bool Blocked { get; set; }
|
|
||||||
public DateTime AlertTime { get; set; }
|
|
||||||
public string Priority { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Fengling.RiskControl.Counter;
|
|
||||||
using Fengling.RiskControl.Evaluation;
|
|
||||||
using Fengling.RiskControl.Rules;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Failover;
|
|
||||||
|
|
||||||
public interface IFailoverStrategy
|
|
||||||
{
|
|
||||||
Task<bool> IsHealthyAsync();
|
|
||||||
RiskControlMode GetCurrentMode();
|
|
||||||
Task ExecuteWithFailoverAsync(Func<Task> action);
|
|
||||||
Task<T> ExecuteWithFailoverAsync<T>(Func<Task<T>> action);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum RiskControlMode
|
|
||||||
{
|
|
||||||
Normal = 0,
|
|
||||||
QuickFail = 1,
|
|
||||||
DenyNewUsers = 2,
|
|
||||||
Maintenance = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FailoverStrategy : IFailoverStrategy, IDisposable
|
|
||||||
{
|
|
||||||
private readonly IConnectionMultiplexer _redis;
|
|
||||||
private readonly IRiskCounterService _counterService;
|
|
||||||
private readonly IRuleLoader _ruleLoader;
|
|
||||||
private readonly RiskControlClientOptions _options;
|
|
||||||
private readonly ILogger<FailoverStrategy> _logger;
|
|
||||||
private Timer? _healthCheckTimer;
|
|
||||||
private RiskControlMode _currentMode = RiskControlMode.Normal;
|
|
||||||
private DateTime _lastFailureTime = DateTime.MinValue;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public FailoverStrategy(
|
|
||||||
IConnectionMultiplexer redis,
|
|
||||||
IRiskCounterService counterService,
|
|
||||||
IRuleLoader ruleLoader,
|
|
||||||
RiskControlClientOptions options,
|
|
||||||
ILogger<FailoverStrategy> logger)
|
|
||||||
{
|
|
||||||
_redis = redis;
|
|
||||||
_counterService = counterService;
|
|
||||||
_ruleLoader = ruleLoader;
|
|
||||||
_options = options;
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
if (_options.RedisFailover.Enabled)
|
|
||||||
{
|
|
||||||
_healthCheckTimer = new Timer(
|
|
||||||
_ => _ = CheckHealthAsync(),
|
|
||||||
null,
|
|
||||||
TimeSpan.FromSeconds(5),
|
|
||||||
TimeSpan.FromSeconds(5)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public RiskControlMode GetCurrentMode() => _currentMode;
|
|
||||||
|
|
||||||
public async Task<bool> IsHealthyAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
await db.PingAsync();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CheckHealthAsync()
|
|
||||||
{
|
|
||||||
var isHealthy = await IsHealthyAsync();
|
|
||||||
|
|
||||||
if (!isHealthy)
|
|
||||||
{
|
|
||||||
if (_lastFailureTime == DateTime.MinValue)
|
|
||||||
{
|
|
||||||
_lastFailureTime = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
var failureDuration = (DateTime.UtcNow - _lastFailureTime).TotalSeconds;
|
|
||||||
|
|
||||||
var newMode = DetermineMode(failureDuration);
|
|
||||||
if (newMode != _currentMode)
|
|
||||||
{
|
|
||||||
_currentMode = newMode;
|
|
||||||
_logger.LogWarning("Redis failure detected, duration={Duration}s, switching to mode={Mode}",
|
|
||||||
failureDuration, _currentMode);
|
|
||||||
|
|
||||||
await PublishFailoverAlertAsync(_currentMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_currentMode != RiskControlMode.Normal)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Redis restored, switching back to Normal mode");
|
|
||||||
_currentMode = RiskControlMode.Normal;
|
|
||||||
_lastFailureTime = DateTime.MinValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private RiskControlMode DetermineMode(double failureDuration)
|
|
||||||
{
|
|
||||||
if (failureDuration < _options.RedisFailover.QuickFailThresholdSeconds)
|
|
||||||
return RiskControlMode.QuickFail;
|
|
||||||
if (failureDuration < _options.RedisFailover.DenyNewUsersThresholdSeconds)
|
|
||||||
return RiskControlMode.DenyNewUsers;
|
|
||||||
return RiskControlMode.Maintenance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteWithFailoverAsync(Func<Task> action)
|
|
||||||
{
|
|
||||||
return ExecuteWithFailoverAsync(async () =>
|
|
||||||
{
|
|
||||||
await action();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<T> ExecuteWithFailoverAsync<T>(Func<Task<T>> action)
|
|
||||||
{
|
|
||||||
switch (_currentMode)
|
|
||||||
{
|
|
||||||
case RiskControlMode.Normal:
|
|
||||||
return await action();
|
|
||||||
|
|
||||||
case RiskControlMode.QuickFail:
|
|
||||||
_logger.LogWarning("QuickFail mode: failing fast");
|
|
||||||
throw new RedisConnectionException(ConnectionFailureType.UnableToConnect,
|
|
||||||
"Redis is temporarily unavailable");
|
|
||||||
|
|
||||||
case RiskControlMode.DenyNewUsers:
|
|
||||||
_logger.LogWarning("DenyNewUsers mode: checking if user has existing session");
|
|
||||||
throw new RedisConnectionException(ConnectionFailureType.UnableToConnect,
|
|
||||||
"Redis is temporarily unavailable, new users denied");
|
|
||||||
|
|
||||||
case RiskControlMode.Maintenance:
|
|
||||||
_logger.LogError("Maintenance mode: Redis unavailable for extended period");
|
|
||||||
throw new RedisConnectionException(ConnectionFailureType.UnableToConnect,
|
|
||||||
"Redis maintenance in progress");
|
|
||||||
|
|
||||||
default:
|
|
||||||
return await action();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PublishFailoverAlertAsync(RiskControlMode mode)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// CAP alert publishing would be handled by the main application
|
|
||||||
_logger.LogError("ALERT: RiskControl entering {Mode} mode due to Redis failure", mode);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_logger.LogError("Failed to publish failover alert");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
_healthCheckTimer?.Dispose();
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<RootNamespace>Fengling.RiskControl</RootNamespace>
|
|
||||||
<Description>Risk Control Client SDK for high-performance risk evaluation with Redis caching, local rule engine, and CAP event integration</Description>
|
|
||||||
<Authors>Fengling Team</Authors>
|
|
||||||
<PackageId>Fengling.RiskControl.Client</PackageId>
|
|
||||||
<Version>1.0.0</Version>
|
|
||||||
<Authors>Fengling Team</Authors>
|
|
||||||
<PackageTags>risk-control;gambling-prevention;fraud-detection;redis;cap;dotnet</PackageTags>
|
|
||||||
<RepositoryUrl>https://github.com/fengling/platform</RepositoryUrl>
|
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
|
||||||
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
|
|
||||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
|
||||||
<PackageReference Include="DotNetCore.CAP" Version="8.4.1" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Fengling.RiskControl.Domain\Fengling.RiskControl.Domain.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl;
|
|
||||||
|
|
||||||
public interface IRiskRuleLoader
|
|
||||||
{
|
|
||||||
Task<List<RiskRule>> GetActiveRulesAsync();
|
|
||||||
Task RefreshRulesAsync();
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Fengling.RiskControl.Counter;
|
|
||||||
using Fengling.RiskControl.Evaluation;
|
|
||||||
using Fengling.RiskControl.Events;
|
|
||||||
using Fengling.RiskControl.Failover;
|
|
||||||
using Fengling.RiskControl.Rules;
|
|
||||||
using Fengling.RiskControl.Sampling;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl;
|
|
||||||
|
|
||||||
public interface IRiskControlClient
|
|
||||||
{
|
|
||||||
Task<RiskEvaluationResult> EvaluateRiskAsync(RiskEvaluationRequest request);
|
|
||||||
Task<bool> IsAllowedAsync(RiskEvaluationRequest request);
|
|
||||||
Task IncrementMetricAsync(string memberId, string metric, int value = 1);
|
|
||||||
Task<Dictionary<string, int>> GetMemberMetricsAsync(string memberId);
|
|
||||||
RiskControlMode GetCurrentMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskControlClient : IRiskControlClient, IDisposable
|
|
||||||
{
|
|
||||||
private readonly IRiskEvaluator _evaluator;
|
|
||||||
private readonly IRiskCounterService _counterService;
|
|
||||||
private readonly IRiskEventPublisher _eventPublisher;
|
|
||||||
private readonly ISamplingService _samplingService;
|
|
||||||
private readonly IFailoverStrategy _failoverStrategy;
|
|
||||||
private readonly RiskControlClientOptions _options;
|
|
||||||
private readonly ILogger<RiskControlClient> _logger;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public RiskControlClient(
|
|
||||||
IRiskEvaluator evaluator,
|
|
||||||
IRiskCounterService counterService,
|
|
||||||
IRiskEventPublisher eventPublisher,
|
|
||||||
ISamplingService samplingService,
|
|
||||||
IFailoverStrategy failoverStrategy,
|
|
||||||
RiskControlClientOptions options,
|
|
||||||
ILogger<RiskControlClient> logger)
|
|
||||||
{
|
|
||||||
_evaluator = evaluator;
|
|
||||||
_counterService = counterService;
|
|
||||||
_eventPublisher = eventPublisher;
|
|
||||||
_samplingService = samplingService;
|
|
||||||
_failoverStrategy = failoverStrategy;
|
|
||||||
_options = options;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RiskEvaluationResult> EvaluateRiskAsync(RiskEvaluationRequest request)
|
|
||||||
{
|
|
||||||
return await _failoverStrategy.ExecuteWithFailoverAsync(async () =>
|
|
||||||
{
|
|
||||||
var result = await _evaluator.EvaluateAsync(request);
|
|
||||||
|
|
||||||
if (_samplingService.ShouldSample(result))
|
|
||||||
{
|
|
||||||
await _eventPublisher.PublishRiskAssessmentAsync(request, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Blocked)
|
|
||||||
{
|
|
||||||
await _eventPublisher.PublishRiskAlertAsync(request, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsAllowedAsync(RiskEvaluationRequest request)
|
|
||||||
{
|
|
||||||
var result = await EvaluateRiskAsync(request);
|
|
||||||
return !result.Blocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task IncrementMetricAsync(string memberId, string metric, int value = 1)
|
|
||||||
{
|
|
||||||
await _failoverStrategy.ExecuteWithFailoverAsync(async () =>
|
|
||||||
{
|
|
||||||
await _counterService.IncrementAsync(memberId, metric, value);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, int>> GetMemberMetricsAsync(string memberId)
|
|
||||||
{
|
|
||||||
return await _failoverStrategy.ExecuteWithFailoverAsync(async () =>
|
|
||||||
{
|
|
||||||
return await _counterService.GetAllValuesAsync(memberId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public RiskControlMode GetCurrentMode() => _failoverStrategy.GetCurrentMode();
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RiskControlClientHostedService : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly IRiskRuleLoader _ruleLoader;
|
|
||||||
private readonly IFailoverStrategy _failoverStrategy;
|
|
||||||
private readonly ILogger<RiskControlClientHostedService> _logger;
|
|
||||||
|
|
||||||
public RiskControlClientHostedService(
|
|
||||||
IRiskRuleLoader ruleLoader,
|
|
||||||
IFailoverStrategy failoverStrategy,
|
|
||||||
ILogger<RiskControlClientHostedService> logger)
|
|
||||||
{
|
|
||||||
_ruleLoader = ruleLoader;
|
|
||||||
_failoverStrategy = failoverStrategy;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("RiskControl Client Hosted Service starting...");
|
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var isHealthy = await _failoverStrategy.IsHealthyAsync();
|
|
||||||
if (!isHealthy)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Redis is unhealthy, client cannot function properly");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error checking health");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
using Fengling.RiskControl;
|
|
||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Fengling.RiskControl.Counter;
|
|
||||||
using Fengling.RiskControl.Evaluation;
|
|
||||||
using Fengling.RiskControl.Events;
|
|
||||||
using Fengling.RiskControl.Failover;
|
|
||||||
using Fengling.RiskControl.Rules;
|
|
||||||
using Fengling.RiskControl.Sampling;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using DotNetCore.CAP;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
public static class RiskControlClientServiceExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddRiskControlClientCore(
|
|
||||||
this IServiceCollection services,
|
|
||||||
Action<RiskControlClientOptions>? configureOptions = null)
|
|
||||||
{
|
|
||||||
if (configureOptions != null)
|
|
||||||
{
|
|
||||||
services.Configure(configureOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
services.TryAddSingleton<RiskControlClientOptions>(sp =>
|
|
||||||
{
|
|
||||||
var options = sp.GetRequiredService<IOptions<RiskControlClientOptions>>().Value;
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.TryAddSingleton<IConnectionMultiplexer>(sp =>
|
|
||||||
{
|
|
||||||
var options = sp.GetRequiredService<RiskControlClientOptions>();
|
|
||||||
var connectionString = options.Redis.ConnectionString;
|
|
||||||
var connection = ConnectionMultiplexer.Connect(connectionString);
|
|
||||||
return connection;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.TryAddSingleton<IRuleLoader, RedisRuleLoader>();
|
|
||||||
services.TryAddSingleton<IRiskCounterService, RedisCounterService>();
|
|
||||||
services.TryAddSingleton<IRiskEvaluator, RiskEvaluator>();
|
|
||||||
services.TryAddSingleton<ISamplingService, SamplingService>();
|
|
||||||
services.TryAddSingleton<IFailoverStrategy, FailoverStrategy>();
|
|
||||||
|
|
||||||
if (services.Any(s => s.ServiceType == typeof(ICapPublisher)))
|
|
||||||
{
|
|
||||||
services.TryAddSingleton<IRiskEventPublisher, CapEventPublisher>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
services.TryAddSingleton<IRiskEventPublisher, NoOpEventPublisher>();
|
|
||||||
}
|
|
||||||
|
|
||||||
services.TryAddSingleton<IRiskControlClient, RiskControlClient>();
|
|
||||||
services.TryAddSingleton<RiskControlClientHostedService>();
|
|
||||||
|
|
||||||
services.TryAddSingleton<IHostedService>(sp => sp.GetRequiredService<RiskControlClientHostedService>());
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddRiskControlClient(
|
|
||||||
this IServiceCollection services,
|
|
||||||
Action<RiskControlClientOptions> configureOptions)
|
|
||||||
{
|
|
||||||
services.Configure(configureOptions);
|
|
||||||
services.AddRiskControlClientCore();
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class NoOpEventPublisher : IRiskEventPublisher
|
|
||||||
{
|
|
||||||
private readonly ILogger<NoOpEventPublisher> _logger;
|
|
||||||
|
|
||||||
public NoOpEventPublisher(ILogger<NoOpEventPublisher> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PublishRiskAssessmentAsync(RiskEvaluationRequest request, RiskEvaluationResult result)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("CAP publisher not configured, skipping RiskAssessmentEvent");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PublishRiskAlertAsync(RiskEvaluationRequest request, RiskEvaluationResult result)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("CAP publisher not configured, skipping RiskAlertEvent");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
|
||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Rules;
|
|
||||||
|
|
||||||
public interface IRuleLoader
|
|
||||||
{
|
|
||||||
Task<List<RiskRule>> GetActiveRulesAsync();
|
|
||||||
Task RefreshRulesAsync();
|
|
||||||
event EventHandler? RulesChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RedisRuleLoader : IRuleLoader, IHostedService
|
|
||||||
{
|
|
||||||
private readonly IConnectionMultiplexer _redis;
|
|
||||||
private readonly RiskControlClientOptions _options;
|
|
||||||
private readonly ILogger<RedisRuleLoader> _logger;
|
|
||||||
private List<RiskRule> _cachedRules = new();
|
|
||||||
private readonly object _lock = new();
|
|
||||||
private Timer? _refreshTimer;
|
|
||||||
|
|
||||||
private string RulesKey => $"{_options.Redis.KeyPrefix}rules:active";
|
|
||||||
|
|
||||||
public event EventHandler? RulesChanged;
|
|
||||||
|
|
||||||
public RedisRuleLoader(
|
|
||||||
IConnectionMultiplexer redis,
|
|
||||||
RiskControlClientOptions options,
|
|
||||||
ILogger<RedisRuleLoader> logger)
|
|
||||||
{
|
|
||||||
_redis = redis;
|
|
||||||
_options = options;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("RuleLoader starting, loading rules from Redis...");
|
|
||||||
|
|
||||||
_ = LoadRulesAsync();
|
|
||||||
|
|
||||||
_refreshTimer = new Timer(
|
|
||||||
_ => _ = RefreshRulesAsync(),
|
|
||||||
null,
|
|
||||||
TimeSpan.FromSeconds(_options.Evaluation.RuleRefreshIntervalSeconds),
|
|
||||||
TimeSpan.FromSeconds(_options.Evaluation.RuleRefreshIntervalSeconds)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_refreshTimer?.Dispose();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<RiskRule> GetActiveRules()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _cachedRules.ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<RiskRule>> GetActiveRulesAsync()
|
|
||||||
{
|
|
||||||
if (_cachedRules.Count == 0)
|
|
||||||
{
|
|
||||||
await LoadRulesAsync();
|
|
||||||
}
|
|
||||||
return GetActiveRules();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshRulesAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await LoadRulesAsync();
|
|
||||||
_logger.LogDebug("Rules refreshed successfully, count: {Count}", _cachedRules.Count);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to refresh rules from Redis");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadRulesAsync()
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var values = await db.ListRangeAsync(RulesKey);
|
|
||||||
var rules = new List<RiskRule>();
|
|
||||||
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
var rule = JsonSerializer.Deserialize<RiskRule>(value!.ToString(), new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
});
|
|
||||||
if (rule != null && rule.IsActive)
|
|
||||||
{
|
|
||||||
rules.Add(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rules = rules.OrderBy(r => r.Priority).ToList();
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_cachedRules = rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
RulesChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
catch (RedisException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to load rules from Redis");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
using Fengling.RiskControl.Configuration;
|
|
||||||
using Fengling.RiskControl.Evaluation;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Fengling.RiskControl.Sampling;
|
|
||||||
|
|
||||||
public interface ISamplingService
|
|
||||||
{
|
|
||||||
bool ShouldSample(RiskEvaluationResult result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SamplingService : ISamplingService
|
|
||||||
{
|
|
||||||
private readonly RiskControlClientOptions _options;
|
|
||||||
private readonly ILogger<SamplingService> _logger;
|
|
||||||
private readonly Random _random = new();
|
|
||||||
|
|
||||||
public SamplingService(
|
|
||||||
RiskControlClientOptions options,
|
|
||||||
ILogger<SamplingService> logger)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShouldSample(RiskEvaluationResult result)
|
|
||||||
{
|
|
||||||
if (!_options.Sampling.Enabled)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
foreach (var rule in _options.Sampling.Rules)
|
|
||||||
{
|
|
||||||
if (EvaluateCondition(rule.Condition, result))
|
|
||||||
{
|
|
||||||
var sampleRate = rule.Rate;
|
|
||||||
var sampled = _random.NextDouble() < sampleRate;
|
|
||||||
_logger.LogDebug("Sampling check: condition={Condition}, rate={Rate}, sampled={Sampled}",
|
|
||||||
rule.Condition, sampleRate, sampled);
|
|
||||||
return sampled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultSampled = _random.NextDouble() < _options.Sampling.DefaultRate;
|
|
||||||
_logger.LogDebug("Default sampling: rate={Rate}, sampled={Sampled}",
|
|
||||||
_options.Sampling.DefaultRate, defaultSampled);
|
|
||||||
return defaultSampled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EvaluateCondition(string condition, RiskEvaluationResult result)
|
|
||||||
{
|
|
||||||
return condition switch
|
|
||||||
{
|
|
||||||
string c when c.Contains("RiskLevel == High", StringComparison.OrdinalIgnoreCase)
|
|
||||||
=> result.RiskLevel == RiskLevel.High,
|
|
||||||
string c when c.Contains("RiskLevel == Medium", StringComparison.OrdinalIgnoreCase)
|
|
||||||
=> result.RiskLevel == RiskLevel.Medium,
|
|
||||||
string c when c.Contains("RiskLevel == Low", StringComparison.OrdinalIgnoreCase)
|
|
||||||
=> result.RiskLevel == RiskLevel.Low,
|
|
||||||
string c when c.Contains("IsBlocked == true", StringComparison.OrdinalIgnoreCase)
|
|
||||||
=> result.Blocked,
|
|
||||||
string c when c.Contains("BlockedOnly", StringComparison.OrdinalIgnoreCase)
|
|
||||||
=> result.Blocked,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||||
|
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Fengling.RiskControl.Domain\Fengling.RiskControl.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
|
||||||
|
using Fengling.RiskControl.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Fengling.RiskControl.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class LotteryActivityRepository : ILotteryActivityRepository
|
||||||
|
{
|
||||||
|
private readonly RiskControlDbContext _context;
|
||||||
|
|
||||||
|
public LotteryActivityRepository(RiskControlDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LotteryActivity?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await _context.LotteryActivities.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<LotteryActivity>> GetByMemberIdAsync(long memberId)
|
||||||
|
{
|
||||||
|
return await _context.LotteryActivities
|
||||||
|
.Where(l => l.MemberId == memberId)
|
||||||
|
.OrderByDescending(l => l.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<LotteryActivity>> GetRecentByMemberIdAsync(long memberId, int count)
|
||||||
|
{
|
||||||
|
return await _context.LotteryActivities
|
||||||
|
.Where(l => l.MemberId == memberId)
|
||||||
|
.OrderByDescending(l => l.CreatedAt)
|
||||||
|
.Take(count)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(LotteryActivity activity)
|
||||||
|
{
|
||||||
|
await _context.LotteryActivities.AddAsync(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(LotteryActivity activity)
|
||||||
|
{
|
||||||
|
_context.LotteryActivities.Update(activity);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(LotteryActivity activity)
|
||||||
|
{
|
||||||
|
_context.LotteryActivities.Remove(activity);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||||
|
using Fengling.RiskControl.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Fengling.RiskControl.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class RiskAlertRepository : IRiskAlertRepository
|
||||||
|
{
|
||||||
|
private readonly RiskControlDbContext _context;
|
||||||
|
|
||||||
|
public RiskAlertRepository(RiskControlDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RiskAlert?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await _context.RiskAlerts.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskAlert>> GetByMemberIdAsync(long memberId)
|
||||||
|
{
|
||||||
|
return await _context.RiskAlerts
|
||||||
|
.Where(a => a.MemberId == memberId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskAlert>> GetPendingAlertsAsync()
|
||||||
|
{
|
||||||
|
return await _context.RiskAlerts
|
||||||
|
.Where(a => a.Status == RiskAlertStatus.Pending)
|
||||||
|
.OrderByDescending(a => a.Priority)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskAlert>> GetAlertsByPriorityAsync(RiskAlertPriority priority)
|
||||||
|
{
|
||||||
|
return await _context.RiskAlerts
|
||||||
|
.Where(a => a.Priority == priority)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(RiskAlert alert)
|
||||||
|
{
|
||||||
|
await _context.RiskAlerts.AddAsync(alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(RiskAlert alert)
|
||||||
|
{
|
||||||
|
_context.RiskAlerts.Update(alert);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(RiskAlert alert)
|
||||||
|
{
|
||||||
|
_context.RiskAlerts.Remove(alert);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||||
|
using Fengling.RiskControl.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Fengling.RiskControl.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class RiskRuleRepository : IRiskRuleRepository
|
||||||
|
{
|
||||||
|
private readonly RiskControlDbContext _context;
|
||||||
|
|
||||||
|
public RiskRuleRepository(RiskControlDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RiskRule?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await _context.RiskRules.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskRule>> GetActiveRulesAsync()
|
||||||
|
{
|
||||||
|
return await _context.RiskRules
|
||||||
|
.Where(r => r.IsActive)
|
||||||
|
.OrderByDescending(r => r.Priority)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskRule>> GetRulesByTypeAsync(RiskRuleType type)
|
||||||
|
{
|
||||||
|
return await _context.RiskRules
|
||||||
|
.Where(r => r.RuleType == type && r.IsActive)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskRule>> GetRulesForEvaluationAsync(string entityType, string actionType)
|
||||||
|
{
|
||||||
|
return await _context.RiskRules
|
||||||
|
.Where(r => r.IsActive)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(RiskRule rule)
|
||||||
|
{
|
||||||
|
await _context.RiskRules.AddAsync(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(RiskRule rule)
|
||||||
|
{
|
||||||
|
_context.RiskRules.Update(rule);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(RiskRule rule)
|
||||||
|
{
|
||||||
|
_context.RiskRules.Remove(rule);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||||
|
using Fengling.RiskControl.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Fengling.RiskControl.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class RiskScoreRepository : IRiskScoreRepository
|
||||||
|
{
|
||||||
|
private readonly RiskControlDbContext _context;
|
||||||
|
|
||||||
|
public RiskScoreRepository(RiskControlDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RiskScore?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await _context.RiskScores.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RiskScore?> GetByMemberAndEntityAsync(long memberId, string entityType, string entityId)
|
||||||
|
{
|
||||||
|
return await _context.RiskScores
|
||||||
|
.FirstOrDefaultAsync(s => s.MemberId == memberId && s.EntityType == entityType && s.EntityId == entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RiskScore?> GetActiveByMemberAndEntityTypeAsync(long memberId, string entityType)
|
||||||
|
{
|
||||||
|
return await _context.RiskScores
|
||||||
|
.Where(s => s.MemberId == memberId && s.EntityType == entityType)
|
||||||
|
.OrderByDescending(s => s.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskScore>> GetByMemberIdAsync(long memberId)
|
||||||
|
{
|
||||||
|
return await _context.RiskScores
|
||||||
|
.Where(s => s.MemberId == memberId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(RiskScore score)
|
||||||
|
{
|
||||||
|
await _context.RiskScores.AddAsync(score);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(RiskScore score)
|
||||||
|
{
|
||||||
|
_context.RiskScores.Update(score);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(RiskScore score)
|
||||||
|
{
|
||||||
|
_context.RiskScores.Remove(score);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Fengling.RiskControl.Infrastructure/RiskControlDbContext.cs
Normal file
64
Fengling.RiskControl.Infrastructure/RiskControlDbContext.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NetCorePal.Extensions.Domain;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||||
|
using Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
|
||||||
|
|
||||||
|
namespace Fengling.RiskControl.Infrastructure;
|
||||||
|
|
||||||
|
public class RiskControlDbContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<RiskRule> RiskRules => Set<RiskRule>();
|
||||||
|
public DbSet<RiskScore> RiskScores => Set<RiskScore>();
|
||||||
|
public DbSet<RiskAlert> RiskAlerts => Set<RiskAlert>();
|
||||||
|
public DbSet<LotteryActivity> LotteryActivities => Set<LotteryActivity>();
|
||||||
|
|
||||||
|
private RiskControlDbContext() { }
|
||||||
|
|
||||||
|
public RiskControlDbContext(DbContextOptions<RiskControlDbContext> options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<RiskRule>(builder =>
|
||||||
|
{
|
||||||
|
builder.ToTable("rc_risk_rules");
|
||||||
|
builder.HasKey(r => r.Id);
|
||||||
|
builder.Property(r => r.Name).HasMaxLength(100).IsRequired();
|
||||||
|
builder.Property(r => r.Description).HasMaxLength(500);
|
||||||
|
builder.Property(r => r.ConfigJson).HasColumnName("config_json");
|
||||||
|
builder.Property(r => r.IsActive).HasDefaultValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<RiskScore>(builder =>
|
||||||
|
{
|
||||||
|
builder.ToTable("rc_risk_scores");
|
||||||
|
builder.HasKey(s => s.Id);
|
||||||
|
builder.Property(s => s.MemberId).IsRequired();
|
||||||
|
builder.Property(s => s.EntityType).HasMaxLength(50).IsRequired();
|
||||||
|
builder.Property(s => s.EntityId).HasMaxLength(100).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<RiskAlert>(builder =>
|
||||||
|
{
|
||||||
|
builder.ToTable("rc_risk_alerts");
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
builder.Property(a => a.MemberId).IsRequired();
|
||||||
|
builder.Property(a => a.AlertType).HasMaxLength(50).IsRequired();
|
||||||
|
builder.Property(a => a.Description).HasMaxLength(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<LotteryActivity>(builder =>
|
||||||
|
{
|
||||||
|
builder.ToTable("rc_lottery_activities");
|
||||||
|
builder.HasKey(l => l.Id);
|
||||||
|
builder.Property(l => l.MemberId).IsRequired();
|
||||||
|
builder.Property(l => l.ActivityType).HasMaxLength(50).IsRequired();
|
||||||
|
builder.Property(l => l.IpAddress).HasMaxLength(50);
|
||||||
|
builder.Property(l => l.DeviceId).HasMaxLength(100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Fengling.RiskControl.Web/Fengling.RiskControl.Web.csproj
Normal file
19
Fengling.RiskControl.Web/Fengling.RiskControl.Web.csproj
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FastEndpoints" />
|
||||||
|
<PackageReference Include="FastEndpoints.Swagger" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||||
|
<PackageReference Include="DotNetCore.CAP" />
|
||||||
|
<PackageReference Include="DotNetCore.CAP.RabbitMQ" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Fengling.RiskControl.Domain\Fengling.RiskControl.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\Fengling.RiskControl.Infrastructure\Fengling.RiskControl.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\Fengling.RiskControl.Application\Fengling.RiskControl.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
7
Fengling.RiskControl.Web/Program.cs
Normal file
7
Fengling.RiskControl.Web/Program.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapGet("/", () => "Fengling RiskControl Service");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
Loading…
Reference in New Issue
Block a user