Files
Edu/docs/superpowers/plans/2026-07-07-p1-foundation-implementation.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

112 KiB
Raw Blame History

P1 地基阶段实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 搭建 next-edu-cloud 新仓库地基:多语言 monorepo + Docker 基础设施 + API Gateway + classes 黄金模板服务 + 文档体系 + CI/CD端到端跑通 classes 域 CRUD。

Architecture: pnpm workspace 多语言 monorepoTS+Go+PythonGo API Gateway 路由转发TS/NestJS classes 服务作为黄金模板完整横切关注点protobuf 契约先行Docker Compose 本地基础设施。

Tech Stack: pnpm + Node.js 20+ + Go 1.22+ + Python 3.12+ + NestJS + Drizzle ORM + Gin + buf + protobuf + Docker Compose + MySQL 8 + Redis 7 + OpenTelemetry + vitest + Playwright

新仓库本地路径: e:\Desktop\Edu 远程仓库: https://git.eazygame.cn/xiner/Edu.git Spec 来源: docs/superpowers/specs/2026-07-07-enterprise-microservices-architecture-upgrade-design.md


文件结构总览

P1 完成后将创建以下文件(按职责分组):

e:\Desktop\Edu\                              # 新仓库根
├─ .github/workflows/                        # CI/CDTask 12
│  ├─ ci-go.yml
│  ├─ ci-ts.yml
│  └─ ci-py.yml
├─ .editorconfig
├─ .gitignore
├─ .commitlintrc.js
├─ .husky/commit-msg                         # Task 2
├─ docs/                                     # 文档体系Task 4 + 13
│  ├─ architecture/0010_architecture.md      # 从旧仓库迁移
│  ├─ architecture/004_architecture_impact_map.md
│  ├─ modules/api-gateway/README.md
│  ├─ modules/classes/README.md
│  ├─ standards/coding-standards.md
│  ├─ standards/git-workflow.md
│  ├─ troubleshooting/known-issues.md
│  └─ superpowers/specs/2026-07-07-...md     # 从旧仓库复制
├─ packages/                                 # 共享包Task 6
│  └─ shared-proto/
│     ├─ proto/classes.proto
│     ├─ buf.yaml
│     ├─ buf.gen.yaml
│     └─ package.json
├─ services/                                 # 微服务
│  ├─ api-gateway/                           # Task 7
│  │  ├─ main.go
│  │  ├─ go.mod
│  │  ├─ internal/{config,middleware,proxy}/
│  │  └─ Dockerfile
│  └─ classes/                               # Task 8-10
│     ├─ src/{main.ts,app.module.ts,config,middleware,classes,shared,generated}/
│     ├─ test/{unit,integration,contract,e2e}/
│     ├─ Dockerfile
│     ├─ nest-cli.json
│     ├─ tsconfig.json
│     ├─ vitest.config.ts
│     └─ package.json
├─ apps/teacher-portal/                      # Task 11
│  ├─ src/app/page.tsx
│  ├─ package.json
│  └─ next.config.js
├─ infra/                                    # Task 3
│  ├─ docker-compose.yml
│  ├─ docker-compose.minimal.yml
│  └─ init-sql/01-init.sql
├─ scripts/arch-scan/                        # Task 5
│  ├─ scanner.ts
│  ├─ scanners/{ts,go,py}-scanner.ts
│  ├─ query.ts
│  └─ package.json
├─ arch.db                                   # Task 5 创建
├─ package.json                              # Task 2 根 workspace
├─ pnpm-workspace.yaml                       # Task 2
├─ go.work                                   # Task 2
├─ pyproject.toml                            # Task 2
├─ project_rules.md                          # Task 4
├─ LICENSE
├─ CHANGELOG.md
├─ CONTRIBUTING.md
├─ SECURITY.md
└─ .env.example

Task 1: 新仓库初始化与 Git 设置

目标:e:\Desktop\Edu 创建新 git 仓库,配置远程,建立基础文件。

Files:

  • Create: e:\Desktop\Edu\.gitignore

  • Create: e:\Desktop\Edu\.editorconfig

  • Create: e:\Desktop\Edu\LICENSE

  • Create: e:\Desktop\Edu\CHANGELOG.md

  • Create: e:\Desktop\Edu\CONTRIBUTING.md

  • Create: e:\Desktop\Edu\SECURITY.md

  • Create: e:\Desktop\Edu\.env.example

  • Create: e:\Desktop\Edu\README.md

  • Step 1: 创建目录并初始化 git

Run:

mkdir -p e:\Desktop\Edu
cd e:\Desktop\Edu
git init -b main

Expected: Initialized empty Git repository in e:/Desktop/Edu/.git/

  • Step 2: 创建 .gitignore

Create e:\Desktop\Edu\.gitignore:

# Dependencies
node_modules/
.pnpm-store/

# Build outputs
dist/
build/
.next/
out/
*.tsbuildinfo
target/
bin/
obj/

# Go
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out

# Python
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
*.egg-info/

# Environment
.env
.env.local
.env.*.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
*.log
logs/

# Database
*.db
*.sqlite
arch.db

# Test coverage
coverage/
.nyc_output/

# Docker
docker-compose.override.yml

# Temp
tmp/
temp/
  • Step 3: 创建 .editorconfig

Create e:\Desktop\Edu\.editorconfig:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.{go,mod,sum}]
indent_style = tab

[*.{py,toml}]
indent_size = 4

[Makefile]
indent_style = tab

[*.md]
trim_trailing_whitespace = false
  • Step 4: 创建 LICENSEMIT

Create e:\Desktop\Edu\LICENSE:

MIT License

Copyright (c) 2026 Edu Cloud

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
  • Step 5: 创建 CHANGELOG.md

Create e:\Desktop\Edu\CHANGELOG.md:

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- P1 地基阶段仓库骨架、Docker 基础设施、API Gateway、classes 黄金模板服务
  • Step 6: 创建 CONTRIBUTING.md

Create e:\Desktop\Edu\CONTRIBUTING.md:

# Contributing

## 开发环境要求

- Node.js 20+
- pnpm 9+
- Go 1.22+
- Python 3.12+
- Docker Desktop
- buf 1.40+ (`go install github.com/bufbuild/buf/cmd/buf@latest`)

## 开发流程

1. 从 main 创建 feature 分支:`git checkout -b feature/xxx`
2. 开发并确保 CI 通过:`pnpm lint && pnpm test && pnpm build`
3. 提交 PR 到 main
4. CI 全绿后合并

## 提交规范

使用 Conventional Commits

():

types: feat, fix, chore, docs, style, refactor, test, perf, ci scope: 服务名或包名(如 api-gateway, classes, shared-proto


示例:
- `feat(api-gateway): add JWT auth middleware`
- `fix(classes): resolve N+1 query in list classes`
- `docs(004): update service dependency graph`

## 文档同步

修改代码后必须:
1. 运行 `npm run arch:scan` 更新 arch.db
2. 若架构意图变化,更新 `docs/architecture/004_architecture_impact_map.md`
3. 若服务接口变化,更新对应 `docs/modules/<service>/README.md`
4. 若发现新经验,更新 `docs/troubleshooting/known-issues.md`
  • Step 7: 创建 SECURITY.md

Create e:\Desktop\Edu\SECURITY.md:

# Security Policy

## 报告漏洞

发现安全漏洞请勿公开提交 issue请发送邮件至 security@example.com。

## 安全架构

- JWT RS256 非对称签名IAM 私钥签发Gateway/服务公钥校验)
- DataScope 6 级数据范围过滤
- 参数化 SQL 查询(禁止字符串拼接)
- 环境变量校验(@t3-oss/env-nextjs / viper / pydantic-settings
- 密钥管理:开发用 .env + docker secrets生产用 VaultP6
  • Step 8: 创建 .env.example

Create e:\Desktop\Edu\.env.example:

# MySQL
MYSQL_ROOT_PASSWORD=changeme
MYSQL_DATABASE=next_edu_cloud
MYSQL_USER=edu
MYSQL_PASSWORD=changeme

# Redis
REDIS_PASSWORD=

# JWT (P1 用 HS256 测试密钥P2 改 RS256)
JWT_SECRET=p1-dev-secret-change-in-production
JWT_ISSUER=next-edu-cloud
JWT_AUDIENCE=next-edu-cloud

# 服务端口
API_GATEWAY_PORT=8080
CLASSES_SERVICE_PORT=3001
TEACHER_PORTAL_PORT=3000

# 数据库连接
DATABASE_URL=mysql://edu:changeme@localhost:3306/next_edu_cloud
REDIS_URL=redis://localhost:6379

# 可观测性
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
LOG_LEVEL=info

# Neo4jP4 启用)
NEO4J_PASSWORD=changeme

# KafkaP3 启用)
KAFKA_BROKERS=localhost:9092
  • Step 9: 创建 README.md

Create e:\Desktop\Edu\README.md:

# Next Edu Cloud

基于 DDD + EDA + CQRS 的企业级微服务架构教育云平台。

## 技术栈

- **网关层**: Go (Gin)
- **业务服务**: TypeScript (NestJS)
- **分析/AI**: Python (FastAPI)
- **前端**: React (Next.js + Module Federation)
- **存储**: MySQL / Redis / ClickHouse / Neo4j / Elasticsearch
- **事件总线**: Kafka + Debezium CDC
- **契约**: protobuf + buf

## 快速开始

```bash
# 1. 安装依赖
pnpm install

# 2. 启动最小基础设施MySQL + Redis
docker compose -f infra/docker-compose.minimal.yml up -d

# 3. 复制环境变量
cp .env.example .env

# 4. 运行 arch.db 扫描
npm run arch:scan

# 5. 启动服务
pnpm dev

文档

开发阶段

  • P1 地基 (M1-M3): 仓库骨架 + 基础设施 + classes 黄金模板
  • P2 身份 (M4-M6): IAM 服务 + Teacher BFF + 微前端骨架
  • P3 核心教学 (M7-M10): CoreEdu + Outbox + 考试/作业/成绩
  • P4 内容分析 (M11-M13): Content + DataAna + CDC
  • P5 沟通AI (M14-M16): Msg + Push + AI + ES
  • P6 硬化 (M17-M18): Service Mesh + 生产硬化

- [ ] **Step 10: 配置远程并首次提交**

Run:
```bash
cd e:\Desktop\Edu
git add -A
git commit -m "chore: initialize next-edu-cloud repository"
git remote add origin https://git.eazygame.cn/xiner/Edu.git
git push -u origin main
# 若首次推送失败,再次推送:
git push -u origin main

Expected: 推送成功,远程仓库可见首次提交。


Task 2: 多语言 monorepo 骨架

目标: 建立 pnpm workspace + go.work + pyproject.toml 多语言 monorepo 骨架。

Files:

  • Create: e:\Desktop\Edu\package.json

  • Create: e:\Desktop\Edu\pnpm-workspace.yaml

  • Create: e:\Desktop\Edu\go.work

  • Create: e:\Desktop\Edu\pyproject.toml

  • Create: e:\Desktop\Edu\.commitlintrc.js

  • Create: e:\Desktop\Edu\.husky\commit-msg

  • Step 1: 创建 pnpm workspace 根 package.json

Create e:\Desktop\Edu\package.json:

{
  "name": "next-edu-cloud",
  "version": "0.1.0",
  "private": true,
  "description": "Enterprise microservices education cloud platform",
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=9.0.0"
  },
  "scripts": {
    "dev": "pnpm -r --parallel run dev",
    "build": "pnpm -r run build",
    "lint": "pnpm -r run lint",
    "test": "pnpm -r run test",
    "arch:scan": "tsx scripts/arch-scan/scanner.ts",
    "arch:query": "tsx scripts/arch-scan/query.ts",
    "proto:gen": "pnpm --filter @next-edu/shared-proto run gen",
    "prepare": "husky"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.3.0",
    "@commitlint/config-conventional": "^19.2.2",
    "husky": "^9.1.4",
    "tsx": "^4.16.0",
    "typescript": "^5.5.0"
  }
}
  • Step 2: 创建 pnpm-workspace.yaml

Create e:\Desktop\Edu\pnpm-workspace.yaml:

packages:
  - 'packages/*'
  - 'services/classes'
  - 'bff/*'
  - 'apps/*'
  - 'scripts/*'
  • Step 3: 安装根依赖

Run:

cd e:\Desktop\Edu
pnpm install

Expected: pnpm 安装根 devDependencies 成功。

  • Step 4: 创建 go.work

Create e:\Desktop\Edu\go.work:

go 1.22

use (
    ./services/api-gateway
)
  • Step 5: 创建 pyproject.toml

Create e:\Desktop\Edu\pyproject.toml:

[tool.uv.workspace]
members = ["services/data-ana", "services/ai-gateway"]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM"]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
  • Step 6: 创建 .commitlintrc.js

Create e:\Desktop\Edu\.commitlintrc.js:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'chore', 'docs', 'style', 'refactor', 'test', 'perf', 'ci', 'build'],
    ],
    'subject-case': [0],
    'header-max-length': [2, 'always', 100],
  },
};
  • Step 7: 配置 husky commit-msg hook

Run:

cd e:\Desktop\Edu
pnpm exec husky init
echo "pnpm exec commitlint --edit \$1" > .husky/commit-msg

Expected: .husky/commit-msg 文件创建。

  • Step 8: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "chore: setup pnpm + go + python monorepo workspace"

Task 3: Docker Compose 基础设施

目标: 创建全量与最小 Docker Compose 配置P1 仅启动 MySQL + Redis。

Files:

  • Create: e:\Desktop\Edu\infra\docker-compose.yml

  • Create: e:\Desktop\Edu\infra\docker-compose.minimal.yml

  • Create: e:\Desktop\Edu\infra\init-sql\01-init.sql

  • Step 1: 创建全量 docker-compose.yml

