docs: refine tenant repository comments

This commit is contained in:
2025-12-03 23:18:17 +08:00
parent bf88f0e041
commit 17d143a351
4 changed files with 136 additions and 45 deletions

112
AGENTS.md
View File

@@ -47,11 +47,21 @@
## 4. 注释与文档 ## 4. 注释与文档
* **强制 XML 注释**:所有 `public` 的类、方法、属性必须有 `<summary>` * **强制 XML 注释**:所有 `public` 的类、方法、属性必须有 `<summary>`
* **步骤注释**:超过 5 行的业务逻辑,必须分步注释 * **分段逻辑注释 (强制)**
```csharp * **空行必注**:代码中每当出现空行分隔逻辑块时,**必须**在空行后的第一行添加 `//` 注释,简要说明紧接着这段代码的意图或作用。
// 1. 验证库存 * **步骤化**对于稍微复杂的业务逻辑必须结合序号1., 2., ...)进行标记。
// 2. 扣减余额 * **示例**
``` ```csharp
// 1. 验证用户是否存在
var user = await _repo.GetAsync(id);
if (user == null) return NotFound();
// 2. (空行后) 扣减余额逻辑
user.Balance -= amount;
// 3. (空行后) 保存更改
await _unitOfWork.SaveChangesAsync();
```
* **Swagger**:必须开启 JWT 鉴权按钮Request/Response 示例必须清晰。 * **Swagger**:必须开启 JWT 鉴权按钮Request/Response 示例必须清晰。
## 5. 异常处理 (防御性编程) ## 5. 异常处理 (防御性编程)
@@ -142,50 +152,64 @@
5. [ ] **性能陷阱**:是否在循环中查询了数据库 (N+1) 5. [ ] **性能陷阱**:是否在循环中查询了数据库 (N+1)
6. [ ] **精度丢失**Long 类型的 ID 是否转为了 String 6. [ ] **精度丢失**Long 类型的 ID 是否转为了 String
7. [ ] **配置硬编码**:是否直接写死了连接串或密钥? 7. [ ] **配置硬编码**:是否直接写死了连接串或密钥?
8. [ ] **文档注释**:是否给所有 `public` 类/方法/属性添加了 `<summary>`
9. [ ] **逻辑注释**:是否在每个空行分隔的逻辑块上方添加了说明注释?
## 17. .NET 10 / C# 14 现代语法最佳实践(增量) ## 17. 现代语法范式 (.NET 10 / C# 14)
> 2025 年推荐的 20 条语法规范,新增特性优先,保持极简 > **原则**:拥抱新特性以减少样板代码,但严禁牺牲可读性
1. **field 关键字**:属性内直接使用 `field` 处理后备字段,`set => field = value.Trim();`。
2. **空值条件赋值 `?.=`**:仅对象非空时赋值,减少 `if`。
3. **未绑定泛型 nameof**`nameof(List<>)` 获取泛型类型名,无需占位类型参数。
4. **Lambda 参数修饰符**:在 Lambda 中可用 `ref/out/in` 与默认参数,例如 `(ref int x, int bonus = 10) => x += bonus;`。
5. **主构造函数 (Primary Constructor)**:服务/数据类优先 `class Foo(IDep dep, ILogger<Foo> logger) { }`。
6. **record/required/init**DTO 默认用 record关键属性用 `required`;不可变属性用 `init`。
7. **集合表达式与展开**:使用 `[]` 创建集合,`[..other]` 拼接,`str[1..^1]` 进行切片。
8. **模式匹配**:列表模式 `[1, 2, .. var rest]`、属性模式 `{ IsActive: true }`、switch 表达式简化分支。
9. **文件范围命名空间/全局 using**:减少缩进与重复引用;复杂泛型用别名。
10. **顶级语句**Program.cs 保持顶级语句风格。
11. **原始/UTF-8 字面量**:多行文本用 `"""`,性能场景用 `"text"u8`。
12. **不可变命令优先**:命令/DTO 优先用 record 和 `with` 非破坏性拷贝,例如 `command = command with { MerchantId = merchantId };`,避免直接 `command.Property = ...` 带来的副作用。
(其余规则继续遵循上文约束:分层、命名、异步、日志、验证、租户/ID 策略等。) 1. **主构造函数 (Primary Constructor)**
* **强制**:依赖注入场景必须使用 `class Service(IDep dep) { }`。
* **禁止**:在主构造函数类中显式定义 `private readonly` 字段来承接参数(直接使用参数即可)。
2. **对象初始化与不可变性**
* **DTO/Command**:必须使用 `record` 类型。
* **属性定义**:默认使用 `init`;必填项加 `required`;逻辑变更使用 `with` 表达式。
3. **集合表达式**
* **统一**:使用 `[]` 初始化集合。
* **拼接**:使用 `[..array1, ..array2]` 替代 `Concat`。
4. **模式匹配 (Pattern Matching)**
* **替代**:严禁复杂的 `if-else if` 链,强制使用 `switch` 表达式。
* **判空**:使用 `is not null` 替代 `!= null`。
5. **极简语法糖**
* **Field 关键字**:属性后备字段必须使用 `field` 关键字(如 `set => field = value.Trim();`)。
* **空值赋值**:使用 `?.=` 简化判空赋值逻辑。
* **字符串**:多行文本强制用 `"""` (Raw String Literal)。
## 18. .NET 10 极致性能优化最佳实践(增量) ## 18. 极致性能规约 (High Performance)
> 侧重零分配、并发与底层优化,遵循 2025 推荐方案 > **原则**:默认编写“零分配 (Zero-Allocation)”代码,热点路径拒绝 GC 压力
1. **Span/ReadOnlySpan 优先**API 参数尽量用 `ReadOnlySpan<char>` 处理字符串/切片,避免 Substring/复制。
2. **栈分配与数组池**:小缓冲用 `stackalloc`,大缓冲统一用 `ArrayPool<T>.Shared`,禁止直接 `new` 大数组。
3. **UTF-8 字面量**:常量字节使用 `"text"u8`,避免运行时编码。
4. **避免装箱**:热点路径规避隐式装箱,必要时用 `ref struct` 约束栈分配。
5. **Frozen 集合**:只读查找表用 `FrozenDictionary/FrozenSet`,初始化后不再修改。
6. **SearchValues SIMD 查找**Span 内多字符搜索用 `SearchValues.Create(...)` + `ContainsAny`。
7. **预设集合容量**`List/Dictionary` 预知规模必须指定 `Capacity`。
8. **ValueTask 热点返回**:可能同步完成的异步返回 `ValueTask<T>`,减少 Task 分配。
9. **Parallel.ForEachAsync 控并发**I/O 并发用 Parallel.ForEachAsync 控制并行度,替代粗暴 Task.WhenAll。
10. **避免 Task.Run**:在 ASP.NET Core 请求中不使用 Task.Run 做后台工作,改用 IHostedService 或 Channel 模式。
11. **Channel<T> 代替锁**:多线程数据传递优先使用 Channels实现无锁生产者-消费者。
12. **NativeAOT/PGO/向量化**:微服务/工具开启 NativeAOT保留动态 PGO计算密集场景考虑 System.Runtime.Intrinsics。
13. **System.Text.Json + 源生成器**:全面替换 Newtonsoft.Json使用 `[JsonSerializable]` + 生成的 `JsonSerializerContext`,兼容 NativeAOT零反射。
14. **Pipelines 处理流**TCP/文件流解析使用 `PipeReader/PipeWriter`,获得零拷贝与缓冲管理。
15. **HybridCache**:内存+分布式缓存统一用 HybridCache利用防击穿合并并发请求。
## 19. 架构优化(增量) 1. **内存管理**
> 架构优化方案 * **字符串处理**API 参数解析层**强制**使用 `ReadOnlySpan<char>`,严禁使用 `Substring`。
1. **Chiseled 容器优先**:生产镜像基于 `mcr.microsoft.com/dotnet/runtime-deps:10.0-jammy-chiseled`,无 Shell、非 root缩小攻击面符合零信任要求 * **数组分配**:小块内存 (<1KB) 使用 `stackalloc`;大块内存 (>1KB) 必须使用 `ArrayPool<T>.Shared`
2. **默认集成 OpenTelemetry**:架构内置 OTel统一通过 OTLP 导出 Metrics/Traces/Logs避免依赖专有 APM 探针。 2. **并发模型**
3. **内部同步调用首选 gRPC**:微服务间禁止 JSON over HTTP同步调用统一使用 gRPC配合 Protobuf 源生成器获取强类型契约与更小载荷 * **无锁编程**:进程内生产者-消费者模型**必须**使用 `System.Threading.Channels`,严禁使用 `lock` 或 `BlockingCollection`
4. **Outbox 模式强制**:处理领域事件时,事件记录必须与业务数据同事务写入 Outbox 表;后台 Worker 轮询 Outbox 再推送 MQRabbitMQ/Kafka禁止事务提交后直接发消息以避免不一致 * **并行控制**I/O 密集型并发必须使用 `Parallel.ForEachAsync` 并配置 `MaxDegreeOfParallelism`
5. **共享资源必加分布式锁**:涉及库存扣减、定时任务抢占等共享资源时,必须引入分布式锁(如 Redis RedLock防止并发竞争与脏写 * **异步返回**:热点路径下,若可能同步完成,返回类型必须为 `ValueTask<T>`
3. **序列化与查找**
* **JSON****全线废弃** Newtonsoft.Json。必须使用 `System.Text.Json` 配合源生成器 (`[JsonSerializable]`) 以支持 NativeAOT。
* **静态集合**:只读字典/集合**必须**使用 `FrozenDictionary` / `FrozenSet`。
* **SIMD 搜索**:多字符匹配场景使用 `SearchValues` 替代 `IndexOfAny`。
4. **缓存架构**
* **统一入口**:必须使用 `HybridCache` (或类似多级缓存抽象),禁止直接操作 `IDistributedCache` 以避免缓存击穿。
## 19. 云原生架构规范 (Architecture)
> **原则**:默认零信任,默认分布式,默认可观测。
1. **容器与部署**
* **基底镜像**:生产环境**强制**使用 Chiseled 镜像 (`runtime-deps:10.0-jammy-chiseled`),无 Shell、无 Root最大化安全。
* **健康检查**:必须包含 Liveness (存活) 和 Readiness (就绪) 探针。
2. **服务间通信**
* **同步调用**:内部微服务间**严禁**使用 REST/JSON**必须**使用 gRPC (Protobuf)。
* **契约管理**Proto 文件必须作为单一事实来源 (Single Source of Truth) 统一管理。
3. **数据一致性 (关键)**
* **Outbox 模式**:领域事件发布**严禁**直接调用 MQ。必须在同一数据库事务中写入 `Outbox` 表,由独立 Worker 异步推送。
* **幂等性**:所有消费者 (Consumer) 必须实现基于 `MessageId` 的幂等处理逻辑。
4. **可观测性 (Observability)**
* **OpenTelemetry**:严禁依赖特定厂商 SDK。必须统一输出 OTLP 标准格式 (Metrics/Logs/Traces)。
* **关联ID**:确保 `TraceId` 在 HTTP Headers 和 MQ Metadata 中全程透传。
5. **并发控制**
* **分布式锁**:任何涉及跨实例的资源竞争(如库存扣减、定时任务),**必须**使用 Redis RedLock 或同等机制。
--- ---

View File

@@ -11,32 +11,46 @@ namespace TakeoutSaaS.Domain.Tenants.Repositories;
public interface ITenantPackageRepository public interface ITenantPackageRepository
{ {
/// <summary> /// <summary>
/// 按 ID 查询套餐。 /// 按套餐 ID 查询套餐。
/// </summary> /// </summary>
/// <param name="id">套餐 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配的套餐实体,未找到返回 null。</returns>
Task<TenantPackage?> FindByIdAsync(long id, CancellationToken cancellationToken = default); Task<TenantPackage?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 按关键词与启用状态搜索套餐。 /// 按关键词与启用状态搜索套餐。
/// </summary> /// </summary>
/// <param name="keyword">名称或描述关键字,空则不按关键字过滤。</param>
/// <param name="isActive">启用状态,空则不按状态过滤。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>符合条件的套餐列表。</returns>
Task<IReadOnlyList<TenantPackage>> SearchAsync(string? keyword, bool? isActive, CancellationToken cancellationToken = default); Task<IReadOnlyList<TenantPackage>> SearchAsync(string? keyword, bool? isActive, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 新增套餐。 /// 新增套餐。
/// </summary> /// </summary>
/// <param name="package">套餐实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddAsync(TenantPackage package, CancellationToken cancellationToken = default); Task AddAsync(TenantPackage package, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 更新套餐。 /// 更新套餐。
/// </summary> /// </summary>
/// <param name="package">套餐实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpdateAsync(TenantPackage package, CancellationToken cancellationToken = default); Task UpdateAsync(TenantPackage package, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 删除套餐。 /// 删除套餐。
/// </summary> /// </summary>
/// <param name="id">套餐 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
Task DeleteAsync(long id, CancellationToken cancellationToken = default); Task DeleteAsync(long id, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 持久化。 /// 持久化。
/// </summary> /// </summary>
/// <param name="cancellationToken">取消标记。</param>
Task SaveChangesAsync(CancellationToken cancellationToken = default); Task SaveChangesAsync(CancellationToken cancellationToken = default);
} }

View File

@@ -14,25 +14,37 @@ public interface ITenantQuotaUsageRepository
/// <summary> /// <summary>
/// 获取租户指定配额的使用情况。 /// 获取租户指定配额的使用情况。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="quotaType">配额类型。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用记录,未初始化则返回 null。</returns>
Task<TenantQuotaUsage?> FindAsync(long tenantId, TenantQuotaType quotaType, CancellationToken cancellationToken = default); Task<TenantQuotaUsage?> FindAsync(long tenantId, TenantQuotaType quotaType, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 按租户批量获取配额使用记录。 /// 按租户批量获取配额使用记录。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>该租户的所有配额使用记录。</returns>
Task<IReadOnlyList<TenantQuotaUsage>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TenantQuotaUsage>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 新增配额使用记录。 /// 新增配额使用记录。
/// </summary> /// </summary>
/// <param name="usage">配额使用实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default); Task AddAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 更新配额使用记录。 /// 更新配额使用记录。
/// </summary> /// </summary>
/// <param name="usage">配额使用实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpdateAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default); Task UpdateAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 持久化。 /// 持久化。
/// </summary> /// </summary>
/// <param name="cancellationToken">取消标记。</param>
Task SaveChangesAsync(CancellationToken cancellationToken = default); Task SaveChangesAsync(CancellationToken cancellationToken = default);
} }

View File

@@ -13,11 +13,18 @@ public interface ITenantRepository
/// <summary> /// <summary>
/// 依据 ID 获取租户。 /// 依据 ID 获取租户。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户实体,未找到返回 null。</returns>
Task<Tenant?> FindByIdAsync(long tenantId, CancellationToken cancellationToken = default); Task<Tenant?> FindByIdAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 按状态与关键词查询租户列表。 /// 按状态与关键词查询租户列表。
/// </summary> /// </summary>
/// <param name="status">租户状态,为空不按状态过滤。</param>
/// <param name="keyword">名称或编码关键字,为空不按关键字过滤。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>符合条件的租户列表。</returns>
Task<IReadOnlyList<Tenant>> SearchAsync( Task<IReadOnlyList<Tenant>> SearchAsync(
TenantStatus? status, TenantStatus? status,
string? keyword, string? keyword,
@@ -26,70 +33,104 @@ public interface ITenantRepository
/// <summary> /// <summary>
/// 新增租户。 /// 新增租户。
/// </summary> /// </summary>
/// <param name="tenant">租户实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddTenantAsync(Tenant tenant, CancellationToken cancellationToken = default); Task AddTenantAsync(Tenant tenant, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 更新租户。 /// 更新租户。
/// </summary> /// </summary>
/// <param name="tenant">租户实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpdateTenantAsync(Tenant tenant, CancellationToken cancellationToken = default); Task UpdateTenantAsync(Tenant tenant, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 判断编码是否存在。 /// 判断编码是否存在。
/// </summary> /// </summary>
/// <param name="code">租户编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsByCodeAsync(string code, CancellationToken cancellationToken = default); Task<bool> ExistsByCodeAsync(string code, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 获取实名资料。 /// 获取实名资料。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>实名资料实体,未提交返回 null。</returns>
Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default); Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 新增或更新实名资料。 /// 新增或更新实名资料。
/// </summary> /// </summary>
/// <param name="profile">实名资料实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpsertVerificationProfileAsync(TenantVerificationProfile profile, CancellationToken cancellationToken = default); Task UpsertVerificationProfileAsync(TenantVerificationProfile profile, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 获取当前订阅。 /// 获取当前订阅。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>当前有效订阅,若无则 null。</returns>
Task<TenantSubscription?> GetActiveSubscriptionAsync(long tenantId, CancellationToken cancellationToken = default); Task<TenantSubscription?> GetActiveSubscriptionAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 依据订阅 ID 查询。 /// 依据订阅 ID 查询。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="subscriptionId">订阅 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅实体,未找到返回 null。</returns>
Task<TenantSubscription?> FindSubscriptionByIdAsync(long tenantId, long subscriptionId, CancellationToken cancellationToken = default); Task<TenantSubscription?> FindSubscriptionByIdAsync(long tenantId, long subscriptionId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 新增订阅。 /// 新增订阅。
/// </summary> /// </summary>
/// <param name="subscription">订阅实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default); Task AddSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 更新订阅。 /// 更新订阅。
/// </summary> /// </summary>
/// <param name="subscription">订阅实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpdateSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default); Task UpdateSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 记录订阅历史。 /// 记录订阅历史。
/// </summary> /// </summary>
/// <param name="history">订阅历史实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddSubscriptionHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default); Task AddSubscriptionHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 获取订阅历史。 /// 获取订阅历史。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅历史列表。</returns>
Task<IReadOnlyList<TenantSubscriptionHistory>> GetSubscriptionHistoryAsync(long tenantId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TenantSubscriptionHistory>> GetSubscriptionHistoryAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 新增审核日志。 /// 新增审核日志。
/// </summary> /// </summary>
/// <param name="log">审核日志实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddAuditLogAsync(TenantAuditLog log, CancellationToken cancellationToken = default); Task AddAuditLogAsync(TenantAuditLog log, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 查询审核日志。 /// 查询审核日志。
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>审核日志列表。</returns>
Task<IReadOnlyList<TenantAuditLog>> GetAuditLogsAsync(long tenantId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TenantAuditLog>> GetAuditLogsAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// 持久化。 /// 持久化。
/// </summary> /// </summary>
/// <param name="cancellationToken">取消标记。</param>
Task SaveChangesAsync(CancellationToken cancellationToken = default); Task SaveChangesAsync(CancellationToken cancellationToken = default);
} }