# 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` 5. **可选链后禁止跟非空断言 `!`**(`x?.y!` 是矛盾的) 6. **所有仅用于类型的导入必须使用 `import type`** 7. **避免 `object` 或 `{}` 作为类型**,使用 `Record` 或具体接口 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 { return this.userService.findById(id); } @Post() @RequirePermission(Permissions.USER_CREATE) @HttpCode(HttpStatus.CREATED) async createUser(@Body() dto: CreateUserDto): Promise { return this.userService.create(dto); } } ``` **规则**: - Controller 必须使用 `@Controller(path)` 装饰器,path 使用 kebab-case 复数 - Controller 类必须使用 `@RequirePermission()` 装饰器(类级默认权限) - 每个 Handler 可选覆盖类级权限(更细粒度) - Handler 必须显式标注返回类型(`Promise`) - 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 { async execute(command: CreateUserCommand): Promise { // 1. 加载聚合(如有) // 2. 调用聚合方法(领域逻辑) // 3. 持久化(Repository) // 4. 写入 Outbox(同一事务) // 5. 返回结果 } } export class GetUserByIdQuery { constructor(public readonly id: string) {} } @QueryHandler(GetUserByIdQuery) export class GetUserByIdHandler implements IQueryHandler { async execute(query: GetUserByIdQuery): Promise { // 直接查询读模型(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 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 -- ` ### 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/` | 微前端共享 |