refactor: 移除管理端与网关项目

This commit is contained in:
root
2026-01-29 03:33:06 +00:00
parent 06981838a6
commit 0ae2589b51
65 changed files with 0 additions and 9184 deletions

View File

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

View File

@@ -1,12 +0,0 @@
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
/// <summary>
/// 字典导出请求。
/// </summary>
public sealed record DictionaryExportRequest
{
/// <summary>
/// 导出格式csv/json
/// </summary>
public string? Format { get; init; }
}

View File

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

View File

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

View File

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

View File

@@ -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&amp;pageSize=20
/// Header: Authorization: Bearer &lt;JWT&gt;
/// 响应:
/// {
/// "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&amp;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);
}
}

View File

@@ -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 &lt;JWT&gt;
/// 响应:
/// {
/// "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 &lt;JWT&gt;
/// 响应:
/// {
/// "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);
}
}

View File

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

View File

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

View File

@@ -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, "配送单不存在");
}
}

View File

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

View File

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

View File

@@ -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, "字典项不存在");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, "菜单已固定,禁止删除"));
}
}

View File

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

View File

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

View File

@@ -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, "订单不存在");
}
}

View File

@@ -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, "支付记录不存在");
}
}

View File

@@ -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&amp;page=1&amp;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, "权限已固定,禁止删除"));
}
}

View File

@@ -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 &lt;JWT&gt;
/// 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&amp;pageSize=20&amp;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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, "档期不存在");
}
}

View File

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

View File

@@ -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, "排班不存在");
}
}

View File

@@ -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, "员工不存在");
}
}

View File

@@ -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, "桌台区域不存在或不可删除");
}
}

View File

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

View File

@@ -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, "节假日配置不存在");
}
}

View File

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

View File

@@ -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, "系统参数不存在");
}
}

View File

@@ -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&amp;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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&amp;page=1&amp;pageSize=20&amp;sortBy=createdAt&amp;sortDescending=true
/// Header: Authorization: Bearer &lt;JWT&gt;
/// 响应:
/// {
/// "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);
}
}

View File

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

View File

@@ -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"]

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]

View File

@@ -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();

View File

@@ -1,12 +0,0 @@
{
"profiles": {
"TakeoutSaaS.ApiGateway": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:2677;http://localhost:2683"
}
}
}

View File

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

View File

@@ -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": "*"
}