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,80 @@
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings;
|
||||
|
||||
/// <summary>
|
||||
/// 账单 DTO 映射助手。
|
||||
/// </summary>
|
||||
internal static class BillingMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 将账单实体映射为账单 DTO。
|
||||
/// </summary>
|
||||
/// <param name="bill">账单实体。</param>
|
||||
/// <param name="tenantName">租户名称。</param>
|
||||
/// <returns>账单 DTO。</returns>
|
||||
public static BillDto ToDto(this TenantBillingStatement bill, string? tenantName = null)
|
||||
=> new()
|
||||
{
|
||||
Id = bill.Id,
|
||||
TenantId = bill.TenantId,
|
||||
TenantName = tenantName,
|
||||
StatementNo = bill.StatementNo,
|
||||
PeriodStart = bill.PeriodStart,
|
||||
PeriodEnd = bill.PeriodEnd,
|
||||
AmountDue = bill.AmountDue,
|
||||
AmountPaid = bill.AmountPaid,
|
||||
Status = bill.Status,
|
||||
DueDate = bill.DueDate,
|
||||
CreatedAt = bill.CreatedAt
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 将账单实体与支付记录映射为账单详情 DTO。
|
||||
/// </summary>
|
||||
/// <param name="bill">账单实体。</param>
|
||||
/// <param name="payments">支付记录列表。</param>
|
||||
/// <param name="tenantName">租户名称。</param>
|
||||
/// <returns>账单详情 DTO。</returns>
|
||||
public static BillDetailDto ToDetailDto(
|
||||
this TenantBillingStatement bill,
|
||||
List<TenantPayment> payments,
|
||||
string? tenantName = null)
|
||||
=> new()
|
||||
{
|
||||
Id = bill.Id,
|
||||
TenantId = bill.TenantId,
|
||||
TenantName = tenantName,
|
||||
StatementNo = bill.StatementNo,
|
||||
PeriodStart = bill.PeriodStart,
|
||||
PeriodEnd = bill.PeriodEnd,
|
||||
AmountDue = bill.AmountDue,
|
||||
AmountPaid = bill.AmountPaid,
|
||||
Status = bill.Status,
|
||||
DueDate = bill.DueDate,
|
||||
LineItemsJson = bill.LineItemsJson,
|
||||
CreatedAt = bill.CreatedAt,
|
||||
Payments = payments.Select(p => p.ToDto()).ToList()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 将支付记录实体映射为支付 DTO。
|
||||
/// </summary>
|
||||
/// <param name="payment">支付记录实体。</param>
|
||||
/// <returns>支付 DTO。</returns>
|
||||
public static PaymentDto ToDto(this TenantPayment payment)
|
||||
=> new()
|
||||
{
|
||||
Id = payment.Id,
|
||||
BillingStatementId = payment.BillingStatementId,
|
||||
Amount = payment.Amount,
|
||||
Method = payment.Method,
|
||||
Status = payment.Status,
|
||||
TransactionNo = payment.TransactionNo,
|
||||
ProofUrl = payment.ProofUrl,
|
||||
PaidAt = payment.PaidAt,
|
||||
Notes = payment.Notes,
|
||||
CreatedAt = payment.CreatedAt
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建账单命令。
|
||||
/// </summary>
|
||||
public sealed record CreateBillCommand : IRequest<BillDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花算法)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 应付金额。
|
||||
/// </summary>
|
||||
public decimal AmountDue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期日(UTC)。
|
||||
/// </summary>
|
||||
public DateTime DueDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 记录支付命令。
|
||||
/// </summary>
|
||||
public sealed record RecordPaymentCommand : IRequest<PaymentDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法)。
|
||||
/// </summary>
|
||||
public long BillId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
/// </summary>
|
||||
public PaymentMethod Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易号。
|
||||
/// </summary>
|
||||
public string? TransactionNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付凭证 URL。
|
||||
/// </summary>
|
||||
public string? ProofUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新账单状态命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateBillStatusCommand : IRequest<BillDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法)。
|
||||
/// </summary>
|
||||
public long BillId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 新状态。
|
||||
/// </summary>
|
||||
public TenantBillingStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 账单详情 DTO。
|
||||
/// </summary>
|
||||
public sealed record BillDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花算法,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户名称。
|
||||
/// </summary>
|
||||
public string? TenantName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单编号。
|
||||
/// </summary>
|
||||
public string StatementNo { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计费周期开始时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime PeriodStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费周期结束时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime PeriodEnd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 应付金额。
|
||||
/// </summary>
|
||||
public decimal AmountDue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 已付金额。
|
||||
/// </summary>
|
||||
public decimal AmountPaid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单状态。
|
||||
/// </summary>
|
||||
public TenantBillingStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期日(UTC)。
|
||||
/// </summary>
|
||||
public DateTime DueDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单明细 JSON。
|
||||
/// </summary>
|
||||
public string? LineItemsJson { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付记录列表。
|
||||
/// </summary>
|
||||
public List<PaymentDto> Payments { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 账单 DTO。
|
||||
/// </summary>
|
||||
public sealed record BillDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花算法,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户名称。
|
||||
/// </summary>
|
||||
public string? TenantName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单编号。
|
||||
/// </summary>
|
||||
public string StatementNo { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计费周期开始时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime PeriodStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费周期结束时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime PeriodEnd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 应付金额。
|
||||
/// </summary>
|
||||
public decimal AmountDue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 已付金额。
|
||||
/// </summary>
|
||||
public decimal AmountPaid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单状态。
|
||||
/// </summary>
|
||||
public TenantBillingStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期日(UTC)。
|
||||
/// </summary>
|
||||
public DateTime DueDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 支付记录 DTO。
|
||||
/// </summary>
|
||||
public sealed record PaymentDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 支付记录 ID(雪花算法,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long BillingStatementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
/// </summary>
|
||||
public PaymentMethod Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付状态。
|
||||
/// </summary>
|
||||
public PaymentStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易号。
|
||||
/// </summary>
|
||||
public string? TransactionNo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付凭证 URL。
|
||||
/// </summary>
|
||||
public string? ProofUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付时间。
|
||||
/// </summary>
|
||||
public DateTime? PaidAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取账单详情查询。
|
||||
/// </summary>
|
||||
public sealed record GetBillDetailQuery : IRequest<BillDetailDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法)。
|
||||
/// </summary>
|
||||
public long BillId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取账单列表查询。
|
||||
/// </summary>
|
||||
public sealed record GetBillListQuery : IRequest<PagedResult<BillDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码(从 1 开始)。
|
||||
/// </summary>
|
||||
public int PageNumber { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 页大小。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID 筛选(可选)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态筛选(可选)。
|
||||
/// </summary>
|
||||
public TenantBillingStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期筛选(可选)。
|
||||
/// </summary>
|
||||
public DateTime? StartDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期筛选(可选)。
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 搜索关键词(账单号或租户名)。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Billings.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取租户支付记录查询。
|
||||
/// </summary>
|
||||
public sealed record GetTenantPaymentsQuery : IRequest<List<PaymentDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单 ID(雪花算法)。
|
||||
/// </summary>
|
||||
public long BillId { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user