feat: 管理端核心实体CRUD补齐
This commit is contained in:
108
src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs
Normal file
108
src/Api/TakeoutSaaS.AdminApi/Controllers/DeliveriesController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/deliveries")]
|
||||||
|
public sealed class DeliveriesController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化控制器。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveriesController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建配送单。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[PermissionAuthorize("delivery:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<DeliveryOrderDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<DeliveryOrderDto>> Create([FromBody] CreateDeliveryOrderCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<DeliveryOrderDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询配送单列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[PermissionAuthorize("delivery:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DeliveryOrderDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<DeliveryOrderDto>>> List([FromQuery] long? orderId, [FromQuery] DeliveryStatus? status, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new SearchDeliveryOrdersQuery
|
||||||
|
{
|
||||||
|
OrderId = orderId,
|
||||||
|
Status = status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<IReadOnlyList<DeliveryOrderDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配送单详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{deliveryOrderId:long}")]
|
||||||
|
[PermissionAuthorize("delivery:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<DeliveryOrderDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<DeliveryOrderDto>> Detail(long deliveryOrderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new GetDeliveryOrderByIdQuery { DeliveryOrderId = deliveryOrderId }, cancellationToken);
|
||||||
|
return result == null
|
||||||
|
? ApiResponse<DeliveryOrderDto>.Error(ErrorCodes.NotFound, "配送单不存在")
|
||||||
|
: ApiResponse<DeliveryOrderDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新配送单。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{deliveryOrderId:long}")]
|
||||||
|
[PermissionAuthorize("delivery:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<DeliveryOrderDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<DeliveryOrderDto>> 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<DeliveryOrderDto>.Error(ErrorCodes.NotFound, "配送单不存在")
|
||||||
|
: ApiResponse<DeliveryOrderDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除配送单。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{deliveryOrderId:long}")]
|
||||||
|
[PermissionAuthorize("delivery:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(long deliveryOrderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await _mediator.Send(new DeleteDeliveryOrderCommand { DeliveryOrderId = deliveryOrderId }, cancellationToken);
|
||||||
|
return success
|
||||||
|
? ApiResponse<object>.Ok(null)
|
||||||
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "配送单不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,38 @@ public sealed class MerchantsController : BaseApiController
|
|||||||
return ApiResponse<IReadOnlyList<MerchantDto>>.Ok(result);
|
return ApiResponse<IReadOnlyList<MerchantDto>>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新商户。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{merchantId:long}")]
|
||||||
|
[PermissionAuthorize("merchant:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<MerchantDto>> 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<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
|
||||||
|
: ApiResponse<MerchantDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商户。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{merchantId:long}")]
|
||||||
|
[PermissionAuthorize("merchant:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(long merchantId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await _mediator.Send(new DeleteMerchantCommand { MerchantId = merchantId }, cancellationToken);
|
||||||
|
return success
|
||||||
|
? ApiResponse<object>.Ok(null)
|
||||||
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取商户详情。
|
/// 获取商户详情。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
116
src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs
Normal file
116
src/Api/TakeoutSaaS.AdminApi/Controllers/OrdersController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/orders")]
|
||||||
|
public sealed class OrdersController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化控制器。
|
||||||
|
/// </summary>
|
||||||
|
public OrdersController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[PermissionAuthorize("order:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<OrderDto>> Create([FromBody] CreateOrderCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<OrderDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询订单列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[PermissionAuthorize("order:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<OrderDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<OrderDto>>> 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<IReadOnlyList<OrderDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取订单详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{orderId:long}")]
|
||||||
|
[PermissionAuthorize("order:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<OrderDto>> Detail(long orderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new GetOrderByIdQuery { OrderId = orderId }, cancellationToken);
|
||||||
|
return result == null
|
||||||
|
? ApiResponse<OrderDto>.Error(ErrorCodes.NotFound, "订单不存在")
|
||||||
|
: ApiResponse<OrderDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新订单。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{orderId:long}")]
|
||||||
|
[PermissionAuthorize("order:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<OrderDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<OrderDto>> 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<OrderDto>.Error(ErrorCodes.NotFound, "订单不存在")
|
||||||
|
: ApiResponse<OrderDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除订单。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{orderId:long}")]
|
||||||
|
[PermissionAuthorize("order:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(long orderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await _mediator.Send(new DeleteOrderCommand { OrderId = orderId }, cancellationToken);
|
||||||
|
return success
|
||||||
|
? ApiResponse<object>.Ok(null)
|
||||||
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "订单不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs
Normal file
108
src/Api/TakeoutSaaS.AdminApi/Controllers/PaymentsController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/payments")]
|
||||||
|
public sealed class PaymentsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化控制器。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentsController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建支付记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[PermissionAuthorize("payment:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PaymentDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PaymentDto>> Create([FromBody] CreatePaymentCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<PaymentDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询支付记录列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[PermissionAuthorize("payment:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PaymentDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<PaymentDto>>> List([FromQuery] long? orderId, [FromQuery] PaymentStatus? status, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new SearchPaymentsQuery
|
||||||
|
{
|
||||||
|
OrderId = orderId,
|
||||||
|
Status = status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<IReadOnlyList<PaymentDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取支付记录详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{paymentId:long}")]
|
||||||
|
[PermissionAuthorize("payment:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PaymentDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<PaymentDto>> Detail(long paymentId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new GetPaymentByIdQuery { PaymentId = paymentId }, cancellationToken);
|
||||||
|
return result == null
|
||||||
|
? ApiResponse<PaymentDto>.Error(ErrorCodes.NotFound, "支付记录不存在")
|
||||||
|
: ApiResponse<PaymentDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新支付记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{paymentId:long}")]
|
||||||
|
[PermissionAuthorize("payment:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PaymentDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<PaymentDto>> 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<PaymentDto>.Error(ErrorCodes.NotFound, "支付记录不存在")
|
||||||
|
: ApiResponse<PaymentDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除支付记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{paymentId:long}")]
|
||||||
|
[PermissionAuthorize("payment:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(long paymentId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await _mediator.Send(new DeletePaymentCommand { PaymentId = paymentId }, cancellationToken);
|
||||||
|
return success
|
||||||
|
? ApiResponse<object>.Ok(null)
|
||||||
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "支付记录不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs
Normal file
109
src/Api/TakeoutSaaS.AdminApi/Controllers/ProductsController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/products")]
|
||||||
|
public sealed class ProductsController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化控制器。
|
||||||
|
/// </summary>
|
||||||
|
public ProductsController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建商品。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[PermissionAuthorize("product:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<ProductDto>> Create([FromBody] CreateProductCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<ProductDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询商品列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[PermissionAuthorize("product:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<ProductDto>>> 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<IReadOnlyList<ProductDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商品详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{productId:long}")]
|
||||||
|
[PermissionAuthorize("product:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<ProductDto>> Detail(long productId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new GetProductByIdQuery { ProductId = productId }, cancellationToken);
|
||||||
|
return result == null
|
||||||
|
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||||
|
: ApiResponse<ProductDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新商品。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{productId:long}")]
|
||||||
|
[PermissionAuthorize("product:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<ProductDto>> 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<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||||
|
: ApiResponse<ProductDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商品。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{productId:long}")]
|
||||||
|
[PermissionAuthorize("product:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(long productId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await _mediator.Send(new DeleteProductCommand { ProductId = productId }, cancellationToken);
|
||||||
|
return success
|
||||||
|
? ApiResponse<object>.Ok(null)
|
||||||
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "商品不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs
Normal file
108
src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/stores")]
|
||||||
|
public sealed class StoresController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化控制器。
|
||||||
|
/// </summary>
|
||||||
|
public StoresController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建门店。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
[PermissionAuthorize("store:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreDto>> Create([FromBody] CreateStoreCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<StoreDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询门店列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
[PermissionAuthorize("store:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<StoreDto>>> List([FromQuery] long? merchantId, [FromQuery] StoreStatus? status, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new SearchStoresQuery
|
||||||
|
{
|
||||||
|
MerchantId = merchantId,
|
||||||
|
Status = status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<IReadOnlyList<StoreDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{storeId:long}")]
|
||||||
|
[PermissionAuthorize("store:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<StoreDto>> Detail(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken);
|
||||||
|
return result == null
|
||||||
|
? ApiResponse<StoreDto>.Error(ErrorCodes.NotFound, "门店不存在")
|
||||||
|
: ApiResponse<StoreDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新门店。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("{storeId:long}")]
|
||||||
|
[PermissionAuthorize("store:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<StoreDto>> 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<StoreDto>.Error(ErrorCodes.NotFound, "门店不存在")
|
||||||
|
: ApiResponse<StoreDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除门店。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{storeId:long}")]
|
||||||
|
[PermissionAuthorize("store:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var success = await _mediator.Send(new DeleteStoreCommand { StoreId = storeId }, cancellationToken);
|
||||||
|
return success
|
||||||
|
? ApiResponse<object>.Ok(null)
|
||||||
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "门店不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Deliveries.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Deliveries.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建配送单命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateDeliveryOrderCommand : IRequest<DeliveryOrderDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务商。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方单号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ProviderOrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送费。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DeliveryFee { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑手姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CourierName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑手电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? CourierPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下发时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DispatchedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取餐时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PickedUpAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeliveredAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Deliveries.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除配送单命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteDeliveryOrderCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long DeliveryOrderId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Deliveries.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Deliveries.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新配送单命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateDeliveryOrderCommand : IRequest<DeliveryOrderDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long DeliveryOrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务商。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryProvider Provider { get; set; } = DeliveryProvider.InHouse;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方单号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ProviderOrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryStatus Status { get; set; } = DeliveryStatus.Pending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送费。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DeliveryFee { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑手姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CourierName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑手电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? CourierPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下发时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DispatchedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取餐时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PickedUpAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeliveredAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送事件 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeliveryEventDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 事件 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long DeliveryOrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 事件类型。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryEventType EventType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 事件时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OccurredAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始载荷。
|
||||||
|
/// </summary>
|
||||||
|
public string? Payload { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeliveryOrderDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送服务商。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryProvider Provider { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方配送单号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ProviderOrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送费。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DeliveryFee { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑手姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CourierName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑手电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? CourierPhone { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下发时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DispatchedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取餐时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PickedUpAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? DeliveredAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? FailureReason { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 事件列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DeliveryEventDto> Events { get; init; } = Array.Empty<DeliveryEventDto>();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建配送单命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository deliveryRepository, ILogger<CreateDeliveryOrderCommandHandler> logger)
|
||||||
|
: IRequestHandler<CreateDeliveryOrderCommand, DeliveryOrderDto>
|
||||||
|
{
|
||||||
|
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
|
||||||
|
private readonly ILogger<CreateDeliveryOrderCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<DeliveryOrderDto> 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<DeliveryEvent> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除配送单命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteDeliveryOrderCommandHandler(
|
||||||
|
IDeliveryRepository deliveryRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<DeleteDeliveryOrderCommandHandler> logger)
|
||||||
|
: IRequestHandler<DeleteDeliveryOrderCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<DeleteDeliveryOrderCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetDeliveryOrderByIdQueryHandler(
|
||||||
|
IDeliveryRepository deliveryRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetDeliveryOrderByIdQuery, DeliveryOrderDto?>
|
||||||
|
{
|
||||||
|
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<DeliveryOrderDto?> 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<DeliveryEvent> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchDeliveryOrdersQueryHandler(
|
||||||
|
IDeliveryRepository deliveryRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchDeliveryOrdersQuery, IReadOnlyList<DeliveryOrderDto>>
|
||||||
|
{
|
||||||
|
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<DeliveryOrderDto>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新配送单命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateDeliveryOrderCommandHandler(
|
||||||
|
IDeliveryRepository deliveryRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<UpdateDeliveryOrderCommandHandler> logger)
|
||||||
|
: IRequestHandler<UpdateDeliveryOrderCommand, DeliveryOrderDto?>
|
||||||
|
{
|
||||||
|
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<UpdateDeliveryOrderCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<DeliveryOrderDto?> 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<DeliveryEvent> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Deliveries.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Deliveries.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单详情查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetDeliveryOrderByIdQuery : IRequest<DeliveryOrderDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long DeliveryOrderId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Deliveries.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Deliveries.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送单列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchDeliveryOrdersQuery : IRequest<IReadOnlyList<DeliveryOrderDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送状态。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryStatus? Status { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商户命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteMerchantCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新商户命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateMerchantCommand : IRequest<MerchantDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 品牌名称。
|
||||||
|
/// </summary>
|
||||||
|
public string BrandName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 品牌简称。
|
||||||
|
/// </summary>
|
||||||
|
public string? BrandAlias { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logo 地址。
|
||||||
|
/// </summary>
|
||||||
|
public string? LogoUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 品类。
|
||||||
|
/// </summary>
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联系电话。
|
||||||
|
/// </summary>
|
||||||
|
public string ContactPhone { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联系邮箱。
|
||||||
|
/// </summary>
|
||||||
|
public string? ContactEmail { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入驻状态。
|
||||||
|
/// </summary>
|
||||||
|
public MerchantStatus Status { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商户命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteMerchantCommandHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<DeleteMerchantCommandHandler> logger)
|
||||||
|
: IRequestHandler<DeleteMerchantCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IMerchantRepository _merchantRepository = merchantRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<DeleteMerchantCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新商户命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateMerchantCommandHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<UpdateMerchantCommandHandler> logger)
|
||||||
|
: IRequestHandler<UpdateMerchantCommand, MerchantDto?>
|
||||||
|
{
|
||||||
|
private readonly IMerchantRepository _merchantRepository = merchantRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<UpdateMerchantCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MerchantDto?> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateOrderCommand : IRequest<OrderDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道。
|
||||||
|
/// </summary>
|
||||||
|
public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 桌号。
|
||||||
|
/// </summary>
|
||||||
|
public string? TableNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排队号。
|
||||||
|
/// </summary>
|
||||||
|
public string? QueueNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预约 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? ReservationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ItemsAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优惠金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PayableAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PaidAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FinishedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CancelledAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? CancelReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 明细。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<OrderItemRequest> Items { get; set; } = Array.Empty<OrderItemRequest>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除订单命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteOrderCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Orders.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单明细请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderItemRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品名称。
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? SkuName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单位。
|
||||||
|
/// </summary>
|
||||||
|
public string? Unit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal UnitPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 折扣金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小计。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SubTotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 属性 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string? AttributesJson { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新订单命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateOrderCommand : IRequest<OrderDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道。
|
||||||
|
/// </summary>
|
||||||
|
public OrderChannel Channel { get; set; } = OrderChannel.MiniProgram;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType DeliveryType { get; set; } = DeliveryType.DineIn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 桌号。
|
||||||
|
/// </summary>
|
||||||
|
public string? TableNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排队号。
|
||||||
|
/// </summary>
|
||||||
|
public string? QueueNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预约 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? ReservationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ItemsAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优惠金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PayableAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PaidAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FinishedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CancelledAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? CancelReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道。
|
||||||
|
/// </summary>
|
||||||
|
public OrderChannel Channel { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryType DeliveryType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus PaymentStatus { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerPhone { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 桌号。
|
||||||
|
/// </summary>
|
||||||
|
public string? TableNo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排队号。
|
||||||
|
/// </summary>
|
||||||
|
public string? QueueNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预约 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long? ReservationId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ItemsAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优惠金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PayableAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实付金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal PaidAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FinishedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CancelledAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? CancelReason { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 明细。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<OrderItemDto> Items { get; init; } = Array.Empty<OrderItemDto>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态流转。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<OrderStatusHistoryDto> StatusHistory { get; init; } = Array.Empty<OrderStatusHistoryDto>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款申请。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<RefundRequestDto> Refunds { get; init; } = Array.Empty<RefundRequestDto>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单明细 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 明细 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品名称。
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? SkuName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单位。
|
||||||
|
/// </summary>
|
||||||
|
public string? Unit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal UnitPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 折扣金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DiscountAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小计。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SubTotal { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 属性 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string? AttributesJson { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态流转 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderStatusHistoryDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long? OperatorId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Notes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OccurredAt { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款申请 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RefundRequestDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 退款 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款单号。
|
||||||
|
/// </summary>
|
||||||
|
public string RefundNo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原因。
|
||||||
|
/// </summary>
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public RefundStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 申请时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RequestedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ProcessedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? ReviewNotes { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateOrderCommandHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
IIdGenerator idGenerator,
|
||||||
|
ILogger<CreateOrderCommandHandler> logger)
|
||||||
|
: IRequestHandler<CreateOrderCommand, OrderDto>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository = orderRepository;
|
||||||
|
private readonly IIdGenerator _idGenerator = idGenerator;
|
||||||
|
private readonly ILogger<CreateOrderCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OrderDto> 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<OrderItem> items,
|
||||||
|
IReadOnlyList<OrderStatusHistory> histories,
|
||||||
|
IReadOnlyList<RefundRequest> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除订单命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteOrderCommandHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<DeleteOrderCommandHandler> logger)
|
||||||
|
: IRequestHandler<DeleteOrderCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository = orderRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<DeleteOrderCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderByIdQueryHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetOrderByIdQuery, OrderDto?>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository = orderRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OrderDto?> 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<OrderItem> items,
|
||||||
|
IReadOnlyList<OrderStatusHistory> histories,
|
||||||
|
IReadOnlyList<RefundRequest> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchOrdersQueryHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchOrdersQuery, IReadOnlyList<OrderDto>>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository = orderRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<OrderDto>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新订单命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateOrderCommandHandler(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<UpdateOrderCommandHandler> logger)
|
||||||
|
: IRequestHandler<UpdateOrderCommand, OrderDto?>
|
||||||
|
{
|
||||||
|
private readonly IOrderRepository _orderRepository = orderRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<UpdateOrderCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OrderDto?> 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<OrderItem> items,
|
||||||
|
IReadOnlyList<OrderStatusHistory> histories,
|
||||||
|
IReadOnlyList<RefundRequest> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Orders.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取订单详情查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetOrderByIdQuery : IRequest<OrderDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchOrdersQuery : IRequest<IReadOnlyList<OrderDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus? PaymentStatus { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号(模糊或精确,由调用方控制)。
|
||||||
|
/// </summary>
|
||||||
|
public string? OrderNo { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Payments.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Payments.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建支付记录命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreatePaymentCommand : IRequest<PaymentDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod Method { get; set; } = PaymentMethod.Unknown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台交易号。
|
||||||
|
/// </summary>
|
||||||
|
public string? TradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道单号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChannelTransactionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始回调。
|
||||||
|
/// </summary>
|
||||||
|
public string? Payload { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Payments.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除支付记录命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeletePaymentCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long PaymentId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Payments.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Payments.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新支付记录命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdatePaymentCommand : IRequest<PaymentDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long PaymentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod Method { get; set; } = PaymentMethod.Unknown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus Status { get; set; } = PaymentStatus.Unpaid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台交易号。
|
||||||
|
/// </summary>
|
||||||
|
public string? TradeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道单号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChannelTransactionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始回调。
|
||||||
|
/// </summary>
|
||||||
|
public string? Payload { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PaymentDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod Method { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平台交易号。
|
||||||
|
/// </summary>
|
||||||
|
public string? TradeNo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道单号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChannelTransactionId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PaidAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始回调。
|
||||||
|
/// </summary>
|
||||||
|
public string? Payload { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款记录。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PaymentRefundDto> Refunds { get; init; } = Array.Empty<PaymentRefundDto>();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退款记录 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PaymentRefundDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 退款记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long PaymentRecordId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道退款号。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChannelRefundId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentRefundStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始回调。
|
||||||
|
/// </summary>
|
||||||
|
public string? Payload { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建支付记录命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreatePaymentCommandHandler(IPaymentRepository paymentRepository, ILogger<CreatePaymentCommandHandler> logger)
|
||||||
|
: IRequestHandler<CreatePaymentCommand, PaymentDto>
|
||||||
|
{
|
||||||
|
private readonly IPaymentRepository _paymentRepository = paymentRepository;
|
||||||
|
private readonly ILogger<CreatePaymentCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PaymentDto> 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<PaymentRefundRecord> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除支付记录命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeletePaymentCommandHandler(
|
||||||
|
IPaymentRepository paymentRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<DeletePaymentCommandHandler> logger)
|
||||||
|
: IRequestHandler<DeletePaymentCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IPaymentRepository _paymentRepository = paymentRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<DeletePaymentCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPaymentByIdQueryHandler(
|
||||||
|
IPaymentRepository paymentRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetPaymentByIdQuery, PaymentDto?>
|
||||||
|
{
|
||||||
|
private readonly IPaymentRepository _paymentRepository = paymentRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PaymentDto?> 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<PaymentRefundRecord> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchPaymentsQueryHandler(
|
||||||
|
IPaymentRepository paymentRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchPaymentsQuery, IReadOnlyList<PaymentDto>>
|
||||||
|
{
|
||||||
|
private readonly IPaymentRepository _paymentRepository = paymentRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<PaymentDto>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新支付记录命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdatePaymentCommandHandler(
|
||||||
|
IPaymentRepository paymentRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<UpdatePaymentCommandHandler> logger)
|
||||||
|
: IRequestHandler<UpdatePaymentCommand, PaymentDto?>
|
||||||
|
{
|
||||||
|
private readonly IPaymentRepository _paymentRepository = paymentRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<UpdatePaymentCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PaymentDto?> 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<PaymentRefundRecord> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Payments.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Payments.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取支付记录详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPaymentByIdQuery : IRequest<PaymentDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long PaymentId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Payments.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Payments.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付记录列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchPaymentsQuery : IRequest<IReadOnlyList<PaymentDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? OrderId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentStatus? Status { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建商品命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateProductCommand : IRequest<ProductDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long CategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品编码。
|
||||||
|
/// </summary>
|
||||||
|
public string SpuCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 副标题。
|
||||||
|
/// </summary>
|
||||||
|
public string? Subtitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单位。
|
||||||
|
/// </summary>
|
||||||
|
public string? Unit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 现价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OriginalPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 库存数量。
|
||||||
|
/// </summary>
|
||||||
|
public int? StockQuantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每单限购。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxQuantityPerOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public ProductStatus Status { get; set; } = ProductStatus.Draft;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主图。
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverImage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图集。
|
||||||
|
/// </summary>
|
||||||
|
public string? GalleryImages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDineIn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnablePickup { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持配送。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDelivery { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否推荐。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFeatured { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商品命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteProductCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新商品命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateProductCommand : IRequest<ProductDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long CategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品编码。
|
||||||
|
/// </summary>
|
||||||
|
public string SpuCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 副标题。
|
||||||
|
/// </summary>
|
||||||
|
public string? Subtitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单位。
|
||||||
|
/// </summary>
|
||||||
|
public string? Unit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 现价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OriginalPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 库存数量。
|
||||||
|
/// </summary>
|
||||||
|
public int? StockQuantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每单限购。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxQuantityPerOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public ProductStatus Status { get; set; } = ProductStatus.Draft;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主图。
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverImage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图集。
|
||||||
|
/// </summary>
|
||||||
|
public string? GalleryImages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDineIn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnablePickup { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持配送。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDelivery { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否推荐。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFeatured { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long CategoryId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SPU 编码。
|
||||||
|
/// </summary>
|
||||||
|
public string SpuCode { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 副标题。
|
||||||
|
/// </summary>
|
||||||
|
public string? Subtitle { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单位。
|
||||||
|
/// </summary>
|
||||||
|
public string? Unit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 现价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Price { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 库存数量。
|
||||||
|
/// </summary>
|
||||||
|
public int? StockQuantity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每单限购。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxQuantityPerOrder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public ProductStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主图。
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverImage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图集。
|
||||||
|
/// </summary>
|
||||||
|
public string? GalleryImages { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDineIn { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnablePickup { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持配送。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDelivery { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否推荐。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFeatured { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建商品命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateProductCommandHandler(IProductRepository productRepository, ILogger<CreateProductCommandHandler> logger)
|
||||||
|
: IRequestHandler<CreateProductCommand, ProductDto>
|
||||||
|
{
|
||||||
|
private readonly IProductRepository _productRepository = productRepository;
|
||||||
|
private readonly ILogger<CreateProductCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ProductDto> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除商品命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteProductCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<DeleteProductCommandHandler> logger)
|
||||||
|
: IRequestHandler<DeleteProductCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IProductRepository _productRepository = productRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<DeleteProductCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetProductByIdQueryHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetProductByIdQuery, ProductDto?>
|
||||||
|
{
|
||||||
|
private readonly IProductRepository _productRepository = productRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ProductDto?> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchProductsQueryHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchProductsQuery, IReadOnlyList<ProductDto>>
|
||||||
|
{
|
||||||
|
private readonly IProductRepository _productRepository = productRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ProductDto>> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新商品命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateProductCommandHandler(
|
||||||
|
IProductRepository productRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<UpdateProductCommandHandler> logger)
|
||||||
|
: IRequestHandler<UpdateProductCommand, ProductDto?>
|
||||||
|
{
|
||||||
|
private readonly IProductRepository _productRepository = productRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<UpdateProductCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ProductDto?> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商品详情查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetProductByIdQuery : IRequest<ProductDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ProductId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Products.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Products.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Products.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchProductsQuery : IRequest<IReadOnlyList<ProductDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? CategoryId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态过滤。
|
||||||
|
/// </summary>
|
||||||
|
public ProductStatus? Status { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建门店命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateStoreCommand : IRequest<StoreDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店编码。
|
||||||
|
/// </summary>
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责人。
|
||||||
|
/// </summary>
|
||||||
|
public string? ManagerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public StoreStatus Status { get; set; } = StoreStatus.Closed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 省份。
|
||||||
|
/// </summary>
|
||||||
|
public string? Province { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 城市。
|
||||||
|
/// </summary>
|
||||||
|
public string? City { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区县。
|
||||||
|
/// </summary>
|
||||||
|
public string? District { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 详细地址。
|
||||||
|
/// </summary>
|
||||||
|
public string? Address { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经度。
|
||||||
|
/// </summary>
|
||||||
|
public double? Longitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 纬度。
|
||||||
|
/// </summary>
|
||||||
|
public double? Latitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告。
|
||||||
|
/// </summary>
|
||||||
|
public string? Announcement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标签。
|
||||||
|
/// </summary>
|
||||||
|
public string? Tags { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送半径。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryRadiusKm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDineIn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsPickup { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持配送。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDelivery { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除门店命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStoreCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新门店命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateStoreCommand : IRequest<StoreDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店编码。
|
||||||
|
/// </summary>
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责人。
|
||||||
|
/// </summary>
|
||||||
|
public string? ManagerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public StoreStatus Status { get; set; } = StoreStatus.Closed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 省份。
|
||||||
|
/// </summary>
|
||||||
|
public string? Province { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 城市。
|
||||||
|
/// </summary>
|
||||||
|
public string? City { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区县。
|
||||||
|
/// </summary>
|
||||||
|
public string? District { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 详细地址。
|
||||||
|
/// </summary>
|
||||||
|
public string? Address { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经度。
|
||||||
|
/// </summary>
|
||||||
|
public double? Longitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 纬度。
|
||||||
|
/// </summary>
|
||||||
|
public double? Latitude { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告。
|
||||||
|
/// </summary>
|
||||||
|
public string? Announcement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标签。
|
||||||
|
/// </summary>
|
||||||
|
public string? Tags { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送半径。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryRadiusKm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDineIn { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsPickup { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持配送。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDelivery { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long MerchantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店编码。
|
||||||
|
/// </summary>
|
||||||
|
public string Code { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? Phone { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责人。
|
||||||
|
/// </summary>
|
||||||
|
public string? ManagerName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public StoreStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 省份。
|
||||||
|
/// </summary>
|
||||||
|
public string? Province { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 城市。
|
||||||
|
/// </summary>
|
||||||
|
public string? City { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区县。
|
||||||
|
/// </summary>
|
||||||
|
public string? District { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 详细地址。
|
||||||
|
/// </summary>
|
||||||
|
public string? Address { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经度。
|
||||||
|
/// </summary>
|
||||||
|
public double? Longitude { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 纬度。
|
||||||
|
/// </summary>
|
||||||
|
public double? Latitude { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公告。
|
||||||
|
/// </summary>
|
||||||
|
public string? Announcement { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标签。
|
||||||
|
/// </summary>
|
||||||
|
public string? Tags { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认配送半径。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryRadiusKm { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDineIn { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持自提。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsPickup { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持配送。
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDelivery { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建门店命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateStoreCommandHandler(IStoreRepository storeRepository, ILogger<CreateStoreCommandHandler> logger)
|
||||||
|
: IRequestHandler<CreateStoreCommand, StoreDto>
|
||||||
|
{
|
||||||
|
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||||
|
private readonly ILogger<CreateStoreCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<StoreDto> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除门店命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStoreCommandHandler(
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<DeleteStoreCommandHandler> logger)
|
||||||
|
: IRequestHandler<DeleteStoreCommand, bool>
|
||||||
|
{
|
||||||
|
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<DeleteStoreCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoreByIdQueryHandler(
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetStoreByIdQuery, StoreDto?>
|
||||||
|
{
|
||||||
|
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<StoreDto?> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchStoresQueryHandler(
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchStoresQuery, IReadOnlyList<StoreDto>>
|
||||||
|
{
|
||||||
|
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<StoreDto>> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新门店命令处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateStoreCommandHandler(
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ILogger<UpdateStoreCommandHandler> logger)
|
||||||
|
: IRequestHandler<UpdateStoreCommand, StoreDto?>
|
||||||
|
{
|
||||||
|
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||||
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ILogger<UpdateStoreCommandHandler> _logger = logger;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<StoreDto?> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店详情查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoreByIdQuery : IRequest<StoreDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchStoresQuery : IRequest<IReadOnlyList<StoreDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public long? MerchantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态过滤。
|
||||||
|
/// </summary>
|
||||||
|
public StoreStatus? Status { get; init; }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TakeoutSaaS.Domain.Deliveries.Entities;
|
using TakeoutSaaS.Domain.Deliveries.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Enums;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Domain.Deliveries.Repositories;
|
namespace TakeoutSaaS.Domain.Deliveries.Repositories;
|
||||||
|
|
||||||
@@ -40,6 +41,11 @@ public interface IDeliveryRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按状态查询配送单。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<DeliveryOrder>> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新配送单。
|
/// 更新配送单。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TakeoutSaaS.Domain.Payments.Entities;
|
using TakeoutSaaS.Domain.Payments.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Domain.Payments.Repositories;
|
namespace TakeoutSaaS.Domain.Payments.Repositories;
|
||||||
|
|
||||||
@@ -40,6 +41,11 @@ public interface IPaymentRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按状态筛选支付记录。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<PaymentRecord>> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新支付记录。
|
/// 更新支付记录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TakeoutSaaS.Domain.Deliveries.Entities;
|
using TakeoutSaaS.Domain.Deliveries.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Deliveries.Enums;
|
||||||
using TakeoutSaaS.Domain.Deliveries.Repositories;
|
using TakeoutSaaS.Domain.Deliveries.Repositories;
|
||||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
@@ -69,6 +70,28 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
|
|||||||
return _context.SaveChangesAsync(cancellationToken);
|
return _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<DeliveryOrder>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default)
|
public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TakeoutSaaS.Domain.Payments.Entities;
|
using TakeoutSaaS.Domain.Payments.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
using TakeoutSaaS.Domain.Payments.Repositories;
|
using TakeoutSaaS.Domain.Payments.Repositories;
|
||||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
@@ -63,6 +64,23 @@ public sealed class EfPaymentRepository : IPaymentRepository
|
|||||||
return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask();
|
return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<PaymentRecord>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user