Files
TakeoutSaaS.TenantApi/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantBillingRepository.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

248 lines
9.3 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 TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户账单仓储。
/// </summary>
public interface ITenantBillingRepository
{
/// <summary>
/// 查询账单列表,按状态与时间范围筛选。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="status">账单状态。</param>
/// <param name="from">开始时间UTC。</param>
/// <param name="to">结束时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> SearchAsync(
long tenantId,
TenantBillingStatus? status,
DateTime? from,
DateTime? to,
CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取账单。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> FindByIdAsync(long tenantId, long billingId, CancellationToken cancellationToken = default);
/// <summary>
/// 按账单编号获取账单。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="statementNo">账单编号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <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>
/// <param name="tenantId">租户 ID。</param>
/// <param name="periodStart">账单周期开始时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsNotCancelledByPeriodStartAsync(
long tenantId,
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>
/// <param name="bill">账单实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default);
/// <summary>
/// 更新账单。
/// </summary>
/// <param name="bill">账单实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default);
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 管理员端分页查询账单列表(跨租户)。
/// </summary>
/// <param name="tenantId">租户 ID 筛选(可选)。</param>
/// <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>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单集合与总数。</returns>
Task<(IReadOnlyList<TenantBillingStatement> Items, int Total)> SearchPagedAsync(
long? tenantId,
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>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <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; }
}