admin/src/Oms.Application/Shared/MarketingPointsManager.cs
zzh 89233bc3da feat: add customer marketing module (M1-M6 backend)
Implement referral, points, coupons, commission, member levels, and order integration with migration SQL and background jobs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 16:42:08 +08:00

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