Create e:\Desktop\Edu\infra\docker-compose.yml:

name: next-edu-cloud

services:
  # === P1 启用 ===
  mysql:
    image: mysql:8.0
    container_name: edu-mysql
    restart: unless-stopped
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init-sql:/docker-entrypoint-initdb.d:ro
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    mem_limit: 512m
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: edu-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    command: redis-server --requirepass ${REDIS_PASSWORD:-}
    mem_limit: 128m
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

  # === P3 启用 ===
  zookeeper:
    image: confluentinc/cp-zookeeper:7.6.0
    container_name: edu-zookeeper
    profiles: ["full", "kafka"]
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
    mem_limit: 256m

  kafka:
    image: confluentinc/cp-kafka:7.6.0
    container_name: edu-kafka
    profiles: ["full", "kafka"]
    depends_on: [zookeeper]
    ports:
      - "9092:9092"
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    mem_limit: 512m

  # === P4 启用 ===
  debezium:
    image: debezium/connect:2.6
    container_name: edu-debezium
    profiles: ["full", "cdc"]
    depends_on: [kafka]
    ports:
      - "8083:8083"
    environment:
      BOOTSTRAP_SERVERS: kafka:9092
      GROUP_ID: 1
      CONFIG_STORAGE_TOPIC: debezium_configs
      OFFSET_STORAGE_TOPIC: debezium_offsets
      STATUS_STORAGE_TOPIC: debezium_statuses
    mem_limit: 512m

  clickhouse:
    image: clickhouse/clickhouse-server:24.3
    container_name: edu-clickhouse
    profiles: ["full", "analytics"]
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - clickhouse-data:/var/lib/clickhouse
    mem_limit: 1g

  neo4j:
    image: neo4j:5.20
    container_name: edu-neo4j
    profiles: ["full", "graph"]
    ports:
      - "7474:7474"
      - "7687:7687"
    environment:
      NEO4J_AUTH: neo4j/${NEO4J_PASSWORD}
    volumes:
      - neo4j-data:/data
    mem_limit: 512m

  # === P5 启用 ===
  elasticsearch:
    image: elasticsearch:8.11.0
    container_name: edu-elasticsearch
    profiles: ["full", "search"]
    ports:
      - "9200:9200"
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
      ES_JAVA_OPTS: "-Xms256m -Xmx256m"
    mem_limit: 512m

  # === P6 启用 ===
  consul:
    image: hashicorp/consul:1.18
    container_name: edu-consul
    profiles: ["full", "config"]
    ports:
      - "8500:8500"
    mem_limit: 128m

  # === 可观测性P6 启用完整栈)===
  jaeger:
    image: jaegertracing/all-in-one:1.56
    container_name: edu-jaeger
    profiles: ["full", "observability"]
    ports:
      - "16686:16686"
      - "4318:4318"
    environment:
      COLLECTOR_OTLP_ENABLED: true
    mem_limit: 256m

  prometheus:
    image: prom/prometheus:v2.53.0
    container_name: edu-prometheus
    profiles: ["full", "observability"]
    ports:
      - "9090:9090"
    mem_limit: 256m

  grafana:
    image: grafana/grafana:11.1.0
    container_name: edu-grafana
    profiles: ["full", "observability"]
    ports:
      - "3001:3001"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    mem_limit: 256m

  loki:
    image: grafana/loki:3.1.0
    container_name: edu-loki
    profiles: ["full", "observability"]
    ports:
      - "3100:3100"
    mem_limit: 256m

volumes:
  mysql-data:
  clickhouse-data:
  neo4j-data:
  • Step 2: 创建最小开发集

Create e:\Desktop\Edu\infra\docker-compose.minimal.yml:

name: next-edu-cloud-min

services:
  mysql:
    image: mysql:8.0
    container_name: edu-mysql
    restart: unless-stopped
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init-sql:/docker-entrypoint-initdb.d:ro
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: edu-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

volumes:
  mysql-data:
  • Step 3: 创建 MySQL 初始化脚本

Create e:\Desktop\Edu\infra\init-sql\01-init.sql:

-- P1 仅创建 classes 表与权限测试用户表
-- 后续阶段的表在对应服务迁移中创建

CREATE TABLE IF NOT EXISTS classes (
  id CHAR(36) NOT NULL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  grade_id CHAR(36) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_grade_id (grade_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- P1 临时测试用户表P2 由 IAM 服务接管)
CREATE TABLE IF NOT EXISTS users (
  id CHAR(36) NOT NULL PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  role VARCHAR(20) NOT NULL DEFAULT 'teacher',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  • Step 4: 验证最小集启动

Run:

cd e:\Desktop\Edu
cp .env.example .env
docker compose -f infra/docker-compose.minimal.yml up -d
docker compose -f infra/docker-compose.minimal.yml ps

Expected: mysql 与 redis 状态为 healthy。

  • Step 5: 提交

Run:

cd e:\Desktop\Edu
docker compose -f infra/docker-compose.minimal.yml down
git add -A
git commit -m "feat(infra): add docker-compose full and minimal configurations"

Task 4: 文档体系骨架移植

目标: 从旧仓库迁移 0010 蓝图,创建 004 多服务版骨架、coding-standards、known-issues、project_rules。

Files:

  • Create: e:\Desktop\Edu\docs\architecture\0010_architecture.md(从旧仓库复制)

  • Create: e:\Desktop\Edu\docs\architecture\004_architecture_impact_map.md

  • Create: e:\Desktop\Edu\docs\standards\coding-standards.md

  • Create: e:\Desktop\Edu\docs\standards\git-workflow.md

  • Create: e:\Desktop\Edu\docs\troubleshooting\known-issues.md

  • Create: e:\Desktop\Edu\project_rules.md

  • Step 1: 复制 0010 蓝图

Run:

cp "e:\Desktop\CICD\docs\architecture\0010_architecture.md" "e:\Desktop\Edu\docs\architecture\0010_architecture.md"
  • Step 2: 创建 004 多服务版骨架

Create e:\Desktop\Edu\docs\architecture\004_architecture_impact_map.md:

# 架构设计意图(多服务版)

> 本文档是 next-edu-cloud 的架构设计意图唯一源(人类可读)。代码结构现状查询 arch.db。

## 1. 项目概览

- **系统边界**: K12 教育云平台,多端(教师/学生/家长/管理),百万级 DAU
- **规模指标**: 8 服务 / 3 BFF / 4 微前端 / 5 存储 / 1 事件总线
- **实施策略**: 方案 A基础设施优先+ 黄金模板服务驱动

## 2. 技术栈选型

| 语言 | 服务 | 框架 |
|------|------|------|
| Go | API Gateway / Push Gateway | Gin |
| TypeScript | IAM / CoreEdu / Content / Msg / BFF / 微前端 | NestJS / Next.js / GraphQL Yoga |
| Python | DataAna / AI 网关 | FastAPI |

## 3. 分层架构

Client微前端 → EdgeGateway + BFF → Service领域微服务 → BusKafka 事件总线) → StorageMySQL/CH/Neo4j/ES/Redis → InfraCDC/Temporal/Consul/Istio


## 4. 服务依赖全景图

```mermaid
graph TB
    GW[API Gateway] --> IAM & CoreEdu & Content & Msg
    BFF_T[Teacher BFF] --> IAM & CoreEdu & Content & DataAna & AI
    BFF_S[Student BFF] --> CoreEdu & Content & DataAna
    CoreEdu -.->|Outbox| Kafka
    Kafka --> DataAna & Push & Content
    MySQL -->|CDC| Kafka

5. 认证与权限架构

  • 认证: IAM 签发 RS256 JWTGateway 公钥校验
  • 权限: RBAC + 6 级 DataScope
  • 视口: 4 层模型(导航/路由/组件/数据)

6. 数据访问架构

  • CQRS: 写入 MySQL 主库,聚合查询 ClickHouse 宽表
  • Outbox: 业务事务同写 outbox_eventsrelay worker 投递 Kafka
  • CDC: Debezium 监听 binlog 同步 ClickHouse
  • 双轨读: 实时查主库,聚合查宽表

7. 事件驱动架构

Topic 生产者 消费者
exam.published CoreEdu Msg / DataAna
homework.graded CoreEdu DataAna / Msg
grade.recorded CoreEdu DataAna
mysql.cdc.* Debezium DataAna

8. 跨服务协作

  • 同步: gRPC + protobuf 契约
  • 异步: Outbox + Kafka 事件
  • 编排: Temporal 工作流P3 试点)

9. 核心业务流程

P3 起补充考试全生命周期 sequence diagram

10. 可观测性

  • Logs: pino/winston/zap 结构化 JSON + traceId
  • Metrics: Prometheus /metrics endpoint
  • Traces: OpenTelemetry SDK + W3C TraceContext

11. API 与实时通信

  • REST/GraphQL: Gateway 路由 + BFF 聚合
  • SSE: AI 网关流式响应
  • WebSocket: Push Gateway 长连接

12. 架构约束

  • 严格服务边界,禁止跨服务直连数据库
  • 契约先行proto + buf
  • 每服务独立部署与扩缩容
  • 单文件行数TS ≤500/800/1000, Go ≤800, Python ≤800

13. ADR

(按阶段补充架构决策记录)

14. 附录

  • arch.db 查询:npm run arch:query -- <command>
  • 模块文档:docs/modules/<service>/README.md

- [ ] **Step 3: 创建 coding-standards.md**

Create `e:\Desktop\Edu\docs\standards\coding-standards.md`:
```markdown
# 编码规范(多语言版)

## 通用规则

- 提交前必须通过 `pnpm lint` 和对应语言类型检查
- 函数返回值必须显式标注
- 禁止 `any`/`Object`/`Dynamic`(未知类型用 `unknown` + 类型守卫)
- 单文件行数TS ≤500/800/1000, Go ≤800, Python ≤800

## TypeScript

- 框架: NestJS服务+ Next.js前端
- ORM: Drizzle
- 仅用于类型的导入用 `import type`
- 可选链后禁止非空断言 `!`
- Server Action 必须 `requirePermission()`
- 前端权限检查用 `usePermission().hasPermission()`

## Go

- 框架: Gin
- ORM: GORM
- error 必须显式处理,禁止 `_`
- 接口先定义,结构体实现
- 包结构: `internal/{config,middleware,handler,service,repository}`

## Python

- 框架: FastAPI
- ORM: SQLAlchemy 2.0
- 全面类型注解PEP 484
- async/await 优先
- ruff 格式化 + lint

## 命名规范

- 目录: kebab-case
- 组件文件: PascalCase
- 变量/函数: camelCaseTS/ snake_caseGo/Python
- 常量: UPPER_SNAKE_CASE
- 类/接口: PascalCase接口不加 I 前缀)

## 契约规范

- protobuf 作为服务间契约唯一源
- `buf lint` + `buf breaking` CI 强制
- 包名带版本: `xxx.v1`、`xxx.v2`

## 安全规范

- JWT RS256P2 起)
- 参数化 SQL禁止字符串拼接
- 环境变量校验
- 禁止 `dangerouslySetInnerHTML`(除非 DOMPurify 清洗)
  • Step 4: 创建 git-workflow.md

Create e:\Desktop\Edu\docs\standards\git-workflow.md:

# Git 工作流

## 分支策略trunk-based 简化版)

main # 受保护,仅 PR 合入,始终可发布 ├─ feature/xxx # feature 分支,生命周期 < 1 周 └─ fix/xxx # bugfix 分支


## 提交规范

Conventional Commits: `<type>(<scope>): <description>`

- types: feat, fix, chore, docs, style, refactor, test, perf, ci, build
- scope: 服务名或包名
- header ≤ 100 字符

## PR 流程

1. 创建 feature 分支
2. 开发 + CI 通过lint + test + build + arch:scan
3. 提交 PR
4. CI 全绿后合并到 main
5. 每阶段末打 tag: `v0.1.0-p1`

## 文档同步

改码后必须:
1. `npm run arch:scan`
2. 架构意图变化 → 更新 004
3. 接口变化 → 更新模块 README
4. 新经验 → 更新 known-issues
  • Step 5: 创建 known-issues.md

Create e:\Desktop\Edu\docs\troubleshooting\known-issues.md:

# 已知问题速查(索引式)

> 场景→技术/规则映射,不写代码示例。新经验按时间倒序追加到"工作经验日志"。

## 全局经验

### Docker Compose 本地基础设施

| 场景 | 技术/规则 |
|------|----------|
| 日常开发启动 | 用 docker-compose.minimal.yml 仅起 MySQL+Redis |
| 全量启动内存不足 | 按阶段 profiles 启用,每服务 mem_limit 限制 |
| Windows 下卷挂载失败 | init-sql 用绝对路径或确保相对路径正确 |

### gRPC 契约管理

| 场景 | 技术/规则 |
|------|----------|
| breaking change 检测 | buf breaking CI 强制,必须升版本号 |
| proto 版本化 | 包名带版本 classes.v1新旧共存 |

### 多语言 arch.db 扫描

| 场景 | 技术/规则 |
|------|----------|
| Go 扫描 | tree-sitter-go 解析 AST |
| Python 扫描 | tree-sitter-python 解析 AST |
| 扫描后验证 | npm run arch:query -- violations 输出 0 |

## 模块经验

### api-gatewayGo

| 场景 | 技术/规则 |
|------|----------|
| P1 鉴权 | Gateway 内置 HS256 JWTP2 改 RS256 |
| 路由转发 | gin + httputil.ReverseProxy |

### classesTS黄金模板

| 场景 | 技术/规则 |
|------|----------|
| 黄金模板复制 | cp -r services/classes services/xxx改错误码前缀+proto+业务逻辑 |
| 横切关注点 | 错误处理/可观测/安全/契约/测试/文档/配置/i18n/CI/Dockerfile |

