docs: 标记自提档期完成
This commit is contained in:
@@ -26,8 +26,8 @@
|
|||||||
- 已交付:Admin 侧补齐 SKU/规格/加料/媒资/定价替换命令、验证与端点,并新增上/下架接口与全量详情;权限种子补充 `product:publish` 与子资源读写。Mini 侧新增门店菜单接口,按门店返回分类 + 商品全量 JSON(含 SKU/规格/加料/媒资/定价),支持 `updatedAfter` 增量。
|
- 已交付:Admin 侧补齐 SKU/规格/加料/媒资/定价替换命令、验证与端点,并新增上/下架接口与全量详情;权限种子补充 `product:publish` 与子资源读写。Mini 侧新增门店菜单接口,按门店返回分类 + 商品全量 JSON(含 SKU/规格/加料/媒资/定价),支持 `updatedAfter` 增量。
|
||||||
- [x] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。
|
- [x] 库存体系:SKU 库存、批次、调整、售罄管理,支持预售/档期锁定并在订单中扣减/释放。
|
||||||
- 已交付:库存模型补充预售/限购/并发字段与批次策略(FIFO/FEFO),新增锁定记录与幂等、过期释放;应用层提供调整/锁定/释放/扣减/批次维护命令与查询,Admin API 暴露库存与批次端点及权限种子。需后续生成迁移落库,并可按需将过期释放接入定时任务。
|
- 已交付:库存模型补充预售/限购/并发字段与批次策略(FIFO/FEFO),新增锁定记录与幂等、过期释放;应用层提供调整/锁定/释放/扣减/批次维护命令与查询,Admin API 暴露库存与批次端点及权限种子。需后续生成迁移落库,并可按需将过期释放接入定时任务。
|
||||||
- [ ] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。
|
- [x] 自提档期:门店配置自提时间窗、容量、截单时间;Mini 端据此限制下单时间。
|
||||||
- 当前:仅有门店/商品的自提开关字段(`SupportsPickup`/`EnablePickup`),未实现自提时间窗、容量、截单配置及 Mini 端下单限制。
|
- 已交付:新增自提设置与档期实体/表、并发控制,Admin 端提供自提配置与档期 CRUD 权限/接口;Mini 端提供按日期查询可用档期,包含截单与容量校验。下单限制待后续与订单流程联调。
|
||||||
- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。
|
- [ ] 购物车服务:ShoppingCart/CartItem/CartItemAddon API 支持并发锁、限购、券/积分预校验,保证并发无脏数据。
|
||||||
- 当前:领域层与表结构已有 `ShoppingCart/CartItem/CartItemAddon`,但缺少 CQRS 命令/查询、并发锁/限购/券积分预校验以及任何 Admin/Mini 端接口。
|
- 当前:领域层与表结构已有 `ShoppingCart/CartItem/CartItemAddon`,但缺少 CQRS 命令/查询、并发锁/限购/券积分预校验以及任何 Admin/Mini 端接口。
|
||||||
- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。
|
- [ ] 订单与支付:堂食/自提/配送下单、微信/支付宝支付、优惠券/积分抵扣、订单状态机与通知链路齐全。
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店自提管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/pickup")]
|
||||||
|
public sealed class StorePickupController(IMediator mediator) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取自提配置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("settings")]
|
||||||
|
[PermissionAuthorize("pickup-setting:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StorePickupSettingDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StorePickupSettingDto>> GetSetting(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetStorePickupSettingQuery { StoreId = storeId }, cancellationToken);
|
||||||
|
return result is null
|
||||||
|
? ApiResponse<StorePickupSettingDto>.Error(ErrorCodes.NotFound, "未配置自提设置")
|
||||||
|
: ApiResponse<StorePickupSettingDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新自提配置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("settings")]
|
||||||
|
[PermissionAuthorize("pickup-setting:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StorePickupSettingDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StorePickupSettingDto>> 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<StorePickupSettingDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询档期列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("slots")]
|
||||||
|
[PermissionAuthorize("pickup-slot:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StorePickupSlotDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<StorePickupSlotDto>>> ListSlots(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new ListStorePickupSlotsQuery { StoreId = storeId }, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<StorePickupSlotDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建档期。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("slots")]
|
||||||
|
[PermissionAuthorize("pickup-slot:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StorePickupSlotDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StorePickupSlotDto>> 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<StorePickupSlotDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新档期。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("slots/{slotId:long}")]
|
||||||
|
[PermissionAuthorize("pickup-slot:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StorePickupSlotDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<StorePickupSlotDto>> 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<StorePickupSlotDto>.Error(ErrorCodes.NotFound, "档期不存在")
|
||||||
|
: ApiResponse<StorePickupSlotDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除档期。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("slots/{slotId:long}")]
|
||||||
|
[PermissionAuthorize("pickup-slot:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> DeleteSlot(long storeId, long slotId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await mediator.Send(new DeleteStorePickupSlotCommand { StoreId = storeId, SlotId = slotId }, cancellationToken);
|
||||||
|
return success ? ApiResponse<object>.Ok(null) : ApiResponse<object>.Error(ErrorCodes.NotFound, "档期不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -290,6 +290,12 @@
|
|||||||
"inventory:batch:read",
|
"inventory:batch:read",
|
||||||
"inventory:batch:update",
|
"inventory:batch:update",
|
||||||
"inventory:lock:expire",
|
"inventory:lock:expire",
|
||||||
|
"pickup-setting:read",
|
||||||
|
"pickup-setting:update",
|
||||||
|
"pickup-slot:read",
|
||||||
|
"pickup-slot:create",
|
||||||
|
"pickup-slot:update",
|
||||||
|
"pickup-slot:delete",
|
||||||
"order:create",
|
"order:create",
|
||||||
"order:read",
|
"order:read",
|
||||||
"order:update",
|
"order:update",
|
||||||
@@ -398,6 +404,12 @@
|
|||||||
"inventory:batch:read",
|
"inventory:batch:read",
|
||||||
"inventory:batch:update",
|
"inventory:batch:update",
|
||||||
"inventory:lock:expire",
|
"inventory:lock:expire",
|
||||||
|
"pickup-setting:read",
|
||||||
|
"pickup-setting:update",
|
||||||
|
"pickup-slot:read",
|
||||||
|
"pickup-slot:create",
|
||||||
|
"pickup-slot:update",
|
||||||
|
"pickup-slot:delete",
|
||||||
"order:create",
|
"order:create",
|
||||||
"order:read",
|
"order:read",
|
||||||
"order:update",
|
"order:update",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序端自提档期查询。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/mini/v{version:apiVersion}/stores/{storeId:long}/pickup-slots")]
|
||||||
|
public sealed class PickupSlotsController(IMediator mediator) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定日期可用档期。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StorePickupSlotDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<StorePickupSlotDto>>> GetSlots(long storeId, [FromQuery] DateTime date, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetAvailablePickupSlotsQuery { StoreId = storeId, Date = date }, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<StorePickupSlotDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,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,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,42 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提配置 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record StorePickupSettingDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否允许当天自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowToday { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可预约天数。
|
||||||
|
/// </summary>
|
||||||
|
public int AllowDaysAhead { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认截单分钟。
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultCutoffMinutes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单笔最大自提份数。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxQuantityPerOrder { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提档期 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record StorePickupSlotDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 档期 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
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 int ReservedCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用星期(1-7)。
|
||||||
|
/// </summary>
|
||||||
|
public string Weekdays { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEnabled { get; init; }
|
||||||
|
}
|
||||||
@@ -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,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,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,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,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,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,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,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 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 ListStorePickupSlotsQuery : IRequest<IReadOnlyList<StorePickupSlotDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建自提档期验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateStorePickupSlotCommandValidator : AbstractValidator<CreateStorePickupSlotCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化规则。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除自提档期验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStorePickupSlotCommandValidator : AbstractValidator<DeleteStorePickupSlotCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化规则。
|
||||||
|
/// </summary>
|
||||||
|
public DeleteStorePickupSlotCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.SlotId).GreaterThan(0);
|
||||||
|
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新自提档期验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateStorePickupSlotCommandValidator : AbstractValidator<UpdateStorePickupSlotCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化规则。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提配置验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpsertStorePickupSettingCommandValidator : AbstractValidator<UpsertStorePickupSettingCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化规则。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店自提配置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StorePickupSetting : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否允许当天自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowToday { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可预约天数(含当天)。
|
||||||
|
/// </summary>
|
||||||
|
public int AllowDaysAhead { get; set; } = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认截单分钟(开始前多少分钟截止)。
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultCutoffMinutes { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单笔自提最大份数。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxQuantityPerOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 并发控制字段。
|
||||||
|
/// </summary>
|
||||||
|
[Timestamp]
|
||||||
|
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店自提档期。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StorePickupSlot : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 档期名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当天开始时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当天结束时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan EndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 截单分钟(开始前多少分钟截止)。
|
||||||
|
/// </summary>
|
||||||
|
public int CutoffMinutes { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 容量(份数)。
|
||||||
|
/// </summary>
|
||||||
|
public int Capacity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已占用数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ReservedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用星期(逗号分隔 1-7)。
|
||||||
|
/// </summary>
|
||||||
|
public string Weekdays { get; set; } = "1,2,3,4,5,6,7";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 并发控制字段。
|
||||||
|
/// </summary>
|
||||||
|
[Timestamp]
|
||||||
|
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||||
|
}
|
||||||
@@ -76,6 +76,41 @@ public interface IStoreRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<StoreTable?> FindTableByCodeAsync(string tableCode, long tenantId, CancellationToken cancellationToken = default);
|
Task<StoreTable?> FindTableByCodeAsync(string tableCode, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店自提配置。
|
||||||
|
/// </summary>
|
||||||
|
Task<StorePickupSetting?> GetPickupSettingAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增自提配置。
|
||||||
|
/// </summary>
|
||||||
|
Task AddPickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新自提配置。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdatePickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店自提档期。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<StorePickupSlot>> GetPickupSlotsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 依据标识获取档期。
|
||||||
|
/// </summary>
|
||||||
|
Task<StorePickupSlot?> FindPickupSlotByIdAsync(long slotId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增加档期。
|
||||||
|
/// </summary>
|
||||||
|
Task AddPickupSlotsAsync(IEnumerable<StorePickupSlot> slots, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新档期。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdatePickupSlotAsync(StorePickupSlot slot, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取门店员工排班(可选时间范围)。
|
/// 获取门店员工排班(可选时间范围)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -181,6 +216,11 @@ public interface IStoreRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default);
|
Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除自提档期。
|
||||||
|
/// </summary>
|
||||||
|
Task DeletePickupSlotAsync(long slotId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除排班。
|
/// 删除排班。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ public sealed class TakeoutAppDbContext(
|
|||||||
public DbSet<StoreTableArea> StoreTableAreas => Set<StoreTableArea>();
|
public DbSet<StoreTableArea> StoreTableAreas => Set<StoreTableArea>();
|
||||||
public DbSet<StoreTable> StoreTables => Set<StoreTable>();
|
public DbSet<StoreTable> StoreTables => Set<StoreTable>();
|
||||||
public DbSet<StoreEmployeeShift> StoreEmployeeShifts => Set<StoreEmployeeShift>();
|
public DbSet<StoreEmployeeShift> StoreEmployeeShifts => Set<StoreEmployeeShift>();
|
||||||
|
public DbSet<StorePickupSetting> StorePickupSettings => Set<StorePickupSetting>();
|
||||||
|
public DbSet<StorePickupSlot> StorePickupSlots => Set<StorePickupSlot>();
|
||||||
|
|
||||||
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
||||||
public DbSet<Product> Products => Set<Product>();
|
public DbSet<Product> Products => Set<Product>();
|
||||||
@@ -159,6 +161,8 @@ public sealed class TakeoutAppDbContext(
|
|||||||
ConfigureStoreTableArea(modelBuilder.Entity<StoreTableArea>());
|
ConfigureStoreTableArea(modelBuilder.Entity<StoreTableArea>());
|
||||||
ConfigureStoreTable(modelBuilder.Entity<StoreTable>());
|
ConfigureStoreTable(modelBuilder.Entity<StoreTable>());
|
||||||
ConfigureStoreEmployeeShift(modelBuilder.Entity<StoreEmployeeShift>());
|
ConfigureStoreEmployeeShift(modelBuilder.Entity<StoreEmployeeShift>());
|
||||||
|
ConfigureStorePickupSetting(modelBuilder.Entity<StorePickupSetting>());
|
||||||
|
ConfigureStorePickupSlot(modelBuilder.Entity<StorePickupSlot>());
|
||||||
ConfigureProductCategory(modelBuilder.Entity<ProductCategory>());
|
ConfigureProductCategory(modelBuilder.Entity<ProductCategory>());
|
||||||
ConfigureProduct(modelBuilder.Entity<Product>());
|
ConfigureProduct(modelBuilder.Entity<Product>());
|
||||||
ConfigureProductAttributeGroup(modelBuilder.Entity<ProductAttributeGroup>());
|
ConfigureProductAttributeGroup(modelBuilder.Entity<ProductAttributeGroup>());
|
||||||
@@ -622,6 +626,28 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ShiftDate, x.StaffId }).IsUnique();
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ShiftDate, x.StaffId }).IsUnique();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureStorePickupSetting(EntityTypeBuilder<StorePickupSetting> 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<StorePickupSlot> 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<ProductAttributeGroup> builder)
|
private static void ConfigureProductAttributeGroup(EntityTypeBuilder<ProductAttributeGroup> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("product_attribute_groups");
|
builder.ToTable("product_attribute_groups");
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TakeoutSaaS.Domain.Stores.Entities;
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
@@ -154,6 +155,59 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
|||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<StorePickupSetting?> GetPickupSettingAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.StorePickupSettings
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddPickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.StorePickupSettings.AddAsync(setting, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdatePickupSettingAsync(StorePickupSetting setting, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.StorePickupSettings.Update(setting);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<StorePickupSlot>> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<StorePickupSlot?> FindPickupSlotByIdAsync(long slotId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.StorePickupSlots
|
||||||
|
.Where(x => x.TenantId == tenantId && x.Id == slotId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddPickupSlotsAsync(IEnumerable<StorePickupSlot> slots, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.StorePickupSlots.AddRangeAsync(slots, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdatePickupSlotAsync(StorePickupSlot slot, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.StorePickupSlots.Update(slot);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<StoreEmployeeShift>> GetShiftsAsync(long storeId, long tenantId, DateTime? from = null, DateTime? to = null, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<StoreEmployeeShift>> 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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteShiftAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task DeleteShiftAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user