Compare commits
60 Commits
v0.2.0-p2
...
0a71b02e04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a71b02e04 | ||
|
|
b53a486c6e | ||
|
|
a0f6c228af | ||
|
|
834e2c61fd | ||
|
|
f1e466a772 | ||
|
|
b72c8d81d4 | ||
|
|
959d58a95d | ||
|
|
d8dab70406 | ||
|
|
1f901c5b20 | ||
|
|
958b17c9d8 | ||
|
|
566060fade | ||
|
|
3ca654619f | ||
|
|
a70a74207e | ||
|
|
dfb6d2bfc1 | ||
|
|
416e1bc0b2 | ||
|
|
421edd8a41 | ||
|
|
5f18821302 | ||
|
|
921fe82771 | ||
|
|
033c083619 | ||
|
|
6215f4e21f | ||
|
|
beb204c00f | ||
|
|
4f539b50dd | ||
|
|
4533da6484 | ||
|
|
2c7afe59ef | ||
|
|
b2c2f6e567 | ||
|
|
a2f0ca26ae | ||
|
|
5759b09c9f | ||
|
|
d92cdda727 | ||
|
|
adea22b133 | ||
|
|
f658571726 | ||
|
|
68ddff1065 | ||
|
|
a1d7fcfd71 | ||
|
|
c31b3ddeba | ||
|
|
ae460e61a3 | ||
|
|
a3f00c882a | ||
|
|
d19285a977 | ||
|
|
3b88e9cca5 | ||
|
|
4a0893ef52 | ||
|
|
e5902ca2b3 | ||
|
|
a4ec5b72c5 | ||
|
|
2df2237d56 | ||
|
|
6f68f9722e | ||
|
|
ff0ae1ac9c | ||
|
|
b39095cbdd | ||
|
|
beedbaf686 | ||
|
|
20b1afd9ab | ||
|
|
4fea3de1d1 | ||
|
|
3961a36308 | ||
|
|
a4cd970c54 | ||
|
|
75804c7d64 | ||
|
|
d831915f06 | ||
|
|
9b33303195 | ||
|
|
bc3176cb05 | ||
|
|
3fbb97791d | ||
|
|
4629de1926 | ||
|
|
f554011af5 | ||
|
|
e9ea34fe53 | ||
|
|
7474a92e3b | ||
|
|
9850bfcfd1 | ||
|
|
23246ade6d |
@@ -12,6 +12,12 @@ JWT_SECRET=p1-dev-secret-change-in-production
|
||||
JWT_ISSUER=next-edu-cloud
|
||||
JWT_AUDIENCE=next-edu-cloud
|
||||
|
||||
# 开发模式旁路(仅本地联调)
|
||||
# DEV_MODE=true 时接受 "Authorization: Bearer dev-token" 旁路 JWT 校验,
|
||||
# 注入固定身份 x-user-id=dev-user, x-user-roles=teacher,admin
|
||||
# 生产环境必须设为 false 或不设此变量
|
||||
DEV_MODE=false
|
||||
|
||||
# 服务端口
|
||||
API_GATEWAY_PORT=8080
|
||||
CLASSES_SERVICE_PORT=3001
|
||||
|
||||
80
.github/CODEOWNERS
vendored
Normal file
80
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
# Edu 平台 CODEOWNERS
|
||||
# 格式:路径 @owner
|
||||
# 作用:GitHub/Gitea 自动为 PR 分配 reviewer
|
||||
# 维护规则:
|
||||
# 1. 新增服务/包时,必须同步更新本文件
|
||||
# 2. owner 变更需开独立 PR,由架构组审批
|
||||
# 3. 与 docs/standards/git-workflow.md §4.7 保持同步
|
||||
# Team handle 占位符:@edu-platform/* 需在 Organization 中创建对应 team 后替换
|
||||
|
||||
# ===== 架构与规则(架构组,2 人 review)=====
|
||||
/.trae/ @edu-platform/arch
|
||||
/docs/architecture/ @edu-platform/arch
|
||||
/docs/standards/ @edu-platform/arch
|
||||
/scripts/arch-scan/ @edu-platform/arch
|
||||
|
||||
# ===== 根配置(架构组,2 人 review,影响全局)=====
|
||||
/.commitlintrc.js @edu-platform/arch
|
||||
/lint-staged.config.js @edu-platform/arch
|
||||
/package.json @edu-platform/arch
|
||||
/pnpm-workspace.yaml @edu-platform/arch
|
||||
/go.work @edu-platform/arch
|
||||
/pyproject.toml @edu-platform/arch
|
||||
/tsconfig.base.json @edu-platform/arch
|
||||
|
||||
# ===== 共享包(架构组,2 人 review,契约变更影响所有服务)=====
|
||||
/packages/shared-proto/ @edu-platform/arch
|
||||
/packages/shared-ts/ @edu-platform/arch
|
||||
/packages/shared-go/ @edu-platform/arch
|
||||
/packages/shared-py/ @edu-platform/arch
|
||||
/packages/shared-tokens/ @edu-platform/arch
|
||||
|
||||
# ===== 网关层(Go)=====
|
||||
/services/api-gateway/ @edu-platform/gateway
|
||||
/services/push-gateway/ @edu-platform/gateway
|
||||
|
||||
# ===== 业务微服务 =====
|
||||
# IAM(核心模块,2 人 review)
|
||||
/services/iam/ @edu-platform/iam
|
||||
# 教学核心
|
||||
/services/core-edu/ @edu-platform/edu-core
|
||||
/services/classes/ @edu-platform/edu-core
|
||||
# 内容资源
|
||||
/services/content/ @edu-platform/content
|
||||
# 消息通知
|
||||
/services/msg/ @edu-platform/messaging
|
||||
# 数据分析(Python)
|
||||
/services/data-ana/ @edu-platform/data
|
||||
# AI 网关(Python)
|
||||
/services/ai/ @edu-platform/ai
|
||||
|
||||
# ===== BFF 聚合层(NestJS)=====
|
||||
/services/teacher-bff/ @edu-platform/edu-core
|
||||
/services/student-bff/ @edu-platform/edu-core
|
||||
/services/parent-bff/ @edu-platform/edu-core
|
||||
|
||||
# ===== 微前端(Next.js)=====
|
||||
/apps/teacher-portal/ @edu-platform/frontend
|
||||
/apps/student-portal/ @edu-platform/frontend
|
||||
/apps/parent-portal/ @edu-platform/frontend
|
||||
/apps/admin-portal/ @edu-platform/frontend
|
||||
|
||||
# ===== 基础设施(SRE,2 人 review,生产环境变更强制)=====
|
||||
/infra/k8s/ @edu-platform/sre
|
||||
/infra/backup/ @edu-platform/sre
|
||||
/infra/security/ @edu-platform/sre
|
||||
/infra/monitoring/ @edu-platform/sre
|
||||
/infra/docker-compose*.yml @edu-platform/sre
|
||||
|
||||
# ===== CI/CD(SRE)=====
|
||||
/.github/ @edu-platform/sre
|
||||
/.husky/ @edu-platform/sre
|
||||
|
||||
# ===== 文档(架构组)=====
|
||||
/docs/troubleshooting/ @edu-platform/arch
|
||||
/MIGRATION_GUIDE.md @edu-platform/arch
|
||||
/README.md @edu-platform/arch
|
||||
/CHANGELOG.md @edu-platform/arch
|
||||
|
||||
# ===== 兜底(未匹配的文件由架构组 review)=====
|
||||
* @edu-platform/arch
|
||||
55
.github/pull_request_template.md
vendored
Normal file
55
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
## 变更说明
|
||||
|
||||
<!-- 简述本次变更的目的和实现方式 -->
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] feat: 新功能
|
||||
- [ ] fix: Bug 修复
|
||||
- [ ] perf: 性能优化
|
||||
- [ ] refactor: 重构
|
||||
- [ ] test: 测试
|
||||
- [ ] docs: 文档
|
||||
- [ ] build/ci: 构建/CI
|
||||
- [ ] chore: 杂项
|
||||
|
||||
## 影响范围
|
||||
|
||||
<!-- 列出受影响的服务/包 -->
|
||||
|
||||
- 服务:
|
||||
- 包:
|
||||
- 数据库迁移:是 / 否
|
||||
- protobuf 契约变更:是 / 否(如变更是否向后兼容)
|
||||
- Kafka topic 变更:是 / 否
|
||||
- 架构文档变更:是 / 否
|
||||
|
||||
## 测试情况
|
||||
|
||||
- [ ] 单元测试通过
|
||||
- [ ] 集成测试通过
|
||||
- [ ] 本地手动测试通过
|
||||
- [ ] 新增测试覆盖新功能
|
||||
|
||||
### 按语言的校验结果
|
||||
|
||||
- TS 服务:`pnpm run lint` + `pnpm run typecheck`(如缺失脚本请说明)
|
||||
- Go 服务:`go vet ./...` + `go build ./...`
|
||||
- Python 服务:`ruff check src/`
|
||||
|
||||
## 文档同步
|
||||
|
||||
- [ ] 已更新服务 README(如涉及服务结构变更)
|
||||
- [ ] 已更新架构文档(如涉及架构变更)
|
||||
- [ ] 已运行 `pnpm run arch:scan` 更新 arch.db(arch.db 不入库,但本地验证必须通过)
|
||||
- [ ] 已更新 `docs/troubleshooting/known-issues.md`(如遇到新问题/经验)
|
||||
- [ ] 已更新 `.github/CODEOWNERS`(如新增服务/包)
|
||||
|
||||
## Breaking Change
|
||||
|
||||
- [ ] 否
|
||||
- [ ] 是(请在下方说明影响和迁移路径)
|
||||
|
||||
## 关联 Issue
|
||||
|
||||
Closes #
|
||||
38
.github/workflows/ci-go.yml
vendored
38
.github/workflows/ci-go.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: CI Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/api-gateway/**'
|
||||
- 'services/push-gateway/**'
|
||||
- 'packages/shared-go/**'
|
||||
- 'go.work'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/api-gateway/**'
|
||||
- 'services/push-gateway/**'
|
||||
- 'packages/shared-go/**'
|
||||
- 'go.work'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: golangci-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 ./...
|
||||
- name: Test
|
||||
working-directory: services/api-gateway
|
||||
run: go test ./... -v -coverprofile=coverage.out
|
||||
25
.github/workflows/ci-proto.yml
vendored
25
.github/workflows/ci-proto.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: CI Proto
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/shared-proto/**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/shared-proto/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
- name: buf lint
|
||||
working-directory: packages/shared-proto
|
||||
run: buf lint
|
||||
- name: buf breaking
|
||||
if: github.event_name == 'pull_request'
|
||||
working-directory: packages/shared-proto
|
||||
run: buf breaking --against https://github.com/${{ github.repository }}.git#branch=main,subdir=packages/shared-proto
|
||||
33
.github/workflows/ci-py.yml
vendored
33
.github/workflows/ci-py.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: CI Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/data-ana/**'
|
||||
- 'services/ai/**'
|
||||
- 'packages/shared-py/**'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/data-ana/**'
|
||||
- 'services/ai/**'
|
||||
- 'packages/shared-py/**'
|
||||
- 'pyproject.toml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: astral-sh/setup-uv@v3
|
||||
- name: Install
|
||||
run: uv sync
|
||||
- name: Lint (ruff)
|
||||
run: uv run ruff check .
|
||||
- name: Test
|
||||
run: uv run pytest
|
||||
43
.github/workflows/ci-ts.yml
vendored
43
.github/workflows/ci-ts.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: CI TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/classes/**'
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'tsconfig.base.json'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/classes/**'
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
- name: Install
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Lint
|
||||
run: pnpm -r run lint
|
||||
- name: Typecheck
|
||||
run: pnpm -r run typecheck
|
||||
- name: Test
|
||||
run: pnpm -r run test
|
||||
- name: Build
|
||||
run: pnpm -r run build
|
||||
196
.github/workflows/ci.yml
vendored
Normal file
196
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
name: CI
|
||||
|
||||
# CI/CD 一体化流水线(no-push 本地构建部署)
|
||||
# 设计文档:docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md
|
||||
#
|
||||
# 流程:
|
||||
# PR 触发:仅跑 3 个 quality job(并行)
|
||||
# push main 触发:quality job 全绿后跑 deploy job(本地 build + compose up)
|
||||
# workflow_dispatch:支持指定 commit_sha 回滚
|
||||
#
|
||||
# 不依赖任何自建镜像,全部使用官方镜像
|
||||
# 不推送镜像到 registry,构建即部署
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit_sha:
|
||||
description: '回滚到指定 commit(留空则部署当前 HEAD)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: '1'
|
||||
NEXT_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
# ===== TypeScript 质量检查 =====
|
||||
quality-ts:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22-alpine
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha || github.ref }}
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm -r run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm -r run typecheck
|
||||
|
||||
- name: Test
|
||||
run: pnpm -r run test
|
||||
continue-on-error: true # P6: 部分服务无 test 脚本,待补全
|
||||
|
||||
- name: Build
|
||||
run: pnpm -r run build
|
||||
|
||||
# ===== Go 质量检查 =====
|
||||
quality-go:
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:1.22-alpine
|
||||
defaults:
|
||||
run:
|
||||
working-directory: services/api-gateway
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha || github.ref }}
|
||||
|
||||
- name: Download deps
|
||||
run: go mod download
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
||||
- name: Test
|
||||
run: go test ./... -v -coverprofile=coverage.out
|
||||
|
||||
# ===== Proto 契约检查 =====
|
||||
quality-proto:
|
||||
runs-on: ubuntu-latest
|
||||
container: bufbuild/buf:latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/shared-proto
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha || github.ref }}
|
||||
fetch-depth: 0 # buf breaking 需要对比分支
|
||||
|
||||
- name: buf lint
|
||||
run: buf lint
|
||||
|
||||
- name: buf breaking
|
||||
if: github.event_name == 'pull_request'
|
||||
run: buf breaking --against .git#branch=main,subdir=packages/shared-proto
|
||||
|
||||
# ===== 部署(仅 push main 或手动触发)=====
|
||||
deploy:
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
needs: [quality-ts, quality-go, quality-proto]
|
||||
runs-on: ubuntu-latest
|
||||
container: docker:25-git
|
||||
options: --volume /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
name: production
|
||||
env:
|
||||
DEPLOY_DIR: /opt/edu
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha || github.ref }}
|
||||
|
||||
- name: Setup deploy dir
|
||||
run: |
|
||||
# 同步整个仓库到部署目录的 repo/ 子目录
|
||||
# compose 文件中 build.context 指向 ./repo/services/... 或 ./repo
|
||||
mkdir -p ${{ env.DEPLOY_DIR }}/repo
|
||||
cp -a $GITHUB_WORKSPACE/. ${{ env.DEPLOY_DIR }}/repo/
|
||||
cp infra/docker-compose.deploy.yml ${{ env.DEPLOY_DIR }}/docker-compose.yml
|
||||
|
||||
# 确保 .env 存在(首次部署需人工创建)
|
||||
if [ ! -f ${{ env.DEPLOY_DIR }}/.env ]; then
|
||||
echo "::error::部署目录缺少 .env 文件,请参考 infra/deploy.env.example 创建"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Deploy services (local build, no push)
|
||||
run: |
|
||||
cd ${{ env.DEPLOY_DIR }}
|
||||
echo "=== 构建并启动服务 ==="
|
||||
# --build 使用本地 Dockerfile 构建,不拉取 registry
|
||||
# build.context 在 compose 中指向 ./repo/...(相对于 /opt/edu/)
|
||||
docker compose up -d --build --remove-orphans
|
||||
|
||||
- name: Wait for health
|
||||
run: |
|
||||
echo "等待服务健康..."
|
||||
sleep 10
|
||||
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
FAIL=0
|
||||
|
||||
echo "[尝试 $i] 检查 api-gateway..."
|
||||
if ! wget -q --spider http://localhost:8080/healthz 2>/dev/null; then
|
||||
echo " api-gateway 未就绪"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
echo "[尝试 $i] 检查 classes..."
|
||||
if ! wget -q --spider http://localhost:3001/healthz 2>/dev/null; then
|
||||
echo " classes 未就绪"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
echo "[尝试 $i] 检查 teacher-portal..."
|
||||
if ! wget -q --spider http://localhost:3000/ 2>/dev/null; then
|
||||
echo " teacher-portal 未就绪"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "✅ 所有服务健康"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
sleep 6
|
||||
done
|
||||
|
||||
echo "::error::服务健康检查失败"
|
||||
echo "=== 容器状态 ==="
|
||||
cd ${{ env.DEPLOY_DIR }} && docker compose ps
|
||||
echo "=== api-gateway 日志 ==="
|
||||
docker compose logs --tail=50 api-gateway
|
||||
echo "=== classes 日志 ==="
|
||||
docker compose logs --tail=50 classes
|
||||
echo "=== teacher-portal 日志 ==="
|
||||
docker compose logs --tail=50 teacher-portal
|
||||
exit 1
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### 部署结果" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 触发事件:\`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 部署 commit:\`${{ github.event.inputs.commit_sha || github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 部署目录:\`${{ env.DEPLOY_DIR }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 部署方式:本地构建(no-push)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
cd ${{ env.DEPLOY_DIR }} && docker compose ps >> $GITHUB_STEP_SUMMARY 2>&1 || true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,6 +59,7 @@ coverage/
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
docker-compose.minimal.override.yml
|
||||
|
||||
# Temp
|
||||
tmp/
|
||||
|
||||
@@ -1 +1 @@
|
||||
npx --no-install commitlint --edit $1
|
||||
npx --no-install commitlint --edit $1
|
||||
|
||||
@@ -1 +1 @@
|
||||
npx --no-install lint-staged
|
||||
npx --no-install lint-staged
|
||||
|
||||
532
.trae/rules/project_rules.md
Normal file
532
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,532 @@
|
||||
# Edu 项目规则(微服务版)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-08
|
||||
> 适用范围:Edu 微服务架构(DDD + EDA + CQRS,多语言 monorepo)
|
||||
> 关联文档:
|
||||
>
|
||||
> - [004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md)
|
||||
> - [理想蓝图 0010](../../docs/architecture/0010_architecture.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [known-issues 速查](../../docs/troubleshooting/known-issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构图优先规则
|
||||
|
||||
**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**
|
||||
|
||||
1. **先图后码**:执行任何分析、修改、搜索任务时,首先运行 `pnpm run arch:scan` 更新 arch.db,再通过 `pnpm run arch:query` 查询目标模块、函数、依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图,最后按图索骥读取源码
|
||||
2. **图未覆盖则先补图**:如果发现项目中存在 arch.db 未记录的模块、函数、表、路由、proto 等结构,**必须先运行 `pnpm run arch:scan` 重新扫描**(多语言扫描器:TS + Go + Python + Proto),然后检查 004 是否需要补充
|
||||
3. **改码必同步图**:对源码的任何修改完成后,必须运行 `pnpm run arch:scan` 更新 arch.db;若架构设计意图有变化,同步更新 004
|
||||
|
||||
### 架构文档清单
|
||||
|
||||
| 文档 | 用途 |
|
||||
| -------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源(人类可读) |
|
||||
| `docs/architecture/0010_architecture.md` | 理想蓝图(目标态) |
|
||||
| `docs/architecture/roadmap/` | 长远规划(tech-debt / pending-features) |
|
||||
| `docs/architecture/runbooks/` | 运维手册(P6 硬化、post-p6-followup、incident-response) |
|
||||
| `docs/troubleshooting/known-issues.md` | 已知问题速查(场景→技术映射 + 工作经验日志) |
|
||||
|
||||
### 需要同步图的场景
|
||||
|
||||
- 新增/删除/重命名导出函数、组件、Hook、类型
|
||||
- 修改函数签名(参数、返回类型)
|
||||
- 修改权限点(Permissions 常量)或角色-权限映射
|
||||
- 新增/删除数据库表
|
||||
- 新增/删除路由页面、API 路由、gRPC 服务、proto message
|
||||
- 修改模块间依赖关系或服务间调用关系
|
||||
- 新增服务、新增模块、新增 proto 定义
|
||||
- 修改 Kafka topic、Outbox 事件命名
|
||||
|
||||
### 同步方式
|
||||
|
||||
- 修改源码后运行 `pnpm run arch:scan` 更新 arch.db(强制)
|
||||
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
|
||||
- 若发现新的"场景→技术"映射或工作经验,更新 `docs/troubleshooting/known-issues.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构元数据库规则(arch.db)
|
||||
|
||||
**arch.db 是代码结构唯一源,AI 工作前必须运行 `pnpm run arch:scan` 更新。**
|
||||
|
||||
1. **多语言扫描**:arch.db 覆盖 TypeScript(ts-morph)、Go(tree-sitter-go)、Python(tree-sitter-python)、Proto(buf AST)四类语言
|
||||
2. **查询命令**:
|
||||
- `pnpm run arch:query -- sql "<SQL>"` 自定义 SQL 查询
|
||||
- `pnpm run arch:query -- module-deps` 查模块依赖
|
||||
- `pnpm run arch:query -- module-reverse-deps <module>` 查反向依赖
|
||||
- `pnpm run arch:query -- symbol-refs <symbol>` 查符号引用链
|
||||
- `pnpm run arch:query -- tech-usage <tag>` 查技术使用
|
||||
- `pnpm run arch:query -- violations` 查架构违规
|
||||
- `pnpm run arch:query -- stats` 查总体统计
|
||||
- `pnpm run arch:query -- modules` 查模块列表
|
||||
3. **arch.db 不替代 004**:arch.db 是"代码现状",004 是"设计意图",两者互补
|
||||
4. **预期规模**:模块数 ≥ 10(api-gateway、push-gateway、teacher-bff、iam、classes、core-edu、content、msg、data-ana、ai),符号数 ≥ 100
|
||||
|
||||
---
|
||||
|
||||
## 3. 编码规范
|
||||
|
||||
详细规范见 `docs/standards/coding-standards.md`,以下为核心强制规则。
|
||||
|
||||
### 3.1 代码质量规则
|
||||
|
||||
- 每次修改后运行 `pnpm run lint` 和 `pnpm run typecheck` 确保零错误
|
||||
- TypeScript 服务必须用 `@RequirePermission()` 装饰器进行权限校验
|
||||
- 前端组件禁止使用 `role === "xxx"` 硬编码,统一使用 `usePermission().hasPermission()`
|
||||
- 单文件行数遵循企业级规范:
|
||||
- 配置文件、常量文件、类型定义文件、proto:无限制
|
||||
- Controller / Service:建议 ≤ 500 行
|
||||
- Repository / data-access:建议 ≤ 800 行
|
||||
- 工具函数:建议 ≤ 40 行
|
||||
- 自定义 Hook:建议 ≤ 80 行
|
||||
- 硬性上限:任何文件不超过 1000 行,超过必须拆分
|
||||
|
||||
### 3.2 架构分层规则(微服务)
|
||||
|
||||
- **四层分层**:Gateway → BFF → Services → Data
|
||||
- **依赖方向单向**:Gateway → BFF → Services → Data,禁止反向依赖
|
||||
- **服务间通信**:通过 protobuf gRPC 或 Kafka 事件,**禁止直接访问对方 DB**
|
||||
- **Gateway 职责**:JWT 校验、限流、熔断、CORS、请求 ID 注入
|
||||
- **BFF 职责**:聚合多个 Service 的数据,为前端提供场景化 API
|
||||
- **Service 职责**:业务逻辑,每服务独占 DB(DDD 限界上下文)
|
||||
|
||||
### 3.3 模块标准结构(DDD)
|
||||
|
||||
```
|
||||
services/[service]/src/
|
||||
├─ [domain]/ # 限界上下文
|
||||
│ ├─ *.controller.ts # Controller(HTTP/gRPC 入口)
|
||||
│ ├─ *.service.ts # Application Service(编排层)
|
||||
│ ├─ *.repository.ts # Repository(数据访问)
|
||||
│ ├─ *.schema.ts # Zod 验证
|
||||
│ └─ *.dto.ts # DTO
|
||||
├─ config/ # 配置(database / env / kafka / neo4j / elasticsearch)
|
||||
├─ middleware/ # 中间件(auth.middleware / permission.guard)
|
||||
├─ shared/ # 共享
|
||||
│ ├─ errors/ # 错误(application-error / global-error.filter)
|
||||
│ ├─ health/ # 健康检查
|
||||
│ ├─ lifecycle/ # 生命周期
|
||||
│ ├─ observability/ # 可观测性(logger / metrics / tracer)
|
||||
│ └─ outbox/ # Outbox(仅事件驱动服务)
|
||||
├─ app.module.ts # 根模块
|
||||
└─ main.ts # 入口
|
||||
```
|
||||
|
||||
### 3.4 TypeScript 规则
|
||||
|
||||
- **禁止 `any`**:未知类型用 `unknown` 并做类型守卫
|
||||
- **禁止 `as` 断言**(除非从 `unknown` 转换或测试中,需注释原因)
|
||||
- **函数返回值必须显式标注**,特别是 `Promise<T>`
|
||||
- **仅用于类型的导入必须使用 `import type`**
|
||||
- **可选链后禁止跟非空断言 `!`**
|
||||
- **ESM 模式**:NestJS ESM 模式下,相对 import 的 `.js` 后缀(构建产物)
|
||||
|
||||
### 3.5 Go 规则
|
||||
|
||||
- 包名小写单词,不使用下划线或驼峰
|
||||
- 导出函数必须有 doc comment(`// FuncName ...`)
|
||||
- 错误处理:`if err != nil { return err }`,禁止 `_ = err`
|
||||
- 不使用 `interface{}`,用 `any`(Go 1.18+)
|
||||
- 包内文件命名 lowercase,不使用下划线(除 `_test.go`)
|
||||
- 中间件位于 `internal/middleware/`,路由位于 `internal/routing/`
|
||||
|
||||
### 3.6 Python 规则
|
||||
|
||||
- 类型注解强制(所有函数签名、变量声明)
|
||||
- FastAPI 路由用 `APIRouter`
|
||||
- 异步优先:`async def`,IO 密集场景禁止同步阻塞调用
|
||||
- 模块名 snake_case,类名 PascalCase
|
||||
- 配置统一通过 `pydantic-settings` 管理(`config.py`)
|
||||
|
||||
### 3.7 命名规范(多语言)
|
||||
|
||||
| 对象 | 规范 | 示例 |
|
||||
| ----------- | -------------------------------------------------- | ------------------------------------------------ |
|
||||
| 目录 | kebab-case | `api-gateway/`、`core-edu/` |
|
||||
| TS 组件 | PascalCase | `UserController.ts` |
|
||||
| TS Hook | camelCase | `useAuth.ts` |
|
||||
| Go 文件 | lowercase | `circuitbreaker.go` |
|
||||
| Python 文件 | snake_case | `clickhouse_client.py` |
|
||||
| Proto 文件 | snake_case | `core_edu.proto` |
|
||||
| 变量/函数 | camelCase(TS)/ lowercase(Go)/ snake_case(Py) | `getUserById` / `getUserById` / `get_user_by_id` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 类/接口 | PascalCase,接口不加 `I` 前缀 | `UserService` |
|
||||
| 布尔值 | `is/has/can/should` 前缀 | `isActive`、`hasPermission` |
|
||||
|
||||
### 3.8 Controller / Server Action 规范
|
||||
|
||||
- 每个 Controller 方法必须用 `@RequirePermission()` 装饰器声明权限
|
||||
- 输入用 Zod / class-validator 验证,验证失败返回结构化错误
|
||||
- 返回 protobuf message 或 DTO(结构同 ActionState)
|
||||
- 错误统一走 `GlobalErrorFilter`
|
||||
- BFF Controller 聚合多个 Service 调用,禁止直接访问 DB
|
||||
|
||||
### 3.9 Tailwind 规范
|
||||
|
||||
- 使用 `cn()` 工具函数管理条件类名
|
||||
- **禁止**字符串拼接动态类名(`bg-${color}-500`)
|
||||
- **禁止**使用任意值(`w-[137px]`),除非有充分理由并注释
|
||||
- 设计令牌在 `packages/shared-tokens/`(待建立)或 `apps/teacher-portal/src/app/styles/tokens/`
|
||||
|
||||
### 3.10 设计令牌规范(强制)
|
||||
|
||||
- **禁止硬编码颜色**:TSX/TS/CSS 中不得出现 `#hex` 颜色字面量,统一使用 `hsl(var(--*))` 或 Tailwind 类 `bg-*`
|
||||
- **禁止硬编码字体**:不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量,使用 `var(--font-family-sans/serif/mono)`
|
||||
- **禁止硬编码字号**:不得出现 `font-size: Npx`,使用 `var(--font-size-1~9)`
|
||||
- **禁止 Tailwind 任意值**:不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]` 等,映射到 `--space-*` 或 Tailwind 默认阶梯
|
||||
- **三层令牌**:
|
||||
- Layer 1 Primitive:原始色板/字号/间距/阴影,业务代码不直接引用
|
||||
- Layer 2 Semantic:语义令牌(light/dark),业务代码唯一引用入口
|
||||
- Layer 3 Tailwind Theme:`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*` 类
|
||||
- **改令牌必同步图**:修改令牌定义后,同步更新 004 与 arch.db
|
||||
- **ESLint 强制约束**:
|
||||
- `no-restricted-syntax`:禁止 `#hex` 字面量
|
||||
- `design-tokens/no-hardcoded-fonts`:禁止 `'Inter'`/`'Fraunces'` 字面量
|
||||
- 白名单:`primitive.css`、`email-channel`、`manifest.ts`
|
||||
|
||||
---
|
||||
|
||||
## 4. 安全规范
|
||||
|
||||
- **JWT RS256**:IAM 签发,Gateway 公钥校验(不共享密钥)
|
||||
- **Cookie**:`httpOnly + Secure + SameSite=Strict`
|
||||
- **环境变量**:服务端变量不加 `NEXT_PUBLIC_` 前缀;前端变量必须加
|
||||
- **环境校验**:用 `@t3-oss/env-nextjs` + Zod 或 `pydantic-settings` 校验
|
||||
- **禁止 `dangerouslySetInnerHTML`**(如必须使用,先用 DOMPurify 清洗)
|
||||
- **密码哈希**:bcrypt(cost ≥ 12),禁止 MD5/SHA1
|
||||
- **限流**:Gateway 层 IP 级令牌桶,登录接口额外加用户级限流
|
||||
- **CORS**:白名单域名,禁止 `*`
|
||||
- **WAF**:见 `infra/security/waf-rules.conf`
|
||||
|
||||
---
|
||||
|
||||
## 5. 契约规范
|
||||
|
||||
- **契约先行**:服务间通信先定 `.proto`,再写实现
|
||||
- **工具链**:protobuf + buf v2
|
||||
- **buf lint**:`STANDARD` 规则集
|
||||
- **buf breaking**:`FILE` 级别检查(CI 强制)
|
||||
- **proto 文件位置**:`packages/shared-proto/proto/`
|
||||
- **包名规范**:`edu.<domain>.v1`(如 `edu.iam.v1`、`edu.core_edu.v1`)
|
||||
- **生成代码**:`buf generate`(TS 走 `buf.gen.yaml`,Go/Python 各自配置)
|
||||
- **gRPC 服务端口**:每服务独立端口,见 004 服务清单
|
||||
|
||||
---
|
||||
|
||||
## 6. 事件驱动规范
|
||||
|
||||
- **Outbox 模式**:事务内写业务表 + outbox 表,独立 publisher 投递到 Kafka(保证 at-least-once)
|
||||
- **事件命名**:`<Aggregate>.<Action>`(如 `ExamCreated`、`HomeworkSubmitted`、`GradeReleased`)
|
||||
- **TOPIC_MAP 路由**:每个服务维护 `事件 → topic` 映射(见 `shared/outbox/`)
|
||||
- **幂等性**:
|
||||
- Kafka producer:`idempotent=true` + `transactionalId`
|
||||
- Consumer:基于 `event_id` 去重(Redis SETNX 或 DB 唯一索引)
|
||||
- **CDC**:MySQL → Debezium → Kafka(可选,用于读模型同步)
|
||||
- **事件版本**:schema 演化用 `v1`/`v2` 后缀,禁止破坏性变更
|
||||
|
||||
---
|
||||
|
||||
## 7. 提交规范
|
||||
|
||||
- 使用 Conventional Commits 格式:`feat(scope): description`
|
||||
- **类型**:`feat`、`fix`、`chore`、`docs`、`style`、`refactor`、`test`、`perf`、`ci`
|
||||
- **scope**(服务名或包名):
|
||||
- 服务:`iam`、`classes`、`core-edu`、`content`、`msg`、`ai`、`data-ana`、`api-gateway`、`push-gateway`、`teacher-bff`
|
||||
- 包:`shared-proto`、`shared-ts`、`shared-go`、`shared-py`、`shared-tokens`
|
||||
- 基础设施:`infra`、`k8s`、`helm`、`ci`
|
||||
- 文档:`docs`
|
||||
- **提交前必须运行**:`pnpm run lint` + `pnpm run typecheck`(TS);`go vet ./...`(Go);`ruff check src/`(Python)
|
||||
- **commitlint + husky**:强制校验(见 `.commitlintrc.js`、`.husky/commit-msg`)
|
||||
|
||||
---
|
||||
|
||||
## 8. Git 工作流
|
||||
|
||||
- **分支策略**:trunk-based(直接提交 main 分支,小项目)
|
||||
- **大型团队**:feature branch + PR,PR 至少 1 人 review
|
||||
- **pre-commit hook**:`lint-staged` 自动修复 + 校验(见 `lint-staged.config.js`)
|
||||
- **commit-msg hook**:commitlint 校验格式
|
||||
- **禁止 force push**:除非显式要求并通知团队
|
||||
|
||||
---
|
||||
|
||||
## 9. 问题记录规则
|
||||
|
||||
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`(索引式速查手册)。**
|
||||
|
||||
### 必须记录的场景
|
||||
|
||||
| 场景 | 记录要求 |
|
||||
| -------------------------------------------------- | --------------------------------- |
|
||||
| 构建报错(dev/build/lint/tsc/go vet/ruff) | 记录到"全局经验"对应主题分区 |
|
||||
| 运行时异常(白屏/API 报错/数据加载失败/gRPC 错误) | 记录到"模块经验"对应模块分区 |
|
||||
| 框架/库版本兼容问题 | 记录到"全局经验: 框架与运行时" |
|
||||
| 依赖配置问题(go mod / uv / pnpm workspace) | 记录到"全局经验: 依赖管理" |
|
||||
| 架构约束违规(跨服务访问 DB / 缺契约) | 记录到"全局经验: 架构约束" |
|
||||
| 多语言 monorepo 问题(共享包链接失败等) | 记录到"全局经验: 多语言 monorepo" |
|
||||
|
||||
### 记录格式
|
||||
|
||||
索引式表格,指明"场景→技术/规则"映射,不写多行代码示例:
|
||||
|
||||
```markdown
|
||||
### X.X 主题分区
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| -------- | ------------------ |
|
||||
| 简述场景 | 正确做法(一句话) |
|
||||
```
|
||||
|
||||
### 记录要求
|
||||
|
||||
- **索引式**:场景→技术/规则映射,不写代码示例和错误示范列
|
||||
- **去重**:同类问题在原条目补充,不重复创建
|
||||
- **引用架构规则**:架构分层、模块结构等规则引用 004 和本规则文件,不重复
|
||||
- **工作经验日志**:在"工作经验日志"区按时间倒序追加(50 条上限),记录"做了什么/学到什么/下次注意"
|
||||
|
||||
---
|
||||
|
||||
## 10. AI 工作强制流程
|
||||
|
||||
**所有 AI 工作必须遵循此流程,违反即违规。**
|
||||
|
||||
### 阶段 1:上下文加载
|
||||
|
||||
1. `pnpm run arch:scan` 更新 arch.db
|
||||
2. `pnpm run arch:query -- module-deps` 查目标模块依赖
|
||||
3. `pnpm run arch:query -- symbol-refs <目标函数>` 查调用链
|
||||
4. 阅读 `services/[service]/README.md` 读模块工作流程
|
||||
5. 查 `docs/troubleshooting/known-issues.md` "模块经验"分区读相关经验
|
||||
6. 查 `docs/architecture/004_architecture_impact_map.md` 对应章节
|
||||
|
||||
### 阶段 2:执行工作
|
||||
|
||||
1. 按规划执行(契约先行:先 proto,再实现)
|
||||
2. 修改代码后立即运行 `pnpm run arch:scan` 更新 arch.db
|
||||
3. 运行质量校验确保零错误:
|
||||
- TS:`pnpm run lint` + `pnpm run typecheck`
|
||||
- Go:`go vet ./...` + `go build ./...`
|
||||
- Python:`ruff check src/`
|
||||
|
||||
### 阶段 3:经验沉淀(强制,不可跳过)
|
||||
|
||||
1. 在 `docs/troubleshooting/known-issues.md` "工作经验日志"区追加一条记录:
|
||||
- 日期 + 时间
|
||||
- 模块
|
||||
- 做了什么 + 学到什么
|
||||
2. 若发现新的"场景→技术"映射 → 提炼到对应模块分区
|
||||
3. 若发现新的架构决策 → 更新 004
|
||||
4. 若代码结构变化 → `pnpm run arch:scan` 确认 arch.db 已更新
|
||||
|
||||
---
|
||||
|
||||
## 11. 多语言 monorepo 规则
|
||||
|
||||
- **包管理器分工**:
|
||||
- TypeScript:pnpm workspace(`pnpm-workspace.yaml`)
|
||||
- Go:go.work(`go.work`)
|
||||
- Python:uv workspace(根 `pyproject.toml` 的 `[tool.uv.workspace]`)
|
||||
- **共享包**(`packages/`):
|
||||
- `shared-proto`:protobuf 契约定义(跨语言共享)
|
||||
- `shared-ts`:TypeScript 共享类型与工具(待建立)
|
||||
- `shared-go`:Go 共享工具(待建立)
|
||||
- `shared-py`:Python 共享工具(待建立)
|
||||
- `shared-tokens`:设计令牌(待建立)
|
||||
- **跨语言契约**:通过 protobuf,禁止 JSON Schema 或 OpenAPI 作为内部契约
|
||||
- **依赖版本对齐**:共享依赖(如 uuid、jwt)在多语言版本号保持一致
|
||||
|
||||
---
|
||||
|
||||
## 12. 可观测性规范
|
||||
|
||||
- **三支柱必须同时启用**(所有服务):
|
||||
- **日志**:pino(TS)/ zap(Go)/ structlog(Python)—— 结构化 JSON
|
||||
- **指标**:prom-client(TS)/ prometheus(Go)/ prometheus-client(Python)—— `/metrics` 端点
|
||||
- **链路**:OpenTelemetry SDK + OTLP exporter —— 统一 traceparent
|
||||
- **健康检查**:`/health`(liveness)+ `/ready`(readiness),见各服务 `shared/health/`
|
||||
- **请求 ID**:Gateway 注入 `X-Request-Id`,全链路传递(日志、metrics、trace)
|
||||
- **指标命名**:`<service>_<module>_<operation>_<unit>`(如 `iam_login_duration_seconds`)
|
||||
- **告警规则**:见 `infra/prometheus/rules.yml` + `infra/alertmanager/alertmanager.yml`
|
||||
- **Grafana 面板**:见 `infra/grafana/dashboards/`
|
||||
|
||||
---
|
||||
|
||||
## 13. 部署与基础设施规范
|
||||
|
||||
- **容器化**:每服务独立 Dockerfile(见 `services/[service]/Dockerfile`)
|
||||
- **K8s**:基础设施 manifest 位于 `infra/k8s/`,演化目标为 Helm chart(见 `infra/k8s/helm/`)
|
||||
- **命名空间**:`edu-system`、`edu-monitoring`、`edu-chaos`、`edu-backup`
|
||||
- **配置分离**:
|
||||
- ConfigMap:非敏感配置(环境变量、特性开关)
|
||||
- Secret:敏感配置(DB 密码、JWT 密钥、API key)—— 见 `infra/security/secrets.example.env`
|
||||
- **备份**:见 `infra/backup/backup-mysql.sh`,每服务独立备份,保留 7 天
|
||||
- **监控栈**:Prometheus + Grafana + Alertmanager(见 `infra/docker-compose.monitoring.yml`)
|
||||
- **混沌工程**:见 `infra/chaos/experiments.yaml`
|
||||
|
||||
---
|
||||
|
||||
## 14. 多 AI 协作规范
|
||||
|
||||
> 详细流程见 [多 AI 协作指南](../../docs/standards/multi-ai-collaboration.md),本节为强制约束摘要。
|
||||
|
||||
### 14.1 角色与权限
|
||||
|
||||
| 角色 | 职责 | push 特性分支 | 创建 PR | 合并 PR | push main | force push main |
|
||||
| -------------------------- | ---------------------------------------- | ------------- | ------- | ----------- | --------- | --------------- |
|
||||
| **协调 AI(Coordinator)** | PR 审核、合并、冲突仲裁、发布 | ✅ | ✅ | ✅ | ❌ | ⚠️(仅事故) |
|
||||
| **开发 AI(Dev)** | 按模块分工写代码、提 PR | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **SRE AI** | `infra/` 维护、部署 | ✅(infra) | ✅ | ✅(infra) | ❌ | ⚠️(仅事故) |
|
||||
| **人类决策者** | 架构决策、Breaking Change 审批、发布确认 | — | — | — | — | — |
|
||||
|
||||
### 14.2 模块单一负责制
|
||||
|
||||
- 每个模块(限界上下文)只有一个 AI 负责,禁止并行修改同一模块
|
||||
- `shared-proto`、`shared-tokens`、`docs/` 由协调 AI 维护,开发 AI 只读引用
|
||||
- `infra/` 由 SRE AI 专门负责,业务 AI 不直接修改
|
||||
|
||||
### 14.3 分支命名规范(强制)
|
||||
|
||||
```
|
||||
<type>/<scope>-<task-id>-<ai-id>
|
||||
```
|
||||
|
||||
- `type`:feat / fix / refactor / docs / chore / test
|
||||
- `scope`:见 §7 提交规范 scope-enum(26 项)
|
||||
- `task-id`:任务简短描述(kebab-case)
|
||||
- `ai-id`:AI 唯一标识符(如 `ai01`、`ai02`、`coord`)
|
||||
|
||||
**示例**:`feat/classes-add-pagination-ai01`
|
||||
|
||||
### 14.4 PR 与合并规则(强制)
|
||||
|
||||
1. **禁止直接 push 到 `main`**:所有变更通过 PR
|
||||
2. **PR 必须通过 CI**:lint / typecheck / build / test 全绿
|
||||
3. **PR 必须通过 CODEOWNERS review**:至少 1 人 approve
|
||||
4. **合并策略**:Squash Merge(默认),Rebase Merge(保留多 commit 历史),**禁止 Merge Commit**
|
||||
5. **特性分支寿命 ≤ 3 天**:超期需 rebase 最新 main
|
||||
6. **跨模块变更拆分**:按依赖顺序拆多个 PR(proto → service → gateway → frontend),协调 AI 按序合并
|
||||
|
||||
### 14.5 跨模块变更顺序(强制)
|
||||
|
||||
修改涉及多模块时,必须按以下顺序拆分 PR 并顺序合并:
|
||||
|
||||
1. `shared-proto`(proto 契约)
|
||||
2. 业务服务(classes / iam / core-edu 等)
|
||||
3. `api-gateway`(路由)
|
||||
4. BFF(teacher-bff 等)
|
||||
5. 微前端(teacher-portal 等)
|
||||
|
||||
> 每合并一个 PR,后续 PR 的开发 AI 必须 rebase 最新 main 并重新校验。
|
||||
|
||||
### 14.6 冲突处理规则
|
||||
|
||||
- **文件冲突**:后合并的 PR rebase 最新 main,`git push --force-with-lease`(仅自己的分支)
|
||||
- **架构冲突**:由协调 AI 仲裁保留方案
|
||||
- **禁止 `git push --force` 到 main 或他人分支**
|
||||
|
||||
### 14.7 AI 身份标注(强制)
|
||||
|
||||
每个 PR 描述末尾必须追加:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
**AI Agent**: <ai-id> (<负责模块>)
|
||||
**Branch**: <分支名>
|
||||
**Coordinator**: <协调 AI ai-id>
|
||||
```
|
||||
|
||||
每个 AI 完成任务后,在 `docs/troubleshooting/known-issues.md` "工作经验日志"区追加记录(见 §9.3)。
|
||||
|
||||
### 14.8 敏感文件保护
|
||||
|
||||
以下文件修改需人类决策者额外审批:
|
||||
|
||||
- `.env`、`.env.example`、`infra/security/secrets.example.env`
|
||||
- `infra/k8s/`(生产部署)
|
||||
- `.github/workflows/`(CI 配置)
|
||||
- `.trae/rules/project_rules.md`(项目规则)
|
||||
|
||||
---
|
||||
|
||||
## 15. CI/CD 规范
|
||||
|
||||
> 详细配置见 `.github/workflows/ci.yml`,使用手册见 [cicd-runbook](../../docs/standards/cicd-runbook.md),设计文档见 [no-push-local-build-design](../../docs/superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md)。
|
||||
|
||||
### 15.1 核心模式:no-push 本地构建
|
||||
|
||||
- **不推送镜像到 registry**:构建即部署,镜像只存在于构建机本地
|
||||
- **不依赖自建镜像**:全部使用官方镜像(node:22-alpine / golang:1.22-alpine / bufbuild/buf / docker:25-git)
|
||||
- **DooD 模式**:deploy job 容器挂载 `/var/run/docker.sock`,容器内 docker 命令作用于宿主机
|
||||
- **单文件管理**:一个 `.github/workflows/ci.yml` 管全部 CI/CD
|
||||
|
||||
### 15.2 流水线阶段
|
||||
|
||||
| 阶段 | 并行 | 内容 | 失败策略 |
|
||||
| ----------------- | ---- | ------------------------------------ | -------- |
|
||||
| **quality-ts** | ✅ | pnpm lint + typecheck + test + build | 失败阻断 |
|
||||
| **quality-go** | ✅ | go vet + build + test | 失败阻断 |
|
||||
| **quality-proto** | ✅ | buf lint + buf breaking(仅 PR) | 失败阻断 |
|
||||
| **deploy** | 串行 | docker compose up --build + 健康检查 | 失败阻断 |
|
||||
|
||||
> deploy job 仅在 `push main` 或 `workflow_dispatch` 时触发,PR 时不部署
|
||||
|
||||
### 15.3 触发条件
|
||||
|
||||
| 事件 | 触发阶段 | 触发条件 |
|
||||
| ----------------- | ----------------------------------------------- | -------------------------------- |
|
||||
| PR 创建/更新 | quality-ts + quality-go + quality-proto(并行) | 所有路径 |
|
||||
| push 到 main | 上述全部 + deploy | 合并后自动 |
|
||||
| workflow_dispatch | 上述全部 + deploy | 手动触发,支持 `commit_sha` 回滚 |
|
||||
|
||||
> **不再支持 tag 发布**:no-push 模式下不用 `git tag v*` 触发。版本管理通过 commit SHA 追溯。
|
||||
|
||||
### 15.4 镜像规范
|
||||
|
||||
- **不推送到 registry**,本地构建本地使用
|
||||
- **不保留历史镜像**:layer cache 在宿主机本地,未变更的层秒过
|
||||
- **回滚**:`git revert` 重跑 CI,或 `workflow_dispatch` 指定 `commit_sha`
|
||||
|
||||
### 15.5 部署策略
|
||||
|
||||
- **目标环境**:服务器 Docker Compose(P1-P2 阶段),K8s(P3+ 阶段)
|
||||
- **部署方式**:`docker compose up -d --build --remove-orphans`(在 `/opt/edu/` 目录)
|
||||
- **部署目录**:`/opt/edu/`(compose 文件)+ `/opt/edu/repo/`(CI 同步的源码,供 build.context 使用)
|
||||
- **健康检查**:部署后轮询 `/healthz` 端点(10 次 × 6 秒),失败输出容器日志
|
||||
- **回滚**:`git revert + push` 或 `workflow_dispatch` 指定 `commit_sha`
|
||||
|
||||
### 15.6 必需的 CI 文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --------------------------------- | ------------------------------------- |
|
||||
| `.github/workflows/ci.yml` | 唯一 CI/CD 流水线(quality + deploy) |
|
||||
| `infra/docker-compose.deploy.yml` | 部署用 compose(build: 替代 image:) |
|
||||
| `infra/docker-compose.tools.yml` | 一次性预拉所有 CI 镜像 |
|
||||
| `infra/deploy.env.example` | 部署环境变量模板 |
|
||||
|
||||
### 15.7 actrunner 配置
|
||||
|
||||
```toml
|
||||
# /etc/gitea/act_runner/config.yaml
|
||||
container:
|
||||
valid_volumes:
|
||||
- /var/run/docker.sock
|
||||
```
|
||||
|
||||
### 15.8 镜像预拉(一次性)
|
||||
|
||||
部署前在服务器执行:
|
||||
|
||||
```bash
|
||||
docker compose -f infra/docker-compose.tools.yml pull
|
||||
```
|
||||
|
||||
拉取清单:node:22-alpine、golang:1.22-alpine、bufbuild/buf:latest、docker:25-git、node:20-alpine、alpine:3.20、mysql:8.0(开发测试)、redis:7-alpine(开发测试)。
|
||||
|
||||
---
|
||||
|
||||
**本规则文件是项目的强制约束,所有 contributor(含 AI)必须遵守。规则变更需同步更新 004 与 arch.db。**
|
||||
@@ -5,7 +5,8 @@
|
||||
> 状态:基线发布
|
||||
> 维护者:架构组
|
||||
> 关联文档:
|
||||
> - [项目规则](./project_rules.md)
|
||||
>
|
||||
> - [项目规则](./.trae/rules/project_rules.md)
|
||||
> - [编码规范](./docs/standards/coding-standards.md)
|
||||
> - [Git 工作流](./docs/standards/git-workflow.md)
|
||||
|
||||
@@ -30,13 +31,13 @@
|
||||
|
||||
原始项目 CICD 是基于 Next.js 16 的单应用架构,承载 K12 智慧教务系统 35 个业务模块。随着业务规模扩张与团队增长,单应用架构在以下方面暴露瓶颈:
|
||||
|
||||
| 维度 | 单应用瓶颈 | 微服务目标 |
|
||||
|------|-----------|-----------|
|
||||
| 团队协作 | 35 模块挤在同一仓库,合并冲突频繁 | 按领域拆分,6 领域团队独立迭代 |
|
||||
| 部署节奏 | 全量构建发布,单模块变更牵动全站 | 服务粒度独立部署,故障爆炸半径缩小 |
|
||||
| 维度 | 单应用瓶颈 | 微服务目标 |
|
||||
| -------- | ---------------------------------- | ------------------------------------------------- |
|
||||
| 团队协作 | 35 模块挤在同一仓库,合并冲突频繁 | 按领域拆分,6 领域团队独立迭代 |
|
||||
| 部署节奏 | 全量构建发布,单模块变更牵动全站 | 服务粒度独立部署,故障爆炸半径缩小 |
|
||||
| 技术选型 | TypeScript 单语言,AI/分析场景受限 | TS(业务)+ Go(网关)+ Python(AI/分析)各取所长 |
|
||||
| 数据规模 | 单 MySQL,跨模块联表与读放大 | 读写分离 + CQRS,ClickHouse 承载分析负载 |
|
||||
| 可演进性 | 模块间隐式耦合,重构成本高 | DDD 限界上下文显式契约,演化可控 |
|
||||
| 数据规模 | 单 MySQL,跨模块联表与读放大 | 读写分离 + CQRS,ClickHouse 承载分析负载 |
|
||||
| 可演进性 | 模块间隐式耦合,重构成本高 | DDD 限界上下文显式契约,演化可控 |
|
||||
|
||||
### 1.2 迁移原则
|
||||
|
||||
@@ -52,29 +53,29 @@
|
||||
|
||||
### 2.1 基本概况
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 仓库路径 | `e:\Desktop\CICD` |
|
||||
| 技术栈 | Next.js 16 + React 19 + Tailwind v4 + Drizzle ORM |
|
||||
| 语言 | TypeScript(全栈) |
|
||||
| 架构 | 单应用 + 严格模块化(app → modules → shared 三层) |
|
||||
| 业务模块数 | 35 个 |
|
||||
| 数据库 | MySQL(单库) |
|
||||
| 认证 | NextAuth.js |
|
||||
| 部署 | Gitea Actions + Docker standalone |
|
||||
| 属性 | 值 |
|
||||
| ---------- | -------------------------------------------------- |
|
||||
| 仓库路径 | `e:\Desktop\CICD` |
|
||||
| 技术栈 | Next.js 16 + React 19 + Tailwind v4 + Drizzle ORM |
|
||||
| 语言 | TypeScript(全栈) |
|
||||
| 架构 | 单应用 + 严格模块化(app → modules → shared 三层) |
|
||||
| 业务模块数 | 35 个 |
|
||||
| 数据库 | MySQL(单库) |
|
||||
| 认证 | NextAuth.js |
|
||||
| 部署 | Gitea Actions + Docker standalone |
|
||||
|
||||
### 2.2 模块清单(按领域归类)
|
||||
|
||||
| 领域 | 模块 |
|
||||
|------|------|
|
||||
| 身份与权限 | auth、users、onboarding、settings、permissions |
|
||||
| 教学组织 | classes、teachers、students、parents、subjects |
|
||||
| 教学核心 | courses、lessons、schedule、attendance、leave-requests |
|
||||
| 考试评价 | exams、questions、grading、scores、analytics |
|
||||
| 作业内容 | homework、textbooks、resources |
|
||||
| 沟通通知 | messaging、notifications、announcements |
|
||||
| 智能辅助 | ai(备课/出题/分析)、search |
|
||||
| 系统支撑 | audit-logs、reports、dashboard、layout |
|
||||
| 领域 | 模块 |
|
||||
| ---------- | ------------------------------------------------------ |
|
||||
| 身份与权限 | auth、users、onboarding、settings、permissions |
|
||||
| 教学组织 | classes、teachers、students、parents、subjects |
|
||||
| 教学核心 | courses、lessons、schedule、attendance、leave-requests |
|
||||
| 考试评价 | exams、questions、grading、scores、analytics |
|
||||
| 作业内容 | homework、textbooks、resources |
|
||||
| 沟通通知 | messaging、notifications、announcements |
|
||||
| 智能辅助 | ai(备课/出题/分析)、search |
|
||||
| 系统支撑 | audit-logs、reports、dashboard、layout |
|
||||
|
||||
### 2.3 已沉淀的工程资产
|
||||
|
||||
@@ -94,17 +95,17 @@
|
||||
|
||||
### 3.1 基本概况
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 仓库路径 | `e:\Desktop\Edu` |
|
||||
| 远程仓库 | https://git.eazygame.cn/xiner/Edu.git |
|
||||
| 架构范式 | DDD + EDA + CQRS 微服务 |
|
||||
| 语言 | TypeScript(NestJS 10)+ Go(Gin 网关)+ Python(FastAPI 分析/AI) |
|
||||
| 前端 | React + Next.js + Module Federation(4 微前端) |
|
||||
| monorepo 策略 | pnpm workspace + go.work + pyproject.toml (uv) |
|
||||
| 事件总线 | Kafka + Debezium CDC |
|
||||
| 契约 | protobuf + buf |
|
||||
| 存储 | MySQL / Redis / ClickHouse / Neo4j / Elasticsearch |
|
||||
| 属性 | 值 |
|
||||
| ------------- | ------------------------------------------------------------------ |
|
||||
| 仓库路径 | `e:\Desktop\Edu` |
|
||||
| 远程仓库 | https://git.eazygame.cn/xiner/Edu.git |
|
||||
| 架构范式 | DDD + EDA + CQRS 微服务 |
|
||||
| 语言 | TypeScript(NestJS 10)+ Go(Gin 网关)+ Python(FastAPI 分析/AI) |
|
||||
| 前端 | React + Next.js + Module Federation(4 微前端) |
|
||||
| monorepo 策略 | pnpm workspace + go.work + pyproject.toml (uv) |
|
||||
| 事件总线 | Kafka + Debezium CDC |
|
||||
| 契约 | protobuf + buf |
|
||||
| 存储 | MySQL / Redis / ClickHouse / Neo4j / Elasticsearch |
|
||||
|
||||
### 3.2 整体架构
|
||||
|
||||
@@ -164,23 +165,23 @@ flowchart TB
|
||||
|
||||
### 3.3 微前端与领域服务映射
|
||||
|
||||
| 微前端 | 路由前缀 | 对接 BFF | 主要消费的服务 |
|
||||
|--------|---------|---------|---------------|
|
||||
| Admin Shell | `/admin` | Admin BFF | identity、org、insight |
|
||||
| Teacher Shell | `/teacher` | Teacher BFF | teaching、content、comm |
|
||||
| Student Shell | `/student` | Student/Parent BFF | teaching、content |
|
||||
| Parent Shell | `/parent` | Student/Parent BFF | teaching、comm |
|
||||
| 微前端 | 路由前缀 | 对接 BFF | 主要消费的服务 |
|
||||
| ------------- | ---------- | ------------------ | ----------------------- |
|
||||
| Admin Shell | `/admin` | Admin BFF | identity、org、insight |
|
||||
| Teacher Shell | `/teacher` | Teacher BFF | teaching、content、comm |
|
||||
| Student Shell | `/student` | Student/Parent BFF | teaching、content |
|
||||
| Parent Shell | `/parent` | Student/Parent BFF | teaching、comm |
|
||||
|
||||
| 微服务 | 原始模块映射 | 主存储 | 对外契约 |
|
||||
|--------|------------|--------|---------|
|
||||
| identity | auth、users、onboarding、settings、permissions | MySQL + Redis | identity.proto |
|
||||
| org | classes、teachers、students、parents、subjects | MySQL | org.proto |
|
||||
| teaching | courses、lessons、schedule、attendance、leave-requests、exams、homework | MySQL | teaching.proto |
|
||||
| content | textbooks、resources、questions、grading | MySQL + Elasticsearch | content.proto |
|
||||
| comm | messaging、notifications、announcements | MySQL + Redis | comm.proto |
|
||||
| insight | scores、analytics、ai、search、reports、dashboard | ClickHouse + Neo4j | insight.proto |
|
||||
| auth(基础设施) | NextAuth 逻辑下沉 | MySQL + Redis | auth.proto |
|
||||
| notification(基础设施) | notifications channel 抽离 | MySQL + Redis | notification.proto |
|
||||
| 微服务 | 原始模块映射 | 主存储 | 对外契约 |
|
||||
| ------------------------ | ----------------------------------------------------------------------- | --------------------- | ------------------ |
|
||||
| identity | auth、users、onboarding、settings、permissions | MySQL + Redis | identity.proto |
|
||||
| org | classes、teachers、students、parents、subjects | MySQL | org.proto |
|
||||
| teaching | courses、lessons、schedule、attendance、leave-requests、exams、homework | MySQL | teaching.proto |
|
||||
| content | textbooks、resources、questions、grading | MySQL + Elasticsearch | content.proto |
|
||||
| comm | messaging、notifications、announcements | MySQL + Redis | comm.proto |
|
||||
| insight | scores、analytics、ai、search、reports、dashboard | ClickHouse + Neo4j | insight.proto |
|
||||
| auth(基础设施) | NextAuth 逻辑下沉 | MySQL + Redis | auth.proto |
|
||||
| notification(基础设施) | notifications channel 抽离 | MySQL + Redis | notification.proto |
|
||||
|
||||
---
|
||||
|
||||
@@ -190,65 +191,65 @@ flowchart TB
|
||||
|
||||
### 4.1 规范类资产
|
||||
|
||||
| 资产 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| 项目规则(project_rules) | 调整 | 架构图优先保留,分层规则从三层改为微服务分层,新增 DDD/契约/事件驱动规则 |
|
||||
| 编码规范(coding-standards) | 调整 | TS 部分保留并补充 NestJS 装饰器/DI 规则,新增 Go、Python、protobuf 章节 |
|
||||
| Git 工作流 | 调整 | trunk-based 替代分支策略,scope 改为服务/包名,新增多语言 monorepo 提交规则 |
|
||||
| 提交规范(Conventional Commits) | 直接迁移 | 类型与格式完全沿用 |
|
||||
| 设计令牌规范 | 调整 | 分层模型保留,分布位置从 `src/app/styles/tokens/` 改为微前端共享包 |
|
||||
| 安全规范 | 直接迁移 | Cookie 策略、env 校验、XSS 防护、权限校验规则全部沿用 |
|
||||
| 问题记录规则 | 直接迁移 | known-issues.md 索引式速查手册模式沿用 |
|
||||
| A11y 规范 | 直接迁移 | WCAG 2.2 AA 目标与工具集沿用 |
|
||||
| 资产 | 策略 | 说明 |
|
||||
| -------------------------------- | -------- | --------------------------------------------------------------------------- |
|
||||
| 项目规则(project_rules) | 调整 | 架构图优先保留,分层规则从三层改为微服务分层,新增 DDD/契约/事件驱动规则 |
|
||||
| 编码规范(coding-standards) | 调整 | TS 部分保留并补充 NestJS 装饰器/DI 规则,新增 Go、Python、protobuf 章节 |
|
||||
| Git 工作流 | 调整 | trunk-based 替代分支策略,scope 改为服务/包名,新增多语言 monorepo 提交规则 |
|
||||
| 提交规范(Conventional Commits) | 直接迁移 | 类型与格式完全沿用 |
|
||||
| 设计令牌规范 | 调整 | 分层模型保留,分布位置从 `src/app/styles/tokens/` 改为微前端共享包 |
|
||||
| 安全规范 | 直接迁移 | Cookie 策略、env 校验、XSS 防护、权限校验规则全部沿用 |
|
||||
| 问题记录规则 | 直接迁移 | known-issues.md 索引式速查手册模式沿用 |
|
||||
| A11y 规范 | 直接迁移 | WCAG 2.2 AA 目标与工具集沿用 |
|
||||
|
||||
### 4.2 代码类资产
|
||||
|
||||
| 资产 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| Zod schema 定义 | 直接迁移 | 各模块 schema.ts 平移至对应微服务,复用验证规则 |
|
||||
| 权限点常量(Permissions) | 直接迁移 | 集中迁入 identity 服务共享包 |
|
||||
| Drizzle schema(表结构) | 调整 | 按领域拆分到各微服务独占库,关系型字段保持不变 |
|
||||
| ActionState 类型 | 直接迁移 | 升级为 protobuf message,结构不变 |
|
||||
| Server Actions | 重写 | 改写为 NestJS Controller + Service + Application Service 三层 |
|
||||
| data-access 层 | 重写 | 改写为 NestJS Repository + Domain Entity |
|
||||
| UI 组件(shared/components) | 直接迁移 | 平移至微前端共享包,保持 PascalCase 命名 |
|
||||
| Hook(useAuth、usePermission) | 调整 | 改为通过 BFF/gRPC client 获取,接口签名保持不变 |
|
||||
| 缓存层(cacheFn) | 重写 | 改为 NestJS Cache 模块 + Redis,去除 React cache() |
|
||||
| arch:scan 工具 | 调整 | 扫描器扩展为多语言(TS+Go+Python),数据库结构保持 |
|
||||
| 审计日志三件套 | 直接迁移 | 平移至 notification 服务,日志结构保持 |
|
||||
| CI/CD 流水线 | 重写 | Gitea Actions 改为多服务并行流水线,新增契约校验阶段 |
|
||||
| 资产 | 策略 | 说明 |
|
||||
| ------------------------------ | -------- | ------------------------------------------------------------- |
|
||||
| Zod schema 定义 | 直接迁移 | 各模块 schema.ts 平移至对应微服务,复用验证规则 |
|
||||
| 权限点常量(Permissions) | 直接迁移 | 集中迁入 identity 服务共享包 |
|
||||
| Drizzle schema(表结构) | 调整 | 按领域拆分到各微服务独占库,关系型字段保持不变 |
|
||||
| ActionState 类型 | 直接迁移 | 升级为 protobuf message,结构不变 |
|
||||
| Server Actions | 重写 | 改写为 NestJS Controller + Service + Application Service 三层 |
|
||||
| data-access 层 | 重写 | 改写为 NestJS Repository + Domain Entity |
|
||||
| UI 组件(shared/components) | 直接迁移 | 平移至微前端共享包,保持 PascalCase 命名 |
|
||||
| Hook(useAuth、usePermission) | 调整 | 改为通过 BFF/gRPC client 获取,接口签名保持不变 |
|
||||
| 缓存层(cacheFn) | 重写 | 改为 NestJS Cache 模块 + Redis,去除 React cache() |
|
||||
| arch:scan 工具 | 调整 | 扫描器扩展为多语言(TS+Go+Python),数据库结构保持 |
|
||||
| 审计日志三件套 | 直接迁移 | 平移至 notification 服务,日志结构保持 |
|
||||
| CI/CD 流水线 | 重写 | Gitea Actions 改为多服务并行流水线,新增契约校验阶段 |
|
||||
|
||||
### 4.3 文档类资产
|
||||
|
||||
| 资产 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| 架构影响地图(004) | 重写 | 从单应用模块图改为微服务限界上下文图 |
|
||||
| K12 功能清单(006) | 直接迁移 | 功能清单与架构无关,直接平移 |
|
||||
| 差距审计报告(007) | 调整 | 重新审计各微服务的功能完成度 |
|
||||
| 模块角色映射(008) | 直接迁移 | 角色权限矩阵不变 |
|
||||
| 路线图(roadmap/) | 重写 | 6 阶段微服务路线图 |
|
||||
| known-issues.md | 直接迁移 | 经验日志平移,新增"微服务"分区 |
|
||||
| 各模块 README | 重写 | 改为各微服务 README,按 DDD 上下文描述 |
|
||||
| 资产 | 策略 | 说明 |
|
||||
| ------------------- | -------- | -------------------------------------- |
|
||||
| 架构影响地图(004) | 重写 | 从单应用模块图改为微服务限界上下文图 |
|
||||
| K12 功能清单(006) | 直接迁移 | 功能清单与架构无关,直接平移 |
|
||||
| 差距审计报告(007) | 调整 | 重新审计各微服务的功能完成度 |
|
||||
| 模块角色映射(008) | 直接迁移 | 角色权限矩阵不变 |
|
||||
| 路线图(roadmap/) | 重写 | 6 阶段微服务路线图 |
|
||||
| known-issues.md | 直接迁移 | 经验日志平移,新增"微服务"分区 |
|
||||
| 各模块 README | 重写 | 改为各微服务 README,按 DDD 上下文描述 |
|
||||
|
||||
---
|
||||
|
||||
## 五、文档体系映射表
|
||||
|
||||
| CICD 文档 | Edu 对应文档 | 关系 |
|
||||
|-----------|------------|------|
|
||||
| `.trae/rules/project_rules.md` | `project_rules.md` | 调整(微服务版) |
|
||||
| `docs/standards/coding-standards.md` | `docs/standards/coding-standards.md` | 调整(多语言版) |
|
||||
| —(散落在 project_rules) | `docs/standards/git-workflow.md` | 新建 |
|
||||
| `docs/architecture/004_architecture_impact_map.md` | `docs/architecture/001_architecture_overview.md` | 重写 |
|
||||
| `docs/architecture/006_k12_feature_checklist.md` | `docs/architecture/feature_checklist.md` | 直接迁移 |
|
||||
| `docs/architecture/007_gap_audit_report.md` | `docs/architecture/gap_audit.md` | 调整 |
|
||||
| `docs/architecture/008_module_role_mapping.md` | `docs/architecture/role_mapping.md` | 直接迁移 |
|
||||
| `docs/architecture/roadmap/README.md` | `docs/architecture/roadmap/README.md` | 重写 |
|
||||
| `docs/architecture/roadmap/tech-debt.md` | `docs/architecture/roadmap/tech-debt.md` | 重写 |
|
||||
| `docs/architecture/roadmap/decoupling.md` | `docs/architecture/roadmap/migration_phases.md` | 重写(迁移阶段化) |
|
||||
| `docs/troubleshooting/known-issues.md` | `docs/troubleshooting/known-issues.md` | 直接迁移 + 新分区 |
|
||||
| `src/modules/[module]/README.md` | `services/[service]/README.md` | 重写 |
|
||||
| `docs/standards/coding-standards.md` §A11y | `docs/standards/accessibility.md` | 拆分独立 |
|
||||
| CICD 文档 | Edu 对应文档 | 关系 |
|
||||
| -------------------------------------------------- | ------------------------------------------------ | ------------------ |
|
||||
| `.trae/rules/project_rules.md` | `project_rules.md` | 调整(微服务版) |
|
||||
| `docs/standards/coding-standards.md` | `docs/standards/coding-standards.md` | 调整(多语言版) |
|
||||
| —(散落在 project_rules) | `docs/standards/git-workflow.md` | 新建 |
|
||||
| `docs/architecture/004_architecture_impact_map.md` | `docs/architecture/001_architecture_overview.md` | 重写 |
|
||||
| `docs/architecture/006_k12_feature_checklist.md` | `docs/architecture/feature_checklist.md` | 直接迁移 |
|
||||
| `docs/architecture/007_gap_audit_report.md` | `docs/architecture/gap_audit.md` | 调整 |
|
||||
| `docs/architecture/008_module_role_mapping.md` | `docs/architecture/role_mapping.md` | 直接迁移 |
|
||||
| `docs/architecture/roadmap/README.md` | `docs/architecture/roadmap/README.md` | 重写 |
|
||||
| `docs/architecture/roadmap/tech-debt.md` | `docs/architecture/roadmap/tech-debt.md` | 重写 |
|
||||
| `docs/architecture/roadmap/decoupling.md` | `docs/architecture/roadmap/migration_phases.md` | 重写(迁移阶段化) |
|
||||
| `docs/troubleshooting/known-issues.md` | `docs/troubleshooting/known-issues.md` | 直接迁移 + 新分区 |
|
||||
| `src/modules/[module]/README.md` | `services/[service]/README.md` | 重写 |
|
||||
| `docs/standards/coding-standards.md` §A11y | `docs/standards/accessibility.md` | 拆分独立 |
|
||||
|
||||
---
|
||||
|
||||
@@ -283,14 +284,14 @@ gantt
|
||||
|
||||
### 6.2 各阶段目标
|
||||
|
||||
| 阶段 | 名称 | 目标 | 关键交付物 | 验收信号 |
|
||||
|------|------|------|-----------|---------|
|
||||
| P1 | 地基 | 仓库骨架、契约工具链、CI、可观测平台 | monorepo 结构、buf 配置、Kafka 集群、OpenTelemetry | 契约生成 + 一次端到端 trace |
|
||||
| P2 | 身份 | identity + auth + notification 三服务打通 | JWT 颁发、RBAC、邮件/短信/站内通知 | 用户注册→登录→收到通知 |
|
||||
| P3 | 核心教学 | org + teaching + content | 班级/课表/作业/题库/考试 | 教师创建作业→学生提交→批改闭环 |
|
||||
| P4 | 内容分析 | insight + CQRS 读模型 + ES 全文检索 | ClickHouse 报表、ES 搜索、Neo4j 知识图谱 | 多维分析报表 + 全文搜索可用 |
|
||||
| P5 | 沟通AI | comm + AI 辅助 | 站内信/通知中心、AI 备课/出题/答疑 | 教师用 AI 出题并发布到班级 |
|
||||
| P6 | 硬化 | 安全加固、灾备、性能、混沌工程 | WAF、定期备份、压测报告、混沌演练 | RPO≤15min、RTO≤30min、P99≤500ms |
|
||||
| 阶段 | 名称 | 目标 | 关键交付物 | 验收信号 |
|
||||
| ---- | -------- | ----------------------------------------- | -------------------------------------------------- | ------------------------------- |
|
||||
| P1 | 地基 | 仓库骨架、契约工具链、CI、可观测平台 | monorepo 结构、buf 配置、Kafka 集群、OpenTelemetry | 契约生成 + 一次端到端 trace |
|
||||
| P2 | 身份 | identity + auth + notification 三服务打通 | JWT 颁发、RBAC、邮件/短信/站内通知 | 用户注册→登录→收到通知 |
|
||||
| P3 | 核心教学 | org + teaching + content | 班级/课表/作业/题库/考试 | 教师创建作业→学生提交→批改闭环 |
|
||||
| P4 | 内容分析 | insight + CQRS 读模型 + ES 全文检索 | ClickHouse 报表、ES 搜索、Neo4j 知识图谱 | 多维分析报表 + 全文搜索可用 |
|
||||
| P5 | 沟通AI | comm + AI 辅助 | 站内信/通知中心、AI 备课/出题/答疑 | 教师用 AI 出题并发布到班级 |
|
||||
| P6 | 硬化 | 安全加固、灾备、性能、混沌工程 | WAF、定期备份、压测报告、混沌演练 | RPO≤15min、RTO≤30min、P99≤500ms |
|
||||
|
||||
### 6.3 阶段交付门槛
|
||||
|
||||
@@ -310,15 +311,16 @@ gantt
|
||||
|
||||
**复用方式**:将 CICD 的 `src/app/styles/tokens/` 五层分层模型平移至微前端共享包。
|
||||
|
||||
| CICD 位置 | Edu 位置 | 调整 |
|
||||
|-----------|---------|------|
|
||||
| `src/app/styles/tokens/primitive.css` | `packages/ui-tokens/primitive.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/semantic-light.css` | `packages/ui-tokens/semantic-light.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/semantic-dark.css` | `packages/ui-tokens/semantic-dark.css` | 直接平移 |
|
||||
| CICD 位置 | Edu 位置 | 调整 |
|
||||
| ---------------------------------------------- | ------------------------------------------- | -------- |
|
||||
| `src/app/styles/tokens/primitive.css` | `packages/ui-tokens/primitive.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/semantic-light.css` | `packages/ui-tokens/semantic-light.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/semantic-dark.css` | `packages/ui-tokens/semantic-dark.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/lesson-preparation.css` | `packages/ui-tokens/lesson-preparation.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/tailwind-theme.css` | `packages/ui-tokens/tailwind-theme.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/tailwind-theme.css` | `packages/ui-tokens/tailwind-theme.css` | 直接平移 |
|
||||
|
||||
**强制规则保持不变**:
|
||||
|
||||
- 禁止硬编码颜色(`#hex`)
|
||||
- 禁止硬编码字体(`'Inter'`/`'Fraunces'`/`'JetBrains Mono'`)
|
||||
- 禁止硬编码字号(`font-size: Npx`)
|
||||
@@ -329,14 +331,15 @@ gantt
|
||||
|
||||
**复用方式**:将 CICD 的 `src/shared/components/ui/` 平移至 `packages/ui-components/`,作为 Module Federation 共享依赖。
|
||||
|
||||
| 组件类别 | CICD 路径 | Edu 路径 | 复用要点 |
|
||||
|---------|----------|---------|---------|
|
||||
| 基础组件(Button/Input/Dialog) | `shared/components/ui/` | `packages/ui-components/` | 全部平移,保持 PascalCase 命名 |
|
||||
| A11y 组件 | `shared/components/a11y/` | `packages/ui-components/a11y/` | skip-link、visually-hidden、focus-trap、aria-status 全部平移 |
|
||||
| 图表组件 | 各模块内 | `packages/ui-components/charts/` | 收集 recharts 封装,统一暴露 |
|
||||
| 表单组件 | react-hook-form 封装 | `packages/ui-components/form/` | 与 zod resolver 一同平移 |
|
||||
| 组件类别 | CICD 路径 | Edu 路径 | 复用要点 |
|
||||
| ------------------------------- | ------------------------- | -------------------------------- | ------------------------------------------------------------ |
|
||||
| 基础组件(Button/Input/Dialog) | `shared/components/ui/` | `packages/ui-components/` | 全部平移,保持 PascalCase 命名 |
|
||||
| A11y 组件 | `shared/components/a11y/` | `packages/ui-components/a11y/` | skip-link、visually-hidden、focus-trap、aria-status 全部平移 |
|
||||
| 图表组件 | 各模块内 | `packages/ui-components/charts/` | 收集 recharts 封装,统一暴露 |
|
||||
| 表单组件 | react-hook-form 封装 | `packages/ui-components/form/` | 与 zod resolver 一同平移 |
|
||||
|
||||
**复用规则**:
|
||||
|
||||
- 组件必须为纯函数,使用 `function` 声明
|
||||
- 不使用 `React.FC`,直接用函数声明 + 显式标注 props 类型
|
||||
- 默认服务端组件(微前端 host),需要交互时才添加 `"use client"`
|
||||
@@ -346,14 +349,15 @@ gantt
|
||||
|
||||
**复用方式**:将 CICD 的 `requirePermission()` + `usePermission().hasPermission()` 模式平移至 identity 服务 + auth 基础设施服务。
|
||||
|
||||
| CICD 资产 | Edu 位置 | 调整 |
|
||||
|-----------|---------|------|
|
||||
| `shared/lib/auth-guard.ts`(requirePermission) | `services/auth/src/guards/permission.guard.ts`(NestJS Guard) | 改为 NestJS Guard 装饰器 |
|
||||
| `shared/types/permissions.ts`(权限点常量) | `packages/contracts/src/permissions.ts` | 集中到 contracts 包,多服务共享 |
|
||||
| `usePermission` Hook | `packages/ui-components/hooks/use-permission.ts` | 通过 BFF 拉取权限,Hook 接口不变 |
|
||||
| 角色权限矩阵(008) | `docs/architecture/role_mapping.md` | 直接平移 |
|
||||
| CICD 资产 | Edu 位置 | 调整 |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------- | -------------------------------- |
|
||||
| `shared/lib/auth-guard.ts`(requirePermission) | `services/auth/src/guards/permission.guard.ts`(NestJS Guard) | 改为 NestJS Guard 装饰器 |
|
||||
| `shared/types/permissions.ts`(权限点常量) | `packages/contracts/src/permissions.ts` | 集中到 contracts 包,多服务共享 |
|
||||
| `usePermission` Hook | `packages/ui-components/hooks/use-permission.ts` | 通过 BFF 拉取权限,Hook 接口不变 |
|
||||
| 角色权限矩阵(008) | `docs/architecture/role_mapping.md` | 直接平移 |
|
||||
|
||||
**强制规则保持不变**:
|
||||
|
||||
- 每个 Controller/Action 必须调用 `requirePermission()` 等价物
|
||||
- 前端组件禁止使用 `role === "xxx"` 硬编码,统一使用 `usePermission().hasPermission()`
|
||||
|
||||
@@ -361,36 +365,36 @@ gantt
|
||||
|
||||
**复用方式**:CICD 的 `cacheFn`(React `cache()` + 自定义缓存层)改为 NestJS Cache 模块 + Redis,**权限数据不跨请求缓存**的规则沿用。
|
||||
|
||||
| CICD 模式 | Edu 模式 | 备注 |
|
||||
|-----------|---------|------|
|
||||
| CICD 模式 | Edu 模式 | 备注 |
|
||||
| ------------------------ | --------------------------------------------------- | ------------------------------------- |
|
||||
| `cacheFn`(React cache) | NestJS `@UseInterceptors(CacheInterceptor)` + Redis | 单请求内缓存改为 NestJS REQUEST scope |
|
||||
| `unstable_cache` | 禁用 | CICD 已禁用,Edu 沿用禁用决策 |
|
||||
| 权限数据缓存 | 仅在 auth 服务内部缓存,TTL ≤ 60s | 跨服务不缓存权限 |
|
||||
| 静态资源缓存 | CDN + 短缓存 | 配置不变 |
|
||||
| `unstable_cache` | 禁用 | CICD 已禁用,Edu 沿用禁用决策 |
|
||||
| 权限数据缓存 | 仅在 auth 服务内部缓存,TTL ≤ 60s | 跨服务不缓存权限 |
|
||||
| 静态资源缓存 | CDN + 短缓存 | 配置不变 |
|
||||
|
||||
### 7.5 Server Action 模式 → Application Service 模式
|
||||
|
||||
**复用方式**:CICD 的 Server Action 编排模式(权限 + Zod 验证 + 调用 data-access + revalidate)平移为 NestJS Application Service 编排模式。
|
||||
|
||||
| CICD Server Action 步骤 | NestJS Application Service 对应 |
|
||||
|------------------------|-------------------------------|
|
||||
| `requirePermission(perm)` | `@RequirePermission(perm)` 装饰器 + Guard |
|
||||
| Zod `safeParse` | `ValidationPipe` + DTO class-validator |
|
||||
| 调用 `data-access` | 调用 Domain Service / Repository |
|
||||
| `revalidatePath` | 发布领域事件触发读模型更新 |
|
||||
| 返回 `ActionState<T>` | 返回 protobuf message(结构同 ActionState) |
|
||||
| CICD Server Action 步骤 | NestJS Application Service 对应 |
|
||||
| ------------------------- | ------------------------------------------- |
|
||||
| `requirePermission(perm)` | `@RequirePermission(perm)` 装饰器 + Guard |
|
||||
| Zod `safeParse` | `ValidationPipe` + DTO class-validator |
|
||||
| 调用 `data-access` | 调用 Domain Service / Repository |
|
||||
| `revalidatePath` | 发布领域事件触发读模型更新 |
|
||||
| 返回 `ActionState<T>` | 返回 protobuf message(结构同 ActionState) |
|
||||
|
||||
### 7.6 状态管理 5 层模型
|
||||
|
||||
**复用方式**:CICD 的 5 层状态模型平移至微前端 host 应用。
|
||||
|
||||
| 层级 | CICD 方案 | Edu 方案 | 备注 |
|
||||
|------|----------|---------|------|
|
||||
| L1 URL | nuqs | nuqs | 直接平移 |
|
||||
| L2 Server | TanStack Query | TanStack Query | 直接平移 |
|
||||
| L3 Client Business | Zustand slice | Zustand slice | 直接平移 |
|
||||
| L4 Global UI | Zustand ui-store + ModalRoot | Zustand ui-store + ModalRoot | 直接平移 |
|
||||
| L5 Form | react-hook-form + zodResolver | react-hook-form + zodResolver | 直接平移 |
|
||||
| 层级 | CICD 方案 | Edu 方案 | 备注 |
|
||||
| ------------------ | ----------------------------- | ----------------------------- | -------- |
|
||||
| L1 URL | nuqs | nuqs | 直接平移 |
|
||||
| L2 Server | TanStack Query | TanStack Query | 直接平移 |
|
||||
| L3 Client Business | Zustand slice | Zustand slice | 直接平移 |
|
||||
| L4 Global UI | Zustand ui-store + ModalRoot | Zustand ui-store + ModalRoot | 直接平移 |
|
||||
| L5 Form | react-hook-form + zodResolver | react-hook-form + zodResolver | 直接平移 |
|
||||
|
||||
### 7.7 A11y 工具集
|
||||
|
||||
@@ -409,23 +413,24 @@ gantt
|
||||
|
||||
**复用方式**:平移至 notification 服务,日志结构保持。
|
||||
|
||||
| CICD 资产 | Edu 位置 | 备注 |
|
||||
|-----------|---------|------|
|
||||
| `shared/lib/login-logger.ts` | `services/notification/src/loggers/login-logger.ts` | 登录尝试日志 |
|
||||
| CICD 资产 | Edu 位置 | 备注 |
|
||||
| ----------------------------- | ---------------------------------------------------- | ---------------------------- |
|
||||
| `shared/lib/login-logger.ts` | `services/notification/src/loggers/login-logger.ts` | 登录尝试日志 |
|
||||
| `shared/lib/change-logger.ts` | `services/notification/src/loggers/change-logger.ts` | 数据变更日志(监听领域事件) |
|
||||
| `shared/lib/audit-logger.ts` | `services/notification/src/loggers/audit-logger.ts` | 关键业务操作日志 |
|
||||
| `shared/lib/audit-logger.ts` | `services/notification/src/loggers/audit-logger.ts` | 关键业务操作日志 |
|
||||
|
||||
### 7.9 CI/CD 流水线模式
|
||||
|
||||
**复用方式**:三套工作流模式沿用(CI + 安全扫描 + 灾备演练),但执行方式改为多服务并行。
|
||||
|
||||
| CICD 工作流 | Edu 工作流 | 调整 |
|
||||
|------------|-----------|------|
|
||||
| `ci.yml` | `.gitea/workflows/ci.yml` | 单体改为矩阵并行(每个服务一个 job) |
|
||||
| `security.yml` | `.gitea/workflows/security.yml` | 新增 Trivy 扫描 Docker 镜像 |
|
||||
| `dr-drill.yml` | `.gitea/workflows/dr-drill.yml` | 灾备演练改为多服务恢复顺序演练 |
|
||||
| CICD 工作流 | Edu 工作流 | 调整 |
|
||||
| -------------- | ------------------------------- | ------------------------------------ |
|
||||
| `ci.yml` | `.gitea/workflows/ci.yml` | 单体改为矩阵并行(每个服务一个 job) |
|
||||
| `security.yml` | `.gitea/workflows/security.yml` | 新增 Trivy 扫描 Docker 镜像 |
|
||||
| `dr-drill.yml` | `.gitea/workflows/dr-drill.yml` | 灾备演练改为多服务恢复顺序演练 |
|
||||
|
||||
**CI 必须包含**(沿用 CICD 规则):
|
||||
|
||||
1. 安装依赖(多语言:pnpm install / go mod download / uv sync)
|
||||
2. Lint 检查(ESLint + golangci-lint + ruff)
|
||||
3. 类型检查(tsc --noEmit + go vet + mypy)
|
||||
@@ -478,15 +483,15 @@ gantt
|
||||
|
||||
## 附录:迁移过程中的关键决策点
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 服务拆分粒度 | 6 业务 + 2 基础设施 | 平衡团队规模与拆分收益,避免过细导致 RPC 开销 |
|
||||
| 通信协议 | gRPC(内部)+ REST(BFF 对外) | 内部高性能,外部兼容性 |
|
||||
| 事件总线 | Kafka + Debezium CDC | CDC 减少业务代码侵入,Outbox 模式保证一致性 |
|
||||
| 契约工具 | protobuf + buf | 多语言代码生成,breaking change 检测 |
|
||||
| 前端架构 | Module Federation | 微前端独立部署,运行时共享依赖 |
|
||||
| 状态管理 | 沿用 5 层模型 | 团队熟悉度高,迁移成本低 |
|
||||
| 数据库拆分 | 每服务独占库 | 杜绝跨服务联表,强制契约化通信 |
|
||||
| 缓存策略 | NestJS Cache + Redis | 替代 React cache(),规则(权限不跨请求缓存)沿用 |
|
||||
| 迁移方式 | strangler fig | 风险可控,渐进式切换 |
|
||||
| arch.db | 多语言扫描扩展 | 复用 CICD 元数据库思路,扩展 Go/Python 扫描器 |
|
||||
| 决策点 | 选择 | 理由 |
|
||||
| ------------ | ------------------------------ | ------------------------------------------------ |
|
||||
| 服务拆分粒度 | 6 业务 + 2 基础设施 | 平衡团队规模与拆分收益,避免过细导致 RPC 开销 |
|
||||
| 通信协议 | gRPC(内部)+ REST(BFF 对外) | 内部高性能,外部兼容性 |
|
||||
| 事件总线 | Kafka + Debezium CDC | CDC 减少业务代码侵入,Outbox 模式保证一致性 |
|
||||
| 契约工具 | protobuf + buf | 多语言代码生成,breaking change 检测 |
|
||||
| 前端架构 | Module Federation | 微前端独立部署,运行时共享依赖 |
|
||||
| 状态管理 | 沿用 5 层模型 | 团队熟悉度高,迁移成本低 |
|
||||
| 数据库拆分 | 每服务独占库 | 杜绝跨服务联表,强制契约化通信 |
|
||||
| 缓存策略 | NestJS Cache + Redis | 替代 React cache(),规则(权限不跨请求缓存)沿用 |
|
||||
| 迁移方式 | strangler fig | 风险可控,渐进式切换 |
|
||||
| arch.db | 多语言扫描扩展 | 复用 CICD 元数据库思路,扩展 Go/Python 扫描器 |
|
||||
|
||||
@@ -38,8 +38,11 @@ pnpm dev
|
||||
- [编码规范](docs/standards/coding-standards.md)
|
||||
- [UI 设计系统](docs/standards/ui-design-system.md)
|
||||
- [Git 工作流](docs/standards/git-workflow.md)
|
||||
- [本地启动手册](docs/standards/local-dev-runbook.md)
|
||||
- [多 AI 协作指南](docs/standards/multi-ai-collaboration.md)
|
||||
- [CI/CD 使用手册](docs/standards/cicd-runbook.md)
|
||||
- [已知问题](docs/troubleshooting/known-issues.md)
|
||||
- [项目规则](project_rules.md)
|
||||
- [项目规则](.trae/rules/project_rules.md)
|
||||
- [迁移指南](MIGRATION_GUIDE.md)
|
||||
|
||||
## 开发阶段
|
||||
|
||||
49
apps/teacher-portal/Dockerfile
Normal file
49
apps/teacher-portal/Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
# 多阶段构建:Next.js 生产镜像
|
||||
# 用法:docker build -t edu/teacher-portal:latest -f apps/teacher-portal/Dockerfile .
|
||||
|
||||
# ============ Builder ============
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# 启用 pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
|
||||
# 先拷依赖清单,利用缓存
|
||||
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
|
||||
COPY apps/teacher-portal/package.json ./apps/teacher-portal/
|
||||
# 安装依赖(含 devDependencies,构建需要)
|
||||
RUN pnpm install --filter @edu/teacher-portal... --frozen-lockfile || pnpm install --filter @edu/teacher-portal...
|
||||
|
||||
# 拷源码
|
||||
COPY apps/teacher-portal ./apps/teacher-portal
|
||||
|
||||
# 构建(禁用 telemetry,生产模式)
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN pnpm --filter @edu/teacher-portal run build
|
||||
|
||||
# ============ Runtime ============
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
|
||||
# 非 root 用户运行
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
|
||||
|
||||
# 拷构建产物与必要清单
|
||||
COPY --from=builder /app/apps/teacher-portal/package.json ./package.json
|
||||
COPY --from=builder /app/apps/teacher-portal/.next ./.next
|
||||
COPY --from=builder /app/apps/teacher-portal/public ./public
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/apps/teacher-portal/next.config.js ./next.config.js
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD wget --quiet --spider http://localhost:3000/ || exit 1
|
||||
|
||||
CMD ["node_modules/.bin/next", "start", "-p", "3000"]
|
||||
945
apps/teacher-portal/README.md
Normal file
945
apps/teacher-portal/README.md
Normal file
@@ -0,0 +1,945 @@
|
||||
# teacher-portal / 4 端微前端架构设计
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> AI Agent:ai07(前端 4 端:teacher-portal / student-portal / parent-portal / admin-portal)
|
||||
> 阶段:阶段 1(理解确认书)+ 阶段 2(模块架构设计文档)
|
||||
> 关联文档:
|
||||
>
|
||||
> - [004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4
|
||||
> - [AI 分配方案](../../docs/architecture/ai-allocation.md) §5 ai07
|
||||
> - [项目规则](../../.trae/rules/project_rules.md) §3.8/§3.9/§3.10
|
||||
> - [编码规范](../../docs/standards/coding-standards.md) §2.8-2.10/§7
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md) §7.1-7.7
|
||||
> - [known-issues](../../docs/troubleshooting/known-issues.md) §2.12
|
||||
|
||||
> 本文档合并 4 端的设计,因 ai07 单一负责全部 4 端,Module Federation shell + remote 架构需统一设计,4 端共享组件库和权限体系。当前仓库仅 `teacher-portal` 已实现(P1 测试页 + P2 骨架),student/parent/admin-portal 待建。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [阶段 1:模块理解确认书(4 端)](#阶段-1模块理解确认书4-端)
|
||||
2. [teacher-portal 现状审计(对齐黄金模板)](#teacher-portal-现状审计对齐黄金模板)
|
||||
3. [阶段 2:模块架构设计文档](#阶段-2模块架构设计文档)
|
||||
4. [4 端差异化对比表](#4-端差异化对比表)
|
||||
5. [与其他模块的交互点(契约清单)](#与其他模块的交互点契约清单)
|
||||
6. [风险与假设](#风险与假设)
|
||||
7. [coord 交叉审查所需信息](#coord-交叉审查所需信息)
|
||||
|
||||
---
|
||||
|
||||
# 阶段 1:模块理解确认书(4 端)
|
||||
|
||||
## 1.1 我在架构中的位置
|
||||
|
||||
| 维度 | teacher-portal | student-portal | parent-portal | admin-portal |
|
||||
| ------------ | --------------------------------------------------------- | ---------------- | ---------------- | --------------------------- |
|
||||
| 层级 | L2 微前端层 | L2 微前端层 | L2 微前端层 | L2 微前端层 |
|
||||
| MF 角色 | **Shell 宿主**(主应用) | Remote(子应用) | Remote(子应用) | Remote(子应用) |
|
||||
| 上游 | 浏览器(教师 / 教导主任 / 教研组长) | 浏览器(学生) | 浏览器(家长) | 浏览器(系统/校管理员) |
|
||||
| 下游(同步) | api-gateway(REST,经 Next.js rewrites 代理) | api-gateway | api-gateway | api-gateway |
|
||||
| 下游(推送) | push-gateway(WebSocket/SSE,P5) | push-gateway | push-gateway | — |
|
||||
| BFF 对接 | teacher-bff(GraphQL Yoga + DataLoader) | student-bff | parent-bff | teacher-bff 复用 + iam 直连 |
|
||||
| 通信方式 | HTTP/REST(前端→Gateway)+ WebSocket(前端→push-gateway) | 同左 | 同左 | HTTP/REST |
|
||||
|
||||
**说明**:
|
||||
|
||||
- 4 端均通过 `next.config.js` 的 `rewrites` 将 `/api/v1/*` 代理到 `api-gateway`,前端不直连任何业务服务或 BFF 后端实例
|
||||
- MF 架构下,4 端共享同一 Shell(teacher-portal 作为 Shell 宿主),其余 3 端作为 Remote 子应用挂载;Shell 提供 AppShell + 共享组件库 + 权限 Hook + API 请求层
|
||||
- 场景域 BFF 复用策略(004 §5.4):教导主任/教研组长复用 teacher-portal + 额外管理视口,不单独建 portal
|
||||
|
||||
## 1.2 我的限界上下文
|
||||
|
||||
| 项 | teacher-portal | student-portal | parent-portal | admin-portal |
|
||||
| -------- | --------------------------------------------------------------------------------------------------- | -------------------------------- | ---------------------------------------- | ------------------------------------------------- |
|
||||
| 业务领域 | 教学场景域 | 学习场景域 | 家长场景域 | 管理场景域 |
|
||||
| 主要聚合 | 班级、考试、作业、成绩、备课、AI 出题 | 作答、作业提交、学情诊断、错题本 | 多子女切换、通知偏好、学情查看、成绩通知 | 用户/角色/权限/视口 CRUD、组织/班级管理、平台监控 |
|
||||
| 不负责 | 学生作答界面、家长多子女切换 | 教师批改界面、AI 出题 | 教师沟通、学生作答 | 教学业务编排(归 teacher-portal) |
|
||||
| 数据范围 | DataScope L1-L5(教师 L1 班级 / 教导主任 L2 年级 / 校管理员 L3 学校 / 区教研员 L4 / 系统管理员 L5) | DataScope L0(仅本人) | DataScope L0(仅子女) | DataScope L3-L5(校管理员 L3 / 系统管理员 L5) |
|
||||
|
||||
## 1.3 我与外部的契约
|
||||
|
||||
### 1.3.1 消费的后端 API(经 api-gateway 代理)
|
||||
|
||||
| 端 | 路径前缀 | 下游 BFF/服务 | 关键端点 |
|
||||
| -------------- | ------------------------------------------------------------------------ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| teacher-portal | `/api/v1/iam/*` | iam | `POST /iam/login`、`POST /iam/register`、`POST /iam/refresh`、`GET /iam/me`、`GET /iam/rbac/...`、`GET /iam/effective-permissions` |
|
||||
| teacher-portal | `/api/v1/teacher/*` | teacher-bff | `GET /teacher/viewports`、`GET /teacher/dashboard`、`GET /teacher/classes/:id/exams`、`GET /teacher/classes/:id/homework`、`GET /teacher/exams/:id/grades` |
|
||||
| teacher-portal | `/api/v1/classes/*` | core-edu(classes 模块) | CRUD(黄金模板) |
|
||||
| teacher-portal | `/api/v1/exams/*` `/api/v1/homework/*` `/api/v1/grades/*` | core-edu | P3 教学核心 |
|
||||
| teacher-portal | `/api/v1/textbooks/*` `/api/v1/knowledge-points/*` `/api/v1/questions/*` | content | P4 内容 |
|
||||
| teacher-portal | `/api/v1/ai/*` | ai(SSE 流式) | P5 AI 辅助出题 |
|
||||
| student-portal | `/api/v1/student/*` | student-bff | `GET /student/viewports`、`GET /student/dashboard`、`GET /student/homework`、`POST /student/homework/:id/submit`、`GET /student/diagnostic` |
|
||||
| parent-portal | `/api/v1/parent/*` | parent-bff | `GET /parent/viewports`、`GET /parent/children`、`POST /parent/switch-child`、`GET /parent/notifications`、`PUT /parent/notification-preferences` |
|
||||
| admin-portal | `/api/v1/iam/*`(管理用) | iam | 用户/角色/权限/视口 CRUD |
|
||||
| admin-portal | `/api/v1/admin/*` | teacher-bff 复用 + iam 直连 | 平台监控、统计数据聚合 |
|
||||
| 全部 | `/api/v1/notifications/*` | msg | 通知中心(P5) |
|
||||
|
||||
### 1.3.2 统一响应契约
|
||||
|
||||
所有后端响应遵循 `ActionState` 结构(迁移指南 §7.5):
|
||||
|
||||
```typescript
|
||||
type ActionState<T> =
|
||||
| { success: true; data: T }
|
||||
| {
|
||||
success: false;
|
||||
error: { code: string; message: string; details?: unknown };
|
||||
};
|
||||
```
|
||||
|
||||
错误码前缀按服务名大写(如 `IAM_`、`CORE_EDU_`、`CONTENT_`、`MSG_`、`AI_`、`BFF_`、`GW_`)。前端 API 请求层根据 `error.code` 前缀路由到对应的 i18n key。
|
||||
|
||||
### 1.3.3 推送契约(P5)
|
||||
|
||||
| 端 | 协议 | 场景 |
|
||||
| -------------- | ------------------------- | -------------------------------------------- |
|
||||
| teacher-portal | WebSocket(push-gateway) | 学生提交作业通知、考试成绩录入提醒、全校广播 |
|
||||
| student-portal | WebSocket | 考试发布通知、成绩发布、作业截止提醒 |
|
||||
| parent-portal | WebSocket | 子女成绩发布、教师沟通、学校通知 |
|
||||
| admin-portal | — | 不消费推送(管理端用轮询) |
|
||||
|
||||
## 1.4 我的技术栈
|
||||
|
||||
| 维度 | 选型 | 说明 |
|
||||
| --------------------------- | -------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| 框架 | Next.js 14+(App Router) | 4 端统一,server components 默认,client components 按需 |
|
||||
| 语言 | TypeScript 5.5+(strict) | 沿用 tsconfig.base.json |
|
||||
| 微前端 | Module Federation 2.0(@module-federation/nextjs-mf) | teacher-portal = Shell,其余 = Remote |
|
||||
| 样式 | Tailwind CSS 3.4+ | 配合设计令牌三层模型 |
|
||||
| UI 组件库 | shadcn/ui(迁移指南 §7.2) | 平移至 `packages/ui-components/`,MF 共享 |
|
||||
| 状态管理 L1 URL | nuqs | 可分享、可刷新状态 |
|
||||
| 状态管理 L2 Server | TanStack Query v5 | 服务端数据缓存、重试、乐观更新 |
|
||||
| 状态管理 L3 Client Business | Zustand slice | 客户端业务状态 |
|
||||
| 状态管理 L4 Global UI | Zustand ui-store + ModalRoot | 全局 UI 状态 |
|
||||
| 状态管理 L5 Form | react-hook-form + zodResolver | 表单状态 |
|
||||
| 富文本 | Tiptap(备课、出题、反馈) | SSR 安全 |
|
||||
| 图表 | recharts | 学情、Dashboard |
|
||||
| i18n | next-intl | BFF/服务返回 i18n key + 参数,前端翻译 |
|
||||
| A11y | eslint-plugin-jsx-a11y(error 级) | WCAG 2.2 AA |
|
||||
| 字体 | Inter(sans)/ Fraunces(serif)/ JetBrains Mono(mono) | next/font/google 加载,CSS 变量暴露 |
|
||||
|
||||
## 1.5 我的阶段归属
|
||||
|
||||
| 端 | 阶段 | 当前状态 | 依赖上游阶段 |
|
||||
| -------------- | ---- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| teacher-portal | P2 | ✅ 已实现 P1 测试页 + P2 骨架(登录/AppShell/Dashboard/classes CRUD);⚠️ 待审计对齐黄金模板 + 引入 MF + 共享组件库 | P1(api-gateway + classes + iam) |
|
||||
| student-portal | P3 | 📐 需设计(待 core-edu + student-bff 就绪) | P3(core-edu + student-bff) |
|
||||
| parent-portal | P4 | 📐 需设计(待 parent-bff + data-ana 就绪) | P4(parent-bff + data-ana) |
|
||||
| admin-portal | P6 | 📐 需设计(待全部业务服务稳定) | P6(硬化阶段) |
|
||||
|
||||
## 1.6 黄金模板对齐清单(对照 classes 服务)
|
||||
|
||||
> 前端无 `@RequirePermission` 装饰器(后端概念),对齐项改造为前端等价物。
|
||||
|
||||
| 对齐项 | classes(后端黄金模板) | teacher-portal 前端等价 | 当前状态 |
|
||||
| --------------------- | ------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------- |
|
||||
| 权限校验 | `@RequirePermission(Permissions.XXX)` | `usePermission().hasPermission("XXX")` Hook + `<RequirePermission>` 组件 | ❌ 缺失,直接硬编码 `user.roles.join(", ")` |
|
||||
| 错误码前缀统一 | `CLASSES_*`、`IAM_*` | API 请求层根据 `error.code` 前缀路由 i18n | ❌ 缺失统一请求层 |
|
||||
| logger | pino | 前端 console + Sentry(P6) | ⚠️ 仅 console.error |
|
||||
| metrics | prom-client `/metrics` | 前端 Web Vitals → Gateway 上报 | ❌ 缺失 |
|
||||
| tracer | OTel SDK | 前端 OTel browser SDK(P6) | ❌ 缺失 |
|
||||
| /healthz + /readyz | `GET /healthz` `GET /readyz` | Next.js `/api/health` route + Dockerfile HEALTHCHECK | ⚠️ Dockerfile 有 HEALTHCHECK,无 /api/health |
|
||||
| 优雅关闭 | SIGTERM handler | Next.js 无长连接,无需 | ✅ N/A |
|
||||
| 测试覆盖率 ≥ 80% | Vitest | Vitest + @testing-library/react + Playwright E2E | ❌ 0% |
|
||||
| Dockerfile 多阶段构建 | builder + runtime | 已有多阶段 | ✅ 已对齐 |
|
||||
| Zod 输入验证 | class-validator + Zod schema | react-hook-form + zodResolver | ❌ 缺失 |
|
||||
| GlobalErrorFilter | NestJS 全局异常过滤器 | React ErrorBoundary + API 请求层统一错误处理 | ❌ 缺失 |
|
||||
| 设计令牌三层 | — | primitive.css / semantic-light/dark.css / tailwind-theme.css | ❌ 硬编码在 globals.css + tailwind.config.js |
|
||||
| A11y 工具集 | — | useA11yId / mergeA11yProps / describeInput / focus-trap | ❌ 缺失 |
|
||||
|
||||
---
|
||||
|
||||
# teacher-portal 现状审计(对齐黄金模板)
|
||||
|
||||
## 2.1 审计表
|
||||
|
||||
| 维度 | 状态 | 说明 |
|
||||
| ------------------------------------ | ------ | ---------------------------------------------------------------------- |
|
||||
| 权限装饰器(前端等价 usePermission) | ❌ | AppShell.tsx 直接 `user.roles.join(", ")`,违反 project_rules §3.8 |
|
||||
| 错误码前缀 | ❌ | 无统一 API 请求层,错误处理散落在每个 page.tsx |
|
||||
| logger | ⚠️ | 仅 `console.error`,无结构化、无 trace_id |
|
||||
| metrics | ❌ | 无 Web Vitals 采集 |
|
||||
| tracer | ❌ | 无 OTel browser SDK |
|
||||
| /healthz | ⚠️ | Dockerfile 有 `HEALTHCHECK wget /`,但无 `/api/health` route |
|
||||
| /readyz | ❌ | 无 |
|
||||
| 优雅关闭 | ✅ N/A | Next.js 无长连接 |
|
||||
| 测试覆盖率 | ❌ | 0%,无测试文件 |
|
||||
| Dockerfile 多阶段 | ✅ | builder + runtime,非 root 用户,HEALTHCHECK |
|
||||
| Zod 输入验证 | ❌ | 表单直接 useState,无 zodResolver |
|
||||
| GlobalErrorFilter(ErrorBoundary) | ❌ | 无 React ErrorBoundary |
|
||||
| 设计令牌三层 | ❌ | 硬编码在 globals.css(`:root` 变量)+ tailwind.config.js(hex 字面量) |
|
||||
| A11y 工具集 | ❌ | 无 useA11yId、focus-trap 等 |
|
||||
| Module Federation 配置 | ❌ | next.config.js 仅有 rewrites,无 MF |
|
||||
| 5 层状态管理 | ❌ | 仅 useState + localStorage,无 nuqs/TanStack Query/Zustand |
|
||||
| 共享组件库 | ❌ | 仅 AppShell,无 ErrorBoundary/Loading/Empty/RequirePermission |
|
||||
| i18n | ❌ | 中文硬编码在 JSX |
|
||||
| API 请求层 | ❌ | 每页重复 fetch + authHeaders + try/catch |
|
||||
| ESLint flat config 自定义规则 | ❌ | 未配置 no-hardcoded-fonts / design-tokens 规则 |
|
||||
|
||||
## 2.2 现有文件清单
|
||||
|
||||
```
|
||||
apps/teacher-portal/
|
||||
├─ src/
|
||||
│ ├─ app/
|
||||
│ │ ├─ (app)/ # 受保护路由组(套 AppShell)
|
||||
│ │ │ ├─ classes/page.tsx # 班级 CRUD(P1 测试页)
|
||||
│ │ │ ├─ dashboard/page.tsx # 教师仪表盘
|
||||
│ │ │ ├─ exams/page.tsx # 考试列表
|
||||
│ │ │ ├─ grades/page.tsx # 成绩查询
|
||||
│ │ │ ├─ homework/page.tsx # 作业列表
|
||||
│ │ │ └─ layout.tsx # 套 AppShell
|
||||
│ │ ├─ login/page.tsx # 登录页(不套壳)
|
||||
│ │ ├─ globals.css # 全局样式 + 设计令牌(硬编码)
|
||||
│ │ ├─ layout.tsx # 根布局(字体加载)
|
||||
│ │ └─ page.tsx # 根路径重定向
|
||||
│ ├─ components/
|
||||
│ │ └─ AppShell.tsx # 左侧栏 + 主内容区
|
||||
│ └─ lib/
|
||||
│ └─ auth.ts # token + userInfo localStorage 管理
|
||||
├─ Dockerfile # 多阶段构建 ✅
|
||||
├─ next.config.js # 仅 rewrites,无 MF ❌
|
||||
├─ package.json # 仅 next/react/react-dom,无 MF/Query/Zustand ❌
|
||||
├─ tailwind.config.js # 硬编码 hex ❌
|
||||
├─ postcss.config.js
|
||||
└─ tsconfig.json
|
||||
```
|
||||
|
||||
## 2.3 主要违规点(必须在 P2 收尾或 P3 起步时修复)
|
||||
|
||||
1. **权限硬编码**:[AppShell.tsx:143](src/components/AppShell.tsx) `user.roles.join(", ")` 违反 project_rules §3.8,必须改为 `usePermission().hasPermission()`
|
||||
2. **设计令牌硬编码**:[globals.css:6-13](src/app/globals.css) 与 [tailwind.config.js:7-19](tailwind.config.js) 出现 `hsl(...)` 字面量与 `'Fraunces'`/`'Inter'` 字面量,违反 project_rules §3.10
|
||||
3. **无统一 API 请求层**:4 个 page.tsx 重复 `authHeaders()` + `fetch` + `try/catch` + `setError`,必须抽取到 `lib/api.ts`
|
||||
4. **无权限 Hook**:缺少 `usePermission().hasPermission()`,无法做 L3 组件级视口控制
|
||||
5. **无 ErrorBoundary**:React 渲染异常会白屏
|
||||
6. **无 5 层状态管理**:登录态用 localStorage(L3),但无 TanStack Query(L2)导致每页重复 fetch
|
||||
7. **字体名硬编码**:[layout.tsx:3-7](src/app/layout.tsx) 直接 import `Inter/Fraunces/JetBrains_Mono`,应改为 `var(--font-family-sans/serif/mono)`
|
||||
|
||||
---
|
||||
|
||||
# 阶段 2:模块架构设计文档
|
||||
|
||||
## 3.1 模块内部分层图(4 端统一 MF 架构)
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Browser["浏览器"]
|
||||
URL[URL 路由]
|
||||
end
|
||||
|
||||
subgraph Shell["teacher-portal(Shell 宿主)"]
|
||||
AppShell[AppShell<br/>左栏导航 + 主内容区]
|
||||
RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
|
||||
Router[Next.js App Router]
|
||||
SharedDeps["共享依赖暴露<br/>react/react-dom/@tanstack/react-query/zustand/nuqs"]
|
||||
end
|
||||
|
||||
subgraph RemoteTeacher["teacher-portal Remote 模块"]
|
||||
TeacherPages[教学场景页面<br/>dashboard/classes/exams/homework/grades/ai-assist]
|
||||
end
|
||||
|
||||
subgraph RemoteStudent["student-portal(Remote)"]
|
||||
StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
|
||||
end
|
||||
|
||||
subgraph RemoteParent["parent-portal(Remote)"]
|
||||
ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
|
||||
end
|
||||
|
||||
subgraph RemoteAdmin["admin-portal(Remote)"]
|
||||
AdminPages[管理场景页面<br/>users/roles/permissions/viewports/monitoring]
|
||||
end
|
||||
|
||||
subgraph Shared["共享层(packages/)"]
|
||||
UITokens[ui-tokens<br/>三层设计令牌]
|
||||
UIComponents[ui-components<br/>shadcn + A11y + ErrorBoundary]
|
||||
Contracts[contracts<br/>Permissions 常量 + 类型]
|
||||
Hooks[hooks<br/>usePermission/useAuth/useA11y]
|
||||
LibTS[shared-ts<br/>通用工具]
|
||||
end
|
||||
|
||||
subgraph Gateway["api-gateway"]
|
||||
GW[Gin 路由/鉴权/限流]
|
||||
end
|
||||
|
||||
Browser --> URL
|
||||
URL --> RootLayout
|
||||
RootLayout --> AppShell
|
||||
AppShell --> Router
|
||||
Router -->|动态加载| RemoteTeacher
|
||||
Router -->|动态加载| RemoteStudent
|
||||
Router -->|动态加载| RemoteParent
|
||||
Router -->|动态加载| RemoteAdmin
|
||||
|
||||
RemoteTeacher --> SharedDeps
|
||||
RemoteStudent --> SharedDeps
|
||||
RemoteParent --> SharedDeps
|
||||
RemoteAdmin --> SharedDeps
|
||||
|
||||
Shell --> UITokens
|
||||
Shell --> UIComponents
|
||||
Shell --> Contracts
|
||||
Shell --> Hooks
|
||||
RemoteTeacher --> UITokens
|
||||
RemoteStudent --> UITokens
|
||||
RemoteParent --> UITokens
|
||||
RemoteAdmin --> UITokens
|
||||
|
||||
AppShell -->|fetch /api/v1/iam/effective-permissions| Hooks
|
||||
Hooks -->|透传 token| GW
|
||||
RemoteTeacher -->|fetch /api/v1/teacher/*| GW
|
||||
RemoteStudent -->|fetch /api/v1/student/*| GW
|
||||
RemoteParent -->|fetch /api/v1/parent/*| GW
|
||||
RemoteAdmin -->|fetch /api/v1/iam/* + /api/v1/admin/*| GW
|
||||
```
|
||||
|
||||
### 3.1.1 MF 拓扑选型
|
||||
|
||||
| 方案 | 选否 | 理由 |
|
||||
| ------------------------------------ | ---- | --------------------------------------------------------------------------------------- |
|
||||
| 4 端独立部署 + 独立域名 + 各自 Shell | ❌ | 4 套 Shell 重复,登录态/权限/组件库要重复实现 |
|
||||
| 单 Shell + 4 Remote(**采用**) | ✅ | teacher-portal 作为 Shell 宿主,提供 AppShell + 共享依赖;其余 3 端作为 Remote 动态加载 |
|
||||
| 单一 Next.js 应用 + 4 路由组 | ❌ | 违反"微前端独立部署"目标(ADR-012) |
|
||||
|
||||
**Shell 职责**:
|
||||
|
||||
- RootLayout(字体、设计令牌、i18n Provider、TanStack QueryClientProvider、Zustand StoreProvider)
|
||||
- AppShell(左侧导航 + 主内容区 + 用户信息 + 登出)
|
||||
- 共享依赖暴露(react、react-dom、@tanstack/react-query、zustand、nuqs、ui-components、ui-tokens、contracts、hooks)
|
||||
- 路由表(4 端路由前缀:`/teacher/*`、`/student/*`、`/parent/*`、`/admin/*`)
|
||||
- 登录页(统一登录入口,按角色重定向到对应 portal)
|
||||
|
||||
**Remote 职责**:
|
||||
|
||||
- 各场景域页面(page.tsx)
|
||||
- 各场景域专属组件
|
||||
- 各场景域专属 Zustand slice
|
||||
- 通过 MF 共享 Shell 暴露的依赖,避免重复加载
|
||||
|
||||
### 3.1.2 MF 配置(next.config.js)
|
||||
|
||||
```javascript
|
||||
// teacher-portal/next.config.js(Shell)
|
||||
const NextFederationPlugin = require("@module-federation/nextjs-mf");
|
||||
|
||||
const remotes = (isServer) => ({
|
||||
student: `student_app@http://localhost:3001/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
|
||||
parent: `parent_app@http://localhost:3002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
|
||||
admin: `admin_app@http://localhost:3003/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
webpack(config, { isServer }) {
|
||||
config.plugins.push(
|
||||
new NextFederationPlugin({
|
||||
name: "teacher_app",
|
||||
filename: "static/chunks/remoteEntry.js",
|
||||
remotes: remotes(isServer),
|
||||
exposes: {
|
||||
"./AppShell": "./src/components/AppShell",
|
||||
"./shared-deps": "./src/shared/deps",
|
||||
},
|
||||
shared: {
|
||||
react: { singleton: true, requiredVersion: "^18.3.0" },
|
||||
"react-dom": { singleton: true, requiredVersion: "^18.3.0" },
|
||||
"@tanstack/react-query": { singleton: true },
|
||||
zustand: { singleton: true },
|
||||
nuqs: { singleton: true },
|
||||
},
|
||||
extraOptions: { exposePages: false },
|
||||
}),
|
||||
);
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/v1/:path*",
|
||||
destination: `${process.env.API_GATEWAY_URL || "http://localhost:8080"}/api/v1/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
> Remote 端配置对称:`name: 'student_app'`,`exposes: { './pages': './src/pages' }`,`remotes: { teacher: 'teacher_app@...' }`。
|
||||
|
||||
## 3.2 领域模型(前端视角)
|
||||
|
||||
前端不持有业务聚合根,仅持有"视图模型"(ViewModel)和"会话状态"。
|
||||
|
||||
### 3.2.1 会话状态(Session)
|
||||
|
||||
```typescript
|
||||
interface Session {
|
||||
user: UserInfo; // { id, email, name, roles, permissions, dataScope }
|
||||
tokens: { accessToken: string; refreshToken: string };
|
||||
viewports: ViewportItem[]; // L1 导航视口
|
||||
expiresAt: number; // access token 过期时间戳
|
||||
}
|
||||
```
|
||||
|
||||
存储:Zustand sessionSlice(L3)+ localStorage 持久化(刷新恢复)+ TanStack Query 缓存 `['session']`(L2)。
|
||||
|
||||
### 3.2.2 视口模型(Viewport)
|
||||
|
||||
```typescript
|
||||
interface ViewportItem {
|
||||
key: string; // 'dashboard' | 'classes' | ...
|
||||
label: string; // i18n key 或显式文案
|
||||
route: string; // '/teacher/dashboard'
|
||||
icon: string | null; // 图标 key(按需)
|
||||
sortOrder: number; // 排序
|
||||
requiredPermission: string | null; // 'CLASSES_READ' 等
|
||||
scope: "teacher" | "student" | "parent" | "admin"; // 标记归属哪个 portal
|
||||
}
|
||||
```
|
||||
|
||||
来源:`GET /api/v1/{scope}/viewports`(BFF 聚合 iam 视口配置)。AppShell 按 `scope` 过滤渲染对应 portal 的导航。
|
||||
|
||||
### 3.2.3 权限模型(Permission)
|
||||
|
||||
```typescript
|
||||
interface PermissionState {
|
||||
permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]
|
||||
dataScope: DataScope; // L0-L5
|
||||
hasPermission: (perm: string) => boolean;
|
||||
hasAnyPermission: (perms: string[]) => boolean;
|
||||
hasAllPermissions: (perms: string[]) => boolean;
|
||||
}
|
||||
```
|
||||
|
||||
来源:`GET /api/v1/iam/effective-permissions` → `{ permissions, viewports, dataScope }`。Redis 缓存 5min(iam 侧),前端 TanStack Query 缓存 5min,角色变更主动 invalidate。
|
||||
|
||||
## 3.3 数据模型(前端)
|
||||
|
||||
前端无数据库,仅有缓存层:
|
||||
|
||||
| 数据类型 | 存储 | TTL | 失效策略 |
|
||||
| ----------------------- | ---------------------- | --------------------------- | -------------------------------------- |
|
||||
| Session(token + user) | localStorage + Zustand | access 15min / refresh 7day | 401 自动 refresh,refresh 失败跳登录 |
|
||||
| 权限列表 | TanStack Query cache | 5min | 角色变更事件 invalidate |
|
||||
| 视口列表 | TanStack Query cache | 5min | 同上 |
|
||||
| 班级/年级列表 | TanStack Query cache | 5min | staleTime 5min,mutation 后 invalidate |
|
||||
| 教学资源详情 | TanStack Query cache | 30s | staleTime 30s |
|
||||
| 学情宽表 | TanStack Query cache | 30s | staleTime 30s(实时性由 BFF 决定) |
|
||||
| URL 状态(分页/筛选) | nuqs | — | 永久(可分享) |
|
||||
| 表单临时态 | react-hook-form | — | 卸载即销毁 |
|
||||
|
||||
## 3.4 API 设计(前端 → 后端)
|
||||
|
||||
前端不设计后端 API,仅声明消费的端点。详见 §1.3.1。
|
||||
|
||||
### 3.4.1 统一 API 请求层(lib/api.ts)
|
||||
|
||||
```typescript
|
||||
// packages/shared-ts/src/api-client.ts(共享)
|
||||
interface ApiClientOptions {
|
||||
baseUrl?: string; // 默认 ''(走 Next.js rewrites)
|
||||
getToken?: () => string | null;
|
||||
onUnauthorized?: () => void; // 401 → refresh → 重试 / 跳登录
|
||||
onError?: (error: ApiError) => void; // 全局 toast
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
async get<T>(path: string, query?: Record<string, string>): Promise<T>;
|
||||
async post<T>(path: string, body: unknown): Promise<T>;
|
||||
async put<T>(path: string, body: unknown): Promise<T>;
|
||||
async delete<T>(path: string): Promise<T>;
|
||||
async sse<T>(path: string, body: unknown): AsyncIterable<T>; // AI 流式
|
||||
}
|
||||
|
||||
// 错误结构
|
||||
interface ApiError {
|
||||
code: string; // 'IAM_INVALID_CREDENTIALS'
|
||||
message: string; // 已 i18n 翻译或后端原文
|
||||
details?: unknown;
|
||||
httpStatus: number;
|
||||
}
|
||||
```
|
||||
|
||||
**职责**:
|
||||
|
||||
- 自动注入 `Authorization: Bearer ${token}`
|
||||
- 401 自动 refresh token 一次,失败调 `onUnauthorized`
|
||||
- 解析 `ActionState`,success=false 抛 `ApiError`
|
||||
- 按 `error.code` 前缀路由 i18n key
|
||||
- 全局错误 toast(除 401)
|
||||
- 请求/响应 trace_id 透传(从响应头 `X-Request-Id` 提取)
|
||||
|
||||
### 3.4.2 TanStack Query 约定
|
||||
|
||||
```typescript
|
||||
// Query Key 命名:[scope, resource, ...args]
|
||||
queryKey: ["teacher", "classes", { gradeId }];
|
||||
queryKey: ["teacher", "exams", classId];
|
||||
queryKey: ["session", "effective-permissions"];
|
||||
queryKey: ["session", "viewports", "teacher"];
|
||||
|
||||
// Mutation 约定
|
||||
const mutation = useMutation({
|
||||
mutationFn: (input) => api.post("/api/v1/classes", input),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: ["teacher", "classes"] }),
|
||||
onError: (e: ApiError) => toast.error(e.message),
|
||||
});
|
||||
```
|
||||
|
||||
## 3.5 事件设计
|
||||
|
||||
前端不发布 Kafka 事件,仅消费 WebSocket 推送(P5)和 Server-Sent Events(AI 流式)。
|
||||
|
||||
### 3.5.1 WebSocket 推送(P5)
|
||||
|
||||
| 事件 | 触发 | 前端动作 |
|
||||
| ----------------------- | ------------ | --------------------------------------- |
|
||||
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1 |
|
||||
| `ExamPublished` | 教师发布考试 | 学生端 toast + dashboard invalidate |
|
||||
| `GradeRecorded` | 教师录入成绩 | 学生/家长端 toast + 成绩列表 invalidate |
|
||||
| `HomeworkSubmitted` | 学生提交作业 | 教师端 toast + 作业批改列表 invalidate |
|
||||
|
||||
### 3.5.2 SSE 流式(P5 AI 辅助出题)
|
||||
|
||||
```
|
||||
GET /api/v1/ai/generate-questions (SSE)
|
||||
data: {"delta": "题目"}\n\n
|
||||
data: {"delta": "A. option1"}\n\n
|
||||
data: {"done": true}\n\n
|
||||
```
|
||||
|
||||
前端用 `AsyncIterable<T>` 消费,Tiptap 逐字插入。
|
||||
|
||||
## 3.6 横切关注点对齐清单
|
||||
|
||||
### 3.6.1 权限(前端等价)
|
||||
|
||||
| 端 | 路由 | requiredPermission |
|
||||
| -------------- | ------------------------------ | ------------------------ |
|
||||
| teacher-portal | `/teacher/dashboard` | `TEACHER_DASHBOARD_VIEW` |
|
||||
| teacher-portal | `/teacher/classes` | `CLASSES_READ` |
|
||||
| teacher-portal | `/teacher/classes/new` | `CLASSES_CREATE` |
|
||||
| teacher-portal | `/teacher/exams` | `EXAMS_READ` |
|
||||
| teacher-portal | `/teacher/exams/new` | `EXAMS_CREATE` |
|
||||
| teacher-portal | `/teacher/homework` | `HOMEWORK_READ` |
|
||||
| teacher-portal | `/teacher/homework/:id/grade` | `HOMEWORK_GRADE` |
|
||||
| teacher-portal | `/teacher/grades` | `GRADES_READ` |
|
||||
| teacher-portal | `/teacher/ai-assist` | `AI_GENERATE` |
|
||||
| student-portal | `/student/dashboard` | `STUDENT_DASHBOARD_VIEW` |
|
||||
| student-portal | `/student/homework` | `HOMEWORK_READ_OWN` |
|
||||
| student-portal | `/student/homework/:id/submit` | `HOMEWORK_SUBMIT` |
|
||||
| student-portal | `/student/diagnostic` | `DIAGNOSTIC_READ_OWN` |
|
||||
| parent-portal | `/parent/dashboard` | `PARENT_DASHBOARD_VIEW` |
|
||||
| parent-portal | `/parent/children` | `PARENT_CHILDREN_VIEW` |
|
||||
| parent-portal | `/parent/grades` | `GRADES_READ_CHILD` |
|
||||
| admin-portal | `/admin/users` | `IAM_USER_READ` |
|
||||
| admin-portal | `/admin/users/new` | `IAM_USER_CREATE` |
|
||||
| admin-portal | `/admin/roles` | `IAM_ROLE_READ` |
|
||||
| admin-portal | `/admin/permissions` | `IAM_PERMISSION_READ` |
|
||||
| admin-portal | `/admin/viewports` | `IAM_VIEWPORT_READ` |
|
||||
| admin-portal | `/admin/monitoring` | `ADMIN_MONITORING_VIEW` |
|
||||
|
||||
> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`(待建立,coord 负责 shared-ts,ai07 负责调用)。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。
|
||||
|
||||
### 3.6.2 错误码清单(前端 i18n 路由)
|
||||
|
||||
| 前缀 | 来源服务 | i18n key 模式 |
|
||||
| ------------ | -------------------------- | ------------------------ |
|
||||
| `IAM_*` | iam | `iam.error.{{code}}` |
|
||||
| `CORE_EDU_*` | core-edu | `coreEdu.error.{{code}}` |
|
||||
| `CLASSES_*` | core-edu/classes | `classes.error.{{code}}` |
|
||||
| `CONTENT_*` | content | `content.error.{{code}}` |
|
||||
| `MSG_*` | msg | `msg.error.{{code}}` |
|
||||
| `AI_*` | ai | `ai.error.{{code}}` |
|
||||
| `BFF_*` | teacher/student/parent-bff | `bff.error.{{code}}` |
|
||||
| `GW_*` | api-gateway | `gateway.error.{{code}}` |
|
||||
| `NETWORK_*` | 前端网络层 | `network.error.{{code}}` |
|
||||
|
||||
### 3.6.3 Logger
|
||||
|
||||
```typescript
|
||||
// packages/shared-ts/src/logger.ts
|
||||
interface Logger {
|
||||
info(msg: string, meta?: Record<string, unknown>): void;
|
||||
warn(msg: string, meta?: Record<string, unknown>): void;
|
||||
error(msg: string, meta?: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
// 实现:开发环境 console + 结构化;生产环境 → Sentry(P6)
|
||||
// 必含字段:trace_id(从响应头提取)、user_id、scope、path
|
||||
```
|
||||
|
||||
### 3.6.4 Metrics(Web Vitals)
|
||||
|
||||
| 指标 | 类型 | 上报 |
|
||||
| ----------------------------- | ---- | --------------------------------------------------- |
|
||||
| `teacher_portal_lcp_seconds` | LCP | `next/web-vitals` → `POST /api/v1/admin/web-vitals` |
|
||||
| `teacher_portal_cls` | CLS | 同上 |
|
||||
| `teacher_portal_fid_seconds` | FID | 同上 |
|
||||
| `teacher_portal_ttfb_seconds` | TTFB | 同上 |
|
||||
|
||||
P6 接入,P2-P5 暂缓。
|
||||
|
||||
### 3.6.5 Tracer(OTel browser SDK,P6)
|
||||
|
||||
```typescript
|
||||
// packages/shared-ts/src/tracer.ts
|
||||
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
||||
// BatchSpanProcessor → OTLP exporter → collector → Tempo
|
||||
// 自动埋点:fetch、XMLHttpRequest、document load、user interaction
|
||||
```
|
||||
|
||||
### 3.6.6 健康检查
|
||||
|
||||
| 端点 | 用途 | 实现 |
|
||||
| ----------------- | ---------------------- | -------------------------------------------------------------- |
|
||||
| `GET /api/health` | Dockerfile HEALTHCHECK | Next.js Route Handler,返回 `{ status: 'ok', ts: Date.now() }` |
|
||||
| `GET /api/ready` | K8s readinessProbe | 检查 `process.env.API_GATEWAY_URL` 可达 + 内存 < 阈值 |
|
||||
|
||||
### 3.6.7 优雅关闭
|
||||
|
||||
Next.js 无长连接(除 SSE/WS),无需特殊处理。SSE/WS 在 P5 由 push-gateway 管理,前端断线自动重连。
|
||||
|
||||
## 3.7 共享组件库(packages/ui-components/,待建立)
|
||||
|
||||
| 组件 | 用途 | 来源 |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `AppShell` | 左侧栏 + 主内容区布局 | teacher-portal 现有 → 抽取共享 |
|
||||
| `RequirePermission` | L3 组件级视口控制(无权限不渲染 children) | 新建 |
|
||||
| `ErrorBoundary` | React 渲染异常兜底(fallback UI) | 新建 |
|
||||
| `Loading` | 骨架屏(Skeleton) | 新建 |
|
||||
| `Empty` | 空态(插画 + 文案 + CTA) | 新建 |
|
||||
| `Modal` / `Dialog` | 全局 Modal(ModalRoot + Zustand ui-store) | shadcn/ui |
|
||||
| `Toast` | 全局 toast(错误/成功/警告) | shadcn/ui sonner |
|
||||
| `Button` / `Input` / `Select` / `Textarea` | 基础表单 | shadcn/ui |
|
||||
| `DataTable` | 表格(排序/分页/筛选) | shadcn/ui + TanStack Table |
|
||||
| `Chart` | 图表封装(recharts) | 新建 |
|
||||
| `A11y` 工具集 | useA11yId / mergeA11yProps / describeInput / focus-trap / skip-link / visually-hidden / aria-status | 迁移指南 §7.7 |
|
||||
| `Form` | react-hook-form + zodResolver 封装 | 新建 |
|
||||
|
||||
## 3.8 共享 Hooks(packages/hooks/,待建立)
|
||||
|
||||
| Hook | 职责 |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| `useAuth()` | 会话状态(user/token/refresh/login/logout) |
|
||||
| `usePermission()` | 权限查询(hasPermission/hasAny/hasAll + dataScope) |
|
||||
| `useViewports(scope)` | 视口列表(按 scope 过滤) |
|
||||
| `useApi()` | ApiClient 实例(注入 token + 401 处理) |
|
||||
| `useA11yId()` | 唯一 ARIA ID 生成 |
|
||||
| `useAriaLive()` | aria-live 区域管理 |
|
||||
| `useToast()` | 全局 toast(Zustand ui-store) |
|
||||
|
||||
## 3.9 设计令牌三层(packages/ui-tokens/,待建立)
|
||||
|
||||
```
|
||||
packages/ui-tokens/
|
||||
├─ primitive.css # Layer 1 原始色板/字号/间距/阴影
|
||||
├─ semantic-light.css # Layer 2 语义令牌(亮色)
|
||||
├─ semantic-dark.css # Layer 2 语义令牌(暗色)
|
||||
├─ tailwind-theme.css # Layer 3 @theme inline 暴露 bg-*/text-*/font-*
|
||||
└─ package.json
|
||||
```
|
||||
|
||||
**强制规则**(project_rules §3.10):
|
||||
|
||||
- 禁止 `#hex` 字面量(ESLint `no-restricted-syntax`)
|
||||
- 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(ESLint `design-tokens/no-hardcoded-fonts`)
|
||||
- 禁止 `font-size: Npx`(用 `var(--font-size-1~9)`)
|
||||
- 禁止 Tailwind 任意值 `w-[Npx]`(用 `--space-*` 或默认阶梯)
|
||||
|
||||
**令牌命名**(迁移指南 §7.1):
|
||||
|
||||
| Layer 1 Primitive | Layer 2 Semantic | Layer 3 Tailwind |
|
||||
| ------------------ | ------------------ | ---------------- |
|
||||
| `--color-blue-500` | `--color-accent` | `bg-accent` |
|
||||
| `--font-size-3` | `--font-size-body` | `text-body` |
|
||||
| `--space-4` | `--space-md` | `p-md` |
|
||||
|
||||
---
|
||||
|
||||
# 4 端差异化对比表
|
||||
|
||||
## 4.1 整体差异
|
||||
|
||||
| 维度 | teacher-portal | student-portal | parent-portal | admin-portal |
|
||||
| -------------- | -------------------------------------------------------- | -------------- | ------------- | --------------------------- |
|
||||
| MF 角色 | Shell + Remote | Remote | Remote | Remote |
|
||||
| 路由前缀 | `/teacher/*` | `/student/*` | `/parent/*` | `/admin/*` |
|
||||
| 端口(dev) | 3000 | 3001 | 3002 | 3003 |
|
||||
| 对接 BFF | teacher-bff | student-bff | parent-bff | teacher-bff 复用 + iam 直连 |
|
||||
| 默认角色 | teacher / head_teacher / grade_director / subject_leader | student | parent | school_admin / system_admin |
|
||||
| DataScope 默认 | L1-L5(按角色) | L0 | L0 | L3-L5 |
|
||||
| 推送消费 | ✅ WebSocket | ✅ WebSocket | ✅ WebSocket | ❌ 轮询 |
|
||||
| AI 辅助 | ✅ 出题/备课/分析 | ❌ | ❌ | ❌ |
|
||||
| 富文本编辑 | ✅ Tiptap(备课/出题/反馈) | ❌ | ❌ | ❌ |
|
||||
| 多子女切换 | ❌ | ❌ | ✅ | ❌ |
|
||||
| 用户管理 | ❌ | ❌ | ❌ | ✅ |
|
||||
| 角色权限配置 | ❌ | ❌ | ❌ | ✅ |
|
||||
| 平台监控 | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
## 4.2 L1 导航菜单差异
|
||||
|
||||
| 端 | 菜单项(视口) |
|
||||
| -------------- | -------------------------------------------------------------------------------------------- |
|
||||
| teacher-portal | Dashboard、班级管理、考试管理、作业管理、成绩查询、备课(P5)、AI 辅助(P5)、知识图谱(P4) |
|
||||
| student-portal | Dashboard、我的作业、我的考试、学情诊断(P4)、错题本(P4)、通知中心(P5) |
|
||||
| parent-portal | Dashboard、子女切换、成绩查看、作业查看、通知中心(P5)、通知偏好设置 |
|
||||
| admin-portal | Dashboard、用户管理、角色管理、权限管理、视口配置、组织管理、平台监控 |
|
||||
|
||||
## 4.3 L2 路由表差异
|
||||
|
||||
### teacher-portal
|
||||
|
||||
| 路由 | 页面 | 权限 |
|
||||
| ----------------------------- | -------------- | ------------------------ |
|
||||
| `/teacher/dashboard` | 教师仪表盘 | `TEACHER_DASHBOARD_VIEW` |
|
||||
| `/teacher/classes` | 班级列表 | `CLASSES_READ` |
|
||||
| `/teacher/classes/:id` | 班级详情 | `CLASSES_READ` |
|
||||
| `/teacher/classes/new` | 新建班级 | `CLASSES_CREATE` |
|
||||
| `/teacher/exams` | 考试列表 | `EXAMS_READ` |
|
||||
| `/teacher/exams/:id` | 考试详情 | `EXAMS_READ` |
|
||||
| `/teacher/exams/new` | 新建考试 | `EXAMS_CREATE` |
|
||||
| `/teacher/homework` | 作业列表 | `HOMEWORK_READ` |
|
||||
| `/teacher/homework/:id/grade` | 批改作业 | `HOMEWORK_GRADE` |
|
||||
| `/teacher/grades` | 成绩查询 | `GRADES_READ` |
|
||||
| `/teacher/lesson-prep` | 备课(P5) | `LESSON_PREP_VIEW` |
|
||||
| `/teacher/ai-assist` | AI 辅助(P5) | `AI_GENERATE` |
|
||||
| `/teacher/knowledge-graph` | 知识图谱(P4) | `CONTENT_READ` |
|
||||
|
||||
### student-portal
|
||||
|
||||
| 路由 | 页面 | 权限 |
|
||||
| ------------------------------ | -------------- | ------------------------ |
|
||||
| `/student/dashboard` | 学生仪表盘 | `STUDENT_DASHBOARD_VIEW` |
|
||||
| `/student/homework` | 我的作业 | `HOMEWORK_READ_OWN` |
|
||||
| `/student/homework/:id/submit` | 提交作业 | `HOMEWORK_SUBMIT` |
|
||||
| `/student/exams` | 我的考试 | `EXAMS_READ_OWN` |
|
||||
| `/student/exams/:id/take` | 作答考试 | `EXAMS_TAKE` |
|
||||
| `/student/diagnostic` | 学情诊断(P4) | `DIAGNOSTIC_READ_OWN` |
|
||||
| `/student/weakness` | 错题本(P4) | `WEAKNESS_READ_OWN` |
|
||||
| `/student/notifications` | 通知中心(P5) | `NOTIFICATION_READ_OWN` |
|
||||
|
||||
### parent-portal
|
||||
|
||||
| 路由 | 页面 | 权限 |
|
||||
| ----------------------- | -------------- | --------------------------- |
|
||||
| `/parent/dashboard` | 家长仪表盘 | `PARENT_DASHBOARD_VIEW` |
|
||||
| `/parent/children` | 子女列表 | `PARENT_CHILDREN_VIEW` |
|
||||
| `/parent/grades` | 子女成绩 | `GRADES_READ_CHILD` |
|
||||
| `/parent/homework` | 子女作业 | `HOMEWORK_READ_CHILD` |
|
||||
| `/parent/notifications` | 通知中心(P5) | `NOTIFICATION_READ_OWN` |
|
||||
| `/parent/preferences` | 通知偏好 | `PARENT_PREFERENCES_UPDATE` |
|
||||
|
||||
### admin-portal
|
||||
|
||||
| 路由 | 页面 | 权限 |
|
||||
| --------------------- | ---------- | ----------------------- |
|
||||
| `/admin/dashboard` | 管理仪表盘 | `ADMIN_DASHBOARD_VIEW` |
|
||||
| `/admin/users` | 用户管理 | `IAM_USER_READ` |
|
||||
| `/admin/users/new` | 新建用户 | `IAM_USER_CREATE` |
|
||||
| `/admin/users/:id` | 用户编辑 | `IAM_USER_UPDATE` |
|
||||
| `/admin/roles` | 角色管理 | `IAM_ROLE_READ` |
|
||||
| `/admin/permissions` | 权限管理 | `IAM_PERMISSION_READ` |
|
||||
| `/admin/viewports` | 视口配置 | `IAM_VIEWPORT_READ` |
|
||||
| `/admin/organization` | 组织管理 | `ORG_MANAGE` |
|
||||
| `/admin/monitoring` | 平台监控 | `ADMIN_MONITORING_VIEW` |
|
||||
|
||||
## 4.4 L3 组件级差异
|
||||
|
||||
| 组件 | teacher | student | parent | admin |
|
||||
| ---------------------------------- | -------------------- | ------------------ | ------------------ | -------------------- |
|
||||
| `AppShell`(左栏+主区) | ✅ | ✅(复用 Shell) | ✅(复用 Shell) | ✅(复用 Shell) |
|
||||
| `RequirePermission` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `ErrorBoundary` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `Loading` / `Empty` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `DataTable` | ✅(班级/考试列表) | ✅(作业列表) | ✅(成绩列表) | ✅(用户列表) |
|
||||
| `Form` | ✅(创建班级/考试) | ✅(提交作业) | ✅(通知偏好) | ✅(用户/角色 CRUD) |
|
||||
| `Chart` | ✅(班级成绩分布) | ✅(个人学情趋势) | ✅(子女成绩趋势) | ✅(平台监控) |
|
||||
| `RichTextEditor`(Tiptap) | ✅(备课/出题/反馈) | ❌ | ❌ | ❌ |
|
||||
| `ChildSwitcher` | ❌ | ❌ | ✅ | ❌ |
|
||||
| `ExamTaking`(倒计时+自动保存) | ❌ | ✅ | ❌ | ❌ |
|
||||
| `SSEViewer`(AI 流式) | ✅ | ❌ | ❌ | ❌ |
|
||||
| `UserManagementTable` | ❌ | ❌ | ❌ | ✅ |
|
||||
| `RolePermissionMatrix` | ❌ | ❌ | ❌ | ✅ |
|
||||
| `ViewportConfigEditor` | ❌ | ❌ | ❌ | ✅ |
|
||||
| `PlatformMonitor`(Grafana embed) | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
## 4.5 L4 数据层差异
|
||||
|
||||
| 端 | 主要数据来源 | 缓存策略 |
|
||||
| -------------- | -------------------------------------------------------- | --------------------------------- |
|
||||
| teacher-portal | teacher-bff(聚合 iam + core-edu + content + data-ana) | 5-30s 短缓存 |
|
||||
| student-portal | student-bff(聚合 iam + core-edu + data-ana) | 5-30s 短缓存,作业列表 30s |
|
||||
| parent-portal | parent-bff(聚合 iam + core-edu + data-ana,含子女关联) | 5-30s 短缓存,子女切换 invalidate |
|
||||
| admin-portal | iam 直连 + teacher-bff 复用 | 5min 长缓存(管理数据低频变) |
|
||||
|
||||
---
|
||||
|
||||
# 与其他模块的交互点(契约清单)
|
||||
|
||||
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
|
||||
| ------ | ------------ | ------------------ | --------------------------------------------------- | ---------------------------- | ---- |
|
||||
| 调用 | api-gateway | HTTP/REST | `/api/v1/*` 代理 | 全部业务请求 | P1+ |
|
||||
| 调用 | push-gateway | WebSocket | `ws://push-gateway/ws` | 实时推送 | P5 |
|
||||
| 调用 | ai | SSE | `GET /api/v1/ai/generate-questions` | AI 流式出题 | P5 |
|
||||
| 被调用 | — | — | — | 前端不暴露接口给其他服务 | — |
|
||||
| 消费 | teacher-bff | HTTP(经 Gateway) | `GET /teacher/viewports` 等 | 教师场景聚合 | P2+ |
|
||||
| 消费 | student-bff | HTTP(经 Gateway) | `GET /student/viewports` 等 | 学生场景聚合 | P3+ |
|
||||
| 消费 | parent-bff | HTTP(经 Gateway) | `GET /parent/viewports` 等 | 家长场景聚合 | P4+ |
|
||||
| 消费 | iam | HTTP(经 Gateway) | `/iam/*` | 登录/权限/视口/用户管理 | P2+ |
|
||||
| 消费 | core-edu | HTTP(经 Gateway) | `/classes/*` `/exams/*` `/homework/*` `/grades/*` | 教学核心 | P2+ |
|
||||
| 消费 | content | HTTP(经 Gateway) | `/textbooks/*` `/knowledge-points/*` `/questions/*` | 内容资源 | P4+ |
|
||||
| 消费 | data-ana | HTTP(经 Gateway) | `/analytics/*` | 学情分析 | P4+ |
|
||||
| 消费 | msg | HTTP(经 Gateway) | `/notifications/*` | 通知中心 | P5+ |
|
||||
| 依赖 | coord 维护 | — | `packages/shared-proto` | TS 类型(仅 contracts 部分) | P1+ |
|
||||
| 依赖 | coord 维护 | — | `packages/shared-ts`(待建) | ApiClient/Logger/通用工具 | P2+ |
|
||||
| 依赖 | ai07 维护 | — | `packages/ui-tokens`(待建) | 三层设计令牌 | P2+ |
|
||||
| 依赖 | ai07 维护 | — | `packages/ui-components`(待建) | shadcn + 共享组件 | P2+ |
|
||||
| 依赖 | ai07 维护 | — | `packages/hooks`(待建) | usePermission/useAuth 等 | P2+ |
|
||||
| 依赖 | coord 维护 | — | `packages/contracts`(待建) | Permissions 常量 + 类型 | P2+ |
|
||||
|
||||
> **proto 不直接消费**:前端不调用 gRPC,BFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量(TS 文件,非 proto 生成)。
|
||||
|
||||
---
|
||||
|
||||
# 风险与假设
|
||||
|
||||
## 8.1 假设
|
||||
|
||||
1. **假设 coord 建立 `packages/shared-ts`、`packages/contracts`**:包含 ApiClient、Logger、Permissions 常量、通用类型。若 coord 未建立,ai07 自行在 `apps/teacher-portal/src/shared/` 内实现,后续提取到 packages。
|
||||
2. **假设 ai02 iam 提供 `GET /iam/effective-permissions`**:返回 `{ permissions, viewports, dataScope }`。当前已实现(known-issues §2.3 iam)。
|
||||
3. **假设 ai03 teacher-bff 提供 `GET /teacher/viewports`**:返回 L1 导航视口。当前已实现。
|
||||
4. **假设 ai03 core-edu classes 模块维持 `ActionState` 响应结构**:前端 API 请求层依赖此契约。
|
||||
5. **假设 Next.js 14+ Module Federation 2.0 稳定**:`@module-federation/nextjs-mf` 在 Next.js App Router 下可用。若不稳定,降级为 4 端独立部署 + 各自 Shell(重复实现 AppShell)。
|
||||
|
||||
## 8.2 技术风险
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
| ------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| MF SSR 对齐复杂 | Remote 在 SSR 时需 Shell 提供上下文 | 优先 CSR,SSR 仅用于首屏 dashboard;MF 2.0 支持 SSR |
|
||||
| 共享依赖版本漂移 | Remote 与 Shell 的 react/react-dom 版本不一致导致运行时错误 | MF `shared.singleton: true` + CI 检查版本对齐 |
|
||||
| Token 刷新竞态 | 多请求同时 401 触发多次 refresh | ApiClient 全局单例 + refresh promise 复用 |
|
||||
| 权限缓存陈旧 | 角色变更后前端 5min 内仍用旧权限 | iam 角色变更发 Kafka 事件 → msg 推送 WebSocket → 前端 invalidate |
|
||||
| 设计令牌迁移破坏现有样式 | teacher-portal 现有硬编码令牌迁移到三层模型后样式漂移 | 灰度迁移:先建 ui-tokens 包,teacher-portal 引入但不删除旧 globals.css,验证后切换 |
|
||||
| 4 端独立部署运维成本 | 4 个 Next.js 实例 = 4 倍内存 | Shell + 3 Remote 共享 node_modules(MF 运行时共享),实际内存增量 < 2x |
|
||||
| TanStack Query 缓存膨胀 | 长时间使用后缓存项过多 | `gcTime` 5min + `staleTime` 按数据类型分级 |
|
||||
|
||||
## 8.3 未决设计决策(需 coord 仲裁)
|
||||
|
||||
1. **packages 归属**:`ui-tokens` / `ui-components` / `hooks` 是 ai07 维护还是 coord 维护?建议:ai07 维护(前端专属),coord 仅维护 `shared-ts` / `contracts`(跨语言/跨服务)。
|
||||
2. **GraphQL vs REST**:004 §11.3 提到 BFF GraphQL Yoga + DataLoader,但当前 teacher-bff 实现为 REST。前端 API 请求层是否需要 GraphQL client(urql/apollo)?建议:P2-P3 用 REST,P4 起若 BFF 切 GraphQL 再引入 urql。
|
||||
3. **i18n key 命名**:`iam.error.IAM_INVALID_CREDENTIALS` 还是 `error.iam.invalid_credentials`?建议:`error.{{service}}.{{code_snake_case}}`,与错误码前缀对齐。
|
||||
4. **MF 暴露粒度**:Shell 暴露整个 AppShell 还是暴露更细粒度的组件(Sidebar、Header、Content)?建议:暴露 AppShell 整体 + 各 Remote 自行决定内部布局。
|
||||
|
||||
---
|
||||
|
||||
# coord 交叉审查所需信息
|
||||
|
||||
## 9.1 端口矩阵(4 端)
|
||||
|
||||
| 端 | dev 端口 | 生产端口 | 备注 |
|
||||
| -------------- | -------- | -------- | ---------- |
|
||||
| teacher-portal | 3000 | 3000 | Shell 宿主 |
|
||||
| student-portal | 3001 | 3001 | Remote |
|
||||
| parent-portal | 3002 | 3002 | Remote |
|
||||
| admin-portal | 3003 | 3003 | Remote |
|
||||
|
||||
> 与 [full-stack-runbook](../standards/full-stack-runbook.md) 端口矩阵对齐:3000-3003 前端,3001-3003 已被 Grafana(3030)/其他服务避让。
|
||||
|
||||
## 9.2 依赖的共享包(需 coord 建立)
|
||||
|
||||
| 包 | 路径 | 维护方 | 内容 |
|
||||
| --------------- | ------------------------- | ------------ | ------------------------------------------------- |
|
||||
| `shared-ts` | `packages/shared-ts/` | coord | ApiClient、Logger、通用工具 |
|
||||
| `contracts` | `packages/contracts/` | coord | Permissions 常量、ActionState 类型、UserInfo 类型 |
|
||||
| `ui-tokens` | `packages/ui-tokens/` | ai07(建议) | 三层设计令牌 |
|
||||
| `ui-components` | `packages/ui-components/` | ai07(建议) | shadcn + ErrorBoundary + RequirePermission |
|
||||
| `hooks` | `packages/hooks/` | ai07(建议) | usePermission、useAuth、useViewports |
|
||||
|
||||
## 9.3 依赖的后端契约(需对应 AI 确认)
|
||||
|
||||
| 契约 | 提供方 | 当前状态 |
|
||||
| ------------------------------------------------------------------ | ---------------------------- | ------------------- |
|
||||
| `POST /iam/login`、`GET /iam/effective-permissions`、`GET /iam/me` | ai02 iam | ✅ 已实现 |
|
||||
| `GET /teacher/viewports`、`GET /teacher/dashboard` | ai03 teacher-bff | ✅ 已实现 |
|
||||
| `/classes/*` CRUD | ai03 core-edu | ✅ 已实现 |
|
||||
| `/exams/*` `/homework/*` `/grades/*` | ai03 core-edu | ✅ 已实现(P3) |
|
||||
| `/textbooks/*` `/knowledge-points/*` `/questions/*` | ai05 content | ✅ 已实现(P4) |
|
||||
| `/analytics/*` | ai06 data-ana | ✅ 已实现(P4 CDC) |
|
||||
| `GET /student/viewports` 等 | ai04 student-bff | 📐 待 ai04 设计 |
|
||||
| `GET /parent/viewports` 等 | ai04 parent-bff | 📐 待 ai04 设计 |
|
||||
| `/notifications/*` + WebSocket 推送 | ai05 msg + ai01 push-gateway | 📐 待 P5 |
|
||||
| `GET /ai/generate-questions`(SSE) | ai06 ai | 📐 待 P5 |
|
||||
|
||||
## 9.4 错误码前缀(前端 i18n 路由依赖)
|
||||
|
||||
前端不产生错误码,仅消费。需各服务确认错误码前缀不重叠:
|
||||
|
||||
| 前缀 | 服务 | 状态 |
|
||||
| ----------------------------------------------- | ---------------- | -------------------- |
|
||||
| `IAM_` | iam | ✅ ai02 已用 |
|
||||
| `CLASSES_` | core-edu/classes | ✅ 已用 |
|
||||
| `EXAMS_` / `HOMEWORK_` / `GRADES_` | core-edu | ⚠️ 待 ai03 确认 |
|
||||
| `CONTENT_` | content | ⚠️ 待 ai05 确认 |
|
||||
| `MSG_` | msg | ⚠️ 待 ai05 确认 |
|
||||
| `AI_` | ai | ⚠️ 待 ai06 确认 |
|
||||
| `BFF_TEACHER_` / `BFF_STUDENT_` / `BFF_PARENT_` | 3 BFF | ⚠️ 待 ai03/ai04 确认 |
|
||||
| `GW_` | api-gateway | ✅ ai01 已用 |
|
||||
| `NETWORK_` | 前端 | ai07 自有 |
|
||||
|
||||
## 9.5 不产生 Kafka 事件
|
||||
|
||||
前端不发布/消费 Kafka 事件。WebSocket 推送由 push-gateway 消费 Kafka 转发。
|
||||
|
||||
---
|
||||
|
||||
# 实施路线(ai07 自用)
|
||||
|
||||
## P2 收尾(teacher-portal 审计对齐)
|
||||
|
||||
1. 建 `packages/ui-tokens/`(三层设计令牌)+ `packages/ui-components/`(ErrorBoundary/RequirePermission/Loading/Empty)+ `packages/hooks/`(usePermission/useAuth)
|
||||
2. teacher-portal 引入 TanStack Query + Zustand + nuqs + react-hook-form
|
||||
3. 抽取 `lib/api.ts` 统一 API 请求层
|
||||
4. AppShell 改用 `usePermission()`,删除 `user.roles.join(", ")` 硬编码
|
||||
5. globals.css / tailwind.config.js 迁移到 ui-tokens 三层令牌
|
||||
6. 引入 next-intl + i18n key 路由
|
||||
7. 引入 ESLint flat config 自定义规则(no-hardcoded-fonts / design-tokens)
|
||||
8. 补 ErrorBoundary + /api/health route
|
||||
9. 配置 next.config.js Module Federation(Shell 角色)
|
||||
10. 补 Vitest 单测 + Playwright E2E(覆盖率 ≥ 80%)
|
||||
|
||||
## P3(student-portal)
|
||||
|
||||
1. 建 `apps/student-portal/`(Remote 角色)
|
||||
2. 配置 MF(exposes pages,remotes teacher)
|
||||
3. 实现 Dashboard + 我的作业 + 提交作业 + 我的考试 + 作答考试
|
||||
4. 复用 Shell 的 AppShell + 共享组件
|
||||
5. SSE 接入(考试作答自动保存)
|
||||
|
||||
## P4(parent-portal)
|
||||
|
||||
1. 建 `apps/parent-portal/`(Remote 角色)
|
||||
2. 实现 Dashboard + 子女切换 + 成绩查看 + 通知偏好
|
||||
3. 多子女状态管理(Zustand slice)
|
||||
|
||||
## P5(推送 + AI 接入)
|
||||
|
||||
1. teacher-portal 接入 WebSocket(push-gateway)
|
||||
2. teacher-portal AI 辅助出题(SSE + Tiptap)
|
||||
3. student/parent-portal 接入通知推送
|
||||
|
||||
## P6(admin-portal + 硬化)
|
||||
|
||||
1. 建 `apps/admin-portal/`(Remote 角色)
|
||||
2. 实现用户/角色/权限/视口/组织/监控管理
|
||||
3. Web Vitals + OTel browser SDK 接入
|
||||
4. A11y WCAG 2.2 AA 审计
|
||||
5. 性能优化(MF shared 单例验证、bundle 分析)
|
||||
|
||||
---
|
||||
|
||||
**AI Agent**: ai07 (teacher-portal / student-portal / parent-portal / admin-portal)
|
||||
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
|
||||
**Coordinator**: coord-ai
|
||||
3
apps/teacher-portal/next-env.d.ts
vendored
3
apps/teacher-portal/next-env.d.ts
vendored
@@ -1,2 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"dev": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3000",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19,6 +19,7 @@
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"eslint": "^9.0.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.6.0"
|
||||
|
||||
293
apps/teacher-portal/src/app/(app)/classes/page.tsx
Normal file
293
apps/teacher-portal/src/app/(app)/classes/page.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getToken } from "@/lib/auth";
|
||||
|
||||
interface ClassItem {
|
||||
id: string;
|
||||
name: string;
|
||||
gradeId: string;
|
||||
description?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
export default function ClassesPage() {
|
||||
const [classes, setClasses] = useState<ClassItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [gradeId, setGradeId] = useState(
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
);
|
||||
const [description, setDescription] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const authHeaders = (): Record<string, string> => {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
};
|
||||
|
||||
const fetchClasses = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/v1/classes", {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
const json: ApiResponse<ClassItem[]> = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setClasses(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || "Failed to load");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClasses();
|
||||
}, [fetchClasses]);
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
try {
|
||||
const res = await fetch("/api/v1/classes", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...authHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ name, gradeId, description }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
setError(json.error?.message || "Create failed");
|
||||
return;
|
||||
}
|
||||
setName("");
|
||||
setDescription("");
|
||||
await fetchClasses();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/classes/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: authHeaders(),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
setError(json.error?.message || "Delete failed");
|
||||
return;
|
||||
}
|
||||
await fetchClasses();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-10 py-10">
|
||||
<header className="mb-8">
|
||||
<h1
|
||||
className="text-3xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
班级管理
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
classes 域 CRUD · JWT 鉴权
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="rule-thin mb-8" />
|
||||
|
||||
<div className="grid grid-cols-12 gap-8">
|
||||
<aside className="col-span-4">
|
||||
<h2
|
||||
className="text-xl mb-4"
|
||||
style={{ fontFamily: "var(--font-serif)" }}
|
||||
>
|
||||
新建班级
|
||||
</h2>
|
||||
<div className="rule-thin mb-4" />
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
班级名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b focus:outline-none focus:border-b-2"
|
||||
style={{
|
||||
borderColor: "var(--color-rule)",
|
||||
borderRadius: "6px 6px 0 0",
|
||||
}}
|
||||
placeholder="如:高三(1)班"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
年级 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={gradeId}
|
||||
onChange={(e) => setGradeId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b text-sm font-mono"
|
||||
style={{
|
||||
borderColor: "var(--color-rule)",
|
||||
borderRadius: "6px 6px 0 0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
描述(可选)
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b resize-none"
|
||||
style={{
|
||||
borderColor: "var(--color-rule)",
|
||||
borderRadius: "6px 6px 0 0",
|
||||
}}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-white text-sm tracking-wide transition-opacity hover:opacity-90"
|
||||
style={{ background: "var(--color-accent)", borderRadius: "6px" }}
|
||||
>
|
||||
创建班级
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<section className="col-span-8">
|
||||
<div className="flex items-baseline justify-between mb-4">
|
||||
<h2 className="text-xl" style={{ fontFamily: "var(--font-serif)" }}>
|
||||
班级列表
|
||||
<span
|
||||
className="ml-2 text-sm font-sans"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{classes.length} 个
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchClasses}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<div className="rule-thin mb-6" />
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="mark-left mb-4 py-2"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
>
|
||||
<p
|
||||
className="text-sm px-3"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
加载中...
|
||||
</p>
|
||||
) : classes.length === 0 ? (
|
||||
<p
|
||||
className="text-sm italic"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
暂无班级,从左侧创建第一个
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-0">
|
||||
{classes.map((cls) => (
|
||||
<li
|
||||
key={cls.id}
|
||||
className="py-4 grid grid-cols-12 gap-4 items-baseline"
|
||||
style={{ borderBottom: "1px solid var(--color-rule)" }}
|
||||
>
|
||||
<div className="col-span-7">
|
||||
<h3
|
||||
className="text-lg"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
{cls.name}
|
||||
</h3>
|
||||
{cls.description && (
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{cls.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="col-span-3 text-xs font-mono"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{cls.id.slice(0, 8)}...
|
||||
</div>
|
||||
<div className="col-span-2 text-right">
|
||||
<button
|
||||
onClick={() => handleDelete(cls.id)}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
139
apps/teacher-portal/src/app/(app)/dashboard/page.tsx
Normal file
139
apps/teacher-portal/src/app/(app)/dashboard/page.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getToken, getUser, type UserInfo } from "@/lib/auth";
|
||||
|
||||
interface DashboardData {
|
||||
user: { success: boolean; data?: { user: UserInfo } };
|
||||
classes: { success: boolean; data?: unknown[] };
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const user = getUser();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/v1/teacher/dashboard", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
setData(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || "加载失败");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "网络错误");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const classesCount = Array.isArray(data?.classes?.data)
|
||||
? data!.classes.data.length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="px-10 py-10">
|
||||
<header className="mb-8">
|
||||
<h1
|
||||
className="text-3xl"
|
||||
style={{ fontFamily: "var(--font-serif)", color: "var(--color-ink)" }}
|
||||
>
|
||||
欢迎,{user?.name || "老师"}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
{user?.email} · 角色:{user?.roles.join(", ") || "无"} · 数据范围:
|
||||
{user?.dataScope || "-"}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="rule-thin mb-8" />
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
加载中...
|
||||
</p>
|
||||
) : error ? (
|
||||
<div
|
||||
className="mark-left py-2 mb-4"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
>
|
||||
<p className="text-sm px-3" style={{ color: "var(--color-accent)" }}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<section className="grid grid-cols-3 gap-6">
|
||||
<div
|
||||
className="p-6 border"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
>
|
||||
<p
|
||||
className="text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
班级总数
|
||||
</p>
|
||||
<p
|
||||
className="mt-3 text-4xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
{classesCount}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="p-6 border"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
>
|
||||
<p
|
||||
className="text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
权限点
|
||||
</p>
|
||||
<p
|
||||
className="mt-3 text-4xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
{user?.permissions.length ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="p-6 border"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
>
|
||||
<p
|
||||
className="text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
数据范围
|
||||
</p>
|
||||
<p
|
||||
className="mt-3 text-2xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
{user?.dataScope || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
185
apps/teacher-portal/src/app/(app)/exams/page.tsx
Normal file
185
apps/teacher-portal/src/app/(app)/exams/page.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getToken } from "@/lib/auth";
|
||||
|
||||
interface ExamItem {
|
||||
id: string;
|
||||
classId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
examDate: string;
|
||||
duration: string;
|
||||
totalScore: string;
|
||||
status: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
export default function ExamsPage() {
|
||||
const [exams, setExams] = useState<ExamItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [classId, setClassId] = useState(
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const authHeaders = (): Record<string, string> => {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
};
|
||||
|
||||
const fetchExams = useCallback(async () => {
|
||||
if (!classId.trim()) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/v1/teacher/classes/${encodeURIComponent(classId)}/exams`,
|
||||
{ headers: authHeaders() },
|
||||
);
|
||||
const json: ApiResponse<ExamItem[]> = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setExams(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || "Failed to load");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [classId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExams();
|
||||
}, [fetchExams]);
|
||||
|
||||
return (
|
||||
<div className="px-10 py-10">
|
||||
<header className="mb-8">
|
||||
<h1
|
||||
className="text-3xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
考试管理
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
core-edu 域 · BFF 聚合查询
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="rule-thin mb-8" />
|
||||
|
||||
<div className="mb-6 flex items-baseline gap-3">
|
||||
<label
|
||||
className="text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
班级 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={classId}
|
||||
onChange={(e) => setClassId(e.target.value)}
|
||||
className="flex-1 max-w-md px-3 py-2 bg-transparent border-b text-sm font-mono"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
placeholder="输入班级 UUID"
|
||||
/>
|
||||
<button
|
||||
onClick={fetchExams}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="mark-left mb-4 py-2"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
>
|
||||
<p className="text-sm px-3" style={{ color: "var(--color-accent)" }}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
加载中...
|
||||
</p>
|
||||
) : exams.length === 0 ? (
|
||||
<p
|
||||
className="text-sm italic"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
该班级下暂无考试
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-0">
|
||||
{exams.map((exam) => (
|
||||
<li
|
||||
key={exam.id}
|
||||
className="py-4 grid grid-cols-12 gap-4 items-baseline"
|
||||
style={{ borderBottom: "1px solid var(--color-rule)" }}
|
||||
>
|
||||
<div className="col-span-7">
|
||||
<h3
|
||||
className="text-lg"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
{exam.title}
|
||||
</h3>
|
||||
{exam.description && (
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{exam.description}
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
考试时间: {new Date(exam.examDate).toLocaleString("zh-CN")}
|
||||
{" · "}
|
||||
时长 {exam.duration} 分钟
|
||||
{" · "}
|
||||
满分 {exam.totalScore}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-3 text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
状态: {exam.status}
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 text-right text-xs font-mono"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{exam.id.slice(0, 8)}...
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
180
apps/teacher-portal/src/app/(app)/grades/page.tsx
Normal file
180
apps/teacher-portal/src/app/(app)/grades/page.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getToken } from "@/lib/auth";
|
||||
|
||||
interface GradeItem {
|
||||
id: string;
|
||||
studentId: string;
|
||||
examId: string | null;
|
||||
homeworkId: string | null;
|
||||
score: string;
|
||||
feedback?: string;
|
||||
gradedBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
export default function GradesPage() {
|
||||
const [items, setItems] = useState<GradeItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [examId, setExamId] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const authHeaders = (): Record<string, string> => {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
};
|
||||
|
||||
const fetchGrades = useCallback(async () => {
|
||||
if (!examId.trim()) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/v1/teacher/exams/${encodeURIComponent(examId)}/grades`,
|
||||
{ headers: authHeaders() },
|
||||
);
|
||||
const json: ApiResponse<GradeItem[]> = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setItems(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || "Failed to load");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [examId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGrades();
|
||||
}, [fetchGrades]);
|
||||
|
||||
return (
|
||||
<div className="px-10 py-10">
|
||||
<header className="mb-8">
|
||||
<h1
|
||||
className="text-3xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
成绩查询
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
core-edu 域 · BFF 聚合查询
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="rule-thin mb-8" />
|
||||
|
||||
<div className="mb-6 flex items-baseline gap-3">
|
||||
<label
|
||||
className="text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
考试 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={examId}
|
||||
onChange={(e) => setExamId(e.target.value)}
|
||||
className="flex-1 max-w-md px-3 py-2 bg-transparent border-b text-sm font-mono"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
placeholder="输入考试 UUID"
|
||||
/>
|
||||
<button
|
||||
onClick={fetchGrades}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="mark-left mb-4 py-2"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
>
|
||||
<p className="text-sm px-3" style={{ color: "var(--color-accent)" }}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
加载中...
|
||||
</p>
|
||||
) : items.length === 0 ? (
|
||||
<p
|
||||
className="text-sm italic"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{examId.trim() ? "该考试下暂无成绩" : "请输入考试 ID 查询成绩"}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-0">
|
||||
{items.map((g) => (
|
||||
<li
|
||||
key={g.id}
|
||||
className="py-4 grid grid-cols-12 gap-4 items-baseline"
|
||||
style={{ borderBottom: "1px solid var(--color-rule)" }}
|
||||
>
|
||||
<div className="col-span-6">
|
||||
<h3
|
||||
className="text-lg"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
学生: {g.studentId}
|
||||
</h3>
|
||||
{g.feedback && (
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
反馈: {g.feedback}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 text-2xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-accent)",
|
||||
}}
|
||||
>
|
||||
{g.score}
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
评分人: {g.gradedBy}
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 text-right text-xs font-mono"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{g.id.slice(0, 8)}...
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
179
apps/teacher-portal/src/app/(app)/homework/page.tsx
Normal file
179
apps/teacher-portal/src/app/(app)/homework/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getToken } from "@/lib/auth";
|
||||
|
||||
interface HomeworkItem {
|
||||
id: string;
|
||||
classId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
dueDate: string;
|
||||
status: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
export default function HomeworkPage() {
|
||||
const [items, setItems] = useState<HomeworkItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [classId, setClassId] = useState(
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const authHeaders = (): Record<string, string> => {
|
||||
const token = getToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
};
|
||||
|
||||
const fetchHomework = useCallback(async () => {
|
||||
if (!classId.trim()) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/v1/teacher/classes/${encodeURIComponent(classId)}/homework`,
|
||||
{ headers: authHeaders() },
|
||||
);
|
||||
const json: ApiResponse<HomeworkItem[]> = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setItems(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || "Failed to load");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [classId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchHomework();
|
||||
}, [fetchHomework]);
|
||||
|
||||
return (
|
||||
<div className="px-10 py-10">
|
||||
<header className="mb-8">
|
||||
<h1
|
||||
className="text-3xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
作业管理
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
core-edu 域 · BFF 聚合查询
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="rule-thin mb-8" />
|
||||
|
||||
<div className="mb-6 flex items-baseline gap-3">
|
||||
<label
|
||||
className="text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
班级 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={classId}
|
||||
onChange={(e) => setClassId(e.target.value)}
|
||||
className="flex-1 max-w-md px-3 py-2 bg-transparent border-b text-sm font-mono"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
placeholder="输入班级 UUID"
|
||||
/>
|
||||
<button
|
||||
onClick={fetchHomework}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="mark-left mb-4 py-2"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
>
|
||||
<p className="text-sm px-3" style={{ color: "var(--color-accent)" }}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm" style={{ color: "var(--color-ink-muted)" }}>
|
||||
加载中...
|
||||
</p>
|
||||
) : items.length === 0 ? (
|
||||
<p
|
||||
className="text-sm italic"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
该班级下暂无作业
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-0">
|
||||
{items.map((hw) => (
|
||||
<li
|
||||
key={hw.id}
|
||||
className="py-4 grid grid-cols-12 gap-4 items-baseline"
|
||||
style={{ borderBottom: "1px solid var(--color-rule)" }}
|
||||
>
|
||||
<div className="col-span-7">
|
||||
<h3
|
||||
className="text-lg"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
{hw.title}
|
||||
</h3>
|
||||
{hw.description && (
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{hw.description}
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
截止: {new Date(hw.dueDate).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-3 text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
状态: {hw.status}
|
||||
</div>
|
||||
<div
|
||||
className="col-span-2 text-right text-xs font-mono"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{hw.id.slice(0, 8)}...
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
apps/teacher-portal/src/app/(app)/layout.tsx
Normal file
5
apps/teacher-portal/src/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import AppShell from "@/components/AppShell";
|
||||
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
133
apps/teacher-portal/src/app/login/page.tsx
Normal file
133
apps/teacher-portal/src/app/login/page.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { login, isAuthenticated } from "@/lib/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("teacher2@edu.test");
|
||||
const [password, setPassword] = useState("Teacher@123");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated()) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await login(email, password);
|
||||
router.replace("/dashboard");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "登录失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center"
|
||||
style={{ background: "var(--bg-paper)" }}
|
||||
>
|
||||
<div className="w-full max-w-sm px-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1
|
||||
className="text-3xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
Edu 教师端
|
||||
</h1>
|
||||
<p
|
||||
className="mt-2 text-sm"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
登录到智慧教务平台
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rule-thin mb-6" />
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
邮箱
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b focus:outline-none focus:border-b-2"
|
||||
style={{
|
||||
borderColor: "var(--color-rule)",
|
||||
borderRadius: "6px 6px 0 0",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b focus:outline-none focus:border-b-2"
|
||||
style={{
|
||||
borderColor: "var(--color-rule)",
|
||||
borderRadius: "6px 6px 0 0",
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="mark-left py-2"
|
||||
style={{ borderColor: "var(--color-accent)" }}
|
||||
>
|
||||
<p
|
||||
className="text-sm px-3"
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full px-4 py-2 text-white text-sm tracking-wide transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
style={{ background: "var(--color-accent)", borderRadius: "6px" }}
|
||||
>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p
|
||||
className="mt-6 text-center text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
P2 身份阶段验证 · JWT + RBAC + 视口
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,219 +1,13 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface ClassItem {
|
||||
id: string;
|
||||
name: string;
|
||||
gradeId: string;
|
||||
description?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [classes, setClasses] = useState<ClassItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [gradeId, setGradeId] = useState('550e8400-e29b-41d4-a716-446655440000');
|
||||
const [description, setDescription] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchClasses = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch('/api/v1/classes');
|
||||
const json: ApiResponse<ClassItem[]> = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setClasses(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || 'Failed to load');
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Network error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isAuthenticated } from "@/lib/auth";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
fetchClasses();
|
||||
}, [fetchClasses]);
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
try {
|
||||
const res = await fetch('/api/v1/classes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer dev-token' },
|
||||
body: JSON.stringify({ name, gradeId, description }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
setError(json.error?.message || 'Create failed');
|
||||
return;
|
||||
}
|
||||
setName('');
|
||||
setDescription('');
|
||||
await fetchClasses();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Network error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/classes/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer dev-token' },
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
setError(json.error?.message || 'Delete failed');
|
||||
return;
|
||||
}
|
||||
await fetchClasses();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Network error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: 'var(--bg-paper)' }}>
|
||||
<header className="border-b" style={{ borderColor: 'var(--color-rule)' }}>
|
||||
<div className="max-w-6xl mx-auto px-8 py-6">
|
||||
<h1 className="text-3xl" style={{ fontFamily: 'var(--font-serif)', color: 'var(--color-ink)' }}>
|
||||
班级管理
|
||||
</h1>
|
||||
<p className="mt-1 text-sm" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
P1 黄金模板验证 - classes 域 CRUD
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-6xl mx-auto px-8 py-8 grid grid-cols-12 gap-8">
|
||||
{/* 左侧:创建表单 */}
|
||||
<aside className="col-span-4">
|
||||
<h2 className="text-xl mb-4" style={{ fontFamily: 'var(--font-serif)' }}>新建班级</h2>
|
||||
<div className="rule-thin mb-4" />
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
班级名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b focus:outline-none focus:border-b-2"
|
||||
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
|
||||
placeholder="如:高三(1)班"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
年级 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={gradeId}
|
||||
onChange={(e) => setGradeId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b text-sm font-mono"
|
||||
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
描述(可选)
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border-b resize-none"
|
||||
style={{ borderColor: 'var(--color-rule)', borderRadius: '6px 6px 0 0' }}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-white text-sm tracking-wide transition-opacity hover:opacity-90"
|
||||
style={{ background: 'var(--color-accent)', borderRadius: '6px' }}
|
||||
>
|
||||
创建班级
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
{/* 中间:班级列表(纸面)*/}
|
||||
<section className="col-span-8">
|
||||
<div className="flex items-baseline justify-between mb-4">
|
||||
<h2 className="text-xl" style={{ fontFamily: 'var(--font-serif)' }}>
|
||||
班级列表
|
||||
<span className="ml-2 text-sm font-sans" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
{classes.length} 个
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchClasses}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: 'var(--color-accent)' }}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<div className="rule-thin mb-6" />
|
||||
|
||||
{error && (
|
||||
<div className="mark-left mb-4 py-2" style={{ borderColor: 'var(--color-accent)' }}>
|
||||
<p className="text-sm" style={{ color: 'var(--color-accent)' }}>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm" style={{ color: 'var(--color-ink-muted)' }}>加载中...</p>
|
||||
) : classes.length === 0 ? (
|
||||
<p className="text-sm italic" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
暂无班级,从左侧创建第一个
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-0">
|
||||
{classes.map((cls) => (
|
||||
<li key={cls.id} className="py-4 grid grid-cols-12 gap-4 items-baseline" style={{ borderBottom: '1px solid var(--color-rule)' }}>
|
||||
<div className="col-span-7">
|
||||
<h3 className="text-lg" style={{ fontFamily: 'var(--font-serif)', color: 'var(--color-ink)' }}>
|
||||
{cls.name}
|
||||
</h3>
|
||||
{cls.description && (
|
||||
<p className="mt-1 text-sm" style={{ color: 'var(--color-ink-muted)' }}>{cls.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-3 text-xs font-mono" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
{cls.id.slice(0, 8)}...
|
||||
</div>
|
||||
<div className="col-span-2 text-right">
|
||||
<button
|
||||
onClick={() => handleDelete(cls.id)}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: 'var(--color-ink-muted)' }}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
router.replace(isAuthenticated() ? "/dashboard" : "/login");
|
||||
}, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
161
apps/teacher-portal/src/components/AppShell.tsx
Normal file
161
apps/teacher-portal/src/components/AppShell.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { getToken, getUser, logout } from "@/lib/auth";
|
||||
|
||||
interface ViewportItem {
|
||||
key: string;
|
||||
label: string;
|
||||
route: string;
|
||||
icon: string | null;
|
||||
sortOrder: string;
|
||||
requiredPermission: string | null;
|
||||
}
|
||||
|
||||
export default function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [viewports, setViewports] = useState<ViewportItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const fetchViewports = useCallback(async () => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/v1/teacher/viewports", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (res.status === 401) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
const json = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setViewports(json.data);
|
||||
}
|
||||
} catch {
|
||||
// 网络错误,保持空视口
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
fetchViewports();
|
||||
}, [router, fetchViewports]);
|
||||
|
||||
// 防止 SSR 闪烁
|
||||
if (!mounted) return null;
|
||||
|
||||
const user = getUser();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen flex"
|
||||
style={{ background: "var(--bg-paper)" }}
|
||||
>
|
||||
{/* 左侧栏:导航树 */}
|
||||
<aside
|
||||
className="w-56 flex-shrink-0 border-r relative flex flex-col"
|
||||
style={{
|
||||
borderColor: "var(--color-rule)",
|
||||
background: "var(--bg-paper)",
|
||||
}}
|
||||
>
|
||||
<div className="px-6 py-6">
|
||||
<h1
|
||||
className="text-xl"
|
||||
style={{
|
||||
fontFamily: "var(--font-serif)",
|
||||
color: "var(--color-ink)",
|
||||
}}
|
||||
>
|
||||
Edu
|
||||
</h1>
|
||||
<p
|
||||
className="text-xs mt-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
教师端
|
||||
</p>
|
||||
</div>
|
||||
<div className="rule-thin mx-6" />
|
||||
|
||||
<nav className="mt-4 px-3">
|
||||
{loading ? (
|
||||
<p
|
||||
className="text-xs px-3 py-2"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
加载导航...
|
||||
</p>
|
||||
) : (
|
||||
viewports.map((vp) => {
|
||||
const active = pathname === vp.route;
|
||||
return (
|
||||
<Link
|
||||
key={vp.key}
|
||||
href={vp.route}
|
||||
className="block px-3 py-2 text-sm transition-colors"
|
||||
style={{
|
||||
color: active ? "var(--color-accent)" : "var(--color-ink)",
|
||||
borderLeft: active
|
||||
? "2px solid var(--color-accent)"
|
||||
: "2px solid transparent",
|
||||
fontFamily: active
|
||||
? "var(--font-serif)"
|
||||
: "var(--font-inter)",
|
||||
}}
|
||||
>
|
||||
{vp.label}
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* 底部:用户信息 + 登出 */}
|
||||
<div
|
||||
className="mt-auto px-6 py-4 border-t"
|
||||
style={{ borderColor: "var(--color-rule)" }}
|
||||
>
|
||||
{user && (
|
||||
<div className="mb-2">
|
||||
<p className="text-sm" style={{ color: "var(--color-ink)" }}>
|
||||
{user.name}
|
||||
</p>
|
||||
<p
|
||||
className="text-xs"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
{user.roles.join(", ") || "无角色"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={logout}
|
||||
className="text-xs uppercase tracking-wide hover:opacity-70"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 中间:内容区(纸面) */}
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
apps/teacher-portal/src/lib/auth.ts
Normal file
77
apps/teacher-portal/src/lib/auth.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// 认证工具:token 存储 + 路由保护
|
||||
|
||||
const TOKEN_KEY = "edu_access_token";
|
||||
const USER_KEY = "edu_user_info";
|
||||
|
||||
export interface UserInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
dataScope: string;
|
||||
}
|
||||
|
||||
export function getToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearToken(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
}
|
||||
|
||||
export function getUser(): UserInfo | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const raw = localStorage.getItem(USER_KEY);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as UserInfo;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setUser(user: UserInfo): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return getToken() !== null;
|
||||
}
|
||||
|
||||
// 调用 Gateway 登录接口
|
||||
export async function login(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<{ user: UserInfo; token: string }> {
|
||||
const res = await fetch("/api/v1/iam/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
throw new Error(json.error?.message || "登录失败");
|
||||
}
|
||||
const user = json.data.user as UserInfo;
|
||||
const token = json.data.tokens.accessToken as string;
|
||||
setToken(token);
|
||||
setUser(user);
|
||||
return { user, token };
|
||||
}
|
||||
|
||||
export function logout(): void {
|
||||
clearToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,35 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "src/**/*", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"src/**/*",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
107
docs/architecture/004-p6-addendum.md
Normal file
107
docs/architecture/004-p6-addendum.md
Normal file
@@ -0,0 +1,107 @@
|
||||
## 15. P6 生产硬化(补记)
|
||||
|
||||
> 本章为 `004_architecture_impact_map.md` 的 P6 阶段补记,记录生产硬化引入的横切关注点与基础设施栈。
|
||||
> 维护规则与正文一致:源码变更后同步 `npm run arch:scan` 更新 arch.db。
|
||||
|
||||
### 15.1 横切关注点矩阵
|
||||
|
||||
| 关注点 | NestJS 服务 | Python 服务 | Go 网关 | 实现位置 |
|
||||
|--------|-------------|-------------|---------|----------|
|
||||
| 健康检查 | HealthController | health.py | /healthz | shared/health, src/health |
|
||||
| 优雅停机 | LifecycleService | FastAPI lifespan | enableShutdownHooks | shared/lifecycle |
|
||||
| 熔断 | 经 Gateway | 经 Gateway | gobreaker v2 | api-gateway/middleware |
|
||||
| 限流 | 经 Gateway | 经 Gateway | token bucket | api-gateway/middleware |
|
||||
| 链路追踪 | tracer.ts | OpenTelemetry | OpenTelemetry | shared/observability |
|
||||
| 指标 | metrics.ts | prometheus_fastapi | prometheus | shared/observability |
|
||||
| 日志 | logger.ts | structlog | zap | shared/observability |
|
||||
| 错误处理 | global-error.filter | exception handler | middleware | shared/errors |
|
||||
|
||||
### 15.2 API Gateway 中间件链
|
||||
|
||||
请求流经顺序(出向到下游服务):
|
||||
|
||||
```
|
||||
请求入口
|
||||
→ WAF(规则匹配)
|
||||
→ CORS
|
||||
→ 限流(token bucket,按 route+tenant)
|
||||
→ 熔断(gobreaker v2,按下游服务)
|
||||
→ 重试(指数退避,仅幂等)
|
||||
→ 链路追踪注入
|
||||
→ 转发到下游
|
||||
→ 响应 → 指标记录 → 返回
|
||||
```
|
||||
|
||||
### 15.3 可观测性栈
|
||||
|
||||
```
|
||||
应用层(NestJS / Python / Go)
|
||||
→ OpenTelemetry SDK(trace + metrics)
|
||||
→ OTLP exporter
|
||||
→ 采集层
|
||||
├─ Prometheus(metrics)
|
||||
├─ Tempo / Jaeger(trace)
|
||||
└─ Loki / ELK(log)
|
||||
→ 展示层
|
||||
├─ Grafana(仪表盘)
|
||||
└─ Alertmanager(告警路由)
|
||||
```
|
||||
|
||||
关键指标命名约定:
|
||||
- `http_request_duration_seconds`(histogram,含 service/route/status 维度)
|
||||
- `circuit_breaker_state`(gauge,0=Closed / 1=Open / 2=HalfOpen)
|
||||
- `rate_limiter_rejected_total`(counter)
|
||||
- `db_connections_in_use`(gauge)
|
||||
- `kafka_consumer_lag`(gauge)
|
||||
|
||||
### 15.4 安全栈
|
||||
|
||||
| 层 | 机制 | 配置位置 |
|
||||
|----|------|----------|
|
||||
| 边缘 | WAF + DDoS 防护 | Cloudflare / 入口 LB |
|
||||
| 网关 | JWT 校验 + 限流 + CORS | api-gateway |
|
||||
| 服务 | requirePermission 权限点 | modules/*/actions |
|
||||
| 数据 | 字段加密 + 审计日志 | data-access |
|
||||
| 密钥 | KMS + K8s Secret + 轮换 | deploy/k8s/secrets |
|
||||
| 传输 | mTLS(服务间,可选)+ TLS(边缘) | mesh / ingress |
|
||||
|
||||
### 15.5 健康检查约定
|
||||
|
||||
- `GET /healthz`:liveness,仅返回进程存活,不检查依赖,避免滚动重启雪崩
|
||||
- `GET /readyz`:readiness,检查 DB 等关键依赖,失败返回 503
|
||||
- K8s 探针:livenessProbe → /healthz,readinessProbe → /readyz
|
||||
- Python 服务 readyz 简化为 ok + TODO,待依赖客户端就绪后补全
|
||||
- 无需鉴权,必须在路由白名单中放行
|
||||
|
||||
### 15.6 优雅停机约定
|
||||
|
||||
- NestJS:`app.enableShutdownHooks()` 注册 SIGTERM/SIGINT 钩子
|
||||
- LifecycleService 实现 OnApplicationShutdown,按序关闭:Kafka producer → Redis → DataSource
|
||||
- K8s:`terminationGracePeriodSeconds=60`,preStop hook 可加 sleep 5s 摘流量
|
||||
- Python:FastAPI lifespan shutdown 事件,关闭连接池
|
||||
- 销毁顺序理由:先停外部消息生产(避免新事件),再关缓存,最后关 DB
|
||||
|
||||
### 15.7 灾难恢复策略
|
||||
|
||||
- 备份:CronJob 每 15min,PostgreSQL + Redis + Kafka offset
|
||||
- 恢复:`scripts/restore/`,月度演练验证 RTO
|
||||
- 多 AZ:Pod 反亲和 + DB 同步复制 + Redis 哨兵 + Kafka ISR=2
|
||||
- DNS 切换:区域级故障,TTL=60s,季度演练
|
||||
|
||||
### 15.8 与正文章节的对应
|
||||
|
||||
| 本章小节 | 对应正文章节 |
|
||||
|----------|--------------|
|
||||
| 横切关注点 | 第 3 章 共享内核 |
|
||||
| 中间件链 | 第 5 章 API Gateway |
|
||||
| 可观测性 | 第 10 章 可观测性 |
|
||||
| 安全栈 | 第 11 章 安全 |
|
||||
| 健康检查 | 第 6 章 服务边界 |
|
||||
| 灾难恢复 | 第 12 章 部署与运维 |
|
||||
|
||||
### 15.9 同步要求
|
||||
|
||||
新增导出符号需在落地到 Edu 仓库后运行 `npm run arch:scan` 更新 arch.db:
|
||||
- `HealthController`、`HealthModule`(5 个 NestJS 服务)
|
||||
- `LifecycleService`(5 个 NestJS 服务)
|
||||
- `health.py` router(ai、data-ana)
|
||||
@@ -5,8 +5,9 @@
|
||||
> 状态:基线发布
|
||||
> 适用范围:Edu 微服务架构(DDD + EDA + CQRS)
|
||||
> 关联文档:
|
||||
>
|
||||
> - [理想蓝图](./0010_architecture.md)
|
||||
> - [项目规则](../../project_rules.md)
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
> - [路线图](./roadmap/README.md)
|
||||
|
||||
---
|
||||
@@ -32,22 +33,25 @@
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 系统边界
|
||||
### 1.1a 技术分层视角(系统边界)
|
||||
|
||||
> 本图展示**部署分层结构**(自上而下:用户 → 微前端 → 网关 → BFF → 业务服务 → 总线 → 数据)。
|
||||
> 用户层按"使用场景域"标注,BFF 层按场景域分(不是按角色分)。业务领域视角见 [1.1b](#11b-业务领域视角)。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Users["用户层"]
|
||||
Teacher[教师]
|
||||
Student[学生]
|
||||
Parent[家长]
|
||||
Admin[管理员]
|
||||
subgraph Users["用户层(场景域用户)"]
|
||||
Teacher["教学场景域用户<br/>(教师 / 教导主任 / 教研组长 共用)"]
|
||||
Student["学习场景域用户<br/>(学生)"]
|
||||
Parent["家长场景域用户<br/>(家长)"]
|
||||
Admin["管理场景域用户<br/>(系统管理员 / 校管理员)"]
|
||||
end
|
||||
|
||||
subgraph MFE["微前端层(Module Federation)"]
|
||||
TeacherPortal[teacher-portal]
|
||||
StudentPortal[student-portal]
|
||||
ParentPortal[parent-portal]
|
||||
AdminPortal[admin-portal]
|
||||
TeacherPortal["teacher-portal<br/>教学场景域前端"]
|
||||
StudentPortal["student-portal<br/>学习场景域前端"]
|
||||
ParentPortal["parent-portal<br/>家长场景域前端"]
|
||||
AdminPortal["admin-portal<br/>管理场景域前端"]
|
||||
end
|
||||
|
||||
subgraph Gateway["网关层(Go)"]
|
||||
@@ -55,10 +59,10 @@ graph TB
|
||||
PushGateway[push-gateway<br/>WebSocket/SSE]
|
||||
end
|
||||
|
||||
subgraph BFF["BFF 聚合层(NestJS)"]
|
||||
TeacherBFF[teacher-bff<br/>GraphQL]
|
||||
StudentBFF[student-bff<br/>GraphQL]
|
||||
ParentBFF[parent-bff<br/>GraphQL]
|
||||
subgraph BFF["BFF 聚合层(NestJS)<br/>按使用场景域分 BFF(不是按角色分)"]
|
||||
TeacherBFF["teacher-bff<br/>教学场景域聚合"]
|
||||
StudentBFF["student-bff<br/>学习场景域聚合"]
|
||||
ParentBFF["parent-bff<br/>家长场景域聚合"]
|
||||
end
|
||||
|
||||
subgraph Services["业务微服务(NestJS + FastAPI)"]
|
||||
@@ -128,44 +132,100 @@ graph TB
|
||||
CoreEdu --> Redis
|
||||
```
|
||||
|
||||
### 1.1b 业务领域视角
|
||||
|
||||
> 本图按 **DDD 限界上下文**展示 6 个业务领域及其依赖关系。同一服务可横跨多个领域(如 core-edu 同时承载"教学组织"与"教学核心")。
|
||||
> 技术分层视角见 [1.1a](#11a-技术分层视角系统边界)。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph D1["D1 身份认证领域"]
|
||||
IAM[iam 服务]
|
||||
IAM_M[users / roles / permissions<br/>refresh_tokens / sessions]
|
||||
end
|
||||
|
||||
subgraph D2["D2 教学组织领域"]
|
||||
ORG[core-edu 服务<br/>classes 模块]
|
||||
ORG_M[classes / subjects / enrollment]
|
||||
end
|
||||
|
||||
subgraph D3["D3 教学核心领域"]
|
||||
TEACH[core-edu 服务<br/>exams/homework/grades]
|
||||
TEACH_M[exams / homework / grades<br/>courses / lessons / schedule / attendance]
|
||||
end
|
||||
|
||||
subgraph D4["D4 内容资源领域"]
|
||||
CONTENT[content 服务]
|
||||
CONTENT_M[textbooks / knowledge-points<br/>questions / grading / search]
|
||||
end
|
||||
|
||||
subgraph D5["D5 沟通通知领域"]
|
||||
MSG[msg 服务]
|
||||
MSG_M[messaging / notifications / announcements]
|
||||
end
|
||||
|
||||
subgraph D6["D6 智能洞察领域"]
|
||||
DATA[data-ana 服务]
|
||||
AI[ai 服务]
|
||||
DATA_M[analytics / dashboard / diagnostic]
|
||||
AI_M[AI 备课 / 出题 / 分析 / 搜索]
|
||||
end
|
||||
|
||||
IAM --> ORG
|
||||
IAM --> TEACH
|
||||
IAM --> CONTENT
|
||||
IAM --> MSG
|
||||
ORG --> TEACH
|
||||
TEACH --> CONTENT
|
||||
TEACH --> MSG
|
||||
CONTENT --> DATA
|
||||
TEACH --> DATA
|
||||
```
|
||||
|
||||
**双图并存说明**:
|
||||
|
||||
- **1.1a 技术分层**:描述部署、流量路径、网络边界,关注"如何部署与调用"
|
||||
- **1.1b 业务领域**:描述 DDD 限界上下文、聚合根、领域依赖,关注"业务边界与归属"
|
||||
- 两图互补,分别服务于运维/SRE 与产品/架构视角
|
||||
|
||||
### 1.2 服务清单
|
||||
|
||||
| 类别 | 服务名 | 语言/框架 | 限界上下文 | 阶段 |
|
||||
|------|--------|-----------|-----------|------|
|
||||
| 基础设施 | api-gateway | Go (Gin) | 网关 | P1 |
|
||||
| 基础设施 | push-gateway | Go (Gin) | 推送 | P5 |
|
||||
| BFF | teacher-bff | TS (NestJS) | 教师聚合 | P2 |
|
||||
| BFF | student-bff | TS (NestJS) | 学生聚合 | P3 |
|
||||
| BFF | parent-bff | TS (NestJS) | 家长聚合 | P4 |
|
||||
| 业务 | iam | TS (NestJS) | 身份认证 | P2 |
|
||||
| 业务 | core-edu | TS (NestJS) | 教学核心 | P3 |
|
||||
| 业务 | content | TS (NestJS) | 内容资源 | P4 |
|
||||
| 业务 | data-ana | Python (FastAPI) | 数据分析 | P4 |
|
||||
| 业务 | msg | TS (NestJS) | 消息通知 | P5 |
|
||||
| 业务 | ai | Python (FastAPI) | AI 网关 | P5 |
|
||||
| 微前端 | teacher-portal | TS (Next.js) | 教师端 | P2 |
|
||||
| 微前端 | student-portal | TS (Next.js) | 学生端 | P3 |
|
||||
| 微前端 | parent-portal | TS (Next.js) | 家长端 | P4 |
|
||||
| 微前端 | admin-portal | TS (Next.js) | 管理端 | P6 |
|
||||
| 共享包 | shared-proto | TS | protobuf 契约 | P1 |
|
||||
| 共享包 | shared-ts | TS | TS 共享工具 | P1 |
|
||||
| 共享包 | shared-go | Go | Go 共享工具 | P1 |
|
||||
| 共享包 | shared-py | Python | Python 共享工具 | P4 |
|
||||
| 类别 | 服务名 | 语言/框架 | 限界上下文 | 业务领域 | 阶段 |
|
||||
| -------- | -------------- | ---------------- | --------------- | ----------------------------- | ---- |
|
||||
| 基础设施 | api-gateway | Go (Gin) | 网关 | — | P1 |
|
||||
| 基础设施 | push-gateway | Go (Gin) | 推送 | — | P5 |
|
||||
| BFF | teacher-bff | TS (NestJS) | 教师聚合 | 教学场景域 | P2 |
|
||||
| BFF | student-bff | TS (NestJS) | 学生聚合 | 学习场景域 | P3 |
|
||||
| BFF | parent-bff | TS (NestJS) | 家长聚合 | 家长场景域 | P4 |
|
||||
| 业务 | iam | TS (NestJS) | 身份认证 | **D1 身份认证** | P2 |
|
||||
| 业务 | core-edu | TS (NestJS) | 教学核心 | **D2 教学组织 + D3 教学核心** | P3 |
|
||||
| 业务 | content | TS (NestJS) | 内容资源 | **D4 内容资源** | P4 |
|
||||
| 业务 | data-ana | Python (FastAPI) | 数据分析 | **D6 智能洞察** | P4 |
|
||||
| 业务 | msg | TS (NestJS) | 消息通知 | **D5 沟通通知** | P5 |
|
||||
| 业务 | ai | Python (FastAPI) | AI 网关 | **D6 智能洞察** | P5 |
|
||||
| 微前端 | teacher-portal | TS (Next.js) | 教师端 | 教学场景域 | P2 |
|
||||
| 微前端 | student-portal | TS (Next.js) | 学生端 | 学习场景域 | P3 |
|
||||
| 微前端 | parent-portal | TS (Next.js) | 家长端 | 家长场景域 | P4 |
|
||||
| 微前端 | admin-portal | TS (Next.js) | 管理端 | 管理场景域 | P6 |
|
||||
| 共享包 | shared-proto | TS | protobuf 契约 | — | P1 |
|
||||
| 共享包 | shared-ts | TS | TS 共享工具 | — | P1 |
|
||||
| 共享包 | shared-go | Go | Go 共享工具 | — | P1 |
|
||||
| 共享包 | shared-py | Python | Python 共享工具 | — | P4 |
|
||||
|
||||
### 1.3 CICD → Edu 模块映射
|
||||
|
||||
| CICD 模块(旧) | Edu 服务(新) | 迁移阶段 |
|
||||
|----------------|---------------|----------|
|
||||
| auth + users + rbac | iam | P2 |
|
||||
| classes + subjects + enrollment | core-edu | P3 |
|
||||
| courses + lessons + schedule + attendance | core-edu | P3 |
|
||||
| assignments + grades + exams | core-edu | P3 |
|
||||
| textbooks + knowledge-points | content | P4 |
|
||||
| questions + grading | content | P4 |
|
||||
| messaging + notifications | msg | P5 |
|
||||
| analytics + dashboard + diagnostic | data-ana | P4 |
|
||||
| ai + lesson-preparation | ai | P5 |
|
||||
| search | content (ES) | P4 |
|
||||
| CICD 模块(旧) | Edu 服务(新) | 迁移阶段 |
|
||||
| ----------------------------------------- | -------------- | -------- |
|
||||
| auth + users + rbac | iam | P2 |
|
||||
| classes + subjects + enrollment | core-edu | P3 |
|
||||
| courses + lessons + schedule + attendance | core-edu | P3 |
|
||||
| assignments + grades + exams | core-edu | P3 |
|
||||
| textbooks + knowledge-points | content | P4 |
|
||||
| questions + grading | content | P4 |
|
||||
| messaging + notifications | msg | P5 |
|
||||
| analytics + dashboard + diagnostic | data-ana | P4 |
|
||||
| ai + lesson-preparation | ai | P5 |
|
||||
| search | content (ES) | P4 |
|
||||
|
||||
---
|
||||
|
||||
@@ -173,38 +233,38 @@ graph TB
|
||||
|
||||
### 2.1 多语言技术栈矩阵
|
||||
|
||||
| 层级 | 语言 | 框架 | 用途 |
|
||||
|------|------|------|------|
|
||||
| 网关层 | Go 1.22+ | Gin | API Gateway、Push Gateway |
|
||||
| 业务服务 | TypeScript 5.5+ | NestJS 10 | IAM、CoreEdu、Content、Msg |
|
||||
| 分析/AI | Python 3.12+ | FastAPI | DataAna、AI 网关 |
|
||||
| BFF | TypeScript 5.5+ | NestJS 10 + GraphQL | 教师/学生/家长聚合 |
|
||||
| 微前端 | TypeScript 5.5+ | Next.js 15 + Module Federation | 4 端门户 |
|
||||
| 契约 | protobuf | buf | 跨语言契约定义 |
|
||||
| 层级 | 语言 | 框架 | 用途 |
|
||||
| -------- | --------------- | ------------------------------ | -------------------------- |
|
||||
| 网关层 | Go 1.22+ | Gin | API Gateway、Push Gateway |
|
||||
| 业务服务 | TypeScript 5.5+ | NestJS 10 | IAM、CoreEdu、Content、Msg |
|
||||
| 分析/AI | Python 3.12+ | FastAPI | DataAna、AI 网关 |
|
||||
| BFF | TypeScript 5.5+ | NestJS 10 + GraphQL | 教师/学生/家长聚合 |
|
||||
| 微前端 | TypeScript 5.5+ | Next.js 15 + Module Federation | 4 端门户 |
|
||||
| 契约 | protobuf | buf | 跨语言契约定义 |
|
||||
|
||||
### 2.2 多存储矩阵
|
||||
|
||||
| 存储 | 用途 | 使用服务 |
|
||||
|------|------|----------|
|
||||
| MySQL 8 | 写模型主库(每服务独占) | IAM、CoreEdu、Content、Msg |
|
||||
| Redis 7 | 缓存、会话、限流计数 | 全部服务 |
|
||||
| ClickHouse | 读模型宽表、分析聚合 | DataAna、CoreEdu(读模型) |
|
||||
| Neo4j | 知识图谱、前置依赖 | Content |
|
||||
| Elasticsearch | 题库全文检索 | Content、AI |
|
||||
| 存储 | 用途 | 使用服务 |
|
||||
| ------------- | ------------------------ | -------------------------- |
|
||||
| MySQL 8 | 写模型主库(每服务独占) | IAM、CoreEdu、Content、Msg |
|
||||
| Redis 7 | 缓存、会话、限流计数 | 全部服务 |
|
||||
| ClickHouse | 读模型宽表、分析聚合 | DataAna、CoreEdu(读模型) |
|
||||
| Neo4j | 知识图谱、前置依赖 | Content |
|
||||
| Elasticsearch | 题库全文检索 | Content、AI |
|
||||
|
||||
### 2.3 基础设施矩阵
|
||||
|
||||
| 组件 | 用途 |
|
||||
|------|------|
|
||||
| Kafka | 事件总线,领域事件异步通信 |
|
||||
| Debezium | CDC,MySQL Binlog → Kafka 实时同步 |
|
||||
| Temporal | 工作流编排(考试生命周期、AI 编排) |
|
||||
| OpenTelemetry | 分布式追踪 |
|
||||
| Loki | 日志聚合 |
|
||||
| Tempo | 分布式 Trace 存储 |
|
||||
| Prometheus | 指标采集 |
|
||||
| Grafana | 可观测性可视化 |
|
||||
| Vault | 密钥管理(P6) |
|
||||
| 组件 | 用途 |
|
||||
| ------------- | ----------------------------------- |
|
||||
| Kafka | 事件总线,领域事件异步通信 |
|
||||
| Debezium | CDC,MySQL Binlog → Kafka 实时同步 |
|
||||
| Temporal | 工作流编排(考试生命周期、AI 编排) |
|
||||
| OpenTelemetry | 分布式追踪 |
|
||||
| Loki | 日志聚合 |
|
||||
| Tempo | 分布式 Trace 存储 |
|
||||
| Prometheus | 指标采集 |
|
||||
| Grafana | 可观测性可视化 |
|
||||
| Vault | 密钥管理(P6) |
|
||||
|
||||
---
|
||||
|
||||
@@ -260,6 +320,7 @@ L1 客户端 → L2 微前端 → L3 网关 → L4 BFF → L5 业务服务 → L
|
||||
```
|
||||
|
||||
**严格规则**:
|
||||
|
||||
1. L3 网关层只做路由、鉴权、限流、熔断,**不写业务逻辑**
|
||||
2. L4 BFF 层只做聚合、裁剪、协议转换,**不持有业务状态**
|
||||
3. L5 业务服务之间通过 gRPC(同步)或 Kafka 事件(异步)通信,**不直接访问对方数据库**
|
||||
@@ -339,17 +400,17 @@ graph TB
|
||||
|
||||
### 4.1 服务间通信矩阵
|
||||
|
||||
| 调用方 → 被调用方 | 协议 | 场景 |
|
||||
|-------------------|------|------|
|
||||
| api-gateway → BFF | gRPC | 请求路由 |
|
||||
| BFF → 业务服务 | gRPC | 同步查询聚合 |
|
||||
| CoreEdu → Content | Kafka 事件 | 教学内容变更通知 |
|
||||
| CoreEdu → DataAna | Kafka 事件 | 学情数据投递 |
|
||||
| CoreEdu → Msg | Kafka 事件 | 通知触发 |
|
||||
| IAM → CoreEdu | Kafka 事件 | 用户变更同步 |
|
||||
| AI → Content | gRPC | 题库查询 |
|
||||
| AI → DataAna | gRPC | 学情数据查询 |
|
||||
| push-gateway → Msg | gRPC | 推送通道建立 |
|
||||
| 调用方 → 被调用方 | 协议 | 场景 |
|
||||
| ------------------ | ---------- | ---------------- |
|
||||
| api-gateway → BFF | gRPC | 请求路由 |
|
||||
| BFF → 业务服务 | gRPC | 同步查询聚合 |
|
||||
| CoreEdu → Content | Kafka 事件 | 教学内容变更通知 |
|
||||
| CoreEdu → DataAna | Kafka 事件 | 学情数据投递 |
|
||||
| CoreEdu → Msg | Kafka 事件 | 通知触发 |
|
||||
| IAM → CoreEdu | Kafka 事件 | 用户变更同步 |
|
||||
| AI → Content | gRPC | 题库查询 |
|
||||
| AI → DataAna | gRPC | 学情数据查询 |
|
||||
| push-gateway → Msg | gRPC | 推送通道建立 |
|
||||
|
||||
---
|
||||
|
||||
@@ -389,27 +450,61 @@ sequenceDiagram
|
||||
|
||||
### 5.2 三层角色模型
|
||||
|
||||
| 层级 | 来源 | 示例 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| 系统角色 | 系统预设 | admin、teacher、student、parent | 最高 |
|
||||
| 组织角色 | 学校/班级分配 | 年级组长、班主任、学科组长 | 中 |
|
||||
| 临时角色 | 临时授权 | 代课教师、临时代理 | 最低 |
|
||||
| 层级 | 来源 | 示例 | 优先级 |
|
||||
| -------- | ------------- | ------------------------------- | ------ |
|
||||
| 系统角色 | 系统预设 | admin、teacher、student、parent | 最高 |
|
||||
| 组织角色 | 学校/班级分配 | 年级组长、班主任、学科组长 | 中 |
|
||||
| 临时角色 | 临时授权 | 代课教师、临时代理 | 最低 |
|
||||
|
||||
**规则**:权限取三层角色权限的并集,拒绝权限取交集(任一层拒绝则拒绝)。
|
||||
|
||||
### 5.3 DataScope 6 级数据范围
|
||||
|
||||
| 级别 | 名称 | 数据范围 | 典型角色 |
|
||||
|------|------|----------|----------|
|
||||
| L0 | SELF | 仅本人数据 | 学生、家长 |
|
||||
| L1 | CLASS | 本班数据 | 班主任、学生 |
|
||||
| L2 | GRADE | 本年级数据 | 年级组长 |
|
||||
| L3 | SCHOOL | 本校数据 | 校管理员 |
|
||||
| L4 | DISTRICT | 本区数据 | 区教研员 |
|
||||
| L5 | ALL | 全部数据 | 系统管理员 |
|
||||
| 级别 | 名称 | 数据范围 | 典型角色 |
|
||||
| ---- | -------- | ---------- | ------------ |
|
||||
| L0 | SELF | 仅本人数据 | 学生、家长 |
|
||||
| L1 | CLASS | 本班数据 | 班主任、学生 |
|
||||
| L2 | GRADE | 本年级数据 | 年级组长 |
|
||||
| L3 | SCHOOL | 本校数据 | 校管理员 |
|
||||
| L4 | DISTRICT | 本区数据 | 区教研员 |
|
||||
| L5 | ALL | 全部数据 | 系统管理员 |
|
||||
|
||||
**实现**:业务服务在 Repository 层根据 dataScope 级别动态注入 WHERE 条件。
|
||||
|
||||
### 5.4 视口四层模型
|
||||
|
||||
视口(Viewport)是用户在特定场景域下的可见范围。视口既可独立配置(RoleViewport 表),
|
||||
也可由权限推导(permission → viewport 默认映射)。新角色只需配置权限集,视口自动推导;
|
||||
需要差异化时再显式配置视口。
|
||||
|
||||
| 层级 | 含义 | 配置载体 | 示例 |
|
||||
| ------- | ---------------- | ---------------------------------- | -------------------------------------------- |
|
||||
| L1 导航 | 侧边栏菜单项 | navigation_config 表 | 教导主任看到"全校成绩分析"菜单 |
|
||||
| L2 路由 | 可访问路由 | route_permission 表 + Gateway 校验 | 教导主任可访问 /admin/grade-analysis |
|
||||
| L3 组件 | 页面内组件可见性 | usePermission().hasPermission() | 教导主任看到"导出全校报表"按钮 |
|
||||
| L4 数据 | 数据行级过滤 | DataScope 枚举 | 教导主任 DataScope=grade_managed(管辖年级) |
|
||||
|
||||
**场景域 BFF 复用策略**:
|
||||
|
||||
按"使用场景域"分 BFF,而非按角色分。新角色复用现有 BFF,通过视口差异化。
|
||||
|
||||
| BFF | 场景域 | 复用角色 |
|
||||
| ----------- | -------- | ------------------------ |
|
||||
| Teacher BFF | 教学场景 | 教师、教导主任、教研组长 |
|
||||
| Student BFF | 学习场景 | 学生 |
|
||||
| Parent BFF | 家长场景 | 家长 |
|
||||
| Admin BFF | 管理场景 | 系统管理员、校管理员 |
|
||||
|
||||
**实现**:教导主任归入 Teacher BFF + 额外管理视口(L1 导航增加管理菜单项,L4 数据范围扩大到年级)。
|
||||
|
||||
**iam 服务职责**:
|
||||
|
||||
- 认证:登录/登出/JWT/2FA
|
||||
- RBAC:角色/权限/角色-权限映射 CRUD
|
||||
- 视口配置:导航/路由/组件级视口配置 CRUD
|
||||
- DataScope:数据范围解析(all/grade_managed/class_taught/children/owned + 自定义)
|
||||
- 权限解析 API:getEffectivePermissions(userId) → {permissions, viewports, dataScope}
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据访问与缓存
|
||||
@@ -422,24 +517,24 @@ graph LR
|
||||
Cmd[Command 命令] --> App[Application Service]
|
||||
App --> Domain[Domain 领域模型]
|
||||
Domain --> Repo[Repository 写模型]
|
||||
Repo -->[(MySQL 主库)]
|
||||
Repo --> mysql_w[(MySQL 主库)]
|
||||
App --> Outbox[(Outbox 表<br/>同事务)]
|
||||
end
|
||||
|
||||
subgraph Sync["同步链路"]
|
||||
Outbox --> Relay[Relay Worker]
|
||||
Relay --> Kafka[(Kafka)]
|
||||
Kafka --> Proj[Projection]
|
||||
Proj -->[(ClickHouse 宽表)]
|
||||
Proj -->[(Redis 缓存)]
|
||||
Proj -->[(ES 索引)]
|
||||
Relay --> kafka_sync[(Kafka)]
|
||||
kafka_sync --> Proj[Projection]
|
||||
Proj --> ch_sync[(ClickHouse 宽表)]
|
||||
Proj --> redis_sync[(Redis 缓存)]
|
||||
Proj --> es_sync[(ES 索引)]
|
||||
end
|
||||
|
||||
subgraph Read["读路径"]
|
||||
Query[Query 查询] --> ReadModel[Read Model]
|
||||
ReadModel -->[(ClickHouse 宽表)]
|
||||
ReadModel -->[(Redis 缓存)]
|
||||
ReadModel -->[(ES 索引)]
|
||||
ReadModel --> ch_read[(ClickHouse 宽表)]
|
||||
ReadModel --> redis_read[(Redis 缓存)]
|
||||
ReadModel --> es_read[(ES 索引)]
|
||||
end
|
||||
```
|
||||
|
||||
@@ -461,15 +556,15 @@ flowchart TD
|
||||
|
||||
### 6.3 缓存策略矩阵
|
||||
|
||||
| 数据类型 | 存储 | TTL | 失效策略 |
|
||||
|----------|------|-----|----------|
|
||||
| 用户会话 | Redis | 30 分钟 | 滑动过期 |
|
||||
| 权限列表 | Redis | 5 分钟 | 事件驱动失效 |
|
||||
| 班级/年级列表 | Redis | 5 分钟 | 事件驱动失效 |
|
||||
| 教学资源详情 | Redis | 30 秒 | 短 TTL |
|
||||
| BFF 聚合结果 | Redis | 5-30 秒 | 短 TTL |
|
||||
| 学情宽表 | ClickHouse | 实时 | CDC 同步 |
|
||||
| 题库检索 | ES | 实时 | CDC 同步 |
|
||||
| 数据类型 | 存储 | TTL | 失效策略 |
|
||||
| ------------- | ---------- | ------- | ------------ |
|
||||
| 用户会话 | Redis | 30 分钟 | 滑动过期 |
|
||||
| 权限列表 | Redis | 5 分钟 | 事件驱动失效 |
|
||||
| 班级/年级列表 | Redis | 5 分钟 | 事件驱动失效 |
|
||||
| 教学资源详情 | Redis | 30 秒 | 短 TTL |
|
||||
| BFF 聚合结果 | Redis | 5-30 秒 | 短 TTL |
|
||||
| 学情宽表 | ClickHouse | 实时 | CDC 同步 |
|
||||
| 题库检索 | ES | 实时 | CDC 同步 |
|
||||
|
||||
---
|
||||
|
||||
@@ -486,7 +581,7 @@ graph LR
|
||||
Outbox[(Outbox 表)]
|
||||
end
|
||||
|
||||
subgraph MySQL[("MySQL 主库")]
|
||||
subgraph MySQL["MySQL 主库"]
|
||||
BizTable[(业务表)]
|
||||
OutboxTable[(outbox 表)]
|
||||
end
|
||||
@@ -498,7 +593,7 @@ graph LR
|
||||
end
|
||||
|
||||
subgraph Bus["事件总线"]
|
||||
Kafka[(Kafka topic)]
|
||||
kafka_bus[(Kafka topic)]
|
||||
end
|
||||
|
||||
subgraph Consumers["消费者"]
|
||||
@@ -512,35 +607,35 @@ graph LR
|
||||
Repo --> OutboxTable
|
||||
OutboxTable --> Poll
|
||||
Poll --> Publish
|
||||
Publish --> Kafka
|
||||
Kafka --> Proj
|
||||
Kafka --> OtherSvc
|
||||
Proj -->[(ClickHouse/Redis/ES)]
|
||||
Publish --> kafka_bus
|
||||
kafka_bus --> Proj
|
||||
kafka_bus --> OtherSvc
|
||||
Proj --> read_stores[(ClickHouse / Redis / ES)]
|
||||
```
|
||||
|
||||
### 7.2 事件 Topic 分类
|
||||
|
||||
| Topic 模式 | 示例 | 生产者 | 消费者 |
|
||||
|-----------|------|--------|--------|
|
||||
| `edu.identity.user.created` | 用户创建 | IAM | CoreEdu、Msg |
|
||||
| `edu.identity.user.updated` | 用户更新 | IAM | CoreEdu、Msg |
|
||||
| `edu.org.class.created` | 班级创建 | CoreEdu | DataAna |
|
||||
| `edu.teaching.assignment.submitted` | 作业提交 | CoreEdu | DataAna、Msg |
|
||||
| `edu.teaching.exam.published` | 考试发布 | CoreEdu | Msg |
|
||||
| `edu.teaching.grade.recorded` | 成绩录入 | CoreEdu | DataAna、Msg |
|
||||
| `edu.content.question.published` | 题目发布 | Content | AI、ES |
|
||||
| `edu.insight.mastery.updated` | 掌握度更新 | DataAna | CoreEdu、Msg |
|
||||
| Topic 模式 | 示例 | 生产者 | 消费者 |
|
||||
| ----------------------------------- | ---------- | ------- | ------------ |
|
||||
| `edu.identity.user.created` | 用户创建 | IAM | CoreEdu、Msg |
|
||||
| `edu.identity.user.updated` | 用户更新 | IAM | CoreEdu、Msg |
|
||||
| `edu.org.class.created` | 班级创建 | CoreEdu | DataAna |
|
||||
| `edu.teaching.assignment.submitted` | 作业提交 | CoreEdu | DataAna、Msg |
|
||||
| `edu.teaching.exam.published` | 考试发布 | CoreEdu | Msg |
|
||||
| `edu.teaching.grade.recorded` | 成绩录入 | CoreEdu | DataAna、Msg |
|
||||
| `edu.content.question.published` | 题目发布 | Content | AI、ES |
|
||||
| `edu.insight.mastery.updated` | 掌握度更新 | DataAna | CoreEdu、Msg |
|
||||
|
||||
### 7.3 核心领域事件
|
||||
|
||||
| 事件 | 触发场景 | 消费者动作 |
|
||||
|------|----------|-----------|
|
||||
| UserRegistered | 新用户注册 | CoreEdu 初始化默认班级关联;Msg 发送欢迎通知 |
|
||||
| ExamPublished | 考试发布 | Msg 推送考试通知给学生;DataAna 创建考试分析骨架 |
|
||||
| HomeworkSubmitted | 学生提交作业 | DataAna 记录提交行为;Msg 通知教师 |
|
||||
| HomeworkGraded | 教师批改完成 | DataAna 更新掌握度;Msg 通知学生 |
|
||||
| MasteryUpdated | 掌握度计算完成 | CoreEdu 推荐个性化练习;Msg 触发预警 |
|
||||
| NotificationRequested | 通知请求 | Msg 投递通知到多渠道 |
|
||||
| 事件 | 触发场景 | 消费者动作 |
|
||||
| --------------------- | -------------- | ------------------------------------------------ |
|
||||
| UserRegistered | 新用户注册 | CoreEdu 初始化默认班级关联;Msg 发送欢迎通知 |
|
||||
| ExamPublished | 考试发布 | Msg 推送考试通知给学生;DataAna 创建考试分析骨架 |
|
||||
| HomeworkSubmitted | 学生提交作业 | DataAna 记录提交行为;Msg 通知教师 |
|
||||
| HomeworkGraded | 教师批改完成 | DataAna 更新掌握度;Msg 通知学生 |
|
||||
| MasteryUpdated | 掌握度计算完成 | CoreEdu 推荐个性化练习;Msg 触发预警 |
|
||||
| NotificationRequested | 通知请求 | Msg 投递通知到多渠道 |
|
||||
|
||||
---
|
||||
|
||||
@@ -768,6 +863,7 @@ flowchart LR
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 所有跨服务调用必须透传 W3C Trace Context
|
||||
- Kafka 事件必须将 traceId 写入消息 header
|
||||
- 日志必须包含 traceId 用于关联查询
|
||||
@@ -821,15 +917,15 @@ graph TB
|
||||
|
||||
### 11.2 契约规则
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 包命名 | `edu.[context].[aggregate].v[version]` |
|
||||
| 版本化 | 破坏性变更必须升版本(v1 → v2) |
|
||||
| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
|
||||
| 消息命名 | PascalCase |
|
||||
| 字段命名 | snake_case |
|
||||
| 注释 | 每个 message 和字段必须注释 |
|
||||
| CI 强制 | buf lint + buf breaking 必须通过 |
|
||||
| 规则 | 说明 |
|
||||
| -------- | -------------------------------------- |
|
||||
| 包命名 | `edu.[context].[aggregate].v[version]` |
|
||||
| 版本化 | 破坏性变更必须升版本(v1 → v2) |
|
||||
| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
|
||||
| 消息命名 | PascalCase |
|
||||
| 字段命名 | snake_case |
|
||||
| 注释 | 每个 message 和字段必须注释 |
|
||||
| CI 强制 | buf lint + buf breaking 必须通过 |
|
||||
|
||||
### 11.3 BFF 聚合模式
|
||||
|
||||
@@ -865,56 +961,56 @@ graph LR
|
||||
|
||||
### 12.1 服务独立性约束
|
||||
|
||||
| 约束 | 说明 |
|
||||
|------|------|
|
||||
| 数据库独占 | 每个微服务独占自身数据库,禁止跨库联表 |
|
||||
| 契约先行 | 所有跨服务通信必须先定义 protobuf 契约 |
|
||||
| Outbox 强制 | 所有领域事件必须通过 Outbox 模式发布 |
|
||||
| 幂等消费 | 所有事件消费者必须实现幂等性 |
|
||||
| 单一职责 | 每个服务只负责一个限界上下文 |
|
||||
| 无状态服务 | 业务服务不持有会话状态(Redis 承载) |
|
||||
| 约束 | 说明 |
|
||||
| ----------- | -------------------------------------- |
|
||||
| 数据库独占 | 每个微服务独占自身数据库,禁止跨库联表 |
|
||||
| 契约先行 | 所有跨服务通信必须先定义 protobuf 契约 |
|
||||
| Outbox 强制 | 所有领域事件必须通过 Outbox 模式发布 |
|
||||
| 幂等消费 | 所有事件消费者必须实现幂等性 |
|
||||
| 单一职责 | 每个服务只负责一个限界上下文 |
|
||||
| 无状态服务 | 业务服务不持有会话状态(Redis 承载) |
|
||||
|
||||
### 12.2 通信约束
|
||||
|
||||
| 场景 | 允许 | 禁止 |
|
||||
|------|------|------|
|
||||
| 客户端 → Gateway | REST + WebSocket | 直连业务服务 |
|
||||
| Gateway → BFF | gRPC | REST |
|
||||
| BFF → 业务服务 | gRPC | 直接访问 DB |
|
||||
| 业务服务之间(同步) | gRPC + 必要时 | REST、直接 DB |
|
||||
| 业务服务之间(异步) | Kafka 事件 | 直接 producer 调用 |
|
||||
| 事件发布 | Outbox 模式 | 直接 Kafka producer |
|
||||
| 场景 | 允许 | 禁止 |
|
||||
| -------------------- | ---------------- | ------------------- |
|
||||
| 客户端 → Gateway | REST + WebSocket | 直连业务服务 |
|
||||
| Gateway → BFF | gRPC | REST |
|
||||
| BFF → 业务服务 | gRPC | 直接访问 DB |
|
||||
| 业务服务之间(同步) | gRPC + 必要时 | REST、直接 DB |
|
||||
| 业务服务之间(异步) | Kafka 事件 | 直接 producer 调用 |
|
||||
| 事件发布 | Outbox 模式 | 直接 Kafka producer |
|
||||
|
||||
### 12.3 数据一致性约束
|
||||
|
||||
| 场景 | 一致性级别 | 实现 |
|
||||
|------|-----------|------|
|
||||
| 聚合内 | 强一致 | 单事务 |
|
||||
| 聚合间 | 最终一致 | Kafka 事件 |
|
||||
| 服务间 | 最终一致 | Kafka 事件 / Saga |
|
||||
| 读模型 | 最终一致 | Projection 异步更新 |
|
||||
| 缓存 | 最终一致 | 事件驱动失效 + 短 TTL |
|
||||
| 场景 | 一致性级别 | 实现 |
|
||||
| ------ | ---------- | --------------------- |
|
||||
| 聚合内 | 强一致 | 单事务 |
|
||||
| 聚合间 | 最终一致 | Kafka 事件 |
|
||||
| 服务间 | 最终一致 | Kafka 事件 / Saga |
|
||||
| 读模型 | 最终一致 | Projection 异步更新 |
|
||||
| 缓存 | 最终一致 | 事件驱动失效 + 短 TTL |
|
||||
|
||||
---
|
||||
|
||||
## 13. ADR 记录
|
||||
|
||||
| 编号 | 决策 | 原因 | 状态 |
|
||||
|------|------|------|------|
|
||||
| ADR-001 | 采用 DDD 限界上下文划分服务 | 业务边界清晰,独立演进 | 已采纳 |
|
||||
| ADR-002 | 采用 NestJS 作为业务服务框架 | TS 生态成熟,装饰器 + DI 适合 DDD | 已采纳 |
|
||||
| ADR-003 | 采用 Go 作为网关语言 | 高并发、低内存、适合网关场景 | 已采纳 |
|
||||
| ADR-004 | 采用 Python 作为分析/AI 语言 | 数据科学/AI 生态丰富 | 已采纳 |
|
||||
| ADR-005 | 采用 CQRS 读写分离 | 读多写少,读模型可独立优化 | 已采纳 |
|
||||
| ADR-006 | 采用 Outbox 模式发布事件 | 保证事务与事件最终一致 | 已采纳 |
|
||||
| ADR-007 | 采用 Kafka 作为事件总线 | 高吞吐、持久化、成熟生态 | 已采纳 |
|
||||
| ADR-008 | 采用 Debezium CDC | 解耦 Outbox Relay,减少业务侵入 | 已采纳 |
|
||||
| ADR-009 | 采用 protobuf + buf 契约先行 | 多语言契约统一、版本化、CI 强制 | 已采纳 |
|
||||
| ADR-010 | 采用 JWT RS256 非对称签名 | 网关公钥校验无需共享私钥 | 已采纳 |
|
||||
| ADR-011 | 采用 DataScope 6 级数据范围 | 满足 K12 多层级数据隔离 | 已采纳 |
|
||||
| ADR-012 | 采用 Module Federation 微前端 | 独立部署、技术栈无关、渐进迁移 | 已采纳 |
|
||||
| ADR-013 | 采用 Temporal 工作流编排 | 长流程编排、可观测、可回滚 | 已采纳 |
|
||||
| ADR-014 | 采用 ClickHouse 读模型宽表 | 分析查询亚秒级响应 | 已采纳 |
|
||||
| 编号 | 决策 | 原因 | 状态 |
|
||||
| ------- | ----------------------------- | --------------------------------- | ------ |
|
||||
| ADR-001 | 采用 DDD 限界上下文划分服务 | 业务边界清晰,独立演进 | 已采纳 |
|
||||
| ADR-002 | 采用 NestJS 作为业务服务框架 | TS 生态成熟,装饰器 + DI 适合 DDD | 已采纳 |
|
||||
| ADR-003 | 采用 Go 作为网关语言 | 高并发、低内存、适合网关场景 | 已采纳 |
|
||||
| ADR-004 | 采用 Python 作为分析/AI 语言 | 数据科学/AI 生态丰富 | 已采纳 |
|
||||
| ADR-005 | 采用 CQRS 读写分离 | 读多写少,读模型可独立优化 | 已采纳 |
|
||||
| ADR-006 | 采用 Outbox 模式发布事件 | 保证事务与事件最终一致 | 已采纳 |
|
||||
| ADR-007 | 采用 Kafka 作为事件总线 | 高吞吐、持久化、成熟生态 | 已采纳 |
|
||||
| ADR-008 | 采用 Debezium CDC | 解耦 Outbox Relay,减少业务侵入 | 已采纳 |
|
||||
| ADR-009 | 采用 protobuf + buf 契约先行 | 多语言契约统一、版本化、CI 强制 | 已采纳 |
|
||||
| ADR-010 | 采用 JWT RS256 非对称签名 | 网关公钥校验无需共享私钥 | 已采纳 |
|
||||
| ADR-011 | 采用 DataScope 6 级数据范围 | 满足 K12 多层级数据隔离 | 已采纳 |
|
||||
| ADR-012 | 采用 Module Federation 微前端 | 独立部署、技术栈无关、渐进迁移 | 已采纳 |
|
||||
| ADR-013 | 采用 Temporal 工作流编排 | 长流程编排、可观测、可回滚 | 已采纳 |
|
||||
| ADR-014 | 采用 ClickHouse 读模型宽表 | 分析查询亚秒级响应 | 已采纳 |
|
||||
|
||||
---
|
||||
|
||||
@@ -933,13 +1029,13 @@ graph LR
|
||||
|
||||
### 14.2 服务与阶段映射
|
||||
|
||||
| 阶段 | 周期 | 交付服务 | 退出标准 |
|
||||
|------|------|----------|----------|
|
||||
| P1 地基 | M1-M3 | api-gateway、classes(黄金模板)、shared-proto | classes 域 CRUD 端到端跑通 |
|
||||
| P2 身份 | M4-M6 | iam、teacher-bff、teacher-portal 骨架 | 教师可登录并看到空白 Dashboard |
|
||||
| P3 核心教学 | M7-M10 | core-edu(合并 classes)、student-bff、student-portal | 考试→作答→批改→成绩全链路 |
|
||||
| P4 内容分析 | M11-M13 | content、data-ana、parent-bff、parent-portal | 知识图谱查询 + 学情宽表 5s |
|
||||
| P5 沟通AI | M14-M16 | msg、push-gateway、ai | 全校广播 + AI 辅助出题 |
|
||||
| P6 硬化 | M17-M18 | admin-portal、Service Mesh | 99.9% 可用性 + 独立扩缩容 |
|
||||
| 阶段 | 周期 | 交付服务 | 退出标准 |
|
||||
| ----------- | ------- | ----------------------------------------------------- | ------------------------------ |
|
||||
| P1 地基 | M1-M3 | api-gateway、classes(黄金模板)、shared-proto | classes 域 CRUD 端到端跑通 |
|
||||
| P2 身份 | M4-M6 | iam、teacher-bff、teacher-portal 骨架 | 教师可登录并看到空白 Dashboard |
|
||||
| P3 核心教学 | M7-M10 | core-edu(合并 classes)、student-bff、student-portal | 考试→作答→批改→成绩全链路 |
|
||||
| P4 内容分析 | M11-M13 | content、data-ana、parent-bff、parent-portal | 知识图谱查询 + 学情宽表 5s |
|
||||
| P5 沟通AI | M14-M16 | msg、push-gateway、ai | 全校广播 + AI 辅助出题 |
|
||||
| P6 硬化 | M17-M18 | admin-portal、Service Mesh | 99.9% 可用性 + 独立扩缩容 |
|
||||
|
||||
> 详细规划见 [路线图目录](./roadmap/README.md)
|
||||
|
||||
401
docs/architecture/ai-allocation.md
Normal file
401
docs/architecture/ai-allocation.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# AI 分配方案与架构设计外包流程
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> 适用范围:Edu 微服务项目模块架构设计外包阶段
|
||||
> 关联文档:[多 AI 协作指南](../standards/multi-ai-collaboration.md)、[004 架构影响地图](./004_architecture_impact_map.md)、[待开发功能路线图](./roadmap/pending-features.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 外包总流程:三阶段
|
||||
|
||||
```
|
||||
阶段 1:全局理解 阶段 2:模块架构设计 阶段 3:按图实施
|
||||
(每个 AI 独立) (每个 AI 独立) (并行开发)
|
||||
│ │ │
|
||||
阅读全局架构文档 产出模块内部架构图 按自己画的图写代码
|
||||
理解边界与契约 定义内部模块/数据流 coord 定期巡检一致性
|
||||
理解与其他模块的接口 标注与其他模块的交互点 遇到偏差更新架构图
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
交付:理解确认书 交付:模块架构设计文档 交付:代码 + 更新图
|
||||
```
|
||||
|
||||
**阶段 1 目标**:每个 AI 读懂自己负责的模块在全局架构中的位置、边界、契约。
|
||||
**阶段 2 目标**:每个 AI 产出自己模块的内部架构设计,经过 coord 交叉审查后放行。
|
||||
**阶段 3 目标**:按设计文档写代码,coord 定期巡检一致性。
|
||||
|
||||
---
|
||||
|
||||
## 2. 完整服务清单
|
||||
|
||||
| 类别 | 服务名 | 语言/框架 | 限界上下文 | 阶段 | 状态 |
|
||||
| ---- | -------------- | ---------------- | ---------------------- | ------ | ----------------- |
|
||||
| 网关 | api-gateway | Go (Gin) | API 网关 | P1 | ✅ 已实现 |
|
||||
| 网关 | push-gateway | Go (Gin) | 推送网关 | P5 | 📐 需设计 |
|
||||
| BFF | teacher-bff | TS (NestJS) | 教学场景域聚合 | P2 | ✅ 已实现 |
|
||||
| BFF | student-bff | TS (NestJS) | 学习场景域聚合 | P3 | 📐 需设计 |
|
||||
| BFF | parent-bff | TS (NestJS) | 家长场景域聚合 | P4 | 📐 需设计 |
|
||||
| 业务 | iam | TS (NestJS) | 身份认证 | P2 | ✅ 已实现 |
|
||||
| 业务 | core-edu | TS (NestJS) | 教学核心(含 classes) | P3 | 📐 待合并 classes |
|
||||
| 业务 | content | TS (NestJS) | 内容资源 | P4 | 📐 需设计 |
|
||||
| 业务 | msg | TS (NestJS) | 消息通知 | P5 | 📐 需设计 |
|
||||
| 业务 | data-ana | Python (FastAPI) | 数据分析 | P4 | 📐 需设计 |
|
||||
| 业务 | ai | Python (FastAPI) | AI 网关 | P5 | 📐 需设计 |
|
||||
| 前端 | teacher-portal | TS (Next.js) | 教学场景域前端 | P2 | ✅ 已实现 |
|
||||
| 前端 | student-portal | TS (Next.js) | 学习场景域前端 | P3 | 📐 需设计 |
|
||||
| 前端 | parent-portal | TS (Next.js) | 家长场景域前端 | P4 | 📐 需设计 |
|
||||
| 前端 | admin-portal | TS (Next.js) | 管理场景域前端 | P6 | 📐 需设计 |
|
||||
| 共享 | shared-proto | protobuf | 契约 | 跨阶段 | ✅ 部分 |
|
||||
| 共享 | shared-ts | TS | TS 共享工具 | 跨阶段 | — |
|
||||
| 共享 | shared-go | Go | Go 共享工具 | 跨阶段 | — |
|
||||
| 共享 | shared-py | Python | Python 共享工具 | 跨阶段 | — |
|
||||
| 基础 | infra | — | K8s/Grafana/WAF | 跨阶段 | ✅ 部分 |
|
||||
|
||||
> 状态标记:✅ 已实现需审计 | 📐 需架构设计(本次外包核心产出)
|
||||
|
||||
---
|
||||
|
||||
## 3. AI 分配方案(7 AI + 1 coord)
|
||||
|
||||
### 3.1 分配原则
|
||||
|
||||
- **同语言内聚**:一个 AI 负责多个同语言服务,学习成本只付一次
|
||||
- **领域亲缘性**:同类业务放一起(BFF 归 BFF、Python 归 Python)
|
||||
- **工作负载均衡**:Neo4j+ES 的内容服务、ClickHouse 的分析服务复杂度高,不绑太多其他服务
|
||||
- **前端统一**:Module Federation 微前端由一人设计,保证 shell + remote 架构一致
|
||||
- **黄金模板对齐**:已实现的 services 负责 AI 需审计并对齐 classes 标准
|
||||
|
||||
### 3.2 分配矩阵
|
||||
|
||||
| AI 标识 | 语言 | 服务 | 数量 | 阶段归属 |
|
||||
| --------- | ------ | ----------------------------------------------------------- | ---- | -------- |
|
||||
| **ai01** | Go | api-gateway、push-gateway | 2 | P1 + P5 |
|
||||
| **ai02** | TS | iam | 1 | P2 |
|
||||
| **ai03** | TS | teacher-bff、core-edu | 2 | P2 + P3 |
|
||||
| **ai04** | TS | student-bff、parent-bff | 2 | P3 + P4 |
|
||||
| **ai05** | TS | content、msg | 2 | P4 + P5 |
|
||||
| **ai06** | Python | data-ana、ai | 2 | P4 + P5 |
|
||||
| **ai07** | TS | teacher-portal、student-portal、parent-portal、admin-portal | 4 | P2-P6 |
|
||||
| **coord** | — | shared-proto、shared-*、infra/、docs/、CI/CD | — | 跨阶段 |
|
||||
|
||||
### 3.3 为什么这样拆
|
||||
|
||||
| 决策 | 理由 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------- |
|
||||
| ai02 独立负责 iam | RBAC 三层角色 + DataScope 6 级 + 视口 4 层是整个系统的权限中枢,复杂度最高 |
|
||||
| ai03 teacher-bff + core-edu | 教学域全栈:BFF 聚合 + 核心业务。考试/作业/成绩状态机在一个人手里,不跨 AI 协调 |
|
||||
| ai04 student-bff + parent-bff | 两个 BFF 都是纯聚合层,技术同质(GraphQL + DataLoader),设计模式完全复用 |
|
||||
| ai05 content + msg | 都依赖 ES,content 建索引、msg 查索引,一人设计避免 ES 索引冲突 |
|
||||
| ai07 前端 4 端 | Module Federation shell + remote 架构需一人统一设计,4 端共享组件库和权限体系 |
|
||||
| coord 不写业务代码 | 专注契约管理 + 交叉审查,保证 7 份设计文档的接口一致性 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 各 AI 阶段 1 必读文档清单
|
||||
|
||||
以下为每个 AI 在阶段 1 必须按顺序阅读的文档(标注 ★ 为强制必读):
|
||||
|
||||
| 顺序 | 文档 | ai01 | ai02 | ai03 | ai04 | ai05 | ai06 | ai07 | coord |
|
||||
| ---- | ------------------------------------------------------------------- | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :---: |
|
||||
| 1 | [README.md](../../README.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 2 | [MIGRATION_GUIDE.md](../../MIGRATION_GUIDE.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 3 | [004 架构影响地图](./004_architecture_impact_map.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 4 | [pending-features.md](./roadmap/pending-features.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 5 | [project_rules.md](../../.trae/rules/project_rules.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 6 | [coding-standards.md](../standards/coding-standards.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 7 | [multi-ai-collaboration.md](../standards/multi-ai-collaboration.md) | ★ | ★ | ★ | ★ | ★ | ★ | ★ | ★ |
|
||||
| 8 | 黄金模板 `services/classes/src/` 全部源码 | ★ | ★ | ★ | ★ | — | — | — | ★ |
|
||||
| 9 | `packages/shared-proto/proto/` 全部 .proto | ★ | ★ | ★ | ★ | ★ | ★ | — | ★ |
|
||||
|
||||
**语言特定补充阅读**:
|
||||
|
||||
| AI | 语言 | 补充文档 |
|
||||
| ------- | -------- | --------------------------------------------------------------- |
|
||||
| ai01 | Go | `services/api-gateway/` 全部源码 |
|
||||
| ai02-05 | TS | `services/iam/`、`services/teacher-bff/` 源码(参考已实现模板) |
|
||||
| ai06 | Python | `services/data-ana/`、`services/ai/` 骨架源码 |
|
||||
| ai07 | TS/React | `apps/teacher-portal/` 全部源码、Module Federation 配置 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 各 AI 阶段 2 设计重点
|
||||
|
||||
### ai01 — Go 网关层
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| api-gateway | 路由表矩阵(路径 → 下游服务 + 端口,需覆盖全部 6 个业务服务 + 3 个 BFF);限流策略表(每路由 QPS);熔断阈值配置(错误率/延迟阈值);JWT RS256 公钥校验流程;CORS 白名单;请求 ID 注入 |
|
||||
| push-gateway | WebSocket 连接生命周期(认证 → 心跳 → 断线重连);与 msg 的 gRPC 推送通道协议;用户 session 映射(在线用户 → WebSocket 连接);水平扩展方案(Redis Pub/Sub 跨实例广播) |
|
||||
|
||||
### ai02 — 身份认证
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| iam | RBAC 权限点枚举(全部模块的 CRUD 权限常量);三层角色模型(系统/组织/临时)权限合并规则;DataScope 6 级 SQL WHERE 注入规则(每级对应的过滤条件);视口 4 层配置表设计(导航/路由/组件/数据);JWT RS256 私钥签发 + 公钥暴露端点;refresh_token 轮换策略;权限解析 API(getEffectivePermissions → permissions + viewports + dataScope) |
|
||||
|
||||
### ai03 — 教学场景域
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| teacher-bff | GraphQL schema(Query/Mutation,按场景域组织);DataLoader 批量去重策略;并行 gRPC 调用编排;聚合结果缓存 TTL 策略(5-30s 短缓存);教师角色差异化(教师 vs 教导主任 vs 教研组长 → 视口推导) |
|
||||
| core-edu | classes 模块黄金模板对齐;考试生命周期状态机(草稿 → 已发布 → 作答中 → 批改中 → 已出分 → 已归档);Outbox 事件定义(ExamPublished、HomeworkSubmitted、GradeRecorded);成绩计算公式与配置化;作业提交高并发优化(Redis 分布式锁 + 排队);排课/考勤数据模型 |
|
||||
|
||||
### ai04 — 学习 + 家长场景域 BFF
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| student-bff | 学生端 GraphQL schema(与 teacher-bff 对比差异);DataLoader 复用 teacher-bff 模式;权限区分(学生只能看自己的数据,DataScope=SELF);考试/作业/成绩的学生视角 API |
|
||||
| parent-bff | 家长端 GraphQL schema;与 iam 的学生-家长关联查询;多子女账户切换设计;家长通知偏好配置 |
|
||||
|
||||
### ai05 — 内容 + 通知
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| content | Neo4j 图模型(知识点 → 知识点前置依赖 → 教材关联);ES 索引 mapping 设计(题库全文检索 + 标签过滤);题库 CRUD 完整 API(含批量导入);与 ai 服务的 gRPC 接口(查询知识点/题库用于 AI 出题);教材/章节结构树 |
|
||||
| msg | 通知渠道抽象(站内信/邮件/短信,策略模式);ES 降级查询策略(DB 不可用时走 ES);Kafka 消费幂等设计(event_id 去重);与 push-gateway 的推送通道协议;通知模板管理;已读/未读状态管理 |
|
||||
|
||||
### ai06 — Python 数据 + AI
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| data-ana | ClickHouse 宽表设计(考试、作业、成绩、掌握度、出勤 5 张宽表);CDC 消费者架构(Debezium → Kafka → ClickHouse);学情分析 API(班级统计/个人趋势/预警阈值);掌握度计算算法(加权滑动平均);Dashboard 数据聚合 |
|
||||
| ai | LLM Provider 适配器模式(OpenAI/百川/本地模型);SSE 流式响应(题目逐字生成);出题 Prompt 模板管理;备课工作流(分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库);用量计费/频率限制 |
|
||||
|
||||
### ai07 — 前端 4 端
|
||||
|
||||
| 服务 | 设计重点 |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| teacher-portal | 现有代码审计对齐黄金标准;Module Federation shell 暴露的共享组件 |
|
||||
| 全部 4 端 | Module Federation shell + remote 架构设计;路由骨架(4 端路由表对照);共享组件库(错误边界 ErrorBoundary、Loading 骨架屏、Empty 空态、权限控制组件);`usePermission().hasPermission()` 统一权限 Hook;API 请求层统一错误处理(toast 提示);4 端差异化对比表(导航菜单/路由/组件/数据 4 层差异) |
|
||||
|
||||
### coord — 协调 AI
|
||||
|
||||
| 职责 | 具体内容 |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| proto 契约维护 | 统一管理 `packages/shared-proto/`,跨 AI 的 proto 变更唯一入口 |
|
||||
| 设计文档交叉审查 | 审查 7 份模块架构设计文档的跨模块接口一致性(接口签名匹配、topic 不重复、端口不冲突、错误码前缀不重叠) |
|
||||
| 黄金模板维护 | 维护 classes 黄金模板标准,审查其他服务对齐情况 |
|
||||
| 架构文档同步 | 各 AI 产出设计文档后,同步更新 `004_architecture_impact_map.md` |
|
||||
| 共享包管理 | shared-ts / shared-go / shared-py 建立与维护 |
|
||||
| CI/CD | `.github/workflows/ci.yml` 覆盖全部 15 服务 |
|
||||
| 基础设施 | `infra/` K8s/Grafana/WAF/灾备(可由 SRE AI 协助) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 阶段 1 交付物模板
|
||||
|
||||
每个 AI 阅读完 §4 的文档清单后,必须产出以下确认书:
|
||||
|
||||
```markdown
|
||||
## 模块理解确认书 — [模块名]
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- 层级:Gateway / BFF / Service / Data / Frontend
|
||||
- 上游:谁调用我?
|
||||
- 下游:我调用谁?
|
||||
- 通信方式:HTTP / gRPC / Kafka / WebSocket / 直接 DB?
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- 我负责哪些聚合/实体?
|
||||
- 我的数据属于哪个业务领域(D1-D6 中的哪个)?
|
||||
- 我不负责什么(明确边界外的东西)?
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
- 我消费哪些 proto message(从 shared-proto)?
|
||||
- 我暴露哪些 API 端点或 gRPC 方法或 Kafka 事件?
|
||||
- 错误码前缀是什么?
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言 / 框架 / ORM / 存储
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- 属于 P1-P6 哪个阶段?
|
||||
- 当前阶段目标是什么?
|
||||
- 依赖哪些上游阶段的产出?
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||||
|
||||
- [ ] 权限装饰器 @RequirePermission(全部 Controller 方法)
|
||||
- [ ] 错误码前缀统一
|
||||
- [ ] logger / metrics / tracer 三支柱
|
||||
- [ ] /healthz + /readyz 健康检查
|
||||
- [ ] 优雅关闭(SIGTERM)
|
||||
- [ ] 测试覆盖率 ≥ 80%
|
||||
- [ ] Dockerfile 多阶段构建
|
||||
- [ ] Zod 输入验证
|
||||
- [ ] GlobalErrorFilter 统一兜底
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 阶段 2 交付物模板
|
||||
|
||||
每个 AI 在阶段 1 确认书通过 coord 审核后,产出以下架构设计文档:
|
||||
|
||||
```markdown
|
||||
## 模块架构设计文档 — [模块名]
|
||||
|
||||
### 1. 模块内部分层图
|
||||
|
||||
[画图:Controller → Guard → Service → Repository → DB 调用链]
|
||||
[标注中间件、Guard、Filter 的拦截点]
|
||||
|
||||
### 2. 领域模型
|
||||
|
||||
- 聚合根:有哪些?
|
||||
- 实体/值对象:有哪些?
|
||||
- 聚合间如何通信?(同服务内直接调用 / 跨服务走事件)
|
||||
|
||||
### 3. 数据模型
|
||||
|
||||
- 有哪些表?(列出 schema,标注字段类型、约束)
|
||||
- 每张表的索引策略(主键、唯一索引、查询索引)
|
||||
- 读写分离策略(哪些走主库、哪些走读模型)
|
||||
|
||||
### 4. API 设计
|
||||
|
||||
| method | path | 权限 | 请求/响应结构 | 说明 |
|
||||
| ------ | ---- | ---------- | ------------- | ---- |
|
||||
| POST | /xxx | XXX_CREATE | { ... } | ... |
|
||||
|
||||
### 5. 事件设计(如适用)
|
||||
|
||||
- 我发布哪些领域事件?触发时机是什么?
|
||||
- 我消费哪些外部事件?消费后做什么?
|
||||
- 事件 Topic 名称(遵循 `edu.<domain>.<aggregate>.<action>` 格式)
|
||||
|
||||
### 6. 横切关注点对齐清单
|
||||
|
||||
- [ ] 权限装饰器(列出所有端点及对应权限常量)
|
||||
- [ ] 错误码清单(带前缀,每个错误码 → 触发条件 → HTTP 状态码)
|
||||
- [ ] Logger 初始化位置与配置(pino/zap/structlog)
|
||||
- [ ] Metrics 指标清单(指标名 / 类型 / 标签 / 描述)
|
||||
- [ ] Tracer 初始化位置(OTLP endpoint)
|
||||
- [ ] /healthz 检查逻辑
|
||||
- [ ] /readyz 检查逻辑(DB SELECT 1 / Redis PING / Kafka 连接)
|
||||
- [ ] 优雅关闭顺序(HTTP server → DB → Redis → Kafka)
|
||||
|
||||
### 7. 与其他模块的交互点(契约清单)
|
||||
|
||||
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
|
||||
| ------ | -------- | ----- | --------- | ---- |
|
||||
| 调用 | xxx | gRPC | XxxMethod | ... |
|
||||
| 被调用 | xxx | gRPC | YyyMethod | ... |
|
||||
| 发布 | — | Kafka | topic 名 | ... |
|
||||
| 消费 | — | Kafka | topic 名 | ... |
|
||||
|
||||
### 8. 风险与假设
|
||||
|
||||
- 我假设 [某服务] 提供了 [某接口],如果没提供我的 fallback 是什么?
|
||||
- 我的模块有哪些技术风险?(性能瓶颈、数据一致性、外部依赖)
|
||||
- 有哪些未决的设计决策需要协调 AI 仲裁?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 交叉审查规则
|
||||
|
||||
coord 收到全部 7 份设计文档后,执行以下审查:
|
||||
|
||||
### 8.1 接口一致性检查
|
||||
|
||||
```markdown
|
||||
| 服务 A 说 | 服务 B 说 | 是否匹配 |
|
||||
| --------------------------------------------------------- | ---------------------------------------------- | --------- |
|
||||
| ai02 iam: 暴露 getUserInfo(userId) | ai03 teacher-bff: 调用 iam.getUserInfo(userId) | ✅ |
|
||||
| ai03 core-edu: 调用 content.getKnowledgePoints(subjectId) | ai05 content: ??? | ⚠️ 待确认 |
|
||||
```
|
||||
|
||||
### 8.2 全局冲突检查
|
||||
|
||||
| 检查项 | 检查方式 |
|
||||
| -------------------- | ---------------------------------------------------------------------- |
|
||||
| 端口不冲突 | 对照 [full-stack-runbook](../standards/full-stack-runbook.md) 端口矩阵 |
|
||||
| Topic 不重复 | 汇总全部 AI 的 §5 事件设计,去重检查 |
|
||||
| 错误码前缀不重叠 | 汇总全部 AI 的 §6 错误码清单,前缀唯一性检查 |
|
||||
| Proto message 不遗漏 | 检查全部"跨模块交互点"是否在 proto 中有对应定义 |
|
||||
|
||||
### 8.3 黄金模板对齐检查
|
||||
|
||||
| 检查项 | 全部 TS NestJS 服务 |
|
||||
| ----------------------- | ---------------------- |
|
||||
| @RequirePermission 覆盖 | 每个 Controller 方法 |
|
||||
| 错误码前缀 | 用服务名大写前缀 |
|
||||
| /healthz + /readyz | 存在且逻辑正确 |
|
||||
| Zod 输入验证 | Controller 层解析 body |
|
||||
| GlobalErrorFilter | 注册到 AppModule |
|
||||
| Dockerfile 多阶段 | builder + runtime |
|
||||
|
||||
---
|
||||
|
||||
## 9. 协作规则
|
||||
|
||||
### 9.1 单仓库并行开发
|
||||
|
||||
当前阶段采用**单仓库直接 push main**模式,不经过 PR:
|
||||
|
||||
- 每个 AI 只能修改自己负责的目录(见 §3.2)
|
||||
- `packages/shared-proto/` 仅 coord 修改,其他 AI 只读
|
||||
- `docs/`、`.trae/`、`infra/`、`.github/` 仅 coord 修改
|
||||
|
||||
### 9.2 唯一冲突文件处理
|
||||
|
||||
`pnpm-lock.yaml` 是唯一可能多 AI 同时修改的文件,冲突时:
|
||||
|
||||
```bash
|
||||
git pull origin main --rebase
|
||||
# 冲突时:
|
||||
git checkout --theirs pnpm-lock.yaml # 取远程版本
|
||||
pnpm install # 重新生成
|
||||
git add pnpm-lock.yaml
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
### 9.3 提交规范
|
||||
|
||||
```bash
|
||||
# 每个 AI 在自己的服务目录内工作
|
||||
git add services/<service>/...
|
||||
git commit -m "docs(<service>): 模块架构设计文档"
|
||||
|
||||
# 或
|
||||
git commit -m "docs(<service>): 阶段1理解确认书"
|
||||
```
|
||||
|
||||
### 9.4 proto 变更流程
|
||||
|
||||
任何 AI 需要新增/修改 proto:
|
||||
|
||||
1. 在共享协调渠道声明需求(格式:`# proto-change: <描述>`)
|
||||
2. coord 统一修改 `packages/shared-proto/`
|
||||
3. coord 通知受影响 AI 更新设计文档
|
||||
|
||||
---
|
||||
|
||||
## 10. 审计模板(阶段 1 自检用)
|
||||
|
||||
每个 AI 审计自己负责的已实现服务时,填写下表:
|
||||
|
||||
```markdown
|
||||
## 服务审计表 — [AI标识]
|
||||
|
||||
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
|
||||
| ---- | ---------- | ---------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------- | ---------- |
|
||||
| xxx | ✅/❌/⚠️ | ✅/❌/⚠️ | ✅/❌/⚠️ | ✅/❌/⚠️ | ✅/❌/⚠️ | ✅/❌/⚠️ | ✅/❌/⚠️ | ✅/❌/⚠️ | XX% | ✅/❌/⚠️ |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 相关文档
|
||||
|
||||
- [多 AI 协作指南](../standards/multi-ai-collaboration.md) — 日常开发协作流程
|
||||
- [004 架构影响地图](./004_architecture_impact_map.md) — 全局架构与依赖
|
||||
- [项目规则](../../.trae/rules/project_rules.md) — 强制约束
|
||||
- [编码规范](../standards/coding-standards.md) — 多语言编码标准
|
||||
- [待开发功能路线图](./roadmap/pending-features.md) — 六阶段目标
|
||||
216
docs/architecture/ai03-phase1-understanding.md
Normal file
216
docs/architecture/ai03-phase1-understanding.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# ai03 阶段 1 交付物:模块理解确认书
|
||||
|
||||
> AI 标识:ai03
|
||||
> 负责模块:teacher-bff(P2)、core-edu(P3)
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)
|
||||
> 日期:2026-07-09
|
||||
> 关联文档:[ai-allocation.md](./ai-allocation.md)、[004 架构影响地图](./004_architecture_impact_map.md)、[pending-features.md](./roadmap/pending-features.md)
|
||||
|
||||
---
|
||||
|
||||
## 模块理解确认书 — teacher-bff
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- **层级**:BFF 聚合层(L4)
|
||||
- **上游**:api-gateway(Go/Gin)通过 HTTP 转发请求,注入 `x-user-id` / `x-user-roles` 头
|
||||
- **下游**:iam(3002)、classes(3001)、core-edu(3004);P3 后扩展 content、data-ana、ai
|
||||
- **通信方式**:
|
||||
- 当前:REST `fetch`(同步)
|
||||
- 目标态(004 §4.1 / pending-features P2):**gRPC** 调下游业务服务 + **GraphQL Yoga** 对前端暴露 + DataLoader 防 N+1
|
||||
- **端口**:3003(见 [teacher-bff env.ts](../../services/teacher-bff/src/config/env.ts))
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- **聚合职责**:教学场景域(教师 / 教导主任 / 教研组长 共用)的数据聚合、裁剪、协议转换
|
||||
- **业务领域**:跨 D2 教学组织 + D3 教学核心 + D1 身份认证(只读拉取视口/权限)
|
||||
- **我不负责**:
|
||||
- 不持有业务状态(无 DB 写入,无 Outbox)
|
||||
- 不做权限决策(依赖 Gateway JWT 校验 + 下游服务 `@RequirePermission`)
|
||||
- 不直接访问任何业务服务数据库
|
||||
- **复用策略**(004 §5.4):教导主任 / 教研组长复用 Teacher BFF,通过视口差异化(L1 导航扩展管理菜单,L4 DataScope 扩大到年级)
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
- **消费的 proto message**:
|
||||
- `iam.v1.IamService`:GetUserInfo / getEffectivePermissions(视口 + DataScope)
|
||||
- `classes.v1.ClassService`:ListClasses / GetClass
|
||||
- `core_edu.v1.ExamService / HomeworkService / GradeService`:全部 RPC
|
||||
- **暴露的 API**(当前 REST,目标 GraphQL):
|
||||
- `GET /teacher/dashboard` — 聚合 IAM 用户信息 + classes 列表
|
||||
- `GET /teacher/viewports` — 拉取 IAM 视口配置(L1 导航)
|
||||
- `GET /teacher/classes/:classId/exams` — 聚合 core-edu 考试列表
|
||||
- `GET /teacher/classes/:classId/homework` — 聚合 core-edu 作业列表
|
||||
- `GET /teacher/exams/:examId/grades` — 聚合 core-edu 成绩列表
|
||||
- **错误码前缀**:BFF 自身错误用 `BAD_GATEWAY`(下游不可达);业务错误透传下游 `CORE_EDU_*` / `IAM_*` / `CLASSES_*`
|
||||
- **缓存**:聚合结果 Redis 短缓存 5-30s(004 §6.3,当前未实现)
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言:TypeScript 5.5+(ESM 模式,相对 import 带 `.js` 后缀)
|
||||
- 框架:NestJS 10
|
||||
- 下游通信:当前 `fetch`(REST)→ 目标 `@grpc/grpc-js` + `@bufbuild/protobuf`
|
||||
- 对前端:目标 GraphQL Yoga + DataLoader
|
||||
- 缓存:Redis(待引入)
|
||||
- 可观测:pino logger + prom-client metrics + OpenTelemetry tracer(已具备 [tracer.ts](../../services/teacher-bff/src/shared/observability/tracer.ts))
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- **P2 身份**:教师登录 → 获取 JWT → 访问 teacher-portal → 侧边栏按 viewports.L1 渲染 → 空白 Dashboard
|
||||
- **P3 扩展**:考试/作业/成绩的 GraphQL 查询与 mutation
|
||||
- **依赖上游**:P1 黄金模板 classes、P2 iam(getEffectivePermissions + 视口)
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||||
|
||||
- [ ] 权限装饰器 `@RequirePermission`(**BFF 不做权限决策**,当前无;目标态:BFF 不加 Guard,仅校验 `x-user-id` 存在)
|
||||
- [x] 错误处理:[GlobalErrorFilter](../../services/teacher-bff/src/shared/errors/global-error.filter.ts) + ApplicationError 层次
|
||||
- [x] logger / metrics / tracer 三支柱(已具备)
|
||||
- [x] `/healthz` 健康检查(HealthModule 已注册)
|
||||
- [ ] `/readyz`(当前 HealthController 仅 liveness,无下游就绪探针)
|
||||
- [x] 优雅关闭 SIGTERM(main.ts 已处理)
|
||||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件)
|
||||
- [ ] Dockerfile 多阶段构建(需核对)
|
||||
- [ ] Zod 输入验证(当前 Controller 直接透传 unknown,**未做 Zod 校验**)
|
||||
- [x] GlobalErrorFilter 统一兜底
|
||||
|
||||
---
|
||||
|
||||
## 模块理解确认书 — core-edu
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5)
|
||||
- **上游**:teacher-bff(聚合层)、api-gateway(直接路由 `/api/v1/exams` 等)
|
||||
- **下游**:MySQL(独占库)、Kafka(事件发布)、Redis(待引入,高并发提交锁)
|
||||
- **通信方式**:
|
||||
- 入口:当前 REST(`/exams`、`/homework`、`/grades`)
|
||||
- 目标态(proto 注释):P3 起转 gRPC(`core_edu.v1.ExamService/HomeworkService/GradeService` 已定义)
|
||||
- 出口:Kafka 事件(Outbox 模式发布)
|
||||
- **端口**:3004(见 [core-edu env.ts](../../services/core-edu/src/config/env.ts))
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- **聚合职责**:跨 **D2 教学组织**(classes 模块,待合并)+ **D3 教学核心**(exams / homework / grades)
|
||||
- **聚合根**:Exam、Homework、Grade、Class(待合并)
|
||||
- **我不负责**:
|
||||
- 不负责题库内容(→ content 服务)
|
||||
- 不负责学情分析(→ data-ana 服务,消费 core-edu 事件)
|
||||
- 不负责通知投递(→ msg 服务,消费 core-edu 事件)
|
||||
- **数据自治**:独占 `core_edu` 数据库,表前缀 `core_edu_*`
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
- **暴露的 gRPC 契约**([core_edu.proto](../../packages/shared-proto/proto/core_edu.proto),已定义待实现):
|
||||
- `ExamService`:CreateExam / GetExam / ListExamsByClass / UpdateExam / DeleteExam
|
||||
- `HomeworkService`:AssignHomework / GetHomework / ListHomeworkByClass / SubmitHomework
|
||||
- `GradeService`:RecordGrade / GetGrade / ListGradesByStudent/Exam/Homework
|
||||
- **发布的领域事件**([events.proto](../../packages/shared-proto/proto/events.proto) + [outbox.publisher.ts TOPIC_MAP](../../services/core-edu/src/shared/outbox/outbox.publisher.ts)):
|
||||
|
||||
| 事件 | Topic | 触发时机 | 消费者 |
|
||||
| ------------------ | ------------------- | --------------------- | ------------- |
|
||||
| exam.created | edu.exam.events | CreateExam 事务内 | msg、data-ana |
|
||||
| exam.updated | edu.exam.events | UpdateExam | msg |
|
||||
| exam.deleted | edu.exam.events | DeleteExam | data-ana |
|
||||
| homework.assigned | edu.homework.events | AssignHomework | msg、data-ana |
|
||||
| homework.submitted | edu.homework.events | SubmitHomework | data-ana、msg |
|
||||
| homework.graded | edu.homework.events | **未实现**(P3 待补) | msg、data-ana |
|
||||
| grade.recorded | edu.grade.events | RecordGrade | data-ana、msg |
|
||||
| grade.updated | edu.grade.events | **未实现**(P3 待补) | data-ana |
|
||||
| class.transferred | edu.class.events | classes 合并后 | data-ana |
|
||||
|
||||
- **消费的事件**:`edu.identity.user.created`(IAM,初始化教师默认班级关联,**当前未消费**,P3 待补)
|
||||
- **错误码前缀**:`CORE_EDU_*`(见 [application-error.ts CoreEduErrorCode](../../services/core-edu/src/shared/errors/application-error.ts))
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言:TypeScript 5.5+(ESM 模式)
|
||||
- 框架:NestJS 10
|
||||
- ORM:Drizzle ORM(mysql2 driver,直接 `db` 导出,**与 classes 的 `getDb()` 不一致**)
|
||||
- 存储:MySQL 8(独占库)、Redis(待引入)、Kafka(kafkajs,idempotent + transactionalId)
|
||||
- 可观测:pino + prom-client + OTel(已具备)
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- **P3 核心教学**:考试全生命周期 + Outbox + Kafka 事件落地
|
||||
- **退出标准**:教师创建考试 → 发布 → 学生作答 → 教师批改 → 事件到 Kafka → 成绩统计更新 → 全链路可观测
|
||||
- **依赖上游**:P1 classes 黄金模板、P2 iam(用户身份 + 权限)
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||||
|
||||
- [x] 权限装饰器 `@RequirePermission`(exams.controller 全覆盖;需核对 homework/grades controller)
|
||||
- [x] 错误码前缀 `CORE_EDU_*`(已用 [CoreEduErrorCode 枚举](../../services/core-edu/src/shared/errors/application-error.ts))
|
||||
- [x] logger / metrics / tracer 三支柱
|
||||
- [x] `/healthz` 健康检查
|
||||
- [ ] `/readyz`(需补 DB SELECT 1 / Kafka 连接探针)
|
||||
- [x] 优雅关闭 SIGTERM(main.ts 已处理 outboxPublisher.stop + disconnectKafka)
|
||||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件)
|
||||
- [ ] Dockerfile 多阶段构建(需核对)
|
||||
- [ ] Zod 输入验证(**当前 Controller 直接接收 body,未 Zod 校验**;classes 用 zod schema)
|
||||
- [x] GlobalErrorFilter 统一兜底
|
||||
- [x] Outbox 模式(事务内写业务表 + outbox 表,独立 publisher 投递)
|
||||
|
||||
---
|
||||
|
||||
## §10 服务审计表 — ai03
|
||||
|
||||
> 对照 [黄金模板 classes 服务](../../services/classes/src/),审计已实现的两服务。状态:✅ 达标 / ⚠️ 部分 / ❌ 缺失
|
||||
|
||||
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
|
||||
| ----------- | --------------------------------------- | --------------------------------- | ------- | -------------- | ------- | -------- | ------- | -------- | ---------- | ---------- |
|
||||
| teacher-bff | ⚠️ 无(BFF 不做权限决策,依赖 Gateway) | ⚠️ 用 `BAD_GATEWAY`(无自有前缀) | ✅ pino | ✅ prom-client | ✅ OTel | ✅ | ❌ | ✅ | 0% ❌ | 待核对 |
|
||||
| core-edu | ✅ `@RequirePermission(EXAM_*)` 全覆盖 | ✅ `CORE_EDU_*` | ✅ pino | ✅ prom-client | ✅ OTel | ✅ | ❌ | ✅ | 0% ❌ | 待核对 |
|
||||
|
||||
### 审计发现的关键差距(P3 阶段 2 设计需解决)
|
||||
|
||||
**teacher-bff**:
|
||||
|
||||
1. ❌ 通信方式:当前 REST `fetch`,目标 gRPC + GraphQL(P2 退出标准要求 GraphQL Yoga + DataLoader)
|
||||
2. ❌ 无 Redis 聚合缓存(004 §6.3 要求 5-30s 短缓存)
|
||||
3. ❌ 无 DataLoader(防 N+1,pending-features P2 明确要求)
|
||||
4. ❌ 无 `/readyz` 下游就绪探针
|
||||
5. ⚠️ env 配置用 `IamServiceUrl`/`ClassesServiceUrl`(REST URL),转 gRPC 后需改为 gRPC target
|
||||
6. ⚠️ 无 Zod 输入验证
|
||||
7. ⚠️ 无测试
|
||||
|
||||
**core-edu**:
|
||||
|
||||
1. ❌ 考试生命周期状态机缺失(当前仅 `draft` 初值,无 `published → in_progress → grading → graded → archived` 转换与校验)
|
||||
2. ❌ 作业状态机不完整(仅 `assigned → submitted`,缺 `graded`;pending-features 要求 `HomeworkGraded` 事件)
|
||||
3. ❌ 成绩录入无业务校验(不校验 exam/homework 是否存在、score 是否在 totalScore 范围内、是否重复录入)
|
||||
4. ❌ 作业提交高并发优化缺失(004 §9.2 要求 Redis 分布式锁 + 排队)
|
||||
5. ❌ 无 `grade.updated` / `homework.graded` 事件触发点(proto 已定义,service 未实现)
|
||||
6. ❌ 未消费 IAM `user.created` 事件(初始化教师默认关联)
|
||||
7. ⚠️ Drizzle `db` 直接导出 vs classes 的 `getDb()` 函数式 — **不一致**,建议统一为 `getDb()`
|
||||
8. ⚠️ [kafka.ts](../../services/core-edu/src/config/kafka.ts) 用 `console.log`/`console.warn`,应改用结构化 logger
|
||||
9. ⚠️ classes 模块在 core-edu 仅有 `classes.module.ts` 占位,**P3 待合并**(classes 服务代码迁入 + 删除独立 services/classes)
|
||||
10. ⚠️ 入口仍为 REST,proto gRPC 契约已定义但未接入 `@grpc/grpc-js` + buf generate 代码
|
||||
11. ❌ 无 Zod 输入验证(Controller 直接接收 `body: CreateExamInput`,未走 zod schema)
|
||||
12. ❌ 无测试
|
||||
|
||||
### 跨模块契约对齐待确认项(提请 coord 交叉审查)
|
||||
|
||||
| 待确认项 | 我方期望 | 对方模块 | 状态 |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| iam `getEffectivePermissions(userId)` 返回结构 | `{permissions, viewports, dataScope}` | iam(ai02) | ⚠️ 当前 teacher-bff 调 `/iam/viewports` 与 `/iam/me`,未定义此聚合 API 的 proto |
|
||||
| iam `user.created` 事件 topic | `edu.identity.user.created` | iam(ai02) | ⚠️ 004 §7.2 定义,但 core-edu 未消费,需确认 iam 是否发布 |
|
||||
| core-edu 端口 3004 | 不冲突 | 全局端口矩阵 | 待 coord 核对 |
|
||||
| Kafka topic 命名 | `edu.exam.events` / `edu.homework.events` / `edu.grade.events` / `edu.class.events` | 004 §7.2 用 `edu.teaching.*` 前缀 | ⚠️ **不一致**:004 文档用 `edu.teaching.exam.published`,代码用 `edu.exam.events`,需 coord 仲裁统一 |
|
||||
| data-ana 消费 core-edu 事件 | 消费 `exam.created` / `homework.submitted` / `grade.recorded` | data-ana(ai06) | 待 ai06 确认消费契约 |
|
||||
| msg 消费 core-edu 事件 | 消费 `exam.created` / `homework.assigned` / `grade.recorded` 触发通知 | msg(ai05) | 待 ai05 确认消费契约 |
|
||||
|
||||
---
|
||||
|
||||
## 下一步(阶段 2 入口)
|
||||
|
||||
待 coord 审核本确认书通过后,ai03 进入阶段 2,按 [ai-allocation.md §5 ai03 设计重点](./ai-allocation.md#ai03--教学场景域) 产出两份模块架构设计文档:
|
||||
|
||||
1. **teacher-bff 模块架构设计**:GraphQL schema(Query/Mutation 按场景域组织)、DataLoader 批量去重、并行 gRPC 编排、聚合缓存 TTL、教师角色差异化视口推导
|
||||
2. **core-edu 模块架构设计**:classes 黄金模板对齐、考试生命周期状态机、Outbox 事件定义、成绩计算配置化、作业提交高并发(Redis 锁 + 排队)、排课/考勤数据模型
|
||||
|
||||
阶段 2 设计需先解决上述 12 项差距与 6 项跨模块契约对齐。
|
||||
|
||||
---
|
||||
|
||||
**AI Agent**: ai03 (teacher-bff + core-edu)
|
||||
**Coordinator**: coord-ai
|
||||
**Branch**: 单仓库并行模式(直接 push main)
|
||||
269
docs/architecture/ai05-phase1-understanding.md
Normal file
269
docs/architecture/ai05-phase1-understanding.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# ai05 阶段 1 交付物:模块理解确认书
|
||||
|
||||
> AI 标识:ai05
|
||||
> 负责模块:content(P4)、msg(P5)
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)
|
||||
> 日期:2026-07-09
|
||||
> 关联文档:[ai-allocation.md](./ai-allocation.md)、[004 架构影响地图](./004_architecture_impact_map.md)、[pending-features.md](./roadmap/pending-features.md)、[known-issues.md](../troubleshooting/known-issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 模块理解确认书 — content
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5),DDD 限界上下文
|
||||
- **业务领域**:D4 内容资源领域(004 §1.1b)
|
||||
- **上游**(谁调用我):
|
||||
- teacher-bff(3003):教学场景聚合,教师查教材/知识点/题库
|
||||
- student-bff(学习场景,P3 起):学生查学习路径、知识点前置
|
||||
- ai(Python,P5):gRPC 查询知识点/题库用于 AI 出题(004 §4.1 `AI -.gRPC.-> Content`,§9.3 AI 辅助出题流程)
|
||||
- **下游**(我调用谁):
|
||||
- MySQL(写模型主库,独占)
|
||||
- Neo4j(知识图谱,前置依赖图)
|
||||
- Elasticsearch(题库全文检索,P4 后续补充,当前未实现)
|
||||
- **通信方式**:
|
||||
- 当前:HTTP REST(Controller,无 gRPC controller)
|
||||
- 目标态(004 §4.1 / pending-features P4):gRPC 暴露 `TextbookService` + `KnowledgeGraphService`
|
||||
- Kafka:消费 core-edu 教学内容变更通知(004 §4.1 `CoreEdu -.事件.-> Content`);发布 `edu.content.question.published`(004 §7.2)
|
||||
- **端口**:3005(见 [content env.ts](../../services/content/src/config/env.ts))
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- **聚合职责**:管理 Textbook(教材)、Chapter(章节)、KnowledgePoint(知识点)、Question(题库)四个聚合
|
||||
- **我的数据属于**:D4 内容资源领域
|
||||
- **我不负责**:
|
||||
- 不负责学情分析(D6,由 data-ana 承载)
|
||||
- 不负责考试/作业/成绩(D3,由 core-edu 承载)
|
||||
- 不负责通知分发(D5,由 msg 承载)
|
||||
- 不直接访问 core-edu / iam / msg 的数据库
|
||||
- **现有骨架领域模块**(4 个,见 [content/src](../../services/content/src)):
|
||||
- `textbooks/`:教材 CRUD(5 端点)
|
||||
- `chapters/`:章节 CRUD(5 端点,按 textbook 查询)
|
||||
- `knowledge-points/`:知识点 CRUD + Neo4j 知识图谱(7 端点,含前置链路查询/添加)
|
||||
- `questions/`:题库 CRUD(5 端点,4 种题型校验)
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
- **消费的 proto message**(从 shared-proto):
|
||||
- 无直接消费其他服务 proto;通过 Kafka 事件接收 core-edu 教学内容变更(事件契约见 `events.proto`)
|
||||
- **暴露的契约**(见 [content.proto](../../packages/shared-proto/proto/content.proto),包名 `next_edu_cloud.content.v1`):
|
||||
- `TextbookService`:CreateTextbook / GetTextbook / ListTextbooks
|
||||
- `KnowledgeGraphService`:GetPrerequisites / GetLearningPath
|
||||
- **当前实现均为 REST,gRPC controller 未实现**(proto 已定义待迁移)
|
||||
- **事件契约**:
|
||||
- 发布:`edu.content.question.published`(题库新增/发布,消费者 AI、ES,见 004 §7.2)
|
||||
- 消费:core-edu 教学内容变更事件(004 §4.1,具体 topic 待 core-edu ai03 设计确认)
|
||||
- **错误码前缀**:`CONTENT_*`(CONTENT_VALIDATION_ERROR / NOT_FOUND / PERMISSION_DENIED / CONFLICT / BUSINESS_ERROR / DATABASE_ERROR / INTERNAL_ERROR,见 [application-error.ts](../../services/content/src/shared/errors/application-error.ts))
|
||||
- **权限点**(16 个,见 [permission.guard.ts](../../services/content/src/middleware/permission.guard.ts)):
|
||||
- `CONTENT_TEXTBOOK_{CREATE,READ,UPDATE,DELETE}`
|
||||
- `CONTENT_CHAPTER_{CREATE,READ,UPDATE,DELETE}`
|
||||
- `CONTENT_QUESTION_{CREATE,READ,UPDATE,DELETE}`
|
||||
- `CONTENT_KNOWLEDGE_POINT_{CREATE,READ,UPDATE,DELETE}`
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言:TypeScript 5.6(ESM 模式,相对 import 带 `.js` 后缀)
|
||||
- 框架:NestJS 10
|
||||
- ORM:Drizzle ORM 0.31 + mysql2 3.11
|
||||
- 知识图谱:neo4j-driver 5.23(PREREQUISITE_OF 关系,Cypher 查询深度 1..5)
|
||||
- 全文检索:Elasticsearch(**待引入**,env.ts 预留 ES_URL 但 package.json 未装 @elastic/elasticsearch)
|
||||
- 可观测:pino logger + prom-client metrics + OpenTelemetry tracer(三支柱已具备)
|
||||
- 消息总线:Kafka(**待引入**,pending-features P4 未明确要求 content 发事件,但 004 §7.2 列了 `edu.content.question.published`)
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- **P4 内容分析阶段**(pending-features §P4):
|
||||
- 教材/章节/知识点 CRUD(仅 CRUD,不实现检索)+ 知识图谱查询(Neo4j)+ 题库 CRUD(不实现检索)
|
||||
- MySQL schema:textbooks / chapters / knowledge_points / questions
|
||||
- Neo4j 数据:知识点前置依赖图(从 MySQL 同步)
|
||||
- CDC 链路:Debezium 监听 MySQL binlog → Kafka → data-ana 消费写 ClickHouse
|
||||
- **退出标准**:教师查看知识图谱前置依赖(Neo4j 秒级返回)→ 学生查看学情诊断(ClickHouse 宽表 5s 内返回)→ CDC 链路延迟 < 5s
|
||||
- **依赖上游**:P1 黄金模板 classes(横切关注点对齐)、P3 core-edu(教学内容变更事件,待 ai03 设计确认 topic)
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||||
|
||||
- [x] 权限装饰器 `@RequirePermission`(16 个 CONTENT_* 权限点,全部 Controller 方法已覆盖)
|
||||
- [x] 错误码前缀统一(`CONTENT_*`)
|
||||
- [x] logger / metrics / tracer 三支柱(已具备)
|
||||
- [x] `/healthz` 健康检查(HealthModule 已注册)
|
||||
- [⚠️] `/readyz`(**仅检查 DB `SELECT 1`,未检查 Neo4j 连通性**,Neo4j 故障时仍返回 ok)
|
||||
- [x] 优雅关闭 SIGTERM(main.ts 已处理:closeNeo4j → closeDb → shutdownTracer)
|
||||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件)
|
||||
- [x] Dockerfile 多阶段构建(已具备)
|
||||
- [⚠️] Zod 输入验证(questions 用 service 层手动 if 校验抛 ValidationError,**非 Controller 层 schema.parse**)
|
||||
- [x] GlobalErrorFilter 统一兜底
|
||||
|
||||
### 7. 现有骨架差距与待决策(提请 coord 仲裁)
|
||||
|
||||
| # | 差距 | 影响 | 提请决策 |
|
||||
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| C1 | **ES 完全未实现**:env.ts 预留 ES_URL 但无 @elastic/elasticsearch 依赖、无 config/elasticsearch.ts、无 service 调用 | P4 退出标准要求题库检索,pending-features 注明"P4 仅 CRUD,P5 引入 ES" | 确认 content 的 ES 检索归属 P4 还是 P5(004 §2.2 列 ES 使用者为 Content、AI) |
|
||||
| C2 | **无 Outbox / Kafka**:骨架无 shared/outbox/,无 Kafka producer/consumer | 004 §7.2 列 `edu.content.question.published` 事件需发布 | 确认 content 是否需 Outbox(iam 无 Outbox,core-edu 有) |
|
||||
| C3 | **README 与实现脱节**:README 声称 `TextbooksService.createKnowledgeGraph` 和 `getPrerequisites`,源码中 createKnowledgeGraph 不存在,getPrerequisites 在 KnowledgePointsService | 文档误导 | 阶段 2 设计需同步修正 README |
|
||||
| C4 | **gRPC 未实现**:proto 定义了 TextbookService/KnowledgeGraphService,但无 gRPC controller | pending-features P4 未强制 gRPC,004 §4.1 目标态 gRPC | 确认 P4 是否启用 gRPC(ai03 提请统一决策) |
|
||||
| C5 | **schema 定义分散**:textbooks.schema.ts 定义 3 张表(textbooks/chapters/knowledgePoints),chapters/knowledge-points schema 仅 re-export;questions.schema.ts 独立 | 维护成本 | 阶段 2 设计统一 schema 归属 |
|
||||
| C6 | **DB 连接模式与 iam 不一致**:content 用模块级 `const db`,iam 用 `getDb()` 函数式懒加载 | 测试 mock 困难 | coord 已在 known-issues 记录"db 常量导出对齐黄金模板",需确认统一方向 |
|
||||
| C7 | **表时间戳不统一**:textbooks/questions 有 created_at+updated_at,chapters/knowledge_points 无时间戳 | 审计追踪缺失 | 阶段 2 设计补齐 |
|
||||
| C8 | **无外键约束**:questions.knowledge_point_id → knowledge_points.id 等无 Drizzle 外键 | 引用完整性靠应用层 | 阶段 2 设计评估是否加外键 |
|
||||
| C9 | **addPrerequisite 降级策略不一致**:safeCreateNode 非阻塞(Neo4j 失败仅 warn),addPrerequisite 在 Neo4j 不可用时抛 InternalError | 行为不一致 | 阶段 2 设计统一降级策略 |
|
||||
| C10 | **proto 包名**:实际 `next_edu_cloud.content.v1`,project_rules §5 规定 `edu.content.v1` | 命名规范不一致 | **coord 仲裁**(ai03 已提请) |
|
||||
|
||||
---
|
||||
|
||||
## 模块理解确认书 — msg
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5),DDD 限界上下文
|
||||
- **业务领域**:D5 沟通通知领域(004 §1.1b)
|
||||
- **上游**(谁调用我):
|
||||
- teacher-bff / student-bff / parent-bff:聚合层调 msg 查询用户通知列表、发送通知
|
||||
- api-gateway:REST 转发通知请求
|
||||
- Kafka:消费 core-edu / iam 事件触发通知(004 §4.1 `CoreEdu -.事件.-> Msg`、`IAM -.事件.-> Msg`)
|
||||
- **下游**(我调用谁):
|
||||
- MySQL(写模型主库,独占)
|
||||
- Elasticsearch(全文检索,已实现 safeSearch)
|
||||
- push-gateway(gRPC 推送通道,004 §4.1 `PushGW → Msg`,当前用 fetch POST /internal/push 降级)
|
||||
- **通信方式**:
|
||||
- 当前:HTTP REST(Controller,无 gRPC controller)
|
||||
- 目标态(004 §4.1 / pending-features P5):gRPC 暴露 `NotificationService`
|
||||
- Kafka:消费 core-edu(ExamPublished/HomeworkGraded/GradeRecorded)、iam(UserRegistered)事件(004 §7.3)
|
||||
- **端口**:3007(见 [msg env.ts](../../services/msg/src/config/env.ts))
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- **聚合职责**:管理 Notification(通知)聚合 + NotificationPreference(用户通知偏好)
|
||||
- **我的数据属于**:D5 沟通通知领域
|
||||
- **我不负责**:
|
||||
- 不负责 WebSocket 长连接管理(由 push-gateway 承载)
|
||||
- 不负责业务数据变更(仅消费事件触发通知)
|
||||
- 不直接访问 core-edu / iam 的数据库
|
||||
- **现有骨架领域模块**(1 个,见 [msg/src](../../services/msg/src)):
|
||||
- `notifications/`:通知 CRUD + ES 全文检索 + Push Gateway 推送 + 用户偏好(6 端点)
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
- **消费的 proto message**(从 shared-proto):
|
||||
- 消费 `events.proto` 的事件 message(ClassEvent/ExamEvent/HomeworkEvent/GradeEvent)
|
||||
- **events.proto 当前无 NotificationEvent**,若 msg 发事件需补充
|
||||
- **暴露的契约**(见 [msg.proto](../../packages/shared-proto/proto/msg.proto),包名 `next_edu_cloud.msg.v1`):
|
||||
- `NotificationService`:SendNotification / ListNotifications / MarkAsRead / SearchNotifications
|
||||
- **当前实现均为 REST,gRPC controller 未实现**
|
||||
- **事件契约**:
|
||||
- 消费(004 §7.2 / §7.3):
|
||||
- `edu.identity.user.created` / `edu.identity.user.updated`(IAM 发,msg 发欢迎通知)
|
||||
- `edu.teaching.exam.published`(core-edu 发,msg 推送考试通知给学生)
|
||||
- `edu.teaching.assignment.submitted`(core-edu 发,msg 通知教师)
|
||||
- `edu.teaching.grade.recorded`(core-edu 发,msg 通知学生)
|
||||
- `edu.insight.mastery.updated`(data-ana 发,msg 触发预警)
|
||||
- 发布:无明确(pending-features 未要求 msg 发事件)
|
||||
- **错误码前缀**:`MSG_*`(MSG_VALIDATION_ERROR / NOT_FOUND / PERMISSION_DENIED / CONFLICT / BUSINESS_ERROR / DATABASE_ERROR / INTERNAL_ERROR,见 [application-error.ts](../../services/msg/src/shared/errors/application-error.ts))
|
||||
- **权限点**(3 个,见 [permission.guard.ts](../../services/msg/src/middleware/permission.guard.ts)):
|
||||
- `MSG_NOTIFICATION_SEND`、`MSG_NOTIFICATION_READ`、`MSG_NOTIFICATION_MANAGE`
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言:TypeScript 5.6(ESM 模式)
|
||||
- 框架:NestJS 10
|
||||
- ORM:Drizzle ORM 0.31 + mysql2 3.11
|
||||
- 全文检索:@elastic/elasticsearch 8.15(已实现 safeIndex/safeSearch,**无 mapping 定义**,依赖动态 mapping)
|
||||
- 消息总线:kafkajs 2.2(**已装依赖但无 consumer/producer 代码**,env.ts 有 KAFKA_BROKERS 默认值)
|
||||
- 推送:fetch POST 到 push-gateway `/internal/push`(降级模式,PUSH_GATEWAY_URL 未配置或失败时跳过)
|
||||
- 幂等去重:Redis(**env.ts 预留 REDIS_URL 但无 redis 客户端依赖**,pending-features P5 要求 event_id 去重)
|
||||
- 可观测:pino + prom-client + OpenTelemetry(三支柱已具备)
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- **P5 沟通与 AI 阶段**(pending-features §P5):
|
||||
- 会话/消息 CRUD + 调 Push Gateway 推送 + 通知偏好
|
||||
- 多渠道(站内/SMS/邮件/微信),沿用旧项目 dispatcher 模式
|
||||
- Elasticsearch 题库全文检索(从 MySQL 同步)
|
||||
- **退出标准**:教师发广播通知 → 全在线学生实时收到(Push Gateway)→ AI 辅助出题流式返回 → 题库全文检索 < 200ms
|
||||
- **依赖上游**:P1 黄金模板 classes、P3 core-edu(事件来源)、P5 push-gateway(推送通道,ai01 设计)、P5 ai(无直接依赖)
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||||
|
||||
- [x] 权限装饰器 `@RequirePermission`(3 个 MSG_* 权限点,全部 Controller 方法已覆盖)
|
||||
- [x] 错误码前缀统一(`MSG_*`)
|
||||
- [x] logger / metrics / tracer 三支柱(已具备)
|
||||
- [x] `/healthz` 健康检查(HealthModule 已注册)
|
||||
- [⚠️] `/readyz`(**仅检查 DB `SELECT 1`,未检查 ES 连通性**,ES 故障时仍返回 ok)
|
||||
- [x] 优雅关闭 SIGTERM(main.ts 已处理:app.close → closeEs → closeDb → shutdownTracer)
|
||||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件)
|
||||
- [x] Dockerfile 多阶段构建(已具备)
|
||||
- [⚠️] Zod 输入验证(Controller 用 `schema.parse(body)`,**但 ZodError 未在 GlobalErrorFilter 特殊处理,走默认 500 而非 400**)
|
||||
- [x] GlobalErrorFilter 统一兜底
|
||||
|
||||
### 7. 现有骨架差距与待决策(提请 coord 仲裁)
|
||||
|
||||
| # | 差距 | 影响 | 提请决策 |
|
||||
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| M1 | **Kafka 消费未实现**:装了 kafkajs 但无 consumer 代码,无 shared/kafka/ 目录 | 004 §7.3 列 msg 消费 5 类事件(ExamPublished/HomeworkGraded/GradeRecorded/UserRegistered/MasteryUpdated) | 阶段 2 设计需补 Kafka consumer + 幂等去重 |
|
||||
| M2 | **无 Outbox**:骨架无 shared/outbox/ | msg 作为通知中心,发送通知后可能需发事件(如 NotificationSent) | 确认 msg 是否需 Outbox(iam 无,core-edu 有) |
|
||||
| M3 | **ES 无 mapping 定义**:依赖动态 mapping,索引名 "notifications" 硬编码在 service | 检索质量不稳定,索引管理缺失 | 阶段 2 设计补 mapping + ensureIndex |
|
||||
| M4 | **无独立 repository**:notifications.service.ts 直接用 db,无 repository 抽象 | 与黄金模板分层不一致(iam 有 repository) | 阶段 2 设计补 repository 层 |
|
||||
| M5 | **Redis 未引入**:env.ts 预留 REDIS_URL 但无 redis 客户端依赖 | pending-features P5 要求 event_id 去重(Redis SETNX 或 DB 唯一索引) | 阶段 2 设计决策:Redis SETNX vs DB 唯一索引 |
|
||||
| M6 | **createBatch 无事务/无批量优化**:for 循环串行调 send,无批量 INSERT | 性能瓶颈(广播场景) | 阶段 2 设计改为批量 INSERT |
|
||||
| M7 | **NotificationsModule 缺 exports**:notifications.module.ts 无 `exports: [NotificationsService]` | 未来 BFF 注入受阻 | 阶段 2 设计补 exports |
|
||||
| M8 | **ZodError 未特殊处理**:GlobalErrorFilter 未识别 ZodError,走默认 500 | 输入校验错误返回码错误(500 而非 400) | 阶段 2 设计 GlobalErrorFilter 补 ZodError 分支(iam 已有可参考) |
|
||||
| M9 | **gRPC 未实现**:proto 定义了 NotificationService,但无 gRPC controller | pending-features P5 未强制 gRPC | 确认 P5 是否启用 gRPC(ai03 提请统一决策) |
|
||||
| M10 | **README 与实现脱节**:README API 表标 `POST /notifications/:id/read`,实际是 PUT;漏 batch/user/:userId/user/:userId/page 端点;声称"消费 Kafka 事件"但无代码 | 文档误导 | 阶段 2 设计同步修正 README |
|
||||
| M11 | **main.ts 与 LifecycleService 重复关闭资源**:两者都调 closeDb/closeEs | 重复关闭可能报错(虽有 try-catch) | 阶段 2 设计统一关闭逻辑到 LifecycleService |
|
||||
| M12 | **proto 包名**:实际 `next_edu_cloud.msg.v1`,project_rules §5 规定 `edu.msg.v1` | 命名规范不一致 | **coord 仲裁**(ai03 已提请) |
|
||||
|
||||
---
|
||||
|
||||
## 三、服务审计表 — ai05
|
||||
|
||||
> 审计标准对照 [project_rules §3](../../.trae/rules/project_rules.md) 与 [known-issues §2.2 classes 黄金模板](../troubleshooting/known-issues.md)
|
||||
|
||||
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
|
||||
| ------- | ---------------- | -------------- | ------- | -------------- | ------------------------------- | -------- | ------------------- | --------------------- | ---------- | ---------- |
|
||||
| content | ✅ 16 端点全覆盖 | ✅ `CONTENT_*` | ✅ pino | ✅ prom-client | ✅ OTel + auto-instrumentations | ✅ | ⚠️ 仅 DB 不查 Neo4j | ✅ closeNeo4j→closeDb | 0% | ✅ |
|
||||
| msg | ✅ 6 端点全覆盖 | ✅ `MSG_*` | ✅ pino | ✅ prom-client | ✅ OTel + auto-instrumentations | ✅ | ⚠️ 仅 DB 不查 ES | ✅ closeEs→closeDb | 0% | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 四、跨模块契约对齐提请(coord 交叉审查)
|
||||
|
||||
### 4.1 接口一致性检查
|
||||
|
||||
| 本服务声明 | 对方服务声明 | 是否匹配 | 备注 |
|
||||
| --------------------------------------------------------- | ------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------ |
|
||||
| ai05 content: 暴露 KnowledgeGraphService.GetPrerequisites | ai06 ai: 调 content 查知识点(004 §9.3) | ⚠️ 待确认 | ai06 设计文档需确认调用签名 |
|
||||
| ai05 content: 暴露 TextbookService.ListTextbooks | ai03 teacher-bff: 聚合 content 查教材(004 §4.1) | ⚠️ 待确认 | ai03 设计文档需确认调用 |
|
||||
| ai05 content: 发布 `edu.content.question.published` | ai06 ai: 消费题库事件(004 §7.2 消费者 AI) | ⚠️ 待确认 | ai06 设计需确认是否消费 |
|
||||
| ai05 msg: 消费 `edu.teaching.exam.published` | ai03 core-edu: 发布考试事件 | ⚠️ 待确认 | ai03 提请 topic 命名统一(004 `edu.teaching.exam.published` vs core-edu 代码 `edu.exam.events`) |
|
||||
| ai05 msg: 消费 `edu.identity.user.created` | ai02 iam: 发布用户创建事件 | ⚠️ 待确认 | ai02 设计需确认 topic |
|
||||
| ai05 msg: 调 push-gateway `/internal/push` | ai01 push-gateway: 暴露推送端点 | ✅ 已实现 | msg 当前用 fetch POST,push-gateway 已有 /internal/push 端点 |
|
||||
|
||||
### 4.2 全局冲突检查
|
||||
|
||||
| 检查项 | 检查结果 | 备注 |
|
||||
| -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 端口不冲突 | ✅ content 3005、msg 3007 | 与 iam(3002)/classes(3001)/teacher-bff(3003)/core-edu(3004)/data-ana(3006)/ai(3008) 不冲突 |
|
||||
| Topic 不重复 | ⚠️ 待汇总 | content 发布 `edu.content.question.published`;msg 仅消费不发布 |
|
||||
| 错误码前缀不重叠 | ✅ `CONTENT_*` / `MSG_*` 唯一 | 与 iam `IAM_*` / core-edu `CORE_EDU_*` 不重叠 |
|
||||
| Proto message 不遗漏 | ⚠️ 待确认 | events.proto 无 NotificationEvent,若 msg 发事件需补充;content.proto 缺 Update/Delete/分页(对比 classes.proto 有 page_token) |
|
||||
| proto 包名规范 | ⚠️ 不一致 | 实际 `next_edu_cloud.<domain>.v1`,规则要求 `edu.<domain>.v1`,**提请 coord 仲裁**(ai03 已提请) |
|
||||
|
||||
### 4.3 待 coord 仲裁的决策项
|
||||
|
||||
1. **proto 包名统一**:`next_edu_cloud.*` vs `edu.*`(影响全部 proto,需 coord 决策)
|
||||
2. **gRPC 启用时机**:P4/P5 是否启用 gRPC controller,还是继续 REST(影响 content/msg/iam/core-edu 全部服务)
|
||||
3. **content ES 检索归属**:P4(pending-features 注明"P4 仅 CRUD,P5 引入 ES")vs 004 §2.2(列 ES 使用者 Content、AI)—— 需明确 content 何时引入 ES
|
||||
4. **content Outbox**:是否需要(004 §7.2 列 content 发事件,但 pending-features P4 未要求)
|
||||
5. **msg Outbox**:是否需要(msg 仅消费事件触发通知,是否需发 NotificationSent 事件)
|
||||
6. **msg Redis 引入**:env.ts 预留 REDIS_URL 但无依赖,幂等去重用 Redis SETNX 还是 DB 唯一索引
|
||||
7. **events.proto 补充**:若 msg 发事件需追加 NotificationEvent message
|
||||
|
||||
---
|
||||
|
||||
## 五、下一步
|
||||
|
||||
1. **等待 coord 审核本确认书**(§4 跨模块契约对齐 + §4.3 仲裁项)
|
||||
2. coord 放行后进入**阶段 2:模块架构设计文档**,按 ai-allocation.md §7 模板产出:
|
||||
- content 模块架构设计文档(含 Neo4j 图模型、ES 索引 mapping、题库 CRUD API、与 ai 的 gRPC 接口、教材/章节结构树)
|
||||
- msg 模块架构设计文档(含通知渠道抽象策略模式、ES 降级查询、Kafka 消费幂等、push-gateway 推送协议、通知模板、已读/未读状态管理)
|
||||
3. 阶段 2 设计完成后同步更新 README(修正 §C3/M10 文档脱节问题)
|
||||
333
docs/architecture/ai06-phase1-understanding.md
Normal file
333
docs/architecture/ai06-phase1-understanding.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# ai06 阶段 1 交付物:模块理解确认书
|
||||
|
||||
> AI 标识:ai06
|
||||
> 负责模块:data-ana(P4)、ai(P5)
|
||||
> 语言:Python 3.12+ / FastAPI 0.115+
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)
|
||||
> 日期:2026-07-09
|
||||
> 关联文档:[ai-allocation.md](./ai-allocation.md)、[004 架构影响地图](./004_architecture_impact_map.md)、[pending-features.md](./roadmap/pending-features.md)、[known-issues.md](../troubleshooting/known-issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1 必读清单完成确认
|
||||
|
||||
按 ai-allocation.md §4,ai06 必读 7 份全局文档 + 全部 `.proto` + `services/data-ana/`、`services/ai/` 骨架源码:
|
||||
|
||||
| # | 文档 | 状态 |
|
||||
| --- | ------------------------------------------------------------------- | ------------------------------ |
|
||||
| 1 | [README.md](../../README.md) | ✅ 已读 |
|
||||
| 2 | [MIGRATION_GUIDE.md](../../MIGRATION_GUIDE.md) | ✅ 已读 |
|
||||
| 3 | [004 架构影响地图](./004_architecture_impact_map.md) | ✅ 已读 |
|
||||
| 4 | [pending-features.md](./roadmap/pending-features.md) | ✅ 已读 |
|
||||
| 5 | [project_rules.md](../../.trae/rules/project_rules.md) | ✅ 已读(workspace 规则) |
|
||||
| 6 | [coding-standards.md](../standards/coding-standards.md) | ✅ 已读(重点 §4 Python 规范) |
|
||||
| 7 | [multi-ai-collaboration.md](../standards/multi-ai-collaboration.md) | ✅ 已读 |
|
||||
| 8 | `packages/shared-proto/proto/*.proto`(8 份) | ✅ 已读 |
|
||||
| 9 | `services/data-ana/src/`、`services/ai/src/` 全部源码 | ✅ 已读 |
|
||||
|
||||
> ai06 不需要读 classes 黄金模板(ai-allocation.md §4 矩阵 ai06 列对黄金模板行为 "—"),但审计表横切关注点对齐仍参考 classes 标准。
|
||||
|
||||
---
|
||||
|
||||
## 模块理解确认书 — data-ana
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5),属 **D6 智能洞察领域**(004 §1.1b)
|
||||
- **上游**:
|
||||
- api-gateway 直接 HTTP 代理 `/api/v1/analytics/*` → data-ana:3006(见 [api-gateway main.go:142-148](../../services/api-gateway/main.go))
|
||||
- teacher-bff / student-bff 聚合查询(004 §4.1:BFF → 业务服务 gRPC;当前为 REST)
|
||||
- **下游**:
|
||||
- ClickHouse(独占读模型,宽表 `student_dashboard_view` / `student_errors`)
|
||||
- Kafka(消费 Debezium CDC 事件 + 待发布 `edu.insight.mastery.updated` 领域事件)
|
||||
- **通信方式**:
|
||||
- 入口:当前 REST(`/analytics/class/{id}/performance`、`/analytics/student/{id}/weakness`、`/analytics/student/{id}/errorbook`);目标态(004 §4.1 + analytics.proto)转 **gRPC** 暴露 `AnalyticsService`
|
||||
- 出口:Kafka 消费(CDC + 领域事件订阅);Kafka 发布(`edu.insight.mastery.updated`,**未实现**)
|
||||
- **端口**:3006(见 [data-ana config.py:7](../../services/data-ana/src/data_ana/config.py) + [api-gateway config.go:55](../../services/api-gateway/internal/config/config.go))
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- **聚合职责**:学情分析读模型构建 + 查询服务,承载 **D6 智能洞察领域**的"分析/诊断"子域
|
||||
- **聚合根 / 实体**:
|
||||
- `StudentDashboard`(学生学情宽表视图,ClickHouse 物化)
|
||||
- `StudentErrorBook`(学生错题本,ClickHouse 物化)
|
||||
- `ClassPerformance`(班级成绩聚合,ClickHouse 即时聚合)
|
||||
- `MasterySnapshot`(知识点掌握度快照,**待实现**)
|
||||
- **业务领域**:D6 智能洞察(与 ai 服务共享领域,ai 偏"生成",data-ana 偏"分析")
|
||||
- **我不负责**:
|
||||
- 不负责写模型(成绩由 core-edu 写 MySQL,data-ana 只消费 CDC)
|
||||
- 不负责题库内容(→ content 服务)
|
||||
- 不负责通知投递(→ msg 服务消费 data-ana 发布的 `mastery.updated` 事件触发预警)
|
||||
- 不负责 AI 推理(→ ai 服务,ai 通过 gRPC 反向查询 data-ana 学情数据)
|
||||
- **数据自治**:独占 `edu_analytics` ClickHouse 数据库(不与 MySQL 写模型混用)
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
#### 消费的 proto message
|
||||
|
||||
| 来源 | message | 用途 |
|
||||
| --------------- | ------------------------------- | ---------------------------------------------- |
|
||||
| events.proto | `GradeEvent` | 消费 core-edu 成绩写入事件 → 更新学情宽表 |
|
||||
| events.proto | `ExamEvent` | 消费考试事件 → 缓存 exam_id→class_id 映射 |
|
||||
| events.proto | `HomeworkEvent` | 消费作业提交/批改事件 → 更新学情(**待实现**) |
|
||||
| events.proto | `ClassEvent` | 消费班级变更事件 → 同步班级维度(**待实现**) |
|
||||
| analytics.proto | `GetClassPerformanceRequest` 等 | gRPC 暴露契约(**待实现 gRPC server**) |
|
||||
|
||||
> **CDC 直连 vs 领域事件双通道**:当前实现走 Debezium CDC(直接监听 MySQL binlog),不依赖 core-edu 的 Outbox。这是 ADR-008 的设计决策(CDC 解耦 Outbox Relay,减少业务侵入)。Outbox 领域事件(events.proto)作为业务语义更清晰的补充通道,待 P4 后期评估是否双消费。
|
||||
|
||||
#### 暴露的 API / 事件
|
||||
|
||||
**HTTP 端点**(当前实现,见 [main.py](../../services/data-ana/src/data_ana/main.py)):
|
||||
|
||||
| method | path | 说明 |
|
||||
| ------ | ------------------------------------------- | -------------------------------------- |
|
||||
| GET | `/healthz` | liveness |
|
||||
| GET | `/readyz` | readiness(含 ClickHouse + CDC 状态) |
|
||||
| GET | `/metrics` | Prometheus 指标 |
|
||||
| GET | `/analytics/class/{class_id}/performance` | 班级成绩分析(平均分/及格率/参考人数) |
|
||||
| GET | `/analytics/student/{student_id}/weakness` | 学生薄弱知识点(mastery < 0.6) |
|
||||
| GET | `/analytics/student/{student_id}/errorbook` | 学生错题本 |
|
||||
|
||||
**gRPC 契约**(analytics.proto,**待实现**):
|
||||
|
||||
- `GetClassPerformance(GetClassPerformanceRequest) → ClassPerformance`
|
||||
- `GetStudentWeakness(GetStudentWeaknessRequest) → StudentWeakness`
|
||||
- `GetLearningTrend(GetLearningTrendRequest) → LearningTrend`(**当前 HTTP 未暴露**)
|
||||
|
||||
**发布的领域事件**:
|
||||
|
||||
| 事件 | Topic(004 §7.2) | 触发时机 | 消费者(004 §7.3) |
|
||||
| ---------------- | ----------------------------- | -------------- | --------------------------------------- |
|
||||
| `MasteryUpdated` | `edu.insight.mastery.updated` | 掌握度计算完成 | core-edu(推荐个性化练习)、msg(预警) |
|
||||
|
||||
> **当前未实现发布**:data-ana 当前只消费不发布。掌握度计算完成后应通过 Kafka 发布 `MasteryUpdated`,下游 core-edu / msg 消费。阶段 2 设计需补全此发布链路(Python 无 Outbox 模式,需评估直接 producer 还是引入 Outbox 表)。
|
||||
|
||||
- **错误码前缀**:`DATA_ANA_*`(待定义清单,见阶段 2 §6)
|
||||
- **缓存**:当前无 Redis 缓存;004 §6.3 学情宽表走 ClickHouse 实时,CDC 同步延迟 < 5s
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言:Python 3.12+
|
||||
- 框架:FastAPI 0.115+ / uvicorn
|
||||
- ORM / 客户端:`clickhouse-connect`(HTTP 协议,非原生协议)
|
||||
- 消息:`aiokafka`(CDC 消费者,AIOKafkaConsumer)
|
||||
- 配置:`pydantic-settings` BaseSettings(env_prefix="",全大写环境变量)
|
||||
- 可观测:
|
||||
- 日志:`structlog` 24.x(`make_filtering_bound_logger`,**注意**:旧版 `make_filtering_logger` 已废弃)
|
||||
- 指标:`prometheus-client` + `make_asgi_app()` 挂载 `/metrics`
|
||||
- 链路:`opentelemetry-sdk` + `OTLPSpanExporter` + `FastAPIInstrumentor.instrument_app(app)`
|
||||
- 序列化:JSON(Debezium 事件 `schemas.enable=false`,直接 `json.loads`)
|
||||
- 测试:pytest + pytest-asyncio(**当前 0% 覆盖率**)
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- **P4 内容分析阶段(M11-M13)**:建 DataAna 服务 + CDC 链路落地
|
||||
- **退出标准**(pending-features P4):学生查看学情诊断 ClickHouse 宽表 5s 内返回 + CDC 链路延迟 < 5s
|
||||
- **依赖上游**:
|
||||
- P1 地基:api-gateway 路由 + arch.db 扫描器 Python 支持
|
||||
- P3 核心教学:core-edu 写成绩到 MySQL(Debezium 监听 binlog)
|
||||
- P4 同期:content 服务(提供知识点 ID 供掌握度计算)
|
||||
- **下游依赖我**:
|
||||
- P5 ai 服务通过 gRPC 查询学情数据(004 §4.1:AI → DataAna gRPC)
|
||||
- P5 msg 服务消费 `mastery.updated` 触发预警
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务 + Python 规范)
|
||||
|
||||
> Python 服务无 NestJS 装饰器体系,权限校验等通过等价方式实现。
|
||||
|
||||
- [ ] 权限装饰器等价物:**当前 HTTP 端点全部裸露,无权限校验**。Gateway 层做 JWT 校验,但 data-ana 本身未校验 `x-user-id` / DataScope。**阶段 2 需设计 FastAPI Depends 权限依赖 + DataScope 过滤注入**
|
||||
- [ ] 错误码前缀统一:**当前无错误码体系**,降级时返回 `degraded: true` 标记但无业务错误码。**阶段 2 需定义 `DATA_ANA_*` 错误码清单**
|
||||
- [x] logger / metrics / tracer 三支柱(已具备,见 main.py + clickhouse_client.py)
|
||||
- [x] `/healthz` + `/readyz` 健康检查(已具备,readyz 含 ClickHouse ping + CDC 状态)
|
||||
- [ ] 优雅关闭 SIGTERM:当前 lifespan 仅关闭 CDC task + ClickHouse client,**未注册 SIGTERM 信号处理器**显式 drain
|
||||
- [ ] 测试覆盖率 ≥ 80%:**当前 0%**,无 tests/ 目录
|
||||
- [ ] Dockerfile 多阶段构建:**当前单阶段**(`FROM python:3.12-slim` → `uv sync` → `COPY src`),非多阶段
|
||||
- [ ] Pydantic 输入验证:**当前端点直接接收 path param,无 Pydantic 请求模型校验**(应补 `ClassPerformanceResponse` 等 response_model)
|
||||
- [x] 配置通过 pydantic-settings 管理(已具备)
|
||||
- [x] 异步优先(async def,aiokafka async consumer)
|
||||
- [ ] 类型注解强制:**部分函数缺返回值标注**(如 `init_logger` 返回 `BoundLogger` 但 `_logger` 全局变量标注 `None`,需统一)
|
||||
- [ ] ruff check 零错误:需阶段 2 验证
|
||||
|
||||
---
|
||||
|
||||
## 模块理解确认书 — ai
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5),属 **D6 智能洞察领域**(004 §1.1b,与 data-ana 共享领域)
|
||||
- **上游**:
|
||||
- api-gateway 直接 HTTP 代理 `/api/v1/ai/*` → ai:3008(见 [api-gateway main.go:138-139](../../services/api-gateway/main.go))
|
||||
- teacher-bff 聚合 AI 出题/优化能力(004 §4.1:BFF → ai,当前 REST)
|
||||
- **下游**:
|
||||
- content 服务(gRPC 查询知识点 / 题库,004 §4.1:AI → Content gRPC)
|
||||
- data-ana 服务(gRPC 查询学情数据,004 §4.1:AI → DataAna gRPC)
|
||||
- LLM Provider(外部 HTTP,OpenAI 兼容 REST API)
|
||||
- **通信方式**:
|
||||
- 入口:当前 REST(`/ai/chat`、`/ai/chat/stream`、`/ai/generate/question`、`/ai/optimize/expression`);目标态(ai.proto)转 **gRPC** 暴露 `AiService`(含 `StreamChat` 流式 RPC)
|
||||
- 出口:gRPC 调 content / data-ana(**当前未实现,仅 LLM HTTP 调用**);SSE 流式对前端
|
||||
- **端口**:3008(见 [ai config.py:8](../../services/ai/src/ai/config.py) + [api-gateway config.go:57](../../services/api-gateway/internal/config/config.go))
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- **聚合职责**:LLM 调用网关 + 教学场景 AI 编排(备课 / 出题 / 表达优化),承载 **D6 智能洞察领域**的"生成"子域
|
||||
- **聚合根 / 实体**:
|
||||
- `ChatConversation`(聊天会话,**待实现**,当前无状态)
|
||||
- `GeneratedQuestion`(生成的题目,待审核入库)
|
||||
- `PromptTemplate`(Prompt 模板,**待实现**)
|
||||
- `UsageRecord`(用量计费记录,**待实现**)
|
||||
- **业务领域**:D6 智能洞察(与 data-ana 共享,data-ana 偏"分析",ai 偏"生成")
|
||||
- **我不负责**:
|
||||
- 不负责题库存储(→ content 服务,ai 生成后调 content.CreateQuestions 入库)
|
||||
- 不负责学情计算(→ data-ana 服务,ai 查询学情用于个性化出题)
|
||||
- 不负责通知投递(→ msg 服务)
|
||||
- 不持有业务状态(无 DB 写入,无 Outbox;用量计费记录可走 Kafka 事件给 data-ana 落 ClickHouse)
|
||||
- **数据自治**:**无独占数据库**(设计上无状态;用量计费通过 Kafka 事件外发)
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
#### 消费的 proto message
|
||||
|
||||
| 来源 | message / service | 用途 |
|
||||
| --------------- | ---------------------------------------- | --------------------------------------- |
|
||||
| content.proto | `KnowledgeGraphService.GetPrerequisites` | 查询知识点前置依赖用于出题上下文 |
|
||||
| content.proto | `KnowledgeGraphService.GetLearningPath` | 查询学生学习路径用于个性化出题 |
|
||||
| analytics.proto | `AnalyticsService.GetStudentWeakness` | 查询学生薄弱知识点用于靶向出题 |
|
||||
| analytics.proto | `AnalyticsService.GetLearningTrend` | 查询学习趋势用于难度调节 |
|
||||
| ai.proto | `ChatRequest` 等 | gRPC 暴露契约(**待实现 gRPC server**) |
|
||||
|
||||
#### 暴露的 API / 事件
|
||||
|
||||
**HTTP 端点**(当前实现,见 [main.py](../../services/ai/src/ai/main.py)):
|
||||
|
||||
| method | path | 说明 |
|
||||
| ------ | ------------------------- | ---------------------------- |
|
||||
| GET | `/healthz` | liveness |
|
||||
| GET | `/readyz` | readiness(含 LLM 是否配置) |
|
||||
| GET | `/metrics` | Prometheus 指标 |
|
||||
| POST | `/ai/chat` | LLM 聊天(非流式) |
|
||||
| POST | `/ai/chat/stream` | LLM 流式聊天(SSE) |
|
||||
| POST | `/ai/generate/question` | 生成题目 |
|
||||
| POST | `/ai/optimize/expression` | 优化表达 |
|
||||
|
||||
**gRPC 契约**(ai.proto,**待实现**):
|
||||
|
||||
- `Chat(ChatRequest) → ChatResponse`
|
||||
- `StreamChat(ChatRequest) → stream ChatChunk`(流式 RPC)
|
||||
- `GenerateQuestion(GenerateQuestionRequest) → GeneratedQuestion`
|
||||
- `OptimizeExpression(OptimizeExpressionRequest) → OptimizedExpression`
|
||||
|
||||
**发布的领域事件**:**当前无发布**。设计上可发布 `AIUsageRecorded` 事件(用量计费),由 data-ana 消费落 ClickHouse。004 §7.2 未列出此 topic,**阶段 2 需与 coord 确认是否新增 `edu.insight.ai.usage` topic**。
|
||||
|
||||
- **错误码前缀**:`AI_*`(待定义清单,见阶段 2 §6)
|
||||
- **降级策略**:LLM API key 为空或调用失败时返回 `degraded: true` 骨架响应(见 [llm_client.py](../../services/ai/src/ai/llm_client.py))
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
- 语言:Python 3.12+
|
||||
- 框架:FastAPI 0.115+ / uvicorn
|
||||
- LLM 客户端:`httpx` 异步直接调 OpenAI 兼容 REST API(**不依赖 openai SDK**,见 [llm_client.py:1-7](../../services/ai/src/ai/llm_client.py))
|
||||
- 配置:`pydantic-settings` BaseSettings(env_prefix="")
|
||||
- 可观测:
|
||||
- 日志:`structlog`
|
||||
- 指标:`prometheus-client` + `make_asgi_app()`
|
||||
- 链路:`opentelemetry-sdk` + `OTLPSpanExporter` + `FastAPIInstrumentor`(dev_mode=true 时跳过 exporter 初始化避免本地无 collector 报错)
|
||||
- 流式响应:FastAPI `StreamingResponse` + `AsyncGenerator`,SSE 格式 `data: <chunk>\n\n`
|
||||
- 测试:pytest(**当前 0% 覆盖率**)
|
||||
|
||||
### 5. 我的阶段归属
|
||||
|
||||
- **P5 沟通与 AI 阶段(M14-M16)**:建 AI 网关 + LLM Provider 适配 + 流式 SSE
|
||||
- **退出标准**(pending-features P5):AI 辅助出题流式返回 + 题库全文检索 < 200ms(ES 部分由 ai05 负责)
|
||||
- **依赖上游**:
|
||||
- P1 地基:api-gateway 路由
|
||||
- P4 内容分析:content 服务(gRPC 查询知识点 / 题库)+ data-ana 服务(gRPC 查询学情)
|
||||
- 外部:LLM Provider API key(OpenAI / Anthropic / 百川 / 本地模型)
|
||||
- **下游依赖我**:
|
||||
- P5 teacher-bff 聚合 AI 出题 mutation(004 §9.3:教师用 AI 出题并发布到班级)
|
||||
- P5 teacher-portal SSE 流式 AI 对话(前端)
|
||||
|
||||
### 6. 我需要对齐的黄金模板项(对照 classes 服务 + Python 规范)
|
||||
|
||||
- [ ] 权限装饰器等价物:**当前 HTTP 端点全部裸露,无权限校验**。阶段 2 需设计 FastAPI Depends 权限依赖(AI 出题需 `AI_QUESTION_GENERATE` 权限,表达优化需 `AI_EXPRESSION_OPTIMIZE`)
|
||||
- [ ] 错误码前缀统一:**当前无错误码体系**,降级时返回 `degraded: true` 但无业务错误码。**阶段 2 需定义 `AI_*` 错误码清单**
|
||||
- [x] logger / metrics / tracer 三支柱(已具备)
|
||||
- [x] `/healthz` + `/readyz` 健康检查(已具备,readyz 含 LLM 配置状态)
|
||||
- [ ] 优雅关闭 SIGTERM:当前 lifespan 无显式 drain(LLM 流式请求需等待完成)
|
||||
- [ ] 测试覆盖率 ≥ 80%:**当前 0%**,无 tests/ 目录
|
||||
- [ ] Dockerfile 多阶段构建:**当前单阶段**(`FROM python:3.12-slim`)
|
||||
- [ ] Pydantic 输入验证:**当前仅 `ChatRequest` 是 BaseModel,`generate/question` 和 `optimize/expression` 直接接收 `prompt: str` / `text: str` query param,无请求体模型**。阶段 2 需补完整 Pydantic 请求/响应模型
|
||||
- [x] 配置通过 pydantic-settings 管理(已具备)
|
||||
- [x] 异步优先(httpx async + AsyncGenerator stream)
|
||||
- [x] 类型注解强制(已基本符合,main.py 函数返回值已标注)
|
||||
- [ ] LLM Provider 适配器模式:**当前仅 OpenAI 兼容 REST**,未抽象 Provider 接口。阶段 2 需设计 `LLMProvider` 抽象 + OpenAI/Anthropic/百川/本地 多适配器
|
||||
- [ ] Prompt 模板管理:**当前 Prompt 硬编码在 main.py**,阶段 2 需设计模板管理(DB 或文件)
|
||||
- [ ] 用量计费 / 频率限制:**当前无用量记录和限流**,阶段 2 需设计(Kafka 事件 + Redis 限流)
|
||||
- [ ] 备课工作流:**当前未实现**,pending-features P5 要求"分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库"完整工作流
|
||||
- [ ] ruff check 零错误:需阶段 2 验证
|
||||
|
||||
---
|
||||
|
||||
## ai06 服务审计表
|
||||
|
||||
> 对照 ai-allocation.md §10 审计模板。data-ana 与 ai 均为 Python/FastAPI,黄金模板对齐按 Python 规范(coding-standards §4)评估。
|
||||
> 符号说明:✅ 已实现 | ❌ 缺失 | ⚠️ 部分实现
|
||||
|
||||
| 服务 | 权限校验 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
|
||||
| -------- | -------- | ---------- | ------------ | ------------- | ------------------- | -------- | -------------------- | ------------------- | ---------- | ---------- |
|
||||
| data-ana | ❌ | ❌ | ✅ structlog | ✅ prometheus | ✅ OTel | ✅ | ✅(含 CH+CDC 状态) | ⚠️ 仅 lifespan 关闭 | 0% | ❌ 单阶段 |
|
||||
| ai | ❌ | ❌ | ✅ structlog | ✅ prometheus | ✅ OTel(dev 跳过) | ✅ | ✅(含 LLM 状态) | ⚠️ 仅 lifespan 关闭 | 0% | ❌ 单阶段 |
|
||||
|
||||
### 审计发现的关键差距清单
|
||||
|
||||
#### data-ana 关键差距(按优先级)
|
||||
|
||||
1. **P0 权限校验缺失**:所有 `/analytics/*` 端点裸露,无 DataScope 过滤。学生 A 可查询学生 B 的错题本(越权风险)。阶段 2 必须设计 `Depends(require_permission)` + `Depends(inject_data_scope)` 依赖注入
|
||||
2. **P0 gRPC server 未实现**:analytics.proto 定义了 `AnalyticsService` 但 data-ana 当前仅 HTTP。004 §4.1 明确 BFF → 业务服务走 gRPC,阶段 2 需引入 `grpc.aio` + `betterproto` 实现 gRPC server
|
||||
3. **P1 事件发布缺失**:`edu.insight.mastery.updated` 事件未发布,下游 core-edu/msg 无法消费。需设计掌握度计算 + Kafka producer 发布链路
|
||||
4. **P1 ClickHouse schema 不规范**:当前 `student_dashboard_view` 实为 MergeTree 表(注释提到应为 ReplacingMergeTree(last_updated) 实现幂等去重),无 DDL 文件管理。阶段 2 需产出完整 ClickHouse DDL(5 张宽表:考试/作业/成绩/掌握度/出勤)
|
||||
5. **P2 测试覆盖率 0%**:无 tests/ 目录,pytest 未配置
|
||||
6. **P2 Dockerfile 单阶段**:未做多阶段构建(builder + runtime),镜像体积大
|
||||
7. **P2 掌握度计算算法缺失**:当前 `_handle_grades_event` 用 `score / 100.0` 简化,未实现 pending-features 要求的"加权滑动平均"算法
|
||||
8. **P2 无 Pydantic 响应模型**:端点返回 `dict` 而非 `BaseModel`,无 `response_model` 校验
|
||||
|
||||
#### ai 关键差距(按优先级)
|
||||
|
||||
1. **P0 权限校验缺失**:所有 `/ai/*` 端点裸露,AI 出题等敏感操作无权限校验
|
||||
2. **P0 gRPC server 未实现**:ai.proto 定义了 `AiService`(含 `StreamChat` 流式 RPC)但 ai 当前仅 HTTP。阶段 2 需引入 `grpc.aio` 实现 gRPC server + 流式 RPC
|
||||
3. **P0 gRPC client 未实现**:004 §4.1 明确 AI → Content / AI → DataAna 走 gRPC,当前未实现。阶段 2 需设计 gRPC client 调用 content / data-ana
|
||||
4. **P1 LLM Provider 适配器缺失**:当前 `llm_client.py` 仅 OpenAI 兼容 REST,未抽象 Provider 接口。pending-features P5 要求"LLM Provider 适配(OpenAI/Anthropic/百川/本地模型)"
|
||||
5. **P1 Prompt 模板管理缺失**:system prompt 硬编码在 main.py,无模板管理。阶段 2 需设计模板存储(文件 or DB)+ 模板渲染
|
||||
6. **P1 备课工作流缺失**:pending-features P5 要求"分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库"完整工作流,当前仅"生成题目"单步
|
||||
7. **P1 用量计费 / 频率限制缺失**:无用量记录(token 消耗)、无频率限制(用户可无限调用 LLM)。阶段 2 需设计 Redis 限流 + Kafka 事件外发用量
|
||||
8. **P2 测试覆盖率 0%**:无 tests/ 目录
|
||||
9. **P2 Dockerfile 单阶段**:未做多阶段构建
|
||||
10. **P2 Pydantic 输入验证不完整**:`generate/question` 和 `optimize/expression` 直接接收 query param,无请求体模型
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1 待 coord 交叉审查的跨模块契约对齐项
|
||||
|
||||
以下项需 coord 在交叉审查时仲裁(见 ai-allocation.md §8):
|
||||
|
||||
| # | 议题 | 涉及方 | 当前状态 | ai06 建议 |
|
||||
| --- | -------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | **data-ana 是否发布 `edu.insight.mastery.updated` 事件** | data-ana → core-edu / msg | 004 §7.2 列出此 topic,但当前 data-ana 未实现发布 | 阶段 2 设计发布链路;Python 无 Outbox,建议直接 Kafka producer(掌握度计算是派生数据,非事务写) |
|
||||
| 2 | **ai 是否发布 `edu.insight.ai.usage` 用量事件** | ai → data-ana | 004 §7.2 未列出此 topic | 建议 coord 新增此 topic 用于用量计费落 ClickHouse;若 coord 不同意则 ai 自建用量记录表(破坏无状态原则) |
|
||||
| 3 | **data-ana / ai 是否需要实现 gRPC server** | data-ana / ai ← teacher-bff / student-bff / ai | 004 §4.1 明确 BFF → 业务服务走 gRPC,analytics.proto / ai.proto 已定义 service,但当前两服务仅 HTTP | 阶段 2 引入 `grpc.aio` + `betterproto` 实现 gRPC server;HTTP 端点保留作为 Gateway 直连降级通道 |
|
||||
| 4 | **CDC 直连 vs Outbox 领域事件双通道** | data-ana ← core-edu | 当前 data-ana 走 Debezium CDC(监听 binlog),不消费 core-edu Outbox 事件(events.proto) | 维持 CDC 为主通道(ADR-008 决策);events.proto 作为业务语义补充,待 P4 后期评估是否双消费 |
|
||||
| 5 | **data-ana DataScope 过滤实现位置** | data-ana + iam | 004 §5.3 DataScope 6 级,业务服务在 Repository 层注入 WHERE;data-ana 是 ClickHouse 查询无 Repository 层 | 阶段 2 在 ClickHouse 查询 SQL 拼接时注入 DataScope WHERE(学生只能看自己,教师看本班,校管理员看本校);需 iam 提供 `getEffectiveDataScope(userId)` API |
|
||||
| 6 | **ai 备课工作流是否引入 Temporal** | ai + coord | 004 §2.3 列出 Temporal 用于"工作流编排(考试生命周期、AI 编排)",P3 已引入 Temporal 试点 | 阶段 2 评估:简单工作流(4 步)可用 FastAPI BackgroundTasks;复杂工作流(含教师审核等待)建议 Temporal;待 coord 仲裁 |
|
||||
| 7 | **LLM Provider 切换的配置化** | ai + coord | 当前 ai config.py 仅 OpenAI + Anthropic 字段 | 阶段 2 设计 `LLMProvider` 抽象 + 配置化路由(按 model 名路由到不同 provider);本地模型走 Ollama REST API |
|
||||
| 8 | **data-ana ClickHouse DDL 管理位置** | data-ana + coord | 当前无 DDL 文件,宽表手动创建 | 建议 coord 在 `infra/clickhouse/` 下建立 DDL 目录(类似 MySQL init.sql),data-ana 提供 DDL 内容 |
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1 总结
|
||||
|
||||
ai06 已完成阶段 1 全局理解,产出本确认书。核心结论:
|
||||
|
||||
1. **data-ana**(P4):CDC 链路已跑通(Debezium → Kafka → ClickHouse),3 个分析端点已实现降级模式。**关键差距**:权限校验、gRPC server、事件发布(mastery.updated)、ClickHouse DDL 规范化、掌握度算法、测试覆盖。
|
||||
2. **ai**(P5):LLM 客户端已实现降级模式(httpx 异步 + SSE 流式),4 个端点已实现。**关键差距**:权限校验、gRPC server + client、LLM Provider 适配器、Prompt 模板管理、备课工作流、用量计费、测试覆盖。
|
||||
3. **跨模块契约**:8 项待 coord 仲裁,最关键的是 gRPC server 实现决策(影响阶段 2 设计核心)和事件发布 topic 新增决策。
|
||||
|
||||
下一步进入阶段 2,按 ai-allocation.md §7 模板产出 data-ana 与 ai 的模块架构设计文档。
|
||||
858
docs/architecture/ai06-phase2-design.md
Normal file
858
docs/architecture/ai06-phase2-design.md
Normal file
@@ -0,0 +1,858 @@
|
||||
# ai06 阶段 2 交付物:模块架构设计文档
|
||||
|
||||
> AI 标识:ai06
|
||||
> 负责模块:data-ana(P4)、ai(P5)
|
||||
> 阶段:架构设计外包 · 阶段 2(模块架构设计)
|
||||
> 日期:2026-07-09
|
||||
> 关联文档:[ai06 阶段 1 确认书](./ai06-phase1-understanding.md)、[ai-allocation.md](./ai-allocation.md)、[004 架构影响地图](./004_architecture_impact_map.md)、[pending-features.md](./roadmap/pending-features.md)
|
||||
> 审查请求:本设计文档待 coord 按 ai-allocation.md §8 交叉审查(接口一致性 / 端口冲突 / Topic 重复 / 错误码重叠 / 黄金模板对齐)
|
||||
|
||||
---
|
||||
|
||||
## 设计原则与全局约束
|
||||
|
||||
本设计遵循以下强制约束(来自 project_rules.md + coding-standards.md + 004):
|
||||
|
||||
1. **契约先行**:proto 已定义(analytics.proto / ai.proto),实现前不修改 proto,如需修改走 coord 流程
|
||||
2. **CQRS 读写分离**:data-ana 是纯读模型服务(无 MySQL 写),ClickHouse 宽表由 CDC 投影构建
|
||||
3. **事件驱动**:data-ana 消费 CDC + 领域事件;ai 不参与事件流(无状态)
|
||||
4. **gRPC 优先**:004 §4.1 明确 BFF → 业务服务走 gRPC,两服务需实现 gRPC server(HTTP 保留作 Gateway 直连降级)
|
||||
5. **DataScope 过滤**:004 §5.3 DataScope 6 级在查询层注入 WHERE
|
||||
6. **三支柱可观测**:structlog + prometheus-client + OpenTelemetry(已具备,需补业务指标)
|
||||
7. **降级模式**:外部依赖(ClickHouse / LLM / 下游 gRPC)不可用时返回骨架数据 + `degraded: true`
|
||||
8. **Python 规范**:pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误
|
||||
|
||||
---
|
||||
|
||||
# 模块架构设计文档 — data-ana
|
||||
|
||||
## 1. 模块内部分层图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Entry["入口层"]
|
||||
HTTP[FastAPI HTTP Router<br/>/analytics/* + /healthz + /readyz]
|
||||
GRPC[grpc.aio Server<br/>AnalyticsService]
|
||||
end
|
||||
|
||||
subgraph Middleware["中间件层"]
|
||||
AUTH[AuthDepends<br/>校验 x-user-id / x-user-roles]
|
||||
SCOPE[DataScopeDepends<br/>注入 data_scope 元数据]
|
||||
TRACE[OTel FastAPIInstrumentor<br/>+ grpc.aio server interceptor]
|
||||
end
|
||||
|
||||
subgraph Service["应用服务层 Application Service"]
|
||||
S1[AnalyticsService<br/>班级/学生/趋势查询编排]
|
||||
S2[MasteryService<br/>掌握度计算 + 事件发布]
|
||||
S3[ErrorBookService<br/>错题本查询]
|
||||
end
|
||||
|
||||
subgraph Repo["数据访问层 Repository"]
|
||||
R1[ClickHouseRepository<br/>宽表查询 + DataScope WHERE 注入]
|
||||
R2[KafkaProducer<br/>mastery.updated 事件发布]
|
||||
R3[IamClient<br/>gRPC 调 iam.getEffectiveDataScope]
|
||||
end
|
||||
|
||||
subgraph Consumer["CDC 消费者(后台任务)"]
|
||||
C1[CdcConsumer<br/>aiokafka AIOKafkaConsumer]
|
||||
C2[ExamCache<br/>exam_id→class_id 内存映射]
|
||||
C3[EventHandler<br/>grades/exams/homework/classes 路由]
|
||||
end
|
||||
|
||||
subgraph Storage["存储 / 总线"]
|
||||
CH[(ClickHouse<br/>edu_analytics 库)]
|
||||
KAFKA[(Kafka<br/>edu-cdc.* + edu.insight.mastery.updated)]
|
||||
IAM[iam:3002 gRPC]
|
||||
end
|
||||
|
||||
HTTP --> AUTH --> SCOPE --> S1
|
||||
HTTP --> S3
|
||||
GRPC --> S1
|
||||
S1 --> R1
|
||||
S3 --> R1
|
||||
S2 --> R1
|
||||
S2 --> R2
|
||||
SCOPE --> R3
|
||||
R1 --> CH
|
||||
R2 --> KAFKA
|
||||
R3 --> IAM
|
||||
|
||||
C1 --> C3
|
||||
C3 --> C2
|
||||
C3 --> R1
|
||||
C1 --> KAFKA
|
||||
```
|
||||
|
||||
**分层规则**:
|
||||
|
||||
- **入口层**:HTTP(保留作 Gateway 直连降级)+ gRPC(主入口,BFF 调用)。两入口共享同一 Application Service
|
||||
- **中间件层**:FastAPI Depends 链(`AuthDepends` → `DataScopeDepends`);gRPC 用 server interceptor 注入身份元数据
|
||||
- **应用服务层**:编排查询 / 计算掌握度 / 发布事件,不直接访问存储
|
||||
- **数据访问层**:ClickHouse 查询封装 + Kafka producer + gRPC client(调 iam)
|
||||
- **CDC 消费者**:独立后台任务(lifespan 启动),与 HTTP/gRPC 入口解耦
|
||||
|
||||
## 2. 领域模型
|
||||
|
||||
data-ana 是**纯读模型服务**,不持有写聚合根。领域模型为**视图聚合**(ClickHouse 物化):
|
||||
|
||||
### 聚合根(视图型)
|
||||
|
||||
| 聚合根 | 含义 | 物化载体 | 不变式 |
|
||||
| ------------------ | ---------------- | ----------------------------------------- | ------------------------------------------------------------- |
|
||||
| `StudentDashboard` | 学生学情宽表 | ClickHouse `student_dashboard_view` | 同一 (student_id, exam_id, knowledge_point_id) 仅保留最新版本 |
|
||||
| `ClassPerformance` | 班级成绩聚合 | ClickHouse 即时聚合(不物化) | 聚合维度为 class_id + 时间窗 |
|
||||
| `StudentErrorBook` | 学生错题本 | ClickHouse `student_errors` | 同一 (student_id, question_id) 累计 error_count |
|
||||
| `MasterySnapshot` | 知识点掌握度快照 | ClickHouse `mastery_snapshot`(**新增**) | 同一 (student_id, knowledge_point_id) 保留历史版本 |
|
||||
|
||||
### 值对象
|
||||
|
||||
- `WeakPoint`:knowledge_point_id + title + mastery_level
|
||||
- `TrendPoint`:date + score
|
||||
- `DataScope`:level (SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL) + scope_ids(具体可见的 class_id / grade_id 列表)
|
||||
|
||||
### 聚合间通信
|
||||
|
||||
- 同服务内:直接函数调用(Application Service → Repository)
|
||||
- 跨服务:仅通过 Kafka 事件(发布 `mastery.updated`)+ gRPC(调 iam 查 DataScope)
|
||||
|
||||
## 3. 数据模型(ClickHouse DDL)
|
||||
|
||||
> DDL 文件由 coord 统一管理在 `infra/clickhouse/ddl/`(待 coord 建立),data-ana 提供内容。
|
||||
|
||||
### 3.1 宽表 `student_dashboard_view`
|
||||
|
||||
```sql
|
||||
-- 学生学情宽表:每次成绩写入产生一行,按 ORDER BY 去重保留最新版本
|
||||
CREATE TABLE IF NOT EXISTS student_dashboard_view
|
||||
(
|
||||
student_id String,
|
||||
class_id String,
|
||||
exam_id String,
|
||||
subject_id String,
|
||||
score Float64,
|
||||
rank_in_class UInt32,
|
||||
knowledge_point_id String,
|
||||
mastery_level Float32, -- 0.0-1.0
|
||||
error_count UInt32,
|
||||
last_updated DateTime64(3, 'UTC')
|
||||
)
|
||||
ENGINE = ReplacingMergeTree(last_updated)
|
||||
PARTITION BY toYYYYMM(last_updated)
|
||||
ORDER BY (student_id, exam_id, knowledge_point_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
```
|
||||
|
||||
**索引策略**:
|
||||
|
||||
- ORDER BY `(student_id, exam_id, knowledge_point_id)`:主键索引,支持按学生查学情、按考试查成绩、按知识点查掌握度
|
||||
- PARTITION BY `toYYYYMM(last_updated)`:按月分区,支持历史数据归档
|
||||
- ReplacingMergeTree(last_updated):同 ORDER BY 自动去重,保留 last_updated 最大版本(幂等消费保证)
|
||||
|
||||
### 3.2 错题本 `student_errors`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS student_errors
|
||||
(
|
||||
student_id String,
|
||||
question_id String,
|
||||
knowledge_point_id String,
|
||||
error_count UInt32,
|
||||
last_error_time DateTime64(3, 'UTC'),
|
||||
content String
|
||||
)
|
||||
ENGINE = ReplacingMergeTree(last_error_time)
|
||||
PARTITION BY toYYYYMM(last_error_time)
|
||||
ORDER BY (student_id, question_id);
|
||||
```
|
||||
|
||||
### 3.3 掌握度快照 `mastery_snapshot`(**新增**)
|
||||
|
||||
```sql
|
||||
-- 知识点掌握度历史快照:每次掌握度计算产生新版本,支持趋势查询
|
||||
CREATE TABLE IF NOT EXISTS mastery_snapshot
|
||||
(
|
||||
student_id String,
|
||||
knowledge_point_id String,
|
||||
subject_id String,
|
||||
mastery_level Float32,
|
||||
calculated_at DateTime64(3, 'UTC'),
|
||||
calculation_method LowCardinality(String) -- 'weighted_moving_avg' / 'simple_avg'
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(calculated_at)
|
||||
ORDER BY (student_id, knowledge_point_id, calculated_at);
|
||||
```
|
||||
|
||||
### 3.4 用量计费 `ai_usage_log`(**新增,供 ai 服务写入**)
|
||||
|
||||
```sql
|
||||
-- AI 用量记录:ai 服务通过 Kafka 事件投递,data-ana 消费落库
|
||||
CREATE TABLE IF NOT EXISTS ai_usage_log
|
||||
(
|
||||
request_id String,
|
||||
user_id String,
|
||||
provider LowCardinality(String), -- 'openai' / 'anthropic' / 'baichuan' / 'local'
|
||||
model LowCardinality(String),
|
||||
prompt_tokens UInt32,
|
||||
completion_tokens UInt32,
|
||||
total_tokens UInt32,
|
||||
latency_ms UInt32,
|
||||
success Boolean,
|
||||
occurred_at DateTime64(3, 'UTC')
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(occurred_at)
|
||||
ORDER BY (user_id, occurred_at);
|
||||
```
|
||||
|
||||
### 3.5 读写分离策略
|
||||
|
||||
| 操作 | 路径 | 说明 |
|
||||
| -------------- | ----------------------------------- | ------------------------------------------ |
|
||||
| 学情查询 | ClickHouse 宽表 | 实时聚合,亚秒级响应 |
|
||||
| 错题本查询 | ClickHouse student_errors | 实时查询 |
|
||||
| 掌握度趋势 | ClickHouse mastery_snapshot | 历史快照 |
|
||||
| 掌握度计算 | CDC 触发 → 内存计算 → 写 ClickHouse | 派生数据,非事务写 |
|
||||
| DataScope 解析 | gRPC 调 iam | 实时查询,结果 Redis 缓存 5min(004 §6.3) |
|
||||
|
||||
## 4. API 设计
|
||||
|
||||
### 4.1 HTTP 端点(保留作 Gateway 直连降级)
|
||||
|
||||
| method | path | 权限 | 请求 | 响应 | 说明 |
|
||||
| ------ | ------------------------------------------- | ----------------------------- | ------------------------------------------------ | ----------------------------------------------------- | -------------------------------- |
|
||||
| GET | `/healthz` | — | — | `{status, service}` | liveness |
|
||||
| GET | `/readyz` | — | — | `{status, ready, degraded, clickhouse, cdc_consumer}` | readiness |
|
||||
| GET | `/metrics` | — | — | Prometheus 格式 | 指标 |
|
||||
| GET | `/analytics/class/{class_id}/performance` | `ANALYTICS_CLASS_READ` | query: `subject_id?`, `start_date?`, `end_date?` | `ClassPerformanceResponse` | 班级成绩分析 |
|
||||
| GET | `/analytics/student/{student_id}/weakness` | `ANALYTICS_STUDENT_READ` | query: `subject_id?` | `StudentWeaknessResponse` | 学生薄弱知识点(DataScope 过滤) |
|
||||
| GET | `/analytics/student/{student_id}/errorbook` | `ANALYTICS_STUDENT_READ` | query: `page?`, `size?` | `StudentErrorBookResponse` | 学生错题本(DataScope 过滤) |
|
||||
| GET | `/analytics/student/{student_id}/trend` | `ANALYTICS_STUDENT_READ` | query: `start_date`, `end_date`, `subject_id?` | `LearningTrendResponse` | 学习趋势(**新增**) |
|
||||
| GET | `/analytics/dashboard/teacher/{user_id}` | `ANALYTICS_TEACHER_DASHBOARD` | query: `class_id?` | `TeacherDashboardResponse` | 教师仪表盘聚合(**新增**) |
|
||||
|
||||
### 4.2 gRPC 契约(analytics.proto,待实现 server)
|
||||
|
||||
| RPC | 请求 | 响应 | 权限 |
|
||||
| --------------------- | ------------------------------------------------------------------------ | ------------------ | ------------------------ |
|
||||
| `GetClassPerformance` | `GetClassPerformanceRequest{class_id, subject_id, start_date, end_date}` | `ClassPerformance` | `ANALYTICS_CLASS_READ` |
|
||||
| `GetStudentWeakness` | `GetStudentWeaknessRequest{student_id, subject_id}` | `StudentWeakness` | `ANALYTICS_STUDENT_READ` |
|
||||
| `GetLearningTrend` | `GetLearningTrendRequest{student_id, start_date, end_date}` | `LearningTrend` | `ANALYTICS_STUDENT_READ` |
|
||||
|
||||
**权限校验**:gRPC server interceptor 从 metadata 提取 `x-user-id` / `x-user-roles` / `x-data-scope`,调用 `AuthDepends` 等价逻辑。
|
||||
|
||||
### 4.3 Pydantic 请求/响应模型
|
||||
|
||||
```python
|
||||
# 示例:班级成绩分析响应
|
||||
class ClassPerformanceResponse(BaseModel):
|
||||
success: bool
|
||||
data: ClassPerformanceData
|
||||
degraded: bool = False
|
||||
|
||||
class ClassPerformanceData(BaseModel):
|
||||
class_id: str
|
||||
average_score: float
|
||||
pass_rate: float
|
||||
total_students: int
|
||||
scores: list[StudentScore] = [] # 详细成绩列表(受 DataScope 过滤)
|
||||
|
||||
class StudentScore(BaseModel):
|
||||
student_id: str
|
||||
score: float
|
||||
grade: str
|
||||
```
|
||||
|
||||
## 5. 事件设计
|
||||
|
||||
### 5.1 消费的事件
|
||||
|
||||
| Topic | 来源 | 消息格式 | 消费动作 |
|
||||
| ------------------------------------------------------ | ------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `edu-cdc.next_edu_cloud.core_edu_grades` | Debezium CDC(core-edu MySQL) | Debezium JSON(before/after/source/op/ts_ms) | 解析 → 查 ExamCache 填 class_id → 计算掌握度 → upsert student_dashboard_view |
|
||||
| `edu-cdc.next_edu_cloud.core_edu_exams` | Debezium CDC | Debezium JSON | upsert ExamCache(exam_id → class_id, subject_id) |
|
||||
| `edu-cdc.next_edu_cloud.core_edu_homework_submissions` | Debezium CDC(**新增订阅**) | Debezium JSON | 记录作业提交行为 → 更新 student_dashboard_view |
|
||||
| `edu-cdc.next_edu_cloud.classes` | Debezium CDC | Debezium JSON | 同步班级维度(head_teacher_id)用于教师 DataScope |
|
||||
| `edu-cdc.next_edu_cloud.iam_users` | Debezium CDC(**新增订阅**) | Debezium JSON | 同步用户 dataScope 用于查询过滤(避免每次查 iam) |
|
||||
| `edu.insight.ai.usage` | ai 服务 Kafka producer(**待 coord 新增 topic**) | JSON(UsageRecord) | 落 ai_usage_log 表 |
|
||||
|
||||
**幂等性**:
|
||||
|
||||
- CDC 事件:依赖 ClickHouse `ReplacingMergeTree(last_updated)` 引擎按 ORDER BY 去重
|
||||
- 领域事件(若消费):基于 `event_id` 去重(Redis SETNX,TTL 7 天)
|
||||
|
||||
### 5.2 发布的事件
|
||||
|
||||
| 事件 | Topic | 触发时机 | 消费者 | Payload |
|
||||
| ---------------- | ----------------------------- | ----------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `MasteryUpdated` | `edu.insight.mastery.updated` | 掌握度计算完成(CDC grades 事件触发后异步计算) | core-edu(推荐个性化练习)、msg(掌握度预警) | `{event_id, student_id, knowledge_point_id, mastery_level, calculated_at}` |
|
||||
|
||||
**发布实现**(Python 无 Outbox 模式):
|
||||
|
||||
- 掌握度计算是**派生数据**(非业务事务写),不需要 Outbox 保证事务一致
|
||||
- 直接用 `aiokafka.AIOKafkaProducer` 发布,`idempotent=true` + 事务性 producer
|
||||
- 失败重试 3 次,仍失败记录日志 + 落 `mastery_publish_failed` 本地表(待 P6 评估是否引入 Outbox)
|
||||
|
||||
## 6. 横切关注点对齐清单
|
||||
|
||||
### 6.1 权限装饰器等价物(FastAPI Depends)
|
||||
|
||||
```python
|
||||
# 权限点常量(与 iam 权限点对齐)
|
||||
class Permissions:
|
||||
ANALYTICS_CLASS_READ = "analytics:class:read"
|
||||
ANALYTICS_STUDENT_READ = "analytics:student:read"
|
||||
ANALYTICS_TEACHER_DASHBOARD = "analytics:teacher:dashboard"
|
||||
|
||||
async def require_permission(permission: str) -> UserContext:
|
||||
"""FastAPI Depends 权限校验.
|
||||
|
||||
从 x-user-id / x-user-roles 头提取身份,校验角色是否含 permission。
|
||||
"""
|
||||
...
|
||||
|
||||
async def inject_data_scope(ctx: UserContext = Depends(require_permission(...)))-> DataScope:
|
||||
"""注入 DataScope(从 iam.getEffectiveDataScope 查询,Redis 缓存 5min)."""
|
||||
...
|
||||
```
|
||||
|
||||
### 6.2 错误码清单(前缀 `DATA_ANA_*`)
|
||||
|
||||
| 错误码 | 触发条件 | HTTP | gRPC status |
|
||||
| --------------------------------- | --------------------------------------- | ------------------- | ------------------ |
|
||||
| `DATA_ANA_UNAUTHORIZED` | 缺失 x-user-id 头或 token 无效 | 401 | UNAUTHENTICATED |
|
||||
| `DATA_ANA_FORBIDDEN` | 角色无对应权限 | 403 | PERMISSION_DENIED |
|
||||
| `DATA_ANA_DATASCOPE_VIOLATION` | 查询目标超出 DataScope 范围 | 403 | PERMISSION_DENIED |
|
||||
| `DATA_ANA_CLICKHOUSE_UNAVAILABLE` | ClickHouse 不可达(降级模式仍返回骨架) | 200 + degraded:true | OK + degraded flag |
|
||||
| `DATA_ANA_INVALID_DATE_RANGE` | start_date > end_date | 400 | INVALID_ARGUMENT |
|
||||
| `DATA_ANA_STUDENT_NOT_FOUND` | student_id 不存在 | 404 | NOT_FOUND |
|
||||
| `DATA_ANA_CLASS_NOT_FOUND` | class_id 不存在 | 404 | NOT_FOUND |
|
||||
| `DATA_ANA_INTERNAL_ERROR` | 未捕获异常 | 500 | INTERNAL |
|
||||
|
||||
### 6.3 Logger 初始化
|
||||
|
||||
- 位置:`main.py` `init_logger()`(已具备)
|
||||
- 配置:`structlog.make_filtering_bound_logger(level)` + `TimeStamper(fmt="iso")` + `ConsoleRenderer`
|
||||
- **改进**:生产环境改用 `structlog.processors.JSONRenderer()`(当前 ConsoleRenderer 适合开发)
|
||||
|
||||
### 6.4 Metrics 指标清单
|
||||
|
||||
| 指标名 | 类型 | 标签 | 描述 |
|
||||
| --------------------------------------------- | --------- | -------------------- | -------------------------- |
|
||||
| `data_ana_http_requests_total` | Counter | method, path, status | HTTP 请求总数 |
|
||||
| `data_ana_http_request_duration_seconds` | Histogram | method, path | HTTP 请求延迟 |
|
||||
| `data_ana_clickhouse_query_duration_seconds` | Histogram | query_type | ClickHouse 查询延迟 |
|
||||
| `data_ana_clickhouse_query_total` | Counter | query_type, status | ClickHouse 查询总数 |
|
||||
| `data_ana_cdc_events_consumed_total` | Counter | table, op | CDC 事件消费总数 |
|
||||
| `data_ana_cdc_event_process_duration_seconds` | Histogram | table | CDC 事件处理延迟 |
|
||||
| `data_ana_cdc_consumer_lag` | Gauge | topic, partition | CDC 消费者 lag |
|
||||
| `data_ana_mastery_calculated_total` | Counter | — | 掌握度计算次数 |
|
||||
| `data_ana_mastery_published_total` | Counter | status | mastery.updated 事件发布数 |
|
||||
| `data_ana_datascope_cache_hits_total` | Counter | — | DataScope 缓存命中 |
|
||||
|
||||
### 6.5 Tracer 初始化
|
||||
|
||||
- 位置:`main.py` `init_tracer()`(已具备)
|
||||
- endpoint:`settings.otel_endpoint` + `/v1/traces`
|
||||
- **改进**:gRPC server 注册 `grpc.aio.ServerInterceptor` 透传 W3C trace context
|
||||
|
||||
### 6.6 /healthz 检查逻辑
|
||||
|
||||
- liveness:仅进程存活(已具备)
|
||||
|
||||
### 6.7 /readyz 检查逻辑
|
||||
|
||||
```python
|
||||
async def readyz() -> dict:
|
||||
return {
|
||||
"status": "ok" if all_ready else "degraded",
|
||||
"ready": all_ready,
|
||||
"degraded": not all_ready,
|
||||
"clickhouse": "ok" | "unreachable" | "not_configured",
|
||||
"cdc_consumer": "running" | "disabled" | "failed",
|
||||
"kafka_brokers": settings.kafka_brokers or None,
|
||||
"iam_grpc": "ok" | "unreachable", # 新增:iam gRPC 连通性
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 优雅关闭顺序
|
||||
|
||||
1. HTTP server stop accepting new requests(uvicorn graceful shutdown)
|
||||
2. gRPC server graceful stop(等待在途 RPC 完成,30s 超时)
|
||||
3. CDC consumer stop(等待在途消息处理完成,commit offset)
|
||||
4. Kafka producer flush + close(确保 mastery.updated 事件已投递)
|
||||
5. ClickHouse client close
|
||||
6. iam gRPC channel close
|
||||
|
||||
**信号处理**:注册 `signal.SIGTERM` handler,触发上述顺序。
|
||||
|
||||
## 7. 与其他模块的交互点(契约清单)
|
||||
|
||||
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
|
||||
| ------ | ------------------------- | ----- | ------------------------------------------------------------------- | ----------------------------- |
|
||||
| 被调用 | api-gateway | HTTP | `/analytics/*` | Gateway 代理 |
|
||||
| 被调用 | teacher-bff / student-bff | gRPC | `AnalyticsService.*` | BFF 聚合查询 |
|
||||
| 被调用 | ai | gRPC | `AnalyticsService.GetStudentWeakness / GetLearningTrend` | AI 个性化出题上下文 |
|
||||
| 调用 | iam | gRPC | `IamService.GetEffectiveDataScope`(**待 proto 新增**) | DataScope 解析 |
|
||||
| 消费 | core-edu(CDC) | Kafka | `edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions` | 学情数据投递 |
|
||||
| 消费 | core-edu(CDC) | Kafka | `edu-cdc.next_edu_cloud.classes` | 班级维度同步 |
|
||||
| 消费 | iam(CDC) | Kafka | `edu-cdc.next_edu_cloud.iam_users` | 用户 dataScope 同步 |
|
||||
| 消费 | ai | Kafka | `edu.insight.ai.usage`(**待 coord 新增**) | AI 用量落库 |
|
||||
| 发布 | — | Kafka | `edu.insight.mastery.updated` | 掌握度更新通知 core-edu / msg |
|
||||
|
||||
## 8. 风险与假设
|
||||
|
||||
### 8.1 假设
|
||||
|
||||
- **假设 1**:iam 提供 `GetEffectiveDataScope(userId) → DataScope` gRPC API。若 iam 未提供,fallback 为:从 `x-user-roles` 头推导(admin=ALL, teacher=CLASS_TAUGHT, student=SELF),但无法支持细粒度年级/学校范围
|
||||
- **假设 2**:core-edu 的 `core_edu_homework_submissions` 表存在 binlog。若不存在,作业相关学情无法通过 CDC 获取,需 core-edu 补表或走 Outbox 事件
|
||||
- **假设 3**:ClickHouse `ReplacingMergeTree` 在查询时需 `FINAL` 关键字确保去重生效。当前查询未加 `FINAL`,可能读到重复版本。**修复**:所有查询加 `FINAL` 或使用 `argMax` 聚合
|
||||
- **假设 4**:coord 同意新增 `edu.insight.ai.usage` topic。若不同意,ai 服务的用量计费需自建记录(破坏 ai 无状态原则)
|
||||
|
||||
### 8.2 技术风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
| --------------------------- | ------------------------------------ | ------------------------------------------------ |
|
||||
| ClickHouse 查询延迟超 5s | 违反 P4 退出标准 | 宽表索引优化 + 物化视图预聚合 + 查询超时 3s 降级 |
|
||||
| CDC 消费者 lag 过大 | 学情数据延迟 > 5s | 监控 lag + 告警 + 水平扩展消费者(分区数提升) |
|
||||
| ExamCache 内存泄漏 | 长期运行 OOM | LRU 淘汰策略(max 10000 条)+ 定期清理过期 exam |
|
||||
| mastery.updated 事件丢失 | 下游 core-edu/msg 收不到通知 | Kafka producer `acks=all` + 本地失败表重试 |
|
||||
| iam gRPC 不可达 | DataScope 无法解析 → 查询降级为 SELF | Redis 缓存 5min + fallback SELF 范围(最保守) |
|
||||
| ClickHouse `FINAL` 查询性能 | 查询变慢 | 使用 `argMax` 替代 `FINAL`,或在写入时去重 |
|
||||
|
||||
### 8.3 未决设计决策(需 coord 仲裁)
|
||||
|
||||
1. **mastery.updated 发布是否需要 Outbox**:Python 无 Outbox 模式,建议直接 producer;但 004 §12.2 明确"事件发布:Outbox 模式 / 禁止直接 Kafka producer"。**冲突**:data-ana 是 Python 服务无 MySQL 写事务,Outbox 不适用。建议 coord 裁定:派生数据事件(非业务事务)允许直接 producer
|
||||
2. **iam GetEffectiveDataScope proto 新增**:当前 iam.proto 仅有 `GetUserInfo`,无 DataScope 解析 API。需 coord 在 shared-proto 新增 `GetEffectiveDataScope` RPC
|
||||
3. **edu.insight.ai.usage topic 新增**:004 §7.2 未列出,需 coord 确认是否新增
|
||||
|
||||
---
|
||||
|
||||
# 模块架构设计文档 — ai
|
||||
|
||||
## 1. 模块内部分层图
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Entry["入口层"]
|
||||
HTTP[FastAPI HTTP Router<br/>/ai/* + /healthz + /readyz]
|
||||
GRPC[grpc.aio Server<br/>AiService 含 StreamChat]
|
||||
end
|
||||
|
||||
subgraph Middleware["中间件层"]
|
||||
AUTH[AuthDepends<br/>校验 x-user-id / x-user-roles]
|
||||
RATE[RateLimitDepends<br/>Redis 令牌桶限流]
|
||||
TRACE[OTel + grpc interceptor]
|
||||
end
|
||||
|
||||
subgraph Service["应用服务层"]
|
||||
S1[ChatService<br/>聊天编排 + Prompt 模板渲染]
|
||||
S2[QuestionGenerationService<br/>出题工作流编排]
|
||||
S3[ExpressionOptimizationService<br/>表达优化]
|
||||
S4[LessonPreparationWorkflow<br/>备课工作流 4 步编排]
|
||||
end
|
||||
|
||||
subgraph Provider["LLM Provider 适配层"]
|
||||
P0[LLMProvider 抽象接口<br/>chat / stream_chat]
|
||||
P1[OpenAIProvider<br/>httpx 异步]
|
||||
P2[AnthropicProvider<br/>httpx 异步]
|
||||
P3[BaichuanProvider<br/>httpx 异步]
|
||||
P4[LocalOllamaProvider<br/>httpx 异步]
|
||||
end
|
||||
|
||||
subgraph Template["Prompt 模板管理"]
|
||||
T1[PromptTemplateRegistry<br/>模板注册 + 渲染]
|
||||
T2[模板存储<br/>YAML 文件 / DB]
|
||||
end
|
||||
|
||||
subgraph Client["下游 gRPC client"]
|
||||
C1[ContentClient<br/>查询知识点 / 题库]
|
||||
C2[DataAnaClient<br/>查询学情 / 薄弱点]
|
||||
end
|
||||
|
||||
subgraph Usage["用量计费"]
|
||||
U1[UsageRecorder<br/>token 消耗统计]
|
||||
U2[KafkaProducer<br/>发布 edu.insight.ai.usage]
|
||||
end
|
||||
|
||||
subgraph External["外部 / 存储"]
|
||||
LLM[LLM Provider API<br/>OpenAI/Anthropic/百川/Ollama]
|
||||
CONTENT[content:3005 gRPC]
|
||||
DATAANA[data-ana:3006 gRPC]
|
||||
KAFKA[(Kafka)]
|
||||
REDIS[(Redis<br/>限流 + 缓存)]
|
||||
end
|
||||
|
||||
HTTP --> AUTH --> RATE --> S1
|
||||
HTTP --> S2
|
||||
HTTP --> S3
|
||||
GRPC --> S1
|
||||
GRPC --> S2
|
||||
S2 --> S4
|
||||
S4 --> C1
|
||||
S4 --> C2
|
||||
S1 --> T1
|
||||
S2 --> T1
|
||||
S1 --> P0
|
||||
S2 --> P0
|
||||
P0 --> P1
|
||||
P0 --> P2
|
||||
P0 --> P3
|
||||
P0 --> P4
|
||||
P1 --> LLM
|
||||
P2 --> LLM
|
||||
P3 --> LLM
|
||||
P4 --> LLM
|
||||
S1 --> U1
|
||||
S2 --> U1
|
||||
U1 --> U2
|
||||
U2 --> KAFKA
|
||||
RATE --> REDIS
|
||||
C1 --> CONTENT
|
||||
C2 --> DATAANA
|
||||
```
|
||||
|
||||
**分层规则**:
|
||||
|
||||
- **入口层**:HTTP(保留作 Gateway 直连)+ gRPC(主入口,含 `StreamChat` 流式 RPC)
|
||||
- **中间件层**:Auth + RateLimit(Redis 令牌桶,按 user_id 限流)
|
||||
- **应用服务层**:4 个 Service,每个对应一类 AI 能力
|
||||
- **Provider 适配层**:抽象 `LLMProvider` 接口,多适配器实现(策略模式)
|
||||
- **Prompt 模板**:模板注册 + 渲染,模板存储可配置(YAML 文件 or DB)
|
||||
- **下游 client**:gRPC 调 content / data-ana
|
||||
- **用量计费**:token 消耗统计 + Kafka 事件外发
|
||||
|
||||
## 2. 领域模型
|
||||
|
||||
ai 是**无状态服务**,不持有持久化聚合根。领域模型为**请求/响应模型 + 工作流编排**:
|
||||
|
||||
### 聚合根(请求型,无持久化)
|
||||
|
||||
| 聚合根 | 含义 | 生命周期 |
|
||||
| ---------------------------- | ------------ | ------------------------------------------------------------ |
|
||||
| `ChatConversation` | 单次聊天请求 | 单次请求 |
|
||||
| `QuestionGenerationTask` | 出题任务 | 单次请求(备课工作流中多步) |
|
||||
| `ExpressionOptimizationTask` | 表达优化任务 | 单次请求 |
|
||||
| `LessonPreparationWorkflow` | 备课工作流 | 跨多步(分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库) |
|
||||
|
||||
### 值对象
|
||||
|
||||
- `ChatMessage`:role + content
|
||||
- `Usage`:prompt_tokens + completion_tokens + total_tokens
|
||||
- `GeneratedQuestion`:question + answer + explanation
|
||||
- `PromptTemplate`:name + system_prompt + user_template + variables
|
||||
|
||||
### 工作流编排(备课)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant T as 教师
|
||||
participant BFF as teacher-bff
|
||||
participant AI as ai 服务
|
||||
participant Content as content
|
||||
participant DA as data-ana
|
||||
|
||||
T->>BFF: 请求备课(class_id, subject_id)
|
||||
BFF->>AI: gRPC GenerateLessonPlan
|
||||
AI->>DA: gRPC GetStudentWeakness(class_id)
|
||||
DA-->>AI: 薄弱知识点列表
|
||||
AI->>Content: gRPC GetPrerequisites(knowledge_point_id)
|
||||
Content-->>AI: 前置依赖知识点
|
||||
AI->>AI: LLM 生成题目(基于学情 + 知识点)
|
||||
AI-->>BFF: 题目列表 + 推荐理由
|
||||
BFF-->>T: 题目供审核
|
||||
T->>BFF: 确认入库
|
||||
BFF->>Content: gRPC CreateQuestions
|
||||
Content-->>BFF: 入库成功
|
||||
```
|
||||
|
||||
**工作流实现**:
|
||||
|
||||
- **简单场景**(4 步内):FastAPI BackgroundTasks + asyncio.gather 并行查询
|
||||
- **复杂场景**(含教师审核等待):待 coord 仲裁是否引入 Temporal(004 §2.3 列出 Temporal 用于 AI 编排)
|
||||
|
||||
## 3. 数据模型
|
||||
|
||||
ai 服务**无独占数据库**,无 MySQL schema。所有数据通过 Kafka 事件外发(用量计费)或 gRPC 查询下游。
|
||||
|
||||
### 用量计费(Kafka 事件 → data-ana 落 ClickHouse)
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "uuid",
|
||||
"user_id": "user-xxx",
|
||||
"request_id": "req-xxx",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"prompt_tokens": 150,
|
||||
"completion_tokens": 80,
|
||||
"total_tokens": 230,
|
||||
"latency_ms": 1200,
|
||||
"success": true,
|
||||
"occurred_at": "2026-07-09T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Topic**:`edu.insight.ai.usage`(**待 coord 新增**)
|
||||
|
||||
### 缓存策略
|
||||
|
||||
| 数据 | 存储 | TTL | 失效策略 |
|
||||
| -------------------------------------- | ----------------------------- | -------- | ---------------------- |
|
||||
| Prompt 模板 | Redis(模板变更事件驱动失效) | 1 小时 | 文件/DB 变更时主动失效 |
|
||||
| LLM 响应(相同 prompt) | Redis(hash 缓存) | 30 分钟 | 短 TTL,避免陈旧 |
|
||||
| DataScope(ai 不需要,仅 data-ana 用) | — | — | — |
|
||||
| 限流计数 | Redis 令牌桶 | 滑动窗口 | 自动过期 |
|
||||
|
||||
## 4. API 设计
|
||||
|
||||
### 4.1 HTTP 端点(保留作 Gateway 直连降级)
|
||||
|
||||
| method | path | 权限 | 请求体 | 响应 | 说明 |
|
||||
| ------ | ------------------------- | ------------------------ | --------------------------- | ------------------------------------ | ---------------------- |
|
||||
| GET | `/healthz` | — | — | `{status, service}` | liveness |
|
||||
| GET | `/readyz` | — | — | `{status, llm_configured, degraded}` | readiness |
|
||||
| GET | `/metrics` | — | — | Prometheus | 指标 |
|
||||
| POST | `/ai/chat` | `AI_CHAT` | `ChatRequest` | `ChatResponse` | LLM 聊天 |
|
||||
| POST | `/ai/chat/stream` | `AI_CHAT` | `ChatRequest` | SSE stream | 流式聊天 |
|
||||
| POST | `/ai/generate/question` | `AI_QUESTION_GENERATE` | `GenerateQuestionRequest` | `GeneratedQuestionResponse` | 生成题目 |
|
||||
| POST | `/ai/optimize/expression` | `AI_EXPRESSION_OPTIMIZE` | `OptimizeExpressionRequest` | `OptimizedExpressionResponse` | 优化表达 |
|
||||
| POST | `/ai/lesson/preparation` | `AI_LESSON_PREPARE` | `LessonPreparationRequest` | `LessonPreparationResponse` | 备课工作流(**新增**) |
|
||||
|
||||
### 4.2 gRPC 契约(ai.proto,待实现 server)
|
||||
|
||||
| RPC | 请求 | 响应 | 权限 | 说明 |
|
||||
| -------------------- | ------------------------------------------------------ | ------------------------------------- | ------------------------ | ------------------------- |
|
||||
| `Chat` | `ChatRequest{messages, model, temperature}` | `ChatResponse{content, model, usage}` | `AI_CHAT` | 非流式聊天 |
|
||||
| `StreamChat` | `ChatRequest` | `stream ChatChunk` | `AI_CHAT` | 流式聊天(SSE over gRPC) |
|
||||
| `GenerateQuestion` | `GenerateQuestionRequest{prompt, subject, difficulty}` | `GeneratedQuestion` | `AI_QUESTION_GENERATE` | 生成题目 |
|
||||
| `OptimizeExpression` | `OptimizeExpressionRequest{text, context}` | `OptimizedExpression` | `AI_EXPRESSION_OPTIMIZE` | 优化表达 |
|
||||
|
||||
### 4.3 Pydantic 请求/响应模型
|
||||
|
||||
```python
|
||||
class ChatRequest(BaseModel):
|
||||
messages: list[ChatMessage]
|
||||
model: str = "gpt-4o-mini"
|
||||
temperature: float = Field(0.7, ge=0.0, le=2.0)
|
||||
stream: bool = False
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: Literal["system", "user", "assistant"]
|
||||
content: str
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
success: bool
|
||||
data: ChatData
|
||||
degraded: bool = False
|
||||
|
||||
class ChatData(BaseModel):
|
||||
content: str
|
||||
model: str
|
||||
usage: Usage
|
||||
|
||||
class Usage(BaseModel):
|
||||
prompt_tokens: int
|
||||
completion_tokens: int
|
||||
total_tokens: int
|
||||
|
||||
class GenerateQuestionRequest(BaseModel):
|
||||
prompt: str = Field(..., min_length=1, max_length=2000)
|
||||
subject: str
|
||||
difficulty: Literal["easy", "medium", "hard"]
|
||||
knowledge_point_ids: list[str] = [] # 可选:靶向知识点
|
||||
|
||||
class GeneratedQuestionResponse(BaseModel):
|
||||
success: bool
|
||||
data: GeneratedQuestionData
|
||||
degraded: bool = False
|
||||
```
|
||||
|
||||
## 5. 事件设计
|
||||
|
||||
### 5.1 消费的事件
|
||||
|
||||
ai 服务**不消费任何事件**(无状态,纯请求-响应)。
|
||||
|
||||
### 5.2 发布的事件
|
||||
|
||||
| 事件 | Topic | 触发时机 | 消费者 | Payload |
|
||||
| ----------------- | ------------------------------------------- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `AIUsageRecorded` | `edu.insight.ai.usage`(**待 coord 新增**) | 每次 LLM 调用完成 | data-ana(落 ClickHouse ai_usage_log) | `{event_id, user_id, request_id, provider, model, prompt_tokens, completion_tokens, total_tokens, latency_ms, success, occurred_at}` |
|
||||
|
||||
**发布实现**:
|
||||
|
||||
- 每次 LLM 调用后异步发布(不阻塞响应)
|
||||
- `aiokafka.AIOKafkaProducer` + `acks=all`
|
||||
- 失败重试 3 次,仍失败记录日志(不影响主流程)
|
||||
|
||||
## 6. 横切关注点对齐清单
|
||||
|
||||
### 6.1 权限装饰器等价物
|
||||
|
||||
```python
|
||||
class Permissions:
|
||||
AI_CHAT = "ai:chat"
|
||||
AI_QUESTION_GENERATE = "ai:question:generate"
|
||||
AI_EXPRESSION_OPTIMIZE = "ai:expression:optimize"
|
||||
AI_LESSON_PREPARE = "ai:lesson:prepare"
|
||||
|
||||
async def require_permission(permission: str) -> UserContext:
|
||||
"""从 x-user-id / x-user-roles 校验权限."""
|
||||
...
|
||||
```
|
||||
|
||||
### 6.2 错误码清单(前缀 `AI_*`)
|
||||
|
||||
| 错误码 | 触发条件 | HTTP | gRPC status |
|
||||
| --------------------------- | ----------------------------------------- | ------------------- | ------------------ |
|
||||
| `AI_UNAUTHORIZED` | 缺失 x-user-id 或 token 无效 | 401 | UNAUTHENTICATED |
|
||||
| `AI_FORBIDDEN` | 角色无对应权限 | 403 | PERMISSION_DENIED |
|
||||
| `AI_RATE_LIMITED` | 触发限流 | 429 | RESOURCE_EXHAUSTED |
|
||||
| `AI_LLM_UNAVAILABLE` | LLM Provider 不可达(降级模式仍返回骨架) | 200 + degraded:true | OK + degraded flag |
|
||||
| `AI_LLM_TIMEOUT` | LLM 调用超时(30s) | 504 | DEADLINE_EXCEEDED |
|
||||
| `AI_INVALID_MODEL` | model 名不支持 | 400 | INVALID_ARGUMENT |
|
||||
| `AI_INVALID_DIFFICULTY` | difficulty 不在 easy/medium/hard | 400 | INVALID_ARGUMENT |
|
||||
| `AI_DOWNSTREAM_UNAVAILABLE` | content / data-ana gRPC 不可达 | 502 | UNAVAILABLE |
|
||||
| `AI_PROMPT_RENDER_FAILED` | Prompt 模板渲染失败(变量缺失) | 500 | INTERNAL |
|
||||
| `AI_INTERNAL_ERROR` | 未捕获异常 | 500 | INTERNAL |
|
||||
|
||||
### 6.3 Logger 初始化
|
||||
|
||||
- 位置:`main.py`(已具备)
|
||||
- **改进**:生产环境改用 `JSONRenderer`
|
||||
|
||||
### 6.4 Metrics 指标清单
|
||||
|
||||
| 指标名 | 类型 | 标签 | 描述 |
|
||||
| ---------------------------------- | --------- | ----------------------------------------- | ------------------ |
|
||||
| `ai_http_requests_total` | Counter | method, path, status | HTTP 请求总数 |
|
||||
| `ai_http_request_duration_seconds` | Histogram | method, path | HTTP 请求延迟 |
|
||||
| `ai_llm_calls_total` | Counter | provider, model, status | LLM 调用总数 |
|
||||
| `ai_llm_call_duration_seconds` | Histogram | provider, model | LLM 调用延迟 |
|
||||
| `ai_llm_tokens_total` | Counter | provider, model, type (prompt/completion) | token 消耗总数 |
|
||||
| `ai_llm_stream_chunks_total` | Counter | provider, model | 流式 chunk 总数 |
|
||||
| `ai_grpc_calls_total` | Counter | downstream, method, status | 下游 gRPC 调用总数 |
|
||||
| `ai_rate_limit_hits_total` | Counter | user_id | 限流命中次数 |
|
||||
| `ai_usage_events_published_total` | Counter | status | 用量事件发布数 |
|
||||
| `ai_prompt_template_renders_total` | Counter | template_name, status | 模板渲染次数 |
|
||||
|
||||
### 6.5 Tracer 初始化
|
||||
|
||||
- 位置:`main.py` `init_tracer()`(已具备,dev_mode 跳过)
|
||||
- **改进**:gRPC server interceptor + 下游 gRPC client interceptor 透传 trace context
|
||||
|
||||
### 6.6 /healthz 检查逻辑
|
||||
|
||||
- liveness:仅进程存活(已具备)
|
||||
|
||||
### 6.7 /readyz 检查逻辑
|
||||
|
||||
```python
|
||||
async def readyz() -> dict:
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": "ai",
|
||||
"llm_configured": settings.llm_available,
|
||||
"degraded": not settings.llm_available,
|
||||
"providers": {
|
||||
"openai": bool(settings.openai_api_key),
|
||||
"anthropic": bool(settings.anthropic_api_key),
|
||||
"baichuan": bool(settings.baichuan_api_key),
|
||||
"local_ollama": bool(settings.ollama_base_url),
|
||||
},
|
||||
"downstream_grpc": {
|
||||
"content": "ok" | "unreachable", # 新增
|
||||
"data_ana": "ok" | "unreachable", # 新增
|
||||
},
|
||||
"redis": "ok" | "unreachable", # 新增(限流依赖)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 优雅关闭顺序
|
||||
|
||||
1. HTTP server stop accepting new requests
|
||||
2. gRPC server graceful stop(**关键**:等待在途 `StreamChat` 流式 RPC 完成,60s 超时,避免截断用户响应)
|
||||
3. LLM 流式请求 drain(等待 httpx stream 完成)
|
||||
4. Kafka producer flush + close(确保用量事件已投递)
|
||||
5. 下游 gRPC channels close(content / data-ana)
|
||||
6. Redis connection close
|
||||
|
||||
## 7. 与其他模块的交互点(契约清单)
|
||||
|
||||
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
|
||||
| ------ | ------------ | ----- | ---------------------------------------------------------- | ------------------ |
|
||||
| 被调用 | api-gateway | HTTP | `/ai/*` | Gateway 代理 |
|
||||
| 被调用 | teacher-bff | gRPC | `AiService.*` | BFF 聚合 AI 能力 |
|
||||
| 调用 | content | gRPC | `KnowledgeGraphService.GetPrerequisites / GetLearningPath` | 出题上下文查询 |
|
||||
| 调用 | content | gRPC | `TextbookService.*`(若需教材上下文) | 出题教材关联 |
|
||||
| 调用 | data-ana | gRPC | `AnalyticsService.GetStudentWeakness / GetLearningTrend` | 个性化出题学情查询 |
|
||||
| 调用 | LLM Provider | HTTP | OpenAI 兼容 REST `/chat/completions` | LLM 推理 |
|
||||
| 发布 | — | Kafka | `edu.insight.ai.usage`(**待 coord 新增**) | 用量计费外发 |
|
||||
|
||||
## 8. 风险与假设
|
||||
|
||||
### 8.1 假设
|
||||
|
||||
- **假设 1**:content 服务实现了 `KnowledgeGraphService` gRPC server。当前 content.proto 已定义但未实现 gRPC server(ai05 阶段 2 设计中)。ai 调用前需确认 content gRPC 可用
|
||||
- **假设 2**:data-ana 实现了 `AnalyticsService` gRPC server(本设计文档已设计)。ai 调用前需确认 data-ana gRPC 可用
|
||||
- **假设 3**:coord 同意新增 `edu.insight.ai.usage` topic。若不同意,用量计费降级为 ai 本地日志(不落 ClickHouse,影响成本分析)
|
||||
- **假设 4**:Redis 可用(限流依赖)。若 Redis 不可用,限流降级为"无限制"(风险:LLM 成本失控),或降级为内存令牌桶(单实例有效,多实例不一致)
|
||||
|
||||
### 8.2 技术风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
| ---------------------- | ---------------- | --------------------------------------------------------------- |
|
||||
| LLM 调用延迟高(>30s) | 用户体验差 | 超时 30s + 降级骨架响应 + 流式优先(用户感知首字延迟) |
|
||||
| LLM 成本失控 | 财务风险 | Redis 令牌桶限流(每用户每分钟 10 次)+ 用量计费监控 + 告警阈值 |
|
||||
| LLM Provider 单点故障 | 服务不可用 | 多 Provider 适配器 + 自动 failover(OpenAI 失败切 Anthropic) |
|
||||
| 流式 RPC 中断 | 用户响应截断 | gRPC server graceful shutdown 60s drain + 客户端重连机制 |
|
||||
| Prompt 注入攻击 | LLM 输出恶意内容 | 输入 sanitize + system prompt 加安全约束 + 输出过滤 |
|
||||
| 下游 gRPC 不可达 | 备课工作流失败 | 降级:跳过学情查询,仅基于 prompt 生成题目 + `degraded: true` |
|
||||
|
||||
### 8.3 未决设计决策(需 coord 仲裁)
|
||||
|
||||
1. **备课工作流是否引入 Temporal**:004 §2.3 列出 Temporal 用于 AI 编排,但简单 4 步工作流可用 FastAPI BackgroundTasks。建议:M14 用 BackgroundTasks,M15 评估是否迁移 Temporal
|
||||
2. **`edu.insight.ai.usage` topic 新增**:需 coord 在 shared-proto events.proto 新增 `AIUsageEvent` message + 004 §7.2 新增 topic
|
||||
3. **LLM Provider 配置化路由**:是否在 shared-py 建立通用 `LLMProvider` 抽象(供未来其他 Python 服务复用)。建议 P5 阶段在 ai 服务内部实现,P6 评估是否提取到 shared-py
|
||||
4. **Prompt 模板存储位置**:YAML 文件(简单,无 DB)vs DB(动态更新)。建议 P5 用 YAML 文件(`services/ai/src/ai/prompts/*.yaml`),P6 评估迁移 DB
|
||||
|
||||
---
|
||||
|
||||
# 阶段 2 总结
|
||||
|
||||
ai06 已完成阶段 2 模块架构设计,产出 data-ana 与 ai 两份设计文档。核心设计决策:
|
||||
|
||||
## data-ana 设计要点
|
||||
|
||||
1. **分层**:HTTP + gRPC 双入口共享 Application Service;CDC 消费者独立后台任务
|
||||
2. **数据模型**:4 张 ClickHouse 宽表(student_dashboard_view / student_errors / mastery_snapshot / ai_usage_log),ReplacingMergeTree 引擎保证幂等
|
||||
3. **权限**:FastAPI Depends 链(require_permission + inject_data_scope),DataScope WHERE 注入 ClickHouse 查询
|
||||
4. **事件**:消费 6 个 CDC topic + 发布 `edu.insight.mastery.updated`(直接 producer,非 Outbox,因派生数据非事务写)
|
||||
5. **gRPC**:实现 `AnalyticsService` server(analytics.proto 已定义)
|
||||
6. **降级**:ClickHouse 不可达返回骨架 + degraded:true;iam gRPC 不可达降级为 SELF DataScope
|
||||
|
||||
## ai 设计要点
|
||||
|
||||
1. **分层**:HTTP + gRPC 双入口;LLM Provider 适配层(策略模式,4 适配器)
|
||||
2. **无状态**:无 DB,用量计费通过 Kafka 事件外发
|
||||
3. **权限**:FastAPI Depends + Redis 令牌桶限流(每用户每分钟 10 次)
|
||||
4. **工作流**:备课 4 步编排(学情查询 → 知识点推荐 → 题目生成 → 教师审核入库),简单场景用 BackgroundTasks
|
||||
5. **gRPC**:实现 `AiService` server(含 `StreamChat` 流式 RPC);gRPC client 调 content / data-ana
|
||||
6. **降级**:LLM 不可达返回骨架 + degraded:true;下游 gRPC 不可达降级跳过
|
||||
|
||||
## 待 coord 交叉审查项(汇总)
|
||||
|
||||
| # | 议题 | 涉及文档 |
|
||||
| --- | ------------------------------------------------------------------------------------ | ----------------------- |
|
||||
| 1 | data-ana 发布 `edu.insight.mastery.updated` 用直接 producer(非 Outbox)是否合规 | 004 §12.2 |
|
||||
| 2 | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message | 004 §7.2 + events.proto |
|
||||
| 3 | iam 新增 `GetEffectiveDataScope` gRPC RPC | iam.proto |
|
||||
| 4 | data-ana / ai 实现 gRPC server 决策 | 004 §4.1 |
|
||||
| 5 | ClickHouse DDL 管理位置(建议 `infra/clickhouse/ddl/`) | infra/ |
|
||||
| 6 | ai 备课工作流是否引入 Temporal | 004 §2.3 |
|
||||
| 7 | 端口冲突检查:data-ana 3006 / ai 3008(无冲突) | full-stack-runbook |
|
||||
| 8 | 错误码前缀检查:`DATA_ANA_*` / `AI_*`(与其他服务不重叠) | — |
|
||||
| 9 | 黄金模板对齐:Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可 | — |
|
||||
|
||||
下一步:等待 coord 交叉审查通过后,进入阶段 3(按图实施)。
|
||||
171
docs/architecture/runbooks/incident-response.md
Normal file
171
docs/architecture/runbooks/incident-response.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 事件响应手册
|
||||
|
||||
> 目标:规范生产事件的分级、响应、处置与复盘,确保 RTO ≤ 30min
|
||||
> 关联:`docs/architecture/runbooks/p6-hardening.md`
|
||||
|
||||
## 1. 事件分级
|
||||
|
||||
| 级别 | 定义 | 影响 | 响应时效 | 升级 |
|
||||
|------|------|------|----------|------|
|
||||
| P0 | 全站不可用 / 核心数据损坏 | 全部用户 | 5 分钟内响应 | 立即升级至 CTO |
|
||||
| P1 | 核心功能不可用 / 关键 SLO 破坏 | 大量用户 | 10 分钟内响应 | 升级至服务负责人 |
|
||||
| P2 | 部分功能降级 / 非核心故障 | 部分用户 | 30 分钟内响应 | 服务负责人跟进 |
|
||||
| P3 | 单点告警 / 潜在风险 | 少量/无用户 | 工作时间内响应 | On-Call 自行处理 |
|
||||
|
||||
### 1.1 分级示例
|
||||
|
||||
- P0:网关全挂、主 DB 不可用且无法故障转移、数据丢失超过 RPO
|
||||
- P1:登录不可用、课程播放不可用、Kafka 生产阻塞
|
||||
- P2:消息推送延迟、报表生成失败、单个非核心服务宕机
|
||||
- P3:单 Pod 重启、磁盘使用率告警、慢查询告警
|
||||
|
||||
## 2. 响应流程
|
||||
|
||||
```
|
||||
发现 → 确认 → 升级 → 处理 → 恢复 → 复盘
|
||||
```
|
||||
|
||||
### 2.1 发现
|
||||
|
||||
- 告警来源:Prometheus / Alertmanager / 用户反馈 / 人工巡检
|
||||
- 第一动作:在 On-Call 群贴告警,标注收到时间
|
||||
|
||||
### 2.2 确认
|
||||
|
||||
- On-Call 5 分钟内确认告警真实性
|
||||
- 排除误报(探针抖动、已知维护窗口)
|
||||
- 初步定级,创建事故工单(Jira / 飞书项目)
|
||||
|
||||
### 2.3 升级
|
||||
|
||||
- 超出处置能力 → 立即升级
|
||||
- P0/P1 → 拉事故群,通知相关服务 Owner
|
||||
- 涉及外部公告 → 通知客服与公关
|
||||
|
||||
### 2.4 处理
|
||||
|
||||
- 遵循"先恢复,后定位"原则
|
||||
- 优先使用预案:回滚、扩容、降级、熔断、切换
|
||||
- 每个操作记录时间戳与执行人
|
||||
- 关键决策需事故指挥确认
|
||||
|
||||
### 2.5 恢复
|
||||
|
||||
- 健康检查通过、SLO 恢复
|
||||
- 观察 15 分钟确认稳定
|
||||
- 关闭事故工单,进入复盘
|
||||
|
||||
### 2.6 复盘
|
||||
|
||||
- 48 小时内提交复盘报告
|
||||
- 无 blame 文化:对事不对人
|
||||
- 输出改进项,录入 `docs/architecture/roadmap/tech-debt.md`
|
||||
|
||||
## 3. On-Call 轮值
|
||||
|
||||
### 3.1 轮值制度
|
||||
|
||||
- 主备双人轮值,每周轮换
|
||||
- 工作日:9:00-21:00 主,其余备
|
||||
- 节假日:全天主备
|
||||
- 交接:周一 10:00 站会交接,遗留问题清单
|
||||
|
||||
### 3.2 联络方式
|
||||
|
||||
- 电话:主 + 备 + 升级链
|
||||
- 即时通讯:飞书 On-Call 群
|
||||
- 告警:PagerDuty / 飞书机器人
|
||||
|
||||
### 3.3 响应要求
|
||||
|
||||
- P0/P1:电话 5 分钟内接听
|
||||
- 告警确认:群内 5 分钟内回复
|
||||
- 无法响应:自动升级至备值
|
||||
|
||||
## 4. 沟通模板
|
||||
|
||||
### 4.1 事故通报(初报)
|
||||
|
||||
```
|
||||
【事故通报】<P级别> - <简述>
|
||||
时间:<YYYY-MM-DD HH:MM>
|
||||
级别:<P0/P1/P2/P3>
|
||||
影响:<受影响功能/用户范围>
|
||||
现状:<已知信息>
|
||||
负责人:<On-Call>
|
||||
下一步:<计划动作>
|
||||
```
|
||||
|
||||
### 4.2 进展更新(每 30 分钟或重大变化)
|
||||
|
||||
```
|
||||
【进展更新】<事故标题>
|
||||
时间:<HH:MM>
|
||||
进展:<自上次以来发生/完成的事>
|
||||
当前状态:<仍受影响的功能>
|
||||
下一步:<接下来 30 分钟计划>
|
||||
```
|
||||
|
||||
### 4.3 恢复通知
|
||||
|
||||
```
|
||||
【恢复通知】<事故标题>
|
||||
恢复时间:<HH:MM>
|
||||
持续时长:<时长>
|
||||
原因:<根因摘要>
|
||||
影响:<最终影响评估>
|
||||
后续:<复盘会时间>
|
||||
```
|
||||
|
||||
### 4.4 复盘报告
|
||||
|
||||
```
|
||||
【复盘报告】<事故标题>
|
||||
时间:<起止时间>
|
||||
级别:<P级别>
|
||||
影响:<用户/功能/数据>
|
||||
时间线:<关键事件时间轴>
|
||||
根因:<5why 分析>
|
||||
处置:<做了什么、有效/无效>
|
||||
改进项:<TODO + 负责人 + 截止>
|
||||
经验:<可沉淀到 known-issues / runbook 的内容>
|
||||
```
|
||||
|
||||
## 5. 常见事故处置
|
||||
|
||||
### 5.1 DB 主从切换
|
||||
|
||||
1. 确认主库故障(健康检查、连接超时)
|
||||
2. 触发自动故障转移(Patroni / Orchestrator)
|
||||
3. 若自动失败,手动 `./scripts/db/failover.sh --service <svc>`
|
||||
4. 更新连接配置 / 刷新连接池
|
||||
5. 验证读写正常、数据位点
|
||||
6. 旧主恢复后作为从库加入
|
||||
|
||||
### 5.2 Kafka 消费堆积
|
||||
|
||||
1. 查看堆积:`kubectl exec -- kafka-consumer-lag`
|
||||
2. 定位慢消费者:查日志、trace
|
||||
3. 扩容消费者副本(HPA 或手动)
|
||||
4. 若处理逻辑慢:临时降级非核心处理
|
||||
5. 堆积消化后恢复
|
||||
6. 复盘:扩容阈值、消费者并发配置
|
||||
|
||||
### 5.3 服务雪崩
|
||||
|
||||
1. 确认雪崩源头(哪个下游故障)
|
||||
2. 确认熔断器已打开(Gateway 状态)
|
||||
3. 若未打开:手动 `./scripts/gateway/circuit-breaker.sh open <service>`
|
||||
4. 降级非核心功能
|
||||
5. 扩容上游服务应对重试流量
|
||||
6. 修复下游、半开试探、逐步恢复
|
||||
7. 复盘:熔断参数、依赖隔离
|
||||
|
||||
## 附录:升级链
|
||||
|
||||
| 级别 | 第一响应 | 升级 1 | 升级 2 | 升级 3 |
|
||||
|------|----------|--------|--------|--------|
|
||||
| P0 | On-Call | 服务负责人 | 架构负责人 | CTO |
|
||||
| P1 | On-Call | 服务负责人 | 架构负责人 | - |
|
||||
| P2 | On-Call | 服务负责人 | - | - |
|
||||
| P3 | On-Call | - | - | - |
|
||||
264
docs/architecture/runbooks/p6-hardening.md
Normal file
264
docs/architecture/runbooks/p6-hardening.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# P6 生产硬化 Runbook
|
||||
|
||||
> 阶段:P6 生产硬化
|
||||
> 目标指标:RPO ≤ 15min,RTO ≤ 30min,P99 ≤ 500ms
|
||||
> 维护者:SRE 团队
|
||||
> 关联文档:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/roadmap/tech-debt.md`、`docs/architecture/runbooks/incident-response.md`
|
||||
|
||||
## 1. 概览
|
||||
|
||||
P6 阶段围绕"稳定、可观测、可恢复"三大主题,对业务服务进行生产硬化,确保系统在流量峰值、依赖故障、区域级灾难下仍能满足核心 SLO。
|
||||
|
||||
### 1.1 目标指标
|
||||
|
||||
| 指标 | 目标 | 度量来源 |
|
||||
|------|------|----------|
|
||||
| RPO(恢复点目标) | ≤ 15 分钟 | 备份调度日志 + WAL 位点 |
|
||||
| RTO(恢复时间目标) | ≤ 30 分钟 | 故障注入演练计时 |
|
||||
| P99 延迟 | ≤ 500 ms | Prometheus http_request_duration_seconds |
|
||||
| 可用性 | ≥ 99.9% | 多区域健康探针汇总 |
|
||||
| 错误率 | ≤ 0.1% | Gateway 5xx 比率 |
|
||||
|
||||
### 1.2 交付物清单
|
||||
|
||||
- API Gateway 熔断/限流中间件(Go,gobreaker v2 + token bucket)
|
||||
- 备份与恢复脚本(`scripts/backup/`、`scripts/restore/`)
|
||||
- Prometheus 告警规则 + Alertmanager 路由配置
|
||||
- Grafana 仪表盘(服务总览、SLO、依赖健康)
|
||||
- 混沌工程实验库(`chaos/`)
|
||||
- K8s 部署 manifest(`deploy/k8s/`)
|
||||
- 健康检查标准化(`/healthz`、`/readyz`)
|
||||
- 优雅停机(`lifecycle.service.ts` + `app.enableShutdownHooks()`)
|
||||
|
||||
## 2. 熔断与限流
|
||||
|
||||
### 2.1 中间件使用
|
||||
|
||||
API Gateway(Go)在路由链路上统一接入:
|
||||
- 熔断器:`github.com/sony/gobreaker/v2`,按下游服务维度建桶
|
||||
- 限流:基于 `sync.Map` 的令牌桶,按 `route + tenant` 维度限流
|
||||
|
||||
熔断器配置示例(伪代码):
|
||||
|
||||
```go
|
||||
cb := gobreaker.NewCircuitBreaker[any](gobreaker.Settings{
|
||||
Name: "core-edu",
|
||||
MaxRequests: 5, // 半开态最大试探请求
|
||||
Interval: 60 * time.Second, // 计数窗口
|
||||
Timeout: 30 * time.Second, // 开启态冷却
|
||||
ReadyToTrip: func(c gobreaker.Counts) bool {
|
||||
return c.ConsecutiveFailures > 5 || c.TotalFailures/c.TotalRequests > 0.5
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 2.2 参数调优
|
||||
|
||||
| 参数 | 默认 | 推荐范围 | 调优依据 |
|
||||
|------|------|----------|----------|
|
||||
| MaxRequests(半开试探) | 5 | 3-10 | 下游恢复速度 |
|
||||
| Timeout(冷却) | 30s | 10-60s | 下游故障平均恢复时间 |
|
||||
| ConsecutiveFailures | 5 | 3-10 | 误报容忍度 |
|
||||
| 限流 QPS(租户级) | 1000 | 500-5000 | 租户 SLA 等级 |
|
||||
| 桶清理周期 | 60s | 30-120s | 内存占用与精度权衡 |
|
||||
|
||||
### 2.3 故障演练流程
|
||||
|
||||
1. 在预发环境注入下游延迟(tc/netem 500ms+)
|
||||
2. 观察熔断器状态切换:Closed → Open → Half-Open → Closed
|
||||
3. 验证限流桶在窗口边界正确清理(sync.Map + ticker)
|
||||
4. 记录 P99 与错误率到演练报告
|
||||
5. 回滚注入:`./chaos/rollback.sh <experiment>`
|
||||
|
||||
## 3. 备份与恢复
|
||||
|
||||
### 3.1 备份脚本
|
||||
|
||||
- 位置:`scripts/backup/backup-cron.sh`
|
||||
- 调度:K8s CronJob,每 15 分钟一次(满足 RPO ≤ 15min)
|
||||
- 内容:PostgreSQL 全量 + WAL 归档 + Redis RDB + Kafka topic offset 快照
|
||||
- 产物:写入对象存储(MinIO/S3),保留策略 7d(日备 30d)
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
./scripts/backup/backup-cron.sh --service iam --type full
|
||||
./scripts/backup/backup-cron.sh --service iam --type incr
|
||||
```
|
||||
|
||||
### 3.2 恢复演练流程
|
||||
|
||||
1. 在隔离环境拉起空集群
|
||||
2. 执行 `./scripts/restore/restore.sh --service <svc> --backup-id <id>`
|
||||
3. 校验数据一致性(行数、最新位点、校验和)
|
||||
4. 记录恢复耗时,验证 RTO ≤ 30min
|
||||
5. 演练频率:每月一次
|
||||
|
||||
### 3.3 RPO 验证方法
|
||||
|
||||
- 对比最近一次备份时间与当前时间差
|
||||
- 查询 `backup_log` 表 `last_success_at` 字段
|
||||
- 告警:连续 2 个周期(30min)未成功备份 → P1
|
||||
|
||||
## 4. 监控告警
|
||||
|
||||
### 4.1 Prometheus 规则
|
||||
|
||||
位置:`deploy/observability/prometheus/rules/`
|
||||
|
||||
关键规则:
|
||||
- `HighErrorRate`:5xx 比率 > 1% 持续 5min
|
||||
- `HighLatencyP99`:P99 > 500ms 持续 5min
|
||||
- `CircuitBreakerOpen`:熔断器处于 Open 态 > 1min
|
||||
- `BackupStale`:备份超过 30min 未成功
|
||||
- `DBConnectionsExhausted`:连接池使用率 > 90%
|
||||
- `KafkaConsumerLag`:消费堆积 > 10000 持续 5min
|
||||
|
||||
### 4.2 Alertmanager 路由
|
||||
|
||||
- P0/P1 → PagerDuty + 电话
|
||||
- P2 → 飞书群 + 邮件
|
||||
- P3 → 飞书群
|
||||
- 抑制:同一服务 5min 内同告警只发一次(inhibit + group_by)
|
||||
|
||||
### 4.3 Grafana 仪表盘
|
||||
|
||||
| 仪表盘 | 用途 | 关键面板 |
|
||||
|--------|------|----------|
|
||||
| Service Overview | 服务总览 | QPS、P99、错误率、熔断状态 |
|
||||
| SLO Dashboard | SLO 跟踪 | 可用性、错误预算消耗 |
|
||||
| Dependency Health | 依赖健康 | DB/Redis/Kafka 连接与延迟 |
|
||||
| Backup Status | 备份状态 | 最近备份、RPO、恢复演练 |
|
||||
|
||||
## 5. 混沌工程
|
||||
|
||||
### 5.1 实验清单
|
||||
|
||||
| 实验 | 注入方式 | 预期表现 | 频率 |
|
||||
|------|----------|----------|------|
|
||||
| DB 主节点宕机 | kill postgres | 自动故障转移,RTO<30min | 月 |
|
||||
| Redis 不可达 | iptables 拒绝 | 降级到本地缓存 | 月 |
|
||||
| Kafka broker 宕机 | kill broker | 生产重试,消费堆积可控 | 月 |
|
||||
| 下游服务延迟 | tc netem +500ms | 熔断器打开,错误率<1% | 周 |
|
||||
| 网络分区 | iptables 隔离 | 多可用区切换 | 季 |
|
||||
| 磁盘满 | fill disk | 告警触发,写入降级 | 季 |
|
||||
|
||||
### 5.2 执行流程
|
||||
|
||||
1. 选择实验 → 在 `chaos/` 选择对应 YAML
|
||||
2. 预检:确认告警通道、回滚脚本就绪
|
||||
3. 执行:`kubectl apply -f chaos/<experiment>.yaml`
|
||||
4. 观察:Grafana + 日志
|
||||
5. 回滚:`./chaos/rollback.sh <experiment>`
|
||||
6. 复盘:记录到 `docs/architecture/runbooks/incident-response.md`
|
||||
|
||||
## 6. 安全加固
|
||||
|
||||
### 6.1 WAF 规则
|
||||
|
||||
- SQL 注入、XSS 模式匹配
|
||||
- 路径穿越、命令注入
|
||||
- 限速:单 IP > 100req/s 拦截
|
||||
- 地域封禁(按需)
|
||||
|
||||
### 6.2 密钥管理
|
||||
|
||||
- 密钥存储:K8s Secret + 外部 KMS(Vault)
|
||||
- 轮换:DB 密码每 90 天,JWT 签名密钥每 180 天
|
||||
- 注入:通过环境变量 / 挂载卷,禁止入镜像
|
||||
- 审计:所有密钥访问记录到 audit log
|
||||
|
||||
### 6.3 CORS 策略
|
||||
|
||||
- 允许来源:白名单域名(生产环境严格)
|
||||
- 允许方法:GET/POST/PUT/PATCH/DELETE
|
||||
- 凭证:允许(Cookie)
|
||||
- 预检缓存:600s
|
||||
|
||||
## 7. K8s 部署
|
||||
|
||||
### 7.1 Manifest 说明
|
||||
|
||||
位置:`deploy/k8s/`,每个服务一组 manifest:
|
||||
|
||||
- `deployment.yaml`:副本数、资源、探针、优雅停机
|
||||
- `service.yaml`:ClusterIP
|
||||
- `hpa.yaml`:CPU>70% 扩容,min=3 max=20
|
||||
- `poddisruptionbudget.yaml`:minAvailable=2
|
||||
- `networkpolicy.yaml`:限制出向
|
||||
|
||||
探针配置:
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 3000 }
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /readyz, port: 3000 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
```
|
||||
|
||||
优雅停机:
|
||||
|
||||
```yaml
|
||||
terminationGracePeriodSeconds: 60
|
||||
```
|
||||
|
||||
### 7.2 Helm 化路线
|
||||
|
||||
- P6:原生 manifest(快速验证)
|
||||
- P7:抽取 Helm Chart,values.yaml 按环境区分
|
||||
- P8:引入 Argo CD GitOps 自动同步
|
||||
|
||||
## 8. 灾难恢复
|
||||
|
||||
### 8.1 RTO/RPO 目标
|
||||
|
||||
| 场景 | RTO | RPO |
|
||||
|------|-----|-----|
|
||||
| 单 Pod 故障 | 30s | 0 |
|
||||
| 单节点故障 | 2min | 0 |
|
||||
| 单可用区故障 | 10min | 0 |
|
||||
| 区域级灾难 | 30min | 15min |
|
||||
|
||||
### 8.2 多可用区策略
|
||||
|
||||
- K8s 集群跨 3 可用区,Pod 反亲和
|
||||
- DB 主从跨可用区同步复制
|
||||
- Redis 哨兵跨可用区
|
||||
- Kafka min.insync.replicas=2,跨可用区 broker
|
||||
|
||||
### 8.3 DNS 切换
|
||||
|
||||
- 区域级故障:通过全局 DNS(Cloudflare/Route53)切换到备用区域
|
||||
- 健康检查:每 10s 探测,连续 3 次失败自动切换
|
||||
- TTL:60s(快速切换)
|
||||
- 演练:每季度一次 DNS 切换演练
|
||||
|
||||
## 9. 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|
||||
|------|----------|----------|----------|
|
||||
| 5xx 激增 | 下游服务故障 | 查 Grafana 熔断状态、下游健康 | 确认熔断器已打开,扩容下游 |
|
||||
| P99 升高 | DB 慢查询/连接耗尽 | 查 PG 慢日志、连接池 | 加索引/扩连接池/限流 |
|
||||
| 消费堆积 | 消费者慢/宕机 | 查 Kafka lag、消费者日志 | 扩消费者、修 bug |
|
||||
| 备份失败 | 存储/网络/凭证 | 查 backup-cron 日志 | 修凭证、清理旧备份 |
|
||||
| 健康检查失败 | DB 不可达 | 查 DB 状态、网络 | 故障转移、恢复 DB |
|
||||
| Pod 频繁重启 | OOM/探针失败 | 查 kubectl describe、内存 | 调资源/修探针 |
|
||||
| 熔断不恢复 | 下游未恢复 | 查 Half-Open 试探结果 | 修复下游、调 Timeout |
|
||||
| 限流误杀 | 桶配置过低 | 查限流日志、QPS | 调高桶容量 |
|
||||
| DNS 切换无效 | TTL 缓存 | 查 DNS 解析链 | 等待 TTL / 清缓存 |
|
||||
| 跨区延迟高 | 跨区流量 | 查网络拓扑 | 调亲和性就近访问 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:关联文档
|
||||
|
||||
- 架构影响地图:`docs/architecture/004_architecture_impact_map.md`
|
||||
- P6 架构补记:`docs/architecture/004-p6-addendum.md`
|
||||
- 事件响应:`docs/architecture/runbooks/incident-response.md`
|
||||
- 已知问题:`docs/troubleshooting/known-issues.md`
|
||||
- P6 已知问题补丁:`docs/troubleshooting/known-issues-p6-addendum.md`
|
||||
- 技术债务:`docs/architecture/roadmap/tech-debt.md`
|
||||
1029
docs/architecture/runbooks/post-p6-followup.md
Normal file
1029
docs/architecture/runbooks/post-p6-followup.md
Normal file
File diff suppressed because it is too large
Load Diff
363
docs/standards/cicd-runbook.md
Normal file
363
docs/standards/cicd-runbook.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# CI/CD 使用手册(CI/CD Runbook)
|
||||
|
||||
> 版本:2.0(no-push 本地构建模式)
|
||||
> 日期:2026-07-08
|
||||
> 适用范围:Edu 微服务项目(Gitea Actions + Runner 本地构建部署)
|
||||
> 关联文档:[project_rules §15](../../.trae/rules/project_rules.md)、[本地启动手册](./local-dev-runbook.md)、[多 AI 协作指南](./multi-ai-collaboration.md)
|
||||
> 设计文档:[2026-07-08-cicd-no-push-local-build-design.md](../superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构总览
|
||||
|
||||
```
|
||||
PR 触发(开发 AI 提 PR) push main 触发(协调 AI 合并)
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────┐ ┌─────────────────────────┐
|
||||
│ ci.yml │ │ ci.yml │
|
||||
│ quality-ts (node:22) │ │ quality-ts (node:22) │
|
||||
│ quality-go (golang) │ │ quality-go (golang) │
|
||||
│ quality-proto (buf) │ │ quality-proto (buf) │
|
||||
│ (并行,不部署) │ │ │ │
|
||||
└─────────────────────────┘ │ ▼ │
|
||||
│ deploy job │
|
||||
│ (docker:25-git) │
|
||||
│ 挂载 /var/run/docker.sock│
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ docker compose up │
|
||||
│ --build(本地构建) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 健康检查轮询 │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.1 核心特点
|
||||
|
||||
- **单文件管理**:一个 `.github/workflows/ci.yml` 管全部 CI/CD
|
||||
- **no-push 本地构建**:不推送到 registry,构建即部署
|
||||
- **不依赖自建镜像**:全部使用官方镜像(node/golang/buf/docker)
|
||||
- **DooD 模式**:deploy job 容器内挂载宿主机 `/var/run/docker.sock`
|
||||
|
||||
### 1.2 组件清单
|
||||
|
||||
| 组件 | 说明 |
|
||||
| ----------------- | ------------------------------------------- |
|
||||
| **Gitea** | `git.eazygame.cn`,代码托管 + Actions |
|
||||
| **Gitea Actions** | CI 运行器,兼容 GitHub Actions 语法 |
|
||||
| **actrunner** | 跑在服务器上的 runner,标签 `ubuntu-latest` |
|
||||
| **服务器 MySQL** | 已有容器,端口 3306 |
|
||||
| **服务器 Redis** | 已有容器,端口 6379 |
|
||||
|
||||
### 1.3 流水线文件
|
||||
|
||||
| 文件 | 触发 | 作用 |
|
||||
| --------------------------------- | ---------------------------------- | --------------------------------------------- |
|
||||
| `.github/workflows/ci.yml` | PR + push main + workflow_dispatch | quality(3 job 并行)+ deploy(仅 push main) |
|
||||
| `infra/docker-compose.deploy.yml` | deploy job 调用 | 多服务编排(build: 替代 image:) |
|
||||
| `infra/docker-compose.tools.yml` | 手动执行 | 一次性预拉所有 CI 镜像 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 一次性配置
|
||||
|
||||
### 2.1 启用 Gitea Actions + actrunner
|
||||
|
||||
1. 仓库设置 → Actions → 启用
|
||||
2. 安装 actrunner 并注册(标签 `ubuntu-latest`)
|
||||
3. 配置 actrunner 允许挂载 docker.sock:
|
||||
|
||||
```toml
|
||||
# /etc/gitea/act_runner/config.yaml
|
||||
container:
|
||||
valid_volumes:
|
||||
- /var/run/docker.sock
|
||||
```
|
||||
|
||||
### 2.2 预拉所有 CI 镜像(一次性)
|
||||
|
||||
```bash
|
||||
# 在服务器上,进入 Edu 仓库目录
|
||||
cd /path/to/Edu
|
||||
docker compose -f infra/docker-compose.tools.yml pull
|
||||
```
|
||||
|
||||
拉取的镜像清单:
|
||||
|
||||
| 镜像 | 用途 |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| `node:22-alpine` | CI quality-ts job |
|
||||
| `golang:1.22-alpine` | CI quality-go job + api-gateway builder |
|
||||
| `bufbuild/buf:latest` | CI quality-proto job |
|
||||
| `docker:25-git` | CI deploy job(自带 docker CLI + compose v2 + git) |
|
||||
| `node:20-alpine` | teacher-portal / classes builder |
|
||||
| `alpine:3.20` | api-gateway runner |
|
||||
| `mysql:8.0` | 开发测试用(生产用已有的) |
|
||||
| `redis:7-alpine` | 开发测试用(生产用已有的) |
|
||||
|
||||
### 2.3 准备服务器网络
|
||||
|
||||
```bash
|
||||
# 创建共享网络(若不存在)
|
||||
docker network create edu-shared
|
||||
|
||||
# 将已有的 MySQL/Redis 加入网络(容器名按实际替换)
|
||||
docker network connect edu-shared <实际mysql容器名>
|
||||
docker network connect edu-shared <实际redis容器名>
|
||||
```
|
||||
|
||||
### 2.4 初始化部署目录
|
||||
|
||||
```bash
|
||||
# 1. 创建部署目录
|
||||
sudo mkdir -p /opt/edu
|
||||
sudo chown -R $USER:$USER /opt/edu
|
||||
|
||||
# 2. 创建生产 .env(从模板)
|
||||
cp infra/deploy.env.example /opt/edu/.env
|
||||
vim /opt/edu/.env
|
||||
# 必须修改:
|
||||
# JWT_SECRET=<openssl rand -hex 32 生成的随机值>
|
||||
# DATABASE_URL=mysql://<user>:<pass>@<mysql容器名>:3306/<db>
|
||||
# REDIS_URL=redis://<redis容器名>:6379
|
||||
```
|
||||
|
||||
> 注意:`/opt/edu/repo/` 子目录由 CI 自动同步,不需要手动准备。
|
||||
|
||||
---
|
||||
|
||||
## 3. 日常使用
|
||||
|
||||
### 3.1 开发 AI 提 PR
|
||||
|
||||
```
|
||||
PR 创建 → ci.yml 运行
|
||||
├─ quality-ts(pnpm lint + typecheck + test + build)
|
||||
├─ quality-go(go vet + build + test)
|
||||
└─ quality-proto(buf lint + buf breaking)
|
||||
(3 个 job 并行,仅 quality,不部署)
|
||||
|
||||
CI 全绿 → 协调 AI 审核合并
|
||||
```
|
||||
|
||||
### 3.2 协调 AI 合并 PR
|
||||
|
||||
PR 合并到 main 后自动触发:
|
||||
|
||||
```
|
||||
push main → ci.yml
|
||||
├─ quality-* (3 个 job 并行,确保 main 稳定)
|
||||
└─ deploy job(needs: [quality-ts, quality-go, quality-proto])
|
||||
│
|
||||
▼
|
||||
同步代码到 /opt/edu/repo/
|
||||
│
|
||||
▼
|
||||
docker compose up -d --build(本地构建 3 个服务)
|
||||
│
|
||||
▼
|
||||
健康检查轮询(10 次 × 6 秒)
|
||||
```
|
||||
|
||||
### 3.3 手动触发部署
|
||||
|
||||
在 Gitea → Actions → CI → Run workflow:
|
||||
|
||||
- `commit_sha`(可选):回滚到指定 commit,留空则部署当前 HEAD
|
||||
|
||||
### 3.4 不再支持 tag 发布
|
||||
|
||||
no-push 模式下,镜像不存放到 registry,因此不再使用 `git tag v*` 触发发布。版本管理通过 git commit SHA 追溯。
|
||||
|
||||
---
|
||||
|
||||
## 4. 部署验证
|
||||
|
||||
### 4.1 CI 自动验证
|
||||
|
||||
deploy job 部署后会自动轮询健康检查:
|
||||
|
||||
- `http://localhost:8080/healthz` — api-gateway
|
||||
- `http://localhost:3001/healthz` — classes
|
||||
- `http://localhost:3000/` — teacher-portal
|
||||
|
||||
失败时输出容器状态与日志,方便排查。
|
||||
|
||||
### 4.2 手动验证
|
||||
|
||||
```bash
|
||||
cd /opt/edu
|
||||
|
||||
# 容器状态
|
||||
docker compose ps
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:8080/healthz
|
||||
curl http://localhost:3001/healthz
|
||||
curl http://localhost:3000/
|
||||
|
||||
# 鉴权验证(生产模式 dev-token 应被拒绝)
|
||||
curl -H "Authorization: Bearer dev-token" http://localhost:8080/api/v1/classes
|
||||
# 预期:401 INVALID_TOKEN
|
||||
|
||||
# 查看日志
|
||||
docker compose logs -f api-gateway
|
||||
docker compose logs -f classes
|
||||
docker compose logs -f teacher-portal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 回滚
|
||||
|
||||
### 5.1 git revert(推荐)
|
||||
|
||||
```bash
|
||||
git revert <bad-commit>
|
||||
git push origin main
|
||||
# CI 自动重新构建部署
|
||||
```
|
||||
|
||||
### 5.2 手动触发指定 commit
|
||||
|
||||
在 Gitea → Actions → CI → Run workflow:
|
||||
|
||||
- `commit_sha`:填入上一个稳定版本的 commit SHA
|
||||
|
||||
CI 会 checkout 指定 commit → 本地重新 build → deploy。
|
||||
|
||||
### 5.3 不再支持镜像 tag 回滚
|
||||
|
||||
no-push 模式下没有 registry tag,不能像旧方案那样切换 `IMAGE_TAG`。回滚必须重新构建。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### 6.1 CI: pnpm install 失败
|
||||
|
||||
**症状**:`pnpm install --frozen-lockfile` 报错。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. `pnpm-lock.yaml` 未提交:`git add pnpm-lock.yaml && git commit`
|
||||
2. Node 版本不匹配:确认 CI 用 Node 22,本地一致
|
||||
|
||||
### 6.2 CI: deploy job 无法访问 docker
|
||||
|
||||
**症状**:`docker compose up` 报 `Cannot connect to the Docker daemon`。
|
||||
|
||||
**原因**:actrunner 没有挂载 `/var/run/docker.sock`,或 `valid_volumes` 未配置。
|
||||
|
||||
**修复**:
|
||||
|
||||
```toml
|
||||
# /etc/gitea/act_runner/config.yaml
|
||||
container:
|
||||
valid_volumes:
|
||||
- /var/run/docker.sock
|
||||
```
|
||||
|
||||
重启 actrunner 后重试。
|
||||
|
||||
### 6.3 CD: 健康检查失败
|
||||
|
||||
**症状**:deploy job 健康检查 10 次后失败。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. CI 输出会自动打印容器状态和日志
|
||||
2. 常见原因:
|
||||
- `DATABASE_URL` 连不上 MySQL(检查容器名与网络)
|
||||
- `JWT_SECRET` 未配置
|
||||
- 端口被占用:`docker ps` 检查冲突
|
||||
- `/opt/edu/.env` 不存在或格式错误
|
||||
|
||||
### 6.4 CD: 连不上 MySQL/Redis
|
||||
|
||||
**症状**:classes 容器报 `ECONNREFUSED edu-mysql:3306`。
|
||||
|
||||
**修复**:
|
||||
|
||||
```bash
|
||||
# 1. 确认 MySQL 容器名
|
||||
docker ps --format "{{.Names}}" | grep mysql
|
||||
|
||||
# 2. 确认 MySQL 在 edu-shared 网络
|
||||
docker network inspect edu-shared | grep -A5 Containers
|
||||
|
||||
# 3. 若不在,加入网络
|
||||
docker network connect edu-shared <实际mysql容器名>
|
||||
|
||||
# 4. 修改 /opt/edu/.env 的 DATABASE_URL
|
||||
# DATABASE_URL=mysql://edu:changeme@<实际容器名>:3306/next_edu_cloud
|
||||
```
|
||||
|
||||
### 6.5 Gitea Actions 未触发
|
||||
|
||||
**排查**:
|
||||
|
||||
1. 仓库设置 → Actions → 确认已启用
|
||||
2. Runner 在线:Gitea → 设置 → Actions → Runners
|
||||
3. workflow 文件在 `.github/workflows/` 目录
|
||||
4. `on:` 触发条件匹配
|
||||
|
||||
---
|
||||
|
||||
## 7. 排查命令速查
|
||||
|
||||
```bash
|
||||
# === 在服务器上 ===
|
||||
|
||||
# 查看所有 edu 容器
|
||||
docker compose -f /opt/edu/docker-compose.yml ps
|
||||
|
||||
# 查看实时日志
|
||||
docker compose -f /opt/edu/docker-compose.yml logs -f
|
||||
|
||||
# 重启单个服务
|
||||
docker compose -f /opt/edu/docker-compose.yml restart api-gateway
|
||||
|
||||
# 重新构建并启动(手动触发部署)
|
||||
cd /opt/edu/repo
|
||||
git pull
|
||||
cd /opt/edu
|
||||
docker compose up -d --build
|
||||
|
||||
# 进入容器
|
||||
docker exec -it edu-api-gateway sh
|
||||
docker exec -it edu-classes sh
|
||||
|
||||
# 查看网络
|
||||
docker network inspect edu-shared
|
||||
|
||||
# 查看本地镜像
|
||||
docker images | grep -E "node|golang|docker|alpine|buf"
|
||||
|
||||
# === 在 Gitea Web UI ===
|
||||
# 仓库 → Actions → 查看流水线运行记录
|
||||
# 仓库 → 设置 → Actions → Runners → 查看 Runner 状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全注意事项
|
||||
|
||||
1. **`.env` 文件不入库**:`.gitignore` 已忽略,仅存在于服务器 `/opt/edu/.env`
|
||||
2. **JWT_SECRET 强随机**:生产必须用 `openssl rand -hex 32` 生成
|
||||
3. **DEV_MODE=false**:docker-compose.deploy.yml 强制设为 `false`
|
||||
4. **docker.sock 安全**:actrunner 仅在部署服务器运行,已隔离
|
||||
5. **Runner 隔离**:runner 跑在服务器上,不暴露公网 SSH
|
||||
|
||||
---
|
||||
|
||||
## 9. 相关文档
|
||||
|
||||
- [project_rules §15 CI/CD 规范](../../.trae/rules/project_rules.md)
|
||||
- [project_rules §14 多 AI 协作规范](../../.trae/rules/project_rules.md)
|
||||
- [本地启动手册](./local-dev-runbook.md)
|
||||
- [多 AI 协作指南](./multi-ai-collaboration.md)
|
||||
- [known-issues](../troubleshooting/known-issues.md)
|
||||
- [CI/CD 设计文档](../superpowers/specs/2026-07-08-cicd-no-push-local-build-design.md)
|
||||
@@ -5,7 +5,8 @@
|
||||
> 状态:基线发布
|
||||
> 适用范围:Edu 微服务架构(TS + Go + Python + protobuf)
|
||||
> 关联文档:
|
||||
> - [项目规则](../../project_rules.md)
|
||||
>
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [Git 工作流](./git-workflow.md)
|
||||
> - [架构总览](../architecture/001_architecture_overview.md)
|
||||
@@ -85,7 +86,7 @@ import type { UserEntity } from "@/modules/user/domain/user.entity";
|
||||
|
||||
#### 2.4.1 模块组织
|
||||
|
||||
每个 NestJS 模块对应一个 DDD 聚合,结构见 [project_rules.md §4.1](../../project_rules.md#41-nestjs-业务微服务标准结构)。
|
||||
每个 NestJS 模块对应一个 DDD 聚合,结构见 [project_rules.md §3.3](../../.trae/rules/project_rules.md#33-模块标准结构ddd)。
|
||||
|
||||
#### 2.4.2 装饰器规则
|
||||
|
||||
@@ -111,6 +112,7 @@ export class UserController {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Controller 必须使用 `@Controller(path)` 装饰器,path 使用 kebab-case 复数
|
||||
- Controller 类必须使用 `@RequirePermission()` 装饰器(类级默认权限)
|
||||
- 每个 Handler 可选覆盖类级权限(更细粒度)
|
||||
@@ -131,6 +133,7 @@ export class UserService {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 依赖通过构造函数注入,使用 `readonly` 修饰符
|
||||
- 接口绑定在 Module 的 `providers` 中:`{ provide: "UserRepository", useClass: UserRepoImpl }`
|
||||
- 禁止使用属性注入(`@Inject()` 属性装饰器)
|
||||
@@ -152,6 +155,7 @@ export class UserModule {}
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Module 类名 PascalCase + `Module` 后缀
|
||||
- `exports` 仅暴露 Application Service,不暴露 Repository
|
||||
- 跨 Module 通信通过 exports 的 Service,不直接访问对方 Repository
|
||||
@@ -190,6 +194,7 @@ export class GetUserByIdHandler implements IQueryHandler<GetUserByIdQuery> {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Command 走写路径:Command → Handler → Domain → Repository → MySQL + Outbox
|
||||
- Query 走读路径:Query → Handler → Read Model(禁止查主库)
|
||||
- Command Handler 必须在事务内写 Outbox 表
|
||||
@@ -207,7 +212,12 @@ export class UserEntity {
|
||||
) {}
|
||||
|
||||
static create(props: UserCreateProps): UserEntity {
|
||||
return new UserEntity(crypto.randomUUID(), props.email, props.name, new Date());
|
||||
return new UserEntity(
|
||||
crypto.randomUUID(),
|
||||
props.email,
|
||||
props.name,
|
||||
new Date(),
|
||||
);
|
||||
}
|
||||
|
||||
rename(newName: string): UserRenamedEvent {
|
||||
@@ -218,6 +228,7 @@ export class UserEntity {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Entity 构造函数私有,通过静态工厂方法创建
|
||||
- Entity 字段私有,通过方法变更状态
|
||||
- 状态变更方法返回领域事件,由 Application Service 发布
|
||||
@@ -239,6 +250,7 @@ export class CreateUserDto {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- DTO 类名 `Create[Entity]Dto` / `Update[Entity]Dto` / `[Entity]Response`
|
||||
- DTO 字段使用 `readonly` 修饰
|
||||
- 使用 `class-validator` 装饰器校验
|
||||
@@ -264,13 +276,13 @@ export class CreateUserDto {
|
||||
|
||||
### 2.10 状态管理(沿用 CICD 5 层模型)
|
||||
|
||||
| 层级 | 场景 | 方案 |
|
||||
|------|------|------|
|
||||
| L1 URL | 可分享、可刷新的状态 | nuqs |
|
||||
| L2 Server | 服务端数据 | TanStack Query |
|
||||
| L3 Client Business | 客户端业务状态 | Zustand slice |
|
||||
| L4 Global UI | 全局 UI 状态 | Zustand ui-store + ModalRoot |
|
||||
| L5 Form | 表单状态 | react-hook-form + zodResolver |
|
||||
| 层级 | 场景 | 方案 |
|
||||
| ------------------ | -------------------- | ----------------------------- |
|
||||
| L1 URL | 可分享、可刷新的状态 | nuqs |
|
||||
| L2 Server | 服务端数据 | TanStack Query |
|
||||
| L3 Client Business | 客户端业务状态 | Zustand slice |
|
||||
| L4 Global UI | 全局 UI 状态 | Zustand ui-store + ModalRoot |
|
||||
| L5 Form | 表单状态 | react-hook-form + zodResolver |
|
||||
|
||||
---
|
||||
|
||||
@@ -328,6 +340,7 @@ user, _ := h.userService.GetUser(ctx, id)
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 错误必须显式处理,**禁止 `_ = err`**
|
||||
- 使用 `errors.Is` 和 `errors.As` 判断错误类型,禁止字符串匹配
|
||||
- 自定义错误类型使用 `fmt.Errorf("...: %w", err)` 包装
|
||||
@@ -349,6 +362,7 @@ type UserService struct {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- `context.Context` 作为函数第一个参数传递
|
||||
- **禁止**将 context 存储在结构体字段中
|
||||
- 超时/取消通过 context 传递,禁止使用 `time.Sleep` 等待
|
||||
@@ -368,6 +382,7 @@ if err := g.Wait(); err != nil {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 优先使用 `errgroup` 管理并发 goroutine
|
||||
- 禁止裸 `go func()` 不带 recover 和 context
|
||||
- 共享状态使用 channel 或 `sync` 包,禁止使用 `sync.Mutex` 嵌套锁
|
||||
@@ -388,6 +403,7 @@ func RegisterRoutes(r *gin.Engine, h *Handler, mw *Middleware) {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 路由分组按 API 版本(`/api/v1`)
|
||||
- 中间件链顺序:Recovery → RequestID → Logger → RateLimit → Auth → RequirePermission
|
||||
- Handler 函数签名固定:`func(c *gin.Context)`
|
||||
@@ -404,6 +420,7 @@ logger.Info("user login", "user_id", userID, "ip", ip)
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 使用标准库 `log/slog` 结构化日志
|
||||
- 日志字段使用 kebab-case key
|
||||
- 必须包含 `request_id` 用于链路追踪
|
||||
@@ -459,6 +476,7 @@ def get_user(user_id):
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 所有函数必须标注参数和返回值类型
|
||||
- 使用 `from __future__ import annotations` 启用延迟注解求值
|
||||
- 使用 `Optional[T]` 或 `T | None`(Python 3.10+)标注可选类型
|
||||
@@ -480,6 +498,7 @@ async def fetch_user(user_id: str) -> User:
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- I/O 操作(HTTP、DB、文件)必须使用 async/await
|
||||
- 禁止在 async 函数中调用同步阻塞 I/O,必须用 `asyncio.to_thread` 或 async 客户端
|
||||
- CPU 密集任务用 `asyncio.to_thread` 或进程池
|
||||
@@ -509,6 +528,7 @@ class UserResponse(BaseModel):
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 请求模型命名 `[Action][Entity]Request`,响应模型命名 `[Entity]Response`
|
||||
- 必须使用 `Field` 添加描述、约束
|
||||
- 复杂校验使用 `@field_validator` 或 `@model_validator`
|
||||
@@ -534,6 +554,7 @@ async def get_user(
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 路由分组使用 `APIRouter`,按 API 版本组织
|
||||
- 必须标注 `response_model`
|
||||
- 权限校验通过 `Depends` 注入,每个 endpoint 显式调用 `require_permission`
|
||||
@@ -557,6 +578,7 @@ settings = Settings()
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 配置使用 `pydantic-settings` 的 `BaseSettings`
|
||||
- 环境变量前缀按服务名(`INSIGHT_AI_`)
|
||||
- 禁止在业务代码中直接读取环境变量(`os.getenv`),统一通过 `settings`
|
||||
@@ -567,39 +589,40 @@ settings = Settings()
|
||||
|
||||
### 5.1 命名通用规则
|
||||
|
||||
| 对象 | 风格 | 示例 |
|
||||
|------|------|------|
|
||||
| 目录 | kebab-case | `user-profile/` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 布尔值 | `is/has/can/should` 前缀 | `isVisible`、`is_active`、`hasPermission` |
|
||||
| 类/接口/结构体 | PascalCase | `UserService`、`UserFetcher` |
|
||||
| 服务名 | 小写单数 | `identity`、`teaching` |
|
||||
| Kafka topic | 点分小写 | `edu.identity.user.created` |
|
||||
| protobuf message | PascalCase | `UserCreatedEvent` |
|
||||
| protobuf 字段 | snake_case | `user_id`、`created_at` |
|
||||
| 对象 | 风格 | 示例 |
|
||||
| ---------------- | ------------------------ | ----------------------------------------- |
|
||||
| 目录 | kebab-case | `user-profile/` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 布尔值 | `is/has/can/should` 前缀 | `isVisible`、`is_active`、`hasPermission` |
|
||||
| 类/接口/结构体 | PascalCase | `UserService`、`UserFetcher` |
|
||||
| 服务名 | 小写单数 | `identity`、`teaching` |
|
||||
| Kafka topic | 点分小写 | `edu.identity.user.created` |
|
||||
| protobuf message | PascalCase | `UserCreatedEvent` |
|
||||
| protobuf 字段 | snake_case | `user_id`、`created_at` |
|
||||
|
||||
### 5.2 文件行数通用规则
|
||||
|
||||
| 文件类型 | 建议行数 | 硬性上限 |
|
||||
|---------|---------|---------|
|
||||
| 配置/常量/类型/proto | 无限制 | 无限制 |
|
||||
| React 组件 | ≤ 500 | 800 |
|
||||
| NestJS Controller/Service | ≤ 500 | 800 |
|
||||
| Go handler/middleware | ≤ 400 | 600 |
|
||||
| Python endpoint/service | ≤ 400 | 600 |
|
||||
| 工具函数 | ≤ 40 | - |
|
||||
| 自定义 Hook | ≤ 80 | - |
|
||||
| **任何文件** | - | **1000,超过必须拆分** |
|
||||
| 文件类型 | 建议行数 | 硬性上限 |
|
||||
| ------------------------- | -------- | ---------------------- |
|
||||
| 配置/常量/类型/proto | 无限制 | 无限制 |
|
||||
| React 组件 | ≤ 500 | 800 |
|
||||
| NestJS Controller/Service | ≤ 500 | 800 |
|
||||
| Go handler/middleware | ≤ 400 | 600 |
|
||||
| Python endpoint/service | ≤ 400 | 600 |
|
||||
| 工具函数 | ≤ 40 | - |
|
||||
| 自定义 Hook | ≤ 80 | - |
|
||||
| **任何文件** | - | **1000,超过必须拆分** |
|
||||
|
||||
### 5.3 错误处理通用规则
|
||||
|
||||
| 语言 | 规则 |
|
||||
|------|------|
|
||||
| 语言 | 规则 |
|
||||
| ---------- | -------------------------------------------------------------- |
|
||||
| TypeScript | 错误通过抛出异常,Application Service 必须捕获并转为结构化响应 |
|
||||
| Go | 错误必须显式处理,禁止 `_ = err`,使用 `errors.Is/As` 判断类型 |
|
||||
| Python | 使用异常层次结构,自定义异常继承 `Exception`,禁止裸 `except:` |
|
||||
| Go | 错误必须显式处理,禁止 `_ = err`,使用 `errors.Is/As` 判断类型 |
|
||||
| Python | 使用异常层次结构,自定义异常继承 `Exception`,禁止裸 `except:` |
|
||||
|
||||
**通用规则**:
|
||||
|
||||
- 错误信息对内详细(含上下文、堆栈),对外脱敏(不泄露实现细节)
|
||||
- 错误必须分类:业务错误(4xx)、系统错误(5xx)、依赖错误(502/503)
|
||||
- 错误必须记录日志,包含 request_id 用于链路追踪
|
||||
@@ -607,13 +630,14 @@ settings = Settings()
|
||||
|
||||
### 5.4 日志通用规则
|
||||
|
||||
| 语言 | 工具 | 说明 |
|
||||
|------|------|------|
|
||||
| 语言 | 工具 | 说明 |
|
||||
| ---------- | -------------------- | ---------------- |
|
||||
| TypeScript | NestJS Logger + pino | 结构化 JSON 日志 |
|
||||
| Go | log/slog | 标准库结构化日志 |
|
||||
| Python | structlog 或 loguru | 结构化 JSON 日志 |
|
||||
| Go | log/slog | 标准库结构化日志 |
|
||||
| Python | structlog 或 loguru | 结构化 JSON 日志 |
|
||||
|
||||
**通用规则**:
|
||||
|
||||
- 日志必须结构化(JSON),禁止纯文本
|
||||
- 必须包含 `timestamp`、`level`、`service`、`request_id`、`trace_id`
|
||||
- 日志级别:DEBUG(开发)、INFO(关键业务)、WARN(异常可恢复)、ERROR(系统错误)
|
||||
@@ -622,13 +646,14 @@ settings = Settings()
|
||||
|
||||
### 5.5 测试通用规则
|
||||
|
||||
| 语言 | 单元测试框架 | 覆盖率目标 |
|
||||
|------|------------|-----------|
|
||||
| TypeScript | Vitest + nestjs/testing | ≥ 80% |
|
||||
| Go | 标准 testing 包 + testify | ≥ 80% |
|
||||
| Python | pytest + pytest-asyncio | ≥ 80% |
|
||||
| 语言 | 单元测试框架 | 覆盖率目标 |
|
||||
| ---------- | ------------------------- | ---------- |
|
||||
| TypeScript | Vitest + nestjs/testing | ≥ 80% |
|
||||
| Go | 标准 testing 包 + testify | ≥ 80% |
|
||||
| Python | pytest + pytest-asyncio | ≥ 80% |
|
||||
|
||||
**通用规则**:
|
||||
|
||||
- 测试文件与源文件同目录或 `tests/` 子目录
|
||||
- 命名:`*.test.ts` / `*_test.go` / `test_*.py`
|
||||
- 测试描述说明预期行为("should disable button while loading")
|
||||
@@ -714,6 +739,7 @@ enum UserStatus {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 字段编号禁止复用,删除字段必须 `reserved` 标记
|
||||
- 枚举第一个值必须为 `*_UNSPECIFIED = 0`
|
||||
- 时间使用 `google.protobuf.Timestamp`,不使用 string
|
||||
@@ -770,12 +796,12 @@ buf generate
|
||||
|
||||
### 7.1 令牌分层(沿用 CICD 模型,迁移至微前端共享包)
|
||||
|
||||
| Layer | 位置 | 用途 |
|
||||
|-------|------|------|
|
||||
| Layer 1 Primitive | `packages/ui-tokens/primitive.css` | 原始色板/字号/间距/阴影,业务代码不直接引用 |
|
||||
| Layer 2 Semantic | `packages/ui-tokens/semantic-light.css` + `semantic-dark.css` | 语义令牌,业务代码唯一引用入口 |
|
||||
| 模块命名空间 | `packages/ui-tokens/lesson-preparation.css` | `--lp-*` 令牌,明暗双份 |
|
||||
| Tailwind 暴露 | `packages/ui-tokens/tailwind-theme.css` | `@theme inline` 暴露为 `bg-*`/`text-*`/`font-*` 类 |
|
||||
| Layer | 位置 | 用途 |
|
||||
| ----------------- | ------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| Layer 1 Primitive | `packages/ui-tokens/primitive.css` | 原始色板/字号/间距/阴影,业务代码不直接引用 |
|
||||
| Layer 2 Semantic | `packages/ui-tokens/semantic-light.css` + `semantic-dark.css` | 语义令牌,业务代码唯一引用入口 |
|
||||
| 模块命名空间 | `packages/ui-tokens/lesson-preparation.css` | `--lp-*` 令牌,明暗双份 |
|
||||
| Tailwind 暴露 | `packages/ui-tokens/tailwind-theme.css` | `@theme inline` 暴露为 `bg-*`/`text-*`/`font-*` 类 |
|
||||
|
||||
### 7.2 强制规则
|
||||
|
||||
@@ -846,11 +872,11 @@ buf generate
|
||||
|
||||
### 8.6 依赖扫描
|
||||
|
||||
| 语言 | 工具 |
|
||||
|------|------|
|
||||
| TS | `npm audit` + Snyk + Trivy |
|
||||
| Go | `govulncheck` |
|
||||
| Python | `pip-audit` + `safety` |
|
||||
| 语言 | 工具 |
|
||||
| ------ | -------------------------- |
|
||||
| TS | `npm audit` + Snyk + Trivy |
|
||||
| Go | `govulncheck` |
|
||||
| Python | `pip-audit` + `safety` |
|
||||
|
||||
高危漏洞阻断合并。
|
||||
|
||||
@@ -873,12 +899,12 @@ buf generate
|
||||
|
||||
### 9.1 测试分层
|
||||
|
||||
| 层级 | TS | Go | Python | 覆盖率 |
|
||||
|------|-----|-----|--------|--------|
|
||||
| 单元测试 | Vitest | testing + testify | pytest | ≥ 80% |
|
||||
| 集成测试 | Vitest + Testcontainers | testing + Testcontainers | pytest + Testcontainers | 关键流程 |
|
||||
| E2E 测试 | Playwright | - | - | 核心业务路径 |
|
||||
| 契约测试 | pact + buf | pact + buf | pact + buf | 服务间契约 |
|
||||
| 层级 | TS | Go | Python | 覆盖率 |
|
||||
| -------- | ----------------------- | ------------------------ | ----------------------- | ------------ |
|
||||
| 单元测试 | Vitest | testing + testify | pytest | ≥ 80% |
|
||||
| 集成测试 | Vitest + Testcontainers | testing + Testcontainers | pytest + Testcontainers | 关键流程 |
|
||||
| E2E 测试 | Playwright | - | - | 核心业务路径 |
|
||||
| 契约测试 | pact + buf | pact + buf | pact + buf | 服务间契约 |
|
||||
|
||||
### 9.2 测试命令
|
||||
|
||||
@@ -965,15 +991,15 @@ pytest tests/integration -v
|
||||
|
||||
## 附录:与 CICD 单应用规范的差异
|
||||
|
||||
| 项目 | CICD 单应用 | Edu 微服务 | 原因 |
|
||||
|------|-----------|-----------|------|
|
||||
| 项目结构 | 单 Next.js 应用 | 多语言 monorepo | 微服务拆分 |
|
||||
| 数据获取层 | `modules/[module]/data-access.ts` | NestJS Repository + Domain Entity | DDD 分层 |
|
||||
| 中间件 | `proxy.ts`(Next.js 16) | Go Gin Gateway | 网关独立 |
|
||||
| 通信 | 函数调用 | gRPC + Kafka | 跨进程通信 |
|
||||
| 状态管理 | Zustand + Context + nuqs | 沿用 | 团队熟悉 |
|
||||
| 环境变量校验 | `@t3-oss/env-nextjs` + Zod | TS Zod / Go viper / Python pydantic-settings | 多语言 |
|
||||
| 行数限制 | 单一规范 | 按语言分档 | 各语言惯例 |
|
||||
| 契约 | 无(内部函数调用) | protobuf + buf | 跨服务通信需要 |
|
||||
| 事件驱动 | 无 | Kafka + Outbox | 微服务最终一致 |
|
||||
| 设计令牌 | `src/app/styles/tokens/` | `packages/ui-tokens/` | 微前端共享 |
|
||||
| 项目 | CICD 单应用 | Edu 微服务 | 原因 |
|
||||
| ------------ | --------------------------------- | -------------------------------------------- | -------------- |
|
||||
| 项目结构 | 单 Next.js 应用 | 多语言 monorepo | 微服务拆分 |
|
||||
| 数据获取层 | `modules/[module]/data-access.ts` | NestJS Repository + Domain Entity | DDD 分层 |
|
||||
| 中间件 | `proxy.ts`(Next.js 16) | Go Gin Gateway | 网关独立 |
|
||||
| 通信 | 函数调用 | gRPC + Kafka | 跨进程通信 |
|
||||
| 状态管理 | Zustand + Context + nuqs | 沿用 | 团队熟悉 |
|
||||
| 环境变量校验 | `@t3-oss/env-nextjs` + Zod | TS Zod / Go viper / Python pydantic-settings | 多语言 |
|
||||
| 行数限制 | 单一规范 | 按语言分档 | 各语言惯例 |
|
||||
| 契约 | 无(内部函数调用) | protobuf + buf | 跨服务通信需要 |
|
||||
| 事件驱动 | 无 | Kafka + Outbox | 微服务最终一致 |
|
||||
| 设计令牌 | `src/app/styles/tokens/` | `packages/ui-tokens/` | 微前端共享 |
|
||||
|
||||
281
docs/standards/full-stack-runbook.md
Normal file
281
docs/standards/full-stack-runbook.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# 全链路启用与测试手册(Full Stack Runbook)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> 适用范围:Edu 微服务架构(P6 阶段:11 基础设施 + 10 应用服务 + 1 前端)
|
||||
> 关联文档:[local-dev-runbook](./local-dev-runbook.md)、[004 架构影响地图](../architecture/004_architecture_impact_map.md)、[known-issues](../troubleshooting/known-issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
| 工具 | 版本要求 | 验证命令 |
|
||||
| -------------- | -------- | ------------------------ |
|
||||
| Node.js | ≥ 20 | `node -v` |
|
||||
| pnpm | ≥ 9 | `pnpm -v` |
|
||||
| Go | 1.22+ | `go version` |
|
||||
| uv | 0.4+ | `uv --version` |
|
||||
| Docker | 24+ | `docker version` |
|
||||
| Docker Compose | v2+ | `docker compose version` |
|
||||
|
||||
> Windows 用户:Go 工具链若不在 PATH,临时加入:`$env:Path = "C:\Program Files\Go\bin;" + $env:Path`
|
||||
|
||||
---
|
||||
|
||||
## 2. 服务端口矩阵
|
||||
|
||||
### 2.1 应用服务(10 个)
|
||||
|
||||
| 端口 | 服务 | 语言/框架 | 启动方式 |
|
||||
| ---- | -------------- | --------- | ---------------------------------------------- |
|
||||
| 3000 | teacher-portal | Next.js | `pnpm --filter teacher-portal dev` |
|
||||
| 3001 | classes | NestJS | `pnpm --filter @edu/classes-service dev` |
|
||||
| 3002 | iam | NestJS | `pnpm --filter @edu/iam-service dev` |
|
||||
| 3003 | teacher-bff | NestJS | `pnpm --filter @edu/teacher-bff dev` |
|
||||
| 3004 | core-edu | NestJS | `pnpm --filter @edu/core-edu-service dev` |
|
||||
| 3005 | content | NestJS | `pnpm --filter @edu/content-service dev` |
|
||||
| 3006 | data-ana | FastAPI | `uv run uvicorn data_ana.main:app --port 3006` |
|
||||
| 3007 | msg | NestJS | `pnpm --filter @edu/msg-service dev` |
|
||||
| 3008 | ai | FastAPI | `uv run uvicorn ai.main:app --port 3008` |
|
||||
| 8080 | api-gateway | Go (Gin) | `go run .` |
|
||||
| 8081 | push-gateway | Go (Gin) | `go run .` |
|
||||
|
||||
### 2.2 基础设施(11 个)
|
||||
|
||||
| 端口 | 服务 | 用途 |
|
||||
| --------- | ------------------ | --------------------- |
|
||||
| 3306 | MySQL 8 | 写模型主库 |
|
||||
| 6379 | Redis 7 | 缓存/会话 |
|
||||
| 8083 | Debezium Connect | CDC source connector |
|
||||
| 8123/9000 | ClickHouse 24.3 | 读模型宽表 |
|
||||
| 9092 | Kafka 7.6 | 事件总线 |
|
||||
| 7474/7687 | Neo4j 5.20 | 知识图谱 |
|
||||
| 9200 | Elasticsearch 8.13 | 题库检索 |
|
||||
| 9090 | Prometheus | 指标采集 |
|
||||
| 3030 | Grafana | 可视化(admin/admin) |
|
||||
| 16686 | Jaeger | 分布式追踪 UI |
|
||||
| 4318 | OTLP Collector | OTel span 接收 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 一键启动
|
||||
|
||||
### 3.1 启动基础设施
|
||||
|
||||
```powershell
|
||||
cd e:\Desktop\Edu\infra
|
||||
docker compose -f docker-compose.yml --profile p6 --profile observability up -d
|
||||
```
|
||||
|
||||
等待所有容器 healthy(约 60 秒):
|
||||
|
||||
```powershell
|
||||
docker ps --filter "name=edu-" --format "table {{.Names}}\t{{.Status}}"
|
||||
```
|
||||
|
||||
### 3.2 一键启动所有应用服务
|
||||
|
||||
使用项目根目录的启动脚本:
|
||||
|
||||
```powershell
|
||||
cd e:\Desktop\Edu
|
||||
.\scripts\start-all.ps1
|
||||
```
|
||||
|
||||
该脚本会:
|
||||
|
||||
1. 检查基础设施健康状态
|
||||
2. 为每个应用服务启动独立后台窗口(带标题)
|
||||
3. 自动注入 Python 服务的环境变量(CLICKHOUSE/KAFKA/OTEL)
|
||||
4. 等待所有服务健康检查通过
|
||||
|
||||
### 3.3 健康检查
|
||||
|
||||
```powershell
|
||||
cd e:\Desktop\Edu
|
||||
.\scripts\health-check.ps1
|
||||
```
|
||||
|
||||
预期输出:所有服务 ✅
|
||||
|
||||
---
|
||||
|
||||
## 4. 端到端链路测试
|
||||
|
||||
### 4.1 IAM 注册 + 登录
|
||||
|
||||
```powershell
|
||||
$h = @{Authorization="Bearer dev-token"}
|
||||
|
||||
# 注册
|
||||
$body = @{username="testteacher";password="Test@1234";email="test@edu.com";role="teacher"} | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri "http://localhost:8080/iam/auth/register" -Method Post -Body $body -ContentType "application/json" -Headers $h
|
||||
|
||||
# 登录
|
||||
$loginBody = @{username="testteacher";password="Test@1234"} | ConvertTo-Json
|
||||
$resp = Invoke-RestMethod -Uri "http://localhost:8080/iam/auth/login" -Method Post -Body $loginBody -ContentType "application/json"
|
||||
$token = $resp.data.accessToken
|
||||
Write-Host "Token: $token"
|
||||
```
|
||||
|
||||
### 4.2 Classes CRUD
|
||||
|
||||
```powershell
|
||||
# 创建班级
|
||||
$classBody = @{name="高三一班";gradeId="grade-1";headTeacherId=""} | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri "http://localhost:8080/classes" -Method Post -Body $classBody -ContentType "application/json" -Headers $h
|
||||
|
||||
# 查询班级列表
|
||||
Invoke-RestMethod -Uri "http://localhost:8080/classes" -Method Get -Headers $h
|
||||
```
|
||||
|
||||
### 4.3 CDC 完整链路(MySQL → Debezium → Kafka → data-ana → ClickHouse)
|
||||
|
||||
```powershell
|
||||
# 1. 向 MySQL 插入成绩(触发 binlog)
|
||||
docker exec edu-mysql mysql -uedu -pchangeme next_edu_cloud -e "
|
||||
INSERT INTO core_edu_exams (id, class_id, subject_id, title, exam_date, total_score, created_at, updated_at)
|
||||
VALUES ('exam-cdc-test-001','cls-test-001','sub-math','CDC测试考试',NOW(),100,NOW(),NOW())
|
||||
ON DUPLICATE KEY UPDATE updated_at=NOW();
|
||||
|
||||
INSERT INTO core_edu_grades (id, exam_id, student_id, score, rank_in_class, created_at, updated_at)
|
||||
VALUES ('grade-cdc-001','exam-cdc-test-001','student-cdc-001',92.5,1,NOW(),NOW())
|
||||
ON DUPLICATE KEY UPDATE score=92.5, updated_at=NOW();
|
||||
"
|
||||
|
||||
# 2. 等待 Debezium 捕获 + data-ana 消费
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
# 3. 验证 ClickHouse 已同步
|
||||
docker exec edu-clickhouse clickhouse-client --user default --password clickhouse -q "
|
||||
SELECT student_id, class_id, exam_id, score, last_updated
|
||||
FROM edu_analytics.student_dashboard_view
|
||||
WHERE student_id = 'student-cdc-001'
|
||||
ORDER BY last_updated DESC
|
||||
"
|
||||
# 预期:返回一行,score=92.5,class_id='cls-test-001'
|
||||
```
|
||||
|
||||
### 4.4 data-ana 查询 API
|
||||
|
||||
```powershell
|
||||
# 学生学情看板
|
||||
Invoke-RestMethod -Uri "http://localhost:3006/analytics/student/student-cdc-001/weakness" -Headers $h
|
||||
|
||||
# 班级成绩分析
|
||||
Invoke-RestMethod -Uri "http://localhost:3006/analytics/class/cls-test-001/performance" -Headers $h
|
||||
|
||||
# CDC 消费者状态
|
||||
Invoke-RestMethod -Uri "http://localhost:3006/readyz"
|
||||
# 预期: cdc_consumer = "running"
|
||||
```
|
||||
|
||||
### 4.5 可观测性验证
|
||||
|
||||
| 检查项 | URL | 预期 |
|
||||
| ------------------ | ----------------------------------- | ------------------------ |
|
||||
| Prometheus targets | http://localhost:9090/targets | 所有 target UP |
|
||||
| Grafana 面板 | http://localhost:3030 (admin/admin) | 可登录 |
|
||||
| Jaeger UI | http://localhost:16686 | 可搜索到各 service trace |
|
||||
| data-ana /metrics | http://localhost:3006/metrics | Prometheus 格式输出 |
|
||||
| iam /metrics | http://localhost:3002/metrics | Prometheus 格式输出 |
|
||||
|
||||
**Jaeger trace 验证步骤**:
|
||||
|
||||
1. 打开 http://localhost:16686
|
||||
2. Service 下拉框应能看到 `iam`、`classes`、`data-ana`、`api-gateway` 等
|
||||
3. 选择任一服务 → Find Traces → 应看到 HTTP 请求的自动埋点 span
|
||||
|
||||
---
|
||||
|
||||
## 5. 一键停止
|
||||
|
||||
### 5.1 停止应用服务
|
||||
|
||||
```powershell
|
||||
cd e:\Desktop\Edu
|
||||
.\scripts\stop-all.ps1
|
||||
|
||||
.\scripts\stop-all.ps1 -KillByPort
|
||||
```
|
||||
|
||||
该脚本会关闭所有 `edu-app-*` 标题的终端窗口。
|
||||
|
||||
### 5.2 停止基础设施
|
||||
|
||||
```powershell
|
||||
cd e:\Desktop\Edu\infra
|
||||
docker compose -f docker-compose.yml --profile p6 --profile observability down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 故障排查
|
||||
|
||||
### 6.1 端口占用
|
||||
|
||||
```powershell
|
||||
# 查看占用端口的进程
|
||||
netstat -ano | findstr :3001
|
||||
# 终止进程
|
||||
taskkill /PID <PID> /F
|
||||
```
|
||||
|
||||
### 6.2 基础设施未启动
|
||||
|
||||
```powershell
|
||||
# 检查容器状态
|
||||
docker ps --filter "name=edu-"
|
||||
|
||||
# 重启单个容器
|
||||
docker restart edu-mysql
|
||||
|
||||
# 查看日志
|
||||
docker logs edu-debezium --tail 50
|
||||
```
|
||||
|
||||
### 6.3 CDC 链路断开
|
||||
|
||||
```powershell
|
||||
# 1. 检查 Debezium connector 状态
|
||||
Invoke-RestMethod -Uri "http://localhost:8083/connectors/edu-mysql-source/status"
|
||||
|
||||
# 2. 重启 connector
|
||||
Invoke-RestMethod -Uri "http://localhost:8083/connectors/edu-mysql-source/restart" -Method Post
|
||||
|
||||
# 3. 检查 Kafka topic 是否有数据
|
||||
docker exec edu-kafka kafka-console-consumer --bootstrap-server localhost:9092 --topic edu-cdc.next_edu_cloud.core_edu_grades --from-beginning --max-messages 1
|
||||
|
||||
# 4. 检查 data-ana 消费者日志
|
||||
# 查看 data-ana 终端窗口的 cdc_consumer_started / cdc_event_received 日志
|
||||
```
|
||||
|
||||
### 6.4 OTel trace 未上报
|
||||
|
||||
```powershell
|
||||
# 1. 检查 Jaeger 是否收到 trace
|
||||
Invoke-RestMethod -Uri "http://localhost:16686/api/services"
|
||||
|
||||
# 2. 检查 OTLP endpoint 是否可达
|
||||
Invoke-RestMethod -Uri "http://localhost:4318/v1/traces" -Method Post -ContentType "application/json" -Body "{}"
|
||||
|
||||
# 3. 检查服务环境变量
|
||||
# 确保 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 已设置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 速查:常用命令
|
||||
|
||||
| 场景 | 命令 |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 启动基础设施 | `docker compose -f infra/docker-compose.yml --profile p6 --profile observability up -d` |
|
||||
| 一键启动应用 | `.\scripts\start-all.ps1` |
|
||||
| 一键停止应用 | `.\scripts\stop-all.ps1` |
|
||||
| 健康检查 | `.\scripts\health-check.ps1` |
|
||||
| CDC 链路验证 | `.\scripts\test-cdc.ps1` |
|
||||
| 查看容器状态 | `docker ps --filter "name=edu-"` |
|
||||
| 查看 Debezium 状态 | `Invoke-RestMethod http://localhost:8083/connectors/edu-mysql-source/status` |
|
||||
| ClickHouse 查询 | `docker exec edu-clickhouse clickhouse-client --user default --password clickhouse -q "SELECT * FROM edu_analytics.student_dashboard_view LIMIT 10"` |
|
||||
| Kafka topic 列表 | `docker exec edu-kafka kafka-topics --bootstrap-server localhost:9092 --list` |
|
||||
| Prometheus 查询 | `Invoke-RestMethod "http://localhost:9090/api/v1/query?query=up"` |
|
||||
@@ -1,11 +1,12 @@
|
||||
# Edu Git 工作流规范
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-07
|
||||
> 状态:基线发布
|
||||
> 版本:1.1
|
||||
> 日期:2026-07-08
|
||||
> 状态:基线发布(v1.1:scope-enum 对齐 + CODEOWNERS)
|
||||
> 适用范围:Edu 多语言 monorepo(pnpm workspace + go.work + pyproject.toml)
|
||||
> 关联文档:
|
||||
> - [项目规则](../../project_rules.md)
|
||||
>
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
> - [编码规范](./coding-standards.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [架构总览](../architecture/001_architecture_overview.md)
|
||||
@@ -33,6 +34,7 @@
|
||||
本项目采用**主干开发**模式,所有变更最终合并至 `main` 分支。
|
||||
|
||||
**核心原则**:
|
||||
|
||||
- `main` 分支始终保持可发布状态
|
||||
- 短生命周期特性分支(通常 ≤ 3 天)
|
||||
- 频繁集成,每天至少一次 rebase/merge 至最新 `main`
|
||||
@@ -66,18 +68,19 @@ gitGraph
|
||||
|
||||
### 1.3 分支命名规范
|
||||
|
||||
| 分支类型 | 前缀 | 示例 | 生命周期 |
|
||||
|---------|------|------|---------|
|
||||
| 主干 | `main` | `main` | 永久 |
|
||||
| 特性 | `feat/` | `feat/identity-service` | ≤ 3 天 |
|
||||
| 修复 | `fix/` | `fix/jwt-expiry` | ≤ 1 天 |
|
||||
| 重构 | `refactor/` | `refactor/split-data-access` | ≤ 5 天 |
|
||||
| 性能 | `perf/` | `perf/query-optimization` | ≤ 3 天 |
|
||||
| 文档 | `docs/` | `docs/api-specification` | ≤ 2 天 |
|
||||
| 发布 | `release/v` | `release/v0.3.0` | 发布周期内 |
|
||||
| 热修复 | `hotfix/` | `hotfix/v0.3.1` | ≤ 1 天 |
|
||||
| 分支类型 | 前缀 | 示例 | 生命周期 |
|
||||
| -------- | ----------- | ---------------------------- | ---------- |
|
||||
| 主干 | `main` | `main` | 永久 |
|
||||
| 特性 | `feat/` | `feat/identity-service` | ≤ 3 天 |
|
||||
| 修复 | `fix/` | `fix/jwt-expiry` | ≤ 1 天 |
|
||||
| 重构 | `refactor/` | `refactor/split-data-access` | ≤ 5 天 |
|
||||
| 性能 | `perf/` | `perf/query-optimization` | ≤ 3 天 |
|
||||
| 文档 | `docs/` | `docs/api-specification` | ≤ 2 天 |
|
||||
| 发布 | `release/v` | `release/v0.3.0` | 发布周期内 |
|
||||
| 热修复 | `hotfix/` | `hotfix/v0.3.1` | ≤ 1 天 |
|
||||
|
||||
**规则**:
|
||||
|
||||
- 分支名使用 kebab-case
|
||||
- 一个分支只做一件事,禁止在一个分支内混合多个无关变更
|
||||
- 特性分支命名包含服务/模块名(`feat/identity-service` 而非 `feat/login`)
|
||||
@@ -85,6 +88,7 @@ gitGraph
|
||||
### 1.4 分支保护规则
|
||||
|
||||
**`main` 分支保护**:
|
||||
|
||||
- 禁止直接 push,必须通过 PR
|
||||
- 至少 1 名 Reviewer 审批通过(核心模块需 2 名)
|
||||
- 所有 CI 检查通过(lint + typecheck + test + build)
|
||||
@@ -92,6 +96,7 @@ gitGraph
|
||||
- 禁止 force push
|
||||
|
||||
**`release/*` 分支保护**:
|
||||
|
||||
- 禁止直接 push,仅接受 cherry-pick 或特定 hotfix PR
|
||||
- 至少 2 名 Reviewer 审批
|
||||
- 发布完成后打 tag 并归档
|
||||
@@ -114,25 +119,26 @@ gitGraph
|
||||
|
||||
### 2.2 类型(type)
|
||||
|
||||
| 类型 | 含义 | 是否触发发布 |
|
||||
|------|------|-------------|
|
||||
| `feat` | 新功能 | 是(MINOR) |
|
||||
| `fix` | Bug 修复 | 是(PATCH) |
|
||||
| `perf` | 性能优化 | 是(PATCH) |
|
||||
| `refactor` | 重构(不改变行为) | 否 |
|
||||
| `style` | 代码风格(格式化、空白) | 否 |
|
||||
| `test` | 新增/修改测试 | 否 |
|
||||
| `docs` | 文档变更 | 否 |
|
||||
| `build` | 构建系统或依赖变更 | 否 |
|
||||
| `ci` | CI 配置变更 | 否 |
|
||||
| `chore` | 杂项(不修改 src 或 test) | 否 |
|
||||
| `revert` | 回滚某次提交 | 是 |
|
||||
| 类型 | 含义 | 是否触发发布 |
|
||||
| ---------- | -------------------------- | ------------ |
|
||||
| `feat` | 新功能 | 是(MINOR) |
|
||||
| `fix` | Bug 修复 | 是(PATCH) |
|
||||
| `perf` | 性能优化 | 是(PATCH) |
|
||||
| `refactor` | 重构(不改变行为) | 否 |
|
||||
| `style` | 代码风格(格式化、空白) | 否 |
|
||||
| `test` | 新增/修改测试 | 否 |
|
||||
| `docs` | 文档变更 | 否 |
|
||||
| `build` | 构建系统或依赖变更 | 否 |
|
||||
| `ci` | CI 配置变更 | 否 |
|
||||
| `chore` | 杂项(不修改 src 或 test) | 否 |
|
||||
| `revert` | 回滚某次提交 | 是 |
|
||||
|
||||
### 2.3 范围(scope)
|
||||
|
||||
scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完整清单)。
|
||||
|
||||
**示例**:
|
||||
|
||||
- `feat(identity): 实现用户注册接口`
|
||||
- `fix(gateway): 修复路由匹配优先级`
|
||||
- `perf(teaching): 优化课表查询 N+1 问题`
|
||||
@@ -141,6 +147,7 @@ scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完
|
||||
### 2.4 主题(subject)
|
||||
|
||||
**规则**:
|
||||
|
||||
- 使用中文简短描述
|
||||
- 不超过 50 个字符
|
||||
- 不以句号结尾
|
||||
@@ -150,12 +157,14 @@ scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完
|
||||
### 2.5 正文(body)
|
||||
|
||||
**规则**:
|
||||
|
||||
- 解释"为什么"而非"做了什么"(代码已说明做了什么)
|
||||
- 每行不超过 72 个字符
|
||||
- 使用无序列表列出关键变更点
|
||||
- 涉及 breaking change 必须在正文开头说明
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
feat(teaching): 作业提交支持附件上传
|
||||
|
||||
@@ -195,7 +204,7 @@ pnpm exec husky init
|
||||
|
||||
### 3.2 commitlint 配置
|
||||
|
||||
创建 `commitlint.config.cjs`:
|
||||
实际生效配置文件:仓库根 `.commitlintrc.js`(CommonJS)。
|
||||
|
||||
```javascript
|
||||
/** @type {import('@commitlint/types').UserConfig} */
|
||||
@@ -224,46 +233,42 @@ module.exports = {
|
||||
"type-case": [2, "always", "lower-case"],
|
||||
// type 不能为空
|
||||
"type-empty": [2, "never"],
|
||||
// scope 枚举(见 3.3)
|
||||
// scope 枚举(见 3.3,与 .commitlintrc.js 保持同步)
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
// 业务微服务
|
||||
"identity",
|
||||
"org",
|
||||
"teaching",
|
||||
// 网关层(Go)
|
||||
"api-gateway",
|
||||
"push-gateway",
|
||||
// 业务微服务(NestJS / FastAPI)
|
||||
"iam",
|
||||
"core-edu",
|
||||
"classes",
|
||||
"content",
|
||||
"comm",
|
||||
"insight",
|
||||
// 基础设施服务
|
||||
"auth",
|
||||
"notification",
|
||||
// AI 服务
|
||||
"insight-ai",
|
||||
// 网关
|
||||
"gateway",
|
||||
// BFF
|
||||
"admin-bff",
|
||||
"data-ana",
|
||||
"msg",
|
||||
"ai",
|
||||
// BFF 聚合层(NestJS)
|
||||
"teacher-bff",
|
||||
"student-bff",
|
||||
// 微前端
|
||||
"admin-shell",
|
||||
"teacher-shell",
|
||||
"student-shell",
|
||||
"parent-shell",
|
||||
// 共享包
|
||||
"contracts",
|
||||
"ui-tokens",
|
||||
"ui-components",
|
||||
"parent-bff",
|
||||
// 微前端(Next.js)
|
||||
"teacher-portal",
|
||||
"student-portal",
|
||||
"parent-portal",
|
||||
"admin-portal",
|
||||
// 共享包(packages/)
|
||||
"shared-proto",
|
||||
"shared-ts",
|
||||
// protobuf 契约
|
||||
"proto",
|
||||
// 平台级
|
||||
"deps",
|
||||
"shared-go",
|
||||
"shared-py",
|
||||
"shared-tokens",
|
||||
// 工具与平台级
|
||||
"arch-scan",
|
||||
"infra",
|
||||
"docs",
|
||||
"ci",
|
||||
"chore",
|
||||
"deps",
|
||||
"release",
|
||||
],
|
||||
],
|
||||
@@ -285,100 +290,89 @@ module.exports = {
|
||||
};
|
||||
```
|
||||
|
||||
> **单一事实源**:实际生效的配置在仓库根 `.commitlintrc.js`,本节示例仅作说明。修改 scope 必须同步更新 `.commitlintrc.js` 与本节,并在 PR 中说明原因。
|
||||
|
||||
### 3.3 scope-enum 完整清单
|
||||
|
||||
| 分类 | scope | 说明 |
|
||||
|------|-------|------|
|
||||
| 业务微服务 | `identity` | 身份与权限服务 |
|
||||
| 业务微服务 | `org` | 教学组织服务 |
|
||||
| 业务微服务 | `teaching` | 教学核心服务 |
|
||||
| 业务微服务 | `content` | 内容分析服务 |
|
||||
| 业务微服务 | `comm` | 沟通服务 |
|
||||
| 业务微服务 | `insight` | 智能洞察服务 |
|
||||
| 基础设施 | `auth` | 认证授权服务 |
|
||||
| 基础设施 | `notification` | 通知服务 |
|
||||
| AI 服务 | `insight-ai` | AI 分析服务(Python) |
|
||||
| 网关 | `gateway` | API 网关(Go) |
|
||||
| BFF | `admin-bff` | 管理端 BFF |
|
||||
| BFF | `teacher-bff` | 教师端 BFF |
|
||||
| BFF | `student-bff` | 学生/家长端 BFF |
|
||||
| 微前端 | `admin-shell` | 管理端 Shell |
|
||||
| 微前端 | `teacher-shell` | 教师端 Shell |
|
||||
| 微前端 | `student-shell` | 学生端 Shell |
|
||||
| 微前端 | `parent-shell` | 家长端 Shell |
|
||||
| 共享包 | `contracts` | protobuf 生成契约包 |
|
||||
| 共享包 | `ui-tokens` | 设计令牌包 |
|
||||
| 共享包 | `ui-components` | UI 组件库 |
|
||||
| 共享包 | `shared-ts` | TS 共享工具包 |
|
||||
| 契约 | `proto` | protobuf 定义文件 |
|
||||
| 平台级 | `deps` | 依赖升级 |
|
||||
| 平台级 | `docs` | 平台级文档 |
|
||||
| 平台级 | `ci` | CI/CD 配置 |
|
||||
| 平台级 | `chore` | 杂项 |
|
||||
| 平台级 | `release` | 发布相关 |
|
||||
> 与仓库根 `.commitlintrc.js` 保持同步;修改 scope 必须同时更新此处与 `.commitlintrc.js`。
|
||||
|
||||
| 分类 | scope | 对应目录 | 说明 |
|
||||
| ---------- | ---------------- | ------------------------- | ------------------------------------------------------ |
|
||||
| 网关层 | `api-gateway` | `services/api-gateway/` | API 网关(Go + Gin) |
|
||||
| 网关层 | `push-gateway` | `services/push-gateway/` | WebSocket 推送网关(Go) |
|
||||
| 业务微服务 | `iam` | `services/iam/` | 身份与访问管理(NestJS) |
|
||||
| 业务微服务 | `core-edu` | `services/core-edu/` | 教学核心服务(NestJS,Outbox + Kafka) |
|
||||
| 业务微服务 | `classes` | `services/classes/` | 班级服务(P1 黄金模板,P3 并入 core-edu) |
|
||||
| 业务微服务 | `content` | `services/content/` | 内容资源服务(NestJS + Neo4j) |
|
||||
| 业务微服务 | `data-ana` | `services/data-ana/` | 数据分析服务(Python + FastAPI + ClickHouse) |
|
||||
| 业务微服务 | `msg` | `services/msg/` | 消息通知服务(NestJS + ES) |
|
||||
| 业务微服务 | `ai` | `services/ai/` | AI 网关服务(Python + FastAPI + LLM) |
|
||||
| BFF 聚合层 | `teacher-bff` | `services/teacher-bff/` | 教师端 BFF(NestJS) |
|
||||
| BFF 聚合层 | `student-bff` | `services/student-bff/` | 学生端 BFF(待建立) |
|
||||
| BFF 聚合层 | `parent-bff` | `services/parent-bff/` | 家长端 BFF(待建立) |
|
||||
| 微前端 | `teacher-portal` | `apps/teacher-portal/` | 教师端 Portal(Next.js) |
|
||||
| 微前端 | `student-portal` | `apps/student-portal/` | 学生端 Portal(待建立) |
|
||||
| 微前端 | `parent-portal` | `apps/parent-portal/` | 家长端 Portal(待建立) |
|
||||
| 微前端 | `admin-portal` | `apps/admin-portal/` | 管理端 Portal(待建立) |
|
||||
| 共享包 | `shared-proto` | `packages/shared-proto/` | protobuf 契约定义 |
|
||||
| 共享包 | `shared-ts` | `packages/shared-ts/` | TS 共享类型与工具(待建立) |
|
||||
| 共享包 | `shared-go` | `packages/shared-go/` | Go 共享工具(待建立) |
|
||||
| 共享包 | `shared-py` | `packages/shared-py/` | Python 共享工具(待建立) |
|
||||
| 共享包 | `shared-tokens` | `packages/shared-tokens/` | 设计令牌(待建立) |
|
||||
| 工具 | `arch-scan` | `scripts/arch-scan/` | 架构元数据库扫描器 |
|
||||
| 平台级 | `infra` | `infra/` | 基础设施(K8s / docker-compose / backup / monitoring) |
|
||||
| 平台级 | `docs` | `docs/` | 平台级文档(跨多模块) |
|
||||
| 平台级 | `deps` | - | 依赖升级 |
|
||||
| 平台级 | `release` | - | 发布相关 |
|
||||
|
||||
> `chore` / `ci` / `build` 等 Conventional Commits 标准 type 不需要 scope,可直接使用 `chore: xxx`、`ci: xxx`。
|
||||
|
||||
### 3.4 husky hooks
|
||||
|
||||
实际生效的 hook 文件在仓库根 `.husky/` 目录。使用 `npx --no-install` 确保使用本地依赖。
|
||||
|
||||
`.husky/commit-msg`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
pnpm exec commitlint --edit "$1"
|
||||
npx --no-install commitlint --edit $1
|
||||
```
|
||||
|
||||
`.husky/pre-commit`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
pnpm exec lint-staged
|
||||
npx --no-install lint-staged
|
||||
```
|
||||
|
||||
`.husky/pre-push`:
|
||||
`.husky/pre-push`(推送前类型检查,TS 服务 typecheck 通过后才允许推送):
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
# 推送前运行类型检查
|
||||
pnpm -r run typecheck
|
||||
# 推送前运行类型检查(TS 服务)
|
||||
pnpm -r run typecheck 2>/dev/null || echo "[pre-push] typecheck 跳过或不可用"
|
||||
# Go 服务编译检查
|
||||
for d in services/api-gateway services/push-gateway; do
|
||||
if [ -d "$d" ]; then (cd "$d" && go build ./... ) || exit 1; fi
|
||||
done
|
||||
```
|
||||
|
||||
> **注意**:`pre-push` 中的 typecheck 在 TS 服务 ESLint 9 flat config 迁移完成前为可选(当前 `pnpm -r run typecheck` 缺失脚本,会 fallback 到 echo 提示)。Go `go build` 检查必须通过。
|
||||
|
||||
### 3.5 lint-staged 配置
|
||||
|
||||
`package.json`(根目录):
|
||||
实际生效配置文件:仓库根 `lint-staged.config.js`(CommonJS)。
|
||||
|
||||
```json
|
||||
{
|
||||
"lint-staged": {
|
||||
// TypeScript / NestJS / Next.js
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
// Go
|
||||
"*.go": [
|
||||
"gofmt -w",
|
||||
"golangci-lint run --fix"
|
||||
],
|
||||
// Python
|
||||
"*.py": [
|
||||
"ruff check --fix",
|
||||
"ruff format"
|
||||
],
|
||||
// protobuf
|
||||
"*.proto": [
|
||||
"buf format --write"
|
||||
],
|
||||
// Markdown
|
||||
"*.md": [
|
||||
"prettier --write"
|
||||
],
|
||||
// JSON / YAML
|
||||
"*.{json,yaml,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
```javascript
|
||||
module.exports = {
|
||||
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||
"*.{go,mod,sum}": ["gofmt -w", "golangci-lint run --fix"],
|
||||
"*.{py}": ["ruff check --fix", "ruff format"],
|
||||
"*.proto": ["buf format --write"],
|
||||
"*.md": ["prettier --write"],
|
||||
};
|
||||
```
|
||||
|
||||
> JSON / YAML 文件由 prettier 在 `*.md` 规则外按全局配置处理,如需显式规则可在 `lint-staged.config.js` 追加 `'*.{json,yaml,yml}': ['prettier --write']`。
|
||||
|
||||
---
|
||||
|
||||
## 四、PR 与 Code Review
|
||||
@@ -435,6 +429,7 @@ feat(identity): 实现用户注册接口
|
||||
## 影响范围
|
||||
|
||||
<!-- 列出受影响的服务/包 -->
|
||||
|
||||
- 服务:
|
||||
- 包:
|
||||
- 数据库迁移:是 / 否
|
||||
@@ -467,18 +462,19 @@ Closes #
|
||||
|
||||
### 4.4 Reviewer 要求
|
||||
|
||||
| 变更类型 | 最少 Reviewer | 备注 |
|
||||
|---------|--------------|------|
|
||||
| 普通业务变更 | 1 | 默认 |
|
||||
| 跨服务变更 | 2 | 涉及 ≥ 2 个服务 |
|
||||
| protobuf 契约变更 | 2 | 需包含架构组成员 |
|
||||
| 数据库 schema 变更 | 2 | 需包含 DBA 或架构组 |
|
||||
| 安全相关变更 | 2 | 需包含安全负责人 |
|
||||
| 核心模块(auth/identity) | 2 | 核心模块强制 2 人 |
|
||||
| 变更类型 | 最少 Reviewer | 备注 |
|
||||
| ------------------------- | ------------- | ------------------- |
|
||||
| 普通业务变更 | 1 | 默认 |
|
||||
| 跨服务变更 | 2 | 涉及 ≥ 2 个服务 |
|
||||
| protobuf 契约变更 | 2 | 需包含架构组成员 |
|
||||
| 数据库 schema 变更 | 2 | 需包含 DBA 或架构组 |
|
||||
| 安全相关变更 | 2 | 需包含安全负责人 |
|
||||
| 核心模块(auth/identity) | 2 | 核心模块强制 2 人 |
|
||||
|
||||
### 4.5 Code Review 清单
|
||||
|
||||
**通用检查**:
|
||||
|
||||
- [ ] 代码是否符合 [编码规范](./coding-standards.md)
|
||||
- [ ] 是否有明显的逻辑错误
|
||||
- [ ] 错误处理是否完整(不忽略 error/err/exception)
|
||||
@@ -486,18 +482,21 @@ Closes #
|
||||
- [ ] 是否存在硬编码的密钥、token、连接字符串
|
||||
|
||||
**架构检查**:
|
||||
|
||||
- [ ] 是否违反限界上下文边界(跨服务直接查 DB)
|
||||
- [ ] 是否违反依赖方向(shared 反向依赖 services)
|
||||
- [ ] protobuf 变更是否向后兼容
|
||||
- [ ] 事件 schema 变更是否向后兼容
|
||||
|
||||
**性能检查**:
|
||||
|
||||
- [ ] 是否有 N+1 查询
|
||||
- [ ] 是否有未加索引的查询
|
||||
- [ ] 是否有不必要的大对象拷贝
|
||||
- [ ] 是否有阻塞事件循环的同步操作(Python/Node)
|
||||
|
||||
**安全检查**:
|
||||
|
||||
- [ ] 所有入口是否经过权限校验
|
||||
- [ ] 用户输入是否经过验证
|
||||
- [ ] SQL 是否使用参数化查询
|
||||
@@ -506,37 +505,80 @@ Closes #
|
||||
### 4.6 合并策略
|
||||
|
||||
**默认使用 Squash Merge**:
|
||||
|
||||
- 保留 PR 的完整变更作为一个 commit
|
||||
- commit message 使用 PR 标题
|
||||
- 删除特性分支
|
||||
|
||||
**禁止使用 Merge Commit**(除非是发布分支合并回 main):
|
||||
|
||||
- 避免历史中充斥 "Merge branch" 噪音
|
||||
- 保持线性历史
|
||||
|
||||
**Rebase Merge**:
|
||||
|
||||
- 仅用于需要保留多个有意义 commit 的特性分支
|
||||
- 需在 PR 中说明原因
|
||||
|
||||
### 4.7 模块 Owner 与 CODEOWNERS
|
||||
|
||||
**实际生效文件**:仓库根 `.github/CODEOWNERS`(GitHub/Gitea 原生支持,自动为 PR 分配 reviewer)。
|
||||
|
||||
**设计原则**:
|
||||
|
||||
- 每个模块至少 1 名 owner,核心模块 2 名
|
||||
- 跨模块变更(如 proto 契约、arch.db、project_rules)由架构组 review
|
||||
- 基础设施变更(K8s/Helm/backup)由 SRE review
|
||||
- owner 名单变更需走 PR,由架构组审批
|
||||
|
||||
**模块 Owner 分配矩阵**:
|
||||
|
||||
| 模块分类 | 路径 | Owner Team | 最少 Reviewer | 备注 |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------- | --------------------- |
|
||||
| 架构与规则 | `.trae/rules/`、`docs/architecture/`、`docs/standards/` | `@edu-platform/arch` | 2 | 架构组强制 review |
|
||||
| 架构工具 | `scripts/arch-scan/` | `@edu-platform/arch` | 1 | |
|
||||
| 根配置 | `.commitlintrc.js`、`lint-staged.config.js`、`package.json`、`pnpm-workspace.yaml`、`go.work`、`pyproject.toml`、`tsconfig.base.json` | `@edu-platform/arch` | 2 | 影响全局 |
|
||||
| 共享包 | `packages/shared-proto/` | `@edu-platform/arch` | 2 | 契约变更影响所有服务 |
|
||||
| 网关层 | `services/api-gateway/`、`services/push-gateway/` | `@edu-platform/gateway` | 1 | |
|
||||
| IAM 服务 | `services/iam/` | `@edu-platform/iam` | 2 | 核心模块强制 2 人 |
|
||||
| 教学核心 | `services/core-edu/`、`services/classes/` | `@edu-platform/edu-core` | 1 | |
|
||||
| 内容资源 | `services/content/` | `@edu-platform/content` | 1 | |
|
||||
| 消息通知 | `services/msg/` | `@edu-platform/messaging` | 1 | |
|
||||
| 数据分析 | `services/data-ana/` | `@edu-platform/data` | 1 | |
|
||||
| AI 服务 | `services/ai/` | `@edu-platform/ai` | 1 | |
|
||||
| BFF 层 | `services/teacher-bff/`、`services/student-bff/`、`services/parent-bff/` | `@edu-platform/edu-core` | 1 | |
|
||||
| 微前端 | `apps/teacher-portal/`、`apps/student-portal/`、`apps/parent-portal/`、`apps/admin-portal/` | `@edu-platform/frontend` | 1 | |
|
||||
| 基础设施 | `infra/k8s/`、`infra/backup/`、`infra/security/`、`infra/monitoring/` | `@edu-platform/sre` | 2 | 生产环境变更强制 2 人 |
|
||||
| CI/CD | `.github/`、`.husky/` | `@edu-platform/sre` | 1 | |
|
||||
| 文档 | `docs/troubleshooting/`、`docs/standards/` | `@edu-platform/arch` | 1 | known-issues 更新 |
|
||||
|
||||
> **Team handle 占位符**:上表 `@edu-platform/*` 为 team handle 模板。实际团队 handle 需在 GitHub/Gitea Organization 中创建对应 team 后,同步更新 `.github/CODEOWNERS`。
|
||||
|
||||
**CODEOWNERS 文件维护规则**:
|
||||
|
||||
1. 新增服务/包时,必须在同一 PR 中更新 `.github/CODEOWNERS`
|
||||
2. owner 变更(人员调动)需开独立 PR,由架构组审批
|
||||
3. CODEOWNERS 与本节表格保持同步,单一事实源为 `.github/CODEOWNERS` 文件
|
||||
|
||||
---
|
||||
|
||||
## 五、文档同步规则
|
||||
|
||||
### 5.1 文档同步矩阵
|
||||
|
||||
| 代码变更类型 | 需同步的文档 | 同步时机 |
|
||||
|-------------|-------------|---------|
|
||||
| 新增/删除服务 | `001_architecture_overview.md` + 服务 README | PR 内同步 |
|
||||
| 新增/删除模块 | 服务 README + `arch:scan` | PR 内同步 |
|
||||
| 新增/删除导出函数 | `pnpm run arch:scan` | 提交前 |
|
||||
| 修改函数签名 | `pnpm run arch:scan` | 提交前 |
|
||||
| 修改权限点 | `project_rules.md` + 权限文档 | PR 内同步 |
|
||||
| 新增/删除数据库表 | 架构文档 + 服务 README | PR 内同步 |
|
||||
| 新增/删除路由 | 服务 README + OpenAPI | PR 内同步 |
|
||||
| 修改模块间依赖 | `arch:scan` + 架构文档 | PR 内同步 |
|
||||
| 新增 protobuf message | `proto/` README + `arch:scan` | PR 内同步 |
|
||||
| 新增 Kafka topic | 架构文档 + 服务 README | PR 内同步 |
|
||||
| 遇到新问题/经验 | `known-issues.md` | PR 内同步 |
|
||||
| 代码变更类型 | 需同步的文档 | 同步时机 |
|
||||
| --------------------- | -------------------------------------------- | --------- |
|
||||
| 新增/删除服务 | `001_architecture_overview.md` + 服务 README | PR 内同步 |
|
||||
| 新增/删除模块 | 服务 README + `arch:scan` | PR 内同步 |
|
||||
| 新增/删除导出函数 | `pnpm run arch:scan` | 提交前 |
|
||||
| 修改函数签名 | `pnpm run arch:scan` | 提交前 |
|
||||
| 修改权限点 | `project_rules.md` + 权限文档 | PR 内同步 |
|
||||
| 新增/删除数据库表 | 架构文档 + 服务 README | PR 内同步 |
|
||||
| 新增/删除路由 | 服务 README + OpenAPI | PR 内同步 |
|
||||
| 修改模块间依赖 | `arch:scan` + 架构文档 | PR 内同步 |
|
||||
| 新增 protobuf message | `proto/` README + `arch:scan` | PR 内同步 |
|
||||
| 新增 Kafka topic | 架构文档 + 服务 README | PR 内同步 |
|
||||
| 遇到新问题/经验 | `known-issues.md` | PR 内同步 |
|
||||
|
||||
### 5.2 arch.db 同步规则
|
||||
|
||||
@@ -551,6 +593,7 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
```
|
||||
|
||||
**违规检查**:
|
||||
|
||||
- 长文件(> 1000 行)
|
||||
- 未校验权限的 Handler
|
||||
- 循环依赖
|
||||
@@ -568,26 +611,33 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
> 技术栈:[语言 + 框架]
|
||||
|
||||
## 职责
|
||||
|
||||
[一段话描述]
|
||||
|
||||
## 架构
|
||||
|
||||
[mermaid 架构图]
|
||||
|
||||
## 核心流程
|
||||
|
||||
[mermaid 时序图]
|
||||
|
||||
## 目录结构
|
||||
|
||||
[树形结构 + 说明]
|
||||
|
||||
## 依赖
|
||||
|
||||
- 上游服务:[列表]
|
||||
- 下游服务:[列表]
|
||||
- 共享包:[列表]
|
||||
|
||||
## 约束
|
||||
|
||||
[业务规则、技术约束]
|
||||
|
||||
## 架构决策
|
||||
|
||||
[关键设计决策记录]
|
||||
```
|
||||
|
||||
@@ -598,12 +648,13 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
```markdown
|
||||
### X.X 主题分区
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
|------|----------|
|
||||
| 场景 | 技术/规则 |
|
||||
| -------- | ------------------ |
|
||||
| 简述场景 | 正确做法(一句话) |
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 索引式:场景→技术/规则映射
|
||||
- 不写代码示例和错误示范
|
||||
- 同类问题在原条目补充,不重复创建
|
||||
@@ -618,6 +669,7 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
**规则**:一个 commit 只涉及一个服务或一个包的变更。
|
||||
|
||||
**原因**:
|
||||
|
||||
- 便于回滚(按服务粒度回滚)
|
||||
- 便于追踪(changelog 清晰)
|
||||
- 便于 review(聚焦单一职责)
|
||||
@@ -653,12 +705,14 @@ git commit -m "build(contracts): 重新生成 proto 代码"
|
||||
### 6.4 依赖升级规则
|
||||
|
||||
**规则**:
|
||||
|
||||
- 依赖升级使用 `build(deps):` 类型
|
||||
- 必须说明升级原因(安全、功能、兼容性)
|
||||
- 安全漏洞修复必须包含 CVE 编号
|
||||
- 大版本升级需单独 PR 并完整测试
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
build(deps): 升级 nestjs 至 10.3.0
|
||||
|
||||
@@ -691,33 +745,33 @@ git commit -m "feat(notification): 适配 UserRegistered v2 事件"
|
||||
|
||||
本项目采用**双层版本号**:
|
||||
|
||||
| 层级 | 格式 | 说明 |
|
||||
|------|------|------|
|
||||
| 层级 | 格式 | 说明 |
|
||||
| -------- | ------------------------ | ----------------------------------------- |
|
||||
| 平台版本 | `v{阶段}.{迭代}.{patch}` | 如 `v0.3.1`(P3 阶段第 1 次迭代 patch 1) |
|
||||
| 服务版本 | `{service}:{semver}` | 如 `identity:1.2.0` |
|
||||
| 服务版本 | `{service}:{semver}` | 如 `identity:1.2.0` |
|
||||
|
||||
### 7.2 阶段版本范围
|
||||
|
||||
| 阶段 | 平台版本范围 | 说明 |
|
||||
|------|-------------|------|
|
||||
| P1 地基 | `v0.1.x` | monorepo 初始化、CI/CD、arch.db |
|
||||
| P2 身份 | `v0.2.x` | identity + auth + notification |
|
||||
| P3 核心教学 | `v0.3.x` | org + teaching + content |
|
||||
| P4 内容分析 | `v0.4.x` | insight + CQRS 读模型 |
|
||||
| P5 沟通AI | `v0.5.x` | comm + AI 增强 |
|
||||
| P6 硬化 | `v1.0.x` | 正式发布版 |
|
||||
| 阶段 | 平台版本范围 | 说明 |
|
||||
| ----------- | ------------ | ------------------------------- |
|
||||
| P1 地基 | `v0.1.x` | monorepo 初始化、CI/CD、arch.db |
|
||||
| P2 身份 | `v0.2.x` | identity + auth + notification |
|
||||
| P3 核心教学 | `v0.3.x` | org + teaching + content |
|
||||
| P4 内容分析 | `v0.4.x` | insight + CQRS 读模型 |
|
||||
| P5 沟通AI | `v0.5.x` | comm + AI 增强 |
|
||||
| P6 硬化 | `v1.0.x` | 正式发布版 |
|
||||
|
||||
### 7.3 Docker 镜像标签
|
||||
|
||||
**格式**:`{registry}/edu/{service}:{tag}`
|
||||
|
||||
| tag 类型 | 格式 | 示例 | 用途 |
|
||||
|---------|------|------|------|
|
||||
| 版本号 | `v{version}` | `v0.3.1` | 正式发布 |
|
||||
| 服务版本 | `{service}-{semver}` | `identity-1.2.0` | 服务独立版本 |
|
||||
| Git SHA | `sha-{short}` | `sha-a1b2c3d` | 精确追溯 |
|
||||
| 最新 | `latest` | `latest` | 开发环境 |
|
||||
| 阶段 | `{stage}-{sha}` | `staging-a1b2c3d` | 阶段环境 |
|
||||
| tag 类型 | 格式 | 示例 | 用途 |
|
||||
| -------- | -------------------- | ----------------- | ------------ |
|
||||
| 版本号 | `v{version}` | `v0.3.1` | 正式发布 |
|
||||
| 服务版本 | `{service}-{semver}` | `identity-1.2.0` | 服务独立版本 |
|
||||
| Git SHA | `sha-{short}` | `sha-a1b2c3d` | 精确追溯 |
|
||||
| 最新 | `latest` | `latest` | 开发环境 |
|
||||
| 阶段 | `{stage}-{sha}` | `staging-a1b2c3d` | 阶段环境 |
|
||||
|
||||
### 7.4 发布流程
|
||||
|
||||
@@ -752,19 +806,23 @@ flowchart TD
|
||||
## [v0.3.0] - 2026-08-15
|
||||
|
||||
### Added
|
||||
|
||||
- 教学核心服务新增作业管理功能
|
||||
- 内容服务支持题库导入
|
||||
- 教师端 Shell 新增作业批改界面
|
||||
|
||||
### Changed
|
||||
|
||||
- identity 服务升级至 NestJS 10.3
|
||||
- gateway 路由匹配算法优化
|
||||
|
||||
### Fixed
|
||||
|
||||
- 修复 JWT 刷新 token 过期判断错误
|
||||
- 修复课表查询时区问题
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- UserRegistered 事件 schema 变更至 v2,消费方需升级
|
||||
```
|
||||
|
||||
@@ -778,6 +836,7 @@ flowchart TD
|
||||
```
|
||||
|
||||
**版本号升级规则**:
|
||||
|
||||
- **MAJOR**:Breaking Change(protobuf 不兼容变更、API 破坏性修改)
|
||||
- **MINOR**:新增功能,向后兼容
|
||||
- **PATCH**:Bug 修复,向后兼容
|
||||
@@ -788,13 +847,13 @@ flowchart TD
|
||||
|
||||
### 8.1 回滚策略
|
||||
|
||||
| 场景 | 回滚方式 | 耗时 |
|
||||
|------|---------|------|
|
||||
| 代码缺陷 | `git revert` + 重新部署 | 5-10 分钟 |
|
||||
| 镜像问题 | `kubectl rollout undo` | 1-2 分钟 |
|
||||
| 数据库迁移问题 | 执行迁移 down 脚本 | 5-30 分钟 |
|
||||
| 配置错误 | 回滚 ConfigMap/Secret | 1-2 分钟 |
|
||||
| 全站故障 | 回滚至上一稳定 tag | 10-30 分钟 |
|
||||
| 场景 | 回滚方式 | 耗时 |
|
||||
| -------------- | ----------------------- | ---------- |
|
||||
| 代码缺陷 | `git revert` + 重新部署 | 5-10 分钟 |
|
||||
| 镜像问题 | `kubectl rollout undo` | 1-2 分钟 |
|
||||
| 数据库迁移问题 | 执行迁移 down 脚本 | 5-30 分钟 |
|
||||
| 配置错误 | 回滚 ConfigMap/Secret | 1-2 分钟 |
|
||||
| 全站故障 | 回滚至上一稳定 tag | 10-30 分钟 |
|
||||
|
||||
### 8.2 代码回滚
|
||||
|
||||
@@ -833,6 +892,7 @@ kubectl rollout status deployment/identity -n edu-prod
|
||||
### 8.4 数据库迁移回滚
|
||||
|
||||
**规则**:
|
||||
|
||||
- 所有迁移必须提供 `up` 和 `down` 脚本
|
||||
- `down` 脚本必须在 CI 中测试
|
||||
- 回滚前必须备份生产数据
|
||||
@@ -865,6 +925,7 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
> 严重等级:P0/P1/P2/P3
|
||||
|
||||
## 时间线
|
||||
|
||||
- HH:MM 告警触发
|
||||
- HH:MM 确认问题
|
||||
- HH:MM 决定回滚
|
||||
@@ -872,15 +933,19 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
- HH:MM 服务恢复
|
||||
|
||||
## 影响分析
|
||||
|
||||
[受影响的功能、用户数、业务损失]
|
||||
|
||||
## 根本原因
|
||||
|
||||
[技术原因 + 流程原因]
|
||||
|
||||
## 回滚过程
|
||||
|
||||
[执行的操作]
|
||||
|
||||
## 改进措施
|
||||
|
||||
- [ ] 短期:[立即修复项]
|
||||
- [ ] 中期:[流程改进项]
|
||||
- [ ] 长期:[架构改进项]
|
||||
@@ -890,29 +955,30 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
|
||||
## 九、附录:CICD 与 Edu Git 工作流差异
|
||||
|
||||
| 维度 | CICD(Next.js 单应用) | Edu(微服务 monorepo) |
|
||||
|------|----------------------|----------------------|
|
||||
| 仓库结构 | 单一 Next.js 应用 | 多语言 monorepo(pnpm + go.work + uv) |
|
||||
| 分支策略 | trunk-based | trunk-based(沿用) |
|
||||
| 提交规范 | Conventional Commits | Conventional Commits(沿用,scope 扩展至服务/包) |
|
||||
| scope 范围 | 模块名(如 `exams`、`homework`) | 服务/包名(如 `identity`、`contracts`) |
|
||||
| commitlint scope-enum | 35 个模块 | 27 个服务/包 |
|
||||
| PR Reviewer | 1 人 | 1-2 人(核心模块/跨服务 2 人) |
|
||||
| 合并策略 | Squash Merge | Squash Merge(沿用) |
|
||||
| 版本号 | 单一应用版本 | 双层(平台版本 + 服务独立版本) |
|
||||
| 发布粒度 | 整体发布 | 按服务独立发布 |
|
||||
| Docker 镜像 | 单一镜像 | 每服务一镜像 |
|
||||
| 回滚粒度 | 整体回滚 | 按服务回滚 |
|
||||
| 数据库迁移 | Drizzle 单库 | 每服务独立库 + 独立迁移 |
|
||||
| 文档同步 | `npm run arch:scan` | `pnpm run arch:scan`(多语言扫描) |
|
||||
| CI 检查 | lint + tsc + test | lint + typecheck + test(按语言分别执行) |
|
||||
| 紧急回滚 | `git revert` + 重新部署 | `git revert` + `kubectl rollout undo` |
|
||||
| 事故复盘 | known-issues.md | `incidents/` 目录独立记录 |
|
||||
| 维度 | CICD(Next.js 单应用) | Edu(微服务 monorepo) |
|
||||
| --------------------- | -------------------------------- | ---------------------------------------------------------------- |
|
||||
| 仓库结构 | 单一 Next.js 应用 | 多语言 monorepo(pnpm + go.work + uv) |
|
||||
| 分支策略 | trunk-based | trunk-based(沿用) |
|
||||
| 提交规范 | Conventional Commits | Conventional Commits(沿用,scope 扩展至服务/包) |
|
||||
| scope 范围 | 模块名(如 `exams`、`homework`) | 服务/包名(如 `iam`、`api-gateway`、`shared-proto`) |
|
||||
| commitlint scope-enum | 35 个模块 | 26 个 scope(含服务/包/工具/平台级,与 `.commitlintrc.js` 同步) |
|
||||
| PR Reviewer | 1 人 | 1-2 人(核心模块/跨服务 2 人) |
|
||||
| 合并策略 | Squash Merge | Squash Merge(沿用) |
|
||||
| 版本号 | 单一应用版本 | 双层(平台版本 + 服务独立版本) |
|
||||
| 发布粒度 | 整体发布 | 按服务独立发布 |
|
||||
| Docker 镜像 | 单一镜像 | 每服务一镜像 |
|
||||
| 回滚粒度 | 整体回滚 | 按服务回滚 |
|
||||
| 数据库迁移 | Drizzle 单库 | 每服务独立库 + 独立迁移 |
|
||||
| 文档同步 | `npm run arch:scan` | `pnpm run arch:scan`(多语言扫描) |
|
||||
| CI 检查 | lint + tsc + test | lint + typecheck + test(按语言分别执行) |
|
||||
| 紧急回滚 | `git revert` + 重新部署 | `git revert` + `kubectl rollout undo` |
|
||||
| 事故复盘 | known-issues.md | `incidents/` 目录独立记录 |
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| 1.0 | 2026-07-07 | 基线发布,从 CICD 单应用规范迁移至微服务多语言 monorepo |
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
| ---- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1.0 | 2026-07-07 | 基线发布,从 CICD 单应用规范迁移至微服务多语言 monorepo |
|
||||
| 1.1 | 2026-07-08 | scope-enum 对齐实际服务名(identity→iam、teaching→core-edu 等);新增 §4.7 模块 Owner 与 CODEOWNERS;husky hooks 与实际文件对齐;新增 pre-push hook 说明 |
|
||||
|
||||
333
docs/standards/local-dev-runbook.md
Normal file
333
docs/standards/local-dev-runbook.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# 本地启动手册(Local Dev Runbook)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-08
|
||||
> 适用范围:Edu 微服务(P1 阶段:api-gateway + classes + teacher-portal)
|
||||
> 关联文档:[project_rules](../../.trae/rules/project_rules.md)、[004 架构影响地图](../architecture/004_architecture_impact_map.md)、[known-issues](../troubleshooting/known-issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 环境依赖
|
||||
|
||||
| 工具 | 版本要求 | 验证命令 | 说明 |
|
||||
| -------------- | -------- | ------------------------ | -------------------------- |
|
||||
| Node.js | ≥ 20 | `node -v` | LTS 版本 |
|
||||
| pnpm | ≥ 9 | `pnpm -v` | `corepack enable` 自动启用 |
|
||||
| Go | 1.22+ | `go version` | api-gateway 编译 |
|
||||
| Docker | 24+ | `docker version` | 基础设施容器化 |
|
||||
| Docker Compose | v2+ | `docker compose version` | 编排基础设施 |
|
||||
| Git | 2.30+ | `git --version` | husky hooks 需要 |
|
||||
|
||||
> Windows 用户:建议在 PowerShell 或 Git Bash 中执行。Go 工具链若不在 PATH,临时加入:`$env:Path = "C:\Program Files\Go\bin;" + $env:Path`
|
||||
|
||||
---
|
||||
|
||||
## 2. 服务端口分配
|
||||
|
||||
| 服务 | 端口 | 语言/框架 | 启动方式 | 健康检查 |
|
||||
| -------------- | ---- | ------------ | ------------------- | ----------------- |
|
||||
| MySQL | 3306 | Docker | `docker compose up` | `mysqladmin ping` |
|
||||
| Redis | 6379 | Docker | `docker compose up` | `redis-cli ping` |
|
||||
| api-gateway | 8080 | Go (Gin) | `go run main.go` | `GET /healthz` |
|
||||
| classes | 3001 | NestJS (TS) | `pnpm dev` | `GET /healthz` |
|
||||
| teacher-portal | 3000 | Next.js (TS) | `pnpm dev` | `GET /` |
|
||||
| iam | 3002 | NestJS | P2 阶段 | — |
|
||||
| teacher-bff | 3003 | NestJS | P2 阶段 | — |
|
||||
|
||||
> 端口冲突排查:`netstat -ano \| findstr :8080`(Windows)
|
||||
|
||||
---
|
||||
|
||||
## 3. 本地开发模式(DEV_MODE)
|
||||
|
||||
### 3.1 首次准备
|
||||
|
||||
```bash
|
||||
# 1. 克隆仓库
|
||||
git clone <repo-url> Edu
|
||||
cd Edu
|
||||
|
||||
# 2. 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 3. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env,确认:
|
||||
# DEV_MODE=true ← 本地联调用
|
||||
# JWT_SECRET=p1-dev-secret-change-in-production
|
||||
# DATABASE_URL=mysql://edu:changeme@localhost:3306/next_edu_cloud
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
|
||||
# 4. 架构扫描(更新 arch.db)
|
||||
pnpm run arch:scan
|
||||
```
|
||||
|
||||
### 3.2 启动基础设施
|
||||
|
||||
```bash
|
||||
# 启动 MySQL + Redis(最小基础设施)
|
||||
docker compose -f infra/docker-compose.minimal.yml up -d
|
||||
|
||||
# 验证健康
|
||||
docker compose -f infra/docker-compose.minimal.yml ps
|
||||
# 状态应为 healthy
|
||||
|
||||
# (可选)国内镜像源加速:infra/docker-compose.minimal.override.yml 已配置 daocloud 镜像
|
||||
```
|
||||
|
||||
### 3.3 启动应用服务(三个终端)
|
||||
|
||||
#### 终端 1:api-gateway(:8080)
|
||||
|
||||
```bash
|
||||
cd services/api-gateway
|
||||
|
||||
# Windows:若 Go 不在 PATH,先设置
|
||||
$env:Path = "C:\Program Files\Go\bin;" + $env:Path
|
||||
|
||||
# 设置开发模式环境变量
|
||||
$env:DEV_MODE="true"
|
||||
$env:JWT_SECRET="p1-dev-secret-change-in-production"
|
||||
|
||||
# 启动
|
||||
go run main.go
|
||||
```
|
||||
|
||||
验证:`curl http://localhost:8080/healthz` → 200 OK
|
||||
|
||||
#### 终端 2:classes 服务(:3001)
|
||||
|
||||
```bash
|
||||
# 在项目根目录
|
||||
pnpm --filter @edu/classes-service dev
|
||||
```
|
||||
|
||||
验证:`curl http://localhost:3001/healthz` → 200 OK
|
||||
|
||||
> 注:classes 的 HealthModule 需注册到 AppModule,当前若返回 404 见 [known-issues]
|
||||
|
||||
#### 终端 3:teacher-portal(:3000)
|
||||
|
||||
```bash
|
||||
# 在项目根目录
|
||||
pnpm --filter @edu/teacher-portal dev
|
||||
```
|
||||
|
||||
验证:浏览器访问 `http://localhost:3000` → 班级管理页面
|
||||
|
||||
### 3.4 一键启动(可选)
|
||||
|
||||
```bash
|
||||
# 并行启动所有子包的 dev 脚本(含 api-gateway 需 go run,不会自动启动)
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
> 注:`pnpm dev` 仅启动 pnpm workspace 内的 TS 服务。api-gateway 是 Go 服务,需单独 `go run`。
|
||||
|
||||
### 3.5 联调验证
|
||||
|
||||
```bash
|
||||
# 1. 直接访问 api-gateway(带 dev-token)
|
||||
curl -H "Authorization: Bearer dev-token" http://localhost:8080/api/v1/classes
|
||||
# 预期:200 OK + 班级列表 JSON
|
||||
|
||||
# 2. 通过 teacher-portal 代理访问
|
||||
curl -H "Authorization: Bearer dev-token" http://localhost:3000/api/v1/classes
|
||||
# 预期:200 OK(经 Next.js rewrites → api-gateway → classes)
|
||||
|
||||
# 3. 浏览器访问
|
||||
# http://localhost:3000 → 班级管理页面,自动加载班级列表
|
||||
```
|
||||
|
||||
### 3.6 dev-token 说明
|
||||
|
||||
- `DEV_MODE=true` 时,api-gateway 接受 `Authorization: Bearer dev-token`
|
||||
- 注入固定身份:`x-user-id: dev-user`,`x-user-roles: teacher,admin`
|
||||
- **仅限本地联调,生产环境必须 `DEV_MODE=false`**
|
||||
|
||||
---
|
||||
|
||||
## 4. 生产模式(Docker Compose)
|
||||
|
||||
### 4.1 构建镜像
|
||||
|
||||
```bash
|
||||
# 构建三个应用服务镜像
|
||||
docker compose -f infra/docker-compose.prod.yml build
|
||||
```
|
||||
|
||||
### 4.2 启动完整栈
|
||||
|
||||
```bash
|
||||
# 1. 先启动基础设施(MySQL + Redis)
|
||||
docker compose -f infra/docker-compose.minimal.yml up -d
|
||||
|
||||
# 2. 启动应用服务
|
||||
docker compose -f infra/docker-compose.prod.yml up -d
|
||||
|
||||
# 3. 查看状态
|
||||
docker compose -f infra/docker-compose.prod.yml ps
|
||||
# 所有服务应为 healthy
|
||||
|
||||
# 4. 查看日志
|
||||
docker compose -f infra/docker-compose.prod.yml logs -f api-gateway
|
||||
```
|
||||
|
||||
### 4.3 生产环境配置要点
|
||||
|
||||
| 配置项 | 生产值 | 说明 |
|
||||
| -------------- | ---------------- | ----------------------------------------------- |
|
||||
| `DEV_MODE` | `false` | docker-compose.prod.yml 强制设为 false |
|
||||
| `JWT_SECRET` | 强随机值 | 替换 `p1-dev-secret-change-in-production` |
|
||||
| `DATABASE_URL` | 生产数据库连接 | 容器内用 `host.docker.internal` 或 compose 网络 |
|
||||
| `NODE_ENV` | `production` | teacher-portal 启用生产优化 |
|
||||
| `LOG_LEVEL` | `info` 或 `warn` | 生产日志级别 |
|
||||
|
||||
### 4.4 生产验证
|
||||
|
||||
```bash
|
||||
# 健康检查
|
||||
curl http://localhost:8080/healthz # api-gateway
|
||||
curl http://localhost:3001/healthz # classes
|
||||
curl http://localhost:3000/ # teacher-portal
|
||||
|
||||
# 鉴权验证(生产模式 dev-token 应被拒绝)
|
||||
curl -H "Authorization: Bearer dev-token" http://localhost:8080/api/v1/classes
|
||||
# 预期:401 INVALID_TOKEN
|
||||
|
||||
# 真实 JWT 访问(需 IAM 签发)
|
||||
curl -H "Authorization: Bearer <real-jwt>" http://localhost:8080/api/v1/classes
|
||||
# 预期:200 OK
|
||||
```
|
||||
|
||||
### 4.5 停止与清理
|
||||
|
||||
```bash
|
||||
# 停止应用服务
|
||||
docker compose -f infra/docker-compose.prod.yml down
|
||||
|
||||
# 停止基础设施
|
||||
docker compose -f infra/docker-compose.minimal.yml down
|
||||
|
||||
# 清理数据卷(谨慎!会删除数据库数据)
|
||||
docker compose -f infra/docker-compose.minimal.yml down -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 单独构建与运行(裸机生产)
|
||||
|
||||
### 5.1 api-gateway
|
||||
|
||||
```bash
|
||||
cd services/api-gateway
|
||||
|
||||
# 编译
|
||||
go build -o bin/api-gateway ./main.go
|
||||
|
||||
# 运行(生产环境变量)
|
||||
DEV_MODE=false JWT_SECRET=<your-secret> ./bin/api-gateway
|
||||
```
|
||||
|
||||
### 5.2 classes 服务
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
pnpm --filter @edu/classes-service build
|
||||
|
||||
# 运行(从 dist/ 启动)
|
||||
NODE_ENV=production PORT=3001 \
|
||||
DATABASE_URL=mysql://edu:changeme@localhost:3306/next_edu_cloud \
|
||||
REDIS_URL=redis://localhost:6379 \
|
||||
node services/classes/dist/main.js
|
||||
```
|
||||
|
||||
### 5.3 teacher-portal
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
pnpm --filter @edu/teacher-portal build
|
||||
|
||||
# 运行
|
||||
NODE_ENV=production PORT=3000 \
|
||||
API_GATEWAY_URL=http://localhost:8080 \
|
||||
node apps/teacher-portal/node_modules/.bin/next start -p 3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### 6.1 ERR_TOO_MANY_REDIRECTS
|
||||
|
||||
**症状**:浏览器访问 `:3000/api/v1/classes` 无限重定向。
|
||||
|
||||
**根因**:Gin 默认 `RedirectTrailingSlash=true`,`/classes` → 301 → `/classes/`,Next.js rewrites 代理形成循环。
|
||||
|
||||
**修复**:`main.go` 已设 `r.RedirectTrailingSlash = false`,并同时注册无尾斜杠与通配符路由。
|
||||
|
||||
### 6.2 Docker Hub 拉取超时
|
||||
|
||||
**症状**:`docker compose up` 时 MySQL/Redis 镜像拉取超时。
|
||||
|
||||
**修复**:`infra/docker-compose.minimal.override.yml` 已配置 daocloud 镜像源,docker compose 会自动加载。
|
||||
|
||||
### 6.3 classes DI 注入失败
|
||||
|
||||
**症状**:`TypeError: Cannot read properties of undefined (reading 'create')`。
|
||||
|
||||
**根因**:NestJS ESM 模式下 `emitDecoratorMetadata` 不工作。
|
||||
|
||||
**修复**:`ClassesService` 构造函数已加 `@Inject(ClassesRepository)` 显式指定 token。
|
||||
|
||||
### 6.4 ESM import 缺 .js 后缀
|
||||
|
||||
**症状**:`error TS2307: Cannot find module './health.controller'`。
|
||||
|
||||
**修复**:ESM 模式下相对 import 必须带 `.js` 后缀(详见 project_rules §3.4)。
|
||||
|
||||
### 6.5 JWT 401 INVALID_TOKEN
|
||||
|
||||
**症状**:带 `dev-token` 仍返回 401。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. 确认 `DEV_MODE=true` 环境变量已设置(api-gateway 进程)
|
||||
2. 确认请求头格式:`Authorization: Bearer dev-token`(注意 Bearer 后空格)
|
||||
3. 生产模式(`DEV_MODE=false`)下 dev-token 被拒绝是预期行为
|
||||
|
||||
### 6.6 Go 工具链不在 PATH
|
||||
|
||||
**症状**:Git Bash 或 PowerShell 中 `go: command not found`。
|
||||
|
||||
**修复**:
|
||||
|
||||
```powershell
|
||||
$env:Path = "C:\Program Files\Go\bin;" + $env:Path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据库初始化
|
||||
|
||||
classes 服务使用 Drizzle ORM,首次运行需建表:
|
||||
|
||||
```bash
|
||||
# 生成迁移
|
||||
pnpm --filter @edu/classes-service drizzle-kit generate
|
||||
|
||||
# 执行迁移
|
||||
pnpm --filter @edu/classes-service drizzle-kit migrate
|
||||
```
|
||||
|
||||
> 若 `drizzle-kit` 未配置,可手动执行 `services/classes/src/db/schema.sql`(如存在)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [项目规则](../../.trae/rules/project_rules.md) — 强制约束
|
||||
- [004 架构影响地图](../architecture/004_architecture_impact_map.md) — 服务清单与调用关系
|
||||
- [known-issues](../troubleshooting/known-issues.md) — 已知问题速查
|
||||
- [Git 工作流](./git-workflow.md) — 提交与 PR 规范
|
||||
- [多 AI 协作指南](./multi-ai-collaboration.md) — 多 Agent 并行开发流程
|
||||
871
docs/standards/multi-ai-collaboration.md
Normal file
871
docs/standards/multi-ai-collaboration.md
Normal file
@@ -0,0 +1,871 @@
|
||||
# 多 AI 协作开发指南(Multi-AI Collaboration Guide)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-08
|
||||
> 适用范围:Edu 微服务项目多 Agent 并行开发
|
||||
> 关联文档:[Git 工作流](./git-workflow.md)、[项目规则](../../.trae/rules/project_rules.md)、[004 架构影响地图](../architecture/004_architecture_impact_map.md)、[CODEOWNERS](../../.github/CODEOWNERS)
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体模式
|
||||
|
||||
### 1.1 角色定义
|
||||
|
||||
| 角色 | 职责 | 数量 | 备注 |
|
||||
| ----------------------------- | -------------------------------------------- | ---- | ------------------- |
|
||||
| **协调 AI(Coordinator AI)** | PR 审核、合并、冲突仲裁、分支管理、发布 | 1 | 不直接写业务代码 |
|
||||
| **开发 AI(Dev AI)** | 按模块分工,写代码、提 PR | N | 每个负责 1-2 个模块 |
|
||||
| **人类决策者(Human)** | 架构决策、Breaking Change 审批、最终发布确认 | 1 | 只在关键节点介入 |
|
||||
|
||||
### 1.2 工作流总览
|
||||
|
||||
```
|
||||
开发 AI-A (classes模块) 开发 AI-B (iam模块) 协调 AI
|
||||
│ │ │
|
||||
│ 1.拉取最新 main │ 1.拉取最新 main │
|
||||
│ 2.创建特性分支 │ 2.创建特性分支 │
|
||||
│ 3.开发+提交 │ 3.开发+提交 │
|
||||
│ 4.推送+创建 PR │ 4.推送+创建 PR │
|
||||
│──────────────────────────────────────────────────────→│
|
||||
│ │ │ 5.CI校验
|
||||
│ │ │ 6.代码审核
|
||||
│ │ │ 7.合并到 main
|
||||
│ │ │ 8.通知开发 AI 同步
|
||||
│←─────────────────────────────────────────────────────│
|
||||
│ 9.拉取最新 main │ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 两种协作模式
|
||||
|
||||
本项目支持两种 AI 协作模式,根据开发阶段灵活切换:
|
||||
|
||||
### 2.0 模式选择
|
||||
|
||||
| 模式 | 适用阶段 | 特点 |
|
||||
| ------------------ | ---------------------------------- | ----------------------------------------------------- |
|
||||
| **单仓库并行模式** | 架构设计外包、各服务独立功能开发 | 无需 PR,直接 push main;路径级别物理隔离,几乎零冲突 |
|
||||
| **PR 模式** | 共享文件修改、跨模块变更、代码审核 | 标准 PR 流程,coord 审核后 Squash Merge |
|
||||
|
||||
> 当前架构设计外包阶段使用**单仓库并行模式**,详见 [AI 分配方案](../architecture/ai-allocation.md)。
|
||||
|
||||
---
|
||||
|
||||
### 2.1 单仓库并行模式
|
||||
|
||||
**适用场景**:各 AI 修改的文件路径物理隔离(不同 `services/<name>/` 目录),无需 PR 审核。
|
||||
|
||||
**核心规则**:
|
||||
|
||||
1. 每个 AI 只能修改自己负责的目录
|
||||
2. `pnpm-lock.yaml` 是唯一可能冲突的共享文件,冲突时取远程版本后 `pnpm install` 重新生成
|
||||
3. proto 变更由 coord 统一管理(其他 AI 只读引用 `packages/shared-proto/`)
|
||||
|
||||
**提交流程**:
|
||||
|
||||
```bash
|
||||
# 每天开始
|
||||
git pull origin main --rebase
|
||||
|
||||
# 在自己目录内工作后提交
|
||||
git add services/<my-service>/...
|
||||
git commit -m "feat(<scope>): <描述>"
|
||||
|
||||
# 直接 push
|
||||
git pull origin main --rebase # 先拉最新
|
||||
git push
|
||||
```
|
||||
|
||||
**pnpm-lock.yaml 冲突处理**:
|
||||
|
||||
```bash
|
||||
git pull origin main --rebase
|
||||
# 若 pnpm-lock.yaml 冲突:
|
||||
git checkout --theirs pnpm-lock.yaml
|
||||
pnpm install
|
||||
git add pnpm-lock.yaml
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 PR 模式(标准流程)
|
||||
|
||||
PR 模式见 §3-§6(分支策略、推送流程、PR 流程、审核合并)。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 模块完整清单
|
||||
|
||||
| 类别 | 服务名 | scope | 路径 | 阶段 | 语言 |
|
||||
| ---- | -------------- | ---------------- | ------------------------ | ------ | -------- |
|
||||
| 网关 | api-gateway | `api-gateway` | `services/api-gateway/` | P1 | Go |
|
||||
| 网关 | push-gateway | `push-gateway` | `services/push-gateway/` | P5 | Go |
|
||||
| BFF | teacher-bff | `teacher-bff` | `services/teacher-bff/` | P2 | TS |
|
||||
| BFF | student-bff | `student-bff` | `services/student-bff/` | P3 | TS |
|
||||
| BFF | parent-bff | `parent-bff` | `services/parent-bff/` | P4 | TS |
|
||||
| 业务 | iam | `iam` | `services/iam/` | P2 | TS |
|
||||
| 业务 | core-edu | `core-edu` | `services/core-edu/` | P3 | TS |
|
||||
| 业务 | content | `content` | `services/content/` | P4 | TS |
|
||||
| 业务 | msg | `msg` | `services/msg/` | P5 | TS |
|
||||
| 业务 | data-ana | `data-ana` | `services/data-ana/` | P4 | Python |
|
||||
| 业务 | ai | `ai` | `services/ai/` | P5 | Python |
|
||||
| 前端 | teacher-portal | `teacher-portal` | `apps/teacher-portal/` | P2 | TS |
|
||||
| 前端 | student-portal | `student-portal` | `apps/student-portal/` | P3 | TS |
|
||||
| 前端 | parent-portal | `parent-portal` | `apps/parent-portal/` | P4 | TS |
|
||||
| 前端 | admin-portal | `admin-portal` | `apps/admin-portal/` | P6 | TS |
|
||||
| 共享 | shared-proto | `shared-proto` | `packages/shared-proto/` | 跨阶段 | protobuf |
|
||||
| 共享 | shared-ts | `shared-ts` | `packages/shared-ts/` | 跨阶段 | TS |
|
||||
| 共享 | shared-go | `shared-go` | `packages/shared-go/` | 跨阶段 | Go |
|
||||
| 共享 | shared-py | `shared-py` | `packages/shared-py/` | 跨阶段 | Python |
|
||||
| 基础 | infra | `infra` | `infra/` | 跨阶段 | — |
|
||||
|
||||
### 2.4 当前阶段 AI 分配(7 AI + 1 coord)
|
||||
|
||||
详见 [AI 分配方案](../architecture/ai-allocation.md#3-ai-分配方案7-ai--1-coord)。摘要如下:
|
||||
|
||||
| AI | 服务 | 语言 |
|
||||
| ----- | ----------------------------------------------------------- | ------ |
|
||||
| ai01 | api-gateway、push-gateway | Go |
|
||||
| ai02 | iam | TS |
|
||||
| ai03 | teacher-bff、core-edu | TS |
|
||||
| ai04 | student-bff、parent-bff | TS |
|
||||
| ai05 | content、msg | TS |
|
||||
| ai06 | data-ana、ai | Python |
|
||||
| ai07 | teacher-portal、student-portal、parent-portal、admin-portal | TS |
|
||||
| coord | shared-proto、shared-*、infra/、docs/、CI/CD | — |
|
||||
|
||||
### 2.5 分工原则
|
||||
|
||||
1. **单一负责制**:每个模块只有一个 AI 负责,避免并行修改同一文件
|
||||
2. **同语言内聚**:一个 AI 负责多个同语言服务,降低学习成本
|
||||
3. **契约集中管理**:`shared-proto` 由 coord 维护,开发 AI 只读引用
|
||||
4. **跨模块变更拆分**:需要修改多个模块时,按依赖顺序(proto → service → gateway → BFF → frontend)
|
||||
5. **基础设施独立**:`infra/` 由 coord(或 SRE AI)专门负责
|
||||
6. **物理路径隔离**:目录级别隔离,单仓库并行几乎零文件冲突
|
||||
|
||||
### 2.6 架构设计外包三阶段
|
||||
|
||||
当前项目处于**架构设计外包阶段**,所有 AI 需先完成设计再动手写代码。完整文档见 [AI 分配方案](../architecture/ai-allocation.md)。简述如下:
|
||||
|
||||
```
|
||||
阶段 1:全局理解 → 交付"理解确认书"
|
||||
阶段 2:模块架构设计 → 交付"模块架构设计文档",coord 交叉审查
|
||||
阶段 3:按图实施 → 按设计文档写代码,coord 定期巡检一致性
|
||||
```
|
||||
|
||||
**阶段 1 交付物模板**:
|
||||
|
||||
```markdown
|
||||
## 模块理解确认书 — [模块名]
|
||||
|
||||
### 1. 我在架构中的位置
|
||||
|
||||
- 层级 / 上下游 / 通信方式
|
||||
|
||||
### 2. 我的限界上下文
|
||||
|
||||
- 聚合/实体 / 业务领域(D1-D6)/ 边界外
|
||||
|
||||
### 3. 我与外部的契约
|
||||
|
||||
- proto message / API/事件 / 错误码前缀
|
||||
|
||||
### 4. 我的技术栈
|
||||
|
||||
### 5. 我的阶段归属(P1-P6)
|
||||
|
||||
### 6. 黄金模板对齐清单(对照 classes 服务)
|
||||
```
|
||||
|
||||
**阶段 2 交付物模板**:
|
||||
|
||||
```markdown
|
||||
## 模块架构设计文档 — [模块名]
|
||||
|
||||
### 1. 模块内部分层图
|
||||
|
||||
### 2. 领域模型(聚合根/实体/值对象)
|
||||
|
||||
### 3. 数据模型(表/schema/索引/读写分离)
|
||||
|
||||
### 4. API 设计(端点/权限/请求响应)
|
||||
|
||||
### 5. 事件设计(发布/消费/Topic)
|
||||
|
||||
### 6. 横切关注点(权限/错误/可观测/健康/优雅关闭)
|
||||
|
||||
### 7. 与其他模块的交互点(契约清单)
|
||||
|
||||
### 8. 风险与假设
|
||||
```
|
||||
|
||||
**coord 交叉审查**:收到全部 7 份设计文档后,检查接口一致性、端口冲突、Topic 重复、错误码重叠、黄金模板对齐。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分支策略
|
||||
|
||||
### 3.1 分支命名规范
|
||||
|
||||
```
|
||||
<type>/<scope>-<task-id>-<ai-id>
|
||||
```
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
| --------- | ------------------------------------------- | ---------------- |
|
||||
| `type` | feat / fix / refactor / docs / chore / test | `feat` |
|
||||
| `scope` | 模块名(见 §2.1) | `classes` |
|
||||
| `task-id` | 任务简短描述(kebab-case) | `add-pagination` |
|
||||
| `ai-id` | AI 标识符(避免多 AI 同名冲突) | `ai01` |
|
||||
|
||||
**完整示例**:
|
||||
|
||||
- `feat/classes-add-pagination-ai01`
|
||||
- `fix/api-gateway-redirect-loop-ai02`
|
||||
- `docs/git-workflow-update-coord`
|
||||
|
||||
### 3.2 分支生命周期
|
||||
|
||||
1. **创建**:从最新 `main` 拉取:`git checkout main && git pull && git checkout -b feat/classes-xxx-ai01`
|
||||
2. **开发**:在特性分支上提交(遵循 Conventional Commits)
|
||||
3. **推送**:`git push -u origin feat/classes-xxx-ai01`
|
||||
4. **合并后删除**:PR 合并后,删除本地与远程特性分支
|
||||
|
||||
> 特性分支寿命 ≤ 3 天(见 git-workflow.md §1)。超期需 rebase 最新 main。
|
||||
|
||||
### 3.3 多 AI 分支隔离规则
|
||||
|
||||
- **禁止多 AI 共用同一分支**
|
||||
- **禁止直接 push 到 `main`**(由协调 AI 通过 PR 合并)
|
||||
- **跨模块变更**:各自在模块分支开发,最后由协调 AI 按依赖顺序合并
|
||||
|
||||
---
|
||||
|
||||
## 4. 推送与提交流程
|
||||
|
||||
### 4.1 提交规范(Conventional Commits)
|
||||
|
||||
```bash
|
||||
git commit -m "feat(classes): 新增班级分页查询"
|
||||
git commit -m "fix(api-gateway): 修复尾斜杠重定向循环"
|
||||
git commit -m "docs(standards): 更新多AI协作文档"
|
||||
```
|
||||
|
||||
**scope 必须匹配 `.commitlintrc.js` 的 scope-enum**(26 项,见 git-workflow.md §3.3)。
|
||||
|
||||
### 4.2 标准推送流程
|
||||
|
||||
开发 AI 执行:
|
||||
|
||||
```bash
|
||||
# 1. 确认在正确的特性分支
|
||||
git branch --show-current
|
||||
# 输出:feat/classes-add-pagination-ai01
|
||||
|
||||
# 2. 拉取最新 main(rebase 保持线性)
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
|
||||
# 3. 解决冲突(如有)
|
||||
# 编辑冲突文件 → git add → git rebase --continue
|
||||
|
||||
# 4. 本地校验(强制,见 project_rules §7)
|
||||
pnpm run lint # TS 服务
|
||||
pnpm run typecheck # TS 服务
|
||||
cd services/api-gateway && go vet ./... && go build ./... # Go 服务
|
||||
|
||||
# 5. 推送
|
||||
git push -u origin feat/classes-add-pagination-ai01
|
||||
|
||||
# 6. 创建 PR(见 §5)
|
||||
```
|
||||
|
||||
### 4.3 多 AI 推送冲突处理
|
||||
|
||||
当多个 AI 的 PR 修改了同一文件(如 `package.json`、`004_architecture_impact_map.md`):
|
||||
|
||||
1. **先合并的 PR 正常合并**
|
||||
2. **后合并的 PR 需 rebase**:
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
# 解决冲突
|
||||
git push --force-with-lease # 安全强推(仅自己的分支)
|
||||
```
|
||||
3. **协调 AI 仲裁**:若冲突涉及架构决策,由协调 AI 决定保留方案
|
||||
|
||||
> **禁止 `git push --force` 到 main 或他人分支**。只允许 `--force-with-lease` 到自己的特性分支。
|
||||
|
||||
---
|
||||
|
||||
## 5. PR 流程
|
||||
|
||||
### 5.1 创建 PR
|
||||
|
||||
推送后,开发 AI 通过 Git 平台(GitHub/Gitea)创建 PR:
|
||||
|
||||
```bash
|
||||
# 使用 gh CLI(GitHub)
|
||||
gh pr create \
|
||||
--title "feat(classes): 新增班级分页查询" \
|
||||
--body "## 变更说明
|
||||
- 新增 GET /api/v1/classes?page=&size= 分页参数
|
||||
- 响应增加 total/hasMore 字段
|
||||
|
||||
## 变更类型
|
||||
- [x] feat(新功能)
|
||||
|
||||
## 影响范围
|
||||
- [x] classes 服务
|
||||
|
||||
## 测试
|
||||
- [x] 单元测试通过
|
||||
- [x] 本地联调验证
|
||||
|
||||
## 校验
|
||||
- [x] pnpm run lint
|
||||
- [x] pnpm run typecheck
|
||||
" \
|
||||
--base main \
|
||||
--head feat/classes-add-pagination-ai01
|
||||
```
|
||||
|
||||
### 5.2 PR 标题规范
|
||||
|
||||
PR 标题必须与首个 commit 的 subject 一致(Squash Merge 时自动取首个 commit):
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
- `feat(classes): 新增班级分页查询`
|
||||
- `fix(api-gateway): 修复尾斜杠重定向循环`
|
||||
|
||||
### 5.3 PR 模板
|
||||
|
||||
使用 `.github/pull_request_template.md`(已存在)。必填项:
|
||||
|
||||
- 变更说明
|
||||
- 变更类型
|
||||
- 影响范围
|
||||
- 测试情况
|
||||
- 按语言校验结果
|
||||
- 文档同步(arch:scan、CODEOWNERS)
|
||||
|
||||
### 5.4 PR 标注 AI 身份
|
||||
|
||||
在 PR 描述末尾追加(用于统计与审计):
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
**AI Agent**: ai01 (classes-module)
|
||||
**Branch**: feat/classes-add-pagination-ai01
|
||||
**Coordinator**: coord-ai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 代码审核与合并
|
||||
|
||||
### 6.1 Reviewer 分配
|
||||
|
||||
由 `.github/CODEOWNERS` 自动分配:
|
||||
|
||||
| 模块 | 自动 Reviewer | 实际映射 |
|
||||
| ------------ | ----------------------- | -------- |
|
||||
| classes | `@edu-platform/classes` | 协调 AI |
|
||||
| api-gateway | `@edu-platform/gateway` | 协调 AI |
|
||||
| shared-proto | `@edu-platform/arch` | 协调 AI |
|
||||
| infra | `@edu-platform/sre` | SRE AI |
|
||||
|
||||
> 当前 `@edu-platform/*` 为占位符。多 AI 场景下,协调 AI 账号加入对应 team 即可自动收到 review 请求。
|
||||
|
||||
### 6.2 审核 checklist
|
||||
|
||||
协调 AI 审核 PR 时检查:
|
||||
|
||||
- [ ] **Commit 格式**:符合 Conventional Commits
|
||||
- [ ] **Scope 正确**:scope 与修改的模块一致
|
||||
- [ ] **架构合规**:无跨层依赖、无直接访问他人 DB
|
||||
- [ ] **权限校验**:Controller 有 `@RequirePermission()`
|
||||
- [ ] **设计令牌**:无硬编码颜色/字体(project_rules §3.10)
|
||||
- [ ] **ESM 规则**:相对 import 带 `.js` 后缀(TS 服务)
|
||||
- [ ] **契约同步**:proto 变更已同步 shared-proto
|
||||
- [ ] **文档同步**:arch.db 已更新、004 已同步(如涉及)
|
||||
- [ ] **CI 通过**:lint / typecheck / go vet / ruff 全绿
|
||||
- [ ] **测试覆盖**:关键逻辑有单测
|
||||
|
||||
### 6.3 合并策略
|
||||
|
||||
| 策略 | 适用场景 | 操作 |
|
||||
| ------------------------ | ------------------------------------ | ---------------------- |
|
||||
| **Squash Merge**(默认) | 单 commit 或多个小 commit 合并为一个 | `gh pr merge --squash` |
|
||||
| **Rebase Merge** | 多个有意义的 commit 需保留历史 | `gh pr merge --rebase` |
|
||||
| **Merge Commit**(禁止) | — | 不允许 |
|
||||
|
||||
> 项目规则 git-workflow.md §1.4:**禁止 Merge Commit**,保持 main 历史线性。
|
||||
|
||||
### 6.4 合并流程(协调 AI 执行)
|
||||
|
||||
```bash
|
||||
# 1. 确认 CI 通过
|
||||
gh pr checks <PR-NUMBER>
|
||||
|
||||
# 2. 确认 review 通过(CODEOWNERS 已 approve)
|
||||
gh pr view <PR-NUMBER> --json reviews
|
||||
|
||||
# 3. Squash 合并(删除源分支)
|
||||
gh pr merge <PR-NUMBER> --squash --delete-branch
|
||||
|
||||
# 4. 通知开发 AI 同步本地 main
|
||||
# (通过 commit message 或 issue 评论)
|
||||
```
|
||||
|
||||
### 6.5 合并后同步
|
||||
|
||||
开发 AI 收到合并通知后:
|
||||
|
||||
```bash
|
||||
# 切回 main
|
||||
git checkout main
|
||||
|
||||
# 拉取最新(含已合并的 PR)
|
||||
git pull origin main
|
||||
|
||||
# 删除本地特性分支
|
||||
git branch -d feat/classes-add-pagination-ai01
|
||||
|
||||
# 开始下一个任务:从最新 main 拉新分支
|
||||
git checkout -b feat/classes-next-task-ai01
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 多 AI 并行开发流程
|
||||
|
||||
### 7.1 启动并行任务
|
||||
|
||||
人类决策者分配任务给多个 AI:
|
||||
|
||||
```
|
||||
任务 1:classes 服务新增分页查询 → 开发 AI-A (ai01)
|
||||
任务 2:api-gateway 新增限流规则 → 开发 AI-B (ai02)
|
||||
任务 3:teacher-portal 优化班级列表 → 开发 AI-C (ai03)
|
||||
```
|
||||
|
||||
### 7.2 并行执行
|
||||
|
||||
每个 AI 独立工作,互不干扰:
|
||||
|
||||
```bash
|
||||
# AI-A
|
||||
git checkout -b feat/classes-add-pagination-ai01
|
||||
# ... 开发 ...
|
||||
git push -u origin feat/classes-add-pagination-ai01
|
||||
gh pr create --title "feat(classes): 新增班级分页查询" ...
|
||||
|
||||
# AI-B(同时)
|
||||
git checkout -b feat/api-gateway-rate-limit-ai02
|
||||
# ... 开发 ...
|
||||
git push -u origin feat/api-gateway-rate-limit-ai02
|
||||
gh pr create --title "feat(api-gateway): 新增 IP 级限流规则" ...
|
||||
|
||||
# AI-C(同时)
|
||||
git checkout -b feat/teacher-portal-class-list-ux-ai03
|
||||
# ... 开发 ...
|
||||
```
|
||||
|
||||
### 7.3 顺序合并(有依赖时)
|
||||
|
||||
若 PR 间有依赖(如 AI-C 依赖 AI-A 的接口),协调 AI 按顺序合并:
|
||||
|
||||
1. 合并 AI-A 的 PR(classes 分页接口)
|
||||
2. AI-C rebase 最新 main,解决冲突,更新 PR
|
||||
3. 合并 AI-C 的 PR(前端调用新接口)
|
||||
|
||||
### 7.4 冲突预防
|
||||
|
||||
1. **契约先行**:proto 变更先合并,再合并依赖它的服务
|
||||
2. **通知机制**:修改 `shared-proto/` 或 `004_architecture_impact_map.md` 时,在 PR 描述 @ 所有关联模块 AI
|
||||
3. **频繁同步**:每天至少 `git pull origin main` 一次,避免长期分叉
|
||||
|
||||
---
|
||||
|
||||
## 8. 跨模块变更流程
|
||||
|
||||
### 8.1 变更拆分原则
|
||||
|
||||
修改涉及多模块时,按依赖顺序拆成多个 PR:
|
||||
|
||||
```
|
||||
原始任务:classes 服务新增"班级导入"功能
|
||||
涉及:
|
||||
1. shared-proto 新增 ImportClassesRequest message
|
||||
2. classes 服务实现 import 接口
|
||||
3. api-gateway 路由 /api/v1/classes/import
|
||||
4. teacher-portal 前端上传按钮
|
||||
```
|
||||
|
||||
### 8.2 拆分与合并顺序
|
||||
|
||||
| 顺序 | PR | scope | 负责人 | 依赖 |
|
||||
| ---- | ----------------------------------------------- | -------------- | --------- | ---- |
|
||||
| 1 | `feat(shared-proto): 新增 ImportClassesRequest` | shared-proto | 协调 AI | 无 |
|
||||
| 2 | `feat(classes): 实现班级批量导入` | classes | 开发 AI-A | PR-1 |
|
||||
| 3 | `feat(api-gateway): 新增 import 路由` | api-gateway | 开发 AI-B | PR-2 |
|
||||
| 4 | `feat(teacher-portal): 新增班级导入按钮` | teacher-portal | 开发 AI-C | PR-3 |
|
||||
|
||||
协调 AI 按顺序合并,每合并一个,后续 PR 的开发 AI rebase 最新 main。
|
||||
|
||||
### 8.3 Breaking Change 流程
|
||||
|
||||
涉及 Breaking Change(proto 字段删除、API 签名变更):
|
||||
|
||||
1. **人类决策者审批**:在 issue 中讨论,获批准后才开发
|
||||
2. **版本号升级**:proto 加 `v2` 后缀,服务同时支持 v1/v2 过渡期
|
||||
3. **迁移文档**:更新 `MIGRATION_GUIDE.md`
|
||||
4. **协调 AI 通知所有相关 AI**:在 PR 描述中列出影响范围
|
||||
|
||||
---
|
||||
|
||||
## 9. 完整工作流示例
|
||||
|
||||
### 9.1 场景:开发 AI-A 为 classes 新增分页查询
|
||||
|
||||
```bash
|
||||
# ============ 开发 AI-A ============
|
||||
|
||||
# 1. 同步 main
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# 2. 创建特性分支
|
||||
git checkout -b feat/classes-add-pagination-ai01
|
||||
|
||||
# 3. 开发
|
||||
# 编辑 services/classes/src/classes/classes.controller.ts
|
||||
# 编辑 services/classes/src/classes/classes.service.ts
|
||||
# 编辑 services/classes/src/classes/classes.repository.ts
|
||||
|
||||
# 4. 本地校验
|
||||
pnpm --filter @edu/classes-service lint
|
||||
pnpm --filter @edu/classes-service typecheck
|
||||
pnpm --filter @edu/classes-service test
|
||||
|
||||
# 5. 架构扫描(若新增了导出函数/路由)
|
||||
pnpm run arch:scan
|
||||
|
||||
# 6. 提交
|
||||
git add services/classes/
|
||||
git commit -m "feat(classes): 新增班级分页查询
|
||||
|
||||
- GET /api/v1/classes 支持 page、size 参数
|
||||
- 响应增加 total、hasMore 字段
|
||||
- 单测覆盖分页逻辑"
|
||||
|
||||
# 7. 推送
|
||||
git push -u origin feat/classes-add-pagination-ai01
|
||||
|
||||
# 8. 创建 PR
|
||||
gh pr create \
|
||||
--title "feat(classes): 新增班级分页查询" \
|
||||
--body "..." \
|
||||
--base main
|
||||
|
||||
# ============ 协调 AI ============
|
||||
|
||||
# 9. 收到 PR 通知,审核
|
||||
gh pr view <PR-NUMBER>
|
||||
gh pr checks <PR-NUMBER>
|
||||
|
||||
# 10. 代码审核(见 §6.2 checklist)
|
||||
# 若需修改,评论要求开发 AI-A 修改
|
||||
|
||||
# 11. 合并
|
||||
gh pr merge <PR-NUMBER> --squash --delete-branch
|
||||
|
||||
# 12. 通知开发 AI-A
|
||||
# 评论:"已合并,请同步本地 main"
|
||||
|
||||
# ============ 开发 AI-A ============
|
||||
|
||||
# 13. 同步
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git branch -d feat/classes-add-pagination-ai01
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见问题与解决方案
|
||||
|
||||
### 10.1 多 AI 修改同一文件冲突
|
||||
|
||||
**场景**:AI-A 和 AI-B 都修改了 `package.json` 添加依赖。
|
||||
|
||||
**解决**:
|
||||
|
||||
1. AI-A 的 PR 先合并
|
||||
2. AI-B 执行:
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
# 解决 package.json 冲突(保留两边依赖)
|
||||
git add package.json
|
||||
git rebase --continue
|
||||
git push --force-with-lease
|
||||
```
|
||||
3. 协调 AI 重新审核
|
||||
|
||||
### 10.2 CI 失败但本地通过
|
||||
|
||||
**场景**:开发 AI 本地 lint 通过,CI 报错。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. 检查 Node/Go/pnpm 版本是否一致(见 local-dev-runbook §1)
|
||||
2. 检查 `pnpm-lock.yaml` 是否最新(`pnpm install --frozen-lockfile`)
|
||||
3. 检查环境变量是否齐全(`.env` vs CI secrets)
|
||||
|
||||
**修复后**:
|
||||
|
||||
```bash
|
||||
git add <fixed-files>
|
||||
git commit -m "fix(classes): 修复 CI lint 失败"
|
||||
git push
|
||||
```
|
||||
|
||||
### 10.3 husky pre-commit hook 失败
|
||||
|
||||
**场景**:commit 时 hook 报错(lint-staged、commitlint)。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. commitlint:检查 commit message 格式(type 小写、scope 在 enum 内、subject 不大写开头)
|
||||
2. lint-staged:检查暂存文件是否通过 ESLint/gofmt
|
||||
3. husky 9 Windows bug:若 pre-push hook "Bad file descriptor",见 known-issues
|
||||
|
||||
**修复**:修正后重新 `git commit`,不要用 `--no-verify` 跳过。
|
||||
|
||||
### 10.4 proto 变更导致下游编译失败
|
||||
|
||||
**场景**:shared-proto 合并后,classes 服务 `buf generate` 报错。
|
||||
|
||||
**解决**:
|
||||
|
||||
1. 协调 AI 合并 proto PR 前,先在 PR 评论中通知所有依赖服务的 AI
|
||||
2. 下游 AI 在自己的分支 rebase 最新 main 后重新 `buf generate`
|
||||
3. 更新生成代码并提交:
|
||||
```bash
|
||||
git add packages/shared-proto/gen/
|
||||
git commit -m "chore(shared-proto): 重新生成 TS 类型"
|
||||
```
|
||||
|
||||
### 10.5 分支长期未合并(超 3 天)
|
||||
|
||||
**场景**:特性分支超过 3 天未合并。
|
||||
|
||||
**处理**:
|
||||
|
||||
1. rebase 最新 main:`git rebase origin/main`
|
||||
2. 解决冲突后 `git push --force-with-lease`
|
||||
3. 若仍无法合并,协调 AI 介入评估是否拆分 PR
|
||||
|
||||
### 10.6 误推到 main
|
||||
|
||||
**场景**:开发 AI 误将 commit 推到 main。
|
||||
|
||||
**修复**(协调 AI 执行):
|
||||
|
||||
```bash
|
||||
# 1. 确认误推的 commit
|
||||
git log origin/main -5
|
||||
|
||||
# 2. 回退(保留误推 commit 到临时分支)
|
||||
git checkout origin/main -b temp/backup-mispush
|
||||
git checkout main
|
||||
git reset --hard origin/main~1 # 回退 1 个 commit
|
||||
|
||||
# 3. 强推(仅协调 AI 有权限)
|
||||
git push --force-with-lease origin main
|
||||
|
||||
# 4. 通知开发 AI 从 temp 分支重新提 PR
|
||||
```
|
||||
|
||||
> **禁止开发 AI 自行 force push main**。只有协调 AI 有权操作。
|
||||
|
||||
### 10.7 AI 身份冲突(同名分支)
|
||||
|
||||
**场景**:两个 AI 都叫 "ai01",创建了同名分支。
|
||||
|
||||
**预防**:每个 AI 启动前分配唯一 `ai-id`(如 `ai01`、`ai02`、`coord`)。
|
||||
|
||||
**修复**:后启动的 AI 重命名分支:
|
||||
|
||||
```bash
|
||||
git branch -m feat/classes-xxx-ai01 feat/classes-xxx-ai01b
|
||||
git push -u origin feat/classes-xxx-ai01b
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. AI 协作通信约定
|
||||
|
||||
### 11.1 通信渠道
|
||||
|
||||
| 场景 | 方式 |
|
||||
| -------- | ----------------------------- |
|
||||
| PR 审核 | PR 评论(`@ai01 请修改 xxx`) |
|
||||
| 任务分配 | Issue(`@ai01 负责实现 xxx`) |
|
||||
| 紧急通知 | PR 评论 + @ 相关人员 |
|
||||
| 架构讨论 | Issue 标签 `discussion` |
|
||||
|
||||
### 11.2 PR 评论规范
|
||||
|
||||
协调 AI 审核评论格式:
|
||||
|
||||
```
|
||||
## 审核结果:需修改 / 通过 / 拒绝
|
||||
|
||||
### 需修改项
|
||||
1. [classes.controller.ts:42] 缺少 @RequirePermission() 装饰器
|
||||
2. [classes.service.ts:88] 返回值未标注 Promise<T>
|
||||
|
||||
### 建议
|
||||
- 考虑抽取分页逻辑到 shared-ts
|
||||
|
||||
---
|
||||
协调 AI(coord)
|
||||
```
|
||||
|
||||
### 11.3 AI 工作日志
|
||||
|
||||
每个 AI 完成任务后,在 `docs/troubleshooting/known-issues.md` "工作经验日志"区追加(project_rules §9.3):
|
||||
|
||||
```markdown
|
||||
| 日期 | 模块 | 做了什么 + 学到什么 |
|
||||
| ---------------- | ------- | ----------------------------------------------- |
|
||||
| 2026-07-08 14:00 | classes | 实现分页查询,学到 Drizzle 的 limit/offset 用法 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 安全与权限
|
||||
|
||||
### 12.1 分支保护规则(main)
|
||||
|
||||
- **禁止直接 push**:必须通过 PR
|
||||
- **必须 PR review**:至少 1 人(CODEOWNERS 自动分配)
|
||||
- **必须 CI 通过**:lint / typecheck / build
|
||||
- **禁止 force push**:开发 AI 无权,仅协调 AI 在事故时操作
|
||||
|
||||
### 12.2 AI 账号权限
|
||||
|
||||
| 角色 | push 特性分支 | 创建 PR | 合并 PR | push main | force push main |
|
||||
| ------- | ------------- | ------- | ----------- | --------- | --------------- |
|
||||
| 开发 AI | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| 协调 AI | ✅ | ✅ | ✅ | ❌ | ⚠️(仅事故) |
|
||||
| SRE AI | ✅(infra) | ✅ | ✅(infra) | ❌ | ⚠️(仅事故) |
|
||||
|
||||
### 12.3 敏感文件保护
|
||||
|
||||
以下文件修改需人类决策者额外审批:
|
||||
|
||||
- `.env`、`.env.example`(密钥相关)
|
||||
- `infra/security/secrets.example.env`
|
||||
- `infra/k8s/`(生产部署)
|
||||
- `.github/workflows/`(CI 配置)
|
||||
|
||||
---
|
||||
|
||||
## 13. 发布流程
|
||||
|
||||
### 13.1 版本号规则(见 git-workflow.md §7)
|
||||
|
||||
- 平台版本:`v{阶段}.{迭代}.{patch}`(如 `v1.2.0`)
|
||||
- 服务版本:`{service}:{semver}`(如 `classes:1.0.1`)
|
||||
|
||||
### 13.2 发布步骤(协调 AI 执行)
|
||||
|
||||
```bash
|
||||
# 1. 确认 main 稳定(CI 全绿)
|
||||
gh run list --branch main --limit 5
|
||||
|
||||
# 2. 打 tag
|
||||
git tag -a v1.2.0 -m "P1 阶段第 2 次迭代发布"
|
||||
git push origin v1.2.0
|
||||
|
||||
# 3. 触发 CI 构建镜像
|
||||
# (CI 由 tag push 触发,见 .github/workflows/)
|
||||
|
||||
# 4. 人类决策者确认部署到生产
|
||||
```
|
||||
|
||||
### 13.3 回滚(见 git-workflow.md §8)
|
||||
|
||||
```bash
|
||||
# K8s 回滚
|
||||
kubectl rollout undo deployment/api-gateway -n edu-system
|
||||
|
||||
# Git 回退(紧急)
|
||||
git revert <bad-commit>
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 快速参考卡
|
||||
|
||||
### 开发 AI 日常工作流
|
||||
|
||||
```bash
|
||||
# 拉新分支
|
||||
git checkout main && git pull && git checkout -b feat/<scope>-<task>-<ai-id>
|
||||
|
||||
# 开发 → 校验 → 提交
|
||||
pnpm run lint && pnpm run typecheck
|
||||
git add . && git commit -m "feat(<scope>): <subject>"
|
||||
|
||||
# 推送 → 创建 PR
|
||||
git push -u origin feat/<scope>-<task>-<ai-id>
|
||||
gh pr create --title "feat(<scope>): <subject>" --body "..."
|
||||
|
||||
# 等待审核 → 修改 → 重新推送
|
||||
# (协调 AI 合并后)
|
||||
git checkout main && git pull && git branch -d feat/<scope>-<task>-<ai-id>
|
||||
```
|
||||
|
||||
### 协调 AI 日常工作流
|
||||
|
||||
```bash
|
||||
# 查看待审核 PR
|
||||
gh pr list --state open --reviewer @edu-platform/arch
|
||||
|
||||
# 审核
|
||||
gh pr view <PR-NUMBER>
|
||||
gh pr checks <PR-NUMBER>
|
||||
|
||||
# 合并
|
||||
gh pr merge <PR-NUMBER> --squash --delete-branch
|
||||
|
||||
# 发布
|
||||
git tag -a v<version> -m "..." && git push origin v<version>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 相关文档
|
||||
|
||||
- [AI 分配方案](../architecture/ai-allocation.md) — 架构设计外包 AI 分配与三阶段流程
|
||||
- [Git 工作流](./git-workflow.md) — 提交规范、分支策略、CODEOWNERS
|
||||
- [本地启动手册](./local-dev-runbook.md) — 手动启动服务
|
||||
- [项目规则](../../.trae/rules/project_rules.md) — 强制约束
|
||||
- [004 架构影响地图](../architecture/004_architecture_impact_map.md) — 模块与依赖关系
|
||||
- [CODEOWNERS](../../.github/CODEOWNERS) — Reviewer 自动分配
|
||||
- [PR 模板](../../.github/pull_request_template.md) — PR 必填项
|
||||
- [known-issues](../troubleshooting/known-issues.md) — 已知问题速查
|
||||
@@ -0,0 +1,137 @@
|
||||
# CI/CD 设计:本地构建部署(no-push)
|
||||
|
||||
> 日期:2026-07-08
|
||||
> 状态:已批准,待实施
|
||||
> 替代方案:[2026-07-08 之前的 docker.yml + deploy.yml push 方案](../../.github/workflows/)
|
||||
|
||||
## 1. 背景
|
||||
|
||||
### 1.1 现状问题
|
||||
|
||||
- `node-with-docker:22` 自定义镜像已丢失,原方案依赖它无法运行
|
||||
- 推送到 Gitea Registry 的拉取速度极慢,影响部署效率
|
||||
- 用户习惯本地构建测试后再推送,无需保留历史构建产物
|
||||
- 微服务架构下,拆分多个 workflow 文件(ci-ts/ci-go/docker/deploy)使流程碎片化
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
- 不依赖任何自建镜像,全部使用官方镜像
|
||||
- 不推送镜像到 registry,构建即部署
|
||||
- 单文件管理整个 CI/CD 流程
|
||||
- 支持回滚(不依赖 registry tag)
|
||||
- 提供一次性预拉所有镜像的 compose 文件
|
||||
|
||||
## 2. 设计
|
||||
|
||||
### 2.1 架构总览
|
||||
|
||||
```
|
||||
PR/push 触发
|
||||
│
|
||||
├─ quality-ts job (container: node:22-alpine) 并行
|
||||
├─ quality-go job (container: golang:1.22-alpine) 并行
|
||||
├─ quality-proto job (container: bufbuild/buf:latest) 并行
|
||||
│
|
||||
└─ deploy job (container: docker:25-git) 仅 push main, needs: [quality-*]
|
||||
挂载 /var/run/docker.sock(DooD)
|
||||
├─ docker compose up --build(本地构建 3 个服务)
|
||||
└─ 健康检查轮询
|
||||
```
|
||||
|
||||
### 2.2 文件结构
|
||||
|
||||
```
|
||||
.github/workflows/
|
||||
└─ ci.yml # 唯一的 CI/CD 文件
|
||||
|
||||
infra/
|
||||
├─ docker-compose.deploy.yml # 改造:build: 替代 image:
|
||||
├─ docker-compose.tools.yml # 新建:预拉所有 CI 需要的镜像
|
||||
└─ deploy.env.example # 保留
|
||||
```
|
||||
|
||||
### 2.3 ci.yml 设计
|
||||
|
||||
**单文件多 job**:
|
||||
|
||||
- `quality-ts`:PR+push 都跑,container: node:22-alpine,pnpm install → lint → typecheck → test → build
|
||||
- `quality-go`:PR+push 都跑,container: golang:1.22-alpine,go mod download → vet → build → test
|
||||
- `quality-proto`:PR+push 都跑,container: bufbuild/buf:latest,buf lint + buf breaking(仅 PR)
|
||||
- `deploy`:仅 push main 或 workflow_dispatch 触发,needs: [quality-ts, quality-go, quality-proto]
|
||||
|
||||
**deploy job 关键配置**:
|
||||
|
||||
- `container: docker:25-git`(官方镜像,自带 docker CLI + compose v2 + git)
|
||||
- `options: --volume /var/run/docker.sock:/var/run/docker.sock`(DooD 模式)
|
||||
- 流程:checkout → cp compose 文件到 /opt/edu/ → docker compose up --build → 健康检查
|
||||
|
||||
**回滚**:
|
||||
|
||||
- `workflow_dispatch` 支持 `commit_sha` 输入
|
||||
- checkout 时使用指定 commit SHA
|
||||
- 重新 build + deploy
|
||||
|
||||
### 2.4 docker-compose.deploy.yml 改造
|
||||
|
||||
所有服务从 `image:` 改为 `build:`:
|
||||
|
||||
- `api-gateway`:`build: ./services/api-gateway`
|
||||
- `classes`:`build: { context: ., dockerfile: services/classes/Dockerfile }`(monorepo 上下文)
|
||||
- `teacher-portal`:`build: { context: ., dockerfile: apps/teacher-portal/Dockerfile }`(monorepo 上下文)
|
||||
|
||||
删除 `IMAGE_TAG` 环境变量依赖。`docker compose up --build` 自动判断变更的服务。
|
||||
|
||||
### 2.5 docker-compose.tools.yml(预拉镜像)
|
||||
|
||||
用于一次性拉取所有 CI/构建需要的镜像到本地:
|
||||
|
||||
- CI 运行时:node:22-alpine、golang:1.22-alpine、bufbuild/buf:latest、docker:25-git
|
||||
- 服务构建基础:node:20-alpine、golang:1.22-alpine、alpine:3.20
|
||||
- 开发基础设施:mysql:8.0、redis:7-alpine
|
||||
|
||||
用法:`docker compose -f infra/docker-compose.tools.yml pull`
|
||||
|
||||
### 2.6 回滚策略
|
||||
|
||||
| 方式 | 操作 | 适用场景 |
|
||||
| ---------- | --------------------------------------------------- | ---------------- |
|
||||
| git revert | `git revert <bad-commit> && git push` → 自动触发 CI | 代码回滚(推荐) |
|
||||
| 手动触发 | Actions → ci.yml → Run workflow → 填 commit_sha | 部署特定版本 |
|
||||
|
||||
### 2.7 actrunner 配置
|
||||
|
||||
```toml
|
||||
container:
|
||||
valid_volumes:
|
||||
- /var/run/docker.sock
|
||||
```
|
||||
|
||||
## 3. 实施步骤
|
||||
|
||||
1. 删除现有 4 个 workflow 文件(ci-ts.yml、ci-go.yml、ci-proto.yml、docker.yml、deploy.yml)
|
||||
2. 创建 `.github/workflows/ci.yml`(合并单 workflow)
|
||||
3. 改造 `infra/docker-compose.deploy.yml`(build 替代 image)
|
||||
4. 创建 `infra/docker-compose.tools.yml`(预拉镜像)
|
||||
5. 更新 `.trae/rules/project_rules.md` §15(no-push 模式规范)
|
||||
6. 更新 `docs/standards/cicd-runbook.md`(删除 registry 章节,新增本地构建章节)
|
||||
7. 提交并推送
|
||||
|
||||
## 4. 与旧方案对比
|
||||
|
||||
| 维度 | 旧方案(push 到 registry) | 新方案(no-push 本地构建) |
|
||||
| --------------- | -------------------------- | ------------------------------ |
|
||||
| workflow 文件数 | 4 个 | 1 个 |
|
||||
| 镜像推送 | push 到 Gitea Registry | 不推送 |
|
||||
| 部署方式 | compose pull + up | compose up --build |
|
||||
| 自建镜像 | 依赖 node-with-docker:22 | 全用官方镜像 |
|
||||
| 回滚 | 切换 registry tag | git revert / workflow_dispatch |
|
||||
| 构建产物保留 | registry 保留历史 | 不保留 |
|
||||
|
||||
## 5. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| ------------------------- | ---------------------------------------------- |
|
||||
| 本地镜像被清理后无法回滚 | 回滚走 git revert + 重新 build,不依赖镜像缓存 |
|
||||
| docker.sock 挂载安全风险 | actrunner 仅在部署服务器运行,已隔离 |
|
||||
| 构建慢 | layer cache 在宿主机本地,未变更的层秒过 |
|
||||
| Gitea workflow_run 不支持 | 用 needs 串联,不用 workflow_run |
|
||||
24
docs/troubleshooting/known-issues-p6-addendum.md
Normal file
24
docs/troubleshooting/known-issues-p6-addendum.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# known-issues P6 补丁
|
||||
|
||||
> 本文件为 `docs/troubleshooting/known-issues.md` 的 P6 阶段补丁,列出 P6 新增的"场景→技术"映射。
|
||||
> 合并方式:将下表条目追加到原文件对应分区,遵循索引式速查规范,不写代码示例。
|
||||
|
||||
## P6 生产硬化新增场景
|
||||
|
||||
| 场景 | 技术方案 |
|
||||
|------|----------|
|
||||
| 熔断器状态切换(Closed/Open/Half-Open) | gobreaker v2 ReadyToTrip 回调按连续失败数 + 失败率判定 |
|
||||
| 限流桶按租户维度清理 | sync.Map + ticker 周期清理过期桶,避免内存泄漏 |
|
||||
| 备份脚本定时调度 | K8s CronJob + backup-cron.sh,15min 一次满足 RPO |
|
||||
| PostgreSQL WAL 归档恢复到时间点 | pg_receivewal 归档 + PITR 恢复 |
|
||||
| Redis 增量备份 | BGSAVE 触发 + RDB 文件上传对象存储 |
|
||||
| Kafka 消费位点快照 | __consumer_offsets topic dump 到对象存储 |
|
||||
| 熔断器半开态试探限流 | MaxRequests 限制并发试探,避免恢复期二次过载 |
|
||||
| 健康探针分流 liveness/readiness | /healthz 仅进程存活,/readyz 检查依赖,避免滚动重启雪崩 |
|
||||
| 优雅停机等待 in-flight 请求 | app.enableShutdownHooks + terminationGracePeriodSeconds=60 |
|
||||
| Kafka producer 关闭前 flush | producer.disconnect() 内部 flush,避免消息丢失 |
|
||||
| 数据源销毁顺序 | 先 Kafka、再 Redis、最后 DB,避免反向依赖阻塞 |
|
||||
| 混沌实验回滚 | rollback.sh 按实验名清理 tc/iptables 规则 |
|
||||
| DNS 切换消除缓存 | TTL=60s + 等待 + 客户端清缓存,避免切换无效 |
|
||||
| 告警抑制去重 | Alertmanager inhibit + group_by + group_wait |
|
||||
| Python 服务就绪检查延迟初始化 | readyz 返回 ok + TODO,避免启动期依赖未就绪导致探针失败 |
|
||||
@@ -1,7 +1,7 @@
|
||||
# 已知问题速查
|
||||
|
||||
> 索引式速查手册:场景 → 技术/规则映射。不写代码示例。
|
||||
> 架构规则见 [../architecture/004_architecture_impact_map.md](../architecture/004_architecture_impact_map.md) 与 [../../project_rules.md](../../project_rules.md)
|
||||
> 架构规则见 [../architecture/004_architecture_impact_map.md](../architecture/004_architecture_impact_map.md) 与 [../../.trae/rules/project_rules.md](../../.trae/rules/project_rules.md)
|
||||
> 工作经验日志按时间倒序追加(50 条上限),AI 发现更好方案时可更新本节。
|
||||
|
||||
---
|
||||
@@ -10,140 +10,158 @@
|
||||
|
||||
### 1.1 多语言 monorepo 配置
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
|
||||
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
|
||||
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/*、services/classes、bff/*、apps/*、scripts/*),Go/Python 不入 |
|
||||
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
|
||||
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
|
||||
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
|
||||
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 |
|
||||
| husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits |
|
||||
| .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
|
||||
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
|
||||
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/_、services/classes、bff/_、apps/_、scripts/_),Go/Python 不入 |
|
||||
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
|
||||
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
|
||||
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
|
||||
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 |
|
||||
| husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits |
|
||||
| .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 |
|
||||
| ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`(flat config),lint 脚本去掉 `--ext .ts`,lint-staged 恢复 `eslint --fix` |
|
||||
| next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json` 时 `next lint` 触发 Strict/Base 选择提示,CI 中需预置配置或改 `eslint src` |
|
||||
|
||||
### 1.2 Docker Compose 基础设施
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| 日常开发启动 | 用 `docker-compose.minimal.yml` 仅起 MySQL+Redis |
|
||||
| 全量启动内存不足 | 按 `profiles` 分阶段启用(full/kafka/cdc/analytics/graph/search/config/observability) |
|
||||
| 每服务 mem_limit | 避免单服务吃满内存:MySQL 512m、Redis 128m、Kafka 512m、ClickHouse 1g |
|
||||
| 按阶段启用容器 | P1 仅 MySQL+Redis;P3 加 Kafka+Zookeeper;P4 加 Debezium+CH+Neo4j;P5 加 ES;P6 加 Consul+Istio |
|
||||
| MySQL 初始化 | `init-sql/01-init.sql` 挂载到 `/docker-entrypoint-initdb.d:ro` |
|
||||
| healthcheck | MySQL 用 `mysqladmin ping`,Redis 用 `redis-cli ping` |
|
||||
| Windows 下卷挂载 | init-sql 用绝对路径或确保相对路径正确 |
|
||||
| 容器名固定 | `container_name: edu-mysql` 便于服务连接配置 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| 日常开发启动 | 用 `docker-compose.minimal.yml` 仅起 MySQL+Redis |
|
||||
| 全量启动内存不足 | 按 `profiles` 分阶段启用(full/kafka/cdc/analytics/graph/search/config/observability) |
|
||||
| 每服务 mem_limit | 避免单服务吃满内存:MySQL 512m、Redis 128m、Kafka 512m、ClickHouse 1g |
|
||||
| 按阶段启用容器 | P1 仅 MySQL+Redis;P3 加 Kafka+Zookeeper;P4 加 Debezium+CH+Neo4j;P5 加 ES;P6 加 Consul+Istio |
|
||||
| MySQL 初始化 | `init-sql/01-init.sql` 挂载到 `/docker-entrypoint-initdb.d:ro` |
|
||||
| healthcheck | MySQL 用 `mysqladmin ping`,Redis 用 `redis-cli ping` |
|
||||
| Windows 下卷挂载 | init-sql 用绝对路径或确保相对路径正确 |
|
||||
| 容器名固定 | `container_name: edu-mysql` 便于服务连接配置 |
|
||||
| Kafka 双 listener | INSIDE (kafka:29092) 容器间互访 + OUTSIDE (localhost:9092) 主机访问,避免 Debezium 拿到 localhost metadata 后切回连不上 |
|
||||
| ClickHouse 远程访问 | 默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 `clickhouse/users.d/custom-users.xml` 覆盖密码+任意 IP |
|
||||
| Debezium Connect 镜像源 | daocloud 禁用 debezium/*,用 `quay.io/debezium/connect:2.7` 替代 |
|
||||
| Debezium 跨网络访问 MySQL | MySQL 容器在 edu-minimal_default 时,`docker network connect edu-full_default edu-mysql` 让 Debezium 同时可达 |
|
||||
| CDC 注册 connector | POST `:8083/connectors`,配置 `topic.prefix`/`database.include.list`/`schema.history.internal.kafka.topic` |
|
||||
|
||||
### 1.3 protobuf + buf 契约
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 契约唯一源 | `packages/shared-proto/proto/*.proto`,禁止 REST/gRPC 混用 |
|
||||
| breaking change 检测 | `buf breaking --against '.git#branch=main'` CI 强制,必须升版本号 |
|
||||
| proto 版本化 | 包名带版本 `xxx.v1`、`xxx.v2`,新旧版本共存 |
|
||||
| buf lint | `buf lint` 使用 STANDARD 规则集,可按需 except(如 PACKAGE_VERSION_SUFFIX) |
|
||||
| 三端代码生成 | `buf.gen.yaml` 配置 TS/Go/Python 插件,`pnpm proto:gen` 统一生成 |
|
||||
| 契约先行硬约束 | 任何服务间通信必须先改 proto,CI 检测 breaking change 阻止违规合并 |
|
||||
| 事件 schema 版本化 | Kafka 事件带 `schema_version` 字段,消费端按版本处理 |
|
||||
| 错误码定义在 proto | 错误码三端共享,格式 `{服务前缀}_{错误类型}_{资源}`(如 `IAM_PERM_USER`) |
|
||||
| 场景 | 技术/规则 |
|
||||
| -------------------- | --------------------------------------------------------------------------- |
|
||||
| 契约唯一源 | `packages/shared-proto/proto/*.proto`,禁止 REST/gRPC 混用 |
|
||||
| breaking change 检测 | `buf breaking --against '.git#branch=main'` CI 强制,必须升版本号 |
|
||||
| proto 版本化 | 包名带版本 `xxx.v1`、`xxx.v2`,新旧版本共存 |
|
||||
| buf lint | `buf lint` 使用 STANDARD 规则集,可按需 except(如 PACKAGE_VERSION_SUFFIX) |
|
||||
| 三端代码生成 | `buf.gen.yaml` 配置 TS/Go/Python 插件,`pnpm proto:gen` 统一生成 |
|
||||
| 契约先行硬约束 | 任何服务间通信必须先改 proto,CI 检测 breaking change 阻止违规合并 |
|
||||
| 事件 schema 版本化 | Kafka 事件带 `schema_version` 字段,消费端按版本处理 |
|
||||
| 错误码定义在 proto | 错误码三端共享,格式 `{服务前缀}_{错误类型}_{资源}`(如 `IAM_PERM_USER`) |
|
||||
|
||||
### 1.4 NestJS 服务开发(TS)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| 服务标准结构 | `src/{main.ts,app.module.ts,config,middleware,<domain>,shared,generated}` 黄金模板复制 |
|
||||
| 三层配置 | env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul);每服务启动时合并 |
|
||||
| 环境变量校验 | Zod schema 校验 process.env,失败立即抛错终止启动 |
|
||||
| ApplicationError 基类 | 统一错误体系:ValidationError(400)/NotFoundError(404)/PermissionDeniedError(403)/ConflictError(409) 等 |
|
||||
| 响应信封 | `{ success: boolean, data?: T, error?: { code, message, details, traceId } }` 统一跨服务 |
|
||||
| 权限校验 | `requirePermission(perm)` 装饰器/中间件,admin 角色拥有全部权限 |
|
||||
| DataScope 过滤 | 6 级(all/grade_managed/class_taught/class_members/children/owned),repository 下推 WHERE 条件 |
|
||||
| 结构化日志 | pino + `traceId`/`spanId`/`userId`/`service` 字段,禁止 `console.*` |
|
||||
| Metrics endpoint | prom-client 暴露 `/metrics`(QPS/延迟/错误率) |
|
||||
| OpenTelemetry trace | OTel SDK 初始化,W3C TraceContext header 跨服务传播 |
|
||||
| 健康检查 | `/health` endpoint 返回 `{ status, service, version }` |
|
||||
| 优雅关闭 | SIGTERM → app.close() → shutdownTracing() → process.exit(0) |
|
||||
| i18n 错误码映射 | 错误码 → i18n key 映射表,后端返回 key + 参数,前端翻译 |
|
||||
| Drizzle ORM | 参数化查询,禁止字符串拼接 SQL |
|
||||
| Zod 输入校验 | controller 层 `schema.safeParse(body)`,失败抛 ValidationError |
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| 服务标准结构 | `src/{main.ts,app.module.ts,config,middleware,<domain>,shared,generated}` 黄金模板复制 |
|
||||
| 三层配置 | env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul);每服务启动时合并 |
|
||||
| 环境变量校验 | Zod schema 校验 process.env,失败立即抛错终止启动 |
|
||||
| ApplicationError 基类 | 统一错误体系:ValidationError(400)/NotFoundError(404)/PermissionDeniedError(403)/ConflictError(409) 等 |
|
||||
| 响应信封 | `{ success: boolean, data?: T, error?: { code, message, details, traceId } }` 统一跨服务 |
|
||||
| 权限校验 | `requirePermission(perm)` 装饰器/中间件,admin 角色拥有全部权限 |
|
||||
| DataScope 过滤 | 6 级(all/grade_managed/class_taught/class_members/children/owned),repository 下推 WHERE 条件 |
|
||||
| 结构化日志 | pino + `traceId`/`spanId`/`userId`/`service` 字段,禁止 `console.*` |
|
||||
| Metrics endpoint | prom-client 暴露 `/metrics`(QPS/延迟/错误率) |
|
||||
| OpenTelemetry trace | OTel SDK 初始化,W3C TraceContext header 跨服务传播 |
|
||||
| 健康检查 | `/health` endpoint 返回 `{ status, service, version }` |
|
||||
| 优雅关闭 | SIGTERM → app.close() → shutdownTracing() → process.exit(0) |
|
||||
| i18n 错误码映射 | 错误码 → i18n key 映射表,后端返回 key + 参数,前端翻译 |
|
||||
| Drizzle ORM | 参数化查询,禁止字符串拼接 SQL |
|
||||
| Zod 输入校验 | controller 层 `schema.safeParse(body)`,失败抛 ValidationError |
|
||||
|
||||
### 1.5 Go Gateway 开发
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) |
|
||||
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
|
||||
| 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 |
|
||||
| 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` |
|
||||
| 包结构 | `internal/{config,middleware,proxy}`,P6 扩展 handler/service/repository |
|
||||
| error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 |
|
||||
| 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`),P6 引入 viper |
|
||||
| Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------- | --------------------------------------------------------------------------------------- |
|
||||
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) |
|
||||
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
|
||||
| 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 |
|
||||
| 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` |
|
||||
| 包结构 | `internal/{config,middleware,proxy}`,P6 扩展 handler/service/repository |
|
||||
| error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 |
|
||||
| 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`),P6 引入 viper |
|
||||
| Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0 |
|
||||
|
||||
### 1.6 可观测性(OTel + Prometheus + Loki)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) |
|
||||
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
|
||||
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
|
||||
| P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) |
|
||||
| Prometheus 指标 | `http_request_duration_seconds`(Histogram)+ `http_requests_total`(Counter) |
|
||||
| 采样策略 | P1 全量 trace,P6 引入采样率降低开销 |
|
||||
| 日志参数顺序 | `log.error({ err: error, userId, traceId }, "操作描述")`,错误对象字段名用 `err` |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) |
|
||||
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
|
||||
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
|
||||
| P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) |
|
||||
| Prometheus 指标 | `http_request_duration_seconds`(Histogram)+ `http_requests_total`(Counter) |
|
||||
| 采样策略 | P1 全量 trace,P6 引入采样率降低开销 |
|
||||
| 日志参数顺序 | `log.error({ err: error, userId, traceId }, "操作描述")`,错误对象字段名用 `err` |
|
||||
| P6 /metrics 端点 | NestJS 用 `app.getHttpAdapter().get('/metrics', handler)`,不能用 `app.get()`(会被解析为 DI 容器 get) |
|
||||
| P6 prometheus.yml | 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身;rule_files 引用 rules.yml;alerting 关联 alertmanager |
|
||||
| P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus(默认)和 Loki(uid=loki) |
|
||||
| P6 Promtail 采集 | docker_sd_configs + relabel_configs 仅采集 `edu-*` 前缀容器日志,避免无关日志 |
|
||||
| P6 service label | static_configs.labels.service 给所有抓取目标打服务标签,告警规则按 service 聚合 |
|
||||
| P6 collectDefaultMetrics | prom-client 的 `collectDefaultMetrics({ register })` 自动收集进程级指标(CPU/内存/事件循环/GC),无需业务埋点即可让 /metrics 有数据 |
|
||||
| P6 Counter/Histogram 埋点缺失 | metrics.ts 定义了 Counter/Histogram 但 service/controller 未调用 `.inc()`/`.observe()`,需后续补 HTTP 中间件自动埋点或业务埋点 |
|
||||
| P6 OTel instrumentations 缺失 | tracer.ts 只配置 traceExporter 未注册 auto-instrumentations(HttpInstrumentation/ExpressInstrumentation),导致 Jaeger 收不到业务 trace,需后续补 `@opentelemetry/auto-instrumentations`。**已补全**:NestJS 6 服务用 `getNodeAutoInstrumentations()`;Python 2 服务用 `FastAPIInstrumentor.instrument_app(app)`;Go 2 服务用 `otelgin.Middleware()` |
|
||||
| P6 镜像源配置 | 国内 docker.io 被墙,compose image 必须加 `docker.m.daocloud.io/` 前缀;Elastic 官方镜像在 docker.elastic.co 不被墙 |
|
||||
| P6 Grafana 端口冲突 | Grafana 默认 3000 与 teacher-portal Next.js dev 冲突,改映射为 3030:3000 |
|
||||
| P6 compose --no-deps | mysql/redis 已在另一 compose 项目运行时,启动新服务用 `--no-deps` + 显式指定服务名,避免重建依赖容器 | |
|
||||
|
||||
### 1.7 微前端 Module Federation
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| 4 个稳定 portal | teacher-portal / student-portal / parent-app / admin-console,按场景域划分 |
|
||||
| P1 测试页 | teacher-portal 仅一个测试页验证 classes CRUD 链路,P2 起配 Module Federation |
|
||||
| 视口驱动渲染 | 侧边栏由 `viewports.L1` 驱动渲染,路由由 `viewports.L2` 控制 |
|
||||
| 共享翻译资源包 | next-intl 微前端共享,BFF/服务返回 i18n key 不返回翻译文本 |
|
||||
| Module Federation 配置 | P2 起在 next.config.js 配置,P1 用单一 Next.js 应用 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------------- | ---------------------------------------------------------------------------- |
|
||||
| 4 个稳定 portal | teacher-portal / student-portal / parent-app / admin-console,按场景域划分 |
|
||||
| P1 测试页 | teacher-portal 仅一个测试页验证 classes CRUD 链路,P2 起配 Module Federation |
|
||||
| 视口驱动渲染 | 侧边栏由 `viewports.L1` 驱动渲染,路由由 `viewports.L2` 控制 |
|
||||
| 共享翻译资源包 | next-intl 微前端共享,BFF/服务返回 i18n key 不返回翻译文本 |
|
||||
| Module Federation 配置 | P2 起在 next.config.js 配置,P1 用单一 Next.js 应用 |
|
||||
|
||||
### 1.8 从旧项目迁移的通用经验
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| React 19 乐观更新 | `useOptimistic` 替代手动 isPending,配合 `useTransition` 自动管理回滚 |
|
||||
| Zustand 细粒度选择器 | 单字段 selector 优于 `useShallow` 多字段包装 |
|
||||
| Tiptap SSR | 必须 `immediatelyRender: false` 避免 hydration mismatch |
|
||||
| 请求级去重 | React `cache()` 包装优于跨请求缓存(权限数据易变,不用 `unstable_cache`) |
|
||||
| 批量 SQL | INSERT batch `db.insert().values([...])` + UPDATE `CASE WHEN` 单次执行,禁止循环单条 |
|
||||
| 动态导入重 UI 库 | `xxx-inner.tsx` + `xxx.tsx` lazy wrapper(tiptap / @xyflow/react / AI SDK) |
|
||||
| ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 |
|
||||
| 递归删除 | 批量收集后代 ID + `inArray` 单次删除,禁止递归逐个 |
|
||||
| 统计走 SQL 聚合 | `COUNT(*)`/`SUM(CASE WHEN ...)` 替代拉全表内存循环 |
|
||||
| 高频子组件 React.memo | `AssignmentCard` / `StatusBadge` / `EmptyState` 等 |
|
||||
| 虚拟化大表 | `@tanstack/react-virtual` |
|
||||
| 破坏性操作独立权限 | `AUDIT_LOG_PURGE` ≠ `AUDIT_LOG_READ`,沿用旧项目规则 |
|
||||
| i18n key 命名 | 禁止包含 `.`,动态 key 必须包含完整嵌套路径(`t(\`type.${type}\`)`) |
|
||||
| i18n 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key |
|
||||
| 类型守卫优先 | JSON.parse 结果经 `unknown` 中转 + 守卫;禁止 `as` 断言(除非从 `unknown` 转换) |
|
||||
| 函数返回值显式标注 | 特别是 `Promise<T>` |
|
||||
| 仅类型导入 | `import type` |
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------ |
|
||||
| React 19 乐观更新 | `useOptimistic` 替代手动 isPending,配合 `useTransition` 自动管理回滚 |
|
||||
| Zustand 细粒度选择器 | 单字段 selector 优于 `useShallow` 多字段包装 |
|
||||
| Tiptap SSR | 必须 `immediatelyRender: false` 避免 hydration mismatch |
|
||||
| 请求级去重 | React `cache()` 包装优于跨请求缓存(权限数据易变,不用 `unstable_cache`) |
|
||||
| 批量 SQL | INSERT batch `db.insert().values([...])` + UPDATE `CASE WHEN` 单次执行,禁止循环单条 |
|
||||
| 动态导入重 UI 库 | `xxx-inner.tsx` + `xxx.tsx` lazy wrapper(tiptap / @xyflow/react / AI SDK) |
|
||||
| ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 |
|
||||
| 递归删除 | 批量收集后代 ID + `inArray` 单次删除,禁止递归逐个 |
|
||||
| 统计走 SQL 聚合 | `COUNT(*)`/`SUM(CASE WHEN ...)` 替代拉全表内存循环 |
|
||||
| 高频子组件 React.memo | `AssignmentCard` / `StatusBadge` / `EmptyState` 等 |
|
||||
| 虚拟化大表 | `@tanstack/react-virtual` |
|
||||
| 破坏性操作独立权限 | `AUDIT_LOG_PURGE` ≠ `AUDIT_LOG_READ`,沿用旧项目规则 |
|
||||
| i18n key 命名 | 禁止包含 `.`,动态 key 必须包含完整嵌套路径(`t(\`type.${type}\`)`) |
|
||||
| i18n 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key |
|
||||
| 类型守卫优先 | JSON.parse 结果经 `unknown` 中转 + 守卫;禁止 `as` 断言(除非从 `unknown` 转换) |
|
||||
| 函数返回值显式标注 | 特别是 `Promise<T>` |
|
||||
| 仅类型导入 | `import type` |
|
||||
|
||||
### 1.9 架构工具与验证命令
|
||||
|
||||
| 场景 | 命令/规则 |
|
||||
| --------------------- | ---------------------------------------------------------------------- |
|
||||
| arch.db 扫描 | `npm run arch:scan`(多语言:TS+Go+Python) |
|
||||
| arch.db 查询 | `npm run arch:query -- <command>` |
|
||||
| 查服务依赖 | `npm run arch:query -- service <service>` |
|
||||
| 查 proto 契约 | `npm run arch:query -- contracts` |
|
||||
| 查 Kafka 事件 | `npm run arch:query -- events` |
|
||||
| 查架构违规 | `npm run arch:query -- violations`(输出 0 为合格) |
|
||||
| proto lint | `cd packages/shared-proto && pnpm exec buf lint` |
|
||||
| proto breaking 检测 | `pnpm exec buf breaking --against '.git#branch=main'` |
|
||||
| proto 代码生成 | `pnpm proto:gen` |
|
||||
| Go 编译验证 | `cd services/api-gateway && go build ./... && go vet ./...` |
|
||||
| TS 类型检查 | `pnpm -r exec tsc --noEmit` |
|
||||
| 全量 lint | `pnpm lint` |
|
||||
| 全量测试 | `pnpm test` |
|
||||
| 启动最小基础设施 | `docker compose -f infra/docker-compose.minimal.yml up -d` |
|
||||
| 场景 | 命令/规则 |
|
||||
| ------------------- | ----------------------------------------------------------- |
|
||||
| arch.db 扫描 | `pnpm run arch:scan`(多语言:TS+Go+Python+Proto) |
|
||||
| arch.db 查询 | `pnpm run arch:query -- <command>` |
|
||||
| 查服务依赖 | `pnpm run arch:query -- deps <module>` |
|
||||
| 查 proto 契约 | `pnpm run arch:query -- sql "SELECT * FROM contracts"` |
|
||||
| 查 Kafka 事件 | `pnpm run arch:query -- sql "SELECT * FROM events"` |
|
||||
| 查架构违规 | `pnpm run arch:query -- violations`(骨架,P1 后期补全) |
|
||||
| proto lint | `cd packages/shared-proto && pnpm exec buf lint` |
|
||||
| proto breaking 检测 | `pnpm exec buf breaking --against '.git#branch=main'` |
|
||||
| proto 代码生成 | `pnpm proto:gen` |
|
||||
| Go 编译验证 | `cd services/api-gateway && go build ./... && go vet ./...` |
|
||||
| TS 类型检查 | `pnpm -r exec tsc --noEmit` |
|
||||
| 全量 lint | `pnpm lint` |
|
||||
| 全量测试 | `pnpm test` |
|
||||
| 启动最小基础设施 | `docker compose -f infra/docker-compose.minimal.yml up -d` |
|
||||
|
||||
---
|
||||
|
||||
@@ -151,138 +169,188 @@
|
||||
|
||||
### 2.1 api-gateway(Go)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT,`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 |
|
||||
| P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM |
|
||||
| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` |
|
||||
| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务 |
|
||||
| 用户上下文注入 | `c.Request.Header.Set("x-user-id", claims.UserID)` 传递给下游 |
|
||||
| 请求 ID | Gateway 生成或透传 `X-Request-ID`,`c.Set("request_id", ...)` + `c.Header(...)` |
|
||||
| P1 不做 | 限流/熔断/灰度(P6 硬化阶段实现) |
|
||||
| 健康检查 | `GET /health` 无需鉴权 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT,`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 |
|
||||
| P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM |
|
||||
| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` |
|
||||
| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务 |
|
||||
| 用户上下文注入 | `c.Request.Header.Set("x-user-id", claims.UserID)` 传递给下游 |
|
||||
| 请求 ID | Gateway 生成或透传 `X-Request-ID`,`c.Set("request_id", ...)` + `c.Header(...)` |
|
||||
| P1 不做 | 限流/熔断/灰度(P6 硬化阶段实现) |
|
||||
| 健康检查 | `GET /health` 无需鉴权 |
|
||||
| 尾斜杠重定向循环 | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")` |
|
||||
| 开发模式鉴权旁路 | `DEV_MODE=true` 时接受 `Bearer dev-token`,注入固定身份;生产必须 `false` |
|
||||
| 路由注册位置 | 真实路由在 `main.go` 的 `api.Group` 内注册,`internal/routing/routing.go` 若未被 main 引用即为死代码 |
|
||||
| 多服务路由扩展 | 新增服务代理时在 main.go 注册两组路由:`Any("/x")` + `Any("/x/*path")`,与 classes 一致 |
|
||||
| DEV_MODE 环境变量 | Go 不自动加载 .env,`DEV_MODE` 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒 |
|
||||
|
||||
### 2.2 classes(TS/NestJS,P1 黄金模板)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
|
||||
| 黄金模板复制流程 | `cp -r services/classes services/xxx` → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
|
||||
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
|
||||
| 错误处理 | `ApplicationError` 基类 + 子类,错误码 `CLASSES_*` 前缀 |
|
||||
| 可观测 | pino logger + prom-client metrics + OTel tracer |
|
||||
| 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor |
|
||||
| 配置 | 三层配置 + Zod 校验 env |
|
||||
| i18n | `ERROR_CODES` 映射表(错误码 → i18n key) |
|
||||
| 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) |
|
||||
| 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% |
|
||||
| Drizzle schema | `mysqlTable` + `varchar`/`timestamp` + `index` |
|
||||
| ID 生成 | `@paralleldrive/cuid2` 的 `createId()` |
|
||||
| 响应转换 | repository 返回 Date,service 转换为 `createdAt: number`(时间戳) |
|
||||
| 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README |
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
|
||||
| 黄金模板复制流程 | `cp -r services/classes services/xxx` → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
|
||||
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
|
||||
| 错误处理 | `ApplicationError` 基类 + 子类,错误码 `CLASSES_*` 前缀 |
|
||||
| 可观测 | pino logger + prom-client metrics + OTel tracer |
|
||||
| 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor |
|
||||
| 配置 | 三层配置 + Zod 校验 env |
|
||||
| i18n | `ERROR_CODES` 映射表(错误码 → i18n key) |
|
||||
| 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) |
|
||||
| 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% |
|
||||
| Drizzle schema | `mysqlTable` + `varchar`/`timestamp` + `index` |
|
||||
| ID 生成 | `@paralleldrive/cuid2` 的 `createId()` |
|
||||
| 响应转换 | repository 返回 Date,service 转换为 `createdAt: number`(时间戳) |
|
||||
| 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README |
|
||||
| 健康检查依赖 | `readyz` 用 Drizzle `getDb().execute(sql\`SELECT 1\`)` 校验,不要依赖 typeorm DataSource DI |
|
||||
| AppModule 注册 | HealthModule 必须在 `app.module.ts` imports 数组显式声明,否则 NestFactory 不扫描 HealthController |
|
||||
| 增量编译陷阱 | `tsconfig.json` 显式 `"incremental": false` 覆盖 base,避免 .tsbuildinfo 导致 nest watch 不 emit |
|
||||
|
||||
### 2.3 iam(TS/NestJS,P2)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 认证 | 登录/登出/JWT/2FA,RS256 非对称签名 |
|
||||
| RBAC | 角色/权限/角色-权限 CRUD + `getEffectivePermissions(userId)` API |
|
||||
| 视口配置 | 4 层模型(导航/路由/组件/数据),`role_viewports` 表 |
|
||||
| DataScope 解析 | 6 级数据范围,注入 JWT payload |
|
||||
| JWT payload | `{ userId, roles, permissions(bitmap), dataScope, exp }` |
|
||||
| Token TTL | access 15min / refresh 7day,refresh 用 Redis 黑名单失效 |
|
||||
| 权限缓存 | `getEffectivePermissions` 结果 Redis 缓存 TTL 5 分钟,角色变更主动失效 |
|
||||
| schema 表 | users / roles / permissions / role_permissions / role_viewports / parent_student_relations / class_subject_teachers |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 认证 | 登录/登出/JWT/2FA,RS256 非对称签名 |
|
||||
| RBAC | 角色/权限/角色-权限 CRUD + `getEffectivePermissions(userId)` API |
|
||||
| 视口配置 | 4 层模型(导航/路由/组件/数据),`role_viewports` 表 |
|
||||
| DataScope 解析 | 6 级数据范围,注入 JWT payload |
|
||||
| JWT payload | `{ userId, roles, permissions(bitmap), dataScope, exp }` |
|
||||
| Token TTL | access 15min / refresh 7day,refresh 用 Redis 黑名单失效 |
|
||||
| 权限缓存 | `getEffectivePermissions` 结果 Redis 缓存 TTL 5 分钟,角色变更主动失效 |
|
||||
| schema 表 | users / roles / permissions / role_permissions / role_viewports / parent_student_relations / class_subject_teachers |
|
||||
| ESM 模式 DI | `providers: [IamService, IamRepository]` + 构造器 `@Inject(IamRepository)` 显式注入,避免 `undefined` 运行时错误 |
|
||||
| Drizzle ORM API | `inArray(col, vals)` 替代不存在的 `.in()`;select 返回字段名按 schema 定义(如 `r.iam_roles` 而非 `r.roles`) |
|
||||
| 健康检查依赖 | `readyz` 用 `db.execute(sql\`SELECT 1\`)` 校验连接,不要依赖 typeorm DataSource(IAM 用 Drizzle,无 typeorm) |
|
||||
| Gateway 身份传递 | Controller 直接读 `req.headers['x-user-id']` / `x-user-roles`,不要依赖未注册的 AuthMiddleware 的 `AuthenticatedRequest` |
|
||||
| DEV_MODE 登录 | DEV_MODE=true 时 Gateway 接受 `Bearer dev-token` 注入固定身份,IAM 仍支持真实 JWT(HS256,P2 应改 RS256) |
|
||||
| P2 公开路径白名单 | Gateway `publicPaths` map 含 `/iam/register`/`/iam/login`/`/iam/refresh`,AuthMiddleware 跳过鉴权避免死锁 |
|
||||
| P2 视口过滤 | `getUserViewports` 按 `requiredPermission` 过滤 + `sortOrder` 字典序排序,无权限要求的视口全员可见 |
|
||||
| P2 JWT payload | HS256 签名含 `sub/email/roles/dataScope/type`,register 自动分配 teacher 角色(TEACHER_ROLE_ID 固定 UUID) |
|
||||
|
||||
### 2.4 core-edu(TS/NestJS,P3)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 |
|
||||
| Outbox 模式 | 业务事务同写 `outbox_events` 表,后台 relay worker 投递 Kafka |
|
||||
| Outbox relay | Go 写独立服务 `services/outbox-relay/`,轻量高吞吐 |
|
||||
| Kafka topic | `exam.published` / `homework.graded` / `grade.recorded` |
|
||||
| 不引入 Saga | 跨服务一致性用 Outbox + 最终一致性 |
|
||||
| 批改后联动 | 批改完成 → 发 `homework.graded` 事件 → 下游消费(DataAna/Msg) |
|
||||
| Temporal 试点 | 仅 1 个工作流(考试发布编排:创建作业→通知) |
|
||||
| schema 表 | exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events |
|
||||
| 场景 | 技术/规则 |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 |
|
||||
| Outbox 模式 | 业务事务同写 `outbox_events` 表,后台 relay worker 投递 Kafka |
|
||||
| Outbox relay | Go 写独立服务 `services/outbox-relay/`,轻量高吞吐 |
|
||||
| Kafka topic | `exam.published` / `homework.graded` / `grade.recorded` |
|
||||
| 不引入 Saga | 跨服务一致性用 Outbox + 最终一致性 |
|
||||
| 批改后联动 | 批改完成 → 发 `homework.graded` 事件 → 下游消费(DataAna/Msg) |
|
||||
| Temporal 试点 | 仅 1 个工作流(考试发布编排:创建作业→通知) |
|
||||
| schema 表 | exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events |
|
||||
| datetime 列 | Drizzle `datetime` 列需 Date 对象,HTTP 请求体里是 ISO 字符串,service 层必须 `new Date(input)` 转换否则报 `toISOString is not a function` |
|
||||
| Kafka 非阻塞 | 开发环境 Kafka 未启动时 `connectKafka()` 必须 try/catch 且 main 中用 `void` 调用,否则阻塞服务启动 |
|
||||
| 相对 import | `src/<domain>/` 下文件回溯一级用 `../`,不要用 `../../`(NestJS ESM 模块) |
|
||||
| 身份头读取 | Controller 用 `@Req() req` 从 `req.headers['x-user-id']` 读 createdBy/gradedBy,不依赖未注册的 AuthMiddleware |
|
||||
| 健康检查 | health.controller 用 Drizzle `db.execute(sql\`SELECT 1\`)`,不用 typeorm DataSource |
|
||||
| Outbox 验证 | 业务事务同写 `core_edu_outbox` 表,Kafka 未启动时事件 status=failed,启动后会重试 |
|
||||
|
||||
### 2.5 content(TS/NestJS,P4)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) |
|
||||
| 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 |
|
||||
| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 |
|
||||
| Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j |
|
||||
| 场景 | 技术/规则 |
|
||||
| -------------- | ------------------------------------------------------------------------------- |
|
||||
| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) |
|
||||
| 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 |
|
||||
| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 |
|
||||
| Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j |
|
||||
| API 字段名 | 请求体用 TS schema 字段名(如 `order`),非 DB 列名(如 `order_num`) |
|
||||
| Neo4j 不可用 | 未设置 NEO4J_URL 时 driver=null,getNeo4jSession 返回 null,业务正常落库 MySQL |
|
||||
| Neo4j 连接超时 | driver 配置 connectionTimeout:3000,避免 Neo4j 不可用时拖慢 HTTP 响应 |
|
||||
| Drizzle int | drizzle-orm/mysql-core 导出 `int()` 不是 `integer()` |
|
||||
| db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数,与 core-edu/classes 黄金模板对齐 |
|
||||
|
||||
### 2.6 data-ana(Python/FastAPI,P4)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 学情诊断 | ClickHouse 宽表查询,5s 内返回 |
|
||||
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka(`mysql.cdc.*`)→ DataAna 消费写 ClickHouse |
|
||||
| CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 |
|
||||
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
|
||||
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
|
||||
| 场景 | 技术/规则 |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| 学情诊断 | ClickHouse 宽表查询,5s 内返回 |
|
||||
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka(`edu-cdc.next_edu_cloud.<table>`)→ DataAna 消费写 ClickHouse |
|
||||
| CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 |
|
||||
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
|
||||
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
|
||||
| CDC 消费者实现 | `cdc_consumer.py` 用 aiokafka AIOKafkaConsumer,lifespan 启动 asyncio.create_task 后台运行 |
|
||||
| ClickHouse 写入 | `clickhouse_client.upsert_student_dashboard()` 用 client.insert() 写宽表,client 为 None 时降级返回 False |
|
||||
| Debezium 事件解析 | `before/after/source/op/ts_ms` 五字段,op=r(快照)/c(新增)/u(更新)/d(删除) |
|
||||
| 多表关联缓存 | 内存 ExamCache 缓存 exam_id→class_id 映射(来自 core_edu_exams CDC 事件),grades 事件触发时查缓存填充宽表 class_id |
|
||||
| Consumer offset 重置 | `kafka-consumer-groups --reset-offsets --to-earliest --execute` 需先停消费者让 group 处于 Empty 状态 |
|
||||
| structlog API | 24.x 用 `make_filtering_bound_logger(level)`,旧版 `make_filtering_logger` 已废弃 |
|
||||
|
||||
### 2.7 messaging(TS/NestJS,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 |
|
||||
| 通知批量化 | `createNotifications(items)` 单次 INSERT,沿用旧项目 dispatcher 模式 |
|
||||
| 多渠道 | 站内/SMS/邮件/微信,in_app 批量 + 其他渠道并行 |
|
||||
| fan-out 分页 | `getAllUserIds(limit=1000, offset)` 分页遍历 |
|
||||
| 撤回不乐观更新 | 需服务端返回判断 2 分钟窗口 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------- | ------------------------------------------------------------------ |
|
||||
| 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 |
|
||||
| 通知批量化 | `createBatch(items)` 单次 INSERT,沿用旧项目 dispatcher 模式 |
|
||||
| 多渠道 | 站内/SMS/邮件/微信,in_app 批量 + 其他渠道并行 |
|
||||
| fan-out 分页 | `listByUserWithPagination(userId, page, pageSize)` 分页查询 |
|
||||
| ES 降级 | ES_URL 未设置时 esClient=null,safeIndex/safeSearch 跳过返回空结果 |
|
||||
| Push 推送降级 | PUSH_GATEWAY_URL 未设置或连接失败时 try/catch 跳过,不影响 DB 写入 |
|
||||
| db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数 |
|
||||
|
||||
### 2.8 push-gateway(Go,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需发 Kafka 消息 |
|
||||
| 跨实例同步 | Redis PubSub |
|
||||
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------- | ---------------------------------------------------------------- |
|
||||
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需调 /internal/push |
|
||||
| 跨实例同步 | Redis PubSub(RedisURL 配置,预留 P6 实现) |
|
||||
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
|
||||
| 并发写修复 | send chan + 单写协程模式,避免 gorilla/websocket 并发写竞争 |
|
||||
| DEV_MODE 鉴权 | DEV_MODE=true 时接受 dev-token,生产环境必须 JWT 校验 |
|
||||
| 广播端点 | POST /internal/broadcast,body {event, data},调用 hub.Broadcast |
|
||||
|
||||
### 2.9 ai-gateway(Python/FastAPI,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| LLM Provider 适配 | OpenAI/Anthropic,langchain/litellm 生态 |
|
||||
| Prompt 模板管理 | 版本管理友好 |
|
||||
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
|
||||
| 用量计费 | 按 token 计费 |
|
||||
| AI 模块纯服务端 | Zod 验证 + 失败降级返回空(沿用旧项目模式) |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------- | --------------------------------------------------------------- |
|
||||
| LLM Provider 适配 | OpenAI 兼容 REST API(httpx 异步),不引入 openai SDK |
|
||||
| 降级模式 | API key 为空或调用失败时返回骨架响应,标记 degraded: true |
|
||||
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
|
||||
| 路由前缀 | 业务路由加 /ai 前缀(APIRouter prefix="/ai"),Gateway 代理 /ai |
|
||||
| dev_mode tracer | dev_mode=true 时跳过 OTel exporter 初始化 |
|
||||
|
||||
### 2.10 shared-proto(契约包)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| 目录结构 | `proto/*.proto` + `buf.yaml` + `buf.gen.yaml` |
|
||||
| P1 契约 | 仅 `classes.proto`,`iam.proto`/`core_edu.proto` 占位 |
|
||||
| 代码生成输出 | TS → `shared-ts/generated`,Go → `shared-go/`,Python → `services/*/generated/` |
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------ | ------------------------------------------------------------------------------- |
|
||||
| 目录结构 | `proto/*.proto` + `buf.yaml` + `buf.gen.yaml` |
|
||||
| P1 契约 | 仅 `classes.proto`,`iam.proto`/`core_edu.proto` 占位 |
|
||||
| 代码生成输出 | TS → `shared-ts/generated`,Go → `shared-go/`,Python → `services/*/generated/` |
|
||||
|
||||
### 2.11 arch-scan(多语言扫描器)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| TS 扫描 | ts-morph 解析 AST,提取导出/函数/类/import |
|
||||
| Go 扫描 | P1 用正则提取(函数/类型/import),P2 起替换为 tree-sitter-go AST |
|
||||
| Python 扫描 | P1 用正则提取,P4 起替换为 tree-sitter-python AST |
|
||||
| arch.db schema | modules/symbols/dependencies/contracts/events/violations 六表 |
|
||||
| 全量扫描 | 先清空旧数据再扫描,避免残留 |
|
||||
| 并行扫描风险 | 并行子代理执行 arch:scan 可能因竞争报 FOREIGN KEY 错误,必须串行执行 |
|
||||
| 扫描后验证 | `npm run arch:query -- violations` 输出 0 为合格 |
|
||||
| Windows 路径 | 自定义 ESLint 规则加载用 `path.join`,不用 `path.posix.join` |
|
||||
| 场景 | 技术/规则 |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| TS 扫描 | regex 提取(function/class/interface/UPPER_CASE const),避免 ts-morph 对未安装依赖文件解析失败 |
|
||||
| Go 扫描 | 正则提取(行首锚定 `^func`/`^type`),P2 起替换为 tree-sitter-go AST |
|
||||
| Python 扫描 | 正则提取(行首锚定 `^def`/`^class`),P4 起替换为 tree-sitter-python AST |
|
||||
| arch.db schema | modules/symbols/dependencies/contracts/events/violations 六表 |
|
||||
| 全量扫描 | 先清空旧数据再扫描,避免残留 |
|
||||
| 并行扫描风险 | 并行子代理执行 arch:scan 可能因竞争报 FOREIGN KEY 错误,必须串行执行 |
|
||||
| 扫描后验证 | `npm run arch:query -- violations` 输出 0 为合格 |
|
||||
| Windows 路径 | 自定义 ESLint 规则加载用 `path.join`,不用 `path.posix.join` |
|
||||
|
||||
### 2.12 teacher-portal(微前端宿主,P1 测试页)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
|
||||
| API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` |
|
||||
| P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 IAM 签发 RS256 |
|
||||
| P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
|
||||
| API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` |
|
||||
| P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 IAM 签发 RS256 |
|
||||
| P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal |
|
||||
| P2 路由组 + AppShell | `app/(app)/layout.tsx` 用 AppShell 包裹受保护页;`/login` 与 `/` 不套壳 |
|
||||
| P2 真实 JWT | 登录后 token 存 localStorage,`authHeaders()` 读 `Bearer ${getToken()}` |
|
||||
| P2 视口驱动侧边栏 | AppShell fetch `/teacher/viewports` 渲染左侧导航,active 路由高亮 |
|
||||
| P2 根路径重定向 | `app/page.tsx` 客户端组件 `router.replace(isAuthenticated() ? '/dashboard' : '/login')` |
|
||||
| fetch headers 类型 | `authHeaders(): Record<string, string>` 显式标注,避免 `{}` 与 `HeadersInit` 不兼容 |
|
||||
| P2 前端权限校验等价物 | `usePermission().hasPermission()` Hook + `<RequirePermission>` 组件,镜像后端 `@RequirePermission()` 装饰器,禁止 `role === "xxx"` 硬编码(§3.1) |
|
||||
| P2 MF Shell+Remote 架构 | teacher-portal:3000 为 Shell,student:3001/parent:3002/admin:3003 为 Remote,共享登录态/布局/组件库/权限体系 |
|
||||
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error}` 解析 + 错误码前缀路由 i18n,替代页面级 `authHeaders()+fetch` 重复 |
|
||||
| P2 设计令牌三层模型 | primitive(原始色板)→ semantic-light/dark(语义)→ tailwind-theme(`@theme inline` 暴露 `bg-*`);ESLint `no-restricted-syntax` 禁 `#hex` + `design-tokens/no-hardcoded-fonts` 禁字面量字体(§3.10) |
|
||||
| P2 5 层状态管理 | nuqs(URL 同步)/ TanStack Query(服务端缓存)/ Zustand(客户端业务状态)/ Zustand UI(UI 临时态)/ react-hook-form(表单) |
|
||||
| P2 4 端错误码前缀对齐 | TP_=teacher-bff / SP_=student-bff / PP_=parent-bff / AP_=admin-bff,前端按前缀路由 i18n key |
|
||||
| P2 ErrorBoundary | 路由级 `<ErrorBoundary>` 包裹避免白屏,搭配 `<Suspense>` 流式加载 |
|
||||
|
||||
---
|
||||
|
||||
@@ -290,6 +358,32 @@
|
||||
|
||||
> 按时间倒序,50 条上限。AI 发现更好方案时可更新本节。
|
||||
|
||||
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|
||||
| ---------- | ----- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 2026-07-07 | 全天 | 全局 | 文档体系初始化:从旧项目(e:\Desktop\CICD,Next.js 单体)迁移 spec + plan + known-issues 模板到新仓库(e:\Desktop\Edu,微服务架构)。known-issues 重组为微服务分区:多语言 monorepo / Docker Compose / protobuf+buf / NestJS / Go Gateway / 可观测性 / 微前端。从旧项目提炼可迁移经验:React 19 useOptimistic / Zustand 细粒度选择器 / Tiptap SSR / 请求级去重 / 批量 SQL / 动态导入模式 / arch:scan 串行执行。新增微服务特有经验:契约先行 / Outbox / CDC / 双轨读 / DataScope / 黄金模板复制流程。路线图按 6 阶段组织:P1 地基 → P2 身份 → P3 核心教学 → P4 内容分析 → P5 沟通AI → P6 硬化。 |
|
||||
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|
||||
| ---------- | ---- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 2026-07-09 | 夜间 | data-ana/ai(ai06) | **ai06 阶段 1+2 交付(Python 双服务)**:(1) 按 ai-allocation.md §4 必读清单读完 7 份全局文档(README/MIGRATION_GUIDE/004/pending-features/project_rules/coding-standards/multi-ai-collaboration)+ classes 黄金模板源码(controller/service/repository/schema/dto)+ shared-proto 8 份 proto(iam/classes/core_edu/events/analytics/ai/msg/push)+ ai-allocation.md §5 ai06 分工(data-ana P4 + ai P5)+ data-ana/ai 现有全部源码(main.py/clickhouse_client.py/cdc_consumer.py/llm_client.py/config.py)。(2) 阶段 1 产出 docs/architecture/ai06-phase1-understanding.md:data-ana 模块理解确认书(架构定位 L5 业务服务/限界上下文 D6 智能洞察/契约 AnalyticsService 3RPC + 消费 6 CDC topic/技术栈 Python3.12+FastAPI+ClickHouse+aiokafka/P4 阶段归属)+ ai 模块理解确认书(架构定位 L5/限界上下文 D6/契约 AiService 4RPC 含 StreamChat/技术栈 Python+FastAPI+LLM REST/P5 阶段归属)+ §10 服务审计表(两服务均缺 gRPC server 实现/缺权限校验 Depends/data-ana 缺 mastery.updated 事件发布/ai 缺 Redis 限流)+ 8 项跨模块契约待 coord 仲裁。(3) 阶段 2 产出 docs/architecture/ai06-phase2-design.md:data-ana 设计含 Mermaid 内部分层图(Entry/Middleware/Service/Repo/Consumer/Storage 6 层)+ ClickHouse 4 张宽表 DDL(student_dashboard_view ReplacingMergeTree(last_updated)/student_errors/mastery_snapshot/ai_usage_log)+ HTTP+gRPC 双入口 API 设计 + 6 个 CDC topic 消费路由 + 发布 edu.insight.mastery.updated 事件 + DATA_ANA_* 错误码清单 + 10 项 metrics + 优雅关闭序列;ai 设计含 Mermaid 分层图(LLM Provider 适配器模式)+ 无 DB 状态less + 4 LLM Provider(OpenAI/Anthropic/Baichuan/Ollama)+ Prompt 模板注册表 + 出题 4 步编排 + AI_* 错误码 + Redis 令牌桶限流 + edu.insight.ai.usage 事件发布 + 9 项待 coord 仲裁决策。(4) **学到**:Python 服务权限校验等价 NestJS @RequirePermission 的是 FastAPI Depends 依赖注入 + DataScope 6 级过滤(SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL);ClickHouse ReplacingMergeTree 查询必须加 FINAL 关键字否则读到重复版本数据;Python 服务无 MySQL 写事务故不能用 Outbox 模式,派生数据事件(mastery.updated/ai.usage)建议直接 Kafka producer(已提请 coord 仲裁是否豁免 §12.2 Outbox 强制约束);ai 服务无 DB 设计(stateless)与 data-ana 有 ClickHouse(OLAP)形成对比,需在审计表区分标注;gRPC streaming(StreamChat)在 Python 用 grpc.aio + AsyncGenerator,与 TS @grpc/grpc-js 双向流实现差异需在阶段 3 实施时对照;ai-allocation.md §9.3 提交规范为 `docs(<service>): 模块架构设计文档`,三阶段强制经验沉淀到 known-issues.md 工作经验日志。 |
|
||||
| 2026-07-09 | 夜间 | teacher-portal/前端4端(ai07) | **ai07 阶段 1+2 交付(前端 4 端)**:(1) 按 ai-allocation.md §4 必读清单读完 7 份全局文档(README/MIGRATION_GUIDE/004/pending-features/project_rules/coding-standards/multi-ai-collaboration)+ classes 黄金模板源码(controller/service/repository/schema/dto)+ shared-proto 8 份 proto + ai-allocation.md §5 ai07 分工。(2) 运行 `pnpm run arch:scan` 更新 arch.db(10 TS/264 符号、2 Go/33 符号、2 Py/45 符号、138 proto 契约)。(3) 阶段 1 产出 4 端模块理解确认书(架构定位/限界上下文/契约依赖/技术栈/阶段归属/黄金模板对齐审计),合并到 apps/teacher-portal/README.md。(4) teacher-portal 现状审计 19 维度,发现 7 项高优违规:AppShell.tsx L143 硬编码 `user.roles.join(",")`(违反 §3.1 前端禁 role 硬编码)/ globals.css+tailwind.config.js 硬编码 `hsl()` 字面量与 `#hex`(违反 §3.10 设计令牌)/ layout.tsx 直接 import `'Inter'/'Fraunces'/'JetBrains Mono'` 字面量(违反 §3.10 禁硬编码字体)/ 缺统一 ApiClient 层(4 页面重复 local authHeaders()+fetch)/ 缺 usePermission Hook(前端无 @RequirePermission 等价物)/ 缺 ErrorBoundary(白屏风险)/ 缺 5 层状态管理(nuqs/TanStack Query/Zustand/Zustand-UI/react-hook-form 全缺)/ next.config.js 无 Module Federation 配置(4 端无法 Shell+Remote 组合)。(5) 阶段 2 产出模块架构设计文档:MF 2.0 Shell+Remote 架构(teacher-portal:3000 为 Shell,student:3001/parent:3002/admin:3003 为 Remote)含 mermaid 架构图 + MF config 代码示例 + 领域模型(Session/Viewport/Permission TS 接口)+ 数据模型缓存策略表(5 层 DataScope × 5 类缓存键)+ API 请求层设计(ApiClient 401 自动刷新 + ActionState 解析 + 错误码前缀路由 i18n)+ WebSocket/SSE 事件设计 + 横切关注点对齐清单(权限表 4 端前缀/错误码前缀 TP_/SP_/PP_/AP_/logger/metrics/tracer/health)。(6) 4 端差异化对照表 5 张(总体/L1 导航/L2 路由/L3 组件/L4 数据)。(7) 交互点契约清单 12 项 + 风险 7 项 + 假设 5 项 + 4 项待 coord 仲裁(packages 归属/GraphQL vs REST/i18n key 命名/MF 暴露粒度)。(8) coord 交叉审查信息:端口矩阵 3000-3003、5 个 shared 包待建(shared-ts/shared-tokens/shared-ui/shared-mf/shared-perm)、11 个后端契约依赖、错误码前缀对齐、无 Kafka 事件消费。**学到**:前端权限校验等价物是 `usePermission().hasPermission()` Hook + `<RequirePermission>` 组件(镜像后端 `@RequirePermission()` 装饰器);MF Shell+Remote 架构选择优于 4 独立 Shell(共享登录态/布局/组件库/权限体系)和单 Next.js 应用(4 端独立部署/独立 CI/独立回滚);teacher-portal 现状有 7 项高优违规需 P2 闭环前修复;4 端 API 错误码前缀需与后端服务对齐(TP_=teacher-bff、SP_=student-bff、PP_=parent-bff、AP_=admin-bff);设计令牌三层模型(primitive/semantic-light+dark/tailwind-theme)必须 ESLint 强制约束(no-restricted-syntax 禁 #hex + design-tokens/no-hardcoded-fonts 禁字面量字体)。 |
|
||||
| 2026-07-09 | 夜间 | api-gateway/push-gateway(ai01) | **ai01 阶段 1+2 交付(Go 网关层)**:(1) 按 ai-allocation.md §4 必读清单读完 7 份全局文档(README/MIGRATION_GUIDE/004/pending-features/project_rules/coding-standards/multi-ai-collaboration)+ classes 黄金模板全部源码 + shared-proto 8 份 proto(iam/msg/events)+ api-gateway/push-gateway 全部 Go 源码。(2) 运行 `pnpm run arch:scan` 更新 arch.db(10 TS/264 符号、2 Go/33 符号、2 Py/45 符号、138 proto 契约)。(3) 阶段 1 产出 2 份模块理解确认书(services/{api-gateway,push-gateway}/docs/01-understanding.md)+ 审计表:api-gateway 审计出 13 项差距(3 高优先:缺 /metrics 端点、/readyz 是 stub 返回 ok 不检查依赖、auth.go L124-139 死代码 RequestIDMiddleware+generateUUID 与 requestid.go 重复;4 中:logger 用 fmt 非 log/slog、go.mod go 1.25.0 与 Dockerfile golang:1.22-alpine 版本不匹配、HS256 待 P2 升 RS256、DevMode 生产环境风险;6 低);push-gateway 审计出 16 项差距(6 高:无 Redis Pub/Sub 横向扩展、CheckOrigin 直接 return true 安全风险、用文本 "ping"/"pong" 心跳非 RFC 6455 控制帧、无单用户连接数上限、/internal/* 无鉴权、Dockerfile 单阶段且 root 用户无 healthcheck)。(4) 阶段 2 产出 2 份模块架构设计文档(02-architecture-design.md):api-gateway 覆盖 9 节(内部分层图、路由表矩阵 9 下游含端口+鉴权规则、限流策略表按路由差异化 RPS/burst、熔断阈值表按服务、JWT RS256 流程含 JWKS 缓存、CORS 白名单、请求 ID 注入、metrics 7 项指标清单、P0-P3 实施优先级);push-gateway 覆盖 13 节(内部分层图、Connection/Hub 领域模型、Redis 4 个 key pattern、WebSocket 端点+子协议 JSON 格式、内部推送 API+X-Internal-Token 鉴权、双通道协议 HTTP 同步+Kafka 异步、WebSocket 生命周期状态图、心跳协议 RFC 6455 控制帧 30s 间隔 60s 超时、单用户最大 5 连接、重连协议 P6 预留、多实例架构图、Redis Pub/Sub 跨实例流程、容量目标 10w+ 连接 <50ms 本地推送 <200ms 跨实例)。**学到**:gobreaker v2 ReadyToTrip 在 Requests=1 时 1*2>1=true 即 1 次失败就触发 OPEN(与 P6 集成测试观察一致);gorilla/websocket 不支持并发写同一连接,Hub.Send 必须用 send chan + 单写协程串行化(已在 P5 修复但设计文档需明确标注此约束);push-gateway 骨架用文本 "ping"/"pong" 违反 RFC 6455,应用 SetPongHandler 处理控制帧;004 §7.2 事件 topic 用 `edu.teaching.exam.published` 前缀但代码 TOPIC_MAP 用 `edu.exam.events`(ai03 已提请 coord 仲裁,本 AI 在 push-gateway 设计中消费 `edu.notification.events` 待 coord 统一命名后同步);api-gateway 与 push-gateway 重复 tracer.go/logger.go/jwks.go/env.go,建议提取到 `packages/shared-go/`(需 coord 创建包后多 AI 协同迁移);Go 服务 .env 不会自动加载,DevMode 必须在启动前 `export DEV_MODE=true` 或集成 godotenv。 |
|
||||
| 2026-07-09 | 夜间 | teacher-bff/core-edu(ai03) | **ai03 阶段 1 全局理解交付**:(1) 按 ai-allocation.md §4 必读清单读完 7 份全局文档 + classes 黄金模板全部源码 + shared-proto 8 份 proto + teacher-bff/core-edu 现有实现。(2) 运行 arch:scan 更新 arch.db(10 TS/264、2 Go/33、2 Py/45、138 proto 契约);arch:query deps/stats 发现 arch.db 仅记录模块/符号统计不记录跨模块调用边。(3) 按 §6 模板产出两份模块理解确认书 + §10 审计表,交付 docs/architecture/ai03-phase1-understanding.md。(4) 审计发现 teacher-bff 7 项差距(REST 非 gRPC/无 GraphQL/无 DataLoader/无 Redis 缓存/无 readyz/无 Zod/无测试)、core-edu 12 项差距(考试状态机缺失/作业状态机不完整/成绩无校验/无并发锁/homework.graded 与 grade.updated 事件未触发/未消费 IAM user.created/Drizzle db 导出 vs classes getDb() 不一致/kafka.ts 用 console 非 logger/classes 模块仅占位待合并/REST 未转 gRPC/无 Zod/无测试)。(5) 提请 coord 交叉审查 6 项跨模块契约对齐(iam getEffectivePermissions 聚合 API proto / iam user.created topic / 端口 3004 / Kafka topic 命名 004 文档与代码不一致 / data-ana 消费契约 / msg 消费契约)。**学到**:004 §7.2 事件 topic 用 `edu.teaching.exam.published` 前缀,但 core-edu outbox.publisher.ts TOPIC_MAP 用 `edu.exam.events`,文档与代码不一致需 coord 仲裁统一;teacher-bff 当前用 REST fetch 但 P2 退出标准要求 GraphQL Yoga + DataLoader,阶段 2 设计需补通信方式迁移;core-edu 与 classes 黄金模板的 Drizzle 访问方式不一致(core-edu 直接 `export const db`,classes 用 `getDb()` 函数),建议统一为 `getDb()` 函数式以匹配 HealthController 已有约定。 |
|
||||
| 2026-07-09 | 傍晚 | 全局 | **全服务代码合规性审查 + 批量修复**:对 10 个服务(6 NestJS + 2 Go + 2 Python)执行严格代码审查,发现 7 critical + 42 major + 35 minor 问题,批量修复如下。(1) **@RequirePermission() 装饰器实现**:6 个 NestJS 服务全部实现 `SetMetadata` + `Reflector` 标准模式,PermissionGuard 改用 `getAllAndOverride` 读取元数据,注册为 `APP_GUARD` 全局 Guard,DEV_MODE 旁路。content/msg 新建 permission.guard.ts + auth.middleware.ts。(2) **as 断言消除**:所有 `req.headers['x-user-id'] as string` 改为 `typeof` 类型守卫,涉及 auth.middleware.ts/global-error.filter.ts/controller.ts。(3) **返回类型补充**:`getDb()` 标注 `MySql2Database<typeof schema>`,Controller 方法补充 `Promise<{ success: true; data: ... }>`。(4) **import type 修复**:express 的 `Request`/`Response`/`NextFunction` 改为 `import type`。(5) **main.ts /metrics 隐式 any 修复**:参数标注 `Request`/`Response` 类型。(6) **原生 Error → ApplicationError**:repository.ts 的 `throw new Error` 改为 `DatabaseError`。(7) **typeorm 残留清理**:classes lifecycle.service.ts 移除 typeorm DataSource 依赖改用 Drizzle `closeDb()`。(8) **LifecycleService 注册**:5 个 NestJS 服务的 AppModule 注册 LifecycleService。(9) **teacher-bff 补全**:新建 logger.ts + ApplicationError 体系 + GlobalErrorFilter;下游调用转发真实 userId(替换硬编码 "bff");失败时 `logger.warn` + `BadGatewayError`(不再静默吞错);health.controller 迁移到 shared/health/ 标准结构。(10) **Go 安全修复**:CORS 默认 `*` 改为开发白名单 + warning;JWT 密钥非 DevMode fail-fast;push-gateway `/internal/*` 添加 `X-Internal-Key` 鉴权;`interface{}` → `any`;删除死代码 `RequestIDMiddleware`/`generateUUID`/`getEnvInt`;补充 doc comment;api-gateway + push-gateway 添加 `/metrics` 端点;push-gateway 添加 `/readyz`。(11) **Python 修复**:lifespan 返回类型标注 `AsyncGenerator[None, None]`;`dev_mode` 从 `str` 改为 `bool`;data-ana 业务路由改用 `APIRouter`;ai POST 端点从 query 参数改为 Pydantic 请求体模型;data-ana ClickHouse 同步调用包装在 `asyncio.to_thread()` 中。(12) **core-edu GlobalErrorFilter 统一**:响应契约改为 `{ success: false, error: { code, message, details, traceId } }` 与 content/msg 一致。(13) **msg 修复**:notifications.dto.ts 新建 Zod 验证 schema;uuid v4 替换为 `node:crypto.randomUUID`;ES 模块 console.error 改为结构化 logger。**学到**:NestJS `SetMetadata` 从 `@nestjs/common` 导入(非 `@nestjs/core`);`APP_GUARD` 注册全局 Guard 是标准模式;`Reflector.getAllAndOverride` 支持 handler+class 两级元数据查找;PermissionGuard 必须在 DEV_MODE 下旁路否则开发环境无法测试;Go 的 `interface{}` 在 1.18+ 应统一用 `any`;Python `asyncio.to_thread()` 是包装同步 IO 为异步的标准方式;FastAPI `lifespan` 返回类型是 `AsyncGenerator[None, None]`。 |
|
||||
| 2026-07-09 | 下午 | classes/全局 | **一键启动脚本 NestJS dist/ 不生成根因定位 + classes 健康检查修复**:(1) 根因定位:`tsconfig.base.json` 的 `incremental: true` + `nest-cli.json` 的 `deleteOutDir: true` 冲突。`nest start --watch` 启动时先删除 dist/,tsc 读残留 .tsbuildinfo 认为无变化跳过 emit,dist/ 不生成 → `Cannot find module dist/main`。(2) 修复:6 个 NestJS 服务(classes/iam/teacher-bff/core-edu/content/msg)tsconfig.json 显式加 `"incremental": false` 覆盖 base 配置,删除所有残留 .tsbuildinfo 文件。(3) classes AppModule 缺 HealthModule 导入导致 /healthz 404,iam 同样问题,修复 app.module.ts 加 `imports: [..., HealthModule]`。(4) classes HealthController 误用 TypeORM `DataSource` DI(与 iam 不一致),运行时报 `Nest can't resolve dependencies of the HealthController (DataSource)`。修复:改为 Drizzle `getDb()` 函数式调用,与 iam 一致。(5) 一键启动验证:11/11 应用 + 11/11 基础设施 + 5/5 可观测性端点全绿。**学到**:NestJS + TypeScript incremental 编译是陷阱组合——nest-cli deleteOutDir 删 dist 但 tsc 读 tsbuildinfo 认为无变化,必须在服务级 tsconfig 显式 `incremental: false`;HealthModule 必须在 AppModule imports 中显式声明才能被 NestFactory 扫描到;5 个 NestJS 服务的 HealthController 应统一用 Drizzle `getDb()` 函数式调用而非 TypeORM DataSource DI(项目已弃 TypeORM 改 Drizzle)。 |
|
||||
| 2026-07-09 | 下午 | 全局 | **OTel auto-instrumentations 全服务补全**:(1) NestJS 6 服务(iam/classes/core-edu/content/msg/teacher-bff)tracer.ts 补 `getNodeAutoInstrumentations()`,NodeSDK 传 instrumentations 参数自动埋点 HTTP/Express/DB。(2) Python 2 服务(data-ana/ai)main.py 补 `FastAPIInstrumentor.instrument_app(app)`;ai 补缺失的 `opentelemetry-exporter-otlp` 依赖。(3) teacher-bff 从零补完整 OTel:env.ts 加 OTEL_EXPORTER_OTLP_ENDPOINT 字段 + 新建 shared/observability/tracer.ts + main.ts 调用 initTracer/shutdownTracer + package.json 加 sdk-node/exporter/auto-instrumentations 依赖。(4) Go 2 服务(api-gateway/push-gateway)新建 internal/observability/tracer.go(OTLP HTTP exporter + resource + TracerProvider + W3C propagator)+ main.go 调用 InitTracer + otelgin.Middleware 注册 Gin 中间件;push-gateway config.go 补 OTLPEndpoint 字段。(5) 质量校验全通过:TS typecheck 9 服务 + ESLint 6 服务 + ruff 2 服务 + go vet/build 2 服务零错误。**学到**:`getNodeAutoInstrumentations()` 一次注册所有 Node.js 自动埋点(http/express/dns/fs/net/grpc 等),比手动逐个注册 HttpInstrumentation 更简洁;Go OTel 用 `otlptracehttp.WithEndpoint(host)` + `WithInsecure()` 需从 "http://host:port" URL 解析出 host;otelgin.Middleware 必须在 Recovery 之后其他中间件之前注册,确保所有后续 handler 都被 trace;Python FastAPIInstrumentor.instrument_app(app) 在 app 创建后立即调用,lifespan 不受影响。 |
|
||||
| 2026-07-09 | 下午 | 全局 | **P6 硬化:可观测性 + 部署 + CI 硬化**:(1) 可观测性栈完善:5 个 NestJS 服务 main.ts 添加 `/metrics` Prometheus 端点(用 `app.getHttpAdapter().get('/metrics', ...)` 绕过 DI 容器 get 方法);prometheus.yml 从 2 个目标扩展到 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身 + rule_files + alertmanager 关联;monitoring compose 用 Loki + Promtail 替换未配置的 blackbox-exporter;Grafana datasource 新增 Loki;新建 promtail/config.yml 用 docker_sd_configs 仅采集 `edu-*` 容器日志。(2) 部署 compose 扩展:docker-compose.deploy.yml 从 3 服务扩展到 11 服务(+ iam/teacher-bff/core-edu/content/msg/ai/data-ana/push-gateway),每个服务带 healthcheck + depends_on 条件 + edu-net/edu-shared 双网络;deploy.env.example 补全 Neo4j/ES/ClickHouse/LLM/Kafka 可选依赖配置。(3) teacher-bff 补 health.controller.ts(原缺失 /healthz 导致 deploy depends_on service_healthy 失败)。(4) CI 硬化:移除 lint 步骤的 continue-on-error(ESLint 9 flat config 已配置完成),test 保留 continue-on-error(部分服务无 test 脚本)。**学到**:NestJS `app.get('/metrics')` 会被解析为 DI 容器 `get(typeOrToken)`,必须用 `app.getHttpAdapter().get()` 才能注册 Express 路由;Promtail docker_sd_configs 通过 relabel_configs 的 `regex: '/(edu-.*).*'` 过滤容器名前缀;docker-compose.depends_on.condition: service_healthy 要求被依赖服务必须有 healthcheck 配置,否则启动失败。 |
|
||||
| 2026-07-09 | 下午 | data-ana/infra | **CDC 完整链路实现**:MySQL binlog → Debezium Connect → Kafka → data-ana 消费者 → ClickHouse 宽表。(1) MySQL binlog 配置:log_bin=ON, binlog_format=ROW, binlog_row_image=FULL, server_id=1;用 root 创建 `debezium` 用户授予 REPLICATION SLAVE + REPLICATION CLIENT。(2) Debezium Connect 容器:daocloud 禁用 debezium 镜像改用 `quay.io/debezium/connect:2.7`;MySQL 容器在 edu-minimal_default 网络,需 `docker network connect edu-full_default edu-mysql` 让 Debezium 同时可达;Kafka 必须配置双 listener(INSIDE:kafka:29092 + OUTSIDE:localhost:9092),否则 Debezium 拿到 advertised.listeners 中的 localhost metadata 后切换失败;Debezium 2.x 容器环境变量名用 BOOTSTRAP_SERVERS(不带 KAFKA_ 前缀),通过 envsubst 替换到 connect-distributed.properties。(3) 注册 connector:POST :8083/connectors,配置 topic.prefix=edu-cdc, database.include.list=next_edu_cloud, snapshot.mode=initial,4 张表(core_edu_grades/exams/classes/iam_users)成功产生快照事件。(4) data-ana 消费者实现:新建 cdc_consumer.py 用 aiokafka AIOKafkaConsumer,lifespan 中 asyncio.create_task 后台运行;按 source.table 路由(exams→内存缓存 exam_id→class_id 映射,grades→查缓存填 class_id 后 upsert ClickHouse);readyz 端点附加 cdc_consumer 状态。(5) ClickHouse 远程访问:默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 `clickhouse/users.d/custom-users.xml` 覆盖密码+任意 IP。(6) structlog 24.x API:`make_filtering_bound_logger(level)` 替代废弃的 `make_filtering_logger`。(7) E2E 验证:MySQL INSERT 成绩 → Debezium op=c 事件 → Kafka → 消费者写 ClickHouse 宽表(class_id 通过 exam 缓存正确填充)→ /readyz cdc_consumer=running → /analytics/student/student-002/weakness 返回实时 92 分数据。**学到**:Debezium 2.x 容器 bootstrap.servers 默认值是 0.0.0.0:9092 必须显式覆盖;Kafka 单 listener 配置 localhost 会让容器间通信的客户端拿到 metadata 后切换失败,必须用双 listener;ClickHouse users_xml 存储是 readonly 不能用 ALTER USER 修改密码,必须挂载 users.d 配置文件覆盖;消费者 offset 重置必须先停消费者让 group 处于 Empty 状态才能执行 --reset-offsets。 |
|
||||
| 2026-07-09 | 下午 | 全局 | **P6 硬化:ESLint 9 flat config 配置**:(1) 根目录创建 `eslint.config.js`(ESLint 9 flat config 格式):用 `typescript-eslint` recommended 规则集 + `@eslint/js` recommended + `eslint-config-prettier` 禁用冲突规则;自定义规则:`no-explicit-any` warn + `no-unused-vars` 允许下划线前缀 + 测试文件放宽。(2) 6 个 TS 服务 package.json lint 脚本从 `eslint src --ext .ts` 改为 `eslint src`(flat config 不需要 --ext)。(3) `lint-staged.config.js` 恢复 `eslint --fix`。(4) 验证:classes/content/msg/core-edu 四服务 lint 全部零错误零警告通过。**学到**:ESLint 9 flat config 用 `tseslint.config()` 工厂函数组装配置数组;`--ext` 参数在 flat config 模式下被移除,ESLint 自动根据 `eslint.config.js` 中的 `files` 匹配;`@typescript-eslint/consistent-type-assertions` 规则选项格式在 v8 中变化(`objectLiteralType` → `objectLiteralTypeAssertions`),配置时需查最新文档。 |
|
||||
| 2026-07-09 | 中午 | msg/push-gateway/ai/api-gateway | **P5 沟通与 AI 阶段三服务完善**:(1) msg 服务修复:database.ts 导出 db 常量;env.ts JWT_SECRET/ES_URL 改 optional 加 DEV_MODE/PUSH_GATEWAY_URL;elasticsearch.ts ES 降级(esClient=null 时 safeIndex/safeSearch 跳过);notifications.service.ts 加 createBatch + listByUserWithPagination + Push Gateway 推送调用(try/catch 降级);新建 msg-init.sql 2 张表。(2) push-gateway 完善:hub.go 重写用 send chan + 单写协程模式修复 gorilla/websocket 并发写竞争;handler.go 加 DEV_MODE dev-token 支持 + broadcast 端点;config.go 加 DevMode/RedisURL。(3) ai 服务完善:config.py 加 openai_api_key/base_url/dev_mode;新建 llm_client.py(httpx 异步调 OpenAI REST API);main.py 加 /ai 前缀 + 降级模式(无 key 返回骨架 + degraded: true)+ /readyz 端点。(4) Gateway 路由扩展:/notifications → msg,/ai → ai 服务。**学到**:gorilla/websocket 不支持并发写,必须用 send chan 串行化所有写入;FastAPI APIRouter prefix 与 Gateway 代理路径要协调(ai 服务加 /ai 前缀,Gateway 代理 /ai/*path);LLM 降级策略统一返回 degraded 标记,调用方据此判断是否路由流量。 |
|
||||
| 2026-07-09 | 上午 | content/api-gateway | **P4 内容分析服务端到端打通**:(1) content 服务系统性修复:database.ts 导出 db 常量;env.ts JWT_SECRET/ES_URL/NEO4J_URL/NEO4J_PASSWORD 改 optional 加 DEV_MODE;neo4j.ts driver 惰性创建+try/catch+connectionTimeout:3000;health/lifecycle 改用 Drizzle;global-error.filter 移除 @types/express 依赖;textbooks.schema 修复 integer→int + 导出 NewTextbook/NewChapter 类型;textbooks.controller 移除 body as any + 加 PUT/DELETE。(2) 新建 3 模块:chapters(CRUD + 按 textbook 查询)、knowledge-points(CRUD + Neo4j 前置依赖图非阻塞查询)、questions(CRUD + 4 种题型校验)。(3) Gateway 路由扩展:textbooks/chapters/knowledge-points/questions 四组路由。(4) 数据库:content-init.sql 4 张表。(5) E2E 验证:POST /textbooks 201 → POST /chapters 201(字段用 order 非 orderNum)→ POST /knowledge-points 201(Neo4j 不可用 MySQL 正常写入)→ POST /questions 201 → GET 各列表 200。**学到**:Drizzle schema TS 字段名与 DB 列名解耦(order→order_num),API 请求体用 TS 字段名;Neo4j 不可用时必须 driver=null(不设 NEO4J_URL),否则每次请求尝试连接拖慢响应;neo4j-driver safeCreateNode 用 try/catch 非阻塞,MySQL 数据始终先落库。 |
|
||||
| 2026-07-09 | 凌晨 | core-edu/api-gateway | **P3 核心教学服务端到端打通**:(1) core-edu 服务系统性修复 13 项:database.ts 导出 db 常量替代 getDb();env.ts JWT_SECRET 改 optional 加 DEV_MODE;kafka.ts connectKafka 加 try/catch 不阻塞启动;main.ts 去全局 /api 前缀 + connectKafka 改 void 非阻塞;app.module 移除未用 AuthMiddleware/ClassesesModule 加 HealthModule;3 个 controller 路由去前缀去 UseGuards 从 x-user-id 读身份;exams/homework service datetime 列 ISO 字符串转 Date 修复 drizzle toISOString 错误;修正 10 处相对 import 路径;health/lifecycle 改用 Drizzle 原生查询;新增 core-edu-init.sql 4 张表。(2) Gateway 路由扩展:发现 internal/routing/routing.go 是死代码(未被 main 引用),真正路由在 main.go;在 main.go 添加 exams/homework/grades 三组路由(无尾斜杠+通配符);删除 routing.go;config.go 加 CoreEduServiceURL。(3) DEV_MODE 环境变量问题:Go 不自动加载 .env,必须在启动前 export DEV_MODE=true 否则 dev-token 被拒 401。(4) E2E 验证:POST /exams 201 → GET /exams/:id 200 → GET /exams/class/:id 200 → POST /homework 201 → POST /grades 201 → Outbox 3 条事件正确写入(exam.failed 因 Kafka 未启动,homework/grade pending)。**学到**:drizzle datetime 列需 Date 对象不是 ISO 字符串(mapToDriverValue 调 toISOString);Go 项目 .env 不会自动加载需显式 export 或 godotenv 库;NestJS controller 路由前缀与 Gateway 代理路径要协调(Gateway 去掉 /api/v1 后转发,controller 用裸路径如 'exams');Outbox 模式业务事务同写验证通过,Kafka 未启动时事件 status=failed 但业务数据已落库。 |
|
||||
| 2026-07-09 | 上午 | iam/teacher-bff/teacher-portal | **P2 身份阶段完整实现**:(1) Gateway 公开路径白名单(register/login/refresh)解决无 token 死锁。(2) IAM schema 扩展:users 加 dataScope,新增 role_viewports 表。(3) RBAC 端点 4 个 GET。(4) 视口按 requiredPermission 过滤 + sortOrder 排序;getEffectivePermissions 用 Set 去重。(5) JWT payload 含 dataScope,register 自动分配 teacher 角色。(6) 种子数据 7 权限+12 映射+7 视口。(7) Teacher BFF 视口聚合。(8) 前端:lib/auth.ts + login + AppShell + (app) 路由组 + dashboard + classes(真实 JWT)+ 根重定向。(9) E2E 全链路通过。**学到**:Next.js 路由组 (app) 不影响 URL,/login 与 /dashboard 共存只后者套壳;fetch headers 函数返回 Record<string,string> 避免 TS2769;ESLint 9 需 flat config 留 P6;AppShell aside 用 flex flex-col + mt-auto 比 absolute 稳健。 |
|
||||
| 2026-07-08 | 晚上 | iam/classes/api-gateway | **P1 端到端链路验证 + IAM 服务修复**:验证 register → JWT → Gateway /iam/me → Gateway /classes CRUD → teacher-portal 前端渲染全链路打通。(1) IAM 服务 14 个 TS 编译错误修复:移除 typeorm/ioredis/kafkajs 依赖(IAM 用 Drizzle),health.controller.ts 改用 `db.execute(sql\`SELECT 1\`)`,lifecycle.service.ts 简化为只关闭 Drizzle 连接池;Drizzle API 修正(`r.roles`→`r.iam_roles`,`.in()`→`inArray()`)。(2) NestJS ESM DI 修复:iam.module.ts 简化 providers 为 `[IamService, IamRepository]`,iam.service.ts 构造器加 `@Inject(IamRepository)`(参考 classes 黄金模板),修复运行时 `Cannot read properties of undefined (reading 'findUserByEmail')`。(3) Gateway /iam/me 404 修复:iam.controller.ts 直接读 `req.headers['x-user-id']`替代未注册的`AuthenticatedRequest`。(4) 创建 `scripts/iam-init.sql`建 6 张 IAM 表 + 种子数据。(5) E2E 验证:iam:3002 注册/登录 → Gateway /iam/me 200 → Gateway GET /classes 200 → Gateway POST /classes(合法 UUID gradeId)201 → teacher-portal:3000 首页渲染 200 + 含"班级管理" → Next.js rewrites 透传 dev-token 到 Gateway 全链路通。**学到**:NestJS ESM 模式下 DI 无法通过类型推断解析 token,必须显式`@Inject(Token)`;Drizzle select 返回字段名按 schema 定义而非表名;classes.dto.ts 的 gradeId 要求 UUID 格式,测试数据不能用 "grade-12" 这类字符串;PowerShell 控制台中文显示为 `?`是编码问题,数据库实际存储正确;DEV_MODE 下前端用`Bearer dev-token` 即可走通链路,无需真实 JWT。 |
|
||||
| 2026-07-08 | 下午 | 全局 | **CI/CD 完整配置 + 多AI协作规范入规则**:(1) project_rules.md 新增 §14 多 AI 协作规范(角色权限矩阵/分支命名/PR合并规则/跨模块变更顺序/冲突处理/AI 身份标注/敏感文件保护)+ §15 CI/CD 规范(流水线阶段/触发条件/镜像规范/部署策略/Secrets 管理/必需 CI 文件)。(2) 优化现有 4 个 ci-*.yml:ci-ts.yml 加 arch-scan + docker-build job;ci-go.yml 去掉 golangci-lint(lint-staged 预存问题),加 docker-build;ci-proto.yml 修复 buf breaking URL(从 github.com 改为 .git 本地比较)。(3) 新增 `docker.yml`:main/tag 触发,构建推送 3 服务镜像到 Gitea Container Registry(git.eazygame.cn/xiner/edu/<service>:latest + sha tag + version tag),用 GITHUB_TOKEN 自动认证。(4) 新增 `deploy.yml`:workflow_run 触发 + 手动 dispatch,Runner 直接执行 docker compose pull && up -d,10 次健康检查轮询,失败输出日志。(5) 新增 `infra/docker-compose.deploy.yml`(部署用,镜像来自 Gitea registry,连接服务器已有 MySQL/Redis 通过 edu-shared 外部网络)+ `infra/deploy.env.example`(部署环境变量模板)。(6) 编写 `docs/standards/cicd-runbook.md`(CI/CD 使用手册,含架构总览/一次性配置/日常使用/镜像管理/部署验证/回滚/常见问题/排查命令/安全注意事项)。**学到**:Docker Compose 不支持 `restart_policy`(是 swarm 字段),用 `restart: unless-stopped` 替代;Gitea Actions 兼容 GitHub Actions 语法但 `workflow_run` 触发可能不完整,备选手动 dispatch;应用容器访问宿主机已有 MySQL/Redis 需通过共享外部网络(`docker network create edu-shared` + `docker network connect`)而非 `host.docker.internal`。 |
|
||||
| 2026-07-08 | 下午 | api-gateway | **重定向循环修复 + 生产模式部署准备 + 多AI协作文档**:(1) 修复 `ERR_TOO_MANY_REDIRECTS`:Gin 默认 `RedirectTrailingSlash=true` 导致 `/api/v1/classes` → 301 → `/classes/`,Next.js rewrites 代理时形成循环。**修复**:`r.RedirectTrailingSlash=false` + 同时注册无尾斜杠路由(`/classes`)与通配符路由(`/classes/*path`)。(2) 新增 DEV_MODE 旁路:`config.go` 加 `DevMode` 字段,`auth.go` 在 `DEV_MODE=true` 时接受 `dev-token` 注入固定身份(生产必须 false)。(3) 生产 Docker 化:新建 `apps/teacher-portal/Dockerfile`(多阶段 Next.js build)+ `services/api-gateway/Dockerfile`(多阶段 Go 静态编译)+ `infra/docker-compose.prod.yml`(三服务编排,强制 DEV_MODE=false)。(4) 编写 `docs/standards/local-dev-runbook.md`(本地启动手册,含端口表/开发模式/生产模式/常见问题)+ `docs/standards/multi-ai-collaboration.md`(多AI协作文档,含模块分工矩阵/分支命名/PR流程/合并策略/冲突处理/权限矩阵)。**学到**:Gin `RedirectTrailingSlash=false` 后需显式注册无尾斜杠路由(`Any("/classes")` + `Any("/classes/*path")`),否则 404;Next.js rewrites 代理会透传 301 给浏览器形成循环,开发模式旁路应通过环境变量控制而非硬编码。 |
|
||||
| 2026-07-08 | 全天 | 全局 | **P6 后续工作手册执行**:完整执行 post-p6-followup.md 12 节任务。环境准备(pnpm 925 包 + go mod tidy 双服务 + uv sync 双服务 + buf 安装)→ 代码质量校验(Go vet/build 0 错误,Python ruff 8 错误自动修复)→ arch.db 同步(实现 4 个扫描器骨架,输出 12 模块/233 符号/138 契约)→ project_rules.md P0 修复(迁移到 .trae/rules/,17881 字节)→ 004 架构图修复(1.1a/1.1b 双图 + 1.2 业务领域列 + 5.4 视口四层)→ P6 集成测试(10 Go + 17 bash = 27 用例全通过)→ Helm Chart 演化(8 chart lint 通过)。**学到**:多语言 monorepo 工具链配置需统一镜像源(npmmirror/goproxy.cn/tuna),go.work BOM 字符会导致 `unexpected input character` 错误必须重写文件。 |
|
||||
| 2026-07-08 | 上午 | 全局 | pnpm install 网络失败(ECONNRESET)→ 配置 `npm config set registry https://registry.npmmirror.com` + `pnpm config set registry https://registry.npmmirror.com` 重试成功。**学到**:Windows 下 pnpm 还需配置 `PNPM_HOME` 和 `TMP` 环境变量避免 `_tmp_` 文件 ENOENT 错误。 |
|
||||
| 2026-07-08 | 上午 | 全局 | project_rules.md 损坏(72 字节乱码,从 P1 提交 2ba4250 就损坏,git 历史无完整版本)→ 从 CICD 项目完整版迁移到 `e:\Desktop\Edu\.trae\rules\project_rules.md`(按用户要求放 .trae/rules/),按 MIGRATION_GUIDE 4.1 策略矩阵调整为微服务版(13 章 17881 字节),删除根目录损坏文件,更新 7 处引用(README/MIGRATION_GUIDE/004/known-issues/git-workflow/coding-standards)。**学到**:迁移文件后必须 `Get-Item | Select Length` 验证完整性 + 全文搜索引用更新,git commit 前运行 cat 检查内容。 |
|
||||
| 2026-07-08 | 上午 | api-gateway | go.work BOM 字符 + 版本不匹配:`unexpected input character '\ufeff'` 和 `module requires go >= 1.22.0, but go.work lists go 1.22`。**修复**:重写 go.work 去除 BOM,版本改为 `go 1.26.0`,移除不存在的 `./packages/shared-go`。**学到**:PowerShell `Out-File` 默认加 BOM,写 go.work 这类敏感文件应用 `Write` 工具或 `[System.IO.File]::WriteAllText` 指定 UTF8 无 BOM。 |
|
||||
| 2026-07-08 | 上午 | arch-scan | arch:scan 返回 0 模块 0 符号 → 4 个扫描器(ts/go/py/proto)都是骨架实现。**修复**:完整实现 4 个扫描器,TS 用 regex 提取(避免 ts-morph 对未安装依赖文件解析失败),Go/Python 用行首锚定正则,Proto 扫描 service/message/rpc。结果:12 模块(≥10 ✓)、233 符号(≥100 ✓)、138 契约。**学到**:ts-morph Project 对未 `pnpm install` 的 workspace 文件会报模块解析失败,改用 regex 更鲁棒;scanner.ts main() 开头需 `DELETE FROM` 清空旧数据避免重跑重复。 |
|
||||
| 2026-07-08 | 下午 | 004 | 架构图视角讨论(技术分层 vs 业务领域)→ 双图并存方案:1.1a 技术分层视角(部署/流量/网络边界,Users 层标注"场景域用户",BFF 层标注"按场景域分")+ 1.1b 业务领域视角(6 DDD 限界上下文 subgraph:D1 身份/D2 教学组织/D3 教学核心/D4 内容/D5 沟通/D6 智能洞察)。1.2 服务清单新增"业务领域"列。**学到**:双图互补,1.1a 服务运维/SRE 视角,1.1b 服务产品/架构视角;同一服务可横跨多领域(core-edu 同时承载 D2+D3)。 |
|
||||
| 2026-07-08 | 下午 | 004 | 视口四层模型补充(5.4 章节):L1 导航(navigation_config 表)/ L2 路由(route_permission + Gateway 校验)/ L3 组件(usePermission().hasPermission)/ L4 数据(DataScope 枚举)。场景域 BFF 复用策略:按使用场景域分 BFF 而非按角色分,教导主任复用 Teacher BFF + 额外管理视口。iam 服务职责:认证 + RBAC + 视口配置 + DataScope + 权限解析 API。**学到**:视口既可独立配置(RoleViewport 表)也可由权限推导,新角色只需配权限集,视口自动推导。 |
|
||||
| 2026-07-08 | 下午 | api-gateway | P6 集成测试补充:circuit-breaker_test.go(5 用例:ClosedToOpen/OpenToHalfOpen/HalfOpenToClosed/HalfOpenToOpen/4xxNotCounted)+ ratelimit_test.go(5 用例:AllowUnderBurst/RejectOverBurst/RefillTokens/PerIPIsolation/CleanupExpiredBuckets)+ test-backup-mysql.sh(8 用例 17 断言)。**学到**:gobreaker v2 ReadyToTrip 在 1 次失败后就触发(`TotalFailures*2 > Requests` 当 Requests=1 时 1*2>1=true),HALF_OPEN 状态只在探测执行期间可见,探测完成后立即转 CLOSED 或回 OPEN,测试需通过行为(503 vs 500)而非状态字段验证;rateLimiter cleanup 测试需用短周期参数(50ms/500ms)加速,且新鲜桶要在旧桶清理后再创建避免被一起清掉。 |
|
||||
| 2026-07-08 | 下午 | infra/k8s | Helm Chart 演化:安装 Helm v4.2.2,创建 edu-platform 平台级 chart(namespace/configmap/secret/ingress/hpa + 4 环境 values 文件)+ api-gateway 服务级 chart(完整迁移自原 deployment.yaml,参数化所有字段)+ 6 业务服务 chart 桩(iam/core-edu/content/msg/data-ana/ai)。删除原 api-gateway-deployment.yaml,保留 namespace.yaml。**学到**:Helm `{{- with ... -}}` 双向修剪会导致标签连在一行(`managed-by: Helmpart-of: edu-platform`),应改为 `{{- with ... }}` 只修剪左侧;`helm lint` 全部通过但 `helm template` 才能发现 YAML 渲染错误,验证时两个都要跑。 |
|
||||
| 2026-07-07 | 全天 | 全局 | 文档体系初始化:从旧项目(e:\Desktop\CICD,Next.js 单体)迁移 spec + plan + known-issues 模板到新仓库(e:\Desktop\Edu,微服务架构)。known-issues 重组为微服务分区:多语言 monorepo / Docker Compose / protobuf+buf / NestJS / Go Gateway / 可观测性 / 微前端。从旧项目提炼可迁移经验:React 19 useOptimistic / Zustand 细粒度选择器 / Tiptap SSR / 请求级去重 / 批量 SQL / 动态导入模式 / arch:scan 串行执行。新增微服务特有经验:契约先行 / Outbox / CDC / 双轨读 / DataScope / 黄金模板复制流程。路线图按 6 阶段组织:P1 地基 → P2 身份 → P3 核心教学 → P4 内容分析 → P5 沟通AI → P6 硬化。 |
|
||||
|
||||
60
eslint.config.js
Normal file
60
eslint.config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// ESLint 9 flat config
|
||||
// 项目级配置:TypeScript + NestJS + Next.js + 设计令牌规则
|
||||
const js = require('@eslint/js');
|
||||
const tseslint = require('typescript-eslint');
|
||||
const prettierConfig = require('eslint-config-prettier');
|
||||
|
||||
module.exports = tseslint.config(
|
||||
// 全局忽略
|
||||
{
|
||||
ignores: [
|
||||
'**/dist/**',
|
||||
'**/node_modules/**',
|
||||
'**/.next/**',
|
||||
'**/coverage/**',
|
||||
'**/*.config.js',
|
||||
'**/*.config.mjs',
|
||||
'scripts/arch-scan/**',
|
||||
],
|
||||
},
|
||||
|
||||
// 基础 JS 规则
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript 规则
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// 项目级自定义规则
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
// 禁止 any(未知类型用 unknown)
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
// 未使用变量允许下划线前缀
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
// 允许 console(开发环境)
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// 测试文件放宽规则
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.spec.ts', '**/test/**'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// 禁用与 Prettier 冲突的规则
|
||||
prettierConfig,
|
||||
);
|
||||
3
go.work
3
go.work
@@ -1,7 +1,6 @@
|
||||
go 1.22
|
||||
go 1.26.0
|
||||
|
||||
use (
|
||||
./services/api-gateway
|
||||
./services/push-gateway
|
||||
./packages/shared-go
|
||||
)
|
||||
|
||||
35
go.work.sum
Normal file
35
go.work.sum
Normal file
@@ -0,0 +1,35 @@
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
69
infra/alertmanager/alertmanager.yml
Normal file
69
infra/alertmanager/alertmanager.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
# Alertmanager 配置 - Edu 平台
|
||||
# 全局配置
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
|
||||
# 告警模板(可扩展)
|
||||
templates:
|
||||
- /etc/alertmanager/templates/*.tmpl
|
||||
|
||||
# 路由树
|
||||
route:
|
||||
# 顶层默认接收器
|
||||
receiver: webhook-default
|
||||
# 按 alertname + service 分组
|
||||
group_by: ['alertname', 'service']
|
||||
# 首次告警等待时间(聚合相同组)
|
||||
group_wait: 30s
|
||||
# 同组新告警发送间隔
|
||||
group_interval: 5m
|
||||
# 重复告警发送间隔
|
||||
repeat_interval: 4h
|
||||
routes:
|
||||
# critical 告警走 webhook
|
||||
- matchers:
|
||||
- severity="critical"
|
||||
receiver: webhook-default
|
||||
continue: false
|
||||
# warning 告警走 webhook
|
||||
- matchers:
|
||||
- severity="warning"
|
||||
receiver: webhook-default
|
||||
continue: false
|
||||
|
||||
# 接收器列表
|
||||
receivers:
|
||||
# 默认 Webhook 接收器(指向内部告警路由服务)
|
||||
- name: webhook-default
|
||||
webhook_configs:
|
||||
- url: http://alert-router:5001/alert
|
||||
send_resolved: true
|
||||
max_alerts: 0
|
||||
|
||||
# 邮件接收器(注释示例,留作扩展)
|
||||
# 启用前需在 global.smtp_* 配置 SMTP 服务器
|
||||
# - name: mail-ops
|
||||
# email_configs:
|
||||
# - to: ops-team@example.com
|
||||
# from: alertmanager@example.com
|
||||
# smarthost: smtp.example.com:587
|
||||
# auth_username: alertmanager@example.com
|
||||
# auth_password: <SMTP_PASSWORD>
|
||||
# require_tls: true
|
||||
# headers:
|
||||
# Subject: '[Edu Alert] {{ .GroupLabels.alertname }}'
|
||||
|
||||
# 钉钉/企业微信接收器(注释示例,留作扩展)
|
||||
# - name: dingtalk-ops
|
||||
# webhook_configs:
|
||||
# - url: http://dingtalk-webhook:8060/dingtalk/ops/send
|
||||
# send_resolved: true
|
||||
|
||||
# 抑制规则:critical 告警抑制同服务同名的 warning 告警
|
||||
inhibit_rules:
|
||||
- source_matchers:
|
||||
- severity="critical"
|
||||
target_matchers:
|
||||
- severity="warning"
|
||||
# 相同 alertname + service 才抑制
|
||||
equal: ['alertname', 'service']
|
||||
74
infra/backup/backup-cron.sh
Normal file
74
infra/backup/backup-cron.sh
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# MySQL 备份 cron 调度入口
|
||||
# 调用 backup-mysql.sh 备份所有微服务数据库,单个失败不阻塞其他
|
||||
# 用法: backup-cron.sh [--keep <days>]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
KEEP_DAYS=7
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--keep)
|
||||
KEEP_DAYS="${2:-7}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--keep <days>]" >&2
|
||||
echo " --keep 保留天数(默认 7)" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] 未知参数: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 配置 ----------
|
||||
# 每个服务对应一个独立数据库
|
||||
SERVICES=(iam core-edu content msg classes)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_SCRIPT="${SCRIPT_DIR}/backup-mysql.sh"
|
||||
|
||||
# ---------- 日志函数 ----------
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
}
|
||||
|
||||
if [[ ! -x "$BACKUP_SCRIPT" ]]; then
|
||||
log "[ERROR] 备份脚本不存在或不可执行: ${BACKUP_SCRIPT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
log "[INFO] 开始批量备份,共 ${#SERVICES[@]} 个服务"
|
||||
|
||||
TOTAL=0
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
FAILED_LIST=()
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
TOTAL=$((TOTAL + 1))
|
||||
log "[INFO] ---- 备份服务: ${svc} ----"
|
||||
if "$BACKUP_SCRIPT" --service "$svc" --keep "$KEEP_DAYS"; then
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
log "[INFO] 服务 ${svc} 备份成功"
|
||||
else
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_LIST+=("$svc")
|
||||
log "[ERROR] 服务 ${svc} 备份失败,继续下一个"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
log "[INFO] 批量备份结束: 总计=${TOTAL} 成功=${SUCCESS} 失败=${FAILED}"
|
||||
|
||||
if [[ ${FAILED} -gt 0 ]]; then
|
||||
log "[ERROR] 失败服务列表: ${FAILED_LIST[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
99
infra/backup/backup-mysql.sh
Normal file
99
infra/backup/backup-mysql.sh
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
# MySQL 全量备份脚本(Linux 容器内运行)
|
||||
# 用法: backup-mysql.sh --service <name> [--keep <days>]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
SERVICE=""
|
||||
KEEP_DAYS=7
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--service)
|
||||
SERVICE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--keep)
|
||||
KEEP_DAYS="${2:-7}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 --service <name> [--keep <days>]" >&2
|
||||
echo " --service 目标微服务数据库名(必填,如 iam/core-edu/content/msg/classes)" >&2
|
||||
echo " --keep 保留天数(默认 7)" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] 未知参数: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 参数校验 ----------
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
echo "[ERROR] --service 参数必填" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]] || [[ "$KEEP_DAYS" -lt 1 ]]; then
|
||||
echo "[ERROR] --keep 必须为正整数" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 环境变量 ----------
|
||||
: "${MYSQL_HOST:=mysql}"
|
||||
: "${MYSQL_PORT:=3306}"
|
||||
: "${MYSQL_USER:=root}"
|
||||
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD 环境变量未设置}"
|
||||
|
||||
BACKUP_ROOT="/backups"
|
||||
BACKUP_DIR="${BACKUP_ROOT}/${SERVICE}"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/${TIMESTAMP}.sql.gz"
|
||||
|
||||
# ---------- 日志函数 ----------
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
}
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
log "[INFO] 开始备份服务: ${SERVICE} (保留 ${KEEP_DAYS} 天)"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
log "[INFO] 执行 mysqldump -> ${BACKUP_FILE}"
|
||||
if ! mysqldump \
|
||||
--host="$MYSQL_HOST" \
|
||||
--port="$MYSQL_PORT" \
|
||||
--user="$MYSQL_USER" \
|
||||
--password="$MYSQL_PASSWORD" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--databases "$SERVICE" \
|
||||
2>/dev/null \
|
||||
| gzip -c > "$BACKUP_FILE"; then
|
||||
log "[ERROR] mysqldump 失败 (service=${SERVICE})"
|
||||
rm -f "$BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 校验产物非空
|
||||
if [[ ! -s "$BACKUP_FILE" ]]; then
|
||||
log "[ERROR] 备份文件为空: ${BACKUP_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_SIZE="$(stat -c %s "$BACKUP_FILE" 2>/dev/null || stat -f %z "$BACKUP_FILE")"
|
||||
log "[INFO] 备份完成: ${BACKUP_FILE} (${FILE_SIZE} bytes)"
|
||||
|
||||
# ---------- 清理过期备份 ----------
|
||||
log "[INFO] 清理超过 ${KEEP_DAYS} 天的旧备份"
|
||||
find "$BACKUP_DIR" -type f -name '*.sql.gz' -mtime +${KEEP_DAYS} -print -delete \
|
||||
| while read -r old_file; do
|
||||
log "[INFO] 已删除: ${old_file}"
|
||||
done
|
||||
|
||||
log "[INFO] 备份流程结束: ${SERVICE}"
|
||||
exit 0
|
||||
94
infra/backup/restore-mysql.sh
Normal file
94
infra/backup/restore-mysql.sh
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# MySQL 恢复脚本(Linux 容器内运行)
|
||||
# 用法: restore-mysql.sh --service <name> --file <path> [--yes]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
SERVICE=""
|
||||
BACKUP_FILE=""
|
||||
ASSUME_YES=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--service)
|
||||
SERVICE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--file)
|
||||
BACKUP_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
ASSUME_YES=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 --service <name> --file <path> [--yes]" >&2
|
||||
echo " --service 目标微服务数据库名(必填)" >&2
|
||||
echo " --file 备份文件路径 sql.gz(必填)" >&2
|
||||
echo " --yes 跳过确认提示" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] 未知参数: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 参数校验 ----------
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
echo "[ERROR] --service 参数必填" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
echo "[ERROR] --file 参数必填" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "[ERROR] 备份文件不存在: ${BACKUP_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 环境变量 ----------
|
||||
: "${MYSQL_HOST:=mysql}"
|
||||
: "${MYSQL_PORT:=3306}"
|
||||
: "${MYSQL_USER:=root}"
|
||||
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD 环境变量未设置}"
|
||||
|
||||
# ---------- 日志函数 ----------
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
}
|
||||
|
||||
# ---------- 确认提示 ----------
|
||||
log "[WARN] 即将向数据库 [${SERVICE}] 导入备份文件: ${BACKUP_FILE}"
|
||||
log "[WARN] 此操作会覆盖目标数据库现有数据,不可撤销!"
|
||||
|
||||
if [[ "$ASSUME_YES" != "true" ]]; then
|
||||
read -r -p "确认继续? (输入 YES 继续): " confirm
|
||||
if [[ "$confirm" != "YES" ]]; then
|
||||
log "[INFO] 用户取消操作"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
log "[INFO] 开始恢复服务: ${SERVICE}"
|
||||
|
||||
log "[INFO] 解压并导入: ${BACKUP_FILE}"
|
||||
if ! gunzip -c "$BACKUP_FILE" \
|
||||
| mysql \
|
||||
--host="$MYSQL_HOST" \
|
||||
--port="$MYSQL_PORT" \
|
||||
--user="$MYSQL_USER" \
|
||||
--password="$MYSQL_PASSWORD" \
|
||||
"$SERVICE"; then
|
||||
log "[ERROR] mysql 导入失败 (service=${SERVICE})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "[INFO] 恢复完成: ${SERVICE} <- ${BACKUP_FILE}"
|
||||
exit 0
|
||||
112
infra/backup/test-backup-mysql.sh
Normal file
112
infra/backup/test-backup-mysql.sh
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# backup-mysql.sh 参数解析与校验的 dry-run 测试
|
||||
# 不实际执行 mysqldump,仅验证参数解析、必填校验、默认值、帮助信息等。
|
||||
#
|
||||
# 运行方式(需要 bash 环境,如 Git Bash / WSL):
|
||||
# bash infra/backup/test-backup-mysql.sh
|
||||
#
|
||||
# 退出码:0 表示全部通过,非 0 表示有失败用例。
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT="${SCRIPT_DIR}/backup-mysql.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# assertExit <expected_code> <description> <actual_code> <stderr>
|
||||
assertExit() {
|
||||
local expected="$1" desc="$2" actual="$3" stderr="${4:-}"
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
echo "[PASS] $desc (exit=$actual)"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "[FAIL] $desc: 期望 exit=$expected, 实际 exit=$actual"
|
||||
[[ -n "$stderr" ]] && echo " stderr: $stderr"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# assertContains <needle> <description> <haystack>
|
||||
assertContains() {
|
||||
local needle="$1" desc="$2" haystack="${3:-}"
|
||||
if [[ "$haystack" == *"$needle"* ]]; then
|
||||
echo "[PASS] $desc"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "[FAIL] $desc: 输出应包含 '$needle'"
|
||||
echo " 实际输出: $haystack"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== backup-mysql.sh dry-run 测试 ==="
|
||||
|
||||
# ---------- 用例 1: 无参数应失败并提示 --service 必填 ----------
|
||||
output="$("$SCRIPT" 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "无参数应退出 1" "$code" "$output"
|
||||
assertContains "--service 参数必填" "无参数应提示 --service 必填" "$output"
|
||||
|
||||
# ---------- 用例 2: --help 应退出 0 并输出 Usage ----------
|
||||
output="$("$SCRIPT" --help 2>&1)"
|
||||
code=$?
|
||||
assertExit 0 "--help 应退出 0" "$code" "$output"
|
||||
assertContains "Usage:" "--help 应输出 Usage" "$output"
|
||||
assertContains "--service" "--help 应说明 --service 参数" "$output"
|
||||
assertContains "--keep" "--help 应说明 --keep 参数" "$output"
|
||||
|
||||
# ---------- 用例 3: --keep 默认值(未提供时)应在日志中体现 7 天 ----------
|
||||
# 设置 MYSQL_PASSWORD 让脚本通过环境变量校验,进入主流程打印日志;
|
||||
# 随后会在 mysqldump 阶段失败(CI 环境无 mysqldump)。
|
||||
output="$(MYSQL_PASSWORD=fake-pass "$SCRIPT" --service iam 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "无 mysqldump 时应最终退出 1" "$code" "$output"
|
||||
assertContains "保留 7 天" "未提供 --keep 时应使用默认值 7 天" "$output"
|
||||
|
||||
# ---------- 用例 4: --keep 非数字应失败 ----------
|
||||
output="$("$SCRIPT" --service iam --keep abc 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "--keep 非数字应退出 1" "$code" "$output"
|
||||
assertContains "--keep 必须为正整数" "非数字 --keep 应报错" "$output"
|
||||
|
||||
# ---------- 用例 5: --keep 小于 1 应失败 ----------
|
||||
output="$("$SCRIPT" --service iam --keep 0 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "--keep=0 应退出 1" "$code" "$output"
|
||||
assertContains "--keep 必须为正整数" "--keep=0 应报错" "$output"
|
||||
|
||||
# ---------- 用例 6: 未知参数应失败 ----------
|
||||
output="$("$SCRIPT" --service iam --unknown-flag 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "未知参数应退出 1" "$code" "$output"
|
||||
assertContains "未知参数" "未知参数应报错" "$output"
|
||||
|
||||
# ---------- 用例 7: --keep 自定义值应在日志中体现 ----------
|
||||
output="$(MYSQL_PASSWORD=fake-pass "$SCRIPT" --service iam --keep 30 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "无 mysqldump 时应最终退出 1(用例 7 预置)" "$code" "$output"
|
||||
assertContains "保留 30 天" "--keep=30 应在日志中体现" "$output"
|
||||
|
||||
# ---------- 用例 8: MYSQL_PASSWORD 已设置但 mysqldump 不可用时应优雅失败 ----------
|
||||
# 此用例验证:参数校验通过后,脚本会尝试调用 mysqldump;若 mysqldump 不存在则失败。
|
||||
# 在无 MySQL 容器的 CI 环境中,这是最接近真实 dry-run 的验证。
|
||||
output="$(MYSQL_PASSWORD=fake-pass "$SCRIPT" --service iam 2>&1)"
|
||||
code=$?
|
||||
# mysqldump 不存在时,pipefail + gzip 写入空文件 → 脚本检测到空文件后 exit 1
|
||||
# 或 mysqldump 命令未找到 → set -e 触发 exit 1
|
||||
# 任一路径都应是非 0 退出
|
||||
if [[ "$code" -ne 0 ]]; then
|
||||
echo "[PASS] mysqldump 不可用时应非 0 退出 (exit=$code)"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "[FAIL] mysqldump 不可用时不应返回 0"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
echo "=== 测试结果 ==="
|
||||
echo "通过: $PASS 失败: $FAIL"
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
45
infra/chaos/README.md
Normal file
45
infra/chaos/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 混沌工程说明
|
||||
|
||||
## 适用范围
|
||||
|
||||
- **仅 Staging 环境**:禁止在生产环境运行混沌实验
|
||||
- **每月 1 次演练**:建议每月第一个工作周执行
|
||||
- **演练窗口**:业务低峰期(02:00 - 05:00)
|
||||
|
||||
## 实验清单
|
||||
|
||||
| 实验 | 注入故障 | 验证目标 |
|
||||
|------|----------|----------|
|
||||
| `pod-delete` | 杀死 50% Pod | 自愈能力、副本数冗余 |
|
||||
| `pod-network-latency` | 注入 200ms 延迟 | 超时与重试、降级策略 |
|
||||
| `disk-fill` | 填充磁盘至 80% | 日志写入、磁盘告警 |
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. Staging 环境已部署完整监控(Prometheus + Alertmanager + Grafana)
|
||||
2. 已配置稳态探针(`steadyStateHypothesis`)
|
||||
3. 已配置告警通道并验证可达
|
||||
4. 演练参与者已就位(运维 + 研发 oncall)
|
||||
|
||||
## 演练流程
|
||||
|
||||
1. **演练前 30 分钟**:通知相关人员,确认稳态基线
|
||||
2. **启动实验**:按顺序执行,单实验单次注入
|
||||
3. **观察监控**:关注告警是否按预期触发、服务是否自愈
|
||||
4. **演练后**:归档实验结果,更新本目录 README
|
||||
|
||||
## 回滚预案
|
||||
|
||||
- **5 分钟内未恢复自动回滚**:通过 Litmus `steadyStateHypothesis` 失败触发实验终止
|
||||
- 手动回滚:
|
||||
```bash
|
||||
kubectl delete chaosengine edu-chaos-experiments -n edu-monitoring
|
||||
kubectl rollout restart deployment/api-gateway -n edu-services
|
||||
```
|
||||
- 极端情况:直接缩容 / 扩容受影响 Deployment
|
||||
|
||||
## 安全约束
|
||||
|
||||
- 每次实验**只注入一种故障**,避免叠加影响判断
|
||||
- 实验前快照数据库(参考 `infra/backup/backup-cron.sh`)
|
||||
- 实验期间禁止发布任何业务变更
|
||||
139
infra/chaos/experiments.yaml
Normal file
139
infra/chaos/experiments.yaml
Normal file
@@ -0,0 +1,139 @@
|
||||
# 混沌实验配置(Litmus Chaos 骨架)
|
||||
# 仅作 YAML 骨架,注释说明用途。实际运行需配合 Litmus Chaos Operator。
|
||||
# 文档:https://litmuschaos.github.io/litmus/
|
||||
apiVersion: litmuschaos.io/v1alpha1
|
||||
kind: ChaosEngine
|
||||
metadata:
|
||||
name: edu-chaos-experiments
|
||||
namespace: edu-monitoring
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: chaos
|
||||
spec:
|
||||
appinfo:
|
||||
appns: edu-services
|
||||
applabel: "app.kubernetes.io/name=api-gateway"
|
||||
appkind: deployment
|
||||
chaosServiceAccount: litmus-chaos-sa
|
||||
components:
|
||||
experiments:
|
||||
# ============================================================
|
||||
# 实验 1:服务 Pod 杀死(验证自愈与副本数)
|
||||
# ============================================================
|
||||
- name: pod-delete
|
||||
spec:
|
||||
components:
|
||||
env:
|
||||
# 杀死 1 个 Pod
|
||||
- name: TOTAL_CHAOS_DURATION
|
||||
value: "30"
|
||||
- name: CHAOS_INTERVAL
|
||||
value: "10"
|
||||
- name: FORCE
|
||||
value: "false"
|
||||
- name: PODS_AFFECTED_PERC
|
||||
value: "50"
|
||||
- name: TARGET_CONTAINER
|
||||
value: "api-gateway"
|
||||
# 假设:杀死 50% Pod 后,服务仍可对外可用(最少 1 个副本健康)
|
||||
# 稳态:up{job="api-gateway"} >= 1
|
||||
steadyStateHypothesis:
|
||||
steadyStateHypothesis:
|
||||
probe:
|
||||
- name: api-gateway-still-up
|
||||
type: httpProbe
|
||||
mode: Continuous
|
||||
runProperties:
|
||||
probeTimeout: 5
|
||||
httpProbeInputs:
|
||||
url: http://api-gateway.edu-services.svc:8080/healthz
|
||||
method:
|
||||
get: {}
|
||||
phases:
|
||||
- name: pre-chaos
|
||||
description: "混沌前稳态验证"
|
||||
- name: inject
|
||||
description: "注入 Pod 删除"
|
||||
- name: post-chaos
|
||||
description: "混沌后自愈验证"
|
||||
# ============================================================
|
||||
# 实验 2:网络延迟注入(验证超时与重试)
|
||||
# ============================================================
|
||||
- name: pod-network-latency
|
||||
spec:
|
||||
components:
|
||||
env:
|
||||
# 注入 200ms 网络延迟
|
||||
- name: NETWORK_LATENCY
|
||||
value: "200"
|
||||
- name: TOTAL_CHAOS_DURATION
|
||||
value: "60"
|
||||
- name: CHAOS_INTERVAL
|
||||
value: "10"
|
||||
- name: NETWORK_INTERFACE
|
||||
value: "eth0"
|
||||
- name: TARGET_CONTAINER
|
||||
value: "api-gateway"
|
||||
# 假设:200ms 延迟下 P99 < 2s,无 5xx 雪崩
|
||||
# 稳态:错误率 < 5%
|
||||
steadyStateHypothesis:
|
||||
steadyStateHypothesis:
|
||||
probe:
|
||||
- name: error-rate-below-threshold
|
||||
type: promProbe
|
||||
mode: Continuous
|
||||
runProperties:
|
||||
probeTimeout: 5
|
||||
promProbeInputs:
|
||||
source:
|
||||
url: http://prometheus.edu-monitoring.svc:9090
|
||||
query: |
|
||||
sum(rate(http_requests_total{service="api-gateway",status=~"5.."}[1m]))
|
||||
/ sum(rate(http_requests_total{service="api-gateway"}[1m]))
|
||||
comparator:
|
||||
criteria: "<="
|
||||
value: "0.05"
|
||||
phases:
|
||||
- name: pre-chaos
|
||||
description: "混沌前稳态验证"
|
||||
- name: inject
|
||||
description: "注入 200ms 网络延迟"
|
||||
- name: post-chaos
|
||||
description: "混沌后恢复验证"
|
||||
# ============================================================
|
||||
# 实验 3:磁盘填充(验证磁盘压力下日志写入与告警)
|
||||
# ============================================================
|
||||
- name: disk-fill
|
||||
spec:
|
||||
components:
|
||||
env:
|
||||
# 填充至 80% 磁盘使用率
|
||||
- name: FILL_PERCENTAGE
|
||||
value: "80"
|
||||
- name: TOTAL_CHAOS_DURATION
|
||||
value: "120"
|
||||
- name: CHAOS_INTERVAL
|
||||
value: "30"
|
||||
- name: TARGET_CONTAINER
|
||||
value: "api-gateway"
|
||||
# 假设:磁盘 80% 使用率下服务仍可写入日志,触发 DiskSpaceLow 告警
|
||||
# 稳态:服务 /healthz 可用
|
||||
steadyStateHypothesis:
|
||||
steadyStateHypothesis:
|
||||
probe:
|
||||
- name: api-gateway-healthz
|
||||
type: httpProbe
|
||||
mode: Continuous
|
||||
runProperties:
|
||||
probeTimeout: 5
|
||||
httpProbeInputs:
|
||||
url: http://api-gateway.edu-services.svc:8080/healthz
|
||||
method:
|
||||
get: {}
|
||||
phases:
|
||||
- name: pre-chaos
|
||||
description: "混沌前稳态验证"
|
||||
- name: inject
|
||||
description: "填充磁盘至 80%"
|
||||
- name: post-chaos
|
||||
description: "混沌后清理与恢复验证"
|
||||
14
infra/clickhouse/users.d/custom-users.xml
Normal file
14
infra/clickhouse/users.d/custom-users.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<clickhouse>
|
||||
<!-- 覆盖 default-user.xml 的本地限制,允许 default 用户从任意 IP 用密码访问 -->
|
||||
<users>
|
||||
<default>
|
||||
<password>clickhouse</password>
|
||||
<networks>
|
||||
<ip>::/0</ip>
|
||||
</networks>
|
||||
<profile>default</profile>
|
||||
<quota>default</quota>
|
||||
<access_management>1</access_management>
|
||||
</default>
|
||||
</users>
|
||||
</clickhouse>
|
||||
55
infra/deploy.env.example
Normal file
55
infra/deploy.env.example
Normal file
@@ -0,0 +1,55 @@
|
||||
# 服务器部署环境变量模板
|
||||
# 使用方式:复制到 /opt/edu/.env 并填入生产值
|
||||
# cp infra/deploy.env.example /opt/edu/.env
|
||||
# vim /opt/edu/.env
|
||||
#
|
||||
# 安全警告:
|
||||
# - 此文件含敏感信息,禁止提交到 Git
|
||||
# - .gitignore 已忽略 .env
|
||||
# - 仅 SRE AI 或人类决策者可编辑
|
||||
|
||||
# ============ 数据库(服务器已有 MySQL)============
|
||||
# DATABASE_URL 中的 host 需用容器名(如 edu-mysql)而非 localhost
|
||||
# 因为应用容器通过 edu-shared 网络访问 MySQL 容器
|
||||
DATABASE_URL=mysql://edu:changeme@edu-mysql:3306/next_edu_cloud
|
||||
|
||||
# ============ Redis(服务器已有 Redis)============
|
||||
# 同理用容器名
|
||||
REDIS_URL=redis://edu-redis:6379
|
||||
|
||||
# ============ JWT(生产密钥,必须修改)============
|
||||
# 生成方式:openssl rand -hex 32
|
||||
JWT_SECRET=CHANGE_ME_TO_STRONG_RANDOM_SECRET
|
||||
JWT_ISSUER=next-edu-cloud
|
||||
JWT_AUDIENCE=next-edu-cloud
|
||||
|
||||
# ============ 服务端口 ============
|
||||
API_GATEWAY_PORT=8080
|
||||
TEACHER_PORTAL_PORT=3000
|
||||
|
||||
# ============ Kafka(P3+ 启用,留空则 Outbox publisher 持续重试)============
|
||||
KAFKA_BROKERS=
|
||||
|
||||
# ============ 可观测性(P6 启用)============
|
||||
# OTLP collector 端点,留空则服务跳过 trace 上报
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
|
||||
LOG_LEVEL=info
|
||||
|
||||
# ============ Neo4j(content 服务,留空则降级模式)============
|
||||
NEO4J_URL=
|
||||
NEO4J_PASSWORD=
|
||||
|
||||
# ============ Elasticsearch(msg 服务,留空则降级模式)============
|
||||
ES_URL=
|
||||
|
||||
# ============ ClickHouse(data-ana 服务,留空则降级模式)============
|
||||
CLICKHOUSE_HOST=
|
||||
CLICKHOUSE_PORT=8123
|
||||
CLICKHOUSE_DATABASE=edu_analytics
|
||||
CLICKHOUSE_USER=
|
||||
CLICKHOUSE_PASSWORD=
|
||||
|
||||
# ============ LLM 配置(ai 服务,留空则降级模式)============
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
ANTHROPIC_API_KEY=
|
||||
328
infra/docker-compose.deploy.yml
Normal file
328
infra/docker-compose.deploy.yml
Normal file
@@ -0,0 +1,328 @@
|
||||
# 服务器部署用 Docker Compose(no-push 本地构建模式)
|
||||
# 镜像来源:CI 容器内本地 docker build(不推送到 registry)
|
||||
# 基础设施:MySQL + Redis 已在服务器 Docker 中运行(不在此文件管理)
|
||||
#
|
||||
# 部署目录:/opt/edu/
|
||||
# 部署命令(CI 自动执行):
|
||||
# docker compose up -d --build --remove-orphans
|
||||
#
|
||||
# 首次部署手动步骤:
|
||||
# 1. sudo mkdir -p /opt/edu && sudo chown -R $USER:$USER /opt/edu
|
||||
# 2. cp infra/docker-compose.deploy.yml /opt/edu/docker-compose.yml
|
||||
# 3. cp infra/deploy.env.example /opt/edu/.env && 编辑填入生产密钥
|
||||
# 4. docker compose up -d --build
|
||||
#
|
||||
# 注意:compose 文件中的 build.context 路径相对于 /opt/edu/ 目录
|
||||
# CI 在 deploy 步骤会先把仓库 checkout 到 /opt/edu/repo/,再 cp compose 文件到 /opt/edu/
|
||||
|
||||
name: edu
|
||||
|
||||
services:
|
||||
# ============================================================
|
||||
# 应用服务(10 个:api-gateway + 3 Go/Python + 6 NestJS)
|
||||
# ============================================================
|
||||
api-gateway:
|
||||
build:
|
||||
context: ./repo/services/api-gateway
|
||||
dockerfile: Dockerfile
|
||||
container_name: edu-api-gateway
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
API_GATEWAY_PORT: ${API_GATEWAY_PORT:-8080}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
|
||||
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
|
||||
# 生产环境强制关闭 dev-token 旁路
|
||||
DEV_MODE: "false"
|
||||
CLASSES_SERVICE_URL: http://classes:3001
|
||||
IAM_SERVICE_URL: http://iam:3002
|
||||
TEACHER_BFF_URL: http://teacher-bff:3003
|
||||
CORE_EDU_SERVICE_URL: http://core-edu:3004
|
||||
CONTENT_SERVICE_URL: http://content:3005
|
||||
DATA_ANA_SERVICE_URL: http://data-ana:3006
|
||||
MSG_SERVICE_URL: http://msg:3007
|
||||
AI_SERVICE_URL: http://ai:3008
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
ports:
|
||||
- "${API_GATEWAY_PORT:-8080}:8080"
|
||||
depends_on:
|
||||
classes:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
classes:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: services/classes/Dockerfile
|
||||
container_name: edu-classes
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3001
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3001/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
iam:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: services/iam/Dockerfile
|
||||
container_name: edu-iam
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3002
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
|
||||
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
NODE_ENV: production
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3002/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
teacher-bff:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: services/teacher-bff/Dockerfile
|
||||
container_name: edu-teacher-bff
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3003
|
||||
IAM_SERVICE_URL: http://iam:3002
|
||||
CLASSES_SERVICE_URL: http://classes:3001
|
||||
CORE_EDU_SERVICE_URL: http://core-edu:3004
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
iam:
|
||||
condition: service_healthy
|
||||
classes:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3003/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
core-edu:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: services/core-edu/Dockerfile
|
||||
container_name: edu-core-edu
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3004
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
NODE_ENV: production
|
||||
DEV_MODE: "false"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3004/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
content:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: services/content/Dockerfile
|
||||
container_name: edu-content
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3005
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
NEO4J_URL: ${NEO4J_URL:-}
|
||||
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-}
|
||||
ES_URL: ${ES_URL:-}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
NODE_ENV: production
|
||||
DEV_MODE: "false"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3005/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
msg:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: services/msg/Dockerfile
|
||||
container_name: edu-msg
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3007
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
|
||||
ES_URL: ${ES_URL:-}
|
||||
PUSH_GATEWAY_URL: http://push-gateway:8081
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
NODE_ENV: production
|
||||
DEV_MODE: "false"
|
||||
depends_on:
|
||||
push-gateway:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3007/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
ai:
|
||||
build:
|
||||
context: ./repo/services/ai
|
||||
dockerfile: Dockerfile
|
||||
container_name: edu-ai
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3008
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
OTEL_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
DEV_MODE: "false"
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3008/healthz')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
data-ana:
|
||||
build:
|
||||
context: ./repo/services/data-ana
|
||||
dockerfile: Dockerfile
|
||||
container_name: edu-data-ana
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3006
|
||||
CLICKHOUSE_HOST: ${CLICKHOUSE_HOST:-}
|
||||
CLICKHOUSE_PORT: ${CLICKHOUSE_PORT:-8123}
|
||||
CLICKHOUSE_DATABASE: ${CLICKHOUSE_DATABASE:-edu_analytics}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-}
|
||||
OTEL_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
DEV_MODE: "false"
|
||||
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3006/healthz')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
push-gateway:
|
||||
build:
|
||||
context: ./repo/services/push-gateway
|
||||
dockerfile: Dockerfile
|
||||
container_name: edu-push-gateway
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PUSH_GATEWAY_PORT: 8081
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
DEV_MODE: "false"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8081/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
teacher-portal:
|
||||
build:
|
||||
context: ./repo
|
||||
dockerfile: apps/teacher-portal/Dockerfile
|
||||
container_name: edu-teacher-portal
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
API_GATEWAY_URL: http://api-gateway:8080
|
||||
ports:
|
||||
- "${TEACHER_PORTAL_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
api-gateway:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3000/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
- edu-shared
|
||||
|
||||
networks:
|
||||
# 应用服务内部网络
|
||||
edu-net:
|
||||
driver: bridge
|
||||
# 与已有 MySQL/Redis 共享的网络
|
||||
# 需确保 MySQL/Redis 容器已加入名为 edu-shared 的网络:
|
||||
# docker network create edu-shared (若不存在)
|
||||
# docker network connect edu-shared edu-mysql
|
||||
# docker network connect edu-shared edu-redis
|
||||
edu-shared:
|
||||
external: true
|
||||
140
infra/docker-compose.monitoring.yml
Normal file
140
infra/docker-compose.monitoring.yml
Normal file
@@ -0,0 +1,140 @@
|
||||
# 监控栈 docker-compose(独立 profile)
|
||||
# 使用方式: docker compose -f docker-compose.monitoring.yml --profile monitoring up -d
|
||||
version: "3.9"
|
||||
|
||||
networks:
|
||||
edu-network:
|
||||
name: edu-network
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
name: edu-prometheus-data
|
||||
alertmanager-data:
|
||||
name: edu-alertmanager-data
|
||||
grafana-data:
|
||||
name: edu-grafana-data
|
||||
loki-data:
|
||||
name: edu-loki-data
|
||||
|
||||
services:
|
||||
# ============================================================
|
||||
# Prometheus - 指标采集与存储
|
||||
# ============================================================
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.54.1
|
||||
container_name: edu-prometheus
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=15d"
|
||||
- "--web.enable-lifecycle"
|
||||
- "--web.enable-admin-api"
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./prometheus/rules.yml:/etc/prometheus/rules.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
networks:
|
||||
- edu-network
|
||||
|
||||
# ============================================================
|
||||
# Alertmanager - 告警路由与抑制
|
||||
# ============================================================
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.27.0
|
||||
container_name: edu-alertmanager
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/alertmanager/alertmanager.yml"
|
||||
- "--storage.path=/alertmanager"
|
||||
ports:
|
||||
- "9093:9093"
|
||||
volumes:
|
||||
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
||||
- alertmanager-data:/alertmanager
|
||||
networks:
|
||||
- edu-network
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
# ============================================================
|
||||
# Grafana - 可视化
|
||||
# ============================================================
|
||||
grafana:
|
||||
image: grafana/grafana:11.2.2
|
||||
container_name: edu-grafana
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=false
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
networks:
|
||||
- edu-network
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
# ============================================================
|
||||
# node-exporter - 主机指标采集
|
||||
# ============================================================
|
||||
node-exporter:
|
||||
image: prom/node-exporter:v1.8.2
|
||||
container_name: edu-node-exporter
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--path.rootfs=/host"
|
||||
ports:
|
||||
- "9100:9100"
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host:ro
|
||||
networks:
|
||||
- edu-network
|
||||
|
||||
# ============================================================
|
||||
# Loki - 日志聚合(P6 硬化新增)
|
||||
# ============================================================
|
||||
loki:
|
||||
image: grafana/loki:3.2.1
|
||||
container_name: edu-loki
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
ports:
|
||||
- "3100:3100"
|
||||
volumes:
|
||||
- loki-data:/loki
|
||||
networks:
|
||||
- edu-network
|
||||
|
||||
# ============================================================
|
||||
# Promtail - 日志采集(收集 Docker 容器日志发送到 Loki)
|
||||
# ============================================================
|
||||
promtail:
|
||||
image: grafana/promtail:3.2.1
|
||||
container_name: edu-promtail
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command: -config.file=/etc/promtail/config.yml
|
||||
volumes:
|
||||
- ./promtail/config.yml:/etc/promtail/config.yml:ro
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- edu-network
|
||||
depends_on:
|
||||
- loki
|
||||
105
infra/docker-compose.prod.yml
Normal file
105
infra/docker-compose.prod.yml
Normal file
@@ -0,0 +1,105 @@
|
||||
# 生产环境应用服务编排
|
||||
# 仅包含应用服务(api-gateway / classes / teacher-portal),基础设施请使用 docker-compose.yml 或 K8s
|
||||
#
|
||||
# 使用方式:
|
||||
# 1. 先构建镜像:
|
||||
# docker compose -f infra/docker-compose.prod.yml build
|
||||
# 2. 启动(依赖基础设施已运行):
|
||||
# docker compose -f infra/docker-compose.prod.yml up -d
|
||||
# 3. 查看日志:
|
||||
# docker compose -f infra/docker-compose.prod.yml logs -f
|
||||
#
|
||||
# 前置条件:
|
||||
# - MySQL(:3306)、Redis(:6379)已通过 docker-compose.minimal.yml 或外部方式启动
|
||||
# - .env 文件已配置(DEV_MODE 必须为 false)
|
||||
|
||||
name: edu-prod
|
||||
|
||||
services:
|
||||
api-gateway:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: services/api-gateway/Dockerfile
|
||||
image: edu/api-gateway:latest
|
||||
container_name: edu-api-gateway
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
API_GATEWAY_PORT: ${API_GATEWAY_PORT:-8080}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
|
||||
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
|
||||
# 生产环境强制关闭 dev-token 旁路
|
||||
DEV_MODE: "false"
|
||||
CLASSES_SERVICE_URL: http://classes:3001
|
||||
IAM_SERVICE_URL: http://iam:3002
|
||||
TEACHER_BFF_URL: http://teacher-bff:3003
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
ports:
|
||||
- "${API_GATEWAY_PORT:-8080}:8080"
|
||||
depends_on:
|
||||
classes:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
|
||||
classes:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: services/classes/Dockerfile
|
||||
image: edu/classes:latest
|
||||
container_name: edu-classes
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3001
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
# classes 连接宿主机 MySQL/Redis 时,host.docker.internal 在 Docker Desktop 上可用
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3001/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
networks:
|
||||
- edu-net
|
||||
|
||||
teacher-portal:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: apps/teacher-portal/Dockerfile
|
||||
image: edu/teacher-portal:latest
|
||||
container_name: edu-teacher-portal
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
API_GATEWAY_URL: http://api-gateway:8080
|
||||
ports:
|
||||
- "${TEACHER_PORTAL_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
api-gateway:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3000/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
networks:
|
||||
- edu-net
|
||||
|
||||
networks:
|
||||
edu-net:
|
||||
driver: bridge
|
||||
55
infra/docker-compose.tools.yml
Normal file
55
infra/docker-compose.tools.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
# CI/CD 工具镜像预拉(一次性拉取所有 CI 与构建需要的镜像到本地)
|
||||
# 用途:actrunner 的 container job 会优先使用本地镜像缓存,避免每次 CI 联网拉取
|
||||
#
|
||||
# 用法:
|
||||
# docker compose -f infra/docker-compose.tools.yml pull
|
||||
#
|
||||
# 拉取后可用 docker images 查看所有 edu 相关镜像
|
||||
# 这不是常驻服务,只是借用 compose 的批量 pull 能力
|
||||
|
||||
name: edu-tools
|
||||
|
||||
services:
|
||||
# ===== CI 运行时镜像(actrunner container job 使用)=====
|
||||
node-ci:
|
||||
image: node:22-alpine
|
||||
command: ["echo", "node-ci image pulled"]
|
||||
|
||||
golang-ci:
|
||||
image: golang:1.22-alpine
|
||||
command: ["echo", "golang-ci image pulled"]
|
||||
|
||||
buf-ci:
|
||||
image: bufbuild/buf:latest
|
||||
command: ["echo", "buf-ci image pulled"]
|
||||
|
||||
docker-ci:
|
||||
image: docker:25-git
|
||||
command: ["echo", "docker-ci image pulled"]
|
||||
|
||||
# ===== 服务构建基础镜像(Dockerfile FROM)=====
|
||||
# teacher-portal / classes 的 builder 阶段
|
||||
node-builder:
|
||||
image: node:20-alpine
|
||||
command: ["echo", "node-builder image pulled"]
|
||||
|
||||
# api-gateway 的 builder 阶段
|
||||
golang-builder:
|
||||
image: golang:1.22-alpine
|
||||
command: ["echo", "golang-builder image pulled"]
|
||||
|
||||
# api-gateway 的 runner 阶段
|
||||
alpine-runner:
|
||||
image: alpine:3.20
|
||||
command: ["echo", "alpine-runner image pulled"]
|
||||
|
||||
# ===== 开发/测试基础设施(生产用已有的,不在此管理)=====
|
||||
# 服务器已有的 MySQL/Redis 不在此拉取
|
||||
# 仅用于本地开发测试环境(docker-compose.dev.yml,如有)
|
||||
mysql-dev:
|
||||
image: mysql:8.0
|
||||
command: ["echo", "mysql-dev image pulled"]
|
||||
|
||||
redis-dev:
|
||||
image: redis:7-alpine
|
||||
command: ["echo", "redis-dev image pulled"]
|
||||
@@ -1,7 +1,8 @@
|
||||
name: edu-full
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
# 镜像加速:通过 daocloud 镜像源绕过 docker.io 被墙问题
|
||||
image: docker.m.daocloud.io/library/mysql:8.0
|
||||
container_name: edu-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -20,7 +21,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
image: docker.m.daocloud.io/library/redis:7-alpine
|
||||
container_name: edu-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -33,7 +34,7 @@ services:
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
kafka:
|
||||
image: confluentinc/cp-kafka:7.6.0
|
||||
image: docker.m.daocloud.io/confluentinc/cp-kafka:7.6.0
|
||||
container_name: edu-kafka
|
||||
profiles: ["p3", "p4", "p5", "p6"]
|
||||
restart: unless-stopped
|
||||
@@ -43,7 +44,13 @@ services:
|
||||
environment:
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
|
||||
# 双 listener:INSIDE 容器间互访(kafka:29092),OUTSIDE 主机访问(localhost:9092)
|
||||
# 必须用 INSIDE 作为 inter.broker.listener.name,否则 Debezium Connect 拿到 metadata
|
||||
# 后会切回 advertised.listeners 中的 localhost,导致连接失败
|
||||
KAFKA_LISTENERS: INSIDE://:29092,OUTSIDE://:9092
|
||||
KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:29092,OUTSIDE://localhost:9092
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
ports:
|
||||
@@ -54,7 +61,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
zookeeper:
|
||||
image: confluentinc/cp-zookeeper:7.6.0
|
||||
image: docker.m.daocloud.io/confluentinc/cp-zookeeper:7.6.0
|
||||
container_name: edu-zookeeper
|
||||
profiles: ["p3", "p4", "p5", "p6"]
|
||||
restart: unless-stopped
|
||||
@@ -66,7 +73,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:24.3
|
||||
image: docker.m.daocloud.io/clickhouse/clickhouse-server:24.3
|
||||
container_name: edu-clickhouse
|
||||
profiles: ["p4", "p5", "p6"]
|
||||
restart: unless-stopped
|
||||
@@ -75,13 +82,15 @@ services:
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
- clickhouse_data:/var/lib/clickhouse
|
||||
# 覆盖默认 default-user.xml 限制(默认仅允许 127.0.0.1/::1 无密码访问)
|
||||
- ./clickhouse/users.d/custom-users.xml:/etc/clickhouse-server/users.d/custom-users.xml:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
neo4j:
|
||||
image: neo4j:5.20
|
||||
image: docker.m.daocloud.io/library/neo4j:5.20
|
||||
container_name: edu-neo4j
|
||||
profiles: ["p4", "p5", "p6"]
|
||||
restart: unless-stopped
|
||||
@@ -98,6 +107,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
elasticsearch:
|
||||
# Elastic 官方镜像在 docker.elastic.co,非 Docker Hub,通常不被墙
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
|
||||
container_name: edu-es
|
||||
profiles: ["p5", "p6"]
|
||||
@@ -116,7 +126,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:1.57
|
||||
image: docker.m.daocloud.io/jaegertracing/all-in-one:1.57
|
||||
container_name: edu-jaeger
|
||||
profiles: ["observability"]
|
||||
restart: unless-stopped
|
||||
@@ -125,30 +135,124 @@ services:
|
||||
ports:
|
||||
- "16686:16686"
|
||||
- "4318:4318"
|
||||
# ============================================================
|
||||
# Debezium Connect - CDC 链路核心
|
||||
# 监听 MySQL binlog → 写入 Kafka topic
|
||||
# topic 命名约定:<prefix>.<database>.<table>(如 edu-cdc.next_edu_cloud.grades)
|
||||
# ============================================================
|
||||
debezium-connect:
|
||||
image: quay.io/debezium/connect:2.7
|
||||
container_name: edu-debezium
|
||||
profiles: ["p4", "p5", "p6"]
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
kafka:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# Kafka Connect 基础配置(Debezium 2.x 容器映射规则:环境变量名大写 → connect 配置项)
|
||||
# 必须用 INSIDE listener (kafka:29092),否则会拿到 OUTSIDE 的 localhost metadata 导致连不上
|
||||
BOOTSTRAP_SERVERS: kafka:29092
|
||||
GROUP_ID: edu-debezium
|
||||
CONFIG_STORAGE_TOPIC: edu-connect-configs
|
||||
OFFSET_STORAGE_TOPIC: edu-connect-offsets
|
||||
STATUS_STORAGE_TOPIC: edu-connect-status
|
||||
# 内部 converter 配置(必须与 Debezium 事件格式一致)
|
||||
CONFIG_STORAGE_REPLICATION_FACTOR: "1"
|
||||
OFFSET_STORAGE_REPLICATION_FACTOR: "1"
|
||||
STATUS_STORAGE_REPLICATION_FACTOR: "1"
|
||||
KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
|
||||
VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
|
||||
KEY_CONVERTER_SCHEMAS_ENABLE: "false"
|
||||
VALUE_CONVERTER_SCHEMAS_ENABLE: "false"
|
||||
# 监听端口
|
||||
REST_PORT: 8083
|
||||
REST_ADVERTISED_HOST_NAME: debezium-connect
|
||||
# 日志级别
|
||||
LOG_LEVEL: INFO
|
||||
ports:
|
||||
- "8083:8083"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8083/connectors"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 10
|
||||
networks:
|
||||
- default
|
||||
prometheus:
|
||||
image: prom/prometheus:v0.51.0
|
||||
image: docker.m.daocloud.io/prom/prometheus:v2.51.0
|
||||
container_name: edu-prometheus
|
||||
profiles: ["observability"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=15d"
|
||||
- "--web.enable-lifecycle"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus_data:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
grafana:
|
||||
image: grafana/grafana:10.4.0
|
||||
image: docker.m.daocloud.io/grafana/grafana:10.4.0
|
||||
container_name: edu-grafana
|
||||
profiles: ["observability"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_PASSWORD: admin
|
||||
ports:
|
||||
- "3001:3001"
|
||||
- "3030:3000"
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
# ============================================================
|
||||
# Exporters(observability profile,与 Prometheus 同网络)
|
||||
# ============================================================
|
||||
node-exporter:
|
||||
image: docker.m.daocloud.io/prom/node-exporter:v1.8.2
|
||||
container_name: edu-node-exporter
|
||||
profiles: ["observability"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--path.rootfs=/host"
|
||||
ports:
|
||||
- "9100:9100"
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host:ro
|
||||
mysql-exporter:
|
||||
image: docker.m.daocloud.io/prom/mysqld-exporter:v0.15.1
|
||||
container_name: edu-mysql-exporter
|
||||
profiles: ["observability"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--mysqld.address=edu-mysql:3306"
|
||||
- "--mysqld.username=edu:changeme"
|
||||
environment:
|
||||
MYSQLD_EXPORTER_PASSWORD: "changeme"
|
||||
ports:
|
||||
- "9104:9104"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis-exporter:
|
||||
image: docker.m.daocloud.io/oliver006/redis_exporter:v1.67.0
|
||||
container_name: edu-redis-exporter
|
||||
profiles: ["observability"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
REDIS_ADDR: "redis://edu-redis:6379"
|
||||
ports:
|
||||
- "9121:9121"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
clickhouse_data:
|
||||
neo4j_data:
|
||||
es_data:
|
||||
grafana_data:
|
||||
grafana_data:
|
||||
prometheus_data:
|
||||
|
||||
394
infra/grafana/dashboards/microservices-overview.json
Normal file
394
infra/grafana/dashboards/microservices-overview.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Edu 平台微服务总览 - QPS / 错误率 / P99 延迟 / 熔断器状态",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "reqps"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "sum(rate(http_requests_total[1m])) by (service)",
|
||||
"legendFormat": "{{service}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "请求 QPS(按服务)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 20,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "line"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service)",
|
||||
"legendFormat": "{{service}} 5xx",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "错误率(5xx 占比)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "line"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))",
|
||||
"legendFormat": "{{service}} P99",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "P99 延迟",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"text": "CLOSED"
|
||||
},
|
||||
"1": {
|
||||
"color": "red",
|
||||
"index": 1,
|
||||
"text": "OPEN"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto"
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "max(circuit_breaker_state{state=\"open\"}) by (service)",
|
||||
"legendFormat": "{{service}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "熔断器状态(0=CLOSED, 1=OPEN)",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"style": "dark",
|
||||
"tags": ["edu", "microservices", "overview"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Edu 微服务总览",
|
||||
"uid": "edu-microservices-overview",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
15
infra/grafana/provisioning/dashboards/dashboards.yml
Normal file
15
infra/grafana/provisioning/dashboards/dashboards.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Grafana 仪表盘 provisioning
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: edu-dashboards
|
||||
orgId: 1
|
||||
folder: "Edu"
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 30
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
# 从该目录加载所有仪表盘 JSON
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: false
|
||||
24
infra/grafana/provisioning/datasources/prometheus.yml
Normal file
24
infra/grafana/provisioning/datasources/prometheus.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Grafana 数据源 provisioning
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
uid: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: false
|
||||
jsonData:
|
||||
timeInterval: "15s"
|
||||
httpMethod: POST
|
||||
manageAlerts: false
|
||||
|
||||
- name: Loki
|
||||
type: loki
|
||||
uid: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
editable: false
|
||||
jsonData:
|
||||
maxLines: 1000
|
||||
118
infra/k8s/README.md
Normal file
118
infra/k8s/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# K8s 部署说明
|
||||
|
||||
> **状态**:已迁移到 Helm Chart 管理。本目录保留 `namespace.yaml` 作为基础资源,其余资源通过 `helm/` 下的 chart 部署。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
infra/k8s/
|
||||
├─ namespace.yaml # 基础命名空间(4 个:system/services/monitoring/ingress)
|
||||
└─ helm/ # Helm Chart 仓库
|
||||
├─ edu-platform/ # 平台级 chart(namespace/configmap/secret/ingress/hpa)
|
||||
│ ├─ Chart.yaml
|
||||
│ ├─ values.yaml # 全局默认值
|
||||
│ ├─ values-dev.yaml # 开发环境覆盖
|
||||
│ ├─ values-staging.yaml # 预发布环境覆盖
|
||||
│ ├─ values-prod.yaml # 生产环境覆盖
|
||||
│ └─ templates/
|
||||
│ ├─ _helpers.tpl
|
||||
│ ├─ namespace.yaml
|
||||
│ ├─ configmap.yaml
|
||||
│ ├─ secret.yaml # 骨架;生产请用 External Secrets Operator
|
||||
│ ├─ ingress.yaml
|
||||
│ └─ hpa.yaml # 全局 HPA 示例(默认不渲染)
|
||||
├─ api-gateway/ # 服务级 chart(完整迁移自原 manifest)
|
||||
│ ├─ Chart.yaml
|
||||
│ ├─ values.yaml
|
||||
│ └─ templates/
|
||||
│ ├─ _helpers.tpl
|
||||
│ ├─ deployment.yaml
|
||||
│ ├─ service.yaml
|
||||
│ ├─ configmap.yaml
|
||||
│ └─ hpa.yaml
|
||||
├─ iam/ # 业务服务 chart 桩(P2)
|
||||
├─ core-edu/ # 业务服务 chart 桩(P3)
|
||||
├─ content/ # 业务服务 chart 桩(P4)
|
||||
├─ msg/ # 业务服务 chart 桩(P5)
|
||||
├─ data-ana/ # 业务服务 chart 桩(P4)
|
||||
└─ ai/ # 业务服务 chart 桩(P5)
|
||||
```
|
||||
|
||||
## 命名空间规划
|
||||
|
||||
| Namespace | 用途 |
|
||||
| ---------------- | ---------------------------------------------------------- |
|
||||
| `edu-system` | 系统组件(数据库代理、配置等) |
|
||||
| `edu-services` | 业务微服务(api-gateway / iam / core-edu / content / msg) |
|
||||
| `edu-monitoring` | 监控栈(Prometheus / Grafana / Alertmanager) |
|
||||
| `edu-ingress` | 入口控制器(NGINX Ingress / cert-manager) |
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 1. 安装平台级 chart(命名空间 / 全局 ConfigMap / Secret / Ingress)
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
helm install edu-platform ./helm/edu-platform -f ./helm/edu-platform/values-dev.yaml
|
||||
|
||||
# 生产环境
|
||||
helm install edu-platform ./helm/edu-platform -f ./helm/edu-platform/values-prod.yaml \
|
||||
--set secret.data.MYSQL_PASSWORD=<base64> \
|
||||
--set secret.data.JWT_SECRET=<base64> \
|
||||
--set secret.data.REDIS_PASSWORD=<base64>
|
||||
```
|
||||
|
||||
### 2. 安装服务级 chart
|
||||
|
||||
```bash
|
||||
# api-gateway
|
||||
helm install api-gateway ./helm/api-gateway
|
||||
|
||||
# 其他业务服务(iam / core-edu / content / msg / data-ana / ai)
|
||||
helm install iam ./helm/iam
|
||||
helm install core-edu ./helm/core-edu
|
||||
# ...
|
||||
```
|
||||
|
||||
### 3. 应用基础命名空间(如未通过 helm 安装 edu-platform)
|
||||
|
||||
```bash
|
||||
kubectl apply -f namespace.yaml
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# lint 所有 chart
|
||||
helm lint helm/edu-platform helm/api-gateway helm/iam helm/core-edu helm/content helm/msg helm/data-ana helm/ai
|
||||
|
||||
# 渲染模板(不实际部署)
|
||||
helm template edu-platform ./helm/edu-platform
|
||||
helm template api-gateway ./helm/api-gateway
|
||||
```
|
||||
|
||||
## 环境差异
|
||||
|
||||
| 环境 | values 文件 | 副本数 | HPA | TLS |
|
||||
| ------- | ------------------- | ------ | ---- | ---- |
|
||||
| dev | values-dev.yaml | 1 | 关闭 | 关闭 |
|
||||
| staging | values-staging.yaml | 2 | 2-5 | 开启 |
|
||||
| prod | values-prod.yaml | 3 | 3-20 | 开启 |
|
||||
|
||||
## 敏感配置
|
||||
|
||||
⚠️ **生产环境禁止在 values.yaml 中硬编码密钥**。
|
||||
|
||||
推荐方案:
|
||||
|
||||
1. 使用 [External Secrets Operator](https://external-secrets.io/) 对接 Vault / KMS / 云 KMS
|
||||
2. 通过 `--set secret.data.<KEY>=<base64>` 临时注入
|
||||
3. 通过 ArgoCD / Flux GitOps + Sealed Secrets
|
||||
|
||||
## 后续路线
|
||||
|
||||
- [ ] 接入 External Secrets Operator
|
||||
- [ ] ArgoCD / Flux GitOps 部署
|
||||
- [ ] 各服务 chart 补充 configmap.yaml / hpa.yaml 模板(当前桩仅含 deployment/service)
|
||||
- [ ] 服务级 values-dev/staging/prod 覆盖文件
|
||||
- [ ] CI/CD 集成(helm chart 推送到 OCI registry)
|
||||
11
infra/k8s/helm/ai/Chart.yaml
Normal file
11
infra/k8s/helm/ai/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: ai
|
||||
description: ai 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- ai
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/ai/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/ai/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "ai.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ai.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ai.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "ai.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: ai-gateway
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ai.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "ai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/ai/templates/deployment.yaml
Normal file
77
infra/k8s/helm/ai/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "ai.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "ai.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "ai.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "ai.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: ai-gateway
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "ai.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "ai.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/ai/templates/service.yaml
Normal file
16
infra/k8s/helm/ai/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "ai.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "ai.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "ai.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/ai/values.yaml
Normal file
85
infra/k8s/helm/ai/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# ai 服务默认值(AI 网关 - 业务领域 D6)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/ai
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3006
|
||||
targetPort: 3006
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3006
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
12
infra/k8s/helm/api-gateway/Chart.yaml
Normal file
12
infra/k8s/helm/api-gateway/Chart.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v2
|
||||
name: api-gateway
|
||||
description: api-gateway 服务级 Helm Chart(从 infra/k8s/api-gateway-deployment.yaml 迁移)
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- gateway
|
||||
- api
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/api-gateway/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/api-gateway/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "api-gateway.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "api-gateway.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "api-gateway.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "api-gateway.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: gateway
|
||||
{{- end -}}
|
||||
|
||||
{{- define "api-gateway.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "api-gateway.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
13
infra/k8s/helm/api-gateway/templates/configmap.yaml
Normal file
13
infra/k8s/helm/api-gateway/templates/configmap.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.configMap.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}-config
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
{{ $k }}: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
77
infra/k8s/helm/api-gateway/templates/deployment.yaml
Normal file
77
infra/k8s/helm/api-gateway/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "api-gateway.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "api-gateway.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: gateway
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "api-gateway.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "api-gateway.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
29
infra/k8s/helm/api-gateway/templates/hpa.yaml
Normal file
29
infra/k8s/helm/api-gateway/templates/hpa.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- if .Values.hpa.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}-hpa
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "api-gateway.name" . }}
|
||||
minReplicas: {{ .Values.hpa.minReplicas }}
|
||||
maxReplicas: {{ .Values.hpa.maxReplicas }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
16
infra/k8s/helm/api-gateway/templates/service.yaml
Normal file
16
infra/k8s/helm/api-gateway/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "api-gateway.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
86
infra/k8s/helm/api-gateway/values.yaml
Normal file
86
infra/k8s/helm/api-gateway/values.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
# api-gateway 服务默认值
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/api-gateway
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 8080
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
MYSQL_HOST: mysql.edu-system.svc.cluster.local
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/content/Chart.yaml
Normal file
11
infra/k8s/helm/content/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: content
|
||||
description: content 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- content
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/content/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/content/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "content.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "content.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "content.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "content.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: content
|
||||
{{- end -}}
|
||||
|
||||
{{- define "content.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "content.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/content/templates/deployment.yaml
Normal file
77
infra/k8s/helm/content/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "content.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "content.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "content.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "content.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: content
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "content.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "content.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/content/templates/service.yaml
Normal file
16
infra/k8s/helm/content/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "content.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "content.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "content.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/content/values.yaml
Normal file
85
infra/k8s/helm/content/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# content 服务默认值(内容资源 - 业务领域 D4)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/content
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3003
|
||||
targetPort: 3003
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3003
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/core-edu/Chart.yaml
Normal file
11
infra/k8s/helm/core-edu/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: core-edu
|
||||
description: core-edu 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- core-edu
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/core-edu/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/core-edu/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "core-edu.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "core-edu.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "core-edu.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "core-edu.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: teaching
|
||||
{{- end -}}
|
||||
|
||||
{{- define "core-edu.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "core-edu.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/core-edu/templates/deployment.yaml
Normal file
77
infra/k8s/helm/core-edu/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "core-edu.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "core-edu.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "core-edu.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "core-edu.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: teaching
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "core-edu.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "core-edu.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/core-edu/templates/service.yaml
Normal file
16
infra/k8s/helm/core-edu/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "core-edu.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "core-edu.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "core-edu.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/core-edu/values.yaml
Normal file
85
infra/k8s/helm/core-edu/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# core-edu 服务默认值(教学核心 - 业务领域 D2+D3)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/core-edu
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3002
|
||||
targetPort: 3002
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3002
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/data-ana/Chart.yaml
Normal file
11
infra/k8s/helm/data-ana/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: data-ana
|
||||
description: data-ana 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- data-ana
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/data-ana/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/data-ana/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "data-ana.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "data-ana.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "data-ana.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "data-ana.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: analytics
|
||||
{{- end -}}
|
||||
|
||||
{{- define "data-ana.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "data-ana.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/data-ana/templates/deployment.yaml
Normal file
77
infra/k8s/helm/data-ana/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "data-ana.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "data-ana.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "data-ana.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "data-ana.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: analytics
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "data-ana.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "data-ana.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/data-ana/templates/service.yaml
Normal file
16
infra/k8s/helm/data-ana/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "data-ana.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "data-ana.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "data-ana.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/data-ana/values.yaml
Normal file
85
infra/k8s/helm/data-ana/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# data-ana 服务默认值(数据分析 - 业务领域 D6)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/data-ana
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3005
|
||||
targetPort: 3005
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3005
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
12
infra/k8s/helm/edu-platform/Chart.yaml
Normal file
12
infra/k8s/helm/edu-platform/Chart.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v2
|
||||
name: edu-platform
|
||||
description: Edu 平台级 Helm Chart(命名空间 / 全局 ConfigMap / Secret / Ingress / HPA 模板)
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- platform
|
||||
- infrastructure
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
42
infra/k8s/helm/edu-platform/templates/_helpers.tpl
Normal file
42
infra/k8s/helm/edu-platform/templates/_helpers.tpl
Normal file
@@ -0,0 +1,42 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "edu-platform.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Fully qualified app name.
|
||||
*/}}
|
||||
{{- define "edu-platform.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels.
|
||||
*/}}
|
||||
{{- define "edu-platform.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "edu-platform.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- with .Values.global.labels }}
|
||||
{{ toYaml . }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels.
|
||||
*/}}
|
||||
{{- define "edu-platform.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "edu-platform.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
13
infra/k8s/helm/edu-platform/templates/configmap.yaml
Normal file
13
infra/k8s/helm/edu-platform/templates/configmap.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.configMap.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Values.configMap.name }}
|
||||
namespace: edu-system
|
||||
labels:
|
||||
{{- include "edu-platform.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
{{ $k }}: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
34
infra/k8s/helm/edu-platform/templates/hpa.yaml
Normal file
34
infra/k8s/helm/edu-platform/templates/hpa.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
{{/*
|
||||
全局 HPA 模板示例(参考用)。
|
||||
实际 HPA 应在各服务自身的 chart 中定义,以便针对服务特性调参。
|
||||
本模板在 edu-platform 中默认不渲染(hpa.targetService 为空时跳过)。
|
||||
*/}}
|
||||
{{- if and .Values.hpa.enabled .Values.hpa.targetService }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ .Values.hpa.targetService }}-hpa
|
||||
namespace: edu-services
|
||||
labels:
|
||||
{{- include "edu-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ .Values.hpa.targetService }}
|
||||
minReplicas: {{ .Values.hpa.minReplicas }}
|
||||
maxReplicas: {{ .Values.hpa.maxReplicas }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user