feat(risk-control): add risk aggregates (RiskRule, RiskScore, RiskAlert, LotteryActivity)
This commit is contained in:
parent
bc12eefc9a
commit
0721965af4
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
|
||||
public enum RiskAlertPriority
|
||||
{
|
||||
Low = 0,
|
||||
Medium = 1,
|
||||
High = 2,
|
||||
Critical = 3
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
|
||||
public enum RiskAlertStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Investigating = 1,
|
||||
Resolved = 2,
|
||||
Dismissed = 3
|
||||
}
|
||||
60
Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRule.cs
Normal file
60
Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRule.cs
Normal 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)!;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 // 异常检测
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
|
||||
public enum RiskLevel
|
||||
{
|
||||
Low = 0,
|
||||
Medium = 1,
|
||||
High = 2,
|
||||
Critical = 3
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
24
Fengling.RiskControl.Domain/Events/LotteryCompletedEvent.cs
Normal file
24
Fengling.RiskControl.Domain/Events/LotteryCompletedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user