Files
Edu/docs/standards/coding-standards.md
SpecialX 3fbb97791d fix(docs): 修复 project_rules.md P0 损坏并同步所有引用
- 根目录 project_rules.md 损坏(72 字节乱码),已删除
- 从 CICD 项目迁移完整版至 .trae/rules/project_rules.md(17881 字节)
- 更新 MIGRATION_GUIDE/README/coding-standards 引用至 .trae/rules/
- P0 紧急修复,影响所有 AI 工作流程
2026-07-08 12:50:00 +08:00

35 KiB
Raw Permalink Blame History

Edu 多语言编码规范

版本1.0 日期2026-07-07 状态:基线发布 适用范围Edu 微服务架构TS + Go + Python + protobuf 关联文档:


目录

  1. 项目原则与理念
  2. TypeScriptNestJS规范
  3. GoGin规范
  4. PythonFastAPI规范
  5. 跨语言通用规范
  6. protobuf 契约规范
  7. 设计令牌规范
  8. 安全规范
  9. 测试规范
  10. 代码审查清单

一、项目原则与理念

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

二、TypeScriptNestJS规范

2.1 适用范围

  • 业务微服务NestJS 10identity、org、teaching、content、comm、insight
  • 基础设施服务NestJS 10auth、notification
  • BFF 层NestJS 10Admin BFF、Teacher BFF、Student/Parent BFF
  • 微前端Next.js + React4 个 shell 应用
  • 共享包TSpackages/*

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. 泛型使用有意义的名称TDataTResponse,避免单字母 T

2.3 导入顺序(强制执行)

// 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

2.4.2 装饰器规则

@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 装饰器校验
  • 参数校验使用 PipeParseUUIDPipeValidationPipe 等)

2.4.3 依赖注入规则

@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 规则

@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 规范

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 规范

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 与验证

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 装饰器校验
  • 全局启用 ValidationPipewhitelist: 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

三、GoGin规范

3.1 适用范围

  • API Gatewaygateway/
  • 未来可能的 sidecar 或 CLI 工具

3.2 包结构

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

3.3 命名规范

  • 包名:小写单数,不使用下划线或驼峰(confighandlermiddleware
  • 文件名snake_caseuser_handler.gorate_limiter.go
  • 导出标识符PascalCaseUserServiceHandleLogin
  • 未导出标识符camelCaseuserServicevalidateToken
  • 接口PascalCase倾向于在消费方定义命名按行为描述UserFetcherTokenValidator

3.4 错误处理

// ✅ 显式处理错误
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.Iserrors.As 判断错误类型,禁止字符串匹配
  • 自定义错误类型使用 fmt.Errorf("...: %w", err) 包装
  • 错误日志必须包含上下文(请求 ID、用户 ID、关键参数
  • 对外返回的错误信息禁止泄露内部实现堆栈、SQL 语句等)

3.5 Context 传递

// ✅ 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 并发规范

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 处理器规范

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 日志规范

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.Printlnlog.Printf 输出业务日志

四、PythonFastAPI规范

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_caseuser_service.pyknowledge_graph
  • PascalCaseUserServiceKnowledgeGraphModel
  • 函数/变量snake_caseget_useris_active
  • 常量UPPER_SNAKE_CASEMAX_RETRY_COUNT
  • 布尔变量:is/has/can/should 前缀(is_activehas_permission

4.4 类型注解(强制)

# ✅ 所有函数必须标注类型
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 | NonePython 3.10+)标注可选类型
  • 容器类型使用泛型:list[User]dict[str, Any](不用 ListDict
  • mypy 严格模式:disallow_untyped_defs = true

4.5 异步规范

# ✅ 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.gatherasyncio.TaskGroup

4.6 Pydantic 模型

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 路由规范

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 配置规范

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-settingsBaseSettings
  • 环境变量前缀按服务名(INSIGHT_AI_
  • 禁止在业务代码中直接读取环境变量(os.getenv),统一通过 settings

五、跨语言通用规范

5.1 命名通用规则

对象 风格 示例
目录 kebab-case user-profile/
常量 UPPER_SNAKE_CASE MAX_RETRY_COUNT
布尔值 is/has/can/should 前缀 isVisibleis_activehasPermission
类/接口/结构体 PascalCase UserServiceUserFetcher
服务名 小写单数 identityteaching
Kafka topic 点分小写 edu.identity.user.created
protobuf message PascalCase UserCreatedEvent
protobuf 字段 snake_case user_idcreated_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禁止纯文本
  • 必须包含 timestamplevelservicerequest_idtrace_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 测试覆盖核心业务路径
  • 集成测试使用 TestcontainersDB/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 文件头模板

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 后缀,如 UserServiceNotificationService
  • RPC 方法名PascalCase 动词开头,如 CreateUserGetUserListUsers
  • Message 名PascalCase + 请求/响应后缀,如 CreateUserRequestCreateUserResponse
  • 字段名snake_caseuser_idcreated_atis_active
  • 枚举名PascalCase枚举值 UPPER_SNAKE_CASE 并以枚举名为前缀

6.4 字段规则

message User {
  string user_id = 1;          // 用户唯一 IDUUID
  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

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 版本化

  • 破坏性变更必须升版本:v1v2
  • 新版本与旧版本并存,消费者逐步迁移
  • 旧版本标记 deprecated,至少保留 2 个迭代周期
  • 字段新增使用 optionalrepeated 保持向后兼容
  • 字段类型变更视为破坏性变更

6.7 校验工具

# 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 manifestapps/*/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
  • UIsans-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.cssservices/notification/src/channels/apps/*/public/manifest.ts

7.6 改令牌必同步图

修改令牌定义后,同步更新 docs/architecture/001_architecture_overview.md 与 arch.dbpnpm run arch:scan)。


八、安全规范

8.1 XSS 防护

  • 禁止 dangerlySetInnerHTML(如必须使用,先用 DOMPurify 清洗)
  • React 默认转义,禁止绕过
  • 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 数据安全

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

8.8 gRPC 安全

  • 内部 gRPC 启用 mTLS
  • gRPC 请求必须携带调用方身份metadata 中 x-caller-idx-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 测试命令

# 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.tsNext.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/ 微前端共享