Compare commits
9 Commits
2df2237d56
...
a1d7fcfd71
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1d7fcfd71 | ||
|
|
c31b3ddeba | ||
|
|
ae460e61a3 | ||
|
|
a3f00c882a | ||
|
|
d19285a977 | ||
|
|
3b88e9cca5 | ||
|
|
4a0893ef52 | ||
|
|
e5902ca2b3 | ||
|
|
a4ec5b72c5 |
@@ -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
|
||||
|
||||
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
|
||||
@@ -369,4 +369,164 @@ services/[service]/src/
|
||||
|
||||
---
|
||||
|
||||
## 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。**
|
||||
|
||||
@@ -38,6 +38,9 @@ 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)
|
||||
- [项目规则](.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"]
|
||||
@@ -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>
|
||||
|
||||
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)
|
||||
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) — 已知问题速查
|
||||
@@ -151,16 +151,18 @@
|
||||
|
||||
### 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 黄金模板)
|
||||
|
||||
@@ -290,15 +292,17 @@
|
||||
|
||||
> 按时间倒序,50 条上限。AI 发现更好方案时可更新本节。
|
||||
|
||||
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|
||||
| ---------- | ---- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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 硬化。 |
|
||||
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|
||||
| ---------- | ---- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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 硬化。 |
|
||||
|
||||
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=
|
||||
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
|
||||
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
|
||||
187
pnpm-lock.yaml
generated
187
pnpm-lock.yaml
generated
@@ -111,6 +111,12 @@ importers:
|
||||
drizzle-orm:
|
||||
specifier: ^0.31.0
|
||||
version: 0.31.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(@types/react@18.3.31)(better-sqlite3@11.10.0)(mysql2@3.22.6(@types/node@22.20.0))(react@18.3.1)
|
||||
ioredis:
|
||||
specifier: ^5.11.1
|
||||
version: 5.11.1
|
||||
kafkajs:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.4
|
||||
mysql2:
|
||||
specifier: ^3.11.0
|
||||
version: 3.22.6(@types/node@22.20.0)
|
||||
@@ -129,6 +135,9 @@ importers:
|
||||
rxjs:
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.2
|
||||
typeorm:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(ioredis@5.11.1)(mysql2@3.22.6(@types/node@22.20.0))
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
@@ -1271,6 +1280,9 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@ioredis/commands@1.10.0':
|
||||
resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2206,6 +2218,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sqltools/formatter@1.2.5':
|
||||
resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
@@ -2520,6 +2535,10 @@ packages:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansis@4.3.1:
|
||||
resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
@@ -2764,10 +2783,18 @@ packages:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
cliui@9.0.1:
|
||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
clone@1.0.4:
|
||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
cluster-key-slot@1.1.1:
|
||||
resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
code-block-writer@13.0.3:
|
||||
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
|
||||
|
||||
@@ -2909,6 +2936,9 @@ packages:
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
dayjs@1.11.21:
|
||||
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
@@ -2930,6 +2960,14 @@ packages:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
dedent@1.7.2:
|
||||
resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==}
|
||||
peerDependencies:
|
||||
babel-plugin-macros: ^3.1.0
|
||||
peerDependenciesMeta:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3595,6 +3633,10 @@ packages:
|
||||
resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ioredis@5.11.1:
|
||||
resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -4459,6 +4501,14 @@ packages:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
||||
redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
@@ -4667,9 +4717,16 @@ packages:
|
||||
resolution: {integrity: sha512-Ti/Zx9J3aITMYUMFhCKbkplQjyi7Kk2SyYXp+rzrkyKZetIy19XbQtVZ+0ZR5aVm/178tIJ6+aVvBqXkH+Xs7w==}
|
||||
engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'}
|
||||
|
||||
sql-highlight@6.1.0:
|
||||
resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
standard-as-callback@2.1.0:
|
||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -4947,6 +5004,58 @@ packages:
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typeorm@1.0.0:
|
||||
resolution: {integrity: sha512-2mSKNqucP8vo+xQLP59xlHUcqLvG6qajxA7q7tnhJgeZjTrA6lK/Ar7LRyiAxdXhyXmGbIPsArPmcUB9Xg+M7w==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24.11.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@google-cloud/spanner': ^8.0.0
|
||||
'@sap/hana-client': ^2.14.22
|
||||
better-sqlite3: ^12.0.0
|
||||
ioredis: ^5.0.4
|
||||
mongodb: ^7.0.0
|
||||
mssql: ^12.0.0
|
||||
mysql2: ^3.15.3
|
||||
oracledb: ^6.3.0
|
||||
pg: ^8.5.1
|
||||
pg-native: ^3.0.0
|
||||
pg-query-stream: ^4.0.0
|
||||
redis: ^5.0.0
|
||||
sql.js: ^1.4.0
|
||||
ts-node: ^10.9.2
|
||||
typeorm-aurora-data-api-driver: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
'@google-cloud/spanner':
|
||||
optional: true
|
||||
'@sap/hana-client':
|
||||
optional: true
|
||||
better-sqlite3:
|
||||
optional: true
|
||||
ioredis:
|
||||
optional: true
|
||||
mongodb:
|
||||
optional: true
|
||||
mssql:
|
||||
optional: true
|
||||
mysql2:
|
||||
optional: true
|
||||
oracledb:
|
||||
optional: true
|
||||
pg:
|
||||
optional: true
|
||||
pg-native:
|
||||
optional: true
|
||||
pg-query-stream:
|
||||
optional: true
|
||||
redis:
|
||||
optional: true
|
||||
sql.js:
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
typeorm-aurora-data-api-driver:
|
||||
optional: true
|
||||
|
||||
typescript@5.7.2:
|
||||
resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -5171,10 +5280,18 @@ packages:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs-parser@22.0.0:
|
||||
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
|
||||
yargs@17.7.3:
|
||||
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@18.0.0:
|
||||
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5753,6 +5870,8 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@ioredis/commands@1.10.0': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -6928,6 +7047,8 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.62.2':
|
||||
optional: true
|
||||
|
||||
'@sqltools/formatter@1.2.5': {}
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.23':
|
||||
@@ -7323,6 +7444,8 @@ snapshots:
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
ansis@4.3.1: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -7580,8 +7703,16 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
cliui@9.0.1:
|
||||
dependencies:
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.2.0
|
||||
wrap-ansi: 9.0.2
|
||||
|
||||
clone@1.0.4: {}
|
||||
|
||||
cluster-key-slot@1.1.1: {}
|
||||
|
||||
code-block-writer@13.0.3: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
@@ -7711,6 +7842,8 @@ snapshots:
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
dayjs@1.11.21: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
@@ -7723,6 +7856,8 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
dedent@1.7.2: {}
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
@@ -8487,6 +8622,18 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
ioredis@5.11.1:
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.10.0
|
||||
cluster-key-slot: 1.1.1
|
||||
debug: 4.4.3
|
||||
denque: 2.1.0
|
||||
redis-errors: 1.2.0
|
||||
redis-parser: 3.0.0
|
||||
standard-as-callback: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
@@ -9313,6 +9460,12 @@ snapshots:
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
redis-errors@1.2.0: {}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
dependencies:
|
||||
redis-errors: 1.2.0
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
repeat-string@1.6.1: {}
|
||||
@@ -9557,8 +9710,12 @@ snapshots:
|
||||
|
||||
sql-escaper@1.4.0: {}
|
||||
|
||||
sql-highlight@6.1.0: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
standard-as-callback@2.1.0: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
@@ -9828,6 +9985,25 @@ snapshots:
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typeorm@1.0.0(ioredis@5.11.1)(mysql2@3.22.6(@types/node@22.20.0)):
|
||||
dependencies:
|
||||
'@sqltools/formatter': 1.2.5
|
||||
ansis: 4.3.1
|
||||
dayjs: 1.11.21
|
||||
debug: 4.4.3
|
||||
dedent: 1.7.2
|
||||
reflect-metadata: 0.2.2
|
||||
sql-highlight: 6.1.0
|
||||
tinyglobby: 0.2.17
|
||||
tslib: 2.8.1
|
||||
yargs: 18.0.0
|
||||
optionalDependencies:
|
||||
ioredis: 5.11.1
|
||||
mysql2: 3.22.6(@types/node@22.20.0)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
typescript@5.7.2: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
@@ -10085,6 +10261,8 @@ snapshots:
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs-parser@22.0.0: {}
|
||||
|
||||
yargs@17.7.3:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
@@ -10095,6 +10273,15 @@ snapshots:
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yargs@18.0.0:
|
||||
dependencies:
|
||||
cliui: 9.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
string-width: 7.2.0
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 22.0.0
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
yocto-queue@1.2.2: {}
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
# Build stage
|
||||
# 多阶段构建:api-gateway 生产镜像
|
||||
# 用法:docker build -t edu/api-gateway:latest -f services/api-gateway/Dockerfile .
|
||||
|
||||
# ============ Builder ============
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# git 与 ca-certificates 为 go mod 下载所需
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
|
||||
# 先拷依赖清单利用缓存
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o api-gateway .
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.20
|
||||
RUN apk --no-cache add ca-certificates
|
||||
# 拷源码并构建
|
||||
COPY . .
|
||||
# 静态编译,CGO_DISABLED 便于 alpine 运行
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/bin/api-gateway ./main.go
|
||||
|
||||
# ============ Runtime ============
|
||||
FROM alpine:3.20 AS runner
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/api-gateway .
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata wget
|
||||
|
||||
# 非 root 用户
|
||||
RUN addgroup -g 1001 -S app && adduser -S app -u 1001 -G app
|
||||
|
||||
COPY --from=builder /app/bin/api-gateway /app/api-gateway
|
||||
|
||||
USER app
|
||||
EXPOSE 8080
|
||||
CMD ["./api-gateway"]
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget --quiet --spider http://localhost:8080/healthz || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/api-gateway"]
|
||||
|
||||
@@ -71,14 +71,20 @@ docker build -t edu/api-gateway .
|
||||
|
||||
通过环境变量配置(见 `internal/config/config.go`):
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --------------------- | --------------------- | -------------------- |
|
||||
| `PORT` | 8080 | 监听端口 |
|
||||
| `JWT_SECRET` | (必填) | HS256 签名密钥(P1) |
|
||||
| `JWT_PUBLIC_KEY` | (P2) | RS256 公钥 |
|
||||
| `CLASSES_SERVICE_URL` | http://localhost:3001 | classes 服务地址 |
|
||||
| `RATE_LIMIT_RPS` | 10 | 每秒令牌数 |
|
||||
| `RATE_LIMIT_BURST` | 20 | 突发容量 |
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| ----------------------------- | --------------------- | ------------------------------------------------------ |
|
||||
| `API_GATEWAY_PORT` | 8080 | 监听端口 |
|
||||
| `JWT_SECRET` | (必填) | HS256 签名密钥(P1) |
|
||||
| `JWT_ISSUER` | next-edu-cloud | JWT 签发者 |
|
||||
| `JWT_AUDIENCE` | next-edu-cloud | JWT 受众 |
|
||||
| `DEV_MODE` | false | 开发模式旁路:true 时接受 `Bearer dev-token`(仅本地) |
|
||||
| `CLASSES_SERVICE_URL` | http://localhost:3001 | classes 服务地址 |
|
||||
| `IAM_SERVICE_URL` | http://localhost:3002 | iam 服务地址 |
|
||||
| `TEACHER_BFF_URL` | http://localhost:3003 | teacher-bff 服务地址 |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | http://localhost:4318 | OpenTelemetry OTLP 端点 |
|
||||
| `LOG_LEVEL` | info | 日志级别 |
|
||||
|
||||
> **生产环境警告**:`DEV_MODE` 必须为 `false` 或不设。设为 `true` 会允许 `dev-token` 旁路鉴权并注入固定 admin 身份。
|
||||
|
||||
## 关联文档
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ type Config struct {
|
||||
TeacherBffURL string
|
||||
OTLPEndpoint string
|
||||
LogLevel string
|
||||
DevMode bool
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
@@ -28,6 +29,7 @@ func Load() *Config {
|
||||
TeacherBffURL: getEnv("TEACHER_BFF_URL", "http://localhost:3003"),
|
||||
OTLPEndpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
|
||||
LogLevel: getEnv("LOG_LEVEL", "info"),
|
||||
DevMode: getEnvBool("DEV_MODE", false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +48,12 @@ func getEnvInt(key string, fallback int) int {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvBool(key string, fallback bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package middleware
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -44,6 +44,15 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 开发模式旁路:DEV_MODE=true 时接受 "dev-token",注入开发用户
|
||||
// 仅用于本地联调,生产环境必须关闭 DEV_MODE
|
||||
if cfg.DevMode && tokenStr == "dev-token" {
|
||||
c.Request.Header.Set("x-user-id", "dev-user")
|
||||
c.Request.Header.Set("x-user-roles", "teacher,admin")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
|
||||
@@ -23,6 +23,8 @@ func main() {
|
||||
cfg := config.Load()
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
// 关闭尾斜杠重定向:避免 Next.js rewrites 代理时 /api/v1/classes → 301 → /api/v1/classes/ 循环
|
||||
r.RedirectTrailingSlash = false
|
||||
|
||||
// 全局中间件(按顺序注册)
|
||||
// 1. panic 恢复(最外层,捕获后续所有中间件与 handler 的 panic)
|
||||
@@ -50,25 +52,33 @@ func main() {
|
||||
api.Use(middleware.AuthMiddleware(cfg))
|
||||
{
|
||||
// classes 服务路由
|
||||
// 注:同时注册无尾斜杠与通配符两条路由。RedirectTrailingSlash=false 时,
|
||||
// Gin 不会自动把 /classes 跳到 /classes/,所以两条都要显式注册。
|
||||
classesProxy, err := proxy.NewProxy(cfg.ClassesServiceURL)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create classes proxy: %v", err)
|
||||
}
|
||||
api.Any("/classes/*path", proxy.ProxyHandler(classesProxy))
|
||||
classesHandler := proxy.ProxyHandler(classesProxy)
|
||||
api.Any("/classes", classesHandler)
|
||||
api.Any("/classes/*path", classesHandler)
|
||||
|
||||
// IAM 服务路由(身份与访问管理)
|
||||
iamProxy, err := proxy.NewProxy(cfg.IamServiceURL)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create iam proxy: %v", err)
|
||||
}
|
||||
api.Any("/iam/*path", proxy.ProxyHandler(iamProxy))
|
||||
iamHandler := proxy.ProxyHandler(iamProxy)
|
||||
api.Any("/iam", iamHandler)
|
||||
api.Any("/iam/*path", iamHandler)
|
||||
|
||||
// Teacher BFF 路由(教师聚合层)
|
||||
bffProxy, err := proxy.NewProxy(cfg.TeacherBffURL)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create teacher-bff proxy: %v", err)
|
||||
}
|
||||
api.Any("/teacher/*path", proxy.ProxyHandler(bffProxy))
|
||||
bffHandler := proxy.ProxyHandler(bffProxy)
|
||||
api.Any("/teacher", bffHandler)
|
||||
api.Any("/teacher/*path", bffHandler)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -17,19 +17,22 @@
|
||||
"@nestjs/common": "^10.4.0",
|
||||
"@nestjs/core": "^10.4.0",
|
||||
"@nestjs/platform-express": "^10.4.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
||||
"@opentelemetry/sdk-node": "^0.53.0",
|
||||
"drizzle-orm": "^0.31.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"kafkajs": "^2.2.0",
|
||||
"mysql2": "^3.11.0",
|
||||
"pino": "^9.4.0",
|
||||
"pino-http": "^10.0.0",
|
||||
"prom-client": "^15.1.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/sdk-node": "^0.53.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
||||
"zod": "^3.23.0",
|
||||
"uuid": "^10.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.0"
|
||||
"rxjs": "^7.8.0",
|
||||
"typeorm": "^1.0.0",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.0",
|
||||
@@ -41,8 +44,8 @@
|
||||
"drizzle-kit": "^0.24.0",
|
||||
"eslint": "^9.10.0",
|
||||
"pino-pretty": "^11.2.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0",
|
||||
"tsx": "^4.19.0"
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ClassesController } from './classes.controller.js';
|
||||
import { ClassesService } from './classes.service.js';
|
||||
import { ClassesRepository } from './classes.repository.js';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ClassesController } from "./classes.controller.js";
|
||||
import { ClassesService } from "./classes.service.js";
|
||||
import { ClassesRepository } from "./classes.repository.js";
|
||||
|
||||
@Module({
|
||||
controllers: [ClassesController],
|
||||
providers: [
|
||||
ClassesService,
|
||||
{
|
||||
provide: ClassesRepository,
|
||||
useFactory: () => new ClassesRepository(),
|
||||
},
|
||||
],
|
||||
providers: [ClassesService, ClassesRepository],
|
||||
})
|
||||
export class ClassesModule {}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ClassesRepository } from './classes.repository.js';
|
||||
import { ValidationError, NotFoundError } from '../shared/errors/application-error.js';
|
||||
import type { CreateClassDto, UpdateClassDto } from './classes.dto.js';
|
||||
import type { Class, NewClass } from './classes.schema.js';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Inject } from "@nestjs/common";
|
||||
import { ClassesRepository } from "./classes.repository.js";
|
||||
import {
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
} from "../shared/errors/application-error.js";
|
||||
import type { CreateClassDto, UpdateClassDto } from "./classes.dto.js";
|
||||
import type { Class, NewClass } from "./classes.schema.js";
|
||||
|
||||
export class ClassesService {
|
||||
constructor(private readonly repository: ClassesRepository) {}
|
||||
constructor(
|
||||
@Inject(ClassesRepository) private readonly repository: ClassesRepository,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateClassDto): Promise<Class> {
|
||||
const newClass: NewClass = {
|
||||
@@ -18,7 +24,7 @@ export class ClassesService {
|
||||
async getById(id: string): Promise<Class> {
|
||||
const result = await this.repository.findById(id);
|
||||
if (!result) {
|
||||
throw new NotFoundError('Class', id);
|
||||
throw new NotFoundError("Class", id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -29,11 +35,11 @@ export class ClassesService {
|
||||
|
||||
async update(id: string, dto: UpdateClassDto): Promise<Class> {
|
||||
if (Object.keys(dto).length === 0) {
|
||||
throw new ValidationError('No fields to update');
|
||||
throw new ValidationError("No fields to update");
|
||||
}
|
||||
const result = await this.repository.update(id, dto);
|
||||
if (!result) {
|
||||
throw new NotFoundError('Class', id);
|
||||
throw new NotFoundError("Class", id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -41,7 +47,7 @@ export class ClassesService {
|
||||
async delete(id: string): Promise<void> {
|
||||
const existing = await this.repository.findById(id);
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Class', id);
|
||||
throw new NotFoundError("Class", id);
|
||||
}
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { HealthController } from "./health.controller.js";
|
||||
|
||||
/**
|
||||
* 健康检查模块。
|
||||
|
||||
Reference in New Issue
Block a user