feat: 完成租户个人中心 API 首版实现

This commit is contained in:
2026-02-09 20:01:11 +08:00
parent f61554fc08
commit 2711893474
53 changed files with 2547 additions and 0 deletions

View File

@@ -56,6 +56,7 @@ public static class AppServiceCollectionExtensions
services.AddScoped<ITenantPackageRepository, EfTenantPackageRepository>();
services.AddScoped<ITenantQuotaUsageRepository, EfTenantQuotaUsageRepository>();
services.AddScoped<ITenantQuotaUsageHistoryRepository, EfTenantQuotaUsageHistoryRepository>();
services.AddScoped<ITenantVisibilityRoleRuleRepository, TenantVisibilityRoleRuleRepository>();
services.AddScoped<IInventoryRepository, EfInventoryRepository>();
services.AddScoped<IQuotaPackageRepository, EfQuotaPackageRepository>();
services.AddScoped<IStatisticsRepository, EfStatisticsRepository>();

View File

@@ -11,6 +11,39 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence.Repositories;
/// </summary>
public sealed class TenantPaymentRepository(TakeoutAppDbContext context) : ITenantPaymentRepository
{
/// <inheritdoc />
public async Task<(IReadOnlyList<TenantPayment> Items, int Total)> SearchPagedAsync(
long tenantId,
DateTime from,
DateTime to,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
// 1. 归一化分页参数
var normalizedPage = page <= 0 ? 1 : page;
var normalizedPageSize = pageSize <= 0 ? 20 : pageSize;
// 2. 构建查询(按支付时间倒序)
var query = context.TenantPayments
.AsNoTracking()
.Where(x => x.DeletedAt == null
&& x.TenantId == tenantId
&& (x.PaidAt ?? x.CreatedAt) >= from
&& (x.PaidAt ?? x.CreatedAt) <= to);
// 3. 执行分页
var total = await query.CountAsync(cancellationToken);
var items = await query
.OrderByDescending(x => x.PaidAt ?? x.CreatedAt)
.Skip((normalizedPage - 1) * normalizedPageSize)
.Take(normalizedPageSize)
.ToListAsync(cancellationToken);
// 4. 返回分页结果
return (items, total);
}
/// <inheritdoc />
public async Task<IReadOnlyList<TenantPayment>> GetByBillingIdAsync(long billingStatementId, CancellationToken cancellationToken = default)
{

View File

@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Persistence.Repositories;
/// <summary>
/// 租户可见角色规则仓储实现。
/// </summary>
public sealed class TenantVisibilityRoleRuleRepository(TakeoutAppDbContext context) : ITenantVisibilityRoleRuleRepository
{
/// <summary>
/// 按租户获取规则。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>规则实体或 null。</returns>
public Task<TenantVisibilityRoleRule?> FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantVisibilityRoleRules
.AsNoTracking()
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
}
/// <summary>
/// 新增规则。
/// </summary>
/// <param name="rule">规则实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddAsync(TenantVisibilityRoleRule rule, CancellationToken cancellationToken = default)
{
return context.TenantVisibilityRoleRules.AddAsync(rule, cancellationToken).AsTask();
}
/// <summary>
/// 更新规则。
/// </summary>
/// <param name="rule">规则实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task UpdateAsync(TenantVisibilityRoleRule rule, CancellationToken cancellationToken = default)
{
context.TenantVisibilityRoleRules.Update(rule);
return Task.CompletedTask;
}
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -88,6 +88,10 @@ public sealed class TakeoutAppDbContext(
/// </summary>
public DbSet<TenantVerificationProfile> TenantVerificationProfiles => Set<TenantVerificationProfile>();
/// <summary>
/// 租户账单/配额可见角色规则。
/// </summary>
public DbSet<TenantVisibilityRoleRule> TenantVisibilityRoleRules => Set<TenantVisibilityRoleRule>();
/// <summary>
/// 配额包定义。
/// </summary>
public DbSet<QuotaPackage> QuotaPackages => Set<QuotaPackage>();
@@ -394,6 +398,7 @@ public sealed class TakeoutAppDbContext(
ConfigureTenantAnnouncement(modelBuilder.Entity<TenantAnnouncement>());
ConfigureTenantAnnouncementRead(modelBuilder.Entity<TenantAnnouncementRead>());
ConfigureTenantVerificationProfile(modelBuilder.Entity<TenantVerificationProfile>());
ConfigureTenantVisibilityRoleRule(modelBuilder.Entity<TenantVisibilityRoleRule>());
ConfigureQuotaPackage(modelBuilder.Entity<QuotaPackage>());
ConfigureTenantQuotaPackagePurchase(modelBuilder.Entity<TenantQuotaPackagePurchase>());
ConfigureMerchantDocument(modelBuilder.Entity<MerchantDocument>());
@@ -834,6 +839,18 @@ public sealed class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.Channel, x.SentAt });
}
private static void ConfigureTenantVisibilityRoleRule(EntityTypeBuilder<TenantVisibilityRoleRule> builder)
{
builder.ToTable("tenant_visibility_role_rules");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.QuotaVisibleRoleCodes).HasColumnType("text[]");
builder.Property(x => x.BillingVisibleRoleCodes).HasColumnType("text[]");
builder.Property(x => x.UpdatedBy).IsRequired();
builder.Property(x => x.UpdatedAt).IsRequired();
builder.HasIndex(x => x.TenantId).IsUnique();
}
private static void ConfigureTenantAnnouncement(EntityTypeBuilder<TenantAnnouncement> builder)
{
builder.ToTable("tenant_announcements");

View File

@@ -1,3 +1,4 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Infrastructure.Logs.Persistence;
@@ -9,6 +10,44 @@ namespace TakeoutSaaS.Infrastructure.Logs.Repositories;
/// </summary>
public sealed class EfOperationLogRepository(TakeoutLogsDbContext logsContext) : IOperationLogRepository
{
/// <inheritdoc />
public async Task<(IReadOnlyList<OperationLog> Items, int Total)> SearchByOperatorPagedAsync(
long tenantId,
string operatorId,
DateTime from,
DateTime to,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
// 1. 归一化参数
var normalizedOperatorId = operatorId.Trim();
var normalizedPage = page <= 0 ? 1 : page;
var normalizedPageSize = pageSize <= 0 ? 50 : pageSize;
// 2. 构建查询(操作人 + 时间窗 + 租户约束)
var query = logsContext.OperationLogs
.AsNoTracking()
.Where(x => x.DeletedAt == null
&& x.OperatorId == normalizedOperatorId
&& x.CreatedAt >= from
&& x.CreatedAt <= to
&& x.Parameters != null
&& (EF.Functions.ILike(x.Parameters, $"%\"tenantId\":{tenantId}%")
|| EF.Functions.ILike(x.Parameters, $"%\"TenantId\":{tenantId}%")));
// 3. 查询总数与分页项
var total = await query.CountAsync(cancellationToken);
var items = await query
.OrderByDescending(x => x.CreatedAt)
.Skip((normalizedPage - 1) * normalizedPageSize)
.Take(normalizedPageSize)
.ToListAsync(cancellationToken);
// 4. 返回分页结果
return (items, total);
}
/// <inheritdoc />
public Task AddAsync(OperationLog log, CancellationToken cancellationToken = default)
{