- 根目录 project_rules.md 损坏(72 字节乱码),已删除 - 从 CICD 项目迁移完整版至 .trae/rules/project_rules.md(17881 字节) - 更新 MIGRATION_GUIDE/README/coding-standards 引用至 .trae/rules/ - P0 紧急修复,影响所有 AI 工作流程
35 KiB
35 KiB
Edu 多语言编码规范
版本:1.0 日期:2026-07-07 状态:基线发布 适用范围:Edu 微服务架构(TS + Go + Python + protobuf) 关联文档:
目录
- 项目原则与理念
- TypeScript(NestJS)规范
- Go(Gin)规范
- Python(FastAPI)规范
- 跨语言通用规范
- protobuf 契约规范
- 设计令牌规范
- 安全规范
- 测试规范
- 代码审查清单
一、项目原则与理念
- 可读性优先于机巧:代码首先是写给队友看的
- 显式优于隐式:避免魔法值、隐式类型转换、隐式全局副作用
- 单一职责:每个文件、函数、组件只做一件事
- 防御性编程:永远假设输入可能是 null/None/nil 或非法格式
- 契约先行:跨服务通信先定 protobuf,再写实现
- 架构图优先:任何任务开始前先查阅 001 架构总览
- 限界上下文封装:跨上下文不直接查询对方 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)
- 禁止
any:未知类型用unknown并做类型守卫。极特殊情况需// eslint-disable-next-line @typescript-eslint/no-explicit-any并注释原因 - 优先
interface描述对象形状,type用于联合、交叉、映射类型 - 不使用
as断言,除非从unknown强制转换或测试中(需注释原因)。可用satisfies保持类型推导 - 函数返回值必须显式标注,特别是
Promise<T> - 可选链后禁止跟非空断言
!(x?.y!是矛盾的) - 所有仅用于类型的导入必须使用
import type - 避免
object或{}作为类型,使用Record<string, unknown>或具体接口 - 泛型使用有意义的名称:
TData、TResponse,避免单字母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装饰器校验 - 参数校验使用 Pipe(
ParseUUIDPipe、ValidationPipe等)
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装饰器校验 - 全局启用
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 错误处理
// ✅ 显式处理错误
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 传递
// ✅ 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.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 类型注解(强制)
# ✅ 所有函数必须标注类型
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 异步规范
# ✅ 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 模型
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-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 文件头模板
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 字段规则
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
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 校验工具
# 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 测试命令
# 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/ |
微前端共享 |