admin/src/Oms.Application/Services/Orders/OrderService.PayCoupon.cs
zzh 223d32f62a
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
feat: 订单支付时选券,新增支付预览接口并按所选券扣款核销
- 新增 GET /order/pay-preview:返回会员折/券互斥后的应付拆解与本单可选券
- POST /order/pay 支持 couponInstanceId(null 沿用已绑定券,0 不用券,>0 指定券),按试算应付扣款并核销券、释放被替换券的锁定
- 被当前订单锁定的券视为可用,修复入库/编辑重算营销金额时误报券被占用

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 14:01:00 +08:00

143 lines
5.3 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 Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Marketing;
using Oms.Application.Dtos.Orders;
using Oms.Application.Shared;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Services.Orders;
public partial class OrderService
{
private const string CouponNotBetterThanLevelReason = "优惠不高于会员折扣";
/// <summary>
/// 支付预览:按当前应收计算会员折/券互斥后的应付,并返回本单可选券。
/// </summary>
/// <param name="input">入参:订单 Id;couponInstanceId 为 null 自动推荐,0 不用券,大于 0 试算指定券。</param>
/// <returns>应付拆解、实际生效券与可选券列表;指定券不可用(未解锁/过期/线路不符等)时抛业务异常。</returns>
[HttpGet("/order/pay-preview")]
public async Task<OrderPayPreviewOutput> GetPayPreviewAsync([FromQuery] OrderPayPreviewInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
var fees = await OrderRepository.GetFeesByOrderIdsAsync([order.Id]);
var receivableSubtotal = MarketingFeeHelper.SumReceivable(fees);
var baseFreightAmount = MarketingFeeHelper.SumReceivableBaseFreight(fees);
var marketingHelper = LazyServiceProvider.GetRequiredService<MarketingOrderHelper>();
var memberOnly = await marketingHelper.BuildBreakdownAsync(
order.CustomerId,
receivableSubtotal,
baseFreightAmount,
null,
order.ConsigneeCountry,
order.Id);
var coupons = await marketingHelper.BuildAvailableCouponsAsync(
order.CustomerId,
baseFreightAmount,
order.ConsigneeCountry,
order.Id);
foreach (var coupon in coupons.Where(x => x.CanUse
&& (x.EstimatedDiscount ?? 0M) <= memberOnly.LevelDiscountAmount))
{
coupon.CanUse = false;
coupon.UnavailableReason = CouponNotBetterThanLevelReason;
}
coupons = coupons
.OrderByDescending(x => x.CanUse)
.ThenByDescending(x => x.EstimatedDiscount ?? 0M)
.ThenBy(x => x.ValidEndTime)
.ToList();
var targetCouponId = input.CouponInstanceId switch
{
0 => null,
> 0 => input.CouponInstanceId,
_ => ResolveRecommendedPayCouponId(order, coupons)
};
var breakdown = targetCouponId.HasValue
? await marketingHelper.BuildBreakdownAsync(
order.CustomerId,
receivableSubtotal,
baseFreightAmount,
targetCouponId,
order.ConsigneeCountry,
order.Id)
: memberOnly;
return new OrderPayPreviewOutput
{
Id = order.Id,
ReceivableSubtotal = breakdown.ReceivableSubtotal,
BaseFreightAmount = breakdown.BaseFreightAmount,
LevelDiscountAmount = breakdown.LevelDiscountAmount,
CouponDiscountAmount = breakdown.CouponDiscountAmount,
PayableAmount = breakdown.PayableAmount,
MemberLevelName = breakdown.MemberLevelName,
AppliedCouponInstanceId = breakdown.CouponInstanceId,
AvailableCoupons = coupons
};
}
/// <summary>
/// 计算支付应付:couponInstanceId 为 null 沿用订单已绑定券(不可用则不用券),0 不用券,大于 0 使用指定券。
/// </summary>
private async Task<MarketingPriceBreakdown> BuildPayBreakdownAsync(
Order order,
IEnumerable<OrderFee> fees,
int? couponInstanceId)
{
var feeList = fees as IList<OrderFee> ?? fees.ToList();
var receivableSubtotal = MarketingFeeHelper.SumReceivable(feeList);
var baseFreightAmount = MarketingFeeHelper.SumReceivableBaseFreight(feeList);
var marketingHelper = LazyServiceProvider.GetRequiredService<MarketingOrderHelper>();
Task<MarketingPriceBreakdown> Build(int? couponId) =>
marketingHelper.BuildBreakdownAsync(
order.CustomerId,
receivableSubtotal,
baseFreightAmount,
couponId,
order.ConsigneeCountry,
order.Id);
if (couponInstanceId is > 0)
{
var breakdown = await Build(couponInstanceId);
if (breakdown.CouponInstanceId != couponInstanceId)
{
throw new BusinessException("所选优惠券未达使用门槛或优惠不高于会员折扣,请重新选择");
}
return breakdown;
}
if (couponInstanceId == 0 || order.CouponInstanceId is not > 0)
{
return await Build(null);
}
try
{
return await Build(order.CouponInstanceId);
}
catch (BusinessException)
{
return await Build(null);
}
}
private static int? ResolveRecommendedPayCouponId(Order order, List<CouponAvailableOutput> coupons)
{
if (order.CouponInstanceId is > 0
&& coupons.Any(x => x.CanUse && x.CouponInstanceId == order.CouponInstanceId.Value))
{
return order.CouponInstanceId;
}
return coupons.FirstOrDefault(x => x.CanUse)?.CouponInstanceId;
}
}