Files
TakeoutSaaS.TenantApi/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPointMallRepository.cs

480 lines
16 KiB
C#

using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Domain.Membership.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary>
/// 会员积分商城 EF Core 仓储实现。
/// </summary>
public sealed class EfPointMallRepository(TakeoutAppDbContext context) : IPointMallRepository
{
/// <inheritdoc />
public Task<MemberPointMallRule?> GetRuleByStoreAsync(
long tenantId,
long storeId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRules
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default)
{
return context.MemberPointMallRules.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallRules.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task<IReadOnlyList<MemberPointMallProduct>> SearchProductsAsync(
long tenantId,
long storeId,
MemberPointMallProductStatus? status,
string? keyword,
CancellationToken cancellationToken = default)
{
var query = context.MemberPointMallProducts
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
if (status.HasValue)
{
query = query.Where(item => item.Status == status.Value);
}
var normalizedKeyword = (keyword ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
{
var like = $"%{normalizedKeyword}%";
query = query.Where(item =>
EF.Functions.ILike(item.Name, like) ||
(item.PhysicalName != null && EF.Functions.ILike(item.PhysicalName, like)));
}
return await query
.OrderByDescending(item => item.Status)
.ThenByDescending(item => item.UpdatedAt ?? item.CreatedAt)
.ThenByDescending(item => item.Id)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task<MemberPointMallProduct?> FindProductByIdAsync(
long tenantId,
long storeId,
long productId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallProducts
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == productId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<MemberPointMallProduct?> GetProductByIdAsync(
long tenantId,
long storeId,
long productId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallProducts
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == productId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
{
return context.MemberPointMallProducts.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallProducts.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task DeleteProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallProducts.Remove(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<bool> HasRecordsByProductIdAsync(
long tenantId,
long storeId,
long pointMallProductId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.AsNoTracking()
.AnyAsync(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.PointMallProductId == pointMallProductId,
cancellationToken);
}
/// <inheritdoc />
public Task<int> CountMemberRedeemsByProductAsync(
long tenantId,
long storeId,
long pointMallProductId,
long memberId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.PointMallProductId == pointMallProductId &&
item.MemberId == memberId &&
item.Status != MemberPointMallRecordStatus.Canceled)
.CountAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<(IReadOnlyList<MemberPointMallRecord> Items, int TotalCount)> SearchRecordsAsync(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var normalizedPage = Math.Max(1, page);
var normalizedPageSize = Math.Clamp(pageSize, 1, 500);
var query = BuildRecordQuery(
tenantId,
storeId,
redeemType,
status,
startUtc,
endUtc,
keyword);
var totalCount = await query.CountAsync(cancellationToken);
if (totalCount == 0)
{
return ([], 0);
}
var items = await query
.OrderByDescending(item => item.RedeemedAt)
.ThenByDescending(item => item.Id)
.Skip((normalizedPage - 1) * normalizedPageSize)
.Take(normalizedPageSize)
.ToListAsync(cancellationToken);
return (items, totalCount);
}
/// <inheritdoc />
public Task<MemberPointMallRecord?> GetRecordByIdAsync(
long tenantId,
long storeId,
long recordId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == recordId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<MemberPointMallRecord?> FindRecordByIdAsync(
long tenantId,
long storeId,
long recordId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == recordId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<MemberPointMallRecord>> ListRecordsForExportAsync(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword,
CancellationToken cancellationToken = default)
{
return await BuildRecordQuery(
tenantId,
storeId,
redeemType,
status,
startUtc,
endUtc,
keyword)
.OrderByDescending(item => item.RedeemedAt)
.ThenByDescending(item => item.Id)
.Take(20_000)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallRecords.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task AddPointLedgerAsync(MemberPointLedger entity, CancellationToken cancellationToken = default)
{
return context.MemberPointLedgers.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public async Task<MemberPointMallRuleStatsSnapshot> GetRuleStatsAsync(
long tenantId,
long storeId,
CancellationToken cancellationToken = default)
{
var redeemedPoints = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
.Select(item => (int?)item.UsedPoints)
.SumAsync(cancellationToken) ?? 0;
var memberIds = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
.Select(item => item.MemberId)
.Distinct()
.ToListAsync(cancellationToken);
var pointMembers = memberIds.Count;
if (pointMembers == 0)
{
return new MemberPointMallRuleStatsSnapshot
{
TotalIssuedPoints = 0,
RedeemedPoints = 0,
PointMembers = 0,
RedeemRate = 0m
};
}
var currentPoints = await context.MemberProfiles
.AsNoTracking()
.Where(item => item.TenantId == tenantId && memberIds.Contains(item.Id))
.Select(item => (int?)item.PointsBalance)
.SumAsync(cancellationToken) ?? 0;
var totalIssuedPoints = Math.Max(redeemedPoints + Math.Max(0, currentPoints), 0);
var redeemRate = totalIssuedPoints <= 0
? 0m
: decimal.Round((decimal)redeemedPoints * 100m / totalIssuedPoints, 1, MidpointRounding.AwayFromZero);
return new MemberPointMallRuleStatsSnapshot
{
TotalIssuedPoints = totalIssuedPoints,
RedeemedPoints = redeemedPoints,
PointMembers = pointMembers,
RedeemRate = redeemRate
};
}
/// <inheritdoc />
public async Task<MemberPointMallRecordStatsSnapshot> GetRecordStatsAsync(
long tenantId,
long storeId,
DateTime nowUtc,
CancellationToken cancellationToken = default)
{
var normalizedNow = NormalizeUtc(nowUtc);
var dayStart = new DateTime(
normalizedNow.Year,
normalizedNow.Month,
normalizedNow.Day,
0,
0,
0,
DateTimeKind.Utc);
var monthStart = new DateTime(
normalizedNow.Year,
normalizedNow.Month,
1,
0,
0,
0,
DateTimeKind.Utc);
var todayRedeemCount = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.RedeemedAt >= dayStart &&
item.RedeemedAt <= normalizedNow)
.CountAsync(cancellationToken);
var pendingPhysicalCount = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.RedeemType == MemberPointMallRedeemType.Physical &&
item.Status == MemberPointMallRecordStatus.PendingPickup)
.CountAsync(cancellationToken);
var currentMonthUsedPoints = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.RedeemedAt >= monthStart &&
item.RedeemedAt <= normalizedNow)
.Select(item => (int?)item.UsedPoints)
.SumAsync(cancellationToken) ?? 0;
return new MemberPointMallRecordStatsSnapshot
{
TodayRedeemCount = todayRedeemCount,
PendingPhysicalCount = pendingPhysicalCount,
CurrentMonthUsedPoints = currentMonthUsedPoints
};
}
/// <inheritdoc />
public async Task<Dictionary<long, MemberPointMallProductAggregateSnapshot>> GetProductAggregatesAsync(
long tenantId,
long storeId,
IReadOnlyCollection<long> pointMallProductIds,
CancellationToken cancellationToken = default)
{
if (pointMallProductIds.Count == 0)
{
return [];
}
var aggregates = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
pointMallProductIds.Contains(item.PointMallProductId))
.GroupBy(item => item.PointMallProductId)
.Select(group => new MemberPointMallProductAggregateSnapshot
{
PointMallProductId = group.Key,
RedeemedCount = group.Count()
})
.ToListAsync(cancellationToken);
return aggregates.ToDictionary(item => item.PointMallProductId, item => item);
}
/// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
private IQueryable<MemberPointMallRecord> BuildRecordQuery(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword)
{
var query = context.MemberPointMallRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
if (redeemType.HasValue)
{
query = query.Where(item => item.RedeemType == redeemType.Value);
}
if (status.HasValue)
{
query = query.Where(item => item.Status == status.Value);
}
if (startUtc.HasValue)
{
var normalizedStart = NormalizeUtc(startUtc.Value);
query = query.Where(item => item.RedeemedAt >= normalizedStart);
}
if (endUtc.HasValue)
{
var normalizedEnd = NormalizeUtc(endUtc.Value);
query = query.Where(item => item.RedeemedAt <= normalizedEnd);
}
var normalizedKeyword = (keyword ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
{
var like = $"%{normalizedKeyword}%";
query = query.Where(item =>
EF.Functions.ILike(item.RecordNo, like) ||
EF.Functions.ILike(item.MemberName, like) ||
EF.Functions.ILike(item.MemberMobileMasked, like) ||
EF.Functions.ILike(item.ProductName, like));
}
return query;
}
private static DateTime NormalizeUtc(DateTime value)
{
return value.Kind switch
{
DateTimeKind.Utc => value,
DateTimeKind.Local => value.ToUniversalTime(),
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
};
}
}