From 1b8c937aa4b903f4e3827ad78440cb69c98c8386 Mon Sep 17 00:00:00 2001 From: movingsam Date: Sat, 28 Feb 2026 23:53:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Gateway=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E5=AE=9E=E4=BD=93=E5=88=B0=20Platform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GatewayAggregate 领域实体 (GwTenant, GwTenantRoute, GwServiceInstance) - 新增 IRouteStore, RouteStore, IInstanceStore, InstanceStore - 新增 IRouteManager, RouteManager - 合并 GatewayDbContext 到 PlatformDbContext - 统一 Extensions.AddPlatformCore 注册所有服务 --- .planning/ROADMAP.md | 47 ++++ .planning/STATE.md | 41 +++ .planning/codebase/ARCHITECTURE.md | 119 +++++++++ .planning/codebase/CONCERNS.md | 196 ++++++++++++++ .planning/codebase/CONVENTIONS.md | 215 +++++++++++++++ .planning/codebase/INTEGRATIONS.md | 86 ++++++ .planning/codebase/STACK.md | 76 ++++++ .planning/codebase/STRUCTURE.md | 138 ++++++++++ .planning/codebase/TESTING.md | 247 ++++++++++++++++++ .../01-gateway-routing-01-PLAN.md | 131 ++++++++++ .../01-gateway-routing-02-PLAN.md | 154 +++++++++++ .../01-gateway-routing-03-PLAN.md | 124 +++++++++ .../01-gateway-routing-CONTEXT.md | 75 ++++++ .../GatewayAggregate/GatewayEnums.cs | 28 ++ .../GatewayAggregate/GwServiceInstance.cs | 69 +++++ .../GatewayAggregate/GwTenant.cs | 54 ++++ .../GatewayAggregate/GwTenantRoute.cs | 74 ++++++ .../Extensions.cs | 8 + .../Fengling.Platform.Infrastructure.csproj | 1 + .../IInstanceStore.cs | 23 ++ .../IRouteManager.cs | 18 ++ .../IRouteStore.cs | 23 ++ .../InstanceStore.cs | 108 ++++++++ .../PlatformDbContext.cs | 41 ++- .../RouteManager.cs | 38 +++ .../RouteStore.cs | 108 ++++++++ 26 files changed, 2241 insertions(+), 1 deletion(-) create mode 100644 .planning/ROADMAP.md create mode 100644 .planning/STATE.md create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md create mode 100644 .planning/phases/01-gateway-routing/01-gateway-routing-01-PLAN.md create mode 100644 .planning/phases/01-gateway-routing/01-gateway-routing-02-PLAN.md create mode 100644 .planning/phases/01-gateway-routing/01-gateway-routing-03-PLAN.md create mode 100644 .planning/phases/01-gateway-routing/01-gateway-routing-CONTEXT.md create mode 100644 Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs create mode 100644 Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs create mode 100644 Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs create mode 100644 Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs create mode 100644 Fengling.Platform.Infrastructure/IInstanceStore.cs create mode 100644 Fengling.Platform.Infrastructure/IRouteManager.cs create mode 100644 Fengling.Platform.Infrastructure/IRouteStore.cs create mode 100644 Fengling.Platform.Infrastructure/InstanceStore.cs create mode 100644 Fengling.Platform.Infrastructure/RouteManager.cs create mode 100644 Fengling.Platform.Infrastructure/RouteStore.cs diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..9b6034c --- /dev/null +++ b/.planning/ROADMAP.md @@ -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()` diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..52a30e2 --- /dev/null +++ b/.planning/STATE.md @@ -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) +- 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 diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..d96c719 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -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` +- 包含: RealName、TenantInfo、时间戳、软删除标志 +- 位置: `Fengling.Platform.Domain/AggregatesModel/UserAggregate/` + +**RoleAggregate:** +- 实体: `ApplicationRole` - 继承 `IdentityRole` +- 包含: 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` +- 模式: 自定义 Store 模式(ASP.NET Core Identity 风格) + +**ITenantManager:** +- 目的: 租户操作的 应用服务 +- 接口位置: `Fengling.Platform.Infrastructure/TenantManager.cs` +- 实现: `TenantManager` +- 委托给 ITenantStore + +**PlatformDbContext:** +- 目的: EF Core 数据库上下文 +- 位置: `Fengling.Platform.Infrastructure/PlatformDbContext.cs` +- 继承: `IdentityDbContext` +- 包含: Tenant、AccessLog、AuditLog DbSets + +## 入口点 + +**依赖注入注册:** +- 方法: `Extensions.AddPlatformCore()` +- 位置: `Fengling.Platform.Infrastructure/Extensions.cs` +- 注册: DbContext、ITenantStore、ITenantManager + +**数据库上下文:** +- 类型: `PlatformDbContext` +- 数据库: PostgreSQL(通过 Npgsql) +- 迁移位置: `Fengling.Platform.Infrastructure/Migrations/` + +## 错误处理 + +**策略:** 标准 ASP.NET Core Identity 结果模式 + +**模式:** +- CRUD 操作返回 `IdentityResult`(成功/失败) +- 空检查返回 `Task.FromResult(null)` 表示未找到 +- 当前未实现自定义异常处理层 + +## 横切关注点 + +**日志:** 此层未明确配置 + +**验证:** 未明确实现(依赖 ASP.NET Core Identity) + +**认证:** 使用 ASP.NET Core Identity + OpenIddict(从迁移中可见) + +**多租户:** +- 方法: 按 TenantInfo.TenantId 过滤 +- TenantInfo 作为拥有实体嵌入 ApplicationUser +- 通过实体上的 IsDeleted 标志进行软删除 + +--- + +*架构分析: 2026-02-28* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..fcba900 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -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` 需要泛型参数,但 `ITenantStore` 接口是非泛型的,使 DI 注册变得笨拙 +- 文件: `Fengling.Platform.Infrastructure/TenantStore.cs`、`Fengling.Platform.Infrastructure/ITenantStore.cs` +- 触发: 注册 `TenantStore` 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?` - 无加密或验证 +- 文件: `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` 实现 `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* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..3178540 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -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:** 启用 +- **可空:** 启用 +- **文档生成:** 启用(`true`) +- **集中包管理:** 启用(`true`) + +### 格式化 +- **大括号:** 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(null)` +```csharp +if (tenantId == null) return Task.FromResult(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 : ITenantStore +where TContext : PlatformDbContext +``` + +### 方法返回类型 +- 异步方法: 返回 `Task` 或 `Task` +- 集合查询: 返回 `IList` 或 `IQueryable` +- 单个实体: 返回 `T?` + +## 注释 + +### 何时注释 +- **中文注释:** 用于领域概念和解释 + ```csharp + /// + /// 租户信息 + /// + public record TenantInfo(...) + ``` +- **XML 文档:** 用于公共 API +- **最少行内注释:** 仅用于复杂业务逻辑 + +### JSDoc/TSDoc +- 对公共 API 使用 `/// ` 和 `/// ` +- 位置: `TenantInfo.cs` 第 3-8 行、11-13 行 + +## 函数设计 + +### 异步方法 +- 始终接受 `CancellationToken cancellationToken = default` 作为最后一个参数 +- 一致使用 `async`/`await` +- 对有返回值操作返回 `Task` + +### 查询方法 +- **分页:** 接受 `page` 和 `pageSize` 参数 +- **过滤:** 接受可选过滤参数(`name`、`tenantCode`、`status`) +- **排序:** 按创建日期应用 `OrderByDescending` + +### IdentityResult 模式 +业务操作返回 `IdentityResult`: +```csharp +Task CreateAsync(Tenant tenant, CancellationToken cancellationToken = default); +Task UpdateAsync(Tenant tenant, CancellationToken cancellationToken = default); +Task DeleteAsync(Tenant tenant, CancellationToken cancellationToken = default); +``` + +## DbContext 设计 + +### 配置 +- **模式:** `OnModelCreating` 中的流式 API +- **实体配置:** 使用 `modelBuilder.Entity(entity => {...})` +- **索引配置:** 使用 `entity.HasIndex(e => e.Property)` +- **拥有类型:** 使用 `entity.OwnsOne(e => e.TenantInfo, navigationBuilder => {...})` + +### 配置发现 +```csharp +modelBuilder.ApplyConfigurationsFromAssembly(typeof(PlatformDbContext).Assembly); +``` + +--- + +*约定分析: 2026-02-28* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..ad84319 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -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()` 配置 + - 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`) + - 角色: `Fengling.Platform.Domain/AggregatesModel/RoleAggregate/ApplicationRole.cs` + - 实现: `Microsoft.AspNetCore.Identity.EntityFrameworkCore` + +**多租户:** +- 通过 `ITenantStore` 和 `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* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..c7b139c --- /dev/null +++ b/.planning/codebase/STACK.md @@ -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()` 扩展方法加载 +- 数据库上下文通过 `AddDbContext()` 模式注册 + +**构建:** +- `Directory.Packages.props` - 集中包版本管理 +- `NuGet.Config` - 包源配置 +- `Dockerfile` - 多阶段构建(基础、构建、发布、最终) + +## 平台要求 + +**开发:** +- .NET 10.0 SDK +- PostgreSQL 数据库 +- 支持 C# 的 IDE (Rider, VS, VS Code) + +**生产:** +- Docker 容器化(存在 Dockerfile) +- PostgreSQL 数据库 +- 多租户租户隔离 + +--- + +*技术栈分析: 2026-02-28* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..0929a68 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -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* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..8414957 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -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 _storeMock; + + public TenantManagerTests() + { + _storeMock = new Mock(); + _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())) + .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())) + .ReturnsAsync(expectedTenant); + +// 空返回模拟 +_storeMock.Setup(s => s.FindByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Tenant?)null); + +// 验证调用 +_storeMock.Verify(s => s.CreateAsync(It.IsAny(), It.IsAny()), 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 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() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + _context = new PlatformDbContext(options); + } +} +``` + +### 端到端测试 +- **不适用:** 这是一个库/项目,不是应用程序 + +## 常见模式 + +### 异步测试 + +```csharp +[Fact] +public async Task GetAllAsync_ReturnsAllTenants() +{ + // Arrange + var tenants = new List { CreateValidTenant(), CreateValidTenant(2) }; + _storeMock.Setup(s => s.GetAllAsync(It.IsAny())) + .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())) + .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* diff --git a/.planning/phases/01-gateway-routing/01-gateway-routing-01-PLAN.md b/.planning/phases/01-gateway-routing/01-gateway-routing-01-PLAN.md new file mode 100644 index 0000000..52fe8b1 --- /dev/null +++ b/.planning/phases/01-gateway-routing/01-gateway-routing-01-PLAN.md @@ -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/ (源实体) + +## 任务 + + + 任务 1: 创建 GatewayEnums + Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs + +创建网关实体使用的枚举类型: +- RouteStatus (Active=1, Inactive=0) +- InstanceHealth (Healthy=1, Unhealthy=0) +- InstanceStatus (Active=1, Inactive=0) + +参考现有: Fengling.Platform.Domain/AggregatesModel/TenantAggregate/Tenant.cs + + 文件可编译,枚举可访问 + GatewayEnums.cs 已创建,包含 RouteStatus, InstanceHealth, InstanceStatus + + + + 任务 2: 创建 GwTenant 实体 + Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs + +创建 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 + + dotnet build 通过 + GwTenant 实体包含 Id, TenantCode, TenantName, Status 字段 + + + + 任务 3: 创建 GwTenantRoute 实体 + Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs + +创建 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 + + dotnet build 通过 + GwTenantRoute 实体包含路由配置字段 + + + + 任务 4: 创建 GwServiceInstance 实体 + Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs + +创建 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 + + dotnet build 通过 + GwServiceInstance 实体包含实例管理字段 + + + + +## 验证 + +- [ ] 所有 4 个实体文件已创建 +- [ ] Build 无错误通过 +- [ ] 实体遵循 Platform 约定 (long ID, PascalCase, 时间戳) + +## 成功标准 + +领域实体准备好进行基础设施层实现。 diff --git a/.planning/phases/01-gateway-routing/01-gateway-routing-02-PLAN.md b/.planning/phases/01-gateway-routing/01-gateway-routing-02-PLAN.md new file mode 100644 index 0000000..d014571 --- /dev/null +++ b/.planning/phases/01-gateway-routing/01-gateway-routing-02-PLAN.md @@ -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) + +## 任务 + + + 任务 1: 创建 GatewayDbContext + Fengling.Platform.Infrastructure/GatewayDbContext.cs + +创建继承 DbContext 的 GatewayDbContext: +- 添加 DbSet GwTenants +- 添加 DbSet GwTenantRoutes +- 添加 DbSet GwServiceInstances +- 在 OnModelCreating 中配置索引 (参考源 ../fengling-gateway) + +关键索引: +- GwTenant: TenantCode 唯一 +- GwTenantRoute: TenantCode, ServiceName, ClusterId, 复合索引 (ServiceName, IsGlobal, Status) +- GwServiceInstance: 复合索引 (ClusterId, DestinationId) 唯一, Health 索引 + + dotnet build 通过 + GatewayDbContext 包含所有 DbSet 和索引配置 + + + + 任务 2: 创建 IRouteStore 和 RouteStore + + Fengling.Platform.Infrastructure/IRouteStore.cs + Fengling.Platform.Infrastructure/RouteStore.cs + + +创建 IRouteStore 接口,包含方法: +- FindByIdAsync, FindByTenantCodeAsync, FindByClusterIdAsync +- GetAllAsync, GetPagedAsync, GetCountAsync +- CreateAsync, UpdateAsync, DeleteAsync (返回 IdentityResult) + +创建实现 IRouteStore 的 RouteStore: +- 参考 TenantStore 模式 +- 泛型约束: where TContext : GatewayDbContext +- 实现所有 CRUD 方法,支持软删除 + + dotnet build 通过 + IRouteStore 接口和 RouteStore 实现 + + + + 任务 3: 创建 IRouteManager 和 RouteManager + + Fengling.Platform.Infrastructure/IRouteManager.cs + Fengling.Platform.Infrastructure/RouteManager.cs + + +创建 IRouteManager 接口,包含方法: +- FindByIdAsync, FindByTenantCodeAsync, GetAllAsync +- CreateRouteAsync, UpdateRouteAsync, DeleteRouteAsync + +创建实现 IRouteManager 的 RouteManager: +- 参考 TenantManager 模式 +- 构造函数: public RouteManager(IRouteStore store) +- 委托给 IRouteStore 进行数据操作 + + dotnet build 通过 + IRouteManager 接口和 RouteManager 实现 + + + + 任务 4: 创建 IInstanceStore 和 InstanceStore + + Fengling.Platform.Infrastructure/IInstanceStore.cs + Fengling.Platform.Infrastructure/InstanceStore.cs + + +创建 IInstanceStore 接口: +- FindByIdAsync, FindByClusterIdAsync, FindByDestinationAsync +- GetAllAsync, GetPagedAsync, GetCountAsync +- CreateAsync, UpdateAsync, DeleteAsync + +创建实现 IInstanceStore 的 InstanceStore: +- 类似 RouteStore 的模式 +- 重点在 ClusterId 和 DestinationId 查询 + + dotnet build 通过 + IInstanceStore 接口和 InstanceStore 实现 + + + + +## 验证 + +- [ ] GatewayDbContext 包含所有 DbSet 可编译 +- [ ] Store 实现遵循 TenantStore 模式 +- [ ] Manager 实现委托给 Stores +- [ ] Build 无错误通过 + +## 成功标准 + +基础设施层准备好进行 Extensions 集成。 diff --git a/.planning/phases/01-gateway-routing/01-gateway-routing-03-PLAN.md b/.planning/phases/01-gateway-routing/01-gateway-routing-03-PLAN.md new file mode 100644 index 0000000..b74004c --- /dev/null +++ b/.planning/phases/01-gateway-routing/01-gateway-routing-03-PLAN.md @@ -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 扩展方法" + exports: ["GatewayDbContext", "IRouteStore", "IRouteManager", "IInstanceStore"] + key_links: + - from: "AddGatewayCore" + to: "AddPlatformCore" + via: "可以一起调用" + pattern: "services.AddPlatformCore().AddGatewayCore()" +--- + +# 计划 03: 网关扩展方法 + +## 目标 + +创建 Extensions 类用于快速 IoC 注册 Gateway 服务。 + +**目的:** 支持单行注册所有 Gateway 服务,类似于 AddPlatformCore。 + +**输出:** 包含 AddGatewayCore 方法的 GatewayExtensions.cs。 + +## 上下文 + +@Fengling.Platform.Infrastructure/Extensions.cs (模式参考) +@Fengling.Platform.Infrastructure/GatewayDbContext.cs (来自计划 02) + +## 任务 + + + 任务 1: 创建 GatewayExtensions + Fengling.Platform.Infrastructure/GatewayExtensions.cs + +创建 GatewayExtensions 静态类: + +```csharp +public static class GatewayExtensions +{ + public static IServiceCollection AddGatewayCore(this IServiceCollection services) + where TContext : GatewayDbContext + { + // 注册 Gateway stores + services.AddScoped>(); + services.AddScoped>(); + + // 注册 Gateway managers + services.AddScoped(); + + return services; + } +} +``` + +参考: Fengling.Platform.Infrastructure/Extensions.cs 模式 + + + dotnet build Fengling.Platform.Infrastructure + + AddGatewayCore 扩展方法注册所有 Gateway 服务 + + + + 任务 2: 生成数据库迁移 + Fengling.Platform.Infrastructure/Migrations/ + +为 Gateway 实体生成 EF Core 迁移: + +1. 创建初始迁移: + - 从 Infrastructure 目录 + - 迁移名称: InitialGateway + +2. 验证迁移包含: + - GwTenants 表 + - GwTenantRoutes 表 + - GwServiceInstances 表 + - GatewayDbContext 中定义的所有索引 + + + dotnet ef migrations list --project Fengling.Platform.Infrastructure + + 迁移已生成并列出 + + + + +## 验证 + +- [ ] GatewayExtensions.cs 可编译 +- [ ] AddGatewayCore 方法注册所有服务 +- [ ] 迁移成功生成 +- [ ] Build 通过 + +## 成功标准 + +Gateway 路由功能完全集成并可使用。 + +## 使用示例 + +```csharp +// 在 Program.cs 或 startup 中 +services.AddPlatformCore(options => + options.UseNpgsql(connectionString)); + +services.AddGatewayCore(options => + options.UseNpgsql(gatewayConnectionString)); +``` diff --git a/.planning/phases/01-gateway-routing/01-gateway-routing-CONTEXT.md b/.planning/phases/01-gateway-routing/01-gateway-routing-CONTEXT.md new file mode 100644 index 0000000..00a6039 --- /dev/null +++ b/.planning/phases/01-gateway-routing/01-gateway-routing-CONTEXT.md @@ -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()` 扩展方法 +- 注册: 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(this IServiceCollection services) + where TContext : GatewayDbContext; +``` + +## Deferred Ideas + +- 服务发现集成 (后续阶段) +- 动态配置热加载 (后续阶段) +- 复杂的负载均衡策略 (后续阶段) + +## Claude's Discretion + +- 实体命名: 保持 Gw 前缀还是简化? (建议保留以避免冲突) +- 是否复用现有 TenantStore? (建议新建独立 Store) +- 是否需要 YARP 集成? (仅实体层,先不包含 YARP 特定配置) diff --git a/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs new file mode 100644 index 0000000..f926e6c --- /dev/null +++ b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GatewayEnums.cs @@ -0,0 +1,28 @@ +namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +/// +/// 路由状态枚举 +/// +public enum RouteStatus +{ + Inactive = 0, + Active = 1 +} + +/// +/// 服务实例健康状态枚举 +/// +public enum InstanceHealth +{ + Unhealthy = 0, + Healthy = 1 +} + +/// +/// 服务实例状态枚举 +/// +public enum InstanceStatus +{ + Inactive = 0, + Active = 1 +} diff --git a/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs new file mode 100644 index 0000000..74033b4 --- /dev/null +++ b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwServiceInstance.cs @@ -0,0 +1,69 @@ +namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +/// +/// 网关服务实例实体 - 表示负载均衡的服务实例 +/// +public class GwServiceInstance +{ + public long Id { get; set; } + + /// + /// 集群ID + /// + public string ClusterId { get; set; } = string.Empty; + + /// + /// 目标ID + /// + public string DestinationId { get; set; } = string.Empty; + + /// + /// 地址 + /// + public string Address { get; set; } = string.Empty; + + /// + /// 健康状态 + /// + public int Health { get; set; } = 1; + + /// + /// 权重 + /// + public int Weight { get; set; } = 1; + + /// + /// 状态 + /// + public int Status { get; set; } = 1; + + /// + /// 创建人ID + /// + public long? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + /// + /// 更新人ID + /// + public long? UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdatedTime { get; set; } + + /// + /// 是否删除 + /// + public bool IsDeleted { get; set; } = false; + + /// + /// 版本号,用于乐观并发 + /// + public int Version { get; set; } = 0; +} diff --git a/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs new file mode 100644 index 0000000..9556630 --- /dev/null +++ b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenant.cs @@ -0,0 +1,54 @@ +namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +/// +/// 网关租户实体 - 表示租户在网关中的配置 +/// +public class GwTenant +{ + public long Id { get; set; } + + /// + /// 租户代码 + /// + public string TenantCode { get; set; } = string.Empty; + + /// + /// 租户名称 + /// + public string TenantName { get; set; } = string.Empty; + + /// + /// 状态 + /// + public int Status { get; set; } = 1; + + /// + /// 创建人ID + /// + public long? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + /// + /// 更新人ID + /// + public long? UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdatedTime { get; set; } + + /// + /// 是否删除 + /// + public bool IsDeleted { get; set; } = false; + + /// + /// 版本号,用于乐观并发 + /// + public int Version { get; set; } = 0; +} diff --git a/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs new file mode 100644 index 0000000..c372f88 --- /dev/null +++ b/Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs @@ -0,0 +1,74 @@ +namespace Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +/// +/// 网关租户路由实体 - 表示路由规则配置 +/// +public class GwTenantRoute +{ + public long Id { get; set; } + + /// + /// 租户代码 + /// + public string TenantCode { get; set; } = string.Empty; + + /// + /// 服务名称 + /// + public string ServiceName { get; set; } = string.Empty; + + /// + /// 集群ID + /// + public string ClusterId { get; set; } = string.Empty; + + /// + /// 路径匹配模式 + /// + public string PathPattern { get; set; } = string.Empty; + + /// + /// 优先级 + /// + public int Priority { get; set; } = 0; + + /// + /// 状态 + /// + public int Status { get; set; } = 1; + + /// + /// 是否全局路由 + /// + public bool IsGlobal { get; set; } = false; + + /// + /// 创建人ID + /// + public long? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + /// + /// 更新人ID + /// + public long? UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdatedTime { get; set; } + + /// + /// 是否删除 + /// + public bool IsDeleted { get; set; } = false; + + /// + /// 版本号,用于乐观并发 + /// + public int Version { get; set; } = 0; +} diff --git a/Fengling.Platform.Infrastructure/Extensions.cs b/Fengling.Platform.Infrastructure/Extensions.cs index 16dcdc5..f52b040 100644 --- a/Fengling.Platform.Infrastructure/Extensions.cs +++ b/Fengling.Platform.Infrastructure/Extensions.cs @@ -18,8 +18,16 @@ public static class Extensions services.AddDbContext(optionsAction); } } + + // Platform 服务 services.AddScoped>(); services.AddScoped(); + + // Gateway 服务 + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped(); + serviceAction?.Invoke(services); return services; diff --git a/Fengling.Platform.Infrastructure/Fengling.Platform.Infrastructure.csproj b/Fengling.Platform.Infrastructure/Fengling.Platform.Infrastructure.csproj index 08493d9..4f648d5 100644 --- a/Fengling.Platform.Infrastructure/Fengling.Platform.Infrastructure.csproj +++ b/Fengling.Platform.Infrastructure/Fengling.Platform.Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/Fengling.Platform.Infrastructure/IInstanceStore.cs b/Fengling.Platform.Infrastructure/IInstanceStore.cs new file mode 100644 index 0000000..362d705 --- /dev/null +++ b/Fengling.Platform.Infrastructure/IInstanceStore.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Identity; + +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Platform.Infrastructure; + +/// +/// 服务实例存储接口 +/// +public interface IInstanceStore +{ + Task FindByIdAsync(long? id, CancellationToken cancellationToken = default); + Task FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default); + Task FindByDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetPagedAsync(int page, int pageSize, string? clusterId = null, + InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default); + Task GetCountAsync(string? clusterId = null, + InstanceHealth? health = null, InstanceStatus? status = null, CancellationToken cancellationToken = default); + Task CreateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default); + Task UpdateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default); + Task DeleteAsync(GwServiceInstance instance, CancellationToken cancellationToken = default); +} diff --git a/Fengling.Platform.Infrastructure/IRouteManager.cs b/Fengling.Platform.Infrastructure/IRouteManager.cs new file mode 100644 index 0000000..ff5d44e --- /dev/null +++ b/Fengling.Platform.Infrastructure/IRouteManager.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Identity; + +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Platform.Infrastructure; + +/// +/// 路由管理器接口 +/// +public interface IRouteManager +{ + Task FindByIdAsync(long? id, CancellationToken cancellationToken = default); + Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task CreateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default); + Task UpdateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default); + Task DeleteRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default); +} diff --git a/Fengling.Platform.Infrastructure/IRouteStore.cs b/Fengling.Platform.Infrastructure/IRouteStore.cs new file mode 100644 index 0000000..8b4bdc4 --- /dev/null +++ b/Fengling.Platform.Infrastructure/IRouteStore.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Identity; + +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Platform.Infrastructure; + +/// +/// 路由存储接口 +/// +public interface IRouteStore +{ + Task FindByIdAsync(long? id, CancellationToken cancellationToken = default); + Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default); + Task FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetPagedAsync(int page, int pageSize, string? tenantCode = null, + string? serviceName = null, RouteStatus? status = null, CancellationToken cancellationToken = default); + Task GetCountAsync(string? tenantCode = null, string? serviceName = null, + RouteStatus? status = null, CancellationToken cancellationToken = default); + Task CreateAsync(GwTenantRoute route, CancellationToken cancellationToken = default); + Task UpdateAsync(GwTenantRoute route, CancellationToken cancellationToken = default); + Task DeleteAsync(GwTenantRoute route, CancellationToken cancellationToken = default); +} diff --git a/Fengling.Platform.Infrastructure/InstanceStore.cs b/Fengling.Platform.Infrastructure/InstanceStore.cs new file mode 100644 index 0000000..7fa5157 --- /dev/null +++ b/Fengling.Platform.Infrastructure/InstanceStore.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Platform.Infrastructure; + +/// +/// 服务实例存储实现 +/// +public class InstanceStore : IInstanceStore + where TContext : PlatformDbContext +{ + private readonly TContext _context; + private readonly DbSet _instances; + + public InstanceStore(TContext context) + { + _context = context; + _instances = context.GwServiceInstances; + } + + public void Dispose() { } + + public virtual Task FindByIdAsync(long? id, CancellationToken cancellationToken = default) + { + if (id == null) return Task.FromResult(null); + return _instances.FirstOrDefaultAsync(i => i.Id == id, cancellationToken); + } + + public virtual Task FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default) + { + return _instances.FirstOrDefaultAsync(i => i.ClusterId == clusterId && !i.IsDeleted, cancellationToken); + } + + public virtual Task 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> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _instances.Where(i => !i.IsDeleted).ToListAsync(cancellationToken); + } + + public virtual async Task> 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 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 CreateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default) + { + _instances.Add(instance); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task UpdateAsync(GwServiceInstance instance, CancellationToken cancellationToken = default) + { + instance.UpdatedTime = DateTime.UtcNow; + _instances.Update(instance); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task DeleteAsync(GwServiceInstance instance, CancellationToken cancellationToken = default) + { + // 软删除 + instance.IsDeleted = true; + instance.UpdatedTime = DateTime.UtcNow; + _instances.Update(instance); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } +} diff --git a/Fengling.Platform.Infrastructure/PlatformDbContext.cs b/Fengling.Platform.Infrastructure/PlatformDbContext.cs index 4ee1efa..584346f 100644 --- a/Fengling.Platform.Infrastructure/PlatformDbContext.cs +++ b/Fengling.Platform.Infrastructure/PlatformDbContext.cs @@ -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; @@ -13,7 +15,12 @@ public class PlatformDbContext(DbContextOptions options) { public DbSet Tenants => Set(); public DbSet AccessLogs => Set(); - public DbSet AuditLogs => Set(); + public DbSet AuditLogs => Set(); + + // Gateway 实体 + public DbSet GwTenants => Set(); + public DbSet GwTenantRoutes => Set(); + public DbSet GwServiceInstances => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -78,6 +85,38 @@ public class PlatformDbContext(DbContextOptions options) entity.Property(e => e.Status).HasMaxLength(20); }); + // Gateway 实体配置 + modelBuilder.Entity(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(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(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); } diff --git a/Fengling.Platform.Infrastructure/RouteManager.cs b/Fengling.Platform.Infrastructure/RouteManager.cs new file mode 100644 index 0000000..7df7f40 --- /dev/null +++ b/Fengling.Platform.Infrastructure/RouteManager.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Identity; +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Platform.Infrastructure; + +/// +/// 路由管理器实现 +/// +public class RouteManager : IRouteManager +{ + private readonly IRouteStore _store; + + public RouteManager(IRouteStore store) + { + _store = store; + } + + public virtual Task FindByIdAsync(long? id, CancellationToken cancellationToken = default) + => _store.FindByIdAsync(id, cancellationToken); + + public virtual Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default) + => _store.FindByTenantCodeAsync(tenantCode, cancellationToken); + + public virtual Task> GetAllAsync(CancellationToken cancellationToken = default) + => _store.GetAllAsync(cancellationToken); + + public virtual Task CreateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default) + { + route.CreatedTime = DateTime.UtcNow; + return _store.CreateAsync(route, cancellationToken); + } + + public virtual Task UpdateRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default) + => _store.UpdateAsync(route, cancellationToken); + + public virtual Task DeleteRouteAsync(GwTenantRoute route, CancellationToken cancellationToken = default) + => _store.DeleteAsync(route, cancellationToken); +} diff --git a/Fengling.Platform.Infrastructure/RouteStore.cs b/Fengling.Platform.Infrastructure/RouteStore.cs new file mode 100644 index 0000000..5e4dd5d --- /dev/null +++ b/Fengling.Platform.Infrastructure/RouteStore.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Platform.Infrastructure; + +/// +/// 路由存储实现 +/// +public class RouteStore : IRouteStore + where TContext : PlatformDbContext +{ + private readonly TContext _context; + private readonly DbSet _routes; + + public RouteStore(TContext context) + { + _context = context; + _routes = context.GwTenantRoutes; + } + + public void Dispose() { } + + public virtual Task FindByIdAsync(long? id, CancellationToken cancellationToken = default) + { + if (id == null) return Task.FromResult(null); + return _routes.FirstOrDefaultAsync(r => r.Id == id, cancellationToken); + } + + public virtual Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default) + { + return _routes.FirstOrDefaultAsync(r => r.TenantCode == tenantCode && !r.IsDeleted, cancellationToken); + } + + public virtual Task FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default) + { + return _routes.FirstOrDefaultAsync(r => r.ClusterId == clusterId && !r.IsDeleted, cancellationToken); + } + + public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _routes.Where(r => !r.IsDeleted).ToListAsync(cancellationToken); + } + + public virtual async Task> 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 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 CreateAsync(GwTenantRoute route, CancellationToken cancellationToken = default) + { + _routes.Add(route); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task UpdateAsync(GwTenantRoute route, CancellationToken cancellationToken = default) + { + route.UpdatedTime = DateTime.UtcNow; + _routes.Update(route); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task DeleteAsync(GwTenantRoute route, CancellationToken cancellationToken = default) + { + // 软删除 + route.IsDeleted = true; + route.UpdatedTime = DateTime.UtcNow; + _routes.Update(route); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } +}