feat(risk-control): add member integration and CAP subscriptions
This commit is contained in:
parent
f76f81ec55
commit
d0650ba24a
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,5 +12,6 @@
|
||||
<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>
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user