feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
This commit is contained in:
411
docs/runbooks/local-stack.md
Normal file
411
docs/runbooks/local-stack.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# 本地全栈启动运维手册
|
||||
|
||||
> 版本:v2.1(混合部署模式)
|
||||
> 日期:2026-07-17
|
||||
> 适用范围:Edu v2.1 架构本地开发与测试环境
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构概览
|
||||
|
||||
### 1.1 部署模式
|
||||
|
||||
**混合部署**:基础设施 + Apollo Router 用 Docker,应用服务本地运行。
|
||||
|
||||
| 层级 | 运行方式 | 说明 |
|
||||
| ------------- | -------------- | --------------------------------------------------------------- |
|
||||
| 基础设施 | Docker Compose | MySQL/Redis/Kafka/ClickHouse/Neo4j/ES 等 |
|
||||
| Apollo Router | Docker 容器 | 联邦 GraphQL 网关(组合 5 个子图,端口 3000) |
|
||||
| 应用服务 | 本地进程 | NestJS(pnpm exec nest start)/ Python(uvicorn)/ Go(go run) |
|
||||
| 前端 | 本地进程 | Next.js dev server(portal-shell) |
|
||||
|
||||
> Apollo Router 在 Docker 容器中运行,通过 `host.docker.internal` 访问 host 上的 5 个 Federation 2 子图(iam/core-edu/content/msg/config-service),compose 出 supergraph 后对外暴露 `http://localhost:3000/graphql`。
|
||||
|
||||
### 1.2 服务端口映射
|
||||
|
||||
#### 基础设施(Docker)
|
||||
|
||||
| 服务 | 端口 | 容器名 |
|
||||
| ------------- | --------- | ----------------- |
|
||||
| MySQL | 3306 | edu-mysql |
|
||||
| Redis | 6379 | edu-redis |
|
||||
| Kafka | 9092 | edu-kafka |
|
||||
| Zookeeper | 2181 | edu-zookeeper |
|
||||
| ClickHouse | 8123 | edu-clickhouse |
|
||||
| Neo4j | 7474/7687 | edu-neo4j |
|
||||
| Elasticsearch | 9200 | edu-es |
|
||||
| Debezium | 8083 | edu-debezium |
|
||||
| Apollo Router | 3000/8088 | edu-apollo-router |
|
||||
| Jaeger | 16686 | edu-jaeger |
|
||||
| Prometheus | 9090 | edu-prometheus |
|
||||
| Grafana | 3030 | edu-grafana |
|
||||
| Alertmanager | 9093 | edu-alertmanager |
|
||||
| Loki | 3100 | edu-loki |
|
||||
|
||||
#### 应用服务(本地)
|
||||
|
||||
| 服务 | 端口 | gRPC 端口 | 类型 | 健康端点 |
|
||||
| -------------- | ---- | --------- | ------- | ----------- |
|
||||
| iam | 3002 | 50052 | NestJS | /healthz |
|
||||
| config-service | 3011 | 50059 | NestJS | /healthz |
|
||||
| classes | 3001 | 50053 | NestJS | /healthz |
|
||||
| core-edu | 3004 | 50054 | NestJS | /healthz |
|
||||
| content | 3005 | 50055 | NestJS | /healthz |
|
||||
| msg | 3007 | 50056 | NestJS | /healthz |
|
||||
| data-ana | 3006 | - | Python | /healthz |
|
||||
| ai | 3008 | - | Python | /healthz |
|
||||
| api-gateway | 8080 | - | Go | /healthz |
|
||||
| push-gateway | 8081 | - | Go | /healthz |
|
||||
| portal-shell | 4010 | - | Next.js | /api/health |
|
||||
|
||||
---
|
||||
|
||||
## 2. 一键脚本使用
|
||||
|
||||
### 2.1 一键启动
|
||||
|
||||
```powershell
|
||||
# 启动全部(基础设施 + 应用服务 + Apollo Router)
|
||||
.\scripts\start-all.ps1
|
||||
|
||||
# 仅启动基础设施
|
||||
.\scripts\start-all.ps1 -SkipApps
|
||||
|
||||
# 仅启动应用服务(基础设施已运行)
|
||||
.\scripts\start-all.ps1 -SkipInfra
|
||||
|
||||
# 自动杀掉占用端口的进程(不交互确认)
|
||||
.\scripts\start-all.ps1 -Force
|
||||
|
||||
# 跳过 Apollo Router(仅用 config-service 直连,调试子图时使用)
|
||||
.\scripts\start-all.ps1 -SkipRouter
|
||||
```
|
||||
|
||||
**启动流程**(8 阶段):
|
||||
|
||||
1. 加载 `.env` 文件
|
||||
2. 启动 Docker 基础设施(p3+p5 profile + observability)
|
||||
3. 基础设施健康检查
|
||||
4. 端口冲突检查与清理
|
||||
5. 确保 `@edu/shared-ts` 已编译
|
||||
6. 清理 NestJS tsbuildinfo 缓存
|
||||
7. 启动应用服务 + 健康检查
|
||||
8. 启动 Apollo Router(Docker 容器,组合 5 子图 supergraph)
|
||||
|
||||
> Apollo Router 依赖 5 个 Federation 2 子图(iam/core-edu/content/msg/config-service)就绪后才能 compose supergraph,因此在第 7 步应用服务健康检查通过后才启动。如果跳过 `-SkipRouter`,portal-shell 会自动降级到 config-service 直连(http://localhost:3011)。
|
||||
|
||||
### 2.2 一键关闭
|
||||
|
||||
```powershell
|
||||
# 关闭应用服务 + Apollo Router
|
||||
.\scripts\stop-all.ps1
|
||||
|
||||
# 强制按端口杀进程(窗口关闭后进程残留时使用)
|
||||
.\scripts\stop-all.ps1 -KillByPort
|
||||
|
||||
# 关闭应用 + Apollo Router + Docker 基础设施
|
||||
.\scripts\stop-all.ps1 -IncludeDocker
|
||||
|
||||
# 全部关闭
|
||||
.\scripts\stop-all.ps1 -KillByPort -IncludeDocker
|
||||
|
||||
# 仅关闭应用,保留 Router 容器
|
||||
.\scripts\stop-all.ps1 -SkipRouter
|
||||
```
|
||||
|
||||
> 关闭顺序:Apollo Router 容器 → 应用服务窗口 → 按端口杀残留 → Docker 基础设施。先关 Router 避免子图关闭时 router 刷连接错误日志。
|
||||
|
||||
### 2.3 典型工作流
|
||||
|
||||
```powershell
|
||||
# 早晨开始工作:一键启动
|
||||
.\scripts\start-all.ps1
|
||||
|
||||
# 中午休息:仅关闭应用(保留 Docker 数据)
|
||||
.\scripts\stop-all.ps1 -KillByPort
|
||||
|
||||
# 下午继续:仅启动应用
|
||||
.\scripts\start-all.ps1 -SkipInfra -Force
|
||||
|
||||
# 下班:全部关闭
|
||||
.\scripts\stop-all.ps1 -KillByPort -IncludeDocker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 手动操作
|
||||
|
||||
### 3.1 仅启动基础设施
|
||||
|
||||
```powershell
|
||||
cd infra
|
||||
# 基础设施服务(跳过需构建镜像的 app 服务)
|
||||
docker compose -f docker-compose.yml --profile p3 --profile p5 up -d mysql redis kafka zookeeper clickhouse neo4j elasticsearch debezium-connect
|
||||
# 可观测性栈
|
||||
docker compose -f docker-compose.yml --profile observability up -d
|
||||
```
|
||||
|
||||
### 3.2 单独启动某个应用服务
|
||||
|
||||
```powershell
|
||||
# NestJS 服务
|
||||
pnpm --filter @edu/iam-service exec nest start
|
||||
pnpm --filter @edu/config-service exec nest start
|
||||
pnpm --filter @edu/classes-service exec nest start
|
||||
pnpm --filter @edu/core-edu-service exec nest start
|
||||
pnpm --filter @edu/content-service exec nest start
|
||||
pnpm --filter @edu/msg-service exec nest start
|
||||
|
||||
# Python 服务
|
||||
cd services\data-ana; uv run uvicorn data_ana.main:app --app-dir src --host 0.0.0.0 --port 3006
|
||||
cd services\ai; uv run uvicorn ai.main:app --app-dir src --host 0.0.0.0 --port 3008
|
||||
|
||||
# Go 服务
|
||||
cd services\api-gateway; go run .
|
||||
cd services\push-gateway; go run .
|
||||
|
||||
# 前端
|
||||
pnpm --filter @edu/portal-shell dev
|
||||
```
|
||||
|
||||
### 3.3 健康检查
|
||||
|
||||
```powershell
|
||||
# 单个服务
|
||||
curl http://localhost:3002/healthz # iam
|
||||
curl http://localhost:4010/api/health # portal-shell
|
||||
|
||||
# 批量健康检查脚本
|
||||
.\scripts\health-check.ps1
|
||||
```
|
||||
|
||||
### 3.4 查看 Docker 容器状态
|
||||
|
||||
```powershell
|
||||
# 运行中的容器
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
# 查看容器日志
|
||||
docker logs edu-mysql --tail 50
|
||||
docker logs edu-kafka --tail 50 -f
|
||||
|
||||
# 容器健康状态
|
||||
docker inspect -f '{{.State.Health.Status}}' edu-mysql
|
||||
```
|
||||
|
||||
### 3.5 手动启动 Apollo Router
|
||||
|
||||
Apollo Router 通常由 `start-all.ps1` 自动启动。如需单独启动(例如调试 router 配置):
|
||||
|
||||
```powershell
|
||||
# 前置:5 个 Federation 2 子图必须先在 host 上运行
|
||||
# iam (3002) / core-edu (3004) / content (3005) / msg (3007) / config-service (3011)
|
||||
# 验证子图:rover subgraph introspect http://localhost:3002/graphql
|
||||
# 返回应包含 @key、@link、_service 字段
|
||||
|
||||
# 启动 Router 容器(dev 模式,子图在 host 上)
|
||||
docker rm -f edu-apollo-router 2>&1 | Out-Null
|
||||
docker run -d --name edu-apollo-router `
|
||||
--add-host=host.docker.internal:host-gateway `
|
||||
-p 3000:3000 -p 8088:8088 `
|
||||
-v e:\Desktop\Edu\infra\apollo-router\dev-supergraph.yaml:/dist/supergraph.yaml `
|
||||
-v e:\Desktop\Edu\infra\apollo-router\dev-entrypoint.sh:/dist/entrypoint.sh `
|
||||
-v e:\Desktop\Edu\infra\apollo-router\router.yaml:/dist/configuration.yaml `
|
||||
-e ROUTER_AUTH_SECRET=dev-router-secret `
|
||||
-e APOLLO_ELV2_LICENSE=accept `
|
||||
-e HTTP_PROXY=http://host.docker.internal:7897 `
|
||||
-e HTTPS_PROXY=http://host.docker.internal:7897 `
|
||||
-e NO_PROXY=localhost,127.0.0.1,host.docker.internal `
|
||||
edu/apollo-router:dev `
|
||||
/dist/entrypoint.sh
|
||||
|
||||
# 验证
|
||||
curl http://localhost:8088/health # {"status":"UP"}
|
||||
curl -X POST http://localhost:3000/graphql `
|
||||
-H "Content-Type: application/json" `
|
||||
-d '{\"query\":\"{ __schema { queryType { fields { name } } } }\"}'
|
||||
|
||||
# 查看日志(supergraph compose 过程 + router 启动)
|
||||
docker logs edu-apollo-router -f
|
||||
```
|
||||
|
||||
> 首次启动需 `docker build -t edu/apollo-router:dev infra/apollo-router/` 构建镜像(包含 rover CLI 和预下载的 supergraph 插件)。`HTTP_PROXY` 用于容器内 rover 下载 supergraph 插件(如已预下载到镜像则可省略)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 环境变量
|
||||
|
||||
### 4.1 必需的 .env 变量
|
||||
|
||||
根目录 `.env` 文件必须包含以下变量:
|
||||
|
||||
```env
|
||||
# 数据库
|
||||
DATABASE_URL=mysql://edu:changeme@localhost:3306/next_edu_cloud
|
||||
MYSQL_ROOT_PASSWORD=changeme
|
||||
MYSQL_DATABASE=next_edu_cloud
|
||||
MYSQL_USER=edu
|
||||
MYSQL_PASSWORD=changeme
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Kafka
|
||||
KAFKA_BROKERS=localhost:9092
|
||||
|
||||
# JWT 密钥路径
|
||||
IAM_PRIVATE_KEY_PATH=E:\Desktop\Edu\keys\iam-private.pem
|
||||
IAM_PUBLIC_KEY_PATH=E:\Desktop\Edu\keys\iam-public.pem
|
||||
|
||||
# 开发模式(绕过 JWT 校验,接受 dev-token)
|
||||
DEV_MODE=true
|
||||
|
||||
# Router 认证密钥(子图校验 Apollo Router 请求)
|
||||
ROUTER_AUTH_SECRET=dev-router-secret
|
||||
|
||||
# Neo4j
|
||||
NEO4J_PASSWORD=changeme
|
||||
|
||||
# 可观测性
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
```
|
||||
|
||||
### 4.2 开发模式(DEV_MODE=true)
|
||||
|
||||
- 绕过 JWT 校验
|
||||
- 接受 `Authorization: Bearer dev-token` 作为有效令牌
|
||||
- 预定义用户角色
|
||||
|
||||
### 4.3 env-loader 机制
|
||||
|
||||
NestJS 服务通过 `@edu/shared-ts/env-loader` 在启动时自动从 monorepo 根加载 `.env` 文件,解决 PowerShell → pnpm → nest 子进程环境变量丢失问题。所有 NestJS 服务的 `main.ts` 顶部已添加:
|
||||
|
||||
```typescript
|
||||
import "@edu/shared-ts/env-loader";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 前置准备
|
||||
|
||||
### 5.1 首次运行
|
||||
|
||||
```powershell
|
||||
# 1. 安装依赖
|
||||
pnpm install --no-frozen-lockfile
|
||||
|
||||
# 2. 编译 shared-ts(其他服务依赖它)
|
||||
pnpm --filter @edu/shared-ts build
|
||||
|
||||
# 3. 生成 JWT 密钥(如不存在)
|
||||
mkdir keys
|
||||
openssl genrsa -out keys/iam-private.pem 2048
|
||||
openssl rsa -in keys/iam-private.pem -pubout -out keys/iam-public.pem
|
||||
|
||||
# 4. 配置 .env(从 .env.example 复制并修改)
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 5.2 工具链要求
|
||||
|
||||
| 工具 | 版本 | 用途 |
|
||||
| ---------- | ------ | ---------------------------- |
|
||||
| Node.js | 22+ | NestJS / Next.js |
|
||||
| pnpm | 11+ | 包管理 |
|
||||
| Go | 1.22+ | api-gateway / push-gateway |
|
||||
| uv | latest | Python 服务(data-ana / ai) |
|
||||
| Docker | 25+ | 基础设施容器 |
|
||||
| PowerShell | 7+ | 启动脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 访问入口
|
||||
|
||||
### 6.1 应用入口
|
||||
|
||||
| 入口 | URL | 说明 |
|
||||
| ------------ | ----------------------------- | -------------------------- |
|
||||
| Portal Shell | http://localhost:4010 | 统一前端 |
|
||||
| API Gateway | http://localhost:8080 | REST API 网关 |
|
||||
| IAM GraphQL | http://localhost:3002/graphql | IAM 子图(需 Router Auth) |
|
||||
|
||||
### 6.2 监控入口
|
||||
|
||||
| 入口 | URL | 登录 |
|
||||
| ------------ | ---------------------- | ------------- |
|
||||
| Grafana | http://localhost:3030 | admin / admin |
|
||||
| Jaeger | http://localhost:16686 | 无需登录 |
|
||||
| Prometheus | http://localhost:9090 | 无需登录 |
|
||||
| Alertmanager | http://localhost:9093 | 无需登录 |
|
||||
|
||||
### 6.3 基础设施管理
|
||||
|
||||
| 服务 | URL / 端口 | 登录 |
|
||||
| ------------- | --------------------- | ------------------ |
|
||||
| MySQL | localhost:3306 | edu / changeme |
|
||||
| Redis | localhost:6379 | 无密码 |
|
||||
| Neo4j | http://localhost:7474 | neo4j / changeme |
|
||||
| ClickHouse | http://localhost:8123 | default / (无密码) |
|
||||
| Elasticsearch | http://localhost:9200 | 无需认证 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 故障排查
|
||||
|
||||
### 7.1 常见问题
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------ |
|
||||
| 端口被占用 | `.\scripts\stop-all.ps1 -KillByPort` 或 `start-all.ps1 -Force` |
|
||||
| NestJS 环境变量缺失 | 确认 `.env` 存在且 `main.ts` 顶部有 `import "@edu/shared-ts/env-loader"` |
|
||||
| shared-ts 找不到 | `pnpm --filter @edu/shared-ts build` |
|
||||
| pnpm install 失败(lockfile) | `pnpm install --no-frozen-lockfile` |
|
||||
| Docker 容器启动失败 | `docker logs <container>` 查看日志 |
|
||||
| MySQL 连接失败 | 确认 `edu-mysql` 容器 healthy:`docker ps` |
|
||||
| Kafka 连接失败 | 确认用 `localhost:9092`(OUTSIDE listener) |
|
||||
| 前端首页超时 | Next.js dev 首次编译慢,等待 30 秒后重试 |
|
||||
| GraphQL 子图返回 401 | 子图需 `Router-Authorization` header(ADR-036),非直接访问 |
|
||||
|
||||
### 7.2 日志查看
|
||||
|
||||
```powershell
|
||||
# Docker 容器日志
|
||||
docker logs edu-mysql --tail 100 -f
|
||||
docker logs edu-kafka --tail 100 -f
|
||||
|
||||
# 应用服务日志
|
||||
# 查看 edu-app-* 命名的 PowerShell 窗口
|
||||
```
|
||||
|
||||
### 7.3 重置环境
|
||||
|
||||
```powershell
|
||||
# 完全重置(删除所有数据)
|
||||
.\scripts\stop-all.ps1 -KillByPort -IncludeDocker
|
||||
cd infra
|
||||
docker compose -f docker-compose.yml --profile p3 --profile p5 --profile observability down -v
|
||||
cd ..
|
||||
.\scripts\start-all.ps1
|
||||
```
|
||||
|
||||
### 7.4 Temporal 服务(可选)
|
||||
|
||||
Temporal 服务用于 AI 工作流引擎(ADR-030),当前未包含在一键启动脚本中。如需启动:
|
||||
|
||||
```powershell
|
||||
cd infra
|
||||
docker compose -f docker-compose.yml --profile p3 up -d temporal-postgresql temporal temporal-ui
|
||||
# 注意:temporal auto-setup 镜像需调整 DB 环境变量为 postgres,当前配置有已知问题
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 架构约束
|
||||
|
||||
> 以下约束来自 project_rules.md 与 ADR,脚本已内置遵守。
|
||||
|
||||
- **混合部署**:基础设施 Docker + 应用本地,避免应用镜像构建失败
|
||||
- **GraphQL 子图隔离**:子图仅接受 Apollo Router 请求(带 `Router-Authorization` header),不直接对外
|
||||
- **gRPC 内部通信**:服务间通过 gRPC(50052-50059),不通过 GraphQL
|
||||
- **CDC + Outbox**:业务代码写 outbox 表,Debezium 监听 binlog 投递 Kafka
|
||||
- **DEV_MODE**:开发模式绕过 JWT 校验,仅限本地开发
|
||||
@@ -727,33 +727,34 @@
|
||||
|
||||
> v2.1 架构:单 Next.js App Router 容器 + Micro-kernel 插件系统,替代旧 4 端微前端。关联 spec `2026-07-14-portal-shell-widget-dashboard-design.md`。
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 插件契约跨包共享 | `packages/shared-ts/src/contracts/` 定义 PluginProps/PluginManifest/Layout 类型;`PluginManifest.Component` 用 `unknown`(shared-ts 不依赖 React),前端包引用时断言为 `React.ComponentType<PluginProps>` |
|
||||
| TVars 泛型约束与 TS interface 不兼容 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下,widget 文件用 `interface XxxVars` 会报 TS2344;改用 `type XxxVars = {...}` 类型别名(type alias 满足 index signature,interface 不满足) |
|
||||
| shared-ts contracts 子路径导出 | `package.json` exports 新增 `"./contracts"` 指向 `./dist/contracts/index.js`;hooks 包通过 `@edu/shared-ts/contracts` 引用;typecheck 前需 `pnpm --filter @edu/shared-ts run build` 编译到 dist/ |
|
||||
| Vitest 解析 workspace 子路径导出失败 | vitest.config.ts resolve.alias 需显式添加 `"@edu/shared-ts/contracts"` → 源码路径;Vite 不自动解析 package.json exports 字段 |
|
||||
| portal-shell tsconfig paths 补全 | tsconfig.json paths 需添加 `"@edu/shared-ts/contracts": ["../../packages/shared-ts/src/contracts/index.ts"]`,否则 tsc 报 TS2307 |
|
||||
| 插件 dynamic import 注册 | `Registry.tsx` 用 `next/dynamic`(ssr:false)懒加载 31 个内置插件;loading 展示 `PluginSkeleton` 变体(card/list/table/stats/chart) |
|
||||
| URL Search Params + Zustand 状态分层 | URL 驱动可分享上下文(classId/childId/termId/view/examId/subjectId);Zustand 管理纯 UI 状态(theme/locale/sidebarCollapsed);插件间禁止直接 import |
|
||||
| useWidgetMutation API 签名 | 返回 `{ run, ...result }`,`run` 是 `async (variables: TVars): Promise<TData \| undefined>`;消费者用 `const { run } = useWidgetMutation(...)` 解构 |
|
||||
| usePluginConfig 不直接调 API | fetcher 由消费者注入,保持 @edu/hooks "hooks 不直接调 API" 原则;支持轮询刷新(refreshInterval 默认 5 分钟)+ 网络恢复刷新 + visibilitychange 刷新 |
|
||||
| usePluginStore 注入模式 | `useSyncExternalStore` + `injectPluginStore()` 让 hooks 包不直接依赖特定 store 实例;未注入 store 时返回 DEFAULT_STATE 空操作 |
|
||||
| 28 个内置插件分类 | universal(7) + sidebar(4) + topbar(4) + teacher(4) + student(4) + parent(2) + admin(3);每个插件含 `index.tsx` + `plugin.manifest.ts`,manifest 导出 `manifestMeta: Omit<PluginManifest, "Component">` |
|
||||
| PluginLifecycle 版本兼容 | MVP 只校验 major 版本(`checkVersionCompatibility` 解析 `^`/`~`/`>=` 前缀 + major 数字);完整 semver range 校验待引入 semver 库 |
|
||||
| pnpm install --no-frozen-lockfile | parent-portal package.json 新增 `@opentelemetry/instrumentation-document-load` 后 lockfile 过期;开发环境用 `--no-frozen-lockfile` 安装,CI 用 frozen |
|
||||
| Widget 内联 gql 字面量废弃 | 禁止 `const Q = gql\`...\``直接写在 widget;统一抽取到`lib/api/operations/*.graphql.ts`,widget 只 import `lib/api/<domain>.ts` 的语义化函数(ADR-042) |
|
||||
| 4 层数据访问分层 | Widget(UI)→ API(语义化函数)→ Operations(gql DocumentNode 集中)→ Hook(useWidgetQuery/useWidgetMutation 封装 Apollo);codegen 从 7 子图 schema 生成类型 |
|
||||
| graphql-codegen skipDocumentsValidation | codegen.yml 必须设 `skipDocumentsValidation: true`,避免子图未启动时校验 operations 失败;schema 归一化脚本移除 federation 指令(@key/@requires/@extends)防止误解析 |
|
||||
| useNotifications 命名冲突 | `universal.ts` 与 `topbar.ts` 同时导出 `useNotifications` 在 barrel `index.ts` 冲突(TS2308);topbar 改名 `useNotificationBell`(铃铛专用,限 N 条) |
|
||||
| APQ(Automatic Persisted Queries) | `apollo-client.ts` 用 `createPersistedQueryLink({ sha256 })` + `crypto-hash`;env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭;Link 链顺序:authLink → pqLink → httpLink |
|
||||
| PQ Manifest 生成 | `scripts/generate-pq-manifest.ts` 遍历 `operations/index.ts` 中 DocumentNode,`print(doc)` + `sha256(query)` 写入 `public/pq-manifest.json`;`prebuild` 钩子串联 codegen + generate |
|
||||
| Windows ESM 动态 import 路径 | Node ESM 动态 `import()` 不支持 Windows 盘符路径(`e:\...`),必须 `url.pathToFileURL(path).href` 转 `file://` URL 再 import |
|
||||
| apollo-router PQ manifest 挂载 | docker-compose 把 `apps/portal-shell/public/pq-manifest.json` 挂载到 router `/etc/apollo-router/pq-manifest.json:ro`;entrypoint.sh 启动前校验 `APOLLO_REQUIRE_PQ_MANIFEST=true` 时文件存在性 |
|
||||
| apollo-router 安全限制 | `router.yaml` 配置 `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5`;`supergraph.introspection` 由 env `APOLLO_ROUTER_INTROSPECTION` 控制(生产 false) |
|
||||
| Resolver @RequirePermission 字段级守卫 | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点;50 resolver 审计后补齐 19 个 TS resolver,Python 子图待补 Strawberry/Ariadne 中间件 |
|
||||
| TS interface 不满足 Record 约束 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下 widget 用 `interface XxxVars` 报 TS2344;改 `type XxxVars = {...}` 别名(满足 index signature) |
|
||||
| parent.test.tsx 可选链 | `data?.[0].name` 报 TS2532(`data?.[0]` 可能为 undefined);改 `data?.[0]?.name` 双重可选链 |
|
||||
| PowerShell 不支持 heredoc | `git commit -m "$(cat <<'EOF'...)"` 在 PowerShell 报错;commit 消息写临时文件 `.git/COMMIT_MSG.txt`,用 `git commit -F .git/COMMIT_MSG.txt` |
|
||||
| commitlint body-max-line-length | commit body 每行 ≤100 字符,Plan/Spec 路径过长会超限;移除 URL 行或换行简化 |
|
||||
| commitlint scope-enum | `security` / `graphql` 不在允许 scope 列表;用 `docs` scope 提交审计报告,或用无 scope commit |
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 插件契约跨包共享 | `packages/shared-ts/src/contracts/` 定义 PluginProps/PluginManifest/Layout 类型;`PluginManifest.Component` 用 `unknown`(shared-ts 不依赖 React),前端包引用时断言为 `React.ComponentType<PluginProps>` |
|
||||
| TVars 泛型约束与 TS interface 不兼容 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下,widget 文件用 `interface XxxVars` 会报 TS2344;改用 `type XxxVars = {...}` 类型别名(type alias 满足 index signature,interface 不满足) |
|
||||
| shared-ts contracts 子路径导出 | `package.json` exports 新增 `"./contracts"` 指向 `./dist/contracts/index.js`;hooks 包通过 `@edu/shared-ts/contracts` 引用;typecheck 前需 `pnpm --filter @edu/shared-ts run build` 编译到 dist/ |
|
||||
| Vitest 解析 workspace 子路径导出失败 | vitest.config.ts resolve.alias 需显式添加 `"@edu/shared-ts/contracts"` → 源码路径;Vite 不自动解析 package.json exports 字段 |
|
||||
| portal-shell tsconfig paths 补全 | tsconfig.json paths 需添加 `"@edu/shared-ts/contracts": ["../../packages/shared-ts/src/contracts/index.ts"]`,否则 tsc 报 TS2307 |
|
||||
| 插件 dynamic import 注册 | `Registry.tsx` 用 `next/dynamic`(ssr:false)懒加载 31 个内置插件;loading 展示 `PluginSkeleton` 变体(card/list/table/stats/chart) |
|
||||
| URL Search Params + Zustand 状态分层 | URL 驱动可分享上下文(classId/childId/termId/view/examId/subjectId);Zustand 管理纯 UI 状态(theme/locale/sidebarCollapsed);插件间禁止直接 import |
|
||||
| useWidgetMutation API 签名 | 返回 `{ run, ...result }`,`run` 是 `async (variables: TVars): Promise<TData \| undefined>`;消费者用 `const { run } = useWidgetMutation(...)` 解构 |
|
||||
| usePluginConfig 不直接调 API | fetcher 由消费者注入,保持 @edu/hooks "hooks 不直接调 API" 原则;支持轮询刷新(refreshInterval 默认 5 分钟)+ 网络恢复刷新 + visibilitychange 刷新 |
|
||||
| usePluginStore 注入模式 | `useSyncExternalStore` + `injectPluginStore()` 让 hooks 包不直接依赖特定 store 实例;未注入 store 时返回 DEFAULT_STATE 空操作 |
|
||||
| 28 个内置插件分类 | universal(7) + sidebar(4) + topbar(4) + teacher(4) + student(4) + parent(2) + admin(3);每个插件含 `index.tsx` + `plugin.manifest.ts`,manifest 导出 `manifestMeta: Omit<PluginManifest, "Component">` |
|
||||
| PluginLifecycle 版本兼容 | MVP 只校验 major 版本(`checkVersionCompatibility` 解析 `^`/`~`/`>=` 前缀 + major 数字);完整 semver range 校验待引入 semver 库 |
|
||||
| pnpm install --no-frozen-lockfile | parent-portal package.json 新增 `@opentelemetry/instrumentation-document-load` 后 lockfile 过期;开发环境用 `--no-frozen-lockfile` 安装,CI 用 frozen |
|
||||
| Widget 内联 gql 字面量废弃 | 禁止 `const Q = gql\`...\``直接写在 widget;统一抽取到`lib/api/operations/*.graphql.ts`,widget 只 import `lib/api/<domain>.ts` 的语义化函数(ADR-042) |
|
||||
| 4 层数据访问分层 | Widget(UI)→ API(语义化函数)→ Operations(gql DocumentNode 集中)→ Hook(useWidgetQuery/useWidgetMutation 封装 Apollo);codegen 从 7 子图 schema 生成类型 |
|
||||
| graphql-codegen skipDocumentsValidation | codegen.yml 必须设 `skipDocumentsValidation: true`,避免子图未启动时校验 operations 失败;schema 归一化脚本移除 federation 指令(@key/@requires/@extends)防止误解析 |
|
||||
| useNotifications 命名冲突 | `universal.ts` 与 `topbar.ts` 同时导出 `useNotifications` 在 barrel `index.ts` 冲突(TS2308);topbar 改名 `useNotificationBell`(铃铛专用,限 N 条) |
|
||||
| APQ(Automatic Persisted Queries) | `apollo-client.ts` 用 `createPersistedQueryLink({ sha256 })` + `crypto-hash`;env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭;Link 链顺序:authLink → pqLink → httpLink |
|
||||
| PQ Manifest 生成 | `scripts/generate-pq-manifest.ts` 遍历 `operations/index.ts` 中 DocumentNode,`print(doc)` + `sha256(query)` 写入 `public/pq-manifest.json`;`prebuild` 钩子串联 codegen + generate |
|
||||
| Windows ESM 动态 import 路径 | Node ESM 动态 `import()` 不支持 Windows 盘符路径(`e:\...`),必须 `url.pathToFileURL(path).href` 转 `file://` URL 再 import |
|
||||
| apollo-router PQ manifest 挂载 | docker-compose 把 `apps/portal-shell/public/pq-manifest.json` 挂载到 router `/etc/apollo-router/pq-manifest.json:ro`;entrypoint.sh 启动前校验 `APOLLO_REQUIRE_PQ_MANIFEST=true` 时文件存在性 |
|
||||
| apollo-router 安全限制 | `router.yaml` 配置 `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5`;`supergraph.introspection` 由 env `APOLLO_ROUTER_INTROSPECTION` 控制(生产 false) |
|
||||
| Resolver @RequirePermission 字段级守卫 | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点;50 resolver 审计后补齐 19 个 TS resolver,Python 子图待补 Strawberry/Ariadne 中间件 |
|
||||
| TS interface 不满足 Record 约束 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下 widget 用 `interface XxxVars` 报 TS2344;改 `type XxxVars = {...}` 别名(满足 index signature) |
|
||||
| parent.test.tsx 可选链 | `data?.[0].name` 报 TS2532(`data?.[0]` 可能为 undefined);改 `data?.[0]?.name` 双重可选链 |
|
||||
| PowerShell 不支持 heredoc | `git commit -m "$(cat <<'EOF'...)"` 在 PowerShell 报错;commit 消息写临时文件 `.git/COMMIT_MSG.txt`,用 `git commit -F .git/COMMIT_MSG.txt` |
|
||||
| commitlint body-max-line-length | commit body 每行 ≤100 字符,Plan/Spec 路径过长会超限;移除 URL 行或换行简化 |
|
||||
| commitlint scope-enum | `security` / `graphql` 不在允许 scope 列表;用 `docs` scope 提交审计报告,或用无 scope commit |
|
||||
| Turbopack 不支持 .js 后缀 import | Next 16 默认 Turbopack 无法像 webpack 那样通过 `resolve.extensionAlias` 将 `.js` 映射到 `.ts/.tsx`;`packages/ui-components` 和 `packages/hooks` 源码内部 import 必须去掉 `.js` 后缀(shared-ts 是 NestJS ESM 模式按规则 §3.4 保留 `.js` 后缀,不修改) |
|
||||
|
||||
Reference in New Issue
Block a user