# LicenseManager Platform - 编码规范

| 项目 | 内容 |
| --- | --- |
| 文档版本 | v1.0 |
| 文档类型 | 编码规范 (Coding Standards) |
| 适用范围 | LicenseManager 全栈仓库（C# 后端、TypeScript/Vue 前端、数据库脚本） |
| 关联文档 | [API 设计规范 v1.0](./LicenseManager-Platform-API-Spec-v1.0.md) · [数据库设计 v1.0](./LicenseManager-Platform-Database-Design-v1.0.md) · [Core 开发指南 v1.0](./LicenseManager.Core-Development-Guide-v1.0.md) |
| 文档日期 | 2026-07-25 |

---

## 0. 修订历史

| 版本 | 日期 | 修订内容 |
| --- | --- | --- |
| v1.0 | 2026-07-25 | 初稿 |
| v1.1 | 2026-09 | 按代码实测同步：§5 数据库约定改为"现状 + 目标"两层（33 表 snake_case 单数 + 13 表 PascalCase；列名现状 PascalCase；软删除未实现）；路由守卫由 `meta.requiresAdmin` 改为 `meta.roles` 数组；HMAC 段去除 nonce 描述（实际无 nonce 机制） |

---

## 1. 概述

### 1.1 文档目的

本规范统一 LicenseManager 平台全栈代码风格，约束 C# 后端、TypeScript/Vue 前端及数据库脚本的组织方式与编写习惯，旨在：

- 降低代码评审成本，减少因风格分歧导致的来回沟通；
- 提升代码可读性与可维护性，使新人能快速理解既有模块；
- 通过约定式命名与结构，使 IDE 重构、静态分析与代码搜索更高效；
- 沉淀安全编码与国际化最佳实践，避免常见漏洞与本地化遗漏。

### 1.2 适用范围

| 层 | 仓库路径 | 技术栈 |
| --- | --- | --- |
| 后端 API | `src/LicenseManager.Api/` | ASP.NET Core 8 + MVC |
| 平台服务 | `src/LicenseManager.Platform/` | C# 12 + DDD 风格分层 |
| 基础设施 | `src/LicenseManager.Infrastructure/` | EF Core 8 + MySQL |
| 核心库 | `src/LicenseManager.Core/` | C# 12（SDK 共享契约） |
| 管理控制台 | `web/admin/` | Vue 3 + TypeScript + Element Plus + Vite |
| 数据库 | `src/LicenseManager.Infrastructure.Migrations.{MySql,PostgreSql,Sqlite,SqlServer,Oracle}/Migrations/` | EF Core Migration（按提供程序分程序集） |

### 1.3 强制级别

| 级别 | 含义 | 标识 |
| --- | --- | --- |
| **必须 (MUST)** | 强制要求，违反即视为缺陷，PR 予以驳回 | 规范条目默认级别 |
| **应当 (SHOULD)** | 强烈建议，偏离需在 PR 中说明理由 | 标注 *SHOULD* |
| **可以 (MAY)** | 可选项，由开发者自行判断 | 标注 *MAY* |

未达 **必须** 级别的历史代码不强制立即重构，但新增与修改代码必须符合本规范。

---

## 2. 通用规范

### 2.1 文件编码与换行

| 项 | 要求 |
| --- | --- |
| 文件编码 | UTF-8（无 BOM） |
| 换行符 | LF（`\n`） |
| 文件末尾 | 必须以一个换行符结束 |
| 行尾空白 | 自动去除（Markdown 文件除外） |

> 配套 `.editorconfig` 已在仓库根目录提供，所有提交的源文件须符合其约束。

### 2.2 缩进

| 文件类型 | 缩进风格 | 缩进宽度 |
| --- | --- | --- |
| `*.cs` | 空格 | 4 |
| `*.ts` / `*.tsx` / `*.vue` / `*.js` | 空格 | 2 |
| `*.json` / `*.yml` / `*.yaml` | 空格 | 2 |
| `*.xml` / `*.csproj` / `*.props` / `*.targets` | 空格 | 2 |
| `*.md` | 空格 | 2（列表嵌套） |

