diff --git a/Document/12_BusinessTodo.md b/Document/12_BusinessTodo.md index 44e9773..870b3e7 100644 --- a/Document/12_BusinessTodo.md +++ b/Document/12_BusinessTodo.md @@ -18,8 +18,8 @@ - 进展:已补充营业时间/配送区/节假日的命令、查询、验证与处理器,Admin API 新增子路由完成 CRUD,门店能力开关(预约/排队)已对外暴露;仓储扩展读写删除并保持租户过滤。 - [x] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIP(POST /api/admin/stores/{id}/tables 可下载)。 - 进展:新增桌台区域/桌码 DTO、命令、查询、验证与处理器,支持批量生成桌码、区域绑定和更新;Admin API 增加桌台区域与桌码 CRUD 及二维码 ZIP 导出端点,使用 QRCoder 生成 SVG 并打包下载;仓储补齐桌台/区域的查找、更新、删除。 -- [ ] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 - - 当前:存在 `StoreEmployeeShift` 表模型,未提供应用层命令/查询和 Admin API,排班创建/查询能力缺失。 +- [x] 员工排班:创建员工、绑定门店角色、维护 StoreEmployeeShift,可查询未来 7 日排班。 + - 进展:新增门店员工 DTO/命令/查询/验证与处理器,支持员工创建/更新/删除及按门店查询;新增排班 CRUD(默认查询未来 7 天),校验员工归属、时间冲突;Admin API 增加员工与排班控制器及权限种子,仓储补充排班查询/更新/删除。 - [ ] 桌码扫码入口:Mini 端解析二维码,GET /api/mini/tables/{code}/context 返回门店、桌台、公告。 - 当前:MiniApi 无桌码相关接口,未实现桌码解析与上下文返回。 - [ ] 菜品建模:分类、SPU、SKU、规格/加料组、价格策略、媒资 CRUD + 上下架流程;Mini 端可拉取完整 JSON。 diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreShiftsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreShiftsController.cs new file mode 100644 index 0000000..4b6fa2d --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreShiftsController.cs @@ -0,0 +1,99 @@ +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}/shifts")] +public sealed class StoreShiftsController(IMediator mediator) : BaseApiController +{ + /// + /// 查询排班(默认未来 7 天)。 + /// + [HttpGet] + [PermissionAuthorize("store-shift:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + long storeId, + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] long? staffId, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new ListStoreEmployeeShiftsQuery + { + StoreId = storeId, + From = from, + To = to, + StaffId = staffId + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 创建排班。 + /// + [HttpPost] + [PermissionAuthorize("store-shift:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create(long storeId, [FromBody] CreateStoreEmployeeShiftCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新排班。 + /// + [HttpPut("{shiftId:long}")] + [PermissionAuthorize("store-shift:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long storeId, long shiftId, [FromBody] UpdateStoreEmployeeShiftCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0 || command.ShiftId == 0) + { + command = command with { StoreId = storeId, ShiftId = shiftId }; + } + + var result = await mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "排班不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除排班。 + /// + [HttpDelete("{shiftId:long}")] + [PermissionAuthorize("store-shift:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long storeId, long shiftId, CancellationToken cancellationToken) + { + var success = await mediator.Send(new DeleteStoreEmployeeShiftCommand { StoreId = storeId, ShiftId = shiftId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "排班不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreStaffsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreStaffsController.cs new file mode 100644 index 0000000..f578d05 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreStaffsController.cs @@ -0,0 +1,98 @@ +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.Merchants.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}/staffs")] +public sealed class StoreStaffsController(IMediator mediator) : BaseApiController +{ + /// + /// 查询门店员工列表。 + /// + [HttpGet] + [PermissionAuthorize("store-staff:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + long storeId, + [FromQuery] StaffRoleType? role, + [FromQuery] StaffStatus? status, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new ListStoreStaffQuery + { + StoreId = storeId, + RoleType = role, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 创建门店员工。 + /// + [HttpPost] + [PermissionAuthorize("store-staff:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create(long storeId, [FromBody] CreateStoreStaffCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0) + { + command = command with { StoreId = storeId }; + } + + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新门店员工。 + /// + [HttpPut("{staffId:long}")] + [PermissionAuthorize("store-staff:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long storeId, long staffId, [FromBody] UpdateStoreStaffCommand command, CancellationToken cancellationToken) + { + if (command.StoreId == 0 || command.StaffId == 0) + { + command = command with { StoreId = storeId, StaffId = staffId }; + } + + var result = await mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "员工不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除门店员工。 + /// + [HttpDelete("{staffId:long}")] + [PermissionAuthorize("store-staff:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long storeId, long staffId, CancellationToken cancellationToken) + { + var success = await mediator.Send(new DeleteStoreStaffCommand { StoreId = storeId, StaffId = staffId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "员工不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json index 0b9e0da..27eaa88 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json @@ -100,6 +100,14 @@ "store-table:update", "store-table:delete", "store-table:export", + "store-staff:read", + "store-staff:create", + "store-staff:update", + "store-staff:delete", + "store-shift:read", + "store-shift:create", + "store-shift:update", + "store-shift:delete", "product:create", "product:read", "product:update", @@ -176,6 +184,14 @@ "store-table:update", "store-table:delete", "store-table:export", + "store-staff:read", + "store-staff:create", + "store-staff:update", + "store-staff:delete", + "store-shift:read", + "store-shift:create", + "store-shift:update", + "store-shift:delete", "product:create", "product:read", "product:update", @@ -216,6 +232,12 @@ "store-table:create", "store-table:update", "store-table:export", + "store-staff:read", + "store-staff:create", + "store-staff:update", + "store-shift:read", + "store-shift:create", + "store-shift:update", "product:create", "product:read", "product:update", @@ -242,6 +264,7 @@ "store:read", "store-table-area:read", "store-table:read", + "store-shift:read", "product:read", "order:read", "order:update", diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreEmployeeShiftCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreEmployeeShiftCommand.cs new file mode 100644 index 0000000..2729a6f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreEmployeeShiftCommand.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 创建员工排班命令。 +/// +public sealed record CreateStoreEmployeeShiftCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 员工 ID。 + /// + public long StaffId { get; init; } + + /// + /// 班次日期。 + /// + public DateTime ShiftDate { get; init; } + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; init; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; init; } + + /// + /// 排班角色。 + /// + public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk; + + /// + /// 备注。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreStaffCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreStaffCommand.cs new file mode 100644 index 0000000..26dce0d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreStaffCommand.cs @@ -0,0 +1,36 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 创建门店员工命令。 +/// +public sealed record CreateStoreStaffCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 姓名。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 手机号。 + /// + public string Phone { get; init; } = string.Empty; + + /// + /// 邮箱。 + /// + public string? Email { get; init; } + + /// + /// 角色。 + /// + public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreEmployeeShiftCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreEmployeeShiftCommand.cs new file mode 100644 index 0000000..0e75d12 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreEmployeeShiftCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除员工排班命令。 +/// +public sealed record DeleteStoreEmployeeShiftCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 排班 ID。 + /// + public long ShiftId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreStaffCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreStaffCommand.cs new file mode 100644 index 0000000..6c9c94b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreStaffCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除门店员工命令。 +/// +public sealed record DeleteStoreStaffCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 员工 ID。 + /// + public long StaffId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreEmployeeShiftCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreEmployeeShiftCommand.cs new file mode 100644 index 0000000..c1da376 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreEmployeeShiftCommand.cs @@ -0,0 +1,51 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新员工排班命令。 +/// +public sealed record UpdateStoreEmployeeShiftCommand : IRequest +{ + /// + /// 排班 ID。 + /// + public long ShiftId { get; init; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 员工 ID。 + /// + public long StaffId { get; init; } + + /// + /// 班次日期。 + /// + public DateTime ShiftDate { get; init; } + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; init; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; init; } + + /// + /// 排班角色。 + /// + public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk; + + /// + /// 备注。 + /// + public string? Notes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreStaffCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreStaffCommand.cs new file mode 100644 index 0000000..335a9cf --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreStaffCommand.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新门店员工命令。 +/// +public sealed record UpdateStoreStaffCommand : IRequest +{ + /// + /// 员工 ID。 + /// + public long StaffId { get; init; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 姓名。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 手机号。 + /// + public string Phone { get; init; } = string.Empty; + + /// + /// 邮箱。 + /// + public string? Email { get; init; } + + /// + /// 角色。 + /// + public StaffRoleType RoleType { get; init; } = StaffRoleType.FrontDesk; + + /// + /// 状态。 + /// + public StaffStatus Status { get; init; } = StaffStatus.Active; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreEmployeeShiftDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreEmployeeShiftDto.cs new file mode 100644 index 0000000..05c9866 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreEmployeeShiftDto.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 员工排班 DTO。 +/// +public sealed record StoreEmployeeShiftDto +{ + /// + /// 排班 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 StaffId { get; init; } + + /// + /// 班次日期。 + /// + public DateTime ShiftDate { get; init; } + + /// + /// 开始时间。 + /// + public TimeSpan StartTime { get; init; } + + /// + /// 结束时间。 + /// + public TimeSpan EndTime { get; init; } + + /// + /// 排班角色。 + /// + public StaffRoleType RoleType { get; init; } + + /// + /// 备注。 + /// + public string? Notes { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreStaffDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreStaffDto.cs new file mode 100644 index 0000000..a11cbc7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreStaffDto.cs @@ -0,0 +1,60 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 门店员工 DTO。 +/// +public sealed record StoreStaffDto +{ + /// + /// 员工 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 商户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long MerchantId { get; init; } + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? StoreId { get; init; } + + /// + /// 姓名。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 手机号。 + /// + public string Phone { get; init; } = string.Empty; + + /// + /// 邮箱。 + /// + public string? Email { get; init; } + + /// + /// 角色类型。 + /// + public StaffRoleType RoleType { get; init; } + + /// + /// 状态。 + /// + public StaffStatus Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreEmployeeShiftCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreEmployeeShiftCommandHandler.cs new file mode 100644 index 0000000..b47af84 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreEmployeeShiftCommandHandler.cs @@ -0,0 +1,72 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 创建排班处理器。 +/// +public sealed class CreateStoreEmployeeShiftCommandHandler( + IStoreRepository storeRepository, + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(CreateStoreEmployeeShiftCommand request, CancellationToken cancellationToken) + { + // 1. 校验门店存在 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); + } + + // 2. 校验员工归属与状态 + var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken); + if (staff is null || (staff.StoreId.HasValue && staff.StoreId != request.StoreId)) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "员工不存在或不属于该门店"); + } + + // 3. 校验日期与冲突 + var from = request.ShiftDate.Date; + var to = request.ShiftDate.Date; + var shifts = await storeRepository.GetShiftsAsync(request.StoreId, tenantId, from, to, cancellationToken); + var hasConflict = shifts.Any(x => x.StaffId == request.StaffId && x.ShiftDate == request.ShiftDate); + if (hasConflict) + { + throw new BusinessException(ErrorCodes.Conflict, "该员工当日已存在排班"); + } + + // 4. 构建实体 + var shift = new StoreEmployeeShift + { + StoreId = request.StoreId, + StaffId = request.StaffId, + ShiftDate = request.ShiftDate.Date, + StartTime = request.StartTime, + EndTime = request.EndTime, + RoleType = request.RoleType, + Notes = request.Notes?.Trim() + }; + + // 5. 持久化 + await storeRepository.AddShiftsAsync(new[] { shift }, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("创建排班 {ShiftId} 员工 {StaffId} 门店 {StoreId}", shift.Id, shift.StaffId, shift.StoreId); + + // 6. 返回 DTO + return StoreMapping.ToDto(shift); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreStaffCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreStaffCommandHandler.cs new file mode 100644 index 0000000..5ae4974 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreStaffCommandHandler.cs @@ -0,0 +1,56 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Entities; +using TakeoutSaaS.Domain.Merchants.Enums; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 创建门店员工处理器。 +/// +public sealed class CreateStoreStaffCommandHandler( + IMerchantRepository merchantRepository, + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(CreateStoreStaffCommand request, CancellationToken cancellationToken) + { + // 1. 校验门店 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); + } + + // 2. 组装员工 + var staff = new MerchantStaff + { + MerchantId = store.MerchantId, + StoreId = request.StoreId, + Name = request.Name.Trim(), + Phone = request.Phone.Trim(), + Email = request.Email?.Trim(), + RoleType = request.RoleType, + Status = StaffStatus.Active + }; + + // 3. 持久化 + await merchantRepository.AddStaffAsync(staff, cancellationToken); + await merchantRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("创建门店员工 {StaffId} 门店 {StoreId}", staff.Id, request.StoreId); + + // 4. 返回 DTO + return StoreMapping.ToDto(staff); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreEmployeeShiftCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreEmployeeShiftCommandHandler.cs new file mode 100644 index 0000000..d29a555 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreEmployeeShiftCommandHandler.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 DeleteStoreEmployeeShiftCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(DeleteStoreEmployeeShiftCommand request, CancellationToken cancellationToken) + { + // 1. 读取排班 + var tenantId = tenantProvider.GetCurrentTenantId(); + var shift = await storeRepository.FindShiftByIdAsync(request.ShiftId, tenantId, cancellationToken); + if (shift is null || shift.StoreId != request.StoreId) + { + return false; + } + + // 2. 删除 + await storeRepository.DeleteShiftAsync(request.ShiftId, tenantId, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("删除排班 {ShiftId} 门店 {StoreId}", request.ShiftId, request.StoreId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreStaffCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreStaffCommandHandler.cs new file mode 100644 index 0000000..5d438f8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreStaffCommandHandler.cs @@ -0,0 +1,35 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 删除门店员工处理器。 +/// +public sealed class DeleteStoreStaffCommandHandler( + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(DeleteStoreStaffCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken); + if (staff is null || staff.StoreId != request.StoreId) + { + return false; + } + + // 逻辑删除未定义,直接物理删除 + await merchantRepository.DeleteStaffAsync(staff.Id, tenantId, cancellationToken); + await merchantRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("删除门店员工 {StaffId} 门店 {StoreId}", request.StaffId, request.StoreId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreEmployeeShiftsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreEmployeeShiftsQueryHandler.cs new file mode 100644 index 0000000..2dde5dd --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreEmployeeShiftsQueryHandler.cs @@ -0,0 +1,37 @@ +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 排班列表查询处理器。 +/// +public sealed class ListStoreEmployeeShiftsQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + /// + public async Task> Handle(ListStoreEmployeeShiftsQuery request, CancellationToken cancellationToken) + { + // 1. 时间范围 + var from = request.From ?? DateTime.UtcNow.Date; + var to = request.To ?? from.AddDays(7); + var tenantId = tenantProvider.GetCurrentTenantId(); + + // 2. 查询排班 + var shifts = await storeRepository.GetShiftsAsync(request.StoreId, tenantId, from, to, cancellationToken); + + if (request.StaffId.HasValue) + { + shifts = shifts.Where(x => x.StaffId == request.StaffId.Value).ToList(); + } + + // 3. 映射 DTO + return shifts.Select(StoreMapping.ToDto).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreStaffQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreStaffQueryHandler.cs new file mode 100644 index 0000000..5a2e770 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreStaffQueryHandler.cs @@ -0,0 +1,47 @@ +using System.Linq; +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 门店员工列表查询处理器。 +/// +public sealed class ListStoreStaffQueryHandler( + IMerchantRepository merchantRepository, + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + /// + public async Task> Handle(ListStoreStaffQuery request, CancellationToken cancellationToken) + { + // 1. 校验门店存在 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + return []; + } + + // 2. 查询员工 + var staffs = await merchantRepository.GetStaffByStoreAsync(request.StoreId, tenantId, cancellationToken); + + if (request.RoleType.HasValue) + { + staffs = staffs.Where(x => x.RoleType == request.RoleType.Value).ToList(); + } + + if (request.Status.HasValue) + { + staffs = staffs.Where(x => x.Status == request.Status.Value).ToList(); + } + + // 3. 映射 DTO + return staffs.Select(StoreMapping.ToDto).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreEmployeeShiftCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreEmployeeShiftCommandHandler.cs new file mode 100644 index 0000000..ed2a975 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreEmployeeShiftCommandHandler.cs @@ -0,0 +1,71 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 更新排班处理器。 +/// +public sealed class UpdateStoreEmployeeShiftCommandHandler( + IStoreRepository storeRepository, + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(UpdateStoreEmployeeShiftCommand request, CancellationToken cancellationToken) + { + // 1. 读取排班 + var tenantId = tenantProvider.GetCurrentTenantId(); + var shift = await storeRepository.FindShiftByIdAsync(request.ShiftId, tenantId, cancellationToken); + if (shift is null) + { + return null; + } + + // 2. 校验门店归属 + if (shift.StoreId != request.StoreId) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "排班不属于该门店"); + } + + // 3. 校验员工归属 + var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken); + if (staff is null || (staff.StoreId.HasValue && staff.StoreId != request.StoreId)) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "员工不存在或不属于该门店"); + } + + // 4. 冲突校验 + var shifts = await storeRepository.GetShiftsAsync(request.StoreId, tenantId, request.ShiftDate.Date, request.ShiftDate.Date, cancellationToken); + var hasConflict = shifts.Any(x => x.Id != request.ShiftId && x.StaffId == request.StaffId && x.ShiftDate == request.ShiftDate); + if (hasConflict) + { + throw new BusinessException(ErrorCodes.Conflict, "该员工当日已存在排班"); + } + + // 5. 更新字段 + shift.StaffId = request.StaffId; + shift.ShiftDate = request.ShiftDate.Date; + shift.StartTime = request.StartTime; + shift.EndTime = request.EndTime; + shift.RoleType = request.RoleType; + shift.Notes = request.Notes?.Trim(); + + // 6. 持久化 + await storeRepository.UpdateShiftAsync(shift, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("更新排班 {ShiftId} 员工 {StaffId} 门店 {StoreId}", shift.Id, shift.StaffId, shift.StoreId); + + // 7. 返回 DTO + return StoreMapping.ToDto(shift); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreStaffCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreStaffCommandHandler.cs new file mode 100644 index 0000000..28c1321 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreStaffCommandHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 更新门店员工处理器。 +/// +public sealed class UpdateStoreStaffCommandHandler( + IMerchantRepository merchantRepository, + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + /// + public async Task Handle(UpdateStoreStaffCommand request, CancellationToken cancellationToken) + { + // 1. 校验门店 + var tenantId = tenantProvider.GetCurrentTenantId(); + var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (store is null) + { + return null; + } + + // 2. 读取员工 + var staff = await merchantRepository.FindStaffByIdAsync(request.StaffId, tenantId, cancellationToken); + if (staff is null || staff.StoreId != request.StoreId) + { + return null; + } + + // 3. 更新字段 + staff.Name = request.Name.Trim(); + staff.Phone = request.Phone.Trim(); + staff.Email = request.Email?.Trim(); + staff.RoleType = request.RoleType; + staff.Status = request.Status; + + // 4. 持久化 + await merchantRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("更新门店员工 {StaffId} 门店 {StoreId}", staff.Id, staff.StoreId); + + // 5. 返回 DTO + return StoreMapping.ToDto(staff); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreEmployeeShiftsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreEmployeeShiftsQuery.cs new file mode 100644 index 0000000..0684b06 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreEmployeeShiftsQuery.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 员工排班列表查询(支持日期区间)。 +/// +public sealed record ListStoreEmployeeShiftsQuery : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 开始日期(含),默认今日。 + /// + public DateTime? From { get; init; } + + /// + /// 结束日期(含),默认今日+7。 + /// + public DateTime? To { get; init; } + + /// + /// 可选员工筛选。 + /// + public long? StaffId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreStaffQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreStaffQuery.cs new file mode 100644 index 0000000..23004f9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/ListStoreStaffQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 门店员工列表查询。 +/// +public sealed record ListStoreStaffQuery : IRequest> +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 角色筛选。 + /// + public StaffRoleType? RoleType { get; init; } + + /// + /// 状态筛选。 + /// + public StaffStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs b/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs index 65f1be5..8a9a0d2 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs @@ -1,4 +1,5 @@ using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Merchants.Entities; using TakeoutSaaS.Domain.Stores.Entities; namespace TakeoutSaaS.Application.App.Stores; @@ -96,4 +97,41 @@ public static class StoreMapping QrCodeUrl = table.QrCodeUrl, CreatedAt = table.CreatedAt }; + + /// + /// 映射排班 DTO。 + /// + /// 排班实体。 + /// DTO。 + public static StoreEmployeeShiftDto ToDto(StoreEmployeeShift shift) => new() + { + Id = shift.Id, + TenantId = shift.TenantId, + StoreId = shift.StoreId, + StaffId = shift.StaffId, + ShiftDate = shift.ShiftDate, + StartTime = shift.StartTime, + EndTime = shift.EndTime, + RoleType = shift.RoleType, + Notes = shift.Notes, + CreatedAt = shift.CreatedAt + }; + + /// + /// 映射门店员工 DTO。 + /// + /// 员工实体。 + /// DTO。 + public static StoreStaffDto ToDto(MerchantStaff staff) => new() + { + Id = staff.Id, + TenantId = staff.TenantId, + MerchantId = staff.MerchantId, + StoreId = staff.StoreId, + Name = staff.Name, + Phone = staff.Phone, + Email = staff.Email, + RoleType = staff.RoleType, + Status = staff.Status + }; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreEmployeeShiftCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreEmployeeShiftCommandValidator.cs new file mode 100644 index 0000000..9a0cb21 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreEmployeeShiftCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 创建排班命令验证器。 +/// +public sealed class CreateStoreEmployeeShiftCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateStoreEmployeeShiftCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.StaffId).GreaterThan(0); + RuleFor(x => x.ShiftDate).NotEmpty(); + RuleFor(x => x.StartTime).LessThan(x => x.EndTime).WithMessage("结束时间必须晚于开始时间"); + RuleFor(x => x.Notes).MaximumLength(256); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreStaffCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreStaffCommandValidator.cs new file mode 100644 index 0000000..7e4e44b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreStaffCommandValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 创建门店员工命令验证器。 +/// +public sealed class CreateStoreStaffCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public CreateStoreStaffCommandValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(64); + RuleFor(x => x.Phone).NotEmpty().MaximumLength(32); + RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email)); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreEmployeeShiftCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreEmployeeShiftCommandValidator.cs new file mode 100644 index 0000000..53eb742 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreEmployeeShiftCommandValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 更新排班命令验证器。 +/// +public sealed class UpdateStoreEmployeeShiftCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateStoreEmployeeShiftCommandValidator() + { + RuleFor(x => x.ShiftId).GreaterThan(0); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.StaffId).GreaterThan(0); + RuleFor(x => x.ShiftDate).NotEmpty(); + RuleFor(x => x.StartTime).LessThan(x => x.EndTime).WithMessage("结束时间必须晚于开始时间"); + RuleFor(x => x.Notes).MaximumLength(256); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreStaffCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreStaffCommandValidator.cs new file mode 100644 index 0000000..27794d2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreStaffCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Stores.Commands; + +namespace TakeoutSaaS.Application.App.Stores.Validators; + +/// +/// 更新门店员工命令验证器。 +/// +public sealed class UpdateStoreStaffCommandValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public UpdateStoreStaffCommandValidator() + { + RuleFor(x => x.StaffId).GreaterThan(0); + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(64); + RuleFor(x => x.Phone).NotEmpty().MaximumLength(32); + RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email)); + } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs index c467a2e..0a746c6 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs @@ -26,6 +26,16 @@ public interface IMerchantRepository /// Task> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 获取指定门店的员工列表。 + /// + Task> GetStaffByStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + + /// + /// 依据标识获取员工。 + /// + Task FindStaffByIdAsync(long staffId, long tenantId, CancellationToken cancellationToken = default); + /// /// 获取指定商户的合同列表。 /// @@ -75,6 +85,11 @@ public interface IMerchantRepository /// Task DeleteMerchantAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 删除员工。 + /// + Task DeleteStaffAsync(long staffId, long tenantId, CancellationToken cancellationToken = default); + /// /// 记录审核日志。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs index 7ed73f1..a4d0146 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs @@ -72,9 +72,14 @@ public interface IStoreRepository Task FindTableByIdAsync(long tableId, long tenantId, CancellationToken cancellationToken = default); /// - /// 获取门店员工排班。 + /// 获取门店员工排班(可选时间范围)。 /// - Task> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + Task> GetShiftsAsync(long storeId, long tenantId, DateTime? from = null, DateTime? to = null, CancellationToken cancellationToken = default); + + /// + /// 依据标识获取排班。 + /// + Task FindShiftByIdAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default); /// /// 新增门店。 @@ -136,6 +141,11 @@ public interface IStoreRepository /// Task AddShiftsAsync(IEnumerable shifts, CancellationToken cancellationToken = default); + /// + /// 更新排班。 + /// + Task UpdateShiftAsync(StoreEmployeeShift shift, CancellationToken cancellationToken = default); + /// /// 持久化变更。 /// @@ -166,6 +176,11 @@ public interface IStoreRepository /// Task DeleteTableAsync(long tableId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 删除排班。 + /// + Task DeleteShiftAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default); + /// /// 更新门店。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs index 35e00a8..fa7f85c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs @@ -55,6 +55,26 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context) : IMerchan return staffs; } + /// + public async Task> GetStaffByStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + { + var staffs = await context.MerchantStaff + .AsNoTracking() + .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .OrderBy(x => x.Name) + .ToListAsync(cancellationToken); + + return staffs; + } + + /// + public Task FindStaffByIdAsync(long staffId, long tenantId, CancellationToken cancellationToken = default) + { + return context.MerchantStaff + .Where(x => x.TenantId == tenantId && x.Id == staffId) + .FirstOrDefaultAsync(cancellationToken); + } + /// public async Task> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) { @@ -161,6 +181,21 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context) : IMerchan context.Merchants.Remove(existing); } + /// + public async Task DeleteStaffAsync(long staffId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await context.MerchantStaff + .Where(x => x.TenantId == tenantId && x.Id == staffId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing == null) + { + return; + } + + context.MerchantStaff.Remove(existing); + } + /// public Task AddAuditLogAsync(MerchantAuditLog log, CancellationToken cancellationToken = default) { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs index 2e146e2..5fb6ebf 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -147,11 +147,23 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos } /// - public async Task> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public async Task> GetShiftsAsync(long storeId, long tenantId, DateTime? from = null, DateTime? to = null, CancellationToken cancellationToken = default) { - var shifts = await context.StoreEmployeeShifts + var query = context.StoreEmployeeShifts .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.StoreId == storeId) + .Where(x => x.TenantId == tenantId && x.StoreId == storeId); + + if (from.HasValue) + { + query = query.Where(x => x.ShiftDate >= from.Value.Date); + } + + if (to.HasValue) + { + query = query.Where(x => x.ShiftDate <= to.Value.Date); + } + + var shifts = await query .OrderBy(x => x.ShiftDate) .ThenBy(x => x.StartTime) .ToListAsync(cancellationToken); @@ -159,6 +171,14 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos return shifts; } + /// + public Task FindShiftByIdAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default) + { + return context.StoreEmployeeShifts + .Where(x => x.TenantId == tenantId && x.Id == shiftId) + .FirstOrDefaultAsync(cancellationToken); + } + /// public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default) { @@ -236,6 +256,13 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos return context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken); } + /// + public Task UpdateShiftAsync(StoreEmployeeShift shift, CancellationToken cancellationToken = default) + { + context.StoreEmployeeShifts.Update(shift); + return Task.CompletedTask; + } + /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) { @@ -307,6 +334,19 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos } } + /// + public async Task DeleteShiftAsync(long shiftId, long tenantId, CancellationToken cancellationToken = default) + { + var existing = await context.StoreEmployeeShifts + .Where(x => x.TenantId == tenantId && x.Id == shiftId) + .FirstOrDefaultAsync(cancellationToken); + + if (existing != null) + { + context.StoreEmployeeShifts.Remove(existing); + } + } + /// public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default) {