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,61 @@
using MediatR;
using TakeoutSaaS.Application.App.Billings.Commands;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 创建账单处理器。
/// </summary>
public sealed class CreateBillCommandHandler(
ITenantBillingRepository billingRepository,
ITenantRepository tenantRepository,
IIdGenerator idGenerator)
: IRequestHandler<CreateBillCommand, BillDto>
{
/// <summary>
/// 处理创建账单请求。
/// </summary>
/// <param name="request">创建命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单 DTO。</returns>
public async Task<BillDto> Handle(CreateBillCommand request, CancellationToken cancellationToken)
{
// 1. 验证租户存在
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken);
if (tenant is null)
{
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
}
// 2. 生成账单编号
var statementNo = $"BILL-{DateTime.UtcNow:yyyyMMdd}-{idGenerator.NextId()}";
// 3. 构建账单实体
var bill = new TenantBillingStatement
{
TenantId = request.TenantId,
StatementNo = statementNo,
PeriodStart = DateTime.UtcNow,
PeriodEnd = DateTime.UtcNow,
AmountDue = request.AmountDue,
AmountPaid = 0,
Status = TenantBillingStatus.Pending,
DueDate = request.DueDate,
LineItemsJson = request.Notes
};
// 4. 持久化账单
await billingRepository.AddAsync(bill, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
return bill.ToDto(tenant.Name);
}
}

View File

@@ -0,0 +1,41 @@
using MediatR;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Application.App.Billings.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 获取账单详情查询处理器。
/// </summary>
public sealed class GetBillDetailQueryHandler(
ITenantBillingRepository billingRepository,
ITenantPaymentRepository paymentRepository,
ITenantRepository tenantRepository)
: IRequestHandler<GetBillDetailQuery, BillDetailDto?>
{
/// <summary>
/// 处理获取账单详情请求。
/// </summary>
/// <param name="request">查询请求。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单详情或 null。</returns>
public async Task<BillDetailDto?> Handle(GetBillDetailQuery request, CancellationToken cancellationToken)
{
// 1. 查询账单
var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken);
if (bill is null)
{
return null;
}
// 2. 查询支付记录
var payments = await paymentRepository.GetByBillingIdAsync(request.BillId, cancellationToken);
// 3. 查询租户名称
var tenant = await tenantRepository.FindByIdAsync(bill.TenantId, cancellationToken);
// 4. 返回详情 DTO
return bill.ToDetailDto(payments.ToList(), tenant?.Name);
}
}

View File

@@ -0,0 +1,53 @@
using MediatR;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Application.App.Billings.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 获取账单列表查询处理器。
/// </summary>
public sealed class GetBillListQueryHandler(
ITenantBillingRepository billingRepository,
ITenantRepository tenantRepository)
: IRequestHandler<GetBillListQuery, PagedResult<BillDto>>
{
/// <summary>
/// 处理获取账单列表请求。
/// </summary>
/// <param name="request">查询请求。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分页账单列表。</returns>
public async Task<PagedResult<BillDto>> Handle(GetBillListQuery request, CancellationToken cancellationToken)
{
// 1. 分页查询账单
var (bills, total) = await billingRepository.SearchPagedAsync(
request.TenantId,
request.Status,
request.StartDate,
request.EndDate,
request.Keyword,
request.PageNumber,
request.PageSize,
cancellationToken);
// 2. 无数据直接返回
if (bills.Count == 0)
{
return new PagedResult<BillDto>([], request.PageNumber, request.PageSize, total);
}
// 3. 批量查询租户信息
var tenantIds = bills.Select(b => b.TenantId).Distinct().ToArray();
var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken);
var tenantDict = tenants.ToDictionary(t => t.Id, t => t.Name);
// 4. 映射 DTO
var result = bills.Select(b => b.ToDto(tenantDict.GetValueOrDefault(b.TenantId))).ToList();
// 5. 返回分页结果
return new PagedResult<BillDto>(result, request.PageNumber, request.PageSize, total);
}
}

View File

@@ -0,0 +1,28 @@
using MediatR;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Application.App.Billings.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 获取租户支付记录查询处理器。
/// </summary>
public sealed class GetTenantPaymentsQueryHandler(ITenantPaymentRepository paymentRepository)
: IRequestHandler<GetTenantPaymentsQuery, List<PaymentDto>>
{
/// <summary>
/// 处理获取支付记录请求。
/// </summary>
/// <param name="request">查询请求。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>支付记录列表。</returns>
public async Task<List<PaymentDto>> Handle(GetTenantPaymentsQuery request, CancellationToken cancellationToken)
{
// 1. 查询支付记录
var payments = await paymentRepository.GetByBillingIdAsync(request.BillId, cancellationToken);
// 2. 映射并返回 DTO
return payments.Select(p => p.ToDto()).ToList();
}
}

View File

@@ -0,0 +1,66 @@
using MediatR;
using TakeoutSaaS.Application.App.Billings.Commands;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 记录支付处理器。
/// </summary>
public sealed class RecordPaymentCommandHandler(
ITenantBillingRepository billingRepository,
ITenantPaymentRepository paymentRepository)
: IRequestHandler<RecordPaymentCommand, PaymentDto>
{
/// <summary>
/// 处理记录支付请求。
/// </summary>
/// <param name="request">记录支付命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>支付 DTO。</returns>
public async Task<PaymentDto> Handle(RecordPaymentCommand request, CancellationToken cancellationToken)
{
// 1. 查询账单
var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken);
if (bill is null)
{
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 2. 构建支付记录
var payment = new TenantPayment
{
TenantId = bill.TenantId,
BillingStatementId = request.BillId,
Amount = request.Amount,
Method = request.Method,
Status = PaymentStatus.Success,
TransactionNo = request.TransactionNo,
ProofUrl = request.ProofUrl,
PaidAt = DateTime.UtcNow,
Notes = request.Notes
};
// 3. 更新账单已付金额
bill.AmountPaid += request.Amount;
// 4. 如果已付金额 >= 应付金额,标记为已支付
if (bill.AmountPaid >= bill.AmountDue)
{
bill.Status = TenantBillingStatus.Paid;
}
// 5. 持久化变更
await paymentRepository.AddAsync(payment, cancellationToken);
await billingRepository.UpdateAsync(bill, cancellationToken);
await paymentRepository.SaveChangesAsync(cancellationToken);
// 6. 返回 DTO
return payment.ToDto();
}
}

View File

@@ -0,0 +1,50 @@
using MediatR;
using TakeoutSaaS.Application.App.Billings.Commands;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 更新账单状态处理器。
/// </summary>
public sealed class UpdateBillStatusCommandHandler(
ITenantBillingRepository billingRepository,
ITenantRepository tenantRepository)
: IRequestHandler<UpdateBillStatusCommand, BillDto?>
{
/// <summary>
/// 处理更新账单状态请求。
/// </summary>
/// <param name="request">更新命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单 DTO 或 null。</returns>
public async Task<BillDto?> Handle(UpdateBillStatusCommand request, CancellationToken cancellationToken)
{
// 1. 查询账单
var bill = await billingRepository.FindByIdAsync(request.BillId, cancellationToken);
if (bill is null)
{
return null;
}
// 2. 更新状态
bill.Status = request.Status;
if (!string.IsNullOrWhiteSpace(request.Notes))
{
bill.LineItemsJson = request.Notes;
}
// 3. 持久化变更
await billingRepository.UpdateAsync(bill, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);
// 4. 查询租户名称
var tenant = await tenantRepository.FindByIdAsync(bill.TenantId, cancellationToken);
// 5. 返回 DTO
return bill.ToDto(tenant?.Name);
}
}