diff --git a/README.md b/README.md index 4774b26..b373bf3 100644 --- a/README.md +++ b/README.md @@ -149,4 +149,4 @@ | 产品方向怎么定的? | [产品方向深度评估](长期规划/[参考]-产品方向深度评估.md) | | 怎么收费? | [商业化与支付模块](长期规划/[进行中]-商业化与支付模块.md) | | 怎么获客? | [营销冷启动调研方案](长期规划/营销与增长/[参考]-营销冷启动调研方案.md) | -| 服务器密码、SSH密钥? | [服务器凭据](凭据配置/服务器凭据.md) | +| 服务器密码、SSH密钥? | [服务器凭据](凭据配置/服务器凭据.md) | \ No newline at end of file diff --git a/技术设计/capi-contract-for-ios.md b/技术设计/capi-contract-for-ios.md new file mode 100644 index 0000000..9f3f4ec --- /dev/null +++ b/技术设计/capi-contract-for-ios.md @@ -0,0 +1,240 @@ +# iOS CAPI 接口契约 + +> 版本:0.1.0 | 更新:2026-05-24 | 基础 URL:`https://api.longde.cloud` + +## 通用约定 + +### 认证 + +所有 `/api/*` 端点需要 JWT Bearer Token: + +``` +Authorization: Bearer +``` + +Token 通过 Apple 登录获取,有效期 1 小时,用 refresh token 续期。 + +### 响应格式 + +```json +// 成功 +{ "success": true, "data": <资源>, "timestamp": "2026-05-24T..." } + +// 失败 +{ "success": false, "statusCode": 401, "message": "请先登录" } +``` + +### 分页 + +请求参数:`?page=1&limit=20`(limit 上限 100) + +响应格式因端点而异,无统一分页包装。常见结构为数据数组 + 通过 Data 字段间接判断。 + +### 错误码 + +| HTTP | 含义 | +|------|------| +| 200 | 成功 | +| 201 | 创建成功 | +| 400 | 请求参数错误(DTO 校验失败) | +| 401 | 未登录 / Token 过期 / 账号禁用 | +| 403 | 无权限 | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | + +--- + +## 接口清单 + +### 1. 认证 `/api/auth` + +| 方法 | 路径 | 说明 | Body | +|------|------|------|------| +| POST | `/api/auth/apple` | Apple 登录 | `{ identityToken, fullName?, email? }` | +| POST | `/api/auth/refresh` | 刷新 Token | `{ refreshToken }` | +| POST | `/api/auth/logout` | 登出 | `{ refreshToken }` | + +**Apple 登录响应:** +```json +{ + "accessToken": "eyJ...", + "refreshToken": "abc123...", + "user": { + "id": "cuid", + "email": "user@example.com", + "nickname": "用户", + "avatarUrl": null, + "role": "USER", + "status": "active", + "onboardingCompleted": false + } +} +``` + +### 2. 用户 `/api/users` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/users/me` | 当前用户信息 | +| PATCH | `/api/users/me` | 更新用户信息 | +| GET | `/api/users/me/profile` | 用户资料 | +| PATCH | `/api/users/me/profile` | 更新资料 | +| PATCH | `/api/users/me/preferences` | 偏好设置 | +| GET | `/api/users/me/membership` | 会员信息 | +| DELETE | `/api/users/me` | 注销账号 | + +### 3. 文件上传 `/api/files` + +| 方法 | 路径 | 说明 | Body | +|------|------|------|------| +| POST | `/api/files/upload-url` | 获取预签名上传 URL | `{ filename, mimeType, sizeBytes }` | +| POST | `/api/files/complete` | 确认上传完成 | `{ objectKey, checksum? }` | +| GET | `/api/files/:id` | 获取文件信息 | +| DELETE | `/api/files/:id` | 删除文件 | + +**上传流程:** +1. `POST /api/files/upload-url` → 获取 `{ uploadUrl, objectKey }` +2. iOS 直接 `PUT ` 到 COS(腾讯云对象存储) +3. `POST /api/files/complete` → 确认完成,获取 `fileId` + +### 4. 知识库 `/api/knowledge-bases` + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/knowledge-bases` | 创建知识库 | +| GET | `/api/knowledge-bases` | 知识库列表 | +| GET | `/api/knowledge-bases/:id` | 知识库详情 | +| PATCH | `/api/knowledge-bases/:id` | 更新 | +| DELETE | `/api/knowledge-bases/:id` | 删除 | + +### 5. 知识源 `/api/knowledge-bases/:kbId/sources` + +| 方法 | 路径 | 说明 | Body DTO | +|------|------|------|----------| +| POST | `/api/knowledge-bases/:kbId/sources` | 添加资料来源 | `AddSourceDto` | +| GET | `/api/knowledge-bases/:kbId/sources` | 资料列表 | — | +| GET | `/api/knowledge-bases/:kbId/sources/:id` | 资料详情 | — | +| DELETE | `/api/knowledge-bases/:kbId/sources/:id` | 删除 | — | + +**AddSourceDto:** `{ fileId?, type?, title?, originalFilename?, mimeType?, sizeBytes?, originalObjectKey? }` +- `type` 枚举:`file` | `link` | `manual` | `paste` + +### 6. 文档导入 `/api/imports` + +| 方法 | 路径 | 说明 | Body DTO | +|------|------|------|----------| +| POST | `/api/imports` | 创建导入任务 | `CreateImportDto` | +| GET | `/api/imports/:id/status` | 查询导入状态 | — | + +**CreateImportDto:** `{ userId?, knowledgeBaseId?, fileName?, sourceType?, rawText? }` + +**状态响应:** +```json +{ + "id": "import-001", + "fileName": "笔记.pdf", + "status": "QUEUED", + "progress": 0, + "message": "任务已加入队列" +} +``` + +### 7. 学习会话 `/api/learning-sessions` + +| 方法 | 路径 | 说明 | Body DTO | +|------|------|------|----------| +| POST | `/api/learning-sessions` | 开始学习 | `StartSessionDto` | +| POST | `/api/learning-sessions/:id/end` | 结束会话 | — | +| GET | `/api/learning-sessions` | 会话列表 | Query: `?page=1&limit=20` | + +**StartSessionDto:** `{ knowledgeItemId?, knowledgeBaseId?, mode? }` +- `mode` 枚举:`active_recall` | `feynman` | `review` | `reading` + +### 8. 主动回忆 `/api/active-recalls` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/active-recalls` | 获取题目列表 | +| POST | `/api/active-recalls/:id/submit` | 提交回答 | + +### 9. AI 分析 `/api/ai-analysis` + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/ai-analysis` | 发起主动回忆分析 | +| POST | `/api/ai-analysis/feynman` | 发起费曼评估 | +| GET | `/api/ai-analysis/jobs/:id` | 查询作业状态 | +| GET | `/api/ai-analysis/:id` | 获取分析结果 | + +### 10. 复习 `/api/reviews` + +| 方法 | 路径 | 说明 | Body | +|------|------|------|------| +| GET | `/api/reviews/due` | 今日待复习 | — | +| POST | `/api/reviews/:id/submit` | 提交复习结果 | `SubmitReviewDto` | +| POST | `/api/reviews/generate-cards` | 生成卡片 | — | + +### 11. 薄弱项 `/api/focus-items` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/focus-items` | 薄弱项列表 | +| POST | `/api/focus-items` | 创建 | +| PATCH | `/api/focus-items/:id` | 更新 | +| POST | `/api/focus-items/:id/complete` | 标记完成 | + +### 12. 学习活动 `/api/activity` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/activity/heatmap` | 学习热力图 | +| GET | `/api/activity/summary` | 活动摘要 | +| GET | `/api/activity/trend` | 学习趋势 | +| GET | `/api/activity/streak` | 连续打卡天数 | +| GET | `/api/activity/recommendations` | 学习推荐 | + +### 13. RAG 对话 `/api/rag-chat` + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/rag-chat/sessions` | 创建对话 | +| GET | `/api/rag-chat/sessions` | 对话列表 | +| POST | `/api/rag-chat/sessions/:id/messages` | 发送消息 | +| GET | `/api/rag-chat/sessions/:id/messages` | 消息历史 | +| DELETE | `/api/rag-chat/sessions/:id` | 删除对话 | + +### 14. 通知 `/api/notifications` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/notifications` | 通知列表 | +| POST | `/api/notifications/:id/read` | 标记已读 | +| POST | `/api/notifications/read-all` | 全部已读 | +| GET/PATCH | `/api/notifications/preferences` | 通知偏好 | +| GET/POST/DELETE | `/api/notifications/push-tokens` | Push Token 管理 | + +### 15. 其他 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/feedback` | 提交反馈(type: bug/feature/general) | +| GET | `/api/workspace/dashboard` | 工作台首页 | +| GET | `/api/workspace/recent` | 最近记录 | +| POST | `/api/workspace/favorites` | 收藏 | +| GET | `/api/workspace/search` | 搜索 | +| GET | `/api/workspace/search-history` | 搜索历史 | + +--- + +## 全局请求头 + +| Header | 说明 | +|--------|------| +| `Authorization` | `Bearer ` | +| `Content-Type` | `application/json` | + +## 限制 + +- 请求体大小:10MB(全局 pipe 限制) +- 文件上传大小:20MB(单个文件) +- Token 有效期:1 小时(access),7 天(refresh) diff --git a/技术设计/capi-error-codes-and-status-enums.md b/技术设计/capi-error-codes-and-status-enums.md new file mode 100644 index 0000000..1b38081 --- /dev/null +++ b/技术设计/capi-error-codes-and-status-enums.md @@ -0,0 +1,122 @@ +# CAPI 错误码与状态枚举 + +> 版本:0.1.0 | 更新:2026-05-24 + +## HTTP 错误码 + +| HTTP | message | 触发场景 | +|------|---------|----------| +| 200 | — | 成功 | +| 201 | — | 创建成功 | +| 400 | — | 请求参数校验失败(DTO validation) | +| 401 | "请先登录" | 未携带 Token | +| 401 | "登录已过期,请重新登录" | Token 过期或无效 | +| 401 | "账号已被禁用" | 用户 status ≠ active | +| 401 | "账号不存在或已注销" | 用户不存在或 deletedAt 不为空 | +| 401 | "无效的访问令牌" | 管理员 Token 访问 CAPI | +| 401 | "Apple 登录未配置,请联系管理员" | Apple 登录未配置 | +| 401 | "刷新令牌无效或已过期" | Refresh Token 无效 | +| 401 | "账号已注销" | 刷新时发现用户已注销 | +| 403 | "dev-login is disabled in production" | 生产环境尝试 dev-login | +| 404 | "资源不存在" | 资源未找到 | +| 500 | — | 服务器内部错误 | + +## 模型状态枚举 + +### User +| 字段 | 可选值 | +|------|--------| +| `status` | `active`, `disabled` | +| `role` | `USER`, `ADMIN` | + +### KnowledgeBase +| 字段 | 可选值 | +|------|--------| +| `status` | `active`, `archived`, `deleted` | + +### KnowledgeItem +| 字段 | 可选值 | +|------|--------| +| `status` | `active`, `archived` | +| `itemType` | `note`, `flashcard`, `concept` | + +### KnowledgeSource +| 字段 | 可选值 | +|------|--------| +| `parseStatus` | `pending`, `parsing`, `completed`, `failed` | +| `indexStatus` | `pending`, `indexing`, `completed`, `failed` | +| `learningStatus` | `pending`, `learning`, `completed`, `skipped` | +| `type` | `file`, `link`, `manual`, `paste` | + +### DocumentImport +| 字段 | 可选值 | +|------|--------| +| `status` | `QUEUED`, `CLAIMED`, `DOWNLOADING`, `PARSING`, `OCR_PROCESSING`, `VISION_PROCESSING`, `CLEANING`, `CHUNKING`, `EMBEDDING`, `INDEXING`, `GENERATING_CANDIDATES`, `COMPLETED`, `FAILED`, `FAILED_FINAL` | + +### ImportCandidate +| 字段 | 可选值 | +|------|--------| +| `status` | `PENDING`, `ACCEPTED`, `REJECTED` | + +### LearningSession +| 字段 | 可选值 | +|------|--------| +| `status` | `active`, `completed`, `cancelled` | +| `mode` | `active_recall`, `feynman`, `review`, `reading` | + +### AiAnalysisJob +| 字段 | 可选值 | +|------|--------| +| `status` | `pending`, `processing`, `completed`, `failed` | +| `jobType` | `active_recall_analysis`, `feynman_evaluation` | + +### ReviewCard +| 字段 | 可选值 | +|------|--------| +| `status` | `active`, `completed`, `archived` | +| `scheduleState` | `new`, `learning`, `review`, `relearning` | +| `difficulty` | 数值(0.0–1.0,SM-2 算法) | + +### ReviewPlan +| 字段 | 可选值 | +|------|--------| +| `status` | `active`, `completed`, `skipped` | + +### FocusItem +| 字段 | 可选值 | +|------|--------| +| `status` | `open`, `completed` | +| `priority` | `high`, `normal`, `low` | + +### Feedback +| 字段 | 可选值 | +|------|--------| +| `type` | `bug`, `feature`, `general` | +| `status` | `pending`, `reviewed`, `resolved`, `closed` | + +### Notification +| 字段 | 可选值 | +|------|--------| +| `scope` | `user`, `admin` | + +## iOS 处理建议 + +### 状态优先级 + +1. **UI 关心的状态子集:** + +| 模块 | 展示用状态 | 说明 | +|------|-----------|------| +| DocumentImport | `QUEUED` → `PROCESSING`(any intermediate) → `COMPLETED` / `FAILED` | 中间态统一显示为"处理中" | +| ReviewCard | `new` / `learning` / `review` / `relearning` | scheduleState 比 status 更有用 | +| FocusItem | `open` / `completed` | 简单二态 | +| LearningSession | `active` / `completed` | — | + +2. **轮询策略:** + - DocumentImport 状态:初始 2s,后续 5s + - AI 分析作业:2s 间隔 + - 最多轮询 5 分钟,超时提示用户 + +3. **错误本地化:** + - 后端返回中文错误消息 + - 建议 iOS 端做本地化映射(中 → 英/其他语言) diff --git a/技术设计/iOS对接可行性审计报告.md b/技术设计/iOS对接可行性审计报告.md new file mode 100644 index 0000000..038f90a --- /dev/null +++ b/技术设计/iOS对接可行性审计报告.md @@ -0,0 +1,468 @@ +# iOS 端对接可行性审计报告 + +> 审计日期:2026-05-24 | 范围:api-server (NestJS 11) | 方式:只读排查,未修改任何代码 +> +> 审计人:Claude Code | 服务端:蜂驰云 8核32G (120.53.227.155) + 轻量云 4核4G (10.2.0.7) + +--- + +## A. 总体结论:有条件可以 ✅ + +**在 P0 阻塞项解决之前,不能发布 iOS 对接任务。** P0 涉及 2 个安全/功能严重问题,预计修复工作量 1-2 天。P1 高风险项建议在 iOS 开发第一周内修复。 + +--- + +## B. 阻塞项清单 + +### 🔴 P0 阻塞 — 不解决不能开始 iOS 对接 + +| # | 问题 | 位置 | 风险 | +|---|------|------|------| +| 1 | **Apple 登录是假的** — `APPLE_BUNDLE_ID` 未设,退回 mock 模式,任何长度≥4 的字符串都当合法 token | `src/modules/auth/apple-auth.service.ts:28-30`,生产 env 缺 `APPLE_BUNDLE_ID` | iOS 用户可以用任意字符串登录,伪造 Apple ID | +| 2 | **`/internal/*` 7 个端点零认证暴露公网** — 无 Guard、无 API Key、无 IP 白名单 | `src/common/guards/jwt-auth.guard.ts:31` 跳过 internal;`internal-rag.controller.ts` 有 `@Public()` 且无 `@UseGuards` | 攻击者可读取文档原文、写入 chunks/candidates、劫持导入作业 | + +### 🟡 P1 高风险 — 可以开始,但必须尽快修(建议 iOS 开发第一周内) + +| # | 问题 | 位置 | 风险 | +|---|------|------|------| +| 3 | **用户 Token 不检查账号状态** — JwtAuthGuard 不解码查库,禁用/已删用户 Token 仍然有效 | `src/common/guards/jwt-auth.guard.ts:42-46` | 禁用用户仍可访问所有 API | +| 4 | **Refresh Token 不检查用户状态** — 刷新时不验证用户是否被禁用 | `src/modules/auth/auth.service.ts:125-128` | 禁用用户可以无限刷新 Token | +| 5 | **用户和管理员 JWT 共用同一密钥** — `ADMIN_JWT_ACCESS_SECRET` 未设,退回 `JWT_SECRET` | `src/config/jwt.config.ts:6`;生产 env 缺变量 | 降低 Token 隔离性,增加横向移动风险 | +| 6 | **请求 DTO 大量缺失** — iOS 核心流程(import、source、learning-session)用 `@Body() body: any` | `document-import.controller.ts:13`、`knowledge-source.controller.ts:18` 等 | iOS 可能发送错误字段,拿到 201 但后续失败 | + +### 🟢 P2 优化项 — 不阻塞 iOS + +| # | 问题 | 位置 | +|---|------|------| +| 7 | 无统一分页响应格式 — `PaginatedResponse` 定义了但从未使用 | `src/common/types/index.ts:9-19` | +| 8 | Swagger 缺少 Response Schema — 所有 `@ApiResponse` 只有 description,无 typed schema | 全部 CAPI 控制器 | +| 9 | 4 个端点双重包装响应 — logout/feedback/reports/waitlist 返回 `{success, data}` 被 ResponseInterceptor 再包一层 | `auth.controller.ts:51`、`feedback.controller.ts:20`、`content-safety.controller.ts:27`、`waitlist.controller.ts:19` | +| 10 | Refresh Token 用 SHA-256 而非 bcrypt 哈希 | `src/modules/auth/token.service.ts:19,24` | +| 11 | CI/CD 中硬编码了生产数据库密码 | `.gitea/workflows/deploy.yml:8,40` | +| 12 | 4核4G 服务器上有旧 MySQL(35 表,dev 密码),但不影响生产 | `10.2.0.7` 上 `mysql-zhixi` 容器 | + +--- + +## C. 服务器与数据库部署 + +### 拓扑图 + +``` +用户 (iOS App) + │ + ▼ +api.longde.cloud (Nginx) + │ + ▼ +蜂驰云 8核32G (120.53.227.155) ← 唯一生产服务器 +├── zhixi-api.service NestJS API (端口 3000) +├── zhixi-worker.service BullMQ Worker +├── rag-worker.service Python RAG Worker (端口 8000) +├── Docker: mysql zhixi_prod (90 表) ← 唯一生产数据库 +├── Docker: redis 缓存/队列/限流 +├── Docker: qdrant 向量数据库 +├── Docker: gitea-runner CI/CD Runner +└── Docker: nginx 反向代理 + +轻量云 4核4G (10.2.0.7) ← 辅助/开发残留 +├── Docker: mysql-zhixi zhixi (35 表) ← 旧 dev 数据库,不服务生产 +├── Docker: gitea Git 代码仓库 +├── Docker: hermes-agent AI Agent +└── zhixi-worker.service 故障中 (auto-restart) +``` + +### 关键结论 + +| 问题 | 结论 | +|------|------| +| 是否存在数据分裂风险? | **否。** 生产仅一套 MySQL `zhixi_prod`,运行在蜂驰云 | +| iOS 是否会连错数据库? | **否。** API 服务固定连接 `zhixi_prod`,通过 env 文件配置 | +| 4核4G 的 MySQL 服务生产流量吗? | **否。** 数据库名不同(`zhixi` vs `zhixi_prod`),仅 35 张旧表,密码是 dev 环境密码 | +| JWT 用户/管理员密钥隔离吗? | **否。** `ADMIN_JWT_ACCESS_SECRET` 未设,退回与用户相同的 `JWT_SECRET` | + +### 生产环境配置概况 + +配置文件:`/opt/zhixi/env/.env.production`(28 行) + +| 配置项 | 状态 | 说明 | +|--------|------|------| +| DATABASE_URL | ✅ 已设 | `mysql://zhixi_user:***@127.0.0.1:3306/zhixi_prod` | +| REDIS_HOST/PORT/PASSWORD | ✅ 已设 | 127.0.0.1:6379 | +| QDRANT_URL | ✅ 已设 | http://127.0.0.1:6333 | +| JWT_SECRET | ✅ 已设 | 64 位 hex | +| ADMIN_JWT_ACCESS_SECRET | ❌ 未设 | 退回 JWT_SECRET | +| APPLE_BUNDLE_ID | ❌ 未设 | 导致 Apple 登录退回 mock | +| ENABLE_SWAGGER | ✅ true | 已恢复,Basic Auth 保护 | +| STORAGE_COS_* | ✅ 已设 | 腾讯云 COS (zhixi-1259685406, ap-beijing) | +| AI_PROVIDER / DEEPSEEK_API_KEY | ✅ 已设 | DeepSeek | +| SILICONFLOW_API_KEY | ✅ 已设 | SiliconFlow (备用) | + +--- + +## D. 各问题详细诊断 + +### P0-1: Apple 登录 Mock 模式 + +**文件:** `src/modules/auth/apple-auth.service.ts` + +**问题代码(第 23-32 行):** +```typescript +async verifyIdentityToken(identityToken: string): Promise<...> { + if (!this.appleBundleId) { + return this.verifyMock(identityToken); // ← 生产走这分支 + } + return this.verifyReal(identityToken); +} +``` + +**Mock 逻辑(第 34-47 行):** +```typescript +private verifyMock(identityToken: string): { appleUserId: string } { + if (!identityToken || identityToken.trim().length < 4) { + throw new UnauthorizedException('identityToken 无效'); + } + return { + appleUserId: crypto.createHash('sha256') + .update(`apple-mock:${identityToken}`) + .digest('hex') + .slice(0, 64), + }; +} +``` + +**真实验证逻辑(第 49-78 行)** — 已实现但未启用: +- 使用 `jose.jwtVerify` 验证 JWT 签名 +- 通过 `createRemoteJWKSet` 获取 Apple 公钥 (`https://appleid.apple.com/auth/keys`) +- 验证 `issuer`(Apple)和 `audience`(Bundle ID) + +**当前状态:** 配置(`src/config/apple.config.ts`)中 `bundleId` 默认为空字符串。生产 env 未设 `APPLE_BUNDLE_ID`。任何人发送任意 ≥4 字符的字符串即可登录。 + +**修复方式:** +1. 在 Apple Developer Console 获取应用的 Bundle ID +2. 在生产 env 添加 `APPLE_BUNDLE_ID=com.longde.zhixi`(或实际值) +3. 建议同时在代码层加保护:当 `appleBundleId` 为空时直接拒绝而非退回 mock + +--- + +### P0-2: /internal/* 零认证暴露 + +**文件:** `src/common/guards/jwt-auth.guard.ts:31` + `src/modules/rag/internal-rag.controller.ts` + +**JwtAuthGuard 跳过 internal 路由:** +```typescript +if (request.path.startsWith('/admin-api') || request.path.startsWith('/internal')) { + return true; // ← 完全放行,不做任何认证 +} +``` + +**控制器层:** `InternalRagController` 有 `@Public()` 装饰器,无 `@UseGuards()`。 + +**暴露的端点(全部无需认证,实测确认):** + +| 方法 | 路径 | 功能 | 实测 HTTP 状态 | +|------|------|------|----------------| +| GET | `/internal/rag/jobs/next` | 获取下一个待处理导入作业(含文档原文) | 200 | +| GET | `/internal/rag/jobs/:id` | 获取指定作业详情(含原文+元数据) | 200 | +| POST | `/internal/rag/jobs/:id/claim` | 认领作业(任意 workerId) | 可访问 | +| POST | `/internal/rag/jobs/:id/heartbeat` | 发送心跳保持作业租约 | 可访问 | +| POST | `/internal/rag/jobs/:id/status` | 更新作业状态/进度/错误 | 可访问 | +| POST | `/internal/rag/chunks` | 批量写入 knowledge_chunk 表 | 可访问 | +| POST | `/internal/rag/candidates` | 批量写入导入候选 | 可访问 | + +**攻击面:** +- 读取所有导入文档的原文(数据泄露) +- 往 `knowledge_chunk` 表注入任意数据(RAG 投毒) +- 认领/劫持作业,干扰合法 Worker(拒绝服务) +- 无速率限制,可无限写入 + +**修复方式:** +- 方案 A:添加 InternalAuthGuard,验证 `X-Internal-API-Key` 请求头(已有 `RAG_WORKER_SECRET` 在生产 env 中) +- 方案 B:在 Nginx 层限制 IP(仅允许 127.0.0.1),因为这些端点仅供本地 Worker 使用 +- 建议:A+B 组合 + +--- + +### P1-3: JwtAuthGuard 不检查用户状态 + +**文件:** `src/common/guards/jwt-auth.guard.ts:42-46` + +```typescript +// 当前逻辑:只验证 JWT 签名,不检查用户状态 +const payload = await this.jwtService.verifyAsync(token, { + secret: this.configService.get('jwt.secret'), +}); +request.user = { id: String(payload.sub), email: payload.email, role: payload.role }; +return true; // ← 禁用的用户、已删除的用户 Token 都通过 +``` + +**对比 AdminAuthGuard(正确示范,admin-auth.guard.ts:45-59):** +```typescript +const adminUser = await this.prisma.adminUser.findUnique({ + where: { id: payload.sub }, +}); +if (!adminUser || adminUser.deletedAt) { + throw new UnauthorizedException('管理员账号不存在'); +} +if (adminUser.status !== 'ACTIVE') { + throw new UnauthorizedException('管理员账号已被禁用'); +} +if (adminUser.lockedUntil && new Date(adminUser.lockedUntil) > new Date()) { + throw new UnauthorizedException('管理员账号已被锁定,请稍后再试'); +} +``` + +**修复方式:** JwtAuthGuard 添加数据库查询,验证 `user.deletedAt === null && user.status === 'active'`。 + +--- + +### P1-4: Refresh Token 不检查用户状态 + +**文件:** `src/modules/auth/auth.service.ts:125-128` + +```typescript +// 刷新时不检查用户状态 +const accessToken = await this.tokenService.generateAccessToken(stored.user); +``` + +刷新流程完整链路:验证 refreshToken → 撤销旧 token → 生成新 token → **未检查 stored.user 是否被禁用** + +**修复方式:** 在生成新 access token 前添加 `if (stored.user.status !== 'active' || stored.user.deletedAt) throw new UnauthorizedException()` + +--- + +### P1-5: 用户/管理员 JWT 共用密钥 + +**文件:** `src/config/jwt.config.ts:6` + +```typescript +const adminSecret = process.env.ADMIN_JWT_ACCESS_SECRET || process.env.JWT_SECRET; +``` + +生产 env 仅设 `JWT_SECRET`,未设 `ADMIN_JWT_ACCESS_SECRET`。用户和管理员 Token 用同一密钥签名。 + +**Token 隔离分析:** + +| 场景 | 结果 | +|------|------| +| 管理员 Token 访问 `/api/*` | JwtAuthGuard 验证通过(同密钥),管理员可以访问用户 API | +| 用户 Token 访问 `/admin-api/*` | AdminAuthGuard 发现 `payload.type !== 'admin'` 拒绝 | +| 用户 Token 的 payload | `{ sub, email, role }` — 无 `type` 字段 | +| 管理员 Token 的 payload | `{ sub, type: 'admin', role, sessionId }` — 有 `type` 字段 | + +**当前状态:** AdminAuthGuard 通过 `type` 字段区分,即使密钥相同也能阻止用户 Token 访问管理员接口。但反之不行——管理员 Token 可以访问用户接口。且如果密钥泄露,攻击者可伪造管理员 Token。 + +**修复方式:** 在生产 env 添加 `ADMIN_JWT_ACCESS_SECRET=<独立密钥>`。 + +--- + +### P1-6: iOS 核心流程缺少 DTO + +**受影响的端点:** + +| 端点 | 文件:行号 | 当前状态 | +|------|-----------|----------| +| `POST /api/imports` | `document-import.controller.ts:13` | `@Body() body: any` | +| `POST /api/knowledge-bases/:kbId/sources` | `knowledge-source.controller.ts:18` | `@Body() dto: any` | +| `POST /api/learning-sessions` | `learning-session.controller.ts` | `@Body() body: any` | +| `POST /api/active-recalls/:id/submit` | `active-recall.controller.ts` | `@Body() body: any` | +| `POST /api/focus-items` | `focus-items.controller.ts` | `@Body() body: any` | + +**影响:** iOS 发送错误字段时,由于无 `class-validator` 校验,可能拿到 201 Created,直到 AI 处理阶段才失败,调试成本高。 + +**修复方式:** 为上述端点创建 DTO 类,添加 `@IsString()`, `@IsOptional()`, `@IsEnum()` 等装饰器。 + +--- + +## E. CAPI 契约检查 + +### 响应格式 + +**统一成功响应**(ResponseInterceptor 全局拦截): +```json +{ + "success": true, + "data": <原始返回值>, + "timestamp": "2026-05-24T..." +} +``` + +**统一错误响应**(GlobalExceptionFilter): +```json +{ + "success": false, + "statusCode": 401, + "message": "请先登录" +} +``` + +**已知异常:** 4 个端点手动返回 `{success, data}` 结构,被拦截器二次包装导致 `data.data` 嵌套: +- `POST /api/auth/logout` +- `POST /api/feedback` +- `POST /api/reports` +- `POST /api/waitlist` + +### 分页 + +`PaginatedResponse` 接口已定义(`src/common/types/index.ts:9-19`),但**从未被任何控制器使用**。CAPI 分页端点各自返回 raw 格式,iOS 需按端点适配。 + +### Swagger 质量 + +- ✅ 所有 CAPI 控制器有 `@ApiTags`(中文标签) +- ✅ 所有端点有 `@ApiOperation`(中文描述) +- ❌ 无 `@ApiResponse({ type: SomeDto })` —— Swagger 无 response schema,iOS 客户端生成工具无法推断返回类型 +- ✅ Bearer Auth 已全局配置 +- ✅ 生产环境可通过 Basic Auth 访问 `/api-docs-json` + +### 状态枚举 + +Prisma schema 使用 `String` 类型(无原生 enum),状态值在应用层约定。以下是 CAPI 相关模型的状态枚举: + +| 模型 | 字段 | 可能的值 | +|------|------|----------| +| User | status | `active`, `disabled`, `deleted` | +| User | role | `USER`, `ADMIN` | +| KnowledgeBase | status | `active`, `archived`, `deleted` | +| KnowledgeItem | status | `active`, `archived` | +| KnowledgeItem | itemType | `note`, `flashcard`, `concept` | +| KnowledgeSource | parseStatus | `pending`, `parsing`, `completed`, `failed` | +| KnowledgeSource | indexStatus | `pending`, `indexing`, `completed`, `failed` | +| KnowledgeSource | learningStatus | `pending`, `learning`, `completed`, `skipped` | +| DocumentImport | status | `QUEUED`, `CLAIMED`, `PARSING`, `COMPLETED`, `FAILED`, `FAILED_FINAL` (另有中间状态 10+ 个) | +| ImportCandidate | status | `PENDING`, `ACCEPTED`, `REJECTED` | +| LearningSession | status | `active`, `completed`, `cancelled` | +| LearningSession | mode | `active_recall`, `feynman`, `review`, `reading` | +| AiAnalysisJob | status | `pending`, `processing`, `completed`, `failed` | +| AiAnalysisJob | jobType | `active_recall_analysis`, `feynman_evaluation` | +| ReviewCard | status | `active`, `completed`, `archived` | +| ReviewCard | scheduleState | `new`, `learning`, `review`, `relearning` | +| ReviewCard | difficulty | 数值 (0.0-1.0, SM-2 算法) | +| FocusItem | status | `open`, `completed` | +| FocusItem | priority | `high`, `normal`, `low` | +| Feedback | status | `pending`, `reviewed`, `resolved`, `closed` | +| Feedback | type | `bug`, `feature`, `general` | + +--- + +## F. 文件上传与导入流程(iOS 适配分析) + +### 完整流程图 + +``` +iOS App API Server COS (腾讯云) BullMQ Worker +======= ========== ============ ============= + +1. POST /api/files/upload-url + { filename, mimeType, sizeBytes } + ──────────────────────────► 校验文件类型(9种)/大小(≤20MB) + 生成预签名 PUT URL + ◄────────────────────────── { uploadUrl, objectKey, expiresIn } + +2. PUT + [二进制文件数据] + ───────────────────────────────────────────────────────► 文件存入 COS + +3. POST /api/files/complete + { objectKey, checksum? } + ──────────────────────────► 验证文件已到达 COS + 创建 UploadedFile 记录 + ◄────────────────────────── { id, filename, sizeBytes } + +4. POST /api/knowledge-bases/:kbId/sources + { fileId, title, originalFilename } + ──────────────────────────► 创建 KnowledgeSource + 自动创建 DocumentImport (QUEUED) + ◄────────────────────────── { id, parseStatus: "pending" } + +5. GET /api/imports/:id/status (轮询) + ──────────────────────────► 查 Redis 实时状态 → 回退 DB + ◄────────────────────────── { status: "QUEUED", progress: 0 } + + ... Worker 处理中 ... + +6. GET /api/imports/:id/status (轮询) + ◄────────────────────────── { status: "COMPLETED", progress: 100, + message: "成功提取 N 个知识点" } +``` + +### 允许的文件类型 + +| 类型 | MIME | +|------|------| +| PDF | `application/pdf` | +| TXT | `text/plain` | +| Markdown | `text/markdown`, `text/x-markdown` | +| CSV | `text/csv` | +| Word | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | +| Excel | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | +| PNG | `image/png` | +| JPEG | `image/jpeg` | +| WebP | `image/webp` | + +文件大小上限:**20MB**,全局 `StrictValidationPipe` 限制请求体 **10MB**。 + +### iOS 适配评估 + +| 评估项 | 结论 | 说明 | +|--------|------|------| +| 预签名 URL 直传 | ✅ 可用 | 标准 HTTP PUT,iOS URLSession/Alamofire 均可 | +| 请求/响应格式 | ✅ 可用 | 标准 JSON,字段简单 | +| 认证方式 | ✅ 可用 | 标准 JWT Bearer Token | +| 分块上传/续传 | ❌ 不支持 | 仅支持完整文件一次 PUT,大文件+弱网可能失败 | +| Checksum 校验 | ⚠️ 可选 | `CompleteUploadDto.checksum` 可选,建议 iOS 传 SHA-256 | +| 错误信息中文 | ⚠️ 注意 | 文件名校验等错误信息为中文,iOS 需处理或本地化 | +| 导入状态 DTO | ❌ 缺失 | `POST /api/imports` 使用 `any`,无验证 | + +--- + +## G. CAPI 端点认证测试(生产环境实测) + +| 端点 | 无 Token | 结论 | +|------|----------|------| +| `GET /health` | 200 | 公开 | +| `GET /api/users/me` | 401 "请先登录" | 受保护 ✅ | +| `GET /api/knowledge-bases` | 401 "请先登录" | 受保护 ✅ | +| `GET /api/activity/streak` | 401 "请先登录" | 受保护 ✅ | +| `GET /api/reviews/due` | 401 "请先登录" | 受保护 ✅ | +| `GET /api/notifications` | 401 "请先登录" | 受保护 ✅ | +| `GET /api/workspace/dashboard` | 401 "请先登录" | 受保护 ✅ | +| `GET /internal/rag/jobs/next` | **200** | **未保护 🔴** | +| `POST /api/auth/dev-login` | 403 "生产环境禁用" | 已禁用 ✅ | +| `GET /api-docs-json` | 401 "Authentication required" | Basic Auth ✅ | +| `GET /api-docs-json` (with auth) | 200 | 正常 ✅ | + +--- + +## H. 给 iOS 团队的接口文档清单 + +以下文档需要在 iOS 对接开始前编写交付: + +| 文档 | 内容 | 优先级 | +|------|------|--------| +| `capi-contract-for-ios.md` | 完整 CAPI 端点清单、请求/响应格式、认证方式、通用约定 | P0 | +| `ios-auth-flow.md` | Apple 登录流程、Token 刷新策略、Keychain 存储建议、登出 | P0 | +| `ios-file-upload-import-flow.md` | 预签名 URL → COS 直传 → Source 创建 → 导入轮询 | P1 | +| `ios-learning-review-flow.md` | 学习会话 → 主动回忆 → AI 分析 → SM-2 复习 → 薄弱项 | P1 | +| `capi-error-codes-and-status-enums.md` | HTTP 错误码、业务错误消息、所有模型状态枚举值 | P1 | + +--- + +## I. 修复优先级路线图 + +``` +Week 0 (iOS 启动前) +├── P0-1: 配置 APPLE_BUNDLE_ID,启用真实验证 +├── P0-2: 添加 InternalAuthGuard 或 Nginx IP 限制 +└── P1-5: 配置 ADMIN_JWT_ACCESS_SECRET(独立密钥) + +Week 1 (iOS 开发启动) +├── P1-3: JwtAuthGuard 添加用户状态检查 +├── P1-4: Refresh Token 添加用户状态检查 +├── P1-6: 为核心端点创建 DTO +└── 编写 iOS 接口文档(5 份) + +Week 2+ +├── P2-7: 统一分页响应格式 +├── P2-8: Swagger 添加 Response Schema +├── P2-9: 修复双重响应包装 +└── P2-10/11/12: 杂项优化 +``` diff --git a/技术设计/ios-auth-flow.md b/技术设计/ios-auth-flow.md new file mode 100644 index 0000000..b14b212 --- /dev/null +++ b/技术设计/ios-auth-flow.md @@ -0,0 +1,78 @@ +# iOS 认证流程 + +> 版本:0.1.0 | 更新:2026-05-24 + +## 流程图 + +``` +iOS App API Server Apple + +1. 用户点击"使用 Apple 登录" + Sign in with Apple 弹窗 + ─────────────────────────────────────────────────────────► 验证用户身份 + ◄───────────────────────────────────────────────────────── 返回 identityToken + +2. POST /api/auth/apple + { identityToken, fullName?, email? } + ──────────────────────────► 验证 identityToken + 通过 Apple JWKS 验签 + 验证 issuer + audience + 查找或创建用户 + 生成 JWT + RefreshToken + ◄────────────────────────── { accessToken, refreshToken, user } + +3. 存储 Token + Keychain: accessToken + refreshToken + +4. 后续请求携带 accessToken + GET /api/users/me + Authorization: Bearer + ──────────────────────────► JwtAuthGuard 验证 + - 解码 JWT + - 检查 type=user + - 查询用户状态 (active/deleted) + ◄────────────────────────── 用户数据 + +5. Token 过期时刷新 (401) + POST /api/auth/refresh + { refreshToken } + ──────────────────────────► 验证 refresh token hash + 检查用户状态 + 撤销旧 token → 签发新对 + ◄────────────────────────── { accessToken, refreshToken, user } + +6. 登出 + POST /api/auth/logout + { refreshToken } + ──────────────────────────► 撤销 refresh token + ◄────────────────────────── 200 OK +``` + +## Token 存储建议 + +| Token | 存储位置 | 说明 | +|-------|----------|------| +| accessToken | Keychain / Secure Enclave | 短期(1h),频繁使用 | +| refreshToken | Keychain | 长期(7d),仅在刷新时使用 | + +## 错误处理 + +| HTTP | message | iOS 处理 | +|------|---------|----------| +| 401 | "请先登录" | 跳转登录页 | +| 401 | "登录已过期,请重新登录" | 尝试刷新 token,失败则跳转登录 | +| 401 | "账号已被禁用" | 显示禁用提示,退出 | +| 401 | "账号不存在或已注销" | 跳转登录页 | +| 401 | "Apple 登录未配置" | 显示维护提示 | + +## 自动刷新策略 + +``` +请求 → 401? + ├── 是 → 尝试 POST /api/auth/refresh + │ ├── 成功 → 更新 Keychain,重试原请求 + │ └── 失败 → 清除 Token,跳转登录 + └── 否 → 正常处理 +``` + +建议在 HTTP 拦截器/中间件层实现,避免每个 API 调用都要处理。 diff --git a/技术设计/ios-file-upload-import-flow.md b/技术设计/ios-file-upload-import-flow.md new file mode 100644 index 0000000..920b1fe --- /dev/null +++ b/技术设计/ios-file-upload-import-flow.md @@ -0,0 +1,98 @@ +# iOS 文件上传与导入流程 + +> 版本:0.1.0 | 更新:2026-05-24 + +## 完整流程 + +``` +iOS App API Server COS (腾讯云) Worker +======= ========== ============ ====== + +1. 获取上传凭证 + POST /api/files/upload-url + { filename, mimeType, sizeBytes } + ──────────────────────────► 校验文件类型(9 种) + 校验大小(≤20MB) + 生成预签名 PUT URL + ◄────────────────────────── { uploadUrl, objectKey, + bucket, region, expiresIn } + +2. 直传文件到 COS + PUT + Content-Type: + [binary file data] + ───────────────────────────────────────────────────────► 文件存入 COS + +3. 确认上传完成 + POST /api/files/complete + { objectKey, checksum? } + ──────────────────────────► COS headObject 验证 + 创建 UploadedFile 记录 + ◄────────────────────────── { id, filename, sizeBytes, + mimeType } + +4. 创建知识源(自动触发导入) + POST /api/knowledge-bases/:kbId/sources + { fileId, title, type, originalFilename, mimeType } + ──────────────────────────► 创建 KnowledgeSource + 自动创建 DocumentImport + (status=QUEUED) + ◄────────────────────────── { id, title, parseStatus } + +5. 轮询导入状态 + GET /api/imports/:importId/status + ──────────────────────────► 查 Redis 实时状态 + → 回退 DB + ◄────────────────────────── { id, status, progress, + message } + + 6. Worker 处理 + 下载文件 + AI 解析提取 + 创建知识点 + 更新状态→COMPLETED + +7. 轮询导入状态 + GET /api/imports/:importId/status + ◄────────────────────────── { status: "COMPLETED", + progress: 100, + message: "成功提取 N 个知识点" } +``` + +## 允许的文件类型 + +| 格式 | MIME | +|------|------| +| PDF | `application/pdf` | +| TXT | `text/plain` | +| Markdown | `text/markdown`, `text/x-markdown` | +| CSV | `text/csv` | +| Word | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | +| Excel | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | +| PNG | `image/png` | +| JPEG | `image/jpeg` | +| WebP | `image/webp` | + +**大小上限:** 20MB + +## 导入状态枚举 + +| 状态 | 含义 | 可操作 | +|------|------|--------| +| `QUEUED` | 排队中 | 轮询等待 | +| `CLAIMED` | 已被 Worker 认领 | 轮询等待 | +| `DOWNLOADING` | 下载中 | 轮询等待 | +| `PARSING` | 解析中 | 轮询等待 | +| `CHUNKING` | 分块中 | 轮询等待 | +| `EMBEDDING` | 向量化中 | 轮询等待 | +| `COMPLETED` | 完成 | 可查看结果 | +| `FAILED` | 失败(会重试 3 次) | 等待自动重试 | +| `FAILED_FINAL` | 最终失败 | 需重新导入 | + +## iOS 实现要点 + +1. **直接 PUT 到 COS**:使用 URLSession 的 `uploadTask(with:fromFile:)` 或 Alamofire 的 `upload(data:to:)` +2. **预签名 URL 有过期时间**:`expiresIn` 秒内有效,拿到后立即使用 +3. **支持进度回调**:大文件上传时建议显示进度条 +4. **Checksum 可选但推荐**:上传完成后传 `checksum`(SHA-256)确保完整性 +5. **导入轮询间隔建议**:初始 2s,逐步退避到 5s diff --git a/技术设计/ios-learning-review-flow.md b/技术设计/ios-learning-review-flow.md new file mode 100644 index 0000000..bebbceb --- /dev/null +++ b/技术设计/ios-learning-review-flow.md @@ -0,0 +1,133 @@ +# iOS 学习与复习流程 + +> 版本:0.1.0 | 更新:2026-05-24 + +## 整体流程 + +``` +创建知识库 → 上传资料 → 导入知识点 → 开始学习 → 主动回忆 → AI 分析 → 复习巩固 +``` + +## 1. 学习会话 + +``` +POST /api/learning-sessions +{ mode: "active_recall", knowledgeBaseId: "kb-1" } +────────────────────────► 创建会话 (status=active) +◄─────────────────────── { id, startedAt, mode } + +...学习过程... + +POST /api/learning-sessions/:id/end +────────────────────────► 结束会话 (status=completed) + 记录 durationSeconds +◄─────────────────────── { id, durationSeconds } +``` + +**mode 枚举:** `active_recall` | `feynman` | `review` | `reading` + +## 2. 主动回忆 + +``` +GET /api/active-recalls?page=1&limit=20 +◄─────────────────────── 题目列表(含问题/选项) + +POST /api/active-recalls/:id/submit +{ answer: "用户答案" } +────────────────────────► 记录回答 → 触发 AI 分析 + (异步:BullMQ ai-analysis 队列) +◄─────────────────────── { success: true } +``` + +## 3. AI 分析 + +``` +POST /api/ai-analysis +{ ... } +────────────────────────► 创建分析作业 + +GET /api/ai-analysis/jobs/:id +◄─────────────────────── { status: "pending|processing|completed|failed" } + +GET /api/ai-analysis/:id +◄─────────────────────── 分析结果(强项/弱项/建议) +``` + +**分析完成后自动生成:** +- ReviewCard(复习卡片,SM-2 算法调度) +- FocusItem(薄弱项,待巩固) + +## 4. 复习(SM-2 间隔重复) + +``` +GET /api/reviews/due +◄─────────────────────── 今日待复习卡片列表 + [{ + id, frontText, difficulty, + scheduleState, intervalDays, + repetitionCount, lapseCount + }] + +POST /api/reviews/:id/submit +{ quality: 4 } +────────────────────────► 提交复习质量评分 (0-5) + SM-2 算法更新: + - easeFactor + - intervalDays + - nextReviewAt + - scheduleState +◄─────────────────────── { id, nextReviewAt, intervalDays } +``` + +**scheduleState:** `new` → `learning` → `review` → `relearning` + +**quality 评分指南:** + +| 评分 | 含义 | +|------|------| +| 0 | 完全忘记 | +| 1 | 错误,有印象 | +| 2 | 错误,但正确答案看起来很熟悉 | +| 3 | 正确,但费了很大劲 | +| 4 | 正确,有些犹豫 | +| 5 | 完美,毫不费力 | + +## 5. 薄弱项跟踪 + +``` +GET /api/focus-items?status=open +◄─────────────────────── [{ id, title, priority, status }] + +POST /api/focus-items/:id/complete +────────────────────────► 标记为已掌握 +◄─────────────────────── { id, status: "completed" } +``` + +**priority 枚举:** `high` | `normal` | `low` + +## 6. 学习统计 + +``` +GET /api/activity/heatmap +◄─────────────────────── [{ date, count }] 365 天热力图数据 + +GET /api/activity/streak +◄─────────────────────── { currentStreak, longestStreak } + +GET /api/activity/summary +◄─────────────────────── { totalSessions, totalDuration, ... } +``` + +## 完整学习主链路 + +``` +1. 创建/选择知识库 POST /api/knowledge-bases +2. 上传资料 POST /api/files/upload-url → PUT COS → POST /api/files/complete +3. 创建来源 + 导入 POST /api/knowledge-bases/:id/sources +4. 轮询导入状态 GET /api/imports/:id/status +5. 开始学习会话 POST /api/learning-sessions +6. 主动回忆练习 GET/POST /api/active-recalls +7. AI 分析 POST /api/ai-analysis +8. 每日复习 GET /api/reviews/due → POST /api/reviews/:id/submit +9. 跟踪薄弱项 GET → POST /api/focus-items/:id/complete +```