feat: add public tenant packages listing and sort order
This commit is contained in:
@@ -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. 持久化并返回
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user