fix: rename typings.d.ts to typings.ts for proper module resolution
This commit is contained in:
parent
293209b1dc
commit
78ef001abc
@ -1,35 +0,0 @@
|
||||
using Fengling.RiskControl.Application.Services;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Commands;
|
||||
|
||||
public record EvaluateRiskCommand : IRequest<RiskEvaluationResult>
|
||||
{
|
||||
public long MemberId { get; init; }
|
||||
public string EntityType { get; init; } = string.Empty;
|
||||
public string EntityId { get; init; } = string.Empty;
|
||||
public string ActionType { get; init; } = string.Empty;
|
||||
public Dictionary<string, object> Context { get; init; } = new();
|
||||
}
|
||||
|
||||
public class EvaluateRiskCommandHandler : IRequestHandler<EvaluateRiskCommand, RiskEvaluationResult>
|
||||
{
|
||||
private readonly IRiskEvaluationService _riskService;
|
||||
|
||||
public EvaluateRiskCommandHandler(IRiskEvaluationService riskService)
|
||||
{
|
||||
_riskService = riskService;
|
||||
}
|
||||
|
||||
public async Task<RiskEvaluationResult> Handle(EvaluateRiskCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _riskService.EvaluateRiskAsync(new RiskEvaluationRequest
|
||||
{
|
||||
MemberId = request.MemberId,
|
||||
EntityType = request.EntityType,
|
||||
EntityId = request.EntityId,
|
||||
ActionType = request.ActionType,
|
||||
Context = request.Context
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
using Fengling.RiskControl.Domain.Events;
|
||||
using Fengling.RiskControl.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Events;
|
||||
|
||||
public class LotteryCompletedEventHandler : INotificationHandler<LotteryCompletedEvent>
|
||||
{
|
||||
private const int BIG_WIN_MULTIPLIER = 5;
|
||||
|
||||
private readonly IRiskScoreRepository _scoreRepository;
|
||||
|
||||
public LotteryCompletedEventHandler(IRiskScoreRepository scoreRepository)
|
||||
{
|
||||
_scoreRepository = scoreRepository;
|
||||
}
|
||||
|
||||
public async Task Handle(LotteryCompletedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var score = RiskScore.Create(
|
||||
notification.MemberId,
|
||||
"lottery_result",
|
||||
notification.ActivityId.ToString(),
|
||||
expiresAt: DateTime.UtcNow.AddDays(1));
|
||||
|
||||
if (notification.IsWin && notification.WinAmount > notification.StakePoints * BIG_WIN_MULTIPLIER)
|
||||
{
|
||||
score.AddRiskFactor("big_win", 20, "赢得超过投入5倍");
|
||||
}
|
||||
|
||||
await _scoreRepository.AddAsync(score);
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
using Fengling.RiskControl.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Events;
|
||||
|
||||
public class MemberRegisteredEventHandler : INotificationHandler<Fengling.Member.Domain.Events.Member.MemberRegisteredEvent>
|
||||
{
|
||||
private readonly IRiskScoreRepository _scoreRepository;
|
||||
|
||||
public MemberRegisteredEventHandler(IRiskScoreRepository scoreRepository)
|
||||
{
|
||||
_scoreRepository = scoreRepository;
|
||||
}
|
||||
|
||||
public async Task Handle(Fengling.Member.Domain.Events.Member.MemberRegisteredEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var baselineScore = RiskScore.Create(
|
||||
notification.MemberId,
|
||||
"member_registration",
|
||||
$"reg_{notification.MemberId}",
|
||||
expiresAt: DateTime.UtcNow.AddDays(30));
|
||||
|
||||
await _scoreRepository.AddAsync(baselineScore);
|
||||
}
|
||||
}
|
||||
|
||||
public class PointsChangedEventHandler : INotificationHandler<Fengling.Member.Domain.Events.Points.PointsChangedEvent>
|
||||
{
|
||||
private const int LARGE_POINT_THRESHOLD = 1000;
|
||||
|
||||
private readonly IRiskScoreRepository _scoreRepository;
|
||||
|
||||
public PointsChangedEventHandler(IRiskScoreRepository scoreRepository)
|
||||
{
|
||||
_scoreRepository = scoreRepository;
|
||||
}
|
||||
|
||||
public async Task Handle(Fengling.Member.Domain.Events.Points.PointsChangedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (notification.ChangedPoints > LARGE_POINT_THRESHOLD)
|
||||
{
|
||||
var score = RiskScore.Create(
|
||||
notification.MemberId,
|
||||
"large_point_change",
|
||||
notification.AccountId.ToString());
|
||||
|
||||
score.AddRiskFactor("large_point_change", 25, $"大额积分变动: {notification.ChangedPoints}");
|
||||
await _scoreRepository.AddAsync(score);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
using Fengling.RiskControl.Application.Services;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
using Fengling.RiskControl.Domain.Events;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Events;
|
||||
|
||||
public class RiskAlertTriggeredEventHandler : INotificationHandler<RiskAlertTriggeredEvent>
|
||||
{
|
||||
private const int ALERT_TRIGGER_THRESHOLD = 30;
|
||||
|
||||
private readonly IRiskAlertService _alertService;
|
||||
|
||||
public RiskAlertTriggeredEventHandler(IRiskAlertService alertService)
|
||||
{
|
||||
_alertService = alertService;
|
||||
}
|
||||
|
||||
public async Task Handle(RiskAlertTriggeredEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (notification.RiskScore < ALERT_TRIGGER_THRESHOLD)
|
||||
return;
|
||||
|
||||
var priority = notification.RiskScore switch
|
||||
{
|
||||
>= 80 => RiskAlertPriority.Critical,
|
||||
>= 60 => RiskAlertPriority.High,
|
||||
>= 40 => RiskAlertPriority.Medium,
|
||||
_ => RiskAlertPriority.Low
|
||||
};
|
||||
|
||||
await _alertService.CreateAlertAsync(
|
||||
notification.MemberId,
|
||||
notification.AlertType,
|
||||
notification.Reason,
|
||||
priority);
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
<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" />
|
||||
<ProjectReference Include="..\..\Fengling.Member\src\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,37 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public record LotteryParticipationRequest
|
||||
{
|
||||
public long MemberId { get; init; }
|
||||
public string ActivityType { get; init; } = string.Empty;
|
||||
public int StakePoints { get; init; }
|
||||
public string? IpAddress { get; init; }
|
||||
public string? DeviceId { get; init; }
|
||||
}
|
||||
|
||||
public record LotteryParticipationResult
|
||||
{
|
||||
public long ActivityId { get; init; }
|
||||
public LotteryParticipationStatus Status { get; init; }
|
||||
public int? WinAmount { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public RiskEvaluationResult? RiskResult { get; init; }
|
||||
}
|
||||
|
||||
public enum LotteryParticipationStatus
|
||||
{
|
||||
Allowed = 0,
|
||||
Blocked = 1,
|
||||
PendingVerification = 2,
|
||||
InsufficientPoints = 3,
|
||||
Failed = 4
|
||||
}
|
||||
|
||||
public interface ILotteryService
|
||||
{
|
||||
Task<LotteryParticipationResult> ParticipateAsync(LotteryParticipationRequest request);
|
||||
Task<LotteryActivity?> GetActivityAsync(long activityId);
|
||||
Task<IEnumerable<LotteryActivity>> GetMemberActivitiesAsync(long memberId);
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public interface IMemberIntegrationService
|
||||
{
|
||||
Task<int> GetMemberPointsBalanceAsync(long memberId);
|
||||
Task<bool> DeductPointsAsync(long memberId, int points, string reason);
|
||||
Task<bool> AddPointsAsync(long memberId, int points, string reason);
|
||||
Task<MemberInfo?> GetMemberInfoAsync(long memberId);
|
||||
}
|
||||
|
||||
public record MemberInfo
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public long TenantId { get; init; }
|
||||
public string? PhoneNumber { get; init; }
|
||||
public int PointsBalance { get; init; }
|
||||
public DateTime RegisteredAt { get; init; }
|
||||
}
|
||||
|
||||
public record PointsBalanceResponse
|
||||
{
|
||||
public int AvailableBalance { get; init; }
|
||||
public int FrozenBalance { get; init; }
|
||||
public int TotalBalance { get; init; }
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public record OrderDiscountValidationRequest
|
||||
{
|
||||
public long MemberId { get; init; }
|
||||
public string OrderId { get; init; } = string.Empty;
|
||||
public int DiscountAmount { get; init; }
|
||||
public int OriginalAmount { get; init; }
|
||||
public string DiscountType { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public record OrderDiscountValidationResult
|
||||
{
|
||||
public bool IsAllowed { get; init; }
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
public int MaxDiscountAllowed { get; init; }
|
||||
public RiskEvaluationResult? RiskDetails { get; init; }
|
||||
}
|
||||
|
||||
public interface IOrderRiskValidationService
|
||||
{
|
||||
Task<OrderDiscountValidationResult> ValidateDiscountAsync(OrderDiscountValidationRequest request);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public interface IRiskAlertService
|
||||
{
|
||||
Task<RiskAlert> CreateAlertAsync(long memberId, string alertType, string description,
|
||||
RiskAlertPriority priority = RiskAlertPriority.Medium);
|
||||
Task<RiskAlert> ResolveAlertAsync(long alertId, string notes);
|
||||
Task<RiskAlert> DismissAlertAsync(long alertId, string notes);
|
||||
Task<RiskAlert> EscalateAlertAsync(long alertId);
|
||||
Task<IEnumerable<RiskAlert>> GetMemberAlertsAsync(long memberId);
|
||||
Task<IEnumerable<RiskAlert>> GetPendingAlertsAsync();
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public record RiskEvaluationRequest
|
||||
{
|
||||
public long MemberId { get; init; }
|
||||
public string EntityType { get; init; } = string.Empty;
|
||||
public string EntityId { get; init; } = string.Empty;
|
||||
public string ActionType { get; init; } = string.Empty;
|
||||
public Dictionary<string, object> Context { get; init; } = new();
|
||||
}
|
||||
|
||||
public record RiskEvaluationResult
|
||||
{
|
||||
public int TotalScore { get; init; }
|
||||
public RiskLevel RiskLevel { get; init; }
|
||||
public RiskRuleAction RecommendedAction { get; init; }
|
||||
public List<RiskFactorResult> Factors { get; init; } = new();
|
||||
public bool Blocked { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public record RiskFactorResult
|
||||
{
|
||||
public string FactorType { get; init; } = string.Empty;
|
||||
public int Points { get; init; }
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string RuleName { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public interface IRiskEvaluationService
|
||||
{
|
||||
Task<RiskEvaluationResult> EvaluateRiskAsync(RiskEvaluationRequest request);
|
||||
Task<bool> IsAllowedAsync(RiskEvaluationRequest request);
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
|
||||
using Fengling.RiskControl.Domain.Events;
|
||||
using Fengling.RiskControl.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public class LotteryService : ILotteryService
|
||||
{
|
||||
private const double WIN_PROBABILITY = 0.3;
|
||||
private const int MIN_WIN_MULTIPLIER = 2;
|
||||
private const int MAX_WIN_MULTIPLIER = 10;
|
||||
|
||||
private readonly ILotteryActivityRepository _activityRepository;
|
||||
private readonly IRiskEvaluationService _riskService;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public LotteryService(
|
||||
ILotteryActivityRepository activityRepository,
|
||||
IRiskEvaluationService riskService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_activityRepository = activityRepository;
|
||||
_riskService = riskService;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<LotteryParticipationResult> ParticipateAsync(LotteryParticipationRequest request)
|
||||
{
|
||||
var riskResult = await _riskService.EvaluateRiskAsync(new RiskEvaluationRequest
|
||||
{
|
||||
MemberId = request.MemberId,
|
||||
EntityType = "lottery",
|
||||
EntityId = request.ActivityType,
|
||||
ActionType = "execute",
|
||||
Context = new Dictionary<string, object>
|
||||
{
|
||||
["stakePoints"] = request.StakePoints,
|
||||
["activityType"] = request.ActivityType
|
||||
}
|
||||
});
|
||||
|
||||
if (riskResult.Blocked)
|
||||
{
|
||||
return new LotteryParticipationResult
|
||||
{
|
||||
Status = LotteryParticipationStatus.Blocked,
|
||||
Message = riskResult.Message,
|
||||
RiskResult = riskResult
|
||||
};
|
||||
}
|
||||
|
||||
var activity = LotteryActivity.Create(
|
||||
request.MemberId,
|
||||
request.ActivityType,
|
||||
request.StakePoints,
|
||||
request.IpAddress,
|
||||
request.DeviceId);
|
||||
|
||||
activity.MarkProcessing();
|
||||
await _activityRepository.AddAsync(activity);
|
||||
|
||||
var (winAmount, isWin) = SimulateLotteryOutcome(request.StakePoints);
|
||||
activity.Complete(winAmount, isWin);
|
||||
|
||||
await _mediator.Publish(new LotteryCompletedEvent(
|
||||
activity.Id, request.MemberId, request.StakePoints, winAmount, isWin, riskResult.TotalScore));
|
||||
|
||||
return new LotteryParticipationResult
|
||||
{
|
||||
ActivityId = activity.Id,
|
||||
Status = LotteryParticipationStatus.Allowed,
|
||||
WinAmount = winAmount,
|
||||
Message = isWin ? $"恭喜中奖! 获得 {winAmount} 积分" : "未中奖",
|
||||
RiskResult = riskResult
|
||||
};
|
||||
}
|
||||
|
||||
private (int winAmount, bool isWin) SimulateLotteryOutcome(int stakePoints)
|
||||
{
|
||||
var random = new Random();
|
||||
var isWin = random.NextDouble() < WIN_PROBABILITY;
|
||||
var winAmount = isWin ? stakePoints * random.Next(MIN_WIN_MULTIPLIER, MAX_WIN_MULTIPLIER + 1) : 0;
|
||||
return (winAmount, isWin);
|
||||
}
|
||||
|
||||
public Task<LotteryActivity?> GetActivityAsync(long activityId)
|
||||
{
|
||||
return _activityRepository.GetByIdAsync(activityId);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<LotteryActivity>> GetMemberActivitiesAsync(long memberId)
|
||||
{
|
||||
return _activityRepository.GetByMemberIdAsync(memberId);
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public class MemberIntegrationService : IMemberIntegrationService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<MemberIntegrationService> _logger;
|
||||
|
||||
public MemberIntegrationService(HttpClient httpClient, ILogger<MemberIntegrationService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<int> GetMemberPointsBalanceAsync(long memberId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/members/{memberId}/points/balance");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to get points balance for member {MemberId}: {StatusCode}",
|
||||
memberId, response.StatusCode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<PointsBalanceResponse>();
|
||||
return result?.AvailableBalance ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting points balance for member {MemberId}", memberId);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeductPointsAsync(long memberId, int points, string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync($"/api/v1/members/{memberId}/points/deduct", new
|
||||
{
|
||||
Points = points,
|
||||
Reason = reason
|
||||
});
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deducting points for member {MemberId}", memberId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddPointsAsync(long memberId, int points, string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync($"/api/v1/members/{memberId}/points/add", new
|
||||
{
|
||||
Points = points,
|
||||
Reason = reason
|
||||
});
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error adding points for member {MemberId}", memberId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MemberInfo?> GetMemberInfoAsync(long memberId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/members/{memberId}");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<MemberInfo>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting member info for {MemberId}", memberId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public class OrderRiskValidationService : IOrderRiskValidationService
|
||||
{
|
||||
private readonly IRiskEvaluationService _riskService;
|
||||
private readonly IMemberIntegrationService _memberService;
|
||||
|
||||
public OrderRiskValidationService(
|
||||
IRiskEvaluationService riskService,
|
||||
IMemberIntegrationService memberService)
|
||||
{
|
||||
_riskService = riskService;
|
||||
_memberService = memberService;
|
||||
}
|
||||
|
||||
public async Task<OrderDiscountValidationResult> ValidateDiscountAsync(OrderDiscountValidationRequest request)
|
||||
{
|
||||
if (request.OriginalAmount <= 0)
|
||||
{
|
||||
return new OrderDiscountValidationResult
|
||||
{
|
||||
IsAllowed = false,
|
||||
Reason = "订单金额必须大于0",
|
||||
MaxDiscountAllowed = 0
|
||||
};
|
||||
}
|
||||
|
||||
var discountRate = (double)request.DiscountAmount / request.OriginalAmount;
|
||||
|
||||
var riskResult = await _riskService.EvaluateRiskAsync(new RiskEvaluationRequest
|
||||
{
|
||||
MemberId = request.MemberId,
|
||||
EntityType = "order_discount",
|
||||
EntityId = request.OrderId,
|
||||
ActionType = "apply_discount",
|
||||
Context = new Dictionary<string, object>
|
||||
{
|
||||
["discountAmount"] = request.DiscountAmount,
|
||||
["originalAmount"] = request.OriginalAmount,
|
||||
["discountRate"] = discountRate,
|
||||
["discountType"] = request.DiscountType
|
||||
}
|
||||
});
|
||||
|
||||
if (riskResult.Blocked)
|
||||
{
|
||||
return new OrderDiscountValidationResult
|
||||
{
|
||||
IsAllowed = false,
|
||||
Reason = riskResult.Message,
|
||||
MaxDiscountAllowed = 0,
|
||||
RiskDetails = riskResult
|
||||
};
|
||||
}
|
||||
|
||||
var maxDiscount = riskResult.RiskLevel switch
|
||||
{
|
||||
RiskLevel.Low => request.OriginalAmount / 2,
|
||||
RiskLevel.Medium => request.OriginalAmount / 5,
|
||||
RiskLevel.High => 0,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
return new OrderDiscountValidationResult
|
||||
{
|
||||
IsAllowed = request.DiscountAmount <= maxDiscount,
|
||||
Reason = request.DiscountAmount <= maxDiscount ? "折扣申请通过" : $"超过最大允许折扣: {maxDiscount}",
|
||||
MaxDiscountAllowed = maxDiscount,
|
||||
RiskDetails = riskResult
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
using Fengling.RiskControl.Domain.Events;
|
||||
using Fengling.RiskControl.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public class RiskAlertService : IRiskAlertService
|
||||
{
|
||||
private readonly IRiskAlertRepository _alertRepository;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public RiskAlertService(IRiskAlertRepository alertRepository, IMediator mediator)
|
||||
{
|
||||
_alertRepository = alertRepository;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<RiskAlert> CreateAlertAsync(long memberId, string alertType, string description,
|
||||
RiskAlertPriority priority = RiskAlertPriority.Medium)
|
||||
{
|
||||
var alert = RiskAlert.Create(memberId, alertType, description, priority);
|
||||
await _alertRepository.AddAsync(alert);
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async Task<RiskAlert> ResolveAlertAsync(long alertId, string notes)
|
||||
{
|
||||
var alert = await _alertRepository.GetByIdAsync(alertId);
|
||||
if (alert == null)
|
||||
throw new KeyNotFoundException($"Alert not found: {alertId}");
|
||||
|
||||
alert.Resolve(notes);
|
||||
await _alertRepository.UpdateAsync(alert);
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async Task<RiskAlert> DismissAlertAsync(long alertId, string notes)
|
||||
{
|
||||
var alert = await _alertRepository.GetByIdAsync(alertId);
|
||||
if (alert == null)
|
||||
throw new KeyNotFoundException($"Alert not found: {alertId}");
|
||||
|
||||
alert.Dismiss(notes);
|
||||
await _alertRepository.UpdateAsync(alert);
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public async Task<RiskAlert> EscalateAlertAsync(long alertId)
|
||||
{
|
||||
var alert = await _alertRepository.GetByIdAsync(alertId);
|
||||
if (alert == null)
|
||||
throw new KeyNotFoundException($"Alert not found: {alertId}");
|
||||
|
||||
alert.Escalate();
|
||||
await _alertRepository.UpdateAsync(alert);
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RiskAlert>> GetMemberAlertsAsync(long memberId)
|
||||
{
|
||||
return _alertRepository.GetByMemberIdAsync(memberId);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RiskAlert>> GetPendingAlertsAsync()
|
||||
{
|
||||
return _alertRepository.GetPendingAlertsAsync();
|
||||
}
|
||||
}
|
||||
@ -1,153 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
using Fengling.RiskControl.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Services;
|
||||
|
||||
public class RiskEvaluationService : IRiskEvaluationService
|
||||
{
|
||||
private const int HIGH_THRESHOLD = 70;
|
||||
private const int MEDIUM_THRESHOLD = 30;
|
||||
private const int BLACKLIST_POINTS = 100;
|
||||
|
||||
private readonly IRiskRuleRepository _ruleRepository;
|
||||
private readonly IRiskScoreRepository _scoreRepository;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public RiskEvaluationService(
|
||||
IRiskRuleRepository ruleRepository,
|
||||
IRiskScoreRepository scoreRepository,
|
||||
IMediator mediator)
|
||||
{
|
||||
_ruleRepository = ruleRepository;
|
||||
_scoreRepository = scoreRepository;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<RiskEvaluationResult> EvaluateRiskAsync(RiskEvaluationRequest request)
|
||||
{
|
||||
var rules = await _ruleRepository.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,
|
||||
Factors = factors,
|
||||
RiskLevel = riskLevel,
|
||||
RecommendedAction = recommendedAction,
|
||||
Blocked = blocked,
|
||||
Message = blocked ? "操作被风险控制系统拒绝" : "操作已通过风险评估"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> IsAllowedAsync(RiskEvaluationRequest request)
|
||||
{
|
||||
var result = await EvaluateRiskAsync(request);
|
||||
return !result.Blocked;
|
||||
}
|
||||
|
||||
private async Task<RiskFactorResult?> EvaluateRuleAsync(RiskRule rule, RiskEvaluationRequest request)
|
||||
{
|
||||
return rule.RuleType switch
|
||||
{
|
||||
RiskRuleType.FrequencyLimit => await EvaluateFrequencyLimitAsync(rule, request),
|
||||
RiskRuleType.AmountLimit => await EvaluateAmountLimitAsync(rule, request),
|
||||
RiskRuleType.Blacklist => EvaluateBlacklist(rule, request),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<RiskFactorResult?> EvaluateFrequencyLimitAsync(RiskRule rule, RiskEvaluationRequest request)
|
||||
{
|
||||
// Frequency limit checking requires additional repository method
|
||||
// Implement when ILotteryActivityRepository.GetLotteryCountByMemberAndTypeAsync is available
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<RiskFactorResult?> EvaluateAmountLimitAsync(RiskRule rule, RiskEvaluationRequest request)
|
||||
{
|
||||
var config = rule.GetConfig<AmountLimitConfig>();
|
||||
if (request.Context.TryGetValue("amount", out var amountObj) && amountObj is int amount)
|
||||
{
|
||||
if (amount > config.MaxAmount)
|
||||
{
|
||||
return new RiskFactorResult
|
||||
{
|
||||
FactorType = "amount_limit",
|
||||
Points = config.Points,
|
||||
Description = $"超过金额限制: {amount}/{config.MaxAmount}",
|
||||
RuleName = rule.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private RiskFactorResult? EvaluateBlacklist(RiskRule rule, RiskEvaluationRequest request)
|
||||
{
|
||||
var blacklist = rule.GetConfig<BlacklistConfig>();
|
||||
if (blacklist.MemberIds.Contains(request.MemberId))
|
||||
{
|
||||
return new RiskFactorResult
|
||||
{
|
||||
FactorType = "blacklist",
|
||||
Points = BLACKLIST_POINTS,
|
||||
Description = "用户在黑名单中",
|
||||
RuleName = rule.Name
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private RiskLevel DetermineRiskLevel(int score)
|
||||
{
|
||||
return score >= HIGH_THRESHOLD ? RiskLevel.High :
|
||||
score >= MEDIUM_THRESHOLD ? RiskLevel.Medium :
|
||||
RiskLevel.Low;
|
||||
}
|
||||
|
||||
private RiskRuleAction DetermineAction(RiskLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
RiskLevel.Critical => RiskRuleAction.Block,
|
||||
RiskLevel.High => RiskRuleAction.RequireVerification,
|
||||
RiskLevel.Medium => RiskRuleAction.FlagForReview,
|
||||
_ => RiskRuleAction.Allow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class FrequencyLimitConfig
|
||||
{
|
||||
public int MaxCount { get; set; }
|
||||
public int WindowMinutes { get; set; }
|
||||
public int Points { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class AmountLimitConfig
|
||||
{
|
||||
public int MaxAmount { get; set; }
|
||||
public int Points { get; set; } = 40;
|
||||
}
|
||||
|
||||
public class BlacklistConfig
|
||||
{
|
||||
public HashSet<long> MemberIds { get; set; } = new();
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
using Fengling.RiskControl.Application.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Fengling.RiskControl.Application.Validators;
|
||||
|
||||
public class EvaluateRiskCommandValidator : AbstractValidator<EvaluateRiskCommand>
|
||||
{
|
||||
public EvaluateRiskCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.MemberId).GreaterThan(0);
|
||||
RuleFor(x => x.EntityType).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.EntityId).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.ActionType).NotEmpty().MaximumLength(50);
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Fengling.RiskControl.Infrastructure;
|
||||
|
||||
public class DesignTimeRiskControlDbContextFactory : IDesignTimeDbContextFactory<RiskControlDbContext>
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public DesignTimeRiskControlDbContextFactory(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public RiskControlDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<RiskControlDbContext>();
|
||||
optionsBuilder.UseNpgsql("Host=localhost;Database=RiskControl;Username=postgres;Password=postgres");
|
||||
optionsBuilder.UseLoggerFactory(_loggerFactory);
|
||||
return new RiskControlDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@ -1,54 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
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;
|
||||
using Fengling.RiskControl.Infrastructure.SeedData;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public void SeedData()
|
||||
{
|
||||
if (!RiskRules.Any())
|
||||
{
|
||||
RiskRules.AddRange(RiskControlSeedData.GetDefaultRules());
|
||||
SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||
|
||||
namespace Fengling.RiskControl.Infrastructure.SeedData;
|
||||
|
||||
public static class RiskControlSeedData
|
||||
{
|
||||
public static IEnumerable<RiskRule> GetDefaultRules()
|
||||
{
|
||||
yield return RiskRule.Create(
|
||||
"高频抽奖限制",
|
||||
"限制单日抽奖次数,防止沉迷",
|
||||
RiskRuleType.FrequencyLimit,
|
||||
RiskRuleAction.Block,
|
||||
"""{"maxCount": 10, "windowMinutes": 1440, "points": 30}""",
|
||||
priority: 10);
|
||||
|
||||
yield return RiskRule.Create(
|
||||
"单次大额抽奖限制",
|
||||
"单次抽奖投入不能超过1000积分",
|
||||
RiskRuleType.AmountLimit,
|
||||
RiskRuleAction.Block,
|
||||
"""{"maxAmount": 1000, "points": 40}""",
|
||||
priority: 9);
|
||||
|
||||
yield return RiskRule.Create(
|
||||
"设备异常检测",
|
||||
"同一设备频繁切换账号",
|
||||
RiskRuleType.DeviceFingerprint,
|
||||
RiskRuleAction.RequireVerification,
|
||||
"""{"maxAccountsPerDevice": 3, "windowMinutes": 60, "points": 50}""",
|
||||
priority: 8);
|
||||
|
||||
yield return RiskRule.Create(
|
||||
"IP地址异常",
|
||||
"同一IP短时间内大量请求",
|
||||
RiskRuleType.VelocityCheck,
|
||||
RiskRuleAction.RateLimit,
|
||||
"""{"maxRequests": 100, "windowMinutes": 1, "points": 20}""",
|
||||
priority: 7);
|
||||
|
||||
yield return RiskRule.Create(
|
||||
"行为模式异常",
|
||||
"检测非正常用户行为模式",
|
||||
RiskRuleType.BehaviorPattern,
|
||||
RiskRuleAction.FlagForReview,
|
||||
"""{"patterns": ["rapid_clicks", "pattern_sequences"]}""",
|
||||
priority: 5);
|
||||
|
||||
yield return RiskRule.Create(
|
||||
"VIP会员白名单",
|
||||
"VIP会员享受较低风控等级",
|
||||
RiskRuleType.Whitelist,
|
||||
RiskRuleAction.Allow,
|
||||
"""{"vipLevels": [3, 4, 5]}""",
|
||||
priority: 100);
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Fengling.RiskControl.Application.Services;
|
||||
using Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
|
||||
|
||||
namespace Fengling.RiskControl.Web.Endpoints;
|
||||
|
||||
public class ParticipateLotteryEndpoint : Endpoint<ParticipateLotteryRequest, LotteryParticipationResult>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public ParticipateLotteryEndpoint(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/v1/lottery/participate");
|
||||
Summary(s => {
|
||||
s.Summary = "参与抽奖";
|
||||
s.Description = "会员参与抽奖活动,系统自动进行风险评估";
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ParticipateLotteryRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await _mediator.Send(new LotteryParticipationRequest
|
||||
{
|
||||
MemberId = req.MemberId,
|
||||
ActivityType = req.ActivityType,
|
||||
StakePoints = req.StakePoints,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
DeviceId = HttpContext.Request.Headers["X-Device-ID"].FirstOrDefault()
|
||||
}, ct);
|
||||
|
||||
Response = (LotteryParticipationResult)result!;
|
||||
}
|
||||
}
|
||||
|
||||
public class ParticipateLotteryRequest
|
||||
{
|
||||
public long MemberId { get; set; }
|
||||
public string ActivityType { get; set; } = string.Empty;
|
||||
public int StakePoints { get; set; }
|
||||
}
|
||||
|
||||
public class GetLotteryHistoryEndpoint : Endpoint<GetLotteryHistoryRequest, IEnumerable<LotteryActivityResponse>>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public GetLotteryHistoryEndpoint(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/v1/lottery/history/{MemberId}");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetLotteryHistoryRequest req, CancellationToken ct)
|
||||
{
|
||||
var activities = await _mediator.Send(new GetMemberLotteriesQuery { MemberId = req.MemberId }, ct);
|
||||
Response = activities.Select(a => new LotteryActivityResponse
|
||||
{
|
||||
Id = a.Id,
|
||||
ActivityType = a.ActivityType,
|
||||
StakePoints = a.StakePoints,
|
||||
WinAmount = a.WinAmount,
|
||||
Status = a.Status.ToString(),
|
||||
CreatedAt = a.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class GetLotteryHistoryRequest
|
||||
{
|
||||
[Microsoft.AspNetCore.Mvc.FromRoute]
|
||||
public long MemberId { get; set; }
|
||||
}
|
||||
|
||||
public record GetMemberLotteriesQuery : IRequest<IEnumerable<LotteryActivityResponse>>
|
||||
{
|
||||
public long MemberId { get; init; }
|
||||
}
|
||||
|
||||
public record LotteryActivityResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string ActivityType { get; init; } = string.Empty;
|
||||
public int StakePoints { get; init; }
|
||||
public int? WinAmount { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
|
||||
namespace Fengling.RiskControl.Web.Endpoints;
|
||||
|
||||
public class GetPendingAlertsEndpoint : Endpoint<EmptyRequest, IEnumerable<RiskAlertResponse>>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public GetPendingAlertsEndpoint(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/v1/risk/alerts/pending");
|
||||
Roles("Admin");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
|
||||
{
|
||||
var alerts = await _mediator.Send(new GetPendingAlertsQuery(), ct);
|
||||
Response = alerts.Select(a => new RiskAlertResponse
|
||||
{
|
||||
Id = a.Id,
|
||||
MemberId = a.MemberId,
|
||||
AlertType = a.AlertType,
|
||||
Description = a.Description,
|
||||
Priority = a.Priority.ToString(),
|
||||
Status = a.Status.ToString(),
|
||||
CreatedAt = a.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class GetPendingAlertsQuery : IRequest<IEnumerable<RiskAlertResponse>>
|
||||
{
|
||||
}
|
||||
|
||||
public record RiskAlertResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public long MemberId { get; init; }
|
||||
public string AlertType { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Priority { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
public class ResolveAlertEndpoint : Endpoint<ResolveAlertRequest, RiskAlertResponse>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public ResolveAlertEndpoint(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/v1/risk/alerts/{AlertId}/resolve");
|
||||
Roles("Admin");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ResolveAlertRequest req, CancellationToken ct)
|
||||
{
|
||||
var alert = await _mediator.Send(new ResolveAlertCommand
|
||||
{
|
||||
AlertId = req.AlertId,
|
||||
Notes = req.Notes
|
||||
}, ct);
|
||||
|
||||
Response = alert;
|
||||
}
|
||||
}
|
||||
|
||||
public class ResolveAlertRequest
|
||||
{
|
||||
[Microsoft.AspNetCore.Mvc.FromRoute]
|
||||
public long AlertId { get; set; }
|
||||
public string Notes { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record ResolveAlertCommand : IRequest<RiskAlertResponse>
|
||||
{
|
||||
public long AlertId { get; init; }
|
||||
public string Notes { get; init; } = string.Empty;
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Fengling.RiskControl.Application.Services;
|
||||
using Fengling.RiskControl.Application.Commands;
|
||||
|
||||
namespace Fengling.RiskControl.Web.Endpoints;
|
||||
|
||||
public class EvaluateRiskEndpoint : Endpoint<EvaluateRiskRequest, RiskEvaluationResult>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public EvaluateRiskEndpoint(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/v1/risk/evaluate");
|
||||
Summary(s => {
|
||||
s.Summary = "风险评估";
|
||||
s.Description = "对会员进行实时风险评估";
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(EvaluateRiskRequest req, CancellationToken ct)
|
||||
{
|
||||
var result = await _mediator.Send(new EvaluateRiskCommand
|
||||
{
|
||||
MemberId = req.MemberId,
|
||||
EntityType = req.EntityType,
|
||||
EntityId = req.EntityId,
|
||||
ActionType = req.ActionType,
|
||||
Context = req.Context ?? new()
|
||||
}, ct);
|
||||
|
||||
Response = result;
|
||||
}
|
||||
}
|
||||
|
||||
public class EvaluateRiskRequest
|
||||
{
|
||||
public long MemberId { get; set; }
|
||||
public string EntityType { get; set; } = string.Empty;
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
public string ActionType { get; set; } = string.Empty;
|
||||
public Dictionary<string, object>? Context { get; set; }
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
using FastEndpoints;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||
|
||||
namespace Fengling.RiskControl.Web.Endpoints;
|
||||
|
||||
public class CreateRiskRuleEndpoint : Endpoint<CreateRiskRuleRequest, RiskRuleResponse>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public CreateRiskRuleEndpoint(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/v1/risk/rules");
|
||||
Roles("Admin");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateRiskRuleRequest req, CancellationToken ct)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(RiskRuleType), req.RuleType))
|
||||
{
|
||||
ThrowError($"无效的规则类型: {req.RuleType}");
|
||||
}
|
||||
if (!Enum.IsDefined(typeof(RiskRuleAction), req.Action))
|
||||
{
|
||||
ThrowError($"无效的规则动作: {req.Action}");
|
||||
}
|
||||
|
||||
var rule = await _mediator.Send(new CreateRiskRuleCommand
|
||||
{
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
RuleType = (RiskRuleType)req.RuleType,
|
||||
Action = (RiskRuleAction)req.Action,
|
||||
ConfigJson = req.ConfigJson,
|
||||
Priority = req.Priority
|
||||
}, ct);
|
||||
|
||||
Response = new RiskRuleResponse
|
||||
{
|
||||
Id = rule.Id,
|
||||
Name = rule.Name,
|
||||
Description = rule.Description,
|
||||
RuleType = ((int)rule.RuleType).ToString(),
|
||||
Action = ((int)rule.Action).ToString(),
|
||||
IsActive = rule.IsActive,
|
||||
CreatedAt = rule.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateRiskRuleRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int RuleType { get; set; }
|
||||
public int Action { get; set; }
|
||||
public string ConfigJson { get; set; } = string.Empty;
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
public record CreateRiskRuleCommand : IRequest<RiskRule>
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public RiskRuleType RuleType { get; init; }
|
||||
public RiskRuleAction Action { get; init; }
|
||||
public string ConfigJson { get; init; } = string.Empty;
|
||||
public int Priority { get; init; }
|
||||
}
|
||||
|
||||
public record RiskRuleResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string RuleType { get; init; } = string.Empty;
|
||||
public string Action { get; init; } = string.Empty;
|
||||
public bool IsActive { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<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>
|
||||
@ -1,46 +0,0 @@
|
||||
using Fengling.RiskControl.Infrastructure;
|
||||
using Fengling.RiskControl.Infrastructure.Repositories;
|
||||
using Fengling.RiskControl.Domain.Repositories;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskRules;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskScores;
|
||||
using Fengling.RiskControl.Domain.Aggregates.RiskAlerts;
|
||||
using Fengling.RiskControl.Domain.Aggregates.LotteryActivities;
|
||||
using Fengling.RiskControl.Application.Services;
|
||||
using FastEndpoints;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddFastEndpoints();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddDbContext<RiskControlDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("RiskControl")));
|
||||
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>(lifetime: ServiceLifetime.Scoped);
|
||||
|
||||
builder.Services.AddScoped<IRiskRuleRepository, RiskRuleRepository>();
|
||||
builder.Services.AddScoped<IRiskScoreRepository, RiskScoreRepository>();
|
||||
builder.Services.AddScoped<IRiskAlertRepository, RiskAlertRepository>();
|
||||
builder.Services.AddScoped<ILotteryActivityRepository, LotteryActivityRepository>();
|
||||
|
||||
builder.Services.AddScoped<IRiskEvaluationService, RiskEvaluationService>();
|
||||
builder.Services.AddScoped<ILotteryService, LotteryService>();
|
||||
builder.Services.AddScoped<IRiskAlertService, RiskAlertService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseFastEndpoints();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.Run();
|
||||
Loading…
Reference in New Issue
Block a user