### 2.3 行宽

- C# 行宽建议不超过 **140** 字符；超过时优先换行并保持链式调用对齐。
- TypeScript/Vue 行宽建议不超过 **120** 字符；模板属性可适当放宽。

### 2.4 引号与分号

| 语言 | 引号 | 分号 |
| --- | --- | --- |
| C# | 双引号 | 必须分号结尾 |
| TypeScript / Vue `<script>` | 单引号 | 可省略（遵循 ESLint 默认配置） |
| JSON | 双引号 | 必须分号结尾 |

---

## 3. C# 编码规范

### 3.1 命名约定

| 元素 | 约定 | 示例 |
| --- | --- | --- |
| 命名空间、类、结构、记录、枚举、委托 | PascalCase | `NotificationService`、`UserStatus` |
| 接口 | `I` 前缀 + PascalCase | `IInMemoryRepository<T>`、`ILicenseManager` |
| 公共方法、属性、事件 | PascalCase | `SendAsync`、`ContextName` |
| 局部变量、方法参数 | camelCase | `templateCode`、`matchedDays` |
| 私有只读字段 | `_` 前缀 + camelCase | `_templateRepo`、`_logger` |
| 私有静态字段 | `_` 前缀 + camelCase 或 PascalCase 常量 | `_logger`、`ExpiryReminderTemplateCode` |
| 常量与静态只读 | PascalCase | `ReminderDays`、`ExpiryReminderTemplateCode` |
| 泛型类型参数 | `T` 前缀 + PascalCase | `TEntity`、`T` |
| 异步方法 | `Async` 后缀 | `GetTemplateAsync`、`SendExpiryRemindersAsync` |
| 布尔属性 | 肯定式 PascalCase，避免 `Is` 前缀除非语义必要 | `IsActive`、`Success` |

### 3.2 文件组织

- **一个 public class per file**：每个 `public` 类型独占一个文件；内部辅助类型可同文件存放但需简短。
- **文件名 = 主类型名**：`NotificationService.cs` 对应 `class NotificationService`；泛型类以 `EfRepository.cs` 表示。
- **命名空间与目录一致**：`LicenseManager.Platform.Services` 对应 `src/LicenseManager.Platform/Services/`。
- **using 顺序**（参见 `NotificationService.cs`、`LicensesController.cs`）：
  1. `System.*` 与 `Microsoft.*` 框架命名空间（按字母序）；
  2. 空行；
  3. `LicenseManager.*` 项目内命名空间（按字母序）；
  4. 空行；
  5. 文件作用域命名空间声明 `namespace LicenseManager.Platform.Services;`。
- 文件作用域命名空间（file-scoped namespace）一律使用，禁止块状 `namespace { ... }`。

### 3.3 XML 文档注释

- **所有 public 成员必须有 `///` 注释**，包括类、接口、方法、属性、构造函数。
- 类注释须描述其职责；方法注释须说明行为、参数、返回值与可能的异常。
- 复杂业务逻辑应使用 `<para>`、`<list type="bullet">`、`<see cref="..."/>`、`<c>...</c>` 丰富排版，参见 `EfRepository<T>` 的类注释。
- 参数注释顺序与方法签名一致，命名与参数名完全匹配。

```csharp
/// <summary>
/// 初始化 <see cref="NotificationService"/> 的新实例。
/// </summary>
/// <param name="templateRepo">通知模板存储。</param>
/// <param name="emailSender">邮件发送器；为空时跳过实际发送（测试环境兼容）。</param>
public NotificationService(
    IInMemoryRepository<NotificationTemplate> templateRepo,
    IEmailSender? emailSender = null)
{
    _templateRepo = templateRepo;
    _emailSender = emailSender;
}
```

### 3.4 异步编程

