diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..fd0591c --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,257 @@ +# 架构分析 + +**分析日期:** 2026-02-28 + +## 模式概述 + +**整体架构:** 基于 ASP.NET Core 的现代 Web 应用架构,采用分层设计结合领域驱动思想。 + +**核心特性:** +- 使用 OpenIddict 实现 OAuth2/OIDC 授权服务器功能 +- 采用 ASP.NET Core Identity 进行用户身份管理 +- 基于 Entity Framework Core 进行数据访问 +- 支持多租户(Tenant)架构 +- 集成 MediatR 实现 CQRS 模式(通过共享基础设施包) + +## 分层架构 + +### 1. 表现层(Presentation Layer) + +**位置:** `src/Controllers/`、`src/Views/` + +**职责:** +- 处理 HTTP 请求与响应 +- 渲染 Razor 视图 +- 用户交互界面 + +**关键控制器:** +- `AccountController`:用户登录、注册、登出 +- `AuthorizationController`:OAuth2 授权端点 +- `TokenController`:令牌颁发端点 +- `DashboardController`:用户仪表板 +- `UsersController`、`RolesController`、`TenantsController`:管理功能 + +### 2. 视图模型层(ViewModel Layer) + +**位置:** `src/ViewModels/` + +**职责:** +- 在控制器与视图之间传递数据 +- 表单数据绑定与验证 + +**关键视图模型:** +- `LoginViewModel`:登录表单 +- `RegisterViewModel`:注册表单 +- `AuthorizeViewModel`:授权确认表单 +- `DashboardViewModel`:仪表板数据 + +### 3. 配置层(Configuration Layer) + +**位置:** `src/Configuration/` + +**职责:** +- 集中管理应用程序配置 +- OpenIddict 服务配置 +- 身份验证策略配置 + +**关键配置:** +- `OpenIddictSetup.cs`:OpenIddict 完整配置,包括授权流程、令牌生命周期、作用域等 +- `FormValueRequiredAttribute.cs`:自定义属性用于表单提交处理 + +### 4. 基础设施层(Infrastructure Layer) + +**外部依赖:** `Fengling.Platform.Infrastructure` NuGet 包 + +**提供的功能:** +- `PlatformDbContext`:Entity Framework Core 数据库上下文 +- `ApplicationUser`:应用程序用户实体 +- `ApplicationRole`:应用程序角色实体 +- 仓储(Repository)模式实现 +- MediatR 中间件与行为(Behaviors) +- 命令锁定行为(Command Lock Behavior) +- 工作单元(Unit of Work)支持 + +## 数据流 + +### 用户认证流程 + +``` +1. 用户访问 /account/login + ↓ +2. AccountController 返回登录视图(Login.cshtml) + ↓ +3. 用户提交表单 → AccountController.Login POST + ↓ +4. UserManager 验证用户名密码 + ↓ +5. SignInManager 创建认证 Cookie + ↓ +6. 重定向到原始 URL 或 /dashboard +``` + +### OAuth2 授权码流程 + +``` +1. 客户端应用重定向到 /connect/authorize + ↓ +2. AuthorizationController.Authorize 检查用户登录状态 + ↓ +3. 未登录 → 重定向到登录页面 + ↓ +4. 已登录 → 检查授权记录 + ↓ +5. 需要用户授权 → 返回授权确认视图(Authorize.cshtml) + ↓ +6. 用户确认 → AuthorizationController.Accept + ↓ +7. 创建授权记录 → 返回令牌 +``` + +### 令牌颁发流程 + +``` +1. 客户端 POST /connect/token + ↓ +2. TokenController.Exchange 处理请求 + ↓ +3. 根据授权类型(password/refresh_token/authorization_code)处理 + ↓ +4. 从 UserManager 获取用户信息 + ↓ +5. 构建 Claims 身份(包括租户信息、角色) + ↓ +6. 设置 Claim Destinations + ↓ +7. SignIn 返回令牌 +``` + +## 关键抽象 + +### 用户与租户抽象 + +**ApplicationUser:** +- 继承自 ASP.NET Core Identity 的 IdentityUser +- 包含 `TenantInfo` 属性实现多租户支持 + +**TenantInfo 值对象:** +- `TenantId`:租户标识符 +- `TenantCode`:租户代码 +- `TenantName`:租户名称 + +### 声明(Claims)抽象 + +**标准声明:** +- `sub`:用户唯一标识 +- `name`:用户名 +- `email`:用户邮箱 +- `role`:用户角色 + +**自定义声明:** +- `tenant_id`:租户 ID +- `tenant_code`:租户代码 +- `tenant_name`:租户名称 + +### 授权(Authorization)抽象 + +**OpenIddict 实体:** +- `OpenIddictApplication`:OAuth2 客户端应用 +- `OpenIddictAuthorization`:授权记录 +- `OpenIddictScope`:授权作用域 +- `OpenIddictToken`:访问令牌/刷新令牌 + +## 入口点 + +### Web 应用程序入口 + +**位置:** `src/Program.cs` + +**启动流程:** +1. 配置 Serilog 日志 +2. 注册平台核心服务(AddPlatformCore) +3. 配置 ASP.NET Core Identity +4. 配置 Razor Pages 和 MVC +5. 配置 Cookie 认证 +6. 配置 OpenIddict +7. 配置 OpenTelemetry 分布式追踪 +8. 配置健康检查 +9. 注册仓储(Repositories) +10. 注册 MediatR 和中间件行为 +11. 配置 Swagger(开发环境) +12. 初始化数据库 + +### HTTP 端点 + +**认证端点:** +- `GET/POST /account/login`:用户登录 +- `GET/POST /account/register`:用户注册 +- `POST /account/~/connect/logout`:用户登出 +- `GET/POST /connect/authorize`:授权端点 +- `POST /connect/token`:令牌端点 +- `GET /connect/userinfo`:用户信息端点 + +**管理端点:** +- `GET /dashboard`:用户仪表板 +- `GET /dashboard/profile`:用户资料 +- `GET /dashboard/settings`:设置页面 +- `GET/POST /users`:用户管理 +- `GET/POST /roles`:角色管理 +- `GET/POST /tenants`:租户管理 + +**系统端点:** +- `GET /health`:健康检查 +- `GET /swagger/index.html`:API 文档 + +## 错误处理 + +**策略:** 基于 ASP.NET Core 标准异常处理 + +**模式:** +- 模型验证失败返回视图(ModelState) +- 认证失败返回 401/403 +- OpenIddict 错误返回标准错误响应 +- 全局异常由中间件捕获并记录 + +## 跨领域关注点 + +### 日志记录 + +**框架:** Serilog + +**配置:** 在 Program.cs 中配置,控制台输出 + +**模板:** +``` +[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception} +``` + +### 分布式追踪 + +**框架:** OpenTelemetry + +**组件:** +- AspNetCoreInstrumentation +- HttpClientInstrumentation +- OpenIddict.Server.AspNetCore 追踪源 +- OTLP Exporter + +### 健康检查 + +**端点:** `/health` + +**检查项:** +- PostgreSQL 数据库连接(NpgSql) + +### 安全配置 + +**认证方案:** +- Cookie 认证(默认方案):用于 Web 界面登录 +- OpenIddict Server:用于 OAuth2/OIDC + +**令牌配置:** +- 访问令牌生命周期:24 小时 +- 使用引用令牌(Reference Tokens) +- 支持刷新令牌 + +--- + +*架构分析:2026-02-28* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..e3f78b5 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,537 @@ +# 代码库问题与关注点 + +**分析日期:** 2026-02-28 + +--- + +## 一、严重安全问题 + +### 1.1 配置文件泄露敏感凭证 + +**问题描述:** `appsettings.json` 包含明文数据库密码和 JWT 密钥。 + +**文件位置:** `src/appsettings.json` + +**泄露内容:** +```json +"ConnectionStrings": { + "DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_auth;Username=movingsam;Password=sl52788542" +}, +"Jwt": { + "Secret": "FenglingAuthSecretKey2024!ChangeThisInProduction!" +} +``` + +**影响:** +- 数据库凭据完全暴露,包括用户名、密码、主机地址和端口 +- JWT 密钥硬编码在配置文件中,攻击者可用其伪造令牌 +- 若此文件被提交到版本控制系统,将造成严重安全漏洞 + +**修复建议:** +- 立即将敏感信息迁移至环境变量或密钥保管库 +- 使用 ASP.NET Core 的密钥管理功能(`dotnet user-secrets`) +- 在生产环境中使用 Azure Key Vault、AWS Secrets Manager 等 +- 创建 `appsettings.Production.json` 并通过环境变量加载配置 + +--- + +### 1.2 Cookie 安全策略配置不当 + +**问题描述:** 认证 Cookie 配置为非安全策略。 + +**文件位置:** `src/Program.cs` 第 43-44 行 + +```csharp +options.Cookie.SecurePolicy = CookieSecurePolicy.None; +options.Cookie.SameSite = SameSiteMode.Lax; +``` + +**影响:** +- `CookieSecurePolicy.None` 允许 Cookie 通过非 HTTPS 连接传输 +- `SameSiteMode.Lax` 无法完全防止 CSRF 攻击 +- 用户凭证可能在网络传输中被截获 + +**修复建议:** +```csharp +options.Cookie.SecurePolicy = CookieSecurePolicy.Always; +options.Cookie.SameSite = SameSiteMode.Strict; +options.Cookie.HttpOnly = true; +``` + +--- + +### 1.3 访问令牌未加密 + +**问题描述:** OpenIddict 配置禁用了访问令牌加密。 + +**文件位置:** `src/Configuration/OpenIddictSetup.cs` 第 64 行 + +```csharp +options.DisableAccessTokenEncryption(); +``` + +**影响:** +- 访问令牌以明文形式传输,任何中间人都能读取令牌内容 +- 攻击者可窃取令牌并冒充合法用户 + +**修复建议:** +- 生产环境必须启用令牌加密 +- 使用有效的加密证书而非开发证书 + +--- + +### 1.4 CORS 允许所有来源 + +**问题描述:** CORS 配置允许任意来源的跨域请求。 + +**文件位置:** `src/Program.cs` 第 102-109 行 + +```csharp +app.UseCors(x => +{ + x.SetIsOriginAllowed(origin => true) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() + .Build(); +}); +``` + +**影响:** +- 任何网站都能向此 API 发起跨域请求 +- 极大增加 CSRF 和数据泄露风险 + +**修复建议:** +- 明确配置允许的来源列表 +- 使用环境变量控制允许的域名 + +--- + +### 1.5 开发证书用于生产环境 + +**问题描述:** OpenIddict 使用开发环境证书进行签名和加密。 + +**文件位置:** `src/Configuration/OpenIddictSetup.cs` 第 61-62 行 + +```csharp +options.AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); +``` + +**影响:** +- 开发证书的私钥是公开的,攻击者可伪造令牌 +- 严重威胁生产环境安全 + +**修复建议:** +- 生产环境使用正式的 SSL/TLS 证书 +- 使用 Azure Key Vault 或类似服务存储证书 + +--- + +## 二、技术债务 + +### 2.1 完全没有单元测试 + +**问题描述:** 代码库中不存在任何测试文件。 + +**文件位置:** 整个项目 + +**影响:** +- 无法确保代码质量 +- 重构风险极高,容易引入 bug +- 难以验证边界条件和错误处理 + +**修复建议:** +- 引入 xUnit 或 NUnit 测试框架 +- 为所有 Controller 编写单元测试 +- 为关键业务逻辑(Token 颁发、用户管理等)编写集成测试 +- 目标覆盖率应达到 70% 以上 + +--- + +### 2.2 使用 .NET 10.0 + +**问题描述:** 项目目标框架为 .NET 10.0。 + +**文件位置:** `src/Fengling.AuthService.csproj` 第 3 行 + +```xml +net10.0 +``` + +**影响:** +- .NET 10.0 目前可能为预览版或早期版本 +- 稳定性存在风险,缺乏长期支持 +- 依赖包兼容性问题 + +**修复建议:** +- 生产环境应使用 LTS(长期支持)版本(如 .NET 8.0) +- 密切关注 .NET 10.0 的正式发布和稳定性评估 + +--- + +### 2.3 审计日志逻辑重复 + +**问题描述:** 审计日志创建逻辑在多个控制器中重复实现。 + +**受影响文件:** +- `src/Controllers/UsersController.cs` - `CreateAuditLog` 方法 +- `src/Controllers/RolesController.cs` - `CreateAuditLog` 方法 +- `src/Controllers/TenantsController.cs` - `CreateAuditLog` 方法 + +**影响:** +- 代码重复,维护困难 +- 行为不一致风险 +- 违反 DRY 原则 + +**修复建议:** +- 提取为独立的审计日志服务 +- 使用 MediatR 管道行为统一处理 +- 创建审计日志特性的 AOP 方案 + +--- + +### 2.4 不一致的依赖注入风格 + +**问题描述:** 控制器同时使用两种不同的依赖注入方式。 + +**文件位置:** +- 构造函数注入(传统方式):`src/Controllers/RolesController.cs` +- Primary Constructor 注入(记录式):`src/Controllers/UsersController.cs` + +**示例对比:** + +传统方式(RolesController): +```csharp +public class RolesController : ControllerBase +{ + private readonly PlatformDbContext _context; + + public RolesController(PlatformDbContext context) + { + _context = context; + } +} +``` + +Primary Constructor 方式(UsersController): +```csharp +public class UsersController( + UserManager userManager, + RoleManager roleManager, + ILogger logger, + PlatformDbContext platformDbContext) + : ControllerBase +``` + +**影响:** +- 代码风格不统一 +- 增加新人学习成本 + +**修复建议:** +- 统一采用 Primary Constructor 方式(ASP.NET Core 8.0+ 推荐) +- 或统一使用传统构造函数注入方式 + +--- + +## 三、已知缺陷 + +### 3.1 未实现的接口方法 + +**问题描述:** 某些接口返回硬编码或空值。 + +**文件位置:** `src/Controllers/TenantsController.cs` 第 171-192 行 + +```csharp +[HttpGet("{tenantId}/settings")] +public async Task> GetTenantSettings(long tenantId) +{ + // ... + var settings = new TenantSettings + { + AllowRegistration = false, + AllowedEmailDomains = "", + DefaultRoleId = null, + PasswordPolicy = new List { "requireNumber", "requireLowercase" }, + MinPasswordLength = 8, + SessionTimeout = 120, + }; + + return Ok(settings); +} +``` + +**影响:** +- 租户设置无法持久化 +- 配置变更不生效 + +**修复建议:** +- 创建 TenantSettings 实体 +- 实现 CRUD 操作 +- 与租户配置表关联 + +--- + +### 3.2 Profile 接口重定向 + +**问题描述:** Profile 和 Settings 路由指向未实现的方法。 + +**文件位置:** `src/Controllers/AccountController.cs` 第 111-117 行 + +```csharp +[HttpGet("profile")] +[HttpGet("settings")] +[HttpGet("~/connect/logout")] +public IActionResult NotImplemented() +{ + return RedirectToAction("Index", "Dashboard"); +} +``` + +**影响:** +- 用户访问个人资料页面时被重定向 +- 功能缺失,用户体验差 + +**修复建议:** +- 实现完整的个人资料页面 +- 允许用户查看和修改基本信息 + +--- + +## 四、性能问题 + +### 4.1 N+1 查询问题 + +**问题描述:** 列表接口在循环中执行数据库查询。 + +**文件位置:** `src/Controllers/RolesController.cs` 第 63-78 行 + +```csharp +foreach (var role in roles) +{ + var users = await _userManager.GetUsersInRoleAsync(role.Name!); + result.Add(new + { + // ... + userCount = users.Count, + // ... + }); +} +``` + +**影响:** +- 假设有 N 个角色,将执行 1+N 次数据库查询 +- 角色数量增加时,性能呈线性下降 +- 数据库负载显著增加 + +**修复建议:** +- 使用单一查询获取所有角色及其用户数 +- 通过 JOIN 或子查询聚合计数 + +--- + +### 4.2 内存分页问题 + +**问题描述:** OAuth 客户端列表先将所有数据加载到内存,再进行分页。 + +**文件位置:** `src/Controllers/OAuthClientsController.cs` 第 31-86 行 + +```csharp +var applications = _applicationManager.ListAsync(); +var clientList = new List(); + +await foreach (var application in applications) +{ + // 处理每个应用... + clientList.Add(new { ... }); +} + +var sortedClients = clientList + .OrderByDescending(c => (c as dynamic).clientId) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToList(); +``` + +**影响:** +- 客户端数量增加时,内存占用大幅增长 +- 首次加载缓慢 +- 无法处理大量客户端场景 + +**修复建议:** +- 使用数据库层面的分页(EF Core 的 Skip/Take) +- 实现服务端分页而非内存分页 + +--- + +### 4.3 缺少数据库索引 + +**问题描述:** 频繁查询的字段可能缺少索引。 + +**涉及字段:** +- `Users.UserName` - 登录查询 +- `Users.TenantInfo.TenantId` - 租户隔离查询 +- `AuditLogs.CreatedAt` - 日志查询 +- `AccessLogs.CreatedAt` - 访问日志查询 + +**影响:** +- 查询性能随数据量增加而下降 +- 大表全表扫描风险 + +**修复建议:** +- 分析慢查询日志 +- 为常用查询字段添加索引 +- 使用 EF Core 的 Fluent API 配置索引 + +--- + +## 五、架构与设计问题 + +### 5.1 异常处理作为流程控制 + +**问题描述:** 使用异常抛出处理正常业务流程。 + +**文件位置:** `src/Controllers/AuthorizationController.cs` 多处 + +```csharp +var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + +var user = await userManager.GetUserAsync(User) ?? + throw new InvalidOperationException("The user details cannot be retrieved."); +``` + +**影响:** +- 异常处理开销大,性能差 +- 代码可读性差 +- 混淆业务逻辑和错误处理 + +**修复建议:** +- 使用空值合并和空值检查 +- 返回适当的 HTTP 状态码(如 400、401) +- 使用 `ActionResult` 模式 + +--- + +### 5.2 租户隔离不完整 + +**问题描述:** 某些查询未正确应用租户隔离。 + +**文件位置:** `src/Controllers/UsersController.cs` 第 32-47 行 + +```csharp +var query = platformDbContext.Users.AsQueryable(); + +if (!string.IsNullOrEmpty(userName)) +{ + query = query.Where(u => u.UserName!.Contains(userName)); +} +// ... +``` + +**影响:** +- 管理员可能查看所有租户的数据 +- 租户数据泄露风险 + +**修复建议:** +- 创建租户过滤的基类或中间件 +- 在所有查询中自动应用租户 ID 过滤 +- 使用 EF Core 的全局查询过滤器 + +--- + +### 5.3 缺少 API 版本控制 + +**问题描述:** API 端点没有版本控制机制。 + +**影响:** +- 无法平滑升级 API +- 客户端兼容性问题 +- 难以废弃旧版 API + +**修复建议:** +- 实现 API 版本控制(URL 路径或 Header) +- 使用 `Microsoft.AspNetCore.Mvc.Versioning` +- 文档化各版本差异 + +--- + +## 六、依赖与兼容性 + +### 6.1 依赖外部包版本风险 + +**问题描述:** 项目依赖可能存在版本兼容性问题。 + +**关键依赖:** +- `OpenIddict.AspNetCore` - OAuth/OIDC 实现 +- `Fengling.Platform.Infrastructure` - 内部共享库 +- `NetCorePal.Extensions.AspNetCore` - 扩展库 + +**风险:** +- 依赖更新可能导致破坏性变更 +- 内部包版本不同步问题 + +**修复建议:** +- 锁定依赖版本 +- 定期审查依赖更新 +- 建立依赖升级测试流程 + +--- + +## 七、测试覆盖缺口 + +### 7.1 高风险未测试区域 + +| 区域 | 文件位置 | 风险 | +|------|----------|------| +| 身份验证流程 | `TokenController.cs` | 令牌颁发、刷新 | +| 用户管理 | `UsersController.cs` | 用户创建、密码重置 | +| 授权逻辑 | `AuthorizationController.cs` | OAuth 授权流程 | +| 租户隔离 | 多个 Controller | 数据泄露风险 | + +### 7.2 缺失的测试类型 + +- **单元测试:** 业务逻辑、验证逻辑 +- **集成测试:** 数据库交互、API 端点 +- **安全测试:** 认证流程、权限检查 +- **性能测试:** 大数据量场景 + +--- + +## 八、优先级修复建议 + +### 高优先级(立即处理) + +1. **移除 `appsettings.json` 中的敏感信息** + - 迁移到环境变量或密钥保管库 + - 轮换已泄露的密码和密钥 + +2. **启用 Cookie 安全策略** + - 改为 `SecurePolicy.Always` + - 启用 HttpOnly 和 SameSite=Strict + +3. **启用访问令牌加密** + - 移除 `DisableAccessTokenEncryption()` + - 配置生产证书 + +4. **修复 CORS 配置** + - 限制允许的来源列表 + +### 中优先级(近期处理) + +1. **添加单元测试框架和基础测试** +2. **修复 N+1 查询问题** +3. **实现内存分页优化** +4. **统一依赖注入风格** +5. **实现租户隔离的全局过滤** + +### 低优先级(长期规划) + +1. **升级到 .NET LTS 版本** +2. **提取审计日志服务** +3. **实现 API 版本控制** +4. **完善租户设置功能** + +--- + +*问题审计完成* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..5314713 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,526 @@ +# 编码约定 + +**分析日期:** 2026-02-28 + +## 项目概述 + +此代码库是一个基于 **.NET 10.0** 的 **ASP.NET Core Web API** 认证授权服务,采用 **OpenIddict** 实现 OAuth2/OIDC 协议,并使用 **ASP.NET Core Identity** 进行用户和角色管理。 + +## 命名模式 + +### 文件命名 + +**约定:** 使用 PascalCase 命名法 + +**示例:** +- `UsersController.cs` +- `TokenController.cs` +- `LoginViewModel.cs` +- `OpenIddictSetup.cs` + +### 类与类型命名 + +**约定:** 使用 PascalCase 命名法 + +**示例:** +```csharp +public class UsersController : ControllerBase +public class LoginViewModel +public class CreateUserDto +public static class OpenIddictSetup +``` + +### 方法命名 + +**约定:** 使用 PascalCase 命名法,动词或动词短语开头 + +**示例:** +```csharp +public async Task> GetUsers(...) +public async Task> CreateUser(CreateUserDto dto) +public async Task UpdateUser(long id, UpdateUserDto dto) +private async Task CreateAuditLog(...) +``` + +### 变量与属性命名 + +**约定:** 使用 camelCase 命名法 + +**示例:** +```csharp +var query = platformDbContext.Users.AsQueryable(); +var totalCount = await query.CountAsync(); +var user = await userManager.FindByNameAsync(request.Username); +var roles = await userManager.GetRolesAsync(user); +``` + +### 常量与枚举 + +**约定:** 使用 PascalCase 命名法 + +**示例:** +```csharp +errors.InvalidGrant +Errors.UnsupportedGrantType +Statuses.Valid +AuthorizationTypes.Permanent +``` + +## 代码风格 + +### 命名空间 + +**约定:** 使用文件级别命名空间(File-scoped namespace),C# 10+ 特性 + +**示例:** +```csharp +namespace Fengling.AuthService.Controllers; +namespace Fengling.AuthService.ViewModels; +namespace Fengling.AuthService.Configuration; +``` + +### 主构造函数 + +**约定:** 优先使用 C# 12 主构造函数(Primary Constructors) + +**示例:** +```csharp +// 主构造函数方式(推荐) +public class UsersController( + UserManager userManager, + RoleManager roleManager, + ILogger logger, + PlatformDbContext platformDbContext) + : ControllerBase +{ +} + +// 传统构造函数方式(部分文件使用) +public class RolesController : ControllerBase +{ + private readonly PlatformDbContext _context; + private readonly RoleManager _roleManager; + + public RolesController( + PlatformDbContext context, + RoleManager roleManager, + ...) + { + _context = context; + _roleManager = roleManager; + } +} +``` + +### 属性与字段 + +**约定:** 私有字段使用下划线前缀(`_camelCase`),公共属性使用 PascalCase + +**示例:** +```csharp +public class RolesController : ControllerBase +{ + private readonly PlatformDbContext _context; + private readonly RoleManager _roleManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; +} +``` + +### 使用 var 推断类型 + +**约定:** 优先使用 `var` 进行类型推断,复杂类型或返回类型明确时显式声明 + +**示例:** +```csharp +var query = platformDbContext.Users.AsQueryable(); +var user = await platformDbContext.Users.FindAsync(id); +var totalCount = await query.CountAsync(); + +ActionResult result = Ok(new { ... }); +IEnumerable destinations = GetDestinations(claim, principal); +``` + +## 导入组织 + +### using 指令顺序 + +**约定:** 按以下顺序排列 using 指令 + +1. 系统命名空间(`System.*`) +2. 项目依赖包命名空间(`Microsoft.*`、`OpenIddict.*`、`Fengling.*`) +3. 当前项目命名空间(`Fengling.AuthService.*`) +4. 静态导入(`using static`) + +**示例:** +```csharp +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using System.Security.Cryptography; + +using Fengling.Platform.Domain.AggregatesModel.UserAggregate; +using Fengling.Platform.Domain.AggregatesModel.RoleAggregate; +using Fengling.Platform.Infrastructure; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; + +using Fengling.AuthService.Configuration; +using Fengling.AuthService.ViewModels; + +using static OpenIddict.Abstractions.OpenIddictConstants; +``` + +### 命名空间别名 + +**约定:** 必要时使用命名空间别名简化长命名空间引用 + +**示例:** +```csharp +using static OpenIddict.Abstractions.OpenIddictConstants; +``` + +## 控制器模式 + +### API 控制器约定 + +**约定:** 所有 API 控制器使用以下属性 + +```csharp +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class UsersController(...) : ControllerBase +``` + +### 路由约定 + +**约定:** +- 集合资源使用复数形式:`api/users` +- 单个资源使用 `/{id}` 形式:`api/users/{id}` +- 特殊端点使用 `connect` 前缀:`connect/token`、`connect/authorize` + +**示例:** +```csharp +[HttpGet] // GET api/users +[HttpGet("{id}")] // GET api/users/123 +[HttpPost] // POST api/users +[HttpPut("{id}")] // PUT api/users/123 +[HttpDelete("{id}")] // DELETE api/users/123 +[HttpPut("{id}/password")] // PUT api/users/123/password +``` + +### 异步模式 + +**约定:** 所有数据库和 I/O 操作使用 async/await 模式 + +**示例:** +```csharp +public async Task> GetUsers( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? userName = null, + [FromQuery] string? email = null, + [FromQuery] string? tenantCode = null) +{ + var query = platformDbContext.Users.AsQueryable(); + + if (!string.IsNullOrEmpty(userName)) + { + query = query.Where(u => u.UserName!.Contains(userName)); + } + + var totalCount = await query.CountAsync(); + var users = await query + .OrderByDescending(u => u.CreatedTime) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return Ok(new { items = users, totalCount, page, pageSize }); +} +``` + +## 错误处理 + +### 控制器错误返回 + +**约定:** 使用标准 HTTP 状态码返回错误 + +**示例:** +```csharp +// 资源不存在 +if (user == null) +{ + return NotFound(); +} + +// 请求数据验证失败 +return BadRequest(result.Errors); + +// 业务规则错误 +if (role.IsSystem) +{ + return BadRequest("系统角色不能修改"); +} + +// 操作成功无内容 +return NoContent(); + +// 创建成功 +return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user); + +// OpenIddict 错误响应 +return BadRequest(new OpenIddictResponse +{ + Error = Errors.InvalidGrant, + ErrorDescription = "用户名或密码错误" +}); +``` + +### 异常抛出 + +**约定:** 在无法恢复的错误情况下抛出 `InvalidOperationException` + +**示例:** +```csharp +var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("OpenIddict request is null"); + +var user = await userManager.GetUserAsync(User) ?? + throw new InvalidOperationException("The user details cannot be retrieved."); +``` + +## 数据传输对象(DTO) + +### DTO 定义位置 + +**约定:** DTO 类定义在对应控制器文件的末尾 + +**示例:** +```csharp +// 位于 UsersController.cs 末尾 +public class CreateUserDto +{ + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string RealName { get; set; } = string.Empty; + public string? Phone { get; set; } + public long? TenantId { get; set; } + public List RoleIds { get; set; } = new(); + public string Password { get; set; } = string.Empty; + public bool EmailConfirmed { get; set; } + public bool IsActive { get; set; } = true; +} + +public class UpdateUserDto +{ + public string Email { get; set; } = string.Empty; + public string RealName { get; set; } = string.Empty; + public string? Phone { get; set; } + public bool EmailConfirmed { get; set; } + public bool IsActive { get; set; } = true; +} +``` + +### DTO 属性命名 + +**约定:** 使用 PascalCase 命名法,与 JSON 序列化配置保持一致 + +**示例:** +```csharp +public class RegisterViewModel +{ + [Required(ErrorMessage = "租户编号不能为空")] + [StringLength(10, MinimumLength = 4, ErrorMessage = "租户编号长度必须在4·10个字符之间")] + public string TenantCode { get; set; } + + [Required(ErrorMessage = "用户名不能为空")] + [StringLength(50, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-50个字符之间")] + public string Username { get; set; } = default!; + + [Required(ErrorMessage = "邮箱不能为空")] + [EmailAddress(ErrorMessage = "请输入有效的邮箱地址")] + public string Email { get; set; } = default!; +} +``` + +## 日志记录 + +### 日志框架 + +**约定:** 使用 `Microsoft.Extensions.Logging` 框架,通过依赖注入 `ILogger` 使用 + +**示例:** +```csharp +public class UsersController( + UserManager userManager, + RoleManager roleManager, + ILogger logger, // 注入日志记录器 + PlatformDbContext platformDbContext) + : ControllerBase +{ +} +``` + +### Serilog 配置 + +**约定:** 在 `Program.cs` 中配置 Serilog,使用配置文件和代码配置结合 + +**示例:** +```csharp +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + +builder.Host.UseSerilog(); +``` + +## 审计日志 + +### 审计日志模式 + +**约定:** 关键业务操作需要记录审计日志,包括操作者、操作类型、操作目标等信息 + +**示例:** +```csharp +private async Task CreateAuditLog( + string operation, + string action, + string targetType, + long? targetId, + string? targetName, + string? oldValue = null, + string? newValue = null) +{ + var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system"; + var tenantId = User.FindFirstValue("TenantId"); + + var log = new AuditLog + { + Operator = userName, + TenantId = tenantId, + Operation = operation, + Action = action, + TargetType = targetType, + TargetId = targetId, + TargetName = targetName, + IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", + Status = "success", + OldValue = oldValue, + NewValue = newValue, + }; + + platformDbContext.AuditLogs.Add(log); + await platformDbContext.SaveChangesAsync(); +} +``` + +## 依赖注入 + +### 服务注册 + +**约定:** 在 `Program.cs` 中注册所有服务,使用链式调用 + +**示例:** +```csharp +builder.Services.AddPlatformCore(options => +{ + options.UseNpgsql(connectionString); + options.UseOpenIddict(); +}).AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +builder.Services.AddOpenIddictConfiguration(builder.Configuration); + +builder.Services.AddMediatR(x => x.RegisterServicesFromAssemblies( + typeof(PlatformDbContext).Assembly, + Assembly.GetExecutingAssembly()) + .AddCommandLockBehavior() + .AddKnownExceptionValidationBehavior() + .AddUnitOfWorkBehaviors() +); +``` + +### 构造函数注入 + +**约定:** 优先使用构造函数注入,通过主构造函数简化 + +## 配置管理 + +### 配置文件 + +**约定:** 使用 `appsettings.json` 系列文件进行配置 + +**文件结构:** +- `appsettings.json` - 默认配置 +- `appsettings.Development.json` - 开发环境配置 +- `appsettings.Testing.json` - 测试环境配置 + +### 配置读取 + +**约定:** 使用 `IConfiguration` 接口和强类型 `IOptions` 模式 + +**示例:** +```csharp +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +var isTesting = builder.Configuration.GetValue("Testing", false); +``` + +## 特性与属性使用 + +### 常用特性 + +**约定:** 合理使用以下特性 + +```csharp +[ApiController] // API 控制器 +[Route("api/[controller]")] // 路由 +[Authorize] // 需要授权 +[AllowAnonymous] // 允许匿名访问 +[HttpGet] [HttpPost] [HttpPut] [HttpDelete] // HTTP 方法 +[FromQuery] // 从查询参数绑定 +[FromBody] // 从请求体绑定 +[FromRoute] // 从路由参数绑定 +[Required] // 必需验证 +[StringLength] // 字符串长度验证 +[EmailAddress] // 邮箱格式验证 +[Compare] // 字段比较验证 +[DataType] // 数据类型 +``` + +## 注释规范 + +### 代码内注释 + +**约定:** 关键业务逻辑添加注释说明,使用英文或中文均可,保持一致 + +**示例:** +```csharp +// 设置 Claim Destinations +foreach (var claim in principal.Claims) +{ + claim.SetDestinations(GetDestinations(claim, principal)); +} + +// 明确处理租户 ID - 这是业务关键信息 +case "tenant_id": + yield return OpenIddictConstants.Destinations.AccessToken; + yield break; + +// Never include the security stamp in the access and identity tokens +case "AspNet.Identity.SecurityStamp": + yield break; +``` + +--- + +*编码约定分析:2026-02-28* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..8b72bbb --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,115 @@ +# 外部集成 + +**分析日期:** 2026-02-28 + +## 数据库 + +**PostgreSQL:** +- **类型:** 关系型数据库 +- **连接信息:** + - 主机:81.68.223.70 + - 端口:15432 + - 数据库:fengling_auth + - 用户名:movingsam +- **客户端库:** Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3 +- **连接字符串来源:** `appsettings.json` 或环境变量 `ConnectionStrings__DefaultConnection` +- **健康检查:** 通过 AspNetCore.HealthChecks.Npgsql 集成 + +## 身份认证与授权 + +**OpenIddict(OAuth2/OIDC):** +- 实现标准的 OAuth2 授权服务器功能 +- 支持的授权类型:password(密码模式) +- JWT Token 发行 +- 令牌配置: + - Issuer(签发者):`http://localhost:5132`(开发环境) + - Audience(受众):`fengling-api` +- JWT Secret(开发环境):`FenglingAuthSecretKey2024!ChangeThisInProduction!` + +**默认用户:** +- **管理员:** 用户名 `admin`,密码 `Admin@123`,角色 `Admin` +- **测试用户:** 用户名 `testuser`,密码 `Test@123`,角色 `User` + +## 可观测性 + +**OpenTelemetry(OTLP 导出):** +- 导出协议:OpenTelemetry Protocol (OTLP) +- 导出目标:未在配置中明确指定(需配置 OTLP 端点) +- Instrumentation: + - ASP.NET Core 请求追踪 + - HTTP 客户端追踪 + - OpenIddict 服务器追踪 + +## 容器与部署 + +**Docker:** +- 镜像仓库:`gitea.shtao1.cn`(Gitea 私有仓库) +- 镜像名称:`fengling/fengling-auth-service:latest` +- Dockerfile:多阶段构建(build + publish) + +**Kubernetes:** +- 命名空间:`fengling` +- 副本数:2 +- 服务端口:80 +- 资源配置: + - 请求:CPU 100m,内存 256Mi + - 限制:CPU 500m,内存 512Mi +- 健康检查: + - 就绪探针:/health,初始延迟 10 秒,周期 10 秒 + - 存活探针:/health,初始延迟 30 秒,周期 30 秒 + +## CI/CD + +**Gitea Actions:** +- 触发条件: + - 推送至 main 或 master 分支 + - 合并至 main 或 master 分支的 Pull Request +- .NET 版本:10.0 +- 阶段: + 1. 构建(Build) + 2. Docker 镜像构建与推送 + 3. 部署至 Kubernetes + +**NuGet 源:** +- 私有源:`https://gitea.shtao1.cn/api/packages/fengling/nuget/index.json` +- 公共源:`https://api.nuget.org/v3/index.json` +- 认证方式:用户名 + 访问令牌 + +## 环境变量 + +**必需的环境变量:** +- `ConnectionStrings__DefaultConnection` - 数据库连接字符串(从 Kubernetes Secret 注入) +- `OpenIddict__Issuer` - Token 签发者 URL(生产环境:`https://auth.fengling.local`) +- `OpenIddict__Audience` - Token 受众 +- `ASPNETCORE_ENVIRONMENT` - 运行环境(如 Production) +- `ASPNETCORE_URLS` - 监听 URL(默认:`http://+:80`) + +**可选变量:** +- `Testing` - 是否为测试模式(启用/禁用 Swagger) + +## API 端点 + +**主要端点:** +- `/connect/token` - 获取 OAuth2 令牌(密码模式) +- `/health` - 健康检查端点 +- `/swagger/v1/swagger.json` - OpenAPI 文档 + +**管理端点(需认证):** +- `/api/users` - 用户管理 +- `/api/roles` - 角色管理 +- `/api/tenants` - 租户管理 +- `/api/oauth/clients` - OAuth 客户端管理 +- `/api/audit/logs` - 审计日志 +- `/api/access/logs` - 访问日志 + +## 密钥管理 + +**生产环境密钥存储:** +- Kubernetes Secret:`fengling-auth-secrets` +- 存储内容: + - `connection-string` - 数据库连接字符串 +- 镜像拉取凭据:Kubernetes ImagePullSecret + +--- + +*外部集成审计:2026-02-28* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..390a04e --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,117 @@ +# 技术栈 + +**分析日期:** 2026-02-28 + +## 语言 + +**主要:** +- **C# 12** - 项目使用最新的 C# 语言特性 +- **.NET 10.0** - 目标框架版本 + +**运行时:** +- **ASP.NET Core 10.0** - 用于构建 Web 应用程序和 API + +## 运行时环境 + +**框架:** +- **.NET 10.0** - 当前稳定版本 +- **ASP.NET Core** - Web 框架 + +**包管理:** +- **NuGet** - .NET 包管理器 +- **Fengling.Platform.Infrastructure** - 1.0.0 版本,内部平台基础设施包 +- **NetCorePal.Extensions** - 3.2.1 版本,一系列扩展库 + +## 核心框架 + +**身份认证与授权:** +- **OpenIddict** 7.2.0 - OAuth2/OpenID Connect 实现 + - `OpenIddict.AspNetCore` - OpenIddict 的 ASP.NET Core 集成 + - `OpenIddict.EntityFrameworkCore` - OpenIddict 的 EF Core 存储 + - `OpenIddict.Quartz` - OpenIddict 的 Quartz.NET 集成(用于后台任务) + +**数据访问:** +- **Entity Framework Core** 10.0.3 - ORM 框架 + - `Microsoft.EntityFrameworkCore` - 核心 EF Core 包 + - `Microsoft.EntityFrameworkCore.Design` - EF Core 设计时工具 + - `Npgsql.EntityFrameworkCore.PostgreSQL` - PostgreSQL 提供程序(已注释) + - `Microsoft.EntityFrameworkCore.InMemory` - 内存数据库(用于测试) + +**用户与角色管理:** +- **ASP.NET Core Identity** 10.0.3 - 用户身份管理系统 + +**Web API:** +- **Swashbuckle.AspNetCore** 10.1.4 - Swagger/OpenAPI 文档生成 + +**健康检查:** +- **AspNetCore.HealthChecks.Npgsql** 9.0.0 - PostgreSQL 健康检查 +- **Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore** 10.0.3 - EF Core 健康检查 + +## 日志与观测 + +**日志:** +- **Serilog** - 结构化日志框架 + - `Serilog.AspNetCore` 10.0.0 - Serilog 的 ASP.NET Core 集成 + - `Serilog.Sinks.Console` 6.1.1 - 控制台输出接收器 + +**分布式追踪:** +- **OpenTelemetry** 1.15.0 - 可观测性标准 + - `OpenTelemetry` - 核心包 + - `OpenTelemetry.Extensions.Hosting` - Host 集成 + - `OpenTelemetry.Instrumentation.AspNetCore` - ASP.NET Core instrumentation + - `OpenTelemetry.Instrumentation.Http` - HTTP 客户端 instrumentation + - `OpenTelemetry.Exporter.OpenTelemetryProtocol` - OTLP 导出器 + +## 业务逻辑 + +**CQRS 与中介者:** +- **MediatR** 12.5.0 - 中介者模式实现 + +**领域驱动设计支持:** +- **NetCorePal.Extensions.Domain.Abstractions** - 领域抽象 +- **NetCorePal.Extensions.Primitives** - 基础类型扩展 +- **NetCorePal.Extensions.Repository.EntityFrameworkCore** - 仓储模式实现 +- **NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake** - 雪花算法 ID 生成 + +## 配置 + +**环境配置:** +- `appsettings.json` - 基础配置 +- `appsettings.Development.json` - 开发环境配置 +- `appsettings.Testing.json` - 测试环境配置 + +**关键配置项:** +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_auth;..." + }, + "Jwt": { + "Issuer": "http://localhost:5132", + "Audience": "fengling-api", + "Secret": "..." + }, + "OpenIddict": { + "Issuer": "http://localhost:5132", + "Audience": "fengling-api" + } +} +``` + +**Docker 配置:** +- 基础镜像:`mcr.microsoft.com/dotnet/aspnet:10.0` +- 构建镜像:`mcr.microsoft.com/dotnet/sdk:10.0` +- 暴露端口:80 + +## 项目结构 + +**解决方案:** +- `Fengling.AuthService.slnx` - 现代解决方案格式 + +**项目文件:** +- `src/Fengling.AuthService.csproj` - 主项目文件 +- `Directory.Packages.props` - 集中化包版本管理 + +--- + +*技术栈分析:2026-02-28* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..b3cbfd5 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,226 @@ +# 代码库结构 + +**分析日期:** 2026-02-28 + +## 目录布局 + +``` +fengling-auth-service/ +├── src/ # 源代码根目录 +│ ├── Controllers/ # MVC 控制器 +│ ├── ViewModels/ # 视图模型 +│ ├── Views/ # Razor 视图 +│ ├── Configuration/ # 配置类 +│ ├── Properties/ # 启动配置 +│ ├── Program.cs # 入口点 +│ ├── Fengling.AuthService.csproj # 项目文件 +│ ├── appsettings.json # 应用配置 +│ ├── appsettings.Development.json # 开发环境配置 +│ └── Fengling.AuthService.http # HTTP 请求测试文件 +├── k8s/ # Kubernetes 部署配置 +│ ├── deployment.yaml # Deployment 配置 +│ └── service.yaml # Service 配置 +├── .gitea/workflows/ # Gittea CI/CD 配置 +│ └── ci.yaml # CI 流程 +├── Dockerfile # Docker 镜像构建 +├── NuGet.Config # NuGet 源配置 +├── Directory.Packages.props # 包版本管理 +├── Fengling.AuthService.slnx # 解决方案文件 +└── README.md # 项目说明 +``` + +## 目录用途 + +### 源代码目录(src/) + +**Controllers/:** +- 用途:MVC 控制器,处理 HTTP 请求 +- 包含: + - `AccountController.cs`:账户管理(登录、注册、登出) + - `AuthorizationController.cs`:OAuth2 授权 + - `TokenController.cs`:令牌颁发 + - `DashboardController.cs`:用户仪表板 + - `UsersController.cs`、`RolesController.cs`、`TenantsController.cs`:管理功能 + - `StatsController.cs`、`AuditLogsController.cs`、`AccessLogsController.cs`:日志统计 + - `OAuthClientsController.cs`:OAuth 客户端管理 + - `LogoutController.cs`:登出处理 + +**ViewModels/:** +- 用途:视图模型,用于视图与控制器之间的数据传输 +- 包含: + - `LoginViewModel.cs`:登录视图模型 + - `RegisterViewModel.cs`:注册视图模型 + - `AuthorizeViewModel.cs`:授权确认视图模型 + - `DashboardViewModel.cs`:仪表板视图模型 + +**Views/:** +- 用途:Razor 视图文件 +- 结构: + - `Account/`:账户相关视图(Login、Register) + - `Authorization/`:授权视图(Authorize) + - `Dashboard/`:仪表板视图(Index、Profile、Settings) + - `Shared/`:共享视图(_Layout) + - `_ViewStart.cshtml`:视图起始配置 + - `_ViewImports.cshtml`:视图导入配置 + +**Configuration/:** +- 用途:集中管理应用程序配置 +- 包含: + - `OpenIddictSetup.cs`:OpenIddict 服务配置 + - `FormValueRequiredAttribute.cs`:自定义验证属性 + +**Properties/:** +- 用途:启动配置文件 +- 包含: + - `launchSettings.json`:启动配置 + +## 关键文件位置 + +### 入口点 + +**`src/Program.cs`:** +- 应用程序启动入口 +- 服务注册 +- 中间件配置 +- 管道构建 + +### 配置文件 + +**`src/appsettings.json`:** +- 数据库连接字符串 +- JWT 配置 +- OpenIddict 配置 +- 日志配置 + +**`src/appsettings.Development.json`:** +- 开发环境特定配置 + +### 项目文件 + +**`src/Fengling.AuthService.csproj`:** +- 项目依赖声明 +- 目标框架:net10.0 +- 关键包: + - OpenIddict(OAuth2/OIDC) + - Entity Framework Core(数据访问) + - ASP.NET Core Identity(身份管理) + - Serilog(日志) + - OpenTelemetry(可观测性) + +## 命名约定 + +### 文件命名 + +**控制器:** +- 模式:`{EntityName}Controller.cs` +- 示例:`AccountController.cs`、`UsersController.cs` + +**视图模型:** +- 模式:`{Feature}ViewModel.cs` +- 示例:`LoginViewModel.cs`、`AuthorizeViewModel.cs` + +**视图:** +- 模式:`{Action}.cshtml` +- 示例:`Login.cshtml`、`Authorize.cshtml` + +**配置类:** +- 模式:`{Feature}Setup.cs` 或 `{Feature}Attribute.cs` +- 示例:`OpenIddictSetup.cs`、`FormValueRequiredAttribute.cs` + +### 目录命名 + +** Controllers:** +- 复数形式,如 `Controllers`、`Users` + +** ViewModels:** +- 复数形式,如 `ViewModels` + +** Views:** +- 与控制器对应,如 `Account`、`Dashboard` + +### 命名空间 + +**模式:** `Fengling.AuthService.{FolderName}` + +**示例:** +- `Fengling.AuthService.Controllers` +- `Fengling.AuthService.ViewModels` +- `Fengling.AuthService.Configuration` + +### 类命名 + +**控制器:** +- 继承自 `Controller` 或 `ControllerBase` +- 使用 `[ControllerName]` 属性或路由属性 + +**视图模型:** +- 使用 `ViewModel` 后缀 +- 包含输入属性和验证属性 + +## 添加新代码的位置 + +### 新增功能模块 + +**场景:** 添加新的业务功能 + +**代码位置:** `src/Controllers/` +- 创建新的 Controller 文件 + +**视图位置:** `src/Views/{Feature}/` + +**视图模型位置:** `src/ViewModels/` + +**配置位置:** `src/Configuration/` + +### 新增 API 端点 + +**场景:** 添加新的 REST API + +**代码位置:** `src/Controllers/` +- 在现有 Controller 中添加 Action +- 或创建新的 Controller(继承 ControllerBase) + +### 新增视图页面 + +**场景:** 添加新的 Razor 页面 + +**视图位置:** `src/Views/{ControllerName}/{Action}.cshtml` + +**视图模型位置:** `src/ViewModels/{Feature}ViewModel.cs` + +### 新增配置 + +**场景:** 添加新的服务配置 + +**配置位置:** `src/Configuration/` +- 创建静态配置类 +- 在 Program.cs 中调用 + +## 特殊目录 + +### k8s/ + +- 用途:Kubernetes 部署配置 +- 生成:手动维护 +- 提交:是的 + +### .gitea/workflows/ + +- 用途:Gitea CI/CD 流水线配置 +- 生成:手动维护 +- 提交:是的 + +### Properties/ + +- 用途:开发环境启动配置 +- 生成:IDE 生成 +- 提交:是的(launchSettings.json 不含敏感信息) + +### Views/Shared/ + +- 用途:共享视图组件 +- 包含:_Layout.cshtml 主布局 + +--- + +*结构分析:2026-02-28* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..9e007a1 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,440 @@ +# 测试模式 + +**分析日期:** 2026-02-28 + +## 测试框架概述 + +此代码库目前**未建立独立的测试项目**,但具备测试基础设施支持。代码库使用以下测试相关配置: + +- `appsettings.Testing.json` - 测试环境配置文件 +- `Microsoft.EntityFrameworkCore.InMemory` - 内存数据库提供程序(用于测试场景) +- 支持通过配置切换测试模式 + +## 测试配置 + +### 测试环境配置 + +**配置文件位置:** `src/appsettings.Testing.json` + +**配置内容:** +```json +{ + "Testing": true, + "ConnectionStrings": { + "DefaultConnection": "..." + }, + "OpenIddict": { + "Issuer": "...", + "Audience": "..." + } +} +``` + +### 测试模式切换 + +**约定:** 通过配置项 `Testing` 控制是否启用完整 OpenIddict 配置 + +**示例:** +```csharp +var isTesting = builder.Configuration.GetValue("Testing", false); + +if (!isTesting) +{ + builder.AddServer(options => + { + // 生产环境 OpenIddict 配置 + options.SetIssuer(configuration["OpenIddict:Issuer"] ?? "http://localhost:5132"); + // ... 其他配置 + }); +} +``` + +### 内存数据库 + +**依赖包:** `Microsoft.EntityFrameworkCore.InMemory` + +**使用场景:** 测试环境中使用内存数据库,避免外部数据库依赖 + +**示例:** +```csharp +// 在测试环境中,可以使用 InMemory 提供程序 +options.UseInMemoryDatabase("TestDatabase"); +``` + +## 测试现状分析 + +### 缺失的测试项目 + +**问题:** 当前代码库没有独立的测试项目(xUnit、NUnit 或 MSTest) + +**影响:** +- 缺少单元测试 +- 缺少集成测试 +- 缺少控制器测试 +- 无法进行回归测试 + +### 建议的测试策略 + +由于代码库采用 Clean Architecture 和 MediatR 模式,建议建立以下测试层次: + +#### 1. 单元测试(xUnit/NUnit) + +**目标:** 验证业务逻辑、命令处理器、查询处理器 + +**测试框架建议:** +- **xUnit** - 推荐,与 .NET 生态集成良好 +- **NUnit** - 传统选择,功能全面 +- **FluentAssertions** - 断言库,提供更自然的断言语法 +- **Moq** - 模拟框架 + +**示例测试结构:** +```csharp +public class UsersControllerTests +{ + private readonly Mock> _userManagerMock; + private readonly Mock> _roleManagerMock; + private readonly Mock> _loggerMock; + private readonly PlatformDbContext _context; + private readonly UsersController _controller; + + public UsersControllerTests() + { + // 设置模拟对象 + var store = new Mock>(); + _userManagerMock = new Mock>( + store.Object, null, null, null, null, null, null, null, null); + + _roleManagerMock = new Mock>(); + _loggerMock = new Mock>(); + + // 使用内存数据库 + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TestDb") + .Options; + _context = new PlatformDbContext(options); + + _controller = new UsersController( + _userManagerMock.Object, + _roleManagerMock.Object, + _loggerMock.Object, + _context); + } +} +``` + +#### 2. 集成测试 + +**目标:** 测试控制器端点、数据库操作、OpenIddict 流程 + +**测试框架建议:** +- **Microsoft.AspNetCore.Mvc.Testing** - ASP.NET Core 测试支持 +- **TestHost** - 内存 Web 主机 +- **FluentAssertions** - 断言库 + +**示例测试结构:** +```csharp +public class UsersControllerIntegrationTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + private readonly HttpClient _client; + + public UsersControllerIntegrationTests(WebApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + } + + [Fact] + public async Task GetUsers_ReturnsPagedResults() + { + // Arrange + var request = "/api/users?page=1&pageSize=10"; + + // Act + var response = await _client.GetAsync(request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + // 验证响应内容 + } +} +``` + +#### 3. 端到端测试 + +**目标:** 验证完整用户流程(可选) + +**工具建议:** +- **Playwright** - 现代 Web 测试框架 +- **Selenium** - 传统选择 + +## 测试文件组织 + +### 建议的目录结构 + +``` +fengling-auth-service/ +├── src/ +│ └── Fengling.AuthService.csproj +└── tests/ + ├── Fengling.AuthService.UnitTests/ + │ ├── Fengling.AuthService.UnitTests.csproj + │ ├── Controllers/ + │ ├── Commands/ + │ ├── Queries/ + │ └── Services/ + ├── Fengling.AuthService.IntegrationTests/ + │ ├── Fengling.AuthService.IntegrationTests.csproj + │ ├── Controllers/ + │ └── Fixtures/ + └── Fengling.AuthService.E2ETests/ + ├── Fengling.AuthService.E2ETests.csproj + └── Scenarios/ +``` + +### 测试项目依赖 + +**建议的测试项目 csproj 配置:** + +```xml + + + net10.0 + enable + enable + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +``` + +## 模拟模式 + +### Moq 使用示例 + +**控制器依赖模拟:** +```csharp +// 模拟 UserManager +var userStoreMock = new Mock>(); +var userManagerMock = new Mock>( + userStoreMock.Object, + null, null, null, null, null, null, null, null); + +// 模拟 RoleManager +var roleStoreMock = new Mock>(); +var roleManagerMock = new Mock>( + roleStoreMock.Object, + null, null, null, null, null, null, null); + +// 模拟 ILogger +var loggerMock = new Mock>(); + +// 设置模拟行为 +userManagerMock + .Setup(x => x.FindByNameAsync(It.IsAny())) + .ReturnsAsync(new ApplicationUser { UserName = "testuser" }); + +userManagerMock + .Setup(x => x.GetRolesAsync(It.IsAny())) + .ReturnsAsync(new List { "User" }); +``` + +### 数据库模拟 + +**使用 InMemory 数据库:** +```csharp +var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + +using var context = new PlatformDbContext(options); + +// 添加测试数据 +context.Users.Add(new ApplicationUser +{ + UserName = "testuser", + Email = "test@example.com" +}); +await context.SaveChangesAsync(); +``` + +## 测试数据工厂 + +### 建议的 Fixture 模式 + +```csharp +public static class UserFixture +{ + public static ApplicationUser CreateUser(string userName = "testuser") + { + return new ApplicationUser + { + UserName = userName, + Email = $"{userName}@example.com", + TenantInfo = new TenantInfo(new Tenant { TenantId = 1, TenantCode = "TEST" }), + EmailConfirmed = true, + CreatedTime = DateTimeOffset.UtcNow + }; + } + + public static ApplicationUser CreateAdminUser() + { + var user = CreateUser("admin"); + return user; + } +} + +public static class RoleFixture +{ + public static ApplicationRole CreateRole(string name = "User") + { + return new ApplicationRole + { + Name = name, + DisplayName = name, + Description = $"{name} Role", + IsSystem = false, + CreatedTime = DateTime.UtcNow + }; + } + + public static ApplicationRole CreateSystemRole() + { + var role = CreateRole("Admin"); + role.IsSystem = true; + return role; + } +} +``` + +## 断言模式 + +### FluentAssertions 使用示例 + +```csharp +using FluentAssertions; + +// 对象断言 +result.Should().NotBeNull(); +result.Should().BeOfType(); +result.Id.Should().Be(1); +result.UserName.Should().Be("testuser"); + +// 集合断言 +users.Should().HaveCount(10); +users.Should().Contain(u => u.UserName == "testuser"); +users.Should().BeInAscendingOrder(u => u.CreatedTime); + +// 异常断言 +action.Should().Throw() + .WithMessage("User not found"); + +// HTTP 响应断言 +response.StatusCode.Should().Be(HttpStatusCode.OK); +response.Content.Headers.ContentType.MediaType.Should().Be("application/json"); +``` + +## 测试覆盖率 + +### 覆盖率目标建议 + +| 测试类型 | 覆盖率目标 | 说明 | +|---------|-----------|------| +| 单元测试 | ≥ 80% | 业务逻辑、命令、查询 | +| 集成测试 | ≥ 60% | 控制器端点、数据访问 | +| 整体 | ≥ 70% | 综合覆盖率 | + +### 覆盖率工具 + +**工具:** `coverlet.collector` 或 `coverlet.msbuild` + +**运行命令:** +```bash +# 使用 dotnet test +dotnet test --collect:"XPlat Code Coverage" + +# 查看覆盖率报告 +dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage +``` + +## CI/CD 测试集成 + +### 建议的测试脚本 + +```yaml +# azure-pipelines.yml 或 .github/workflows/test.yml +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: UseDotNet@2 + inputs: + packageType: 'sdk' + version: '10.0.x' + + - script: | + dotnet restore + dotnet build --configuration Release + displayName: 'Build' + + - script: | + dotnet test --configuration Release --collect:"XPlat Code Coverage" --results-directory ./coverage + displayName: 'Test' + + - script: | + dotnet tool install --global dotnet-reportgenerator-globaltool + reportgenerator -reports:./coverage/coverage.cobertura.xml -targetdir:./coverage-report -reporttypes:Html + displayName: 'Generate Coverage Report' +``` + +## 当前测试缺口 + +### 未测试的关键领域 + +1. **控制器端点** - 缺少 API 端点测试 +2. **业务逻辑** - 命令处理器和查询处理器未测试 +3. **身份验证流程** - OAuth2/OIDC 流程未测试 +4. **多租户隔离** - 租户数据隔离逻辑未验证 +5. **角色权限** - RBAC 逻辑未测试 + +### 优先测试建议 + +基于代码库特点,建议按以下优先级添加测试: + +1. **高优先级** + - `TokenController` - 令牌颁发逻辑 + - `UsersController` - 用户 CRUD 操作 + - `RolesController` - 角色管理操作 + +2. **中优先级** + - `AuthorizationController` - 授权流程 + - 审计日志功能 + - 多租户数据隔离 + +3. **低优先级** + - 健康检查端点 + - 静态文件服务 + +--- + +*测试模式分析:2026-02-28*