前言
前面的专题已经分别介绍了 HQServer 的 JWT、ORM、Quartz、RabbitMQ、SignalR 和基础架构整理。随着这些能力逐步加入,框架已经从“能跑起来的 Web API 项目”继续向“可以直接作为业务项目起点的基础架构”演进。
这次更新没有引入复杂的 CQRS、MediatR 或大型领域框架,而是把企业 API 项目中经常重复、容易遗漏的能力继续收敛到 HQ.Common:统一响应和异常、参数校验、TraceId、健康检查、跨仓储事务、HTTP 客户端、缓存、文件存储,以及更完整的 Demo 和测试。
本文以当前 HQServer 源码为准,重点说明这次框架修改解决了什么问题,以及新项目应该怎样使用。
一、这次更新包含哪些内容
- 统一 API 成功和失败响应,增加错误码与 TraceId。
- 增加参数校验过滤器和业务异常处理。
- 补充 HTTPS 重定向、CORS、存活检查和数据库就绪检查。
- 增加
IHQUnitOfWork,支持多个仓储共用一个事务边界。 - 将
HttpClientHelper接入IHttpClientFactory。 - 增加轻量内存缓存抽象和本地文件存储抽象。
- 优化 Quartz 错失触发策略,补强 RabbitMQ 消费者生命周期和 QoS。
- 增加可直接调试的待办 Demo、Docker Compose、环境变量示例和基础测试。
二、统一响应、错误码和 TraceId
以前的接口只返回 Success、Message 和 Data,前端无法稳定区分参数错误、未授权、资源不存在和业务冲突。这次给 ApiResult 增加了稳定的错误码和 TraceId。
public sealed class ApiResult<T>
{
public bool Success { get; init; }
public int Code { get; init; }
public string? Message { get; init; }
public string? TraceId { get; init; }
public T? Data { get; init; }
public static ApiResult<T> Ok(
T? data,
string? message = null,
string? traceId = null)
=> new()
{
Success = true,
Code = ApiResultCodes.Success,
Message = message,
TraceId = traceId,
Data = data
};
}
常用错误码集中在 ApiResultCodes 中,例如参数错误、未授权、无权限、资源不存在和服务器错误。业务层可以抛出 BusinessException,全局异常中间件会转换成统一响应,不需要每个 Controller 重复写 try-catch。
if (order is null)
throw new BusinessException(
"订单不存在",
ApiResultCodes.NotFound);
请求 TraceId 会写入响应体和 X-Trace-Id 响应头。线上排查时,前端只要提供 TraceId,后端就可以在日志中定位对应请求。
三、参数校验和 Web 生产基线
注册 Controller 时可以加入全局参数校验过滤器:
builder.Services.AddControllers(options =>
options.Filters.Add<ApiValidationFilter>());
模型绑定失败后,接口会直接返回统一的参数错误结构。业务规则校验仍然放在 Service 中,避免把业务逻辑堆进过滤器。
Web 默认管道还补齐了 HTTPS 重定向、CORS 和健康检查:
/health:只检查应用是否能够响应,适合存活探针。/health/ready:额外检查数据库连接,适合就绪探针。- CORS 从
Cors:AllowedOrigins读取允许的前端来源。 - 生产环境建议使用 HTTPS,并明确配置允许的域名,不要长期使用通配来源。
四、跨仓储事务使用 IHQUnitOfWork
单个仓储的新增、修改和删除默认会使用事务。但订单创建通常还要同时写订单明细、库存记录和操作日志,这时必须由业务层明确建立一个外层事务。
public sealed class OrderService(
IHQUnitOfWork unitOfWork,
IBaseRepository<Order, long> orderRepository,
IBaseRepository<OrderItem, long> itemRepository)
{
public async Task CreateAsync(
Order order,
List<OrderItem> items,
CancellationToken cancellationToken = default)
{
await unitOfWork.ExecuteAsync(async token =>
{
// 关闭仓储自身事务,加入 UnitOfWork 外层事务。
var orders = orderRepository.WithoutTransaction();
var orderItems = itemRepository.WithoutTransaction();
await orders.AddAsync(order);
await orderItems.AddRangeAsync(items);
}, cancellationToken: cancellationToken);
}
}
执行边界变成:
BeginTranAsync
OrderRepository.AddAsync
OrderItemRepository.AddRangeAsync
CommitTranAsync
任意一个仓储操作抛出异常,UnitOfWork 都会回滚整个事务。事务内部不要再次调用 WithTransaction(),应使用 WithoutTransaction(),否则容易形成嵌套事务或不一致的提交边界。
IHQUnitOfWork 同时支持返回值和指定隔离级别:
var result = await unitOfWork.ExecuteAsync(
async token =>
{
var orders = orderRepository.WithoutTransaction();
await orders.AddAsync(order);
return new OrderDto(order.Id, order.Amount);
},
isolationLevel: IsolationLevel.ReadCommitted,
cancellationToken: cancellationToken);
数据库事务和 RabbitMQ 消息不是同一个事务。需要保证“数据库成功后消息最终送达”时,应进一步设计 Outbox,而不能把消息发布代码放进数据库事务就认为两者已经具备原子性。
五、HttpClientHelper 和 IHttpClientFactory
框架保留了原有的 HttpClientHelper,并增加统一注册入口:
builder.Services.AddHQHttpClient(builder.Configuration);
底层交给 IHttpClientFactory 管理 HttpClient 生命周期,同时保留超时、最大响应体、并发连接数、请求体日志和 QueryString 日志控制。
var result = await httpClient.GetJsonAsync<ProductDto>(
"/products/1",
headers: new Dictionary<string, string>
{
["Authorization"] = $"Bearer {token}"
},
ct: cancellationToken);
默认响应体上限为 10 MB,请求体和 QueryString 默认不写入日志。若目标 URL 来自用户输入,仍然需要增加目标域名白名单和内网地址拦截,避免 SSRF。
六、缓存和文件存储抽象
本次增加了轻量的 IHQCache 和 IFileStorage。默认缓存使用内存实现,默认文件存储使用本地目录。这样开发环境不需要强制依赖 Redis、MinIO 或 OSS,后续也可以直接替换 Provider。
var cached = await cache.GetAsync<List<ProductDto>>(
"products:list",
cancellationToken);
if (cached is null)
{
cached = await service.LoadFromDatabaseAsync(cancellationToken);
await cache.SetAsync(
"products:list",
cached,
TimeSpan.FromMinutes(1),
cancellationToken);
}
文件存储使用相对路径,并在存储层校验路径不能越过根目录。上传文件时不要直接使用用户提供的原始文件名作为物理路径,应该限制大小、校验扩展名并使用服务端生成的文件名。
七、Quartz 和 RabbitMQ 的稳定性增强
Quartz Cron 任务现在默认使用错失触发后跳过的策略,应用停机期间错过的任务不会在恢复时瞬间集中补跑。需要补偿执行的任务,应单独设计补偿机制。
RabbitMQ 消费端增加了 QoS 预取、消费者注册记录和停止时的资源释放。消息处理成功后 ACK,失败后根据 RequeueOnConsumerError 决定 NACK 是否重新入队。失败消息建议配合死信队列,不要让坏消息无限重试。
await consumers.SubscribeAsync<OrderCreated>(
new RabbitMQQueueDeclareOptions
{
QueueName = "order.created",
EnableDeadLetter = true
},
async (message, ct) =>
{
await HandleAsync(message, ct);
},
cancellationToken);
八、可运行 Demo 和项目结构
项目内置待办 Demo,用来串联 JWT、权限、SqlSugar、UnitOfWork、缓存、文件存储、SignalR、Quartz 和 RabbitMQ。主要目录如下:
HQ.Application/Application/Demo
├── DemoAuthController.cs
├── DemoTodoController.cs
├── DemoFileController.cs
├── DemoMessageController.cs
└── DemoRealtimeController.cs
HQ.Service/Service/DemoTodoService.cs
HQ.Entity/Model/DemoTodo.cs
HQ.Common/ORM/SQLSugar/HQUnitOfWork.cs
本地可以复制 .env.example 为 .env,使用 Docker Compose 启动 SQL Server 和 RabbitMQ,再运行项目:
cp .env.example .env
docker compose up -d
dotnet restore
dotnet build HQserver.sln -c Release
dotnet test HQserver.sln -c Release
dotnet run --project HQ.Application -c Release
开发环境可以通过 Swagger 获取 Demo JWT,再调用待办接口。生产环境的数据库密码、RabbitMQ 密码和 JWT 签名密钥必须通过环境变量或 Secret 注入,不能写进提交文件。
九、测试和后续扩展
项目增加了基础测试项目,覆盖统一响应和 Quartz 任务定义等核心行为。提交前建议至少执行:
dotnet build HQserver.sln -c Release
dotnet test HQserver.sln -c Release
dotnet list HQserver.sln package --vulnerable --include-transitive
这次更新的重点不是增加更多层次,而是让通用能力有统一入口、默认行为更安全、业务层更容易组合。后续可以在当前结构上继续接入 Redis、MinIO/OSS、Outbox、审计字段和软删除,而不需要推翻现有分层。
总结
HQServer 当前的推荐开发路径是:Entity 定义数据结构,Service 组织业务,Controller 负责 HTTP,Common 提供基础设施。单仓储写入直接使用仓储,跨仓储一致性使用 IHQUnitOfWork,实时通知使用 SignalR,异步解耦使用 RabbitMQ,定时执行使用 Quartz。
完整架构说明和各模块示例已经同步到项目 README,本文则作为本次框架更新的专题记录。









