chore(deps): 统一依赖管理 - pnpm 11 + node:22 + golang:1.25 + python:3.12 + shared-* 集中化
Some checks failed
CI / quality-ts (push) Failing after 6s
CI / quality-go (push) Failing after 25s
CI / quality-proto (push) Failing after 6s
CI / deploy (push) Has been skipped

- Node.js 统一到 node:22-alpine,Go 统一到 golang:1.25-alpine,Python 统一到 python:3.12-slim

- pnpm 升级到 11.13.0(corepack),新增 allowBuilds 白名单解决 ERR_PNPM_IGNORED_BUILDS

- 新增 packages/shared-py 集中 Python 共享依赖,shared-ts 补充 graphql-yoga/prom-client

- api-gateway 修复 go.mod 的 shared-go 依赖 + Dockerfile 改用 repo 根作 context

- Python 服务(data-ana/ai)Dockerfile 改用 repo 根作 context + 声明 uv workspace sources

- 16 个服务的 Dockerfile + CI + docker-compose.tools.yml 全部对齐版本矩阵

- known-issues.md 沉淀 9 条 pnpm 11 / uv workspace / Go shared-go 迁移经验

- 验证:4 服务完全成功(api-gateway /healthz 200),其余 install 成功(build 失败为预存 TS 错误)
This commit is contained in:
SpecialX
2026-07-14 12:04:49 +08:00
parent 74077c7083
commit 81a539b9ab
39 changed files with 31999 additions and 3244 deletions

View File

