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