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": {
|
||||
"Mini": []
|
||||
"Database": {
|
||||
"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": {
|
||||
"Endpoint": "",
|
||||
"UseConsoleExporter": true
|
||||
}
|
||||
"Cors": { "Mini": [] },
|
||||
"Otel": { "Endpoint": "", "UseConsoleExporter": true }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
{
|
||||
"Cors": {
|
||||
"Mini": []
|
||||
"Database": {
|
||||
"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": {
|
||||
"Endpoint": "",
|
||||
"UseConsoleExporter": false
|
||||
}
|
||||
"Cors": { "Mini": [] },
|
||||
"Otel": { "Endpoint": "", "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.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;
|
||||
|
||||
@@ -9,14 +16,21 @@ namespace TakeoutSaaS.Infrastructure.App.Extensions;
|
||||
public static class AppServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册骨架所需的基础设施能力。
|
||||
/// 注册小程序端基础设施能力。
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合。</param>
|
||||
/// <param name="configuration">配置源。</param>
|
||||
/// <returns>服务集合。</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Dapper" Version="2.1.72" />
|
||||
<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>
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.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" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user