diff --git a/Document/12_BusinessTodo.md b/Document/12_BusinessTodo.md index 0d2cad0..44e9773 100644 --- a/Document/12_BusinessTodo.md +++ b/Document/12_BusinessTodo.md @@ -16,8 +16,8 @@ - 已交付:新增账单/公告/通知实体与仓储,Admin 端提供 `/tenants/{id}/billings`(列表/详情/创建/标记支付)、`/announcements`(列表/详情/创建/更新/删除/已读)、`/notifications`(列表/已读)端点;权限码补充 `tenant-bill:*`、`tenant-announcement:*`、`tenant-notification:*`,种子模板更新;配额/订阅告警可通过通知表承载。 - [x] 门店管理:Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。 - 进展:已补充营业时间/配送区/节假日的命令、查询、验证与处理器,Admin API 新增子路由完成 CRUD,门店能力开关(预约/排队)已对外暴露;仓储扩展读写删除并保持租户过滤。 -- [ ] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 - - 当前:模型/仓储已包含 `StoreTable`/`StoreTableArea`,但缺少命令/查询/控制器,未实现批量生成、区域容量绑定和二维码导出。 +- [x] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 + - 进展:新增桌台区域/桌码 DTO、命令、查询、验证与处理器,支持批量生成桌码、区域绑定和更新;Admin API 增加桌台区域与桌码 CRUD 及二维码 ZIP 导出端点,使用 QRCoder 生成 SVG 并打包下载;仓储补齐桌台/区域的查找、更新、删除。 - [ ] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 - 当前:存在 `StoreEmployeeShift` 表模型,未提供应用层命令/查询和 Admin API,排班创建/查询能力缺失。 - [ ] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreTableAreasController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreTableAreasController.cs new file mode 100644 index 0000000..26275da --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreTableAreasController.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +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}/table-areas")] +public sealed class StoreTableAreasController(IMediator mediator) : BaseApiController +{ + /// + /// 查询区域列表。 + /// + [HttpGet] + [PermissionAuthorize("store-table-area:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List(long storeId, CancellationToken cancellationToken) + { + var result = await mediator.Send(new ListStoreTableAreasQuery { StoreId = storeId }, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 创建区域。 + /// + [HttpPost] + [PermissionAuthorize("store-table-area:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create(long storeId, [FromBody] CreateStoreTableAreaCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新区域。 + /// + [HttpPut("{areaId:long}")] + [PermissionAuthorize("store-table-area:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long storeId, long areaId, [FromBody] UpdateStoreTableAreaCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0 || command.AreaId == 0) + { + command = command with { StoreId = storeId, AreaId = areaId }; + } + + var result = await mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "桌台区域不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除区域。 + /// + [HttpDelete("{areaId:long}")] + [PermissionAuthorize("store-table-area:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long storeId, long areaId, CancellationToken cancellationToken) + { + var success = await mediator.Send(new DeleteStoreTableAreaCommand { StoreId = storeId, AreaId = areaId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "桌台区域不存在或不可删除"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreTablesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreTablesController.cs new file mode 100644 index 0000000..01af828 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreTablesController.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +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.Domain.Stores.Enums; +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}/tables")] +public sealed class StoreTablesController(IMediator mediator) : BaseApiController +{ + /// + /// 查询桌码列表。 + /// + [HttpGet] + [PermissionAuthorize("store-table:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + long storeId, + [FromQuery] long? areaId, + [FromQuery] StoreTableStatus? status, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new ListStoreTablesQuery + { + StoreId = storeId, + AreaId = areaId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 批量生成桌码。 + /// + [HttpPost] + [PermissionAuthorize("store-table:create")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> Generate(long storeId, [FromBody] GenerateStoreTablesCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + + var result = await mediator.Send(command, cancellationToken); + return ApiResponse>.Ok(result); + } + + /// + /// 更新桌码。 + /// + [HttpPut("{tableId:long}")] + [PermissionAuthorize("store-table:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long storeId, long tableId, [FromBody] UpdateStoreTableCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0 || command.TableId == 0) + { + command = command with { StoreId = storeId, TableId = tableId }; + } + + var result = await mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "桌码不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除桌码。 + /// + [HttpDelete("{tableId:long}")] + [PermissionAuthorize("store-table:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long storeId, long tableId, CancellationToken cancellationToken) + { + var success = await mediator.Send(new DeleteStoreTableCommand { StoreId = storeId, TableId = tableId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "桌码不存在"); + } + + /// + /// 导出桌码二维码 ZIP。 + /// + [HttpPost("export")] + [PermissionAuthorize("store-table:export")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task Export(long storeId, [FromBody] ExportStoreTableQRCodesQuery query, CancellationToken cancellationToken) + { + if (query.StoreId == 0) + { + query = query with { StoreId = storeId }; + } + + var result = await mediator.Send(query, cancellationToken); + if (result is null) + { + return Ok(ApiResponse.Error(ErrorCodes.NotFound, "未找到可导出的桌码")); + } + + return File(result.Content, result.ContentType, result.FileName); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json index 6ebda51..0b9e0da 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json @@ -91,6 +91,15 @@ "store:read", "store:update", "store:delete", + "store-table-area:read", + "store-table-area:create", + "store-table-area:update", + "store-table-area:delete", + "store-table:read", + "store-table:create", + "store-table:update", + "store-table:delete", + "store-table:export", "product:create", "product:read", "product:update", @@ -158,6 +167,15 @@ "store:read", "store:update", "store:delete", + "store-table-area:read", + "store-table-area:create", + "store-table-area:update", + "store-table-area:delete", + "store-table:read", + "store-table:create", + "store-table:update", + "store-table:delete", + "store-table:export", "product:create", "product:read", "product:update", @@ -190,6 +208,14 @@ "identity:profile:read", "store:read", "store:update", + "store-table-area:read", + "store-table-area:create", + "store-table-area:update", + "store-table-area:delete", + "store-table:read", + "store-table:create", + "store-table:update", + "store-table:export", "product:create", "product:read", "product:update", @@ -214,6 +240,8 @@ "Permissions": [ "identity:profile:read", "store:read", + "store-table-area:read", + "store-table:read", "product:read", "order:read", "order:update", diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreTableAreaCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreTableAreaCommand.cs new file mode 100644 index 0000000..60511b4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreTableAreaCommand.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 创建桌台区域命令。 +/// +public sealed record CreateStoreTableAreaCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 区域名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 区域描述。 + /// + public string? Description { get; init; } + + /// + /// 排序值。 + /// + public int SortOrder { get; init; } = 100; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreTableAreaCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreTableAreaCommand.cs new file mode 100644 index 0000000..9f74c48 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreTableAreaCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除桌台区域命令。 +/// +public sealed record DeleteStoreTableAreaCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 区域 ID。 + /// + public long AreaId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreTableCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreTableCommand.cs new file mode 100644 index 0000000..0480296 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreTableCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除桌码命令。 +/// +public sealed record DeleteStoreTableCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 桌台 ID。 + /// + public long TableId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/GenerateStoreTablesCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/GenerateStoreTablesCommand.cs new file mode 100644 index 0000000..4c10d56 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/GenerateStoreTablesCommand.cs @@ -0,0 +1,45 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 批量生成桌码命令。 +/// +public sealed record GenerateStoreTablesCommand : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 桌码前缀。 + /// + public string TableCodePrefix { get; init; } = "T"; + + /// + /// 起始序号。 + /// + public int StartNumber { get; init; } = 1; + + /// + /// 生成数量。 + /// + public int Count { get; init; } + + /// + /// 默认容量。 + /// + public int DefaultCapacity { get; init; } = 2; + + /// + /// 区域 ID。 + /// + public long? AreaId { get; init; } + + /// + /// 标签。 + /// + public string? Tags { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreTableAreaCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreTableAreaCommand.cs new file mode 100644 index 0000000..b113491 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreTableAreaCommand.cs @@ -0,0 +1,35 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新桌台区域命令。 +/// +public sealed record UpdateStoreTableAreaCommand : IRequest +{ + /// + /// 区域 ID。 + /// + public long AreaId { get; init; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 区域名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 区域描述。 + /// + public string? Description { get; init; } + + /// + /// 排序值。 + /// + public int SortOrder { get; init; } = 100; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreTableCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreTableCommand.cs new file mode 100644 index 0000000..d93e92c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreTableCommand.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新桌码命令。 +/// +public sealed record UpdateStoreTableCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 桌台 ID。 + /// + public long TableId { get; init; } + + /// + /// 区域 ID。 + /// + public long? AreaId { get; init; } + + /// + /// 桌码。 + /// + public string TableCode { get; init; } = string.Empty; + + /// + /// 容量。 + /// + public int Capacity { get; init; } + + /// + /// 标签。 + /// + public string? Tags { get; init; } + + /// + /// 状态。 + /// + public StoreTableStatus Status { get; init; } = StoreTableStatus.Idle; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableAreaDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableAreaDto.cs new file mode 100644 index 0000000..8c5e533 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableAreaDto.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 桌台区域 DTO。 +/// +public sealed record StoreTableAreaDto +{ + /// + /// 区域 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 区域名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 区域描述。 + /// + public string? Description { get; init; } + + /// + /// 排序值。 + /// + public int SortOrder { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableDto.cs new file mode 100644 index 0000000..b66b60c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableDto.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 桌台 DTO。 +/// +public sealed record StoreTableDto +{ + /// + /// 桌台 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 区域 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? AreaId { get; init; } + + /// + /// 桌码。 + /// + public string TableCode { get; init; } = string.Empty; + + /// + /// 容量。 + /// + public int Capacity { get; init; } + + /// + /// 标签。 + /// + public string? Tags { get; init; } + + /// + /// 状态。 + /// + public StoreTableStatus Status { get; init; } + + /// + /// 二维码地址。 + /// + public string? QrCodeUrl { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableExportResult.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableExportResult.cs new file mode 100644 index 0000000..fea6b4e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreTableExportResult.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 桌台二维码导出结果。 +/// +public sealed record StoreTableExportResult +{ + /// + /// 文件名。 + /// + public string FileName { get; init; } = string.Empty; + + /// + /// 内容类型。 + /// + public string ContentType { get; init; } = string.Empty; + + /// + /// 文件内容。 + /// + public byte[] Content { get; init; } = []; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreTableAreaCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreTableAreaCommandHandler.cs new file mode 100644 index 0000000..4999f4f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreTableAreaCommandHandler.cs @@ -0,0 +1,58 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 创建桌台区域处理器。 +/// +public sealed class CreateStoreTableAreaCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(CreateStoreTableAreaCommand request, CancellationToken cancellationToken) + { + // 1. 校验门店存在 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); + } + + // 2. 校验区域名称唯一 + var existingAreas = await storeRepository.GetTableAreasAsync(request.StoreId, tenantId, cancellationToken); + var hasDuplicate = existingAreas.Any(x => x.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)); + if (hasDuplicate) + { + throw new BusinessException(ErrorCodes.Conflict, "区域名称已存在"); + } + + // 3. 构建实体 + var area = new StoreTableArea + { + StoreId = request.StoreId, + Name = request.Name.Trim(), + Description = request.Description?.Trim(), + SortOrder = request.SortOrder + }; + + // 4. 持久化 + await storeRepository.AddTableAreasAsync(new[] { area }, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("创建桌台区域 {AreaId} 对应门店 {StoreId}", area.Id, request.StoreId); + + // 5. 返回 DTO + return StoreMapping.ToDto(area); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreTableAreaCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreTableAreaCommandHandler.cs new file mode 100644 index 0000000..48dad11 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreTableAreaCommandHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 删除桌台区域处理器。 +/// +public sealed class DeleteStoreTableAreaCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(DeleteStoreTableAreaCommand request, CancellationToken cancellationToken) + { + // 1. 读取区域 + var tenantId = tenantProvider.GetCurrentTenantId(); + var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId, tenantId, cancellationToken); + if (area is null) + { + return false; + } + + // 2. 校验门店归属 + if (area.StoreId != request.StoreId) + { + return false; + } + + // 3. 校验区域下无桌码 + var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken); + var hasTable = tables.Any(x => x.AreaId == request.AreaId); + if (hasTable) + { + throw new BusinessException(ErrorCodes.Conflict, "区域下仍有桌码,无法删除"); + } + + // 4. 删除 + await storeRepository.DeleteTableAreaAsync(request.AreaId, tenantId, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("删除桌台区域 {AreaId} 对应门店 {StoreId}", request.AreaId, request.StoreId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreTableCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreTableCommandHandler.cs new file mode 100644 index 0000000..9828253 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreTableCommandHandler.cs @@ -0,0 +1,36 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 删除桌码处理器。 +/// +public sealed class DeleteStoreTableCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(DeleteStoreTableCommand request, CancellationToken cancellationToken) + { + // 1. 读取桌码 + var tenantId = tenantProvider.GetCurrentTenantId(); + var table = await storeRepository.FindTableByIdAsync(request.TableId, tenantId, cancellationToken); + if (table is null || table.StoreId != request.StoreId) + { + return false; + } + + // 2. 删除 + await storeRepository.DeleteTableAsync(request.TableId, tenantId, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("删除桌码 {TableId} 对应门店 {StoreId}", request.TableId, request.StoreId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ExportStoreTableQRCodesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ExportStoreTableQRCodesQueryHandler.cs new file mode 100644 index 0000000..95ead70 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ExportStoreTableQRCodesQueryHandler.cs @@ -0,0 +1,86 @@ +using System.IO.Compression; +using System.Linq; +using System.Text; +using MediatR; +using Microsoft.Extensions.Logging; +using QRCoder; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 导出桌码二维码处理器。 +/// +public sealed class ExportStoreTableQRCodesQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(ExportStoreTableQRCodesQuery request, CancellationToken cancellationToken) + { + // 1. 校验门店存在 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + return null; + } + + // 2. 获取桌码列表 + var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken); + if (request.AreaId.HasValue) + { + tables = tables.Where(x => x.AreaId == request.AreaId.Value).ToList(); + } + + if (tables.Count == 0) + { + return null; + } + + // 3. 生成 ZIP + var template = string.IsNullOrWhiteSpace(request.QrContentTemplate) ? "{code}" : request.QrContentTemplate!; + using var memoryStream = new MemoryStream(); + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true, Encoding.UTF8)) + { + foreach (var table in tables) + { + var content = BuildPayload(template, table.TableCode); + var svg = RenderSvg(content); + var entry = archive.CreateEntry($"{table.TableCode}.svg", CompressionLevel.Fastest); + using var entryStream = entry.Open(); + using var writer = new StreamWriter(entryStream, Encoding.UTF8); + writer.Write(svg); + } + } + + // 4. 返回导出结果 + var fileName = $"store_{request.StoreId}_tables_{DateTime.UtcNow:yyyyMMddHHmmss}.zip"; + logger.LogInformation("导出门店 {StoreId} 桌码二维码 {Count} 个", request.StoreId, tables.Count); + return new StoreTableExportResult + { + FileName = fileName, + ContentType = "application/zip", + Content = memoryStream.ToArray() + }; + } + + private static string BuildPayload(string template, string tableCode) + { + var payload = template.Replace("{code}", tableCode, StringComparison.OrdinalIgnoreCase); + return string.IsNullOrWhiteSpace(payload) ? tableCode : payload; + } + + private static string RenderSvg(string payload) + { + using var generator = new QRCodeGenerator(); + var data = generator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.Q); + var svg = new SvgQRCode(data); + return svg.GetGraphic(5); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GenerateStoreTablesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GenerateStoreTablesCommandHandler.cs new file mode 100644 index 0000000..ad2cf70 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GenerateStoreTablesCommandHandler.cs @@ -0,0 +1,73 @@ +using System.Linq; +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 批量生成桌码处理器。 +/// +public sealed class GenerateStoreTablesCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler> +{ + /// + public async Task> Handle(GenerateStoreTablesCommand request, CancellationToken cancellationToken) + { + // 1. 校验门店存在 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); + } + + // 2. 校验区域归属 + if (request.AreaId.HasValue) + { + var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId.Value, tenantId, cancellationToken); + if (area is null || area.StoreId != request.StoreId) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "桌台区域不存在或不属于该门店"); + } + } + + // 3. 校验桌码唯一性 + var existingTables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken); + var newCodes = Enumerable.Range(request.StartNumber, request.Count) + .Select(i => $"{request.TableCodePrefix.Trim()}{i}") + .ToList(); + var conflicts = existingTables.Where(t => newCodes.Contains(t.TableCode, StringComparer.OrdinalIgnoreCase)).ToList(); + if (conflicts.Count > 0) + { + throw new BusinessException(ErrorCodes.Conflict, "桌码已存在,生成失败"); + } + + // 4. 构建实体 + var tables = newCodes.Select(code => new StoreTable + { + StoreId = request.StoreId, + AreaId = request.AreaId, + TableCode = code, + Capacity = request.DefaultCapacity, + Tags = request.Tags?.Trim() + }).ToList(); + + // 5. 持久化 + await storeRepository.AddTablesAsync(tables, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("批量创建桌码 {Count} 条 对应门店 {StoreId}", tables.Count, request.StoreId); + + // 6. 返回 DTO + return tables.Select(StoreMapping.ToDto).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreTableAreasQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreTableAreasQueryHandler.cs new file mode 100644 index 0000000..5de8fa5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreTableAreasQueryHandler.cs @@ -0,0 +1,26 @@ +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 桌台区域列表查询处理器。 +/// +public sealed class ListStoreTableAreasQueryHandler(IStoreRepository storeRepository, ITenantProvider tenantProvider) + : IRequestHandler> +{ + /// + public async Task> Handle(ListStoreTableAreasQuery request, CancellationToken cancellationToken) + { + // 1. 查询区域列表 + var tenantId = tenantProvider.GetCurrentTenantId(); + var areas = await storeRepository.GetTableAreasAsync(request.StoreId, tenantId, cancellationToken); + + // 2. 映射 DTO + return areas.Select(StoreMapping.ToDto).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreTablesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreTablesQueryHandler.cs new file mode 100644 index 0000000..786b67f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreTablesQueryHandler.cs @@ -0,0 +1,39 @@ +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 桌码列表查询处理器。 +/// +public sealed class ListStoreTablesQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + /// + public async Task> Handle(ListStoreTablesQuery request, CancellationToken cancellationToken) + { + // 1. 查询桌码列表 + var tenantId = tenantProvider.GetCurrentTenantId(); + var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken); + + // 2. 过滤 + if (request.AreaId.HasValue) + { + tables = tables.Where(x => x.AreaId == request.AreaId.Value).ToList(); + } + + if (request.Status.HasValue) + { + tables = tables.Where(x => x.Status == request.Status.Value).ToList(); + } + + // 3. 映射 DTO + return tables.Select(StoreMapping.ToDto).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreTableAreaCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreTableAreaCommandHandler.cs new file mode 100644 index 0000000..e488d6b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreTableAreaCommandHandler.cs @@ -0,0 +1,59 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 更新桌台区域处理器。 +/// +public sealed class UpdateStoreTableAreaCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(UpdateStoreTableAreaCommand request, CancellationToken cancellationToken) + { + // 1. 读取区域 + var tenantId = tenantProvider.GetCurrentTenantId(); + var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId, tenantId, cancellationToken); + if (area is null) + { + return null; + } + + // 2. 校验门店归属 + if (area.StoreId != request.StoreId) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "区域不属于该门店"); + } + + // 3. 名称唯一校验 + var areas = await storeRepository.GetTableAreasAsync(request.StoreId, tenantId, cancellationToken); + var hasDuplicate = areas.Any(x => x.Id != request.AreaId && x.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)); + if (hasDuplicate) + { + throw new BusinessException(ErrorCodes.Conflict, "区域名称已存在"); + } + + // 4. 更新字段 + area.Name = request.Name.Trim(); + area.Description = request.Description?.Trim(); + area.SortOrder = request.SortOrder; + + // 5. 持久化 + await storeRepository.UpdateTableAreaAsync(area, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("更新桌台区域 {AreaId} 对应门店 {StoreId}", area.Id, area.StoreId); + + // 6. 返回 DTO + return StoreMapping.ToDto(area); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreTableCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreTableCommandHandler.cs new file mode 100644 index 0000000..b6f8474 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreTableCommandHandler.cs @@ -0,0 +1,72 @@ +using System.Linq; +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 更新桌码处理器。 +/// +public sealed class UpdateStoreTableCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(UpdateStoreTableCommand request, CancellationToken cancellationToken) + { + // 1. 读取桌码 + var tenantId = tenantProvider.GetCurrentTenantId(); + var table = await storeRepository.FindTableByIdAsync(request.TableId, tenantId, cancellationToken); + if (table is null) + { + return null; + } + + // 2. 校验门店归属 + if (table.StoreId != request.StoreId) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "桌码不属于该门店"); + } + + // 3. 校验区域归属 + if (request.AreaId.HasValue) + { + var area = await storeRepository.FindTableAreaByIdAsync(request.AreaId.Value, tenantId, cancellationToken); + if (area is null || area.StoreId != request.StoreId) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "桌台区域不存在或不属于该门店"); + } + } + + // 4. 校验桌码唯一 + var tables = await storeRepository.GetTablesAsync(request.StoreId, tenantId, cancellationToken); + var exists = tables.Any(x => x.Id != request.TableId && x.TableCode.Equals(request.TableCode, StringComparison.OrdinalIgnoreCase)); + if (exists) + { + throw new BusinessException(ErrorCodes.Conflict, "桌码已存在"); + } + + // 5. 更新字段 + table.AreaId = request.AreaId; + table.TableCode = request.TableCode.Trim(); + table.Capacity = request.Capacity; + table.Tags = request.Tags?.Trim(); + table.Status = request.Status; + + // 6. 持久化 + await storeRepository.UpdateTableAsync(table, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("更新桌码 {TableId} 对应门店 {StoreId}", table.Id, table.StoreId); + + // 7. 返回 DTO + return StoreMapping.ToDto(table); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ExportStoreTableQRCodesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ExportStoreTableQRCodesQuery.cs new file mode 100644 index 0000000..230fda0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ExportStoreTableQRCodesQuery.cs @@ -0,0 +1,25 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 导出桌码二维码查询。 +/// +public sealed record ExportStoreTableQRCodesQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 区域筛选。 + /// + public long? AreaId { get; init; } + + /// + /// 内容模板,使用 {code} 占位。 + /// + public string? QrContentTemplate { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreTableAreasQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreTableAreasQuery.cs new file mode 100644 index 0000000..6af7efc --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreTableAreasQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 门店桌台区域列表查询。 +/// +public sealed record ListStoreTableAreasQuery : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreTablesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreTablesQuery.cs new file mode 100644 index 0000000..707e915 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreTablesQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 门店桌码列表查询。 +/// +public sealed record ListStoreTablesQuery : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 区域筛选。 + /// + public long? AreaId { get; init; } + + /// + /// 状态筛选。 + /// + public StoreTableStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs b/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs index 06c9094..65f1be5 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs @@ -61,4 +61,39 @@ public static class StoreMapping Reason = holiday.Reason, CreatedAt = holiday.CreatedAt }; + + /// + /// 映射桌台区域 DTO。 + /// + /// 区域实体。 + /// DTO。 + public static StoreTableAreaDto ToDto(StoreTableArea area) => new() + { + Id = area.Id, + TenantId = area.TenantId, + StoreId = area.StoreId, + Name = area.Name, + Description = area.Description, + SortOrder = area.SortOrder, + CreatedAt = area.CreatedAt + }; + + /// + /// 映射桌台 DTO。 + /// + /// 桌台实体。 + /// DTO。 + public static StoreTableDto ToDto(StoreTable table) => new() + { + Id = table.Id, + TenantId = table.TenantId, + StoreId = table.StoreId, + AreaId = table.AreaId, + TableCode = table.TableCode, + Capacity = table.Capacity, + Tags = table.Tags, + Status = table.Status, + QrCodeUrl = table.QrCodeUrl, + CreatedAt = table.CreatedAt + }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreTableAreaCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreTableAreaCommandValidator.cs new file mode 100644 index 0000000..9f6649b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreTableAreaCommandValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 创建桌台区域命令验证器。 +/// +public sealed class CreateStoreTableAreaCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateStoreTableAreaCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(64); + RuleFor(x => x.Description).MaximumLength(256); + RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/GenerateStoreTablesCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/GenerateStoreTablesCommandValidator.cs new file mode 100644 index 0000000..534caca --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/GenerateStoreTablesCommandValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 批量生成桌码命令验证器。 +/// +public sealed class GenerateStoreTablesCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public GenerateStoreTablesCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.TableCodePrefix).NotEmpty().MaximumLength(16); + RuleFor(x => x.StartNumber).GreaterThan(0); + RuleFor(x => x.Count).GreaterThan(0).LessThanOrEqualTo(500); + RuleFor(x => x.DefaultCapacity).GreaterThan(0).LessThanOrEqualTo(50); + RuleFor(x => x.Tags).MaximumLength(128); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreTableAreaCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreTableAreaCommandValidator.cs new file mode 100644 index 0000000..31d2b98 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreTableAreaCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 更新桌台区域命令验证器。 +/// +public sealed class UpdateStoreTableAreaCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateStoreTableAreaCommandValidator() + { + RuleFor(x => x.AreaId).GreaterThan(0); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(64); + RuleFor(x => x.Description).MaximumLength(256); + RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreTableCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreTableCommandValidator.cs new file mode 100644 index 0000000..340453e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreTableCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 更新桌码命令验证器。 +/// +public sealed class UpdateStoreTableCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateStoreTableCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.TableId).GreaterThan(0); + RuleFor(x => x.TableCode).NotEmpty().MaximumLength(32); + RuleFor(x => x.Capacity).GreaterThan(0).LessThanOrEqualTo(50); + RuleFor(x => x.Tags).MaximumLength(128); + } +} diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index acb8fc2..9b5901b 100644 Binary files a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj and b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj differ diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs index 252140b..7ed73f1 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs @@ -56,11 +56,21 @@ public interface IStoreRepository /// Task> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 依据标识获取桌台区域。 + /// + Task FindTableAreaByIdAsync(long areaId, long tenantId, CancellationToken cancellationToken = default); + /// /// 获取门店桌台列表。 /// Task> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 依据标识获取桌台。 + /// + Task FindTableByIdAsync(long tableId, long tenantId, CancellationToken cancellationToken = default); + /// /// 获取门店员工排班。 /// @@ -106,11 +116,21 @@ public interface IStoreRepository /// Task AddTableAreasAsync(IEnumerable areas, CancellationToken cancellationToken = default); + /// + /// 更新桌台区域。 + /// + Task UpdateTableAreaAsync(StoreTableArea area, CancellationToken cancellationToken = default); + /// /// 新增桌台。 /// Task AddTablesAsync(IEnumerable tables, CancellationToken cancellationToken = default); + /// + /// 更新桌台。 + /// + Task UpdateTableAsync(StoreTable table, CancellationToken cancellationToken = default); + /// /// 新增排班。 /// @@ -136,6 +156,16 @@ public interface IStoreRepository /// Task DeleteHolidayAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 删除桌台区域。 + /// + Task DeleteTableAreaAsync(long areaId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 删除桌台。 + /// + Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default); + /// /// 更新门店。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs index b944cf3..2e146e2 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -118,6 +118,14 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos return areas; } + /// + public Task FindTableAreaByIdAsync(long areaId, long tenantId, CancellationToken cancellationToken = default) + { + return context.StoreTableAreas + .Where(x => x.TenantId == tenantId && x.Id == areaId) + .FirstOrDefaultAsync(cancellationToken); + } + /// public async Task> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { @@ -130,6 +138,14 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos return tables; } + /// + public Task FindTableByIdAsync(long tableId, long tenantId, CancellationToken cancellationToken = default) + { + return context.StoreTables + .Where(x => x.TenantId == tenantId && x.Id == tableId) + .FirstOrDefaultAsync(cancellationToken); + } + /// public async Task> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) { @@ -194,12 +210,26 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos return context.StoreTableAreas.AddRangeAsync(areas, cancellationToken); } + /// + public Task UpdateTableAreaAsync(StoreTableArea area, CancellationToken cancellationToken = default) + { + context.StoreTableAreas.Update(area); + return Task.CompletedTask; + } + /// public Task AddTablesAsync(IEnumerable tables, CancellationToken cancellationToken = default) { return context.StoreTables.AddRangeAsync(tables, cancellationToken); } + /// + public Task UpdateTableAsync(StoreTable table, CancellationToken cancellationToken = default) + { + context.StoreTables.Update(table); + return Task.CompletedTask; + } + /// public Task AddShiftsAsync(IEnumerable shifts, CancellationToken cancellationToken = default) { @@ -251,6 +281,32 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos } } + /// + public async Task DeleteTableAreaAsync(long areaId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await context.StoreTableAreas + .Where(x => x.TenantId == tenantId && x.Id == areaId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing != null) + { + context.StoreTableAreas.Remove(existing); + } + } + + /// + public async Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await context.StoreTables + .Where(x => x.TenantId == tenantId && x.Id == tableId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing != null) + { + context.StoreTables.Remove(existing); + } + } + /// public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default) {