All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
Co-authored-by: Cursor <cursoragent@cursor.com>
257 lines
9.2 KiB
C#
257 lines
9.2 KiB
C#
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");
|
||
}
|
||
}
|