增加Mediat Pip特性,并用实例来做一个demo

This commit is contained in:
chfei committed 2026-10-22 17:32:58 +08:00
1 parent 292cb8638c
commit c9ac7412ab
19 files changed
+682 -108

No files matched your search

@@ -0,0 +1,44 @@
using MAX.Framework.Commons;
using MAX.Framework.Mediation;
using MediatR;
using Store.Users.Entities;
namespace Store.Users.Behaviors;
/// <summary>锁定积分账户并检查消费额度,不依赖商铺信息。</summary>
public sealed class AccountConsumptionBehavior<TRequest, TResponse>(
UnitOfWorkExecutionContext executionContext)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>, IAccountConsumptionRequest
{
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(request.UserName))
throw new AppException("用户名不能为空");
var userName = request.UserName.Trim();
var user = await executionContext.Orm.Select<UserEntity>().Where(a => a.UserName == userName)
.ForUpdate().FirstAsync(cancellationToken)
?? throw new AppException("不存在的用户");
var quote = request.CalculateConsumption(user);
if (quote.Amount <= 0 || quote.UnitPrice <= 0)
throw new AppException("消费金额必须大于0");
if (user.Points < quote.Amount)
throw new AppException("余额不足");
lock (request)
{
if (request.ConsumptionContext != null)
throw new InvalidOperationException("同一个消费命令不能同时执行");
request.ConsumptionContext = new AccountConsumptionContext(user, quote);
}
try
{
return await next();
}
finally
{
request.ConsumptionContext = null;
}
}
}
@@ -0,0 +1,23 @@
using Store.Users.Entities;
using YourNamespace;
namespace Store.Users.Behaviors;
/// <summary>需要账户积分消费检查的请求。</summary>
public interface IAccountConsumptionRequest
{
string UserName { get; }
AccountConsumptionContext? ConsumptionContext { get; set; }
ConsumptionQuote CalculateConsumption(UserEntity user);
}
/// <summary>需要检查商铺归属的请求,与是否消费无关。</summary>
public interface IShopOwnershipRequest
{
string UserName { get; }
string ShopId { get; }
ShopEntity? OwnedShop { get; set; }
}
public sealed record ConsumptionQuote(decimal Amount, decimal UnitPrice);
public sealed record AccountConsumptionContext(UserEntity User, ConsumptionQuote Quote);
@@ -0,0 +1,43 @@
using MAX.Framework.Commons;
using MAX.Framework.Mediation;
using MediatR;
using YourNamespace;
namespace Store.Users.Behaviors;
/// <summary>独立检查商铺归属;事务中执行时锁定商铺,读取场景可不启用事务。</summary>
public sealed class ShopOwnershipBehavior<TRequest, TResponse>(
IFreeSql freeSql, UnitOfWorkExecutionContext executionContext) : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>, IShopOwnershipRequest
{
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.ShopId))
throw new AppException("用户名和商铺ID不能为空");
var userName = request.UserName.Trim();
var shopId = request.ShopId.Trim();
var orm = executionContext.IsActive ? executionContext.Orm : freeSql;
var query = orm.Select<ShopEntity>().Where(a => a.ShopId == shopId && a.UserName == userName);
if (executionContext.IsActive) query = query.ForUpdate();
var shop = await query.FirstAsync(cancellationToken)
?? throw new AppException("不存在的商铺或该商铺不属于当前用户");
if (!string.Equals(shop.UserName, userName, StringComparison.OrdinalIgnoreCase))
throw new AppException("该商铺不属于当前用户");
lock (request)
{
if (request.OwnedShop != null)
throw new InvalidOperationException("同一个商铺操作命令不能同时执行");
request.OwnedShop = shop;
}
try
{
return await next();
}
finally
{
request.OwnedShop = null;
}
}
}
+138 -73
View File
@@ -1,81 +1,146 @@
//using MediatR;
//using Store.Framework.Models;
//using Store.Shops.Contracts;
//using Store.Users.Contracts.Dtos;
//using Store.Users.Entities;
//using Store.Users.Repositories;
//using System;
//using System.Collections.Generic;
//using System.Text;
using System.ComponentModel.DataAnnotations;
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>
// /// <param name="UserName"></param>
// /// <param name="ShopId"></param>
// /// <param name="RenewDuration"></param>
// internal record class ShopRenewCommand(string UserName, string ShopId, int RenewDuration) : IRequest<ShopRenewResponse>;
// internal class ShopRenewCommandHandler : IRequestHandler<ShopRenewCommand, ShopRenewResponse>
// {
// private readonly IUserRepository _userRepository;
// private readonly IShopQueryService _shopQueryService;
namespace Store.Users.Commands;
// public ShopRenewCommandHandler(IUserRepository userRepository, IShopQueryService shopQueryService)
// {
// _userRepository = userRepository;
// _shopQueryService = shopQueryService;
// }
// async Task<ShopRenewResponse> IRequestHandler<ShopRenewCommand, ShopRenewResponse>.Handle(ShopRenewCommand request, CancellationToken cancellationToken)
// {
// ShopRenewResponse response = new ShopRenewResponse();
// var user = await _userRepository.GetUser(request.UserName);
// int shopPointsPerMonth = Math.Max(30, user.ShopPointsPerMonth);
// int deltaPoints = shopPointsPerMonth * request.RenewDuration;
// int newPoints = (int)user.Points - deltaPoints;
// DateTime expireTime = _shopQueryService.GetExpireTime(request.ShopId);
/// <summary>商铺续费,续费时长单位为月。</summary>
[MediatRBehavior(typeof(ValidationBehavior<,>))]
[MediatRBehavior(typeof(TransactionBehavior<,>))]
[MediatRBehavior(typeof(AccountConsumptionBehavior<,>))]
[MediatRBehavior(typeof(ShopOwnershipBehavior<,>))]
public sealed record ShopRenewCommand(string UserName, string ShopId, int RenewDuration, string? Remark = null)
: IRequest<ShopRenewResponse>, IValidatableRequest, IAccountConsumptionRequest, IShopOwnershipRequest
{
// 由消费管道建立,仅在当前事务执行期间有效。
AccountConsumptionContext? IAccountConsumptionRequest.ConsumptionContext { get; set; }
ShopEntity? IShopOwnershipRequest.OwnedShop { get; set; }
void IValidatableRequest.Validate()
{
if (string.IsNullOrWhiteSpace(UserName) || UserName.Trim().Length > 20)
throw new ValidationException("用户名不能为空且长度不能超过20个字符");
if (string.IsNullOrWhiteSpace(ShopId) || ShopId.Trim().Length > 50)
throw new ValidationException("店铺ID不能为空且长度不能超过50个字符");
if (RenewDuration is < 1 or > 120)
throw new ValidationException("续费时长必须为1-120个月");
if (Remark?.Trim().Length > 255)
throw new ValidationException("备注长度不能超过255个字符");
}
ConsumptionQuote IAccountConsumptionRequest.CalculateConsumption(UserEntity user)
{
if (RenewDuration is < 1 or > 120)
throw new AppException("续费时长必须为1-120个月");
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)
{
ArgumentNullException.ThrowIfNull(request);
cancellationToken.ThrowIfCancellationRequested();
var userName = request.UserName.Trim();
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 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);
// if (currentExpire != null && currentExpire.ExpireTime > 0)
// {
// DateTime currentExpireTime = TimeUtils.StampToDateTime(currentExpire.ExpireTime);
// if (currentExpireTime > expireBaseTime)
// {
// expireBaseTime = currentExpireTime;
// }
// }
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("账户已发生改变,请刷新账户后重新操作");
// int newExpireTime = CalculateNewExpireTime(expireBaseTime, request.RenewDuration);
for (int i = 0; i < request.RenewDuration; i++)
{
var record = new UserPointsRecordEntity
{
OptionType = "续费",
OperatorType = "用户",
OperatorName = 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 };
}
// response.ShopId = request.ShopId;
// response.UserPoints = newPoints;
// response.ExpireTime‌ = newExpireTime;
// return response;
// }
// private int CalculateNewExpireTime(DateTime expireBaseTime, int renewDuration)
// {
// DateTime newExpireDateTime;
// try
// {
// newExpireDateTime = expireBaseTime.AddMonths(renewDuration);
// }
// catch (ArgumentOutOfRangeException)
// {
// throw new InvalidOperationException("续费时长超出允许范围");
// }
// if (newExpireDateTime > TimeUtils.StampToDateTime(int.MaxValue))
// {
// throw new InvalidOperationException("续费时间超出系统支持范围");
// }
// return TimeUtils.DateTimeToTimestampSeconds(newExpireDateTime);
// }
// }
//}
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;
}
}
@@ -1,29 +1,45 @@
using Asp.Versioning;
using Asp.Versioning;
using MAX.Framework.Security;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Store.Users.Commands;
using Store.Users.Contracts;
using Store.Users.Contracts.Dtos;
using Store.Users.Services;
using System;
using System.Collections.Generic;
using System.Text;
using Store.Users.Dtos;
namespace Store.Users.Controllers
namespace Store.Users.Controllers;
[ApiController]
[ApiVersionNeutral]
[Route("user/[action]")]
public class UserController : ControllerBase
{
[ApiController]
[ApiVersionNeutral]
[Route("user/[action]")]
public class UserController : ControllerBase
{
private readonly IUserQueryService _userQuery;
private readonly IUserQueryService _userQuery;
private readonly IMediator _mediator;
private readonly ICurrentUser _currentUser;
public UserController(IUserQueryService userQuery)
{
_userQuery = userQuery;
}
[HttpGet]
public async Task<UserInfo> GetUser(string userName)
{
return await _userQuery.GetUser(userName);
}
}
public UserController(IUserQueryService userQuery, IMediator mediator, ICurrentUser currentUser)
{
_userQuery = userQuery;
_mediator = mediator;
_currentUser = currentUser;
}
[HttpGet]
public async Task<UserInfo> GetUser(string userName)
{
return await _userQuery.GetUser(userName);
}
/// <summary>使用当前登录用户的积分为商铺续费。</summary>
[HttpPost]
public Task<ShopRenewResponse> ShopRenew([FromBody] ShopRenewRequest request, CancellationToken cancellationToken)
{
if (!_currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(_currentUser.UserName))
throw new UnauthorizedAccessException();
return _mediator.Send(new ShopRenewCommand(
_currentUser.UserName, request.ShopId, request.RenewDuration, request.Remark), cancellationToken);
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
@@ -8,6 +8,6 @@ namespace Store.Users.Contracts.Dtos
{
public string ShopId { get; set; }
public int ExpireTime‌ { get; set; }
public int UserPoints { get; set; }
public decimal UserPoints { get; set; }
}
}
+5 -1
View File
@@ -1,4 +1,7 @@
using MAX.Core.DependencyInjection;
using MediatR;
using Store.Users.Commands;
using Store.Users.Contracts.Dtos;
using MAX.Core.DependencyInjection;
using MAX.Core.Interfaces;
namespace Store.Users
@@ -13,6 +16,7 @@ namespace Store.Users
public void Register(IServiceRegistrar registrar)
{
registrar.RegisterTransient<IRequestHandler<ShopRenewCommand, ShopRenewResponse>, ShopRenewCommandHandler>();
}
public void Start(IServiceProvider serviceProvider)
+2 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
@@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Store.Framework\Store.Framework.csproj" />
<ProjectReference Include="..\Store.Shops.Contracts\Store.Shops.Contracts.csproj" />
<ProjectReference Include="..\Store.Shops\Store.Shops.csproj" />
<ProjectReference Include="..\Store.Users.Contracts\Store.Users.Contracts.csproj" />
</ItemGroup>