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.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 MarketingCouponTemplateService : ApplicationService { private ICouponTemplateRepository TemplateRepository => LazyServiceProvider.GetRequiredService(); /// /// 分页查询优惠券模板。 /// [HttpPost("/marketing/coupon-template/list")] public async Task> GetListAsync(CouponTemplateListInput input) { var predicate = BuildListPredicate(input); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await TemplateRepository.GetPagedListAsync( predicate, pageIndex, pageSize, new Dictionary { [nameof(CouponTemplate.UpdateTime)] = false, [nameof(CouponTemplate.Id)] = false }); var total = await TemplateRepository.CountAsync(predicate); return new OmsPageResult { Items = entities.Select(MapOutput).ToList(), Total = total }; } /// /// 新增优惠券模板(草稿)。 /// [HttpPost("/marketing/coupon-template/create")] public async Task CreateAsync(CouponTemplateSaveInput input) { await ValidateSaveAsync(input, null); var entity = MapToEntity(input, new CouponTemplate { Status = CouponTemplateStatus.Draft, IssuedQuantity = 0 }); await TemplateRepository.InsertAsync(entity); } /// /// 更新优惠券模板。 /// [HttpPost("/marketing/coupon-template/update")] public async Task UpdateAsync(CouponTemplateSaveInput input) { if (!input.Id.HasValue || input.Id.Value <= 0) { throw new BusinessException("Template id is invalid"); } var entity = await GetTemplateOrThrowAsync(input.Id.Value); if (entity.Status == CouponTemplateStatus.Ended) { throw new BusinessException("Ended template cannot be edited"); } await ValidateSaveAsync(input, entity.Id); MapToEntity(input, entity); await TemplateRepository.UpdateAsync(entity); } /// /// 发布优惠券模板(草稿 → 生效)。 /// [HttpPost("/marketing/coupon-template/publish")] public async Task PublishAsync(IdInput input) { var entity = await GetTemplateOrThrowAsync(input.Id); if (entity.Status != CouponTemplateStatus.Draft) { throw new BusinessException("Only draft templates can be published"); } entity.Status = CouponTemplateStatus.Active; await TemplateRepository.UpdateAsync(entity); } /// /// 结束优惠券模板(生效 → 已结束)。 /// [HttpPost("/marketing/coupon-template/end")] public async Task EndAsync(IdInput input) { var entity = await GetTemplateOrThrowAsync(input.Id); if (entity.Status != CouponTemplateStatus.Active) { throw new BusinessException("Only active templates can be ended"); } entity.Status = CouponTemplateStatus.Ended; await TemplateRepository.UpdateAsync(entity); } private static Expression> BuildListPredicate( CouponTemplateListInput input) { var keyword = input.Keyword?.Trim(); var hasKeyword = !string.IsNullOrWhiteSpace(keyword); var status = input.Status; var hasStatus = status.HasValue; return x => (!hasStatus || x.Status == status!.Value) && (!hasKeyword || x.TemplateCode.Contains(keyword!) || x.TemplateName.Contains(keyword!)); } private async Task ValidateSaveAsync(CouponTemplateSaveInput input, int? excludeId) { var templateCode = input.TemplateCode.Trim(); var templateName = input.TemplateName.Trim(); if (string.IsNullOrWhiteSpace(templateCode)) { throw new BusinessException("Template code is required"); } if (string.IsNullOrWhiteSpace(templateName)) { throw new BusinessException("Template name is required"); } if (string.IsNullOrWhiteSpace(input.CouponType)) { throw new BusinessException("Coupon type is required"); } if (input.PerCustomerLimit <= 0) { throw new BusinessException("Per customer limit must be positive"); } if (input.TotalQuantity < 0) { throw new BusinessException("Total quantity cannot be negative"); } if (await TemplateRepository.ExistsTemplateCodeAsync(templateCode, excludeId)) { throw new BusinessException("Template code already exists"); } ValidateCouponTypeFields(input); } private static void ValidateCouponTypeFields(CouponTemplateSaveInput input) { switch (input.CouponType) { case MarketingCouponTypes.FixedOff: case MarketingCouponTypes.DirectOff: case MarketingCouponTypes.ShippingOff: if (!input.FaceValue.HasValue || input.FaceValue.Value <= 0) { throw new BusinessException("Face value must be positive"); } break; case MarketingCouponTypes.PercentOff: if (!input.DiscountRate.HasValue || input.DiscountRate.Value <= 0 || input.DiscountRate.Value >= 1) { throw new BusinessException("Discount rate must be between 0 and 1"); } break; default: throw new BusinessException("Unsupported coupon type"); } } private static CouponTemplate MapToEntity(CouponTemplateSaveInput input, CouponTemplate entity) { entity.TemplateCode = input.TemplateCode.Trim(); entity.TemplateName = input.TemplateName.Trim(); entity.CouponType = input.CouponType.Trim(); entity.FaceValue = input.FaceValue; entity.DiscountRate = input.DiscountRate; entity.MaxDiscountAmount = input.MaxDiscountAmount; entity.MinOrderAmount = input.MinOrderAmount; entity.TotalQuantity = input.TotalQuantity; entity.PerCustomerLimit = input.PerCustomerLimit; entity.ValidType = input.ValidType; entity.ValidStartTime = input.ValidStartTime; entity.ValidEndTime = input.ValidEndTime; entity.ValidDaysAfterReceive = input.ValidDaysAfterReceive; entity.ApplyScopeJson = string.IsNullOrWhiteSpace(input.ApplyScopeJson) ? null : input.ApplyScopeJson.Trim(); entity.GrantChannel = string.IsNullOrWhiteSpace(input.GrantChannel) ? "manual" : input.GrantChannel.Trim(); entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim(); return entity; } private static CouponTemplateOutput MapOutput(CouponTemplate entity) => new() { Id = entity.Id, TemplateCode = entity.TemplateCode, TemplateName = entity.TemplateName, CouponType = entity.CouponType, CouponTypeName = MarketingEnumNames.OfCouponType(entity.CouponType), FaceValue = entity.FaceValue, DiscountRate = entity.DiscountRate, MaxDiscountAmount = entity.MaxDiscountAmount, MinOrderAmount = entity.MinOrderAmount, TotalQuantity = entity.TotalQuantity, IssuedQuantity = entity.IssuedQuantity, PerCustomerLimit = entity.PerCustomerLimit, ValidType = entity.ValidType, ValidStartTime = entity.ValidStartTime, ValidEndTime = entity.ValidEndTime, ValidDaysAfterReceive = entity.ValidDaysAfterReceive, ApplyScopeJson = entity.ApplyScopeJson, GrantChannel = entity.GrantChannel, Status = entity.Status, StatusName = MarketingEnumNames.Of(entity.Status), Remark = entity.Remark, CreateTime = entity.CreateTime, UpdateTime = entity.UpdateTime }; private async Task GetTemplateOrThrowAsync(int id) { return await TemplateRepository.FirstOrDefaultAsync(x => x.Id == id) ?? throw new BusinessException("Coupon template not found"); } }