diff --git a/Document/12_BusinessTodo.md b/Document/12_BusinessTodo.md index 13a4e25..00d98dd 100644 --- a/Document/12_BusinessTodo.md +++ b/Document/12_BusinessTodo.md @@ -26,8 +26,8 @@ - 已交付:Admin 侧补齐 SKU/规格/加料/媒资/定价替换命令、验证与端点,并新增上/下架接口与全量详情;权限种子补充 `product:publish` 与子资源读写。Mini 侧新增门店菜单接口,按门店返回分类 + 商品全量 JSON(含 SKU/规格/加料/媒资/定价),支持 `updatedAfter` 增量。 - [x] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。 - 已交付:库存模型补充预售/限购/并发字段与批次策略(FIFO/FEFO),新增锁定记录与幂等、过期释放;应用层提供调整/锁定/释放/扣减/批次维护命令与查询,Admin API 暴露库存与批次端点及权限种子。需后续生成迁移落库,并可按需将过期释放接入定时任务。 -- [ ] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。 - - 当前:仅有门店/商品的自提开关字段(`SupportsPickup`/`EnablePickup`),未实现自提时间窗、容量、截单配置及 Mini 端下单限制。 +- [x] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。 + - 已交付:新增自提设置与档期实体/表、并发控制,Admin 端提供自提配置与档期 CRUD 权限/接口;Mini 端提供按日期查询可用档期,包含截单与容量校验。下单限制待后续与订单流程联调。 - [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。 - 当前:领域层与表结构已有 `ShoppingCart/CartItem/CartItemAddon`,但缺少 CQRS 命令/查询、并发锁/限购/券积分预校验以及任何 Admin/Mini 端接口。 - [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StorePickupController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StorePickupController.cs new file mode 100644 index 0000000..f07addd --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StorePickupController.cs @@ -0,0 +1,114 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 门店自提管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/pickup")] +public sealed class StorePickupController(IMediator mediator) : BaseApiController +{ + /// + /// 获取自提配置。 + /// + [HttpGet("settings")] + [PermissionAuthorize("pickup-setting:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> GetSetting(long storeId, CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetStorePickupSettingQuery { StoreId = storeId }, cancellationToken); + return result is null + ? ApiResponse.Error(ErrorCodes.NotFound, "未配置自提设置") + : ApiResponse.Ok(result); + } + + /// + /// 更新自提配置。 + /// + [HttpPut("settings")] + [PermissionAuthorize("pickup-setting:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> UpsertSetting(long storeId, [FromBody] UpsertStorePickupSettingCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询档期列表。 + /// + [HttpGet("slots")] + [PermissionAuthorize("pickup-slot:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> ListSlots(long storeId, CancellationToken cancellationToken) + { + var result = await mediator.Send(new ListStorePickupSlotsQuery { StoreId = storeId }, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 创建档期。 + /// + [HttpPost("slots")] + [PermissionAuthorize("pickup-slot:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> CreateSlot(long storeId, [FromBody] CreateStorePickupSlotCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新档期。 + /// + [HttpPut("slots/{slotId:long}")] + [PermissionAuthorize("pickup-slot:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> UpdateSlot(long storeId, long slotId, [FromBody] UpdateStorePickupSlotCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0 || command.SlotId == 0) + { + command = command with { StoreId = storeId, SlotId = slotId }; + } + + var result = await mediator.Send(command, cancellationToken); + return result is null + ? ApiResponse.Error(ErrorCodes.NotFound, "档期不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除档期。 + /// + [HttpDelete("slots/{slotId:long}")] + [PermissionAuthorize("pickup-slot:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> DeleteSlot(long storeId, long slotId, CancellationToken cancellationToken) + { + var success = await mediator.Send(new DeleteStorePickupSlotCommand { StoreId = storeId, SlotId = slotId }, cancellationToken); + return success ? ApiResponse.Ok(null) : ApiResponse.Error(ErrorCodes.NotFound, "档期不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json index 4c483b1..5e78e4f 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json @@ -290,6 +290,12 @@ "inventory:batch:read", "inventory:batch:update", "inventory:lock:expire", + "pickup-setting:read", + "pickup-setting:update", + "pickup-slot:read", + "pickup-slot:create", + "pickup-slot:update", + "pickup-slot:delete", "order:create", "order:read", "order:update", @@ -398,6 +404,12 @@ "inventory:batch:read", "inventory:batch:update", "inventory:lock:expire", + "pickup-setting:read", + "pickup-setting:update", + "pickup-slot:read", + "pickup-slot:create", + "pickup-slot:update", + "pickup-slot:delete", "order:create", "order:read", "order:update", diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/PickupSlotsController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/PickupSlotsController.cs new file mode 100644 index 0000000..66ce98d --- /dev/null +++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/PickupSlotsController.cs @@ -0,0 +1,31 @@ +using System; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.MiniApi.Controllers; + +/// +/// 小程序端自提档期查询。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/mini/v{version:apiVersion}/stores/{storeId:long}/pickup-slots")] +public sealed class PickupSlotsController(IMediator mediator) : BaseApiController +{ + /// + /// 获取指定日期可用档期。 + /// + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> GetSlots(long storeId, [FromQuery] DateTime date, CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetAvailablePickupSlotsQuery { StoreId = storeId, Date = date }, cancellationToken); + return ApiResponse>.Ok(result); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStorePickupSlotCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStorePickupSlotCommand.cs new file mode 100644 index 0000000..0de3fa9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStorePickupSlotCommand.cs @@ -0,0 +1,50 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 创建自提档期命令。 +/// +public sealed record CreateStorePickupSlotCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; init; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; init; } + + /// + /// 截单分钟。 + /// + public int CutoffMinutes { get; init; } = 30; + + /// + /// 容量。 + /// + public int Capacity { get; init; } + + /// + /// 适用星期。 + /// + public string Weekdays { get; init; } = string.Empty; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; init; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStorePickupSlotCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStorePickupSlotCommand.cs new file mode 100644 index 0000000..e1a80f5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStorePickupSlotCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除自提档期命令。 +/// +public sealed record DeleteStorePickupSlotCommand : IRequest +{ + /// + /// 档期 ID。 + /// + public long SlotId { get; init; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStorePickupSlotCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStorePickupSlotCommand.cs new file mode 100644 index 0000000..9505407 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStorePickupSlotCommand.cs @@ -0,0 +1,55 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新自提档期命令。 +/// +public sealed record UpdateStorePickupSlotCommand : IRequest +{ + /// + /// 档期 ID。 + /// + public long SlotId { get; init; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; init; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; init; } + + /// + /// 截单分钟。 + /// + public int CutoffMinutes { get; init; } + + /// + /// 容量。 + /// + public int Capacity { get; init; } + + /// + /// 适用星期。 + /// + public string Weekdays { get; init; } = string.Empty; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs new file mode 100644 index 0000000..4822b8f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs @@ -0,0 +1,35 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 新增或更新自提配置命令。 +/// +public sealed record UpsertStorePickupSettingCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 是否允许当天。 + /// + public bool AllowToday { get; init; } = true; + + /// + /// 可预约天数。 + /// + public int AllowDaysAhead { get; init; } = 3; + + /// + /// 默认截单分钟。 + /// + public int DefaultCutoffMinutes { get; init; } = 30; + + /// + /// 单笔最大份数。 + /// + public int? MaxQuantityPerOrder { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs new file mode 100644 index 0000000..5b071cd --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 自提配置 DTO。 +/// +public sealed record StorePickupSettingDto +{ + /// + /// 配置 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 是否允许当天自提。 + /// + public bool AllowToday { get; init; } + + /// + /// 可预约天数。 + /// + public int AllowDaysAhead { get; init; } + + /// + /// 默认截单分钟。 + /// + public int DefaultCutoffMinutes { get; init; } + + /// + /// 单笔最大自提份数。 + /// + public int? MaxQuantityPerOrder { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSlotDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSlotDto.cs new file mode 100644 index 0000000..5dc36ce --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSlotDto.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 自提档期 DTO。 +/// +public sealed record StorePickupSlotDto +{ + /// + /// 档期 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; init; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; init; } + + /// + /// 截单分钟。 + /// + public int CutoffMinutes { get; init; } + + /// + /// 容量。 + /// + public int Capacity { get; init; } + + /// + /// 已占用。 + /// + public int ReservedCount { get; init; } + + /// + /// 适用星期(1-7)。 + /// + public string Weekdays { get; init; } = string.Empty; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStorePickupSlotCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStorePickupSlotCommandHandler.cs new file mode 100644 index 0000000..9454424 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStorePickupSlotCommandHandler.cs @@ -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; + +/// +/// 创建自提档期处理器。 +/// +public sealed class CreateStorePickupSlotCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStorePickupSlotCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStorePickupSlotCommandHandler.cs new file mode 100644 index 0000000..c42e4d0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStorePickupSlotCommandHandler.cs @@ -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; + +/// +/// 删除自提档期处理器。 +/// +public sealed class DeleteStorePickupSlotCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task 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; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetAvailablePickupSlotsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetAvailablePickupSlotsQueryHandler.cs new file mode 100644 index 0000000..bbfb8f7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetAvailablePickupSlotsQueryHandler.cs @@ -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; + +/// +/// 可用自提档期查询处理器。 +/// +public sealed class GetAvailablePickupSlotsQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + /// + public async Task> 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); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs new file mode 100644 index 0000000..f7899e3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs @@ -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; + +/// +/// 获取自提配置处理器。 +/// +public sealed class GetStorePickupSettingQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStorePickupSlotsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStorePickupSlotsQueryHandler.cs new file mode 100644 index 0000000..146a7a4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStorePickupSlotsQueryHandler.cs @@ -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; + +/// +/// 自提档期列表查询处理器。 +/// +public sealed class ListStorePickupSlotsQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + /// + public async Task> 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(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStorePickupSlotCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStorePickupSlotCommandHandler.cs new file mode 100644 index 0000000..30355a3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStorePickupSlotCommandHandler.cs @@ -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; + +/// +/// 更新自提档期处理器。 +/// +public sealed class UpdateStorePickupSlotCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs new file mode 100644 index 0000000..abb8866 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs @@ -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; + +/// +/// 自提配置维护处理器。 +/// +public sealed class UpsertStorePickupSettingCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetAvailablePickupSlotsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetAvailablePickupSlotsQuery.cs new file mode 100644 index 0000000..f3310b8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetAvailablePickupSlotsQuery.cs @@ -0,0 +1,21 @@ +using System; +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 获取可用自提档期查询。 +/// +public sealed record GetAvailablePickupSlotsQuery : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 目标日期(本地日期部分)。 + /// + public DateTime Date { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStorePickupSettingQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStorePickupSettingQuery.cs new file mode 100644 index 0000000..f0a4e28 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStorePickupSettingQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 获取门店自提配置查询。 +/// +public sealed record GetStorePickupSettingQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStorePickupSlotsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStorePickupSlotsQuery.cs new file mode 100644 index 0000000..88660af --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStorePickupSlotsQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 门店档期列表查询。 +/// +public sealed record ListStorePickupSlotsQuery : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStorePickupSlotCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStorePickupSlotCommandValidator.cs new file mode 100644 index 0000000..75def24 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStorePickupSlotCommandValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 创建自提档期验证器。 +/// +public sealed class CreateStorePickupSlotCommandValidator : AbstractValidator +{ + /// + /// 初始化规则。 + /// + public CreateStorePickupSlotCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(64); + RuleFor(x => x.Capacity).GreaterThan(0); + RuleFor(x => x.Weekdays).NotEmpty().MaximumLength(32); + RuleFor(x => x.CutoffMinutes).GreaterThanOrEqualTo(0); + RuleFor(x => x.EndTime).GreaterThan(x => x.StartTime); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/DeleteStorePickupSlotCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/DeleteStorePickupSlotCommandValidator.cs new file mode 100644 index 0000000..a7b70f5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/DeleteStorePickupSlotCommandValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 删除自提档期验证器。 +/// +public sealed class DeleteStorePickupSlotCommandValidator : AbstractValidator +{ + /// + /// 初始化规则。 + /// + public DeleteStorePickupSlotCommandValidator() + { + RuleFor(x => x.SlotId).GreaterThan(0); + RuleFor(x => x.StoreId).GreaterThan(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStorePickupSlotCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStorePickupSlotCommandValidator.cs new file mode 100644 index 0000000..7ae91d0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStorePickupSlotCommandValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 更新自提档期验证器。 +/// +public sealed class UpdateStorePickupSlotCommandValidator : AbstractValidator +{ + /// + /// 初始化规则。 + /// + public UpdateStorePickupSlotCommandValidator() + { + RuleFor(x => x.SlotId).GreaterThan(0); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(64); + RuleFor(x => x.Capacity).GreaterThan(0); + RuleFor(x => x.Weekdays).NotEmpty().MaximumLength(32); + RuleFor(x => x.CutoffMinutes).GreaterThanOrEqualTo(0); + RuleFor(x => x.EndTime).GreaterThan(x => x.StartTime); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs new file mode 100644 index 0000000..2cc3d04 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 自提配置验证器。 +/// +public sealed class UpsertStorePickupSettingCommandValidator : AbstractValidator +{ + /// + /// 初始化规则。 + /// + public UpsertStorePickupSettingCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.AllowDaysAhead).GreaterThanOrEqualTo(0); + RuleFor(x => x.DefaultCutoffMinutes).GreaterThanOrEqualTo(0); + RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue); + } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs new file mode 100644 index 0000000..d47984c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店自提配置。 +/// +public sealed class StorePickupSetting : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public long StoreId { get; set; } + + /// + /// 是否允许当天自提。 + /// + public bool AllowToday { get; set; } = true; + + /// + /// 可预约天数(含当天)。 + /// + public int AllowDaysAhead { get; set; } = 3; + + /// + /// 默认截单分钟(开始前多少分钟截止)。 + /// + public int DefaultCutoffMinutes { get; set; } = 30; + + /// + /// 单笔自提最大份数。 + /// + public int? MaxQuantityPerOrder { get; set; } + + /// + /// 并发控制字段。 + /// + [Timestamp] + public byte[] RowVersion { get; set; } = Array.Empty(); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSlot.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSlot.cs new file mode 100644 index 0000000..8ab3f67 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSlot.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店自提档期。 +/// +public sealed class StorePickupSlot : MultiTenantEntityBase +{ + /// + /// 门店标识。 + /// + public long StoreId { get; set; } + + /// + /// 档期名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 当天开始时间(UTC)。 + /// + public TimeSpan StartTime { get; set; } + + /// + /// 当天结束时间(UTC)。 + /// + public TimeSpan EndTime { get; set; } + + /// + /// 截单分钟(开始前多少分钟截止)。 + /// + public int CutoffMinutes { get; set; } = 30; + + /// + /// 容量(份数)。 + /// + public int Capacity { get; set; } + + /// + /// 已占用数量。 + /// + public int ReservedCount { get; set; } + + /// + /// 适用星期(逗号分隔 1-7)。 + /// + public string Weekdays { get; set; } = "1,2,3,4,5,6,7"; + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 并发控制字段。 + /// + [Timestamp] + public byte[] RowVersion { get; set; } = Array.Empty(); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs index 2cdbfac..b7a4766 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs @@ -76,6 +76,41 @@ public interface IStoreRepository /// Task FindTableByCodeAsync(string tableCode, long tenantId, CancellationToken cancellationToken = default); + /// + /// 获取门店自提配置。 + /// + Task GetPickupSettingAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增自提配置。 + /// + Task AddPickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default); + + /// + /// 更新自提配置。 + /// + Task UpdatePickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default); + + /// + /// 获取门店自提档期。 + /// + Task> GetPickupSlotsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 依据标识获取档期。 + /// + Task FindPickupSlotByIdAsync(long slotId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增加档期。 + /// + Task AddPickupSlotsAsync(IEnumerable slots, CancellationToken cancellationToken = default); + + /// + /// 更新档期。 + /// + Task UpdatePickupSlotAsync(StorePickupSlot slot, CancellationToken cancellationToken = default); + /// /// 获取门店员工排班(可选时间范围)。 /// @@ -181,6 +216,11 @@ public interface IStoreRepository /// Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 删除自提档期。 + /// + Task DeletePickupSlotAsync(long slotId, long tenantId, CancellationToken cancellationToken = default); + /// /// 删除排班。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 1630554..0d38a33 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -62,6 +62,8 @@ public sealed class TakeoutAppDbContext( public DbSet StoreTableAreas => Set(); public DbSet StoreTables => Set(); public DbSet StoreEmployeeShifts => Set(); + public DbSet StorePickupSettings => Set(); + public DbSet StorePickupSlots => Set(); public DbSet ProductCategories => Set(); public DbSet Products => Set(); @@ -159,6 +161,8 @@ public sealed class TakeoutAppDbContext( ConfigureStoreTableArea(modelBuilder.Entity()); ConfigureStoreTable(modelBuilder.Entity()); ConfigureStoreEmployeeShift(modelBuilder.Entity()); + ConfigureStorePickupSetting(modelBuilder.Entity()); + ConfigureStorePickupSlot(modelBuilder.Entity()); ConfigureProductCategory(modelBuilder.Entity()); ConfigureProduct(modelBuilder.Entity()); ConfigureProductAttributeGroup(modelBuilder.Entity()); @@ -622,6 +626,28 @@ public sealed class TakeoutAppDbContext( builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ShiftDate, x.StaffId }).IsUnique(); } + private static void ConfigureStorePickupSetting(EntityTypeBuilder builder) + { + builder.ToTable("store_pickup_settings"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.DefaultCutoffMinutes).HasDefaultValue(30); + builder.Property(x => x.RowVersion).IsRowVersion(); + builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique(); + } + + private static void ConfigureStorePickupSlot(EntityTypeBuilder builder) + { + builder.ToTable("store_pickup_slots"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.Weekdays).HasMaxLength(32).IsRequired(); + builder.Property(x => x.CutoffMinutes).HasDefaultValue(30); + builder.Property(x => x.RowVersion).IsRowVersion(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }); + } + private static void ConfigureProductAttributeGroup(EntityTypeBuilder builder) { builder.ToTable("product_attribute_groups"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs index 0722b3c..1e1886c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Stores.Entities; @@ -154,6 +155,59 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos .FirstOrDefaultAsync(cancellationToken); } + /// + public Task GetPickupSettingAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + return context.StorePickupSettings + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public Task AddPickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default) + { + return context.StorePickupSettings.AddAsync(setting, cancellationToken).AsTask(); + } + + /// + public Task UpdatePickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default) + { + context.StorePickupSettings.Update(setting); + return Task.CompletedTask; + } + + /// + public async Task> GetPickupSlotsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var slots = await context.StorePickupSlots + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.StartTime) + .ToListAsync(cancellationToken); + return slots; + } + + /// + public Task FindPickupSlotByIdAsync(long slotId, long tenantId, CancellationToken cancellationToken = default) + { + return context.StorePickupSlots + .Where(x => x.TenantId == tenantId && x.Id == slotId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public Task AddPickupSlotsAsync(IEnumerable slots, CancellationToken cancellationToken = default) + { + return context.StorePickupSlots.AddRangeAsync(slots, cancellationToken); + } + + /// + public Task UpdatePickupSlotAsync(StorePickupSlot slot, CancellationToken cancellationToken = default) + { + context.StorePickupSlots.Update(slot); + return Task.CompletedTask; + } + /// public async Task> GetShiftsAsync(long storeId, long tenantId, DateTime? from = null, DateTime? to = null, CancellationToken cancellationToken = default) { @@ -342,6 +396,19 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos } } + /// + public async Task DeletePickupSlotAsync(long slotId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await context.StorePickupSlots + .Where(x => x.TenantId == tenantId && x.Id == slotId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing != null) + { + context.StorePickupSlots.Remove(existing); + } + } + /// public async Task DeleteShiftAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default) {