admin/src/Oms.Application/Services/Marketing/MarketingCouponTemplateService.cs
zzh f972a28d79
All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
feat: 落地滚动会员、券发放与邀请分成奖励
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 17:40:24 +08:00

257 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 优惠券模板 API(管理端)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Marketing")]
public class MarketingCouponTemplateService : ApplicationService
{
private ICouponTemplateRepository TemplateRepository =>
LazyServiceProvider.GetRequiredService<ICouponTemplateRepository>();
/// <summary>
/// 分页查询优惠券模板。
/// </summary>
[HttpPost("/marketing/coupon-template/list")]
public async Task<OmsPageResult<CouponTemplateOutput>> 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<string, bool>
{
[nameof(CouponTemplate.UpdateTime)] = false,
[nameof(CouponTemplate.Id)] = false
});
var total = await TemplateRepository.CountAsync(predicate);
return new OmsPageResult<CouponTemplateOutput>
{
Items = entities.Select(MapOutput).ToList(),
Total = total
};
}
/// <summary>
/// 新增优惠券模板(草稿)。
/// </summary>
[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);
}
/// <summary>
/// 更新优惠券模板。
/// </summary>
[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);
}
/// <summary>
/// 发布优惠券模板(草稿 → 生效)。
/// </summary>
[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);
}
/// <summary>
/// 结束优惠券模板(生效 → 已结束)。
/// </summary>
[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<Func<CouponTemplate, bool>> 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");
}
if (input.ValidType == CouponValidType.DaysAfterUnlock &&
(!input.ValidDaysAfterReceive.HasValue || input.ValidDaysAfterReceive.Value <= 0))
{
throw new BusinessException("Unlock validity days is required");
}
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,
GrantChannelName = MarketingEnumNames.OfGrantChannel(entity.GrantChannel),
Status = entity.Status,
StatusName = MarketingEnumNames.Of(entity.Status),
Remark = entity.Remark,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime
};
private async Task<CouponTemplate> GetTemplateOrThrowAsync(int id)
{
return await TemplateRepository.FirstOrDefaultAsync(x => x.Id == id)
?? throw new BusinessException("Coupon template not found");
}
}