fix(billing): 修复账单详情查询的数据库并发错误

问题:
- Npgsql.NpgsqlOperationInProgressException: A command is already in progress
- 在同一个数据库连接上,billReader 未释放就执行 paymentReader

根因:
- GetBillingDetailQueryHandler 中先查询账单并打开 billReader
- 读取账单数据后未释放 reader
- 直接在同一连接上执行支付记录查询,触发并发异常

解决方案:
- 将账单字段先读取到本地变量
- 主动 DisposeAsync 释放 billReader
- 再执行支付记录查询
- 最后用本地变量组装 BillingDetailDto
This commit is contained in:
2025-12-18 11:45:44 +08:00
parent 4b53862ded
commit a5abd6ef90

View File

@@ -46,14 +46,35 @@ public sealed class GetBillingDetailQueryHandler(
throw new BusinessException(ErrorCodes.NotFound, "账单不存在"); 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? reminderSentAt = billReader.IsDBNull(15) ? null : billReader.GetDateTime(15);
DateTime? overdueNotifiedAt = billReader.IsDBNull(16) ? null : billReader.GetDateTime(16); DateTime? overdueNotifiedAt = billReader.IsDBNull(16) ? null : billReader.GetDateTime(16);
var notes = billReader.IsDBNull(17) ? null : billReader.GetString(17); var notes = billReader.IsDBNull(17) ? null : billReader.GetString(17);
var lineItemsJson = billReader.IsDBNull(18) ? null : billReader.GetString(18); var lineItemsJson = billReader.IsDBNull(18) ? null : billReader.GetString(18);
var createdAt = billReader.GetDateTime(19);
long? createdBy = billReader.IsDBNull(20) ? null : billReader.GetInt64(20); 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); long? updatedBy = billReader.IsDBNull(22) ? null : billReader.GetInt64(22);
// 1.2 (空行后) 反序列化账单明细 // 1.3 (空行后) 主动释放账单 Reader确保后续查询不会触发 Npgsql 并发命令异常
await billReader.DisposeAsync();
// 1.4 (空行后) 反序列化账单明细
var lineItems = new List<BillingLineItemDto>(); var lineItems = new List<BillingLineItemDto>();
if (!string.IsNullOrWhiteSpace(lineItemsJson)) if (!string.IsNullOrWhiteSpace(lineItemsJson))
{ {
@@ -67,7 +88,7 @@ public sealed class GetBillingDetailQueryHandler(
} }
} }
// 1.3 (空行后) 查询支付记录 // 1.5 (空行后) 查询支付记录
var payments = new List<PaymentRecordDto>(); var payments = new List<PaymentRecordDto>();
await using var paymentCommand = CreateCommand( await using var paymentCommand = CreateCommand(
connection, connection,
@@ -100,39 +121,36 @@ public sealed class GetBillingDetailQueryHandler(
}); });
} }
// 1.4 (空行后) 组装详情 DTO // 1.6 (空行后) 组装详情 DTO
var amountDue = billReader.GetDecimal(9);
var discountAmount = billReader.GetDecimal(10);
var taxAmount = billReader.GetDecimal(11);
var totalAmount = amountDue - discountAmount + taxAmount; var totalAmount = amountDue - discountAmount + taxAmount;
return new BillingDetailDto return new BillingDetailDto
{ {
Id = billReader.GetInt64(0), Id = billingId,
TenantId = billReader.GetInt64(1), TenantId = tenantId,
TenantName = billReader.IsDBNull(2) ? string.Empty : billReader.GetString(2), TenantName = tenantName,
SubscriptionId = billReader.IsDBNull(3) ? null : billReader.GetInt64(3), SubscriptionId = subscriptionId,
StatementNo = billReader.GetString(4), StatementNo = statementNo,
BillingType = (BillingType)billReader.GetInt32(5), BillingType = billingType,
Status = (TenantBillingStatus)billReader.GetInt32(6), Status = status,
PeriodStart = billReader.GetDateTime(7), PeriodStart = periodStart,
PeriodEnd = billReader.GetDateTime(8), PeriodEnd = periodEnd,
AmountDue = billReader.GetDecimal(9), AmountDue = amountDue,
DiscountAmount = billReader.GetDecimal(10), DiscountAmount = discountAmount,
TaxAmount = billReader.GetDecimal(11), TaxAmount = taxAmount,
TotalAmount = totalAmount, TotalAmount = totalAmount,
AmountPaid = billReader.GetDecimal(12), AmountPaid = amountPaid,
Currency = billReader.IsDBNull(13) ? "CNY" : billReader.GetString(13), Currency = currency,
DueDate = billReader.GetDateTime(14), DueDate = dueDate,
ReminderSentAt = reminderSentAt, ReminderSentAt = reminderSentAt,
OverdueNotifiedAt = overdueNotifiedAt, OverdueNotifiedAt = overdueNotifiedAt,
LineItemsJson = lineItemsJson, LineItemsJson = lineItemsJson,
LineItems = lineItems, LineItems = lineItems,
Payments = payments, Payments = payments,
Notes = notes, Notes = notes,
CreatedAt = billReader.GetDateTime(19), CreatedAt = createdAt,
CreatedBy = createdBy, CreatedBy = createdBy,
UpdatedAt = billReader.IsDBNull(21) ? null : billReader.GetDateTime(21), UpdatedAt = updatedAt,
UpdatedBy = updatedBy UpdatedBy = updatedBy
}; };
}, },