feat: 添加 Gateway 路由实体到 Platform
- 新增 GatewayAggregate 领域实体 (GwTenant, GwTenantRoute, GwServiceInstance) - 新增 IRouteStore, RouteStore, IInstanceStore, InstanceStore - 新增 IRouteManager, RouteManager - 合并 GatewayDbContext 到 PlatformDbContext - 统一 Extensions.AddPlatformCore 注册所有服务
This commit is contained in:
parent
7877f89d35
commit
1b8c937aa4
47
.planning/ROADMAP.md
Normal file
47
.planning/ROADMAP.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Roadmap
|
||||
|
||||
**Project:** Fengling.Platform
|
||||
**Milestone:** v1.0 - Platform Foundation
|
||||
**Status:** In Progress
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Gateway Routing Migration
|
||||
|
||||
**Goal:** Migrate YARP gateway routing entities from fengling-gateway to Platform project with unified management
|
||||
|
||||
**Status:** ○ Planned
|
||||
|
||||
**Requirements:**
|
||||
- [ ] GATEWAY-01: GwTenant entity and management
|
||||
- [ ] GATEWAY-02: GwTenantRoute entity and management
|
||||
- [ ] GATEWAY-03: GwServiceInstance entity and management
|
||||
- [ ] GATEWAY-04: Extensions for IoC registration
|
||||
- [ ] GATEWAY-05: Database migrations
|
||||
|
||||
**Plans:**
|
||||
- [ ] 01-01-PLAN.md — Domain entities (GwTenant, GwTenantRoute, GwServiceInstance)
|
||||
- [ ] 01-02-PLAN.md — Infrastructure (Store, Manager, DbContext)
|
||||
- [ ] 01-03-PLAN.md — Extensions and IoC integration
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Platform Core (Future)
|
||||
|
||||
**Goal:** Complete multi-tenant platform infrastructure
|
||||
|
||||
**Status:** ○ Planned
|
||||
|
||||
**Requirements:**
|
||||
- [ ] USER-01: User management
|
||||
- [ ] USER-02: Role and permissions
|
||||
- [ ] AUTH-01: Authentication flows
|
||||
- [ ] AUTH-02: Authorization
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Gateway routing entities will be migrated from `../fengling-gateway/src/Models/`
|
||||
- Pattern: Manager + Store (same as Tenant management)
|
||||
- Extensions for quick IoC installation via `AddPlatformCore<TContext>()`
|
||||
41
.planning/STATE.md
Normal file
41
.planning/STATE.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Project State
|
||||
|
||||
**Last Updated:** 2026-02-28
|
||||
|
||||
## Status
|
||||
|
||||
- **Phase:** Planning new gateway routing feature
|
||||
- **Milestone:** v1.0 - Platform Foundation
|
||||
|
||||
## Project Context
|
||||
|
||||
This is the Fengling.Platform project - a multi-tenant identity and authentication infrastructure.
|
||||
|
||||
### Current State
|
||||
|
||||
- Platform layer initialized with Tenant, User, Role aggregates
|
||||
- Manager + Store pattern established (ITenantStore, ITenantManager)
|
||||
- Extensions for DI registration (AddPlatformCore<TContext>)
|
||||
- PostgreSQL database with EF Core migrations
|
||||
|
||||
### Source for Migration
|
||||
|
||||
**fengling-gateway** project (parent directory):
|
||||
- `GwTenant` - 租户实体
|
||||
- `GwTenantRoute` - 路由配置实体
|
||||
- `GwServiceInstance` - 服务实例实体
|
||||
- GatewayDbContext with PostgreSQL
|
||||
|
||||
## Decisions
|
||||
|
||||
- Using Manager + Store pattern from existing Tenant implementation
|
||||
- Extensions-based DI registration for quick IoC setup
|
||||
- Align with existing Platform coding conventions
|
||||
|
||||
## Blockers
|
||||
|
||||
None
|
||||
|
||||
## Pending
|
||||
|
||||
- Plan and implement gateway routing migration
|
||||
119
.planning/codebase/ARCHITECTURE.md
Normal file
119
.planning/codebase/ARCHITECTURE.md
Normal file
@ -0,0 +1,119 @@
|
||||
# 架构
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## 模式概述
|
||||
|
||||
**总体:** 清洁架构 + DDD 启发的聚合模式
|
||||
|
||||
**关键特性:**
|
||||
- 使用 ASP.NET Core Identity 模式的多租户平台基础设施
|
||||
- 领域驱动设计(DDD)聚合用于业务实体
|
||||
- Manager + Store 模式用于数据访问(ASP.NET Core Identity 风格)
|
||||
- 通过 EF Core 的仓储抽象
|
||||
- 多租户,TenantInfo 值对象嵌入在用户实体中
|
||||
|
||||
## 层级
|
||||
|
||||
**领域层 (`Fengling.Platform.Domain`):**
|
||||
- 目的: 核心业务实体和领域逻辑
|
||||
- 位置: `Fengling.Platform.Domain/AggregatesModel/`
|
||||
- 包含: 聚合、实体、值对象
|
||||
- 依赖: Microsoft.AspNetCore.Identity(仅接口)
|
||||
- 被使用: 基础设施层
|
||||
|
||||
**基础设施层 (`Fengling.Platform.Infrastructure`):**
|
||||
- 目的: 数据持久化、外部集成、应用服务
|
||||
- 位置: `Fengling.Platform.Infrastructure/`
|
||||
- 包含: DbContext、TenantStore、TenantManager、EF 配置、迁移
|
||||
- 依赖: Fengling.Platform.Domain、EF Core、Identity
|
||||
- 被使用: 消费应用(如 Console 等)
|
||||
|
||||
## 聚合
|
||||
|
||||
**TenantAggregate:**
|
||||
- 实体: `Tenant` - 表示租户组织
|
||||
- 值对象: `TenantInfo` - 租户上下文(TenantId、租户代码、租户名称)
|
||||
- 模式: 贫血模型(仅属性,无业务逻辑)
|
||||
- 位置: `Fengling.Platform.Domain/AggregatesModel/TenantAggregate/`
|
||||
|
||||
**UserAggregate:**
|
||||
- 实体: `ApplicationUser` - 继承 `IdentityUser<long>`
|
||||
- 包含: RealName、TenantInfo、时间戳、软删除标志
|
||||
- 位置: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/`
|
||||
|
||||
**RoleAggregate:**
|
||||
- 实体: `ApplicationRole` - 继承 `IdentityRole<long>`
|
||||
- 包含: Description、TenantId、IsSystem、DisplayName、Permissions
|
||||
- 位置: `Fengling.Platform.Domain/AggregatesModel/RoleAggregate/`
|
||||
|
||||
## 数据流
|
||||
|
||||
**租户解析流程:**
|
||||
1. 用户认证 → 加载 ApplicationUser 及 TenantInfo
|
||||
2. TenantInfo(TenantId、租户代码、租户名称)嵌入用户实体
|
||||
3. 所有租户范围查询按 TenantInfo.TenantId 过滤
|
||||
|
||||
**租户 CRUD 流程:**
|
||||
1. Controller/Service 调用 `ITenantManager`
|
||||
2. `TenantManager` 委托给 `ITenantStore`
|
||||
3. `TenantStore` 通过 `PlatformDbContext` 执行 EF Core 操作
|
||||
4. 更改持久化到 PostgreSQL 数据库
|
||||
|
||||
## 关键抽象
|
||||
|
||||
**ITenantStore:**
|
||||
- 目的: Tenant 实体的数据访问操作
|
||||
- 接口位置: `Fengling.Platform.Infrastructure/ITenantStore.cs`
|
||||
- 实现: `TenantStore<TContext>`
|
||||
- 模式: 自定义 Store 模式(ASP.NET Core Identity 风格)
|
||||
|
||||
**ITenantManager:**
|
||||
- 目的: 租户操作的 应用服务
|
||||
- 接口位置: `Fengling.Platform.Infrastructure/TenantManager.cs`
|
||||
- 实现: `TenantManager`
|
||||
- 委托给 ITenantStore
|
||||
|
||||
**PlatformDbContext:**
|
||||
- 目的: EF Core 数据库上下文
|
||||
- 位置: `Fengling.Platform.Infrastructure/PlatformDbContext.cs`
|
||||
- 继承: `IdentityDbContext<ApplicationUser, ApplicationRole, long>`
|
||||
- 包含: Tenant、AccessLog、AuditLog DbSets
|
||||
|
||||
## 入口点
|
||||
|
||||
**依赖注入注册:**
|
||||
- 方法: `Extensions.AddPlatformCore<TContext>()`
|
||||
- 位置: `Fengling.Platform.Infrastructure/Extensions.cs`
|
||||
- 注册: DbContext、ITenantStore、ITenantManager
|
||||
|
||||
**数据库上下文:**
|
||||
- 类型: `PlatformDbContext`
|
||||
- 数据库: PostgreSQL(通过 Npgsql)
|
||||
- 迁移位置: `Fengling.Platform.Infrastructure/Migrations/`
|
||||
|
||||
## 错误处理
|
||||
|
||||
**策略:** 标准 ASP.NET Core Identity 结果模式
|
||||
|
||||
**模式:**
|
||||
- CRUD 操作返回 `IdentityResult`(成功/失败)
|
||||
- 空检查返回 `Task.FromResult<T>(null)` 表示未找到
|
||||
- 当前未实现自定义异常处理层
|
||||
|
||||
## 横切关注点
|
||||
|
||||
**日志:** 此层未明确配置
|
||||
|
||||
**验证:** 未明确实现(依赖 ASP.NET Core Identity)
|
||||
|
||||
**认证:** 使用 ASP.NET Core Identity + OpenIddict(从迁移中可见)
|
||||
|
||||
**多租户:**
|
||||
- 方法: 按 TenantInfo.TenantId 过滤
|
||||
- TenantInfo 作为拥有实体嵌入 ApplicationUser
|
||||
- 通过实体上的 IsDeleted 标志进行软删除
|
||||
|
||||
---
|
||||
|
||||
*架构分析: 2026-02-28*
|
||||
196
.planning/codebase/CONCERNS.md
Normal file
196
.planning/codebase/CONCERNS.md
Normal file
@ -0,0 +1,196 @@
|
||||
# 代码库问题
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## 技术债务
|
||||
|
||||
**租户软删除未实现:**
|
||||
- 问题: `Tenant` 实体有 `IsDeleted` 属性,但 `TenantStore` 删除操作执行硬删除
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs`、`Fengling.Platform.Infrastructure/TenantStore.cs`
|
||||
- 影响: 永久删除租户,破坏引用完整性和审计跟踪
|
||||
- 修复方法: 在 `TenantStore.DeleteAsync()` 中实现软删除并添加全局查询过滤器
|
||||
|
||||
**ApplicationUser 软删除未强制:**
|
||||
- 问题: `ApplicationUser.IsDeleted` 属性存在,但没有 EF Core 全局查询过滤器强制执行
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/ApplicationUser.cs`、`Fengling.Platform.Infrastructure/PlatformDbContext.cs`
|
||||
- 影响: 已删除用户仍可被查询和认证
|
||||
- 修复方法: 在 `PlatformDbContext.OnModelCreating` 中为 `ApplicationUser` 添加 `HasQueryFilter`
|
||||
|
||||
**混合 DateTime 使用:**
|
||||
- 问题: `Tenant` 使用 `DateTime`,而 `ApplicationUser`/`ApplicationRole` 使用 `DateTimeOffset`
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs`、`Fengling.Platform.Domain/AggregatesModel/UserAggregate/ApplicationUser.cs`、`Fengling.Platform.Domain/AggregatesModel/RoleAggregate/ApplicationRole.cs`
|
||||
- 影响: 实体间时区不一致,可能出现 UTC/混合时区 bug
|
||||
- 修复方法: 将所有时间属性统一为 `DateTimeOffset`
|
||||
|
||||
**重复的 TenantId 数据类型:**
|
||||
- 问题: `AccessLog.TenantId` 和 `AuditLog.TenantId` 是 `string?`,而 `Tenant.Id` 是 `long`
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/AccessLog.cs`、`Fengling.Platform.Domain/AggregatesModel/UserAggregate/AuditLog.cs`
|
||||
- 影响: 类型不匹配,可能数据损坏,查询问题
|
||||
- 修复方法: 改为 `long?` 以匹配 `Tenant.Id` 类型
|
||||
|
||||
**未配置 RowVersion 用于乐观并发:**
|
||||
- 问题: `Tenant.RowVersion` 属性存在,但没有 EF Core 并发令牌配置
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs`、`Fengling.Platform.Infrastructure/Configurations/TenantConfiguration.cs`
|
||||
- 影响: 乐观并发更新可能在未检测的情况下覆盖更改
|
||||
- 修复方法: 在配置中为 `RowVersion` 属性添加 `.IsRowVersion()`
|
||||
|
||||
---
|
||||
|
||||
## 已知 Bug
|
||||
|
||||
**租户 Store 泛型约束问题:**
|
||||
- 问题: `TenantStore<TContext>` 需要泛型参数,但 `ITenantStore` 接口是非泛型的,使 DI 注册变得笨拙
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`、`Fengling.Platform.Infrastructure/ITenantStore.cs`
|
||||
- 触发: 注册 `TenantStore<PlatformDbContext>` vs `ITenantStore`
|
||||
- 变通方案: 使用工厂模式或开放泛型注册
|
||||
|
||||
**ApplicationUser 导航属性缺失:**
|
||||
- 问题: `ApplicationUser` 没有到 `Tenant` 的导航属性,强制通过 `TenantInfo` 进行连接
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/ApplicationUser.cs`
|
||||
- 影响: 无法在用户查询中轻松预加载租户数据
|
||||
- 修复方法: 添加 `public Tenant? Tenant { get; set; }` 并正确配置
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑
|
||||
|
||||
**默认查询中无租户隔离:**
|
||||
- 风险: `TenantStore.GetAllAsync()` 返回所有租户,无授权检查
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`
|
||||
- 当前缓解: 无
|
||||
- 建议: 在所有列表操作中添加授权检查或实现租户范围查询
|
||||
|
||||
**角色权限序列化:**
|
||||
- 风险: `ApplicationRole.Permissions` 是 `List<string>?` - 无加密或验证
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/RoleAggregate/ApplicationRole.cs`
|
||||
- 当前缓解: 无
|
||||
- 建议: 考虑带验证的结构化权限模型
|
||||
|
||||
**访问/审计日志敏感数据:**
|
||||
- 风险: `AccessLog` 将 `RequestData` 和 `ResponseData` 存储为纯字符串,可能包含 PII/密钥
|
||||
- 文件: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/AccessLog.cs`
|
||||
- 当前缓解: 无
|
||||
- 建议: 实现数据脱敏或从日志中排除敏感字段
|
||||
|
||||
---
|
||||
|
||||
## 性能瓶颈
|
||||
|
||||
**租户查找无缓存:**
|
||||
- 问题: 每次租户检查都查询数据库 - `FindByIdAsync`、`FindByTenantCodeAsync`
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`
|
||||
- 原因: 无分布式或内存缓存
|
||||
- 改进路径: 为租户查找添加 `IMemoryCache` 或分布式缓存
|
||||
|
||||
**GetAllAsync 加载整表:**
|
||||
- 问题: `TenantStore.GetAllAsync()` 将所有租户加载到内存中使用 `ToListAsync()`
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`(第 34 行)
|
||||
- 原因: `GetAllAsync` 无分页支持
|
||||
- 改进路径: 始终使用分页查询;弃用 `GetAllAsync`
|
||||
|
||||
**重复查询逻辑:**
|
||||
- 问题: 过滤逻辑在 `GetPagedAsync` 和 `GetCountAsync` 中重复
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`(第 40-72 行)
|
||||
- 原因: 无查询构建器抽象
|
||||
- 改进路径: 提取到共享谓词构建器
|
||||
|
||||
---
|
||||
|
||||
## 脆弱区域
|
||||
|
||||
**租户 Store 删除级联:**
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`(第 90-95 行)
|
||||
- 为何脆弱: 硬删除不检查相关用户/角色 - 可能使数据孤立
|
||||
- 安全修改: 删除前添加引用完整性检查
|
||||
- 测试覆盖: 未找到级联删除场景的测试
|
||||
|
||||
**TenantCode 更改无验证:**
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`(第 135-139 行)、`TenantManager.cs`(第 70-74 行)
|
||||
- 为何脆弱: `SetTenantCodeAsync` 在更新前不检查唯一性
|
||||
- 安全修改: 持久化前添加唯一性验证
|
||||
- 测试覆盖: 未找到重复代码场景的测试
|
||||
|
||||
**通用 Store 模式不完整:**
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`
|
||||
- 为何脆弱: `TenantStore<TContext>` 实现 `ITenantStore` 但无基类 - 复制了 ASP.NET Identity 的模式
|
||||
- 安全修改: 考虑继承 `TenantStoreBase` 或正确添加接口实现
|
||||
- 测试覆盖: 未找到单元测试
|
||||
|
||||
---
|
||||
|
||||
## 扩展限制
|
||||
|
||||
**数据库:**
|
||||
- 当前容量: 通过 EF Core 的单个 PostgreSQL 实例
|
||||
- 限制: 无只读副本配置,仅垂直扩展
|
||||
- 扩展路径: 实现读写分离,添加连接池调优
|
||||
|
||||
**日志:**
|
||||
- 当前容量: 访问/审计日志写入与实体相同的数据库
|
||||
- 限制: 高容量日志会降低事务性能
|
||||
- 扩展路径: 实现带后台队列的异步日志,考虑单独日志存储
|
||||
|
||||
---
|
||||
|
||||
## 有风险的依赖
|
||||
|
||||
**NetCorePal.Extensions 包:**
|
||||
- 风险: 来自 `NetCorePal` 命名空间的自定义/内部包
|
||||
- 影响: 版本兼容性问题,可能破坏性变更
|
||||
- 迁移计划: 监控包发布,维护版本锁定
|
||||
|
||||
**OpenIddict.EntityFrameworkCore:**
|
||||
- 风险: 复杂配置的 OpenID 提供程序
|
||||
- 影响: 数据库模式变更需要迁移
|
||||
- 迁移计划: 保持迁移更新,升级时审查破坏性变更
|
||||
|
||||
**net10.0 目标:**
|
||||
- 风险: .NET 10 是未来版本(当前: .NET 8/9)
|
||||
- 影响: 稳定性和 LTS 担忧
|
||||
- 迁移计划: 目标稳定 LTS(.NET 8)直到 .NET 10 发布
|
||||
|
||||
---
|
||||
|
||||
## 缺失的关键功能
|
||||
|
||||
**无租户过滤中间件:**
|
||||
- 问题: 无中间件从请求中提取租户并强制隔离
|
||||
- 阻塞: API 级别的多租户数据隔离
|
||||
|
||||
**无租户订阅/配额强制:**
|
||||
- 问题: 配置了 `MaxUsers` 但在用户创建时从不检查
|
||||
- 阻塞: 防止租户超出用户限制
|
||||
|
||||
**无租户变更审计跟踪:**
|
||||
- 问题: 创建/更新/删除租户时无自动日志记录
|
||||
- 阻塞: 合规性和变更跟踪
|
||||
|
||||
**无用户邀请系统:**
|
||||
- 问题: 多租户用户入驻无邀请流程
|
||||
- 阻塞: 受控用户注册
|
||||
|
||||
---
|
||||
|
||||
## 测试覆盖缺口
|
||||
|
||||
**未找到单元测试:**
|
||||
- 未测试: 所有核心功能
|
||||
- 文件: 解决方案中未检测到测试项目
|
||||
- 风险: 租户管理、认证和授权中的 bug 未被检测
|
||||
- 优先级: **高**
|
||||
|
||||
**租户 CRUD 操作:**
|
||||
- 未测试: 租户的创建、读取、更新、删除
|
||||
- 文件: `Fengling.Platform.Infrastructure/TenantManager.cs`、`TenantStore.cs`
|
||||
- 风险: 业务逻辑 bug 隐藏
|
||||
- 优先级: **高**
|
||||
|
||||
**多租户隔离:**
|
||||
- 未测试: 跨租户数据访问预防
|
||||
- 文件: 所有查询操作
|
||||
- 风险: 租户数据泄露导致安全漏洞
|
||||
- 优先级: **关键**
|
||||
|
||||
---
|
||||
|
||||
*问题审计: 2026-02-28*
|
||||
215
.planning/codebase/CONVENTIONS.md
Normal file
215
.planning/codebase/CONVENTIONS.md
Normal file
@ -0,0 +1,215 @@
|
||||
# 代码约定
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## 命名模式
|
||||
|
||||
### 文件
|
||||
- **类/记录:** PascalCase(如 `ApplicationUser.cs`、`TenantManager.cs`、`TenantInfo.cs`)
|
||||
- **枚举:** PascalCase(如 `TenantStatus`,定义在 `Tenant.cs` 中)
|
||||
- **接口:** PascalCase + "I" 前缀(如 `ITenantStore.cs`、`ITenantManager.cs`)
|
||||
- **配置:** PascalCase + "Configuration" 后缀(如 `TenantConfiguration.cs`)
|
||||
|
||||
### 目录
|
||||
- **聚合文件夹:** PascalCase(如 `UserAggregate/`、`TenantAggregate/`、`RoleAggregate/`)
|
||||
- **用途文件夹:** PascalCase(如 `Configurations/`、`Migrations/`)
|
||||
|
||||
### 命名空间
|
||||
- **模式:** `Fengling.Platform.{层级}.{聚合}.{组件}`
|
||||
- **示例:**
|
||||
- `Fengling.Platform.Domain.AggregatesModel.TenantAggregate`
|
||||
- `Fengling.Platform.Domain.AggregatesModel.UserAggregate`
|
||||
- `Fengling.Platform.Infrastructure`
|
||||
|
||||
### 类型
|
||||
|
||||
| 类型 | 模式 | 示例 |
|
||||
|------|------|------|
|
||||
| 类 | PascalCase | `TenantManager`, `PlatformDbContext` |
|
||||
| 接口 | I + PascalCase | `ITenantManager`, `ITenantStore` |
|
||||
| 记录 | PascalCase | `TenantInfo` |
|
||||
| 枚举 | PascalCase | `TenantStatus` |
|
||||
| 枚举值 | PascalCase | `Active`, `Inactive`, `Frozen` |
|
||||
| 属性 | PascalCase | `TenantCode`, `CreatedAt`, `IsDeleted` |
|
||||
| 方法 | PascalCase | `FindByIdAsync`, `GetAllAsync` |
|
||||
| 参数 | camelCase | `tenantId`, `tenantCode`, `cancellationToken` |
|
||||
| 私有字段 | camelCase | `_context`, `_tenants` |
|
||||
|
||||
## 代码风格
|
||||
|
||||
### 项目配置
|
||||
- **目标框架:** .NET 10.0
|
||||
- **隐式 using:** 启用
|
||||
- **可空:** 启用
|
||||
- **文档生成:** 启用(`<GenerateDocumentationFile>true</GenerateDocumentationFile>`)
|
||||
- **集中包管理:** 启用(`<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>`)
|
||||
|
||||
### 格式化
|
||||
- **大括号:** K&R 风格(开括号在同一行)
|
||||
- **缩进:** 标准 Visual Studio 默认(4 空格)
|
||||
- **行尾:** 平台默认(Unix LF,Windows CRLF)
|
||||
|
||||
### 全局 Using
|
||||
|
||||
Domain 项目 (`Fengling.Platform.Domain/GlobalUsings.cs`):
|
||||
```csharp
|
||||
global using NetCorePal.Extensions.Domain;
|
||||
global using NetCorePal.Extensions.Primitives;
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
```
|
||||
|
||||
Infrastructure 项目 (`Fengling.Platform.Infrastructure/GlobalUsings.cs`):
|
||||
```csharp
|
||||
global using NetCorePal.Extensions.Domain;
|
||||
global using NetCorePal.Extensions.Primitives;
|
||||
global using NetCorePal.Extensions.Repository;
|
||||
global using NetCorePal.Extensions.Repository.EntityFrameworkCore;
|
||||
global using MediatR;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
```
|
||||
|
||||
## 导入组织
|
||||
|
||||
### 顺序
|
||||
1. 系统命名空间(通过 ImplicitUsings 隐式)
|
||||
2. 外部包(MediatR、EF Core)
|
||||
3. 领域命名空间(本地项目)
|
||||
4. 基础设施命名空间(本地项目)
|
||||
|
||||
### 完全限定
|
||||
- 需要时使用 `Microsoft.AspNetCore.Identity` 的完全限定(如 `ITenantStore.cs` 中的 `Microsoft.AspNetCore.Identity.IdentityResult`)
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 空检查
|
||||
**模式:** 显式参数空检查并抛出异常
|
||||
```csharp
|
||||
if (modelBuilder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelBuilder));
|
||||
}
|
||||
```
|
||||
- 位置: `PlatformDbContext.cs` 第 20-23 行
|
||||
|
||||
### 空返回
|
||||
**模式:** 对可空结果返回 `Task.FromResult<T?>(null)`
|
||||
```csharp
|
||||
if (tenantId == null) return Task.FromResult<Tenant?>(null);
|
||||
```
|
||||
- 位置: `TenantStore.cs` 第 23 行
|
||||
|
||||
### 验证
|
||||
- 查询中使用 `string.IsNullOrEmpty()` 进行字符串验证
|
||||
- 对引用类型使用可空注解 `?` 后缀
|
||||
|
||||
## 记录类型
|
||||
|
||||
### 值对象
|
||||
**模式:** 带参数的主构造函数记录
|
||||
```csharp
|
||||
public record TenantInfo(long? TenantId, string? TenantCode, string? TenantName)
|
||||
{
|
||||
public TenantInfo(Tenant? tenant)
|
||||
: this(tenant?.Id, tenant?.TenantCode, tenant?.Name)
|
||||
{
|
||||
}
|
||||
|
||||
public static TenantInfo Admin => new TenantInfo(null, null, null);
|
||||
}
|
||||
```
|
||||
- 位置: `TenantInfo.cs`
|
||||
|
||||
## 实体设计
|
||||
|
||||
### 贫血领域模型
|
||||
- **Tenant:** 贫血模型(带公共 setter 的数据容器)
|
||||
- **ApplicationUser:** 富模型,带到 `TenantInfo` 的导航属性
|
||||
- **ID 类型:** 所有实体标识使用 `long`
|
||||
|
||||
### 属性模式
|
||||
```csharp
|
||||
public long Id { get; set; }
|
||||
public string TenantCode { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
```
|
||||
|
||||
## 类设计
|
||||
|
||||
### 主构造函数
|
||||
**模式:** 用于依赖注入的主构造函数
|
||||
```csharp
|
||||
public sealed class TenantManager(ITenantStore store) : ITenantManager
|
||||
{
|
||||
// 构造函数参数自动成为私有 readonly 字段
|
||||
}
|
||||
```
|
||||
- 位置: `TenantManager.cs` 第 21 行
|
||||
|
||||
### 泛型约束
|
||||
```csharp
|
||||
public class TenantStore<TContext> : ITenantStore
|
||||
where TContext : PlatformDbContext
|
||||
```
|
||||
|
||||
### 方法返回类型
|
||||
- 异步方法: 返回 `Task<T>` 或 `Task`
|
||||
- 集合查询: 返回 `IList<T>` 或 `IQueryable<T>`
|
||||
- 单个实体: 返回 `T?`
|
||||
|
||||
## 注释
|
||||
|
||||
### 何时注释
|
||||
- **中文注释:** 用于领域概念和解释
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 租户信息
|
||||
/// </summary>
|
||||
public record TenantInfo(...)
|
||||
```
|
||||
- **XML 文档:** 用于公共 API
|
||||
- **最少行内注释:** 仅用于复杂业务逻辑
|
||||
|
||||
### JSDoc/TSDoc
|
||||
- 对公共 API 使用 `/// <summary>` 和 `/// <param name="...">`
|
||||
- 位置: `TenantInfo.cs` 第 3-8 行、11-13 行
|
||||
|
||||
## 函数设计
|
||||
|
||||
### 异步方法
|
||||
- 始终接受 `CancellationToken cancellationToken = default` 作为最后一个参数
|
||||
- 一致使用 `async`/`await`
|
||||
- 对有返回值操作返回 `Task<T>`
|
||||
|
||||
### 查询方法
|
||||
- **分页:** 接受 `page` 和 `pageSize` 参数
|
||||
- **过滤:** 接受可选过滤参数(`name`、`tenantCode`、`status`)
|
||||
- **排序:** 按创建日期应用 `OrderByDescending`
|
||||
|
||||
### IdentityResult 模式
|
||||
业务操作返回 `IdentityResult`:
|
||||
```csharp
|
||||
Task<IdentityResult> CreateAsync(Tenant tenant, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> UpdateAsync(Tenant tenant, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> DeleteAsync(Tenant tenant, CancellationToken cancellationToken = default);
|
||||
```
|
||||
|
||||
## DbContext 设计
|
||||
|
||||
### 配置
|
||||
- **模式:** `OnModelCreating` 中的流式 API
|
||||
- **实体配置:** 使用 `modelBuilder.Entity<T>(entity => {...})`
|
||||
- **索引配置:** 使用 `entity.HasIndex(e => e.Property)`
|
||||
- **拥有类型:** 使用 `entity.OwnsOne(e => e.TenantInfo, navigationBuilder => {...})`
|
||||
|
||||
### 配置发现
|
||||
```csharp
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PlatformDbContext).Assembly);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*约定分析: 2026-02-28*
|
||||
86
.planning/codebase/INTEGRATIONS.md
Normal file
86
.planning/codebase/INTEGRATIONS.md
Normal file
@ -0,0 +1,86 @@
|
||||
# 外部集成
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## API 与外部服务
|
||||
|
||||
**OAuth2/OpenID Connect:**
|
||||
- OpenIddict 7.2.0 - 实现
|
||||
- 框架: OpenIddict.EntityFrameworkCore
|
||||
- 用途: 平台的认证服务器
|
||||
- 存储: EntityFrameworkCore(存储在 PostgreSQL 中)
|
||||
|
||||
## 数据存储
|
||||
|
||||
**数据库:**
|
||||
- PostgreSQL
|
||||
- 提供程序: `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.0
|
||||
- 连接: 通过服务注册中的 `AddDbContext<TContext>()` 配置
|
||||
- ORM: Entity Framework Core 10.0.0
|
||||
- 上下文: `Fengling.Platform.Infrastructure/PlatformDbContext.cs`
|
||||
- 迁移: `Fengling.Platform.Infrastructure/Migrations/`
|
||||
|
||||
**文件存储:**
|
||||
- 未检测到(此服务仅使用本地文件系统)
|
||||
|
||||
**缓存:**
|
||||
- 未检测到
|
||||
|
||||
## 认证与身份
|
||||
|
||||
**认证提供程序:**
|
||||
- ASP.NET Core Identity + 自定义存储
|
||||
- 用户: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/ApplicationUser.cs`(继承 `IdentityUser<long>`)
|
||||
- 角色: `Fengling.Platform.Domain/AggregatesModel/RoleAggregate/ApplicationRole.cs`
|
||||
- 实现: `Microsoft.AspNetCore.Identity.EntityFrameworkCore`
|
||||
|
||||
**多租户:**
|
||||
- 通过 `ITenantStore<TContext>` 和 `ITenantManager` 进行租户管理
|
||||
- Store: `Fengling.Platform.Infrastructure/TenantStore.cs`
|
||||
- Manager: `Fengling.Platform.Infrastructure/TenantManager.cs`
|
||||
- 实体: `Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs`
|
||||
|
||||
## 监控与可观测性
|
||||
|
||||
**错误追踪:**
|
||||
- 依赖中未检测到
|
||||
|
||||
**日志:**
|
||||
- 标准 ASP.NET Core 日志(ILogger)
|
||||
|
||||
## CI/CD 与部署
|
||||
|
||||
**托管:**
|
||||
- Docker(容器化)
|
||||
- 基础镜像: `mcr.microsoft.com/dotnet/aspnet:10.0`
|
||||
- 构建: 多阶段 Dockerfile
|
||||
|
||||
**CI 流水线:**
|
||||
- 此仓库中未明确配置
|
||||
|
||||
## 环境配置
|
||||
|
||||
**所需环境变量:**
|
||||
- 数据库连接字符串(通过 `AddDbContext` 配置)
|
||||
- 标准 ASP.NET Core 环境变量
|
||||
|
||||
**密钥位置:**
|
||||
- 基于环境的配置(代码中未检测到)
|
||||
|
||||
## Webhook 与回调
|
||||
|
||||
**传入:**
|
||||
- 当前实现中未检测到
|
||||
|
||||
**传出:**
|
||||
- 当前实现中未检测到
|
||||
|
||||
## 内部依赖
|
||||
|
||||
**共享框架:**
|
||||
- NetCorePal (v3.2.1) - 内部框架包
|
||||
- 源: `https://gitea.shtao1.cn/api/packages/fengling/nuget/index.json`
|
||||
|
||||
---
|
||||
|
||||
*集成审计: 2026-02-28*
|
||||
76
.planning/codebase/STACK.md
Normal file
76
.planning/codebase/STACK.md
Normal file
@ -0,0 +1,76 @@
|
||||
# 技术栈
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## 语言
|
||||
|
||||
**主要:**
|
||||
- C# 12 / .NET 10.0 - 核心平台实现
|
||||
|
||||
## 运行时
|
||||
|
||||
**环境:**
|
||||
- .NET 10.0 (ASP.NET Core)
|
||||
- 运行时: `mcr.microsoft.com/dotnet/aspnet:10.0` (Docker)
|
||||
|
||||
**包管理:**
|
||||
- NuGet
|
||||
- 源:
|
||||
- `https://gitea.shtao1.cn/api/packages/fengling/nuget/index.json` (内部)
|
||||
- `https://api.nuget.org/v3/index.json` (NuGet.org)
|
||||
- 集中版本管理: `Directory.Packages.props`
|
||||
|
||||
## 框架
|
||||
|
||||
**核心:**
|
||||
- ASP.NET Core 10.0 - Web 框架
|
||||
- Entity Framework Core 10.0.0 - ORM
|
||||
|
||||
**认证与身份:**
|
||||
- Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.0 - Identity 框架
|
||||
- OpenIddict.EntityFrameworkCore 7.2.0 - OAuth2/OpenID Connect 服务器
|
||||
|
||||
**CQRS 与中介者:**
|
||||
- MediatR 12.5.0 - 请求/命令处理的中介者模式
|
||||
|
||||
**内部框架:**
|
||||
- NetCorePal.Extensions.Domain.Abstractions 3.2.1
|
||||
- NetCorePal.Extensions.Primitives 3.2.1
|
||||
- NetCorePal.Extensions.Repository.EntityFrameworkCore 3.2.1
|
||||
- NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake 3.2.1
|
||||
|
||||
## 关键依赖
|
||||
|
||||
**数据库:**
|
||||
- Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0 - EF Core 的 PostgreSQL 提供程序
|
||||
- Microsoft.EntityFrameworkCore.Design 10.0.0 - EF Core 设计时支持
|
||||
|
||||
**基础设施:**
|
||||
- 无其他明确配置
|
||||
|
||||
## 配置
|
||||
|
||||
**环境:**
|
||||
- 配置通过 `Fengling.Platform.Infrastructure/Extensions.cs` 中的 `AddPlatformCore<TContext>()` 扩展方法加载
|
||||
- 数据库上下文通过 `AddDbContext<TContext>()` 模式注册
|
||||
|
||||
**构建:**
|
||||
- `Directory.Packages.props` - 集中包版本管理
|
||||
- `NuGet.Config` - 包源配置
|
||||
- `Dockerfile` - 多阶段构建(基础、构建、发布、最终)
|
||||
|
||||
## 平台要求
|
||||
|
||||
**开发:**
|
||||
- .NET 10.0 SDK
|
||||
- PostgreSQL 数据库
|
||||
- 支持 C# 的 IDE (Rider, VS, VS Code)
|
||||
|
||||
**生产:**
|
||||
- Docker 容器化(存在 Dockerfile)
|
||||
- PostgreSQL 数据库
|
||||
- 多租户租户隔离
|
||||
|
||||
---
|
||||
|
||||
*技术栈分析: 2026-02-28*
|
||||
138
.planning/codebase/STRUCTURE.md
Normal file
138
.planning/codebase/STRUCTURE.md
Normal file
@ -0,0 +1,138 @@
|
||||
# 代码库结构
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## 目录布局
|
||||
|
||||
```
|
||||
fengling-platform/
|
||||
├── Fengling.Platform.Domain/
|
||||
│ ├── AggregatesModel/
|
||||
│ │ ├── UserAggregate/
|
||||
│ │ │ ├── ApplicationUser.cs
|
||||
│ │ │ ├── AuditLog.cs
|
||||
│ │ │ └── AccessLog.cs
|
||||
│ │ ├── RoleAggregate/
|
||||
│ │ │ └── ApplicationRole.cs
|
||||
│ │ └── TenantAggregate/
|
||||
│ │ ├── Tenant.cs
|
||||
│ │ └── TenantInfo.cs
|
||||
│ ├── GlobalUsings.cs
|
||||
│ └── Fengling.Platform.Domain.csproj
|
||||
├── Fengling.Platform.Infrastructure/
|
||||
│ ├── Configurations/
|
||||
│ │ └── TenantConfiguration.cs
|
||||
│ ├── Migrations/
|
||||
│ │ ├── 20260221065049_Initial.cs
|
||||
│ │ ├── 20260221065049_Initial.Designer.cs
|
||||
│ │ ├── 20260221071055_OpenIddict.cs
|
||||
│ │ ├── 20260221071055_OpenIddict.Designer.cs
|
||||
│ │ └── PlatformDbContextModelSnapshot.cs
|
||||
│ ├── Extensions.cs
|
||||
│ ├── GlobalUsings.cs
|
||||
│ ├── ITenantStore.cs
|
||||
│ ├── TenantManager.cs
|
||||
│ ├── TenantStore.cs
|
||||
│ ├── PlatformDbContext.cs
|
||||
│ ├── SeedData.cs
|
||||
│ ├── DesignTimeApplicationDbContextFactory.cs
|
||||
│ └── Fengling.Platform.Infrastructure.csproj
|
||||
├── Directory.Packages.props
|
||||
├── NuGet.Config
|
||||
├── Dockerfile
|
||||
└── AGENTS.md
|
||||
```
|
||||
|
||||
## 目录用途
|
||||
|
||||
**Fengling.Platform.Domain:**
|
||||
- 目的: 包含核心业务实体的领域层
|
||||
- 包含: 聚合(Tenant、User、Role)、值对象
|
||||
- 关键文件: `AggregatesModel/*/*.cs`
|
||||
|
||||
**Fengling.Platform.Infrastructure:**
|
||||
- 目的: 数据访问和外部关注的基础设施层
|
||||
- 包含: DbContext、Stores、Managers、EF 配置、迁移
|
||||
- 关键文件: `PlatformDbContext.cs`、`TenantStore.cs`、`TenantManager.cs`
|
||||
|
||||
**迁移:**
|
||||
- 目的: EF Core 数据库迁移
|
||||
- 已生成: 是(基于时间戳)
|
||||
- 已提交: 是
|
||||
|
||||
## 关键文件位置
|
||||
|
||||
**入口点:**
|
||||
- `Fengling.Platform.Infrastructure/Extensions.cs`: DI 注册入口点
|
||||
- `Fengling.Platform.Infrastructure/PlatformDbContext.cs`: 数据库上下文
|
||||
|
||||
**配置:**
|
||||
- `Directory.Packages.props`: 集中包版本管理
|
||||
- `Fengling.Platform.Infrastructure/Configurations/TenantConfiguration.cs`: EF 租户配置
|
||||
|
||||
**核心逻辑:**
|
||||
- `Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs`: 租户实体
|
||||
- `Fengling.Platform.Domain/AggregatesModel/UserAggregate/ApplicationUser.cs`: 用户实体
|
||||
- `Fengling.Platform.Infrastructure/TenantStore.cs`: 租户数据访问
|
||||
- `Fengling.Platform.Infrastructure/TenantManager.cs`: 租户业务逻辑
|
||||
|
||||
## 命名约定
|
||||
|
||||
**文件:**
|
||||
- 实体: `{EntityName}.cs`(如 `Tenant.cs`、`ApplicationUser.cs`)
|
||||
- 接口: `I{InterfaceName}.cs`(如 `ITenantStore.cs`、`ITenantManager.cs`)
|
||||
- 实现: `{InterfaceName}.cs`(如 `TenantStore.cs`)
|
||||
|
||||
**目录:**
|
||||
- 聚合: `*Aggregate/`(如 `TenantAggregate/`)
|
||||
- 配置: `Configurations/`
|
||||
- 迁移: `Migrations/`
|
||||
|
||||
**类:**
|
||||
- 实体: PascalCase(如 `ApplicationUser`、`Tenant`)
|
||||
- 枚举: PascalCase(如 `TenantStatus`)
|
||||
- 值对象: PascalCase 记录(如 `TenantInfo`)
|
||||
|
||||
## 新增代码位置
|
||||
|
||||
**新聚合:**
|
||||
- 领域实体: `Fengling.Platform.Domain/AggregatesModel/{AggregateName}/`
|
||||
- Store 接口: `Fengling.Platform.Infrastructure/I{EntityName}Store.cs`
|
||||
- Store 实现: `Fengling.Platform.Infrastructure/{EntityName}Store.cs`
|
||||
- Manager 接口: `Fengling.Platform.Infrastructure/I{EntityName}Manager.cs`
|
||||
- Manager 实现: `Fengling.Platform.Infrastructure/{EntityName}Manager.cs`
|
||||
- EF 配置: `Fengling.Platform.Infrastructure/Configurations/`
|
||||
|
||||
**新实体属性:**
|
||||
- 领域: 在 `AggregatesModel/` 中的现有实体添加
|
||||
- 基础设施: 在 `Configurations/` 中添加配置或在 `PlatformDbContext.OnModelCreating()` 中添加
|
||||
|
||||
**新迁移:**
|
||||
- 位置: `Fengling.Platform.Infrastructure/Migrations/`
|
||||
- 通过以下方式生成: 从 Infrastructure 目录运行 `dotnet ef migrations add`
|
||||
|
||||
## 特殊目录
|
||||
|
||||
**迁移:**
|
||||
- 目的: EF Core 数据库模式迁移
|
||||
- 已生成: 是(由 dotnet ef 自动生成)
|
||||
- 已提交: 是(纳入版本控制)
|
||||
|
||||
**配置:**
|
||||
- 目的: EF Core 实体配置
|
||||
- 包含: IEntityTypeConfiguration 实现
|
||||
|
||||
## 依赖关系
|
||||
|
||||
**Domain → Infrastructure:**
|
||||
- Domain 引用: Microsoft.AspNetCore.Identity(仅接口)
|
||||
- Domain 不引用 EF Core
|
||||
|
||||
**Infrastructure → Domain:**
|
||||
- Infrastructure 引用: Fengling.Platform.Domain
|
||||
- Infrastructure 引用: Microsoft.EntityFrameworkCore
|
||||
- Infrastructure 引用: Npgsql.EntityFrameworkCore.PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
*结构分析: 2026-02-28*
|
||||
247
.planning/codebase/TESTING.md
Normal file
247
.planning/codebase/TESTING.md
Normal file
@ -0,0 +1,247 @@
|
||||
# 测试模式
|
||||
|
||||
**分析日期:** 2026-02-28
|
||||
|
||||
## 测试框架
|
||||
|
||||
**状态:** 此仓库中目前不存在测试项目。
|
||||
|
||||
### 预期框架(未实现)
|
||||
|
||||
基于项目结构和依赖,测试项目应使用:
|
||||
|
||||
- **测试运行器:** xUnit(.NET 标准)
|
||||
- **模拟:** Moq 或 NSubstitute
|
||||
- **内存数据库:** `Microsoft.EntityFrameworkCore.InMemory` 用于 DbContext 测试
|
||||
- **断言:** FluentAssertions(可选,用于可读断言)
|
||||
|
||||
### 建议的项目结构
|
||||
|
||||
```
|
||||
Fengling.Platform.Tests/
|
||||
├── Fengling.Platform.Tests.csproj
|
||||
├── Unit/
|
||||
│ ├── TenantManagerTests.cs
|
||||
│ ├── TenantStoreTests.cs
|
||||
│ └── PlatformDbContextTests.cs
|
||||
├── Integration/
|
||||
│ └── TenantRepositoryTests.cs
|
||||
└── Usings.cs
|
||||
```
|
||||
|
||||
## 测试文件组织
|
||||
|
||||
### 位置
|
||||
- **模式:** 单独测试项目(`Fengling.Platform.Tests/`)
|
||||
- **结构:** 镜像源项目结构
|
||||
|
||||
### 命名
|
||||
- **测试类:** `{类名}Tests` 或 `{类名}Tests`
|
||||
- **测试方法:** `{方法名}_{场景}_{预期结果}`
|
||||
|
||||
## 测试结构
|
||||
|
||||
### 套件组织(预期模式)
|
||||
|
||||
```csharp
|
||||
public class TenantManagerTests
|
||||
{
|
||||
private readonly ITenantManager _tenantManager;
|
||||
private readonly Mock<ITenantStore> _storeMock;
|
||||
|
||||
public TenantManagerTests()
|
||||
{
|
||||
_storeMock = new Mock<ITenantStore>();
|
||||
_tenantManager = new TenantManager(_storeMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindByIdAsync_WithValidId_ReturnsTenant()
|
||||
{
|
||||
// Arrange
|
||||
var tenantId = 1L;
|
||||
var expectedTenant = new Tenant { Id = tenantId, Name = "Test" };
|
||||
_storeMock.Setup(s => s.FindByIdAsync(tenantId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedTenant);
|
||||
|
||||
// Act
|
||||
var result = await _tenantManager.FindByIdAsync(tenantId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(tenantId, result.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 模拟
|
||||
|
||||
### 框架: Moq
|
||||
|
||||
**要模拟:**
|
||||
- `ITenantStore` - `TenantManager` 的依赖
|
||||
- `PlatformDbContext` - 用于仓储测试
|
||||
|
||||
### 模式
|
||||
|
||||
```csharp
|
||||
// 带参数的模拟设置
|
||||
_storeMock.Setup(s => s.FindByIdAsync(tenantId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedTenant);
|
||||
|
||||
// 空返回模拟
|
||||
_storeMock.Setup(s => s.FindByIdAsync(It.IsAny<long?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Tenant?)null);
|
||||
|
||||
// 验证调用
|
||||
_storeMock.Verify(s => s.CreateAsync(It.IsAny<Tenant>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
```
|
||||
|
||||
**不要模拟:**
|
||||
- `Tenant` 实体(使用真实实例)
|
||||
- 值类型和简单 DTO
|
||||
|
||||
## 测试夹具和工厂
|
||||
|
||||
### 测试数据
|
||||
|
||||
```csharp
|
||||
public static class TenantFixture
|
||||
{
|
||||
public static Tenant CreateValidTenant(long id = 1)
|
||||
=> new Tenant
|
||||
{
|
||||
Id = id,
|
||||
TenantCode = "TEST",
|
||||
Name = "Test Tenant",
|
||||
ContactName = "John Doe",
|
||||
ContactEmail = "john@test.com",
|
||||
Status = TenantStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
public static IEnumerable<Tenant> CreateTenantCollection(int count)
|
||||
=> Enumerable.Range(1, count)
|
||||
.Select(i => CreateValidTenant(i));
|
||||
}
|
||||
```
|
||||
|
||||
### 位置
|
||||
- `Tests/Fixtures/` 或 `Tests/Factories/`
|
||||
|
||||
## 测试类型
|
||||
|
||||
### 单元测试
|
||||
- **范围:** 隔离的单个类
|
||||
- 目标: `TenantManager` - CRUD 操作
|
||||
- 目标: `TenantStore` - 数据访问方法
|
||||
- 目标: 实体验证逻辑
|
||||
- **方法:** 模拟依赖,隔离测试
|
||||
|
||||
### 集成测试
|
||||
- **范围:** 使用 `PlatformDbContext` 的数据库操作
|
||||
- **方法:**
|
||||
- 使用 `InMemoryDatabase` 隔离
|
||||
- 使用 `DbContextOptionsBuilder` + `UseInMemoryDatabase`
|
||||
|
||||
```csharp
|
||||
public class PlatformDbContextTests
|
||||
{
|
||||
private readonly PlatformDbContext _context;
|
||||
|
||||
public PlatformDbContextTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<PlatformDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
_context = new PlatformDbContext(options);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 端到端测试
|
||||
- **不适用:** 这是一个库/项目,不是应用程序
|
||||
|
||||
## 常见模式
|
||||
|
||||
### 异步测试
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task GetAllAsync_ReturnsAllTenants()
|
||||
{
|
||||
// Arrange
|
||||
var tenants = new List<Tenant> { CreateValidTenant(), CreateValidTenant(2) };
|
||||
_storeMock.Setup(s => s.GetAllAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(tenants);
|
||||
|
||||
// Act
|
||||
var result = await _tenantManager.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
```
|
||||
|
||||
### 错误测试
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithDuplicateTenantCode_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var tenant = CreateValidTenant();
|
||||
_storeMock.Setup(s => s.FindByTenantCodeAsync(tenant.TenantCode, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(tenant); // 模拟已存在的租户
|
||||
|
||||
// Act
|
||||
var result = await _tenantManager.CreateAsync(tenant);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Succeeded);
|
||||
}
|
||||
```
|
||||
|
||||
### 空参数测试
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task FindByIdAsync_WithNullId_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _tenantManager.FindByIdAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
```
|
||||
|
||||
## 覆盖率
|
||||
|
||||
### 要求
|
||||
- **未强制** - 目前未定义覆盖率目标
|
||||
|
||||
### 建议目标
|
||||
- **最低:** 领域逻辑 70%
|
||||
- **目标:** 关键路径(租户 CRUD)80%
|
||||
|
||||
### 查看覆盖率
|
||||
```bash
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
# 或使用 coverlet
|
||||
dotnet test /p:CollectCoverage=true /p:Threshold=80
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 命令(预期)
|
||||
```bash
|
||||
dotnet test # 运行所有测试
|
||||
dotnet test --filter "FullyQualifiedName~TenantManagerTests" # 运行特定类
|
||||
dotnet test --verbosity normal # 详细输出
|
||||
dotnet test --collect:"XPlat Code Coverage" # 带覆盖率
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*测试分析: 2026-02-28*
|
||||
@ -0,0 +1,131 @@
|
||||
---
|
||||
phase: 01-gateway-routing
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs
|
||||
- Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs
|
||||
- Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs
|
||||
- Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs
|
||||
autonomous: true
|
||||
requirements:
|
||||
- GATEWAY-01
|
||||
- GATEWAY-02
|
||||
- GATEWAY-03
|
||||
must_haves:
|
||||
truths:
|
||||
- "Gateway 实体可以在 Platform DbContext 中使用"
|
||||
- "实体遵循现有 Platform 命名约定"
|
||||
- "实体 ID 统一使用 long 类型"
|
||||
artifacts:
|
||||
- path: "Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs"
|
||||
provides: "网关租户实体,包含通用属性"
|
||||
min_lines: 30
|
||||
- path: "Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs"
|
||||
provides: "YARP 路由配置实体"
|
||||
min_lines: 30
|
||||
- path: "Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs"
|
||||
provides: "负载均衡服务实例实体"
|
||||
min_lines: 30
|
||||
key_links:
|
||||
- from: "GwTenantRoute"
|
||||
to: "GwTenant"
|
||||
via: "TenantCode 字段"
|
||||
pattern: "TenantCode string"
|
||||
---
|
||||
|
||||
# 计划 01: 网关领域实体
|
||||
|
||||
## 目标
|
||||
|
||||
创建从 fengling-gateway 项目迁移的网关路由领域实体。
|
||||
|
||||
**目的:** 在 Platform 领域层建立网关聚合,包含符合 YARP 要求的实体。
|
||||
|
||||
**输出:** 新建 GatewayAggregate 文件夹中的三个实体文件。
|
||||
|
||||
## 上下文
|
||||
|
||||
@Fengling.Platform.Domain/AggregatesModel/ (现有 Tenant 聚合结构)
|
||||
@../fengling-gateway/src/Models/ (源实体)
|
||||
|
||||
## 任务
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 1: 创建 GatewayEnums</name>
|
||||
<files>Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs</files>
|
||||
<action>
|
||||
创建网关实体使用的枚举类型:
|
||||
- RouteStatus (Active=1, Inactive=0)
|
||||
- InstanceHealth (Healthy=1, Unhealthy=0)
|
||||
- InstanceStatus (Active=1, Inactive=0)
|
||||
|
||||
参考现有: Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs
|
||||
</action>
|
||||
<verify>文件可编译,枚举可访问</verify>
|
||||
<done>GatewayEnums.cs 已创建,包含 RouteStatus, InstanceHealth, InstanceStatus</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 2: 创建 GwTenant 实体</name>
|
||||
<files>Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs</files>
|
||||
<action>
|
||||
创建 GwTenant 实体,包含:
|
||||
- Id (long), TenantCode (string), TenantName (string)
|
||||
- Status (int), IsDeleted (bool), Version (int)
|
||||
- CreatedTime, UpdatedTime, CreatedBy, UpdatedBy
|
||||
|
||||
参考源: ../fengling-gateway/src/Models/GwTenant.cs
|
||||
参考模式: Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>GwTenant 实体包含 Id, TenantCode, TenantName, Status 字段</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 3: 创建 GwTenantRoute 实体</name>
|
||||
<files>Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs</files>
|
||||
<action>
|
||||
创建 GwTenantRoute 实体,包含:
|
||||
- Id (long), TenantCode (string), ServiceName (string)
|
||||
- ClusterId (string), PathPattern (string), Priority (int)
|
||||
- Status (int), IsGlobal (bool)
|
||||
- IsDeleted (bool), Version (int)
|
||||
- CreatedTime, UpdatedTime, CreatedBy, UpdatedBy
|
||||
|
||||
参考源: ../fengling-gateway/src/Models/GwTenantRoute.cs
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>GwTenantRoute 实体包含路由配置字段</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 4: 创建 GwServiceInstance 实体</name>
|
||||
<files>Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs</files>
|
||||
<action>
|
||||
创建 GwServiceInstance 实体,包含:
|
||||
- Id (long), ClusterId (string), DestinationId (string)
|
||||
- Address (string), Health (int), Weight (int)
|
||||
- Status (int)
|
||||
- IsDeleted (bool), Version (int)
|
||||
- CreatedTime, UpdatedTime, CreatedBy, UpdatedBy
|
||||
|
||||
参考源: ../fengling-gateway/src/Models/GwServiceInstance.cs
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>GwServiceInstance 实体包含实例管理字段</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## 验证
|
||||
|
||||
- [ ] 所有 4 个实体文件已创建
|
||||
- [ ] Build 无错误通过
|
||||
- [ ] 实体遵循 Platform 约定 (long ID, PascalCase, 时间戳)
|
||||
|
||||
## 成功标准
|
||||
|
||||
领域实体准备好进行基础设施层实现。
|
||||
@ -0,0 +1,154 @@
|
||||
---
|
||||
phase: 01-gateway-routing
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- Fengling.Platform.Infrastructure/GatewayDbContext.cs
|
||||
- Fengling.Platform.Infrastructure/IRouteStore.cs
|
||||
- Fengling.Platform.Infrastructure/RouteStore.cs
|
||||
- Fengling.Platform.Infrastructure/IRouteManager.cs
|
||||
- Fengling.Platform.Infrastructure/RouteManager.cs
|
||||
- Fengling.Platform.Infrastructure/IInstanceStore.cs
|
||||
- Fengling.Platform.Infrastructure/InstanceStore.cs
|
||||
autonomous: true
|
||||
requirements:
|
||||
- GATEWAY-01
|
||||
- GATEWAY-02
|
||||
- GATEWAY-03
|
||||
must_haves:
|
||||
truths:
|
||||
- "Store 实现遵循 TenantStore 模式"
|
||||
- "Manager 实现委托给 Stores"
|
||||
- "DbContext 包含所有 Gateway DbSets"
|
||||
artifacts:
|
||||
- path: "Fengling.Platform.Infrastructure/GatewayDbContext.cs"
|
||||
provides: "Gateway 实体的 EF Core DbContext"
|
||||
exports: ["GwTenants", "GwTenantRoutes", "GwServiceInstances"]
|
||||
- path: "Fengling.Platform.Infrastructure/IRouteStore.cs"
|
||||
provides: "路由 CRUD 接口"
|
||||
- path: "Fengling.Platform.Infrastructure/RouteStore.cs"
|
||||
provides: "路由数据访问实现"
|
||||
- path: "Fengling.Platform.Infrastructure/IRouteManager.cs"
|
||||
provides: "路由业务操作"
|
||||
- path: "Fengling.Platform.Infrastructure/RouteManager.cs"
|
||||
provides: "路由业务逻辑"
|
||||
key_links:
|
||||
- from: "RouteManager"
|
||||
to: "IRouteStore"
|
||||
via: "构造函数注入"
|
||||
pattern: "public RouteManager(IRouteStore store)"
|
||||
---
|
||||
|
||||
# 计划 02: 网关基础设施
|
||||
|
||||
## 目标
|
||||
|
||||
创建网关路由的基础设施层 - Store、Manager 和 DbContext。
|
||||
|
||||
**目的:** 实现数据访问和业务逻辑,遵循 Tenant 管理模式。
|
||||
|
||||
**输出:** GatewayDbContext、Store 接口/实现、Manager 接口/实现。
|
||||
|
||||
## 上下文
|
||||
|
||||
@Fengling.Platform.Infrastructure/Extensions.cs (DI 注册模式)
|
||||
@Fengling.Platform.Infrastructure/TenantStore.cs (Store 模式参考)
|
||||
@Fengling.Platform.Infrastructure/TenantManager.cs (Manager 模式参考)
|
||||
@../fengling-gateway/src/Data/GatewayDbContext.cs (源 DbContext)
|
||||
|
||||
## 任务
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 1: 创建 GatewayDbContext</name>
|
||||
<files>Fengling.Platform.Infrastructure/GatewayDbContext.cs</files>
|
||||
<action>
|
||||
创建继承 DbContext 的 GatewayDbContext:
|
||||
- 添加 DbSet<GwTenant> GwTenants
|
||||
- 添加 DbSet<GwTenantRoute> GwTenantRoutes
|
||||
- 添加 DbSet<GwServiceInstance> GwServiceInstances
|
||||
- 在 OnModelCreating 中配置索引 (参考源 ../fengling-gateway)
|
||||
|
||||
关键索引:
|
||||
- GwTenant: TenantCode 唯一
|
||||
- GwTenantRoute: TenantCode, ServiceName, ClusterId, 复合索引 (ServiceName, IsGlobal, Status)
|
||||
- GwServiceInstance: 复合索引 (ClusterId, DestinationId) 唯一, Health 索引
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>GatewayDbContext 包含所有 DbSet 和索引配置</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 2: 创建 IRouteStore 和 RouteStore</name>
|
||||
<files>
|
||||
Fengling.Platform.Infrastructure/IRouteStore.cs
|
||||
Fengling.Platform.Infrastructure/RouteStore.cs
|
||||
</files>
|
||||
<action>
|
||||
创建 IRouteStore 接口,包含方法:
|
||||
- FindByIdAsync, FindByTenantCodeAsync, FindByClusterIdAsync
|
||||
- GetAllAsync, GetPagedAsync, GetCountAsync
|
||||
- CreateAsync, UpdateAsync, DeleteAsync (返回 IdentityResult)
|
||||
|
||||
创建实现 IRouteStore 的 RouteStore<TContext>:
|
||||
- 参考 TenantStore 模式
|
||||
- 泛型约束: where TContext : GatewayDbContext
|
||||
- 实现所有 CRUD 方法,支持软删除
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>IRouteStore 接口和 RouteStore 实现</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 3: 创建 IRouteManager 和 RouteManager</name>
|
||||
<files>
|
||||
Fengling.Platform.Infrastructure/IRouteManager.cs
|
||||
Fengling.Platform.Infrastructure/RouteManager.cs
|
||||
</files>
|
||||
<action>
|
||||
创建 IRouteManager 接口,包含方法:
|
||||
- FindByIdAsync, FindByTenantCodeAsync, GetAllAsync
|
||||
- CreateRouteAsync, UpdateRouteAsync, DeleteRouteAsync
|
||||
|
||||
创建实现 IRouteManager 的 RouteManager:
|
||||
- 参考 TenantManager 模式
|
||||
- 构造函数: public RouteManager(IRouteStore store)
|
||||
- 委托给 IRouteStore 进行数据操作
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>IRouteManager 接口和 RouteManager 实现</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 4: 创建 IInstanceStore 和 InstanceStore</name>
|
||||
<files>
|
||||
Fengling.Platform.Infrastructure/IInstanceStore.cs
|
||||
Fengling.Platform.Infrastructure/InstanceStore.cs
|
||||
</files>
|
||||
<action>
|
||||
创建 IInstanceStore 接口:
|
||||
- FindByIdAsync, FindByClusterIdAsync, FindByDestinationAsync
|
||||
- GetAllAsync, GetPagedAsync, GetCountAsync
|
||||
- CreateAsync, UpdateAsync, DeleteAsync
|
||||
|
||||
创建实现 IInstanceStore 的 InstanceStore<TContext>:
|
||||
- 类似 RouteStore 的模式
|
||||
- 重点在 ClusterId 和 DestinationId 查询
|
||||
</action>
|
||||
<verify>dotnet build 通过</verify>
|
||||
<done>IInstanceStore 接口和 InstanceStore 实现</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## 验证
|
||||
|
||||
- [ ] GatewayDbContext 包含所有 DbSet 可编译
|
||||
- [ ] Store 实现遵循 TenantStore 模式
|
||||
- [ ] Manager 实现委托给 Stores
|
||||
- [ ] Build 无错误通过
|
||||
|
||||
## 成功标准
|
||||
|
||||
基础设施层准备好进行 Extensions 集成。
|
||||
@ -0,0 +1,124 @@
|
||||
---
|
||||
phase: 01-gateway-routing
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 01-gateway-routing-01
|
||||
- 01-gateway-routing-02
|
||||
files_modified:
|
||||
- Fengling.Platform.Infrastructure/GatewayExtensions.cs
|
||||
autonomous: true
|
||||
requirements:
|
||||
- GATEWAY-04
|
||||
- GATEWAY-05
|
||||
must_haves:
|
||||
truths:
|
||||
- "Extensions 方法注册所有 Gateway 服务"
|
||||
- "DI 中 AddGatewayCore 后服务可用"
|
||||
- "可以生成数据库迁移"
|
||||
artifacts:
|
||||
- path: "Fengling.Platform.Infrastructure/GatewayExtensions.cs"
|
||||
provides: "AddGatewayCore<TContext> 扩展方法"
|
||||
exports: ["GatewayDbContext", "IRouteStore", "IRouteManager", "IInstanceStore"]
|
||||
key_links:
|
||||
- from: "AddGatewayCore"
|
||||
to: "AddPlatformCore"
|
||||
via: "可以一起调用"
|
||||
pattern: "services.AddPlatformCore<T>().AddGatewayCore<T>()"
|
||||
---
|
||||
|
||||
# 计划 03: 网关扩展方法
|
||||
|
||||
## 目标
|
||||
|
||||
创建 Extensions 类用于快速 IoC 注册 Gateway 服务。
|
||||
|
||||
**目的:** 支持单行注册所有 Gateway 服务,类似于 AddPlatformCore。
|
||||
|
||||
**输出:** 包含 AddGatewayCore<TContext> 方法的 GatewayExtensions.cs。
|
||||
|
||||
## 上下文
|
||||
|
||||
@Fengling.Platform.Infrastructure/Extensions.cs (模式参考)
|
||||
@Fengling.Platform.Infrastructure/GatewayDbContext.cs (来自计划 02)
|
||||
|
||||
## 任务
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 1: 创建 GatewayExtensions</name>
|
||||
<files>Fengling.Platform.Infrastructure/GatewayExtensions.cs</files>
|
||||
<action>
|
||||
创建 GatewayExtensions 静态类:
|
||||
|
||||
```csharp
|
||||
public static class GatewayExtensions
|
||||
{
|
||||
public static IServiceCollection AddGatewayCore<TContext>(this IServiceCollection services)
|
||||
where TContext : GatewayDbContext
|
||||
{
|
||||
// 注册 Gateway stores
|
||||
services.AddScoped<IRouteStore, RouteStore<TContext>>();
|
||||
services.AddScoped<IInstanceStore, InstanceStore<TContext>>();
|
||||
|
||||
// 注册 Gateway managers
|
||||
services.AddScoped<IRouteManager, RouteManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
参考: Fengling.Platform.Infrastructure/Extensions.cs 模式
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build Fengling.Platform.Infrastructure</automated>
|
||||
</verify>
|
||||
<done>AddGatewayCore 扩展方法注册所有 Gateway 服务</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 2: 生成数据库迁移</name>
|
||||
<files>Fengling.Platform.Infrastructure/Migrations/</files>
|
||||
<action>
|
||||
为 Gateway 实体生成 EF Core 迁移:
|
||||
|
||||
1. 创建初始迁移:
|
||||
- 从 Infrastructure 目录
|
||||
- 迁移名称: InitialGateway
|
||||
|
||||
2. 验证迁移包含:
|
||||
- GwTenants 表
|
||||
- GwTenantRoutes 表
|
||||
- GwServiceInstances 表
|
||||
- GatewayDbContext 中定义的所有索引
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet ef migrations list --project Fengling.Platform.Infrastructure</automated>
|
||||
</verify>
|
||||
<done>迁移已生成并列出</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## 验证
|
||||
|
||||
- [ ] GatewayExtensions.cs 可编译
|
||||
- [ ] AddGatewayCore 方法注册所有服务
|
||||
- [ ] 迁移成功生成
|
||||
- [ ] Build 通过
|
||||
|
||||
## 成功标准
|
||||
|
||||
Gateway 路由功能完全集成并可使用。
|
||||
|
||||
## 使用示例
|
||||
|
||||
```csharp
|
||||
// 在 Program.cs 或 startup 中
|
||||
services.AddPlatformCore<PlatformDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
services.AddGatewayCore<GatewayDbContext>(options =>
|
||||
options.UseNpgsql(gatewayConnectionString));
|
||||
```
|
||||
@ -0,0 +1,75 @@
|
||||
# Phase 1: Gateway Routing Migration - Context
|
||||
|
||||
**Gathered:** 2026-02-28
|
||||
**Status:** Ready for planning
|
||||
**Source:** User request (YARP gateway migration)
|
||||
|
||||
## Phase Boundary
|
||||
|
||||
Migrate YARP gateway routing entities from fengling-gateway to Platform project:
|
||||
- GwTenant - 租户在网关中的配置
|
||||
- GwTenantRoute - 路由规则配置
|
||||
- GwServiceInstance - 服务实例管理
|
||||
|
||||
Output: Domain entities, Infrastructure (Store/Manager), Extensions for IoC
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
### Architecture Pattern
|
||||
- **Manager + Store 模式**: 与现有 Tenant 管理一致
|
||||
- ITenantStore → ITenantRouteStore
|
||||
- ITenantManager → ITenantRouteManager
|
||||
- 参考: `Fengling.Platform.Infrastructure/TenantStore.cs`, `TenantManager.cs`
|
||||
|
||||
### Entity Design
|
||||
- **GwTenant**: 租户网关配置 (继承现有 Tenant 概念)
|
||||
- **GwTenantRoute**: 路由规则 (ClusterId, PathPattern, Priority)
|
||||
- **GwServiceInstance**: 服务实例 (Address, Health, Weight)
|
||||
|
||||
### Extensions Pattern
|
||||
- 参考现有: `Fengling.Platform.Infrastructure/Extensions.cs`
|
||||
- 新增: `AddGatewayCore<TContext>()` 扩展方法
|
||||
- 注册: DbContext, IRouteStore, IRouteManager, IServiceInstanceStore
|
||||
|
||||
### Database
|
||||
- PostgreSQL (已有)
|
||||
- 新迁移: Gateway 实体相关表
|
||||
|
||||
## Specific Ideas
|
||||
|
||||
### 从 fengling-gateway 迁移的实体
|
||||
|
||||
```
|
||||
../fengling-gateway/src/Models/GwTenant.cs
|
||||
../fengling-gateway/src/Models/GwTenantRoute.cs
|
||||
../fengling-gateway/src/Models/GwServiceInstance.cs
|
||||
../fengling-gateway/src/Data/GatewayDbContext.cs
|
||||
```
|
||||
|
||||
### Manager/Store 接口命名建议
|
||||
|
||||
```
|
||||
ITenantRouteStore / TenantRouteStore
|
||||
ITenantRouteManager / TenantRouteManager
|
||||
IServiceInstanceStore / ServiceInstanceStore
|
||||
IServiceInstanceManager / ServiceInstanceManager
|
||||
```
|
||||
|
||||
### 扩展方法签名
|
||||
|
||||
```csharp
|
||||
public static IServiceCollection AddGatewayCore<TContext>(this IServiceCollection services)
|
||||
where TContext : GatewayDbContext;
|
||||
```
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- 服务发现集成 (后续阶段)
|
||||
- 动态配置热加载 (后续阶段)
|
||||
- 复杂的负载均衡策略 (后续阶段)
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- 实体命名: 保持 Gw 前缀还是简化? (建议保留以避免冲突)
|
||||
- 是否复用现有 TenantStore? (建议新建独立 Store)
|
||||
- 是否需要 YARP 集成? (仅实体层,先不包含 YARP 特定配置)
|
||||
@ -0,0 +1,28 @@
|
||||
namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 路由状态枚举
|
||||
/// </summary>
|
||||
public enum RouteStatus
|
||||
{
|
||||
Inactive = 0,
|
||||
Active = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务实例健康状态枚举
|
||||
/// </summary>
|
||||
public enum InstanceHealth
|
||||
{
|
||||
Unhealthy = 0,
|
||||
Healthy = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务实例状态枚举
|
||||
/// </summary>
|
||||
public enum InstanceStatus
|
||||
{
|
||||
Inactive = 0,
|
||||
Active = 1
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 网关服务实例实体 - 表示负载均衡的服务实例
|
||||
/// </summary>
|
||||
public class GwServiceInstance
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 集群ID
|
||||
/// </summary>
|
||||
public string ClusterId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 目标ID
|
||||
/// </summary>
|
||||
public string DestinationId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 地址
|
||||
/// </summary>
|
||||
public string Address { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 健康状态
|
||||
/// </summary>
|
||||
public int Health { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 权重
|
||||
/// </summary>
|
||||
public int Weight { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 创建人ID
|
||||
/// </summary>
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 更新人ID
|
||||
/// </summary>
|
||||
public long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否删除
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 版本号,用于乐观并发
|
||||
/// </summary>
|
||||
public int Version { get; set; } = 0;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 网关租户实体 - 表示租户在网关中的配置
|
||||
/// </summary>
|
||||
public class GwTenant
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户代码
|
||||
/// </summary>
|
||||
public string TenantCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 租户名称
|
||||
/// </summary>
|
||||
public string TenantName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 创建人ID
|
||||
/// </summary>
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 更新人ID
|
||||
/// </summary>
|
||||
public long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否删除
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 版本号,用于乐观并发
|
||||
/// </summary>
|
||||
public int Version { get; set; } = 0;
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 网关租户路由实体 - 表示路由规则配置
|
||||
/// </summary>
|
||||
public class GwTenantRoute
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户代码
|
||||
/// </summary>
|
||||
public string TenantCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 服务名称
|
||||
/// </summary>
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 集群ID
|
||||
/// </summary>
|
||||
public string ClusterId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 路径匹配模式
|
||||
/// </summary>
|
||||
public string PathPattern { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 优先级
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否全局路由
|
||||
/// </summary>
|
||||
public bool IsGlobal { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 创建人ID
|
||||
/// </summary>
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 更新人ID
|
||||
/// </summary>
|
||||
public long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否删除
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 版本号,用于乐观并发
|
||||
/// </summary>
|
||||
public int Version { get; set; } = 0;
|
||||
}
|
||||
@ -18,8 +18,16 @@ public static class Extensions
|
||||
services.AddDbContext<TContext>(optionsAction);
|
||||
}
|
||||
}
|
||||
|
||||
// Platform 服务
|
||||
services.AddScoped<ITenantStore, TenantStore<TContext>>();
|
||||
services.AddScoped<ITenantManager, TenantManager>();
|
||||
|
||||
// Gateway 服务
|
||||
services.AddScoped<IRouteStore, RouteStore<TContext>>();
|
||||
services.AddScoped<IInstanceStore, InstanceStore<TContext>>();
|
||||
services.AddScoped<IRouteManager, RouteManager>();
|
||||
|
||||
serviceAction?.Invoke(services);
|
||||
|
||||
return services;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake" />
|
||||
<PackageReference Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" />
|
||||
<PackageReference Include="MediatR" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
23
Fengling.Platform.Infrastructure/IInstanceStore.cs
Normal file
23
Fengling.Platform.Infrastructure/IInstanceStore.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
namespace Fengling.Platform.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 服务实例存储接口
|
||||
/// </summary>
|
||||
public interface IInstanceStore
|
||||
{
|
||||
Task<GwServiceInstance?> FindByIdAsync(long? id, CancellationToken cancellationToken = default);
|
||||
Task<GwServiceInstance?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default);
|
||||
Task<GwServiceInstance?> FindByDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default);
|
||||
Task<IList<GwServiceInstance>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<IList<GwServiceInstance>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
|
||||
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default);
|
||||
Task<int> GetCountAsync(string? clusterId = null,
|
||||
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> CreateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> UpdateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> DeleteAsync(GwServiceInstance instance, CancellationToken cancellationToken = default);
|
||||
}
|
||||
18
Fengling.Platform.Infrastructure/IRouteManager.cs
Normal file
18
Fengling.Platform.Infrastructure/IRouteManager.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
namespace Fengling.Platform.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 路由管理器接口
|
||||
/// </summary>
|
||||
public interface IRouteManager
|
||||
{
|
||||
Task<GwTenantRoute?> FindByIdAsync(long? id, CancellationToken cancellationToken = default);
|
||||
Task<GwTenantRoute?> FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default);
|
||||
Task<IList<GwTenantRoute>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> CreateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> UpdateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> DeleteRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default);
|
||||
}
|
||||
23
Fengling.Platform.Infrastructure/IRouteStore.cs
Normal file
23
Fengling.Platform.Infrastructure/IRouteStore.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
namespace Fengling.Platform.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 路由存储接口
|
||||
/// </summary>
|
||||
public interface IRouteStore
|
||||
{
|
||||
Task<GwTenantRoute?> FindByIdAsync(long? id, CancellationToken cancellationToken = default);
|
||||
Task<GwTenantRoute?> FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default);
|
||||
Task<GwTenantRoute?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default);
|
||||
Task<IList<GwTenantRoute>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<IList<GwTenantRoute>> GetPagedAsync(int page, int pageSize, string? tenantCode = null,
|
||||
string? serviceName = null, RouteStatus? status = null, CancellationToken cancellationToken = default);
|
||||
Task<int> GetCountAsync(string? tenantCode = null, string? serviceName = null,
|
||||
RouteStatus? status = null, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> CreateAsync(GwTenantRoute route, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> UpdateAsync(GwTenantRoute route, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> DeleteAsync(GwTenantRoute route, CancellationToken cancellationToken = default);
|
||||
}
|
||||
108
Fengling.Platform.Infrastructure/InstanceStore.cs
Normal file
108
Fengling.Platform.Infrastructure/InstanceStore.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
namespace Fengling.Platform.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 服务实例存储实现
|
||||
/// </summary>
|
||||
public class InstanceStore<TContext> : IInstanceStore
|
||||
where TContext : PlatformDbContext
|
||||
{
|
||||
private readonly TContext _context;
|
||||
private readonly DbSet<GwServiceInstance> _instances;
|
||||
|
||||
public InstanceStore(TContext context)
|
||||
{
|
||||
_context = context;
|
||||
_instances = context.GwServiceInstances;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public virtual Task<GwServiceInstance?> FindByIdAsync(long? id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (id == null) return Task.FromResult<GwServiceInstance?>(null);
|
||||
return _instances.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<GwServiceInstance?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _instances.FirstOrDefaultAsync(i => i.ClusterId == clusterId && !i.IsDeleted, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<GwServiceInstance?> FindByDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _instances.FirstOrDefaultAsync(i => i.ClusterId == clusterId && i.DestinationId == destinationId && !i.IsDeleted, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<IList<GwServiceInstance>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _instances.Where(i => !i.IsDeleted).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<IList<GwServiceInstance>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
|
||||
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _instances.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(clusterId))
|
||||
query = query.Where(i => i.ClusterId.Contains(clusterId));
|
||||
|
||||
if (health.HasValue)
|
||||
query = query.Where(i => i.Health == (int)health.Value);
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(i => i.Status == (int)status.Value);
|
||||
|
||||
return await query
|
||||
.Where(i => !i.IsDeleted)
|
||||
.OrderByDescending(i => i.CreatedTime)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<int> GetCountAsync(string? clusterId = null,
|
||||
InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _instances.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(clusterId))
|
||||
query = query.Where(i => i.ClusterId.Contains(clusterId));
|
||||
|
||||
if (health.HasValue)
|
||||
query = query.Where(i => i.Health == (int)health.Value);
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(i => i.Status == (int)status.Value);
|
||||
|
||||
return await query.Where(i => !i.IsDeleted).CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<IdentityResult> CreateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_instances.Add(instance);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public virtual async Task<IdentityResult> UpdateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
instance.UpdatedTime = DateTime.UtcNow;
|
||||
_instances.Update(instance);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public virtual async Task<IdentityResult> DeleteAsync(GwServiceInstance instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 软删除
|
||||
instance.IsDeleted = true;
|
||||
instance.UpdatedTime = DateTime.UtcNow;
|
||||
_instances.Update(instance);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
using Fengling.Platform.Domain.AggregatesModel.RoleAggregate;
|
||||
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
|
||||
using Fengling.Platform.Domain.AggregatesModel.UserAggregate;
|
||||
@ -15,6 +17,11 @@ public class PlatformDbContext(DbContextOptions options)
|
||||
public DbSet<AccessLog> AccessLogs => Set<AccessLog>();
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
|
||||
// Gateway 实体
|
||||
public DbSet<GwTenant> GwTenants => Set<GwTenant>();
|
||||
public DbSet<GwTenantRoute> GwTenantRoutes => Set<GwTenantRoute>();
|
||||
public DbSet<GwServiceInstance> GwServiceInstances => Set<GwServiceInstance>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
if (modelBuilder is null)
|
||||
@ -78,6 +85,38 @@ public class PlatformDbContext(DbContextOptions options)
|
||||
entity.Property(e => e.Status).HasMaxLength(20);
|
||||
});
|
||||
|
||||
// Gateway 实体配置
|
||||
modelBuilder.Entity<GwTenant>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.TenantCode).HasMaxLength(50).IsRequired();
|
||||
entity.Property(e => e.TenantName).HasMaxLength(100).IsRequired();
|
||||
entity.HasIndex(e => e.TenantCode).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<GwTenantRoute>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.TenantCode).HasMaxLength(50);
|
||||
entity.Property(e => e.ServiceName).HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.PathPattern).HasMaxLength(200).IsRequired();
|
||||
entity.HasIndex(e => e.TenantCode);
|
||||
entity.HasIndex(e => e.ServiceName);
|
||||
entity.HasIndex(e => e.ClusterId);
|
||||
entity.HasIndex(e => new { e.ServiceName, e.IsGlobal, e.Status });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<GwServiceInstance>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.DestinationId).HasMaxLength(100).IsRequired();
|
||||
entity.Property(e => e.Address).HasMaxLength(200).IsRequired();
|
||||
entity.HasIndex(e => new { e.ClusterId, e.DestinationId }).IsUnique();
|
||||
entity.HasIndex(e => e.Health);
|
||||
});
|
||||
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PlatformDbContext).Assembly);
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
38
Fengling.Platform.Infrastructure/RouteManager.cs
Normal file
38
Fengling.Platform.Infrastructure/RouteManager.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
namespace Fengling.Platform.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 路由管理器实现
|
||||
/// </summary>
|
||||
public class RouteManager : IRouteManager
|
||||
{
|
||||
private readonly IRouteStore _store;
|
||||
|
||||
public RouteManager(IRouteStore store)
|
||||
{
|
||||
_store = store;
|
||||
}
|
||||
|
||||
public virtual Task<GwTenantRoute?> FindByIdAsync(long? id, CancellationToken cancellationToken = default)
|
||||
=> _store.FindByIdAsync(id, cancellationToken);
|
||||
|
||||
public virtual Task<GwTenantRoute?> FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default)
|
||||
=> _store.FindByTenantCodeAsync(tenantCode, cancellationToken);
|
||||
|
||||
public virtual Task<IList<GwTenantRoute>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
=> _store.GetAllAsync(cancellationToken);
|
||||
|
||||
public virtual Task<IdentityResult> CreateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default)
|
||||
{
|
||||
route.CreatedTime = DateTime.UtcNow;
|
||||
return _store.CreateAsync(route, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<IdentityResult> UpdateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default)
|
||||
=> _store.UpdateAsync(route, cancellationToken);
|
||||
|
||||
public virtual Task<IdentityResult> DeleteRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default)
|
||||
=> _store.DeleteAsync(route, cancellationToken);
|
||||
}
|
||||
108
Fengling.Platform.Infrastructure/RouteStore.cs
Normal file
108
Fengling.Platform.Infrastructure/RouteStore.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||||
|
||||
namespace Fengling.Platform.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// 路由存储实现
|
||||
/// </summary>
|
||||
public class RouteStore<TContext> : IRouteStore
|
||||
where TContext : PlatformDbContext
|
||||
{
|
||||
private readonly TContext _context;
|
||||
private readonly DbSet<GwTenantRoute> _routes;
|
||||
|
||||
public RouteStore(TContext context)
|
||||
{
|
||||
_context = context;
|
||||
_routes = context.GwTenantRoutes;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public virtual Task<GwTenantRoute?> FindByIdAsync(long? id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (id == null) return Task.FromResult<GwTenantRoute?>(null);
|
||||
return _routes.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<GwTenantRoute?> FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _routes.FirstOrDefaultAsync(r => r.TenantCode == tenantCode && !r.IsDeleted, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<GwTenantRoute?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _routes.FirstOrDefaultAsync(r => r.ClusterId == clusterId && !r.IsDeleted, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<IList<GwTenantRoute>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _routes.Where(r => !r.IsDeleted).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<IList<GwTenantRoute>> GetPagedAsync(int page, int pageSize, string? tenantCode = null,
|
||||
string? serviceName = null, RouteStatus? status = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _routes.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(tenantCode))
|
||||
query = query.Where(r => r.TenantCode.Contains(tenantCode));
|
||||
|
||||
if (!string.IsNullOrEmpty(serviceName))
|
||||
query = query.Where(r => r.ServiceName.Contains(serviceName));
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(r => r.Status == (int)status.Value);
|
||||
|
||||
return await query
|
||||
.Where(r => !r.IsDeleted)
|
||||
.OrderByDescending(r => r.CreatedTime)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<int> GetCountAsync(string? tenantCode = null, string? serviceName = null,
|
||||
RouteStatus? status = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _routes.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(tenantCode))
|
||||
query = query.Where(r => r.TenantCode.Contains(tenantCode));
|
||||
|
||||
if (!string.IsNullOrEmpty(serviceName))
|
||||
query = query.Where(r => r.ServiceName.Contains(serviceName));
|
||||
|
||||
if (status.HasValue)
|
||||
query = query.Where(r => r.Status == (int)status.Value);
|
||||
|
||||
return await query.Where(r => !r.IsDeleted).CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public virtual async Task<IdentityResult> CreateAsync(GwTenantRoute route, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_routes.Add(route);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public virtual async Task<IdentityResult> UpdateAsync(GwTenantRoute route, CancellationToken cancellationToken = default)
|
||||
{
|
||||
route.UpdatedTime = DateTime.UtcNow;
|
||||
_routes.Update(route);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public virtual async Task<IdentityResult> DeleteAsync(GwTenantRoute route, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 软删除
|
||||
route.IsDeleted = true;
|
||||
route.UpdatedTime = DateTime.UtcNow;
|
||||
_routes.Update(route);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user