feat: 新增配额包/支付相关实体与迁移

App:新增 operation_logs/quota_packages/tenant_payments/tenant_quota_package_purchases 表

Identity:修正 Avatar 字段类型(varchar(256)->text),保持现有数据不变
This commit is contained in:
2025-12-17 17:27:45 +08:00
parent 9c28790f5e
commit ab59e2e3e2
103 changed files with 14450 additions and 4 deletions

View File

@@ -0,0 +1,54 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Commands;
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 创建配额包命令处理器。
/// </summary>
public sealed class CreateQuotaPackageCommandHandler(
IQuotaPackageRepository quotaPackageRepository,
IIdGenerator idGenerator)
: IRequestHandler<CreateQuotaPackageCommand, QuotaPackageDto>
{
/// <inheritdoc />
public async Task<QuotaPackageDto> Handle(CreateQuotaPackageCommand request, CancellationToken cancellationToken)
{
// 1. 创建配额包实体
var quotaPackage = new QuotaPackage
{
Id = idGenerator.NextId(),
Name = request.Name,
QuotaType = request.QuotaType,
QuotaValue = request.QuotaValue,
Price = request.Price,
IsActive = request.IsActive,
SortOrder = request.SortOrder,
Description = request.Description,
CreatedAt = DateTime.UtcNow
};
// 2. 保存到数据库
await quotaPackageRepository.AddAsync(quotaPackage, cancellationToken);
await quotaPackageRepository.SaveChangesAsync(cancellationToken);
// 3. 返回 DTO
return new QuotaPackageDto
{
Id = quotaPackage.Id,
Name = quotaPackage.Name,
QuotaType = quotaPackage.QuotaType,
QuotaValue = quotaPackage.QuotaValue,
Price = quotaPackage.Price,
IsActive = quotaPackage.IsActive,
SortOrder = quotaPackage.SortOrder,
Description = quotaPackage.Description,
CreatedAt = quotaPackage.CreatedAt,
UpdatedAt = quotaPackage.UpdatedAt
};
}
}

View File

@@ -0,0 +1,29 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 删除配额包命令处理器。
/// </summary>
public sealed class DeleteQuotaPackageCommandHandler(IQuotaPackageRepository quotaPackageRepository)
: IRequestHandler<DeleteQuotaPackageCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(DeleteQuotaPackageCommand request, CancellationToken cancellationToken)
{
// 1. 软删除配额包
var deleted = await quotaPackageRepository.SoftDeleteAsync(request.QuotaPackageId, cancellationToken);
if (!deleted)
{
return false;
}
// 2. 保存变更
await quotaPackageRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,41 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
using TakeoutSaaS.Application.App.QuotaPackages.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 获取配额包列表查询处理器。
/// </summary>
public sealed class GetQuotaPackageListQueryHandler(IQuotaPackageRepository quotaPackageRepository)
: IRequestHandler<GetQuotaPackageListQuery, PagedResult<QuotaPackageListDto>>
{
/// <inheritdoc />
public async Task<PagedResult<QuotaPackageListDto>> Handle(GetQuotaPackageListQuery request, CancellationToken cancellationToken)
{
// 1. 分页查询
var (items, total) = await quotaPackageRepository.SearchPagedAsync(
request.QuotaType,
request.IsActive,
request.Page,
request.PageSize,
cancellationToken);
// 2. 映射为 DTO
var dtos = items.Select(x => new QuotaPackageListDto
{
Id = x.Id,
Name = x.Name,
QuotaType = x.QuotaType,
QuotaValue = x.QuotaValue,
Price = x.Price,
IsActive = x.IsActive,
SortOrder = x.SortOrder
}).ToList();
// 3. 返回分页结果
return new PagedResult<QuotaPackageListDto>(dtos, request.Page, request.PageSize, total);
}
}

View File

@@ -0,0 +1,43 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
using TakeoutSaaS.Application.App.QuotaPackages.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 获取租户配额购买记录查询处理器。
/// </summary>
public sealed class GetTenantQuotaPurchasesQueryHandler(IQuotaPackageRepository quotaPackageRepository)
: IRequestHandler<GetTenantQuotaPurchasesQuery, PagedResult<TenantQuotaPurchaseDto>>
{
/// <inheritdoc />
public async Task<PagedResult<TenantQuotaPurchaseDto>> Handle(GetTenantQuotaPurchasesQuery request, CancellationToken cancellationToken)
{
// 1. 分页查询购买记录
var (items, total) = await quotaPackageRepository.GetPurchasesPagedAsync(
request.TenantId,
request.Page,
request.PageSize,
cancellationToken);
// 2. 映射为 DTO
var dtos = items.Select(x => new TenantQuotaPurchaseDto
{
Id = x.Purchase.Id,
TenantId = x.Purchase.TenantId,
QuotaPackageId = x.Purchase.QuotaPackageId,
QuotaPackageName = x.Package.Name,
QuotaType = x.Package.QuotaType,
QuotaValue = x.Purchase.QuotaValue,
Price = x.Purchase.Price,
PurchasedAt = x.Purchase.PurchasedAt,
ExpiredAt = x.Purchase.ExpiredAt,
Notes = x.Purchase.Notes
}).ToList();
// 3. 返回分页结果
return new PagedResult<TenantQuotaPurchaseDto>(dtos, request.Page, request.PageSize, total);
}
}

View File

@@ -0,0 +1,35 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
using TakeoutSaaS.Application.App.QuotaPackages.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 获取租户配额使用情况查询处理器。
/// </summary>
public sealed class GetTenantQuotaUsageQueryHandler(IQuotaPackageRepository quotaPackageRepository)
: IRequestHandler<GetTenantQuotaUsageQuery, IReadOnlyList<TenantQuotaUsageDto>>
{
/// <inheritdoc />
public async Task<IReadOnlyList<TenantQuotaUsageDto>> Handle(GetTenantQuotaUsageQuery request, CancellationToken cancellationToken)
{
// 1. 查询配额使用情况
var items = await quotaPackageRepository.GetUsageByTenantAsync(
request.TenantId,
request.QuotaType,
cancellationToken);
// 2. 映射为 DTO
return items.Select(x => new TenantQuotaUsageDto
{
TenantId = x.TenantId,
QuotaType = x.QuotaType,
LimitValue = x.LimitValue,
UsedValue = x.UsedValue,
RemainingValue = x.LimitValue - x.UsedValue,
ResetCycle = x.ResetCycle,
LastResetAt = x.LastResetAt
}).ToList();
}
}

View File

@@ -0,0 +1,72 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Commands;
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 购买配额包命令处理器。
/// </summary>
public sealed class PurchaseQuotaPackageCommandHandler(
IQuotaPackageRepository quotaPackageRepository,
IIdGenerator idGenerator)
: IRequestHandler<PurchaseQuotaPackageCommand, TenantQuotaPurchaseDto>
{
/// <inheritdoc />
public async Task<TenantQuotaPurchaseDto> Handle(PurchaseQuotaPackageCommand request, CancellationToken cancellationToken)
{
// 1. 查找配额包
var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken);
if (quotaPackage == null)
{
throw new InvalidOperationException("配额包不存在");
}
// 2. 创建购买记录
var purchase = new TenantQuotaPackagePurchase
{
Id = idGenerator.NextId(),
TenantId = request.TenantId,
QuotaPackageId = request.QuotaPackageId,
QuotaValue = quotaPackage.QuotaValue,
Price = quotaPackage.Price,
PurchasedAt = DateTime.UtcNow,
ExpiredAt = request.ExpiredAt,
Notes = request.Notes,
CreatedAt = DateTime.UtcNow
};
// 3. 保存购买记录
await quotaPackageRepository.AddPurchaseAsync(purchase, cancellationToken);
// 4. 更新租户配额(根据配额类型更新对应配额)
var quotaUsage = await quotaPackageRepository.FindUsageAsync(request.TenantId, quotaPackage.QuotaType, cancellationToken);
if (quotaUsage != null)
{
quotaUsage.LimitValue += quotaPackage.QuotaValue;
await quotaPackageRepository.UpdateUsageAsync(quotaUsage, cancellationToken);
}
await quotaPackageRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
return new TenantQuotaPurchaseDto
{
Id = purchase.Id,
TenantId = purchase.TenantId,
QuotaPackageId = purchase.QuotaPackageId,
QuotaPackageName = quotaPackage.Name,
QuotaType = quotaPackage.QuotaType,
QuotaValue = purchase.QuotaValue,
Price = purchase.Price,
PurchasedAt = purchase.PurchasedAt,
ExpiredAt = purchase.ExpiredAt,
Notes = purchase.Notes
};
}
}

