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>
This commit is contained in:
2025-12-18 11:24:44 +08:00
parent 98f49ea7ad
commit 4b53862ded
73 changed files with 12688 additions and 305 deletions

View File

@@ -42,6 +42,14 @@ public interface ITenantBillingRepository
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default);
/// <summary>
/// 按账单编号获取账单(不限租户,管理员端使用)。
/// </summary>
/// <param name="statementNo">账单编号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> GetByStatementNoAsync(string statementNo, CancellationToken cancellationToken = default);
/// <summary>
/// 判断是否已存在指定周期开始时间的未取消账单(用于自动续费幂等)。
/// </summary>
@@ -54,6 +62,39 @@ public interface ITenantBillingRepository
DateTime periodStart,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取逾期账单列表(已过到期日且未支付)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>逾期账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetOverdueBillingsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 获取即将到期的账单列表(未来 N 天内到期且未支付)。
/// </summary>
/// <param name="daysAhead">提前天数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>即将到期的账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetBillingsDueSoonAsync(int daysAhead, CancellationToken cancellationToken = default);
/// <summary>
/// 按租户 ID 获取账单列表。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 列表批量获取账单(管理员端/批量操作场景)。
/// </summary>
/// <param name="billingIds">账单 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体列表。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetByIdsAsync(
IReadOnlyCollection<long> billingIds,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增账单。
/// </summary>
@@ -84,6 +125,8 @@ public interface ITenantBillingRepository
/// <param name="status">账单状态筛选(可选)。</param>
/// <param name="from">开始时间UTC可选。</param>
/// <param name="to">结束时间UTC可选。</param>
/// <param name="minAmount">最小应付金额筛选(包含,可选)。</param>
/// <param name="maxAmount">最大应付金额筛选(包含,可选)。</param>
/// <param name="keyword">关键词搜索(账单号或租户名)。</param>
/// <param name="pageNumber">页码(从 1 开始)。</param>
/// <param name="pageSize">页大小。</param>
@@ -94,11 +137,29 @@ public interface ITenantBillingRepository
TenantBillingStatus? status,
DateTime? from,
DateTime? to,
decimal? minAmount,
decimal? maxAmount,
string? keyword,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取账单统计数据(用于报表与仪表盘)。
/// </summary>
/// <param name="tenantId">租户 ID可选管理员可查询所有租户。</param>
/// <param name="startDate">统计开始时间UTC。</param>
/// <param name="endDate">统计结束时间UTC。</param>
/// <param name="groupBy">分组方式Day/Week/Month。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>统计结果。</returns>
Task<TenantBillingStatistics> GetStatisticsAsync(
long? tenantId,
DateTime startDate,
DateTime endDate,
string groupBy,
CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取账单(不限租户,管理员端使用)。
/// </summary>
@@ -107,3 +168,80 @@ public interface ITenantBillingRepository
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> FindByIdAsync(long billingId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 账单统计结果。
/// </summary>
public sealed record TenantBillingStatistics
{
/// <summary>
/// 总账单金额(统计区间内)。
/// </summary>
public decimal TotalAmount { get; init; }
/// <summary>
/// 已支付金额(统计区间内)。
/// </summary>
public decimal PaidAmount { get; init; }
/// <summary>
/// 未支付金额(统计区间内)。
/// </summary>
public decimal UnpaidAmount { get; init; }
/// <summary>
/// 逾期金额(统计区间内)。
/// </summary>
public decimal OverdueAmount { get; init; }
/// <summary>
/// 总账单数量(统计区间内)。
/// </summary>
public int TotalCount { get; init; }
/// <summary>
/// 已支付账单数量(统计区间内)。
/// </summary>
public int PaidCount { get; init; }
/// <summary>
/// 未支付账单数量(统计区间内)。
/// </summary>
public int UnpaidCount { get; init; }
/// <summary>
/// 逾期账单数量(统计区间内)。
/// </summary>
public int OverdueCount { get; init; }
/// <summary>
/// 趋势数据(按 groupBy 聚合)。
/// </summary>
public IReadOnlyList<TenantBillingTrendDataPoint> TrendData { get; init; } = [];
}
/// <summary>
/// 账单趋势统计点。
/// </summary>
public sealed record TenantBillingTrendDataPoint
{
/// <summary>
/// 分组时间点Day/Week/Month 的代表日期UTC
/// </summary>
public DateTime Period { get; init; }
/// <summary>
/// 账单数量。
/// </summary>
public int Count { get; init; }
/// <summary>
/// 总金额。
/// </summary>
public decimal TotalAmount { get; init; }
/// <summary>
/// 已支付金额。
/// </summary>
public decimal PaidAmount { get; init; }
}