diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs
new file mode 100644
index 0000000..3952b9b
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs
@@ -0,0 +1,49 @@
+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;
+
+///
+/// 公域租户订阅自助接口(需登录,无权限校验)。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[EnableRateLimiting("public-self-service")]
+[Route("api/public/v{version:apiVersion}/tenants")]
+public sealed class PublicTenantSubscriptionsController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 初次绑定租户订阅(默认 0 个月)。
+ ///
+ /// 租户 ID。
+ /// 绑定请求。
+ /// 取消标记。
+ /// 绑定后的订阅信息。
+ [HttpPost("{tenantId:long}/subscriptions/initial")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
+ public async Task> 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.Ok(result);
+ }
+}
+
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs
new file mode 100644
index 0000000..56192f5
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs
@@ -0,0 +1,29 @@
+using MediatR;
+using System.ComponentModel.DataAnnotations;
+using TakeoutSaaS.Application.App.Tenants.Dto;
+
+namespace TakeoutSaaS.Application.App.Tenants.Commands;
+
+///
+/// 租户初次绑定订阅命令(默认 0 个月)。
+///
+public sealed record BindInitialTenantSubscriptionCommand : IRequest
+{
+ ///
+ /// 租户 ID(雪花算法)。
+ ///
+ [Required]
+ public long TenantId { get; init; }
+
+ ///
+ /// 套餐 ID。
+ ///
+ [Required]
+ public long TenantPackageId { get; init; }
+
+ ///
+ /// 是否自动续费。
+ ///
+ public bool AutoRenew { get; init; }
+}
+
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs
new file mode 100644
index 0000000..4da9d2c
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs
@@ -0,0 +1,112 @@
+using MediatR;
+using System.Collections.Concurrent;
+using TakeoutSaaS.Application.App.Tenants.Commands;
+using TakeoutSaaS.Application.App.Tenants.Dto;
+using TakeoutSaaS.Application.App.Tenants;
+using TakeoutSaaS.Domain.Tenants.Entities;
+using TakeoutSaaS.Domain.Tenants.Enums;
+using TakeoutSaaS.Domain.Tenants.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Ids;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Tenants.Handlers;
+
+///
+/// 租户初次绑定订阅处理器。
+///
+public sealed class BindInitialTenantSubscriptionCommandHandler(
+ ITenantRepository tenantRepository,
+ IIdGenerator idGenerator,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ private static readonly ConcurrentDictionary TenantLocks = new();
+
+ ///
+ public async Task Handle(BindInitialTenantSubscriptionCommand request, CancellationToken cancellationToken)
+ {
+ // 1. 校验租户上下文
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId == 0 || currentTenantId != request.TenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "租户上下文不匹配,请在请求头 X-Tenant-Id 指定目标租户");
+ }
+
+ // 1.2 获取租户级幂等锁,避免并发重复创建
+ var tenantLock = GetTenantLock(request.TenantId);
+ await tenantLock.WaitAsync(cancellationToken);
+ try
+ {
+ // 2. 获取租户
+ var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
+ ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
+
+ // 3. 幂等校验:若已存在订阅则直接返回
+ var existing = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
+ if (existing is not null)
+ {
+ return existing.ToSubscriptionDto()
+ ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅读取失败");
+ }
+
+ // 4. 创建 0 个月订阅(待支付/待生效)
+ var now = DateTime.UtcNow;
+ var subscription = new TenantSubscription
+ {
+ Id = idGenerator.NextId(),
+ TenantId = tenant.Id,
+ TenantPackageId = request.TenantPackageId,
+ EffectiveFrom = now,
+ EffectiveTo = now,
+ NextBillingDate = now,
+ Status = SubscriptionStatus.Pending,
+ AutoRenew = request.AutoRenew,
+ Notes = "初次绑定订阅"
+ };
+
+ // 5. 记录订阅与历史
+ await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
+ await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
+ {
+ Id = idGenerator.NextId(),
+ TenantId = tenant.Id,
+ TenantSubscriptionId = subscription.Id,
+ FromPackageId = request.TenantPackageId,
+ ToPackageId = request.TenantPackageId,
+ ChangeType = SubscriptionChangeType.New,
+ EffectiveFrom = now,
+ EffectiveTo = now,
+ Amount = null,
+ Currency = null,
+ Notes = "初次绑定订阅(0 个月)"
+ }, cancellationToken);
+
+ // 6. 记录审计日志
+ await tenantRepository.AddAuditLogAsync(new TenantAuditLog
+ {
+ TenantId = tenant.Id,
+ Action = TenantAuditAction.SubscriptionUpdated,
+ Title = "初次绑定订阅",
+ Description = $"套餐 {request.TenantPackageId},时长 0 月"
+ }, cancellationToken);
+
+ // 7. 保存变更
+ await tenantRepository.SaveChangesAsync(cancellationToken);
+
+ // 8. 返回 DTO
+ return subscription.ToSubscriptionDto()
+ ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅绑定失败");
+ }
+ finally
+ {
+ // 9. 释放幂等锁
+ tenantLock.Release();
+ }
+ }
+
+ // 获取或创建租户级幂等锁实例。
+ private static SemaphoreSlim GetTenantLock(long tenantId)
+ => TenantLocks.GetOrAdd(tenantId, _ => new SemaphoreSlim(1, 1));
+}