View File

@@ -0,0 +1,54 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Commands;
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 更新配额包命令处理器。
/// </summary>
public sealed class UpdateQuotaPackageCommandHandler(IQuotaPackageRepository quotaPackageRepository)
: IRequestHandler<UpdateQuotaPackageCommand, QuotaPackageDto?>
{
/// <inheritdoc />
public async Task<QuotaPackageDto?> Handle(UpdateQuotaPackageCommand request, CancellationToken cancellationToken)
{
// 1. 查找配额包
var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken);
if (quotaPackage == null)
{
return null;
}
// 2. 更新配额包
quotaPackage.Name = request.Name;
quotaPackage.QuotaType = request.QuotaType;
quotaPackage.QuotaValue = request.QuotaValue;
quotaPackage.Price = request.Price;
quotaPackage.IsActive = request.IsActive;
quotaPackage.SortOrder = request.SortOrder;
quotaPackage.Description = request.Description;
quotaPackage.UpdatedAt = DateTime.UtcNow;
// 3. 保存到数据库
await quotaPackageRepository.UpdateAsync(quotaPackage, cancellationToken);
await quotaPackageRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return new QuotaPackageDto
{
Id = quotaPackage.Id,
Name = quotaPackage.Name,
QuotaType = quotaPackage.QuotaType,
QuotaValue = quotaPackage.QuotaValue,
Price = quotaPackage.Price,
IsActive = quotaPackage.IsActive,
SortOrder = quotaPackage.SortOrder,
Description = quotaPackage.Description,
CreatedAt = quotaPackage.CreatedAt,
UpdatedAt = quotaPackage.UpdatedAt
};
}
}

View File

@@ -0,0 +1,34 @@
using MediatR;
using TakeoutSaaS.Application.App.QuotaPackages.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.QuotaPackages.Handlers;
/// <summary>
/// 更新配额包状态命令处理器。
/// </summary>
public sealed class UpdateQuotaPackageStatusCommandHandler(IQuotaPackageRepository quotaPackageRepository)
: IRequestHandler<UpdateQuotaPackageStatusCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(UpdateQuotaPackageStatusCommand request, CancellationToken cancellationToken)
{
// 1. 查找配额包
var quotaPackage = await quotaPackageRepository.FindByIdAsync(request.QuotaPackageId, cancellationToken);
if (quotaPackage == null)
{
return false;
}
// 2. 更新状态
quotaPackage.IsActive = request.IsActive;
quotaPackage.UpdatedAt = DateTime.UtcNow;
// 3. 保存到数据库
await quotaPackageRepository.UpdateAsync(quotaPackage, cancellationToken);
await quotaPackageRepository.SaveChangesAsync(cancellationToken);
return true;
}
}