# Edu 多语言编码规范

> 版本：1.0
> 日期：2026-07-07
> 状态：基线发布
> 适用范围：Edu 微服务架构（TS + Go + Python + protobuf）
> 关联文档：
>
> - [项目规则](../../.trae/rules/project_rules.md)
> - [迁移指南](../../MIGRATION_GUIDE.md)
> - [Git 工作流](./git-workflow.md)
> - [架构总览](../architecture/001_architecture_overview.md)

---

## 目录

1. [项目原则与理念](#一项目原则与理念)
2. [TypeScript（NestJS）规范](#二typescriptnestjs规范)
3. [Go（Gin）规范](#三gogin规范)
4. [Python（FastAPI）规范](#四pythonfastapi规范)
5. [跨语言通用规范](#五跨语言通用规范)
6. [protobuf 契约规范](#六protobuf-契约规范)
7. [设计令牌规范](#七设计令牌规范)
8. [安全规范](#八安全规范)
9. [测试规范](#九测试规范)
10. [代码审查清单](#十代码审查清单)

---

## 一、项目原则与理念

1. **可读性优先于机巧**：代码首先是写给队友看的
2. **显式优于隐式**：避免魔法值、隐式类型转换、隐式全局副作用
3. **单一职责**：每个文件、函数、组件只做一件事
4. **防御性编程**：永远假设输入可能是 null/None/nil 或非法格式
5. **契约先行**：跨服务通信先定 protobuf，再写实现
6. **架构图优先**：任何任务开始前先查阅 [001 架构总览](../architecture/001_architecture_overview.md)
7. **限界上下文封装**：跨上下文不直接查询对方 DB，必须通过 gRPC 或事件

---

## 二、TypeScript（NestJS）规范

### 2.1 适用范围

- 业务微服务（NestJS 10）：identity、org、teaching、content、comm、insight
- 基础设施服务（NestJS 10）：auth、notification
- BFF 层（NestJS 10）：Admin BFF、Teacher BFF、Student/Parent BFF
- 微前端（Next.js + React）：4 个 shell 应用
- 共享包（TS）：packages/*

### 2.2 TypeScript 类型规则（沿用 CICD）

1. **禁止 `any`**：未知类型用 `unknown` 并做类型守卫。极特殊情况需 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 并注释原因
2. **优先 `interface` 描述对象形状**，`type` 用于联合、交叉、映射类型
3. **不使用 `as` 断言**，除非从 `unknown` 强制转换或测试中（需注释原因）。可用 `satisfies` 保持类型推导
4. **函数返回值必须显式标注**，特别是 `Promise<T>`
5. **可选链后禁止跟非空断言 `!`**（`x?.y!` 是矛盾的）
6. **所有仅用于类型的导入必须使用 `import type`**
7. **避免 `object` 或 `{}` 作为类型**，使用 `Record<string, unknown>` 或具体接口
8. **泛型使用有意义的名称**：`TData`、`TResponse`，避免单字母 `T`

### 2.3 导入顺序（强制执行）

```typescript
// 1. Node.js 内置
import { readFile } from "node:fs/promises";
// 2. 第三方库
import { Controller, Get } from "@nestjs/common";
import { z } from "zod";
// 3. monorepo 内部包（@edu/* 别名）
import { requirePermission } from "@edu/auth";
import { User } from "@edu/contracts";
// 4. 服务内绝对路径（@/ 别名，仅服务内）
import { UserService } from "@/modules/user/user.service";
// 5. 相对路径导入
import { CreateUserDto } from "./dto/create-user.dto";
// 6. 类型导入
import type { UserEntity } from "@/modules/user/domain/user.entity";
```

使用 `eslint-plugin-import` 规则 `import/order` 自动排序，分组间空一行。

### 2.4 NestJS 模块规范

#### 2.4.1 模块组织

每个 NestJS 模块对应一个 DDD 聚合，结构见 [project_rules.md §3.3](../../.trae/rules/project_rules.md#33-模块标准结构ddd)。

#### 2.4.2 装饰器规则

```typescript
@Controller("users")
@RequirePermission(Permissions.USER_READ)
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(":id")
  @RequirePermission(Permissions.USER_READ)
  async getUser(@Param("id", ParseUUIDPipe) id: string): Promise<UserResponse> {
    return this.userService.findById(id);
  }

  @Post()
  @RequirePermission(Permissions.USER_CREATE)
  @HttpCode(HttpStatus.CREATED)
  async createUser(@Body() dto: CreateUserDto): Promise<UserResponse> {
    return this.userService.create(dto);
  }
}
```

**规则**：

- Controller 必须使用 `@Controller(path)` 装饰器，path 使用 kebab-case 复数
- Controller 类必须使用 `@RequirePermission()` 装饰器（类级默认权限）
- 每个 Handler 可选覆盖类级权限（更细粒度）
- Handler 必须显式标注返回类型（`Promise<T>`）
- DTO 必须使用 `class-validator` 装饰器校验
- 参数校验使用 Pipe（`ParseUUIDPipe`、`ValidationPipe` 等）

#### 2.4.3 依赖注入规则

```typescript
@Injectable()
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly eventBus: EventBus,
  ) {}
}
```

**规则**：

- 依赖通过构造函数注入，使用 `readonly` 修饰符
- 接口绑定在 Module 的 `providers` 中：`{ provide: "UserRepository", useClass: UserRepoImpl }`
- 禁止使用属性注入（`@Inject()` 属性装饰器）
- 循环依赖通过 `forwardRef` 解决，但需在 PR 中说明原因

#### 2.4.4 Module 规则

```typescript
@Module({
  imports: [DatabaseModule, AuthModule],
  controllers: [UserController],
  providers: [
    UserService,
    { provide: "UserRepository", useClass: UserRepoImpl },
  ],
  exports: [UserService],
})
export class UserModule {}
```

**规则**：

- Module 类名 PascalCase + `Module` 后缀
- `exports` 仅暴露 Application Service，不暴露 Repository
- 跨 Module 通信通过 exports 的 Service，不直接访问对方 Repository

### 2.5 CQRS 规范

```typescript
export class CreateUserCommand {
  constructor(
    public readonly email: string,
    public readonly name: string,
  ) {}
}

@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
  async execute(command: CreateUserCommand): Promise<string> {
    // 1. 加载聚合（如有）
    // 2. 调用聚合方法（领域逻辑）
    // 3. 持久化（Repository）
    // 4. 写入 Outbox（同一事务）
    // 5. 返回结果
  }
}

export class GetUserByIdQuery {
  constructor(public readonly id: string) {}
}

@QueryHandler(GetUserByIdQuery)
export class GetUserByIdHandler implements IQueryHandler<GetUserByIdQuery> {
  async execute(query: GetUserByIdQuery): Promise<UserReadModel> {
    // 直接查询读模型（ClickHouse / ES / Redis），不走主库
  }
}
```

**规则**：

- Command 走写路径：Command → Handler → Domain → Repository → MySQL + Outbox
- Query 走读路径：Query → Handler → Read Model（禁止查主库）
- Command Handler 必须在事务内写 Outbox 表
- Query Handler 必须从读模型读，**禁止在 Query 中调用主库**

### 2.6 Domain Entity 规范

```typescript
export class UserEntity {
  private constructor(
    public readonly id: string,
    private email: string,
    private name: string,
    private readonly createdAt: Date,
  ) {}

  static create(props: UserCreateProps): UserEntity {
    return new UserEntity(
      crypto.randomUUID(),
      props.email,
      props.name,
      new Date(),
    );
  }

  rename(newName: string): UserRenamedEvent {
    this.name = newName;
    return new UserRenamedEvent(this.id, newName);
  }
}
```

**规则**：

- Entity 构造函数私有，通过静态工厂方法创建
- Entity 字段私有，通过方法变更状态
- 状态变更方法返回领域事件，由 Application Service 发布
- 聚合根负责维护自身不变式

### 2.7 DTO 与验证

```typescript
import { IsEmail, IsString, MinLength } from "class-validator";

export class CreateUserDto {
  @IsEmail()
  readonly email!: string;

  @IsString()
  @MinLength(2)
  readonly name!: string;
}
```

**规则**：

- DTO 类名 `Create[Entity]Dto` / `Update[Entity]Dto` / `[Entity]Response`
- DTO 字段使用 `readonly` 修饰
- 使用 `class-validator` 装饰器校验
- 全局启用 `ValidationPipe`：`whitelist: true, forbidNonWhitelisted: true, transform: true`

### 2.8 React 组件规范（沿用 CICD）

- 组件必须为**纯函数**，使用 `function` 声明（非箭头函数）
- 页面组件（`page.tsx`）使用**默认导出**；其余所有组件使用**具名导出**
- 默认服务端组件，需要交互时才在文件第一行添加 `"use client"`
- **禁止在渲染期间**修改外部变量、执行网络请求、读取/写入 DOM
- **不使用 `React.FC`**，直接用函数声明 + 显式标注 props 类型
- 组件行数 ≤ 500 行（复杂表单/大型表格可放宽至 800 行）

### 2.9 Hook 规范（沿用 CICD）

- 命名以 `use` 开头，驼峰式
- **单一职责**：一个 Hook 只做一件事
- 返回值使用**对象形式**（非数组）
- 必须编写 JSDoc
- 所有 `useEffect` 必须提供**清理函数**
- `useEffect` 依赖数组必须**完整**

### 2.10 状态管理（沿用 CICD 5 层模型）

| 层级               | 场景                 | 方案                          |
| ------------------ | -------------------- | ----------------------------- |
| L1 URL             | 可分享、可刷新的状态 | nuqs                          |
| L2 Server          | 服务端数据           | TanStack Query                |
| L3 Client Business | 客户端业务状态       | Zustand slice                 |
| L4 Global UI       | 全局 UI 状态         | Zustand ui-store + ModalRoot  |
| L5 Form            | 表单状态             | react-hook-form + zodResolver |

---

## 三、Go（Gin）规范

### 3.1 适用范围

- API Gateway（gateway/）
- 未来可能的 sidecar 或 CLI 工具

### 3.2 包结构

```
gateway/
├─ cmd/server/main.go
├─ internal/
│  ├─ config/         # 配置加载
│  ├─ handler/        # HTTP 处理器
│  ├─ middleware/     # 中间件
│  ├─ router/         # 路由注册
│  ├─ client/         # 下游 gRPC 客户端
│  └─ pkg/            # 服务内工具
└─ api/               # OpenAPI 定义
```

### 3.3 命名规范

- 包名：小写单数，不使用下划线或驼峰（`config`、`handler`、`middleware`）
- 文件名：snake_case（`user_handler.go`、`rate_limiter.go`）
- 导出标识符：PascalCase（`UserService`、`HandleLogin`）
- 未导出标识符：camelCase（`userService`、`validateToken`）
- 接口：PascalCase，倾向于在消费方定义，命名按行为描述（`UserFetcher`、`TokenValidator`）

### 3.4 错误处理

```go
// ✅ 显式处理错误
func (h *UserHandler) GetUser(c *gin.Context) {
    id := c.Param("id")
    user, err := h.userService.GetUser(c.Request.Context(), id)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
            return
        }
        h.logger.Error("get user failed", "err", err, "id", id)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
        return
    }
    c.JSON(http.StatusOK, user)
}

// ❌ 禁止忽略错误
user, _ := h.userService.GetUser(ctx, id)
```

**规则**：

- 错误必须显式处理，**禁止 `_ = err`**
- 使用 `errors.Is` 和 `errors.As` 判断错误类型，禁止字符串匹配
- 自定义错误类型使用 `fmt.Errorf("...: %w", err)` 包装
- 错误日志必须包含上下文（请求 ID、用户 ID、关键参数）
- 对外返回的错误信息禁止泄露内部实现（堆栈、SQL 语句等）

### 3.5 Context 传递

```go
// ✅ context 作为第一个参数
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    // ...
}

// ❌ 禁止 context 作为结构体字段
type UserService struct {
    ctx context.Context  // 禁止
}
```

**规则**：

- `context.Context` 作为函数第一个参数传递
- **禁止**将 context 存储在结构体字段中
- 超时/取消通过 context 传递，禁止使用 `time.Sleep` 等待
- 使用 `ctx.Err()` 检查取消信号

### 3.6 并发规范

```go
import "golang.org/x/sync/errgroup"

g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return fetchUser(ctx) })
g.Go(func() error { return fetchProfile(ctx) })
if err := g.Wait(); err != nil {
    return err
}
```

**规则**：

- 优先使用 `errgroup` 管理并发 goroutine
- 禁止裸 `go func()` 不带 recover 和 context
- 共享状态使用 channel 或 `sync` 包，禁止使用 `sync.Mutex` 嵌套锁
- goroutine 必须可被 context 取消

### 3.7 Gin 处理器规范

```go
func RegisterRoutes(r *gin.Engine, h *Handler, mw *Middleware) {
    r.Use(gin.Recovery(), mw.RequestID(), mw.Logger(), mw.RateLimit())

    v1 := r.Group("/api/v1")
    {
        v1.GET("/users/:id", mw.Auth(), h.GetUser)
        v1.POST("/users", mw.Auth(), mw.RequirePermission("user:create"), h.CreateUser)
    }
}
```

**规则**：

- 路由分组按 API 版本（`/api/v1`）
- 中间件链顺序：Recovery → RequestID → Logger → RateLimit → Auth → RequirePermission
- Handler 函数签名固定：`func(c *gin.Context)`
- 响应统一使用 `c.JSON(status, body)`，禁止直接写 `c.Writer.Write`
- 错误响应格式统一：`{"error": "message", "code": "ERROR_CODE", "request_id": "..."}`

### 3.8 日志规范

```go
import "log/slog"

logger := slog.With("service", "gateway", "request_id", requestID)
logger.Info("user login", "user_id", userID, "ip", ip)
```

**规则**：

- 使用标准库 `log/slog` 结构化日志
- 日志字段使用 kebab-case key
- 必须包含 `request_id` 用于链路追踪
- 禁止使用 `fmt.Println` 或 `log.Printf` 输出业务日志

---

## 四、Python（FastAPI）规范

### 4.1 适用范围

- 分析/AI 服务：insight-ai（知识图谱、AI 备课、智能推荐等）
- 数据处理脚本

### 4.2 包结构

```
services/insight-ai/
├─ src/
│  ├─ main.py                 # FastAPI 启动入口
│  ├─ core/
│  │  ├─ config.py            # Pydantic Settings
│  │  └─ logging.py           # 结构化日志
│  ├─ api/v1/endpoints/       # 路由处理器
│  ├─ models/                 # Pydantic 模型
│  ├─ services/               # 业务逻辑
│  ├─ repositories/           # 数据访问
│  └─ clients/                # 外部客户端
└─ tests/
```

### 4.3 命名规范

- 模块/包：snake_case（`user_service.py`、`knowledge_graph`）
- 类：PascalCase（`UserService`、`KnowledgeGraphModel`）
- 函数/变量：snake_case（`get_user`、`is_active`）
- 常量：UPPER_SNAKE_CASE（`MAX_RETRY_COUNT`）
- 布尔变量：`is/has/can/should` 前缀（`is_active`、`has_permission`）

### 4.4 类型注解（强制）

```python
# ✅ 所有函数必须标注类型
async def get_user(user_id: str) -> UserResponse:
    user = await user_repository.get_by_id(user_id)
    if user is None:
        raise UserNotFoundError(user_id)
    return UserResponse.model_validate(user)

# ❌ 禁止无类型注解
def get_user(user_id):
    ...
```

**规则**：

- 所有函数必须标注参数和返回值类型
- 使用 `from __future__ import annotations` 启用延迟注解求值
- 使用 `Optional[T]` 或 `T | None`（Python 3.10+）标注可选类型
- 容器类型使用泛型：`list[User]`、`dict[str, Any]`（不用 `List`、`Dict`）
- mypy 严格模式：`disallow_untyped_defs = true`

### 4.5 异步规范

```python
# ✅ I/O 操作必须 async
async def fetch_user(user_id: str) -> User:
    async with httpx.AsyncClient() as client:
        response = await client.get(f"/users/{user_id}")
        return User.model_validate(response.json())

# ❌ 禁止在 async 函数中调用同步 I/O
async def fetch_user(user_id: str) -> User:
    requests.get(f"/users/{user_id}")  # 阻塞事件循环
```

**规则**：

- I/O 操作（HTTP、DB、文件）必须使用 async/await
- 禁止在 async 函数中调用同步阻塞 I/O，必须用 `asyncio.to_thread` 或 async 客户端
- CPU 密集任务用 `asyncio.to_thread` 或进程池
- 并发请求使用 `asyncio.gather` 或 `asyncio.TaskGroup`

### 4.6 Pydantic 模型

```python
from pydantic import BaseModel, Field, field_validator

class UserCreateRequest(BaseModel):
    email: str = Field(..., description="用户邮箱")
    name: str = Field(..., min_length=2, max_length=50, description="用户姓名")

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("invalid email")
        return v

class UserResponse(BaseModel):
    id: str
    email: str
    name: str
    created_at: datetime
```

**规则**：

- 请求模型命名 `[Action][Entity]Request`，响应模型命名 `[Entity]Response`
- 必须使用 `Field` 添加描述、约束
- 复杂校验使用 `@field_validator` 或 `@model_validator`
- ORM 模型转换使用 `model_config = ConfigDict(from_attributes=True)`

### 4.7 FastAPI 路由规范

```python
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter(prefix="/api/v1/users", tags=["users"])

@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
    user_id: str,
    current_user: User = Depends(get_current_user),
) -> UserResponse:
    require_permission(current_user, "user:read")
    user = await user_service.get_user(user_id)
    if user is None:
        raise HTTPException(status_code=404, detail="user not found")
    return user
```

**规则**：

- 路由分组使用 `APIRouter`，按 API 版本组织
- 必须标注 `response_model`
- 权限校验通过 `Depends` 注入，每个 endpoint 显式调用 `require_permission`
- 路径参数使用 `str` 类型注解
- 错误通过 `HTTPException` 抛出，禁止直接返回错误 dict

### 4.8 配置规范

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="INSIGHT_AI_")

    database_url: str
    kafka_servers: list[str]
    log_level: str = "INFO"
    enable_ai_cache: bool = True

settings = Settings()
```

**规则**：

- 配置使用 `pydantic-settings` 的 `BaseSettings`
- 环境变量前缀按服务名（`INSIGHT_AI_`）
- 禁止在业务代码中直接读取环境变量（`os.getenv`），统一通过 `settings`

---

## 五、跨语言通用规范

### 5.1 命名通用规则

| 对象             | 风格                     | 示例                                      |
| ---------------- | ------------------------ | ----------------------------------------- |
| 目录             | kebab-case               | `user-profile/`                           |
| 常量             | UPPER_SNAKE_CASE         | `MAX_RETRY_COUNT`                         |
| 布尔值           | `is/has/can/should` 前缀 | `isVisible`、`is_active`、`hasPermission` |
| 类/接口/结构体   | PascalCase               | `UserService`、`UserFetcher`              |
| 服务名           | 小写单数                 | `identity`、`teaching`                    |
| Kafka topic      | 点分小写                 | `edu.identity.user.created`               |
| protobuf message | PascalCase               | `UserCreatedEvent`                        |
| protobuf 字段    | snake_case               | `user_id`、`created_at`                   |

### 5.2 文件行数通用规则

| 文件类型                  | 建议行数 | 硬性上限               |
| ------------------------- | -------- | ---------------------- |
| 配置/常量/类型/proto      | 无限制   | 无限制                 |
| React 组件                | ≤ 500    | 800                    |
| NestJS Controller/Service | ≤ 500    | 800                    |
| Go handler/middleware     | ≤ 400    | 600                    |
| Python endpoint/service   | ≤ 400    | 600                    |
| 工具函数                  | ≤ 40     | -                      |
| 自定义 Hook               | ≤ 80     | -                      |
| **任何文件**              | -        | **1000，超过必须拆分** |

### 5.3 错误处理通用规则

| 语言       | 规则                                                           |
| ---------- | -------------------------------------------------------------- |
| TypeScript | 错误通过抛出异常，Application Service 必须捕获并转为结构化响应 |
| Go         | 错误必须显式处理，禁止 `_ = err`，使用 `errors.Is/As` 判断类型 |
| Python     | 使用异常层次结构，自定义异常继承 `Exception`，禁止裸 `except:` |

**通用规则**：

- 错误信息对内详细（含上下文、堆栈），对外脱敏（不泄露实现细节）
- 错误必须分类：业务错误（4xx）、系统错误（5xx）、依赖错误（502/503）
- 错误必须记录日志，包含 request_id 用于链路追踪
- 重试逻辑仅用于幂等操作和瞬时错误（网络抖动、超时）

### 5.4 日志通用规则

| 语言       | 工具                 | 说明             |
| ---------- | -------------------- | ---------------- |
| TypeScript | NestJS Logger + pino | 结构化 JSON 日志 |
| Go         | log/slog             | 标准库结构化日志 |
| Python     | structlog 或 loguru  | 结构化 JSON 日志 |

**通用规则**：

- 日志必须结构化（JSON），禁止纯文本
- 必须包含 `timestamp`、`level`、`service`、`request_id`、`trace_id`
- 日志级别：DEBUG（开发）、INFO（关键业务）、WARN（异常可恢复）、ERROR（系统错误）
- 禁止打印敏感信息（密码、token、身份证号、信用卡号）
- 采样规则：高频日志（如请求日志）按比例采样，错误日志全量保留

### 5.5 测试通用规则

| 语言       | 单元测试框架              | 覆盖率目标 |
| ---------- | ------------------------- | ---------- |
| TypeScript | Vitest + nestjs/testing   | ≥ 80%      |
| Go         | 标准 testing 包 + testify | ≥ 80%      |
| Python     | pytest + pytest-asyncio   | ≥ 80%      |

**通用规则**：

- 测试文件与源文件同目录或 `tests/` 子目录
- 命名：`*.test.ts` / `*_test.go` / `test_*.py`
- 测试描述说明预期行为（"should disable button while loading"）
- Mock 仅用于外部边界（数据库、外部 API），内部逻辑须真实运行
- E2E 测试覆盖核心业务路径
- 集成测试使用 Testcontainers（DB/Kafka）

### 5.6 注释通用规则

- **不写废话注释**：不重复代码已表达的信息
- **写 why 不写 what**：解释为什么这样做，不解释做了什么
- 复杂业务逻辑必须有注释，引用需求文档或 PR 链接
- TODO 必须附带 issue 编号：`// TODO(JIRA-123): handle edge case`
- 公共 API 必须有文档注释（JSDoc / godoc / docstring）

---

## 六、protobuf 契约规范

### 6.1 文件组织

```
packages/contracts/proto/
├─ identity/
│  ├─ user.proto
│  ├─ role.proto
│  └─ permission.proto
├─ org/
├─ teaching/
├─ content/
├─ comm/
├─ insight/
├─ auth/
└─ notification/
```

### 6.2 文件头模板

```protobuf
syntax = "proto3";

package edu.identity.user.v1;

import "google/protobuf/timestamp.proto";

option go_package = "edu/contracts/identity/user/v1;userv1";

// 用户服务契约。
// 提供用户 CRUD、角色绑定、权限查询能力。
service UserService {
  // 创建用户
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  // 查询用户
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
```

### 6.3 命名规则

- **包名**：`edu.[context].[aggregate].v[version]`，如 `edu.identity.user.v1`
- **Service 名**：PascalCase + `Service` 后缀，如 `UserService`、`NotificationService`
- **RPC 方法名**：PascalCase 动词开头，如 `CreateUser`、`GetUser`、`ListUsers`
- **Message 名**：PascalCase + 请求/响应后缀，如 `CreateUserRequest`、`CreateUserResponse`
- **字段名**：snake_case，如 `user_id`、`created_at`、`is_active`
- **枚举名**：PascalCase，枚举值 UPPER_SNAKE_CASE 并以枚举名为前缀

### 6.4 字段规则

```protobuf
message User {
  string user_id = 1;          // 用户唯一 ID（UUID）
  string email = 2;            // 邮箱（唯一）
  string name = 3;             // 姓名
  google.protobuf.Timestamp created_at = 4;  // 创建时间
  UserStatus status = 5;       // 用户状态
}

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;  // 未指定（必须从 0 开始）
  USER_STATUS_ACTIVE = 1;       // 活跃
  USER_STATUS_DISABLED = 2;     // 禁用
}
```

**规则**：

- 字段编号禁止复用，删除字段必须 `reserved` 标记
- 枚举第一个值必须为 `*_UNSPECIFIED = 0`
- 时间使用 `google.protobuf.Timestamp`，不使用 string
- 字段必须有 `//` 注释说明用途
- 字段命名与领域事件保持一致

### 6.5 事件 schema

```protobuf
message UserCreatedEvent {
  string event_id = 1;          // 事件唯一 ID（用于幂等）
  string aggregate_id = 2;      // 聚合根 ID
  string aggregate_type = 3;    // 聚合类型（如 "User"）
  string event_type = 4;        // 事件类型（如 "UserCreated"）
  google.protobuf.Timestamp occurred_at = 5;  // 发生时间
  bytes payload = 6;            // 事件负载（JSON 序列化）
  map<string, string> headers = 7;  // 元数据（trace_id 等）
}
```

### 6.6 版本化

- 破坏性变更必须升版本：`v1` → `v2`
- 新版本与旧版本并存，消费者逐步迁移
- 旧版本标记 `deprecated`，至少保留 2 个迭代周期
- 字段新增使用 `optional` 或 `repeated` 保持向后兼容
- 字段类型变更视为破坏性变更

### 6.7 校验工具

```bash
# Lint 检查
buf lint

# 破坏性变更检测
buf breaking --against '.git#branch=main'

# 代码生成（TS / Go / Python）
buf generate
```

**CI 强制**：每次 PR 必须通过 `buf lint` + `buf breaking`。

### 6.8 生成代码规则

- 生成代码位于各服务 `src/generated/`（TS）/ `gen/`（Go）/ `generated/`（Python）
- **禁止手改生成代码**
- 生成代码不纳入行数统计
- proto 变更后必须立即 `buf generate` 并提交生成代码

---

## 七、设计令牌规范

### 7.1 令牌分层（沿用 CICD 模型，迁移至微前端共享包）

| Layer             | 位置                                                          | 用途                                               |
| ----------------- | ------------------------------------------------------------- | -------------------------------------------------- |
| Layer 1 Primitive | `packages/ui-tokens/primitive.css`                            | 原始色板/字号/间距/阴影，业务代码不直接引用        |
| Layer 2 Semantic  | `packages/ui-tokens/semantic-light.css` + `semantic-dark.css` | 语义令牌，业务代码唯一引用入口                     |
| 模块命名空间      | `packages/ui-tokens/lesson-preparation.css`                   | `--lp-*` 令牌，明暗双份                            |
| Tailwind 暴露     | `packages/ui-tokens/tailwind-theme.css`                       | `@theme inline` 暴露为 `bg-*`/`text-*`/`font-*` 类 |

### 7.2 强制规则

- **禁止硬编码颜色**：TSX/TS/CSS 中不得出现 `#hex` 颜色字面量，使用 `hsl(var(--*))` 或 Tailwind 类 `bg-*`
- **禁止硬编码字体**：不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量，使用 `var(--font-family-sans/serif/mono)`
- **禁止硬编码字号**：不得出现 `font-size: Npx`，使用 `var(--font-size-1~9)`
- **禁止 Tailwind 任意值**：不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]`，映射到 `--space-*` 或 Tailwind 默认阶梯

### 7.3 豁免场景（需注释）

- PWA manifest（`apps/*/public/manifest.ts`）
- 邮件 HTML 内联样式（`services/notification/src/channels/`）
- 图表 SVG 固定画布尺寸
- loading.tsx 占位骨架
- Dialog 固定宽度等无法令牌化的设计固定尺寸

豁免写法：`// eslint-disable-next-line no-restricted-syntax -- <reason>`

### 7.4 排版差异化（用户偏好）

- 主文：serif 字体（`var(--font-family-serif)`，对应 Fraunces）
- UI：sans-serif 字体（`var(--font-family-sans)`，对应 Inter）
- 代码：mono 字体（`var(--font-family-mono)`，对应 JetBrains Mono）
- 界面风格：简洁干净，最少图标

### 7.5 ESLint 强制约束

- `no-restricted-syntax`：禁止 `#hex` 字面量
- `design-tokens/no-hardcoded-fonts`：禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量
- 白名单：`packages/ui-tokens/primitive.css`、`services/notification/src/channels/`、`apps/*/public/manifest.ts`

### 7.6 改令牌必同步图

修改令牌定义后，同步更新 `docs/architecture/001_architecture_overview.md` 与 arch.db（`pnpm run arch:scan`）。

---

## 八、安全规范

### 8.1 XSS 防护

- **禁止 `dangerlySetInnerHTML`**（如必须使用，先用 DOMPurify 清洗）
- React 默认转义，禁止绕过

### 8.2 认证与 Cookie

- JWT/session ID 存储在 `httpOnly` + `Secure` + `SameSite=Strict` 的 Cookie 中
- JWT 过期时间 ≤ 1 小时，refresh token ≤ 7 天
- refresh token 必须支持撤销

### 8.3 环境变量

- 服务端环境变量**不加** `NEXT_PUBLIC_` 前缀
- 客户端环境变量**必须加** `NEXT_PUBLIC_` 前缀，且仅暴露非敏感信息
- TS 使用 Zod / Go 使用 viper / Python 使用 `pydantic-settings` 在应用启动时校验
- 敏感配置通过 Vault 或 Kubernetes Secret 注入，禁止入仓

### 8.4 权限校验

- 每个 Controller/Handler/endpoint 必须调用 `@RequirePermission()` 等价物
- 前端组件禁止使用 `role === "xxx"` 硬编码，统一使用 `usePermission().hasPermission()`
- 权限点常量集中维护在 `packages/contracts/src/permissions.ts`

### 8.5 CSRF 防护

- 所有状态变更操作必须校验 Origin/Referer 头
- 对 SameSite Cookie 不足的场景，使用 CSRF token

### 8.6 依赖扫描

| 语言   | 工具                       |
| ------ | -------------------------- |
| TS     | `npm audit` + Snyk + Trivy |
| Go     | `govulncheck`              |
| Python | `pip-audit` + `safety`     |

高危漏洞阻断合并。

### 8.7 数据安全

- 密码使用 bcrypt（cost ≥ 12）或 argon2id 哈希
- 敏感字段（身份证号、手机号）加密存储
- 日志中禁止打印敏感信息（密码、token、身份证号）
- 审计日志保留 ≥ 180 天

### 8.8 gRPC 安全

- 内部 gRPC 启用 mTLS
- gRPC 请求必须携带调用方身份（metadata 中 `x-caller-id`、`x-trace-id`）
- 限制 gRPC 消息大小（默认 4MB），防止 DoS

---

## 九、测试规范

### 9.1 测试分层

| 层级     | TS                      | Go                       | Python                  | 覆盖率       |
| -------- | ----------------------- | ------------------------ | ----------------------- | ------------ |
| 单元测试 | Vitest                  | testing + testify        | pytest                  | ≥ 80%        |
| 集成测试 | Vitest + Testcontainers | testing + Testcontainers | pytest + Testcontainers | 关键流程     |
| E2E 测试 | Playwright              | -                        | -                       | 核心业务路径 |
| 契约测试 | pact + buf              | pact + buf               | pact + buf              | 服务间契约   |

### 9.2 测试命令

```bash
# TypeScript
pnpm run test:unit
pnpm run test:integration
pnpm run test:e2e

# Go
go test ./... -race -coverprofile=coverage.out

# Python
pytest tests/unit -v --cov=src --cov-report=term-missing
pytest tests/integration -v
```

### 9.3 编写规范

- 测试文件与源文件同目录或 `tests/` 子目录
- 命名：`*.test.ts` / `*_test.go` / `test_*.py`
- 测试描述说明预期行为：`it("should disable button while loading")`
- Mock 仅用于外部边界（API、数据库），内部逻辑须真实运行
- 异步测试必须等待结果，**禁止固定 `setTimeout` / `time.Sleep`**
- 集成测试使用 Testcontainers 启动真实依赖（MySQL、Redis、Kafka）

### 9.4 契约测试

- 每个微服务必须为自身对外暴露的 gRPC 接口编写 contract test
- 契约测试使用 pact 或 buf 的 breaking check
- 契约变更必须同步更新消费者，消费者 CI 必须验证契约兼容性

---

## 十、代码审查清单

审查者必须逐一确认：

### 10.1 架构与设计

- [ ] 命名表意清晰，无歧义
- [ ] 类型安全：无 `any` / `interface{}` / `Any`，无多余断言
- [ ] 文件/函数/组件行数符合规范
- [ ] 单一职责：每个文件/函数/组件只做一件事
- [ ] 限界上下文封装：无跨服务直接 DB 查询
- [ ] CQRS 分层：Command 不查读模型，Query 不写主库

### 10.2 实现质量

- [ ] 设计令牌符合主题，无硬编码颜色/字体/字号
- [ ] 错误处理完善：业务错误 4xx，系统错误 5xx
- [ ] 权限校验到位（Controller/Handler/endpoint + 前端 usePermission）
- [ ] 日志结构化，含 request_id/trace_id
- [ ] Outbox 模式：领域事件通过 Outbox 发布，未直接调 Kafka producer
- [ ] 幂等性：事件消费者已实现幂等

### 10.3 契约与事件

- [ ] proto 变更通过 `buf lint` + `buf breaking`
- [ ] proto 变更已 `buf generate` 并提交生成代码
- [ ] 新增 Kafka topic 已在 001 文档登记
- [ ] 事件 schema 已在 proto 中定义
- [ ] 事件版本化：破坏性变更已升版本

### 10.4 安全与合规

- [ ] 无 XSS 风险（无 `dangerlySetInnerHTML`）
- [ ] Cookie 安全（httpOnly + Secure + SameSite）
- [ ] 环境变量未泄露（服务端变量无 `NEXT_PUBLIC_` 前缀）
- [ ] 敏感信息未入日志
- [ ] 依赖无高危漏洞

### 10.5 测试与文档

- [ ] 关键逻辑有单元测试
- [ ] 新增接口有契约测试
- [ ] 服务 README 已更新
- [ ] `docs/architecture/001_architecture_overview.md` 已同步
- [ ] `docs/troubleshooting/known-issues.md` 已追加经验日志
- [ ] arch.db 已通过 `pnpm run arch:scan` 更新
- [ ] 提交信息规范，scope 为服务名/包名

---

## 附录：与 CICD 单应用规范的差异

| 项目         | CICD 单应用                       | Edu 微服务                                   | 原因           |
| ------------ | --------------------------------- | -------------------------------------------- | -------------- |
| 项目结构     | 单 Next.js 应用                   | 多语言 monorepo                              | 微服务拆分     |
| 数据获取层   | `modules/[module]/data-access.ts` | NestJS Repository + Domain Entity            | DDD 分层       |
| 中间件       | `proxy.ts`（Next.js 16）          | Go Gin Gateway                               | 网关独立       |
| 通信         | 函数调用                          | gRPC + Kafka                                 | 跨进程通信     |
| 状态管理     | Zustand + Context + nuqs          | 沿用                                         | 团队熟悉       |
| 环境变量校验 | `@t3-oss/env-nextjs` + Zod        | TS Zod / Go viper / Python pydantic-settings | 多语言         |
| 行数限制     | 单一规范                          | 按语言分档                                   | 各语言惯例     |
| 契约         | 无（内部函数调用）                | protobuf + buf                               | 跨服务通信需要 |
| 事件驱动     | 无                                | Kafka + Outbox                               | 微服务最终一致 |
| 设计令牌     | `src/app/styles/tokens/`          | `packages/ui-tokens/`                        | 微前端共享     |
