Files
ruoyi-vue-pro/docs/education/scalar-contract-from-source.md

7.9 KiB
Raw Blame History

Scalar (tiku-backend) 接口契约 —— 从源码提取

日期2026-07-28 来源:/Users/tiku1/code/tiku-backend 仓库 NestJS Controller、DTO 和装饰器 状态:源码级契约冻结,真实 endpoint 验证需启动完整运行时Supabase + API server

运行时信息

  • 框架NestJS + Fastify
  • OpenAPI 路径:/openapi.json(仅非生产环境)
  • Scalar 文档:/docs(仅非生产环境)
  • 认证Bearer TokenJWT+ x-tenant-id header
  • 响应 envelope{ items/item, meta: { requestId } }

RuoYi Education 实际调用的路径

以下为 RuoYi ScalarCatalogProvider 使用的只读 GET 路径:

1. GET /api/catalog/regions

  • 安全方案: x-tenant-id header (@ApiSecurity('tenant-id'), @TenantAccess())
  • 查询参数: regionId? (UUID, 可选)
  • 响应: { items: CatalogRegionResponseDto[], meta: { requestId } }
  • CatalogRegionResponseDto 字段:
    • id (uuid, 必填)
    • legacyId (string, nullable)
    • name (string, 必填)
    • code (string, nullable)
    • shortName (string, nullable)
    • fullName (string, nullable)
    • icon (string, nullable)
    • pinyin (string, nullable)
    • isHot (boolean, 必填)
    • isActive (boolean, 必填)
    • order (number, 必填)

2. GET /api/catalog/region-modules

  • 安全方案: x-tenant-id
  • 查询参数: regionId? (UUID)
  • 响应: { items: CatalogRegionModuleResponseDto[], meta }
  • 字段: id, legacyId, regionId, name, type, icon, color, textColor, description, route, isPrimarySchoolModule, isActive, order

3. GET /api/catalog/module-nodes

  • 安全方案: x-tenant-id
  • 查询参数:
    • regionId? (UUID)
    • moduleId? (UUID)
    • parentId? (string, "root" 表示根节点)
  • 响应: { items: CatalogEntityDto[], meta } — 通用实体列表

4. GET /api/catalog/schools

  • 安全方案: x-tenant-id
  • 查询参数: regionId?, schoolId?
  • 响应: { items: CatalogSchoolResponseDto[], meta }
  • 字段: id, legacyId, regionId, moduleId, name, professionalExamDate, metadata, createdAt, updatedAt

5. GET /api/catalog/majors

  • 安全方案: x-tenant-id
  • 查询参数: regionId?, schoolId?, majorId?, moduleId?, type?
  • 响应: { items: CatalogMajorResponseDto[], meta }

6. GET /api/catalog/subjects

  • 安全方案: x-tenant-id
  • 查询参数: regionId?, schoolId?, majorId? (UUID), moduleId? (UUID), type? (string)
  • 响应: { items: CatalogEntityDto[], meta }

7. GET /api/catalog/categories

  • 安全方案: x-tenant-id
  • 查询参数: subjectId? (UUID), nodeId? (UUID, 旧导航节点)
  • 响应: { items: CatalogEntityDto[], meta }

8. GET /api/catalog/questions

  • 安全方案: x-tenant-id
  • 查询参数:
    • subjectId? (UUID)
    • categoryId? (UUID)
    • nodeId? (UUID, 旧导航节点)
    • entryId? (UUID)
    • contentNodeId? (UUID)
    • collectionId? (UUID)
    • questionIds? (string | string[], 逗号分隔或重复传参)
    • limit? (int, 1-2000)
  • 响应: { items: QuestionResponseDto[], meta }
  • QuestionResponseDto 字段 (extends CatalogEntityDto):
    • id (uuid)
    • type (string, 必填 — 题型)
    • typeLabel (string, nullable)
    • difficulty (number, nullable)
    • content (unknown, 题干)
    • options (array, 选项列表)
    • explanation (string, nullable — 敏感字段)
    • hasVideoExplanation (boolean)
    • 继承字段: legacyId, name, title, regionId, order, isActive, description, metadata
  • 注意: options 中包含正确选项标记、explanation 包含答案解析。RuoYi 在返回学生端 DTO 前必须剥离这些字段。

9. GET /api/catalog/content-entries

  • 安全方案: x-tenant-id
  • 查询参数:
    • regionId? (UUID)
    • entryType? (string)
    • includeHidden? (boolean, default false)
  • 响应: { items: ContentEntryResponseDto[], meta }

