feat: 添加 Gateway 路由实体到 Platform
Some checks failed
Build and Push Docker / build (push) Failing after 23s
Publish NuGet Packages / build (push) Failing after 8s

- 新增 GatewayAggregate 领域实体 (GwTenant, GwTenantRoute, GwServiceInstance)
- 新增 IRouteStore, RouteStore, IInstanceStore, InstanceStore
- 新增 IRouteManager, RouteManager
- 合并 GatewayDbContext 到 PlatformDbContext
- 统一 Extensions.AddPlatformCore 注册所有服务
This commit is contained in:
movingsam 2026-02-28 23:53:00 +08:00
parent 7877f89d35
commit 1b8c937aa4
26 changed files with 2241 additions and 1 deletions

47
.planning/ROADMAP.md Normal file
View 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
View 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

View 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. TenantInfoTenantId、租户代码、租户名称嵌入用户实体
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*

View 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*

View 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 LFWindows 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*

View 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*

View 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*

View 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*

View 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%
- **目标:** 关键路径(租户 CRUD80%
### 查看覆盖率
```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*

View File

@ -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, 时间戳)
## 成功标准
领域实体准备好进行基础设施层实现。

View File

@ -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 集成。

View File

@ -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));
```

View File

@ -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 特定配置)

View File

@ -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
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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>

View 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);
}

View 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);
}

View 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);
}

View 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;
}
}

View File

@ -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<Tenant> Tenants => Set<Tenant>();
public DbSet<AccessLog> AccessLogs => Set<AccessLog>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
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)
{
@ -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);
}

View 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);
}

View 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;
}
}