# imsvr C 端接口文档

> 面向客户端 SDK（Flutter / iOS / Android / PC / Web）对接。所有接口均为 `POST + JSON body`，路由前缀 `/v1/sdk/...`（少量挂在 `/v1/...` 根下）。当文档与代码冲突，以代码为准。

---

## 通用约定

### 请求头

| Header | 必填 | 说明 |
|---|---|---|
| `X-Token` | ✓ | 登录拿到的 token；未登录接口（如 `/v1/login`、`/v1/captcha/get`）不需要 |
| `X-Lang` |  | `zh` / `en` / `ja` / `vi` / `es` / `pt`，默认 `zh` |
| `KK-Version` |  | 客户端版本 |

### 响应统一结构

```json
{
  "code":   0,
  "msg":    "",
  "data":   { ... },
  "config": { ... }
}
```

| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | int | `0`=成功；非 0 为业务/系统错误码 |
| `msg` | string | 错误时是本地化文案 |
| `data` | object | 业务数据，无数据时省略 |
| `config` | object | 可选附加配置 |

### 通用错误码

| code | 含义 |
|---|---|
| 0 | 成功 |
| 400 | 参数错误 |
| 401 / 403 | 鉴权失败 / 无权限 |
| 426 | 客户端需升级 |
| 500 | 内部错误 |
| 1001 | 业务规则不满足（如企业关闭/删除、密码错误）|
| 1007 | 群权限不足（不在群、禁言、管理员设置禁止某类型）|
| 1010 | 跨 appkey 操作被拒 |
| 1013 | 密码错误超限 |
| 2010 | 单聊关系异常（好友删除、被对方删除）|
| 2011 | 黑名单关系 |

---



## 应用与推送

### /v1/sdk/app/latest 查询客户端最新版本

依据当前请求的 `KK-Version` 与传入的 `build` 比较服务端配置的最新版本，提示是否需要升级。该接口免登录。

#### 接口地址

```
POST /v1/sdk/app/latest
Content-Type: application/json
```

#### 请求头

| Header | 必填 | 说明 |
|---|---|---|
| `KK-Version` | ✓ | 客户端版本（用来判断是否需要升级） |
| `X-Token` |  | 不需要 |

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `build` | string |  | 客户端打包号（字符串形式数字） |

#### 请求示例

```json
{
  "build": "1024"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `hasUpdate` | bool | `true`=有新版本可升级 |
| `version` | string | 最新版本号 |
| `downloadUri` | string | 下载地址 |
| `description` | string | 更新说明 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "hasUpdate": true,
    "version": "1.6.0",
    "downloadUri": "https://download.example.com/app-1.6.0.apk",
    "description": "修复若干已知问题"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 版本不存在 | 服务端未配置该平台版本 |
| 500 | (内部 err) | DB 查询失败、KK-Version 解析失败 |

---

### /v1/sdk/push/register/report 极光推送设备 ID 上报

客户端拿到极光 SDK 的 `registration_id` 后回传服务端，建立 uid + 设备 + registrationId 的映射，用于离线推送下发。

#### 接口地址

```
POST /v1/sdk/push/register/report
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `registration_id` | string | ✓ | 极光 SDK 返回的 RegistrationId |

#### 请求示例

```json
{
  "registration_id": "1a0018970abc1234567"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 写入失败 |

---

## 验证码与 OTP

### /v1/captcha/get 获取图形验证码

获取一次性图形验证码（base64 原始数据），返回的 `token` 用于后续校验 / 关联 OTP。免登录。

#### 接口地址

```
POST /v1/captcha/get
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `lang` | string |  | 文案语言，默认服务端配置 |

#### 请求示例

```json
{
  "lang": "zh"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `token` | string | 本次验证标识，校验时回传 |
| `raw` | string | 图片原始数据（base64） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "token": "cap_5f4a3b2c1d",
    "raw": "data:image/png;base64,iVBORw0KGgo..."
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | 生成验证码失败 |

---

### /v1/captcha/chk 校验图形验证码

校验用户输入的验证码内容。成功后该 `token` 仍可被后续 OTP 接口使用（一次性消费由 OTP 那侧负责）。免登录。

#### 接口地址

```
POST /v1/captcha/chk
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `token` | string | ✓ | 来自 `/v1/captcha/get` 返回的 token |
| `code` | string | ✓ | 用户输入的验证码 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "token": "cap_5f4a3b2c1d",
  "code": "8h3n",
  "lang": "zh"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | token / code 为空 |
| 400 | 图形验证码验证失败 | 验证码错误或已过期 |
| 500 | (内部 err) | 校验过程异常 |

---

### /v1/sdk/otp/send 发送短信/邮件验证码

按业务类型 `send_type` 给手机或邮箱发送 6 位 OTP。需要先通过 `/v1/captcha/chk` 拿到 `captchaToken`，并通过 `/v1/chkacct` 拿到 `eventTag` 表明账号身份。免登录。

#### 接口地址

```
POST /v1/sdk/otp/send
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码（服务器编号），决定所属 appkey |
| `captchaToken` | string | ✓ | 来自图形验证码 `/v1/captcha/get` 的 token |
| `eventTag` | string | ✓ | 来自 `/v1/chkacct` 返回的事件标识 |
| `send_type` | string | ✓ | 业务类型，见下表 |
| `byEmail` | bool |  | 工号账号时是否走邮箱通道（默认走手机） |
| `lang` | string |  | 文案语言 |

**`send_type` 枚举**

| 值 | 含义 | 用户必须存在 |
|---|---|---|
| `1` | 验证号码 | ✓ |
| `2` | 找回密码 | ✓ |
| `10` | 注册 | × |
| `12` | 登录 | ✓ |
| `13` | 注销 | ✓ |
| `14` | 设置号码 | × |
| `15` | 重置密码（密保流程） | ✓ |
| `16` | 更换号码 | × |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "captchaToken": "cap_5f4a3b2c1d",
  "eventTag": "evt_9a8b7c6d5e",
  "send_type": "10",
  "lang": "zh"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 邀请码为空 / 邀请码不存在 | vcode 为空或查不到 |
| 400 | 参数错误 | captchaToken / eventTag / send_type 缺失或不合法 |
| 400 | 图形验证码验证失败 | captchaToken 已失效或未通过 |
| 1000 | 用户已存在 | send_type 属于"必须不存在"（10/14/16）但账号已注册 |
| 1000 | 用户不存在 | send_type 属于"必须存在" 但账号未注册 |
| 1012 | 频率超限 / 当日次数用完 | IP / 账号 / 业务类型超频 |
| 1012 | 服务异常稍后重试 | 短信网关 / 邮件发送失败 |
| 1023 / 1024 | 用户未绑定/未验证手机号 | 走手机通道但未绑 |
| 1033 / 1034 | 用户未绑定/未验证邮箱 | 走邮箱通道但未绑 |
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 调试环境 `AppOTPSwitch=false` 时手机 OTP 固定 `666666`、邮件 OTP 固定 `888888`，方便测试
- 同一账号 + send_type 重新调用本接口会清空 `/v1/sdk/otp/verify` 的失败次数计数

---

### /v1/sdk/otp/verify 校验 OTP 并换取 otp_token

校验通过后返回一次性 `otp_token`，作为 `/v1/signup`、`/v1/loginotp`、`/v1/resetpwd` 等的入参证明用户掌控手机号 / 邮箱。免登录。

#### 接口地址

```
POST /v1/sdk/otp/verify
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `eventTag` | string | ✓ | 来自 `/v1/chkacct` 的事件标识 |
| `send_type` | string | ✓ | 与发送时保持一致，枚举见 `/v1/sdk/otp/send` |
| `code` | string | ✓ | 用户输入的验证码 |
| `byEmail` | bool |  | 工号账号是否走邮箱通道 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "eventTag": "evt_9a8b7c6d5e",
  "send_type": "10",
  "code": "666666"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `otp_token` | string | 业务后续接口（如 `/v1/signup`）回传的令牌 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "otp_token": "a1b2c3d4e5f6071829304a5b6c7d8e9f"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 邀请码为空 / 不存在 | vcode 异常 |
| 400 | 参数错误 | eventTag / send_type 异常 |
| 1000 | 用户已存在 / 用户不存在 | 与 `send_type` 期望不符 |
| 1013 | 验证次数超限 | 同账号 + 同 send_type 校验失败次数已超日限，需要重新发送 |
| 1014 | 验证码已过期或不存在 | 服务端未存到该 OTP |
| 1015 | 验证码错误 | 输入与服务端 OTP 不匹配 |
| 1023 / 1024 / 1033 / 1034 | 同 `/v1/sdk/otp/send` | 手机/邮箱未绑定或未验证 |
| 500 | (内部 err) | DB / Redis 失败 |

---

## 登录注册

### /v1/invite 通过邀请码拉取服务配置

客户端拿到邀请码后第一次调用，确定该商户对应的 `appkey`、各类后端端口路由及功能开关。免登录。

#### 接口地址

```
POST /v1/invite
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `appkey` | string | 子应用 appkey |
| `port_api` | int64 | API 端口（兼容老字段，等同于 port_tapi） |
| `port_sync` | int64 | Sync 端口（兼容老字段，等同于 port_tsync） |
| `port_tapi` | int64 | T 端 API 端口 |
| `port_tsync` | int64 | T 端 Sync 端口 |
| `port_sapi` | int64 | S 端 API 端口 |
| `route_tapi` | string | T 端 API 路由地址 |
| `route_tsync` | string | T 端 Sync 路由地址 |
| `route_sapi` | string | S 端 API 路由地址 |
| `dynamic` | int64 | 动态路由开关 |
| `dsource` | string | 数据来源标识 |
| `cs_link` | string | 客服跳转地址 |
| `name` | string | 企业名称 |
| `switch_regist` | int32 | 注册开关 `1`=开 |
| `switch_rtc` | int32 | RTC 开关 |
| `switch_tab_main` | int32 | 主页 Tab 开关 |
| `switch_tab_mine` | int32 | 我的 Tab 开关 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "appkey": "app_demo",
    "port_api": 3000,
    "port_sync": 3001,
    "port_tapi": 3000,
    "port_tsync": 3001,
    "port_sapi": 4000,
    "route_tapi": "https://t.example.com",
    "route_tsync": "wss://t.example.com",
    "route_sapi": "https://s.example.com",
    "dynamic": 1,
    "dsource": "default",
    "cs_link": "https://cs.example.com",
    "name": "示例企业",
    "switch_regist": 1,
    "switch_rtc": 1,
    "switch_tab_main": 1,
    "switch_tab_mine": 1
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 请填写邀请码 | vcode 为空 |
| 400 | 邀请码不存在 | vcode 在 invite_map 或 business 表中查不到 |

---

### /v1/chkacct 检查账号是否存在

输入账号（用户名 / 手机 / 邮箱），返回是否已注册、绑定情况、以及后续 OTP 流程要用的 `eventTag`。免登录。

#### 接口地址

```
POST /v1/chkacct
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `account` | string | ✓ | 账号，自动识别用户名 / 手机号 / 邮箱 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "account": "user@example.com"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `exist` | bool | 账号是否已注册 |
| `uid` | int64 | 已注册时返回 uid |
| `boundTelno` | bool | 是否已绑定且验证过手机号 |
| `boundEmail` | bool | 是否已绑定且验证过邮箱 |
| `boundSecurity` | bool | 是否已设置密保问题 |
| `eventTag` | string | 后续 OTP / 重置密码流程的事件标识，5 分钟有效 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "exist": true,
    "uid": 10000245,
    "boundTelno": false,
    "boundEmail": true,
    "boundSecurity": false,
    "eventTag": "evt_9a8b7c6d5e"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 请填写邀请码 | vcode 为空 |
| 400 | 邀请码不存在 | vcode 查不到 |
| 400 | 账号格式不正确 | 既不是手机号也不是邮箱也不是工号格式 |
| 400 | 被风控 | 同 IP 当日操作过多 |
| 500 | (内部 err) | DB 失败 |

---

### /v1/signup 注册

手机号 / 邮箱方式注册需要先通过 `/v1/sdk/otp/verify` 拿到 `sms_token`；工号方式（暂未启用对外）不传该字段。免登录。

#### 接口地址

```
POST /v1/signup
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `deviceId` | string | ✓ | 设备号，长度 ≥ 6 |
| `account` | string | ✓ | 注册账号（手机 / 邮箱） |
| `passwd` | string | ✓ | 密码，MD5-32 小写 |
| `aggrement` | bool | ✓ | 是否已勾选用户协议，必须为 `true` |
| `sms_token` | string |  | 来自 `/v1/sdk/otp/verify` 的 otp_token；手机/邮箱注册必传 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "deviceId": "device_abcdef1234",
  "account": "13800138000",
  "passwd": "e10adc3949ba59abbe56e057f20f883e",
  "aggrement": true,
  "sms_token": "a1b2c3d4e5f6071829304a5b6c7d8e9f"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 设备号格式不正确 | deviceId 长度 < 6 |
| 400 | 请填邀请码 / 邀请码不存在 | vcode 异常 |
| 400 | 请阅读并同意用户协议 | `aggrement` ≠ true |
| 400 | 账号不能为空 / 账号格式不正确 | account 为空或非手机/邮箱 |
| 400 | 密码格式不正确 | 不是 32 位 MD5 |
| 400 | 参数非法 / OTPToken 验证失败 | sms_token 缺失或与 account 不匹配 |
| 400 | 在 IP 或者设备黑名单中 | 命中后台黑名单 |
| 400 | 已经达到注册上限 | 企业用户数超过 `maxUserTotal` |
| 400 | 用户已存在 | account 已被注册 |
| 500 | (内部 err) | DB 写入失败 |

---

### /v1/login 密码登录

#### 接口地址

```
POST /v1/login
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `deviceId` | string | ✓ | 设备号，长度 6 ~ 50 |
| `account` | string | ✓ | 账号 |
| `passwd` | string | ✓ | 密码，MD5-32 小写 |
| `deviceModel` | string |  | 设备型号（写入设备列表） |
| `deviceOS` | string |  | 操作系统 |
| `platform` | string |  | 平台标识 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "deviceId": "device_abcdef1234",
  "account": "13800138000",
  "passwd": "e10adc3949ba59abbe56e057f20f883e",
  "deviceModel": "iPhone15,3",
  "deviceOS": "iOS 17.4",
  "platform": "ios"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `token` | string | 登录 token，后续接口的 `X-Token` |
| `uid` | int64 | 用户 uid |
| `appkey` | string | 子应用 appkey |
| `invite` | string | 邀请码 |
| `plat` | string | 平台分组 `B`/`P`/`F` |
| `secret` | string | 消息加密秘钥（存在时正文需加密） |
| `encrypt` | int64 | `1`=当前开启正文加密 |
| `msgAssistant` | int64 | `1`=开启群发助手 |
| `msgTranslate` | int64 | `1`=开启翻译助手 |
| `customerServiceUrl` | string | 客服 url |

`config` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `nickGuide` | bool | 是否进入昵称修改引导 |
| `loginGuide` | bool | 是否进入登录引导 |
| `name` | string | 企业名称 |
| `switch_regist` | int32 | 注册开关 |
| `switch_rtc` | int32 | RTC 开关 |
| `switch_tab_main` | int32 | 主页 Tab 开关 |
| `switch_tab_mine` | int32 | 我的 Tab 开关 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "invite": "ABC123",
    "appkey": "app_demo",
    "plat": "B",
    "token": "tk_1717000000_abc123def456",
    "uid": 10000245,
    "encrypt": 1,
    "secret": "aes-key-base64",
    "msgTranslate": 1
  },
  "config": {
    "nickGuide": false,
    "loginGuide": true,
    "name": "示例企业",
    "switch_regist": 1,
    "switch_rtc": 1,
    "switch_tab_main": 1,
    "switch_tab_mine": 1
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 设备号格式不正确 | deviceId 长度不在 6~50 |
| 400 | 请填邀请码 / 邀请码不存在 | vcode 异常 |
| 400 | 登陆账号不能为空 / 账号格式不正确 | account 异常 |
| 400 | 密码格式不正确 | 不是 32 位 MD5 |
| 400 | 在 IP 或者设备黑名单中 | 命中黑名单 |
| 1000 | 用户不存在 | account 没注册 |
| 1001 | 密码错误 / 账户不存在/已关闭/状态异常 | 密码不匹配或用户状态异常 |
| 1013 | 连续密码错误超过 5 次,请找回密码 | 当日密码错误次数超限 |
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 登录成功会同时写入设备列表（参与多端互踢规则）
- 不需要 `X-Token`，由本接口下发新 token

---

### /v1/loginotp 验证码登录

通过手机号 / 邮箱 + OTP 登录，需要先通过 `/v1/sdk/otp/verify` 拿到 `sms_token`。

#### 接口地址

```
POST /v1/loginotp
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `deviceId` | string | ✓ | 设备号，长度 6 ~ 50 |
| `account` | string | ✓ | 手机号 / 邮箱（不支持工号） |
| `sms_token` | string | ✓ | 来自 `/v1/sdk/otp/verify` 的 otp_token |
| `deviceModel` | string |  | 设备型号 |
| `deviceOS` | string |  | 操作系统 |
| `platform` | string |  | 平台 |
| `lastLoginTime` | int64 |  | 客户端上次登录时间（毫秒） |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "deviceId": "device_abcdef1234",
  "account": "13800138000",
  "sms_token": "a1b2c3d4e5f6071829304a5b6c7d8e9f",
  "deviceModel": "iPhone15,3",
  "deviceOS": "iOS 17.4",
  "platform": "ios"
}
```

#### 返回示例

返回结构同 `/v1/login`。

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 设备号格式不正确 | deviceId 长度不在 6~50 |
| 400 | 请填邀请码 / 邀请码不存在 | vcode 异常 |
| 400 | 登陆账号不能为空 / 账号格式不正确 | account 为空或为工号格式 |
| 400 | 参数非法 | sms_token 为空 |
| 400 | 在 IP 或者设备黑名单中 | 命中黑名单 |
| 1000 | 用户不存在 | account 没注册 |
| 1001 | OTPToken 验证失败 | sms_token 失效或与 account 不匹配 |
| 1001 | 账户不存在/已关闭/状态异常 | 用户状态异常 |
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 不需要 `X-Token`

---

### /v1/logout 退出登录

注销当前 token；可同时撤销请求体里列出的其它端 token。

#### 接口地址

```
POST /v1/logout
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `deviceId` | string |  | 当前设备号 |
| `other_auth` | []object |  | 同时注销的其它端 token 列表 |

`other_auth` 元素：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uid` | int | ✓ | 其它端 uid |
| `token` | string | ✓ | 其它端 token |

#### 请求示例

```json
{
  "deviceId": "device_abcdef1234",
  "other_auth": [
    { "uid": 10000245, "token": "tk_xxx_other_end" }
  ]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

---

### /v1/resetpwd 找回密码（OTP 通道）

使用 `/v1/sdk/otp/verify` 拿到的 `otp_token` 重置密码，重置成功后该用户全部 token 失效。免登录。

#### 接口地址

```
POST /v1/resetpwd
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `eventTag` | string | ✓ | 来自 `/v1/chkacct` |
| `otp_token` | string | ✓ | 来自 `/v1/sdk/otp/verify` |
| `passwd` | string | ✓ | 新密码，MD5-32 小写 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "eventTag": "evt_9a8b7c6d5e",
  "otp_token": "a1b2c3d4e5f6071829304a5b6c7d8e9f",
  "passwd": "25f9e794323b453885f5181f1b624d0b"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 邀请码为空 / 邀请码不存在 | vcode 异常 |
| 400 | 参数错误 | eventTag / otp_token 异常 |
| 400 | 密码格式不正确 | 不是 32 位 MD5 |
| 400 | OTPToken 验证失败 | otp_token 已失效 |
| 1000 | 用户不存在 | account 没注册 |
| 500 | (内部 err) | DB 失败 |

---

## 设备

### /v1/device/info 已登录设备列表

返回当前用户所有仍持有有效 token 的设备。设备记录与有效 token 取交集，已失效 token 对应设备不返回。

#### 接口地址

```
POST /v1/device/info
Content-Type: application/json
```

#### 请求参数

无。

#### 返回参数

返回值是 `[]object`，元素结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `deviceId` | string | 设备号 |
| `deviceModel` | string | 设备型号 |
| `deviceOS` | string | 操作系统 |
| `platform` | string | 平台 |
| `lastLoginTime` | int64 | 最后登陆时间（毫秒） |
| `userId` | int64 | 用户 uid |
| `token` | string | 该设备的 token |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "deviceId": "device_abcdef1234",
      "deviceModel": "iPhone15,3",
      "deviceOS": "iOS 17.4",
      "platform": "ios",
      "lastLoginTime": 1717000000123,
      "userId": 10000245,
      "token": "tk_1717000000_abc123def456"
    }
  ]
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | Redis 查询失败 |

