Implement referral, points, coupons, commission, member levels, and order integration with migration SQL and background jobs. Co-authored-by: Cursor <cursoragent@cursor.com>
219 lines
7.2 KiB
C#
219 lines
7.2 KiB
C#
using Fake.DependencyInjection;
|
|
using Fake.Application;
|
|
using Fake.Domain.Exceptions;
|
|
using Fake.Security.Claims;
|
|
using Fake.Users;
|
|
using Oms.Domain.CustomersAggregate.Repositories;
|
|
using Oms.Domain.MarketingAggregate.Consts;
|
|
using Oms.Domain.MarketingAggregate.Entities;
|
|
using Oms.Domain.MarketingAggregate.Enums;
|
|
using Oms.Domain.MarketingAggregate.Repositories;
|
|
using Oms.Domain.OrdersAggregate.Repositories;
|
|
using System.Text.Json;
|
|
|
|
namespace Oms.Application.Shared;
|
|
|
|
/// <summary>
|
|
/// 积分发放、调账与等级升级。
|
|
/// </summary>
|
|
public class MarketingPointsManager : IScopedDependency
|
|
{
|
|
private readonly IMemberPointsRuleRepository _ruleRepository;
|
|
private readonly ICustomerPointsAccountRepository _accountRepository;
|
|
private readonly ICustomerPointsFlowRepository _flowRepository;
|
|
private readonly IMemberLevelRepository _levelRepository;
|
|
private readonly ICustomerRepository _customerRepository;
|
|
private readonly IOrderRepository _orderRepository;
|
|
private readonly IMarketingAuditLogRepository _auditLogRepository;
|
|
private readonly ICurrentUser _currentUser;
|
|
|
|
public MarketingPointsManager(
|
|
IMemberPointsRuleRepository ruleRepository,
|
|
ICustomerPointsAccountRepository accountRepository,
|
|
ICustomerPointsFlowRepository flowRepository,
|
|
IMemberLevelRepository levelRepository,
|
|
ICustomerRepository customerRepository,
|
|
IOrderRepository orderRepository,
|
|
IMarketingAuditLogRepository auditLogRepository,
|
|
ICurrentUser currentUser)
|
|
{
|
|
_ruleRepository = ruleRepository;
|
|
_accountRepository = accountRepository;
|
|
_flowRepository = flowRepository;
|
|
_levelRepository = levelRepository;
|
|
_customerRepository = customerRepository;
|
|
_orderRepository = orderRepository;
|
|
_auditLogRepository = auditLogRepository;
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task EarnOnOrderPaidAsync(int mainCustomerId, int orderId, decimal payableAmount, CancellationToken cancellationToken = default)
|
|
{
|
|
if (payableAmount <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (await _flowRepository.ExistsEarnForOrderAsync(mainCustomerId, orderId, cancellationToken))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var rule = await _ruleRepository.GetActiveRuleAsync(cancellationToken);
|
|
if (rule == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var points = (int)Math.Floor(payableAmount * rule.PointsPerYuan);
|
|
if (points <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isFirstOrder = !await _orderRepository.AnyAsync(
|
|
x => x.CustomerId == mainCustomerId && x.IsPaid > 0 && x.Id != orderId,
|
|
cancellationToken);
|
|
if (isFirstOrder && rule.FirstOrderMultiplier > 1)
|
|
{
|
|
points = (int)Math.Floor(points * rule.FirstOrderMultiplier);
|
|
}
|
|
|
|
if (rule.MaxPointsPerOrder > 0)
|
|
{
|
|
points = Math.Min(points, rule.MaxPointsPerOrder);
|
|
}
|
|
|
|
await ApplyPointsChangeAsync(
|
|
mainCustomerId,
|
|
PointsChangeType.Earn,
|
|
points,
|
|
MarketingPointsBizTypes.OrderPay,
|
|
orderId,
|
|
null,
|
|
"订单支付积分",
|
|
cancellationToken);
|
|
await TryUpgradeLevelAsync(mainCustomerId, cancellationToken);
|
|
}
|
|
|
|
public async Task AdjustAsync(int mainCustomerId, int points, string remark, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(remark) || remark.Trim().Length < 10)
|
|
{
|
|
throw new BusinessException("调账原因至少 10 个字");
|
|
}
|
|
|
|
await ApplyPointsChangeAsync(
|
|
mainCustomerId,
|
|
PointsChangeType.Adjust,
|
|
points,
|
|
MarketingPointsBizTypes.AdminAdjust,
|
|
null,
|
|
null,
|
|
remark.Trim(),
|
|
cancellationToken);
|
|
|
|
await _auditLogRepository.InsertAsync(new MarketingAuditLog
|
|
{
|
|
Action = MarketingAuditActions.AdjustPoints,
|
|
OperatorId = _currentUser.GetId<int>(),
|
|
TargetType = "customer",
|
|
TargetId = mainCustomerId,
|
|
AfterJson = JsonSerializer.Serialize(new { points }),
|
|
Remark = remark.Trim()
|
|
}, cancellationToken);
|
|
|
|
if (points > 0)
|
|
{
|
|
await TryUpgradeLevelAsync(mainCustomerId, cancellationToken);
|
|
}
|
|
}
|
|
|
|
public async Task<CustomerPointsAccount> GetOrCreateAccountAsync(int mainCustomerId, CancellationToken cancellationToken = default)
|
|
{
|
|
var account = await _accountRepository.GetByCustomerIdAsync(mainCustomerId, cancellationToken);
|
|
if (account != null)
|
|
{
|
|
return account;
|
|
}
|
|
|
|
account = new CustomerPointsAccount
|
|
{
|
|
CustomerId = mainCustomerId,
|
|
TotalPoints = 0,
|
|
AvailablePoints = 0,
|
|
Version = 0
|
|
};
|
|
await _accountRepository.InsertAsync(account, cancellationToken);
|
|
return account;
|
|
}
|
|
|
|
private async Task ApplyPointsChangeAsync(
|
|
int mainCustomerId,
|
|
PointsChangeType changeType,
|
|
int points,
|
|
string bizType,
|
|
int? bizId,
|
|
string? bizNo,
|
|
string? remark,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (points == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var account = await GetOrCreateAccountAsync(mainCustomerId, cancellationToken);
|
|
if (changeType == PointsChangeType.Earn || changeType == PointsChangeType.Adjust)
|
|
{
|
|
if (points < 0 && account.AvailablePoints + points < 0)
|
|
{
|
|
throw new BusinessException("可用积分不足");
|
|
}
|
|
|
|
account.TotalPoints = Math.Max(0, account.TotalPoints + Math.Max(0, points));
|
|
account.AvailablePoints += points;
|
|
}
|
|
else
|
|
{
|
|
account.AvailablePoints += points;
|
|
}
|
|
|
|
await _accountRepository.UpdateAsync(account, cancellationToken);
|
|
await _flowRepository.InsertAsync(new CustomerPointsFlow
|
|
{
|
|
CustomerId = mainCustomerId,
|
|
ChangeType = changeType,
|
|
Points = points,
|
|
BizType = bizType,
|
|
BizId = bizId,
|
|
BizNo = bizNo,
|
|
Remark = remark
|
|
}, cancellationToken);
|
|
}
|
|
|
|
private async Task TryUpgradeLevelAsync(int mainCustomerId, CancellationToken cancellationToken = default)
|
|
{
|
|
var account = await GetOrCreateAccountAsync(mainCustomerId, cancellationToken);
|
|
var levels = await _levelRepository.GetAllEnabledOrderedAsync(cancellationToken);
|
|
var target = levels
|
|
.Where(x => x.MinPoints <= account.TotalPoints)
|
|
.OrderByDescending(x => x.MinPoints)
|
|
.ThenByDescending(x => x.Sort)
|
|
.FirstOrDefault();
|
|
if (target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var customer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == mainCustomerId, cancellationToken);
|
|
if (customer == null || customer.MemberLevelId == target.Id)
|
|
{
|
|
return;
|
|
}
|
|
|
|
customer.MemberLevelId = target.Id;
|
|
await _customerRepository.UpdateAsync(customer, cancellationToken);
|
|
}
|
|
}
|