startup-plan/技术设计/iOS对接可行性审计报告.md
WangDL 0eb5f53873 docs: iOS integration audit report + 5 CAPI docs + nginx fix
- iOS integration readiness audit (blocker list, module map, deployment check)
- CAPI contract for iOS (15 modules, 80+ endpoints)
- CAPI error codes and status enums
- iOS auth flow, file upload import flow, learning review flow
- Web product page nginx fix (reenable longde.cloud)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:55:22 +08:00

20 KiB
Raw Permalink Blame History

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 跳过 internalinternal-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:13knowledge-source.controller.ts:18 iOS 可能发送错误字段,拿到 201 但后续失败

🟢 P2 优化项 — 不阻塞 iOS

# 问题 位置
7 无统一分页响应格式 — PaginatedResponse<T> 定义了但从未使用 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:51feedback.controller.ts:20content-safety.controller.ts:27waitlist.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 服务器上有旧 MySQL35 表dev 密码),但不影响生产 10.2.0.7mysql-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.production28 行)

配置项 状态 说明
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 行):

async verifyIdentityToken(identityToken: string): Promise<...> {
  if (!this.appleBundleId) {
    return this.verifyMock(identityToken);  // ← 生产走这分支
  }
  return this.verifyReal(identityToken);
}

Mock 逻辑(第 34-47 行):

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)
  • 验证 issuerAppleaudienceBundle 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 路由:

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

// 当前逻辑:只验证 JWT 签名,不检查用户状态
const payload = await this.jwtService.verifyAsync(token, {
  secret: this.configService.get<string>('jwt.secret'),
});
request.user = { id: String(payload.sub), email: payload.email, role: payload.role };
return true;  // ← 禁用的用户、已删除的用户 Token 都通过

对比 AdminAuthGuard正确示范admin-auth.guard.ts:45-59

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

// 刷新时不检查用户状态
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

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 全局拦截):

{
  "success": true,
  "data": <原始返回值>,
  "timestamp": "2026-05-24T..."
}

统一错误响应GlobalExceptionFilter

{
  "success": false,
  "statusCode": 401,
  "message": "请先登录"
}

已知异常: 4 个端点手动返回 {success, data} 结构,被拦截器二次包装导致 data.data 嵌套:

  • POST /api/auth/logout
  • POST /api/feedback
  • POST /api/reports
  • POST /api/waitlist

分页

PaginatedResponse<T> 接口已定义(src/common/types/index.ts:9-19),但从未被任何控制器使用。CAPI 分页端点各自返回 raw 格式iOS 需按端点适配。

Swagger 质量

  • 所有 CAPI 控制器有 @ApiTags(中文标签)
  • 所有端点有 @ApiOperation(中文描述)
  • @ApiResponse({ type: SomeDto }) —— Swagger 无 response schemaiOS 客户端生成工具无法推断返回类型
  • 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 <uploadUrl>
   [二进制文件数据]
   ───────────────────────────────────────────────────────►  文件存入 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 PUTiOS 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: 杂项优化