7.9 KiB
7.9 KiB
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 Token(JWT)+
x-tenant-idheader - 响应 envelope:
{ items/item, meta: { requestId } }
RuoYi Education 实际调用的路径
以下为 RuoYi ScalarCatalogProvider 使用的只读 GET 路径:
1. GET /api/catalog/regions
- 安全方案:
x-tenant-idheader (@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(只读、租户级访问)
assets、assets/download、assets/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 包含正确选项标记 |
⚠️ 关键差异
- 分页: RuoYi
ScalarCatalogProvider使用page+pageSizequery 参数,但 Scalar 只接受limit。Java 端第 526-540 行固定追加page和pageSize—— 这些参数在 Scalar controller 中不存在,会被忽略。 - total 字段: RuoYi adapter 期望服务端返回
total用于分页,但 Scalar 响应没有此字段。如果 RuoYi 依赖total做前端分页计算,需要确认 adapter 如何处理。 - 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。源码级契约已在此文档冻结。