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