## 工作经验日志

| 日期 | 模块 | 做了什么 + 学到什么 |
|------|------|-------------------|
| 2026-07-07 | 全局 | P1 启动:新仓库初始化 + monorepo 骨架 + Docker 基础设施 |
  • Step 6: 创建 project_rules.md

Create e:\Desktop\Edu\project_rules.md:

# 项目规则(多语言版)

## 架构图优先规则

**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**

1. **先图后码**: 执行任何任务时,先运行 `npm run arch:scan` 更新 arch.db再通过 `npm run arch:query` 查询目标模块/服务/依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图
2. **图未覆盖则先补图**: 发现 arch.db 未记录的模块/函数/服务,先运行 `npm run arch:scan` 重新扫描
3. **改码必同步图**: 修改源码后必须运行 `npm run arch:scan` 更新 arch.db若架构设计意图变化同步更新 004

### 需要同步图的场景

- 新增/删除/重命名导出函数、组件、Hook、类型TS/Go/Python
- 修改函数签名
- 修改权限点或角色-权限映射
- 新增/删除数据库表
- 新增/删除 gRPC 方法或 proto message
- 新增/删除 Kafka topic 或事件类型
- 新增/删除服务
- 修改服务间依赖关系

## arch.db 元数据库规则

**arch.db 是代码结构唯一源AI 工作前必须运行 `npm run arch:scan` 更新。**

### 查询命令

- `npm run arch:scan`: 全量扫描
- `npm run arch:query -- sql "<SQL>"`: 自定义 SQL
- `npm run arch:query -- module <module>`: 模块依赖
- `npm run arch:query -- service <service>`: 服务级依赖
- `npm run arch:query -- contracts`: proto 契约清单
- `npm run arch:query -- events`: Kafka 事件清单
- `npm run arch:query -- violations`: 架构违规

## 编码规范

详细见 `docs/standards/coding-standards.md`,核心强制规则:

### 代码质量

- 每次修改后运行 lint 和类型检查确保零错误
- 服务接口必须 `requirePermission()` 权限校验
- 单文件行数: TS ≤500/800/1000, Go ≤800, Python ≤800

### 架构分层

- 严格服务边界,禁止跨服务直连数据库
- 服务间通信用 gRPC + protobuf 契约
- 服务间事件通信用 Outbox + Kafka
- 单一契约源: proto禁止 REST/gRPC 混用

### 服务标准结构(黄金模板复制)

services// ├─ src/ │ ├─ main.ts # bootstrap + OTel 初始化 │ ├─ config/ # 三层配置 │ ├─ middleware/ # auth/permission/data-scope │ ├─ / # 业务模块 │ ├─ shared/ # errors/observability/i18n │ └─ generated/ # proto 生成代码 ├─ test/{unit,integration,contract,e2e}/ ├─ Dockerfile └─ README.md


## 提交规范

- Conventional Commits: `feat(scope): description`
- 提交前: `pnpm lint && pnpm test`

## 问题记录规则

**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`。**

### 必须记录的场景

| 场景 | 记录位置 |
|------|---------|
| 构建报错 | 全局经验对应主题 |
| 运行时异常 | 模块经验对应服务 |
| 框架/库兼容问题 | 全局经验 |
| 架构违规 | 全局经验 |

## AI 工作强制流程

### 阶段 1: 上下文加载

1. `npm run arch:scan` 更新 arch.db
2. `npm run arch:query -- service <目标服务>` 查服务依赖
3. 阅读 `docs/modules/<服务>/README.md` 读服务工作流程
4. 查 `docs/troubleshooting/known-issues.md` 对应服务分区

### 阶段 2: 执行工作

1. 按规划执行
2. 修改代码后立即运行 `npm run arch:scan` 更新 arch.db
3. 运行 lint 和类型检查确保零错误

### 阶段 3: 经验沉淀

1. 在 `known-issues.md` 工作经验日志追加一条记录
2. 若发现新"场景→技术"映射,提炼到对应分区
3. 若架构决策变化,更新 004
4. `npm run arch:scan` 确认 arch.db 已更新
  • Step 7: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "docs: migrate architecture documents and add multi-language standards"

Task 5: arch.db 多语言扫描器骨架

目标: 创建支持 TS+Go+Python 的 arch.db 扫描器骨架P1 先实现 TS 扫描 + Go 扫描骨架。

Files:

  • Create: e:\Desktop\Edu\scripts\arch-scan\package.json

  • Create: e:\Desktop\Edu\scripts\arch-scan\scanner.ts

  • Create: e:\Desktop\Edu\scripts\arch-scan\scanners\ts-scanner.ts

  • Create: e:\Desktop\Edu\scripts\arch-scan\scanners\go-scanner.ts

  • Create: e:\Desktop\Edu\scripts\arch-scan\scanners\py-scanner.ts

  • Create: e:\Desktop\Edu\scripts\arch-scan\query.ts

  • Create: e:\Desktop\Edu\scripts\arch-scan\schema.ts

  • Step 1: 创建 arch-scan package.json

Create e:\Desktop\Edu\scripts\arch-scan\package.json:

{
  "name": "@next-edu/arch-scan",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "bin": {
    "arch-scan": "./scanner.ts",
    "arch-query": "./query.ts"
  },
  "scripts": {
    "scan": "tsx scanner.ts",
    "query": "tsx query.ts"
  },
  "dependencies": {
    "better-sqlite3": "^11.0.0",
    "ts-morph": "^24.0.0",
    "tree-sitter": "^0.21.0",
    "tree-sitter-go": "^0.21.0",
    "tree-sitter-python": "^0.21.0"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.10"
  }
}
  • Step 2: 安装依赖

Run:

cd e:\Desktop\Edu
pnpm install
  • Step 3: 创建 schema.ts数据库 schema 定义)

Create e:\Desktop\Edu\scripts\arch-scan\schema.ts:

import Database from 'better-sqlite3';

const SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS modules (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE,
  path TEXT NOT NULL,
  language TEXT,
  service TEXT,
  type TEXT
);

CREATE TABLE IF NOT EXISTS symbols (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  module_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  kind TEXT NOT NULL,
  language TEXT,
  signature TEXT,
  line INTEGER,
  FOREIGN KEY (module_id) REFERENCES modules(id)
);

CREATE TABLE IF NOT EXISTS dependencies (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  source_module_id INTEGER NOT NULL,
  target_module_id INTEGER,
  target_name TEXT NOT NULL,
  dependency_type TEXT,
  FOREIGN KEY (source_module_id) REFERENCES modules(id)
);

CREATE TABLE IF NOT EXISTS contracts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  proto_file TEXT NOT NULL,
  service_name TEXT NOT NULL,
  method_name TEXT NOT NULL,
  request_type TEXT,
  response_type TEXT,
  breaking_change INTEGER DEFAULT 0
);

CREATE TABLE IF NOT EXISTS events (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  topic TEXT NOT NULL,
  event_type TEXT,
  producer_service TEXT,
  consumer_services TEXT,
  schema_proto TEXT
);

CREATE TABLE IF NOT EXISTS violations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  rule TEXT NOT NULL,
  file TEXT NOT NULL,
  line INTEGER,
  message TEXT,
  severity TEXT
);
`;

export function initializeDatabase(db: Database.Database): void {
  db.exec(SCHEMA_SQL);
}
  • Step 4: 创建 ts-scanner.ts

Create e:\Desktop\Edu\scripts\arch-scan\scanners\ts-scanner.ts:

import { Project, SyntaxKind } from 'ts-morph';
import path from 'node:path';
import type Database from 'better-sqlite3';

interface ScanResult {
  modules: number;
  symbols: number;
  dependencies: number;
}

export function scanTypeScript(rootPath: string, db: Database.Database, serviceName?: string): ScanResult {
  const project = new Project({
    tsConfigFilePath: undefined,
    skipAddingFilesFromTsConfig: true,
  });

  const tsFiles = findTsFiles(rootPath);
  let modules = 0;
  let symbols = 0;
  let dependencies = 0;

  const insertModule = db.prepare(
    'INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)'
  );
  const insertSymbol = db.prepare(
    'INSERT INTO symbols (module_id, name, kind, language, signature, line) VALUES (?, ?, ?, ?, ?, ?)'
  );
  const insertDep = db.prepare(
    'INSERT INTO dependencies (source_module_id, target_name, dependency_type) VALUES (?, ?, ?)'
  );
  const findModule = db.prepare('SELECT id FROM modules WHERE name = ?');

  for (const filePath of tsFiles) {
    const sourceFile = project.addSourceFileAtPath(filePath);
    const moduleName = path.relative(rootPath, filePath).replace(/\.tsx?$/, '');

    insertModule.run(moduleName, filePath, 'ts', serviceName, 'file');
    const moduleRow = findModule.get(moduleName) as { id: number } | undefined;
    if (!moduleRow) continue;
    modules++;

    // 扫描导出符号
    for (const exportDecl of sourceFile.getExportDeclarations()) {
      for (const specifier of exportDecl.getNamedExports()) {
        insertSymbol.run(moduleRow.id, specifier.getName(), 'export', 'ts', null, specifier.getStartLineNumber());
        symbols++;
      }
    }

    // 扫描函数声明
    for (const func of sourceFile.getFunctions()) {
      const name = func.getName();
      if (name) {
        insertSymbol.run(moduleRow.id, name, 'function', 'ts', func.getSignature().toString(), func.getStartLineNumber());
        symbols++;
      }
    }

    // 扫描类声明
    for (const cls of sourceFile.getClasses()) {
      insertSymbol.run(moduleRow.id, cls.getName() || 'anonymous', 'class', 'ts', null, cls.getStartLineNumber());
      symbols++;
    }

    // 扫描 import 依赖
    for (const importDecl of sourceFile.getImportDeclarations()) {
      const moduleSpecifier = importDecl.getModuleSpecifierValue();
      insertDep.run(moduleRow.id, moduleSpecifier, 'import');
      dependencies++;
    }
  }

  return { modules, symbols, dependencies };
}

function findTsFiles(rootPath: string): string[] {
  const { globSync } = require('node:fs');
  const patterns = ['**/*.ts', '**/*.tsx'];
  const ignorePatterns = ['**/node_modules/**', '**/dist/**', '**/.next/**', '**/generated/**'];
  const files: string[] = [];
  for (const pattern of patterns) {
    const matches = globSync(pattern, { cwd: rootPath, ignore: ignorePatterns, absolute: true });
    files.push(...matches);
  }
  return files;
}
  • Step 5: 创建 go-scanner.ts骨架

Create e:\Desktop\Edu\scripts\arch-scan\scanners\go-scanner.ts:

import fs from 'node:fs';
import path from 'node:path';
import type Database from 'better-sqlite3';

interface ScanResult {
  modules: number;
  symbols: number;
  dependencies: number;
}

export function scanGo(rootPath: string, db: Database.Database, serviceName?: string): ScanResult {
  // P1 骨架:用正则提取 Go 符号tree-sitter-go 原生绑定较复杂,先用简化版)
  // TODO P2: 替换为 tree-sitter-go AST 解析
  const goFiles = findGoFiles(rootPath);
  let modules = 0;
  let symbols = 0;
  let dependencies = 0;

  const insertModule = db.prepare(
    'INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)'
  );
  const insertSymbol = db.prepare(
    'INSERT INTO symbols (module_id, name, kind, language, signature, line) VALUES (?, ?, ?, ?, ?, ?)'
  );
  const insertDep = db.prepare(
    'INSERT INTO dependencies (source_module_id, target_name, dependency_type) VALUES (?, ?, ?)'
  );
  const findModule = db.prepare('SELECT id FROM modules WHERE name = ?');

  for (const filePath of goFiles) {
    const content = fs.readFileSync(filePath, 'utf-8');
    const moduleName = path.relative(rootPath, filePath).replace(/\.go$/, '');

    insertModule.run(moduleName, filePath, 'go', serviceName, 'file');
    const moduleRow = findModule.get(moduleName) as { id: number } | undefined;
    if (!moduleRow) continue;
    modules++;

    // 正则提取函数
    const funcRegex = /^func\s+(?:\([^)]*\)\s+)?([A-Z]\w+)\s*\(/gm;
    let match: RegExpExecArray | null;
    while ((match = funcRegex.exec(content)) !== null) {
      insertSymbol.run(moduleRow.id, match[1], 'function', 'go', null, getLineNumber(content, match.index));
      symbols++;
    }

    // 正则提取类型
    const typeRegex = /^type\s+([A-Z]\w+)\s+/gm;
    while ((match = typeRegex.exec(content)) !== null) {
      insertSymbol.run(moduleRow.id, match[1], 'type', 'go', null, getLineNumber(content, match.index));
      symbols++;
    }

    // 正则提取 import
    const importRegex = /"([^"]+)"/g;
    const importBlock = content.match(/import\s*\(([\s\S]*?)\)/);
    if (importBlock) {
      while ((match = importRegex.exec(importBlock[1])) !== null) {
        insertDep.run(moduleRow.id, match[1], 'import');
        dependencies++;
      }
    }
  }

  return { modules, symbols, dependencies };
}

function findGoFiles(rootPath: string): string[] {
  const { globSync } = require('node:fs');
  return globSync('**/*.go', { cwd: rootPath, ignore: ['**/vendor/**'], absolute: true });
}

function getLineNumber(content: string, index: number): number {
  return content.substring(0, index).split('\n').length;
}
  • Step 6: 创建 py-scanner.ts骨架

Create e:\Desktop\Edu\scripts\arch-scan\scanners\py-scanner.ts:

import fs from 'node:fs';
import path from 'node:path';
import type Database from 'better-sqlite3';

interface ScanResult {
  modules: number;
  symbols: number;
  dependencies: number;
}

