Compare commits
21 Commits
v0.1.0-p1
...
2df2237d56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df2237d56 | ||
|
|
6f68f9722e | ||
|
|
ff0ae1ac9c | ||
|
|
b39095cbdd | ||
|
|
beedbaf686 | ||
|
|
20b1afd9ab | ||
|
|
4fea3de1d1 | ||
|
|
3961a36308 | ||
|
|
a4cd970c54 | ||
|
|
75804c7d64 | ||
|
|
d831915f06 | ||
|
|
9b33303195 | ||
|
|
bc3176cb05 | ||
|
|
3fbb97791d | ||
|
|
4629de1926 | ||
|
|
f554011af5 | ||
|
|
e9ea34fe53 | ||
|
|
7474a92e3b | ||
|
|
9850bfcfd1 | ||
|
|
23246ade6d | ||
|
|
524204d30a |
80
.github/CODEOWNERS
vendored
Normal file
80
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
# Edu 平台 CODEOWNERS
|
||||
# 格式:路径 @owner
|
||||
# 作用:GitHub/Gitea 自动为 PR 分配 reviewer
|
||||
# 维护规则:
|
||||
# 1. 新增服务/包时,必须同步更新本文件
|
||||
# 2. owner 变更需开独立 PR,由架构组审批
|
||||
# 3. 与 docs/standards/git-workflow.md §4.7 保持同步
|
||||
# Team handle 占位符:@edu-platform/* 需在 Organization 中创建对应 team 后替换
|
||||
|
||||
# ===== 架构与规则(架构组,2 人 review)=====
|
||||
/.trae/ @edu-platform/arch
|
||||
/docs/architecture/ @edu-platform/arch
|
||||
/docs/standards/ @edu-platform/arch
|
||||
/scripts/arch-scan/ @edu-platform/arch
|
||||
|
||||
# ===== 根配置(架构组,2 人 review,影响全局)=====
|
||||
/.commitlintrc.js @edu-platform/arch
|
||||
/lint-staged.config.js @edu-platform/arch
|
||||
/package.json @edu-platform/arch
|
||||
/pnpm-workspace.yaml @edu-platform/arch
|
||||
/go.work @edu-platform/arch
|
||||
/pyproject.toml @edu-platform/arch
|
||||
/tsconfig.base.json @edu-platform/arch
|
||||
|
||||
# ===== 共享包(架构组,2 人 review,契约变更影响所有服务)=====
|
||||
/packages/shared-proto/ @edu-platform/arch
|
||||
/packages/shared-ts/ @edu-platform/arch
|
||||
/packages/shared-go/ @edu-platform/arch
|
||||
/packages/shared-py/ @edu-platform/arch
|
||||
/packages/shared-tokens/ @edu-platform/arch
|
||||
|
||||
# ===== 网关层(Go)=====
|
||||
/services/api-gateway/ @edu-platform/gateway
|
||||
/services/push-gateway/ @edu-platform/gateway
|
||||
|
||||
# ===== 业务微服务 =====
|
||||
# IAM(核心模块,2 人 review)
|
||||
/services/iam/ @edu-platform/iam
|
||||
# 教学核心
|
||||
/services/core-edu/ @edu-platform/edu-core
|
||||
/services/classes/ @edu-platform/edu-core
|
||||
# 内容资源
|
||||
/services/content/ @edu-platform/content
|
||||
# 消息通知
|
||||
/services/msg/ @edu-platform/messaging
|
||||
# 数据分析(Python)
|
||||
/services/data-ana/ @edu-platform/data
|
||||
# AI 网关(Python)
|
||||
/services/ai/ @edu-platform/ai
|
||||
|
||||
# ===== BFF 聚合层(NestJS)=====
|
||||
/services/teacher-bff/ @edu-platform/edu-core
|
||||
/services/student-bff/ @edu-platform/edu-core
|
||||
/services/parent-bff/ @edu-platform/edu-core
|
||||
|
||||
# ===== 微前端(Next.js)=====
|
||||
/apps/teacher-portal/ @edu-platform/frontend
|
||||
/apps/student-portal/ @edu-platform/frontend
|
||||
/apps/parent-portal/ @edu-platform/frontend
|
||||
/apps/admin-portal/ @edu-platform/frontend
|
||||
|
||||
# ===== 基础设施(SRE,2 人 review,生产环境变更强制)=====
|
||||
/infra/k8s/ @edu-platform/sre
|
||||
/infra/backup/ @edu-platform/sre
|
||||
/infra/security/ @edu-platform/sre
|
||||
/infra/monitoring/ @edu-platform/sre
|
||||
/infra/docker-compose*.yml @edu-platform/sre
|
||||
|
||||
# ===== CI/CD(SRE)=====
|
||||
/.github/ @edu-platform/sre
|
||||
/.husky/ @edu-platform/sre
|
||||
|
||||
# ===== 文档(架构组)=====
|
||||
/docs/troubleshooting/ @edu-platform/arch
|
||||
/MIGRATION_GUIDE.md @edu-platform/arch
|
||||
/README.md @edu-platform/arch
|
||||
/CHANGELOG.md @edu-platform/arch
|
||||
|
||||
# ===== 兜底(未匹配的文件由架构组 review)=====
|
||||
* @edu-platform/arch
|
||||
55
.github/pull_request_template.md
vendored
Normal file
55
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
## 变更说明
|
||||
|
||||
<!-- 简述本次变更的目的和实现方式 -->
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] feat: 新功能
|
||||
- [ ] fix: Bug 修复
|
||||
- [ ] perf: 性能优化
|
||||
- [ ] refactor: 重构
|
||||
- [ ] test: 测试
|
||||
- [ ] docs: 文档
|
||||
- [ ] build/ci: 构建/CI
|
||||
- [ ] chore: 杂项
|
||||
|
||||
## 影响范围
|
||||
|
||||
<!-- 列出受影响的服务/包 -->
|
||||
|
||||
- 服务:
|
||||
- 包:
|
||||
- 数据库迁移:是 / 否
|
||||
- protobuf 契约变更:是 / 否(如变更是否向后兼容)
|
||||
- Kafka topic 变更:是 / 否
|
||||
- 架构文档变更:是 / 否
|
||||
|
||||
## 测试情况
|
||||
|
||||
- [ ] 单元测试通过
|
||||
- [ ] 集成测试通过
|
||||
- [ ] 本地手动测试通过
|
||||
- [ ] 新增测试覆盖新功能
|
||||
|
||||
### 按语言的校验结果
|
||||
|
||||
- TS 服务:`pnpm run lint` + `pnpm run typecheck`(如缺失脚本请说明)
|
||||
- Go 服务:`go vet ./...` + `go build ./...`
|
||||
- Python 服务:`ruff check src/`
|
||||
|
||||
## 文档同步
|
||||
|
||||
- [ ] 已更新服务 README(如涉及服务结构变更)
|
||||
- [ ] 已更新架构文档(如涉及架构变更)
|
||||
- [ ] 已运行 `pnpm run arch:scan` 更新 arch.db(arch.db 不入库,但本地验证必须通过)
|
||||
- [ ] 已更新 `docs/troubleshooting/known-issues.md`(如遇到新问题/经验)
|
||||
- [ ] 已更新 `.github/CODEOWNERS`(如新增服务/包)
|
||||
|
||||
## Breaking Change
|
||||
|
||||
- [ ] 否
|
||||
- [ ] 是(请在下方说明影响和迁移路径)
|
||||
|
||||
## 关联 Issue
|
||||
|
||||
Closes #
|
||||
@@ -1 +1 @@
|
||||
npx --no-install commitlint --edit $1
|
||||
npx --no-install commitlint --edit $1
|
||||
|
||||
@@ -1 +1 @@
|
||||
npx --no-install lint-staged
|
||||
npx --no-install lint-staged
|
||||
|
||||
372
.trae/rules/project_rules.md
Normal file
372
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Edu 项目规则(微服务版)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-08
|
||||
> 适用范围:Edu 微服务架构(DDD + EDA + CQRS,多语言 monorepo)
|
||||
> 关联文档:
|
||||
>
|
||||
> - [004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md)
|
||||
> - [理想蓝图 0010](../../docs/architecture/0010_architecture.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [known-issues 速查](../../docs/troubleshooting/known-issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构图优先规则
|
||||
|
||||
**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**
|
||||
|
||||
1. **先图后码**:执行任何分析、修改、搜索任务时,首先运行 `pnpm run arch:scan` 更新 arch.db,再通过 `pnpm run arch:query` 查询目标模块、函数、依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图,最后按图索骥读取源码
|
||||
2. **图未覆盖则先补图**:如果发现项目中存在 arch.db 未记录的模块、函数、表、路由、proto 等结构,**必须先运行 `pnpm run arch:scan` 重新扫描**(多语言扫描器:TS + Go + Python + Proto),然后检查 004 是否需要补充
|
||||
3. **改码必同步图**:对源码的任何修改完成后,必须运行 `pnpm run arch:scan` 更新 arch.db;若架构设计意图有变化,同步更新 004
|
||||
|
||||
### 架构文档清单
|
||||
|
||||
| 文档 | 用途 |
|
||||
| -------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源(人类可读) |
|
||||
| `docs/architecture/0010_architecture.md` | 理想蓝图(目标态) |
|
||||
| `docs/architecture/roadmap/` | 长远规划(tech-debt / pending-features) |
|
||||
| `docs/architecture/runbooks/` | 运维手册(P6 硬化、post-p6-followup、incident-response) |
|
||||
| `docs/troubleshooting/known-issues.md` | 已知问题速查(场景→技术映射 + 工作经验日志) |
|
||||
|
||||
### 需要同步图的场景
|
||||
|
||||
- 新增/删除/重命名导出函数、组件、Hook、类型
|
||||
- 修改函数签名(参数、返回类型)
|
||||
- 修改权限点(Permissions 常量)或角色-权限映射
|
||||
- 新增/删除数据库表
|
||||
- 新增/删除路由页面、API 路由、gRPC 服务、proto message
|
||||
- 修改模块间依赖关系或服务间调用关系
|
||||
- 新增服务、新增模块、新增 proto 定义
|
||||
- 修改 Kafka topic、Outbox 事件命名
|
||||
|
||||
### 同步方式
|
||||
|
||||
- 修改源码后运行 `pnpm run arch:scan` 更新 arch.db(强制)
|
||||
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
|
||||
- 若发现新的"场景→技术"映射或工作经验,更新 `docs/troubleshooting/known-issues.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构元数据库规则(arch.db)
|
||||
|
||||
**arch.db 是代码结构唯一源,AI 工作前必须运行 `pnpm run arch:scan` 更新。**
|
||||
|
||||
1. **多语言扫描**:arch.db 覆盖 TypeScript(ts-morph)、Go(tree-sitter-go)、Python(tree-sitter-python)、Proto(buf AST)四类语言
|
||||
2. **查询命令**:
|
||||
- `pnpm run arch:query -- sql "<SQL>"` 自定义 SQL 查询
|
||||
- `pnpm run arch:query -- module-deps` 查模块依赖
|
||||
- `pnpm run arch:query -- module-reverse-deps <module>` 查反向依赖
|
||||
- `pnpm run arch:query -- symbol-refs <symbol>` 查符号引用链
|
||||
- `pnpm run arch:query -- tech-usage <tag>` 查技术使用
|
||||
- `pnpm run arch:query -- violations` 查架构违规
|
||||
- `pnpm run arch:query -- stats` 查总体统计
|
||||
- `pnpm run arch:query -- modules` 查模块列表
|
||||
3. **arch.db 不替代 004**:arch.db 是"代码现状",004 是"设计意图",两者互补
|
||||
4. **预期规模**:模块数 ≥ 10(api-gateway、push-gateway、teacher-bff、iam、classes、core-edu、content、msg、data-ana、ai),符号数 ≥ 100
|
||||
|
||||
---
|
||||
|
||||
## 3. 编码规范
|
||||
|
||||
详细规范见 `docs/standards/coding-standards.md`,以下为核心强制规则。
|
||||
|
||||
### 3.1 代码质量规则
|
||||
|
||||
- 每次修改后运行 `pnpm run lint` 和 `pnpm run typecheck` 确保零错误
|
||||
- TypeScript 服务必须用 `@RequirePermission()` 装饰器进行权限校验
|
||||
- 前端组件禁止使用 `role === "xxx"` 硬编码,统一使用 `usePermission().hasPermission()`
|
||||
- 单文件行数遵循企业级规范:
|
||||
- 配置文件、常量文件、类型定义文件、proto:无限制
|
||||
- Controller / Service:建议 ≤ 500 行
|
||||
- Repository / data-access:建议 ≤ 800 行
|
||||
- 工具函数:建议 ≤ 40 行
|
||||
- 自定义 Hook:建议 ≤ 80 行
|
||||
- 硬性上限:任何文件不超过 1000 行,超过必须拆分
|
||||
|
||||
### 3.2 架构分层规则(微服务)
|
||||
|
||||
- **四层分层**:Gateway → BFF → Services → Data
|
||||
- **依赖方向单向**:Gateway → BFF → Services → Data,禁止反向依赖
|
||||
- **服务间通信**:通过 protobuf gRPC 或 Kafka 事件,**禁止直接访问对方 DB**
|
||||
- **Gateway 职责**:JWT 校验、限流、熔断、CORS、请求 ID 注入
|
||||
- **BFF 职责**:聚合多个 Service 的数据,为前端提供场景化 API
|
||||
- **Service 职责**:业务逻辑,每服务独占 DB(DDD 限界上下文)
|
||||
|
||||
### 3.3 模块标准结构(DDD)
|
||||
|
||||
```
|
||||
services/[service]/src/
|
||||
├─ [domain]/ # 限界上下文
|
||||
│ ├─ *.controller.ts # Controller(HTTP/gRPC 入口)
|
||||
│ ├─ *.service.ts # Application Service(编排层)
|
||||
│ ├─ *.repository.ts # Repository(数据访问)
|
||||
│ ├─ *.schema.ts # Zod 验证
|
||||
│ └─ *.dto.ts # DTO
|
||||
├─ config/ # 配置(database / env / kafka / neo4j / elasticsearch)
|
||||
├─ middleware/ # 中间件(auth.middleware / permission.guard)
|
||||
├─ shared/ # 共享
|
||||
│ ├─ errors/ # 错误(application-error / global-error.filter)
|
||||
│ ├─ health/ # 健康检查
|
||||
│ ├─ lifecycle/ # 生命周期
|
||||
│ ├─ observability/ # 可观测性(logger / metrics / tracer)
|
||||
│ └─ outbox/ # Outbox(仅事件驱动服务)
|
||||
├─ app.module.ts # 根模块
|
||||
└─ main.ts # 入口
|
||||
```
|
||||
|
||||
### 3.4 TypeScript 规则
|
||||
|
||||
- **禁止 `any`**:未知类型用 `unknown` 并做类型守卫
|
||||
- **禁止 `as` 断言**(除非从 `unknown` 转换或测试中,需注释原因)
|
||||
- **函数返回值必须显式标注**,特别是 `Promise<T>`
|
||||
- **仅用于类型的导入必须使用 `import type`**
|
||||
- **可选链后禁止跟非空断言 `!`**
|
||||
- **ESM 模式**:NestJS ESM 模式下,相对 import 的 `.js` 后缀(构建产物)
|
||||
|
||||
### 3.5 Go 规则
|
||||
|
||||
- 包名小写单词,不使用下划线或驼峰
|
||||
- 导出函数必须有 doc comment(`// FuncName ...`)
|
||||
- 错误处理:`if err != nil { return err }`,禁止 `_ = err`
|
||||
- 不使用 `interface{}`,用 `any`(Go 1.18+)
|
||||
- 包内文件命名 lowercase,不使用下划线(除 `_test.go`)
|
||||
- 中间件位于 `internal/middleware/`,路由位于 `internal/routing/`
|
||||
|
||||
### 3.6 Python 规则
|
||||
|
||||
- 类型注解强制(所有函数签名、变量声明)
|
||||
- FastAPI 路由用 `APIRouter`
|
||||
- 异步优先:`async def`,IO 密集场景禁止同步阻塞调用
|
||||
- 模块名 snake_case,类名 PascalCase
|
||||
- 配置统一通过 `pydantic-settings` 管理(`config.py`)
|
||||
|
||||
### 3.7 命名规范(多语言)
|
||||
|
||||
| 对象 | 规范 | 示例 |
|
||||
| ----------- | -------------------------------------------------- | ------------------------------------------------ |
|
||||
| 目录 | kebab-case | `api-gateway/`、`core-edu/` |
|
||||
| TS 组件 | PascalCase | `UserController.ts` |
|
||||
| TS Hook | camelCase | `useAuth.ts` |
|
||||
| Go 文件 | lowercase | `circuitbreaker.go` |
|
||||
| Python 文件 | snake_case | `clickhouse_client.py` |
|
||||
| Proto 文件 | snake_case | `core_edu.proto` |
|
||||
| 变量/函数 | camelCase(TS)/ lowercase(Go)/ snake_case(Py) | `getUserById` / `getUserById` / `get_user_by_id` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 类/接口 | PascalCase,接口不加 `I` 前缀 | `UserService` |
|
||||
| 布尔值 | `is/has/can/should` 前缀 | `isActive`、`hasPermission` |
|
||||
|
||||
### 3.8 Controller / Server Action 规范
|
||||
|
||||
- 每个 Controller 方法必须用 `@RequirePermission()` 装饰器声明权限
|
||||
- 输入用 Zod / class-validator 验证,验证失败返回结构化错误
|
||||
- 返回 protobuf message 或 DTO(结构同 ActionState)
|
||||
- 错误统一走 `GlobalErrorFilter`
|
||||
- BFF Controller 聚合多个 Service 调用,禁止直接访问 DB
|
||||
|
||||
### 3.9 Tailwind 规范
|
||||
|
||||
- 使用 `cn()` 工具函数管理条件类名
|
||||
- **禁止**字符串拼接动态类名(`bg-${color}-500`)
|
||||
- **禁止**使用任意值(`w-[137px]`),除非有充分理由并注释
|
||||
- 设计令牌在 `packages/shared-tokens/`(待建立)或 `apps/teacher-portal/src/app/styles/tokens/`
|
||||
|
||||
### 3.10 设计令牌规范(强制)
|
||||
|
||||
- **禁止硬编码颜色**:TSX/TS/CSS 中不得出现 `#hex` 颜色字面量,统一使用 `hsl(var(--*))` 或 Tailwind 类 `bg-*`
|
||||
- **禁止硬编码字体**:不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量,使用 `var(--font-family-sans/serif/mono)`
|
||||
- **禁止硬编码字号**:不得出现 `font-size: Npx`,使用 `var(--font-size-1~9)`
|
||||
- **禁止 Tailwind 任意值**:不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]` 等,映射到 `--space-*` 或 Tailwind 默认阶梯
|
||||
- **三层令牌**:
|
||||
- Layer 1 Primitive:原始色板/字号/间距/阴影,业务代码不直接引用
|
||||
- Layer 2 Semantic:语义令牌(light/dark),业务代码唯一引用入口
|
||||
- Layer 3 Tailwind Theme:`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*` 类
|
||||
- **改令牌必同步图**:修改令牌定义后,同步更新 004 与 arch.db
|
||||
- **ESLint 强制约束**:
|
||||
- `no-restricted-syntax`:禁止 `#hex` 字面量
|
||||
- `design-tokens/no-hardcoded-fonts`:禁止 `'Inter'`/`'Fraunces'` 字面量
|
||||
- 白名单:`primitive.css`、`email-channel`、`manifest.ts`
|
||||
|
||||
---
|
||||
|
||||
## 4. 安全规范
|
||||
|
||||
- **JWT RS256**:IAM 签发,Gateway 公钥校验(不共享密钥)
|
||||
- **Cookie**:`httpOnly + Secure + SameSite=Strict`
|
||||
- **环境变量**:服务端变量不加 `NEXT_PUBLIC_` 前缀;前端变量必须加
|
||||
- **环境校验**:用 `@t3-oss/env-nextjs` + Zod 或 `pydantic-settings` 校验
|
||||
- **禁止 `dangerouslySetInnerHTML`**(如必须使用,先用 DOMPurify 清洗)
|
||||
- **密码哈希**:bcrypt(cost ≥ 12),禁止 MD5/SHA1
|
||||
- **限流**:Gateway 层 IP 级令牌桶,登录接口额外加用户级限流
|
||||
- **CORS**:白名单域名,禁止 `*`
|
||||
- **WAF**:见 `infra/security/waf-rules.conf`
|
||||
|
||||
---
|
||||
|
||||
## 5. 契约规范
|
||||
|
||||
- **契约先行**:服务间通信先定 `.proto`,再写实现
|
||||
- **工具链**:protobuf + buf v2
|
||||
- **buf lint**:`STANDARD` 规则集
|
||||
- **buf breaking**:`FILE` 级别检查(CI 强制)
|
||||
- **proto 文件位置**:`packages/shared-proto/proto/`
|
||||
- **包名规范**:`edu.<domain>.v1`(如 `edu.iam.v1`、`edu.core_edu.v1`)
|
||||
- **生成代码**:`buf generate`(TS 走 `buf.gen.yaml`,Go/Python 各自配置)
|
||||
- **gRPC 服务端口**:每服务独立端口,见 004 服务清单
|
||||
|
||||
---
|
||||
|
||||
## 6. 事件驱动规范
|
||||
|
||||
- **Outbox 模式**:事务内写业务表 + outbox 表,独立 publisher 投递到 Kafka(保证 at-least-once)
|
||||
- **事件命名**:`<Aggregate>.<Action>`(如 `ExamCreated`、`HomeworkSubmitted`、`GradeReleased`)
|
||||
- **TOPIC_MAP 路由**:每个服务维护 `事件 → topic` 映射(见 `shared/outbox/`)
|
||||
- **幂等性**:
|
||||
- Kafka producer:`idempotent=true` + `transactionalId`
|
||||
- Consumer:基于 `event_id` 去重(Redis SETNX 或 DB 唯一索引)
|
||||
- **CDC**:MySQL → Debezium → Kafka(可选,用于读模型同步)
|
||||
- **事件版本**:schema 演化用 `v1`/`v2` 后缀,禁止破坏性变更
|
||||
|
||||
---
|
||||
|
||||
## 7. 提交规范
|
||||
|
||||
- 使用 Conventional Commits 格式:`feat(scope): description`
|
||||
- **类型**:`feat`、`fix`、`chore`、`docs`、`style`、`refactor`、`test`、`perf`、`ci`
|
||||
- **scope**(服务名或包名):
|
||||
- 服务:`iam`、`classes`、`core-edu`、`content`、`msg`、`ai`、`data-ana`、`api-gateway`、`push-gateway`、`teacher-bff`
|
||||
- 包:`shared-proto`、`shared-ts`、`shared-go`、`shared-py`、`shared-tokens`
|
||||
- 基础设施:`infra`、`k8s`、`helm`、`ci`
|
||||
- 文档:`docs`
|
||||
- **提交前必须运行**:`pnpm run lint` + `pnpm run typecheck`(TS);`go vet ./...`(Go);`ruff check src/`(Python)
|
||||
- **commitlint + husky**:强制校验(见 `.commitlintrc.js`、`.husky/commit-msg`)
|
||||
|
||||
---
|
||||
|
||||
## 8. Git 工作流
|
||||
|
||||
- **分支策略**:trunk-based(直接提交 main 分支,小项目)
|
||||
- **大型团队**:feature branch + PR,PR 至少 1 人 review
|
||||
- **pre-commit hook**:`lint-staged` 自动修复 + 校验(见 `lint-staged.config.js`)
|
||||
- **commit-msg hook**:commitlint 校验格式
|
||||
- **禁止 force push**:除非显式要求并通知团队
|
||||
|
||||
---
|
||||
|
||||
## 9. 问题记录规则
|
||||
|
||||
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`(索引式速查手册)。**
|
||||
|
||||
### 必须记录的场景
|
||||
|
||||
| 场景 | 记录要求 |
|
||||
| -------------------------------------------------- | --------------------------------- |
|
||||
| 构建报错(dev/build/lint/tsc/go vet/ruff) | 记录到"全局经验"对应主题分区 |
|
||||
| 运行时异常(白屏/API 报错/数据加载失败/gRPC 错误) | 记录到"模块经验"对应模块分区 |
|
||||
| 框架/库版本兼容问题 | 记录到"全局经验: 框架与运行时" |
|
||||
| 依赖配置问题(go mod / uv / pnpm workspace) | 记录到"全局经验: 依赖管理" |
|
||||
| 架构约束违规(跨服务访问 DB / 缺契约) | 记录到"全局经验: 架构约束" |
|
||||
| 多语言 monorepo 问题(共享包链接失败等) | 记录到"全局经验: 多语言 monorepo" |
|
||||
|
||||
### 记录格式
|
||||
|
||||
索引式表格,指明"场景→技术/规则"映射,不写多行代码示例:
|
||||
|
||||
```markdown
|
||||
### X.X 主题分区
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| -------- | ------------------ |
|
||||
| 简述场景 | 正确做法(一句话) |
|
||||
```
|
||||
|
||||
### 记录要求
|
||||
|
||||
- **索引式**:场景→技术/规则映射,不写代码示例和错误示范列
|
||||
- **去重**:同类问题在原条目补充,不重复创建
|
||||
- **引用架构规则**:架构分层、模块结构等规则引用 004 和本规则文件,不重复
|
||||
- **工作经验日志**:在"工作经验日志"区按时间倒序追加(50 条上限),记录"做了什么/学到什么/下次注意"
|
||||
|
||||
---
|
||||
|
||||
## 10. AI 工作强制流程
|
||||
|
||||
**所有 AI 工作必须遵循此流程,违反即违规。**
|
||||
|
||||
### 阶段 1:上下文加载
|
||||
|
||||
1. `pnpm run arch:scan` 更新 arch.db
|
||||
2. `pnpm run arch:query -- module-deps` 查目标模块依赖
|
||||
3. `pnpm run arch:query -- symbol-refs <目标函数>` 查调用链
|
||||
4. 阅读 `services/[service]/README.md` 读模块工作流程
|
||||
5. 查 `docs/troubleshooting/known-issues.md` "模块经验"分区读相关经验
|
||||
6. 查 `docs/architecture/004_architecture_impact_map.md` 对应章节
|
||||
|
||||
### 阶段 2:执行工作
|
||||
|
||||
1. 按规划执行(契约先行:先 proto,再实现)
|
||||
2. 修改代码后立即运行 `pnpm run arch:scan` 更新 arch.db
|
||||
3. 运行质量校验确保零错误:
|
||||
- TS:`pnpm run lint` + `pnpm run typecheck`
|
||||
- Go:`go vet ./...` + `go build ./...`
|
||||
- Python:`ruff check src/`
|
||||
|
||||
### 阶段 3:经验沉淀(强制,不可跳过)
|
||||
|
||||
1. 在 `docs/troubleshooting/known-issues.md` "工作经验日志"区追加一条记录:
|
||||
- 日期 + 时间
|
||||
- 模块
|
||||
- 做了什么 + 学到什么
|
||||
2. 若发现新的"场景→技术"映射 → 提炼到对应模块分区
|
||||
3. 若发现新的架构决策 → 更新 004
|
||||
4. 若代码结构变化 → `pnpm run arch:scan` 确认 arch.db 已更新
|
||||
|
||||
---
|
||||
|
||||
## 11. 多语言 monorepo 规则
|
||||
|
||||
- **包管理器分工**:
|
||||
- TypeScript:pnpm workspace(`pnpm-workspace.yaml`)
|
||||
- Go:go.work(`go.work`)
|
||||
- Python:uv workspace(根 `pyproject.toml` 的 `[tool.uv.workspace]`)
|
||||
- **共享包**(`packages/`):
|
||||
- `shared-proto`:protobuf 契约定义(跨语言共享)
|
||||
- `shared-ts`:TypeScript 共享类型与工具(待建立)
|
||||
- `shared-go`:Go 共享工具(待建立)
|
||||
- `shared-py`:Python 共享工具(待建立)
|
||||
- `shared-tokens`:设计令牌(待建立)
|
||||
- **跨语言契约**:通过 protobuf,禁止 JSON Schema 或 OpenAPI 作为内部契约
|
||||
- **依赖版本对齐**:共享依赖(如 uuid、jwt)在多语言版本号保持一致
|
||||
|
||||
---
|
||||
|
||||
## 12. 可观测性规范
|
||||
|
||||
- **三支柱必须同时启用**(所有服务):
|
||||
- **日志**:pino(TS)/ zap(Go)/ structlog(Python)—— 结构化 JSON
|
||||
- **指标**:prom-client(TS)/ prometheus(Go)/ prometheus-client(Python)—— `/metrics` 端点
|
||||
- **链路**:OpenTelemetry SDK + OTLP exporter —— 统一 traceparent
|
||||
- **健康检查**:`/health`(liveness)+ `/ready`(readiness),见各服务 `shared/health/`
|
||||
- **请求 ID**:Gateway 注入 `X-Request-Id`,全链路传递(日志、metrics、trace)
|
||||
- **指标命名**:`<service>_<module>_<operation>_<unit>`(如 `iam_login_duration_seconds`)
|
||||
- **告警规则**:见 `infra/prometheus/rules.yml` + `infra/alertmanager/alertmanager.yml`
|
||||
- **Grafana 面板**:见 `infra/grafana/dashboards/`
|
||||
|
||||
---
|
||||
|
||||
## 13. 部署与基础设施规范
|
||||
|
||||
- **容器化**:每服务独立 Dockerfile(见 `services/[service]/Dockerfile`)
|
||||
- **K8s**:基础设施 manifest 位于 `infra/k8s/`,演化目标为 Helm chart(见 `infra/k8s/helm/`)
|
||||
- **命名空间**:`edu-system`、`edu-monitoring`、`edu-chaos`、`edu-backup`
|
||||
- **配置分离**:
|
||||
- ConfigMap:非敏感配置(环境变量、特性开关)
|
||||
- Secret:敏感配置(DB 密码、JWT 密钥、API key)—— 见 `infra/security/secrets.example.env`
|
||||
- **备份**:见 `infra/backup/backup-mysql.sh`,每服务独立备份,保留 7 天
|
||||
- **监控栈**:Prometheus + Grafana + Alertmanager(见 `infra/docker-compose.monitoring.yml`)
|
||||
- **混沌工程**:见 `infra/chaos/experiments.yaml`
|
||||
|
||||
---
|
||||
|
||||
**本规则文件是项目的强制约束,所有 contributor(含 AI)必须遵守。规则变更需同步更新 004 与 arch.db。**
|
||||
@@ -5,7 +5,8 @@
|
||||
> 状态:基线发布
|
||||
> 维护者:架构组
|
||||
> 关联文档:
|
||||
> - [项目规则](./project_rules.md)
|
||||
>
|
||||
> - [项目规则](./.trae/rules/project_rules.md)
|
||||
> - [编码规范](./docs/standards/coding-standards.md)
|
||||
> - [Git 工作流](./docs/standards/git-workflow.md)
|
||||
|
||||
@@ -31,7 +32,7 @@
|
||||
原始项目 CICD 是基于 Next.js 16 的单应用架构,承载 K12 智慧教务系统 35 个业务模块。随着业务规模扩张与团队增长,单应用架构在以下方面暴露瓶颈:
|
||||
|
||||
| 维度 | 单应用瓶颈 | 微服务目标 |
|
||||
|------|-----------|-----------|
|
||||
| -------- | ---------------------------------- | ------------------------------------------------- |
|
||||
| 团队协作 | 35 模块挤在同一仓库,合并冲突频繁 | 按领域拆分,6 领域团队独立迭代 |
|
||||
| 部署节奏 | 全量构建发布,单模块变更牵动全站 | 服务粒度独立部署,故障爆炸半径缩小 |
|
||||
| 技术选型 | TypeScript 单语言,AI/分析场景受限 | TS(业务)+ Go(网关)+ Python(AI/分析)各取所长 |
|
||||
@@ -53,7 +54,7 @@
|
||||
### 2.1 基本概况
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| ---------- | -------------------------------------------------- |
|
||||
| 仓库路径 | `e:\Desktop\CICD` |
|
||||
| 技术栈 | Next.js 16 + React 19 + Tailwind v4 + Drizzle ORM |
|
||||
| 语言 | TypeScript(全栈) |
|
||||
@@ -66,7 +67,7 @@
|
||||
### 2.2 模块清单(按领域归类)
|
||||
|
||||
| 领域 | 模块 |
|
||||
|------|------|
|
||||
| ---------- | ------------------------------------------------------ |
|
||||
| 身份与权限 | auth、users、onboarding、settings、permissions |
|
||||
| 教学组织 | classes、teachers、students、parents、subjects |
|
||||
| 教学核心 | courses、lessons、schedule、attendance、leave-requests |
|
||||
@@ -95,7 +96,7 @@
|
||||
### 3.1 基本概况
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| ------------- | ------------------------------------------------------------------ |
|
||||
| 仓库路径 | `e:\Desktop\Edu` |
|
||||
| 远程仓库 | https://git.eazygame.cn/xiner/Edu.git |
|
||||
| 架构范式 | DDD + EDA + CQRS 微服务 |
|
||||
@@ -165,14 +166,14 @@ flowchart TB
|
||||
### 3.3 微前端与领域服务映射
|
||||
|
||||
| 微前端 | 路由前缀 | 对接 BFF | 主要消费的服务 |
|
||||
|--------|---------|---------|---------------|
|
||||
| ------------- | ---------- | ------------------ | ----------------------- |
|
||||
| Admin Shell | `/admin` | Admin BFF | identity、org、insight |
|
||||
| Teacher Shell | `/teacher` | Teacher BFF | teaching、content、comm |
|
||||
| Student Shell | `/student` | Student/Parent BFF | teaching、content |
|
||||
| Parent Shell | `/parent` | Student/Parent BFF | teaching、comm |
|
||||
|
||||
| 微服务 | 原始模块映射 | 主存储 | 对外契约 |
|
||||
|--------|------------|--------|---------|
|
||||
| ------------------------ | ----------------------------------------------------------------------- | --------------------- | ------------------ |
|
||||
| identity | auth、users、onboarding、settings、permissions | MySQL + Redis | identity.proto |
|
||||
| org | classes、teachers、students、parents、subjects | MySQL | org.proto |
|
||||
| teaching | courses、lessons、schedule、attendance、leave-requests、exams、homework | MySQL | teaching.proto |
|
||||
@@ -191,7 +192,7 @@ flowchart TB
|
||||
### 4.1 规范类资产
|
||||
|
||||
| 资产 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| -------------------------------- | -------- | --------------------------------------------------------------------------- |
|
||||
| 项目规则(project_rules) | 调整 | 架构图优先保留,分层规则从三层改为微服务分层,新增 DDD/契约/事件驱动规则 |
|
||||
| 编码规范(coding-standards) | 调整 | TS 部分保留并补充 NestJS 装饰器/DI 规则,新增 Go、Python、protobuf 章节 |
|
||||
| Git 工作流 | 调整 | trunk-based 替代分支策略,scope 改为服务/包名,新增多语言 monorepo 提交规则 |
|
||||
@@ -204,7 +205,7 @@ flowchart TB
|
||||
### 4.2 代码类资产
|
||||
|
||||
| 资产 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| ------------------------------ | -------- | ------------------------------------------------------------- |
|
||||
| Zod schema 定义 | 直接迁移 | 各模块 schema.ts 平移至对应微服务,复用验证规则 |
|
||||
| 权限点常量(Permissions) | 直接迁移 | 集中迁入 identity 服务共享包 |
|
||||
| Drizzle schema(表结构) | 调整 | 按领域拆分到各微服务独占库,关系型字段保持不变 |
|
||||
@@ -221,7 +222,7 @@ flowchart TB
|
||||
### 4.3 文档类资产
|
||||
|
||||
| 资产 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| ------------------- | -------- | -------------------------------------- |
|
||||
| 架构影响地图(004) | 重写 | 从单应用模块图改为微服务限界上下文图 |
|
||||
| K12 功能清单(006) | 直接迁移 | 功能清单与架构无关,直接平移 |
|
||||
| 差距审计报告(007) | 调整 | 重新审计各微服务的功能完成度 |
|
||||
@@ -235,7 +236,7 @@ flowchart TB
|
||||
## 五、文档体系映射表
|
||||
|
||||
| CICD 文档 | Edu 对应文档 | 关系 |
|
||||
|-----------|------------|------|
|
||||
| -------------------------------------------------- | ------------------------------------------------ | ------------------ |
|
||||
| `.trae/rules/project_rules.md` | `project_rules.md` | 调整(微服务版) |
|
||||
| `docs/standards/coding-standards.md` | `docs/standards/coding-standards.md` | 调整(多语言版) |
|
||||
| —(散落在 project_rules) | `docs/standards/git-workflow.md` | 新建 |
|
||||
@@ -284,7 +285,7 @@ gantt
|
||||
### 6.2 各阶段目标
|
||||
|
||||
| 阶段 | 名称 | 目标 | 关键交付物 | 验收信号 |
|
||||
|------|------|------|-----------|---------|
|
||||
| ---- | -------- | ----------------------------------------- | -------------------------------------------------- | ------------------------------- |
|
||||
| P1 | 地基 | 仓库骨架、契约工具链、CI、可观测平台 | monorepo 结构、buf 配置、Kafka 集群、OpenTelemetry | 契约生成 + 一次端到端 trace |
|
||||
| P2 | 身份 | identity + auth + notification 三服务打通 | JWT 颁发、RBAC、邮件/短信/站内通知 | 用户注册→登录→收到通知 |
|
||||
| P3 | 核心教学 | org + teaching + content | 班级/课表/作业/题库/考试 | 教师创建作业→学生提交→批改闭环 |
|
||||
@@ -311,7 +312,7 @@ gantt
|
||||
**复用方式**:将 CICD 的 `src/app/styles/tokens/` 五层分层模型平移至微前端共享包。
|
||||
|
||||
| CICD 位置 | Edu 位置 | 调整 |
|
||||
|-----------|---------|------|
|
||||
| ---------------------------------------------- | ------------------------------------------- | -------- |
|
||||
| `src/app/styles/tokens/primitive.css` | `packages/ui-tokens/primitive.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/semantic-light.css` | `packages/ui-tokens/semantic-light.css` | 直接平移 |
|
||||
| `src/app/styles/tokens/semantic-dark.css` | `packages/ui-tokens/semantic-dark.css` | 直接平移 |
|
||||
@@ -319,6 +320,7 @@ gantt
|
||||
| `src/app/styles/tokens/tailwind-theme.css` | `packages/ui-tokens/tailwind-theme.css` | 直接平移 |
|
||||
|
||||
**强制规则保持不变**:
|
||||
|
||||
- 禁止硬编码颜色(`#hex`)
|
||||
- 禁止硬编码字体(`'Inter'`/`'Fraunces'`/`'JetBrains Mono'`)
|
||||
- 禁止硬编码字号(`font-size: Npx`)
|
||||
@@ -330,13 +332,14 @@ gantt
|
||||
**复用方式**:将 CICD 的 `src/shared/components/ui/` 平移至 `packages/ui-components/`,作为 Module Federation 共享依赖。
|
||||
|
||||
| 组件类别 | CICD 路径 | Edu 路径 | 复用要点 |
|
||||
|---------|----------|---------|---------|
|
||||
| ------------------------------- | ------------------------- | -------------------------------- | ------------------------------------------------------------ |
|
||||
| 基础组件(Button/Input/Dialog) | `shared/components/ui/` | `packages/ui-components/` | 全部平移,保持 PascalCase 命名 |
|
||||
| A11y 组件 | `shared/components/a11y/` | `packages/ui-components/a11y/` | skip-link、visually-hidden、focus-trap、aria-status 全部平移 |
|
||||
| 图表组件 | 各模块内 | `packages/ui-components/charts/` | 收集 recharts 封装,统一暴露 |
|
||||
| 表单组件 | react-hook-form 封装 | `packages/ui-components/form/` | 与 zod resolver 一同平移 |
|
||||
|
||||
**复用规则**:
|
||||
|
||||
- 组件必须为纯函数,使用 `function` 声明
|
||||
- 不使用 `React.FC`,直接用函数声明 + 显式标注 props 类型
|
||||
- 默认服务端组件(微前端 host),需要交互时才添加 `"use client"`
|
||||
@@ -347,13 +350,14 @@ gantt
|
||||
**复用方式**:将 CICD 的 `requirePermission()` + `usePermission().hasPermission()` 模式平移至 identity 服务 + auth 基础设施服务。
|
||||
|
||||
| CICD 资产 | Edu 位置 | 调整 |
|
||||
|-----------|---------|------|
|
||||
| ----------------------------------------------- | -------------------------------------------------------------- | -------------------------------- |
|
||||
| `shared/lib/auth-guard.ts`(requirePermission) | `services/auth/src/guards/permission.guard.ts`(NestJS Guard) | 改为 NestJS Guard 装饰器 |
|
||||
| `shared/types/permissions.ts`(权限点常量) | `packages/contracts/src/permissions.ts` | 集中到 contracts 包,多服务共享 |
|
||||
| `usePermission` Hook | `packages/ui-components/hooks/use-permission.ts` | 通过 BFF 拉取权限,Hook 接口不变 |
|
||||
| 角色权限矩阵(008) | `docs/architecture/role_mapping.md` | 直接平移 |
|
||||
|
||||
**强制规则保持不变**:
|
||||
|
||||
- 每个 Controller/Action 必须调用 `requirePermission()` 等价物
|
||||
- 前端组件禁止使用 `role === "xxx"` 硬编码,统一使用 `usePermission().hasPermission()`
|
||||
|
||||
@@ -362,7 +366,7 @@ gantt
|
||||
**复用方式**:CICD 的 `cacheFn`(React `cache()` + 自定义缓存层)改为 NestJS Cache 模块 + Redis,**权限数据不跨请求缓存**的规则沿用。
|
||||
|
||||
| CICD 模式 | Edu 模式 | 备注 |
|
||||
|-----------|---------|------|
|
||||
| ------------------------ | --------------------------------------------------- | ------------------------------------- |
|
||||
| `cacheFn`(React cache) | NestJS `@UseInterceptors(CacheInterceptor)` + Redis | 单请求内缓存改为 NestJS REQUEST scope |
|
||||
| `unstable_cache` | 禁用 | CICD 已禁用,Edu 沿用禁用决策 |
|
||||
| 权限数据缓存 | 仅在 auth 服务内部缓存,TTL ≤ 60s | 跨服务不缓存权限 |
|
||||
@@ -373,7 +377,7 @@ gantt
|
||||
**复用方式**:CICD 的 Server Action 编排模式(权限 + Zod 验证 + 调用 data-access + revalidate)平移为 NestJS Application Service 编排模式。
|
||||
|
||||
| CICD Server Action 步骤 | NestJS Application Service 对应 |
|
||||
|------------------------|-------------------------------|
|
||||
| ------------------------- | ------------------------------------------- |
|
||||
| `requirePermission(perm)` | `@RequirePermission(perm)` 装饰器 + Guard |
|
||||
| Zod `safeParse` | `ValidationPipe` + DTO class-validator |
|
||||
| 调用 `data-access` | 调用 Domain Service / Repository |
|
||||
@@ -385,7 +389,7 @@ gantt
|
||||
**复用方式**:CICD 的 5 层状态模型平移至微前端 host 应用。
|
||||
|
||||
| 层级 | CICD 方案 | Edu 方案 | 备注 |
|
||||
|------|----------|---------|------|
|
||||
| ------------------ | ----------------------------- | ----------------------------- | -------- |
|
||||
| L1 URL | nuqs | nuqs | 直接平移 |
|
||||
| L2 Server | TanStack Query | TanStack Query | 直接平移 |
|
||||
| L3 Client Business | Zustand slice | Zustand slice | 直接平移 |
|
||||
@@ -410,7 +414,7 @@ gantt
|
||||
**复用方式**:平移至 notification 服务,日志结构保持。
|
||||
|
||||
| CICD 资产 | Edu 位置 | 备注 |
|
||||
|-----------|---------|------|
|
||||
| ----------------------------- | ---------------------------------------------------- | ---------------------------- |
|
||||
| `shared/lib/login-logger.ts` | `services/notification/src/loggers/login-logger.ts` | 登录尝试日志 |
|
||||
| `shared/lib/change-logger.ts` | `services/notification/src/loggers/change-logger.ts` | 数据变更日志(监听领域事件) |
|
||||
| `shared/lib/audit-logger.ts` | `services/notification/src/loggers/audit-logger.ts` | 关键业务操作日志 |
|
||||
@@ -420,12 +424,13 @@ gantt
|
||||
**复用方式**:三套工作流模式沿用(CI + 安全扫描 + 灾备演练),但执行方式改为多服务并行。
|
||||
|
||||
| CICD 工作流 | Edu 工作流 | 调整 |
|
||||
|------------|-----------|------|
|
||||
| -------------- | ------------------------------- | ------------------------------------ |
|
||||
| `ci.yml` | `.gitea/workflows/ci.yml` | 单体改为矩阵并行(每个服务一个 job) |
|
||||
| `security.yml` | `.gitea/workflows/security.yml` | 新增 Trivy 扫描 Docker 镜像 |
|
||||
| `dr-drill.yml` | `.gitea/workflows/dr-drill.yml` | 灾备演练改为多服务恢复顺序演练 |
|
||||
|
||||
**CI 必须包含**(沿用 CICD 规则):
|
||||
|
||||
1. 安装依赖(多语言:pnpm install / go mod download / uv sync)
|
||||
2. Lint 检查(ESLint + golangci-lint + ruff)
|
||||
3. 类型检查(tsc --noEmit + go vet + mypy)
|
||||
@@ -479,7 +484,7 @@ gantt
|
||||
## 附录:迁移过程中的关键决策点
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| ------------ | ------------------------------ | ------------------------------------------------ |
|
||||
| 服务拆分粒度 | 6 业务 + 2 基础设施 | 平衡团队规模与拆分收益,避免过细导致 RPC 开销 |
|
||||
| 通信协议 | gRPC(内部)+ REST(BFF 对外) | 内部高性能,外部兼容性 |
|
||||
| 事件总线 | Kafka + Debezium CDC | CDC 减少业务代码侵入,Outbox 模式保证一致性 |
|
||||
|
||||
@@ -39,7 +39,7 @@ pnpm dev
|
||||
- [UI 设计系统](docs/standards/ui-design-system.md)
|
||||
- [Git 工作流](docs/standards/git-workflow.md)
|
||||
- [已知问题](docs/troubleshooting/known-issues.md)
|
||||
- [项目规则](project_rules.md)
|
||||
- [项目规则](.trae/rules/project_rules.md)
|
||||
- [迁移指南](MIGRATION_GUIDE.md)
|
||||
|
||||
## 开发阶段
|
||||
|
||||
3
apps/teacher-portal/next-env.d.ts
vendored
3
apps/teacher-portal/next-env.d.ts
vendored
@@ -1,2 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
@@ -2,16 +2,35 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "src/**/*", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"src/**/*",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
107
docs/architecture/004-p6-addendum.md
Normal file
107
docs/architecture/004-p6-addendum.md
Normal file
@@ -0,0 +1,107 @@
|
||||
## 15. P6 生产硬化(补记)
|
||||
|
||||
> 本章为 `004_architecture_impact_map.md` 的 P6 阶段补记,记录生产硬化引入的横切关注点与基础设施栈。
|
||||
> 维护规则与正文一致:源码变更后同步 `npm run arch:scan` 更新 arch.db。
|
||||
|
||||
### 15.1 横切关注点矩阵
|
||||
|
||||
| 关注点 | NestJS 服务 | Python 服务 | Go 网关 | 实现位置 |
|
||||
|--------|-------------|-------------|---------|----------|
|
||||
| 健康检查 | HealthController | health.py | /healthz | shared/health, src/health |
|
||||
| 优雅停机 | LifecycleService | FastAPI lifespan | enableShutdownHooks | shared/lifecycle |
|
||||
| 熔断 | 经 Gateway | 经 Gateway | gobreaker v2 | api-gateway/middleware |
|
||||
| 限流 | 经 Gateway | 经 Gateway | token bucket | api-gateway/middleware |
|
||||
| 链路追踪 | tracer.ts | OpenTelemetry | OpenTelemetry | shared/observability |
|
||||
| 指标 | metrics.ts | prometheus_fastapi | prometheus | shared/observability |
|
||||
| 日志 | logger.ts | structlog | zap | shared/observability |
|
||||
| 错误处理 | global-error.filter | exception handler | middleware | shared/errors |
|
||||
|
||||
### 15.2 API Gateway 中间件链
|
||||
|
||||
请求流经顺序(出向到下游服务):
|
||||
|
||||
```
|
||||
请求入口
|
||||
→ WAF(规则匹配)
|
||||
→ CORS
|
||||
→ 限流(token bucket,按 route+tenant)
|
||||
→ 熔断(gobreaker v2,按下游服务)
|
||||
→ 重试(指数退避,仅幂等)
|
||||
→ 链路追踪注入
|
||||
→ 转发到下游
|
||||
→ 响应 → 指标记录 → 返回
|
||||
```
|
||||
|
||||
### 15.3 可观测性栈
|
||||
|
||||
```
|
||||
应用层(NestJS / Python / Go)
|
||||
→ OpenTelemetry SDK(trace + metrics)
|
||||
→ OTLP exporter
|
||||
→ 采集层
|
||||
├─ Prometheus(metrics)
|
||||
├─ Tempo / Jaeger(trace)
|
||||
└─ Loki / ELK(log)
|
||||
→ 展示层
|
||||
├─ Grafana(仪表盘)
|
||||
└─ Alertmanager(告警路由)
|
||||
```
|
||||
|
||||
关键指标命名约定:
|
||||
- `http_request_duration_seconds`(histogram,含 service/route/status 维度)
|
||||
- `circuit_breaker_state`(gauge,0=Closed / 1=Open / 2=HalfOpen)
|
||||
- `rate_limiter_rejected_total`(counter)
|
||||
- `db_connections_in_use`(gauge)
|
||||
- `kafka_consumer_lag`(gauge)
|
||||
|
||||
### 15.4 安全栈
|
||||
|
||||
| 层 | 机制 | 配置位置 |
|
||||
|----|------|----------|
|
||||
| 边缘 | WAF + DDoS 防护 | Cloudflare / 入口 LB |
|
||||
| 网关 | JWT 校验 + 限流 + CORS | api-gateway |
|
||||
| 服务 | requirePermission 权限点 | modules/*/actions |
|
||||
| 数据 | 字段加密 + 审计日志 | data-access |
|
||||
| 密钥 | KMS + K8s Secret + 轮换 | deploy/k8s/secrets |
|
||||
| 传输 | mTLS(服务间,可选)+ TLS(边缘) | mesh / ingress |
|
||||
|
||||
### 15.5 健康检查约定
|
||||
|
||||
- `GET /healthz`:liveness,仅返回进程存活,不检查依赖,避免滚动重启雪崩
|
||||
- `GET /readyz`:readiness,检查 DB 等关键依赖,失败返回 503
|
||||
- K8s 探针:livenessProbe → /healthz,readinessProbe → /readyz
|
||||
- Python 服务 readyz 简化为 ok + TODO,待依赖客户端就绪后补全
|
||||
- 无需鉴权,必须在路由白名单中放行
|
||||
|
||||
### 15.6 优雅停机约定
|
||||
|
||||
- NestJS:`app.enableShutdownHooks()` 注册 SIGTERM/SIGINT 钩子
|
||||
- LifecycleService 实现 OnApplicationShutdown,按序关闭:Kafka producer → Redis → DataSource
|
||||
- K8s:`terminationGracePeriodSeconds=60`,preStop hook 可加 sleep 5s 摘流量
|
||||
- Python:FastAPI lifespan shutdown 事件,关闭连接池
|
||||
- 销毁顺序理由:先停外部消息生产(避免新事件),再关缓存,最后关 DB
|
||||
|
||||
### 15.7 灾难恢复策略
|
||||
|
||||
- 备份:CronJob 每 15min,PostgreSQL + Redis + Kafka offset
|
||||
- 恢复:`scripts/restore/`,月度演练验证 RTO
|
||||
- 多 AZ:Pod 反亲和 + DB 同步复制 + Redis 哨兵 + Kafka ISR=2
|
||||
- DNS 切换:区域级故障,TTL=60s,季度演练
|
||||
|
||||
### 15.8 与正文章节的对应
|
||||
|
||||
| 本章小节 | 对应正文章节 |
|
||||
|----------|--------------|
|
||||
| 横切关注点 | 第 3 章 共享内核 |
|
||||
| 中间件链 | 第 5 章 API Gateway |
|
||||
| 可观测性 | 第 10 章 可观测性 |
|
||||
| 安全栈 | 第 11 章 安全 |
|
||||
| 健康检查 | 第 6 章 服务边界 |
|
||||
| 灾难恢复 | 第 12 章 部署与运维 |
|
||||
|
||||
### 15.9 同步要求
|
||||
|
||||
新增导出符号需在落地到 Edu 仓库后运行 `npm run arch:scan` 更新 arch.db:
|
||||
- `HealthController`、`HealthModule`(5 个 NestJS 服务)
|
||||
- `LifecycleService`(5 个 NestJS 服务)
|
||||
- `health.py` router(ai、data-ana)
|
||||
@@ -5,8 +5,9 @@
|
||||
> 状态:基线发布
|
||||
> 适用范围:Edu 微服务架构(DDD + EDA + CQRS)
|
||||
> 关联文档:
|
||||
>
|
||||
> - [理想蓝图](./0010_architecture.md)
|
||||
> - [项目规则](../../project_rules.md)
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
> - [路线图](./roadmap/README.md)
|
||||
|
||||
---
|
||||
@@ -32,22 +33,25 @@
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 系统边界
|
||||
### 1.1a 技术分层视角(系统边界)
|
||||
|
||||
> 本图展示**部署分层结构**(自上而下:用户 → 微前端 → 网关 → BFF → 业务服务 → 总线 → 数据)。
|
||||
> 用户层按"使用场景域"标注,BFF 层按场景域分(不是按角色分)。业务领域视角见 [1.1b](#11b-业务领域视角)。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Users["用户层"]
|
||||
Teacher[教师]
|
||||
Student[学生]
|
||||
Parent[家长]
|
||||
Admin[管理员]
|
||||
subgraph Users["用户层(场景域用户)"]
|
||||
Teacher["教学场景域用户<br/>(教师 / 教导主任 / 教研组长 共用)"]
|
||||
Student["学习场景域用户<br/>(学生)"]
|
||||
Parent["家长场景域用户<br/>(家长)"]
|
||||
Admin["管理场景域用户<br/>(系统管理员 / 校管理员)"]
|
||||
end
|
||||
|
||||
subgraph MFE["微前端层(Module Federation)"]
|
||||
TeacherPortal[teacher-portal]
|
||||
StudentPortal[student-portal]
|
||||
ParentPortal[parent-portal]
|
||||
AdminPortal[admin-portal]
|
||||
TeacherPortal["teacher-portal<br/>教学场景域前端"]
|
||||
StudentPortal["student-portal<br/>学习场景域前端"]
|
||||
ParentPortal["parent-portal<br/>家长场景域前端"]
|
||||
AdminPortal["admin-portal<br/>管理场景域前端"]
|
||||
end
|
||||
|
||||
subgraph Gateway["网关层(Go)"]
|
||||
@@ -55,10 +59,10 @@ graph TB
|
||||
PushGateway[push-gateway<br/>WebSocket/SSE]
|
||||
end
|
||||
|
||||
subgraph BFF["BFF 聚合层(NestJS)"]
|
||||
TeacherBFF[teacher-bff<br/>GraphQL]
|
||||
StudentBFF[student-bff<br/>GraphQL]
|
||||
ParentBFF[parent-bff<br/>GraphQL]
|
||||
subgraph BFF["BFF 聚合层(NestJS)<br/>按使用场景域分 BFF(不是按角色分)"]
|
||||
TeacherBFF["teacher-bff<br/>教学场景域聚合"]
|
||||
StudentBFF["student-bff<br/>学习场景域聚合"]
|
||||
ParentBFF["parent-bff<br/>家长场景域聚合"]
|
||||
end
|
||||
|
||||
subgraph Services["业务微服务(NestJS + FastAPI)"]
|
||||
@@ -128,34 +132,90 @@ graph TB
|
||||
CoreEdu --> Redis
|
||||
```
|
||||
|
||||
### 1.1b 业务领域视角
|
||||
|
||||
> 本图按 **DDD 限界上下文**展示 6 个业务领域及其依赖关系。同一服务可横跨多个领域(如 core-edu 同时承载"教学组织"与"教学核心")。
|
||||
> 技术分层视角见 [1.1a](#11a-技术分层视角系统边界)。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph D1["D1 身份认证领域"]
|
||||
IAM[iam 服务]
|
||||
IAM_M[users / roles / permissions<br/>refresh_tokens / sessions]
|
||||
end
|
||||
|
||||
subgraph D2["D2 教学组织领域"]
|
||||
ORG[core-edu 服务<br/>classes 模块]
|
||||
ORG_M[classes / subjects / enrollment]
|
||||
end
|
||||
|
||||
subgraph D3["D3 教学核心领域"]
|
||||
TEACH[core-edu 服务<br/>exams/homework/grades]
|
||||
TEACH_M[exams / homework / grades<br/>courses / lessons / schedule / attendance]
|
||||
end
|
||||
|
||||
subgraph D4["D4 内容资源领域"]
|
||||
CONTENT[content 服务]
|
||||
CONTENT_M[textbooks / knowledge-points<br/>questions / grading / search(ES)]
|
||||
end
|
||||
|
||||
subgraph D5["D5 沟通通知领域"]
|
||||
MSG[msg 服务]
|
||||
MSG_M[messaging / notifications / announcements]
|
||||
end
|
||||
|
||||
subgraph D6["D6 智能洞察领域"]
|
||||
DATA[data-ana 服务]
|
||||
AI[ai 服务]
|
||||
DATA_M[analytics / dashboard / diagnostic(ClickHouse)]
|
||||
AI_M[ai 备课/出题/分析 / search]
|
||||
end
|
||||
|
||||
D1 --> D2
|
||||
D1 --> D3
|
||||
D1 --> D4
|
||||
D1 --> D5
|
||||
D2 --> D3
|
||||
D3 --> D4
|
||||
D3 --> D5
|
||||
D4 --> D6
|
||||
D3 --> D6
|
||||
```
|
||||
|
||||
**双图并存说明**:
|
||||
|
||||
- **1.1a 技术分层**:描述部署、流量路径、网络边界,关注"如何部署与调用"
|
||||
- **1.1b 业务领域**:描述 DDD 限界上下文、聚合根、领域依赖,关注"业务边界与归属"
|
||||
- 两图互补,分别服务于运维/SRE 与产品/架构视角
|
||||
|
||||
### 1.2 服务清单
|
||||
|
||||
| 类别 | 服务名 | 语言/框架 | 限界上下文 | 阶段 |
|
||||
|------|--------|-----------|-----------|------|
|
||||
| 基础设施 | api-gateway | Go (Gin) | 网关 | P1 |
|
||||
| 基础设施 | push-gateway | Go (Gin) | 推送 | P5 |
|
||||
| BFF | teacher-bff | TS (NestJS) | 教师聚合 | P2 |
|
||||
| BFF | student-bff | TS (NestJS) | 学生聚合 | P3 |
|
||||
| BFF | parent-bff | TS (NestJS) | 家长聚合 | P4 |
|
||||
| 业务 | iam | TS (NestJS) | 身份认证 | P2 |
|
||||
| 业务 | core-edu | TS (NestJS) | 教学核心 | P3 |
|
||||
| 业务 | content | TS (NestJS) | 内容资源 | P4 |
|
||||
| 业务 | data-ana | Python (FastAPI) | 数据分析 | P4 |
|
||||
| 业务 | msg | TS (NestJS) | 消息通知 | P5 |
|
||||
| 业务 | ai | Python (FastAPI) | AI 网关 | P5 |
|
||||
| 微前端 | teacher-portal | TS (Next.js) | 教师端 | P2 |
|
||||
| 微前端 | student-portal | TS (Next.js) | 学生端 | P3 |
|
||||
| 微前端 | parent-portal | TS (Next.js) | 家长端 | P4 |
|
||||
| 微前端 | admin-portal | TS (Next.js) | 管理端 | P6 |
|
||||
| 共享包 | shared-proto | TS | protobuf 契约 | P1 |
|
||||
| 共享包 | shared-ts | TS | TS 共享工具 | P1 |
|
||||
| 共享包 | shared-go | Go | Go 共享工具 | P1 |
|
||||
| 共享包 | shared-py | Python | Python 共享工具 | P4 |
|
||||
| 类别 | 服务名 | 语言/框架 | 限界上下文 | 业务领域 | 阶段 |
|
||||
| -------- | -------------- | ---------------- | --------------- | ----------------------------- | ---- |
|
||||
| 基础设施 | api-gateway | Go (Gin) | 网关 | — | P1 |
|
||||
| 基础设施 | push-gateway | Go (Gin) | 推送 | — | P5 |
|
||||
| BFF | teacher-bff | TS (NestJS) | 教师聚合 | 教学场景域 | P2 |
|
||||
| BFF | student-bff | TS (NestJS) | 学生聚合 | 学习场景域 | P3 |
|
||||
| BFF | parent-bff | TS (NestJS) | 家长聚合 | 家长场景域 | P4 |
|
||||
| 业务 | iam | TS (NestJS) | 身份认证 | **D1 身份认证** | P2 |
|
||||
| 业务 | core-edu | TS (NestJS) | 教学核心 | **D2 教学组织 + D3 教学核心** | P3 |
|
||||
| 业务 | content | TS (NestJS) | 内容资源 | **D4 内容资源** | P4 |
|
||||
| 业务 | data-ana | Python (FastAPI) | 数据分析 | **D6 智能洞察** | P4 |
|
||||
| 业务 | msg | TS (NestJS) | 消息通知 | **D5 沟通通知** | P5 |
|
||||
| 业务 | ai | Python (FastAPI) | AI 网关 | **D6 智能洞察** | P5 |
|
||||
| 微前端 | teacher-portal | TS (Next.js) | 教师端 | 教学场景域 | P2 |
|
||||
| 微前端 | student-portal | TS (Next.js) | 学生端 | 学习场景域 | P3 |
|
||||
| 微前端 | parent-portal | TS (Next.js) | 家长端 | 家长场景域 | P4 |
|
||||
| 微前端 | admin-portal | TS (Next.js) | 管理端 | 管理场景域 | P6 |
|
||||
| 共享包 | shared-proto | TS | protobuf 契约 | — | P1 |
|
||||
| 共享包 | shared-ts | TS | TS 共享工具 | — | P1 |
|
||||
| 共享包 | shared-go | Go | Go 共享工具 | — | P1 |
|
||||
| 共享包 | shared-py | Python | Python 共享工具 | — | P4 |
|
||||
|
||||
### 1.3 CICD → Edu 模块映射
|
||||
|
||||
| CICD 模块(旧) | Edu 服务(新) | 迁移阶段 |
|
||||
|----------------|---------------|----------|
|
||||
| ----------------------------------------- | -------------- | -------- |
|
||||
| auth + users + rbac | iam | P2 |
|
||||
| classes + subjects + enrollment | core-edu | P3 |
|
||||
| courses + lessons + schedule + attendance | core-edu | P3 |
|
||||
@@ -174,7 +234,7 @@ graph TB
|
||||
### 2.1 多语言技术栈矩阵
|
||||
|
||||
| 层级 | 语言 | 框架 | 用途 |
|
||||
|------|------|------|------|
|
||||
| -------- | --------------- | ------------------------------ | -------------------------- |
|
||||
| 网关层 | Go 1.22+ | Gin | API Gateway、Push Gateway |
|
||||
| 业务服务 | TypeScript 5.5+ | NestJS 10 | IAM、CoreEdu、Content、Msg |
|
||||
| 分析/AI | Python 3.12+ | FastAPI | DataAna、AI 网关 |
|
||||
@@ -185,7 +245,7 @@ graph TB
|
||||
### 2.2 多存储矩阵
|
||||
|
||||
| 存储 | 用途 | 使用服务 |
|
||||
|------|------|----------|
|
||||
| ------------- | ------------------------ | -------------------------- |
|
||||
| MySQL 8 | 写模型主库(每服务独占) | IAM、CoreEdu、Content、Msg |
|
||||
| Redis 7 | 缓存、会话、限流计数 | 全部服务 |
|
||||
| ClickHouse | 读模型宽表、分析聚合 | DataAna、CoreEdu(读模型) |
|
||||
@@ -195,7 +255,7 @@ graph TB
|
||||
### 2.3 基础设施矩阵
|
||||
|
||||
| 组件 | 用途 |
|
||||
|------|------|
|
||||
| ------------- | ----------------------------------- |
|
||||
| Kafka | 事件总线,领域事件异步通信 |
|
||||
| Debezium | CDC,MySQL Binlog → Kafka 实时同步 |
|
||||
| Temporal | 工作流编排(考试生命周期、AI 编排) |
|
||||
@@ -260,6 +320,7 @@ L1 客户端 → L2 微前端 → L3 网关 → L4 BFF → L5 业务服务 → L
|
||||
```
|
||||
|
||||
**严格规则**:
|
||||
|
||||
1. L3 网关层只做路由、鉴权、限流、熔断,**不写业务逻辑**
|
||||
2. L4 BFF 层只做聚合、裁剪、协议转换,**不持有业务状态**
|
||||
3. L5 业务服务之间通过 gRPC(同步)或 Kafka 事件(异步)通信,**不直接访问对方数据库**
|
||||
@@ -340,7 +401,7 @@ graph TB
|
||||
### 4.1 服务间通信矩阵
|
||||
|
||||
| 调用方 → 被调用方 | 协议 | 场景 |
|
||||
|-------------------|------|------|
|
||||
| ------------------ | ---------- | ---------------- |
|
||||
| api-gateway → BFF | gRPC | 请求路由 |
|
||||
| BFF → 业务服务 | gRPC | 同步查询聚合 |
|
||||
| CoreEdu → Content | Kafka 事件 | 教学内容变更通知 |
|
||||
@@ -390,7 +451,7 @@ sequenceDiagram
|
||||
### 5.2 三层角色模型
|
||||
|
||||
| 层级 | 来源 | 示例 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| -------- | ------------- | ------------------------------- | ------ |
|
||||
| 系统角色 | 系统预设 | admin、teacher、student、parent | 最高 |
|
||||
| 组织角色 | 学校/班级分配 | 年级组长、班主任、学科组长 | 中 |
|
||||
| 临时角色 | 临时授权 | 代课教师、临时代理 | 最低 |
|
||||
@@ -400,7 +461,7 @@ sequenceDiagram
|
||||
### 5.3 DataScope 6 级数据范围
|
||||
|
||||
| 级别 | 名称 | 数据范围 | 典型角色 |
|
||||
|------|------|----------|----------|
|
||||
| ---- | -------- | ---------- | ------------ |
|
||||
| L0 | SELF | 仅本人数据 | 学生、家长 |
|
||||
| L1 | CLASS | 本班数据 | 班主任、学生 |
|
||||
| L2 | GRADE | 本年级数据 | 年级组长 |
|
||||
@@ -410,6 +471,40 @@ sequenceDiagram
|
||||
|
||||
**实现**:业务服务在 Repository 层根据 dataScope 级别动态注入 WHERE 条件。
|
||||
|
||||
### 5.4 视口四层模型
|
||||
|
||||
视口(Viewport)是用户在特定场景域下的可见范围。视口既可独立配置(RoleViewport 表),
|
||||
也可由权限推导(permission → viewport 默认映射)。新角色只需配置权限集,视口自动推导;
|
||||
需要差异化时再显式配置视口。
|
||||
|
||||
| 层级 | 含义 | 配置载体 | 示例 |
|
||||
| ------- | ---------------- | ---------------------------------- | -------------------------------------------- |
|
||||
| L1 导航 | 侧边栏菜单项 | navigation_config 表 | 教导主任看到"全校成绩分析"菜单 |
|
||||
| L2 路由 | 可访问路由 | route_permission 表 + Gateway 校验 | 教导主任可访问 /admin/grade-analysis |
|
||||
| L3 组件 | 页面内组件可见性 | usePermission().hasPermission() | 教导主任看到"导出全校报表"按钮 |
|
||||
| L4 数据 | 数据行级过滤 | DataScope 枚举 | 教导主任 DataScope=grade_managed(管辖年级) |
|
||||
|
||||
**场景域 BFF 复用策略**:
|
||||
|
||||
按"使用场景域"分 BFF,而非按角色分。新角色复用现有 BFF,通过视口差异化。
|
||||
|
||||
| BFF | 场景域 | 复用角色 |
|
||||
| ----------- | -------- | ------------------------ |
|
||||
| Teacher BFF | 教学场景 | 教师、教导主任、教研组长 |
|
||||
| Student BFF | 学习场景 | 学生 |
|
||||
| Parent BFF | 家长场景 | 家长 |
|
||||
| Admin BFF | 管理场景 | 系统管理员、校管理员 |
|
||||
|
||||
**实现**:教导主任归入 Teacher BFF + 额外管理视口(L1 导航增加管理菜单项,L4 数据范围扩大到年级)。
|
||||
|
||||
**iam 服务职责**:
|
||||
|
||||
- 认证:登录/登出/JWT/2FA
|
||||
- RBAC:角色/权限/角色-权限映射 CRUD
|
||||
- 视口配置:导航/路由/组件级视口配置 CRUD
|
||||
- DataScope:数据范围解析(all/grade_managed/class_taught/children/owned + 自定义)
|
||||
- 权限解析 API:getEffectivePermissions(userId) → {permissions, viewports, dataScope}
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据访问与缓存
|
||||
@@ -462,7 +557,7 @@ flowchart TD
|
||||
### 6.3 缓存策略矩阵
|
||||
|
||||
| 数据类型 | 存储 | TTL | 失效策略 |
|
||||
|----------|------|-----|----------|
|
||||
| ------------- | ---------- | ------- | ------------ |
|
||||
| 用户会话 | Redis | 30 分钟 | 滑动过期 |
|
||||
| 权限列表 | Redis | 5 分钟 | 事件驱动失效 |
|
||||
| 班级/年级列表 | Redis | 5 分钟 | 事件驱动失效 |
|
||||
@@ -521,7 +616,7 @@ graph LR
|
||||
### 7.2 事件 Topic 分类
|
||||
|
||||
| Topic 模式 | 示例 | 生产者 | 消费者 |
|
||||
|-----------|------|--------|--------|
|
||||
| ----------------------------------- | ---------- | ------- | ------------ |
|
||||
| `edu.identity.user.created` | 用户创建 | IAM | CoreEdu、Msg |
|
||||
| `edu.identity.user.updated` | 用户更新 | IAM | CoreEdu、Msg |
|
||||
| `edu.org.class.created` | 班级创建 | CoreEdu | DataAna |
|
||||
@@ -534,7 +629,7 @@ graph LR
|
||||
### 7.3 核心领域事件
|
||||
|
||||
| 事件 | 触发场景 | 消费者动作 |
|
||||
|------|----------|-----------|
|
||||
| --------------------- | -------------- | ------------------------------------------------ |
|
||||
| UserRegistered | 新用户注册 | CoreEdu 初始化默认班级关联;Msg 发送欢迎通知 |
|
||||
| ExamPublished | 考试发布 | Msg 推送考试通知给学生;DataAna 创建考试分析骨架 |
|
||||
| HomeworkSubmitted | 学生提交作业 | DataAna 记录提交行为;Msg 通知教师 |
|
||||
@@ -768,6 +863,7 @@ flowchart LR
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 所有跨服务调用必须透传 W3C Trace Context
|
||||
- Kafka 事件必须将 traceId 写入消息 header
|
||||
- 日志必须包含 traceId 用于关联查询
|
||||
@@ -822,7 +918,7 @@ graph TB
|
||||
### 11.2 契约规则
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| -------- | -------------------------------------- |
|
||||
| 包命名 | `edu.[context].[aggregate].v[version]` |
|
||||
| 版本化 | 破坏性变更必须升版本(v1 → v2) |
|
||||
| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
|
||||
@@ -866,7 +962,7 @@ graph LR
|
||||
### 12.1 服务独立性约束
|
||||
|
||||
| 约束 | 说明 |
|
||||
|------|------|
|
||||
| ----------- | -------------------------------------- |
|
||||
| 数据库独占 | 每个微服务独占自身数据库,禁止跨库联表 |
|
||||
| 契约先行 | 所有跨服务通信必须先定义 protobuf 契约 |
|
||||
| Outbox 强制 | 所有领域事件必须通过 Outbox 模式发布 |
|
||||
@@ -877,7 +973,7 @@ graph LR
|
||||
### 12.2 通信约束
|
||||
|
||||
| 场景 | 允许 | 禁止 |
|
||||
|------|------|------|
|
||||
| -------------------- | ---------------- | ------------------- |
|
||||
| 客户端 → Gateway | REST + WebSocket | 直连业务服务 |
|
||||
| Gateway → BFF | gRPC | REST |
|
||||
| BFF → 业务服务 | gRPC | 直接访问 DB |
|
||||
@@ -888,7 +984,7 @@ graph LR
|
||||
### 12.3 数据一致性约束
|
||||
|
||||
| 场景 | 一致性级别 | 实现 |
|
||||
|------|-----------|------|
|
||||
| ------ | ---------- | --------------------- |
|
||||
| 聚合内 | 强一致 | 单事务 |
|
||||
| 聚合间 | 最终一致 | Kafka 事件 |
|
||||
| 服务间 | 最终一致 | Kafka 事件 / Saga |
|
||||
@@ -900,7 +996,7 @@ graph LR
|
||||
## 13. ADR 记录
|
||||
|
||||
| 编号 | 决策 | 原因 | 状态 |
|
||||
|------|------|------|------|
|
||||
| ------- | ----------------------------- | --------------------------------- | ------ |
|
||||
| ADR-001 | 采用 DDD 限界上下文划分服务 | 业务边界清晰,独立演进 | 已采纳 |
|
||||
| ADR-002 | 采用 NestJS 作为业务服务框架 | TS 生态成熟,装饰器 + DI 适合 DDD | 已采纳 |
|
||||
| ADR-003 | 采用 Go 作为网关语言 | 高并发、低内存、适合网关场景 | 已采纳 |
|
||||
@@ -934,7 +1030,7 @@ graph LR
|
||||
### 14.2 服务与阶段映射
|
||||
|
||||
| 阶段 | 周期 | 交付服务 | 退出标准 |
|
||||
|------|------|----------|----------|
|
||||
| ----------- | ------- | ----------------------------------------------------- | ------------------------------ |
|
||||
| P1 地基 | M1-M3 | api-gateway、classes(黄金模板)、shared-proto | classes 域 CRUD 端到端跑通 |
|
||||
| P2 身份 | M4-M6 | iam、teacher-bff、teacher-portal 骨架 | 教师可登录并看到空白 Dashboard |
|
||||
| P3 核心教学 | M7-M10 | core-edu(合并 classes)、student-bff、student-portal | 考试→作答→批改→成绩全链路 |
|
||||
|
||||
171
docs/architecture/runbooks/incident-response.md
Normal file
171
docs/architecture/runbooks/incident-response.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 事件响应手册
|
||||
|
||||
> 目标:规范生产事件的分级、响应、处置与复盘,确保 RTO ≤ 30min
|
||||
> 关联:`docs/architecture/runbooks/p6-hardening.md`
|
||||
|
||||
## 1. 事件分级
|
||||
|
||||
| 级别 | 定义 | 影响 | 响应时效 | 升级 |
|
||||
|------|------|------|----------|------|
|
||||
| P0 | 全站不可用 / 核心数据损坏 | 全部用户 | 5 分钟内响应 | 立即升级至 CTO |
|
||||
| P1 | 核心功能不可用 / 关键 SLO 破坏 | 大量用户 | 10 分钟内响应 | 升级至服务负责人 |
|
||||
| P2 | 部分功能降级 / 非核心故障 | 部分用户 | 30 分钟内响应 | 服务负责人跟进 |
|
||||
| P3 | 单点告警 / 潜在风险 | 少量/无用户 | 工作时间内响应 | On-Call 自行处理 |
|
||||
|
||||
### 1.1 分级示例
|
||||
|
||||
- P0:网关全挂、主 DB 不可用且无法故障转移、数据丢失超过 RPO
|
||||
- P1:登录不可用、课程播放不可用、Kafka 生产阻塞
|
||||
- P2:消息推送延迟、报表生成失败、单个非核心服务宕机
|
||||
- P3:单 Pod 重启、磁盘使用率告警、慢查询告警
|
||||
|
||||
## 2. 响应流程
|
||||
|
||||
```
|
||||
发现 → 确认 → 升级 → 处理 → 恢复 → 复盘
|
||||
```
|
||||
|
||||
### 2.1 发现
|
||||
|
||||
- 告警来源:Prometheus / Alertmanager / 用户反馈 / 人工巡检
|
||||
- 第一动作:在 On-Call 群贴告警,标注收到时间
|
||||
|
||||
### 2.2 确认
|
||||
|
||||
- On-Call 5 分钟内确认告警真实性
|
||||
- 排除误报(探针抖动、已知维护窗口)
|
||||
- 初步定级,创建事故工单(Jira / 飞书项目)
|
||||
|
||||
### 2.3 升级
|
||||
|
||||
- 超出处置能力 → 立即升级
|
||||
- P0/P1 → 拉事故群,通知相关服务 Owner
|
||||
- 涉及外部公告 → 通知客服与公关
|
||||
|
||||
### 2.4 处理
|
||||
|
||||
- 遵循"先恢复,后定位"原则
|
||||
- 优先使用预案:回滚、扩容、降级、熔断、切换
|
||||
- 每个操作记录时间戳与执行人
|
||||
- 关键决策需事故指挥确认
|
||||
|
||||
### 2.5 恢复
|
||||
|
||||
- 健康检查通过、SLO 恢复
|
||||
- 观察 15 分钟确认稳定
|
||||
- 关闭事故工单,进入复盘
|
||||
|
||||
### 2.6 复盘
|
||||
|
||||
- 48 小时内提交复盘报告
|
||||
- 无 blame 文化:对事不对人
|
||||
- 输出改进项,录入 `docs/architecture/roadmap/tech-debt.md`
|
||||
|
||||
## 3. On-Call 轮值
|
||||
|
||||
### 3.1 轮值制度
|
||||
|
||||
- 主备双人轮值,每周轮换
|
||||
- 工作日:9:00-21:00 主,其余备
|
||||
- 节假日:全天主备
|
||||
- 交接:周一 10:00 站会交接,遗留问题清单
|
||||
|
||||
### 3.2 联络方式
|
||||
|
||||
- 电话:主 + 备 + 升级链
|
||||
- 即时通讯:飞书 On-Call 群
|
||||
- 告警:PagerDuty / 飞书机器人
|
||||
|
||||
### 3.3 响应要求
|
||||
|
||||
- P0/P1:电话 5 分钟内接听
|
||||
- 告警确认:群内 5 分钟内回复
|
||||
- 无法响应:自动升级至备值
|
||||
|
||||
## 4. 沟通模板
|
||||
|
||||
### 4.1 事故通报(初报)
|
||||
|
||||
```
|
||||
【事故通报】<P级别> - <简述>
|
||||
时间:<YYYY-MM-DD HH:MM>
|
||||
级别:<P0/P1/P2/P3>
|
||||
影响:<受影响功能/用户范围>
|
||||
现状:<已知信息>
|
||||
负责人:<On-Call>
|
||||
下一步:<计划动作>
|
||||
```
|
||||
|
||||
### 4.2 进展更新(每 30 分钟或重大变化)
|
||||
|
||||
```
|
||||
【进展更新】<事故标题>
|
||||
时间:<HH:MM>
|
||||
进展:<自上次以来发生/完成的事>
|
||||
当前状态:<仍受影响的功能>
|
||||
下一步:<接下来 30 分钟计划>
|
||||
```
|
||||
|
||||
### 4.3 恢复通知
|
||||
|
||||
```
|
||||
【恢复通知】<事故标题>
|
||||
恢复时间:<HH:MM>
|
||||
持续时长:<时长>
|
||||
原因:<根因摘要>
|
||||
影响:<最终影响评估>
|
||||
后续:<复盘会时间>
|
||||
```
|
||||
|
||||
### 4.4 复盘报告
|
||||
|
||||
```
|
||||
【复盘报告】<事故标题>
|
||||
时间:<起止时间>
|
||||
级别:<P级别>
|
||||
影响:<用户/功能/数据>
|
||||
时间线:<关键事件时间轴>
|
||||
根因:<5why 分析>
|
||||
处置:<做了什么、有效/无效>
|
||||
改进项:<TODO + 负责人 + 截止>
|
||||
经验:<可沉淀到 known-issues / runbook 的内容>
|
||||
```
|
||||
|
||||
## 5. 常见事故处置
|
||||
|
||||
### 5.1 DB 主从切换
|
||||
|
||||
1. 确认主库故障(健康检查、连接超时)
|
||||
2. 触发自动故障转移(Patroni / Orchestrator)
|
||||
3. 若自动失败,手动 `./scripts/db/failover.sh --service <svc>`
|
||||
4. 更新连接配置 / 刷新连接池
|
||||
5. 验证读写正常、数据位点
|
||||
6. 旧主恢复后作为从库加入
|
||||
|
||||
### 5.2 Kafka 消费堆积
|
||||
|
||||
1. 查看堆积:`kubectl exec -- kafka-consumer-lag`
|
||||
2. 定位慢消费者:查日志、trace
|
||||
3. 扩容消费者副本(HPA 或手动)
|
||||
4. 若处理逻辑慢:临时降级非核心处理
|
||||
5. 堆积消化后恢复
|
||||
6. 复盘:扩容阈值、消费者并发配置
|
||||
|
||||
### 5.3 服务雪崩
|
||||
|
||||
1. 确认雪崩源头(哪个下游故障)
|
||||
2. 确认熔断器已打开(Gateway 状态)
|
||||
3. 若未打开:手动 `./scripts/gateway/circuit-breaker.sh open <service>`
|
||||
4. 降级非核心功能
|
||||
5. 扩容上游服务应对重试流量
|
||||
6. 修复下游、半开试探、逐步恢复
|
||||
7. 复盘:熔断参数、依赖隔离
|
||||
|
||||
## 附录:升级链
|
||||
|
||||
| 级别 | 第一响应 | 升级 1 | 升级 2 | 升级 3 |
|
||||
|------|----------|--------|--------|--------|
|
||||
| P0 | On-Call | 服务负责人 | 架构负责人 | CTO |
|
||||
| P1 | On-Call | 服务负责人 | 架构负责人 | - |
|
||||
| P2 | On-Call | 服务负责人 | - | - |
|
||||
| P3 | On-Call | - | - | - |
|
||||
264
docs/architecture/runbooks/p6-hardening.md
Normal file
264
docs/architecture/runbooks/p6-hardening.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# P6 生产硬化 Runbook
|
||||
|
||||
> 阶段:P6 生产硬化
|
||||
> 目标指标:RPO ≤ 15min,RTO ≤ 30min,P99 ≤ 500ms
|
||||
> 维护者:SRE 团队
|
||||
> 关联文档:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/roadmap/tech-debt.md`、`docs/architecture/runbooks/incident-response.md`
|
||||
|
||||
## 1. 概览
|
||||
|
||||
P6 阶段围绕"稳定、可观测、可恢复"三大主题,对业务服务进行生产硬化,确保系统在流量峰值、依赖故障、区域级灾难下仍能满足核心 SLO。
|
||||
|
||||
### 1.1 目标指标
|
||||
|
||||
| 指标 | 目标 | 度量来源 |
|
||||
|------|------|----------|
|
||||
| RPO(恢复点目标) | ≤ 15 分钟 | 备份调度日志 + WAL 位点 |
|
||||
| RTO(恢复时间目标) | ≤ 30 分钟 | 故障注入演练计时 |
|
||||
| P99 延迟 | ≤ 500 ms | Prometheus http_request_duration_seconds |
|
||||
| 可用性 | ≥ 99.9% | 多区域健康探针汇总 |
|
||||
| 错误率 | ≤ 0.1% | Gateway 5xx 比率 |
|
||||
|
||||
### 1.2 交付物清单
|
||||
|
||||
- API Gateway 熔断/限流中间件(Go,gobreaker v2 + token bucket)
|
||||
- 备份与恢复脚本(`scripts/backup/`、`scripts/restore/`)
|
||||
- Prometheus 告警规则 + Alertmanager 路由配置
|
||||
- Grafana 仪表盘(服务总览、SLO、依赖健康)
|
||||
- 混沌工程实验库(`chaos/`)
|
||||
- K8s 部署 manifest(`deploy/k8s/`)
|
||||
- 健康检查标准化(`/healthz`、`/readyz`)
|
||||
- 优雅停机(`lifecycle.service.ts` + `app.enableShutdownHooks()`)
|
||||
|
||||
## 2. 熔断与限流
|
||||
|
||||
### 2.1 中间件使用
|
||||
|
||||
API Gateway(Go)在路由链路上统一接入:
|
||||
- 熔断器:`github.com/sony/gobreaker/v2`,按下游服务维度建桶
|
||||
- 限流:基于 `sync.Map` 的令牌桶,按 `route + tenant` 维度限流
|
||||
|
||||
熔断器配置示例(伪代码):
|
||||
|
||||
```go
|
||||
cb := gobreaker.NewCircuitBreaker[any](gobreaker.Settings{
|
||||
Name: "core-edu",
|
||||
MaxRequests: 5, // 半开态最大试探请求
|
||||
Interval: 60 * time.Second, // 计数窗口
|
||||
Timeout: 30 * time.Second, // 开启态冷却
|
||||
ReadyToTrip: func(c gobreaker.Counts) bool {
|
||||
return c.ConsecutiveFailures > 5 || c.TotalFailures/c.TotalRequests > 0.5
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 2.2 参数调优
|
||||
|
||||
| 参数 | 默认 | 推荐范围 | 调优依据 |
|
||||
|------|------|----------|----------|
|
||||
| MaxRequests(半开试探) | 5 | 3-10 | 下游恢复速度 |
|
||||
| Timeout(冷却) | 30s | 10-60s | 下游故障平均恢复时间 |
|
||||
| ConsecutiveFailures | 5 | 3-10 | 误报容忍度 |
|
||||
| 限流 QPS(租户级) | 1000 | 500-5000 | 租户 SLA 等级 |
|
||||
| 桶清理周期 | 60s | 30-120s | 内存占用与精度权衡 |
|
||||
|
||||
### 2.3 故障演练流程
|
||||
|
||||
1. 在预发环境注入下游延迟(tc/netem 500ms+)
|
||||
2. 观察熔断器状态切换:Closed → Open → Half-Open → Closed
|
||||
3. 验证限流桶在窗口边界正确清理(sync.Map + ticker)
|
||||
4. 记录 P99 与错误率到演练报告
|
||||
5. 回滚注入:`./chaos/rollback.sh <experiment>`
|
||||
|
||||
## 3. 备份与恢复
|
||||
|
||||
### 3.1 备份脚本
|
||||
|
||||
- 位置:`scripts/backup/backup-cron.sh`
|
||||
- 调度:K8s CronJob,每 15 分钟一次(满足 RPO ≤ 15min)
|
||||
- 内容:PostgreSQL 全量 + WAL 归档 + Redis RDB + Kafka topic offset 快照
|
||||
- 产物:写入对象存储(MinIO/S3),保留策略 7d(日备 30d)
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
./scripts/backup/backup-cron.sh --service iam --type full
|
||||
./scripts/backup/backup-cron.sh --service iam --type incr
|
||||
```
|
||||
|
||||
### 3.2 恢复演练流程
|
||||
|
||||
1. 在隔离环境拉起空集群
|
||||
2. 执行 `./scripts/restore/restore.sh --service <svc> --backup-id <id>`
|
||||
3. 校验数据一致性(行数、最新位点、校验和)
|
||||
4. 记录恢复耗时,验证 RTO ≤ 30min
|
||||
5. 演练频率:每月一次
|
||||
|
||||
### 3.3 RPO 验证方法
|
||||
|
||||
- 对比最近一次备份时间与当前时间差
|
||||
- 查询 `backup_log` 表 `last_success_at` 字段
|
||||
- 告警:连续 2 个周期(30min)未成功备份 → P1
|
||||
|
||||
## 4. 监控告警
|
||||
|
||||
### 4.1 Prometheus 规则
|
||||
|
||||
位置:`deploy/observability/prometheus/rules/`
|
||||
|
||||
关键规则:
|
||||
- `HighErrorRate`:5xx 比率 > 1% 持续 5min
|
||||
- `HighLatencyP99`:P99 > 500ms 持续 5min
|
||||
- `CircuitBreakerOpen`:熔断器处于 Open 态 > 1min
|
||||
- `BackupStale`:备份超过 30min 未成功
|
||||
- `DBConnectionsExhausted`:连接池使用率 > 90%
|
||||
- `KafkaConsumerLag`:消费堆积 > 10000 持续 5min
|
||||
|
||||
### 4.2 Alertmanager 路由
|
||||
|
||||
- P0/P1 → PagerDuty + 电话
|
||||
- P2 → 飞书群 + 邮件
|
||||
- P3 → 飞书群
|
||||
- 抑制:同一服务 5min 内同告警只发一次(inhibit + group_by)
|
||||
|
||||
### 4.3 Grafana 仪表盘
|
||||
|
||||
| 仪表盘 | 用途 | 关键面板 |
|
||||
|--------|------|----------|
|
||||
| Service Overview | 服务总览 | QPS、P99、错误率、熔断状态 |
|
||||
| SLO Dashboard | SLO 跟踪 | 可用性、错误预算消耗 |
|
||||
| Dependency Health | 依赖健康 | DB/Redis/Kafka 连接与延迟 |
|
||||
| Backup Status | 备份状态 | 最近备份、RPO、恢复演练 |
|
||||
|
||||
## 5. 混沌工程
|
||||
|
||||
### 5.1 实验清单
|
||||
|
||||
| 实验 | 注入方式 | 预期表现 | 频率 |
|
||||
|------|----------|----------|------|
|
||||
| DB 主节点宕机 | kill postgres | 自动故障转移,RTO<30min | 月 |
|
||||
| Redis 不可达 | iptables 拒绝 | 降级到本地缓存 | 月 |
|
||||
| Kafka broker 宕机 | kill broker | 生产重试,消费堆积可控 | 月 |
|
||||
| 下游服务延迟 | tc netem +500ms | 熔断器打开,错误率<1% | 周 |
|
||||
| 网络分区 | iptables 隔离 | 多可用区切换 | 季 |
|
||||
| 磁盘满 | fill disk | 告警触发,写入降级 | 季 |
|
||||
|
||||
### 5.2 执行流程
|
||||
|
||||
1. 选择实验 → 在 `chaos/` 选择对应 YAML
|
||||
2. 预检:确认告警通道、回滚脚本就绪
|
||||
3. 执行:`kubectl apply -f chaos/<experiment>.yaml`
|
||||
4. 观察:Grafana + 日志
|
||||
5. 回滚:`./chaos/rollback.sh <experiment>`
|
||||
6. 复盘:记录到 `docs/architecture/runbooks/incident-response.md`
|
||||
|
||||
## 6. 安全加固
|
||||
|
||||
### 6.1 WAF 规则
|
||||
|
||||
- SQL 注入、XSS 模式匹配
|
||||
- 路径穿越、命令注入
|
||||
- 限速:单 IP > 100req/s 拦截
|
||||
- 地域封禁(按需)
|
||||
|
||||
### 6.2 密钥管理
|
||||
|
||||
- 密钥存储:K8s Secret + 外部 KMS(Vault)
|
||||
- 轮换:DB 密码每 90 天,JWT 签名密钥每 180 天
|
||||
- 注入:通过环境变量 / 挂载卷,禁止入镜像
|
||||
- 审计:所有密钥访问记录到 audit log
|
||||
|
||||
### 6.3 CORS 策略
|
||||
|
||||
- 允许来源:白名单域名(生产环境严格)
|
||||
- 允许方法:GET/POST/PUT/PATCH/DELETE
|
||||
- 凭证:允许(Cookie)
|
||||
- 预检缓存:600s
|
||||
|
||||
## 7. K8s 部署
|
||||
|
||||
### 7.1 Manifest 说明
|
||||
|
||||
位置:`deploy/k8s/`,每个服务一组 manifest:
|
||||
|
||||
- `deployment.yaml`:副本数、资源、探针、优雅停机
|
||||
- `service.yaml`:ClusterIP
|
||||
- `hpa.yaml`:CPU>70% 扩容,min=3 max=20
|
||||
- `poddisruptionbudget.yaml`:minAvailable=2
|
||||
- `networkpolicy.yaml`:限制出向
|
||||
|
||||
探针配置:
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet: { path: /healthz, port: 3000 }
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet: { path: /readyz, port: 3000 }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
```
|
||||
|
||||
优雅停机:
|
||||
|
||||
```yaml
|
||||
terminationGracePeriodSeconds: 60
|
||||
```
|
||||
|
||||
### 7.2 Helm 化路线
|
||||
|
||||
- P6:原生 manifest(快速验证)
|
||||
- P7:抽取 Helm Chart,values.yaml 按环境区分
|
||||
- P8:引入 Argo CD GitOps 自动同步
|
||||
|
||||
## 8. 灾难恢复
|
||||
|
||||
### 8.1 RTO/RPO 目标
|
||||
|
||||
| 场景 | RTO | RPO |
|
||||
|------|-----|-----|
|
||||
| 单 Pod 故障 | 30s | 0 |
|
||||
| 单节点故障 | 2min | 0 |
|
||||
| 单可用区故障 | 10min | 0 |
|
||||
| 区域级灾难 | 30min | 15min |
|
||||
|
||||
### 8.2 多可用区策略
|
||||
|
||||
- K8s 集群跨 3 可用区,Pod 反亲和
|
||||
- DB 主从跨可用区同步复制
|
||||
- Redis 哨兵跨可用区
|
||||
- Kafka min.insync.replicas=2,跨可用区 broker
|
||||
|
||||
### 8.3 DNS 切换
|
||||
|
||||
- 区域级故障:通过全局 DNS(Cloudflare/Route53)切换到备用区域
|
||||
- 健康检查:每 10s 探测,连续 3 次失败自动切换
|
||||
- TTL:60s(快速切换)
|
||||
- 演练:每季度一次 DNS 切换演练
|
||||
|
||||
## 9. 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|
||||
|------|----------|----------|----------|
|
||||
| 5xx 激增 | 下游服务故障 | 查 Grafana 熔断状态、下游健康 | 确认熔断器已打开,扩容下游 |
|
||||
| P99 升高 | DB 慢查询/连接耗尽 | 查 PG 慢日志、连接池 | 加索引/扩连接池/限流 |
|
||||
| 消费堆积 | 消费者慢/宕机 | 查 Kafka lag、消费者日志 | 扩消费者、修 bug |
|
||||
| 备份失败 | 存储/网络/凭证 | 查 backup-cron 日志 | 修凭证、清理旧备份 |
|
||||
| 健康检查失败 | DB 不可达 | 查 DB 状态、网络 | 故障转移、恢复 DB |
|
||||
| Pod 频繁重启 | OOM/探针失败 | 查 kubectl describe、内存 | 调资源/修探针 |
|
||||
| 熔断不恢复 | 下游未恢复 | 查 Half-Open 试探结果 | 修复下游、调 Timeout |
|
||||
| 限流误杀 | 桶配置过低 | 查限流日志、QPS | 调高桶容量 |
|
||||
| DNS 切换无效 | TTL 缓存 | 查 DNS 解析链 | 等待 TTL / 清缓存 |
|
||||
| 跨区延迟高 | 跨区流量 | 查网络拓扑 | 调亲和性就近访问 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:关联文档
|
||||
|
||||
- 架构影响地图:`docs/architecture/004_architecture_impact_map.md`
|
||||
- P6 架构补记:`docs/architecture/004-p6-addendum.md`
|
||||
- 事件响应:`docs/architecture/runbooks/incident-response.md`
|
||||
- 已知问题:`docs/troubleshooting/known-issues.md`
|
||||
- P6 已知问题补丁:`docs/troubleshooting/known-issues-p6-addendum.md`
|
||||
- 技术债务:`docs/architecture/roadmap/tech-debt.md`
|
||||
1029
docs/architecture/runbooks/post-p6-followup.md
Normal file
1029
docs/architecture/runbooks/post-p6-followup.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,8 @@
|
||||
> 状态:基线发布
|
||||
> 适用范围:Edu 微服务架构(TS + Go + Python + protobuf)
|
||||
> 关联文档:
|
||||
> - [项目规则](../../project_rules.md)
|
||||
>
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [Git 工作流](./git-workflow.md)
|
||||
> - [架构总览](../architecture/001_architecture_overview.md)
|
||||
@@ -85,7 +86,7 @@ import type { UserEntity } from "@/modules/user/domain/user.entity";
|
||||
|
||||
#### 2.4.1 模块组织
|
||||
|
||||
每个 NestJS 模块对应一个 DDD 聚合,结构见 [project_rules.md §4.1](../../project_rules.md#41-nestjs-业务微服务标准结构)。
|
||||
每个 NestJS 模块对应一个 DDD 聚合,结构见 [project_rules.md §3.3](../../.trae/rules/project_rules.md#33-模块标准结构ddd)。
|
||||
|
||||
#### 2.4.2 装饰器规则
|
||||
|
||||
@@ -111,6 +112,7 @@ export class UserController {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Controller 必须使用 `@Controller(path)` 装饰器,path 使用 kebab-case 复数
|
||||
- Controller 类必须使用 `@RequirePermission()` 装饰器(类级默认权限)
|
||||
- 每个 Handler 可选覆盖类级权限(更细粒度)
|
||||
@@ -131,6 +133,7 @@ export class UserService {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 依赖通过构造函数注入,使用 `readonly` 修饰符
|
||||
- 接口绑定在 Module 的 `providers` 中:`{ provide: "UserRepository", useClass: UserRepoImpl }`
|
||||
- 禁止使用属性注入(`@Inject()` 属性装饰器)
|
||||
@@ -152,6 +155,7 @@ export class UserModule {}
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Module 类名 PascalCase + `Module` 后缀
|
||||
- `exports` 仅暴露 Application Service,不暴露 Repository
|
||||
- 跨 Module 通信通过 exports 的 Service,不直接访问对方 Repository
|
||||
@@ -190,6 +194,7 @@ export class GetUserByIdHandler implements IQueryHandler<GetUserByIdQuery> {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Command 走写路径:Command → Handler → Domain → Repository → MySQL + Outbox
|
||||
- Query 走读路径:Query → Handler → Read Model(禁止查主库)
|
||||
- Command Handler 必须在事务内写 Outbox 表
|
||||
@@ -207,7 +212,12 @@ export class UserEntity {
|
||||
) {}
|
||||
|
||||
static create(props: UserCreateProps): UserEntity {
|
||||
return new UserEntity(crypto.randomUUID(), props.email, props.name, new Date());
|
||||
return new UserEntity(
|
||||
crypto.randomUUID(),
|
||||
props.email,
|
||||
props.name,
|
||||
new Date(),
|
||||
);
|
||||
}
|
||||
|
||||
rename(newName: string): UserRenamedEvent {
|
||||
@@ -218,6 +228,7 @@ export class UserEntity {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- Entity 构造函数私有,通过静态工厂方法创建
|
||||
- Entity 字段私有,通过方法变更状态
|
||||
- 状态变更方法返回领域事件,由 Application Service 发布
|
||||
@@ -239,6 +250,7 @@ export class CreateUserDto {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- DTO 类名 `Create[Entity]Dto` / `Update[Entity]Dto` / `[Entity]Response`
|
||||
- DTO 字段使用 `readonly` 修饰
|
||||
- 使用 `class-validator` 装饰器校验
|
||||
@@ -265,7 +277,7 @@ export class CreateUserDto {
|
||||
### 2.10 状态管理(沿用 CICD 5 层模型)
|
||||
|
||||
| 层级 | 场景 | 方案 |
|
||||
|------|------|------|
|
||||
| ------------------ | -------------------- | ----------------------------- |
|
||||
| L1 URL | 可分享、可刷新的状态 | nuqs |
|
||||
| L2 Server | 服务端数据 | TanStack Query |
|
||||
| L3 Client Business | 客户端业务状态 | Zustand slice |
|
||||
@@ -328,6 +340,7 @@ user, _ := h.userService.GetUser(ctx, id)
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 错误必须显式处理,**禁止 `_ = err`**
|
||||
- 使用 `errors.Is` 和 `errors.As` 判断错误类型,禁止字符串匹配
|
||||
- 自定义错误类型使用 `fmt.Errorf("...: %w", err)` 包装
|
||||
@@ -349,6 +362,7 @@ type UserService struct {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- `context.Context` 作为函数第一个参数传递
|
||||
- **禁止**将 context 存储在结构体字段中
|
||||
- 超时/取消通过 context 传递,禁止使用 `time.Sleep` 等待
|
||||
@@ -368,6 +382,7 @@ if err := g.Wait(); err != nil {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 优先使用 `errgroup` 管理并发 goroutine
|
||||
- 禁止裸 `go func()` 不带 recover 和 context
|
||||
- 共享状态使用 channel 或 `sync` 包,禁止使用 `sync.Mutex` 嵌套锁
|
||||
@@ -388,6 +403,7 @@ func RegisterRoutes(r *gin.Engine, h *Handler, mw *Middleware) {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 路由分组按 API 版本(`/api/v1`)
|
||||
- 中间件链顺序:Recovery → RequestID → Logger → RateLimit → Auth → RequirePermission
|
||||
- Handler 函数签名固定:`func(c *gin.Context)`
|
||||
@@ -404,6 +420,7 @@ logger.Info("user login", "user_id", userID, "ip", ip)
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 使用标准库 `log/slog` 结构化日志
|
||||
- 日志字段使用 kebab-case key
|
||||
- 必须包含 `request_id` 用于链路追踪
|
||||
@@ -459,6 +476,7 @@ def get_user(user_id):
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 所有函数必须标注参数和返回值类型
|
||||
- 使用 `from __future__ import annotations` 启用延迟注解求值
|
||||
- 使用 `Optional[T]` 或 `T | None`(Python 3.10+)标注可选类型
|
||||
@@ -480,6 +498,7 @@ async def fetch_user(user_id: str) -> User:
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- I/O 操作(HTTP、DB、文件)必须使用 async/await
|
||||
- 禁止在 async 函数中调用同步阻塞 I/O,必须用 `asyncio.to_thread` 或 async 客户端
|
||||
- CPU 密集任务用 `asyncio.to_thread` 或进程池
|
||||
@@ -509,6 +528,7 @@ class UserResponse(BaseModel):
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 请求模型命名 `[Action][Entity]Request`,响应模型命名 `[Entity]Response`
|
||||
- 必须使用 `Field` 添加描述、约束
|
||||
- 复杂校验使用 `@field_validator` 或 `@model_validator`
|
||||
@@ -534,6 +554,7 @@ async def get_user(
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 路由分组使用 `APIRouter`,按 API 版本组织
|
||||
- 必须标注 `response_model`
|
||||
- 权限校验通过 `Depends` 注入,每个 endpoint 显式调用 `require_permission`
|
||||
@@ -557,6 +578,7 @@ settings = Settings()
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 配置使用 `pydantic-settings` 的 `BaseSettings`
|
||||
- 环境变量前缀按服务名(`INSIGHT_AI_`)
|
||||
- 禁止在业务代码中直接读取环境变量(`os.getenv`),统一通过 `settings`
|
||||
@@ -568,7 +590,7 @@ settings = Settings()
|
||||
### 5.1 命名通用规则
|
||||
|
||||
| 对象 | 风格 | 示例 |
|
||||
|------|------|------|
|
||||
| ---------------- | ------------------------ | ----------------------------------------- |
|
||||
| 目录 | kebab-case | `user-profile/` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 布尔值 | `is/has/can/should` 前缀 | `isVisible`、`is_active`、`hasPermission` |
|
||||
@@ -581,7 +603,7 @@ settings = Settings()
|
||||
### 5.2 文件行数通用规则
|
||||
|
||||
| 文件类型 | 建议行数 | 硬性上限 |
|
||||
|---------|---------|---------|
|
||||
| ------------------------- | -------- | ---------------------- |
|
||||
| 配置/常量/类型/proto | 无限制 | 无限制 |
|
||||
| React 组件 | ≤ 500 | 800 |
|
||||
| NestJS Controller/Service | ≤ 500 | 800 |
|
||||
@@ -594,12 +616,13 @@ settings = Settings()
|
||||
### 5.3 错误处理通用规则
|
||||
|
||||
| 语言 | 规则 |
|
||||
|------|------|
|
||||
| ---------- | -------------------------------------------------------------- |
|
||||
| TypeScript | 错误通过抛出异常,Application Service 必须捕获并转为结构化响应 |
|
||||
| Go | 错误必须显式处理,禁止 `_ = err`,使用 `errors.Is/As` 判断类型 |
|
||||
| Python | 使用异常层次结构,自定义异常继承 `Exception`,禁止裸 `except:` |
|
||||
|
||||
**通用规则**:
|
||||
|
||||
- 错误信息对内详细(含上下文、堆栈),对外脱敏(不泄露实现细节)
|
||||
- 错误必须分类:业务错误(4xx)、系统错误(5xx)、依赖错误(502/503)
|
||||
- 错误必须记录日志,包含 request_id 用于链路追踪
|
||||
@@ -608,12 +631,13 @@ settings = Settings()
|
||||
### 5.4 日志通用规则
|
||||
|
||||
| 语言 | 工具 | 说明 |
|
||||
|------|------|------|
|
||||
| ---------- | -------------------- | ---------------- |
|
||||
| TypeScript | NestJS Logger + pino | 结构化 JSON 日志 |
|
||||
| Go | log/slog | 标准库结构化日志 |
|
||||
| Python | structlog 或 loguru | 结构化 JSON 日志 |
|
||||
|
||||
**通用规则**:
|
||||
|
||||
- 日志必须结构化(JSON),禁止纯文本
|
||||
- 必须包含 `timestamp`、`level`、`service`、`request_id`、`trace_id`
|
||||
- 日志级别:DEBUG(开发)、INFO(关键业务)、WARN(异常可恢复)、ERROR(系统错误)
|
||||
@@ -623,12 +647,13 @@ settings = Settings()
|
||||
### 5.5 测试通用规则
|
||||
|
||||
| 语言 | 单元测试框架 | 覆盖率目标 |
|
||||
|------|------------|-----------|
|
||||
| ---------- | ------------------------- | ---------- |
|
||||
| TypeScript | Vitest + nestjs/testing | ≥ 80% |
|
||||
| Go | 标准 testing 包 + testify | ≥ 80% |
|
||||
| Python | pytest + pytest-asyncio | ≥ 80% |
|
||||
|
||||
**通用规则**:
|
||||
|
||||
- 测试文件与源文件同目录或 `tests/` 子目录
|
||||
- 命名:`*.test.ts` / `*_test.go` / `test_*.py`
|
||||
- 测试描述说明预期行为("should disable button while loading")
|
||||
@@ -714,6 +739,7 @@ enum UserStatus {
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 字段编号禁止复用,删除字段必须 `reserved` 标记
|
||||
- 枚举第一个值必须为 `*_UNSPECIFIED = 0`
|
||||
- 时间使用 `google.protobuf.Timestamp`,不使用 string
|
||||
@@ -771,7 +797,7 @@ buf generate
|
||||
### 7.1 令牌分层(沿用 CICD 模型,迁移至微前端共享包)
|
||||
|
||||
| Layer | 位置 | 用途 |
|
||||
|-------|------|------|
|
||||
| ----------------- | ------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| Layer 1 Primitive | `packages/ui-tokens/primitive.css` | 原始色板/字号/间距/阴影,业务代码不直接引用 |
|
||||
| Layer 2 Semantic | `packages/ui-tokens/semantic-light.css` + `semantic-dark.css` | 语义令牌,业务代码唯一引用入口 |
|
||||
| 模块命名空间 | `packages/ui-tokens/lesson-preparation.css` | `--lp-*` 令牌,明暗双份 |
|
||||
@@ -847,7 +873,7 @@ buf generate
|
||||
### 8.6 依赖扫描
|
||||
|
||||
| 语言 | 工具 |
|
||||
|------|------|
|
||||
| ------ | -------------------------- |
|
||||
| TS | `npm audit` + Snyk + Trivy |
|
||||
| Go | `govulncheck` |
|
||||
| Python | `pip-audit` + `safety` |
|
||||
@@ -874,7 +900,7 @@ buf generate
|
||||
### 9.1 测试分层
|
||||
|
||||
| 层级 | TS | Go | Python | 覆盖率 |
|
||||
|------|-----|-----|--------|--------|
|
||||
| -------- | ----------------------- | ------------------------ | ----------------------- | ------------ |
|
||||
| 单元测试 | Vitest | testing + testify | pytest | ≥ 80% |
|
||||
| 集成测试 | Vitest + Testcontainers | testing + Testcontainers | pytest + Testcontainers | 关键流程 |
|
||||
| E2E 测试 | Playwright | - | - | 核心业务路径 |
|
||||
@@ -966,7 +992,7 @@ pytest tests/integration -v
|
||||
## 附录:与 CICD 单应用规范的差异
|
||||
|
||||
| 项目 | CICD 单应用 | Edu 微服务 | 原因 |
|
||||
|------|-----------|-----------|------|
|
||||
| ------------ | --------------------------------- | -------------------------------------------- | -------------- |
|
||||
| 项目结构 | 单 Next.js 应用 | 多语言 monorepo | 微服务拆分 |
|
||||
| 数据获取层 | `modules/[module]/data-access.ts` | NestJS Repository + Domain Entity | DDD 分层 |
|
||||
| 中间件 | `proxy.ts`(Next.js 16) | Go Gin Gateway | 网关独立 |
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Edu Git 工作流规范
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-07
|
||||
> 状态:基线发布
|
||||
> 版本:1.1
|
||||
> 日期:2026-07-08
|
||||
> 状态:基线发布(v1.1:scope-enum 对齐 + CODEOWNERS)
|
||||
> 适用范围:Edu 多语言 monorepo(pnpm workspace + go.work + pyproject.toml)
|
||||
> 关联文档:
|
||||
> - [项目规则](../../project_rules.md)
|
||||
>
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
> - [编码规范](./coding-standards.md)
|
||||
> - [迁移指南](../../MIGRATION_GUIDE.md)
|
||||
> - [架构总览](../architecture/001_architecture_overview.md)
|
||||
@@ -33,6 +34,7 @@
|
||||
本项目采用**主干开发**模式,所有变更最终合并至 `main` 分支。
|
||||
|
||||
**核心原则**:
|
||||
|
||||
- `main` 分支始终保持可发布状态
|
||||
- 短生命周期特性分支(通常 ≤ 3 天)
|
||||
- 频繁集成,每天至少一次 rebase/merge 至最新 `main`
|
||||
@@ -67,7 +69,7 @@ gitGraph
|
||||
### 1.3 分支命名规范
|
||||
|
||||
| 分支类型 | 前缀 | 示例 | 生命周期 |
|
||||
|---------|------|------|---------|
|
||||
| -------- | ----------- | ---------------------------- | ---------- |
|
||||
| 主干 | `main` | `main` | 永久 |
|
||||
| 特性 | `feat/` | `feat/identity-service` | ≤ 3 天 |
|
||||
| 修复 | `fix/` | `fix/jwt-expiry` | ≤ 1 天 |
|
||||
@@ -78,6 +80,7 @@ gitGraph
|
||||
| 热修复 | `hotfix/` | `hotfix/v0.3.1` | ≤ 1 天 |
|
||||
|
||||
**规则**:
|
||||
|
||||
- 分支名使用 kebab-case
|
||||
- 一个分支只做一件事,禁止在一个分支内混合多个无关变更
|
||||
- 特性分支命名包含服务/模块名(`feat/identity-service` 而非 `feat/login`)
|
||||
@@ -85,6 +88,7 @@ gitGraph
|
||||
### 1.4 分支保护规则
|
||||
|
||||
**`main` 分支保护**:
|
||||
|
||||
- 禁止直接 push,必须通过 PR
|
||||
- 至少 1 名 Reviewer 审批通过(核心模块需 2 名)
|
||||
- 所有 CI 检查通过(lint + typecheck + test + build)
|
||||
@@ -92,6 +96,7 @@ gitGraph
|
||||
- 禁止 force push
|
||||
|
||||
**`release/*` 分支保护**:
|
||||
|
||||
- 禁止直接 push,仅接受 cherry-pick 或特定 hotfix PR
|
||||
- 至少 2 名 Reviewer 审批
|
||||
- 发布完成后打 tag 并归档
|
||||
@@ -115,7 +120,7 @@ gitGraph
|
||||
### 2.2 类型(type)
|
||||
|
||||
| 类型 | 含义 | 是否触发发布 |
|
||||
|------|------|-------------|
|
||||
| ---------- | -------------------------- | ------------ |
|
||||
| `feat` | 新功能 | 是(MINOR) |
|
||||
| `fix` | Bug 修复 | 是(PATCH) |
|
||||
| `perf` | 性能优化 | 是(PATCH) |
|
||||
@@ -133,6 +138,7 @@ gitGraph
|
||||
scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完整清单)。
|
||||
|
||||
**示例**:
|
||||
|
||||
- `feat(identity): 实现用户注册接口`
|
||||
- `fix(gateway): 修复路由匹配优先级`
|
||||
- `perf(teaching): 优化课表查询 N+1 问题`
|
||||
@@ -141,6 +147,7 @@ scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完
|
||||
### 2.4 主题(subject)
|
||||
|
||||
**规则**:
|
||||
|
||||
- 使用中文简短描述
|
||||
- 不超过 50 个字符
|
||||
- 不以句号结尾
|
||||
@@ -150,12 +157,14 @@ scope 必须是服务名或包名,详见 [§3.3 scope-enum](#33-scope-enum-完
|
||||
### 2.5 正文(body)
|
||||
|
||||
**规则**:
|
||||
|
||||
- 解释"为什么"而非"做了什么"(代码已说明做了什么)
|
||||
- 每行不超过 72 个字符
|
||||
- 使用无序列表列出关键变更点
|
||||
- 涉及 breaking change 必须在正文开头说明
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
feat(teaching): 作业提交支持附件上传
|
||||
|
||||
@@ -195,7 +204,7 @@ pnpm exec husky init
|
||||
|
||||
### 3.2 commitlint 配置
|
||||
|
||||
创建 `commitlint.config.cjs`:
|
||||
实际生效配置文件:仓库根 `.commitlintrc.js`(CommonJS)。
|
||||
|
||||
```javascript
|
||||
/** @type {import('@commitlint/types').UserConfig} */
|
||||
@@ -224,46 +233,42 @@ module.exports = {
|
||||
"type-case": [2, "always", "lower-case"],
|
||||
// type 不能为空
|
||||
"type-empty": [2, "never"],
|
||||
// scope 枚举(见 3.3)
|
||||
// scope 枚举(见 3.3,与 .commitlintrc.js 保持同步)
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
// 业务微服务
|
||||
"identity",
|
||||
"org",
|
||||
"teaching",
|
||||
// 网关层(Go)
|
||||
"api-gateway",
|
||||
"push-gateway",
|
||||
// 业务微服务(NestJS / FastAPI)
|
||||
"iam",
|
||||
"core-edu",
|
||||
"classes",
|
||||
"content",
|
||||
"comm",
|
||||
"insight",
|
||||
// 基础设施服务
|
||||
"auth",
|
||||
"notification",
|
||||
// AI 服务
|
||||
"insight-ai",
|
||||
// 网关
|
||||
"gateway",
|
||||
// BFF
|
||||
"admin-bff",
|
||||
"data-ana",
|
||||
"msg",
|
||||
"ai",
|
||||
// BFF 聚合层(NestJS)
|
||||
"teacher-bff",
|
||||
"student-bff",
|
||||
// 微前端
|
||||
"admin-shell",
|
||||
"teacher-shell",
|
||||
"student-shell",
|
||||
"parent-shell",
|
||||
// 共享包
|
||||
"contracts",
|
||||
"ui-tokens",
|
||||
"ui-components",
|
||||
"parent-bff",
|
||||
// 微前端(Next.js)
|
||||
"teacher-portal",
|
||||
"student-portal",
|
||||
"parent-portal",
|
||||
"admin-portal",
|
||||
// 共享包(packages/)
|
||||
"shared-proto",
|
||||
"shared-ts",
|
||||
// protobuf 契约
|
||||
"proto",
|
||||
// 平台级
|
||||
"deps",
|
||||
"shared-go",
|
||||
"shared-py",
|
||||
"shared-tokens",
|
||||
// 工具与平台级
|
||||
"arch-scan",
|
||||
"infra",
|
||||
"docs",
|
||||
"ci",
|
||||
"chore",
|
||||
"deps",
|
||||
"release",
|
||||
],
|
||||
],
|
||||
@@ -285,100 +290,89 @@ module.exports = {
|
||||
};
|
||||
```
|
||||
|
||||
> **单一事实源**:实际生效的配置在仓库根 `.commitlintrc.js`,本节示例仅作说明。修改 scope 必须同步更新 `.commitlintrc.js` 与本节,并在 PR 中说明原因。
|
||||
|
||||
### 3.3 scope-enum 完整清单
|
||||
|
||||
| 分类 | scope | 说明 |
|
||||
|------|-------|------|
|
||||
| 业务微服务 | `identity` | 身份与权限服务 |
|
||||
| 业务微服务 | `org` | 教学组织服务 |
|
||||
| 业务微服务 | `teaching` | 教学核心服务 |
|
||||
| 业务微服务 | `content` | 内容分析服务 |
|
||||
| 业务微服务 | `comm` | 沟通服务 |
|
||||
| 业务微服务 | `insight` | 智能洞察服务 |
|
||||
| 基础设施 | `auth` | 认证授权服务 |
|
||||
| 基础设施 | `notification` | 通知服务 |
|
||||
| AI 服务 | `insight-ai` | AI 分析服务(Python) |
|
||||
| 网关 | `gateway` | API 网关(Go) |
|
||||
| BFF | `admin-bff` | 管理端 BFF |
|
||||
| BFF | `teacher-bff` | 教师端 BFF |
|
||||
| BFF | `student-bff` | 学生/家长端 BFF |
|
||||
| 微前端 | `admin-shell` | 管理端 Shell |
|
||||
| 微前端 | `teacher-shell` | 教师端 Shell |
|
||||
| 微前端 | `student-shell` | 学生端 Shell |
|
||||
| 微前端 | `parent-shell` | 家长端 Shell |
|
||||
| 共享包 | `contracts` | protobuf 生成契约包 |
|
||||
| 共享包 | `ui-tokens` | 设计令牌包 |
|
||||
| 共享包 | `ui-components` | UI 组件库 |
|
||||
| 共享包 | `shared-ts` | TS 共享工具包 |
|
||||
| 契约 | `proto` | protobuf 定义文件 |
|
||||
| 平台级 | `deps` | 依赖升级 |
|
||||
| 平台级 | `docs` | 平台级文档 |
|
||||
| 平台级 | `ci` | CI/CD 配置 |
|
||||
| 平台级 | `chore` | 杂项 |
|
||||
| 平台级 | `release` | 发布相关 |
|
||||
> 与仓库根 `.commitlintrc.js` 保持同步;修改 scope 必须同时更新此处与 `.commitlintrc.js`。
|
||||
|
||||
| 分类 | scope | 对应目录 | 说明 |
|
||||
| ---------- | ---------------- | ------------------------- | ------------------------------------------------------ |
|
||||
| 网关层 | `api-gateway` | `services/api-gateway/` | API 网关(Go + Gin) |
|
||||
| 网关层 | `push-gateway` | `services/push-gateway/` | WebSocket 推送网关(Go) |
|
||||
| 业务微服务 | `iam` | `services/iam/` | 身份与访问管理(NestJS) |
|
||||
| 业务微服务 | `core-edu` | `services/core-edu/` | 教学核心服务(NestJS,Outbox + Kafka) |
|
||||
| 业务微服务 | `classes` | `services/classes/` | 班级服务(P1 黄金模板,P3 并入 core-edu) |
|
||||
| 业务微服务 | `content` | `services/content/` | 内容资源服务(NestJS + Neo4j) |
|
||||
| 业务微服务 | `data-ana` | `services/data-ana/` | 数据分析服务(Python + FastAPI + ClickHouse) |
|
||||
| 业务微服务 | `msg` | `services/msg/` | 消息通知服务(NestJS + ES) |
|
||||
| 业务微服务 | `ai` | `services/ai/` | AI 网关服务(Python + FastAPI + LLM) |
|
||||
| BFF 聚合层 | `teacher-bff` | `services/teacher-bff/` | 教师端 BFF(NestJS) |
|
||||
| BFF 聚合层 | `student-bff` | `services/student-bff/` | 学生端 BFF(待建立) |
|
||||
| BFF 聚合层 | `parent-bff` | `services/parent-bff/` | 家长端 BFF(待建立) |
|
||||
| 微前端 | `teacher-portal` | `apps/teacher-portal/` | 教师端 Portal(Next.js) |
|
||||
| 微前端 | `student-portal` | `apps/student-portal/` | 学生端 Portal(待建立) |
|
||||
| 微前端 | `parent-portal` | `apps/parent-portal/` | 家长端 Portal(待建立) |
|
||||
| 微前端 | `admin-portal` | `apps/admin-portal/` | 管理端 Portal(待建立) |
|
||||
| 共享包 | `shared-proto` | `packages/shared-proto/` | protobuf 契约定义 |
|
||||
| 共享包 | `shared-ts` | `packages/shared-ts/` | TS 共享类型与工具(待建立) |
|
||||
| 共享包 | `shared-go` | `packages/shared-go/` | Go 共享工具(待建立) |
|
||||
| 共享包 | `shared-py` | `packages/shared-py/` | Python 共享工具(待建立) |
|
||||
| 共享包 | `shared-tokens` | `packages/shared-tokens/` | 设计令牌(待建立) |
|
||||
| 工具 | `arch-scan` | `scripts/arch-scan/` | 架构元数据库扫描器 |
|
||||
| 平台级 | `infra` | `infra/` | 基础设施(K8s / docker-compose / backup / monitoring) |
|
||||
| 平台级 | `docs` | `docs/` | 平台级文档(跨多模块) |
|
||||
| 平台级 | `deps` | - | 依赖升级 |
|
||||
| 平台级 | `release` | - | 发布相关 |
|
||||
|
||||
> `chore` / `ci` / `build` 等 Conventional Commits 标准 type 不需要 scope,可直接使用 `chore: xxx`、`ci: xxx`。
|
||||
|
||||
### 3.4 husky hooks
|
||||
|
||||
实际生效的 hook 文件在仓库根 `.husky/` 目录。使用 `npx --no-install` 确保使用本地依赖。
|
||||
|
||||
`.husky/commit-msg`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
pnpm exec commitlint --edit "$1"
|
||||
npx --no-install commitlint --edit $1
|
||||
```
|
||||
|
||||
`.husky/pre-commit`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
pnpm exec lint-staged
|
||||
npx --no-install lint-staged
|
||||
```
|
||||
|
||||
`.husky/pre-push`:
|
||||
`.husky/pre-push`(推送前类型检查,TS 服务 typecheck 通过后才允许推送):
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env sh
|
||||
# 推送前运行类型检查
|
||||
pnpm -r run typecheck
|
||||
# 推送前运行类型检查(TS 服务)
|
||||
pnpm -r run typecheck 2>/dev/null || echo "[pre-push] typecheck 跳过或不可用"
|
||||
# Go 服务编译检查
|
||||
for d in services/api-gateway services/push-gateway; do
|
||||
if [ -d "$d" ]; then (cd "$d" && go build ./... ) || exit 1; fi
|
||||
done
|
||||
```
|
||||
|
||||
> **注意**:`pre-push` 中的 typecheck 在 TS 服务 ESLint 9 flat config 迁移完成前为可选(当前 `pnpm -r run typecheck` 缺失脚本,会 fallback 到 echo 提示)。Go `go build` 检查必须通过。
|
||||
|
||||
### 3.5 lint-staged 配置
|
||||
|
||||
`package.json`(根目录):
|
||||
实际生效配置文件:仓库根 `lint-staged.config.js`(CommonJS)。
|
||||
|
||||
```json
|
||||
{
|
||||
"lint-staged": {
|
||||
// TypeScript / NestJS / Next.js
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
// Go
|
||||
"*.go": [
|
||||
"gofmt -w",
|
||||
"golangci-lint run --fix"
|
||||
],
|
||||
// Python
|
||||
"*.py": [
|
||||
"ruff check --fix",
|
||||
"ruff format"
|
||||
],
|
||||
// protobuf
|
||||
"*.proto": [
|
||||
"buf format --write"
|
||||
],
|
||||
// Markdown
|
||||
"*.md": [
|
||||
"prettier --write"
|
||||
],
|
||||
// JSON / YAML
|
||||
"*.{json,yaml,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
```javascript
|
||||
module.exports = {
|
||||
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||
"*.{go,mod,sum}": ["gofmt -w", "golangci-lint run --fix"],
|
||||
"*.{py}": ["ruff check --fix", "ruff format"],
|
||||
"*.proto": ["buf format --write"],
|
||||
"*.md": ["prettier --write"],
|
||||
};
|
||||
```
|
||||
|
||||
> JSON / YAML 文件由 prettier 在 `*.md` 规则外按全局配置处理,如需显式规则可在 `lint-staged.config.js` 追加 `'*.{json,yaml,yml}': ['prettier --write']`。
|
||||
|
||||
---
|
||||
|
||||
## 四、PR 与 Code Review
|
||||
@@ -435,6 +429,7 @@ feat(identity): 实现用户注册接口
|
||||
## 影响范围
|
||||
|
||||
<!-- 列出受影响的服务/包 -->
|
||||
|
||||
- 服务:
|
||||
- 包:
|
||||
- 数据库迁移:是 / 否
|
||||
@@ -468,7 +463,7 @@ Closes #
|
||||
### 4.4 Reviewer 要求
|
||||
|
||||
| 变更类型 | 最少 Reviewer | 备注 |
|
||||
|---------|--------------|------|
|
||||
| ------------------------- | ------------- | ------------------- |
|
||||
| 普通业务变更 | 1 | 默认 |
|
||||
| 跨服务变更 | 2 | 涉及 ≥ 2 个服务 |
|
||||
| protobuf 契约变更 | 2 | 需包含架构组成员 |
|
||||
@@ -479,6 +474,7 @@ Closes #
|
||||
### 4.5 Code Review 清单
|
||||
|
||||
**通用检查**:
|
||||
|
||||
- [ ] 代码是否符合 [编码规范](./coding-standards.md)
|
||||
- [ ] 是否有明显的逻辑错误
|
||||
- [ ] 错误处理是否完整(不忽略 error/err/exception)
|
||||
@@ -486,18 +482,21 @@ Closes #
|
||||
- [ ] 是否存在硬编码的密钥、token、连接字符串
|
||||
|
||||
**架构检查**:
|
||||
|
||||
- [ ] 是否违反限界上下文边界(跨服务直接查 DB)
|
||||
- [ ] 是否违反依赖方向(shared 反向依赖 services)
|
||||
- [ ] protobuf 变更是否向后兼容
|
||||
- [ ] 事件 schema 变更是否向后兼容
|
||||
|
||||
**性能检查**:
|
||||
|
||||
- [ ] 是否有 N+1 查询
|
||||
- [ ] 是否有未加索引的查询
|
||||
- [ ] 是否有不必要的大对象拷贝
|
||||
- [ ] 是否有阻塞事件循环的同步操作(Python/Node)
|
||||
|
||||
**安全检查**:
|
||||
|
||||
- [ ] 所有入口是否经过权限校验
|
||||
- [ ] 用户输入是否经过验证
|
||||
- [ ] SQL 是否使用参数化查询
|
||||
@@ -506,18 +505,61 @@ Closes #
|
||||
### 4.6 合并策略
|
||||
|
||||
**默认使用 Squash Merge**:
|
||||
|
||||
- 保留 PR 的完整变更作为一个 commit
|
||||
- commit message 使用 PR 标题
|
||||
- 删除特性分支
|
||||
|
||||
**禁止使用 Merge Commit**(除非是发布分支合并回 main):
|
||||
|
||||
- 避免历史中充斥 "Merge branch" 噪音
|
||||
- 保持线性历史
|
||||
|
||||
**Rebase Merge**:
|
||||
|
||||
- 仅用于需要保留多个有意义 commit 的特性分支
|
||||
- 需在 PR 中说明原因
|
||||
|
||||
### 4.7 模块 Owner 与 CODEOWNERS
|
||||
|
||||
**实际生效文件**:仓库根 `.github/CODEOWNERS`(GitHub/Gitea 原生支持,自动为 PR 分配 reviewer)。
|
||||
|
||||
**设计原则**:
|
||||
|
||||
- 每个模块至少 1 名 owner,核心模块 2 名
|
||||
- 跨模块变更(如 proto 契约、arch.db、project_rules)由架构组 review
|
||||
- 基础设施变更(K8s/Helm/backup)由 SRE review
|
||||
- owner 名单变更需走 PR,由架构组审批
|
||||
|
||||
**模块 Owner 分配矩阵**:
|
||||
|
||||
| 模块分类 | 路径 | Owner Team | 最少 Reviewer | 备注 |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------- | --------------------- |
|
||||
| 架构与规则 | `.trae/rules/`、`docs/architecture/`、`docs/standards/` | `@edu-platform/arch` | 2 | 架构组强制 review |
|
||||
| 架构工具 | `scripts/arch-scan/` | `@edu-platform/arch` | 1 | |
|
||||
| 根配置 | `.commitlintrc.js`、`lint-staged.config.js`、`package.json`、`pnpm-workspace.yaml`、`go.work`、`pyproject.toml`、`tsconfig.base.json` | `@edu-platform/arch` | 2 | 影响全局 |
|
||||
| 共享包 | `packages/shared-proto/` | `@edu-platform/arch` | 2 | 契约变更影响所有服务 |
|
||||
| 网关层 | `services/api-gateway/`、`services/push-gateway/` | `@edu-platform/gateway` | 1 | |
|
||||
| IAM 服务 | `services/iam/` | `@edu-platform/iam` | 2 | 核心模块强制 2 人 |
|
||||
| 教学核心 | `services/core-edu/`、`services/classes/` | `@edu-platform/edu-core` | 1 | |
|
||||
| 内容资源 | `services/content/` | `@edu-platform/content` | 1 | |
|
||||
| 消息通知 | `services/msg/` | `@edu-platform/messaging` | 1 | |
|
||||
| 数据分析 | `services/data-ana/` | `@edu-platform/data` | 1 | |
|
||||
| AI 服务 | `services/ai/` | `@edu-platform/ai` | 1 | |
|
||||
| BFF 层 | `services/teacher-bff/`、`services/student-bff/`、`services/parent-bff/` | `@edu-platform/edu-core` | 1 | |
|
||||
| 微前端 | `apps/teacher-portal/`、`apps/student-portal/`、`apps/parent-portal/`、`apps/admin-portal/` | `@edu-platform/frontend` | 1 | |
|
||||
| 基础设施 | `infra/k8s/`、`infra/backup/`、`infra/security/`、`infra/monitoring/` | `@edu-platform/sre` | 2 | 生产环境变更强制 2 人 |
|
||||
| CI/CD | `.github/`、`.husky/` | `@edu-platform/sre` | 1 | |
|
||||
| 文档 | `docs/troubleshooting/`、`docs/standards/` | `@edu-platform/arch` | 1 | known-issues 更新 |
|
||||
|
||||
> **Team handle 占位符**:上表 `@edu-platform/*` 为 team handle 模板。实际团队 handle 需在 GitHub/Gitea Organization 中创建对应 team 后,同步更新 `.github/CODEOWNERS`。
|
||||
|
||||
**CODEOWNERS 文件维护规则**:
|
||||
|
||||
1. 新增服务/包时,必须在同一 PR 中更新 `.github/CODEOWNERS`
|
||||
2. owner 变更(人员调动)需开独立 PR,由架构组审批
|
||||
3. CODEOWNERS 与本节表格保持同步,单一事实源为 `.github/CODEOWNERS` 文件
|
||||
|
||||
---
|
||||
|
||||
## 五、文档同步规则
|
||||
@@ -525,7 +567,7 @@ Closes #
|
||||
### 5.1 文档同步矩阵
|
||||
|
||||
| 代码变更类型 | 需同步的文档 | 同步时机 |
|
||||
|-------------|-------------|---------|
|
||||
| --------------------- | -------------------------------------------- | --------- |
|
||||
| 新增/删除服务 | `001_architecture_overview.md` + 服务 README | PR 内同步 |
|
||||
| 新增/删除模块 | 服务 README + `arch:scan` | PR 内同步 |
|
||||
| 新增/删除导出函数 | `pnpm run arch:scan` | 提交前 |
|
||||
@@ -551,6 +593,7 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
```
|
||||
|
||||
**违规检查**:
|
||||
|
||||
- 长文件(> 1000 行)
|
||||
- 未校验权限的 Handler
|
||||
- 循环依赖
|
||||
@@ -568,26 +611,33 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
> 技术栈:[语言 + 框架]
|
||||
|
||||
## 职责
|
||||
|
||||
[一段话描述]
|
||||
|
||||
## 架构
|
||||
|
||||
[mermaid 架构图]
|
||||
|
||||
## 核心流程
|
||||
|
||||
[mermaid 时序图]
|
||||
|
||||
## 目录结构
|
||||
|
||||
[树形结构 + 说明]
|
||||
|
||||
## 依赖
|
||||
|
||||
- 上游服务:[列表]
|
||||
- 下游服务:[列表]
|
||||
- 共享包:[列表]
|
||||
|
||||
## 约束
|
||||
|
||||
[业务规则、技术约束]
|
||||
|
||||
## 架构决策
|
||||
|
||||
[关键设计决策记录]
|
||||
```
|
||||
|
||||
@@ -599,11 +649,12 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
### X.X 主题分区
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
|------|----------|
|
||||
| -------- | ------------------ |
|
||||
| 简述场景 | 正确做法(一句话) |
|
||||
```
|
||||
|
||||
**规则**:
|
||||
|
||||
- 索引式:场景→技术/规则映射
|
||||
- 不写代码示例和错误示范
|
||||
- 同类问题在原条目补充,不重复创建
|
||||
@@ -618,6 +669,7 @@ git commit -m "feat(identity): 实现用户注册接口"
|
||||
**规则**:一个 commit 只涉及一个服务或一个包的变更。
|
||||
|
||||
**原因**:
|
||||
|
||||
- 便于回滚(按服务粒度回滚)
|
||||
- 便于追踪(changelog 清晰)
|
||||
- 便于 review(聚焦单一职责)
|
||||
@@ -653,12 +705,14 @@ git commit -m "build(contracts): 重新生成 proto 代码"
|
||||
### 6.4 依赖升级规则
|
||||
|
||||
**规则**:
|
||||
|
||||
- 依赖升级使用 `build(deps):` 类型
|
||||
- 必须说明升级原因(安全、功能、兼容性)
|
||||
- 安全漏洞修复必须包含 CVE 编号
|
||||
- 大版本升级需单独 PR 并完整测试
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
build(deps): 升级 nestjs 至 10.3.0
|
||||
|
||||
@@ -692,14 +746,14 @@ git commit -m "feat(notification): 适配 UserRegistered v2 事件"
|
||||
本项目采用**双层版本号**:
|
||||
|
||||
| 层级 | 格式 | 说明 |
|
||||
|------|------|------|
|
||||
| -------- | ------------------------ | ----------------------------------------- |
|
||||
| 平台版本 | `v{阶段}.{迭代}.{patch}` | 如 `v0.3.1`(P3 阶段第 1 次迭代 patch 1) |
|
||||
| 服务版本 | `{service}:{semver}` | 如 `identity:1.2.0` |
|
||||
|
||||
### 7.2 阶段版本范围
|
||||
|
||||
| 阶段 | 平台版本范围 | 说明 |
|
||||
|------|-------------|------|
|
||||
| ----------- | ------------ | ------------------------------- |
|
||||
| P1 地基 | `v0.1.x` | monorepo 初始化、CI/CD、arch.db |
|
||||
| P2 身份 | `v0.2.x` | identity + auth + notification |
|
||||
| P3 核心教学 | `v0.3.x` | org + teaching + content |
|
||||
@@ -712,7 +766,7 @@ git commit -m "feat(notification): 适配 UserRegistered v2 事件"
|
||||
**格式**:`{registry}/edu/{service}:{tag}`
|
||||
|
||||
| tag 类型 | 格式 | 示例 | 用途 |
|
||||
|---------|------|------|------|
|
||||
| -------- | -------------------- | ----------------- | ------------ |
|
||||
| 版本号 | `v{version}` | `v0.3.1` | 正式发布 |
|
||||
| 服务版本 | `{service}-{semver}` | `identity-1.2.0` | 服务独立版本 |
|
||||
| Git SHA | `sha-{short}` | `sha-a1b2c3d` | 精确追溯 |
|
||||
@@ -752,19 +806,23 @@ flowchart TD
|
||||
## [v0.3.0] - 2026-08-15
|
||||
|
||||
### Added
|
||||
|
||||
- 教学核心服务新增作业管理功能
|
||||
- 内容服务支持题库导入
|
||||
- 教师端 Shell 新增作业批改界面
|
||||
|
||||
### Changed
|
||||
|
||||
- identity 服务升级至 NestJS 10.3
|
||||
- gateway 路由匹配算法优化
|
||||
|
||||
### Fixed
|
||||
|
||||
- 修复 JWT 刷新 token 过期判断错误
|
||||
- 修复课表查询时区问题
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- UserRegistered 事件 schema 变更至 v2,消费方需升级
|
||||
```
|
||||
|
||||
@@ -778,6 +836,7 @@ flowchart TD
|
||||
```
|
||||
|
||||
**版本号升级规则**:
|
||||
|
||||
- **MAJOR**:Breaking Change(protobuf 不兼容变更、API 破坏性修改)
|
||||
- **MINOR**:新增功能,向后兼容
|
||||
- **PATCH**:Bug 修复,向后兼容
|
||||
@@ -789,7 +848,7 @@ flowchart TD
|
||||
### 8.1 回滚策略
|
||||
|
||||
| 场景 | 回滚方式 | 耗时 |
|
||||
|------|---------|------|
|
||||
| -------------- | ----------------------- | ---------- |
|
||||
| 代码缺陷 | `git revert` + 重新部署 | 5-10 分钟 |
|
||||
| 镜像问题 | `kubectl rollout undo` | 1-2 分钟 |
|
||||
| 数据库迁移问题 | 执行迁移 down 脚本 | 5-30 分钟 |
|
||||
@@ -833,6 +892,7 @@ kubectl rollout status deployment/identity -n edu-prod
|
||||
### 8.4 数据库迁移回滚
|
||||
|
||||
**规则**:
|
||||
|
||||
- 所有迁移必须提供 `up` 和 `down` 脚本
|
||||
- `down` 脚本必须在 CI 中测试
|
||||
- 回滚前必须备份生产数据
|
||||
@@ -865,6 +925,7 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
> 严重等级:P0/P1/P2/P3
|
||||
|
||||
## 时间线
|
||||
|
||||
- HH:MM 告警触发
|
||||
- HH:MM 确认问题
|
||||
- HH:MM 决定回滚
|
||||
@@ -872,15 +933,19 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
- HH:MM 服务恢复
|
||||
|
||||
## 影响分析
|
||||
|
||||
[受影响的功能、用户数、业务损失]
|
||||
|
||||
## 根本原因
|
||||
|
||||
[技术原因 + 流程原因]
|
||||
|
||||
## 回滚过程
|
||||
|
||||
[执行的操作]
|
||||
|
||||
## 改进措施
|
||||
|
||||
- [ ] 短期:[立即修复项]
|
||||
- [ ] 中期:[流程改进项]
|
||||
- [ ] 长期:[架构改进项]
|
||||
@@ -891,12 +956,12 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
## 九、附录:CICD 与 Edu Git 工作流差异
|
||||
|
||||
| 维度 | CICD(Next.js 单应用) | Edu(微服务 monorepo) |
|
||||
|------|----------------------|----------------------|
|
||||
| --------------------- | -------------------------------- | ---------------------------------------------------------------- |
|
||||
| 仓库结构 | 单一 Next.js 应用 | 多语言 monorepo(pnpm + go.work + uv) |
|
||||
| 分支策略 | trunk-based | trunk-based(沿用) |
|
||||
| 提交规范 | Conventional Commits | Conventional Commits(沿用,scope 扩展至服务/包) |
|
||||
| scope 范围 | 模块名(如 `exams`、`homework`) | 服务/包名(如 `identity`、`contracts`) |
|
||||
| commitlint scope-enum | 35 个模块 | 27 个服务/包 |
|
||||
| scope 范围 | 模块名(如 `exams`、`homework`) | 服务/包名(如 `iam`、`api-gateway`、`shared-proto`) |
|
||||
| commitlint scope-enum | 35 个模块 | 26 个 scope(含服务/包/工具/平台级,与 `.commitlintrc.js` 同步) |
|
||||
| PR Reviewer | 1 人 | 1-2 人(核心模块/跨服务 2 人) |
|
||||
| 合并策略 | Squash Merge | Squash Merge(沿用) |
|
||||
| 版本号 | 单一应用版本 | 双层(平台版本 + 服务独立版本) |
|
||||
@@ -914,5 +979,6 @@ pnpm --filter identity run migrate:down -- --to <version>
|
||||
## 变更记录
|
||||
|
||||
| 版本 | 日期 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| ---- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1.0 | 2026-07-07 | 基线发布,从 CICD 单应用规范迁移至微服务多语言 monorepo |
|
||||
| 1.1 | 2026-07-08 | scope-enum 对齐实际服务名(identity→iam、teaching→core-edu 等);新增 §4.7 模块 Owner 与 CODEOWNERS;husky hooks 与实际文件对齐;新增 pre-push hook 说明 |
|
||||
|
||||
24
docs/troubleshooting/known-issues-p6-addendum.md
Normal file
24
docs/troubleshooting/known-issues-p6-addendum.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# known-issues P6 补丁
|
||||
|
||||
> 本文件为 `docs/troubleshooting/known-issues.md` 的 P6 阶段补丁,列出 P6 新增的"场景→技术"映射。
|
||||
> 合并方式:将下表条目追加到原文件对应分区,遵循索引式速查规范,不写代码示例。
|
||||
|
||||
## P6 生产硬化新增场景
|
||||
|
||||
| 场景 | 技术方案 |
|
||||
|------|----------|
|
||||
| 熔断器状态切换(Closed/Open/Half-Open) | gobreaker v2 ReadyToTrip 回调按连续失败数 + 失败率判定 |
|
||||
| 限流桶按租户维度清理 | sync.Map + ticker 周期清理过期桶,避免内存泄漏 |
|
||||
| 备份脚本定时调度 | K8s CronJob + backup-cron.sh,15min 一次满足 RPO |
|
||||
| PostgreSQL WAL 归档恢复到时间点 | pg_receivewal 归档 + PITR 恢复 |
|
||||
| Redis 增量备份 | BGSAVE 触发 + RDB 文件上传对象存储 |
|
||||
| Kafka 消费位点快照 | __consumer_offsets topic dump 到对象存储 |
|
||||
| 熔断器半开态试探限流 | MaxRequests 限制并发试探,避免恢复期二次过载 |
|
||||
| 健康探针分流 liveness/readiness | /healthz 仅进程存活,/readyz 检查依赖,避免滚动重启雪崩 |
|
||||
| 优雅停机等待 in-flight 请求 | app.enableShutdownHooks + terminationGracePeriodSeconds=60 |
|
||||
| Kafka producer 关闭前 flush | producer.disconnect() 内部 flush,避免消息丢失 |
|
||||
| 数据源销毁顺序 | 先 Kafka、再 Redis、最后 DB,避免反向依赖阻塞 |
|
||||
| 混沌实验回滚 | rollback.sh 按实验名清理 tc/iptables 规则 |
|
||||
| DNS 切换消除缓存 | TTL=60s + 等待 + 客户端清缓存,避免切换无效 |
|
||||
| 告警抑制去重 | Alertmanager inhibit + group_by + group_wait |
|
||||
| Python 服务就绪检查延迟初始化 | readyz 返回 ok + TODO,避免启动期依赖未就绪导致探针失败 |
|
||||
@@ -1,7 +1,7 @@
|
||||
# 已知问题速查
|
||||
|
||||
> 索引式速查手册:场景 → 技术/规则映射。不写代码示例。
|
||||
> 架构规则见 [../architecture/004_architecture_impact_map.md](../architecture/004_architecture_impact_map.md) 与 [../../project_rules.md](../../project_rules.md)
|
||||
> 架构规则见 [../architecture/004_architecture_impact_map.md](../architecture/004_architecture_impact_map.md) 与 [../../.trae/rules/project_rules.md](../../.trae/rules/project_rules.md)
|
||||
> 工作经验日志按时间倒序追加(50 条上限),AI 发现更好方案时可更新本节。
|
||||
|
||||
---
|
||||
@@ -11,10 +11,10 @@
|
||||
### 1.1 多语言 monorepo 配置
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
|
||||
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
|
||||
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/*、services/classes、bff/*、apps/*、scripts/*),Go/Python 不入 |
|
||||
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/_、services/classes、bff/_、apps/_、scripts/_),Go/Python 不入 |
|
||||
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
|
||||
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
|
||||
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
|
||||
@@ -25,7 +25,7 @@
|
||||
### 1.2 Docker Compose 基础设施
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| 日常开发启动 | 用 `docker-compose.minimal.yml` 仅起 MySQL+Redis |
|
||||
| 全量启动内存不足 | 按 `profiles` 分阶段启用(full/kafka/cdc/analytics/graph/search/config/observability) |
|
||||
| 每服务 mem_limit | 避免单服务吃满内存:MySQL 512m、Redis 128m、Kafka 512m、ClickHouse 1g |
|
||||
@@ -38,7 +38,7 @@
|
||||
### 1.3 protobuf + buf 契约
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| -------------------- | --------------------------------------------------------------------------- |
|
||||
| 契约唯一源 | `packages/shared-proto/proto/*.proto`,禁止 REST/gRPC 混用 |
|
||||
| breaking change 检测 | `buf breaking --against '.git#branch=main'` CI 强制,必须升版本号 |
|
||||
| proto 版本化 | 包名带版本 `xxx.v1`、`xxx.v2`,新旧版本共存 |
|
||||
@@ -51,7 +51,7 @@
|
||||
### 1.4 NestJS 服务开发(TS)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| 服务标准结构 | `src/{main.ts,app.module.ts,config,middleware,<domain>,shared,generated}` 黄金模板复制 |
|
||||
| 三层配置 | env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul);每服务启动时合并 |
|
||||
| 环境变量校验 | Zod schema 校验 process.env,失败立即抛错终止启动 |
|
||||
@@ -71,7 +71,7 @@
|
||||
### 1.5 Go Gateway 开发
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ----------------- | --------------------------------------------------------------------------------------- |
|
||||
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) |
|
||||
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
|
||||
@@ -85,7 +85,7 @@
|
||||
### 1.6 可观测性(OTel + Prometheus + Loki)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| --------------- | -------------------------------------------------------------------------------- |
|
||||
| P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) |
|
||||
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
|
||||
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
|
||||
@@ -97,7 +97,7 @@
|
||||
### 1.7 微前端 Module Federation
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| ---------------------- | ---------------------------------------------------------------------------- |
|
||||
| 4 个稳定 portal | teacher-portal / student-portal / parent-app / admin-console,按场景域划分 |
|
||||
| P1 测试页 | teacher-portal 仅一个测试页验证 classes CRUD 链路,P2 起配 Module Federation |
|
||||
| 视口驱动渲染 | 侧边栏由 `viewports.L1` 驱动渲染,路由由 `viewports.L2` 控制 |
|
||||
@@ -107,7 +107,7 @@
|
||||
### 1.8 从旧项目迁移的通用经验
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| --------------------- | ------------------------------------------------------------------------------------ |
|
||||
| React 19 乐观更新 | `useOptimistic` 替代手动 isPending,配合 `useTransition` 自动管理回滚 |
|
||||
| Zustand 细粒度选择器 | 单字段 selector 优于 `useShallow` 多字段包装 |
|
||||
| Tiptap SSR | 必须 `immediatelyRender: false` 避免 hydration mismatch |
|
||||
@@ -129,13 +129,13 @@
|
||||
### 1.9 架构工具与验证命令
|
||||
|
||||
| 场景 | 命令/规则 |
|
||||
| --------------------- | ---------------------------------------------------------------------- |
|
||||
| arch.db 扫描 | `npm run arch:scan`(多语言:TS+Go+Python) |
|
||||
| arch.db 查询 | `npm run arch:query -- <command>` |
|
||||
| 查服务依赖 | `npm run arch:query -- service <service>` |
|
||||
| 查 proto 契约 | `npm run arch:query -- contracts` |
|
||||
| 查 Kafka 事件 | `npm run arch:query -- events` |
|
||||
| 查架构违规 | `npm run arch:query -- violations`(输出 0 为合格) |
|
||||
| ------------------- | ----------------------------------------------------------- |
|
||||
| arch.db 扫描 | `pnpm run arch:scan`(多语言:TS+Go+Python+Proto) |
|
||||
| arch.db 查询 | `pnpm run arch:query -- <command>` |
|
||||
| 查服务依赖 | `pnpm run arch:query -- deps <module>` |
|
||||
| 查 proto 契约 | `pnpm run arch:query -- sql "SELECT * FROM contracts"` |
|
||||
| 查 Kafka 事件 | `pnpm run arch:query -- sql "SELECT * FROM events"` |
|
||||
| 查架构违规 | `pnpm run arch:query -- violations`(骨架,P1 后期补全) |
|
||||
| proto lint | `cd packages/shared-proto && pnpm exec buf lint` |
|
||||
| proto breaking 检测 | `pnpm exec buf breaking --against '.git#branch=main'` |
|
||||
| proto 代码生成 | `pnpm proto:gen` |
|
||||
@@ -152,7 +152,7 @@
|
||||
### 2.1 api-gateway(Go)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| -------------- | ------------------------------------------------------------------------------- |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT,`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 |
|
||||
| P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM |
|
||||
| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` |
|
||||
@@ -165,7 +165,7 @@
|
||||
### 2.2 classes(TS/NestJS,P1 黄金模板)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
|
||||
| 黄金模板复制流程 | `cp -r services/classes services/xxx` → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
|
||||
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
|
||||
@@ -184,7 +184,7 @@
|
||||
### 2.3 iam(TS/NestJS,P2)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| 认证 | 登录/登出/JWT/2FA,RS256 非对称签名 |
|
||||
| RBAC | 角色/权限/角色-权限 CRUD + `getEffectivePermissions(userId)` API |
|
||||
| 视口配置 | 4 层模型(导航/路由/组件/数据),`role_viewports` 表 |
|
||||
@@ -197,7 +197,7 @@
|
||||
### 2.4 core-edu(TS/NestJS,P3)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 |
|
||||
| Outbox 模式 | 业务事务同写 `outbox_events` 表,后台 relay worker 投递 Kafka |
|
||||
| Outbox relay | Go 写独立服务 `services/outbox-relay/`,轻量高吞吐 |
|
||||
@@ -210,7 +210,7 @@
|
||||
### 2.5 content(TS/NestJS,P4)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ---------- | ------------------------------------------------------------------ |
|
||||
| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) |
|
||||
| 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 |
|
||||
| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 |
|
||||
@@ -219,7 +219,7 @@
|
||||
### 2.6 data-ana(Python/FastAPI,P4)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ------------ | ------------------------------------------------------------------------------ |
|
||||
| 学情诊断 | ClickHouse 宽表查询,5s 内返回 |
|
||||
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka(`mysql.cdc.*`)→ DataAna 消费写 ClickHouse |
|
||||
| CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 |
|
||||
@@ -229,7 +229,7 @@
|
||||
### 2.7 messaging(TS/NestJS,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| -------------- | -------------------------------------------------------------------- |
|
||||
| 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 |
|
||||
| 通知批量化 | `createNotifications(items)` 单次 INSERT,沿用旧项目 dispatcher 模式 |
|
||||
| 多渠道 | 站内/SMS/邮件/微信,in_app 批量 + 其他渠道并行 |
|
||||
@@ -239,7 +239,7 @@
|
||||
### 2.8 push-gateway(Go,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ---------------- | ----------------------------------------------- |
|
||||
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需发 Kafka 消息 |
|
||||
| 跨实例同步 | Redis PubSub |
|
||||
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
|
||||
@@ -247,7 +247,7 @@
|
||||
### 2.9 ai-gateway(Python/FastAPI,P5)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ----------------- | ------------------------------------------- |
|
||||
| LLM Provider 适配 | OpenAI/Anthropic,langchain/litellm 生态 |
|
||||
| Prompt 模板管理 | 版本管理友好 |
|
||||
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
|
||||
@@ -257,7 +257,7 @@
|
||||
### 2.10 shared-proto(契约包)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ------------ | ------------------------------------------------------------------------------- |
|
||||
| 目录结构 | `proto/*.proto` + `buf.yaml` + `buf.gen.yaml` |
|
||||
| P1 契约 | 仅 `classes.proto`,`iam.proto`/`core_edu.proto` 占位 |
|
||||
| 代码生成输出 | TS → `shared-ts/generated`,Go → `shared-go/`,Python → `services/*/generated/` |
|
||||
@@ -265,10 +265,10 @@
|
||||
### 2.11 arch-scan(多语言扫描器)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| TS 扫描 | ts-morph 解析 AST,提取导出/函数/类/import |
|
||||
| Go 扫描 | P1 用正则提取(函数/类型/import),P2 起替换为 tree-sitter-go AST |
|
||||
| Python 扫描 | P1 用正则提取,P4 起替换为 tree-sitter-python AST |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| TS 扫描 | regex 提取(function/class/interface/UPPER_CASE const),避免 ts-morph 对未安装依赖文件解析失败 |
|
||||
| Go 扫描 | 正则提取(行首锚定 `^func`/`^type`),P2 起替换为 tree-sitter-go AST |
|
||||
| Python 扫描 | 正则提取(行首锚定 `^def`/`^class`),P4 起替换为 tree-sitter-python AST |
|
||||
| arch.db schema | modules/symbols/dependencies/contracts/events/violations 六表 |
|
||||
| 全量扫描 | 先清空旧数据再扫描,避免残留 |
|
||||
| 并行扫描风险 | 并行子代理执行 arch:scan 可能因竞争报 FOREIGN KEY 错误,必须串行执行 |
|
||||
@@ -278,7 +278,7 @@
|
||||
### 2.12 teacher-portal(微前端宿主,P1 测试页)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| -------------------- | ------------------------------------------------------------------------- |
|
||||
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
|
||||
| API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` |
|
||||
| P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 IAM 签发 RS256 |
|
||||
@@ -291,5 +291,14 @@
|
||||
> 按时间倒序,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 硬化。 |
|
||||
|
||||
3
go.work
3
go.work
@@ -1,7 +1,6 @@
|
||||
go 1.22
|
||||
go 1.26.0
|
||||
|
||||
use (
|
||||
./services/api-gateway
|
||||
./services/push-gateway
|
||||
./packages/shared-go
|
||||
)
|
||||
|
||||
69
infra/alertmanager/alertmanager.yml
Normal file
69
infra/alertmanager/alertmanager.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
# Alertmanager 配置 - Edu 平台
|
||||
# 全局配置
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
|
||||
# 告警模板(可扩展)
|
||||
templates:
|
||||
- /etc/alertmanager/templates/*.tmpl
|
||||
|
||||
# 路由树
|
||||
route:
|
||||
# 顶层默认接收器
|
||||
receiver: webhook-default
|
||||
# 按 alertname + service 分组
|
||||
group_by: ['alertname', 'service']
|
||||
# 首次告警等待时间(聚合相同组)
|
||||
group_wait: 30s
|
||||
# 同组新告警发送间隔
|
||||
group_interval: 5m
|
||||
# 重复告警发送间隔
|
||||
repeat_interval: 4h
|
||||
routes:
|
||||
# critical 告警走 webhook
|
||||
- matchers:
|
||||
- severity="critical"
|
||||
receiver: webhook-default
|
||||
continue: false
|
||||
# warning 告警走 webhook
|
||||
- matchers:
|
||||
- severity="warning"
|
||||
receiver: webhook-default
|
||||
continue: false
|
||||
|
||||
# 接收器列表
|
||||
receivers:
|
||||
# 默认 Webhook 接收器(指向内部告警路由服务)
|
||||
- name: webhook-default
|
||||
webhook_configs:
|
||||
- url: http://alert-router:5001/alert
|
||||
send_resolved: true
|
||||
max_alerts: 0
|
||||
|
||||
# 邮件接收器(注释示例,留作扩展)
|
||||
# 启用前需在 global.smtp_* 配置 SMTP 服务器
|
||||
# - name: mail-ops
|
||||
# email_configs:
|
||||
# - to: ops-team@example.com
|
||||
# from: alertmanager@example.com
|
||||
# smarthost: smtp.example.com:587
|
||||
# auth_username: alertmanager@example.com
|
||||
# auth_password: <SMTP_PASSWORD>
|
||||
# require_tls: true
|
||||
# headers:
|
||||
# Subject: '[Edu Alert] {{ .GroupLabels.alertname }}'
|
||||
|
||||
# 钉钉/企业微信接收器(注释示例,留作扩展)
|
||||
# - name: dingtalk-ops
|
||||
# webhook_configs:
|
||||
# - url: http://dingtalk-webhook:8060/dingtalk/ops/send
|
||||
# send_resolved: true
|
||||
|
||||
# 抑制规则:critical 告警抑制同服务同名的 warning 告警
|
||||
inhibit_rules:
|
||||
- source_matchers:
|
||||
- severity="critical"
|
||||
target_matchers:
|
||||
- severity="warning"
|
||||
# 相同 alertname + service 才抑制
|
||||
equal: ['alertname', 'service']
|
||||
74
infra/backup/backup-cron.sh
Normal file
74
infra/backup/backup-cron.sh
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# MySQL 备份 cron 调度入口
|
||||
# 调用 backup-mysql.sh 备份所有微服务数据库,单个失败不阻塞其他
|
||||
# 用法: backup-cron.sh [--keep <days>]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
KEEP_DAYS=7
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--keep)
|
||||
KEEP_DAYS="${2:-7}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--keep <days>]" >&2
|
||||
echo " --keep 保留天数(默认 7)" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] 未知参数: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 配置 ----------
|
||||
# 每个服务对应一个独立数据库
|
||||
SERVICES=(iam core-edu content msg classes)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_SCRIPT="${SCRIPT_DIR}/backup-mysql.sh"
|
||||
|
||||
# ---------- 日志函数 ----------
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
}
|
||||
|
||||
if [[ ! -x "$BACKUP_SCRIPT" ]]; then
|
||||
log "[ERROR] 备份脚本不存在或不可执行: ${BACKUP_SCRIPT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
log "[INFO] 开始批量备份,共 ${#SERVICES[@]} 个服务"
|
||||
|
||||
TOTAL=0
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
FAILED_LIST=()
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
TOTAL=$((TOTAL + 1))
|
||||
log "[INFO] ---- 备份服务: ${svc} ----"
|
||||
if "$BACKUP_SCRIPT" --service "$svc" --keep "$KEEP_DAYS"; then
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
log "[INFO] 服务 ${svc} 备份成功"
|
||||
else
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_LIST+=("$svc")
|
||||
log "[ERROR] 服务 ${svc} 备份失败,继续下一个"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
log "[INFO] 批量备份结束: 总计=${TOTAL} 成功=${SUCCESS} 失败=${FAILED}"
|
||||
|
||||
if [[ ${FAILED} -gt 0 ]]; then
|
||||
log "[ERROR] 失败服务列表: ${FAILED_LIST[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
99
infra/backup/backup-mysql.sh
Normal file
99
infra/backup/backup-mysql.sh
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
# MySQL 全量备份脚本(Linux 容器内运行)
|
||||
# 用法: backup-mysql.sh --service <name> [--keep <days>]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
SERVICE=""
|
||||
KEEP_DAYS=7
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--service)
|
||||
SERVICE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--keep)
|
||||
KEEP_DAYS="${2:-7}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 --service <name> [--keep <days>]" >&2
|
||||
echo " --service 目标微服务数据库名(必填,如 iam/core-edu/content/msg/classes)" >&2
|
||||
echo " --keep 保留天数(默认 7)" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] 未知参数: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 参数校验 ----------
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
echo "[ERROR] --service 参数必填" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]] || [[ "$KEEP_DAYS" -lt 1 ]]; then
|
||||
echo "[ERROR] --keep 必须为正整数" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 环境变量 ----------
|
||||
: "${MYSQL_HOST:=mysql}"
|
||||
: "${MYSQL_PORT:=3306}"
|
||||
: "${MYSQL_USER:=root}"
|
||||
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD 环境变量未设置}"
|
||||
|
||||
BACKUP_ROOT="/backups"
|
||||
BACKUP_DIR="${BACKUP_ROOT}/${SERVICE}"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/${TIMESTAMP}.sql.gz"
|
||||
|
||||
# ---------- 日志函数 ----------
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
}
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
log "[INFO] 开始备份服务: ${SERVICE} (保留 ${KEEP_DAYS} 天)"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
log "[INFO] 执行 mysqldump -> ${BACKUP_FILE}"
|
||||
if ! mysqldump \
|
||||
--host="$MYSQL_HOST" \
|
||||
--port="$MYSQL_PORT" \
|
||||
--user="$MYSQL_USER" \
|
||||
--password="$MYSQL_PASSWORD" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--databases "$SERVICE" \
|
||||
2>/dev/null \
|
||||
| gzip -c > "$BACKUP_FILE"; then
|
||||
log "[ERROR] mysqldump 失败 (service=${SERVICE})"
|
||||
rm -f "$BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 校验产物非空
|
||||
if [[ ! -s "$BACKUP_FILE" ]]; then
|
||||
log "[ERROR] 备份文件为空: ${BACKUP_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_SIZE="$(stat -c %s "$BACKUP_FILE" 2>/dev/null || stat -f %z "$BACKUP_FILE")"
|
||||
log "[INFO] 备份完成: ${BACKUP_FILE} (${FILE_SIZE} bytes)"
|
||||
|
||||
# ---------- 清理过期备份 ----------
|
||||
log "[INFO] 清理超过 ${KEEP_DAYS} 天的旧备份"
|
||||
find "$BACKUP_DIR" -type f -name '*.sql.gz' -mtime +${KEEP_DAYS} -print -delete \
|
||||
| while read -r old_file; do
|
||||
log "[INFO] 已删除: ${old_file}"
|
||||
done
|
||||
|
||||
log "[INFO] 备份流程结束: ${SERVICE}"
|
||||
exit 0
|
||||
94
infra/backup/restore-mysql.sh
Normal file
94
infra/backup/restore-mysql.sh
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# MySQL 恢复脚本(Linux 容器内运行)
|
||||
# 用法: restore-mysql.sh --service <name> --file <path> [--yes]
|
||||
set -euo pipefail
|
||||
|
||||
# ---------- 参数解析 ----------
|
||||
SERVICE=""
|
||||
BACKUP_FILE=""
|
||||
ASSUME_YES=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--service)
|
||||
SERVICE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--file)
|
||||
BACKUP_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
ASSUME_YES=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 --service <name> --file <path> [--yes]" >&2
|
||||
echo " --service 目标微服务数据库名(必填)" >&2
|
||||
echo " --file 备份文件路径 sql.gz(必填)" >&2
|
||||
echo " --yes 跳过确认提示" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] 未知参数: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- 参数校验 ----------
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
echo "[ERROR] --service 参数必填" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$BACKUP_FILE" ]]; then
|
||||
echo "[ERROR] --file 参数必填" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "[ERROR] 备份文件不存在: ${BACKUP_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 环境变量 ----------
|
||||
: "${MYSQL_HOST:=mysql}"
|
||||
: "${MYSQL_PORT:=3306}"
|
||||
: "${MYSQL_USER:=root}"
|
||||
: "${MYSQL_PASSWORD:?MYSQL_PASSWORD 环境变量未设置}"
|
||||
|
||||
# ---------- 日志函数 ----------
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
}
|
||||
|
||||
# ---------- 确认提示 ----------
|
||||
log "[WARN] 即将向数据库 [${SERVICE}] 导入备份文件: ${BACKUP_FILE}"
|
||||
log "[WARN] 此操作会覆盖目标数据库现有数据,不可撤销!"
|
||||
|
||||
if [[ "$ASSUME_YES" != "true" ]]; then
|
||||
read -r -p "确认继续? (输入 YES 继续): " confirm
|
||||
if [[ "$confirm" != "YES" ]]; then
|
||||
log "[INFO] 用户取消操作"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- 主流程 ----------
|
||||
log "[INFO] 开始恢复服务: ${SERVICE}"
|
||||
|
||||
log "[INFO] 解压并导入: ${BACKUP_FILE}"
|
||||
if ! gunzip -c "$BACKUP_FILE" \
|
||||
| mysql \
|
||||
--host="$MYSQL_HOST" \
|
||||
--port="$MYSQL_PORT" \
|
||||
--user="$MYSQL_USER" \
|
||||
--password="$MYSQL_PASSWORD" \
|
||||
"$SERVICE"; then
|
||||
log "[ERROR] mysql 导入失败 (service=${SERVICE})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "[INFO] 恢复完成: ${SERVICE} <- ${BACKUP_FILE}"
|
||||
exit 0
|
||||
112
infra/backup/test-backup-mysql.sh
Normal file
112
infra/backup/test-backup-mysql.sh
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# backup-mysql.sh 参数解析与校验的 dry-run 测试
|
||||
# 不实际执行 mysqldump,仅验证参数解析、必填校验、默认值、帮助信息等。
|
||||
#
|
||||
# 运行方式(需要 bash 环境,如 Git Bash / WSL):
|
||||
# bash infra/backup/test-backup-mysql.sh
|
||||
#
|
||||
# 退出码:0 表示全部通过,非 0 表示有失败用例。
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT="${SCRIPT_DIR}/backup-mysql.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# assertExit <expected_code> <description> <actual_code> <stderr>
|
||||
assertExit() {
|
||||
local expected="$1" desc="$2" actual="$3" stderr="${4:-}"
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
echo "[PASS] $desc (exit=$actual)"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "[FAIL] $desc: 期望 exit=$expected, 实际 exit=$actual"
|
||||
[[ -n "$stderr" ]] && echo " stderr: $stderr"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# assertContains <needle> <description> <haystack>
|
||||
assertContains() {
|
||||
local needle="$1" desc="$2" haystack="${3:-}"
|
||||
if [[ "$haystack" == *"$needle"* ]]; then
|
||||
echo "[PASS] $desc"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "[FAIL] $desc: 输出应包含 '$needle'"
|
||||
echo " 实际输出: $haystack"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== backup-mysql.sh dry-run 测试 ==="
|
||||
|
||||
# ---------- 用例 1: 无参数应失败并提示 --service 必填 ----------
|
||||
output="$("$SCRIPT" 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "无参数应退出 1" "$code" "$output"
|
||||
assertContains "--service 参数必填" "无参数应提示 --service 必填" "$output"
|
||||
|
||||
# ---------- 用例 2: --help 应退出 0 并输出 Usage ----------
|
||||
output="$("$SCRIPT" --help 2>&1)"
|
||||
code=$?
|
||||
assertExit 0 "--help 应退出 0" "$code" "$output"
|
||||
assertContains "Usage:" "--help 应输出 Usage" "$output"
|
||||
assertContains "--service" "--help 应说明 --service 参数" "$output"
|
||||
assertContains "--keep" "--help 应说明 --keep 参数" "$output"
|
||||
|
||||
# ---------- 用例 3: --keep 默认值(未提供时)应在日志中体现 7 天 ----------
|
||||
# 设置 MYSQL_PASSWORD 让脚本通过环境变量校验,进入主流程打印日志;
|
||||
# 随后会在 mysqldump 阶段失败(CI 环境无 mysqldump)。
|
||||
output="$(MYSQL_PASSWORD=fake-pass "$SCRIPT" --service iam 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "无 mysqldump 时应最终退出 1" "$code" "$output"
|
||||
assertContains "保留 7 天" "未提供 --keep 时应使用默认值 7 天" "$output"
|
||||
|
||||
# ---------- 用例 4: --keep 非数字应失败 ----------
|
||||
output="$("$SCRIPT" --service iam --keep abc 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "--keep 非数字应退出 1" "$code" "$output"
|
||||
assertContains "--keep 必须为正整数" "非数字 --keep 应报错" "$output"
|
||||
|
||||
# ---------- 用例 5: --keep 小于 1 应失败 ----------
|
||||
output="$("$SCRIPT" --service iam --keep 0 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "--keep=0 应退出 1" "$code" "$output"
|
||||
assertContains "--keep 必须为正整数" "--keep=0 应报错" "$output"
|
||||
|
||||
# ---------- 用例 6: 未知参数应失败 ----------
|
||||
output="$("$SCRIPT" --service iam --unknown-flag 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "未知参数应退出 1" "$code" "$output"
|
||||
assertContains "未知参数" "未知参数应报错" "$output"
|
||||
|
||||
# ---------- 用例 7: --keep 自定义值应在日志中体现 ----------
|
||||
output="$(MYSQL_PASSWORD=fake-pass "$SCRIPT" --service iam --keep 30 2>&1)"
|
||||
code=$?
|
||||
assertExit 1 "无 mysqldump 时应最终退出 1(用例 7 预置)" "$code" "$output"
|
||||
assertContains "保留 30 天" "--keep=30 应在日志中体现" "$output"
|
||||
|
||||
# ---------- 用例 8: MYSQL_PASSWORD 已设置但 mysqldump 不可用时应优雅失败 ----------
|
||||
# 此用例验证:参数校验通过后,脚本会尝试调用 mysqldump;若 mysqldump 不存在则失败。
|
||||
# 在无 MySQL 容器的 CI 环境中,这是最接近真实 dry-run 的验证。
|
||||
output="$(MYSQL_PASSWORD=fake-pass "$SCRIPT" --service iam 2>&1)"
|
||||
code=$?
|
||||
# mysqldump 不存在时,pipefail + gzip 写入空文件 → 脚本检测到空文件后 exit 1
|
||||
# 或 mysqldump 命令未找到 → set -e 触发 exit 1
|
||||
# 任一路径都应是非 0 退出
|
||||
if [[ "$code" -ne 0 ]]; then
|
||||
echo "[PASS] mysqldump 不可用时应非 0 退出 (exit=$code)"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "[FAIL] mysqldump 不可用时不应返回 0"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
echo "=== 测试结果 ==="
|
||||
echo "通过: $PASS 失败: $FAIL"
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
45
infra/chaos/README.md
Normal file
45
infra/chaos/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 混沌工程说明
|
||||
|
||||
## 适用范围
|
||||
|
||||
- **仅 Staging 环境**:禁止在生产环境运行混沌实验
|
||||
- **每月 1 次演练**:建议每月第一个工作周执行
|
||||
- **演练窗口**:业务低峰期(02:00 - 05:00)
|
||||
|
||||
## 实验清单
|
||||
|
||||
| 实验 | 注入故障 | 验证目标 |
|
||||
|------|----------|----------|
|
||||
| `pod-delete` | 杀死 50% Pod | 自愈能力、副本数冗余 |
|
||||
| `pod-network-latency` | 注入 200ms 延迟 | 超时与重试、降级策略 |
|
||||
| `disk-fill` | 填充磁盘至 80% | 日志写入、磁盘告警 |
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. Staging 环境已部署完整监控(Prometheus + Alertmanager + Grafana)
|
||||
2. 已配置稳态探针(`steadyStateHypothesis`)
|
||||
3. 已配置告警通道并验证可达
|
||||
4. 演练参与者已就位(运维 + 研发 oncall)
|
||||
|
||||
## 演练流程
|
||||
|
||||
1. **演练前 30 分钟**:通知相关人员,确认稳态基线
|
||||
2. **启动实验**:按顺序执行,单实验单次注入
|
||||
3. **观察监控**:关注告警是否按预期触发、服务是否自愈
|
||||
4. **演练后**:归档实验结果,更新本目录 README
|
||||
|
||||
## 回滚预案
|
||||
|
||||
- **5 分钟内未恢复自动回滚**:通过 Litmus `steadyStateHypothesis` 失败触发实验终止
|
||||
- 手动回滚:
|
||||
```bash
|
||||
kubectl delete chaosengine edu-chaos-experiments -n edu-monitoring
|
||||
kubectl rollout restart deployment/api-gateway -n edu-services
|
||||
```
|
||||
- 极端情况:直接缩容 / 扩容受影响 Deployment
|
||||
|
||||
## 安全约束
|
||||
|
||||
- 每次实验**只注入一种故障**,避免叠加影响判断
|
||||
- 实验前快照数据库(参考 `infra/backup/backup-cron.sh`)
|
||||
- 实验期间禁止发布任何业务变更
|
||||
139
infra/chaos/experiments.yaml
Normal file
139
infra/chaos/experiments.yaml
Normal file
@@ -0,0 +1,139 @@
|
||||
# 混沌实验配置(Litmus Chaos 骨架)
|
||||
# 仅作 YAML 骨架,注释说明用途。实际运行需配合 Litmus Chaos Operator。
|
||||
# 文档:https://litmuschaos.github.io/litmus/
|
||||
apiVersion: litmuschaos.io/v1alpha1
|
||||
kind: ChaosEngine
|
||||
metadata:
|
||||
name: edu-chaos-experiments
|
||||
namespace: edu-monitoring
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: chaos
|
||||
spec:
|
||||
appinfo:
|
||||
appns: edu-services
|
||||
applabel: "app.kubernetes.io/name=api-gateway"
|
||||
appkind: deployment
|
||||
chaosServiceAccount: litmus-chaos-sa
|
||||
components:
|
||||
experiments:
|
||||
# ============================================================
|
||||
# 实验 1:服务 Pod 杀死(验证自愈与副本数)
|
||||
# ============================================================
|
||||
- name: pod-delete
|
||||
spec:
|
||||
components:
|
||||
env:
|
||||
# 杀死 1 个 Pod
|
||||
- name: TOTAL_CHAOS_DURATION
|
||||
value: "30"
|
||||
- name: CHAOS_INTERVAL
|
||||
value: "10"
|
||||
- name: FORCE
|
||||
value: "false"
|
||||
- name: PODS_AFFECTED_PERC
|
||||
value: "50"
|
||||
- name: TARGET_CONTAINER
|
||||
value: "api-gateway"
|
||||
# 假设:杀死 50% Pod 后,服务仍可对外可用(最少 1 个副本健康)
|
||||
# 稳态:up{job="api-gateway"} >= 1
|
||||
steadyStateHypothesis:
|
||||
steadyStateHypothesis:
|
||||
probe:
|
||||
- name: api-gateway-still-up
|
||||
type: httpProbe
|
||||
mode: Continuous
|
||||
runProperties:
|
||||
probeTimeout: 5
|
||||
httpProbeInputs:
|
||||
url: http://api-gateway.edu-services.svc:8080/healthz
|
||||
method:
|
||||
get: {}
|
||||
phases:
|
||||
- name: pre-chaos
|
||||
description: "混沌前稳态验证"
|
||||
- name: inject
|
||||
description: "注入 Pod 删除"
|
||||
- name: post-chaos
|
||||
description: "混沌后自愈验证"
|
||||
# ============================================================
|
||||
# 实验 2:网络延迟注入(验证超时与重试)
|
||||
# ============================================================
|
||||
- name: pod-network-latency
|
||||
spec:
|
||||
components:
|
||||
env:
|
||||
# 注入 200ms 网络延迟
|
||||
- name: NETWORK_LATENCY
|
||||
value: "200"
|
||||
- name: TOTAL_CHAOS_DURATION
|
||||
value: "60"
|
||||
- name: CHAOS_INTERVAL
|
||||
value: "10"
|
||||
- name: NETWORK_INTERFACE
|
||||
value: "eth0"
|
||||
- name: TARGET_CONTAINER
|
||||
value: "api-gateway"
|
||||
# 假设:200ms 延迟下 P99 < 2s,无 5xx 雪崩
|
||||
# 稳态:错误率 < 5%
|
||||
steadyStateHypothesis:
|
||||
steadyStateHypothesis:
|
||||
probe:
|
||||
- name: error-rate-below-threshold
|
||||
type: promProbe
|
||||
mode: Continuous
|
||||
runProperties:
|
||||
probeTimeout: 5
|
||||
promProbeInputs:
|
||||
source:
|
||||
url: http://prometheus.edu-monitoring.svc:9090
|
||||
query: |
|
||||
sum(rate(http_requests_total{service="api-gateway",status=~"5.."}[1m]))
|
||||
/ sum(rate(http_requests_total{service="api-gateway"}[1m]))
|
||||
comparator:
|
||||
criteria: "<="
|
||||
value: "0.05"
|
||||
phases:
|
||||
- name: pre-chaos
|
||||
description: "混沌前稳态验证"
|
||||
- name: inject
|
||||
description: "注入 200ms 网络延迟"
|
||||
- name: post-chaos
|
||||
description: "混沌后恢复验证"
|
||||
# ============================================================
|
||||
# 实验 3:磁盘填充(验证磁盘压力下日志写入与告警)
|
||||
# ============================================================
|
||||
- name: disk-fill
|
||||
spec:
|
||||
components:
|
||||
env:
|
||||
# 填充至 80% 磁盘使用率
|
||||
- name: FILL_PERCENTAGE
|
||||
value: "80"
|
||||
- name: TOTAL_CHAOS_DURATION
|
||||
value: "120"
|
||||
- name: CHAOS_INTERVAL
|
||||
value: "30"
|
||||
- name: TARGET_CONTAINER
|
||||
value: "api-gateway"
|
||||
# 假设:磁盘 80% 使用率下服务仍可写入日志,触发 DiskSpaceLow 告警
|
||||
# 稳态:服务 /healthz 可用
|
||||
steadyStateHypothesis:
|
||||
steadyStateHypothesis:
|
||||
probe:
|
||||
- name: api-gateway-healthz
|
||||
type: httpProbe
|
||||
mode: Continuous
|
||||
runProperties:
|
||||
probeTimeout: 5
|
||||
httpProbeInputs:
|
||||
url: http://api-gateway.edu-services.svc:8080/healthz
|
||||
method:
|
||||
get: {}
|
||||
phases:
|
||||
- name: pre-chaos
|
||||
description: "混沌前稳态验证"
|
||||
- name: inject
|
||||
description: "填充磁盘至 80%"
|
||||
- name: post-chaos
|
||||
description: "混沌后清理与恢复验证"
|
||||
119
infra/docker-compose.monitoring.yml
Normal file
119
infra/docker-compose.monitoring.yml
Normal file
@@ -0,0 +1,119 @@
|
||||
# 监控栈 docker-compose(独立 profile)
|
||||
# 使用方式: docker compose -f docker-compose.monitoring.yml --profile monitoring up -d
|
||||
version: "3.9"
|
||||
|
||||
networks:
|
||||
edu-network:
|
||||
name: edu-network
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
name: edu-prometheus-data
|
||||
alertmanager-data:
|
||||
name: edu-alertmanager-data
|
||||
grafana-data:
|
||||
name: edu-grafana-data
|
||||
|
||||
services:
|
||||
# ============================================================
|
||||
# Prometheus - 指标采集与存储
|
||||
# ============================================================
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.54.1
|
||||
container_name: edu-prometheus
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=15d"
|
||||
- "--web.enable-lifecycle"
|
||||
- "--web.enable-admin-api"
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./prometheus/rules.yml:/etc/prometheus/rules.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
networks:
|
||||
- edu-network
|
||||
|
||||
# ============================================================
|
||||
# Alertmanager - 告警路由与抑制
|
||||
# ============================================================
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.27.0
|
||||
container_name: edu-alertmanager
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/alertmanager/alertmanager.yml"
|
||||
- "--storage.path=/alertmanager"
|
||||
ports:
|
||||
- "9093:9093"
|
||||
volumes:
|
||||
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
||||
- alertmanager-data:/alertmanager
|
||||
networks:
|
||||
- edu-network
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
# ============================================================
|
||||
# Grafana - 可视化
|
||||
# ============================================================
|
||||
grafana:
|
||||
image: grafana/grafana:11.2.2
|
||||
container_name: edu-grafana
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=false
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
networks:
|
||||
- edu-network
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
# ============================================================
|
||||
# node-exporter - 主机指标采集
|
||||
# ============================================================
|
||||
node-exporter:
|
||||
image: prom/node-exporter:v1.8.2
|
||||
container_name: edu-node-exporter
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--path.rootfs=/host"
|
||||
ports:
|
||||
- "9100:9100"
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/host:ro
|
||||
networks:
|
||||
- edu-network
|
||||
|
||||
# ============================================================
|
||||
# blackbox-exporter - 黑盒探测(HTTP / TCP / ICMP)
|
||||
# ============================================================
|
||||
blackbox-exporter:
|
||||
image: prom/blackbox-exporter:v0.25.0
|
||||
container_name: edu-blackbox-exporter
|
||||
profiles: ["monitoring"]
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9115:9115"
|
||||
volumes:
|
||||
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
|
||||
networks:
|
||||
- edu-network
|
||||
394
infra/grafana/dashboards/microservices-overview.json
Normal file
394
infra/grafana/dashboards/microservices-overview.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Edu 平台微服务总览 - QPS / 错误率 / P99 延迟 / 熔断器状态",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "reqps"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "sum(rate(http_requests_total[1m])) by (service)",
|
||||
"legendFormat": "{{service}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "请求 QPS(按服务)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 20,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "line"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service)",
|
||||
"legendFormat": "{{service}} 5xx",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "错误率(5xx 占比)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "line"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))",
|
||||
"legendFormat": "{{service}} P99",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "P99 延迟",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"text": "CLOSED"
|
||||
},
|
||||
"1": {
|
||||
"color": "red",
|
||||
"index": 1,
|
||||
"text": "OPEN"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto"
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"expr": "max(circuit_breaker_state{state=\"open\"}) by (service)",
|
||||
"legendFormat": "{{service}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "熔断器状态(0=CLOSED, 1=OPEN)",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"style": "dark",
|
||||
"tags": ["edu", "microservices", "overview"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Edu 微服务总览",
|
||||
"uid": "edu-microservices-overview",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
15
infra/grafana/provisioning/dashboards/dashboards.yml
Normal file
15
infra/grafana/provisioning/dashboards/dashboards.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Grafana 仪表盘 provisioning
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: edu-dashboards
|
||||
orgId: 1
|
||||
folder: "Edu"
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 30
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
# 从该目录加载所有仪表盘 JSON
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: false
|
||||
15
infra/grafana/provisioning/datasources/prometheus.yml
Normal file
15
infra/grafana/provisioning/datasources/prometheus.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Grafana 数据源 provisioning
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
uid: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: false
|
||||
jsonData:
|
||||
timeInterval: "15s"
|
||||
httpMethod: POST
|
||||
manageAlerts: false
|
||||
118
infra/k8s/README.md
Normal file
118
infra/k8s/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# K8s 部署说明
|
||||
|
||||
> **状态**:已迁移到 Helm Chart 管理。本目录保留 `namespace.yaml` 作为基础资源,其余资源通过 `helm/` 下的 chart 部署。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
infra/k8s/
|
||||
├─ namespace.yaml # 基础命名空间(4 个:system/services/monitoring/ingress)
|
||||
└─ helm/ # Helm Chart 仓库
|
||||
├─ edu-platform/ # 平台级 chart(namespace/configmap/secret/ingress/hpa)
|
||||
│ ├─ Chart.yaml
|
||||
│ ├─ values.yaml # 全局默认值
|
||||
│ ├─ values-dev.yaml # 开发环境覆盖
|
||||
│ ├─ values-staging.yaml # 预发布环境覆盖
|
||||
│ ├─ values-prod.yaml # 生产环境覆盖
|
||||
│ └─ templates/
|
||||
│ ├─ _helpers.tpl
|
||||
│ ├─ namespace.yaml
|
||||
│ ├─ configmap.yaml
|
||||
│ ├─ secret.yaml # 骨架;生产请用 External Secrets Operator
|
||||
│ ├─ ingress.yaml
|
||||
│ └─ hpa.yaml # 全局 HPA 示例(默认不渲染)
|
||||
├─ api-gateway/ # 服务级 chart(完整迁移自原 manifest)
|
||||
│ ├─ Chart.yaml
|
||||
│ ├─ values.yaml
|
||||
│ └─ templates/
|
||||
│ ├─ _helpers.tpl
|
||||
│ ├─ deployment.yaml
|
||||
│ ├─ service.yaml
|
||||
│ ├─ configmap.yaml
|
||||
│ └─ hpa.yaml
|
||||
├─ iam/ # 业务服务 chart 桩(P2)
|
||||
├─ core-edu/ # 业务服务 chart 桩(P3)
|
||||
├─ content/ # 业务服务 chart 桩(P4)
|
||||
├─ msg/ # 业务服务 chart 桩(P5)
|
||||
├─ data-ana/ # 业务服务 chart 桩(P4)
|
||||
└─ ai/ # 业务服务 chart 桩(P5)
|
||||
```
|
||||
|
||||
## 命名空间规划
|
||||
|
||||
| Namespace | 用途 |
|
||||
| ---------------- | ---------------------------------------------------------- |
|
||||
| `edu-system` | 系统组件(数据库代理、配置等) |
|
||||
| `edu-services` | 业务微服务(api-gateway / iam / core-edu / content / msg) |
|
||||
| `edu-monitoring` | 监控栈(Prometheus / Grafana / Alertmanager) |
|
||||
| `edu-ingress` | 入口控制器(NGINX Ingress / cert-manager) |
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 1. 安装平台级 chart(命名空间 / 全局 ConfigMap / Secret / Ingress)
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
helm install edu-platform ./helm/edu-platform -f ./helm/edu-platform/values-dev.yaml
|
||||
|
||||
# 生产环境
|
||||
helm install edu-platform ./helm/edu-platform -f ./helm/edu-platform/values-prod.yaml \
|
||||
--set secret.data.MYSQL_PASSWORD=<base64> \
|
||||
--set secret.data.JWT_SECRET=<base64> \
|
||||
--set secret.data.REDIS_PASSWORD=<base64>
|
||||
```
|
||||
|
||||
### 2. 安装服务级 chart
|
||||
|
||||
```bash
|
||||
# api-gateway
|
||||
helm install api-gateway ./helm/api-gateway
|
||||
|
||||
# 其他业务服务(iam / core-edu / content / msg / data-ana / ai)
|
||||
helm install iam ./helm/iam
|
||||
helm install core-edu ./helm/core-edu
|
||||
# ...
|
||||
```
|
||||
|
||||
### 3. 应用基础命名空间(如未通过 helm 安装 edu-platform)
|
||||
|
||||
```bash
|
||||
kubectl apply -f namespace.yaml
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# lint 所有 chart
|
||||
helm lint helm/edu-platform helm/api-gateway helm/iam helm/core-edu helm/content helm/msg helm/data-ana helm/ai
|
||||
|
||||
# 渲染模板(不实际部署)
|
||||
helm template edu-platform ./helm/edu-platform
|
||||
helm template api-gateway ./helm/api-gateway
|
||||
```
|
||||
|
||||
## 环境差异
|
||||
|
||||
| 环境 | values 文件 | 副本数 | HPA | TLS |
|
||||
| ------- | ------------------- | ------ | ---- | ---- |
|
||||
| dev | values-dev.yaml | 1 | 关闭 | 关闭 |
|
||||
| staging | values-staging.yaml | 2 | 2-5 | 开启 |
|
||||
| prod | values-prod.yaml | 3 | 3-20 | 开启 |
|
||||
|
||||
## 敏感配置
|
||||
|
||||
⚠️ **生产环境禁止在 values.yaml 中硬编码密钥**。
|
||||
|
||||
推荐方案:
|
||||
|
||||
1. 使用 [External Secrets Operator](https://external-secrets.io/) 对接 Vault / KMS / 云 KMS
|
||||
2. 通过 `--set secret.data.<KEY>=<base64>` 临时注入
|
||||
3. 通过 ArgoCD / Flux GitOps + Sealed Secrets
|
||||
|
||||
## 后续路线
|
||||
|
||||
- [ ] 接入 External Secrets Operator
|
||||
- [ ] ArgoCD / Flux GitOps 部署
|
||||
- [ ] 各服务 chart 补充 configmap.yaml / hpa.yaml 模板(当前桩仅含 deployment/service)
|
||||
- [ ] 服务级 values-dev/staging/prod 覆盖文件
|
||||
- [ ] CI/CD 集成(helm chart 推送到 OCI registry)
|
||||
11
infra/k8s/helm/ai/Chart.yaml
Normal file
11
infra/k8s/helm/ai/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: ai
|
||||
description: ai 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- ai
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/ai/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/ai/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "ai.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ai.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ai.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "ai.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: ai-gateway
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ai.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "ai.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/ai/templates/deployment.yaml
Normal file
77
infra/k8s/helm/ai/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "ai.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "ai.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "ai.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "ai.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: ai-gateway
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "ai.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "ai.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/ai/templates/service.yaml
Normal file
16
infra/k8s/helm/ai/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "ai.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "ai.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "ai.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/ai/values.yaml
Normal file
85
infra/k8s/helm/ai/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# ai 服务默认值(AI 网关 - 业务领域 D6)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/ai
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3006
|
||||
targetPort: 3006
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3006
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
12
infra/k8s/helm/api-gateway/Chart.yaml
Normal file
12
infra/k8s/helm/api-gateway/Chart.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v2
|
||||
name: api-gateway
|
||||
description: api-gateway 服务级 Helm Chart(从 infra/k8s/api-gateway-deployment.yaml 迁移)
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- gateway
|
||||
- api
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/api-gateway/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/api-gateway/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "api-gateway.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "api-gateway.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "api-gateway.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "api-gateway.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: gateway
|
||||
{{- end -}}
|
||||
|
||||
{{- define "api-gateway.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "api-gateway.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
13
infra/k8s/helm/api-gateway/templates/configmap.yaml
Normal file
13
infra/k8s/helm/api-gateway/templates/configmap.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.configMap.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}-config
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
{{ $k }}: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
77
infra/k8s/helm/api-gateway/templates/deployment.yaml
Normal file
77
infra/k8s/helm/api-gateway/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "api-gateway.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "api-gateway.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: gateway
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "api-gateway.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "api-gateway.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
29
infra/k8s/helm/api-gateway/templates/hpa.yaml
Normal file
29
infra/k8s/helm/api-gateway/templates/hpa.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- if .Values.hpa.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}-hpa
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "api-gateway.name" . }}
|
||||
minReplicas: {{ .Values.hpa.minReplicas }}
|
||||
maxReplicas: {{ .Values.hpa.maxReplicas }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
16
infra/k8s/helm/api-gateway/templates/service.yaml
Normal file
16
infra/k8s/helm/api-gateway/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "api-gateway.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "api-gateway.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "api-gateway.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
86
infra/k8s/helm/api-gateway/values.yaml
Normal file
86
infra/k8s/helm/api-gateway/values.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
# api-gateway 服务默认值
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/api-gateway
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 8080
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
MYSQL_HOST: mysql.edu-system.svc.cluster.local
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/content/Chart.yaml
Normal file
11
infra/k8s/helm/content/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: content
|
||||
description: content 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- content
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/content/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/content/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "content.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "content.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "content.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "content.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: content
|
||||
{{- end -}}
|
||||
|
||||
{{- define "content.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "content.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/content/templates/deployment.yaml
Normal file
77
infra/k8s/helm/content/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "content.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "content.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "content.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "content.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: content
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "content.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "content.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/content/templates/service.yaml
Normal file
16
infra/k8s/helm/content/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "content.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "content.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "content.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/content/values.yaml
Normal file
85
infra/k8s/helm/content/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# content 服务默认值(内容资源 - 业务领域 D4)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/content
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3003
|
||||
targetPort: 3003
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3003
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/core-edu/Chart.yaml
Normal file
11
infra/k8s/helm/core-edu/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: core-edu
|
||||
description: core-edu 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- core-edu
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/core-edu/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/core-edu/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "core-edu.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "core-edu.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "core-edu.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "core-edu.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: teaching
|
||||
{{- end -}}
|
||||
|
||||
{{- define "core-edu.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "core-edu.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/core-edu/templates/deployment.yaml
Normal file
77
infra/k8s/helm/core-edu/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "core-edu.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "core-edu.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "core-edu.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "core-edu.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: teaching
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "core-edu.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "core-edu.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/core-edu/templates/service.yaml
Normal file
16
infra/k8s/helm/core-edu/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "core-edu.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "core-edu.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "core-edu.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/core-edu/values.yaml
Normal file
85
infra/k8s/helm/core-edu/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# core-edu 服务默认值(教学核心 - 业务领域 D2+D3)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/core-edu
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3002
|
||||
targetPort: 3002
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3002
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/data-ana/Chart.yaml
Normal file
11
infra/k8s/helm/data-ana/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: data-ana
|
||||
description: data-ana 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- data-ana
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/data-ana/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/data-ana/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "data-ana.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "data-ana.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "data-ana.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "data-ana.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: analytics
|
||||
{{- end -}}
|
||||
|
||||
{{- define "data-ana.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "data-ana.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/data-ana/templates/deployment.yaml
Normal file
77
infra/k8s/helm/data-ana/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "data-ana.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "data-ana.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "data-ana.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "data-ana.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: analytics
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "data-ana.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "data-ana.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/data-ana/templates/service.yaml
Normal file
16
infra/k8s/helm/data-ana/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "data-ana.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "data-ana.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "data-ana.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/data-ana/values.yaml
Normal file
85
infra/k8s/helm/data-ana/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# data-ana 服务默认值(数据分析 - 业务领域 D6)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/data-ana
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3005
|
||||
targetPort: 3005
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3005
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
12
infra/k8s/helm/edu-platform/Chart.yaml
Normal file
12
infra/k8s/helm/edu-platform/Chart.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v2
|
||||
name: edu-platform
|
||||
description: Edu 平台级 Helm Chart(命名空间 / 全局 ConfigMap / Secret / Ingress / HPA 模板)
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- platform
|
||||
- infrastructure
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
42
infra/k8s/helm/edu-platform/templates/_helpers.tpl
Normal file
42
infra/k8s/helm/edu-platform/templates/_helpers.tpl
Normal file
@@ -0,0 +1,42 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "edu-platform.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Fully qualified app name.
|
||||
*/}}
|
||||
{{- define "edu-platform.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels.
|
||||
*/}}
|
||||
{{- define "edu-platform.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "edu-platform.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- with .Values.global.labels }}
|
||||
{{ toYaml . }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels.
|
||||
*/}}
|
||||
{{- define "edu-platform.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "edu-platform.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
13
infra/k8s/helm/edu-platform/templates/configmap.yaml
Normal file
13
infra/k8s/helm/edu-platform/templates/configmap.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.configMap.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Values.configMap.name }}
|
||||
namespace: edu-system
|
||||
labels:
|
||||
{{- include "edu-platform.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
{{ $k }}: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
34
infra/k8s/helm/edu-platform/templates/hpa.yaml
Normal file
34
infra/k8s/helm/edu-platform/templates/hpa.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
{{/*
|
||||
全局 HPA 模板示例(参考用)。
|
||||
实际 HPA 应在各服务自身的 chart 中定义,以便针对服务特性调参。
|
||||
本模板在 edu-platform 中默认不渲染(hpa.targetService 为空时跳过)。
|
||||
*/}}
|
||||
{{- if and .Values.hpa.enabled .Values.hpa.targetService }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ .Values.hpa.targetService }}-hpa
|
||||
namespace: edu-services
|
||||
labels:
|
||||
{{- include "edu-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ .Values.hpa.targetService }}
|
||||
minReplicas: {{ .Values.hpa.minReplicas }}
|
||||
maxReplicas: {{ .Values.hpa.maxReplicas }}
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.hpa.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
35
infra/k8s/helm/edu-platform/templates/ingress.yaml
Normal file
35
infra/k8s/helm/edu-platform/templates/ingress.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-ingress
|
||||
namespace: edu-ingress
|
||||
labels:
|
||||
{{- include "edu-platform.labels" $ | nindent 4 }}
|
||||
{{- with $.Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ $.Values.ingress.className }}
|
||||
{{- with $.Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ .service }}
|
||||
port:
|
||||
number: {{ .port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
10
infra/k8s/helm/edu-platform/templates/namespace.yaml
Normal file
10
infra/k8s/helm/edu-platform/templates/namespace.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
{{- range .Values.namespaces }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .name }}
|
||||
labels:
|
||||
{{- include "edu-platform.labels" $ | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
{{- end }}
|
||||
26
infra/k8s/helm/edu-platform/templates/secret.yaml
Normal file
26
infra/k8s/helm/edu-platform/templates/secret.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
{{- if .Values.secret.enabled }}
|
||||
{{- /*
|
||||
⚠️ 生产环境警告:不要在 values.yaml 中硬编码真实密钥!
|
||||
推荐方案:使用 External Secrets Operator 对接 Vault / KMS / 云 KMS
|
||||
本模板仅作骨架;空字符串字段不会被渲染(避免覆盖已存在的 Secret)。
|
||||
*/ -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Values.secret.name }}
|
||||
namespace: edu-system
|
||||
labels:
|
||||
{{- include "edu-platform.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install
|
||||
"helm.sh/hook-delete-policy": before-hook-creation
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $k, $v := .Values.secret.data }}
|
||||
{{- if $v }}
|
||||
{{ $k }}: {{ $v }}
|
||||
{{- else }}
|
||||
{{ $k }}: "" # 占位:部署时通过 --set 或 ExternalSecrets 注入
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
24
infra/k8s/helm/edu-platform/values-dev.yaml
Normal file
24
infra/k8s/helm/edu-platform/values-dev.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# 开发环境覆盖
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/environment: dev
|
||||
|
||||
configMap:
|
||||
data:
|
||||
NODE_ENV: development
|
||||
LOG_LEVEL: debug
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: edu-dev.local
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
tls: [] # 开发环境不强制 TLS
|
||||
|
||||
hpa:
|
||||
enabled: false # 开发环境单副本即可
|
||||
35
infra/k8s/helm/edu-platform/values-prod.yaml
Normal file
35
infra/k8s/helm/edu-platform/values-prod.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# 生产环境覆盖
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/environment: prod
|
||||
imagePullSecrets:
|
||||
- name: registry-credentials
|
||||
|
||||
configMap:
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: warn
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- host: edu.example.com
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
- path: /auth
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
tls:
|
||||
- secretName: edu-tls
|
||||
hosts:
|
||||
- edu.example.com
|
||||
|
||||
hpa:
|
||||
minReplicas: 3
|
||||
maxReplicas: 20
|
||||
targetCPUUtilizationPercentage: 60
|
||||
targetMemoryUtilizationPercentage: 70
|
||||
23
infra/k8s/helm/edu-platform/values-staging.yaml
Normal file
23
infra/k8s/helm/edu-platform/values-staging.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# 预发布环境覆盖
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/environment: staging
|
||||
|
||||
configMap:
|
||||
data:
|
||||
NODE_ENV: staging
|
||||
LOG_LEVEL: info
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- host: edu-staging.example.com
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
|
||||
hpa:
|
||||
minReplicas: 2
|
||||
maxReplicas: 5
|
||||
72
infra/k8s/helm/edu-platform/values.yaml
Normal file
72
infra/k8s/helm/edu-platform/values.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
# Edu 平台全局默认值
|
||||
# 环境覆盖文件:values-dev.yaml / values-staging.yaml / values-prod.yaml
|
||||
|
||||
# 全局标签(自动注入到所有资源)
|
||||
global:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
# 镜像拉取凭证(生产建议使用 Secret + serviceAccount)
|
||||
imagePullSecrets: []
|
||||
|
||||
# 命名空间规划
|
||||
namespaces:
|
||||
- name: edu-system
|
||||
component: system
|
||||
- name: edu-services
|
||||
component: services
|
||||
- name: edu-monitoring
|
||||
component: monitoring
|
||||
- name: edu-ingress
|
||||
component: ingress
|
||||
|
||||
# 全局 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
name: edu-platform-config
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
MYSQL_HOST: mysql.edu-system.svc.cluster.local
|
||||
REDIS_HOST: redis.edu-system.svc.cluster.local
|
||||
KAFKA_BROKERS: kafka.edu-system.svc.cluster.local:9092
|
||||
|
||||
# 全局 Secret(仅骨架;生产请使用 External Secrets Operator 对接 Vault/KMS)
|
||||
secret:
|
||||
enabled: true
|
||||
name: edu-platform-secret
|
||||
# 真实部署时通过 --set secret.data.MYSQL_PASSWORD=<base64> 注入
|
||||
# 或通过 externalSecrets 引用
|
||||
data:
|
||||
MYSQL_PASSWORD: ""
|
||||
JWT_SECRET: ""
|
||||
REDIS_PASSWORD: ""
|
||||
|
||||
# 全局 Ingress
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: edu.example.com
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
- path: /auth
|
||||
pathType: Prefix
|
||||
service: api-gateway
|
||||
port: 8080
|
||||
tls:
|
||||
- secretName: edu-tls
|
||||
hosts:
|
||||
- edu.example.com
|
||||
|
||||
# HPA 全局默认策略(各服务可在自身 chart 中覆盖)
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
11
infra/k8s/helm/iam/Chart.yaml
Normal file
11
infra/k8s/helm/iam/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: iam
|
||||
description: iam 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- iam
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/iam/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/iam/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "iam.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "iam.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "iam.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "iam.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: identity
|
||||
{{- end -}}
|
||||
|
||||
{{- define "iam.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "iam.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/iam/templates/deployment.yaml
Normal file
77
infra/k8s/helm/iam/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "iam.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "iam.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "iam.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "iam.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: identity
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "iam.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "iam.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/iam/templates/service.yaml
Normal file
16
infra/k8s/helm/iam/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "iam.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "iam.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "iam.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/iam/values.yaml
Normal file
85
infra/k8s/helm/iam/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# iam 服务默认值(身份认证 - 业务领域 D1)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/iam
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3001
|
||||
targetPort: 3001
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3001
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
11
infra/k8s/helm/msg/Chart.yaml
Normal file
11
infra/k8s/helm/msg/Chart.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: msg
|
||||
description: msg 服务级 Helm Chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- edu
|
||||
- msg
|
||||
maintainers:
|
||||
- name: edu-arch
|
||||
29
infra/k8s/helm/msg/templates/_helpers.tpl
Normal file
29
infra/k8s/helm/msg/templates/_helpers.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{- define "msg.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "msg.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "msg.labels" -}}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
{{ include "msg.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: messaging
|
||||
{{- end -}}
|
||||
|
||||
{{- define "msg.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "msg.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
77
infra/k8s/helm/msg/templates/deployment.yaml
Normal file
77
infra/k8s/helm/msg/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "msg.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "msg.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "msg.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
rollingUpdate:
|
||||
maxSurge: {{ .Values.strategy.maxSurge }}
|
||||
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "msg.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: messaging
|
||||
{{- if .Values.metrics.enabled }}
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: {{ .Values.metrics.port | quote }}
|
||||
prometheus.io/path: {{ .Values.metrics.path | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ include "msg.name" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.configMap.enabled }}
|
||||
{{- range $k, $v := .Values.configMap.data }}
|
||||
- name: {{ $k }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "msg.name" $ }}-config
|
||||
key: {{ $k }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $secret := .Values.secretRefs }}
|
||||
{{- range $key := $secret.keys }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $secret.name }}
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
16
infra/k8s/helm/msg/templates/service.yaml
Normal file
16
infra/k8s/helm/msg/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "msg.name" . }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
{{- include "msg.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "msg.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
85
infra/k8s/helm/msg/values.yaml
Normal file
85
infra/k8s/helm/msg/values.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
# msg 服务默认值(消息通知 - 业务领域 D5)
|
||||
|
||||
# 副本数(生产建议 ≥ 2)
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: edu/msg
|
||||
tag: latest # 生产请固定 tag,避免 latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 服务端口
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3004
|
||||
targetPort: 3004
|
||||
|
||||
# 命名空间(默认 edu-services,由 edu-platform chart 创建)
|
||||
namespace: edu-services
|
||||
|
||||
# 探针配置
|
||||
probes:
|
||||
liveness:
|
||||
path: /healthz
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
path: /readyz
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 2
|
||||
|
||||
# 资源配额
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
# 滚动更新策略
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
|
||||
# Prometheus 指标采集
|
||||
metrics:
|
||||
enabled: true
|
||||
port: 3004
|
||||
path: /metrics
|
||||
|
||||
# 安全上下文
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
|
||||
# 服务级 ConfigMap(非敏感配置)
|
||||
configMap:
|
||||
enabled: true
|
||||
data:
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
|
||||
# HPA
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 70
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# 敏感配置(从 Secret 引用,Secret 由 edu-platform chart 或 ExternalSecrets 管理)
|
||||
secretRefs:
|
||||
- name: edu-platform-secret
|
||||
keys:
|
||||
- MYSQL_PASSWORD
|
||||
- JWT_SECRET
|
||||
- REDIS_PASSWORD
|
||||
33
infra/k8s/namespace.yaml
Normal file
33
infra/k8s/namespace.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
# K8s 命名空间定义 - Edu 平台
|
||||
# 划分 4 个命名空间:system / services / monitoring / ingress
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-system
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-services
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: services
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-monitoring
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: monitoring
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: edu-ingress
|
||||
labels:
|
||||
app.kubernetes.io/part-of: edu-platform
|
||||
app.kubernetes.io/component: ingress
|
||||
122
infra/prometheus/rules.yml
Normal file
122
infra/prometheus/rules.yml
Normal file
@@ -0,0 +1,122 @@
|
||||
# Prometheus 告警规则 - Edu 平台
|
||||
# 分组:服务可用性 / 性能 / 资源
|
||||
groups:
|
||||
# ============================================================
|
||||
# 服务可用性
|
||||
# ============================================================
|
||||
- name: service-availability
|
||||
rules:
|
||||
# 服务宕机
|
||||
- alert: ServiceDown
|
||||
expr: up == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: availability
|
||||
annotations:
|
||||
summary: "服务宕机 {{ $labels.instance }}"
|
||||
description: "目标 {{ $labels.job }} / {{ $labels.instance }} 已离线超过 1 分钟。"
|
||||
|
||||
# 熔断器打开
|
||||
- alert: CircuitBreakerOpen
|
||||
expr: circuit_breaker_state{state="open"} == 1
|
||||
for: 30s
|
||||
labels:
|
||||
severity: critical
|
||||
category: availability
|
||||
annotations:
|
||||
summary: "熔断器打开 {{ $labels.service }}"
|
||||
description: "服务 {{ $labels.service }} 熔断器处于 open 状态,可能正在拒绝请求。"
|
||||
|
||||
# ============================================================
|
||||
# 性能
|
||||
# ============================================================
|
||||
- name: performance
|
||||
rules:
|
||||
# 高错误率(5xx 占比 > 5%)
|
||||
- alert: HighErrorRate
|
||||
expr: |
|
||||
(sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
|
||||
/ sum(rate(http_requests_total[5m])) by (service))
|
||||
> 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: performance
|
||||
annotations:
|
||||
summary: "错误率过高 {{ $labels.service }}"
|
||||
description: "服务 {{ $labels.service }} 5xx 错误率超过 5%,持续 5 分钟。"
|
||||
|
||||
# P99 延迟过高
|
||||
- alert: HighLatencyP99
|
||||
expr: |
|
||||
histogram_quantile(0.99,
|
||||
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
|
||||
> 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: performance
|
||||
annotations:
|
||||
summary: "P99 延迟过高 {{ $labels.service }}"
|
||||
description: "服务 {{ $labels.service }} P99 延迟超过 1 秒,持续 5 分钟。"
|
||||
|
||||
# Kafka 消费者积压
|
||||
- alert: KafkaConsumerLag
|
||||
expr: kafka_consumergroup_lag > 1000
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: performance
|
||||
annotations:
|
||||
summary: "Kafka 消费者积压 {{ $labels.consumergroup }}"
|
||||
description: "消费组 {{ $labels.consumergroup }} 在 topic {{ $labels.topic }} 上积压超过 1000 条,持续 10 分钟。"
|
||||
|
||||
# ============================================================
|
||||
# 资源
|
||||
# ============================================================
|
||||
- name: resources
|
||||
rules:
|
||||
# MySQL 连接数过高
|
||||
- alert: MySQLConnectionsHigh
|
||||
expr: |
|
||||
(mysql_global_status_threads_connected
|
||||
/ mysql_global_variables_max_connections)
|
||||
> 0.8
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: resource
|
||||
annotations:
|
||||
summary: "MySQL 连接数过高 {{ $labels.instance }}"
|
||||
description: "MySQL 实例 {{ $labels.instance }} 连接数超过最大连接数的 80%,持续 5 分钟。"
|
||||
|
||||
# 磁盘空间不足
|
||||
- alert: DiskSpaceLow
|
||||
expr: |
|
||||
(100
|
||||
- (node_filesystem_avail_bytes
|
||||
/ node_filesystem_size_bytes * 100))
|
||||
> 85
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: resource
|
||||
annotations:
|
||||
summary: "磁盘空间不足 {{ $labels.instance }}"
|
||||
description: "节点 {{ $labels.instance }} 挂载点 {{ $labels.mountpoint }} 磁盘使用率超过 85%,持续 10 分钟。"
|
||||
|
||||
# 内存使用过高
|
||||
- alert: MemoryHigh
|
||||
expr: |
|
||||
(1
|
||||
- (node_memory_MemAvailable_bytes
|
||||
/ node_memory_MemTotal_bytes))
|
||||
> 0.9
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: resource
|
||||
annotations:
|
||||
summary: "内存使用过高 {{ $labels.instance }}"
|
||||
description: "节点 {{ $labels.instance }} 内存使用率超过 90%,持续 5 分钟。"
|
||||
50
infra/security/secrets.example.env
Normal file
50
infra/security/secrets.example.env
Normal file
@@ -0,0 +1,50 @@
|
||||
# Edu 平台密钥示例环境变量文件
|
||||
# ============================================================
|
||||
# 警告:本文件仅作示例,禁止包含真实密钥。
|
||||
# 生产环境请通过 K8s Secret / External Secrets / Vault 注入。
|
||||
# 复制为 .env 后用真实值替换所有 <placeholder>。
|
||||
|
||||
# ---------- MySQL ----------
|
||||
# 用途:MySQL root 用户密码,用于初始化与备份/恢复
|
||||
# 最小长度:32 字符
|
||||
# 复杂度:含大小写字母 + 数字 + 特殊符号
|
||||
MYSQL_ROOT_PASSWORD=<replace-with-32-char-strong-password>
|
||||
|
||||
# ---------- JWT ----------
|
||||
# 用途:JWT Access Token 签名密钥(HS256)
|
||||
# 最小长度:64 字符(建议使用 openssl rand -base64 48 生成)
|
||||
# 注意:旋转后所有已签发的 Access Token 立即失效
|
||||
JWT_SECRET=<replace-with-64-char-jwt-signing-secret>
|
||||
|
||||
# 用途:JWT Refresh Token 签名密钥(HS256)
|
||||
# 最小长度:64 字符
|
||||
# 注意:与 JWT_SECRET 必须不同;旋转后所有用户需重新登录
|
||||
JWT_REFRESH_SECRET=<replace-with-64-char-refresh-signing-secret>
|
||||
|
||||
# ---------- Kafka ----------
|
||||
# 用途:Kafka SASL/PLAIN 认证密码
|
||||
# 最小长度:24 字符
|
||||
KAFKA_SASL_PASSWORD=<replace-with-24-char-kafka-password>
|
||||
|
||||
# ---------- Elasticsearch ----------
|
||||
# 用途:Elasticsearch 内置 elastic 用户密码
|
||||
# 最小长度:24 字符
|
||||
ES_PASSWORD=<replace-with-24-char-es-password>
|
||||
|
||||
# ---------- Neo4j ----------
|
||||
# 用途:Neo4j 数据库管理员密码
|
||||
# 最小长度:24 字符
|
||||
NEO4J_PASSWORD=<replace-with-24-char-neo4j-password>
|
||||
|
||||
# ---------- Redis ----------
|
||||
# 用途:Redis ACL 默认用户密码
|
||||
# 最小长度:24 字符
|
||||
# 注意:生产环境建议启用 ACL,按用户分配最小权限
|
||||
REDIS_PASSWORD=<replace-with-24-char-redis-password>
|
||||
|
||||
# ---------- 应用层加密 ----------
|
||||
# 用途:应用层字段级加密密钥(AES-256-GCM)
|
||||
# 最小长度:32 字节(base64 编码后约 44 字符)
|
||||
# 生成:openssl rand -base64 32
|
||||
# 注意:旋转前需先解密所有已加密字段,旋转后重新加密
|
||||
ENCRYPTION_KEY=<replace-with-base64-32-byte-aes-key>
|
||||
64
infra/security/waf-rules.conf
Normal file
64
infra/security/waf-rules.conf
Normal file
@@ -0,0 +1,64 @@
|
||||
# ModSecurity 兼容 WAF 规则骨架 - Edu 平台
|
||||
# ============================================================
|
||||
# 说明:本文件为 ModSecurity SecRule 语法骨架,
|
||||
# 生产部署前请配合 OWASP CRS 使用并完成规则调优。
|
||||
# 参考:https://github.com/SpiderLabs/ModSecurity
|
||||
# https://github.com/coreruleset/coreruleset
|
||||
|
||||
# ---------- 基础配置 ----------
|
||||
# 启用规则引擎
|
||||
SecRuleEngine On
|
||||
|
||||
# 请求体大小限制(10MB)
|
||||
SecRequestBodyLimit 10485760
|
||||
|
||||
# 默认动作:记录日志 + 拒绝(生产前调整)
|
||||
SecDefaultAction "phase:2,log,deny,status:403"
|
||||
|
||||
# ============================================================
|
||||
# 规则 1:SQL 注入检测
|
||||
# 检测常见 SQL 注入关键字与元字符
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_BODY|XML:/* \
|
||||
"(?i)(union\s+select|select\s+.*\s+from|insert\s+into|update\s+.*\s+set|delete\s+from|drop\s+table|alter\s+table|create\s+table|exec\s*\(|;\s*drop\s|--\s|/\*.*\*/|or\s+1\s*=\s*1|and\s+1\s*=\s*1|'\s*or\s*'|sleep\s*\(|benchmark\s*\(|load_file\s*\(|into\s+outfile)" \
|
||||
"id:1001,phase:2,log,deny,status:403,msg:'SQL Injection attempt',tag:'attack-sqli',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 2:XSS 检测
|
||||
# 检测常见 XSS payload 与事件处理器
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_BODY|XML:/* \
|
||||
"(?i)(<script|</script>|javascript:|onerror\s*=|onload\s*=|onclick\s*=|onmouseover\s*=|onfocus\s*=|onblur\s*=|<iframe|<img[^>]+src\s*=|<svg[^>]+onload|document\.cookie|window\.location|eval\s*\(|alert\s*\(|prompt\s*\(|String\.fromCharCode)" \
|
||||
"id:1002,phase:2,log,deny,status:403,msg:'XSS attempt',tag:'attack-xss',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 3:路径遍历检测
|
||||
# 检测 ../ 与编码变体,防止读取敏感文件
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_FILENAME|REQUEST_URI|REQUEST_BODY \
|
||||
"(?i)(\.\./|\.\.\\|\.\.%2f|\.\.%5c|%2e%2e%2f|%2e%2e%5c|/etc/passwd|/etc/shadow|/etc/hosts|/proc/self/environ|c:\\windows\\win\.ini|c:\\boot\.ini|\.\./\.\./\.\./)" \
|
||||
"id:1003,phase:2,log,deny,status:403,msg:'Path Traversal attempt',tag:'attack-lfi',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 4:User-Agent 黑名单
|
||||
# 拦截已知恶意扫描器与爬虫
|
||||
# ============================================================
|
||||
SecRule REQUEST_HEADERS:User-Agent \
|
||||
"(?i)(sqlmap|nikto|nmap|nessus|acunetix|wpscan|hydra|metasploit|burpcollaborator|masscan|zgrab|dirbuster|gobuster|fuzzer|webinspect|appscan|jaeles|xrkmd)" \
|
||||
"id:1004,phase:1,log,deny,status:403,msg:'Malicious User-Agent blocked',tag:'attack-scanner',severity:WARNING"
|
||||
|
||||
# ============================================================
|
||||
# 规则 5:远程文件包含(RFI)检测
|
||||
# 检测通过 URL 参数引入远程文件
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES \
|
||||
"(?i)(^(http|https|ftp|php|data)://|(http|https|ftp|php|data)://.*\?(php|http|https|ftp|data)://)" \
|
||||
"id:1005,phase:2,log,deny,status:403,msg:'Remote File Inclusion attempt',tag:'attack-rfi',severity:CRITICAL"
|
||||
|
||||
# ============================================================
|
||||
# 规则 6:命令注入检测
|
||||
# 检测 shell 元字符与命令执行关键字
|
||||
# ============================================================
|
||||
SecRule ARGS|ARGS_NAMES|REQUEST_BODY|REQUEST_COOKIES \
|
||||
"(?i)(;\s*(ls|cat|id|whoami|uname|pwd|wget|curl|nc|bash|sh|python|perl|ruby)\s|\$\(|`|&&\s*\w+\s|\|\|\s*\w+\s|\|\s*(ls|cat|id|whoami|uname|pwd|wget|curl|nc|bash|sh|python|perl|ruby)\s|/bin/(ba)?sh|/bin/cat|/usr/bin/(wget|curl|python|perl))" \
|
||||
"id:1006,phase:2,log,deny,status:403,msg:'Command Injection attempt',tag:'attack-rce',severity:CRITICAL"
|
||||
@@ -1,7 +1,9 @@
|
||||
module.exports = {
|
||||
'*.{ts,tsx}': ['eslint --fix', 'prettier --write'],
|
||||
'*.{go,mod,sum}': ['gofmt -w', 'golangci-lint run --fix'],
|
||||
'*.{py}': ['ruff check --fix', 'ruff format'],
|
||||
module.exports = {
|
||||
// ESLint 9 flat config 迁移完成后恢复:['eslint --fix', 'prettier --write']
|
||||
'*.{ts,tsx}': ['prettier --write'],
|
||||
// Go 工具链不在 git hook PATH 中,Go 文件格式化由 go fmt 手动执行
|
||||
// golangci-lint 安装后恢复:['gofmt -w', 'golangci-lint run --fix']
|
||||
'*.py': ['ruff check --fix', 'ruff format'],
|
||||
'*.proto': ['buf format --write'],
|
||||
'*.md': ['prettier --write'],
|
||||
};
|
||||
|
||||
60
packages/shared-proto/proto/ai.proto
Normal file
60
packages/shared-proto/proto/ai.proto
Normal file
@@ -0,0 +1,60 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.ai.v1;
|
||||
|
||||
service AiService {
|
||||
rpc Chat(ChatRequest) returns (ChatResponse);
|
||||
rpc StreamChat(ChatRequest) returns (stream ChatChunk);
|
||||
rpc GenerateQuestion(GenerateQuestionRequest) returns (GeneratedQuestion);
|
||||
rpc OptimizeExpression(OptimizeExpressionRequest) returns (OptimizedExpression);
|
||||
}
|
||||
|
||||
message ChatRequest {
|
||||
repeated ChatMessage messages = 1;
|
||||
string model = 2;
|
||||
double temperature = 3;
|
||||
}
|
||||
|
||||
message ChatMessage {
|
||||
string role = 1;
|
||||
string content = 2;
|
||||
}
|
||||
|
||||
message ChatResponse {
|
||||
string content = 1;
|
||||
string model = 2;
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
message Usage {
|
||||
int32 prompt_tokens = 1;
|
||||
int32 completion_tokens = 2;
|
||||
int32 total_tokens = 3;
|
||||
}
|
||||
|
||||
message ChatChunk {
|
||||
string content = 1;
|
||||
bool done = 2;
|
||||
}
|
||||
|
||||
message GenerateQuestionRequest {
|
||||
string prompt = 1;
|
||||
string subject = 2;
|
||||
string difficulty = 3;
|
||||
}
|
||||
|
||||
message GeneratedQuestion {
|
||||
string question = 1;
|
||||
string answer = 2;
|
||||
string explanation = 3;
|
||||
}
|
||||
|
||||
message OptimizeExpressionRequest {
|
||||
string text = 1;
|
||||
string context = 2;
|
||||
}
|
||||
|
||||
message OptimizedExpression {
|
||||
string optimized = 1;
|
||||
repeated string suggestions = 2;
|
||||
}
|
||||
61
packages/shared-proto/proto/analytics.proto
Normal file
61
packages/shared-proto/proto/analytics.proto
Normal file
@@ -0,0 +1,61 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.analytics.v1;
|
||||
|
||||
service AnalyticsService {
|
||||
rpc GetClassPerformance(GetClassPerformanceRequest) returns (ClassPerformance);
|
||||
rpc GetStudentWeakness(GetStudentWeaknessRequest) returns (StudentWeakness);
|
||||
rpc GetLearningTrend(GetLearningTrendRequest) returns (LearningTrend);
|
||||
}
|
||||
|
||||
message GetClassPerformanceRequest {
|
||||
string class_id = 1;
|
||||
string subject_id = 2;
|
||||
int64 start_date = 3;
|
||||
int64 end_date = 4;
|
||||
}
|
||||
|
||||
message ClassPerformance {
|
||||
string class_id = 1;
|
||||
double average_score = 2;
|
||||
double pass_rate = 3;
|
||||
repeated StudentScore scores = 4;
|
||||
}
|
||||
|
||||
message StudentScore {
|
||||
string student_id = 1;
|
||||
double score = 2;
|
||||
string grade = 3;
|
||||
}
|
||||
|
||||
message GetStudentWeaknessRequest {
|
||||
string student_id = 1;
|
||||
string subject_id = 2;
|
||||
}
|
||||
|
||||
message StudentWeakness {
|
||||
string student_id = 1;
|
||||
repeated WeakPoint weak_points = 2;
|
||||
}
|
||||
|
||||
message WeakPoint {
|
||||
string knowledge_point_id = 1;
|
||||
string title = 2;
|
||||
double mastery = 3;
|
||||
}
|
||||
|
||||
message GetLearningTrendRequest {
|
||||
string student_id = 1;
|
||||
int64 start_date = 2;
|
||||
int64 end_date = 3;
|
||||
}
|
||||
|
||||
message LearningTrend {
|
||||
string student_id = 1;
|
||||
repeated TrendPoint points = 2;
|
||||
}
|
||||
|
||||
message TrendPoint {
|
||||
int64 date = 1;
|
||||
double score = 2;
|
||||
}
|
||||
47
packages/shared-proto/proto/content.proto
Normal file
47
packages/shared-proto/proto/content.proto
Normal file
@@ -0,0 +1,47 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.content.v1;
|
||||
|
||||
service TextbookService {
|
||||
rpc CreateTextbook(CreateTextbookRequest) returns (Textbook);
|
||||
rpc GetTextbook(GetTextbookRequest) returns (Textbook);
|
||||
rpc ListTextbooks(ListTextbooksRequest) returns (ListTextbooksResponse);
|
||||
}
|
||||
|
||||
service KnowledgeGraphService {
|
||||
rpc GetPrerequisites(GetPrerequisitesRequest) returns (KnowledgePointsResponse);
|
||||
rpc GetLearningPath(GetLearningPathRequest) returns (LearningPath);
|
||||
}
|
||||
|
||||
message Textbook {
|
||||
string id = 1;
|
||||
string title = 2;
|
||||
string subject_id = 3;
|
||||
string grade_id = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message CreateTextbookRequest {
|
||||
string title = 1;
|
||||
string subject_id = 2;
|
||||
string grade_id = 3;
|
||||
string version = 4;
|
||||
}
|
||||
|
||||
message GetTextbookRequest { string id = 1; }
|
||||
message ListTextbooksRequest { string subject_id = 1; string grade_id = 2; }
|
||||
message ListTextbooksResponse { repeated Textbook textbooks = 1; }
|
||||
|
||||
message KnowledgePoint {
|
||||
string id = 1;
|
||||
string title = 2;
|
||||
}
|
||||
|
||||
message GetPrerequisitesRequest { string knowledge_point_id = 1; }
|
||||
message KnowledgePointsResponse { repeated KnowledgePoint points = 1; }
|
||||
|
||||
message GetLearningPathRequest { string student_id = 1; string subject_id = 2; }
|
||||
message LearningPath {
|
||||
repeated KnowledgePoint points = 1;
|
||||
repeated string recommended_order = 2;
|
||||
}
|
||||
181
packages/shared-proto/proto/core_edu.proto
Normal file
181
packages/shared-proto/proto/core_edu.proto
Normal file
@@ -0,0 +1,181 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.core_edu.v1;
|
||||
|
||||
// CoreEdu service contracts - P3 core teaching domain.
|
||||
// Covers exam management, homework assignment, and grade recording.
|
||||
// Event contracts live in events.proto under next_edu_cloud.events.v1.
|
||||
|
||||
service ExamService {
|
||||
rpc CreateExam(CreateExamRequest) returns (CreateExamResponse);
|
||||
rpc GetExam(GetExamRequest) returns (Exam);
|
||||
rpc ListExamsByClass(ListExamsByClassRequest) returns (ListExamsResponse);
|
||||
rpc UpdateExam(UpdateExamRequest) returns (UpdateExamResponse);
|
||||
rpc DeleteExam(DeleteExamRequest) returns (DeleteExamResponse);
|
||||
}
|
||||
|
||||
service HomeworkService {
|
||||
rpc AssignHomework(AssignHomeworkRequest) returns (AssignHomeworkResponse);
|
||||
rpc GetHomework(GetHomeworkRequest) returns (Homework);
|
||||
rpc ListHomeworkByClass(ListHomeworkByClassRequest) returns (ListHomeworkResponse);
|
||||
rpc SubmitHomework(SubmitHomeworkRequest) returns (SubmitHomeworkResponse);
|
||||
}
|
||||
|
||||
service GradeService {
|
||||
rpc RecordGrade(RecordGradeRequest) returns (RecordGradeResponse);
|
||||
rpc GetGrade(GetGradeRequest) returns (Grade);
|
||||
rpc ListGradesByStudent(ListGradesByStudentRequest) returns (ListGradesResponse);
|
||||
rpc ListGradesByExam(ListGradesByExamRequest) returns (ListGradesResponse);
|
||||
rpc ListGradesByHomework(ListGradesByHomeworkRequest) returns (ListGradesResponse);
|
||||
}
|
||||
|
||||
message Exam {
|
||||
string id = 1;
|
||||
string class_id = 2;
|
||||
string title = 3;
|
||||
string description = 4;
|
||||
string exam_date = 5;
|
||||
string duration = 6;
|
||||
string total_score = 7;
|
||||
string status = 8;
|
||||
string created_by = 9;
|
||||
string created_at = 10;
|
||||
string updated_at = 11;
|
||||
}
|
||||
|
||||
message Homework {
|
||||
string id = 1;
|
||||
string class_id = 2;
|
||||
string title = 3;
|
||||
string description = 4;
|
||||
string due_date = 5;
|
||||
string status = 6;
|
||||
string created_by = 7;
|
||||
string created_at = 8;
|
||||
string updated_at = 9;
|
||||
}
|
||||
|
||||
message Grade {
|
||||
string id = 1;
|
||||
string student_id = 2;
|
||||
string exam_id = 3;
|
||||
string homework_id = 4;
|
||||
string score = 5;
|
||||
string feedback = 6;
|
||||
string graded_by = 7;
|
||||
string created_at = 8;
|
||||
string updated_at = 9;
|
||||
}
|
||||
|
||||
message CreateExamRequest {
|
||||
string class_id = 1;
|
||||
string title = 2;
|
||||
string description = 3;
|
||||
string exam_date = 4;
|
||||
string duration = 5;
|
||||
string total_score = 6;
|
||||
string created_by = 7;
|
||||
}
|
||||
|
||||
message CreateExamResponse {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message GetExamRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message ListExamsByClassRequest {
|
||||
string class_id = 1;
|
||||
}
|
||||
|
||||
message ListExamsResponse {
|
||||
repeated Exam exams = 1;
|
||||
}
|
||||
|
||||
message UpdateExamRequest {
|
||||
string id = 1;
|
||||
string title = 2;
|
||||
string description = 3;
|
||||
string exam_date = 4;
|
||||
string duration = 5;
|
||||
string total_score = 6;
|
||||
string status = 7;
|
||||
}
|
||||
|
||||
message UpdateExamResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message DeleteExamRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DeleteExamResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message AssignHomeworkRequest {
|
||||
string class_id = 1;
|
||||
string title = 2;
|
||||
string description = 3;
|
||||
string due_date = 4;
|
||||
string created_by = 5;
|
||||
}
|
||||
|
||||
message AssignHomeworkResponse {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message GetHomeworkRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message ListHomeworkByClassRequest {
|
||||
string class_id = 1;
|
||||
}
|
||||
|
||||
message ListHomeworkResponse {
|
||||
repeated Homework homework = 1;
|
||||
}
|
||||
|
||||
message SubmitHomeworkRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message SubmitHomeworkResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message RecordGradeRequest {
|
||||
string student_id = 1;
|
||||
string exam_id = 2;
|
||||
string homework_id = 3;
|
||||
string score = 4;
|
||||
string feedback = 5;
|
||||
string graded_by = 6;
|
||||
}
|
||||
|
||||
message RecordGradeResponse {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message GetGradeRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message ListGradesByStudentRequest {
|
||||
string student_id = 1;
|
||||
}
|
||||
|
||||
message ListGradesByExamRequest {
|
||||
string exam_id = 1;
|
||||
}
|
||||
|
||||
message ListGradesByHomeworkRequest {
|
||||
string homework_id = 1;
|
||||
}
|
||||
|
||||
message ListGradesResponse {
|
||||
repeated Grade grades = 1;
|
||||
}
|
||||
60
packages/shared-proto/proto/events.proto
Normal file
60
packages/shared-proto/proto/events.proto
Normal file
@@ -0,0 +1,60 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.events.v1;
|
||||
|
||||
// Cross-service event contracts published by CoreEdu via the transactional
|
||||
// outbox pattern and consumed by downstream services (notifications, analytics,
|
||||
// audit, etc.). Topics follow the convention edu.{domain}.events.
|
||||
//
|
||||
// Event routing (TOPIC_MAP in outbox.publisher.ts):
|
||||
// edu.exam.events <- exam.created / exam.updated / exam.deleted
|
||||
// edu.homework.events <- homework.assigned / homework.submitted / homework.graded
|
||||
// edu.grade.events <- grade.recorded / grade.updated
|
||||
// edu.class.events <- class.transferred
|
||||
|
||||
message ClassEvent {
|
||||
string event_id = 1;
|
||||
string aggregate_id = 2;
|
||||
string event_type = 3;
|
||||
int64 occurred_at = 4;
|
||||
string class_id = 5;
|
||||
string name = 6;
|
||||
string action = 7;
|
||||
map<string, string> metadata = 8;
|
||||
}
|
||||
|
||||
message ExamEvent {
|
||||
string event_id = 1;
|
||||
string aggregate_id = 2;
|
||||
string event_type = 3;
|
||||
int64 occurred_at = 4;
|
||||
string exam_id = 5;
|
||||
string class_id = 6;
|
||||
string title = 7;
|
||||
string action = 8;
|
||||
map<string, string> metadata = 9;
|
||||
}
|
||||
|
||||
message HomeworkEvent {
|
||||
string event_id = 1;
|
||||
string aggregate_id = 2;
|
||||
string event_type = 3;
|
||||
int64 occurred_at = 4;
|
||||
string homework_id = 5;
|
||||
string class_id = 6;
|
||||
string title = 7;
|
||||
string action = 8;
|
||||
map<string, string> metadata = 9;
|
||||
}
|
||||
|
||||
message GradeEvent {
|
||||
string event_id = 1;
|
||||
string aggregate_id = 2;
|
||||
string event_type = 3;
|
||||
int64 occurred_at = 4;
|
||||
string grade_id = 5;
|
||||
string student_id = 6;
|
||||
string score = 7;
|
||||
string action = 8;
|
||||
map<string, string> metadata = 9;
|
||||
}
|
||||
50
packages/shared-proto/proto/iam.proto
Normal file
50
packages/shared-proto/proto/iam.proto
Normal file
@@ -0,0 +1,50 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.iam.v1;
|
||||
|
||||
// IamService 定义身份与访问管理契约
|
||||
// P2: REST 实现,P3 起转 gRPC
|
||||
service IamService {
|
||||
rpc Register(RegisterRequest) returns (AuthResponse);
|
||||
rpc Login(LoginRequest) returns (AuthResponse);
|
||||
rpc RefreshToken(RefreshTokenRequest) returns (TokenPair);
|
||||
rpc GetUserInfo(GetUserInfoRequest) returns (UserInfo);
|
||||
}
|
||||
|
||||
message RegisterRequest {
|
||||
string email = 1;
|
||||
string password = 2;
|
||||
string name = 3;
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
string email = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message RefreshTokenRequest {
|
||||
string refresh_token = 1;
|
||||
}
|
||||
|
||||
message GetUserInfoRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
|
||||
message AuthResponse {
|
||||
UserInfo user = 1;
|
||||
TokenPair tokens = 2;
|
||||
}
|
||||
|
||||
message TokenPair {
|
||||
string access_token = 1;
|
||||
string refresh_token = 2;
|
||||
int32 expires_in = 3;
|
||||
}
|
||||
|
||||
message UserInfo {
|
||||
string id = 1;
|
||||
string email = 2;
|
||||
string name = 3;
|
||||
repeated string roles = 4;
|
||||
repeated string permissions = 5;
|
||||
}
|
||||
48
packages/shared-proto/proto/msg.proto
Normal file
48
packages/shared-proto/proto/msg.proto
Normal file
@@ -0,0 +1,48 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package next_edu_cloud.msg.v1;
|
||||
|
||||
service NotificationService {
|
||||
rpc SendNotification(SendNotificationRequest) returns (Notification);
|
||||
rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse);
|
||||
rpc MarkAsRead(MarkAsReadRequest) returns (Empty);
|
||||
rpc SearchNotifications(SearchNotificationsRequest) returns (SearchNotificationsResponse);
|
||||
}
|
||||
|
||||
message Notification {
|
||||
string id = 1;
|
||||
string user_id = 2;
|
||||
string type = 3;
|
||||
string title = 4;
|
||||
string content = 5;
|
||||
string channel = 6;
|
||||
bool is_read = 7;
|
||||
int64 created_at = 8;
|
||||
}
|
||||
|
||||
message SendNotificationRequest {
|
||||
string user_id = 1;
|
||||
string type = 2;
|
||||
string title = 3;
|
||||
string content = 4;
|
||||
string channel = 5;
|
||||
}
|
||||
|
||||
message ListNotificationsRequest {
|
||||
string user_id = 1;
|
||||
bool only_unread = 2;
|
||||
}
|
||||
|
||||
message ListNotificationsResponse {
|
||||
repeated Notification notifications = 1;
|
||||
}
|
||||
|
||||
message MarkAsReadRequest { string id = 1; }
|
||||
message SearchNotificationsRequest {
|
||||
string user_id = 1;
|
||||
string query = 2;
|
||||
}
|
||||
message SearchNotificationsResponse {
|
||||
repeated Notification notifications = 1;
|
||||
}
|
||||
message Empty {}
|
||||
10102
pnpm-lock.yaml
generated
Normal file
10102
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
4. 若代ç <C3A7>结构å<E2809E>˜åŒ?â†?`pnpm run arch:scan` 确认 arch.db 已更æ–?
|
||||
@@ -1,17 +1,22 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { createDb } from './schema.js';
|
||||
import { scanTypeScript } from './scanners/ts-scanner.js';
|
||||
import { scanGo } from './scanners/go-scanner.js';
|
||||
import { scanPython } from './scanners/py-scanner.js';
|
||||
import { scanProtobuf } from './scanners/proto-scanner.js';
|
||||
import path from 'node:path';
|
||||
import { createDb } from "./schema.js";
|
||||
import { scanTypeScript } from "./scanners/ts-scanner.js";
|
||||
import { scanGo } from "./scanners/go-scanner.js";
|
||||
import { scanPython } from "./scanners/py-scanner.js";
|
||||
import { scanProtobuf } from "./scanners/proto-scanner.js";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
function main(): void {
|
||||
const db = createDb(path.join(ROOT, 'arch.db'));
|
||||
const db = createDb(path.join(ROOT, "arch.db"));
|
||||
|
||||
console.log('🔍 arch-scan: 开始多语言扫描...');
|
||||
// 清空旧数据(避免重跑产生重复符号)
|
||||
db.exec(
|
||||
"DELETE FROM calls; DELETE FROM dependencies; DELETE FROM symbols; DELETE FROM contracts; DELETE FROM events; DELETE FROM modules;",
|
||||
);
|
||||
|
||||
console.log("🔍 arch-scan: 开始多语言扫描...");
|
||||
|
||||
// 串行扫描(并行会导致 FOREIGN KEY 错误)
|
||||
const tsStats = scanTypeScript(db, ROOT);
|
||||
@@ -26,7 +31,7 @@ function main(): void {
|
||||
const protoStats = scanProtobuf(db, ROOT);
|
||||
console.log(` Protobuf: ${protoStats.contracts} 契约`);
|
||||
|
||||
console.log('✅ arch-scan 完成');
|
||||
console.log("✅ arch-scan 完成");
|
||||
db.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,133 @@
|
||||
import type { Database as DBType } from 'better-sqlite3';
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { Database as DBType } from "better-sqlite3";
|
||||
|
||||
interface ScanStats {
|
||||
modules: number;
|
||||
symbols: number;
|
||||
}
|
||||
|
||||
export function scanGo(db: DBType, root: string): ScanStats {
|
||||
// Go 扫描器骨架:P1 后期用 tree-sitter-go 实现
|
||||
// 当前仅扫描 go.mod 识别模块
|
||||
return { modules: 0, symbols: 0 };
|
||||
/** 递归收集目录下匹配扩展名的所有文件 */
|
||||
function walkDir(dir: string, exts: string[]): string[] {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const results: string[] = [];
|
||||
const stack: string[] = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (["node_modules", "dist", ".git", "vendor"].includes(entry.name))
|
||||
continue;
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (exts.includes(ext)) results.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Go 符号正则:匹配导出函数、类型、结构体、接口
|
||||
const GO_FUNC_RE = /^func\s+(?:\([^)]*\)\s+)?([A-Z]\w*)\s*\(/gm;
|
||||
const GO_TYPE_RE = /^type\s+([A-Z]\w*)\s+/gm;
|
||||
const GO_STRUCT_RE = /^type\s+([A-Z]\w*)\s+struct\s*\{/gm;
|
||||
const GO_INTERFACE_RE = /^type\s+([A-Z]\w*)\s+interface\s*\{/gm;
|
||||
|
||||
export function scanGo(db: DBType, root: string): ScanStats {
|
||||
const insertModule = db.prepare(
|
||||
"INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)",
|
||||
);
|
||||
const insertSymbol = db.prepare(
|
||||
"INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
);
|
||||
const getModuleId = db.prepare("SELECT id FROM modules WHERE name = ?") as {
|
||||
get: (name: string) => { id: number } | undefined;
|
||||
};
|
||||
|
||||
let modules = 0;
|
||||
let symbols = 0;
|
||||
|
||||
// 扫描 services/* 下有 go.mod 的服务
|
||||
const servicesDir = path.join(root, "services");
|
||||
if (!fs.existsSync(servicesDir)) return { modules, symbols };
|
||||
|
||||
const entries = fs.readdirSync(servicesDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const serviceName = entry.name;
|
||||
const servicePath = path.join(servicesDir, serviceName);
|
||||
const hasGoMod = fs.existsSync(path.join(servicePath, "go.mod"));
|
||||
if (!hasGoMod) continue;
|
||||
|
||||
insertModule.run(serviceName, servicePath, "go", serviceName, "service");
|
||||
const modRow = getModuleId.get(serviceName);
|
||||
if (modRow) {
|
||||
modules++;
|
||||
// 扫描 .go 文件
|
||||
const goFiles = walkDir(servicePath, [".go"]);
|
||||
for (const filePath of goFiles) {
|
||||
// 跳过测试文件
|
||||
if (filePath.endsWith("_test.go")) continue;
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const lines = content.split("\n");
|
||||
|
||||
// 函数
|
||||
let match: RegExpExecArray | null;
|
||||
const funcRe = new RegExp(GO_FUNC_RE);
|
||||
while ((match = funcRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"function",
|
||||
"go",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
1,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
|
||||
// 类型(struct / interface / type alias)
|
||||
const typeRe = new RegExp(GO_TYPE_RE);
|
||||
while ((match = typeRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
// 判断是 struct 还是 interface 还是普通 type
|
||||
const line = lines[lineNum - 1] || "";
|
||||
let kind = "type";
|
||||
if (line.includes("struct")) kind = "struct";
|
||||
else if (line.includes("interface")) kind = "interface";
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
kind,
|
||||
"go",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
1,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { modules, symbols };
|
||||
}
|
||||
@@ -1,10 +1,92 @@
|
||||
import type { Database as DBType } from 'better-sqlite3';
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { Database as DBType } from "better-sqlite3";
|
||||
|
||||
interface ScanStats {
|
||||
contracts: number;
|
||||
}
|
||||
|
||||
// Proto 符号正则
|
||||
const PROTO_SERVICE_RE = /^service\s+(\w+)\s*\{/gm;
|
||||
const PROTO_MESSAGE_RE = /^message\s+(\w+)\s*\{/gm;
|
||||
const PROTO_RPC_RE = /^\s*rpc\s+(\w+)\s*\(/gm;
|
||||
|
||||
export function scanProtobuf(db: DBType, root: string): ScanStats {
|
||||
// Protobuf 扫描器骨架:扫描 packages/shared-proto/proto/*.proto
|
||||
return { contracts: 0 };
|
||||
const insertContract = db.prepare(
|
||||
"INSERT OR IGNORE INTO contracts (name, type, file_path, service, content) VALUES (?, ?, ?, ?, ?)",
|
||||
);
|
||||
|
||||
let contracts = 0;
|
||||
|
||||
// 扫描 packages/shared-proto/proto/*.proto
|
||||
const protoDir = path.join(root, "packages", "shared-proto", "proto");
|
||||
if (!fs.existsSync(protoDir)) return { contracts };
|
||||
|
||||
const entries = fs.readdirSync(protoDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".proto")) continue;
|
||||
const filePath = path.join(protoDir, entry.name);
|
||||
const protoName = path.basename(entry.name, ".proto");
|
||||
const serviceName = protoName.replace("_", "-");
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取 package 名
|
||||
const pkgMatch = content.match(/^package\s+(\S+);/m);
|
||||
const pkgName = pkgMatch ? pkgMatch[1] : "";
|
||||
|
||||
// 记录 proto 文件作为契约
|
||||
insertContract.run(protoName, "proto-file", filePath, serviceName, pkgName);
|
||||
contracts++;
|
||||
|
||||
// 提取 service 定义
|
||||
let match: RegExpExecArray | null;
|
||||
const serviceRe = new RegExp(PROTO_SERVICE_RE);
|
||||
while ((match = serviceRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
insertContract.run(
|
||||
`${protoName}.${name}`,
|
||||
"service",
|
||||
filePath,
|
||||
serviceName,
|
||||
name,
|
||||
);
|
||||
contracts++;
|
||||
}
|
||||
|
||||
// 提取 message 定义
|
||||
const messageRe = new RegExp(PROTO_MESSAGE_RE);
|
||||
while ((match = messageRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
insertContract.run(
|
||||
`${protoName}.${name}`,
|
||||
"message",
|
||||
filePath,
|
||||
serviceName,
|
||||
name,
|
||||
);
|
||||
contracts++;
|
||||
}
|
||||
|
||||
// 提取 rpc 方法
|
||||
const rpcRe = new RegExp(PROTO_RPC_RE);
|
||||
while ((match = rpcRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
insertContract.run(
|
||||
`${protoName}.rpc.${name}`,
|
||||
"rpc",
|
||||
filePath,
|
||||
serviceName,
|
||||
name,
|
||||
);
|
||||
contracts++;
|
||||
}
|
||||
}
|
||||
|
||||
return { contracts };
|
||||
}
|
||||
@@ -1,11 +1,142 @@
|
||||
import type { Database as DBType } from 'better-sqlite3';
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { Database as DBType } from "better-sqlite3";
|
||||
|
||||
interface ScanStats {
|
||||
modules: number;
|
||||
symbols: number;
|
||||
}
|
||||
|
||||
export function scanPython(db: DBType, root: string): ScanStats {
|
||||
// Python 扫描器骨架:P1 后期用 tree-sitter-python 实现
|
||||
return { modules: 0, symbols: 0 };
|
||||
/** 递归收集目录下匹配扩展名的所有文件 */
|
||||
function walkDir(dir: string, exts: string[]): string[] {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const results: string[] = [];
|
||||
const stack: string[] = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (
|
||||
[
|
||||
"node_modules",
|
||||
"dist",
|
||||
".git",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
].includes(entry.name)
|
||||
)
|
||||
continue;
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (exts.includes(ext)) results.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Python 符号正则
|
||||
const PY_FUNC_RE = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
|
||||
const PY_CLASS_RE = /^class\s+(\w+)\s*[\(:]/gm;
|
||||
|
||||
export function scanPython(db: DBType, root: string): ScanStats {
|
||||
const insertModule = db.prepare(
|
||||
"INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)",
|
||||
);
|
||||
const insertSymbol = db.prepare(
|
||||
"INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
);
|
||||
const getModuleId = db.prepare("SELECT id FROM modules WHERE name = ?") as {
|
||||
get: (name: string) => { id: number } | undefined;
|
||||
};
|
||||
|
||||
let modules = 0;
|
||||
let symbols = 0;
|
||||
|
||||
// 扫描 services/* 下有 pyproject.toml 的服务
|
||||
const servicesDir = path.join(root, "services");
|
||||
if (!fs.existsSync(servicesDir)) return { modules, symbols };
|
||||
|
||||
const entries = fs.readdirSync(servicesDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const serviceName = entry.name;
|
||||
const servicePath = path.join(servicesDir, serviceName);
|
||||
const hasPyproject = fs.existsSync(
|
||||
path.join(servicePath, "pyproject.toml"),
|
||||
);
|
||||
if (!hasPyproject) continue;
|
||||
|
||||
insertModule.run(
|
||||
serviceName,
|
||||
servicePath,
|
||||
"python",
|
||||
serviceName,
|
||||
"service",
|
||||
);
|
||||
const modRow = getModuleId.get(serviceName);
|
||||
if (modRow) {
|
||||
modules++;
|
||||
// 扫描 .py 文件
|
||||
const pyFiles = walkDir(servicePath, [".py"]);
|
||||
for (const filePath of pyFiles) {
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 函数(含 async def)
|
||||
let match: RegExpExecArray | null;
|
||||
const funcRe = new RegExp(PY_FUNC_RE);
|
||||
while ((match = funcRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
// Python 中以 _ 开头的为私有
|
||||
const isExported = !name.startsWith("_") ? 1 : 0;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"function",
|
||||
"python",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
isExported,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
|
||||
// 类
|
||||
const classRe = new RegExp(PY_CLASS_RE);
|
||||
while ((match = classRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"class",
|
||||
"python",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
1,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { modules, symbols };
|
||||
}
|
||||
@@ -1,38 +1,211 @@
|
||||
import { Project, SyntaxKind } from 'ts-morph';
|
||||
import path from 'node:path';
|
||||
import type { Database as DBType } from 'better-sqlite3';
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { Database as DBType } from "better-sqlite3";
|
||||
|
||||
interface ScanStats {
|
||||
modules: number;
|
||||
symbols: number;
|
||||
}
|
||||
|
||||
export function scanTypeScript(db: DBType, root: string): ScanStats {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: undefined,
|
||||
skipAddingFilesFromTsConfig: true,
|
||||
compilerOptions: {
|
||||
allowJs: true,
|
||||
declaration: false,
|
||||
resolveJsonModule: true,
|
||||
},
|
||||
});
|
||||
/** 递归收集目录下匹配扩展名的所有文件 */
|
||||
function walkDir(dir: string, exts: string[]): string[] {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const results: string[] = [];
|
||||
const stack: string[] = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
// 跳过 node_modules / dist / .git
|
||||
if (
|
||||
["node_modules", "dist", ".git", ".next", "build"].includes(
|
||||
entry.name,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (exts.includes(ext)) results.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function scanTypeScript(db: DBType, root: string): ScanStats {
|
||||
const insertModule = db.prepare(
|
||||
"INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)",
|
||||
);
|
||||
const insertSymbol = db.prepare(
|
||||
"INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
);
|
||||
const getModuleId = db.prepare("SELECT id FROM modules WHERE name = ?") as {
|
||||
get: (name: string) => { id: number } | undefined;
|
||||
};
|
||||
|
||||
const patterns = ['services/*/src/**/*.ts', 'apps/*/src/**/*.ts', 'packages/*/src/**/*.ts'];
|
||||
let modules = 0;
|
||||
let symbols = 0;
|
||||
|
||||
const insertModule = db.prepare(
|
||||
'INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
const insertSymbol = db.prepare(
|
||||
'INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
// 扫描 services/* 和 apps/* 和 packages/* 下的 TS 服务
|
||||
const serviceDirs = ["services", "apps"];
|
||||
const moduleList: { name: string; path: string; service: string }[] = [];
|
||||
|
||||
// 简化实现:扫描 services/*/src 目录作为模块
|
||||
const servicesDir = path.join(root, 'services');
|
||||
// 实际实现用 fast-glob 模式匹配
|
||||
// 此处为骨架,P1 后期补全
|
||||
for (const dir of serviceDirs) {
|
||||
const baseDir = path.join(root, dir);
|
||||
if (!fs.existsSync(baseDir)) continue;
|
||||
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const serviceName = entry.name;
|
||||
const servicePath = path.join(baseDir, serviceName);
|
||||
// 确认是 TS 服务(有 src 目录或 package.json)
|
||||
const hasSrc = fs.existsSync(path.join(servicePath, "src"));
|
||||
const hasPkg = fs.existsSync(path.join(servicePath, "package.json"));
|
||||
if (!hasSrc && !hasPkg) continue;
|
||||
|
||||
const moduleName = serviceName;
|
||||
insertModule.run(moduleName, servicePath, "ts", serviceName, "service");
|
||||
const row = getModuleId.get(moduleName);
|
||||
if (row) {
|
||||
modules++;
|
||||
moduleList.push({
|
||||
name: moduleName,
|
||||
path: servicePath,
|
||||
service: serviceName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// packages/* 下的 TS 包
|
||||
const packagesDir = path.join(root, "packages");
|
||||
if (fs.existsSync(packagesDir)) {
|
||||
const entries = fs.readdirSync(packagesDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const pkgName = entry.name;
|
||||
const pkgPath = path.join(packagesDir, pkgName);
|
||||
const hasSrc = fs.existsSync(path.join(pkgPath, "src"));
|
||||
const hasPkg = fs.existsSync(path.join(pkgPath, "package.json"));
|
||||
const hasProto = fs.existsSync(path.join(pkgPath, "proto"));
|
||||
if (!hasSrc && !hasPkg && !hasProto) continue;
|
||||
const moduleName = pkgName;
|
||||
insertModule.run(moduleName, pkgPath, "ts", pkgName, "package");
|
||||
const row = getModuleId.get(moduleName);
|
||||
if (row) {
|
||||
modules++;
|
||||
moduleList.push({ name: moduleName, path: pkgPath, service: pkgName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集所有 .ts 文件并用 regex 提取符号(ts-morph 对未安装依赖的文件可能解析失败)
|
||||
const TS_FUNC_RE = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
|
||||
const TS_CLASS_RE = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g;
|
||||
const TS_INTERFACE_RE = /(?:export\s+)?interface\s+(\w+)/g;
|
||||
const TS_CONST_RE = /(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)\s*=/g;
|
||||
|
||||
for (const mod of moduleList) {
|
||||
const srcDir = path.join(mod.path, "src");
|
||||
if (!fs.existsSync(srcDir)) continue;
|
||||
const tsFiles = walkDir(srcDir, [".ts", ".tsx"]);
|
||||
const modRow = getModuleId.get(mod.name);
|
||||
if (!modRow) continue;
|
||||
|
||||
for (const filePath of tsFiles) {
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 函数
|
||||
let match: RegExpExecArray | null;
|
||||
const funcRe = new RegExp(TS_FUNC_RE);
|
||||
while ((match = funcRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
const isExported = match[0].includes("export") ? 1 : 0;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"function",
|
||||
"ts",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
isExported,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
|
||||
// 类
|
||||
const classRe = new RegExp(TS_CLASS_RE);
|
||||
while ((match = classRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
const isExported = match[0].includes("export") ? 1 : 0;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"class",
|
||||
"ts",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
isExported,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
|
||||
// 接口
|
||||
const ifaceRe = new RegExp(TS_INTERFACE_RE);
|
||||
while ((match = ifaceRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
const isExported = match[0].includes("export") ? 1 : 0;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"interface",
|
||||
"ts",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
isExported,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
|
||||
// 常量(UPPER_CASE)
|
||||
const constRe = new RegExp(TS_CONST_RE);
|
||||
while ((match = constRe.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
const isExported = match[0].includes("export") ? 1 : 0;
|
||||
insertSymbol.run(
|
||||
modRow.id,
|
||||
name,
|
||||
"const",
|
||||
"ts",
|
||||
filePath,
|
||||
lineNum,
|
||||
lineNum,
|
||||
isExported,
|
||||
);
|
||||
symbols++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { modules, symbols };
|
||||
}
|
||||
8
services/ai/Dockerfile
Normal file
8
services/ai/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
RUN pip install uv
|
||||
COPY pyproject.toml .
|
||||
RUN uv sync --no-dev
|
||||
COPY src ./src
|
||||
EXPOSE 3008
|
||||
CMD ["uv", "run", "uvicorn", "src.ai.main:app", "--host", "0.0.0.0", "--port", "3008"]
|
||||
45
services/ai/README.md
Normal file
45
services/ai/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# AI 网关服务
|
||||
|
||||
> 版本:0.1(P5 骨架)
|
||||
> 端口:3008
|
||||
|
||||
## 职责
|
||||
|
||||
AI 网关限界上下文(Python 实现),统一封装 LLM 调用(多模型路由、重试、限流、成本控制)。
|
||||
提供辅助出题、表达优化、分层提问等能力。通过 gRPC 查询 content 题库与 data-ana 学情数据。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.12 + FastAPI 0.115
|
||||
- Pydantic 2 + pydantic-settings
|
||||
- OpenTelemetry(LLM 调用链追踪)
|
||||
- prometheus-client + structlog
|
||||
- SSE 流式响应
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run uvicorn src.ai.main:app --reload --port 3008
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /healthz | 健康检查 |
|
||||
| POST | /chat | LLM 聊天接口 |
|
||||
| POST | /chat/stream | 流式聊天(SSE) |
|
||||
| POST | /generate/question | 生成题目 |
|
||||
| POST | /optimize/expression | 优化表达 |
|
||||
| GET | /metrics | Prometheus 指标 |
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| port | 3008 | 服务端口 |
|
||||
| openai_api_key | - | OpenAI API 密钥 |
|
||||
| anthropic_api_key | - | Anthropic API 密钥 |
|
||||
| otel_endpoint | http://localhost:4318 | OpenTelemetry OTLP 端点 |
|
||||
| log_level | info | 日志级别 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user