feat: add public tenant packages listing and sort order

This commit is contained in:
2025-12-11 23:57:04 +08:00
parent cf9927c078
commit c7df64f2e1
28 changed files with 731 additions and 5 deletions

View File

@@ -37,7 +37,8 @@ public sealed class CreateTenantPackageCommandHandler(ITenantPackageRepository p
MaxSmsCredits = request.MaxSmsCredits,
MaxDeliveryOrders = request.MaxDeliveryOrders,
FeaturePoliciesJson = request.FeaturePoliciesJson,
IsActive = request.IsActive
IsActive = request.IsActive,
SortOrder = request.SortOrder
};
// 3. 持久化并返回

View File

@@ -0,0 +1,36 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 公共场景分页查询启用套餐处理器。
/// </summary>
public sealed class GetPublicTenantPackagesQueryHandler(ITenantPackageRepository packageRepository)
: IRequestHandler<GetPublicTenantPackagesQuery, PagedResult<TenantPackageDto>>
{
/// <inheritdoc />
public async Task<PagedResult<TenantPackageDto>> Handle(GetPublicTenantPackagesQuery request, CancellationToken cancellationToken)
{
// 1. 仅查询启用套餐
var packages = await packageRepository.SearchAsync(null, true, cancellationToken);
// 2. 规范化分页参数
var pageIndex = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
// 3. 执行排序、分页与映射
var ordered = packages
.OrderBy(x => x.SortOrder)
.ThenByDescending(x => x.CreatedAt)
.ToList();
var items = ordered
.Skip((pageIndex - 1) * size)
.Take(size)
.Select(x => x.ToDto())
.ToList();
// 4. 返回分页结果
return new PagedResult<TenantPackageDto>(items, pageIndex, size, ordered.Count);
}
}

View File

@@ -0,0 +1,39 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 租户入住进度查询处理器。
/// </summary>
public sealed class GetTenantProgressQueryHandler(ITenantRepository tenantRepository)
: IRequestHandler<GetTenantProgressQuery, TenantProgressDto>
{
/// <inheritdoc />
public async Task<TenantProgressDto> Handle(GetTenantProgressQuery request, CancellationToken cancellationToken)
{
// 1. 查询租户
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
// 2. 查询订阅与实名
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
// 3. 组装进度信息
return new TenantProgressDto
{
TenantId = tenant.Id,
Code = tenant.Code,
Status = tenant.Status,
VerificationStatus = verification?.Status ?? TenantVerificationStatus.Draft,
EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom,
EffectiveTo = subscription?.EffectiveTo ?? tenant.EffectiveTo
};
}
}

View File

@@ -19,10 +19,12 @@ public sealed class SearchTenantPackagesQueryHandler(ITenantPackageRepository pa
var packages = await packageRepository.SearchAsync(request.Keyword, request.IsActive, cancellationToken);
// 2. 排序与分页
var ordered = packages.OrderByDescending(x => x.CreatedAt).ToList();
var ordered = packages
.OrderBy(x => x.SortOrder)
.ThenByDescending(x => x.CreatedAt)
.ToList();
var pageIndex = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
var pagedItems = ordered
.Skip((pageIndex - 1) * size)
.Take(size)

View File

@@ -0,0 +1,133 @@
using MediatR;
using Microsoft.AspNetCore.Identity;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Domain.Identity.Repositories;
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;
/// <summary>
/// 自助注册租户处理器。
/// </summary>
public sealed class SelfRegisterTenantCommandHandler(
ITenantRepository tenantRepository,
IIdentityUserRepository identityUserRepository,
IRoleRepository roleRepository,
IPasswordHasher<IdentityUser> passwordHasher,
IIdGenerator idGenerator,
IMediator mediator,
ITenantContextAccessor tenantContextAccessor)
: IRequestHandler<SelfRegisterTenantCommand, SelfRegisterResultDto>
{
/// <inheritdoc />
public async Task<SelfRegisterResultDto> Handle(SelfRegisterTenantCommand request, CancellationToken cancellationToken)
{
// 1. 唯一性校验
var normalizedAccount = request.AdminAccount.Trim();
if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在");
}
// 1.2 校验手机号唯一性
var normalizedPhone = request.AdminPhone.Trim();
if (await tenantRepository.ExistsByContactPhoneAsync(normalizedPhone, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册");
}
// 2. 生成租户标识与编码
var tenantId = idGenerator.NextId();
var tenantCode = $"t{tenantId}";
// 3. 构建租户(无订阅,待审核)
var tenant = new Tenant
{
Id = tenantId,
Code = tenantCode,
Name = normalizedAccount,
ShortName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(),
ContactName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(),
ContactPhone = normalizedPhone,
ContactEmail = request.AdminEmail,
Status = TenantStatus.PendingReview,
EffectiveFrom = null,
EffectiveTo = null
};
// 4. 写入审计日志
var auditLog = new TenantAuditLog
{
TenantId = tenant.Id,
Action = TenantAuditAction.RegistrationSubmitted,
Title = "自助注册",
Description = "自助注册提交,等待补充资料与审核"
};
// 5. 持久化租户与审计
await tenantRepository.AddTenantAsync(tenant, cancellationToken);
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
// 6. 临时切换租户上下文,保证身份与权限写入正确
var previousContext = tenantContextAccessor.Current;
tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "self-register");
try
{
// 7. 使用用户自设密码创建管理员
var adminUser = new IdentityUser
{
TenantId = tenant.Id,
Account = normalizedAccount,
DisplayName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(),
PasswordHash = string.Empty
};
adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword);
await identityUserRepository.AddAsync(adminUser, cancellationToken);
await identityUserRepository.SaveChangesAsync(cancellationToken);
// 8. 初始化租户管理员角色模板
await mediator.Send(new InitializeRoleTemplatesCommand
{
TemplateCodes = new[] { "tenant-admin" }
}, cancellationToken);
// 9. 绑定租户管理员角色
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenant.Id, cancellationToken);
if (tenantAdminRole != null)
{
await mediator.Send(new AssignUserRolesCommand
{
UserId = adminUser.Id,
RoleIds = new[] { tenantAdminRole.Id }
}, cancellationToken);
}
// 10. 返回注册结果
return new SelfRegisterResultDto
{
TenantId = tenant.Id,
Code = tenant.Code,
Status = tenant.Status,
VerificationStatus = TenantVerificationStatus.Draft,
EffectiveFrom = tenant.EffectiveFrom,
EffectiveTo = tenant.EffectiveTo,
AdminAccount = adminUser.Account
};
}
finally
{
// 11. 恢复上下文
tenantContextAccessor.Current = previousContext;
}
}
}

View File

@@ -42,6 +42,7 @@ public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository p
package.MaxDeliveryOrders = request.MaxDeliveryOrders;
package.FeaturePoliciesJson = request.FeaturePoliciesJson;
package.IsActive = request.IsActive;
package.SortOrder = request.SortOrder;
// 4. 持久化并返回
await packageRepository.UpdateAsync(package, cancellationToken);