- **必须使用 `async`/`await`**，禁止 `Task.Run` 包装本就异步的调用。
- **公共异步方法必须接受 `CancellationToken ct = default`** 并向下游传递，参见 `NotificationService.SendAsync`。
- **禁止 `.Result` / `.Wait()` / `.GetAwaiter().GetResult()`**（除同步适配器边界，且必须注释说明）。
- I/O 操作（数据库、HTTP、文件）一律异步；CPU 密集型操作如需后台执行使用 `Task.Run` 并显式传递 `CancellationToken`。
- 异步方法返回 `Task` 或 `Task<T>`，事件处理器使用 `ValueTask` 仅限高频热路径。
- 异常处理：捕获预期异常并记录日志后继续；未预期异常向上传播由中间件统一处理。

```csharp
public async Task<NotificationTemplate?> GetTemplateAsync(string code, CancellationToken ct = default)
{
    var all = await _templateRepo.ListAsync(ct);
    return all.FirstOrDefault(t => t.Code == code && t.IsActive);
}
```

### 3.5 依赖注入

- **构造函数注入**：所有依赖通过构造函数注入，禁止服务定位（`IServiceProvider.GetService`）。
- **生命周期选择**：

  | 生命周期 | 适用场景 | 示例 |
  | --- | --- | --- |
  | Singleton | 无状态服务、静态配置、`SecretProtector` 依赖 | `IHttpContextAccessor`、`IMetricsRegistry` |
  | Scoped | 仓储、DbContext、含租户上下文的服务 | `EfRepository<T>`、`LicenseManagerDbContext` |
  | Transient | 轻量无状态服务、临时工厂 | 视具体场景而定 |

- **可选依赖** 使用可空构造参数 `IEmailSender? emailSender = null` 表达，并在使用前判空，参见 `NotificationService`。
- **依赖注册**集中放在 `Platform/DependencyInjection.cs`、`Api/Program.cs` 等扩展方法中，禁止在控制器/服务内自行 `new`。
- 仓储统一通过 `IInMemoryRepository<T>` 抽象，`EfRepository<T>` 为其数据库实现，便于内存/数据库双轨切换。

```csharp
public EfRepository(LicenseManagerDbContext db)
{
    _db = db ?? throw new ArgumentNullException(nameof(db));
    _set = db.Set<T>();
}
```

### 3.6 DTO 组织

- DTO 目录按职责分层，每层下区分 `Requests/` 与 `Responses/`：

  ```
  src/LicenseManager.Api/Dtos/
      Requests/
          ActivateRequest.cs
          VerifyRequest.cs
          HeartbeatRequest.cs
      Responses/
          FeatureStateResponse.cs
          ConsumeResult.cs

  src/LicenseManager.Platform/Dtos/
      Requests/
          CreateWebhookEndpointRequest.cs
          UpdateProductRequest.cs
      Responses/
          WebhookEndpointDto.cs
          ProductDto.cs
  ```

- **Request DTO** 用于入参绑定，命名 `动词 + 资源 + Request`（如 `CreateProductRequest`、`UpdateCustomerRequest`）。
- **Response DTO** 用于出参序列化，命名 `资源 + Dto` 或 `动作 + Result`（如 `ProductDto`、`ActivationResult`）。
- DTO 字段使用 PascalCase 属性，与 JSON 序列化默认配置一致。
- 严格区分领域实体（`Platform/Domain/`）与对外 DTO，禁止直接暴露实体给控制器。

### 3.7 控制器规范

- 控制器位于 `Api/Controllers/`，命名 `资源 + Controller`（如 `LicensesController`）。
- 路由前缀统一 `/v1/{context}/{resource}`，使用 `[ApiController]`、`[Route]`、`[Produces("application/json")]` 特性。
- 每个动作方法必须：
  - 标注 HTTP 动词特性 `[HttpGet]` / `[HttpPost]` 等；
  - 标注 `[ProducesResponseType(typeof(T), StatusCodes.Status200OK)]` 等响应类型；
  - 接受 `CancellationToken ct` 参数（异步动作）；
  - 返回 `IActionResult` 并使用 `Ok(...)` / `Problem(...)` 统一封装。
