chore: 提交现有修改
This commit is contained in:
@@ -3,16 +3,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
/// <summary>
|
||||
/// 业务异常(用于可预期的业务校验错误)。
|
||||
/// </summary>
|
||||
public class BusinessException : Exception
|
||||
public class BusinessException(int errorCode, string message) : Exception(message)
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务错误码。
|
||||
/// </summary>
|
||||
public int ErrorCode { get; }
|
||||
|
||||
public BusinessException(int errorCode, string message) : base(message)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
public int ErrorCode { get; } = errorCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
/// <summary>
|
||||
/// 验证异常(用于聚合验证错误信息)。
|
||||
/// </summary>
|
||||
public class ValidationException : Exception
|
||||
public class ValidationException(IDictionary<string, string[]> errors) : Exception("一个或多个验证错误")
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段/属性的错误集合。
|
||||
/// </summary>
|
||||
public IDictionary<string, string[]> Errors { get; }
|
||||
|
||||
public ValidationException(IDictionary<string, string[]> errors)
|
||||
: base("一个或多个验证错误")
|
||||
{
|
||||
Errors = errors;
|
||||
}
|
||||
public IDictionary<string, string[]> Errors { get; } = errors;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,42 +6,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||
/// 分页结果包装,携带列表与总条数等元数据。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型。</typeparam>
|
||||
public sealed class PagedResult<T>
|
||||
/// <remarks>
|
||||
/// 初始化分页结果。
|
||||
/// </remarks>
|
||||
public sealed class PagedResult<T>(IReadOnlyList<T> items, int page, int pageSize, int totalCount)
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<T> Items { get; }
|
||||
public IReadOnlyList<T> Items { get; } = items;
|
||||
|
||||
/// <summary>
|
||||
/// 当前页码,从 1 开始。
|
||||
/// </summary>
|
||||
public int Page { get; }
|
||||
public int Page { get; } = page;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; }
|
||||
public int PageSize { get; } = pageSize;
|
||||
|
||||
/// <summary>
|
||||
/// 总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; }
|
||||
public int TotalCount { get; } = totalCount;
|
||||
|
||||
/// <summary>
|
||||
/// 总页数。
|
||||
/// </summary>
|
||||
public int TotalPages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化分页结果。
|
||||
/// </summary>
|
||||
public PagedResult(IReadOnlyList<T> items, int page, int pageSize, int totalCount)
|
||||
{
|
||||
Items = items;
|
||||
Page = page;
|
||||
PageSize = pageSize;
|
||||
TotalCount = totalCount;
|
||||
TotalPages = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
}
|
||||
public int TotalPages { get; } = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
}
|
||||
|
||||
@@ -3,40 +3,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
/// <summary>
|
||||
/// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。
|
||||
/// </summary>
|
||||
public sealed class TenantContext
|
||||
/// <remarks>
|
||||
/// 初始化租户上下文。
|
||||
/// </remarks>
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="tenantCode">租户编码(可选)</param>
|
||||
/// <param name="source">解析来源</param>
|
||||
public sealed class TenantContext(long tenantId, string? tenantCode, string source)
|
||||
{
|
||||
/// <summary>
|
||||
/// 未解析到租户时的默认上下文。
|
||||
/// </summary>
|
||||
public static TenantContext Empty { get; } = new(0, null, "unresolved");
|
||||
|
||||
/// <summary>
|
||||
/// 初始化租户上下文。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID</param>
|
||||
/// <param name="tenantCode">租户编码(可选)</param>
|
||||
/// <param name="source">解析来源</param>
|
||||
public TenantContext(long tenantId, string? tenantCode, string source)
|
||||
{
|
||||
TenantId = tenantId;
|
||||
TenantCode = tenantCode;
|
||||
Source = source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前租户 ID,未解析时为 Guid.Empty。
|
||||
/// </summary>
|
||||
public long TenantId { get; }
|
||||
public long TenantId { get; } = tenantId;
|
||||
|
||||
/// <summary>
|
||||
/// 当前租户编码(例如子域名或业务编码),可为空。
|
||||
/// </summary>
|
||||
public string? TenantCode { get; }
|
||||
public string? TenantCode { get; } = tenantCode;
|
||||
|
||||
/// <summary>
|
||||
/// 租户解析来源(Header、Host、Token 等)。
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
public string Source { get; } = source;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已成功解析到租户。
|
||||
|
||||
@@ -8,7 +8,12 @@ namespace TakeoutSaaS.Shared.Kernel.Ids;
|
||||
/// <summary>
|
||||
/// 基于雪花算法的长整型 ID 生成器。
|
||||
/// </summary>
|
||||
public sealed class SnowflakeIdGenerator : IIdGenerator
|
||||
/// <remarks>
|
||||
/// 初始化生成器。
|
||||
/// </remarks>
|
||||
/// <param name="workerId">工作节点 ID。</param>
|
||||
/// <param name="datacenterId">机房 ID。</param>
|
||||
public sealed class SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0) : IIdGenerator
|
||||
{
|
||||
private const long Twepoch = 1577836800000L; // 2020-01-01 UTC
|
||||
private const int WorkerIdBits = 5;
|
||||
@@ -23,23 +28,12 @@ public sealed class SnowflakeIdGenerator : IIdGenerator
|
||||
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
|
||||
private const long SequenceMask = -1L ^ (-1L << SequenceBits);
|
||||
|
||||
private readonly long _workerId;
|
||||
private readonly long _datacenterId;
|
||||
private readonly long _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId));
|
||||
private readonly long _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId));
|
||||
private long _lastTimestamp = -1L;
|
||||
private long _sequence;
|
||||
private long _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask);
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化生成器。
|
||||
/// </summary>
|
||||
/// <param name="workerId">工作节点 ID。</param>
|
||||
/// <param name="datacenterId">机房 ID。</param>
|
||||
public SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0)
|
||||
{
|
||||
_workerId = Normalize(workerId, MaxWorkerId, nameof(workerId));
|
||||
_datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId));
|
||||
_sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long NextId()
|
||||
|
||||
@@ -11,22 +11,11 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
/// <summary>
|
||||
/// 统一 TraceId/CorrelationId,贯穿日志与响应。
|
||||
/// </summary>
|
||||
public sealed class CorrelationIdMiddleware
|
||||
public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
|
||||
{
|
||||
private const string TraceHeader = "X-Trace-Id";
|
||||
private const string RequestHeader = "X-Request-Id";
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<CorrelationIdMiddleware> _logger;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
|
||||
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_idGenerator = idGenerator;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var traceId = ResolveTraceId(context);
|
||||
@@ -39,14 +28,14 @@ public sealed class CorrelationIdMiddleware
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["TraceId"] = traceId
|
||||
}))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -67,7 +56,7 @@ public sealed class CorrelationIdMiddleware
|
||||
return requestId;
|
||||
}
|
||||
|
||||
return _idGenerator.NextId().ToString();
|
||||
return idGenerator.NextId().ToString();
|
||||
}
|
||||
|
||||
private static bool TryGetHeader(HttpContext context, string headerName, out string value)
|
||||
|
||||
@@ -14,34 +14,23 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
/// <summary>
|
||||
/// 全局异常处理中间件,将异常统一映射为 ApiResponse。
|
||||
/// </summary>
|
||||
public sealed class ExceptionHandlingMiddleware
|
||||
public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||
private readonly IHostEnvironment _environment;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "未处理异常:{Message}", ex.Message);
|
||||
logger.LogError(ex, "未处理异常:{Message}", ex.Message);
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +39,7 @@ public sealed class ExceptionHandlingMiddleware
|
||||
{
|
||||
var (statusCode, response) = BuildErrorResponse(exception);
|
||||
|
||||
if (_environment.IsDevelopment())
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
response = response with
|
||||
{
|
||||
|
||||
@@ -9,29 +9,21 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
/// <summary>
|
||||
/// 基础请求日志(方法、路径、耗时、状态码、TraceId)。
|
||||
/// </summary>
|
||||
public sealed class RequestLoggingMiddleware
|
||||
public sealed class RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<RequestLoggingMiddleware> _logger;
|
||||
|
||||
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
var traceId = TraceContext.TraceId ?? context.TraceIdentifier;
|
||||
_logger.LogInformation(
|
||||
logger.LogInformation(
|
||||
"HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
|
||||
@@ -7,24 +7,19 @@ namespace TakeoutSaaS.Shared.Web.Security;
|
||||
/// <summary>
|
||||
/// 基于 HttpContext 的当前用户访问器。
|
||||
/// </summary>
|
||||
public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor
|
||||
/// <remarks>
|
||||
/// 初始化访问器。
|
||||
/// </remarks>
|
||||
public sealed class HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) : ICurrentUserAccessor
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化访问器。
|
||||
/// </summary>
|
||||
public HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var principal = _httpContextAccessor.HttpContext?.User;
|
||||
var principal = httpContextAccessor.HttpContext?.User;
|
||||
if (principal == null || !principal.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return 0;
|
||||
|
||||
@@ -9,22 +9,15 @@ namespace TakeoutSaaS.Shared.Web.Swagger;
|
||||
/// <summary>
|
||||
/// 根据 API 版本动态注册 Swagger 文档。
|
||||
/// </summary>
|
||||
internal sealed class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
internal sealed class ConfigureSwaggerOptions(
|
||||
IApiVersionDescriptionProvider provider,
|
||||
IOptions<SwaggerDocumentSettings> settings) : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IApiVersionDescriptionProvider _provider;
|
||||
private readonly SwaggerDocumentSettings _settings;
|
||||
|
||||
public ConfigureSwaggerOptions(
|
||||
IApiVersionDescriptionProvider provider,
|
||||
IOptions<SwaggerDocumentSettings> settings)
|
||||
{
|
||||
_provider = provider;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
private readonly SwaggerDocumentSettings _settings = settings.Value;
|
||||
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
foreach (var description in _provider.ApiVersionDescriptions)
|
||||
foreach (var description in provider.ApiVersionDescriptions)
|
||||
{
|
||||
var info = new OpenApiInfo
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user