完成门店管理后端接口与任务

This commit is contained in:
2026-01-01 07:26:14 +08:00
parent dc9f6136d6
commit fc55003d3d
131 changed files with 15333 additions and 201 deletions

View File

@@ -0,0 +1,198 @@
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;
/// <summary>
/// 门店定时任务服务实现。
/// </summary>
public sealed class StoreSchedulerService(
TakeoutAppDbContext context,
ILogger<StoreSchedulerService> logger)
: IStoreSchedulerService
{
/// <inheritdoc />
public async Task<int> 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.IsClosed
&& holiday.Date.Date == today)
.ToListAsync(cancellationToken);
// 3. (空行后) 构造查找表
var hoursLookup = hours
.GroupBy(hour => hour.StoreId)
.ToDictionary(group => group.Key, group => (IReadOnlyList<StoreBusinessHour>)group.ToList());
var holidaySet = holidays.Select(holiday => holiday.StoreId).ToHashSet();
// 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 isHolidayClosed = holidaySet.Contains(store.Id);
var hasHours = hoursLookup.TryGetValue(store.Id, out var storeHours) && storeHours.Count > 0;
var isOpen = !isHolidayClosed && hasHours && IsWithinBusinessHours(storeHours ?? [], now);
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;
}
/// <inheritdoc />
public async Task<int> CheckQualificationExpiryAsync(DateTime now, CancellationToken cancellationToken)
{
// 1. 查询过期门店
var expiredStoreIds = await context.StoreQualifications
.AsNoTracking()
.Where(qualification => qualification.DeletedAt == null
&& qualification.ExpiresAt.HasValue
&& qualification.ExpiresAt.Value < now)
.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<StoreBusinessHour> 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 DayOfWeek NextDay(DayOfWeek day)
{
var next = (int)day + 1;
return next > 6 ? DayOfWeek.Sunday : (DayOfWeek)next;
}
}