- 控制器只做参数校验与编排，业务逻辑下沉到服务层。

### 3.8 表达式与模式

- 使用 C# 12+ 现代语法：`is not null`、`is null`、模式匹配、`?? throw`、目标类型 `new()`。
- 集合判空优先 `is { Count: > 0 }` 或 `Any()`，避免 `Count() > 0`。
- 字符串插值使用 `$"...{var}..."`；日志模板使用结构化占位符 `{Recipient}`、`{Template}`。

---

## 4. TypeScript / Vue 编码规范

### 4.1 命名约定

| 元素 | 约定 | 示例 |
| --- | --- | --- |
| Vue 组件文件 / 视图文件 | PascalCase | `ProductList.vue`、`PageHeader.vue`、`LicenseDetail.vue` |
| 组件注册名 | PascalCase | `<PageHeader />`、`<ProductForm />` |
| TypeScript interface / type | PascalCase | `Product`、`ProductPayload`、`PageResult<T>` |
| 变量、函数 | camelCase | `loadData`、`openCreate`、`search` |
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY`、`API_PREFIX` |
| API 模块文件 | camelCase | `product.ts`、`apiKey.ts`、`license.ts` |
| Pinia store 文件 | camelCase | `product.ts`（位于 `src/stores/`） |
| Composable 文件 | camelCase + `use` 前缀 | `useI18n`、`useAuthStore` |
| 路由 name | PascalCase | `ProductList`、`LicenseDetail` |
| CSS class | kebab-case + BEM 可选 | `page-container`、`toolbar`、`pagination-bar` |

### 4.2 Vue SFC 结构

`<template>` → `<script setup lang="ts">` → `<style scoped>` 顺序固定：

```vue
<template>
  <div class="page-container">
    <!-- 模板内容 -->
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import PageHeader from '@/components/PageHeader.vue'

const { t } = useI18n()
// 组合式 API 逻辑
onMounted(loadData)
</script>

<style scoped>
.page-container {
  padding: 16px;
}
</style>
```

- **必须使用 `<script setup lang="ts">`**，禁止 Options API 与 `<script>` 非阻塞写法。
- 组件导入使用 `@/` 别名（指向 `src/`）。
- `<style>` 默认 `scoped`；全局样式集中到 `src/styles/`。
- 模板中属性较多时按指令 → 属性 → 事件顺序排列，每行一个属性。

### 4.3 i18n 规范

- **所有面向用户的文本必须通过 `t('key')` 调用**，禁止在 `<template>` 或脚本中硬编码中文/英文字符串。
- 文案键存放于 `src/i18n/zh-CN.json` 与 `src/i18n/en-US.json`，两份文件**键必须完全一致**，缺失键视为缺陷。
- 键名按 `模块.动作` 分层组织（如 `products.title`、`common.save`、`common.confirm`）。
- 带占位符的文案使用 `{var}` 模板：`t('products.confirmDelete', { name: row.name })`。
- 通用词汇归入 `common.*` 命名空间，业务词汇归入对应模块命名空间。
- 后端返回的错误消息如需展示，先尝试 `t('errors.<code>')`，未命中时回退原始消息。

```ts
// 正确
<el-button @click="loadData">{{ t('common.search') }}</el-button>