export function scanPython(rootPath: string, db: Database.Database, serviceName?: string): ScanResult {
  // P1 骨架:用正则提取 Python 符号
  // TODO P4: 替换为 tree-sitter-python AST 解析
  const pyFiles = findPyFiles(rootPath);
  let modules = 0;
  let symbols = 0;
  let dependencies = 0;

  const insertModule = db.prepare(
    'INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)'
  );
  const insertSymbol = db.prepare(
    'INSERT INTO symbols (module_id, name, kind, language, signature, line) VALUES (?, ?, ?, ?, ?, ?)'
  );
  const insertDep = db.prepare(
    'INSERT INTO dependencies (source_module_id, target_name, dependency_type) VALUES (?, ?, ?)'
  );
  const findModule = db.prepare('SELECT id FROM modules WHERE name = ?');

  for (const filePath of pyFiles) {
    const content = fs.readFileSync(filePath, 'utf-8');
    const moduleName = path.relative(rootPath, filePath).replace(/\.py$/, '').replace(/\//g, '.');

    insertModule.run(moduleName, filePath, 'python', serviceName, 'file');
    const moduleRow = findModule.get(moduleName) as { id: number } | undefined;
    if (!moduleRow) continue;
    modules++;

    // 正则提取函数
    const funcRegex = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
    let match: RegExpExecArray | null;
    while ((match = funcRegex.exec(content)) !== null) {
      insertSymbol.run(moduleRow.id, match[1], 'function', 'python', null, getLineNumber(content, match.index));
      symbols++;
    }

    // 正则提取类
    const classRegex = /^class\s+(\w+)/m;
    while ((match = classRegex.exec(content)) !== null) {
      insertSymbol.run(moduleRow.id, match[1], 'class', 'python', null, getLineNumber(content, match.index));
      symbols++;
    }

    // 正则提取 import
    const importRegex = /^(?:from\s+([\w.]+)\s+)?import\s+(.+)/gm;
    while ((match = importRegex.exec(content)) !== null) {
      const target = match[1] || match[2].split(',')[0].trim();
      insertDep.run(moduleRow.id, target, 'import');
      dependencies++;
    }
  }

  return { modules, symbols, dependencies };
}

function findPyFiles(rootPath: string): string[] {
  const { globSync } = require('node:fs');
  return globSync('**/*.py', { cwd: rootPath, ignore: ['**/__pycache__/**', '**/.venv/**'], absolute: true });
}

function getLineNumber(content: string, index: number): number {
  return content.substring(0, index).split('\n').length;
}
  • Step 7: 创建 scanner.ts 主入口

Create e:\Desktop\Edu\scripts\arch-scan\scanner.ts:

import Database from 'better-sqlite3';
import path from 'node:path';
import { initializeDatabase } from './schema.js';
import { scanTypeScript } from './scanners/ts-scanner.js';
import { scanGo } from './scanners/go-scanner.js';
import { scanPython } from './scanners/py-scanner.js';

const ROOT = path.resolve(import.meta.dirname, '..', '..');
const DB_PATH = path.join(ROOT, 'arch.db');

function main(): void {
  console.log('🔍 Starting arch.db scan...');
  console.log(`   Root: ${ROOT}`);
  console.log(`   DB: ${DB_PATH}`);

  const db = new Database(DB_PATH);
  db.pragma('journal_mode = WAL');

  // 清空旧数据(全量扫描)
  db.exec('DELETE FROM dependencies; DELETE FROM symbols; DELETE FROM modules; DELETE FROM violations;');

  initializeDatabase(db);

  const stats = {
    ts: { modules: 0, symbols: 0, dependencies: 0 },
    go: { modules: 0, symbols: 0, dependencies: 0 },
    py: { modules: 0, symbols: 0, dependencies: 0 },
  };

  // 扫描 TS 服务
  const tsServiceDirs = ['services/classes', 'bff', 'apps', 'packages'];
  for (const dir of tsServiceDirs) {
    const fullPath = path.join(ROOT, dir);
    if (require('node:fs').existsSync(fullPath)) {
      const result = scanTypeScript(fullPath, db, path.basename(dir));
      stats.ts.modules += result.modules;
      stats.ts.symbols += result.symbols;
      stats.ts.dependencies += result.dependencies;
    }
  }

  // 扫描 Go 服务
  const goServiceDirs = ['services/api-gateway'];
  for (const dir of goServiceDirs) {
    const fullPath = path.join(ROOT, dir);
    if (require('node:fs').existsSync(fullPath)) {
      const result = scanGo(fullPath, db, path.basename(dir));
      stats.go.modules += result.modules;
      stats.go.symbols += result.symbols;
      stats.go.dependencies += result.dependencies;
    }
  }

  // 扫描 Python 服务P4 起有内容)
  const pyServiceDirs = ['services/data-ana', 'services/ai-gateway'];
  for (const dir of pyServiceDirs) {
    const fullPath = path.join(ROOT, dir);
    if (require('node:fs').existsSync(fullPath)) {
      const result = scanPython(fullPath, db, path.basename(dir));
      stats.py.modules += result.modules;
      stats.py.symbols += result.symbols;
      stats.py.dependencies += result.dependencies;
    }
  }

  console.log('\n✅ Scan complete:');
  console.log(`   TypeScript: ${stats.ts.modules} modules, ${stats.ts.symbols} symbols, ${stats.ts.dependencies} deps`);
  console.log(`   Go: ${stats.go.modules} modules, ${stats.go.symbols} symbols, ${stats.go.dependencies} deps`);
  console.log(`   Python: ${stats.py.modules} modules, ${stats.py.symbols} symbols, ${stats.py.dependencies} deps`);

  db.close();
}

main();
  • Step 8: 创建 query.ts 查询工具

Create e:\Desktop\Edu\scripts\arch-scan\query.ts:

import Database from 'better-sqlite3';
import path from 'node:path';

const ROOT = path.resolve(import.meta.dirname, '..', '..');
const DB_PATH = path.join(ROOT, 'arch.db');

function printUsage(): void {
  console.log(`Usage: npm run arch:query -- <command> [args]

Commands:
  sql "<SQL>"                  自定义 SQL 查询
  module <name>                模块依赖
  service <name>               服务级依赖
  symbols <name>               符号引用
  contracts                    proto 契约清单
  events                       Kafka 事件清单
  violations                   架构违规
  stats                        统计信息
`);
}

function main(): void {
  const [, , command, ...args] = process.argv;

  if (!command) {
    printUsage();
    process.exit(1);
  }

  const db = new Database(DB_PATH, { readonly: true });

  switch (command) {
    case 'sql': {
      const sql = args.join(' ');
      const rows = db.prepare(sql).all();
      console.log(JSON.stringify(rows, null, 2));
      break;
    }
    case 'module': {
      const name = args[0];
      const module = db.prepare('SELECT * FROM modules WHERE name LIKE ?').all(`%${name}%`);
      console.log('Module:', module);
      const deps = db.prepare(`
        SELECT d.*, m.name as source_name FROM dependencies d
        JOIN modules m ON d.source_module_id = m.id
        WHERE m.name LIKE ?
      `).all(`%${name}%`);
      console.log('Dependencies:', deps);
      break;
    }
    case 'service': {
      const name = args[0];
      const modules = db.prepare('SELECT * FROM modules WHERE service = ?').all(name);
      console.log(`Service "${name}" modules:`, modules);
      break;
    }
    case 'symbols': {
      const name = args[0];
      const symbols = db.prepare('SELECT * FROM symbols WHERE name LIKE ?').all(`%${name}%`);
      console.log('Symbols:', symbols);
      break;
    }
    case 'contracts': {
      const contracts = db.prepare('SELECT * FROM contracts').all();
      console.log('Contracts:', contracts);
      break;
    }
    case 'events': {
      const events = db.prepare('SELECT * FROM events').all();
      console.log('Events:', events);
      break;
    }
    case 'violations': {
      const violations = db.prepare('SELECT * FROM violations').all();
      console.log('Violations:', violations);
      break;
    }
    case 'stats': {
      const moduleCount = db.prepare('SELECT COUNT(*) as count FROM modules').get() as { count: number };
      const symbolCount = db.prepare('SELECT COUNT(*) as count FROM symbols').get() as { count: number };
      const depCount = db.prepare('SELECT COUNT(*) as count FROM dependencies').get() as { count: number };
      console.log(`Modules: ${moduleCount.count}`);
      console.log(`Symbols: ${symbolCount.count}`);
      console.log(`Dependencies: ${depCount.count}`);
      break;
    }
    default:
      printUsage();
      process.exit(1);
  }

  db.close();
}

main();
  • Step 9: 验证扫描器运行

Run:

cd e:\Desktop\Edu
npm run arch:scan
npm run arch:query -- stats

Expected: 输出模块/符号/依赖统计P1 此时可能为 0因为还没有服务代码

  • Step 10: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "feat(arch-scan): add multi-language arch.db scanner skeleton"

Task 6: 共享 proto 包与 buf 配置

目标: 创建 shared-proto 包,定义 classes 契约,配置 buf 代码生成。

Files:

  • Create: e:\Desktop\Edu\packages\shared-proto\package.json

  • Create: e:\Desktop\Edu\packages\shared-proto\buf.yaml

  • Create: e:\Desktop\Edu\packages\shared-proto\buf.gen.yaml

  • Create: e:\Desktop\Edu\packages\shared-proto\proto\classes.proto

  • Step 1: 创建 package.json

Create e:\Desktop\Edu\packages\shared-proto\package.json:

{
  "name": "@next-edu/shared-proto",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "gen": "buf generate",
    "lint": "buf lint",
    "breaking": "buf breaking --against '.git#branch=main'"
  }
}
  • Step 2: 创建 buf.yaml

Create e:\Desktop\Edu\packages\shared-proto\buf.yaml:

version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD
  except:
    - PACKAGE_VERSION_SUFFIX
breaking:
  use:
    - FILE
  except:
    - EXTENSION_NO_DELETE
  • Step 3: 创建 buf.gen.yaml

Create e:\Desktop\Edu\packages\shared-proto\buf.gen.yaml:

version: v2
plugins:
  # TypeScriptNestJS 用)
  - local: ts
    out: ../shared-ts/generated
    opt: target=ts
  # GoAPI Gateway 用)
    # Go 插件在 Task 7 配置
managed:
  enabled: true
  • Step 4: 创建 classes.proto

Create e:\Desktop\Edu\packages\shared-proto\proto\classes.proto:

syntax = "proto3";

package next_edu_cloud.classes.v1;

// ClassService 班级服务
service ClassService {
  // 创建班级
  rpc CreateClass(CreateClassRequest) returns (Class);
  // 查询班级
  rpc GetClass(GetClassRequest) returns (Class);
  // 查询班级列表
  rpc ListClasses(ListClassesRequest) returns (ListClassesResponse);
  // 更新班级
  rpc UpdateClass(UpdateClassRequest) returns (Class);
  // 删除班级
  rpc DeleteClass(DeleteClassRequest) returns (Empty);
}

// 班级
message Class {
  string id = 1;
  string name = 2;
  string grade_id = 3;
  int64 created_at = 4;
  int64 updated_at = 5;
}

message CreateClassRequest {
  string name = 1;
  string grade_id = 2;
}

message GetClassRequest {
  string id = 1;
}

message ListClassesRequest {
  string grade_id = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListClassesResponse {
  repeated Class classes = 1;
  string next_page_token = 2;
}

message UpdateClassRequest {
  string id = 1;
  string name = 2;
  string grade_id = 3;
}

message DeleteClassRequest {
  string id = 1;
}

message Empty {}
  • Step 5: 验证 buf lint

Run:

cd e:\Desktop\Edu\packages\shared-proto
pnpm exec buf lint

Expected: 无 lint 错误。

  • Step 6: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "feat(shared-proto): add classes proto contract and buf configuration"

Task 7: API Gateway 骨架Go

目标: 创建 Go API Gateway实现路由 + JWT 校验HS256+ 转发到 classes 服务。

Files:

  • Create: e:\Desktop\Edu\services\api-gateway\go.mod

  • Create: e:\Desktop\Edu\services\api-gateway\main.go

  • Create: e:\Desktop\Edu\services\api-gateway\internal\config\config.go

  • Create: e:\Desktop\Edu\services\api-gateway\internal\middleware\auth.go

  • Create: e:\Desktop\Edu\services\api-gateway\internal\proxy\proxy.go

  • Create: e:\Desktop\Edu\services\api-gateway\Dockerfile

  • Step 1: 初始化 Go module

Run:

cd e:\Desktop\Edu\services\api-gateway
go mod init next-edu-cloud/api-gateway
go get github.com/gin-gonic/gin@v1.10.0
go get github.com/golang-jwt/jwt/v5@v5.2.1
  • Step 2: 创建 config.go

Create e:\Desktop\Edu\services\api-gateway\internal\config\config.go:

package config

import (
	"os"
	"strconv"
)

// Config API Gateway 配置
type Config struct {
	Port            string
	JWTSecret       string
	JWTIssuer       string
	JWTAudience     string
	ClassesServiceURL string
}

// Load 从环境变量加载配置
func Load() Config {
	return Config{
		Port:              getEnv("API_GATEWAY_PORT", "8080"),
		JWTSecret:         getEnv("JWT_SECRET", "p1-dev-secret"),
		JWTIssuer:         getEnv("JWT_ISSUER", "next-edu-cloud"),
		JWTAudience:       getEnv("JWT_AUDIENCE", "next-edu-cloud"),
		ClassesServiceURL: getEnv("CLASSES_SERVICE_URL", "http://localhost:3001"),
	}
}

func getEnv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func getEnvInt(key string, fallback int) int {
	if v := os.Getenv(key); v != "" {
		if i, err := strconv.Atoi(v); err == nil {
			return i
		}
	}
	return fallback
}
  • Step 3: 创建 auth middleware

Create e:\Desktop\Edu\services\api-gateway\internal\middleware\auth.go:

package middleware

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
)

