diff --git a/Fengling.RiskControl.Domain/Aggregates/LotteryActivities/LotteryActivity.cs b/Fengling.RiskControl.Domain/Aggregates/LotteryActivities/LotteryActivity.cs new file mode 100644 index 0000000..deef236 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/LotteryActivities/LotteryActivity.cs @@ -0,0 +1,50 @@ +using NetCorePal.Extensions.Domain; + +namespace Fengling.RiskControl.Domain.Aggregates.LotteryActivities; + +public class LotteryActivity : Entity, 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; + } +} diff --git a/Fengling.RiskControl.Domain/Aggregates/LotteryActivities/LotteryStatus.cs b/Fengling.RiskControl.Domain/Aggregates/LotteryActivities/LotteryStatus.cs new file mode 100644 index 0000000..6444f56 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/LotteryActivities/LotteryStatus.cs @@ -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 +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlert.cs b/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlert.cs new file mode 100644 index 0000000..f72ecea --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlert.cs @@ -0,0 +1,59 @@ +using NetCorePal.Extensions.Domain; + +namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts; + +public class RiskAlert : Entity, 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; + } +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlertPriority.cs b/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlertPriority.cs new file mode 100644 index 0000000..cde238f --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlertPriority.cs @@ -0,0 +1,9 @@ +namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts; + +public enum RiskAlertPriority +{ + Low = 0, + Medium = 1, + High = 2, + Critical = 3 +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlertStatus.cs b/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlertStatus.cs new file mode 100644 index 0000000..7933c51 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskAlerts/RiskAlertStatus.cs @@ -0,0 +1,9 @@ +namespace Fengling.RiskControl.Domain.Aggregates.RiskAlerts; + +public enum RiskAlertStatus +{ + Pending = 0, + Investigating = 1, + Resolved = 2, + Dismissed = 3 +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRule.cs b/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRule.cs new file mode 100644 index 0000000..7394e7f --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRule.cs @@ -0,0 +1,60 @@ +using NetCorePal.Extensions.Domain; + +namespace Fengling.RiskControl.Domain.Aggregates.RiskRules; + +public class RiskRule : Entity, 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() where T : class + { + return System.Text.Json.JsonSerializer.Deserialize(ConfigJson)!; + } +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRuleAction.cs b/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRuleAction.cs new file mode 100644 index 0000000..d306296 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRuleAction.cs @@ -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 +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRuleType.cs b/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRuleType.cs new file mode 100644 index 0000000..4d7c598 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskRules/RiskRuleType.cs @@ -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 // 异常检测 +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskFactor.cs b/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskFactor.cs new file mode 100644 index 0000000..aa24af9 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskFactor.cs @@ -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 + }; + } +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskLevel.cs b/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskLevel.cs new file mode 100644 index 0000000..8fc0959 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskLevel.cs @@ -0,0 +1,9 @@ +namespace Fengling.RiskControl.Domain.Aggregates.RiskScores; + +public enum RiskLevel +{ + Low = 0, + Medium = 1, + High = 2, + Critical = 3 +} diff --git a/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskScore.cs b/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskScore.cs new file mode 100644 index 0000000..2fcba61 --- /dev/null +++ b/Fengling.RiskControl.Domain/Aggregates/RiskScores/RiskScore.cs @@ -0,0 +1,58 @@ +using NetCorePal.Extensions.Domain; + +namespace Fengling.RiskControl.Domain.Aggregates.RiskScores; + +public class RiskScore : Entity, 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 _factors = new(); + public IReadOnlyCollection 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; + } +} diff --git a/Fengling.RiskControl.Domain/Events/LotteryCompletedEvent.cs b/Fengling.RiskControl.Domain/Events/LotteryCompletedEvent.cs new file mode 100644 index 0000000..83cba8e --- /dev/null +++ b/Fengling.RiskControl.Domain/Events/LotteryCompletedEvent.cs @@ -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; + } +} diff --git a/Fengling.RiskControl.Domain/Events/RiskAlertTriggeredEvent.cs b/Fengling.RiskControl.Domain/Events/RiskAlertTriggeredEvent.cs new file mode 100644 index 0000000..8eef9ad --- /dev/null +++ b/Fengling.RiskControl.Domain/Events/RiskAlertTriggeredEvent.cs @@ -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; + } +} diff --git a/Fengling.RiskControl.Domain/Events/RiskAssessmentRequestedEvent.cs b/Fengling.RiskControl.Domain/Events/RiskAssessmentRequestedEvent.cs new file mode 100644 index 0000000..0b54c29 --- /dev/null +++ b/Fengling.RiskControl.Domain/Events/RiskAssessmentRequestedEvent.cs @@ -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 Context { get; } + public DateTime RequestedAt { get; } + + public RiskAssessmentRequestedEvent(long memberId, string entityType, string entityId, + string actionType, Dictionary? context = null) + { + MemberId = memberId; + EntityType = entityType; + EntityId = entityId; + ActionType = actionType; + Context = context ?? new Dictionary(); + RequestedAt = DateTime.UtcNow; + } +}