为 Spark Messenger 主路径补齐 POST /v1/friends/requests/reject,并落地法律协议/反馈/订阅/密码登录及管理页,同步 OpenAPI 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
4141 lines
139 KiB
YAML
4141 lines
139 KiB
YAML
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: client
|
||
description: 客户端配置(协议/反馈/订阅)
|
||
- 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/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: |
|
||
可匿名提交。若带 Bearer 会关联 user_id。Body:`content` 必填;可选 `email`、`app_version`。
|
||
operationId: submitFeedback
|
||
parameters:
|
||
- $ref: "#/components/parameters/X-Language"
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [content]
|
||
properties:
|
||
content: { type: string, minLength: 2, maxLength: 2000 }
|
||
email: { type: string, format: email }
|
||
app_version: { type: string }
|
||
responses:
|
||
"200":
|
||
description: data 为反馈记录(含 id)
|
||
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)。
|
||
公开接口;无库数据时回退默认四档。
|
||
operationId: subscriptionPlans
|
||
parameters:
|
||
- $ref: "#/components/parameters/X-Language"
|
||
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/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":
|
||
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。写入 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。写入 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"
|
||
|
||
# —— 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/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。
|