// AuthClaims JWT claims
type AuthClaims struct {
	UserID string   `json:"user_id"`
	Roles  []string `json:"roles"`
	jwt.RegisteredClaims
}

// Auth JWT 鉴权中间件P1 用 HS256P2 改 RS256
func Auth(secret, issuer, audience string) gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
			return
		}

		parts := strings.SplitN(authHeader, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"})
			return
		}

		tokenString := parts[1]
		claims := &AuthClaims{}

		token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, jwt.ErrTokenSignatureInvalid
			}
			return []byte(secret), nil
		})

		if err != nil || !token.Valid {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
			return
		}

		// 注入用户上下文到请求头(传递给下游服务)
		c.Request.Header.Set("x-user-id", claims.UserID)
		c.Request.Header.Set("x-user-roles", strings.Join(claims.Roles, ","))
		c.Next()
	}
}
  • Step 4: 创建 proxy

Create e:\Desktop\Edu\services\api-gateway\internal\proxy\proxy.go:

package proxy

import (
	"net/http"
	"net/http/httputil"
	"net/url"

	"github.com/gin-gonic/gin"
)

// ReverseProxy 反向代理
func ReverseProxy(targetURL string) gin.HandlerFunc {
	target, err := url.Parse(targetURL)
	if err != nil {
		panic("invalid target URL: " + targetURL)
	}

	proxy := httputil.NewSingleHostReverseProxy(target)

	return func(c *gin.Context) {
		// 重写路径:去掉 /api/v1 前缀
		originalPath := c.Request.URL.Path
		c.Request.URL.Path = strings.TrimPrefix(originalPath, "/api/v1")
		if c.Request.URL.Path == "" {
			c.Request.URL.Path = "/"
		}

		proxy.ServeHTTP(c.Writer, c.Request)
	}
}

注:需要 import "strings",实际文件顶部 import 块需包含。

修正 proxy.go 完整内容:

package proxy

import (
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
)

// ReverseProxy 反向代理
func ReverseProxy(targetURL string) gin.HandlerFunc {
	target, err := url.Parse(targetURL)
	if err != nil {
		panic("invalid target URL: " + targetURL)
	}

	proxy := httputil.NewSingleHostReverseProxy(target)

	return func(c *gin.Context) {
		originalPath := c.Request.URL.Path
		c.Request.URL.Path = strings.TrimPrefix(originalPath, "/api/v1")
		if c.Request.URL.Path == "" {
			c.Request.URL.Path = "/"
		}

		proxy.ServeHTTP(c.Writer, c.Request)
	}
}
  • Step 5: 创建 main.go

Create e:\Desktop\Edu\services\api-gateway\main.go:

package main

import (
	"log"
	"time"

	"github.com/gin-gonic/gin"

	"next-edu-cloud/api-gateway/internal/config"
	"next-edu-cloud/api-gateway/internal/middleware"
	"next-edu-cloud/api-gateway/internal/proxy"
)

func main() {
	cfg := config.Load()

	r := gin.New()
	r.Use(gin.Logger())
	r.Use(gin.Recovery())
	r.Use(RequestID())

	// 健康检查(无需鉴权)
	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok", "timestamp": time.Now().Unix()})
	})

	// API v1 路由(需鉴权)
	v1 := r.Group("/api/v1")
	v1.Use(middleware.Auth(cfg.JWTSecret, cfg.JWTIssuer, cfg.JWTAudience))
	{
		v1.Any("/classes/*path", proxy.ReverseProxy(cfg.ClassesServiceURL))
	}

	log.Printf("API Gateway starting on :%s", cfg.Port)
	if err := r.Run(":" + cfg.Port); err != nil {
		log.Fatalf("failed to start server: %v", err)
	}
}

// RequestID 注入请求 ID
func RequestID() gin.HandlerFunc {
	return func(c *gin.Context) {
		requestID := c.GetHeader("X-Request-ID")
		if requestID == "" {
			requestID = time.Now().Format("20060102150405") + "-" + randomString(8)
		}
		c.Set("request_id", requestID)
		c.Header("X-Request-ID", requestID)
		c.Next()
	}
}

func randomString(n int) string {
	const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
	b := make([]byte, n)
	for i := range b {
		b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
	}
	return string(b)
}
  • Step 6: 创建 Dockerfile

Create e:\Desktop\Edu\services\api-gateway\Dockerfile:

# 构建阶段
FROM golang:1.22-alpine AS builder

WORKDIR /app

# 缓存依赖
COPY go.mod go.sum ./
RUN go mod download

# 复制源码并构建
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o api-gateway .

# 运行阶段
FROM alpine:3.20

RUN apk --no-cache add ca-certificates

WORKDIR /app
COPY --from=builder /app/api-gateway .

EXPOSE 8080

CMD ["./api-gateway"]
  • Step 7: 验证编译

Run:

cd e:\Desktop\Edu\services\api-gateway
go mod tidy
go build ./...

Expected: 编译成功,无错误。

  • Step 8: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "feat(api-gateway): add Go API Gateway with JWT auth and reverse proxy"

Task 8: classes 黄金模板服务 - 项目骨架与横切关注点

目标: 创建 NestJS classes 服务骨架实现所有横切关注点错误处理、可观测、安全、配置、i18n

Files:

  • Create: e:\Desktop\Edu\services\classes\package.json

  • Create: e:\Desktop\Edu\services\classes\tsconfig.json

  • Create: e:\Desktop\Edu\services\classes\nest-cli.json

  • Create: e:\Desktop\Edu\services\classes\src\main.ts

  • Create: e:\Desktop\Edu\services\classes\src\app.module.ts

  • Create: e:\Desktop\Edu\services\classes\src\config\config.ts

  • Create: e:\Desktop\Edu\services\classes\src\config\env.ts

  • Create: e:\Desktop\Edu\services\classes\src\shared\errors\application-error.ts

  • Create: e:\Desktop\Edu\services\classes\src\shared\observability\logger.ts

  • Create: e:\Desktop\Edu\services\classes\src\shared\observability\metrics.ts

  • Create: e:\Desktop\Edu\services\classes\src\shared\observability\tracer.ts

  • Create: e:\Desktop\Edu\services\classes\src\shared\i18n\error-codes.ts

  • Create: e:\Desktop\Edu\services\classes\src\middleware\auth.middleware.ts

  • Create: e:\Desktop\Edu\services\classes\src\middleware\permission.guard.ts

  • Create: e:\Desktop\Edu\services\classes\src\middleware\data-scope.interceptor.ts

  • Create: e:\Desktop\Edu\services\classes\Dockerfile

  • Step 1: 创建 package.json

Create e:\Desktop\Edu\services\classes\package.json:

{
  "name": "@next-edu/classes",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "nest start --watch",
    "build": "nest build",
    "start": "node dist/main.js",
    "lint": "eslint src --ext .ts",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:e2e": "vitest run --config vitest.e2e.config.ts"
  },
  "dependencies": {
    "@nestjs/common": "^10.3.0",
    "@nestjs/core": "^10.3.0",
    "@nestjs/platform-express": "^10.3.0",
    "drizzle-orm": "^0.31.0",
    "mysql2": "^3.10.0",
    "pino": "^9.3.0",
    "pino-http": "^10.2.0",
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/sdk-node": "^0.53.0",
    "@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
    "@opentelemetry/resources": "^1.26.0",
    "@opentelemetry/semantic-conventions": "^1.26.0",
    "prom-client": "^15.1.0",
    "uuid": "^10.0.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.4.0",
    "@types/node": "^20.14.0",
    "@types/uuid": "^10.0.0",
    "drizzle-kit": "^0.23.0",
    "eslint": "^9.7.0",
    "typescript": "^5.5.0",
    "vitest": "^2.0.0",
    "@vitest/coverage-v8": "^2.0.0",
    "testcontainers": "^10.13.0"
  }
}
  • Step 2: 创建 tsconfig.json

Create e:\Desktop\Edu\services\classes\tsconfig.json:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "lib": ["ES2022"],
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitAny": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}
  • Step 3: 创建 nest-cli.json

Create e:\Desktop\Edu\services\classes\nest-cli.json:

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true
  }
}
  • Step 4: 创建 config/env.ts环境变量校验

Create e:\Desktop\Edu\services\classes\src\config\env.ts:

import { z } from 'zod';

const envSchema = z.object({
  PORT: z.string().default('3001').transform(Number),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url().optional(),
  JWT_SECRET: z.string().min(16),
  JWT_ISSUER: z.string().default('next-edu-cloud'),
  OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
  LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});

export type Env = z.infer<typeof envSchema>;

export function loadEnv(): Env {
  const result = envSchema.safeParse(process.env);
  if (!result.success) {
    console.error('❌ Invalid environment variables:', result.error.flatten().fieldErrors);
    throw new Error('Invalid environment variables');
  }
  return result.data;
}

注:需要 pnpm add zod 到 dependencies。

  • Step 5: 创建 config/config.ts三层配置

Create e:\Desktop\Edu\services\classes\src\config\config.ts:

import { loadEnv, type Env } from './env.js';

export interface AppConfig {
  env: Env;
  service: {
    name: string;
    version: string;
  };
  database: {
    url: string;
    poolSize: number;
  };
  jwt: {
    secret: string;
    issuer: string;
  };
  observability: {
    otlpEndpoint?: string;
    logLevel: string;
  };
}

export function loadConfig(): AppConfig {
  const env = loadEnv();
  return {
    env,
    service: {
      name: 'classes',
      version: '0.1.0',
    },
    database: {
      url: env.DATABASE_URL,
      poolSize: 10,
    },
    jwt: {
      secret: env.JWT_SECRET,
      issuer: env.JWT_ISSUER,
    },
    observability: {
      otlpEndpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT,
      logLevel: env.LOG_LEVEL,
    },
  };
}
  • Step 6: 创建 errors/application-error.ts

Create e:\Desktop\Edu\services\classes\src\shared\errors\application-error.ts:

export type ErrorType =
  | 'VALIDATION'
  | 'NOT_FOUND'
  | 'PERMISSION_DENIED'
  | 'CONFLICT'
  | 'BUSINESS'
  | 'DATABASE'
  | 'UPSTREAM'
  | 'TIMEOUT'
  | 'INTERNAL'
  | 'CONTRACT';

export interface ErrorDetails {
  field?: string;
  message?: string;
  [key: string]: unknown;
}

export abstract class ApplicationError extends Error {
  abstract readonly type: ErrorType;
  abstract readonly statusCode: number;
  readonly code: string;
  readonly details?: ErrorDetails;
  readonly traceId?: string;

  constructor(code: string, message: string, details?: ErrorDetails) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
    this.details = details;
  }

  toJSON(): Record<string, unknown> {
    return {
      success: false,
      error: {
        code: this.code,
        message: this.message,
        details: this.details,
        traceId: this.traceId,
      },
    };
  }
}

export class ValidationError extends ApplicationError {
  readonly type: ErrorType = 'VALIDATION';
  readonly statusCode = 400;
}

export class NotFoundError extends ApplicationError {
  readonly type: ErrorType = 'NOT_FOUND';
  readonly statusCode = 404;
}

export class PermissionDeniedError extends ApplicationError {
  readonly type: ErrorType = 'PERMISSION_DENIED';
  readonly statusCode = 403;
}

export class ConflictError extends ApplicationError {
  readonly type: ErrorType = 'CONFLICT';
  readonly statusCode = 409;
}

export class BusinessError extends ApplicationError {
  readonly type: ErrorType = 'BUSINESS';
  readonly statusCode = 400;
}

export class DatabaseError extends ApplicationError {
  readonly type: ErrorType = 'DATABASE';
  readonly statusCode = 500;
}

export class InternalError extends ApplicationError {
  readonly type: ErrorType = 'INTERNAL';
  readonly statusCode = 500;
}
  • Step 7: 创建 observability/logger.ts

Create e:\Desktop\Edu\services\classes\src\shared\observability\logger.ts:

import pino from 'pino';

export interface LogContext {
  traceId?: string;
  spanId?: string;
  userId?: string;
  service?: string;
  [key: string]: unknown;
}

export type Logger = pino.Logger;

export function createLogger(serviceName: string, logLevel: string): Logger {
  return pino({
    name: serviceName,
    level: logLevel,
    formatters: {
      level(label: string): Record<string, unknown> {
        return { level: label };
      },
    },
    timestamp: pino.stdTimeFunctions.isoTime,
  });
}

export function childLogger(parent: Logger, context: LogContext): Logger {
  return parent.child(context);
}
  • Step 8: 创建 observability/metrics.ts

Create e:\Desktop\Edu\services\classes\src\shared\observability\metrics.ts:

import promClient from 'prom-client';

export function setupMetrics(serviceName: string): promClient.Registry {
  const registry = new promClient.Registry();
  registry.setDefaultLabels({ service: serviceName });

  promClient.collectDefaultMetrics({ register: registry });

  // 自定义指标
  const httpRequestDuration = new promClient.Histogram({
    name: 'http_request_duration_seconds',
    help: 'HTTP request duration in seconds',
    labelNames: ['method', 'route', 'status'],
    buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
    registers: [registry],
  });

  const httpRequestTotal = new promClient.Counter({
    name: 'http_requests_total',
    help: 'Total HTTP requests',
    labelNames: ['method', 'route', 'status'],
    registers: [registry],
  });

  return registry;
}

export { promClient };
  • Step 9: 创建 observability/tracer.ts

Create e:\Desktop\Edu\services\classes\src\shared\observability\tracer.ts:

import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

let sdk: NodeSDK | undefined;

