refactor: 移除管理端与网关项目
This commit is contained in:
@@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{81034408-37C8-1011-444E-4C15C2FADA8E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.AdminApi", "src\Api\TakeoutSaaS.AdminApi\TakeoutSaaS.AdminApi.csproj", "{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8D626EA8-CB54-BC41-363A-217881BEBA6E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Web", "TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj", "{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}"
|
||||
@@ -37,10 +35,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.MiniApi", "src\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.UserApi", "src\Api\TakeoutSaaS.UserApi\TakeoutSaaS.UserApi.csproj", "{1C0BCC51-AF18-44F3-A1E6-A693F74276B5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gateway", "Gateway", "{6306A8FB-679E-111F-6585-8F70E0EE6013}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.ApiGateway", "src\Gateway\TakeoutSaaS.ApiGateway\TakeoutSaaS.ApiGateway.csproj", "{A2620200-D487-49A7-ABAF-9B84951F81DD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Kernel", "TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Kernel\TakeoutSaaS.Shared.Kernel.csproj", "{BBC99B58-ECA8-42C3-9070-9AA058D778D3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Storage", "src\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj", "{05058F44-6FB7-43AF-8648-8BF538E283EF}"
|
||||
@@ -71,18 +65,6 @@ Global
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954}.Release|x86.Build.0 = Release|Any CPU
|
||||
{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -191,18 +173,6 @@ Global
|
||||
{1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -317,7 +287,6 @@ Global
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{81034408-37C8-1011-444E-4C15C2FADA8E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{0F32CC9C-E8B2-4854-BBF0-D8D2DDFFA954} = {81034408-37C8-1011-444E-4C15C2FADA8E}
|
||||
{8D626EA8-CB54-BC41-363A-217881BEBA6E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{022FCF39-EC48-46EA-AC08-FA2EAD1548B7} = {8D626EA8-CB54-BC41-363A-217881BEBA6E}
|
||||
{0DA03B31-E718-4424-A1F0-9989E79FFE81} = {8D626EA8-CB54-BC41-363A-217881BEBA6E}
|
||||
@@ -332,8 +301,6 @@ Global
|
||||
{5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
|
||||
{12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D} = {81034408-37C8-1011-444E-4C15C2FADA8E}
|
||||
{1C0BCC51-AF18-44F3-A1E6-A693F74276B5} = {81034408-37C8-1011-444E-4C15C2FADA8E}
|
||||
{6306A8FB-679E-111F-6585-8F70E0EE6013} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{A2620200-D487-49A7-ABAF-9B84951F81DD} = {6306A8FB-679E-111F-6585-8F70E0EE6013}
|
||||
{BBC99B58-ECA8-42C3-9070-9AA058D778D3} = {8D626EA8-CB54-BC41-363A-217881BEBA6E}
|
||||
{05058F44-6FB7-43AF-8648-8BF538E283EF} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
|
||||
{5C12177E-6C25-4F78-BFD4-AA073CFC0650} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导出请求。
|
||||
/// </summary>
|
||||
public sealed record DictionaryExportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出格式(csv/json)。
|
||||
/// </summary>
|
||||
public string? Format { get; init; }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导入表单请求。
|
||||
/// </summary>
|
||||
public sealed record DictionaryImportFormRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入文件。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IFormFile File { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 冲突解决模式(Skip/Overwrite/Append)。
|
||||
/// </summary>
|
||||
public string? ConflictMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式(csv/json)。
|
||||
/// </summary>
|
||||
public string? Format { get; init; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 文件上传表单请求。
|
||||
/// </summary>
|
||||
public sealed record FileUploadFormRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传文件。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IFormFile File { get; init; }
|
||||
/// <summary>
|
||||
/// 上传类型。
|
||||
/// </summary>
|
||||
public string? Type { get; init; }
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 租户账单分页查询请求(QueryString 参数)。
|
||||
/// </summary>
|
||||
public sealed record SearchTenantBillsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 账单状态筛选。
|
||||
/// </summary>
|
||||
public TenantBillingStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单起始时间(UTC)筛选。
|
||||
/// </summary>
|
||||
public DateTime? From { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 账单结束时间(UTC)筛选。
|
||||
/// </summary>
|
||||
public DateTime? To { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码(从 1 开始)。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 应用端公告(面向已认证用户)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/app/announcements")]
|
||||
public sealed class AppAnnouncementsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的公告列表(已发布/有效期内)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/app/announcements?page=1&pageSize=20
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[SwaggerOperation(Summary = "获取可见公告列表", Description = "仅返回已发布且在有效期内的公告(含平台公告)。")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = query with
|
||||
{
|
||||
Status = AnnouncementStatus.Published,
|
||||
IsActive = true,
|
||||
OnlyEffective = true
|
||||
};
|
||||
|
||||
var result = await mediator.Send(request, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户未读公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/app/announcements/unread?page=1&pageSize=20
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet("unread")]
|
||||
[SwaggerOperation(Summary = "获取未读公告", Description = "仅返回未读且在有效期内的已发布公告。")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> GetUnread([FromQuery] GetUnreadAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/app/announcements/900123456789012345/mark-read
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "isRead": true
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/mark-read")]
|
||||
[SwaggerOperation(Summary = "标记公告已读", Description = "仅已发布且可见的公告允许标记已读。")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new MarkAnnouncementAsReadCommand { AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
using TakeoutSaaS.Shared.Web.Security;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
/// <summary>
|
||||
/// 管理后台认证接口
|
||||
/// </summary>
|
||||
/// <remarks>提供登录、刷新 Token 以及用户权限查询能力。</remarks>
|
||||
/// <param name="authService">认证服务</param>
|
||||
/// <param name="mediator">中介者。</param>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/auth")]
|
||||
public sealed class AuthController(IAdminAuthService authService, IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录获取 Token
|
||||
/// </summary>
|
||||
/// <param name="request">登录请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>包含访问令牌与刷新令牌的响应。</returns>
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TokenResponse>> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await authService.LoginAsync(request, cancellationToken);
|
||||
return ApiResponse<TokenResponse>.Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 免租户号登录(仅账号+密码)。
|
||||
/// </summary>
|
||||
/// <param name="request">登录请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>包含访问令牌与刷新令牌的响应。</returns>
|
||||
/// <remarks>用于前端简化登录,无需额外传递租户号。</remarks>
|
||||
[HttpPost("login/simple")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TokenResponse>> LoginSimple([FromBody] AdminLoginRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await authService.LoginSimpleAsync(request, cancellationToken);
|
||||
return ApiResponse<TokenResponse>.Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新 Token
|
||||
/// </summary>
|
||||
/// <param name="request">刷新令牌请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>新的访问令牌与刷新令牌。</returns>
|
||||
[HttpPost("refresh")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TokenResponse>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await authService.RefreshTokenAsync(request, cancellationToken);
|
||||
return ApiResponse<TokenResponse>.Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过重置链接令牌重置管理员密码。
|
||||
/// </summary>
|
||||
/// <remarks>令牌为一次性使用;成功后即可使用新密码登录。</remarks>
|
||||
[HttpPost("reset-password")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> ResetPassword([FromBody] ResetAdminPasswordRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 通过令牌重置密码
|
||||
await mediator.Send(new ResetAdminPasswordByTokenCommand
|
||||
{
|
||||
Token = request.Token,
|
||||
NewPassword = request.NewPassword
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回成功
|
||||
return ApiResponse.Success("密码重置成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户信息
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/auth/profile
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "message": "操作成功",
|
||||
/// "data": {
|
||||
/// "userId": "900123456789012345",
|
||||
/// "account": "admin@tenant1",
|
||||
/// "displayName": "租户管理员",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "merchantId": null,
|
||||
/// "roles": ["TenantAdmin"],
|
||||
/// "permissions": ["identity:permission:read", "merchant:read", "order:read"],
|
||||
/// "avatar": "https://cdn.example.com/avatar.png"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>当前用户档案信息。</returns>
|
||||
[HttpGet("profile")]
|
||||
[PermissionAuthorize("identity:profile:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<CurrentUserProfile>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<CurrentUserProfile>), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ApiResponse<CurrentUserProfile>> GetProfile(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 从 JWT 中获取当前用户标识
|
||||
var userId = User.GetUserId();
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
|
||||
}
|
||||
|
||||
// 2. 读取用户档案并返回
|
||||
var profile = await authService.GetProfileAsync(userId, cancellationToken);
|
||||
return ApiResponse<CurrentUserProfile>.Ok(profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户的菜单树(按权限过滤)。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>当前用户可见的菜单树。</returns>
|
||||
[HttpGet("menu")]
|
||||
[PermissionAuthorize("identity:profile:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MenuNodeDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MenuNodeDto>>> GetMenuTree(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取当前用户标识
|
||||
var userId = User.GetUserId();
|
||||
if (userId == 0)
|
||||
{
|
||||
return ApiResponse<IReadOnlyList<MenuNodeDto>>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
|
||||
}
|
||||
// 2. 生成菜单树
|
||||
var menu = await authService.GetMenuTreeAsync(userId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MenuNodeDto>>.Ok(menu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定用户的角色与权限概览(当前租户范围)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/auth/permissions/900123456789012346
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "userId": "900123456789012346",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "merchantId": "200000000000000001",
|
||||
/// "account": "ops.manager",
|
||||
/// "displayName": "运营经理",
|
||||
/// "roles": ["OpsManager", "Reporter"],
|
||||
/// "permissions": ["delivery:read", "order:read", "payment:read"],
|
||||
/// "createdAt": "2025-12-01T08:30:00Z"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
/// <param name="userId">目标用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>用户权限概览,未找到则返回 404。</returns>
|
||||
[HttpGet("permissions/{userId:long}")]
|
||||
[PermissionAuthorize("identity:permission:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserPermissionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserPermissionDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<UserPermissionDto>> GetUserPermissions(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await authService.GetUserPermissionsAsync(userId, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<UserPermissionDto>.Error(ErrorCodes.NotFound, "用户不存在或不属于当前租户")
|
||||
: ApiResponse<UserPermissionDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Billings.Commands;
|
||||
using TakeoutSaaS.Application.App.Billings.Dto;
|
||||
using TakeoutSaaS.Application.App.Billings.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 账单管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/billings")]
|
||||
public sealed class BillingsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询账单列表。
|
||||
/// </summary>
|
||||
/// <returns>账单分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<BillingListDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<BillingListDto>>> GetList([FromQuery] GetBillingListQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询账单列表
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<BillingListDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取账单详情。
|
||||
/// </summary>
|
||||
/// <param name="id">账单 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>账单详情。</returns>
|
||||
[HttpGet("{id:long}")]
|
||||
[PermissionAuthorize("bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BillingDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<BillingDetailDto>> GetDetail(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询账单详情(若不存在则抛出业务异常,由全局异常处理转换为 404)
|
||||
var result = await mediator.Send(new GetBillingDetailQuery { BillingId = id }, cancellationToken);
|
||||
|
||||
// 2. 返回详情
|
||||
return ApiResponse<BillingDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动创建账单。
|
||||
/// </summary>
|
||||
/// <param name="command">创建账单命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建的账单信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("bill:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BillingDetailDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<BillingDetailDto>> Create([FromBody, Required] CreateBillingCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建账单
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<BillingDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新账单状态。
|
||||
/// </summary>
|
||||
/// <param name="id">账单 ID。</param>
|
||||
/// <param name="command">更新状态命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新结果。</returns>
|
||||
[HttpPut("{id:long}/status")]
|
||||
[PermissionAuthorize("bill:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> UpdateStatus(long id, [FromBody, Required] UpdateBillingStatusCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定账单标识
|
||||
command = command with { BillingId = id };
|
||||
|
||||
// 2. 更新账单状态(若不存在则抛出业务异常,由全局异常处理转换为 404)
|
||||
await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回成功结果
|
||||
return ApiResponse<object>.Ok(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消账单。
|
||||
/// </summary>
|
||||
/// <param name="id">账单 ID。</param>
|
||||
/// <param name="reason">取消原因(可选)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>取消结果。</returns>
|
||||
[HttpDelete("{id:long}")]
|
||||
[PermissionAuthorize("bill:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Cancel(long id, [FromQuery] string? reason, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 取消账单(取消原因支持可选)
|
||||
await mediator.Send(new CancelBillingCommand { BillingId = id, Reason = reason ?? string.Empty }, cancellationToken);
|
||||
|
||||
// 2. 返回成功结果
|
||||
return ApiResponse<object>.Ok(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取账单支付记录。
|
||||
/// </summary>
|
||||
/// <param name="id">账单 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>支付记录列表。</returns>
|
||||
[HttpGet("{id:long}/payments")]
|
||||
[PermissionAuthorize("bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<List<PaymentRecordDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<List<PaymentRecordDto>>> GetPayments(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询支付记录
|
||||
var result = await mediator.Send(new GetBillingPaymentsQuery { BillingId = id }, cancellationToken);
|
||||
|
||||
// 2. 返回列表
|
||||
return ApiResponse<List<PaymentRecordDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录支付(线下支付确认)。
|
||||
/// </summary>
|
||||
/// <param name="id">账单 ID。</param>
|
||||
/// <param name="command">记录支付命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>支付记录信息。</returns>
|
||||
[HttpPost("{id:long}/payments")]
|
||||
[PermissionAuthorize("bill:pay")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PaymentRecordDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<PaymentRecordDto>> RecordPayment(long id, [FromBody, Required] RecordPaymentCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定账单标识
|
||||
command = command with { BillingId = id };
|
||||
|
||||
// 2. 记录支付
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回支付记录
|
||||
return ApiResponse<PaymentRecordDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一键确认收款(记录支付 + 立即审核通过)。
|
||||
/// </summary>
|
||||
/// <param name="id">账单 ID。</param>
|
||||
/// <param name="command">确认收款命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>确认后的支付记录。</returns>
|
||||
[HttpPost("{id:long}/payments/confirm")]
|
||||
[PermissionAuthorize("bill:pay")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PaymentRecordDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<PaymentRecordDto>> ConfirmPayment(long id, [FromBody, Required] ConfirmPaymentCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定账单标识
|
||||
command = command with { BillingId = id };
|
||||
|
||||
// 2. 一键确认收款(含:写入 VerifiedBy/VerifiedAt,并同步更新账单已收金额/状态)
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
return ApiResponse<PaymentRecordDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核支付记录。
|
||||
/// </summary>
|
||||
/// <param name="paymentId">支付记录 ID。</param>
|
||||
/// <param name="command">审核参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>审核后的支付记录。</returns>
|
||||
[HttpPut("payments/{paymentId:long}/verify")]
|
||||
[PermissionAuthorize("bill:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PaymentRecordDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<PaymentRecordDto>> VerifyPayment(long paymentId, [FromBody, Required] VerifyPaymentCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定支付记录标识
|
||||
command = command with { PaymentId = paymentId };
|
||||
|
||||
// 2. 审核支付记录
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回审核结果
|
||||
return ApiResponse<PaymentRecordDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新账单状态。
|
||||
/// </summary>
|
||||
/// <param name="command">批量更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新条数。</returns>
|
||||
[HttpPost("batch/status")]
|
||||
[PermissionAuthorize("bill:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<int>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<int>> BatchUpdateStatus([FromBody, Required] BatchUpdateStatusCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行批量更新
|
||||
var affected = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回更新条数
|
||||
return ApiResponse<int>.Ok(affected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出账单(Excel/PDF/CSV)。
|
||||
/// </summary>
|
||||
/// <param name="query">导出请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>导出文件。</returns>
|
||||
[HttpPost("export")]
|
||||
[PermissionAuthorize("bill:read")]
|
||||
[Produces("application/octet-stream")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Export([FromBody, Required] ExportBillingsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行导出
|
||||
var bytes = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 解析格式并生成文件名
|
||||
var extension = ResolveExportFileExtension(query.Format);
|
||||
var fileName = $"billings_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}";
|
||||
|
||||
// 3. 显式写入 Content-Disposition,确保浏览器以附件形式下载
|
||||
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
||||
{
|
||||
FileName = fileName,
|
||||
FileNameStar = fileName
|
||||
}.ToString();
|
||||
|
||||
// 4. 返回二进制流(统一 octet-stream,避免被默认 JSON Produces 影响)
|
||||
return File(bytes, "application/octet-stream");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取账单统计数据。
|
||||
/// </summary>
|
||||
/// <param name="query">统计查询参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>统计结果。</returns>
|
||||
[HttpGet("statistics")]
|
||||
[PermissionAuthorize("bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BillingStatisticsDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<BillingStatisticsDto>> Statistics([FromQuery] GetBillingStatisticsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询统计数据
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回统计结果
|
||||
return ApiResponse<BillingStatisticsDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取逾期账单列表。
|
||||
/// </summary>
|
||||
/// <param name="query">逾期列表查询参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>逾期账单分页结果。</returns>
|
||||
[HttpGet("overdue")]
|
||||
[PermissionAuthorize("bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<BillingListDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<BillingListDto>>> Overdue([FromQuery] GetOverdueBillingsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询逾期账单分页列表
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<BillingListDto>>.Ok(result);
|
||||
}
|
||||
|
||||
private static string ResolveExportFileExtension(string? format)
|
||||
{
|
||||
// 1. 归一化导出格式
|
||||
var normalized = (format ?? string.Empty).Trim();
|
||||
|
||||
// 2. 映射扩展名
|
||||
return normalized.ToUpperInvariant() switch
|
||||
{
|
||||
"PDF" => "pdf",
|
||||
"CSV" => "csv",
|
||||
_ => "xlsx"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Caching;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存监控指标接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Roles = "PlatformAdmin")]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/metrics")]
|
||||
public sealed class CacheMetricsController(
|
||||
CacheMetricsCollector metricsCollector,
|
||||
ICacheInvalidationLogRepository invalidationLogRepository)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取缓存统计信息。
|
||||
/// </summary>
|
||||
[HttpGet("cache-stats")]
|
||||
[ProducesResponseType(typeof(ApiResponse<CacheStatsSnapshot>), StatusCodes.Status200OK)]
|
||||
public ApiResponse<CacheStatsSnapshot> GetCacheStats([FromQuery] string? timeRange = "1h")
|
||||
{
|
||||
var window = timeRange?.ToLowerInvariant() switch
|
||||
{
|
||||
"24h" => TimeSpan.FromHours(24),
|
||||
"7d" => TimeSpan.FromDays(7),
|
||||
_ => TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
var snapshot = metricsCollector.GetSnapshot(window);
|
||||
return ApiResponse<CacheStatsSnapshot>.Ok(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存失效事件列表。
|
||||
/// </summary>
|
||||
[HttpGet("invalidation-events")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<CacheInvalidationLog>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<CacheInvalidationLog>>> GetInvalidationEvents(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var safePage = page <= 0 ? 1 : page;
|
||||
var safePageSize = pageSize <= 0 ? 20 : pageSize;
|
||||
|
||||
var (items, total) = await invalidationLogRepository.GetPagedAsync(
|
||||
safePage,
|
||||
safePageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
cancellationToken);
|
||||
|
||||
var result = new PagedResult<CacheInvalidationLog>(items, safePage, safePageSize, total);
|
||||
return ApiResponse<PagedResult<CacheInvalidationLog>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Deliveries.Commands;
|
||||
using TakeoutSaaS.Application.App.Deliveries.Dto;
|
||||
using TakeoutSaaS.Application.App.Deliveries.Queries;
|
||||
using TakeoutSaaS.Domain.Deliveries.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 配送单管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/deliveries")]
|
||||
public sealed class DeliveriesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建配送单。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建后的配送单。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("delivery:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DeliveryOrderDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DeliveryOrderDto>> Create([FromBody] CreateDeliveryOrderCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建配送单
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<DeliveryOrderDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询配送单列表。
|
||||
/// </summary>
|
||||
/// <param name="orderId">订单 ID。</param>
|
||||
/// <param name="status">配送状态。</param>
|
||||
/// <param name="page">页码。</param>
|
||||
/// <param name="pageSize">每页大小。</param>
|
||||
/// <param name="sortBy">排序字段。</param>
|
||||
/// <param name="sortDesc">是否倒序。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>配送单分页列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("delivery:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<DeliveryOrderDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<DeliveryOrderDto>>> List(
|
||||
[FromQuery] long? orderId,
|
||||
[FromQuery] DeliveryStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组装查询参数
|
||||
var result = await mediator.Send(new SearchDeliveryOrdersQuery
|
||||
{
|
||||
OrderId = orderId,
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<DeliveryOrderDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配送单详情。
|
||||
/// </summary>
|
||||
/// <param name="deliveryOrderId">配送单 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>配送单详情或未找到。</returns>
|
||||
[HttpGet("{deliveryOrderId:long}")]
|
||||
[PermissionAuthorize("delivery:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DeliveryOrderDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<DeliveryOrderDto>> Detail(long deliveryOrderId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询配送单详情
|
||||
var result = await mediator.Send(new GetDeliveryOrderByIdQuery { DeliveryOrderId = deliveryOrderId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<DeliveryOrderDto>.Error(ErrorCodes.NotFound, "配送单不存在")
|
||||
: ApiResponse<DeliveryOrderDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配送单。
|
||||
/// </summary>
|
||||
/// <param name="deliveryOrderId">配送单 ID。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的配送单或未找到。</returns>
|
||||
[HttpPut("{deliveryOrderId:long}")]
|
||||
[PermissionAuthorize("delivery:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DeliveryOrderDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<DeliveryOrderDto>> Update(long deliveryOrderId, [FromBody] UpdateDeliveryOrderCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令携带配送单标识
|
||||
if (command.DeliveryOrderId == 0)
|
||||
{
|
||||
command = command with { DeliveryOrderId = deliveryOrderId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result == null
|
||||
? ApiResponse<DeliveryOrderDto>.Error(ErrorCodes.NotFound, "配送单不存在")
|
||||
: ApiResponse<DeliveryOrderDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除配送单。
|
||||
/// </summary>
|
||||
/// <param name="deliveryOrderId">配送单 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果,未找到则返回错误。</returns>
|
||||
[HttpDelete("{deliveryOrderId:long}")]
|
||||
[PermissionAuthorize("delivery:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long deliveryOrderId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteDeliveryOrderCommand { DeliveryOrderId = deliveryOrderId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "配送单不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Dictionary.Abstractions;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 参数字典管理。
|
||||
/// </summary>
|
||||
/// <param name="dictionaryAppService">字典服务</param>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionaries")]
|
||||
public sealed class DictionaryController(IDictionaryAppService dictionaryAppService) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询字典分组。
|
||||
/// </summary>
|
||||
/// <param name="query">分组查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryGroupDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<DictionaryGroupDto>>> GetGroups([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询字典分组
|
||||
var groups = await dictionaryAppService.SearchGroupsAsync(query, cancellationToken);
|
||||
|
||||
// 2. 返回分组列表
|
||||
return ApiResponse<IReadOnlyList<DictionaryGroupDto>>.Ok(groups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字典分组。
|
||||
/// </summary>
|
||||
/// <param name="request">创建分组请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建后的分组。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("dictionary:group:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> CreateGroup([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建字典分组
|
||||
var group = await dictionaryAppService.CreateGroupAsync(request, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典分组。
|
||||
/// </summary>
|
||||
/// <param name="groupId">分组 ID。</param>
|
||||
/// <param name="request">更新请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的分组。</returns>
|
||||
[HttpPut("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 更新字典分组
|
||||
var group = await dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken);
|
||||
|
||||
// 2. 返回更新结果
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典分组。
|
||||
/// </summary>
|
||||
/// <param name="groupId">分组 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
[HttpDelete("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteGroup(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除字典分组
|
||||
await dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken);
|
||||
|
||||
// 2. 返回成功响应
|
||||
return ApiResponse.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字典项。
|
||||
/// </summary>
|
||||
/// <param name="groupId">分组 ID。</param>
|
||||
/// <param name="request">创建请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建的字典项。</returns>
|
||||
[HttpPost("{groupId:long}/items")]
|
||||
[PermissionAuthorize("dictionary:item:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定分组标识
|
||||
request.GroupId = groupId;
|
||||
|
||||
// 2. 创建字典项
|
||||
var item = await dictionaryAppService.CreateItemAsync(request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典项。
|
||||
/// </summary>
|
||||
/// <param name="itemId">字典项 ID。</param>
|
||||
/// <param name="request">更新请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的字典项。</returns>
|
||||
[HttpPut("items/{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 更新字典项
|
||||
var item = await dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken);
|
||||
|
||||
// 2. 返回更新结果
|
||||
return ApiResponse<DictionaryItemDto>.Ok(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典项。
|
||||
/// </summary>
|
||||
/// <param name="itemId">字典项 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
[HttpDelete("items/{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteItem(long itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除字典项
|
||||
await dictionaryAppService.DeleteItemAsync(itemId, cancellationToken);
|
||||
|
||||
// 2. 返回成功响应
|
||||
return ApiResponse.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取字典项(命中缓存)。
|
||||
/// </summary>
|
||||
/// <param name="request">批量查询请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组编码到字典项列表的映射。</returns>
|
||||
[HttpPost("batch")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>> BatchGet([FromBody] DictionaryBatchQueryRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 批量读取并命中缓存
|
||||
var dictionaries = await dictionaryAppService.GetCachedItemsAsync(request, cancellationToken);
|
||||
|
||||
// 2. 返回批量结果
|
||||
return ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>.Ok(dictionaries);
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 字典分组管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/groups")]
|
||||
public sealed class DictionaryGroupsController(
|
||||
DictionaryCommandService commandService,
|
||||
DictionaryQueryService queryService,
|
||||
DictionaryImportExportService importExportService)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询字典分组。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<DictionaryGroupDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<DictionaryGroupDto>>> List([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await queryService.GetGroupsAsync(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<DictionaryGroupDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典分组详情。
|
||||
/// </summary>
|
||||
[HttpGet("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> Detail(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await queryService.GetGroupByIdAsync(groupId, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<DictionaryGroupDto>.Error(ErrorCodes.NotFound, "字典分组不存在")
|
||||
: ApiResponse<DictionaryGroupDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字典分组。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("dictionary:group:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> Create([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await commandService.CreateGroupAsync(request, cancellationToken);
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典分组。
|
||||
/// </summary>
|
||||
[HttpPut("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> Update(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await commandService.UpdateGroupAsync(groupId, request, cancellationToken);
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典分组。
|
||||
/// </summary>
|
||||
[HttpDelete("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await commandService.DeleteGroupAsync(groupId, cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "字典分组不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出字典分组数据。
|
||||
/// </summary>
|
||||
[HttpPost("{groupId:long}/export")]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[Produces("application/octet-stream")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Export(long groupId, [FromBody] DictionaryExportRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var format = NormalizeFormat(request.Format);
|
||||
await using var stream = new MemoryStream();
|
||||
|
||||
if (format == "json")
|
||||
{
|
||||
await importExportService.ExportToJsonAsync(groupId, stream, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await importExportService.ExportToCsvAsync(groupId, stream, cancellationToken);
|
||||
}
|
||||
|
||||
var extension = format == "json" ? "json" : "csv";
|
||||
var fileName = $"dictionary_{groupId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}";
|
||||
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
||||
{
|
||||
FileName = fileName,
|
||||
FileNameStar = fileName
|
||||
}.ToString();
|
||||
|
||||
var contentType = format == "json" ? MediaTypeNames.Application.Json : "text/csv";
|
||||
return File(stream.ToArray(), contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入字典分组数据。
|
||||
/// </summary>
|
||||
[HttpPost("{groupId:long}/import")]
|
||||
[PermissionAuthorize("dictionary:item:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryImportResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryImportResultDto>> Import(
|
||||
long groupId,
|
||||
[FromForm] DictionaryImportFormRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.File.Length > 10 * 1024 * 1024)
|
||||
{
|
||||
return ApiResponse<DictionaryImportResultDto>.Error(ErrorCodes.BadRequest, "导入文件不能超过 10MB");
|
||||
}
|
||||
|
||||
var format = NormalizeFormat(request.Format);
|
||||
var conflictMode = ParseConflictMode(request.ConflictMode);
|
||||
|
||||
await using var stream = request.File.OpenReadStream();
|
||||
var importRequest = new DictionaryImportRequest
|
||||
{
|
||||
GroupId = groupId,
|
||||
FileName = request.File.FileName,
|
||||
FileSize = request.File.Length,
|
||||
ConflictMode = conflictMode,
|
||||
FileStream = stream
|
||||
};
|
||||
|
||||
var result = format == "json"
|
||||
? await importExportService.ImportFromJsonAsync(importRequest, cancellationToken)
|
||||
: await importExportService.ImportFromCsvAsync(importRequest, cancellationToken);
|
||||
|
||||
return ApiResponse<DictionaryImportResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
private static string NormalizeFormat(string? format)
|
||||
{
|
||||
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "json";
|
||||
}
|
||||
|
||||
return "csv";
|
||||
}
|
||||
|
||||
private static ConflictResolutionMode ParseConflictMode(string? conflictMode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conflictMode))
|
||||
{
|
||||
return ConflictResolutionMode.Skip;
|
||||
}
|
||||
|
||||
return Enum.TryParse<ConflictResolutionMode>(conflictMode, ignoreCase: true, out var mode)
|
||||
? mode
|
||||
: ConflictResolutionMode.Skip;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 字典项管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/groups/{groupId:long}/items")]
|
||||
public sealed class DictionaryItemsController(
|
||||
DictionaryCommandService commandService,
|
||||
DictionaryQueryService queryService)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询字典项列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<DictionaryItemDto>>> List(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await queryService.GetItemsByGroupIdAsync(groupId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<DictionaryItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字典项。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("dictionary:item:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> Create(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.GroupId = groupId;
|
||||
var result = await commandService.CreateItemAsync(request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典项。
|
||||
/// </summary>
|
||||
[HttpPut("{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> Update(long groupId, long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = groupId;
|
||||
var result = await commandService.UpdateItemAsync(itemId, request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典项。
|
||||
/// </summary>
|
||||
[HttpDelete("{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long groupId, long itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = groupId;
|
||||
var success = await commandService.DeleteItemAsync(itemId, cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "字典项不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 字典标签覆盖管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/label-overrides")]
|
||||
public sealed class DictionaryLabelOverridesController(
|
||||
DictionaryLabelOverrideService labelOverrideService,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: BaseApiController
|
||||
{
|
||||
private const string TenantIdHeaderName = "X-Tenant-Id";
|
||||
|
||||
#region 租户端 API(租户覆盖系统字典)
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前租户的标签覆盖列表。
|
||||
/// </summary>
|
||||
[HttpGet("tenant")]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<LabelOverrideDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<LabelOverrideDto>>> ListTenantOverrides(
|
||||
[FromQuery] OverrideType? overrideType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<IReadOnlyList<LabelOverrideDto>>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await labelOverrideService.GetOverridesAsync(tenantId, overrideType, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<LabelOverrideDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户覆盖系统字典项的标签。
|
||||
/// </summary>
|
||||
[HttpPost("tenant")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<LabelOverrideDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<LabelOverrideDto>> CreateTenantOverride(
|
||||
[FromBody] UpsertLabelOverrideRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<LabelOverrideDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var operatorId = currentUserAccessor.UserId;
|
||||
var result = await labelOverrideService.UpsertTenantOverrideAsync(tenantId, request, operatorId, cancellationToken);
|
||||
return ApiResponse<LabelOverrideDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户删除自己的标签覆盖。
|
||||
/// </summary>
|
||||
[HttpDelete("tenant/{dictionaryItemId:long}")]
|
||||
[PermissionAuthorize("dictionary:override:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> DeleteTenantOverride(long dictionaryItemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<object>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var operatorId = currentUserAccessor.UserId;
|
||||
var success = await labelOverrideService.DeleteOverrideAsync(
|
||||
tenantId,
|
||||
dictionaryItemId,
|
||||
operatorId,
|
||||
allowPlatformEnforcement: false,
|
||||
cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "覆盖配置不存在");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 平台端 API(平台管理所有租户的覆盖)
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定租户的所有标签覆盖(平台管理员用)。
|
||||
/// </summary>
|
||||
[HttpGet("platform/{targetTenantId:long}")]
|
||||
[PermissionAuthorize("dictionary:override:platform:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<LabelOverrideDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<LabelOverrideDto>>> ListPlatformOverrides(
|
||||
long targetTenantId,
|
||||
[FromQuery] OverrideType? overrideType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await labelOverrideService.GetOverridesAsync(targetTenantId, overrideType, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<LabelOverrideDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平台强制覆盖租户字典项的标签。
|
||||
/// </summary>
|
||||
[HttpPost("platform/{targetTenantId:long}")]
|
||||
[PermissionAuthorize("dictionary:override:platform:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<LabelOverrideDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<LabelOverrideDto>> CreatePlatformOverride(
|
||||
long targetTenantId,
|
||||
[FromBody] UpsertLabelOverrideRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var operatorId = currentUserAccessor.UserId;
|
||||
var result = await labelOverrideService.UpsertPlatformOverrideAsync(targetTenantId, request, operatorId, cancellationToken);
|
||||
return ApiResponse<LabelOverrideDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平台删除对租户的强制覆盖。
|
||||
/// </summary>
|
||||
[HttpDelete("platform/{targetTenantId:long}/{dictionaryItemId:long}")]
|
||||
[PermissionAuthorize("dictionary:override:platform:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> DeletePlatformOverride(
|
||||
long targetTenantId,
|
||||
long dictionaryItemId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var operatorId = currentUserAccessor.UserId;
|
||||
var success = await labelOverrideService.DeleteOverrideAsync(
|
||||
targetTenantId,
|
||||
dictionaryItemId,
|
||||
operatorId,
|
||||
cancellationToken: cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "覆盖配置不存在");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private ApiResponse<T>? EnsureTenantHeader<T>()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
|
||||
}
|
||||
|
||||
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户字典覆盖配置管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/overrides")]
|
||||
public sealed class DictionaryOverridesController(
|
||||
DictionaryOverrideService overrideService,
|
||||
ITenantProvider tenantProvider)
|
||||
: BaseApiController
|
||||
{
|
||||
private const string TenantIdHeaderName = "X-Tenant-Id";
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前租户的覆盖配置列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<OverrideConfigDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<OverrideConfigDto>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<IReadOnlyList<OverrideConfigDto>>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.GetOverridesAsync(tenantId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定字典分组的覆盖配置。
|
||||
/// </summary>
|
||||
[HttpGet("{groupCode}")]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> Detail(string groupCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.GetOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<OverrideConfigDto>.Error(ErrorCodes.NotFound, "覆盖配置不存在")
|
||||
: ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用覆盖模式。
|
||||
/// </summary>
|
||||
[HttpPost("{groupCode}/enable")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> Enable(string groupCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.EnableOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用覆盖模式。
|
||||
/// </summary>
|
||||
[HttpPost("{groupCode}/disable")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Disable(string groupCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<object>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var success = await overrideService.DisableOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "覆盖配置不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新隐藏的系统字典项。
|
||||
/// </summary>
|
||||
[HttpPut("{groupCode}/hidden-items")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> UpdateHiddenItems(
|
||||
string groupCode,
|
||||
[FromBody] DictionaryOverrideHiddenItemsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.UpdateHiddenItemsAsync(tenantId, groupCode, request.HiddenItemIds, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新自定义排序。
|
||||
/// </summary>
|
||||
[HttpPut("{groupCode}/sort-order")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> UpdateSortOrder(
|
||||
string groupCode,
|
||||
[FromBody] DictionaryOverrideSortOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.UpdateCustomSortOrderAsync(tenantId, groupCode, request.SortOrder, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
private ApiResponse<T>? EnsureTenantHeader<T>()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
|
||||
}
|
||||
|
||||
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
using TakeoutSaaS.Application.Storage.Abstractions;
|
||||
using TakeoutSaaS.Application.Storage.Contracts;
|
||||
using TakeoutSaaS.Application.Storage.Extensions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 管理后台文件上传。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/files")]
|
||||
public sealed class FilesController(IFileStorageService fileStorageService) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传图片或文件。
|
||||
/// </summary>
|
||||
/// <returns>文件上传响应信息。</returns>
|
||||
[HttpPost("upload")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)]
|
||||
public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验文件有效性
|
||||
if (request.File is null || request.File.Length == 0)
|
||||
{
|
||||
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空");
|
||||
}
|
||||
// 2. 解析上传类型
|
||||
if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
|
||||
{
|
||||
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法");
|
||||
}
|
||||
// 3. 提取请求来源
|
||||
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
|
||||
await using var stream = request.File.OpenReadStream();
|
||||
// 4. 调用存储服务执行上传
|
||||
var result = await fileStorageService.UploadAsync(
|
||||
new UploadFileRequest(uploadType, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin),
|
||||
cancellationToken);
|
||||
// 5. 返回上传结果
|
||||
return ApiResponse<FileUploadResponse>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 管理后台 - 健康检查。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/admin/v{version:apiVersion}/[controller]")]
|
||||
public class HealthController : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取服务健康状态。
|
||||
/// </summary>
|
||||
/// <returns>健康状态</returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public ApiResponse<object> Get()
|
||||
{
|
||||
// 1. 构造健康状态
|
||||
var payload = new { status = "OK", service = "AdminApi", time = DateTime.UtcNow };
|
||||
|
||||
// 2. 返回健康响应
|
||||
return ApiResponse<object>.Ok(payload);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Inventory.Commands;
|
||||
using TakeoutSaaS.Application.App.Inventory.Dto;
|
||||
using TakeoutSaaS.Application.App.Inventory.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 库存管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/inventory")]
|
||||
public sealed class InventoryController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询库存。
|
||||
/// </summary>
|
||||
[HttpGet("{productSkuId:long}")]
|
||||
[PermissionAuthorize("inventory:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<InventoryItemDto>> Get(long storeId, long productSkuId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetInventoryItemQuery { StoreId = storeId, ProductSkuId = productSkuId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<InventoryItemDto>.Error(ErrorCodes.NotFound, "库存不存在")
|
||||
: ApiResponse<InventoryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整库存(入库/盘点/报损)。
|
||||
/// </summary>
|
||||
[HttpPost("adjust")]
|
||||
[PermissionAuthorize("inventory:adjust")]
|
||||
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<InventoryItemDto>> Adjust(long storeId, [FromBody] AdjustInventoryCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<InventoryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 锁定库存(下单占用)。
|
||||
/// </summary>
|
||||
[HttpPost("lock")]
|
||||
[PermissionAuthorize("inventory:lock")]
|
||||
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<InventoryItemDto>> Lock(long storeId, [FromBody] LockInventoryCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<InventoryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放库存(取消订单等)。
|
||||
/// </summary>
|
||||
[HttpPost("release")]
|
||||
[PermissionAuthorize("inventory:release")]
|
||||
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<InventoryItemDto>> Release(long storeId, [FromBody] ReleaseInventoryCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<InventoryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扣减库存(支付或履约成功)。
|
||||
/// </summary>
|
||||
[HttpPost("deduct")]
|
||||
[PermissionAuthorize("inventory:deduct")]
|
||||
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<InventoryItemDto>> Deduct(long storeId, [FromBody] DeductInventoryCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<InventoryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询批次列表。
|
||||
/// </summary>
|
||||
[HttpGet("{productSkuId:long}/batches")]
|
||||
[PermissionAuthorize("inventory:batch:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<InventoryBatchDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<InventoryBatchDto>>> GetBatches(long storeId, long productSkuId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetInventoryBatchesQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
ProductSkuId = productSkuId
|
||||
}, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<InventoryBatchDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新批次。
|
||||
/// </summary>
|
||||
[HttpPost("{productSkuId:long}/batches")]
|
||||
[PermissionAuthorize("inventory:batch:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<InventoryBatchDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<InventoryBatchDto>> UpsertBatch(long storeId, long productSkuId, [FromBody] UpsertInventoryBatchCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.ProductSkuId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, ProductSkuId = productSkuId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<InventoryBatchDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放过期锁定。
|
||||
/// </summary>
|
||||
[HttpPost("locks/expire")]
|
||||
[PermissionAuthorize("inventory:release")]
|
||||
[ProducesResponseType(typeof(ApiResponse<int>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<int>> ReleaseExpiredLocks(CancellationToken cancellationToken)
|
||||
{
|
||||
var count = await mediator.Send(new ReleaseExpiredInventoryLocksCommand(), cancellationToken);
|
||||
return ApiResponse<int>.Ok(count);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 菜单管理(可增删改查)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/menus")]
|
||||
public sealed class MenusController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前租户的菜单列表(平铺)。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("identity:menu:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MenuDefinitionDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MenuDefinitionDto>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询菜单列表
|
||||
var result = await mediator.Send(new ListMenusQuery(), cancellationToken);
|
||||
|
||||
// 2. 返回数据
|
||||
return ApiResponse<IReadOnlyList<MenuDefinitionDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取菜单详情。
|
||||
/// </summary>
|
||||
[HttpGet("{menuId:long}")]
|
||||
[PermissionAuthorize("identity:menu:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<MenuDefinitionDto>> Detail(long menuId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询详情
|
||||
var result = await mediator.Send(new MenuDetailQuery { Id = menuId }, cancellationToken);
|
||||
|
||||
// 2. 返回或 404
|
||||
return result is null
|
||||
? ApiResponse<MenuDefinitionDto>.Error(StatusCodes.Status404NotFound, "菜单不存在")
|
||||
: ApiResponse<MenuDefinitionDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建菜单。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("identity:menu:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MenuDefinitionDto>> Create([FromBody, Required] CreateMenuCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 菜单已固定,禁止新增
|
||||
return await Task.FromResult(ApiResponse<MenuDefinitionDto>.Error(StatusCodes.Status403Forbidden, "菜单已固定,禁止新增"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新菜单。
|
||||
/// </summary>
|
||||
[HttpPut("{menuId:long}")]
|
||||
[PermissionAuthorize("identity:menu:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<MenuDefinitionDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<MenuDefinitionDto>> Update(
|
||||
long menuId,
|
||||
[FromBody, Required] UpdateMenuCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 菜单已固定,禁止修改
|
||||
return await Task.FromResult(ApiResponse<MenuDefinitionDto>.Error(StatusCodes.Status403Forbidden, "菜单已固定,禁止修改"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除菜单。
|
||||
/// </summary>
|
||||
[HttpDelete("{menuId:long}")]
|
||||
[PermissionAuthorize("identity:menu:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long menuId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 菜单已固定,禁止删除
|
||||
return await Task.FromResult(ApiResponse<bool>.Error(StatusCodes.Status403Forbidden, "菜单已固定,禁止删除"));
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 商户类目管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/merchant-categories")]
|
||||
public sealed class MerchantCategoriesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 列出所有类目。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>类目列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("merchant_category:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantCategoryDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantCategoryDto>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询所有类目
|
||||
var result = await mediator.Send(new ListMerchantCategoriesQuery(), cancellationToken);
|
||||
|
||||
// 2. 返回类目列表
|
||||
return ApiResponse<IReadOnlyList<MerchantCategoryDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增类目。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建的类目。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("merchant_category:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantCategoryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantCategoryDto>> Create([FromBody] CreateMerchantCategoryCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建类目
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<MerchantCategoryDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除类目。
|
||||
/// </summary>
|
||||
/// <param name="categoryId">类目 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果,未找到则返回错误。</returns>
|
||||
[HttpDelete("{categoryId:long}")]
|
||||
[PermissionAuthorize("merchant_category:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long categoryId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteMerchantCategoryCommand(categoryId), cancellationToken);
|
||||
|
||||
// 2. 返回删除结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "类目不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量调整类目排序。
|
||||
/// </summary>
|
||||
/// <param name="command">排序命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>执行结果。</returns>
|
||||
[HttpPost("reorder")]
|
||||
[PermissionAuthorize("merchant_category:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> Reorder([FromBody] ReorderMerchantCategoriesCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行排序调整
|
||||
await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回成功结果
|
||||
return ApiResponse<object>.Ok(null);
|
||||
}
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 商户管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/merchants")]
|
||||
public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建商户。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建后的商户。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("merchant:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantDto>> Create([FromBody] CreateMerchantCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建商户
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<MerchantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询商户列表。
|
||||
/// </summary>
|
||||
/// <param name="query">查询参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>商户分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantListItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<MerchantListItemDto>>> List(
|
||||
[FromQuery] GetMerchantListQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<MerchantListItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 待审核商户列表。
|
||||
/// </summary>
|
||||
[HttpGet("pending-review")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantReviewListItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<MerchantReviewListItemDto>>> PendingReviewList(
|
||||
[FromQuery] GetPendingReviewListQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<MerchantReviewListItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新商户。
|
||||
/// </summary>
|
||||
/// <param name="merchantId">商户 ID。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的商户或未找到。</returns>
|
||||
[HttpPut("{merchantId:long}")]
|
||||
[PermissionAuthorize("merchant:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UpdateMerchantResultDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<UpdateMerchantResultDto>), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<UpdateMerchantResultDto>> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.MerchantId != 0 && command.MerchantId != merchantId)
|
||||
{
|
||||
return ApiResponse<UpdateMerchantResultDto>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
|
||||
}
|
||||
|
||||
command = command with { MerchantId = merchantId };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
if (result == null)
|
||||
{
|
||||
return ApiResponse<UpdateMerchantResultDto>.Error(ErrorCodes.NotFound, "商户不存在");
|
||||
}
|
||||
|
||||
if (result.RequiresReview)
|
||||
{
|
||||
return ApiResponse<UpdateMerchantResultDto>.Error(
|
||||
ErrorCodes.ValidationFailed,
|
||||
"关键信息修改,商户已进入待审核状态,业务已冻结")
|
||||
with { Data = result };
|
||||
}
|
||||
|
||||
return ApiResponse<UpdateMerchantResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除商户。
|
||||
/// </summary>
|
||||
/// <param name="merchantId">商户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{merchantId:long}")]
|
||||
[PermissionAuthorize("merchant:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteMerchantCommand { MerchantId = merchantId }, cancellationToken);
|
||||
|
||||
// 2. 返回删除结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "商户不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商户概览。
|
||||
/// </summary>
|
||||
/// <param name="merchantId">商户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>商户概览或未找到。</returns>
|
||||
[HttpGet("{merchantId:long}")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<MerchantDetailDto>> Detail(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询商户概览
|
||||
var result = await mediator.Send(new GetMerchantDetailQuery(merchantId), cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<MerchantDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取审核领取信息。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/review/claim")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto?>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ClaimInfoDto?>> GetReviewClaim(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetMerchantReviewClaimQuery(merchantId), cancellationToken);
|
||||
return ApiResponse<ClaimInfoDto?>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 领取审核。
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:long}/review/claim")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ClaimInfoDto>> ClaimReview(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ClaimMerchantReviewCommand { MerchantId = merchantId }, cancellationToken);
|
||||
return ApiResponse<ClaimInfoDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放审核领取。
|
||||
/// </summary>
|
||||
[HttpDelete("{merchantId:long}/review/claim")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto?>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ClaimInfoDto?>> ReleaseReviewClaim(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ReleaseClaimCommand { MerchantId = merchantId }, cancellationToken);
|
||||
return ApiResponse<ClaimInfoDto?>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商户详细资料(含证照、合同)。
|
||||
/// </summary>
|
||||
/// <returns>创建的证照信息。</returns>
|
||||
[HttpGet("{merchantId:long}/detail")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDetailDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantDetailDto>> FullDetail(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询商户详细资料
|
||||
var result = await mediator.Send(new GetMerchantDetailQuery(merchantId), cancellationToken);
|
||||
|
||||
// 2. 返回详情
|
||||
return ApiResponse<MerchantDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传商户证照信息(先通过文件上传接口获取 COS 地址)。
|
||||
/// </summary>
|
||||
/// <returns>创建的证照信息。</returns>
|
||||
[HttpPost("{merchantId:long}/documents")]
|
||||
[PermissionAuthorize("merchant:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDocumentDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantDocumentDto>> CreateDocument(
|
||||
long merchantId,
|
||||
[FromBody] AddMerchantDocumentCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定商户标识
|
||||
var command = body with { MerchantId = merchantId };
|
||||
|
||||
// 2. 创建证照记录
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<MerchantDocumentDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 商户证照列表。
|
||||
/// </summary>
|
||||
/// <returns>商户证照列表。</returns>
|
||||
[HttpGet("{merchantId:long}/documents")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDocumentDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantDocumentDto>>> Documents(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询证照列表
|
||||
var result = await mediator.Send(new GetMerchantDocumentsQuery(merchantId), cancellationToken);
|
||||
|
||||
// 2. 返回证照集合
|
||||
return ApiResponse<IReadOnlyList<MerchantDocumentDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核指定证照。
|
||||
/// </summary>
|
||||
/// <returns>审核后的证照信息。</returns>
|
||||
[HttpPost("{merchantId:long}/documents/{documentId:long}/review")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDocumentDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantDocumentDto>> ReviewDocument(
|
||||
long merchantId,
|
||||
long documentId,
|
||||
[FromBody] ReviewMerchantDocumentCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定商户与证照标识
|
||||
var command = body with { MerchantId = merchantId, DocumentId = documentId };
|
||||
|
||||
// 2. 执行审核
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<MerchantDocumentDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增商户合同。
|
||||
/// </summary>
|
||||
/// <returns>创建的合同信息。</returns>
|
||||
[HttpPost("{merchantId:long}/contracts")]
|
||||
[PermissionAuthorize("merchant:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantContractDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantContractDto>> CreateContract(
|
||||
long merchantId,
|
||||
[FromBody] CreateMerchantContractCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定商户标识
|
||||
var command = body with { MerchantId = merchantId };
|
||||
|
||||
// 2. 创建合同
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<MerchantContractDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合同列表。
|
||||
/// </summary>
|
||||
/// <returns>商户合同列表。</returns>
|
||||
[HttpGet("{merchantId:long}/contracts")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantContractDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantContractDto>>> Contracts(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询合同列表
|
||||
var result = await mediator.Send(new GetMerchantContractsQuery(merchantId), cancellationToken);
|
||||
|
||||
// 2. 返回合同集合
|
||||
return ApiResponse<IReadOnlyList<MerchantContractDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新合同状态(生效/终止等)。
|
||||
/// </summary>
|
||||
/// <returns>更新后的合同信息。</returns>
|
||||
[HttpPut("{merchantId:long}/contracts/{contractId:long}/status")]
|
||||
[PermissionAuthorize("merchant:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantContractDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantContractDto>> UpdateContractStatus(
|
||||
long merchantId,
|
||||
long contractId,
|
||||
[FromBody] UpdateMerchantContractStatusCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定商户与合同标识
|
||||
var command = body with { MerchantId = merchantId, ContractId = contractId };
|
||||
|
||||
// 2. 更新合同状态
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<MerchantContractDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核商户(通过/驳回)。
|
||||
/// </summary>
|
||||
/// <returns>审核后的商户信息。</returns>
|
||||
[HttpPost("{merchantId:long}/review")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<MerchantDto>> Review(long merchantId, [FromBody] ReviewMerchantCommand body, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定商户标识
|
||||
var command = body with { MerchantId = merchantId };
|
||||
|
||||
// 2. 执行审核
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<MerchantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销审核。
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:long}/review/revoke")]
|
||||
[PermissionAuthorize("merchant:review:revoke")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> RevokeReview(
|
||||
long merchantId,
|
||||
[FromBody] RevokeMerchantReviewCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (body.MerchantId != 0 && body.MerchantId != merchantId)
|
||||
{
|
||||
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
|
||||
}
|
||||
|
||||
var command = new RevokeMerchantReviewCommand
|
||||
{
|
||||
MerchantId = merchantId,
|
||||
Reason = body.Reason
|
||||
};
|
||||
await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<object>.Ok(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核历史。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/audit-history")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantAuditLogDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantAuditLogDto>>> AuditHistory(
|
||||
long merchantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetMerchantAuditHistoryQuery(merchantId), cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MerchantAuditLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更历史。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/change-history")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantChangeLogDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantChangeLogDto>>> ChangeHistory(
|
||||
long merchantId,
|
||||
[FromQuery] string? fieldName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetMerchantChangeHistoryQuery(merchantId, fieldName), cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MerchantChangeLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核日志。
|
||||
/// </summary>
|
||||
/// <returns>商户审核日志分页结果。</returns>
|
||||
[HttpGet("{merchantId:long}/audits")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantAuditLogDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<MerchantAuditLogDto>>> AuditLogs(
|
||||
long merchantId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询审核日志
|
||||
var result = await mediator.Send(new GetMerchantAuditLogsQuery(merchantId, page, pageSize), cancellationToken);
|
||||
|
||||
// 2. 返回日志分页
|
||||
return ApiResponse<PagedResult<MerchantAuditLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出商户 PDF。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/export-pdf")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[Produces("application/pdf")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ExportPdf(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var bytes = await mediator.Send(new ExportMerchantPdfQuery(merchantId), cancellationToken);
|
||||
var fileName = $"merchant_{merchantId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.pdf";
|
||||
|
||||
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
||||
{
|
||||
FileName = fileName,
|
||||
FileNameStar = fileName
|
||||
}.ToString();
|
||||
|
||||
return File(bytes, "application/pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可选商户类目列表。
|
||||
/// </summary>
|
||||
/// <returns>可选的商户类目列表。</returns>
|
||||
[HttpGet("categories")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<string>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<string>>> Categories(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询可选类目
|
||||
var result = await mediator.Send(new GetMerchantCategoriesQuery(), cancellationToken);
|
||||
|
||||
// 2. 返回类目列表
|
||||
return ApiResponse<IReadOnlyList<string>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Orders.Commands;
|
||||
using TakeoutSaaS.Application.App.Orders.Dto;
|
||||
using TakeoutSaaS.Application.App.Orders.Queries;
|
||||
using TakeoutSaaS.Domain.Orders.Enums;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 订单管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/orders")]
|
||||
public sealed class OrdersController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建订单。
|
||||
/// </summary>
|
||||
/// <returns>创建的订单信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("order:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OrderDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OrderDto>> Create([FromBody] CreateOrderCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建订单
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<OrderDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单列表。
|
||||
/// </summary>
|
||||
/// <returns>订单分页列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("order:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<OrderDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<OrderDto>>> List(
|
||||
[FromQuery] long? storeId,
|
||||
[FromQuery] OrderStatus? status,
|
||||
[FromQuery] PaymentStatus? paymentStatus,
|
||||
[FromQuery] string? orderNo,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组装查询参数并执行查询
|
||||
var result = await mediator.Send(new SearchOrdersQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
Status = status,
|
||||
PaymentStatus = paymentStatus,
|
||||
OrderNo = orderNo,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<OrderDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取订单详情。
|
||||
/// </summary>
|
||||
/// <returns>订单详情。</returns>
|
||||
[HttpGet("{orderId:long}")]
|
||||
[PermissionAuthorize("order:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OrderDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<OrderDto>> Detail(long orderId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询订单详情
|
||||
var result = await mediator.Send(new GetOrderByIdQuery { OrderId = orderId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<OrderDto>.Error(ErrorCodes.NotFound, "订单不存在")
|
||||
: ApiResponse<OrderDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新订单。
|
||||
/// </summary>
|
||||
/// <returns>更新后的订单信息。</returns>
|
||||
[HttpPut("{orderId:long}")]
|
||||
[PermissionAuthorize("order:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OrderDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<OrderDto>> Update(long orderId, [FromBody] UpdateOrderCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令包含订单标识
|
||||
if (command.OrderId == 0)
|
||||
{
|
||||
command = command with { OrderId = orderId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result == null
|
||||
? ApiResponse<OrderDto>.Error(ErrorCodes.NotFound, "订单不存在")
|
||||
: ApiResponse<OrderDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除订单。
|
||||
/// </summary>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{orderId:long}")]
|
||||
[PermissionAuthorize("order:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long orderId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteOrderCommand { OrderId = orderId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "订单不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Payments.Commands;
|
||||
using TakeoutSaaS.Application.App.Payments.Dto;
|
||||
using TakeoutSaaS.Application.App.Payments.Queries;
|
||||
using TakeoutSaaS.Domain.Payments.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 支付记录管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/payments")]
|
||||
public sealed class PaymentsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建支付记录。
|
||||
/// </summary>
|
||||
/// <returns>创建的支付记录信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("payment:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PaymentDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PaymentDto>> Create([FromBody] CreatePaymentCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建支付记录
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<PaymentDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询支付记录列表。
|
||||
/// </summary>
|
||||
/// <returns>支付记录分页列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("payment:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<PaymentDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<PaymentDto>>> List(
|
||||
[FromQuery] long? orderId,
|
||||
[FromQuery] PaymentStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组装查询参数并执行查询
|
||||
var result = await mediator.Send(new SearchPaymentsQuery
|
||||
{
|
||||
OrderId = orderId,
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<PaymentDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取支付记录详情。
|
||||
/// </summary>
|
||||
/// <returns>支付记录详情。</returns>
|
||||
[HttpGet("{paymentId:long}")]
|
||||
[PermissionAuthorize("payment:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PaymentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<PaymentDto>> Detail(long paymentId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询支付记录详情
|
||||
var result = await mediator.Send(new GetPaymentByIdQuery { PaymentId = paymentId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<PaymentDto>.Error(ErrorCodes.NotFound, "支付记录不存在")
|
||||
: ApiResponse<PaymentDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新支付记录。
|
||||
/// </summary>
|
||||
/// <returns>更新后的支付记录信息。</returns>
|
||||
[HttpPut("{paymentId:long}")]
|
||||
[PermissionAuthorize("payment:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PaymentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<PaymentDto>> Update(long paymentId, [FromBody] UpdatePaymentCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令包含支付记录标识
|
||||
if (command.PaymentId == 0)
|
||||
{
|
||||
command = command with { PaymentId = paymentId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result == null
|
||||
? ApiResponse<PaymentDto>.Error(ErrorCodes.NotFound, "支付记录不存在")
|
||||
: ApiResponse<PaymentDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除支付记录。
|
||||
/// </summary>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{paymentId:long}")]
|
||||
[PermissionAuthorize("payment:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long paymentId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeletePaymentCommand { PaymentId = paymentId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "支付记录不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 权限管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/permissions")]
|
||||
public sealed class PermissionsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询权限。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:GET /api/admin/v1/permissions?keyword=order&page=1&pageSize=20
|
||||
/// </remarks>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限的分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("identity:permission:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<PermissionDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<PermissionDto>>> Search([FromQuery] SearchPermissionsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<PermissionDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取权限树。
|
||||
/// </summary>
|
||||
/// <param name="keyword">关键字(可选)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限树列表。</returns>
|
||||
[HttpGet("tree")]
|
||||
[PermissionAuthorize("identity:permission:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionTreeDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<PermissionTreeDto>>> Tree([FromQuery] string? keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 构造查询对象
|
||||
var query = new PermissionTreeQuery { Keyword = keyword };
|
||||
|
||||
// 2. 查询权限树
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
return ApiResponse<IReadOnlyList<PermissionTreeDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建权限。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建的权限。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("identity:permission:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PermissionDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PermissionDto>> Create([FromBody, Required] CreatePermissionCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 权限已固定,禁止新增
|
||||
return await Task.FromResult(ApiResponse<PermissionDto>.Error(StatusCodes.Status403Forbidden, "权限已固定,禁止新增"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的权限,未找到时返回 404。</returns>
|
||||
[HttpPut("{permissionId:long}")]
|
||||
[PermissionAuthorize("identity:permission:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PermissionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<PermissionDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<PermissionDto>> Update(long permissionId, [FromBody, Required] UpdatePermissionCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 权限已固定,禁止修改
|
||||
return await Task.FromResult(ApiResponse<PermissionDto>.Error(StatusCodes.Status403Forbidden, "权限已固定,禁止修改"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{permissionId:long}")]
|
||||
[PermissionAuthorize("identity:permission:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long permissionId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 权限已固定,禁止删除
|
||||
return await Task.FromResult(ApiResponse<bool>.Error(StatusCodes.Status403Forbidden, "权限已固定,禁止删除"));
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 平台公告管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/platform/announcements")]
|
||||
[Route("api/admin/v{version:apiVersion}/platform/announcements")]
|
||||
public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建平台公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/platform/announcements
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "平台升级通知",
|
||||
/// "content": "系统将于今晚 23:00 维护。",
|
||||
/// "announcementType": 0,
|
||||
/// "priority": 10,
|
||||
/// "effectiveFrom": "2025-12-20T00:00:00Z",
|
||||
/// "effectiveTo": null,
|
||||
/// "targetType": "all",
|
||||
/// "targetParameters": null
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "0",
|
||||
/// "title": "平台升级通知",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "创建平台公告", Description = "需要权限:platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Create([FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with
|
||||
{
|
||||
TenantId = 0,
|
||||
PublisherScope = PublisherScope.Platform
|
||||
};
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询平台公告列表。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/platform/announcements?page=1&pageSize=20&status=Published
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("platform-announcement:read", "platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "查询平台公告列表", Description = "需要权限:platform-announcement:read 或 platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = query with { TenantId = 0 };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取平台公告详情。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/platform/announcements/900123456789012345
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "0",
|
||||
/// "title": "平台升级通知",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet("{announcementId:long}")]
|
||||
[PermissionAuthorize("platform-announcement:read", "platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "获取平台公告详情", Description = "需要权限:platform-announcement:read 或 platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await ExecuteAsPlatformAsync(() =>
|
||||
mediator.Send(new GetAnnouncementByIdQuery { TenantId = 0, AnnouncementId = announcementId }, cancellationToken));
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新平台公告(仅草稿)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// PUT /api/platform/announcements/900123456789012345
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "平台升级通知(更新)",
|
||||
/// "content": "维护时间调整为 23:30。",
|
||||
/// "targetType": "all",
|
||||
/// "targetParameters": null,
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPut("{announcementId:long}")]
|
||||
[PermissionAuthorize("platform-announcement:create")]
|
||||
[SwaggerOperation(Summary = "更新平台公告", Description = "仅草稿可更新;需要权限:platform-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { TenantId = 0, AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布平台公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/platform/announcements/900123456789012345/publish
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Published"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/publish")]
|
||||
[PermissionAuthorize("platform-announcement:publish")]
|
||||
[SwaggerOperation(Summary = "发布平台公告", Description = "需要权限:platform-announcement:publish")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(command, cancellationToken));
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销平台公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/platform/announcements/900123456789012345/revoke
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Revoked"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/revoke")]
|
||||
[PermissionAuthorize("platform-announcement:revoke")]
|
||||
[SwaggerOperation(Summary = "撤销平台公告", Description = "需要权限:platform-announcement:revoke")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(command, cancellationToken));
|
||||
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsPlatformAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
var original = tenantContextAccessor.Current;
|
||||
tenantContextAccessor.Current = new TenantContext(0, null, "platform");
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContextAccessor.Current = original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Products.Commands;
|
||||
using TakeoutSaaS.Application.App.Products.Dto;
|
||||
using TakeoutSaaS.Application.App.Products.Queries;
|
||||
using TakeoutSaaS.Domain.Products.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 商品管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/products")]
|
||||
public sealed class ProductsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建商品。
|
||||
/// </summary>
|
||||
/// <returns>创建的商品信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("product:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ProductDto>> Create([FromBody] CreateProductCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建商品
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<ProductDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询商品列表。
|
||||
/// </summary>
|
||||
/// <returns>商品分页列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("product:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<ProductDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<ProductDto>>> List(
|
||||
[FromQuery] long? storeId,
|
||||
[FromQuery] long? categoryId,
|
||||
[FromQuery] ProductStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组装查询参数并执行查询
|
||||
var result = await mediator.Send(new SearchProductsQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
CategoryId = categoryId,
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<ProductDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品详情。
|
||||
/// </summary>
|
||||
/// <returns>商品详情。</returns>
|
||||
[HttpGet("{productId:long}")]
|
||||
[PermissionAuthorize("product:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<ProductDto>> Detail(long productId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询商品详情
|
||||
var result = await mediator.Send(new GetProductByIdQuery { ProductId = productId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||
: ApiResponse<ProductDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新商品。
|
||||
/// </summary>
|
||||
/// <returns>更新后的商品信息。</returns>
|
||||
[HttpPut("{productId:long}")]
|
||||
[PermissionAuthorize("product:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<ProductDto>> Update(long productId, [FromBody] UpdateProductCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令包含商品标识
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result == null
|
||||
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||
: ApiResponse<ProductDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除商品。
|
||||
/// </summary>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{productId:long}")]
|
||||
[PermissionAuthorize("product:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long productId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteProductCommand { ProductId = productId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "商品不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品全量详情。
|
||||
/// </summary>
|
||||
[HttpGet("{productId:long}/detail")]
|
||||
[PermissionAuthorize("product:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<ProductDetailDto>> FullDetail(long productId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetProductDetailQuery { ProductId = productId }, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<ProductDetailDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||
: ApiResponse<ProductDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上架商品。
|
||||
/// </summary>
|
||||
[HttpPost("{productId:long}/publish")]
|
||||
[PermissionAuthorize("product:publish")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<ProductDto>> Publish(long productId, [FromBody] PublishProductCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||
: ApiResponse<ProductDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下架商品。
|
||||
/// </summary>
|
||||
[HttpPost("{productId:long}/unpublish")]
|
||||
[PermissionAuthorize("product:publish")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<ProductDto>> Unpublish(long productId, [FromBody] UnpublishProductCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<ProductDto>.Error(ErrorCodes.NotFound, "商品不存在")
|
||||
: ApiResponse<ProductDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换商品 SKU。
|
||||
/// </summary>
|
||||
[HttpPut("{productId:long}/skus")]
|
||||
[PermissionAuthorize("product-sku:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductSkuDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductSkuDto>>> ReplaceSkus(long productId, [FromBody] ReplaceProductSkusCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<ProductSkuDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换商品规格。
|
||||
/// </summary>
|
||||
[HttpPut("{productId:long}/attributes")]
|
||||
[PermissionAuthorize("product-attr:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductAttributeGroupDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductAttributeGroupDto>>> ReplaceAttributes(long productId, [FromBody] ReplaceProductAttributesCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<ProductAttributeGroupDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换商品加料。
|
||||
/// </summary>
|
||||
[HttpPut("{productId:long}/addons")]
|
||||
[PermissionAuthorize("product-addon:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductAddonGroupDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductAddonGroupDto>>> ReplaceAddons(long productId, [FromBody] ReplaceProductAddonsCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<ProductAddonGroupDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换商品媒资。
|
||||
/// </summary>
|
||||
[HttpPut("{productId:long}/media")]
|
||||
[PermissionAuthorize("product-media:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductMediaAssetDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductMediaAssetDto>>> ReplaceMedia(long productId, [FromBody] ReplaceProductMediaCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<ProductMediaAssetDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换商品价格策略。
|
||||
/// </summary>
|
||||
[HttpPut("{productId:long}/pricing-rules")]
|
||||
[PermissionAuthorize("product-pricing:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ProductPricingRuleDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ProductPricingRuleDto>>> ReplacePricingRules(long productId, [FromBody] ReplaceProductPricingRulesCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.ProductId == 0)
|
||||
{
|
||||
command = command with { ProductId = productId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<ProductPricingRuleDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 公共租户套餐查询接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting("public-self-service")]
|
||||
[Route("api/public/v{version:apiVersion}/tenant-packages")]
|
||||
public sealed class PublicTenantPackagesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页获取已启用的租户套餐。
|
||||
/// </summary>
|
||||
/// <param name="query">分页参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>启用套餐的分页列表。</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantPackageDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantPackageDto>>> List(
|
||||
[FromQuery, Required] GetPublicTenantPackagesQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行查询
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
// 2. 返回结果
|
||||
return ApiResponse<PagedResult<TenantPackageDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 公域租户订阅自助接口(需登录,无权限校验)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[EnableRateLimiting("public-self-service")]
|
||||
[Route("api/public/v{version:apiVersion}/tenants")]
|
||||
public sealed class PublicTenantSubscriptionsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 初次绑定租户订阅(默认 0 个月)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="body">绑定请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>绑定后的订阅信息。</returns>
|
||||
[HttpPost("{tenantId:long}/subscriptions/initial")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status409Conflict)]
|
||||
public async Task<ApiResponse<TenantSubscriptionDto>> BindInitialSubscription(
|
||||
long tenantId,
|
||||
[FromBody, Required] BindInitialTenantSubscriptionCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 合并路由租户标识
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 执行初次订阅绑定
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回绑定结果
|
||||
return ApiResponse<TenantSubscriptionDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 公域租户自助入住接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting("public-self-service")]
|
||||
[Route("api/public/v{version:apiVersion}/tenants")]
|
||||
public sealed class PublicTenantsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 自助注册租户并生成初始管理员。
|
||||
/// </summary>
|
||||
/// <param name="command">自助注册命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>注册结果(含临时密码)。</returns>
|
||||
[HttpPost("self-register")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SelfRegisterResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<SelfRegisterResultDto>> SelfRegister(
|
||||
[FromBody, Required] SelfRegisterTenantCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行自助注册
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<SelfRegisterResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自助提交或更新实名资料。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="command">实名资料。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>实名资料结果。</returns>
|
||||
[HttpPost("{tenantId:long}/verification")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantVerificationDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantVerificationDto>> SubmitVerification(
|
||||
long tenantId,
|
||||
[FromBody, Required] SubmitTenantVerificationCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户 ID
|
||||
var merged = command with { TenantId = tenantId };
|
||||
// 2. 提交实名
|
||||
var result = await mediator.Send(merged, cancellationToken);
|
||||
return ApiResponse<TenantVerificationDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询租户入住进度。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>入住进度。</returns>
|
||||
[HttpGet("{tenantId:long}/status")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantProgressDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantProgressDto>> Progress(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询进度
|
||||
var query = new GetTenantProgressQuery { TenantId = tenantId };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<TenantProgressDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.QuotaPackages.Commands;
|
||||
using TakeoutSaaS.Application.App.QuotaPackages.Dto;
|
||||
using TakeoutSaaS.Application.App.QuotaPackages.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 配额包管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/quota-packages")]
|
||||
public sealed class QuotaPackagesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 配额包列表。
|
||||
/// </summary>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>配额包分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("quota-package:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<QuotaPackageListDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<QuotaPackageListDto>>> List([FromQuery] GetQuotaPackageListQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询配额包分页
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<PagedResult<QuotaPackageListDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建配额包。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建后的配额包。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("quota-package:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<QuotaPackageDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<QuotaPackageDto>> Create([FromBody, Required] CreateQuotaPackageCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行创建
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<QuotaPackageDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配额包。
|
||||
/// </summary>
|
||||
/// <param name="quotaPackageId">配额包 ID。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的配额包或未找到。</returns>
|
||||
[HttpPut("{quotaPackageId:long}")]
|
||||
[PermissionAuthorize("quota-package:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<QuotaPackageDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<QuotaPackageDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<QuotaPackageDto>> Update(long quotaPackageId, [FromBody, Required] UpdateQuotaPackageCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { QuotaPackageId = quotaPackageId };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result is null
|
||||
? ApiResponse<QuotaPackageDto>.Error(StatusCodes.Status404NotFound, "配额包不存在")
|
||||
: ApiResponse<QuotaPackageDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除配额包。
|
||||
/// </summary>
|
||||
/// <param name="quotaPackageId">配额包 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{quotaPackageId:long}")]
|
||||
[PermissionAuthorize("quota-package:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long quotaPackageId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 构建删除命令
|
||||
var command = new DeleteQuotaPackageCommand { QuotaPackageId = quotaPackageId };
|
||||
|
||||
// 2. 执行删除并返回
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上架/下架配额包。
|
||||
/// </summary>
|
||||
/// <param name="quotaPackageId">配额包 ID。</param>
|
||||
/// <param name="command">状态更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新结果。</returns>
|
||||
[HttpPut("{quotaPackageId:long}/status")]
|
||||
[PermissionAuthorize("quota-package:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> UpdateStatus(long quotaPackageId, [FromBody, Required] UpdateQuotaPackageStatusCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { QuotaPackageId = quotaPackageId };
|
||||
|
||||
// 2. 执行状态更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为租户购买配额包。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="command">购买命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>购买记录。</returns>
|
||||
[HttpPost("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-packages")]
|
||||
[PermissionAuthorize("tenant:quota:purchase")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantQuotaPurchaseDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantQuotaPurchaseDto>> PurchaseForTenant(
|
||||
long tenantId,
|
||||
[FromBody, Required] PurchaseQuotaPackageCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户 ID
|
||||
command = command with { TenantId = tenantId };
|
||||
|
||||
// 2. 执行购买
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回购买结果
|
||||
return ApiResponse<TenantQuotaPurchaseDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户配额使用情况。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>配额使用情况列表。</returns>
|
||||
[HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-usage")]
|
||||
[PermissionAuthorize("tenant:quota:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<TenantQuotaUsageDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<TenantQuotaUsageDto>>> GetTenantQuotaUsage(
|
||||
long tenantId,
|
||||
[FromQuery] GetTenantQuotaUsageQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户 ID
|
||||
query = query with { TenantId = tenantId };
|
||||
|
||||
// 2. 查询配额使用情况
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
return ApiResponse<IReadOnlyList<TenantQuotaUsageDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户配额购买记录。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>购买记录分页结果。</returns>
|
||||
[HttpGet("~/api/admin/v{version:apiVersion}/tenants/{tenantId:long}/quota-purchases")]
|
||||
[PermissionAuthorize("tenant:quota:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantQuotaPurchaseDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantQuotaPurchaseDto>>> GetTenantQuotaPurchases(
|
||||
long tenantId,
|
||||
[FromQuery] GetTenantQuotaPurchasesQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户 ID
|
||||
query = query with { TenantId = tenantId };
|
||||
|
||||
// 2. 查询购买记录
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
return ApiResponse<PagedResult<TenantQuotaPurchaseDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 角色模板管理(平台蓝本)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/role-templates")]
|
||||
public sealed class RoleTemplatesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询角色模板。
|
||||
/// </summary>
|
||||
/// <param name="isActive">是否启用筛选。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色模板列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("identity:role:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleTemplateDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<RoleTemplateDto>>> List([FromQuery] bool? isActive, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 构造查询参数
|
||||
var query = new ListRoleTemplatesQuery { IsActive = isActive };
|
||||
|
||||
// 2. 查询模板集合
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回模板列表
|
||||
return ApiResponse<IReadOnlyList<RoleTemplateDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 克隆角色模板。
|
||||
/// </summary>
|
||||
/// <param name="templateCode">源模板编码。</param>
|
||||
/// <param name="command">克隆命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>新模板详情。</returns>
|
||||
[HttpPost("{templateCode}/clone")]
|
||||
[PermissionAuthorize("role-template:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<RoleTemplateDto>> Clone(
|
||||
string templateCode,
|
||||
[FromBody, Required] CloneRoleTemplateCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定源模板编码
|
||||
command = command with { SourceTemplateCode = templateCode };
|
||||
|
||||
// 2. 执行克隆
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回新模板
|
||||
return ApiResponse<RoleTemplateDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取角色模板详情。
|
||||
/// </summary>
|
||||
/// <param name="templateCode">模板编码。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色模板详情。</returns>
|
||||
[HttpGet("{templateCode}")]
|
||||
[PermissionAuthorize("identity:role:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<RoleTemplateDto>> Detail(string templateCode, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询模板详情
|
||||
var result = await mediator.Send(new GetRoleTemplateQuery { TemplateCode = templateCode }, cancellationToken);
|
||||
|
||||
// 2. 返回模板或 404
|
||||
return result is null
|
||||
? ApiResponse<RoleTemplateDto>.Error(StatusCodes.Status404NotFound, "角色模板不存在")
|
||||
: ApiResponse<RoleTemplateDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建角色模板。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建后的模板。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("role-template:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<RoleTemplateDto>> Create([FromBody, Required] CreateRoleTemplateCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建模板
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<RoleTemplateDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取模板的权限列表。
|
||||
/// </summary>
|
||||
/// <param name="templateCode">模板编码。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限集合。</returns>
|
||||
[HttpGet("{templateCode}/permissions")]
|
||||
[PermissionAuthorize("identity:role:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionTemplateDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<PermissionTemplateDto>>> GetPermissions(string templateCode, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询模板权限
|
||||
var result = await mediator.Send(new RoleTemplatePermissionsQuery { TemplateCode = templateCode }, cancellationToken);
|
||||
|
||||
// 2. 返回权限集合
|
||||
return ApiResponse<IReadOnlyList<PermissionTemplateDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新角色模板。
|
||||
/// </summary>
|
||||
/// <param name="templateCode">模板编码。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的模板。</returns>
|
||||
[HttpPut("{templateCode}")]
|
||||
[PermissionAuthorize("role-template:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<RoleTemplateDto>> Update(
|
||||
string templateCode,
|
||||
[FromBody, Required] UpdateRoleTemplateCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定模板编码
|
||||
command = command with { TemplateCode = templateCode };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result is null
|
||||
? ApiResponse<RoleTemplateDto>.Error(StatusCodes.Status404NotFound, "角色模板不存在")
|
||||
: ApiResponse<RoleTemplateDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除角色模板。
|
||||
/// </summary>
|
||||
/// <param name="templateCode">模板编码。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{templateCode}")]
|
||||
[PermissionAuthorize("role-template:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(string templateCode, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var result = await mediator.Send(new DeleteRoleTemplateCommand { TemplateCode = templateCode }, cancellationToken);
|
||||
|
||||
// 2. 返回执行结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前租户批量初始化预置角色模板。
|
||||
/// </summary>
|
||||
/// <param name="command">初始化命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>生成的租户角色列表。</returns>
|
||||
[HttpPost("init")]
|
||||
[PermissionAuthorize("identity:role:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<RoleDto>>> Initialize(
|
||||
[FromBody] InitializeRoleTemplatesCommand? command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令存在
|
||||
command ??= new InitializeRoleTemplatesCommand();
|
||||
|
||||
// 2. 初始化模板到租户
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回新建的角色列表
|
||||
return ApiResponse<IReadOnlyList<RoleDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将单个模板初始化到当前租户。
|
||||
/// </summary>
|
||||
/// <param name="templateCode">模板编码。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>生成的角色列表。</returns>
|
||||
[HttpPost("{templateCode}/initialize-tenant")]
|
||||
[PermissionAuthorize("identity:role:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<RoleDto>>> InitializeSingle(string templateCode, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 构造初始化命令
|
||||
var command = new InitializeRoleTemplatesCommand
|
||||
{
|
||||
TemplateCodes = new[] { templateCode }
|
||||
};
|
||||
|
||||
// 2. 初始化模板到租户
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回生成的角色列表
|
||||
return ApiResponse<IReadOnlyList<RoleDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Statistics.Dto;
|
||||
using TakeoutSaaS.Application.App.Statistics.Queries;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 统计数据接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/statistics")]
|
||||
public sealed class StatisticsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取订阅概览统计。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>订阅概览数据。</returns>
|
||||
[HttpGet("subscription-overview")]
|
||||
[PermissionAuthorize("statistics:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionOverviewDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<SubscriptionOverviewDto>> GetSubscriptionOverview(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetSubscriptionOverviewQuery(), cancellationToken);
|
||||
return ApiResponse<SubscriptionOverviewDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配额使用排行。
|
||||
/// </summary>
|
||||
/// <param name="quotaType">配额类型。</param>
|
||||
/// <param name="topN">返回前N条记录,默认10。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>配额使用排行数据。</returns>
|
||||
[HttpGet("quota-ranking")]
|
||||
[PermissionAuthorize("statistics:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<QuotaUsageRankingDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<QuotaUsageRankingDto>> GetQuotaRanking(
|
||||
[FromQuery] TenantQuotaType quotaType,
|
||||
[FromQuery] int topN = 10,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetQuotaUsageRankingQuery { QuotaType = quotaType, TopN = topN };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<QuotaUsageRankingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取收入统计。
|
||||
/// </summary>
|
||||
/// <param name="monthsCount">统计月份数量,默认12个月。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>收入统计数据。</returns>
|
||||
[HttpGet("revenue")]
|
||||
[PermissionAuthorize("statistics:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RevenueStatisticsDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<RevenueStatisticsDto>> GetRevenue(
|
||||
[FromQuery] int monthsCount = 12,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetRevenueStatisticsQuery { MonthsCount = monthsCount };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<RevenueStatisticsDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取即将到期的订阅列表。
|
||||
/// </summary>
|
||||
/// <param name="daysAhead">筛选天数,默认7天内到期。</param>
|
||||
/// <param name="onlyWithoutAutoRenew">是否只返回未开启自动续费的订阅。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>即将到期的订阅列表。</returns>
|
||||
[HttpGet("expiring-subscriptions")]
|
||||
[PermissionAuthorize("statistics:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<ExpiringSubscriptionDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<ExpiringSubscriptionDto>>> GetExpiringSubscriptions(
|
||||
[FromQuery] int daysAhead = 7,
|
||||
[FromQuery] bool onlyWithoutAutoRenew = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetExpiringSubscriptionsQuery
|
||||
{
|
||||
DaysAhead = daysAhead,
|
||||
OnlyWithoutAutoRenew = onlyWithoutAutoRenew
|
||||
};
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<ExpiringSubscriptionDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核与风控管理(平台)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/platform/store-audits")]
|
||||
[Route("api/admin/v{version:apiVersion}/platform/store-audits")]
|
||||
public sealed class StoreAuditsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询待审核门店列表。
|
||||
/// </summary>
|
||||
/// <returns>待审核门店分页列表。</returns>
|
||||
[HttpGet("pending")]
|
||||
[PermissionAuthorize("store-audit:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<PendingStoreAuditDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<PendingStoreAuditDto>>> ListPending(
|
||||
[FromQuery] ListPendingStoreAuditsQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询待审核门店列表
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken));
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<PendingStoreAuditDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店审核详情。
|
||||
/// </summary>
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>审核详情。</returns>
|
||||
[HttpGet("{storeId:long}")]
|
||||
[PermissionAuthorize("store-audit:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreAuditDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreAuditDetailDto>> GetDetail(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取审核详情
|
||||
var result = await ExecuteAsPlatformAsync(() =>
|
||||
mediator.Send(new GetStoreAuditDetailQuery { StoreId = storeId }, cancellationToken));
|
||||
|
||||
// 2. 返回详情或未找到
|
||||
return result is null
|
||||
? ApiResponse<StoreAuditDetailDto>.Error(ErrorCodes.NotFound, "门店不存在")
|
||||
: ApiResponse<StoreAuditDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核通过。
|
||||
/// </summary>
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="command">审核命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
[HttpPost("{storeId:long}/approve")]
|
||||
[PermissionAuthorize("store-audit:approve")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreAuditActionResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreAuditActionResultDto>> Approve(
|
||||
long storeId,
|
||||
[FromBody, Required] ApproveStoreCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行审核通过
|
||||
var request = command with { StoreId = storeId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<StoreAuditActionResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核驳回。
|
||||
/// </summary>
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="command">驳回命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
[HttpPost("{storeId:long}/reject")]
|
||||
[PermissionAuthorize("store-audit:reject")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreAuditActionResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreAuditActionResultDto>> Reject(
|
||||
long storeId,
|
||||
[FromBody, Required] RejectStoreCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行审核驳回
|
||||
var request = command with { StoreId = storeId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<StoreAuditActionResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询审核记录。
|
||||
/// </summary>
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="page">页码。</param>
|
||||
/// <param name="pageSize">每页数量。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>审核记录分页列表。</returns>
|
||||
[HttpGet("{storeId:long}/records")]
|
||||
[PermissionAuthorize("store-audit:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<StoreAuditRecordDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<StoreAuditRecordDto>>> ListRecords(
|
||||
long storeId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 执行记录查询
|
||||
var query = new ListStoreAuditRecordsQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken));
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<StoreAuditRecordDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取审核统计数据。
|
||||
/// </summary>
|
||||
/// <param name="query">查询参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>统计数据。</returns>
|
||||
[HttpGet("statistics")]
|
||||
[PermissionAuthorize("store-audit:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreAuditStatisticsDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreAuditStatisticsDto>> GetStatistics(
|
||||
[FromQuery] GetStoreAuditStatisticsQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行统计查询
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken));
|
||||
|
||||
// 2. 返回统计结果
|
||||
return ApiResponse<StoreAuditStatisticsDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制关闭门店。
|
||||
/// </summary>
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="command">关闭命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
[HttpPost("{storeId:long}/force-close")]
|
||||
[PermissionAuthorize("store-audit:force-close")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreAuditActionResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreAuditActionResultDto>> ForceClose(
|
||||
long storeId,
|
||||
[FromBody, Required] ForceCloseStoreCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行强制关闭
|
||||
var request = command with { StoreId = storeId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<StoreAuditActionResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解除强制关闭。
|
||||
/// </summary>
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="command">解除命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
[HttpPost("{storeId:long}/reopen")]
|
||||
[PermissionAuthorize("store-audit:force-close")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreAuditActionResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreAuditActionResultDto>> Reopen(
|
||||
long storeId,
|
||||
[FromBody, Required] ReopenStoreCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行解除强制关闭
|
||||
var request = command with { StoreId = storeId };
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<StoreAuditActionResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsPlatformAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
var original = tenantContextAccessor.Current;
|
||||
tenantContextAccessor.Current = new TenantContext(0, null, "platform");
|
||||
|
||||
// 1. (空行后) 切换到平台上下文执行
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContextAccessor.Current = original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店自提管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/pickup")]
|
||||
public sealed class StorePickupController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取自提配置。
|
||||
/// </summary>
|
||||
[HttpGet("settings")]
|
||||
[PermissionAuthorize("pickup-setting:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StorePickupSettingDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StorePickupSettingDto>> GetSetting(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetStorePickupSettingQuery { StoreId = storeId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<StorePickupSettingDto>.Error(ErrorCodes.NotFound, "未配置自提设置")
|
||||
: ApiResponse<StorePickupSettingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新自提配置。
|
||||
/// </summary>
|
||||
[HttpPut("settings")]
|
||||
[PermissionAuthorize("pickup-setting:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StorePickupSettingDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StorePickupSettingDto>> UpsertSetting(long storeId, [FromBody] UpsertStorePickupSettingCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StorePickupSettingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询档期列表。
|
||||
/// </summary>
|
||||
[HttpGet("slots")]
|
||||
[PermissionAuthorize("pickup-slot:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StorePickupSlotDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StorePickupSlotDto>>> ListSlots(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStorePickupSlotsQuery { StoreId = storeId }, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<StorePickupSlotDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建档期。
|
||||
/// </summary>
|
||||
[HttpPost("slots")]
|
||||
[PermissionAuthorize("pickup-slot:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StorePickupSlotDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StorePickupSlotDto>> CreateSlot(long storeId, [FromBody] CreateStorePickupSlotCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StorePickupSlotDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新档期。
|
||||
/// </summary>
|
||||
[HttpPut("slots/{slotId:long}")]
|
||||
[PermissionAuthorize("pickup-slot:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StorePickupSlotDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StorePickupSlotDto>> UpdateSlot(long storeId, long slotId, [FromBody] UpdateStorePickupSlotCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.SlotId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, SlotId = slotId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<StorePickupSlotDto>.Error(ErrorCodes.NotFound, "档期不存在")
|
||||
: ApiResponse<StorePickupSlotDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除档期。
|
||||
/// </summary>
|
||||
[HttpDelete("slots/{slotId:long}")]
|
||||
[PermissionAuthorize("pickup-slot:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteSlot(long storeId, long slotId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStorePickupSlotCommand { StoreId = storeId, SlotId = slotId }, cancellationToken);
|
||||
return success ? ApiResponse<object>.Ok(null) : ApiResponse<object>.Error(ErrorCodes.NotFound, "档期不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店资质预警(平台)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/platform/store-qualifications")]
|
||||
[Route("api/admin/v{version:apiVersion}/platform/store-qualifications")]
|
||||
public sealed class StoreQualificationsController(
|
||||
IMediator mediator,
|
||||
ITenantContextAccessor tenantContextAccessor)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询资质即将过期/已过期列表。
|
||||
/// </summary>
|
||||
/// <param name="query">查询参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>资质预警分页结果。</returns>
|
||||
[HttpGet("expiring")]
|
||||
[PermissionAuthorize("store-qualification:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreQualificationAlertResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreQualificationAlertResultDto>> ListExpiring(
|
||||
[FromQuery] ListExpiringStoreQualificationsQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询资质预警
|
||||
var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken));
|
||||
|
||||
// 2. (空行后) 返回结果
|
||||
return ApiResponse<StoreQualificationAlertResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsPlatformAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
var original = tenantContextAccessor.Current;
|
||||
tenantContextAccessor.Current = new TenantContext(0, null, "platform");
|
||||
|
||||
// 1. (空行后) 切换到平台上下文执行
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContextAccessor.Current = original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店排班管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/shifts")]
|
||||
public sealed class StoreShiftsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询排班(默认未来 7 天)。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("store-shift:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreEmployeeShiftDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreEmployeeShiftDto>>> List(
|
||||
long storeId,
|
||||
[FromQuery] DateTime? from,
|
||||
[FromQuery] DateTime? to,
|
||||
[FromQuery] long? staffId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreEmployeeShiftsQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
From = from,
|
||||
To = to,
|
||||
StaffId = staffId
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<StoreEmployeeShiftDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建排班。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("store-shift:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreEmployeeShiftDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreEmployeeShiftDto>> Create(long storeId, [FromBody] CreateStoreEmployeeShiftCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StoreEmployeeShiftDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新排班。
|
||||
/// </summary>
|
||||
[HttpPut("{shiftId:long}")]
|
||||
[PermissionAuthorize("store-shift:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreEmployeeShiftDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreEmployeeShiftDto>> Update(long storeId, long shiftId, [FromBody] UpdateStoreEmployeeShiftCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.ShiftId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, ShiftId = shiftId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreEmployeeShiftDto>.Error(ErrorCodes.NotFound, "排班不存在")
|
||||
: ApiResponse<StoreEmployeeShiftDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除排班。
|
||||
/// </summary>
|
||||
[HttpDelete("{shiftId:long}")]
|
||||
[PermissionAuthorize("store-shift:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long storeId, long shiftId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreEmployeeShiftCommand { StoreId = storeId, ShiftId = shiftId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "排班不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店员工管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/staffs")]
|
||||
public sealed class StoreStaffsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询门店员工列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("store-staff:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreStaffDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreStaffDto>>> List(
|
||||
long storeId,
|
||||
[FromQuery] StaffRoleType? role,
|
||||
[FromQuery] StaffStatus? status,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreStaffQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
RoleType = role,
|
||||
Status = status
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<StoreStaffDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建门店员工。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("store-staff:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreStaffDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreStaffDto>> Create(long storeId, [FromBody] CreateStoreStaffCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StoreStaffDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店员工。
|
||||
/// </summary>
|
||||
[HttpPut("{staffId:long}")]
|
||||
[PermissionAuthorize("store-staff:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreStaffDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreStaffDto>> Update(long storeId, long staffId, [FromBody] UpdateStoreStaffCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.StaffId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, StaffId = staffId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreStaffDto>.Error(ErrorCodes.NotFound, "员工不存在")
|
||||
: ApiResponse<StoreStaffDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店员工。
|
||||
/// </summary>
|
||||
[HttpDelete("{staffId:long}")]
|
||||
[PermissionAuthorize("store-staff:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long storeId, long staffId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreStaffCommand { StoreId = storeId, StaffId = staffId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "员工不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店桌台区域管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/table-areas")]
|
||||
public sealed class StoreTableAreasController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询区域列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("store-table-area:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreTableAreaDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreTableAreaDto>>> List(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreTableAreasQuery { StoreId = storeId }, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<StoreTableAreaDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建区域。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("store-table-area:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreTableAreaDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreTableAreaDto>> Create(long storeId, [FromBody] CreateStoreTableAreaCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StoreTableAreaDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新区域。
|
||||
/// </summary>
|
||||
[HttpPut("{areaId:long}")]
|
||||
[PermissionAuthorize("store-table-area:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreTableAreaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreTableAreaDto>> Update(long storeId, long areaId, [FromBody] UpdateStoreTableAreaCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.AreaId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, AreaId = areaId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreTableAreaDto>.Error(ErrorCodes.NotFound, "桌台区域不存在")
|
||||
: ApiResponse<StoreTableAreaDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除区域。
|
||||
/// </summary>
|
||||
[HttpDelete("{areaId:long}")]
|
||||
[PermissionAuthorize("store-table-area:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long storeId, long areaId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreTableAreaCommand { StoreId = storeId, AreaId = areaId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "桌台区域不存在或不可删除");
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店桌码管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/tables")]
|
||||
public sealed class StoreTablesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询桌码列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("store-table:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreTableDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreTableDto>>> List(
|
||||
long storeId,
|
||||
[FromQuery] long? areaId,
|
||||
[FromQuery] StoreTableStatus? status,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreTablesQuery
|
||||
{
|
||||
StoreId = storeId,
|
||||
AreaId = areaId,
|
||||
Status = status
|
||||
}, cancellationToken);
|
||||
|
||||
return ApiResponse<IReadOnlyList<StoreTableDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量生成桌码。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("store-table:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreTableDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreTableDto>>> Generate(long storeId, [FromBody] GenerateStoreTablesCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<StoreTableDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新桌码。
|
||||
/// </summary>
|
||||
[HttpPut("{tableId:long}")]
|
||||
[PermissionAuthorize("store-table:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreTableDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreTableDto>> Update(long storeId, long tableId, [FromBody] UpdateStoreTableCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.TableId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, TableId = tableId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreTableDto>.Error(ErrorCodes.NotFound, "桌码不存在")
|
||||
: ApiResponse<StoreTableDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除桌码。
|
||||
/// </summary>
|
||||
[HttpDelete("{tableId:long}")]
|
||||
[PermissionAuthorize("store-table:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long storeId, long tableId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreTableCommand { StoreId = storeId, TableId = tableId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "桌码不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出桌码二维码 ZIP。
|
||||
/// </summary>
|
||||
[HttpPost("export")]
|
||||
[PermissionAuthorize("store-table:export")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Export(long storeId, [FromBody] ExportStoreTableQRCodesQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
if (query.StoreId == 0)
|
||||
{
|
||||
query = query with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
if (result is null)
|
||||
{
|
||||
return Ok(ApiResponse<object>.Error(ErrorCodes.NotFound, "未找到可导出的桌码"));
|
||||
}
|
||||
|
||||
return File(result.Content, result.ContentType, result.FileName);
|
||||
}
|
||||
}
|
||||
@@ -1,581 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/stores")]
|
||||
public sealed class StoresController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建门店。
|
||||
/// </summary>
|
||||
/// <returns>创建的门店信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("store:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreDto>> Create([FromBody] CreateStoreCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建门店
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<StoreDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询门店列表。
|
||||
/// </summary>
|
||||
/// <returns>门店分页列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<StoreDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<StoreDto>>> List(
|
||||
[FromQuery] long? merchantId,
|
||||
[FromQuery] StoreStatus? status,
|
||||
[FromQuery] StoreAuditStatus? auditStatus,
|
||||
[FromQuery] StoreBusinessStatus? businessStatus,
|
||||
[FromQuery] StoreOwnershipType? ownershipType,
|
||||
[FromQuery] string? keyword,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组装查询参数并执行查询
|
||||
var result = await mediator.Send(new SearchStoresQuery
|
||||
{
|
||||
MerchantId = merchantId,
|
||||
Status = status,
|
||||
AuditStatus = auditStatus,
|
||||
BusinessStatus = businessStatus,
|
||||
OwnershipType = ownershipType,
|
||||
Keyword = keyword,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<StoreDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店详情。
|
||||
/// </summary>
|
||||
/// <returns>门店详情。</returns>
|
||||
[HttpGet("{storeId:long}")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreDto>> Detail(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店详情
|
||||
var result = await mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<StoreDto>.Error(ErrorCodes.NotFound, "门店不存在")
|
||||
: ApiResponse<StoreDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店。
|
||||
/// </summary>
|
||||
/// <returns>更新后的门店信息。</returns>
|
||||
[HttpPut("{storeId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreDto>> Update(long storeId, [FromBody] UpdateStoreCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令包含门店标识
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result == null
|
||||
? ApiResponse<StoreDto>.Error(ErrorCodes.NotFound, "门店不存在")
|
||||
: ApiResponse<StoreDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店。
|
||||
/// </summary>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{storeId:long}")]
|
||||
[PermissionAuthorize("store:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteStoreCommand { StoreId = storeId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交门店审核。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/submit")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> SubmitAudit(long storeId, [FromBody] SubmitStoreAuditCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行提交
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换门店经营状态。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/business-status")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreDto>> ToggleBusinessStatus(long storeId, [FromBody] ToggleBusinessStatusCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行切换
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<StoreDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询门店资质列表。
|
||||
/// </summary>
|
||||
[HttpGet("{storeId:long}/qualifications")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreQualificationDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreQualificationDto>>> ListQualifications(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询资质列表
|
||||
var result = await mediator.Send(new ListStoreQualificationsQuery { StoreId = storeId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<IReadOnlyList<StoreQualificationDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查门店资质完整性。
|
||||
/// </summary>
|
||||
[HttpGet("{storeId:long}/qualifications/check")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreQualificationCheckResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreQualificationCheckResultDto>> CheckQualifications(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行检查
|
||||
var result = await mediator.Send(new CheckStoreQualificationsQuery { StoreId = storeId }, cancellationToken);
|
||||
|
||||
// 2. 返回检查结果
|
||||
return ApiResponse<StoreQualificationCheckResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增门店资质。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/qualifications")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreQualificationDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreQualificationDto>> CreateQualification(long storeId, [FromBody] CreateStoreQualificationCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行创建
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<StoreQualificationDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店资质。
|
||||
/// </summary>
|
||||
[HttpPut("{storeId:long}/qualifications/{qualificationId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreQualificationDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreQualificationDto>> UpdateQualification(
|
||||
long storeId,
|
||||
long qualificationId,
|
||||
[FromBody] UpdateStoreQualificationCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定资质 ID
|
||||
if (command.StoreId == 0 || command.QualificationId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, QualificationId = qualificationId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果或 404
|
||||
return result is null
|
||||
? ApiResponse<StoreQualificationDto>.Error(ErrorCodes.NotFound, "资质不存在")
|
||||
: ApiResponse<StoreQualificationDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除门店资质。
|
||||
/// </summary>
|
||||
[HttpDelete("{storeId:long}/qualifications/{qualificationId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> DeleteQualification(long storeId, long qualificationId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var result = await mediator.Send(new DeleteStoreQualificationCommand
|
||||
{
|
||||
StoreId = storeId,
|
||||
QualificationId = qualificationId
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新营业时段。
|
||||
/// </summary>
|
||||
[HttpPut("{storeId:long}/business-hours/batch")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreBusinessHourDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreBusinessHourDto>>> BatchUpdateBusinessHours(
|
||||
long storeId,
|
||||
[FromBody] BatchUpdateBusinessHoursCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行批量更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<IReadOnlyList<StoreBusinessHourDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询门店营业时段。
|
||||
/// </summary>
|
||||
[HttpGet("{storeId:long}/business-hours")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreBusinessHourDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreBusinessHourDto>>> ListBusinessHours(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreBusinessHoursQuery { StoreId = storeId }, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<StoreBusinessHourDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增营业时段。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/business-hours")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreBusinessHourDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreBusinessHourDto>> CreateBusinessHour(long storeId, [FromBody] CreateStoreBusinessHourCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StoreBusinessHourDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新营业时段。
|
||||
/// </summary>
|
||||
[HttpPut("{storeId:long}/business-hours/{businessHourId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreBusinessHourDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreBusinessHourDto>> UpdateBusinessHour(long storeId, long businessHourId, [FromBody] UpdateStoreBusinessHourCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.BusinessHourId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, BusinessHourId = businessHourId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreBusinessHourDto>.Error(ErrorCodes.NotFound, "营业时段不存在")
|
||||
: ApiResponse<StoreBusinessHourDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除营业时段。
|
||||
/// </summary>
|
||||
[HttpDelete("{storeId:long}/business-hours/{businessHourId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> DeleteBusinessHour(long storeId, long businessHourId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreBusinessHourCommand { StoreId = storeId, BusinessHourId = businessHourId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "营业时段不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询配送区域。
|
||||
/// </summary>
|
||||
[HttpGet("{storeId:long}/delivery-zones")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreDeliveryZoneDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreDeliveryZoneDto>>> ListDeliveryZones(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreDeliveryZonesQuery { StoreId = storeId }, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<StoreDeliveryZoneDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增配送区域。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/delivery-zones")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDeliveryZoneDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreDeliveryZoneDto>> CreateDeliveryZone(long storeId, [FromBody] CreateStoreDeliveryZoneCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StoreDeliveryZoneDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新配送区域。
|
||||
/// </summary>
|
||||
[HttpPut("{storeId:long}/delivery-zones/{deliveryZoneId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDeliveryZoneDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreDeliveryZoneDto>> UpdateDeliveryZone(long storeId, long deliveryZoneId, [FromBody] UpdateStoreDeliveryZoneCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.DeliveryZoneId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, DeliveryZoneId = deliveryZoneId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreDeliveryZoneDto>.Error(ErrorCodes.NotFound, "配送区域不存在")
|
||||
: ApiResponse<StoreDeliveryZoneDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除配送区域。
|
||||
/// </summary>
|
||||
[HttpDelete("{storeId:long}/delivery-zones/{deliveryZoneId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> DeleteDeliveryZone(long storeId, long deliveryZoneId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreDeliveryZoneCommand { StoreId = storeId, DeliveryZoneId = deliveryZoneId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "配送区域不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配送范围检测。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/delivery-check")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreDeliveryCheckResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreDeliveryCheckResultDto>> CheckDeliveryZone(
|
||||
long storeId,
|
||||
[FromBody] CheckStoreDeliveryZoneQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (query.StoreId == 0)
|
||||
{
|
||||
query = query with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行检测
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<StoreDeliveryCheckResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店费用配置。
|
||||
/// </summary>
|
||||
[HttpGet("{storeId:long}/fee")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreFeeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreFeeDto>> GetFee(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询费用配置
|
||||
var result = await mediator.Send(new GetStoreFeeQuery { StoreId = storeId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<StoreFeeDto>.Ok(result ?? new StoreFeeDto());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新门店费用配置。
|
||||
/// </summary>
|
||||
[HttpPut("{storeId:long}/fee")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreFeeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreFeeDto>> UpdateFee(long storeId, [FromBody] UpdateStoreFeeCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<StoreFeeDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 门店费用预览。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/fee/calculate")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreFeeCalculationResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreFeeCalculationResultDto>> CalculateFee(
|
||||
long storeId,
|
||||
[FromBody] CalculateStoreFeeQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定门店 ID
|
||||
if (query.StoreId == 0)
|
||||
{
|
||||
query = query with { StoreId = storeId };
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行计算
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return ApiResponse<StoreFeeCalculationResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询门店节假日。
|
||||
/// </summary>
|
||||
[HttpGet("{storeId:long}/holidays")]
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<StoreHolidayDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<StoreHolidayDto>>> ListHolidays(long storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ListStoreHolidaysQuery { StoreId = storeId }, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<StoreHolidayDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增节假日配置。
|
||||
/// </summary>
|
||||
[HttpPost("{storeId:long}/holidays")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreHolidayDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<StoreHolidayDto>> CreateHoliday(long storeId, [FromBody] CreateStoreHolidayCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<StoreHolidayDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新节假日配置。
|
||||
/// </summary>
|
||||
[HttpPut("{storeId:long}/holidays/{holidayId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<StoreHolidayDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreHolidayDto>> UpdateHoliday(long storeId, long holidayId, [FromBody] UpdateStoreHolidayCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.StoreId == 0 || command.HolidayId == 0)
|
||||
{
|
||||
command = command with { StoreId = storeId, HolidayId = holidayId };
|
||||
}
|
||||
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<StoreHolidayDto>.Error(ErrorCodes.NotFound, "节假日配置不存在")
|
||||
: ApiResponse<StoreHolidayDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除节假日配置。
|
||||
/// </summary>
|
||||
[HttpDelete("{storeId:long}/holidays/{holidayId:long}")]
|
||||
[PermissionAuthorize("store:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> DeleteHoliday(long storeId, long holidayId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStoreHolidayCommand { StoreId = storeId, HolidayId = holidayId }, cancellationToken);
|
||||
return success
|
||||
? ApiResponse<object>.Ok(null)
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "节假日配置不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Subscriptions.Commands;
|
||||
using TakeoutSaaS.Application.App.Subscriptions.Dto;
|
||||
using TakeoutSaaS.Application.App.Subscriptions.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/subscriptions")]
|
||||
public sealed class SubscriptionsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询订阅列表(支持按状态、套餐、到期时间筛选)。
|
||||
/// </summary>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>订阅分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("subscription:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<SubscriptionListDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<SubscriptionListDto>>> List(
|
||||
[FromQuery] GetSubscriptionListQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询订阅分页
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<PagedResult<SubscriptionListDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看订阅详情(含套餐信息、配额使用、变更历史)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>订阅详情或未找到。</returns>
|
||||
[HttpGet("{subscriptionId:long}")]
|
||||
[PermissionAuthorize("subscription:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SubscriptionDetailDto>> Detail(
|
||||
long subscriptionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询订阅详情
|
||||
var result = await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscriptionId }, cancellationToken);
|
||||
|
||||
// 2. 返回查询结果或 404
|
||||
return result is null
|
||||
? ApiResponse<SubscriptionDetailDto>.Error(StatusCodes.Status404NotFound, "订阅不存在")
|
||||
: ApiResponse<SubscriptionDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新订阅基础信息(备注、自动续费等)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的订阅详情或未找到。</returns>
|
||||
[HttpPut("{subscriptionId:long}")]
|
||||
[PermissionAuthorize("subscription:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SubscriptionDetailDto>> Update(
|
||||
long subscriptionId,
|
||||
[FromBody, Required] UpdateSubscriptionCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { SubscriptionId = subscriptionId };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result is null
|
||||
? ApiResponse<SubscriptionDetailDto>.Error(StatusCodes.Status404NotFound, "订阅不存在")
|
||||
: ApiResponse<SubscriptionDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 延期订阅(增加订阅时长)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="command">延期命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>延期后的订阅详情或未找到。</returns>
|
||||
[HttpPost("{subscriptionId:long}/extend")]
|
||||
[PermissionAuthorize("subscription:extend")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SubscriptionDetailDto>> Extend(
|
||||
long subscriptionId,
|
||||
[FromBody, Required] ExtendSubscriptionCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { SubscriptionId = subscriptionId };
|
||||
|
||||
// 2. 执行延期
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回延期结果或 404
|
||||
return result is null
|
||||
? ApiResponse<SubscriptionDetailDto>.Error(StatusCodes.Status404NotFound, "订阅不存在")
|
||||
: ApiResponse<SubscriptionDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更套餐(支持立即生效或下周期生效)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="command">变更套餐命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>变更后的订阅详情或未找到。</returns>
|
||||
[HttpPost("{subscriptionId:long}/change-plan")]
|
||||
[PermissionAuthorize("subscription:change-plan")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SubscriptionDetailDto>> ChangePlan(
|
||||
long subscriptionId,
|
||||
[FromBody, Required] ChangeSubscriptionPlanCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { SubscriptionId = subscriptionId };
|
||||
|
||||
// 2. 执行套餐变更
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回变更结果或 404
|
||||
return result is null
|
||||
? ApiResponse<SubscriptionDetailDto>.Error(StatusCodes.Status404NotFound, "订阅不存在")
|
||||
: ApiResponse<SubscriptionDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更订阅状态。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="command">状态变更命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>变更后的订阅详情或未找到。</returns>
|
||||
[HttpPost("{subscriptionId:long}/status")]
|
||||
[PermissionAuthorize("subscription:update-status")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SubscriptionDetailDto>> UpdateStatus(
|
||||
long subscriptionId,
|
||||
[FromBody, Required] UpdateSubscriptionStatusCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { SubscriptionId = subscriptionId };
|
||||
|
||||
// 2. 执行状态变更
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回变更结果或 404
|
||||
return result is null
|
||||
? ApiResponse<SubscriptionDetailDto>.Error(StatusCodes.Status404NotFound, "订阅不存在")
|
||||
: ApiResponse<SubscriptionDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量延期订阅。
|
||||
/// </summary>
|
||||
/// <param name="command">批量延期命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>批量延期结果。</returns>
|
||||
[HttpPost("batch-extend")]
|
||||
[PermissionAuthorize("subscription:batch-extend")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BatchExtendResult>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<BatchExtendResult>> BatchExtend(
|
||||
[FromBody, Required] BatchExtendSubscriptionsCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<BatchExtendResult>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量发送续费提醒。
|
||||
/// </summary>
|
||||
/// <param name="command">批量发送提醒命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>批量发送提醒结果。</returns>
|
||||
[HttpPost("batch-remind")]
|
||||
[PermissionAuthorize("subscription:batch-remind")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BatchSendReminderResult>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<BatchSendReminderResult>> BatchRemind(
|
||||
[FromBody, Required] BatchSendReminderCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<BatchSendReminderResult>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.SystemParameters.Commands;
|
||||
using TakeoutSaaS.Application.App.SystemParameters.Dto;
|
||||
using TakeoutSaaS.Application.App.SystemParameters.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 系统参数管理。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 提供参数的新增、修改、查询与删除。
|
||||
/// </remarks>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/system-parameters")]
|
||||
public sealed class SystemParametersController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建系统参数。
|
||||
/// </summary>
|
||||
/// <returns>创建的系统参数信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("system-parameter:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SystemParameterDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<SystemParameterDto>> Create([FromBody] CreateSystemParameterCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建系统参数
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<SystemParameterDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询系统参数列表。
|
||||
/// </summary>
|
||||
/// <returns>分页的系统参数列表。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("system-parameter:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<SystemParameterDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<SystemParameterDto>>> List(
|
||||
[FromQuery] string? keyword,
|
||||
[FromQuery] bool? isEnabled,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组合查询参数
|
||||
var result = await mediator.Send(new SearchSystemParametersQuery
|
||||
{
|
||||
Keyword = keyword,
|
||||
IsEnabled = isEnabled,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<SystemParameterDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统参数详情。
|
||||
/// </summary>
|
||||
/// <returns>系统参数详情。</returns>
|
||||
[HttpGet("{parameterId:long}")]
|
||||
[PermissionAuthorize("system-parameter:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SystemParameterDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SystemParameterDto>> Detail(long parameterId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询参数详情
|
||||
var result = await mediator.Send(new GetSystemParameterByIdQuery(parameterId), cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<SystemParameterDto>.Error(ErrorCodes.NotFound, "系统参数不存在")
|
||||
: ApiResponse<SystemParameterDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新系统参数。
|
||||
/// </summary>
|
||||
/// <returns>更新后的系统参数信息。</returns>
|
||||
[HttpPut("{parameterId:long}")]
|
||||
[PermissionAuthorize("system-parameter:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SystemParameterDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SystemParameterDto>> Update(long parameterId, [FromBody] UpdateSystemParameterCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 确保命令包含参数标识
|
||||
if (command.ParameterId == 0)
|
||||
{
|
||||
command = command with { ParameterId = parameterId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果或 404
|
||||
return result == null
|
||||
? ApiResponse<SystemParameterDto>.Error(ErrorCodes.NotFound, "系统参数不存在")
|
||||
: ApiResponse<SystemParameterDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除系统参数。
|
||||
/// </summary>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{parameterId:long}")]
|
||||
[PermissionAuthorize("system-parameter:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long parameterId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var success = await mediator.Send(new DeleteSystemParameterCommand { ParameterId = parameterId }, cancellationToken);
|
||||
|
||||
// 2. 返回成功或 404
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "系统参数不存在");
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/announcements")]
|
||||
public sealed class TenantAnnouncementsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController
|
||||
{
|
||||
private const string TenantIdHeaderName = "X-Tenant-Id";
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/tenants/100000000000000001/announcements?page=1&pageSize=20
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 0
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[SwaggerOperation(Summary = "查询租户公告列表", Description = "需要权限:tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> Search(long tenantId, [FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
|
||||
{
|
||||
var request = query with { TenantId = 0 };
|
||||
var platformResult = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken));
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(platformResult);
|
||||
}
|
||||
|
||||
var headerError = EnsureTenantHeader<PagedResult<TenantAnnouncementDto>>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
query = query with { TenantId = tenantId };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公告详情。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "title": "租户公告",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpGet("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[SwaggerOperation(Summary = "获取公告详情", Description = "需要权限:tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var result = await mediator.Send(new GetAnnouncementByIdQuery { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "租户公告",
|
||||
/// "content": "新品上线提醒",
|
||||
/// "announcementType": 0,
|
||||
/// "priority": 5,
|
||||
/// "effectiveFrom": "2025-12-20T00:00:00Z",
|
||||
/// "targetType": "roles",
|
||||
/// "targetParameters": "{\"roles\":[\"OpsManager\"]}"
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "title": "租户公告",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant-announcement:create")]
|
||||
[SwaggerOperation(Summary = "创建租户公告", Description = "需要权限:tenant-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
command = command with { TenantId = tenantId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新公告(仅草稿)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// PUT /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345
|
||||
/// Body:
|
||||
/// {
|
||||
/// "title": "租户公告(更新)",
|
||||
/// "content": "公告内容更新",
|
||||
/// "targetType": "all",
|
||||
/// "targetParameters": null,
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Draft"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPut("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:update")]
|
||||
[SwaggerOperation(Summary = "更新租户公告", Description = "仅草稿可更新;需要权限:tenant-announcement:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
command = command with { TenantId = tenantId, AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/publish
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Published"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/publish")]
|
||||
[PermissionAuthorize("tenant-announcement:publish")]
|
||||
[SwaggerOperation(Summary = "发布租户公告", Description = "需要权限:tenant-announcement:publish")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Publish(long tenantId, long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/revoke
|
||||
/// Body:
|
||||
/// {
|
||||
/// "rowVersion": "AAAAAAAAB9E="
|
||||
/// }
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "status": "Revoked"
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/revoke")]
|
||||
[PermissionAuthorize("tenant-announcement:revoke")]
|
||||
[SwaggerOperation(Summary = "撤销租户公告", Description = "需要权限:tenant-announcement:revoke")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Revoke(long tenantId, long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
command = command with { AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除公告。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// DELETE /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": true
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpDelete("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:delete")]
|
||||
[SwaggerOperation(Summary = "删除租户公告", Description = "需要权限:tenant-announcement:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<bool>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读(兼容旧路径)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/read
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "id": "900123456789012345",
|
||||
/// "isRead": true
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
[HttpPost("{announcementId:long}/read")]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[SwaggerOperation(Summary = "标记公告已读", Description = "需要权限:tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<TenantAnnouncementDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
private ApiResponse<T>? EnsureTenantHeader<T>()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
|
||||
}
|
||||
|
||||
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsPlatformAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
var original = tenantContextAccessor.Current;
|
||||
tenantContextAccessor.Current = new TenantContext(0, null, "platform");
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContextAccessor.Current = original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户账单管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/billings")]
|
||||
public sealed class TenantBillingsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询账单。
|
||||
/// </summary>
|
||||
/// <returns>租户账单分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantBillingDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantBillingDto>>> Search(long tenantId, [FromQuery] SearchTenantBillsRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 组装查询对象(TenantId 仅来自路由,避免与 QueryString 重复)
|
||||
var query = new SearchTenantBillsQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Status = request.Status,
|
||||
From = request.From,
|
||||
To = request.To,
|
||||
Page = request.Page,
|
||||
PageSize = request.PageSize,
|
||||
};
|
||||
|
||||
// 2. 查询账单列表
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回分页结果
|
||||
return ApiResponse<PagedResult<TenantBillingDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 账单详情。
|
||||
/// </summary>
|
||||
/// <returns>租户账单详情。</returns>
|
||||
[HttpGet("{billingId:long}")]
|
||||
[PermissionAuthorize("tenant-bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantBillingDto>> Detail(long tenantId, long billingId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询账单详情
|
||||
var result = await mediator.Send(new GetTenantBillQuery { TenantId = tenantId, BillingId = billingId }, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result is null
|
||||
? ApiResponse<TenantBillingDto>.Error(StatusCodes.Status404NotFound, "账单不存在")
|
||||
: ApiResponse<TenantBillingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建账单。
|
||||
/// </summary>
|
||||
/// <returns>创建的账单信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant-bill:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantBillingDto>> Create(long tenantId, [FromBody, Required] CreateTenantBillingCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
command = command with { TenantId = tenantId };
|
||||
|
||||
// 2. 创建账单
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantBillingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记账单已支付。
|
||||
/// </summary>
|
||||
/// <returns>标记支付后的账单信息。</returns>
|
||||
[HttpPost("{billingId:long}/pay")]
|
||||
[PermissionAuthorize("tenant-bill:pay")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantBillingDto>> MarkPaid(long tenantId, long billingId, [FromBody, Required] MarkTenantBillingPaidCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户与账单标识
|
||||
command = command with { TenantId = tenantId, BillingId = billingId };
|
||||
|
||||
// 2. 标记支付状态
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果或 404
|
||||
return result is null
|
||||
? ApiResponse<TenantBillingDto>.Error(StatusCodes.Status404NotFound, "账单不存在")
|
||||
: ApiResponse<TenantBillingDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户通知接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/notifications")]
|
||||
public sealed class TenantNotificationsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询通知。
|
||||
/// </summary>
|
||||
/// <returns>租户通知分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-notification:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantNotificationDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantNotificationDto>>> Search(long tenantId, [FromQuery] SearchTenantNotificationsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
query = query with { TenantId = tenantId };
|
||||
|
||||
// 2. 查询通知列表
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回分页结果
|
||||
return ApiResponse<PagedResult<TenantNotificationDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记通知已读。
|
||||
/// </summary>
|
||||
/// <returns>标记已读后的通知信息。</returns>
|
||||
[HttpPost("{notificationId:long}/read")]
|
||||
[PermissionAuthorize("tenant-notification:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantNotificationDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantNotificationDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantNotificationDto>> MarkRead(long tenantId, long notificationId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 标记通知为已读
|
||||
var result = await mediator.Send(new MarkTenantNotificationReadCommand { TenantId = tenantId, NotificationId = notificationId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return result is null
|
||||
? ApiResponse<TenantNotificationDto>.Error(StatusCodes.Status404NotFound, "通知不存在")
|
||||
: ApiResponse<TenantNotificationDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户套餐管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenant-packages")]
|
||||
public sealed class TenantPackagesController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询租户套餐。
|
||||
/// </summary>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>租户套餐分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-package:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantPackageDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantPackageDto>>> Search([FromQuery] SearchTenantPackagesQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询租户套餐分页
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<PagedResult<TenantPackageDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询套餐使用统计(订阅关联数量、使用租户数量)。
|
||||
/// </summary>
|
||||
/// <param name="tenantPackageIds">套餐 ID 列表(为空表示查询全部)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>套餐使用统计列表。</returns>
|
||||
[HttpGet("usages")]
|
||||
[PermissionAuthorize("tenant-package:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<TenantPackageUsageDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<TenantPackageUsageDto>>> Usages(
|
||||
[FromQuery] long[]? tenantPackageIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询使用统计
|
||||
var result = await mediator.Send(new GetTenantPackageUsagesQuery { TenantPackageIds = tenantPackageIds }, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<IReadOnlyList<TenantPackageUsageDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询套餐当前使用租户列表(按有效订阅口径)。
|
||||
/// </summary>
|
||||
/// <param name="tenantPackageId">套餐 ID。</param>
|
||||
/// <param name="keyword">关键词(可选)。</param>
|
||||
/// <param name="expiringWithinDays">可选:未来 N 天内到期筛选。</param>
|
||||
/// <param name="page">页码(从 1 开始)。</param>
|
||||
/// <param name="pageSize">每页大小。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>使用租户分页结果。</returns>
|
||||
[HttpGet("{tenantPackageId:long}/tenants")]
|
||||
[PermissionAuthorize("tenant-package:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantPackageTenantDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantPackageTenantDto>>> Tenants(
|
||||
long tenantPackageId,
|
||||
[FromQuery] string? keyword,
|
||||
[FromQuery] int? expiringWithinDays,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询套餐使用租户分页
|
||||
var result = await mediator.Send(new GetTenantPackageTenantsQuery
|
||||
{
|
||||
TenantPackageId = tenantPackageId,
|
||||
Keyword = keyword,
|
||||
ExpiringWithinDays = expiringWithinDays,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回结果
|
||||
return ApiResponse<PagedResult<TenantPackageTenantDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看套餐详情。
|
||||
/// </summary>
|
||||
/// <param name="tenantPackageId">套餐 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>套餐详情或未找到。</returns>
|
||||
[HttpGet("{tenantPackageId:long}")]
|
||||
[PermissionAuthorize("tenant-package:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantPackageDto>> Detail(long tenantPackageId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询套餐详情
|
||||
var result = await mediator.Send(new GetTenantPackageByIdQuery { TenantPackageId = tenantPackageId }, cancellationToken);
|
||||
|
||||
// 2. 返回查询结果或 404
|
||||
return result is null
|
||||
? ApiResponse<TenantPackageDto>.Error(StatusCodes.Status404NotFound, "套餐不存在")
|
||||
: ApiResponse<TenantPackageDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建套餐。
|
||||
/// </summary>
|
||||
/// <param name="command">创建命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>创建后的套餐。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant-package:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantPackageDto>> Create([FromBody, Required] CreateTenantPackageCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行创建
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<TenantPackageDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新套餐。
|
||||
/// </summary>
|
||||
/// <param name="tenantPackageId">套餐 ID。</param>
|
||||
/// <param name="command">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新后的套餐或未找到。</returns>
|
||||
[HttpPut("{tenantPackageId:long}")]
|
||||
[PermissionAuthorize("tenant-package:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantPackageDto>> Update(long tenantPackageId, [FromBody, Required] UpdateTenantPackageCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定路由 ID
|
||||
command = command with { TenantPackageId = tenantPackageId };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result is null
|
||||
? ApiResponse<TenantPackageDto>.Error(StatusCodes.Status404NotFound, "套餐不存在")
|
||||
: ApiResponse<TenantPackageDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除套餐。
|
||||
/// </summary>
|
||||
/// <param name="tenantPackageId">套餐 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>删除结果。</returns>
|
||||
[HttpDelete("{tenantPackageId:long}")]
|
||||
[PermissionAuthorize("tenant-package:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantPackageId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 构建删除命令
|
||||
var command = new DeleteTenantPackageCommand { TenantPackageId = tenantPackageId };
|
||||
|
||||
// 2. 执行删除并返回
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户角色管理(实例层)。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/roles")]
|
||||
public sealed class TenantRolesController(IMediator mediator, ITenantProvider tenantProvider) : BaseApiController
|
||||
{
|
||||
private const long PlatformRootTenantId = 1000000000001;
|
||||
|
||||
/// <summary>
|
||||
/// 租户角色分页。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("identity:role:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<RoleDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<RoleDto>>> List(
|
||||
long tenantId,
|
||||
[FromQuery] SearchRolesQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验路由租户与上下文一致(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<PagedResult<RoleDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 绑定租户并查询角色分页
|
||||
var request = new SearchRolesQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Keyword = query.Keyword,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize,
|
||||
SortBy = query.SortBy,
|
||||
SortDescending = query.SortDescending
|
||||
};
|
||||
var result = await mediator.Send(request, cancellationToken);
|
||||
|
||||
// 3. 返回分页数据
|
||||
return ApiResponse<PagedResult<RoleDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色详情(含权限)。
|
||||
/// </summary>
|
||||
[HttpGet("{roleId:long}")]
|
||||
[PermissionAuthorize("identity:role:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<RoleDetailDto>> Detail(long tenantId, long roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<RoleDetailDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 查询角色详情
|
||||
var result = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 3. 返回数据或 404
|
||||
return result is null
|
||||
? ApiResponse<RoleDetailDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
|
||||
: ApiResponse<RoleDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建角色。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("identity:role:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<RoleDto>> Create(
|
||||
long tenantId,
|
||||
[FromBody, Required] CreateRoleCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 创建角色
|
||||
var result = await mediator.Send(command with { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 3. 返回创建结果
|
||||
return ApiResponse<RoleDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新角色。
|
||||
/// </summary>
|
||||
[HttpPut("{roleId:long}")]
|
||||
[PermissionAuthorize("identity:role:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<RoleDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<RoleDto>> Update(
|
||||
long tenantId,
|
||||
long roleId,
|
||||
[FromBody, Required] UpdateRoleCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<RoleDto>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 绑定角色 ID
|
||||
command = command with { RoleId = roleId, TenantId = tenantId };
|
||||
|
||||
// 3. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 4. 返回结果或 404
|
||||
return result is null
|
||||
? ApiResponse<RoleDto>.Error(StatusCodes.Status404NotFound, "角色不存在")
|
||||
: ApiResponse<RoleDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除角色。
|
||||
/// </summary>
|
||||
[HttpDelete("{roleId:long}")]
|
||||
[PermissionAuthorize("identity:role:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantId, long roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 执行删除
|
||||
var command = new DeleteRoleCommand { RoleId = roleId, TenantId = tenantId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取角色权限列表。
|
||||
/// </summary>
|
||||
[HttpGet("{roleId:long}/permissions")]
|
||||
[PermissionAuthorize("identity:role:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionDto>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionDto>>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<IReadOnlyList<PermissionDto>>> GetPermissions(
|
||||
long tenantId,
|
||||
long roleId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 查询角色详情并提取权限
|
||||
var detail = await mediator.Send(new RoleDetailQuery { RoleId = roleId, TenantId = tenantId }, cancellationToken);
|
||||
if (detail is null)
|
||||
{
|
||||
return ApiResponse<IReadOnlyList<PermissionDto>>.Error(StatusCodes.Status404NotFound, "角色不存在");
|
||||
}
|
||||
|
||||
// 3. 返回权限集合
|
||||
return ApiResponse<IReadOnlyList<PermissionDto>>.Ok(detail.Permissions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖角色权限。
|
||||
/// </summary>
|
||||
[HttpPut("{roleId:long}/permissions")]
|
||||
[PermissionAuthorize("identity:role:bind-permission")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> BindPermissions(
|
||||
long tenantId,
|
||||
long roleId,
|
||||
[FromBody, Required] BindRolePermissionsCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验租户上下文(超管租户 1000000000001 放行)
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId != PlatformRootTenantId && tenantId != currentTenantId)
|
||||
{
|
||||
return ApiResponse<bool>.Error(StatusCodes.Status400BadRequest, "租户上下文不一致");
|
||||
}
|
||||
|
||||
// 2. 绑定角色 ID
|
||||
command = command with { RoleId = roleId, TenantId = tenantId };
|
||||
|
||||
// 3. 覆盖授权
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants")]
|
||||
public sealed class TenantsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册租户并初始化套餐。
|
||||
/// </summary>
|
||||
/// <returns>注册的租户信息。</returns>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantDto>> Register([FromBody] RegisterTenantCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 注册租户并初始化套餐
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回注册结果
|
||||
return ApiResponse<TenantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)。
|
||||
/// </summary>
|
||||
/// <returns>新增后的租户详情。</returns>
|
||||
[HttpPost("manual")]
|
||||
[PermissionAuthorize("tenant:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantDetailDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantDetailDto>> CreateManually([FromBody] CreateTenantManuallyCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 后台手动新增租户(直接可用)
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<TenantDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询租户。
|
||||
/// </summary>
|
||||
/// <returns>租户分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantDto>>> Search([FromQuery] SearchTenantsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询租户分页
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回分页数据
|
||||
return ApiResponse<PagedResult<TenantDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查看租户详情。
|
||||
/// </summary>
|
||||
/// <returns>租户详情。</returns>
|
||||
[HttpGet("{tenantId:long}")]
|
||||
[PermissionAuthorize("tenant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantDetailDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantDetailDto>> Detail(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询租户详情
|
||||
var result = await mediator.Send(new GetTenantByIdQuery(tenantId), cancellationToken);
|
||||
|
||||
// 2. 返回租户信息
|
||||
return ApiResponse<TenantDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新租户基础信息。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="body">更新命令。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>更新结果。</returns>
|
||||
[HttpPut("{tenantId:long}")]
|
||||
[PermissionAuthorize("tenant:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> Update(
|
||||
long tenantId,
|
||||
[FromBody, Required] UpdateTenantCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验路由与请求体租户标识一致
|
||||
if (body.TenantId != 0 && body.TenantId != tenantId)
|
||||
{
|
||||
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 tenantId 与请求体 tenantId 不一致");
|
||||
}
|
||||
|
||||
// 2. 绑定租户标识并执行更新(若不存在或冲突则抛出业务异常,由全局异常处理转换为 404/409)
|
||||
var command = body with { TenantId = tenantId };
|
||||
await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回成功结果
|
||||
return ApiResponse<object>.Ok(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交或更新实名认证资料。
|
||||
/// </summary>
|
||||
/// <returns>提交的实名认证信息。</returns>
|
||||
[HttpPost("{tenantId:long}/verification")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantVerificationDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantVerificationDto>> SubmitVerification(
|
||||
long tenantId,
|
||||
[FromBody] SubmitTenantVerificationCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 合并路由中的租户标识
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 提交或更新认证资料
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回认证结果
|
||||
return ApiResponse<TenantVerificationDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核租户。
|
||||
/// </summary>
|
||||
/// <returns>审核后的租户信息。</returns>
|
||||
[HttpPost("{tenantId:long}/review")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantDto>> Review(long tenantId, [FromBody] ReviewTenantCommand body, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 执行审核
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回审核结果
|
||||
return ApiResponse<TenantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询当前租户审核领取信息。
|
||||
/// </summary>
|
||||
/// <returns>领取信息,未领取返回 null。</returns>
|
||||
[HttpGet("{tenantId:long}/review/claim")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto?>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantReviewClaimDto?>> GetReviewClaim(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询领取信息
|
||||
var result = await mediator.Send(new GetTenantReviewClaimQuery(tenantId), cancellationToken);
|
||||
|
||||
// 2. 返回领取信息
|
||||
return ApiResponse<TenantReviewClaimDto?>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 领取租户入驻审核(领取后仅领取人可操作审核)。
|
||||
/// </summary>
|
||||
/// <returns>领取结果。</returns>
|
||||
[HttpPost("{tenantId:long}/review/claim")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantReviewClaimDto>> ClaimReview(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行领取
|
||||
var result = await mediator.Send(new ClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 2. 返回领取结果
|
||||
return ApiResponse<TenantReviewClaimDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制接管租户入驻审核(仅超级管理员可用)。
|
||||
/// </summary>
|
||||
/// <returns>接管后的领取信息。</returns>
|
||||
[HttpPost("{tenantId:long}/review/force-claim")]
|
||||
[PermissionAuthorize("tenant:review:force-claim")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantReviewClaimDto>> ForceClaimReview(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行强制接管
|
||||
var result = await mediator.Send(new ForceClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 2. 返回接管结果
|
||||
return ApiResponse<TenantReviewClaimDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放租户入驻审核领取(仅领取人可释放)。
|
||||
/// </summary>
|
||||
/// <returns>释放后的领取信息,未领取返回 null。</returns>
|
||||
[HttpPost("{tenantId:long}/review/release")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto?>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantReviewClaimDto?>> ReleaseReview(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行释放
|
||||
var result = await mediator.Send(new ReleaseTenantReviewClaimCommand { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 2. 返回释放结果
|
||||
return ApiResponse<TenantReviewClaimDto?>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 冻结租户(暂停服务)。
|
||||
/// </summary>
|
||||
/// <returns>冻结后的租户信息。</returns>
|
||||
[HttpPost("{tenantId:long}/freeze")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantDto>> Freeze(
|
||||
long tenantId,
|
||||
[FromBody] FreezeTenantCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 合并路由参数
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 执行冻结
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回冻结结果
|
||||
return ApiResponse<TenantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解冻租户(恢复服务)。
|
||||
/// </summary>
|
||||
/// <returns>解冻后的租户信息。</returns>
|
||||
[HttpPost("{tenantId:long}/unfreeze")]
|
||||
[PermissionAuthorize("tenant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantDto>> Unfreeze(
|
||||
long tenantId,
|
||||
[FromBody] UnfreezeTenantCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 合并路由参数
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 执行解冻
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回解冻结果
|
||||
return ApiResponse<TenantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建或续费租户订阅。
|
||||
/// </summary>
|
||||
/// <returns>创建或续费的订阅信息。</returns>
|
||||
[HttpPost("{tenantId:long}/subscriptions")]
|
||||
[PermissionAuthorize("tenant:subscription")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantSubscriptionDto>> CreateSubscription(
|
||||
long tenantId,
|
||||
[FromBody] CreateTenantSubscriptionCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户并创建或续费订阅
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 返回订阅结果
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantSubscriptionDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 延期/赠送租户订阅时长(按当前订阅套餐续费)。
|
||||
/// </summary>
|
||||
/// <returns>续费后的订阅信息。</returns>
|
||||
[HttpPost("{tenantId:long}/subscriptions/extend")]
|
||||
[PermissionAuthorize("tenant:subscription")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantSubscriptionDto>> ExtendSubscription(
|
||||
long tenantId,
|
||||
[FromBody] ExtendTenantSubscriptionCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 合并租户标识
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 执行延期/赠送
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回订阅结果
|
||||
return ApiResponse<TenantSubscriptionDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 套餐升降配。
|
||||
/// </summary>
|
||||
/// <returns>更新后的订阅信息。</returns>
|
||||
[HttpPut("{tenantId:long}/subscriptions/{subscriptionId:long}/plan")]
|
||||
[PermissionAuthorize("tenant:subscription")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantSubscriptionDto>> ChangePlan(
|
||||
long tenantId,
|
||||
long subscriptionId,
|
||||
[FromBody] ChangeTenantSubscriptionPlanCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户与订阅标识
|
||||
var command = body with { TenantId = tenantId, TenantSubscriptionId = subscriptionId };
|
||||
|
||||
// 2. 执行升降配
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回调整后的订阅
|
||||
return ApiResponse<TenantSubscriptionDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询审核日志。
|
||||
/// </summary>
|
||||
/// <returns>租户审核日志分页结果。</returns>
|
||||
[HttpGet("{tenantId:long}/audits")]
|
||||
[PermissionAuthorize("tenant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAuditLogDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAuditLogDto>>> AuditLogs(
|
||||
long tenantId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构造审核日志查询
|
||||
var query = new GetTenantAuditLogsQuery(tenantId, page, pageSize);
|
||||
|
||||
// 2. 查询并返回分页结果
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAuditLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 伪装登录租户(仅平台超级管理员可用)。
|
||||
/// </summary>
|
||||
/// <returns>目标租户主管理员的令牌对。</returns>
|
||||
[HttpPost("{tenantId:long}/impersonate")]
|
||||
[PermissionAuthorize("tenant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TokenResponse>> Impersonate(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行伪装登录
|
||||
var result = await mediator.Send(new ImpersonateTenantCommand { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 2. 返回令牌
|
||||
return ApiResponse<TokenResponse>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成租户主管理员重置密码链接(仅平台超级管理员可用)。
|
||||
/// </summary>
|
||||
/// <remarks>链接默认 24 小时有效且仅可使用一次。</remarks>
|
||||
/// <returns>重置密码链接。</returns>
|
||||
[HttpPost("{tenantId:long}/admin/reset-link")]
|
||||
[PermissionAuthorize("tenant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<string>> CreateAdminResetLink(long tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 生成一次性令牌
|
||||
var token = await mediator.Send(new CreateTenantAdminResetLinkTokenCommand { TenantId = tenantId }, cancellationToken);
|
||||
|
||||
// 2. 解析前端来源(优先 Origin,避免拼成 AdminApi 域名)
|
||||
var origin = Request.Headers.Origin.ToString();
|
||||
if (string.IsNullOrWhiteSpace(origin))
|
||||
{
|
||||
origin = $"{Request.Scheme}://{Request.Host}";
|
||||
}
|
||||
|
||||
origin = origin.TrimEnd('/');
|
||||
var resetUrl = $"{origin}/#/auth/reset-password?token={Uri.EscapeDataString(token)}";
|
||||
|
||||
// 3. 返回链接
|
||||
return ApiResponse<string>.Ok(data: resetUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配额校验并占用额度(门店/账号/短信/配送)。
|
||||
/// </summary>
|
||||
/// <remarks>需在请求头携带 X-Tenant-Id 对应的租户。</remarks>
|
||||
/// <returns>配额校验结果。</returns>
|
||||
[HttpPost("{tenantId:long}/quotas/check")]
|
||||
[PermissionAuthorize("tenant:quota:check")]
|
||||
[ProducesResponseType(typeof(ApiResponse<QuotaCheckResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<QuotaCheckResultDto>> CheckQuota(
|
||||
long tenantId,
|
||||
[FromBody, Required] CheckTenantQuotaCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
var command = body with { TenantId = tenantId };
|
||||
|
||||
// 2. 校验并占用配额
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<QuotaCheckResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询租户配额使用历史。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="query">查询条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>配额使用历史分页结果。</returns>
|
||||
[HttpGet("{tenantId:long}/quota-usage-history")]
|
||||
[PermissionAuthorize("tenant:quota:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<QuotaUsageHistoryDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<QuotaUsageHistoryDto>>> GetQuotaUsageHistory(
|
||||
long tenantId,
|
||||
[FromQuery] GetTenantQuotaUsageHistoryQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定租户标识
|
||||
query = query with { TenantId = tenantId };
|
||||
|
||||
// 2. 查询配额使用历史
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 3. 返回分页结果
|
||||
return ApiResponse<PagedResult<QuotaUsageHistoryDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 用户权限洞察接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/users/permissions")]
|
||||
public sealed class UserPermissionsController(IAdminAuthService authService) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询当前租户用户的角色与权限概览。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// GET /api/admin/v1/users/permissions?keyword=ops&page=1&pageSize=20&sortBy=createdAt&sortDescending=true
|
||||
/// Header: Authorization: Bearer <JWT>
|
||||
/// 响应:
|
||||
/// {
|
||||
/// "success": true,
|
||||
/// "code": 200,
|
||||
/// "data": {
|
||||
/// "items": [
|
||||
/// {
|
||||
/// "userId": "900123456789012346",
|
||||
/// "tenantId": "100000000000000001",
|
||||
/// "merchantId": "200000000000000001",
|
||||
/// "account": "ops.manager",
|
||||
/// "displayName": "运营经理",
|
||||
/// "roles": ["OpsManager", "Reporter"],
|
||||
/// "permissions": ["delivery:read", "order:read", "payment:read"],
|
||||
/// "createdAt": "2025-12-01T08:30:00Z"
|
||||
/// }
|
||||
/// ],
|
||||
/// "page": 1,
|
||||
/// "pageSize": 20,
|
||||
/// "totalCount": 1,
|
||||
/// "totalPages": 1
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
/// <param name="query">搜索条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分页的用户权限概览。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("identity:permission:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<UserPermissionDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<UserPermissionDto>>> Search(
|
||||
[FromQuery] SearchUserPermissionsQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询当前租户的用户权限概览
|
||||
var result = await authService.SearchUserPermissionsAsync(
|
||||
query.Keyword,
|
||||
query.Page,
|
||||
query.PageSize,
|
||||
query.SortBy,
|
||||
query.SortDescending,
|
||||
cancellationToken);
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<UserPermissionDto>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.Identity.Commands;
|
||||
using TakeoutSaaS.Application.Identity.Contracts;
|
||||
using TakeoutSaaS.Application.Identity.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 用户管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/users")]
|
||||
public sealed class UsersController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户分页列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("identity:user:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<UserListItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<UserListItemDto>>> List(
|
||||
[FromQuery] SearchIdentityUsersQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户分页
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
// 2. 返回分页数据
|
||||
return ApiResponse<PagedResult<UserListItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户详情。
|
||||
/// </summary>
|
||||
[HttpGet("{userId:long}")]
|
||||
[PermissionAuthorize("identity:user:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<UserDetailDto>> Detail(long userId, [FromQuery] bool includeDeleted, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户详情
|
||||
var result = await mediator.Send(new GetIdentityUserDetailQuery
|
||||
{
|
||||
UserId = userId,
|
||||
IncludeDeleted = includeDeleted
|
||||
}, cancellationToken);
|
||||
|
||||
// 2. 返回详情或 404
|
||||
return result == null
|
||||
? ApiResponse<UserDetailDto>.Error(ErrorCodes.NotFound, "用户不存在")
|
||||
: ApiResponse<UserDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户权限明细。
|
||||
/// </summary>
|
||||
[HttpGet("{userId:long}/permissions")]
|
||||
[PermissionAuthorize("identity:user:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<string>>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<string>>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<IReadOnlyList<string>>> Permissions(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询用户详情并提取权限
|
||||
var detail = await mediator.Send(new GetIdentityUserDetailQuery { UserId = userId }, cancellationToken);
|
||||
if (detail == null)
|
||||
{
|
||||
return ApiResponse<IReadOnlyList<string>>.Error(ErrorCodes.NotFound, "用户不存在");
|
||||
}
|
||||
|
||||
// 2. 返回权限编码列表
|
||||
return ApiResponse<IReadOnlyList<string>>.Ok(detail.Permissions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建用户。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("identity:user:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDetailDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<UserDetailDto>> Create([FromBody, Required] CreateIdentityUserCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 创建用户
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回创建结果
|
||||
return ApiResponse<UserDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户。
|
||||
/// </summary>
|
||||
[HttpPut("{userId:long}")]
|
||||
[PermissionAuthorize("identity:user:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<UserDetailDto>> Update(long userId, [FromBody, Required] UpdateIdentityUserCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定用户 ID
|
||||
if (command.UserId == 0)
|
||||
{
|
||||
command = command with { UserId = userId };
|
||||
}
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果或 404
|
||||
return result == null
|
||||
? ApiResponse<UserDetailDto>.Error(ErrorCodes.NotFound, "用户不存在")
|
||||
: ApiResponse<UserDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除用户。
|
||||
/// </summary>
|
||||
[HttpDelete("{userId:long}")]
|
||||
[PermissionAuthorize("identity:user:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行删除
|
||||
var result = await mediator.Send(new DeleteIdentityUserCommand { UserId = userId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return result
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "用户不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复用户。
|
||||
/// </summary>
|
||||
[HttpPost("{userId:long}/restore")]
|
||||
[PermissionAuthorize("identity:user:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Restore(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行恢复
|
||||
var result = await mediator.Send(new RestoreIdentityUserCommand { UserId = userId }, cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return result
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "用户不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户状态。
|
||||
/// </summary>
|
||||
[HttpPut("{userId:long}/status")]
|
||||
[PermissionAuthorize("identity:user:status")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> ChangeStatus(long userId, [FromBody, Required] ChangeIdentityUserStatusCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定用户 ID
|
||||
if (command.UserId == 0)
|
||||
{
|
||||
command = command with { UserId = userId };
|
||||
}
|
||||
|
||||
// 2. 执行状态变更
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回结果或 404
|
||||
return result
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "用户不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成重置密码链接。
|
||||
/// </summary>
|
||||
[HttpPost("{userId:long}/password-reset")]
|
||||
[PermissionAuthorize("identity:user:reset-password")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ResetIdentityUserPasswordResult>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ResetIdentityUserPasswordResult>> ResetPassword(long userId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 生成重置令牌
|
||||
var result = await mediator.Send(new ResetIdentityUserPasswordCommand { UserId = userId }, cancellationToken);
|
||||
|
||||
// 2. 返回令牌信息
|
||||
return ApiResponse<ResetIdentityUserPasswordResult>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量用户操作。
|
||||
/// </summary>
|
||||
[HttpPost("batch")]
|
||||
[PermissionAuthorize("identity:user:batch")]
|
||||
[ProducesResponseType(typeof(ApiResponse<BatchIdentityUserOperationResult>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<BatchIdentityUserOperationResult>> Batch(
|
||||
[FromBody, Required] BatchIdentityUserOperationCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 执行批量操作
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 2. 返回操作结果
|
||||
return ApiResponse<BatchIdentityUserOperationResult>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy only what's needed for restore first, so `dotnet restore` can be cached.
|
||||
COPY ["Directory.Build.props", "./"]
|
||||
COPY ["TakeoutSaaS.sln", "./"]
|
||||
COPY ["stylecop.json", "./"]
|
||||
COPY ["src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj", "src/Api/TakeoutSaaS.AdminApi/"]
|
||||
COPY ["src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj", "src/Application/TakeoutSaaS.Application/"]
|
||||
COPY ["TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj", "TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Abstractions/"]
|
||||
COPY ["TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj", "TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Kernel/"]
|
||||
COPY ["TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj", "TakeoutSaaS.BuildingBlocks/src/Core/TakeoutSaaS.Shared.Web/"]
|
||||
COPY ["src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj", "src/Domain/TakeoutSaaS.Domain/"]
|
||||
COPY ["src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj", "src/Infrastructure/TakeoutSaaS.Infrastructure/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj", "src/Modules/TakeoutSaaS.Module.Authorization/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj", "src/Modules/TakeoutSaaS.Module.Delivery/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj", "src/Modules/TakeoutSaaS.Module.Dictionary/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj", "src/Modules/TakeoutSaaS.Module.Messaging/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj", "src/Modules/TakeoutSaaS.Module.Scheduler/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj", "src/Modules/TakeoutSaaS.Module.Sms/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj", "src/Modules/TakeoutSaaS.Module.Storage/"]
|
||||
COPY ["src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj", "src/Modules/TakeoutSaaS.Module.Tenancy/"]
|
||||
|
||||
RUN dotnet restore "src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj"
|
||||
|
||||
# Copy the rest of the source after restore for best cache reuse.
|
||||
COPY . .
|
||||
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish --no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 7801
|
||||
ENV ASPNETCORE_URLS=http://+:7801
|
||||
ENTRYPOINT ["dotnet", "TakeoutSaaS.AdminApi.dll"]
|
||||
@@ -1,199 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using Serilog;
|
||||
using System.Threading.RateLimiting;
|
||||
using TakeoutSaaS.Application.App.Extensions;
|
||||
using TakeoutSaaS.Application.Identity.Extensions;
|
||||
using TakeoutSaaS.Application.Messaging.Extensions;
|
||||
using TakeoutSaaS.Application.Sms.Extensions;
|
||||
using TakeoutSaaS.Application.Storage.Extensions;
|
||||
using TakeoutSaaS.Infrastructure.App.Extensions;
|
||||
using TakeoutSaaS.Infrastructure.Identity.Extensions;
|
||||
using TakeoutSaaS.Infrastructure.Logs.Extensions;
|
||||
using TakeoutSaaS.Module.Authorization.Extensions;
|
||||
using TakeoutSaaS.Module.Dictionary.Extensions;
|
||||
using TakeoutSaaS.Module.Messaging.Extensions;
|
||||
using TakeoutSaaS.Module.Scheduler.Extensions;
|
||||
using TakeoutSaaS.Module.Sms.Extensions;
|
||||
using TakeoutSaaS.Module.Storage.Extensions;
|
||||
using TakeoutSaaS.Module.Tenancy.Extensions;
|
||||
using TakeoutSaaS.Shared.Web.Extensions;
|
||||
using TakeoutSaaS.Shared.Web.Swagger;
|
||||
|
||||
// 1. 创建构建器与日志模板
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}";
|
||||
var isDevelopment = builder.Environment.IsDevelopment();
|
||||
|
||||
// 2. 加载种子配置文件
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.Seed.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.Seed.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
|
||||
// 3. 配置 Serilog 输出
|
||||
builder.Host.UseSerilog((context, _, configuration) =>
|
||||
{
|
||||
configuration
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Service", "AdminApi")
|
||||
.WriteTo.Console(outputTemplate: logTemplate)
|
||||
.WriteTo.File(
|
||||
"logs/admin-api-.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 7,
|
||||
shared: true,
|
||||
outputTemplate: logTemplate);
|
||||
});
|
||||
|
||||
// 4. 注册通用 Web 能力,开发环境启用 Swagger
|
||||
builder.Services.AddSharedWebCore();
|
||||
if (isDevelopment)
|
||||
{
|
||||
builder.Services.AddSharedSwagger(options =>
|
||||
{
|
||||
options.Title = "外卖SaaS - 管理后台";
|
||||
options.Description = "管理后台 API 文档";
|
||||
options.EnableAuthorization = true;
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 注册领域与基础设施模块
|
||||
builder.Services.AddIdentityApplication();
|
||||
builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true);
|
||||
builder.Services.AddAppInfrastructure(builder.Configuration);
|
||||
builder.Services.AddAppApplication();
|
||||
builder.Services.AddJwtAuthentication(builder.Configuration);
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddPermissionAuthorization();
|
||||
builder.Services.AddTenantResolution(builder.Configuration);
|
||||
builder.Services.AddDictionaryModule(builder.Configuration);
|
||||
builder.Services.AddStorageModule(builder.Configuration);
|
||||
builder.Services.AddStorageApplication();
|
||||
builder.Services.AddSmsModule(builder.Configuration);
|
||||
builder.Services.AddSmsApplication(builder.Configuration);
|
||||
builder.Services.AddMessagingModule(builder.Configuration);
|
||||
builder.Services.AddMessagingApplication();
|
||||
builder.Services.AddOperationLogOutbox(builder.Configuration);
|
||||
builder.Services.AddSchedulerModule(builder.Configuration);
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddFixedWindowLimiter("public-self-service", limiterOptions =>
|
||||
{
|
||||
limiterOptions.PermitLimit = 10;
|
||||
limiterOptions.Window = TimeSpan.FromMinutes(1);
|
||||
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
limiterOptions.QueueLimit = 2;
|
||||
});
|
||||
});
|
||||
|
||||
// 6. 配置 OpenTelemetry 采集
|
||||
var otelSection = builder.Configuration.GetSection("Otel");
|
||||
var otelEndpoint = otelSection.GetValue<string>("Endpoint");
|
||||
var useConsoleExporter = otelSection.GetValue<bool?>("UseConsoleExporter") ?? builder.Environment.IsDevelopment();
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource => resource.AddService(
|
||||
serviceName: "TakeoutSaaS.AdminApi",
|
||||
serviceVersion: "1.0.0",
|
||||
serviceInstanceId: Environment.MachineName))
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing
|
||||
.SetSampler(new ParentBasedSampler(new AlwaysOnSampler()))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddEntityFrameworkCoreInstrumentation();
|
||||
if (!string.IsNullOrWhiteSpace(otelEndpoint))
|
||||
{
|
||||
tracing.AddOtlpExporter(exporter =>
|
||||
{
|
||||
exporter.Endpoint = new Uri(otelEndpoint);
|
||||
});
|
||||
}
|
||||
|
||||
if (useConsoleExporter)
|
||||
{
|
||||
tracing.AddConsoleExporter();
|
||||
}
|
||||
})
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation()
|
||||
.AddPrometheusExporter();
|
||||
if (!string.IsNullOrWhiteSpace(otelEndpoint))
|
||||
{
|
||||
metrics.AddOtlpExporter(exporter =>
|
||||
{
|
||||
exporter.Endpoint = new Uri(otelEndpoint);
|
||||
});
|
||||
}
|
||||
|
||||
if (useConsoleExporter)
|
||||
{
|
||||
metrics.AddConsoleExporter();
|
||||
}
|
||||
});
|
||||
|
||||
// 7. 解析并配置 CORS
|
||||
var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin");
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AdminApiCors", policy =>
|
||||
{
|
||||
ConfigureCorsPolicy(policy, adminOrigins);
|
||||
});
|
||||
});
|
||||
|
||||
// 8. 构建应用并配置中间件管道
|
||||
var app = builder.Build();
|
||||
app.UseCors("AdminApiCors");
|
||||
app.UseTenantResolution();
|
||||
app.UseRateLimiter();
|
||||
app.UseSharedWebCore();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSharedSwagger();
|
||||
}
|
||||
app.UseSchedulerDashboard(builder.Configuration);
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.MapPrometheusScrapingEndpoint();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
|
||||
// 9. 解析配置中的 CORS 域名
|
||||
static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey)
|
||||
{
|
||||
var origins = configuration.GetSection(sectionKey).Get<string[]>();
|
||||
return origins?
|
||||
.Where(origin => !string.IsNullOrWhiteSpace(origin))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray() ?? [];
|
||||
}
|
||||
|
||||
// 10. 构建 CORS 策略
|
||||
static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins)
|
||||
{
|
||||
if (origins.Length == 0)
|
||||
{
|
||||
policy.AllowAnyOrigin();
|
||||
}
|
||||
else
|
||||
{
|
||||
policy.WithOrigins(origins)
|
||||
.AllowCredentials();
|
||||
}
|
||||
|
||||
policy
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"TakeoutSaaS.AdminApi": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:2680"
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "http://localhost:7801/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"publishAllPorts": false,
|
||||
"useSSL": false,
|
||||
"dockerRunOptions": "-p 7801:7801 --name TakeoutAdminApi"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>../../..</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.14.0-beta.2" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.14.0-beta.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Dictionary\TakeoutSaaS.Module.Dictionary.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Messaging\TakeoutSaaS.Module.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Scheduler\TakeoutSaaS.Module.Scheduler.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj" />
|
||||
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,195 +0,0 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false"
|
||||
},
|
||||
"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
|
||||
},
|
||||
"IdentityDatabase": {
|
||||
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"Reads": [
|
||||
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||
],
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxRetryCount": 3,
|
||||
"MaxRetryDelaySeconds": 5
|
||||
},
|
||||
"DictionaryDatabase": {
|
||||
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"Reads": [
|
||||
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||
],
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxRetryCount": 3,
|
||||
"MaxRetryDelaySeconds": 5
|
||||
},
|
||||
"LogsDatabase": {
|
||||
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"Reads": [
|
||||
"Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||
],
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxRetryCount": 3,
|
||||
"MaxRetryDelaySeconds": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"Identity": {
|
||||
"Jwt": {
|
||||
"Issuer": "takeout-saas",
|
||||
"Audience": "takeout-saas-clients",
|
||||
"Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx",
|
||||
"AccessTokenExpirationMinutes": 120,
|
||||
"RefreshTokenExpirationMinutes": 10080
|
||||
},
|
||||
"LoginRateLimit": {
|
||||
"WindowSeconds": 60,
|
||||
"MaxAttempts": 5
|
||||
},
|
||||
"RefreshTokenStore": {
|
||||
"Prefix": "identity:refresh:"
|
||||
},
|
||||
"AdminSeed": {
|
||||
"Users": []
|
||||
}
|
||||
},
|
||||
"Dictionary": {
|
||||
"Cache": {
|
||||
"SlidingExpiration": "00:30:00"
|
||||
}
|
||||
},
|
||||
"CacheWarmup": {
|
||||
"DictionaryCodes": [
|
||||
"order_status",
|
||||
"payment_method",
|
||||
"shipping_method",
|
||||
"product_category",
|
||||
"user_role"
|
||||
]
|
||||
},
|
||||
"Tenancy": {
|
||||
"TenantIdHeaderName": "X-Tenant-Id",
|
||||
"TenantCodeHeaderName": "X-Tenant-Code",
|
||||
"IgnoredPaths": [
|
||||
"/health"
|
||||
],
|
||||
"RootDomain": ""
|
||||
},
|
||||
"Storage": {
|
||||
"Provider": "TencentCos",
|
||||
"CdnBaseUrl": "https://image-admin.laosankeji.com",
|
||||
"TencentCos": {
|
||||
"SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R",
|
||||
"SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa",
|
||||
"Region": "ap-beijing",
|
||||
"Bucket": "saas-admin-1388556178",
|
||||
"Endpoint": "https://cos.ap-beijing.myqcloud.com",
|
||||
"CdnBaseUrl": "https://image-admin.laosankeji.com",
|
||||
"UseHttps": true,
|
||||
"ForcePathStyle": false
|
||||
},
|
||||
"QiniuKodo": {
|
||||
"AccessKey": "QINIU_ACCESS_KEY",
|
||||
"SecretKey": "QINIU_SECRET_KEY",
|
||||
"Bucket": "takeout-files",
|
||||
"DownloadDomain": "",
|
||||
"Endpoint": "",
|
||||
"UseHttps": true,
|
||||
"SignedUrlExpirationMinutes": 30
|
||||
},
|
||||
"AliyunOss": {
|
||||
"AccessKeyId": "OSS_ACCESS_KEY_ID",
|
||||
"AccessKeySecret": "OSS_ACCESS_KEY_SECRET",
|
||||
"Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
|
||||
"Bucket": "takeout-files",
|
||||
"CdnBaseUrl": "",
|
||||
"UseHttps": true
|
||||
},
|
||||
"Security": {
|
||||
"MaxFileSizeBytes": 10485760,
|
||||
"AllowedImageExtensions": [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".webp",
|
||||
".gif"
|
||||
],
|
||||
"AllowedFileExtensions": [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".webp",
|
||||
".gif",
|
||||
".pdf"
|
||||
],
|
||||
"DefaultUrlExpirationMinutes": 30,
|
||||
"EnableRefererValidation": false,
|
||||
"AllowedReferers": [
|
||||
"https://admin.example.com",
|
||||
"https://miniapp.example.com"
|
||||
],
|
||||
"AntiLeechTokenSecret": "ReplaceWithARandomToken"
|
||||
}
|
||||
},
|
||||
"Sms": {
|
||||
"Provider": "Tencent",
|
||||
"DefaultSignName": "外卖SaaS",
|
||||
"UseMock": true,
|
||||
"Tencent": {
|
||||
"SecretId": "TENCENT_SMS_SECRET_ID",
|
||||
"SecretKey": "TENCENT_SMS_SECRET_KEY",
|
||||
"SdkAppId": "1400000000",
|
||||
"SignName": "外卖SaaS",
|
||||
"Region": "ap-beijing",
|
||||
"Endpoint": "https://sms.tencentcloudapi.com"
|
||||
},
|
||||
"Aliyun": {
|
||||
"AccessKeyId": "ALIYUN_SMS_AK",
|
||||
"AccessKeySecret": "ALIYUN_SMS_SK",
|
||||
"Endpoint": "dysmsapi.aliyuncs.com",
|
||||
"SignName": "外卖SaaS",
|
||||
"Region": "cn-hangzhou"
|
||||
},
|
||||
"SceneTemplates": {
|
||||
"login": "LOGIN_TEMPLATE_ID",
|
||||
"register": "REGISTER_TEMPLATE_ID",
|
||||
"reset": "RESET_TEMPLATE_ID"
|
||||
},
|
||||
"VerificationCode": {
|
||||
"CodeLength": 6,
|
||||
"ExpireMinutes": 5,
|
||||
"CooldownSeconds": 60,
|
||||
"CachePrefix": "sms:code"
|
||||
}
|
||||
},
|
||||
"RabbitMQ": {
|
||||
"Host": "49.232.6.45",
|
||||
"Port": 5672,
|
||||
"Username": "Admin",
|
||||
"Password": "MsuMshk112233",
|
||||
"VirtualHost": "/",
|
||||
"Exchange": "takeout.events",
|
||||
"ExchangeType": "topic",
|
||||
"PrefetchCount": 20
|
||||
},
|
||||
"Scheduler": {
|
||||
"ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_hangfire_db;Username=hangfire_user;Password=HangFire112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"WorkerCount": 5,
|
||||
"DashboardEnabled": false,
|
||||
"DashboardPath": "/hangfire"
|
||||
},
|
||||
"Otel": {
|
||||
"Endpoint": "",
|
||||
"Sampling": "ParentBasedAlwaysOn",
|
||||
"UseConsoleExporter": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false"
|
||||
},
|
||||
"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
|
||||
},
|
||||
"IdentityDatabase": {
|
||||
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"Reads": [
|
||||
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||
],
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxRetryCount": 3,
|
||||
"MaxRetryDelaySeconds": 5
|
||||
},
|
||||
"DictionaryDatabase": {
|
||||
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"Reads": [
|
||||
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||
],
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxRetryCount": 3,
|
||||
"MaxRetryDelaySeconds": 5
|
||||
},
|
||||
"LogsDatabase": {
|
||||
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"Reads": [
|
||||
"Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
|
||||
],
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxRetryCount": 3,
|
||||
"MaxRetryDelaySeconds": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"Identity": {
|
||||
"Jwt": {
|
||||
"Issuer": "takeout-saas",
|
||||
"Audience": "takeout-saas-clients",
|
||||
"Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx",
|
||||
"AccessTokenExpirationMinutes": 120,
|
||||
"RefreshTokenExpirationMinutes": 10080
|
||||
},
|
||||
"LoginRateLimit": {
|
||||
"WindowSeconds": 60,
|
||||
"MaxAttempts": 5
|
||||
},
|
||||
"RefreshTokenStore": {
|
||||
"Prefix": "identity:refresh:"
|
||||
},
|
||||
"AdminSeed": {
|
||||
"Users": []
|
||||
}
|
||||
},
|
||||
"Dictionary": {
|
||||
"Cache": {
|
||||
"SlidingExpiration": "00:30:00"
|
||||
}
|
||||
},
|
||||
"CacheWarmup": {
|
||||
"DictionaryCodes": [
|
||||
"order_status",
|
||||
"payment_method",
|
||||
"shipping_method",
|
||||
"product_category",
|
||||
"user_role"
|
||||
]
|
||||
},
|
||||
"Tenancy": {
|
||||
"TenantIdHeaderName": "X-Tenant-Id",
|
||||
"TenantCodeHeaderName": "X-Tenant-Code",
|
||||
"IgnoredPaths": [
|
||||
"/health"
|
||||
],
|
||||
"RootDomain": ""
|
||||
},
|
||||
"Storage": {
|
||||
"Provider": "TencentCos",
|
||||
"CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com",
|
||||
"TencentCos": {
|
||||
"SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R",
|
||||
"SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa",
|
||||
"Region": "ap-beijing",
|
||||
"Bucket": "saas2025-1388556178",
|
||||
"Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com",
|
||||
"CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com",
|
||||
"UseHttps": true,
|
||||
"ForcePathStyle": false
|
||||
},
|
||||
"QiniuKodo": {
|
||||
"AccessKey": "QINIU_ACCESS_KEY",
|
||||
"SecretKey": "QINIU_SECRET_KEY",
|
||||
"Bucket": "takeout-files",
|
||||
"DownloadDomain": "",
|
||||
"Endpoint": "",
|
||||
"UseHttps": true,
|
||||
"SignedUrlExpirationMinutes": 30
|
||||
},
|
||||
"AliyunOss": {
|
||||
"AccessKeyId": "OSS_ACCESS_KEY_ID",
|
||||
"AccessKeySecret": "OSS_ACCESS_KEY_SECRET",
|
||||
"Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
|
||||
"Bucket": "takeout-files",
|
||||
"CdnBaseUrl": "",
|
||||
"UseHttps": true
|
||||
},
|
||||
"Security": {
|
||||
"MaxFileSizeBytes": 10485760,
|
||||
"AllowedImageExtensions": [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".webp",
|
||||
".gif"
|
||||
],
|
||||
"AllowedFileExtensions": [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".webp",
|
||||
".gif",
|
||||
".pdf"
|
||||
],
|
||||
"DefaultUrlExpirationMinutes": 30,
|
||||
"EnableRefererValidation": true,
|
||||
"AllowedReferers": [
|
||||
"https://admin.example.com",
|
||||
"https://miniapp.example.com"
|
||||
],
|
||||
"AntiLeechTokenSecret": "ReplaceWithARandomToken"
|
||||
}
|
||||
},
|
||||
"Sms": {
|
||||
"Provider": "Tencent",
|
||||
"DefaultSignName": "外卖SaaS",
|
||||
"UseMock": true,
|
||||
"Tencent": {
|
||||
"SecretId": "TENCENT_SMS_SECRET_ID",
|
||||
"SecretKey": "TENCENT_SMS_SECRET_KEY",
|
||||
"SdkAppId": "1400000000",
|
||||
"SignName": "外卖SaaS",
|
||||
"Region": "ap-beijing",
|
||||
"Endpoint": "https://sms.tencentcloudapi.com"
|
||||
},
|
||||
"Aliyun": {
|
||||
"AccessKeyId": "ALIYUN_SMS_AK",
|
||||
"AccessKeySecret": "ALIYUN_SMS_SK",
|
||||
"Endpoint": "dysmsapi.aliyuncs.com",
|
||||
"SignName": "外卖SaaS",
|
||||
"Region": "cn-hangzhou"
|
||||
},
|
||||
"SceneTemplates": {
|
||||
"login": "LOGIN_TEMPLATE_ID",
|
||||
"register": "REGISTER_TEMPLATE_ID",
|
||||
"reset": "RESET_TEMPLATE_ID"
|
||||
},
|
||||
"VerificationCode": {
|
||||
"CodeLength": 6,
|
||||
"ExpireMinutes": 5,
|
||||
"CooldownSeconds": 60,
|
||||
"CachePrefix": "sms:code"
|
||||
}
|
||||
},
|
||||
"RabbitMQ": {
|
||||
"Host": "49.232.6.45",
|
||||
"Port": 5672,
|
||||
"Username": "Admin",
|
||||
"Password": "MsuMshk112233",
|
||||
"VirtualHost": "/",
|
||||
"Exchange": "takeout.events",
|
||||
"ExchangeType": "topic",
|
||||
"PrefetchCount": 20
|
||||
},
|
||||
"Scheduler": {
|
||||
"ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_hangfire_db;Username=hangfire_user;Password=HangFire112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
|
||||
"WorkerCount": 5,
|
||||
"DashboardEnabled": false,
|
||||
"DashboardPath": "/hangfire"
|
||||
},
|
||||
"Otel": {
|
||||
"Endpoint": "",
|
||||
"Sampling": "ParentBasedAlwaysOn",
|
||||
"UseConsoleExporter": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
{
|
||||
"App": {
|
||||
"Seed": {
|
||||
"Enabled": false,
|
||||
"DefaultTenant": {
|
||||
"TenantId": 1000000000001,
|
||||
"Code": "demo",
|
||||
"Name": "Demo租户",
|
||||
"ShortName": "Demo",
|
||||
"ContactName": "DemoAdmin",
|
||||
"ContactPhone": "13800000000"
|
||||
},
|
||||
"DictionaryGroups": [
|
||||
{
|
||||
"Code": "order_status",
|
||||
"Name": "订单状态",
|
||||
"Scope": "Business",
|
||||
"Items": [
|
||||
{ "Key": "pending", "Value": "待支付", "SortOrder": 10 },
|
||||
{ "Key": "paid", "Value": "已支付", "SortOrder": 20 },
|
||||
{ "Key": "finished", "Value": "已完成", "SortOrder": 30 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"Code": "store_tags",
|
||||
"Name": "门店标签",
|
||||
"Scope": "Business",
|
||||
"Items": [
|
||||
{ "Key": "hot", "Value": "热门", "SortOrder": 10 },
|
||||
{ "Key": "new", "Value": "新店", "SortOrder": 20 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"SystemParameters": [
|
||||
{ "Key": "site_name", "Value": "外卖SaaS Demo", "Description": "演示环境站点名称", "SortOrder": 10, "IsEnabled": true },
|
||||
{ "Key": "order_auto_cancel_minutes", "Value": "30", "Description": "待支付自动取消时间(分钟)", "SortOrder": 20, "IsEnabled": true }
|
||||
]
|
||||
}
|
||||
},
|
||||
"Identity": {
|
||||
"AdminSeed": {
|
||||
"Enabled": false,
|
||||
"RoleTemplates": [
|
||||
{
|
||||
"TemplateCode": "platform-admin",
|
||||
"Name": "平台管理员",
|
||||
"Description": "平台全量权限",
|
||||
"IsActive": true,
|
||||
"Permissions": [
|
||||
"identity:profile:read",
|
||||
"identity:role:read",
|
||||
"identity:role:create",
|
||||
"identity:role:update",
|
||||
"identity:role:delete",
|
||||
"identity:role:bind-permission",
|
||||
"identity:permission:read",
|
||||
"identity:permission:create",
|
||||
"identity:permission:update",
|
||||
"identity:permission:delete",
|
||||
"identity:user:read",
|
||||
"identity:user:create",
|
||||
"identity:user:update",
|
||||
"identity:user:delete",
|
||||
"identity:user:status",
|
||||
"identity:user:reset-password",
|
||||
"identity:user:batch",
|
||||
"role-template:read",
|
||||
"role-template:create",
|
||||
"role-template:update",
|
||||
"role-template:delete",
|
||||
"tenant-bill:read",
|
||||
"tenant-bill:create",
|
||||
"tenant-bill:pay",
|
||||
"tenant-announcement:read",
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-announcement:publish",
|
||||
"tenant-announcement:revoke",
|
||||
"platform-announcement:create",
|
||||
"platform-announcement:publish",
|
||||
"platform-announcement:revoke",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:create",
|
||||
"tenant:read",
|
||||
"tenant:review",
|
||||
"tenant:review:force-claim",
|
||||
"tenant:subscription",
|
||||
"tenant:quota:check",
|
||||
"tenant-package:read",
|
||||
"tenant-package:create",
|
||||
"tenant-package:update",
|
||||
"tenant-package:delete",
|
||||
"merchant:create",
|
||||
"merchant:read",
|
||||
"merchant:update",
|
||||
"merchant:delete",
|
||||
"merchant:review",
|
||||
"merchant_category:read",
|
||||
"merchant_category:create",
|
||||
"merchant_category:update",
|
||||
"merchant_category:delete",
|
||||
"store:create",
|
||||
"store:read",
|
||||
"store:read:all",
|
||||
"store:update",
|
||||
"store:delete",
|
||||
"store-table-area:read",
|
||||
"store-table-area:create",
|
||||
"store-table-area:update",
|
||||
"store-table-area:delete",
|
||||
"store-table:read",
|
||||
"store-table:create",
|
||||
"store-table:update",
|
||||
"store-table:delete",
|
||||
"store-table:export",
|
||||
"store-staff:read",
|
||||
"store-staff:create",
|
||||
"store-staff:update",
|
||||
"store-staff:delete",
|
||||
"store-shift:read",
|
||||
"store-shift:create",
|
||||
"store-shift:update",
|
||||
"store-shift:delete",
|
||||
"product:create",
|
||||
"product:read",
|
||||
"product:update",
|
||||
"product:delete",
|
||||
"product:publish",
|
||||
"product-sku:read",
|
||||
"product-sku:update",
|
||||
"product-attr:read",
|
||||
"product-attr:update",
|
||||
"product-addon:read",
|
||||
"product-addon:update",
|
||||
"product-media:read",
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
"order:delete",
|
||||
"payment:create",
|
||||
"payment:read",
|
||||
"payment:update",
|
||||
"payment:delete",
|
||||
"delivery:create",
|
||||
"delivery:read",
|
||||
"delivery:update",
|
||||
"delivery:delete",
|
||||
"dictionary:group:read",
|
||||
"dictionary:group:create",
|
||||
"dictionary:group:update",
|
||||
"dictionary:group:delete",
|
||||
"dictionary:item:create",
|
||||
"dictionary:item:update",
|
||||
"dictionary:item:delete",
|
||||
"dictionary:override:read",
|
||||
"dictionary:override:update",
|
||||
"system-parameter:create",
|
||||
"system-parameter:read",
|
||||
"system-parameter:update",
|
||||
"system-parameter:delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateCode": "tenant-admin",
|
||||
"Name": "租户管理员",
|
||||
"Description": "管理本租户的门店、商品、订单与权限",
|
||||
"IsActive": true,
|
||||
"Permissions": [
|
||||
"identity:profile:read",
|
||||
"identity:role:read",
|
||||
"identity:role:create",
|
||||
"identity:role:update",
|
||||
"identity:role:delete",
|
||||
"identity:role:bind-permission",
|
||||
"identity:permission:read",
|
||||
"identity:permission:create",
|
||||
"identity:permission:update",
|
||||
"identity:permission:delete",
|
||||
"identity:user:read",
|
||||
"identity:user:create",
|
||||
"identity:user:update",
|
||||
"identity:user:delete",
|
||||
"identity:user:status",
|
||||
"identity:user:reset-password",
|
||||
"identity:user:batch",
|
||||
"tenant-bill:read",
|
||||
"tenant-bill:create",
|
||||
"tenant-bill:pay",
|
||||
"tenant-announcement:read",
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-announcement:publish",
|
||||
"tenant-announcement:revoke",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:read",
|
||||
"tenant:subscription",
|
||||
"tenant:quota:check",
|
||||
"merchant:read",
|
||||
"merchant:update",
|
||||
"merchant_category:read",
|
||||
"merchant_category:create",
|
||||
"merchant_category:update",
|
||||
"merchant_category:delete",
|
||||
"store:create",
|
||||
"store:read",
|
||||
"store:update",
|
||||
"store:delete",
|
||||
"store-table-area:read",
|
||||
"store-table-area:create",
|
||||
"store-table-area:update",
|
||||
"store-table-area:delete",
|
||||
"store-table:read",
|
||||
"store-table:create",
|
||||
"store-table:update",
|
||||
"store-table:delete",
|
||||
"store-table:export",
|
||||
"store-staff:read",
|
||||
"store-staff:create",
|
||||
"store-staff:update",
|
||||
"store-staff:delete",
|
||||
"store-shift:read",
|
||||
"store-shift:create",
|
||||
"store-shift:update",
|
||||
"store-shift:delete",
|
||||
"product:create",
|
||||
"product:read",
|
||||
"product:update",
|
||||
"product:delete",
|
||||
"product:publish",
|
||||
"product-sku:read",
|
||||
"product-sku:update",
|
||||
"product-attr:read",
|
||||
"product-attr:update",
|
||||
"product-addon:read",
|
||||
"product-addon:update",
|
||||
"product-media:read",
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"inventory:read",
|
||||
"inventory:adjust",
|
||||
"inventory:lock",
|
||||
"inventory:release",
|
||||
"inventory:deduct",
|
||||
"inventory:batch:read",
|
||||
"inventory:batch:update",
|
||||
"inventory:lock:expire",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
"delivery:create",
|
||||
"delivery:read",
|
||||
"delivery:update",
|
||||
"payment:create",
|
||||
"payment:read",
|
||||
"payment:update",
|
||||
"dictionary:group:read",
|
||||
"dictionary:group:create",
|
||||
"dictionary:group:update",
|
||||
"dictionary:group:delete",
|
||||
"dictionary:item:create",
|
||||
"dictionary:item:update",
|
||||
"dictionary:item:delete",
|
||||
"dictionary:override:read",
|
||||
"dictionary:override:update",
|
||||
"system-parameter:read"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateCode": "store-manager",
|
||||
"Name": "店长",
|
||||
"Description": "负责门店运营与商品、订单管理",
|
||||
"IsActive": true,
|
||||
"Permissions": [
|
||||
"identity:profile:read",
|
||||
"store:read",
|
||||
"store:update",
|
||||
"store-table-area:read",
|
||||
"store-table-area:create",
|
||||
"store-table-area:update",
|
||||
"store-table-area:delete",
|
||||
"store-table:read",
|
||||
"store-table:create",
|
||||
"store-table:update",
|
||||
"store-table:export",
|
||||
"store-staff:read",
|
||||
"store-staff:create",
|
||||
"store-staff:update",
|
||||
"store-shift:read",
|
||||
"store-shift:create",
|
||||
"store-shift:update",
|
||||
"product:create",
|
||||
"product:read",
|
||||
"product:update",
|
||||
"product:publish",
|
||||
"product-sku:read",
|
||||
"product-sku:update",
|
||||
"product-attr:read",
|
||||
"product-attr:update",
|
||||
"product-addon:read",
|
||||
"product-addon:update",
|
||||
"product-media:read",
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"inventory:read",
|
||||
"inventory:adjust",
|
||||
"inventory:lock",
|
||||
"inventory:release",
|
||||
"inventory:deduct",
|
||||
"inventory:batch:read",
|
||||
"inventory:batch:update",
|
||||
"inventory:lock:expire",
|
||||
"pickup-setting:read",
|
||||
"pickup-setting:update",
|
||||
"pickup-slot:read",
|
||||
"pickup-slot:create",
|
||||
"pickup-slot:update",
|
||||
"pickup-slot:delete",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
"delivery:read",
|
||||
"delivery:update",
|
||||
"payment:read",
|
||||
"payment:update",
|
||||
"dictionary:group:read",
|
||||
"dictionary:item:create",
|
||||
"dictionary:item:update",
|
||||
"dictionary:item:delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"TemplateCode": "store-staff",
|
||||
"Name": "店员",
|
||||
"Description": "处理订单履约与收款查询",
|
||||
"IsActive": true,
|
||||
"Permissions": [
|
||||
"identity:profile:read",
|
||||
"store:read",
|
||||
"store-table-area:read",
|
||||
"store-table:read",
|
||||
"store-shift:read",
|
||||
"product:read",
|
||||
"order:read",
|
||||
"order:update",
|
||||
"delivery:read",
|
||||
"payment:read"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Users": [
|
||||
{
|
||||
"Account": "admin",
|
||||
"DisplayName": "平台管理员",
|
||||
"Password": "Admin@123456",
|
||||
"TenantId": 1000000000001,
|
||||
"Roles": [ "PlatformAdmin" ],
|
||||
"Permissions": [
|
||||
"identity:profile:read",
|
||||
"identity:role:read",
|
||||
"identity:role:create",
|
||||
"identity:role:update",
|
||||
"identity:role:delete",
|
||||
"identity:role:bind-permission",
|
||||
"identity:permission:read",
|
||||
"identity:permission:create",
|
||||
"identity:permission:update",
|
||||
"identity:permission:delete",
|
||||
"role-template:read",
|
||||
"role-template:create",
|
||||
"role-template:update",
|
||||
"role-template:delete",
|
||||
"tenant-bill:read",
|
||||
"tenant-bill:create",
|
||||
"tenant-bill:pay",
|
||||
"tenant-announcement:read",
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-announcement:publish",
|
||||
"tenant-announcement:revoke",
|
||||
"platform-announcement:create",
|
||||
"platform-announcement:publish",
|
||||
"platform-announcement:revoke",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:create",
|
||||
"tenant:read",
|
||||
"tenant:review",
|
||||
"tenant:subscription",
|
||||
"tenant:quota:check",
|
||||
"tenant-package:read",
|
||||
"tenant-package:create",
|
||||
"tenant-package:update",
|
||||
"tenant-package:delete",
|
||||
"merchant:create",
|
||||
"merchant:read",
|
||||
"merchant:update",
|
||||
"merchant:delete",
|
||||
"merchant:review",
|
||||
"merchant_category:read",
|
||||
"merchant_category:create",
|
||||
"merchant_category:update",
|
||||
"merchant_category:delete",
|
||||
"store:create",
|
||||
"store:read",
|
||||
"store:read:all",
|
||||
"store:update",
|
||||
"store:delete",
|
||||
"product:create",
|
||||
"product:read",
|
||||
"product:update",
|
||||
"product:delete",
|
||||
"product:publish",
|
||||
"product-sku:read",
|
||||
"product-sku:update",
|
||||
"product-attr:read",
|
||||
"product-attr:update",
|
||||
"product-addon:read",
|
||||
"product-addon:update",
|
||||
"product-media:read",
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"inventory:read",
|
||||
"inventory:adjust",
|
||||
"inventory:lock",
|
||||
"inventory:release",
|
||||
"inventory:deduct",
|
||||
"inventory:batch:read",
|
||||
"inventory:batch:update",
|
||||
"inventory:lock:expire",
|
||||
"pickup-setting:read",
|
||||
"pickup-setting:update",
|
||||
"pickup-slot:read",
|
||||
"pickup-slot:create",
|
||||
"pickup-slot:update",
|
||||
"pickup-slot:delete",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
"order:delete",
|
||||
"payment:create",
|
||||
"payment:read",
|
||||
"payment:update",
|
||||
"payment:delete",
|
||||
"delivery:create",
|
||||
"delivery:read",
|
||||
"delivery:update",
|
||||
"delivery:delete",
|
||||
"dictionary:group:read",
|
||||
"dictionary:group:create",
|
||||
"dictionary:group:update",
|
||||
"dictionary:group:delete",
|
||||
"dictionary:item:create",
|
||||
"dictionary:item:update",
|
||||
"dictionary:item:delete",
|
||||
"system-parameter:create",
|
||||
"system-parameter:read",
|
||||
"system-parameter:update",
|
||||
"system-parameter:delete"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace TakeoutSaaS.ApiGateway.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 网关 OpenTelemetry 导出配置。
|
||||
/// </summary>
|
||||
public class GatewayOpenTelemetryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用 OpenTelemetry。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 服务名称,用于 Resource 标识。
|
||||
/// </summary>
|
||||
public string ServiceName { get; set; } = "TakeoutSaaS.ApiGateway";
|
||||
|
||||
/// <summary>
|
||||
/// OTLP 导出端点(http/https)。
|
||||
/// </summary>
|
||||
public string? OtlpEndpoint { get; set; }
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace TakeoutSaaS.ApiGateway.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 网关限流参数配置。
|
||||
/// </summary>
|
||||
public class GatewayRateLimitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否开启固定窗口限流。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 固定窗口内允许的最大请求数。
|
||||
/// </summary>
|
||||
public int PermitLimit { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// 固定窗口长度(秒)。
|
||||
/// </summary>
|
||||
public int WindowSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 排队等待的最大请求数。
|
||||
/// </summary>
|
||||
public int QueueLimit { get; set; } = 100;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# 1. 先只复制 csproj 文件 (利用 Docker 缓存)
|
||||
COPY ["src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj", "src/Gateway/TakeoutSaaS.ApiGateway/"]
|
||||
|
||||
# 2. 还原依赖 (如果 csproj 没变,这一步会直接使用缓存,瞬间完成)
|
||||
RUN dotnet restore "src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj"
|
||||
|
||||
# 3. 再复制剩余的所有源代码
|
||||
COPY . .
|
||||
|
||||
# 4. 发布
|
||||
RUN dotnet publish "src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj" -c Release -o /app/publish
|
||||
|
||||
# --- 运行时环境 ---
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# 显式声明端口
|
||||
EXPOSE 5000
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
|
||||
ENTRYPOINT ["dotnet", "TakeoutSaaS.ApiGateway.dll"]
|
||||
@@ -1,215 +0,0 @@
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using OpenTelemetry.Logs;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using Serilog;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.RateLimiting;
|
||||
using TakeoutSaaS.ApiGateway.Configuration;
|
||||
|
||||
const string CorsPolicyName = "GatewayCors";
|
||||
|
||||
// 1. 创建构建器并配置 Serilog
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
|
||||
{
|
||||
loggerConfiguration
|
||||
.ReadFrom.Configuration(context.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext();
|
||||
});
|
||||
|
||||
// 2. 配置 YARP 反向代理
|
||||
builder.Services.AddReverseProxy()
|
||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||
|
||||
// 3. 转发头部配置
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
options.KnownIPNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
// 4. 配置 CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(CorsPolicyName, policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
// 5. 配置网关限流
|
||||
builder.Services.Configure<GatewayRateLimitOptions>(builder.Configuration.GetSection("Gateway:RateLimiting"));
|
||||
var rateLimitOptions = builder.Configuration.GetSection("Gateway:RateLimiting").Get<GatewayRateLimitOptions>() ?? new();
|
||||
|
||||
if (rateLimitOptions.Enabled)
|
||||
{
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
|
||||
{
|
||||
var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
return RateLimitPartition.GetFixedWindowLimiter(remoteIp, _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = Math.Max(1, rateLimitOptions.PermitLimit),
|
||||
Window = TimeSpan.FromSeconds(Math.Max(1, rateLimitOptions.WindowSeconds)),
|
||||
QueueLimit = Math.Max(0, rateLimitOptions.QueueLimit),
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 配置 OpenTelemetry
|
||||
var otelOptions = builder.Configuration.GetSection("OpenTelemetry").Get<GatewayOpenTelemetryOptions>() ?? new();
|
||||
if (otelOptions.Enabled)
|
||||
{
|
||||
builder.Services.AddOpenTelemetry()
|
||||
// 1. 配置统一的 Resource,便于追踪定位。
|
||||
.ConfigureResource(resource => resource.AddService(otelOptions.ServiceName ?? "TakeoutSaaS.ApiGateway"))
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
|
||||
{
|
||||
metrics.AddOtlpExporter(options =>
|
||||
{
|
||||
options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
|
||||
});
|
||||
}
|
||||
})
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing.AddAspNetCoreInstrumentation(options =>
|
||||
{
|
||||
options.RecordException = true;
|
||||
})
|
||||
.AddHttpClientInstrumentation();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
|
||||
{
|
||||
tracing.AddOtlpExporter(options =>
|
||||
{
|
||||
options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeScopes = true;
|
||||
logging.ParseStateValues = true;
|
||||
if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint))
|
||||
{
|
||||
logging.AddOtlpExporter(options =>
|
||||
{
|
||||
options.Endpoint = new Uri(otelOptions.OtlpEndpoint);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7. 构建应用
|
||||
var app = builder.Build();
|
||||
|
||||
// 8. 转发头中间件
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
// 9. 全局异常处理中间件
|
||||
app.UseExceptionHandler(errorApp =>
|
||||
{
|
||||
// 1. 捕获所有未处理异常并返回统一结构。
|
||||
errorApp.Run(async context =>
|
||||
{
|
||||
var feature = context.Features.Get<IExceptionHandlerFeature>();
|
||||
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var payload = new
|
||||
{
|
||||
success = false,
|
||||
code = 500,
|
||||
message = "Gateway internal error",
|
||||
traceId
|
||||
};
|
||||
|
||||
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("Gateway");
|
||||
logger.LogError(feature?.Error, "网关异常 {TraceId}", traceId);
|
||||
await context.Response.WriteAsJsonAsync(payload, cancellationToken: context.RequestAborted);
|
||||
});
|
||||
});
|
||||
|
||||
// 10. 请求日志
|
||||
app.UseSerilogRequestLogging(options =>
|
||||
{
|
||||
options.MessageTemplate = "网关请求 {RequestMethod} {RequestPath} => {StatusCode} 用时 {Elapsed:0.000} 秒";
|
||||
options.GetLevel = (httpContext, elapsed, ex) => ex is not null ? Serilog.Events.LogEventLevel.Error : Serilog.Events.LogEventLevel.Information;
|
||||
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||||
{
|
||||
diagnosticContext.Set("TraceId", Activity.Current?.Id ?? httpContext.TraceIdentifier);
|
||||
diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
|
||||
};
|
||||
});
|
||||
|
||||
// 11. CORS 与限流
|
||||
app.UseCors(CorsPolicyName);
|
||||
|
||||
if (rateLimitOptions.Enabled)
|
||||
{
|
||||
app.UseRateLimiter();
|
||||
}
|
||||
|
||||
// 12. 透传请求头并保证 Trace
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
// 1. 确保请求拥有可追踪的 ID。
|
||||
if (!context.Request.Headers.ContainsKey("X-Request-Id"))
|
||||
{
|
||||
context.Request.Headers["X-Request-Id"] = Activity.Current?.Id ?? Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
// 2. 透传租户等标识头,方便下游继续使用。
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"];
|
||||
if (!string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
||||
}
|
||||
|
||||
var tenantCode = context.Request.Headers["X-Tenant-Code"];
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
context.Request.Headers["X-Tenant-Code"] = tenantCode;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
});
|
||||
|
||||
// 13. 映射反向代理与健康接口
|
||||
app.MapReverseProxy();
|
||||
|
||||
app.MapGet("/", () => Results.Json(new
|
||||
{
|
||||
Service = "TakeoutSaaS.ApiGateway",
|
||||
Status = "OK",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}));
|
||||
|
||||
app.MapGet("/healthz", () => Results.Json(new
|
||||
{
|
||||
Service = "TakeoutSaaS.ApiGateway",
|
||||
Status = "Healthy",
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}));
|
||||
|
||||
app.Run();
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"TakeoutSaaS.ApiGateway": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:2677;http://localhost:2683"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"OpenTelemetry": {
|
||||
"Enabled": false
|
||||
},
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"admin-route": {
|
||||
"ClusterId": "admin",
|
||||
"Match": { "Path": "/api/admin/{**catch-all}" }
|
||||
},
|
||||
"mini-route": {
|
||||
"ClusterId": "mini",
|
||||
"Match": { "Path": "/api/mini/{**catch-all}" }
|
||||
},
|
||||
"user-route": {
|
||||
"ClusterId": "user",
|
||||
"Match": { "Path": "/api/user/{**catch-all}" }
|
||||
}
|
||||
},
|
||||
"Clusters": {
|
||||
"admin": {
|
||||
"Destinations": {
|
||||
"d1": { "Address": "http://localhost:5001/" }
|
||||
}
|
||||
},
|
||||
"mini": {
|
||||
"Destinations": {
|
||||
"d1": { "Address": "http://localhost:5002/" }
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"Destinations": {
|
||||
"d1": { "Address": "http://localhost:5003/" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning",
|
||||
"Yarp": "Information"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [ "FromLogContext" ],
|
||||
"Properties": {
|
||||
"Application": "TakeoutSaaS.ApiGateway"
|
||||
}
|
||||
},
|
||||
"Gateway": {
|
||||
"RateLimiting": {
|
||||
"Enabled": true,
|
||||
"PermitLimit": 300,
|
||||
"WindowSeconds": 60,
|
||||
"QueueLimit": 100
|
||||
}
|
||||
},
|
||||
"OpenTelemetry": {
|
||||
"Enabled": true,
|
||||
"ServiceName": "TakeoutSaaS.ApiGateway",
|
||||
"OtlpEndpoint": "http://localhost:4317"
|
||||
},
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"admin-route": {
|
||||
"ClusterId": "admin",
|
||||
"Match": { "Path": "/api/admin/{**catch-all}" }
|
||||
},
|
||||
"mini-route": {
|
||||
"ClusterId": "mini",
|
||||
"Match": { "Path": "/api/mini/{**catch-all}" }
|
||||
},
|
||||
"user-route": {
|
||||
"ClusterId": "user",
|
||||
"Match": { "Path": "/api/user/{**catch-all}" }
|
||||
}
|
||||
},
|
||||
"Clusters": {
|
||||
"admin": {
|
||||
"Destinations": {
|
||||
"primary": { "Address": "http://49.7.179.246:7801/" }
|
||||
}
|
||||
},
|
||||
"mini": {
|
||||
"Destinations": {
|
||||
"primary": { "Address": "http://49.7.179.246:7701/" }
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"Destinations": {
|
||||
"primary": { "Address": "http://49.7.179.246:7901/" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user