Files
Edu/docs/standards/coding-standards.md
SpecialX 2ba4250165
Some checks failed
CI Go / test (push) Has been cancelled
CI Proto / lint (push) Has been cancelled
CI Python / test (push) Has been cancelled
CI TypeScript / test (push) Has been cancelled
feat(p1): complete P1 foundation stage
- monorepo: pnpm workspace + go.work + pyproject.toml + commitlint/husky
- infra: docker-compose (minimal + full profiles) + init-sql + prometheus
- arch-scan: multi-language scanner skeleton (TS/Go/Python/Proto)
- shared-proto: buf v2 + classes.proto (ClassService CRUD contract)
- api-gateway: Go/Gin + JWT HS256 auth + reverse proxy + request ID
- classes: NestJS golden template (error system + observability + middleware + CRUD + tests)
- teacher-portal: Next.js + paper-feel UI design system
- CI/CD: 4 workflows (go/ts/py/proto)
- docs: migration guide + project_rules + coding-standards + git-workflow + ui-design-system + 004 + 9 module READMEs + known-issues + spec/plan migration + roadmap
2026-07-07 23:39:37 +08:00

980 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Edu 多语言编码规范
> 版本1.0
> 日期2026-07-07
> 状态:基线发布
> 适用范围Edu 微服务架构TS + Go + Python + protobuf
> 关联文档:
> - [项目规则](../../project_rules.md)
> - [迁移指南](../../MIGRATION_GUIDE.md)
> - [Git 工作流](./git-workflow.md)
> - [架构总览](../architecture/001_architecture_overview.md)
---
## 目录
1. [项目原则与理念](#一项目原则与理念)
2. [TypeScriptNestJS规范](#二typescriptnestjs规范)
3. [GoGin规范](#三gogin规范)
4. [PythonFastAPI规范](#四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 或事件
---
## 二、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. **泛型使用有意义的名称**`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 §4.1](../../project_rules.md#41-nestjs-业务微服务标准结构)。
#### 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 |
---
## 三、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 命名规范
- 包名:小写单数,不使用下划线或驼峰(`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` 输出业务日志
---
## 四、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_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 测试覆盖核心业务路径
- 集成测试使用 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 文件头模板
```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; // 用户唯一 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
```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
- 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.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 数据安全
- 密码使用 bcryptcost ≥ 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/` | 微前端共享 |