短剧统一 API 对接文档
- 文档版本:v1.1
- 更新日期:2026-08-10
- 服务域名:
https://api.xueyuanpie.com - 接口前缀:
/app/playlet - 请求方式:全部为
POST - 请求格式:
application/json; charset=UTF-8 - 响应格式:
application/json; charset=UTF-8 - 鉴权方式:当前 5 个接口均标记
@TokenIgnore,暂不需要 Token - 字符编码:UTF-8
503 和明确的 JSON 提示;这表示 API 后端未部署,不是请求参数错误。1. 快速开始
1.1 最小请求示例
curl -X POST 'https://api.xueyuanpie.com/app/playlet/search' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d '{"platform":0,"keyword":"重生","page":1,"size":20}'1.2 基本调用链路
选择平台
↓
获取分类 categories(可选)
↓
分页列表 page / 搜索 search
↓
详情与剧集 detail
↓
若 episodes[].playUrl 为空,则调用 play
↓
播放 url;失败时依次尝试 backupUrls1.3 客户端必须遵守的三条规则
sourceId、episodeId必须按 字符串 保存和传输。- 调用详情和播放时,必须沿用列表结果对应的同一个
platform。 - 播放地址可能带时效签名,应在即将播放时获取,不要永久缓存。
2. 平台编码
请求中的 platform 使用整数编码;响应中短剧条目的 platform 使用枚举字符串。
| 请求值 | 平台 | 响应枚举名 | 分类能力 |
|---|---|---|---|
0 | 红果短剧 | HONGGUO | 上游可能无独立分类,允许返回空数组 |
1 | 七猫短剧 | QIMAO | 支持分类 |
2 | 河马短剧 | HEMA | 支持分类 |
2.1 ID 精度说明
上游 ID 可能超过 JavaScript 安全整数上限 9007199254740991。即使某个平台当前返回较短的数字,也必须统一视为字符串。
正确:
{
"sourceId": "7594403964322335768",
"episodeId": "615427457"
}错误:
{
"sourceId": 7594403964322335768,
"episodeId": 615427457
}Java/Kotlin 使用 String;TypeScript 使用 string;Swift 使用 String,不要使用 Long、Double 或 JavaScript number 承载这些字段。
3. 请求约定
3.1 HTTP 头
Content-Type: application/json; charset=UTF-8
Accept: application/json当前不需要 Authorization。如果生产环境后续启用鉴权,应由服务端另行约定 Header,不建议客户端预写固定 Token。
3.2 分页参数归一化
Controller 会对分页参数做以下处理:
| 参数情况 | 实际处理 |
|---|---|
不传 page | 使用 1 |
page < 1 | 按 1 处理 |
不传 size | 使用 20 |
size < 1 | 按 1 处理 |
size > 50 | 按 50 处理 |
客户端仍应主动传入合法值,不要依赖服务端修正。
3.3 空值兼容
上游平台字段完整度不同,以下字段都可能为 null、空字符串或空数组:
- 封面、简介、评分、播放量、完结状态;
- 总集数、剧集封面、剧集时长、清晰度;
- 详情中的直接播放地址;
- 备用播放地址。
UI 应提供默认封面、默认文案和空状态,不应因单个可选字段为空而导致页面崩溃。
4. 通用响应结构
接口使用宿主 Cool 项目的统一 R<T> 结构,成功业务数据位于 data。
{
"code": 1000,
"message": "success",
"data": {}
}| 字段 | 类型 | 必有 | 说明 |
|---|---|---|---|
code | integer | 是 | 业务状态码;示例成功值为 1000 |
message | string | 否 | 提示信息;部分宿主版本可能序列化为 msg |
msg | string | 否 | 兼容字段,与 message 二选一读取 |
data | object/array/null | 是 | 业务数据;失败时通常为 null |
R<T> 序列化为准。客户端应集中封装,不要在每个页面重复判断。4.1 推荐判断顺序
- 检查是否发生 DNS、TLS、连接或超时错误;
- 检查 HTTP 状态是否为
2xx; - 尝试解析 JSON;
- 根据
code判断业务是否成功; - 成功后读取
data; - 失败文案优先读取
message,为空再读取msg; - 未知错误显示通用提示,并记录请求路径、HTTP 状态、业务码和 trace 信息,禁止记录播放签名或敏感配置。
4.2 后端未部署响应
当公网 Nginx 可用、但 127.0.0.1:18082 没有短剧服务监听时,返回:
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Cache-Control: no-store{
"code": 503,
"message": "短剧 API 后端尚未部署,请稍后重试",
"data": null
}此响应不能按业务成功处理。客户端可展示“服务暂不可用”,不应无限重试。
4.3 HTTP 状态处理建议
| HTTP 状态 | 含义 | 客户端建议 |
|---|---|---|
200 | 请求已到达业务服务 | 继续判断业务 code |
400 | JSON 或请求参数不合法 | 不自动重试,检查请求体 |
404 | 路由不存在 | 检查 URL、接口前缀和版本 |
429 | 请求过于频繁 | 按 Retry-After 或指数退避重试 |
500 | 服务内部异常 | 提示稍后重试并上报日志 |
502 | 网关连接上游失败 | 短暂退避后有限重试 |
503 | 服务未部署或暂不可用 | 展示服务不可用,避免高频重试 |
504 | 网关等待服务超时 | 查询接口可有限重试,播放接口谨慎重试 |
5. 数据模型与字段字典
5.1 CategoryTab(分类分组)
分类接口使用宿主项目已有的 CategoryTabVo。典型结构如下:
{
"category": "题材",
"items": [
{
"id": "1273",
"name": "都市",
"current": false
}
]
}由于该模型来自宿主模块,不同版本可能存在字段命名差异。客户端初次联调时应以实际 JSON 为准。分类项的 id 必须按字符串保存,并原样作为 page.categoryId 传回。
5.2 PlayletItem(短剧条目)
{
"platform": "QIMAO",
"sourceId": "41000288558",
"title": "短剧名称",
"cover": "https://example.com/cover.jpg",
"introduction": "剧情简介",
"episodeCount": 80,
"score": "9.3",
"playCount": "1200万",
"tags": ["都市", "重生"],
"finishStatus": "已完结"
}| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
platform | string | 是 | 平台枚举:HONGGUO / QIMAO / HEMA |
sourceId | string | 是 | 上游短剧 ID;详情和播放链路的关键参数 |
title | string | 是 | 短剧名称 |
cover | string | 是 | 封面 URL;客户端需准备占位图 |
introduction | string | 是 | 剧情简介 |
episodeCount | integer | 是 | 总集数,未知时可能为空 |
score | string | 是 | 上游评分原文,不保证可转数字 |
playCount | string | 是 | 上游播放量或热度原文,如“1200万” |
tags | string[] | 否 | 题材标签;默认空数组 |
finishStatus | string | 是 | 完结状态原文,如“已完结”“连载中” |
5.3 Episode(剧集)
{
"sourceEpisodeId": "615427457",
"episodeNumber": 1,
"title": "第1集",
"cover": "https://example.com/episode-1.jpg",
"durationSeconds": 125,
"playable": true,
"playUrl": null,
"quality": "720P"
}| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
sourceEpisodeId | string | 是 | 上游剧集 ID;作为 play.episodeId 传入 |
episodeNumber | integer | 是 | 剧集序号;详情结果默认升序 |
title | string | 是 | 剧集标题 |
cover | string | 是 | 剧集封面 URL |
durationSeconds | integer | 是 | 时长,单位秒 |
playable | boolean | 否 | 是否具备播放条件,默认 true |
playUrl | string | 是 | 详情直接提供的地址;为空时调用 /play |
quality | string | 是 | 上游清晰度标识,如 720P |
playable=false 时,客户端应禁用播放按钮或提示该集暂不可播,不要强行调用播放器。5.4 PlayletDetail(详情与剧集)
{
"detail": {
"platform": "HEMA",
"sourceId": "41000288558",
"title": "短剧名称",
"cover": "https://example.com/cover.jpg",
"introduction": "剧情简介",
"episodeCount": 80,
"score": null,
"playCount": null,
"tags": ["都市"],
"finishStatus": "已完结"
},
"episodes": []
}| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
detail | PlayletItem | 是 | 平台未返回有效详情时可能为 null |
episodes | Episode[] | 否 | 剧集列表,默认空数组并按集数升序 |
5.5 PlayletPlay(播放信息)
{
"sourceEpisodeId": "615427457",
"url": "https://example.com/video.m3u8?sign=...",
"quality": "720P",
"format": "m3u8",
"backupUrls": [
"https://backup.example.com/video.m3u8?sign=..."
]
}| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
sourceEpisodeId | string | 是 | 对应请求中的剧集 ID |
url | string | 是 | 首选播放地址 |
quality | string | 是 | 清晰度标识 |
format | string | 是 | 封装格式,如 mp4、m3u8 |
backupUrls | string[] | 否 | 备用地址,默认空数组 |
url 为空不代表 HTTP 请求失败,但客户端无法开始播放,应提示“暂未获取到播放地址”,并允许用户稍后手动重试。
5.6 PageResult(分页结果)
{
"list": [],
"pagination": {
"page": 1,
"size": 20,
"total": 99999
}
}| 字段 | 类型 | 说明 |
|---|---|---|
list | PlayletItem[] | 当前页数据 |
pagination.page | integer | 当前页码 |
pagination.size | integer | 每页条数 |
pagination.total | integer | 总记录数;可能为未知总数占位值 99999 |
部分上游不返回精确总记录数,服务端会使用 99999。因此客户端不要使用 page * size >= total 作为唯一的结束条件。
推荐结束判断:
本页 list 为空 → 已到底
本页 list.size < 请求 size → 通常已到底
本页 list.size == 请求 size → 可以继续请求下一页如上游最后一页刚好等于 size,客户端可能多请求一次空页,这是可接受的。
6. 接口总览
| 接口 | 路径 | 主要用途 | 返回 data |
|---|---|---|---|
| 获取分类 | /app/playlet/categories | 获取平台分类筛选项 | CategoryTab[] |
| 分页列表 | /app/playlet/page | 推荐或分类短剧列表 | PageResult<PlayletItem> |
| 搜索短剧 | /app/playlet/search | 按关键词搜索 | PageResult<PlayletItem> |
| 详情剧集 | /app/playlet/detail | 获取短剧信息和剧集 | PlayletDetail |
| 播放信息 | /app/playlet/play | 获取指定剧集播放地址 | PlayletPlay |
7. 接口详情
7.1 获取平台分类
POST /app/playlet/categories请求参数
| 字段 | 类型 | 必填 | 允许值 | 说明 |
|---|---|---|---|---|
platform | integer | 是 | 0 / 1 / 2 | 平台编码 |
请求示例
{
"platform": 1
}完整成功响应示例
{
"code": 1000,
"message": "success",
"data": [
{
"category": "题材",
"items": [
{
"id": "1273",
"name": "都市",
"current": false
},
{
"id": "1274",
"name": "重生",
"current": false
}
]
}
]
}空分类响应
红果或上游未提供独立分类时,正常返回空数组,不属于错误:
{
"code": 1000,
"message": "success",
"data": []
}cURL
curl -X POST 'https://api.xueyuanpie.com/app/playlet/categories' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d '{"platform":1}'客户端处理
- 分类为空时隐藏分类栏,直接调用分页列表;
- 分类 ID 原样以字符串传入
page.categoryId; - 切换平台时清空旧平台的分类选择和分页状态。
7.2 分页获取短剧列表
POST /app/playlet/page请求参数
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
platform | integer | 是 | - | 平台编码 |
categoryId | string | 否 | null | 分类接口返回的 ID;红果可不传 |
page | integer | 否 | 1 | 页码,最小 1 |
size | integer | 否 | 20 | 每页条数,范围 1~50 |
请求示例
{
"platform": 1,
"categoryId": "1273",
"page": 1,
"size": 20
}不使用分类时:
{
"platform": 0,
"page": 1,
"size": 20
}完整成功响应示例
{
"code": 1000,
"message": "success",
"data": {
"list": [
{
"platform": "QIMAO",
"sourceId": "41000288558",
"title": "重生后我走向人生巅峰",
"cover": "https://example.com/cover.jpg",
"introduction": "一段示例剧情简介",
"episodeCount": 80,
"score": "9.3",
"playCount": "1200万",
"tags": ["都市", "重生"],
"finishStatus": "已完结"
}
],
"pagination": {
"page": 1,
"size": 20,
"total": 99999
}
}
}cURL
curl -X POST 'https://api.xueyuanpie.com/app/playlet/page' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d '{"platform":1,"categoryId":"1273","page":1,"size":20}'客户端处理
- 首次进入、切换平台或切换分类时把
page重置为1; - 刷新操作替换列表,加载更多操作追加列表;
- 可用
platform + sourceId作为跨平台唯一键; - 不要只用
sourceId去重,因为不同平台的命名空间可能重合; - 使用当前页数据量判断是否还能加载,不能完全依赖
total。
7.3 搜索短剧
POST /app/playlet/search请求参数
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
platform | integer | 是 | - | 平台编码 |
keyword | string | 是 | - | 搜索词,去除首尾空白后不得为空 |
page | integer | 否 | 1 | 页码,最小 1 |
size | integer | 否 | 20 | 每页条数,范围 1~50 |
请求示例
{
"platform": 0,
"keyword": "重生",
"page": 1,
"size": 20
}完整成功响应示例
{
"code": 1000,
"message": "success",
"data": {
"list": [
{
"platform": "HONGGUO",
"sourceId": "7594403964322335768",
"title": "重生之示例短剧",
"cover": "https://example.com/hongguo-cover.jpg",
"introduction": "搜索结果示例",
"episodeCount": 60,
"score": null,
"playCount": "热播",
"tags": ["重生"],
"finishStatus": "已完结"
}
],
"pagination": {
"page": 1,
"size": 20,
"total": 99999
}
}
}空结果响应
{
"code": 1000,
"message": "success",
"data": {
"list": [],
"pagination": {
"page": 1,
"size": 20,
"total": 99999
}
}
}参数错误示意
业务错误的具体 code 由宿主项目决定,提示内容如下:
{
"code": 400,
"message": "搜索关键词不能为空",
"data": null
}cURL
curl -X POST 'https://api.xueyuanpie.com/app/playlet/search' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d '{"platform":0,"keyword":"重生","page":1,"size":20}'客户端处理
- 输入为空时在客户端直接拦截;
- 建议输入停止
300~500ms后再发起搜索; - 新关键词搜索前取消旧请求,防止旧响应覆盖新结果;
- 搜索历史应只保存关键词,不保存短时播放 URL;
- 切换平台后重新搜索,并清空旧平台结果。
7.4 获取详情和剧集列表
POST /app/playlet/detail请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
platform | integer | 是 | 必须与列表结果来源平台一致 |
sourceId | string | 是 | 列表或搜索结果返回的短剧 ID |
请求示例
{
"platform": 2,
"sourceId": "41000288558"
}完整成功响应示例
{
"code": 1000,
"message": "success",
"data": {
"detail": {
"platform": "HEMA",
"sourceId": "41000288558",
"title": "示例短剧",
"cover": "https://example.com/cover.jpg",
"introduction": "剧情简介",
"episodeCount": 2,
"score": "9.0",
"playCount": "100万",
"tags": ["都市"],
"finishStatus": "已完结"
},
"episodes": [
{
"sourceEpisodeId": "615427457",
"episodeNumber": 1,
"title": "第1集",
"cover": "https://example.com/episode-1.jpg",
"durationSeconds": 125,
"playable": true,
"playUrl": null,
"quality": "720P"
},
{
"sourceEpisodeId": "615427458",
"episodeNumber": 2,
"title": "第2集",
"cover": null,
"durationSeconds": 132,
"playable": true,
"playUrl": "https://example.com/direct-video.m3u8?sign=...",
"quality": "720P"
}
]
}
}cURL
curl -X POST 'https://api.xueyuanpie.com/app/playlet/detail' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d '{"platform":2,"sourceId":"41000288558"}'客户端处理
episodes默认按episodeNumber升序,但客户端仍可做防御性排序;playable=false的剧集不发起播放;playUrl非空时可直接播放,失败后再调用/play刷新地址;playUrl为空时,把sourceEpisodeId作为/play的episodeId;detail=null但episodes非空时仍可展示基础剧集页;episodes=[]时展示“暂无可用剧集”,不要访问数组第一项。
7.5 获取播放信息
POST /app/playlet/play请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
platform | integer | 是 | 与详情请求保持一致 |
sourceId | string | 是 | 短剧 ID |
episodeId | string | 是 | episodes[].sourceEpisodeId |
请求示例
{
"platform": 2,
"sourceId": "41000288558",
"episodeId": "615427457"
}完整成功响应示例
{
"code": 1000,
"message": "success",
"data": {
"sourceEpisodeId": "615427457",
"url": "https://example.com/video.m3u8?sign=...",
"quality": "720P",
"format": "m3u8",
"backupUrls": [
"https://backup-1.example.com/video.m3u8?sign=...",
"https://backup-2.example.com/video.m3u8?sign=..."
]
}
}无可用地址示例
{
"code": 1000,
"message": "success",
"data": {
"sourceEpisodeId": "615427457",
"url": null,
"quality": null,
"format": null,
"backupUrls": []
}
}cURL
curl -X POST 'https://api.xueyuanpie.com/app/playlet/play' \
-H 'Content-Type: application/json; charset=UTF-8' \
-d '{"platform":2,"sourceId":"41000288558","episodeId":"615427457"}'推荐播放策略
- 用户点击某集后检查
playable; - 若详情中的
playUrl非空,先尝试该地址; - 否则调用
/play; - 优先使用返回的
url; - 主地址发生网络错误、403、404 或媒体解析失败时,按顺序尝试
backupUrls; - 全部失败后允许用户点击“重新加载”,重新调用
/play获取新签名; - 切换剧集时取消上一集尚未完成的播放地址请求。
播放器注意事项
m3u8通常使用 HLS 播放;Android 推荐 Media3/ExoPlayer,iOS 推荐 AVPlayer;- 不要只根据 URL 后缀判断格式,优先参考
format,必要时结合响应 Content-Type; - 某些 CDN 可能校验 User-Agent、Referer 或时效签名,遇到
403应先重新获取地址; - 不要把完整带签名播放 URL 写入公开日志、埋点参数或崩溃报告;
- 不建议后台预取大量剧集播放地址,避免签名过期和上游压力。
8. 参数校验与典型错误
Controller 当前具备以下校验:
| 场景 | 提示文案 |
|---|---|
未传 platform | 短剧平台不能为空 |
platform 不是 0/1/2 | 不支持的短剧平台: {code} |
| 搜索词为空 | 搜索关键词不能为空 |
详情未传 sourceId | 短剧ID不能为空 |
播放未传 sourceId | 短剧ID不能为空 |
播放未传 episodeId | 剧集ID不能为空 |
业务错误码的具体数字由 Cool 宿主的异常处理器决定,因此客户端不要硬编码“某段文案必然对应某个数字”。
8.1 重试原则
| 请求 | 是否建议自动重试 | 建议 |
|---|---|---|
| 分类、列表、搜索、详情 | 有条件 | 只对连接失败、超时、502/504 重试 1~2 次 |
| 播放信息 | 谨慎 | 最多重试 1 次;用户操作优先 |
| 400、业务参数错误 | 否 | 修正请求参数后再发起 |
| 404 | 否 | 检查路径或部署版本 |
| 429 | 是 | 遵循服务端等待时间并指数退避 |
| 503 | 不立即重试 | 提示服务暂不可用,稍后由用户重试 |
推荐退避:第一次等待约 500ms,第二次约 1500ms,加入少量随机抖动。不要无限循环。
9. Android / Kotlin 完整示例
以下示例使用 Retrofit、OkHttp 和 Gson 风格模型。若项目使用 Moshi 或 Kotlinx Serialization,可保持字段类型不变。
9.1 请求模型
data class PlatformRequest(
val platform: Int
)
data class PageRequest(
val platform: Int,
val categoryId: String? = null,
val page: Int = 1,
val size: Int = 20
)
data class SearchRequest(
val platform: Int,
val keyword: String,
val page: Int = 1,
val size: Int = 20
)
data class DetailRequest(
val platform: Int,
val sourceId: String
)
data class PlayRequest(
val platform: Int,
val sourceId: String,
val episodeId: String
)9.2 响应模型
data class ApiResponse<T>(
val code: Int,
val message: String? = null,
val msg: String? = null,
val data: T? = null
) {
fun displayMessage(): String = message ?: msg ?: "请求失败"
}
data class Pagination(
val page: Long = 1,
val size: Long = 20,
val total: Long = 0
)
data class PageResult<T>(
val list: List<T> = emptyList(),
val pagination: Pagination = Pagination()
)
data class PlayletItem(
val platform: String? = null,
val sourceId: String? = null,
val title: String? = null,
val cover: String? = null,
val introduction: String? = null,
val episodeCount: Int? = null,
val score: String? = null,
val playCount: String? = null,
val tags: List<String> = emptyList(),
val finishStatus: String? = null
)
data class Episode(
val sourceEpisodeId: String? = null,
val episodeNumber: Int? = null,
val title: String? = null,
val cover: String? = null,
val durationSeconds: Long? = null,
val playable: Boolean = true,
val playUrl: String? = null,
val quality: String? = null
)
data class PlayletDetail(
val detail: PlayletItem? = null,
val episodes: List<Episode> = emptyList()
)
data class PlayletPlay(
val sourceEpisodeId: String? = null,
val url: String? = null,
val quality: String? = null,
val format: String? = null,
val backupUrls: List<String> = emptyList()
)9.3 Retrofit 接口
interface PlayletApi {
@POST("app/playlet/categories")
suspend fun categories(
@Body body: PlatformRequest
): ApiResponse<List<Map<String, Any?>>>
@POST("app/playlet/page")
suspend fun page(
@Body body: PageRequest
): ApiResponse<PageResult<PlayletItem>>
@POST("app/playlet/search")
suspend fun search(
@Body body: SearchRequest
): ApiResponse<PageResult<PlayletItem>>
@POST("app/playlet/detail")
suspend fun detail(
@Body body: DetailRequest
): ApiResponse<PlayletDetail>
@POST("app/playlet/play")
suspend fun play(
@Body body: PlayRequest
): ApiResponse<PlayletPlay>
}9.4 Retrofit 初始化
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
val api = Retrofit.Builder()
.baseUrl("https://api.xueyuanpie.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PlayletApi::class.java)9.5 详情到播放示例
suspend fun resolveEpisodeUrl(
api: PlayletApi,
platform: Int,
sourceId: String,
episode: Episode
): List<String> {
if (!episode.playable) return emptyList()
// 详情已直接给出地址时先使用它。
episode.playUrl?.takeIf { it.isNotBlank() }?.let {
return listOf(it)
}
val episodeId = episode.sourceEpisodeId
?: return emptyList()
val response = api.play(
PlayRequest(platform, sourceId, episodeId)
)
if (response.code != 1000) {
throw IllegalStateException(response.displayMessage())
}
val play = response.data ?: return emptyList()
return buildList {
play.url?.takeIf { it.isNotBlank() }?.let(::add)
addAll(play.backupUrls.filter { it.isNotBlank() })
}.distinct()
}1000,应把成功判断集中配置,而不是散落在业务代码中。10. Java / OkHttp 示例
MediaType JSON = MediaType.get("application/json; charset=utf-8");
String bodyJson = "{\"platform\":2,\"sourceId\":\"41000288558\"}";
Request request = new Request.Builder()
.url("https://api.xueyuanpie.com/app/playlet/detail")
.header("Accept", "application/json")
.post(RequestBody.create(bodyJson, JSON))
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
String responseText = response.body() == null
? ""
: response.body().string();
if (!response.isSuccessful()) {
throw new IOException(
"HTTP " + response.code() + ": " + responseText
);
}
// 使用 Gson/Jackson 解析 ApiResponse<PlayletDetail>。
// 先判断 code,再读取 data;提示兼容 message 和 msg。
}11. TypeScript / H5 示例
type ApiResponse<T> = {
code: number;
message?: string;
msg?: string;
data: T | null;
};
type DetailRequest = {
platform: 0 | 1 | 2;
sourceId: string; // 必须是 string
};
async function postPlaylet<T>(path: string, body: unknown): Promise<T> {
const response = await fetch(`https://api.xueyuanpie.com${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF-8",
"Accept": "application/json"
},
body: JSON.stringify(body)
});
const result = await response.json() as ApiResponse<T>;
if (!response.ok) {
throw new Error(result.message ?? result.msg ?? `HTTP ${response.status}`);
}
if (result.code !== 1000 || result.data == null) {
throw new Error(result.message ?? result.msg ?? "业务请求失败");
}
return result.data;
}
const detail = await postPlaylet(
"/app/playlet/detail",
{ platform: 2, sourceId: "41000288558" } satisfies DetailRequest
);禁止写成 sourceId: 41000288558,否则对于长 ID 可能在 JSON 发送前就已经发生精度损失。
12. 缓存、并发与性能建议
12.1 可缓存内容
| 内容 | 建议缓存时间 | 说明 |
|---|---|---|
| 平台分类 | 10~30 分钟 | 切换平台时按平台分别缓存 |
| 推荐/分类列表 | 1~5 分钟 | 下拉刷新可绕过缓存 |
| 搜索结果 | 可不缓存 | 可仅保留当前会话 |
| 短剧详情 | 1~5 分钟 | 连载短剧集数可能变化 |
| 播放地址 | 不持久缓存 | 可能存在短时签名 |
| 图片 | 遵循 CDN Header | 使用常规图片缓存库 |
12.2 并发控制
- 同一页面只保留最新一次搜索请求;
- 切换平台、分类或关键词时取消旧分页请求;
- 防止重复触发“加载更多”;
- 播放按钮快速连点时,同一集只保留一个
/play请求; - 不要在进入详情页时并发请求全部剧集的播放地址。
12.3 日志脱敏
允许记录:接口路径、平台编码、分页参数、耗时、HTTP 状态、业务码。
谨慎记录:sourceId、episodeId,可仅保留末 4 位。
禁止记录:上游密钥、Cookie、设备会话参数、完整带签名播放 URL。
13. APP 页面状态建议
每个列表页至少区分以下状态:
- 首次加载:骨架屏或加载动画;
- 有数据:正常展示列表;
- 搜索为空:显示“未找到相关短剧”;
- 分类为空:隐藏分类控件,不作为错误;
- 网络错误:显示重试按钮;
- 服务不可用:针对
503显示“服务暂不可用,请稍后再试”; - 加载更多结束:本页为空或少于
size时停止; - 播放地址为空:显示“暂时无法播放”,提供手动重试。
14. 联调检查清单
14.1 基础网络
- □
https://api.xueyuanpie.comTLS 证书校验通过; - □请求使用 HTTPS,不允许回退 HTTP;
- □Header 为
Content-Type: application/json; charset=UTF-8; - □中文关键词发送和响应均无乱码;
- □能区分 HTTP 错误与业务
code错误。
14.2 三个平台
对 platform=0/1/2 分别验证:
- □分类接口返回数组,允许为空;
- □列表接口至少能解析一页或正常空数组;
- □搜索接口可正确传递中文关键词;
- □详情返回
detail和episodes; - □可选择一集并获得播放地址;
- □响应枚举名分别能解析
HONGGUO、QIMAO、HEMA。
14.3 边界条件
- □
page=0时客户端不会崩溃; - □
size=100时能接受服务端按 50 返回; - □空关键词在客户端被拦截;
- □无效平台能展示服务端错误;
- □
sourceId超过 16 位仍保持原字符串; - □
detail=null、episodes=[]、playUrl=null均可正常展示; - □主播放地址失败时会尝试备用地址;
- □
503时不会无限自动重试。
14.4 上线前
- □实际短剧后端已监听
127.0.0.1:18082; - □公网 5 个接口返回真实业务 JSON,不再返回未部署
503; - □成功业务码和
message/msg字段已按实际宿主确认; - □已配置生产鉴权、限流及日志脱敏;
- □播放资源的使用方式符合上游授权和内容合规要求;
- □证书自动续签已验证。
15. 常见问题 FAQ
Q1:为什么分类接口返回空数组?
部分平台,尤其红果,上游可能没有独立分类接口。空数组是正常成功结果,直接请求分页列表即可。
Q2:为什么 total 总是 99999?
这代表上游没有提供准确总数。它是占位值,不代表真的有 99999 条。请根据每页返回数量判断是否继续加载。
Q3:详情里已有 playUrl,还需要调用 /play 吗?
不需要。可以先直接播放;如果为空、过期或播放失败,再调用 /play 获取最新地址。
Q4:为什么同一个剧集播放地址过一段时间失效?
上游 CDN 地址可能带时间戳或签名。不要永久缓存,应在用户准备播放时重新获取。
Q5:能否把三个平台的结果混合在一个列表?
可以,但每条记录必须保留平台信息,并以 platform + sourceId 作为唯一标识。调用详情和播放时必须使用该条目的原平台。
Q6:HTTP 200 是否一定成功?
不一定。HTTP 200 只代表请求到达业务服务,还需要检查响应 code。
Q7:HTTP 503 是否是参数错误?
不是。当前部署中,503 通常表示短剧宿主服务尚未启动或暂不可用。参数错误应由业务响应提示。
Q8:接口是否支持 GET 或表单提交?
不支持。5 个接口统一使用 JSON POST。
16. 版本与兼容性说明
- 新增可选字段时,客户端应忽略未知 JSON 字段;
- 可空字段未来可能补全,客户端模型不应限制为非空;
- 请求平台整数编码在 v1 内保持不变;
- 响应平台枚举名固定为
HONGGUO、QIMAO、HEMA; - 若未来增加鉴权、API 版本前缀或新的成功码,应通过文档版本和发布通知同步;
- 当前示例域名为正式 HTTPS 域名,但后端部署完成前接口可能返回明确的未部署 503。
17. 联系与问题反馈建议
联调反馈至少提供以下信息,便于快速定位:
发生时间(含时区):
接口路径:
platform:
HTTP 状态:
业务 code:
提示信息:
请求耗时:
是否可稳定复现:
脱敏后的 sourceId / episodeId:请勿在群聊、工单或截图中公开上游密钥、Cookie、设备会话、完整签名播放地址。