// 错误
<el-button @click="loadData">搜索</el-button>
```

### 4.4 API 客户端规范

- 所有 HTTP 调用集中于 `src/api/` 目录，每个资源一个文件（`product.ts`、`license.ts`、`customer.ts`）。
- 统一通过 `client.ts` 导出的 `get / post / patch / put / del` 类型化助手调用，禁止在业务代码中直接 `import axios`。
- 类型化助手使用重载声明（参见 `client.ts`），可选响应类型 `T = any`、可选请求体类型 `B = unknown`：

  ```ts
  export function post<T = any, B = unknown>(url: string, data?: B, config?: AxiosRequestConfig): Promise<T>
  export async function post<T = any, B = unknown>(url: string, data?: B, config?: AxiosRequestConfig): Promise<T> {
    return client.post(url, data, config) as unknown as Promise<T>
  }
  ```

- 每个资源模块导出 `interface`（响应/请求类型）与 `async function`（CRUD 调用），命名 `动词 + 资源`：`listProducts`、`getProduct`、`createProduct`、`updateProduct`、`deleteProduct`。
- 错误处理统一在 `client.ts` 响应拦截器内：
  - 业务错误（`success: false`）→ `ElMessage.error`；
  - 401 → 清空 auth 并跳转 `/login`；
  - 403 → `ElMessage.error('无权限访问')`；
  - 422 → 表单校验错误，不弹全局消息，交由表单组件内联处理；
  - 其他网络错误 → 提取 `message`/`error` 字段后 `ElMessage.error`。
- 请求拦截器自动注入 `Authorization: Bearer <token>`。

### 4.5 路由规范

- 路由配置统一位于 `src/router/index.ts`，使用 `createRouter` + `createWebHistory`。
- **所有页面必须懒加载**：`component: () => import('@/views/ProductList.vue')`。
- **命名路由**：每条路由必须配置 `name`，导航使用 `router.push({ name: 'LicenseDetail' })` 而非路径字符串。
- **路由元信息**：
  - `meta.public = true`：公开页面（如 `/login`）；
  - `meta.title`：页面标题；
  - `meta.roles = ['admin', ...]`：允许访问的角色数组，由 `hasAnyRole()` 判定。
- **全局守卫**在 `router.beforeEach` 中实现：
  - 未认证访问受保护页面 → 重定向 `{ name: 'Login' }`；
  - 已认证访问 `/login` → 重定向 `{ name: 'Dashboard' }`；
  - `meta.roles` 不含当前用户角色 → `ElMessage.error('无权限访问')` 并重定向 `{ name: 'Dashboard' }`。

### 4.6 Pinia Store 规范

- Store 文件位于 `src/stores/`，命名与资源一致（`product.ts`、`auth.ts`）。
- 使用组合式 store：`export function useProductStore() { ... }` 通过 `defineStore('product', () => { ... })` 定义。
- Store 内只封装状态与异步加载逻辑，组件直接调用 `store.fetchAll(...)` / `store.create(...)`。
- 跨组件共享状态必须经过 store，禁止组件间通过 `provide/inject` 传递业务数据。

---

## 5. 数据库规范

> 本节分**现状**与**目标**两层描述：现状为代码实测（以 `LicenseManagerDbContext` 与迁移快照为准），目标为演进方向。新增实体按目标约定编写，历史表不强制立即迁移。

### 5.1 表命名

- **目标**：表名使用 **蛇形小写**（snake_case），单数形式：`user_account`、`audit_log`、`activation`、`api_key`、`webhook_endpoint`。
- **现状**：33 个显式 `ToTable` 实体符合 snake_case 单数约定；13 个高级模块实体走 EF Core 默认命名（PascalCase 复数名），暂不强制改名，后续随迁移计划统一。
- 多对多关联表命名 `主表_从表`（如 `license_entitlement`）。
- 临时/迁移表加 `tmp_` 或 `mig_` 前缀。

### 5.2 列命名

- **目标**：列名使用 **蛇形小写**（snake_case）：`created_at`、`updated_at`、`license_key`、`tenant_id`。
- **现状**：全库列名为 PascalCase（未使用 `HasColumnName`，无命名约定），以代码为准；改名列入后续迁移计划。
- 布尔列以 `is_` 前缀：`is_active`、`is_deleted`。
- 外键列以 `_id` 后缀：`customer_id`、`license_id`。

### 5.3 主键

- 主键统一使用 **`Guid`** 类型，列名 `id`。
- C# 实体中以 `Id` 属性表达（PascalCase），由 EF Core 映射到数据库 `id` 列。
- 主键值由应用层生成（`Guid.NewGuid()`），不依赖数据库自增，便于分布式写入与离线场景。

### 5.4 审计与软删除字段

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `Guid` | 主键 |
| `created_at` | `DateTimeOffset` | 创建时间，由应用层写入 `DateTimeOffset.UtcNow` |
| `updated_at` | `DateTimeOffset?` | 最近更新时间，每次保存时刷新 |
| `deleted_at` | `DateTimeOffset?` | **（目标）**软删除标记——现状未实现，删除均为物理删除 |
| `tenant_id` | `Guid?` | 租户标识（多租户实体必须） |

- **现状**：无软删除（全库无 `DeletedAt` / `IsDeleted` 字段），业务数据删除即物理删除；软删除列入路线图。
- 多租户过滤：27 个含 `TenantId` 的实体由 `LicenseManagerDbContext` 通过反射动态附加全局查询过滤器。
- 审计字段由仓储或 `SaveAsync` 实现自动维护，业务代码不应手动赋值 `CreatedAt`。

### 5.5 EF Core 规范

- DbContext 位于 `src/LicenseManager.Infrastructure/Persistence/LicenseManagerDbContext.cs`，生命周期为 Scoped。
- 仓储统一通过 `IInMemoryRepository<T>` 抽象，`EfRepository<T>` 为数据库实现，禁止在业务代码中直接 `DbContext.Set<T>()`。
- **只读查询必须 `AsNoTracking()`**，避免变更跟踪器膨胀（参见 `EfRepository.ListAsync`）。
- 大表全量查询必须设置上限保护（`OrderBy(Id).Take(10000)`），分页查询走专用接口。
- 跨仓储事务使用 `IUnitOfWork` 显式提交，禁止在仓储内自行开事务。
- Migration 通过 `dotnet ef migrations add` 生成，禁止手改已应用迁移。
- 字符集统一 `utf8mb4`，GUID 列使用 `char(36)` + `ascii_general_ci` 排序以优化索引。
- JSON 列（如 `Scopes`、`SubscribedEvents`）使用 EF Core `json` 列类型，避免大字段反序列化开销。

---

## 6. Git 规范

### 6.1 分支策略

| 分支 | 用途 | 保护级别 |
| --- | --- | --- |
| `main` | 生产发布分支，始终可部署 | 强保护，仅接受 PR 合并，禁止直接 push |
| `develop` | 集成分支，每日构建 | 保护，禁止直接 push |
| `feature-<scope>-<short-desc>` | 新功能开发 | 例：`feature-license-floating-pool` |
| `fix-<short-desc>` | Bug 修复 | 例：`fix-webhook-signature-leak` |
| `hotfix-<short-desc>` | 生产紧急修复，从 `main` 拉出 | 合并回 `main` 与 `develop` |
| `release-<version>` | 发布准备 | 合并回 `main` 与 `develop` |

- 分支命名一律小写，单词以 `-` 分隔，避免使用下划线与中文。
- 功能分支合并后由 PR 触发自动删除（保留可追溯的 squash commit）。

### 6.2 提交信息格式

采用 **`type: description`** 单行格式，描述以中文或英文动词开头：

```
type: 简短描述（≤ 72 字符）
```

| type | 含义 |
| --- | --- |
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `refactor` | 重构（不改变外部行为） |
| `perf` | 性能优化 |
| `docs` | 文档变更 |
| `test` | 测试相关 |
| `chore` | 构建、依赖、配置 |
| `style` | 代码风格（不影响逻辑） |
| `ci` | CI/CD 配置 |

示例：
- `feat: 新增浮动席位池管理界面`
- `fix: 修复 webhook 签名验证时间戳容差判定错误`
- `refactor: 将内存仓储替换为 EfRepository 持久化实现`

复杂变更可在首行后空一行附加详细说明（≤ 1000 字符），遵循 72/100 规则。

### 6.3 PR 审查要求

| 项 | 要求 |
| --- | --- |
| 审查人数 | 至少 1 名成员 approve；影响核心域（License/Activation/Order）需 2 人 |
| CI 状态 | 构建、单元测试、lint 全部通过 |
| 覆盖率 | 新增/修改代码行覆盖率不低于 70% |
| 文档 | 涉及 API 或数据库变更须同步更新对应设计文档 |
| 提交粒度 | 单个 PR 控制在 800 行 diff 以内，超出需拆分 |
| 关联 Issue | PR 描述必须关联对应 Issue 编号 |

---

## 7. 安全编码规范

### 7.1 输入验证与输出编码

- **所有外部输入必须验证**：控制器层使用 `[FromBody]` + DataAnnotations 或 FluentValidation；服务层进行业务规则二次校验。
- 字符串输入必须限定长度（`[MaxLength]`）与字符集（正则白名单）。
- URL 输入须校验协议（仅 `https`，开发环境允许 `http`，参见 `WebhookEndpointService.ValidateWebhookUrl`）。
- 枚举输入须校验值合法性，拒绝未声明的枚举值。
- **输出编码**：响应 JSON 由框架自动序列化；模板变量替换必须 HTML 转义（参见 `NotificationService.ReplaceVariables` 中的 `System.Net.WebUtility.HtmlEncode`）。

### 7.2 敏感数据处理

- **加密存储**：HMAC 密钥、API Key 密钥、Webhook Secret 等敏感字段必须通过 `SecretProtector` 加密后入库，禁止明文持久化。
- **密钥初始化**：`SecretProtector.Initialize(configuration, environment, logger)` 必须在任何服务注册前调用，由 `AddLicenseManagerPlatform` 扩展方法保证。
- **日志脱敏**：日志中禁止出现完整密钥、Token、密码；如需标识使用前 4 + `***` + 后 4 形式或仅记录 `Id`。
- **内存清理**：敏感字节数组使用后调用 `CryptographicOperations.ZeroMemory` 或 `Array.Clear`。
- **传输安全**：生产环境强制 TLS 1.2+，HSTS 启用；敏感字段不进入 URL 查询参数。

```csharp
var secret = GenerateHmacSecret();
var endpoint = new WebhookEndpoint
{
    Secret = EncryptSecret(secret),  // SecretProtector 加密
    // ...
};
```

### 7.3 SQL 注入防护

- **必须使用 EF Core 参数化查询**，禁止字符串拼接 SQL。
- 仅在性能关键路径使用原生 SQL，且必须通过 `FromSqlInterpolated` 或 `FromSqlRaw` + 参数对象。
- 动态排序、过滤字段必须使用白名单校验，禁止直接拼接用户输入到 LINQ 表达式。
- 存储过程调用使用 `SqlParameter` 显式参数化。
- `EfRepository<T>.ExtractId` 使用反射而非字符串拼接，杜绝注入面。

```csharp
// 正确：EF Core 参数化
return await _set.AsNoTracking()
    .OrderBy(e => EF.Property<Guid>(e, "Id"))
    .Take(10000)
    .ToListAsync(ct);
