- 描述整体基于ASP.NET Core的分层架构与领域驱动设计 - 详细说明表现层、视图模型层、配置层和基础设施层职责 - 介绍用户认证、OAuth2授权码与令牌颁发的数据流过程 - 抽象说明用户与租户、声明和授权实体设计 - 说明应用启动入口和关键HTTP端点 - 列出错误处理策略和跨领域关注点(日志、追踪、安全) docs(concerns): 新增代码库问题与关注点分析文档 - 汇总并详述安全漏洞如配置文件泄露、Cookie策略不当 - 记录技术债务包括缺乏单元测试、依赖注入不统一等 - 罗列性能问题和具体代码缺陷 - 给出优先级明确的修复建议和改进措施 - 涵盖架构设计问题和依赖兼容性风险 - 说明测试覆盖缺口及高风险未测试区域 docs(conventions): 新增编码约定与规范文档 - 明确文件、类、方法、变量等命名规则 - 规范代码风格包括命名空间、主构造函数使用 - 制定日志记录、审计日志和依赖注入的标准 - 规定控制器路由、异步模式和错误处理方式 - 说明DTO命名及特性使用规范 - 描述配置管理、注释规范及常用代码注释样例 docs(integrations): 添加外部系统集成文档 - 介绍数据库连接和PostgreSQL客户端库版本 - 描述身份认证与授权服务及默认用户信息 - 说明可观测性方案及OpenTelemetry组件 - 涵盖容器化部署相关Docker与Kubernetes配置 - 说明CI/CD流水线触发条件与构建流程 - 列出环境变量需求和主要API端点 - 强调生产环境密钥管理与安全存储机制
527 lines
14 KiB
Markdown
527 lines
14 KiB
Markdown
# 编码约定
|
||
|
||
**分析日期:** 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*
|