Files
TakeoutSaaS.TenantApi/src/Application/TakeoutSaaS.Application/App/Billings/BillingMapping.cs
MSuMshk 4b53862ded feat: 完成账单管理模块后端功能开发及API优化
核心功能:
- 账单CRUD操作(创建、查询、详情、更新状态、删除)
- 支付记录管理(创建支付、审核支付)
- 批量操作支持(批量更新账单状态)
- 统计分析功能(账单统计、逾期账单查询)
- 导出功能(Excel/PDF/CSV)

API端点 (16个):
- GET /api/admin/v1/billings - 账单列表(分页、筛选、排序)
- POST /api/admin/v1/billings - 创建账单
- GET /api/admin/v1/billings/{id} - 账单详情
- DELETE /api/admin/v1/billings/{id} - 删除账单
- PUT /api/admin/v1/billings/{id}/status - 更新状态
- POST /api/admin/v1/billings/batch/status - 批量更新
- GET /api/admin/v1/billings/{id}/payments - 支付记录
- POST /api/admin/v1/billings/{id}/payments - 创建支付
- PUT /api/admin/v1/billings/payments/{paymentId}/verify - 审核支付
- GET /api/admin/v1/billings/statistics - 统计数据
- GET /api/admin/v1/billings/overdue - 逾期账单
- POST /api/admin/v1/billings/export - 导出账单

架构优化:
- 采用CQRS模式分离读写(MediatR + Dapper + EF Core)
- 完整的领域模型设计(TenantBillingStatement, TenantPayment等)
- FluentValidation请求验证
- 状态机管理账单和支付状态流转

API设计优化 (三项改进):
1. 导出API响应Content-Type改为application/octet-stream
2. 支付审核API添加Approved和Notes可选参数,支持通过/拒绝
3. 移除TenantBillings API中重复的TenantId参数

数据库变更:
- 新增账单相关表及关系
- 支持Snowflake ID主键
- 完整的审计字段支持

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 11:24:44 +08:00

201 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
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="billing">账单实体。</param>
/// <param name="tenantName">租户名称。</param>
/// <returns>账单列表 DTO。</returns>
public static BillingListDto ToBillingListDto(this TenantBillingStatement billing, string? tenantName = null)
=> new()
{
Id = billing.Id,
TenantId = billing.TenantId,
SubscriptionId = billing.SubscriptionId,
TenantName = tenantName ?? string.Empty,
StatementNo = billing.StatementNo,
BillingType = billing.BillingType,
Status = billing.Status,
PeriodStart = billing.PeriodStart,
PeriodEnd = billing.PeriodEnd,
AmountDue = billing.AmountDue,
AmountPaid = billing.AmountPaid,
DiscountAmount = billing.DiscountAmount,
TaxAmount = billing.TaxAmount,
TotalAmount = billing.CalculateTotalAmount(),
Currency = billing.Currency,
DueDate = billing.DueDate,
CreatedAt = billing.CreatedAt,
UpdatedAt = billing.UpdatedAt,
IsOverdue = billing.Status == TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Overdue
|| (billing.Status == TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Pending && billing.DueDate < DateTime.UtcNow),
OverdueDays = (billing.Status is TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Pending
or TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus.Overdue)
&& billing.DueDate < DateTime.UtcNow
? (int)(DateTime.UtcNow - billing.DueDate).TotalDays
: 0
};
/// <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="billing">账单实体。</param>
/// <param name="payments">支付记录列表。</param>
/// <param name="tenantName">租户名称。</param>
/// <returns>账单详情 DTO。</returns>
public static BillingDetailDto ToBillingDetailDto(
this TenantBillingStatement billing,
List<TenantPayment> payments,
string? tenantName = null)
{
// 反序列化账单明细
var lineItems = new List<BillingLineItemDto>();
if (!string.IsNullOrWhiteSpace(billing.LineItemsJson))
{
try
{
lineItems = JsonSerializer.Deserialize<List<BillingLineItemDto>>(billing.LineItemsJson) ?? [];
}
catch
{
lineItems = [];
}
}
return new BillingDetailDto
{
Id = billing.Id,
TenantId = billing.TenantId,
TenantName = tenantName ?? string.Empty,
SubscriptionId = billing.SubscriptionId,
StatementNo = billing.StatementNo,
BillingType = billing.BillingType,
Status = billing.Status,
PeriodStart = billing.PeriodStart,
PeriodEnd = billing.PeriodEnd,
AmountDue = billing.AmountDue,
AmountPaid = billing.AmountPaid,
DiscountAmount = billing.DiscountAmount,
TaxAmount = billing.TaxAmount,
TotalAmount = billing.CalculateTotalAmount(),
Currency = billing.Currency,
DueDate = billing.DueDate,
ReminderSentAt = billing.ReminderSentAt,
OverdueNotifiedAt = billing.OverdueNotifiedAt,
LineItemsJson = billing.LineItemsJson,
LineItems = lineItems,
Payments = payments.Select(p => p.ToPaymentRecordDto()).ToList(),
Notes = billing.Notes,
CreatedAt = billing.CreatedAt,
CreatedBy = billing.CreatedBy,
UpdatedAt = billing.UpdatedAt,
UpdatedBy = billing.UpdatedBy
};
}
/// <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
};
/// <summary>
/// 将支付记录实体映射为支付记录 DTO新版
/// </summary>
/// <param name="payment">支付记录实体。</param>
/// <returns>支付记录 DTO。</returns>
public static PaymentRecordDto ToPaymentRecordDto(this TenantPayment payment)
=> new()
{
Id = payment.Id,
TenantId = payment.TenantId,
BillingId = payment.BillingStatementId,
Amount = payment.Amount,
Method = payment.Method,
Status = payment.Status,
TransactionNo = payment.TransactionNo,
ProofUrl = payment.ProofUrl,
IsVerified = payment.VerifiedAt.HasValue,
PaidAt = payment.PaidAt,
VerifiedBy = payment.VerifiedBy,
VerifiedAt = payment.VerifiedAt,
RefundReason = payment.RefundReason,
RefundedAt = payment.RefundedAt,
Notes = payment.Notes,
CreatedAt = payment.CreatedAt
};
}