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;
}
}