```

### 7.4 XSS 防护

- **Vue 模板默认自动转义**：禁止使用 `v-html` 渲染用户可控内容；确需渲染 HTML 时必须先经过 DOMPurify 等净化库。
- **后端模板变量替换必须 HTML 转义**：`NotificationService.ReplaceVariables` 使用 `System.Net.WebUtility.HtmlEncode`。
- **CSP 头**：生产环境启用 Content-Security-Policy，禁止 inline script（`unsafe-inline`）。
- **Cookie 安全**：会话 Cookie 设置 `HttpOnly`、`Secure`、`SameSite=Lax`。
- **CSRF 防护**：API 默认使用 Bearer Token，不依赖 Cookie；如使用 Cookie 鉴权须配置 AntiForgeryToken。

### 7.5 认证与授权

- 控制器动作必须显式声明所需角色或权限点，通过 `AuthorizationMiddleware` 强制校验。
- 路由 `meta.roles = ['admin', ...]` 声明允许访问的角色数组，前端 `router.beforeEach` 守卫（`hasAnyRole()`）与后端中间件双重校验。
- API Key 可配合 HMAC 签名（`Authorization: Hmac <keyId>:<signature>` 请求头 + `X-LicenseManager-Api-Key` 与 `X-LicenseManager-Timestamp`，签名公式 `HMAC-SHA256(secret, timestamp + "\n" + body)`，hex 小写），时间戳容差 ±300 秒；无 nonce 机制，重放防护仅依赖时间戳窗口。
- PAT（Personal Access Token）绑定用户与显式 scope，禁止跨用户共享。

---

## 8. 注释规范

### 8.1 何时需要注释

| 场景 | 注释类型 | 示例 |
| --- | --- | --- |
| 复杂业务规则 | 行内或块注释 | 提醒窗口 60/30/7 天匹配逻辑 |
| 安全敏感操作 | 块注释 + 风险说明 | 密钥加密、XSS 转义、签名校验 |
| 性能权衡 | 块注释 | `AsNoTracking` 原因、`Take(10000)` 上限保护 |
| 非显而易见的设计决策 | 块注释 | `licenseId` 路径参数保留但未使用的语义 |
| 跨服务协作约定 | 块注释 | `IUnitOfWork` 显式提交时机 |
| 公共 API 契约 | XML 文档注释 | 所有 public 成员 |
| 兼容性历史遗留 | 块注释 | "为兼容未配置 SMTP 的环境" |

### 8.2 何时不需要注释

- 显而易见的赋值、条件判断、循环（如 `var page = 1;`）。
- 已通过类型名与方法名表达清晰的代码（如 `await store.fetchAll(...)`）。
- 与代码同步过时的注释（注释必须随代码更新，过期注释比无注释更糟糕）。
- 强行复述代码的"翻译式"注释（如 `// 设置 name 为空` 对应 `name = ''`）。