---

### /v1/device/remove 移除登录设备

按设备号批量下线设备，撤销对应 token。返回剩余设备列表。

#### 接口地址

```
POST /v1/device/remove
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `deviceIds` | []string | ✓ | 被移除设备的 ID 数组 |

#### 请求示例

```json
{
  "deviceIds": [
    "device_old_phone",
    "device_old_pc"
  ]
}
```

#### 返回示例

结构同 `/v1/device/info`。

---

### /v1/chktoken 校验目标 token 是否有效

通常用于「切换账号」场景：当前已登录用户 A，校验另一份历史保存的 token 是否仍可继续使用（同一台设备）。

#### 接口地址

```
POST /v1/chktoken
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `token` | string | ✓ | 待校验的 token |

#### 请求示例

```json
{
  "token": "tk_1717000000_other_account"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `valid` | bool | token 是否有效 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "valid": true }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | token 不能为空 | 请求体 token 为空 |
| 1003 | (业务文案) | 目标 token 已过期或非法 |
| -1 | deviceId mismatch | 目标 token 的设备号与当前 token 不一致 |
| -1 | token not found | 目标 token 不在该用户的有效 token 列表中 |
| 500 | (内部 err) | Redis / DB 失败 |

---

## PC 端登录

### /v1/pc/login PC 端发起登录并轮询

PC 端进入登录页时调用，建立一条登录记录（`mac + ip + timestamp` 唯一）。PC 端按一定间隔重复调用，直到 `status` 变为 `2` 或 `3`。免登录。

#### 接口地址

```
POST /v1/pc/login
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mac` | string | ✓ | PC 设备 mac，长度 6 ~ 50 |
| `ip` | string | ✓ | PC 出口 IP |
| `timestamp` | string | ✓ | 同一次扫码会话保持不变（毫秒字符串） |
| `deviceModel` | string | ✓ | 设备型号 |
| `deviceOS` | string | ✓ | 操作系统 |
| `platform` | string | ✓ | 平台标识（落库时被 `KK-Version` 覆盖） |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "mac": "AA:BB:CC:DD:EE:FF",
  "ip": "203.0.113.10",
  "timestamp": "1717000000000",
  "deviceModel": "MacBookPro18,1",
  "deviceOS": "macOS 14.4",
  "platform": "mac"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `status` | int | `0`=pending `1`=scan `2`=cancel `3`=confirm |
| 其余 | object | confirm 状态下额外返回 `LoginRsp` 字段（结构同 `/v1/login` 的 data），未 confirm 时为空值 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "status": 0,
    "invite": "",
    "appkey": "",
    "plat": "",
    "token": "",
    "uid": 0
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | Mac 格式不正确 / 长度不正确 | mac 校验失败 |
| 400 | IP 格式不正确 | ip 校验失败 |
| 400 | 时间戳格式不正确 | timestamp 不是非负整数 |
| 400 | 设备型号/操作系统/平台不能为空 | 任一缺失 |
| 400 | 查询登陆记录失败 | DB 失败 |

#### 备注

- PC 端不带 `X-Token`

---

### /v1/pc/scan 手机端扫描 PC 二维码

手机扫到 PC 二维码后调用，将该登录记录状态置为 `scan`（PC 端轮询会感知到）。

#### 接口地址

```
POST /v1/pc/scan
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mac` | string | ✓ | PC 二维码内携带的 mac |
| `ip` | string | ✓ | PC 二维码内携带的 ip |
| `timestamp` | string | ✓ | PC 二维码内携带的 timestamp |

#### 请求示例

```json
{
  "mac": "AA:BB:CC:DD:EE:FF",
  "ip": "203.0.113.10",
  "timestamp": "1717000000000"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 登录记录 id（用于 `/v1/pc/confirm`） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "id": "7283910472183847264" }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | Mac 格式不正确 / IP 格式不正确 / 时间戳格式不正确 | 参数格式异常 |
| 400 | 查询登陆记录失败 | 该 (mac, ip, timestamp) 记录不存在或更新失败 |

---

### /v1/pc/confirm 手机端确认 / 取消

`operatorType=1` 时执行真正的 PC 端登录授权，写回 PC 端的轮询返回结果（含 token、appkey、加密配置）。

#### 接口地址

```
POST /v1/pc/confirm
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 来自 `/v1/pc/scan` 的登录记录 id |
| `operatorType` | int | ✓ | `0`=cancel `1`=confirm |

#### 请求示例

```json
{
  "id": "7283910472183847264",
  "operatorType": 1
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | Id 格式不正确 | id 不是数字 |
| 400 | 查询登陆记录失败 | 记录不存在 |
| 400 | 在 IP 或者设备黑名单中 | 命中黑名单 |
| 400 | 更新登陆记录失败 | DB 更新失败 |
| 1000 | 用户不存在 | 当前 token 关联用户在库中没找到 |
| 1001 | 账户不存在/已关闭/状态异常 | 用户状态异常 |
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/pc/clientip 获取客户端出口 IP

服务端按 `X-Forwarded-For` -> `X-Real-Ip` -> `RemoteAddr` 顺序返回客户端出口 IP，用于 PC 端拼装登录二维码内容。免登录。

#### 接口地址

```
POST /v1/pc/clientip
Content-Type: application/json
```

#### 请求参数

无。

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `clientIp` | string | 客户端出口 IP |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "clientIp": "203.0.113.10" }
}
```

---

## 密保

### /v1/sdk/system/security/problem 查询密保问题

返回当前用户曾设置的密保问题与答案（明文）。

#### 接口地址

```
POST /v1/sdk/system/security/problem
Content-Type: application/json
```

#### 请求参数

无。

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `security_problem` | string | 已设置的密保问题；未设置时为空 |
| `security_answer` | string | 已设置的密保答案；未设置时为空 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "security_problem": "我的小学班主任是？",
    "security_answer": "张老师"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 1020 | 用户信息不存在 | DB 查询失败 |

---

### /v1/sdk/system/set/security/problem 设置/修改密保问题

两种鉴权方式：传 `passwd`（账号密码）走密码校验；不传则按当前 `X-Token` 鉴权。

#### 接口地址

```
POST /v1/sdk/system/set/security/problem
Content-Type: application/json
```

#### 请求头

| Header | 必填 | 说明 |
|---|---|---|
| `X-Token` |  | 当 `passwd` 为空时必填 |

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `account` | string | ✓ | 账号 |
| `security_problem` | string | ✓ | 密保问题 |
| `security_answer` | string | ✓ | 密保答案 |
| `passwd` | string |  | 账号密码（MD5-32 小写）；不传则用 X-Token 鉴权 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "account": "user@example.com",
  "passwd": "e10adc3949ba59abbe56e057f20f883e",
  "security_problem": "我的小学班主任是？",
  "security_answer": "张老师"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 请填邀请码 / 邀请码不存在 | vcode 异常 |
| 400 | 参数错误 | problem / answer 为空 |
| 400 | 账号格式不正确 | account 解析失败 |
| 1000 | 用户不存在 | account 没注册 |
| 1001 | 密码错误 | 传了 passwd 但与用户实际密码不匹配 |
| 500 | (内部 err) | DB 失败 |

---

### /v1/sdk/system/reset/password/step 密保重置密码第一步

输入账号查询服务端配的密保问题（不含答案），客户端拿到 `security_problem` 后让用户作答。免登录。

#### 接口地址

```
POST /v1/sdk/system/reset/password/step
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `account` | string | ✓ | 账号 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "account": "user@example.com"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `uid` | int64 | 用户 uid |
| `security_info` | object | 包含 `security_problem` 字段 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "uid": 10000245,
    "security_info": {
      "security_problem": "我的小学班主任是？"
    }
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数非法 | account 为空 |
| 400 | 账号格式不正确 | account 解析失败 |
| 400 | 被风控 | IP 当日操作过多 |
| 400 | 邀请码不存在 | vcode 异常 |
| 400 | 未设置密保问题，找回密码请联系客服 | 用户未设置密保 |
| 1000 | 用户不存在 | account 没注册 |
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/sdk/system/reset/password 密保重置密码

第二步：携带密保问题 + 答案 + 新密码完成重置；成功后该用户全部 token 失效。免登录。

#### 接口地址

```
POST /v1/sdk/system/reset/password
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `account` | string | ✓ | 账号 |
| `passwd` | string | ✓ | 新密码，MD5-32 小写 |
| `security_info.security_problem` | string | ✓ | 密保问题 |
| `security_info.security_answer` | string | ✓ | 密保答案 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "account": "user@example.com",
  "passwd": "25f9e794323b453885f5181f1b624d0b",
  "security_info": {
    "security_problem": "我的小学班主任是？",
    "security_answer": "张老师"
  }
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数非法 | account 为空或 passwd 非 32 位 MD5 |
| 400 | 账号格式不正确 | account 解析失败 |
| 400 | 邀请码不存在 | vcode 异常 |
| 400 | 安全问题或者安全答案非法 | security_info 缺失字段 |
| 400 | 未设置密保问题，找回密码请联系客服 | 用户未设置密保 |
| 400 | 密保答案错误，请重新输入 | 答案不匹配 |
| 1000 | 用户不存在 | account 没注册 |
| 500 | (内部 err) | DB 失败 |

---

### /v1/sdk/system/check/security/problem 校验密保问题

校验当前登录用户输入的密保问答是否正确，常用于"修改密保前的二次验证"。

#### 接口地址

```
POST /v1/sdk/system/check/security/problem
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `problem` | string | ✓ | 密保问题 |
| `answer` | string | ✓ | 密保答案 |

#### 请求示例