export function setupTracing(serviceName: string, otlpEndpoint?: string): void {
  if (!otlpEndpoint) {
    return;
  }

  sdk = new NodeSDK({
    resource: new Resource({
      [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
    }),
    traceExporter: new OTLPTraceExporter({ url: otlpEndpoint }),
  });

  sdk.start();
}

export async function shutdownTracing(): Promise<void> {
  if (sdk) {
    await sdk.shutdown();
  }
}
  • Step 10: 创建 i18n/error-codes.ts

Create e:\Desktop\Edu\services\classes\src\shared\i18n\error-codes.ts:

// 错误码 → i18n key 映射
// 前端根据 key 翻译,后端不返回翻译后文本

export const ERROR_CODES = {
  // 班级相关
  CLASSES_VAL_NAME_REQUIRED: 'errors.classes.validation.nameRequired',
  CLASSES_VAL_GRADE_REQUIRED: 'errors.classes.validation.gradeRequired',
  CLASSES_NOT_FOUND: 'errors.classes.notFound',
  CLASSES_CONFLICT_DUPLICATE: 'errors.classes.conflict.duplicate',
  CLASSES_PERM_CREATE: 'errors.classes.permission.create',
  CLASSES_PERM_UPDATE: 'errors.classes.permission.update',
  CLASSES_PERM_DELETE: 'errors.classes.permission.delete',

  // 通用
  INTERNAL_ERROR: 'errors.common.internal',
  DATABASE_ERROR: 'errors.common.database',
} as const;

export type ErrorCode = keyof typeof ERROR_CODES;

export function getI18nKey(code: ErrorCode): string {
  return ERROR_CODES[code] ?? 'errors.common.unknown';
}
  • Step 11: 创建 middleware/auth.middleware.ts

Create e:\Desktop\Edu\services\classes\src\middleware\auth.middleware.ts:

import type { Request, Response, NextFunction } from 'express';

export interface UserContext {
  userId: string;
  roles: string[];
  permissions: string[];
  dataScope: DataScope;
}

export type DataScope =
  | 'all'
  | 'grade_managed'
  | 'class_taught'
  | 'class_members'
  | 'children'
  | 'owned';

declare module 'express' {
  interface Request {
    user?: UserContext;
  }
}

// auth.middleware 信任 Gateway 注入的 x-user-* 头
// P2 起由 IAM 签发 JWTGateway 解析后注入
export function authMiddleware() {
  return (req: Request, res: Response, next: NextFunction): void => {
    const userId = req.header('x-user-id');
    const rolesHeader = req.header('x-user-roles');

    if (!userId) {
      res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Missing user context' } });
      return;
    }

    req.user = {
      userId,
      roles: rolesHeader ? rolesHeader.split(',') : [],
      permissions: [], // P2 由 IAM 解析注入
      dataScope: 'all', // P1 默认 allP2 由 IAM 解析
    };

    next();
  };
}
  • Step 12: 创建 middleware/permission.guard.ts

Create e:\Desktop\Edu\services\classes\src\middleware\permission.guard.ts:

import type { CanActivate, ExecutionContext } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import type { Request } from 'express';
import { PermissionDeniedError } from '../shared/errors/application-error.js';

// 权限点常量(沿用旧项目命名)
export const Permissions = {
  CLASS_CREATE: 'class:create',
  CLASS_READ: 'class:read',
  CLASS_UPDATE: 'class:update',
  CLASS_DELETE: 'class:delete',
} as const;

export type Permission = typeof Permissions[keyof typeof Permissions];

@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(private readonly requiredPermission: Permission) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    const user = request.user;

    if (!user) {
      throw new PermissionDeniedError('CLASSES_PERM_AUTH', 'User not authenticated');
    }

    // admin 角色拥有全部权限
    if (user.roles.includes('admin')) {
      return true;
    }

    if (!user.permissions.includes(this.requiredPermission)) {
      throw new PermissionDeniedError(
        this.requiredPermission.replace(':', '_').toUpperCase(),
        `Permission ${this.requiredPermission} required`,
      );
    }

    return true;
  }
}

// 便捷装饰器函数
export function requirePermission(permission: Permission) {
  return new PermissionGuard(permission);
}
  • Step 13: 创建 middleware/data-scope.interceptor.ts

Create e:\Desktop\Edu\services\classes\src\middleware\data-scope.interceptor.ts:

import type { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import type { Observable } from 'rxjs';
import type { Request } from 'express';

// DataScope 过滤拦截器
// P1 简化版:仅注入 dataScope 到 request供 repository 使用
// P2 起由 IAM 解析 DataScope 并注入 JWT
@Injectable()
export class DataScopeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest<Request>();
    // dataScope 已由 authMiddleware 注入
    // repository 根据 dataScope 添加 WHERE 条件
    return next.handle();
  }
}
  • Step 14: 创建 main.ts

Create e:\Desktop\Edu\services\classes\src\main.ts:

import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module.js';
import { loadConfig } from './config/config.js';
import { createLogger } from './shared/observability/logger.js';
import { setupTracing, shutdownTracing } from './shared/observability/tracer.js';
import { setupMetrics } from './shared/observability/metrics.js';
import type { ApplicationError } from './shared/errors/application-error.js';

async function bootstrap(): Promise<void> {
  const config = loadConfig();
  const logger = createLogger(config.service.name, config.observability.logLevel);

  // 初始化 OpenTelemetry
  setupTracing(config.service.name, config.observability.otlpEndpoint);

  // 初始化 metrics
  const metricsRegistry = setupMetrics(config.service.name);

  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    logger: ['log', 'error', 'warn', 'debug', 'verbose'],
  });

  // 全局异常过滤器
  app.useGlobalFilters({
    catch(exception: unknown, host: import('@nestjs/common').ArgumentsHost) {
      const ctx = host.switchToHttp();
      const response = ctx.getResponse();
      const request = ctx.getRequest();

      const traceId = (request.headers['x-request-id'] as string) ?? undefined;

      if (exception instanceof (require('./shared/errors/application-error.js').ApplicationError)) {
        const appError = exception as ApplicationError & { statusCode: number };
        appError.traceId = traceId;
        response.status(appError.statusCode).json(appError.toJSON());
      } else {
        logger.error({ err: exception, traceId }, 'Unhandled exception');
        response.status(500).json({
          success: false,
          error: {
            code: 'INTERNAL_ERROR',
            message: 'Internal server error',
            traceId,
          },
        });
      }
    },
  });

  // metrics endpoint
  app.use('/metrics', (req, res) => {
    res.set('Content-Type', metricsRegistry.contentType);
    res.end(metricsRegistry.metrics());
  });

  // 健康检查
  app.use('/health', (req, res) => {
    res.json({ status: 'ok', service: config.service.name, version: config.service.version });
  });

  await app.listen(config.env.PORT);

  logger.info(`🚀 ${config.service.name} service started on port ${config.env.PORT}`);

  // 优雅关闭
  process.on('SIGTERM', async () => {
    logger.info('SIGTERM received, shutting down...');
    await app.close();
    await shutdownTracing();
    process.exit(0);
  });
}

bootstrap().catch((err) => {
  console.error('Failed to bootstrap application', err);
  process.exit(1);
});
  • Step 15: 创建 app.module.ts骨架业务在 Task 9 填充)

Create e:\Desktop\Edu\services\classes\src\app.module.ts:

import { Module } from '@nestjs/common';
import { ClassesModule } from './classes/classes.module.js';

@Module({
  imports: [ClassesModule],
})
export class AppModule {}
  • Step 16: 创建 Dockerfile

Create e:\Desktop\Edu\services\classes\Dockerfile:

# 构建阶段
FROM node:20-alpine AS builder

WORKDIR /app

RUN corepack enable pnpm

# 缓存依赖
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod=false

# 复制源码并构建
COPY . .
RUN pnpm build

# 运行阶段
FROM node:20-alpine

WORKDIR /app

RUN corepack enable pnpm

COPY package.json pnpm-lock.yaml ./
COPY --from=builder /app/dist ./dist
RUN pnpm install --frozen-lockfile --prod

EXPOSE 3001

CMD ["node", "dist/main.js"]
  • Step 17: 安装依赖并验证编译

Run:

cd e:\Desktop\Edu
pnpm install
cd services/classes
pnpm exec tsc --noEmit

Expected: 编译通过(可能有 classes.module 不存在的错误,下一个 Task 创建)。

  • Step 18: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "feat(classes): add NestJS golden template service skeleton with cross-cutting concerns"

Task 9: classes 黄金模板服务 - 业务逻辑 CRUD

目标: 实现 classes 域 CRUD 业务逻辑controller + service + repository + module

Files:

  • Create: e:\Desktop\Edu\services\classes\src\classes\classes.module.ts

  • Create: e:\Desktop\Edu\services\classes\src\classes\classes.controller.ts

  • Create: e:\Desktop\Edu\services\classes\src\classes\classes.service.ts

  • Create: e:\Desktop\Edu\services\classes\src\classes\classes.repository.ts

  • Create: e:\Desktop\Edu\services\classes\src\classes\classes.dto.ts

  • Create: e:\Desktop\Edu\services\classes\src\classes\classes.schema.ts

  • Step 1: 创建 classes.schema.tsDrizzle schema

Create e:\Desktop\Edu\services\classes\src\classes\classes.schema.ts:

import { mysqlTable, varchar, timestamp, index } from 'drizzle-orm/mysql-core';
import { createId } from '@paralleldrive/cuid2';

export const classes = mysqlTable(
  'classes',
  {
    id: varchar('id', { length: 36 }).$defaultFn(() => createId()).primaryKey(),
    name: varchar('name', { length: 100 }).notNull(),
    gradeId: varchar('grade_id', { length: 36 }).notNull(),
    createdAt: timestamp('created_at').defaultNow().notNull(),
    updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),
  },
  (table) => ({
    gradeIdIdx: index('idx_grade_id').on(table.gradeId),
  }),
);

export type Class = typeof classes.$inferSelect;
export type NewClass = typeof classes.$inferInsert;

注:需要 pnpm add @paralleldrive/cuid2 到 dependencies。

  • Step 2: 创建 classes.dto.ts

Create e:\Desktop\Edu\services\classes\src\classes\classes.dto.ts:

import { z } from 'zod';

export const createClassSchema = z.object({
  name: z.string().min(1, 'Class name is required').max(100),
  gradeId: z.string().min(1, 'Grade ID is required'),
});

export const updateClassSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  gradeId: z.string().min(1).optional(),
});

export type CreateClassDto = z.infer<typeof createClassSchema>;
export type UpdateClassDto = z.infer<typeof updateClassSchema>;

export interface ClassResponse {
  id: string;
  name: string;
  gradeId: string;
  createdAt: number;
  updatedAt: number;
}
  • Step 3: 创建 classes.repository.ts

Create e:\Desktop\Edu\services\classes\src\classes\classes.repository.ts:

import { Injectable } from '@nestjs/common';
import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2';
import { eq } from 'drizzle-orm';
import { classes, type Class, type NewClass } from './classes.schema.js';
import { DatabaseError, NotFoundError } from '../shared/errors/application-error.js';

@Injectable()
export class ClassesRepository {
  private readonly db: MySql2Database;

  constructor() {
    const databaseUrl = process.env.DATABASE_URL;
    if (!databaseUrl) {
      throw new Error('DATABASE_URL is required');
    }
    this.db = drizzle(databaseUrl);
  }

  async create(newClass: NewClass): Promise<Class> {
    try {
      const [result] = await this.db.insert(classes).values(newClass);
      const [created] = await this.db.select().from(classes).where(eq(classes.id, newClass.id)).limit(1);
      return created as Class;
    } catch (err) {
      throw new DatabaseError('CLASSES_DB_CREATE', 'Failed to create class', { error: String(err) });
    }
  }

  async findById(id: string): Promise<Class> {
    try {
      const [result] = await this.db.select().from(classes).where(eq(classes.id, id)).limit(1);
      if (!result) {
        throw new NotFoundError('CLASSES_NOT_FOUND', `Class ${id} not found`);
      }
      return result as Class;
    } catch (err) {
      if (err instanceof NotFoundError) throw err;
      throw new DatabaseError('CLASSES_DB_FIND', 'Failed to find class', { error: String(err) });
    }
  }

  async list(gradeId?: string, limit = 50): Promise<Class[]> {
    try {
      const query = this.db.select().from(classes).limit(limit);
      if (gradeId) {
        return await query.where(eq(classes.gradeId, gradeId)) as Class[];
      }
      return await query as Class[];
    } catch (err) {
      throw new DatabaseError('CLASSES_DB_LIST', 'Failed to list classes', { error: String(err) });
    }
  }

  async update(id: string, data: Partial<NewClass>): Promise<Class> {
    try {
      await this.db.update(classes).set(data).where(eq(classes.id, id));
      return await this.findById(id);
    } catch (err) {
      if (err instanceof NotFoundError) throw err;
      throw new DatabaseError('CLASSES_DB_UPDATE', 'Failed to update class', { error: String(err) });
    }
  }

  async delete(id: string): Promise<void> {
    try {
      await this.db.delete(classes).where(eq(classes.id, id));
    } catch (err) {
      throw new DatabaseError('CLASSES_DB_DELETE', 'Failed to delete class', { error: String(err) });
    }
  }
}
  • Step 4: 创建 classes.service.ts

Create e:\Desktop\Edu\services\classes\src\classes\classes.service.ts:

import { Injectable } from '@nestjs/common';
import { ClassesRepository } from './classes.repository.js';
import type { CreateClassDto, UpdateClassDto, ClassResponse } from './classes.dto.js';
import type { Class } from './classes.schema.js';
import { ValidationError } from '../shared/errors/application-error.js';

@Injectable()
export class ClassesService {
  constructor(private readonly repository: ClassesRepository) {}