### 8.3 TODO / FIXME 格式

- 统一使用 `TODO`、`FIXME`、`HACK`、`BUG` 大写前缀，后接 `: ` 与说明，必要时附 Issue 编号。

```csharp
// TODO: 支持多语言模板切换（issue #142）
// FIXME: 高并发下 Heartbeat 可能错过续期窗口（issue #88）
// HACK: 临时绕过第三方邮件服务限流，待 vNext 升级后移除
```

- 提交前通过 `grep -rn "TODO\|FIXME\|HACK" src/` 自查，确保未遗留过期标记。
- `TODO` 必须可执行：要么关联 Issue，要么说明下一步动作，禁止泛泛 `TODO: 优化`。

### 8.4 注释语言

- C# 与 TypeScript 源码注释统一使用 **中文**（与项目主语言一致），便于业务语义传达。
- 日志消息使用 **英文**（结构化占位符），便于国际日志聚合与检索。
- XML 文档注释使用中文描述，标签 `<see cref="..."/>`、`<param name="..."/>` 保持英文标识符。
- 公开 API 文档（OpenAPI 描述）可中英双语，但默认中文。

---

## 附录 A：规范遵循检查清单

PR 提交前自检：

- [ ] 文件编码 UTF-8、换行 LF、缩进符合语言要求
- [ ] C# public 成员均有 XML 文档注释
- [ ] 异步方法接受并传递 `CancellationToken`
- [ ] 依赖通过构造函数注入，无服务定位
- [ ] DTO 位于 `Requests/` 或 `Responses/` 对应目录
- [ ] Vue 模板无硬编码中文/英文，全部走 `t()`
- [ ] `zh-CN.json` 与 `en-US.json` 键一致
- [ ] API 调用经 `src/api/` 模块，未直接 `import axios`
- [ ] 路由懒加载、命名、`meta` 完整
- [ ] 数据库表/列命名符合 §5"现状 + 目标"约定、主键 `Guid`、审计字段齐全
- [ ] 敏感字段经 `SecretProtector` 加密
- [ ] 无字符串拼接 SQL，全部参数化
- [ ] 无 `v-html` 渲染用户输入
- [ ] 提交信息符合 `type: description` 格式
- [ ] CI 全绿，覆盖率达标
