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