fix:修复注释错误
This commit is contained in:
@@ -56,10 +56,10 @@
|
|||||||
var user = await _repo.GetAsync(id);
|
var user = await _repo.GetAsync(id);
|
||||||
if (user == null) return NotFound();
|
if (user == null) return NotFound();
|
||||||
|
|
||||||
// 2. (空行后) 扣减余额逻辑
|
// 2. 扣减余额逻辑
|
||||||
user.Balance -= amount;
|
user.Balance -= amount;
|
||||||
|
|
||||||
// 3. (空行后) 保存更改
|
// 3. 保存更改
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
```
|
```
|
||||||
* **Swagger**:必须开启 JWT 鉴权按钮,Request/Response 示例必须清晰。
|
* **Swagger**:必须开启 JWT 鉴权按钮,Request/Response 示例必须清晰。
|
||||||
|
|||||||
35
scripts/build-adminapi-forlinux
Executable file
35
scripts/build-adminapi-forlinux
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 用法:在 Linux 终端执行本脚本,自动构建并重启 AdminApi 容器。
|
||||||
|
# 前置:已安装并运行 Docker。
|
||||||
|
set -euo pipefail
|
||||||
|
# 0. 遇到异常时输出错误信息,方便查看
|
||||||
|
trap 'echo "发生错误:${BASH_COMMAND}" >&2' ERR
|
||||||
|
# 1. 基本变量(脚本位于 repo_root/scripts,下移一层再上跳到仓库根)
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd "${script_dir}/.." && pwd)"
|
||||||
|
image_name='takeout.api.admin:dev'
|
||||||
|
container_name='takeout.api.admin'
|
||||||
|
dockerfile_path="${repo_root}/src/Api/TakeoutSaaS.AdminApi/Dockerfile"
|
||||||
|
echo "工作目录:${repo_root}"
|
||||||
|
# 2. 停止并删除旧容器
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -qx "${container_name}"; then
|
||||||
|
echo "发现旧容器,正在移除:${container_name}"
|
||||||
|
docker stop "${container_name}" >/dev/null
|
||||||
|
docker rm "${container_name}" >/dev/null
|
||||||
|
fi
|
||||||
|
# 3. 删除旧镜像
|
||||||
|
if docker images --format '{{.Repository}}:{{.Tag}}' | grep -qx "${image_name}"; then
|
||||||
|
echo "发现旧镜像,正在移除:${image_name}"
|
||||||
|
docker rmi "${image_name}" >/dev/null
|
||||||
|
fi
|
||||||
|
# 4. 构建最新镜像(使用仓库根作为上下文)
|
||||||
|
echo "开始构建镜像:${image_name}"
|
||||||
|
docker build -f "${dockerfile_path}" -t "${image_name}" "${repo_root}"
|
||||||
|
# 5. 运行新容器并映射端口
|
||||||
|
echo "运行新容器:${container_name} (端口映射 7801:7801,环境 Development)"
|
||||||
|
docker run -d --name "${container_name}" -e ASPNETCORE_ENVIRONMENT=Development -p 7801:7801 "${image_name}"
|
||||||
|
echo "完成。镜像:${image_name},容器:${container_name}。Swagger 访问:http://localhost:7801/swagger"
|
||||||
|
# 6. 交互式终端下暂停,方便查看输出
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
read -r -p "按回车关闭窗口" _
|
||||||
|
fi
|
||||||
@@ -173,10 +173,10 @@ public sealed class BillingsController(IMediator mediator) : BaseApiController
|
|||||||
// 1. 绑定账单标识
|
// 1. 绑定账单标识
|
||||||
command = command with { BillingId = id };
|
command = command with { BillingId = id };
|
||||||
|
|
||||||
// 2. (空行后) 一键确认收款(含:写入 VerifiedBy/VerifiedAt,并同步更新账单已收金额/状态)
|
// 2. 一键确认收款(含:写入 VerifiedBy/VerifiedAt,并同步更新账单已收金额/状态)
|
||||||
var result = await mediator.Send(command, cancellationToken);
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回结果
|
// 3. 返回结果
|
||||||
return ApiResponse<PaymentRecordDto>.Ok(result);
|
return ApiResponse<PaymentRecordDto>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +196,10 @@ public sealed class BillingsController(IMediator mediator) : BaseApiController
|
|||||||
// 1. 绑定支付记录标识
|
// 1. 绑定支付记录标识
|
||||||
command = command with { PaymentId = paymentId };
|
command = command with { PaymentId = paymentId };
|
||||||
|
|
||||||
// 2. (空行后) 审核支付记录
|
// 2. 审核支付记录
|
||||||
var result = await mediator.Send(command, cancellationToken);
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回审核结果
|
// 3. 返回审核结果
|
||||||
return ApiResponse<PaymentRecordDto>.Ok(result);
|
return ApiResponse<PaymentRecordDto>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,18 +236,18 @@ public sealed class BillingsController(IMediator mediator) : BaseApiController
|
|||||||
// 1. 执行导出
|
// 1. 执行导出
|
||||||
var bytes = await mediator.Send(query, cancellationToken);
|
var bytes = await mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 解析格式并生成文件名
|
// 2. 解析格式并生成文件名
|
||||||
var extension = ResolveExportFileExtension(query.Format);
|
var extension = ResolveExportFileExtension(query.Format);
|
||||||
var fileName = $"billings_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}";
|
var fileName = $"billings_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}";
|
||||||
|
|
||||||
// 3. (空行后) 显式写入 Content-Disposition,确保浏览器以附件形式下载
|
// 3. 显式写入 Content-Disposition,确保浏览器以附件形式下载
|
||||||
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
||||||
{
|
{
|
||||||
FileName = fileName,
|
FileName = fileName,
|
||||||
FileNameStar = fileName
|
FileNameStar = fileName
|
||||||
}.ToString();
|
}.ToString();
|
||||||
|
|
||||||
// 4. (空行后) 返回二进制流(统一 octet-stream,避免被默认 JSON Produces 影响)
|
// 4. 返回二进制流(统一 octet-stream,避免被默认 JSON Produces 影响)
|
||||||
return File(bytes, "application/octet-stream");
|
return File(bytes, "application/octet-stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ public sealed class BillingsController(IMediator mediator) : BaseApiController
|
|||||||
// 1. 归一化导出格式
|
// 1. 归一化导出格式
|
||||||
var normalized = (format ?? string.Empty).Trim();
|
var normalized = (format ?? string.Empty).Trim();
|
||||||
|
|
||||||
// 2. (空行后) 映射扩展名
|
// 2. 映射扩展名
|
||||||
return normalized.ToUpperInvariant() switch
|
return normalized.ToUpperInvariant() switch
|
||||||
{
|
{
|
||||||
"PDF" => "pdf",
|
"PDF" => "pdf",
|
||||||
|
|||||||
@@ -105,11 +105,11 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
|||||||
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 tenantId 与请求体 tenantId 不一致");
|
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 tenantId 与请求体 tenantId 不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 绑定租户标识并执行更新(若不存在或冲突则抛出业务异常,由全局异常处理转换为 404/409)
|
// 2. 绑定租户标识并执行更新(若不存在或冲突则抛出业务异常,由全局异常处理转换为 404/409)
|
||||||
var command = body with { TenantId = tenantId };
|
var command = body with { TenantId = tenantId };
|
||||||
await mediator.Send(command, cancellationToken);
|
await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回成功结果
|
// 3. 返回成功结果
|
||||||
return ApiResponse<object>.Ok(null);
|
return ApiResponse<object>.Ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
|||||||
// 1. 生成一次性令牌
|
// 1. 生成一次性令牌
|
||||||
var token = await mediator.Send(new CreateTenantAdminResetLinkTokenCommand { TenantId = tenantId }, cancellationToken);
|
var token = await mediator.Send(new CreateTenantAdminResetLinkTokenCommand { TenantId = tenantId }, cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 解析前端来源(优先 Origin,避免拼成 AdminApi 域名)
|
// 2. 解析前端来源(优先 Origin,避免拼成 AdminApi 域名)
|
||||||
var origin = Request.Headers.Origin.ToString();
|
var origin = Request.Headers.Origin.ToString();
|
||||||
if (string.IsNullOrWhiteSpace(origin))
|
if (string.IsNullOrWhiteSpace(origin))
|
||||||
{
|
{
|
||||||
@@ -387,7 +387,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
|||||||
origin = origin.TrimEnd('/');
|
origin = origin.TrimEnd('/');
|
||||||
var resetUrl = $"{origin}/#/auth/reset-password?token={Uri.EscapeDataString(token)}";
|
var resetUrl = $"{origin}/#/auth/reset-password?token={Uri.EscapeDataString(token)}";
|
||||||
|
|
||||||
// 3. (空行后) 返回链接
|
// 3. 返回链接
|
||||||
return ApiResponse<string>.Ok(data: resetUrl);
|
return ApiResponse<string>.Ok(data: resetUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,10 +430,10 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
|||||||
// 1. 绑定租户标识
|
// 1. 绑定租户标识
|
||||||
query = query with { TenantId = tenantId };
|
query = query with { TenantId = tenantId };
|
||||||
|
|
||||||
// 2. (空行后) 查询配额使用历史
|
// 2. 查询配额使用历史
|
||||||
var result = await mediator.Send(query, cancellationToken);
|
var result = await mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回分页结果
|
// 3. 返回分页结果
|
||||||
return ApiResponse<PagedResult<QuotaUsageHistoryDto>>.Ok(result);
|
return ApiResponse<PagedResult<QuotaUsageHistoryDto>>.Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ public sealed class UsersController(IMediator mediator) : BaseApiController
|
|||||||
command = command with { UserId = userId };
|
command = command with { UserId = userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 执行更新
|
// 2. 执行更新
|
||||||
var result = await mediator.Send(command, cancellationToken);
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回结果或 404
|
// 3. 返回结果或 404
|
||||||
return result == null
|
return result == null
|
||||||
? ApiResponse<UserDetailDto>.Error(ErrorCodes.NotFound, "用户不存在")
|
? ApiResponse<UserDetailDto>.Error(ErrorCodes.NotFound, "用户不存在")
|
||||||
: ApiResponse<UserDetailDto>.Ok(result);
|
: ApiResponse<UserDetailDto>.Ok(result);
|
||||||
@@ -169,10 +169,10 @@ public sealed class UsersController(IMediator mediator) : BaseApiController
|
|||||||
command = command with { UserId = userId };
|
command = command with { UserId = userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 执行状态变更
|
// 2. 执行状态变更
|
||||||
var result = await mediator.Send(command, cancellationToken);
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回结果或 404
|
// 3. 返回结果或 404
|
||||||
return result
|
return result
|
||||||
? ApiResponse.Success()
|
? ApiResponse.Success()
|
||||||
: ApiResponse<object>.Error(ErrorCodes.NotFound, "用户不存在");
|
: ApiResponse<object>.Error(ErrorCodes.NotFound, "用户不存在");
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ public sealed class CancelBillingCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 取消账单(领域规则校验在实体方法内)
|
// 2. 取消账单(领域规则校验在实体方法内)
|
||||||
billing.Cancel(request.Reason);
|
billing.Cancel(request.Reason);
|
||||||
|
|
||||||
// 3. (空行后) 持久化
|
// 3. 持久化
|
||||||
await billingRepository.UpdateAsync(billing, cancellationToken);
|
await billingRepository.UpdateAsync(billing, cancellationToken);
|
||||||
await billingRepository.SaveChangesAsync(cancellationToken);
|
await billingRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ public sealed class ConfirmPaymentCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份");
|
throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 查询账单
|
// 2. 查询账单
|
||||||
var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken);
|
var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken);
|
||||||
if (billing is null)
|
if (billing is null)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 业务规则检查
|
// 3. 业务规则检查
|
||||||
if (billing.Status == TenantBillingStatus.Paid)
|
if (billing.Status == TenantBillingStatus.Paid)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款");
|
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款");
|
||||||
@@ -48,7 +48,7 @@ public sealed class ConfirmPaymentCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款");
|
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 金额边界:不允许超过剩余应收(与前端校验保持一致)
|
// 4. 金额边界:不允许超过剩余应收(与前端校验保持一致)
|
||||||
var totalAmount = billing.CalculateTotalAmount();
|
var totalAmount = billing.CalculateTotalAmount();
|
||||||
var remainingAmount = totalAmount - billing.AmountPaid;
|
var remainingAmount = totalAmount - billing.AmountPaid;
|
||||||
if (request.Amount > remainingAmount)
|
if (request.Amount > remainingAmount)
|
||||||
@@ -56,7 +56,7 @@ public sealed class ConfirmPaymentCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "支付金额不能超过剩余应收");
|
throw new BusinessException(ErrorCodes.BadRequest, "支付金额不能超过剩余应收");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 幂等校验:交易号唯一
|
// 5. 幂等校验:交易号唯一
|
||||||
if (!string.IsNullOrWhiteSpace(request.TransactionNo))
|
if (!string.IsNullOrWhiteSpace(request.TransactionNo))
|
||||||
{
|
{
|
||||||
var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken);
|
var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken);
|
||||||
@@ -66,7 +66,7 @@ public sealed class ConfirmPaymentCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 构建支付记录并立即审核通过
|
// 6. 构建支付记录并立即审核通过
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var payment = new TenantPayment
|
var payment = new TenantPayment
|
||||||
{
|
{
|
||||||
@@ -84,15 +84,15 @@ public sealed class ConfirmPaymentCommandHandler(
|
|||||||
|
|
||||||
payment.Verify(currentUserAccessor.UserId);
|
payment.Verify(currentUserAccessor.UserId);
|
||||||
|
|
||||||
// 7. (空行后) 同步更新账单已收金额/状态(支持分次收款)
|
// 7. 同步更新账单已收金额/状态(支持分次收款)
|
||||||
billing.MarkAsPaid(payment.Amount, payment.TransactionNo ?? string.Empty);
|
billing.MarkAsPaid(payment.Amount, payment.TransactionNo ?? string.Empty);
|
||||||
|
|
||||||
// 8. (空行后) 持久化变更(同一 DbContext 下单次 SaveChanges 可提交两张表)
|
// 8. 持久化变更(同一 DbContext 下单次 SaveChanges 可提交两张表)
|
||||||
await paymentRepository.AddAsync(payment, cancellationToken);
|
await paymentRepository.AddAsync(payment, cancellationToken);
|
||||||
await billingRepository.UpdateAsync(billing, cancellationToken);
|
await billingRepository.UpdateAsync(billing, cancellationToken);
|
||||||
await paymentRepository.SaveChangesAsync(cancellationToken);
|
await paymentRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 9. (空行后) 返回 DTO
|
// 9. 返回 DTO
|
||||||
return payment.ToPaymentRecordDto();
|
return payment.ToPaymentRecordDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class CreateBillingCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 构建账单实体
|
// 2. 构建账单实体
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}";
|
var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}";
|
||||||
var lineItemsJson = JsonSerializer.Serialize(request.LineItems);
|
var lineItemsJson = JsonSerializer.Serialize(request.LineItems);
|
||||||
@@ -54,11 +54,11 @@ public sealed class CreateBillingCommandHandler(
|
|||||||
Notes = request.Notes
|
Notes = request.Notes
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. (空行后) 持久化账单
|
// 3. 持久化账单
|
||||||
await billingRepository.AddAsync(billing, cancellationToken);
|
await billingRepository.AddAsync(billing, cancellationToken);
|
||||||
await billingRepository.SaveChangesAsync(cancellationToken);
|
await billingRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 返回详情 DTO
|
// 4. 返回详情 DTO
|
||||||
return billing.ToBillingDetailDto([], tenant.Name);
|
return billing.ToBillingDetailDto([], tenant.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ public sealed class ExportBillingsQueryHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "账单 ID 列表不能为空");
|
throw new BusinessException(ErrorCodes.BadRequest, "账单 ID 列表不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 查询账单数据
|
// 2. 查询账单数据
|
||||||
var billings = await billingRepository.GetByIdsAsync(request.BillingIds, cancellationToken);
|
var billings = await billingRepository.GetByIdsAsync(request.BillingIds, cancellationToken);
|
||||||
if (billings.Count == 0)
|
if (billings.Count == 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.NotFound, "未找到任何匹配的账单");
|
throw new BusinessException(ErrorCodes.NotFound, "未找到任何匹配的账单");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 根据格式导出
|
// 3. 根据格式导出
|
||||||
var format = (request.Format ?? string.Empty).Trim().ToLowerInvariant();
|
var format = (request.Format ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
return format switch
|
return format switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "订阅不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "订阅不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 校验套餐价格信息
|
// 2. 校验套餐价格信息
|
||||||
var subscription = detail.Subscription;
|
var subscription = detail.Subscription;
|
||||||
var package = detail.Package;
|
var package = detail.Package;
|
||||||
if (package is null)
|
if (package is null)
|
||||||
@@ -38,7 +38,7 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BusinessError, "订阅未关联有效套餐,无法生成账单");
|
throw new BusinessException(ErrorCodes.BusinessError, "订阅未关联有效套餐,无法生成账单");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 按订阅周期选择价格(简化规则:优先按年/按月)
|
// 3. 按订阅周期选择价格(简化规则:优先按年/按月)
|
||||||
var billingPeriodDays = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays;
|
var billingPeriodDays = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays;
|
||||||
var amountDue = billingPeriodDays >= 300
|
var amountDue = billingPeriodDays >= 300
|
||||||
? package.YearlyPrice
|
? package.YearlyPrice
|
||||||
@@ -49,14 +49,14 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BusinessError, "套餐价格未配置,无法生成账单");
|
throw new BusinessException(ErrorCodes.BusinessError, "套餐价格未配置,无法生成账单");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 幂等校验:同一周期开始时间仅允许存在一张未取消账单
|
// 4. 幂等校验:同一周期开始时间仅允许存在一张未取消账单
|
||||||
var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync(subscription.TenantId, subscription.EffectiveFrom, cancellationToken);
|
var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync(subscription.TenantId, subscription.EffectiveFrom, cancellationToken);
|
||||||
if (exists)
|
if (exists)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Conflict, "该订阅周期的账单已存在");
|
throw new BusinessException(ErrorCodes.Conflict, "该订阅周期的账单已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 构建账单实体
|
// 5. 构建账单实体
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}";
|
var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}";
|
||||||
var lineItems = new List<BillingLineItemDto>
|
var lineItems = new List<BillingLineItemDto>
|
||||||
@@ -91,11 +91,11 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
|
|||||||
Notes = subscription.Notes
|
Notes = subscription.Notes
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. (空行后) 持久化账单
|
// 6. 持久化账单
|
||||||
await billingRepository.AddAsync(billing, cancellationToken);
|
await billingRepository.AddAsync(billing, cancellationToken);
|
||||||
await billingRepository.SaveChangesAsync(cancellationToken);
|
await billingRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 7. (空行后) 返回详情 DTO
|
// 7. 返回详情 DTO
|
||||||
return billing.ToBillingDetailDto([], detail.TenantName);
|
return billing.ToBillingDetailDto([], detail.TenantName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public sealed class GetBillingDetailQueryHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.2 (空行后) 读取账单行数据到内存(释放 Reader,避免同连接并发执行命令)
|
// 1.2 读取账单行数据到内存(释放 Reader,避免同连接并发执行命令)
|
||||||
var billingId = billReader.GetInt64(0);
|
var billingId = billReader.GetInt64(0);
|
||||||
var tenantId = billReader.GetInt64(1);
|
var tenantId = billReader.GetInt64(1);
|
||||||
var tenantName = billReader.IsDBNull(2) ? string.Empty : billReader.GetString(2);
|
var tenantName = billReader.IsDBNull(2) ? string.Empty : billReader.GetString(2);
|
||||||
@@ -71,10 +71,10 @@ public sealed class GetBillingDetailQueryHandler(
|
|||||||
DateTime? updatedAt = billReader.IsDBNull(21) ? null : billReader.GetDateTime(21);
|
DateTime? updatedAt = billReader.IsDBNull(21) ? null : billReader.GetDateTime(21);
|
||||||
long? updatedBy = billReader.IsDBNull(22) ? null : billReader.GetInt64(22);
|
long? updatedBy = billReader.IsDBNull(22) ? null : billReader.GetInt64(22);
|
||||||
|
|
||||||
// 1.3 (空行后) 主动释放账单 Reader,确保后续查询不会触发 Npgsql 并发命令异常
|
// 1.3 主动释放账单 Reader,确保后续查询不会触发 Npgsql 并发命令异常
|
||||||
await billReader.DisposeAsync();
|
await billReader.DisposeAsync();
|
||||||
|
|
||||||
// 1.4 (空行后) 反序列化账单明细
|
// 1.4 反序列化账单明细
|
||||||
var lineItems = new List<BillingLineItemDto>();
|
var lineItems = new List<BillingLineItemDto>();
|
||||||
if (!string.IsNullOrWhiteSpace(lineItemsJson))
|
if (!string.IsNullOrWhiteSpace(lineItemsJson))
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@ public sealed class GetBillingDetailQueryHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.5 (空行后) 查询支付记录
|
// 1.5 查询支付记录
|
||||||
var payments = new List<PaymentRecordDto>();
|
var payments = new List<PaymentRecordDto>();
|
||||||
await using var paymentCommand = CreateCommand(
|
await using var paymentCommand = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
@@ -121,7 +121,7 @@ public sealed class GetBillingDetailQueryHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.6 (空行后) 组装详情 DTO
|
// 1.6 组装详情 DTO
|
||||||
var totalAmount = amountDue - discountAmount + taxAmount;
|
var totalAmount = amountDue - discountAmount + taxAmount;
|
||||||
|
|
||||||
return new BillingDetailDto
|
return new BillingDetailDto
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ public sealed class GetBillingListQueryHandler(
|
|||||||
var maxAmount = request.MaxAmount;
|
var maxAmount = request.MaxAmount;
|
||||||
var offset = (page - 1) * pageSize;
|
var offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
// 1.1 (空行后) 金额区间规范化(避免 min > max 导致结果为空)
|
// 1.1 金额区间规范化(避免 min > max 导致结果为空)
|
||||||
if (minAmount.HasValue && maxAmount.HasValue && minAmount.Value > maxAmount.Value)
|
if (minAmount.HasValue && maxAmount.HasValue && minAmount.Value > maxAmount.Value)
|
||||||
{
|
{
|
||||||
(minAmount, maxAmount) = (maxAmount, minAmount);
|
(minAmount, maxAmount) = (maxAmount, minAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 排序白名单(防 SQL 注入)
|
// 2. 排序白名单(防 SQL 注入)
|
||||||
var orderBy = request.SortBy?.Trim() switch
|
var orderBy = request.SortBy?.Trim() switch
|
||||||
{
|
{
|
||||||
"DueDate" => "b.\"DueDate\"",
|
"DueDate" => "b.\"DueDate\"",
|
||||||
@@ -50,7 +50,7 @@ public sealed class GetBillingListQueryHandler(
|
|||||||
_ => "b.\"CreatedAt\""
|
_ => "b.\"CreatedAt\""
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. (空行后) 查询总数 + 列表
|
// 3. 查询总数 + 列表
|
||||||
return await dapperExecutor.QueryAsync(
|
return await dapperExecutor.QueryAsync(
|
||||||
DatabaseConstants.AppDataSource,
|
DatabaseConstants.AppDataSource,
|
||||||
DatabaseConnectionRole.Read,
|
DatabaseConnectionRole.Read,
|
||||||
@@ -72,7 +72,7 @@ public sealed class GetBillingListQueryHandler(
|
|||||||
],
|
],
|
||||||
token);
|
token);
|
||||||
|
|
||||||
// 3.2 (空行后) 查询列表
|
// 3.2 查询列表
|
||||||
var listSql = BuildListSql(orderBy, request.SortDesc);
|
var listSql = BuildListSql(orderBy, request.SortDesc);
|
||||||
await using var listCommand = CreateCommand(
|
await using var listCommand = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
@@ -102,7 +102,7 @@ public sealed class GetBillingListQueryHandler(
|
|||||||
var taxAmount = reader.GetDecimal(10);
|
var taxAmount = reader.GetDecimal(10);
|
||||||
var totalAmount = amountDue - discountAmount + taxAmount;
|
var totalAmount = amountDue - discountAmount + taxAmount;
|
||||||
|
|
||||||
// 3.2.1 (空行后) 逾期辅助字段
|
// 3.2.1 逾期辅助字段
|
||||||
var isOverdue = status is TenantBillingStatus.Overdue
|
var isOverdue = status is TenantBillingStatus.Overdue
|
||||||
|| (status is TenantBillingStatus.Pending && dueDate < now);
|
|| (status is TenantBillingStatus.Pending && dueDate < now);
|
||||||
var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0;
|
var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0;
|
||||||
@@ -132,7 +132,7 @@ public sealed class GetBillingListQueryHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.3 (空行后) 返回分页
|
// 3.3 返回分页
|
||||||
return new PagedResult<BillingListDto>(items, page, pageSize, total);
|
return new PagedResult<BillingListDto>(items, page, pageSize, total);
|
||||||
},
|
},
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class GetBillingPaymentsQueryHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.2 (空行后) 查询支付记录
|
// 1.2 查询支付记录
|
||||||
await using var command = CreateCommand(
|
await using var command = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public sealed class GetBillingStatisticsQueryHandler(
|
|||||||
var endDate = request.EndDate ?? DateTime.UtcNow;
|
var endDate = request.EndDate ?? DateTime.UtcNow;
|
||||||
var groupBy = NormalizeGroupBy(request.GroupBy);
|
var groupBy = NormalizeGroupBy(request.GroupBy);
|
||||||
|
|
||||||
// 2. (空行后) 查询统计数据(总览 + 趋势)
|
// 2. 查询统计数据(总览 + 趋势)
|
||||||
return await dapperExecutor.QueryAsync(
|
return await dapperExecutor.QueryAsync(
|
||||||
DatabaseConstants.AppDataSource,
|
DatabaseConstants.AppDataSource,
|
||||||
DatabaseConnectionRole.Read,
|
DatabaseConnectionRole.Read,
|
||||||
@@ -59,7 +59,7 @@ public sealed class GetBillingStatisticsQueryHandler(
|
|||||||
var totalAmountUnpaid = summaryReader.IsDBNull(7) ? 0m : summaryReader.GetDecimal(7);
|
var totalAmountUnpaid = summaryReader.IsDBNull(7) ? 0m : summaryReader.GetDecimal(7);
|
||||||
var totalOverdueAmount = summaryReader.IsDBNull(8) ? 0m : summaryReader.GetDecimal(8);
|
var totalOverdueAmount = summaryReader.IsDBNull(8) ? 0m : summaryReader.GetDecimal(8);
|
||||||
|
|
||||||
// 2.2 (空行后) 趋势数据
|
// 2.2 趋势数据
|
||||||
await using var trendCommand = CreateCommand(
|
await using var trendCommand = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
BuildTrendSql(groupBy),
|
BuildTrendSql(groupBy),
|
||||||
@@ -83,7 +83,7 @@ public sealed class GetBillingStatisticsQueryHandler(
|
|||||||
countTrend[key] = trendReader.IsDBNull(3) ? 0 : trendReader.GetInt32(3);
|
countTrend[key] = trendReader.IsDBNull(3) ? 0 : trendReader.GetInt32(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.3 (空行后) 组装 DTO
|
// 2.3 组装 DTO
|
||||||
return new BillingStatisticsDto
|
return new BillingStatisticsDto
|
||||||
{
|
{
|
||||||
TenantId = request.TenantId,
|
TenantId = request.TenantId,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class GetOverdueBillingsQueryHandler(
|
|||||||
var offset = (page - 1) * pageSize;
|
var offset = (page - 1) * pageSize;
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
// 2. (空行后) 查询总数 + 列表
|
// 2. 查询总数 + 列表
|
||||||
return await dapperExecutor.QueryAsync(
|
return await dapperExecutor.QueryAsync(
|
||||||
DatabaseConstants.AppDataSource,
|
DatabaseConstants.AppDataSource,
|
||||||
DatabaseConnectionRole.Read,
|
DatabaseConnectionRole.Read,
|
||||||
@@ -46,7 +46,7 @@ public sealed class GetOverdueBillingsQueryHandler(
|
|||||||
],
|
],
|
||||||
token);
|
token);
|
||||||
|
|
||||||
// 2.2 (空行后) 查询列表
|
// 2.2 查询列表
|
||||||
await using var listCommand = CreateCommand(
|
await using var listCommand = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
BuildListSql(),
|
BuildListSql(),
|
||||||
@@ -93,7 +93,7 @@ public sealed class GetOverdueBillingsQueryHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.3 (空行后) 返回分页
|
// 2.3 返回分页
|
||||||
return new PagedResult<BillingListDto>(items, page, pageSize, total);
|
return new PagedResult<BillingListDto>(items, page, pageSize, total);
|
||||||
},
|
},
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public sealed class RecordPaymentCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 业务规则检查
|
// 2. 业务规则检查
|
||||||
if (billing.Status == TenantBillingStatus.Paid)
|
if (billing.Status == TenantBillingStatus.Paid)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款");
|
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款");
|
||||||
@@ -45,7 +45,7 @@ public sealed class RecordPaymentCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款");
|
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 幂等校验:交易号唯一
|
// 3. 幂等校验:交易号唯一
|
||||||
if (!string.IsNullOrWhiteSpace(request.TransactionNo))
|
if (!string.IsNullOrWhiteSpace(request.TransactionNo))
|
||||||
{
|
{
|
||||||
var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken);
|
var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken);
|
||||||
@@ -55,7 +55,7 @@ public sealed class RecordPaymentCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 构建支付记录(默认待审核)
|
// 4. 构建支付记录(默认待审核)
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var payment = new TenantPayment
|
var payment = new TenantPayment
|
||||||
{
|
{
|
||||||
@@ -71,11 +71,11 @@ public sealed class RecordPaymentCommandHandler(
|
|||||||
Notes = request.Notes
|
Notes = request.Notes
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. (空行后) 持久化变更
|
// 5. 持久化变更
|
||||||
await paymentRepository.AddAsync(payment, cancellationToken);
|
await paymentRepository.AddAsync(payment, cancellationToken);
|
||||||
await paymentRepository.SaveChangesAsync(cancellationToken);
|
await paymentRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 6. (空行后) 返回 DTO
|
// 6. 返回 DTO
|
||||||
return payment.ToPaymentRecordDto();
|
return payment.ToPaymentRecordDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class UpdateBillingStatusCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 状态转换规则校验
|
// 2. 状态转换规则校验
|
||||||
if (billing.Status == TenantBillingStatus.Paid && request.NewStatus != TenantBillingStatus.Paid)
|
if (billing.Status == TenantBillingStatus.Paid && request.NewStatus != TenantBillingStatus.Paid)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许改为其他状态");
|
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许改为其他状态");
|
||||||
@@ -35,7 +35,7 @@ public sealed class UpdateBillingStatusCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许变更状态");
|
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许变更状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 更新状态与备注
|
// 3. 更新状态与备注
|
||||||
billing.Status = request.NewStatus;
|
billing.Status = request.NewStatus;
|
||||||
if (!string.IsNullOrWhiteSpace(request.Notes))
|
if (!string.IsNullOrWhiteSpace(request.Notes))
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ public sealed class UpdateBillingStatusCommandHandler(
|
|||||||
: $"{billing.Notes}\n[状态变更] {request.Notes}";
|
: $"{billing.Notes}\n[状态变更] {request.Notes}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 持久化
|
// 4. 持久化
|
||||||
await billingRepository.UpdateAsync(billing, cancellationToken);
|
await billingRepository.UpdateAsync(billing, cancellationToken);
|
||||||
await billingRepository.SaveChangesAsync(cancellationToken);
|
await billingRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -26,24 +26,24 @@ public sealed class VerifyPaymentCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份");
|
throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 查询支付记录
|
// 2. 查询支付记录
|
||||||
var payment = await paymentRepository.FindByIdAsync(request.PaymentId, cancellationToken);
|
var payment = await paymentRepository.FindByIdAsync(request.PaymentId, cancellationToken);
|
||||||
if (payment is null)
|
if (payment is null)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.NotFound, "支付记录不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "支付记录不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询关联账单
|
// 3. 查询关联账单
|
||||||
var billing = await billingRepository.FindByIdAsync(payment.BillingStatementId, cancellationToken);
|
var billing = await billingRepository.FindByIdAsync(payment.BillingStatementId, cancellationToken);
|
||||||
if (billing is null)
|
if (billing is null)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.NotFound, "关联账单不存在");
|
throw new BusinessException(ErrorCodes.NotFound, "关联账单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 归一化审核备注
|
// 4. 归一化审核备注
|
||||||
var normalizedNotes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
|
var normalizedNotes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
|
||||||
|
|
||||||
// 5. (空行后) 根据审核结果更新支付与账单状态
|
// 5. 根据审核结果更新支付与账单状态
|
||||||
if (request.Approved)
|
if (request.Approved)
|
||||||
{
|
{
|
||||||
payment.Verify(currentUserAccessor.UserId);
|
payment.Verify(currentUserAccessor.UserId);
|
||||||
@@ -57,17 +57,17 @@ public sealed class VerifyPaymentCommandHandler(
|
|||||||
payment.Notes = normalizedNotes;
|
payment.Notes = normalizedNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 持久化更新状态
|
// 6. 持久化更新状态
|
||||||
await paymentRepository.UpdateAsync(payment, cancellationToken);
|
await paymentRepository.UpdateAsync(payment, cancellationToken);
|
||||||
if (request.Approved)
|
if (request.Approved)
|
||||||
{
|
{
|
||||||
await billingRepository.UpdateAsync(billing, cancellationToken);
|
await billingRepository.UpdateAsync(billing, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. (空行后) 保存数据库更改
|
// 7. 保存数据库更改
|
||||||
await paymentRepository.SaveChangesAsync(cancellationToken);
|
await paymentRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 8. (空行后) 返回 DTO
|
// 8. 返回 DTO
|
||||||
return payment.ToPaymentRecordDto();
|
return payment.ToPaymentRecordDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,32 +15,32 @@ public sealed class ConfirmPaymentCommandValidator : AbstractValidator<ConfirmPa
|
|||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("账单 ID 必须大于 0");
|
.WithMessage("账单 ID 必须大于 0");
|
||||||
|
|
||||||
// 2. (空行后) 支付金额必须大于 0
|
// 2. 支付金额必须大于 0
|
||||||
RuleFor(x => x.Amount)
|
RuleFor(x => x.Amount)
|
||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("支付金额必须大于 0")
|
.WithMessage("支付金额必须大于 0")
|
||||||
.LessThanOrEqualTo(1_000_000_000)
|
.LessThanOrEqualTo(1_000_000_000)
|
||||||
.WithMessage("支付金额不能超过 10 亿");
|
.WithMessage("支付金额不能超过 10 亿");
|
||||||
|
|
||||||
// 3. (空行后) 支付方式必填
|
// 3. 支付方式必填
|
||||||
RuleFor(x => x.Method)
|
RuleFor(x => x.Method)
|
||||||
.IsInEnum()
|
.IsInEnum()
|
||||||
.WithMessage("支付方式无效");
|
.WithMessage("支付方式无效");
|
||||||
|
|
||||||
// 4. (空行后) 交易号必填
|
// 4. 交易号必填
|
||||||
RuleFor(x => x.TransactionNo)
|
RuleFor(x => x.TransactionNo)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("交易号不能为空")
|
.WithMessage("交易号不能为空")
|
||||||
.MaximumLength(64)
|
.MaximumLength(64)
|
||||||
.WithMessage("交易号不能超过 64 个字符");
|
.WithMessage("交易号不能超过 64 个字符");
|
||||||
|
|
||||||
// 5. (空行后) 支付凭证 URL(可选)
|
// 5. 支付凭证 URL(可选)
|
||||||
RuleFor(x => x.ProofUrl)
|
RuleFor(x => x.ProofUrl)
|
||||||
.MaximumLength(500)
|
.MaximumLength(500)
|
||||||
.WithMessage("支付凭证 URL 不能超过 500 个字符")
|
.WithMessage("支付凭证 URL 不能超过 500 个字符")
|
||||||
.When(x => !string.IsNullOrWhiteSpace(x.ProofUrl));
|
.When(x => !string.IsNullOrWhiteSpace(x.ProofUrl));
|
||||||
|
|
||||||
// 6. (空行后) 备注(可选)
|
// 6. 备注(可选)
|
||||||
RuleFor(x => x.Notes)
|
RuleFor(x => x.Notes)
|
||||||
.MaximumLength(500)
|
.MaximumLength(500)
|
||||||
.WithMessage("备注不能超过 500 个字符")
|
.WithMessage("备注不能超过 500 个字符")
|
||||||
|
|||||||
@@ -15,27 +15,27 @@ public sealed class CreateBillingCommandValidator : AbstractValidator<CreateBill
|
|||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("租户 ID 必须大于 0");
|
.WithMessage("租户 ID 必须大于 0");
|
||||||
|
|
||||||
// 2. (空行后) 账单类型必填
|
// 2. 账单类型必填
|
||||||
RuleFor(x => x.BillingType)
|
RuleFor(x => x.BillingType)
|
||||||
.IsInEnum()
|
.IsInEnum()
|
||||||
.WithMessage("账单类型无效");
|
.WithMessage("账单类型无效");
|
||||||
|
|
||||||
// 3. (空行后) 应付金额必须大于 0
|
// 3. 应付金额必须大于 0
|
||||||
RuleFor(x => x.AmountDue)
|
RuleFor(x => x.AmountDue)
|
||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("应付金额必须大于 0");
|
.WithMessage("应付金额必须大于 0");
|
||||||
|
|
||||||
// 4. (空行后) 到期日必须是未来时间
|
// 4. 到期日必须是未来时间
|
||||||
RuleFor(x => x.DueDate)
|
RuleFor(x => x.DueDate)
|
||||||
.GreaterThan(DateTime.UtcNow)
|
.GreaterThan(DateTime.UtcNow)
|
||||||
.WithMessage("到期日必须是未来时间");
|
.WithMessage("到期日必须是未来时间");
|
||||||
|
|
||||||
// 5. (空行后) 账单明细至少包含一项
|
// 5. 账单明细至少包含一项
|
||||||
RuleFor(x => x.LineItems)
|
RuleFor(x => x.LineItems)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("账单明细不能为空");
|
.WithMessage("账单明细不能为空");
|
||||||
|
|
||||||
// 6. (空行后) 账单明细项验证
|
// 6. 账单明细项验证
|
||||||
RuleForEach(x => x.LineItems)
|
RuleForEach(x => x.LineItems)
|
||||||
.ChildRules(lineItem =>
|
.ChildRules(lineItem =>
|
||||||
{
|
{
|
||||||
@@ -64,7 +64,7 @@ public sealed class CreateBillingCommandValidator : AbstractValidator<CreateBill
|
|||||||
.WithMessage("账单明细金额不能为负数");
|
.WithMessage("账单明细金额不能为负数");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7. (空行后) 备注长度限制(可选)
|
// 7. 备注长度限制(可选)
|
||||||
RuleFor(x => x.Notes)
|
RuleFor(x => x.Notes)
|
||||||
.MaximumLength(500)
|
.MaximumLength(500)
|
||||||
.WithMessage("备注不能超过 500 个字符")
|
.WithMessage("备注不能超过 500 个字符")
|
||||||
|
|||||||
@@ -15,32 +15,32 @@ public sealed class RecordPaymentCommandValidator : AbstractValidator<RecordPaym
|
|||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("账单 ID 必须大于 0");
|
.WithMessage("账单 ID 必须大于 0");
|
||||||
|
|
||||||
// 2. (空行后) 支付金额必须大于 0
|
// 2. 支付金额必须大于 0
|
||||||
RuleFor(x => x.Amount)
|
RuleFor(x => x.Amount)
|
||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("支付金额必须大于 0")
|
.WithMessage("支付金额必须大于 0")
|
||||||
.LessThanOrEqualTo(1_000_000_000)
|
.LessThanOrEqualTo(1_000_000_000)
|
||||||
.WithMessage("支付金额不能超过 10 亿");
|
.WithMessage("支付金额不能超过 10 亿");
|
||||||
|
|
||||||
// 3. (空行后) 支付方式必填
|
// 3. 支付方式必填
|
||||||
RuleFor(x => x.Method)
|
RuleFor(x => x.Method)
|
||||||
.IsInEnum()
|
.IsInEnum()
|
||||||
.WithMessage("支付方式无效");
|
.WithMessage("支付方式无效");
|
||||||
|
|
||||||
// 4. (空行后) 交易号必填
|
// 4. 交易号必填
|
||||||
RuleFor(x => x.TransactionNo)
|
RuleFor(x => x.TransactionNo)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("交易号不能为空")
|
.WithMessage("交易号不能为空")
|
||||||
.MaximumLength(64)
|
.MaximumLength(64)
|
||||||
.WithMessage("交易号不能超过 64 个字符");
|
.WithMessage("交易号不能超过 64 个字符");
|
||||||
|
|
||||||
// 5. (空行后) 支付凭证 URL(可选)
|
// 5. 支付凭证 URL(可选)
|
||||||
RuleFor(x => x.ProofUrl)
|
RuleFor(x => x.ProofUrl)
|
||||||
.MaximumLength(500)
|
.MaximumLength(500)
|
||||||
.WithMessage("支付凭证 URL 不能超过 500 个字符")
|
.WithMessage("支付凭证 URL 不能超过 500 个字符")
|
||||||
.When(x => !string.IsNullOrWhiteSpace(x.ProofUrl));
|
.When(x => !string.IsNullOrWhiteSpace(x.ProofUrl));
|
||||||
|
|
||||||
// 6. (空行后) 备注(可选)
|
// 6. 备注(可选)
|
||||||
RuleFor(x => x.Notes)
|
RuleFor(x => x.Notes)
|
||||||
.MaximumLength(500)
|
.MaximumLength(500)
|
||||||
.WithMessage("备注不能超过 500 个字符")
|
.WithMessage("备注不能超过 500 个字符")
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ public sealed class UpdateBillingStatusCommandValidator : AbstractValidator<Upda
|
|||||||
.GreaterThan(0)
|
.GreaterThan(0)
|
||||||
.WithMessage("账单 ID 必须大于 0");
|
.WithMessage("账单 ID 必须大于 0");
|
||||||
|
|
||||||
// 2. (空行后) 状态枚举校验
|
// 2. 状态枚举校验
|
||||||
RuleFor(x => x.NewStatus)
|
RuleFor(x => x.NewStatus)
|
||||||
.IsInEnum()
|
.IsInEnum()
|
||||||
.WithMessage("新状态无效");
|
.WithMessage("新状态无效");
|
||||||
|
|
||||||
// 3. (空行后) 备注长度限制(可选)
|
// 3. 备注长度限制(可选)
|
||||||
RuleFor(x => x.Notes)
|
RuleFor(x => x.Notes)
|
||||||
.MaximumLength(500)
|
.MaximumLength(500)
|
||||||
.WithMessage("备注不能超过 500 个字符")
|
.WithMessage("备注不能超过 500 个字符")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public sealed class PurchaseQuotaPackageCommandHandler(
|
|||||||
quotaUsage.LimitValue += quotaPackage.QuotaValue;
|
quotaUsage.LimitValue += quotaPackage.QuotaValue;
|
||||||
await quotaPackageRepository.UpdateUsageAsync(quotaUsage, cancellationToken);
|
await quotaPackageRepository.UpdateUsageAsync(quotaUsage, cancellationToken);
|
||||||
|
|
||||||
// 4.1 (空行后) 记录配额变更历史(购买配额包视为“剩余增加”)
|
// 4.1 记录配额变更历史(购买配额包视为“剩余增加”)
|
||||||
await quotaUsageHistoryRepository.AddAsync(new TenantQuotaUsageHistory
|
await quotaUsageHistoryRepository.AddAsync(new TenantQuotaUsageHistory
|
||||||
{
|
{
|
||||||
TenantId = request.TenantId,
|
TenantId = request.TenantId,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ public sealed class ProcessAutoRenewalCommandHandler(
|
|||||||
// 1. 以年月差作为周期(月),兼容“按月续费”模型
|
// 1. 以年月差作为周期(月),兼容“按月续费”模型
|
||||||
var months = (effectiveTo.Year - effectiveFrom.Year) * 12 + effectiveTo.Month - effectiveFrom.Month;
|
var months = (effectiveTo.Year - effectiveFrom.Year) * 12 + effectiveTo.Month - effectiveFrom.Month;
|
||||||
|
|
||||||
// 2. (空行后) 对不足 1 个月的情况兜底为 1
|
// 2. 对不足 1 个月的情况兜底为 1
|
||||||
return months <= 0 ? 1 : months;
|
return months <= 0 ? 1 : months;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ public sealed class ProcessAutoRenewalCommandHandler(
|
|||||||
return monthlyPrice * durationMonths;
|
return monthlyPrice * durationMonths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 按年 + 月组合计算金额
|
// 2. 按年 + 月组合计算金额
|
||||||
var years = durationMonths / 12;
|
var years = durationMonths / 12;
|
||||||
var remainingMonths = durationMonths % 12;
|
var remainingMonths = durationMonths % 12;
|
||||||
return yearlyPrice.Value * years + monthlyPrice * remainingMonths;
|
return yearlyPrice.Value * years + monthlyPrice * remainingMonths;
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ public sealed class ProcessSubscriptionExpiryCommandHandler(
|
|||||||
await subscriptionRepository.UpdateAsync(subscription, cancellationToken);
|
await subscriptionRepository.UpdateAsync(subscription, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 宽限期到期自动暂停
|
// 3. 宽限期到期自动暂停
|
||||||
foreach (var subscription in gracePeriodExpired)
|
foreach (var subscription in gracePeriodExpired)
|
||||||
{
|
{
|
||||||
subscription.Status = SubscriptionStatus.Suspended;
|
subscription.Status = SubscriptionStatus.Suspended;
|
||||||
await subscriptionRepository.UpdateAsync(subscription, cancellationToken);
|
await subscriptionRepository.UpdateAsync(subscription, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 保存变更
|
// 4. 保存变更
|
||||||
var totalChanged = expiredActive.Count + gracePeriodExpired.Count;
|
var totalChanged = expiredActive.Count + gracePeriodExpired.Count;
|
||||||
if (totalChanged > 0)
|
if (totalChanged > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public sealed class CheckTenantQuotaCommandHandler(
|
|||||||
ResetCycle = ResolveResetCycle(request.QuotaType)
|
ResetCycle = ResolveResetCycle(request.QuotaType)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4.1 (空行后) 记录是否为首次初始化(用于落库历史)
|
// 4.1 记录是否为首次初始化(用于落库历史)
|
||||||
var isNewUsage = usage.Id == 0;
|
var isNewUsage = usage.Id == 0;
|
||||||
|
|
||||||
var usedAfter = usage.UsedValue + request.Delta;
|
var usedAfter = usage.UsedValue + request.Delta;
|
||||||
@@ -79,7 +79,7 @@ public sealed class CheckTenantQuotaCommandHandler(
|
|||||||
usage.UsedValue = usedAfter;
|
usage.UsedValue = usedAfter;
|
||||||
usage.ResetCycle ??= ResolveResetCycle(request.QuotaType);
|
usage.ResetCycle ??= ResolveResetCycle(request.QuotaType);
|
||||||
|
|
||||||
// 5.1 (空行后) 落库历史(初始化 + 本次消耗)
|
// 5.1 落库历史(初始化 + 本次消耗)
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
if (isNewUsage)
|
if (isNewUsage)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ public sealed class ClaimTenantReviewCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {existingClaim.ClaimedByName} 领取");
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {existingClaim.ClaimedByName} 领取");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 获取当前用户显示名(用于展示快照)
|
// 3. 获取当前用户显示名(用于展示快照)
|
||||||
var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
|
var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: profile.DisplayName;
|
: profile.DisplayName;
|
||||||
|
|
||||||
// 4. (空行后) 构造领取记录与审计日志
|
// 4. 构造领取记录与审计日志
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var claim = new TenantReviewClaim
|
var claim = new TenantReviewClaim
|
||||||
{
|
{
|
||||||
@@ -68,7 +68,7 @@ public sealed class ClaimTenantReviewCommandHandler(
|
|||||||
CurrentStatus = tenant.Status
|
CurrentStatus = tenant.Status
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. (空行后) 写入领取记录(处理并发领取冲突)
|
// 5. 写入领取记录(处理并发领取冲突)
|
||||||
var success = await tenantRepository.TryAddReviewClaimAsync(claim, auditLog, cancellationToken);
|
var success = await tenantRepository.TryAddReviewClaimAsync(claim, auditLog, cancellationToken);
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,7 @@ public sealed class ClaimTenantReviewCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {current.ClaimedByName} 领取");
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {current.ClaimedByName} 领取");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 返回领取结果
|
// 6. 返回领取结果
|
||||||
return claim.ToDto();
|
return claim.ToDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可生成重置链接");
|
throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可生成重置链接");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 校验租户存在且存在主管理员
|
// 2. 校验租户存在且存在主管理员
|
||||||
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
|
||||||
// 2.1 (空行后) 若缺少主管理员则自动回填(兼容历史数据)
|
// 2.1 若缺少主管理员则自动回填(兼容历史数据)
|
||||||
if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
|
if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
|
||||||
{
|
{
|
||||||
var originalContextForFix = tenantContextAccessor.Current;
|
var originalContextForFix = tenantContextAccessor.Current;
|
||||||
@@ -65,10 +65,10 @@ public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 签发一次性重置令牌(默认 24 小时有效)
|
// 3. 签发一次性重置令牌(默认 24 小时有效)
|
||||||
var token = await tokenStore.IssueAsync(tenant.PrimaryOwnerUserId.Value, DateTime.UtcNow.AddHours(24), cancellationToken);
|
var token = await tokenStore.IssueAsync(tenant.PrimaryOwnerUserId.Value, DateTime.UtcNow.AddHours(24), cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 写入审计日志
|
// 4. 写入审计日志
|
||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
@@ -88,7 +88,7 @@ public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
|
|||||||
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
|
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 5. (空行后) 返回令牌
|
// 5. 返回令牌
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {normalizedCode} 已存在");
|
throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {normalizedCode} 已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 校验联系人手机号唯一性(仅当填写时)
|
// 3. 校验联系人手机号唯一性(仅当填写时)
|
||||||
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
|
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
|
||||||
{
|
{
|
||||||
var normalizedPhone = request.ContactPhone.Trim();
|
var normalizedPhone = request.ContactPhone.Trim();
|
||||||
@@ -59,14 +59,14 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 校验管理员账号唯一性
|
// 4. 校验管理员账号唯一性
|
||||||
var normalizedAccount = request.AdminAccount.Trim();
|
var normalizedAccount = request.AdminAccount.Trim();
|
||||||
if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken))
|
if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken))
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在");
|
throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 校验套餐存在且可用
|
// 5. 校验套餐存在且可用
|
||||||
var package = await tenantPackageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken)
|
var package = await tenantPackageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
|
||||||
if (!package.IsActive)
|
if (!package.IsActive)
|
||||||
@@ -74,12 +74,12 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "套餐未启用,无法绑定订阅");
|
throw new BusinessException(ErrorCodes.BadRequest, "套餐未启用,无法绑定订阅");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 计算订阅生效与到期时间(UTC)
|
// 6. 计算订阅生效与到期时间(UTC)
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var subscriptionEffectiveFrom = request.SubscriptionEffectiveFrom ?? now;
|
var subscriptionEffectiveFrom = request.SubscriptionEffectiveFrom ?? now;
|
||||||
var subscriptionEffectiveTo = subscriptionEffectiveFrom.AddMonths(request.DurationMonths);
|
var subscriptionEffectiveTo = subscriptionEffectiveFrom.AddMonths(request.DurationMonths);
|
||||||
|
|
||||||
// 7. (空行后) 构建租户与订阅
|
// 7. 构建租户与订阅
|
||||||
var tenantId = idGenerator.NextId();
|
var tenantId = idGenerator.NextId();
|
||||||
var tenant = new Tenant
|
var tenant = new Tenant
|
||||||
{
|
{
|
||||||
@@ -108,7 +108,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
Remarks = request.Remarks
|
Remarks = request.Remarks
|
||||||
};
|
};
|
||||||
|
|
||||||
// 8. (空行后) 构建订阅实体
|
// 8. 构建订阅实体
|
||||||
var subscription = new TenantSubscription
|
var subscription = new TenantSubscription
|
||||||
{
|
{
|
||||||
Id = idGenerator.NextId(),
|
Id = idGenerator.NextId(),
|
||||||
@@ -123,7 +123,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
Notes = request.SubscriptionNotes
|
Notes = request.SubscriptionNotes
|
||||||
};
|
};
|
||||||
|
|
||||||
// 9. (空行后) 构建认证资料(默认直接通过)
|
// 9. 构建认证资料(默认直接通过)
|
||||||
var actorName = currentUserAccessor.IsAuthenticated
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: "system";
|
: "system";
|
||||||
@@ -150,7 +150,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
ReviewRemarks = request.ReviewRemarks
|
ReviewRemarks = request.ReviewRemarks
|
||||||
};
|
};
|
||||||
|
|
||||||
// 10. (空行后) 写入审计日志与订阅历史
|
// 10. 写入审计日志与订阅历史
|
||||||
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
@@ -201,13 +201,13 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
Notes = request.SubscriptionNotes
|
Notes = request.SubscriptionNotes
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
// 11. (空行后) 持久化租户、订阅与认证资料
|
// 11. 持久化租户、订阅与认证资料
|
||||||
await tenantRepository.AddTenantAsync(tenant, cancellationToken);
|
await tenantRepository.AddTenantAsync(tenant, cancellationToken);
|
||||||
await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
|
await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
|
||||||
await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
|
await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 12. (空行后) 临时切换租户上下文,保证身份与权限写入正确
|
// 12. 临时切换租户上下文,保证身份与权限写入正确
|
||||||
var previousContext = tenantContextAccessor.Current;
|
var previousContext = tenantContextAccessor.Current;
|
||||||
tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create");
|
tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create");
|
||||||
try
|
try
|
||||||
@@ -228,7 +228,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
await identityUserRepository.AddAsync(adminUser, cancellationToken);
|
await identityUserRepository.AddAsync(adminUser, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 14. (空行后) 初始化租户管理员角色模板并绑定角色
|
// 14. 初始化租户管理员角色模板并绑定角色
|
||||||
await mediator.Send(new InitializeRoleTemplatesCommand
|
await mediator.Send(new InitializeRoleTemplatesCommand
|
||||||
{
|
{
|
||||||
TemplateCodes = new[] { "tenant-admin" }
|
TemplateCodes = new[] { "tenant-admin" }
|
||||||
@@ -244,7 +244,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 15. (空行后) 回写租户所有者账号
|
// 15. 回写租户所有者账号
|
||||||
tenant.PrimaryOwnerUserId = adminUser.Id;
|
tenant.PrimaryOwnerUserId = adminUser.Id;
|
||||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
@@ -255,7 +255,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
|||||||
tenantContextAccessor.Current = previousContext;
|
tenantContextAccessor.Current = previousContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 17. (空行后) 返回创建结果
|
// 17. 返回创建结果
|
||||||
logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code);
|
logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code);
|
||||||
|
|
||||||
return new TenantDetailDto
|
return new TenantDetailDto
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public sealed class ExtendTenantSubscriptionCommandHandler(
|
|||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: "system";
|
: "system";
|
||||||
|
|
||||||
// 3. (空行后) 创建续费订阅
|
// 3. 创建续费订阅
|
||||||
var subscription = new TenantSubscription
|
var subscription = new TenantSubscription
|
||||||
{
|
{
|
||||||
Id = idGenerator.NextId(),
|
Id = idGenerator.NextId(),
|
||||||
@@ -75,7 +75,7 @@ public sealed class ExtendTenantSubscriptionCommandHandler(
|
|||||||
Notes = request.Notes
|
Notes = request.Notes
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 若租户处于到期状态则恢复为正常(冻结状态需先解冻)
|
// 4. 若租户处于到期状态则恢复为正常(冻结状态需先解冻)
|
||||||
if (tenant.Status == TenantStatus.Expired)
|
if (tenant.Status == TenantStatus.Expired)
|
||||||
{
|
{
|
||||||
tenant.Status = TenantStatus.Active;
|
tenant.Status = TenantStatus.Active;
|
||||||
@@ -86,7 +86,7 @@ public sealed class ExtendTenantSubscriptionCommandHandler(
|
|||||||
|
|
||||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||||
|
|
||||||
// 5. (空行后) 记录审计
|
// 5. 记录审计
|
||||||
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
{
|
{
|
||||||
TenantId = tenant.Id,
|
TenantId = tenant.Id,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public sealed class ForceClaimTenantReviewCommandHandler(
|
|||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: profile.DisplayName;
|
: profile.DisplayName;
|
||||||
|
|
||||||
// 3. (空行后) 读取当前领取记录(可跟踪用于更新)
|
// 3. 读取当前领取记录(可跟踪用于更新)
|
||||||
var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken);
|
var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken);
|
||||||
if (claim == null)
|
if (claim == null)
|
||||||
{
|
{
|
||||||
@@ -75,13 +75,13 @@ public sealed class ForceClaimTenantReviewCommandHandler(
|
|||||||
return created.ToDto();
|
return created.ToDto();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 已由自己领取则直接返回
|
// 5. 已由自己领取则直接返回
|
||||||
if (claim.ClaimedBy == currentUserAccessor.UserId)
|
if (claim.ClaimedBy == currentUserAccessor.UserId)
|
||||||
{
|
{
|
||||||
return claim.ToDto();
|
return claim.ToDto();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 更新领取人并记录审计
|
// 6. 更新领取人并记录审计
|
||||||
var previousOwner = claim.ClaimedByName;
|
var previousOwner = claim.ClaimedByName;
|
||||||
claim.ClaimedBy = currentUserAccessor.UserId;
|
claim.ClaimedBy = currentUserAccessor.UserId;
|
||||||
claim.ClaimedByName = displayName;
|
claim.ClaimedByName = displayName;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public sealed class FreezeTenantCommandHandler(
|
|||||||
tenant.SuspendedAt = DateTime.UtcNow;
|
tenant.SuspendedAt = DateTime.UtcNow;
|
||||||
tenant.SuspensionReason = request.Reason;
|
tenant.SuspensionReason = request.Reason;
|
||||||
|
|
||||||
// 3. (空行后) 同步暂停订阅
|
// 3. 同步暂停订阅
|
||||||
if (subscription != null)
|
if (subscription != null)
|
||||||
{
|
{
|
||||||
subscription.Status = SubscriptionStatus.Suspended;
|
subscription.Status = SubscriptionStatus.Suspended;
|
||||||
@@ -53,7 +53,7 @@ public sealed class FreezeTenantCommandHandler(
|
|||||||
|
|
||||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 记录审计
|
// 4. 记录审计
|
||||||
var actorName = currentUserAccessor.IsAuthenticated
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: "system";
|
: "system";
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ public sealed class GetTenantByIdQueryHandler(
|
|||||||
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
||||||
var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
|
var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 查询当前套餐
|
// 3. 查询当前套餐
|
||||||
var package = subscription == null
|
var package = subscription == null
|
||||||
? null
|
? null
|
||||||
: await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken);
|
: await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 组装返回
|
// 4. 组装返回
|
||||||
return new TenantDetailDto
|
return new TenantDetailDto
|
||||||
{
|
{
|
||||||
Tenant = TenantMapping.ToDto(tenant, subscription, verification),
|
Tenant = TenantMapping.ToDto(tenant, subscription, verification),
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperEx
|
|||||||
var pageSize = request.PageSize <= 0 ? 20 : request.PageSize;
|
var pageSize = request.PageSize <= 0 ? 20 : request.PageSize;
|
||||||
var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
|
var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
|
||||||
|
|
||||||
// 2. (空行后) 以当前时间为准筛选“有效订阅”
|
// 2. 以当前时间为准筛选“有效订阅”
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var expiringDays = request.ExpiringWithinDays is > 0 ? request.ExpiringWithinDays : null;
|
var expiringDays = request.ExpiringWithinDays is > 0 ? request.ExpiringWithinDays : null;
|
||||||
var expiryEnd = expiringDays.HasValue ? now.AddDays(expiringDays.Value) : (DateTime?)null;
|
var expiryEnd = expiringDays.HasValue ? now.AddDays(expiringDays.Value) : (DateTime?)null;
|
||||||
var offset = (page - 1) * pageSize;
|
var offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
// 3. (空行后) 查询总数 + 列表
|
// 3. 查询总数 + 列表
|
||||||
return await dapperExecutor.QueryAsync(
|
return await dapperExecutor.QueryAsync(
|
||||||
DatabaseConstants.AppDataSource,
|
DatabaseConstants.AppDataSource,
|
||||||
DatabaseConnectionRole.Read,
|
DatabaseConnectionRole.Read,
|
||||||
@@ -48,7 +48,7 @@ public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperEx
|
|||||||
],
|
],
|
||||||
token);
|
token);
|
||||||
|
|
||||||
// 3.2 (空行后) 查询列表
|
// 3.2 查询列表
|
||||||
var listSql = BuildListSql(expiryEnd.HasValue);
|
var listSql = BuildListSql(expiryEnd.HasValue);
|
||||||
await using var listCommand = CreateCommand(
|
await using var listCommand = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
@@ -79,7 +79,7 @@ public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperEx
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.3 (空行后) 返回分页
|
// 3.3 返回分页
|
||||||
return new PagedResult<TenantPackageTenantDto>(items, page, pageSize, total);
|
return new PagedResult<TenantPackageTenantDto>(items, page, pageSize, total);
|
||||||
},
|
},
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// 2. (空行后) 构造 SQL(以当前时间为准统计“有效订阅/使用租户/到期分布”)
|
// 2. 构造 SQL(以当前时间为准统计“有效订阅/使用租户/到期分布”)
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var date7 = now.AddDays(7);
|
var date7 = now.AddDays(7);
|
||||||
var date15 = now.AddDays(15);
|
var date15 = now.AddDays(15);
|
||||||
var date30 = now.AddDays(30);
|
var date30 = now.AddDays(30);
|
||||||
var sql = BuildSql(ids, out var parameters, now, date7, date15, date30);
|
var sql = BuildSql(ids, out var parameters, now, date7, date15, date30);
|
||||||
|
|
||||||
// 3. (空行后) 查询统计结果
|
// 3. 查询统计结果
|
||||||
return await dapperExecutor.QueryAsync(
|
return await dapperExecutor.QueryAsync(
|
||||||
DatabaseConstants.AppDataSource,
|
DatabaseConstants.AppDataSource,
|
||||||
DatabaseConnectionRole.Read,
|
DatabaseConnectionRole.Read,
|
||||||
@@ -40,7 +40,7 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
|
|||||||
await using var reader = await command.ExecuteReaderAsync(token);
|
await using var reader = await command.ExecuteReaderAsync(token);
|
||||||
var list = new List<TenantPackageUsageDto>();
|
var list = new List<TenantPackageUsageDto>();
|
||||||
|
|
||||||
// 4. (空行后) 逐行读取
|
// 4. 逐行读取
|
||||||
while (await reader.ReadAsync(token))
|
while (await reader.ReadAsync(token))
|
||||||
{
|
{
|
||||||
list.Add(new TenantPackageUsageDto
|
list.Add(new TenantPackageUsageDto
|
||||||
@@ -94,7 +94,7 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
|
|||||||
("date30", date30)
|
("date30", date30)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. (空行后) 可选按套餐 ID 过滤
|
// 2. 可选按套餐 ID 过滤
|
||||||
if (ids is { Length: > 0 })
|
if (ids is { Length: > 0 })
|
||||||
{
|
{
|
||||||
builder.Append(" and \"TenantPackageId\" in (");
|
builder.Append(" and \"TenantPackageId\" in (");
|
||||||
@@ -113,7 +113,7 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
|
|||||||
builder.AppendLine(")");
|
builder.AppendLine(")");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 分组与回连套餐表
|
// 3. 分组与回连套餐表
|
||||||
builder.AppendLine("""
|
builder.AppendLine("""
|
||||||
group by "TenantPackageId"
|
group by "TenantPackageId"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
|
|||||||
_ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
_ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
|
||||||
// 2. (空行后) 规范化分页
|
// 2. 规范化分页
|
||||||
var page = request.Page <= 0 ? 1 : request.Page;
|
var page = request.Page <= 0 ? 1 : request.Page;
|
||||||
var pageSize = request.PageSize is <= 0 or > 100 ? 10 : request.PageSize;
|
var pageSize = request.PageSize is <= 0 or > 100 ? 10 : request.PageSize;
|
||||||
var offset = (page - 1) * pageSize;
|
var offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
// 3. (空行后) 查询总数 + 列表
|
// 3. 查询总数 + 列表
|
||||||
return await dapperExecutor.QueryAsync(
|
return await dapperExecutor.QueryAsync(
|
||||||
DatabaseConstants.AppDataSource,
|
DatabaseConstants.AppDataSource,
|
||||||
DatabaseConnectionRole.Read,
|
DatabaseConnectionRole.Read,
|
||||||
@@ -50,7 +50,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
|
|||||||
],
|
],
|
||||||
token);
|
token);
|
||||||
|
|
||||||
// 3.2 (空行后) 查询列表
|
// 3.2 查询列表
|
||||||
await using var listCommand = CreateCommand(
|
await using var listCommand = CreateCommand(
|
||||||
connection,
|
connection,
|
||||||
BuildListSql(),
|
BuildListSql(),
|
||||||
@@ -75,7 +75,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
|
|||||||
decimal? changeAmount = reader.IsDBNull(5) ? null : reader.GetDecimal(5);
|
decimal? changeAmount = reader.IsDBNull(5) ? null : reader.GetDecimal(5);
|
||||||
var changeReason = reader.IsDBNull(6) ? null : reader.GetString(6);
|
var changeReason = reader.IsDBNull(6) ? null : reader.GetString(6);
|
||||||
|
|
||||||
// 3.2.1 (空行后) 映射 DTO
|
// 3.2.1 映射 DTO
|
||||||
items.Add(new QuotaUsageHistoryDto
|
items.Add(new QuotaUsageHistoryDto
|
||||||
{
|
{
|
||||||
QuotaType = quotaType,
|
QuotaType = quotaType,
|
||||||
@@ -88,7 +88,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.3 (空行后) 返回分页
|
// 3.3 返回分页
|
||||||
return new PagedResult<QuotaUsageHistoryDto>(items, page, pageSize, total);
|
return new PagedResult<QuotaUsageHistoryDto>(items, page, pageSize, total);
|
||||||
},
|
},
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|||||||
@@ -38,17 +38,17 @@ public sealed class ImpersonateTenantCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可执行伪装登录");
|
throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可执行伪装登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 读取操作者信息(在平台租户上下文内)
|
// 2. 读取操作者信息(在平台租户上下文内)
|
||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
|
|
||||||
// 2. (空行后) 校验租户存在且存在主管理员
|
// 2. 校验租户存在且存在主管理员
|
||||||
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
|
||||||
// 2.1 (空行后) 若缺少主管理员则自动回填(兼容历史数据)
|
// 2.1 若缺少主管理员则自动回填(兼容历史数据)
|
||||||
if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
|
if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
|
||||||
{
|
{
|
||||||
var originalContextForFix = tenantContextAccessor.Current;
|
var originalContextForFix = tenantContextAccessor.Current;
|
||||||
@@ -72,7 +72,7 @@ public sealed class ImpersonateTenantCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户)
|
// 3. 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户)
|
||||||
var originalTenantContext = tenantContextAccessor.Current;
|
var originalTenantContext = tenantContextAccessor.Current;
|
||||||
tenantContextAccessor.Current = new TenantContext(tenant.Id, null, "admin:impersonate");
|
tenantContextAccessor.Current = new TenantContext(tenant.Id, null, "admin:impersonate");
|
||||||
try
|
try
|
||||||
@@ -81,7 +81,7 @@ public sealed class ImpersonateTenantCommandHandler(
|
|||||||
var targetProfile = await adminAuthService.GetProfileAsync(tenant.PrimaryOwnerUserId.Value, cancellationToken);
|
var targetProfile = await adminAuthService.GetProfileAsync(tenant.PrimaryOwnerUserId.Value, cancellationToken);
|
||||||
var token = await jwtTokenService.CreateTokensAsync(targetProfile, false, cancellationToken);
|
var token = await jwtTokenService.CreateTokensAsync(targetProfile, false, cancellationToken);
|
||||||
|
|
||||||
// 5. (空行后) 恢复租户上下文后写入审计日志
|
// 5. 恢复租户上下文后写入审计日志
|
||||||
tenantContextAccessor.Current = originalTenantContext;
|
tenantContextAccessor.Current = originalTenantContext;
|
||||||
var auditLog = new TenantAuditLog
|
var auditLog = new TenantAuditLog
|
||||||
{
|
{
|
||||||
@@ -97,12 +97,12 @@ public sealed class ImpersonateTenantCommandHandler(
|
|||||||
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
|
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 6. (空行后) 返回令牌
|
// 6. 返回令牌
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// 7. (空行后) 确保恢复租户上下文
|
// 7. 确保恢复租户上下文
|
||||||
tenantContextAccessor.Current = originalTenantContext;
|
tenantContextAccessor.Current = originalTenantContext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ public sealed class ReleaseTenantReviewClaimCommandHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 非领取人不允许释放(如需接管请使用强制接管)
|
// 3. 非领取人不允许释放(如需接管请使用强制接管)
|
||||||
if (claim.ClaimedBy != currentUserAccessor.UserId)
|
if (claim.ClaimedBy != currentUserAccessor.UserId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {claim.ClaimedByName} 领取");
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {claim.ClaimedByName} 领取");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 释放领取并记录审计
|
// 4. 释放领取并记录审计
|
||||||
var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
|
var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ public sealed class ReviewTenantCommandHandler(
|
|||||||
CurrentStatus = tenant.Status
|
CurrentStatus = tenant.Status
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
// 7. (空行后) 审核完成自动释放领取
|
// 7. 审核完成自动释放领取
|
||||||
reviewClaim.ReleasedAt = DateTime.UtcNow;
|
reviewClaim.ReleasedAt = DateTime.UtcNow;
|
||||||
await tenantRepository.UpdateReviewClaimAsync(reviewClaim, cancellationToken);
|
await tenantRepository.UpdateReviewClaimAsync(reviewClaim, cancellationToken);
|
||||||
await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog
|
await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog
|
||||||
|
|||||||
@@ -27,24 +27,24 @@ public sealed class SearchTenantsQueryHandler(ITenantRepository tenantRepository
|
|||||||
request.PageSize,
|
request.PageSize,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 无数据直接返回
|
// 2. 无数据直接返回
|
||||||
if (tenants.Count == 0)
|
if (tenants.Count == 0)
|
||||||
{
|
{
|
||||||
return new PagedResult<TenantDto>([], request.Page, request.PageSize, total);
|
return new PagedResult<TenantDto>([], request.Page, request.PageSize, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 批量查询订阅与实名资料(避免 N+1)
|
// 3. 批量查询订阅与实名资料(避免 N+1)
|
||||||
var tenantIds = tenants.Select(x => x.Id).ToArray();
|
var tenantIds = tenants.Select(x => x.Id).ToArray();
|
||||||
var subscriptions = await tenantRepository.GetSubscriptionsAsync(tenantIds, cancellationToken);
|
var subscriptions = await tenantRepository.GetSubscriptionsAsync(tenantIds, cancellationToken);
|
||||||
var verifications = await tenantRepository.GetVerificationProfilesAsync(tenantIds, cancellationToken);
|
var verifications = await tenantRepository.GetVerificationProfilesAsync(tenantIds, cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 构建订阅与实名资料映射
|
// 4. 构建订阅与实名资料映射
|
||||||
var subscriptionByTenantId = subscriptions
|
var subscriptionByTenantId = subscriptions
|
||||||
.GroupBy(x => x.TenantId)
|
.GroupBy(x => x.TenantId)
|
||||||
.ToDictionary(x => x.Key, x => x.FirstOrDefault());
|
.ToDictionary(x => x.Key, x => x.FirstOrDefault());
|
||||||
var verificationByTenantId = verifications.ToDictionary(x => x.TenantId);
|
var verificationByTenantId = verifications.ToDictionary(x => x.TenantId);
|
||||||
|
|
||||||
// 5. (空行后) 映射 DTO(带订阅与认证)
|
// 5. 映射 DTO(带订阅与认证)
|
||||||
var result = new List<TenantDto>(tenants.Count);
|
var result = new List<TenantDto>(tenants.Count);
|
||||||
foreach (var tenant in tenants)
|
foreach (var tenant in tenants)
|
||||||
{
|
{
|
||||||
@@ -53,7 +53,7 @@ public sealed class SearchTenantsQueryHandler(ITenantRepository tenantRepository
|
|||||||
result.Add(TenantMapping.ToDto(tenant, subscription, verification));
|
result.Add(TenantMapping.ToDto(tenant, subscription, verification));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 返回分页结果
|
// 6. 返回分页结果
|
||||||
return new PagedResult<TenantDto>(result, request.Page, request.PageSize, total);
|
return new PagedResult<TenantDto>(result, request.Page, request.PageSize, total);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ public sealed class SelfRegisterTenantCommandHandler(
|
|||||||
await identityUserRepository.AddAsync(adminUser, cancellationToken);
|
await identityUserRepository.AddAsync(adminUser, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 7.1 (空行后) 回填主管理员标识,确保后续伪装登录/重置管理员等能力可用
|
// 7.1 回填主管理员标识,确保后续伪装登录/重置管理员等能力可用
|
||||||
tenant.PrimaryOwnerUserId = adminUser.Id;
|
tenant.PrimaryOwnerUserId = adminUser.Id;
|
||||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public sealed class UnfreezeTenantCommandHandler(
|
|||||||
tenant.SuspendedAt = null;
|
tenant.SuspendedAt = null;
|
||||||
tenant.SuspensionReason = null;
|
tenant.SuspensionReason = null;
|
||||||
|
|
||||||
// 3. (空行后) 同步订阅状态
|
// 3. 同步订阅状态
|
||||||
if (subscription != null)
|
if (subscription != null)
|
||||||
{
|
{
|
||||||
subscription.Status = isExpired ? SubscriptionStatus.GracePeriod : SubscriptionStatus.Active;
|
subscription.Status = isExpired ? SubscriptionStatus.GracePeriod : SubscriptionStatus.Active;
|
||||||
@@ -51,7 +51,7 @@ public sealed class UnfreezeTenantCommandHandler(
|
|||||||
|
|
||||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 记录审计
|
// 4. 记录审计
|
||||||
var actorName = currentUserAccessor.IsAuthenticated
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
? $"user:{currentUserAccessor.UserId}"
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
: "system";
|
: "system";
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ public sealed class UpdateTenantCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "租户名称不能为空");
|
throw new BusinessException(ErrorCodes.BadRequest, "租户名称不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 查询租户
|
// 2. 查询租户
|
||||||
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
|
||||||
// 3. (空行后) 校验租户名称唯一性(排除自身)
|
// 3. 校验租户名称唯一性(排除自身)
|
||||||
var normalizedName = request.Name.Trim();
|
var normalizedName = request.Name.Trim();
|
||||||
if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: request.TenantId, cancellationToken))
|
if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: request.TenantId, cancellationToken))
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Conflict, $"租户名称 {normalizedName} 已存在");
|
throw new BusinessException(ErrorCodes.Conflict, $"租户名称 {normalizedName} 已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 校验联系人手机号唯一性(仅当填写时)
|
// 4. 校验联系人手机号唯一性(仅当填写时)
|
||||||
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
|
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
|
||||||
{
|
{
|
||||||
var normalizedPhone = request.ContactPhone.Trim();
|
var normalizedPhone = request.ContactPhone.Trim();
|
||||||
@@ -51,7 +51,7 @@ public sealed class UpdateTenantCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 更新基础信息(禁止修改 Code)
|
// 5. 更新基础信息(禁止修改 Code)
|
||||||
tenant.Name = normalizedName;
|
tenant.Name = normalizedName;
|
||||||
tenant.ShortName = string.IsNullOrWhiteSpace(request.ShortName) ? null : request.ShortName.Trim();
|
tenant.ShortName = string.IsNullOrWhiteSpace(request.ShortName) ? null : request.ShortName.Trim();
|
||||||
tenant.Industry = string.IsNullOrWhiteSpace(request.Industry) ? null : request.Industry.Trim();
|
tenant.Industry = string.IsNullOrWhiteSpace(request.Industry) ? null : request.Industry.Trim();
|
||||||
@@ -59,11 +59,11 @@ public sealed class UpdateTenantCommandHandler(
|
|||||||
tenant.ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim();
|
tenant.ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim();
|
||||||
tenant.ContactEmail = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim();
|
tenant.ContactEmail = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim();
|
||||||
|
|
||||||
// 6. (空行后) 持久化更新
|
// 6. 持久化更新
|
||||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 7. (空行后) 记录日志
|
// 7. 记录日志
|
||||||
logger.LogInformation("已更新租户基础信息 {TenantId}", tenant.Id);
|
logger.LogInformation("已更新租户基础信息 {TenantId}", tenant.Id);
|
||||||
|
|
||||||
return Unit.Value;
|
return Unit.Value;
|
||||||
|
|||||||
@@ -45,18 +45,18 @@ public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository p
|
|||||||
package.IsPublicVisible = request.IsPublicVisible;
|
package.IsPublicVisible = request.IsPublicVisible;
|
||||||
package.IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase;
|
package.IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase;
|
||||||
|
|
||||||
// 3. (空行后) 更新发布状态(若未传则保持不变,避免默认值覆盖)
|
// 3. 更新发布状态(若未传则保持不变,避免默认值覆盖)
|
||||||
if (request.PublishStatus.HasValue)
|
if (request.PublishStatus.HasValue)
|
||||||
{
|
{
|
||||||
package.PublishStatus = request.PublishStatus.Value;
|
package.PublishStatus = request.PublishStatus.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 更新展示配置(推荐与标签)
|
// 4. 更新展示配置(推荐与标签)
|
||||||
package.IsRecommended = request.IsRecommended;
|
package.IsRecommended = request.IsRecommended;
|
||||||
package.Tags = request.Tags ?? [];
|
package.Tags = request.Tags ?? [];
|
||||||
package.SortOrder = request.SortOrder;
|
package.SortOrder = request.SortOrder;
|
||||||
|
|
||||||
// 5. (空行后) 持久化并返回
|
// 5. 持久化并返回
|
||||||
await packageRepository.UpdateAsync(package, cancellationToken);
|
await packageRepository.UpdateAsync(package, cancellationToken);
|
||||||
await packageRepository.SaveChangesAsync(cancellationToken);
|
await packageRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryValidator : AbstractValidator
|
|||||||
RuleFor(x => x.Page).GreaterThanOrEqualTo(1);
|
RuleFor(x => x.Page).GreaterThanOrEqualTo(1);
|
||||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
|
RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
|
||||||
|
|
||||||
// (空行后) 时间范围校验
|
// 时间范围校验
|
||||||
When(x => x.StartDate.HasValue && x.EndDate.HasValue, () =>
|
When(x => x.StartDate.HasValue && x.EndDate.HasValue, () =>
|
||||||
{
|
{
|
||||||
RuleFor(x => x.EndDate!.Value).GreaterThanOrEqualTo(x => x.StartDate!.Value);
|
RuleFor(x => x.EndDate!.Value).GreaterThanOrEqualTo(x => x.StartDate!.Value);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户批量操作用户");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户批量操作用户");
|
||||||
@@ -47,7 +47,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户");
|
throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 解析用户 ID 列表
|
// 3. 解析用户 ID 列表
|
||||||
var tenantId = request.TenantId ?? currentTenantId;
|
var tenantId = request.TenantId ?? currentTenantId;
|
||||||
var userIds = ParseIds(request.UserIds, "用户");
|
var userIds = ParseIds(request.UserIds, "用户");
|
||||||
if (userIds.Length == 0)
|
if (userIds.Length == 0)
|
||||||
@@ -61,12 +61,12 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 查询目标用户集合
|
// 4. 查询目标用户集合
|
||||||
var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore;
|
var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore;
|
||||||
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken);
|
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken);
|
||||||
var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer<long>.Default);
|
var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer<long>.Default);
|
||||||
|
|
||||||
// 5. (空行后) 预计算租户管理员约束
|
// 5. 预计算租户管理员约束
|
||||||
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
|
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
|
||||||
var tenantAdminUserIds = tenantAdminRole == null
|
var tenantAdminUserIds = tenantAdminRole == null
|
||||||
? Array.Empty<long>()
|
? Array.Empty<long>()
|
||||||
@@ -88,7 +88,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
}, isSuperAdmin, cancellationToken)).Total;
|
}, isSuperAdmin, cancellationToken)).Total;
|
||||||
var remainingActiveAdmins = activeAdminCount;
|
var remainingActiveAdmins = activeAdminCount;
|
||||||
|
|
||||||
// 6. (空行后) 执行批量操作
|
// 6. 执行批量操作
|
||||||
var failures = new List<BatchIdentityUserFailureItem>();
|
var failures = new List<BatchIdentityUserFailureItem>();
|
||||||
var successCount = 0;
|
var successCount = 0;
|
||||||
var exportItems = new List<UserListItemDto>();
|
var exportItems = new List<UserListItemDto>();
|
||||||
@@ -185,7 +185,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 6.1 (空行后) 处理导出数据
|
// 6.1 处理导出数据
|
||||||
if (request.Operation == IdentityUserBatchOperation.Export)
|
if (request.Operation == IdentityUserBatchOperation.Export)
|
||||||
{
|
{
|
||||||
var roleCodesLookup = await ResolveRoleCodesAsync(users, userRoleRepository, roleRepository, cancellationToken);
|
var roleCodesLookup = await ResolveRoleCodesAsync(users, userRoleRepository, roleRepository, cancellationToken);
|
||||||
@@ -208,7 +208,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. (空行后) 构建操作日志消息
|
// 7. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -229,7 +229,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
Success = failures.Count == 0
|
Success = failures.Count == 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// 8. (空行后) 写入 Outbox 并保存变更
|
// 8. 写入 Outbox 并保存变更
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
return Array.Empty<long>();
|
return Array.Empty<long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 解析并去重
|
// 2. 解析并去重
|
||||||
var ids = new List<long>(values.Length);
|
var ids = new List<long>(values.Length);
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
@@ -262,7 +262,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
ids.Add(id);
|
ids.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 返回去重结果
|
// 3. 返回去重结果
|
||||||
return ids.Distinct().ToArray();
|
return ids.Distinct().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
// 1. 预分配字典容量
|
// 1. 预分配字典容量
|
||||||
var result = new Dictionary<long, string[]>(users.Count);
|
var result = new Dictionary<long, string[]>(users.Count);
|
||||||
|
|
||||||
// 2. (空行后) 按租户分组,降低角色查询次数
|
// 2. 按租户分组,降低角色查询次数
|
||||||
foreach (var group in users.GroupBy(user => user.TenantId))
|
foreach (var group in users.GroupBy(user => user.TenantId))
|
||||||
{
|
{
|
||||||
var tenantId = group.Key;
|
var tenantId = group.Key;
|
||||||
@@ -285,21 +285,21 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询用户角色映射
|
// 3. 查询用户角色映射
|
||||||
var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
||||||
if (relations.Count == 0)
|
if (relations.Count == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 查询角色并构建映射
|
// 4. 查询角色并构建映射
|
||||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||||
var roles = roleIds.Length == 0
|
var roles = roleIds.Length == 0
|
||||||
? Array.Empty<Role>()
|
? Array.Empty<Role>()
|
||||||
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||||
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
||||||
|
|
||||||
// 5. (空行后) 组装用户角色编码列表
|
// 5. 组装用户角色编码列表
|
||||||
foreach (var relationGroup in relations.GroupBy(x => x.UserId))
|
foreach (var relationGroup in relations.GroupBy(x => x.UserId))
|
||||||
{
|
{
|
||||||
var codes = relationGroup
|
var codes = relationGroup
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户修改用户状态");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户修改用户状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询用户实体
|
// 3. 查询用户实体
|
||||||
var user = isSuperAdmin
|
var user = isSuperAdmin
|
||||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||||
@@ -53,13 +53,13 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 校验租户管理员保留规则
|
// 4. 校验租户管理员保留规则
|
||||||
if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active)
|
if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active)
|
||||||
{
|
{
|
||||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
|
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 更新状态
|
// 5. 更新状态
|
||||||
var previousStatus = user.Status;
|
var previousStatus = user.Status;
|
||||||
switch (request.Status)
|
switch (request.Status)
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "无效的用户状态");
|
throw new BusinessException(ErrorCodes.BadRequest, "无效的用户状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 构建操作日志消息
|
// 6. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -107,7 +107,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
|||||||
Success = true
|
Success = true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 7. (空行后) 写入 Outbox 并保存变更
|
// 7. 写入 Outbox 并保存变更
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -123,14 +123,14 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 判断用户是否为租户管理员
|
// 2. 判断用户是否为租户管理员
|
||||||
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||||
if (!relations.Any(x => x.RoleId == tenantAdminRole.Id))
|
if (!relations.Any(x => x.RoleId == tenantAdminRole.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 统计活跃管理员数量
|
// 3. 统计活跃管理员数量
|
||||||
var filter = new IdentityUserSearchFilter
|
var filter = new IdentityUserSearchFilter
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 规范化输入并准备校验
|
// 3. 规范化输入并准备校验
|
||||||
var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId;
|
var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId;
|
||||||
var account = request.Account.Trim();
|
var account = request.Account.Trim();
|
||||||
var displayName = request.DisplayName.Trim();
|
var displayName = request.DisplayName.Trim();
|
||||||
@@ -55,7 +55,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
|
var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
|
||||||
var roleIds = ParseIds(request.RoleIds, "角色");
|
var roleIds = ParseIds(request.RoleIds, "角色");
|
||||||
|
|
||||||
// 4. (空行后) 唯一性校验
|
// 4. 唯一性校验
|
||||||
if (await identityUserRepository.ExistsByAccountAsync(tenantId, account, null, cancellationToken))
|
if (await identityUserRepository.ExistsByAccountAsync(tenantId, account, null, cancellationToken))
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Conflict, "账号已存在");
|
throw new BusinessException(ErrorCodes.Conflict, "账号已存在");
|
||||||
@@ -73,7 +73,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Conflict, "邮箱已存在");
|
throw new BusinessException(ErrorCodes.Conflict, "邮箱已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 校验角色合法性
|
// 5. 校验角色合法性
|
||||||
if (roleIds.Length > 0)
|
if (roleIds.Length > 0)
|
||||||
{
|
{
|
||||||
var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||||
@@ -83,7 +83,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 创建用户实体
|
// 6. 创建用户实体
|
||||||
var user = new IdentityUser
|
var user = new IdentityUser
|
||||||
{
|
{
|
||||||
Id = idGenerator.NextId(),
|
Id = idGenerator.NextId(),
|
||||||
@@ -102,7 +102,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
};
|
};
|
||||||
user.PasswordHash = passwordHasher.HashPassword(user, request.Password);
|
user.PasswordHash = passwordHasher.HashPassword(user, request.Password);
|
||||||
|
|
||||||
// 7. (空行后) 构建操作日志消息
|
// 7. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -131,18 +131,18 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
Success = true
|
Success = true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 8. (空行后) 持久化用户并写入 Outbox
|
// 8. 持久化用户并写入 Outbox
|
||||||
await identityUserRepository.AddAsync(user, cancellationToken);
|
await identityUserRepository.AddAsync(user, cancellationToken);
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 9. (空行后) 绑定角色
|
// 9. 绑定角色
|
||||||
if (roleIds.Length > 0)
|
if (roleIds.Length > 0)
|
||||||
{
|
{
|
||||||
await userRoleRepository.ReplaceUserRolesAsync(tenantId, user.Id, roleIds, cancellationToken);
|
await userRoleRepository.ReplaceUserRolesAsync(tenantId, user.Id, roleIds, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. (空行后) 返回用户详情
|
// 10. 返回用户详情
|
||||||
var detail = await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken);
|
var detail = await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken);
|
||||||
return detail ?? new UserDetailDto
|
return detail ?? new UserDetailDto
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
return Array.Empty<long>();
|
return Array.Empty<long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 解析并去重
|
// 2. 解析并去重
|
||||||
var ids = new List<long>(values.Length);
|
var ids = new List<long>(values.Length);
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
@@ -185,7 +185,7 @@ public sealed class CreateIdentityUserCommandHandler(
|
|||||||
ids.Add(id);
|
ids.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 返回去重结果
|
// 3. 返回去重结果
|
||||||
return ids.Distinct().ToArray();
|
return ids.Distinct().ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ public sealed class DeleteIdentityUserCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户删除用户");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户删除用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询用户实体
|
// 3. 查询用户实体
|
||||||
var user = isSuperAdmin
|
var user = isSuperAdmin
|
||||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||||
@@ -53,13 +53,13 @@ public sealed class DeleteIdentityUserCommandHandler(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 校验租户管理员保留规则
|
// 4. 校验租户管理员保留规则
|
||||||
if (user.Status == IdentityUserStatus.Active)
|
if (user.Status == IdentityUserStatus.Active)
|
||||||
{
|
{
|
||||||
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
|
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 构建操作日志消息
|
// 5. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -80,7 +80,7 @@ public sealed class DeleteIdentityUserCommandHandler(
|
|||||||
Success = true
|
Success = true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. (空行后) 软删除用户并写入 Outbox
|
// 6. 软删除用户并写入 Outbox
|
||||||
await identityUserRepository.RemoveAsync(user, cancellationToken);
|
await identityUserRepository.RemoveAsync(user, cancellationToken);
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
@@ -97,14 +97,14 @@ public sealed class DeleteIdentityUserCommandHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 判断用户是否为租户管理员
|
// 2. 判断用户是否为租户管理员
|
||||||
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
|
||||||
if (!relations.Any(x => x.RoleId == tenantAdminRole.Id))
|
if (!relations.Any(x => x.RoleId == tenantAdminRole.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 统计活跃管理员数量
|
// 3. 统计活跃管理员数量
|
||||||
var filter = new IdentityUserSearchFilter
|
var filter = new IdentityUserSearchFilter
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 查询用户实体
|
// 2. 查询用户实体
|
||||||
IdentityUser? user;
|
IdentityUser? user;
|
||||||
if (request.IncludeDeleted)
|
if (request.IncludeDeleted)
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 加载角色与权限
|
// 3. 加载角色与权限
|
||||||
var roleRelations = await userRoleRepository.GetByUserIdAsync(user.TenantId, user.Id, cancellationToken);
|
var roleRelations = await userRoleRepository.GetByUserIdAsync(user.TenantId, user.Id, cancellationToken);
|
||||||
var roleIds = roleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
var roleIds = roleRelations.Select(x => x.RoleId).Distinct().ToArray();
|
||||||
var roles = roleIds.Length == 0
|
var roles = roleIds.Length == 0
|
||||||
@@ -79,7 +79,7 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
|||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// 4. (空行后) 组装详情 DTO
|
// 4. 组装详情 DTO
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return new UserDetailDto
|
return new UserDetailDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ public sealed class ResetAdminPasswordByTokenCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.BadRequest, "新密码长度需为 6~32 位");
|
throw new BusinessException(ErrorCodes.BadRequest, "新密码长度需为 6~32 位");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 校验并消费令牌
|
// 2. 校验并消费令牌
|
||||||
var userId = await tokenStore.ConsumeAsync(token, cancellationToken);
|
var userId = await tokenStore.ConsumeAsync(token, cancellationToken);
|
||||||
if (!userId.HasValue || userId.Value == 0)
|
if (!userId.HasValue || userId.Value == 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.BadRequest, "重置链接无效或已过期");
|
throw new BusinessException(ErrorCodes.BadRequest, "重置链接无效或已过期");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 获取用户(可更新,忽略租户过滤器)并写入新密码哈希
|
// 3. 获取用户(可更新,忽略租户过滤器)并写入新密码哈希
|
||||||
var user = await userRepository.GetForUpdateIgnoringTenantAsync(userId.Value, cancellationToken)
|
var user = await userRepository.GetForUpdateIgnoringTenantAsync(userId.Value, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询用户实体
|
// 3. 查询用户实体
|
||||||
var user = isSuperAdmin
|
var user = isSuperAdmin
|
||||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||||
@@ -53,11 +53,11 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 签发重置令牌(1 小时有效)
|
// 4. 签发重置令牌(1 小时有效)
|
||||||
var expiresAt = DateTime.UtcNow.AddHours(1);
|
var expiresAt = DateTime.UtcNow.AddHours(1);
|
||||||
var token = await tokenStore.IssueAsync(user.Id, expiresAt, cancellationToken);
|
var token = await tokenStore.IssueAsync(user.Id, expiresAt, cancellationToken);
|
||||||
|
|
||||||
// 5. (空行后) 标记用户需重置密码
|
// 5. 标记用户需重置密码
|
||||||
user.MustChangePassword = true;
|
user.MustChangePassword = true;
|
||||||
user.FailedLoginCount = 0;
|
user.FailedLoginCount = 0;
|
||||||
user.LockedUntil = null;
|
user.LockedUntil = null;
|
||||||
@@ -66,7 +66,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
|||||||
user.Status = IdentityUserStatus.Active;
|
user.Status = IdentityUserStatus.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 构建操作日志消息
|
// 6. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -87,7 +87,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
|
|||||||
Success = true
|
Success = true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 7. (空行后) 写入 Outbox 并保存变更
|
// 7. 写入 Outbox 并保存变更
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ public sealed class RestoreIdentityUserCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户恢复用户");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户恢复用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询用户实体(包含已删除)
|
// 3. 查询用户实体(包含已删除)
|
||||||
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken);
|
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@@ -53,7 +53,7 @@ public sealed class RestoreIdentityUserCommandHandler(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 构建操作日志消息
|
// 4. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -74,7 +74,7 @@ public sealed class RestoreIdentityUserCommandHandler(
|
|||||||
Success = true
|
Success = true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. (空行后) 恢复软删除状态并写入 Outbox
|
// 5. 恢复软删除状态并写入 Outbox
|
||||||
user.DeletedAt = null;
|
user.DeletedAt = null;
|
||||||
user.DeletedBy = null;
|
user.DeletedBy = null;
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ public sealed class SearchIdentityUsersQueryHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询用户");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 组装查询过滤条件
|
// 3. 组装查询过滤条件
|
||||||
var filter = new IdentityUserSearchFilter
|
var filter = new IdentityUserSearchFilter
|
||||||
{
|
{
|
||||||
TenantId = isSuperAdmin ? request.TenantId : currentTenantId,
|
TenantId = isSuperAdmin ? request.TenantId : currentTenantId,
|
||||||
@@ -57,17 +57,17 @@ public sealed class SearchIdentityUsersQueryHandler(
|
|||||||
SortDescending = request.SortDescending
|
SortDescending = request.SortDescending
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. (空行后) 执行分页查询
|
// 4. 执行分页查询
|
||||||
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, isSuperAdmin, cancellationToken);
|
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, isSuperAdmin, cancellationToken);
|
||||||
if (items.Count == 0)
|
if (items.Count == 0)
|
||||||
{
|
{
|
||||||
return new PagedResult<UserListItemDto>(Array.Empty<UserListItemDto>(), request.Page, request.PageSize, total);
|
return new PagedResult<UserListItemDto>(Array.Empty<UserListItemDto>(), request.Page, request.PageSize, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 加载角色编码映射
|
// 5. 加载角色编码映射
|
||||||
var roleCodesLookup = await ResolveRoleCodesAsync(items, userRoleRepository, roleRepository, cancellationToken);
|
var roleCodesLookup = await ResolveRoleCodesAsync(items, userRoleRepository, roleRepository, cancellationToken);
|
||||||
|
|
||||||
// 6. (空行后) 组装 DTO
|
// 6. 组装 DTO
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var dtos = items.Select(user => new UserListItemDto
|
var dtos = items.Select(user => new UserListItemDto
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
|||||||
LastLoginAt = user.LastLoginAt
|
LastLoginAt = user.LastLoginAt
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// 7. (空行后) 返回分页结果
|
// 7. 返回分页结果
|
||||||
return new PagedResult<UserListItemDto>(dtos, request.Page, request.PageSize, total);
|
return new PagedResult<UserListItemDto>(dtos, request.Page, request.PageSize, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
|||||||
// 1. 预分配字典容量
|
// 1. 预分配字典容量
|
||||||
var result = new Dictionary<long, string[]>(users.Count);
|
var result = new Dictionary<long, string[]>(users.Count);
|
||||||
|
|
||||||
// 2. (空行后) 按租户分组,降低角色查询次数
|
// 2. 按租户分组,降低角色查询次数
|
||||||
foreach (var group in users.GroupBy(user => user.TenantId))
|
foreach (var group in users.GroupBy(user => user.TenantId))
|
||||||
{
|
{
|
||||||
var tenantId = group.Key;
|
var tenantId = group.Key;
|
||||||
@@ -113,21 +113,21 @@ public sealed class SearchIdentityUsersQueryHandler(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 查询用户角色映射
|
// 3. 查询用户角色映射
|
||||||
var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
|
||||||
if (relations.Count == 0)
|
if (relations.Count == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 查询角色并构建映射
|
// 4. 查询角色并构建映射
|
||||||
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
|
||||||
var roles = roleIds.Length == 0
|
var roles = roleIds.Length == 0
|
||||||
? Array.Empty<Role>()
|
? Array.Empty<Role>()
|
||||||
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||||
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
||||||
|
|
||||||
// 5. (空行后) 组装用户角色编码列表
|
// 5. 组装用户角色编码列表
|
||||||
foreach (var relationGroup in relations.GroupBy(x => x.UserId))
|
foreach (var relationGroup in relations.GroupBy(x => x.UserId))
|
||||||
{
|
{
|
||||||
var codes = relationGroup
|
var codes = relationGroup
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. (空行后) 校验跨租户访问权限
|
// 2. 校验跨租户访问权限
|
||||||
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新用户");
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 获取用户实体
|
// 3. 获取用户实体
|
||||||
var user = isSuperAdmin
|
var user = isSuperAdmin
|
||||||
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
|
||||||
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
||||||
@@ -56,7 +56,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 规范化输入并校验唯一性
|
// 4. 规范化输入并校验唯一性
|
||||||
var displayName = request.DisplayName.Trim();
|
var displayName = request.DisplayName.Trim();
|
||||||
var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim();
|
var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim();
|
||||||
var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
|
var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
|
||||||
@@ -85,14 +85,14 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 更新用户字段
|
// 5. 更新用户字段
|
||||||
user.DisplayName = displayName;
|
user.DisplayName = displayName;
|
||||||
user.Phone = phone;
|
user.Phone = phone;
|
||||||
user.Email = email;
|
user.Email = email;
|
||||||
user.Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim();
|
user.Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim();
|
||||||
user.RowVersion = request.RowVersion;
|
user.RowVersion = request.RowVersion;
|
||||||
|
|
||||||
// 6. (空行后) 构建操作日志消息
|
// 6. 构建操作日志消息
|
||||||
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
|
||||||
? operatorProfile.Account
|
? operatorProfile.Account
|
||||||
: operatorProfile.DisplayName;
|
: operatorProfile.DisplayName;
|
||||||
@@ -120,7 +120,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
Success = true
|
Success = true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 7. (空行后) 持久化用户更新并写入 Outbox
|
// 7. 持久化用户更新并写入 Outbox
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
||||||
@@ -131,13 +131,13 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试");
|
throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. (空行后) 覆盖角色绑定(仅当显式传入时)
|
// 8. 覆盖角色绑定(仅当显式传入时)
|
||||||
if (roleIds != null)
|
if (roleIds != null)
|
||||||
{
|
{
|
||||||
await userRoleRepository.ReplaceUserRolesAsync(user.TenantId, user.Id, roleIds, cancellationToken);
|
await userRoleRepository.ReplaceUserRolesAsync(user.TenantId, user.Id, roleIds, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. (空行后) 返回用户详情
|
// 9. 返回用户详情
|
||||||
return await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken);
|
return await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
return Array.Empty<long>();
|
return Array.Empty<long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 解析并去重
|
// 2. 解析并去重
|
||||||
var ids = new List<long>(values.Length);
|
var ids = new List<long>(values.Length);
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
@@ -161,7 +161,7 @@ public sealed class UpdateIdentityUserCommandHandler(
|
|||||||
ids.Add(id);
|
ids.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 返回去重结果
|
// 3. 返回去重结果
|
||||||
return ids.Distinct().ToArray();
|
return ids.Distinct().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ public sealed class AdminAuthService(
|
|||||||
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 更新登录成功状态
|
// 4. 更新登录成功状态
|
||||||
await UpdateLoginSuccessAsync(user.Id, now, cancellationToken);
|
await UpdateLoginSuccessAsync(user.Id, now, cancellationToken);
|
||||||
|
|
||||||
// 5. (空行后) 构建用户档案并生成令牌
|
// 5. 构建用户档案并生成令牌
|
||||||
var profile = await BuildProfileAsync(user, cancellationToken);
|
var profile = await BuildProfileAsync(user, cancellationToken);
|
||||||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,25 +26,25 @@ public sealed class TenantBillingStatementConfiguration : IEntityTypeConfigurati
|
|||||||
builder.Property(x => x.Currency).HasMaxLength(8).HasDefaultValue("CNY");
|
builder.Property(x => x.Currency).HasMaxLength(8).HasDefaultValue("CNY");
|
||||||
builder.Property(x => x.Status).HasConversion<int>();
|
builder.Property(x => x.Status).HasConversion<int>();
|
||||||
|
|
||||||
// 2. (空行后) JSON 字段(当前以 text 存储 JSON 字符串,便于兼容历史迁移)
|
// 2. JSON 字段(当前以 text 存储 JSON 字符串,便于兼容历史迁移)
|
||||||
builder.Property(x => x.LineItemsJson).HasColumnType("text");
|
builder.Property(x => x.LineItemsJson).HasColumnType("text");
|
||||||
|
|
||||||
// 3. (空行后) 备注字段
|
// 3. 备注字段
|
||||||
builder.Property(x => x.Notes).HasMaxLength(512);
|
builder.Property(x => x.Notes).HasMaxLength(512);
|
||||||
|
|
||||||
// 4. (空行后) 唯一约束与索引
|
// 4. 唯一约束与索引
|
||||||
builder.HasIndex(x => new { x.TenantId, x.StatementNo }).IsUnique();
|
builder.HasIndex(x => new { x.TenantId, x.StatementNo }).IsUnique();
|
||||||
|
|
||||||
// 5. (空行后) 性能索引(高频查询:租户+状态+到期日)
|
// 5. 性能索引(高频查询:租户+状态+到期日)
|
||||||
builder.HasIndex(x => new { x.TenantId, x.Status, x.DueDate })
|
builder.HasIndex(x => new { x.TenantId, x.Status, x.DueDate })
|
||||||
.HasDatabaseName("idx_billing_tenant_status_duedate");
|
.HasDatabaseName("idx_billing_tenant_status_duedate");
|
||||||
|
|
||||||
// 6. (空行后) 逾期扫描索引(仅索引 Pending/Overdue)
|
// 6. 逾期扫描索引(仅索引 Pending/Overdue)
|
||||||
builder.HasIndex(x => new { x.Status, x.DueDate })
|
builder.HasIndex(x => new { x.Status, x.DueDate })
|
||||||
.HasDatabaseName("idx_billing_status_duedate")
|
.HasDatabaseName("idx_billing_status_duedate")
|
||||||
.HasFilter($"\"Status\" IN ({(int)TenantBillingStatus.Pending}, {(int)TenantBillingStatus.Overdue})");
|
.HasFilter($"\"Status\" IN ({(int)TenantBillingStatus.Pending}, {(int)TenantBillingStatus.Overdue})");
|
||||||
|
|
||||||
// 7. (空行后) 创建时间索引(支持列表倒序)
|
// 7. 创建时间索引(支持列表倒序)
|
||||||
builder.HasIndex(x => x.CreatedAt)
|
builder.HasIndex(x => x.CreatedAt)
|
||||||
.HasDatabaseName("idx_billing_created_at");
|
.HasDatabaseName("idx_billing_created_at");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ public sealed class TenantPaymentConfiguration : IEntityTypeConfiguration<Tenant
|
|||||||
builder.Property(x => x.RefundReason).HasMaxLength(512);
|
builder.Property(x => x.RefundReason).HasMaxLength(512);
|
||||||
builder.Property(x => x.Notes).HasMaxLength(512);
|
builder.Property(x => x.Notes).HasMaxLength(512);
|
||||||
|
|
||||||
// 2. (空行后) 复合索引:租户+账单
|
// 2. 复合索引:租户+账单
|
||||||
builder.HasIndex(x => new { x.TenantId, x.BillingStatementId });
|
builder.HasIndex(x => new { x.TenantId, x.BillingStatementId });
|
||||||
|
|
||||||
// 3. (空行后) 支付记录时间排序索引
|
// 3. 支付记录时间排序索引
|
||||||
builder.HasIndex(x => new { x.BillingStatementId, x.PaidAt })
|
builder.HasIndex(x => new { x.BillingStatementId, x.PaidAt })
|
||||||
.HasDatabaseName("idx_payment_billing_paidat");
|
.HasDatabaseName("idx_payment_billing_paidat");
|
||||||
|
|
||||||
// 4. (空行后) 交易号索引(部分索引:仅非空)
|
// 4. 交易号索引(部分索引:仅非空)
|
||||||
builder.HasIndex(x => x.TransactionNo)
|
builder.HasIndex(x => x.TransactionNo)
|
||||||
.HasDatabaseName("idx_payment_transaction_no")
|
.HasDatabaseName("idx_payment_transaction_no")
|
||||||
.HasFilter("\"TransactionNo\" IS NOT NULL");
|
.HasFilter("\"TransactionNo\" IS NOT NULL");
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.DeletedAt == null && x.TenantId == tenantId);
|
.Where(x => x.DeletedAt == null && x.TenantId == tenantId);
|
||||||
|
|
||||||
// 2. (空行后) 按状态过滤
|
// 2. 按状态过滤
|
||||||
if (status.HasValue)
|
if (status.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.Status == status.Value);
|
query = query.Where(x => x.Status == status.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 按日期范围过滤(账单周期)
|
// 3. 按日期范围过滤(账单周期)
|
||||||
if (from.HasValue)
|
if (from.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.PeriodStart >= from.Value);
|
query = query.Where(x => x.PeriodStart >= from.Value);
|
||||||
@@ -42,7 +42,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
query = query.Where(x => x.PeriodEnd <= to.Value);
|
query = query.Where(x => x.PeriodEnd <= to.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 排序返回
|
// 4. 排序返回
|
||||||
return await query
|
return await query
|
||||||
.OrderByDescending(x => x.PeriodEnd)
|
.OrderByDescending(x => x.PeriodEnd)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -102,7 +102,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
// 1. 以当前 UTC 时间作为逾期判断基准
|
// 1. 以当前 UTC 时间作为逾期判断基准
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
// 2. (空行后) 查询逾期且仍处于待支付的账单(仅 Pending 才允许自动切换为 Overdue)
|
// 2. 查询逾期且仍处于待支付的账单(仅 Pending 才允许自动切换为 Overdue)
|
||||||
return await context.TenantBillingStatements
|
return await context.TenantBillingStatements
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -120,7 +120,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var dueTo = now.AddDays(daysAhead);
|
var dueTo = now.AddDays(daysAhead);
|
||||||
|
|
||||||
// 2. (空行后) 仅查询待支付账单
|
// 2. 仅查询待支付账单
|
||||||
return await context.TenantBillingStatements
|
return await context.TenantBillingStatements
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -198,19 +198,19 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.DeletedAt == null);
|
.Where(x => x.DeletedAt == null);
|
||||||
|
|
||||||
// 2. (空行后) 按租户过滤(可选)
|
// 2. 按租户过滤(可选)
|
||||||
if (tenantId.HasValue)
|
if (tenantId.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 按状态过滤(可选)
|
// 3. 按状态过滤(可选)
|
||||||
if (status.HasValue)
|
if (status.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.Status == status.Value);
|
query = query.Where(x => x.Status == status.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 按日期范围过滤(账单周期)
|
// 4. 按日期范围过滤(账单周期)
|
||||||
if (from.HasValue)
|
if (from.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.PeriodStart >= from.Value);
|
query = query.Where(x => x.PeriodStart >= from.Value);
|
||||||
@@ -221,7 +221,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
query = query.Where(x => x.PeriodEnd <= to.Value);
|
query = query.Where(x => x.PeriodEnd <= to.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 按金额范围过滤(应付金额,包含边界)
|
// 5. 按金额范围过滤(应付金额,包含边界)
|
||||||
if (minAmount.HasValue)
|
if (minAmount.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.AmountDue >= minAmount.Value);
|
query = query.Where(x => x.AmountDue >= minAmount.Value);
|
||||||
@@ -232,7 +232,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
query = query.Where(x => x.AmountDue <= maxAmount.Value);
|
query = query.Where(x => x.AmountDue <= maxAmount.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 关键字过滤(账单号或租户名)
|
// 6. 关键字过滤(账单号或租户名)
|
||||||
if (!string.IsNullOrWhiteSpace(keyword))
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
{
|
{
|
||||||
var normalized = keyword.Trim();
|
var normalized = keyword.Trim();
|
||||||
@@ -249,10 +249,10 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
select b;
|
select b;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. (空行后) 统计总数
|
// 7. 统计总数
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
// 8. (空行后) 分页查询
|
// 8. 分页查询
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderByDescending(x => x.PeriodEnd)
|
.OrderByDescending(x => x.PeriodEnd)
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
@@ -279,7 +279,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
&& x.PeriodStart >= startDate
|
&& x.PeriodStart >= startDate
|
||||||
&& x.PeriodEnd <= endDate);
|
&& x.PeriodEnd <= endDate);
|
||||||
|
|
||||||
// 2. (空行后) 聚合统计(金额统一使用:应付 - 折扣 + 税费)
|
// 2. 聚合统计(金额统一使用:应付 - 折扣 + 税费)
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var totalAmount = await query.SumAsync(x => x.AmountDue - x.DiscountAmount + x.TaxAmount, cancellationToken);
|
var totalAmount = await query.SumAsync(x => x.AmountDue - x.DiscountAmount + x.TaxAmount, cancellationToken);
|
||||||
var paidAmount = await query.Where(x => x.Status == TenantBillingStatus.Paid).SumAsync(x => x.AmountPaid, cancellationToken);
|
var paidAmount = await query.Where(x => x.Status == TenantBillingStatus.Paid).SumAsync(x => x.AmountPaid, cancellationToken);
|
||||||
@@ -288,13 +288,13 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
.Where(x => (x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue) && x.DueDate < now)
|
.Where(x => (x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue) && x.DueDate < now)
|
||||||
.SumAsync(x => (x.AmountDue - x.DiscountAmount + x.TaxAmount) - x.AmountPaid, cancellationToken);
|
.SumAsync(x => (x.AmountDue - x.DiscountAmount + x.TaxAmount) - x.AmountPaid, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 数量统计
|
// 3. 数量统计
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
var paidCount = await query.CountAsync(x => x.Status == TenantBillingStatus.Paid, cancellationToken);
|
var paidCount = await query.CountAsync(x => x.Status == TenantBillingStatus.Paid, cancellationToken);
|
||||||
var unpaidCount = await query.CountAsync(x => x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue, cancellationToken);
|
var unpaidCount = await query.CountAsync(x => x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue, cancellationToken);
|
||||||
var overdueCount = await query.CountAsync(x => (x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue) && x.DueDate < now, cancellationToken);
|
var overdueCount = await query.CountAsync(x => (x.Status == TenantBillingStatus.Pending || x.Status == TenantBillingStatus.Overdue) && x.DueDate < now, cancellationToken);
|
||||||
|
|
||||||
// 4. (空行后) 趋势统计
|
// 4. 趋势统计
|
||||||
var normalizedGroupBy = NormalizeGroupBy(groupBy);
|
var normalizedGroupBy = NormalizeGroupBy(groupBy);
|
||||||
var trendRaw = await query
|
var trendRaw = await query
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
@@ -307,7 +307,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
// 4.1 (空行后) 在内存中按 Day/Week/Month 聚合(避免依赖特定数据库函数扩展)
|
// 4.1 在内存中按 Day/Week/Month 聚合(避免依赖特定数据库函数扩展)
|
||||||
var trend = trendRaw
|
var trend = trendRaw
|
||||||
.GroupBy(x => GetTrendBucket(x.PeriodStart, normalizedGroupBy))
|
.GroupBy(x => GetTrendBucket(x.PeriodStart, normalizedGroupBy))
|
||||||
.Select(g => new TenantBillingTrendDataPoint
|
.Select(g => new TenantBillingTrendDataPoint
|
||||||
@@ -371,7 +371,7 @@ public sealed class TenantBillingRepository(TakeoutAppDbContext context) : ITena
|
|||||||
var dayOfWeek = (int)date.DayOfWeek; // Sunday=0, Monday=1, ...
|
var dayOfWeek = (int)date.DayOfWeek; // Sunday=0, Monday=1, ...
|
||||||
var daysSinceMonday = (dayOfWeek + 6) % 7;
|
var daysSinceMonday = (dayOfWeek + 6) % 7;
|
||||||
|
|
||||||
// 2. (空行后) 回退到周一 00:00:00(UTC)
|
// 2. 回退到周一 00:00:00(UTC)
|
||||||
var monday = date.AddDays(-daysSinceMonday);
|
var monday = date.AddDays(-daysSinceMonday);
|
||||||
return new DateTime(monday.Year, monday.Month, monday.Day, 0, 0, 0, DateTimeKind.Utc);
|
return new DateTime(monday.Year, monday.Month, monday.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -698,8 +698,8 @@ public sealed class TakeoutAppDbContext(
|
|||||||
.HasComment("发布状态:0=草稿,1=已发布。");
|
.HasComment("发布状态:0=草稿,1=已发布。");
|
||||||
|
|
||||||
// 1. 解决 EF Core 默认值哨兵问题:当我们希望插入 false/0 时,若数据库配置了 default 且 EF 认为该值是“未设置”,会导致 insert 省略列,最终落库为默认值。
|
// 1. 解决 EF Core 默认值哨兵问题:当我们希望插入 false/0 时,若数据库配置了 default 且 EF 认为该值是“未设置”,会导致 insert 省略列,最终落库为默认值。
|
||||||
// 2. (空行后) 发布状态使用 -1 作为哨兵,避免 Draft=0 被误判为“未设置”而触发数据库默认值(发布/草稿切换必须可控)。
|
// 2. 发布状态使用 -1 作为哨兵,避免 Draft=0 被误判为“未设置”而触发数据库默认值(发布/草稿切换必须可控)。
|
||||||
// 3. (空行后) 将布尔开关哨兵值设置为数据库默认值:true 作为哨兵,false 才会被显式写入,从而保证“可见性/可售开关”在新增时可正确落库。
|
// 3. 将布尔开关哨兵值设置为数据库默认值:true 作为哨兵,false 才会被显式写入,从而保证“可见性/可售开关”在新增时可正确落库。
|
||||||
builder.Property(x => x.IsPublicVisible)
|
builder.Property(x => x.IsPublicVisible)
|
||||||
.HasDefaultValue(true)
|
.HasDefaultValue(true)
|
||||||
.HasSentinel(true)
|
.HasSentinel(true)
|
||||||
@@ -709,7 +709,7 @@ public sealed class TakeoutAppDbContext(
|
|||||||
.HasSentinel(true)
|
.HasSentinel(true)
|
||||||
.HasComment("是否允许新租户购买/选择(仅影响新购)。");
|
.HasComment("是否允许新租户购买/选择(仅影响新购)。");
|
||||||
|
|
||||||
// 4. (空行后) 展示配置:推荐标识与标签(用于套餐展示页/对比页)
|
// 4. 展示配置:推荐标识与标签(用于套餐展示页/对比页)
|
||||||
builder.Property(x => x.IsRecommended)
|
builder.Property(x => x.IsRecommended)
|
||||||
.HasDefaultValue(false)
|
.HasDefaultValue(false)
|
||||||
.HasComment("是否推荐展示(运营推荐标识)。");
|
.HasComment("是否推荐展示(运营推荐标识)。");
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog
|
|||||||
// 1. 保存业务库变更
|
// 1. 保存业务库变更
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 保存日志库变更
|
// 2. 保存日志库变更
|
||||||
await logsContext.SaveChangesAsync(cancellationToken);
|
await logsContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
|
|||||||
// 1. 保存业务库变更
|
// 1. 保存业务库变更
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 保存日志库变更
|
// 2. 保存日志库变更
|
||||||
await logsContext.SaveChangesAsync(cancellationToken);
|
await logsContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,21 +105,21 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
|
|||||||
query = query.Where(x => EF.Functions.ILike(x.Name, $"%{normalizedName}%"));
|
query = query.Where(x => EF.Functions.ILike(x.Name, $"%{normalizedName}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 按联系人过滤(模糊匹配)
|
// 4. 按联系人过滤(模糊匹配)
|
||||||
if (!string.IsNullOrWhiteSpace(contactName))
|
if (!string.IsNullOrWhiteSpace(contactName))
|
||||||
{
|
{
|
||||||
var normalizedContactName = contactName.Trim();
|
var normalizedContactName = contactName.Trim();
|
||||||
query = query.Where(x => EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{normalizedContactName}%"));
|
query = query.Where(x => EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{normalizedContactName}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. (空行后) 按联系电话过滤(模糊匹配)
|
// 5. 按联系电话过滤(模糊匹配)
|
||||||
if (!string.IsNullOrWhiteSpace(contactPhone))
|
if (!string.IsNullOrWhiteSpace(contactPhone))
|
||||||
{
|
{
|
||||||
var normalizedContactPhone = contactPhone.Trim();
|
var normalizedContactPhone = contactPhone.Trim();
|
||||||
query = query.Where(x => EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedContactPhone}%"));
|
query = query.Where(x => EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedContactPhone}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. (空行后) 兼容关键字查询:名称/编码/联系人/电话
|
// 6. 兼容关键字查询:名称/编码/联系人/电话
|
||||||
if (!string.IsNullOrWhiteSpace(keyword))
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
{
|
{
|
||||||
var normalizedKeyword = keyword.Trim();
|
var normalizedKeyword = keyword.Trim();
|
||||||
@@ -130,10 +130,10 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
|
|||||||
EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedKeyword}%"));
|
EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedKeyword}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. (空行后) 先统计总数,再按创建时间倒序分页
|
// 7. 先统计总数,再按创建时间倒序分页
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
// 8. (空行后) 查询当前页数据
|
// 8. 查询当前页数据
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderByDescending(x => x.CreatedAt)
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
@@ -169,18 +169,18 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
|
|||||||
// 1. 标准化名称
|
// 1. 标准化名称
|
||||||
var normalized = name.Trim();
|
var normalized = name.Trim();
|
||||||
|
|
||||||
// 2. (空行后) 构建查询(名称使用 ILike 做不区分大小写的等值匹配)
|
// 2. 构建查询(名称使用 ILike 做不区分大小写的等值匹配)
|
||||||
var query = context.Tenants
|
var query = context.Tenants
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => EF.Functions.ILike(x.Name, normalized));
|
.Where(x => EF.Functions.ILike(x.Name, normalized));
|
||||||
|
|
||||||
// 3. (空行后) 更新场景排除自身
|
// 3. 更新场景排除自身
|
||||||
if (excludeTenantId.HasValue)
|
if (excludeTenantId.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(x => x.Id != excludeTenantId.Value);
|
query = query.Where(x => x.Id != excludeTenantId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 判断是否存在
|
// 4. 判断是否存在
|
||||||
return query.AnyAsync(cancellationToken);
|
return query.AnyAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
|
|||||||
await context.TenantReviewClaims.AddAsync(claim, cancellationToken);
|
await context.TenantReviewClaims.AddAsync(claim, cancellationToken);
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 写入审计日志
|
// 2. 写入审计日志
|
||||||
await logsContext.TenantAuditLogs.AddAsync(auditLog, cancellationToken);
|
await logsContext.TenantAuditLogs.AddAsync(auditLog, cancellationToken);
|
||||||
await logsContext.SaveChangesAsync(cancellationToken);
|
await logsContext.SaveChangesAsync(cancellationToken);
|
||||||
return true;
|
return true;
|
||||||
@@ -292,7 +292,7 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
|
|||||||
// 1. 释放实体跟踪避免重复写入
|
// 1. 释放实体跟踪避免重复写入
|
||||||
context.Entry(claim).State = EntityState.Detached;
|
context.Entry(claim).State = EntityState.Detached;
|
||||||
|
|
||||||
// 2. (空行后) 返回抢占失败
|
// 2. 返回抢占失败
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +398,7 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context, TakeoutLogsD
|
|||||||
// 1. 保存业务库变更
|
// 1. 保存业务库变更
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 2. (空行后) 保存日志库变更
|
// 2. 保存日志库变更
|
||||||
await logsContext.SaveChangesAsync(cancellationToken);
|
await logsContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ public sealed class BillingDomainService(
|
|||||||
throw new InvalidOperationException("该订阅周期的账单已存在。");
|
throw new InvalidOperationException("该订阅周期的账单已存在。");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 查询套餐价格信息
|
// 2. 查询套餐价格信息
|
||||||
var package = await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken);
|
var package = await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken);
|
||||||
if (package is null)
|
if (package is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("订阅未关联有效套餐,无法生成账单。");
|
throw new InvalidOperationException("订阅未关联有效套餐,无法生成账单。");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 选择价格(简化规则:优先按年/按月)
|
// 3. 选择价格(简化规则:优先按年/按月)
|
||||||
var days = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays;
|
var days = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays;
|
||||||
var amountDue = days >= 300 ? package.YearlyPrice : package.MonthlyPrice;
|
var amountDue = days >= 300 ? package.YearlyPrice : package.MonthlyPrice;
|
||||||
if (!amountDue.HasValue)
|
if (!amountDue.HasValue)
|
||||||
@@ -48,7 +48,7 @@ public sealed class BillingDomainService(
|
|||||||
throw new InvalidOperationException("套餐价格未配置,无法生成账单。");
|
throw new InvalidOperationException("套餐价格未配置,无法生成账单。");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 生成账单明细
|
// 4. 生成账单明细
|
||||||
var lineItems = new List<BillingLineItem>
|
var lineItems = new List<BillingLineItem>
|
||||||
{
|
{
|
||||||
BillingLineItem.Create(
|
BillingLineItem.Create(
|
||||||
@@ -58,7 +58,7 @@ public sealed class BillingDomainService(
|
|||||||
unitPrice: amountDue.Value)
|
unitPrice: amountDue.Value)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. (空行后) 构建账单实体
|
// 5. 构建账单实体
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return new TenantBillingStatement
|
return new TenantBillingStatement
|
||||||
{
|
{
|
||||||
@@ -98,7 +98,7 @@ public sealed class BillingDomainService(
|
|||||||
// 1. 计算金额
|
// 1. 计算金额
|
||||||
var amountDue = quotaPackage.Price * quantity;
|
var amountDue = quotaPackage.Price * quantity;
|
||||||
|
|
||||||
// 2. (空行后) 生成账单明细
|
// 2. 生成账单明细
|
||||||
var lineItems = new List<BillingLineItem>
|
var lineItems = new List<BillingLineItem>
|
||||||
{
|
{
|
||||||
BillingLineItem.Create(
|
BillingLineItem.Create(
|
||||||
@@ -108,7 +108,7 @@ public sealed class BillingDomainService(
|
|||||||
unitPrice: quotaPackage.Price)
|
unitPrice: quotaPackage.Price)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. (空行后) 构建账单实体
|
// 3. 构建账单实体
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var billing = new TenantBillingStatement
|
var billing = new TenantBillingStatement
|
||||||
{
|
{
|
||||||
@@ -139,7 +139,7 @@ public sealed class BillingDomainService(
|
|||||||
// 1. 账单号格式:BILL-{yyyyMMdd}-{序号}
|
// 1. 账单号格式:BILL-{yyyyMMdd}-{序号}
|
||||||
var date = DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
|
var date = DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
// 2. (空行后) 使用雪花 ID 作为全局递增序号,确保分布式唯一
|
// 2. 使用雪花 ID 作为全局递增序号,确保分布式唯一
|
||||||
var sequence = idGenerator.NextId();
|
var sequence = idGenerator.NextId();
|
||||||
return $"BILL-{date}-{sequence}";
|
return $"BILL-{date}-{sequence}";
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ public sealed class BillingDomainService(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 批量标记逾期(防御性:再次判断 Pending)
|
// 2. 批量标记逾期(防御性:再次判断 Pending)
|
||||||
var processedAt = DateTime.UtcNow;
|
var processedAt = DateTime.UtcNow;
|
||||||
var updated = 0;
|
var updated = 0;
|
||||||
foreach (var billing in overdueBillings)
|
foreach (var billing in overdueBillings)
|
||||||
@@ -172,7 +172,7 @@ public sealed class BillingDomainService(
|
|||||||
updated++;
|
updated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 持久化
|
// 3. 持久化
|
||||||
if (updated > 0)
|
if (updated > 0)
|
||||||
{
|
{
|
||||||
await billingRepository.SaveChangesAsync(cancellationToken);
|
await billingRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
using var workbook = new XLWorkbook();
|
using var workbook = new XLWorkbook();
|
||||||
var worksheet = workbook.Worksheets.Add("Billings");
|
var worksheet = workbook.Worksheets.Add("Billings");
|
||||||
|
|
||||||
// 2. (空行后) 写入表头
|
// 2. 写入表头
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
"Id", "TenantId", "StatementNo", "BillingType", "Status",
|
"Id", "TenantId", "StatementNo", "BillingType", "Status",
|
||||||
@@ -46,7 +46,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
worksheet.Cell(1, i + 1).Value = headers[i];
|
worksheet.Cell(1, i + 1).Value = headers[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. (空行后) 写入数据行
|
// 3. 写入数据行
|
||||||
for (var rowIndex = 0; rowIndex < billings.Count; rowIndex++)
|
for (var rowIndex = 0; rowIndex < billings.Count; rowIndex++)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -73,7 +73,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
worksheet.Cell(r, 16).Value = billing.LineItemsJson ?? string.Empty;
|
worksheet.Cell(r, 16).Value = billing.LineItemsJson ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) 自动调整列宽并输出
|
// 4. 自动调整列宽并输出
|
||||||
worksheet.Columns().AdjustToContents();
|
worksheet.Columns().AdjustToContents();
|
||||||
|
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
@@ -102,7 +102,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
// 2. 标题
|
// 2. 标题
|
||||||
column.Item().Text("Billings Export").FontSize(16).SemiBold();
|
column.Item().Text("Billings Export").FontSize(16).SemiBold();
|
||||||
|
|
||||||
// 3. (空行后) 逐条输出
|
// 3. 逐条输出
|
||||||
for (var i = 0; i < billings.Count; i++)
|
for (var i = 0; i < billings.Count; i++)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -129,7 +129,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. (空行后) 输出字节
|
// 4. 输出字节
|
||||||
var bytes = document.GeneratePdf();
|
var bytes = document.GeneratePdf();
|
||||||
return Task.FromResult(bytes);
|
return Task.FromResult(bytes);
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
csv.WriteField("LineItemsJson");
|
csv.WriteField("LineItemsJson");
|
||||||
await csv.NextRecordAsync();
|
await csv.NextRecordAsync();
|
||||||
|
|
||||||
// 3. (空行后) 写入数据行
|
// 3. 写入数据行
|
||||||
foreach (var b in billings)
|
foreach (var b in billings)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -196,7 +196,7 @@ public sealed class BillingExportService : IBillingExportService
|
|||||||
await csv.NextRecordAsync();
|
await csv.NextRecordAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. (空行后) Flush 并返回字节
|
// 4. Flush 并返回字节
|
||||||
await writer.FlushAsync(cancellationToken);
|
await writer.FlushAsync(cancellationToken);
|
||||||
return stream.ToArray();
|
return stream.ToArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ public sealed class RedisAdminPasswordResetTokenStore(
|
|||||||
// 1. 生成 URL 安全的随机令牌
|
// 1. 生成 URL 安全的随机令牌
|
||||||
var token = GenerateUrlSafeToken(48);
|
var token = GenerateUrlSafeToken(48);
|
||||||
|
|
||||||
// 2. (空行后) 写入缓存(Value:userId)
|
// 2. 写入缓存(Value:userId)
|
||||||
await cache.SetStringAsync(BuildKey(token), userId.ToString(), new DistributedCacheEntryOptions
|
await cache.SetStringAsync(BuildKey(token), userId.ToString(), new DistributedCacheEntryOptions
|
||||||
{
|
{
|
||||||
AbsoluteExpiration = expiresAt
|
AbsoluteExpiration = expiresAt
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 返回令牌
|
// 3. 返回令牌
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +43,10 @@ public sealed class RedisAdminPasswordResetTokenStore(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 删除缓存(一次性令牌)
|
// 2. 删除缓存(一次性令牌)
|
||||||
await cache.RemoveAsync(key, cancellationToken);
|
await cache.RemoveAsync(key, cancellationToken);
|
||||||
|
|
||||||
// 3. (空行后) 解析用户 ID
|
// 3. 解析用户 ID
|
||||||
return long.TryParse(value, out var userId) ? userId : null;
|
return long.TryParse(value, out var userId) ? userId : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class IdentityUserOperationLogConsumer(TakeoutLogsDbContext logsCo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 构建日志实体与去重记录
|
// 2. 构建日志实体与去重记录
|
||||||
var message = context.Message;
|
var message = context.Message;
|
||||||
var log = new OperationLog
|
var log = new OperationLog
|
||||||
{
|
{
|
||||||
@@ -50,7 +50,7 @@ public sealed class IdentityUserOperationLogConsumer(TakeoutLogsDbContext logsCo
|
|||||||
});
|
});
|
||||||
logsContext.OperationLogs.Add(log);
|
logsContext.OperationLogs.Add(log);
|
||||||
|
|
||||||
// 3. (空行后) 保存并处理并发去重冲突
|
// 3. 保存并处理并发去重冲突
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await logsContext.SaveChangesAsync(context.CancellationToken);
|
await logsContext.SaveChangesAsync(context.CancellationToken);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public static class OperationLogOutboxServiceCollectionExtensions
|
|||||||
throw new InvalidOperationException("缺少 RabbitMQ 配置。");
|
throw new InvalidOperationException("缺少 RabbitMQ 配置。");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. (空行后) 注册 MassTransit 与 Outbox
|
// 2. 注册 MassTransit 与 Outbox
|
||||||
services.AddMassTransit(configurator =>
|
services.AddMassTransit(configurator =>
|
||||||
{
|
{
|
||||||
configurator.AddConsumer<IdentityUserOperationLogConsumer>();
|
configurator.AddConsumer<IdentityUserOperationLogConsumer>();
|
||||||
@@ -50,7 +50,7 @@ public static class OperationLogOutboxServiceCollectionExtensions
|
|||||||
cfg.ConfigureEndpoints(context);
|
cfg.ConfigureEndpoints(context);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// 3. (空行后) 返回服务集合
|
// 3. 返回服务集合
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed class RecurringJobRegistrar(
|
|||||||
RecurringJob.AddOrUpdate<CouponExpireJob>("coupons.expire", job => job.ExecuteAsync(), "0 */1 * * *");
|
RecurringJob.AddOrUpdate<CouponExpireJob>("coupons.expire", job => job.ExecuteAsync(), "0 */1 * * *");
|
||||||
RecurringJob.AddOrUpdate<LogCleanupJob>("logs.cleanup", job => job.ExecuteAsync(), "0 3 * * *");
|
RecurringJob.AddOrUpdate<LogCleanupJob>("logs.cleanup", job => job.ExecuteAsync(), "0 3 * * *");
|
||||||
|
|
||||||
// 2. (空行后) 订阅自动化任务(自动续费、续费提醒、到期进入宽限期)
|
// 2. 订阅自动化任务(自动续费、续费提醒、到期进入宽限期)
|
||||||
var options = subscriptionAutomationOptions.CurrentValue;
|
var options = subscriptionAutomationOptions.CurrentValue;
|
||||||
RecurringJob.AddOrUpdate<SubscriptionAutoRenewalJob>(
|
RecurringJob.AddOrUpdate<SubscriptionAutoRenewalJob>(
|
||||||
"subscriptions.auto-renewal",
|
"subscriptions.auto-renewal",
|
||||||
@@ -37,7 +37,7 @@ public sealed class RecurringJobRegistrar(
|
|||||||
job => job.ExecuteAsync(),
|
job => job.ExecuteAsync(),
|
||||||
$"0 {options.SubscriptionExpiryCheckExecuteHourUtc} * * *");
|
$"0 {options.SubscriptionExpiryCheckExecuteHourUtc} * * *");
|
||||||
|
|
||||||
// 3. (空行后) 账单自动化任务(逾期标记)
|
// 3. 账单自动化任务(逾期标记)
|
||||||
var billingOptions = billingAutomationOptions.CurrentValue;
|
var billingOptions = billingAutomationOptions.CurrentValue;
|
||||||
RecurringJob.AddOrUpdate<BillingOverdueProcessJob>(
|
RecurringJob.AddOrUpdate<BillingOverdueProcessJob>(
|
||||||
"billings.overdue-process",
|
"billings.overdue-process",
|
||||||
|
|||||||
Reference in New Issue
Block a user