diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AdminRolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AdminRolesController.cs
new file mode 100644
index 0000000..b8113a0
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AdminRolesController.cs
@@ -0,0 +1,197 @@
+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;
+
+///
+/// 平台角色管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/admin/v{version:apiVersion}/roles")]
+public sealed class AdminRolesController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 获取平台角色列表。
+ ///
+ /// 关键字(角色名称/编码)。
+ /// 页码(从 1 开始)。
+ /// 每页条数。
+ /// 取消标记。
+ /// 角色分页列表。
+ [HttpGet]
+ [PermissionAuthorize("identity:role:read")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] string? keyword,
+ [FromQuery] int page = 1,
+ [FromQuery] int pageSize = 20,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造查询
+ var query = new ListAdminRolesQuery
+ {
+ Keyword = keyword,
+ Page = page,
+ PageSize = pageSize
+ };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 返回分页结果
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 创建平台角色。
+ ///
+ /// 创建命令。
+ /// 取消标记。
+ /// 创建后的角色。
+ [HttpPost]
+ [PermissionAuthorize("identity:role:create")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Create(
+ [FromBody, Required] CreateAdminRoleCommand command,
+ CancellationToken cancellationToken)
+ {
+ // 1. 执行创建
+ var result = await mediator.Send(command, cancellationToken);
+
+ // 2. 返回创建结果
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 更新平台角色。
+ ///
+ /// 角色 ID。
+ /// 更新命令。
+ /// 取消标记。
+ /// 更新后的角色。
+ [HttpPut("{roleId:long}")]
+ [PermissionAuthorize("identity:role:update")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> Update(
+ long roleId,
+ [FromBody, Required] UpdateAdminRoleCommand command,
+ CancellationToken cancellationToken)
+ {
+ // 1. 绑定路由参数
+ command = command with { RoleId = roleId };
+
+ // 2. 执行更新
+ var result = await mediator.Send(command, cancellationToken);
+
+ // 3. 返回更新结果或 404
+ return result is null
+ ? ApiResponse.Error(StatusCodes.Status404NotFound, "角色不存在")
+ : ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 获取平台角色权限列表。
+ ///
+ /// 角色 ID。
+ /// 取消标记。
+ /// 权限集合。
+ [HttpGet("{roleId:long}/permissions")]
+ [PermissionAuthorize("identity:role:read")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> GetPermissions(
+ long roleId,
+ CancellationToken cancellationToken)
+ {
+ // 1. 构造查询
+ var query = new GetAdminRolePermissionsQuery { RoleId = roleId };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 返回权限集合
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 更新平台角色权限。
+ ///
+ /// 角色 ID。
+ /// 更新命令。
+ /// 取消标记。
+ /// 更新结果。
+ [HttpPut("{roleId:long}/permissions")]
+ [PermissionAuthorize("identity:role:bind-permission")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> UpdatePermissions(
+ long roleId,
+ [FromBody, Required] UpdateAdminRolePermissionsCommand command,
+ CancellationToken cancellationToken)
+ {
+ // 1. 绑定路由参数
+ command = command with { RoleId = roleId };
+
+ // 2. 执行更新
+ var result = await mediator.Send(command, cancellationToken);
+
+ // 3. 返回更新结果
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 删除平台角色。
+ ///
+ /// 角色 ID。
+ /// 取消标记。
+ /// 删除结果。
+ [HttpDelete("{roleId:long}")]
+ [PermissionAuthorize("identity:role:delete")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Delete(
+ long roleId,
+ CancellationToken cancellationToken)
+ {
+ // 1. 构造命令
+ var command = new DeleteAdminRoleCommand { RoleId = roleId };
+
+ // 2. 执行删除
+ var result = await mediator.Send(command, cancellationToken);
+
+ // 3. 返回删除结果
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 克隆平台角色。
+ ///
+ /// 源角色 ID。
+ /// 克隆命令。
+ /// 取消标记。
+ /// 克隆后的新角色。
+ [HttpPost("{roleId:long}/clone")]
+ [PermissionAuthorize("identity:role:create")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Clone(
+ long roleId,
+ [FromBody, Required] CloneAdminRoleCommand command,
+ CancellationToken cancellationToken)
+ {
+ // 1. 绑定路由参数
+ command = command with { SourceRoleId = roleId };
+
+ // 2. 执行克隆
+ var result = await mediator.Send(command, cancellationToken);
+
+ // 3. 返回克隆结果
+ return ApiResponse.Ok(result);
+ }
+}
diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs
new file mode 100644
index 0000000..c34edac
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/BillingsController.cs
@@ -0,0 +1,147 @@
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.Billings.Commands;
+using TakeoutSaaS.Application.App.Billings.Contracts;
+using TakeoutSaaS.Application.App.Billings.Queries;
+using TakeoutSaaS.Domain.Billings.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;
+
+///
+/// 账单管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/admin/v{version:apiVersion}/billings")]
+public sealed class BillingsController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 获取账单列表(分页)。
+ ///
+ /// 租户 ID。
+ /// 账单状态。
+ /// 账单类型。
+ /// 开始日期。
+ /// 结束日期。
+ /// 最小金额。
+ /// 最大金额。
+ /// 关键词(账单号、租户名)。
+ /// 排序字段。
+ /// 是否降序。
+ /// 页码(从 1 开始)。
+ /// 每页条数。
+ /// 取消标记。
+ /// 账单分页列表。
+ [HttpGet]
+ [PermissionAuthorize("tenant:read")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] long? TenantId,
+ [FromQuery] TenantBillingStatus? Status,
+ [FromQuery] TenantBillingType? BillingType,
+ [FromQuery] DateTime? StartDate,
+ [FromQuery] DateTime? EndDate,
+ [FromQuery] decimal? MinAmount,
+ [FromQuery] decimal? MaxAmount,
+ [FromQuery] string? Keyword,
+ [FromQuery] string? SortBy,
+ [FromQuery] bool? SortDesc,
+ [FromQuery] int PageNumber = 1,
+ [FromQuery] int PageSize = 10,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造查询
+ var query = new ListBillingsQuery
+ {
+ TenantId = TenantId,
+ Status = Status,
+ BillingType = BillingType,
+ StartDate = StartDate,
+ EndDate = EndDate,
+ MinAmount = MinAmount,
+ MaxAmount = MaxAmount,
+ Keyword = Keyword,
+ SortBy = SortBy,
+ SortDesc = SortDesc,
+ PageNumber = PageNumber,
+ PageSize = PageSize
+ };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 返回账单分页列表
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 获取账单详情。
+ ///
+ /// 账单 ID。
+ /// 取消标记。
+ /// 账单详情。
+ [HttpGet("{id:long}")]
+ [PermissionAuthorize("tenant:read")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> GetDetail(
+ [FromRoute] long id,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造查询
+ var query = new GetBillingDetailQuery { BillingId = id };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "账单不存在");
+ }
+
+ // 4. 返回账单详情
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 一键确认收款(记录支付 + 立即审核通过 + 同步更新账单状态)。
+ ///
+ /// 账单 ID。
+ /// 确认收款命令。
+ /// 取消标记。
+ /// 支付记录。
+ [HttpPost("{id:long}/payments/confirm")]
+ [PermissionAuthorize("tenant:read")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> ConfirmPayment(
+ [FromRoute] long id,
+ [FromBody] ConfirmPaymentCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造命令(使用路由中的 ID)
+ var cmd = command with { BillingId = id };
+
+ // 2. 执行命令
+ var result = await mediator.Send(cmd, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "账单不存在");
+ }
+
+ // 4. 返回支付记录
+ return ApiResponse.Ok(result);
+ }
+}
diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs
new file mode 100644
index 0000000..0083789
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs
@@ -0,0 +1,243 @@
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.Subscriptions.Commands;
+using TakeoutSaaS.Application.App.Subscriptions.Contracts;
+using TakeoutSaaS.Application.App.Subscriptions.Queries;
+using TakeoutSaaS.Domain.Tenants.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;
+
+///
+/// 订阅管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/admin/v{version:apiVersion}/subscriptions")]
+public sealed class SubscriptionsController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 获取订阅列表(分页)。
+ ///
+ /// 订阅状态。
+ /// 套餐 ID。
+ /// 租户 ID。
+ /// 租户关键词(名称或编码)。
+ /// 即将到期天数筛选。
+ /// 是否自动续费。
+ /// 到期时间范围开始。
+ /// 到期时间范围结束。
+ /// 页码(从 1 开始)。
+ /// 每页条数。
+ /// 取消标记。
+ /// 订阅分页列表。
+ [HttpGet]
+ [PermissionAuthorize("tenant:subscription")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] SubscriptionStatus? Status,
+ [FromQuery] long? TenantPackageId,
+ [FromQuery] long? TenantId,
+ [FromQuery] string? TenantKeyword,
+ [FromQuery] int? ExpiringWithinDays,
+ [FromQuery] bool? AutoRenew,
+ [FromQuery] DateTime? ExpireFrom,
+ [FromQuery] DateTime? ExpireTo,
+ [FromQuery] int Page = 1,
+ [FromQuery] int PageSize = 10,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造查询
+ var query = new ListSubscriptionsQuery
+ {
+ Status = Status,
+ TenantPackageId = TenantPackageId,
+ TenantId = TenantId,
+ TenantKeyword = TenantKeyword,
+ ExpiringWithinDays = ExpiringWithinDays,
+ AutoRenew = AutoRenew,
+ ExpireFrom = ExpireFrom,
+ ExpireTo = ExpireTo,
+ Page = Page,
+ PageSize = PageSize
+ };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 返回订阅分页列表
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 获取订阅详情。
+ ///
+ /// 订阅 ID。
+ /// 取消标记。
+ /// 订阅详情。
+ [HttpGet("{id:long}")]
+ [PermissionAuthorize("tenant:subscription")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> GetDetail(
+ [FromRoute] long id,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造查询
+ var query = new GetSubscriptionDetailQuery { SubscriptionId = id };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "订阅不存在");
+ }
+
+ // 4. 返回订阅详情
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 更新订阅。
+ ///
+ /// 订阅 ID。
+ /// 更新命令。
+ /// 取消标记。
+ /// 更新后的订阅信息。
+ [HttpPut("{id:long}")]
+ [PermissionAuthorize("tenant:subscription")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> Update(
+ [FromRoute] long id,
+ [FromBody] UpdateSubscriptionCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造命令(使用路由中的 ID)
+ var cmd = command with { SubscriptionId = id };
+
+ // 2. 执行命令
+ var result = await mediator.Send(cmd, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "订阅不存在");
+ }
+
+ // 4. 返回更新后的订阅信息
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 延期订阅。
+ ///
+ /// 订阅 ID。
+ /// 延期命令。
+ /// 取消标记。
+ /// 延期后的订阅信息。
+ [HttpPost("{id:long}/extend")]
+ [PermissionAuthorize("tenant:subscription")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> Extend(
+ [FromRoute] long id,
+ [FromBody] ExtendSubscriptionCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造命令(使用路由中的 ID)
+ var cmd = command with { SubscriptionId = id };
+
+ // 2. 执行命令
+ var result = await mediator.Send(cmd, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "订阅不存在");
+ }
+
+ // 4. 返回延期后的订阅信息
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 变更套餐。
+ ///
+ /// 订阅 ID。
+ /// 变更套餐命令。
+ /// 取消标记。
+ /// 变更后的订阅信息。
+ [HttpPost("{id:long}/change-plan")]
+ [PermissionAuthorize("tenant:subscription")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> ChangePlan(
+ [FromRoute] long id,
+ [FromBody] ChangePlanCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造命令(使用路由中的 ID)
+ var cmd = command with { SubscriptionId = id };
+
+ // 2. 执行命令
+ var result = await mediator.Send(cmd, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "订阅不存在");
+ }
+
+ // 4. 返回变更后的订阅信息
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 更新订阅状态。
+ ///
+ /// 订阅 ID。
+ /// 更新状态命令。
+ /// 取消标记。
+ /// 更新后的订阅信息。
+ [HttpPost("{id:long}/status")]
+ [PermissionAuthorize("tenant:subscription")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> UpdateStatus(
+ [FromRoute] long id,
+ [FromBody] UpdateStatusCommand command,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造命令(使用路由中的 ID)
+ var cmd = command with { SubscriptionId = id };
+
+ // 2. 执行命令
+ var result = await mediator.Send(cmd, cancellationToken);
+
+ // 3. 如果不存在,返回 404
+ if (result is null)
+ {
+ return ApiResponse.Error(
+ ErrorCodes.NotFound,
+ "订阅不存在");
+ }
+
+ // 4. 返回更新后的订阅信息
+ return ApiResponse.Ok(result);
+ }
+}
diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs
new file mode 100644
index 0000000..59ddfb9
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantPackagesController.cs
@@ -0,0 +1,238 @@
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.TenantPackages.Commands;
+using TakeoutSaaS.Application.App.TenantPackages.Contracts;
+using TakeoutSaaS.Application.App.TenantPackages.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;
+
+///
+/// 租户套餐管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/admin/v{version:apiVersion}/tenant-packages")]
+public sealed class TenantPackagesController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 获取租户套餐列表(分页)。
+ ///
+ /// 关键字(套餐名称)。
+ /// 是否启用。
+ /// 页码(从 1 开始)。
+ /// 每页条数。
+ /// 取消标记。
+ /// 套餐分页列表。
+ [HttpGet]
+ [PermissionAuthorize("tenant-package:read")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] string? Keyword,
+ [FromQuery] bool? IsActive,
+ [FromQuery] int Page = 1,
+ [FromQuery] int PageSize = 10,
+ CancellationToken cancellationToken = default)
+ {
+ // 1. 构造查询
+ var query = new ListTenantPackagesQuery
+ {
+ Keyword = Keyword,
+ IsActive = IsActive,
+ Page = Page,
+ PageSize = PageSize
+ };
+
+ // 2. 执行查询
+ var result = await mediator.Send(query, cancellationToken);
+
+ // 3. 返回套餐分页列表
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 获取租户套餐详情。
+ ///
+ /// 套餐 ID。
+ /// 取消标记。
+ /// 套餐详情。
+ [HttpGet("{tenantPackageId:long}")]
+ [PermissionAuthorize("tenant-package:read")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse