feat(risk-control): add risk aggregates (RiskRule, RiskScore, RiskAlert, LotteryActivity)

This commit is contained in:
Sam 2026-02-05 14:45:25 +08:00
parent bc12eefc9a
commit 0721965af4
14 changed files with 382 additions and 0 deletions

View File

@ -0,0 +1,50 @@
using NetCorePal.Extensions.Domain;
namespace Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
public class LotteryActivity : Entity<long>, IAggregateRoot
{
public long MemberId { get; private set; }
public string ActivityType { get; private set; } = string.Empty;
public int StakePoints { get; private set; }
public int? WinAmount { get; private set; }
public LotteryStatus Status { get; private set; } = LotteryStatus.Pending;
public string? IpAddress { get; private set; }
public string? DeviceId { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? CompletedAt { get; private set; }
private LotteryActivity() { }
public static LotteryActivity Create(long memberId, string activityType, int stakePoints,
string? ipAddress = null, string? deviceId = null)
{
return new LotteryActivity
{
MemberId = memberId,
ActivityType = activityType,
StakePoints = stakePoints,
Status = LotteryStatus.Pending,
IpAddress = ipAddress,
DeviceId = deviceId
};
}
public void MarkProcessing()
{
Status = LotteryStatus.Processing;
}
public void Complete(int winAmount, bool isWin)
{
Status = isWin ? LotteryStatus.Won : LotteryStatus.Lost;
WinAmount = isWin ? winAmount : null;
CompletedAt = DateTime.UtcNow;
}
public void Fail(string reason)
{
Status = LotteryStatus.Failed;
CompletedAt = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,11 @@
namespace Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
public enum LotteryStatus
{
Pending = 0,
Processing = 1,
Won = 2,
Lost = 3,
Cancelled = 4,
Failed = 5
}

View File

@ -0,0 +1,59 @@
using NetCorePal.Extensions.Domain;
namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
public class RiskAlert : Entity<long>, IAggregateRoot
{
public long MemberId { get; private set; }
public string AlertType { get; private set; } = string.Empty;
public string Description { get; private set; } = string.Empty;
public RiskAlertPriority Priority { get; private set; }
public RiskAlertStatus Status { get; private set; } = RiskAlertStatus.Pending;
public string? ResolutionNotes { get; private set; }
public long? AssignedTo { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? ResolvedAt { get; private set; }
private RiskAlert() { }
public static RiskAlert Create(long memberId, string alertType, string description,
RiskAlertPriority priority = RiskAlertPriority.Medium)
{
return new RiskAlert
{
MemberId = memberId,
AlertType = alertType,
Description = description,
Priority = priority,
Status = RiskAlertStatus.Pending
};
}
public void Resolve(string notes)
{
Status = RiskAlertStatus.Resolved;
ResolutionNotes = notes;
ResolvedAt = DateTime.UtcNow;
}
public void Dismiss(string notes)
{
Status = RiskAlertStatus.Dismissed;
ResolutionNotes = notes;
ResolvedAt = DateTime.UtcNow;
}
public void Escalate()
{
Priority = Priority == RiskAlertPriority.Critical
? RiskAlertPriority.Critical
: Priority + 1;
Status = RiskAlertStatus.Investigating;
}
public void AssignTo(long adminId)
{
AssignedTo = adminId;
Status = RiskAlertStatus.Investigating;
}
}

View File

@ -0,0 +1,9 @@
namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
public enum RiskAlertPriority
{
Low = 0,
Medium = 1,
High = 2,
Critical = 3
}

View File

@ -0,0 +1,9 @@
namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
public enum RiskAlertStatus
{
Pending = 0,
Investigating = 1,
Resolved = 2,
Dismissed = 3
}

View File

@ -0,0 +1,60 @@
using NetCorePal.Extensions.Domain;
namespace Fengling.RiskControl.Domain.Aggregates.RiskRules;
public class RiskRule : Entity<long>, IAggregateRoot
{
public string Name { get; private set; } = string.Empty;
public string Description { get; private set; } = string.Empty;
public RiskRuleType RuleType { get; private set; }
public RiskRuleAction Action { get; private set; }
public string ConfigJson { get; private set; } = string.Empty;
public int Priority { get; private set; }
public bool IsActive { get; private set; } = true;
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
private RiskRule() { }
public static RiskRule Create(string name, string description, RiskRuleType type,
RiskRuleAction action, string configJson, int priority = 0)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("规则名称不能为空", nameof(name));
if (string.IsNullOrWhiteSpace(configJson))
throw new ArgumentException("配置不能为空", nameof(configJson));
System.Text.Json.JsonDocument.Parse(configJson);
return new RiskRule
{
Name = name,
Description = description,
RuleType = type,
Action = action,
ConfigJson = configJson,
Priority = priority,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
}
public void Deactivate()
{
IsActive = false;
UpdatedAt = DateTime.UtcNow;
}
public void UpdateConfig(string newConfigJson)
{
System.Text.Json.JsonDocument.Parse(newConfigJson);
ConfigJson = newConfigJson;
UpdatedAt = DateTime.UtcNow;
}
public T GetConfig<T>() where T : class
{
return System.Text.Json.JsonSerializer.Deserialize<T>(ConfigJson)!;
}
}

View File

@ -0,0 +1,11 @@
namespace Fengling.RiskControl.Domain.Aggregates.RiskRules;
public enum RiskRuleAction
{
Allow = 0,
Block = 1,
RequireVerification = 2,
FlagForReview = 3,
RateLimit = 4,
LogOnly = 5
}

View File

@ -0,0 +1,15 @@
namespace Fengling.RiskControl.Domain.Aggregates.RiskRules;
public enum RiskRuleType
{
FrequencyLimit = 1, // 频率限制
AmountLimit = 2, // 金额限制
TimeWindowLimit = 3, // 时间窗口限制
GeoRestriction = 4, // 地域限制
DeviceFingerprint = 5, // 设备指纹
BehaviorPattern = 6, // 行为模式
Blacklist = 7, // 黑名单
Whitelist = 8, // 白名单
VelocityCheck = 9, // 速度检测
AnomalyDetection = 10 // 异常检测
}

View File

@ -0,0 +1,21 @@
namespace Fengling.RiskControl.Domain.Aggregates.RiskScores;
public class RiskFactor
{
public string FactorType { get; private set; } = string.Empty;
public int Points { get; private set; }
public string Description { get; private set; } = string.Empty;
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
private RiskFactor() { }
public static RiskFactor Create(string factorType, int points, string description)
{
return new RiskFactor
{
FactorType = factorType,
Points = points,
Description = description
};
}
}

View File

@ -0,0 +1,9 @@
namespace Fengling.RiskControl.Domain.Aggregates.RiskScores;
public enum RiskLevel
{
Low = 0,
Medium = 1,
High = 2,
Critical = 3
}

View File

@ -0,0 +1,58 @@
using NetCorePal.Extensions.Domain;
namespace Fengling.RiskControl.Domain.Aggregates.RiskScores;
public class RiskScore : Entity<long>, IAggregateRoot
{
public long MemberId { get; private set; }
public string EntityType { get; private set; } = string.Empty;
public string EntityId { get; private set; } = string.Empty;
public int TotalScore { get; private set; }
public RiskLevel RiskLevel { get; private set; }
private readonly List<RiskFactor> _factors = new();
public IReadOnlyCollection<RiskFactor> Factors => _factors.AsReadOnly();
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; private set; }
private RiskScore() { }
public static RiskScore Create(long memberId, string entityType, string entityId,
DateTime? expiresAt = null)
{
return new RiskScore
{
MemberId = memberId,
EntityType = entityType,
EntityId = entityId,
TotalScore = 0,
RiskLevel = RiskLevel.Low,
ExpiresAt = expiresAt
};
}
public void AddRiskFactor(string factorType, int points, string description)
{
_factors.Add(RiskFactor.Create(factorType, points, description));
RecalculateScore();
}
public void Reset()
{
_factors.Clear();
TotalScore = 0;
RiskLevel = RiskLevel.Low;
}
private void RecalculateScore()
{
TotalScore = _factors.Sum(f => f.Points);
RiskLevel = TotalScore >= 70 ? RiskLevel.High :
TotalScore >= 30 ? RiskLevel.Medium :
RiskLevel.Low;
}
public bool IsExpired()
{
return ExpiresAt.HasValue && DateTime.UtcNow > ExpiresAt.Value;
}
}

View File

@ -0,0 +1,24 @@
using MediatR;
namespace Fengling.RiskControl.Domain.Events;
public class LotteryCompletedEvent : INotification
{
public long ActivityId { get; }
public long MemberId { get; }
public int StakePoints { get; }
public int? WinAmount { get; }
public bool IsWin { get; }
public int RiskScore { get; }
public LotteryCompletedEvent(long activityId, long memberId, int stakePoints,
int? winAmount, bool isWin, int riskScore)
{
ActivityId = activityId;
MemberId = memberId;
StakePoints = stakePoints;
WinAmount = winAmount;
IsWin = isWin;
RiskScore = riskScore;
}
}

View File

@ -0,0 +1,22 @@
using MediatR;
namespace Fengling.RiskControl.Domain.Events;
public class RiskAlertTriggeredEvent : INotification
{
public long AlertId { get; }
public long MemberId { get; }
public string AlertType { get; }
public int RiskScore { get; }
public string Reason { get; }
public RiskAlertTriggeredEvent(long alertId, long memberId, string alertType,
int riskScore, string reason)
{
AlertId = alertId;
MemberId = memberId;
AlertType = alertType;
RiskScore = riskScore;
Reason = reason;
}
}

View File

@ -0,0 +1,24 @@
using MediatR;
namespace Fengling.RiskControl.Domain.Events;
public class RiskAssessmentRequestedEvent : INotification
{
public long MemberId { get; }
public string EntityType { get; }
public string EntityId { get; }
public string ActionType { get; }
public Dictionary<string, object> Context { get; }
public DateTime RequestedAt { get; }
public RiskAssessmentRequestedEvent(long memberId, string entityType, string entityId,
string actionType, Dictionary<string, object>? context = null)
{
MemberId = memberId;
EntityType = entityType;
EntityId = entityId;
ActionType = actionType;
Context = context ?? new Dictionary<string, object>();
RequestedAt = DateTime.UtcNow;
}
}