spark-server/internal/transport/http/docs/openapi.yaml
DESKTOP-L8OUL9F\gfl0034 753c276496 feat: PopAdmin 拉取管理、签到能量 API 与图片 150KiB 硬上限
管理端按用户查询开启通讯录/相册/媒体拉取;服务端写入路径自动压图;新增 /v1/sign 与 /v1/energy。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 12:44:06 +08:00

4849 lines
167 KiB
YAML
Raw Permalink 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: Spark IM API
version: 0.1.0
description: |
本规范描述 **HTTP REST**。即时消息长连接是另一套 **TCP Frame**(TLS + 23 字节头 + JSON Body),见文末「TCP 协议」。没有 Protobuf。
## 环境与地址
公网只开放 `443`。Nginx 按 TLS SNI 分流:HTTP 证书打到 `sparkapi.wyrs365.com`,IM 证书打到 `sparkim.wyrs365.com`,对象存储(RustFS S3)打到 `sparkr.wyrs365.com`。进程本身只绑本机。
| 环境 | 用途 | HTTP Base URL | IM 长连接 |
| --- | --- | --- | --- |
| 生产 | App / 联调 | `https://sparkapi.wyrs365.com` | `sparkim.wyrs365.com:443`(TLS,SNI=`sparkim.wyrs365.com`) |
| 本机开发 | 直连 Gin / gnet | `http://127.0.0.1:8080` | `127.0.0.1:8090`(明文,无 TLS) |
Swagger 右上角 **Servers** 切换 HTTP 环境;Try it out 打的是 HTTP,不会建 IM 长连接。
文档:
1. HTTP API / Swagger:`/docs`
2. 枚举目录:`/docs/enums`
3. 错误码:`/docs/errors`
---
## 统一响应(HTTP 与 TCP Body 同一套)
所有业务响应 Body 都是:
```json
{"code":"0","msg":"ok","request_id":"","data":{}}
```
| 字段 | 类型 | 规则 |
| --- | --- | --- |
| `code` | **字符串** | `"0"` 成功;其它失败。不要用数字 `0` 比较。全表见 [错误码](/docs/errors)。 |
| `msg` | 字符串 | 给人看的文案,随请求语言变化(`zh-CN` / `en` / `zh-TW`)。成功一般为 `ok`。 |
| `request_id` | 字符串 | 服务端生成,11 位 base62(`0-9A-Za-z`)。客户端不用传;从响应体读取。与 Header `X-Request-ID` 相同。 |
| `data` | object / array | 成功时为业务对象;失败时固定 `{}`。字段缺失当空对象,不要当 `null`。 |
**判定成功只看 `code === "0"`。** 不要只看 HTTP 状态码。
| 场景 | HTTP 状态 | envelope |
| --- | --- | --- |
| 业务成功 / 业务失败(参数、未登录、无权限等) | **200** | `code` 为 `"0"` 或 `"40001"` / `"40101"` / `"40301"` 等 |
| `/readyz` 未就绪 | **503** | `code`=`"50301"`,`data` 仍有探活字段 |
| panic / 未捕获 | 200 | `code`=`"50001"` |
Snowflake(`uid` / `msg_id` / `conversation_id`)是 int64。Android/iOS/Java 用 `long`;JavaScript 不要用 `Number` 接,用字符串或 BigInt。
---
## HTTP 请求:Header / Path / Query / Body
```
METHOD {base}{path}?{query}
Header: 鉴权、语言、设备、追踪
Body: 仅 POST / PATCH 的 JSON(GET / DELETE 无 Body)
```
`Content-Type`:有 Body 时必须是 `application/json`。
### Header(客户端可设)
| Header | 放哪 | 必填 | 说明 |
| --- | --- | --- | --- |
| `Authorization` | Header | 需登录的接口必填 | `Bearer <access_token>`。登录、发码、刷新、`/healthz`、`/readyz`、`/docs`、`/openapi.yaml`、`GET /v1/meta/enums`、以及公开音乐读接口(`/v1/music/playlist`、`/v1/music/playlist/grouped`、`/v1/music/tracks/{id}`、`/v1/music/categories`、`/v1/music/settings`)**不要**带。 |
| `X-Device-ID` | Header | 可选 | 十进制 `device_id`,须等于登录响应 / JWT `did`(**服务端按物理指纹生成**,客户端勿自造)。不一致则 `40101`。 |
| `X-App-ID` | Header | 可选 | 若传,须与 JWT **`aid`(app_id)** 一致。 |
| `X-Language` | Header | 可选 | `zh-CN` / `en` / `zh-TW`。优先于 `Accept-Language`。缺省 `zh-CN`。决定 `msg` 语言。 |
| `Accept-Language` | Header | 可选 | 无 `X-Language` 时使用。 |
| `X-App-Version` | Header | 可选 | 客户端版本,写入请求上下文。 |
| `Content-Type` | Header | 有 Body 时必填 | `application/json`。 |
**不要**把 token、语言、`device_id`、`app_id` 塞进业务 JSON(登录 / 刷新除外:登录 Body 带 `app_id` 与物理指纹字段,刷新 Body 带 `refresh_token`)。
网关写入、客户端不要发:`X-Real-IP`、`X-Forwarded-For`(限流/审计用,见 C6)。
### 响应 Header
| Header | 说明 |
| --- | --- |
| `X-Request-ID` | 与响应体 `request_id` 相同,服务端生成,客户端不用传 |
| `Content-Type` | `application/json; charset=utf-8` |
### Path / Query / Body
| 位置 | 放什么 | 例子 |
| --- | --- | --- |
| Path | 资源 id | `/v1/friends/{uid}` 的 `uid`;`/v1/groups/{id}` 的群号;`/v1/messages/{id}` 的 MsgId |
| Query | GET 筛选 / 分页 | `GET /v1/friends/lookup?q=`;`limit`;历史 `conversation_id`、`last_msg_id` |
| Body | POST/PATCH 的业务字段 | 登录的 `email`/`verify_code`/`app_id`/`device_type`/`idfv` 或 `android_id`;发消息的 `content` |
### 调用顺序
1. (可选)`POST /v1/sms/codes` Header **`X-Language` 必填**(`zh-CN` / `en` / `zh-TW`),Body `{"purpose":1,"target":"邮箱或手机号"}`。封控见 [docs/sms.md](../../../docs/sms.md)(IP 桶 + 同 target 60s 冷却,均 `42902`)。测试号 `test01@spark.com`~`test100@spark.com` 可跳过发码。
2. `POST /v1/auth/login` Body 拿 `access_token` / `refresh_token`。**不要**传客户端自造 `device_id`;传 **`app_id`**、**`device_type`(1–6)** 与 **`idfv` / `android_id`**,由服务端生成 `device_id`。持久化响应里的 `device_id` 与 `app_id`。
3. 其它 HTTP:Header `Authorization: Bearer …`,可选 `X-Device-ID`(= JWT `did`)、`X-App-ID`(= JWT `aid`)、`X-Language`。
4. 续期:`POST /v1/auth/token/refresh`,**Body** `{"refresh_token"}`,不要带 Bearer;新 token 保留相同 `did` / `aid`。
5. App 在线发消息、收推送、增量同步走 **TCP**,不要用 `POST /v1/messages` 当主路径。
请求示例(生产):
```http
POST /v1/auth/login HTTP/1.1
Host: sparkapi.wyrs365.com
Content-Type: application/json
X-Language: zh-CN
X-App-Version: 1.0.0
{"email":"test01@spark.com","verify_code":"000000","app_id":1,"device_type":1,"idfv":"00000000-0000-0000-0000-000000000007","device_name":"iPhone"}
```
```http
GET /v1/users/me HTTP/1.1
Host: sparkapi.wyrs365.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
X-Device-ID: 7
X-App-ID: 1
X-Language: zh-CN
```
```http
POST /v1/messages HTTP/1.1
Host: sparkapi.wyrs365.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json
X-Device-ID: 7
X-Language: zh-CN
{"peer_id":1940000000000000001,"client_msg_id":"c-1","content":{"t":1,"text":{"body":"hi"}}}
```
成功:`{"code":"0","msg":"ok","request_id":"...","data":{...}}`。失败:`{"code":"40101","msg":"未授权","request_id":"...","data":{}}`。
---
## TCP 协议(生产 `sparkim.wyrs365.com:443`)
不是 WebSocket、不是 HTTP、不是 Protobuf。客户端对 `sparkim.wyrs365.com:443` 做 **TLS**(SNI 必须是该域名),握手完成后在 TLS 上写二进制 Frame。本机开发可直连 `127.0.0.1:8090` 明文。
心跳 30s 一次;空闲 120s 服务端断开。Frame 最大 64KB。媒体字节走 HTTP 上传,消息里只带 object key。
### Frame(大端,固定 23 字节头)
```
offset 0 Magic uint16 0x5350(ASCII "SP")
offset 2 Version uint8 1
offset 3 Command uint16
offset 5 Flags uint16 bit0=1 表示 Body 是 gzip
offset 7 RequestID uint64 客户端追踪,可 0;响应原样带回
offset 15 SequenceID uint32 请求/响应配对;PUSH 推送用它做 PUSH_ACK
offset 19 BodyLength uint32 Body 字节数(压缩后)
offset 23 Body bytes UTF-8 JSON;Flags bit0 则先 gzip 再当 Body
```
读包:先读 23 字节,按 `BodyLength` 再读 Body。`Magic` 不对或总长超过 64KB 应断开。
压缩:Body ≥ 512B 且 gzip 更小时可设 Flags bit0。未压缩 Flags=0。解压后仍不得超过 64KB。
### Command
<table class="tcp-cmd">
<thead>
<tr><th>值</th><th>名</th><th>方向</th><th>请求 Body</th><th>响应 Command / Body</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>AUTH</td><td>客户端 → 服务端</td><td><strong>不是 envelope</strong>:<code>{"token","device_id?","app_version","language"}</code>;<code>device_id</code> 可选但须等于 JWT <code>did</code>;<strong>不必</strong>传 <code>app_id</code>(以 JWT <code>aid</code> 为准)</td><td>同 1,envelope,<code>data</code>=<code>{"user_id","device_id"}</code></td></tr>
<tr><td>2</td><td>HEARTBEAT</td><td>客户端 → 服务端</td><td>空 Body</td><td>同 2,envelope <code>data={}</code></td></tr>
<tr><td>3</td><td>GENERATE_MSG_ID</td><td>客户端 → 服务端</td><td>空</td><td>同 3,<code>data.msg_id</code></td></tr>
<tr><td>4</td><td>SEND_MESSAGE</td><td>客户端 → 服务端</td><td><strong>不是 envelope</strong>:与 HTTP <code>POST /v1/messages</code> 同一 JSON</td><td><strong>5 SEND_ACK</strong>,<code>data</code>=<code>{"msg_id","conversation_id","client_msg_id","server_time"}</code></td></tr>
<tr><td>5</td><td>SEND_ACK</td><td>服务端 → 客户端</td><td>—</td><td>仅作 4 的应答</td></tr>
<tr><td>6</td><td>MESSAGE</td><td>服务端 → 客户端</td><td>—</td><td>envelope,<code>data</code> 为消息 Item(<code>msg_id</code> / <code>content</code> / …)</td></tr>
<tr><td>7</td><td>PUSH_ACK</td><td>客户端 → 服务端</td><td>空;<code>SequenceID</code>=推送帧的 SequenceID</td><td>无业务响应</td></tr>
<tr><td>8</td><td>READ</td><td>客户端 → 服务端</td><td><strong>不是 envelope</strong>:<code>conversation_id</code>,以及 <code>msg_id</code> 或 <code>last_msg_id</code>(任一大于 0;两者都大于 0 时用 <code>msg_id</code>)</td><td>同 8,envelope,<code>code</code> 为 <code>"0"</code>,<code>data</code>=<code>{"conversation_id","msg_id"}</code>。<code>data.msg_id</code> 是落库后的 <code>last_read_msg_id</code>(只增不减),并清该成员 Redis 未读</td></tr>
<tr><td>9</td><td>RECALL</td><td>客户端 → 服务端</td><td><code>{"msg_id"}</code></td><td>同 9,envelope</td></tr>
<tr><td>10</td><td>SYNC_INBOX</td><td>客户端 → 服务端</td><td><code>{"mailbox_seq","dialog_score","limit"}</code>,均可 0</td><td>同 10,<code>data.dialogs</code> / <code>mailbox</code> / 游标</td></tr>
<tr><td>12</td><td>SYNC_MESSAGES</td><td>客户端 → 服务端</td><td><code>{"conversation_id","last_msg_id","limit"}</code></td><td>同 12,<code>data.items</code> / <code>has_more</code></td></tr>
<tr><td>14</td><td>KICK</td><td>服务端 → 客户端</td><td>—</td><td>同设备顶号,<code>data.reason</code></td></tr>
<tr><td>15</td><td>DELETE</td><td>客户端 → 服务端</td><td><code>{"msg_id"}</code></td><td>同 15</td></tr>
</tbody>
</table>
除 AUTH 外,业务命令须先 AUTH,否则 `40102`。HEARTBEAT / PUSH_ACK 可以未登录。
**重要:** AUTH、SEND、SYNC、撤回/删除的 **请求 Body 是业务 JSON**,不要包 `{code,msg,request_id,data}`。服务端应答和 MESSAGE 推送的 Body 才是统一 envelope。失败时 Command 仍为请求 Command(发送失败不是 SEND_ACK),`code`≠`"0"`。
发送成功条件(C5):MySQL COMMIT **并且** 入队 `deliver:queue` 后才 SEND_ACK。未收到 ACK 用同一 `client_msg_id` 重试。发送端其它设备会收到 MESSAGE;本设备不回推。
上线增量:先 `SYNC_INBOX`(会话列表/未读 + 撤回删除邮箱),再对脏会话 `SYNC_MESSAGES`(`last_msg_id` 之后)。成功在线 MESSAGE 不进邮箱。
<details>
<summary>Java 示例(点击展开,生产域名)</summary>
```java
import javax.net.ssl.*;
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class SparkImClient {
static final String API = "https://sparkapi.wyrs365.com";
static final String IM_HOST = "sparkim.wyrs365.com";
static final int IM_PORT = 443;
static final int HDR = 23;
public static void main(String[] args) throws Exception {
String login = httpPost(API + "/v1/auth/login",
"{\"email\":\"test01@spark.com\",\"verify_code\":\"000000\",\"app_id\":1,\"device_type\":2,\"android_id\":\"YOUR_ANDROID_ID\"}");
String token = jsonString(login, "access_token"); // 从 data.access_token 取出
SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket s = (SSLSocket) f.createSocket();
SSLParameters p = s.getSSLParameters();
p.setServerNames(List.of(new SNIHostName(IM_HOST)));
s.setSSLParameters(p);
s.connect(new InetSocketAddress(IM_HOST, IM_PORT), 10_000);
s.startHandshake();
DataOutputStream out = new DataOutputStream(s.getOutputStream());
DataInputStream in = new DataInputStream(s.getInputStream());
write(out, 1, 1, "{\"token\":\"" + token + "\",\"device_id\":7,\"app_version\":\"1.0.0\",\"language\":\"zh-CN\"}");
System.out.println("AUTH " + read(in));
write(out, 2, 2, "");
System.out.println("HB " + read(in));
write(out, 4, 3, "{\"peer_id\":1,\"client_msg_id\":\"c1\",\"content\":{\"t\":1,\"text\":{\"body\":\"hi\"}}}");
System.out.println("ACK " + read(in)); // Command=5
}
static void write(DataOutputStream out, int cmd, int seq, String body) throws IOException {
byte[] b = body.getBytes(StandardCharsets.UTF_8);
ByteBuffer buf = ByteBuffer.allocate(HDR + b.length);
buf.putShort((short) 0x5350).put((byte) 1).putShort((short) cmd).putShort((short) 0)
.putLong(0).putInt(seq).putInt(b.length).put(b);
out.write(buf.array());
out.flush();
}
static String read(DataInputStream in) throws IOException {
byte[] h = in.readNBytes(HDR);
ByteBuffer buf = ByteBuffer.wrap(h);
if (buf.getShort() != 0x5350) throw new IOException("magic");
buf.get();
int cmd = buf.getShort() & 0xffff;
int flags = buf.getShort() & 0xffff;
buf.getLong();
int seq = buf.getInt();
int n = buf.getInt();
byte[] body = in.readNBytes(n);
if ((flags & 1) != 0) {
try (java.util.zip.GZIPInputStream gz = new java.util.zip.GZIPInputStream(new ByteArrayInputStream(body))) {
body = gz.readAllBytes();
}
}
return "cmd=" + cmd + " seq=" + seq + " " + new String(body, StandardCharsets.UTF_8);
}
static String httpPost(String url, String json) throws IOException {
HttpURLConnection c = (HttpURLConnection) URI.create(url).toURL().openConnection();
c.setRequestMethod("POST");
c.setDoOutput(true);
c.setRequestProperty("Content-Type", "application/json");
c.setRequestProperty("X-Language", "zh-CN");
try (OutputStream os = c.getOutputStream()) { os.write(json.getBytes(StandardCharsets.UTF_8)); }
try (InputStream is = c.getInputStream()) { return new String(is.readAllBytes(), StandardCharsets.UTF_8); }
}
static String jsonString(String env, String key) {
int i = env.indexOf("\"" + key + "\"");
int a = env.indexOf('"', env.indexOf(':', i) + 1) + 1;
return env.substring(a, env.indexOf('"', a));
}
}
```
</details>
<details>
<summary>iOS (Swift) 示例(点击展开,生产域名)</summary>
```swift
import Foundation
import Network
import Compression
let api = URL(string: "https://sparkapi.wyrs365.com")!
let imHost = NWEndpoint.Host("sparkim.wyrs365.com")
let imPort = NWEndpoint.Port(443)!
struct Envelope: Decodable { let code: String; let msg: String; let request_id: String }
struct LoginEnv: Decodable {
struct Data: Decodable { let access_token: String }
let code: String; let data: Data
}
func login() throws -> String {
var req = URLRequest(url: api.appendingPathComponent("/v1/auth/login"))
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("zh-CN", forHTTPHeaderField: "X-Language")
req.httpBody = """
{"email":"test01@spark.com","verify_code":"000000","app_id":1,"device_type":1,"idfv":"00000000-0000-0000-0000-000000000007","device_name":"iPhone"}
""".data(using: .utf8)
let data = try URLSession.shared.send(req)
return try JSONDecoder().decode(LoginEnv.self, from: data).data.access_token
}
func frame(cmd: UInt16, seq: UInt32, body: Data) -> Data {
var d = Data(capacity: 23 + body.count)
func u16(_ v: UInt16) { d.append(UInt8(v >> 8)); d.append(UInt8(v & 0xff)) }
func u32(_ v: UInt32) {
d.append(UInt8(v >> 24)); d.append(UInt8(v >> 16 & 0xff))
d.append(UInt8(v >> 8 & 0xff)); d.append(UInt8(v & 0xff))
}
u16(0x5350); d.append(1); u16(cmd); u16(0)
d.append(contentsOf: [UInt8](repeating: 0, count: 8)); u32(seq); u32(UInt32(body.count)); d.append(body)
return d
}
// TLS:NWParameters.tls,SNI 随 Host 自动带 sparkim.wyrs365.com
let conn = NWConnection(host: imHost, port: imPort, using: .tls)
conn.start(queue: .main)
// 连接 ready 后:
// let token = try login()
// let auth = #"{"token":"\#(token)","device_id":7,"app_version":"1.0.0","language":"zh-CN"}"#
// conn.send(content: frame(cmd: 1, seq: 1, body: Data(auth.utf8)), completion: .idempotent)
// 收包:先 23 字节,再 BodyLength;Flags & 1 则 gunzip。看 JSON code == "0"。
// HEARTBEAT cmd=2;SEND_MESSAGE cmd=4,应答 cmd=5;推送 cmd=6 后发 PUSH_ACK cmd=7。
```
</details>
本机 TCP 把 Host/Port 换成 `127.0.0.1:8090`,并用 **明文 TCP**(不要 TLS)。HTTP 仍用 `http://127.0.0.1:8080`。
仓库可运行示例:`docs/clients/SparkImClient.java`、`docs/clients/SparkImClient.swift`。
---
## 客户端页面跳转
这一节是当前 iOS 客户端的页面导航,不是新接口。不要据此发明或调用本规范未列出的 HTTP / TCP 命令。底部实际是四个 Tab:消息、广场、匹配、我的。「发现」是广场里的分段,不是第五个底栏。返回用侧滑或返回键,弹窗用关闭,大图和「我的」侧栏盖在当前页上。这是客户端导航,工作区后续改动可能使它过时。
主路径:
```mermaid
flowchart TD
start["启动"] -->|替换根| login["邮箱登录"]
start -->|已登录 替换根| home["主页"]
start -->|封禁中 替换根| ban["违规页"]
login -->|push| code["邮箱验证码"]
login -->|present| agree["用户协议 / 隐私政策"]
login -->|push| fb["意见反馈"]
login -->|同页切换| pwd["密码登录"]
code -->|替换根| home
pwd -->|替换根| home
code -->|返回| login
home -->|tab| msg["消息"]
home -->|tab| square["广场"]
home -->|tab| match["匹配"]
home -->|tab| me["我的"]
msg -->|push| single["单聊"]
msg -->|push| group["群聊"]
msg -->|push| sys["系统消息"]
msg -->|push| contacts["通讯录"]
msg -->|push| add["添加朋友"]
single -->|push| detail["聊天详情"]
single -->|覆盖层| preview["图片与视频大图"]
single -->|push| video["视频播放"]
group -->|push| gset["群设置"]
contacts -->|push| profile["好友资料"]
profile -->|push| single
square -->|分段| follow["关注"]
square -->|分段| discover["发现"]
square -->|push| publish["发布动态"]
follow -->|push| post["动态详情"]
discover -->|push| post
follow -->|push| homePage["广场个人主页"]
discover -->|push| homePage
match -->|资料已齐 tab| card["卡片匹配"]
match -->|资料未齐 tab| fill["资料填写"]
fill -->|push| card
card -->|push| cardInfo["名片详情"]
me -->|覆盖层| drawer["我的侧栏"]
drawer -->|push| setting["设置"]
me -->|push| myProfile["个人资料"]
```
设置与账号:
```mermaid
flowchart TD
setting["设置"] -->|push| account["账号与安全"]
setting -->|push| teen["青少年模式"]
setting -->|push| myProfile["个人资料"]
setting -->|push| notice["消息通知"]
setting -->|push| chatSet["聊天"]
setting -->|push| privacy["隐私"]
setting -->|push| general["通用"]
setting -->|push| help["帮助"]
setting -->|push| feedback["意见反馈"]
setting -->|push| about["关于我们"]
account -->|push| bind["第三方账号绑定"]
account -->|push| real["实名认证"]
account -->|push| resetPwd["设置或重置登录密码"]
account -->|push| secPwd["安全密码"]
account -->|push| cancel["账号注销"]
myProfile -->|present| avatarCrop["头像裁切"]
myProfile -->|push| qr["我的二维码"]
```
图片预览与编辑。编辑含涂鸦、贴图、文字、裁剪、马赛克。头像只有裁切。
```mermaid
flowchart TD
chat["单聊或群聊"] -->|覆盖层| browser["图片与视频大图"]
chat -->|push| player["视频播放"]
chat -->|present| album["相册"]
chat -->|push| albumPreview["相册预览"]
browser -->|present| editor["图片编辑"]
album -->|present| editor
profile["个人资料"] -->|present| cropOnly["头像裁切"]
editor -->|同页| doodle["涂鸦"]
editor -->|同页| sticker["贴图"]
editor -->|覆盖层| text["文字"]
editor -->|同页| clip["裁剪"]
editor -->|同页| mosaic["马赛克"]
```
消息右上角还能进添加朋友、扫一扫、发起群聊和群发助手。系统消息进意见反馈。单聊详情进好友资料、查找记录、投诉和会话锁定。侧栏里「附近的人」和「我的钱包」当前点不到。语音通话在菜单里已被注释。这些同样只是客户端导航。
servers:
- url: https://sparkapi.wyrs365.com
description: 生产(IM / 音乐客户端)
- url: http://127.0.0.1:8080
description: 本机开发
- url: https://popadmin.wyrs365.com
description: popadmin(音乐管理 Admin API)
- url: https://admin.fullstorynovel.com
description: FullStoryNovel 小说 CMS / 公开 API
- url: https://fullstorynovel.com
description: FullStoryNovel 官网(公开 /api 同源时可切换)
security:
- bearerAuth: []
tags:
- name: enums
description: "[打开枚举页](/docs/enums)"
externalDocs:
description: 打开枚举页
url: /docs/enums
- name: errors
description: "[打开错误码页](/docs/errors)"
externalDocs:
description: 打开错误码页
url: /docs/errors
- name: system
- name: meta
description: 客户端枚举 JSON。`GET /v1/meta/enums`,无需登录。
- name: client
description: 客户端配置(协议/反馈/订阅)
- name: auth
- name: users
- name: friends
- name: groups
- name: conversations
- name: messages
- name: files
- name: favorites
- name: moments
- name: square
description: |
多广场:路径 `/v1/{square_channel}/…`,channel=`square|squarenovel|squaresteps|squarehealth`。
分类 Tab:`GET …/categories`;Feed 用 query `category`(推荐/最新/热门/附近/搞笑/美女)。
- name: music
description: |
运营音乐列表。客户端公开读(无需登录,V3 envelope):
`GET /v1/music/playlist`(可带 `categoryId` / `requireMember`)、
`GET /v1/music/playlist/grouped`(分类分区嵌套曲目,对齐欢休 zone 列表)、
`GET /v1/music/tracks/{id}`(单曲详情)、
`GET /v1/music/categories`、`GET /v1/music/settings`。
playlist 返回曲目、`categories` 分类树与 `memberPreviewSeconds`;曲目含 `backgroundUrl`、`requireMember`、`categoryIds`。
- name: sign
description: |
签到 / 补签(需登录)。`GET /v1/sign/calendar` 月历;`POST /v1/sign` 当日签到;
`GET /v1/sign/makeup?date=` 补签资格;`POST /v1/sign/makeup` 执行补签。奖励写入能量钱包。
- name: energy
description: |
能量查询(需登录)。`GET /v1/energy` 余额;`GET /v1/energy/logs` 明细(兼容旧能量账单页)。
- name: admin-music
description: |
popadmin 音乐管理:`/api/admin/spark/music*`(`/api/admin/pop/music*` 别名)。
曲目 CRUD、封面/背景/音频上传、分类 CRUD、曲目分类绑定(多对多)、全局试听秒数。
敏感词:`/api/admin/spark/sensitive*`(`/api/admin/pop/sensitive*` 别名);词库与命中审计、热重载。
Admin JWT;响应 `{success,message,data}`。Host 常用 `https://popadmin.wyrs365.com`。
- name: novels
description: |
FullStoryNovel 公开小说 API(App/官网)。生产基址见 servers 中 fullstorynovel。
响应多为直接 JSON(非 V3 envelope)。
- name: admin-novels
description: |
FullStoryNovel 小说 CMS(8095)。生产 `https://admin.fullstorynovel.com`。
CMS Bearer;或用 wyrs Admin JWT 调 `POST /api/admin/adopt-ops-session` 换取。
popadmin「小说内容管理」iframe 嵌入该后台。
paths:
/healthz:
get:
tags: [system]
security: []
summary: 进程探活
operationId: healthz
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
status: ok
/readyz:
get:
tags: [system]
security: []
summary: 就绪探活(MySQL / Redis / 是否接流量)
description: |
进程存活用 `/healthz`(始终 200)。
`/readyz` 在未接流量、依赖不可用或关停中返回 HTTP 503,`code=50301`。
Nginx / 编排应摘流看本接口,不要看 `/healthz`。
operationId: readyz
responses:
"200":
description: 已就绪
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
status: ok
accepting: true
mysql: ok
redis: ok
taskpool: ok
"503":
description: 未就绪
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "50301"
msg: 服务不可用
data:
status: not_ready
accepting: false
mysql: skipped
redis: skipped
taskpool: ok
/docs:
get:
tags: [system]
security: []
summary: HTTP API 文档页(Swagger UI)
description: |
文档入口:
1. 本页 `/docs` — 接口(Servers 切换环境)
2. `/docs/enums` — 枚举,左侧目录可跳转
3. `/docs/errors` — 错误码(envelope.code)
operationId: docsUI
responses:
"200":
description: HTML
content:
text/html:
schema:
type: string
/docs/enums:
get:
tags: [enums]
security: []
summary: 枚举目录页
description: |
拉取 `GET /v1/meta/enums`,按集合列出 value / name / label。
左侧有序列表点击跳到对应集合(`#enum-{set}`,例如 `/docs/enums#enum-message_type`)。
错误码见 `/docs/errors`。
operationId: docsEnums
responses:
"200":
description: HTML
content:
text/html:
schema:
type: string
/docs/errors:
get:
tags: [errors]
security: []
summary: 错误码页
description: |
envelope.code 全表,与 HTTP/TCP 同一套。`code` 为字符串,`"0"` 成功。
左侧点击跳到对应码(`#code-40101`)。数据来自 `GET /v1/meta/enums` 的 `envelope_code`。
operationId: docsErrors
responses:
"200":
description: HTML
content:
text/html:
schema:
type: string
/openapi.yaml:
get:
tags: [system]
security: []
summary: OpenAPI 规范原文
operationId: openAPISpec
responses:
"200":
description: YAML
content:
application/yaml:
schema:
type: string
/v1/meta/enums:
get:
tags: [meta]
security: []
summary: 类型 / 状态枚举目录
description: |
客户端接入用。无需登录。`label` 随 `X-Language` / `Accept-Language` 变化。
`value` 与 JSON/TCP/SQL 落库值一致(数字集合为 number,字符串集合为 string)。
`name` 是稳定英文标识,不随语言变化。人读目录:`/docs/enums`(类型/状态)、`/docs/errors`(envelope.code)。
operationId: listEnums
responses:
"200":
description: data 为 EnumCatalog
content:
application/json:
schema:
$ref: "#/components/schemas/EnumsEnvelope"
/v1/sms/codes:
post:
tags: [sms]
security: []
summary: 发送验证码
description: |
Body:`purpose` 1=登录 2=注销;`target` 为邮箱或大陆 11 位手机号(服务端识别,无需 channel)。
**Header `X-Language` 必填**(不接受仅 Accept-Language),用于邮件主题/正文。
登录发码无需 Bearer;注销发码需 Bearer,且 `target` 须为当前绑定邮箱(不支持手机号注销发码)。
**封控(失败均为 envelope `42902`,HTTP 仍 200)**
1. **按客户端 IP**(`RealIP`,依赖网关 `X-Real-IP` / PROXY):令牌桶,速率 `rate_limit.auth_per_second`(默认 5/s),与
`POST /v1/auth/login`、`POST /v1/auth/token/refresh` **共用**同一 IP 桶。
2. **按 target**:同一「渠道 + purpose + 规范化 target」**60 秒内**只能成功触发一次发信/发短信;Redis 键 `sms:cd:*`(无 Redis 时为进程内内存)。
不同 purpose、不同邮箱/手机号互不影响。成功响应 `data.expires_in` 固定 **300**(码有效期秒数,与 60s 冷却无关)。
测试邮箱 `test01@spark.com`~`test100@spark.com` 不发信且不占用 target 冷却。详情 [docs/sms.md](../../../docs/sms.md)。
邮箱经 SMTP;手机走阿里云短信(按号段选运营商模板)。
operationId: sendVerificationCode
parameters:
- $ref: "#/components/parameters/X-Language"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SendVerificationCodeRequest"
example:
purpose: 1
target: ada@example.com
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/auth/login:
post:
tags: [auth]
security: []
summary: 邮箱验证码登录并签发 JWT
description: |
首次验证成功会创建用户(Snowflake uid、默认 spark_id、shortid 个人二维码票据,不含 uid)。
登录 UPSERT `user_devices` 与 `devices`,并追加 `user_login_logs`(含 app_id)。
返回 access_token 与 refresh_token;`device_id` 由服务端生成。测试账号任意验证码即可。
`device_type` 必填:整数 1–6(见 meta `device_type`);可选 `platform` 同枚举作别名。
**设备模型(Breaking,见 `docs/devices-client.md`)**:
- Body **不要**再传客户端自造 `device_id`。
- **必填** `app_id`(整数,如 Spark IM=`1`)、`device_type`(整数 1–6,见 meta / `AppId` / `DeviceType`)。
- 物理指纹:ios/macos 必填 `idfv`;android/harmony 必填 `android_id`;windows/web 至少一个。
- 服务端按 `SHA256("{device_type}:{idfv|android_id}")` 派生 `device_id`(同机多 App **共用**同一 `device_id`)。
- JWT access/refresh 含 `sub`(user_id)、`did`(device_id)、**`aid`(app_id)**、`use`。
- 响应 `data` 含服务端 `device_id` 与 `app_id`;客户端须持久化后用于 Header / TCP。
- 可选 `platform`:与 `device_type` 同枚举,仅当未传 `device_type` 时作别名;可选 `device_name`。
首次验证成功会创建用户。测试账号任意验证码即可。
Header(可选):`X-Language` / `Accept-Language`、`X-App-Version`。不要带 `Authorization`。
Body:`email`、`verify_code`、`app_id`、`device_type`、`idfv`/`android_id`、可选 `device_name`。
operationId: login
parameters:
- $ref: "#/components/parameters/X-Language"
- $ref: "#/components/parameters/Accept-Language"
- $ref: "#/components/parameters/X-App-Version"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
example:
email: test01@spark.com
verify_code: "000000"
app_id: 1
device_type: 1
idfv: "00000000-0000-0000-0000-000000000007"
device_name: iPhone
responses:
"200":
description: 统一 envelope。成功时 data 为 LoginData;失败时 data 为 {}
content:
application/json:
schema:
$ref: "#/components/schemas/LoginEnvelope"
examples:
ok:
value:
code: "0"
msg: ok
data:
access_token: eyJhbGciOiJIUzI1NiJ9...
refresh_token: eyJhbGciOiJIUzI1NiJ9...
expires_in: 604800
refresh_expires_in: 2592000
user_id: 1940000000000000000
device_id: 7
app_id: 1
spark_id: s1a2b3c0
unauthorized:
value:
code: "40101"
msg: 未授权
data: {}
invalid:
value:
code: "40001"
msg: 请填写验证码
data: {}
/v1/auth/password-login:
post:
tags: [auth]
security: []
summary: 邮箱密码登录并签发 JWT
description: |
与验证码登录相同的设备模型(`app_id` / `device_type` / `idfv|android_id`)。
需先通过 `POST /v1/auth/password` 用验证码设置密码;测试邮箱可任意密码。
按 RealIP 限流。不要带 Authorization。
operationId: passwordLogin
parameters:
- $ref: "#/components/parameters/X-Language"
- $ref: "#/components/parameters/Accept-Language"
- $ref: "#/components/parameters/X-App-Version"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password, app_id]
properties:
email: { type: string, format: email }
password: { type: string, minLength: 6, maxLength: 72 }
app_id: { type: integer, minimum: 1 }
device_type: { type: integer, minimum: 1, maximum: 6 }
platform: { type: integer, minimum: 1, maximum: 6 }
device_name: { type: string }
idfv: { type: string }
android_id: { type: string }
example:
email: test01@spark.com
password: "123456"
app_id: 1
device_type: 1
idfv: "00000000-0000-0000-0000-000000000007"
responses:
"200":
description: 与登录相同的 LoginData
content:
application/json:
schema:
$ref: "#/components/schemas/LoginEnvelope"
/v1/auth/password:
post:
tags: [auth]
security: []
summary: 验证码设置/重置邮箱登录密码
description: |
校验邮箱验证码后写入 bcrypt 哈希,供 `password-login` 使用。
Body:`email`、`verify_code`、`password`(6–72)。不需要 Bearer。
operationId: setPassword
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, verify_code, password]
properties:
email: { type: string, format: email }
verify_code: { type: string }
password: { type: string, minLength: 6, maxLength: 72 }
responses:
"200":
description: 统一 envelope,data 为空对象
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/auth/token/refresh:
post:
tags: [auth]
security: []
summary: 用 refresh_token 续期
description: |
校验 refresh_token 后签发新的 access/refresh,旧一对立即失效(jti denylist)。
新 token 对保留相同 **`did` / `aid`**(与首次登录 App 绑定)。
不需要 access Bearer。按 RealIP 限流。Body 仅 `refresh_token`。
operationId: refreshToken
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshRequest"
example:
refresh_token: eyJhbGciOiJIUzI1NiJ9...
responses:
"200":
description: 与登录相同的 LoginData
content:
application/json:
schema:
$ref: "#/components/schemas/LoginEnvelope"
/v1/auth/logout:
post:
tags: [auth]
summary: 退出当前设备会话
description: |
吊销当前 **`(user, device, app)`** 会话(默认 `triple`:各 App 独立 refresh,互不踢)。
若运维切到 `pair` 模式,同设备其它 App 的 refresh 可能被互踢(需产品公告)。
并踢掉同 `device_id` 的 TCP 连接。
operationId: logout
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/legal/agreements:
get:
tags: [client]
security: []
summary: 隐私政策与用户协议
description: |
按 `X-Language` / `Accept-Language` 返回隐私政策与用户协议正文。
无库表数据时回退内置默认文案。公开接口。
operationId: legalAgreements
parameters:
- $ref: "#/components/parameters/X-Language"
- $ref: "#/components/parameters/Accept-Language"
responses:
"200":
description: data.privacy / data.terms
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/feedback:
post:
tags: [client]
security: []
summary: 提交意见反馈(登录可选)
description: |
可选鉴权:带有效 `Authorization: Bearer` → `auth_type=login` 并写入 `user_id`;
无 token → `auth_type=guest`,Body 必填 `contact`(邮箱或手机)与 `content`。
兼容字段 `email`(游客未传 contact 时可用 email 作为 contact)。
operationId: submitFeedback
parameters:
- $ref: "#/components/parameters/X-Language"
- $ref: "#/components/parameters/X-Device-ID"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [content]
properties:
content: { type: string, minLength: 2, maxLength: 2000 }
contact:
type: string
description: 游客必填;邮箱或手机号
email:
type: string
format: email
description: 兼容旧字段;游客可代替 contact
app_version: { type: string }
responses:
"200":
description: data 为反馈记录(含 id、auth_type、contact)
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/subscriptions/plans:
get:
tags: [client]
security: []
summary: 订阅套餐引导配置
description: |
返回 headline/subheadline 与 week/month/quarter/year 套餐卡片(含 IAP product_id)。
公开接口;无库数据时回退默认四档。
支持按 **包名 / applicationId** 筛选:`packageName`(或 `package_name`)Query,或 Header `X-Wyrs-Package`。
有该包精确配置则返回之;否则回退 `package_name` 为空的默认档。
operationId: subscriptionPlans
parameters:
- $ref: "#/components/parameters/X-Language"
- name: packageName
in: query
required: false
schema: { type: string }
description: applicationId / Bundle ID / 包名;也可用 package_name 或 Header X-Wyrs-Package
- name: X-Wyrs-Package
in: header
required: false
schema: { type: string }
description: 与 packageName 等价
responses:
"200":
description: data.plans
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/users/me:
get:
tags: [users]
summary: 当前用户资料
description: |
Header:`Authorization: Bearer <token>`(必填),可选 `X-Device-ID`、`X-App-ID`、`X-Language`。无 Body。
`avatar` 是 object_key,可匿名 GET `https://sparkr.wyrs365.com/spark/{avatar}`。
operationId: getMe
parameters:
- $ref: "#/components/parameters/X-Device-ID"
- $ref: "#/components/parameters/X-App-ID"
- $ref: "#/components/parameters/X-Language"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
uid: 1940000000000000000
spark_id: alice_ok
nickname: test01
avatar: perm/avatar/20260917/3H8kPq2mN1Rabcde.png
qrcode: "-NDveu-9Q"
birthday: ""
gender: 0
status: 1
last_login_time: 1726550000000
last_active_time: 1726550000000
create_time: 1726550000000
update_time: 1726550000000
patch:
tags: [users]
summary: 修改资料
description: 可改 spark_id(8-16 位,字母开头)、nickname、avatar、gender(0/1/2)、birthday(YYYY-MM-DD)。未出现的字段不改。已有超范围 spark_id 可继续使用,改号须符合新规则。
operationId: patchMe
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PatchMeRequest"
example:
nickname: Ada
gender: 2
birthday: "1994-05-01"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [users]
summary: 注销当前账号
description: |
吊销全部设备的 JWT 并踢掉 TCP。将 `users` 行迁入 `user_delete` 后删除原行(邮箱可重新注册)。
非测试账号需先 `POST /v1/sms/codes`(`purpose=2`,`target` 为绑定邮箱)发码,再在本接口提交 `verify_code`(遵守发码封控,见 docs/sms.md)。
同时清除好友关系、朋友圈、收藏、群成员(群主把群转给一名管理员,没有管理员则转给成员表第一位后退出;仅剩自己则解散)、会话成员、收件箱 Redis 键与 mailbox。
对方仍可见历史消息(不删 `messages`)。
operationId: deleteMe
requestBody:
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteAccountRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/lookup:
get:
tags: [friends]
summary: 查找用户
description: |
查询参数 `q`:邮箱则按邮箱查;纯数字按 uid 查;其余先按 spark_id,未命中再按 qrcode。
二维码为 shortid 个人扫描票据,不含 uid。对方已拉黑当前用户时返回 40401。
operationId: lookupFriend
parameters:
- in: query
name: q
required: true
schema: { type: string }
description: 邮箱 / uid / Spark账号 / 二维码
example: test02@spark.com
responses:
"200":
description: 统一 envelope,data 为好友卡片(含 relation:0无 1好友 2已拉黑)
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/requests:
get:
tags: [friends]
summary: 收到的待处理好友申请
operationId: listFriendRequests
parameters:
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 100 }
responses:
"200":
description: data.items
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [friends]
summary: 发起好友申请
description: |
通过扫描二维码、搜索邮箱或 spark_id 发起。可填申请理由与备注。
同一对用户只保留一条申请,重复发起覆盖理由/备注,不新增行。
若对方已有待处理申请指向自己,则自动成为双向好友并写入一条系统消息。
不支持单向好友。
operationId: applyFriend
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FriendApplyRequest"
example:
email: test02@spark.com
reason: 我是同事
remark: 同事
responses:
"200":
description: data 见 FriendApplyData。status 为 1 待处理或 2 已同意(与 friend_requests.status 同值,不是字符串)
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/requests/reject:
post:
tags: [friends]
summary: 拒绝/忽略好友申请
description: |
仅申请接收方可操作。将 pending 申请标为 rejected(status=3)。
已拒绝则幂等成功;非 pending 返回 403。
operationId: rejectFriendRequest
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [request_id]
properties:
request_id: { type: integer, format: int64 }
example:
request_id: 1940000000000000100
responses:
"200":
description: 统一 envelope,data 为空对象
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends:
get:
tags: [friends]
summary: 好友列表
operationId: listFriends
parameters:
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 100 }
responses:
"200":
description: data.items,置顶好友排在前面;每项含 remark、blocked、pinned
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [friends]
summary: 同意好友申请
description: 成为双向好友后自动在单聊中插入一条 `kind=friend.became` 系统消息并入队 deliver。
operationId: acceptFriend
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FriendAcceptRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/{uid}:
get:
tags: [friends]
summary: 查看好友资料
operationId: getFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [friends]
summary: 删除好友
description: 解除双向关系,删除双方各一条好友边。
operationId: deleteFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/{uid}/remark:
put:
tags: [friends]
summary: 设置好友备注
operationId: setFriendRemark
parameters:
- $ref: "#/components/parameters/FriendUID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FriendRemarkRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/{uid}/pin:
put:
tags: [friends]
summary: 置顶好友会话
description: 仅写入自己这一侧的好友边 is_pinned,不影响对方。
operationId: pinFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [friends]
summary: 取消置顶
operationId: unpinFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/{uid}/block:
put:
tags: [friends]
summary: 拉黑
description: |
单向拉黑,写在自己这一侧的好友边上,不解除好友关系。
任一侧拉黑后双方都不能发消息。被拉黑一方再申请时视为用户不存在。
operationId: blockFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [friends]
summary: 取消拉黑
operationId: unblockFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/friends/{uid}/reports:
post:
tags: [friends]
summary: 投诉好友
operationId: reportFriend
parameters:
- $ref: "#/components/parameters/FriendUID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FriendReportRequest"
example:
reason: 1
detail: spam
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/follows/{uid}:
get:
tags: [friends]
summary: 查看是否关注
description: |
返回关注状态,不是新的好友模型。
只有一方关注时 `friend` 为 false。
双方互相关注时自动建立双向好友,`friend` 为 true。
已经互关之后,任何一方取消关注都会删除双方好友边,不删除历史消息。
operationId: getFollow
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: |
data:`uid`、`following`、`followed_by`、`mutual`、`friend`。
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
put:
tags: [friends]
summary: 关注
description: |
无请求体。只写入一条 `user_id` → `target_id` 的关注。
只有一方关注时不成为好友。
双方互相关注时,复用现有好友边自动建立双向好友(来源为互关),并写入已有的成为好友系统消息。
operationId: followUser
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: |
data:`uid`、`following`、`followed_by`、`mutual`、`friend`。
单关时 `following=true`、`mutual=false`、`friend=false`。
补上对方已有的关注后 `mutual=true`、`friend=true`。
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [friends]
summary: 取消关注
description: |
只删除自己这一条关注。
若取消前双方已互关,则删除双方好友边,不是只删一方。历史消息保留。
若本来只有一方关注,不改已有的好友申请关系。
operationId: unfollowUser
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: |
data 字段与关注相同。互关后取关:`friend=false`,双方好友边都没有了。
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/groups:
get:
tags: [groups]
summary: 我加入的群
operationId: listGroups
parameters:
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 100 }
responses:
"200":
description: data.items,id 为自增群号,conversation_id 为 Snowflake 会话 id
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [groups]
summary: 创建群
description: |
`id` 为表内自增群号。发消息、同步使用返回的 `conversation_id`(Snowflake)。
只能拉好友入群。群主无法直接退群,需先转让。
建群成功后服务端按实际人数拼接成员已有头像(注册默认或自定义),最多 9 人;两人就是并排两格,不会凑九宫格。
成员头像拉不到时用字母占位图,整张拼图失败则改用群名默认头像,`avatar` 不会为空。
写入 `perm/group/{群号}/`。之后可用 `purpose=group_avatar` 直传自定义头像再 PATCH。
operationId: createGroup
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateGroupRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/groups/{id}:
get:
tags: [groups]
summary: 群资料
operationId: getGroup
parameters:
- $ref: "#/components/parameters/GroupID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
patch:
tags: [groups]
summary: 修改群名称 / 头像 / 公告 / 全员禁言
description: 仅群主和管理员。`announcement` 为空字符串表示清空公告。
operationId: updateGroup
parameters:
- $ref: "#/components/parameters/GroupID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateGroupRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [groups]
summary: 解散群(仅群主)
operationId: dissolveGroup
parameters:
- $ref: "#/components/parameters/GroupID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/groups/{id}/members:
get:
tags: [groups]
summary: 群成员
operationId: listGroupMembers
parameters:
- $ref: "#/components/parameters/GroupID"
- in: query
name: limit
schema: { type: integer, default: 200, maximum: 200 }
responses:
"200":
description: data.items
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [groups]
summary: 邀请好友入群
operationId: inviteGroupMembers
parameters:
- $ref: "#/components/parameters/GroupID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/InviteGroupMembersRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/groups/{id}/members/{uid}:
patch:
tags: [groups]
summary: 设管理员、禁言、群昵称、群备注
description: |
`role` 仅群主可改(2=管理员 3=普通成员)。`speak_muted` 群主/管理员可对成员设置;管理员不能禁言其他管理员。
`nickname` 是我在群里的昵称(自己可改;管理员也可改他人)。`remark` 是群备注,仅自己可改。
operationId: updateGroupMember
parameters:
- $ref: "#/components/parameters/GroupID"
- $ref: "#/components/parameters/FriendUID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateGroupMemberRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [groups]
summary: 踢人,或 uid 为自己时退群
operationId: removeGroupMember
parameters:
- $ref: "#/components/parameters/GroupID"
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/groups/{id}/transfer:
post:
tags: [groups]
summary: 转让群主
operationId: transferGroup
parameters:
- $ref: "#/components/parameters/GroupID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TransferGroupRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/files:
post:
tags: [files]
summary: 签发对象存储直传凭证
description: |
客户端只传 `purpose` / `mime` / `size`,服务端生成 object_key 并签发短时 Presigned PUT。
文件直传 RustFS,不经过 Gin。
`avatar` 写入 `perm/avatar/{yyyyMMdd}/{name}.{ext}`(不过期,路径不含 uid,匿名 GET);
`group_avatar` 写入 `perm/group/{群号}/{name}.{ext}`(需 `group_id`,群主/管理员,匿名 GET);
聊天按类型:`chat/image|voice|video|gif|file/{yyyyMMdd}/{name}.{ext}`(桶生命周期按前缀过期)。
朋友圈:`moment_image` / `moment_video` / `moment_cover` 写入 `perm/moment/{image|video|cover}/{yyyyMMdd}/{name}.{ext}`(不过期,删帖/换封面时删对象)。
广场:`square_image` / `square_video` 写入 `perm/square/{image|video}/{yyyyMMdd}/{name}.{ext}`(不过期,删帖时删对象)。
`{name}` 为服务端 16 位 base62 随机串,不占用 Snowflake。
发图片:客户端压成 JPEG/PNG 后 `POST /v1/files/batch` 直传缩略图、压缩大图、原图(未勾选原图只传前两项)。
服务端不转码。iOS HEIC、Android WebP 须客户端先转 JPEG/PNG,两端才能互看。
客户端 PUT 必须原样带返回的 `headers`。消息只存 object_key。
operationId: createUploadCredential
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUploadRequest"
example:
purpose: chat_image
mime: image/jpeg
size: 245760
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
object_key: chat/image/20260917/3H8kPq2mN1Rabcde.jpg
upload_url: https://sparkr.wyrs365.com/spark/chat/image/20260917/3H8kPq2mN1Rabcde.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256
method: PUT
headers:
Content-Type: image/jpeg
Content-Length: "245760"
If-None-Match: "*"
expires_in: 900
/v1/files/batch:
post:
tags: [files]
summary: 批量签发直传凭证
description: |
一次返回 1~3 个 Presigned PUT,顺序与 `items` 相同。聊天图片请按「缩略图、压缩大图、原图」排列;未勾选原图只传前两项。
mime 仅 `image/jpeg` / `image/png`(HEIC、WebP 请客户端先转)。限流按条数计入 `upload_per_second`。文件仍直传 RustFS。
operationId: createUploadCredentials
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUploadBatchRequest"
example:
items:
- purpose: chat_image
mime: image/jpeg
size: 8192
- purpose: chat_image
mime: image/jpeg
size: 245760
- purpose: chat_image
mime: image/jpeg
size: 3500000
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
items:
- object_key: chat/image/20260917/3H8kPq2mN1Rabcde.jpg
method: PUT
headers:
Content-Type: image/jpeg
Content-Length: "8192"
If-None-Match: "*"
- object_key: chat/image/20260917/4kPq2mN1RabcdeXY.jpg
method: PUT
- object_key: chat/image/20260917/5mN1RabcdeXY12ab.jpg
method: PUT
/v1/files/downloads:
post:
tags: [files]
summary: 签发对象存储下载凭证
description: 已登录用户用 object_key 换取短时 Presigned GET。key 必须是 `perm/` 或 `chat/` 前缀。九宫格请用 `POST /v1/files/downloads/batch`。
operationId: createDownloadCredential
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateDownloadRequest"
example:
object_key: chat/20260917/1/2.jpg
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/files/downloads/batch:
post:
tags: [files]
summary: 批量签发下载凭证
description: |
一次返回 1~9 个 Presigned GET,顺序与 `object_keys` 相同。朋友圈九宫格一次拿齐。
限流按请求计入,不按条数。文件仍直传 RustFS GET。
operationId: createDownloadCredentials
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateDownloadBatchRequest"
example:
object_keys:
- perm/moment/image/20260918/a.jpg
- perm/moment/image/20260918/b.jpg
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
items:
- object_key: perm/moment/image/20260918/a.jpg
download_url: https://sparkr.wyrs365.com/spark/perm/moment/image/20260918/a.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256
method: GET
expires_in: 900
- object_key: perm/moment/image/20260918/b.jpg
method: GET
/v1/favorites:
get:
tags: [favorites]
summary: 收藏列表
operationId: listFavorites
parameters:
- in: query
name: limit
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [favorites]
summary: 收藏一条消息
description: |
会话成员可将消息收入收藏。图片/语音/视频/文件会 CopyObject 到 `perm/fav/{yyyyMMdd}/{name}.{ext}`。
源对象已从 `chat/` 过期删除则失败(`code=40402`),不提供本地补传。
同一 `msg_id` 重复收藏返回已有记录。撤回不影响已收藏快照。
operationId: createFavorite
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateFavoriteRequest"
example:
msg_id: 1
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/favorites/{id}:
delete:
tags: [favorites]
summary: 取消收藏
operationId: deleteFavorite
parameters:
- $ref: "#/components/parameters/FavoriteID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments:
get:
tags: [moments]
summary: 好友朋友圈 Feed
description: |
`cursor` 为上一页最后一条 `post_id`,0 表示最新。读时按好友、拉黑、不看他/不让他看、可见范围过滤。
不写会话 inbox。
operationId: listMomentFeed
parameters:
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [moments]
summary: 发朋友圈
description: |
纯文,或最多 9 张图,或 1 条视频(图/视频互斥)。媒体先 `POST /v1/files`(`moment_image` / `moment_video`)再填 key。
浏览九宫格:`POST /v1/files/downloads/batch` 一次换 9 张 Presigned GET。
`visibility`:0 好友公开 1 私密 2 白名单 3 黑名单。2/3 需 `audience_uids`(好友,最多 200)。
operationId: publishMoment
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PublishMomentRequest"
example:
text: hello
visibility: 0
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/unread:
get:
tags: [moments]
summary: 发现页红点
operationId: momentUnread
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/read:
put:
tags: [moments]
summary: 更新发现页已读水位
operationId: momentMarkRead
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MomentReadRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/notices:
get:
tags: [moments]
summary: 赞评通知列表
operationId: listMomentNotices
parameters:
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/notices/read:
post:
tags: [moments]
summary: 通知标已读
operationId: readMomentNotices
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/MomentNoticeReadRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/settings:
get:
tags: [moments]
summary: 朋友圈设置
operationId: getMomentSettings
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
patch:
tags: [moments]
summary: 改封面或可见天数
operationId: patchMomentSettings
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PatchMomentSettingsRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/settings/hides/{uid}:
put:
tags: [moments]
summary: 不看他 / 不让他看
operationId: putMomentHide
parameters:
- $ref: "#/components/parameters/FriendUID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MomentHideRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [moments]
summary: 清除对该好友的隐藏
operationId: deleteMomentHide
parameters:
- $ref: "#/components/parameters/FriendUID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/users/{uid}:
get:
tags: [moments]
summary: 某人朋友圈相册
description: 非好友返回 404。看自己不过可见天数,能看到私密帖。
operationId: listMomentAlbum
parameters:
- $ref: "#/components/parameters/FriendUID"
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/{id}:
get:
tags: [moments]
summary: 单条动态
operationId: getMoment
parameters:
- $ref: "#/components/parameters/MomentID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [moments]
summary: 删除自己的动态
operationId: deleteMoment
parameters:
- $ref: "#/components/parameters/MomentID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/{id}/like:
put:
tags: [moments]
summary: 点赞
operationId: likeMoment
parameters:
- $ref: "#/components/parameters/MomentID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [moments]
summary: 取消赞
operationId: unlikeMoment
parameters:
- $ref: "#/components/parameters/MomentID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/{id}/comments:
post:
tags: [moments]
summary: 评论或回复
operationId: commentMoment
parameters:
- $ref: "#/components/parameters/MomentID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MomentCommentRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/moments/{id}/comments/{cid}:
delete:
tags: [moments]
summary: 删除评论
operationId: deleteMomentComment
parameters:
- $ref: "#/components/parameters/MomentID"
- name: cid
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/categories:
get:
tags: [square]
summary: 广场分类 Tab
description: |
返回筛选分类:`recommend`/`latest`/`hot`/`nearby`(feed 模式)与 `funny`/`beauty`(内容标签)。
各广场实例共用同一套分类枚举;数据按 `square_channel` 隔离。
operationId: listSquareCategories
parameters:
- $ref: "#/components/parameters/SquareChannel"
responses:
"200":
description: 统一 envelope;data.items[].value / kind(feed|tag)
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/posts:
get:
tags: [square]
summary: 广场 Feed
description: |
`square_channel`:`square`(原路径兼容)/ `squarenovel` / `squaresteps` / `squarehealth`。
`category`:推荐/最新/热门/附近为排序模式;搞笑/美女按帖标签筛选。
`nearby` 可传 `lat`/`lng`(曼哈顿距离 stub);未传则仅返回带 POI 的帖并按 id 倒序。
`cursor` 为上一页最后一条 `post_id`,0 表示最新。
operationId: listSquareFeed
parameters:
- $ref: "#/components/parameters/SquareChannel"
- in: query
name: category
schema:
$ref: "#/components/schemas/SquareCategory"
description: 缺省 recommend
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
- in: query
name: lat
schema: { type: number, format: double }
description: 附近排序纬度(category=nearby)
- in: query
name: lng
schema: { type: number, format: double }
description: 附近排序经度(category=nearby)
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [square]
summary: 发广场帖
description: |
内容与朋友圈相同。媒体先 `POST /v1/files`(`square_image` / `square_video`)再填 key。
`visibility`:0 全站公开 1 仅自己 2 白名单 3 黑名单。2/3 需 `audience_uids`(双向好友,最多 200)。
可选 `category` 内容标签:`funny` / `beauty`(不可传 feed 模式值)。
operationId: publishSquarePost
parameters:
- $ref: "#/components/parameters/SquareChannel"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PublishSquareRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/posts/{id}:
get:
tags: [square]
summary: 单条广场帖
operationId: getSquarePost
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/SquarePostID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [square]
summary: 删除自己的广场帖
operationId: deleteSquarePost
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/SquarePostID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/posts/{id}/like:
put:
tags: [square]
summary: 点赞
operationId: likeSquarePost
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/SquarePostID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
delete:
tags: [square]
summary: 取消赞
operationId: unlikeSquarePost
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/SquarePostID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/posts/{id}/comments:
post:
tags: [square]
summary: 评论或回复
operationId: commentSquarePost
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/SquarePostID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MomentCommentRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/posts/{id}/comments/{cid}:
delete:
tags: [square]
summary: 删除评论
operationId: deleteSquareComment
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/SquarePostID"
- name: cid
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/users/{uid}/posts:
get:
tags: [square]
summary: 某用户广场帖
description: 按当前浏览者与帖可见性、拉黑过滤;看自己可见私密与白名单帖。仅返回该 channel 下的帖。
operationId: listSquareUserPosts
parameters:
- $ref: "#/components/parameters/SquareChannel"
- $ref: "#/components/parameters/FriendUID"
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/notices:
get:
tags: [square]
summary: 赞评通知列表
operationId: listSquareNotices
parameters:
- $ref: "#/components/parameters/SquareChannel"
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/notices/unread:
get:
tags: [square]
summary: 广场互动未读角标
operationId: squareNoticeUnread
parameters:
- $ref: "#/components/parameters/SquareChannel"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/{square_channel}/notices/read:
post:
tags: [square]
summary: 通知标已读
operationId: readSquareNotices
parameters:
- $ref: "#/components/parameters/SquareChannel"
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/MomentNoticeReadRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/music/playlist:
get:
tags: [music]
summary: 音乐列表(公开)
description: |
返回已上架曲目、启用中的分类树与全局「会员音乐免费试听秒数」。无需登录。
封面/背景图/音频为 sparkr 公网 URL(`https://sparkr.wyrs365.com/spark/perm/music/...`)。
客户端应按 `requireMember` 与 `memberPreviewSeconds` 控制试听;`backgroundUrl` 可空。
传 `categoryId` 时仅返回归属该分类或其子分类的曲目(多对多)。
传 `requireMember=true|false` 可仅返回会员曲或免费曲(对齐欢休 VIP 分区筛选)。
operationId: getMusicPlaylist
security: []
parameters:
- name: categoryId
in: query
required: false
schema: { type: integer, format: int64, minimum: 0 }
description: 按分类筛选;0 或不传=全部上架曲目
- name: requireMember
in: query
required: false
schema: { type: boolean }
description: true=仅会员曲;false=仅免费曲;不传=全部
responses:
"200":
description: 统一 envelope,`data` 为 MusicPlaylist
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- type: object
properties:
data:
$ref: "#/components/schemas/MusicPlaylist"
/v1/music/playlist/grouped:
get:
tags: [music]
summary: 分区嵌套音乐列表(公开)
description: |
按叶子分类分组返回曲目,便于首页「分区 + 分区下音乐」一次拉取。
对齐欢休 `zoneAndZoneMusicList` 语义(Spark 用 category 作 zone)。
`parentId`>0 时:该分类为叶子则只返回自身;有子分类则返回其子叶子。
无需登录。
operationId: getMusicPlaylistGrouped
security: []
parameters:
- name: parentId
in: query
required: false
schema: { type: integer, format: int64, minimum: 0 }
description: 父分类 id;0 或不传=全部叶子分类
responses:
"200":
description: 统一 envelope,`data` 为 MusicGroupedPlaylist
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- type: object
properties:
data:
$ref: "#/components/schemas/MusicGroupedPlaylist"
/v1/music/tracks/{id}:
get:
tags: [music]
summary: 单曲详情(公开)
description: |
返回已上架曲目详情(含播放 `audioUrl`、封面/背景)。下架或不存在返回 `40401`。
对齐欢休 `/music/v1/detail`;无需登录。
operationId: getMusicTrack
security: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64, minimum: 1 }
responses:
"200":
description: 统一 envelope,`data` 为 MusicTrack
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- type: object
properties:
data:
$ref: "#/components/schemas/MusicTrack"
/v1/music/categories:
get:
tags: [music]
summary: 音乐分类树(公开)
description: 返回启用中的总分类与子分类树。无需登录。
operationId: getMusicCategories
security: []
responses:
"200":
description: 统一 envelope,`data.categories` 为 MusicCategory 数组(含 children)
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- type: object
properties:
data:
type: object
properties:
categories:
type: array
items:
$ref: "#/components/schemas/MusicCategory"
/v1/music/settings:
get:
tags: [music]
summary: 音乐全局配置(公开)
description: 仅返回 `memberPreviewSeconds`(会员曲免费试听秒数)。无需登录。
operationId: getMusicSettings
security: []
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- type: object
properties:
data:
$ref: "#/components/schemas/MusicSettings"
/api/admin/spark/music:
get:
tags: [admin-music]
summary: 管理端曲目列表
description: |
popadmin 同源调用。`data.items` 为全部曲目(含下架与 `categoryIds`),`data.settings` 为全局试听配置,`data.categories` 为分类树。
别名:`GET /api/admin/pop/music`。
operationId: adminListMusic
security:
- bearerAuth: []
responses:
"200":
description: "AdminApiResponse:success、message、data(含 items 与 settings)"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
post:
tags: [admin-music]
summary: 新建曲目
operationId: adminCreateMusic
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MusicUpsertRequest"
responses:
"200":
description: data 为 MusicTrack(AdminApiResponse)
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/settings:
get:
tags: [admin-music]
summary: 读取全局试听时长
operationId: adminGetMusicSettings
security:
- bearerAuth: []
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
put:
tags: [admin-music]
summary: 更新全局试听时长
description: Body `{"memberPreviewSeconds":0..3600}`
operationId: adminPutMusicSettings
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MusicSettings"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/{id}:
patch:
tags: [admin-music]
summary: 更新曲目元数据(曲名/会员/排序/上下架/分类)
description: |
可改 `title`、`requireMember`、`sortOrder`、`enabled`、`categoryIds`。
封面/背景/音频请分别调用 POST `/music/{id}/cover|background|audio` 上传或替换。
operationId: adminUpdateMusic
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MusicUpsertRequest"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
delete:
tags: [admin-music]
summary: 删除曲目(并尝试删除对象存储文件)
operationId: adminDeleteMusic
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/categories:
get:
tags: [admin-music]
summary: 分类树(含下架)
description: "`data.categories` 为总分类树(children 为子分类)。别名 `/api/admin/pop/music/categories`。"
operationId: adminListMusicCategories
security:
- bearerAuth: []
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
post:
tags: [admin-music]
summary: 新建总分类或子分类
description: |
Body:`name` 必填;`parentId` 空/0=总分类,非空=挂到该总分类下(仅两级);
`sortOrder`、`enabled` 可选。
operationId: adminCreateMusicCategory
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MusicCategoryUpsertRequest"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/categories/{id}:
patch:
tags: [admin-music]
summary: 更新分类(改名/排序/启用/父级)
operationId: adminUpdateMusicCategory
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MusicCategoryUpsertRequest"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
delete:
tags: [admin-music]
summary: 删除分类(级联删子分类与曲目绑定)
operationId: adminDeleteMusicCategory
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/{id}/categories:
put:
tags: [admin-music]
summary: 设置曲目所属分类(多对多全量替换)
description: Body `{"categoryIds":[1,2,3]}`;空数组清除全部归属。亦可在 PATCH `/music/{id}` 传 `categoryIds`。
operationId: adminSetMusicTrackCategories
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [categoryIds]
properties:
categoryIds:
type: array
items: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/{id}/cover:
post:
tags: [admin-music]
summary: 上传封面(multipart file)
description: 字段名 `file`;jpg/png/webp/gif,≤5MB 接收后自动压到 ≤150KiB。写入 RustFS `perm/music/cover/...`。
operationId: adminUploadMusicCover
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/{id}/background:
post:
tags: [admin-music]
summary: 上传背景图(multipart file)
description: 字段名 `file`;jpg/png/webp/gif,≤5MB 接收后自动压到 ≤150KiB。写入 RustFS `perm/music/background/...`。可空(未上传时 `backgroundUrl` 为空串)。
operationId: adminUploadMusicBackground
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/music/{id}/audio:
post:
tags: [admin-music]
summary: 上传 mp3(multipart file)
description: 字段名 `file`;audio/mpeg,≤50MB。写入 RustFS `perm/music/audio/...`。
operationId: adminUploadMusicAudio
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/sensitive/words:
get:
tags: [admin-music]
summary: 敏感词分页列表
description: |
Query:`q`(匹配 word_raw/word)、`category`(1–9)、`enabled`(`true`/`false`/`1`/`0`)、`page`、`size`。
别名:`GET /api/admin/pop/sensitive/words`。
operationId: adminListSensitiveWords
security:
- bearerAuth: []
parameters:
- name: q
in: query
schema: { type: string }
- name: category
in: query
schema: { $ref: "#/components/schemas/SensitiveCategory" }
- name: enabled
in: query
schema: { type: string }
description: true/false 或 1/0
- name: page
in: query
schema: { type: integer, default: 0 }
- name: size
in: query
schema: { type: integer, default: 50, maximum: 200 }
responses:
"200":
description: "`data.items` 为词条,`data.total` 为总数"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
post:
tags: [admin-music]
summary: 新建敏感词
description: |
`word` 为运营录入原文;服务端 Fold 后写入 `word` 列(唯一键)。`action=4`(hide)在 v1 会按 reject 处理。
成功后自动 Reload 自动机。
operationId: adminCreateSensitiveWord
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SensitiveWordCreateRequest"
responses:
"200":
description: 返回新建词条
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/sensitive/reload:
post:
tags: [admin-music]
summary: 从数据库重建敏感词自动机
operationId: adminReloadSensitiveWords
security:
- bearerAuth: []
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/sensitive/words/{id}:
patch:
tags: [admin-music]
summary: 更新敏感词
operationId: adminPatchSensitiveWord
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SensitiveWordPatchRequest"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
delete:
tags: [admin-music]
summary: 下线敏感词(enabled=0)
operationId: adminDeleteSensitiveWord
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
/api/admin/spark/sensitive/hits:
get:
tags: [admin-music]
summary: 敏感词命中审计分页
description: Query:`uid`、`category`、`since`/`until`(RFC3339)、`page`、`size`。
operationId: adminListSensitiveHits
security:
- bearerAuth: []
parameters:
- name: uid
in: query
schema: { type: integer, format: int64 }
- name: category
in: query
schema: { $ref: "#/components/schemas/SensitiveCategory" }
- name: since
in: query
schema: { type: string, format: date-time }
- name: until
in: query
schema: { type: string, format: date-time }
- name: page
in: query
schema: { type: integer, default: 0 }
- name: size
in: query
schema: { type: integer, default: 50, maximum: 200 }
responses:
"200":
description: "`data.items` 为命中行,`data.total` 为总数"
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiResponse"
# —— FullStoryNovel 公开小说 API(基址选 admin.fullstorynovel.com 或 fullstorynovel.com)——
/api/home:
get:
tags: [novels]
summary: 首页聚合(精选/热门/新书/更新/分类)
operationId: fsnHome
servers:
- url: https://admin.fullstorynovel.com
- url: https://fullstorynovel.com
security: []
responses:
"200":
description: HomeDto 直接 JSON
content:
application/json:
schema:
$ref: "#/components/schemas/FsnHome"
/api/genres:
get:
tags: [novels]
summary: 分类列表
operationId: fsnGenres
servers:
- url: https://admin.fullstorynovel.com
- url: https://fullstorynovel.com
security: []
parameters:
- name: tree
in: query
schema: { type: boolean }
description: true 时返回一级含 children 树
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FsnGenre"
/api/novels:
get:
tags: [novels]
summary: 小说分页搜索
operationId: fsnListNovels
servers:
- url: https://admin.fullstorynovel.com
- url: https://fullstorynovel.com
security: []
parameters:
- { name: status, in: query, schema: { type: string } }
- { name: language, in: query, schema: { type: string } }
- { name: genre, in: query, schema: { type: string }, description: 一级或二级 slug }
- { name: q, in: query, schema: { type: string } }
- { name: sort, in: query, schema: { type: string, default: popular } }
- { name: page, in: query, schema: { type: integer, default: 0 } }
- { name: size, in: query, schema: { type: integer, default: 24 } }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnNovelPage"
/api/novels/{slug}:
get:
tags: [novels]
summary: 小说详情
operationId: fsnGetNovel
servers:
- url: https://admin.fullstorynovel.com
- url: https://fullstorynovel.com
security: []
parameters:
- name: slug
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnNovelCard"
/api/novels/{slug}/chapters:
get:
tags: [novels]
summary: 章节目录
operationId: fsnListChapters
servers:
- url: https://admin.fullstorynovel.com
- url: https://fullstorynovel.com
security: []
parameters:
- name: slug
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FsnChapterMeta"
/api/novels/{novelSlug}/chapters/{chapterSlug}:
get:
tags: [novels]
summary: 章节正文
operationId: fsnGetChapter
servers:
- url: https://admin.fullstorynovel.com
- url: https://fullstorynovel.com
security: []
parameters:
- { name: novelSlug, in: path, required: true, schema: { type: string } }
- { name: chapterSlug, in: path, required: true, schema: { type: string } }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnChapterDetail"
# —— FullStoryNovel CMS 管理 API(基址 admin.fullstorynovel.com)——
/api/admin/adopt-ops-session:
post:
tags: [admin-novels]
summary: 用 wyrs Admin JWT 换取 CMS 会话
description: |
Header `Authorization: Bearer <wyrs_admin_token>`(与 popadmin 登录同一 JWT)。
返回 CMS `token`;popadmin iframe 嵌入时用 postMessage 注入。
operationId: fsnAdoptOpsSession
servers:
- url: https://admin.fullstorynovel.com
security: []
responses:
"200":
description: "CMS JSON:token、username、opsToken 等"
content:
application/json:
schema:
type: object
properties:
token: { type: string, description: FSN CMS Bearer }
username: { type: string }
opsToken: { type: string }
expiresInHours: { type: integer }
/api/admin/stats:
get:
tags: [admin-novels]
summary: CMS 仪表盘计数
operationId: fsnAdminStats
servers:
- url: https://admin.fullstorynovel.com
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
novels: { type: integer, format: int64 }
publishedNovels: { type: integer, format: int64 }
chapters: { type: integer, format: int64 }
genres: { type: integer, format: int64 }
/api/admin/novels:
get:
tags: [admin-novels]
summary: 管理端小说列表
operationId: fsnAdminListNovels
servers:
- url: https://admin.fullstorynovel.com
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FsnAdminNovelCard"
post:
tags: [admin-novels]
summary: 新建小说
operationId: fsnAdminCreateNovel
servers:
- url: https://admin.fullstorynovel.com
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FsnNovelUpsert"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnAdminNovelCard"
/api/admin/novels/{id}:
get:
tags: [admin-novels]
summary: 管理端小说详情
operationId: fsnAdminGetNovel
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnAdminNovelCard"
put:
tags: [admin-novels]
summary: 更新小说(含上下架 published / 精选 featured)
operationId: fsnAdminUpdateNovel
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FsnNovelUpsert"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnAdminNovelCard"
delete:
tags: [admin-novels]
summary: 删除小说及章节
operationId: fsnAdminDeleteNovel
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
/api/admin/covers:
post:
tags: [admin-novels]
summary: 上传封面 multipart
operationId: fsnAdminUploadCover
servers:
- url: https://admin.fullstorynovel.com
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file: { type: string, format: binary }
responses:
"200":
description: 含 coverUrl
content:
application/json:
schema:
type: object
properties:
coverUrl: { type: string }
/api/admin/novels/{id}/chapters:
get:
tags: [admin-novels]
summary: 管理端章节列表
operationId: fsnAdminListChapters
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FsnChapterMeta"
post:
tags: [admin-novels]
summary: 新增章节
operationId: fsnAdminCreateChapter
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FsnChapterUpsert"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnChapterMeta"
/api/admin/chapters/{id}:
get:
tags: [admin-novels]
summary: 章节详情(含正文)
operationId: fsnAdminGetChapter
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: Chapter 实体 JSON
put:
tags: [admin-novels]
summary: 更新章节
operationId: fsnAdminUpdateChapter
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FsnChapterUpsert"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnChapterMeta"
delete:
tags: [admin-novels]
summary: 删除章节
operationId: fsnAdminDeleteChapter
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
/api/admin/genres:
get:
tags: [admin-novels]
summary: 管理端分类/标签
operationId: fsnAdminListGenres
servers:
- url: https://admin.fullstorynovel.com
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FsnAdminGenre"
post:
tags: [admin-novels]
summary: 新建分类或二级标签
operationId: fsnAdminCreateGenre
servers:
- url: https://admin.fullstorynovel.com
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FsnGenreUpsert"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnAdminGenre"
/api/admin/genres/{id}:
put:
tags: [admin-novels]
summary: 更新分类
operationId: fsnAdminUpdateGenre
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/FsnGenreUpsert"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/FsnAdminGenre"
delete:
tags: [admin-novels]
summary: 删除分类
operationId: fsnAdminDeleteGenre
servers:
- url: https://admin.fullstorynovel.com
parameters:
- name: id
in: path
required: true
schema: { type: integer, format: int64 }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
/v1/sign/calendar:
get:
tags: [sign]
summary: 签到月历
description: |
返回指定年月每日签到状态,字段对齐旧客户端 `SignInBean`:
`continue_days` / `need_days` / `reward_point` / `status_list[]`。
`sign_in_status`:`0` 未签到/未来,`1` 已签到,`2` 已补签,`3` 可补签。
同时返回当前 `energy`(能量余额)。
operationId: getSignCalendar
parameters:
- in: query
name: year
schema: { type: integer, example: 2026 }
- in: query
name: month
schema: { type: integer, minimum: 1, maximum: 12, example: 10 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
continue_days: 1
need_days: 6
reward_point: 15
today_sign_status: "1"
energy: 10
total_reward_point: 10
status_list:
- day: 20261026
sign_in_status: "1"
reward_point: 10
/v1/sign:
post:
tags: [sign]
summary: 当日签到
description: |
对当前时区(Asia/Shanghai)今天签到并发放能量。重复签到返回 `40901`。
响应含 `reward_point`、`continue_days`、`energy`。
operationId: postSign
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
day: 20261026
reward_point: 10
continue_days: 1
need_days: 6
energy: 10
kind: 1
/v1/sign/makeup:
get:
tags: [sign]
summary: 补签资格查询
description: |
Query `date` 为 `YYYYMMDD`。仅允许补签本月、早于今天且尚未签到的日期。
`need_watch_video_count` 当前为 0(激励视频闸门未启用)。
operationId: getSignMakeup
parameters:
- in: query
name: date
required: true
schema: { type: integer, example: 20261025 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
can_resign: true
need_watch_video_count: 0
date: 20261025
post:
tags: [sign]
summary: 执行补签
description: |
Body `{"date":YYYYMMDD}`。成功发放补签能量;不可补签返回 `40301`,已签过返回 `40901`/`40301`。
operationId: postSignMakeup
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [date]
properties:
date: { type: integer, example: 20261025 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/energy:
get:
tags: [energy]
summary: 能量余额
description: 返回 `{energy}`。无钱包时惰性创建余额 0。
operationId: getEnergy
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
example:
code: "0"
msg: ok
data:
energy: 10
/v1/energy/logs:
get:
tags: [energy]
summary: 能量明细
description: |
分页能量流水,字段兼容旧账单:`reward_details[{source,date,value}]`,
`next_cursor` / `is_end`。`cursor` 为上一页最后一条流水 id(0 表示首页)。
operationId: getEnergyLogs
parameters:
- in: query
name: cursor
schema: { type: integer, format: int64, default: 0 }
- in: query
name: limit
schema: { type: integer, default: 20, maximum: 50 }
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/conversations:
get:
tags: [conversations]
summary: 会话列表增量(离线后上线)
description: |
与 TCP `SYNC_INBOX` 同一 Application。`dialog_score` 是 Redis 会话 ZSET 游标(score=MsgId),
`mailbox_seq` 是撤回/删除等控制事件游标。成功在线推送的新消息不进 mailbox,正文用
`GET /v1/messages?last_msg_id=` 或 `SYNC_MESSAGES` 按 MsgId 补齐。
Header:`Authorization`(必填),可选 `X-Device-ID`、`X-Language`。
Query:`dialog_score`、`mailbox_seq`、`limit`。无 Body。
operationId: syncInbox
parameters:
- $ref: "#/components/parameters/X-Device-ID"
- $ref: "#/components/parameters/X-Language"
- in: query
name: dialog_score
required: false
schema:
type: integer
format: int64
default: 0
- in: query
name: mailbox_seq
required: false
schema:
type: integer
format: int64
default: 0
- in: query
name: limit
required: false
schema:
type: integer
default: 200
maximum: 200
responses:
"200":
description: data 含 dialogs / mailbox / dialog_score / mailbox_seq / has_more
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/messages:
get:
tags: [messages]
summary: 会话历史
description: |
`id > last_msg_id` 升序,最多 200 条,`has_more` 表示还有更新消息。
私聊读 `private_message`(含 `receiver_id`),群聊读 `group_message`(含 `group_id` 群号)。
不含当前用户已删除的消息;已撤回的消息 `status=2` 仍返回。
长连接优先 `SYNC_MESSAGES`。
Header:`Authorization`(必填),可选 `X-Device-ID`、`X-Language`。
Query:`conversation_id`、`last_msg_id`、`limit`。无 Body。
operationId: listMessages
parameters:
- $ref: "#/components/parameters/X-Device-ID"
- $ref: "#/components/parameters/X-Language"
- in: query
name: conversation_id
required: true
schema:
type: integer
format: int64
- in: query
name: last_msg_id
required: false
schema:
type: integer
format: int64
default: 0
- in: query
name: limit
required: false
schema:
type: integer
default: 200
maximum: 200
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
post:
tags: [messages]
summary: 发消息(调试/非长连接;App 在线发送用 TCP)
description: |
**客户端在线发消息走 TCP `SEND_MESSAGE`(Command=4)**,不是本接口。对方实时收消息走 TCP `MESSAGE` 推送。
本接口与 TCP 调用同一套 Application(落库 + deliver 入队),给调试、单测、尚未接 TCP 的页面用。HTTP 发完不会变成长连接,也收不到实时推送。
`conversation_id` 与 `peer_id` 至少填一个。单聊写入 `private_message`,群聊写入 `group_message`。
回复在 `content.reply_to`。群聊 `content.at_uids` 可@成员;`content.at_all` 仅群主/管理员可@所有人。
成功 data 含 `msg_id`、`conversation_id`、`client_msg_id`、`server_time`。
ACK 仅在 MySQL COMMIT 且 deliver 入队成功后返回(与 TCP `SEND_ACK` 同一条件)。
Header:`Authorization: Bearer <token>`(必填)、可选 `X-Device-ID`、`X-Language`、`X-App-Version`。
Body(JSON):`conversation_id` 或 `peer_id`、`client_msg_id`、`content`。token / device_id / 语言不放 Body。
operationId: sendMessage
parameters:
- $ref: "#/components/parameters/X-Device-ID"
- $ref: "#/components/parameters/X-Language"
- $ref: "#/components/parameters/X-App-Version"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SendMessageRequest"
example:
peer_id: 2
client_msg_id: "c-1"
content:
t: 1
reply_to: 0
text:
body: hello
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/messages/{id}/recall:
post:
tags: [messages]
summary: 撤回消息
description: 仅发送者、2 分钟内。全员可见 `status=2`,经 deliver 下发 `RECALL_MESSAGE`。
operationId: recallMessage
parameters:
- $ref: "#/components/parameters/MessageID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/messages/{id}:
delete:
tags: [messages]
summary: 删除消息(仅自己)
description: 写入 `message_deletes`,对端历史不变。其他设备经 `DELETE_MESSAGE` 同步隐藏。
operationId: deleteMessage
parameters:
- $ref: "#/components/parameters/MessageID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
HTTP Header:`Authorization: Bearer <access_token>`。
`access_token` 来自 `POST /v1/auth/login` 的 `data.access_token`。TCP AUTH Body 的 `token` 用同一值(不是 Header)。
schemas:
Gender:
type: integer
enum: [0, 1, 2]
description: users.gender。0未知 1男 2女
UserStatus:
type: integer
enum: [0, 1]
description: users.status。0停用 1正常
ConversationType:
type: integer
enum: [1, 2]
description: conversations.type。1单聊 2群聊
MessageKind:
type: integer
enum: [1, 2]
description: 消息分表,与 ConversationType 同值。1=private_message 2=group_message
MessageType:
type: integer
enum: [1, 2, 3, 4, 5, 6, 7, 8, 9]
description: messages.message_type / content.t。1文本 2图片 3语音 4视频 5表情 6GIF 7文件 8位置 9系统
MessageStatus:
type: integer
enum: [1, 2, 3]
description: private_message/group_message.status。1正常 2已撤回 3审核隐藏
RelationKind:
type: integer
enum: [0, 1, 2]
description: 查看者视角 JSON relation(不是 friend_relations.status)。0无 1好友 2已拉黑
FriendRequestStatus:
type: integer
enum: [1, 2, 3]
description: friend_requests.status,申请结果 status 同此。1待处理 2已同意 3已拒绝
FriendSource:
type: integer
enum: [0, 1, 2, 3, 4]
description: 好友添加来源。0未知 1二维码 2邮箱 3spark_id 4互关
ReportReason:
type: integer
enum: [1, 2, 3, 4]
description: friend_reports.reason。1垃圾 2骚扰 3不实身份 4其他
GroupStatus:
type: integer
enum: [1, 2]
description: groups.status。1正常 2已解散
GroupRole:
type: integer
enum: [1, 2, 3]
description: group_members.role。1群主 2管理员 3成员。PATCH 成员只能设 2 或 3
MailboxEventType:
type: integer
format: int16
enum: [1, 2, 3]
description: mailbox_events.event_type。1漏推消息 2撤回 3删除
DeviceType:
type: integer
format: int32
enum: [1, 2, 3, 4, 5, 6]
description: |
设备类型,见 meta `device_type`。1=ios 2=android 3=harmony 4=windows 5=macos 6=web。
指纹 payload 使用该数值。
AppId:
type: integer
enum: [1]
description: |
登录 App 标识。写入 JWT `aid`。当前文档约定 Spark IM=`1`(以 `GET /v1/meta/enums` → `app_id` 为准;实现落地后枚举会扩展)。
Language:
type: string
enum: [zh-CN, en, zh-TW]
description: 缺省 zh-CN
UploadPurpose:
type: string
enum: [avatar, group_avatar, chat_image, chat_voice, chat_video, chat_gif, chat_file, moment_image, moment_video, moment_cover, square_image, square_video]
SystemKind:
type: string
enum: [friend.became, system.welcome, group.created, group.invite, group.leave, group.kick, group.dissolve, group.transfer]
description: content.system.kind,服务端系统消息
MomentKind:
type: integer
enum: [1, 2, 3]
description: moment_posts.kind。1纯文 2图 3视频
MomentVisibility:
type: integer
enum: [0, 1, 2, 3]
description: moment_posts.visibility。0好友公开 1私密 2白名单 3黑名单
MomentStatus:
type: integer
enum: [0, 1]
description: moment_posts.status。0正常 1已删
MomentNoticeKind:
type: integer
enum: [1, 2, 3]
description: moment_notices.kind。1赞 2评论 3回复
MomentVisibleDays:
type: integer
enum: [0, 3, 30, 180]
description: moment_settings.visible_days。0全部 3/30/180 天
SquareChannel:
type: string
enum: [square, squarenovel, squaresteps, squarehealth]
description: 广场实例路径段;与 t_square_posts.channel 一致
SquareCategory:
type: string
enum: [recommend, latest, hot, nearby, funny, beauty]
description: |
Feed 筛选。recommend/latest/hot/nearby 为排序模式;funny/beauty 为帖标签(发帖可写)。
PublishSquareRequest:
allOf:
- $ref: "#/components/schemas/PublishMomentRequest"
- type: object
properties:
category:
$ref: "#/components/schemas/SquareCategory"
description: 仅 funny/beauty;发帖不可传 feed 模式值
PublishMomentRequest:
type: object
properties:
text:
type: string
maxLength: 1000
images:
type: array
maxItems: 9
items:
$ref: "#/components/schemas/MomentImage"
video:
$ref: "#/components/schemas/MomentVideo"
visibility:
$ref: "#/components/schemas/MomentVisibility"
audience_uids:
type: array
maxItems: 200
items:
type: integer
format: int64
poi:
$ref: "#/components/schemas/MomentPOI"
MomentImage:
type: object
required: [key]
properties:
key: { type: string }
mime: { type: string }
w: { type: integer }
h: { type: integer }
size: { type: integer, format: int64 }
thumb_key: { type: string }
orig_key: { type: string }
MomentVideo:
type: object
required: [key, duration_ms]
properties:
key: { type: string }
mime: { type: string }
w: { type: integer }
h: { type: integer }
duration_ms: { type: integer, maximum: 30000 }
size: { type: integer, format: int64 }
cover_key: { type: string }
MomentPOI:
type: object
properties:
lat: { type: number, format: double }
lng: { type: number, format: double }
name: { type: string }
address: { type: string }
MomentReadRequest:
type: object
required: [post_id]
properties:
post_id: { type: integer, format: int64 }
MomentNoticeReadRequest:
type: object
properties:
cursor: { type: integer, format: int64 }
PatchMomentSettingsRequest:
type: object
properties:
cover_key: { type: string }
visible_days:
$ref: "#/components/schemas/MomentVisibleDays"
MomentHideRequest:
type: object
properties:
hide_him: { type: boolean }
hide_me: { type: boolean }
MomentCommentRequest:
type: object
required: [text]
properties:
text: { type: string, maxLength: 500 }
reply_to_id: { type: integer, format: int64 }
TcpCommand:
type: integer
enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
description: |
Frame.Command:1 AUTH 2 HEARTBEAT 3 GENERATE_MSG_ID 4 SEND_MESSAGE 5 SEND_ACK
6 MESSAGE 7 PUSH_ACK 8 READ 9 RECALL 10 SYNC_INBOX 11 SYNC_INBOX_FINISH
12 SYNC_MESSAGES 13 SYNC_MESSAGES_FINISH 14 KICK 15 DELETE_MESSAGE。
命令 8 READ 请求体不是 envelope:`conversation_id`,以及 `msg_id` 或 `last_msg_id`
(任一大于 0;两者都大于 0 时用 `msg_id`)。成功应答命令仍为 8,
Body 为 Envelope,`code` 为 `"0"`,`data` 为 `{"conversation_id","msg_id"}`,其中 `msg_id` 是
`t_conversation_members.last_read_msg_id`(只增不减)。服务端清该用户 Redis
`im:dialog:unreadcnt` 字段,并把会话从 `im:dialog:unread` 挪到 `im:dialog:read`。
水位前进时通知该用户其他在线设备(命令 8,不系统推送、不写 mailbox)。
缺 `conversation_id`,或两个消息 id 都小于等于 0 时,`code` 为 `40001`。
EnvelopeCode:
type: string
enum: ["0", "40001", "40002", "40003", "40004", "40005", "40101", "40102", "40103", "40301", "40302", "40303", "40401", "40402", "40901", "42901", "42902", "42903", "49901", "50001", "50002", "50003", "50301", "50401"]
description: |
envelope.code。成功为 "0"。
`40005` media_oversized:媒体分辨率/体积超限(开启 media.user_reject 时拒收)。
Envelope:
type: object
required: [code, msg, request_id, data]
description: |
HTTP 与 TCP 应答 Body 的统一信封。业务成败看 `code` 字符串,不看 HTTP 状态(除 `/readyz` 503)。
失败时 `data` 为 `{}`。成功时 `data` 为对象或列表,不会是 `null`。
`request_id` 由服务端生成,客户端不用传;与 Header `X-Request-ID` 相同。
properties:
code:
allOf:
- $ref: "#/components/schemas/EnvelopeCode"
description: 字符串。`"0"` 成功,其它失败。不要用 JSON number。
example: "0"
msg:
type: string
description: 展示文案,随 `X-Language` / `Accept-Language` / TCP AUTH `language` 变化。
example: ok
request_id:
type: string
description: 服务端生成的本次请求 id(11 位 base62,`0-9A-Za-z`)。客户端不用传,从响应体读取。
example: 3H8kPq2mN1R
data:
type: object
additionalProperties: true
description: 成功为业务对象;失败为 {}
default: {}
AdminApiResponse:
type: object
description: popadmin / Java 风格管理端响应(非 V3 envelope)
properties:
success: { type: boolean }
message: { type: string }
data: {}
MusicTrack:
type: object
description: |
曲目。管理端可 PATCH 改 title/requireMember/sortOrder/enabled/categoryIds;
封面/背景/音频分别用 POST .../cover|background|audio(multipart `file`)上传或替换。
properties:
id: { type: integer, format: int64 }
title: { type: string }
coverKey: { type: string }
audioKey: { type: string }
backgroundKey: { type: string, description: 背景图 object_key;可空 }
coverUrl: { type: string, description: sparkr 公网 URL;可空直至上传 }
audioUrl: { type: string, description: sparkr 公网 URL;可空直至上传 }
backgroundUrl: { type: string, description: 背景图 sparkr 公网 URL;可空 }
requireMember: { type: boolean, description: true=需会员 }
sortOrder: { type: integer, description: 越小越靠前 }
enabled: { type: boolean, description: true=上架 }
categoryIds:
type: array
items: { type: integer, format: int64 }
description: 归属的分类 id 列表(总分类与/或子分类,多对多)
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
MusicCategory:
type: object
properties:
id: { type: integer, format: int64 }
parentId: { type: integer, format: int64, nullable: true, description: null=总分类 }
name: { type: string }
sortOrder: { type: integer }
enabled: { type: boolean }
children:
type: array
items:
$ref: "#/components/schemas/MusicCategory"
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
MusicCategoryUpsertRequest:
type: object
properties:
name: { type: string }
parentId: { type: integer, format: int64, nullable: true }
sortOrder: { type: integer }
enabled: { type: boolean }
MusicSettings:
type: object
properties:
memberPreviewSeconds:
type: integer
minimum: 0
maximum: 3600
description: 会员曲免费试听秒数(全局)
MusicPlaylist:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/MusicTrack"
categories:
type: array
items:
$ref: "#/components/schemas/MusicCategory"
description: 启用中的分类树
memberPreviewSeconds: { type: integer }
MusicPlaylistGroup:
type: object
description: 一个分区(叶子分类)及其曲目
properties:
categoryId: { type: integer, format: int64 }
name: { type: string }
parentId: { type: integer, format: int64, nullable: true }
items:
type: array
items:
$ref: "#/components/schemas/MusicTrack"
MusicGroupedPlaylist:
type: object
properties:
groups:
type: array
items:
$ref: "#/components/schemas/MusicPlaylistGroup"
memberPreviewSeconds: { type: integer }
SensitiveCategory:
type: integer
enum: [1, 2, 3, 4, 5, 6, 7, 8, 9]
description: |
1=涉黑 2=涉毒 3=涉黄 4=涉赌 5=涉政 6=宗教 7=辱骂 8=广告 9=其他
SensitiveAction:
type: integer
enum: [1, 2, 3, 4, 5, 6]
description: |
1=log 2=replace 3=reject 4=hide(v1 按 reject)5=mute 6=disable
SensitiveWord:
type: object
properties:
id: { type: integer, format: int64 }
word_raw: { type: string, description: 运营录入原文 }
word: { type: string, description: 规范化词面,进自动机 }
category: { $ref: "#/components/schemas/SensitiveCategory" }
action: { $ref: "#/components/schemas/SensitiveAction" }
mute_seconds: { type: integer, description: action=mute 时生效 }
enabled: { type: boolean }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
SensitiveWordCreateRequest:
type: object
required: [word, category, action]
properties:
word:
type: string
maxLength: 64
description: 词面原文(入库后 Fold 为 word 列)
category: { $ref: "#/components/schemas/SensitiveCategory" }
action: { $ref: "#/components/schemas/SensitiveAction" }
mute_seconds: { type: integer, minimum: 0, default: 0 }
enabled: { type: boolean, default: true }
SensitiveWordPatchRequest:
type: object
properties:
word: { type: string, maxLength: 64 }
category: { $ref: "#/components/schemas/SensitiveCategory" }
action: { $ref: "#/components/schemas/SensitiveAction" }
mute_seconds: { type: integer, minimum: 0 }
enabled: { type: boolean }
SensitiveHit:
type: object
properties:
id: { type: integer, format: int64 }
user_id: { type: integer, format: int64 }
conversation_id: { type: integer, format: int64 }
word_id: { type: integer, format: int64 }
category: { $ref: "#/components/schemas/SensitiveCategory" }
action: { $ref: "#/components/schemas/SensitiveAction" }
excerpt: { type: string, maxLength: 128 }
real_ip: { type: string }
created_at: { type: string, format: date-time }
MusicUpsertRequest:
type: object
description: |
管理端新建/更新曲目元数据。上传封面/背景/音频请用独立 multipart 接口。
`categoryIds` 传入时全量替换曲目分类绑定。
properties:
title: { type: string }
requireMember: { type: boolean }
sortOrder: { type: integer }
enabled: { type: boolean }
categoryIds:
type: array
items: { type: integer, format: int64 }
FsnGenre:
type: object
properties:
id: { type: integer, format: int64 }
slug: { type: string }
name: { type: string }
description: { type: string }
parentId: { type: integer, format: int64, nullable: true }
parentSlug: { type: string, nullable: true }
sortOrder: { type: integer }
children:
type: array
items:
$ref: "#/components/schemas/FsnGenre"
FsnAdminGenre:
allOf:
- $ref: "#/components/schemas/FsnGenre"
- type: object
properties:
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
FsnNovelCard:
type: object
properties:
id: { type: integer, format: int64 }
slug: { type: string }
title: { type: string }
author: { type: string }
synopsis: { type: string }
coverUrl: { type: string }
status: { type: string }
language: { type: string }
genres: { type: array, items: { type: string } }
rating: { type: number }
views: { type: integer, format: int64 }
chapterCount: { type: integer }
featured: { type: boolean }
published: { type: boolean }
latestChapterAt: { type: string, format: date-time, nullable: true }
createdAt: { type: string, format: date-time }
FsnAdminNovelCard:
allOf:
- $ref: "#/components/schemas/FsnNovelCard"
- type: object
properties:
updatedAt: { type: string, format: date-time }
FsnNovelPage:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/FsnNovelCard"
page: { type: integer }
size: { type: integer }
total: { type: integer, format: int64 }
totalPages: { type: integer }
FsnHome:
type: object
properties:
featured:
type: array
items:
$ref: "#/components/schemas/FsnNovelCard"
popular:
type: array
items:
$ref: "#/components/schemas/FsnNovelCard"
newReleases:
type: array
items:
$ref: "#/components/schemas/FsnNovelCard"
recentlyUpdated:
type: array
items:
$ref: "#/components/schemas/FsnNovelCard"
genres:
type: array
items:
$ref: "#/components/schemas/FsnGenre"
FsnChapterMeta:
type: object
properties:
id: { type: integer, format: int64 }
chapterNumber: { type: integer }
slug: { type: string }
title: { type: string }
publishedAt: { type: string, format: date-time, nullable: true }
FsnChapterDetail:
type: object
properties:
id: { type: integer, format: int64 }
novelId: { type: integer, format: int64 }
novelSlug: { type: string }
novelTitle: { type: string }
chapterNumber: { type: integer }
slug: { type: string }
title: { type: string }
content: { type: string }
publishedAt: { type: string, format: date-time, nullable: true }
prev:
$ref: "#/components/schemas/FsnChapterMeta"
next:
$ref: "#/components/schemas/FsnChapterMeta"
FsnNovelUpsert:
type: object
properties:
title: { type: string }
slug: { type: string }
author: { type: string }
synopsis: { type: string }
coverUrl: { type: string }
status: { type: string }
language: { type: string }
genres: { type: array, items: { type: string } }
rating: { type: number }
featured: { type: boolean }
published: { type: boolean, description: 上下架 }
FsnChapterUpsert:
type: object
properties:
chapterNumber: { type: integer }
title: { type: string }
slug: { type: string }
content: { type: string }
published: { type: boolean }
FsnGenreUpsert:
type: object
properties:
name: { type: string }
slug: { type: string }
description: { type: string }
parentId: { type: integer, format: int64, nullable: true }
parentSlug: { type: string, nullable: true }
sortOrder: { type: integer }
EnumsEnvelope:
type: object
required: [code, msg, request_id, data]
properties:
code:
$ref: "#/components/schemas/EnvelopeCode"
msg:
type: string
request_id:
type: string
data:
$ref: "#/components/schemas/EnumCatalog"
EnumItem:
type: object
required: [value, name, label]
properties:
value:
description: 与 JSON/TCP/SQL 一致。数字集合为 integer,字符串集合为 string。
oneOf:
- type: integer
- type: string
name:
type: string
description: 稳定英文标识,不随语言变化
label:
type: string
description: 当前请求语言下的展示文案
EnumCatalog:
type: object
properties:
gender:
type: array
items:
$ref: "#/components/schemas/EnumItem"
user_status:
type: array
items:
$ref: "#/components/schemas/EnumItem"
conversation_type:
type: array
items:
$ref: "#/components/schemas/EnumItem"
message_kind:
type: array
items:
$ref: "#/components/schemas/EnumItem"
message_type:
type: array
items:
$ref: "#/components/schemas/EnumItem"
message_status:
type: array
items:
$ref: "#/components/schemas/EnumItem"
relation_kind:
type: array
items:
$ref: "#/components/schemas/EnumItem"
friend_request_status:
type: array
items:
$ref: "#/components/schemas/EnumItem"
friend_source:
type: array
items:
$ref: "#/components/schemas/EnumItem"
report_reason:
type: array
items:
$ref: "#/components/schemas/EnumItem"
group_status:
type: array
items:
$ref: "#/components/schemas/EnumItem"
group_role:
type: array
items:
$ref: "#/components/schemas/EnumItem"
mailbox_event_type:
type: array
items:
$ref: "#/components/schemas/EnumItem"
tcp_command:
type: array
items:
$ref: "#/components/schemas/EnumItem"
envelope_code:
type: array
items:
$ref: "#/components/schemas/EnumItem"
device_type:
type: array
items:
$ref: "#/components/schemas/EnumItem"
language:
type: array
items:
$ref: "#/components/schemas/EnumItem"
upload_purpose:
type: array
items:
$ref: "#/components/schemas/EnumItem"
system_kind:
type: array
items:
$ref: "#/components/schemas/EnumItem"
SendVerificationCodeRequest:
type: object
required: [purpose, target]
properties:
purpose:
$ref: "#/components/schemas/VerificationPurpose"
target:
type: string
description: 邮箱或大陆 11 位手机号(可带 +86)
DeleteAccountRequest:
type: object
properties:
verify_code:
type: string
description: 注销验证码(purpose=account_delete)。测试账号可省略。
VerificationPurpose:
type: integer
enum: [1, 2]
description: 1=登录 2=注销账号
LoginRequest:
type: object
required: [email, verify_code, app_id, device_type]
description: |
Breaking:删除客户端 `device_id`;新增 `app_id`、整数 `device_type`、物理指纹字段。
指纹:`payload="{device_type}:{idfv|android_id}"` → `device_id=uint64(SHA256(HEX(SHA256(payload)))[0:8])`(非 0)。
properties:
email:
type: string
format: email
verify_code:
type: string
description: 邮箱验证码。测试账号 test01@spark.com~test100@spark.com 可任意填写。
app_id:
type: integer
minimum: 1
description: 当前客户端 App,见 meta `app_id` 枚举(JWT claim `aid`)。
device_type:
$ref: "#/components/schemas/DeviceType"
platform:
type: integer
format: int32
enum: [1, 2, 3, 4, 5, 6]
description: 与 device_type 相同枚举;仅当未传 device_type 时可作别名
device_name:
type: string
maxLength: 128
idfv:
type: string
maxLength: 36
description: iOS / macOS 必填(物理设备标识,服务端据此生成 device_id)。
android_id:
type: string
maxLength: 20
description: Android / Harmony 必填。
LoginData:
type: object
required: [access_token, refresh_token, expires_in, refresh_expires_in, user_id, device_id, app_id, spark_id]
properties:
access_token:
type: string
description: JWT。claims 含 sub、did、aid、use=access
refresh_token:
type: string
description: JWT。claims 含 sub、did、aid、use=refresh;刷新后 aid 不变
expires_in:
type: integer
format: int64
description: access TTL in seconds (default 604800 = 7 days)
example: 604800
refresh_expires_in:
type: integer
format: int64
description: refresh TTL in seconds (default 2592000 = 30 days)
example: 2592000
user_id:
type: integer
format: int64
description: Snowflake user id(= JWT sub)
device_id:
type: integer
format: int64
description: 服务端按物理指纹生成的设备 id(JWT `did`)。客户端持久化该值,勿自行生成。
app_id:
type: integer
description: 与 JWT claim `aid` 一致
spark_id:
type: string
description: Public handle, unique and user-changeable. New and updated values are 8-16 characters.
RefreshRequest:
type: object
required: [refresh_token]
properties:
refresh_token:
type: string
PatchMeRequest:
type: object
properties:
spark_id:
type: string
minLength: 8
maxLength: 16
pattern: "^[a-zA-Z][a-zA-Z0-9_]{7,15}$"
nickname:
type: string
minLength: 1
maxLength: 64
avatar:
type: string
maxLength: 512
description: |
对象存储 key。仅 `perm/avatar/`、`perm/group/` 可匿名 GET(桶本身不公开)。
URL:`{public_endpoint}/spark/{avatar}`,Nginx 也可省略桶名 `{public_endpoint}/{avatar}`。
`chat/`、`perm/fav/`、`perm/moment/` 必须 Presigned GET。
gender:
$ref: "#/components/schemas/Gender"
birthday:
type: string
description: YYYY-MM-DD;空字符串表示清除
example: "1994-05-01"
FriendApplyRequest:
type: object
properties:
uid:
type: integer
format: int64
email:
type: string
format: email
spark_id:
type: string
qrcode:
type: string
description: 个人二维码扫描票据(shortid),不含 uid
reason:
type: string
maxLength: 128
remark:
type: string
maxLength: 64
FriendApplyData:
type: object
required: [status, uid]
properties:
status:
$ref: "#/components/schemas/FriendRequestStatus"
request_id:
type: integer
format: int64
description: status=1 时返回,待处理申请 id
uid:
type: integer
format: int64
description: 对方 Snowflake uid
FriendAcceptRequest:
type: object
required: [request_id]
properties:
request_id:
type: integer
format: int64
remark:
type: string
maxLength: 64
FriendRemarkRequest:
type: object
properties:
remark:
type: string
maxLength: 64
FriendReportRequest:
type: object
required: [reason]
properties:
reason:
$ref: "#/components/schemas/ReportReason"
detail:
type: string
maxLength: 512
CreateUploadRequest:
type: object
required: [purpose, mime, size]
properties:
purpose:
$ref: "#/components/schemas/UploadPurpose"
mime:
type: string
example: image/jpeg
description: |
avatar / group_avatar / chat_image 仅 image/jpeg、image/jpg、image/png。
iOS HEIC、Android WebP 须客户端转成 JPEG/PNG 再申请凭证。
size:
type: integer
format: int64
minimum: 1
group_id:
type: integer
format: int64
minimum: 1
description: purpose=group_avatar 时必填,为自增群号
CreateUploadBatchRequest:
type: object
required: [items]
properties:
items:
type: array
minItems: 1
maxItems: 3
items:
$ref: "#/components/schemas/CreateUploadRequest"
CreateDownloadRequest:
type: object
required: [object_key]
properties:
object_key:
type: string
maxLength: 512
CreateDownloadBatchRequest:
type: object
required: [object_keys]
properties:
object_keys:
type: array
minItems: 1
maxItems: 9
items:
type: string
maxLength: 512
SendMessageRequest:
type: object
required: [client_msg_id, content]
properties:
conversation_id:
type: integer
format: int64
peer_id:
type: integer
format: int64
description: 单聊对方 uid;与 conversation_id 至少填一个
client_msg_id:
type: string
maxLength: 64
msg_id:
type: integer
format: int64
description: 可选,须为 ±5 分钟内的 Snowflake
content:
type: object
required: [t]
properties:
t:
$ref: "#/components/schemas/MessageType"
reply_to:
type: integer
format: int64
description: 被回复消息 MsgId
text:
type: object
properties:
body:
type: string
image:
type: object
description: |
t=2。点开看 `key`(压缩预览);气泡用 `thumb_key`;`orig_key` 有则显示「查看原图」。
三档由客户端压成 JPEG/PNG 后 `POST /v1/files/batch` 直传。服务端不转码;不要传 HEIC/WebP。
required: [key]
properties:
key:
type: string
description: 点开查看用的压缩图 object_key
thumb_key:
type: string
description: 气泡缩略图,可省略
orig_key:
type: string
description: 原图 object_key;空则没有「查看原图」
orig_size:
type: integer
format: int64
description: 原图像素文件大小,供「查看原图 3.2MB」按钮
orig_w:
type: integer
orig_h:
type: integer
mime:
type: string
w:
type: integer
h:
type: integer
size:
type: integer
format: int64
at_all:
type: boolean
description: "@所有人,仅群主/管理员"
at_uids:
type: array
maxItems: 50
items:
type: integer
format: int64
description: "@的群成员 uid"
system:
type: object
properties:
kind:
$ref: "#/components/schemas/SystemKind"
body:
type: string
CreateFavoriteRequest:
type: object
required: [msg_id]
properties:
msg_id:
type: integer
format: int64
minimum: 1
CreateGroupRequest:
type: object
required: [name, member_uids]
properties:
name:
type: string
minLength: 1
maxLength: 32
member_uids:
type: array
minItems: 1
maxItems: 199
items:
type: integer
format: int64
UpdateGroupRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 32
avatar:
type: string
muted_all:
type: boolean
announcement:
type: string
maxLength: 1024
InviteGroupMembersRequest:
type: object
required: [uids]
properties:
uids:
type: array
minItems: 1
items:
type: integer
format: int64
UpdateGroupMemberRequest:
type: object
properties:
role:
allOf:
- $ref: "#/components/schemas/GroupRole"
description: 仅 2=管理员 3=普通成员;群主只能通过转让变更
speak_muted:
type: boolean
nickname:
type: string
maxLength: 32
remark:
type: string
maxLength: 32
description: 对该群的备注,仅自己可改
TransferGroupRequest:
type: object
required: [uid]
properties:
uid:
type: integer
format: int64
LoginEnvelope:
type: object
required: [code, msg, request_id, data]
properties:
code:
type: string
msg:
type: string
request_id:
type: string
data:
$ref: "#/components/schemas/LoginData"
AuthData:
type: object
properties:
user_id:
type: integer
format: int64
device_id:
type: integer
format: int64
MsgIDData:
type: object
properties:
msg_id:
type: integer
format: int64
headers:
X-Request-ID:
schema:
type: string
description: 服务端生成的本次请求 id。与响应体 `request_id` 相同。客户端不用传。
X-Device-ID:
schema:
type: string
description: Optional. Must match JWT did when present(服务端生成的 device_id)。
X-App-ID:
schema:
type: integer
description: Optional. Must match JWT aid(app_id) when present.
X-App-Version:
schema:
type: string
Accept-Language:
schema:
$ref: "#/components/schemas/Language"
example: zh-CN
description: 用户语言。也可用 X-Language。缺省 zh-CN。
X-Language:
schema:
$ref: "#/components/schemas/Language"
description: 覆盖 Accept-Language 的简洁语言标签。
X-Real-IP:
schema:
type: string
description: Nginx PROXY / SNI 写入的客户端真实 IP,进入 reqctx.RealIP。
parameters:
Authorization:
in: header
name: Authorization
required: true
schema:
type: string
example: Bearer eyJhbGciOiJIUzI1NiJ9...
description: Header。`Bearer <access_token>`。登录/发码/刷新/探活/文档/枚举不要带。
X-Device-ID:
in: header
name: X-Device-ID
required: false
schema:
type: string
example: "7"
description: Header。十进制 device_id,须等于登录响应 / JWT did(服务端按物理指纹生成)。不放 Body。
X-App-ID:
in: header
name: X-App-ID
required: false
schema:
type: integer
example: 1
description: Header。若传须等于 JWT aid(app_id)。不放业务 Body。
X-App-Version:
in: header
name: X-App-Version
required: false
schema:
type: string
example: 1.0.0
description: Header。客户端版本。登录时也可在 Body 带 app_version;此后用本 Header。
Accept-Language:
in: header
name: Accept-Language
required: false
schema:
$ref: "#/components/schemas/Language"
description: Header。用户语言。缺省 zh-CN。无 X-Language 时生效。
X-Language:
in: header
name: X-Language
required: false
schema:
$ref: "#/components/schemas/Language"
description: Header。覆盖 Accept-Language。决定 envelope.msg 语言。不放 Body。
FriendUID:
in: path
name: uid
required: true
schema:
type: integer
format: int64
description: 对方 Snowflake uid。
FavoriteID:
in: path
name: id
required: true
schema:
type: integer
format: int64
description: 收藏 Snowflake id。
MomentID:
in: path
name: id
required: true
schema:
type: integer
format: int64
description: 朋友圈 Snowflake post_id。
SquareChannel:
in: path
name: square_channel
required: true
schema:
$ref: "#/components/schemas/SquareChannel"
description: 广场实例 square / squarenovel / squaresteps / squarehealth
SquarePostID:
in: path
name: id
required: true
schema:
type: integer
format: int64
description: 广场 Snowflake post_id。
GroupID:
in: path
name: id
required: true
schema:
type: integer
format: int64
description: 自增群号(不是 conversation_id)。
MessageID:
in: path
name: id
required: true
schema:
type: integer
format: int64
description: Snowflake MsgId。