Compare commits
26 Commits
v0.5.0-p5
...
a1d7fcfd71
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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 #
|
||||
48
.github/workflows/ci-go.yml
vendored
48
.github/workflows/ci-go.yml
vendored
@@ -1,5 +1,11 @@
|
||||
name: CI Go
|
||||
|
||||
# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml
|
||||
# Go 服务分两阶段:
|
||||
# - quality: golang:1.22-alpine 容器(不需要 docker)
|
||||
# - docker-build: dockerreg.eazygame.cn/node-with-docker:22 + setup-go(需要 docker 命令)
|
||||
# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -8,6 +14,7 @@ on:
|
||||
- 'services/push-gateway/**'
|
||||
- 'packages/shared-go/**'
|
||||
- 'go.work'
|
||||
- '.github/workflows/ci-go.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
@@ -15,24 +22,39 @@ on:
|
||||
- 'services/push-gateway/**'
|
||||
- 'packages/shared-go/**'
|
||||
- 'go.work'
|
||||
- '.github/workflows/ci-go.yml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:1.22-alpine
|
||||
defaults:
|
||||
run:
|
||||
working-directory: services/api-gateway
|
||||
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: Download deps
|
||||
run: go mod download
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Build
|
||||
working-directory: services/api-gateway
|
||||
run: |
|
||||
go mod download
|
||||
go build ./...
|
||||
run: go build ./...
|
||||
|
||||
- name: Test
|
||||
working-directory: services/api-gateway
|
||||
run: go test ./... -v -coverprofile=coverage.out
|
||||
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
needs: quality
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build api-gateway
|
||||
run: |
|
||||
docker build \
|
||||
-t edu/api-gateway:ci \
|
||||
-f services/api-gateway/Dockerfile \
|
||||
services/api-gateway
|
||||
|
||||
2
.github/workflows/ci-proto.yml
vendored
2
.github/workflows/ci-proto.yml
vendored
@@ -22,4 +22,4 @@ jobs:
|
||||
- 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
|
||||
run: buf breaking --against .git#branch=main,subdir=packages/shared-proto
|
||||
|
||||
73
.github/workflows/ci-ts.yml
vendored
73
.github/workflows/ci-ts.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: CI TypeScript
|
||||
|
||||
# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml
|
||||
# 关键配置:container: dockerreg.eazygame.cn/node-with-docker:22(带 docker 的 node 22)
|
||||
# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -11,6 +15,7 @@ on:
|
||||
- 'package.json'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'tsconfig.base.json'
|
||||
- '.github/workflows/ci-ts.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
@@ -18,26 +23,76 @@ on:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'tsconfig.base.json'
|
||||
- '.github/workflows/ci-ts.yml'
|
||||
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: '1'
|
||||
NEXT_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
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
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm -r run lint
|
||||
continue-on-error: true # P1: ESLint 9 flat config 迁移未完成
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm -r run typecheck
|
||||
|
||||
- name: Test
|
||||
run: pnpm -r run test
|
||||
continue-on-error: true # P1: 部分服务无 test 脚本
|
||||
|
||||
- name: Build
|
||||
run: pnpm -r run build
|
||||
|
||||
arch-scan:
|
||||
runs-on: ubuntu-latest
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
needs: quality
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@9
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: arch:scan
|
||||
run: pnpm run arch:scan
|
||||
- name: Verify arch.db committed
|
||||
run: |
|
||||
if git diff --quiet -- scripts/arch-scan/arch.db; then
|
||||
echo "arch.db up to date"
|
||||
else
|
||||
echo "::warning::arch.db 未同步,请运行 pnpm run arch:scan 并提交"
|
||||
fi
|
||||
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
needs: quality
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- { name: classes, dockerfile: services/classes/Dockerfile, context: . }
|
||||
- { name: teacher-portal, dockerfile: apps/teacher-portal/Dockerfile, context: . }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build ${{ matrix.service.name }}
|
||||
run: |
|
||||
docker build \
|
||||
-t edu/${{ matrix.service.name }}:ci \
|
||||
-f ${{ matrix.service.dockerfile }} \
|
||||
${{ matrix.service.context }}
|
||||
|
||||
145
.github/workflows/deploy.yml
vendored
Normal file
145
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
name: Deploy
|
||||
|
||||
# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml
|
||||
# 关键配置:container: dockerreg.eazygame.cn/node-with-docker:22(job 内执行 docker compose)
|
||||
# 部署到服务器
|
||||
# 触发条件:main 分支 docker.yml 完成后 自动,或手动 workflow_dispatch
|
||||
# 部署方式:Runner 直接执行 docker compose(Runner 跑在服务器上)
|
||||
#
|
||||
# 前置条件(一次性配置):
|
||||
# 1. 服务器上已安装 Gitea Actions Runner,标签为 'ubuntu-latest'
|
||||
# 2. 服务器上已存在 /opt/edu/docker-compose.deploy.yml(由 SRE AI 首次部署)
|
||||
# 3. 服务器上已存在 /opt/edu/.env(含生产 JWT_SECRET 等)
|
||||
# 4. 服务器 Docker 已登录 Gitea Container Registry:
|
||||
# docker login git.eazygame.cn -u <user> -p <token>
|
||||
# 5. MySQL + Redis 已在服务器 Docker 中运行(3306/6379)
|
||||
# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: '镜像 tag(默认 latest)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
rollback:
|
||||
description: '回滚到上一版本(跳过 pull)'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
workflow_run:
|
||||
workflows: ['Docker Build & Push']
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DEPLOY_DIR: /opt/edu
|
||||
REGISTRY: git.eazygame.cn
|
||||
IMAGE_TAG: ${{ github.event.inputs.image_tag || 'latest' }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# 仅在 docker.yml 成功后触发,或手动触发
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
environment:
|
||||
name: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup deploy dir
|
||||
run: |
|
||||
sudo mkdir -p ${{ env.DEPLOY_DIR }}
|
||||
sudo chown -R $(id -u):$(id -g) ${{ env.DEPLOY_DIR }}
|
||||
|
||||
- name: Sync compose file
|
||||
run: |
|
||||
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: Login to Gitea Container Registry
|
||||
run: |
|
||||
# 使用预存的 ~/.docker/config.json(首次部署时人工 docker login)
|
||||
# 或使用 secrets 注入
|
||||
if [ -n "${{ secrets.GITEA_REGISTRY_TOKEN }}" ]; then
|
||||
echo "${{ secrets.GITEA_REGISTRY_TOKEN }}" | \
|
||||
docker login ${{ env.REGISTRY }} -u ${{ secrets.GITEA_REGISTRY_USER }} --password-stdin
|
||||
fi
|
||||
|
||||
- name: Pull images
|
||||
if: ${{ github.event.inputs.rollback != 'true' }}
|
||||
run: |
|
||||
cd ${{ env.DEPLOY_DIR }}
|
||||
export IMAGE_TAG=${{ env.IMAGE_TAG }}
|
||||
docker compose pull
|
||||
continue-on-error: true # 首次部署可能无旧镜像
|
||||
|
||||
- name: Deploy services
|
||||
run: |
|
||||
cd ${{ env.DEPLOY_DIR }}
|
||||
export IMAGE_TAG=${{ env.IMAGE_TAG }}
|
||||
docker compose up -d --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 ! curl -sf http://localhost:8080/healthz > /dev/null; then
|
||||
echo " api-gateway 未就绪"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
echo "[尝试 $i] 检查 classes..."
|
||||
if ! curl -sf http://localhost:3001/healthz > /dev/null; then
|
||||
echo " classes 未就绪"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
echo "[尝试 $i] 检查 teacher-portal..."
|
||||
if ! curl -sf http://localhost:3000/ > /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
|
||||
exit 1
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### 部署结果" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 镜像 tag:\`${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 部署目录:\`${{ env.DEPLOY_DIR }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 回滚模式:${{ github.event.inputs.rollback || 'false' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
cd ${{ env.DEPLOY_DIR }} && docker compose ps >> $GITHUB_STEP_SUMMARY 2>&1 || true
|
||||
92
.github/workflows/docker.yml
vendored
Normal file
92
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Docker Build & Push
|
||||
|
||||
# 参考实现:E:\Desktop\CICD\.gitea\workflows\ci.yml
|
||||
# 关键配置:container: dockerreg.eazygame.cn/node-with-docker:22(job 内执行 docker build/push)
|
||||
# 触发条件:main 分支推送 或 打 v* tag
|
||||
# 镜像推送到 Gitea Container Registry: git.eazygame.cn/xiner/edu/<service>:<tag>
|
||||
# 详见 docs/standards/cicd-runbook.md §10、project_rules §15.7
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'services/api-gateway/**'
|
||||
- 'services/classes/**'
|
||||
- 'apps/teacher-portal/**'
|
||||
- 'packages/shared-proto/**'
|
||||
- '.github/workflows/docker.yml'
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
REGISTRY: git.eazygame.cn
|
||||
OWNER: xiner
|
||||
REPO_LOWER: edu
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
runs-on: ubuntu-latest
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- { name: api-gateway, dockerfile: services/api-gateway/Dockerfile, context: ./services/api-gateway }
|
||||
- { name: classes, dockerfile: services/classes/Dockerfile, context: . }
|
||||
- { name: teacher-portal, dockerfile: apps/teacher-portal/Dockerfile, context: . }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# 登录 Gitea Container Registry
|
||||
# GITHUB_TOKEN 由 Gitea Actions 自动注入,对应仓库写权限
|
||||
- name: Login to Gitea Container Registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
|
||||
|
||||
# 生成镜像 tag
|
||||
# main 分支:latest + git-sha
|
||||
# v* tag:v<version> + latest
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
run: |
|
||||
TAGS="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO_LOWER }}/${{ matrix.service.name }}:latest"
|
||||
SHA_TAG="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO_LOWER }}/${{ matrix.service.name }}:sha-${GITHUB_SHA::7}"
|
||||
TAGS="$TAGS,$SHA_TAG"
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
TAGS="$TAGS,${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO_LOWER }}/${{ matrix.service.name }}:v${VERSION}"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "Resolved tags: $TAGS"
|
||||
|
||||
- name: Build and push ${{ matrix.service.name }}
|
||||
run: |
|
||||
IFS=',' read -ra TAG_ARRAY <<< "${{ steps.meta.outputs.tags }}"
|
||||
TAG_ARGS=""
|
||||
for tag in "${TAG_ARRAY[@]}"; do
|
||||
TAG_ARGS="$TAG_ARGS -t $tag"
|
||||
done
|
||||
|
||||
docker build \
|
||||
$TAG_ARGS \
|
||||
-f ${{ matrix.service.dockerfile }} \
|
||||
${{ matrix.service.context }}
|
||||
|
||||
for tag in "${TAG_ARRAY[@]}"; do
|
||||
echo "Pushing $tag..."
|
||||
docker push $tag
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### Docker 镜像已推送" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "服务:\`${{ matrix.service.name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "镜像标签:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -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/`,本节为强制约束。
|
||||
|
||||
### 15.1 流水线阶段(强制)
|
||||
|
||||
所有 PR 与 main 分支推送必须通过以下阶段:
|
||||
|
||||
| 阶段 | 并行 | 内容 | 失败策略 |
|
||||
| ---------------- | ------------ | ------------------------------------------ | -------- |
|
||||
| **lint** | ✅(按语言) | TS ESLint / Go vet / Python ruff | 失败阻断 |
|
||||
| **typecheck** | ✅ | TS `tsc --noEmit` | 失败阻断 |
|
||||
| **test** | ✅(按服务) | Go test / Vitest / pytest | 失败阻断 |
|
||||
| **build** | ✅(按服务) | Go build / nest build / next build | 失败阻断 |
|
||||
| **arch:scan** | ✅ | `pnpm run arch:scan` + 校验 arch.db 一致性 | 失败阻断 |
|
||||
| **docker-build** | ✅(按服务) | 构建镜像(不推送) | 失败阻断 |
|
||||
|
||||
> main 分支额外阶段:`docker-push`(推送镜像)+ `deploy`(部署到服务器)
|
||||
|
||||
### 15.2 触发条件
|
||||
|
||||
| 事件 | 触发阶段 | 触发条件 |
|
||||
| ------------ | ---------------------------------------------------------- | ---------- |
|
||||
| PR 创建/更新 | lint + typecheck + test + build + arch:scan + docker-build | 所有路径 |
|
||||
| push 到 main | 上述全部 + docker-push + deploy | 合并后自动 |
|
||||
| tag `v*` | docker-push(tag 镜像)+ deploy(生产) | 手动打 tag |
|
||||
|
||||
### 15.3 镜像规范
|
||||
|
||||
- **镜像名**:`edu/<service>:<tag>`
|
||||
- **tag 策略**:
|
||||
- `latest`:main 分支最新
|
||||
- `<git-sha>`:每次构建的 commit SHA(可追溯)
|
||||
- `v<version>`:正式发布 tag
|
||||
- **镜像扫描**:docker-push 后自动运行 Trivy 扫描,CRITICAL 漏洞阻断部署
|
||||
|
||||
### 15.4 部署策略
|
||||
|
||||
- **目标环境**:服务器 Docker Compose(P1-P2 阶段),K8s(P3+ 阶段)
|
||||
- **部署方式**:SSH 到服务器,`docker compose pull && docker compose up -d`
|
||||
- **健康检查**:部署后轮询 `/healthz` 端点,连续 3 次失败回滚
|
||||
- **回滚**:`docker compose rollback`(上一版本镜像 tag)
|
||||
|
||||
### 15.5 Secrets 管理
|
||||
|
||||
- **CI Secrets**:存于 GitHub Actions secrets / Gitea Actions secrets
|
||||
- **部署服务器**:`.env` 文件由 SRE AI 管理,不进版本库
|
||||
- **JWT_SECRET**:生产环境强随机值,不与开发环境共享
|
||||
|
||||
### 15.6 必需的 CI 文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
| ------------------------------ | ------------------------------------------- |
|
||||
| `.github/workflows/ci.yml` | 主流水线(lint + typecheck + test + build) |
|
||||
| `.github/workflows/docker.yml` | Docker 镜像构建与推送(main + tag 触发) |
|
||||
| `.github/workflows/deploy.yml` | 部署到服务器(main 触发,需环境审批) |
|
||||
|
||||
### 15.7 参考实现(强制参照)
|
||||
|
||||
**所有 CI/CD 配置必须参照 `E:\Desktop\CICD\` 项目(同机 Gitea 仓库),不另行自创。**
|
||||
|
||||
- **参考项目位置**:`E:\Desktop\CICD\.gitea\workflows\ci.yml`(同机单仓库 CI 实现)
|
||||
- **关键配置点(必须沿用)**:
|
||||
- **Runner 标签**:`ubuntu-latest`(actrunner 已配置;参考项目用 `CDCD`,Edu 改为 `ubuntu-latest`)
|
||||
- **容器化运行**:TS 类 job 必须使用 `container: dockerreg.eazygame.cn/node-with-docker:22`(带 docker 的 node 22 镜像,用于在 job 内执行 `docker build`/`docker run`)
|
||||
- **Go 类 job**:使用 `container: golang:1.22-alpine`(仅 quality 阶段),`docker-build` 阶段切换到 `node-with-docker:22` 镜像 + `setup-go`
|
||||
- **部署方式**:保留 `docker compose`(不照搬参考项目的 `docker run` 单容器方式,因 Edu 是微服务多容器)
|
||||
- **代理配置**:runner 已配置全局代理,CI 内不再设置 npm/docker 代理(参考项目中的 `172.17.0.1:7890` 代理段不要照搬)
|
||||
- **Next.js standalone 构建**:teacher-portal 沿用参考项目的 standalone 模式(`output: 'standalone'` + 复制 public/.next/static)
|
||||
- **网络**:部署用 `edu-shared` 外部网络连接服务器已有 MySQL/Redis(参考项目用 `1panel-network`,Edu 不同)
|
||||
- **改动需同步**:修改任何 workflow 文件前,先比对参考项目同类配置,确保不偏离上述约定
|
||||
|
||||
> 当参考项目与本规范冲突时,以本规范为准(Edu 是微服务架构,参考项目是单体 Next.js)。
|
||||
|
||||
---
|
||||
|
||||
**本规则文件是项目的强制约束,所有 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"]
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
interface ClassItem {
|
||||
id: string;
|
||||
@@ -20,24 +20,28 @@ interface ApiResponse<T> {
|
||||
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 [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 res = await fetch("/api/v1/classes", {
|
||||
headers: { Authorization: "Bearer dev-token" },
|
||||
});
|
||||
const json: ApiResponse<ClassItem[]> = await res.json();
|
||||
if (json.success && json.data) {
|
||||
setClasses(json.data);
|
||||
} else {
|
||||
setError(json.error?.message || 'Failed to load');
|
||||
setError(json.error?.message || "Failed to load");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Network error');
|
||||
setError(e instanceof Error ? e.message : "Network error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -51,49 +55,61 @@ export default function HomePage() {
|
||||
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' },
|
||||
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');
|
||||
setError(json.error?.message || "Create failed");
|
||||
return;
|
||||
}
|
||||
setName('');
|
||||
setDescription('');
|
||||
setName("");
|
||||
setDescription("");
|
||||
await fetchClasses();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Network error');
|
||||
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' },
|
||||
method: "DELETE",
|
||||
headers: { Authorization: "Bearer dev-token" },
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
setError(json.error?.message || 'Delete failed');
|
||||
setError(json.error?.message || "Delete failed");
|
||||
return;
|
||||
}
|
||||
await fetchClasses();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Network error');
|
||||
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="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
|
||||
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)' }}>
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
P1 黄金模板验证 - classes 域 CRUD
|
||||
</p>
|
||||
</div>
|
||||
@@ -102,11 +118,19 @@ export default function HomePage() {
|
||||
<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>
|
||||
<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
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
班级名称
|
||||
</label>
|
||||
<input
|
||||
@@ -114,13 +138,19 @@ export default function HomePage() {
|
||||
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' }}
|
||||
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)' }}>
|
||||
<label
|
||||
className="block text-xs uppercase tracking-wide mb-1"
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
年级 ID
|
||||
</label>
|
||||
<input
|
||||
@@ -128,25 +158,34 @@ export default function HomePage() {
|
||||
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' }}
|
||||
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
|
||||
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' }}
|
||||
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' }}
|
||||
style={{ background: "var(--color-accent)", borderRadius: "6px" }}
|
||||
>
|
||||
创建班级
|
||||
</button>
|
||||
@@ -156,16 +195,19 @@ export default function HomePage() {
|
||||
{/* 中间:班级列表(纸面)*/}
|
||||
<section className="col-span-8">
|
||||
<div className="flex items-baseline justify-between mb-4">
|
||||
<h2 className="text-xl" style={{ fontFamily: 'var(--font-serif)' }}>
|
||||
<h2 className="text-xl" style={{ fontFamily: "var(--font-serif)" }}>
|
||||
班级列表
|
||||
<span className="ml-2 text-sm font-sans" style={{ color: 'var(--color-ink-muted)' }}>
|
||||
<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)' }}
|
||||
style={{ color: "var(--color-accent)" }}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
@@ -173,37 +215,65 @@ export default function HomePage() {
|
||||
<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
|
||||
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>
|
||||
<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
|
||||
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)' }}>
|
||||
<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)' }}>
|
||||
<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>
|
||||
<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)' }}>
|
||||
<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)' }}
|
||||
style={{ color: "var(--color-ink-muted)" }}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
|
||||
@@ -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(ES)]
|
||||
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(ClickHouse)]
|
||||
AI_M[ai 备课/出题/分析 / search]
|
||||
end
|
||||
|
||||
D1 --> D2
|
||||
D1 --> D3
|
||||
D1 --> D4
|
||||
D1 --> D5
|
||||
D2 --> D3
|
||||
D3 --> D4
|
||||
D3 --> D5
|
||||
D4 --> D6
|
||||
D3 --> D6
|
||||
```
|
||||
|
||||
**双图并存说明**:
|
||||
|
||||
- **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. 数据访问与缓存
|
||||
@@ -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 同步 |
|
||||
|
||||
---
|
||||
|
||||
@@ -520,27 +615,27 @@ graph LR
|
||||
|
||||
### 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)
|
||||
|
||||
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
506
docs/standards/cicd-runbook.md
Normal file
506
docs/standards/cicd-runbook.md
Normal file
@@ -0,0 +1,506 @@
|
||||
# CI/CD 使用手册(CI/CD Runbook)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-08
|
||||
> 适用范围:Edu 微服务项目(Gitea Actions + Gitea Container Registry + Runner 直接部署)
|
||||
> 关联文档:[project_rules §15](../../.trae/rules/project_rules.md)、[本地启动手册](./local-dev-runbook.md)、[多 AI 协作指南](./multi-ai-collaboration.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构总览
|
||||
|
||||
```
|
||||
开发 AI 推送 PR 协调 AI 合并到 main
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ ci-*.yml │ │ docker.yml │
|
||||
│ lint+test+ │ │ build+push │
|
||||
│ build+scan │ │ 镜像到 Gitea│
|
||||
└─────────────┘ │ Registry │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ deploy.yml │
|
||||
│ Runner 直接 │
|
||||
│ docker │
|
||||
│ compose up │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 1.1 组件清单
|
||||
|
||||
| 组件 | 说明 |
|
||||
| ---------------------------- | ---------------------------------------------------------- |
|
||||
| **Gitea** | `git.eazygame.cn`,代码托管 + Actions + Container Registry |
|
||||
| **Gitea Actions** | CI 运行器,兼容 GitHub Actions 语法 |
|
||||
| **Gitea Container Registry** | 镜像仓库,地址 `git.eazygame.cn/xiner/edu/<service>:<tag>` |
|
||||
| **Deploy Runner** | 跑在服务器上的 actrunner,标签 `ubuntu-latest` |
|
||||
| **服务器 MySQL** | 已有容器,端口 3306,容器名 `edu-mysql`(假设) |
|
||||
| **服务器 Redis** | 已有容器,端口 6379,容器名 `edu-redis`(假设) |
|
||||
|
||||
### 1.2 流水线文件
|
||||
|
||||
| 文件 | 触发 | 作用 |
|
||||
| -------------------------------- | ------------------------ | ---------------------------------------------------------- |
|
||||
| `.github/workflows/ci-ts.yml` | TS 路径变更 | lint + typecheck + test + build + arch:scan + docker-build |
|
||||
| `.github/workflows/ci-go.yml` | Go 路径变更 | vet + build + test + docker-build |
|
||||
| `.github/workflows/ci-py.yml` | Python 路径变更 | ruff + pytest(P4 阶段) |
|
||||
| `.github/workflows/ci-proto.yml` | proto 变更 | buf lint + buf breaking |
|
||||
| `.github/workflows/docker.yml` | push main / tag `v*` | 构建并推送镜像到 Gitea Registry |
|
||||
| `.github/workflows/deploy.yml` | docker.yml 完成后 / 手动 | Runner 执行 docker compose up |
|
||||
|
||||
---
|
||||
|
||||
## 2. 一次性配置(SRE AI 或人类执行)
|
||||
|
||||
### 2.1 启用 Gitea Actions
|
||||
|
||||
在 Gitea 仓库 `xiner/Edu` 设置:
|
||||
|
||||
1. **仓库设置 → Actions → 启用**
|
||||
2. 安装 actrunner(若未安装):
|
||||
```bash
|
||||
# 在服务器上
|
||||
wget https://gitea.com/gitea/act_runner/raw/branch/main/act_runner
|
||||
chmod +x act_runner
|
||||
./act_runner register --instance https://git.eazygame.cn --token <TOKEN>
|
||||
```
|
||||
3. 配置 runner 标签为 `ubuntu-latest`(CI 与部署共用)
|
||||
|
||||
### 2.2 启用 Gitea Container Registry
|
||||
|
||||
Gitea 1.20+ 自带 Container Registry,无需额外启用。确认:
|
||||
|
||||
```bash
|
||||
# 测试登录
|
||||
docker login git.eazygame.cn -u <你的用户名>
|
||||
# 输入密码或 token
|
||||
```
|
||||
|
||||
### 2.3 准备服务器网络
|
||||
|
||||
确保 MySQL/Redis 容器与应用容器在同一 Docker 网络:
|
||||
|
||||
```bash
|
||||
# 创建共享网络(若不存在)
|
||||
docker network create edu-shared
|
||||
|
||||
# 将已有的 MySQL/Redis 加入网络(容器名按实际替换)
|
||||
docker network connect edu-shared edu-mysql # 或实际容器名
|
||||
docker network connect edu-shared edu-redis # 或实际容器名
|
||||
```
|
||||
|
||||
> 查看实际容器名:`docker ps --format "{{.Names}}" | grep -E "mysql|redis"`
|
||||
|
||||
### 2.4 初始化部署目录
|
||||
|
||||
```bash
|
||||
# 1. 创建部署目录
|
||||
sudo mkdir -p /opt/edu
|
||||
sudo chown -R $USER:$USER /opt/edu
|
||||
|
||||
# 2. 拷贝 compose 文件
|
||||
cd /path/to/Edu
|
||||
cp infra/docker-compose.deploy.yml /opt/edu/docker-compose.yml
|
||||
|
||||
# 3. 创建生产 .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
|
||||
|
||||
# 4. 登录 Gitea Container Registry(Runner 用户)
|
||||
docker login git.eazygame.cn -u <user> -p <token>
|
||||
# token 在 Gitea → 设置 → 应用 → 生成新 token
|
||||
```
|
||||
|
||||
### 2.5 配置 Gitea Secrets
|
||||
|
||||
在 Gitea 仓库 → 设置 → Actions → Secrets,添加:
|
||||
|
||||
| Secret 名 | 用途 | 必填 |
|
||||
| ---------------------- | -------------------------------- | ---- |
|
||||
| `GITHUB_TOKEN` | Gitea 自动注入,推送镜像用 | 自动 |
|
||||
| `GITEA_REGISTRY_USER` | deploy.yml 登录 registry(可选) | 否 |
|
||||
| `GITEA_REGISTRY_TOKEN` | deploy.yml 登录 registry(可选) | 否 |
|
||||
|
||||
> `GITHUB_TOKEN` 由 Gitea Actions 自动注入,无需手动配置。`docker.yml` 用它推送镜像。
|
||||
> `deploy.yml` 中 Runner 已在服务器上,若已 `docker login` 过则无需 secrets。
|
||||
|
||||
---
|
||||
|
||||
## 3. 日常使用
|
||||
|
||||
### 3.1 开发 AI 提 PR
|
||||
|
||||
开发 AI 推送特性分支后创建 PR,自动触发 CI:
|
||||
|
||||
```
|
||||
PR 创建 → ci-ts.yml / ci-go.yml 运行
|
||||
├─ quality(lint + typecheck + test + build)
|
||||
├─ arch-scan(架构扫描)
|
||||
└─ docker-build(构建不推送)
|
||||
|
||||
CI 全绿 → 协调 AI 审核合并
|
||||
```
|
||||
|
||||
### 3.2 协调 AI 合并 PR
|
||||
|
||||
PR 合并到 main 后,自动触发:
|
||||
|
||||
```
|
||||
push main →
|
||||
├─ ci-*.yml(确保 main 稳定)
|
||||
└─ docker.yml → 构建并推送镜像到 git.eazygame.cn/xiner/edu/<service>:latest
|
||||
│
|
||||
▼
|
||||
deploy.yml → Runner 执行 docker compose pull && up -d
|
||||
│
|
||||
▼
|
||||
健康检查(轮询 /healthz)
|
||||
```
|
||||
|
||||
### 3.3 手动部署
|
||||
|
||||
在 Gitea → Actions → Deploy → Run workflow:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| ----------- | --------------------------------------- |
|
||||
| `image_tag` | 部署指定 tag(默认 `latest`) |
|
||||
| `rollback` | 勾选则跳过 pull,用本地已有镜像重新启动 |
|
||||
|
||||
### 3.4 发布正式版本(tag)
|
||||
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "P1 阶段首次发布"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
触发 `docker.yml` 推送 `v1.0.0` tag 镜像,再手动触发 deploy.yml 部署 `v1.0.0`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 镜像管理
|
||||
|
||||
### 4.1 镜像地址
|
||||
|
||||
```
|
||||
git.eazygame.cn/xiner/edu/<service>:<tag>
|
||||
```
|
||||
|
||||
| service | 说明 |
|
||||
| ---------------- | --------------- |
|
||||
| `api-gateway` | Go 网关 |
|
||||
| `classes` | NestJS 班级服务 |
|
||||
| `teacher-portal` | Next.js 前端 |
|
||||
|
||||
### 4.2 Tag 策略
|
||||
|
||||
| tag | 来源 | 用途 |
|
||||
| ------------------ | ----------------- | ---------- |
|
||||
| `latest` | main 分支最新构建 | 默认部署 |
|
||||
| `main-<sha>` | main 分支每次构建 | 可追溯 |
|
||||
| `v<version>` | 打 `v*` tag 触发 | 正式版本 |
|
||||
| `v<major>.<minor>` | 打 `v*` tag 触发 | 大版本追踪 |
|
||||
|
||||
### 4.3 查看与清理镜像
|
||||
|
||||
在 Gitea → 用户设置 → Packages,可查看与管理所有镜像。
|
||||
|
||||
清理旧 tag(手动):
|
||||
|
||||
```bash
|
||||
# 列出所有 tag
|
||||
docker images git.eazygame.cn/xiner/edu/classes --format "{{.Tag}}"
|
||||
|
||||
# 删除本地旧镜像
|
||||
docker rmi git.eazygame.cn/xiner/edu/classes:main-abc1234
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 部署验证
|
||||
|
||||
### 5.1 CI 自动验证
|
||||
|
||||
`deploy.yml` 部署后会自动轮询健康检查(最多 10 次,每次间隔 6 秒):
|
||||
|
||||
- `http://localhost:8080/healthz` — api-gateway
|
||||
- `http://localhost:3001/healthz` — classes
|
||||
- `http://localhost:3000/` — teacher-portal
|
||||
|
||||
失败时输出容器日志,方便排查。
|
||||
|
||||
### 5.2 手动验证
|
||||
|
||||
```bash
|
||||
# SSH 到服务器后
|
||||
cd /opt/edu
|
||||
|
||||
# 容器状态
|
||||
docker compose ps
|
||||
|
||||
# 健康检查
|
||||
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
|
||||
|
||||
# 查看日志
|
||||
docker compose logs -f api-gateway
|
||||
docker compose logs -f classes
|
||||
docker compose logs -f teacher-portal
|
||||
```
|
||||
|
||||
### 5.3 外部访问
|
||||
|
||||
若服务器有公网 IP(如 `1.2.3.4`):
|
||||
|
||||
- 前端:`http://1.2.3.4:3000`
|
||||
- API 网关:`http://1.2.3.4:8080`
|
||||
- 健康检查:`http://1.2.3.4:8080/healthz`
|
||||
|
||||
---
|
||||
|
||||
## 6. 回滚
|
||||
|
||||
### 6.1 CI 回滚(推荐)
|
||||
|
||||
在 Gitea → Actions → Deploy → Run workflow:
|
||||
|
||||
- `image_tag`:填入上一个稳定版本的 tag(如 `main-abc1234`)
|
||||
- `rollback`:勾选(跳过 pull,用本地已有镜像)
|
||||
|
||||
### 6.2 手动回滚
|
||||
|
||||
```bash
|
||||
cd /opt/edu
|
||||
|
||||
# 查看本地镜像
|
||||
docker images git.eazygame.cn/xiner/edu/classes --format "{{.Tag}}"
|
||||
|
||||
# 回滚到指定版本
|
||||
IMAGE_TAG=main-abc1234 docker compose up -d
|
||||
```
|
||||
|
||||
### 6.3 紧急回滚(Git revert)
|
||||
|
||||
若代码有问题:
|
||||
|
||||
```bash
|
||||
git revert <bad-commit>
|
||||
git push origin main
|
||||
# CI 自动重新构建部署
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见问题
|
||||
|
||||
### 7.1 CI: pnpm install 失败
|
||||
|
||||
**症状**:`pnpm install --frozen-lockfile` 报错。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. `pnpm-lock.yaml` 未提交:`git add pnpm-lock.yaml && git commit -m "chore(deps): 更新 lockfile"`
|
||||
2. Node 版本不匹配:确认 CI 用 Node 20,本地一致
|
||||
|
||||
### 7.2 CI: docker-build 失败
|
||||
|
||||
**症状**:`docker/build-push-action` 报错。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. Dockerfile 语法错误:本地 `docker build` 验证
|
||||
2. 路径错误:确认 `context` 与 `file` 参数
|
||||
|
||||
### 7.3 CD: 镜像拉取失败
|
||||
|
||||
**症状**:`docker compose pull` 报 `unauthorized`。
|
||||
|
||||
**修复**:
|
||||
|
||||
```bash
|
||||
# 服务器上重新登录
|
||||
docker login git.eazygame.cn -u <user> -p <token>
|
||||
|
||||
# 或配置 Gitea secrets GITEA_REGISTRY_USER / GITEA_REGISTRY_TOKEN
|
||||
```
|
||||
|
||||
### 7.4 CD: 健康检查失败
|
||||
|
||||
**症状**:`deploy.yml` 健康检查 10 次后失败。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. 查看日志:`cd /opt/edu && docker compose logs api-gateway`
|
||||
2. 常见原因:
|
||||
- `DATABASE_URL` 连不上 MySQL(检查容器名与网络)
|
||||
- `JWT_SECRET` 未配置
|
||||
- 端口被占用:`docker ps` 检查冲突
|
||||
|
||||
### 7.5 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
|
||||
```
|
||||
|
||||
### 7.6 Gitea Actions 未触发
|
||||
|
||||
**症状**:push 后 Actions 不运行。
|
||||
|
||||
**排查**:
|
||||
|
||||
1. 仓库设置 → Actions → 确认已启用
|
||||
2. Runner 在线:Gitea → 设置 → Actions → Runners
|
||||
3. workflow 文件在 `.github/workflows/` 目录(非 `.gitea/workflows/`)
|
||||
4. `on:` 触发条件匹配(paths 过滤)
|
||||
|
||||
### 7.7 workflow_run 不触发
|
||||
|
||||
**症状**:`docker.yml` 完成后 `deploy.yml` 不自动运行。
|
||||
|
||||
**原因**:Gitea Actions 对 `workflow_run` 触发支持可能不完整。
|
||||
|
||||
**解决**:
|
||||
|
||||
- 改用手动触发:Actions → Deploy → Run workflow
|
||||
- 或在 `docker.yml` 末尾加 job 调用 deploy(需要 `workflow_call`)
|
||||
|
||||
---
|
||||
|
||||
## 8. 排查命令速查
|
||||
|
||||
```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
|
||||
|
||||
# 进入容器
|
||||
docker exec -it edu-api-gateway sh
|
||||
docker exec -it edu-classes sh
|
||||
|
||||
# 查看网络
|
||||
docker network inspect edu-shared
|
||||
|
||||
# 查看镜像列表
|
||||
docker images git.eazygame.cn/xiner/edu
|
||||
|
||||
# === 在 Gitea Web UI ===
|
||||
# 仓库 → Actions → 查看流水线运行记录
|
||||
# 用户设置 → Packages → 管理镜像
|
||||
# 仓库 → 设置 → Secrets → 管理 CI 密钥
|
||||
# 仓库 → 设置 → Actions → Runners → 查看 Runner 状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全注意事项
|
||||
|
||||
1. **`.env` 文件不入库**:`.gitignore` 已忽略,仅存在于服务器 `/opt/edu/.env`
|
||||
2. **JWT_SECRET 强随机**:生产必须用 `openssl rand -hex 32` 生成
|
||||
3. **DEV_MODE=false**:docker-compose.deploy.yml 强制设为 `false`
|
||||
4. **Gitea Token 权限最小化**:仅给 `package:write` 和 `contents:read`
|
||||
5. **Runner 隔离**:deploy runner 仅跑在目标服务器,不暴露公网 SSH
|
||||
6. **镜像扫描**(P6):`docker.yml` 后续加 Trivy 扫描步骤
|
||||
|
||||
---
|
||||
|
||||
## 10. 参考实现(强制参照)
|
||||
|
||||
**所有 CI/CD 配置变更必须参照同机的 `E:\Desktop\CICD\` 项目(Gitea 单仓库 CI 实现),不另行自创。**
|
||||
|
||||
### 10.1 参考项目位置
|
||||
|
||||
| 路径 | 内容 |
|
||||
| ------------------------------------------------- | ------------------------------------- |
|
||||
| `E:\Desktop\CICD\.gitea\workflows\ci.yml` | 主 CI 配置(build-deploy + 安全扫描) |
|
||||
| `E:\Desktop\CICD\.gitea\workflows\dr-drill.yml` | 灾备演练 |
|
||||
| `E:\Desktop\CICD\.gitea\workflows\lighthouse.yml` | 性能审计 |
|
||||
| `E:\Desktop\CICD\.gitea\workflows\security.yml` | 安全扫描 |
|
||||
| `E:\Desktop\CICD\.trae\rules\project_rules.md` | 参考项目规则(对照学习) |
|
||||
|
||||
### 10.2 关键配置点(必须沿用)
|
||||
|
||||
参考项目 `ci.yml` 的核心配置:
|
||||
|
||||
| 配置项 | 参考项目值 | Edu 项目取值 |
|
||||
| ------------------- | ------------------------------------------------------ | ------------------------------------------- |
|
||||
| **Runner 标签** | `CDCD` | `ubuntu-latest` |
|
||||
| **TS job 容器** | `dockerreg.eazygame.cn/node-with-docker:22` | 同(带 docker 的 node 22 镜像) |
|
||||
| **Go quality 容器** | (无 Go) | `golang:1.22-alpine` |
|
||||
| **Go docker-build** | (无 Go) | `node-with-docker:22` + `setup-go` |
|
||||
| **部署方式** | `docker build` + `docker run --network 1panel-network` | `docker compose pull && up`(多服务编排) |
|
||||
| **网络** | `1panel-network` | `edu-shared`(连接已有 MySQL/Redis) |
|
||||
| **npm 代理** | docker gateway IP `172.17.0.1:7890` | 不配置(runner 已全局代理) |
|
||||
| **Next.js 构建** | standalone + 复制 `public`/`.next/static` | teacher-portal 沿用 |
|
||||
| **环境变量** | secrets 注入 `DATABASE_URL`/`NEXTAUTH_SECRET` | secrets 注入 `DATABASE_URL`/`JWT_SECRET` 等 |
|
||||
| **定时备份** | `schedule: cron "0 2 * * *"` | 待 P6 阶段实现 |
|
||||
|
||||
### 10.3 关键差异(Edu vs 参考项目)
|
||||
|
||||
| 维度 | 参考项目 | Edu 项目 |
|
||||
| -------- | ------------------------ | ------------------------------------------------------ |
|
||||
| 架构 | 单体 Next.js | 微服务(TS + Go + Python + Proto) |
|
||||
| 包管理器 | npm | pnpm(TS)/ go mod(Go)/ uv(Python) |
|
||||
| 服务数量 | 1 个(nextjs-app) | 3+(api-gateway / classes / teacher-portal) |
|
||||
| 部署方式 | docker run 单容器 | docker compose 多服务编排 |
|
||||
| CI 拆分 | 单 ci.yml 大而全 | 按语言拆 ci-ts/ci-go/ci-py/ci-proto |
|
||||
| 镜像仓库 | 本地 `nextjs-app:latest` | Gitea Registry `git.eazygame.cn/xiner/edu/<svc>:<tag>` |
|
||||
|
||||
### 10.4 修改 workflow 的流程
|
||||
|
||||
1. **先读参考项目同类配置**:`Read E:\Desktop\CICD\.gitea\workflows\ci.yml`
|
||||
2. **比对差异**:参照 §10.3 表格,确认 Edu 项目的特殊取值
|
||||
3. **修改 Edu workflow**:保持关键配置点(container、runner 标签)一致
|
||||
4. **YAML 语法校验**:`python -c "import yaml; yaml.safe_load(open('<file>'))"`
|
||||
5. **提交并推送**:触发 CI 验证
|
||||
|
||||
### 10.5 当参考项目与本规范冲突时
|
||||
|
||||
- **架构差异**(单体 vs 微服务):以 Edu 项目规范为准
|
||||
- **部署方式**(docker run vs compose):以 Edu 项目规范为准
|
||||
- **镜像/网络/标签**:以 Edu 项目规范为准
|
||||
- **CI 容器化、构建流程、缓存策略**:参照参考项目
|
||||
|
||||
> 详见 [project_rules §15.7 参考实现](../../.trae/rules/project_rules.md)
|
||||
|
||||
---
|
||||
|
||||
## 11. 相关文档
|
||||
|
||||
- [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)
|
||||
- [Git 工作流](./git-workflow.md)
|
||||
- [known-issues](../troubleshooting/known-issues.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/` | 微前端共享 |
|
||||
|
||||
@@ -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 并行开发流程
|
||||
735
docs/standards/multi-ai-collaboration.md
Normal file
735
docs/standards/multi-ai-collaboration.md
Normal file
@@ -0,0 +1,735 @@
|
||||
# 多 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. 模块分工矩阵
|
||||
|
||||
### 2.1 模块清单与 AI 分配
|
||||
|
||||
每个 AI 负责一个"限界上下文",避免跨模块修改导致冲突。
|
||||
|
||||
| 模块 | scope | 路径 | 负责阶段 | 建议 AI 数 |
|
||||
| -------------- | ---------------- | ------------------------- | -------- | ------------ |
|
||||
| api-gateway | `api-gateway` | `services/api-gateway/` | P1 | 1 |
|
||||
| classes | `classes` | `services/classes/` | P1 | 1 |
|
||||
| teacher-portal | `teacher-portal` | `apps/teacher-portal/` | P1-P2 | 1 |
|
||||
| iam | `iam` | `services/iam/` | P2 | 1 |
|
||||
| teacher-bff | `teacher-bff` | `services/teacher-bff/` | P2 | 1 |
|
||||
| core-edu | `core-edu` | `services/core-edu/` | P3 | 1 |
|
||||
| content | `content` | `services/content/` | P4 | 1 |
|
||||
| data-ana | `data-ana` | `services/data-ana/` | P4 | 1 |
|
||||
| msg | `msg` | `services/msg/` | P5 | 1 |
|
||||
| ai | `ai` | `services/ai/` | P5 | 1 |
|
||||
| push-gateway | `push-gateway` | `services/push-gateway/` | P5 | 1 |
|
||||
| shared-proto | `shared-proto` | `packages/shared-proto/` | 跨阶段 | 协调 AI 维护 |
|
||||
| shared-tokens | `shared-tokens` | `packages/shared-tokens/` | 跨阶段 | 协调 AI 维护 |
|
||||
| infra | `infra` | `infra/` | 跨阶段 | 1 (SRE AI) |
|
||||
| docs | `docs` | `docs/` | 跨阶段 | 协调 AI 维护 |
|
||||
|
||||
### 2.2 分工原则
|
||||
|
||||
1. **单一负责制**:每个模块只有一个 AI 负责,避免并行修改同一文件
|
||||
2. **契约集中管理**:`shared-proto` 由协调 AI 维护,开发 AI 只读引用
|
||||
3. **跨模块变更拆分**:需要修改多个模块时,拆成多个 PR,按依赖顺序合并
|
||||
4. **基础设施独立**:`infra/` 由 SRE AI 专门负责,业务 AI 不直接修改
|
||||
|
||||
---
|
||||
|
||||
## 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. 相关文档
|
||||
|
||||
- [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) — 已知问题速查
|
||||
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,140 @@
|
||||
|
||||
### 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 空格 |
|
||||
|
||||
### 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` 便于服务连接配置 |
|
||||
|
||||
### 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` |
|
||||
|
||||
### 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 +151,140 @@
|
||||
|
||||
### 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` |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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(`mysql.cdc.*`)→ DataAna 消费写 ClickHouse |
|
||||
| CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 |
|
||||
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
|
||||
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
|
||||
|
||||
### 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 推送 + 通知偏好 |
|
||||
| 通知批量化 | `createNotifications(items)` 单次 INSERT,沿用旧项目 dispatcher 模式 |
|
||||
| 多渠道 | 站内/SMS/邮件/微信,in_app 批量 + 其他渠道并行 |
|
||||
| fan-out 分页 | `getAllUserIds(limit=1000, offset)` 分页遍历 |
|
||||
| 撤回不乐观更新 | 需服务端返回判断 2 分钟窗口 |
|
||||
|
||||
### 2.8 push-gateway(Go,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需发 Kafka 消息 |
|
||||
| 跨实例同步 | Redis PubSub |
|
||||
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------- | ----------------------------------------------- |
|
||||
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需发 Kafka 消息 |
|
||||
| 跨实例同步 | Redis PubSub |
|
||||
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
|
||||
|
||||
### 2.9 ai-gateway(Python/FastAPI,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| LLM Provider 适配 | OpenAI/Anthropic,langchain/litellm 生态 |
|
||||
| Prompt 模板管理 | 版本管理友好 |
|
||||
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
|
||||
| 用量计费 | 按 token 计费 |
|
||||
| AI 模块纯服务端 | Zod 验证 + 失败降级返回空(沿用旧项目模式) |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------- | ------------------------------------------- |
|
||||
| LLM Provider 适配 | OpenAI/Anthropic,langchain/litellm 生态 |
|
||||
| Prompt 模板管理 | 版本管理友好 |
|
||||
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
|
||||
| 用量计费 | 按 token 计费 |
|
||||
| AI 模块纯服务端 | Zod 验证 + 失败降级返回空(沿用旧项目模式) |
|
||||
|
||||
### 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -290,6 +292,17 @@
|
||||
|
||||
> 按时间倒序,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-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 硬化。 |
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
4
go.work.sum
Normal file
4
go.work.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
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: "混沌后清理与恢复验证"
|
||||
38
infra/deploy.env.example
Normal file
38
infra/deploy.env.example
Normal file
@@ -0,0 +1,38 @@
|
||||
# 服务器部署环境变量模板
|
||||
# 使用方式:复制到 /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 阶段启用,P1 留空)============
|
||||
KAFKA_BROKERS=
|
||||
|
||||
# ============ 可观测性(P6 阶段启用,P1 留空)============
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
LOG_LEVEL=info
|
||||
|
||||
# ============ 镜像 tag(由 CI 注入,手动部署时可改)============
|
||||
# IMAGE_TAG=latest ← 默认 latest,CI 通过 export IMAGE_TAG 覆盖
|
||||
105
infra/docker-compose.deploy.yml
Normal file
105
infra/docker-compose.deploy.yml
Normal file
@@ -0,0 +1,105 @@
|
||||
# 服务器部署用 Docker Compose
|
||||
# 镜像来源:Gitea Container Registry (git.eazygame.cn/xiner/edu/<service>:<tag>)
|
||||
# 基础设施:MySQL + Redis 已在服务器 Docker 中运行(不在此文件管理)
|
||||
#
|
||||
# 部署目录:/opt/edu/
|
||||
# 部署命令(CI 自动执行):
|
||||
# IMAGE_TAG=latest docker compose pull && docker compose up -d
|
||||
#
|
||||
# 首次部署手动步骤:
|
||||
# 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 login git.eazygame.cn -u <user> -p <token>
|
||||
# 5. cd /opt/edu && IMAGE_TAG=latest docker compose up -d
|
||||
|
||||
name: edu
|
||||
|
||||
services:
|
||||
api-gateway:
|
||||
image: git.eazygame.cn/xiner/edu/api-gateway:${IMAGE_TAG:-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
|
||||
- edu-shared
|
||||
|
||||
classes:
|
||||
image: git.eazygame.cn/xiner/edu/classes:${IMAGE_TAG:-latest}
|
||||
container_name: edu-classes
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PORT: 3001
|
||||
# 连接服务器已有的 MySQL(容器名 edu-mysql,需在 edu-shared 网络)
|
||||
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
|
||||
|
||||
teacher-portal:
|
||||
image: git.eazygame.cn/xiner/edu/teacher-portal:${IMAGE_TAG:-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
|
||||
- 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
|
||||
119
infra/docker-compose.monitoring.yml
Normal file
119
infra/docker-compose.monitoring.yml
Normal file
@@ -0,0 +1,119 @@
|
||||
# 监控栈 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
|
||||
|
||||
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
|
||||
|
||||
# ============================================================
|
||||
# blackbox-exporter - 黑盒探测(HTTP / TCP / ICMP)
|
||||
# ============================================================
|
||||
blackbox-exporter:
|
||||
image: prom/blackbox-exporter:v0.25.0
|
||||
container_name: edu-blackbox-exporter
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9115:9115"
|
||||
volumes:
|
||||
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
|
||||
networks:
|
||||
- edu-network
|
||||
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
|
||||
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
|
||||
15
infra/grafana/provisioning/datasources/prometheus.yml
Normal file
15
infra/grafana/provisioning/datasources/prometheus.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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
|
||||
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 }}
|
||||
35
infra/k8s/helm/edu-platform/templates/ingress.yaml
Normal file
35
infra/k8s/helm/edu-platform/templates/ingress.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-ingress
|
||||
namespace: edu-ingress
|
||||
labels:
|
||||
{{- include "edu-platform.labels" $ | nindent 4 }}
|
||||
{{- with $.Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ $.Values.ingress.className }}
|
||||
{{- with $.Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ .service }}
|
||||
port:
|
||||
number: {{ .port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
10
infra/k8s/helm/edu-platform/templates/namespace.yaml
Normal file
10
infra/k8s/helm/edu-platform/templates/namespace.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
{{- range .Values.namespaces }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .name }}
|
||||
labels:
|
||||
{{- include "edu-platform.labels" $ | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
{{- end }}
|
||||
26
infra/k8s/helm/edu-platform/templates/secret.yaml
Normal file
26
infra/k8s/helm/edu-platform/templates/secret.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
{{- if .Values.secret.enabled }}
|
||||
{{- /*
|
||||
⚠️ 生产环境警告:不要在 values.yaml 中硬编码真实密钥!
|
||||
推荐方案:使用 External Secrets Operator 对接 Vault / KMS / 云 KMS
|
||||
本模板仅作骨架;空字符串字段不会被渲染(避免覆盖已存在的 Secret)。
|
||||
*/ -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Values.secret.name }}
|
||||
namespace: edu-system
|
||||
labels:
|
||||
{{- include "edu-platform.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install
|
||||
"helm.sh/hook-delete-policy": before-hook-creation
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $k, $v := .Values.secret.data }}
|
||||
{{- if $v }}
|
||||
{{ $k }}: {{ $v }}
|
||||
{{- else }}
|
||||
{{ $k }}: "" # 占位:部署时通过 --set 或 ExternalSecrets 注入
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
24
infra/k8s/helm/edu-platform/values-dev.yaml
Normal file
24
infra/k8s/helm/edu-platform/values-dev.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# 开发环境覆盖
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/environment: dev
|
||||
|
||||
configMap:
|
||||
data:
|
||||
NODE_ENV: development
|
||||
LOG_LEVEL: debug
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: edu-dev.local
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
tls: [] # 开发环境不强制 TLS
|
||||
|
||||
hpa:
|
||||
enabled: false # 开发环境单副本即可
|
||||
35
infra/k8s/helm/edu-platform/values-prod.yaml
Normal file
35
infra/k8s/helm/edu-platform/values-prod.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# 生产环境覆盖
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/environment: prod
|
||||
imagePullSecrets:
|
||||
- name: registry-credentials
|
||||
|
||||
configMap:
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: warn
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- host: edu.example.com
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
- path: /auth
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
tls:
|
||||
- secretName: edu-tls
|
||||
hosts:
|
||||
- edu.example.com
|
||||
|
||||
hpa:
|
||||
minReplicas: 3
|
||||
maxReplicas: 20
|
||||
targetCPUUtilizationPercentage: 60
|
||||
targetMemoryUtilizationPercentage: 70
|
||||
23
infra/k8s/helm/edu-platform/values-staging.yaml
Normal file
23
infra/k8s/helm/edu-platform/values-staging.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# 预发布环境覆盖
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/environment: staging
|
||||
|
||||
configMap:
|
||||
data:
|
||||
NODE_ENV: staging
|
||||
LOG_LEVEL: info
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- host: edu-staging.example.com
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
|
||||
hpa:
|
||||
minReplicas: 2
|
||||
maxReplicas: 5
|
||||
72
infra/k8s/helm/edu-platform/values.yaml
Normal file
72
infra/k8s/helm/edu-platform/values.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
# Edu 平台全局默认值
|
||||
# 环境覆盖文件:values-dev.yaml / values-staging.yaml / values-prod.yaml
|
||||
|
||||
# 全局标签(自动注入到所有资源)
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
# 镜像拉取凭证(生产建议使用 Secret + serviceAccount)
|
||||
imagePullSecrets: []
|
||||
|
||||
# 命名空间规划
|
||||
namespaces:
|
||||
- name: edu-system
|
||||
component: system
|
||||
- name: edu-services
|
||||
component: services
|
||||
- name: edu-monitoring
|
||||
component: monitoring
|
||||
- name: edu-ingress
|
||||
component: ingress
|
||||
|
||||
# 全局 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
name: edu-platform-config
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
MYSQL_HOST: mysql.edu-system.svc.cluster.local
|
||||
REDIS_HOST: redis.edu-system.svc.cluster.local
|
||||
KAFKA_BROKERS: kafka.edu-system.svc.cluster.local:9092
|
||||
|
||||
# 全局 Secret(仅骨架;生产请使用 External Secrets Operator 对接 Vault/KMS)
|
||||
secret:
|
||||
enabled: true
|
||||
name: edu-platform-secret
|
||||
# 真实部署时通过 --set secret.data.MYSQL_PASSWORD=<base64> 注入
|
||||
# 或通过 externalSecrets 引用
|
||||
data:
|
||||
MYSQL_PASSWORD: ""
|
||||
JWT_SECRET: ""
|
||||
REDIS_PASSWORD: ""
|
||||
|
||||
# 全局 Ingress
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: edu.example.com
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
- path: /auth
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
tls:
|
||||
- secretName: edu-tls
|
||||
hosts:
|
||||
- edu.example.com
|
||||
|
||||
# HPA 全局默认策略(各服务可在自身 chart 中覆盖)
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
11
infra/k8s/helm/iam/Chart.yaml
Normal file
11
infra/k8s/helm/iam/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: iam
|
||||
description: iam 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- iam
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/iam/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/iam/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "iam.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "iam.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 "iam.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "iam.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: identity
|
||||
{{- end -}}
|
||||
|
||||
{{- define "iam.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "iam.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/iam/templates/deployment.yaml
Normal file
77
infra/k8s/helm/iam/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "iam.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "iam.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "iam.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "iam.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: identity
|
||||
{{- 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 "iam.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 "iam.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/iam/templates/service.yaml
Normal file
16
infra/k8s/helm/iam/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "iam.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "iam.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "iam.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/iam/values.yaml
Normal file
85
infra/k8s/helm/iam/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# iam 服务默认值(身份认证 - 业务领域 D1)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/iam
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3001
|
||||
targetPort: 3001
|
||||
|
||||
# 命名空间(默认 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: 3001
|
||||
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/msg/Chart.yaml
Normal file
11
infra/k8s/helm/msg/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: msg
|
||||
description: msg 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- msg
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/msg/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/msg/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "msg.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "msg.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 "msg.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "msg.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: messaging
|
||||
{{- end -}}
|
||||
|
||||
{{- define "msg.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "msg.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/msg/templates/deployment.yaml
Normal file
77
infra/k8s/helm/msg/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "msg.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "msg.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "msg.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "msg.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: messaging
|
||||
{{- 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 "msg.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 "msg.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/msg/templates/service.yaml
Normal file
16
infra/k8s/helm/msg/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "msg.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "msg.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "msg.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/msg/values.yaml
Normal file
85
infra/k8s/helm/msg/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# msg 服务默认值(消息通知 - 业务领域 D5)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/msg
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3004
|
||||
targetPort: 3004
|
||||
|
||||
# 命名空间(默认 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: 3004
|
||||
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
|
||||
33
infra/k8s/namespace.yaml
Normal file
33
infra/k8s/namespace.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
# K8s 命名空间定义 - Edu 平台
|
||||
# 划分 4 个命名空间:system / services / monitoring / ingress
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-system
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-services
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: services
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-monitoring
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: monitoring
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-ingress
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: ingress
|
||||
122
infra/prometheus/rules.yml
Normal file
122
infra/prometheus/rules.yml
Normal file
@@ -0,0 +1,122 @@
|
||||
# Prometheus 告警规则 - Edu 平台
|
||||
# 分组:服务可用性 / 性能 / 资源
|
||||
groups:
|
||||
# ============================================================
|
||||
# 服务可用性
|
||||
# ============================================================
|
||||
- name: service-availability
|
||||
rules:
|
||||
# 服务宕机
|
||||
- alert: ServiceDown
|
||||
expr: up == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: availability
|
||||
annotations:
|
||||
summary: "服务宕机 {{ $labels.instance }}"
|
||||
description: "目标 {{ $labels.job }} / {{ $labels.instance }} 已离线超过 1 分钟。"
|
||||
|
||||
# 熔断器打开
|
||||
- alert: CircuitBreakerOpen
|
||||
expr: circuit_breaker_state{state="open"} == 1
|
||||
for: 30s
|
||||
labels:
|
||||
severity: critical
|
||||
category: availability
|
||||
annotations:
|
||||
summary: "熔断器打开 {{ $labels.service }}"
|
||||
description: "服务 {{ $labels.service }} 熔断器处于 open 状态,可能正在拒绝请求。"
|
||||
|
||||
# ============================================================
|
||||
# 性能
|
||||
# ============================================================
|
||||
- name: performance
|
||||
rules:
|
||||
# 高错误率(5xx 占比 > 5%)
|
||||
- alert: HighErrorRate
|
||||
expr: |
|
||||
(sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
|
||||
/ sum(rate(http_requests_total[5m])) by (service))
|
||||
> 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: performance
|
||||
annotations:
|
||||
summary: "错误率过高 {{ $labels.service }}"
|
||||
description: "服务 {{ $labels.service }} 5xx 错误率超过 5%,持续 5 分钟。"
|
||||
|
||||
# P99 延迟过高
|
||||
- alert: HighLatencyP99
|
||||
expr: |
|
||||
histogram_quantile(0.99,
|
||||
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
|
||||
> 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: performance
|
||||
annotations:
|
||||
summary: "P99 延迟过高 {{ $labels.service }}"
|
||||
description: "服务 {{ $labels.service }} P99 延迟超过 1 秒,持续 5 分钟。"
|
||||
|
||||
# Kafka 消费者积压
|
||||
- alert: KafkaConsumerLag
|
||||
expr: kafka_consumergroup_lag > 1000
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: performance
|
||||
annotations:
|
||||
summary: "Kafka 消费者积压 {{ $labels.consumergroup }}"
|
||||
description: "消费组 {{ $labels.consumergroup }} 在 topic {{ $labels.topic }} 上积压超过 1000 条,持续 10 分钟。"
|
||||
|
||||
# ============================================================
|
||||
# 资源
|
||||
# ============================================================
|
||||
- name: resources
|
||||
rules:
|
||||
# MySQL 连接数过高
|
||||
- alert: MySQLConnectionsHigh
|
||||
expr: |
|
||||
(mysql_global_status_threads_connected
|
||||
/ mysql_global_variables_max_connections)
|
||||
> 0.8
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: resource
|
||||
annotations:
|
||||
summary: "MySQL 连接数过高 {{ $labels.instance }}"
|
||||
description: "MySQL 实例 {{ $labels.instance }} 连接数超过最大连接数的 80%,持续 5 分钟。"
|
||||
|
||||
# 磁盘空间不足
|
||||
- alert: DiskSpaceLow
|
||||
expr: |
|
||||
(100
|
||||
- (node_filesystem_avail_bytes
|
||||
/ node_filesystem_size_bytes * 100))
|
||||
> 85
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: resource
|
||||
annotations:
|
||||
summary: "磁盘空间不足 {{ $labels.instance }}"
|
||||
description: "节点 {{ $labels.instance }} 挂载点 {{ $labels.mountpoint }} 磁盘使用率超过 85%,持续 10 分钟。"
|
||||
|
||||
# 内存使用过高
|
||||
- alert: MemoryHigh
|
||||
expr: |
|
||||
(1
|
||||
- (node_memory_MemAvailable_bytes
|
||||
/ node_memory_MemTotal_bytes))
|
||||
> 0.9
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: resource
|
||||
annotations:
|
||||
summary: "内存使用过高 {{ $labels.instance }}"
|
||||
description: "节点 {{ $labels.instance }} 内存使用率超过 90%,持续 5 分钟。"
|
||||
50
infra/security/secrets.example.env
Normal file
50
infra/security/secrets.example.env
Normal file
@@ -0,0 +1,50 @@
|
||||
# Edu 平台密钥示例环境变量文件
|
||||
# ============================================================
|
||||
# 警告:本文件仅作示例,禁止包含真实密钥。
|
||||
# 生产环境请通过 K8s Secret / External Secrets / Vault 注入。
|
||||
# 复制为 .env 后用真实值替换所有 <placeholder>。
|
||||
|
||||
# ---------- MySQL ----------
|
||||
# 用途:MySQL root 用户密码,用于初始化与备份/恢复
|
||||
# 最小长度:32 字符
|
||||
# 复杂度:含大小写字母 + 数字 + 特殊符号
|
||||
MYSQL_ROOT_PASSWORD=<replace-with-32-char-strong-password>
|
||||
|
||||
# ---------- JWT ----------
|
||||
# 用途:JWT Access Token 签名密钥(HS256)
|
||||
# 最小长度:64 字符(建议使用 openssl rand -base64 48 生成)
|
||||
# 注意:旋转后所有已签发的 Access Token 立即失效
|
||||
JWT_SECRET=<replace-with-64-char-jwt-signing-secret>
|
||||
|
||||
# 用途:JWT Refresh Token 签名密钥(HS256)
|
||||
# 最小长度:64 字符
|
||||
# 注意:与 JWT_SECRET 必须不同;旋转后所有用户需重新登录
|
||||
JWT_REFRESH_SECRET=<replace-with-64-char-refresh-signing-secret>
|
||||
|
||||
# ---------- Kafka ----------
|
||||
# 用途:Kafka SASL/PLAIN 认证密码
|
||||
# 最小长度:24 字符
|
||||
KAFKA_SASL_PASSWORD=<replace-with-24-char-kafka-password>
|
||||
|
||||
# ---------- Elasticsearch ----------
|
||||
# 用途:Elasticsearch 内置 elastic 用户密码
|
||||
# 最小长度:24 字符
|
||||
ES_PASSWORD=<replace-with-24-char-es-password>
|
||||
|
||||
# ---------- Neo4j ----------
|
||||
# 用途:Neo4j 数据库管理员密码
|
||||
# 最小长度:24 字符
|
||||
NEO4J_PASSWORD=<replace-with-24-char-neo4j-password>
|
||||
|
||||
# ---------- Redis ----------
|
||||
# 用途:Redis ACL 默认用户密码
|
||||
# 最小长度:24 字符
|
||||
# 注意:生产环境建议启用 ACL,按用户分配最小权限
|
||||
REDIS_PASSWORD=<replace-with-24-char-redis-password>
|
||||
|
||||
# ---------- 应用层加密 ----------
|
||||
# 用途:应用层字段级加密密钥(AES-256-GCM)
|
||||
# 最小长度:32 字节(base64 编码后约 44 字符)
|
||||
# 生成:openssl rand -base64 32
|
||||
# 注意:旋转前需先解密所有已加密字段,旋转后重新加密
|
||||
ENCRYPTION_KEY=<replace-with-base64-32-byte-aes-key>
|
||||
64
infra/security/waf-rules.conf
Normal file
64
infra/security/waf-rules.conf
Normal file
@@ -0,0 +1,64 @@
|
||||
# ModSecurity 兼容 WAF 规则骨架 - Edu 平台
|
||||
# ============================================================
|
||||
# 说明:本文件为 ModSecurity SecRule 语法骨架,
|
||||
# 生产部署前请配合 OWASP CRS 使用并完成规则调优。
|
||||
# 参考:https://github.com/SpiderLabs/ModSecurity
|
||||
# https://github.com/coreruleset/coreruleset
|
||||
|
||||
# ---------- 基础配置 ----------
|
||||
# 启用规则引擎
|
||||
SecRuleEngine On
|
||||
|
||||
# 请求体大小限制(10MB)
|
||||
SecRequestBodyLimit 10485760
|
||||
|
||||
# 默认动作:记录日志 + 拒绝(生产前调整)
|
||||
SecDefaultAction "phase:2,log,deny,status:403"
|
||||
|
||||
# ============================================================
|
||||
# 规则 1:SQL 注入检测
|
||||
# 检测常见 SQL 注入关键字与元字符
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_BODY|XML:/* \
|
||||
"(?i)(union\s+select|select\s+.*\s+from|insert\s+into|update\s+.*\s+set|delete\s+from|drop\s+table|alter\s+table|create\s+table|exec\s*\(|;\s*drop\s|--\s|/\*.*\*/|or\s+1\s*=\s*1|and\s+1\s*=\s*1|'\s*or\s*'|sleep\s*\(|benchmark\s*\(|load_file\s*\(|into\s+outfile)" \
|
||||
"id:1001,phase:2,log,deny,status:403,msg:'SQL Injection attempt',tag:'attack-sqli',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 2:XSS 检测
|
||||
# 检测常见 XSS payload 与事件处理器
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_BODY|XML:/* \
|
||||
"(?i)(<script|</script>|javascript:|onerror\s*=|onload\s*=|onclick\s*=|onmouseover\s*=|onfocus\s*=|onblur\s*=|<iframe|<img[^>]+src\s*=|<svg[^>]+onload|document\.cookie|window\.location|eval\s*\(|alert\s*\(|prompt\s*\(|String\.fromCharCode)" \
|
||||
"id:1002,phase:2,log,deny,status:403,msg:'XSS attempt',tag:'attack-xss',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 3:路径遍历检测
|
||||
# 检测 ../ 与编码变体,防止读取敏感文件
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_FILENAME|REQUEST_URI|REQUEST_BODY \
|
||||
"(?i)(\.\./|\.\.\\|\.\.%2f|\.\.%5c|%2e%2e%2f|%2e%2e%5c|/etc/passwd|/etc/shadow|/etc/hosts|/proc/self/environ|c:\\windows\\win\.ini|c:\\boot\.ini|\.\./\.\./\.\./)" \
|
||||
"id:1003,phase:2,log,deny,status:403,msg:'Path Traversal attempt',tag:'attack-lfi',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 4:User-Agent 黑名单
|
||||
# 拦截已知恶意扫描器与爬虫
|
||||
# ============================================================
|
||||
SecRule REQUEST_HEADERS:User-Agent \
|
||||
"(?i)(sqlmap|nikto|nmap|nessus|acunetix|wpscan|hydra|metasploit|burpcollaborator|masscan|zgrab|dirbuster|gobuster|fuzzer|webinspect|appscan|jaeles|xrkmd)" \
|
||||
"id:1004,phase:1,log,deny,status:403,msg:'Malicious User-Agent blocked',tag:'attack-scanner',severity:WARNING"
|
||||
|
||||
# ============================================================
|
||||
# 规则 5:远程文件包含(RFI)检测
|
||||
# 检测通过 URL 参数引入远程文件
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES \
|
||||
"(?i)(^(http|https|ftp|php|data)://|(http|https|ftp|php|data)://.*\?(php|http|https|ftp|data)://)" \
|
||||
"id:1005,phase:2,log,deny,status:403,msg:'Remote File Inclusion attempt',tag:'attack-rfi',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 6:命令注入检测
|
||||
# 检测 shell 元字符与命令执行关键字
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_BODY|REQUEST_COOKIES \
|
||||
"(?i)(;\s*(ls|cat|id|whoami|uname|pwd|wget|curl|nc|bash|sh|python|perl|ruby)\s|\$\(|`|&&\s*\w+\s|\|\|\s*\w+\s|\|\s*(ls|cat|id|whoami|uname|pwd|wget|curl|nc|bash|sh|python|perl|ruby)\s|/bin/(ba)?sh|/bin/cat|/usr/bin/(wget|curl|python|perl))" \
|
||||
"id:1006,phase:2,log,deny,status:403,msg:'Command Injection attempt',tag:'attack-rce',severity:CRITICAL"
|
||||
@@ -1,7 +1,9 @@
|
||||
module.exports = {
|
||||
'*.{ts,tsx}': ['eslint --fix', 'prettier --write'],
|
||||
'*.{go,mod,sum}': ['gofmt -w', 'golangci-lint run --fix'],
|
||||
'*.{py}': ['ruff check --fix', 'ruff format'],
|
||||
module.exports = {
|
||||
// ESLint 9 flat config 迁移完成后恢复:['eslint --fix', 'prettier --write']
|
||||
'*.{ts,tsx}': ['prettier --write'],
|
||||
// Go 工具链不在 git hook PATH 中,Go 文件格式化由 go fmt 手动执行
|
||||
// golangci-lint 安装后恢复:['gofmt -w', 'golangci-lint run --fix']
|
||||
'*.py': ['ruff check --fix', 'ruff format'],
|
||||
'*.proto': ['buf format --write'],
|
||||
'*.md': ['prettier --write'],
|
||||
};
|
||||
|
||||
10289
pnpm-lock.yaml
generated
Normal file
10289
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user