课程上传 API 说明
面向自动化 agent / 上游内容系统的 HTTP API。基址 https://classroom.deepwire.win,全部请求/响应均为 JSON(媒体二进制除外)。
1. 认证
所有 /api/v1/admin/* 接口需要 Bearer token:
Authorization: Bearer <token>
token 由管理员在后台 /admin/courses 页的「上传 API 凭据」创建(明文只显示一次)。凭据可随时在后台撤销,撤销立即生效。
未带/无效凭据返回 401 {"error":"unauthorized"}。
2. 媒体上传(视频 / 封面图)
三步流程:创建媒体 → 上传字节 → 完成。完成后拿到媒体 id,供课程提交引用。
2.1 创建媒体
POST /api/v1/admin/media
Content-Type: application/json
{
"kind": "video", // "video" | "image"
"filename": "ch1.mp4",
"content_type": "video/mp4", // video 必须 video/mp4;image 须 image/* 前缀
"size": 28364 // 精确字节数(int > 0)
}
响应 201:
{ "id": "84eef757-b5ba-4c2e-8fde-d2556fc8bdee", "upload": { "single_max_bytes": 33554432 } }
2.2 上传字节
≤32MB 走单文件直传(请求体即文件内容,须带 Content-Length):
PUT /api/v1/admin/media/<id>
Content-Type: video/mp4
<二进制体>
>32MB 走分段上传(每段 1-64MB,段号 1 起连续 1-10000,不支持 chunked):
POST /api/v1/admin/media/<id>/multipart
# → { "upload_id": "..." }
PUT /api/v1/admin/media/<id>/multipart/1?upload_id=<upload_id>
<第 1 段二进制> # → { "etag": "...", "size": N };同段重传覆盖
POST /api/v1/admin/media/<id>/multipart/2?upload_id=<upload_id>
<第 2 段二进制>
...
2.3 完成上传
POST /api/v1/admin/media/<id>/complete
Content-Type: application/json
{} # 单文件模式
{ "upload_id": "..." } # 分段模式
完成时校验:分段须从 1 连续无缺;实际总大小与申报 size 容差 ±1MB,超差 422 size_mismatch。放弃上传可 POST /api/v1/admin/media/<id>/abort(body 带 upload_id)。
3. 提交课程(幂等)
POST /api/v1/admin/courses
Content-Type: application/json
Authorization: Bearer <token>
以 external_id 为幂等键:已存在则整体更新,不存在则新建。
| 字段 | 类型 | 说明 |
|---|---|---|
external_id | string | 必填,幂等键(如上游系统课程 ID) |
title | string | 必填 |
intro_md | string | 课程介绍 Markdown;缺省=保留原值 |
price_cents | int | 单课价格(分),0-100000000;缺省=保留原值 |
category | string|null | 按分类名匹配(须已存在);null=移除分类;缺省=保留 |
cover_media_id | string|null | 封面媒体 id(须已 complete 的 image);null=移除;缺省=保留 |
chapters | array | 本次提交的章节,按数组序排 sort(1 起);未包含的既有章节不动 |
remove_chapter_external_ids | string[] | 按章节 external_id 删除指定章节 |
章节对象:
| 字段 | 类型 | 说明 |
|---|---|---|
external_id | string | 必填,章节幂等键(课程内唯一) |
title | string | 必填 |
content_md | string | 正文 Markdown,缺省 "" |
video_media_id | string | 视频媒体 id(须已 complete 的 video/mp4);缺省=不设视频 |
响应:
{ "course_id": "839a2d5d-...", "chapter_ids": { "<章节external_id>": "<uuid>" } }
校验失败 422(零写入,字段级报错):
{ "errors": [ { "field": "chapters[0].video_media_id", "message": "媒体不存在、未上传或不是 MP4 视频" } ] }
Markdown 内嵌图片
正文/介绍里引用上传的图片用规范形态 /api/v1/media/<媒体id>,站点渲染时自动改写为按入口鉴权的路径:

4. 完整示例:上传一门两章课程
B=https://classroom.deepwire.win; T="Bearer <token>"
# ① 封面
ID=$(curl -s -X POST $B/api/v1/admin/media -H "Authorization: $T" \
-d '{"kind":"image","filename":"cover.png","content_type":"image/png","size":12345}' | jq -r .id)
curl -s -X PUT $B/api/v1/admin/media/$ID -H "Authorization: $T" \
-H "Content-Type: image/png" --data-binary @cover.png >/dev/null
curl -s -X POST $B/api/v1/admin/media/$ID/complete -H "Authorization: $T" -d '{}' >/dev/null
COVER=$ID
# ② 各章视频(同法),得 V1、V2
# ③ 提交课程
curl -s -X POST $B/api/v1/admin/courses -H "Authorization: $T" \
-d @- <<'EOF'
{
"external_id": "upstream-course-001",
"title": "数学思维入门",
"intro_md": "从直觉到方法的数学思维课。\n\n",
"price_cents": 10,
"category": "数学",
"cover_media_id": "54b29335-d563-4c3d-a213-8f1a9d7d3238",
"chapters": [
{ "external_id": "ch-001", "title": "第一章:什么是数学思维", "content_md": "本章免费试学。", "video_media_id": "84eef757-b5ba-4c2e-8fde-d2556fc8bdee" },
{ "external_id": "ch-002", "title": "第二章:结构化拆解", "content_md": "…", "video_media_id": "a7472a0d-c508-4ad0-a834-60dc0bd584fa" }
]
}
EOF
5. 上线还差两步(管理后台操作,API 不代办)
API 提交的课程一律 published=0(草稿)。学员可见前需管理员在 /admin:
- 把课程加入目标子系统站点(
/admin/subsystems→ 选择课程加入),站点入口形如/s/<slug>; - 发布课程(
/admin/courses→ 发布)。发布校验:至少一章,全部被引用媒体(视频/封面/Markdown 内嵌图)已上传完成。第一章自动成为免费试学章。
6. 错误码速查
| 状态 | body | 含义 |
|---|---|---|
| 401 | {"error":"unauthorized"} | 缺少/无效 Bearer token |
| 400 | {"error":"invalid_json"} 等 | 媒体创建参数不合法(invalid_kind / invalid_size / invalid_content_type 等) |
| 413 | too_large_for_single / part_too_large | 超单文件 32MB / 单段 64MB 上限 |
| 422 | size_mismatch / missing_parts | complete 时大小不符(±1MB 容差)/ 段号不连续 |
| 422 | {"errors":[{field,message}]} | 课程提交字段校验失败(零写入) |