feat: add mini ordering catalog and order APIs
All checks were successful
Build and Deploy MiniApi / build-and-deploy (push) Successful in 23s
All checks were successful
Build and Deploy MiniApi / build-and-deploy (push) Successful in 23s
This commit is contained in:
113
src/Api/TakeoutSaaS.MiniApi/Controllers/CatalogController.cs
Normal file
113
src/Api/TakeoutSaaS.MiniApi/Controllers/CatalogController.cs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.App.Mini;
|
||||||
|
using TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.MiniApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序商品与门店查询接口。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Route("api/mini/v{version:apiVersion}")]
|
||||||
|
public sealed class CatalogController(IMiniAppService miniAppService, ITenantProvider tenantProvider) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前租户下可用门店列表。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>门店摘要列表。</returns>
|
||||||
|
[HttpGet("stores")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MiniStoreSummaryDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<MiniStoreSummaryDto>>> GetStoresAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.GetStoresAsync(tenantProvider.GetCurrentTenantId(), cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<MiniStoreSummaryDto>>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定门店在当前履约场景下的商品分类。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="storeId">门店编号。</param>
|
||||||
|
/// <param name="scene">履约场景。</param>
|
||||||
|
/// <param name="channel">访问渠道。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>分类列表。</returns>
|
||||||
|
[HttpGet("categories")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MiniCategoryDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<MiniCategoryDto>>> GetCategoriesAsync([FromQuery] string storeId, [FromQuery] string scene, [FromQuery] string channel, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.GetCategoriesAsync(tenantProvider.GetCurrentTenantId(), storeId, scene, channel, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<MiniCategoryDto>>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定门店的菜单分组与商品列表。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="storeId">门店编号。</param>
|
||||||
|
/// <param name="scene">履约场景。</param>
|
||||||
|
/// <param name="channel">访问渠道。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>菜单分组列表。</returns>
|
||||||
|
[HttpGet("menus/{storeId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MiniMenuSectionDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<MiniMenuSectionDto>>> GetMenuAsync([FromRoute] string storeId, [FromQuery] string scene, [FromQuery] string channel, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.GetMenuAsync(tenantProvider.GetCurrentTenantId(), storeId, scene, channel, cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<MiniMenuSectionDto>>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商品详情与可选规格信息。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="productId">商品编号。</param>
|
||||||
|
/// <param name="scene">履约场景。</param>
|
||||||
|
/// <param name="channel">访问渠道。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>商品详情。</returns>
|
||||||
|
[HttpGet("products/{productId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MiniProductDetailDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<MiniProductDetailDto>> GetProductDetailAsync([FromRoute] string productId, [FromQuery] string scene, [FromQuery] string channel, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.GetProductDetailAsync(tenantProvider.GetCurrentTenantId(), productId, scene, channel, cancellationToken);
|
||||||
|
return data == null ? ApiResponse<MiniProductDetailDto>.Error(ErrorCodes.NotFound, "商品不存在") : ApiResponse<MiniProductDetailDto>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 试算购物车金额。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">试算请求。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>金额试算结果。</returns>
|
||||||
|
[HttpPost("products/price-estimate")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MiniPriceEstimateResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<MiniPriceEstimateResponse>> EstimatePriceAsync([FromBody] MiniPriceEstimateRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.EstimatePriceAsync(tenantProvider.GetCurrentTenantId(), request, cancellationToken);
|
||||||
|
return ApiResponse<MiniPriceEstimateResponse>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单前校验商品与价格是否有效。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">结算校验请求。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>结算校验结果。</returns>
|
||||||
|
[HttpPost("products/checkout-validate")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MiniCheckoutValidationResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<MiniCheckoutValidationResponse>> CheckoutValidateAsync([FromBody] MiniCheckoutValidationRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.ValidateCheckoutAsync(tenantProvider.GetCurrentTenantId(), request, cancellationToken);
|
||||||
|
return ApiResponse<MiniCheckoutValidationResponse>.Ok(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Api/TakeoutSaaS.MiniApi/Controllers/OrdersController.cs
Normal file
87
src/Api/TakeoutSaaS.MiniApi/Controllers/OrdersController.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.App.Mini;
|
||||||
|
using TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.MiniApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序订单接口。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Route("api/mini/v{version:apiVersion}/orders")]
|
||||||
|
public sealed class OrdersController(IMiniAppService miniAppService, ITenantProvider tenantProvider) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前顾客的订单列表。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>订单摘要列表。</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MiniOrderSummaryDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<IReadOnlyList<MiniOrderSummaryDto>>> GetOrdersAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.GetOrdersAsync(tenantProvider.GetCurrentTenantId(), ResolveCustomerPhone(), cancellationToken);
|
||||||
|
return ApiResponse<IReadOnlyList<MiniOrderSummaryDto>>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定订单详情。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderId">订单编号。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>订单详情。</returns>
|
||||||
|
[HttpGet("{orderId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MiniOrderDetailDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<MiniOrderDetailDto>> GetOrderDetailAsync([FromRoute] string orderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.GetOrderDetailAsync(tenantProvider.GetCurrentTenantId(), orderId, ResolveCustomerPhone(), cancellationToken);
|
||||||
|
return data == null ? ApiResponse<MiniOrderDetailDto>.Error(ErrorCodes.NotFound, "订单不存在") : ApiResponse<MiniOrderDetailDto>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">下单请求。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>创建结果。</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MiniCreateOrderResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<MiniCreateOrderResponse>> CreateOrderAsync([FromBody] MiniCreateOrderRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.CreateOrderAsync(tenantProvider.GetCurrentTenantId(), ResolveCustomerName(), ResolveCustomerPhone(), request, cancellationToken);
|
||||||
|
return ApiResponse<MiniCreateOrderResponse>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟支付指定订单。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderId">订单编号。</param>
|
||||||
|
/// <param name="cancellationToken">请求取消令牌。</param>
|
||||||
|
/// <returns>模拟支付结果。</returns>
|
||||||
|
[HttpPost("{orderId}/mock-pay")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<MiniMockPayResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<MiniMockPayResponse>> MockPayAsync([FromRoute] string orderId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var data = await miniAppService.MockPayAsync(tenantProvider.GetCurrentTenantId(), orderId, ResolveCustomerPhone(), cancellationToken);
|
||||||
|
return ApiResponse<MiniMockPayResponse>.Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveCustomerPhone()
|
||||||
|
{
|
||||||
|
return Request.Headers.TryGetValue("X-Mini-Customer-Phone", out var phone) ? phone.FirstOrDefault()?.Trim() ?? string.Empty : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveCustomerName()
|
||||||
|
{
|
||||||
|
return Request.Headers.TryGetValue("X-Mini-Customer-Name", out var name) ? name.FirstOrDefault()?.Trim() ?? string.Empty : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
{
|
{
|
||||||
"Cors": {
|
"Database": {
|
||||||
"Mini": []
|
"DataSources": {
|
||||||
|
"AppDatabase": {
|
||||||
|
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||||
|
"Reads": [
|
||||||
|
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||||
|
],
|
||||||
|
"CommandTimeoutSeconds": 30,
|
||||||
|
"MaxRetryCount": 3,
|
||||||
|
"MaxRetryDelaySeconds": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Otel": {
|
"Cors": { "Mini": [] },
|
||||||
"Endpoint": "",
|
"Otel": { "Endpoint": "", "UseConsoleExporter": true }
|
||||||
"UseConsoleExporter": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
{
|
{
|
||||||
"Cors": {
|
"Database": {
|
||||||
"Mini": []
|
"DataSources": {
|
||||||
|
"AppDatabase": {
|
||||||
|
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||||
|
"Reads": [
|
||||||
|
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||||
|
],
|
||||||
|
"CommandTimeoutSeconds": 30,
|
||||||
|
"MaxRetryCount": 3,
|
||||||
|
"MaxRetryDelaySeconds": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Otel": {
|
"Cors": { "Mini": [] },
|
||||||
"Endpoint": "",
|
"Otel": { "Endpoint": "", "UseConsoleExporter": false }
|
||||||
"UseConsoleExporter": false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类摘要。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniCategoryDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public int Sort { get; init; }
|
||||||
|
public int ProductCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算校验请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniCheckoutValidationRequest
|
||||||
|
{
|
||||||
|
public string StoreId { get; init; } = string.Empty;
|
||||||
|
public string Scene { get; init; } = string.Empty;
|
||||||
|
public string Channel { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<MiniOrderLineInput> Items { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算校验结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniCheckoutValidationResponse
|
||||||
|
{
|
||||||
|
public bool Valid { get; init; }
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniCreateOrderRequest
|
||||||
|
{
|
||||||
|
public string StoreId { get; init; } = string.Empty;
|
||||||
|
public string Scene { get; init; } = string.Empty;
|
||||||
|
public string Channel { get; init; } = string.Empty;
|
||||||
|
public string? Remark { get; init; }
|
||||||
|
public string? TableNo { get; init; }
|
||||||
|
public IReadOnlyList<MiniOrderLineInput> Items { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniCreateOrderResponse
|
||||||
|
{
|
||||||
|
public string OrderId { get; init; } = string.Empty;
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
public string StatusText { get; init; } = string.Empty;
|
||||||
|
public string PaymentStatusText { get; init; } = string.Empty;
|
||||||
|
public decimal PayableAmount { get; init; }
|
||||||
|
public string PayableAmountText { get; init; } = "0.00";
|
||||||
|
public bool MockPayAvailable { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单分组。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniMenuSectionDto
|
||||||
|
{
|
||||||
|
public string CategoryId { get; init; } = string.Empty;
|
||||||
|
public string CategoryName { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<MiniMenuProductDto> Products { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 菜单商品卡片。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniMenuProductDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
public string CoverImageUrl { get; init; } = string.Empty;
|
||||||
|
public decimal Price { get; init; }
|
||||||
|
public string PriceText { get; init; } = "0.00";
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
public string? OriginalPriceText { get; init; }
|
||||||
|
public string SalesText { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<string> TagTexts { get; init; } = [];
|
||||||
|
public bool SoldOut { get; init; }
|
||||||
|
public bool HasOptions { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟支付结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniMockPayResponse
|
||||||
|
{
|
||||||
|
public string OrderId { get; init; } = string.Empty;
|
||||||
|
public string StatusText { get; init; } = string.Empty;
|
||||||
|
public string PaymentStatusText { get; init; } = string.Empty;
|
||||||
|
public decimal PaidAmount { get; init; }
|
||||||
|
public string PaidAmountText { get; init; } = "0.00";
|
||||||
|
public string SuccessTip { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniOrderDetailDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
public string StoreId { get; init; } = string.Empty;
|
||||||
|
public string StoreName { get; init; } = string.Empty;
|
||||||
|
public string StatusText { get; init; } = string.Empty;
|
||||||
|
public string PaymentStatusText { get; init; } = string.Empty;
|
||||||
|
public string Scene { get; init; } = string.Empty;
|
||||||
|
public string CustomerName { get; init; } = string.Empty;
|
||||||
|
public string CustomerPhone { get; init; } = string.Empty;
|
||||||
|
public string TableNo { get; init; } = string.Empty;
|
||||||
|
public string Remark { get; init; } = string.Empty;
|
||||||
|
public decimal ItemsAmount { get; init; }
|
||||||
|
public decimal PackagingFee { get; init; }
|
||||||
|
public decimal DeliveryFee { get; init; }
|
||||||
|
public decimal DiscountAmount { get; init; }
|
||||||
|
public decimal PayableAmount { get; init; }
|
||||||
|
public decimal PaidAmount { get; init; }
|
||||||
|
public string ItemSummary { get; init; } = string.Empty;
|
||||||
|
public string CreatedAt { get; init; } = string.Empty;
|
||||||
|
public string? PaidAt { get; init; }
|
||||||
|
public string ActionText { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<MiniOrderItemDto> Items { get; init; } = [];
|
||||||
|
public IReadOnlyList<MiniOrderTimelineDto> Timeline { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单商品明细。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniOrderItemDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string ProductId { get; init; } = string.Empty;
|
||||||
|
public string ProductName { get; init; } = string.Empty;
|
||||||
|
public string SkuName { get; init; } = string.Empty;
|
||||||
|
public string Unit { get; init; } = string.Empty;
|
||||||
|
public int Quantity { get; init; }
|
||||||
|
public decimal UnitPrice { get; init; }
|
||||||
|
public string UnitPriceText { get; init; } = "0.00";
|
||||||
|
public decimal SubTotal { get; init; }
|
||||||
|
public string SubTotalText { get; init; } = "0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单时间线。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniOrderTimelineDto
|
||||||
|
{
|
||||||
|
public int Status { get; init; }
|
||||||
|
public string StatusText { get; init; } = string.Empty;
|
||||||
|
public string Notes { get; init; } = string.Empty;
|
||||||
|
public string OccurredAt { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算行项目输入。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniOrderLineInput
|
||||||
|
{
|
||||||
|
public string ProductId { get; init; } = string.Empty;
|
||||||
|
public string? SkuId { get; init; }
|
||||||
|
public int Quantity { get; init; }
|
||||||
|
public IReadOnlyList<string> AddonItemIds { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单摘要。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniOrderSummaryDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string OrderNo { get; init; } = string.Empty;
|
||||||
|
public string StoreName { get; init; } = string.Empty;
|
||||||
|
public string StatusText { get; init; } = string.Empty;
|
||||||
|
public string PaymentStatusText { get; init; } = string.Empty;
|
||||||
|
public string Scene { get; init; } = string.Empty;
|
||||||
|
public string ItemSummary { get; init; } = string.Empty;
|
||||||
|
public decimal TotalAmount { get; init; }
|
||||||
|
public string TotalAmountText { get; init; } = "0.00";
|
||||||
|
public string CreatedAt { get; init; } = string.Empty;
|
||||||
|
public string ActionText { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额试算请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniPriceEstimateRequest
|
||||||
|
{
|
||||||
|
public string StoreId { get; init; } = string.Empty;
|
||||||
|
public string Scene { get; init; } = string.Empty;
|
||||||
|
public string Channel { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<MiniOrderLineInput> Items { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额试算结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniPriceEstimateResponse
|
||||||
|
{
|
||||||
|
public string StoreId { get; init; } = string.Empty;
|
||||||
|
public string Scene { get; init; } = string.Empty;
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
public decimal OriginalAmount { get; init; }
|
||||||
|
public string OriginalAmountText { get; init; } = "0.00";
|
||||||
|
public decimal PackagingFee { get; init; }
|
||||||
|
public string PackagingFeeText { get; init; } = "0.00";
|
||||||
|
public decimal DeliveryFee { get; init; }
|
||||||
|
public string DeliveryFeeText { get; init; } = "0.00";
|
||||||
|
public decimal DiscountAmount { get; init; }
|
||||||
|
public string DiscountAmountText { get; init; } = "0.00";
|
||||||
|
public decimal PayableAmount { get; init; }
|
||||||
|
public string PayableAmountText { get; init; } = "0.00";
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniProductDetailDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string StoreId { get; init; } = string.Empty;
|
||||||
|
public string CategoryId { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Subtitle { get; init; } = string.Empty;
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
public string CoverImageUrl { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<string> GalleryImages { get; init; } = [];
|
||||||
|
public string Unit { get; init; } = string.Empty;
|
||||||
|
public decimal BasePrice { get; init; }
|
||||||
|
public string BasePriceText { get; init; } = "0.00";
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
public string? OriginalPriceText { get; init; }
|
||||||
|
public bool SoldOut { get; init; }
|
||||||
|
public int MonthlySales { get; init; }
|
||||||
|
public IReadOnlyList<string> TagTexts { get; init; } = [];
|
||||||
|
public string? DefaultSkuId { get; init; }
|
||||||
|
public IReadOnlyList<MiniProductSkuDto> Skus { get; init; } = [];
|
||||||
|
public IReadOnlyList<MiniProductOptionGroupDto> OptionGroups { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 SKU。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniProductSkuDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public decimal Price { get; init; }
|
||||||
|
public string PriceText { get; init; } = "0.00";
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
public string? OriginalPriceText { get; init; }
|
||||||
|
public int? StockQuantity { get; init; }
|
||||||
|
public bool SoldOut { get; init; }
|
||||||
|
public IReadOnlyList<string> SelectedOptionIds { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品选项分组。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniProductOptionGroupDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string GroupType { get; init; } = string.Empty;
|
||||||
|
public string SelectionType { get; init; } = string.Empty;
|
||||||
|
public bool Required { get; init; }
|
||||||
|
public int MinSelect { get; init; }
|
||||||
|
public int MaxSelect { get; init; }
|
||||||
|
public IReadOnlyList<MiniProductOptionDto> Options { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品选项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniProductOptionDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public decimal ExtraPrice { get; init; }
|
||||||
|
public string ExtraPriceText { get; init; } = "0.00";
|
||||||
|
public bool SoldOut { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店摘要。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniStoreSummaryDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Address { get; init; } = string.Empty;
|
||||||
|
public string BusinessHours { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<string> Supports { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> TagTexts { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using TakeoutSaaS.Application.App.Mini.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Mini;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序真实业务服务抽象。
|
||||||
|
/// </summary>
|
||||||
|
public interface IMiniAppService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询可选门店。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MiniStoreSummaryDto>> GetStoresAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询门店分类。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MiniCategoryDto>> GetCategoriesAsync(long tenantId, string storeId, string scene, string channel, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询门店菜单。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MiniMenuSectionDto>> GetMenuAsync(long tenantId, string storeId, string scene, string channel, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询商品详情。
|
||||||
|
/// </summary>
|
||||||
|
Task<MiniProductDetailDto?> GetProductDetailAsync(long tenantId, string productId, string scene, string channel, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 试算订单金额。
|
||||||
|
/// </summary>
|
||||||
|
Task<MiniPriceEstimateResponse> EstimatePriceAsync(long tenantId, MiniPriceEstimateRequest request, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 校验结算请求。
|
||||||
|
/// </summary>
|
||||||
|
Task<MiniCheckoutValidationResponse> ValidateCheckoutAsync(long tenantId, MiniCheckoutValidationRequest request, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询顾客订单列表。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MiniOrderSummaryDto>> GetOrdersAsync(long tenantId, string customerPhone, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询顾客订单详情。
|
||||||
|
/// </summary>
|
||||||
|
Task<MiniOrderDetailDto?> GetOrderDetailAsync(long tenantId, string orderId, string customerPhone, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 创建订单。
|
||||||
|
/// </summary>
|
||||||
|
Task<MiniCreateOrderResponse> CreateOrderAsync(long tenantId, string customerName, string customerPhone, MiniCreateOrderRequest request, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟支付。
|
||||||
|
/// </summary>
|
||||||
|
Task<MiniMockPayResponse> MockPayAsync(long tenantId, string orderId, string customerPhone, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
92
src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs
Normal file
92
src/Domain/TakeoutSaaS.Domain/Orders/Entities/Order.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Orders.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序订单聚合根。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Order : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <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.Delivery;
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
50
src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs
Normal file
50
src/Domain/TakeoutSaaS.Domain/Orders/Entities/OrderItem.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Orders.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单明细。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderItem : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
/// <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,31 @@
|
|||||||
|
using TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Orders.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态流转记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OrderStatusHistory : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 变更后的状态。
|
||||||
|
/// </summary>
|
||||||
|
public OrderStatus Status { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? OperatorId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 发生时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime OccurredAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
20
src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs
Normal file
20
src/Domain/TakeoutSaaS.Domain/Orders/Enums/DeliveryType.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 履约方式。
|
||||||
|
/// </summary>
|
||||||
|
public enum DeliveryType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 堂食。
|
||||||
|
/// </summary>
|
||||||
|
DineIn = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 自提。
|
||||||
|
/// </summary>
|
||||||
|
Pickup = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 配送。
|
||||||
|
/// </summary>
|
||||||
|
Delivery = 2
|
||||||
|
}
|
||||||
20
src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs
Normal file
20
src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderChannel.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下单渠道。
|
||||||
|
/// </summary>
|
||||||
|
public enum OrderChannel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 未知渠道。
|
||||||
|
/// </summary>
|
||||||
|
Unknown = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序。
|
||||||
|
/// </summary>
|
||||||
|
MiniProgram = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 扫码点餐。
|
||||||
|
/// </summary>
|
||||||
|
ScanToOrder = 2
|
||||||
|
}
|
||||||
32
src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs
Normal file
32
src/Domain/TakeoutSaaS.Domain/Orders/Enums/OrderStatus.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Orders.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态。
|
||||||
|
/// </summary>
|
||||||
|
public enum OrderStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待付款。
|
||||||
|
/// </summary>
|
||||||
|
PendingPayment = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 已付款待制作。
|
||||||
|
/// </summary>
|
||||||
|
AwaitingPreparation = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 制作中。
|
||||||
|
/// </summary>
|
||||||
|
InProgress = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// 待取餐或待送达。
|
||||||
|
/// </summary>
|
||||||
|
Ready = 3,
|
||||||
|
/// <summary>
|
||||||
|
/// 已完成。
|
||||||
|
/// </summary>
|
||||||
|
Completed = 4,
|
||||||
|
/// <summary>
|
||||||
|
/// 已取消。
|
||||||
|
/// </summary>
|
||||||
|
Cancelled = 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using TakeoutSaaS.Domain.Orders.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单写仓储。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOrderRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 新增订单。
|
||||||
|
/// </summary>
|
||||||
|
Task AddAsync(Order order, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 批量新增订单明细。
|
||||||
|
/// </summary>
|
||||||
|
Task AddItemsAsync(IReadOnlyCollection<OrderItem> items, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 新增状态流转记录。
|
||||||
|
/// </summary>
|
||||||
|
Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询可写订单。
|
||||||
|
/// </summary>
|
||||||
|
Task<Order?> FindAsync(long tenantId, long orderId, CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// 保存变更。
|
||||||
|
/// </summary>
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付状态。
|
||||||
|
/// </summary>
|
||||||
|
public enum PaymentStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 未支付。
|
||||||
|
/// </summary>
|
||||||
|
Unpaid = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 支付中。
|
||||||
|
/// </summary>
|
||||||
|
Paying = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 已支付。
|
||||||
|
/// </summary>
|
||||||
|
Paid = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// 支付失败。
|
||||||
|
/// </summary>
|
||||||
|
Failed = 3,
|
||||||
|
/// <summary>
|
||||||
|
/// 已退款。
|
||||||
|
/// </summary>
|
||||||
|
Refunded = 4
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Mini;
|
||||||
|
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Repositories;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Tenancy;
|
||||||
|
using TakeoutSaaS.Module.Tenancy;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Infrastructure.App.Extensions;
|
namespace TakeoutSaaS.Infrastructure.App.Extensions;
|
||||||
|
|
||||||
@@ -9,14 +16,21 @@ namespace TakeoutSaaS.Infrastructure.App.Extensions;
|
|||||||
public static class AppServiceCollectionExtensions
|
public static class AppServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册骨架所需的基础设施能力。
|
/// 注册小程序端基础设施能力。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">服务集合。</param>
|
|
||||||
/// <param name="configuration">配置源。</param>
|
|
||||||
/// <returns>服务集合。</returns>
|
|
||||||
public static IServiceCollection AddAppInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddAppInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_ = configuration;
|
var writeConnectionString = configuration["Database:DataSources:AppDatabase:Write"];
|
||||||
|
if (string.IsNullOrWhiteSpace(writeConnectionString))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("缺少 Database:DataSources:AppDatabase:Write 配置。");
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddDbContext<MiniOrderDbContext>(options => options.UseNpgsql(writeConnectionString));
|
||||||
|
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
||||||
|
services.AddScoped<IMiniAppService, MiniAppService>();
|
||||||
|
services.AddScoped<ITenantCodeResolver, DatabaseTenantCodeResolver>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1402
src/Infrastructure/TakeoutSaaS.Infrastructure/App/MiniAppService.cs
Normal file
1402
src/Infrastructure/TakeoutSaaS.Infrastructure/App/MiniAppService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Orders.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序订单写库上下文。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MiniOrderDbContext(DbContextOptions<MiniOrderDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<Order> Orders => Set<Order>();
|
||||||
|
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
|
||||||
|
public DbSet<OrderStatusHistory> OrderStatusHistories => Set<OrderStatusHistory>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模型配置。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
var orderBuilder = modelBuilder.Entity<Order>();
|
||||||
|
orderBuilder.ToTable("orders");
|
||||||
|
orderBuilder.HasKey(x => x.Id);
|
||||||
|
orderBuilder.Property(x => x.OrderNo).HasMaxLength(64).IsRequired();
|
||||||
|
orderBuilder.Property(x => x.Channel).HasConversion<int>();
|
||||||
|
orderBuilder.Property(x => x.DeliveryType).HasConversion<int>();
|
||||||
|
orderBuilder.Property(x => x.Status).HasConversion<int>();
|
||||||
|
orderBuilder.Property(x => x.PaymentStatus).HasConversion<int>();
|
||||||
|
orderBuilder.Property(x => x.ItemsAmount).HasPrecision(18, 2);
|
||||||
|
orderBuilder.Property(x => x.DiscountAmount).HasPrecision(18, 2);
|
||||||
|
orderBuilder.Property(x => x.PayableAmount).HasPrecision(18, 2);
|
||||||
|
orderBuilder.Property(x => x.PaidAmount).HasPrecision(18, 2);
|
||||||
|
orderBuilder.Property(x => x.CustomerName).HasMaxLength(64);
|
||||||
|
orderBuilder.Property(x => x.CustomerPhone).HasMaxLength(32);
|
||||||
|
orderBuilder.Property(x => x.TableNo).HasMaxLength(64);
|
||||||
|
orderBuilder.Property(x => x.QueueNumber).HasMaxLength(64);
|
||||||
|
orderBuilder.Property(x => x.CancelReason).HasMaxLength(256);
|
||||||
|
orderBuilder.Property(x => x.Remark).HasMaxLength(512);
|
||||||
|
|
||||||
|
var itemBuilder = modelBuilder.Entity<OrderItem>();
|
||||||
|
itemBuilder.ToTable("order_items");
|
||||||
|
itemBuilder.HasKey(x => x.Id);
|
||||||
|
itemBuilder.Property(x => x.ProductName).HasMaxLength(128).IsRequired();
|
||||||
|
itemBuilder.Property(x => x.SkuName).HasMaxLength(256);
|
||||||
|
itemBuilder.Property(x => x.Unit).HasMaxLength(32);
|
||||||
|
itemBuilder.Property(x => x.UnitPrice).HasPrecision(18, 2);
|
||||||
|
itemBuilder.Property(x => x.DiscountAmount).HasPrecision(18, 2);
|
||||||
|
itemBuilder.Property(x => x.SubTotal).HasPrecision(18, 2);
|
||||||
|
itemBuilder.Property(x => x.AttributesJson).HasColumnType("text");
|
||||||
|
itemBuilder.HasOne<Order>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.OrderId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
var historyBuilder = modelBuilder.Entity<OrderStatusHistory>();
|
||||||
|
historyBuilder.ToTable("order_status_histories");
|
||||||
|
historyBuilder.HasKey(x => x.Id);
|
||||||
|
historyBuilder.Property(x => x.Status).HasConversion<int>();
|
||||||
|
historyBuilder.Property(x => x.Notes).HasMaxLength(256);
|
||||||
|
historyBuilder.HasOne<Order>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.OrderId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Orders.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单写仓储 EF 实现。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EfOrderRepository(MiniOrderDbContext dbContext) : IOrderRepository
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddAsync(Order order, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = cancellationToken;
|
||||||
|
dbContext.Orders.Add(order);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddItemsAsync(IReadOnlyCollection<OrderItem> items, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = cancellationToken;
|
||||||
|
if (items.Count > 0)
|
||||||
|
{
|
||||||
|
dbContext.OrderItems.AddRange(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = cancellationToken;
|
||||||
|
dbContext.OrderStatusHistories.Add(history);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Order?> FindAsync(long tenantId, long orderId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return dbContext.Orders.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == orderId && x.DeletedAt == null, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Dapper;
|
||||||
|
using Npgsql;
|
||||||
|
using TakeoutSaaS.Module.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Tenancy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于业务库的租户编码解析器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DatabaseTenantCodeResolver(IConfiguration configuration) : ITenantCodeResolver
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<long?> ResolveAsync(string code, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(code))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectionString = configuration["Database:DataSources:AppDatabase:Reads:0"]
|
||||||
|
?? configuration["Database:DataSources:AppDatabase:Write"];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(connectionString))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var connection = new NpgsqlConnection(connectionString);
|
||||||
|
const string sql = """
|
||||||
|
select "Id"
|
||||||
|
from public.tenants
|
||||||
|
where "DeletedAt" is null
|
||||||
|
and "Code" = @Code
|
||||||
|
limit 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
return await connection.ExecuteScalarAsync<long?>(new CommandDefinition(
|
||||||
|
sql,
|
||||||
|
new { Code = code.Trim() },
|
||||||
|
cancellationToken: cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,12 +5,18 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
<PackageReference Include="Dapper" Version="2.1.72" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||||
<ProjectReference Include="..\..\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj" />
|
<ProjectReference Include="..\..\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj" />
|
||||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user