docs: remove GSD workflow planning documents
- Remove .planning/ directory (GSD workflow artifacts) - Remove 网关配置的新想法.md (outdated design doc) - Keep only essential technical documentation
This commit is contained in:
parent
7ca5e879b4
commit
ca27d8659d
@ -1,88 +0,0 @@
|
||||
# Fengling Gateway
|
||||
|
||||
## 这是什么
|
||||
|
||||
基于 YARP (Yet Another Reverse Proxy) 的 API 网关,用于风灵微服务生态系统。支持多租户路由、动态配置和分布式负载均衡,将请求路由到下游服务(auth-service、member-service、activity、platform、risk-control 等)。
|
||||
|
||||
## 核心价值
|
||||
|
||||
可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。
|
||||
|
||||
## 需求
|
||||
|
||||
### 已验证(现有功能)
|
||||
|
||||
- ✓ 基于 URL 路径的多租户路由 — 已有
|
||||
- ✓ JWT Token 解析和租户声明提取 — 已有
|
||||
- ✓ PostgreSQL 动态路由配置 — 已有
|
||||
- ✓ Kubernetes 服务发现集成 — 已有
|
||||
- ✓ 加权轮询负载均衡 — 已有
|
||||
- ✓ 通过 PostgreSQL NOTIFY 实现配置热重载 — 已有
|
||||
|
||||
### 进行中
|
||||
|
||||
- [ ] 实现 console 驱动的配置管理(配置在 fengling-console 变更,网关监听并重载)
|
||||
- [ ] 通过 PostgreSQL NOTIFY 广播支持多网关实例部署
|
||||
- [ ] 将 K8s 健康检查从网关移除(委托给 console)
|
||||
|
||||
### 范围外
|
||||
|
||||
- [直接配置网关的 UI] — 由 fengling-console 负责
|
||||
- [网关中的 K8s 服务健康检查] — 委托给 console
|
||||
- [认证/授权逻辑] — 由 auth-service 负责
|
||||
|
||||
## 背景
|
||||
|
||||
**生态系统结构:**
|
||||
```
|
||||
fengling-gateway/ # 当前项目 - API 网关 (YARP)
|
||||
↓ 路由流量到:
|
||||
fengling-console/ # 中央管理控制台 - 配置、租户管理
|
||||
fengling-console-web/ # 控制台 Web UI
|
||||
fengling-auth-service/ # 认证服务
|
||||
fengling-member-service/ # 会员服务
|
||||
fengling-activity/ # 活动服务
|
||||
fengling-platform/ # 平台服务
|
||||
fengling-risk-control/ # 风控服务
|
||||
fengling-service-discovery/# 服务发现
|
||||
```
|
||||
|
||||
**架构决策(新):**
|
||||
- 网关配置由 fengling-console 管理,网关不直接配置
|
||||
- Console 发布配置变更 → 网关订阅并重载
|
||||
- 多实例支持通过 PostgreSQL NOTIFY/LISTEN 实现(更轻量,无需 Redis)
|
||||
- Console 负责所有 K8s 服务健康监控
|
||||
- 网关只处理请求路由
|
||||
|
||||
**当前问题(来自 CONCERNS.md):**
|
||||
- 硬编码凭据(安全风险)
|
||||
- JWT Token 未验证(安全风险)
|
||||
- API 端点无认证(安全风险)
|
||||
- 负载均衡锁竞争
|
||||
- 缺少单元测试
|
||||
|
||||
**Console 集成现状:**
|
||||
- fengling-console 已实现 GatewayController 和 GatewayService
|
||||
- Console 拥有 GatewayDbContext,可直接管理网关配置数据
|
||||
- Console 的 ReloadGatewayAsync() 目前为空实现,未实现广播机制
|
||||
- 网关已有 PgSqlConfigChangeListener 使用 NOTIFY/LISTEN,可复用
|
||||
|
||||
## 约束
|
||||
|
||||
- **多实例**:网关必须支持同时运行多个实例
|
||||
- **热重载**:配置变更无需重启
|
||||
- **技术栈**:.NET 10.0, YARP, PostgreSQL
|
||||
- **部署**:Docker + Kubernetes
|
||||
|
||||
## 关键决策
|
||||
|
||||
| 决策 | 理由 | 结果 |
|
||||
|------|------|------|
|
||||
| Console 驱动配置 | 集中管理,单一事实来源 | ✓ 良好 |
|
||||
| PostgreSQL NOTIFY 广播 | 轻量方案,无需额外依赖 | ✓ 良好 |
|
||||
| K8s 健康委托给 console | 降低网关复杂度,console 是运维中心 | ✓ 良好 |
|
||||
| 保持 YARP 为核心 | 微软维护,支持良好 | ✓ 良好 |
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-02 初始化后*
|
||||
@ -1,102 +0,0 @@
|
||||
# 需求文档:Fengling Gateway
|
||||
|
||||
**定义日期:** 2026-03-02
|
||||
**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。
|
||||
|
||||
## v1 需求
|
||||
|
||||
初始发布版本的需求。每个需求对应一个 Roadmap 阶段。
|
||||
|
||||
### 配置管理
|
||||
|
||||
- [x] **CFG-01**:网关监听来自 fengling-console 的配置变更事件(PostgreSQL NOTIFY)
|
||||
- [x] **CFG-02**:收到通知后网关无需重启即可重载配置
|
||||
- [x] **CFG-03**:多实例网关通过 PostgreSQL NOTIFY 广播接收配置更新
|
||||
|
||||
### 多实例支持
|
||||
|
||||
- [x] **INST-01**:多个网关实例可以同时运行
|
||||
- [x] **INST-02**:配置变更通过 NOTIFY 广播传播到所有实例
|
||||
- [x] **INST-03**:使用 PostgreSQL LISTEN 订阅配置变更频道
|
||||
|
||||
### K8s 健康委托
|
||||
|
||||
- [x] **K8S-01**:从网关注销 K8s 健康监控
|
||||
- [x] **K8S-02**:网关将服务健康检查委托给 console
|
||||
|
||||
- [ ] **K8S-01**:从网关注销 K8s 健康监控
|
||||
- [ ] **K8S-02**:网关将服务健康检查委托给 console
|
||||
|
||||
### 安全修复
|
||||
|
||||
- [ ] **SEC-01**:移除源代码中的硬编码凭据
|
||||
- [ ] **SEC-02**:实现正确的 JWT Token 验证
|
||||
- [ ] **SEC-03**:为网关管理 API 端点添加认证
|
||||
|
||||
### 性能优化
|
||||
|
||||
- [ ] **PERF-01**:优化负载均衡锁竞争
|
||||
- [ ] **PERF-02**:实现增量路由缓存更新
|
||||
|
||||
## v2 需求
|
||||
|
||||
延期到未来版本。已记录但不在当前 Roadmap 中。
|
||||
|
||||
### 可观测性
|
||||
|
||||
- **OBS-01**:分布式追踪集成
|
||||
- **OBS-02**:网关性能自定义指标
|
||||
|
||||
### 测试
|
||||
|
||||
- **TEST-01**:RouteCache 单元测试
|
||||
- **TEST-02**:JwtTransformMiddleware 单元测试
|
||||
- **TEST-03**:负载均衡策略单元测试
|
||||
|
||||
## 范围外
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| 直接配置网关的 UI | 由 fengling-console 处理 |
|
||||
| 网关中的 K8s 服务健康检查 | 委托给 console |
|
||||
| 网关中的认证逻辑 | 由 auth-service 处理 |
|
||||
| 网关中的授权逻辑 | 由下游服务处理 |
|
||||
|
||||
## 可追溯性
|
||||
|
||||
哪些阶段覆盖哪些需求。Roadmap 创建时更新。
|
||||
|
||||
| 需求 | 阶段 | 状态 |
|
||||
|------|------|------|
|
||||
| CFG-01 | 阶段 1 | ✅ 已完成 |
|
||||
| CFG-02 | 阶段 1 | ✅ 已完成 |
|
||||
| CFG-03 | 阶段 1 | ✅ 已完成 |
|
||||
| INST-01 | 阶段 1 | ✅ 已完成 |
|
||||
| INST-02 | 阶段 1 | ✅ 已完成 |
|
||||
| INST-03 | 阶段 1 | ✅ 已完成 |
|
||||
QH|| K8S-01 | 阶段 2 | ✅ 已完成 |
|
||||
BH|| K8S-02 | 阶段 2 | ✅ 已完成 |
|
||||
| K8S-02 | 阶段 2 | 待处理 |
|
||||
| SEC-01 | 阶段 3 | 待处理 |
|
||||
| SEC-02 | 阶段 3 | 待处理 |
|
||||
| SEC-03 | 阶段 3 | 待处理 |
|
||||
| PERF-01 | 阶段 4 | 待处理 |
|
||||
| PERF-02 | 阶段 4 | 待处理 |
|
||||
|
||||
**覆盖率:**
|
||||
- v1 需求:共 12 项
|
||||
- 已映射到阶段:12 项
|
||||
- 未映射:0 ✓
|
||||
- 已完成:8 项(阶段 1 + 阶段 2)
|
||||
- 待处理:4 项
|
||||
- 待处理:6 项
|
||||
|
||||
---
|
||||
|
||||
**阶段 1 完成说明:**
|
||||
- 现有 `PgSqlConfigChangeListener.cs` 已实现所有监听需求
|
||||
- 监听频道:`gateway_config_changed`
|
||||
- 包含断线重连和回退轮询机制
|
||||
|
||||
*需求定义:2026-03-02*
|
||||
*最后更新:2026-03-02 阶段1完成后*
|
||||
@ -1,198 +0,0 @@
|
||||
#MY|# Roadmap:Fengling Gateway
|
||||
|
||||
**创建日期:** 2026-03-02
|
||||
**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:配置变更监听与多实例支持 ✅ 已完成
|
||||
|
||||
**目标:** 实现网关对配置变更的监听机制,支持多实例部署。
|
||||
|
||||
> **注意:** 此阶段只实现 YARP 网关部分的监听代码。Console 端的广播通知由 fengling-console 项目负责。
|
||||
|
||||
**需求:**
|
||||
- [x] CFG-01:网关监听来自 fengling-console 的配置变更事件(PostgreSQL NOTIFY)
|
||||
- [x] CFG-02:收到通知后网关无需重启即可重载配置
|
||||
- [x] CFG-03:多实例网关通过 PostgreSQL NOTIFY 广播接收配置更新
|
||||
- [x] INST-01:多个网关实例可以同时运行
|
||||
- [x] INST-02:配置变更通过 NOTIFY 广播传播到所有实例
|
||||
- [x] INST-03:使用 PostgreSQL LISTEN 订阅配置变更频道
|
||||
|
||||
**成功标准:**
|
||||
- [x] 网关使用 LISTEN 订阅配置变更频道(如 `gateway_config_changed`)
|
||||
- [x] 收到 NOTIFY 后触发配置重载,无需重启
|
||||
- [x] 多个网关实例通过数据库 NOTIFY 保持同步
|
||||
- [x] 广播事件在 5 秒内到达所有实例
|
||||
|
||||
**实现文件:**
|
||||
- `src/yarpgateway/Services/PgSqlConfigChangeListener.cs`
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:K8s 健康检查委托 ✅ 已完成
|
||||
|
||||
**目标:** 将 K8s 服务健康监控从网关移除,委托给 fengling-console。
|
||||
|
||||
**需求:**
|
||||
- [x] K8S-01:从网关注销 K8s 健康监控
|
||||
- [x] K8S-02:网关将服务健康检查委托给 console
|
||||
|
||||
**成功标准:**
|
||||
- [x] KubernetesPendingSyncService 已从网关移除
|
||||
- [x] PendingServicesController 已从网关移除
|
||||
- [x] 网关只执行请求路由,不做健康监控
|
||||
|
||||
**已删除的文件:**
|
||||
- `Services/KubernetesPendingSyncService.cs`
|
||||
- `Controllers/PendingServicesController.cs`
|
||||
- `Models/GwPendingServiceDiscovery.cs`
|
||||
|
||||
**已修改的文件:**
|
||||
- `Program.cs` - 移除服务注册和 using 语句
|
||||
- `Data/GatewayDbContext.cs` - 移除 DbSet 和模型配置
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:安全加固
|
||||
|
||||
**目标:** 修复关键安全漏洞。
|
||||
|
||||
**需求:**
|
||||
- [ ] SEC-01:移除源代码中的硬编码凭据
|
||||
- [ ] SEC-02:实现正确的 JWT Token 验证
|
||||
- [ ] SEC-03:为网关管理 API 端点添加认证
|
||||
|
||||
**成功标准:**
|
||||
1. 源代码中无硬编码密码/密钥
|
||||
2. JWT Token 经过验证(签名、过期时间、颁发者、受众)
|
||||
3. 所有 /api/gateway/* 端点需要认证
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4:性能优化
|
||||
|
||||
**目标:** 优化高负载下的网关性能。
|
||||
|
||||
**需求:**
|
||||
- [ ] PERF-01:优化负载均衡锁竞争
|
||||
- [ ] PERF-02:实现增量路由缓存更新
|
||||
|
||||
**成功标准:**
|
||||
1. 负载均衡不需要每个请求都获取 Redis 锁
|
||||
2. 路由缓存更新是增量式的,而非全量重载
|
||||
3. 网关处理能力提升 10 倍
|
||||
|
||||
---
|
||||
|
||||
## 阶段 5:可观测性与测试
|
||||
|
||||
**目标:** 添加可观测性和测试覆盖。
|
||||
|
||||
**需求:**
|
||||
- [ ] OBS-01:分布式追踪集成
|
||||
- [ ] OBS-02:网关性能自定义指标
|
||||
- [ ] TEST-01:RouteCache 单元测试
|
||||
- [ ] TEST-02:JwtTransformMiddleware 单元测试
|
||||
- [ ] TEST-03:负载均衡策略单元测试
|
||||
|
||||
**成功标准:**
|
||||
1. 分布式追踪包含网关跨度
|
||||
2. 导出关键指标(请求数、延迟、错误率)
|
||||
3. 核心组件测试覆盖率 >80%
|
||||
|
||||
---
|
||||
|
||||
BZ|## 阶段 6:网关插件技术调研与实现 ✅ 进行中
|
||||
|
||||
**目标:** 实现网关插件化支持。
|
||||
|
||||
**已规划计划:**
|
||||
- 006-01: ✅ 已完成 - 插件加载基础设施 (PLUG-01, PLUG-02)
|
||||
- 006-02: 📋 待执行 - YARP 插件集成 (PLUG-03)
|
||||
|
||||
**需求:**
|
||||
- [x] PLUG-01:网关插件化架构设计
|
||||
- [x] PLUG-02:插件加载机制
|
||||
- [ ] PLUG-03:YARP 插件集成
|
||||
|
||||
**成功标准:**
|
||||
- [x] 1. 网关支持动态加载插件
|
||||
- [x] 2. 插件之间相互隔离
|
||||
- [ ] 3. 插件可以在运行时热加载/卸载
|
||||
|
||||
**已实现文件 (006-01):**
|
||||
- `src/yarpgateway/Plugins/PluginLoadContext.cs` - ALC 隔离
|
||||
- `src/yarpgateway/Plugins/PluginLoader.cs` - 发现和加载
|
||||
- `src/yarpgateway/Plugins/PluginHost.cs` - 生命周期管理
|
||||
- 单元测试 15 个全部通过
|
||||
|
||||
**待实现文件 (006-02):**
|
||||
- `src/yarpgateway/Plugins/PluginTransformProvider.cs` - YARP Transform 提供者
|
||||
- `src/yarpgateway/Plugins/DestinationSelector.cs` - 目标选择器
|
||||
- `src/yarpgateway/Plugins/PluginConfigWatcher.cs` - Console DB 通知监听
|
||||
|
||||
**目标:** 实现网关插件化支持。
|
||||
|
||||
**需求:**
|
||||
- [x] PLUG-01:网关插件化架构设计
|
||||
- [x] PLUG-02:插件加载机制
|
||||
- [ ] PLUG-03:插件隔离与生命周期管理
|
||||
|
||||
**成功标准:**
|
||||
- [x] 1. 网关支持动态加载插件
|
||||
- [x] 2. 插件之间相互隔离
|
||||
- [ ] 3. 插件可以在运行时热加载/卸载
|
||||
|
||||
**已实现文件:**
|
||||
- `src/yarpgateway/Plugins/PluginLoadContext.cs` - ALC 隔离
|
||||
- `src/yarpgateway/Plugins/PluginLoader.cs` - 发现和加载
|
||||
- `src/yarpgateway/Plugins/PluginHost.cs` - 生命周期管理
|
||||
- 单元测试 15 个全部通过
|
||||
|
||||
---
|
||||
|
||||
## Roadmap 摘要
|
||||
|
||||
| 阶段 | 名称 | 需求数 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 |
|
||||
| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 |
|
||||
| 3 | 安全加固 | 3 | 未规划 |
|
||||
| 4 | 性能优化 | 2 | 未规划 |
|
||||
| 5 | 可观测性与测试 | 5 | 未规划 |
|
||||
| 6 | 网关插件技术调研与实现 | 3 | ✅ 进行中 |
|
||||
|
||||
## 阶段 7:网关配置重构规划
|
||||
|
||||
**目标:** 分析网关配置新想法的可行性,重新规划网关配置架构。
|
||||
|
||||
**需求:**
|
||||
- [ ] REPL-01:分析"网关配置的新想法.md"中的方案
|
||||
- [ ] REPL-02:识别与现有需求的冲突点
|
||||
- [ ] REPL-03:制定新的网关配置架构
|
||||
|
||||
**成功标准:**
|
||||
1. 完成网关配置新想法的可行性分析
|
||||
2. 提出与现有阶段兼容的配置方案
|
||||
3. 更新 roadmap 以反映新的配置架构
|
||||
|
||||
---
|
||||
|
||||
## Roadmap 摘要
|
||||
|
||||
| 阶段 | 名称 | 需求数 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 |
|
||||
| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 |
|
||||
| 3 | 安全加固 | 3 | 未规划 |
|
||||
| 4 | 性能优化 | 2 | 未规划 |
|
||||
| 5 | 可观测性与测试 | 5 | 未规划 |
|
||||
| 6 | 网关插件技术调研与实现 | 3 | ✅ 进行中 |
|
||||
| 7 | 网关配置重构规划 | 3 | 待规划 |
|
||||
|
||||
**总计:** 7 个阶段 | 24 个需求 | 8 项已完成
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-04 阶段6已完成 PLUG-01 和 PLUG-02*
|
||||
@ -1,128 +0,0 @@
|
||||
#VR|# 状态:Fengling Gateway
|
||||
|
||||
**最后更新:** 2026-03-04
|
||||
|
||||
---
|
||||
|
||||
## 项目引用
|
||||
|
||||
参考:.planning/PROJECT.md(更新于 2026-03-02)
|
||||
|
||||
**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。
|
||||
|
||||
**当前重点:** 阶段 6:网关插件技术调研与实现
|
||||
|
||||
---
|
||||
|
||||
## 项目状态
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| PROJECT.md | ✓ 已初始化 |
|
||||
| config.json | ✓ 已创建 |
|
||||
| 需求文档 | ✓ 已定义(18 个需求) |
|
||||
| Roadmap | ✓ 已创建(6 个阶段) |
|
||||
| 研究 | 未开始(自动模式跳过) |
|
||||
|
||||
---
|
||||
|
||||
## 阶段状态
|
||||
|
||||
| 阶段 | 名称 | 状态 | 计划数 | 进度 |
|
||||
|------|------|------|--------|------|
|
||||
| 1 | 配置变更监听与多实例支持 | ✅ 已完成 | 0 | 100% |
|
||||
| 2 | K8s 健康检查委托 | ✅ 已完成 | 0 | 100% |
|
||||
| 3 | 安全加固 | 未规划 | 0 | 0% |
|
||||
| 4 | 性能优化 | 未规划 | 0 | 0% |
|
||||
| 5 | 可观测性与测试 | 未规划 | 0 | 0% |
|
||||
#NH|QJ|| 6 | 网关插件技术调研与实现 | ✅ 进行中 | 2 | 50% |
|
||||
#PW|| 7 | 网关配置重构规划 | 待规划 | 0 | 0% |
|
||||
|
||||
---
|
||||
|
||||
## 累积上下文
|
||||
|
||||
### 初始化
|
||||
|
||||
- **2026-03-02:** 通过 /gsd-new-project --auto 初始化项目
|
||||
- 现有代码库的重构项目(已存在 ARCHITECTURE.md、CONCERNS.md、STACK.md)
|
||||
- 用户提供背景:网关架构讨论,重点是 console 驱动的配置管理
|
||||
|
||||
### 关键决策
|
||||
|
||||
| 决策 | 日期 | 备注 |
|
||||
|------|------|------|
|
||||
| Console 驱动配置 | 2026-03-02 | 配置在 fengling-console 变更,网关监听 |
|
||||
| PostgreSQL NOTIFY 广播 | 2026-03-02 | 使用 PostgreSQL NOTIFY/LISTEN,更轻量 |
|
||||
| K8s 健康委托 | 2026-03-02 | Console 处理 K8s 健康,非网关 |
|
||||
|
||||
### 阶段 1 分析结论
|
||||
|
||||
- **2026-03-02:** 分析现有代码 `PgSqlConfigChangeListener.cs`
|
||||
- 结论:现有实现已完整满足阶段 1 所有需求
|
||||
- 监听频道:`gateway_config_changed`
|
||||
- 包含:断线重连、回退轮询(5分钟)
|
||||
|
||||
### Console 集成现状
|
||||
|
||||
- Console 已实现 GatewayController 和 GatewayService
|
||||
- Console 拥有 GatewayDbContext,可直接管理网关配置
|
||||
- ReloadGatewayAsync() 为空实现,需要在 fengling-console 中实现 NOTIFY 发送
|
||||
|
||||
SK|### 阶段 6 实施
|
||||
|
||||
**2026-03-04:** 实现插件加载基础设施
|
||||
- 完成 PLUG-01:网关插件化架构设计
|
||||
- 完成 PLUG-02:插件加载机制
|
||||
- 实现文件:
|
||||
- `src/yarpgateway/Plugins/PluginLoadContext.cs`
|
||||
- `src/yarpgateway/Plugins/PluginLoader.cs`
|
||||
- `src/yarpgateway/Plugins/PluginHost.cs`
|
||||
- 单元测试:15 个全部通过
|
||||
|
||||
**2026-03-04:** YARP 集成设计决策(与用户讨论)
|
||||
- 管道选择:Transforms(更轻量,少资源占用)
|
||||
- OnRouteMatchedAsync:用于目标选择(特殊租户访问特殊目标)
|
||||
- 负载均衡:暂不考虑,使用内置
|
||||
- 插件启用:Metadata + Console DB 通知触发
|
||||
- 插件排序:需要(通过 PluginOrder metadata)
|
||||
|
||||
**已规划计划 006-02:**
|
||||
- PluginTransformProvider - YARP Transform 提供者
|
||||
- DestinationSelector - 目标选择器
|
||||
- PluginConfigWatcher - Console DB 通知监听
|
||||
|
||||
XX|---
|
||||
|
||||
|
||||
|
||||
- **2026-03-04:** 实现插件加载基础设施
|
||||
- 完成 PLUG-01:网关插件化架构设计
|
||||
- 完成 PLUG-02:插件加载机制
|
||||
- 实现文件:
|
||||
- `src/yarpgateway/Plugins/PluginLoadContext.cs`
|
||||
- `src/yarpgateway/Plugins/PluginLoader.cs`
|
||||
- `src/yarpgateway/Plugins/PluginHost.cs`
|
||||
- 单元测试:15 个全部通过
|
||||
|
||||
### Roadmap Evolution
|
||||
|
||||
- **2026-03-07:** 阶段 7 添加:网关配置重构规划(分析"网关配置的新想法.md"中的方案,识别冲突点,制定新架构)
|
||||
|
||||
## 快速任务完成
|
||||
|
||||
| # | Description | Date | Commit | Directory |
|
||||
|---|-------------|------|--------|----------|
|
||||
| 001 | 升级 Fengling.Platform 包并修复编译警告 | 2026-03-04 | 42b8c9c | [001-upgrade-platform](./quick/001-upgrade-platform/) |
|
||||
|
||||
---
|
||||
|
||||
## 备注
|
||||
|
||||
- 自动模式:跳过研究,工作流偏好设置为 yolo
|
||||
- 配置变更应提交到 git(commit_docs: true)
|
||||
- gsd-tools.cjs 不可用 - 项目结构手动创建
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-04 - 完成阶段 6 计划 006-01:插件加载基础设施(PLUG-01, PLUG-02)*
|
||||
@ -1,457 +0,0 @@
|
||||
# YARP Gateway 架构文档
|
||||
|
||||
## 1. 整体架构模式
|
||||
|
||||
本项目基于 **YARP (Yet Another Reverse Proxy)** 实现的 API 网关,采用 **反向代理模式**,支持多租户路由、动态配置和分布式负载均衡。
|
||||
|
||||
### 1.1 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 外部请求 │
|
||||
└─────────────────────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ASP.NET Core Pipeline │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ CORS 中间件 │ -> │ JwtTransformMiddleware │ -> │ TenantRoutingMiddleware │ │
|
||||
│ └────────────────┘ └─────────────────────┘ └──────────────────────┘ │
|
||||
└─────────────────────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ YARP Reverse Proxy │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌───────────────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ DynamicProxyConfigProvider │ -> │ DistributedWeightedRoundRobinPolicy │ │
|
||||
│ └───────────┬───────────────┘ └──────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ RouteConfig / ClusterConfig │ │
|
||||
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 后端服务集群 │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Service A│ │ Service B│ │ Service C│ │ Service D│ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 核心设计模式
|
||||
|
||||
| 模式 | 应用场景 | 实现位置 |
|
||||
|------|----------|----------|
|
||||
| 反向代理 | 请求转发 | `Yarp.ReverseProxy` |
|
||||
| 策略模式 | 负载均衡策略 | `DistributedWeightedRoundRobinPolicy` |
|
||||
| 观察者模式 | 配置变更监听 | `PgSqlConfigChangeListener` |
|
||||
| 工厂模式 | DbContext 创建 | `GatewayDbContextFactory` |
|
||||
| 单例模式 | 配置提供者 | `DatabaseRouteConfigProvider`, `DatabaseClusterConfigProvider` |
|
||||
| 生产者-消费者 | 配置变更通知 | `Channel<bool>` in `PgSqlConfigChangeListener` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心组件和职责
|
||||
|
||||
### 2.1 中间件层 (Middleware)
|
||||
|
||||
#### JwtTransformMiddleware
|
||||
**文件路径**: `src/Middleware/JwtTransformMiddleware.cs`
|
||||
|
||||
**职责**:
|
||||
- 解析 JWT Token
|
||||
- 提取租户信息 (tenant claim)
|
||||
- 将用户信息注入请求头
|
||||
|
||||
**处理流程**:
|
||||
```
|
||||
Authorization Header -> JWT 解析 -> 提取 Claims -> 注入 X-Tenant-Id, X-User-Id, X-User-Name, X-Roles
|
||||
```
|
||||
|
||||
#### TenantRoutingMiddleware
|
||||
**文件路径**: `src/Middleware/TenantRoutingMiddleware.cs`
|
||||
|
||||
**职责**:
|
||||
- 从请求头获取租户 ID
|
||||
- 根据 URL 路径提取服务名称
|
||||
- 查询路由缓存获取目标集群
|
||||
- 设置动态集群 ID
|
||||
|
||||
### 2.2 配置提供层 (Config Providers)
|
||||
|
||||
#### DynamicProxyConfigProvider
|
||||
**文件路径**: `src/DynamicProxy/DynamicProxyConfigProvider.cs`
|
||||
|
||||
**职责**:
|
||||
- 实现 YARP 的 `IProxyConfigProvider` 接口
|
||||
- 整合路由和集群配置
|
||||
- 提供配置变更通知机制
|
||||
|
||||
```csharp
|
||||
public interface IProxyConfigProvider
|
||||
{
|
||||
IProxyConfig GetConfig();
|
||||
}
|
||||
```
|
||||
|
||||
#### DatabaseRouteConfigProvider
|
||||
**文件路径**: `src/Config/DatabaseRouteConfigProvider.cs`
|
||||
|
||||
**职责**:
|
||||
- 从数据库加载路由配置
|
||||
- 转换为 YARP `RouteConfig` 格式
|
||||
- 支持热重载
|
||||
|
||||
#### DatabaseClusterConfigProvider
|
||||
**文件路径**: `src/Config/DatabaseClusterConfigProvider.cs`
|
||||
|
||||
**职责**:
|
||||
- 从数据库加载集群配置
|
||||
- 管理服务实例 (地址、权重)
|
||||
- 配置健康检查策略
|
||||
|
||||
### 2.3 服务层 (Services)
|
||||
|
||||
#### RouteCache
|
||||
**文件路径**: `src/Services/RouteCache.cs`
|
||||
|
||||
**职责**:
|
||||
- 内存缓存路由信息
|
||||
- 支持全局路由和租户专用路由
|
||||
- 提供快速查询接口
|
||||
|
||||
**数据结构**:
|
||||
```
|
||||
_globalRoutes: ConcurrentDictionary<string, RouteInfo> // 全局路由
|
||||
_tenantRoutes: ConcurrentDictionary<string, ConcurrentDictionary<string, RouteInfo>> // 租户路由
|
||||
```
|
||||
|
||||
**查询优先级**: 租户专用路由 > 全局路由
|
||||
|
||||
#### PgSqlConfigChangeListener
|
||||
**文件路径**: `src/Services/PgSqlConfigChangeListener.cs`
|
||||
|
||||
**职责**:
|
||||
- 监听 PostgreSQL NOTIFY 事件
|
||||
- 双重保障:事件监听 + 轮询回退
|
||||
- 触发配置热重载
|
||||
|
||||
**监听流程**:
|
||||
```
|
||||
PostgreSQL NOTIFY -> OnNotification -> _reloadChannel -> ReloadConfigAsync
|
||||
│
|
||||
└── FallbackPollingAsync (5分钟轮询)
|
||||
```
|
||||
|
||||
#### KubernetesPendingSyncService
|
||||
**文件路径**: `src/Services/KubernetesPendingSyncService.cs`
|
||||
|
||||
**职责**:
|
||||
- 同步 Kubernetes 服务发现
|
||||
- 管理待处理服务列表
|
||||
- 清理过期服务记录
|
||||
|
||||
#### RedisConnectionManager
|
||||
**文件路径**: `src/Services/RedisConnectionManager.cs`
|
||||
|
||||
**职责**:
|
||||
- 管理 Redis 连接
|
||||
- 提供分布式锁实现
|
||||
- 连接池管理
|
||||
|
||||
### 2.4 负载均衡层
|
||||
|
||||
#### DistributedWeightedRoundRobinPolicy
|
||||
**文件路径**: `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs`
|
||||
|
||||
**职责**:
|
||||
- 实现加权轮询负载均衡
|
||||
- 基于 Redis 的分布式状态存储
|
||||
- 支持实例权重配置
|
||||
|
||||
**算法流程**:
|
||||
```
|
||||
1. 获取分布式锁 (Redis)
|
||||
2. 读取负载均衡状态
|
||||
3. 计算权重选择目标
|
||||
4. 更新状态并释放锁
|
||||
5. 失败时降级到简单选择
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据流和请求处理流程
|
||||
|
||||
### 3.1 请求处理流程图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 客户端
|
||||
participant CORS as CORS中间件
|
||||
participant JWT as JwtTransformMiddleware
|
||||
participant Tenant as TenantRoutingMiddleware
|
||||
participant YARP as YARP代理
|
||||
participant LB as 负载均衡器
|
||||
participant Service as 后端服务
|
||||
|
||||
Client->>CORS: HTTP请求
|
||||
CORS->>JWT: 跨域检查通过
|
||||
JWT->>JWT: 解析JWT Token
|
||||
JWT->>Tenant: 注入租户信息头
|
||||
Tenant->>Tenant: 提取服务名称
|
||||
Tenant->>Tenant: 查询RouteCache
|
||||
Tenant->>YARP: 设置动态集群ID
|
||||
YARP->>LB: 获取可用目标
|
||||
LB->>LB: 加权轮询选择
|
||||
LB->>Service: 转发请求
|
||||
Service-->>Client: 返回响应
|
||||
```
|
||||
|
||||
### 3.2 配置变更流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[数据库变更] --> B[SaveChangesAsync]
|
||||
B --> C[DetectConfigChanges]
|
||||
C --> D[NOTIFY gateway_config_changed]
|
||||
D --> E[PgSqlConfigChangeListener]
|
||||
E --> F{收到通知?}
|
||||
F -->|是| G[ReloadConfigAsync]
|
||||
F -->|否| H[轮询检测版本变化]
|
||||
H --> G
|
||||
G --> I[RouteCache.ReloadAsync]
|
||||
G --> J[DatabaseRouteConfigProvider.ReloadAsync]
|
||||
G --> K[DatabaseClusterConfigProvider.ReloadAsync]
|
||||
I --> L[更新内存缓存]
|
||||
J --> L
|
||||
K --> L
|
||||
L --> M[DynamicProxyConfigProvider.UpdateConfig]
|
||||
M --> N[触发 IChangeToken]
|
||||
N --> O[YARP重新加载配置]
|
||||
```
|
||||
|
||||
### 3.3 Kubernetes 服务发现流程
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Kubernetes API │
|
||||
└────────┬────────┘
|
||||
│ 30s 间隔
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ KubernetesPendingSyncService │
|
||||
├─────────────────────────────┤
|
||||
│ 1. 获取 K8s 服务列表 │
|
||||
│ 2. 对比现有待处理记录 │
|
||||
│ 3. 新增/更新/清理记录 │
|
||||
└────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ GwPendingServiceDiscovery │
|
||||
│ (待处理服务发现表) │
|
||||
└────────┬────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ PendingServicesController │
|
||||
│ - GET: 查看待处理服务 │
|
||||
│ - POST /assign: 分配集群 │
|
||||
│ - POST /reject: 拒绝服务 │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键抽象层
|
||||
|
||||
### 4.1 配置模型
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ 配置层次结构 │
|
||||
├───────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ JwtConfig │ │ RedisConfig │ │
|
||||
│ │ - Authority │ │ - Connection │ │
|
||||
│ │ - Audience │ │ - Database │ │
|
||||
│ │ - Validate* │ │ - InstanceName │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ DynamicProxyConfigProvider │ │
|
||||
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
|
||||
│ │ │ RouteConfig[] │ │ ClusterConfig[] │ │ │
|
||||
│ │ │ - RouteId │ │ - ClusterId │ │ │
|
||||
│ │ │ - ClusterId │ │ - Destinations │ │ │
|
||||
│ │ │ - Match.Path │ │ - LoadBalancing │ │ │
|
||||
│ │ │ - Metadata │ │ - HealthCheck │ │ │
|
||||
│ │ └─────────────────┘ └─────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 数据模型
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ GwTenant │ │ GwTenantRoute │
|
||||
├─────────────────┤ ├─────────────────┤
|
||||
│ Id │ │ Id │
|
||||
│ TenantCode ────┼────►│ TenantCode │
|
||||
│ TenantName │ │ ServiceName │
|
||||
│ Status │ │ ClusterId │
|
||||
│ Version │ │ PathPattern │
|
||||
│ IsDeleted │ │ Priority │
|
||||
└─────────────────┘ │ IsGlobal │
|
||||
│ Status │
|
||||
│ Version │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ GwServiceInstance│
|
||||
├─────────────────┤
|
||||
│ Id │
|
||||
│ ClusterId ────┤
|
||||
│ DestinationId │
|
||||
│ Address │
|
||||
│ Health │
|
||||
│ Weight │
|
||||
│ Status │
|
||||
│ Version │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 接口定义
|
||||
|
||||
```csharp
|
||||
// 路由缓存接口
|
||||
public interface IRouteCache
|
||||
{
|
||||
Task InitializeAsync();
|
||||
Task ReloadAsync();
|
||||
RouteInfo? GetRoute(string tenantCode, string serviceName);
|
||||
RouteInfo? GetRouteByPath(string path);
|
||||
}
|
||||
|
||||
// Redis 连接管理接口
|
||||
public interface IRedisConnectionManager
|
||||
{
|
||||
IConnectionMultiplexer GetConnection();
|
||||
Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null);
|
||||
Task<T> ExecuteInLockAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry = null);
|
||||
}
|
||||
|
||||
// 负载均衡策略接口 (YARP)
|
||||
public interface ILoadBalancingPolicy
|
||||
{
|
||||
string Name { get; }
|
||||
DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 入口点分析
|
||||
|
||||
### 5.1 程序入口 (`Program.cs`)
|
||||
|
||||
**文件路径**: `src/Program.cs`
|
||||
|
||||
**启动流程**:
|
||||
|
||||
```
|
||||
1. 创建 WebApplication Builder
|
||||
└── 配置 Serilog 日志
|
||||
|
||||
2. 配置选项
|
||||
├── JwtConfig (JWT 认证配置)
|
||||
└── RedisConfig (Redis 连接配置)
|
||||
|
||||
3. 注册数据库服务
|
||||
└── GatewayDbContext (PostgreSQL)
|
||||
|
||||
4. 注册核心服务 (Singleton)
|
||||
├── DatabaseRouteConfigProvider
|
||||
├── DatabaseClusterConfigProvider
|
||||
├── RouteCache
|
||||
├── RedisConnectionManager
|
||||
├── DynamicProxyConfigProvider
|
||||
└── DistributedWeightedRoundRobinPolicy
|
||||
|
||||
5. 注册后台服务 (HostedService)
|
||||
├── PgSqlConfigChangeListener
|
||||
└── KubernetesPendingSyncService
|
||||
|
||||
6. 配置中间件管道
|
||||
├── CORS
|
||||
├── JwtTransformMiddleware
|
||||
└── TenantRoutingMiddleware
|
||||
|
||||
7. 映射端点
|
||||
├── /health (健康检查)
|
||||
├── /api/gateway/* (管理 API)
|
||||
└── /api/* (代理路由)
|
||||
|
||||
8. 初始化并运行
|
||||
└── RouteCache.InitializeAsync()
|
||||
```
|
||||
|
||||
### 5.2 依赖注入关系
|
||||
|
||||
```
|
||||
Program.cs
|
||||
│
|
||||
├── Config/
|
||||
│ ├── JwtConfig (Options)
|
||||
│ ├── RedisConfig (Options + Singleton)
|
||||
│ ├── DatabaseRouteConfigProvider (Singleton)
|
||||
│ └── DatabaseClusterConfigProvider (Singleton)
|
||||
│
|
||||
├── DynamicProxy/
|
||||
│ └── DynamicProxyConfigProvider (Singleton, IProxyConfigProvider)
|
||||
│
|
||||
├── Services/
|
||||
│ ├── RouteCache (Singleton, IRouteCache)
|
||||
│ ├── RedisConnectionManager (Singleton)
|
||||
│ ├── PgSqlConfigChangeListener (HostedService)
|
||||
│ └── KubernetesPendingSyncService (HostedService)
|
||||
│
|
||||
├── LoadBalancing/
|
||||
│ └── DistributedWeightedRoundRobinPolicy (Singleton, ILoadBalancingPolicy)
|
||||
│
|
||||
└── Data/
|
||||
└── GatewayDbContext (DbContextFactory)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术栈
|
||||
|
||||
| 组件 | 技术 | 用途 |
|
||||
|------|------|------|
|
||||
| 反向代理 | YARP 2.x | 核心代理功能 |
|
||||
| 数据库 | PostgreSQL + EF Core | 配置存储 |
|
||||
| 缓存 | Redis | 分布式状态、锁 |
|
||||
| 服务发现 | Fengling.ServiceDiscovery | Kubernetes 集成 |
|
||||
| 日志 | Serilog | 结构化日志 |
|
||||
| 容器化 | Docker | 部署支持 |
|
||||
| 目标框架 | .NET 10.0 | 运行时 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 扩展点
|
||||
|
||||
1. **负载均衡策略**: 实现 `ILoadBalancingPolicy` 接口
|
||||
2. **配置提供者**: 继承 `IProxyConfigProvider`
|
||||
3. **中间件**: 添加自定义中间件到管道
|
||||
4. **服务发现**: 扩展 `IServiceDiscoveryProvider`
|
||||
5. **健康检查**: 配置 `HealthCheckConfig`
|
||||
@ -1,499 +0,0 @@
|
||||
# YARP 网关项目技术债务与关注点分析
|
||||
|
||||
> 分析日期:2026-02-28
|
||||
> 分析范围:核心代码、配置、数据访问层
|
||||
|
||||
---
|
||||
|
||||
## 一、严重安全问题 🔴
|
||||
|
||||
### 1.1 硬编码凭据泄露
|
||||
|
||||
**文件位置:** `src/Config/RedisConfig.cs:5`
|
||||
```csharp
|
||||
public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542";
|
||||
```
|
||||
|
||||
**问题描述:** Redis 连接字符串包含明文密码,直接硬编码在源代码中。此代码提交到版本控制系统后,密码将永久暴露。
|
||||
|
||||
**影响范围:**
|
||||
- 攻击者获取代码后可直接访问 Redis 服务
|
||||
- 违反安全合规要求(如等保、GDPR)
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
// 使用环境变量或密钥管理服务
|
||||
public string ConnectionString { get; set; } =
|
||||
Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING") ?? string.Empty;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 配置文件凭据泄露
|
||||
|
||||
**文件位置:** `src/appsettings.json:19,28`
|
||||
```json
|
||||
"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
|
||||
"ConnectionString": "81.68.223.70:6379"
|
||||
```
|
||||
|
||||
**问题描述:** 数据库连接字符串和 Redis 配置包含明文凭据,且这些配置文件通常会被提交到 Git 仓库。
|
||||
|
||||
**改进建议:**
|
||||
- 使用 `appsettings.Development.json` 存储开发环境配置,并加入 `.gitignore`
|
||||
- 生产环境使用环境变量或 Azure Key Vault / AWS Secrets Manager
|
||||
- 敏感配置使用 `dotnet user-secrets` 管理
|
||||
|
||||
---
|
||||
|
||||
### 1.3 JWT 令牌未验证
|
||||
|
||||
**文件位置:** `src/Middleware/JwtTransformMiddleware.cs:39-40`
|
||||
```csharp
|
||||
var jwtHandler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = jwtHandler.ReadJwtToken(token);
|
||||
```
|
||||
|
||||
**问题描述:** 中间件仅**读取**JWT令牌,未进行签名验证、过期检查或颁发者验证。攻击者可伪造任意JWT令牌。
|
||||
|
||||
**影响范围:**
|
||||
- 任何人可伪造租户ID、用户ID、角色信息
|
||||
- 可冒充任意用户访问系统
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
// 应使用标准的 JWT 验证流程
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = _jwtConfig.Authority,
|
||||
ValidAudience = _jwtConfig.Audience,
|
||||
IssuerSigningKey = GetSigningKey() // 从配置获取公钥
|
||||
};
|
||||
|
||||
var principal = jwtHandler.ValidateToken(token, validationParameters, out _);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.4 API 端点无认证保护
|
||||
|
||||
**文件位置:** `src/Controllers/GatewayConfigController.cs` 和 `src/Controllers/PendingServicesController.cs`
|
||||
|
||||
**问题描述:** 所有管理API端点均未添加 `[Authorize]` 特性,任何人可直接调用:
|
||||
- `POST /api/gateway/tenants` - 创建租户
|
||||
- `POST /api/gateway/routes` - 创建路由
|
||||
- `POST /api/gateway/clusters/{clusterId}/instances` - 添加服务实例
|
||||
- `POST /api/gateway/pending-services/{id}/assign` - 分配服务
|
||||
|
||||
**影响范围:**
|
||||
- 攻击者可随意修改网关配置
|
||||
- 可注入恶意服务地址进行流量劫持
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/gateway")]
|
||||
[Authorize(Roles = "Admin")] // 添加认证要求
|
||||
public class GatewayConfigController : ControllerBase
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.5 租户ID头部信任问题
|
||||
|
||||
**文件位置:** `src/Middleware/TenantRoutingMiddleware.cs:25`
|
||||
```csharp
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
```
|
||||
|
||||
**问题描述:** 直接从请求头读取租户ID,未与JWT中的租户声明进行比对验证。攻击者可伪造 `X-Tenant-Id` 头部访问其他租户数据。
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
// 从已验证的 JWT claims 中获取租户ID
|
||||
var jwtTenantId = context.User.FindFirst("tenant")?.Value;
|
||||
var headerTenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrEmpty(jwtTenantId) && jwtTenantId != headerTenantId)
|
||||
{
|
||||
// 记录安全事件
|
||||
_logger.LogWarning("Tenant ID mismatch: JWT={JwtTenant}, Header={HeaderTenant}",
|
||||
jwtTenantId, headerTenantId);
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、技术债务 🟠
|
||||
|
||||
### 2.1 ID生成策略问题
|
||||
|
||||
**文件位置:** `src/Controllers/GatewayConfigController.cs:484-487`
|
||||
```csharp
|
||||
private long GenerateId()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
```
|
||||
|
||||
**问题描述:** 使用时间戳毫秒生成ID,在高并发场景下可能产生重复ID。
|
||||
|
||||
**改进建议:**
|
||||
- 使用数据库自增主键(已有配置)
|
||||
- 或使用雪花算法(Snowflake ID)
|
||||
- 或使用 `Guid.NewGuid()`
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Redis连接重复初始化
|
||||
|
||||
**文件位置:**
|
||||
- `src/Program.cs:39-60` - 注册 `IConnectionMultiplexer`
|
||||
- `src/Services/RedisConnectionManager.cs:25-46` - 内部再次创建连接
|
||||
|
||||
**问题描述:** Redis连接被初始化两次,造成资源浪费和配置不一致风险。
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
// Program.cs 中只注册一次
|
||||
builder.Services.AddSingleton<IRedisConnectionManager, RedisConnectionManager>();
|
||||
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||
sp.GetRequiredService<IRedisConnectionManager>().GetConnection());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 DTO 内嵌定义
|
||||
|
||||
**文件位置:** `src/Controllers/GatewayConfigController.cs:444-481`
|
||||
|
||||
**问题描述:** 多个DTO类定义在Controller内部,不利于复用和测试。
|
||||
|
||||
**改进建议:**
|
||||
- 将 DTO 移至 `src/DTOs/` 或 `src/Models/Dto/` 目录
|
||||
- 使用 Auto Mapper 或 Mapster 进行对象映射
|
||||
|
||||
---
|
||||
|
||||
### 2.4 魔法数字
|
||||
|
||||
**文件位置:** 多处使用数字常量
|
||||
|
||||
```csharp
|
||||
// RouteCache.cs:99
|
||||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
|
||||
// GatewayConfigController.cs:239
|
||||
route.Status = 1;
|
||||
|
||||
// KubernetesPendingSyncService.cs:13
|
||||
private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30);
|
||||
```
|
||||
|
||||
**问题描述:** 状态值、超时时间等使用硬编码数字,降低代码可读性和可维护性。
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
// 定义常量或枚举
|
||||
public static class RouteStatus
|
||||
{
|
||||
public const int Active = 1;
|
||||
public const int Inactive = 0;
|
||||
}
|
||||
|
||||
public static class ServiceConstants
|
||||
{
|
||||
public static readonly TimeSpan DefaultSyncInterval = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.5 异步方法命名不一致
|
||||
|
||||
**文件位置:** `src/Config/DatabaseRouteConfigProvider.cs:23`
|
||||
```csharp
|
||||
_ = LoadConfigAsync(); // Fire-and-forget without await
|
||||
```
|
||||
|
||||
**问题描述:** 构造函数中调用异步方法但未等待完成,可能导致初始化竞态条件。
|
||||
|
||||
**改进建议:**
|
||||
- 使用工厂模式异步初始化
|
||||
- 或在 `Program.cs` 中显式调用初始化方法
|
||||
|
||||
---
|
||||
|
||||
## 三、性能瓶颈风险 🟡
|
||||
|
||||
### 3.1 负载均衡锁竞争
|
||||
|
||||
**文件位置:** `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs:48-53`
|
||||
```csharp
|
||||
var lockAcquired = db.StringSet(
|
||||
lockKey,
|
||||
lockValue,
|
||||
TimeSpan.FromMilliseconds(500),
|
||||
When.NotExists
|
||||
);
|
||||
```
|
||||
|
||||
**问题描述:** 每次请求都需要获取Redis分布式锁,高并发下会成为瓶颈。锁获取失败时降级策略不可靠。
|
||||
|
||||
**影响:**
|
||||
- 单集群QPS受限
|
||||
- Redis延迟增加时网关吞吐量下降
|
||||
|
||||
**改进建议:**
|
||||
- 考虑使用本地缓存 + 定期同步策略
|
||||
- 或使用一致性哈希算法避免锁需求
|
||||
- 增加本地计数器作为快速路径
|
||||
|
||||
---
|
||||
|
||||
### 3.2 路由缓存全量加载
|
||||
|
||||
**文件位置:** `src/Services/RouteCache.cs:94-137`
|
||||
```csharp
|
||||
var routes = await db.TenantRoutes
|
||||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
**问题描述:** 每次重载都清空并重新加载所有路由,大数据量下性能差。
|
||||
|
||||
**改进建议:**
|
||||
- 实现增量更新机制
|
||||
- 使用版本号比对只更新变更项
|
||||
- 添加分页加载支持
|
||||
|
||||
---
|
||||
|
||||
### 3.3 数据库查询未优化
|
||||
|
||||
**文件位置:** `src/Controllers/GatewayConfigController.cs:145-148`
|
||||
```csharp
|
||||
var currentRouteVersion = await db.TenantRoutes
|
||||
.OrderByDescending(r => r.Version)
|
||||
.Select(r => r.Version)
|
||||
.FirstOrDefaultAsync(stoppingToken);
|
||||
```
|
||||
|
||||
**问题描述:** 每次轮询都执行 `ORDER BY` 查询获取最大版本号,缺少索引优化。
|
||||
|
||||
**改进建议:**
|
||||
```sql
|
||||
-- 添加索引
|
||||
CREATE INDEX IX_TenantRoutes_Version ON "TenantRoutes" ("Version" DESC);
|
||||
|
||||
-- 或使用 MAX 聚合
|
||||
SELECT MAX("Version") FROM "TenantRoutes";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 PostgreSQL NOTIFY 连接管理
|
||||
|
||||
**文件位置:** `src/Data/GatewayDbContext.cs:72-75`
|
||||
```csharp
|
||||
using var connection = new NpgsqlConnection(connectionString);
|
||||
connection.Open();
|
||||
using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection);
|
||||
cmd.ExecuteNonQuery();
|
||||
```
|
||||
|
||||
**问题描述:** 每次保存变更都创建新的数据库连接发送通知,连接开销大。
|
||||
|
||||
**改进建议:**
|
||||
- 使用连接池中的连接
|
||||
- 或复用 `PgSqlConfigChangeListener` 中的连接发送通知
|
||||
|
||||
---
|
||||
|
||||
## 四、脆弱区域 🟠
|
||||
|
||||
### 4.1 租户路由外键约束
|
||||
|
||||
**文件位置:** `src/Migrations/20260201120312_InitialCreate.cs:83-89`
|
||||
```csharp
|
||||
table.ForeignKey(
|
||||
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||
column: x => x.TenantCode,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "TenantCode",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
```
|
||||
|
||||
**问题描述:** `TenantRoutes.TenantCode` 有外键约束,但全局路由(`IsGlobal=true`)时 `TenantCode` 可为空字符串,可能导致数据一致性问题。
|
||||
|
||||
**改进建议:**
|
||||
- 全局路由使用特定的占位符(如 "GLOBAL")
|
||||
- 或修改外键约束为条件约束
|
||||
|
||||
---
|
||||
|
||||
### 4.2 健康检查配置硬编码
|
||||
|
||||
**文件位置:** `src/Config/DatabaseClusterConfigProvider.cs:77-86`
|
||||
```csharp
|
||||
HealthCheck = new HealthCheckConfig
|
||||
{
|
||||
Active = new ActiveHealthCheckConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Interval = TimeSpan.FromSeconds(30),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Path = "/health"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**问题描述:** 健康检查路径和间隔硬编码,不同服务可能需要不同的健康检查配置。
|
||||
|
||||
**改进建议:**
|
||||
- 将健康检查配置存储在数据库
|
||||
- 或在模型中添加健康检查配置字段
|
||||
|
||||
---
|
||||
|
||||
### 4.3 端口选择逻辑
|
||||
|
||||
**文件位置:** `src/Controllers/PendingServicesController.cs:119-120`
|
||||
```csharp
|
||||
var discoveredPorts = JsonSerializer.Deserialize<List<int>>(pendingService.DiscoveredPorts) ?? new List<int>();
|
||||
var primaryPort = discoveredPorts.FirstOrDefault() > 0 ? discoveredPorts.First() : 80;
|
||||
```
|
||||
|
||||
**问题描述:** 简单选择第一个端口作为主端口,可能不适合所有服务场景。
|
||||
|
||||
**改进建议:**
|
||||
- 支持端口选择策略配置
|
||||
- 优先选择知名端口(如 80, 443, 8080)
|
||||
- 允许用户在审批时选择端口
|
||||
|
||||
---
|
||||
|
||||
### 4.4 异常处理不完整
|
||||
|
||||
**文件位置:** `src/Services/PgSqlConfigChangeListener.cs:59-62`
|
||||
```csharp
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize PgSql listener");
|
||||
// 未重试或终止服务
|
||||
}
|
||||
```
|
||||
|
||||
**问题描述:** 初始化失败后仅记录日志,服务继续运行但功能不完整。
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize PgSql listener, retrying in 5 seconds...");
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
await InitializeListenerAsync(stoppingToken); // 重试
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.5 状态变更无事务保护
|
||||
|
||||
**文件位置:** `src/Controllers/PendingServicesController.cs:137-145`
|
||||
```csharp
|
||||
db.ServiceInstances.Add(newInstance);
|
||||
pendingService.Status = (int)PendingServiceStatus.Approved;
|
||||
// ...
|
||||
await db.SaveChangesAsync();
|
||||
```
|
||||
|
||||
**问题描述:** 创建实例和更新状态在同一事务中,但如果缓存重载失败,数据可能不一致。
|
||||
|
||||
**改进建议:**
|
||||
- 使用 TransactionScope 或数据库事务明确边界
|
||||
- 添加补偿机制处理失败情况
|
||||
|
||||
---
|
||||
|
||||
## 五、可维护性问题 🟡
|
||||
|
||||
### 5.1 日志结构不统一
|
||||
|
||||
**问题描述:** 日志消息格式不统一,有的包含结构化数据,有的仅是文本。
|
||||
|
||||
**改进建议:**
|
||||
- 制定统一的日志格式规范
|
||||
- 使用结构化日志模板:`LogInformation("Operation {Operation} completed for {Entity} with ID {Id}", "Create", "Route", route.Id)`
|
||||
|
||||
---
|
||||
|
||||
### 5.2 缺少单元测试
|
||||
|
||||
**问题描述:** 项目中未发现测试项目,核心逻辑缺少测试覆盖。
|
||||
|
||||
**改进建议:**
|
||||
- 创建 `tests/YarpGateway.Tests/` 测试项目
|
||||
- 对以下核心组件编写单元测试:
|
||||
- `RouteCache` - 路由查找逻辑
|
||||
- `JwtTransformMiddleware` - JWT 解析逻辑
|
||||
- `DistributedWeightedRoundRobinPolicy` - 负载均衡算法
|
||||
|
||||
---
|
||||
|
||||
### 5.3 配置验证缺失
|
||||
|
||||
**文件位置:** `src/Config/JwtConfig.cs`, `src/Config/RedisConfig.cs`
|
||||
|
||||
**问题描述:** 配置类没有验证逻辑,无效配置可能导致运行时错误。
|
||||
|
||||
**改进建议:**
|
||||
```csharp
|
||||
public class JwtConfig : IValidatableObject
|
||||
{
|
||||
public string Authority { get; set; } = string.Empty;
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Authority))
|
||||
yield return new ValidationResult("Authority is required", new[] { nameof(Authority) });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、改进优先级建议
|
||||
|
||||
| 优先级 | 问题 | 风险等级 | 建议处理时间 |
|
||||
|--------|------|----------|--------------|
|
||||
| P0 | 硬编码凭据泄露 | 严重 | 立即修复 |
|
||||
| P0 | JWT未验证 | 严重 | 立即修复 |
|
||||
| P0 | API无认证保护 | 严重 | 立即修复 |
|
||||
| P1 | 租户ID信任问题 | 高 | 1周内 |
|
||||
| P1 | ID生成策略 | 高 | 1周内 |
|
||||
| P2 | 负载均衡锁竞争 | 中 | 2周内 |
|
||||
| P2 | 路由缓存优化 | 中 | 2周内 |
|
||||
| P3 | DTO内嵌定义 | 低 | 1个月内 |
|
||||
| P3 | 缺少单元测试 | 低 | 持续改进 |
|
||||
|
||||
---
|
||||
|
||||
## 七、总结
|
||||
|
||||
本项目存在多个**严重安全漏洞**,主要涉及:
|
||||
1. 敏感信息硬编码
|
||||
2. 认证授权缺失
|
||||
3. 输入验证不足
|
||||
|
||||
技术债务主要集中在代码组织、异常处理和性能优化方面。建议优先处理安全相关问题,然后逐步优化性能和可维护性。
|
||||
|
||||
---
|
||||
|
||||
*文档由自动化分析生成,建议人工复核后纳入迭代计划。*
|
||||
@ -1,690 +0,0 @@
|
||||
# YARP Gateway 编码约定文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了 YARP Gateway 项目的编码约定和最佳实践,旨在帮助开发人员理解和遵循项目规范。
|
||||
|
||||
---
|
||||
|
||||
## 1. 代码风格
|
||||
|
||||
### 1.1 命名约定
|
||||
|
||||
#### 类和接口命名
|
||||
|
||||
```csharp
|
||||
// 接口:使用 I 前缀 + PascalCase
|
||||
public interface IRouteCache
|
||||
{
|
||||
Task InitializeAsync();
|
||||
Task ReloadAsync();
|
||||
RouteInfo? GetRoute(string tenantCode, string serviceName);
|
||||
}
|
||||
|
||||
// 实现类:PascalCase,描述性名称
|
||||
public class RouteCache : IRouteCache
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
// 配置类:以 Config 后缀
|
||||
public class RedisConfig
|
||||
{
|
||||
public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542";
|
||||
public int Database { get; set; } = 0;
|
||||
public string InstanceName { get; set; } = "YarpGateway";
|
||||
}
|
||||
|
||||
// DTO 类:以 Dto 后缀
|
||||
public class CreateTenantDto
|
||||
{
|
||||
public string TenantCode { get; set; } = string.Empty;
|
||||
public string TenantName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// 数据模型:Gw 前缀标识网关实体
|
||||
public class GwTenantRoute
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string TenantCode { get; set; } = string.Empty;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 私有字段命名
|
||||
|
||||
```csharp
|
||||
// 使用下划线前缀 + camelCase
|
||||
public class TenantRoutingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IRouteCache _routeCache;
|
||||
private readonly ILogger<TenantRoutingMiddleware> _logger;
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:下划线前缀清晰区分私有字段和局部变量,避免 `this.` 的频繁使用。
|
||||
|
||||
#### 方法命名
|
||||
|
||||
```csharp
|
||||
// 异步方法:Async 后缀
|
||||
public async Task InitializeAsync()
|
||||
public async Task ReloadAsync()
|
||||
private async Task LoadFromDatabaseAsync()
|
||||
|
||||
// 同步方法:动词开头
|
||||
public RouteInfo? GetRoute(string tenantCode, string serviceName)
|
||||
private string ExtractServiceName(string path)
|
||||
```
|
||||
|
||||
### 1.2 文件组织
|
||||
|
||||
项目采用按功能分层的方式组织代码:
|
||||
|
||||
```
|
||||
src/
|
||||
├── Config/ # 配置类和配置提供者
|
||||
├── Controllers/ # API 控制器
|
||||
├── Data/ # 数据库上下文和工厂
|
||||
├── DynamicProxy/ # 动态代理配置
|
||||
├── LoadBalancing/ # 负载均衡策略
|
||||
├── Metrics/ # 指标收集
|
||||
├── Middleware/ # 中间件
|
||||
├── Migrations/ # 数据库迁移
|
||||
├── Models/ # 数据模型
|
||||
└── Services/ # 业务服务
|
||||
```
|
||||
|
||||
**原因**:按功能分层便于代码定位,降低耦合度。
|
||||
|
||||
---
|
||||
|
||||
## 2. 依赖注入模式
|
||||
|
||||
### 2.1 服务注册
|
||||
|
||||
```csharp
|
||||
// Program.cs 中的服务注册
|
||||
|
||||
// 配置选项模式
|
||||
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection("Jwt"));
|
||||
builder.Services.Configure<RedisConfig>(builder.Configuration.GetSection("Redis"));
|
||||
|
||||
// 直接注册配置实例(当需要直接使用配置对象时)
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<RedisConfig>>().Value);
|
||||
|
||||
// DbContext 使用工厂模式
|
||||
builder.Services.AddDbContextFactory<GatewayDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
|
||||
);
|
||||
|
||||
// 单例服务(无状态或线程安全)
|
||||
builder.Services.AddSingleton<DatabaseRouteConfigProvider>();
|
||||
builder.Services.AddSingleton<DatabaseClusterConfigProvider>();
|
||||
builder.Services.AddSingleton<IRouteCache, RouteCache>();
|
||||
|
||||
// 接口与实现分离注册
|
||||
builder.Services.AddSingleton<IRedisConnectionManager, RedisConnectionManager>();
|
||||
|
||||
// 后台服务
|
||||
builder.Services.AddHostedService<PgSqlConfigChangeListener>();
|
||||
builder.Services.AddHostedService<KubernetesPendingSyncService>();
|
||||
```
|
||||
|
||||
### 2.2 依赖注入构造函数模式
|
||||
|
||||
```csharp
|
||||
public class RouteCache : IRouteCache
|
||||
{
|
||||
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||
private readonly ILogger<RouteCache> _logger;
|
||||
|
||||
public RouteCache(
|
||||
IDbContextFactory<GatewayDbContext> dbContextFactory,
|
||||
ILogger<RouteCache> logger)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**模式要点**:
|
||||
1. 所有依赖通过构造函数注入
|
||||
2. 使用 `readonly` 修饰私有字段
|
||||
3. 依赖项按类别排序(框架 → 基础设施 → 业务服务)
|
||||
|
||||
**原因**:构造函数注入确保依赖不可变,便于测试和依赖管理。
|
||||
|
||||
### 2.3 IDbContextFactory 模式
|
||||
|
||||
```csharp
|
||||
// 在 Singleton 服务中使用 DbContextFactory
|
||||
public class RouteCache : IRouteCache
|
||||
{
|
||||
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||
|
||||
private async Task LoadFromDatabaseAsync()
|
||||
{
|
||||
// 使用 using 确保上下文正确释放
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
var routes = await db.TenantRoutes
|
||||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 在 BackgroundService 中使用 Scope
|
||||
public class KubernetesPendingSyncService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private async Task SyncPendingServicesAsync(CancellationToken ct)
|
||||
{
|
||||
// 创建作用域以获取 Scoped 服务
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<GatewayDbContext>>();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:`IDbContextFactory` 避免了 Singleton 服务直接持有 DbContext 的生命周期问题。
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置管理模式
|
||||
|
||||
### 3.1 配置类定义
|
||||
|
||||
```csharp
|
||||
// 简单 POCO 配置类
|
||||
namespace YarpGateway.Config;
|
||||
|
||||
public class JwtConfig
|
||||
{
|
||||
public string Authority { get; set; } = string.Empty;
|
||||
public string Audience { get; set; } = string.Empty;
|
||||
public bool ValidateIssuer { get; set; } = true;
|
||||
public bool ValidateAudience { get; set; } = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 配置绑定和注入
|
||||
|
||||
```csharp
|
||||
// Program.cs 中绑定配置
|
||||
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection("Jwt"));
|
||||
|
||||
// 通过 IOptions<T> 注入
|
||||
public class JwtTransformMiddleware
|
||||
{
|
||||
private readonly JwtConfig _jwtConfig;
|
||||
|
||||
public JwtTransformMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptions<JwtConfig> jwtConfig, // 使用 IOptions<T>
|
||||
ILogger<JwtTransformMiddleware> logger)
|
||||
{
|
||||
_jwtConfig = jwtConfig.Value; // 获取实际配置值
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 动态配置更新
|
||||
|
||||
```csharp
|
||||
// 配置变更通知通道
|
||||
public static class ConfigNotifyChannel
|
||||
{
|
||||
public const string GatewayConfigChanged = "gateway_config_changed";
|
||||
}
|
||||
|
||||
// DbContext 在保存时检测变更并通知
|
||||
public class GatewayDbContext : DbContext
|
||||
{
|
||||
private bool _configChangeDetected;
|
||||
|
||||
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DetectConfigChanges();
|
||||
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
if (_configChangeDetected)
|
||||
{
|
||||
await NotifyConfigChangedAsync(cancellationToken);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DetectConfigChanges()
|
||||
{
|
||||
var entries = ChangeTracker.Entries()
|
||||
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
|
||||
.Where(e => e.Entity is GwTenantRoute or GwServiceInstance or GwTenant);
|
||||
_configChangeDetected = entries.Any();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:使用 PostgreSQL NOTIFY/LISTEN 实现配置热更新,避免轮询。
|
||||
|
||||
---
|
||||
|
||||
## 4. 错误处理方式
|
||||
|
||||
### 4.1 中间件错误处理
|
||||
|
||||
```csharp
|
||||
public class JwtTransformMiddleware
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// 快速失败模式:前置条件检查后直接调用 next
|
||||
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 业务逻辑
|
||||
var jwtHandler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = jwtHandler.ReadJwtToken(token);
|
||||
// ...
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但不中断请求流程
|
||||
_logger.LogError(ex, "Failed to parse JWT token");
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 控制器错误处理
|
||||
|
||||
```csharp
|
||||
[HttpPost("{id}/assign")]
|
||||
public async Task<IActionResult> AssignService(long id, [FromBody] AssignServiceRequest request)
|
||||
{
|
||||
await using var db = _dbContextFactory.CreateDbContext();
|
||||
|
||||
// 早期返回模式
|
||||
var pendingService = await db.PendingServiceDiscoveries.FindAsync(id);
|
||||
if (pendingService == null || pendingService.IsDeleted)
|
||||
{
|
||||
return NotFound(new { message = "Pending service not found" });
|
||||
}
|
||||
|
||||
if (pendingService.Status != (int)PendingServiceStatus.Pending)
|
||||
{
|
||||
return BadRequest(new { message = $"Service is already {((PendingServiceStatus)pendingService.Status)}, cannot assign" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.ClusterId))
|
||||
{
|
||||
return BadRequest(new { message = "ClusterId is required" });
|
||||
}
|
||||
|
||||
// 业务逻辑...
|
||||
return Ok(new { success = true, message = "..." });
|
||||
}
|
||||
```
|
||||
|
||||
**模式要点**:
|
||||
1. 使用早期返回(Guard Clauses)减少嵌套
|
||||
2. 返回结构化的错误信息
|
||||
3. 使用 HTTP 状态码语义
|
||||
|
||||
### 4.3 后台服务错误处理
|
||||
|
||||
```csharp
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SyncPendingServicesAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但继续运行
|
||||
_logger.LogError(ex, "Error during K8s pending service sync");
|
||||
}
|
||||
|
||||
await Task.Delay(_syncInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:后台服务不应因单次错误而终止,需具备自恢复能力。
|
||||
|
||||
---
|
||||
|
||||
## 5. 日志记录约定
|
||||
|
||||
### 5.1 结构化日志
|
||||
|
||||
```csharp
|
||||
// 使用 Serilog 结构化日志
|
||||
_logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes",
|
||||
_globalRoutes.Count, _tenantRoutes.Count);
|
||||
|
||||
_logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName);
|
||||
|
||||
_logger.LogError(ex, "Redis connection failed");
|
||||
|
||||
_logger.LogDebug("Released lock for key: {Key}", _key);
|
||||
```
|
||||
|
||||
**模式要点**:
|
||||
1. 使用占位符 `{PropertyName}` 而非字符串插值
|
||||
2. 日志消息使用常量,便于聚合分析
|
||||
3. 包含足够的上下文信息
|
||||
|
||||
### 5.2 Serilog 配置
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Host.UseSerilog(
|
||||
(context, services, configuration) =>
|
||||
configuration
|
||||
.ReadFrom.Configuration(context.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext()
|
||||
);
|
||||
```
|
||||
|
||||
### 5.3 日志级别使用
|
||||
|
||||
| 级别 | 使用场景 |
|
||||
|------|----------|
|
||||
| `LogDebug` | 详细调试信息,生产环境通常关闭 |
|
||||
| `LogInformation` | 正常业务流程关键节点 |
|
||||
| `LogWarning` | 可恢复的异常情况 |
|
||||
| `LogError` | 错误需要关注但不影响整体运行 |
|
||||
| `LogFatal` | 致命错误,应用无法继续运行 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 异步编程模式
|
||||
|
||||
### 6.1 async/await 使用
|
||||
|
||||
```csharp
|
||||
// 正确:异步方法使用 Async 后缀
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_logger.LogInformation("Initializing route cache from database...");
|
||||
await LoadFromDatabaseAsync();
|
||||
}
|
||||
|
||||
// 正确:使用 ConfigureAwait(false) 在库代码中
|
||||
private async Task LoadFromDatabaseAsync()
|
||||
{
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
var routes = await db.TenantRoutes
|
||||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 CancellationToken 使用
|
||||
|
||||
```csharp
|
||||
// 控制器方法
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetPendingServices(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] int? status = null)
|
||||
{
|
||||
await using var db = _dbContextFactory.CreateDbContext();
|
||||
// EF Core 自动处理 CancellationToken
|
||||
var total = await query.CountAsync();
|
||||
// ...
|
||||
}
|
||||
|
||||
// 后台服务
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(_syncInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 并发控制
|
||||
|
||||
```csharp
|
||||
// 使用 ReaderWriterLockSlim 保护读写
|
||||
public class RouteCache : IRouteCache
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _lock = new();
|
||||
|
||||
public RouteInfo? GetRoute(string tenantCode, string serviceName)
|
||||
{
|
||||
_lock.EnterUpgradeableReadLock();
|
||||
try
|
||||
{
|
||||
// 读取逻辑
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitUpgradeableReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadFromDatabaseAsync()
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
// 写入逻辑
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 SemaphoreSlim 进行异步锁定
|
||||
public class DatabaseRouteConfigProvider
|
||||
{
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
|
||||
public async Task ReloadAsync()
|
||||
{
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await LoadConfigInternalAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- `ReaderWriterLockSlim` 支持多读单写,适合读多写少场景
|
||||
- `SemaphoreSlim` 支持异步等待,适合异步方法
|
||||
|
||||
### 6.4 Redis 分布式锁模式
|
||||
|
||||
```csharp
|
||||
public async Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null)
|
||||
{
|
||||
var redis = GetConnection();
|
||||
var db = redis.GetDatabase();
|
||||
var lockKey = $"lock:{_config.InstanceName}:{key}";
|
||||
var lockValue = Environment.MachineName + ":" + Process.GetCurrentProcess().Id;
|
||||
|
||||
var acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists);
|
||||
|
||||
if (!acquired)
|
||||
{
|
||||
// 退避重试
|
||||
var backoff = TimeSpan.FromMilliseconds(100);
|
||||
while (!acquired && retryCount < maxRetries)
|
||||
{
|
||||
await Task.Delay(backoff);
|
||||
acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists);
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return new RedisLock(db, lockKey, lockValue, _logger);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 中间件模式
|
||||
|
||||
### 7.1 标准中间件结构
|
||||
|
||||
```csharp
|
||||
public class TenantRoutingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IRouteCache _routeCache;
|
||||
private readonly ILogger<TenantRoutingMiddleware> _logger;
|
||||
|
||||
// 构造函数注入依赖
|
||||
public TenantRoutingMiddleware(
|
||||
RequestDelegate next,
|
||||
IRouteCache routeCache,
|
||||
ILogger<TenantRoutingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_routeCache = routeCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// InvokeAsync 方法签名固定
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// 1. 前置处理
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||||
|
||||
// 2. 快速返回
|
||||
if (string.IsNullOrEmpty(tenantId))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 业务逻辑
|
||||
var route = _routeCache.GetRoute(tenantId, serviceName);
|
||||
|
||||
// 4. 设置上下文
|
||||
context.Items["DynamicClusterId"] = route.ClusterId;
|
||||
|
||||
// 5. 调用下一个中间件
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 中间件注册顺序
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors("AllowFrontend");
|
||||
app.UseMiddleware<JwtTransformMiddleware>(); // JWT 解析
|
||||
app.UseMiddleware<TenantRoutingMiddleware>(); // 租户路由
|
||||
|
||||
app.MapControllers();
|
||||
app.MapReverseProxy();
|
||||
```
|
||||
|
||||
**顺序原因**:
|
||||
1. CORS 需最先处理跨域请求
|
||||
2. JWT 中间件解析用户信息供后续使用
|
||||
3. 租户路由根据用户信息选择目标服务
|
||||
|
||||
---
|
||||
|
||||
## 8. 控制器约定
|
||||
|
||||
### 8.1 控制器结构
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/gateway")]
|
||||
public class GatewayConfigController : ControllerBase
|
||||
{
|
||||
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||
private readonly IRouteCache _routeCache;
|
||||
|
||||
public GatewayConfigController(
|
||||
IDbContextFactory<GatewayDbContext> dbContextFactory,
|
||||
IRouteCache routeCache)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_routeCache = routeCache;
|
||||
}
|
||||
|
||||
#region Tenants
|
||||
// 租户相关端点
|
||||
#endregion
|
||||
|
||||
#region Routes
|
||||
// 路由相关端点
|
||||
#endregion
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 端点命名
|
||||
|
||||
```csharp
|
||||
// GET 集合
|
||||
[HttpGet("tenants")]
|
||||
public async Task<IActionResult> GetTenants(...) { }
|
||||
|
||||
// GET 单个
|
||||
[HttpGet("tenants/{id}")]
|
||||
public async Task<IActionResult> GetTenant(long id) { }
|
||||
|
||||
// POST 创建
|
||||
[HttpPost("tenants")]
|
||||
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto dto) { }
|
||||
|
||||
// PUT 更新
|
||||
[HttpPut("tenants/{id}")]
|
||||
public async Task<IActionResult> UpdateTenant(long id, [FromBody] UpdateTenantDto dto) { }
|
||||
|
||||
// DELETE 删除
|
||||
[HttpDelete("tenants/{id}")]
|
||||
public async Task<IActionResult> DeleteTenant(long id) { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 总结
|
||||
|
||||
本项目的编码约定遵循以下核心原则:
|
||||
|
||||
1. **一致性**:统一的命名和代码组织方式
|
||||
2. **可测试性**:依赖注入和接口抽象便于测试
|
||||
3. **可维护性**:清晰的结构和文档注释
|
||||
4. **可观测性**:结构化日志和指标收集
|
||||
5. **健壮性**:完善的错误处理和并发控制
|
||||
|
||||
遵循这些约定可以确保代码质量和团队协作效率。
|
||||
@ -1,374 +0,0 @@
|
||||
# YARP 网关外部集成文档
|
||||
|
||||
## 1. PostgreSQL 数据库集成
|
||||
|
||||
### 概述
|
||||
PostgreSQL 作为主数据库,存储网关配置数据,包括租户、路由、服务实例等信息。
|
||||
|
||||
### 连接配置
|
||||
**配置位置**: `src/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=***"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DbContext 配置
|
||||
**文件**: `src/Data/GatewayDbContext.cs`
|
||||
|
||||
```csharp
|
||||
// 注册 DbContext 工厂
|
||||
builder.Services.AddDbContextFactory<GatewayDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
|
||||
);
|
||||
```
|
||||
|
||||
### 数据模型
|
||||
| 实体 | 表名 | 用途 |
|
||||
|------|------|------|
|
||||
| `GwTenant` | Tenants | 租户信息 |
|
||||
| `GwTenantRoute` | TenantRoutes | 租户路由配置 |
|
||||
| `GwServiceInstance` | ServiceInstances | 服务实例(集群节点) |
|
||||
| `GwPendingServiceDiscovery` | PendingServiceDiscoveries | K8s 待处理服务发现 |
|
||||
|
||||
### 配置变更通知机制
|
||||
**文件**: `src/Config/ConfigNotifyChannel.cs`
|
||||
|
||||
使用 PostgreSQL `LISTEN/NOTIFY` 机制实现配置变更实时通知:
|
||||
|
||||
```csharp
|
||||
// 发送通知(在 DbContext.SaveChangesAsync 中触发)
|
||||
await using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection);
|
||||
|
||||
// 监听通知(在 PgSqlConfigChangeListener 中)
|
||||
cmd.CommandText = $"LISTEN {ConfigNotifyChannel.GatewayConfigChanged}";
|
||||
```
|
||||
|
||||
**监听服务**: `src/Services/PgSqlConfigChangeListener.cs`
|
||||
- 监听 PostgreSQL NOTIFY 通道
|
||||
- 检测配置版本变更
|
||||
- 触发路由/集群配置热更新
|
||||
- 提供 5 分钟兜底轮询机制
|
||||
|
||||
---
|
||||
|
||||
## 2. Redis 集成
|
||||
|
||||
### 概述
|
||||
Redis 用于分布式锁、路由缓存同步,确保多实例网关的配置一致性。
|
||||
|
||||
### 连接配置
|
||||
**配置位置**: `src/Config/RedisConfig.cs`
|
||||
|
||||
```csharp
|
||||
public class RedisConfig
|
||||
{
|
||||
public string ConnectionString { get; set; } = "81.68.223.70:16379,password=***";
|
||||
public int Database { get; set; } = 0;
|
||||
public string InstanceName { get; set; } = "YarpGateway";
|
||||
}
|
||||
```
|
||||
|
||||
### 连接管理器
|
||||
**文件**: `src/Services/RedisConnectionManager.cs`
|
||||
|
||||
```csharp
|
||||
// 注册 Redis 连接
|
||||
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||
{
|
||||
var config = sp.GetRequiredService<RedisConfig>();
|
||||
var connectionOptions = ConfigurationOptions.Parse(config.ConnectionString);
|
||||
connectionOptions.AbortOnConnectFail = false;
|
||||
connectionOptions.ConnectRetry = 3;
|
||||
connectionOptions.ConnectTimeout = 5000;
|
||||
connectionOptions.SyncTimeout = 3000;
|
||||
connectionOptions.DefaultDatabase = config.Database;
|
||||
|
||||
return ConnectionMultiplexer.Connect(connectionOptions);
|
||||
});
|
||||
```
|
||||
|
||||
### 分布式锁实现
|
||||
**接口**: `IRedisConnectionManager`
|
||||
|
||||
```csharp
|
||||
public interface IRedisConnectionManager
|
||||
{
|
||||
IConnectionMultiplexer GetConnection();
|
||||
Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null);
|
||||
Task<T> ExecuteInLockAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry = null);
|
||||
}
|
||||
```
|
||||
|
||||
**锁机制特性**:
|
||||
- 基于键值对的分布式锁
|
||||
- 自动过期时间(默认 10 秒)
|
||||
- 指数退避重试策略
|
||||
- Lua 脚本安全释放锁
|
||||
|
||||
---
|
||||
|
||||
## 3. Kubernetes 服务发现集成
|
||||
|
||||
### 概述
|
||||
通过自定义的 Fengling.ServiceDiscovery 包实现 Kubernetes 服务自动发现,将 K8s Service 自动注册为网关后端服务。
|
||||
|
||||
### 配置
|
||||
**文件**: `src/Program.cs`
|
||||
|
||||
```csharp
|
||||
// 添加 Kubernetes 服务发现
|
||||
var useInClusterConfig = builder.Configuration.GetValue<bool>("ServiceDiscovery:UseInClusterConfig", true);
|
||||
builder.Services.AddKubernetesServiceDiscovery(options =>
|
||||
{
|
||||
options.LabelSelector = "app.kubernetes.io/managed-by=yarp";
|
||||
options.UseInClusterConfig = useInClusterConfig;
|
||||
});
|
||||
|
||||
builder.Services.AddServiceDiscovery();
|
||||
```
|
||||
|
||||
### 依赖包
|
||||
| 包名 | 用途 |
|
||||
|------|------|
|
||||
| `Fengling.ServiceDiscovery.Core` | 服务发现核心接口 |
|
||||
| `Fengling.ServiceDiscovery.Kubernetes` | Kubernetes 实现 |
|
||||
| `Fengling.ServiceDiscovery.Static` | 静态配置实现 |
|
||||
|
||||
### 后台同步服务
|
||||
**文件**: `src/Services/KubernetesPendingSyncService.cs`
|
||||
|
||||
```csharp
|
||||
public class KubernetesPendingSyncService : BackgroundService
|
||||
{
|
||||
private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30);
|
||||
private readonly TimeSpan _staleThreshold = TimeSpan.FromHours(24);
|
||||
|
||||
// 同步 K8s 服务到数据库待处理表
|
||||
}
|
||||
```
|
||||
|
||||
**同步逻辑**:
|
||||
1. 每 30 秒从 K8s API 获取服务列表
|
||||
2. 对比数据库中的待处理服务记录
|
||||
3. 新增/更新/清理过期服务
|
||||
4. 标记不再存在的 K8s 服务
|
||||
|
||||
### 待处理服务数据模型
|
||||
**文件**: `src/Models/GwPendingServiceDiscovery.cs`
|
||||
|
||||
```csharp
|
||||
public class GwPendingServiceDiscovery
|
||||
{
|
||||
public string K8sServiceName { get; set; } // K8s Service 名称
|
||||
public string K8sNamespace { get; set; } // K8s 命名空间
|
||||
public string K8sClusterIP { get; set; } // ClusterIP
|
||||
public string DiscoveredPorts { get; set; } // JSON 序列化的端口列表
|
||||
public string Labels { get; set; } // K8s 标签
|
||||
public string AssignedClusterId { get; set; } // 分配的集群 ID
|
||||
public int Status { get; set; } // 状态
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. JWT 认证集成
|
||||
|
||||
### 概述
|
||||
网关解析 JWT Token,提取租户和用户信息,转换为下游服务可用的 HTTP 头。
|
||||
|
||||
### 配置
|
||||
**文件**: `src/Config/JwtConfig.cs`
|
||||
|
||||
```csharp
|
||||
public class JwtConfig
|
||||
{
|
||||
public string Authority { get; set; } = string.Empty; // 认证服务器地址
|
||||
public string Audience { get; set; } = string.Empty; // 受众
|
||||
public bool ValidateIssuer { get; set; } = true; // 验证签发者
|
||||
public bool ValidateAudience { get; set; } = true; // 验证受众
|
||||
}
|
||||
```
|
||||
|
||||
**配置示例** (`src/appsettings.json`):
|
||||
```json
|
||||
{
|
||||
"Jwt": {
|
||||
"Authority": "https://your-auth-server.com",
|
||||
"Audience": "fengling-gateway",
|
||||
"ValidateIssuer": true,
|
||||
"ValidateAudience": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JWT 转换中间件
|
||||
**文件**: `src/Middleware/JwtTransformMiddleware.cs`
|
||||
|
||||
```csharp
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
|
||||
{
|
||||
var token = authHeader.Substring("Bearer ".Length).Trim();
|
||||
var jwtToken = jwtHandler.ReadJwtToken(token);
|
||||
|
||||
// 提取声明并转换为 HTTP 头
|
||||
var tenantId = jwtToken.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value;
|
||||
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
||||
context.Request.Headers["X-User-Id"] = userId;
|
||||
context.Request.Headers["X-User-Name"] = userName;
|
||||
context.Request.Headers["X-Roles"] = string.Join(",", roles);
|
||||
}
|
||||
await _next(context);
|
||||
}
|
||||
```
|
||||
|
||||
### JWT 声明到 HTTP 头映射
|
||||
| JWT 声明类型 | HTTP 头 | 说明 |
|
||||
|--------------|---------|------|
|
||||
| `tenant` | `X-Tenant-Id` | 租户标识 |
|
||||
| `ClaimTypes.NameIdentifier` | `X-User-Id` | 用户 ID |
|
||||
| `ClaimTypes.Name` | `X-User-Name` | 用户名 |
|
||||
| `ClaimTypes.Role` | `X-Roles` | 角色列表(逗号分隔) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 外部 API 和服务连接
|
||||
|
||||
### CORS 配置
|
||||
**文件**: `src/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"Cors": {
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:5174"
|
||||
],
|
||||
"AllowAnyOrigin": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 健康检查端点
|
||||
**文件**: `src/Program.cs`
|
||||
|
||||
```csharp
|
||||
app.MapGet("/health", () => Results.Ok(new {
|
||||
status = "healthy",
|
||||
timestamp = DateTime.UtcNow
|
||||
}));
|
||||
```
|
||||
|
||||
### 下游服务健康检查
|
||||
**文件**: `src/Config/DatabaseClusterConfigProvider.cs`
|
||||
|
||||
```csharp
|
||||
HealthCheck = new HealthCheckConfig
|
||||
{
|
||||
Active = new ActiveHealthCheckConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Interval = TimeSpan.FromSeconds(30),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Path = "/health"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动态代理配置
|
||||
**文件**: `src/DynamicProxy/DynamicProxyConfigProvider.cs`
|
||||
|
||||
实现 `IProxyConfigProvider` 接口,从数据库动态加载路由和集群配置:
|
||||
|
||||
```csharp
|
||||
public class DynamicProxyConfigProvider : IProxyConfigProvider
|
||||
{
|
||||
public IProxyConfig GetConfig() => _config;
|
||||
|
||||
public void UpdateConfig()
|
||||
{
|
||||
var routes = _routeProvider.GetRoutes();
|
||||
var clusters = _clusterProvider.GetClusters();
|
||||
_config = new InMemoryProxyConfig(routes, clusters, ...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 集成架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 客户端请求 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ YARP Gateway │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 中间件管道 │ │
|
||||
│ │ CORS → JWT转换 → 租户路由 → Controllers → Proxy │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │
|
||||
│ │RouteCache│ │ConfigProv│ │LoadBalancingPolicy │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └──────────────────────────┘ │
|
||||
└───────┼─────────────┼────────────────────────────────────────┘
|
||||
│ │
|
||||
┌───────────────┼─────────────┼───────────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐
|
||||
│ PostgreSQL │ │ Redis │ │ K8s │ │ Auth Server │
|
||||
│ │ │ │ │ API │ │ (JWT) │
|
||||
│ - 租户配置 │ │ - 分布式锁│ │ │ │ │
|
||||
│ - 路由配置 │ │ - 缓存 │ │ - Service │ │ - Token 签发 │
|
||||
│ - 服务实例 │ │ │ │ - Pod │ │ - 声明信息 │
|
||||
│ - NOTIFY机制 │ │ │ │ │ │ │
|
||||
└───────────────┘ └───────────┘ └───────────┘ └───────────────────┘
|
||||
│
|
||||
│ LISTEN/NOTIFY
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ 配置变更监听器 │
|
||||
│ PgSqlConfigChangeListener + FallbackPolling │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 配置热更新流程
|
||||
|
||||
```
|
||||
数据库配置变更
|
||||
│
|
||||
▼
|
||||
DbContext.SaveChangesAsync()
|
||||
│
|
||||
▼
|
||||
NOTIFY gateway_config_changed
|
||||
│
|
||||
▼
|
||||
PgSqlConfigChangeListener.OnNotification()
|
||||
│
|
||||
▼
|
||||
RouteCache.ReloadAsync()
|
||||
│
|
||||
▼
|
||||
DynamicProxyConfigProvider.UpdateConfig()
|
||||
│
|
||||
▼
|
||||
YARP 配置生效(无需重启)
|
||||
```
|
||||
|
||||
**兜底机制**: 每 5 分钟检查版本号,防止 NOTIFY 丢失导致配置不一致。
|
||||
@ -1,368 +0,0 @@
|
||||
# 🔒 YARP 网关安全审计报告
|
||||
|
||||
> 审计日期:2026-02-28
|
||||
> 审计范围:认证授权、注入漏洞、敏感信息、访问控制、配置安全
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
| 严重程度 | 数量 |
|
||||
|---------|------|
|
||||
| 🔴 严重 (CRITICAL) | 3 |
|
||||
| 🟠 高危 (HIGH) | 3 |
|
||||
| 🟡 中危 (MEDIUM) | 4 |
|
||||
| 🟢 低危 (LOW) | 3 |
|
||||
| **总计** | **13** |
|
||||
|
||||
---
|
||||
|
||||
## 🔴 严重漏洞
|
||||
|
||||
### 1. 硬编码数据库凭据泄露
|
||||
|
||||
**文件:** `src/appsettings.json` 第 19 行
|
||||
|
||||
**问题代码:**
|
||||
```json
|
||||
"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
|
||||
```
|
||||
|
||||
**攻击场景:**
|
||||
- 代码泄露或被推送到公开仓库时,攻击者直接获得数据库完整访问权限
|
||||
- 可读取、修改、删除所有业务数据
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
// 使用环境变量或 Secret Manager
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
// 或使用 Azure Key Vault / AWS Secrets Manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 硬编码 Redis 凭据泄露
|
||||
|
||||
**文件:** `src/Config/RedisConfig.cs` 第 5 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542";
|
||||
```
|
||||
|
||||
**攻击场景:**
|
||||
- 攻击者可连接 Redis 服务器,读取缓存数据、修改路由配置、注入恶意数据
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
public string ConnectionString { get; set; } = string.Empty;
|
||||
// 从环境变量或配置中心读取
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 管理 API 完全无认证保护
|
||||
|
||||
**文件:** `src/Controllers/GatewayConfigController.cs` 及 `src/Controllers/PendingServicesController.cs`
|
||||
|
||||
**问题描述:**
|
||||
- 所有 API 端点均无 `[Authorize]` 特性
|
||||
- `Program.cs` 中未配置 `AddAuthentication()` 和 `UseAuthentication()`
|
||||
- 项目搜索未发现任何认证中间件
|
||||
|
||||
**攻击场景:**
|
||||
```
|
||||
# 攻击者可直接调用以下 API:
|
||||
POST /api/gateway/tenants # 创建任意租户
|
||||
DELETE /api/gateway/tenants/{id} # 删除租户
|
||||
POST /api/gateway/routes # 创建恶意路由
|
||||
POST /api/gateway/config/reload # 重载配置
|
||||
DELETE /api/gateway/clusters/{id} # 删除服务集群
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options => { /* 配置 JWT 验证 */ });
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Controllers
|
||||
[ApiController]
|
||||
[Route("api/gateway")]
|
||||
[Authorize] // 添加认证要求
|
||||
public class GatewayConfigController : ControllerBase
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟠 高危漏洞
|
||||
|
||||
### 4. JWT 签名验证缺失
|
||||
|
||||
**文件:** `src/Middleware/JwtTransformMiddleware.cs` 第 39-40 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
var jwtHandler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = jwtHandler.ReadJwtToken(token); // 仅读取,不验证!
|
||||
```
|
||||
|
||||
**攻击场景:**
|
||||
```python
|
||||
# 攻击者可伪造任意 JWT
|
||||
import jwt
|
||||
fake_token = jwt.encode({"tenant": "admin-tenant", "sub": "admin"}, "any_secret", algorithm="HS256")
|
||||
# 网关会接受这个伪造的 token
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = _jwtConfig.Authority,
|
||||
ValidAudience = _jwtConfig.Audience,
|
||||
IssuerSigningKey = /* 从 Authority 获取公钥 */
|
||||
};
|
||||
|
||||
var principal = jwtHandler.ValidateToken(token, validationParameters, out _);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 租户隔离可被 Header 注入绕过
|
||||
|
||||
**文件:** `src/Middleware/JwtTransformMiddleware.cs` 第 54 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
||||
```
|
||||
|
||||
**攻击场景:**
|
||||
```bash
|
||||
# 攻击者直接注入 Header 绕过 JWT
|
||||
curl -H "X-Tenant-Id: target-tenant" \
|
||||
-H "X-User-Id: admin" \
|
||||
-H "X-Roles: admin" \
|
||||
https://gateway/api/sensitive-data
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
// 在中间件开始时移除所有 X-* Header
|
||||
foreach (var header in context.Request.Headers.Where(h => h.Key.StartsWith("X-")).ToList())
|
||||
{
|
||||
context.Request.Headers.Remove(header.Key);
|
||||
}
|
||||
|
||||
// 然后再从 JWT 设置可信的 header
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 租户路由信息泄露
|
||||
|
||||
**文件:** `src/Middleware/TenantRoutingMiddleware.cs` 第 44 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
_logger.LogWarning("Route not found - Tenant: {Tenant}, Service: {Service}", tenantId, serviceName);
|
||||
```
|
||||
|
||||
**攻击场景:**
|
||||
- 日志中记录租户 ID 和服务名,攻击者可通过日志收集系统架构信息
|
||||
- 配合其他攻击进行侦察
|
||||
|
||||
**修复建议:**
|
||||
- 敏感信息不应记录到普通日志
|
||||
- 使用脱敏处理或仅记录哈希值
|
||||
|
||||
---
|
||||
|
||||
## 🟡 中危漏洞
|
||||
|
||||
### 7. 日志记录敏感连接信息
|
||||
|
||||
**文件:** `src/Services/RedisConnectionManager.cs` 第 44 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
_logger.LogInformation("Connected to Redis at {ConnectionString}", _config.ConnectionString);
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
_logger.LogInformation("Connected to Redis at {Host}",
|
||||
configuration.EndPoints.FirstOrDefault()?.ToString());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. CORS 凭据配置存在风险
|
||||
|
||||
**文件:** `src/Program.cs` 第 89-100 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
if (allowAnyOrigin)
|
||||
{
|
||||
policy.AllowAnyOrigin();
|
||||
}
|
||||
// ...
|
||||
policy.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials(); // 与 AllowAnyOrigin 不兼容
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
if (allowAnyOrigin)
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
// 不允许 AllowCredentials
|
||||
}
|
||||
else
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 健康检查端点信息泄露
|
||||
|
||||
**文件:** `src/Program.cs` 第 115 行
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
// 添加访问限制或使用标准健康检查
|
||||
builder.Services.AddHealthChecks();
|
||||
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
ResponseWriter = async (c, r) =>
|
||||
await c.Response.WriteAsync("healthy")
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. JWT Authority 使用占位符 URL
|
||||
|
||||
**文件:** `src/appsettings.json` 第 22 行
|
||||
|
||||
**问题代码:**
|
||||
```json
|
||||
"Authority": "https://your-auth-server.com"
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
- 强制要求配置有效的 Authority URL
|
||||
- 启动时验证配置有效性
|
||||
|
||||
---
|
||||
|
||||
## 🟢 低危漏洞
|
||||
|
||||
### 11. 可预测的 ID 生成
|
||||
|
||||
**文件:** `src/Controllers/GatewayConfigController.cs` 第 484-487 行
|
||||
|
||||
**问题代码:**
|
||||
```csharp
|
||||
private long GenerateId()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
```
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
// 使用 GUID 或雪花算法
|
||||
private long GenerateId() => SnowflakeIdGenerator.NextId();
|
||||
// 或
|
||||
private string GenerateId() => Guid.NewGuid().ToString("N");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12. 缺少输入验证
|
||||
|
||||
**文件:** `src/Controllers/GatewayConfigController.cs` 多处
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
public class CreateTenantDto
|
||||
{
|
||||
[Required]
|
||||
[RegularExpression(@"^[a-zA-Z0-9-]{1,50}$")]
|
||||
public string TenantCode { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(100, MinimumLength = 1)]
|
||||
public string TenantName { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13. 错误消息暴露内部信息
|
||||
|
||||
**文件:** `src/Controllers/PendingServicesController.cs` 第 116 行
|
||||
|
||||
**修复建议:**
|
||||
```csharp
|
||||
return BadRequest(new { message = "Invalid cluster configuration" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 修复优先级建议
|
||||
|
||||
| 优先级 | 漏洞编号 | 修复时间建议 |
|
||||
|-------|---------|------------|
|
||||
| P0 (立即) | #1, #2, #3 | 24小时内 |
|
||||
| P1 (紧急) | #4, #5, #6 | 1周内 |
|
||||
| P2 (重要) | #7, #8, #9, #10 | 2周内 |
|
||||
| P3 (一般) | #11, #12, #13 | 1个月内 |
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 安全加固建议
|
||||
|
||||
### 1. 认证授权
|
||||
- 实施完整的 JWT 验证流程
|
||||
- 为所有管理 API 添加 `[Authorize]`
|
||||
- 实施基于角色的访问控制 (RBAC)
|
||||
|
||||
### 2. 配置安全
|
||||
- 使用 Azure Key Vault / AWS Secrets Manager 管理密钥
|
||||
- 移除所有硬编码凭据
|
||||
- 生产环境禁用调试模式
|
||||
|
||||
### 3. 租户隔离
|
||||
- 在网关层强制验证租户归属
|
||||
- 使用加密签名验证内部 Header
|
||||
- 实施租户数据隔离审计
|
||||
|
||||
### 4. 日志安全
|
||||
- 敏感信息脱敏
|
||||
- 限制日志访问权限
|
||||
- 使用结构化日志便于审计
|
||||
|
||||
---
|
||||
|
||||
*报告由安全审计生成,建议人工复核后纳入迭代计划。*
|
||||
@ -1,189 +0,0 @@
|
||||
# YARP 网关技术栈文档
|
||||
|
||||
## 1. 语言和运行时
|
||||
|
||||
### .NET 版本
|
||||
- **目标框架**: .NET 10.0
|
||||
- **项目文件**: `src/YarpGateway.csproj`
|
||||
- **SDK**: `Microsoft.NET.Sdk.Web`
|
||||
|
||||
```xml
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
```
|
||||
|
||||
## 2. 核心框架
|
||||
|
||||
### YARP (Yet Another Reverse Proxy)
|
||||
- **包**: `Yarp.ReverseProxy`
|
||||
- **用途**: 微服务 API 网关核心反向代理引擎
|
||||
- **主要功能**:
|
||||
- 动态路由配置
|
||||
- 负载均衡策略
|
||||
- 健康检查
|
||||
- 请求转发
|
||||
|
||||
### ASP.NET Core
|
||||
- **用途**: Web 应用宿主框架
|
||||
- **特性**:
|
||||
- 依赖注入 (DI)
|
||||
- 中间件管道
|
||||
- 配置系统
|
||||
- 日志集成
|
||||
|
||||
## 3. 主要依赖包
|
||||
|
||||
### 数据访问
|
||||
| 包名 | 用途 |
|
||||
|------|------|
|
||||
| `Npgsql.EntityFrameworkCore.PostgreSQL` | PostgreSQL Entity Framework Core 提供程序 |
|
||||
| `Microsoft.EntityFrameworkCore.Design` | EF Core 设计时工具(迁移) |
|
||||
|
||||
### 缓存与分布式锁
|
||||
| 包名 | 用途 |
|
||||
|------|------|
|
||||
| `StackExchange.Redis` | Redis 客户端,用于分布式锁和缓存 |
|
||||
|
||||
### 认证授权
|
||||
| 包名 | 用途 |
|
||||
|------|------|
|
||||
| `Microsoft.AspNetCore.Authentication.JwtBearer` | JWT Bearer 认证支持 |
|
||||
|
||||
### 日志
|
||||
| 包名 | 用途 |
|
||||
|------|------|
|
||||
| `Serilog.AspNetCore` | Serilog ASP.NET Core 集成 |
|
||||
| `Serilog.Sinks.Console` | 控制台日志输出 |
|
||||
| `Serilog.Sinks.File` | 文件日志输出 |
|
||||
|
||||
### 服务发现(自定义包)
|
||||
| 包名 | 用途 |
|
||||
|------|------|
|
||||
| `Fengling.ServiceDiscovery.Core` | 服务发现核心接口 |
|
||||
| `Fengling.ServiceDiscovery.Kubernetes` | Kubernetes 服务发现实现 |
|
||||
| `Fengling.ServiceDiscovery.Static` | 静态配置服务发现 |
|
||||
|
||||
## 4. 配置文件
|
||||
|
||||
### 主配置文件
|
||||
**位置**: `src/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=...;Port=...;Database=...;Username=...;Password=..."
|
||||
},
|
||||
"Jwt": {
|
||||
"Authority": "https://your-auth-server.com",
|
||||
"Audience": "fengling-gateway",
|
||||
"ValidateIssuer": true,
|
||||
"ValidateAudience": true
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "host:port",
|
||||
"Database": 0,
|
||||
"InstanceName": "YarpGateway"
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": ["http://localhost:5173"],
|
||||
"AllowAnyOrigin": false
|
||||
},
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": { "Url": "http://0.0.0.0:8080" }
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information",
|
||||
"WriteTo": [
|
||||
{ "Name": "Console" },
|
||||
{ "Name": "File", "Args": { "path": "logs/gateway-.log", "rollingInterval": "Day" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
| 文件路径 | 类名 | 用途 |
|
||||
|----------|------|------|
|
||||
| `src/Config/JwtConfig.cs` | `JwtConfig` | JWT 认证配置 |
|
||||
| `src/Config/RedisConfig.cs` | `RedisConfig` | Redis 连接配置 |
|
||||
| `src/Config/ConfigNotifyChannel.cs` | `ConfigNotifyChannel` | PostgreSQL NOTIFY 通道常量 |
|
||||
|
||||
## 5. Docker 支持
|
||||
|
||||
### Dockerfile
|
||||
**位置**: `Dockerfile`
|
||||
|
||||
```dockerfile
|
||||
# 基础镜像
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
# 构建镜像
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
# 多阶段构建...
|
||||
|
||||
# 最终镜像
|
||||
FROM base AS final
|
||||
ENTRYPOINT ["dotnet", "YarpGateway.dll"]
|
||||
```
|
||||
|
||||
### Docker 配置
|
||||
- **默认目标 OS**: Linux
|
||||
- **暴露端口**: 8080 (HTTP), 8081 (HTTPS)
|
||||
- **工作目录**: `/app`
|
||||
|
||||
## 6. 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── Config/ # 配置类
|
||||
│ ├── JwtConfig.cs
|
||||
│ ├── RedisConfig.cs
|
||||
│ ├── ConfigNotifyChannel.cs
|
||||
│ ├── DatabaseRouteConfigProvider.cs
|
||||
│ └── DatabaseClusterConfigProvider.cs
|
||||
├── Data/ # 数据访问层
|
||||
│ ├── GatewayDbContext.cs
|
||||
│ └── GatewayDbContextFactory.cs
|
||||
├── DynamicProxy/ # 动态代理配置
|
||||
│ └── DynamicProxyConfigProvider.cs
|
||||
├── LoadBalancing/ # 负载均衡策略
|
||||
│ └── DistributedWeightedRoundRobinPolicy.cs
|
||||
├── Middleware/ # 中间件
|
||||
│ ├── JwtTransformMiddleware.cs
|
||||
│ └── TenantRoutingMiddleware.cs
|
||||
├── Models/ # 数据模型
|
||||
│ ├── GwTenant.cs
|
||||
│ ├── GwTenantRoute.cs
|
||||
│ ├── GwServiceInstance.cs
|
||||
│ └── GwPendingServiceDiscovery.cs
|
||||
├── Services/ # 业务服务
|
||||
│ ├── RouteCache.cs
|
||||
│ ├── RedisConnectionManager.cs
|
||||
│ ├── KubernetesPendingSyncService.cs
|
||||
│ └── PgSqlConfigChangeListener.cs
|
||||
├── Program.cs # 应用入口
|
||||
├── appsettings.json # 配置文件
|
||||
└── YarpGateway.csproj # 项目文件
|
||||
```
|
||||
|
||||
## 7. 中间件管道
|
||||
|
||||
请求处理管道顺序(`Program.cs`):
|
||||
|
||||
1. **CORS** - 跨域请求处理
|
||||
2. **JwtTransformMiddleware** - JWT 解析与转换
|
||||
3. **TenantRoutingMiddleware** - 租户路由解析
|
||||
4. **Controllers** - API 控制器
|
||||
5. **ReverseProxy** - YARP 反向代理
|
||||
|
||||
## 8. 托管与部署
|
||||
|
||||
### Kestrel 配置
|
||||
- 监听地址: `http://0.0.0.0:8080`
|
||||
- 支持 Docker 容器化部署
|
||||
- 支持 Kubernetes 集群部署
|
||||
@ -1,465 +0,0 @@
|
||||
# YARP Gateway 目录结构文档
|
||||
|
||||
## 1. 目录布局
|
||||
|
||||
```
|
||||
fengling-gateway/
|
||||
├── .planning/ # 规划文档目录
|
||||
│ └── codebase/ # 代码库分析文档
|
||||
│ ├── ARCHITECTURE.md # 架构文档
|
||||
│ └── STRUCTURE.md # 本文档
|
||||
│
|
||||
├── src/ # 源代码目录
|
||||
│ ├── Config/ # 配置类和提供者
|
||||
│ ├── Controllers/ # API 控制器
|
||||
│ ├── Data/ # 数据访问层
|
||||
│ ├── DynamicProxy/ # YARP 动态代理
|
||||
│ ├── LoadBalancing/ # 负载均衡策略
|
||||
│ ├── Migrations/ # 数据库迁移
|
||||
│ ├── Metrics/ # 监控指标
|
||||
│ ├── Middleware/ # 中间件
|
||||
│ ├── Models/ # 数据模型
|
||||
│ ├── Properties/ # 项目属性
|
||||
│ ├── Services/ # 业务服务
|
||||
│ ├── Program.cs # 程序入口
|
||||
│ ├── YarpGateway.csproj # 项目文件
|
||||
│ ├── appsettings.json # 配置文件
|
||||
│ └── appsettings.Development.json # 开发环境配置
|
||||
│
|
||||
└── (根目录其他文件)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细目录说明
|
||||
|
||||
### 2.1 Config/ - 配置层
|
||||
|
||||
**路径**: `src/Config/`
|
||||
|
||||
**用途**: 存放配置模型和配置提供者
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `JwtConfig.cs` | 10 | JWT 认证配置模型,包含 Authority、Audience 等属性 |
|
||||
| `RedisConfig.cs` | 9 | Redis 连接配置模型,包含连接字符串、数据库索引等 |
|
||||
| `ConfigNotifyChannel.cs` | 7 | PostgreSQL NOTIFY 通道名称常量定义 |
|
||||
| `DatabaseRouteConfigProvider.cs` | 84 | 从数据库加载路由配置,转换为 YARP RouteConfig |
|
||||
| `DatabaseClusterConfigProvider.cs` | 100 | 从数据库加载集群配置,管理服务实例列表 |
|
||||
|
||||
**设计特点**:
|
||||
- 配置类使用 POCO 模型,通过 Options 模式注入
|
||||
- Provider 类使用单例模式,支持热重载
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Controllers/ - 控制器层
|
||||
|
||||
**路径**: `src/Controllers/`
|
||||
|
||||
**用途**: RESTful API 端点
|
||||
|
||||
| 文件 | 行数 | 路由前缀 | 用途 |
|
||||
|------|------|----------|------|
|
||||
| `GatewayConfigController.cs` | 489 | `/api/gateway` | 网关配置管理 API |
|
||||
| `PendingServicesController.cs` | 210 | `/api/gateway/pending-services` | 待处理服务管理 API |
|
||||
|
||||
**GatewayConfigController 端点**:
|
||||
|
||||
| 方法 | 路由 | 功能 |
|
||||
|------|------|------|
|
||||
| GET | `/tenants` | 获取租户列表(分页) |
|
||||
| GET | `/tenants/{id}` | 获取单个租户 |
|
||||
| POST | `/tenants` | 创建租户 |
|
||||
| PUT | `/tenants/{id}` | 更新租户 |
|
||||
| DELETE | `/tenants/{id}` | 删除租户 |
|
||||
| GET | `/routes` | 获取路由列表(分页) |
|
||||
| GET | `/routes/global` | 获取全局路由 |
|
||||
| GET | `/routes/tenant/{tenantCode}` | 获取租户路由 |
|
||||
| POST | `/routes` | 创建路由 |
|
||||
| PUT | `/routes/{id}` | 更新路由 |
|
||||
| DELETE | `/routes/{id}` | 删除路由 |
|
||||
| GET | `/clusters` | 获取集群列表 |
|
||||
| GET | `/clusters/{clusterId}` | 获取集群详情 |
|
||||
| POST | `/clusters` | 创建集群 |
|
||||
| DELETE | `/clusters/{clusterId}` | 删除集群 |
|
||||
| GET | `/clusters/{clusterId}/instances` | 获取实例列表 |
|
||||
| POST | `/clusters/{clusterId}/instances` | 添加实例 |
|
||||
| DELETE | `/instances/{id}` | 删除实例 |
|
||||
| POST | `/config/reload` | 重载配置 |
|
||||
| GET | `/config/status` | 获取配置状态 |
|
||||
| GET | `/config/versions` | 获取版本信息 |
|
||||
| GET | `/stats/overview` | 获取统计概览 |
|
||||
|
||||
**PendingServicesController 端点**:
|
||||
|
||||
| 方法 | 路由 | 功能 |
|
||||
|------|------|------|
|
||||
| GET | `/` | 获取待处理服务列表 |
|
||||
| GET | `/{id}` | 获取待处理服务详情 |
|
||||
| POST | `/{id}/assign` | 分配服务到集群 |
|
||||
| POST | `/{id}/reject` | 拒绝服务 |
|
||||
| GET | `/clusters` | 获取可用集群列表 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Data/ - 数据访问层
|
||||
|
||||
**路径**: `src/Data/`
|
||||
|
||||
**用途**: Entity Framework Core 数据库上下文
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `GatewayDbContext.cs` | 142 | EF Core 数据库上下文,包含实体配置和变更通知 |
|
||||
| `GatewayDbContextFactory.cs` | 23 | 设计时 DbContext 工厂,用于迁移命令 |
|
||||
|
||||
**DbContext 特性**:
|
||||
- 自动检测配置变更
|
||||
- 集成 PostgreSQL NOTIFY 机制
|
||||
- 支持软删除(IsDeleted 标记)
|
||||
- 版本号追踪(Version 字段)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 DynamicProxy/ - 动态代理层
|
||||
|
||||
**路径**: `src/DynamicProxy/`
|
||||
|
||||
**用途**: YARP 动态配置提供
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `DynamicProxyConfigProvider.cs` | 79 | 实现 IProxyConfigProvider,整合路由和集群配置 |
|
||||
|
||||
**核心职责**:
|
||||
- 实现 YARP 配置提供接口
|
||||
- 协调 Route 和 Cluster 配置
|
||||
- 提供配置变更通知(通过 CancellationToken)
|
||||
|
||||
---
|
||||
|
||||
### 2.5 LoadBalancing/ - 负载均衡层
|
||||
|
||||
**路径**: `src/LoadBalancing/`
|
||||
|
||||
**用途**: 自定义负载均衡策略
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `DistributedWeightedRoundRobinPolicy.cs` | 244 | 基于 Redis 的分布式加权轮询策略 |
|
||||
|
||||
**策略特点**:
|
||||
- 策略名称: `DistributedWeightedRoundRobin`
|
||||
- 支持实例权重配置
|
||||
- Redis 分布式状态存储
|
||||
- 降级策略(锁获取失败时)
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Migrations/ - 数据库迁移
|
||||
|
||||
**路径**: `src/Migrations/`
|
||||
|
||||
**用途**: Entity Framework Core 迁移文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `20260201120312_InitialCreate.cs` | 初始数据库创建 |
|
||||
| `20260201133826_AddIsGlobalToTenantRoute.cs` | 添加 IsGlobal 字段 |
|
||||
| `20260222134342_AddPendingServiceDiscovery.cs` | 添加待处理服务发现表 |
|
||||
| `*ModelSnapshot.cs` | 当前模型快照 |
|
||||
| `*.Designer.cs` | 设计器生成文件 |
|
||||
|
||||
---
|
||||
|
||||
### 2.7 Metrics/ - 监控指标
|
||||
|
||||
**路径**: `src/Metrics/`
|
||||
|
||||
**用途**: OpenTelemetry 指标定义
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `GatewayMetrics.cs` | 31 | 定义网关监控指标 |
|
||||
|
||||
**指标列表**:
|
||||
- `gateway_requests_total` - 请求总数计数器
|
||||
- `gateway_request_duration_seconds` - 请求延迟直方图
|
||||
|
||||
---
|
||||
|
||||
### 2.8 Middleware/ - 中间件层
|
||||
|
||||
**路径**: `src/Middleware/`
|
||||
|
||||
**用途**: ASP.NET Core 中间件
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `JwtTransformMiddleware.cs` | 84 | JWT Token 解析,提取租户信息注入请求头 |
|
||||
| `TenantRoutingMiddleware.cs` | 64 | 租户路由解析,根据路径查找目标集群 |
|
||||
|
||||
**中间件执行顺序**:
|
||||
```
|
||||
CORS -> JwtTransformMiddleware -> TenantRoutingMiddleware -> YARP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.9 Models/ - 数据模型层
|
||||
|
||||
**路径**: `src/Models/`
|
||||
|
||||
**用途**: 实体类定义
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `GwTenant.cs` | 16 | 租户实体 |
|
||||
| `GwTenantRoute.cs` | 20 | 路由配置实体 |
|
||||
| `GwServiceInstance.cs` | 19 | 服务实例实体 |
|
||||
| `GwPendingServiceDiscovery.cs` | 28 | 待处理服务发现实体 + 状态枚举 |
|
||||
|
||||
**实体通用字段**:
|
||||
- `Id` - 主键(雪花 ID 格式)
|
||||
- `Status` - 状态(1=启用)
|
||||
- `CreatedBy/UpdatedBy` - 操作人
|
||||
- `CreatedTime/UpdatedTime` - 时间戳
|
||||
- `IsDeleted` - 软删除标记
|
||||
- `Version` - 版本号(乐观锁)
|
||||
|
||||
---
|
||||
|
||||
### 2.10 Services/ - 服务层
|
||||
|
||||
**路径**: `src/Services/`
|
||||
|
||||
**用途**: 业务逻辑和后台服务
|
||||
|
||||
| 文件 | 行数 | 类型 | 用途 |
|
||||
|------|------|------|------|
|
||||
| `RouteCache.cs` | 139 | Singleton | 路由缓存,支持租户路由和全局路由 |
|
||||
| `RedisConnectionManager.cs` | 139 | Singleton | Redis 连接管理,分布式锁实现 |
|
||||
| `PgSqlConfigChangeListener.cs` | 223 | HostedService | PostgreSQL 配置变更监听 |
|
||||
| `KubernetesPendingSyncService.cs` | 162 | HostedService | Kubernetes 服务发现同步 |
|
||||
|
||||
**服务生命周期**:
|
||||
- Singleton: RouteCache, RedisConnectionManager(状态服务)
|
||||
- HostedService: PgSqlConfigChangeListener, KubernetesPendingSyncService(后台任务)
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键文件位置
|
||||
|
||||
### 3.1 入口文件
|
||||
|
||||
| 文件 | 路径 | 用途 |
|
||||
|------|------|------|
|
||||
| `Program.cs` | `src/Program.cs` | 应用程序入口,服务注册和中间件配置 |
|
||||
|
||||
### 3.2 配置文件
|
||||
|
||||
| 文件 | 路径 | 用途 |
|
||||
|------|------|------|
|
||||
| `appsettings.json` | `src/appsettings.json` | 生产环境配置 |
|
||||
| `appsettings.Development.json` | `src/appsettings.Development.json` | 开发环境配置 |
|
||||
| `YarpGateway.csproj` | `src/YarpGateway.csproj` | 项目文件,包引用 |
|
||||
|
||||
### 3.3 数据库相关
|
||||
|
||||
| 文件 | 路径 | 用途 |
|
||||
|------|------|------|
|
||||
| `GatewayDbContext.cs` | `src/Data/GatewayDbContext.cs` | 数据库上下文 |
|
||||
| `GatewayDbContextFactory.cs` | `src/Data/GatewayDbContextFactory.cs` | 迁移工具工厂 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 命名约定
|
||||
|
||||
### 4.1 文件命名
|
||||
|
||||
| 类型 | 命名规则 | 示例 |
|
||||
|------|----------|------|
|
||||
| 实体类 | `Gw` 前缀 + PascalCase | `GwTenant.cs`, `GwTenantRoute.cs` |
|
||||
| 配置类 | `*Config` 后缀 | `JwtConfig.cs`, `RedisConfig.cs` |
|
||||
| 提供者 | `*Provider` 后缀 | `DatabaseRouteConfigProvider.cs` |
|
||||
| 中间件 | `*Middleware` 后缀 | `JwtTransformMiddleware.cs` |
|
||||
| 控制器 | `*Controller` 后缀 | `GatewayConfigController.cs` |
|
||||
| 服务 | 功能描述 + 类型 | `RouteCache.cs`, `PgSqlConfigChangeListener.cs` |
|
||||
| 策略 | `*Policy` 后缀 | `DistributedWeightedRoundRobinPolicy.cs` |
|
||||
|
||||
### 4.2 命名空间
|
||||
|
||||
```
|
||||
YarpGateway # 根命名空间
|
||||
├── Config # 配置相关
|
||||
├── Controllers # API 控制器
|
||||
├── Data # 数据访问
|
||||
├── DynamicProxy # 动态代理
|
||||
├── LoadBalancing # 负载均衡
|
||||
├── Metrics # 监控指标
|
||||
├── Middleware # 中间件
|
||||
├── Models # 数据模型
|
||||
└── Services # 业务服务
|
||||
```
|
||||
|
||||
### 4.3 接口命名
|
||||
|
||||
| 类型 | 命名规则 | 示例 |
|
||||
|------|----------|------|
|
||||
| 服务接口 | `I` 前缀 | `IRouteCache`, `IRedisConnectionManager` |
|
||||
| DTO 类 | `*Dto` 后缀 | `CreateTenantDto`, `CreateRouteDto` |
|
||||
| 请求类 | `*Request` 后缀 | `AssignServiceRequest` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 模块组织
|
||||
|
||||
### 5.1 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Middleware │ │ Controllers │ │
|
||||
│ │ - JWT 解析 │ │ - GatewayConfigController │ │
|
||||
│ │ - 租户路由 │ │ - PendingServicesController │ │
|
||||
│ └─────────────────┘ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Business Logic Layer │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Services │ │ DynamicProxy │ │
|
||||
│ │ - RouteCache │ │ - DynamicProxyConfigProvider │ │
|
||||
│ │ - RedisManager │ │ │ │
|
||||
│ │ - ConfigListen │ └─────────────────────────────────┘ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Data Access Layer │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Models │ │ Data │ │
|
||||
│ │ - GwTenant │ │ - GatewayDbContext │ │
|
||||
│ │ - GwRoute │ │ - GatewayDbContextFactory │ │
|
||||
│ │ - GwInstance │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Config │ │ LoadBalancing │ │
|
||||
│ │ - JwtConfig │ │ - WeightedRoundRobinPolicy │ │
|
||||
│ │ - RedisConfig │ │ │ │
|
||||
│ │ - Providers │ └─────────────────────────────────┘ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 模块依赖关系
|
||||
|
||||
```
|
||||
Program.cs
|
||||
│
|
||||
├── Config/
|
||||
│ ├── JwtConfig ◄── appsettings.json
|
||||
│ ├── RedisConfig ◄── appsettings.json
|
||||
│ ├── DatabaseRouteConfigProvider ◄── Data/GatewayDbContext
|
||||
│ └── DatabaseClusterConfigProvider ◄── Data/GatewayDbContext
|
||||
│
|
||||
├── DynamicProxy/
|
||||
│ └── DynamicProxyConfigProvider ◄── Config/*
|
||||
│
|
||||
├── Services/
|
||||
│ ├── RouteCache ◄── Data/GatewayDbContext, Models/*
|
||||
│ ├── RedisConnectionManager ◄── Config/RedisConfig
|
||||
│ ├── PgSqlConfigChangeListener ◄── DynamicProxy, Services/RouteCache
|
||||
│ └── KubernetesPendingSyncService ◄── Data/GatewayDbContext
|
||||
│
|
||||
├── Middleware/
|
||||
│ ├── JwtTransformMiddleware ◄── Config/JwtConfig
|
||||
│ └── TenantRoutingMiddleware ◄── Services/RouteCache
|
||||
│
|
||||
└── Controllers/
|
||||
├── GatewayConfigController ◄── Config/*, Services/RouteCache
|
||||
└── PendingServicesController ◄── Data/GatewayDbContext
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 项目依赖
|
||||
|
||||
### 6.1 NuGet 包引用
|
||||
|
||||
```xml
|
||||
<!-- 核心框架 -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" />
|
||||
|
||||
<!-- 数据库 -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
|
||||
<!-- 缓存 -->
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
|
||||
<!-- 日志 -->
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
|
||||
<!-- 服务发现 -->
|
||||
<PackageReference Include="Fengling.ServiceDiscovery.Core" />
|
||||
<PackageReference Include="Fengling.ServiceDiscovery.Kubernetes" />
|
||||
<PackageReference Include="Fengling.ServiceDiscovery.Static" />
|
||||
```
|
||||
|
||||
### 6.2 目标框架
|
||||
|
||||
```xml
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 文件统计
|
||||
|
||||
| 目录/文件 | 文件数 | 总行数 | 主要用途 |
|
||||
|-----------|--------|--------|----------|
|
||||
| `Config/` | 5 | ~210 | 配置模型和提供者 |
|
||||
| `Controllers/` | 2 | ~700 | REST API 端点 |
|
||||
| `Data/` | 2 | ~165 | 数据库上下文 |
|
||||
| `DynamicProxy/` | 1 | ~79 | YARP 配置集成 |
|
||||
| `LoadBalancing/` | 1 | ~244 | 负载均衡策略 |
|
||||
| `Migrations/` | 6 | ~500+ | 数据库迁移 |
|
||||
| `Metrics/` | 1 | ~31 | 监控指标 |
|
||||
| `Middleware/` | 2 | ~148 | 请求处理中间件 |
|
||||
| `Models/` | 4 | ~83 | 数据实体 |
|
||||
| `Services/` | 4 | ~665 | 业务服务 |
|
||||
| `Program.cs` | 1 | 135 | 应用入口 |
|
||||
| **总计** | **29** | **~2900+** | - |
|
||||
|
||||
---
|
||||
|
||||
## 8. 扩展建议
|
||||
|
||||
### 8.1 建议新增目录
|
||||
|
||||
| 目录 | 用途 |
|
||||
|------|------|
|
||||
| `Extensions/` | 扩展方法 |
|
||||
| `Constants/` | 常量定义 |
|
||||
| `Exceptions/` | 自定义异常 |
|
||||
| `Validators/` | 输入验证器 |
|
||||
| `Dtos/` | 数据传输对象(从 Controllers 提取) |
|
||||
|
||||
### 8.2 代码组织建议
|
||||
|
||||
1. 将 Controller 中的 DTO 类提取到独立的 `Dtos/` 目录
|
||||
2. 添加 `Extensions/` 存放 IServiceCollection 扩展方法
|
||||
3. 考虑将配置验证逻辑提取到 `Validators/`
|
||||
@ -1,833 +0,0 @@
|
||||
# YARP Gateway 测试文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了 YARP Gateway 项目的测试策略、测试模式和最佳实践。
|
||||
|
||||
---
|
||||
|
||||
## 1. 测试框架
|
||||
|
||||
### 1.1 当前测试状态
|
||||
|
||||
**项目当前没有专门的测试目录或测试项目。**
|
||||
|
||||
检查项目结构:
|
||||
```
|
||||
fengling-gateway/
|
||||
├── src/ # 源代码
|
||||
│ └── YarpGateway.csproj # 主项目
|
||||
├── .planning/
|
||||
└── (无 tests/ 或 test/ 目录)
|
||||
```
|
||||
|
||||
检查 `.csproj` 文件确认无测试框架依赖:
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
**结论**:项目目前处于开发阶段,尚未建立测试基础设施。
|
||||
|
||||
### 1.2 推荐测试框架
|
||||
|
||||
基于项目技术栈,推荐以下测试框架:
|
||||
|
||||
| 框架 | 用途 | NuGet 包 |
|
||||
|------|------|----------|
|
||||
| xUnit | 单元测试框架 | `xunit` |
|
||||
| Moq | Mock 框架 | `Moq` |
|
||||
| FluentAssertions | 断言库 | `FluentAssertions` |
|
||||
| Microsoft.NET.Test.Sdk | 测试 SDK | `Microsoft.NET.Test.Sdk` |
|
||||
| Testcontainers | 集成测试容器 | `Testcontainers.PostgreSql`, `Testcontainers.Redis` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 推荐测试结构
|
||||
|
||||
### 2.1 测试项目组织
|
||||
|
||||
建议创建独立的测试项目:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── YarpGateway.UnitTests/ # 单元测试
|
||||
│ ├── Services/
|
||||
│ │ ├── RouteCacheTests.cs
|
||||
│ │ └── RedisConnectionManagerTests.cs
|
||||
│ ├── Middleware/
|
||||
│ │ ├── JwtTransformMiddlewareTests.cs
|
||||
│ │ └── TenantRoutingMiddlewareTests.cs
|
||||
│ └── Controllers/
|
||||
│ └── GatewayConfigControllerTests.cs
|
||||
│
|
||||
├── YarpGateway.IntegrationTests/ # 集成测试
|
||||
│ ├── GatewayEndpointsTests.cs
|
||||
│ └── DatabaseTests.cs
|
||||
│
|
||||
└── YarpGateway.LoadTests/ # 负载测试(可选)
|
||||
└── RoutePerformanceTests.cs
|
||||
```
|
||||
|
||||
### 2.2 测试命名约定
|
||||
|
||||
```csharp
|
||||
// 命名格式:[被测类]Tests
|
||||
public class RouteCacheTests { }
|
||||
|
||||
// 方法命名格式:[方法名]_[场景]_[期望结果]
|
||||
[Fact]
|
||||
public async Task InitializeAsync_WithValidData_LoadsRoutesFromDatabase() { }
|
||||
|
||||
[Fact]
|
||||
public async Task GetRoute_WithNonexistentTenant_ReturnsNull() { }
|
||||
|
||||
[Fact]
|
||||
public async Task ReloadAsync_WhenCalled_RefreshesCache() { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 单元测试模式
|
||||
|
||||
### 3.1 服务层测试示例
|
||||
|
||||
```csharp
|
||||
// RouteCacheTests.cs
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class RouteCacheTests
|
||||
{
|
||||
private readonly Mock<IDbContextFactory<GatewayDbContext>> _mockDbContextFactory;
|
||||
private readonly Mock<ILogger<RouteCache>> _mockLogger;
|
||||
private readonly RouteCache _sut; // System Under Test
|
||||
|
||||
public RouteCacheTests()
|
||||
{
|
||||
_mockDbContextFactory = new Mock<IDbContextFactory<GatewayDbContext>>();
|
||||
_mockLogger = new Mock<ILogger<RouteCache>>();
|
||||
_sut = new RouteCache(_mockDbContextFactory.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitializeAsync_ShouldLoadRoutesFromDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var routes = new List<GwTenantRoute>
|
||||
{
|
||||
new() { Id = 1, ServiceName = "user-service", ClusterId = "user-cluster", IsGlobal = true }
|
||||
};
|
||||
|
||||
var mockDbSet = CreateMockDbSet(routes);
|
||||
var mockContext = new Mock<GatewayDbContext>();
|
||||
mockContext.Setup(c => c.TenantRoutes).Returns(mockDbSet.Object);
|
||||
|
||||
_mockDbContextFactory
|
||||
.Setup(f => f.CreateDbContext())
|
||||
.Returns(mockContext.Object);
|
||||
|
||||
// Act
|
||||
await _sut.InitializeAsync();
|
||||
|
||||
// Assert
|
||||
var result = _sut.GetRoute("tenant1", "user-service");
|
||||
result.Should().NotBeNull();
|
||||
result!.ClusterId.Should().Be("user-cluster");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRoute_WhenTenantRouteExists_ReturnsTenantRoute()
|
||||
{
|
||||
// Arrange - 设置租户专用路由
|
||||
// ...
|
||||
|
||||
// Act
|
||||
var result = _sut.GetRoute("tenant1", "service1");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.IsGlobal.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRoute_WhenNoTenantRouteButGlobalExists_ReturnsGlobalRoute()
|
||||
{
|
||||
// Arrange
|
||||
// ...
|
||||
|
||||
// Act
|
||||
var result = _sut.GetRoute("tenant-without-route", "global-service");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.IsGlobal.Should().BeTrue();
|
||||
}
|
||||
|
||||
// 辅助方法:创建模拟 DbSet
|
||||
private Mock<DbSet<T>> CreateMockDbSet<T>(List<T> data) where T : class
|
||||
{
|
||||
var queryable = data.AsQueryable();
|
||||
var mockSet = new Mock<DbSet<T>>();
|
||||
mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
|
||||
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
|
||||
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
|
||||
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator());
|
||||
return mockSet;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 中间件测试示例
|
||||
|
||||
```csharp
|
||||
// TenantRoutingMiddlewareTests.cs
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
public class TenantRoutingMiddlewareTests
|
||||
{
|
||||
private readonly Mock<RequestDelegate> _mockNext;
|
||||
private readonly Mock<IRouteCache> _mockRouteCache;
|
||||
private readonly Mock<ILogger<TenantRoutingMiddleware>> _mockLogger;
|
||||
private readonly TenantRoutingMiddleware _sut;
|
||||
|
||||
public TenantRoutingMiddlewareTests()
|
||||
{
|
||||
_mockNext = new Mock<RequestDelegate>();
|
||||
_mockRouteCache = new Mock<IRouteCache>();
|
||||
_mockLogger = new Mock<ILogger<TenantRoutingMiddleware>>();
|
||||
_sut = new TenantRoutingMiddleware(_mockNext.Object, _mockRouteCache.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_WithoutTenantHeader_CallsNextWithoutProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/user-service/users";
|
||||
|
||||
// Act
|
||||
await _sut.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
_mockNext.Verify(n => n(context), Times.Once);
|
||||
_mockRouteCache.Verify(r => r.GetRoute(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_WithValidTenantAndRoute_SetsDynamicClusterId()
|
||||
{
|
||||
// Arrange
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/order-service/orders";
|
||||
context.Request.Headers["X-Tenant-Id"] = "tenant-123";
|
||||
|
||||
var routeInfo = new RouteInfo
|
||||
{
|
||||
ClusterId = "order-cluster",
|
||||
IsGlobal = false
|
||||
};
|
||||
|
||||
_mockRouteCache
|
||||
.Setup(r => r.GetRoute("tenant-123", "order-service"))
|
||||
.Returns(routeInfo);
|
||||
|
||||
// Act
|
||||
await _sut.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
context.Items["DynamicClusterId"].Should().Be("order-cluster");
|
||||
_mockNext.Verify(n => n(context), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_WithNoMatchingRoute_CallsNextWithoutClusterId()
|
||||
{
|
||||
// Arrange
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/unknown-service/data";
|
||||
context.Request.Headers["X-Tenant-Id"] = "tenant-123";
|
||||
|
||||
_mockRouteCache
|
||||
.Setup(r => r.GetRoute("tenant-123", "unknown-service"))
|
||||
.Returns((RouteInfo?)null);
|
||||
|
||||
// Act
|
||||
await _sut.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
context.Items.ContainsKey("DynamicClusterId").Should().BeFalse();
|
||||
_mockNext.Verify(n => n(context), Times.Once);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 控制器测试示例
|
||||
|
||||
```csharp
|
||||
// GatewayConfigControllerTests.cs
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
public class GatewayConfigControllerTests
|
||||
{
|
||||
private readonly Mock<IDbContextFactory<GatewayDbContext>> _mockDbFactory;
|
||||
private readonly Mock<DatabaseRouteConfigProvider> _mockRouteProvider;
|
||||
private readonly Mock<DatabaseClusterConfigProvider> _mockClusterProvider;
|
||||
private readonly Mock<IRouteCache> _mockRouteCache;
|
||||
private readonly GatewayConfigController _sut;
|
||||
|
||||
public GatewayConfigControllerTests()
|
||||
{
|
||||
_mockDbFactory = new Mock<IDbContextFactory<GatewayDbContext>>();
|
||||
_mockRouteProvider = new Mock<DatabaseRouteConfigProvider>();
|
||||
_mockClusterProvider = new Mock<DatabaseClusterConfigProvider>();
|
||||
_mockRouteCache = new Mock<IRouteCache>();
|
||||
|
||||
_sut = new GatewayConfigController(
|
||||
_mockDbFactory.Object,
|
||||
_mockRouteProvider.Object,
|
||||
_mockClusterProvider.Object,
|
||||
_mockRouteCache.Object
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTenants_ShouldReturnPaginatedList()
|
||||
{
|
||||
// Arrange
|
||||
var tenants = new List<GwTenant>
|
||||
{
|
||||
new() { Id = 1, TenantCode = "tenant1", TenantName = "Tenant 1" },
|
||||
new() { Id = 2, TenantCode = "tenant2", TenantName = "Tenant 2" }
|
||||
};
|
||||
|
||||
// 设置模拟 DbContext...
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetTenants(page: 1, pageSize: 10);
|
||||
|
||||
// Assert
|
||||
var okResult = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||
var response = okResult.Value.Should().BeAnonymousType();
|
||||
response.Property("total").Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTenant_WithValidData_ReturnsCreatedTenant()
|
||||
{
|
||||
// Arrange
|
||||
var dto = new GatewayConfigController.CreateTenantDto
|
||||
{
|
||||
TenantCode = "new-tenant",
|
||||
TenantName = "New Tenant"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _sut.CreateTenant(dto);
|
||||
|
||||
// Assert
|
||||
var okResult = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||
okResult.Value.Should().BeAssignableTo<GwTenant>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTenant_WithNonexistentId_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
// 设置模拟返回 null
|
||||
|
||||
// Act
|
||||
var result = await _sut.DeleteTenant(999);
|
||||
|
||||
// Assert
|
||||
result.Should().BeOfType<NotFoundResult>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Mock 模式
|
||||
|
||||
### 4.1 接口 Mock
|
||||
|
||||
```csharp
|
||||
// 使用 Moq 模拟接口
|
||||
public class RouteCacheTests
|
||||
{
|
||||
private readonly Mock<IRouteCache> _mockRouteCache;
|
||||
|
||||
public RouteCacheTests()
|
||||
{
|
||||
_mockRouteCache = new Mock<IRouteCache>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestMethod()
|
||||
{
|
||||
// 设置返回值
|
||||
_mockRouteCache
|
||||
.Setup(r => r.GetRoute("tenant1", "service1"))
|
||||
.Returns(new RouteInfo { ClusterId = "cluster1" });
|
||||
|
||||
// 设置异步方法
|
||||
_mockRouteCache
|
||||
.Setup(r => r.InitializeAsync())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// 验证调用
|
||||
_mockRouteCache.Verify(r => r.GetRoute(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 DbContext Mock
|
||||
|
||||
```csharp
|
||||
// 使用 In-Memory 数据库进行测试
|
||||
public class TestDatabaseFixture
|
||||
{
|
||||
public GatewayDbContext CreateContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<GatewayDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
var context = new GatewayDbContext(options);
|
||||
|
||||
// 种子数据
|
||||
context.Tenants.Add(new GwTenant { Id = 1, TenantCode = "test-tenant" });
|
||||
context.TenantRoutes.Add(new GwTenantRoute
|
||||
{
|
||||
Id = 1,
|
||||
ServiceName = "test-service",
|
||||
ClusterId = "test-cluster"
|
||||
});
|
||||
context.SaveChanges();
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
public class GatewayDbContextTests : IClassFixture<TestDatabaseFixture>
|
||||
{
|
||||
private readonly TestDatabaseFixture _fixture;
|
||||
|
||||
public GatewayDbContextTests(TestDatabaseFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveChangesAsync_ShouldNotifyConfigChange()
|
||||
{
|
||||
// Arrange
|
||||
await using var context = _fixture.CreateContext();
|
||||
|
||||
// Act
|
||||
var route = new GwTenantRoute { ServiceName = "new-service", ClusterId = "new-cluster" };
|
||||
context.TenantRoutes.Add(route);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Assert
|
||||
// 验证通知行为(如果需要)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Redis Mock
|
||||
|
||||
```csharp
|
||||
// 使用 Moq 模拟 Redis
|
||||
public class RedisConnectionManagerTests
|
||||
{
|
||||
private readonly Mock<IConnectionMultiplexer> _mockRedis;
|
||||
private readonly Mock<IDatabase> _mockDatabase;
|
||||
|
||||
public RedisConnectionManagerTests()
|
||||
{
|
||||
_mockRedis = new Mock<IConnectionMultiplexer>();
|
||||
_mockDatabase = new Mock<IDatabase>();
|
||||
_mockRedis.Setup(r => r.GetDatabase(It.IsAny<int>(), It.IsAny<object>()))
|
||||
.Returns(_mockDatabase.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireLockAsync_WhenLockAvailable_ReturnsDisposable()
|
||||
{
|
||||
// Arrange
|
||||
_mockDatabase
|
||||
.Setup(d => d.StringSetAsync(
|
||||
It.IsAny<RedisKey>(),
|
||||
It.IsAny<RedisValue>(),
|
||||
It.IsAny<TimeSpan?>(),
|
||||
It.IsAny<When>(),
|
||||
It.IsAny<CommandFlags>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act & Assert
|
||||
// 测试逻辑...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 集成测试模式
|
||||
|
||||
### 5.1 WebApplicationFactory 模式
|
||||
|
||||
```csharp
|
||||
// 使用 WebApplicationFactory 进行 API 集成测试
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
public class GatewayIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public GatewayIntegrationTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// 替换真实服务为测试替身
|
||||
services.RemoveAll<IDbContextFactory<GatewayDbContext>>();
|
||||
services.AddDbContextFactory<GatewayDbContext>(options =>
|
||||
options.UseInMemoryDatabase("TestDb"));
|
||||
});
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHealth_ReturnsHealthy()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/health");
|
||||
|
||||
// Assert
|
||||
response.Should().BeSuccessful();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("healthy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTenants_ReturnsPaginatedList()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/gateway/tenants?page=1&pageSize=10");
|
||||
|
||||
// Assert
|
||||
response.Should().BeSuccessful();
|
||||
// 进一步验证响应内容...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Testcontainers 模式
|
||||
|
||||
```csharp
|
||||
// 使用 Testcontainers 进行真实数据库集成测试
|
||||
using Testcontainers.PostgreSql;
|
||||
using Testcontainers.Redis;
|
||||
|
||||
public class DatabaseIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly PostgreSqlContainer _postgresContainer;
|
||||
private readonly RedisContainer _redisContainer;
|
||||
|
||||
public DatabaseIntegrationTests()
|
||||
{
|
||||
_postgresContainer = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:15-alpine")
|
||||
.WithDatabase("test_gateway")
|
||||
.WithUsername("test")
|
||||
.WithPassword("test")
|
||||
.Build();
|
||||
|
||||
_redisContainer = new RedisBuilder()
|
||||
.WithImage("redis:7-alpine")
|
||||
.Build();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _postgresContainer.StartAsync();
|
||||
await _redisContainer.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _postgresContainer.DisposeAsync();
|
||||
await _redisContainer.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_CreateTenantAndRoute_RouteShouldWork()
|
||||
{
|
||||
// Arrange
|
||||
var connectionString = _postgresContainer.GetConnectionString();
|
||||
|
||||
// 使用真实连接进行端到端测试...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试覆盖率
|
||||
|
||||
### 6.1 当前状态
|
||||
|
||||
项目当前无测试覆盖率数据。
|
||||
|
||||
### 6.2 推荐覆盖率目标
|
||||
|
||||
| 层级 | 目标覆盖率 | 说明 |
|
||||
|------|-----------|------|
|
||||
| Services | 80%+ | 核心业务逻辑,必须高覆盖 |
|
||||
| Middleware | 75%+ | 关键请求处理逻辑 |
|
||||
| Controllers | 70%+ | API 端点行为验证 |
|
||||
| Config | 60%+ | 配置加载和验证 |
|
||||
| Models | 30%+ | 简单 POCO 类,低优先级 |
|
||||
|
||||
### 6.3 配置覆盖率收集
|
||||
|
||||
```xml
|
||||
<!-- 添加到 .csproj 文件 -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
```bash
|
||||
# 运行测试并收集覆盖率
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
|
||||
# 生成覆盖率报告
|
||||
dotnet tool install -g dotnet-reportgenerator-globaltool
|
||||
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage-report"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 如何运行测试
|
||||
|
||||
### 7.1 运行所有测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
dotnet test
|
||||
|
||||
# 运行特定项目
|
||||
dotnet test tests/YarpGateway.UnitTests
|
||||
|
||||
# 运行特定测试类
|
||||
dotnet test --filter "FullyQualifiedName~RouteCacheTests"
|
||||
|
||||
# 运行特定测试方法
|
||||
dotnet test --filter "FullyQualifiedName~RouteCacheTests.InitializeAsync_ShouldLoadRoutesFromDatabase"
|
||||
```
|
||||
|
||||
### 7.2 运行测试类别
|
||||
|
||||
```csharp
|
||||
// 定义测试类别
|
||||
[Trait("Category", "Unit")]
|
||||
public class RouteCacheTests { }
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public class GatewayIntegrationTests { }
|
||||
```
|
||||
|
||||
```bash
|
||||
# 只运行单元测试
|
||||
dotnet test --filter "Category=Unit"
|
||||
|
||||
# 排除集成测试
|
||||
dotnet test --filter "Category!=Integration"
|
||||
```
|
||||
|
||||
### 7.3 CI/CD 配置示例
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_DB: test_gateway
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage"
|
||||
env:
|
||||
ConnectionStrings__DefaultConnection: "Host=localhost;Database=test_gateway;Username=test;Password=test"
|
||||
Redis__ConnectionString: "localhost:6379"
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./tests/**/coverage.cobertura.xml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试最佳实践
|
||||
|
||||
### 8.1 AAA 模式
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Method_Scenario_ExpectedResult()
|
||||
{
|
||||
// Arrange - 准备测试数据和环境
|
||||
var input = "test-data";
|
||||
|
||||
// Act - 执行被测试的方法
|
||||
var result = await _sut.MethodAsync(input);
|
||||
|
||||
// Assert - 验证结果
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 单一职责
|
||||
|
||||
```csharp
|
||||
// ✅ 好:每个测试只验证一个行为
|
||||
[Fact]
|
||||
public async Task CreateTenant_WithValidData_ReturnsCreatedTenant() { }
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTenant_WithDuplicateCode_ReturnsBadRequest() { }
|
||||
|
||||
// ❌ 差:一个测试验证多个行为
|
||||
[Fact]
|
||||
public async Task CreateTenant_TestsAllScenarios() { }
|
||||
```
|
||||
|
||||
### 8.3 测试隔离
|
||||
|
||||
```csharp
|
||||
public class RouteCacheTests
|
||||
{
|
||||
// 每个测试使用独立实例
|
||||
private readonly RouteCache _sut;
|
||||
|
||||
public RouteCacheTests()
|
||||
{
|
||||
// 在构造函数中初始化,确保每个测试独立
|
||||
_sut = new RouteCache(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 避免实现细节测试
|
||||
|
||||
```csharp
|
||||
// ✅ 好:测试行为而非实现
|
||||
[Fact]
|
||||
public async Task GetRoute_ReturnsCorrectRoute() { }
|
||||
|
||||
// ❌ 差:测试内部实现细节
|
||||
[Fact]
|
||||
public void InternalDictionary_ContainsCorrectKey() { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 总结
|
||||
|
||||
### 当前状态
|
||||
- ❌ 无测试项目
|
||||
- ❌ 无测试框架依赖
|
||||
- ❌ 无测试覆盖率
|
||||
- ❌ 无 CI/CD 测试配置
|
||||
|
||||
### 建议行动计划
|
||||
|
||||
1. **创建测试项目**
|
||||
```bash
|
||||
dotnet new xunit -n YarpGateway.UnitTests -o tests/YarpGateway.UnitTests
|
||||
dotnet new xunit -n YarpGateway.IntegrationTests -o tests/YarpGateway.IntegrationTests
|
||||
```
|
||||
|
||||
2. **添加测试依赖**
|
||||
```bash
|
||||
dotnet add package Moq
|
||||
dotnet add package FluentAssertions
|
||||
dotnet add package coverlet.collector
|
||||
```
|
||||
|
||||
3. **优先测试核心服务**
|
||||
- `RouteCache` - 路由缓存核心逻辑
|
||||
- `RedisConnectionManager` - Redis 连接和分布式锁
|
||||
- `TenantRoutingMiddleware` - 租户路由中间件
|
||||
|
||||
4. **建立 CI/CD 测试流程**
|
||||
- 每次提交运行单元测试
|
||||
- 每次合并运行集成测试
|
||||
- 生成覆盖率报告
|
||||
|
||||
通过建立完善的测试体系,可以显著提高代码质量和项目可维护性。
|
||||
@ -1,180 +0,0 @@
|
||||
# 🧪 YARP 网关测试覆盖计划
|
||||
|
||||
> 分析日期:2026-02-28
|
||||
> 当前状态:**无任何测试代码**
|
||||
|
||||
---
|
||||
|
||||
## 测试项目结构
|
||||
|
||||
```
|
||||
tests/
|
||||
└── YarpGateway.Tests/
|
||||
├── YarpGateway.Tests.csproj
|
||||
├── Unit/
|
||||
│ ├── Middleware/
|
||||
│ │ ├── JwtTransformMiddlewareTests.cs
|
||||
│ │ └── TenantRoutingMiddlewareTests.cs
|
||||
│ ├── Services/
|
||||
│ │ ├── RouteCacheTests.cs
|
||||
│ │ ├── RedisConnectionManagerTests.cs
|
||||
│ │ └── PgSqlConfigChangeListenerTests.cs
|
||||
│ ├── LoadBalancing/
|
||||
│ │ └── DistributedWeightedRoundRobinPolicyTests.cs
|
||||
│ └── Config/
|
||||
│ ├── DatabaseRouteConfigProviderTests.cs
|
||||
│ └── DatabaseClusterConfigProviderTests.cs
|
||||
├── Integration/
|
||||
│ ├── Controllers/
|
||||
│ │ ├── GatewayConfigControllerTests.cs
|
||||
│ │ └── PendingServicesControllerTests.cs
|
||||
│ └── Middleware/
|
||||
│ └── MiddlewarePipelineTests.cs
|
||||
└── TestHelpers/
|
||||
├── MockDbContext.cs
|
||||
├── MockRedis.cs
|
||||
└── TestFixtures.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P0 - 必须覆盖(核心安全)
|
||||
|
||||
### JwtTransformMiddlewareTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldValidateJwtSignature` | 应验证 JWT 签名 | 有效签名的 JWT | 解析成功,Claims 正确 | `IOptions<JwtConfig>` |
|
||||
| `ShouldRejectInvalidToken` | 应拒绝无效 Token | 伪造/过期 JWT | 返回 401 或跳过处理 | - |
|
||||
| `ShouldExtractTenantClaim` | 应正确提取租户 ID | 含 tenant claim 的 JWT | X-Tenant-Id header 设置正确 | - |
|
||||
| `ShouldHandleMissingToken` | 应处理无 Token 请求 | 无 Authorization header | 继续处理(不设置 headers) | - |
|
||||
| `ShouldHandleMalformedToken` | 应处理格式错误 Token | 无效 JWT 格式 | 记录警告,继续处理 | - |
|
||||
|
||||
### TenantRoutingMiddlewareTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldValidateTenantIdAgainstJwt` | 应验证 header 与 JWT 一致 | X-Tenant-Id ≠ JWT tenant | 返回 403 Forbidden | `IRouteCache` |
|
||||
| `ShouldExtractServiceNameFromPath` | 应正确解析服务名 | `/api/user-service/users` | serviceName = "user-service" | - |
|
||||
| `ShouldFindRouteInCache` | 应从缓存找到路由 | 有效租户+服务名 | 设置正确的 clusterId | `IRouteCache` |
|
||||
| `ShouldHandleRouteNotFound` | 应处理路由未找到 | 不存在的服务名 | 记录警告,继续处理 | - |
|
||||
| `ShouldPrioritizeTenantRouteOverGlobal` | 租户路由优先于全局 | 同时存在两种路由 | 使用租户路由 | - |
|
||||
|
||||
---
|
||||
|
||||
## P1 - 重要覆盖(核心业务)
|
||||
|
||||
### RouteCacheTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldLoadGlobalRoutes` | 应加载全局路由 | 全局路由数据 | `_globalRoutes` 填充 | `IDbContextFactory<GatewayDbContext>` |
|
||||
| `ShouldLoadTenantRoutes` | 应加载租户路由 | 租户路由数据 | `_tenantRoutes` 填充 | - |
|
||||
| `ShouldReturnCorrectRoute` | 应返回正确路由 | 查询请求 | 正确的 `RouteInfo` | - |
|
||||
| `ShouldReturnNullForMissingRoute` | 不存在路由返回 null | 不存在的服务名 | `null` | - |
|
||||
| `ShouldHandleConcurrentReads` | 并发读取应安全 | 多线程读取 | 无异常,数据一致 | - |
|
||||
| `ShouldReloadCorrectly` | 应正确重载 | Reload 调用 | 旧数据清除,新数据加载 | - |
|
||||
|
||||
### RedisConnectionManagerTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldAcquireLock` | 应获取分布式锁 | 有效 key | 锁获取成功 | `IConnectionMultiplexer` |
|
||||
| `ShouldReleaseLockCorrectly` | 应正确释放锁 | 已获取的锁 | 锁释放成功 | - |
|
||||
| `ShouldNotReleaseOthersLock` | 不应释放他人锁 | 其他实例的锁 | 释放失败(安全) | - |
|
||||
| `ShouldHandleConnectionFailure` | 应处理连接失败 | Redis 不可用 | 记录错误,返回失败 | - |
|
||||
| `ShouldExecuteInLock` | 应在锁内执行操作 | 操作委托 | 操作执行,锁正确释放 | - |
|
||||
|
||||
### DistributedWeightedRoundRobinPolicyTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldSelectByWeight` | 应按权重选择 | 权重 [3, 1, 1] | 约 60% 选第一个 | `IConnectionMultiplexer` |
|
||||
| `ShouldFallbackOnLockFailure` | 锁失败应降级 | Redis 不可用 | 降级选择第一个可用 | - |
|
||||
| `ShouldReturnNullWhenNoDestinations` | 无目标返回 null | 空目标列表 | `null` | - |
|
||||
| `ShouldPersistStateToRedis` | 状态应持久化到 Redis | 多次选择 | 状态存储正确 | - |
|
||||
| `ShouldExpireStateAfterTTL` | 状态应在 TTL 后过期 | 1 小时后 | 状态重新初始化 | - |
|
||||
|
||||
### PgSqlConfigChangeListenerTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldListenForNotifications` | 应监听通知 | NOTIFY 事件 | 触发重载 | `NpgsqlConnection` |
|
||||
| `ShouldFallbackToPolling` | 应回退到轮询 | 通知失败 | 定时轮询检测 | - |
|
||||
| `ShouldReconnectOnFailure` | 失败应重连 | 连接断开 | 自动重连 | - |
|
||||
| `ShouldDetectVersionChange` | 应检测版本变化 | 版本号增加 | 触发重载 | - |
|
||||
|
||||
---
|
||||
|
||||
## P2 - 推荐覆盖(业务逻辑)
|
||||
|
||||
### GatewayConfigControllerTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldCreateTenant` | 应创建租户 | 有效 DTO | 201 Created | `IDbContextFactory` |
|
||||
| `ShouldRejectDuplicateTenant` | 应拒绝重复租户 | 已存在的 TenantCode | 400 BadRequest | - |
|
||||
| `ShouldCreateRoute` | 应创建路由 | 有效 DTO | 201 Created | - |
|
||||
| `ShouldDeleteTenant` | 应删除租户 | 有效 ID | 204 NoContent | - |
|
||||
| `ShouldReturn404ForMissingTenant` | 不存在租户返回 404 | 无效 ID | 404 NotFound | - |
|
||||
| `ShouldReloadConfig` | 应重载配置 | POST /config/reload | 200 OK | `IRouteCache` |
|
||||
|
||||
### PendingServicesControllerTests
|
||||
|
||||
| 测试用例 | 描述 | 输入 | 预期输出 | Mock 需求 |
|
||||
|---------|------|------|----------|-----------|
|
||||
| `ShouldListPendingServices` | 应列出待处理服务 | GET 请求 | 待处理服务列表 | `IDbContextFactory` |
|
||||
| `ShouldAssignService` | 应分配服务 | 有效请求 | 服务实例创建 | - |
|
||||
| `ShouldRejectInvalidCluster` | 应拒绝无效集群 | 不存在的 ClusterId | 400 BadRequest | - |
|
||||
| `ShouldRejectService` | 应拒绝服务 | reject 请求 | 状态更新为 Rejected | - |
|
||||
|
||||
---
|
||||
|
||||
## 测试依赖
|
||||
|
||||
```xml
|
||||
<!-- YarpGateway.Tests.csproj -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.7.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
|
||||
<PackageReference Include="Testcontainers.Redis" Version="3.7.0" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="3.7.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行测试命令
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
dotnet test
|
||||
|
||||
# 运行特定测试类
|
||||
dotnet test --filter "FullyQualifiedName~JwtTransformMiddlewareTests"
|
||||
|
||||
# 生成覆盖率报告
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 覆盖率目标
|
||||
|
||||
| 组件 | 目标覆盖率 | 优先级 |
|
||||
|------|-----------|--------|
|
||||
| JwtTransformMiddleware | 90% | P0 |
|
||||
| TenantRoutingMiddleware | 85% | P0 |
|
||||
| RouteCache | 80% | P1 |
|
||||
| DistributedWeightedRoundRobinPolicy | 80% | P1 |
|
||||
| Controllers | 70% | P2 |
|
||||
| 整体项目 | 75% | - |
|
||||
|
||||
---
|
||||
|
||||
*测试计划由分析生成,建议按优先级逐步实现。*
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"mode": "yolo",
|
||||
"depth": "standard",
|
||||
"parallelization": true,
|
||||
"commit_docs": true,
|
||||
"model_profile": "balanced",
|
||||
"workflow": {
|
||||
"research": false,
|
||||
"plan_check": false,
|
||||
"verifier": false,
|
||||
"auto_advance": true
|
||||
}
|
||||
}
|
||||
@ -1,712 +0,0 @@
|
||||
# 网关插件系统技术方案
|
||||
|
||||
## 一、概述
|
||||
|
||||
本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。
|
||||
|
||||
---
|
||||
|
||||
## 二、整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ fengling-console │ (运维后端 - Backend)
|
||||
│ web 前端 │
|
||||
└─────────┬───────────┘
|
||||
│ HTTP API
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ fengling-console │ (运维服务端)
|
||||
│ │
|
||||
│ - 路由管理 API │ ───▶ 数据库持久化
|
||||
│ - 集群管理 API │ ───▶ Redis Pub/Sub (发布事件)
|
||||
│ - 插件管理 API │
|
||||
└─────────┬───────────┘
|
||||
▲
|
||||
│ 事件订阅
|
||||
│
|
||||
┌─────────┴───────────┐
|
||||
│ fengling-gateway │ (YARP 网关多实例)
|
||||
│ - YARP 代理 │
|
||||
│ - 插件执行 │
|
||||
│ - 事件监听 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 项目职责
|
||||
|
||||
| 项目 | 职责 |
|
||||
|------|------|
|
||||
| **fengling-gateway** | 纯 YARP 代理 + 事件订阅 + 插件执行 |
|
||||
| **fengling-console** | 运维 API + 配置持久化 + 事件发布 |
|
||||
| **fengling-console-web** | 前端 UI (Monaco Editor) |
|
||||
|
||||
---
|
||||
|
||||
## 三、Web UI 管理界面
|
||||
|
||||
### 3.1 技术选型
|
||||
|
||||
| 项目 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 前端框架 | React/Vue | 独立前端项目 |
|
||||
| 编辑器 | Monaco Editor | VS Code 同款,体验一致 |
|
||||
| 路由 | `/gateway` | 运维平台内统一路由 |
|
||||
|
||||
### 3.2 功能模块
|
||||
|
||||
```
|
||||
/gateway
|
||||
├── 路由管理 (Routes)
|
||||
│ ├── 列表/搜索
|
||||
│ ├── 创建/编辑/删除
|
||||
│ └── 路由规则配置
|
||||
├── 集群管理 (Clusters)
|
||||
│ ├── 上下游服务列表
|
||||
│ ├── 实例管理
|
||||
│ └── 健康状态
|
||||
├── 插件管理 (Plugins)
|
||||
│ ├── 已加载插件列表
|
||||
│ ├── 上传 DLL
|
||||
│ └── 在线编写 C# 代码
|
||||
└── 监控统计
|
||||
├── QPS/延迟
|
||||
└── 流量图表
|
||||
```
|
||||
|
||||
## 一、概述
|
||||
|
||||
本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。
|
||||
|
||||
---
|
||||
|
||||
## 二、Web UI 管理界面
|
||||
|
||||
### 2.1 技术选型
|
||||
|
||||
| 项目 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 框架 | Razor Pages | 嵌入主应用,单项目部署 |
|
||||
| 路由 | `/gateway/ui` | 参考 SwaggerUI 风格 |
|
||||
| 编辑器 | Monaco Editor | VS Code 同款,体验一致 |
|
||||
|
||||
### 2.2 功能模块
|
||||
|
||||
```
|
||||
/gateway/ui
|
||||
├── 路由管理 (Routes)
|
||||
│ ├── 列表/搜索
|
||||
│ ├── 创建/编辑/删除
|
||||
│ └── 路由规则配置
|
||||
├── 集群管理 (Clusters)
|
||||
│ ├── 上下游服务列表
|
||||
│ ├── 实例管理
|
||||
│ └── 健康状态
|
||||
├── 插件管理 (Plugins)
|
||||
│ ├── 已加载插件列表
|
||||
│ ├── 上传 DLL
|
||||
│ └── 在线编写 C# 代码
|
||||
└── 监控统计
|
||||
├── QPS/延迟
|
||||
└── 流量图表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、插件系统架构
|
||||
|
||||
### 3.1 插件类型定义
|
||||
|
||||
```csharp
|
||||
namespace Fengling.Gateway.Plugin.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 插件基础接口
|
||||
/// </summary>
|
||||
public interface IGatewayPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
string Version { get; }
|
||||
string? Description { get; }
|
||||
|
||||
Task OnLoadAsync();
|
||||
Task OnUnloadAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求处理插件
|
||||
/// </summary>
|
||||
public interface IRequestPlugin : IGatewayPlugin
|
||||
{
|
||||
/// <summary>请求到达网关前</summary>
|
||||
Task<HttpContext?> OnRequestAsync(HttpContext context);
|
||||
|
||||
/// <summary>路由决策后</summary>
|
||||
Task<HttpContext?> OnRouteMatchedAsync(HttpContext context, RouteConfig route);
|
||||
|
||||
/// <summary>转发到后端前</summary>
|
||||
Task<HttpContext?> OnForwardingAsync(HttpContext context, HttpRequestMessage request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 响应处理插件
|
||||
/// </summary>
|
||||
public interface IResponsePlugin : IGatewayPlugin
|
||||
{
|
||||
/// <summary>后端响应后</summary>
|
||||
Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response);
|
||||
|
||||
/// <summary>返回客户端前</summary>
|
||||
Task OnResponseFinalizingAsync(HttpContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 路由转换插件
|
||||
/// </summary>
|
||||
public interface IRouteTransformPlugin : IGatewayPlugin
|
||||
{
|
||||
Task<RouteConfig> TransformRouteAsync(RouteConfig original, HttpContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 负载均衡插件
|
||||
/// </summary>
|
||||
public interface ILoadBalancePlugin : IGatewayPlugin
|
||||
{
|
||||
Task<Destination> SelectDestinationAsync(
|
||||
IReadOnlyList<Destination> destinations,
|
||||
HttpContext context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 插件阶段枚举
|
||||
|
||||
```csharp
|
||||
public enum PipelineStage
|
||||
{
|
||||
None = 0,
|
||||
OnRequest = 1, // 请求到达网关前
|
||||
OnRoute = 2, // 路由决策时
|
||||
OnRequestBackend = 3, // 转发到后端前
|
||||
OnResponseBackend = 4, // 后端响应后
|
||||
OnResponse = 5 // 返回给客户端前
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、核心模块设计
|
||||
|
||||
### 4.1 依赖管理(A方案)
|
||||
|
||||
#### 简单场景:直接使用网关已有程序集
|
||||
|
||||
```csharp
|
||||
// API 暴露网关程序集
|
||||
[ApiController]
|
||||
public class AssembliesController : ControllerBase
|
||||
{
|
||||
[HttpGet("available")]
|
||||
public List<AssemblyInfo> GetAvailableAssemblies()
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
|
||||
.Select(a => new AssemblyInfo
|
||||
{
|
||||
Name = a.GetName().Name,
|
||||
Version = a.GetName().Version?.ToString(),
|
||||
Location = a.Location
|
||||
})
|
||||
.OrderBy(a => a.Name)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 复杂场景:上传 ZIP 包
|
||||
|
||||
```csharp
|
||||
public class PluginUploadService
|
||||
{
|
||||
private readonly IObjectStorage _storage;
|
||||
private readonly PluginDbContext _db;
|
||||
private readonly string _localPluginPath = "/app/plugins";
|
||||
|
||||
public async Task<PluginPackage> UploadPluginAsync(IFormFile zipFile, string pluginName)
|
||||
{
|
||||
// 1. 上传到对象存储
|
||||
var storageKey = $"plugins/{Guid.NewGuid()}/{zipFile.FileName}";
|
||||
await _storage.UploadAsync(zipFile.OpenReadStream(), storageKey);
|
||||
|
||||
// 2. 保存到数据库
|
||||
var plugin = new PluginPackage
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = pluginName,
|
||||
StorageKey = storageKey,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
Status = PluginStatus.Pending
|
||||
};
|
||||
|
||||
await _db.PluginPackages.AddAsync(plugin);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public async Task ExtractAndLoadAsync(Guid pluginId)
|
||||
{
|
||||
var plugin = await _db.PluginPackages.FindAsync(pluginId);
|
||||
|
||||
// 3. 下载到本地
|
||||
var localDir = Path.Combine(_localPluginPath, plugin.Id.ToString());
|
||||
Directory.CreateDirectory(localDir);
|
||||
|
||||
await _storage.DownloadAsync(plugin.StorageKey, localDir + ".zip");
|
||||
|
||||
// 4. 解压
|
||||
ZipFile.ExtractToDirectory(localDir + ".zip", localDir, overwriteFiles: true);
|
||||
File.Delete(localDir + ".zip");
|
||||
|
||||
// 5. 加载插件
|
||||
await _pluginManager.LoadFromDirectoryAsync(localDir);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ZIP 上传验证
|
||||
|
||||
```csharp
|
||||
public class PluginValidationService
|
||||
{
|
||||
public async Task<PluginValidationResult> ValidateAsync(Stream zipStream)
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
await ExtractZipAsync(zipStream, tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var dlls = Directory.GetFiles(tempDir, "*.dll");
|
||||
var result = new PluginValidationResult();
|
||||
|
||||
foreach (var dll in dlls)
|
||||
{
|
||||
var dllResult = await ValidateDllAsync(dll);
|
||||
result.Assemblies.Add(dllResult);
|
||||
}
|
||||
|
||||
var validPlugins = result.Assemblies
|
||||
.Where(a => a.IsValidPlugin)
|
||||
.ToList();
|
||||
|
||||
if (validPlugins.Count == 0)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.ErrorMessage = "未找到实现 IGatewayPlugin 接口的类";
|
||||
}
|
||||
else
|
||||
{
|
||||
result.IsValid = true;
|
||||
result.ValidPluginTypes = validPlugins
|
||||
.SelectMany(a => a.PluginTypes)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DllValidationResult> ValidateDllAsync(string dllPath)
|
||||
{
|
||||
var result = new DllValidationResult { DllName = Path.GetFileName(dllPath) };
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath);
|
||||
|
||||
var pluginTypes = assembly.GetTypes()
|
||||
.Where(t => typeof(IGatewayPlugin).IsAssignableFrom(t))
|
||||
.Where(t => !t.IsAbstract && !t.IsInterface)
|
||||
.ToList();
|
||||
|
||||
if (pluginTypes.Count > 0)
|
||||
{
|
||||
result.IsValidPlugin = true;
|
||||
result.PluginTypes = pluginTypes.Select(t => new PluginTypeInfo
|
||||
{
|
||||
TypeName = t.FullName,
|
||||
ImplementedInterfaces = t.GetInterfaces().Select(i => i.Name).ToList()
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.ErrorMessage = ex.Message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 插件间通信(B方案)
|
||||
|
||||
采用**方案 3:强类型 + 弱类型混合**
|
||||
|
||||
#### 插件上下文
|
||||
|
||||
```csharp
|
||||
public class GatewayContext
|
||||
{
|
||||
// 预定义常用字段(强类型)
|
||||
public string? UserId { get; set; }
|
||||
public string? TenantId { get; set; }
|
||||
public UserTier Tier { get; set; }
|
||||
public bool IsAuthenticated { get; set; }
|
||||
public DateTime RequestTime { get; set; }
|
||||
|
||||
// 扩展数据(弱类型)
|
||||
public PluginDataBag Data { get; } = new();
|
||||
}
|
||||
|
||||
public class PluginDataBag
|
||||
{
|
||||
private readonly Dictionary<string, object> _data = new();
|
||||
|
||||
public T? Get<T>(string key) => _data.TryGetValue(key, out var v) ? (T)v : default;
|
||||
public void Set<T>(string key, T value) => _data[key] = value!;
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```csharp
|
||||
// 插件 A: 认证
|
||||
public class AuthPlugin : IRequestPlugin
|
||||
{
|
||||
public async Task<HttpContext?> OnRequestAsync(HttpContext context)
|
||||
{
|
||||
var userId = ValidateToken(context);
|
||||
|
||||
if (userId != null)
|
||||
{
|
||||
context.Items["CurrentUserId"] = userId;
|
||||
context.Items["IsAuthenticated"] = true;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
// 插件 B: 审计
|
||||
public class AuditPlugin : IRequestPlugin
|
||||
{
|
||||
public async Task<HttpContext?> OnRequestAsync(HttpContext context)
|
||||
{
|
||||
if (context.Items.TryGetValue("CurrentUserId", out var userId))
|
||||
{
|
||||
await _logger.LogAsync($"User {userId} accessed {context.Request.Path}");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.3 在线代码编辑器(C方案)
|
||||
|
||||
采用 **Monaco Editor + 前端模拟补全**
|
||||
|
||||
> **补充说明**:如需更完整的 C# IntelliSense(如真实代码分析、跳转到定义),可使用 Microsoft 官方的 **roslyn-language-server**(VS Code C# 扩展背后使用的语言服务器)。
|
||||
>
|
||||
> **部署方式**:
|
||||
> ```bash
|
||||
> # 安装
|
||||
> dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer
|
||||
>
|
||||
> # 启动服务
|
||||
> roslyn-languageserver --port 5000
|
||||
> ```
|
||||
>
|
||||
> 前端通过 WebSocket 连接该服务获取完整的语言特性支持。但目前阶段前端模拟补全已足够使用。
|
||||
|
||||
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
|
||||
<div id="editor" style="height: 500px;"></div>
|
||||
|
||||
<script>
|
||||
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' }});
|
||||
|
||||
require(['vs/editor/editor.main'], function() {
|
||||
monaco.editor.create(document.getElementById('editor'), {
|
||||
value: getEditorTemplate(),
|
||||
language: 'csharp',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
scrollBeyondLastLine: false
|
||||
});
|
||||
|
||||
// 注册 C# 补全
|
||||
monaco.languages.registerCompletionItemProvider('csharp', {
|
||||
provideCompletionItems: (model, position) => {
|
||||
const suggestions = [
|
||||
// 常用类型
|
||||
{ label: 'HttpContext', kind: monaco.languages.CompletionItemKind.Class },
|
||||
{ label: 'HttpRequest', kind: monaco.languages.CompletionItemKind.Class },
|
||||
{ label: 'HttpResponse', kind: monaco.languages.CompletionItemKind.Class },
|
||||
|
||||
// 插件接口方法
|
||||
{ label: 'OnRequestAsync', kind: monaco.languages.CompletionItemKind.Method },
|
||||
{ label: 'OnResponseAsync', kind: monaco.languages.CompletionItemKind.Method },
|
||||
{ label: 'TransformRouteAsync', kind: monaco.languages.CompletionItemKind.Method },
|
||||
|
||||
// 常用属性
|
||||
{ label: 'ctx.Request', kind: monaco.languages.CompletionItemKind.Property },
|
||||
{ label: 'ctx.Response', kind: monaco.languages.CompletionItemKind.Property },
|
||||
{ label: 'ctx.Items', kind: monaco.languages.CompletionItemKind.Property },
|
||||
];
|
||||
return { suggestions };
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 编辑器模板
|
||||
|
||||
```csharp
|
||||
// 生成的代码模板
|
||||
$@"
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Fengling.Gateway.Plugin.Abstractions;
|
||||
|
||||
public class {pluginName} : IRequestPlugin
|
||||
{{
|
||||
public string Name => ""{pluginName}"";
|
||||
public string Version => ""1.0.0"";
|
||||
|
||||
public async Task<HttpContext?> OnRequestAsync(HttpContext ctx)
|
||||
{{
|
||||
// 编写你的逻辑
|
||||
{userCode}
|
||||
}}
|
||||
}}
|
||||
";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 插件生命周期管理
|
||||
|
||||
```csharp
|
||||
public class PluginManager
|
||||
{
|
||||
private readonly Dictionary<string, PluginInstance> _plugins = new();
|
||||
private readonly RoslynPluginCompiler _compiler = new();
|
||||
|
||||
public async Task<PluginInstance> LoadPluginAsync(byte[] assemblyBytes, string pluginName)
|
||||
{
|
||||
// 1. 隔离加载程序集
|
||||
var context = new PluginLoadContext(pluginName);
|
||||
var assembly = context.LoadFromStream(new MemoryStream(assemblyBytes));
|
||||
|
||||
// 2. 查找插件入口类型
|
||||
var pluginType = assembly.GetTypes()
|
||||
.FirstOrDefault(t => typeof(IGatewayPlugin).IsAssignableFrom(t));
|
||||
|
||||
if (pluginType == null)
|
||||
{
|
||||
context.Unload();
|
||||
throw new InvalidOperationException("No IGatewayPlugin implementation found");
|
||||
}
|
||||
|
||||
// 3. 创建实例
|
||||
var plugin = (IGatewayPlugin)Activator.CreateInstance(pluginType)!;
|
||||
await plugin.OnLoadAsync();
|
||||
|
||||
// 4. 保存实例
|
||||
var instance = new PluginInstance
|
||||
{
|
||||
Name = pluginName,
|
||||
Assembly = assembly,
|
||||
Context = context,
|
||||
Plugin = plugin,
|
||||
LoadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_plugins[pluginName] = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
public async Task UnloadPluginAsync(string pluginName)
|
||||
{
|
||||
if (!_plugins.TryGetValue(pluginName, out var instance))
|
||||
return;
|
||||
|
||||
await instance.Plugin.OnUnloadAsync();
|
||||
instance.Context.Unload();
|
||||
_plugins.Remove(pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
public class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
public PluginLoadContext(string name) : base(name, isCollectible: true) { }
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.5 插件编译服务
|
||||
|
||||
```csharp
|
||||
public class RoslynPluginCompiler
|
||||
{
|
||||
private readonly IEnumerable<MetadataReference> _defaultReferences;
|
||||
|
||||
public RoslynPluginCompiler()
|
||||
{
|
||||
_defaultReferences = GetDefaultReferences();
|
||||
}
|
||||
|
||||
public CompileResult Compile(string sourceCode, string pluginName, IEnumerable<string> extraAssemblies)
|
||||
{
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
|
||||
|
||||
var references = _defaultReferences.Concat(
|
||||
extraAssemblies.Select(a => MetadataReference.CreateFromFile(a))
|
||||
);
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
assemblyName: $"Plugin_{pluginName}_{Guid.NewGuid():N}",
|
||||
syntaxTrees: new[] { syntaxTree },
|
||||
references: references,
|
||||
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
.WithAllowUnsafe(false)
|
||||
.WithOptimizationLevel(OptimizationLevel.Release));
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
var emitResult = compilation.Emit(ms);
|
||||
|
||||
if (!emitResult.Success)
|
||||
{
|
||||
return CompileResult.Fail(emitResult.Diagnostics);
|
||||
}
|
||||
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
return CompileResult.Success(ms.ToArray());
|
||||
}
|
||||
|
||||
private IEnumerable<MetadataReference> GetDefaultReferences()
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库模型
|
||||
|
||||
```csharp
|
||||
public class PluginPackage
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = "1.0.0";
|
||||
public string? Description { get; set; }
|
||||
public string StorageKey { get; set; } = string.Empty;
|
||||
public PluginStatus Status { get; set; }
|
||||
public DateTime UploadedAt { get; set; }
|
||||
public DateTime? LoadedAt { get; set; }
|
||||
}
|
||||
|
||||
public class PluginPipeline
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid PluginId { get; set; }
|
||||
public PipelineStage Stage { get; set; }
|
||||
public int Order { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
public enum PluginStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Validated = 1,
|
||||
Loaded = 2,
|
||||
Failed = 3,
|
||||
Disabled = 4
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、实施计划
|
||||
|
||||
| 阶段 | 任务 | 优先级 |
|
||||
|------|------|--------|
|
||||
| Phase 1 | 集成 YARP 到现有项目 | 高 |
|
||||
| Phase 2 | 插件基础接口定义 | 高 |
|
||||
| Phase 3 | 插件编译 + 加载框架 | 高 |
|
||||
| Phase 4 | 嵌入式 Razor UI | 中 |
|
||||
| Phase 5 | Monaco Editor 集成 | 中 |
|
||||
| Phase 6 | ZIP 上传验证功能 | 中 |
|
||||
| Phase 7 | 测试与优化 | 低 |
|
||||
|
||||
---
|
||||
|
||||
## 七、NuGet 依赖
|
||||
|
||||
```xml
|
||||
<!-- 核心编译 -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
|
||||
|
||||
<!-- Scripting API (可选) -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.13.0" />
|
||||
|
||||
<!-- 依赖解析 -->
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、总结
|
||||
|
||||
本方案实现了:
|
||||
|
||||
1. **Web UI 管理**:类 SwaggerUI 风格的可视化界面
|
||||
2. **动态编译**:Roslyn 在线编译 C# 代码
|
||||
3. **插件加载**:独立 AssemblyLoadContext,支持热卸载
|
||||
4. **灵活扩展**:支持简单场景(使用已有程序集)和复杂场景(上传 ZIP)
|
||||
5. **流程控制**:插件可分配到 5 个不同阶段执行
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 1.0*
|
||||
*最后更新: 2026-03-01*
|
||||
@ -1,290 +0,0 @@
|
||||
---
|
||||
phase: 06-gateway-plugin-research
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified: []
|
||||
autonomous: true
|
||||
requirements: [PLUG-01, PLUG-02]
|
||||
must_haves:
|
||||
truths:
|
||||
- "插件可以从指定目录动态加载"
|
||||
- "插件在独立的 AssemblyLoadContext 中运行"
|
||||
- "插件可以被卸载并释放内存"
|
||||
artifacts:
|
||||
- path: "src/yarpgateway/Plugins/PluginLoadContext.cs"
|
||||
provides: "ALC 隔离机制"
|
||||
- path: "src/yarpgateway/Plugins/PluginLoader.cs"
|
||||
provides: "插件发现和加载"
|
||||
- path: "src/yarpgateway/Plugins/PluginHost.cs"
|
||||
provides: "插件生命周期管理"
|
||||
- path: "tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs"
|
||||
provides: "加载/卸载验证"
|
||||
key_links:
|
||||
- from: "PluginLoader"
|
||||
to: "PluginLoadContext"
|
||||
via: "实例化并加载程序集"
|
||||
- from: "PluginHost"
|
||||
to: "PluginLoader"
|
||||
via: "协调插件生命周期"
|
||||
---
|
||||
|
||||
# 计划 01:插件加载基础设施
|
||||
|
||||
<objective>
|
||||
实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。
|
||||
|
||||
**目的:** 为网关提供安全的插件加载机制,支持隔离和热重载。
|
||||
**产出:** 可工作的插件加载系统,含单元测试。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/mac/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
|
||||
|
||||
# 现有基础设施
|
||||
@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
|
||||
@src/yarpgateway/Program.cs
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- 现有插件接口 -->
|
||||
```csharp
|
||||
// Fengling.Gateway.Plugin.Abstractions
|
||||
public interface IGatewayPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
string Version { get; }
|
||||
string? Description { get; }
|
||||
Task OnLoadAsync();
|
||||
Task OnUnloadAsync();
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: 创建 PluginLoadContext 隔离机制</name>
|
||||
<files>
|
||||
src/yarpgateway/Plugins/PluginLoadContext.cs,
|
||||
tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: 加载插件程序集到独立 ALC
|
||||
- Test 2: 共享契约程序集使用默认 ALC
|
||||
- Test 3: 卸载 ALC 后内存被回收
|
||||
</behavior>
|
||||
<action>
|
||||
创建可卸载的 AssemblyLoadContext:
|
||||
|
||||
1. 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs`
|
||||
2. 继承 AssemblyLoadContext,设置 isCollectible: true
|
||||
3. 使用 AssemblyDependencyResolver 解析依赖
|
||||
4. 关键:共享 `Fengling.Gateway.Plugin.Abstractions` 到默认 ALC
|
||||
|
||||
```csharp
|
||||
public sealed class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
|
||||
|
||||
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
if (assemblyName.Name == _sharedAssemblyName)
|
||||
return null; // 使用默认 ALC
|
||||
var path = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
return path != null ? LoadFromAssemblyPath(path) : null;
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:先写测试,确保卸载验证通过。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- PluginLoadContext 类存在
|
||||
- 测试验证隔离和卸载
|
||||
- 构建通过
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: 创建 PluginLoader 发现和加载逻辑</name>
|
||||
<files>
|
||||
src/yarpgateway/Plugins/PluginLoader.cs,
|
||||
src/yarpgateway/Plugins/DiscoveredPlugin.cs,
|
||||
tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: 从目录发现插件程序集
|
||||
- Test 2: 加载插件并返回 IGatewayPlugin 实例
|
||||
- Test 3: 处理无效插件(返回 null 或异常)
|
||||
</behavior>
|
||||
<action>
|
||||
创建插件发现和加载器:
|
||||
|
||||
1. 创建 `DiscoveredPlugin.cs` 记录:
|
||||
- Id, Name, Version, AssemblyPath, EntryPoint
|
||||
|
||||
2. 创建 `PluginLoader.cs`:
|
||||
- `DiscoverPlugins(string directory)` - 扫描目录发现插件
|
||||
- `LoadPlugin(DiscoveredPlugin discovered)` - 加载并实例化
|
||||
- 使用 System.Reflection.Metadata 或简单扫描
|
||||
|
||||
```csharp
|
||||
public class PluginLoader
|
||||
{
|
||||
public IEnumerable<DiscoveredPlugin> DiscoverPlugins(string pluginDirectory)
|
||||
{
|
||||
// 扫描 plugin.json 或程序集属性
|
||||
}
|
||||
|
||||
public IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered)
|
||||
{
|
||||
var shadowPath = CreateShadowCopy(discovered.AssemblyPath);
|
||||
var alc = new PluginLoadContext(shadowPath);
|
||||
var assembly = alc.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(discovered.AssemblyPath)));
|
||||
var type = assembly.GetType(discovered.EntryPoint);
|
||||
return Activator.CreateInstance(type) as IGatewayPlugin;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**影子复制**:创建 `CreateShadowCopy` 方法,将插件复制到临时目录。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- PluginLoader 类存在
|
||||
- 可发现和加载插件
|
||||
- 测试通过
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: 创建 PluginHost 生命周期管理</name>
|
||||
<files>
|
||||
src/yarpgateway/Plugins/PluginHost.cs,
|
||||
src/yarpgateway/Plugins/PluginHandle.cs,
|
||||
tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: LoadAllAsync 加载目录下所有插件
|
||||
- Test 2: UnloadAsync 卸载指定插件
|
||||
- Test 3: GetPlugins 返回当前加载的插件
|
||||
- Test 4: 插件卸载后 WeakReference 显示已回收
|
||||
</behavior>
|
||||
<action>
|
||||
创建插件生命周期管理器:
|
||||
|
||||
1. 创建 `PluginHandle.cs`:
|
||||
- 封装 PluginLoadContext 和 IGatewayPlugin
|
||||
- 实现 IAsyncDisposable
|
||||
- 提供 TrackUnloadability() 返回 WeakReference
|
||||
|
||||
```csharp
|
||||
public sealed class PluginHandle : IAsyncDisposable
|
||||
{
|
||||
private readonly PluginLoadContext _alc;
|
||||
private readonly IGatewayPlugin _plugin;
|
||||
private readonly string _shadowDirectory;
|
||||
private bool _disposed;
|
||||
|
||||
public IGatewayPlugin Plugin => _plugin;
|
||||
public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
await _plugin.OnUnloadAsync();
|
||||
_alc.Unload();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 创建 `PluginHost.cs`:
|
||||
- 注入 PluginLoader
|
||||
- 管理插件字典 (name -> PluginHandle)
|
||||
- 提供 LoadAllAsync, UnloadAsync, GetPlugins
|
||||
|
||||
```csharp
|
||||
public class PluginHost
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PluginHandle> _plugins = new();
|
||||
private readonly PluginLoader _loader;
|
||||
|
||||
public async Task LoadAllAsync(string pluginDirectory, CancellationToken ct = default)
|
||||
{
|
||||
var discovered = _loader.DiscoverPlugins(pluginDirectory);
|
||||
foreach (var d in discovered)
|
||||
{
|
||||
var handle = _loader.LoadPlugin(d);
|
||||
if (handle != null)
|
||||
{
|
||||
await handle.Plugin.OnLoadAsync();
|
||||
_plugins[d.Id] = handle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnloadAsync(string pluginId)
|
||||
{
|
||||
if (_plugins.TryRemove(pluginId, out var handle))
|
||||
{
|
||||
await handle.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- PluginHost 和 PluginHandle 类存在
|
||||
- 可加载/卸载插件
|
||||
- 卸载验证测试通过
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误
|
||||
2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过
|
||||
3. 插件加载/卸载功能可用
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 插件可在独立 AssemblyLoadContext 中加载
|
||||
- 插件可通过 WeakReference 验证卸载
|
||||
- 所有单元测试通过
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 `.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md`
|
||||
</output>
|
||||
@ -1,57 +0,0 @@
|
||||
# 计划 006-01 总结:插件加载基础设施
|
||||
|
||||
## 执行状态:✅ 已完成
|
||||
|
||||
## 完成的任务
|
||||
|
||||
### Task 1: PluginLoadContext 隔离机制
|
||||
- ✅ 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs`
|
||||
- ✅ 实现可卸载的 AssemblyLoadContext
|
||||
- ✅ 支持共享契约程序集(使用默认 ALC)
|
||||
- ✅ 创建单元测试 `PluginLoadContextTests.cs`
|
||||
|
||||
### Task 2: PluginLoader 发现和加载逻辑
|
||||
- ✅ 创建 `src/yarpgateway/Plugins/PluginLoader.cs`
|
||||
- ✅ 实现插件发现(从目录扫描 plugin.json)
|
||||
- ✅ 实现影子复制(支持热重载)
|
||||
- ✅ 创建 `DiscoveredPlugin.cs` 和 `PluginMetadata.cs`
|
||||
- ✅ 创建单元测试 `PluginLoaderTests.cs`
|
||||
|
||||
### Task 3: PluginHost 生命周期管理
|
||||
- ✅ 创建 `src/yarpgateway/Plugins/PluginHost.cs`
|
||||
- ✅ 实现 `PluginHandle.cs` 封装 ALC 和插件实例
|
||||
- ✅ 支持加载/卸载/重载插件
|
||||
- ✅ 提供 WeakReference 卸载验证
|
||||
- ✅ 创建单元测试 `PluginHostTests.cs`
|
||||
|
||||
## 实现的文件
|
||||
|
||||
| 文件 | 描述 |
|
||||
|------|------|
|
||||
| `src/yarpgateway/Plugins/PluginLoadContext.cs` | ALC 隔离机制 |
|
||||
| `src/yarpgateway/Plugins/PluginLoader.cs` | 插件发现和加载 |
|
||||
| `src/yarpgateway/Plugins/PluginHost.cs` | 生命周期管理 |
|
||||
| `tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs` | 隔离测试 |
|
||||
| `tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs` | 加载测试 |
|
||||
| `tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs` | 生命周期测试 |
|
||||
|
||||
## 测试结果
|
||||
|
||||
```
|
||||
dotnet test --filter "FullyQualifiedName~Plugin"
|
||||
已通过! - 失败: 0,通过: 15,总计: 15
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
- ✅ 构建通过:`dotnet build src/yarpgateway/YarpGateway.csproj` - 0 错误
|
||||
- ✅ 所有单元测试通过
|
||||
- ✅ 插件可在独立 AssemblyLoadContext 中加载
|
||||
- ✅ 插件可通过 WeakReference 验证卸载
|
||||
- ✅ 支持动态发现和加载插件
|
||||
|
||||
## 后续计划
|
||||
|
||||
阶段 6 还需完成:
|
||||
- PLUG-03:插件隔离与生命周期管理(已实现基础设施)
|
||||
- YARP 集成:将插件集成到 YARP 管道
|
||||
@ -1,366 +0,0 @@
|
||||
---
|
||||
phase: 06-gateway-plugin-research
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: [006-01]
|
||||
files_modified: []
|
||||
autonomous: true
|
||||
requirements: [PLUG-03]
|
||||
must_haves:
|
||||
truths:
|
||||
- "插件通过路由 Metadata 启用"
|
||||
- "请求 Transform 轻量处理请求"
|
||||
- "目标选择在路由匹配后执行"
|
||||
- "插件按配置顺序执行"
|
||||
artifacts:
|
||||
- path: "src/yarpgateway/Plugins/PluginTransformProvider.cs"
|
||||
provides: "YARP Transform 提供者"
|
||||
- path: "src/yarpgateway/Plugins/YarpPluginMiddleware.cs"
|
||||
provides: "插件管道集成"
|
||||
- path: "src/yarpgateway/Plugins/PluginConfigWatcher.cs"
|
||||
provides: "Console DB 通知监听"
|
||||
- path: "tests/YarpGateway.Tests/Unit/Plugins/YarpIntegrationTests.cs"
|
||||
provides: "集成测试"
|
||||
key_links:
|
||||
- from: "PluginTransformProvider"
|
||||
to: "PluginHost"
|
||||
via: "获取已加载插件"
|
||||
- from: "YarpPluginMiddleware"
|
||||
to: "PluginTransformProvider"
|
||||
via: "应用 Transform"
|
||||
- from: "PluginConfigWatcher"
|
||||
to: "PluginHost"
|
||||
via: "触发重载"
|
||||
---
|
||||
|
||||
# 计划 02:YARP 插件集成
|
||||
|
||||
<objective>
|
||||
将插件系统集成到 YARP 反向代理管道,实现 Transform 方式的请求/响应处理,以及通过 Metadata 驱动的插件启用机制。
|
||||
|
||||
**目的:** 实现 PLUG-03 - 插件隔离与生命周期管理,完成网关插件化。
|
||||
**产出:** 可工作的 YARP 插件集成系统,含单元测试。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/mac/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
|
||||
@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
|
||||
@src/yarpgateway/Plugins/PluginHost.cs
|
||||
@src/yarpgateway/Program.cs
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
## 现有插件接口
|
||||
|
||||
```csharp
|
||||
// Fengling.Gateway.Plugin.Abstractions
|
||||
public interface IGatewayPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
string Version { get; }
|
||||
string? Description { get; }
|
||||
Task OnLoadAsync();
|
||||
Task OnUnloadAsync();
|
||||
}
|
||||
|
||||
public interface IRequestPlugin : IGatewayPlugin
|
||||
{
|
||||
Task<HttpContext?> OnRequestAsync(HttpContext context);
|
||||
Task<HttpContext?> OnRouteMatchedAsync(HttpContext context, RouteConfig route);
|
||||
Task<HttpContext?> OnForwardingAsync(HttpContext context, HttpRequestMessage request);
|
||||
}
|
||||
|
||||
public interface IResponsePlugin : IGatewayPlugin
|
||||
{
|
||||
Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response);
|
||||
Task OnResponseFinalizingAsync(HttpContext context);
|
||||
}
|
||||
|
||||
public interface IRouteTransformPlugin : IGatewayPlugin
|
||||
{
|
||||
Task<RouteConfig> TransformRouteAsync(RouteConfig original, HttpContext context);
|
||||
}
|
||||
```
|
||||
|
||||
## YARP Transform 接口
|
||||
|
||||
```csharp
|
||||
// YARP Transform
|
||||
public interface IRequestTransform
|
||||
{
|
||||
Task ApplyAsync(RequestTransformContext context);
|
||||
}
|
||||
|
||||
public interface IResponseTransform
|
||||
{
|
||||
Task ApplyAsync(ResponseTransformContext context);
|
||||
}
|
||||
|
||||
public interface IClusterDestinationsTransform
|
||||
{
|
||||
Task ApplyAsync(ClusterDestinationsContext context);
|
||||
}
|
||||
```
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: 创建 PluginTransformProvider</name>
|
||||
<files>
|
||||
src/yarpgateway/Plugins/PluginTransformProvider.cs,
|
||||
tests/YarpGateway.Tests/Unit/Plugins/PluginTransformProviderTests.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: 从路由 Metadata 发现启用的插件
|
||||
- Test 2: 按 PluginOrder 排序
|
||||
- Test 3: 创建 RequestTransform
|
||||
- Test 4: 创建 ResponseTransform
|
||||
</behavior>
|
||||
<action>
|
||||
创建 YARP Transform 提供者:
|
||||
|
||||
1. 创建 `PluginTransformProvider.cs`:
|
||||
- 实现 `IProxyConfigFilter` 或 `IDynamicRouteConfigProvider`
|
||||
- 扫描路由 Metadata 中的 "Plugins" 键
|
||||
- 从 PluginHost 获取已加载的插件
|
||||
- 按 "PluginOrder" 排序
|
||||
|
||||
2. 核心逻辑:
|
||||
```csharp
|
||||
public class PluginTransformProvider : IProxyConfigFilter
|
||||
{
|
||||
private readonly PluginHost _pluginHost;
|
||||
|
||||
public async Task ApplyTransformAsync(HttpContext context, RouteConfig route)
|
||||
{
|
||||
var pluginIds = route.Metadata?["Plugins"]?.Split(',');
|
||||
if (pluginIds == null) return;
|
||||
|
||||
var order = route.Metadata?["PluginOrder"]?.Split(',') ?? pluginIds;
|
||||
var orderedPlugins = order.Select(id => pluginIds.IndexOf(id))
|
||||
.Select(i => _pluginHost.GetPlugins().ElementAt(i));
|
||||
|
||||
foreach (var plugin in orderedPlugins.OfType<IRequestPlugin>())
|
||||
{
|
||||
await plugin.OnRouteMatchedAsync(context, route);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 创建 Request/Response Transform 类:
|
||||
- `PluginRequestTransform` - 调用 IRequestPlugin
|
||||
- `PluginResponseTransform` - 调用 IResponsePlugin
|
||||
- `PluginRouteTransform` - 调用 IRouteTransformPlugin
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginTransformProviderTests" --no-build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- PluginTransformProvider 存在
|
||||
- Transform 测试通过
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: 创建目标选择器 (DestinationSelector)</name>
|
||||
<files>
|
||||
src/yarpgateway/Plugins/DestinationSelector.cs,
|
||||
tests/YarpGateway.Tests/Unit/Plugins/DestinationSelectorTests.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: OnRouteMatchedAsync 选择目标
|
||||
- Test 2: 根据上下文修改目标列表
|
||||
- Test 3: 特殊租户路由到特殊目标
|
||||
</behavior>
|
||||
<action>
|
||||
创建目标选择器,用于 OnRouteMatchedAsync 阶段:
|
||||
|
||||
1. 创建 `DestinationSelector.cs`:
|
||||
- 实现 IClusterDestinationsTransform
|
||||
- 在路由匹配后、负载均衡前执行
|
||||
- 可以根据 HttpContext 修改可用目标列表
|
||||
|
||||
```csharp
|
||||
public class DestinationSelector : IClusterDestinationsTransform
|
||||
{
|
||||
private readonly PluginHost _pluginHost;
|
||||
|
||||
public async Task ApplyAsync(ClusterDestinationsContext context)
|
||||
{
|
||||
var httpContext = context.HttpContext;
|
||||
|
||||
// 获取路由上启用的插件
|
||||
var routeConfig = httpContext.GetRouteConfig();
|
||||
var pluginIds = routeConfig?.Metadata?["Plugins"]?.Split(',');
|
||||
|
||||
if (pluginIds == null) return;
|
||||
|
||||
foreach (var pluginId in pluginIds)
|
||||
{
|
||||
var plugin = _pluginHost.GetPlugin(pluginId);
|
||||
if (plugin is IRequestPlugin requestPlugin)
|
||||
{
|
||||
// 让插件过滤/修改目标列表
|
||||
var availableDestinations = context.Destinations.ToList();
|
||||
// 插件逻辑可以修改 availableDestinations
|
||||
context.Destinations = availableDestinations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 注册到 YARP:
|
||||
```csharp
|
||||
builder.Services.AddSingleton<IClusterDestinationsTransform, DestinationSelector>();
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~DestinationSelectorTests" --no-build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- DestinationSelector 存在
|
||||
- 目标选择逻辑工作正常
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: 创建 PluginConfigWatcher (Console DB 通知)</name>
|
||||
<files>
|
||||
src/yarpgateway/Plugins/PluginConfigWatcher.cs,
|
||||
tests/YarpGateway.Tests/Unit/Plugins/PluginConfigWatcherTests.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: 监听配置变更通知
|
||||
- Test 2: 触发插件重载
|
||||
- Test 3: 处理通知失败
|
||||
</behavior>
|
||||
<action>
|
||||
创建配置监听器,监听 Console DB 通知:
|
||||
|
||||
1. 创建 `PluginConfigWatcher.cs`:
|
||||
- 实现 `IHostedService` 或使用现有的 `PgSqlConfigChangeListener`
|
||||
- 监听插件配置变更频道(如 `plugin_config_changed`)
|
||||
- 触发 PluginHost 重载
|
||||
|
||||
```csharp
|
||||
public class PluginConfigWatcher : BackgroundService
|
||||
{
|
||||
private readonly PluginHost _pluginHost;
|
||||
private readonly NpgsqlConnection _connection;
|
||||
private const string Channel = "plugin_config_changed";
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await _connection.OpenAsync(stoppingToken);
|
||||
await _connection.ListenAsync(Channel, stoppingToken);
|
||||
|
||||
await foreach (var notification in _connection.Notifications(stoppingToken))
|
||||
{
|
||||
// 解析通知,获取需要重载的插件
|
||||
var payload = JsonSerializer.Deserialize<PluginConfigPayload>(notification.Payload);
|
||||
await _pluginHost.ReloadAsync(payload.PluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 注册到 DI:
|
||||
```csharp
|
||||
builder.Services.AddHostedService<PluginConfigWatcher>();
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginConfigWatcherTests" --no-build</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- PluginConfigWatcher 存在
|
||||
- 监听器正确响应通知
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: 更新 Program.cs 集成插件系统</name>
|
||||
<files>
|
||||
src/yarpgateway/Program.cs
|
||||
</files>
|
||||
<behavior>
|
||||
- Test 1: 插件系统在启动时初始化
|
||||
- Test 2: Transform 被正确应用
|
||||
</behavior>
|
||||
<action>
|
||||
更新 Program.cs 集成插件系统:
|
||||
|
||||
1. 注册插件服务:
|
||||
```csharp
|
||||
// 插件目录
|
||||
var pluginDirectory = configuration.GetValue<string>("Plugin:Directory") ?? "plugins";
|
||||
|
||||
// 插件主机
|
||||
builder.Services.AddSingleton(sp => new PluginHost(pluginDirectory));
|
||||
|
||||
// Transform 提供者
|
||||
builder.Services.AddSingleton<IProxyConfigFilter, PluginTransformProvider>();
|
||||
|
||||
// 目标选择器
|
||||
builder.Services.AddSingleton<IClusterDestinationsTransform, DestinationSelector>();
|
||||
|
||||
// 配置监听器
|
||||
builder.Services.AddHostedService<PluginConfigWatcher>();
|
||||
```
|
||||
|
||||
2. 初始化插件:
|
||||
```csharp
|
||||
var app = builder.Build();
|
||||
|
||||
// 启动时加载插件
|
||||
var pluginHost = app.Services.GetRequiredService<PluginHost>();
|
||||
await pluginHost.LoadAllAsync();
|
||||
```
|
||||
|
||||
3. 配置 YARP 使用 Transform:
|
||||
```csharp
|
||||
builder.Services.AddReverseProxy()
|
||||
.AddTransforms<PluginTransformProvider>();
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build src/yarpgateway/YarpGateway.csproj</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Program.cs 正确集成插件系统
|
||||
- 构建通过
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误
|
||||
2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~Plugin" 通过
|
||||
3. 插件可通过 Metadata 启用
|
||||
4. Transform 正确应用到请求/响应
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 插件通过 YARP Transform 管道处理请求
|
||||
- 目标选择在路由匹配后执行
|
||||
- 插件通过 Metadata 启用
|
||||
- Console DB 通知可触发插件重载
|
||||
- 所有单元测试通过
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 `.planning/phases/006-gateway-plugin-research/006-02-SUMMARY.md`
|
||||
</output>
|
||||
@ -1,284 +0,0 @@
|
||||
# 阶段 6 研究:网关插件技术调研与实现
|
||||
|
||||
**研究日期:** 2026-03-04
|
||||
**状态:** 已完成
|
||||
|
||||
---
|
||||
|
||||
## 1. 现有基础设施分析
|
||||
|
||||
### 1.1 已有插件抽象层
|
||||
|
||||
项目已创建 `Fengling.Gateway.Plugin.Abstractions` 程序集,定义了核心插件接口:
|
||||
|
||||
```csharp
|
||||
// 已定义的接口
|
||||
public interface IGatewayPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
string Version { get; }
|
||||
string? Description { get; }
|
||||
Task OnLoadAsync();
|
||||
Task OnUnloadAsync();
|
||||
}
|
||||
|
||||
public interface IRequestPlugin : IGatewayPlugin { ... }
|
||||
public interface IResponsePlugin : IGatewayPlugin { ... }
|
||||
public interface IRouteTransformPlugin : IGatewayPlugin { ... }
|
||||
public interface ILoadBalancePlugin : IGatewayPlugin { ... }
|
||||
```
|
||||
|
||||
### 1.2 缺失的组件
|
||||
|
||||
- ❌ **插件加载器** - 动态加载程序集的机制
|
||||
- ❌ **插件生命周期管理** - 加载/卸载/热重载
|
||||
- ❌ **插件隔离** - AssemblyLoadContext 隔离
|
||||
- ❌ **YARP 集成** - 将插件集成到 YARP 管道
|
||||
|
||||
---
|
||||
|
||||
## 2. YARP 扩展点
|
||||
|
||||
### 2.1 中间件管道
|
||||
|
||||
YARP 使用 ASP.NET Core 中间件管道,允许自定义注入点:
|
||||
|
||||
```csharp
|
||||
app.MapReverseProxy(proxyPipeline =>
|
||||
{
|
||||
proxyPipeline.Use(async (context, next) =>
|
||||
{
|
||||
// 插件前置执行
|
||||
await pluginFeature.ExecutePreProxyAsync(context);
|
||||
await next();
|
||||
// 插件后置执行
|
||||
await pluginFeature.ExecutePostProxyAsync(context);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2.2 Transform 管道(推荐)
|
||||
|
||||
Transform 是修改请求/响应的推荐方式:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddReverseProxy()
|
||||
.AddTransforms(context =>
|
||||
{
|
||||
var plugins = PluginManager.GetTransformPlugins(context.Route);
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
context.AddRequestTransform(plugin.ApplyAsync);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 路由扩展
|
||||
|
||||
YARP 2.0+ 支持自定义路由元数据,插件可消费:
|
||||
|
||||
```json
|
||||
{
|
||||
"Routes": {
|
||||
"api-route": {
|
||||
"Extensions": {
|
||||
"PluginConfig": {
|
||||
"PluginId": "rate-limiter",
|
||||
"MaxRequests": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. .NET 插件加载最佳实践
|
||||
|
||||
### 3.1 AssemblyLoadContext 隔离
|
||||
|
||||
使用 **可卸载的 AssemblyLoadContext** 配合 **AssemblyDependencyResolver**:
|
||||
|
||||
```csharp
|
||||
public sealed class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
|
||||
|
||||
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
// 共享契约程序集(防止类型身份问题)
|
||||
if (assemblyName.Name == _sharedAssemblyName)
|
||||
return null; // 回退到默认 ALC
|
||||
|
||||
var path = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
return path != null ? LoadFromAssemblyPath(path) : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 影子复制(热重载必需)
|
||||
|
||||
Windows 锁定加载的 DLL,需要影子复制:
|
||||
|
||||
```csharp
|
||||
public static string CreateShadowCopy(string pluginDirectory)
|
||||
{
|
||||
var shadowDir = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"PluginShadows",
|
||||
$"{Path.GetFileName(pluginDirectory)}_{Guid.NewGuid():N}"
|
||||
);
|
||||
CopyDirectory(pluginDirectory, shadowDir);
|
||||
return shadowDir;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 插件句柄模式
|
||||
|
||||
```csharp
|
||||
public sealed class PluginHandle : IAsyncDisposable
|
||||
{
|
||||
private readonly PluginLoadContext _alc;
|
||||
private readonly IGatewayPlugin _plugin;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_plugin is IAsyncDisposable ad) await ad.DisposeAsync();
|
||||
_alc.Unload(); // 计划回收
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 卸载验证
|
||||
|
||||
```csharp
|
||||
public static bool VerifyUnload(WeakReference weakRef, int maxAttempts = 10)
|
||||
{
|
||||
for (var i = 0; i < maxAttempts && weakRef.IsAlive; i++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
}
|
||||
return !weakRef.IsAlive;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 插件隔离模式
|
||||
|
||||
### 4.1 契约边界(最重要的设计决策)
|
||||
|
||||
**规则**:契约必须驻留在 **默认 ALC**,绝不在插件 ALC 中。
|
||||
|
||||
插件项目配置:
|
||||
```xml
|
||||
<ProjectReference Include="..\Plugin.Abstractions\Plugin.Abstractions.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
```
|
||||
|
||||
### 4.2 静态缓存问题
|
||||
|
||||
**问题**:主机单例缓存插件类型会阻止卸载。
|
||||
|
||||
**解决方案**:只使用 DTO 跨越边界,不传递插件类型。
|
||||
|
||||
---
|
||||
|
||||
## 5. 实现架构
|
||||
|
||||
### 5.1 组件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── Fengling.Gateway.Plugin.Abstractions/ # 已存在
|
||||
│ └── IGatewayPlugin.cs
|
||||
├── yarpgateway/
|
||||
│ ├── Plugins/
|
||||
│ │ ├── PluginLoadContext.cs # ALC 隔离
|
||||
│ │ ├── PluginLoader.cs # 加载逻辑
|
||||
│ │ ├── PluginHost.cs # 生命周期管理
|
||||
│ │ └── PluginMiddleware.cs # YARP 集成
|
||||
│ └── Program.cs
|
||||
└── plugins/ # 插件目录
|
||||
└── sample-plugin/
|
||||
└── SamplePlugin.csproj
|
||||
```
|
||||
|
||||
### 5.2 插件目录结构
|
||||
|
||||
```
|
||||
plugins/
|
||||
├── rate-limiter/
|
||||
│ ├── RateLimiterPlugin.dll
|
||||
│ ├── RateLimiterPlugin.deps.json
|
||||
│ └── plugin.json # 元数据
|
||||
└── jwt-transform/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 5.3 插件元数据 (plugin.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "rate-limiter",
|
||||
"name": "Rate Limiter Plugin",
|
||||
"version": "1.0.0",
|
||||
"entryPoint": "RateLimiterPlugin.RateLimiterPlugin",
|
||||
"interfaces": ["IRequestPlugin"],
|
||||
"dependencies": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证架构
|
||||
|
||||
### 6.1 测试策略
|
||||
|
||||
1. **单元测试**:PluginLoadContext 隔离验证
|
||||
2. **集成测试**:插件加载/卸载/热重载
|
||||
3. **性能测试**:插件执行开销
|
||||
|
||||
### 6.2 成功标准验证
|
||||
|
||||
| 标准 | 验证方法 |
|
||||
|------|---------|
|
||||
| 动态加载插件 | 单元测试:从目录加载并执行 |
|
||||
| 插件相互隔离 | 单元测试:异常不传播到其他插件 |
|
||||
| 热加载/卸载 | 集成测试:WeakReference 验证卸载 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 推荐库
|
||||
|
||||
| 用途 | 库 |
|
||||
|------|---|
|
||||
| 网关核心 | Yarp.ReverseProxy 2.3.0+ |
|
||||
| 插件加载 | 自定义 AssemblyLoadContext |
|
||||
| 元数据读取 | System.Reflection.Metadata |
|
||||
| 依赖注入 | Microsoft.Extensions.DependencyInjection |
|
||||
|
||||
---
|
||||
|
||||
## 8. 关键注意事项
|
||||
|
||||
1. **不要在主机单例中缓存插件类型**
|
||||
2. **始终用 WeakReference 测试卸载**
|
||||
3. **Windows 上必须影子复制以支持热重载**
|
||||
4. **使用仅元数据发现避免仅扫描而加载程序集**
|
||||
5. **谨慎处理原生依赖**(它们不能干净卸载)
|
||||
|
||||
---
|
||||
|
||||
*研究完成:2026-03-04*
|
||||
@ -1,71 +0,0 @@
|
||||
# 阶段 2 计划:K8s 健康检查委托
|
||||
|
||||
## 目标
|
||||
|
||||
将 K8s 服务健康监控从网关移除,委托给 fengling-console。网关只专注于请求路由。
|
||||
|
||||
## 需要移除的代码
|
||||
|
||||
### 1. 后台服务
|
||||
- **文件**: `src/yarpgateway/Services/KubernetesPendingSyncService.cs`
|
||||
- **操作**: 删除文件
|
||||
- **影响**: 停止每 30 秒同步 K8s 服务
|
||||
|
||||
### 2. 服务注册
|
||||
- **文件**: `src/yarpgateway/Program.cs`
|
||||
- **行号**: ~118
|
||||
- **代码**: `builder.Services.AddHostedService<KubernetesPendingSyncService>();`
|
||||
- **操作**: 删除该行
|
||||
|
||||
### 3. API 控制器
|
||||
- **文件**: `src/yarpgateway/Controllers/PendingServicesController.cs`
|
||||
- **操作**: 删除文件
|
||||
- **影响**: 移除 `/api/gateway/pending-services/*` API
|
||||
|
||||
### 4. 数据模型
|
||||
- **文件**: `src/yarpgateway/Models/GwPendingServiceDiscovery.cs`
|
||||
- **操作**: 删除文件
|
||||
- **影响**: 移除待处理服务发现实体
|
||||
|
||||
### 5. DbContext
|
||||
- **文件**: `src/yarpgateway/Data/GatewayDbContext.cs`
|
||||
- **操作**: 移除 `DbSet<GwPendingServiceDiscovery>` 属性
|
||||
|
||||
### 6. 迁移文件(可选)
|
||||
- **文件**: `src/yarpgateway/Migrations/20260222134342_AddPendingServiceDiscovery.cs`
|
||||
- **操作**: 保留(数据库已有该表)或删除(如果重新创建数据库)
|
||||
|
||||
## 不需要移除
|
||||
|
||||
| 组件 | 理由 |
|
||||
|------|------|
|
||||
| DatabaseClusterConfigProvider | YARP 集群配置仍然需要 |
|
||||
| PgSqlConfigChangeListener | 配置监听仍然需要 |
|
||||
| 现有健康检查 | YARP 内置被动健康检查 |
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 移除 PendingServicesController.cs
|
||||
2. 移除 KubernetesPendingSyncService.cs
|
||||
3. 移除 GwPendingServiceDiscovery.cs
|
||||
4. 更新 GatewayDbContext.cs
|
||||
5. 更新 Program.cs
|
||||
6. 更新 ROADMAP.md 标记为完成
|
||||
7. 提交代码
|
||||
|
||||
## 风险
|
||||
|
||||
- **数据丢失**: 如果数据库已有 `PendingServiceDiscoveries` 表,删除代码后数据仍然存在但无法访问
|
||||
- **API 变更**: 移除 `/api/gateway/pending-services/*` 端点,需要通知 console 团队
|
||||
|
||||
## 验证
|
||||
|
||||
完成后验证:
|
||||
- `dotnet build` 成功
|
||||
- 无 `KubernetesPendingSyncService` 引用
|
||||
- 无 `PendingServicesController` 引用
|
||||
- 无 `GwPendingServiceDiscovery` 引用
|
||||
|
||||
---
|
||||
|
||||
*计划创建: 2026-03-02*
|
||||
@ -1,39 +0,0 @@
|
||||
# Plan: 升级 Fengling.Platform 包到最新
|
||||
|
||||
## 任务描述
|
||||
升级 fengling-gateway 项目中的 Fengling.Platform.Infrastructure 包到最新版本,并修复现有编译警告。
|
||||
|
||||
## 变更分析
|
||||
|
||||
### fengling-platform 新版本主要变更
|
||||
1. **主键类型变更**:从 `long Id` 改为 `string Id`(Guid)
|
||||
2. **新增 GwCluster 聚合根**:包含内嵌 Destinations 列表
|
||||
3. **GwTenantRoute 扩展**:新增 Match (GwRouteMatch)、Transforms 等字段
|
||||
4. **移除GwServiceInstance**:作为 GwCluster 的内嵌值对象 GwDestination
|
||||
5. **新增值对象**:GwRouteMatch、GwTransform、GwLoadBalancingPolicy、GwHealthCheckConfig、GwSessionAffinityConfig
|
||||
|
||||
### 当前编译警告
|
||||
1. **CS0108**: GatewayDbContext.Tenants 隐藏继承成员 PlatformDbContext.Tenants
|
||||
2. **NU1506**: 重复 PackageVersion 定义
|
||||
3. **NU1507**: 配置了多个包源
|
||||
|
||||
## 任务列表
|
||||
|
||||
### Task 1: 分析并修复 CS0108 警告
|
||||
- **文件**: src/yarpgateway/Data/GatewayDbContext.cs
|
||||
- **操作**: 将 `Tenants` 属性添加 `new` 关键字,或使用不同的名称避免隐藏
|
||||
- **验证**: dotnet build 无 CS0108 警告
|
||||
|
||||
### Task 2: 修复 NU1506 重复包版本警告
|
||||
- **文件**: Directory.Packages.props 或 YarpGateway.csproj
|
||||
- **操作**: 检查并移除重复的 PackageVersion 定义
|
||||
- **验证**: dotnet restore 无 NU1506 警告
|
||||
|
||||
### Task 3: 配置包源映射解决 NU1507
|
||||
- **文件**: NuGet.Config 或 Directory.Build.props
|
||||
- **操作**: 添加包源映射配置
|
||||
- **验证**: dotnet restore 无 NU1507 警告
|
||||
|
||||
## 验证
|
||||
- dotnet build 成功,0 错误
|
||||
- 无 CS0108、NU1506、NU1507 警告
|
||||
@ -1,41 +0,0 @@
|
||||
# Quick Task 001 Summary: 升级 Fengling.Platform 包并修复编译警告
|
||||
|
||||
## 任务概述
|
||||
升级 fengling-gateway 项目中的 Fengling.Platform.Infrastructure 包引用,并修复编译警告。
|
||||
|
||||
## 变更内容
|
||||
|
||||
### 1. 修复 CS0108 警告
|
||||
**文件**: `src/yarpgateway/Data/GatewayDbContext.cs`
|
||||
**问题**: `GatewayDbContext.Tenants` 隐藏了继承的成员 `PlatformDbContext.Tenants`
|
||||
**修复**: 添加 `new` 关键字明确表示新定义
|
||||
```csharp
|
||||
// 修复前
|
||||
public DbSet<GwTenant> Tenants => Set<GwTenant>();
|
||||
// 修复后
|
||||
public new DbSet<GwTenant> Tenants => Set<GwTenant>();
|
||||
```
|
||||
|
||||
### 2. 修复 NU1506 重复 PackageVersion
|
||||
**文件**: `src/yarpgateway/Directory.Packages.props`
|
||||
**问题**: 重复定义了 Microsoft.AspNetCore.Authentication.JwtBearer、Microsoft.EntityFrameworkCore.Design、Microsoft.EntityFrameworkCore
|
||||
**修复**: 移除重复的 PackageVersion 定义
|
||||
|
||||
### 3. 修复 NU1507 多个包源警告
|
||||
**文件**: 根目录创建 `NuGet.Config`
|
||||
**问题**: 配置了多个包源(gitea、nuget.org),需要包源映射
|
||||
**修复**: 添加 packageSourceMapping 配置到 NuGet.Config,并删除子目录中的重复配置
|
||||
|
||||
## 构建结果
|
||||
- ✅ 0 错误
|
||||
- ✅ CS0108 警告已修复
|
||||
- ✅ NU1506 警告已修复
|
||||
- ✅ NU1507 警告已修复
|
||||
- ⚠️ MSB3277 警告(EntityFrameworkCore.Relational 版本冲突)- 来自测试项目依赖,不影响构建
|
||||
|
||||
## 相关文件
|
||||
- `src/yarpgateway/Data/GatewayDbContext.cs` - 添加 new 关键字
|
||||
- `src/yarpgateway/Directory.Packages.props` - 移除重复包版本
|
||||
- `src/yarpgateway/Directory.Build.props` - 添加包源映射
|
||||
- `tests/Directory.Build.props` - 添加包源映射
|
||||
- `NuGet.Config` - 新建包源映射配置
|
||||
29
Dockerfile
29
Dockerfile
@ -1,30 +1,37 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
FROM 192.168.100.120:8418/fengling/dotnet/aspnet:10.0 AS base
|
||||
USER root
|
||||
RUN apk add --no-cache krb5-libs
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
FROM 192.168.100.120:8418/fengling/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
# Copy project files from src
|
||||
COPY ["src/Directory.Packages.props", "src/"]
|
||||
COPY ["src/YarpGateway.csproj", "src/"]
|
||||
COPY ["src/NuGet.Config", "src/"]
|
||||
# Copy NuGet config first
|
||||
COPY ["NuGet.Config", "./"]
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore "src/YarpGateway.csproj"
|
||||
# Copy all project files for restore
|
||||
COPY ["src/yarpgateway/*.props", "src/yarpgateway/"]
|
||||
COPY ["src/yarpgateway/*.csproj", "src/yarpgateway/"]
|
||||
COPY ["src/Fengling.Gateway.Plugin.Abstractions/*.csproj", "src/Fengling.Gateway.Plugin.Abstractions/"]
|
||||
|
||||
# Copy all source code
|
||||
COPY . .
|
||||
|
||||
WORKDIR "/src"
|
||||
RUN dotnet build "src/YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
# Restore dependencies (after copying all code to ensure project references work)
|
||||
RUN dotnet restore "src/yarpgateway/YarpGateway.csproj" --configfile "NuGet.Config"
|
||||
|
||||
# Build
|
||||
WORKDIR "/src/src/yarpgateway"
|
||||
RUN dotnet build "YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/build --no-restore
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "src/YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
WORKDIR "/src/src/yarpgateway"
|
||||
RUN dotnet publish "YarpGateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false --no-restore
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
15
NuGet.Config
15
NuGet.Config
@ -1,16 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="gitea" value="http://192.168.100.120:8418/api/packages/movingsam/nuget" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="Microsoft.*" />
|
||||
<package pattern="Serilog.*" />
|
||||
<package pattern="Npgsql.*" />
|
||||
<package pattern="StackExchange.Redis" />
|
||||
<package pattern="Yarp.*" />
|
||||
<package pattern="xunit" />
|
||||
<package pattern="Moq" />
|
||||
<package pattern="FluentAssertions" />
|
||||
<package pattern="Microsoft.NET.Test.Sdk" />
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="gitea">
|
||||
<package pattern="Fengling.*" />
|
||||
|
||||
79
k8s/test/deployment.yaml
Normal file
79
k8s/test/deployment.yaml
Normal file
@ -0,0 +1,79 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fengling-gateway
|
||||
namespace: fengling-test
|
||||
labels:
|
||||
app: fengling-gateway
|
||||
version: v2
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fengling-gateway
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fengling-gateway
|
||||
version: v2
|
||||
spec:
|
||||
containers:
|
||||
- name: gateway
|
||||
image: 192.168.100.120:8418/fengling/fengling-gateway:test
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Production"
|
||||
- name: ASPNETCORE_URLS
|
||||
value: "http://+:8080"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: fengling-gateway-config
|
||||
- secretRef:
|
||||
name: fengling-gateway-secret
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 512Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: fengling-gateway
|
||||
namespace: fengling-test
|
||||
labels:
|
||||
app: fengling-gateway
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: fengling-gateway
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
name: http
|
||||
- port: 8081
|
||||
targetPort: 8081
|
||||
name: management
|
||||
12
k8s/test/secret.yaml
Normal file
12
k8s/test/secret.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: fengling-gateway-secret
|
||||
namespace: fengling-test
|
||||
type: Opaque
|
||||
stringData:
|
||||
# 使用与生产环境相同的数据库(或修改为测试数据库)
|
||||
ConnectionStrings__DefaultConnection: "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
|
||||
ConnectionStrings__ConsoleConnection: "Host=81.68.223.70;Port=15432;Database=fengling_console;Username=movingsam;Password=sl52788542"
|
||||
Jwt__Authority: "https://apigateway.shtao1.cn"
|
||||
Redis__ConnectionString: "81.68.223.70:16379,password=sl52788542"
|
||||
@ -51,7 +51,7 @@ public class DatabaseRouteConfigProvider
|
||||
await using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
var routes = await dbContext
|
||||
.GwTenantRoutes.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.GwRoutes.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
var newRoutes = new ConcurrentDictionary<string, RouteConfig>();
|
||||
@ -65,7 +65,6 @@ public class DatabaseRouteConfigProvider
|
||||
Match = new RouteMatch { Path = route.Match?.Path ?? string.Empty },
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["TenantCode"] = route.TenantCode,
|
||||
["ServiceName"] = route.ServiceName,
|
||||
},
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ namespace YarpGateway.Data;
|
||||
public class GatewayDbContext : PlatformDbContext
|
||||
{
|
||||
// DbSet 别名,兼容旧代码
|
||||
public DbSet<GwTenantRoute> TenantRoutes => GwTenantRoutes;
|
||||
public DbSet<GwRoute> Routes => GwRoutes;
|
||||
public DbSet<GwCluster> ServiceInstances => GwClusters;
|
||||
|
||||
// 服务发现相关
|
||||
@ -51,7 +51,7 @@ public class GatewayDbContext : PlatformDbContext
|
||||
{
|
||||
var entries = ChangeTracker.Entries()
|
||||
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
|
||||
.Where(e => e.Entity is GwTenantRoute or GwCluster or Tenant);
|
||||
.Where(e => e.Entity is GwRoute or GwCluster or Tenant);
|
||||
|
||||
_configChangeDetected = entries.Any();
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Fengling ServiceDiscovery Packages (from Gitea) -->
|
||||
<PackageVersion Include="Fengling.Platform.Infrastructure" Version="1.0.14" />
|
||||
<PackageVersion Include="Fengling.Platform.Infrastructure" Version="1.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
|
||||
|
||||
@ -77,7 +77,7 @@ public class TenantRoutingMiddleware
|
||||
}
|
||||
|
||||
// 4. 从 RouteCache 获取路由信息
|
||||
var route = _routeCache.GetRoute(headerTenantId, serviceName);
|
||||
var route = _routeCache.GetRoute(serviceName);
|
||||
if (route == null)
|
||||
{
|
||||
_logger.LogDebug("Route not found - Tenant: {Tenant}, Service: {Service}", headerTenantId, serviceName);
|
||||
@ -89,9 +89,8 @@ public class TenantRoutingMiddleware
|
||||
context.Items["DynamicClusterId"] = route.ClusterId;
|
||||
context.Items["TenantId"] = headerTenantId; // 供 Transform 使用
|
||||
|
||||
var routeType = route.IsGlobal ? "global" : "tenant-specific";
|
||||
_logger.LogDebug("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}, Type: {Type}",
|
||||
headerTenantId, serviceName, route.ClusterId, routeType);
|
||||
_logger.LogDebug("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}",
|
||||
headerTenantId, serviceName, route.ClusterId);
|
||||
|
||||
// 6. 继续执行,由 TenantRoutingTransform 处理 Destination 选择
|
||||
await _next(context);
|
||||
|
||||
@ -1,209 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YarpGateway.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
[DbContext(typeof(GatewayDbContext))]
|
||||
[Migration("20260201120312_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Health")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Health");
|
||||
|
||||
b.HasIndex("ClusterId", "DestinationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ServiceInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("TenantName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantCode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tenants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PathPattern")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterId");
|
||||
|
||||
b.HasIndex("TenantCode", "ServiceName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TenantRoutes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||
{
|
||||
b.HasOne("YarpGateway.Models.GwTenant", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantCode")
|
||||
.HasPrincipalKey("TenantCode")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServiceInstances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
DestinationId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Address = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Health = table.Column<int>(type: "integer", nullable: false),
|
||||
Weight = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServiceInstances", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tenants",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
TenantName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tenants", x => x.Id);
|
||||
table.UniqueConstraint("AK_Tenants_TenantCode", x => x.TenantCode);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TenantRoutes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
ServiceName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
PathPattern = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Priority = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TenantRoutes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||
column: x => x.TenantCode,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "TenantCode",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServiceInstances_ClusterId_DestinationId",
|
||||
table: "ServiceInstances",
|
||||
columns: new[] { "ClusterId", "DestinationId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServiceInstances_Health",
|
||||
table: "ServiceInstances",
|
||||
column: "Health");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TenantRoutes_ClusterId",
|
||||
table: "TenantRoutes",
|
||||
column: "ClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TenantRoutes_TenantCode_ServiceName",
|
||||
table: "TenantRoutes",
|
||||
columns: new[] { "TenantCode", "ServiceName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tenants_TenantCode",
|
||||
table: "Tenants",
|
||||
column: "TenantCode",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServiceInstances");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TenantRoutes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tenants");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,205 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YarpGateway.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
[DbContext(typeof(GatewayDbContext))]
|
||||
[Migration("20260201133826_AddIsGlobalToTenantRoute")]
|
||||
partial class AddIsGlobalToTenantRoute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Health")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Health");
|
||||
|
||||
b.HasIndex("ClusterId", "DestinationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ServiceInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("TenantName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantCode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tenants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PathPattern")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterId");
|
||||
|
||||
b.HasIndex("ServiceName");
|
||||
|
||||
b.HasIndex("TenantCode");
|
||||
|
||||
b.HasIndex("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
b.ToTable("TenantRoutes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsGlobalToTenantRoute : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||
table: "TenantRoutes");
|
||||
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_Tenants_TenantCode",
|
||||
table: "Tenants");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TenantRoutes_TenantCode_ServiceName",
|
||||
table: "TenantRoutes");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGlobal",
|
||||
table: "TenantRoutes",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TenantRoutes_ServiceName",
|
||||
table: "TenantRoutes",
|
||||
column: "ServiceName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TenantRoutes_ServiceName_IsGlobal_Status",
|
||||
table: "TenantRoutes",
|
||||
columns: new[] { "ServiceName", "IsGlobal", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TenantRoutes_TenantCode",
|
||||
table: "TenantRoutes",
|
||||
column: "TenantCode");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TenantRoutes_ServiceName",
|
||||
table: "TenantRoutes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TenantRoutes_ServiceName_IsGlobal_Status",
|
||||
table: "TenantRoutes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_TenantRoutes_TenantCode",
|
||||
table: "TenantRoutes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGlobal",
|
||||
table: "TenantRoutes");
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_Tenants_TenantCode",
|
||||
table: "Tenants",
|
||||
column: "TenantCode");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TenantRoutes_TenantCode_ServiceName",
|
||||
table: "TenantRoutes",
|
||||
columns: new[] { "TenantCode", "ServiceName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||||
table: "TenantRoutes",
|
||||
column: "TenantCode",
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "TenantCode",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,275 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YarpGateway.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
[DbContext(typeof(GatewayDbContext))]
|
||||
[Migration("20260222134342_AddPendingServiceDiscovery")]
|
||||
partial class AddPendingServiceDiscovery
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwPendingServiceDiscovery", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime?>("AssignedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("AssignedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("AssignedClusterId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("DiscoveredAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DiscoveredPorts")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("K8sClusterIP")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("K8sNamespace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("K8sServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Labels")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("PodCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DiscoveredAt");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("K8sServiceName", "K8sNamespace", "IsDeleted")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PendingServiceDiscoveries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Health")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Health");
|
||||
|
||||
b.HasIndex("ClusterId", "DestinationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ServiceInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("TenantName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantCode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tenants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PathPattern")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterId");
|
||||
|
||||
b.HasIndex("ServiceName");
|
||||
|
||||
b.HasIndex("TenantCode");
|
||||
|
||||
b.HasIndex("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
b.ToTable("TenantRoutes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPendingServiceDiscovery : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PendingServiceDiscoveries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
K8sServiceName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
K8sNamespace = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
K8sClusterIP = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
DiscoveredPorts = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
Labels = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
PodCount = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
AssignedClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
AssignedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
AssignedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
DiscoveredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PendingServiceDiscoveries", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PendingServiceDiscoveries_DiscoveredAt",
|
||||
table: "PendingServiceDiscoveries",
|
||||
column: "DiscoveredAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PendingServiceDiscoveries_K8sServiceName_K8sNamespace_IsDel~",
|
||||
table: "PendingServiceDiscoveries",
|
||||
columns: new[] { "K8sServiceName", "K8sNamespace", "IsDeleted" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PendingServiceDiscoveries_Status",
|
||||
table: "PendingServiceDiscoveries",
|
||||
column: "Status");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PendingServiceDiscoveries");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,272 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YarpGateway.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YarpGateway.Migrations
|
||||
{
|
||||
[DbContext(typeof(GatewayDbContext))]
|
||||
partial class GatewayDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwPendingServiceDiscovery", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime?>("AssignedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("AssignedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("AssignedClusterId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("DiscoveredAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DiscoveredPorts")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("K8sClusterIP")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("K8sNamespace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("K8sServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Labels")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("PodCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DiscoveredAt");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("K8sServiceName", "K8sNamespace", "IsDeleted")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PendingServiceDiscoveries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Health")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Health");
|
||||
|
||||
b.HasIndex("ClusterId", "DestinationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ServiceInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("TenantName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantCode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tenants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("ClusterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PathPattern")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterId");
|
||||
|
||||
b.HasIndex("ServiceName");
|
||||
|
||||
b.HasIndex("TenantCode");
|
||||
|
||||
b.HasIndex("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
b.ToTable("TenantRoutes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" character varying(150) NOT NULL,
|
||||
"ProductVersion" character varying(32) NOT NULL,
|
||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||
);
|
||||
|
||||
START TRANSACTION;
|
||||
CREATE TABLE "ServiceInstances" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"ClusterId" character varying(100) NOT NULL,
|
||||
"DestinationId" character varying(100) NOT NULL,
|
||||
"Address" character varying(200) NOT NULL,
|
||||
"Health" integer NOT NULL,
|
||||
"Weight" integer NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedBy" bigint,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedBy" bigint,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE "Tenants" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"TenantCode" character varying(50) NOT NULL,
|
||||
"TenantName" character varying(100) NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedBy" bigint,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedBy" bigint,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode")
|
||||
);
|
||||
|
||||
CREATE TABLE "TenantRoutes" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"TenantCode" character varying(50) NOT NULL,
|
||||
"ServiceName" character varying(100) NOT NULL,
|
||||
"ClusterId" character varying(100) NOT NULL,
|
||||
"PathPattern" character varying(200) NOT NULL,
|
||||
"Priority" integer NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedBy" bigint,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedBy" bigint,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId");
|
||||
|
||||
CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health");
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260201120312_InitialCreate', '10.0.2');
|
||||
|
||||
COMMIT;
|
||||
|
||||
START TRANSACTION;
|
||||
ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode";
|
||||
|
||||
ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode";
|
||||
|
||||
DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName";
|
||||
|
||||
ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName");
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260201133826_AddIsGlobalToTenantRoute', '10.0.2');
|
||||
|
||||
COMMIT;
|
||||
|
||||
START TRANSACTION;
|
||||
CREATE TABLE "PendingServiceDiscoveries" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"K8sServiceName" character varying(255) NOT NULL,
|
||||
"K8sNamespace" character varying(255) NOT NULL,
|
||||
"K8sClusterIP" character varying(50),
|
||||
"DiscoveredPorts" character varying(500) NOT NULL,
|
||||
"Labels" character varying(2000) NOT NULL,
|
||||
"PodCount" integer NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"AssignedClusterId" character varying(100),
|
||||
"AssignedBy" character varying(100),
|
||||
"AssignedAt" timestamp with time zone,
|
||||
"DiscoveredAt" timestamp with time zone NOT NULL,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_PendingServiceDiscoveries" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_PendingServiceDiscoveries_DiscoveredAt" ON "PendingServiceDiscoveries" ("DiscoveredAt");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_PendingServiceDiscoveries_K8sServiceName_K8sNamespace_IsDel~" ON "PendingServiceDiscoveries" ("K8sServiceName", "K8sNamespace", "IsDeleted");
|
||||
|
||||
CREATE INDEX "IX_PendingServiceDiscoveries_Status" ON "PendingServiceDiscoveries" ("Status");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260222134342_AddPendingServiceDiscovery', '10.0.2');
|
||||
|
||||
COMMIT;
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" character varying(150) NOT NULL,
|
||||
"ProductVersion" character varying(32) NOT NULL,
|
||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||
);
|
||||
|
||||
START TRANSACTION;
|
||||
CREATE TABLE "ServiceInstances" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"ClusterId" character varying(100) NOT NULL,
|
||||
"DestinationId" character varying(100) NOT NULL,
|
||||
"Address" character varying(200) NOT NULL,
|
||||
"Health" integer NOT NULL,
|
||||
"Weight" integer NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedBy" bigint,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedBy" bigint,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE "Tenants" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"TenantCode" character varying(50) NOT NULL,
|
||||
"TenantName" character varying(100) NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedBy" bigint,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedBy" bigint,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode")
|
||||
);
|
||||
|
||||
CREATE TABLE "TenantRoutes" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"TenantCode" character varying(50) NOT NULL,
|
||||
"ServiceName" character varying(100) NOT NULL,
|
||||
"ClusterId" character varying(100) NOT NULL,
|
||||
"PathPattern" character varying(200) NOT NULL,
|
||||
"Priority" integer NOT NULL,
|
||||
"Status" integer NOT NULL,
|
||||
"CreatedBy" bigint,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedBy" bigint,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"Version" integer NOT NULL,
|
||||
CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId");
|
||||
|
||||
CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health");
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260201120312_InitialCreate', '9.0.0');
|
||||
|
||||
ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode";
|
||||
|
||||
ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode";
|
||||
|
||||
DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName";
|
||||
|
||||
ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName");
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260201133826_AddIsGlobalToTenantRoute', '9.0.0');
|
||||
|
||||
COMMIT;
|
||||
|
||||
@ -141,9 +141,8 @@ builder.Services.AddMemoryCache();
|
||||
// 注册租户路由转换器
|
||||
builder.Services.AddSingleton<TenantRoutingTransform>();
|
||||
|
||||
// 配置 YARP 反向代理
|
||||
// 配置 YARP 反向代理 - 使用数据库配置
|
||||
builder.Services.AddReverseProxy()
|
||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
|
||||
.AddTransforms(transformBuilder =>
|
||||
{
|
||||
transformBuilder.AddRequestTransform(async context =>
|
||||
@ -172,7 +171,31 @@ app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = Dat
|
||||
app.MapControllers();
|
||||
app.MapReverseProxy();
|
||||
|
||||
await app.Services.GetRequiredService<IRouteCache>().InitializeAsync();
|
||||
// Gateway: 确保数据库表存在(在测试环境中自动创建表)
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<GatewayDbContext>();
|
||||
try
|
||||
{
|
||||
Log.Information("Ensuring Gateway database tables exist...");
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
Log.Information("Gateway database tables ready");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to ensure database tables exist");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化路由缓存
|
||||
try
|
||||
{
|
||||
await app.Services.GetRequiredService<IRouteCache>().InitializeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to initialize route cache.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@ -142,7 +142,7 @@ public class PgSqlConfigChangeListener : BackgroundService
|
||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||
await using var db = scope.ServiceProvider.GetRequiredService<GatewayDbContext>();
|
||||
|
||||
var currentRouteVersion = await db.GwTenantRoutes
|
||||
var currentRouteVersion = await db.GwRoutes
|
||||
.OrderByDescending(r => r.Version)
|
||||
.Select(r => r.Version)
|
||||
.FirstOrDefaultAsync(stoppingToken);
|
||||
@ -176,7 +176,7 @@ public class PgSqlConfigChangeListener : BackgroundService
|
||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||
await using var db = scope.ServiceProvider.GetRequiredService<GatewayDbContext>();
|
||||
|
||||
_lastRouteVersion = await db.GwTenantRoutes
|
||||
_lastRouteVersion = await db.GwRoutes
|
||||
.OrderByDescending(r => r.Version)
|
||||
.Select(r => r.Version)
|
||||
.FirstOrDefaultAsync(stoppingToken);
|
||||
|
||||
@ -12,14 +12,13 @@ public class RouteInfo
|
||||
public string ClusterId { get; set; } = string.Empty;
|
||||
public string PathPattern { get; set; } = string.Empty;
|
||||
public int Priority { get; set; }
|
||||
public bool IsGlobal { get; set; }
|
||||
}
|
||||
|
||||
public interface IRouteCache
|
||||
{
|
||||
Task InitializeAsync();
|
||||
Task ReloadAsync();
|
||||
RouteInfo? GetRoute(string tenantCode, string serviceName);
|
||||
RouteInfo? GetRoute(string serviceName);
|
||||
RouteInfo? GetRouteByPath(string path);
|
||||
}
|
||||
|
||||
@ -28,8 +27,7 @@ public class RouteCache : IRouteCache
|
||||
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
|
||||
private readonly ILogger<RouteCache> _logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string, RouteInfo> _globalRoutes = new();
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, RouteInfo>> _tenantRoutes = new();
|
||||
private readonly ConcurrentDictionary<string, RouteInfo> _routes = new();
|
||||
private readonly ConcurrentDictionary<string, RouteInfo> _pathRoutes = new();
|
||||
private readonly ReaderWriterLockSlim _lock = new();
|
||||
|
||||
@ -42,57 +40,47 @@ public class RouteCache : IRouteCache
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_logger.LogInformation("Initializing route cache from database...");
|
||||
await LoadFromDatabaseAsync();
|
||||
_logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes",
|
||||
_globalRoutes.Count, _tenantRoutes.Count);
|
||||
try
|
||||
{
|
||||
await LoadFromDatabaseAsync();
|
||||
_logger.LogInformation("Route cache initialized: {Count} routes", _routes.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load routes from database. This is normal if database is not initialized yet.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReloadAsync()
|
||||
{
|
||||
_logger.LogInformation("Reloading route cache...");
|
||||
await LoadFromDatabaseAsync();
|
||||
_logger.LogInformation("Route cache reloaded");
|
||||
_logger.LogInformation("Route cache reloaded: {Count} routes", _routes.Count);
|
||||
}
|
||||
|
||||
public RouteInfo? GetRoute(string? tenantCode, string? serviceName)
|
||||
public RouteInfo? GetRoute(string? serviceName)
|
||||
{
|
||||
// 参数校验
|
||||
if (string.IsNullOrEmpty(serviceName))
|
||||
{
|
||||
_logger.LogDebug("GetRoute called with null or empty serviceName");
|
||||
return null;
|
||||
}
|
||||
|
||||
tenantCode ??= string.Empty;
|
||||
|
||||
_lock.EnterUpgradeableReadLock();
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// 1. 优先查找租户专用路由
|
||||
if (!string.IsNullOrEmpty(tenantCode) &&
|
||||
_tenantRoutes.TryGetValue(tenantCode, out var tenantRouteMap) &&
|
||||
tenantRouteMap.TryGetValue(serviceName, out var tenantRoute))
|
||||
if (_routes.TryGetValue(serviceName, out var route))
|
||||
{
|
||||
_logger.LogDebug("Found tenant-specific route: {Tenant}/{Service} -> {Cluster}",
|
||||
tenantCode, serviceName, tenantRoute.ClusterId);
|
||||
return tenantRoute;
|
||||
_logger.LogDebug("Found route: {Service} -> {Cluster}", serviceName, route.ClusterId);
|
||||
return route;
|
||||
}
|
||||
|
||||
// 2. 查找全局路由
|
||||
if (_globalRoutes.TryGetValue(serviceName, out var globalRoute))
|
||||
{
|
||||
_logger.LogDebug("Found global route: {Service} -> {Cluster} for tenant {Tenant}",
|
||||
serviceName, globalRoute.ClusterId, tenantCode);
|
||||
return globalRoute;
|
||||
}
|
||||
|
||||
// 3. 没找到
|
||||
_logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName);
|
||||
_logger.LogWarning("No route found for: {Service}", serviceName);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitUpgradeableReadLock();
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,15 +93,23 @@ public class RouteCache : IRouteCache
|
||||
{
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
|
||||
var routes = await db.GwTenantRoutes
|
||||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
List<GwRoute> routes;
|
||||
try
|
||||
{
|
||||
routes = await db.GwRoutes
|
||||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Database table not found. Returning empty route list.");
|
||||
routes = new List<GwRoute>();
|
||||
}
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_globalRoutes.Clear();
|
||||
_tenantRoutes.Clear();
|
||||
_routes.Clear();
|
||||
_pathRoutes.Clear();
|
||||
|
||||
foreach (var route in routes)
|
||||
@ -125,34 +121,14 @@ public class RouteCache : IRouteCache
|
||||
Id = route.Id,
|
||||
ClusterId = route.ClusterId,
|
||||
PathPattern = pathPattern,
|
||||
Priority = route.Priority,
|
||||
IsGlobal = route.IsGlobal
|
||||
Priority = route.Priority
|
||||
};
|
||||
|
||||
// 1. 全局路由
|
||||
if (route.IsGlobal)
|
||||
{
|
||||
_globalRoutes[route.ServiceName] = routeInfo;
|
||||
_pathRoutes[pathPattern] = routeInfo;
|
||||
_logger.LogDebug("Loaded global route: {Service} -> {Cluster}",
|
||||
route.ServiceName, route.ClusterId);
|
||||
}
|
||||
_routes[route.ServiceName] = routeInfo;
|
||||
_pathRoutes[pathPattern] = routeInfo;
|
||||
|
||||
// 2. 租户专属路由(无论 IsGlobal 值如何,只要有 TenantCode 就添加到租户路由表)
|
||||
if (!string.IsNullOrEmpty(route.TenantCode))
|
||||
{
|
||||
_tenantRoutes.GetOrAdd(route.TenantCode, _ => new ConcurrentDictionary<string, RouteInfo>())
|
||||
[route.ServiceName] = routeInfo;
|
||||
|
||||
// 如果不是全局路由,也添加到路径路由
|
||||
if (!route.IsGlobal)
|
||||
{
|
||||
_pathRoutes[pathPattern] = routeInfo;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Loaded tenant route: {Tenant}/{Service} -> {Cluster}",
|
||||
route.TenantCode, route.ServiceName, route.ClusterId);
|
||||
}
|
||||
_logger.LogDebug("Loaded route: {Service} -> {Cluster}",
|
||||
route.ServiceName, route.ClusterId);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
"DefaultConnection": ""
|
||||
},
|
||||
"Jwt": {
|
||||
"Authority": "",
|
||||
"Authority": "http://auth.fengling.local:30080",
|
||||
"Audience": "fengling-gateway",
|
||||
"ValidateIssuer": true,
|
||||
"ValidateAudience": true
|
||||
"ValidateAudience": true,
|
||||
"RequireHttpsMetadata": false
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "",
|
||||
@ -64,4 +65,4 @@
|
||||
"ServiceDiscovery": {
|
||||
"UseInClusterConfig": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
网关配置的新想法.md
23
网关配置的新想法.md
@ -1,23 +0,0 @@
|
||||
#### 网关配置的新想法
|
||||
|
||||
路由/集群/目标 等配置还是通过数据库变更通知网关进行重新加载的方式触发变更
|
||||
|
||||
k8s 的service中需要有固定的label来约定产生新的配置
|
||||
以下是范例
|
||||
service-label
|
||||
- app-router-host = https://hostname #代表网关域名地址
|
||||
- app-router-name = member # 代表路由名
|
||||
- app-router-prefix = /member # 代表路由前缀
|
||||
- app-cluster-name = member #代表集群名(id)
|
||||
- app-cluster-destination = default # 代表标准服务目标 如果不是default 比如 1668 则代表企业编号独有的目标
|
||||
详细请求说明:
|
||||
比如一个请求进来了 请求路径是 {host}/member/api/v1/memberinfo/{id} header:{authorization: bearer xxx}
|
||||
-> 根据 host+ prefix匹配到 member路由 -> 进入到对应的cluster ->
|
||||
中间件进行解析或者是yarp-transform能做到最好使用yarp-transform来处理 或者如果我集成了openiddict是否可以拿到jwt这部分的信息
|
||||
-> 解析出 customer/或者是租户id都行 只要能表示租户的都算 -> 找到对应的租户就进对应的目标服务 找不到就进 标准服务目标
|
||||
|
||||
配置生效详细说明:
|
||||
1.在console项目中监听k8s 的服务 如果发现有新的app-router-name 相关信息就要产生待执行配置 用户明确确认后 生成数据库配置 在此之前只能存在于内存/缓存中 ;
|
||||
2.同样的监听服务 发现app-cluster-name 检查是否有新的app-cluster-name 如果有新的 同样产生待执行配置 后续于路由一致
|
||||
3.同样监听服务 发现app-cluster-destination 检查对应的cluster是否存在这个destination 如果不存在 同样产生待执行配置 后续与上述一致
|
||||
4.用户确认是否执行这些配置 用户加载到界面后可调整配置,同意后产生持久化数据到数据库,点击立即生效才下发到yarp网关 网关重新根据新的配置加载到最新配置后生效
|
||||
Loading…
Reference in New Issue
Block a user