148 lines
6.1 KiB
C#
148 lines
6.1 KiB
C#
using Store.Users.Services;
|
||
using MAX.Framework.Validation;
|
||
using Store.Users.Dtos;
|
||
using YourNamespace;
|
||
using MAX.Framework.Mediation;
|
||
using MAX.Core.Mediation;
|
||
using Store.Users.Behaviors;
|
||
using MAX.Core.Helpers;
|
||
using MAX.Framework.Commons;
|
||
using MediatR;
|
||
using Store.Users.Contracts.Dtos;
|
||
using Store.Users.Entities;
|
||
|
||
namespace Store.Users.Commands;
|
||
|
||
/// <summary>商铺续费,续费时长单位为月。</summary>
|
||
[MediatRBehavior(typeof(ValidationBehavior<,>))]
|
||
[MediatRBehavior(typeof(TransactionBehavior<,>))]
|
||
[MediatRBehavior(typeof(AccountConsumptionBehavior<,>))]
|
||
[MediatRBehavior(typeof(ShopOwnershipBehavior<,>))]
|
||
public sealed record ShopRenewCommand(ShopRenewRequest Input)
|
||
: IRequest<ShopRenewResponse>, IValidatableRequest, IAccountConsumptionRequest, IShopOwnershipRequest
|
||
{
|
||
// 请求只包含业务输入;实际操作者与目标账户由消费管道从数据库确定。
|
||
public string? UserName => Input.UserName;
|
||
public string ShopId => Input.ShopId;
|
||
public int RenewDuration => Input.RenewDuration;
|
||
public string? Remark => Input.Remark;
|
||
private AccountConsumptionContext? consumptionContext;
|
||
AccountConsumptionContext? IAccountConsumptionRequest.ConsumptionContext
|
||
{
|
||
get => consumptionContext;
|
||
set => consumptionContext = value;
|
||
}
|
||
// 消费管道先于店铺管道执行,店铺校验使用已授权的账户名称。
|
||
string IShopOwnershipRequest.UserName => consumptionContext!.User.UserName;
|
||
ShopEntity? IShopOwnershipRequest.OwnedShop { get; set; }
|
||
|
||
void IValidatableRequest.Validate() => RequestValidator.Validate(Input);
|
||
|
||
ConsumptionQuote IAccountConsumptionRequest.CalculateConsumption(UserEntity user)
|
||
{
|
||
int price = Math.Max(30, user.ShopPointsPerMonth);
|
||
return new ConsumptionQuote((decimal)price * RenewDuration, price);
|
||
}
|
||
}
|
||
|
||
internal sealed class ShopRenewCommandHandler(UnitOfWorkExecutionContext executionContext)
|
||
: IRequestHandler<ShopRenewCommand, ShopRenewResponse>
|
||
{
|
||
public async Task<ShopRenewResponse> Handle(ShopRenewCommand request, CancellationToken cancellationToken)
|
||
{
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
var shopId = request.ShopId.Trim();
|
||
var remark = string.IsNullOrWhiteSpace(request.Remark) ? null : request.Remark.Trim();
|
||
|
||
var context = ((IAccountConsumptionRequest)request).ConsumptionContext
|
||
?? throw new InvalidOperationException("续费命令必须通过账户消费管道执行");
|
||
var orm = executionContext.Orm;
|
||
var user = context.User;
|
||
var userName = user.UserName;
|
||
var actor = context.Actor;
|
||
var shop = ((IShopOwnershipRequest)request).OwnedShop
|
||
?? throw new InvalidOperationException("续费命令必须通过商铺归属管道执行");
|
||
decimal pointsPerMonth = context.Quote.UnitPrice;
|
||
decimal cost = context.Quote.Amount;
|
||
decimal newPoints = user.Points - cost;
|
||
var now = DateTime.Now;
|
||
var currentExpire = await orm.Select<ShopExpireEntity>().Where(a => a.ShopId == shopId)
|
||
.FirstAsync(cancellationToken);
|
||
var expireBase = now;
|
||
if (currentExpire is { ExpireTime: > 0 })
|
||
{
|
||
var currentTime = DateHelper.ConvertFromUnixTimestampInSeconds(currentExpire.ExpireTime);
|
||
if (currentTime > expireBase)
|
||
expireBase = currentTime;
|
||
}
|
||
int newExpireTime = CalculateNewExpireTime(expireBase, request.RenewDuration);
|
||
|
||
|
||
int affected = await orm.Update<UserEntity>().Set(a => a.Points, newPoints)
|
||
.Where(a => a.Id == user.Id && a.Points == user.Points)
|
||
.ExecuteAffrowsAsync(cancellationToken);
|
||
if (affected != 1)
|
||
throw new AppException("账户已发生改变,请刷新账户后重新操作");
|
||
|
||
for (int i = 0; i < request.RenewDuration; i++)
|
||
{
|
||
var record = new UserPointsRecordEntity
|
||
{
|
||
OptionType = "续费",
|
||
OperatorType = UserAccessPolicy.IsAdmin(actor) ? "管理员" : "用户",
|
||
OperatorName = actor.UserName,
|
||
UserName = userName,
|
||
UserNick = user.NickName,
|
||
DeltaPoints = pointsPerMonth,
|
||
NewPoints = user.Points - (decimal)pointsPerMonth * (i + 1),
|
||
ShopId = shop.ShopId,
|
||
ShopAccount = shop.ShopAccount,
|
||
Remark = remark,
|
||
AddTime = now.AddSeconds(i)
|
||
};
|
||
if (await orm.Insert(record).ExecuteAffrowsAsync(cancellationToken) != 1)
|
||
throw new AppException("新增操作记录失败,请稍候重试");
|
||
}
|
||
|
||
if (currentExpire == null)
|
||
{
|
||
affected = await orm.Insert(new ShopExpireEntity
|
||
{
|
||
ShopId = shopId,
|
||
ExpireTime = newExpireTime,
|
||
AddTime = now,
|
||
MdyTime = now
|
||
}).ExecuteAffrowsAsync(cancellationToken);
|
||
}
|
||
else
|
||
{
|
||
affected = await orm.Update<ShopExpireEntity>()
|
||
.Set(a => a.ExpireTime, newExpireTime).Set(a => a.MdyTime, now)
|
||
.Where(a => a.Id == currentExpire.Id && a.ExpireTime == currentExpire.ExpireTime)
|
||
.ExecuteAffrowsAsync(cancellationToken);
|
||
}
|
||
if (affected != 1)
|
||
throw new AppException("更新商铺过期时间失败,请刷新店铺后重新操作");
|
||
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
return new ShopRenewResponse { ShopId = shopId, UserPoints = newPoints, ExpireTime = newExpireTime };
|
||
}
|
||
|
||
private static int CalculateNewExpireTime(DateTime expireBase, int duration)
|
||
{
|
||
DateTime newExpireDate;
|
||
try
|
||
{
|
||
newExpireDate = expireBase.AddMonths(duration);
|
||
}
|
||
catch (ArgumentOutOfRangeException)
|
||
{
|
||
throw new AppException("续费时长超出允许范围");
|
||
}
|
||
long timestamp = DateHelper.GetUnixTimestampInSeconds(newExpireDate);
|
||
if (timestamp > int.MaxValue || timestamp < 0)
|
||
throw new AppException("续费时间超出系统支持范围");
|
||
return (int)timestamp;
|
||
}
|
||
}
|