docs(architecture): 添加系统架构分析文档
- 描述整体基于ASP.NET Core的分层架构与领域驱动设计 - 详细说明表现层、视图模型层、配置层和基础设施层职责 - 介绍用户认证、OAuth2授权码与令牌颁发的数据流过程 - 抽象说明用户与租户、声明和授权实体设计 - 说明应用启动入口和关键HTTP端点 - 列出错误处理策略和跨领域关注点(日志、追踪、安全) docs(concerns): 新增代码库问题与关注点分析文档 - 汇总并详述安全漏洞如配置文件泄露、Cookie策略不当 - 记录技术债务包括缺乏单元测试、依赖注入不统一等 - 罗列性能问题和具体代码缺陷 - 给出优先级明确的修复建议和改进措施 - 涵盖架构设计问题和依赖兼容性风险 - 说明测试覆盖缺口及高风险未测试区域 docs(conventions): 新增编码约定与规范文档 - 明确文件、类、方法、变量等命名规则 - 规范代码风格包括命名空间、主构造函数使用 - 制定日志记录、审计日志和依赖注入的标准 - 规定控制器路由、异步模式和错误处理方式 - 说明DTO命名及特性使用规范 - 描述配置管理、注释规范及常用代码注释样例 docs(integrations): 添加外部系统集成文档 - 介绍数据库连接和PostgreSQL客户端库版本 - 描述身份认证与授权服务及默认用户信息 - 说明可观测性方案及OpenTelemetry组件 - 涵盖容器化部署相关Docker与Kubernetes配置 - 说明CI/CD流水线触发条件与构建流程 - 列出环境变量需求和主要API端点 - 强调生产环境密钥管理与安全存储机制
This commit is contained in:
parent
9a7948e634
commit
2a60caae80
257
.planning/codebase/ARCHITECTURE.md
Normal file
257
.planning/codebase/ARCHITECTURE.md
Normal file
@ -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*
|
||||
537
.planning/codebase/CONCERNS.md
Normal file
537
.planning/codebase/CONCERNS.md
Normal file
@ -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
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
```
|
||||
|
||||
**影响:**
|
||||
- .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<ApplicationUser> userManager,
|
||||
RoleManager<ApplicationRole> roleManager,
|
||||
ILogger<UsersController> 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<ActionResult<TenantSettings>> GetTenantSettings(long tenantId)
|
||||
{
|
||||
// ...
|
||||
var settings = new TenantSettings
|
||||
{
|
||||
AllowRegistration = false,
|
||||
AllowedEmailDomains = "",
|
||||
DefaultRoleId = null,
|
||||
PasswordPolicy = new List<string> { "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<object>();
|
||||
|
||||
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<T>` 模式
|
||||
|
||||
---
|
||||
|
||||
### 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. **完善租户设置功能**
|
||||
|
||||
---
|
||||
|
||||
*问题审计完成*
|
||||
526
.planning/codebase/CONVENTIONS.md
Normal file
526
.planning/codebase/CONVENTIONS.md
Normal file
@ -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<ActionResult<object>> GetUsers(...)
|
||||
public async Task<ActionResult<ApplicationUser>> CreateUser(CreateUserDto dto)
|
||||
public async Task<IActionResult> 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<ApplicationUser> userManager,
|
||||
RoleManager<ApplicationRole> roleManager,
|
||||
ILogger<UsersController> logger,
|
||||
PlatformDbContext platformDbContext)
|
||||
: ControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
// 传统构造函数方式(部分文件使用)
|
||||
public class RolesController : ControllerBase
|
||||
{
|
||||
private readonly PlatformDbContext _context;
|
||||
private readonly RoleManager<ApplicationRole> _roleManager;
|
||||
|
||||
public RolesController(
|
||||
PlatformDbContext context,
|
||||
RoleManager<ApplicationRole> roleManager,
|
||||
...)
|
||||
{
|
||||
_context = context;
|
||||
_roleManager = roleManager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 属性与字段
|
||||
|
||||
**约定:** 私有字段使用下划线前缀(`_camelCase`),公共属性使用 PascalCase
|
||||
|
||||
**示例:**
|
||||
```csharp
|
||||
public class RolesController : ControllerBase
|
||||
{
|
||||
private readonly PlatformDbContext _context;
|
||||
private readonly RoleManager<ApplicationRole> _roleManager;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<RolesController> _logger;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 var 推断类型
|
||||
|
||||
**约定:** 优先使用 `var` 进行类型推断,复杂类型或返回类型明确时显式声明
|
||||
|
||||
**示例:**
|
||||
```csharp
|
||||
var query = platformDbContext.Users.AsQueryable();
|
||||
var user = await platformDbContext.Users.FindAsync(id);
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
ActionResult<object> result = Ok(new { ... });
|
||||
IEnumerable<string> 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<ActionResult<object>> 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<long> 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<T>` 使用
|
||||
|
||||
**示例:**
|
||||
```csharp
|
||||
public class UsersController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<ApplicationRole> roleManager,
|
||||
ILogger<UsersController> 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<PlatformDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(connectionString);
|
||||
options.UseOpenIddict();
|
||||
}).AddIdentity<ApplicationUser, ApplicationRole>()
|
||||
.AddEntityFrameworkStores<PlatformDbContext>()
|
||||
.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<T>` 模式
|
||||
|
||||
**示例:**
|
||||
```csharp
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
var isTesting = builder.Configuration.GetValue<bool>("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*
|
||||
115
.planning/codebase/INTEGRATIONS.md
Normal file
115
.planning/codebase/INTEGRATIONS.md
Normal file
@ -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*
|
||||
117
.planning/codebase/STACK.md
Normal file
117
.planning/codebase/STACK.md
Normal file
@ -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*
|
||||
226
.planning/codebase/STRUCTURE.md
Normal file
226
.planning/codebase/STRUCTURE.md
Normal file
@ -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*
|
||||
440
.planning/codebase/TESTING.md
Normal file
440
.planning/codebase/TESTING.md
Normal file
@ -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<bool>("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<UserManager<ApplicationUser>> _userManagerMock;
|
||||
private readonly Mock<RoleManager<ApplicationRole>> _roleManagerMock;
|
||||
private readonly Mock<ILogger<UsersController>> _loggerMock;
|
||||
private readonly PlatformDbContext _context;
|
||||
private readonly UsersController _controller;
|
||||
|
||||
public UsersControllerTests()
|
||||
{
|
||||
// 设置模拟对象
|
||||
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||
_userManagerMock = new Mock<UserManager<ApplicationUser>>(
|
||||
store.Object, null, null, null, null, null, null, null, null);
|
||||
|
||||
_roleManagerMock = new Mock<RoleManager<ApplicationRole>>();
|
||||
_loggerMock = new Mock<ILogger<UsersController>>();
|
||||
|
||||
// 使用内存数据库
|
||||
var options = new DbContextOptionsBuilder<PlatformDbContext>()
|
||||
.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<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public UsersControllerIntegrationTests(WebApplicationFactory<Program> 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
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="*" />
|
||||
<PackageReference Include="FluentAssertions" Version="*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="*" />
|
||||
<PackageReference Include="Moq" Version="*" />
|
||||
<PackageReference Include="xunit" Version="*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="*">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Fengling.AuthService.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## 模拟模式
|
||||
|
||||
### Moq 使用示例
|
||||
|
||||
**控制器依赖模拟:**
|
||||
```csharp
|
||||
// 模拟 UserManager
|
||||
var userStoreMock = new Mock<IUserStore<ApplicationUser>>();
|
||||
var userManagerMock = new Mock<UserManager<ApplicationUser>>(
|
||||
userStoreMock.Object,
|
||||
null, null, null, null, null, null, null, null);
|
||||
|
||||
// 模拟 RoleManager
|
||||
var roleStoreMock = new Mock<IRoleStore<ApplicationRole>>();
|
||||
var roleManagerMock = new Mock<RoleManager<ApplicationRole>>(
|
||||
roleStoreMock.Object,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
// 模拟 ILogger
|
||||
var loggerMock = new Mock<ILogger<UsersController>>();
|
||||
|
||||
// 设置模拟行为
|
||||
userManagerMock
|
||||
.Setup(x => x.FindByNameAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync(new ApplicationUser { UserName = "testuser" });
|
||||
|
||||
userManagerMock
|
||||
.Setup(x => x.GetRolesAsync(It.IsAny<ApplicationUser>()))
|
||||
.ReturnsAsync(new List<string> { "User" });
|
||||
```
|
||||
|
||||
### 数据库模拟
|
||||
|
||||
**使用 InMemory 数据库:**
|
||||
```csharp
|
||||
var options = new DbContextOptionsBuilder<PlatformDbContext>()
|
||||
.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<ApplicationUser>();
|
||||
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<InvalidOperationException>()
|
||||
.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*
|
||||
Loading…
Reference in New Issue
Block a user