Compare commits
No commits in common. "154484d2dc4468df8c08636f4df8578d31cf71d4" and "807dcfd4221ec702bfd7fb43cf6d3ca6c41df8ee" have entirely different histories.
154484d2dc
...
807dcfd422
@ -1,100 +0,0 @@
|
||||
# Fengling Console
|
||||
|
||||
## 这是什么
|
||||
|
||||
Fengling 微服务生态系统的中央管理控制台。负责用户管理、租户管理、OAuth 客户端管理,以及**网关配置管理**。是所有运维操作的中枢后台。
|
||||
|
||||
## 核心价值
|
||||
|
||||
统一的管理入口,负责所有运维相关的配置和操作,让其他服务专注于业务逻辑。
|
||||
|
||||
## 需求
|
||||
|
||||
### 已验证(现有功能)
|
||||
|
||||
- ✓ 用户管理(CRUD、角色分配)— 已有
|
||||
- ✓ 租户管理 — 已有
|
||||
- ✓ OAuth 客户端管理 — 已有
|
||||
- ✓ 网关服务/路由/实例管理 — 已有(GatewayController)
|
||||
- ✓ 租户申请审批流程 — 已有
|
||||
|
||||
### 进行中
|
||||
|
||||
- [ ] 实现配置变更广播机制(通过 PostgreSQL NOTIFY 通知所有网关实例)
|
||||
- [ ] 实现 K8s 服务健康检查功能
|
||||
|
||||
- [ ] 实现配置变更广播机制(通知所有网关实例)
|
||||
- [ ] 实现 K8s 服务健康检查功能
|
||||
- [ ] 集成 Redis pub/sub 用于多实例通信
|
||||
|
||||
### 范围外
|
||||
|
||||
- [业务逻辑] — 由各微服务负责
|
||||
- [API 认证] — 由 auth-service 负责
|
||||
- [服务发现] — 由 service-discovery 负责
|
||||
|
||||
## 背景
|
||||
|
||||
**与 Gateway 的关系:**
|
||||
```
|
||||
fengling-console (管理平面)
|
||||
│
|
||||
├── 用户/租户/配置管理
|
||||
│
|
||||
└── 网关配置管理
|
||||
│
|
||||
├── GatewayDbContext (直接操作数据库)
|
||||
├── GatewayController (API)
|
||||
└── ReloadGatewayAsync() (待实现广播)
|
||||
│
|
||||
▼
|
||||
fengling-gateway (数据平面)
|
||||
│
|
||||
└── 监听配置变更,重新加载
|
||||
```
|
||||
|
||||
**当前问题(来自 CONCERNS.md):**
|
||||
- OAuth 密钥硬编码
|
||||
- CORS 允许所有(开发环境)
|
||||
- 缺少测试覆盖
|
||||
- ReloadGatewayAsync() 为空实现
|
||||
|
||||
**Console 已有能力:**
|
||||
- GatewayDbContext - 管理网关路由、集群、实例数据
|
||||
- GatewayController - 提供 /api/console/gateway/* API
|
||||
- GatewayService - 业务逻辑
|
||||
- 网关已有 PgSqlConfigChangeListener 使用 NOTIFY/LISTEN,可复用
|
||||
- OAuth 密钥硬编码
|
||||
- CORS 允许所有(开发环境)
|
||||
- 缺少测试覆盖
|
||||
- Redis 已引用但未使用
|
||||
- ReloadGatewayAsync() 为空实现
|
||||
|
||||
**Console 已有能力:**
|
||||
- GatewayDbContext - 管理网关路由、集群、实例数据
|
||||
- GatewayController - 提供 /api/console/gateway/* API
|
||||
- GatewayService - 业务逻辑
|
||||
- Redis 引用 - 可用于 pub/sub 广播
|
||||
|
||||
## 约束
|
||||
|
||||
- **多实例**:Console 必须支持多实例部署
|
||||
- **配置广播**:配置变更需要通知所有网关实例
|
||||
- **K8s 健康**:Console 需要实现 K8s 服务健康检查
|
||||
- **技术栈**:.NET 10.0, ASP.NET Core, PostgreSQL
|
||||
|
||||
## 关键决策
|
||||
|
||||
| 决策 | 理由 | 结果 |
|
||||
|------|------|------|
|
||||
| Console 作为运维中枢 | 集中管理,降低复杂度 | ✓ 良好 |
|
||||
| Gateway 配置从 Console 变更 | 单一事实来源 | ✓ 良好 |
|
||||
| PostgreSQL NOTIFY 广播 | 轻量方案,无需额外依赖 | ✓ 良好 |
|
||||
|------|------|------|
|
||||
| Console 作为运维中枢 | 集中管理,降低复杂度 | ✓ 良好 |
|
||||
| Gateway 配置从 Console 变更 | 单一事实来源 | ✓ 良好 |
|
||||
| Redis pub/sub 广播 | 成熟方案,易于实现 | ✓ 良好 |
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-02 初始化后*
|
||||
@ -1,93 +0,0 @@
|
||||
# Roadmap
|
||||
|
||||
## 当前里程碑
|
||||
|
||||
### Phase 1: 实现 Gateway 配置管理及事件推送
|
||||
|
||||
- **目标**: 实现 Console 对 Gateway 配置的增删改查功能,并添加事件推送机制,使下游 yarpgateway 能够监听到配置变更
|
||||
- **状态**: ✓ 完成
|
||||
|
||||
#### Goal
|
||||
|
||||
实现 Console 管理 Gateway 配置的完整能力,包括:
|
||||
- Gateway 配置的 CRUD 操作
|
||||
- 配置变更事件推送
|
||||
- 下游 Gateway 监听配置变更并重载
|
||||
|
||||
#### Depends on
|
||||
|
||||
- 无外部依赖
|
||||
|
||||
#### Plans
|
||||
|
||||
- [x] 01-PLAN.md — 实现配置变更广播机制
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 实现 Gateway 插件系统
|
||||
|
||||
- **目标**: 实现 YARP 网关的插件系统,包括 Web UI 管理界面和动态编译加载功能
|
||||
- **状态**: Not planned yet
|
||||
|
||||
#### Goal
|
||||
|
||||
实现 YARP 网关的插件系统规划与实现,包括:
|
||||
- Web UI 管理界面(路由管理、集群管理、插件管理)
|
||||
- 在线 C# 代码编辑(Monaco Editor)
|
||||
- 动态编译加载(Roslyn)
|
||||
- 插件生命周期管理
|
||||
|
||||
#### Depends on
|
||||
|
||||
- Phase 1: 实现 Gateway 配置管理及事件推送
|
||||
|
||||
#### Plans
|
||||
|
||||
- [ ] 02-PLAN.md — 实施计划
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 网关配置变更广播机制
|
||||
|
||||
- **目标**: 理解现有网关配置的完整链路:路由 -> 服务 -> 下游服务,梳理配置变更时如何发送新增/变更广播事件
|
||||
- **状态**: Planned
|
||||
|
||||
#### Goal
|
||||
|
||||
理解现有网关配置的完整链路:
|
||||
- 路由配置如何传递到下游服务
|
||||
- 服务发现与下游服务的关系
|
||||
- 配置变更时的新增/变更广播事件机制
|
||||
|
||||
#### Depends on
|
||||
|
||||
- Phase 2: 实现 Gateway 插件系统
|
||||
|
||||
#### Plans
|
||||
|
||||
- [x] 03-PLAN.md — 实施计划
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 适配 Platform 1.0.12 Gateway 实体变更
|
||||
|
||||
- **目标**: 适配 Platform 1.0.12 中的 Gateway 实体重构,修复编译错误,更新 Console 代码以使用新的 GwCluster/GwDestination/GwTenantRoute 模型
|
||||
WR|- [x] 04-PLAN.md — 实施计划
|
||||
NH|- **状态**: Planned
|
||||
|
||||
#### Goal
|
||||
|
||||
适配 Platform 1.0.12 实体变更:
|
||||
- 移除 IInstanceStore 依赖,改用 IClusterStore
|
||||
- 更新 GatewayService 使用新的接口方法
|
||||
- 更新数据模型映射(GatewayInstanceDto → GwDestination)
|
||||
- 修复编译错误
|
||||
|
||||
#### Depends on
|
||||
|
||||
- Phase 3: 网关配置变更广播机制
|
||||
|
||||
#### Plans
|
||||
|
||||
- [ ] 04-PLAN.md — 实施计划
|
||||
@ -1,83 +0,0 @@
|
||||
# 状态:Fengling Console
|
||||
|
||||
**最后更新:** 2026-03-04
|
||||
|
||||
---
|
||||
|
||||
## 项目引用
|
||||
|
||||
参考:.planning/PROJECT.md(更新于 2026-03-02)
|
||||
|
||||
**核心价值:** 统一的管理入口,负责所有运维相关的配置和操作,让其他服务专注于业务逻辑。
|
||||
|
||||
**当前重点:** Phase 4: 待添加(适配 Platform 1.0.12 实体变更)
|
||||
|
||||
---
|
||||
|
||||
## 项目状态
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| PROJECT.md | ✓ 已初始化 |
|
||||
| CODEBASE | ✓ 已有(ARCHITECTURE.md, CONCERNS.md, STACK.md 等) |
|
||||
| Roadmap | ✓ 已创建 |
|
||||
| 变更文档 | ✓ 已创建 |
|
||||
|
||||
---
|
||||
|
||||
## 累积上下文
|
||||
|
||||
### 初始化
|
||||
|
||||
- **2026-03-02:** 创建 PROJECT.md,定义 Console 在生态系统中的角色
|
||||
- 现有代码库(已有 ARCHITECTURE.md、INTEGRATIONS.md 等)
|
||||
|
||||
### 路线图演进
|
||||
|
||||
- **2026-03-02:** Phase 1 已添加:实现 Gateway 配置管理及事件推送
|
||||
- **2026-03-02:** Phase 1 执行完成
|
||||
- **2026-03-02:** Phase 2 已添加:实现 Gateway 插件系统
|
||||
- **2026-03-03:** Phase 3 已添加:网关配置变更广播机制
|
||||
- **2026-03-03:** Phase 3 已规划
|
||||
- **2026-03-03:** Phase 3 上下文已捕获:广播策略 = 仅手动触发
|
||||
- **2026-03-04:** Platform 1.0.12 实体变更:Gateway → GwCluster/GwDestination/GwTenantRoute
|
||||
|
||||
### 与 Gateway 的集成
|
||||
|
||||
| 组件 | 位置 | 现状 |
|
||||
|------|------|------|
|
||||
| GatewayDbContext | src/Data/ | 已实现,管理网关配置数据 |
|
||||
| GatewayController | src/Controllers/ | 已实现,提供 API |
|
||||
| GatewayService | src/Services/ | 已实现,业务逻辑 |
|
||||
| ConfigNotificationService | src/Services/ | ✓ 已实现 PostgreSQL NOTIFY |
|
||||
| ReloadGatewayAsync | src/Services/GatewayService.cs | 待集成通知服务 |
|
||||
|
||||
### 待完成任务
|
||||
|
||||
- **适配 Platform 1.0.12 实体变更**(编译错误待修复)
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
### Platform 1.0.12 Gateway 实体变更
|
||||
|
||||
详细变更见:`.planning/docs/gateway-entity-changes-1.0.12.md`
|
||||
|
||||
**主要变更:**
|
||||
1. GatewayInstance → GwDestination(内嵌值对象)
|
||||
2. GatewayCluster → GwCluster(聚合根,包含 Destinations)
|
||||
3. GatewayRoute → GwTenantRoute(通过 ClusterId 关联)
|
||||
4. IInstanceStore 移除,改用 IClusterStore
|
||||
|
||||
---
|
||||
|
||||
## 备注
|
||||
|
||||
- Console 是运维中枢,网关配置的单一管理门户
|
||||
- 广播策略:仅手动触发(通过 /reload 接口)
|
||||
- 下游网关收到通知后自行查询数据库刷新
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-04*
|
||||
@ -1,141 +0,0 @@
|
||||
# Gateway 实体变更记录
|
||||
|
||||
**变更日期:** 2026-03-04
|
||||
**Platform 版本:** 1.0.12
|
||||
|
||||
---
|
||||
|
||||
## 变更概述
|
||||
|
||||
Platform 1.0.12 对 Gateway 相关实体进行了重构,主要变化是将实例(Instance)内嵌到集群(Cluster)中,简化了领域模型。
|
||||
|
||||
---
|
||||
|
||||
## 实体变更
|
||||
|
||||
### 1. GwDestination(新增 - 原 GatewayInstance)
|
||||
|
||||
**旧名称:** GatewayInstance
|
||||
**新名称:** GwDestination
|
||||
**类型:** 值对象(内嵌于 GwCluster)
|
||||
|
||||
```csharp
|
||||
public class GwDestination
|
||||
{
|
||||
public string DestinationId { get; set; } // 目标标识
|
||||
public string Address { get; set; } // 后端地址
|
||||
public string? Health { get; set; } // 健康检查端点
|
||||
public int Weight { get; set; } = 1; // 权重
|
||||
public int HealthStatus { get; set; } = 1; // 健康状态
|
||||
public int Status { get; set; } = 1; // 状态
|
||||
}
|
||||
```
|
||||
|
||||
### 2. GwCluster(重构 - 原 GatewayCluster)
|
||||
|
||||
**旧名称:** GatewayCluster
|
||||
**新名称:** GwCluster
|
||||
**类型:** 聚合根
|
||||
|
||||
**主要变化:**
|
||||
- 包含 `List<GwDestination> Destinations` 作为内嵌集合
|
||||
- 包含 `GwLoadBalancingPolicy` 负载均衡策略
|
||||
- 包含 `GwHealthCheckConfig` 健康检查配置
|
||||
- 包含 `GwSessionAffinityConfig` 会话亲和配置
|
||||
|
||||
### 3. GwTenantRoute(重构 - 原 GatewayRoute)
|
||||
|
||||
**旧名称:** GatewayRoute
|
||||
**新名称:** GwTenantRoute
|
||||
**类型:** 实体
|
||||
|
||||
**主要变化:**
|
||||
- 通过 `ClusterId` 关联到 `GwCluster`
|
||||
- 包含 `GwRouteMatch` 路由匹配配置
|
||||
- 支持 `GwLoadBalancingPolicy` 路由级别负载均衡覆盖
|
||||
|
||||
---
|
||||
|
||||
## 接口变更
|
||||
|
||||
### IInstanceStore(已移除)
|
||||
|
||||
**状态:** 已移除
|
||||
|
||||
**原因:** 实例(Destination)现在是 GwCluster 的内嵌对象,不再需要独立的 IInstanceStore 接口。
|
||||
|
||||
### IClusterStore(新增)
|
||||
|
||||
```csharp
|
||||
public interface IClusterStore
|
||||
{
|
||||
// Basic CRUD
|
||||
Task<GwCluster?> FindByIdAsync(string? id, CancellationToken cancellationToken = default);
|
||||
Task<GwCluster?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default);
|
||||
Task<IList<GwCluster>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<IList<GwCluster>> GetPagedAsync(int page, int pageSize, string? clusterId = null,
|
||||
string? name = null, int? status = null, CancellationToken cancellationToken = default);
|
||||
Task<int> GetCountAsync(string? clusterId = null, string? name = null,
|
||||
int? status = null, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> CreateAsync(GwCluster cluster, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> UpdateAsync(GwCluster cluster, CancellationToken cancellationToken = default);
|
||||
Task<IdentityResult> DeleteAsync(GwCluster cluster, CancellationToken cancellationToken = default);
|
||||
|
||||
// Destination management (NEW)
|
||||
Task<GwCluster?> AddDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default);
|
||||
Task<GwCluster?> UpdateDestinationAsync(string clusterId, string destinationId, GwDestination destination, CancellationToken cancellationToken = default);
|
||||
Task<GwCluster?> RemoveDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构变化
|
||||
|
||||
### 旧架构
|
||||
|
||||
```
|
||||
Route → ClusterId → Instance (独立实体)
|
||||
```
|
||||
|
||||
### 新架构
|
||||
|
||||
```
|
||||
TenantRoute → ClusterId → GwCluster (聚合根) → List<GwDestination>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Console 适配需求
|
||||
|
||||
由于接口变更,Console 需要进行以下适配:
|
||||
|
||||
1. **移除 IInstanceStore 依赖**
|
||||
- 移除 `IInstanceStore` 注入
|
||||
- 使用 `IClusterStore` 替代
|
||||
|
||||
2. **更新 GatewayService**
|
||||
- 实例操作改为通过 `IClusterStore.AddDestinationAsync` 等方法
|
||||
- 查询实例改为从 `GwCluster.Destinations` 获取
|
||||
|
||||
3. **更新数据模型**
|
||||
- GatewayInstanceDto → 从 GwDestination 映射
|
||||
- GatewayClusterDto → 从 GwCluster 映射
|
||||
|
||||
4. **更新 API 端点**
|
||||
- `/instances` 相关端点可能需要调整
|
||||
|
||||
---
|
||||
|
||||
## 相关文件
|
||||
|
||||
### Platform 侧
|
||||
- `Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwCluster.cs`
|
||||
- `Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwDestination.cs`
|
||||
- `Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwTenantRoute.cs`
|
||||
- `Fengling.Platform.Infrastructure/IClusterStore.cs`
|
||||
- `Fengling.Platform.Infrastructure/ClusterStore.cs`
|
||||
|
||||
### Console 侧(需要适配)
|
||||
- `src/Services/GatewayService.cs` - 需要适配新接口
|
||||
- `src/Program.cs` - 需要更新依赖注入
|
||||
@ -1,75 +0,0 @@
|
||||
# Phase 1: 实现 Gateway 配置管理及事件推送 - Context
|
||||
|
||||
**收集日期:** 2026-03-02
|
||||
**状态:** Ready for planning
|
||||
**来源:** Manual planning (gsd-tools not available)
|
||||
|
||||
**更新:** 2026-03-03 - 添加数据源决策
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
实现 Console 管理 Gateway 配置的完整能力,包括:
|
||||
- Gateway 配置的 CRUD 操作(已大部实现)
|
||||
- 配置变更事件推送(待实现)
|
||||
- 下游 Gateway 监听配置变更并重载
|
||||
|
||||
**现有能力:**
|
||||
- GatewayController: API 端点已实现
|
||||
- GatewayService: 业务逻辑已实现
|
||||
- DTOs: 数据传输对象已定义
|
||||
|
||||
**待实现:**
|
||||
- ReloadGatewayAsync() 广播机制
|
||||
- 配置变更时自动触发广播
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### 技术选型
|
||||
- **广播机制**: PostgreSQL NOTIFY/LISTEN(轻量方案,无需额外依赖)
|
||||
- **备选方案**: Redis pub/sub(如需多实例通信)
|
||||
|
||||
### 数据源
|
||||
- **通知服务数据库连接**: 从 EF Core DbContext 获取,而非从配置文件读取
|
||||
- **实现方式**: 注入 ConsoleDbContext,使用 `DbContext.Database.GetConnectionString()`
|
||||
|
||||
### 功能决策
|
||||
- **自动广播**: 配置变更(创建/更新/删除)时自动触发广播
|
||||
- **手动广播**: 提供 /api/console/gateway/reload 手动触发端点
|
||||
|
||||
### Claude's Discretion
|
||||
- 具体的 NOTIFY 通道名称格式
|
||||
- 事件 payload 结构设计
|
||||
- 是否需要事件类型区分(service/route/instance)
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
**关键文件:**
|
||||
- src/Services/GatewayService.cs - ReloadGatewayAsync() 空实现需填充
|
||||
- src/Controllers/GatewayController.cs - POST /reload 端点
|
||||
- src/Services/ConfigNotificationService.cs - 需修改为使用 DbContext 获取连接字符串
|
||||
|
||||
**依赖:**
|
||||
- Npgsql - PostgreSQL 通知(已通过 EF Core 引用)
|
||||
- Redis(可选)- 如选择 Redis pub/sub
|
||||
|
||||
**参考实现:**
|
||||
- 网关已有 PgSqlConfigChangeListener 使用 NOTIFY/LISTEN,可复用
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- K8s 服务健康检查(后续 Phase)
|
||||
- Redis pub/sub(如果 PostgreSQL NOTIFY 方案不够用再考虑)
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 01-gateway-config-management*
|
||||
*Context gathered: 2026-03-02, updated 2026-03-03*
|
||||
@ -1,146 +0,0 @@
|
||||
---
|
||||
phase: 01-gateway-config-management
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Services/GatewayService.cs
|
||||
- src/Services/ConfigNotificationService.cs
|
||||
- src/Data/ConsoleDbContext.cs
|
||||
autonomous: true
|
||||
requirements: []
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "配置变更后下游 Gateway 能收到通知"
|
||||
- "手动触发 /reload 端点能广播配置变更"
|
||||
- "自动触发:服务/路由/实例变更时自动广播"
|
||||
artifacts:
|
||||
- path: "src/Services/ConfigNotificationService.cs"
|
||||
provides: "配置变更通知服务"
|
||||
contains: "INotificationService"
|
||||
- path: "src/Services/GatewayService.cs"
|
||||
provides: "触发通知逻辑"
|
||||
contains: "ReloadGatewayAsync"
|
||||
key_links:
|
||||
- from: "GatewayService"
|
||||
to: "ConfigNotificationService"
|
||||
via: "依赖注入"
|
||||
pattern: "INotificationService"
|
||||
---
|
||||
|
||||
<objective>
|
||||
实现配置变更广播机制,使下游 Gateway 能够监听到配置变更。
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/PROJECT.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-gateway-config-management/01-CONTEXT.md
|
||||
@src/Services/GatewayService.cs
|
||||
@src/Controllers/GatewayController.cs
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 修改 ConfigNotificationService 使用 DbContext 获取连接字符串</name>
|
||||
<files>src/Services/ConfigNotificationService.cs</files>
|
||||
<action>
|
||||
修改现有的 PgSqlNotificationService 实现:
|
||||
|
||||
1. 修改构造函数:
|
||||
- 注入 ConsoleDbContext(而非使用 IConfiguration)
|
||||
- 使用 DbContext.Database.GetConnectionString() 获取连接字符串
|
||||
|
||||
2. 移除:
|
||||
- IConfiguration 依赖
|
||||
- _configuration.GetConnectionString("DefaultConnection")
|
||||
|
||||
3. 示例代码:
|
||||
```csharp
|
||||
public PgSqlNotificationService(
|
||||
ConsoleDbContext dbContext,
|
||||
ILogger<PgSqlNotificationService> logger)
|
||||
{
|
||||
_connectionString = dbContext.Database.GetConnectionString()
|
||||
?? throw new InvalidOperationException("DefaultConnection not configured");
|
||||
_logger = logger;
|
||||
}
|
||||
```
|
||||
|
||||
4. 在 Program.cs 中注册服务时传入 DbContext:
|
||||
```csharp
|
||||
services.AddScoped<INotificationService>(sp =>
|
||||
new PgSqlNotificationService(
|
||||
sp.GetRequiredService<ConsoleDbContext>(),
|
||||
sp.GetRequiredService<ILogger<PgSqlNotificationService>>()));
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build --no-restore 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>PgSqlNotificationService 已修改为使用 DbContext 获取连接字符串</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 修改 GatewayService 集成通知服务</name>
|
||||
<files>src/Services/GatewayService.cs</files>
|
||||
<action>
|
||||
修改 GatewayService 以集成通知服务:
|
||||
|
||||
1. 添加 INotificationService 依赖注入到 GatewayService 构造函数
|
||||
|
||||
2. 修改 ReloadGatewayAsync() 实现:
|
||||
- 调用 _notificationService.PublishAsync("gateway_config_changed", JsonSerialize(reloadEvent))
|
||||
- 日志记录广播成功
|
||||
|
||||
3. 在以下 CRUD 操作中添加自动广播(创建/更新/删除后):
|
||||
- RegisterServiceAsync - 服务注册
|
||||
- UnregisterServiceAsync - 服务注销
|
||||
- CreateRouteAsync - 路由创建
|
||||
- AddInstanceAsync - 实例添加
|
||||
- RemoveInstanceAsync - 实例删除
|
||||
- UpdateInstanceWeightAsync - 权重更新
|
||||
|
||||
4. 事件 Payload 格式:
|
||||
```json
|
||||
{
|
||||
"eventType": "service|route|instance",
|
||||
"action": "create|update|delete|reload",
|
||||
"timestamp": "2026-03-02T12:00:00Z",
|
||||
"details": { ... }
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build --no-restore 2>&1 | head -30</automated>
|
||||
</verify>
|
||||
<done>GatewayService 集成通知服务,所有配置变更操作自动触发广播</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
整体验证:
|
||||
1. dotnet build 编译通过
|
||||
2. 手动调用 POST /api/console/gateway/reload 返回成功
|
||||
3. PostgreSQL 数据库能收到 NOTIFY 消息
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [x] ConfigNotificationService 改为使用 DbContext 获取连接字符串
|
||||
- [ ] INotificationService 接口定义完成
|
||||
- [ ] PgSqlNotificationService 实现完成
|
||||
- [ ] GatewayService 集成通知服务
|
||||
- [ ] ReloadGatewayAsync 触发广播
|
||||
- [ ] CRUD 操作自动触发广播
|
||||
- [ ] 编译通过
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 .planning/phases/01-gateway-config-management/01-SUMMARY.md
|
||||
</output>
|
||||
@ -1,83 +0,0 @@
|
||||
# Phase 1: 实现 Gateway 配置管理及事件推送 - 执行摘要
|
||||
|
||||
**完成日期:** 2026-03-02
|
||||
**状态:** ✓ Complete
|
||||
|
||||
## 执行结果
|
||||
|
||||
| Plan | 任务 | 状态 |
|
||||
|------|------|------|
|
||||
| 01 | Task 1: 创建配置通知服务 | ✓ |
|
||||
| 01 | Task 2: 修改 GatewayService 集成通知服务 | ✓ |
|
||||
|
||||
## 实现的功能
|
||||
|
||||
### 1. 配置通知服务 (ConfigNotificationService.cs)
|
||||
|
||||
**创建/修改的文件:**
|
||||
- `src/Services/ConfigNotificationService.cs`
|
||||
|
||||
**包含:**
|
||||
- `INotificationService` 接口 - 通知服务抽象
|
||||
- `PgSqlNotificationService` 实现 - 使用 PostgreSQL NOTIFY 机制
|
||||
- `ConfigChangeEvent` - 配置变更事件数据模型
|
||||
- 通知通道: `gateway_config_changed`
|
||||
|
||||
**实现细节:**
|
||||
- 使用 `DbContextOptions<ConsoleDbContext>` 获取连接字符串(而非直接从配置文件读取)
|
||||
- 通过反射从 EF Core Npgsql 扩展中提取连接字符串
|
||||
|
||||
**创建的文件:**
|
||||
- `src/Services/ConfigNotificationService.cs`
|
||||
|
||||
**包含:**
|
||||
- `INotificationService` 接口 - 通知服务抽象
|
||||
- `PgSqlNotificationService` 实现 - 使用 PostgreSQL NOTIFY 机制
|
||||
- `ConfigChangeEvent` - 配置变更事件数据模型
|
||||
- 通知通道: `gateway_config_changed`
|
||||
|
||||
**事件格式:**
|
||||
```json
|
||||
{
|
||||
"eventType": "service|route|instance|gateway",
|
||||
"action": "create|update|delete|reload",
|
||||
"timestamp": "2026-03-02T12:00:00Z",
|
||||
"details": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. GatewayService 集成
|
||||
|
||||
**修改的文件:**
|
||||
- `src/Services/GatewayService.cs` - 添加 INotificationService 依赖
|
||||
- `src/Program.cs` - 注册 NotificationService
|
||||
|
||||
**自动广播触发点:**
|
||||
- `RegisterServiceAsync` - 服务注册时
|
||||
- `UnregisterServiceAsync` - 服务注销时
|
||||
- `CreateRouteAsync` - 路由创建时
|
||||
- `AddInstanceAsync` - 实例添加时
|
||||
- `RemoveInstanceAsync` - 实例删除时
|
||||
- `UpdateInstanceWeightAsync` - 权重更新时
|
||||
- `ReloadGatewayAsync` - 手动触发重载时
|
||||
|
||||
## 验证
|
||||
|
||||
- [x] dotnet build 编译通过
|
||||
- [x] INotificationService 接口定义完成
|
||||
- [x] PgSqlNotificationService 实现完成
|
||||
- [x] GatewayService 集成通知服务
|
||||
- [x] ReloadGatewayAsync 触发广播
|
||||
- [x] CRUD 操作自动触发广播
|
||||
|
||||
## 下游使用
|
||||
|
||||
下游 Gateway (yarpgateway) 需要实现:
|
||||
1. 监听 `gateway_config_changed` 通道
|
||||
2. 收到通知后重新加载配置
|
||||
|
||||
---
|
||||
|
||||
*Phase: 01-gateway-config-management*
|
||||
*Plan: 01*
|
||||
*Executed: 2026-03-02*
|
||||
@ -1,27 +0,0 @@
|
||||
# Phase 2: 实现 Gateway 插件系统
|
||||
|
||||
- **目标**: 实现 YARP 网关的插件系统,包括 Web UI 管理界面和动态编译加载功能
|
||||
- **状态**: Not planned yet
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
实现 YARP 网关的插件系统规划与实现,包括:
|
||||
|
||||
- Web UI 管理界面(路由管理、集群管理、插件管理)
|
||||
- 在线 C# 代码编辑(Monaco Editor)
|
||||
- 动态编译加载(Roslyn)
|
||||
- 插件生命周期管理
|
||||
|
||||
## Depends on
|
||||
|
||||
- Phase 1: 实现 Gateway 配置管理及事件推送
|
||||
|
||||
## Plans
|
||||
|
||||
- [ ] 02-PLAN.md — 实施计划
|
||||
|
||||
---
|
||||
|
||||
*相关文档:.planning/docs/gateway-plugin-system.md*
|
||||
@ -1,74 +0,0 @@
|
||||
# Phase 3: 网关配置变更广播机制 - Context
|
||||
|
||||
**Gathered:** 2026-03-03
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
分析现有的网关配置广播机制,梳理路由→服务→下游的完整链路,确定配置变更时的广播策略。
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### 广播触发策略
|
||||
- **仅手动触发**:所有 CRUD 操作(路由、集群、实例、权重)不自动广播
|
||||
- 下游需要刷新时,手动调用 POST /api/console/gateway/reload
|
||||
- 事件只通知"需要刷新",不包含具体变更内容
|
||||
- 下游收到通知后,自行查询数据库刷新配置
|
||||
|
||||
### 广播事件格式
|
||||
- 通道:`gateway_config_changed`
|
||||
- 事件内容:只包含 action: "reload",不含具体变更详情
|
||||
- 下游逻辑:收到通知 → 查询数据库 → 刷新内存缓存
|
||||
|
||||
### 需分析的现有代码
|
||||
- ConfigNotificationService.cs - 已实现的 NOTIFY 机制
|
||||
- GatewayService.cs - 需集成通知服务
|
||||
- GatewayController.cs - /reload 接口
|
||||
|
||||
### Claude's Discretion
|
||||
- 自动触发 vs 手动触发的具体实现方式
|
||||
- 广播失败时的错误处理策略
|
||||
- 日志记录细节
|
||||
|
||||
</decisions>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- ConfigNotificationService.cs: PostgreSQL NOTIFY 机制已实现
|
||||
- INotificationService 接口: 可直接复用
|
||||
|
||||
### Established Patterns
|
||||
- 使用 PgSqlNotificationService 发布通知
|
||||
- 通道名称: `gateway_config_changed`
|
||||
|
||||
### Integration Points
|
||||
- GatewayService 需注入 INotificationService
|
||||
- ReloadGatewayAsync 需调用通知服务
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- 事件 payload 尽量精简,只传递 "reload" action
|
||||
- 下游网关监听同一数据库连接,收到 NOTIFY 后刷新
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- 自动触发广播(未来可选优化)
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 03-gateway-config-broadcast*
|
||||
*Context gathered: 2026-03-03*
|
||||
@ -1,112 +0,0 @@
|
||||
---
|
||||
phase: 03-gateway-config-broadcast
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified: []
|
||||
autonomous: true
|
||||
requirements: []
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "现有广播机制已文档化"
|
||||
- "路由 -> 服务 -> 下游流程已理解"
|
||||
- "配置变更事件已验证可用"
|
||||
artifacts:
|
||||
- path: ".planning/phases/03-gateway-config-broadcast/03-SUMMARY.md"
|
||||
provides: "阶段执行摘要"
|
||||
key_links: []
|
||||
---
|
||||
|
||||
<objective>
|
||||
分析和文档化现有的网关配置广播机制。理解从路由配置到下游服务的完整链路,并验证配置变更事件广播是否正常工作。
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
@.planning/phases/01-gateway-config-management/01-SUMMARY.md
|
||||
@.planning/phases/01-gateway-config-management/01-PLAN.md
|
||||
|
||||
## 现有实现(来自 Phase 1)
|
||||
|
||||
广播机制使用 PostgreSQL NOTIFY:
|
||||
- **通道:** `gateway_config_changed`
|
||||
- **事件类型:** service, route, instance, gateway
|
||||
- **操作:** create, update, delete, reload
|
||||
- **服务:** ConfigNotificationService.cs
|
||||
- **集成:** GatewayService.cs 在所有 CRUD 操作时触发广播
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 1: 分析现有广播实现</name>
|
||||
<files>src/Services/ConfigNotificationService.cs, src/Services/GatewayService.cs</files>
|
||||
<action>
|
||||
分析现有实现以了解:
|
||||
1. ConfigNotificationService 如何工作(PostgreSQL NOTIFY)
|
||||
2. GatewayService 如何在 CRUD 操作时触发广播
|
||||
3. 发送的事件类型和载荷是什么
|
||||
|
||||
阅读源代码并记录发现。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>文件存在且包含通知逻辑</automated>
|
||||
</verify>
|
||||
<done>实现分析完成,发现已记录</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 2: 绘制路由 -> 服务 -> 下游流程</name>
|
||||
<files></files>
|
||||
<action>
|
||||
文档化完整配置链路:
|
||||
1. 路由如何在 Console 中定义
|
||||
2. 路由如何映射到服务
|
||||
3. 下游 Gateway 如何发现服务
|
||||
4. 配置变更时,广播如何到达下游
|
||||
|
||||
参考 src/Models/、src/Services/、src/Controllers/ 中的现有代码
|
||||
</action>
|
||||
<verify>
|
||||
<automated>流程文档已创建</automated>
|
||||
</verify>
|
||||
<done>配置链路已文档化</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 3: 验证广播端到端工作</name>
|
||||
<files></files>
|
||||
<action>
|
||||
验证广播机制:
|
||||
1. 检查 PostgreSQL LISTEN/NOTIFY 是否正确配置
|
||||
2. 验证 ReloadGatewayAsync 发送正确事件
|
||||
3. 确认所有 CRUD 操作(服务/路由/实例)都触发广播
|
||||
4. 如可能,测试端到端流程
|
||||
</action>
|
||||
<verify>
|
||||
<automated>编译成功,API 端点可用</automated>
|
||||
</verify>
|
||||
<done>广播验证完成</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. 阅读并分析 ConfigNotificationService.cs
|
||||
2. 阅读并分析 GatewayService.cs
|
||||
3. 文档化路由 -> 服务 -> 下游流程
|
||||
4. 验证编译通过
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [x] 现有广播实现已分析
|
||||
- [x] 配置链路已文档化
|
||||
- [x] 广播事件已验证
|
||||
- [x] 摘要已创建
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 `.planning/phases/03-gateway-config-broadcast/03-SUMMARY.md`
|
||||
</output>
|
||||
@ -1,128 +0,0 @@
|
||||
---
|
||||
phase: 04-gateway-entity-update
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Services/GatewayService.cs
|
||||
- src/Program.cs
|
||||
autonomous: true
|
||||
requirements: []
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "编译错误已修复"
|
||||
- "GatewayService 使用新的 IClusterStore 接口"
|
||||
- "实例操作改为通过 Cluster.Destinations 管理"
|
||||
artifacts:
|
||||
- path: "src/Services/GatewayService.cs"
|
||||
provides: "GatewayService 使用 IClusterStore"
|
||||
- path: "src/Program.cs"
|
||||
provides: "依赖注入更新"
|
||||
key_links:
|
||||
- from: "GatewayService"
|
||||
to: "IClusterStore"
|
||||
via: "依赖注入"
|
||||
---
|
||||
|
||||
<objective>
|
||||
适配 Platform 1.0.12 中的 Gateway 实体重构,修复编译错误,更新 Console 代码以使用新的 GwCluster/GwDestination/GwTenantRoute 模型。
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
@.planning/docs/gateway-entity-changes-1.0.12.md
|
||||
|
||||
## 编译错误
|
||||
|
||||
当前编译错误:
|
||||
```
|
||||
error CS0246: IInstanceStore 找不到
|
||||
```
|
||||
|
||||
## 变更摘要
|
||||
|
||||
1. **IInstanceStore 已移除** - 实例现在是 GwCluster 的内嵌对象
|
||||
2. **IClusterStore 是新接口** - 包含 Destination 管理方法
|
||||
3. **数据模型变化**:
|
||||
- GatewayInstance → GwDestination(内嵌值对象)
|
||||
- GatewayCluster → GwCluster(聚合根,包含 Destinations)
|
||||
- 路由通过 ClusterId 关联到集群
|
||||
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 1: 更新 Program.cs 依赖注入</name>
|
||||
<files>src/Program.cs</files>
|
||||
<action>
|
||||
1. 移除 IInstanceStore 的注入(如果有)
|
||||
2. 添加 IClusterStore 的注入:
|
||||
```csharp
|
||||
builder.Services.AddScoped<IClusterStore, ClusterStore<PlatformDbContext>>();
|
||||
```
|
||||
3. 确保使用正确的 PlatformDbContext
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build --no-restore 2>&1 | head -30</automated>
|
||||
</verify>
|
||||
<done>Program.cs 依赖注入已更新</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 2: 更新 GatewayService 使用 IClusterStore</name>
|
||||
<files>src/Services/GatewayService.cs</files>
|
||||
<action>
|
||||
1. 移除 IInstanceStore 依赖
|
||||
2. 添加 IClusterStore 依赖注入
|
||||
3. 更新实例相关方法:
|
||||
- GetInstancesAsync → 从 Cluster.Destinations 获取
|
||||
- AddInstanceAsync → 使用 IClusterStore.AddDestinationAsync
|
||||
- RemoveInstanceAsync → 使用 IClusterStore.RemoveDestinationAsync
|
||||
- UpdateInstanceWeightAsync → 使用 IClusterStore.UpdateDestinationAsync
|
||||
4. 更新数据模型映射:
|
||||
- GatewayInstanceDto → 从 GwDestination 映射
|
||||
- GatewayClusterDto → 从 GwCluster 映射
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build --no-restore 2>&1 | head -30</automated>
|
||||
</verify>
|
||||
<done>GatewayService 已更新为使用 IClusterStore</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>任务 3: 验证编译通过</name>
|
||||
<files></files>
|
||||
<action>
|
||||
运行完整编译验证:
|
||||
```bash
|
||||
dotnet build src/Fengling.Console.csproj
|
||||
```
|
||||
确保没有编译错误。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>dotnet build src/Fengling.Console.csproj 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>编译通过,无错误</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. dotnet build 编译通过
|
||||
2. GatewayService 使用 IClusterStore
|
||||
3. 实例操作通过 Cluster.Destinations 管理
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [x] IInstanceStore 依赖已移除
|
||||
- [x] IClusterStore 已集成
|
||||
- [x] 编译错误已修复
|
||||
- [x] GatewayService 功能正常
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
完成后创建 `.planning/phases/04-gateway-entity-update/04-SUMMARY.md`
|
||||
</output>
|
||||
@ -1,78 +0,0 @@
|
||||
# Phase 4 总结:适配 Platform 1.0.12 Gateway 实体变更
|
||||
|
||||
## 概述
|
||||
|
||||
本次 Phase 4 成功完成了 Fengling Console 对 Platform 1.0.12 Gateway 实体变更的适配工作。
|
||||
|
||||
## 主要变更
|
||||
|
||||
### 1. Program.cs 依赖注入更新
|
||||
|
||||
**变更内容:**
|
||||
- 移除了 `IInstanceStore` 和 `InstanceStore` 的注册
|
||||
- 保留了 `IClusterStore` 和 `ClusterStore` 的注册
|
||||
|
||||
**变更原因:**
|
||||
Platform 1.0.12 移除了 `IInstanceStore` 接口,实例(Destination)现在是 `GwCluster` 的内嵌对象。
|
||||
|
||||
### 2. ConsoleDbContext 实体配置清理
|
||||
|
||||
**变更内容:**
|
||||
- 移除了 `GwTenant` 实体配置(原平台中已移除)
|
||||
- 移除了 `GwServiceInstance` 实体配置(已重构为 GwDestination)
|
||||
|
||||
### 3. GatewayService 实体属性适配
|
||||
|
||||
**变更内容:**
|
||||
|
||||
| 旧属性 | 新属性 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `GwTenantRoute.PathPattern` (string) | `GwTenantRoute.Match.Path` ( GwRouteMatch.Path ) | 路由匹配配置从简单字符串升级为复杂对象 |
|
||||
| `Status = RouteStatus.Active` (enum) | `Status = (int)RouteStatus.Active` (int) | Status 字段为 int 类型,需要显式转换枚举 |
|
||||
|
||||
**具体代码变更:**
|
||||
|
||||
```csharp
|
||||
// 旧代码
|
||||
new GwTenantRoute
|
||||
{
|
||||
PathPattern = pathPattern,
|
||||
Status = RouteStatus.Active,
|
||||
}
|
||||
|
||||
// 新代码
|
||||
new GwTenantRoute
|
||||
{
|
||||
Match = new GwRouteMatch { Path = pathPattern },
|
||||
Status = (int)RouteStatus.Active,
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 旧代码读取
|
||||
r.PathPattern
|
||||
r.Status
|
||||
|
||||
// 新代码读取
|
||||
r.Match.Path
|
||||
r.Status (已是 int)
|
||||
```
|
||||
|
||||
## 编译结果
|
||||
|
||||
✅ 编译成功,0 个错误,3 个警告(警告为预存在的代码质量问题,与本次变更无关)
|
||||
|
||||
## 验证
|
||||
|
||||
- [x] `dotnet build` 通过
|
||||
- [x] 无新增编译错误
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 实体变更详情:`.planning/docs/gateway-entity-changes-1.0.12.md`
|
||||
|
||||
## 下一步
|
||||
|
||||
可以考虑的改进:
|
||||
1. 修复 TenantService.cs 中的警告(roleManager 参数未使用)
|
||||
2. 完善 GatewayService 中的空值处理(Match 可能为 null)
|
||||
@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Microsoft Packages -->
|
||||
<PackageVersion Include="Fengling.Platform.Infrastructure" Version="1.0.12" />
|
||||
<PackageVersion Include="Fengling.Platform.Infrastructure" Version="1.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
|
||||
|
||||
@ -24,11 +24,21 @@ public class ConsoleDbContext : PlatformDbContext
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// ========== Gateway 模块 ==========
|
||||
modelBuilder.Entity<GwTenant>(entity =>
|
||||
{
|
||||
entity.ToTable("gw_tenants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<GwTenantRoute>(entity =>
|
||||
{
|
||||
entity.ToTable("gw_tenant_routes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<GwServiceInstance>(entity =>
|
||||
{
|
||||
entity.ToTable("gw_service_instances");
|
||||
});
|
||||
|
||||
// ========== Tenant 模块 ==========
|
||||
modelBuilder.Entity<Tenant>(entity =>
|
||||
{
|
||||
|
||||
254
src/Fengling.Console/Migrations/sql/initial.sql
Normal file
254
src/Fengling.Console/Migrations/sql/initial.sql
Normal file
@ -0,0 +1,254 @@
|
||||
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 gw_service_instances (
|
||||
"Id" text NOT NULL,
|
||||
"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_gw_service_instances" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE gw_tenant_routes (
|
||||
"Id" text NOT NULL,
|
||||
"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,
|
||||
"IsGlobal" boolean 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_gw_tenant_routes" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE gw_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_gw_tenants" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE idn_roles (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"Description" character varying(200),
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"TenantId" bigint,
|
||||
"IsSystem" boolean NOT NULL,
|
||||
"DisplayName" text,
|
||||
"Permissions" text[],
|
||||
"Name" character varying(256),
|
||||
"NormalizedName" character varying(256),
|
||||
"ConcurrencyStamp" text,
|
||||
CONSTRAINT "PK_idn_roles" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE idn_users (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"RealName" text NOT NULL,
|
||||
"TenantId" bigint,
|
||||
"TenantCode" text,
|
||||
"TenantName" text,
|
||||
"CreatedTime" timestamp with time zone NOT NULL,
|
||||
"UpdatedTime" timestamp with time zone,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"UserName" character varying(256),
|
||||
"NormalizedUserName" character varying(256),
|
||||
"Email" character varying(256),
|
||||
"NormalizedEmail" character varying(256),
|
||||
"EmailConfirmed" boolean NOT NULL,
|
||||
"PasswordHash" text,
|
||||
"SecurityStamp" text,
|
||||
"ConcurrencyStamp" text,
|
||||
"PhoneNumber" character varying(20),
|
||||
"PhoneNumberConfirmed" boolean NOT NULL,
|
||||
"TwoFactorEnabled" boolean NOT NULL,
|
||||
"LockoutEnd" timestamp with time zone,
|
||||
"LockoutEnabled" boolean NOT NULL,
|
||||
"AccessFailedCount" integer NOT NULL,
|
||||
CONSTRAINT "PK_idn_users" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE sys_access_logs (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"UserName" character varying(50),
|
||||
"TenantId" character varying(50),
|
||||
"Action" character varying(20) NOT NULL,
|
||||
"Resource" character varying(200),
|
||||
"Method" character varying(10),
|
||||
"IpAddress" character varying(50),
|
||||
"UserAgent" character varying(500),
|
||||
"Status" character varying(20) NOT NULL,
|
||||
"Duration" integer NOT NULL,
|
||||
"RequestData" text,
|
||||
"ResponseData" text,
|
||||
"ErrorMessage" text,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_sys_access_logs" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE sys_audit_logs (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"Operator" character varying(50) NOT NULL,
|
||||
"TenantId" character varying(50),
|
||||
"Operation" character varying(20) NOT NULL,
|
||||
"Action" character varying(20) NOT NULL,
|
||||
"TargetType" character varying(50),
|
||||
"TargetId" bigint,
|
||||
"TargetName" character varying(100),
|
||||
"IpAddress" character varying(50) NOT NULL,
|
||||
"Description" character varying(500),
|
||||
"OldValue" text,
|
||||
"NewValue" text,
|
||||
"ErrorMessage" text,
|
||||
"Status" character varying(20) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_sys_audit_logs" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE sys_tenants (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||
"TenantCode" character varying(50) NOT NULL,
|
||||
"Name" character varying(100) NOT NULL,
|
||||
"ContactName" character varying(50) NOT NULL,
|
||||
"ContactEmail" character varying(100) NOT NULL,
|
||||
"ContactPhone" character varying(20),
|
||||
"MaxUsers" integer,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
"UpdatedAt" timestamp with time zone,
|
||||
"ExpiresAt" timestamp with time zone,
|
||||
"Description" character varying(500),
|
||||
"Status" integer NOT NULL,
|
||||
"IsDeleted" boolean NOT NULL,
|
||||
"RowVersion" bigint NOT NULL,
|
||||
CONSTRAINT "PK_sys_tenants" PRIMARY KEY ("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE idn_role_claims (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"RoleId" bigint NOT NULL,
|
||||
"ClaimType" text,
|
||||
"ClaimValue" text,
|
||||
CONSTRAINT "PK_idn_role_claims" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_idn_role_claims_idn_roles_RoleId" FOREIGN KEY ("RoleId") REFERENCES idn_roles ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE idn_user_claims (
|
||||
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
"UserId" bigint NOT NULL,
|
||||
"ClaimType" text,
|
||||
"ClaimValue" text,
|
||||
CONSTRAINT "PK_idn_user_claims" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_idn_user_claims_idn_users_UserId" FOREIGN KEY ("UserId") REFERENCES idn_users ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE idn_user_logins (
|
||||
"LoginProvider" text NOT NULL,
|
||||
"ProviderKey" text NOT NULL,
|
||||
"ProviderDisplayName" text,
|
||||
"UserId" bigint NOT NULL,
|
||||
CONSTRAINT "PK_idn_user_logins" PRIMARY KEY ("LoginProvider", "ProviderKey"),
|
||||
CONSTRAINT "FK_idn_user_logins_idn_users_UserId" FOREIGN KEY ("UserId") REFERENCES idn_users ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE idn_user_roles (
|
||||
"UserId" bigint NOT NULL,
|
||||
"RoleId" bigint NOT NULL,
|
||||
CONSTRAINT "PK_idn_user_roles" PRIMARY KEY ("UserId", "RoleId"),
|
||||
CONSTRAINT "FK_idn_user_roles_idn_roles_RoleId" FOREIGN KEY ("RoleId") REFERENCES idn_roles ("Id") ON DELETE CASCADE,
|
||||
CONSTRAINT "FK_idn_user_roles_idn_users_UserId" FOREIGN KEY ("UserId") REFERENCES idn_users ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE idn_user_tokens (
|
||||
"UserId" bigint NOT NULL,
|
||||
"LoginProvider" text NOT NULL,
|
||||
"Name" text NOT NULL,
|
||||
"Value" text,
|
||||
CONSTRAINT "PK_idn_user_tokens" PRIMARY KEY ("UserId", "LoginProvider", "Name"),
|
||||
CONSTRAINT "FK_idn_user_tokens_idn_users_UserId" FOREIGN KEY ("UserId") REFERENCES idn_users ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_gw_service_instances_ClusterId_DestinationId" ON gw_service_instances ("ClusterId", "DestinationId");
|
||||
|
||||
CREATE INDEX "IX_gw_service_instances_Health" ON gw_service_instances ("Health");
|
||||
|
||||
CREATE INDEX "IX_gw_tenant_routes_ClusterId" ON gw_tenant_routes ("ClusterId");
|
||||
|
||||
CREATE INDEX "IX_gw_tenant_routes_ServiceName" ON gw_tenant_routes ("ServiceName");
|
||||
|
||||
CREATE INDEX "IX_gw_tenant_routes_ServiceName_IsGlobal_Status" ON gw_tenant_routes ("ServiceName", "IsGlobal", "Status");
|
||||
|
||||
CREATE INDEX "IX_gw_tenant_routes_TenantCode" ON gw_tenant_routes ("TenantCode");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_gw_tenants_TenantCode" ON gw_tenants ("TenantCode");
|
||||
|
||||
CREATE INDEX "IX_idn_role_claims_RoleId" ON idn_role_claims ("RoleId");
|
||||
|
||||
CREATE UNIQUE INDEX "RoleNameIndex" ON idn_roles ("NormalizedName");
|
||||
|
||||
CREATE INDEX "IX_idn_user_claims_UserId" ON idn_user_claims ("UserId");
|
||||
|
||||
CREATE INDEX "IX_idn_user_logins_UserId" ON idn_user_logins ("UserId");
|
||||
|
||||
CREATE INDEX "IX_idn_user_roles_RoleId" ON idn_user_roles ("RoleId");
|
||||
|
||||
CREATE INDEX "EmailIndex" ON idn_users ("NormalizedEmail");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_idn_users_PhoneNumber" ON idn_users ("PhoneNumber");
|
||||
|
||||
CREATE UNIQUE INDEX "UserNameIndex" ON idn_users ("NormalizedUserName");
|
||||
|
||||
CREATE INDEX "IX_sys_access_logs_Action" ON sys_access_logs ("Action");
|
||||
|
||||
CREATE INDEX "IX_sys_access_logs_CreatedAt" ON sys_access_logs ("CreatedAt");
|
||||
|
||||
CREATE INDEX "IX_sys_access_logs_Status" ON sys_access_logs ("Status");
|
||||
|
||||
CREATE INDEX "IX_sys_access_logs_TenantId" ON sys_access_logs ("TenantId");
|
||||
|
||||
CREATE INDEX "IX_sys_access_logs_UserName" ON sys_access_logs ("UserName");
|
||||
|
||||
CREATE INDEX "IX_sys_audit_logs_Action" ON sys_audit_logs ("Action");
|
||||
|
||||
CREATE INDEX "IX_sys_audit_logs_CreatedAt" ON sys_audit_logs ("CreatedAt");
|
||||
|
||||
CREATE INDEX "IX_sys_audit_logs_Operation" ON sys_audit_logs ("Operation");
|
||||
|
||||
CREATE INDEX "IX_sys_audit_logs_Operator" ON sys_audit_logs ("Operator");
|
||||
|
||||
CREATE INDEX "IX_sys_audit_logs_TenantId" ON sys_audit_logs ("TenantId");
|
||||
|
||||
CREATE INDEX "IX_sys_tenants_Status" ON sys_tenants ("Status");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_sys_tenants_TenantCode" ON sys_tenants ("TenantCode");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20260301040647_Initial', '10.0.3');
|
||||
|
||||
COMMIT;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using Fengling.Console.Data;
|
||||
using Fengling.Console.Services;
|
||||
using Fengling.Console.Services;
|
||||
using Fengling.Platform.Domain.AggregatesModel.UserAggregate;
|
||||
using Fengling.Platform.Domain.AggregatesModel.RoleAggregate;
|
||||
using Fengling.Platform.Infrastructure;
|
||||
@ -56,8 +57,11 @@ builder.Services.AddScoped<ITenantManager, TenantManager>();
|
||||
|
||||
// Register Gateway managers
|
||||
builder.Services.AddScoped<IRouteStore, RouteStore<ConsoleDbContext>>();
|
||||
builder.Services.AddScoped<IClusterStore, ClusterStore<PlatformDbContext>>();
|
||||
builder.Services.AddScoped<IInstanceStore, InstanceStore<ConsoleDbContext>>();
|
||||
builder.Services.AddScoped<IInstanceStore, InstanceStore<PlatformDbContext>>();
|
||||
builder.Services.AddScoped<IRouteManager, RouteManager>();
|
||||
builder.Services.AddScoped<ITenantStore, TenantStore<ConsoleDbContext>>();
|
||||
builder.Services.AddScoped<ITenantManager, TenantManager>();
|
||||
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<ITenantService, TenantService>();
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using Fengling.Console.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Fengling.Console.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 配置变更通知服务接口
|
||||
/// </summary>
|
||||
public interface INotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 发布通知到指定通道
|
||||
/// </summary>
|
||||
Task PublishAsync(string channel, string payload, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发布配置变更事件
|
||||
/// </summary>
|
||||
Task PublishConfigChangeAsync(
|
||||
string eventType,
|
||||
string action,
|
||||
object? details = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置变更事件数据
|
||||
/// </summary>
|
||||
public class ConfigChangeEvent
|
||||
{
|
||||
public string EventType { get; set; } = ""; // service, route, instance, gateway
|
||||
public string Action { get; set; } = ""; // create, update, delete, reload
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
public object? Details { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL 通知服务实现
|
||||
/// 使用 PostgreSQL NOTIFY/LISTEN 机制广播配置变更
|
||||
/// </summary>
|
||||
public class PgSqlNotificationService : INotificationService
|
||||
{
|
||||
private readonly ILogger<PgSqlNotificationService> _logger;
|
||||
private readonly string _connectionString;
|
||||
|
||||
public const string GatewayConfigChangedChannel = "gateway_config_changed";
|
||||
|
||||
public PgSqlNotificationService(DbContextOptions<ConsoleDbContext> dbContextOptions, ILogger<PgSqlNotificationService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// 从 DbContextOptions 获取连接字符串
|
||||
string? connectionString = null;
|
||||
|
||||
foreach (var ext in dbContextOptions.Extensions)
|
||||
{
|
||||
var extType = ext.GetType();
|
||||
if (extType.Name.Contains("Npgsql"))
|
||||
{
|
||||
var prop = extType.GetProperty("ConnectionString");
|
||||
if (prop != null && prop.PropertyType == typeof(string))
|
||||
{
|
||||
connectionString = prop.GetValue(ext) as string;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_connectionString = connectionString
|
||||
?? throw new InvalidOperationException("DefaultConnection not configured");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PublishAsync(string channel, string payload, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
await using var cmd = new NpgsqlCommand(
|
||||
$"SELECT pg_notify(@channel, @payload)",
|
||||
connection);
|
||||
|
||||
cmd.Parameters.AddWithValue("channel", channel);
|
||||
cmd.Parameters.AddWithValue("payload", payload);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Published notification to channel '{Channel}': {Payload}", channel, payload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to publish notification to channel '{Channel}'", channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建配置变更事件并发布
|
||||
/// </summary>
|
||||
public async Task PublishConfigChangeAsync(
|
||||
string eventType,
|
||||
string action,
|
||||
object? details = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var configEvent = new ConfigChangeEvent
|
||||
{
|
||||
EventType = eventType,
|
||||
Action = action,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Details = details
|
||||
};
|
||||
|
||||
var payload = JsonSerializer.Serialize(configEvent);
|
||||
await PublishAsync(GatewayConfigChangedChannel, payload, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知服务扩展方法
|
||||
/// </summary>
|
||||
public static class NotificationServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddNotificationService(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<INotificationService, PgSqlNotificationService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@ -23,46 +23,38 @@ public interface IGatewayService
|
||||
public class GatewayService : IGatewayService
|
||||
{
|
||||
private readonly IRouteStore _routeStore;
|
||||
private readonly IClusterStore _clusterStore;
|
||||
private readonly IInstanceStore _instanceStore;
|
||||
private readonly ILogger<GatewayService> _logger;
|
||||
|
||||
public GatewayService(
|
||||
IRouteStore routeStore,
|
||||
IClusterStore clusterStore,
|
||||
IInstanceStore instanceStore,
|
||||
ILogger<GatewayService> logger)
|
||||
{
|
||||
_routeStore = routeStore;
|
||||
_clusterStore = clusterStore;
|
||||
_instanceStore = instanceStore;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GatewayStatisticsDto> GetStatisticsAsync()
|
||||
{
|
||||
var routes = await _routeStore.GetAllAsync();
|
||||
var clusters = await _clusterStore.GetAllAsync();
|
||||
var instances = await _instanceStore.GetAllAsync();
|
||||
|
||||
var activeRoutes = routes.Where(r => !r.IsDeleted).ToList();
|
||||
|
||||
// Count destinations from all clusters
|
||||
var totalInstances = clusters
|
||||
.Where(c => !c.IsDeleted)
|
||||
.Sum(c => c.Destinations?.Count(d => d.Status == 1) ?? 0);
|
||||
|
||||
var healthyInstances = clusters
|
||||
.Where(c => !c.IsDeleted)
|
||||
.Sum(c => c.Destinations?.Count(d => d.HealthStatus == 1) ?? 0);
|
||||
var activeInstances = instances.Where(i => !i.IsDeleted).ToList();
|
||||
|
||||
return new GatewayStatisticsDto
|
||||
{
|
||||
TotalServices = activeRoutes.Select(r => r.ServiceName).Distinct().Count(),
|
||||
GlobalRoutes = activeRoutes.Count(r => r.IsGlobal),
|
||||
TenantRoutes = activeRoutes.Count(r => !r.IsGlobal),
|
||||
TotalInstances = totalInstances,
|
||||
HealthyInstances = healthyInstances,
|
||||
TotalInstances = activeInstances.Count,
|
||||
HealthyInstances = activeInstances.Count(i => i.Health == (int)InstanceHealth.Healthy),
|
||||
RecentServices = activeRoutes
|
||||
.OrderByDescending(r => r.CreatedTime)
|
||||
.Take(5)
|
||||
.Select(r => MapToServiceDto(r, 0))
|
||||
.Select(MapToServiceDto)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
@ -70,7 +62,7 @@ public class GatewayService : IGatewayService
|
||||
public async Task<List<GatewayServiceDto>> GetServicesAsync(bool globalOnly = false, string? tenantCode = null)
|
||||
{
|
||||
var routes = await _routeStore.GetAllAsync();
|
||||
var clusters = await _clusterStore.GetAllAsync();
|
||||
var instances = await _instanceStore.GetAllAsync();
|
||||
|
||||
var query = routes.Where(r => !r.IsDeleted);
|
||||
|
||||
@ -80,14 +72,12 @@ public class GatewayService : IGatewayService
|
||||
query = query.Where(r => r.TenantCode == tenantCode);
|
||||
|
||||
var routeList = query.OrderByDescending(r => r.CreatedTime).ToList();
|
||||
var clusters = routeList.Select(r => r.ClusterId).Distinct().ToList();
|
||||
|
||||
// Build instance count dict from clusters
|
||||
var instancesDict = clusters
|
||||
.Where(c => !c.IsDeleted && routeList.Any(r => r.ClusterId == c.ClusterId))
|
||||
.ToDictionary(
|
||||
c => c.ClusterId,
|
||||
c => c.Destinations?.Count(d => d.Status == 1) ?? 0
|
||||
);
|
||||
var instancesDict = instances
|
||||
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted)
|
||||
.GroupBy(i => i.ClusterId)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
return routeList.Select(r => MapToServiceDto(r, instancesDict.GetValueOrDefault(r.ClusterId, 0))).ToList();
|
||||
}
|
||||
@ -102,8 +92,8 @@ public class GatewayService : IGatewayService
|
||||
|
||||
if (route == null) return null;
|
||||
|
||||
var cluster = await _clusterStore.FindByClusterIdAsync(route.ClusterId);
|
||||
var instanceCount = cluster?.Destinations?.Count(d => d.Status == 1) ?? 0;
|
||||
var instances = await _instanceStore.GetAllAsync();
|
||||
var instanceCount = instances.Count(i => i.ClusterId == route.ClusterId && !i.IsDeleted);
|
||||
|
||||
return MapToServiceDto(route, instanceCount);
|
||||
}
|
||||
@ -128,30 +118,20 @@ public class GatewayService : IGatewayService
|
||||
throw new InvalidOperationException($"Service {dto.ServicePrefix} already registered");
|
||||
}
|
||||
|
||||
// Create or get cluster
|
||||
var cluster = await _clusterStore.FindByClusterIdAsync(clusterId);
|
||||
if (cluster == null)
|
||||
// Add instance
|
||||
var instanceId = Guid.CreateVersion7().ToString("N");
|
||||
var instance = new GwServiceInstance
|
||||
{
|
||||
cluster = new GwCluster
|
||||
{
|
||||
Id = Guid.CreateVersion7().ToString("N"),
|
||||
Id = instanceId,
|
||||
ClusterId = clusterId,
|
||||
Name = $"{dto.ServicePrefix} Service",
|
||||
Destinations = new List<GwDestination>()
|
||||
};
|
||||
await _clusterStore.CreateAsync(cluster);
|
||||
}
|
||||
|
||||
// Add destination to cluster
|
||||
var destination = new GwDestination
|
||||
{
|
||||
DestinationId = destinationId,
|
||||
Address = dto.ServiceAddress,
|
||||
Weight = dto.Weight,
|
||||
HealthStatus = 1, // Healthy
|
||||
Status = 1 // Active
|
||||
Health = (int)InstanceHealth.Healthy,
|
||||
Status = (int)InstanceStatus.Active,
|
||||
CreatedTime = DateTime.UtcNow
|
||||
};
|
||||
await _clusterStore.AddDestinationAsync(clusterId, destination);
|
||||
await _instanceStore.CreateAsync(instance);
|
||||
|
||||
// Add route
|
||||
var routeId = Guid.CreateVersion7().ToString("N");
|
||||
@ -161,7 +141,7 @@ public class GatewayService : IGatewayService
|
||||
TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "",
|
||||
ServiceName = dto.ServicePrefix,
|
||||
ClusterId = clusterId,
|
||||
Match = new GwRouteMatch { Path = pathPattern },
|
||||
PathPattern = pathPattern,
|
||||
Priority = dto.IsGlobal ? 0 : 10,
|
||||
Status = (int)RouteStatus.Active,
|
||||
IsGlobal = dto.IsGlobal,
|
||||
@ -189,8 +169,16 @@ public class GatewayService : IGatewayService
|
||||
route.UpdatedTime = DateTime.UtcNow;
|
||||
await _routeStore.UpdateAsync(route);
|
||||
|
||||
// Note: We don't delete destinations when unregistering a service
|
||||
// The cluster and its destinations persist until explicitly deleted
|
||||
// Soft delete instances
|
||||
var instances = await _instanceStore.GetAllAsync();
|
||||
var routeInstances = instances.Where(i => i.ClusterId == route.ClusterId && !i.IsDeleted).ToList();
|
||||
|
||||
foreach (var instance in routeInstances)
|
||||
{
|
||||
instance.IsDeleted = true;
|
||||
instance.UpdatedTime = DateTime.UtcNow;
|
||||
await _instanceStore.UpdateAsync(instance);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Unregistered service {Service}", serviceName);
|
||||
|
||||
@ -200,7 +188,7 @@ public class GatewayService : IGatewayService
|
||||
public async Task<List<GatewayRouteDto>> GetRoutesAsync(bool globalOnly = false)
|
||||
{
|
||||
var routes = await _routeStore.GetAllAsync();
|
||||
var clusters = await _clusterStore.GetAllAsync();
|
||||
var instances = await _instanceStore.GetAllAsync();
|
||||
|
||||
var query = routes.Where(r => !r.IsDeleted);
|
||||
|
||||
@ -208,21 +196,19 @@ public class GatewayService : IGatewayService
|
||||
query = query.Where(r => r.IsGlobal);
|
||||
|
||||
var routeList = query.OrderByDescending(r => r.Priority).ToList();
|
||||
var clusters = routeList.Select(r => r.ClusterId).Distinct().ToList();
|
||||
|
||||
// Build instance count dict from clusters
|
||||
var instancesDict = clusters
|
||||
.Where(c => !c.IsDeleted && routeList.Any(r => r.ClusterId == c.ClusterId))
|
||||
.ToDictionary(
|
||||
c => c.ClusterId,
|
||||
c => c.Destinations?.Count(d => d.Status == 1) ?? 0
|
||||
);
|
||||
var instancesDict = instances
|
||||
.Where(i => clusters.Contains(i.ClusterId) && !i.IsDeleted)
|
||||
.GroupBy(i => i.ClusterId)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
return routeList.Select(r => new GatewayRouteDto
|
||||
{
|
||||
Id = r.Id,
|
||||
ServiceName = r.ServiceName,
|
||||
ClusterId = r.ClusterId,
|
||||
PathPattern = r.Match.Path ?? "",
|
||||
PathPattern = r.PathPattern,
|
||||
Priority = r.Priority,
|
||||
IsGlobal = r.IsGlobal,
|
||||
TenantCode = r.TenantCode,
|
||||
@ -250,7 +236,7 @@ public class GatewayService : IGatewayService
|
||||
TenantCode = dto.IsGlobal ? "" : dto.TenantCode ?? "",
|
||||
ServiceName = dto.ServiceName,
|
||||
ClusterId = dto.ClusterId,
|
||||
Match = new GwRouteMatch { Path = dto.PathPattern },
|
||||
PathPattern = dto.PathPattern,
|
||||
Priority = dto.Priority,
|
||||
Status = (int)RouteStatus.Active,
|
||||
IsGlobal = dto.IsGlobal,
|
||||
@ -264,7 +250,7 @@ public class GatewayService : IGatewayService
|
||||
Id = route.Id,
|
||||
ServiceName = route.ServiceName,
|
||||
ClusterId = route.ClusterId,
|
||||
PathPattern = route.Match.Path ?? "",
|
||||
PathPattern = route.PathPattern,
|
||||
Priority = route.Priority,
|
||||
IsGlobal = route.IsGlobal,
|
||||
TenantCode = route.TenantCode,
|
||||
@ -275,104 +261,81 @@ public class GatewayService : IGatewayService
|
||||
|
||||
public async Task<List<GatewayInstanceDto>> GetInstancesAsync(string clusterId)
|
||||
{
|
||||
var cluster = await _clusterStore.FindByClusterIdAsync(clusterId);
|
||||
if (cluster == null || cluster.Destinations == null)
|
||||
return new List<GatewayInstanceDto>();
|
||||
var instances = await _instanceStore.GetAllAsync();
|
||||
var clusterInstances = instances
|
||||
.Where(i => i.ClusterId == clusterId && !i.IsDeleted)
|
||||
.OrderByDescending(i => i.Weight)
|
||||
.ToList();
|
||||
|
||||
return cluster.Destinations
|
||||
.Where(d => d.Status == 1)
|
||||
.OrderByDescending(d => d.Weight)
|
||||
.Select(d => new GatewayInstanceDto
|
||||
return clusterInstances.Select(i => new GatewayInstanceDto
|
||||
{
|
||||
Id = d.DestinationId,
|
||||
ClusterId = clusterId,
|
||||
DestinationId = d.DestinationId,
|
||||
Address = d.Address ?? "",
|
||||
Weight = d.Weight,
|
||||
Health = d.HealthStatus,
|
||||
Status = d.Status,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
Id = i.Id,
|
||||
ClusterId = i.ClusterId,
|
||||
DestinationId = i.DestinationId,
|
||||
Address = i.Address,
|
||||
Weight = i.Weight,
|
||||
Health = (int)i.Health,
|
||||
Status = (int)i.Status,
|
||||
CreatedAt = i.CreatedTime
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<GatewayInstanceDto> AddInstanceAsync(CreateGatewayInstanceDto dto)
|
||||
{
|
||||
var destination = new GwDestination
|
||||
var existing = await _instanceStore.FindByDestinationAsync(dto.ClusterId, dto.DestinationId);
|
||||
if (existing != null && !existing.IsDeleted)
|
||||
{
|
||||
DestinationId = dto.DestinationId,
|
||||
Address = dto.Address,
|
||||
Weight = dto.Weight,
|
||||
HealthStatus = 1, // Healthy
|
||||
Status = 1 // Active
|
||||
};
|
||||
|
||||
var cluster = await _clusterStore.AddDestinationAsync(dto.ClusterId, destination);
|
||||
if (cluster == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cluster {dto.ClusterId} not found");
|
||||
throw new InvalidOperationException($"Instance {dto.DestinationId} already exists in cluster {dto.ClusterId}");
|
||||
}
|
||||
|
||||
return new GatewayInstanceDto
|
||||
var instance = new GwServiceInstance
|
||||
{
|
||||
Id = dto.DestinationId,
|
||||
Id = Guid.CreateVersion7().ToString("N"),
|
||||
ClusterId = dto.ClusterId,
|
||||
DestinationId = dto.DestinationId,
|
||||
Address = dto.Address,
|
||||
Weight = dto.Weight,
|
||||
Health = 1,
|
||||
Status = 1,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
Health = (int)InstanceHealth.Healthy,
|
||||
Status = (int)InstanceStatus.Active,
|
||||
CreatedTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _instanceStore.CreateAsync(instance);
|
||||
|
||||
return new GatewayInstanceDto
|
||||
{
|
||||
Id = instance.Id,
|
||||
ClusterId = instance.ClusterId,
|
||||
DestinationId = instance.DestinationId,
|
||||
Address = instance.Address,
|
||||
Weight = instance.Weight,
|
||||
Health = (int)instance.Health,
|
||||
Status = (int)instance.Status,
|
||||
CreatedAt = instance.CreatedTime
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveInstanceAsync(string instanceId)
|
||||
{
|
||||
// We need to find the cluster and destination
|
||||
// Since we don't have direct lookup, iterate through clusters
|
||||
var clusters = await _clusterStore.GetAllAsync();
|
||||
var instance = await _instanceStore.FindByIdAsync(instanceId);
|
||||
if (instance == null) return false;
|
||||
|
||||
foreach (var cluster in clusters)
|
||||
{
|
||||
if (cluster.Destinations == null) continue;
|
||||
|
||||
var dest = cluster.Destinations.FirstOrDefault(d => d.DestinationId == instanceId);
|
||||
if (dest != null)
|
||||
{
|
||||
await _clusterStore.RemoveDestinationAsync(cluster.ClusterId, instanceId);
|
||||
instance.IsDeleted = true;
|
||||
instance.UpdatedTime = DateTime.UtcNow;
|
||||
await _instanceStore.UpdateAsync(instance);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateInstanceWeightAsync(string instanceId, int weight)
|
||||
{
|
||||
// Find the cluster containing this destination
|
||||
var clusters = await _clusterStore.GetAllAsync();
|
||||
var instance = await _instanceStore.FindByIdAsync(instanceId);
|
||||
if (instance == null) return false;
|
||||
|
||||
foreach (var cluster in clusters)
|
||||
{
|
||||
if (cluster.Destinations == null) continue;
|
||||
|
||||
var dest = cluster.Destinations.FirstOrDefault(d => d.DestinationId == instanceId);
|
||||
if (dest != null)
|
||||
{
|
||||
var updatedDest = new GwDestination
|
||||
{
|
||||
DestinationId = dest.DestinationId,
|
||||
Address = dest.Address ?? "",
|
||||
Weight = weight,
|
||||
HealthStatus = dest.HealthStatus,
|
||||
Status = dest.Status
|
||||
};
|
||||
await _clusterStore.UpdateDestinationAsync(cluster.ClusterId, instanceId, updatedDest);
|
||||
instance.Weight = weight;
|
||||
instance.UpdatedTime = DateTime.UtcNow;
|
||||
await _instanceStore.UpdateAsync(instance);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task ReloadGatewayAsync()
|
||||
{
|
||||
@ -388,7 +351,7 @@ public class GatewayService : IGatewayService
|
||||
ServicePrefix = route.ServiceName,
|
||||
ServiceName = route.ServiceName,
|
||||
ClusterId = route.ClusterId,
|
||||
PathPattern = route.Match.Path ?? "",
|
||||
PathPattern = route.PathPattern,
|
||||
ServiceAddress = "",
|
||||
DestinationId = "",
|
||||
Weight = 1,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user