@@ -11,20 +11,28 @@
### 1.1 多语言 monorepo 配置
| 场景 | 技术/规则 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| 多语言 workspace | pnpm workspaceTS+ go.workGo+ pyproject.toml/uv workspacePython三套并存 |
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
| pnpm-workspace.yaml | 仅声明 TS 包路径packages/_、services/classes、bff/_、apps/_、scripts/_Go/Python 不入 |
| go.work | 列出所有 Go 服务模块services/api-gateway、services/push-gateway |
| pyproject.toml | uv workspace members 列 Python 服务services/data-ana、services/ai-gateway |
| 跨语言共享类型 | protobuf 生成三端代码TS/Go/Python单一契约源 |
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 |
| husky + commitlint | pre-commit 跑 eslint+prettiercommit-msg 校验 Conventional Commits |
| .editorconfig 多语言缩进 | Go 用 tabPython 用 4 空格TS/默认用 2 空格 |
| ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`flat configlint 脚本去掉 `--ext .ts`lint-staged 恢复 `eslint --fix` |
| next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json``next lint` 触发 Strict/Base 选择提示CI 中需预置配置或改 `eslint src` |
| uv.lock 解析失败 | `uv.lock``grpcio-health-checking``source` 字段且匹配多包,导致 `uv run pytest` 全 workspace Python 服务不可用,需重建 lock`uv lock` |
| 场景 | 技术/规则 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 多语言 workspace | pnpm workspaceTS+ go.workGo+ pyproject.toml/uv workspacePython三套并存 |
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
| pnpm-workspace.yaml | 仅声明 TS 包路径packages/_、services/classes、bff/_、apps/_、scripts/_Go/Python 不入 |
| go.work | 列出所有 Go 服务模块services/api-gateway、services/push-gateway |
| pyproject.toml | uv workspace members 列 Python 服务services/data-ana、services/ai-gateway |
| 跨语言共享类型 | protobuf 生成三端代码TS/Go/Python单一契约源 |
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 |
| husky + commitlint | pre-commit 跑 eslint+prettiercommit-msg 校验 Conventional Commits |
| .editorconfig 多语言缩进 | Go 用 tabPython 用 4 空格TS/默认用 2 空格 |
| ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`flat configlint 脚本去掉 `--ext .ts`lint-staged 恢复 `eslint --fix` |
| next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json``next lint` 触发 Strict/Base 选择提示CI 中需预置配置或改 `eslint src` |
| uv.lock 解析失败 | `uv.lock``grpcio-health-checking``source` 字段且匹配多包,导致 `uv run pytest` 全 workspace Python 服务不可用,需重建 lock`uv lock` |
| pnpm 11 allowBuilds | pnpm 11 默认拒绝 build scriptspostinstall需在 `pnpm-workspace.yaml``allowBuilds: { pkg: true }` 对象格式声明白名单(非 `package.json``pnpm.onlyBuiltDependencies`,已废弃) |
| pnpm 11 独立安装 | Dockerfile 仅 COPY `package.json`(无 `pnpm-workspace.yaml`pnpm 11 无法读取 `allowBuilds`,需加 `--ignore-scripts` 跳过 build scripts@nestjs/core/protobufjs 的 postinstall 非功能必需) |
| pnpm 11 无 lockfile 安装 | 服务目录无独立 `pnpm-lock.yaml`Dockerfile 用 `--no-frozen-lockfile` 替代 `--frozen-lockfile`(如 classes 服务) |
| uv workspace 依赖声明 | 服务 `pyproject.toml` 依赖 workspace 内包(如 `edu-shared-py`)时,必须声明 `[tool.uv.sources]\nedu-shared-py = { workspace = true }`,否则 `uv sync` 报 "missing an entry in tool.uv.sources" |
| uv workspace .venv 路径 | uv workspace 在根目录创建 `.venv`非服务目录Dockerfile runtime stage 的 `COPY --from=builder /app/.venv` 路径需指向 `/app/.venv` |
| Go 服务 Dockerfile 上下文 | 依赖 `shared-go` 的 Go 服务api-gateway/push-gatewayDockerfile 必须用 repo 根 `.` 作为 build context并显式 COPY `packages/shared-go/`(非 `services/xxx/` 上下文) |
| Go 服务 shared-go 依赖 | `go.mod` 必须 `require github.com/edu-cloud/shared-go v0.0.0` + `replace github.com/edu-cloud/shared-go => ../../packages/shared-go`,否则编译报 "no required module provides package" |
| Python 服务 Dockerfile 上下文 | 依赖 `shared-py` 的 Python 服务 Dockerfile 必须用 repo 根 `.` 作为 build contextCOPY `pyproject.toml` + `uv.lock` + `packages/shared-py/`(非 `services/xxx/` 上下文) |
### 1.2 Docker Compose 基础设施
@@ -79,17 +87,17 @@
### 1.5 Go Gateway 开发
| 场景 | 技术/规则 |
| ----------------- | --------------------------------------------------------------------------------------- |
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
| P1 鉴权 | Gateway 内置 HS256 JWT 校验测试密钥P2 改 RS256IAM 签发,公钥校验) |
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
| 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 |
| 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` |
| 包结构 | `internal/{config,middleware,proxy}`P6 扩展 handler/service/repository |
| error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 |
| 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`P6 引入 viper |
| Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行CGO_ENABLED=0 |
| 场景 | 技术/规则 |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
| P1 鉴权 | Gateway 内置 HS256 JWT 校验测试密钥P2 改 RS256IAM 签发,公钥校验) |
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
| 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 |
| 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` |
| 包结构 | `internal/{config,middleware,proxy}`P6 扩展 handler/service/repository |
| error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 |
| 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`P6 引入 viper |
| Dockerfile 多阶段 | golang:1.25-alpine 构建 → alpine:3.20 运行CGO_ENABLED=0,依赖 shared-go 时用 repo 根作 build context |
### 1.6 可观测性OTel + Prometheus + Loki
@@ -245,32 +253,39 @@
### 2.3 iamTS/NestJS
| 场景 | 技术/规则 |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 双入口 | REST `/v1/iam/*` + gRPC 50052package `next_edu_cloud.iam.v1`),同一 IamService 实例共用Hybrid app `connectMicroservice` |
| 认证 | RS256 非对称签名,私钥本地文件(`IAM_PRIVATE_KEY_PATH`),公钥通过 JWKS 端点暴露给 Gateway |
| JWT payload | `{ sub, email, roles, dataScope, type, iss, aud, jti(仅 refresh), kid(header) }`register 自动分配 teacher 角色 |
| Token TTL | access 15min / refresh 7dayrefresh 含 jti轮换时旧 jti 加入 Redis 黑名单 |
| RBAC | 角色/权限/角色-权限查询 + `getEffectivePermissions(userId)` API三层角色模型system/organization/temporarylevel 0/1/2 |
| 视口配置 | 4 层模型(导航/路由/组件/数据),`iam_role_viewports` 表含 `level` ENUM(admin/teacher/student/parent) |
| DataScope 解析 | 6 级self/subject/class/grade/school/allSUBJECT 替代 DISTRICT注入 JWT payload |
| 权限校验 | PermissionGuardAPP_GUARDDB 驱动 + Redis 缓存 TTL 5min`data_scope=all` 直放行Key `iam:perm:{userId}` |
| Token 黑名单 | Redis Key `iam:bl:{jti}` TTL 与 refresh_token 剩余有效期对齐logout/refresh 时写入 |
| 审计日志 | `iam_user_audit_log` 表 + `AuditCreated` Kafka 事件,登录/角色变更/密码修改等关键操作记录 |
| 家长-学生关系 | `iam_student_guardians` 表unique(studentId, guardianId)`GetChildrenByParent` RPC + `GET /v1/iam/children` REST |
| schema 表 | iam_users / iam_roles / iam_user_roles / iam_permissions / iam_role_permissions / iam_refresh_tokens / iam_role_viewports / iam_student_guardians / iam_user_audit_log / iam_password_history |
| Outbox | shared-ts `OutboxModule.forRoot({ config, db, kafkaProducer })`,表名 `iam_outbox`topic `edu.iam.user.events`/`edu.iam.role.events`/`edu.iam.audit.created` |
| AuthMiddleware 注册 | `app.module.ts` `configure()` 注册于 me/logout/viewports/permissions/roles/audit/children 路由,解析 `x-user-*` 头 |
| 公开路径 | register/login/refresh/jwks.json 无 `@RequirePermission()`PermissionGuard 旁路 |
| ESM 模式 DI | `providers: [IamService, IamRepository]` + 构造器 `@Inject(IamRepository)` 显式注入,避免 `undefined` 运行时错误 |
| APP_GUARD DI 导出 | PermissionGuard 注册为 APP_GUARDAppModule 级),其依赖 PermissionCacheService/IamRepository 须在 IamModule `exports` 中导出,否则 AppModule 上下文无法注入 |
| Proto 路径解析 | `resolveProtoPath()` 兼顾 cwd=monorepo 根CI与 cwd=services/iam`pnpm --filter` dev先 `packages/shared-proto/proto/`,回退 `../../packages/shared-proto/proto/` 与本地 `proto/`Docker |
| Drizzle ORM API | `inArray(col, vals)` 替代不存在的 `.in()``delete().where()` 不支持链式多次 where用 `and(eq(...), eq(...))` 组合 |
| Drizzle DataScope 类型 | `mysqlEnum` 推断类型为字面量联合Repository 方法参数须用 `DataScope` 类型而非 `string`,否则 TS 报错 |
| ioredis 类型 | `import { Redis } from "ioredis"`named import`type RedisClient = InstanceType<typeof Redis>`,默认导入在 TS 中不可构造 |
| jwt.sign expiresIn 类型 | `@types/jsonwebtoken` v9 `expiresIn` 类型为 `number \| StringValue`,普通 string 不兼容,用 `ttlToSeconds(ttl)` 返回 number |
| 健康检查依赖 | `/readyz` 5 依赖DB(SELECT 1) / Redis(ping) / Kafka(producer 实例) / JWKS(密钥文件可读) / gRPC(进程内) |
| 优雅停机 | 顺序Kafka producer disconnect → Redis quit → DB pool endLifecycleService `onApplicationShutdown` |
| 场景 | 技术/规则 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 双入口 | REST `/v1/iam/*` + gRPC 50052package `next_edu_cloud.iam.v1`),同一 IamService 实例共用Hybrid app `connectMicroservice` |
| 认证 | RS256 非对称签名,私钥本地文件(`IAM_PRIVATE_KEY_PATH`),公钥通过 JWKS 端点暴露给 Gateway |
| JWT payload | `{ sub, email, roles, dataScope, type, iss, aud, jti(仅 refresh), kid(header) }`register 自动分配 teacher 角色 |
| Token TTL | access 15min / refresh 7dayrefresh 含 jti轮换时旧 jti 加入 Redis 黑名单 |
| RBAC | 角色/权限/角色-权限查询 + `getEffectivePermissions(userId)` API三层角色模型system/organization/temporarylevel 0/1/2 |
| 视口配置 | 4 层模型(导航/路由/组件/数据),`iam_role_viewports` 表含 `level` ENUM(admin/teacher/student/parent) |
| DataScope 解析 | 6 级self/subject/class/grade/school/allSUBJECT 替代 DISTRICT注入 JWT payload |
| 权限校验 | PermissionGuardAPP_GUARDDB 驱动 + Redis 缓存 TTL 5min`data_scope=all` 直放行Key `iam:perm:{userId}` |
| Token 黑名单 | Redis Key `iam:bl:{jti}` TTL 与 refresh_token 剩余有效期对齐logout/refresh 时写入 |
| 审计日志 | `iam_user_audit_log` 表 + `AuditCreated` Kafka 事件,登录/角色变更/密码修改等关键操作记录 |
| 家长-学生关系 | `iam_student_guardians` 表unique(studentId, guardianId)`GetChildrenByParent` RPC + `GET /v1/iam/children` REST |
| schema 表 | iam_users / iam_roles / iam_user_roles / iam_permissions / iam_role_permissions / iam_refresh_tokens / iam_role_viewports / iam_student_guardians / iam_user_audit_log / iam_password_history |
| Outbox | shared-ts `OutboxModule.forRoot({ config, db, kafkaProducer })`,表名 `iam_outbox`topic `edu.iam.user.events`/`edu.iam.role.events`/`edu.iam.audit.created` |
| AuthMiddleware 注册 | `app.module.ts` `configure()` 注册于 me/logout/viewports/permissions/roles/audit/children/change-password/users/totp 路由,解析 `x-user-*` 头;漏注册会报 "Missing user identity" |
| 公开路径 | register/login/refresh/jwks.json 无 `@RequirePermission()`PermissionGuard 旁路 |
| ESM 模式 DI | `providers: [IamService, IamRepository]` + 构造器 `@Inject(IamRepository)` 显式注入,避免 `undefined` 运行时错误 |
| APP_GUARD DI 导出 | PermissionGuard 注册为 APP_GUARDAppModule 级),其依赖 PermissionCacheService/IamRepository 须在 IamModule `exports` 中导出,否则 AppModule 上下文无法注入 |
| Proto 路径解析 | `resolveProtoPath()` 兼顾 cwd=monorepo 根CI与 cwd=services/iam`pnpm --filter` dev先 `packages/shared-proto/proto/`,回退 `../../packages/shared-proto/proto/` 与本地 `proto/`Docker |
| Drizzle ORM API | `inArray(col, vals)` 替代不存在的 `.in()``delete().where()` 不支持链式多次 where用 `and(eq(...), eq(...))` 组合 |
| Drizzle MySQL insert | `MySqlInsertBase` 无 `.ignore()` 方法PostgreSQL 才有),改用 SELECT-then-INSERT 手动存在性检查避免唯一键冲突 |
| Docker 多阶段构建 | builder 阶段先 `WORKDIR /app/packages/shared-ts && pnpm build` 再 `WORKDIR /app/services/iam && pnpm build`runtime 阶段需 `COPY --from=builder /app/packages/shared-ts/dist` 复制预编译产物 |
| Husky prepare 脚本移除 | `ENV HUSKY=0` 无效pnpm install 仍会执行 prepare用 `node -e "delete p.scripts.prepare"` 在 install 前删除根 package.json 的 prepare 脚本 |
| TOTP 表 schema | `iam_user_totp`user_id UNIQUE+ `iam_totp_backup_codes`user_id 索引 + code_hash必须在 `02-all-services-schema.sql` 初始化,否则 TOTP 启用端点报 Table not exists |
| JWT RS256 密钥生成 | `docker run --rm -v <host>:/keys alpine/openssl genrsa -out /keys/iam-private.pem 2048` + `openssl rsa -in ... -pubout -out ...`,加载用 `IAM_PRIVATE_KEY_PATH` / `IAM_PUBLIC_KEY_PATH` |
| PowerShell curl JSON | PowerShell 内联 JSON 转义不可靠(双引号嵌套),改用 `curl.exe -d "@file.json"` 从文件读取请求体;务必用 `curl.exe` 而非 PowerShell 别名 `curl` |
| ESLint no-useless-escape | 正则字符类 `[...]` 内的 `[` `]` `/` 无需转义,`/[!@#$%^&*()_+\-=[\]{};':"\\ | ,.<>/?`~]/` 是正确写法(`\[` `\/` 触发 no-useless-escape |
| Drizzle DataScope 类型 | `mysqlEnum` 推断类型为字面量联合Repository 方法参数须用 `DataScope` 类型而非 `string`,否则 TS 报错 |
| ioredis 类型 | `import { Redis } from "ioredis"`named import`type RedisClient = InstanceType<typeof Redis>`,默认导入在 TS 中不可构造 |
| jwt.sign expiresIn 类型 | `@types/jsonwebtoken` v9 `expiresIn` 类型为 `number \| StringValue`,普通 string 不兼容,用 `ttlToSeconds(ttl)` 返回 number |
| 健康检查依赖 | `/readyz` 5 依赖DB(SELECT 1) / Redis(ping) / Kafka(producer 实例) / JWKS(密钥文件可读) / gRPC(进程内) |
| 优雅停机 | 顺序Kafka producer disconnect → Redis quit → DB pool endLifecycleService `onApplicationShutdown` |
### 2.4 core-eduTS/NestJSP3