admin/src/Oms.Application/Services/Marketing/MarketingCommissionAdminService.cs
zzh 60cb2c5e0f fix: document marketing APIs and hide Order batch helpers from Swagger
Add Chinese XML summaries for OMS-Marketing endpoints, and mark OrderService batch worker methods internal to stop OMS-Order swagger generation conflicts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 11:09:55 +08:00

333 lines
13 KiB
C#

using System.Linq.Expressions;
using System.Text.Json;
using Fake.Application;
using Fake.Domain.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Marketing;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Entities;
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;
namespace Oms.Application.Services.Marketing;
/// <summary>
/// ????????? API??????
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Marketing")]
public class MarketingCommissionAdminService : ApplicationService
{
private IReferralCommissionDetailRepository DetailRepository =>
LazyServiceProvider.GetRequiredService<IReferralCommissionDetailRepository>();
private ICommissionWithdrawRequestRepository WithdrawRepository =>
LazyServiceProvider.GetRequiredService<ICommissionWithdrawRequestRepository>();
private ICustomerRepository CustomerRepository =>
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private IMarketingAuditLogRepository AuditLogRepository =>
LazyServiceProvider.GetRequiredService<IMarketingAuditLogRepository>();
private CommissionBalanceMutator CommissionBalanceMutator =>
LazyServiceProvider.GetRequiredService<CommissionBalanceMutator>();
/// <summary>
/// ?????????
/// </summary>
[HttpPost("/marketing/commission-detail/list")]
public async Task<OmsPageResult<CommissionDetailOutput>> GetDetailListAsync(CommissionDetailListInput input)
{
var predicate = BuildDetailPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await DetailRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(ReferralCommissionDetail.CreateTime)] = false, [nameof(ReferralCommissionDetail.Id)] = false });
var total = await DetailRepository.CountAsync(predicate);
return new OmsPageResult<CommissionDetailOutput>
{
Items = await MapDetailOutputsAsync(entities),
Total = total
};
}
/// <summary>
/// ?????????
/// </summary>
[HttpPost("/marketing/commission-detail/adjust")]
public async Task AdjustAsync(CommissionAdjustInput input)
{
if (input.Amount == 0)
{
throw new BusinessException("Adjust amount cannot be zero");
}
if (string.IsNullOrWhiteSpace(input.Remark) || input.Remark.Trim().Length < 10)
{
throw new BusinessException("Remark must be at least 10 characters");
}
var customer = await ResolveCustomerAsync(input.CustomerId, input.CustomerCode);
if (customer.ParentCustomerId.HasValue)
{
throw new BusinessException("Adjust main customer only");
}
var before = customer.CommissionBalance;
await CommissionBalanceMutator.AdjustCommissionAsync(customer, input.Amount, input.DetailId, input.Remark.Trim());
await AuditLogRepository.InsertAsync(new MarketingAuditLog
{
Action = MarketingAuditActions.AdjustCommission,
OperatorId = CurrentUser.GetId<int>(),
TargetType = "customer",
TargetId = customer.Id,
BeforeJson = JsonSerializer.Serialize(new { commissionBalance = before }),
AfterJson = JsonSerializer.Serialize(new { commissionBalance = before + input.Amount, input.Amount }),
Remark = input.Remark.Trim()
});
}
/// <summary>
/// ?????????
/// </summary>
[HttpPost("/marketing/commission-withdraw/list")]
public async Task<OmsPageResult<CommissionWithdrawOutput>> GetWithdrawListAsync(CommissionWithdrawListInput input)
{
await ResolveWithdrawCustomerIdAsync(input);
var predicate = BuildWithdrawPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await WithdrawRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(CommissionWithdrawRequest.CreateTime)] = false, [nameof(CommissionWithdrawRequest.Id)] = false });
var total = await WithdrawRepository.CountAsync(predicate);
return new OmsPageResult<CommissionWithdrawOutput>
{
Items = await MapWithdrawOutputsAsync(entities),
Total = total
};
}
private async Task ResolveWithdrawCustomerIdAsync(CommissionWithdrawListInput input)
{
if (input.CustomerId is > 0)
{
return;
}
var code = input.CustomerCode?.Trim();
if (string.IsNullOrWhiteSpace(code))
{
return;
}
var customer = await CustomerRepository.GetByCustomerCodeAsync(code)
?? throw new BusinessException($"???? {code} ???");
input.CustomerId = customer.ParentCustomerId ?? customer.Id;
}
/// <summary>
/// ?????????
/// </summary>
[HttpPost("/marketing/commission-withdraw/approve")]
public async Task ApproveWithdrawAsync(CommissionWithdrawApproveInput input)
{
var request = await GetWithdrawOrThrowAsync(input.Id);
if (request.Status != WithdrawStatus.Pending)
{
throw new BusinessException("Only pending requests can be approved");
}
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == request.CustomerId)
?? throw new BusinessException("Customer not found");
if (request.WithdrawType == CommissionWithdrawType.ToBalance)
{
await CommissionBalanceMutator.DebitForTransferToBalanceAsync(
customer, request.Amount, request.Id, request.SerialNumber, input.AuditRemark ?? "Commission to balance");
}
else
{
await CommissionBalanceMutator.DebitForWithdrawAsync(
customer, request.Amount, request.Id, request.SerialNumber, input.AuditRemark ?? "Commission withdraw");
}
request.Status = WithdrawStatus.Approved;
request.AuditRemark = input.AuditRemark;
request.AuditBy = CurrentUser.UserName;
request.AuditTime = DateTime.Now;
await WithdrawRepository.UpdateAsync(request);
}
/// <summary>
/// ???????
/// </summary>
[HttpPost("/marketing/commission-withdraw/reject")]
public async Task RejectWithdrawAsync(CommissionWithdrawRejectInput input)
{
var request = await GetWithdrawOrThrowAsync(input.Id);
if (request.Status != WithdrawStatus.Pending)
{
throw new BusinessException("Only pending requests can be rejected");
}
if (string.IsNullOrWhiteSpace(input.AuditRemark))
{
throw new BusinessException("Reject reason is required");
}
request.Status = WithdrawStatus.Rejected;
request.AuditRemark = input.AuditRemark.Trim();
request.AuditBy = CurrentUser.UserName;
request.AuditTime = DateTime.Now;
await WithdrawRepository.UpdateAsync(request);
}
private static Expression<Func<ReferralCommissionDetail, bool>> BuildDetailPredicate(CommissionDetailListInput input)
{
var orderNo = input.OrderNo?.Trim();
var hasOrderNo = !string.IsNullOrWhiteSpace(orderNo);
var hasReferrer = input.ReferrerCustomerId is > 0;
var referrerId = input.ReferrerCustomerId.GetValueOrDefault();
var hasReferee = input.RefereeCustomerId is > 0;
var refereeId = input.RefereeCustomerId.GetValueOrDefault();
var status = input.Status;
var hasStatus = status.HasValue;
return x =>
(!hasReferrer || x.ReferrerCustomerId == referrerId) &&
(!hasReferee || x.RefereeCustomerId == refereeId) &&
(!hasStatus || x.Status == status!.Value) &&
(!hasOrderNo || x.OrderNo.Contains(orderNo!));
}
private static Expression<Func<CommissionWithdrawRequest, bool>> BuildWithdrawPredicate(CommissionWithdrawListInput input)
{
var hasCustomer = input.CustomerId is > 0;
var customerId = input.CustomerId.GetValueOrDefault();
var withdrawType = input.WithdrawType;
var hasWithdrawType = withdrawType.HasValue;
var status = input.Status;
var hasStatus = status.HasValue;
return x =>
(!hasCustomer || x.CustomerId == customerId) &&
(!hasWithdrawType || x.WithdrawType == withdrawType!.Value) &&
(!hasStatus || x.Status == status!.Value);
}
private async Task<List<CommissionDetailOutput>> MapDetailOutputsAsync(List<ReferralCommissionDetail> entities)
{
var customerIds = entities.SelectMany(x => new[] { x.ReferrerCustomerId, x.RefereeCustomerId }).Distinct().ToArray();
var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return entities.Select(entity =>
{
customerMap.TryGetValue(entity.ReferrerCustomerId, out var referrer);
customerMap.TryGetValue(entity.RefereeCustomerId, out var referee);
return new CommissionDetailOutput
{
Id = entity.Id,
ReferrerCustomerId = entity.ReferrerCustomerId,
ReferrerCustomerCode = referrer?.CustomerCode,
RefereeCustomerId = entity.RefereeCustomerId,
RefereeCustomerCode = referee?.CustomerCode,
OrderId = entity.OrderId,
OrderNo = entity.OrderNo,
BasePayableAmount = entity.BasePayableAmount,
CommissionAmount = entity.CommissionAmount,
Status = entity.Status,
StatusName = MarketingEnumNames.Of(entity.Status),
EligibleTime = entity.EligibleTime,
SettleTime = entity.SettleTime,
CancelReason = entity.CancelReason,
CreateTime = entity.CreateTime
};
}).ToList();
}
private async Task<List<CommissionWithdrawOutput>> MapWithdrawOutputsAsync(List<CommissionWithdrawRequest> entities)
{
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return entities.Select(entity =>
{
customerMap.TryGetValue(entity.CustomerId, out var customer);
return new CommissionWithdrawOutput
{
Id = entity.Id,
CustomerId = entity.CustomerId,
CustomerCode = customer?.CustomerCode,
SerialNumber = entity.SerialNumber,
Amount = entity.Amount,
CommissionBalance = customer?.CommissionBalance ?? 0,
WithdrawType = entity.WithdrawType,
WithdrawTypeName = MarketingEnumNames.Of(entity.WithdrawType),
Status = entity.Status,
StatusName = MarketingEnumNames.Of(entity.Status),
Remark = entity.Remark,
AuditRemark = entity.AuditRemark,
AuditBy = entity.AuditBy,
AuditTime = entity.AuditTime,
CreateTime = entity.CreateTime
};
}).ToList();
}
internal static CommissionWithdrawOutput MapWithdrawOutput(
CommissionWithdrawRequest entity,
string? customerCode,
decimal commissionBalance = 0) => new()
{
Id = entity.Id,
CustomerId = entity.CustomerId,
CustomerCode = customerCode,
SerialNumber = entity.SerialNumber,
Amount = entity.Amount,
CommissionBalance = commissionBalance,
WithdrawType = entity.WithdrawType,
WithdrawTypeName = MarketingEnumNames.Of(entity.WithdrawType),
Status = entity.Status,
StatusName = MarketingEnumNames.Of(entity.Status),
Remark = entity.Remark,
AuditRemark = entity.AuditRemark,
AuditBy = entity.AuditBy,
AuditTime = entity.AuditTime,
CreateTime = entity.CreateTime
};
private async Task<Customer> ResolveCustomerAsync(
int? customerId,
string? customerCode)
{
if (customerId is > 0)
{
return await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId.Value)
?? throw new BusinessException("Customer not found");
}
var code = customerCode?.Trim();
if (string.IsNullOrWhiteSpace(code))
{
throw new BusinessException("CustomerId or CustomerCode required");
}
return await CustomerRepository.GetByCustomerCodeAsync(code)
?? throw new BusinessException("Customer not found");
}
private async Task<CommissionWithdrawRequest> GetWithdrawOrThrowAsync(int id)
{
return await WithdrawRepository.FirstOrDefaultAsync(x => x.Id == id)
?? throw new BusinessException("Withdraw request not found");
}
}