chore(deps): 统一依赖管理 - pnpm 11 + node:22 + golang:1.25 + python:3.12 + shared-* 集中化
- 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:
@@ -11,20 +11,28 @@
|
||||
|
||||
### 1.1 多语言 monorepo 配置
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
|
||||
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
|
||||
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/_、services/classes、bff/_、apps/_、scripts/_),Go/Python 不入 |
|
||||
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
|
||||
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
|
||||
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
|
||||
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 |
|
||||
| husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits |
|
||||
| .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 |
|
||||
| ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`(flat config),lint 脚本去掉 `--ext .ts`,lint-staged 恢复 `eslint --fix` |
|
||||
| next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json` 时 `next lint` 触发 Strict/Base 选择提示,CI 中需预置配置或改 `eslint src` |
|
||||
| uv.lock 解析失败 | `uv.lock` 中 `grpcio-health-checking` 缺 `source` 字段且匹配多包,导致 `uv run pytest` 全 workspace Python 服务不可用,需重建 lock(`uv lock`) |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
|
||||
| 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` |
|
||||
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/_、services/classes、bff/_、apps/_、scripts/_),Go/Python 不入 |
|
||||
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
|
||||
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
|
||||
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
|
||||
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 |
|
||||
| husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits |
|
||||
| .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 |
|
||||
| ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`(flat config),lint 脚本去掉 `--ext .ts`,lint-staged 恢复 `eslint --fix` |
|
||||
| next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json` 时 `next lint` 触发 Strict/Base 选择提示,CI 中需预置配置或改 `eslint src` |
|
||||
| uv.lock 解析失败 | `uv.lock` 中 `grpcio-health-checking` 缺 `source` 字段且匹配多包,导致 `uv run pytest` 全 workspace Python 服务不可用,需重建 lock(`uv lock`) |
|
||||
| pnpm 11 allowBuilds | pnpm 11 默认拒绝 build scripts(postinstall),需在 `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-gateway)Dockerfile 必须用 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 context,COPY `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 改 RS256(IAM 签发,公钥校验) |
|
||||
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
|
||||
| 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 |
|
||||
| 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` |
|
||||
| 包结构 | `internal/{config,middleware,proxy}`,P6 扩展 handler/service/repository |
|
||||
| error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 |
|
||||
| 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`),P6 引入 viper |
|
||||
| Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
|
||||
| P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) |
|
||||
| JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 |
|
||||
| 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 |
|
||||
| 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` |
|
||||
| 包结构 | `internal/{config,middleware,proxy}`,P6 扩展 handler/service/repository |
|
||||
| error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 |
|
||||
| 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`),P6 引入 viper |
|
||||
| Dockerfile 多阶段 | golang:1.25-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0,依赖 shared-go 时用 repo 根作 build context |
|
||||
|
||||
### 1.6 可观测性(OTel + Prometheus + Loki)
|
||||
|
||||
@@ -245,32 +253,39 @@
|
||||
|
||||
### 2.3 iam(TS/NestJS)
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 双入口 | REST `/v1/iam/*` + gRPC 50052(package `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 7day,refresh 含 jti,轮换时旧 jti 加入 Redis 黑名单 |
|
||||
| RBAC | 角色/权限/角色-权限查询 + `getEffectivePermissions(userId)` API,三层角色模型(system/organization/temporary,level 0/1/2) |
|
||||
| 视口配置 | 4 层模型(导航/路由/组件/数据),`iam_role_viewports` 表含 `level` ENUM(admin/teacher/student/parent) |
|
||||
| DataScope 解析 | 6 级:self/subject/class/grade/school/all(SUBJECT 替代 DISTRICT),注入 JWT payload |
|
||||
| 权限校验 | PermissionGuard(APP_GUARD)DB 驱动 + 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_GUARD(AppModule 级),其依赖 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 end(LifecycleService `onApplicationShutdown`) |
|
||||
| 场景 | 技术/规则 |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 双入口 | REST `/v1/iam/*` + gRPC 50052(package `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 7day,refresh 含 jti,轮换时旧 jti 加入 Redis 黑名单 |
|
||||
| RBAC | 角色/权限/角色-权限查询 + `getEffectivePermissions(userId)` API,三层角色模型(system/organization/temporary,level 0/1/2) |
|
||||
| 视口配置 | 4 层模型(导航/路由/组件/数据),`iam_role_viewports` 表含 `level` ENUM(admin/teacher/student/parent) |
|
||||
| DataScope 解析 | 6 级:self/subject/class/grade/school/all(SUBJECT 替代 DISTRICT),注入 JWT payload |
|
||||
| 权限校验 | PermissionGuard(APP_GUARD)DB 驱动 + 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_GUARD(AppModule 级),其依赖 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 end(LifecycleService `onApplicationShutdown`) |
|
||||
|
||||
### 2.4 core-edu(TS/NestJS,P3)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user