using System; using System.Linq; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Infrastructure.App.Persistence; namespace TakeoutSaaS.Infrastructure.App.Repositories; /// /// 门店聚合的 EF Core 仓储实现。 /// /// /// 初始化仓储。 /// public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepository { /// public Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var query = context.Stores.AsNoTracking(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } return query .Where(x => x.Id == storeId) .FirstOrDefaultAsync(cancellationToken); } /// public async Task> GetByMerchantIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { return await context.Stores .AsNoTracking() .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .OrderBy(x => x.Name) .ToListAsync(cancellationToken); } /// public async Task> SearchAsync( long tenantId, long? merchantId, StoreStatus? status, StoreAuditStatus? auditStatus, StoreBusinessStatus? businessStatus, StoreOwnershipType? ownershipType, string? keyword, bool ignoreTenantFilter = false, CancellationToken cancellationToken = default) { var query = context.Stores.AsNoTracking(); if (ignoreTenantFilter) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } if (merchantId.HasValue) { query = query.Where(x => x.MerchantId == merchantId.Value); } if (status.HasValue) { query = query.Where(x => x.Status == status.Value); } if (auditStatus.HasValue) { query = query.Where(x => x.AuditStatus == auditStatus.Value); } if (businessStatus.HasValue) { query = query.Where(x => x.BusinessStatus == businessStatus.Value); } if (ownershipType.HasValue) { query = query.Where(x => x.OwnershipType == ownershipType.Value); } if (!string.IsNullOrWhiteSpace(keyword)) { var trimmed = keyword.Trim(); query = query.Where(x => x.Name.Contains(trimmed) || x.Code.Contains(trimmed)); } var stores = await query .OrderBy(x => x.Name) .ToListAsync(cancellationToken); return stores; } /// public async Task ExistsStoreWithinDistanceAsync( long merchantId, long tenantId, double longitude, double latitude, double distanceMeters, CancellationToken cancellationToken = default) { // 1. 校验距离阈值 if (distanceMeters <= 0) { return false; } // 2. (空行后) 拉取候选坐标 var coordinates = await context.Stores .AsNoTracking() .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .Where(x => x.Longitude.HasValue && x.Latitude.HasValue) .Select(x => new { Longitude = x.Longitude!.Value, Latitude = x.Latitude!.Value }) .ToListAsync(cancellationToken); // 3. (空行后) 计算距离并判断是否命中 foreach (var coordinate in coordinates) { var distance = CalculateDistanceMeters(latitude, longitude, coordinate.Latitude, coordinate.Longitude); if (distance <= distanceMeters) { return true; } } // 4. (空行后) 返回未命中结果 return false; } /// public async Task> GetStoreCountsAsync(long? tenantId, IReadOnlyCollection merchantIds, CancellationToken cancellationToken = default) { if (merchantIds.Count == 0) { return new Dictionary(); } var query = context.Stores.AsNoTracking(); if (!tenantId.HasValue || tenantId.Value <= 0) { query = query.IgnoreQueryFilters(); } else { query = query.Where(x => x.TenantId == tenantId.Value); } return await query .Where(x => merchantIds.Contains(x.MerchantId)) .GroupBy(x => x.MerchantId) .Select(group => new { group.Key, Count = group.Count() }) .ToDictionaryAsync(x => x.Key, x => x.Count, cancellationToken); } /// public async Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreBusinessHours.AsNoTracking(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } var hours = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.DayOfWeek) .ThenBy(x => x.StartTime) .ToListAsync(cancellationToken); return hours; } /// public Task GetStoreFeeAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreFees.AsNoTracking(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } return query .Where(x => x.StoreId == storeId) .FirstOrDefaultAsync(cancellationToken); } /// public Task AddStoreFeeAsync(StoreFee storeFee, CancellationToken cancellationToken = default) { return context.StoreFees.AddAsync(storeFee, cancellationToken).AsTask(); } /// public Task UpdateStoreFeeAsync(StoreFee storeFee, CancellationToken cancellationToken = default) { context.StoreFees.Update(storeFee); return Task.CompletedTask; } /// public async Task> GetQualificationsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreQualifications.AsNoTracking(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } var qualifications = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.SortOrder) .ThenBy(x => x.QualificationType) .ToListAsync(cancellationToken); return qualifications; } /// public Task FindQualificationByIdAsync(long qualificationId, long tenantId, CancellationToken cancellationToken = default) { return context.StoreQualifications .Where(x => x.TenantId == tenantId && x.Id == qualificationId) .FirstOrDefaultAsync(cancellationToken); } /// public Task AddQualificationAsync(StoreQualification qualification, CancellationToken cancellationToken = default) { return context.StoreQualifications.AddAsync(qualification, cancellationToken).AsTask(); } /// public Task UpdateQualificationAsync(StoreQualification qualification, CancellationToken cancellationToken = default) { context.StoreQualifications.Update(qualification); return Task.CompletedTask; } /// public async Task DeleteQualificationAsync(long qualificationId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.StoreQualifications .Where(x => x.TenantId == tenantId && x.Id == qualificationId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreQualifications.Remove(existing); } } /// public Task AddAuditRecordAsync(StoreAuditRecord record, CancellationToken cancellationToken = default) { return context.StoreAuditRecords.AddAsync(record, cancellationToken).AsTask(); } /// public async Task> GetAuditRecordsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var records = await context.StoreAuditRecords .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderByDescending(x => x.CreatedAt) .ToListAsync(cancellationToken); return records; } /// public Task FindBusinessHourByIdAsync(long businessHourId, long tenantId, CancellationToken cancellationToken = default) { return context.StoreBusinessHours .Where(x => x.TenantId == tenantId && x.Id == businessHourId) .FirstOrDefaultAsync(cancellationToken); } /// public async Task> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreDeliveryZones.AsNoTracking(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } var zones = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.SortOrder) .ToListAsync(cancellationToken); return zones; } /// public Task FindDeliveryZoneByIdAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreDeliveryZones.AsQueryable(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } return query .Where(x => x.Id == deliveryZoneId) .FirstOrDefaultAsync(cancellationToken); } /// public async Task> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreHolidays.AsNoTracking(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } var holidays = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.Date) .ToListAsync(cancellationToken); return holidays; } /// public Task FindHolidayByIdAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreHolidays.AsQueryable(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } return query .Where(x => x.Id == holidayId) .FirstOrDefaultAsync(cancellationToken); } /// public async Task> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var areas = await context.StoreTableAreas .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.SortOrder) .ToListAsync(cancellationToken); return areas; } /// public Task FindTableAreaByIdAsync(long areaId, long tenantId, CancellationToken cancellationToken = default) { return context.StoreTableAreas .Where(x => x.TenantId == tenantId && x.Id == areaId) .FirstOrDefaultAsync(cancellationToken); } /// public async Task> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var tables = await context.StoreTables .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.TableCode) .ToListAsync(cancellationToken); return tables; } /// public Task FindTableByIdAsync(long tableId, long tenantId, CancellationToken cancellationToken = default) { return context.StoreTables .Where(x => x.TenantId == tenantId && x.Id == tableId) .FirstOrDefaultAsync(cancellationToken); } /// public Task FindTableByCodeAsync(string tableCode, long tenantId, CancellationToken cancellationToken = default) { return context.StoreTables .Where(x => x.TenantId == tenantId && x.TableCode == tableCode) .FirstOrDefaultAsync(cancellationToken); } /// public Task GetPickupSettingAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { return context.StorePickupSettings .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .FirstOrDefaultAsync(cancellationToken); } /// public Task AddPickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default) { return context.StorePickupSettings.AddAsync(setting, cancellationToken).AsTask(); } /// public Task UpdatePickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default) { context.StorePickupSettings.Update(setting); return Task.CompletedTask; } /// public async Task> GetPickupSlotsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var slots = await context.StorePickupSlots .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId) .OrderBy(x => x.StartTime) .ToListAsync(cancellationToken); return slots; } /// public Task FindPickupSlotByIdAsync(long slotId, long tenantId, CancellationToken cancellationToken = default) { return context.StorePickupSlots .Where(x => x.TenantId == tenantId && x.Id == slotId) .FirstOrDefaultAsync(cancellationToken); } /// public Task AddPickupSlotsAsync(IEnumerable slots, CancellationToken cancellationToken = default) { return context.StorePickupSlots.AddRangeAsync(slots, cancellationToken); } /// public Task UpdatePickupSlotAsync(StorePickupSlot slot, CancellationToken cancellationToken = default) { context.StorePickupSlots.Update(slot); return Task.CompletedTask; } /// public async Task> GetShiftsAsync(long storeId, long tenantId, DateTime? from = null, DateTime? to = null, CancellationToken cancellationToken = default) { var query = context.StoreEmployeeShifts .AsNoTracking() .Where(x => x.TenantId == tenantId && x.StoreId == storeId); if (from.HasValue) { query = query.Where(x => x.ShiftDate >= from.Value.Date); } if (to.HasValue) { query = query.Where(x => x.ShiftDate <= to.Value.Date); } var shifts = await query .OrderBy(x => x.ShiftDate) .ThenBy(x => x.StartTime) .ToListAsync(cancellationToken); return shifts; } /// public Task FindShiftByIdAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default) { return context.StoreEmployeeShifts .Where(x => x.TenantId == tenantId && x.Id == shiftId) .FirstOrDefaultAsync(cancellationToken); } /// public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default) { return context.Stores.AddAsync(store, cancellationToken).AsTask(); } /// public Task AddBusinessHoursAsync(IEnumerable hours, CancellationToken cancellationToken = default) { return context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken); } /// public Task UpdateBusinessHourAsync(StoreBusinessHour hour, CancellationToken cancellationToken = default) { context.StoreBusinessHours.Update(hour); return Task.CompletedTask; } /// public Task AddDeliveryZonesAsync(IEnumerable zones, CancellationToken cancellationToken = default) { return context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken); } /// public Task UpdateDeliveryZoneAsync(StoreDeliveryZone zone, CancellationToken cancellationToken = default) { context.StoreDeliveryZones.Update(zone); return Task.CompletedTask; } /// public Task AddHolidaysAsync(IEnumerable holidays, CancellationToken cancellationToken = default) { return context.StoreHolidays.AddRangeAsync(holidays, cancellationToken); } /// public Task UpdateHolidayAsync(StoreHoliday holiday, CancellationToken cancellationToken = default) { context.StoreHolidays.Update(holiday); return Task.CompletedTask; } /// public Task AddTableAreasAsync(IEnumerable areas, CancellationToken cancellationToken = default) { return context.StoreTableAreas.AddRangeAsync(areas, cancellationToken); } /// public Task UpdateTableAreaAsync(StoreTableArea area, CancellationToken cancellationToken = default) { context.StoreTableAreas.Update(area); return Task.CompletedTask; } /// public Task AddTablesAsync(IEnumerable tables, CancellationToken cancellationToken = default) { return context.StoreTables.AddRangeAsync(tables, cancellationToken); } /// public Task UpdateTableAsync(StoreTable table, CancellationToken cancellationToken = default) { context.StoreTables.Update(table); return Task.CompletedTask; } /// public Task AddShiftsAsync(IEnumerable shifts, CancellationToken cancellationToken = default) { return context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken); } /// public Task UpdateShiftAsync(StoreEmployeeShift shift, CancellationToken cancellationToken = default) { context.StoreEmployeeShifts.Update(shift); return Task.CompletedTask; } /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { return context.SaveChangesAsync(cancellationToken); } /// public async Task DeleteBusinessHourAsync(long businessHourId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.StoreBusinessHours .Where(x => x.TenantId == tenantId && x.Id == businessHourId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreBusinessHours.Remove(existing); } } /// public async Task DeleteDeliveryZoneAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreDeliveryZones.AsQueryable(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } var existing = await query .Where(x => x.Id == deliveryZoneId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreDeliveryZones.Remove(existing); } } /// public async Task DeleteHolidayAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default) { var query = context.StoreHolidays.AsQueryable(); if (tenantId <= 0) { query = query.IgnoreQueryFilters() .Where(x => x.DeletedAt == null); } else { query = query.Where(x => x.TenantId == tenantId); } var existing = await query .Where(x => x.Id == holidayId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreHolidays.Remove(existing); } } /// public async Task DeleteTableAreaAsync(long areaId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.StoreTableAreas .Where(x => x.TenantId == tenantId && x.Id == areaId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreTableAreas.Remove(existing); } } /// public async Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.StoreTables .Where(x => x.TenantId == tenantId && x.Id == tableId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreTables.Remove(existing); } } /// public async Task DeletePickupSlotAsync(long slotId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.StorePickupSlots .Where(x => x.TenantId == tenantId && x.Id == slotId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StorePickupSlots.Remove(existing); } } /// public async Task DeleteShiftAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.StoreEmployeeShifts .Where(x => x.TenantId == tenantId && x.Id == shiftId) .FirstOrDefaultAsync(cancellationToken); if (existing != null) { context.StoreEmployeeShifts.Remove(existing); } } /// public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default) { context.Stores.Update(store); return Task.CompletedTask; } /// public async Task DeleteStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { var existing = await context.Stores .Where(x => x.TenantId == tenantId && x.Id == storeId) .FirstOrDefaultAsync(cancellationToken); if (existing == null) { return; } context.Stores.Remove(existing); } private static double CalculateDistanceMeters(double latitude1, double longitude1, double latitude2, double longitude2) { const double earthRadius = 6371000d; var latRad1 = DegreesToRadians(latitude1); var latRad2 = DegreesToRadians(latitude2); var deltaLat = DegreesToRadians(latitude2 - latitude1); var deltaLon = DegreesToRadians(longitude2 - longitude1); var sinLat = Math.Sin(deltaLat / 2); var sinLon = Math.Sin(deltaLon / 2); var a = sinLat * sinLat + Math.Cos(latRad1) * Math.Cos(latRad2) * sinLon * sinLon; var c = 2 * Math.Asin(Math.Min(1, Math.Sqrt(a))); return earthRadius * c; } private static double DegreesToRadians(double degrees) => degrees * (Math.PI / 180d); }