using Microsoft.EntityFrameworkCore;
using Npgsql;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories;
///
/// 租户聚合的 EF Core 仓储实现。
///
public sealed class EfTenantRepository(TakeoutAppDbContext context) : ITenantRepository
{
///
public Task FindByIdAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.Tenants
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == tenantId, cancellationToken);
}
///
public async Task> FindByIdsAsync(IReadOnlyCollection tenantIds, CancellationToken cancellationToken = default)
{
if (tenantIds.Count == 0)
{
return Array.Empty();
}
return await context.Tenants
.AsNoTracking()
.Where(x => tenantIds.Contains(x.Id))
.ToListAsync(cancellationToken);
}
///
public async Task> SearchAsync(
TenantStatus? status,
string? keyword,
CancellationToken cancellationToken = default)
{
// 1. 构建基础查询
var query = context.Tenants.AsNoTracking();
// 2. 按状态过滤
if (status.HasValue)
{
query = query.Where(x => x.Status == status.Value);
}
// 3. 按关键字过滤
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
query = query.Where(x =>
EF.Functions.ILike(x.Name, $"%{keyword}%") ||
EF.Functions.ILike(x.Code, $"%{keyword}%") ||
EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{keyword}%") ||
EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{keyword}%"));
}
// 4. 排序返回
return await query
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
///
public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync(
TenantStatus? status,
TenantVerificationStatus? verificationStatus,
string? name,
string? contactName,
string? contactPhone,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = context.Tenants.AsNoTracking();
// 1. 按租户状态过滤
if (status.HasValue)
{
query = query.Where(x => x.Status == status.Value);
}
// 2. 按实名认证状态过滤(未提交视为 Draft)
if (verificationStatus.HasValue)
{
query = from tenant in query
join profile in context.TenantVerificationProfiles.AsNoTracking()
on tenant.Id equals profile.TenantId into profiles
from profile in profiles.DefaultIfEmpty()
where (profile == null ? TenantVerificationStatus.Draft : profile.Status) == verificationStatus.Value
select tenant;
}
// 3. 按名称/联系人/电话过滤(模糊匹配)
if (!string.IsNullOrWhiteSpace(name))
{
var normalizedName = name.Trim();
query = query.Where(x => EF.Functions.ILike(x.Name, $"%{normalizedName}%"));
}
// 4. (空行后) 按联系人过滤(模糊匹配)
if (!string.IsNullOrWhiteSpace(contactName))
{
var normalizedContactName = contactName.Trim();
query = query.Where(x => EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{normalizedContactName}%"));
}
// 5. (空行后) 按联系电话过滤(模糊匹配)
if (!string.IsNullOrWhiteSpace(contactPhone))
{
var normalizedContactPhone = contactPhone.Trim();
query = query.Where(x => EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedContactPhone}%"));
}
// 6. (空行后) 兼容关键字查询:名称/编码/联系人/电话
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalizedKeyword = keyword.Trim();
query = query.Where(x =>
EF.Functions.ILike(x.Name, $"%{normalizedKeyword}%") ||
EF.Functions.ILike(x.Code, $"%{normalizedKeyword}%") ||
EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{normalizedKeyword}%") ||
EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedKeyword}%"));
}
// 7. (空行后) 先统计总数,再按创建时间倒序分页
var total = await query.CountAsync(cancellationToken);
// 8. (空行后) 查询当前页数据
var items = await query
.OrderByDescending(x => x.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (items, total);
}
///
public Task AddTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
return context.Tenants.AddAsync(tenant, cancellationToken).AsTask();
}
///
public Task UpdateTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
context.Tenants.Update(tenant);
return Task.CompletedTask;
}
///
public Task ExistsByCodeAsync(string code, CancellationToken cancellationToken = default)
{
var normalized = code.Trim();
return context.Tenants.AnyAsync(x => x.Code == normalized, cancellationToken);
}
///
public Task ExistsByNameAsync(string name, long? excludeTenantId = null, CancellationToken cancellationToken = default)
{
// 1. 标准化名称
var normalized = name.Trim();
// 2. (空行后) 构建查询(名称使用 ILike 做不区分大小写的等值匹配)
var query = context.Tenants
.AsNoTracking()
.Where(x => EF.Functions.ILike(x.Name, normalized));
// 3. (空行后) 更新场景排除自身
if (excludeTenantId.HasValue)
{
query = query.Where(x => x.Id != excludeTenantId.Value);
}
// 4. (空行后) 判断是否存在
return query.AnyAsync(cancellationToken);
}
///
public Task ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default)
{
var normalized = phone.Trim();
return context.Tenants.AnyAsync(x => x.ContactPhone == normalized, cancellationToken);
}
///
public Task FindTenantIdByContactPhoneAsync(string phone, CancellationToken cancellationToken = default)
{
// 1. 标准化手机号
var normalized = phone.Trim();
// 2. 查询租户 ID
return context.Tenants.AsNoTracking()
.Where(x => x.ContactPhone == normalized)
.Select(x => (long?)x.Id)
.FirstOrDefaultAsync(cancellationToken);
}
///
public Task GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantVerificationProfiles
.IgnoreQueryFilters()
.AsNoTracking()
.FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == tenantId, cancellationToken);
}
///
public async Task> GetVerificationProfilesAsync(
IReadOnlyCollection tenantIds,
CancellationToken cancellationToken = default)
{
// 1. tenantIds 为空直接返回
if (tenantIds.Count == 0)
{
return Array.Empty();
}
// 2. 批量查询实名资料
return await context.TenantVerificationProfiles
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && tenantIds.Contains(x.TenantId))
.ToListAsync(cancellationToken);
}
///
public async Task UpsertVerificationProfileAsync(TenantVerificationProfile profile, CancellationToken cancellationToken = default)
{
// 1. 查询现有实名资料
var existing = await context.TenantVerificationProfiles
.IgnoreQueryFilters()
.FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == profile.TenantId, cancellationToken);
if (existing == null)
{
// 2. 不存在则新增
await context.TenantVerificationProfiles.AddAsync(profile, cancellationToken);
return;
}
// 3. 存在则更新当前值
profile.Id = existing.Id;
context.Entry(existing).CurrentValues.SetValues(profile);
}
///
public Task GetActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantReviewClaims
.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ReleasedAt == null)
.OrderByDescending(x => x.ClaimedAt)
.FirstOrDefaultAsync(cancellationToken);
}
///
public Task FindActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantReviewClaims
.Where(x => x.TenantId == tenantId && x.ReleasedAt == null)
.OrderByDescending(x => x.ClaimedAt)
.FirstOrDefaultAsync(cancellationToken);
}
///
public async Task TryAddReviewClaimAsync(
TenantReviewClaim claim,
TenantAuditLog auditLog,
CancellationToken cancellationToken = default)
{
try
{
await context.TenantReviewClaims.AddAsync(claim, cancellationToken);
await context.TenantAuditLogs.AddAsync(auditLog, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return true;
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException pg && pg.SqlState == PostgresErrorCodes.UniqueViolation)
{
context.Entry(claim).State = EntityState.Detached;
context.Entry(auditLog).State = EntityState.Detached;
return false;
}
}
///
public Task UpdateReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default)
{
context.TenantReviewClaims.Update(claim);
return Task.CompletedTask;
}
///
public Task GetActiveSubscriptionAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptions
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
.OrderByDescending(x => x.EffectiveTo)
.FirstOrDefaultAsync(cancellationToken);
}
///
public async Task> GetSubscriptionsAsync(
IReadOnlyCollection tenantIds,
CancellationToken cancellationToken = default)
{
// 1. tenantIds 为空直接返回
if (tenantIds.Count == 0)
{
return Array.Empty();
}
// 2. 批量查询订阅数据
return await context.TenantSubscriptions
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && tenantIds.Contains(x.TenantId))
.OrderByDescending(x => x.EffectiveTo)
.ToListAsync(cancellationToken);
}
///
public Task FindSubscriptionByIdAsync(long tenantId, long subscriptionId, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptions
.IgnoreQueryFilters()
.FirstOrDefaultAsync(
x => x.DeletedAt == null && x.TenantId == tenantId && x.Id == subscriptionId,
cancellationToken);
}
///
public Task AddSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptions.AddAsync(subscription, cancellationToken).AsTask();
}
///
public Task UpdateSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
{
context.TenantSubscriptions.Update(subscription);
return Task.CompletedTask;
}
///
public Task AddSubscriptionHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptionHistories.AddAsync(history, cancellationToken).AsTask();
}
///
public async Task> GetSubscriptionHistoryAsync(long tenantId, CancellationToken cancellationToken = default)
{
return await context.TenantSubscriptionHistories
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
.OrderByDescending(x => x.EffectiveFrom)
.ToListAsync(cancellationToken);
}
///
public Task AddAuditLogAsync(TenantAuditLog log, CancellationToken cancellationToken = default)
{
return context.TenantAuditLogs.AddAsync(log, cancellationToken).AsTask();
}
///
public async Task> GetAuditLogsAsync(long tenantId, CancellationToken cancellationToken = default)
{
return await context.TenantAuditLogs
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
///
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
}