using System.Linq.Expressions; 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.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; /// /// 优惠券发放与实例查询 API。 /// [ApiExplorerSettings(GroupName = "OMS-Marketing")] public class MarketingCouponGrantService : ApplicationService { private ICouponTemplateRepository TemplateRepository => LazyServiceProvider.GetRequiredService(); private ICouponInstanceRepository InstanceRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); private MarketingCouponManager CouponManager => LazyServiceProvider.GetRequiredService(); /// /// 批量发券(子账号编号自动归主客户)。 /// [HttpPost("/marketing/coupon/grant")] public async Task GrantAsync(CouponGrantInput input) { if (input.CustomerCodes.Count == 0) { throw new BusinessException("客户编号不能为空"); } var template = await TemplateRepository.FirstOrDefaultAsync(x => x.Id == input.TemplateId) ?? throw new BusinessException("券模板不存在"); var quantity = Math.Clamp(input.QuantityPerCustomer, 1, 10); var grantedMainIds = await ResolveGrantMainCustomerIdsAsync(input.CustomerCodes); foreach (var mainCustomerId in grantedMainIds) { for (var i = 0; i < quantity; i++) { await CouponManager.GrantAsync(template.Id, mainCustomerId, input.Remark); } } } /// /// 批量发券预览(不落库)。 /// [HttpPost("/marketing/coupon/grant/preview")] public async Task PreviewGrantAsync(CouponGrantInput input) { if (input.CustomerCodes.Count == 0) { throw new BusinessException("客户编号不能为空"); } var template = await TemplateRepository.FirstOrDefaultAsync(x => x.Id == input.TemplateId) ?? throw new BusinessException("券模板不存在"); var quantity = Math.Clamp(input.QuantityPerCustomer, 1, 10); var grantedMainIds = await ResolveGrantMainCustomerIdsAsync(input.CustomerCodes); var customerCount = grantedMainIds.Count; var totalCoupons = customerCount * quantity; var maxPerCoupon = EstimateTemplateMaxDiscount(template); return new CouponGrantPreviewOutput { CustomerCount = customerCount, TotalCoupons = totalCoupons, EstimatedMaxDiscount = maxPerCoupon * totalCoupons, TemplateName = template.TemplateName }; } /// /// 分页查询券实例。 /// [HttpPost("/marketing/coupon-instance/list")] public async Task> GetInstanceListAsync(CouponInstanceListInput input) { var predicate = BuildInstancePredicate(input); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await InstanceRepository.GetPagedListAsync( predicate, pageIndex, pageSize, new Dictionary { [nameof(CouponInstance.ReceiveTime)] = false, [nameof(CouponInstance.Id)] = false }); var total = await InstanceRepository.CountAsync(predicate); return new OmsPageResult { Items = await MapInstanceOutputsAsync(entities), Total = total }; } private async Task> ResolveGrantMainCustomerIdsAsync(IEnumerable customerCodes) { var grantedMainIds = new HashSet(); foreach (var rawCode in customerCodes) { var code = rawCode?.Trim(); if (string.IsNullOrWhiteSpace(code)) { continue; } var customer = await CustomerRepository.GetByCustomerCodeAsync(code) ?? throw new BusinessException($"客户编号 {code} 不存在"); var mainCustomerId = await CouponManager.ResolveMainCustomerIdAsync(customer.Id); grantedMainIds.Add(mainCustomerId); } return grantedMainIds; } private static decimal EstimateTemplateMaxDiscount(CouponTemplate template) { if (string.Equals(template.CouponType, MarketingCouponTypes.FixedOff, StringComparison.OrdinalIgnoreCase) || string.Equals(template.CouponType, MarketingCouponTypes.DirectOff, StringComparison.OrdinalIgnoreCase)) { return template.FaceValue ?? 0M; } return template.MaxDiscountAmount ?? template.FaceValue ?? 0M; } private static Expression> BuildInstancePredicate(CouponInstanceListInput input) { var keyword = input.Keyword?.Trim(); var hasKeyword = !string.IsNullOrWhiteSpace(keyword); var hasTemplate = input.TemplateId is > 0; var templateId = input.TemplateId.GetValueOrDefault(); var hasCustomer = input.CustomerId is > 0; var customerId = input.CustomerId.GetValueOrDefault(); var status = input.Status; var hasStatus = status.HasValue; return x => (!hasTemplate || x.TemplateId == templateId) && (!hasCustomer || x.CustomerId == customerId) && (!hasStatus || x.Status == status!.Value) && (!hasKeyword || x.CouponCode.Contains(keyword!)); } private async Task> MapInstanceOutputsAsync(List entities) { var templateIds = entities.Select(x => x.TemplateId).Distinct().ToArray(); var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray(); var templates = templateIds.Length == 0 ? [] : await TemplateRepository.GetListAsync(x => templateIds.Contains(x.Id)); var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id)); var templateMap = templates.ToDictionary(x => x.Id); var customerMap = customers.ToDictionary(x => x.Id); return entities.Select(entity => { templateMap.TryGetValue(entity.TemplateId, out var template); customerMap.TryGetValue(entity.CustomerId, out var customer); return MapInstanceOutput(entity, template, customer?.CustomerCode); }).ToList(); } internal static CouponInstanceOutput MapInstanceOutput( CouponInstance entity, CouponTemplate? template, string? customerCode) => new() { Id = entity.Id, TemplateId = entity.TemplateId, TemplateName = template?.TemplateName, CouponType = template?.CouponType, CustomerId = entity.CustomerId, CustomerCode = customerCode, CouponCode = entity.CouponCode, Status = entity.Status, StatusName = MarketingEnumNames.Of(entity.Status), ReceiveTime = entity.ReceiveTime, ValidStartTime = entity.ValidStartTime, ValidEndTime = entity.ValidEndTime, UseTime = entity.UseTime, OrderId = entity.OrderId, LockExpireTime = entity.LockExpireTime }; }