feat: 新增配额包/支付相关实体与迁移
App:新增 operation_logs/quota_packages/tenant_payments/tenant_quota_package_purchases 表 Identity:修正 Avatar 字段类型(varchar(256)->text),保持现有数据不变
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user