diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs new file mode 100644 index 0000000..a36e2e2 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs @@ -0,0 +1,108 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Application.App.Deliveries.Queries; +using TakeoutSaaS.Domain.Deliveries.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}/deliveries")] +public sealed class DeliveriesController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public DeliveriesController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建配送单。 + /// + [HttpPost] + [PermissionAuthorize("delivery:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateDeliveryOrderCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询配送单列表。 + /// + [HttpGet] + [PermissionAuthorize("delivery:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? orderId, [FromQuery] DeliveryStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchDeliveryOrdersQuery + { + OrderId = orderId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取配送单详情。 + /// + [HttpGet("{deliveryOrderId:long}")] + [PermissionAuthorize("delivery:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long deliveryOrderId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetDeliveryOrderByIdQuery { DeliveryOrderId = deliveryOrderId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新配送单。 + /// + [HttpPut("{deliveryOrderId:long}")] + [PermissionAuthorize("delivery:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long deliveryOrderId, [FromBody] UpdateDeliveryOrderCommand command, CancellationToken cancellationToken) + { + command.DeliveryOrderId = command.DeliveryOrderId == 0 ? deliveryOrderId : command.DeliveryOrderId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除配送单。 + /// + [HttpDelete("{deliveryOrderId:long}")] + [PermissionAuthorize("delivery:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long deliveryOrderId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteDeliveryOrderCommand { DeliveryOrderId = deliveryOrderId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "配送单不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs index cc11ec1..c24ec8d 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs @@ -55,6 +55,38 @@ public sealed class MerchantsController : BaseApiController return ApiResponse>.Ok(result); } + /// + /// 更新商户。 + /// + [HttpPut("{merchantId:long}")] + [PermissionAuthorize("merchant:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken) + { + command.MerchantId = command.MerchantId == 0 ? merchantId : command.MerchantId; + + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商户不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除商户。 + /// + [HttpDelete("{merchantId:long}")] + [PermissionAuthorize("merchant:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long merchantId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteMerchantCommand { MerchantId = merchantId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "商户不存在"); + } + /// /// 获取商户详情。 /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs new file mode 100644 index 0000000..f66b159 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs @@ -0,0 +1,116 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Application.App.Orders.Queries; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.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}/orders")] +public sealed class OrdersController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public OrdersController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建订单。 + /// + [HttpPost] + [PermissionAuthorize("order:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateOrderCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询订单列表。 + /// + [HttpGet] + [PermissionAuthorize("order:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List( + [FromQuery] long? storeId, + [FromQuery] OrderStatus? status, + [FromQuery] PaymentStatus? paymentStatus, + [FromQuery] string? orderNo, + CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchOrdersQuery + { + StoreId = storeId, + Status = status, + PaymentStatus = paymentStatus, + OrderNo = orderNo + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取订单详情。 + /// + [HttpGet("{orderId:long}")] + [PermissionAuthorize("order:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long orderId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetOrderByIdQuery { OrderId = orderId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "订单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新订单。 + /// + [HttpPut("{orderId:long}")] + [PermissionAuthorize("order:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long orderId, [FromBody] UpdateOrderCommand command, CancellationToken cancellationToken) + { + command.OrderId = command.OrderId == 0 ? orderId : command.OrderId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "订单不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除订单。 + /// + [HttpDelete("{orderId:long}")] + [PermissionAuthorize("order:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long orderId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteOrderCommand { OrderId = orderId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "订单不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs new file mode 100644 index 0000000..93ca700 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs @@ -0,0 +1,108 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.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}/payments")] +public sealed class PaymentsController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public PaymentsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建支付记录。 + /// + [HttpPost] + [PermissionAuthorize("payment:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreatePaymentCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询支付记录列表。 + /// + [HttpGet] + [PermissionAuthorize("payment:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? orderId, [FromQuery] PaymentStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchPaymentsQuery + { + OrderId = orderId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取支付记录详情。 + /// + [HttpGet("{paymentId:long}")] + [PermissionAuthorize("payment:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long paymentId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetPaymentByIdQuery { PaymentId = paymentId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新支付记录。 + /// + [HttpPut("{paymentId:long}")] + [PermissionAuthorize("payment:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long paymentId, [FromBody] UpdatePaymentCommand command, CancellationToken cancellationToken) + { + command.PaymentId = command.PaymentId == 0 ? paymentId : command.PaymentId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除支付记录。 + /// + [HttpDelete("{paymentId:long}")] + [PermissionAuthorize("payment:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long paymentId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeletePaymentCommand { PaymentId = paymentId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "支付记录不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs new file mode 100644 index 0000000..4c8d42c --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs @@ -0,0 +1,109 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Application.App.Products.Queries; +using TakeoutSaaS.Domain.Products.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}/products")] +public sealed class ProductsController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public ProductsController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建商品。 + /// + [HttpPost] + [PermissionAuthorize("product:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateProductCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询商品列表。 + /// + [HttpGet] + [PermissionAuthorize("product:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? storeId, [FromQuery] long? categoryId, [FromQuery] ProductStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchProductsQuery + { + StoreId = storeId, + CategoryId = categoryId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取商品详情。 + /// + [HttpGet("{productId:long}")] + [PermissionAuthorize("product:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long productId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetProductByIdQuery { ProductId = productId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商品不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新商品。 + /// + [HttpPut("{productId:long}")] + [PermissionAuthorize("product:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long productId, [FromBody] UpdateProductCommand command, CancellationToken cancellationToken) + { + command.ProductId = command.ProductId == 0 ? productId : command.ProductId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "商品不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除商品。 + /// + [HttpDelete("{productId:long}")] + [PermissionAuthorize("product:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long productId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteProductCommand { ProductId = productId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "商品不存在"); + } +} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs new file mode 100644 index 0000000..e485df6 --- /dev/null +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs @@ -0,0 +1,108 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Stores.Commands; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.AdminApi.Controllers; + +/// +/// 门店管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/admin/v{version:apiVersion}/stores")] +public sealed class StoresController : BaseApiController +{ + private readonly IMediator _mediator; + + /// + /// 初始化控制器。 + /// + public StoresController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// 创建门店。 + /// + [HttpPost] + [PermissionAuthorize("store:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Create([FromBody] CreateStoreCommand command, CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 查询门店列表。 + /// + [HttpGet] + [PermissionAuthorize("store:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> List([FromQuery] long? merchantId, [FromQuery] StoreStatus? status, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new SearchStoresQuery + { + MerchantId = merchantId, + Status = status + }, cancellationToken); + + return ApiResponse>.Ok(result); + } + + /// + /// 获取门店详情。 + /// + [HttpGet("{storeId:long}")] + [PermissionAuthorize("store:read")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Detail(long storeId, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "门店不存在") + : ApiResponse.Ok(result); + } + + /// + /// 更新门店。 + /// + [HttpPut("{storeId:long}")] + [PermissionAuthorize("store:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Update(long storeId, [FromBody] UpdateStoreCommand command, CancellationToken cancellationToken) + { + command.StoreId = command.StoreId == 0 ? storeId : command.StoreId; + var result = await _mediator.Send(command, cancellationToken); + return result == null + ? ApiResponse.Error(ErrorCodes.NotFound, "门店不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除门店。 + /// + [HttpDelete("{storeId:long}")] + [PermissionAuthorize("store:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> Delete(long storeId, CancellationToken cancellationToken) + { + var success = await _mediator.Send(new DeleteStoreCommand { StoreId = storeId }, cancellationToken); + return success + ? ApiResponse.Ok(null) + : ApiResponse.Error(ErrorCodes.NotFound, "门店不存在"); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs new file mode 100644 index 0000000..8f5a2b7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/CreateDeliveryOrderCommand.cs @@ -0,0 +1,66 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Enums; + +namespace TakeoutSaaS.Application.App.Deliveries.Commands; + +/// +/// 创建配送单命令。 +/// +public sealed class CreateDeliveryOrderCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 服务商。 + /// + public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse; + + /// + /// 第三方单号。 + /// + public string? ProviderOrderId { get; set; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending; + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; set; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; set; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; set; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; set; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; set; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs new file mode 100644 index 0000000..b05162d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/DeleteDeliveryOrderCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Deliveries.Commands; + +/// +/// 删除配送单命令。 +/// +public sealed class DeleteDeliveryOrderCommand : IRequest +{ + /// + /// 配送单 ID。 + /// + public long DeliveryOrderId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs new file mode 100644 index 0000000..cf57bbb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Commands/UpdateDeliveryOrderCommand.cs @@ -0,0 +1,71 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Enums; + +namespace TakeoutSaaS.Application.App.Deliveries.Commands; + +/// +/// 更新配送单命令。 +/// +public sealed class UpdateDeliveryOrderCommand : IRequest +{ + /// + /// 配送单 ID。 + /// + public long DeliveryOrderId { get; set; } + + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 服务商。 + /// + public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse; + + /// + /// 第三方单号。 + /// + public string? ProviderOrderId { get; set; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending; + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; set; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; set; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; set; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; set; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; set; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs new file mode 100644 index 0000000..a8d2a7e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryEventDto.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Deliveries.Dto; + +/// +/// 配送事件 DTO。 +/// +public sealed class DeliveryEventDto +{ + /// + /// 事件 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 配送单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long DeliveryOrderId { get; init; } + + /// + /// 事件类型。 + /// + public DeliveryEventType EventType { get; init; } + + /// + /// 描述。 + /// + public string? Message { get; init; } + + /// + /// 事件时间。 + /// + public DateTime OccurredAt { get; init; } + + /// + /// 原始载荷。 + /// + public string? Payload { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs new file mode 100644 index 0000000..21fc36c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Dto/DeliveryOrderDto.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Deliveries.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Deliveries.Dto; + +/// +/// 配送单 DTO。 +/// +public sealed class DeliveryOrderDto +{ + /// + /// 配送单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 配送服务商。 + /// + public DeliveryProvider Provider { get; init; } + + /// + /// 第三方配送单号。 + /// + public string? ProviderOrderId { get; init; } + + /// + /// 状态。 + /// + public DeliveryStatus Status { get; init; } + + /// + /// 配送费。 + /// + public decimal? DeliveryFee { get; init; } + + /// + /// 骑手姓名。 + /// + public string? CourierName { get; init; } + + /// + /// 骑手电话。 + /// + public string? CourierPhone { get; init; } + + /// + /// 下发时间。 + /// + public DateTime? DispatchedAt { get; init; } + + /// + /// 取餐时间。 + /// + public DateTime? PickedUpAt { get; init; } + + /// + /// 完成时间。 + /// + public DateTime? DeliveredAt { get; init; } + + /// + /// 异常原因。 + /// + public string? FailureReason { get; init; } + + /// + /// 事件列表。 + /// + public IReadOnlyList Events { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs new file mode 100644 index 0000000..8e775e9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/CreateDeliveryOrderCommandHandler.cs @@ -0,0 +1,69 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 创建配送单命令处理器。 +/// +public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository deliveryRepository, ILogger logger) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateDeliveryOrderCommand request, CancellationToken cancellationToken) + { + var deliveryOrder = new DeliveryOrder + { + OrderId = request.OrderId, + Provider = request.Provider, + ProviderOrderId = request.ProviderOrderId?.Trim(), + Status = request.Status, + DeliveryFee = request.DeliveryFee, + CourierName = request.CourierName?.Trim(), + CourierPhone = request.CourierPhone?.Trim(), + DispatchedAt = request.DispatchedAt, + PickedUpAt = request.PickedUpAt, + DeliveredAt = request.DeliveredAt, + FailureReason = request.FailureReason?.Trim() + }; + + await _deliveryRepository.AddDeliveryOrderAsync(deliveryOrder, cancellationToken); + await _deliveryRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建配送单 {DeliveryOrderId} 对应订单 {OrderId}", deliveryOrder.Id, deliveryOrder.OrderId); + + return MapToDto(deliveryOrder, []); + } + + private static DeliveryOrderDto MapToDto(DeliveryOrder deliveryOrder, IReadOnlyList events) => new() + { + Id = deliveryOrder.Id, + TenantId = deliveryOrder.TenantId, + OrderId = deliveryOrder.OrderId, + Provider = deliveryOrder.Provider, + ProviderOrderId = deliveryOrder.ProviderOrderId, + Status = deliveryOrder.Status, + DeliveryFee = deliveryOrder.DeliveryFee, + CourierName = deliveryOrder.CourierName, + CourierPhone = deliveryOrder.CourierPhone, + DispatchedAt = deliveryOrder.DispatchedAt, + PickedUpAt = deliveryOrder.PickedUpAt, + DeliveredAt = deliveryOrder.DeliveredAt, + FailureReason = deliveryOrder.FailureReason, + Events = events.Select(x => new DeliveryEventDto + { + Id = x.Id, + DeliveryOrderId = x.DeliveryOrderId, + EventType = x.EventType, + Message = x.Message, + OccurredAt = x.OccurredAt, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs new file mode 100644 index 0000000..40974f1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/DeleteDeliveryOrderCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 删除配送单命令处理器。 +/// +public sealed class DeleteDeliveryOrderCommandHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteDeliveryOrderCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + await _deliveryRepository.DeleteDeliveryOrderAsync(request.DeliveryOrderId, tenantId, cancellationToken); + await _deliveryRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除配送单 {DeliveryOrderId}", request.DeliveryOrderId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs new file mode 100644 index 0000000..8d047de --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/GetDeliveryOrderByIdQueryHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Application.App.Deliveries.Queries; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 配送单详情查询处理器。 +/// +public sealed class GetDeliveryOrderByIdQueryHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetDeliveryOrderByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var order = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken); + if (order == null) + { + return null; + } + + var events = await _deliveryRepository.GetEventsAsync(order.Id, tenantId, cancellationToken); + return MapToDto(order, events); + } + + private static DeliveryOrderDto MapToDto(DeliveryOrder deliveryOrder, IReadOnlyList events) => new() + { + Id = deliveryOrder.Id, + TenantId = deliveryOrder.TenantId, + OrderId = deliveryOrder.OrderId, + Provider = deliveryOrder.Provider, + ProviderOrderId = deliveryOrder.ProviderOrderId, + Status = deliveryOrder.Status, + DeliveryFee = deliveryOrder.DeliveryFee, + CourierName = deliveryOrder.CourierName, + CourierPhone = deliveryOrder.CourierPhone, + DispatchedAt = deliveryOrder.DispatchedAt, + PickedUpAt = deliveryOrder.PickedUpAt, + DeliveredAt = deliveryOrder.DeliveredAt, + FailureReason = deliveryOrder.FailureReason, + Events = events.Select(x => new DeliveryEventDto + { + Id = x.Id, + DeliveryOrderId = x.DeliveryOrderId, + EventType = x.EventType, + Message = x.Message, + OccurredAt = x.OccurredAt, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs new file mode 100644 index 0000000..5dea6a9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/SearchDeliveryOrdersQueryHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Application.App.Deliveries.Queries; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 配送单列表查询处理器。 +/// +public sealed class SearchDeliveryOrdersQueryHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchDeliveryOrdersQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var orders = await _deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken); + + return orders.Select(order => new DeliveryOrderDto + { + Id = order.Id, + TenantId = order.TenantId, + OrderId = order.OrderId, + Provider = order.Provider, + ProviderOrderId = order.ProviderOrderId, + Status = order.Status, + DeliveryFee = order.DeliveryFee, + CourierName = order.CourierName, + CourierPhone = order.CourierPhone, + DispatchedAt = order.DispatchedAt, + PickedUpAt = order.PickedUpAt, + DeliveredAt = order.DeliveredAt, + FailureReason = order.FailureReason + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs new file mode 100644 index 0000000..424e2a9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Handlers/UpdateDeliveryOrderCommandHandler.cs @@ -0,0 +1,79 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Deliveries.Commands; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Deliveries.Handlers; + +/// +/// 更新配送单命令处理器。 +/// +public sealed class UpdateDeliveryOrderCommandHandler( + IDeliveryRepository deliveryRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IDeliveryRepository _deliveryRepository = deliveryRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateDeliveryOrderCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + existing.OrderId = request.OrderId; + existing.Provider = request.Provider; + existing.ProviderOrderId = request.ProviderOrderId?.Trim(); + existing.Status = request.Status; + existing.DeliveryFee = request.DeliveryFee; + existing.CourierName = request.CourierName?.Trim(); + existing.CourierPhone = request.CourierPhone?.Trim(); + existing.DispatchedAt = request.DispatchedAt; + existing.PickedUpAt = request.PickedUpAt; + existing.DeliveredAt = request.DeliveredAt; + existing.FailureReason = request.FailureReason?.Trim(); + + await _deliveryRepository.UpdateDeliveryOrderAsync(existing, cancellationToken); + await _deliveryRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新配送单 {DeliveryOrderId}", existing.Id); + + var events = await _deliveryRepository.GetEventsAsync(existing.Id, tenantId, cancellationToken); + return MapToDto(existing, events); + } + + private static DeliveryOrderDto MapToDto(DeliveryOrder deliveryOrder, IReadOnlyList events) => new() + { + Id = deliveryOrder.Id, + TenantId = deliveryOrder.TenantId, + OrderId = deliveryOrder.OrderId, + Provider = deliveryOrder.Provider, + ProviderOrderId = deliveryOrder.ProviderOrderId, + Status = deliveryOrder.Status, + DeliveryFee = deliveryOrder.DeliveryFee, + CourierName = deliveryOrder.CourierName, + CourierPhone = deliveryOrder.CourierPhone, + DispatchedAt = deliveryOrder.DispatchedAt, + PickedUpAt = deliveryOrder.PickedUpAt, + DeliveredAt = deliveryOrder.DeliveredAt, + FailureReason = deliveryOrder.FailureReason, + Events = events.Select(x => new DeliveryEventDto + { + Id = x.Id, + DeliveryOrderId = x.DeliveryOrderId, + EventType = x.EventType, + Message = x.Message, + OccurredAt = x.OccurredAt, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs new file mode 100644 index 0000000..0b89cae --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/GetDeliveryOrderByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; + +namespace TakeoutSaaS.Application.App.Deliveries.Queries; + +/// +/// 配送单详情查询。 +/// +public sealed class GetDeliveryOrderByIdQuery : IRequest +{ + /// + /// 配送单 ID。 + /// + public long DeliveryOrderId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs new file mode 100644 index 0000000..53d439c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Deliveries/Queries/SearchDeliveryOrdersQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Deliveries.Dto; +using TakeoutSaaS.Domain.Deliveries.Enums; + +namespace TakeoutSaaS.Application.App.Deliveries.Queries; + +/// +/// 配送单列表查询。 +/// +public sealed class SearchDeliveryOrdersQuery : IRequest> +{ + /// + /// 订单 ID(可选)。 + /// + public long? OrderId { get; init; } + + /// + /// 配送状态。 + /// + public DeliveryStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs new file mode 100644 index 0000000..415665c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/DeleteMerchantCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Merchants.Commands; + +/// +/// 删除商户命令。 +/// +public sealed class DeleteMerchantCommand : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs new file mode 100644 index 0000000..2b73adc --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs @@ -0,0 +1,51 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Enums; + +namespace TakeoutSaaS.Application.App.Merchants.Commands; + +/// +/// 更新商户命令。 +/// +public sealed class UpdateMerchantCommand : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; set; } + + /// + /// 品牌名称。 + /// + public string BrandName { get; set; } = string.Empty; + + /// + /// 品牌简称。 + /// + public string? BrandAlias { get; set; } + + /// + /// Logo 地址。 + /// + public string? LogoUrl { get; set; } + + /// + /// 品类。 + /// + public string? Category { get; set; } + + /// + /// 联系电话。 + /// + public string ContactPhone { get; set; } = string.Empty; + + /// + /// 联系邮箱。 + /// + public string? ContactEmail { get; set; } + + /// + /// 入驻状态。 + /// + public MerchantStatus Status { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs new file mode 100644 index 0000000..8f69ff0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/DeleteMerchantCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Merchants.Commands; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 删除商户命令处理器。 +/// +public sealed class DeleteMerchantCommandHandler( + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteMerchantCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _merchantRepository.DeleteMerchantAsync(request.MerchantId, tenantId, cancellationToken); + await _merchantRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除商户 {MerchantId}", request.MerchantId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs new file mode 100644 index 0000000..d825b72 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs @@ -0,0 +1,65 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Merchants.Commands; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Domain.Merchants.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Merchants.Handlers; + +/// +/// 更新商户命令处理器。 +/// +public sealed class UpdateMerchantCommandHandler( + IMerchantRepository merchantRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IMerchantRepository _merchantRepository = merchantRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateMerchantCommand request, CancellationToken cancellationToken) + { + // 1. 读取现有商户 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.BrandName = request.BrandName.Trim(); + existing.BrandAlias = request.BrandAlias?.Trim(); + existing.LogoUrl = request.LogoUrl?.Trim(); + existing.Category = request.Category?.Trim(); + existing.ContactPhone = request.ContactPhone.Trim(); + existing.ContactEmail = request.ContactEmail?.Trim(); + existing.Status = request.Status; + + // 3. 持久化 + await _merchantRepository.UpdateMerchantAsync(existing, cancellationToken); + await _merchantRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新商户 {MerchantId} - {BrandName}", existing.Id, existing.BrandName); + + // 4. 返回 DTO + return MapToDto(existing); + } + + private static MerchantDto MapToDto(Domain.Merchants.Entities.Merchant merchant) => new() + { + Id = merchant.Id, + TenantId = merchant.TenantId, + BrandName = merchant.BrandName, + BrandAlias = merchant.BrandAlias, + LogoUrl = merchant.LogoUrl, + Category = merchant.Category, + ContactPhone = merchant.ContactPhone, + ContactEmail = merchant.ContactEmail, + Status = merchant.Status, + JoinedAt = merchant.JoinedAt + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs new file mode 100644 index 0000000..824de33 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/CreateOrderCommand.cs @@ -0,0 +1,117 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 创建订单命令。 +/// +public sealed class CreateOrderCommand : IRequest +{ + /// + /// 订单号。 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 渠道。 + /// + public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram; + + /// + /// 履约方式。 + /// + public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn; + + /// + /// 状态。 + /// + public OrderStatus Status { get; set; } = OrderStatus.PendingPayment; + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid; + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; set; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; set; } + + /// + /// 桌号。 + /// + public string? TableNo { get; set; } + + /// + /// 排队号。 + /// + public string? QueueNumber { get; set; } + + /// + /// 预约 ID。 + /// + public long? ReservationId { get; set; } + + /// + /// 商品金额。 + /// + public decimal ItemsAmount { get; set; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; set; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 明细。 + /// + public IReadOnlyList Items { get; set; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs new file mode 100644 index 0000000..f7632c6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/DeleteOrderCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 删除订单命令。 +/// +public sealed class DeleteOrderCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs new file mode 100644 index 0000000..544cd85 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/OrderItemRequest.cs @@ -0,0 +1,52 @@ +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 订单明细请求。 +/// +public sealed class OrderItemRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; set; } + + /// + /// 商品名称。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// SKU 描述。 + /// + public string? SkuName { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 数量。 + /// + public int Quantity { get; set; } + + /// + /// 单价。 + /// + public decimal UnitPrice { get; set; } + + /// + /// 折扣金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 小计。 + /// + public decimal SubTotal { get; set; } + + /// + /// 属性 JSON。 + /// + public string? AttributesJson { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs new file mode 100644 index 0000000..a9c832a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Commands/UpdateOrderCommand.cs @@ -0,0 +1,117 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Orders.Commands; + +/// +/// 更新订单命令。 +/// +public sealed class UpdateOrderCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 订单号。 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 渠道。 + /// + public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram; + + /// + /// 履约方式。 + /// + public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn; + + /// + /// 状态。 + /// + public OrderStatus Status { get; set; } = OrderStatus.PendingPayment; + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid; + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; set; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; set; } + + /// + /// 桌号。 + /// + public string? TableNo { get; set; } + + /// + /// 排队号。 + /// + public string? QueueNumber { get; set; } + + /// + /// 预约 ID。 + /// + public long? ReservationId { get; set; } + + /// + /// 商品金额。 + /// + public decimal ItemsAmount { get; set; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; set; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; set; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; set; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs new file mode 100644 index 0000000..0358f7c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderDto.cs @@ -0,0 +1,141 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 订单 DTO。 +/// +public sealed class OrderDto +{ + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 订单号。 + /// + public string OrderNo { get; init; } = string.Empty; + + /// + /// 门店 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long StoreId { get; init; } + + /// + /// 渠道。 + /// + public OrderChannel Channel { get; init; } + + /// + /// 履约方式。 + /// + public DeliveryType DeliveryType { get; init; } + + /// + /// 状态。 + /// + public OrderStatus Status { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus PaymentStatus { get; init; } + + /// + /// 顾客姓名。 + /// + public string? CustomerName { get; init; } + + /// + /// 顾客手机号。 + /// + public string? CustomerPhone { get; init; } + + /// + /// 桌号。 + /// + public string? TableNo { get; init; } + + /// + /// 排队号。 + /// + public string? QueueNumber { get; init; } + + /// + /// 预约 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? ReservationId { get; init; } + + /// + /// 商品金额。 + /// + public decimal ItemsAmount { get; init; } + + /// + /// 优惠金额。 + /// + public decimal DiscountAmount { get; init; } + + /// + /// 应付金额。 + /// + public decimal PayableAmount { get; init; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; init; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; init; } + + /// + /// 完成时间。 + /// + public DateTime? FinishedAt { get; init; } + + /// + /// 取消时间。 + /// + public DateTime? CancelledAt { get; init; } + + /// + /// 取消原因。 + /// + public string? CancelReason { get; init; } + + /// + /// 备注。 + /// + public string? Remark { get; init; } + + /// + /// 明细。 + /// + public IReadOnlyList Items { get; init; } = Array.Empty(); + + /// + /// 状态流转。 + /// + public IReadOnlyList StatusHistory { get; init; } = Array.Empty(); + + /// + /// 退款申请。 + /// + public IReadOnlyList Refunds { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs new file mode 100644 index 0000000..6baa720 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderItemDto.cs @@ -0,0 +1,68 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 订单明细 DTO。 +/// +public sealed class OrderItemDto +{ + /// + /// 明细 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 商品 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long ProductId { get; init; } + + /// + /// 商品名称。 + /// + public string ProductName { get; init; } = string.Empty; + + /// + /// SKU 描述。 + /// + public string? SkuName { get; init; } + + /// + /// 单位。 + /// + public string? Unit { get; init; } + + /// + /// 数量。 + /// + public int Quantity { get; init; } + + /// + /// 单价。 + /// + public decimal UnitPrice { get; init; } + + /// + /// 折扣金额。 + /// + public decimal DiscountAmount { get; init; } + + /// + /// 小计。 + /// + public decimal SubTotal { get; init; } + + /// + /// 属性 JSON。 + /// + public string? AttributesJson { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs new file mode 100644 index 0000000..e62e45e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/OrderStatusHistoryDto.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 订单状态流转 DTO。 +/// +public sealed class OrderStatusHistoryDto +{ + /// + /// 记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 状态。 + /// + public OrderStatus Status { get; init; } + + /// + /// 操作人。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long? OperatorId { get; init; } + + /// + /// 备注。 + /// + public string? Notes { get; init; } + + /// + /// 时间。 + /// + public DateTime OccurredAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs new file mode 100644 index 0000000..d389554 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Dto/RefundRequestDto.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Orders.Dto; + +/// +/// 退款申请 DTO。 +/// +public sealed class RefundRequestDto +{ + /// + /// 退款 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 退款单号。 + /// + public string RefundNo { get; init; } = string.Empty; + + /// + /// 金额。 + /// + public decimal Amount { get; init; } + + /// + /// 原因。 + /// + public string Reason { get; init; } = string.Empty; + + /// + /// 状态。 + /// + public RefundStatus Status { get; init; } + + /// + /// 申请时间。 + /// + public DateTime RequestedAt { get; init; } + + /// + /// 处理时间。 + /// + public DateTime? ProcessedAt { get; init; } + + /// + /// 审核备注。 + /// + public string? ReviewNotes { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs new file mode 100644 index 0000000..057b018 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/CreateOrderCommandHandler.cs @@ -0,0 +1,156 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 创建订单命令处理器。 +/// +public sealed class CreateOrderCommandHandler( + IOrderRepository orderRepository, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly IIdGenerator _idGenerator = idGenerator; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken) + { + // 1. 构建订单 + var order = new Order + { + Id = _idGenerator.NextId(), + OrderNo = request.OrderNo.Trim(), + StoreId = request.StoreId, + Channel = request.Channel, + DeliveryType = request.DeliveryType, + Status = request.Status, + PaymentStatus = request.PaymentStatus, + CustomerName = request.CustomerName?.Trim(), + CustomerPhone = request.CustomerPhone?.Trim(), + TableNo = request.TableNo?.Trim(), + QueueNumber = request.QueueNumber?.Trim(), + ReservationId = request.ReservationId, + ItemsAmount = request.ItemsAmount, + DiscountAmount = request.DiscountAmount, + PayableAmount = request.PayableAmount, + PaidAmount = request.PaidAmount, + PaidAt = request.PaidAt, + FinishedAt = request.FinishedAt, + CancelledAt = request.CancelledAt, + CancelReason = request.CancelReason?.Trim(), + Remark = request.Remark?.Trim() + }; + + // 2. 构建明细 + var items = request.Items.Select(item => new OrderItem + { + OrderId = order.Id, + ProductId = item.ProductId, + ProductName = item.ProductName.Trim(), + SkuName = item.SkuName?.Trim(), + Unit = item.Unit?.Trim(), + Quantity = item.Quantity, + UnitPrice = item.UnitPrice, + DiscountAmount = item.DiscountAmount, + SubTotal = item.SubTotal, + AttributesJson = item.AttributesJson?.Trim() + }).ToList(); + + // 3. 补充金额字段 + if (items.Count > 0) + { + var itemsAmount = items.Sum(x => x.SubTotal); + order.ItemsAmount = itemsAmount; + if (order.PayableAmount <= 0) + { + order.PayableAmount = itemsAmount - order.DiscountAmount; + } + } + + // 4. 持久化 + await _orderRepository.AddOrderAsync(order, cancellationToken); + if (items.Count > 0) + { + await _orderRepository.AddItemsAsync(items, cancellationToken); + } + await _orderRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id); + + // 5. 返回 DTO + return MapToDto(order, items, [], []); + } + + private static OrderDto MapToDto( + Order order, + IReadOnlyList items, + IReadOnlyList histories, + IReadOnlyList refunds) => new() + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark, + Items = items.Select(x => new OrderItemDto + { + Id = x.Id, + OrderId = x.OrderId, + ProductId = x.ProductId, + ProductName = x.ProductName, + SkuName = x.SkuName, + Unit = x.Unit, + Quantity = x.Quantity, + UnitPrice = x.UnitPrice, + DiscountAmount = x.DiscountAmount, + SubTotal = x.SubTotal, + AttributesJson = x.AttributesJson + }).ToList(), + StatusHistory = histories.Select(x => new OrderStatusHistoryDto + { + Id = x.Id, + OrderId = x.OrderId, + Status = x.Status, + OperatorId = x.OperatorId, + Notes = x.Notes, + OccurredAt = x.OccurredAt + }).ToList(), + Refunds = refunds.Select(x => new RefundRequestDto + { + Id = x.Id, + OrderId = x.OrderId, + RefundNo = x.RefundNo, + Amount = x.Amount, + Reason = x.Reason, + Status = x.Status, + RequestedAt = x.RequestedAt, + ProcessedAt = x.ProcessedAt, + ReviewNotes = x.ReviewNotes + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs new file mode 100644 index 0000000..d376e47 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/DeleteOrderCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 删除订单命令处理器。 +/// +public sealed class DeleteOrderCommandHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteOrderCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _orderRepository.DeleteOrderAsync(request.OrderId, tenantId, cancellationToken); + await _orderRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除订单 {OrderId}", request.OrderId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs new file mode 100644 index 0000000..dbdd1c2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/GetOrderByIdQueryHandler.cs @@ -0,0 +1,102 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Application.App.Orders.Queries; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 订单详情查询处理器。 +/// +public sealed class GetOrderByIdQueryHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var order = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken); + if (order == null) + { + return null; + } + + var items = await _orderRepository.GetItemsAsync(order.Id, tenantId, cancellationToken); + var histories = await _orderRepository.GetStatusHistoryAsync(order.Id, tenantId, cancellationToken); + var refunds = await _orderRepository.GetRefundsAsync(order.Id, tenantId, cancellationToken); + + return MapToDto(order, items, histories, refunds); + } + + private static OrderDto MapToDto( + Order order, + IReadOnlyList items, + IReadOnlyList histories, + IReadOnlyList refunds) => new() + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark, + Items = items.Select(x => new OrderItemDto + { + Id = x.Id, + OrderId = x.OrderId, + ProductId = x.ProductId, + ProductName = x.ProductName, + SkuName = x.SkuName, + Unit = x.Unit, + Quantity = x.Quantity, + UnitPrice = x.UnitPrice, + DiscountAmount = x.DiscountAmount, + SubTotal = x.SubTotal, + AttributesJson = x.AttributesJson + }).ToList(), + StatusHistory = histories.Select(x => new OrderStatusHistoryDto + { + Id = x.Id, + OrderId = x.OrderId, + Status = x.Status, + OperatorId = x.OperatorId, + Notes = x.Notes, + OccurredAt = x.OccurredAt + }).ToList(), + Refunds = refunds.Select(x => new RefundRequestDto + { + Id = x.Id, + OrderId = x.OrderId, + RefundNo = x.RefundNo, + Amount = x.Amount, + Reason = x.Reason, + Status = x.Status, + RequestedAt = x.RequestedAt, + ProcessedAt = x.ProcessedAt, + ReviewNotes = x.ReviewNotes + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs new file mode 100644 index 0000000..d1b054a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/SearchOrdersQueryHandler.cs @@ -0,0 +1,65 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Application.App.Orders.Queries; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 订单列表查询处理器。 +/// +public sealed class SearchOrdersQueryHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchOrdersQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var orders = await _orderRepository.SearchAsync(tenantId, request.Status, request.PaymentStatus, cancellationToken); + + if (request.StoreId.HasValue) + { + orders = orders.Where(x => x.StoreId == request.StoreId.Value).ToList(); + } + + if (!string.IsNullOrWhiteSpace(request.OrderNo)) + { + var orderNo = request.OrderNo.Trim(); + orders = orders + .Where(x => x.OrderNo.Contains(orderNo, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + return orders.Select(order => new OrderDto + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs new file mode 100644 index 0000000..3901fd9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Handlers/UpdateOrderCommandHandler.cs @@ -0,0 +1,134 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Orders.Commands; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Entities; +using TakeoutSaaS.Domain.Orders.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Orders.Handlers; + +/// +/// 更新订单命令处理器。 +/// +public sealed class UpdateOrderCommandHandler( + IOrderRepository orderRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateOrderCommand request, CancellationToken cancellationToken) + { + // 1. 读取订单 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.OrderNo = request.OrderNo.Trim(); + existing.StoreId = request.StoreId; + existing.Channel = request.Channel; + existing.DeliveryType = request.DeliveryType; + existing.Status = request.Status; + existing.PaymentStatus = request.PaymentStatus; + existing.CustomerName = request.CustomerName?.Trim(); + existing.CustomerPhone = request.CustomerPhone?.Trim(); + existing.TableNo = request.TableNo?.Trim(); + existing.QueueNumber = request.QueueNumber?.Trim(); + existing.ReservationId = request.ReservationId; + existing.ItemsAmount = request.ItemsAmount; + existing.DiscountAmount = request.DiscountAmount; + existing.PayableAmount = request.PayableAmount; + existing.PaidAmount = request.PaidAmount; + existing.PaidAt = request.PaidAt; + existing.FinishedAt = request.FinishedAt; + existing.CancelledAt = request.CancelledAt; + existing.CancelReason = request.CancelReason?.Trim(); + existing.Remark = request.Remark?.Trim(); + + // 3. 持久化 + await _orderRepository.UpdateOrderAsync(existing, cancellationToken); + await _orderRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新订单 {OrderNo} ({OrderId})", existing.OrderNo, existing.Id); + + // 4. 读取关联数据并返回 + var items = await _orderRepository.GetItemsAsync(existing.Id, tenantId, cancellationToken); + var histories = await _orderRepository.GetStatusHistoryAsync(existing.Id, tenantId, cancellationToken); + var refunds = await _orderRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken); + + return MapToDto(existing, items, histories, refunds); + } + + private static OrderDto MapToDto( + Order order, + IReadOnlyList items, + IReadOnlyList histories, + IReadOnlyList refunds) => new() + { + Id = order.Id, + TenantId = order.TenantId, + OrderNo = order.OrderNo, + StoreId = order.StoreId, + Channel = order.Channel, + DeliveryType = order.DeliveryType, + Status = order.Status, + PaymentStatus = order.PaymentStatus, + CustomerName = order.CustomerName, + CustomerPhone = order.CustomerPhone, + TableNo = order.TableNo, + QueueNumber = order.QueueNumber, + ReservationId = order.ReservationId, + ItemsAmount = order.ItemsAmount, + DiscountAmount = order.DiscountAmount, + PayableAmount = order.PayableAmount, + PaidAmount = order.PaidAmount, + PaidAt = order.PaidAt, + FinishedAt = order.FinishedAt, + CancelledAt = order.CancelledAt, + CancelReason = order.CancelReason, + Remark = order.Remark, + Items = items.Select(x => new OrderItemDto + { + Id = x.Id, + OrderId = x.OrderId, + ProductId = x.ProductId, + ProductName = x.ProductName, + SkuName = x.SkuName, + Unit = x.Unit, + Quantity = x.Quantity, + UnitPrice = x.UnitPrice, + DiscountAmount = x.DiscountAmount, + SubTotal = x.SubTotal, + AttributesJson = x.AttributesJson + }).ToList(), + StatusHistory = histories.Select(x => new OrderStatusHistoryDto + { + Id = x.Id, + OrderId = x.OrderId, + Status = x.Status, + OperatorId = x.OperatorId, + Notes = x.Notes, + OccurredAt = x.OccurredAt + }).ToList(), + Refunds = refunds.Select(x => new RefundRequestDto + { + Id = x.Id, + OrderId = x.OrderId, + RefundNo = x.RefundNo, + Amount = x.Amount, + Reason = x.Reason, + Status = x.Status, + RequestedAt = x.RequestedAt, + ProcessedAt = x.ProcessedAt, + ReviewNotes = x.ReviewNotes + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs new file mode 100644 index 0000000..1446c27 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/GetOrderByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; + +namespace TakeoutSaaS.Application.App.Orders.Queries; + +/// +/// 获取订单详情查询。 +/// +public sealed class GetOrderByIdQuery : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs new file mode 100644 index 0000000..99fa7b3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Orders/Queries/SearchOrdersQuery.cs @@ -0,0 +1,32 @@ +using MediatR; +using TakeoutSaaS.Application.App.Orders.Dto; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Orders.Queries; + +/// +/// 订单列表查询。 +/// +public sealed class SearchOrdersQuery : IRequest> +{ + /// + /// 门店 ID(可选)。 + /// + public long? StoreId { get; init; } + + /// + /// 订单状态。 + /// + public OrderStatus? Status { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus? PaymentStatus { get; init; } + + /// + /// 订单号(模糊或精确,由调用方控制)。 + /// + public string? OrderNo { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs new file mode 100644 index 0000000..ed3c66c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/CreatePaymentCommand.cs @@ -0,0 +1,56 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Payments.Commands; + +/// +/// 创建支付记录命令。 +/// +public sealed class CreatePaymentCommand : IRequest +{ + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; set; } = PaymentMethod.Unknown; + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; set; } + + /// + /// 渠道单号。 + /// + public string? ChannelTransactionId { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 原始回调。 + /// + public string? Payload { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs new file mode 100644 index 0000000..5c42b47 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/DeletePaymentCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Payments.Commands; + +/// +/// 删除支付记录命令。 +/// +public sealed class DeletePaymentCommand : IRequest +{ + /// + /// 支付记录 ID。 + /// + public long PaymentId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs new file mode 100644 index 0000000..8d8263f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Commands/UpdatePaymentCommand.cs @@ -0,0 +1,61 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Payments.Commands; + +/// +/// 更新支付记录命令。 +/// +public sealed class UpdatePaymentCommand : IRequest +{ + /// + /// 支付记录 ID。 + /// + public long PaymentId { get; set; } + + /// + /// 订单 ID。 + /// + public long OrderId { get; set; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; set; } = PaymentMethod.Unknown; + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; set; } + + /// + /// 渠道单号。 + /// + public string? ChannelTransactionId { get; set; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 原始回调。 + /// + public string? Payload { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs new file mode 100644 index 0000000..807d0f1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentDto.cs @@ -0,0 +1,74 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Payments.Dto; + +/// +/// 支付记录 DTO。 +/// +public sealed class PaymentDto +{ + /// + /// 支付记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 支付方式。 + /// + public PaymentMethod Method { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus Status { get; init; } + + /// + /// 金额。 + /// + public decimal Amount { get; init; } + + /// + /// 平台交易号。 + /// + public string? TradeNo { get; init; } + + /// + /// 渠道单号。 + /// + public string? ChannelTransactionId { get; init; } + + /// + /// 支付时间。 + /// + public DateTime? PaidAt { get; init; } + + /// + /// 备注。 + /// + public string? Remark { get; init; } + + /// + /// 原始回调。 + /// + public string? Payload { get; init; } + + /// + /// 退款记录。 + /// + public IReadOnlyList Refunds { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs new file mode 100644 index 0000000..5c9cfa8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Dto/PaymentRefundDto.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Payments.Dto; + +/// +/// 退款记录 DTO。 +/// +public sealed class PaymentRefundDto +{ + /// + /// 退款记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 支付记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long PaymentRecordId { get; init; } + + /// + /// 订单 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long OrderId { get; init; } + + /// + /// 金额。 + /// + public decimal Amount { get; init; } + + /// + /// 渠道退款号。 + /// + public string? ChannelRefundId { get; init; } + + /// + /// 状态。 + /// + public PaymentRefundStatus Status { get; init; } + + /// + /// 原始回调。 + /// + public string? Payload { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs new file mode 100644 index 0000000..463d903 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/CreatePaymentCommandHandler.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 创建支付记录命令处理器。 +/// +public sealed class CreatePaymentCommandHandler(IPaymentRepository paymentRepository, ILogger logger) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreatePaymentCommand request, CancellationToken cancellationToken) + { + var payment = new PaymentRecord + { + OrderId = request.OrderId, + Method = request.Method, + Status = request.Status, + Amount = request.Amount, + TradeNo = request.TradeNo?.Trim(), + ChannelTransactionId = request.ChannelTransactionId?.Trim(), + PaidAt = request.PaidAt, + Remark = request.Remark?.Trim(), + Payload = request.Payload + }; + + await _paymentRepository.AddPaymentAsync(payment, cancellationToken); + await _paymentRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建支付记录 {PaymentId} 对应订单 {OrderId}", payment.Id, payment.OrderId); + + return MapToDto(payment, []); + } + + private static PaymentDto MapToDto(PaymentRecord payment, IReadOnlyList refunds) => new() + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload, + Refunds = refunds.Select(x => new PaymentRefundDto + { + Id = x.Id, + PaymentRecordId = x.PaymentRecordId, + OrderId = x.OrderId, + Amount = x.Amount, + ChannelRefundId = x.ChannelRefundId, + Status = x.Status, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs new file mode 100644 index 0000000..07b2ca9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/DeletePaymentCommandHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 删除支付记录命令处理器。 +/// +public sealed class DeletePaymentCommandHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeletePaymentCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _paymentRepository.FindByIdAsync(request.PaymentId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + await _paymentRepository.DeletePaymentAsync(request.PaymentId, tenantId, cancellationToken); + await _paymentRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除支付记录 {PaymentId}", request.PaymentId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs new file mode 100644 index 0000000..225697c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/GetPaymentByIdQueryHandler.cs @@ -0,0 +1,59 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 支付记录详情查询处理器。 +/// +public sealed class GetPaymentByIdQueryHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetPaymentByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var payment = await _paymentRepository.FindByIdAsync(request.PaymentId, tenantId, cancellationToken); + if (payment == null) + { + return null; + } + + var refunds = await _paymentRepository.GetRefundsAsync(payment.Id, tenantId, cancellationToken); + return MapToDto(payment, refunds); + } + + private static PaymentDto MapToDto(PaymentRecord payment, IReadOnlyList refunds) => new() + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload, + Refunds = refunds.Select(x => new PaymentRefundDto + { + Id = x.Id, + PaymentRecordId = x.PaymentRecordId, + OrderId = x.OrderId, + Amount = x.Amount, + ChannelRefundId = x.ChannelRefundId, + Status = x.Status, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs new file mode 100644 index 0000000..45ff7cb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/SearchPaymentsQueryHandler.cs @@ -0,0 +1,46 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Application.App.Payments.Queries; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 支付记录列表查询处理器。 +/// +public sealed class SearchPaymentsQueryHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchPaymentsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var payments = await _paymentRepository.SearchAsync(tenantId, request.Status, cancellationToken); + + if (request.OrderId.HasValue) + { + payments = payments.Where(x => x.OrderId == request.OrderId.Value).ToList(); + } + + return payments.Select(payment => new PaymentDto + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload + }).ToList(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs new file mode 100644 index 0000000..f55a37d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Handlers/UpdatePaymentCommandHandler.cs @@ -0,0 +1,76 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Payments.Commands; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Payments.Handlers; + +/// +/// 更新支付记录命令处理器。 +/// +public sealed class UpdatePaymentCommandHandler( + IPaymentRepository paymentRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IPaymentRepository _paymentRepository = paymentRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdatePaymentCommand request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _paymentRepository.FindByIdAsync(request.PaymentId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + existing.OrderId = request.OrderId; + existing.Method = request.Method; + existing.Status = request.Status; + existing.Amount = request.Amount; + existing.TradeNo = request.TradeNo?.Trim(); + existing.ChannelTransactionId = request.ChannelTransactionId?.Trim(); + existing.PaidAt = request.PaidAt; + existing.Remark = request.Remark?.Trim(); + existing.Payload = request.Payload; + + await _paymentRepository.UpdatePaymentAsync(existing, cancellationToken); + await _paymentRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新支付记录 {PaymentId}", existing.Id); + + var refunds = await _paymentRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken); + return MapToDto(existing, refunds); + } + + private static PaymentDto MapToDto(PaymentRecord payment, IReadOnlyList refunds) => new() + { + Id = payment.Id, + TenantId = payment.TenantId, + OrderId = payment.OrderId, + Method = payment.Method, + Status = payment.Status, + Amount = payment.Amount, + TradeNo = payment.TradeNo, + ChannelTransactionId = payment.ChannelTransactionId, + PaidAt = payment.PaidAt, + Remark = payment.Remark, + Payload = payment.Payload, + Refunds = refunds.Select(x => new PaymentRefundDto + { + Id = x.Id, + PaymentRecordId = x.PaymentRecordId, + OrderId = x.OrderId, + Amount = x.Amount, + ChannelRefundId = x.ChannelRefundId, + Status = x.Status, + Payload = x.Payload + }).ToList() + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs new file mode 100644 index 0000000..3ca6c8c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/GetPaymentByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; + +namespace TakeoutSaaS.Application.App.Payments.Queries; + +/// +/// 获取支付记录详情。 +/// +public sealed class GetPaymentByIdQuery : IRequest +{ + /// + /// 支付记录 ID。 + /// + public long PaymentId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs new file mode 100644 index 0000000..91b5bad --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Payments/Queries/SearchPaymentsQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Payments.Dto; +using TakeoutSaaS.Domain.Payments.Enums; + +namespace TakeoutSaaS.Application.App.Payments.Queries; + +/// +/// 支付记录列表查询。 +/// +public sealed class SearchPaymentsQuery : IRequest> +{ + /// + /// 订单 ID(可选)。 + /// + public long? OrderId { get; init; } + + /// + /// 支付状态。 + /// + public PaymentStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs new file mode 100644 index 0000000..50d08c0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/CreateProductCommand.cs @@ -0,0 +1,101 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Application.App.Products.Commands; + +/// +/// 创建商品命令。 +/// +public sealed class CreateProductCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 分类 ID。 + /// + public long CategoryId { get; set; } + + /// + /// 商品编码。 + /// + public string SpuCode { get; set; } = string.Empty; + + /// + /// 名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 副标题。 + /// + public string? Subtitle { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 现价。 + /// + public decimal Price { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 库存数量。 + /// + public int? StockQuantity { get; set; } + + /// + /// 每单限购。 + /// + public int? MaxQuantityPerOrder { get; set; } + + /// + /// 状态。 + /// + public ProductStatus Status { get; set; } = ProductStatus.Draft; + + /// + /// 主图。 + /// + public string? CoverImage { get; set; } + + /// + /// 图集。 + /// + public string? GalleryImages { get; set; } + + /// + /// 描述。 + /// + public string? Description { get; set; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; set; } = true; + + /// + /// 是否推荐。 + /// + public bool IsFeatured { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs new file mode 100644 index 0000000..9a17c86 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/DeleteProductCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Products.Commands; + +/// +/// 删除商品命令。 +/// +public sealed class DeleteProductCommand : IRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs b/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs new file mode 100644 index 0000000..ba3de2b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Commands/UpdateProductCommand.cs @@ -0,0 +1,106 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Application.App.Products.Commands; + +/// +/// 更新商品命令。 +/// +public sealed class UpdateProductCommand : IRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; set; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 分类 ID。 + /// + public long CategoryId { get; set; } + + /// + /// 商品编码。 + /// + public string SpuCode { get; set; } = string.Empty; + + /// + /// 名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 副标题。 + /// + public string? Subtitle { get; set; } + + /// + /// 单位。 + /// + public string? Unit { get; set; } + + /// + /// 现价。 + /// + public decimal Price { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 库存数量。 + /// + public int? StockQuantity { get; set; } + + /// + /// 每单限购。 + /// + public int? MaxQuantityPerOrder { get; set; } + + /// + /// 状态。 + /// + public ProductStatus Status { get; set; } = ProductStatus.Draft; + + /// + /// 主图。 + /// + public string? CoverImage { get; set; } + + /// + /// 图集。 + /// + public string? GalleryImages { get; set; } + + /// + /// 描述。 + /// + public string? Description { get; set; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; set; } = true; + + /// + /// 是否推荐。 + /// + public bool IsFeatured { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs b/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs new file mode 100644 index 0000000..7661594 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Dto/ProductDto.cs @@ -0,0 +1,115 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Products.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Products.Dto; + +/// +/// 商品 DTO。 +/// +public sealed class ProductDto +{ + /// + /// 商品 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 CategoryId { get; init; } + + /// + /// SPU 编码。 + /// + public string SpuCode { get; init; } = string.Empty; + + /// + /// 名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 副标题。 + /// + public string? Subtitle { get; init; } + + /// + /// 单位。 + /// + public string? Unit { get; init; } + + /// + /// 现价。 + /// + public decimal Price { get; init; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; init; } + + /// + /// 库存数量。 + /// + public int? StockQuantity { get; init; } + + /// + /// 每单限购。 + /// + public int? MaxQuantityPerOrder { get; init; } + + /// + /// 状态。 + /// + public ProductStatus Status { get; init; } + + /// + /// 主图。 + /// + public string? CoverImage { get; init; } + + /// + /// 图集。 + /// + public string? GalleryImages { get; init; } + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 支持堂食。 + /// + public bool EnableDineIn { get; init; } + + /// + /// 支持自提。 + /// + public bool EnablePickup { get; init; } + + /// + /// 支持配送。 + /// + public bool EnableDelivery { get; init; } + + /// + /// 是否推荐。 + /// + public bool IsFeatured { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs new file mode 100644 index 0000000..46201fa --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/CreateProductCommandHandler.cs @@ -0,0 +1,77 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Repositories; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 创建商品命令处理器。 +/// +public sealed class CreateProductCommandHandler(IProductRepository productRepository, ILogger logger) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken) + { + // 1. 构建实体 + var product = new Product + { + StoreId = request.StoreId, + CategoryId = request.CategoryId, + SpuCode = request.SpuCode.Trim(), + Name = request.Name.Trim(), + Subtitle = request.Subtitle?.Trim(), + Unit = request.Unit?.Trim(), + Price = request.Price, + OriginalPrice = request.OriginalPrice, + StockQuantity = request.StockQuantity, + MaxQuantityPerOrder = request.MaxQuantityPerOrder, + Status = request.Status, + CoverImage = request.CoverImage?.Trim(), + GalleryImages = request.GalleryImages?.Trim(), + Description = request.Description?.Trim(), + EnableDineIn = request.EnableDineIn, + EnablePickup = request.EnablePickup, + EnableDelivery = request.EnableDelivery, + IsFeatured = request.IsFeatured + }; + + // 2. 持久化 + await _productRepository.AddProductAsync(product, cancellationToken); + await _productRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建商品 {ProductId} - {ProductName}", product.Id, product.Name); + + // 3. 返回 DTO + return MapToDto(product); + } + + private static ProductDto MapToDto(Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs new file mode 100644 index 0000000..f06cfa1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/DeleteProductCommandHandler.cs @@ -0,0 +1,40 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 删除商品命令处理器。 +/// +public sealed class DeleteProductCommandHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteProductCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _productRepository.DeleteProductAsync(request.ProductId, tenantId, cancellationToken); + await _productRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除商品 {ProductId}", request.ProductId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs new file mode 100644 index 0000000..018c325 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/GetProductByIdQueryHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Application.App.Products.Queries; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 商品详情查询处理器。 +/// +public sealed class GetProductByIdQueryHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetProductByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var product = await _productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken); + return product == null ? null : MapToDto(product); + } + + private static ProductDto MapToDto(Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs new file mode 100644 index 0000000..d24784e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/SearchProductsQueryHandler.cs @@ -0,0 +1,57 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Application.App.Products.Queries; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 商品列表查询处理器。 +/// +public sealed class SearchProductsQueryHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchProductsQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var products = await _productRepository.SearchAsync(tenantId, request.CategoryId, request.Status, cancellationToken); + + if (request.StoreId.HasValue) + { + products = products.Where(x => x.StoreId == request.StoreId.Value).ToList(); + } + + return products.Select(MapToDto).ToList(); + } + + private static ProductDto MapToDto(Domain.Products.Entities.Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs new file mode 100644 index 0000000..df33a23 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Handlers/UpdateProductCommandHandler.cs @@ -0,0 +1,87 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Application.App.Products.Commands; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Entities; +using TakeoutSaaS.Domain.Products.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Products.Handlers; + +/// +/// 更新商品命令处理器。 +/// +public sealed class UpdateProductCommandHandler( + IProductRepository productRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IProductRepository _productRepository = productRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateProductCommand request, CancellationToken cancellationToken) + { + // 1. 读取商品 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _productRepository.FindByIdAsync(request.ProductId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.StoreId = request.StoreId; + existing.CategoryId = request.CategoryId; + existing.SpuCode = request.SpuCode.Trim(); + existing.Name = request.Name.Trim(); + existing.Subtitle = request.Subtitle?.Trim(); + existing.Unit = request.Unit?.Trim(); + existing.Price = request.Price; + existing.OriginalPrice = request.OriginalPrice; + existing.StockQuantity = request.StockQuantity; + existing.MaxQuantityPerOrder = request.MaxQuantityPerOrder; + existing.Status = request.Status; + existing.CoverImage = request.CoverImage?.Trim(); + existing.GalleryImages = request.GalleryImages?.Trim(); + existing.Description = request.Description?.Trim(); + existing.EnableDineIn = request.EnableDineIn; + existing.EnablePickup = request.EnablePickup; + existing.EnableDelivery = request.EnableDelivery; + existing.IsFeatured = request.IsFeatured; + + // 3. 持久化 + await _productRepository.UpdateProductAsync(existing, cancellationToken); + await _productRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新商品 {ProductId} - {ProductName}", existing.Id, existing.Name); + + // 4. 返回 DTO + return MapToDto(existing); + } + + private static ProductDto MapToDto(Product product) => new() + { + Id = product.Id, + TenantId = product.TenantId, + StoreId = product.StoreId, + CategoryId = product.CategoryId, + SpuCode = product.SpuCode, + Name = product.Name, + Subtitle = product.Subtitle, + Unit = product.Unit, + Price = product.Price, + OriginalPrice = product.OriginalPrice, + StockQuantity = product.StockQuantity, + MaxQuantityPerOrder = product.MaxQuantityPerOrder, + Status = product.Status, + CoverImage = product.CoverImage, + GalleryImages = product.GalleryImages, + Description = product.Description, + EnableDineIn = product.EnableDineIn, + EnablePickup = product.EnablePickup, + EnableDelivery = product.EnableDelivery, + IsFeatured = product.IsFeatured + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs new file mode 100644 index 0000000..08830cb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Queries/GetProductByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; + +namespace TakeoutSaaS.Application.App.Products.Queries; + +/// +/// 获取商品详情查询。 +/// +public sealed class GetProductByIdQuery : IRequest +{ + /// + /// 商品 ID。 + /// + public long ProductId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs new file mode 100644 index 0000000..07541c8 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Products/Queries/SearchProductsQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.Products.Dto; +using TakeoutSaaS.Domain.Products.Enums; + +namespace TakeoutSaaS.Application.App.Products.Queries; + +/// +/// 商品列表查询。 +/// +public sealed class SearchProductsQuery : IRequest> +{ + /// + /// 门店 ID(可选)。 + /// + public long? StoreId { get; init; } + + /// + /// 分类 ID(可选)。 + /// + public long? CategoryId { get; init; } + + /// + /// 状态过滤。 + /// + public ProductStatus? Status { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs new file mode 100644 index 0000000..21eb9c5 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs @@ -0,0 +1,101 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 创建门店命令。 +/// +public sealed class CreateStoreCommand : IRequest +{ + /// + /// 商户 ID。 + /// + public long MerchantId { get; set; } + + /// + /// 门店编码。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 电话。 + /// + public string? Phone { get; set; } + + /// + /// 负责人。 + /// + public string? ManagerName { get; set; } + + /// + /// 状态。 + /// + public StoreStatus Status { get; set; } = StoreStatus.Closed; + + /// + /// 省份。 + /// + public string? Province { get; set; } + + /// + /// 城市。 + /// + public string? City { get; set; } + + /// + /// 区县。 + /// + public string? District { get; set; } + + /// + /// 详细地址。 + /// + public string? Address { get; set; } + + /// + /// 经度。 + /// + public double? Longitude { get; set; } + + /// + /// 纬度。 + /// + public double? Latitude { get; set; } + + /// + /// 公告。 + /// + public string? Announcement { get; set; } + + /// + /// 标签。 + /// + public string? Tags { get; set; } + + /// + /// 配送半径。 + /// + public decimal DeliveryRadiusKm { get; set; } + + /// + /// 支持堂食。 + /// + public bool SupportsDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool SupportsPickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool SupportsDelivery { get; set; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs new file mode 100644 index 0000000..9a66dbb --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/DeleteStoreCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 删除门店命令。 +/// +public sealed class DeleteStoreCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs new file mode 100644 index 0000000..0cecf53 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs @@ -0,0 +1,106 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Commands; + +/// +/// 更新门店命令。 +/// +public sealed class UpdateStoreCommand : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 商户 ID。 + /// + public long MerchantId { get; set; } + + /// + /// 门店编码。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 电话。 + /// + public string? Phone { get; set; } + + /// + /// 负责人。 + /// + public string? ManagerName { get; set; } + + /// + /// 状态。 + /// + public StoreStatus Status { get; set; } = StoreStatus.Closed; + + /// + /// 省份。 + /// + public string? Province { get; set; } + + /// + /// 城市。 + /// + public string? City { get; set; } + + /// + /// 区县。 + /// + public string? District { get; set; } + + /// + /// 详细地址。 + /// + public string? Address { get; set; } + + /// + /// 经度。 + /// + public double? Longitude { get; set; } + + /// + /// 纬度。 + /// + public double? Latitude { get; set; } + + /// + /// 公告。 + /// + public string? Announcement { get; set; } + + /// + /// 标签。 + /// + public string? Tags { get; set; } + + /// + /// 配送半径。 + /// + public decimal DeliveryRadiusKm { get; set; } + + /// + /// 支持堂食。 + /// + public bool SupportsDineIn { get; set; } = true; + + /// + /// 支持自提。 + /// + public bool SupportsPickup { get; set; } = true; + + /// + /// 支持配送。 + /// + public bool SupportsDelivery { get; set; } = true; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs new file mode 100644 index 0000000..e924b43 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDto.cs @@ -0,0 +1,114 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Stores.Dto; + +/// +/// 门店 DTO。 +/// +public sealed class StoreDto +{ + /// + /// 门店 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; } + + /// + /// 门店编码。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 门店名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 电话。 + /// + public string? Phone { get; init; } + + /// + /// 负责人。 + /// + public string? ManagerName { get; init; } + + /// + /// 状态。 + /// + public StoreStatus Status { get; init; } + + /// + /// 省份。 + /// + public string? Province { get; init; } + + /// + /// 城市。 + /// + public string? City { get; init; } + + /// + /// 区县。 + /// + public string? District { get; init; } + + /// + /// 详细地址。 + /// + public string? Address { get; init; } + + /// + /// 经度。 + /// + public double? Longitude { get; init; } + + /// + /// 纬度。 + /// + public double? Latitude { get; init; } + + /// + /// 公告。 + /// + public string? Announcement { get; init; } + + /// + /// 标签。 + /// + public string? Tags { get; init; } + + /// + /// 默认配送半径。 + /// + public decimal DeliveryRadiusKm { get; init; } + + /// + /// 支持堂食。 + /// + public bool SupportsDineIn { get; init; } + + /// + /// 支持自提。 + /// + public bool SupportsPickup { get; init; } + + /// + /// 支持配送。 + /// + public bool SupportsDelivery { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs new file mode 100644 index 0000000..56f9d9c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs @@ -0,0 +1,77 @@ +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; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 创建门店命令处理器。 +/// +public sealed class CreateStoreCommandHandler(IStoreRepository storeRepository, ILogger logger) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(CreateStoreCommand request, CancellationToken cancellationToken) + { + // 1. 构建实体 + var store = new Store + { + MerchantId = request.MerchantId, + Code = request.Code.Trim(), + Name = request.Name.Trim(), + Phone = request.Phone?.Trim(), + ManagerName = request.ManagerName?.Trim(), + Status = request.Status, + Province = request.Province?.Trim(), + City = request.City?.Trim(), + District = request.District?.Trim(), + Address = request.Address?.Trim(), + Longitude = request.Longitude, + Latitude = request.Latitude, + Announcement = request.Announcement?.Trim(), + Tags = request.Tags?.Trim(), + DeliveryRadiusKm = request.DeliveryRadiusKm, + SupportsDineIn = request.SupportsDineIn, + SupportsPickup = request.SupportsPickup, + SupportsDelivery = request.SupportsDelivery + }; + + // 2. 持久化 + await _storeRepository.AddStoreAsync(store, cancellationToken); + await _storeRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("创建门店 {StoreId} - {StoreName}", store.Id, store.Name); + + // 3. 返回 DTO + return MapToDto(store); + } + + private static StoreDto MapToDto(Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs new file mode 100644 index 0000000..7e30eaf --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreCommandHandler.cs @@ -0,0 +1,40 @@ +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 DeleteStoreCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(DeleteStoreCommand request, CancellationToken cancellationToken) + { + // 1. 校验存在性 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (existing == null) + { + return false; + } + + // 2. 删除 + await _storeRepository.DeleteStoreAsync(request.StoreId, tenantId, cancellationToken); + await _storeRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("删除门店 {StoreId}", request.StoreId); + + return true; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs new file mode 100644 index 0000000..4cb8cb0 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores.Queries; +using TakeoutSaaS.Domain.Stores.Entities; +using TakeoutSaaS.Domain.Stores.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 门店详情查询处理器。 +/// +public sealed class GetStoreByIdQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task Handle(GetStoreByIdQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var store = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + return store == null ? null : MapToDto(store); + } + + private static StoreDto MapToDto(Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs new file mode 100644 index 0000000..4efe538 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs @@ -0,0 +1,59 @@ +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 SearchStoresQueryHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + + /// + public async Task> Handle(SearchStoresQuery request, CancellationToken cancellationToken) + { + var tenantId = _tenantProvider.GetCurrentTenantId(); + var stores = await _storeRepository.SearchAsync(tenantId, request.Status, cancellationToken); + + if (request.MerchantId.HasValue) + { + stores = stores.Where(x => x.MerchantId == request.MerchantId.Value).ToList(); + } + + return stores + .Select(MapToDto) + .ToList(); + } + + private static StoreDto MapToDto(Domain.Stores.Entities.Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs new file mode 100644 index 0000000..1680932 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs @@ -0,0 +1,87 @@ +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.Tenancy; + +namespace TakeoutSaaS.Application.App.Stores.Handlers; + +/// +/// 更新门店命令处理器。 +/// +public sealed class UpdateStoreCommandHandler( + IStoreRepository storeRepository, + ITenantProvider tenantProvider, + ILogger logger) + : IRequestHandler +{ + private readonly IStoreRepository _storeRepository = storeRepository; + private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ILogger _logger = logger; + + /// + public async Task Handle(UpdateStoreCommand request, CancellationToken cancellationToken) + { + // 1. 读取门店 + var tenantId = _tenantProvider.GetCurrentTenantId(); + var existing = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + if (existing == null) + { + return null; + } + + // 2. 更新字段 + existing.MerchantId = request.MerchantId; + existing.Code = request.Code.Trim(); + existing.Name = request.Name.Trim(); + existing.Phone = request.Phone?.Trim(); + existing.ManagerName = request.ManagerName?.Trim(); + existing.Status = request.Status; + existing.Province = request.Province?.Trim(); + existing.City = request.City?.Trim(); + existing.District = request.District?.Trim(); + existing.Address = request.Address?.Trim(); + existing.Longitude = request.Longitude; + existing.Latitude = request.Latitude; + existing.Announcement = request.Announcement?.Trim(); + existing.Tags = request.Tags?.Trim(); + existing.DeliveryRadiusKm = request.DeliveryRadiusKm; + existing.SupportsDineIn = request.SupportsDineIn; + existing.SupportsPickup = request.SupportsPickup; + existing.SupportsDelivery = request.SupportsDelivery; + + // 3. 持久化 + await _storeRepository.UpdateStoreAsync(existing, cancellationToken); + await _storeRepository.SaveChangesAsync(cancellationToken); + _logger.LogInformation("更新门店 {StoreId} - {StoreName}", existing.Id, existing.Name); + + // 4. 返回 DTO + return MapToDto(existing); + } + + private static StoreDto MapToDto(Store store) => new() + { + Id = store.Id, + TenantId = store.TenantId, + MerchantId = store.MerchantId, + Code = store.Code, + Name = store.Name, + Phone = store.Phone, + ManagerName = store.ManagerName, + Status = store.Status, + Province = store.Province, + City = store.City, + District = store.District, + Address = store.Address, + Longitude = store.Longitude, + Latitude = store.Latitude, + Announcement = store.Announcement, + Tags = store.Tags, + DeliveryRadiusKm = store.DeliveryRadiusKm, + SupportsDineIn = store.SupportsDineIn, + SupportsPickup = store.SupportsPickup, + SupportsDelivery = store.SupportsDelivery + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs new file mode 100644 index 0000000..7f0699e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 获取门店详情查询。 +/// +public sealed class GetStoreByIdQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs new file mode 100644 index 0000000..085a557 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs @@ -0,0 +1,21 @@ +using MediatR; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; + +namespace TakeoutSaaS.Application.App.Stores.Queries; + +/// +/// 门店列表查询。 +/// +public sealed class SearchStoresQuery : IRequest> +{ + /// + /// 商户 ID(可选)。 + /// + public long? MerchantId { get; init; } + + /// + /// 状态过滤。 + /// + public StoreStatus? Status { get; init; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs index d0a2132..27d5c95 100644 --- a/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Deliveries/Repositories/IDeliveryRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Enums; namespace TakeoutSaaS.Domain.Deliveries.Repositories; @@ -40,6 +41,11 @@ public interface IDeliveryRepository /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + /// + /// 按状态查询配送单。 + /// + Task> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default); + /// /// 更新配送单。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs index ec286b5..f12c937 100644 --- a/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Payments/Repositories/IPaymentRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Enums; namespace TakeoutSaaS.Domain.Payments.Repositories; @@ -40,6 +41,11 @@ public interface IPaymentRepository /// Task SaveChangesAsync(CancellationToken cancellationToken = default); + /// + /// 按状态筛选支付记录。 + /// + Task> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default); + /// /// 更新支付记录。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs index 5c408ef..32b482f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfDeliveryRepository.cs @@ -1,6 +1,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Deliveries.Entities; +using TakeoutSaaS.Domain.Deliveries.Enums; using TakeoutSaaS.Domain.Deliveries.Repositories; using TakeoutSaaS.Infrastructure.App.Persistence; @@ -69,6 +70,28 @@ public sealed class EfDeliveryRepository : IDeliveryRepository return _context.SaveChangesAsync(cancellationToken); } + /// + public async Task> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default) + { + var query = _context.DeliveryOrders + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + if (orderId.HasValue) + { + query = query.Where(x => x.OrderId == orderId.Value); + } + + return await query + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + } + /// public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs index 8ea2124..f4dfb6c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPaymentRepository.cs @@ -1,6 +1,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Payments.Entities; +using TakeoutSaaS.Domain.Payments.Enums; using TakeoutSaaS.Domain.Payments.Repositories; using TakeoutSaaS.Infrastructure.App.Persistence; @@ -63,6 +64,23 @@ public sealed class EfPaymentRepository : IPaymentRepository return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask(); } + /// + public async Task> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default) + { + var query = _context.PaymentRecords + .AsNoTracking() + .Where(x => x.TenantId == tenantId); + + if (status.HasValue) + { + query = query.Where(x => x.Status == status.Value); + } + + return await query + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + } + /// public Task SaveChangesAsync(CancellationToken cancellationToken = default) {