feat(risk-control): add member integration and CAP subscriptions

This commit is contained in:
Sam 2026-02-05 15:22:21 +08:00
parent f76f81ec55
commit d0650ba24a
6 changed files with 255 additions and 0 deletions

View File

@ -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<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 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);
}
}
}

View File

@ -12,5 +12,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Fengling.RiskControl.Domain\Fengling.RiskControl.Domain.csproj" /> <ProjectReference Include="..\Fengling.RiskControl.Domain\Fengling.RiskControl.Domain.csproj" />
<ProjectReference Include="..\Fengling.RiskControl.Infrastructure\Fengling.RiskControl.Infrastructure.csproj" /> <ProjectReference Include="..\Fengling.RiskControl.Infrastructure\Fengling.RiskControl.Infrastructure.csproj" />
<ProjectReference Include="..\..\Fengling.Member\src\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,25 @@
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; }
}

View File

@ -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<OrderDiscountValidationResult> ValidateDiscountAsync(OrderDiscountValidationRequest request);
}

View File

@ -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<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;
}
}
}

View File

@ -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<OrderDiscountValidationResult> 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<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,
_ => 0
};
return new OrderDiscountValidationResult
{
IsAllowed = request.DiscountAmount <= maxDiscount,
Reason = request.DiscountAmount <= maxDiscount ? "折扣申请通过" : $"超过最大允许折扣: {maxDiscount}",
MaxDiscountAllowed = maxDiscount,
RiskDetails = riskResult
};
}
}