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
This commit is contained in:
979
docs/standards/coding-standards.md
Normal file
979
docs/standards/coding-standards.md
Normal file
@@ -0,0 +1,979 @@
|
||||
# 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. [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 §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 |
|
||||
|
||||
---
|
||||
|
||||
## 三、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/` | 微前端共享 |
|
||||
918
docs/standards/git-workflow.md
Normal file
918
docs/standards/git-workflow.md
Normal file
@@ -0,0 +1,918 @@
|
||||
# Edu Git 工作流规范
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-07
|
||||
> 状态:基线发布
|
||||
> 适用范围:Edu 多语言 monorepo(pnpm workspace + go.work + pyproject.toml)
|
||||
> 关联文档:
|
||||
> - [项目规则](../../project_rules.md)
|
||||
> - [编码规范](./coding-standards.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [架构总览](../architecture/001_architecture_overview.md)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [分支策略](#一分支策略)
|
||||
2. [Conventional Commits 规范](#二conventional-commits-规范)
|
||||
3. [commitlint + husky 配置](#三commitlint--husky-配置)
|
||||
4. [PR 与 Code Review](#四pr-与-code-review)
|
||||
5. [文档同步规则](#五文档同步规则)
|
||||
6. [多语言 monorepo 提交规则](#六多语言-monorepo-提交规则)
|
||||
7. [版本与发布](#七版本与发布)
|
||||
8. [紧急回滚](#八紧急回滚)
|
||||
9. [附录:CICD 与 Edu Git 工作流差异](#九附录cicd-与-edu-git-工作流差异)
|
||||
|
||||
---
|
||||
|
||||
## 一、分支策略
|
||||
|
||||
### 1.1 主干开发(Trunk-Based Development)
|
||||
|
||||
本项目采用**主干开发**模式,所有变更最终合并至 `main` 分支。
|
||||
|
||||
**核心原则**:
|
||||
- `main` 分支始终保持可发布状态
|
||||
- 短生命周期特性分支(通常 ≤ 3 天)
|
||||
- 频繁集成,每天至少一次 rebase/merge 至最新 `main`
|
||||
- 通过特性开关(Feature Flag)控制未完成功能的暴露
|
||||
|
||||
### 1.2 分支模型
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit id: "init"
|
||||
commit id: "P1-foundation"
|
||||
branch feat/identity-service
|
||||
checkout feat/identity-service
|
||||
commit id: "scaffold"
|
||||
commit id: "implement-login"
|
||||
commit id: "add-tests"
|
||||
checkout main
|
||||
merge feat/identity-service tag: "v0.2.0"
|
||||
branch fix/jwt-expiry
|
||||
checkout fix/jwt-expiry
|
||||
commit id: "fix-token-refresh"
|
||||
checkout main
|
||||
merge fix/jwt-expiry tag: "v0.2.1"
|
||||
branch release/v0.3
|
||||
checkout release/v0.3
|
||||
commit id: "freeze"
|
||||
commit id: "hotfix"
|
||||
checkout main
|
||||
merge release/v0.3 tag: "v0.3.0"
|
||||
```
|
||||
|
||||
### 1.3 分支命名规范
|
||||
|
||||
| 分支类型 | 前缀 | 示例 | 生命周期 |
|
||||
|---------|------|------|---------|
|
||||
| 主干 | `main` | `main` | 永久 |
|
||||
| 特性 | `feat/` | `feat/identity-service` | ≤ 3 天 |
|
||||
| 修复 | `fix/` | `fix/jwt-expiry` | ≤ 1 天 |
|
||||
| 重构 | `refactor/` | `refactor/split-data-access` | ≤ 5 天 |
|
||||
| 性能 | `perf/` | `perf/query-optimization` | ≤ 3 天 |
|
||||
| 文档 | `docs/` | `docs/api-specification` | ≤ 2 天 |
|
||||
| 发布 | `release/v` | `release/v0.3.0` | 发布周期内 |
|
||||
| 热修复 | `hotfix/` | `hotfix/v0.3.1` | ≤ 1 天 |
|
||||
|
||||
**规则**:
|
||||
- 分支名使用 kebab-case
|
||||
- 一个分支只做一件事,禁止在一个分支内混合多个无关变更
|
||||
- 特性分支命名包含服务/模块名(`feat/identity-service` 而非 `feat/login`)
|
||||
|
||||
### 1.4 分支保护规则
|
||||
|
||||
**`main` 分支保护**:
|
||||
- 禁止直接 push,必须通过 PR
|
||||
- 至少 1 名 Reviewer 审批通过(核心模块需 2 名)
|
||||
- 所有 CI 检查通过(lint + typecheck + test + build)
|
||||
- 分支必须与 `main` 保持最新(无冲突或已 rebase)
|
||||
- 禁止 force push
|
||||
|
||||
**`release/*` 分支保护**:
|
||||
- 禁止直接 push,仅接受 cherry-pick 或特定 hotfix PR
|
||||
- 至少 2 名 Reviewer 审批
|
||||
- 发布完成后打 tag 并归档
|
||||
|
||||
---
|
||||
|
||||
## 二、Conventional Commits 规范
|
||||
|
||||
### 2.1 提交信息格式
|
||||
|
||||
所有提交信息必须遵循 [Conventional Commits 1.0.0](https://www.conventionalcommits.org/zh-hans/):
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### 2.2 类型(type)
|
||||
|
||||
| 类型 | 含义 | 是否触发发布 |
|
||||
|------|------|-------------|
|
||||
| `feat` | 新功能 | 是(MINOR) |
|
||||
| `fix` | Bug 修复 | 是(PATCH) |
|
||||
| `perf` | 性能优化 | 是(PATCH) |
|
||||
| `refactor` | 重构(不改变行为) | 否 |
|
||||
| `style` | 代码风格(格式化、空白) | 否 |
|
||||
| `test` | 新增/修改测试 | 否 |
|
||||
| `docs` | 文档变更 | 否 |
|
||||
| `build` | 构建系统或依赖变更 | 否 |
|
||||
| `ci` | CI 配置变更 | 否 |
|
||||
| `chore` | 杂项(不修改 src 或 test) | 否 |
|
||||
| `revert` | 回滚某次提交 | 是 |
|
||||
|
||||
### 2.3 范围(scope)
|
||||
|
||||
scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完整清单)。
|
||||
|
||||
**示例**:
|
||||
- `feat(identity): 实现用户注册接口`
|
||||
- `fix(gateway): 修复路由匹配优先级`
|
||||
- `perf(teaching): 优化课表查询 N+1 问题`
|
||||
- `docs(contracts): 补充 identity.proto 字段说明`
|
||||
|
||||
### 2.4 主题(subject)
|
||||
|
||||
**规则**:
|
||||
- 使用中文简短描述
|
||||
- 不超过 50 个字符
|
||||
- 不以句号结尾
|
||||
- 使用祈使句("实现用户注册"而非"实现了用户注册")
|
||||
- 不包含 type/scope(已在前面体现)
|
||||
|
||||
### 2.5 正文(body)
|
||||
|
||||
**规则**:
|
||||
- 解释"为什么"而非"做了什么"(代码已说明做了什么)
|
||||
- 每行不超过 72 个字符
|
||||
- 使用无序列表列出关键变更点
|
||||
- 涉及 breaking change 必须在正文开头说明
|
||||
|
||||
**示例**:
|
||||
```
|
||||
feat(teaching): 作业提交支持附件上传
|
||||
|
||||
- 新增 Attachment 聚合根,支持多文件关联
|
||||
- Outbox 事件 HomeworkSubmitted 新增 attachments 字段
|
||||
- BFF 接口新增 multipart/form-data 支持
|
||||
|
||||
BREAKING CHANGE: HomeworkSubmitted 事件 schema 变更,
|
||||
消费方需升级 proto 至 v2 才能消费新事件
|
||||
```
|
||||
|
||||
### 2.6 脚注(footer)
|
||||
|
||||
用于关联 Issue、PR、Jira 卡片:
|
||||
|
||||
```
|
||||
fix(content): 修复题库导入时重复题目标题去重逻辑
|
||||
|
||||
Closes #123
|
||||
Refs EDU-456
|
||||
Reviewed-by: @reviewer-name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、commitlint + husky 配置
|
||||
|
||||
### 3.1 安装与初始化
|
||||
|
||||
```bash
|
||||
# 在 monorepo 根目录
|
||||
pnpm add -Dw @commitlint/cli @commitlint/config-conventional husky lint-staged
|
||||
|
||||
# 初始化 husky
|
||||
pnpm exec husky init
|
||||
```
|
||||
|
||||
### 3.2 commitlint 配置
|
||||
|
||||
创建 `commitlint.config.cjs`:
|
||||
|
||||
```javascript
|
||||
/** @type {import('@commitlint/types').UserConfig} */
|
||||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
// type 枚举
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"style",
|
||||
"test",
|
||||
"docs",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert",
|
||||
],
|
||||
],
|
||||
// type 小写
|
||||
"type-case": [2, "always", "lower-case"],
|
||||
// type 不能为空
|
||||
"type-empty": [2, "never"],
|
||||
// scope 枚举(见 3.3)
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
// 业务微服务
|
||||
"identity",
|
||||
"org",
|
||||
"teaching",
|
||||
"content",
|
||||
"comm",
|
||||
"insight",
|
||||
// 基础设施服务
|
||||
"auth",
|
||||
"notification",
|
||||
// AI 服务
|
||||
"insight-ai",
|
||||
// 网关
|
||||
"gateway",
|
||||
// BFF
|
||||
"admin-bff",
|
||||
"teacher-bff",
|
||||
"student-bff",
|
||||
// 微前端
|
||||
"admin-shell",
|
||||
"teacher-shell",
|
||||
"student-shell",
|
||||
"parent-shell",
|
||||
// 共享包
|
||||
"contracts",
|
||||
"ui-tokens",
|
||||
"ui-components",
|
||||
"shared-ts",
|
||||
// protobuf 契约
|
||||
"proto",
|
||||
// 平台级
|
||||
"deps",
|
||||
"docs",
|
||||
"ci",
|
||||
"chore",
|
||||
"release",
|
||||
],
|
||||
],
|
||||
// scope 小写
|
||||
"scope-case": [2, "always", "lower-case"],
|
||||
// subject 不能为空
|
||||
"subject-empty": [2, "never"],
|
||||
// subject 不超过 50 字符
|
||||
"subject-max-length": [2, "always", 72],
|
||||
// subject 不以句号结尾
|
||||
"subject-full-stop": [2, "never", "."],
|
||||
// body 每行不超过 100 字符
|
||||
"body-max-line-length": [1, "always", 100],
|
||||
// footer 每行不超过 100 字符
|
||||
"footer-max-line-length": [1, "always", 100],
|
||||
// header 不超过 100 字符
|
||||
"header-max-length": [2, "always", 100],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 scope-enum 完整清单
|
||||
|
||||
| 分类 | scope | 说明 |
|
||||
|------|-------|------|
|
||||
| 业务微服务 | `identity` | 身份与权限服务 |
|
||||
| 业务微服务 | `org` | 教学组织服务 |
|
||||
| 业务微服务 | `teaching` | 教学核心服务 |
|
||||
| 业务微服务 | `content` | 内容分析服务 |
|
||||
| 业务微服务 | `comm` | 沟通服务 |
|
||||
| 业务微服务 | `insight` | 智能洞察服务 |
|
||||
| 基础设施 | `auth` | 认证授权服务 |
|
||||
| 基础设施 | `notification` | 通知服务 |
|
||||
| AI 服务 | `insight-ai` | AI 分析服务(Python) |
|
||||
| 网关 | `gateway` | API 网关(Go) |
|
||||
| BFF | `admin-bff` | 管理端 BFF |
|
||||
| BFF | `teacher-bff` | 教师端 BFF |
|
||||
| BFF | `student-bff` | 学生/家长端 BFF |
|
||||
| 微前端 | `admin-shell` | 管理端 Shell |
|
||||
| 微前端 | `teacher-shell` | 教师端 Shell |
|
||||
| 微前端 | `student-shell` | 学生端 Shell |
|
||||
| 微前端 | `parent-shell` | 家长端 Shell |
|
||||
| 共享包 | `contracts` | protobuf 生成契约包 |
|
||||
| 共享包 | `ui-tokens` | 设计令牌包 |
|
||||
| 共享包 | `ui-components` | UI 组件库 |
|
||||
| 共享包 | `shared-ts` | TS 共享工具包 |
|
||||
| 契约 | `proto` | protobuf 定义文件 |
|
||||
| 平台级 | `deps` | 依赖升级 |
|
||||
| 平台级 | `docs` | 平台级文档 |
|
||||
| 平台级 | `ci` | CI/CD 配置 |
|
||||
| 平台级 | `chore` | 杂项 |
|
||||
| 平台级 | `release` | 发布相关 |
|
||||
|
||||
### 3.4 husky hooks
|
||||
|
||||
`.husky/commit-msg`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
pnpm exec commitlint --edit "$1"
|
||||
```
|
||||
|
||||
`.husky/pre-commit`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
pnpm exec lint-staged
|
||||
```
|
||||
|
||||
`.husky/pre-push`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
# 推送前运行类型检查
|
||||
pnpm -r run typecheck
|
||||
```
|
||||
|
||||
### 3.5 lint-staged 配置
|
||||
|
||||
`package.json`(根目录):
|
||||
|
||||
```json
|
||||
{
|
||||
"lint-staged": {
|
||||
// TypeScript / NestJS / Next.js
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
// Go
|
||||
"*.go": [
|
||||
"gofmt -w",
|
||||
"golangci-lint run --fix"
|
||||
],
|
||||
// Python
|
||||
"*.py": [
|
||||
"ruff check --fix",
|
||||
"ruff format"
|
||||
],
|
||||
// protobuf
|
||||
"*.proto": [
|
||||
"buf format --write"
|
||||
],
|
||||
// Markdown
|
||||
"*.md": [
|
||||
"prettier --write"
|
||||
],
|
||||
// JSON / YAML
|
||||
"*.{json,yaml,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、PR 与 Code Review
|
||||
|
||||
### 4.1 PR 流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[开发者创建特性分支] --> B[提交代码]
|
||||
B --> C[本地通过 lint + typecheck + test]
|
||||
C --> D[rebase 至最新 main]
|
||||
D --> E[推送至远程]
|
||||
E --> F[创建 PR]
|
||||
F --> G[CI 自动检查]
|
||||
G --> H{CI 通过?}
|
||||
H -->|否| I[修复问题]
|
||||
I --> B
|
||||
H -->|是| J[Reviewer 审查]
|
||||
J --> K{审查通过?}
|
||||
K -->|需修改| L[根据反馈修改]
|
||||
L --> B
|
||||
K -->|通过| M[Squash Merge]
|
||||
M --> N[删除特性分支]
|
||||
N --> O[CI 部署至 staging]
|
||||
```
|
||||
|
||||
### 4.2 PR 标题
|
||||
|
||||
PR 标题必须符合 Conventional Commits 规范(与最终 squash merge 的 commit message 一致):
|
||||
|
||||
```
|
||||
feat(identity): 实现用户注册接口
|
||||
```
|
||||
|
||||
### 4.3 PR 模板
|
||||
|
||||
`.github/pull_request_template.md`(或 Gitea 等价路径):
|
||||
|
||||
```markdown
|
||||
## 变更说明
|
||||
|
||||
<!-- 简述本次变更的目的和实现方式 -->
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] feat: 新功能
|
||||
- [ ] fix: Bug 修复
|
||||
- [ ] perf: 性能优化
|
||||
- [ ] refactor: 重构
|
||||
- [ ] test: 测试
|
||||
- [ ] docs: 文档
|
||||
- [ ] build/ci: 构建/CI
|
||||
|
||||
## 影响范围
|
||||
|
||||
<!-- 列出受影响的服务/包 -->
|
||||
- 服务:
|
||||
- 包:
|
||||
- 数据库迁移:是 / 否
|
||||
- protobuf 契约变更:是 / 否
|
||||
- Kafka topic 变更:是 / 否
|
||||
|
||||
## 测试情况
|
||||
|
||||
- [ ] 单元测试通过
|
||||
- [ ] 集成测试通过
|
||||
- [ ] 本地手动测试通过
|
||||
- [ ] 新增测试覆盖新功能
|
||||
|
||||
## 文档同步
|
||||
|
||||
- [ ] 已更新服务 README(如涉及服务结构变更)
|
||||
- [ ] 已更新架构文档(如涉及架构变更)
|
||||
- [ ] 已运行 `pnpm run arch:scan` 更新 arch.db
|
||||
- [ ] 已更新 known-issues.md(如遇到新问题)
|
||||
|
||||
## Breaking Change
|
||||
|
||||
- [ ] 否
|
||||
- [ ] 是(请在下方说明影响和迁移路径)
|
||||
|
||||
## 关联 Issue
|
||||
|
||||
Closes #
|
||||
```
|
||||
|
||||
### 4.4 Reviewer 要求
|
||||
|
||||
| 变更类型 | 最少 Reviewer | 备注 |
|
||||
|---------|--------------|------|
|
||||
| 普通业务变更 | 1 | 默认 |
|
||||
| 跨服务变更 | 2 | 涉及 ≥ 2 个服务 |
|
||||
| protobuf 契约变更 | 2 | 需包含架构组成员 |
|
||||
| 数据库 schema 变更 | 2 | 需包含 DBA 或架构组 |
|
||||
| 安全相关变更 | 2 | 需包含安全负责人 |
|
||||
| 核心模块(auth/identity) | 2 | 核心模块强制 2 人 |
|
||||
|
||||
### 4.5 Code Review 清单
|
||||
|
||||
**通用检查**:
|
||||
- [ ] 代码是否符合 [编码规范](./coding-standards.md)
|
||||
- [ ] 是否有明显的逻辑错误
|
||||
- [ ] 错误处理是否完整(不忽略 error/err/exception)
|
||||
- [ ] 日志是否包含足够的上下文(request_id、user_id 等)
|
||||
- [ ] 是否存在硬编码的密钥、token、连接字符串
|
||||
|
||||
**架构检查**:
|
||||
- [ ] 是否违反限界上下文边界(跨服务直接查 DB)
|
||||
- [ ] 是否违反依赖方向(shared 反向依赖 services)
|
||||
- [ ] protobuf 变更是否向后兼容
|
||||
- [ ] 事件 schema 变更是否向后兼容
|
||||
|
||||
**性能检查**:
|
||||
- [ ] 是否有 N+1 查询
|
||||
- [ ] 是否有未加索引的查询
|
||||
- [ ] 是否有不必要的大对象拷贝
|
||||
- [ ] 是否有阻塞事件循环的同步操作(Python/Node)
|
||||
|
||||
**安全检查**:
|
||||
- [ ] 所有入口是否经过权限校验
|
||||
- [ ] 用户输入是否经过验证
|
||||
- [ ] SQL 是否使用参数化查询
|
||||
- [ ] 是否有 SQL 注入、XSS、SSRF 风险
|
||||
|
||||
### 4.6 合并策略
|
||||
|
||||
**默认使用 Squash Merge**:
|
||||
- 保留 PR 的完整变更作为一个 commit
|
||||
- commit message 使用 PR 标题
|
||||
- 删除特性分支
|
||||
|
||||
**禁止使用 Merge Commit**(除非是发布分支合并回 main):
|
||||
- 避免历史中充斥 "Merge branch" 噪音
|
||||
- 保持线性历史
|
||||
|
||||
**Rebase Merge**:
|
||||
- 仅用于需要保留多个有意义 commit 的特性分支
|
||||
- 需在 PR 中说明原因
|
||||
|
||||
---
|
||||
|
||||
## 五、文档同步规则
|
||||
|
||||
### 5.1 文档同步矩阵
|
||||
|
||||
| 代码变更类型 | 需同步的文档 | 同步时机 |
|
||||
|-------------|-------------|---------|
|
||||
| 新增/删除服务 | `001_architecture_overview.md` + 服务 README | PR 内同步 |
|
||||
| 新增/删除模块 | 服务 README + `arch:scan` | PR 内同步 |
|
||||
| 新增/删除导出函数 | `pnpm run arch:scan` | 提交前 |
|
||||
| 修改函数签名 | `pnpm run arch:scan` | 提交前 |
|
||||
| 修改权限点 | `project_rules.md` + 权限文档 | PR 内同步 |
|
||||
| 新增/删除数据库表 | 架构文档 + 服务 README | PR 内同步 |
|
||||
| 新增/删除路由 | 服务 README + OpenAPI | PR 内同步 |
|
||||
| 修改模块间依赖 | `arch:scan` + 架构文档 | PR 内同步 |
|
||||
| 新增 protobuf message | `proto/` README + `arch:scan` | PR 内同步 |
|
||||
| 新增 Kafka topic | 架构文档 + 服务 README | PR 内同步 |
|
||||
| 遇到新问题/经验 | `known-issues.md` | PR 内同步 |
|
||||
|
||||
### 5.2 arch.db 同步规则
|
||||
|
||||
**强制规则**:任何代码变更提交前必须运行 `pnpm run arch:scan` 更新 arch.db。
|
||||
|
||||
```bash
|
||||
# 提交前流程
|
||||
pnpm run arch:scan # 更新 arch.db
|
||||
pnpm run arch:query -- violations # 检查是否有违规
|
||||
git add arch.db # 将 arch.db 纳入提交
|
||||
git commit -m "feat(identity): 实现用户注册接口"
|
||||
```
|
||||
|
||||
**违规检查**:
|
||||
- 长文件(> 1000 行)
|
||||
- 未校验权限的 Handler
|
||||
- 循环依赖
|
||||
- 跨限界上下文直接 DB 访问
|
||||
|
||||
### 5.3 服务 README 要求
|
||||
|
||||
每个服务必须包含 `README.md`,结构如下:
|
||||
|
||||
```markdown
|
||||
# [服务名] 服务
|
||||
|
||||
> 版本:1.0
|
||||
> 限界上下文:[上下文名]
|
||||
> 技术栈:[语言 + 框架]
|
||||
|
||||
## 职责
|
||||
[一段话描述]
|
||||
|
||||
## 架构
|
||||
[mermaid 架构图]
|
||||
|
||||
## 核心流程
|
||||
[mermaid 时序图]
|
||||
|
||||
## 目录结构
|
||||
[树形结构 + 说明]
|
||||
|
||||
## 依赖
|
||||
- 上游服务:[列表]
|
||||
- 下游服务:[列表]
|
||||
- 共享包:[列表]
|
||||
|
||||
## 约束
|
||||
[业务规则、技术约束]
|
||||
|
||||
## 架构决策
|
||||
[关键设计决策记录]
|
||||
```
|
||||
|
||||
### 5.4 known-issues.md 格式
|
||||
|
||||
`docs/troubleshooting/known-issues.md` 采用索引式速查手册格式:
|
||||
|
||||
```markdown
|
||||
### X.X 主题分区
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
|------|----------|
|
||||
| 简述场景 | 正确做法(一句话) |
|
||||
```
|
||||
|
||||
**规则**:
|
||||
- 索引式:场景→技术/规则映射
|
||||
- 不写代码示例和错误示范
|
||||
- 同类问题在原条目补充,不重复创建
|
||||
- 在"工作经验日志"区按时间倒序追加记录
|
||||
|
||||
---
|
||||
|
||||
## 六、多语言 monorepo 提交规则
|
||||
|
||||
### 6.1 单服务单提交原则
|
||||
|
||||
**规则**:一个 commit 只涉及一个服务或一个包的变更。
|
||||
|
||||
**原因**:
|
||||
- 便于回滚(按服务粒度回滚)
|
||||
- 便于追踪(changelog 清晰)
|
||||
- 便于 review(聚焦单一职责)
|
||||
|
||||
**例外**:跨服务重构或契约变更可在同一 commit 中涉及多个服务,但需在 body 中说明。
|
||||
|
||||
### 6.2 生成代码单独提交
|
||||
|
||||
**规则**:protobuf 生成的代码必须与手写代码分开提交。
|
||||
|
||||
```bash
|
||||
# 正确流程
|
||||
# 1. 修改 proto 定义
|
||||
git commit -m "feat(proto): 新增 UserRegistered 事件 schema"
|
||||
|
||||
# 2. 生成代码
|
||||
pnpm run proto:generate
|
||||
|
||||
# 3. 单独提交生成代码
|
||||
git commit -m "build(contracts): 重新生成 proto 代码"
|
||||
```
|
||||
|
||||
### 6.3 跨语言变更拆分
|
||||
|
||||
当一个功能涉及多个语言(如 TS 服务 + Go 网关 + Python AI),按以下顺序拆分提交:
|
||||
|
||||
1. **契约先行**:`feat(proto): ...`
|
||||
2. **生成代码**:`build(contracts): ...`
|
||||
3. **后端服务**:`feat(teaching): ...`
|
||||
4. **网关层**:`feat(gateway): ...`
|
||||
5. **前端**:`feat(teacher-shell): ...`
|
||||
|
||||
### 6.4 依赖升级规则
|
||||
|
||||
**规则**:
|
||||
- 依赖升级使用 `build(deps):` 类型
|
||||
- 必须说明升级原因(安全、功能、兼容性)
|
||||
- 安全漏洞修复必须包含 CVE 编号
|
||||
- 大版本升级需单独 PR 并完整测试
|
||||
|
||||
**示例**:
|
||||
```
|
||||
build(deps): 升级 nestjs 至 10.3.0
|
||||
|
||||
- 修复 CVE-2024-1234 漏洞
|
||||
- 升级至最新 patch 版本
|
||||
- 全部测试通过
|
||||
```
|
||||
|
||||
### 6.5 跨服务依赖更新
|
||||
|
||||
当一个服务的 protobuf 变更影响其他服务:
|
||||
|
||||
```bash
|
||||
# 1. 提交 proto 变更
|
||||
git commit -m "feat(proto): UserRegistered 事件新增 attachments 字段"
|
||||
|
||||
# 2. 生成新契约
|
||||
pnpm run proto:generate
|
||||
git commit -m "build(contracts): 生成 UserRegistered v2"
|
||||
|
||||
# 3. 更新消费方服务
|
||||
git commit -m "feat(notification): 适配 UserRegistered v2 事件"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、版本与发布
|
||||
|
||||
### 7.1 版本号体系
|
||||
|
||||
本项目采用**双层版本号**:
|
||||
|
||||
| 层级 | 格式 | 说明 |
|
||||
|------|------|------|
|
||||
| 平台版本 | `v{阶段}.{迭代}.{patch}` | 如 `v0.3.1`(P3 阶段第 1 次迭代 patch 1) |
|
||||
| 服务版本 | `{service}:{semver}` | 如 `identity:1.2.0` |
|
||||
|
||||
### 7.2 阶段版本范围
|
||||
|
||||
| 阶段 | 平台版本范围 | 说明 |
|
||||
|------|-------------|------|
|
||||
| P1 地基 | `v0.1.x` | monorepo 初始化、CI/CD、arch.db |
|
||||
| P2 身份 | `v0.2.x` | identity + auth + notification |
|
||||
| P3 核心教学 | `v0.3.x` | org + teaching + content |
|
||||
| P4 内容分析 | `v0.4.x` | insight + CQRS 读模型 |
|
||||
| P5 沟通AI | `v0.5.x` | comm + AI 增强 |
|
||||
| P6 硬化 | `v1.0.x` | 正式发布版 |
|
||||
|
||||
### 7.3 Docker 镜像标签
|
||||
|
||||
**格式**:`{registry}/edu/{service}:{tag}`
|
||||
|
||||
| tag 类型 | 格式 | 示例 | 用途 |
|
||||
|---------|------|------|------|
|
||||
| 版本号 | `v{version}` | `v0.3.1` | 正式发布 |
|
||||
| 服务版本 | `{service}-{semver}` | `identity-1.2.0` | 服务独立版本 |
|
||||
| Git SHA | `sha-{short}` | `sha-a1b2c3d` | 精确追溯 |
|
||||
| 最新 | `latest` | `latest` | 开发环境 |
|
||||
| 阶段 | `{stage}-{sha}` | `staging-a1b2c3d` | 阶段环境 |
|
||||
|
||||
### 7.4 发布流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[main 分支达到发布标准] --> B[创建 release/v0.3.x 分支]
|
||||
B --> C[运行完整测试套件]
|
||||
C --> D{测试通过?}
|
||||
D -->|否| E[修复问题]
|
||||
E --> C
|
||||
D -->|是| F[更新 CHANGELOG.md]
|
||||
F --> G[打 tag v0.3.0]
|
||||
G --> H[CI 自动构建镜像]
|
||||
H --> I[部署至 staging]
|
||||
I --> J[冒烟测试]
|
||||
J --> K{冒烟通过?}
|
||||
K -->|否| L[修复并重新发布]
|
||||
L --> C
|
||||
K -->|是| M[审批发布至 production]
|
||||
M --> N[monitoring 观察指标]
|
||||
N --> O{指标正常?}
|
||||
O -->|否| P[紧急回滚]
|
||||
O -->|是| Q[发布完成]
|
||||
Q --> R[合并 release 分支回 main]
|
||||
```
|
||||
|
||||
### 7.5 CHANGELOG 格式
|
||||
|
||||
`CHANGELOG.md` 按 [Keep a Changelog](https://keepachangelog.com/zh-CN/) 格式维护:
|
||||
|
||||
```markdown
|
||||
## [v0.3.0] - 2026-08-15
|
||||
|
||||
### Added
|
||||
- 教学核心服务新增作业管理功能
|
||||
- 内容服务支持题库导入
|
||||
- 教师端 Shell 新增作业批改界面
|
||||
|
||||
### Changed
|
||||
- identity 服务升级至 NestJS 10.3
|
||||
- gateway 路由匹配算法优化
|
||||
|
||||
### Fixed
|
||||
- 修复 JWT 刷新 token 过期判断错误
|
||||
- 修复课表查询时区问题
|
||||
|
||||
### Breaking Changes
|
||||
- UserRegistered 事件 schema 变更至 v2,消费方需升级
|
||||
```
|
||||
|
||||
### 7.6 服务独立版本
|
||||
|
||||
每个服务维护独立的 `VERSION` 文件,内容为 semver:
|
||||
|
||||
```
|
||||
# services/identity/VERSION
|
||||
1.2.0
|
||||
```
|
||||
|
||||
**版本号升级规则**:
|
||||
- **MAJOR**:Breaking Change(protobuf 不兼容变更、API 破坏性修改)
|
||||
- **MINOR**:新增功能,向后兼容
|
||||
- **PATCH**:Bug 修复,向后兼容
|
||||
|
||||
---
|
||||
|
||||
## 八、紧急回滚
|
||||
|
||||
### 8.1 回滚策略
|
||||
|
||||
| 场景 | 回滚方式 | 耗时 |
|
||||
|------|---------|------|
|
||||
| 代码缺陷 | `git revert` + 重新部署 | 5-10 分钟 |
|
||||
| 镜像问题 | `kubectl rollout undo` | 1-2 分钟 |
|
||||
| 数据库迁移问题 | 执行迁移 down 脚本 | 5-30 分钟 |
|
||||
| 配置错误 | 回滚 ConfigMap/Secret | 1-2 分钟 |
|
||||
| 全站故障 | 回滚至上一稳定 tag | 10-30 分钟 |
|
||||
|
||||
### 8.2 代码回滚
|
||||
|
||||
```bash
|
||||
# 1. 确认要回滚的 commit
|
||||
git log --oneline -10
|
||||
|
||||
# 2. 创建回滚分支
|
||||
git checkout -b hotfix/v0.3.1
|
||||
|
||||
# 3. 回滚指定 commit
|
||||
git revert <commit-sha>
|
||||
|
||||
# 4. 推送并创建 PR
|
||||
git push origin hotfix/v0.3.1
|
||||
|
||||
# 5. 紧急审批合并后部署
|
||||
```
|
||||
|
||||
### 8.3 K8s 部署回滚
|
||||
|
||||
```bash
|
||||
# 查看部署历史
|
||||
kubectl rollout history deployment/identity -n edu-prod
|
||||
|
||||
# 回滚至上一版本
|
||||
kubectl rollout undo deployment/identity -n edu-prod
|
||||
|
||||
# 回滚至指定版本
|
||||
kubectl rollout undo deployment/identity -n edu-prod --to-revision=3
|
||||
|
||||
# 监控回滚状态
|
||||
kubectl rollout status deployment/identity -n edu-prod
|
||||
```
|
||||
|
||||
### 8.4 数据库迁移回滚
|
||||
|
||||
**规则**:
|
||||
- 所有迁移必须提供 `up` 和 `down` 脚本
|
||||
- `down` 脚本必须在 CI 中测试
|
||||
- 回滚前必须备份生产数据
|
||||
|
||||
```bash
|
||||
# 回滚最近一次迁移
|
||||
pnpm --filter identity run migrate:down
|
||||
|
||||
# 回滚至指定版本
|
||||
pnpm --filter identity run migrate:down -- --to <version>
|
||||
```
|
||||
|
||||
### 8.5 回滚原则
|
||||
|
||||
1. **优先保证可用性**:回滚决策应在 5 分钟内做出,无需 root cause
|
||||
2. **保留现场**:回滚前保存日志、metrics、错误堆栈
|
||||
3. **通知相关方**:回滚后立即通知运维、产品、相关开发
|
||||
4. **复盘改进**:回滚后 24 小时内完成事故复盘,记录至 `docs/troubleshooting/incidents/`
|
||||
|
||||
### 8.6 回滚记录格式
|
||||
|
||||
`docs/troubleshooting/incidents/YYYY-MM-DD-incident.md`:
|
||||
|
||||
```markdown
|
||||
# 事故记录:[简述]
|
||||
|
||||
> 日期:YYYY-MM-DD
|
||||
> 影响范围:[服务/用户]
|
||||
> 持续时间:[开始-结束]
|
||||
> 严重等级:P0/P1/P2/P3
|
||||
|
||||
## 时间线
|
||||
- HH:MM 告警触发
|
||||
- HH:MM 确认问题
|
||||
- HH:MM 决定回滚
|
||||
- HH:MM 回滚完成
|
||||
- HH:MM 服务恢复
|
||||
|
||||
## 影响分析
|
||||
[受影响的功能、用户数、业务损失]
|
||||
|
||||
## 根本原因
|
||||
[技术原因 + 流程原因]
|
||||
|
||||
## 回滚过程
|
||||
[执行的操作]
|
||||
|
||||
## 改进措施
|
||||
- [ ] 短期:[立即修复项]
|
||||
- [ ] 中期:[流程改进项]
|
||||
- [ ] 长期:[架构改进项]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、附录:CICD 与 Edu Git 工作流差异
|
||||
|
||||
| 维度 | CICD(Next.js 单应用) | Edu(微服务 monorepo) |
|
||||
|------|----------------------|----------------------|
|
||||
| 仓库结构 | 单一 Next.js 应用 | 多语言 monorepo(pnpm + go.work + uv) |
|
||||
| 分支策略 | trunk-based | trunk-based(沿用) |
|
||||
| 提交规范 | Conventional Commits | Conventional Commits(沿用,scope 扩展至服务/包) |
|
||||
| scope 范围 | 模块名(如 `exams`、`homework`) | 服务/包名(如 `identity`、`contracts`) |
|
||||
| commitlint scope-enum | 35 个模块 | 27 个服务/包 |
|
||||
| PR Reviewer | 1 人 | 1-2 人(核心模块/跨服务 2 人) |
|
||||
| 合并策略 | Squash Merge | Squash Merge(沿用) |
|
||||
| 版本号 | 单一应用版本 | 双层(平台版本 + 服务独立版本) |
|
||||
| 发布粒度 | 整体发布 | 按服务独立发布 |
|
||||
| Docker 镜像 | 单一镜像 | 每服务一镜像 |
|
||||
| 回滚粒度 | 整体回滚 | 按服务回滚 |
|
||||
| 数据库迁移 | Drizzle 单库 | 每服务独立库 + 独立迁移 |
|
||||
| 文档同步 | `npm run arch:scan` | `pnpm run arch:scan`(多语言扫描) |
|
||||
| CI 检查 | lint + tsc + test | lint + typecheck + test(按语言分别执行) |
|
||||
| 紧急回滚 | `git revert` + 重新部署 | `git revert` + `kubectl rollout undo` |
|
||||
| 事故复盘 | known-issues.md | `incidents/` 目录独立记录 |
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| 1.0 | 2026-07-07 | 基线发布,从 CICD 单应用规范迁移至微服务多语言 monorepo |
|
||||
1520
docs/standards/ui-design-system.md
Normal file
1520
docs/standards/ui-design-system.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user