一同看 Android API 对接文档
版本:2026-04-24 · 后端:https://ytk-api.yitongcs.com
Android Retrofit + OkHttp Bearer Token HLS (ExoPlayer) Fingerprint.com
本文面向 Android 客户端开发,覆盖一同看所有对外 HTTP 接口的请求格式、响应契约、鉴权规则、错误处理、接入示例。服务端实现在 Cloudflare Workers,文档以"服务端真实行为"为准。配套:项目文档(架构总览)。
1. 通用约定
1.1 Base URL
| 环境 | Base URL |
|---|---|
| 生产 | https://ytk-api.yitongcs.com |
| 预览(CF Pages) | https://ytk-ang.pages.dev(前端)+ 同一 API |
所有路径以 /api/ 开头;少量 /admin/* 是运维 / 机器接口(客户端不用)。
1.2 请求头
| Header | 必需 | 说明 |
|---|---|---|
Content-Type: application/json; charset=utf-8 | 写请求必需 | 所有 POST/PATCH/PUT 都用 JSON |
Authorization: Bearer <b64(userId:token)> | 需登录的接口必需 | token 从登录/注册响应拿;注销前本地持久化 |
User-Agent | 建议 | 默认 Python/Java UA 会被 CF 以 1010 拦掉;用 ytk-android/<versionName> |
X-Forwarded-For | 禁止 | CF 自动从连接拿 IP,客户端别加 |
1.3 Bearer Token 构造
登录 / 注册成功后,响应 data.token 是 32 字符 hex(内部称 md5 列)。构造 Authorization:
val raw = "${userId}:${token}"
val b64 = Base64.encodeToString(raw.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
val header = "Bearer $b64"
Base64.encodeToString 默认会加换行符,务必用 Base64.NO_WRAP。
服务端用 atob(auth.slice(7)) 解出后取冒号后半段作为 token,再读 KV user:{token} 拿完整会话。
1.4 统一响应格式
所有 /api/* 响应都是:
{
"code": "200",
"message": "ok",
"msg": "ok",
"data": { ... }
}
code是字符串(非 number),"200"= 成功message与msg字段内容一致(历史遗留,兼容 PHP 老客户端)- HTTP status 多数情况下和
code同步,但业务错有时返回 HTTP 200 + code="201"/"400"(鉴权错、参数错、已存在等)。不要只信 HTTP status,一定要读code
1.5 HTTP / 业务 code 对照
| HTTP | 典型 code | 场景 |
|---|---|---|
| 200 | "200" | 成功 |
| 200 | "201" | 业务错(登录失败、滑块错等),message 里有详情 |
| 400 | "400" | 参数不合法 |
| 401 | "401" | 未登录 / token 失效 |
| 403 | "403" | 权限不足(admin 端点被 IP 白名单拦) |
| 404 | "404" | 资源不存在 |
| 405 | "405" | 方法不允许 |
| 422 | "422" | 内容被拒(评论 / 昵称 / 简介 AI 审核,data.reason 带原因) |
| 500 | "500" | 服务端内部错误 |
| 504 | — | 子请求 6s 超时 |
1.6 分页
请求参数:page=<1-based>、page_size=<N>(多数端点最大 50,history 是 100)
响应:
{
"items": [ ... ],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 137,
"totalPages": 7,
"hasPrev": false,
"hasNext": true
}
}
传参固定用 page_size 和 page(snake_case),响应里读 pagination.*(camelCase)。
1.7 时间戳
全部 Unix 秒(10 位 int),UTC 时区。不要期望带时区或毫秒。
1.8 CORS
生产白名单仅 ytk.yitongcs.com + *.ytk-ang.pages.dev。Android 原生 HTTP 客户端不走 CORS(CORS 是浏览器约束),所以随便用 Base URL 即可。
1.9 限流
Worker 层没有显式 QPS 限流,但间接节流:
ipreg:{ip}— 同 IP 30 天只能注册 1 次拿 trial VIPfp:{fingerprint}— 同设备指纹永久只能拿一次 trial VIPfb:daily:{uid}:{yyyymmdd}— 意见反馈每用户每天 5 条- CF 边缘对异常高频 / 已知 Bot 返回 1010,务必带合理
User-Agent
1.10 重试策略(建议)
| 错误类型 | 是否可重试 |
|---|---|
| 504 / 网络超时 | ✅ 指数退避,最多 2 次 |
| 500 | ⚠️ 谨慎,最多 1 次 |
| 401 | ❌ 不重试,强制走重登流程 |
| 422 | ❌ 不重试,给用户看 data.reason |
code="201" 业务错 | ❌ 不重试 |
2. 启动序列
推荐 App 冷启动 / 前台恢复时按以下顺序打,能并发就并发:
1. GET /api/base/bootstrap (公开) ← 站点名 / 首屏精选 / 导航
2. [登录态] GET /api/member/me (需 token) ← 当前身份 / VIP / shareDomain
3. GET /api/auth/test-domain?deviceId= (仅 CN) ← 反封连通性探测
4. POST /api/auth/report-test-domain ← 回报 pass/fail
2 和 3/4 完全并发;首屏先渲 1 的结果,登录态和分享域名后到。
2.1 首屏数据 GET /api/base/bootstrap
无参数,无鉴权。响应 data 字段:
{
"siteName": "一同看",
"tagline": "最新热门资源",
"imgUrl": "https://pic.yulecdn.com",
"imageUrl": "https://pic.yulecdn.com",
"sections": [
{ "key": "gv", "label": "GV", "path": "/app/library/gv" },
{ "key": "mv", "label": "电影", "path": "/app/library/mv" },
{ "key": "tv", "label": "电视剧","path": "/app/library/tv" },
{ "key": "pic", "label": "图库", "path": "/app/library/pic" }
],
"featured": [
{ "id": 209058, "section": "gv", "title": "…",
"cover": "https://pic.yulecdn.com/…", "year": "",
"has_hd": true, "durationSeconds": 571 }
// 共 18 条
],
"user": { "loggedIn": false, "username": null }
}
featured是 GV 最新 18 条(带时长 + HD 徽章)- 所有返回的
cover可能是相对路径也可能是完整 URL;客户端:cover.startsWith("http") ? cover : imgUrl + cover - 60s edge + 600s KV 缓存,短时间重复请求走缓存
3. 鉴权与 Auth 模块
3.1 滑块挑战 GET/api/auth/slider-challenge
注册前必须先拿一个滑块 token,5 分钟内再拼进注册请求。
{
"code": "200",
"data": {
"token": "uuid:expiresAt:signature",
"expiresAt": 1776957168000
}
}
token=<UUID>:<expiresAtMs>:<b64url(HMAC)>三段expiresAt是毫秒时间戳- 服务端要求 token 最少 1.5s 才能用(防 fetch + 立即 POST),最多 5 分钟
3.2 注册 POST/api/auth/register
请求 body:
{
"username": "testuser01",
"password": "abc12345",
"confirmPassword": "abc12345",
"verificationCode": "",
"sliderToken": "uuid:1776957168000:sig…",
"fingerprint": "<Fingerprint.com visitorId>",
"fxid": 12345
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | ✅ | 3-30 字符,只允许 A-Za-z0-9_- |
password | string | ✅ | 6-32 字符 |
confirmPassword | string | ✅ | 必须等于 password(旧 PHP 字段名 checkpass 也被接受) |
verificationCode | string | ❌ | 已废弃,填空串 |
sliderToken | string | ✅ | 从 3.1 拿,1.5s 后用,5min 内 |
fingerprint | string | ✅(推荐) | Fingerprint.com visitorId;没有会被视为"指纹重复"→ 拿不到 trial VIP |
fxid | number | ❌ | 邀请人 uid,来自 URL ?fxid= 或本地;没有时传 0 或不传 |
响应(成功):
{
"code": "200",
"message": "Registration successful.",
"data": {
"token": "64 hex chars",
"user": {
"id": 54321,
"username": "testuser01",
"vipTime": 1776960768,
"isVIP": true
}
}
}
vipTime是到期秒时间戳;trial VIP 给 1 小时(now + 3600)- 若被反作弊降级,账号仍创建成功,但 message 变成
"Registration completed, but no trial VIP was granted for this device."且vipTime=0 - 客户端拿到 token 后立即持久化到
EncryptedSharedPreferences
响应(失败):业务错都是 HTTP 200 + code="201" + message 里有原因:
{ "code": "201", "message": "Username already exists.", "data": {} }
{ "code": "201", "message": "Please complete the slider verification.",
"data": { "sliderReason": "expired" } }
{ "code": "201", "message": "This account or device is blocked.", "data": {} }
3.3 登录 POST/api/auth/login
{
"username": "testuser01",
"password": "abc12345",
"fingerprint": "<visitorId>"
}
响应同注册 shape。
- 登录成功会把本次 IP 续
ipreg:{ip}30 天锁 - Token 每次登录都会重新生成,旧 token 被 evict → 其他设备顶下线
3.4 登出 POST/api/auth/logout
Body 空;需要 Authorization。服务端清 KV + MySQL md5 作废;客户端也要清本地 token 回登录页。
3.5 推广来源登记 POST/api/auth/referral
Android 一般不用。响应的 Set-Cookie Android HTTP 客户端默认不处理。推荐:直接在 register body 里带 fxid。
3.6 滑块 UI 协议
- 滑块视觉是纯前端;服务端只校验
sliderToken的签名 + 时间窗,不管用户真拖没拖 - Android 不必强制用户滑动,但推荐模拟一个 ≥ 1.5s 的挑战窗口保证 token 过最小年龄
4. 会员资料
全部需要 Authorization。未登录 401 + code="401"。
4.1 我的信息 GET/api/member/me
{
"code": "200",
"data": {
"id": 54321,
"username": "testuser01",
"nickname": "测试昵称",
"headimg": "https://ytk-api.yitongcs.com/cdn/avatars/54321.jpg",
"vipTime": 1776960768,
"isVIP": true,
"email": "xxx@example.com",
"mobile": "",
"shareDomain": "https://gvm1.fun/?fxid=54321",
"shareStats": { "share": 5, "sharePending": 2, "shareRewarded": 3 }
}
}
vipTime总是从 MySQL 现读,确保支付后刷新就能看到shareDomain可能为null,前端降级显示
4.2 资料详情 GET/api/member/profile
响应 data.profile:
{
"id": 54321,
"username": "testuser01",
"nickname": "测试昵称",
"headimg": "https://.../avatars/54321.jpg",
"birthday": 946684800,
"sex": 1,
"intro": "这是个人简介",
"vipTime": 1776960768,
"isVIP": true,
"email": "xxx@example.com",
"mobile": "",
"mobileCountryCode": ""
}
sex: 0=未设置 / 1=男 / 2=女 / 3=其它birthday: Unix 秒;0=未设置
4.3 修改资料 PATCH/api/member/profile
字段全部可选,只传要改的:
{ "nickname": "新昵称", "sex": 1, "birthday": 946684800, "intro": "新简介" }
| 字段 | 约束 |
|---|---|
nickname | 1-20 字符,不含 <>" |
sex | 0 / 1 / 2 / 3 |
birthday | Unix 秒;-2208988800 到 now;0 清空 |
intro | 最多 255 字符 |
响应(审核拒):HTTP 422 + code="422":
{
"code": "422",
"message": "包含脏话或辱骂词汇",
"data": { "reasonCode": "NICKNAME_REJECTED", "reason": "包含脏话或辱骂词汇" }
}
reasonCode 可能是 NICKNAME_REJECTED 或 INTRO_REJECTED。服务端会给该用户发一条 type=system 站内通知。
4.4 改密码 PATCH/api/member/password
{ "oldPassword": "abc12345", "newPassword": "xyz98765" }
约束:密码 8-32 字符,必须同时含字母和数字,不能等于老密码。
成功响应:
{ "code": "200", "data": { "token": "新的 32 char hex" } }
data.token 覆盖本地 token。
错误 code:FIELDS_MISSING / OLD_PASSWORD_WRONG(HTTP 409)/ PASSWORD_WEAK / PASSWORD_SAME_AS_OLD
4.5 发邮箱验证码 POST/api/member/email/send-code
// Request
{ "email": "xxx@example.com" }
// Response
{ "code": "200", "data": { "cooldownSeconds": 60 } }
4.6 验证邮箱 POST/api/member/email/verify
// Request
{ "email": "xxx@example.com", "code": "123456" }
// Response
{ "code": "200", "data": { "verified": true } }
4.7 上传头像 POST/api/member/avatar
请求:Content-Type: multipart/form-data,字段名 file,最大 5 MB,只接 JPEG/PNG/WebP。
{
"code": "200",
"data": { "url": "https://ytk-api.yitongcs.com/cdn/avatars/54321-<ts>.jpg" }
}
- 文件存 R2
USER_ASSETS桶,URL 永久 - 换头像会刷新 URL(文件名带 timestamp)
- 图片库可能缓存老 URL,用 Glide/Coil 的
signature(MediaStoreSignature(...))或附?t=tsbust
4.8 绑定用户中心 POST/api/member/bind-uc
// Request
{ "username": "uc-user", "password": "xxxxx" }
// Response
{ "code": "200", "data": { "ucUserId": 123456 } }
5. VIP / 流量 / 订单
5.1 VIP 状态 GET/api/member/vip
{
"code": "200",
"data": { "vipTime": 1776960768, "isVIP": true, "remainingSeconds": 3561 }
}
5.2 流量 GET/api/member/flow
{
"code": "200",
"data": { "flow": 1073741824, "flowText": "1GB" }
}
flow 单位字节;flowText 服务端格式化好了直接显示。
5.3 创建订单 POST/api/member/order/create
// Request
{ "productId": 1, "payType": "wx", "returnUrl": "ytkapp://pay/return" }
// Response
{
"code": "200",
"data": {
"orderId": "YTK202604241234567",
"payUrl": "https://pay.gvpay.vip/xxx",
"amount": 3900,
"currency": "CNY"
}
}
productId:1=月卡 / 2=季卡 / 3=年卡payType:wx/alipay/usdtamount单位 分payUrl跳 WebView 让用户完成支付;支付成功由老 PHP 回调更新tp_user.vip_time- 客户端轮询
/api/member/vip检测是否到账
6. 邀请与裂变
6.1 邀请战绩 GET/api/member/referrals?page=1&page_size=20
{
"shareDomain": "https://gvm1.fun/?fxid=54321",
"share": 5,
"sharePending": 2,
"shareRewarded": 3,
"items": [
{ "id": 9001, "username": "t***r02",
"time": 1776900000, "good": 1, "is_dood": 1 }
],
"pagination": { "page": 1, "pageSize": 20, "total": 5, ... }
}
shareDomain带fxid=<uid>的完整 URL,客户端直接生二维码 / 转发share=总邀请数;shareRewarded=已结算到 VIP 的份数;sharePending=差值- 列表项
username已做掩码处理 - 2 分钟 KV 缓存
6.2 邀请统计 GET/api/member/invite/stats
只返回三个计数,比 /referrals 轻量。
6.3 邀请列表 GET/api/member/invite/list
只返回列表,不带 shareDomain。
7. 内容浏览
不需要鉴权(除了 /play 和 /download)。
7.1 GV 列表 GET/api/content/gv
| 参数 | 取值 | 说明 |
|---|---|---|
page | >=1 | 默认 1 |
page_size | 1..100 | 默认 30 |
type | all / china / korea / japan / southeast / europe | 默认 all |
sort | latest / favorites / random | 默认 latest |
seed | 1 .. 2^31-1 | 仅 sort=random 用,同 seed 同排列,翻页稳定 |
响应 data:
{
"section": "gv",
"title": "GV Library",
"type": "all",
"filters": [
{ "value": "all", "label": "全部", "count": 89786 },
{ "value": "china", "label": "中国", "count": 12340 }
// ...
],
"items": [
{
"id": 209011, "section": "gv", "title": "…",
"cover": "https://pic.yulecdn.com/…",
"year": "", "has_hd": true,
"durationSeconds": 1213, "viewCount": 5243
}
],
"pagination": { ... }
}
缓存:sort=latest|favorites 有 60s edge + 600s KV 缓存;sort=random 跳过 edge cache,但同 seed 仍稳定。
7.2 GV 详情 GET/api/content/gv/:id
{
"id": 109114, "section": "gv", "title": "…",
"cover": "https://…", "year": "", "has_hd": true,
"playUrls": {
"p480": "https://ytk-api.yitongcs.com/api/media/…",
"p720": "https://…",
"p1080": "https://…"
},
"durationSeconds": 1516,
"viewCount": 5243,
"legacyPlayerUrl": "/play-109114.html?embed=1"
}
playUrls.p* 是签名 URL,有效期约 5 分钟,获取后立即使用。
7.3 MV / TV / 图集列表
与 GV 列表同参数结构。MV 没有 durationSeconds / has_hd(老数据未存)。
TV 详情附带剧集:
{
"id": 5, "section": "tv", "title": "剧集名",
"episodes": [
{ "id": 101, "name": "第1集", "playUrl": "/data/…/index.m3u8" },
{ "id": 102, "name": "第2集", "playUrl": "…" }
]
}
播放某一集:GET /api/content/tv/:tvId/play?episode_id=101
图集详情带 images: string[] 完整 URL 数组。
7.4 全站搜索 GET/api/content/search?wd=§ion=gv&page=1&page_size=30
{
"keyword": "abc",
"items": [ /* 与该 section item 同形状 */ ],
"pagination": { ... }
}
缓存 3h(10800s KV)。
8. 媒体播放与下载
8.1 GV 播放 GET/api/content/gv/:id/play
需要 VIP。未登录 401;登录但非 VIP 403。
{
"code": "200",
"data": {
"url": "https://ytk-api.yitongcs.com/api/media/…",
"defaultUrl": "https://…",
"qualities": [
{ "label": "流畅", "url": "https://…" },
{ "label": "标清", "url": "https://…" },
{ "label": "高清", "url": "https://…" }
],
"sizeBytes": 104857600,
"viewCount": 5243
}
}
url=qualities[0].url(p480 默认起播)- 每个 URL 签名 ~5 分钟过期;过期后重调
/play - 切码率直接换
qualities[i].url
8.2 MV / TV 播放
同 shape(MV / TV 免 VIP):GET /api/content/mv/:id/play、GET /api/content/tv/:id/play?episode_id=<ep>
8.3 GV 下载 GET/api/content/gv/:id/download
需要 VIP + 流量余额。
{
"code": "200",
"data": {
"url": "https://down.gv.live/xxx?token=...",
"title": "视频文件名",
"format": "MP4",
"durationSeconds": 1516,
"requiredFlow": 524288000,
"requiredFlowText": "500MB",
"remainingFlow": 1073741824,
"remainingFlowText": "1GB",
"alreadyGranted": false
}
}
- 第一次请求会扣
requiredFlow字节 alreadyGranted=true→ 6 小时内同用户 + 同 md5 已扣过(down:{uid}:{md5}KV 21600s)url用后即焚
流量不足:
{
"code": "400",
"message": "Insufficient remaining flow.",
"data": {
"requiredFlow": 524288000, "requiredFlowText": "500MB",
"remainingFlow": 100000, "remainingFlowText": "100KB"
}
}
8.4 媒体回源 /api/media/<signed_path>
这是播放地址本身,客户端不要手动构造。/play 返回的 qualities[].url 已经是带签名的完整 URL,直接交 ExoPlayer。
9. 评论
9.1 列表 GET/api/comments?section=gv&id=<contentId>&page=1&page_size=20
无需鉴权(登录用户多一个 liked 标记)。
{
"items": [
{
"id": 1234, "section": "gv", "content_id": 109114,
"user_id": 54321, "username": "t***r01",
"text": "评论内容", "created_at": 1776900000,
"likes": 5, "parent_id": null, "liked": false,
"replies": [
{ "id": 1235, "parent_id": 1234, "username": "u***r02",
"text": "回复", "created_at": 1776900100, "likes": 1, "liked": false }
]
}
],
"total": 137, "page": 1, "page_size": 20
}
- 只有顶层评论分页;每条顶层的
replies全量返回 liked只有当前用户登录才可能为 true
9.2 发表评论 POST/api/comments
需要鉴权。
{
"section": "gv",
"id": 109114,
"text": "这个视频不错",
"parent_id": null
}
id,不是 contentId(和 favorites/check 不同,历史遗留)。
服务端 sanitize 后入库,前端收到的 data.text 可能短过你发的(去了 <script> 等)。
审核拒响应(HTTP 422):
{
"code": "422",
"message": "content rejected",
"data": { "reasonCode": "rejected", "reason": "包含脏话或辱骂词汇" }
}
其它错 code="400" + data.reasonCode:empty / too_long / invalid_parent / shard_missing
9.3 切换点赞 POST/api/comments/:id/like
{ "code": "200", "data": { "likes": 6, "liked": true } }
9.4 删除评论 DELETE/api/comments/:id
只能删自己的;顶层评论会级联删 replies。
{ "code": "200", "data": { "deleted": true } }
10. 收藏
10.1 列表 GET/api/member/favorites?section=gv&page=1&page_size=20
section 可选。响应 data.items:
[{
"uid": 54321, "section": "gv", "content_id": 109114,
"title": "…", "cover": "https://…", "year": "",
"durationSeconds": 1516, "added_at": 1776800000
}]
10.2 是否已收藏 GET/api/member/favorites/check?section=gv&contentId=109114
{ "code": "200", "data": { "favorited": true } }
contentId 驼峰(和 /api/comments 的 id 不一致,历史遗留)。
10.3 添加 / 取消 POST/api/member/favorites
{
"section": "gv",
"id": 109114,
"action": "add",
"title": "…", // add 时必填
"cover": "https://…", // add 时必填
"year": "",
"durationSeconds": 1516
}
action = "add" 或 "remove"。
10.4 RESTful 删除 DELETE/api/member/favorites/:section/:contentId
语义等于 {action:"remove"}。
11. 观看历史
11.1 上报心跳 POST/api/member/history
{
"section": "gv",
"contentId": 109114,
"title": "视频标题",
"cover": "https://…",
"durationSeconds": 1516, // 可选,服务端能补
"watchedSeconds": 45,
"progress": 3, // 0-100 百分比
"episodeId": 0, // TV 必填对应集数 id
"episodeLabel": "" // TV 可选文案
}
调用时机建议:
- 进入详情页:上报一次初始心跳
- 播放中:每 30 秒一次
- 暂停 / 切集 / 退出:立即再报
{ "code": "200", "data": { "id": "gv-109114-0" } }
id = ${section}-${contentId}-${episodeId} 合成 key,用于后续删除。
11.2 历史列表 GET/api/member/history?section=gv&page=1&page_size=20
{
"items": [
{ "id": "gv-109114-0", "section": "gv", "contentId": 109114,
"episodeId": 0, "episodeLabel": "",
"title": "…", "cover": "https://…",
"progress": 67, "durationSeconds": 1516, "watchedSeconds": 1015,
"watchedAt": 1776900000 }
],
"stats": {
"totalAll": 37,
"sectionCounts": { "gv": 15, "mv": 10, "tv": 10, "pic": 2 },
"todayCount": 5
},
"pagination": { ... }
}
11.3 删除某条 DELETE/api/member/history/:id
:id 用 11.1 返回的合成 key,如 gv-109114-0。
11.4 清空全部 DELETE/api/member/history
Body 空;清除所有 section 全部记录。
11.5 查归档(>90 天) GET/api/member/history/archive?from=&to=&page=&page_size=
from / to Unix 秒,最大区间 366 天。读冷存储 R2,可能慢 1-3 秒,UI 加 loading 提示。
12. 点赞 / 踩
投票 POST/api/content/:section/:id/vote
// Request
{ "vote": 1 } // 1=点赞 / -1=踩 / 0=取消
// Response
{
"code": "200",
"data": { "likes": 120, "dislikes": 5, "userVote": 1 }
}
不需鉴权也能投;登录用户状态持久化更好。
13. 站内信
13.1 列表 GET/api/member/notifications?page=1&page_size=20
{
"items": [
{
"id": 1000000042,
"type": "system",
"title": "昵称未通过审核",
"content": "你提交的昵称内容未通过审核,原因:…",
"meta": { "field": "nickname", "reason": "…" },
"createdAt": 1776960000,
"expiresAt": null,
"read": false,
"isBroadcast": true
}
],
"total": 5, "unread": 3,
"pagination": { ... }
}
type:system/activity/invite_reward/vip_expiry/custommeta是自由结构,按type解读isBroadcast=true:全站广播(userid=0)
13.2 未读计数 GET/api/member/notifications/unread-count
{ "code": "200", "data": { "count": 3 } }
高频(顶部红点),建议客户端 1 分钟刷一次。
13.3 标记已读
- 单条:
POST/api/member/notifications/:id/read - 全部:
POST/api/member/notifications/read-all→{"updated": 3}
14. 意见反馈
14.1 提交 POST/api/member/feedback
{
"content": "反馈内容",
"contact": "微信/邮箱(可选)",
"category": "bug" // bug / suggestion / complaint / other
}
限流:同用户每天最多 5 条(fb:daily:{uid}:{yyyymmdd})。
14.2 我的反馈列表 GET/api/member/feedback/list?page=&page_size=
{
"items": [
{ "id": 42, "content": "…", "category": "bug",
"status": "open", // open / replying / closed
"reply": null, "replied_at": null,
"created_at": 1776900000 }
],
"pagination": { ... }
}
15. 搜索与热词
15.1 热词 GET/api/search/trending?limit=20
15.2 最新搜索 GET/api/search/recent?limit=20
{
"code": "200",
"data": {
"items": [
{ "query": "写真", "count": 22, "last_used": 1776853463 },
{ "query": "帅哥", "count": 22, "last_used": 1776792354 }
]
}
}
trending 按 count DESC;recent 按 last_used DESC。
15.3 记录查询 POST/api/search/log
{ "query": "搜索词" }
不需鉴权,纯匿名统计。用户按下回车 / 点搜索按钮时调。
16. 分享域名检测
反封锁基础设施,大多数客户端不需要实现。
16.1 拿测试域名 GET/api/auth/test-domain?deviceId=<uuid>
非 CN 大陆 + 未识别运营商返回 code="400" + "IP not eligible"。CN + 运营商识别成功返回 {domain: "gvm1.fun"}。
16.2 回报结果 POST/api/auth/report-test-domain
{ "deviceId": "<uuid>", "status": "pass", "domain": "gvm1.fun" }
status: pass / fail / timeout
17. 错误码索引
17.1 业务 code 对照
| code | HTTP | 含义 |
|---|---|---|
"200" | 200 | 成功 |
"201" | 200 | 业务错(登录失败 / 用户名占用 / 滑块错 / 黑名单等) |
"400" | 400 | 参数不合法 / 限流 / 余额不足 |
"401" | 401 | 未登录或 token 失效 |
"403" | 403 | 权限不足 |
"404" | 404 | 资源不存在 |
"405" | 405 | 方法不允许 |
"422" | 422 | 内容被 AI 审核拒(data.reason 带原因) |
"500" | 500 | 服务端内部错误 |
17.2 资料更新 reasonCode
NICKNAME_INVALID— 长度/字符集非法NICKNAME_REJECTED— AI 审核拒INTRO_TOO_LONG— > 255 字符INTRO_REJECTED— AI 审核拒SEX_INVALID/BIRTHDAY_RANGE
17.3 密码修改 code
FIELDS_MISSING/OLD_PASSWORD_WRONG/PASSWORD_WEAK/PASSWORD_SAME_AS_OLD
17.4 评论 reasonCode
empty/too_long/invalid_parent/shard_missing/rejected
18. Android 集成最佳实践
18.1 Retrofit + OkHttp 样板
// build.gradle.kts
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
object YtkApi {
private const val BASE = "https://ytk-api.yitongcs.com/"
private val authInterceptor = Interceptor { chain ->
val req = chain.request()
val token = TokenStore.current // 已 b64
val builder = req.newBuilder()
.header("User-Agent", "ytk-android/${BuildConfig.VERSION_NAME}")
if (token != null) builder.header("Authorization", "Bearer $token")
chain.proceed(builder.build())
}
private val client = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) BODY else NONE
})
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE)
.client(client)
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
))
.build()
}
18.2 统一响应包装
@JsonClass(generateAdapter = true)
data class Envelope<T>(
val code: String, val message: String,
val msg: String, val data: T?
)
fun <T> Envelope<T>.unwrap(): Result<T> = when (code) {
"200" -> if (data != null) Result.success(data)
else Result.failure(ApiError(code, message))
else -> Result.failure(ApiError(code, message))
}
class ApiError(val code: String, msg: String) : RuntimeException(msg)
18.3 Token 存储(EncryptedSharedPreferences)
object TokenStore {
private val prefs by lazy {
val key = MasterKey.Builder(App.ctx)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
App.ctx, "ytk_secure", key,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
var uid: Int
get() = prefs.getInt("uid", 0)
set(v) = prefs.edit().putInt("uid", v).apply()
var rawToken: String?
get() = prefs.getString("token", null)
set(v) = prefs.edit().putString("token", v).apply()
val current: String? get() {
val u = uid; val t = rawToken
if (u <= 0 || t == null) return null
val raw = "$u:$t".toByteArray(Charsets.UTF_8)
return Base64.encodeToString(raw, Base64.NO_WRAP)
}
fun set(uid: Int, token: String) { this.uid = uid; this.rawToken = token }
fun clear() { prefs.edit().clear().apply() }
}
18.4 Fingerprint.com 集成
implementation("com.fingerprint:fingerprint-pro-android:2.x")
val fp = FingerprintAndroidFactory.createInstance(applicationContext,
Configuration(apiKey = "<公开 key,前端可暴露>"))
fp.getVisitorId({ visitorId ->
FxStore.visitorId = visitorId // 注册时带进 body.fingerprint
}, { error ->
FxStore.visitorId = "" // 拿不到指纹 → 按"重复设备"处理
})
18.5 fxid 归因(Deep Link)
<!-- AndroidManifest.xml -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="gvm1.fun" android:pathPattern="/.*" />
<data android:scheme="https" android:host="gvm2.fun" android:pathPattern="/.*" />
<!-- 分享域名可能动态下发 -->
</intent-filter>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val fxid = intent?.data?.getQueryParameter("fxid")?.toIntOrNull() ?: 0
if (fxid > 0) FxStore.fxid = fxid // 5h 有效,注册时取
}
18.6 Token 失效处理
val authGuard = Interceptor { chain ->
val resp = chain.proceed(chain.request())
val peek = resp.peekBody(Long.MAX_VALUE).string()
try {
val json = JSONObject(peek)
if (json.optString("code") == "401") {
TokenStore.clear()
App.eventBus.post(TokenExpired)
}
} catch (_: Throwable) { /* 非 JSON 响应忽略 */ }
resp
}
18.7 图片加载
Glide.with(this)
.load(video.cover)
.placeholder(R.drawable.ph_cover)
.into(binding.imgCover)
18.8 播放器接入(ExoPlayer)
val playResult = YtkApi.retrofit.create(Gv::class.java).play(id).unwrap().getOrThrow()
val mediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory())
.createMediaSource(MediaItem.fromUri(playResult.url))
player.setMediaSource(mediaSource)
player.prepare()
player.playWhenReady = true
/play 拿新 URL 即可。本地 URL 缓存不超过 3 分钟。
18.9 通用错误提示映射
fun ApiError.userMessage(ctx: Context): String = when (code) {
"401" -> ctx.getString(R.string.err_need_login)
"403" -> ctx.getString(R.string.err_no_permission)
"404" -> ctx.getString(R.string.err_not_found)
"422" -> message // AI 审核 reason,直接展示
"500" -> ctx.getString(R.string.err_server)
else -> message.ifBlank { ctx.getString(R.string.err_unknown) }
}
19. 端点速览
| Section | Method | Path | 鉴权 | 说明 |
|---|---|---|---|---|
| Base | GET | /api/base/bootstrap | 🔓 | 站点配置 + 首屏精选 |
| Auth | GET | /api/auth/slider-challenge | 🔓 | 发滑块 token |
| Auth | POST | /api/auth/register | 🔓 | 注册 |
| Auth | POST | /api/auth/login | 🔓 | 登录 |
| Auth | POST | /api/auth/logout | 🔒 | 登出 |
| Auth | POST | /api/auth/referral | 🔓 | 记 fxid cookie(Android 可跳过) |
| Auth | GET | /api/auth/test-domain | 🔓 | 分享域连通性测试 |
| Auth | POST | /api/auth/report-test-domain | 🔓 | 回报 |
| Member | GET | /api/member/me | 🔒 | 身份 + VIP + shareDomain |
| Member | GET/PATCH | /api/member/profile | 🔒 | 查/改资料 |
| Member | PATCH | /api/member/password | 🔒 | 改密码 |
| Member | POST | /api/member/avatar | 🔒 | 上传头像 |
| Member | POST | /api/member/email/send-code | 🔒 | 发邮箱验证码 |
| Member | POST | /api/member/email/verify | 🔒 | 验证邮箱 |
| Member | POST | /api/member/bind-uc | 🔒 | 绑定 UC |
| VIP | GET | /api/member/vip | 🔒 | VIP 详情 |
| VIP | GET | /api/member/flow | 🔒 | 流量 |
| VIP | POST | /api/member/order/create | 🔒 | 下单 |
| Share | GET | /api/member/referrals | 🔒 | 战绩 |
| Share | GET | /api/member/invite/stats | 🔒 | 邀请计数 |
| Share | GET | /api/member/invite/list | 🔒 | 邀请列表 |
| Content | GET | /api/content/gv | 🔓 | GV 列表 |
| Content | GET | /api/content/gv/:id | 🔓 | GV 详情 |
| Content | GET | /api/content/gv/:id/play | 🔒 VIP | GV 播放 URL |
| Content | GET | /api/content/gv/:id/download | 🔒 VIP | GV 下载 URL |
| Content | GET | /api/content/mv|tv|pic[/:id][/play] | 🔓 / 🔒 | 各 section |
| Content | GET | /api/content/search | 🔓 | 全文搜索 |
| Content | POST | /api/content/:section/:id/vote | 🔓 | 点赞 / 踩 |
| Comment | GET | /api/comments | 🔓 | 评论列表 |
| Comment | POST | /api/comments | 🔒 | 发评论(AI 审核) |
| Comment | POST | /api/comments/:id/like | 🔒 | 切换点赞 |
| Comment | DELETE | /api/comments/:id | 🔒 | 删评论 |
| Favorite | GET/POST | /api/member/favorites | 🔒 | 收藏 |
| Favorite | GET | /api/member/favorites/check | 🔒 | 是否已收藏 |
| Favorite | DELETE | /api/member/favorites/:section/:id | 🔒 | 取消收藏 |
| History | GET/POST/DELETE | /api/member/history | 🔒 | 历史 |
| History | DELETE | /api/member/history/:id | 🔒 | 删单条 |
| History | GET | /api/member/history/archive | 🔒 | 冷归档查询 |
| Notify | GET | /api/member/notifications | 🔒 | 站内信列表 |
| Notify | GET | /api/member/notifications/unread-count | 🔒 | 未读计数 |
| Notify | POST | /api/member/notifications/:id/read | 🔒 | 标记已读 |
| Notify | POST | /api/member/notifications/read-all | 🔒 | 全部已读 |
| Feedback | POST | /api/member/feedback | 🔒 | 提交反馈(限流 5/日) |
| Feedback | GET | /api/member/feedback/list | 🔒 | 我的反馈 |
| Search | GET | /api/search/trending | 🔓 | 热词 |
| Search | GET | /api/search/recent | 🔓 | 最新搜索 |
| Search | POST | /api/search/log | 🔓 | 记录查询 |
🔓 公开 · 🔒 需 Bearer Token · 🔒 VIP 需登录且 isVIP=true