Files
opendance-windows/contracts/rest-api.yaml
T

818 lines
26 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: OpenDance Studio Web 管理服务端 API
version: 0.1.0
description: |
OpenDance Studio 的唯一数据源与契约权威。
契约版本见 contracts/VERSION。字段语义见 contracts/data-models.md。
- 所有 JSON 字段 camelCase
- 列表接口统一 { items, total, page, pageSize }
- 来源五元组(sourceType/sourceName/sourceUrl/license/author)在三类资产上均为必填
servers:
- url: http://localhost:3000
description: 本地开发
- url: http://<host>:3000
description: Docker 部署
tags:
- name: health
- name: songs
- name: motions
- name: models
- name: reviews
- name: catalog
- name: assets
paths:
/health:
get:
tags: [health]
summary: 健康检查(含数据库连通性)
responses:
'200':
description: 服务与数据库均正常
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
'503':
description: 数据库不可用
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
/api/songs:
get:
tags: [songs]
summary: 曲目列表
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- name: q
in: query
schema: { type: string }
description: 按 title/artist/album 模糊匹配
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
- name: artist
in: query
schema: { type: string }
responses:
'200':
description: 分页曲目
content:
application/json:
schema:
$ref: '#/components/schemas/SongPage'
post:
tags: [songs]
summary: 新建曲目
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SongCreate'
responses:
'201':
description: 已创建
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
/api/songs/{id}:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [songs]
summary: 曲目详情
responses:
'200':
description: 曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [songs]
summary: 更新曲目
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SongUpdate' }
responses:
'200':
description: 更新后的曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [songs]
summary: 删除曲目(软删除)
responses:
'204': { description: 已删除 }
'404': { $ref: '#/components/responses/NotFound' }
/api/songs/{id}/audio:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [songs]
summary: 上传曲目音频
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file: { type: string, format: binary }
responses:
'201':
description: 上传后的曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
/api/songs/{id}/cover:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [songs]
summary: 上传曲目封面
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file: { type: string, format: binary }
responses:
'201':
description: 上传后的曲目
content:
application/json:
schema: { $ref: '#/components/schemas/Song' }
'400': { $ref: '#/components/responses/BadRequest' }
/api/motions:
get:
tags: [motions]
summary: 动作列表
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- name: q
in: query
schema: { type: string }
- name: songId
in: query
schema: { type: integer, format: int64 }
- name: format
in: query
schema: { type: string, enum: [bvh, fbx, gltf, glb, keypoints-json] }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
responses:
'200':
description: 分页动作
content:
application/json:
schema: { $ref: '#/components/schemas/MotionPage' }
post:
tags: [motions]
summary: 上传动作文件 + 来源信息
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file, name, sourceType, sourceName, sourceUrl, license, author]
properties:
file: { type: string, format: binary }
name: { type: string }
songId: { type: integer, format: int64 }
fps: { type: number }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
autoPreprocess: { type: boolean, default: false }
responses:
'201':
description: 已创建
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'400': { $ref: '#/components/responses/BadRequest' }
'422':
description: 文件格式不支持或无法解析
/api/motions/{id}:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [motions]
summary: 动作详情
responses:
'200':
description: 动作
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [motions]
summary: 更新动作元数据
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/MotionUpdate' }
responses:
'200':
description: 更新后的动作
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'400': { $ref: '#/components/responses/BadRequest' }
delete:
tags: [motions]
summary: 删除动作(软删除)
responses:
'204': { description: 已删除 }
/api/motions/{id}/preprocess:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [motions]
summary: 触发 BVH/FBX → MediaPipe33 关键点序列预处理(同步执行)
description: |
同步执行并返回最终状态:成功时 preprocessState=done;
失败时返回 422,且 Motion.preprocessError 记录可读原因。
responses:
'200':
description: 预处理完成
content:
application/json:
schema: { $ref: '#/components/schemas/Motion' }
'404': { $ref: '#/components/responses/NotFound' }
'422':
description: 预处理失败,reason 见 preprocessError
/api/motions/{id}/keypoints:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [motions]
summary: 下载关键点序列(NDJSON,每行一帧)
responses:
'200':
description: |
application/x-ndjson,每行一个帧对象:
{"frame":0,"timeMs":0,"points":[[x,y,z,conf], …33],"angles":[9 个余弦]}
content:
application/x-ndjson:
schema: { type: string }
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: 尚未完成预处理
/api/models:
get:
tags: [models]
summary: 模型列表
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- name: q
in: query
schema: { type: string }
- name: format
in: query
schema: { type: string, enum: [vrm, glb, gltf, fbx] }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
responses:
'200':
description: 分页模型
content:
application/json:
schema: { $ref: '#/components/schemas/ModelPage' }
post:
tags: [models]
summary: 上传模型 + 来源信息(VRM 优先)
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file, name, sourceType, sourceName, sourceUrl, license, author]
properties:
file: { type: string, format: binary }
name: { type: string }
isPrimary: { type: boolean }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
responses:
'201':
description: 已创建
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'400': { $ref: '#/components/responses/BadRequest' }
'422':
description: 不支持的模型格式
/api/models/{id}:
parameters: [{ $ref: '#/components/parameters/Id' }]
get:
tags: [models]
summary: 模型详情
responses:
'200':
description: 模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [models]
summary: 更新模型元数据
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ModelUpdate' }
responses:
'200':
description: 更新后的模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'400': { $ref: '#/components/responses/BadRequest' }
delete:
tags: [models]
summary: 删除模型(软删除)
responses:
'204': { description: 已删除 }
/api/models/{id}/primary:
parameters: [{ $ref: '#/components/parameters/Id' }]
post:
tags: [models]
summary: 设为全局默认模型(互斥,同事务清零其余)
responses:
'200':
description: 设置后的模型
content:
application/json:
schema: { $ref: '#/components/schemas/Model' }
'404': { $ref: '#/components/responses/NotFound' }
/api/reviews:
get:
tags: [reviews]
summary: 审核流水 / 待审列表
parameters:
- name: targetType
in: query
schema: { type: string, enum: [song, motion, model] }
- name: targetId
in: query
schema: { type: integer, format: int64 }
- name: status
in: query
schema: { $ref: '#/components/schemas/PublishStatus' }
description: 传入时改为返回该状态的待审资产列表(合并三类)
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
responses:
'200':
description: 审核流水或待审资产
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/ReviewPage'
- $ref: '#/components/schemas/PendingPage'
/api/reviews/{targetType}/{id}/submit:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 提交审核
requestBody:
content:
application/json:
schema:
type: object
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'409': { description: 当前状态不允许该操作 }
/api/reviews/{targetType}/{id}/approve:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 审核通过 → published
requestBody:
content:
application/json:
schema:
type: object
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'409': { description: 当前状态不允许该操作 }
/api/reviews/{targetType}/{id}/withdraw:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 撤回审核 → draft
requestBody:
content:
application/json:
schema:
type: object
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'409': { description: 当前状态不允许该操作 }
/api/reviews/{targetType}/{id}/history:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
get:
tags: [reviews]
summary: 单个资产的审核历史
responses:
'200':
description: 审核流水(按时间倒序)
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/ReviewRecord' }
'404': { $ref: '#/components/responses/NotFound' }
/api/reviews/{targetType}/{id}/reject:
parameters:
- name: targetType
in: path
required: true
schema: { type: string, enum: [song, motion, model] }
- $ref: '#/components/parameters/Id'
post:
tags: [reviews]
summary: 审核驳回 → rejected
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [comment]
properties:
reviewer: { type: string }
comment: { type: string }
responses:
'200': { description: 状态已流转 }
'400': { description: 驳回必须填写理由 }
'409': { description: 当前状态不允许该操作 }
/api/catalog/export:
get:
tags: [catalog]
summary: 客户端一次性拉取已发布目录
responses:
'200':
description: CatalogBundle
content:
application/json:
schema: { $ref: '#/components/schemas/CatalogBundle' }
/api/catalog/version:
get:
tags: [catalog]
summary: 契约版本与目录指纹(客户端增量同步用)
responses:
'200':
description: 版本信息
content:
application/json:
schema:
type: object
properties:
contractVersion: { type: object, additionalProperties: { type: string } }
revision: { type: string, description: 目录内容 sha256,变化即需重新拉取 }
components:
parameters:
Id:
name: id
in: path
required: true
schema: { type: integer, format: int64 }
Page:
name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
PageSize:
name: pageSize
in: query
schema: { type: integer, minimum: 1, maximum: 200, default: 20 }
responses:
BadRequest:
description: 参数校验失败(含来源五元组缺失)
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
NotFound:
description: 资源不存在
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
schemas:
SourceType:
type: string
enum: [original, licensed, cc, public_domain, user_upload, other]
PublishStatus:
type: string
enum: [draft, pending, published, rejected]
MotionFormat:
type: string
enum: [bvh, fbx, gltf, glb, keypoints-json]
ModelFormat:
type: string
enum: [vrm, glb, gltf, fbx]
ApiError:
type: object
properties:
statusCode: { type: integer }
message: { oneOf: [{ type: string }, { type: array, items: { type: string } }] }
error: { type: string }
SourceFields:
type: object
required: [sourceType, sourceName, sourceUrl, license, author]
properties:
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string, maxLength: 200 }
sourceUrl: { type: string, maxLength: 500, description: 必须为 http(s) URL }
license: { type: string, maxLength: 100 }
author: { type: string, maxLength: 200 }
Health:
type: object
properties:
status: { type: string, enum: [ok, degraded] }
uptimeSec: { type: number }
version: { type: string }
contractVersion: { type: object, additionalProperties: { type: string } }
database:
type: object
properties:
connected: { type: boolean }
server: { type: string, description: '如 MariaDB 10.11.19' }
database: { type: string }
migrationsPending: { type: integer }
Song:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
properties:
id: { type: integer, format: int64 }
title: { type: string }
artist: { type: string }
album: { type: string, nullable: true }
language: { type: string, nullable: true }
durationMs: { type: integer, nullable: true }
bpm: { type: number, nullable: true }
difficulty: { type: integer, nullable: true, minimum: 1, maximum: 5 }
tags: { type: array, items: { type: string } }
coverUrl: { type: string, nullable: true }
audioUrl: { type: string, nullable: true }
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
SongCreate:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
required: [title, artist]
properties:
title: { type: string, maxLength: 200 }
artist: { type: string, maxLength: 200 }
album: { type: string, maxLength: 200 }
language: { type: string }
durationMs: { type: integer }
bpm: { type: number }
difficulty: { type: integer, minimum: 1, maximum: 5 }
tags: { type: array, items: { type: string } }
status: { $ref: '#/components/schemas/PublishStatus' }
SongUpdate:
type: object
description: 所有字段可选;提供 source* 时必须五元组同时提供
properties:
title: { type: string }
artist: { type: string }
album: { type: string }
language: { type: string }
durationMs: { type: integer }
bpm: { type: number }
difficulty: { type: integer, minimum: 1, maximum: 5 }
tags: { type: array, items: { type: string } }
status: { $ref: '#/components/schemas/PublishStatus' }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
SongPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Song' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
Motion:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
properties:
id: { type: integer, format: int64 }
songId: { type: integer, format: int64, nullable: true }
name: { type: string }
format: { $ref: '#/components/schemas/MotionFormat' }
fileUrl: { type: string, nullable: true }
fileSize: { type: integer, nullable: true }
fps: { type: number, nullable: true }
frameCount: { type: integer, nullable: true }
durationMs: { type: integer, nullable: true }
keypointFormat: { type: string, nullable: true }
keypointUrl: { type: string, nullable: true }
keypointFrames: { type: integer, nullable: true }
preprocessState: { type: string, enum: [none, running, done, failed] }
preprocessError: { type: string, nullable: true }
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
MotionUpdate:
type: object
properties:
name: { type: string }
songId: { type: integer, format: int64, nullable: true }
fps: { type: number }
status: { $ref: '#/components/schemas/PublishStatus' }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
MotionPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Motion' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
Model:
allOf:
- $ref: '#/components/schemas/SourceFields'
- type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
format: { $ref: '#/components/schemas/ModelFormat' }
vrmVersion: { type: string, nullable: true }
isPrimary: { type: boolean }
fileUrl: { type: string, nullable: true }
fileSize: { type: integer, nullable: true }
thumbnailUrl: { type: string, nullable: true }
status: { $ref: '#/components/schemas/PublishStatus' }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
ModelUpdate:
type: object
properties:
name: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
sourceType: { $ref: '#/components/schemas/SourceType' }
sourceName: { type: string }
sourceUrl: { type: string }
license: { type: string }
author: { type: string }
ModelPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/Model' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
ReviewRecord:
type: object
properties:
id: { type: integer, format: int64 }
targetType: { type: string, enum: [song, motion, model] }
targetId: { type: integer, format: int64 }
action: { type: string, enum: [submit, approve, reject, withdraw] }
fromStatus: { $ref: '#/components/schemas/PublishStatus' }
toStatus: { $ref: '#/components/schemas/PublishStatus' }
reviewer: { type: string, nullable: true }
comment: { type: string, nullable: true }
createdAt: { type: string, format: date-time }
ReviewPage:
type: object
properties:
items: { type: array, items: { $ref: '#/components/schemas/ReviewRecord' } }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
PendingPage:
type: object
properties:
items:
type: array
items:
type: object
properties:
targetType: { type: string, enum: [song, motion, model] }
targetId: { type: integer, format: int64 }
name: { type: string }
status: { $ref: '#/components/schemas/PublishStatus' }
source: { $ref: '#/components/schemas/SourceFields' }
updatedAt: { type: string, format: date-time }
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
CatalogBundle:
type: object
properties:
contractVersion: { type: object, additionalProperties: { type: string } }
generatedAt: { type: string, format: date-time }
songs: { type: array, items: { $ref: '#/components/schemas/Song' } }
motions: { type: array, items: { $ref: '#/components/schemas/Motion' } }
models: { type: array, items: { $ref: '#/components/schemas/Model' } }
defaultModelId: { type: integer, format: int64, nullable: true }