spark-server/internal/transport/http/docs/openapi.yaml
DESKTOP-L8OUL9F\gfl0034 d3a68c3cad feat(music): 分类多对多、后台完整编辑与 OpenAPI
新增总/子分类与曲目绑定;playlist 可按 categoryId 筛选并返回分类树;
popadmin 支持行内改曲名/排序/上下架/会员开关与封面背景音频上传;文档同步。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 10:38:06 +08:00

3944 lines
133 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

openapi: 3.0.3
info:
title: 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`、`GET /v1/music/playlist`、`GET /v1/music/categories`、`GET /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>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`。
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: auth
- name: users
- name: friends
- name: groups
- name: conversations
- name: messages
- name: files
- name: favorites
- name: moments
- name: square
- name: music
description: |
运营音乐列表。客户端公开读 `GET /v1/music/playlist`(可带 `categoryId`)、`GET /v1/music/categories`、`GET /v1/music/settings`(无需登录,V3 envelope)。
playlist 返回曲目、`categories` 分类树与 `memberPreviewSeconds`;曲目含 `backgroundUrl`、`requireMember`、`categoryIds`。
- name: admin-music
description: |
popadmin 音乐管理:`/api/admin/spark/music*`(`/api/admin/pop/music*` 别名)。
曲目 CRUD、封面/背景/音频上传、分类 CRUD、曲目分类绑定(多对多)、全局试听秒数。
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/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/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:
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/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/posts:
get:
tags: [square]
summary: 全站广场 Feed
description: |
`cursor` 为上一页最后一条 `post_id`,0 表示最新。仅 `visibility` 公开与不给谁看、且审核通过的帖;
读时按拉黑、黑名单过滤。评论随帖详情返回,无单独评论列表接口。
operationId: listSquareFeed
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: [square]
summary: 发广场帖
description: |
内容与朋友圈相同。媒体先 `POST /v1/files`(`square_image` / `square_video`)再填 key。
`visibility`:0 全站公开 1 仅自己 2 白名单 3 黑名单。2/3 需 `audience_uids`(双向好友,最多 200)。
operationId: publishSquarePost
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PublishMomentRequest"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/square/posts/{id}:
get:
tags: [square]
summary: 单条广场帖
operationId: getSquarePost
parameters:
- $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/SquarePostID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/square/posts/{id}/like:
put:
tags: [square]
summary: 点赞
operationId: likeSquarePost
parameters:
- $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/SquarePostID"
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/square/posts/{id}/comments:
post:
tags: [square]
summary: 评论或回复
operationId: commentSquarePost
parameters:
- $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/posts/{id}/comments/{cid}:
delete:
tags: [square]
summary: 删除评论
operationId: deleteSquareComment
parameters:
- $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/users/{uid}/posts:
get:
tags: [square]
summary: 某用户广场帖
description: 按当前浏览者与帖可见性、拉黑过滤;看自己可见私密与白名单帖。
operationId: listSquareUserPosts
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/square/notices:
get:
tags: [square]
summary: 赞评通知列表
operationId: listSquareNotices
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/square/notices/unread:
get:
tags: [square]
summary: 广场互动未读角标
operationId: squareNoticeUnread
responses:
"200":
description: 统一 envelope
content:
application/json:
schema:
$ref: "#/components/schemas/Envelope"
/v1/square/notices/read:
post:
tags: [square]
summary: 通知标已读
operationId: readSquareNotices
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` 时仅返回归属该分类或其子分类的曲目(多对多)。
operationId: getMusicPlaylist
security: []
parameters:
- name: categoryId
in: query
required: false
schema: { type: integer, format: int64, minimum: 0 }
description: 按分类筛选;0 或不传=全部上架曲目
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/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":
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":
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":
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":
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":
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":
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":
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":
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":
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。写入 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":
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。写入 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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
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":
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
/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]
description: 好友添加来源。0未知 1二维码 2邮箱 3spark_id
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 天
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
EnvelopeCode:
type: string
enum: ["0", "40001", "40002", "40003", "40004", "40101", "40102", "40301", "40401", "40402", "40901", "42901", "42902", "42903", "49901", "50001", "50002", "50003", "50301", "50401"]
description: envelope.code。成功为 "0"
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 }
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。
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。