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