diff --git a/Fengling.RiskControl.Application/Events/MemberEventSubscriptions.cs b/Fengling.RiskControl.Application/Events/MemberEventSubscriptions.cs new file mode 100644 index 0000000..2e4eb9e --- /dev/null +++ b/Fengling.RiskControl.Application/Events/MemberEventSubscriptions.cs @@ -0,0 +1,50 @@ +using Fengling.RiskControl.Domain.Aggregates.RiskScores; +using Fengling.RiskControl.Domain.Repositories; +using MediatR; + +namespace Fengling.RiskControl.Application.Events; + +public class MemberRegisteredEventHandler : INotificationHandler +{ + 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 +{ + 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 > 1000) + { + var score = RiskScore.Create( + notification.MemberId, + "large_point_change", + notification.AccountId.ToString()); + + score.AddRiskFactor("large_point_change", 25, $"大额积分变动: {notification.ChangedPoints}"); + await _scoreRepository.AddAsync(score); + } + } +} diff --git a/Fengling.RiskControl.Application/Fengling.RiskControl.Application.csproj b/Fengling.RiskControl.Application/Fengling.RiskControl.Application.csproj index 9828993..61e33da 100644 --- a/Fengling.RiskControl.Application/Fengling.RiskControl.Application.csproj +++ b/Fengling.RiskControl.Application/Fengling.RiskControl.Application.csproj @@ -12,5 +12,6 @@ + diff --git a/Fengling.RiskControl.Application/Services/IMemberIntegrationService.cs b/Fengling.RiskControl.Application/Services/IMemberIntegrationService.cs new file mode 100644 index 0000000..8282c3d --- /dev/null +++ b/Fengling.RiskControl.Application/Services/IMemberIntegrationService.cs @@ -0,0 +1,25 @@ +namespace Fengling.RiskControl.Application.Services; + +public interface IMemberIntegrationService +{ + Task GetMemberPointsBalanceAsync(long memberId); + Task DeductPointsAsync(long memberId, int points, string reason); + Task AddPointsAsync(long memberId, int points, string reason); + Task 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; } +} diff --git a/Fengling.RiskControl.Application/Services/IOrderRiskValidationService.cs b/Fengling.RiskControl.Application/Services/IOrderRiskValidationService.cs new file mode 100644 index 0000000..86a0d7e --- /dev/null +++ b/Fengling.RiskControl.Application/Services/IOrderRiskValidationService.cs @@ -0,0 +1,23 @@ +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 ValidateDiscountAsync(OrderDiscountValidationRequest request); +} diff --git a/Fengling.RiskControl.Application/Services/MemberIntegrationService.cs b/Fengling.RiskControl.Application/Services/MemberIntegrationService.cs new file mode 100644 index 0000000..4203b2d --- /dev/null +++ b/Fengling.RiskControl.Application/Services/MemberIntegrationService.cs @@ -0,0 +1,93 @@ +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 _logger; + + public MemberIntegrationService(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task 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(); + return result?.AvailableBalance ?? 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting points balance for member {MemberId}", memberId); + return 0; + } + } + + public async Task 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 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 GetMemberInfoAsync(long memberId) + { + try + { + var response = await _httpClient.GetAsync($"/api/v1/members/{memberId}"); + if (!response.IsSuccessStatusCode) + return null; + + return await response.Content.ReadFromJsonAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting member info for {MemberId}", memberId); + return null; + } + } +} diff --git a/Fengling.RiskControl.Application/Services/OrderRiskValidationService.cs b/Fengling.RiskControl.Application/Services/OrderRiskValidationService.cs new file mode 100644 index 0000000..8aab84c --- /dev/null +++ b/Fengling.RiskControl.Application/Services/OrderRiskValidationService.cs @@ -0,0 +1,63 @@ +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 ValidateDiscountAsync(OrderDiscountValidationRequest request) + { + 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 + { + ["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, + _ => 0 + }; + + return new OrderDiscountValidationResult + { + IsAllowed = request.DiscountAmount <= maxDiscount, + Reason = request.DiscountAmount <= maxDiscount ? "折扣申请通过" : $"超过最大允许折扣: {maxDiscount}", + MaxDiscountAllowed = maxDiscount, + RiskDetails = riskResult + }; + } +}