```json
{
  "problem": "我的小学班主任是？",
  "answer": "张老师"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | problem / answer 为空 |
| 400 | 您未设置过安全问题，无法通过该方式找回 | 用户未设置密保 |
| 400 | 密保问题未校验通过 | 答案不匹配 |
| 500 | (内部 err) | DB 失败 |

---

### /v1/sdk/system/forgot/password 忘记密码（密保通道）

与 `/v1/sdk/system/reset/password` 流程基本一致，是给「未登录用户」的入口。免登录。

#### 接口地址

```
POST /v1/sdk/system/forgot/password
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vcode` | string | ✓ | 邀请码 |
| `account` | string | ✓ | 账号 |
| `passwd` | string | ✓ | 新密码，MD5-32 小写 |
| `security_info.security_problem` | string | ✓ | 密保问题 |
| `security_info.security_answer` | string | ✓ | 密保答案 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "vcode": "ABC123",
  "account": "user@example.com",
  "passwd": "25f9e794323b453885f5181f1b624d0b",
  "security_info": {
    "security_problem": "我的小学班主任是？",
    "security_answer": "张老师"
  }
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 请填邀请码 / 邀请码不存在 | vcode 异常 |
| 400 | 账号不能为空 / 账号格式不正确 | account 异常 |
| 400 | 密码格式不正确 | 不是 32 位 MD5 |
| 400 | 安全问题或者安全答案非法 | security_info 缺失字段 |
| 400 | 未设置密保问题，找回密码请联系客服 | 用户未设置密保 |
| 400 | 密保答案错误，请重新输入 | 答案不匹配 |
| 1000 | 用户不存在 | account 没注册 |
| 500 | (内部 err) | DB 失败 |

---


## 用户

### /v1/sdk/user/detail 用户详情

按 uid 列表查询用户档案；当列表里包含当前登录用户时会顺带签发一份 7 天有效的个人二维码。

#### 接口地址

```
POST /v1/sdk/user/detail
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uids` | []int64 | ✓ | 用户 uid 列表，至少 1 个 |

#### 请求示例

```json
{
  "uids": [10000245, 10000246, 10000301]
}
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `users` | []UserDao | 用户档案列表，结构见附录 UserDao；当前登录用户额外带 `qrcode` / `qrcode_desc` |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "users": [
      {
        "uid": 10000245,
        "name": "张三",
        "nick": "三哥",
        "avatar": "https://cdn.example.com/avatar/10000245.png",
        "sex": "1",
        "appkey": "appkey_demo",
        "mainAppkey": "main_demo",
        "remark": "客户A",
        "qrcode": "http://qr.example.com/qrcode?uid=10000245&code=xxx",
        "qrcode_desc": "该二维码7天内(2026-06-02前)有效，重新进入将更新"
      },
      {
        "uid": 10000246,
        "name": "李四",
        "nick": "李四",
        "avatar": "",
        "appkey": "appkey_demo",
        "mainAppkey": "main_demo"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `uids` 为空 |
| 403 | 您无权查看该用户信息 | 查询的用户跟当前登录者不属于同一个 mainAppkey（机器人 uid 例外）|
| 500 | (内部 err) | DB / 缓存失败 |

#### 备注

- 二维码仅当查询包含本人 uid 时返回；有效期 7 天，过期或重新拉取会重新签发
- 返回的 `users` 已剔除 `passwd` / `ctime` 等敏感字段

---

### /v1/sdk/user/profile 用户基本信息

获取当前登录用户自己的档案 + 所属企业信息，常用于个人中心页。

#### 接口地址

```
POST /v1/sdk/user/profile
Content-Type: application/json
```

#### 请求参数

无请求体。

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `user` | UserDao | 当前用户档案，结构见附录 UserDao |
| `app` | AppDao | 当前用户所在企业（appkey）信息 |

`app` 主要字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `appkey` | string | 企业 appkey |
| `mainAppkey` | string | 主应用 appkey（B/P/F 多端共用） |
| `plat` | string | 端类型：`B`/`P`/`F`/`S` |
| `company` | string | 公司全称 |
| `companyShort` | string | 公司简称 |
| `companyDesc` | string | 公司描述 |
| `adminUserId` | int64 | 企业超级管理员 uid |
| `status` | string | 空=正常 `closed`=关闭 `deleted`=删除 |
| `ctime` | int64 | 企业创建时间（秒）|

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "user": {
      "uid": 10000245,
      "name": "张三",
      "nick": "三哥",
      "avatar": "https://cdn.example.com/avatar/10000245.png",
      "telno": "13800138000",
      "email": "zhangsan@example.com",
      "appkey": "appkey_demo",
      "mainAppkey": "main_demo",
      "position": "技术总监",
      "loginGuide": 0
    },
    "app": {
      "appkey": "appkey_demo",
      "mainAppkey": "main_demo",
      "plat": "B",
      "company": "示例科技有限公司",
      "companyShort": "示例科技",
      "adminUserId": 10000001,
      "status": "",
      "ctime": 1717000000
    }
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 403 | ErrUserNotFound / ErrAppNotFound | 用户档案或所属企业找不到 |
| 500 | (内部 err) | DB / 缓存失败 |

---

### /v1/sdk/user/profile/set 设置用户基本信息

批量设置当前登录用户的可编辑字段。**约定：字段为空字符串表示"修改为空"，未传字段才表示"不修改"**，调用方要按这个语义组装请求。

#### 接口地址

```
POST /v1/sdk/user/profile/set
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `avatar` | string |  | 头像 url |
| `nick` | string |  | 昵称 |
| `sex` | string |  | 性别（业务约定字符串，例如 `0`/`1`） |
| `birth` | string |  | 出生年月日（业务约定字符串） |
| `rtel` | string |  | 备注手机 |
| `remail` | string |  | 备注邮箱 |
| `privacy` | string |  | `1`=不公开账号，其余=公开账号 |
| `position` | string |  | 职位 |
| `titles` | string |  | 职称 / 标签 |
| `present` | string |  | 当前状态文案 |

#### 请求示例

```json
{
  "avatar": "https://cdn.example.com/avatar/10000245_v2.png",
  "nick": "三哥",
  "sex": "1",
  "birth": "1990-01-01",
  "position": "技术总监",
  "titles": "架构师",
  "present": "工作中"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | (业务文案) | socialevent 处理失败（昵称非法、字段越界等） |
| 500 | (内部 err) | DB / RPC 失败 |

#### 备注

- 后端用 `UserInfoSetHandler` 统一处理，会向其它在线端同步用户信息变更通知
- 单独改头像/昵称可以直接调 `/v1/sdk/user/avatar/set` 或 `/v1/sdk/user/nick/set`

---

### /v1/sdk/user/avatar/set 设置头像

`/v1/sdk/user/profile/set` 的语法糖；只是为了让客户端语义清晰。请求参数、返回、错误码完全等同 `/v1/sdk/user/profile/set`，**实际只取 `avatar` 字段生效**，其它字段建议不要传。

#### 接口地址

```
POST /v1/sdk/user/avatar/set
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `avatar` | string | ✓ | 头像 url；传空字符串=清空头像 |

#### 请求示例

```json
{
  "avatar": "https://cdn.example.com/avatar/10000245_v2.png"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

同 `/v1/sdk/user/profile/set`。

---

### /v1/sdk/user/nick/set 设置昵称

只修改当前用户的昵称，必填且不能为空字符串（与 `profile/set` 的"空=清空"语义不同）。

#### 接口地址

```
POST /v1/sdk/user/nick/set
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `nick` | string | ✓ | 昵称，trim 后不能为空 |

#### 请求示例

```json
{
  "nick": "三哥"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 请填写昵称 | trim 后为空 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/user/loginguide/off 关闭登录引导

把当前用户的 `loginGuide` 字段置为 `-1`，标记"不再展示登录引导/新手指引"。

#### 接口地址

```
POST /v1/sdk/user/loginguide/off
Content-Type: application/json
```

#### 请求参数

无请求体。

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 更新失败 |

#### 备注

- `loginGuide` 字段语义：`0` 或 `1`=开启引导，`-1`=已关闭
- 已经是 `-1` 时再次调用也返回成功

---

### /v1/sdk/user/harass 会话免打扰

设置某个会话（群 / 单聊）是否免打扰，开启后该会话的新消息不再触发推送提醒，但消息体仍会下发。

#### 接口地址

```
POST /v1/sdk/user/harass
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 目标会话：群聊填 `g_<群id>`；单聊可以直接填对端 uid（数字），后端会拼成 `s_<minUid>_<maxUid>` |
| `op` | int | ✓ | `1`=开启免打扰，`0`=关闭免打扰 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "op": 1
}
```

或单聊（直接传对端 uid）：

```json
{
  "tid": "10000246",
  "op": 1
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `op` 非 0/1，或 `tid` 既不是群 tid 也不是合法 uid |

#### 备注

- 后端先写 Redis 免打扰 hash（兼容老行为），再走 `TopicModifyHandler` 把 `mute` 同步到 Mongo 并通知其它在线端
- 如果对应 Topic 在 Mongo 不存在，handler 会返回"会话不存在"但**不阻断响应**，Redis 一侧已写入，行为符合老语义
- 与原 `/sdk/userobj/set` 的 `mute` 字段统一走 `TopicModifyHandler`

---

### /v1/sdk/user/passwd/change 修改登录密码

校验旧密码后用新密码替换；成功后会让当前用户**除本端外**的其它 token 全部失效，强制其它端重新登录。

#### 接口地址

```
POST /v1/sdk/user/passwd/change
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `passwd` | string | ✓ | 旧密码，MD5-32 位小写 |
| `newPasswd` | string | ✓ | 新密码，MD5-32 位小写 |

#### 请求示例

```json
{
  "passwd": "e10adc3949ba59abbe56e057f20f883e",
  "newPasswd": "25f9e794323b453885f5181f1b624d0b"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 密码格式不正确 / 新密码格式不正确 | 不是 32 位 |
| 1007 | 旧密码错误，请重新输入！ | 旧密码 hash 不匹配 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) / [BUG] xxx user not found | DB 异常 / 会话有效但库里查不到用户 |

---

### /v1/sdk/user/phone/change 更换登录手机号

通过短信 OTPToken 校验新手机号归属，再把当前用户的主手机和备注手机一起改成新号码，并标记备注手机已验证。

#### 接口地址

```
POST /v1/sdk/user/phone/change
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `telno` | string | ✓ | 目标手机号 |
| `sms_token` | string | ✓ | 短信验证通过后拿到的 OTPToken，必须是同一手机号签发的 |

#### 请求示例

```json
{
  "telno": "13900139000",
  "sms_token": "otp_4f7e2a3b9c1d5e..."
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | OTPToken 验证失败 | token 失效或 token 绑定的号码与 `telno` 不一致 |
| 1000 | 手机号码已经存在 | 该号码已被同 appkey 下另一个用户占用 |
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 如果目标手机号就是当前用户自己的号码，直接返回成功（幂等）
- 同时写入 `telno` / `rtel` / `rtelCheck=1` 三个字段

---

### /v1/sdk/user/email/change 更换登录邮箱

通过邮箱 OTPToken 校验新邮箱归属，再把主邮箱和备注邮箱一起改成新邮箱，并标记备注邮箱已验证。

#### 接口地址

```
POST /v1/sdk/user/email/change
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `email` | string | ✓ | 目标邮箱 |
| `email_token` | string | ✓ | 邮箱验证通过后拿到的 OTPToken，必须是同一邮箱签发的 |

#### 请求示例

```json
{
  "email": "zhangsan_new@example.com",
  "email_token": "otp_9c1d5e4f7e2a3b..."
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | OTPToken 验证失败 | token 失效或 token 绑定的邮箱与 `email` 不一致 |
| 1000 | 邮箱已存在 | 该邮箱已被同 appkey 下另一个用户占用 |
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 如果目标邮箱就是当前用户自己的邮箱，直接返回成功（幂等）
- 同时写入 `email` / `remail` / `remailCheck=1` 三个字段

---

### /v1/sdk/user/delete 注销账号

用户主动注销账号；需要校验当前密码，可选叠加 OTPToken 二次验证。成功后用户状态置为 `deleted`，所有 token 失效。

#### 接口地址

```
POST /v1/sdk/user/delete
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `password` | string | ✓ | 当前登录密码，MD5-32 位小写 |
| `sms_token` | string |  | OTPToken（短信或邮箱二次验证）；传了就会校验 token 绑定账号必须是当前用户的 `telno` / `rtel` / `email` / `remail` 之一 |

#### 请求示例

```json
{
  "password": "e10adc3949ba59abbe56e057f20f883e",
  "sms_token": "otp_4f7e2a3b9c1d5e..."
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 密码格式不正确 | password 不是 32 位 |
| 400 | OTPToken 验证失败 | OTPToken 失效 |
| 400 | sms_token验证失败 | OTPToken 绑定的账号不属于当前用户 |
| 400 | 密码不正确 | 密码 hash 与库中不一致 |
| 403 | ErrUserNotFound | 当前 uid 在用户表查不到 |
| 400 | (业务文案) | UserDeletedHandler 失败 |
| 500 | (内部 err) | DB / RPC 失败 |

#### 备注

- 用户状态已经是 `closed` 或 `deleted` 时直接返回成功（幂等）
- 注销后会触发 `invalidateTokens` 让该用户所有端立刻掉线，并把 Redis 中用户状态标记为 `deleted`

---


## 好友

好友模块覆盖好友关系、申请、标签分组、黑名单、备注、搜索、扫码添加等场景。响应中的 `users` 字段统一返回 `UserDao`（结构见附录），其中 `remark` 由当前用户在会话中设置的备注合并得到。

### /v1/sdk/friend/list 好友列表

分页拉取当前登录用户的全部有效好友及其对应的用户信息，`users[i].remark` 会自动带上当前用户在 `s_<minUid>_<maxUid>` 会话上的备注。

#### 接口地址

```
POST /v1/sdk/friend/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skip` | int64 |  | 跳过条数，默认 `0` |
| `limit` | int64 |  | 单页条数，取值 `[100, 1000]`，越界回落为 `100` |

#### 请求示例

```json
{
  "skip": 0,
  "limit": 100
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 好友总数 |
| `friends` | []Friend | 本页好友关系列表，结构见下 |
| `users` | []UserDao | `friends` 中 `friendUid` 关联的用户信息（含 `remark`） |

**Friend**

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 主键，`<myUid>:<friendUid>` 拼接 |
| `friendUid` | int64 | 好友 uid |
| `scene` | string | 加好友来源（`phone` / `qrcode` / `search` 等） |
| `hello` | string | 申请时的招呼语 |
| `applyUid` | int64 | 当初的申请人 uid |
| `applyTime` | int64 | 申请时间（毫秒） |
| `addedTime` | int64 | 成为好友的时间（毫秒） |
| `flag` | int32 | `0`=有效 `1`=已删除 |
| `utime` | int64 | 记录更新时间 |
| `ctime` | int64 | 记录创建时间 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "friends": [
      {
        "id": "10000245:10000246",
        "friendUid": 10000246,
        "scene": "search",
        "hello": "你好，加个好友",
        "applyUid": 10000245,
        "applyTime": 1716700000000,
        "addedTime": 1716700050000,
        "flag": 0,
        "utime": 1716700050000,
        "ctime": 1716700050000
      }
    ],
    "users": [
      {
        "uid": 10000246,
        "nick": "张三",
        "avatar": "https://cdn.example.com/avatar/zs.png",
        "remark": "客户张总"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 查询失败 |

---

### /v1/sdk/friend/delete 删除好友

单向删除一位好友（仅删除自己侧关系），同时清空该单聊会话的本地消息标记。

#### 接口地址

```
POST /v1/sdk/friend/delete
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `friendUid` | int64 | ✓ | 要删除的好友 uid，必须 `> 0` |

#### 请求示例

```json
{
  "friendUid": 10000246
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `friendUid <= 0` |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB / 消息清理失败 |

---

### /v1/sdk/friend/apply/list 好友申请列表

拉取发给当前用户的好友申请，按申请时间倒序分页返回；首页（`skip == 0`）会同步触发多端「申请已读」通知。

#### 接口地址

```
POST /v1/sdk/friend/apply/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skip` | int64 |  | 跳过条数，默认 `0` |
| `limit` | int64 |  | 单页条数，取值 `[100, 1000]`，越界回落为 `100` |

#### 请求示例

```json
{
  "skip": 0,
  "limit": 100
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `friendRequests` | []FriendRequest | 申请记录列表 |
| `users` | []UserDao | 申请人用户信息（已合并 `remark`） |

**FriendRequest**

| 字段 | 类型 | 说明 |
|---|---|---|
| `applyUid` | int64 | 申请人 uid |
| `hello` | string | 招呼语 |
| `scene` | string | 加好友来源 |
| `applyTime` | int64 | 申请时间（毫秒），前端用其作角标记数 |
| `etime` | int64 | 过期时间（毫秒） |
| `status` | string | 空=待处理 `pass`=已通过 `refuse`=已拒绝 |
| `utime` | int64 | 更新时间 |
| `ctime` | int64 | 创建时间 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "friendRequests": [
      {
        "applyUid": 10000300,
        "hello": "我是市场部的小李",
        "scene": "search",
        "applyTime": 1716800000000,
        "etime": 1719392000000,
        "status": "",
        "utime": 1716800000000,
        "ctime": 1716800000000
      }
    ],
    "users": [
      {
        "uid": 10000300,
        "nick": "小李",
        "avatar": "https://cdn.example.com/avatar/xl.png"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 查询失败 |

---

### /v1/sdk/friend/apply/add 发起好友申请

发起加好友申请。若对方已经加了我（单向已添加），则直接互加并返回成功；否则写入申请记录，等待对方处理。

#### 接口地址

```
POST /v1/sdk/friend/apply/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `touid` | int64 | ✓ | 要加的目标用户 uid |
| `hello` | string |  | 招呼语 |
| `scene` | string |  | 加好友来源，`phone` 代表通过手机号搜索 |

**关系准入规则**（任一满足即可）

- `scene == "phone"`
- 双方任一方是 P 端账号
- 双方 `appkey` 一致（同一公司同事）

#### 请求示例

```json
{
  "touid": 10000246,
  "hello": "我是搜索手机号加的",
  "scene": "phone"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `isAdd` | bool | `true` 表示首次写入了申请记录；`false` 表示直接互加成功（对方已加我）或未变更 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "isAdd": true }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 目标用户不存在 | `touid` 查不到或与自己不属同一 MainAppkey |
| 400 | 不是同一个主体，不能加好友 | 双方 `appkey` 不一致 |
| 403 | 不能与该用户成为好友 | 不满足关系准入规则 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/friend/apply/deal 处理好友申请

接受或拒绝某条好友申请。

#### 接口地址

```
POST /v1/sdk/friend/apply/deal
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `applyUid` | int64 | ✓ | 申请人 uid |
| `op` | string | ✓ | `pass`=同意 `refuse`=拒绝，其他值会被拒绝 |

#### 请求示例

```json
{
  "applyUid": 10000300,
  "op": "pass"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `op` 不在 `pass` / `refuse` 中 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/friend/label/list 好友标签列表

分页拉取当前用户的所有标签（通讯录分组），每个标签内附带组内成员的 `UserDao` 列表（已合并 `remark`）。

#### 接口地址

```
POST /v1/sdk/friend/label/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skip` | int64 |  | 跳过条数，默认 `0` |
| `limit` | int64 |  | 单页条数，取值 `[100, 1000]` |

#### 请求示例

```json
{
  "skip": 0,
  "limit": 100
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 标签总数 |
| `lables` | []LabelGroup | 标签分组列表（字段名与代码保持一致，注意拼写） |

**LabelGroup**

| 字段 | 类型 | 说明 |
|---|---|---|
| `label` | string | 标签名 |
| `users` | []UserDao | 该标签下的成员用户信息 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "lables": [
      {
        "label": "重要客户",
        "users": [
          { "uid": 10000246, "nick": "张三", "remark": "客户张总" }
        ]
      },
      {
        "label": "同事",
        "users": [
          { "uid": 10000301, "nick": "王五", "remark": "" }
        ]
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 查询失败 |

---

### /v1/sdk/friend/label/add 添加好友标签

新建标签或在已有标签里追加成员。`type == 1` 时若标签已存在直接报错；`type == 0` 时不存在会自动创建，存在则只追加成员。

#### 接口地址

```
POST /v1/sdk/friend/label/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `label` | string | ✓ | 标签名，不能为纯空白 |
| `uids` | []int64 |  | 要加入该标签的好友 uid 列表，`<=0` 的会被跳过 |
| `type` | int64 |  | `0`=追加（默认）`1`=新建（重名报错） |

#### 请求示例

```json
{
  "label": "重要客户",
  "uids": [10000246, 10000301],
  "type": 0
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误，参数不能为空 | `label` 去空白后为空 |
| 400 | 标签已存在 | `type == 1` 且同名标签已存在 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB 操作失败 |

---

### /v1/sdk/friend/label/delete 删除好友标签

从标签中移除指定成员，或整体删除某个标签。

#### 接口地址

```
POST /v1/sdk/friend/label/delete
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `label` | string | ✓ | 标签名 |
| `uids` | []int64 |  | `type == 0` 时要移除的成员 uid 列表 |
| `type` | int64 | ✓ | `0`=从标签移除成员 `1`=删除整个标签 |

#### 请求示例

**例 1：移除标签内的某些成员**

```json
{
  "label": "重要客户",
  "uids": [10000301],
  "type": 0
}
```

**例 2：删除整个标签**

```json
{
  "label": "重要客户",
  "type": 1
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB 操作失败（`type == 1` 时） |

---

### /v1/sdk/friend/label/modify 修改好友标签名

把 `old_label` 改名为 `new_label`。若 `new_label` 已存在且未被删除，则报错；若已存在但为「已删除」状态，会先清掉旧记录再改名。

#### 接口地址

```
POST /v1/sdk/friend/label/modify
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `old_label` | string | ✓ | 原标签名 |
| `new_label` | string | ✓ | 新标签名 |

#### 请求示例

```json
{
  "old_label": "重要客户",
  "new_label": "VIP 客户"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误，参数不能为空 | `old_label` / `new_label` 任一为空 |
| 400 | 新标签名字已经存在 | `new_label` 已有有效标签 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB 操作失败 |

---

### /v1/sdk/user/black/list 黑名单列表

分页拉取当前用户加入黑名单的对象列表，附带对应的用户信息（含 `remark`）。

#### 接口地址

```
POST /v1/sdk/user/black/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skip` | int64 |  | 跳过条数，默认 `0` |
| `limit` | int64 |  | 单页条数，取值 `[100, 1000]`，越界回落为 `100` |

#### 请求示例

```json
{
  "skip": 0,
  "limit": 100
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `blackRecords` | []BlackRequest | 黑名单记录 |
| `users` | []UserDao | 被拉黑用户信息 |

**BlackRequest**

| 字段 | 类型 | 说明 |
|---|---|---|
| `blackUid` | int64 | 被拉黑用户 uid |
| `utime` | int64 | 更新时间（毫秒，省略表示未更新过） |
| `ctime` | int64 | 创建时间（毫秒） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "blackRecords": [
      {
        "blackUid": 10000310,
        "utime": 1716900000000,
        "ctime": 1716800000000
      }
    ],
    "users": [
      { "uid": 10000310, "nick": "骚扰用户" }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 查询失败 |

---

### /v1/sdk/user/black/operator 黑名单操作

将某用户加入或移出黑名单。

#### 接口地址

```
POST /v1/sdk/user/black/operator
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `blackUid` | int64 | ✓ | 目标用户 uid |
| `op` | int64 | ✓ | `0`=加入黑名单 `1`=移出黑名单 |

#### 请求示例

```json
{
  "blackUid": 10000310,
  "op": 0
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `op` 不是 `0` / `1` |
| 400 | 目标用户不存在 | `blackUid` 查不到 |
| 400 | 不是同一个主体，不能操作 | 双方 `appkey` 不一致 |
| 500 | (内部 err) | DB 操作失败 |

---

### /v1/sdk/user/remark 设置好友备注

为某个用户在单聊会话 `s_<minUid>_<maxUid>` 上设置备注，备注存储在 `TopicDao.remark`，会反映在好友/搜索等接口的 `users[].remark`。

#### 接口地址

```
POST /v1/sdk/user/remark
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uid` | int64 | ✓ | 要备注的用户 uid，必须 `> 0` |
| `remark` | string |  | 备注内容，传空字符串等于清空 |

#### 请求示例

```json
{
  "uid": 10000246,
  "remark": "客户张总"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 用户标识参数不正确 | `uid <= 0` |
| 400 | (业务文案) | socialevent 处理失败 / DB 更新失败 |
| 500 | (内部 err) | RPC 失败 |

---

### /v1/sdk/user/search 搜索用户

按账号（用户名 / 手机号 / 邮箱，任一精确匹配）搜索本主应用下的用户。会自动过滤掉 `privacy == "1"` 的私密账号，并合并备注信息。

#### 接口地址

```
POST /v1/sdk/user/search
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `account` | string | ✓ | 用户名 / 手机号 / 邮箱中的任一字段 |

#### 请求示例

```json
{
  "account": "13800001234"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `users` | []UserDao | 匹配的用户列表；无结果时为 `null` |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "users": [
      {
        "uid": 10000246,
        "nick": "张三",
        "telno": "13800001234",
        "avatar": "https://cdn.example.com/avatar/zs.png",
        "remark": "客户张总"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 账号不能为空 | `account` 去空白后为空 |
| 400 | 请勿频繁操作 | 单用户 1 秒内超过 3 次搜索 |
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 命中频控时返回 `400`，1 秒后自然解除
- 命中后端会自动剔除返回结果里的 `privacy == "1"` 的账号

---

### /v1/sdk/user/qrcode/apply/add 扫码加好友

扫描对方的个人二维码后调用本接口加好友。接口先解码并校验二维码有效性，校验通过后内部转调 `/v1/sdk/friend/apply/add`，参数与返回与之保持一致。

#### 接口地址

```
POST /v1/sdk/user/qrcode/apply/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `touid` | int64 | ✓ | 二维码所属用户 uid，必须 `> 0` |
| `code` | string | ✓ | 扫描到的二维码密文（AES，key = `MD5(appkey)` 大写） |
| `hello` | string |  | 招呼语 |
| `scene` | string |  | 加好友来源，建议传 `qrcode` |

#### 请求示例

```json
{
  "touid": 10000246,
  "code": "AE3FB1...",
  "hello": "扫码加好友",
  "scene": "qrcode"
}
```

#### 返回参数

同 `/v1/sdk/friend/apply/add`：

| 字段 | 类型 | 说明 |
|---|---|---|
| `isAdd` | bool | `true`=已写入申请记录；`false`=直接互加成功或未变更 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "isAdd": true }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 200 | (空响应 OK) | `touid == 自己 uid`，幂等返回成功不做任何操作 |
| 400 | 参数非法 | `touid <= 0` 或 `code` 为空 |
| 400 | 二维码失效 | 解码后长度异常 |
| 400 | 二维码非法 | JSON 解析失败 / `uid <= 0` / `touid` 与二维码不符 |
| 400 | 二维码参数非法 | 二维码 `sign` 校验失败 |
| 400 | 二维码已经失效 | 二维码 `etime` 已过期（生成时默认 7 天有效） |
| 400 / 403 / 500 | — | 透传 `/v1/sdk/friend/apply/add` 的错误 |

#### 备注

- 二维码明文是 `{ "sign": "...", "uid": <int64>, "etime": <unix秒> }`，由 `/v1/sdk/user/detail` 在查询自己时下发
- `sign = MD5(fmt.Sprintf("fuliao-%d-%d", uid, etime))`，服务端会重算校验

---

### /v1/sdk/user/contact 通讯录查询（已废弃）

旧版的同公司通讯录分页查询接口，按 `plat` 分平台过滤（B / F / P / VP）。当前版本已被 `/v1/sdk/friend/list`、`/v1/sdk/user/search` 等替代，**不建议**新业务接入。如确需对接，请参考代码 `bm/imsvr/api/sdk_user.go::UserContact`。

---


## 群聊

群相关接口统一基于 `tid` 标识群会话（群 id 形如 `g_10000001_5`）。多数接口要求调用者为群成员；管理类操作（解散/转让/踢人/拉黑/审核处理/修改群设置等）额外要求群主或管理员身份。

---

### /v1/sdk/group/list 我的群列表

返回当前用户加入的所有群（按 `topic` 关联查询，已退群不返回）。

#### 接口地址

```
POST /v1/sdk/group/list
Content-Type: application/json
```

#### 请求参数

无（仅需 `X-Token`）。

#### 请求示例

```json
{}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `records` | []Record | 群列表 |

Record 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 群会话 id |
| `uid` | int64 | 群主 uid |
| `name` | string | 群名称 |
| `tag` | string | 群标签文案（同事 / 供应商 / 服务商 / 供应链） |
| `tagid` | int32 | 群标签 id：`1`=企业群 `2`=供应商 `3`=服务商 `4`=供应链 |
| `ctime` | int64 | 群创建时间（毫秒） |
| `avatar` | string | 群头像 url |
| `remark` | string | 当前用户给该群设置的备注 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "records": [
      {
        "tid": "g_10000001_5",
        "uid": 10000001,
        "name": "产品讨论群",
        "tag": "同事",
        "tagid": 1,
        "ctime": 1716700000000,
        "avatar": "https://cdn.example.com/g/10000001_5.png",
        "remark": "项目-A 协作群"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/sdk/group/detail 群详情

返回某个群的完整信息、成员 uid 列表、前 9 个加入者（用于拼接群头像）、以及当前用户的群二维码。

#### 接口地址

```
POST /v1/sdk/group/detail
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |

#### 请求示例

```json
{
  "tid": "g_10000001_5"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `group` | Group | 群结构，详见附录 |
| `members` | []int64 | 群成员 uid 列表 |
| `avatarUids` | []int64 | 最早加入的最多 9 个成员 uid（前端用于拼九宫格头像） |

`group.qrcode` 是按 `<uid, tid, 7 天后到期>` 加密生成的链接，重新调用 `detail` 会刷新（7 天有效）。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "group": {
      "tid": "g_10000001_5",
      "uid": 10000001,
      "name": "产品讨论群",
      "adminUids": [10000001, 10000002],
      "avatar": "https://cdn.example.com/g/10000001_5.png",
      "notice": "下周一上线发布会，请准时参加",
      "tag": "同事",
      "tagid": 1,
      "ctime": 1716700000000,
      "flag": 0,
      "qrcode": "http://qrcode.example.com/qrcode?tid=g_10000001_5&code=...",
      "qrcode_desc": "该二维码7天内(2026-06-02前)有效，重新进入将更新",
      "group_check_switch": "1",
      "group_chat_interval_time": "0",
      "max_cnt": 500,
      "group_sn": "100005",
      "intro": "讨论新产品迭代",
      "remark": "项目-A 协作群"
    },
    "members": [10000001, 10000002, 10000003],
    "avatarUids": [10000001, 10000002, 10000003]
  }
}
```

若群不存在，`data` 返回空对象（不报错）。

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/sdk/group/delete 解散群

仅群主可调用。解散后会给所有群成员发送群解散的系统消息，所有成员被踢出群。

#### 接口地址

```
POST /v1/sdk/group/delete
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |

#### 请求示例

```json
{
  "tid": "g_10000001_5"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误，群信息不存在 | 群不存在 |
| 403 | 非群主禁止解散群 | 调用方不是群主 |
| 500 | (内部 err) | socialevent 处理失败 |

---

### /v1/sdk/group/join 拉人入群

直接把指定用户加进群（不走审核）。已废弃使用，推荐用 `/v1/sdk/group/apply/join` 统一入口。

#### 接口地址

```
POST /v1/sdk/group/join
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `members` | []int64 | ✓ | 被拉入群的 uid 列表 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "members": [10000003, 10000004]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 群信息不存在 | 群不存在 |
| 403 | 群已解散 | flag=2 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/quit 主动退群

当前用户主动退出群聊。退群成功后会写一条退群事件，群成员收到状态同步。

#### 接口地址

```
POST /v1/sdk/group/quit
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |

#### 请求示例

```json
{
  "tid": "g_10000001_5"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 群信息不存在 | 群不存在 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

#### 备注

- 群主退群不允许，需先调用 `/v1/sdk/group/holder/change` 转让群主
- 退群后该用户在群的备注、免打扰、群昵称等会被清理

---

### /v1/sdk/group/kick 踢人

群主或管理员把指定成员踢出群。

#### 接口地址

```
POST /v1/sdk/group/kick
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `uid` | int64 |  | 被踢用户单值（与 `uids` 合并去重） |
| `uids` | []int64 |  | 被踢用户列表（与 `uid` 合并去重） |

`uid` 与 `uids` 至少一个非空。

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "uids": [10000005, 10000006]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 不能踢自己 | uids 包含调用方 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 非群管理员不允许操作 | 调用方不是管理员或群主 |
| 403 | 非群创建者不能踢出管理员 | 普通管理员尝试踢另一名管理员 |
| 403 | 群管理员不能踢出群主 | uids 中包含群主 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/sdk/group/apply/list 入群申请列表

群管理员查看本群的入群申请记录（带申请人用户信息）。

#### 接口地址

```
POST /v1/sdk/group/apply/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `skip` | int64 |  | 跳过条数，默认 0 |
| `limit` | int64 |  | 每页条数，默认 100，取值 [100, 1000) |
| `type` | int64 | ✓ | `0`=仅审核中 `-1`=全部 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "skip": 0,
  "limit": 100,
  "type": 0
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `groupRequests` | []GroupRequest | 申请记录列表 |
| `users` | []User | 申请人用户信息（与 `applyUid` 对应） |

GroupRequest 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 群 id |
| `applyUid` | int64 | 申请人 uid |
| `hello` | string | 申请时的打招呼语 |
| `scene` | string | 申请来源场景：`search` / `invite` / `qrcode` 等 |
| `applyTime` | int64 | 申请时间（毫秒） |
| `etime` | int64 | 申请过期时间（毫秒，默认 +3 天） |
| `status` | int64 | `0`=审核中 `1`=已通过 `2`=已拒绝 |
| `utime` | int64 | 记录更新时间（毫秒） |
| `ctime` | int64 | 记录创建时间（毫秒） |
| `inviteUid` | int64 | 邀请人 uid（搜索/扫码加群时是申请人自己） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "groupRequests": [
      {
        "tid": "g_10000001_5",
        "applyUid": 10000007,
        "hello": "我是销售部老李，麻烦通过下",
        "scene": "search",
        "applyTime": 1716700123456,
        "etime": 1716959323456,
        "status": 0,
        "utime": 1716700123456,
        "ctime": 1716700123456,
        "inviteUid": 10000007
      }
    ],
    "users": [
      {
        "uid": 10000007,
        "name": "老李",
        "nick": "销售-老李",
        "avatar": "https://cdn.example.com/u/10000007.png",
        "remark": ""
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | tid 为空或 type 非法 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 群已解散 | flag=2 |
| 403 | 没有权限 | 调用方不是管理员或群主 |
| 500 | (内部 err) | DB 失败 |

---

### /v1/sdk/group/apply/add 加群申请（旧入口）

旧版加群申请入口，逻辑被 `/v1/sdk/group/apply/join` 整合，建议新代码使用 `apply/join`。仍保留：用于扫码加群（带 `code` 时会解出 `tid`，强制把 `scene` 置为 `扫码`）。

行为：

- 若群未开启入群审核（`group_check_switch != "1"`），直接走 `/v1/sdk/group/join` 入群
- 若调用方是管理员，直接入群
- 否则写入 `group_request` 并给群主/管理员推 `group_apply` 系统消息

#### 接口地址

```
POST /v1/sdk/group/apply/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id（扫码时由 `code` 解出可省略） |
| `hello` | string |  | 打招呼内容 |
| `scene` | string |  | 申请场景（扫码会被覆盖为 `扫码`） |
| `members` | []int64 | ✓ | 被申请加入的用户 uid 列表 |
| `code` | string |  | 群二维码加密参数；填了 `code` 则忽略 `tid` |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "hello": "想加入产品讨论群",
  "scene": "search",
  "members": [10000007]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 二维码参数非法 / 二维码已经失效 | code 解密失败、签名不对或过期 |
| 400 | 群参数非法 / 成员参数非法 | tid 或 members 为空 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 群已解散 | flag=2 |
| 403 | 群成员已达上线 | 已达 `max_cnt` |
| 400 | 不是同一个主体，不能加群 | 跨 appkey |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/apply/join 加群统一入口

整合后的加群入口，自动判断"直接入群 / 需要审核"两种走向：

1. 调用方是群主/管理员，或扫码二维码的拥有者是群主/管理员 → 直接拉人入群
2. 群未开启审核（`group_check_switch != "1"`）→ 直接入群
3. 群开启审核 → 写入 `group_request` 并给群主和所有管理员发送 `group_apply_notify` 系统消息

非扫码场景下，调用方必须已经是本群成员；扫码场景以 `code` 解出的 `tid` 为准。

#### 接口地址

```
POST /v1/sdk/group/apply/join
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id（扫码时由 `code` 解出可省略） |
| `members` | []int64 | ✓ | 待加入的 uid 列表 |
| `hello` | string |  | 申请打招呼语，仅在审核分支生效 |
| `scene` | string |  | 来源：`search`-搜索 / `invite`-邀请 / `qrcode`-扫码（扫码会被覆盖） |
| `code` | string |  | 群二维码加密参数；填了 `code` 则忽略 `tid` 并自动定 `scene=qrcode` |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "members": [10000008],
  "hello": "我是新来的同事",
  "scene": "invite"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `status` | int | `0`=已直接入群 `1`=已提交入群申请待审核 |
| `msg` | string | 提示文案（审核分支返回"已提交入群申请，请等待审核"） |

#### 返回示例

直接入群：

```json
{ "code": 0, "msg": "", "data": { "status": 0 } }
```

待审核：

```json
{
  "code": 0,
  "msg": "",
  "data": { "status": 1, "msg": "已提交入群申请，请等待审核" }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 二维码参数非法 / 二维码已经失效 | code 解密失败、签名不对或过期 |
| 400 | 该用户不是群成员 | 非扫码场景下调用方不在群里 |
| 400 | 群参数非法 / 成员参数非法 | tid 或 members 为空 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 群已解散 | flag=2 |
| 403 | 群成员已达上限 | 已达 `max_cnt` |
| 400 | 不是同一个主体，不能加群 | 跨 appkey |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/apply/deal 处理入群申请

管理员同意或拒绝某条入群申请。

#### 接口地址

```
POST /v1/sdk/group/apply/deal
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `applyUid` | int64 | ✓ | 申请人 uid |
| `op` | int | ✓ | `1`=同意 `2`=拒绝 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "applyUid": 10000007,
  "op": 1
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | op 非 1/2，或 tid 为空 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 群已解散 | flag=2 |
| 403 | 没有权限 | 调用方不是管理员或群主 |
| 400 | 目标用户不存在 | 申请人或当前用户查不到 |
| 400 | 不是同一个主体，不能加群 | 跨 appkey |
| 400 | 入群申请记录不存在 | 没有这条申请 |
| 400 | 入群申请已经处理过，不能重复处理 | status != 0 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/black/list 群黑名单列表

返回当前群的黑名单 uid 列表（含禁言用户）。**所有群成员都可查看**（不限管理员）。返回前会做一次清理：已不在群里的黑名单 uid 会被同步移除。

#### 接口地址

```
POST /v1/sdk/group/black/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |

#### 请求示例

```json
{
  "tid": "g_10000001_5"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `members` | []int64 | 黑名单 uid 列表，无则返回空数组 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "members": [10000009]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | tid 为空 |
| 400 | 群信息不存在 | 群不存在 |
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/sdk/group/black/operator 群黑名单操作

群主或管理员把成员拉入或拉出群黑名单。

#### 接口地址

```
POST /v1/sdk/group/black/operator
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `uids` | []int64 | ✓ | 被操作的 uid 列表（去重） |
| `type` | int64 | ✓ | `0`=拉入黑名单 `1`=拉出黑名单 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "uids": [10000009],
  "type": 0
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | uids 为空或 type 非 0/1，或 tid 为空 |
| 400 | 不能操作自己 | uids 包含调用方 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 非群管理员不允许操作 | 调用方不是管理员或群主 |
| 403 | 有非群成员,不能操作 | uids 中有人不在群里 |
| 403 | 非群创建者不能拉黑管理员 | 管理员试图拉黑另一名管理员 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / Redis 失败 |

---

### /v1/sdk/group/profile/set 修改群信息

修改群名称、公告、头像、入群审核开关、各类成员发送权限等。除了基本群信息修改，部分字段（如 `group_check_switch`、`group_friend_invite_enter`、`group_member_alias` 等）仅群主或管理员可改；普通成员只能改自己有权限的字段且需是群成员。修改成功后会下发群信息变更的系统消息。

#### 接口地址

```
POST /v1/sdk/group/profile/set
Content-Type: application/json
```

#### 请求参数

字段全部可选，按需带；未传字段不会被改动。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `name` | string |  | 群名称 |
| `avatar` | string |  | 群头像 url |
| `notice` | string |  | 群公告 |
| `intro` | string |  | 群简介 |
| `group_check_switch` | string |  | 入群审核：`1`=需要审核，其余不审核 |
| `group_allow_member_list` | string |  | 允许普通成员查看成员列表：`1`=允许 |
| `group_allow_member_visit` | string |  | 允许普通成员访问他人个人页：`1`=允许 |
| `group_friend_invite_enter` | string |  | 允许普通成员邀请好友入群：`1`=允许 |
| `group_ban_chat` | string |  | 全体禁言（仅群主/管理员可发言）：`1`=禁言 |
| `group_chat_interval_time` | string |  | 普通成员发言间隔（秒），`0`=不限 |
| `group_member_alias` | string |  | 允许成员设置群昵称：`1`=允许 |
| `group_member_image` | string |  | 允许发送图片：`1`=允许 |
| `group_member_video` | string |  | 允许发送视频：`1`=允许 |
| `group_member_voice` | string |  | 允许发送语音：`1`=允许 |
| `group_member_doc` | string |  | 允许发送文件：`1`=允许 |
| `group_member_pkg` | string |  | 允许发红包：`1`=允许 |
| `group_member_quit_msg` | string |  | 允许显示退群消息：`1`=允许 |
| `join_notice` | int64 |  | 入群是否发系统消息：`1`=发送 `0`=不发送 |
| `group_admin_special` | string |  | 管理员/群主的发言字体设置（JSON 字符串） |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "name": "产品讨论群-2026Q2",
  "notice": "下周一上线发布会，请准时参加",
  "group_check_switch": "1",
  "group_chat_interval_time": "5"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | tid参数错误 | tid 为空 |
| 403 | 不是管理员不允许修改 | 修改的字段中含管理员专属字段但调用方不是管理员 |
| 403 | 不是该群成员不允许修改 | 调用方不在群里 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/holder/change 群主转让

仅群主本人可调用，被转让用户必须是当前群成员。换人成功后会给所有群成员推送群主变更的系统消息。新群主若在黑名单里会被自动移除。

#### 接口地址

```
POST /v1/sdk/group/holder/change
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `toUid` | int64 | ✓ | 新群主 uid |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "toUid": 10000002
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 群信息不存在 | 群不存在 |
| 403 | 群已解散 | flag=2 |
| 400 | 参数错误，新群主必须是当前群成员 | toUid 不在群里 |
| 400 | 参数错误，当前用户不是群主 | 调用方不是群主 |
| 400 | 参数错误，群信息无变化 | 新旧群主相同 |
| 400 | (业务文案) | socialevent 业务校验失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/member/remark 设置我在群里的昵称

当前用户为自己在该群设置群昵称。需群设置 `group_member_alias=1` 才允许（由 socialevent 内部校验）。

#### 接口地址

```
POST /v1/sdk/group/member/remark
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `remark` | string |  | 新的群昵称，空字符串表示清除 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "remark": "产品-小张"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | tid 为空 |
| 400 | 群信息不存在 | 群不存在 |
| 400 | (业务文案) | 群禁止修改昵称或其他业务规则 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/qrcode/apply/add 扫码加群

扫码加群专用入口。会先校验二维码 `code` 合法、未过期，并要求 `members[0]` 必须是当前调用方本人；通过后转入 `/v1/sdk/group/apply/add` 走标准入群申请流程。

#### 接口地址

```
POST /v1/sdk/group/qrcode/apply/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id（必须与 `code` 解出的 tid 一致） |
| `hello` | string |  | 打招呼内容 |
| `members` | []int64 | ✓ | 加群人 uid，首个必须是调用方自己 |
| `code` | string | ✓ | 群二维码加密串 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "hello": "扫码加入",
  "members": [10000010],
  "code": "<encrypted>"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数非法 | members 或 code 为空，或 members[0] 不是调用方 |
| 400 | 二维码失效 / 二维码非法 / 二维码参数非法 / 二维码已经失效 | code 解密失败、签名不对、过期或与 tid 不匹配 |
| 400 | 群参数非法 | code 解出的 tid 为空 |
| 后续错误 | (转入 `/v1/sdk/group/apply/add` 的错误码) | 见上 |

---

### /v1/sdk/group/admin 设置/取消群管理员

群主把指定用户设为管理员或取消。失败成员（不在群、重复设置）会拼接在 `msg` 字段里返回，整体仍是 `code=0`。

#### 接口地址

```
POST /v1/sdk/group/admin
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `uids` | []int64 | ✓ | 被操作的 uid 列表 |
| `op` | string | ✓ | `set`=设为管理员 `unset`=取消管理员 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "uids": [10000002, 10000003],
  "op": "set"
}
```

#### 返回示例

```json
{
  "code": 0,
  "msg": "[10000003] 参数错误被设置用户不在群里"
}
```

`msg` 为空表示全部成功；非空时包含每条失败原因，以 `\r\n` 拼接。

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 群信息不存在 | 群不存在 |
| 400 | (业务文案) | socialevent 业务校验失败（非群主等） |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/group/send/msg/interval/page 群发言间隔选项页

返回群发言间隔的可选选项（设置页面用）。仅群管理员可访问。

#### 接口地址

```
POST /v1/sdk/group/send/msg/interval/page
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |

#### 请求示例

```json
{
  "tid": "g_10000001_5"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | 页面标题（本地化） |
| `labels` | []Label | 可选项列表 |

Label 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `interval_second` | int64 | 间隔秒数：`0`=无间隔 / `3` / `5` / `15` / `30` / `60` / `300` |
| `interval_desc` | string | 描述文案（本地化） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "title": "一般成员发话间隔设置",
    "labels": [
      { "interval_second": 0,   "interval_desc": "无间隔" },
      { "interval_second": 3,   "interval_desc": "3秒" },
      { "interval_second": 5,   "interval_desc": "5秒" },
      { "interval_second": 15,  "interval_desc": "15秒" },
      { "interval_second": 30,  "interval_desc": "30秒" },
      { "interval_second": 60,  "interval_desc": "1分钟" },
      { "interval_second": 300, "interval_desc": "5分钟" }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | tid 为空 |
| 400 | 群信息不存在 | 群不存在 |
| 403 | 非群管理员不允许操作 | 调用方不是管理员或群主 |
| 500 | (内部 err) | DB 失败 |

---

### /v1/sdk/group/search 群搜索

按群号或群名（模糊）搜索本 appkey 下的群，返回带成员数与"我是否在群"标记的群列表。

#### 接口地址

```
POST /v1/sdk/group/search
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `keyword` | string | ✓ | 群号（精确匹配 `group_sn`）或群名（模糊匹配 `name`） |
| `pageSize` | int64 |  | 每页条数，默认 20，最大 200 |
| `pageIndex` | int64 |  | 第几页（从 0 开始） |

#### 请求示例

```json
{
  "keyword": "产品讨论",
  "pageSize": 20,
  "pageIndex": 0
}
```

#### 返回参数

`data` 直接是 `[]Group` 数组，结构详见附录；额外字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `memberCnt` | int64 | 当前群成员总数 |
| `im_in` | bool | 当前用户是否已在该群 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "tid": "g_10000001_5",
      "uid": 10000001,
      "name": "产品讨论群",
      "tag": "同事",
      "tagid": 1,
      "ctime": 1716700000000,
      "flag": 0,
      "group_sn": "100005",
      "intro": "讨论新产品迭代",
      "memberCnt": 12,
      "im_in": true
    }
  ]
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | keyword 为空 |
| 500 | (内部 err) | DB 失败 |

## 会话

### /v1/sdk/topic/question/create 创建产品/订单咨询群

按用户身份 + 咨询的产品或订单创建客服群（通过 quan5 rpcx 服务）。

#### 接口地址

```
POST /v1/sdk/topic/question/create
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uid` | int64 | ✓ | 发起咨询的用户 uid |
| `scene` | int32 | ✓ | 咨询场景，`3`=产品咨询群、`4`=订单咨询群；其它值会被拒 |
| `productCode` | string |  | 产品 id（`scene=3` 必填） |
| `orderCode` | string |  | 订单号（`scene=4` 必填，自动剥离前缀 `订单编码:`） |
| `appkey` | string |  | 服务端会自动用 uid 查到的 appkey 覆盖，可不填 |
| `avatar` | string |  | 群头像 |

#### 请求示例

```json
{
  "uid": 10000001,
  "scene": 4,
  "orderCode": "订单编码:SO20260415000812",
  "avatar": "https://cdn.example.com/avatar/order.png"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 新建的咨询群会话 id |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "tid": "g_10000001_812" }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 场景id错误 | `scene` 不是 3 / 4 |
| 400 | 用户不存在 | `uid` 查不到用户 |
| 500 | (内部 err) | quan5 rpcx 调用失败 |

---

### /v1/sdk/topic/create 创建会话

创建单聊或群聊会话。单聊根据双方 uid 拼接 `s_<minUid>_<maxUid>`；群聊生成 `g_<creatorUid>_<seq>` 并建立成员关系。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | ✓ | `single`=单聊、`group`=群聊 |
| `members` | []int64 | ✓ | 成员 uid 列表，单聊填对端 uid（自己会被自动加入），群聊填初始成员 uid |
| `name` | string |  | 群名（`type=group` 必填，单聊忽略） |
| `avatar` | string |  | 群头像 |
| `intro` | string |  | 群简介 |
| `cid` | string |  | 客户端生成的去重 id |
| `scene` | int32 |  | 创建场景，`0`=普通、`1`=客服页面唤起、`2`=B 端企业自动建群 |
| `sceneid` | string |  | 场景关联数据，例如 B 端企业 appkey |

#### 请求示例

```json
{
  "type": "group",
  "name": "供应链对账群",
  "avatar": "https://cdn.example.com/avatar/g_812.png",
  "intro": "2026Q1 对账协作",
  "cid": "cid-9c4f1a2b3d0e1234",
  "members": [10000002, 10000003, 10000004],
  "scene": 0,
  "sceneid": ""
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `topicid` | string | 会话 id |
| `type` | string | `single` / `group` |
| `tag` | string | 群标签：内部群、供应链、服务商、供应商（群聊才有） |
| `tagid` | int32 | 群标签 id |
| `topic` | TopicInfo | 会话详情，结构见附录 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "topicid": "g_10000001_812",
    "type": "group",
    "tag": "供应链",
    "tagid": 2,
    "topic": {
      "tid": "g_10000001_812",
      "uid": 10000001,
      "name": "供应链对账群",
      "cid": "cid-9c4f1a2b3d0e1234",
      "ctime": 1716691200000,
      "type": "group",
      "sid": 0,
      "hidemid": "",
      "order": 0,
      "lvTime": 0,
      "lsMid": "",
      "remark": "",
      "mute": false
    }
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type` 不在枚举内 |
| 400 | 成员不能为空 | 单聊 members 为空 |
| 400 | 创建群名称不能为空 | 群聊 name 为空 |
| 403 | 超过群成员上限 | members 超出 appkey 配置上限 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/topic/list 会话列表

按最近活跃倒序拉取当前用户的会话；可同步拉置顶会话。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skip` | int64 | ✓ | 跳过条数，`≥0` |
| `size` | int64 | ✓ | 本次返回条数，`>0` |
| `order` | int64 |  | 置顶时间阈值；传 `>0` 时同时返回该值之后的置顶会话 |

#### 请求示例

```json
{
  "skip": 0,
  "size": 30,
  "order": 1716691200000
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 普通会话总数 |
| `topics` | []TopicInfo | 普通会话列表 |
| `topTotal` | int64 | 置顶会话数（请求带 `order` 才返回） |
| `topTopics` | []TopicInfo | 置顶会话列表 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 12,
    "topics": [
      {
        "tid": "g_10000001_812",
        "uid": 10000001,
        "name": "供应链对账群",
        "type": "group",
        "sid": 1716694800123,
        "order": 0,
        "lvTime": 1716694500000,
        "lsMid": "g_10000001_812:2048",
        "remark": "",
        "mute": false
      },
      {
        "tid": "s_10000001_10000002",
        "uid": 10000002,
        "type": "single",
        "sid": 1716694700987,
        "order": 0,
        "lvTime": 1716694200000,
        "lsMid": "s_10000001_10000002:1530",
        "remark": "采购张三",
        "mute": false
      }
    ],
    "topTotal": 1,
    "topTopics": [
      {
        "tid": "g_10000001_700",
        "uid": 10000001,
        "name": "公告群",
        "type": "group",
        "sid": 1716691200000,
        "order": 1716691800000,
        "lvTime": 1716691500000,
        "lsMid": "g_10000001_700:880",
        "remark": "",
        "mute": true
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | Size或Skip参数错误 | `size<=0` 或 `skip<0` |
| 500 | (内部 err) | Redis / DB 失败 |

#### 备注

- 单聊对端用户状态非 `open`（已封禁/删除）的会话会被即时清理，`total` 也会相应递减
- 群聊中当前用户已不在群的会话同样会被即时清理

---

### /v1/sdk/topic/detail 会话详情

批量拉取会话基本信息，并在用户漏接收到该会话时回填到 Redis 索引。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tidList` | []string | ✓ | 会话 id 列表 |

#### 请求示例

```json
{
  "tidList": [
    "g_10000001_812",
    "s_10000001_10000002"
  ]
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `topics` | []TopicInfo | 会话信息，结构见附录 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "topics": [
      {
        "tid": "g_10000001_812",
        "uid": 10000001,
        "name": "供应链对账群",
        "type": "group",
        "sid": 0,
        "ctime": 1716691200000,
        "order": 0,
        "lvTime": 0,
        "lsMid": "",
        "remark": "",
        "mute": false
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB / Redis 失败 |

#### 备注

- 单聊 `tid` 必须包含当前 uid，否则会被丢弃
- 不存在的会话会被跳过，不报错

---

### /v1/sdk/topic/user/detail 会话成员资料

拉取某会话中指定 uid 列表的用户资料，群备注/昵称会合并进来。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `uids` | []int64 | ✓ | 要查询的 uid 列表 |

#### 请求示例

```json
{
  "tid": "g_10000001_812",
  "uids": [10000001, 10000002, 10000003]
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `users` | []UserDao | 用户资料列表，字段同 `/v1/sdk/user/detail`；`remark` 会优先取群备注 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "users": [
      {
        "uid": 10000002,
        "appkey": "appkey_xxxx",
        "mainAppkey": "main_xxxx",
        "avatar": "https://cdn.example.com/avatar/10000002.png",
        "name": "张三",
        "nick": "采购张三",
        "remark": "供应链-张三"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `uids` 或 `tid` 为空 |
| 403 | 您无权查看该用户信息 | 目标用户不在同一 mainAppkey（非机器人）|
| 500 | (内部 err) | DB 失败 |

---

### /v1/sdk/topic/remark 设置群备注（旧接口）

旧版设置会话备注的接口，前端已经不再使用，新版统一走 `/v1/sdk/topic/modify`。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `remark` | string | ✓ | 备注内容 |

#### 请求示例

```json
{
  "tid": "g_10000001_812",
  "remark": "Q1 对账"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 会话标识格式错误 | `tid` 不能解析 |
| 400 | 该会话不属于该用户 | 当前 uid 不在群里 / 不是单聊一方 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB 失败 |

#### 备注

- 已废弃，新代码用 `/v1/sdk/topic/modify` 的 `remark` 字段

---

### /v1/sdk/topic/modify 修改会话状态

会话级状态的统一入口：可改置顶、隐藏、上次打开时间、最大已展示消息 id、备注。所有可选字段同一次请求中可任意组合，未传的字段保持不变。原 `/sdk/userobj/get|set` 已并入此接口。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `hidemid` | *string |  | 隐藏分界消息 id，`mid` 之前的消息隐藏；非空时必须以 `tid:` 开头 |
| `ontop` | *bool |  | `true`=置顶（写入当前时间到 order）、`false`=取消置顶 |
| `lvTime` | *int64 |  | 上次打开时间（毫秒） |
| `lsMid` | *string |  | 已展示的最大消息 id，用于会话计数；非空时必须以 `tid:` 开头 |
| `remark` | *string |  | 会话备注 |

#### 请求示例

```json
{
  "tid": "g_10000001_812",
  "ontop": true,
  "lvTime": 1716694800000,
  "lsMid": "g_10000001_812:2050",
  "remark": "Q1 对账"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `topic` | TopicDao | 修改后的整条会话记录 |

`TopicDao` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 会话 id |
| `type` | string | `single` / `group` |
| `hidemid` | string | 隐藏分界 mid |
| `mute` | bool | 是否静音 |
| `order` | int64 | 置顶时间戳（毫秒），`0`=未置顶 |
| `lvTime` | int64 | 上次打开时间（毫秒） |
| `lsMid` | string | 已展示的最大 mid |
| `remark` | string | 备注 |
| `ctime` | int64 | 会话创建时间（毫秒） |
| `groupremark` | string | 用户在群里的昵称 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "topic": {
      "tid": "g_10000001_812",
      "type": "group",
      "hidemid": "",
      "mute": false,
      "order": 1716694800123,
      "lvTime": 1716694800000,
      "lsMid": "g_10000001_812:2050",
      "remark": "Q1 对账",
      "ctime": 1716691200000,
      "groupremark": ""
    }
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 会话标识格式错误 | `tid` 不能解析 |
| 400 | 该会话不属于该用户 | 当前 uid 不在群里 / 不是单聊一方 |
| 403 | 参数非法 | `hidemid` / `lsMid` 不包含 `tid`，疑似跨会话越权 |
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB 失败 |

#### 备注

- `hidemid` / `lsMid` 的安全规则沿用旧 `userobj/set`：必须带 `tid:` 前缀，防止把别人会话的 mid 写到自己的状态里
- 修改后会通过状态同步推到该用户的其他端

---

## 消息

### /v1/sdk/msg/send 发送消息

#### 接口地址

```
POST /v1/sdk/msg/send
Content-Type: application/json
```

#### 请求头

| Header | 必填 | 说明 |
|---|---|---|
| `X-Token` | ✓ | 登录拿到的 token |
| `X-Lang` |  | `zh` / `en` / `ja` / `vi` / `es` / `pt`，默认 `zh` |
| `KK-Version` |  | 客户端版本 |

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `toTopidId` | string | ✓ | 目标会话 id：群聊 `g_<群id>`，单聊 `s_<minUid>_<maxUid>` |
| `cid` | string | ✓ | 客户端生成的唯一消息 id，长度 ≥ 13，用于客户端去重 |
| `type` | string | ✓ | 消息类型，枚举见下表 |
| `content` | string | ✓ | 消息正文（type 不同语义不同：文本=字符串；文件/图片=url 或 JSON 元数据） |
| `atUids` | []int64 |  | 群聊中被 @ 的用户 uid 列表；单聊不用 |
| `meta` | map[string]string |  | 自定义 KV，客户端透传字段 |
| `fromUserId` | int64 |  | 指定发送者（要求超级管理员权限，用于机器人代发；普通用户填 0 或省略） |
| `encrypt` | int32 |  | `1`=正文已加密（key = `MD5(MD5(appkey)+"-"+cid)`，AES）；其它=明文 |

**`type` 枚举**

| 值 | 含义 |
|---|---|
| `empty` | 空消息（不展示） |
| `txt` | 文本消息 |
| `tip` | 提示信息 |
| `file` | 文件 |
| `img` | 图片 |
| `voice` | 语音 |
| `video` | 视频 |
| `voice_rtc` | RTC 语音通话 |
| `video_rtc` | RTC 视频通话 |
| `talk` | 转发消息 |
| `coupon` | 红包 |
| `quan5_prod` | 全屋产品消息（业务专用） |
| `quan5_order` | 全屋订单消息（业务专用） |

#### 请求示例

**例 1：单聊文本**

```json
{
  "toTopidId": "s_10000245_10000246",
  "cid": "client_abc_1717000000_001",
  "type": "txt",
  "content": "Hello"
}
```

**例 2：群聊带 @**

```json
{
  "toTopidId": "g_10000001_5",
  "cid": "client_abc_1717000000_002",
  "type": "txt",
  "content": "请相关同学留意 @张三 @李四",
  "atUids": [10000300, 10000301],
  "meta": { "scene": "daily_report" }
}
```

**例 3：图片**

```json
{
  "toTopidId": "g_10000001_5",
  "cid": "client_abc_1717000000_003",
  "type": "img",
  "content": "{\"url\":\"https://cdn.example.com/img/x.jpg\",\"w\":1080,\"h\":1920,\"size\":234567}"
}
```

**例 4：加密文本（端到端）**

```json
{
  "toTopidId": "s_10000245_10000246",
  "cid": "client_abc_1717000000_004",
  "type": "txt",
  "content": "<AES Base64 ciphertext>",
  "encrypt": 1
}
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `mid` | string | 消息 id，格式 `<topicId>:<tmid>`，全局唯一 |
| `auditWord` | string | 命中的敏感词（命中时返回，未命中省略） |
| `auditWordTip` | string | 敏感词命中时的展示提示（如"含违规词，已隐藏"），客户端可直接显示 |
| `replys` | []MsgReply | 即便单条也返回数组，便于和 batch 接口保持 schema 一致 |

`MsgReply` 子结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `mid` | string | 同上 |
| `auditWord` | string | 同上 |

#### 返回示例

**成功**

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "mid": "s_10000245_10000246:100",
    "auditWordTip": "",
    "replys": [
      { "mid": "s_10000245_10000246:100" }
    ]
  }
}
```

**命中敏感词（消息照常发，正文会被替换 / 折叠）**

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "mid": "s_10000245_10000246:101",
    "auditWord": "违规词",
    "auditWordTip": "含违规词，已隐藏",
    "replys": [
      { "mid": "s_10000245_10000246:101", "auditWord": "违规词" }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 客户端生成的唯一的消息Id必填 / 格式不正确 / 消息类型错误 / 无消息内容 / 目标为空 | Req 字段校验 |
| 400 | 单聊会话Id参数错误 / 无权进行该单聊 | tid 解析失败或不属于当前 user |
| 400 | 目标用户不存在 / 群不存在 | tid 对应实体不存在 |
| 400 | 禁言中, 请稍后再试 | appkey 级禁言 |
| 400 | 含敏感词的本地提示 | 用户级敏感词命中且不在白名单 |
| 1007 | 不在群里无权发送 / 管理员设置禁止发图片/视频/语音/文件/红包 / 禁言中 / 被禁言无权发送 | 群权限 |
| 1010 | 不属于同一个主体 / 单聊目标用户不属于同一个主体 | 跨 appkey |
| 2010 | 对方已将你删除 / 非双边好友关系不能发消息 | 单聊好友关系 |
| 2011 | 对方在你的黑名单不能发送消息 / 你在对方的黑名单不能发送消息 | 单聊黑名单 |
| 500 | (内部 err) | DB / 敏感词 RPC / socialevent handler 失败 |

#### 备注

- **加密**：仅 `type=txt` 时支持 `encrypt=1`，其它类型字段不解密
- **机器人**：单聊目标是机器人 uid 时跳过好友关系/黑名单检查（业务约定）
- **批量**：单次发多条用 `/v1/sdk/msg/sendbatch`，schema 接近，`data.replys` 多元素

---

### /v1/sdk/msg/sendbatch 批量发送同一条消息到多个会话

同一条消息正文一次推到多个目标会话（群 / 单聊混合）。失败按会话分桶返回，不是整批回滚。

#### 接口地址

```
POST /v1/sdk/msg/sendbatch
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `cid` | string | ✓ | 客户端生成的唯一消息 id，长度 ≥ 13 |
| `toTopicIds` | []string | ✓ | 目标会话 id 列表，群 `g_xxx`、单聊 `s_min_max` 可混合 |
| `type` | string | ✓ | 消息类型，枚举同 `/v1/sdk/msg/send` |
| `content` | string | ✓ | 消息正文 |
| `encrypt` | int32 |  | `1`=正文已加密（同 `/v1/sdk/msg/send`） |
| `meta` | map[string]string |  | 自定义 KV |

#### 请求示例

```json
{
  "cid": "client_abc_1717000000_010",
  "toTopicIds": [
    "g_10000001_5",
    "g_10000001_6",
    "s_10000245_10000246"
  ],
  "type": "txt",
  "content": "公司本周五全员大会，请准时参加"
}
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `auditWord` | string | 命中的敏感词（命中时返回） |
| `auditWordTip` | string | 敏感词命中时的展示提示 |
| `topicNotFound` | []string | 目标会话不存在（群不存在 / 对端用户不存在 / 跨 appkey） |
| `topicNotIn` | []string | 用户未在会话中（不是群成员 / 非双边好友） |
| `topicNotAllowType` | []string | 群管理员禁止该类消息（图片/视频/语音/文件/红包） |
| `topicMute` | []string | 群禁言中或用户被禁言 |
| `topicBlock` | []string | 黑名单关系（互拉黑均归此桶） |
| `topicSendFail` | []string | 后端推送失败（DB / RPC 异常） |

> 没出现在任何桶里的 `toTopicIds` 视为发送成功。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "topicNotFound": [],
    "topicNotIn": ["g_10000001_6"],
    "topicNotAllowType": [],
    "topicMute": [],
    "topicBlock": [],
    "topicSendFail": []
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 客户端生成的唯一的消息Id必填 / 格式不正确 / 消息类型错误 / 无消息内容 | Req 字段校验 |
| 400 | 禁言中, 请稍后再试 | appkey 级禁言 |
| 400 | 含敏感词的本地提示 | 用户级敏感词命中且不在白名单 |
| 400 | 参数错误发送目标不能为空 | 全部目标均被过滤掉 |
| 500 | (内部 err) | DB / 敏感词 RPC 失败 |

#### 备注

- 整个请求只算一条消息的敏感词检测，结果应用到所有会话
- 单条目标级别的失败不会让整个接口返回非 0；要看 `data` 里 6 个桶
- 想给单一目标做完整的语义化错误信息，用 `/v1/sdk/msg/send` 单发

---

### /v1/sdk/msg/latest 拉取最近消息（多端同步）

客户端启动 / 重连后增量同步：根据本地 `sid` 拉取这之后变更过的会话及其最新消息，并把本端最后已读的 `mid` ack 给服务端。

#### 接口地址

```
POST /v1/sdk/msg/latest
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `sid` | int64 |  | 本端已知的最小 sid，左开区间（取消息时实际用 `sid+1`）；首次同步传 `0` |
| `maxSid` | int64 |  | 拉取上界 sid，`≤0` 表示无穷大；用于 `leftReq` 分页续拉 |
| `ack` | []TopicAck |  | 各会话本端已读的最大 mid，服务端记录用于多端同步 |

`TopicAck` 子结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 会话 id |
| `mid` | string | 该会话本端已读到的最大 mid（必须以 `tid` 为前缀） |

#### 请求示例

```json
{
  "sid": 0,
  "ack": [
    { "tid": "g_10000001_5", "mid": "g_10000001_5:1200" },
    { "tid": "s_10000245_10000246", "mid": "s_10000245_10000246:88" }
  ]
}
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `nsid` | int64 | 下次同步用的 sid（取本批最大值） |
| `topics` | []TopicMsgRsp | 本次返回的会话最新消息，最多 200 个 |
| `leftReq` | object | 还有未拉取完的会话；不为空时把它的 `sid`/`maxSid` 直接当下次请求参数继续拉 |

`TopicMsgRsp` 子结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 会话 id |
| `sid` | int64 | 该会话的 sid（按变更时间打分） |
| `minMsgId` | string | 本次返回消息范围下界（开区间 ack 之上） |
| `maxMsgId` | string | 本次返回消息范围上界 |
| `next` | bool | 该会话单次没拉完，需要再 `/v1/sdk/msg/history` 补齐 |
| `msgs` | []MsgDao | 消息正文列表（结构见 [MsgDao](#msgdao-消息结构)） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "nsid": 17170012345,
    "topics": [
      {
        "tid": "g_10000001_5",
        "sid": 17170012345,
        "minMsgId": "g_10000001_5:1200",
        "maxMsgId": "g_10000001_5:1215",
        "next": false,
        "msgs": [
          { "mid": "g_10000001_5:1215", "tid": "g_10000001_5", "cid": "client_xxx", "sender": 10000300, "stime": 1717001234567, "type": "txt", "content": "晚上吃啥" }
        ]
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | TopicAck参数错误 | `ack[].mid` 不是以 `ack[].tid` 为前缀 |
| 500 | (内部 err) | Redis / Mongo 查询失败 |

#### 备注

- 首次同步（`sid=0`）每个会话仅返回最近 20 条，全量靠 `/v1/sdk/msg/history`
- 单次响应最多 2000 条消息（gzip 后 ≈1MB），先单聊后群聊
- 没有新消息的会话不会出现在 `topics` 里，但 `nsid` 仍可能推进

---

### /v1/sdk/msg/history 历史消息查询

按 `mid` / 时间 / 类型组合查询某个会话的历史消息，单次最多 2000 条。

#### 接口地址

```
POST /v1/sdk/msg/history
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `minMsgId` | string |  | 最小 mid 边界（默认开区间） |
| `maxMsgId` | string |  | 最大 mid 边界（默认开区间） |
| `minTime` | int64 |  | 最小发送时间（毫秒） |
| `maxTime` | int64 |  | 最大发送时间（毫秒） |
| `containMin` | bool |  | 是否包含 min 边界 |
| `containMax` | bool |  | 是否包含 max 边界 |
| `type` | []string |  | 消息类型过滤，枚举同 `/v1/sdk/msg/send` |
| `reverse` | bool |  | `false`（默认）= mid 倒序，`true` = mid 正序 |
| `size` | int64 |  | 单次返回条数上限，最大 2000；不传或 ≤0 用默认 |

> 所有过滤条件都不传时，按"取最近"语义处理。

#### 请求示例

**例 1：拉某个 mid 之前的 50 条**

```json
{
  "tid": "g_10000001_5",
  "maxMsgId": "g_10000001_5:1200",
  "size": 50
}
```

**例 2：按时间窗口 + 类型过滤**

```json
{
  "tid": "g_10000001_5",
  "minTime": 1716998400000,
  "maxTime": 1717084800000,
  "type": ["img", "video"],
  "size": 200
}
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `next` | bool | 本次未查完，客户端需调小 mid 边界继续拉 |
| `msgs` | []MsgDao | 消息正文列表，结构见 [MsgDao](#msgdao-消息结构) |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "next": true,
    "msgs": [
      { "mid": "g_10000001_5:1199", "tid": "g_10000001_5", "sender": 10000301, "stime": 1717001230000, "type": "txt", "content": "..." }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误会话Id不能为空 / mid 格式不正确 / MinTime或MaxTime格式不正确 / TopicId参数错误 | Req 字段校验 |
| 1008 | 用户未加入该会话 | 群非成员；单聊非双方 |
| 500 | (内部 err) | Mongo / Redis 查询失败 |

#### 备注

- 群聊会被裁剪到当前用户允许看到的最大 mid（入群前 / 退出后的消息看不到）
- 客户端本端"删除会话"会写一个 `minMsgId` 标记，请求会自动抬到该边界之上

---

### /v1/sdk/msg/detail 消息状态/详情

根据 `mid` 列表批量查每条消息当前的状态（已读人数、@ 已读、撤回标记、可见性、审核结果、RTC 时长等）。

#### 接口地址

```
POST /v1/sdk/msg/detail
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mids` | []string | ✓ | 消息 id 列表 |

#### 请求示例

```json
{
  "mids": [
    "g_10000001_5:1215",
    "s_10000245_10000246:88"
  ]
}
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `records` | []Record | 每条消息的状态记录 |

`Record` 子结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `mid` | string | 消息 id |
| `sender` | int64 | 发送方 uid |
| `targetCnt` | int32 | 目标人数（仅发送者本人能看到下列 `target*`/`read*`/`at*` 字段） |
| `targetUids` | []int64 | 目标 uid 列表 |
| `readCnt` | int32 | 已读人数 |
| `readUids` | []int64 | 已读 uid 列表 |
| `atCnt` | int32 | 被 @ 的人数 |
| `atUids` | []int64 | 被 @ 的 uid 列表 |
| `atReadCnt` | int32 | 被 @ 已读人数 |
| `atReadUids` | []int64 | 被 @ 已读 uid 列表 |
| `mask` | int32 | `0`=正常 `1`=删除 `2`=撤回 |
| `visible` | int32 | `0`=所有人可见 `1`=自己+管理员可见 `-1`=自己+管理员不可见 `-2`=所有人不可见 |
| `auditRst` | string | 审核结果：`pass` / `refused` / 空 |
| `rtcId` | int64 | RTC 通话 id（RTC 消息） |
| `rtcDuration` | int32 | RTC 通话时长（秒） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "records": [
      {
        "mid": "g_10000001_5:1215",
        "sender": 10000300,
        "targetCnt": 5,
        "targetUids": [10000300,10000301,10000302,10000303,10000304],
        "readCnt": 3,
        "readUids": [10000301,10000302,10000303],
        "atCnt": 1,
        "atUids": [10000301],
        "atReadCnt": 1,
        "atReadUids": [10000301],
        "mask": 0,
        "visible": 0
      },
      {
        "mid": "s_10000245_10000246:88",
        "sender": 10000245,
        "mask": 2
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | Mongo 查询失败 |

#### 备注

- 不是发送者本人时，`target*`/`read*`/`at*` 不返回
- `mask=2`（撤回）后 `content` 在 `/v1/sdk/msg/history` / `/v1/sdk/msg/latest` 中会被清掉

---

### /v1/sdk/msg/mask 撤回 / 删除标记

把一条消息标记为撤回或删除。撤回会广播给会话内所有人，删除只影响发起人侧的展示。

#### 接口地址

```
POST /v1/sdk/msg/mask
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mid` | string | ✓ | 消息 id |
| `mask` | int32 | ✓ | `1`=删除 `2`=撤回 |

#### 请求示例

```json
{ "mid": "g_10000001_5:1215", "mask": 2 }
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | Mask参数错误 / MsgId参数错误 | Req 字段校验 |
| 400 | (业务文案) | socialevent 校验失败：非本人消息撤回、超过撤回时限、非管理员撤回他人消息等 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 撤回会推 `MsgMaskEvent` 事件给会话所有在线端，详见 wsevents 文档
- 删除仅写发起人本地标记，不影响其他端

---

### /v1/sdk/msg/delete 会话消息删除（截止 mid）

把某个会话 `mid ≤ <maxMsgId>` 的所有消息从当前用户视角抹掉，常用于"清空聊天记录"。

#### 接口地址

```
POST /v1/sdk/msg/delete
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `mid` | string | ✓ | 删除截止 mid（必须以 `tid` 为前缀） |

#### 请求示例

```json
{ "tid": "g_10000001_5", "mid": "g_10000001_5:1215" }
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误不能为空 / 参数错误消息Id格式不正确 | Req 字段校验 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 仅写"用户级最小 mid 边界"，原始消息不会真删
- 后续 `/v1/sdk/msg/history` / `/v1/sdk/msg/latest` 会自动跳过该边界以下的消息

---

### /v1/sdk/msg/delete/batch 批量会话消息删除

`/v1/sdk/msg/delete` 的多会话批量版，逐条按 (`tid`, `mid`) 写边界。

#### 接口地址

```
POST /v1/sdk/msg/delete/batch
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `delete_infos` | []DeleteInfo | ✓ | 多个 (会话, 截止 mid) 对 |

`DeleteInfo` 子结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 会话 id |
| `mid` | string | 删除截止 mid（必须以 `tid` 为前缀） |

#### 请求示例

```json
{
  "delete_infos": [
    { "tid": "g_10000001_5", "mid": "g_10000001_5:1215" },
    { "tid": "s_10000245_10000246", "mid": "s_10000245_10000246:88" }
  ]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误不能为空 / 参数错误消息Id格式不正确 | Req 字段校验（任一条不合法即整批退） |
| 500 | (无 msg) | 有条目写入失败 |

---

### /v1/sdk/msg/local/delete 删除指定消息（多端同步）

把某些 `mid` 加入"本端隐藏"集合，并通过事件推给当前用户其它在线端，让多端表现一致。

#### 接口地址

```
POST /v1/sdk/msg/local/delete
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `mids` | []string | ✓ | 要隐藏的 mid 列表（必须以 `tid` 为前缀，非法元素被丢弃） |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "mids": [
    "g_10000001_5:1210",
    "g_10000001_5:1212"
  ]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误不能为空 | `tid` / `mids` 为空 |
| 400 | (业务文案) | socialevent 多端通知失败 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 与 `/v1/sdk/msg/delete` 区别：`delete` 是一刀切到 mid 阈值，`local/delete` 是按 mid 精挑

---

### /v1/sdk/msg/translate 文本翻译

把指定 `mid` 的文本消息翻译到目标语言；命中缓存直接返回，否则调 Azure Translate 并落库。

#### 接口地址

```
POST /v1/sdk/msg/translate
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mid` | string | ✓ | 消息 id |
| `to` | string | ✓ | 目标语言代码（如 `zh`、`en`、`ja`、`vi`、`es`、`pt`） |
| `msg` | string |  | 覆盖消息正文：不传则用 `mid` 对应消息的 `content`（自动解密 encrypt=1） |

#### 请求示例

```json
{ "mid": "s_10000245_10000246:88", "to": "en" }
```

#### 返回参数

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `mid` | string | 透传请求 mid |
| `src_content` | string | 原文（解密后） |
| `desc_content` | string | 译文 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "mid": "s_10000245_10000246:88",
    "src_content": "明天一起吃饭吗",
    "desc_content": "Want to have dinner together tomorrow?"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数异常 | `mid` 为空 |
| 400 | 没有此条消息 | `mid` 在 DB 中不存在 |
| 400 | 此条不是文本消息 | 非 `txt` 类型或正文为空且未传 `msg` |
| 400 | 调用翻译接口失败 | Azure 调用 / 解析失败 |
| 500 | (内部 err) | DB 查询失败 |

#### 备注

- 同一个 (`mid`, `to`) 第二次调用直接读缓存，不会再花翻译额度
- 不支持 `img`/`file`/`voice` 等非文本消息

---

### /v1/sdk/msg/report 消息接收上报

客户端收到推送后，把"已收到的最大游标"回传给服务端，用于服务端推进未读 / 离线状态。

#### 接口地址

```
POST /v1/sdk/msg/report
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `chatCursor` | int64 |  | 聊天消息游标（本端已收到的最大值） |
| `systemCursor` | int64 |  | 系统消息游标；`-1`=不要历史系统消息 |

#### 请求示例

```json
{ "chatCursor": 17170012345, "systemCursor": -1 }
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | (业务文案) | socialevent 处理失败 |
| 500 | (内部 err) | DB / RPC 失败 |

---

### /v1/sdk/msg/read 消息已读上报

把一批消息标记为当前用户已读，触发已读事件给发送者。

#### 接口地址

```
POST /v1/sdk/msg/read
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `msgIds` | []string | ✓ | 已读消息 mid 列表 |

#### 请求示例

```json
{
  "msgIds": [
    "g_10000001_5:1215",
    "g_10000001_5:1216"
  ]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | (业务文案) | socialevent 处理失败（消息不存在、非接收方等） |
| 500 | (内部 err) | DB / RPC 失败 |

#### 备注

- 已读事件会推给原消息发送者，更新 `/v1/sdk/msg/detail` 中的 `readCnt`/`readUids`
- 群里被 @ 的消息还会更新 `atReadCnt`/`atReadUids`

---

## 表情收藏

### /v1/sdk/emoticon/add 添加收藏表情

把一张图片/表情 URL 加入当前用户的收藏。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `url` | string | ✓ | 表情图片 URL（trim 后不能为空） |

#### 请求示例

```json
{
  "url": "https://cdn.example.com/emoticon/cat-roll.gif"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 收藏 id，格式 `<uid>:<md5(url)>`，重复添加同一 URL 返回同一个 id |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": { "id": "10000001:3e9b1c7a8f4d2e1b0a7c4d5f6e8a9b21" }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误，参数不能为空。 | `url` 为空 |
| 500 | (内部 err) | DB 失败 |

#### 备注

- 同 URL 重复添加：服务端走 upsert，flag 重置为 `0`（有效），不会产生重复记录

---

### /v1/sdk/emoticon/delete 删除收藏表情

按 id 批量删除收藏（软删除，flag 置 1）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | []string | ✓ | 收藏 id 列表 |

#### 请求示例

```json
{
  "ids": [
    "10000001:3e9b1c7a8f4d2e1b0a7c4d5f6e8a9b21",
    "10000001:11aa22bb33cc44dd55ee66ff77881122"
  ]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误，参数不能为空。 | `ids` 为空 |

#### 备注

- 单条删除失败只记日志，不会让整批请求失败，最终始终返回 `0`

---

### /v1/sdk/emoticon/list 收藏表情列表

分页拉取当前用户的收藏列表。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skip` | int64 |  | 跳过条数，默认 `0` |
| `limit` | int64 |  | 单页条数，建议 `[100, 1000]` |

#### 请求示例

```json
{
  "skip": 0,
  "limit": 100
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 用户当前有效收藏总数 |
| `emoticons` | []Emoticon | 收藏列表，按 `_id` 升序 |

`Emoticon` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 收藏 id |
| `url` | string | 表情 URL |
| `ctime` | int64 | 收藏时间（毫秒） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "emoticons": [
      {
        "id": "10000001:11aa22bb33cc44dd55ee66ff77881122",
        "url": "https://cdn.example.com/emoticon/dog-wave.gif",
        "ctime": 1716690000000
      },
      {
        "id": "10000001:3e9b1c7a8f4d2e1b0a7c4d5f6e8a9b21",
        "url": "https://cdn.example.com/emoticon/cat-roll.gif",
        "ctime": 1716691200000
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 失败 |

---

## RTC 通话

> 当前 `/v1/sdk/rtc/request`、`/v1/sdk/rtc/answer`、`/v1/sdk/rtc/query` 三个接口实现已被注释，处理函数直接返回 `{"code":0}`，业务能力等待新版 RTC 接入完成后启用。文档仅保留约定的请求/返回结构，方便前端先行接入。

### /v1/sdk/rtc/request 发起通话

发起单聊语音/视频 RTC 邀请。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | string | ✓ | `voice_rtc`=语音、`video_rtc`=视频 |
| `toTopicId` | string | ✓ | 目标会话 id，当前仅支持单聊 `s_<minUid>_<maxUid>` |
| `cid` | string | ✓ | 客户端生成的唯一去重 id |

#### 请求示例

```json
{
  "type": "voice_rtc",
  "toTopicId": "s_10000001_10000002",
  "cid": "cid-rtc-9c4f1a2b3d0e"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `rtcId` | int64 | 本次通话 id |
| `busyUids` | []int64 | 当前在通话中、无法接听的 uid 列表 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "rtcId": 88123001,
    "busyUids": []
  }
}
```

#### 错误码（旧实现，仅供参考）

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type` 不在枚举内 / `toTopicId` 为空 |
| 400 | 已处于通话中 | 当前 uid 已有进行中的 RTC |
| 400 | 功能暂未开放 | 群聊 RTC |
| 400 | 单聊会话Id参数错误 | `toTopicId` 不能解析 |
| 400 | 无权进行该单聊 | 当前 uid 不在该单聊里 |
| 400 | 目标用户不存在 | 对端 uid 查不到 |
| 1010 | 单聊目标用户不属于同一个主体 | 跨 mainAppkey |
| 2010 | 非双边好友关系不能发消息 | 非双边好友 |
| 2011 | 对方在你的黑名单不能发送消息 / 你在对方的黑名单不能发送消息 | 黑名单关系 |
| 400 | 对方忙 | 全部被叫均繁忙 |
| 500 | (内部 err) | DB / RPC 失败 |

#### 备注

- 处理逻辑当前已挂起，仅保留协议形态
- 群聊 RTC 始终被拒，等新版上线后再开放

---

### /v1/sdk/rtc/answer 应答通话

被叫端接受/拒绝，或主叫端取消通话。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `rtcId` | string | ✓ | `/sdk/rtc/request` 返回的 `rtcId`（字符串形式传） |
| `isAccept` | bool | ✓ | `true`=接听 / 继续、`false`=拒绝（被叫）或取消（主叫） |

#### 请求示例

```json
{
  "rtcId": "88123001",
  "isAccept": true
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码（旧实现，仅供参考）

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 目标不存在 | `rtcId` 找不到对应记录或已 done |
| 400 | 参数错误 | uid 不在通话成员里 / 发起人自己又点同意 |
| 500 | (内部 err) | DB / RPC 失败 |

#### 备注

- 处理逻辑当前已挂起
- 旧实现中接听后会下发阿里 RTC SDK Token，新版上线后会换签发逻辑

---

### /v1/sdk/rtc/query 查询通话

查询当前 RTC 通话的完整状态。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `rtcId` | string | ✓ | 通话 id（字符串形式传） |

#### 请求示例

```json
{
  "rtcId": "88123001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `rtc` | RTCDao | 本次通话记录，含状态、成员、各自 SDK Token 等 |

`RTCDao` 主要字段（旧实现，仅供参考）：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | int64 | 通话 id |
| `appkey` | string | 应用 |
| `type` | string | `voice_rtc` / `video_rtc` |
| `tid` | string | 会话 id |
| `sender` | int64 | 发起 uid |
| `status` | int32 | `0`=待应答 `1`=待开始（已凑齐 2 人） `2`=通话中 `3`=已结束 `4`=已取消 |
| `stime` | int64 | 发起时间（毫秒） |
| `members` | []RTCMember | 成员列表 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "rtc": {
      "id": 88123001,
      "appkey": "appkey_xxxx",
      "type": "voice_rtc",
      "tid": "s_10000001_10000002",
      "sender": 10000001,
      "status": 1,
      "stime": 1716694800000,
      "members": [
        { "uid": 10000001, "status": 1, "sdktoken": "rtc-token-xxx" },
        { "uid": 10000002, "status": 1, "sdktoken": "" }
      ]
    }
  }
}
```

#### 错误码（旧实现，仅供参考）

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 目标不存在 | `rtcId` 没有记录 |
| 400 | 参数错误 | uid 不在通话成员里 |
| 500 | (内部 err) | DB 失败 |

#### 备注

- 处理逻辑当前已挂起
- 接口只会回填发请求 uid 自己的 `sdktoken`，其他成员的 token 字段会被清空

---

### /v1/sdk/rtc/callback RTC 厂商回调

阿里 RTC 频道事件回调入口，处理用户入会/离会的统计与状态变更。

#### 接口地址

```
POST /v1/sdk/rtc/callback
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `MsgId` | string |  | 厂商消息 id |
| `MsgTimestamp` | int64 |  | 消息发送时的 Unix 时间戳 |
| `SubscribeId` | string |  | 厂商订阅 id |
| `AppId` | string |  | 产生该事件的 RTC AppId |
| `ChannelId` | string | ✓ | 频道 id（等于 imsvr 这边的 `rtcId`） |
| `Contents` | []Content | ✓ | 事件列表，遍历找 `Event=UserEvent` 处理 |

`Content`：

| 字段 | 类型 | 说明 |
|---|---|---|
| `Event` | string | 事件分类，目前只关心 `UserEvent` |
| `ChannelEvent` | ChannelEvent | 频道级事件，含 `EventTag`：`Open`=会议开始、`Close`=会议结束 |
| `UserEvent` | UserEvent | 用户级事件 |

`UserEvent`：

| 字段 | 类型 | 说明 |
|---|---|---|
| `UserId` | string | 用户 uid（字符串） |
| `EventTag` | string | `Join`=入会、`Leave`=离会、`PublishVideo` / `PublishAudio` / `PublishScreen` / `UnpublishXxx` / `Roleupdate` |
| `SessionId` | string | 该事件对应的 SessionID |
| `Timestamp` | int64 | 事件发生时的 Unix 时间戳 |
| `Reason` | int | 入会/离会原因，`1`=正常 `2`=重连入会 `3`=跨频道转推 `4`=超时离会 `5`=新会话挤下线 `6`=被踢出 `7`=频道解散 |
| `Role` | int | 角色，`1`=主播 `2`=观众 |
| `CurrentMedias` | string | 推流类型，`1`=音频 `2`=视频 `3`=屏幕共享 |

#### 请求示例

```json
{
  "MsgId": "msg-aliyun-001",
  "MsgTimestamp": 1716694800500,
  "SubscribeId": "sub-xxx",
  "AppId": "rtc-app-xxxx",
  "ChannelId": "88123001",
  "Contents": [
    {
      "Event": "UserEvent",
      "UserEvent": {
        "UserId": "10000002",
        "EventTag": "Join",
        "SessionId": "sess-xxx",
        "Timestamp": 1716694801000,
        "Reason": 1,
        "Role": 1
      }
    }
  ]
}
```

#### 返回示例

```json
{ "code": 200, "msg": "" }
```

#### 备注

- 该接口是 RTC 厂商 -> imsvr 的回调，**不带 `X-Token`**；上层版本控制中已经为它放开了 `没有设置系统版本` 的拦截
- 仅处理 `Join` 与 `Leave` 两类事件，其它事件只记日志不落库
- `Leave` 累计到当前频道人数为 0 时，会更新 RTC 状态为 `3`（done），算出整段通话时长并写入消息体

---


## 朋友圈

### /v1/sdk/moments/pub 发布朋友圈

发布一条朋友圈动态，类型支持纯文本、图文、文本+视频。

#### 接口地址

```
POST /v1/sdk/moments/pub
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | int32 | ✓ | 消息类型：`1`=纯文本 `2`=文本+图片 `3`=文本+视频 |
| `content` | string |  | 文本正文，`content` 与 `attachments` 至少一个非空 |
| `extra` | string |  | 拓展内容，客户端自定义 JSON / 字符串 |
| `attachments` | []string |  | 附件 URL 列表（图片或视频地址） |

#### 请求示例

```json
{
  "type": 2,
  "content": "今天团建去了西山公园，风景真不错",
  "extra": "{\"location\":\"西山公园\"}",
  "attachments": [
    "https://fs.example.com/moments/img/2025/05/26/a1.jpg",
    "https://fs.example.com/moments/img/2025/05/26/a2.jpg"
  ]
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `momentsId` | int64 | 朋友圈 id（雪花算法） |
| `appkey` | string | 应用标识 |
| `ctime` | int64 | 发布时间（毫秒） |
| `userId` | int64 | 发布人 uid |
| `type` | int32 | 消息类型，同请求 |
| `content` | string | 文本正文 |
| `extra` | string | 拓展内容 |
| `attachments` | []string | 附件 URL 列表 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "momentsId": 7234518129384112128,
    "appkey": "yino",
    "ctime": 1748246400123,
    "userId": 10086001,
    "type": 2,
    "content": "今天团建去了西山公园，风景真不错",
    "extra": "{\"location\":\"西山公园\"}",
    "attachments": [
      "https://fs.example.com/moments/img/2025/05/26/a1.jpg",
      "https://fs.example.com/moments/img/2025/05/26/a2.jpg"
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type` 不在 `1/2/3` 内；或 `content` 与 `attachments` 同时为空 |
| 500 | (内部 err) | DB 写入失败 |

---

### /v1/sdk/moments/del 删除朋友圈

删除一条自己发布的朋友圈，只能删自己的。

#### 接口地址

```
POST /v1/sdk/moments/del
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 朋友圈 id（`momentsId` 的字符串形式） |

#### 请求示例

```json
{
  "id": "7234518129384112128"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `id` 为空 |
| 500 | (内部 err) | `id` 不是合法整数；DB 删除失败 |

#### 备注

- 删除按 `appkey + userId + id` 匹配，删别人或别 appkey 的朋友圈不会成功（也不报错）

---

### /v1/sdk/moments/list 查询朋友圈列表

按分页+时间戳拉取自己发布的朋友圈。出于隐私限制当前永远只返回当前登录用户自己的内容。

#### 接口地址

```
POST /v1/sdk/moments/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageSize` | int64 | ✓ | 每页条数 |
| `pageIndex` | int64 | ✓ | 当前页码，从 1 开始 |
| `timeStamp` | int64 |  | 时间戳锚点（毫秒），通常传 0 表示从最新开始 |
| `target` | int64 |  | 目标 uid。服务端硬性覆盖为当前登录 uid，传任何值都只返回自己的 |

#### 请求示例

```json
{
  "pageSize": 20,
  "pageIndex": 1,
  "timeStamp": 0,
  "target": -1
}
```

#### 返回参数

返回 `data` 为朋友圈数组，元素字段同 `/v1/sdk/moments/pub` 的返回。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "momentsId": 7234518129384112128,
      "appkey": "yino",
      "ctime": 1748246400123,
      "userId": 10086001,
      "type": 2,
      "content": "今天团建去了西山公园，风景真不错",
      "extra": "{\"location\":\"西山公园\"}",
      "attachments": [
        "https://fs.example.com/moments/img/2025/05/26/a1.jpg"
      ]
    }
  ]
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | DB 查询失败 |

---

## 办公 OA

> OA 业务统一的主类型 `type_main` 枚举：
>
> | 值 | 含义 |
> |---|---|
> | `1` | 待办事项（ToDo） |
> | `2` | 日程安排（Schedule） |
> | `3` | 打卡（TimeIn） |
> | `4` | 日报/周报（Daily），`type_sub` `1`=计划 `2`=结算 |
> | `5` | 备忘录（Memo） |
>
> 状态 `status` 枚举：`0`=禁用 `1`=正常/待处理 `2`=已完成。

### /v1/sdk/oa/add 新增 OA 项

创建一条 OA 记录。不同 `type_main` 必填字段不同：
- `1` 待办：`title` `content` `stime` `etime` 必填
- `2` 日程：`title` `stime` `etime` 必填
- `3` 打卡：`type_sub` 必须为 `1`(签到)/`2`(签退)，`tag` 由服务端覆盖为当天日期 `YYYY-MM-DD`
- `4` 日报：`type_sub` 必须为 `1`(计划)/`2`(结算)，`title` `content` 必填
- `5` 备忘：`title` `content` `stime` `etime` 必填

#### 接口地址

```
POST /v1/sdk/oa/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type_main` | int32 | ✓ | 主类型，见上表 |
| `type_sub` | int32 |  | 子类型，打卡/日报必填 |
| `tag` | string |  | 业务标识；打卡类型由服务端覆盖 |
| `title` | string |  | 标题 |
| `content` | string |  | 内容 |
| `remark` | string |  | 备注 |
| `stime` | int64 |  | 开始时间（毫秒） |
| `etime` | int64 |  | 结束时间（毫秒） |

#### 请求示例

```json
{
  "type_main": 1,
  "title": "整理 Q2 工作复盘",
  "content": "梳理 KPI 完成情况、未达成原因、Q3 改进点",
  "remark": "PPT 模板用统一版",
  "stime": 1748246400000,
  "etime": 1748332800000
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | OA 记录 id（雪花算法，字符串） |
| `appkey` | string | 应用标识 |
| `uid` | string | 归属用户 uid |
| `type_main` | int32 | 主类型 |
| `type_sub` | int32 | 子类型 |
| `tag` | string | 标识 |
| `title` | string | 标题 |
| `content` | string | 内容 |
| `ctime` | int64 | 创建时间（毫秒） |
| `stime` | int64 | 开始时间（毫秒） |
| `etime` | int64 | 结束时间（毫秒） |
| `remark` | string | 备注 |
| `status` | int32 | 状态，新建时为 `1` |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "7234518129384112256",
    "appkey": "yino",
    "uid": "10086001",
    "type_main": 1,
    "type_sub": 0,
    "tag": "",
    "title": "整理 Q2 工作复盘",
    "content": "梳理 KPI 完成情况、未达成原因、Q3 改进点",
    "ctime": 1748246400123,
    "stime": 1748246400000,
    "etime": 1748332800000,
    "remark": "PPT 模板用统一版",
    "status": 1
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type_main` 不在 `1..5`；或对应类型的必填字段缺失；或打卡/日报 `type_sub` 非法 |
| 500 | (内部 err) | DB 写入失败 |

---

### /v1/sdk/oa/del 删除 OA 项

删除一条自己创建的 OA 记录。

#### 接口地址

```
POST /v1/sdk/oa/del
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | OA 记录 id |

#### 请求示例

```json
{
  "id": "7234518129384112256"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `id` 不是合法整数；或记录不属于当前用户 |
| 500 | (内部 err) | DB 查询/删除失败 |

---

### /v1/sdk/oa/edit 修改 OA 项

更新一条已存在的 OA 记录。只能改自己的，且 `type_main` 不能是打卡（`3`）。

#### 接口地址

```
POST /v1/sdk/oa/edit
Content-Type: application/json
```

#### 请求参数

所有可选字段都使用指针语义，缺省即未传；但当前实现会将所有字段直接落库，建议客户端把当前完整值都传过来再修改需要变更的字段。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | OA 记录 id |
| `type_sub` | int32 |  | 子类型 |
| `tag` | string |  | 标识 |
| `title` | string |  | 标题 |
| `content` | string |  | 内容 |
| `stime` | int64 |  | 开始时间（毫秒） |
| `etime` | int64 |  | 结束时间（毫秒） |
| `remark` | string |  | 备注 |
| `status` | int32 |  | 状态：`0`=禁用 `1`=正常/待处理 `2`=已完成 |

#### 请求示例

```json
{
  "id": "7234518129384112256",
  "title": "整理 Q2 工作复盘（已完成）",
  "content": "已上传到共享盘 /docs/2025Q2/",
  "stime": 1748246400000,
  "etime": 1748332800000,
  "remark": "PPT 模板用统一版",
  "status": 2
}
```

#### 返回参数

更新成功后返回更新后的整条 OA 记录，字段同 `/v1/sdk/oa/add` 的返回。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "7234518129384112256",
    "appkey": "yino",
    "uid": "10086001",
    "type_main": 1,
    "type_sub": 0,
    "tag": "",
    "title": "整理 Q2 工作复盘（已完成）",
    "content": "已上传到共享盘 /docs/2025Q2/",
    "ctime": 1748246400123,
    "stime": 1748246400000,
    "etime": 1748332800000,
    "remark": "PPT 模板用统一版",
    "status": 2
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `id` 非整数；记录不属于当前用户；记录 `type_main` 是打卡（`3`）；DB 报受影响行数为 0 时返回 `操作失败` |
| 500 | (内部 err) | DB 查询/更新失败 |

---

### /v1/sdk/oa/detail 查询单条 OA

按 id 查询一条自己创建的 OA 记录。

#### 接口地址

```
POST /v1/sdk/oa/detail
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | OA 记录 id |

#### 请求示例

```json
{
  "id": "7234518129384112256"
}
```

#### 返回参数

字段同 `/v1/sdk/oa/add` 的返回。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "7234518129384112256",
    "appkey": "yino",
    "uid": "10086001",
    "type_main": 1,
    "type_sub": 0,
    "tag": "",
    "title": "整理 Q2 工作复盘",
    "content": "梳理 KPI 完成情况、未达成原因、Q3 改进点",
    "ctime": 1748246400123,
    "stime": 1748246400000,
    "etime": 1748332800000,
    "remark": "PPT 模板用统一版",
    "status": 1
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `id` 非整数；或记录不属于当前用户 |
| 500 | (内部 err) | DB 查询失败 |

---

### /v1/sdk/oa/list 查询 OA 列表（不分页）

按主类型 + 多种可选条件查询当前用户的 OA 记录，不分页，直接返回数组。

#### 接口地址

```
POST /v1/sdk/oa/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type_main` | int32 | ✓ | 主类型，必须为 `1..5` |
| `type_sub` | int32 |  | 子类型过滤 |
| `tag` | string |  | 标识过滤 |
| `status` | int32 |  | 状态过滤 |
| `ctime_from` | int64 |  | 创建时间下限（毫秒） |
| `ctime_until` | int64 |  | 创建时间上限（毫秒） |
| `stime_from` | int64 |  | 开始时间下限（毫秒） |
| `stime_until` | int64 |  | 开始时间上限（毫秒） |
| `etime_from` | int64 |  | 结束时间下限（毫秒） |
| `etime_until` | int64 |  | 结束时间上限（毫秒） |

#### 请求示例

```json
{
  "type_main": 3,
  "tag": "2025-05-26",
  "ctime_from": 1748188800000,
  "ctime_until": 1748275199999
}
```

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "id": "7234518129384112300",
      "appkey": "yino",
      "uid": "10086001",
      "type_main": 3,
      "type_sub": 1,
      "tag": "2025-05-26",
      "title": "",
      "content": "",
      "ctime": 1748246400123,
      "stime": 0,
      "etime": 0,
      "remark": "",
      "status": 1
    }
  ]
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type_main` 不在 `1..5` |
| 500 | (内部 err) | DB 查询失败 |

---

### /v1/sdk/oa/page 分页查询 OA 列表

参数与 `/v1/sdk/oa/list` 相同，多了分页字段；目前 `data` 仍只返回数组，不返回 total。

#### 接口地址

```
POST /v1/sdk/oa/page
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageSize` | int64 | ✓ | 每页条数 |
| `pageIndex` | int64 | ✓ | 页码，从 0 开始 |
| `type_main` | int32 | ✓ | 主类型，必须为 `1..5` |
| `type_sub` | int32 |  | 子类型过滤 |
| `tag` | string |  | 标识过滤 |
| `status` | int32 |  | 状态过滤 |
| `ctime_from` | int64 |  | 创建时间下限（毫秒） |
| `ctime_until` | int64 |  | 创建时间上限（毫秒） |
| `stime_from` | int64 |  | 开始时间下限（毫秒） |
| `stime_until` | int64 |  | 开始时间上限（毫秒） |
| `etime_from` | int64 |  | 结束时间下限（毫秒） |
| `etime_until` | int64 |  | 结束时间上限（毫秒） |

#### 请求示例

```json
{
  "pageSize": 20,
  "pageIndex": 0,
  "type_main": 1,
  "status": 1
}
```

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "id": "7234518129384112256",
      "appkey": "yino",
      "uid": "10086001",
      "type_main": 1,
      "type_sub": 0,
      "tag": "",
      "title": "整理 Q2 工作复盘",
      "content": "梳理 KPI 完成情况、未达成原因、Q3 改进点",
      "ctime": 1748246400123,
      "stime": 1748246400000,
      "etime": 1748332800000,
      "remark": "PPT 模板用统一版",
      "status": 1
    }
  ]
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type_main` 不在 `1..5` |
| 500 | (内部 err) | DB 查询失败 |

---

## 系统

### /v1/sdk/system/navigation/bar 导航栏服务 Tab 信息

拉取首页底部"服务"Tab 的标题、跳转 URL 和图标资源。后台未配置时返回内置默认值。

#### 接口地址

```
POST /v1/sdk/system/navigation/bar
Content-Type: application/json
```

#### 请求参数

无业务字段，仅需 `X-Token`。

#### 请求示例

```json
{}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `title` | string | Tab 主标题 |
| `url` | string | 点击 Tab 跳转的 URL |
| `icon_select` | string | 选中态图标（兼容旧字段） |
| `icon` | string | 默认图标（兼容旧字段） |
| `icon_on` | string | 聚焦态图标 |
| `icon_off` | string | 失焦态图标 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "title": "服务",
    "url": "https://service.example.com/h5/index",
    "icon_select": "https://fs.example.com/service/service3.png",
    "icon": "https://fs.example.com/service/icon_tab_serve_normal@3x.png",
    "icon_on": "https://fs.example.com/service/service3.png",
    "icon_off": "https://fs.example.com/service/icon_tab_serve_normal@3x.png"
  }
}
```

#### 错误码

仅鉴权错误，无业务错误。

#### 备注

- 后台未配置 `title`/`content` 时返回空 `data`，客户端按内置默认占位渲染

---

### /v1/sdk/system/other/text 其他文本协议拉取

拉取"帮助"或"关于软件"文案 + 对应 H5 跳转地址。

#### 接口地址

```
POST /v1/sdk/system/other/text
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | int64 | ✓ | `1`=帮助 `2`=关于软件 |

#### 请求示例

```json
{
  "type": 1
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `text` | string | HTML 富文本片段 |
| `url` | string | 详情 H5 URL |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "text": "<h2>如果遇到异常请联系客服电话：<font color=#FF0000>400123123</font></br>希望软件能给你带来方便</h2>",
    "url": "https://help.example.com/h5/zh/faq"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `type` 不在 `1/2` |

#### 备注

- `text` 是已经组装好的 HTML 片段，客户端直接渲染即可
- 语言为空时回退为 `zh`

---

### /v1/sdk/system/tipoff/list 举报理由列表

拉取举报弹层的多选项文本，固定 9 个分类，按当前用户语言返回。

#### 接口地址

```
POST /v1/sdk/system/tipoff/list
Content-Type: application/json
```

#### 请求参数

无业务字段。

#### 请求示例

```json
{}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `contents` | []string | 举报理由文本，按用户语言本地化 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "contents": [
      "色情低俗",
      "违法犯罪",
      "诈骗行为",
      "骚扰攻击",
      "广告内容",
      "青少年不宜",
      "时政不实",
      "涉嫌网络暴力",
      "其他违规"
    ]
  }
}
```

#### 错误码

仅鉴权错误，无业务错误。

---

### /v1/sdk/system/tipoff/submit 举报提交

用户对某个对象（用户/群/消息等）发起举报。

#### 接口地址

```
POST /v1/sdk/system/tipoff/submit
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `target` | string |  | 举报对象，业务自定（uid、群 id、消息 mid 等字符串） |
| `contents` | []string | ✓ | 举报理由文本列表，元素来自 `/v1/sdk/system/tipoff/list` |

#### 请求示例

```json
{
  "target": "10086002",
  "contents": [
    "骚扰攻击",
    "广告内容"
  ]
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `contents` 为空 |
| 500 | (内部 err) | DB 写入失败 |

---

### /v1/sdk/system/problem/feedback 问题反馈

用户向后台提交一条问题反馈（含截图、描述、联系方式）。

#### 接口地址

```
POST /v1/sdk/system/problem/feedback
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `images` | []string |  | 截图 URL 列表 |
| `desc` | string | ✓ | 问题描述 |
| `contract` | string | ✓ | 联系方式（手机/邮箱/微信等） |
| `appKey` | string |  | 应用标识，缺省即用 token 中的 appkey |

#### 请求示例

```json
{
  "images": [
    "https://fs.example.com/feedback/2025/05/26/s1.png",
    "https://fs.example.com/feedback/2025/05/26/s2.png"
  ],
  "desc": "登录后点底部聊天 Tab 偶现白屏，重启 App 后恢复",
  "contract": "13800138000",
  "appKey": "yino"
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `desc` 或 `contract` 去空白后为空 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 整个请求体会被 JSON 序列化后落到 `content` 字段，方便审核台看原始上下文

---

## 日志

### /v1/sdk/log/report APP 端日志上报

客户端把链路日志/事件/异常以结构化方式上报到服务端 MongoDB，供运维排查。

#### 接口地址

```
POST /v1/sdk/log/report
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `traceId` | string |  | 链路追踪 id，前端生成，串联同一次请求/操作的多条日志；最长 128 |
| `type` | string | ✓ | 日志类型，前端自定义字符串；最长 64 |
| `status` | int32 |  | 状态码，前端自定义（如 `0`=正常 `1`=警告 `2`=错误） |
| `content` | string |  | 日志正文，前端任意字符串；最长 64KB |
| `eventTime` | int64 |  | 事件触发时间（毫秒）；`<=0` 时服务端用当前时间填充 |

#### 请求示例

```json
{
  "traceId": "f3a91b2c-0001-4e2c-9a01-2b9c8d4e1234",
  "type": "ui.login.click",
  "status": 0,
  "content": "{\"page\":\"login\",\"channel\":\"telno\",\"latencyMs\":182}",
  "eventTime": 1748246400123
}
```

#### 返回示例

```json
{ "code": 0, "msg": "" }
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 日志类型不能为空 | `type` 去空白后为空 |
| 400 | 日志类型过长 | `type` 超过 64 字节 |
| 400 | traceId过长 | `traceId` 超过 128 字节 |
| 400 | 日志内容过长 | `content` 超过 64KB |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 服务端会自动补 `appkey`（来自 token）和 `reportTime`（落库时的当前毫秒时间戳）
- 不要把整段大对象往 `content` 里塞，超过 64KB 直接 400；建议 JSON 字符串化后再发

---

## 附录

### Group 群结构

`/v1/sdk/group/detail`、`/v1/sdk/group/search` 等返回的 group 详情统一结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 群 id |
| `uid` | int64 | 群主 uid |
| `name` | string | 群名称 |
| `namePY` | string | 群名拼音（驼峰），例如 `ChanPinTaoLunQun` |
| `namePYS` | string | 群名拼音首字母，例如 `CPTLQ` |
| `adminUids` | []int64 | 管理员 uid 列表（包含群主） |
| `cid` | string | 客户端创建时填的 cid（去重用） |
| `avatar` | string | 群头像 url |
| `notice` | string | 群公告 |
| `tag` | string | 群标签文案（按调用方 appkey 修正） |
| `tagid` | int32 | 标签 id：`1`=企业群 `2`=供应商 `3`=服务商 `4`=供应链 |
| `ctime` | int64 | 群创建时间（毫秒） |
| `scene` | int32 | 创建场景：`0`=正常 `1`=客服自动 `2`=企业自建 `3`=产品咨询 `4`=订单咨询 |
| `sceneid` | string | 场景关联值（企业群=关联企业 appkey） |
| `flag` | int32 | `0`=正常 `2`=已解散 |
| `qrcode` | string | 群二维码 url（仅 detail 返回） |
| `qrcode_desc` | string | 群二维码描述文案（本地化） |
| `group_check_switch` | string | 入群审核：`1`=需要审核 |
| `group_allow_member_list` | string | 普通成员是否能看成员列表：`1`=允许 |
| `group_allow_member_visit` | string | 普通成员是否能访问个人页：`1`=允许 |
| `group_friend_invite_enter` | string | 是否允许普通成员邀请好友入群：`1`=允许 |
| `group_ban_chat` | string | 全体禁言：`1`=禁言 |
| `group_chat_interval_time` | string | 普通成员发言间隔（秒），`0`=不限 |
| `group_member_alias` | string | 允许成员设置群昵称：`1`=允许 |
| `group_member_image` | string | 允许发图片：`1`=允许 |
| `group_member_video` | string | 允许发视频：`1`=允许 |
| `group_member_voice` | string | 允许发语音：`1`=允许 |
| `group_member_doc` | string | 允许发文件：`1`=允许 |
| `group_member_pkg` | string | 允许发红包：`1`=允许 |
| `group_member_quit_msg` | string | 是否显示退群消息：`1`=显示 |
| `group_admin_special` | string | 管理员/群主发言字体设置（JSON 字符串） |
| `join_notice` | int64 | 入群是否发系统消息：`1`=发送 `0`=不发送 |
| `max_cnt` | int64 | 群成员上限，`0`=不限 |
| `group_sn` | string | 群号（搜索/展示用） |
| `intro` | string | 群简介 |
| `remark` | string | 当前用户给该群设置的备注（仅 detail / list 返回） |
| `memberCnt` | int64 | 群成员数（仅 search 返回） |
| `im_in` | bool | 当前用户是否已在该群（仅 search 返回） |


### MsgDao 消息结构

`/v1/sdk/msg/latest`、`/v1/sdk/msg/history` 等接口返回的消息正文统一结构：

| 字段 | 类型 | 说明 |
|---|---|---|
| `mid` | string | 消息 id（`<tid>:<tmid>`） |
| `tid` | string | 会话 id |
| `cid` | string | 客户端生成的去重 id |
| `sender` | int64 | 发送方 uid |
| `stime` | int64 | 发送时间（毫秒） |
| `type` | string | 消息类型，枚举见 `/v1/sdk/msg/send` |
| `content` | string | 正文（撤回后为空） |
| `meta` | map[string]string | 客户端自定义 KV |
| `etime` | int64 | 失效时间（毫秒）；批量拉取时已过期消息会被跳过，单拉仍可见 |
| `targetCnt` | int32 | 目标人数 |
| `readCnt` | int32 | 已读人数 |
| `atCnt` | int32 | 被 @ 人数 |
| `atReadCnt` | int32 | 被 @ 已读人数 |
| `mask` | int32 | `0`=正常 `1`=删除 `2`=撤回 |
| `visible` | int32 | 可见性，同 `/v1/sdk/msg/detail` |
| `auditWord` | string | 命中的敏感词（命中时返回） |
| `auditRst` | string | 审核结果：`pass` / `refused` / 空 |
