@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新营业时段命令。
|
||||
/// </summary>
|
||||
public sealed record BatchUpdateBusinessHoursCommand : IRequest<IReadOnlyList<StoreBusinessHourDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 营业时段集合。
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoreBusinessHourInputDto> Items { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建营业时段命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreBusinessHourCommand : IRequest<StoreBusinessHourDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 星期几。
|
||||
/// </summary>
|
||||
public DayOfWeek DayOfWeek { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 时段类型。
|
||||
/// </summary>
|
||||
public BusinessHourType HourType { get; init; } = BusinessHourType.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public TimeSpan StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public TimeSpan EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 容量限制。
|
||||
/// </summary>
|
||||
public int? CapacityLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建配送区域命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreDeliveryZoneCommand : IRequest<StoreDeliveryZoneDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域名称。
|
||||
/// </summary>
|
||||
public string ZoneName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// GeoJSON。
|
||||
/// </summary>
|
||||
public string PolygonGeoJson { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 起送价。
|
||||
/// </summary>
|
||||
public decimal? MinimumOrderAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 配送费。
|
||||
/// </summary>
|
||||
public decimal? DeliveryFee { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 预计分钟。
|
||||
/// </summary>
|
||||
public int? EstimatedMinutes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序。
|
||||
/// </summary>
|
||||
public int SortOrder { get; init; } = 100;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建员工排班命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreEmployeeShiftCommand : IRequest<StoreEmployeeShiftDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工 ID。
|
||||
/// </summary>
|
||||
public long StaffId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 班次日期。
|
||||
/// </summary>
|
||||
public DateTime ShiftDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public TimeSpan StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public TimeSpan EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排班角色。
|
||||
/// </summary>
|
||||
public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk;
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建临时时段配置命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreHolidayCommand : IRequest<StoreHolidayDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期。
|
||||
/// </summary>
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(可选)。
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否全天。
|
||||
/// </summary>
|
||||
public bool IsAllDay { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(IsAllDay=false 时必填)。
|
||||
/// </summary>
|
||||
public TimeSpan? StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间(IsAllDay=false 时必填)。
|
||||
/// </summary>
|
||||
public TimeSpan? EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖类型。
|
||||
/// </summary>
|
||||
public OverrideType OverrideType { get; init; } = OverrideType.Closed;
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建自提档期命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStorePickupSlotCommand : IRequest<StorePickupSlotDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public TimeSpan StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public TimeSpan EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 截单分钟。
|
||||
/// </summary>
|
||||
public int CutoffMinutes { get; init; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 容量。
|
||||
/// </summary>
|
||||
public int Capacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用星期。
|
||||
/// </summary>
|
||||
public string Weekdays { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建门店资质命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreQualificationCommand : IRequest<StoreQualificationDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 资质类型。
|
||||
/// </summary>
|
||||
public StoreQualificationType QualificationType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 证照文件 URL。
|
||||
/// </summary>
|
||||
public string FileUrl { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 证照编号。
|
||||
/// </summary>
|
||||
public string? DocumentNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 签发日期。
|
||||
/// </summary>
|
||||
public DateOnly? IssuedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期日期。
|
||||
/// </summary>
|
||||
public DateOnly? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int SortOrder { get; init; } = 100;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建门店员工命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreStaffCommand : IRequest<StoreStaffDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
public string Phone { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱。
|
||||
/// </summary>
|
||||
public string? Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色。
|
||||
/// </summary>
|
||||
public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建桌台区域命令。
|
||||
/// </summary>
|
||||
public sealed record CreateStoreTableAreaCommand : IRequest<StoreTableAreaDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 区域描述。
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int SortOrder { get; init; } = 100;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除营业时段命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreBusinessHourCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 营业时段 ID。
|
||||
/// </summary>
|
||||
public long BusinessHourId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除配送区域命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreDeliveryZoneCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 配送区域 ID。
|
||||
/// </summary>
|
||||
public long DeliveryZoneId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除员工排班命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreEmployeeShiftCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排班 ID。
|
||||
/// </summary>
|
||||
public long ShiftId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除节假日配置命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreHolidayCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 节假日 ID。
|
||||
/// </summary>
|
||||
public long HolidayId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除自提档期命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStorePickupSlotCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 档期 ID。
|
||||
/// </summary>
|
||||
public long SlotId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店资质命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreQualificationCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 资质 ID。
|
||||
/// </summary>
|
||||
public long QualificationId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店员工命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreStaffCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工 ID。
|
||||
/// </summary>
|
||||
public long StaffId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除桌台区域命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreTableAreaCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域 ID。
|
||||
/// </summary>
|
||||
public long AreaId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除桌码命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteStoreTableCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 桌台 ID。
|
||||
/// </summary>
|
||||
public long TableId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 批量生成桌码命令。
|
||||
/// </summary>
|
||||
public sealed record GenerateStoreTablesCommand : IRequest<IReadOnlyList<StoreTableDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 桌码前缀。
|
||||
/// </summary>
|
||||
public string TableCodePrefix { get; init; } = "T";
|
||||
|
||||
/// <summary>
|
||||
/// 起始序号。
|
||||
/// </summary>
|
||||
public int StartNumber { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 生成数量。
|
||||
/// </summary>
|
||||
public int Count { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认容量。
|
||||
/// </summary>
|
||||
public int DefaultCapacity { get; init; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// 区域 ID。
|
||||
/// </summary>
|
||||
public long? AreaId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签。
|
||||
/// </summary>
|
||||
public string? Tags { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 提交门店审核命令。
|
||||
/// </summary>
|
||||
public sealed record SubmitStoreAuditCommand : IRequest<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 切换门店经营状态命令。
|
||||
/// </summary>
|
||||
public sealed record ToggleBusinessStatusCommand : IRequest<StoreDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标经营状态。
|
||||
/// </summary>
|
||||
public StoreBusinessStatus BusinessStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 歇业原因。
|
||||
/// </summary>
|
||||
public StoreClosureReason? ClosureReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 歇业原因补充说明。
|
||||
/// </summary>
|
||||
public string? ClosureReasonText { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新营业时段命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreBusinessHourCommand : IRequest<StoreBusinessHourDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 营业时段 ID。
|
||||
/// </summary>
|
||||
public long BusinessHourId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 星期几。
|
||||
/// </summary>
|
||||
public DayOfWeek DayOfWeek { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 时段类型。
|
||||
/// </summary>
|
||||
public BusinessHourType HourType { get; init; } = BusinessHourType.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public TimeSpan StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public TimeSpan EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 容量限制。
|
||||
/// </summary>
|
||||
public int? CapacityLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新配送区域命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreDeliveryZoneCommand : IRequest<StoreDeliveryZoneDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 配送区域 ID。
|
||||
/// </summary>
|
||||
public long DeliveryZoneId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域名称。
|
||||
/// </summary>
|
||||
public string ZoneName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// GeoJSON。
|
||||
/// </summary>
|
||||
public string PolygonGeoJson { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 起送价。
|
||||
/// </summary>
|
||||
public decimal? MinimumOrderAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 配送费。
|
||||
/// </summary>
|
||||
public decimal? DeliveryFee { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 预计分钟。
|
||||
/// </summary>
|
||||
public int? EstimatedMinutes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序。
|
||||
/// </summary>
|
||||
public int SortOrder { get; init; } = 100;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新员工排班命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreEmployeeShiftCommand : IRequest<StoreEmployeeShiftDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 排班 ID。
|
||||
/// </summary>
|
||||
public long ShiftId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工 ID。
|
||||
/// </summary>
|
||||
public long StaffId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 班次日期。
|
||||
/// </summary>
|
||||
public DateTime ShiftDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public TimeSpan StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public TimeSpan EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排班角色。
|
||||
/// </summary>
|
||||
public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk;
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店费用配置命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreFeeCommand : IRequest<StoreFeeDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 起送费。
|
||||
/// </summary>
|
||||
public decimal MinimumOrderAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 配送费。
|
||||
/// </summary>
|
||||
public decimal DeliveryFee { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 打包费模式。
|
||||
/// </summary>
|
||||
public PackagingFeeMode PackagingFeeMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单打包费规则。
|
||||
/// </summary>
|
||||
public OrderPackagingFeeMode OrderPackagingFeeMode { get; init; } = OrderPackagingFeeMode.Fixed;
|
||||
|
||||
/// <summary>
|
||||
/// 固定打包费。
|
||||
/// </summary>
|
||||
public decimal? FixedPackagingFee { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 阶梯打包费配置。
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoreFeeTierDto> PackagingFeeTiers { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 免配送费门槛。
|
||||
/// </summary>
|
||||
public decimal? FreeDeliveryThreshold { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新临时时段配置命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreHolidayCommand : IRequest<StoreHolidayDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 节假日 ID。
|
||||
/// </summary>
|
||||
public long HolidayId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期。
|
||||
/// </summary>
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(可选)。
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否全天。
|
||||
/// </summary>
|
||||
public bool IsAllDay { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(IsAllDay=false 时必填)。
|
||||
/// </summary>
|
||||
public TimeSpan? StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间(IsAllDay=false 时必填)。
|
||||
/// </summary>
|
||||
public TimeSpan? EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖类型。
|
||||
/// </summary>
|
||||
public OverrideType OverrideType { get; init; } = OverrideType.Closed;
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新自提档期命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStorePickupSlotCommand : IRequest<StorePickupSlotDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 档期 ID。
|
||||
/// </summary>
|
||||
public long SlotId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public TimeSpan StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public TimeSpan EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 截单分钟。
|
||||
/// </summary>
|
||||
public int CutoffMinutes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 容量。
|
||||
/// </summary>
|
||||
public int Capacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用星期。
|
||||
/// </summary>
|
||||
public string Weekdays { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店资质命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreQualificationCommand : IRequest<StoreQualificationDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 资质 ID。
|
||||
/// </summary>
|
||||
public long QualificationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 证照文件 URL。
|
||||
/// </summary>
|
||||
public string? FileUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 证照编号。
|
||||
/// </summary>
|
||||
public string? DocumentNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 签发日期。
|
||||
/// </summary>
|
||||
public DateOnly? IssuedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期日期。
|
||||
/// </summary>
|
||||
public DateOnly? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int? SortOrder { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店员工命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreStaffCommand : IRequest<StoreStaffDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 员工 ID。
|
||||
/// </summary>
|
||||
public long StaffId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
public string Phone { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱。
|
||||
/// </summary>
|
||||
public string? Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色。
|
||||
/// </summary>
|
||||
public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk;
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public StaffStatus Status { get; init; } = StaffStatus.Active;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新桌台区域命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreTableAreaCommand : IRequest<StoreTableAreaDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 区域 ID。
|
||||
/// </summary>
|
||||
public long AreaId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 区域描述。
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int SortOrder { get; init; } = 100;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新桌码命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateStoreTableCommand : IRequest<StoreTableDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 桌台 ID。
|
||||
/// </summary>
|
||||
public long TableId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域 ID。
|
||||
/// </summary>
|
||||
public long? AreaId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 桌码。
|
||||
/// </summary>
|
||||
public string TableCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 容量。
|
||||
/// </summary>
|
||||
public int Capacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 标签。
|
||||
/// </summary>
|
||||
public string? Tags { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public StoreTableStatus Status { get; init; } = StoreTableStatus.Idle;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新自提配置命令。
|
||||
/// </summary>
|
||||
public sealed record UpsertStorePickupSettingCommand : IRequest<StorePickupSettingDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许当天。
|
||||
/// </summary>
|
||||
public bool AllowToday { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 可预约天数。
|
||||
/// </summary>
|
||||
public int AllowDaysAhead { get; init; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 默认截单分钟。
|
||||
/// </summary>
|
||||
public int DefaultCutoffMinutes { get; init; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 单笔最大份数。
|
||||
/// </summary>
|
||||
public int? MaxQuantityPerOrder { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Validators;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新营业时段处理器。
|
||||
/// </summary>
|
||||
public sealed class BatchUpdateBusinessHoursCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<BatchUpdateBusinessHoursCommandHandler> logger)
|
||||
: IRequestHandler<BatchUpdateBusinessHoursCommand, IReadOnlyList<StoreBusinessHourDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreBusinessHourDto>> Handle(BatchUpdateBusinessHoursCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
var storeTenantId = store.TenantId;
|
||||
|
||||
// 2. (空行后) 校验时段重叠
|
||||
var overlapError = BusinessHourValidators.ValidateOverlap(request.Items);
|
||||
if (!string.IsNullOrWhiteSpace(overlapError))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, overlapError);
|
||||
}
|
||||
|
||||
// 3. (空行后) 删除旧时段
|
||||
var existingHours = await storeRepository.GetBusinessHoursAsync(request.StoreId, storeTenantId, cancellationToken);
|
||||
foreach (var hour in existingHours)
|
||||
{
|
||||
await storeRepository.DeleteBusinessHourAsync(hour.Id, storeTenantId, cancellationToken);
|
||||
}
|
||||
|
||||
// 4. (空行后) 新增时段配置
|
||||
if (request.Items.Count > 0)
|
||||
{
|
||||
var hours = request.Items.Select(item => new StoreBusinessHour
|
||||
{
|
||||
TenantId = storeTenantId,
|
||||
StoreId = request.StoreId,
|
||||
DayOfWeek = item.DayOfWeek,
|
||||
HourType = item.HourType,
|
||||
StartTime = item.StartTime,
|
||||
EndTime = item.EndTime,
|
||||
CapacityLimit = item.CapacityLimit,
|
||||
Notes = item.Notes?.Trim()
|
||||
}).ToList();
|
||||
|
||||
await storeRepository.AddBusinessHoursAsync(hours, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. (空行后) 保存并返回结果
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("批量更新门店营业时段 {StoreId}", request.StoreId);
|
||||
|
||||
var refreshed = await storeRepository.GetBusinessHoursAsync(request.StoreId, storeTenantId, cancellationToken);
|
||||
return refreshed.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店费用预览查询处理器。
|
||||
/// </summary>
|
||||
public sealed class CalculateStoreFeeQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IStoreFeeCalculationService feeCalculationService)
|
||||
: IRequestHandler<CalculateStoreFeeQuery, StoreFeeCalculationResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreFeeCalculationResultDto> Handle(CalculateStoreFeeQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 获取费用配置
|
||||
var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken)
|
||||
?? new StoreFee
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
MinimumOrderAmount = 0m,
|
||||
BaseDeliveryFee = 0m,
|
||||
PackagingFeeMode = PackagingFeeMode.Fixed,
|
||||
OrderPackagingFeeMode = OrderPackagingFeeMode.Fixed,
|
||||
FixedPackagingFee = 0m
|
||||
};
|
||||
|
||||
// 3. (空行后) 执行费用计算
|
||||
var calculationRequest = new StoreFeeCalculationRequestDto
|
||||
{
|
||||
OrderAmount = request.OrderAmount,
|
||||
ItemCount = request.ItemCount,
|
||||
Items = request.Items
|
||||
};
|
||||
return feeCalculationService.Calculate(fee, calculationRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 配送范围检测查询处理器。
|
||||
/// </summary>
|
||||
public sealed class CheckStoreDeliveryZoneQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IDeliveryZoneService deliveryZoneService)
|
||||
: IRequestHandler<CheckStoreDeliveryZoneQuery, StoreDeliveryCheckResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreDeliveryCheckResultDto> Handle(CheckStoreDeliveryZoneQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行配送范围判断
|
||||
var zones = await storeRepository.GetDeliveryZonesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var result = deliveryZoneService.CheckPointInZones(zones, request.Longitude, request.Latitude);
|
||||
|
||||
// 3. (空行后) 计算距离
|
||||
if (store.Longitude.HasValue && store.Latitude.HasValue)
|
||||
{
|
||||
var distance = CalculateDistanceKm(store.Latitude.Value, store.Longitude.Value, request.Latitude, request.Longitude);
|
||||
result = result with { Distance = (decimal)Math.Round(distance, 2, MidpointRounding.AwayFromZero) };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double CalculateDistanceKm(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 / 1000d;
|
||||
}
|
||||
|
||||
private static double DegreesToRadians(double degrees) => degrees * (Math.PI / 180d);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店资质完整性检查处理器。
|
||||
/// </summary>
|
||||
public sealed class CheckStoreQualificationsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<CheckStoreQualificationsQuery, StoreQualificationCheckResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreQualificationCheckResultDto> Handle(CheckStoreQualificationsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 同主体门店默认视为完整
|
||||
if (store.OwnershipType == StoreOwnershipType.SameEntity)
|
||||
{
|
||||
return new StoreQualificationCheckResultDto
|
||||
{
|
||||
IsComplete = true,
|
||||
CanSubmitAudit = true
|
||||
};
|
||||
}
|
||||
|
||||
// 3. (空行后) 读取资质列表并统计
|
||||
var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var grouped = qualifications
|
||||
.GroupBy(x => x.QualificationType)
|
||||
.ToDictionary(x => x.Key, x => x.ToList());
|
||||
|
||||
var expiredCount = qualifications.Count(x => x.IsExpired);
|
||||
var expiringSoonCount = qualifications.Count(x => x.IsExpiringSoon);
|
||||
|
||||
var foodStats = BuildRequirement(grouped, StoreQualificationType.FoodServiceLicense, true);
|
||||
var businessStats = BuildRequirement(grouped, StoreQualificationType.BusinessLicense, true);
|
||||
var storefrontStats = BuildRequirement(grouped, StoreQualificationType.StorefrontPhoto, true);
|
||||
var interiorStats = BuildInteriorRequirement(grouped);
|
||||
|
||||
var hasLicense = foodStats.IsValid || businessStats.IsValid;
|
||||
var hasStorefront = storefrontStats.IsValid;
|
||||
var hasInterior = interiorStats.IsValid;
|
||||
|
||||
var missingTypes = new List<string>();
|
||||
if (!hasLicense)
|
||||
{
|
||||
missingTypes.Add("营业执照/食品经营许可证");
|
||||
}
|
||||
|
||||
if (!hasStorefront)
|
||||
{
|
||||
missingTypes.Add("门头实景照");
|
||||
}
|
||||
|
||||
if (!hasInterior)
|
||||
{
|
||||
missingTypes.Add("店内环境照(至少2张)");
|
||||
}
|
||||
|
||||
var warnings = missingTypes.Count == 0
|
||||
? Array.Empty<string>()
|
||||
: missingTypes.Select(type => $"缺少必要资质:{type}").ToArray();
|
||||
|
||||
// 4. (空行后) 组装结果
|
||||
var requirements = new List<StoreQualificationRequirementDto>
|
||||
{
|
||||
foodStats,
|
||||
businessStats,
|
||||
storefrontStats,
|
||||
interiorStats
|
||||
};
|
||||
|
||||
var isComplete = hasLicense && hasStorefront && hasInterior;
|
||||
return new StoreQualificationCheckResultDto
|
||||
{
|
||||
IsComplete = isComplete,
|
||||
CanSubmitAudit = isComplete,
|
||||
RequiredTypes = requirements,
|
||||
ExpiringSoonCount = expiringSoonCount,
|
||||
ExpiredCount = expiredCount,
|
||||
MissingTypes = missingTypes,
|
||||
Warnings = warnings
|
||||
};
|
||||
}
|
||||
|
||||
private static StoreQualificationRequirementDto BuildRequirement(
|
||||
IReadOnlyDictionary<StoreQualificationType, List<Domain.Stores.Entities.StoreQualification>> grouped,
|
||||
StoreQualificationType type,
|
||||
bool required)
|
||||
{
|
||||
var list = grouped.TryGetValue(type, out var items) ? items : [];
|
||||
var hasUploaded = list.Count > 0;
|
||||
var hasValid = list.Any(item => !item.IsExpired);
|
||||
return new StoreQualificationRequirementDto
|
||||
{
|
||||
QualificationType = type,
|
||||
IsRequired = required,
|
||||
IsUploaded = hasUploaded,
|
||||
IsValid = hasValid,
|
||||
UploadedCount = list.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static StoreQualificationRequirementDto BuildInteriorRequirement(
|
||||
IReadOnlyDictionary<StoreQualificationType, List<Domain.Stores.Entities.StoreQualification>> grouped)
|
||||
{
|
||||
var list = grouped.TryGetValue(StoreQualificationType.InteriorPhoto, out var items) ? items : [];
|
||||
var validCount = list.Count(item => !item.IsExpired);
|
||||
return new StoreQualificationRequirementDto
|
||||
{
|
||||
QualificationType = StoreQualificationType.InteriorPhoto,
|
||||
IsRequired = true,
|
||||
IsUploaded = list.Count > 0,
|
||||
IsValid = validCount >= 2,
|
||||
UploadedCount = list.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建营业时段处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreBusinessHourCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStoreBusinessHourCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreBusinessHourCommand, StoreBusinessHourDto>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<CreateStoreBusinessHourCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreBusinessHourDto> Handle(CreateStoreBusinessHourCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var store = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 构建实体
|
||||
var hour = new StoreBusinessHour
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
DayOfWeek = request.DayOfWeek,
|
||||
HourType = request.HourType,
|
||||
StartTime = request.StartTime,
|
||||
EndTime = request.EndTime,
|
||||
CapacityLimit = request.CapacityLimit,
|
||||
Notes = request.Notes?.Trim()
|
||||
};
|
||||
|
||||
// 3. 持久化
|
||||
await _storeRepository.AddBusinessHoursAsync(new[] { hour }, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("创建营业时段 {BusinessHourId} 对应门店 {StoreId}", hour.Id, request.StoreId);
|
||||
|
||||
// 4. 返回 DTO
|
||||
return StoreMapping.ToDto(hour);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建配送区域处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreDeliveryZoneCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IGeoJsonValidationService geoJsonValidationService,
|
||||
ILogger<CreateStoreDeliveryZoneCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreDeliveryZoneCommand, StoreDeliveryZoneDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreDeliveryZoneDto> Handle(CreateStoreDeliveryZoneCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
var storeTenantId = store.TenantId;
|
||||
|
||||
// 2. (空行后) 校验 GeoJSON
|
||||
var validation = geoJsonValidationService.ValidatePolygon(request.PolygonGeoJson);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, validation.ErrorMessage ?? "配送范围格式错误");
|
||||
}
|
||||
|
||||
// 3. (空行后) 构建实体
|
||||
var zone = new StoreDeliveryZone
|
||||
{
|
||||
TenantId = storeTenantId,
|
||||
StoreId = request.StoreId,
|
||||
ZoneName = request.ZoneName.Trim(),
|
||||
PolygonGeoJson = (validation.NormalizedGeoJson ?? request.PolygonGeoJson).Trim(),
|
||||
MinimumOrderAmount = request.MinimumOrderAmount,
|
||||
DeliveryFee = request.DeliveryFee,
|
||||
EstimatedMinutes = request.EstimatedMinutes,
|
||||
SortOrder = request.SortOrder
|
||||
};
|
||||
|
||||
// 4. (空行后) 持久化
|
||||
await storeRepository.AddDeliveryZonesAsync(new[] { zone }, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("创建配送区域 {DeliveryZoneId} 对应门店 {StoreId}", zone.Id, request.StoreId);
|
||||
|
||||
// 5. (空行后) 返回 DTO
|
||||
return StoreMapping.ToDto(zone);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建排班处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreEmployeeShiftCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
IMerchantRepository merchantRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStoreEmployeeShiftCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreEmployeeShiftCommand, StoreEmployeeShiftDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreEmployeeShiftDto> Handle(CreateStoreEmployeeShiftCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 校验员工归属与状态
|
||||
var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken);
|
||||
if (staff is null || (staff.StoreId.HasValue && staff.StoreId != request.StoreId))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "员工不存在或不属于该门店");
|
||||
}
|
||||
|
||||
// 3. 校验日期与冲突
|
||||
var from = request.ShiftDate.Date;
|
||||
var to = request.ShiftDate.Date;
|
||||
var shifts = await storeRepository.GetShiftsAsync(request.StoreId, tenantId, from, to, cancellationToken);
|
||||
var hasConflict = shifts.Any(x => x.StaffId == request.StaffId && x.ShiftDate == request.ShiftDate);
|
||||
if (hasConflict)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "该员工当日已存在排班");
|
||||
}
|
||||
|
||||
// 4. 构建实体
|
||||
var shift = new StoreEmployeeShift
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
StaffId = request.StaffId,
|
||||
ShiftDate = request.ShiftDate.Date,
|
||||
StartTime = request.StartTime,
|
||||
EndTime = request.EndTime,
|
||||
RoleType = request.RoleType,
|
||||
Notes = request.Notes?.Trim()
|
||||
};
|
||||
|
||||
// 5. 持久化
|
||||
await storeRepository.AddShiftsAsync(new[] { shift }, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("创建排班 {ShiftId} 员工 {StaffId} 门店 {StoreId}", shift.Id, shift.StaffId, shift.StoreId);
|
||||
|
||||
// 6. 返回 DTO
|
||||
return StoreMapping.ToDto(shift);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建节假日配置处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreHolidayCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStoreHolidayCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreHolidayCommand, StoreHolidayDto>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<CreateStoreHolidayCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreHolidayDto> Handle(CreateStoreHolidayCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var store = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
var storeTenantId = store.TenantId;
|
||||
|
||||
// 2. 构建实体
|
||||
var holiday = new StoreHoliday
|
||||
{
|
||||
TenantId = storeTenantId,
|
||||
StoreId = request.StoreId,
|
||||
Date = NormalizeToUtc(request.Date),
|
||||
EndDate = request.EndDate.HasValue ? NormalizeToUtc(request.EndDate.Value) : null,
|
||||
IsAllDay = request.IsAllDay,
|
||||
StartTime = request.StartTime,
|
||||
EndTime = request.EndTime,
|
||||
OverrideType = request.OverrideType,
|
||||
IsClosed = request.OverrideType == OverrideType.Closed,
|
||||
Reason = request.Reason?.Trim()
|
||||
};
|
||||
|
||||
// 3. 持久化
|
||||
await _storeRepository.AddHolidaysAsync(new[] { holiday }, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("创建节假日 {HolidayId} 对应门店 {StoreId}", holiday.Id, request.StoreId);
|
||||
|
||||
// 4. 返回 DTO
|
||||
return StoreMapping.ToDto(holiday);
|
||||
}
|
||||
|
||||
private static DateTime NormalizeToUtc(DateTime value)
|
||||
{
|
||||
return value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建自提档期处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStorePickupSlotCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStorePickupSlotCommandHandler> logger)
|
||||
: IRequestHandler<CreateStorePickupSlotCommand, StorePickupSlotDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StorePickupSlotDto> Handle(CreateStorePickupSlotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 新建档期
|
||||
var slot = new StorePickupSlot
|
||||
{
|
||||
TenantId = tenantId,
|
||||
StoreId = request.StoreId,
|
||||
Name = request.Name.Trim(),
|
||||
StartTime = request.StartTime,
|
||||
EndTime = request.EndTime,
|
||||
CutoffMinutes = request.CutoffMinutes,
|
||||
Capacity = request.Capacity,
|
||||
ReservedCount = 0,
|
||||
Weekdays = request.Weekdays,
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
await storeRepository.AddPickupSlotsAsync(new[] { slot }, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("创建自提档期 {SlotId} for store {StoreId}", slot.Id, request.StoreId);
|
||||
return new StorePickupSlotDto
|
||||
{
|
||||
Id = slot.Id,
|
||||
StoreId = slot.StoreId,
|
||||
Name = slot.Name,
|
||||
StartTime = slot.StartTime,
|
||||
EndTime = slot.EndTime,
|
||||
CutoffMinutes = slot.CutoffMinutes,
|
||||
Capacity = slot.Capacity,
|
||||
ReservedCount = slot.ReservedCount,
|
||||
Weekdays = slot.Weekdays,
|
||||
IsEnabled = slot.IsEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建门店资质处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreQualificationCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStoreQualificationCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreQualificationCommand, StoreQualificationDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreQualificationDto> Handle(CreateStoreQualificationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 审核中门店禁止修改资质
|
||||
if (store.AuditStatus == StoreAuditStatus.Pending)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店审核中,无法修改资质");
|
||||
}
|
||||
|
||||
// 3. (空行后) 检查是否需要替换同类型记录
|
||||
var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var shouldReplace = ShouldReplace(request.QualificationType);
|
||||
var existing = shouldReplace
|
||||
? qualifications.FirstOrDefault(x => x.QualificationType == request.QualificationType)
|
||||
: null;
|
||||
|
||||
// 4. (空行后) 构建或更新资质实体
|
||||
if (existing is null)
|
||||
{
|
||||
existing = new StoreQualification
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
QualificationType = request.QualificationType,
|
||||
FileUrl = request.FileUrl.Trim(),
|
||||
DocumentNumber = request.DocumentNumber?.Trim(),
|
||||
IssuedAt = request.IssuedAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
SortOrder = request.SortOrder
|
||||
};
|
||||
|
||||
await storeRepository.AddQualificationAsync(existing, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.FileUrl = request.FileUrl.Trim();
|
||||
existing.DocumentNumber = request.DocumentNumber?.Trim();
|
||||
existing.IssuedAt = request.IssuedAt;
|
||||
existing.ExpiresAt = request.ExpiresAt;
|
||||
existing.SortOrder = request.SortOrder;
|
||||
|
||||
await storeRepository.UpdateQualificationAsync(existing, cancellationToken);
|
||||
}
|
||||
|
||||
// 5. (空行后) 保存变更并返回结果
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新门店资质 {QualificationId} 对应门店 {StoreId}", existing.Id, request.StoreId);
|
||||
|
||||
return StoreMapping.ToDto(existing);
|
||||
}
|
||||
|
||||
private static bool ShouldReplace(StoreQualificationType type)
|
||||
=> type is StoreQualificationType.BusinessLicense
|
||||
or StoreQualificationType.FoodServiceLicense
|
||||
or StoreQualificationType.StorefrontPhoto;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建门店员工处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreStaffCommandHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStoreStaffCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreStaffCommand, StoreStaffDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreStaffDto> Handle(CreateStoreStaffCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 组装员工
|
||||
var staff = new MerchantStaff
|
||||
{
|
||||
MerchantId = store.MerchantId,
|
||||
StoreId = request.StoreId,
|
||||
Name = request.Name.Trim(),
|
||||
Phone = request.Phone.Trim(),
|
||||
Email = request.Email?.Trim(),
|
||||
RoleType = request.RoleType,
|
||||
Status = StaffStatus.Active
|
||||
};
|
||||
|
||||
// 3. 持久化
|
||||
await merchantRepository.AddStaffAsync(staff, cancellationToken);
|
||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("创建门店员工 {StaffId} 门店 {StoreId}", staff.Id, request.StoreId);
|
||||
|
||||
// 4. 返回 DTO
|
||||
return StoreMapping.ToDto(staff);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 创建桌台区域处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateStoreTableAreaCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<CreateStoreTableAreaCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreTableAreaCommand, StoreTableAreaDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreTableAreaDto> Handle(CreateStoreTableAreaCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 校验区域名称唯一
|
||||
var existingAreas = await storeRepository.GetTableAreasAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var hasDuplicate = existingAreas.Any(x => x.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (hasDuplicate)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "区域名称已存在");
|
||||
}
|
||||
|
||||
// 3. 构建实体
|
||||
var area = new StoreTableArea
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
Name = request.Name.Trim(),
|
||||
Description = request.Description?.Trim(),
|
||||
SortOrder = request.SortOrder
|
||||
};
|
||||
|
||||
// 4. 持久化
|
||||
await storeRepository.AddTableAreasAsync(new[] { area }, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("创建桌台区域 {AreaId} 对应门店 {StoreId}", area.Id, request.StoreId);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return StoreMapping.ToDto(area);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除营业时段处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreBusinessHourCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreBusinessHourCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreBusinessHourCommand, bool>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<DeleteStoreBusinessHourCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreBusinessHourCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取时段
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var existing = await _storeRepository.FindBusinessHourByIdAsync(request.BusinessHourId, tenantId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (existing.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 删除
|
||||
await _storeRepository.DeleteBusinessHourAsync(request.BusinessHourId, tenantId, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("删除营业时段 {BusinessHourId} 对应门店 {StoreId}", request.BusinessHourId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除配送区域处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreDeliveryZoneCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreDeliveryZoneCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreDeliveryZoneCommand, bool>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<DeleteStoreDeliveryZoneCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreDeliveryZoneCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取区域
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var existing = await _storeRepository.FindDeliveryZoneByIdAsync(request.DeliveryZoneId, tenantId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (existing.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 删除
|
||||
await _storeRepository.DeleteDeliveryZoneAsync(request.DeliveryZoneId, tenantId, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("删除配送区域 {DeliveryZoneId} 对应门店 {StoreId}", request.DeliveryZoneId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除排班处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreEmployeeShiftCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreEmployeeShiftCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreEmployeeShiftCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreEmployeeShiftCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取排班
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var shift = await storeRepository.FindShiftByIdAsync(request.ShiftId, tenantId, cancellationToken);
|
||||
if (shift is null || shift.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 删除
|
||||
await storeRepository.DeleteShiftAsync(request.ShiftId, tenantId, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("删除排班 {ShiftId} 门店 {StoreId}", request.ShiftId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除节假日配置处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreHolidayCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreHolidayCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreHolidayCommand, bool>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<DeleteStoreHolidayCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreHolidayCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取配置
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var existing = await _storeRepository.FindHolidayByIdAsync(request.HolidayId, tenantId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (existing.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 删除
|
||||
await _storeRepository.DeleteHolidayAsync(request.HolidayId, tenantId, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("删除节假日 {HolidayId} 对应门店 {StoreId}", request.HolidayId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除自提档期处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStorePickupSlotCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStorePickupSlotCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStorePickupSlotCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStorePickupSlotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除档期
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
await storeRepository.DeletePickupSlotAsync(request.SlotId, tenantId, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("删除自提档期 {SlotId}", request.SlotId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店资质处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreQualificationCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreQualificationCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreQualificationCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreQualificationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 审核中门店禁止删除资质
|
||||
if (store.AuditStatus == StoreAuditStatus.Pending)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店审核中,无法删除资质");
|
||||
}
|
||||
|
||||
// 3. (空行后) 获取资质记录
|
||||
var qualification = await storeRepository.FindQualificationByIdAsync(request.QualificationId, tenantId, cancellationToken);
|
||||
if (qualification is null || qualification.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. (空行后) 已激活的外部门店必须保留必要资质
|
||||
if (store.OwnershipType == StoreOwnershipType.DifferentEntity
|
||||
&& store.AuditStatus == StoreAuditStatus.Activated
|
||||
&& IsLicenseType(qualification.QualificationType))
|
||||
{
|
||||
var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var remainingValid = qualifications
|
||||
.Where(item => IsLicenseType(item.QualificationType))
|
||||
.Where(item => item.Id != qualification.Id && !item.IsExpired)
|
||||
.ToList();
|
||||
|
||||
if (remainingValid.Count == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "不能删除必要资质");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. (空行后) 执行删除
|
||||
await storeRepository.DeleteQualificationAsync(request.QualificationId, tenantId, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("删除门店资质 {QualificationId} 对应门店 {StoreId}", qualification.Id, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsLicenseType(StoreQualificationType type)
|
||||
=> type is StoreQualificationType.BusinessLicense or StoreQualificationType.FoodServiceLicense;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店员工处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreStaffCommandHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreStaffCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreStaffCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreStaffCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken);
|
||||
if (staff is null || staff.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 逻辑删除未定义,直接物理删除
|
||||
await merchantRepository.DeleteStaffAsync(staff.Id, tenantId, cancellationToken);
|
||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("删除门店员工 {StaffId} 门店 {StoreId}", request.StaffId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除桌台区域处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreTableAreaCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreTableAreaCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreTableAreaCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreTableAreaCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取区域
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId, tenantId, cancellationToken);
|
||||
if (area is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (area.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 校验区域下无桌码
|
||||
var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var hasTable = tables.Any(x => x.AreaId == request.AreaId);
|
||||
if (hasTable)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "区域下仍有桌码,无法删除");
|
||||
}
|
||||
|
||||
// 4. 删除
|
||||
await storeRepository.DeleteTableAreaAsync(request.AreaId, tenantId, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("删除桌台区域 {AreaId} 对应门店 {StoreId}", request.AreaId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除桌码处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreTableCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DeleteStoreTableCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreTableCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteStoreTableCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取桌码
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var table = await storeRepository.FindTableByIdAsync(request.TableId, tenantId, cancellationToken);
|
||||
if (table is null || table.StoreId != request.StoreId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 删除
|
||||
await storeRepository.DeleteTableAsync(request.TableId, tenantId, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("删除桌码 {TableId} 对应门店 {StoreId}", request.TableId, request.StoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QRCoder;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 导出桌码二维码处理器。
|
||||
/// </summary>
|
||||
public sealed class ExportStoreTableQRCodesQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<ExportStoreTableQRCodesQueryHandler> logger)
|
||||
: IRequestHandler<ExportStoreTableQRCodesQuery, StoreTableExportResult?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreTableExportResult?> Handle(ExportStoreTableQRCodesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 获取桌码列表
|
||||
var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (request.AreaId.HasValue)
|
||||
{
|
||||
tables = tables.Where(x => x.AreaId == request.AreaId.Value).ToList();
|
||||
}
|
||||
|
||||
if (tables.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 生成 ZIP
|
||||
var template = string.IsNullOrWhiteSpace(request.QrContentTemplate) ? "{code}" : request.QrContentTemplate!;
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true, Encoding.UTF8))
|
||||
{
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var content = BuildPayload(template, table.TableCode);
|
||||
var svg = RenderSvg(content);
|
||||
var entry = archive.CreateEntry($"{table.TableCode}.svg", CompressionLevel.Fastest);
|
||||
using var entryStream = entry.Open();
|
||||
using var writer = new StreamWriter(entryStream, Encoding.UTF8);
|
||||
writer.Write(svg);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 返回导出结果
|
||||
var fileName = $"store_{request.StoreId}_tables_{DateTime.UtcNow:yyyyMMddHHmmss}.zip";
|
||||
logger.LogInformation("导出门店 {StoreId} 桌码二维码 {Count} 个", request.StoreId, tables.Count);
|
||||
return new StoreTableExportResult
|
||||
{
|
||||
FileName = fileName,
|
||||
ContentType = "application/zip",
|
||||
Content = memoryStream.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildPayload(string template, string tableCode)
|
||||
{
|
||||
var payload = template.Replace("{code}", tableCode, StringComparison.OrdinalIgnoreCase);
|
||||
return string.IsNullOrWhiteSpace(payload) ? tableCode : payload;
|
||||
}
|
||||
|
||||
private static string RenderSvg(string payload)
|
||||
{
|
||||
using var generator = new QRCodeGenerator();
|
||||
var data = generator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.Q);
|
||||
var svg = new SvgQRCode(data);
|
||||
return svg.GetGraphic(5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 批量生成桌码处理器。
|
||||
/// </summary>
|
||||
public sealed class GenerateStoreTablesCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<GenerateStoreTablesCommandHandler> logger)
|
||||
: IRequestHandler<GenerateStoreTablesCommand, IReadOnlyList<StoreTableDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreTableDto>> Handle(GenerateStoreTablesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 校验区域归属
|
||||
if (request.AreaId.HasValue)
|
||||
{
|
||||
var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId.Value, tenantId, cancellationToken);
|
||||
if (area is null || area.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "桌台区域不存在或不属于该门店");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 校验桌码唯一性
|
||||
var existingTables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var newCodes = Enumerable.Range(request.StartNumber, request.Count)
|
||||
.Select(i => $"{request.TableCodePrefix.Trim()}{i}")
|
||||
.ToList();
|
||||
var conflicts = existingTables.Where(t => newCodes.Contains(t.TableCode, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
if (conflicts.Count > 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "桌码已存在,生成失败");
|
||||
}
|
||||
|
||||
// 4. 构建实体
|
||||
var tables = newCodes.Select(code => new StoreTable
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
AreaId = request.AreaId,
|
||||
TableCode = code,
|
||||
Capacity = request.DefaultCapacity,
|
||||
Tags = request.Tags?.Trim()
|
||||
}).ToList();
|
||||
|
||||
// 5. 持久化
|
||||
await storeRepository.AddTablesAsync(tables, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("批量创建桌码 {Count} 条 对应门店 {StoreId}", tables.Count, request.StoreId);
|
||||
|
||||
// 6. 返回 DTO
|
||||
return tables.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 可用自提档期查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetAvailablePickupSlotsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetAvailablePickupSlotsQuery, IReadOnlyList<StorePickupSlotDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StorePickupSlotDto>> Handle(GetAvailablePickupSlotsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var date = request.Date.Date;
|
||||
// 1. 读取配置
|
||||
var setting = await storeRepository.GetPickupSettingAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var allowDays = setting?.AllowDaysAhead ?? 0;
|
||||
var allowToday = setting?.AllowToday ?? false;
|
||||
var defaultCutoff = setting?.DefaultCutoffMinutes ?? 30;
|
||||
|
||||
// 2. 校验日期范围
|
||||
if (!allowToday && date == DateTime.UtcNow.Date)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (date > DateTime.UtcNow.Date.AddDays(allowDays))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. 读取档期
|
||||
var slots = await storeRepository.GetPickupSlotsAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var weekday = (int)date.DayOfWeek;
|
||||
weekday = weekday == 0 ? 7 : weekday;
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
|
||||
// 4. 过滤可用
|
||||
var available = slots
|
||||
.Where(x => x.IsEnabled && ContainsDay(x.Weekdays, weekday))
|
||||
.Select(slot =>
|
||||
{
|
||||
var cutoff = slot.CutoffMinutes == 0 ? defaultCutoff : slot.CutoffMinutes;
|
||||
var slotStartUtc = date.Add(slot.StartTime);
|
||||
// 判断截单
|
||||
var cutoffTime = slotStartUtc.AddMinutes(-cutoff);
|
||||
var isCutoff = nowUtc > cutoffTime;
|
||||
var remaining = slot.Capacity - slot.ReservedCount;
|
||||
return (slot, isCutoff, remaining);
|
||||
})
|
||||
.Where(x => !x.isCutoff && x.remaining > 0)
|
||||
.Select(x => new StorePickupSlotDto
|
||||
{
|
||||
Id = x.slot.Id,
|
||||
StoreId = x.slot.StoreId,
|
||||
Name = x.slot.Name,
|
||||
StartTime = x.slot.StartTime,
|
||||
EndTime = x.slot.EndTime,
|
||||
CutoffMinutes = x.slot.CutoffMinutes,
|
||||
Capacity = x.slot.Capacity,
|
||||
ReservedCount = x.slot.ReservedCount,
|
||||
Weekdays = x.slot.Weekdays,
|
||||
IsEnabled = x.slot.IsEnabled
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
private static bool ContainsDay(string weekdays, int target)
|
||||
{
|
||||
// 解析适用星期
|
||||
var parts = weekdays.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return parts.Any(p => int.TryParse(p, out var val) && val == target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreByIdQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetStoreByIdQuery, StoreDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreDto?> Handle(GetStoreByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
return store == null ? null : StoreMapping.ToDto(store);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店费用配置处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreFeeQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetStoreFeeQuery, StoreFeeDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreFeeDto?> Handle(GetStoreFeeQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 查询费用配置
|
||||
var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (fee is null)
|
||||
{
|
||||
var fallback = new StoreFee
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
MinimumOrderAmount = 0m,
|
||||
BaseDeliveryFee = 0m,
|
||||
PackagingFeeMode = Domain.Stores.Enums.PackagingFeeMode.Fixed,
|
||||
OrderPackagingFeeMode = Domain.Stores.Enums.OrderPackagingFeeMode.Fixed,
|
||||
FixedPackagingFee = 0m
|
||||
};
|
||||
return StoreMapping.ToDto(fallback);
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return StoreMapping.ToDto(fee);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 获取自提配置处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStorePickupSettingQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetStorePickupSettingQuery, StorePickupSettingDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StorePickupSettingDto?> Handle(GetStorePickupSettingQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var setting = await storeRepository.GetPickupSettingAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (setting is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StorePickupSettingDto
|
||||
{
|
||||
Id = setting.Id,
|
||||
StoreId = setting.StoreId,
|
||||
AllowToday = setting.AllowToday,
|
||||
AllowDaysAhead = setting.AllowDaysAhead,
|
||||
DefaultCutoffMinutes = setting.DefaultCutoffMinutes,
|
||||
MaxQuantityPerOrder = setting.MaxQuantityPerOrder
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 桌码上下文查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreTableContextQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<GetStoreTableContextQueryHandler> logger)
|
||||
: IRequestHandler<GetStoreTableContextQuery, StoreTableContextDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreTableContextDto?> Handle(GetStoreTableContextQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询桌码
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var table = await storeRepository.FindTableByCodeAsync(request.TableCode, tenantId, cancellationToken);
|
||||
if (table is null)
|
||||
{
|
||||
logger.LogWarning("未找到桌码 {TableCode}", request.TableCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 查询门店
|
||||
var store = await storeRepository.FindByIdAsync(table.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 3. 组装上下文
|
||||
return new StoreTableContextDto
|
||||
{
|
||||
StoreId = store.Id,
|
||||
StoreName = store.Name,
|
||||
Announcement = store.Announcement,
|
||||
Tags = store.Tags,
|
||||
TableId = table.Id,
|
||||
TableCode = table.TableCode,
|
||||
AreaId = table.AreaId,
|
||||
Capacity = table.Capacity,
|
||||
TableTags = table.Tags,
|
||||
Status = table.Status
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 资质预警查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListExpiringStoreQualificationsQueryHandler(
|
||||
IDapperExecutor dapperExecutor,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListExpiringStoreQualificationsQuery, StoreQualificationAlertResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreQualificationAlertResultDto> Handle(
|
||||
ListExpiringStoreQualificationsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 规范化参数
|
||||
var page = request.Page <= 0 ? 1 : request.Page;
|
||||
var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
|
||||
var daysThreshold = request.DaysThreshold is null or <= 0 ? 30 : request.DaysThreshold.Value;
|
||||
if (daysThreshold > 365)
|
||||
{
|
||||
daysThreshold = 365;
|
||||
}
|
||||
var offset = (page - 1) * pageSize;
|
||||
var now = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var expiringBefore = now.AddDays(daysThreshold);
|
||||
|
||||
// 2. (空行后) 读取当前租户并校验跨租户
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
|
||||
}
|
||||
|
||||
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询资质预警");
|
||||
}
|
||||
var tenantId = currentTenantId;
|
||||
|
||||
// 3. (空行后) 执行查询
|
||||
return await dapperExecutor.QueryAsync(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
// 3.1 统计汇总
|
||||
var summary = await ExecuteSummaryAsync(connection, now, expiringBefore, tenantId, token);
|
||||
|
||||
// 3.2 (空行后) 统计总数
|
||||
var total = await ExecuteScalarIntAsync(
|
||||
connection,
|
||||
BuildCountSql(),
|
||||
[
|
||||
("tenantId", tenantId),
|
||||
("expiredOnly", request.Expired),
|
||||
("now", now),
|
||||
("expiringBefore", expiringBefore)
|
||||
],
|
||||
token);
|
||||
if (total == 0)
|
||||
{
|
||||
return BuildResult([], page, pageSize, total, summary);
|
||||
}
|
||||
|
||||
// 3.3 (空行后) 查询列表
|
||||
await using var listCommand = CreateCommand(
|
||||
connection,
|
||||
BuildListSql(),
|
||||
[
|
||||
("tenantId", tenantId),
|
||||
("expiredOnly", request.Expired),
|
||||
("now", now),
|
||||
("expiringBefore", expiringBefore),
|
||||
("offset", offset),
|
||||
("limit", pageSize)
|
||||
]);
|
||||
|
||||
await using var reader = await listCommand.ExecuteReaderAsync(token);
|
||||
if (!reader.HasRows)
|
||||
{
|
||||
return BuildResult([], page, pageSize, total, summary);
|
||||
}
|
||||
|
||||
// 3.4 (空行后) 初始化字段序号
|
||||
var qualificationIdOrdinal = reader.GetOrdinal("QualificationId");
|
||||
var storeIdOrdinal = reader.GetOrdinal("StoreId");
|
||||
var storeNameOrdinal = reader.GetOrdinal("StoreName");
|
||||
var storeCodeOrdinal = reader.GetOrdinal("StoreCode");
|
||||
var tenantIdOrdinal = reader.GetOrdinal("TenantId");
|
||||
var tenantNameOrdinal = reader.GetOrdinal("TenantName");
|
||||
var typeOrdinal = reader.GetOrdinal("QualificationType");
|
||||
var expiresAtOrdinal = reader.GetOrdinal("ExpiresAt");
|
||||
var businessStatusOrdinal = reader.GetOrdinal("BusinessStatus");
|
||||
|
||||
// 3.5 (空行后) 读取并映射
|
||||
List<StoreQualificationAlertDto> items = [];
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
DateOnly? expiresAt = reader.IsDBNull(expiresAtOrdinal)
|
||||
? null
|
||||
: DateOnly.FromDateTime(reader.GetDateTime(expiresAtOrdinal));
|
||||
var isExpired = expiresAt.HasValue && expiresAt.Value < now;
|
||||
int? daysUntilExpiry = expiresAt.HasValue
|
||||
? expiresAt.Value.DayNumber - now.DayNumber
|
||||
: null;
|
||||
|
||||
items.Add(new StoreQualificationAlertDto
|
||||
{
|
||||
QualificationId = reader.GetInt64(qualificationIdOrdinal),
|
||||
StoreId = reader.GetInt64(storeIdOrdinal),
|
||||
StoreName = reader.GetString(storeNameOrdinal),
|
||||
StoreCode = reader.GetString(storeCodeOrdinal),
|
||||
TenantId = reader.GetInt64(tenantIdOrdinal),
|
||||
TenantName = reader.GetString(tenantNameOrdinal),
|
||||
QualificationType = (StoreQualificationType)reader.GetInt32(typeOrdinal),
|
||||
ExpiresAt = expiresAt,
|
||||
DaysUntilExpiry = daysUntilExpiry,
|
||||
IsExpired = isExpired,
|
||||
StoreBusinessStatus = (StoreBusinessStatus)reader.GetInt32(businessStatusOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
// 3.6 (空行后) 组装结果
|
||||
return BuildResult(items, page, pageSize, total, summary);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static StoreQualificationAlertResultDto BuildResult(
|
||||
IReadOnlyList<StoreQualificationAlertDto> items,
|
||||
int page,
|
||||
int pageSize,
|
||||
int totalCount,
|
||||
StoreQualificationAlertSummaryDto summary)
|
||||
{
|
||||
// 1. 计算总页数
|
||||
var totalPages = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
|
||||
// 2. (空行后) 组装分页结果
|
||||
return new StoreQualificationAlertResultDto
|
||||
{
|
||||
Items = items,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages,
|
||||
Summary = summary
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<StoreQualificationAlertSummaryDto> ExecuteSummaryAsync(
|
||||
IDbConnection connection,
|
||||
DateOnly now,
|
||||
DateOnly expiringBefore,
|
||||
long tenantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildSummarySql(),
|
||||
[
|
||||
("tenantId", tenantId),
|
||||
("now", now),
|
||||
("expiringBefore", expiringBefore)
|
||||
]);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!reader.HasRows || !await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return new StoreQualificationAlertSummaryDto();
|
||||
}
|
||||
|
||||
// 1. (空行后) 读取统计结果
|
||||
var expiringSoonOrdinal = reader.GetOrdinal("ExpiringSoonCount");
|
||||
var expiredOrdinal = reader.GetOrdinal("ExpiredCount");
|
||||
|
||||
return new StoreQualificationAlertSummaryDto
|
||||
{
|
||||
ExpiringSoonCount = reader.IsDBNull(expiringSoonOrdinal) ? 0 : reader.GetInt32(expiringSoonOrdinal),
|
||||
ExpiredCount = reader.IsDBNull(expiredOrdinal) ? 0 : reader.GetInt32(expiredOrdinal)
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildCountSql()
|
||||
{
|
||||
return """
|
||||
select count(*)
|
||||
from public.store_qualifications q
|
||||
join public.stores s on s."Id" = q."StoreId" and s."DeletedAt" is null
|
||||
join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
|
||||
where q."DeletedAt" is null
|
||||
and q."ExpiresAt" is not null
|
||||
and s."TenantId" = @tenantId
|
||||
and (
|
||||
(@expiredOnly::boolean = true and q."ExpiresAt" < @now)
|
||||
or (@expiredOnly::boolean = false and q."ExpiresAt" <= @expiringBefore)
|
||||
);
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildListSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
q."Id" as "QualificationId",
|
||||
q."StoreId",
|
||||
s."Name" as "StoreName",
|
||||
s."Code" as "StoreCode",
|
||||
s."TenantId",
|
||||
t."Name" as "TenantName",
|
||||
q."QualificationType",
|
||||
q."ExpiresAt",
|
||||
s."BusinessStatus"
|
||||
from public.store_qualifications q
|
||||
join public.stores s on s."Id" = q."StoreId" and s."DeletedAt" is null
|
||||
join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
|
||||
where q."DeletedAt" is null
|
||||
and q."ExpiresAt" is not null
|
||||
and s."TenantId" = @tenantId
|
||||
and (
|
||||
(@expiredOnly::boolean = true and q."ExpiresAt" < @now)
|
||||
or (@expiredOnly::boolean = false and q."ExpiresAt" <= @expiringBefore)
|
||||
)
|
||||
order by q."ExpiresAt" asc, q."Id" asc
|
||||
offset @offset
|
||||
limit @limit;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildSummarySql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
coalesce(sum(case when q."ExpiresAt" < @now then 1 else 0 end), 0) as "ExpiredCount",
|
||||
coalesce(sum(case when q."ExpiresAt" >= @now and q."ExpiresAt" <= @expiringBefore then 1 else 0 end), 0) as "ExpiringSoonCount"
|
||||
from public.store_qualifications q
|
||||
join public.stores s on s."Id" = q."StoreId" and s."DeletedAt" is null
|
||||
join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
|
||||
where q."DeletedAt" is null
|
||||
and q."ExpiresAt" is not null
|
||||
and s."TenantId" = @tenantId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteScalarIntAsync(
|
||||
IDbConnection connection,
|
||||
string sql,
|
||||
(string Name, object? Value)[] parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = CreateCommand(connection, sql, parameters);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken);
|
||||
return result is null or DBNull ? 0 : Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(
|
||||
IDbConnection connection,
|
||||
string sql,
|
||||
(string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = name;
|
||||
parameter.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(parameter);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 营业时段列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreBusinessHoursQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreBusinessHoursQuery, IReadOnlyList<StoreBusinessHourDto>>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreBusinessHourDto>> Handle(ListStoreBusinessHoursQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询时段列表
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var hours = await _storeRepository.GetBusinessHoursAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
// 2. 映射 DTO
|
||||
return hours.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 配送区域列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreDeliveryZonesQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreDeliveryZonesQuery, IReadOnlyList<StoreDeliveryZoneDto>>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreDeliveryZoneDto>> Handle(ListStoreDeliveryZonesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询配送区域
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var zones = await _storeRepository.GetDeliveryZonesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
// 2. 映射 DTO
|
||||
return zones.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 排班列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreEmployeeShiftsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreEmployeeShiftsQuery, IReadOnlyList<StoreEmployeeShiftDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreEmployeeShiftDto>> Handle(ListStoreEmployeeShiftsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 时间范围
|
||||
var from = request.From ?? DateTime.UtcNow.Date;
|
||||
var to = request.To ?? from.AddDays(7);
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
// 2. 查询排班
|
||||
var shifts = await storeRepository.GetShiftsAsync(request.StoreId, tenantId, from, to, cancellationToken);
|
||||
|
||||
if (request.StaffId.HasValue)
|
||||
{
|
||||
shifts = shifts.Where(x => x.StaffId == request.StaffId.Value).ToList();
|
||||
}
|
||||
|
||||
// 3. 映射 DTO
|
||||
return shifts.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店节假日列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreHolidaysQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreHolidaysQuery, IReadOnlyList<StoreHolidayDto>>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreHolidayDto>> Handle(ListStoreHolidaysQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询节假日
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var holidays = await _storeRepository.GetHolidaysAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
// 2. 映射 DTO
|
||||
return holidays.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 自提档期列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStorePickupSlotsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStorePickupSlotsQuery, IReadOnlyList<StorePickupSlotDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StorePickupSlotDto>> Handle(ListStorePickupSlotsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var slots = await storeRepository.GetPickupSlotsAsync(request.StoreId, tenantId, cancellationToken);
|
||||
return slots
|
||||
.Select(x => new StorePickupSlotDto
|
||||
{
|
||||
Id = x.Id,
|
||||
StoreId = x.StoreId,
|
||||
Name = x.Name,
|
||||
StartTime = x.StartTime,
|
||||
EndTime = x.EndTime,
|
||||
CutoffMinutes = x.CutoffMinutes,
|
||||
Capacity = x.Capacity,
|
||||
ReservedCount = x.ReservedCount,
|
||||
Weekdays = x.Weekdays,
|
||||
IsEnabled = x.IsEnabled
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店资质列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreQualificationsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreQualificationsQuery, IReadOnlyList<StoreQualificationDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreQualificationDto>> Handle(ListStoreQualificationsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 读取资质列表
|
||||
var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
// 3. (空行后) 映射 DTO
|
||||
return qualifications.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店员工列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreStaffQueryHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreStaffQuery, IReadOnlyList<StoreStaffDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreStaffDto>> Handle(ListStoreStaffQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. 查询员工
|
||||
var staffs = await merchantRepository.GetStaffByStoreAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
if (request.RoleType.HasValue)
|
||||
{
|
||||
staffs = staffs.Where(x => x.RoleType == request.RoleType.Value).ToList();
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
staffs = staffs.Where(x => x.Status == request.Status.Value).ToList();
|
||||
}
|
||||
|
||||
// 3. 映射 DTO
|
||||
return staffs.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 桌台区域列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreTableAreasQueryHandler(IStoreRepository storeRepository, ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreTableAreasQuery, IReadOnlyList<StoreTableAreaDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreTableAreaDto>> Handle(ListStoreTableAreasQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询区域列表
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var areas = await storeRepository.GetTableAreasAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
// 2. 映射 DTO
|
||||
return areas.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 桌码列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreTablesQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ListStoreTablesQuery, IReadOnlyList<StoreTableDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreTableDto>> Handle(ListStoreTablesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询桌码列表
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
|
||||
// 2. 过滤
|
||||
if (request.AreaId.HasValue)
|
||||
{
|
||||
tables = tables.Where(x => x.AreaId == request.AreaId.Value).ToList();
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
tables = tables.Where(x => x.Status == request.Status.Value).ToList();
|
||||
}
|
||||
|
||||
// 3. 映射 DTO
|
||||
return tables.Select(StoreMapping.ToDto).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 提交门店审核处理器。
|
||||
/// </summary>
|
||||
public sealed class SubmitStoreAuditCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IMediator mediator,
|
||||
ILogger<SubmitStoreAuditCommandHandler> logger)
|
||||
: IRequestHandler<SubmitStoreAuditCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(SubmitStoreAuditCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
if (store.AuditStatus is not StoreAuditStatus.Draft and not StoreAuditStatus.Rejected)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店不处于可提交状态");
|
||||
}
|
||||
|
||||
// 2. (空行后) 处理同主体门店直接激活
|
||||
var now = DateTime.UtcNow;
|
||||
if (store.OwnershipType == StoreOwnershipType.SameEntity)
|
||||
{
|
||||
var previousStatus = store.AuditStatus;
|
||||
store.AuditStatus = StoreAuditStatus.Activated;
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.SubmittedAt ??= now;
|
||||
store.ActivatedAt ??= now;
|
||||
store.RejectionReason = null;
|
||||
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
|
||||
{
|
||||
StoreId = store.Id,
|
||||
Action = StoreAuditAction.AutoActivate,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = store.AuditStatus,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
OperatorName = ResolveOperatorName(),
|
||||
Remarks = "同主体门店自动激活"
|
||||
}, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("门店 {StoreId} 同主体自动激活", store.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. (空行后) 校验资质完整性
|
||||
var checkResult = await mediator.Send(new CheckStoreQualificationsQuery { StoreId = request.StoreId }, cancellationToken);
|
||||
if (!checkResult.CanSubmitAudit)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "门店资质不完整,无法提交审核");
|
||||
}
|
||||
|
||||
// 4. (空行后) 更新审核状态
|
||||
var action = store.AuditStatus == StoreAuditStatus.Rejected
|
||||
? StoreAuditAction.Resubmit
|
||||
: StoreAuditAction.Submit;
|
||||
var previous = store.AuditStatus;
|
||||
store.AuditStatus = StoreAuditStatus.Pending;
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.SubmittedAt = now;
|
||||
store.RejectionReason = null;
|
||||
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
|
||||
{
|
||||
StoreId = store.Id,
|
||||
Action = action,
|
||||
PreviousStatus = previous,
|
||||
NewStatus = store.AuditStatus,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
OperatorName = ResolveOperatorName(),
|
||||
Remarks = action == StoreAuditAction.Resubmit ? "门店重新提交审核" : "门店提交审核"
|
||||
}, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("门店 {StoreId} 提交审核", store.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private long? ResolveOperatorId()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? null : id;
|
||||
}
|
||||
|
||||
private string ResolveOperatorName()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? "system" : $"user:{id}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 切换门店经营状态处理器。
|
||||
/// </summary>
|
||||
public sealed class ToggleBusinessStatusCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<ToggleBusinessStatusCommandHandler> logger)
|
||||
: IRequestHandler<ToggleBusinessStatusCommand, StoreDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreDto> Handle(ToggleBusinessStatusCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
if (store.AuditStatus != StoreAuditStatus.Activated)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法切换状态");
|
||||
}
|
||||
|
||||
if (store.BusinessStatus == StoreBusinessStatus.ForceClosed)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店已被风控强制关闭,无法切换");
|
||||
}
|
||||
|
||||
// 2. (空行后) 应用状态变更
|
||||
if (request.BusinessStatus == StoreBusinessStatus.Resting)
|
||||
{
|
||||
if (!request.ClosureReason.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "切换休息中必须选择歇业原因");
|
||||
}
|
||||
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.ClosureReason = request.ClosureReason;
|
||||
store.ClosureReasonText = request.ClosureReasonText?.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
store.BusinessStatus = StoreBusinessStatus.Open;
|
||||
store.ClosureReason = null;
|
||||
store.ClosureReasonText = null;
|
||||
}
|
||||
|
||||
// 3. (空行后) 保存并返回
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("切换门店 {StoreId} 状态至 {BusinessStatus}", store.Id, store.BusinessStatus);
|
||||
|
||||
return StoreMapping.ToDto(store);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新营业时段处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreBusinessHourCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreBusinessHourCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreBusinessHourCommand, StoreBusinessHourDto?>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<UpdateStoreBusinessHourCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreBusinessHourDto?> Handle(UpdateStoreBusinessHourCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取时段
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var existing = await _storeRepository.FindBusinessHourByIdAsync(request.BusinessHourId, tenantId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (existing.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "营业时段不属于该门店");
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
existing.DayOfWeek = request.DayOfWeek;
|
||||
existing.HourType = request.HourType;
|
||||
existing.StartTime = request.StartTime;
|
||||
existing.EndTime = request.EndTime;
|
||||
existing.CapacityLimit = request.CapacityLimit;
|
||||
existing.Notes = request.Notes?.Trim();
|
||||
|
||||
// 4. 持久化
|
||||
await _storeRepository.UpdateBusinessHourAsync(existing, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("更新营业时段 {BusinessHourId} 对应门店 {StoreId}", existing.Id, existing.StoreId);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return StoreMapping.ToDto(existing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新配送区域处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreDeliveryZoneCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IGeoJsonValidationService geoJsonValidationService,
|
||||
ILogger<UpdateStoreDeliveryZoneCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreDeliveryZoneCommand, StoreDeliveryZoneDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreDeliveryZoneDto?> Handle(UpdateStoreDeliveryZoneCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取区域
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var existing = await storeRepository.FindDeliveryZoneByIdAsync(request.DeliveryZoneId, tenantId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (existing.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "配送区域不属于该门店");
|
||||
}
|
||||
|
||||
// 3. (空行后) 校验 GeoJSON
|
||||
var validation = geoJsonValidationService.ValidatePolygon(request.PolygonGeoJson);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, validation.ErrorMessage ?? "配送范围格式错误");
|
||||
}
|
||||
|
||||
// 4. (空行后) 更新字段
|
||||
existing.ZoneName = request.ZoneName.Trim();
|
||||
existing.PolygonGeoJson = (validation.NormalizedGeoJson ?? request.PolygonGeoJson).Trim();
|
||||
existing.MinimumOrderAmount = request.MinimumOrderAmount;
|
||||
existing.DeliveryFee = request.DeliveryFee;
|
||||
existing.EstimatedMinutes = request.EstimatedMinutes;
|
||||
existing.SortOrder = request.SortOrder;
|
||||
|
||||
// 5. (空行后) 持久化
|
||||
await storeRepository.UpdateDeliveryZoneAsync(existing, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新配送区域 {DeliveryZoneId} 对应门店 {StoreId}", existing.Id, existing.StoreId);
|
||||
|
||||
// 6. (空行后) 返回 DTO
|
||||
return StoreMapping.ToDto(existing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新排班处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreEmployeeShiftCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
IMerchantRepository merchantRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreEmployeeShiftCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreEmployeeShiftCommand, StoreEmployeeShiftDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreEmployeeShiftDto?> Handle(UpdateStoreEmployeeShiftCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取排班
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var shift = await storeRepository.FindShiftByIdAsync(request.ShiftId, tenantId, cancellationToken);
|
||||
if (shift is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (shift.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "排班不属于该门店");
|
||||
}
|
||||
|
||||
// 3. 校验员工归属
|
||||
var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken);
|
||||
if (staff is null || (staff.StoreId.HasValue && staff.StoreId != request.StoreId))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "员工不存在或不属于该门店");
|
||||
}
|
||||
|
||||
// 4. 冲突校验
|
||||
var shifts = await storeRepository.GetShiftsAsync(request.StoreId, tenantId, request.ShiftDate.Date, request.ShiftDate.Date, cancellationToken);
|
||||
var hasConflict = shifts.Any(x => x.Id != request.ShiftId && x.StaffId == request.StaffId && x.ShiftDate == request.ShiftDate);
|
||||
if (hasConflict)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "该员工当日已存在排班");
|
||||
}
|
||||
|
||||
// 5. 更新字段
|
||||
shift.StaffId = request.StaffId;
|
||||
shift.ShiftDate = request.ShiftDate.Date;
|
||||
shift.StartTime = request.StartTime;
|
||||
shift.EndTime = request.EndTime;
|
||||
shift.RoleType = request.RoleType;
|
||||
shift.Notes = request.Notes?.Trim();
|
||||
|
||||
// 6. 持久化
|
||||
await storeRepository.UpdateShiftAsync(shift, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新排班 {ShiftId} 员工 {StaffId} 门店 {StoreId}", shift.Id, shift.StaffId, shift.StoreId);
|
||||
|
||||
// 7. 返回 DTO
|
||||
return StoreMapping.ToDto(shift);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店费用配置处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreFeeCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreFeeCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreFeeCommand, StoreFeeDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreFeeDto> Handle(UpdateStoreFeeCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店状态
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
var storeTenantId = store.TenantId;
|
||||
if (store.AuditStatus != StoreAuditStatus.Activated)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法配置费用");
|
||||
}
|
||||
if (store.BusinessStatus == StoreBusinessStatus.ForceClosed)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店已被强制关闭,无法配置费用");
|
||||
}
|
||||
|
||||
// 2. (空行后) 获取或创建费用配置
|
||||
var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var isNew = fee is null;
|
||||
fee ??= new StoreFee
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
TenantId = storeTenantId
|
||||
};
|
||||
|
||||
// 3. (空行后) 应用更新字段
|
||||
fee.MinimumOrderAmount = request.MinimumOrderAmount;
|
||||
fee.BaseDeliveryFee = request.DeliveryFee;
|
||||
fee.PackagingFeeMode = request.PackagingFeeMode;
|
||||
fee.OrderPackagingFeeMode = request.PackagingFeeMode == PackagingFeeMode.Fixed
|
||||
? request.OrderPackagingFeeMode
|
||||
: OrderPackagingFeeMode.Fixed;
|
||||
if (request.PackagingFeeMode == PackagingFeeMode.Fixed && request.OrderPackagingFeeMode == OrderPackagingFeeMode.Tiered)
|
||||
{
|
||||
var normalizedTiers = StoreFeeTierHelper.Normalize(request.PackagingFeeTiers);
|
||||
fee.FixedPackagingFee = 0m;
|
||||
fee.PackagingFeeTiersJson = StoreFeeTierHelper.Serialize(normalizedTiers);
|
||||
}
|
||||
else
|
||||
{
|
||||
fee.FixedPackagingFee = request.PackagingFeeMode == PackagingFeeMode.Fixed
|
||||
? request.FixedPackagingFee ?? 0m
|
||||
: 0m;
|
||||
fee.PackagingFeeTiersJson = null;
|
||||
}
|
||||
fee.FreeDeliveryThreshold = request.FreeDeliveryThreshold;
|
||||
|
||||
// 4. (空行后) 保存并返回
|
||||
if (isNew)
|
||||
{
|
||||
await storeRepository.AddStoreFeeAsync(fee, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await storeRepository.UpdateStoreFeeAsync(fee, cancellationToken);
|
||||
}
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新门店 {StoreId} 费用配置", request.StoreId);
|
||||
return StoreMapping.ToDto(fee);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新节假日配置处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreHolidayCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreHolidayCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreHolidayCommand, StoreHolidayDto?>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly ILogger<UpdateStoreHolidayCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreHolidayDto?> Handle(UpdateStoreHolidayCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取配置
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var existing = await _storeRepository.FindHolidayByIdAsync(request.HolidayId, tenantId, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (existing.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "节假日配置不属于该门店");
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
existing.Date = NormalizeToUtc(request.Date);
|
||||
existing.EndDate = request.EndDate.HasValue ? NormalizeToUtc(request.EndDate.Value) : null;
|
||||
existing.IsAllDay = request.IsAllDay;
|
||||
existing.StartTime = request.StartTime;
|
||||
existing.EndTime = request.EndTime;
|
||||
existing.OverrideType = request.OverrideType;
|
||||
existing.IsClosed = request.OverrideType == OverrideType.Closed;
|
||||
existing.Reason = request.Reason?.Trim();
|
||||
|
||||
// 4. 持久化
|
||||
await _storeRepository.UpdateHolidayAsync(existing, cancellationToken);
|
||||
await _storeRepository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("更新节假日 {HolidayId} 对应门店 {StoreId}", existing.Id, existing.StoreId);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return StoreMapping.ToDto(existing);
|
||||
}
|
||||
|
||||
private static DateTime NormalizeToUtc(DateTime value)
|
||||
{
|
||||
return value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新自提档期处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStorePickupSlotCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStorePickupSlotCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStorePickupSlotCommand, StorePickupSlotDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StorePickupSlotDto?> Handle(UpdateStorePickupSlotCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询档期
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var slot = await storeRepository.FindPickupSlotByIdAsync(request.SlotId, tenantId, cancellationToken);
|
||||
if (slot is null || slot.StoreId != request.StoreId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
slot.Name = request.Name.Trim();
|
||||
slot.StartTime = request.StartTime;
|
||||
slot.EndTime = request.EndTime;
|
||||
slot.CutoffMinutes = request.CutoffMinutes;
|
||||
slot.Capacity = request.Capacity;
|
||||
slot.Weekdays = request.Weekdays;
|
||||
slot.IsEnabled = request.IsEnabled;
|
||||
await storeRepository.UpdatePickupSlotAsync(slot, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新自提档期 {SlotId}", request.SlotId);
|
||||
return new StorePickupSlotDto
|
||||
{
|
||||
Id = slot.Id,
|
||||
StoreId = slot.StoreId,
|
||||
Name = slot.Name,
|
||||
StartTime = slot.StartTime,
|
||||
EndTime = slot.EndTime,
|
||||
CutoffMinutes = slot.CutoffMinutes,
|
||||
Capacity = slot.Capacity,
|
||||
ReservedCount = slot.ReservedCount,
|
||||
Weekdays = slot.Weekdays,
|
||||
IsEnabled = slot.IsEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店资质处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreQualificationCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreQualificationCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreQualificationCommand, StoreQualificationDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreQualificationDto?> Handle(UpdateStoreQualificationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. (空行后) 审核中门店禁止修改资质
|
||||
if (store.AuditStatus == StoreAuditStatus.Pending)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店审核中,无法修改资质");
|
||||
}
|
||||
|
||||
// 3. (空行后) 校验资质记录
|
||||
var qualification = await storeRepository.FindQualificationByIdAsync(request.QualificationId, tenantId, cancellationToken);
|
||||
if (qualification is null || qualification.StoreId != request.StoreId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. (空行后) 更新字段
|
||||
if (!string.IsNullOrWhiteSpace(request.FileUrl))
|
||||
{
|
||||
qualification.FileUrl = request.FileUrl.Trim();
|
||||
}
|
||||
|
||||
if (request.DocumentNumber is not null)
|
||||
{
|
||||
qualification.DocumentNumber = request.DocumentNumber.Trim();
|
||||
}
|
||||
|
||||
if (request.IssuedAt.HasValue)
|
||||
{
|
||||
qualification.IssuedAt = request.IssuedAt;
|
||||
}
|
||||
|
||||
if (request.ExpiresAt.HasValue)
|
||||
{
|
||||
qualification.ExpiresAt = request.ExpiresAt;
|
||||
}
|
||||
|
||||
if (request.SortOrder.HasValue)
|
||||
{
|
||||
qualification.SortOrder = request.SortOrder.Value;
|
||||
}
|
||||
|
||||
// 5. (空行后) 保存变更并返回结果
|
||||
await storeRepository.UpdateQualificationAsync(qualification, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新门店资质 {QualificationId} 对应门店 {StoreId}", qualification.Id, request.StoreId);
|
||||
|
||||
return StoreMapping.ToDto(qualification);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店员工处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreStaffCommandHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreStaffCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreStaffCommand, StoreStaffDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreStaffDto?> Handle(UpdateStoreStaffCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 读取员工
|
||||
var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken);
|
||||
if (staff is null || staff.StoreId != request.StoreId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
staff.Name = request.Name.Trim();
|
||||
staff.Phone = request.Phone.Trim();
|
||||
staff.Email = request.Email?.Trim();
|
||||
staff.RoleType = request.RoleType;
|
||||
staff.Status = request.Status;
|
||||
|
||||
// 4. 持久化
|
||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新门店员工 {StaffId} 门店 {StoreId}", staff.Id, staff.StoreId);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return StoreMapping.ToDto(staff);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新桌台区域处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreTableAreaCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreTableAreaCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreTableAreaCommand, StoreTableAreaDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreTableAreaDto?> Handle(UpdateStoreTableAreaCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取区域
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId, tenantId, cancellationToken);
|
||||
if (area is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (area.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "区域不属于该门店");
|
||||
}
|
||||
|
||||
// 3. 名称唯一校验
|
||||
var areas = await storeRepository.GetTableAreasAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var hasDuplicate = areas.Any(x => x.Id != request.AreaId && x.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (hasDuplicate)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "区域名称已存在");
|
||||
}
|
||||
|
||||
// 4. 更新字段
|
||||
area.Name = request.Name.Trim();
|
||||
area.Description = request.Description?.Trim();
|
||||
area.SortOrder = request.SortOrder;
|
||||
|
||||
// 5. 持久化
|
||||
await storeRepository.UpdateTableAreaAsync(area, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新桌台区域 {AreaId} 对应门店 {StoreId}", area.Id, area.StoreId);
|
||||
|
||||
// 6. 返回 DTO
|
||||
return StoreMapping.ToDto(area);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 更新桌码处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreTableCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpdateStoreTableCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreTableCommand, StoreTableDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreTableDto?> Handle(UpdateStoreTableCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取桌码
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var table = await storeRepository.FindTableByIdAsync(request.TableId, tenantId, cancellationToken);
|
||||
if (table is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 校验门店归属
|
||||
if (table.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "桌码不属于该门店");
|
||||
}
|
||||
|
||||
// 3. 校验区域归属
|
||||
if (request.AreaId.HasValue)
|
||||
{
|
||||
var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId.Value, tenantId, cancellationToken);
|
||||
if (area is null || area.StoreId != request.StoreId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "桌台区域不存在或不属于该门店");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 校验桌码唯一
|
||||
var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken);
|
||||
var exists = tables.Any(x => x.Id != request.TableId && x.TableCode.Equals(request.TableCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (exists)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "桌码已存在");
|
||||
}
|
||||
|
||||
// 5. 更新字段
|
||||
table.AreaId = request.AreaId;
|
||||
table.TableCode = request.TableCode.Trim();
|
||||
table.Capacity = request.Capacity;
|
||||
table.Tags = request.Tags?.Trim();
|
||||
table.Status = request.Status;
|
||||
|
||||
// 6. 持久化
|
||||
await storeRepository.UpdateTableAsync(table, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新桌码 {TableId} 对应门店 {StoreId}", table.Id, table.StoreId);
|
||||
|
||||
// 7. 返回 DTO
|
||||
return StoreMapping.ToDto(table);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 自提配置维护处理器。
|
||||
/// </summary>
|
||||
public sealed class UpsertStorePickupSettingCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<UpsertStorePickupSettingCommandHandler> logger)
|
||||
: IRequestHandler<UpsertStorePickupSettingCommand, StorePickupSettingDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StorePickupSettingDto> Handle(UpsertStorePickupSettingCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验门店存在
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 2. 读取或创建配置
|
||||
var setting = await storeRepository.GetPickupSettingAsync(request.StoreId, tenantId, cancellationToken);
|
||||
if (setting is null)
|
||||
{
|
||||
setting = new StorePickupSetting
|
||||
{
|
||||
TenantId = tenantId,
|
||||
StoreId = request.StoreId
|
||||
};
|
||||
await storeRepository.AddPickupSettingAsync(setting, cancellationToken);
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
setting.AllowToday = request.AllowToday;
|
||||
setting.AllowDaysAhead = request.AllowDaysAhead;
|
||||
setting.DefaultCutoffMinutes = request.DefaultCutoffMinutes;
|
||||
setting.MaxQuantityPerOrder = request.MaxQuantityPerOrder;
|
||||
await storeRepository.UpdatePickupSettingAsync(setting, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("更新门店 {StoreId} 自提配置", request.StoreId);
|
||||
return new StorePickupSettingDto
|
||||
{
|
||||
Id = setting.Id,
|
||||
StoreId = setting.StoreId,
|
||||
AllowToday = setting.AllowToday,
|
||||
AllowDaysAhead = setting.AllowDaysAhead,
|
||||
DefaultCutoffMinutes = setting.DefaultCutoffMinutes,
|
||||
MaxQuantityPerOrder = setting.MaxQuantityPerOrder
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 费用预览计算查询。
|
||||
/// </summary>
|
||||
public sealed record CalculateStoreFeeQuery : IRequest<StoreFeeCalculationResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品金额。
|
||||
/// </summary>
|
||||
public decimal OrderAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品种类数量。
|
||||
/// </summary>
|
||||
public int? ItemCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoreFeeCalculationItemDto> Items { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 配送范围检测查询。
|
||||
/// </summary>
|
||||
public sealed record CheckStoreDeliveryZoneQuery : IRequest<StoreDeliveryCheckResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经度。
|
||||
/// </summary>
|
||||
public double Longitude { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 纬度。
|
||||
/// </summary>
|
||||
public double Latitude { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 检查门店资质完整性查询。
|
||||
/// </summary>
|
||||
public sealed record CheckStoreQualificationsQuery : IRequest<StoreQualificationCheckResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 导出桌码二维码查询。
|
||||
/// </summary>
|
||||
public sealed record ExportStoreTableQRCodesQuery : IRequest<StoreTableExportResult?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区域筛选。
|
||||
/// </summary>
|
||||
public long? AreaId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容模板,使用 {code} 占位。
|
||||
/// </summary>
|
||||
public string? QrContentTemplate { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用自提档期查询。
|
||||
/// </summary>
|
||||
public sealed record GetAvailablePickupSlotsQuery : IRequest<IReadOnlyList<StorePickupSlotDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标日期(本地日期部分)。
|
||||
/// </summary>
|
||||
public DateTime Date { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店详情查询。
|
||||
/// </summary>
|
||||
public sealed class GetStoreByIdQuery : IRequest<StoreDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店费用配置查询。
|
||||
/// </summary>
|
||||
public sealed record GetStoreFeeQuery : IRequest<StoreFeeDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店自提配置查询。
|
||||
/// </summary>
|
||||
public sealed record GetStorePickupSettingQuery : IRequest<StorePickupSettingDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 桌码上下文查询。
|
||||
/// </summary>
|
||||
public sealed record GetStoreTableContextQuery : IRequest<StoreTableContextDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 桌码。
|
||||
/// </summary>
|
||||
public string TableCode { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 资质预警分页查询。
|
||||
/// </summary>
|
||||
public sealed record ListExpiringStoreQualificationsQuery : IRequest<StoreQualificationAlertResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 过期阈值天数(默认 30 天)。
|
||||
/// </summary>
|
||||
public int? DaysThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(可选,默认当前租户;禁止跨租户)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅显示已过期。
|
||||
/// </summary>
|
||||
public bool Expired { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 营业时段列表查询。
|
||||
/// </summary>
|
||||
public sealed record ListStoreBusinessHoursQuery : IRequest<IReadOnlyList<StoreBusinessHourDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 配送区域列表查询。
|
||||
/// </summary>
|
||||
public sealed record ListStoreDeliveryZonesQuery : IRequest<IReadOnlyList<StoreDeliveryZoneDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 员工排班列表查询(支持日期区间)。
|
||||
/// </summary>
|
||||
public sealed record ListStoreEmployeeShiftsQuery : IRequest<IReadOnlyList<StoreEmployeeShiftDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期(含),默认今日。
|
||||
/// </summary>
|
||||
public DateTime? From { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(含),默认今日+7。
|
||||
/// </summary>
|
||||
public DateTime? To { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选员工筛选。
|
||||
/// </summary>
|
||||
public long? StaffId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 门店节假日列表查询。
|
||||
/// </summary>
|
||||
public sealed record ListStoreHolidaysQuery : IRequest<IReadOnlyList<StoreHolidayDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 门店档期列表查询。
|
||||
/// </summary>
|
||||
public sealed record ListStorePickupSlotsQuery : IRequest<IReadOnlyList<StorePickupSlotDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询门店资质列表。
|
||||
/// </summary>
|
||||
public sealed record ListStoreQualificationsQuery : IRequest<IReadOnlyList<StoreQualificationDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 门店员工列表查询。
|
||||
/// </summary>
|
||||
public sealed record ListStoreStaffQuery : IRequest<IReadOnlyList<StoreStaffDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色筛选。
|
||||
/// </summary>
|
||||
public StaffRoleType? RoleType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态筛选。
|
||||
/// </summary>
|
||||
public StaffStatus? Status { get; init; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user