63 KiB
第一阶段接口清单(后台管理 + 客户端)
版本 v1.0 · 配套 PRD 与 后端设计 状态说明:已实现 = 代码已落地可联调;下一批 = 表结构与契约已冻结,代码待补。
0. 通用约定
服务入口:后台管理 fruitshorts-admin-api 只暴露 /admin/**(8080);
客户端 fruitshorts-app-api 只暴露 /app/** 与媒资 /files/**(8081)。
统一响应体
{ "code": 0, "message": "ok", "data": {} }
code = 0 成功;非 0 时 message 可直接展示给用户。分页统一为 { total, list }。
ID 字段一律以字符串下发(2026-10-16 J1 起,全局生效)
所有 ID 字段(主键与外键,例如 id / dramaId / episodeId / userId / orderId …)
在 JSON 里都是字符串,不是数字:
{ "code": 0, "message": "ok", "data": { "id": "2111012753262120962", "dramaId": "5" } }
原因:S0 之后主键是雪花 id,19 位十进制数已超过 JS 的 Number.MAX_SAFE_INTEGER
(9007199254740991)。裸数字下发会被浏览器/IEEE754 客户端四舍五入
(实测库里的 2111012753262120962 在浏览器里变成 2111012753262121000),
页面再拿它回查就是 200002 数据不存在。
约定与边界:
- id 回传时按字符串传(路径变量 / 查询参数 / 请求体都接受字符串,服务端自己转
Long); - 实现方式:服务端全局把
Long/long序列化成字符串(fruitshorts-framework的JacksonLongToStringConfig),各模块不再逐字段加注解; - 例外:分页
total仍是数字 —— 它是条数不是 id,前端分页组件要数字;Integer/int字段(pageIndex/pageSize/ 各类计数如totalEpisodes、状态码code) 也保持数字。
鉴权:Authorization: Bearer <token>。
管理端与客户端用两套独立密钥签发,管理端 token 打到客户端接口会直接被拒。
客户端 access token 默认 2 小时,refresh token 30 天且每次刷新轮换。
设备标识:客户端建议所有请求都带 X-Device-Id,用于下载授权、并发播放限制与风控。
主要错误码
| code | 含义 | 客户端处理 |
|---|---|---|
| 0 | 成功 | — |
| 400 | 参数校验失败 | 提示 message |
| 401 | 未登录或登录过期 | 用 refresh token 换新 token,失败则跳登录 |
| 403 | 无权限 | 提示无权限 |
| 429 | 请求过于频繁 | 提示稍后再试(验证码频控走这里) |
| 100000 | 账号或密码错误 | 提示 |
| 100001 | 账号已停用 | 展示封禁申诉入口 |
| 100002 | 金币余额不足 | 拉起充值 |
| 100003 | 剧集未发布或不存在 | 返回上一页 |
| 100005 | 订单已支付或已关闭 | 刷新订单状态 |
| 200002 | 数据不存在 | 提示 |
| 200003 | 当前状态不允许该操作 | 提示 |
1. 客户端接口(/app/**)
1.1 账号(已实现)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /app/auth/otp |
匿名 | 发送验证码。scene = REGISTER/LOGIN/RESET_PWD/BIND |
| POST | /app/auth/register |
匿名 | 手机号/邮箱注册,返回令牌 |
| POST | /app/auth/login |
匿名 | 验证码或密码登录 |
| POST | /app/auth/refresh |
匿名 | 用 refresh token 换新令牌并轮换 |
| POST | /app/auth/logout |
可选 | 登出;allDevices=true 吊销全部会话 |
发送验证码:
POST /app/auth/otp
{ "scene": "REGISTER", "targetType": "PHONE", "target": "08123456789", "deviceId": "d-1" }
手机号会自动规范化成 E.164(印尼本地 08xx → +62xxx)。频控:同号 60 秒一次、每日 10 次,
同 IP 每小时 20 次;连续输错 5 次该验证码作废。
注册:
POST /app/auth/register
{
"targetType": "PHONE", "target": "08123456789", "code": "123456",
"password": "可选,设置后可用密码登录",
"nickname": "可选",
"deviceId": "d-1", "platform": "IOS", "appVersion": "1.0.0",
"agreementVersion": "2026-10-01", "marketingConsent": false
}
邮箱 + 密码注册(C 端第一批口径)
POST /app/auth/register
{
"targetType": "EMAIL", "target": "User@Example.com ", "password": "nonton2026",
"deviceId": "d-1", "countryCode": "ID", "agreementVersion": "2026-10-01"
}
| 规则 | 口径 |
|---|---|
targetType=EMAIL |
code 可以不传(邮箱+密码直接注册);传了就走一次验证码校验(邮箱验证能力保留) |
targetType=PHONE |
维持原状:code 必填 |
| 邮箱规范化 | 去空格 + 转小写(batch1@Example.com → batch1@example.com);格式不合法直接 400 邮箱格式不正确 |
| 密码强度 | 8–64 位,且同时包含字母与数字(不强制特殊字符:低端机输入成本高,拦机器注册靠频控) |
| 注册频控 | 同 IP / 同设备 24h 默认上限分别为 5 / 3 个,超出返回 429(TOO_MANY_REQUESTS) |
| 邮箱验证状态 | 免码注册时 app_user_credential.verified = 0(保留"待验证"),不阻塞登录;只有验证码登录会把 verified 置 1,密码登录不改变该状态 |
频控口径落库在 app_user.register_ip / app_user.register_device_id 上(按 24h 计数,不新建计数表)。
注册频控阈值存储在 sys_config,可在后台「系统配置」修改:
app.register.max-per-ip-per-day
app.register.max-per-device-per-day
运行时优先读取 sys_config。键缺失或值不是整数时,回退 application.yml
中的 fruitshorts.auth.register-max-per-ip-per-day /
fruitshorts.auth.register-max-per-device-per-day,缺省为 5 / 3;
值小于等于 0 表示对应维度不限制。
开发种子配置为 0 / 0,生产建议恢复为 5 / 3。
残留风险:IP 取自 X-Forwarded-For、设备号由客户端上报,都能伪造,只能挡脚本批量;
真要防住需要接图形/行为验证码或风控服务,本阶段未做。验证码通道目前是 LogOtpSender(只写日志不发信),
所以邮箱可达性未验证 —— 这也是"不阻塞注册"的前提。
响应(登录、刷新同结构):
{
"code": 0, "message": "ok",
"data": {
"accessToken": "...", "refreshToken": "...", "expiresIn": 7200, "tokenType": "Bearer",
"newUser": true,
"user": { "uid": "U8KQ2ZP1", "nickname": "User8KQ2ZP", "status": "normal",
"countryCode": "ID", "language": "id-ID", "currency": "IDR",
"maskedTarget": "+628****789", "vipLevel": 0, "vipExpireAt": null }
}
}
1.2 个人中心(已实现)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /app/me |
当前用户资料 |
| PUT | /app/me |
修改昵称/头像/性别/生日/语言/时区/区域 |
| GET | /app/me/devices |
登录设备列表 |
| GET | /app/me/export |
数据导出(商店与隐私法规要求) |
| POST | /app/me/deactivate |
申请注销,进入 15 天冷静期并立即登出全部设备 |
| POST | /app/me/deactivate/cancel |
冷静期内撤销注销 |
1.3 内容浏览(已实现)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /app/home/feed |
首页:轮播 + 分区,卡片带 preview 可播预览集与 language 语种 |
| GET | /app/discover/feed |
发现页分区 |
| GET | /app/feed?types=ANIME,LIVE&categoryId=7&cursor=&size=20 |
推荐流;支持内容类型与分类叠加过滤,游标翻页 |
| GET | /app/categories |
分类列表 |
| GET | /app/dramas/{id} |
详情(含剧集列表,免费集带已签名播放地址) |
| GET | /app/search?keyword=&pageIndex=&pageSize= |
搜索已上线短剧:按标题/简介模糊匹配,分页返回卡片列表 |
卡片(
AppDramaCardVO)统一新增language字段(取值如zh-CN/id), 首页、发现页、分类、收藏、搜索等所有下发卡片的场景都带该字段,客户端可据此做语言筛选。卡片同时下发
likeCount/favoriteCount/commentCount(均为 JSON 字符串)。favoriteCount取整部剧;likeCount/commentCount取卡片preview当前集的drama_episode.like_count与content_comment.episode_id统计(排除逻辑删除与status = 'deleted');viewCount热度仍取整部剧。首页、发现、分类、收藏、搜索等列表卡片结构一致。卡片新增
dramaType:ANIME表示漫剧,LIVE表示真人剧。 客户端字段仍返回主内容类型;后台多选值保存在drama.drama_types, App 类型筛选按逗号列整词匹配,因此ANIME,LIVE多标签剧可被任一类型命中。
推荐类型流(GET /app/feed)
types可选,逗号分隔,支持ANIME、LIVE或多选ANIME,LIVE;不传或空值返回全部已上线内容。 过滤按drama_types整词匹配,多标签剧可被多个类型同时命中。categoryId可选;不传返回全部分类。传入后按category_ids整词匹配, 可与types叠加,只返回同时命中内容类型和分类的已上线短剧。cursor可选,上一页返回的nextCursor;size默认 20,最大 50。 游标短剧必须同时满足已上线、内容类型命中和分类命中,否则返回空列表。- 排序:
recommend_weight DESC NULLS LAST, view_count DESC, id DESC。 - 返回
Result<CursorResult<AppDramaCardVO>>,卡片字段与首页一致,包含互动计数、preview、language、dramaType。
短剧详情响应(GET /app/dramas/{id})
{
"code": 0, "message": "ok",
"data": {
"id": "5",
"title": "命运的相遇",
"synopsis": "一次意外的相遇,让两个世界的人走到一起。",
"cover": "/files/drama/202610/cover.jpg",
"genre": "都市 · 情感",
"category": "都市",
"totalEpisodes": 80,
"freeEpisodes": 10,
"unlockCoin": 30,
"isHot": true,
"viewCount": "12400",
"likeCount": "3200",
"favoriteCount": "1800",
"commentCount": "96",
"episodes": [
{
"id": "2111012753262120962",
"number": 1,
"title": "第 1 集",
"duration": "03:00",
"durationSec": 180,
"isFree": true,
"locked": false,
"unlockCoin": 0,
"likeCount": "45",
"commentCount": "8",
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c...",
"thumbnailsUrl": "/files/transcoded/88/thumbnails/thumbnails.vtt?e=1760000000&s=8a1d..."
}
]
}
}
详情里的互动数量:
| 字段 | 类型 | 说明 |
|---|---|---|
likeCount |
Long(JSON 字符串) | 剧级历史汇总,保留兼容;新客户端按集使用 episodes[].likeCount |
favoriteCount |
Long(JSON 字符串) | 取 drama.favorite_count,短剧累计收藏数 |
commentCount |
Long(JSON 字符串) | content_comment 中该剧未删除评论数:按 drama_id 统计,排除 status = 'deleted' 与逻辑删除记录 |
viewCount |
Long(JSON 字符串) | 取 drama.view_count,累计播放量,既有字段语义不变 |
episodes[].likeCount |
Long(JSON 字符串) | 本集点赞数,取 drama_episode.like_count |
episodes[].commentCount |
Long(JSON 字符串) | 本集未删除评论数,按 episode_id 统计 |
episodes[].thumbnailsUrl |
String / null | 本集雪碧图 WebVTT 时间轴签名地址;没有 READY 产物时为 null |
计数是只读快照:互动操作完成后客户端重新拉取详情即可刷新;Long 字段一律按字符串下发,
客户端需要转成数字再展示(与 viewCount 相同)。
1.4 播放(已实现,链路已重做)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | /app/episodes/{id}/play |
可选 | 签发播放票据与签名地址 |
{
"code": 0, "message": "ok",
"data": {
"episodeId": 12, "dramaId": 5, "number": 1, "title": "第 1 集", "durationSec": 180,
"accessible": true,
"accessType": "FREE",
"playUrl": "/files/episode/202610/xxx.mp4?e=1760000000&s=9f2c...",
"qualities": ["AUTO"],
"thumbnailsUrl": "/files/transcoded/88/thumbnails/thumbnails.vtt?e=1760000000&s=8a1d...",
"unlockCoin": 0,
"previewSeconds": 0,
"likeCount": "45",
"commentCount": "8"
}
}
accessType 取值:FREE(免费集)、ENTITLED(已购权益)、COIN_REQUIRED(需解锁,
此时 playUrl 为空、unlockCoin 有值)、NO_MEDIA(还没上传片源或未转码完成)。
判定顺序由服务端决定,客户端不得自行推断可否播放。 签名默认 15 分钟有效;
开启 fruitshorts.media.signature-required 后,/files/episode/** 没带有效签名会返回 403。
thumbnailsUrl 指向 WebVTT:cue 链接为同目录相对雪碧图
sprite_XXX.jpg#xywh=x,y,w,h。有 READY media_thumbnail 时返回签名地址,
无产物时为 null;该字段不影响 playUrl、qualities 和 HLS/原文件回落。
1.5 观看记录与收藏(已实现)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /app/watch/progress |
上报进度(建议每 10s 一次,切集时补一次) |
| GET | /app/watch/progress?dramaId= |
某剧续看位置 |
| GET | /app/watch/recent |
最近观看分页;pageIndex 默认 1,pageSize 默认 20、上限 100 |
| GET | /app/watch/recent?limit=20 |
旧版最近观看 List 响应,仅保留一版兼容 |
| DELETE | /app/watch/progress/{dramaId} |
删除某剧记录 |
| DELETE | /app/watch/progress |
清空记录 |
| POST | /app/favorites/toggle |
收藏/取消收藏,返回切换后状态 |
| GET | /app/favorites |
收藏分页;pageIndex 默认 1,pageSize 默认 20、上限 100 |
| DELETE | /app/favorites/{dramaId} |
取消收藏 |
进度合并规则:同一集内只前进不后退;换集直接采纳新集;finished 只在接近片尾或客户端显式上报时置位。
这样用户换设备不会看到进度倒退。
收藏分页
GET /app/favorites?pageIndex=1&pageSize=20
请求参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
pageIndex |
int | 否 | 1 | 页码,小于 1 按 1 处理 |
pageSize |
int | 否 | 20 | 每页数量,上限 100,小于 1 按 1 处理 |
响应 data 为 PageResult<AppDramaCardVO>:
{
"total": "23",
"list": [
{
"id": "12",
"title": "替嫁新娘:总裁的掌心宠",
"cover": "/files/drama_cover/12.jpg",
"preview": {}
}
]
}
排序固定为收藏时间倒序 created_at DESC,收藏时间相同时按 drama_id DESC。
仅统计和返回仍处于已上线状态的短剧。
最近观看分页
GET /app/watch/recent?pageIndex=1&pageSize=20
请求参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
pageIndex |
int | 否 | 1 | 页码,小于 1 按 1 处理 |
pageSize |
int | 否 | 20 | 每页数量,上限 100,小于 1 按 1 处理 |
limit |
int | 否 | 无 | 兼容旧版;只要传该参数就返回 List,不再返回 PageResult |
分页响应 data 为 PageResult<ProgressRespVO>:
{
"total": "18",
"list": [
{
"dramaId": "12",
"episodeId": "88",
"dramaTitle": "替嫁新娘:总裁的掌心宠",
"dramaCover": "/files/drama_cover/12.jpg",
"episodeNumber": 6,
"totalEpisodes": 68,
"positionMs": "80000",
"durationMs": "120000",
"finished": false,
"playCount": 3,
"updatedAt": "2026-10-22 10:00:00"
}
]
}
排序固定为 updated_at DESC, drama_id DESC。每页记录通过批量 IN 查询装配短剧和分集,
不逐条访问数据库。
ProgressRespVO 展示字段:
| 字段 | 类型 | 说明 |
|---|---|---|
dramaTitle |
string / null | 短剧标题 |
dramaCover |
string / null | 短剧封面 |
episodeNumber |
int / null | 当前续看集号 ep_no |
totalEpisodes |
int / null | 短剧总集数 |
旧兼容请求 GET /app/watch/recent?limit=20 的 data 仍为 List<ProgressRespVO>,
行为在本版本保留;新客户端应使用分页响应。
1.6 离线下载(已实现)
服务端只发授权与短时效地址,分片由客户端直接向 CDN 拉取 —— 这样并发下载不会打到应用服务器,服务端只记账(进度、配额、有效期)。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /app/downloads/policy |
策略与配额:单设备上限、授权天数、仅 Wi-Fi、清晰度上限、已用/剩余 |
| PUT | /app/downloads/settings |
偏好设置:仅 Wi-Fi 下载、最高清晰度 |
| POST | /app/downloads |
申请下载授权(同剧集重复申请幂等,只刷新有效期) |
| GET | /app/downloads |
下载列表(授权 + 任务进度,可直接渲染) |
| GET | /app/downloads/{episodeId}/url |
断点续传前重新取地址(授权不变,地址是短时效的) |
| POST | /app/downloads/{episodeId}/progress |
上报下载进度,用于断点续传与跨端展示 |
| DELETE | /app/downloads/{episodeId} |
删除单条下载(吊销授权) |
| DELETE | /app/downloads |
清空当前设备全部下载,返回清除条数 |
设备标识优先取请求体 deviceId,其次取请求头 X-Device-Id。
授权默认 30 天有效,过期由每日定时任务置为 EXPIRED;被吊销或过期的授权客户端下次联网即失效。
需要解锁的剧集走权益判定,未解锁不允许下载(当前内容均为免费集,付费链路见 09)。
配额与有效期由 fruitshorts.download.* 配置,通过 policy 下发,客户端不要写死。
1.7 互动与内容治理(已实现)
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /app/comments |
登录 | 发表评论/回复:过敏感词后按策略发布或转待审 |
| GET | /app/comments?dramaId=&cursor=&size= |
匿名 | 游标翻页;不传 rootId 返回顶层评论,传了返回该楼层回复 |
| GET | /app/comments/count?dramaId=&episodeId= |
匿名 | 传 episodeId 返回该集评论数;仅传 dramaId 返回全剧兼容汇总 |
| DELETE | /app/comments/{id} |
登录 | 删除自己的评论(逻辑置为 deleted) |
| POST | /app/likes/toggle |
登录 | 点赞/取消点赞,targetType 支持 DRAMA / EPISODE / COMMENT;新客户端点赞按集 |
| POST | /app/reports |
登录 | 举报评论/用户/短剧;同一目标只保留一条待处理工单 |
敏感词三档处置:BLOCK 直接拒绝并返回原因、REPLACE 替换后放行、REVIEW 转人工待审。
审核策略由 fruitshorts.interaction.comment-audit-mode 控制:
MACHINE_FIRST(机审通过即发布,当前默认)或 PRE_REVIEW(全部先审后发,生产建议)。
可见性:匿名与其他人只看得到 published;自己待审的评论也会返回,便于展示"审核中"。
评论列表禁止重复提交(同一剧集每分钟 5 条,可配)。
1.8 金币与权益(已实现)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /app/wallet |
金币余额 + 最近流水 |
| POST | /app/episodes/{id}/unlock |
金币解锁分集;幂等键 unlock:{userId}:{episodeId},重复调用不重复扣币 |
| GET | /app/products |
充值商品列表(只返回上架商品,type 可选 COIN_PACK/VIP) |
| POST | /app/orders |
创建充值订单(body 传 productId,返回 CREATED 订单,等待支付) |
| GET | /app/orders |
我的订单 |
| POST | /app/orders/verify |
提交 Google Play 票据:服务端校验 → 建单 → 发权益(详见下方说明) |
GET /app/products只返回status=1(上架)的商品,按sort升序、id降序。POST /app/orders请求体{ "productId": 123 },商品下架时返回400 商品已下架; 订单coinAmount = coinAmount + bonusCoin(与商店票据口径一致),状态为CREATED。 VIP 商品下单后交付时发放TYPE_VIP权益(dramaId=0、episodeId=0,永久有效), 播放/下载的权益判定在EntitlementPort中增加「存在有效 VIP 权益即放行」的判定。
解锁后播放入口与下载入口会同时放行 —— 权益判定由交易模块的 EntitlementPort 实现提供,
播放域与下载域都不需要改代码。金币变动一律带幂等键并写 coin_ledger,钱包余额与流水同事务。
POST /app/orders/verify(B1-1:Google Play 票据校验闭环)
请求体:
{ "platform": "GOOGLE_PLAY", "productId": "coin_550", "receipt": "<purchaseToken>" }
| 字段 | 说明 |
|---|---|
platform |
GOOGLE / GOOGLE_PLAY(两者等价,入库统一记 GOOGLE);Apple 走 B1-2 |
productId |
商店商品 id(Google Play 的 purchases.products.get 必需),与本平台 product.store_product_id 对得上才发权益 |
receipt |
Google Play 的 purchaseToken |
行为与错误码:
| 场景 | 返回 |
|---|---|
| 校验通过 | code=0,data 是订单(status=DELIVERED、coinAmount=商品金币+赠送、storeTransactionId=Google orderId) |
| 同一票据重复提交(同用户) | code=0 返回既有订单,不重复发币、不重复建单 |
| 票据已绑定其他账号 | 403 该票据已绑定其他账号,不能重复使用(防跨用户重放) |
| 票据无效 / 未购买 / 商品不一致 / 缺 orderId | 200003 + 具体原因;不产生订单、不发权益 |
| 商品未在后台配置 | 200002 该商品未在后台配置 |
| 平台不在支持范围 | 400 不支持的商店平台 |
商店未配置(默认 verifier=disabled) |
200003 Apple / Google 商店校验尚未配置(与 B1-1 之前的行为一致) |
幂等键是 (platform, store_transaction_id)(DB 唯一索引 uk_orders_store_txn),
重复/并发提交由数据库兜底;票据审计写 store_receipt(唯一索引 uk_store_receipt(platform, transaction_id)),
B1-2 的退款/拒付复验依赖它。服务端凭据(packageName + service account JSON)走配置/环境变量,不入库。
恢复购买:客户端换设备后重新提交同一个 purchaseToken(或调用 GET /app/orders 拉已购列表),
服务端按 (platform, storeTransactionId) 识别"权益已发过",返回既有订单、不重复发币;
若商店侧有购买而本地没有订单(例如换机前没同步),会按校验结果补单落库再发放,不需要人工介入。
1.9 创作者入驻、启动配置与公告(C 端,已实现)
说明 1:本节原来把
/app/orders、/app/wallet、/app/episodes/{id}/unlock也列在这里, 那三条属「金币与订单」,已在 §1.8 实现并冻结,因此已从本节移出。说明 2(口径变更 2026-10-16):创作者中心(工作台 / 作品管理 / 数据看板)由 Web 端承载 —— 创作者登录后台管理使用,见 §2.10;App 客户端不提供创作者中心。 因此本节已移除
GET /app/creator/works、GET /app/creator/stats两条原定义。 C 端只保留「入驻申请」+「启动配置」+「系统公告」,下面三条即全部。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /app/creator/apply |
登录 | 创作者入驻申请(幂等:重复提交返回当前状态,不重复建档) |
| GET | /app/config |
匿名 | 启动配置(协议版本、最低版本、开关;只下发白名单键) |
| GET | /app/notices |
匿名 | 系统公告(无数据返回空列表,不是 404) |
1.9.1 GET /app/config(匿名)
客户端登录前就要拿,用于:协议版本校验、强制更新判断、功能开关。
{
"code": 0, "message": "ok",
"data": {
"agreementVersion": "2026-10-01",
"minAppVersion": "1.0.0",
"forceUpdate": false,
"noticeEnabled": true,
"coinUnlockEnabled": true,
"downloadEnabled": true
}
}
| 键(对客户端字段) | 后台配置键 | 契约默认值 | 说明 |
|---|---|---|---|
agreementVersion |
app.agreement.version |
2026-10-01 |
当前协议版本;/app/creator/apply 必须与它一致 |
minAppVersion |
app.min.version |
1.0.0 |
低于它客户端应提示升级 |
forceUpdate |
app.force.update |
false |
是否强制更新 |
noticeEnabled |
app.notice.enabled |
true |
公告入口开关 |
coinUnlockEnabled |
app.coin.unlock.enabled |
true |
金币解锁开关 |
downloadEnabled |
app.download.enabled |
true |
离线下载开关 |
口径:只下发上述白名单键(服务端硬编码白名单),后台配置里的其它键(可能含内部开关) 一律不下发;库里缺键时用「契约默认值」兜底,不返回 null 让客户端自己猜默认值。
1.9.2 GET /app/notices(匿名)
GET /app/notices?pageIndex=1&pageSize=20
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
pageIndex |
int | 否 | 默认 1 |
pageSize |
int | 否 | 默认 20,最大 50 |
{
"code": 0, "message": "ok",
"data": {
"total": 1,
"list": [
{ "id": 2110123456789, "title": "开服公告", "content": "…", "type": "SYSTEM",
"publishAt": "2026-10-16 10:00:00", "expireAt": null }
]
}
}
口径:只返回 status=1(启用)且 target ∈ {ALL, USER}、publish_at <= 现在、
expire_at 为空或未过期的公告;按 publish_at desc, id desc 排序。
表里没有数据时返回 total=0, list=[](HTTP 与 code 都是成功),不是 404。
1.9.3 POST /app/creator/apply(登录)
{
"name": "星野工作室", "type": "COMPANY",
"contactName": "张三", "contactMobile": "08123456789", "contactEmail": "creator@example.com",
"countryCode": "ID", "agreementVersion": "2026-10-01"
}
| 字段 | 必填 | 校验 |
|---|---|---|
name |
✅ | 1–128 字 |
type |
否 | INDIVIDUAL(默认)/ COMPANY |
contactName / contactMobile / contactEmail |
否 | 只是联系方式,不参与鉴权 |
countryCode |
否 | 默认 ID |
agreementVersion |
✅ | 必须等于 /app/config 的 agreementVersion,否则 400 |
幂等口径(硬要求):以「C 端账号唯一」防重(DB 唯一索引 uk_creator_user(user_id) where user_id > 0):
| 情况 | 行为 |
|---|---|
| 首次申请 | 建档,status=pending,返回 {id, code, status, statusText, auditNote, appliedAt} |
| 已申请(pending) | 返回当前状态,不重复建档、不改动已有记录 |
| 已通过(approved) | 返回 status=approved(创作者工作台在 Web 端,见 §2.10;App 端只提示"已通过,请到创作者后台使用") |
| 被驳回 / 冻结 | 返回当前状态与 auditNote(是否允许重新申请由运营在后台处理,本期不做二次申请) |
| 并发重复提交 | 唯一索引兜底:捕获冲突后读回已有记录返回,不报 500 |
2. 后台管理接口(/admin/**)
2.1 认证与系统(已实现)
认证
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /admin/auth/login |
匿名 | 登录,返回 token |
| GET | /admin/auth/menus |
登录 | 当前用户菜单树 |
| GET | /admin/auth/profile |
登录 | 当前用户资料 |
| POST | /admin/auth/logout |
登录 | 登出 |
| POST | /admin/auth/password |
登录 | 修改密码 |
系统管理(RBAC)
| 资源 | 方法 | 路径 | 权限点 |
|---|---|---|---|
| 管理员 | GET | /admin/admin-users/page · /{id} |
system:admin-user |
| 管理员 | POST | /admin/admin-users |
system:admin-user |
| 管理员 | PUT | /admin/admin-users/{id} · /{id}/status · /{id}/password |
system:admin-user |
| 管理员 | DELETE | /admin/admin-users/{id} |
system:admin-user |
| 角色 | GET | /admin/roles/page · /list · /{id} · /{id}/permissions |
system:role(/list 登录即可) |
| 角色 | POST | /admin/roles |
system:role |
| 角色 | PUT | /admin/roles/{id} · /{id}/permissions |
system:role |
| 角色 | DELETE | /admin/roles/{id} |
system:role |
| 权限 | GET | /admin/permissions |
system:role |
| 菜单 | GET | /admin/menus/tree |
system:menu |
| 菜单 | POST · PUT · DELETE | /admin/menus · /admin/menus/{id} |
system:menu |
| 角色分类 | GET · POST | /admin/role-categories |
system:role-category |
| 角色分类 | PUT · DELETE | /admin/role-categories/{id} |
system:role-category |
| 系统配置 | GET | /admin/configs |
system:role / system:menu |
| 系统配置 | PUT | /admin/configs |
system:role |
| 操作日志 | GET | /admin/operation-logs/page · /{id} |
log:list |
| 字典 | GET | /admin/dicts/drama-category · /content-status |
drama:list |
2.2 内容与媒资(已实现)
新增字段:短剧 rating(内容分级)、regionCodes(可播区域)、版权窗口、
creatorId、tags、trailerUrl;剧集 transcodeStatus、hlsUrl、previewSeconds、字幕。
短剧(含审核发布状态机)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/dramas/page |
drama:list |
检索(含热门、状态、dramaTypes[]、categoryIds[] 多选筛选) |
| GET | /admin/dramas/{id} |
drama:list |
详情 |
| POST | /admin/dramas |
drama:create |
新建短剧;优先接收 dramaTypes[] / categoryIds[] |
| PUT | /admin/dramas/{id} |
drama:update |
编辑短剧;优先接收 dramaTypes[] / categoryIds[] |
| DELETE | /admin/dramas/{id} |
drama:delete |
删除短剧 |
| POST | /admin/dramas/{id}/submit |
drama:review / drama:update |
提交审核 |
| POST | /admin/dramas/{id}/approve |
drama:review |
审核通过 |
| POST | /admin/dramas/{id}/reject |
drama:review |
驳回 |
| POST | /admin/dramas/{id}/publish |
drama:review |
上线 |
| POST | /admin/dramas/{id}/offline |
drama:review |
下线 |
| GET | /admin/dramas/{id}/review-records |
drama:list |
审核记录 |
多选字段说明:
- 保存优先使用
dramaTypes[]/categoryIds[],去重并保持提交顺序;drama_type/category_id写第一个值,供旧客户端与旧接口兼容。 - 仅传
dramaType/categoryId时保持原单值行为。 - 列表响应返回
dramaTypes[]、categoryIds[]与按选中顺序映射的categoryNames[]; 分类名缺失时回退genre。 - 后台与 App 过滤均使用
(',' || column || ',') LIKE '%,value,%'整词匹配。
剧集
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/episodes?dramaId= |
episode:list |
某剧分集列表 |
| POST | /admin/episodes |
episode:upload |
新增分集 |
| PUT | /admin/episodes/{id} |
episode:upload |
编辑分集 |
| DELETE | /admin/episodes/{id} |
episode:upload |
删除分集 |
| PUT | /admin/episodes/sort |
episode:sort |
批量排序 |
短剧类型
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/drama-categories |
drama:list |
类型列表 |
| POST | /admin/drama-categories |
drama:create |
新增类型 |
| PUT | /admin/drama-categories/{id} |
drama:update |
编辑类型 |
| DELETE | /admin/drama-categories/{id} |
drama:delete |
删除类型 |
轮播图
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/banners/page · /admin/banners/{id} |
banner:list |
检索 / 详情 |
| POST | /admin/banners |
banner:create |
新增 |
| PUT | /admin/banners/{id} · /{id}/status · /sort |
banner:update |
编辑 / 改状态 / 排序 |
| DELETE | /admin/banners/{id} |
banner:delete |
删除 |
媒资分片上传(后台与创作者共用)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /admin/media/upload/init |
建上传会话(返回 uploadId / chunkSize / totalChunks / objectKey) |
| POST | /admin/media/upload/chunk |
传分片(uploadId + index + file),可重复传同一片 |
| GET | /admin/media/upload/{uploadId} |
断点续传回显(receivedChunks / totalChunks / percent / status / 完成后带 url) |
| POST | /admin/media/upload/complete |
合并分片出媒资(幂等:同一 uploadId 重复调用返回同一产物) |
| GET | /admin/media/{id} |
媒资详情 |
媒资上传与详情权限点均为
cover:upload或episode:upload(@ss.hasAnyPermission)。
上传会话归属:uploadId 相当于上传凭证,chunk / complete / 进度查询都校验会话发起人,
别人拿到 uploadId 也塞不进分片({"code":403,"message":"无权操作该上传会话"})。
转码档位配置
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/media/transcode-profile |
episode:upload |
读取当前配置;缺省返回四档默认值 |
| PUT | /admin/media/transcode-profile |
episode:upload |
保存配置并记录操作日志 |
配置保存在 sys_config 的 media.transcode.profile,JSON 结构:
{
"enabled": true,
"segmentSeconds": 6,
"ladder": [
{
"quality": "360p",
"width": 640,
"height": 360,
"videoBitrateKbps": 400,
"audioBitrateKbps": 64,
"enabled": true
}
],
"sprite": {
"enabled": true,
"intervalSeconds": 1,
"columns": 10,
"thumbWidth": 160
}
}
quality 必须唯一且非空,宽高必须为大于 0 的偶数,音视频码率必须大于 0,
至少启用一档;segmentSeconds 2–10、intervalSeconds 1–10、columns 5–20、
thumbWidth 120–240。非法请求返回统一错误码 400 且不写库。
2.3 App 用户管理(已实现)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/app-users/page |
app-user:list |
检索:keyword(uid/昵称/手机号/邮箱)、status、countryCode、registerSource |
| GET | /admin/app-users/{id} |
app-user:list |
详情 |
| GET | /admin/app-users/{id}/ban-records |
app-user:list |
封禁与申诉记录 |
| GET | /admin/app-users/{id}/watch-history |
app-user:list |
观看流水明细(带剧名/集号,客服核对用) |
| GET | /admin/watch-history/page |
watch-history:list |
跨用户观看记录分页:keyword、userId、dramaTitle、dramaId、episodeId、source、finished、startTime、endTime |
| PUT | /admin/app-users/{id}/status |
app-user:update |
改状态:normal/limited/banned |
状态处置会立即吊销该用户全部会话并写入封禁记录,避免"封了还能用"。
2.4 下载管理(已实现)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/downloads/page |
download:list |
按用户 / 设备 / 状态检索下载授权,含剧名与集号 |
| GET | /admin/downloads/stats |
download:list |
授权与任务的分布统计 |
| PUT | /admin/downloads/{id}/revoke |
download:revoke |
吊销授权,用户侧下次联网即失效 |
| POST | /admin/downloads/cleanup |
download:revoke |
手动清理过期授权(默认每天凌晨自动执行) |
2.5 内容治理(已实现)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/comments/page |
comment:list |
检索评论;status 为空返回全部,pending 即审核队列 |
| GET | /admin/comments/{id}/audit-records |
comment:list |
审核留证:机审与人审的完整链路 |
| PUT | /admin/comments/{id}/audit |
comment:audit |
通过 / 驳回(带原因码与说明) |
| DELETE | /admin/comments/{id} |
comment:audit |
删除评论 |
| GET | /admin/reports/page |
report:list |
举报工单检索(状态 / 类型 / 原因),返回举报人、剧名、集号、集标题与目标摘要 |
| PUT | /admin/reports/{id}/handle |
report:handle |
处理工单;结论成立会联动驳回被举报评论,可选择同时封禁被举报用户 |
| GET | /admin/sensitive-words/page |
sensitive-word:list |
敏感词检索 |
| POST/PUT/DELETE | /admin/sensitive-words[/{id}] |
sensitive-word:update |
维护词库,改动即时生效(服务端缓存失效重建) |
后台评论行除 dramaId/episodeId 外,还返回 dramaTitle/episodeNo/episodeTitle;
剧或集已删除时对应展示字段留空。后台评论与举报的 status 空值均表示全部状态。
2.6 订单与权益(已实现)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/orders/page /admin/orders/{orderNo} /{orderNo}/events |
order:list |
订单检索、详情、状态流转流水 |
| POST | /admin/orders/{orderNo}/compensate |
order:update |
补发:已支付未交付的订单重新交付,已交付幂等返回 |
| POST | /admin/orders/{orderNo}/refund |
order:update |
退款/撤销:回收权益 + 扣回金币(余额不足时留人工跟进备注) |
| GET | /admin/entitlements/page |
entitlement:list |
权益检索 |
| POST | /admin/entitlements |
entitlement:update |
人工补发权益(客服补偿) |
| PUT | /admin/entitlements/{id}/revoke |
entitlement:update |
回收权益,播放判定实时生效 |
| GET | /admin/wallets/{userId}/ledger |
order:list |
金币流水 |
| POST | /admin/wallets/coins |
wallet:update |
人工补币/扣回,必须带幂等键,先校验用户存在 |
| POST | /admin/reconcile/run · GET /admin/reconcile/page |
reconcile:run / order:list |
手动对账与历史(默认每天 04:20 自动跑前一天) |
对账口径(B1-2 起生效,字段名不变,新增两个字段)
| 字段 | 含义 |
|---|---|
totalCount |
当期本地有效订单数(排除 CREATED/CLOSED) |
matchedCount |
其中能在 store_receipt 找到同一 (platform, transaction_id) 的笔数 |
diffCount |
差异笔数 = 本地缺票据 + 票据无本地订单 + 商店已作废但本地无单 |
amountSum |
本地订单金额合计(本地口径,不是商店结算口径) |
storeAmountSum |
商店侧结算金额。Developer API 不提供(要向 Play Console 财务报告取)→ 无数据源时为 null |
notes |
说明差异构成、本次退款回收情况、以及"缺哪些数据源"。契约强制:取不到的指标用 null + notes 说明,不许用 0 冒充 |
status |
商店侧调用失败时为 FAILED(不会写成 SUCCESS + 一堆 0) |
POST /admin/reconcile/run 同时会拉取商店已作废交易并回收权益与金币(退款/拒付回收),
回收是幂等的:同一笔作废交易重复出现在时间窗里,只回收一次。
口径细节:Google 一次性商品的作废是整笔,拿不到部分金额,因此按整单回收;
找不到本地订单的作废交易会计入 diffCount 并在 notes 里点名。
2.7 创作者与内容审核(已实现)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/creators/page /admin/creators/{id} /{id}/documents |
creator:list |
创作者检索、详情、资质文件 |
| PUT | /admin/creators/{id}/audit |
creator:audit |
入驻审核:APPROVE / REJECT(必填原因+原因码)/ FREEZE |
| POST | /admin/creators |
creator:audit |
手工录入创作者主体(C 端申请入驻未实现前的打通用口) |
| POST | /admin/creators/{id}/account |
creator:account |
开通创作者账号(幂等) |
| POST | /admin/creators/{id}/account/reset-password |
creator:account |
重置创作者账号密码(一次性明文返回) |
| GET | /admin/submissions/page /admin/submissions/{id} |
submission:list |
内容提交检索与详情 |
| PUT | /admin/submissions/{id}/audit |
submission:audit |
审核通过后按 type 落到短剧状态机(CREATE/UPDATE 留档、PUBLISH 上线、OFFLINE 下线) |
| GET · PUT | /admin/prices |
price:update |
分集价格:整剧 freeEpisodes/unlockCoin + 逐集覆盖;改价立即影响播放与下载判定 |
定时发布:DramaPublishJob 每分钟扫一次,把 publish_at 到点的 approved 短剧推上线,
走的是短剧状态机(不直接改状态字段)。上下架也可通过内容提交的 OFFLINE 审核触发。
2.8 数据看板(已实现,只读)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/dashboard/overview |
dashboard:view |
总览:用户 / 内容 / 播放 / 收入 / 成本 |
| GET | /admin/dashboard/transcode |
dashboard:view |
转码队列积压与失败任务明细(含 errorMsg / retryCount) |
口径(与代码注释一致,前端 tooltip 可直接复用)
| 分组 | 指标 | 口径 |
|---|---|---|
| 用户 | total / newToday / new7d |
app_user 未删除总数;按 created_at 落在当天 / 近 7 天 |
| 用户 | active7d |
last_login_at 在近 7 天内。不是 DAU,日活需要逐日登录快照,当前无产出 |
| 用户 | banned / limited / deactivated |
按 status 计数 |
| 内容 | drama* |
短剧按 status 计数(reviewing 为待审、published 为已上线) |
| 内容 | episodeTotal / Published / WithMedia |
剧集总数 / 已上线数 / video_url 非空数(均排除逻辑删除) |
| 内容 | commentPending / reportPending |
待审评论、待处理举报 |
| 播放 | viewTotal / playTotal |
短剧表累计计数列求和(累计口径,不限时间) |
| 播放 | watchedDramas / finishedDramas / finishRate |
watch_progress 行数 / 其中 finished=1 / 完播率 |
| 播放 | playEvents7d / playEventsTotal |
watch_history 流水口径(与累计口径不可相加) |
| 收入 | order* / paidAmount |
订单按状态计数;金额为已支付+已交付合计(商店未接入前恒为 0) |
| 收入 | coinRecharged / coinConsumed / coinBalance |
取自 coin_ledger(RECHARGE / UNLOCK)与 wallet 余额合计 |
| 收入 | entitlementActive |
有效权益数 |
| 成本 | transcode* / renditions |
transcode_task 按状态计数、media_rendition 产物数 |
| 成本 | otpSent7d |
近 7 天验证码发送条数(app_user_otp),作为短信成本近似 |
| 成本 | cdnTrafficBytes |
暂无数据源,返回 null 并在 notes 说明 |
取不到数据源的指标一律返回 null 并在 notes 里写明缺什么,不用 0 冒充。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/comments/page |
评论审核队列 |
| PUT | /admin/comments/{id}/audit |
通过/驳回(带原因码) |
| GET | /admin/reports/page |
举报工单 |
| PUT | /admin/reports/{id}/handle |
处理举报(可联动封禁) |
| GET/POST/PUT | /admin/sensitive-words |
敏感词库 |
| GET | /admin/orders/page |
订单检索(可按用户、状态、平台) |
| POST | /admin/orders/{orderNo}/compensate |
票据丢失的补发 |
| PUT | /admin/entitlements/{id}/revoke |
权益回收 |
| POST | /admin/reconcile/run |
手动触发对账 |
| GET | /admin/creators/page |
创作者审核队列 |
| PUT | /admin/creators/{id}/audit |
入驻审核 |
| GET | /admin/submissions/page |
内容提交审核 |
| PUT | /admin/submissions/{id}/audit |
内容审核(通过/驳回/驳回原因码) |
| PUT | /admin/prices |
分集价格与免费集配置 |
| GET | /admin/dashboard/overview |
经营大盘(用户/播放/收入/成本) |
| GET | /admin/dashboard/transcode |
转码队列与失败任务 |
2.9 后台前端页面(已实现)
ReelShortWeb(Vue3 + Element Plus,dev server :3005)的五个运营页面,统一沿用「api 封装 + 视图 + 样式」的写法;
表格列与下面这些接口的字段一一对应,不在前端编字段,时间列统一显示 yyyy-MM-dd HH:mm:ss。
| 页面 | 路由 | 菜单组件路径(sys_menu.component) |
依赖权限点 | 覆盖接口 |
|---|---|---|---|---|
| 内容治理 | /governance/content |
views/governance/ContentGovernanceView |
comment:list comment:audit report:list report:handle sensitive-word:list sensitive-word:update |
评论检索与审核、举报工单处理、敏感词库增删改查 |
| 订单与权益 | /commerce/orders |
views/commerce/OrderManagementView |
order:list order:update entitlement:list entitlement:update wallet:update reconcile:run |
订单检索 / 流水 / 补发 / 退款、权益检索 / 人工补发 / 回收、金币流水 / 人工补币、对账运行与历史 |
| 创作者入驻 | /creator/creators |
views/creator/CreatorAuditView |
creator:list creator:audit |
创作者检索、资质查看、入驻审核(通过 / 驳回 / 冻结) |
| 内容审核 | /creator/submissions |
views/creator/SubmissionAuditView |
submission:list submission:audit |
内容提交检索、通过 / 驳回(带原因码) |
| 数据看板 | /board/overview |
views/dashboard/BusinessDashboardView |
dashboard:view |
overview + transcode(纯只读) |
菜单、权限点与超管授权由 sql/data-phase1.sql 幂等落库(内容治理 / 订单与权益 / 数据看板挂菜单 id 20–25,创作者两条为 17–19)。
页面只读态与可操作态按权限点区分:没有 *:update / *:audit 的角色进入页面时按钮隐藏或显示「只读」。
2.10 创作者中心(批 1 ~ 批 3 已实现:工作台 / 内容编辑 / 上传挂载 / 收费配置)
创作者 Web 端做在后台管理里(ReelShortWeb),身份复用 sys_admin_user:
creator.admin_user_id 绑定后台账号(部分唯一索引,0 = 未绑定),角色 creator 只拿 creator-center:* 权限点。
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/creator-center/profile |
creator-center:view |
当前创作者主体与入驻状态 |
| GET | /admin/creator-center/dramas/page |
creator-center:view |
我的作品(含最近一次提交的审核进度:类型 / 状态 / 驳回原因码 / 时间) |
| GET | /admin/creator-center/dramas/{id} |
creator-center:view |
我的作品详情 |
| GET | /admin/creator-center/notices |
creator-center:view |
我的通知:入驻审核结果 + 我提交内容的审核结果 |
契约红线:以上接口一律不接收 creatorId,归属由登录态(sys_admin_user.id → creator.admin_user_id)解析,
查询强制带 where creator_id = 当前创作者;访问他人作品统一返回 {"code":403,"message":"无权访问该作品"},
未绑定创作者主体返回 {"code":403,"message":"当前账号未绑定创作者主体,无法进入创作者中心"}。
通知不新建表:第一阶段由 creator(入驻审核)与 content_submission(内容提交审核)派生;
系统公告 ops_notice 目前没有产出方,等有公告功能再接。
内容编辑与提交(批 2)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| POST | /admin/creator-center/dramas |
creator-center:drama:edit |
新建草稿(归属写自己,状态固定 draft) |
| PUT | /admin/creator-center/dramas/{id} |
creator-center:drama:edit |
编辑作品信息(未传的字段沿用现值) |
| GET | /admin/creator-center/dramas/{id}/episodes |
creator-center:view |
分集列表 |
| POST | /admin/creator-center/dramas/{id}/episodes |
creator-center:drama:edit |
新增分集(不传集号则排到最大集号之后,默认免费) |
| PUT | /admin/creator-center/dramas/{id}/episodes/{episodeId} |
creator-center:drama:edit |
编辑分集(时长 / 封面 / 片源 / 排序;未传字段沿用现值) |
| DELETE | /admin/creator-center/dramas/{id}/episodes/{episodeId} |
creator-center:drama:edit |
删除分集 |
| PUT | /admin/creator-center/dramas/{id}/episodes/sort |
creator-center:drama:edit |
批量排序(列表里出现不属于该剧的分集直接 403) |
| POST | /admin/creator-center/dramas/{id}/submit |
creator-center:drama:edit |
提交审核,type = CREATE / UPDATE / PUBLISH / OFFLINE |
| POST | /admin/creator-center/submissions/{id}/withdraw |
creator-center:drama:edit |
撤回自己的提交(撤回后作品退回草稿) |
| GET | /admin/creator-center/dramas/{id}/submissions |
creator-center:view |
我的提交历史(审核进度) |
可编辑状态闸:只有 draft(草稿)与 offline(已下线)能直接改内容;reviewing 提示"提交正在审核中,请先撤回再改",
approved 不可编辑,published 提示"已上线的作品只读,请先提交「申请下线」"(错误码统一 200003)。
提交矩阵(状态 → 允许的提交类型):
| 作品状态 | 允许提交 | 提交后 |
|---|---|---|
draft |
CREATE(仅首次)/ UPDATE / PUBLISH |
作品转 reviewing |
offline |
UPDATE / PUBLISH |
作品转 reviewing |
reviewing |
PUBLISH(内容审核通过后再申请上线) |
保持 reviewing,等后台上线审核 |
approved |
PUBLISH |
保持 approved,等后台上线审核 |
published |
OFFLINE |
保持 published,等后台下线审核 |
同一部剧同时只能有一条在审提交;提交后按其类型落到短剧状态机(审核动作仍在后台,创作者侧不新造状态)。
后台审核 PUBLISH 通过时,该剧仍处于 draft 的分集一并上线(否则会出现"剧上线了却一集都播不了");
后台驳回或创作者撤回时,处于 reviewing 的剧退回 draft,避免创作者卡在只读状态。
上传挂载与收费配置(批 3)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| POST | /admin/creator-center/dramas/{id}/episodes/{episodeId}/attach |
creator-center:upload |
把已上传的媒资挂到自己的分集(写 video_asset_id / video_url,时长为 0 时用产物时长补齐) |
| GET | /admin/creator-center/dramas/{id}/prices |
creator-center:view |
读整剧免费集数 + 逐集免费 / 收费 / 试看秒数 |
| PUT | /admin/creator-center/dramas/{id}/prices |
creator-center:price:update |
保存收费与试看秒数,复用后台同一套 PriceService |
上传走已有的 /admin/media/upload/{init,chunk,complete}(创作者角色直接持 cover:upload / episode:upload,
不新造一套上传接口);挂载时三道校验:分集属于自己这部剧 → 作品可编辑 → 产物是本次登录的人自己上传的
(media_asset.create_by 必须等于当前账号,否则 {"code":403,"message":"无权使用该上传产物"})。
只接受 bizType=EPISODE 且状态为 ready / uploaded 的产物。
收费配置:previewSeconds 落在 drama_episode.preview_seconds(0 = 不限制),
与 is_free / unlock_coin 一起就是播放与下载付费判定读的字段,改完立即生效(实测:未登录请求付费集返回
{"accessType":"COIN_REQUIRED","unlockCoin":50,"previewSeconds":15};免费集直接返回签名播放地址)。
定价沿用「可编辑状态」口径:draft / offline 可改,已上线要改价需先申请下线(避免创作者悄悄改已上线内容的价格);
dramaId 以路径为准,body 里传别人的剧 id 无效,列表里带别人的 episodeId 会被忽略(只作用于自己剧的分集)。
创作数据看板(批 4,只读)
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/creator-center/dashboard |
creator-center:stat:view |
播放 / 完播 / 收藏 / 评论 / 解锁收入概览 |
口径(与代码注释一致,前端 tooltip 可直接复用;统计范围只含 drama.creator_id = 当前创作者)
| 分组 | 指标 | 口径 |
|---|---|---|
| 作品 | total / draft / reviewing / approved / published / offline |
我的短剧按 status 计数(当前快照) |
| 作品 | episodeTotal / episodeWithMedia |
我的分集总数 / video_url 非空数 |
| 播放 | viewTotal / playTotal |
短剧表累计计数列求和(累计口径,不限时间) |
| 播放 | watchedDramas / finishedDramas / finishRate |
watch_progress 是按「用户 × 剧」存的续看进度(唯一索引 user_id + drama_id),所以是有多少人看过我的剧 / 其中最近一次标记完播的 / 完播率;不是按集 |
| 播放 | playEvents7d / playEventsTotal |
watch_history 流水口径(近 7 天 / 全部),与累计口径不可相加 |
| 播放 | finishEvents |
watch_history 中 position_ms >= duration_ms 的完播事件数(流水口径) |
| 互动 | favoriteTotal |
user_favorite 指向我作品的行数 |
| 互动 | commentTotal / commentPending |
content_comment 中属于我作品的评论数 / 其中待审数 |
| 收入 | unlockCount |
entitlement 中 source=ORDER 且剧属于我的记录数(人工补发的 ADMIN 权益不计) |
| 收入 | coinIncome / coinIncome7d |
coin_ledger 中 biz_type=UNLOCK 且 biz_id 命中我分集的扣币绝对值合计(全部 / 近 7 天) |
| 收入 | paidOrderAmount |
暂无数据源,恒为 null:金币解锁不产生法币订单,真实金额要等 Apple/Google 商店接入 |
| 趋势 | daily |
暂无数据源,恒为 null:creator_stat_daily 表已建但无产出任务 |
取不到的一律 null 并在 notes 说明缺什么、怎么补,不用 0 冒充(与运营大盘同口径)。
创作者中心前端页面(批 5 已实现)
ReelShortWeb 里新增四个页面(菜单 id 26–30,sql/data-phase1.sql 幂等落库),沿用 AppUsersView 的
「api 封装 + composable + 视图 + 样式」写法与 console.css:
| 页面 | 路由 | 菜单组件路径 | 依赖权限点 | 覆盖接口 |
|---|---|---|---|---|
| 我的作品 | /creator-center/workspace |
views/creator-center/WorkspaceView |
creator-center:view creator-center:drama:edit |
主体信息、作品列表(含审核进度)、通知、新建草稿、提交 / 撤回 |
| 作品编辑 | /creator-center/dramas/:id |
views/creator-center/DramaEditView |
creator-center:drama:edit creator-center:upload |
作品信息、分集增删改与排序、分片上传(含断点续传)、产物挂载、提交记录 |
| 收费配置 | /creator-center/prices |
views/creator-center/PriceConfigView |
creator-center:price:update |
免费集数、解锁金币、试看秒数 |
| 数据看板 | /creator-center/dashboard |
views/creator-center/DashboardView |
creator-center:stat:view |
看板概览(只读,缺口显示「暂无数据源」) |
创作者角色只挂这五条菜单(含一条 visible=0 的隐藏路由):登录后侧边栏只有「创作者中心」,
看不到运营后台的任何菜单 —— 实测创作者菜单 JSON 里只有 /creator-center 及其子项,超管才拿得到全部。
「作品编辑」是详情页,用 visible=0 下发(后端把它翻译成 meta.hidden),路由照常注册、侧边栏不显示。
本阶段进度:07 全部 5 批完成,第一阶段 05 / 06 / 07 / 08 四块闭环。
超管代看(只读)
后台账号如果没绑定创作者主体,又点进了创作者中心,接口不再直接 403 白屏,而是三级解析:
| 情况 | 结果 |
|---|---|
账号绑定了 creator.admin_user_id |
用自己的身份(创作者角色永远走这条,代看逻辑不影响他) |
未绑定 + 有 creator-center:impersonate + 请求带 X-Creator-Id |
只读代看该创作者 |
| 其余 | 200004(未绑定)/ 200005(有代看权限但没选目标)—— 前端据此渲染引导页 |
红线:
- 目标创作者走独立请求头
X-Creator-Id,不做查询参数 —— creator-center 接口依旧不接收creatorId; - 这个头只有持有
creator-center:impersonate的账号才生效,创作者自己带上没有任何作用(实测:带头上限仍只看到自己的数据,仍能正常编辑自己的作品); - 代看只读:作品编辑、分集增删改与排序、上传会话(
/admin/media/upload/*的 init/chunk/complete)与产物挂载、 提交 / 撤回、收费配置全部拒绝,错误码200006; - 代看进入写
audit_log(biz_type=CREATOR、action=IMPERSONATE_VIEW、操作人、IP、目标创作者), 同一操作人对同一目标 10 分钟内去重,避免翻页把审计表刷爆。
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/creator-center/impersonation/targets |
creator-center:impersonate |
代看目标下拉的数据源(id / 名称 / 状态) |
2.11 商品(金币包)配置(已实现)
背景:
product表 16 个字段早就建好,但后台一直没有入口,运营只能改库;而 C 端/app/orders/verify依赖store_product_id对齐商店商品(对不上就是200002 该商品未在后台配置)。本节补齐 CRUD。 不改表结构。
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/products/page |
product:list |
分页检索(type / status / platform / keyword,分页沿用 {total,list}) |
| GET | /admin/products/{id} |
product:list |
商品详情 |
| POST | /admin/products |
product:update |
新建 |
| PUT | /admin/products/{id} |
product:update |
编辑 |
| PUT | /admin/products/{id}/status |
product:update |
上架 / 下架(status:1 上架、0 下架) |
| DELETE | /admin/products/{id} |
product:update |
删除(逻辑删除) |
字段(与表一致,响应里额外带 statusText / typeText,前端不再写死中文):
| 字段 | 说明 |
|---|---|
code |
商品编码,全局唯一 |
name |
展示名 |
type |
COIN_PACK(金币包)/ VIP(会员,第二阶段) |
platform |
ALL / GOOGLE / APPLE |
storeProductId |
商店商品 id,同一 platform 下唯一(空串表示未绑定,允许多个) |
coinAmount / bonusCoin |
金币数与赠送(type=COIN_PACK 时 coinAmount > 0) |
priceAmount / currency / displayPrice |
价格与展示价 |
regionCodes |
生效区域,逗号分隔,空=全区域 |
vipDays |
会员天数(type=VIP 时 > 0) |
sort / status |
排序 / 上下架 |
effectiveAt / expireAt |
生效与失效时间(可空) |
id以字符串下发:见 §0「ID 字段一律以字符串下发」(本节不再单独规定)。
校验与错误码:
| 场景 | 结果 |
|---|---|
code 已存在 |
200001 商品编码已存在:{code} |
storeProductId 在同 platform 下已存在 |
200001 该平台下商店商品 id 已被占用:{id} |
type=COIN_PACK 但 coinAmount <= 0 |
400 金币包的金币数量必须大于 0 |
type=VIP 但 vipDays <= 0 |
400 会员商品的会员天数必须大于 0 |
type=VIP 且要启用(status=1) |
400 会员商品在订阅功能上线前不能启用(可先建档,上线后再上下架) |
| 商品不存在 | 200002 商品不存在 |
VIP 口径(2026-10-16 定,与 B1-1「VIP 订阅本轮不做」一致):允许创建、不允许启用。 运营可以先录好商品与商店 product id,等 B2 上线后直接上下架;启用才是会误导客户端的动作,拦在这里。
2.12 系统公告管理(已实现)
背景:
ops_notice表 0 行、契约原先写着"没有产出方";E1 已交付 C 端GET /app/notices(匿名只读)。 本节补齐后台写入。不改表结构。
| 方法 | 路径 | 权限点 | 说明 |
|---|---|---|---|
| GET | /admin/notices/page |
notice:list |
分页检索(status / target / type / keyword) |
| GET | /admin/notices/{id} |
notice:list |
公告详情 |
| POST | /admin/notices |
notice:update |
新建 |
| PUT | /admin/notices/{id} |
notice:update |
编辑 |
| PUT | /admin/notices/{id}/status |
notice:update |
发布(status=1)/ 下架(status=0) |
| DELETE | /admin/notices/{id} |
notice:update |
删除(逻辑删除) |
字段:title / content / target(ALL / USER / CREATOR)/ type(SYSTEM / AUDIT / PAYMENT / ACTIVITY)/
status / publishAt / expireAt;响应另带 statusText / targetText / typeText。
id以字符串下发:见 §0「ID 字段一律以字符串下发」。
与 C 端口径严格对齐(同一条查询实现):C 端 GET /app/notices 只返回
status=1 且 target ∈ {ALL, USER}、publish_at 为空或 <= now、expire_at 为空或未过期的公告,
按 publish_at desc, id desc 排序。后台发布即 C 端可见;下架或过期后 C 端不可见。
(实现上收敛在 OpsNoticeService.pageForApp(...) 一处,避免两端条件漂移。)
筛选与错误码:title 必填(1–128);content 最长 2000;公告不存在 → 200002 公告不存在;
status 只接受 0/1,其他值 → 400。
3. 契约与实现状态对照
最后更新:2026-10-16(
develop分支)。未完成项汇总见仓库根README.md「尚未完成」。
| 模块 | 表结构 | 接口 | 代码 |
|---|---|---|---|
| 01 基础与交付准备 | 已就绪 | 约定已冻结 | 脚手架 / 契约 / 签名 / 转码骨架 已实现;S0 雪花主键、S1 Swagger 在线文档 已完成 |
| 02 用户账号与登录 | 已建 | 已冻结 | 已实现(手机号/邮箱,第三方登录走同一凭证表) |
| 03 内容发现与详情 | 已建 | 已冻结 | 已实现(补了分级、区域、版权、标签字段) |
| 04 播放器与播放控制 | 已建 | 已冻结 | 已实现:票据 + 签名 + 进度;多码率 HLS 由转码 worker(C1)产出 |
| 05 记录、收藏与下载 | 已建 | 已冻结 | 已实现:进度、观看流水、收藏、离线下载授权与任务 |
| 06 评论、点赞与分享 | 已建 | 已冻结 | 已实现:评论(游标翻页/审核/删除)、点赞、举报、敏感词;分享埋点待 C3 |
| 07 创作者中心 | 已建 | 已冻结 | 已实现(在后台管理 Web 端):创作者工作台(我的作品 / 审核进度 / 通知)、内容编辑与分集管理、分片上传与断点续传、提交审核与上下架、分集收费与试看、数据看板;Web 端页面见 2.10,不含提现 / 结算。App 客户端不提供创作者中心(口径 2026-10-16) |
| 08 平台运营后台 | 已建 | 已冻结 | 已实现:后台接口(用户管理、内容治理、订单与权益、创作者与内容审核、数据看板)与后台页面(内容治理 / 订单与权益 / 创作者审核 / 数据看板,见 2.9)全部落地 |
| 09 分集收费与支付 | 已建 | 已冻结 | Google Play 票据校验闭环已实现(校验→建单→发权益、幂等与防重放、恢复购买、退款拒付回收、对账);Apple IAP 只留端口未实现(印尼 iOS 占比低,口径为"预留") |
| 10 质量、数据与上线 | 已建 | 已冻结 | 转码 worker(C1)已实现;Redis 缓存与限流(C2)、埋点(C3)、压测 未做 |