343 lines
12 KiB
C#
343 lines
12 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||
using TakeoutSaaS.Infrastructure.Logs.Persistence;
|
||
|
||
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||
|
||
/// <summary>
|
||
/// 租户聚合的 EF Core 仓储实现。
|
||
/// </summary>
|
||
public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsDbContext logsContext) : ITenantRepository
|
||
{
|
||
/// <inheritdoc />
|
||
public Task<Tenant?> FindByIdAsync(long tenantId, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.Tenants
|
||
.AsNoTracking()
|
||
.FirstOrDefaultAsync(x => x.Id == tenantId, cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<Tenant>> FindByIdsAsync(IReadOnlyCollection<long> tenantIds, CancellationToken cancellationToken = default)
|
||
{
|
||
if (tenantIds.Count == 0)
|
||
{
|
||
return Array.Empty<Tenant>();
|
||
}
|
||
|
||
return await context.Tenants
|
||
.AsNoTracking()
|
||
.Where(x => tenantIds.Contains(x.Id))
|
||
.ToListAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<Tenant>> 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);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<(IReadOnlyList<Tenant> 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);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task AddTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.Tenants.AddAsync(tenant, cancellationToken).AsTask();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task UpdateTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
|
||
{
|
||
context.Tenants.Update(tenant);
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<bool> ExistsByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||
{
|
||
var normalized = code.Trim();
|
||
return context.Tenants.AnyAsync(x => x.Code == normalized, cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<bool> 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);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<bool> ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default)
|
||
{
|
||
var normalized = phone.Trim();
|
||
return context.Tenants.AnyAsync(x => x.ContactPhone == normalized, cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<long?> 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);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.TenantVerificationProfiles
|
||
.AsNoTracking()
|
||
.FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == tenantId, cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<TenantVerificationProfile>> GetVerificationProfilesAsync(
|
||
IReadOnlyCollection<long> tenantIds,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. tenantIds 为空直接返回
|
||
if (tenantIds.Count == 0)
|
||
{
|
||
return Array.Empty<TenantVerificationProfile>();
|
||
}
|
||
|
||
// 2. 批量查询实名资料
|
||
return await context.TenantVerificationProfiles
|
||
.AsNoTracking()
|
||
.Where(x => x.DeletedAt == null && tenantIds.Contains(x.TenantId))
|
||
.ToListAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task UpsertVerificationProfileAsync(TenantVerificationProfile profile, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 查询现有实名资料
|
||
var existing = await context.TenantVerificationProfiles
|
||
.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);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<TenantSubscription?> GetActiveSubscriptionAsync(long tenantId, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.TenantSubscriptions
|
||
.AsNoTracking()
|
||
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
|
||
.OrderByDescending(x => x.EffectiveTo)
|
||
.FirstOrDefaultAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<TenantSubscription>> GetSubscriptionsAsync(
|
||
IReadOnlyCollection<long> tenantIds,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. tenantIds 为空直接返回
|
||
if (tenantIds.Count == 0)
|
||
{
|
||
return Array.Empty<TenantSubscription>();
|
||
}
|
||
|
||
// 2. 批量查询订阅数据
|
||
return await context.TenantSubscriptions
|
||
.AsNoTracking()
|
||
.Where(x => x.DeletedAt == null && tenantIds.Contains(x.TenantId))
|
||
.OrderByDescending(x => x.EffectiveTo)
|
||
.ToListAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<TenantSubscription?> FindSubscriptionByIdAsync(long tenantId, long subscriptionId, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.TenantSubscriptions
|
||
.FirstOrDefaultAsync(
|
||
x => x.DeletedAt == null && x.TenantId == tenantId && x.Id == subscriptionId,
|
||
cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task AddSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.TenantSubscriptions.AddAsync(subscription, cancellationToken).AsTask();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task UpdateSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
|
||
{
|
||
context.TenantSubscriptions.Update(subscription);
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task AddSubscriptionHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default)
|
||
{
|
||
return context.TenantSubscriptionHistories.AddAsync(history, cancellationToken).AsTask();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<TenantSubscriptionHistory>> GetSubscriptionHistoryAsync(long tenantId, CancellationToken cancellationToken = default)
|
||
{
|
||
return await context.TenantSubscriptionHistories
|
||
.AsNoTracking()
|
||
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
|
||
.OrderByDescending(x => x.EffectiveFrom)
|
||
.ToListAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task AddAuditLogAsync(TenantAuditLog log, CancellationToken cancellationToken = default)
|
||
{
|
||
return logsContext.TenantAuditLogs.AddAsync(log, cancellationToken).AsTask();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<TenantAuditLog>> GetAuditLogsAsync(long tenantId, CancellationToken cancellationToken = default)
|
||
{
|
||
return await logsContext.TenantAuditLogs
|
||
.AsNoTracking()
|
||
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
|
||
.OrderByDescending(x => x.CreatedAt)
|
||
.ToListAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 保存业务库变更
|
||
await context.SaveChangesAsync(cancellationToken);
|
||
|
||
// 2. 保存日志库变更
|
||
await logsContext.SaveChangesAsync(cancellationToken);
|
||
}
|
||
}
|