  async create(dto: CreateClassDto): Promise<ClassResponse> {
    return this.toResponse(await this.repository.create(dto));
  }

  async findById(id: string): Promise<ClassResponse> {
    return this.toResponse(await this.repository.findById(id));
  }

  async list(gradeId?: string): Promise<ClassResponse[]> {
    const items = await this.repository.list(gradeId);
    return items.map((c) => this.toResponse(c));
  }

  async update(id: string, dto: UpdateClassDto): Promise<ClassResponse> {
    if (Object.keys(dto).length === 0) {
      throw new ValidationError('CLASSES_VAL_EMPTY_UPDATE', 'No fields to update');
    }
    return this.toResponse(await this.repository.update(id, dto));
  }

  async delete(id: string): Promise<void> {
    await this.repository.delete(id);
  }

  private toResponse(c: Class): ClassResponse {
    return {
      id: c.id,
      name: c.name,
      gradeId: c.gradeId,
      createdAt: c.createdAt.getTime(),
      updatedAt: c.updatedAt.getTime(),
    };
  }
}
  • Step 5: 创建 classes.controller.ts

Create e:\Desktop\Edu\services\classes\src\classes\classes.controller.ts:

import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Req, HttpCode, HttpStatus } from '@nestjs/common';
import { ClassesService } from './classes.service.js';
import { createClassSchema, updateClassSchema } from './classes.dto.js';
import { ValidationError } from '../shared/errors/application-error.js';
import { authMiddleware } from '../middleware/auth.middleware.js';
import type { Request } from 'express';

@Controller('/classes')
export class ClassesController {
  constructor(private readonly service: ClassesService) {}

  @Post()
  @UseGuards(authMiddleware())
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() body: unknown, @Req() req: Request): Promise<{ success: true; data: unknown }> {
    const result = createClassSchema.safeParse(body);
    if (!result.success) {
      throw new ValidationError('CLASSES_VAL_CREATE', 'Invalid input', { errors: result.error.flatten() });
    }
    const data = await this.service.create(result.data);
    return { success: true, data };
  }

  @Get()
  @UseGuards(authMiddleware())
  async list(@Query('gradeId') gradeId?: string): Promise<{ success: true; data: unknown[] }> {
    const data = await this.service.list(gradeId);
    return { success: true, data };
  }

  @Get(':id')
  @UseGuards(authMiddleware())
  async findById(@Param('id') id: string): Promise<{ success: true; data: unknown }> {
    const data = await this.service.findById(id);
    return { success: true, data };
  }

  @Put(':id')
  @UseGuards(authMiddleware())
  async update(@Param('id') id: string, @Body() body: unknown): Promise<{ success: true; data: unknown }> {
    const result = updateClassSchema.safeParse(body);
    if (!result.success) {
      throw new ValidationError('CLASSES_VAL_UPDATE', 'Invalid input', { errors: result.error.flatten() });
    }
    const data = await this.service.update(id, result.data);
    return { success: true, data };
  }

  @Delete(':id')
  @UseGuards(authMiddleware())
  @HttpCode(HttpStatus.NO_CONTENT)
  async delete(@Param('id') id: string): Promise<void> {
    await this.service.delete(id);
  }
}
  • Step 6: 创建 classes.module.ts

Create e:\Desktop\Edu\services\classes\src\classes\classes.module.ts:

import { Module } from '@nestjs/common';
import { ClassesController } from './classes.controller.js';
import { ClassesService } from './classes.service.js';
import { ClassesRepository } from './classes.repository.js';

@Module({
  controllers: [ClassesController],
  providers: [ClassesService, ClassesRepository],
  exports: [ClassesService],
})
export class ClassesModule {}
  • Step 7: 安装额外依赖并验证编译

Run:

cd e:\Desktop\Edu\services\classes
pnpm add @paralleldrive/cuid2 zod
pnpm exec tsc --noEmit

Expected: 编译通过。

  • Step 8: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "feat(classes): implement CRUD business logic with repository pattern"

Task 10: classes 黄金模板服务 - 测试套件

目标: 创建单元测试 + 集成测试 + 契约测试 + E2E 测试骨架。

Files:

  • Create: e:\Desktop\Edu\services\classes\vitest.config.ts

  • Create: e:\Desktop\Edu\services\classes\test\unit\classes.service.test.ts

  • Create: e:\Desktop\Edu\services\classes\test\integration\classes.repository.test.ts

  • Create: e:\Desktop\Edu\services\classes\test\contract\classes.contract.test.ts

  • Create: e:\Desktop\Edu\services\classes\test\e2e\classes.e2e.test.ts

  • Step 1: 创建 vitest.config.ts

