using MediatR;
using System.Data;
using System.Data.Common;
using System.Text.Json;
using TakeoutSaaS.Application.App.Billings.Dto;
using TakeoutSaaS.Application.App.Billings.Queries;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Data;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
///
/// 查询账单详情处理器。
///
public sealed class GetBillingDetailQueryHandler(
IDapperExecutor dapperExecutor)
: IRequestHandler
{
///
/// 处理查询账单详情请求。
///
/// 查询命令。
/// 取消标记。
/// 账单详情 DTO。
public async Task Handle(GetBillingDetailQuery request, CancellationToken cancellationToken)
{
// 1. 查询账单 + 支付记录(同一连接,避免多次往返)
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
async (connection, token) =>
{
// 1.1 查询账单
await using var billCommand = CreateCommand(
connection,
BuildBillingSql(),
[
("billingId", request.BillingId)
]);
await using var billReader = await billCommand.ExecuteReaderAsync(token);
if (!await billReader.ReadAsync(token))
{
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 1.2 (空行后) 读取账单行数据到内存(释放 Reader,避免同连接并发执行命令)
var billingId = billReader.GetInt64(0);
var tenantId = billReader.GetInt64(1);
var tenantName = billReader.IsDBNull(2) ? string.Empty : billReader.GetString(2);
long? subscriptionId = billReader.IsDBNull(3) ? null : billReader.GetInt64(3);
var statementNo = billReader.GetString(4);
var billingType = (BillingType)billReader.GetInt32(5);
var status = (TenantBillingStatus)billReader.GetInt32(6);
var periodStart = billReader.GetDateTime(7);
var periodEnd = billReader.GetDateTime(8);
var amountDue = billReader.GetDecimal(9);
var discountAmount = billReader.GetDecimal(10);
var taxAmount = billReader.GetDecimal(11);
var amountPaid = billReader.GetDecimal(12);
var currency = billReader.IsDBNull(13) ? "CNY" : billReader.GetString(13);
var dueDate = billReader.GetDateTime(14);
DateTime? reminderSentAt = billReader.IsDBNull(15) ? null : billReader.GetDateTime(15);
DateTime? overdueNotifiedAt = billReader.IsDBNull(16) ? null : billReader.GetDateTime(16);
var notes = billReader.IsDBNull(17) ? null : billReader.GetString(17);
var lineItemsJson = billReader.IsDBNull(18) ? null : billReader.GetString(18);
var createdAt = billReader.GetDateTime(19);
long? createdBy = billReader.IsDBNull(20) ? null : billReader.GetInt64(20);
DateTime? updatedAt = billReader.IsDBNull(21) ? null : billReader.GetDateTime(21);
long? updatedBy = billReader.IsDBNull(22) ? null : billReader.GetInt64(22);
// 1.3 (空行后) 主动释放账单 Reader,确保后续查询不会触发 Npgsql 并发命令异常
await billReader.DisposeAsync();
// 1.4 (空行后) 反序列化账单明细
var lineItems = new List();
if (!string.IsNullOrWhiteSpace(lineItemsJson))
{
try
{
lineItems = JsonSerializer.Deserialize>(lineItemsJson) ?? [];
}
catch
{
lineItems = [];
}
}
// 1.5 (空行后) 查询支付记录
var payments = new List();
await using var paymentCommand = CreateCommand(
connection,
BuildPaymentsSql(),
[
("billingId", request.BillingId)
]);
await using var paymentReader = await paymentCommand.ExecuteReaderAsync(token);
while (await paymentReader.ReadAsync(token))
{
payments.Add(new PaymentRecordDto
{
Id = paymentReader.GetInt64(0),
TenantId = paymentReader.GetInt64(1),
BillingId = paymentReader.GetInt64(2),
Amount = paymentReader.GetDecimal(3),
Method = (TenantPaymentMethod)paymentReader.GetInt32(4),
Status = (TenantPaymentStatus)paymentReader.GetInt32(5),
TransactionNo = paymentReader.IsDBNull(6) ? null : paymentReader.GetString(6),
ProofUrl = paymentReader.IsDBNull(7) ? null : paymentReader.GetString(7),
Notes = paymentReader.IsDBNull(8) ? null : paymentReader.GetString(8),
VerifiedBy = paymentReader.IsDBNull(9) ? null : paymentReader.GetInt64(9),
VerifiedAt = paymentReader.IsDBNull(10) ? null : paymentReader.GetDateTime(10),
RefundReason = paymentReader.IsDBNull(11) ? null : paymentReader.GetString(11),
RefundedAt = paymentReader.IsDBNull(12) ? null : paymentReader.GetDateTime(12),
PaidAt = paymentReader.IsDBNull(13) ? null : paymentReader.GetDateTime(13),
IsVerified = !paymentReader.IsDBNull(10),
CreatedAt = paymentReader.GetDateTime(14)
});
}
// 1.6 (空行后) 组装详情 DTO
var totalAmount = amountDue - discountAmount + taxAmount;
return new BillingDetailDto
{
Id = billingId,
TenantId = tenantId,
TenantName = tenantName,
SubscriptionId = subscriptionId,
StatementNo = statementNo,
BillingType = billingType,
Status = status,
PeriodStart = periodStart,
PeriodEnd = periodEnd,
AmountDue = amountDue,
DiscountAmount = discountAmount,
TaxAmount = taxAmount,
TotalAmount = totalAmount,
AmountPaid = amountPaid,
Currency = currency,
DueDate = dueDate,
ReminderSentAt = reminderSentAt,
OverdueNotifiedAt = overdueNotifiedAt,
LineItemsJson = lineItemsJson,
LineItems = lineItems,
Payments = payments,
Notes = notes,
CreatedAt = createdAt,
CreatedBy = createdBy,
UpdatedAt = updatedAt,
UpdatedBy = updatedBy
};
},
cancellationToken);
}
private static string BuildBillingSql()
{
return """
select
b."Id",
b."TenantId",
t."Name" as "TenantName",
b."SubscriptionId",
b."StatementNo",
b."BillingType",
b."Status",
b."PeriodStart",
b."PeriodEnd",
b."AmountDue",
b."DiscountAmount",
b."TaxAmount",
b."AmountPaid",
b."Currency",
b."DueDate",
b."ReminderSentAt",
b."OverdueNotifiedAt",
b."Notes",
b."LineItemsJson",
b."CreatedAt",
b."CreatedBy",
b."UpdatedAt",
b."UpdatedBy"
from public.tenant_billing_statements b
join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null
where b."DeletedAt" is null
and b."Id" = @billingId
limit 1;
""";
}
private static string BuildPaymentsSql()
{
return """
select
p."Id",
p."TenantId",
p."BillingStatementId",
p."Amount",
p."Method",
p."Status",
p."TransactionNo",
p."ProofUrl",
p."Notes",
p."VerifiedBy",
p."VerifiedAt",
p."RefundReason",
p."RefundedAt",
p."PaidAt",
p."CreatedAt"
from public.tenant_payments p
where p."DeletedAt" is null
and p."BillingStatementId" = @billingId
order by p."CreatedAt" desc;
""";
}
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
{
var command = connection.CreateCommand();
command.CommandText = sql;
foreach (var (name, value) in parameters)
{
var p = command.CreateParameter();
p.ParameterName = name;
p.Value = value ?? DBNull.Value;
command.Parameters.Add(p);
}
return (DbCommand)command;
}
}