using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores.Services; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Infrastructure.App.Persistence; namespace TakeoutSaaS.Infrastructure.App.Services; /// /// 门店定时任务服务实现。 /// public sealed class StoreSchedulerService( TakeoutAdminDbContext context, ILogger logger) : IStoreSchedulerService { /// public async Task AutoSwitchBusinessStatusAsync(DateTime now, CancellationToken cancellationToken) { // 1. 读取候选门店 var stores = await context.Stores .Where(store => store.DeletedAt == null && store.AuditStatus == StoreAuditStatus.Activated && store.BusinessStatus != StoreBusinessStatus.ForceClosed) .ToListAsync(cancellationToken); if (stores.Count == 0) { return 0; } // 2. (空行后) 读取营业时段与休息日 var storeIds = stores.Select(store => store.Id).ToArray(); var hours = await context.StoreBusinessHours .AsNoTracking() .Where(hour => storeIds.Contains(hour.StoreId)) .ToListAsync(cancellationToken); var today = now.Date; var holidays = await context.StoreHolidays .AsNoTracking() .Where(holiday => storeIds.Contains(holiday.StoreId) && holiday.Date <= today && (holiday.EndDate == null || holiday.EndDate >= today)) .ToListAsync(cancellationToken); // 3. (空行后) 构造查找表 var hoursLookup = hours .GroupBy(hour => hour.StoreId) .ToDictionary(group => group.Key, group => (IReadOnlyList)group.ToList()); var holidayLookup = holidays .GroupBy(holiday => holiday.StoreId) .ToDictionary(group => group.Key, group => (IReadOnlyList)group.ToList()); // 4. (空行后) 判定状态并更新 var updated = 0; foreach (var store in stores) { // 4.1 跳过强制关闭门店 if (store.BusinessStatus == StoreBusinessStatus.ForceClosed) { continue; } // 4.2 (空行后) 尊重手动歇业原因 if (store.ClosureReason.HasValue && store.ClosureReason != StoreClosureReason.OutOfBusinessHours) { continue; } // 4.3 (空行后) 计算营业状态 var storeHolidays = holidayLookup.TryGetValue(store.Id, out var matched) ? matched : []; var nowTime = now.TimeOfDay; var isHolidayClosed = storeHolidays.Any(holiday => holiday.OverrideType == OverrideType.Closed && IsWithinHolidayTime(holiday, nowTime)); var hasModifiedHours = storeHolidays.Any(holiday => holiday.OverrideType == OverrideType.ModifiedHours); var isModifiedOpen = hasModifiedHours && storeHolidays.Any(holiday => holiday.OverrideType == OverrideType.ModifiedHours && IsWithinHolidayTime(holiday, nowTime)); var isTemporaryOpen = storeHolidays.Any(holiday => holiday.OverrideType == OverrideType.TemporaryOpen && IsWithinHolidayTime(holiday, nowTime)); var hasHours = hoursLookup.TryGetValue(store.Id, out var storeHours) && storeHours.Count > 0; var isOpen = false; if (isHolidayClosed) { isOpen = false; } else if (hasModifiedHours) { isOpen = isModifiedOpen; } else { isOpen = hasHours && IsWithinBusinessHours(storeHours ?? [], now); if (!isOpen && isTemporaryOpen) { isOpen = true; } } if (isOpen) { if (store.BusinessStatus != StoreBusinessStatus.Open) { store.BusinessStatus = StoreBusinessStatus.Open; store.ClosureReason = null; store.ClosureReasonText = null; updated++; } continue; } // 4.4 (空行后) 非营业时段切换为休息 if (store.BusinessStatus != StoreBusinessStatus.Resting || store.ClosureReason != StoreClosureReason.OutOfBusinessHours) { store.BusinessStatus = StoreBusinessStatus.Resting; store.ClosureReason = StoreClosureReason.OutOfBusinessHours; store.ClosureReasonText = "非营业时间自动休息"; updated++; } } // 5. (空行后) 保存变更并记录日志 if (updated > 0) { await context.SaveChangesAsync(cancellationToken); } logger.LogInformation("定时任务:营业状态自动切换完成,更新 {UpdatedCount} 家门店", updated); return updated; } /// public async Task CheckQualificationExpiryAsync(DateTime now, CancellationToken cancellationToken) { // 1. 查询过期门店 var today = DateOnly.FromDateTime(now); var expiredStoreIds = await context.StoreQualifications .AsNoTracking() .Where(qualification => qualification.DeletedAt == null && qualification.ExpiresAt.HasValue && qualification.ExpiresAt.Value < today) .Select(qualification => qualification.StoreId) .Distinct() .ToListAsync(cancellationToken); if (expiredStoreIds.Count == 0) { return 0; } // 2. (空行后) 加载门店并更新状态 var stores = await context.Stores .Where(store => expiredStoreIds.Contains(store.Id) && store.DeletedAt == null && store.AuditStatus == StoreAuditStatus.Activated && store.BusinessStatus != StoreBusinessStatus.ForceClosed) .ToListAsync(cancellationToken); var updated = 0; foreach (var store in stores) { // 2.1 跳过已标记过期门店 if (store.BusinessStatus == StoreBusinessStatus.Resting && store.ClosureReason == StoreClosureReason.LicenseExpired) { continue; } // 2.2 (空行后) 设置资质过期状态 store.BusinessStatus = StoreBusinessStatus.Resting; store.ClosureReason = StoreClosureReason.LicenseExpired; store.ClosureReasonText = "证照过期自动休息"; updated++; } // 3. (空行后) 保存变更并记录日志 if (updated > 0) { await context.SaveChangesAsync(cancellationToken); } logger.LogInformation("定时任务:资质过期检查完成,更新 {UpdatedCount} 家门店", updated); return updated; } private static bool IsWithinBusinessHours(IReadOnlyList hours, DateTime now) { // 1. 提取当前时间 var day = now.DayOfWeek; var time = now.TimeOfDay; foreach (var hour in hours) { if (hour.HourType == BusinessHourType.Closed) { continue; } if (hour.StartTime == hour.EndTime) { continue; } if (hour.StartTime < hour.EndTime) { if (hour.DayOfWeek == day && time >= hour.StartTime && time < hour.EndTime) { return true; } continue; } var nextDay = NextDay(hour.DayOfWeek); if (hour.DayOfWeek == day && time >= hour.StartTime) { return true; } if (nextDay == day && time < hour.EndTime) { return true; } } return false; } private static bool IsWithinHolidayTime(StoreHoliday holiday, TimeSpan time) { if (holiday.IsAllDay) { return true; } if (!holiday.StartTime.HasValue || !holiday.EndTime.HasValue) { return false; } return time >= holiday.StartTime.Value && time < holiday.EndTime.Value; } private static DayOfWeek NextDay(DayOfWeek day) { var next = (int)day + 1; return next > 6 ? DayOfWeek.Sunday : (DayOfWeek)next; } }