Create e:\Desktop\Edu\services\classes\vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['test/unit/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'dist/', 'test/', '**/*.d.ts'],
      thresholds: {
        statements: 60,
        branches: 60,
        functions: 60,
        lines: 60,
      },
    },
  },
});
  • Step 2: 创建单元测试

Create e:\Desktop\Edu\services\classes\test\unit\classes.service.test.ts:

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ClassesService } from '../../src/classes/classes.service.js';
import { ValidationError } from '../../src/shared/errors/application-error.js';

describe('ClassesService', () => {
  let service: ClassesService;
  let mockRepository: any;

  beforeEach(() => {
    mockRepository = {
      create: vi.fn(),
      findById: vi.fn(),
      list: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    };
    service = new ClassesService(mockRepository);
  });

  describe('create', () => {
    it('should create a class successfully', async () => {
      const dto = { name: 'Class 1A', gradeId: 'grade-1' };
      const mockClass = {
        id: 'cls-1',
        name: 'Class 1A',
        gradeId: 'grade-1',
        createdAt: new Date('2026-01-01'),
        updatedAt: new Date('2026-01-01'),
      };
      mockRepository.create.mockResolvedValue(mockClass);

      const result = await service.create(dto);

      expect(result).toEqual({
        id: 'cls-1',
        name: 'Class 1A',
        gradeId: 'grade-1',
        createdAt: expect.any(Number),
        updatedAt: expect.any(Number),
      });
      expect(mockRepository.create).toHaveBeenCalledWith(dto);
    });
  });

  describe('update', () => {
    it('should throw ValidationError when no fields provided', async () => {
      await expect(service.update('cls-1', {})).rejects.toThrow(ValidationError);
    });

    it('should update class successfully', async () => {
      const mockClass = {
        id: 'cls-1',
        name: 'Updated',
        gradeId: 'grade-1',
        createdAt: new Date('2026-01-01'),
        updatedAt: new Date('2026-01-02'),
      };
      mockRepository.update.mockResolvedValue(mockClass);

      const result = await service.update('cls-1', { name: 'Updated' });

      expect(result.name).toBe('Updated');
      expect(mockRepository.update).toHaveBeenCalledWith('cls-1', { name: 'Updated' });
    });
  });

  describe('list', () => {
    it('should list classes without gradeId filter', async () => {
      const mockClasses = [
        { id: 'cls-1', name: 'A', gradeId: 'g1', createdAt: new Date(), updatedAt: new Date() },
      ];
      mockRepository.list.mockResolvedValue(mockClasses);

      const result = await service.list();

      expect(result).toHaveLength(1);
      expect(mockRepository.list).toHaveBeenCalledWith(undefined);
    });

    it('should list classes with gradeId filter', async () => {
      mockRepository.list.mockResolvedValue([]);

      await service.list('grade-1');

      expect(mockRepository.list).toHaveBeenCalledWith('grade-1');
    });
  });

  describe('delete', () => {
    it('should delete class', async () => {
      mockRepository.delete.mockResolvedValue(undefined);

      await service.delete('cls-1');

      expect(mockRepository.delete).toHaveBeenCalledWith('cls-1');
    });
  });
});
  • Step 3: 创建集成测试骨架Testcontainers

Create e:\Desktop\Edu\services\classes\test\integration\classes.repository.test.ts:

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
// P1 骨架:集成测试用 Testcontainers 起 MySQL
// 完整实现需安装 testcontainers 依赖

describe('ClassesRepository (integration)', () => {
  // TODO: 用 Testcontainers 启动 MySQL 容器
  // beforeAll: 启动容器 + 运行迁移
  // afterAll: 停止容器

  it('should be defined', () => {
    // 骨架测试
    expect(true).toBe(true);
  });

  it.skip('should create and find class', async () => {
    // 完整集成测试在 P1 后期补全
  });
});
  • Step 4: 创建契约测试骨架

Create e:\Desktop\Edu\services\classes\test\contract\classes.contract.test.ts:

import { describe, it, expect } from 'vitest';

// 契约测试:验证 classes.proto 定义的 API 符合消费者期望
// P3 起用 Pact 完整实现

describe('Classes API Contract', () => {
  it('should have CreateClass method', () => {
    // 读取 proto 文件验证
    expect(true).toBe(true);
  });

  it('should have ListClasses method', () => {
    expect(true).toBe(true);
  });

  it.skip('should validate consumer expectations (Pact)', () => {
    // P3 起用 Pact 实现消费端驱动契约测试
  });
});
  • Step 5: 创建 E2E 测试骨架

Create e:\Desktop\Edu\services\classes\test\e2e\classes.e2e.test.ts:

import { describe, it, expect } from 'vitest';

// E2E 测试:端到端验证 classes API
// P1 末尾用真实 MySQL 容器 + HTTP 请求验证

describe('Classes E2E', () => {
  const API_BASE = process.env.E2E_API_BASE ?? 'http://localhost:3001';

  it.skip('should create and retrieve class', async () => {
    // P1 末尾补全:
    // 1. POST /classes 创建班级
    // 2. GET /classes/:id 查询
    // 3. 验证返回数据一致
  });

  it.skip('should list classes', async () => {
    // P1 末尾补全
  });
});
  • Step 6: 运行测试

Run:

cd e:\Desktop\Edu\services\classes
pnpm test

Expected: 单元测试通过4 个 describe 全绿),集成/契约/E2E 骨架测试通过。

  • Step 7: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "test(classes): add unit tests and skeleton for integration/contract/e2e"

Task 11: teacher-portal 测试页

目标: 创建 Next.js 测试页,端到端验证 classes CRUD 链路。

Files:

  • Create: e:\Desktop\Edu\apps\teacher-portal\package.json

  • Create: e:\Desktop\Edu\apps\teacher-portal\next.config.js

  • Create: e:\Desktop\Edu\apps\teacher-portal\tsconfig.json

  • Create: e:\Desktop\Edu\apps\teacher-portal\src\app\page.tsx

  • Create: e:\Desktop\Edu\apps\teacher-portal\src\app\layout.tsx

  • Step 1: 创建 package.json

Create e:\Desktop\Edu\apps\teacher-portal\package.json:

{
  "name": "@next-edu/teacher-portal",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3000",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "^14.2.0",
    "react": "^18.3.0",
    "react-dom": "^18.3.0"
  },
  "devDependencies": {
    "@types/node": "^20.14.0",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0",
    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.0",
    "typescript": "^5.5.0"
  }
}
  • Step 2: 创建 next.config.js

Create e:\Desktop\Edu\apps\teacher-portal\next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  // P1 简化配置
  // P2 起配置 Module Federation
  env: {
    NEXT_PUBLIC_API_GATEWAY_URL: process.env.NEXT_PUBLIC_API_GATEWAY_URL ?? 'http://localhost:8080',
  },
};

module.exports = nextConfig;
  • Step 3: 创建 tsconfig.json

Create e:\Desktop\Edu\apps\teacher-portal\tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
  • Step 4: 创建 layout.tsx

Create e:\Desktop\Edu\apps\teacher-portal\src\app\layout.tsx:

import type { Metadata } from 'next';
import type { ReactNode } from 'react';

export const metadata: Metadata = {
  title: 'Teacher Portal - Next Edu Cloud',
  description: 'P1 测试页',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="zh-CN">
      <body style={{ fontFamily: 'system-ui, sans-serif', margin: 0, padding: '24px' }}>
        {children}
      </body>
    </html>
  );
}
  • Step 5: 创建 page.tsxclasses CRUD 测试页)

Create e:\Desktop\Edu\apps\teacher-portal\src\app\page.tsx:

'use client';

import { useState, useEffect, useCallback } from 'react';

interface ClassItem {
  id: string;
  name: string;
  gradeId: string;
  createdAt: number;
  updatedAt: number;
}

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: { code: string; message: string };
}

const API_BASE = process.env.NEXT_PUBLIC_API_GATEWAY_URL ?? 'http://localhost:8080';
// P1 测试用 JWT开发工具生成
const TEST_JWT = process.env.NEXT_PUBLIC_TEST_JWT ?? 'dev-token';

export default function HomePage() {
  const [classes, setClasses] = useState<ClassItem[]>([]);
  const [name, setName] = useState('');
  const [gradeId, setGradeId] = useState('grade-1');
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');

  const fetchClasses = useCallback(async () => {
    setLoading(true);
    try {
      const res = await fetch(`${API_BASE}/api/v1/classes`, {
        headers: { Authorization: `Bearer ${TEST_JWT}` },
      });
      const json: ApiResponse<ClassItem[]> = await res.json();
      if (json.success && json.data) {
        setClasses(json.data);
      } else {
        setMessage(`查询失败: ${json.error?.message ?? '未知错误'}`);
      }
    } catch (err) {
      setMessage(`网络错误: ${String(err)}`);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchClasses();
  }, [fetchClasses]);

  const handleCreate = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!name.trim()) return;
    setLoading(true);
    try {
      const res = await fetch(`${API_BASE}/api/v1/classes`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${TEST_JWT}`,
        },
        body: JSON.stringify({ name, gradeId }),
      });
      const json: ApiResponse<ClassItem> = await res.json();
      if (json.success) {
        setMessage(`创建成功: ${json.data?.name}`);
        setName('');
        await fetchClasses();
      } else {
        setMessage(`创建失败: ${json.error?.message}`);
      }
    } catch (err) {
      setMessage(`网络错误: ${String(err)}`);
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (id: string) => {
    setLoading(true);
    try {
      const res = await fetch(`${API_BASE}/api/v1/classes/${id}`, {
        method: 'DELETE',
        headers: { Authorization: `Bearer ${TEST_JWT}` },
      });
      if (res.ok) {
        setMessage('删除成功');
        await fetchClasses();
      }
    } catch (err) {
      setMessage(`网络错误: ${String(err)}`);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ maxWidth: '800px', margin: '0 auto' }}>
      <h1>Next Edu Cloud - P1 测试页</h1>
      <p>classes  CRUD 端到端验证</p>

      {message && (
        <div style={{ padding: '12px', background: '#f0f0f0', borderRadius: '4px', margin: '12px 0' }}>
          {message}
        </div>
      )}

      <form onSubmit={handleCreate} style={{ display: 'flex', gap: '8px', margin: '16px 0' }}>
        <input
          type="text"
          placeholder="班级名称"
          value={name}
          onChange={(e) => setName(e.target.value)}
          style={{ padding: '8px', flex: 1 }}
        />
        <input
          type="text"
          placeholder="年级 ID"
          value={gradeId}
          onChange={(e) => setGradeId(e.target.value)}
          style={{ padding: '8px', width: '120px' }}
        />
        <button type="submit" disabled={loading} style={{ padding: '8px 16px' }}>
          创建班级
        </button>
        <button type="button" onClick={fetchClasses} disabled={loading} style={{ padding: '8px 16px' }}>
          刷新
        </button>
      </form>

      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr style={{ background: '#f5f5f5', textAlign: 'left' }}>
            <th style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>ID</th>
            <th style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>名称</th>
            <th style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>年级</th>
            <th style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>操作</th>
          </tr>
        </thead>
        <tbody>
          {classes.map((c) => (
            <tr key={c.id}>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd', fontSize: '12px' }}>{c.id}</td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{c.name}</td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>{c.gradeId}</td>
              <td style={{ padding: '8px', borderBottom: '1px solid #ddd' }}>
                <button onClick={() => handleDelete(c.id)} disabled={loading}>
                  删除
                </button>
              </td>
            </tr>
          ))}
          {classes.length === 0 && !loading && (
            <tr>
              <td colSpan={4} style={{ padding: '16px', textAlign: 'center', color: '#999' }}>
                暂无班级
              </td>
            </tr>
          )}
        </tbody>
      </table>

      <div style={{ marginTop: '32px', fontSize: '12px', color: '#999' }}>
        <p>API Gateway: {API_BASE}</p>
        <p>链路: 浏览器  teacher-portal  API Gateway  classes service  MySQL</p>
      </div>
    </div>
  );
}
  • Step 6: 安装依赖并验证编译

Run:

cd e:\Desktop\Edu
pnpm install
cd apps/teacher-portal
pnpm exec tsc --noEmit

Expected: 编译通过。

  • Step 7: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "feat(teacher-portal): add P1 test page for classes CRUD verification"

Task 12: CI/CD 骨架

目标: 创建三语言 CI workflow。

Files:

  • Create: e:\Desktop\Edu\.github\workflows\ci-go.yml

  • Create: e:\Desktop\Edu\.github\workflows\ci-ts.yml

  • Create: e:\Desktop\Edu\.github\workflows\ci-py.yml

  • Step 1: 创建 ci-go.yml

Create e:\Desktop\Edu\.github\workflows\ci-go.yml:

name: CI Go

on:
  push:
    branches: [main]
    paths:
      - 'services/api-gateway/**'
      - 'services/push-gateway/**'
      - '.github/workflows/ci-go.yml'
  pull_request:
    paths:
      - 'services/api-gateway/**'
      - 'services/push-gateway/**'

jobs:
  lint-test-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Lint
        uses: golangci/golangci-lint-action@v6
        with:
          working-directory: services/api-gateway

      - name: Build
        working-directory: services/api-gateway
        run: |
          go mod download
          go build ./...
          go vet ./...

      - name: Test
        working-directory: services/api-gateway
        run: go test ./... -v -coverprofile=coverage.out
  • Step 2: 创建 ci-ts.yml

Create e:\Desktop\Edu\.github\workflows\ci-ts.yml:

name: CI TypeScript

on:
  push:
    branches: [main]
    paths:
      - 'services/classes/**'
      - 'bff/**'
      - 'apps/**'
      - 'packages/**'
      - 'scripts/**'
      - '.github/workflows/ci-ts.yml'
  pull_request:
    paths:
      - 'services/classes/**'
      - 'bff/**'
      - 'apps/**'
      - 'packages/**'
      - 'scripts/**'

jobs:
  lint-test-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm -r run lint

      - name: Type check
        run: pnpm -r exec tsc --noEmit

      - name: Test
        run: pnpm -r run test

      - name: Build
        run: pnpm -r run build

      - name: Arch scan
        run: npm run arch:scan
  • Step 3: 创建 ci-py.yml

Create e:\Desktop\Edu\.github\workflows\ci-py.yml:

name: CI Python

on:
  push:
    branches: [main]
    paths:
      - 'services/data-ana/**'
      - 'services/ai-gateway/**'
      - '.github/workflows/ci-py.yml'
  pull_request:
    paths:
      - 'services/data-ana/**'
      - 'services/ai-gateway/**'

jobs:
  lint-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install uv
        run: pip install uv

      - name: Lint
        run: uvx ruff check .

      - name: Test
        run: uvx pytest
  • Step 4: 提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "ci: add Go/TypeScript/Python CI workflows"

Task 13: P1 文档完善与验收

目标: 创建模块 README运行 arch:scan端到端验证 P1 退出标准。

Files:

  • Create: e:\Desktop\Edu\docs\modules\api-gateway\README.md

  • Create: e:\Desktop\Edu\docs\modules\classes\README.md

  • Step 1: 创建 api-gateway README

Create e:\Desktop\Edu\docs\modules\api-gateway\README.md:

# API Gateway

## 1. 模块职责

API 网关:路由转发 + JWT 鉴权 + 请求 ID 注入。所有外部请求的统一入口。

## 2. 技术栈

| 技术 | 版本 |
|------|------|
| Go | 1.22 |
| Gin | 1.10 |
| golang-jwt | v5 |

## 3. 架构图

```mermaid
graph TB
    Client[客户端] --> GW[API Gateway]
    GW --> Auth[Auth 中间件 JWT HS256]
    Auth --> Router[路由 /api/v1/*]
    Router --> Proxy[反向代理]
    Proxy --> Classes[classes 服务]

4. 核心流程图

sequenceDiagram
    Client->>GW: GET /api/v1/classes
    GW->>GW: 验证 JWT
    GW->>GW: 注入 x-user-id
    GW->>Classes: 转发请求
    Classes-->>GW: 响应
    GW-->>Client: 响应

5. 对外契约

  • 路由: /api/v1/classes/* → classes 服务
  • P2 起增加: /api/v1/iam/*/api/v1/exams/*
  • 鉴权: JWT HS256P1/ RS256P2 起)

6. 依赖关系

  • 下游服务: classes 服务P1、IAMP2、CoreEduP3
  • 无上游依赖

7. 架构约束

  • 仅路由转发,不含业务逻辑
  • P1 用 HS256P2 改 RS256IAM 签发)
  • 不直接访问数据库

8. 架构决策

  • P1 不做限流/熔断P6 硬化)
  • 请求 ID 由 Gateway 注入,全链路传递

- [ ] **Step 2: 创建 classes README**

Create `e:\Desktop\Edu\docs\modules\classes\README.md`:
```markdown
# Classes 服务(黄金模板)

## 1. 模块职责

班级域 CRUD 服务。P1 作为黄金模板,完整实现所有横切关注点,后续服务复制此模板。

## 2. 技术栈

| 技术 | 版本 |
|------|------|
| Node.js | 20 |
| NestJS | 10 |
| Drizzle ORM | 0.31 |
| MySQL | 8.0 |
| pino | 9 |
| OpenTelemetry | 0.53 |
| prom-client | 15 |
| vitest | 2 |

## 3. 架构图

```mermaid
graph TB
    GW[API Gateway] --> Ctrl[ClassesController]
    Ctrl --> AuthMW[Auth Middleware]
    AuthMW --> Svc[ClassesService]
    Svc --> Repo[ClassesRepository]
    Repo --> DB[(MySQL classes 表)]
    Ctrl --> ErrFilter[Global Error Filter]
    Obs[Observability] --> Logger[pino Logger]
    Obs --> Metrics[Prometheus /metrics]
    Obs --> Tracer[OTel Tracer]

4. 核心流程图

sequenceDiagram
    participant GW as API Gateway
    participant Ctrl as Controller
    participant Svc as Service
    participant Repo as Repository
    participant DB as MySQL

    GW->>Ctrl: POST /classes
    Ctrl->>Ctrl: Zod 验证
    Ctrl->>Svc: create(dto)
    Svc->>Repo: create(newClass)
    Repo->>DB: INSERT
    DB-->>Repo: 结果
    Repo-->>Svc: Class
    Svc-->>Ctrl: ClassResponse
    Ctrl-->>GW: { success: true, data }

5. 对外契约

  • REST API: POST/GET/PUT/DELETE /classes
  • proto: packages/shared-proto/proto/classes.protoP3 起转 gRPC
  • 响应信封: { success: boolean, data?: T, error?: { code, message, details, traceId } }

6. 依赖关系

  • 上游: API Gateway
  • 下游: MySQLclasses 表)
  • 中间件: auth / permission / data-scope
  • 共享: shared/errors、shared/observability、shared/i18n

7. 架构约束

  • 黄金模板:所有横切关注点必须落地
  • 单文件行数 ≤ 500组件/ 800service/repository
  • 错误处理用 ApplicationError 体系
  • 日志含 traceId/userId/service
  • 权限校验用 requirePermission()

8. 架构决策(黄金模板复用清单)

后续服务复制时需修改:

  1. 错误码前缀 CLASSES_* → 新服务前缀
  2. proto 文件
  3. 业务逻辑controller/service/repository/schema
  4. README 模块名与架构图
  5. CI workflow 服务名
  6. Dockerfile 入口(如需要)

无需修改(直接复用):

  • shared/errors/application-error.ts
  • shared/observability/*
  • shared/i18n/error-codes.ts 结构
  • middleware/* 结构
  • config/* 结构
  • test/ 目录结构

- [ ] **Step 3: 运行 arch:scan 验证**

Run:
```bash
cd e:\Desktop\Edu
npm run arch:scan
npm run arch:query -- stats
npm run arch:query -- violations

Expected:

  • stats 输出模块/符号/依赖计数 > 0

  • violations 输出为空0 违规)

  • Step 4: 启动服务端到端验证

Run:

# 终端 1: 启动基础设施
cd e:\Desktop\Edu
docker compose -f infra/docker-compose.minimal.yml up -d

# 终端 2: 启动 classes 服务
cd e:\Desktop\Edu\services\classes
pnpm dev

# 终端 3: 启动 API Gateway
cd e:\Desktop\Edu\services\api-gateway
go run main.go

# 终端 4: 启动 teacher-portal
cd e:\Desktop\Edu\apps\teacher-portal
pnpm dev

打开浏览器访问 http://localhost:3000,创建班级、刷新列表、删除班级,验证全链路。

  • Step 5: 验证 P1 退出标准

Run:

cd e:\Desktop\Edu
# 验证契约
cd packages/shared-proto && pnpm exec buf lint && cd ../..

# 验证编译
pnpm -r exec tsc --noEmit
cd services/api-gateway && go build ./... && cd ../..

# 验证测试
pnpm -r run test

# 验证 arch.db
npm run arch:scan
npm run arch:query -- violations

确认全部通过后继续。

  • Step 6: 更新 known-issues 工作日志

Edit e:\Desktop\Edu\docs\troubleshooting\known-issues.md,在工作经验日志表格末尾追加:

| 2026-07-07 | 全局 | P1 完成:新仓库 + monorepo + Docker + API Gateway + classes 黄金模板 + 文档体系 + CI。验证 classes CRUD 端到端链路通畅。 |
  • Step 7: 打 tag 并提交

Run:

cd e:\Desktop\Edu
git add -A
git commit -m "docs: add module READMEs and complete P1 documentation"
git tag v0.1.0-p1
git push origin main
git push origin v0.1.0-p1
# 若推送失败,再次推送:
git push origin main && git push origin v0.1.0-p1
  • Step 8: P1 验收检查清单

逐项确认 P1 退出标准:

  • 基础设施:docker-compose up -d 全量容器健康
  • 链路:浏览器创建班级 → MySQL 落库 → 列表显示
  • 横切关注点:错误处理 + 可观测 + 安全 + 契约 + 测试 + 文档 + 配置 + i18n + CI + Dockerfile 全部落地
  • 契约:buf lint + buf breaking 通过,三端代码生成成功
  • CI三语言 workflow 配置就绪
  • 测试:单元 + 集成 + 契约 + E2E 四类测试齐备,覆盖率达标
  • 文档004 + 模块 README + known-issues 完整

验收完成

P1 地基阶段全部任务完成后,仓库应包含:

  • 多语言 monorepo 骨架pnpm + go.work + pyproject
  • Docker Compose 全量与最小基础设施配置
  • API GatewayGo路由转发 + JWT 校验
  • classes 黄金模板服务TS/NestJS含全横切关注点
  • shared-proto 契约包 + buf 配置
  • arch.db 多语言扫描器骨架
  • teacher-portal 测试页
  • 三语言 CI workflow
  • 完整文档体系004 + 模块 README + known-issues + project_rules + coding-standards + git-workflow

打 tag v0.1.0-p1,进入 P2 身份阶段。