feat: add mini ordering catalog and order APIs
All checks were successful
Build and Deploy MiniApi / build-and-deploy (push) Successful in 23s

This commit is contained in:
2026-03-10 10:03:32 +08:00
parent 2a0e2e6d62
commit 227266d183
33 changed files with 2483 additions and 19 deletions

View 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);
}
}

View 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;
}
}

View File

@@ -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 }
}

View File

@@ -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 }
}