10. GET /api/catalog/content-nodes

  • 安全方案: x-tenant-id
  • 查询参数:
    • entryId (UUID, 必填)
    • parentId? (string, "root" 表示根节点)
    • mode? ('children' | 'flat', default 'children')
    • includeInactive? (boolean, default false)
    • markerType? (string)
  • 响应: { items: ContentNodeResponseDto[], meta }

11. GET /api/catalog/question-collections

  • 安全方案: x-tenant-id
  • 查询参数:
    • regionId? (UUID)
    • entryId? (UUID)
    • nodeId? (UUID)
    • collectionType? (string)
    • limit? (int, 1-2000)
  • 响应: { items: QuestionCollectionResponseDto[], meta }

12. GET /api/catalog/question-collections/questions

  • 安全方案: x-tenant-id
  • 查询参数:
    • collectionId (UUID, 必填)
    • limit? (int, 1-2000)
  • 响应: { items: QuestionResponseDto[], meta }

13. GET /api/catalog/practice-blueprints

  • 安全方案: x-tenant-id
  • 查询参数:
    • entryId? (UUID)
    • nodeId? (UUID)
    • collectionId? (UUID)
    • mode? (string)
    • limit? (int, 1-2000)
  • 响应: { items: PracticeBlueprintResponseDto[], meta }
  • 字段: id, mode, entryId, nodeId, collectionId, questionLimit, durationMinutes + CatalogEntityDto 继承字段

认证机制

租户识别

  • 所有 catalog 路径使用 @TenantAccess() 装饰器 → AccessPolicy { kind: 'tenant' }
  • 租户 ID 从 x-tenant-id 请求头提取CORS 白名单包含此头)
  • Principal 装饰器从请求上下文提取 principal.tenant.tenantId

Bearer Token

  • catalog 的大多数端点不需要 Bearer只读、租户级访问
  • assetsassets/downloadassets/preview 需要 @ApiBearerAuth()
  • 学习写入路径 (/api/learning/*) 需要 @ApiBearerAuth() + @TenantUserAccess()

响应格式

成功

{
  "items": [...],
  "meta": { "requestId": "uuid" }
}

{
  "item": {...},
  "meta": { "requestId": "uuid" }
}

错误

{
  "error": "面向调用方的错误信息",
  "code": "REQUIRED_FIELD",
  "requestId": "uuid",
  "meta": { "requestId": "uuid" }
}

与 RuoYi adapter 的差异

项目 RuoYi (Java) 假设 Scalar (tiku-backend) 实际
基础路径 配置的 base-url /api/catalog/*
认证头 Authorization: Bearer <token> 大多数 catalog 端点只需 x-tenant-id,不需要 Bearer
租户头 x-tenant-id x-tenant-id 一致
分页 page + pageSize limit (1-2000),无 page 参数!
题目过滤 published=true&hidden=false 由 RuoYi 追加 Scalar 端已有 isActive 过滤,但无 published/hidden query 参数
响应 envelope 预期 items + 可能的 total items + meta.requestId,无 total 字段!
正确答案 RuoYi 在返回学生端前剥离 QuestionResponseDto.options 包含正确选项标记

⚠️ 关键差异

  1. 分页: RuoYi ScalarCatalogProvider 使用 page + pageSize query 参数,但 Scalar 只接受 limit。Java 端第 526-540 行固定追加 pagepageSize —— 这些参数在 Scalar controller 中不存在,会被忽略。
  2. total 字段: RuoYi adapter 期望服务端返回 total 用于分页,但 Scalar 响应没有此字段。如果 RuoYi 依赖 total 做前端分页计算,需要确认 adapter 如何处理。
  3. published/hidden: RuoYi 端固定追加 published=true&hidden=false,但这些参数在 Scalar controller DTO 中未定义。Scalar 的过滤逻辑在 repository 层而非 query 参数层。

OpenAPI 生成方式

# 需要 Docker + Supabase 运行
cd tiku-backend
npm run supabase:start
npm run dev:api
curl http://127.0.0.1:8787/openapi.json > /tmp/tiku-openapi.json

# 或通过测试套件(也会启动真实服务器)
BACKEND_TEST_SKIP_DATABASE=true npm run test:backend:migration
# 生成文件:/tmp/tiku-openapi.json

当前环境不具备 Supabase/Docker无法生成运行时 OpenAPI JSON。源码级契约已在此文档冻结。