# imsvr 后台接口文档

> 面向后台管理界面（admin web UI）对接。所有接口均为 `POST + JSON body`，路由前缀 `/admin/...`。当文档与代码冲突，以代码为准。

---

## 通用约定

### 请求头

| Header | 必填 | 说明 |
|---|---|---|
| `X-Token` | ✓ | `/admin/login` 拿到的 token；登录、OTP、健康检查、联系我们等公开接口不需要 |
| `X-Lang` |  | `zh` / `en` / `ja` / `vi` / `es` / `pt`，默认 `zh` |
| `X-Origin-IP` |  | 内部服务转发请求时填真实客户端 IP，用于穿透 IP 白名单/控制 |

### 响应统一结构

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

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

### 通用错误码

| code | 含义 |
|---|---|
| 0 | 成功 |
| 400 | 参数错误 |
| 401 / 403 | 鉴权失败 / 无权限（含 IP 白名单未命中） |
| 500 | 内部错误 |
| 1000 | 资源不存在（用户/邀请码/记录） |
| 1001 | 业务规则不满足（账户状态、密码错误等） |
| 1011 | 短信通道发送失败 |
| 1012 | 短信发送超频或超限 |
| 1013 | 校验失败次数超限 |
| 1014 | 验证码已过期或不存在 |
| 1015 | 验证码不匹配 |

### IP 白名单

> manage 接口默认对 `127.0.0.1` / `::1` 放行；其它来源 IP 在拿到 token 之前会按 IP 白名单 (`/admin/ipctrl/*` 维护) 校验。内部服务转发的请求会带 `X-Origin-IP` header 表示真实客户端 IP，校验以该 header 为准。

### 平台账户与商户账户

后台账户分两类：

- **平台账户**：`appkey == "admin"`，`type=1`，可跨商户操作；其中账号 `admin` 为最高级超管 (`level=0`)
- **商户账户**：`appkey` 为商户 appkey，`type=0`，操作范围限于本商户；商户账户的"创建者"为 `level=0`，其余为 `level=1`

涉及商户范围的接口通常带 `business_id` / `business_name` / `appkey` 三个字段；平台账户传 `business_id` 时由 manage 节点转发到对应商户节点处理，不传或传 `admin` 则在平台节点直接处理。

---

## 系统

### /admin/test 健康检查

返回 `<环境> - <服务名>` 字符串，用于探活。不需要鉴权。

#### 接口地址

```
POST /admin/test
```

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": "prod - imsvr"
}
```

---

### /admin/fix/sessionclean 清空所有用户会话缓存

遍历所有用户，清除 `pushid:<uid>` 和 `{uid:<uid>}:tokens` 两个 Redis 键。**重操作，谨慎调用**——会导致全员被强制下线、推送通道失效。

#### 接口地址

```
POST /admin/fix/sessionclean
```

#### 请求参数

无。

#### 返回示例

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

#### 错误码

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

#### 备注

- 没有鉴权门槛，依赖外层 IP 白名单保护
- 清理 Redis 失败不会反馈，单个用户失败被静默跳过

---

### /admin/contactus/apply 联系我们提交

收到表单立即返回成功，**不做任何处理**。该路由用于占位/前端透传，实际表单内容若需归档由前端转发到独立工单系统。

#### 接口地址

```
POST /admin/contactus/apply
```

#### 返回示例

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

---

### /admin/fs/uploadaws 文件上传到对象存储

走 `multipart/form-data`，把文件上传到配置的 S3 / AWS 兼容存储，返回访问 URL。上传规则（大小限制、允许后缀）来自 `/admin/attachcfg/load` 读到的附件配置。

#### 接口地址

```
POST /admin/fs/uploadaws
Content-Type: multipart/form-data
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `Key` | string | ✓ | 业务类型（附件配置里的 key，如 `avatar` / `chat` / `attach`） |
| `Ext` | string | ✓ | 文件后缀，**不带点**，如 `jpg` / `pdf` |
| `Files` | file[] | ✓ | 文件字段（form file），支持多文件 |

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `urls` | []string | 上传成功的文件 URL 列表，下标与 `Files` 对应 |
| `errs` | []string | 错误信息列表，下标与 `Files` 对应；成功项为空字符串 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "urls": [
      "https://cdn.example.com/uploads/avatar/2026/05/26/a1b2c3.jpg"
    ],
    "errs": [""]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | `Key` / `Ext` / `Files` 任一为空 |
| 400 | 参数错误 | 附件配置查不到对应 handler（key + ext 组合非法） |
| 400 | 文件大小不符 | 单文件超过附件配置的大小上限 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | 写存储/读配置失败 |

---

## OTP

### /admin/otp/send 发送手机验证码

支持两种入口：

- 直接传 `phone`（注册前/找回密码）
- 只传 `account`（系统按账号反查绑定手机号）

频控：单手机号 24 小时内总次数 ≤ `MAX_DAILY_OTP_SEND_TIMES`；同一手机号 + admin 类型 1 分钟内只能发 1 条。

#### 接口地址

```
POST /admin/otp/send
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `phone` | string | 二选一 | 手机号 |
| `account` | string | 二选一 | 后台账号；优先 `phone`，仅 `account` 时反查 |
| `identifyCode` | string |  | 通道类型（保留字段，目前服务端不使用） |

#### 请求示例

```json
{
  "account": "merchant_admin_01"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数非法 | `phone` 和 `account` 都为空 |
| 1000 | 用户不存在 | 仅传 `account` 但反查不到商户用户 |
| 1000 | 账号未绑定手机号码 | 账号存在但 `tel` 为空 |
| 1012 | 您今日短信使用次数已经用完 | 当日累计发送次数超 `MAX_DAILY_OTP_SEND_TIMES` |
| 1012 | 您的验证码服务超过频率限制，请稍后 1 分钟重试 | 1 分钟内重复发送 |
| 1012 | 服务异常稍后重试 | 短信通道返回非 0 |
| 500 | (内部 err) | DB / Redis 错误 |

#### 备注

- `AppOTPSwitch=false` 调试态下，验证码固定为 `123456`，不会真发短信
- 验证码 TTL 与每日验证计数都通过 Redis 控制；本接口发码时会清零失败计数

---

### /admin/otp/verify 校验手机验证码

校验通过后下发 `sms_token`（MD5 串），可用于密码找回等敏感操作的二次身份证明。

#### 接口地址

```
POST /admin/otp/verify
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `phone` | string | 二选一 | 手机号 |
| `account` | string | 二选一 | 后台账号；优先 `phone`，仅 `account` 时反查 |
| `identifyCode` | string | ✓ | 用户输入的 6 位验证码 |

#### 请求示例

```json
{
  "phone": "13800138000",
  "identifyCode": "123456"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `phone` | string | 实际校验的手机号 |
| `sms_token` | string | OTP token，业务接口透传 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "phone": "13800138000",
    "sms_token": "f3c1e5a9d7b2..."
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数非法 | `phone` 和 `account` 都为空 |
| 400 | 验证码为空 | `identifyCode` 为空 |
| 1000 | 用户不存在 / 账号未绑定手机号码 | 仅传 `account` 时反查失败 |
| 1013 | 验证超过次数限制了，请重新获取验证码 | 当日校验失败次数超 `MAX_DAILY_OTP_VERIFY_COUNT` |
| 1014 | 验证不成功，验证码已过期或不存在 | Redis 中无对应 vcode |
| 1015 | 验证不成功，验证码不匹配 | vcode 不一致 |
| 500 | (内部 err) | Redis 错误 |

---

## 登录态

### /admin/login 后台登录

平台账户和商户账户共用同一入口，区分方式：

- 传 `vcode`（邀请码）时按商户账户处理，按邀请码反查 appkey 再校验账号
- 不传 `vcode` 时按平台账户处理（`appkey=admin`）

首次登录（用户尚未绑定 MFA）会在校验密码前先下发 `mfaQRCode`，前端引导扫码绑定后重新登录。已绑定的账户必须传 6 位 MFA 验证码。

#### 接口地址

```
POST /admin/login
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `account` | string | ✓ | 账号 |
| `password` | string | ✓ | 密码，MD5-32 小写 |
| `mfaCode` | string | ✓ | 6 位 MFA 验证码；debug 模式可选 |
| `vcode` | string |  | 邀请码；传则按商户账户路径 |
| `deviceId` | string |  | 设备号，用于多端互踢 |
| `lang` | string |  | 文案语言 |

#### 请求示例

```json
{
  "account": "merchant_admin_01",
  "password": "e10adc3949ba59abbe56e057f20f883e",
  "mfaCode": "318472",
  "vcode": "ABC123",
  "deviceId": "web_chrome_a1b2c3"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `token` | string | 登录 token，后续接口的 `X-Token` |
| `uid` | int64 | 后台账号 uid |
| `appkey` | string | 所属 appkey；平台账户为 `admin` |
| `type` | int64 | `0`=普通管理员 `1`=超级管理员（即平台账户） |
| `level` | int | `0`=对应范围的超管（admin 账号或商户创建者）`1`=普通管理员 |
| `permission` | string | 权限位串 |
| `name` | string | 昵称 |
| `avatar` | string | 头像 url |
| `telno` | string | 手机号 |
| `email` | string | 邮箱 |
| `mfaQRCode` | string | MFA 绑定二维码（base64）；仅首次未绑定时下发，此时其它字段为空 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "token": "tk_1717000000_admin_xyz",
    "uid": 1023,
    "appkey": "kk_merchant_demo",
    "type": 0,
    "level": 0,
    "permission": "user.manage,group.manage,msg.audit",
    "name": "运营管理员",
    "avatar": "https://cdn.example.com/avatar/1023.png",
    "telno": "13800138000",
    "email": "ops@example.com",
    "mfaQRCode": ""
  }
}
```

首次绑定 MFA 时：

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "mfaQRCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | account 不能为空 | `account` 缺失 |
| 400 | 密码格式不对 | `password` 不是 32 位 |
| 400 | 参数错误 | `mfaCode` 不是 6 位（非 debug） |
| 400 | 邀请码不存在 | `vcode` 在 invite_map 查不到 |
| 400 | MFA 更新失败 | 写入 MFA secret 失败 |
| 403 | IP not allowed | 命中 IP 控制黑名单 |
| 1000 | 用户不存在 | 账号未注册 |
| 1001 | 密码错误 | 密码不匹配 |
| 1001 | 账户不存在 / 用户已关闭 / 用户状态异常 | 用户状态为已删除/关闭/其它 |
| 1001 | MFA 校验失败 | MFA 验证码错误 |
| 1013 | 连续密码错误超过 5 次,请找回密码 | 当日密码/MFA 错误累计 > `MAX_DAILY_PWD_VERIFY_COUNT` |
| 500 | (内部 err) | DB / Redis / MFA 库错误 |

#### 备注

- 登录成功会在 Redis 写入 `manage_login_data` hash，记录最后一次登录时间
- 不需要 `X-Token`，由本接口下发新 token

---

### /admin/logout 退出登录

清除当前 token 在 Redis 中的会话记录与全局状态。

#### 接口地址

```
POST /admin/logout
```

#### 请求参数

无（凭 `X-Token` 找当前会话）。

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 401 | (token 失效) | 未登录或 token 已过期 |

---

### /admin/password 修改密码

旧密码校验失败计入密码错误计数（与登录共用 `MAX_DAILY_PWD_VERIFY_COUNT` 限制）；修改成功后清零该账户的失败计数。

#### 接口地址

```
POST /admin/password
```

#### 请求参数

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

#### 请求示例

```json
{
  "oldPassword": "e10adc3949ba59abbe56e057f20f883e",
  "newPassword": "25d55ad283aa400af464c76d713c07ad"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 旧密码格式不对 / 新密码格式不对 | 不是 32 位 |
| 400 | 账号不存在 | token 对应账户不存在 |
| 400 | 旧密码不正确 | 旧密码不匹配 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 写入失败 |

---

### /admin/update 修改自己的基础信息

修改当前登录账户的昵称、头像、电话、邮箱。`name` 必填；其它字段空字符串视为不修改。

#### 接口地址

```
POST /admin/update
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | ✓ | 昵称 |
| `avatar` | string |  | 头像 url（先经 `/admin/fs/uploadaws` 上传） |
| `telno` | string |  | 手机号 |
| `Email` | string |  | 邮箱（注意首字母大写） |

#### 请求示例

```json
{
  "name": "运营管理员",
  "avatar": "https://cdn.example.com/avatar/1023.png",
  "telno": "13800138000",
  "Email": "ops@example.com"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 昵称不能为空 | `name` 缺失 |
| 400 | 账号不存在 | token 对应账户不存在 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 写入失败 |

---

### /admin/getCurrentUserInfo 获取当前登录用户信息

#### 接口地址

```
POST /admin/getCurrentUserInfo
```

#### 请求参数

无（凭 `X-Token` 找当前账户）。

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `userid` | string | uid 字符串形式 |
| `type` | int64 | `0`=普通管理员 `1`=超级管理员（平台账户） |
| `account` | string | 登录账号 |
| `name` | string | 昵称 |
| `avatar` | string | 头像 url |
| `phone` | string | 手机号 |
| `email` | string | 邮箱 |
| `permission` | string | 权限位串 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "userid": "1023",
    "type": 0,
    "account": "merchant_admin_01",
    "name": "运营管理员",
    "avatar": "https://cdn.example.com/avatar/1023.png",
    "phone": "13800138000",
    "email": "ops@example.com",
    "permission": "user.manage,group.manage,msg.audit"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 账号不存在 | token 对应账户不存在 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 读失败 |

---

## 总览数据

### /admin/data/screen 数据总览

按选定维度（注册、活跃、消息、禁用、新增商户）返回时间序列。平台账户可不传 `business_id` 看全平台；传 `business_id` 由 manage 节点转发到对应商户节点。

#### 接口地址

```
POST /admin/data/screen
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `showType` | int64 | ✓ | 时间粒度：`1`=小时（区间默认近 1 天）`2`=天（区间默认近 7 天） |
| `types` | []int64 | ✓ | 维度组合：`1`=注册用户 `2`=活跃用户 `3`=消息 `4`=禁用用户 `5`=新增商户；非法值会被忽略 |
| `startTime` | string |  | 起始时间 `YYYY-MM-DD HH:mm:ss`；空则按 showType 推算 |
| `endTime` | string |  | 结束时间 `YYYY-MM-DD HH:mm:ss`；空则取当前 |
| `business_id` | string |  | 商户 id；平台账户传值时转发到商户节点 |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "showType": 2,
  "types": [1, 2, 3],
  "startTime": "2026-05-19 00:00:00",
  "endTime": "2026-05-26 00:00:00",
  "business_id": "biz_2024_001"
}
```

#### 返回参数

返回是以 `types` 为 key 的 map：

| 字段 | 类型 | 说明 |
|---|---|---|
| `<type>.total` | int64 | 全周期累计 |
| `<type>.filterTotal` | int64 | 当前过滤区间累计 |
| `<type>.list` | []object | 时间序列 |
| `<type>.list[].time` | string | 时间标签：`HH:00` 或 `YYYY-MM-DD` |
| `<type>.list[].value` | int64 | 对应区间值 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "1": {
      "total": 12300,
      "filterTotal": 320,
      "list": [
        { "time": "2026-05-20", "value": 35 },
        { "time": "2026-05-21", "value": 48 },
        { "time": "2026-05-22", "value": 51 }
      ]
    },
    "3": {
      "total": 8920031,
      "filterTotal": 42105,
      "list": [
        { "time": "2026-05-20", "value": 5230 },
        { "time": "2026-05-21", "value": 6112 }
      ]
    }
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | showType 非法 | 既不是 1 也不是 2 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 查询失败 |

#### 备注

- `total` 为对应维度的历史累计；`list` 内 `value` 之和 = `filterTotal`
- 商户节点处理时只看到自己的数据；维度 `5`（新增商户）仅在平台节点有意义

---

### /admin/scatterplot/regist/daily 每日注册账号设备分布

返回时间区间内（默认近 7 天）按平台聚合的注册账号数。结果是 `platform => count` 的 map。

#### 接口地址

```
POST /admin/scatterplot/regist/daily
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `startTime` | string |  | 起始时间 `YYYY-MM-DD HH:mm:ss`；空则取近 7 天 |
| `endTime` | string |  | 结束时间；空则取当前 |
| `business_id` | string |  | 商户 id；平台账户传值时转发到商户节点 |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "startTime": "2026-05-19 00:00:00",
  "endTime": "2026-05-26 00:00:00"
}
```

#### 返回参数

返回为 `platform => count` 的 map：

| 字段 | 类型 | 说明 |
|---|---|---|
| `<platform>` | int64 | 该平台累计注册数；platform 枚举如 `ios` / `android` / `web` / `pc` 等，取决于客户端注册时上报值 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "ios": 412,
    "android": 587,
    "web": 96,
    "pc": 23
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | Redis 读失败 |

#### 备注

- 数据源是 Redis hash `scatterplot_regist:<appkey>:<YMD>`，由注册流程实时累加；时间区间按天遍历汇总
- 单日 Redis 读失败会被静默跳过，不中断整体响应

---

## 权限

权限对象是一组命名好的权限位组合（`Permission` 字符串），分配给后台账户后限制其可访问的菜单/接口。`id` 由 `name` 转拼音生成，因此 `name` 唯一。

平台账户操作 `admin` 范围的权限；商户账户只能看到本商户的权限；平台账户传 `business_id` 时会转发到目标商户节点。

### /admin/permission/add 新增权限

#### 接口地址

```
POST /admin/permission/add
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | ✓ | 权限名称（用于生成 id） |
| `desc` | string |  | 描述 |
| `status` | int64 | ✓ | `0`=禁用 `1`=启用 |
| `permission` | string |  | 权限位串（如 `user.manage,group.manage`） |
| `business_id` | string |  | 商户 id；平台账户操作平台权限可不传或传 `admin` |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "name": "客服管理员",
  "desc": "处理工单、查看会话",
  "status": 1,
  "permission": "user.view,chat.audit,report.deal",
  "business_id": "biz_2024_001",
  "business_name": "示例商户"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | name 是必填字段 | `name` 缺失 |
| 400 | status 字段非法 | `status` 不是 0 也不是 1 |
| 1000 | 已经存在对应权限名称，请修改权限名称重新提交 | 同范围内同名权限已存在 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 写入失败 |

---

### /admin/permission/del 删除权限

按 `id` 列表批量删除。范围（平台/商户）由调用方账户类型 + `business_id` 决定。

#### 接口地址

```
POST /admin/permission/del
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | []string | ✓ | 权限 id 列表 |
| `business_id` | string |  | 商户 id；平台账户操作商户权限时传 |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "ids": ["kefuguanliyuan", "yunyingzhuli"],
  "business_id": "biz_2024_001"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 唯一 ID 必填 | `ids` 为空 |
| 401 | (token 失效) | 未登录或 token 已过期 |

#### 备注

- 单个 id 删除失败只打日志，整体仍返回成功；调用方需通过 `/admin/permission/list` 确认
- 删除会写操作日志（business 操作日志）

---

### /admin/permission/edit 修改权限

字段为指针类型，**未传字段不修改**；`id` 必填。

#### 接口地址

```
POST /admin/permission/edit
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 权限 id |
| `name` | string |  | 改名（不影响 id） |
| `desc` | string |  | 改描述 |
| `status` | int64 |  | `0`=禁用 `1`=启用 |
| `permission` | string |  | 权限位串 |
| `business_id` | string |  | 商户 id；定位范围 |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "id": "kefuguanliyuan",
  "status": 1,
  "permission": "user.view,chat.audit,report.deal,suggest.deal",
  "business_id": "biz_2024_001"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 唯一 ID 必填 | `id` 缺失 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 修改后会写操作日志

---

### /admin/permission/detail 权限详情

#### 接口地址

```
POST /admin/permission/detail
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 权限 id |
| `business_id` | string |  | 商户 id；定位范围 |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "id": "kefuguanliyuan",
  "business_id": "biz_2024_001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 权限 id |
| `name` | string | 名称 |
| `desc` | string | 描述 |
| `status` | int64 | `0`=禁用 `1`=启用 |
| `permission` | string | 权限位串 |
| `update_time` | string | 最后更新时间 `YYYY-MM-DD HH:mm:ss` |
| `business_id` | string | 所属商户；平台范围不返回此字段 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "kefuguanliyuan",
    "name": "客服管理员",
    "desc": "处理工单、查看会话",
    "status": 1,
    "permission": "user.view,chat.audit,report.deal,suggest.deal",
    "update_time": "2026-05-25 14:32:10",
    "business_id": "biz_2024_001"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 唯一 ID 必填 | `id` 缺失 |
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 读失败 |

---

### /admin/permission/list 权限列表

支持按 name 模糊过滤；`simple=true` 时只返回 `id` / `name`，用于下拉框等场景。

#### 接口地址

```
POST /admin/permission/list
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 0 开始；默认 0 |
| `pageSize` | int64 |  | 每页条数；默认 20 |
| `searchKey` | string |  | 过滤字段名，目前仅支持 `name` |
| `searchVal` | string |  | 过滤值 |
| `simple` | bool |  | `true` 时只返回 `id` / `name`，其它字段省略 |
| `business_id` | string |  | 商户 id；定位范围 |
| `business_name` | string |  | 商户名（仅日志） |
| `appkey` | string |  | 商户 appkey（仅日志） |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "searchKey": "name",
  "searchVal": "客服",
  "business_id": "biz_2024_001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 满足条件的总数 |
| `list` | []object | 权限列表，单项结构同 `/admin/permission/detail` |

`list[]` 在 `simple=true` 时仅含 `id` / `name`。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "list": [
      {
        "id": "kefuguanliyuan",
        "name": "客服管理员",
        "desc": "处理工单、查看会话",
        "status": 1,
        "permission": "user.view,chat.audit,report.deal,suggest.deal",
        "update_time": "2026-05-25 14:32:10",
        "business_id": "biz_2024_001"
      },
      {
        "id": "kefuzhuli",
        "name": "客服助理",
        "desc": "查看会话",
        "status": 1,
        "permission": "user.view,chat.audit",
        "update_time": "2026-05-20 11:08:22",
        "business_id": "biz_2024_001"
      }
    ]
  }
}
```

`simple=true` 时：

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "list": [
      { "id": "kefuguanliyuan", "name": "客服管理员" },
      { "id": "kefuzhuli", "name": "客服助理" }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 401 | (token 失效) | 未登录或 token 已过期 |
| 500 | (内部 err) | DB 读失败 |

---


## 管理员账号

平台/商户管理员账号管理。鉴权规则：`X-Token` 对应的 token 中 `appkey == "admin"` 视为平台管理员，可管理任意商户下的管理员；非 admin 则只能管理自己名下商户（`businessDao.Uid == tokenData.UserId`）的管理员。

---

### /admin/manage/add 新增管理员

#### 接口地址

`POST /admin/manage/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `account` | string | ✓ | 登录账号，会被强制 lower+trim |
| `password` | string | ✓ | 密码（前端 MD5，固定 32 位小写） |
| `business_id` | string |  | 商户 id；平台管理员传空或 `"admin"` 表示新增平台管理员，否则为商户管理员 |
| `business_name` | string |  | 商户名称，仅用于操作日志 |
| `appkey` | string |  | 服务端会根据 `business_id` 覆盖，传值无效 |
| `name` | string |  | 显示名 |
| `avatar` | string |  | 头像 url |
| `desc` | string |  | 简介 |
| `remark` | string |  | 备注 |
| `url` | string |  | 预留字段 |
| `permission` | string |  | 权限 JSON 字符串，存储到 `position` 字段 |
| `tel` | string |  | 电话 |
| `email` | string |  | 邮箱 |
| `status` | int64 |  | `0`=禁用 `1`=启用 |

#### 请求示例

```json
{
  "account": "biz_admin01",
  "password": "e10adc3949ba59abbe56e057f20f883e",
  "business_id": "B0001",
  "business_name": "示例商户",
  "name": "运营张三",
  "tel": "13800000000",
  "email": "ops@example.com",
  "permission": "{\"label\":\"运营\",\"value\":\"op\",\"key\":\"op\",\"title\":\"运营\"}",
  "status": 1
}
```

#### 返回参数

`data` 为空，只看 `code`。

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `account是必填字段` / `密码格式不正确`（password 非 32 位）/ `没有操作权限` |
| 500 | 数据库查询/写入失败 |
| 1000 | `账号已经存在`（account 已被占用） |

#### 备注

- 平台管理员（`appkey="admin"`）必须 `Level==0`，否则 400 没有操作权限
- 商户管理员落库时 `ntype=0 / appkey=businessDao.Appkey`；平台管理员 `ntype=1 / appkey="admin"`
- 调用成功会在 `operator_records` 留下一条 `RecordOperationLogsBusiness` 操作记录

---

### /admin/manage/del 删除管理员

#### 接口地址

`POST /admin/manage/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uids` | []string | ✓ | 待删除管理员 uid 数组 |
| `business_id` | string |  | 商户 id，仅用于操作日志 |
| `business_name` | string |  | 商户名称，仅用于操作日志 |
| `appkey` | string |  | 仅用于操作日志 |

#### 请求示例

```json
{
  "uids": ["10000123", "10000124"],
  "business_id": "B0001",
  "business_name": "示例商户"
}
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `账号uids必填` / `没有权限查询管理员列表` / `没有对应商户信息` / `没有对应用户信息` / `有管理员用户不容许删除` |
| 500 | 数据库异常 |

#### 备注

- 平台管理员（token.appkey==admin）只能删 `ntype=0` 的平台管理员；商户管理员（token.appkey!=admin）只能删自己 `Position` 商户名下的管理员
- 商户主账号（`businessDao.Account`）和平台 `admin` 账号不允许删除
- 删除走 `DeleteBusinessUserByUid`（物理删）

---

### /admin/manage/edit 编辑管理员

#### 接口地址

`POST /admin/manage/edit`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uid` | string | ✓ | 目标管理员 uid（字符串） |
| `account` | string | ✓ | 账号（仅用于校验非空，实际不会更新数据库 account 字段） |
| `password` | string |  | 新密码，传值时必须 32 位小写 MD5 |
| `name` | *string |  | 显示名 |
| `avatar` | *string |  | 头像 url |
| `desc` | *string |  | 简介 |
| `remark` | *string |  | 备注 |
| `permission` | *string |  | 权限 JSON |
| `tel` | *string |  | 电话 |
| `email` | *string |  | 邮箱 |
| `status` | *int64 |  | `0`=禁用 `1`=启用 `2`=不变（直接返回 OK） |
| `business_id` | string |  | 商户 id，仅日志 |
| `business_name` | string |  | 商户名称，仅日志 |
| `appkey` | string |  | 仅日志 |
| `url` | string |  | 预留字段 |

#### 请求示例

```json
{
  "uid": "10000123",
  "account": "biz_admin01",
  "name": "运营张三-改",
  "tel": "13900000000",
  "status": 1,
  "remark": "VIP 客户对接"
}
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `账号uid必填` / `账号uid非法` / `account是必填字段` / `密码格式不正确` / `没有权限查询管理员列表` / `没有对应商户信息` / `权限非法` |
| 500 | 数据库异常 |
| 1000 | `用户不存在` |

#### 备注

- 状态变更会同步写 `globalkv.SetUserStatus`，禁用 / 启用即时生效
- 修改密码会清空登录失败次数（`ClearVerifyAccountPwdCount`）
- 编辑完成会清缓存 `business_user_<uid>`

---

### /admin/manage/detail 管理员详情

#### 接口地址

`POST /admin/manage/detail`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uid` | string | ✓ | 目标管理员 uid |
| `business_id` | string |  | 商户 id（仅平台 token 时有效） |
| `business_name` | string |  | 未使用 |
| `appkey` | string |  | 未使用 |

#### 请求示例

```json
{
  "uid": "10000123",
  "business_id": "B0001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `uid` | string | 管理员 uid |
| `account` | string | 登录账号 |
| `name` | string | 显示名 |
| `avatar` | string | 头像 url |
| `desc` | string | 简介（一般空） |
| `contact` | string | 联系方式（一般空） |
| `tel` | string | 电话 |
| `email` | string | 邮箱 |
| `status` | int64 | `0`=禁用 `1`=启用 |
| `permission` | string | 权限 JSON 字符串；服务端会按 `business_id` 对应的权限名回填 `label` / `title` |
| `business_id` | string | 商户 id；平台管理员该字段为空 |
| `business_name` | string | 商户名称 |
| `ctime` | string | 创建时间（字符串） |
| `remark` | string | 备注 |
| `login_time` | string | 上次登录时间，取自 redis `manage_login_data` |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "uid": "10000123",
    "account": "biz_admin01",
    "name": "运营张三",
    "tel": "13800000000",
    "email": "ops@example.com",
    "status": 1,
    "permission": "{\"label\":\"运营\",\"value\":\"op\",\"key\":\"op\",\"title\":\"运营\"}",
    "business_id": "B0001",
    "business_name": "示例商户",
    "login_time": "2026-05-26 10:23:11"
  }
}
```

#### 错误码

| code | 说明 |
|---|---|
| 400 | `uid非法` / `用户不存在` / `权限非法`（跨商户访问） |
| 500 | 数据库异常 |

---

### /admin/manage/list 管理员列表

#### 接口地址

`POST /admin/manage/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 `0` 开始 |
| `pageSize` | int64 |  | 每页条数，默认 `20` |
| `business_id` | string |  | 商户 id（平台 token 时筛选指定商户；商户 token 时忽略，强制为自己的 `Position`） |
| `searchKey` | string |  | 搜索字段名，可选 `name` / `business_id` / `uid` |
| `searchVal` | string |  | 搜索值 |
| `business_name` | string |  | 未使用 |
| `appkey` | string |  | 未使用 |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "business_id": "B0001",
  "searchKey": "name",
  "searchVal": "张"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总记录数 |
| `list` | []object | 列表项，结构同 `/admin/manage/detail` data（去掉 `desc`/`contact`，加 `desc`/`remark` 仅做日志展示） |

list 内每项字段同 detail，常用：`uid` / `account` / `name` / `avatar` / `tel` / `email` / `status` / `permission` / `business_id` / `business_name` / `ctime` / `login_time`。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "list": [
      {
        "uid": "10000123",
        "account": "biz_admin01",
        "name": "运营张三",
        "status": 1,
        "business_id": "B0001",
        "business_name": "示例商户",
        "ctime": "2026-04-01 10:00:00",
        "login_time": "2026-05-26 10:23:11"
      },
      {
        "uid": "10000124",
        "account": "biz_admin02",
        "name": "运营李四",
        "status": 1,
        "business_id": "B0001",
        "business_name": "示例商户",
        "ctime": "2026-04-02 11:00:00"
      }
    ]
  }
}
```

#### 错误码

| code | 说明 |
|---|---|
| 400 | `搜索uid非法` / `没有权限查询管理员列表` |
| 500 | 数据库异常 |

#### 备注

- 平台 token 不传 `business_id` 时默认查 `business_id="admin"`，即平台管理员列表
- 商户 token 强制 `ntype=1`，无法跨商户

---

### /admin/manage/reset/mfa 重置 MFA

#### 接口地址

`POST /admin/manage/reset/mfa`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uid` | int64 | ✓ | 待重置 MFA 的管理员 uid |

#### 请求示例

```json
{ "uid": 10000123 }
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `参数错误`（uid=0）/ `没有权限查询管理员列表` |
| 500 | 数据库更新失败 |

#### 备注

- 重置成功后，对应账号下次登录会强制重新绑定 MFA

---

### /admin/manage/business/records/list 商户管理员操作记录

#### 接口地址

`POST /admin/manage/business/records/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 `0` 开始 |
| `pageSize` | int64 |  | 每页条数，默认 `20` |
| `business_id` | string |  | 商户 id；平台 token 传 `"admin"` 表示查平台操作记录，其它值会按对应租户 appkey 过滤 |
| `appkey` | string |  | 与 `business_id` 配合使用 |
| `uid` | string \| int |  | 按操作者 uid 过滤 |
| `searchKey` | string |  | 搜索字段名，可选 `name` / `uid` / `time` |
| `searchVal` | string |  | 搜索值；`time` 为 `YYYY-MM-DD HH:MM:SS` |
| `business_name` | string |  | 未使用 |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "business_id": "B0001",
  "appkey": "PXXXXXXXX",
  "searchKey": "time",
  "searchVal": "2026-05-26 00:00:00"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总记录数 |
| `is_exisit` | bool | 是否存在二级目录（兼容老前端） |
| `list[]` | object | 操作记录 |
| `list[].uid` | string | 操作者 uid |
| `list[].account` | string | 操作者账号 |
| `list[].name` | string | 操作者显示名（从 `business_user` 缓存补全） |
| `list[].desc` | string | 操作描述文本 |
| `list[].time` | string | 操作时间，格式化 `YYYY-MM-DD HH:MM:SS` |
| `list[].business_id` | string | 商户 id |
| `list[].business_name` | string | 商户名称 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 1,
    "is_exisit": false,
    "list": [
      {
        "uid": "10000123",
        "account": "biz_admin01",
        "name": "运营张三",
        "desc": "商户id[B0001],商户名称[示例商户] 新增管理员商户 账号[op02]",
        "time": "2026-05-26 10:30:00",
        "business_id": "B0001",
        "business_name": "示例商户"
      }
    ]
  }
}
```

#### 错误码

| code | 说明 |
|---|---|
| 400 | `没有对应商户信息` |
| 500 | 数据库异常 |

---

### /admin/manage/platform/records/list 平台管理员操作记录

#### 接口地址

`POST /admin/manage/platform/records/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 `0` 开始 |
| `pageSize` | int64 |  | 每页条数，默认 `20` |
| `business_id` | string |  | 仅 `searchKey=business_id` 时使用 |
| `searchKey` | string |  | 搜索字段名，可选 `name` / `business_id` / `uid` / `time` |
| `searchVal` | string |  | 搜索值 |
| `business_name` | string |  | 未使用 |
| `appkey` | string |  | 未使用 |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 50,
  "searchKey": "uid",
  "searchVal": "1001"
}
```

#### 返回参数

字段同 `/admin/manage/business/records/list`，区别仅在仅返回 `appkey="admin"` 范围的记录，并强制 `is_exisit=true`。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 1,
    "is_exisit": true,
    "list": [
      {
        "uid": "1001",
        "account": "admin",
        "name": "超级管理员",
        "desc": "创建了商户信息,商户id[B0001],商户名称[示例商户]",
        "time": "2026-05-26 09:00:00"
      }
    ]
  }
}
```

#### 错误码

| code | 说明 |
|---|---|
| 400 | `没有权限查询管理员列表`（token.appkey 非 `admin`） |
| 500 | 数据库异常 |

#### 备注

- 仅平台管理员（`X-Token.appkey == "admin"`）可调用；商户 token 会被转发到 `/admin/manage/business/records/list` 失败后直接 400

---

## 商家

平台对租户（商户）的全生命周期管理：新增 / 删除 / 编辑 / 详情 / 列表 / 主机配置 / 业务开关 / 加解密辅助 / 邀请码管理。所有接口均需平台超管 token（部分接口允许商户主账号自查自己的商户）。

---

### /admin/business/add 新增商户

#### 接口地址

`POST /admin/business/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | ✓ | 商户名称 |
| `account` | string | ✓ | 商户主账号 |
| `password` | string | ✓ | 主账号密码，32 位小写 MD5 |
| `contact` | string |  | 联系人 |
| `tel` | string |  | 联系电话 |
| `email` | string |  | 联系邮箱 |
| `desc` | string |  | 简介 |
| `areaId` | string |  | 区域 id |
| `level` | string |  | 等级 |
| `userNums` | int64 |  | 用户上限，默认 `200000` |
| `status` | int64 |  | `0`=禁用 `1`=启用，默认启用 |
| `secret` | int64 |  | `0`=不加密 `1`=加密，默认 `1` |
| `groupMaxCnt` | int64 |  | 群成员上限，默认 `300` |
| `msgDays` | int64 |  | 消息保留天数，默认 `1` |
| `permission` | string |  | 商户级权限 JSON |
| `invitationCode` | string |  | 自定义邀请码（6 位纯数字），可选；不传则只生成默认邀请码 |

#### 请求示例

```json
{
  "name": "示例商户",
  "account": "biz_admin",
  "password": "e10adc3949ba59abbe56e057f20f883e",
  "contact": "王经理",
  "tel": "13800001234",
  "email": "biz@example.com",
  "userNums": 500000,
  "status": 1,
  "secret": 1,
  "groupMaxCnt": 500,
  "msgDays": 30,
  "permission": "openTranslation",
  "invitationCode": "888666"
}
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `参数错误，密码格式不对` / `account是必填字段` / `用户不存在` / `account已经存在` |
| 500 | 商户/管理员/App 任一写入失败 |
| 10001 | 邀请码正则错误 |
| 10002 | 邀请码格式不正确（须 6 位纯数字） |
| 10003 | 查询邀请码时发生错误 |
| 10004 | 邀请码已存在 |

#### 备注

- 服务端自动从 MD5(dao) 中截取尾部生成 `appkey`（首位 `P`），遇冲突按位前移重试
- 写库顺序：`InsertBusiness` → `AddBusinessUserUniq`（建主账号）→ `InsertApp` → `InsertInvite`（自动邀请码 + `invite_map` `is_auto=true`）→ 自定义 `invite_map` `is_auto=false`
- 操作记录走 `RecordOperationLogsPlat`

---

### /admin/business/del 删除商户

#### 接口地址

`POST /admin/business/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | []string | ✓ | 待删除商户 id 数组 |

#### 请求示例

```json
{ "ids": ["B0001", "B0002"] }
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `参数错误,id不存在` |
| 500 | 数据库异常 |

#### 备注

- 逻辑删除：商户表 `status=deleted`，对应 `app` 表 `status=deleted` 且 `max_user_total=0`
- 每条 id 都会写一条 `RecordOperationLogsPlat`

---

### /admin/business/edit 编辑商户

#### 接口地址

`POST /admin/business/edit`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | *string | ✓ | 商户 id |
| `account` | string | ✓ | 商户账号（用于校验商户存在） |
| `name` | *string |  | 商户名称 |
| `desc` | *string |  | 简介 |
| `contact` | *string |  | 联系人 |
| `tel` | *string |  | 电话 |
| `email` | *string |  | 邮箱 |
| `areaId` | *string |  | 区域 id |
| `level` | *string |  | 等级 |
| `userNums` | *int64 |  | 用户上限 |
| `status` | *int64 |  | `0`=禁用 `1`=启用 |
| `password` | *string |  | 主账号密码，32 位 MD5 |
| `secret` | *int64 |  | `0`/`1`，越界自动归 `1` |
| `groupMaxCnt` | *int64 |  | 群成员上限 |
| `msgDays` | *int64 |  | 消息保留天数 |
| `permission` | *string |  | 商户权限 JSON |

#### 请求示例

```json
{
  "id": "B0001",
  "account": "biz_admin",
  "name": "示例商户-新名",
  "userNums": 800000,
  "status": 1,
  "msgDays": 60,
  "permission": "openTranslation"
}
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `参数错误,id不存在` / `密码格式不正确` / `服务器信息未设置` |
| 500 | 数据库异常 |
| 10006 | 根据商户名称查询商户信息发生错误 |
| 10007 | 没有查询到对应商户信息 |

#### 备注

- 商户须先通过 `/admin/business/hostset` 设置 `ip_ex`/`ip_in`/`port_bo`/`dsource`，否则 400 `服务器信息未设置`
- `status`/`userNums`/`secret` 变化会同步更新 `app` 表，并 `globalkv.SetUserStatus` 实时禁用 / 启用主账号
- `permission` 中包含 `openTranslation` 变化时，会 Kafka 通知 APP（`admin_topic`，`NotifyType=7`），同时远程调 `/admin/user/msg/translate`
- `groupMaxCnt` 变化时远程调 `/admin/user/group/set/max`

---

### /admin/business/detail 商户详情

#### 接口地址

`POST /admin/business/detail`

#### 请求参数

通过 `FormValue("id")` 读取，建议 query 或 form 提交 `id=<business_id>`。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string |  | 商户 id；平台超管必填，商户主账号无须传，自动查自己 |

#### 请求示例

```
POST /admin/business/detail?id=B0001
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 商户 id |
| `name` | string | 商户名 |
| `account` | string | 主账号 |
| `password` | string | 主账号密码（已 MD5） |
| `desc` | string | 简介 |
| `contact` | string | 联系人 |
| `tel` | string | 电话 |
| `email` | string | 邮箱 |
| `areaId` | string | 区域 |
| `level` | string | 等级 |
| `userNums` | int64 | 用户上限 |
| `status` | int64 | `0`=禁用 `1`=启用 |
| `secret` | int64 | `0`=不加密 `1`=加密 |
| `groupMaxCnt` | int64 | 群成员上限 |
| `msgDays` | int64 | 消息保留天数 |
| `permission` | string | 权限 JSON |
| `vcodes[]` | object | 邀请码列表 |
| `vcodes[].vcode` | string | 邀请码 |
| `vcodes[].is_auto` | bool | `true`=自动生成、不可删 `false`=人工添加、可删 |
| `ip_ex` | string | 外部 IP |
| `ip_in` | string | 内部 IP |
| `port_bo` | int64 | 后台端口 |
| `port_tapi` | int64 | 租户 API 端口 |
| `port_tsync` | int64 | 租户 Sync 端口 |
| `port_sapi` | int64 | 系统 API 端口 |
| `route_tapi` | string | 租户 API 路由 |
| `route_tsync` | string | 租户 Sync 路由 |
| `route_sapi` | string | 系统 API 路由 |
| `dynamic` | int64 | 动态开关 `0`/`1` |
| `dsource` | string | 域名源 |
| `cs_link` | string | 客服链接 |
| `switch_regist` | int32 | 注册开关 `0`/`1` |
| `switch_rtc` | int32 | 音视频开关 |
| `switch_tab_main` | int32 | 主页 Tab 开关 |
| `switch_tab_mine` | int32 | 我的 Tab 开关 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "B0001",
    "name": "示例商户",
    "account": "biz_admin",
    "userNums": 500000,
    "status": 1,
    "secret": 1,
    "groupMaxCnt": 500,
    "msgDays": 30,
    "vcodes": [
      { "vcode": "AB12CD", "is_auto": true },
      { "vcode": "888666", "is_auto": false }
    ],
    "ip_ex": "1.2.3.4",
    "ip_in": "10.0.0.4",
    "port_bo": 13000,
    "switch_regist": 1,
    "switch_rtc": 1
  }
}
```

#### 错误码

| code | 说明 |
|---|---|
| 400 | `没有此数据` / `没有此商户数据` |
| 500 | 数据库异常 |

---

### /admin/business/list 商户列表

#### 接口地址

`POST /admin/business/list`

#### 请求参数

通过 `FormValue` 读取，建议 query 提交。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int |  | 页码，从 `0` 开始 |
| `pageSize` | int |  | 每页条数，默认 `20` |
| `searchKey` | string |  | 搜索字段，可选 `name` / `desc` / `contract` / `account` / `email` / `telno` / `vcode` |
| `searchVal` | string |  | 搜索值 |

#### 请求示例

```
POST /admin/business/list?page=0&pageSize=20&searchKey=name&searchVal=示例
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总记录数 |
| `list[]` | object | 商户列表项 |

list 中每项字段同 `/admin/business/detail` data，并额外包含：

| 字段 | 类型 | 说明 |
|---|---|---|
| `uid` | string | 商户主账号 uid |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 1,
    "list": [
      {
        "id": "B0001",
        "uid": "10000001",
        "name": "示例商户",
        "account": "biz_admin",
        "userNums": 500000,
        "status": 1,
        "vcodes": [
          { "vcode": "AB12CD", "is_auto": true }
        ],
        "switch_regist": 1
      }
    ]
  }
}
```

#### 错误码

| code | 说明 |
|---|---|
| 400 | `没有此商户数据`（非超管且名下无商户） |
| 500 | 数据库异常 |

#### 备注

- 平台超管（`admin_user.ntype=0`）走全量分页；商户主账号自动只返回自己名下的商户

---

### /admin/business/hostset 设置商户主机

#### 接口地址

`POST /admin/business/hostset`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 商户 id |
| `ip_ex` | string | ✓ | 外部 IP |
| `ip_in` | string | ✓ | 内部 IP |
| `port_bo` | int64 | ✓ | 后台端口 |
| `port_tapi` | int64 | ✓ | 租户 API 端口 |
| `port_tsync` | int64 | ✓ | 租户 Sync 端口 |
| `port_sapi` | int64 | ✓ | 系统 API 端口 |
| `route_tapi` | string |  | 租户 API 路由 |
| `route_tsync` | string |  | 租户 Sync 路由 |
| `route_sapi` | string |  | 系统 API 路由 |
| `dynamic` | int64 | ✓ | 动态开关 `0`/`1` |
| `dsource` | string | ✓ | 域名源 |
| `cs_link` | string |  | 客服链接 |

#### 请求示例

```json
{
  "id": "B0001",
  "ip_ex": "1.2.3.4",
  "ip_in": "10.0.0.4",
  "port_bo": 13000,
  "port_tapi": 13001,
  "port_tsync": 13002,
  "port_sapi": 13003,
  "route_tapi": "/tapi",
  "route_tsync": "/tsync",
  "route_sapi": "/sapi",
  "dynamic": 1,
  "dsource": "example.com",
  "cs_link": "https://cs.example.com"
}
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `参数错误` / `外部IP不能为空` / `内部IP不能为空` / `后台端口不能为空` / `租户 API 端口不能为空` / `租户 Sync 端口不能为空` / `系统 API 端口不能为空` / `动态开关格式不正确` / `域名源不能为空` / `操作失败` |
| 500 | 数据库异常 |

#### 备注

- 当 `ip_in` 从空被首次设置后，会自动远程调 `/admin/config/init` 初始化租户配置

---

### /admin/business/switch 商户业务开关

#### 接口地址

`POST /admin/business/switch`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 商户 id |
| `switch_regist` | *int32 |  | 注册开关 `0`=关 `1`=开 |
| `switch_rtc` | *int32 |  | 音视频开关 |
| `switch_tab_main` | *int32 |  | 主页 Tab 开关 |
| `switch_tab_mine` | *int32 |  | 我的 Tab 开关 |

#### 请求示例

```json
{
  "id": "B0001",
  "switch_regist": 0,
  "switch_rtc": 1
}
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `操作失败` |
| 500 | 数据库异常 |

#### 备注

- 成功后通过 Kafka `admin_topic`（`NotifyType=10`）推送 `BusinessSwitchInfo` 给该租户所有用户，APP 端会实时更新 Tab/注册/RTC 可见性

---

### /admin/business/enc 字符串加密

#### 接口地址

`POST /admin/business/enc`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `digit` | int | ✓ | 加密位数（与 `tools.Encrypt` 实现绑定） |
| `plainText` | string | ✓ | 明文 |

#### 请求示例

```json
{ "digit": 6, "plainText": "hello" }
```

#### 返回参数

`data` 直接为加密后的密文字符串。

#### 返回示例

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

#### 备注

- 仅做工具用途，不做参数合法性校验

---

### /admin/business/dec 字符串解密

#### 接口地址

`POST /admin/business/dec`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `cipherText` | string | ✓ | 密文 |

#### 请求示例

```json
{ "cipherText": "Aq8z3K..." }
```

#### 返回参数

`data` 为解密后的明文。

#### 返回示例

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

---

### /admin/business/vcode/add 新增邀请码

#### 接口地址

`POST /admin/business/vcode/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 商户 id |
| `vcode` | string | ✓ | 自定义邀请码，须满足 `^[0-9]{4,10}$`（前端按 6 位纯数字校验） |

#### 请求示例

```json
{ "id": "B0001", "vcode": "888666" }
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `邀请码不能为空` |
| 500 | 数据库异常 |
| 10001 | 校验邀请码发生错误 |
| 10002 | 邀请码格式不正确，必须为6位纯数字 |
| 10003 | 查询邀请码时发生错误 |
| 10004 | 邀请码已存在 |

#### 备注

- 写入 `invite_map`，`is_auto=false`，可被 `/admin/business/vcode/del` 删除

---

### /admin/business/vcode/del 删除邀请码

#### 接口地址

`POST /admin/business/vcode/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 商户 id |
| `vcode` | string | ✓ | 待删邀请码 |

#### 请求示例

```json
{ "id": "B0001", "vcode": "888666" }
```

#### 返回示例

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

#### 错误码

| code | 说明 |
|---|---|
| 400 | `邀请码不能为空` / 后端拒绝（如 `is_auto=true` 不允许删，错误信息透传） |
| 500 | 数据库异常 |

#### 备注

- 仅能删除 `is_auto=false` 的人工邀请码；商户创建时自动生成的（`is_auto=true`）不可删


---


## 用户

业务终端用户（appkey 下注册登录使用 IM 的真实用户）相关的后台管理接口。所有接口均为 `POST + JSON body`，路由前缀 `/admin/user/...`；公共业务字段：`appkey` / `business_id` / `business_name` 用于多商户路由与操作日志，由调用方填入，不再单独说明。

---

### /admin/user/add 新增用户

#### 接口地址

`POST /admin/user/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `appkey` | string | ✓ | 目标商户 appkey |
| `business_id` | string | ✓ | 商户 id |
| `business_name` | string |  | 商户名称（日志展示） |
| `name` | string | ✓ | 昵称，需通过昵称合法性校验 |
| `account` | string | ✓ | 登录账号（员工号格式） |
| `password` | string | ✓ | MD5 后 32 位小写 |
| `avatar` | string |  | 头像 url |
| `sex` | int | ✓ | `0`=未知 `1`=男 `2`=女 |
| `tel` | string |  | 手机号 |
| `email` | string |  | 邮箱 |

#### 请求示例

```json
{
  "appkey": "kkim_demo",
  "business_id": "B1001",
  "business_name": "示例商户",
  "name": "张三",
  "account": "zhangsan01",
  "password": "e10adc3949ba59abbe56e057f20f883e",
  "avatar": "https://cdn.example.com/avatar/1.png",
  "sex": 1,
  "tel": "13800000001",
  "email": "zhangsan@example.com"
}
```

#### 返回参数

成功仅返回 `code=0`，无 `data`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 性别参数非法 | `sex` 不是 0/1/2 |
| 400 | 密码格式不正确 | `password` 长度不是 32 |
| 400 | 参数非法 | `name` 或 `account` 为空 |
| 400 | 昵称格式不正确 | 命中非法字符 |
| 400 | 账号格式不正确 | `account` 不符合员工号规则 |
| 400 | 手机格式不正确 | `tel` 不符合手机号规则 |
| 400 | 邮箱格式不正确 | `email` 不符合邮箱规则 |
| 400 | 用户名已经存在 | `account` 在该 appkey 下已注册 |
| 400 | 商户不存在 | `appkey` 没有对应 business |
| 400 | 已经达到注册上限 | 当前商户用户数已超过 `MaxUserTotal` |
| 400 | 用户已存在 | DB 唯一约束冲突 |

#### 备注

- 若商户配置 `Permission` 包含 `openTranslation`，新用户默认开启翻译助手
- 写操作完成后会记录商户操作日志

---

### /admin/user/del 删除用户

#### 接口地址

`POST /admin/user/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | []string | ✓ | 待删除的用户 uid 数组 |
| `sign` | string | ✓ | 验签：`md5(md5(ids[0] + "-fuliao"))` |
| `appkey` | string |  | 目标商户 appkey；`admin` 跨商户可省略 |
| `business_id` | string | ✓ | 商户 id（兼容 `businessId`） |
| `businessId` | string |  | 兼容旧字段，等价于 `business_id` |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "ids": ["10000123", "10000124"],
  "sign": "5fa3c6d2b1ee9e0f8a7c4b3d2e1a9c80",
  "appkey": "kkim_demo",
  "business_id": "B1001",
  "business_name": "示例商户"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 参数非法 | `ids` 为空或 `business_id` 为空 |
| 400 | 验签非法 | `sign` 校验失败 |
| 400 | 有用户身份为群主或管理员 | 任一 uid 当前还是某群群主/管理员 |
| 400 | 权限非法 | 非 `admin` token 跨 appkey 删用户 |
| 400 | (业务文案) | `UserDeletedHandler` 返回失败消息 |
| 500 | (内部 err) | DB 或事件 handler 异常 |

#### 备注

- 删除会触发 `UserDeletedHandler` 清理好友、群成员、token 等关联数据
- 删除成功后会按商户 vcode 同步有效用户数

---

### /admin/user/edit 编辑用户

#### 接口地址

`POST /admin/user/edit`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 用户 uid |
| `sign` | string | ✓ | 验签：`md5(md5(id + "-fuliao"))` |
| `name` | string | ✓ | 昵称 |
| `avatar` | string |  | 头像 url |
| `status` | int |  | `0`=禁用 `1`=启用，省略表示不改 |
| `password` | string |  | 新密码 MD5 32 位；省略表示不改 |
| `tel` | string |  | 手机号；传空字符串清空 |
| `email` | string |  | 邮箱；传空字符串清空 |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `businessId` | string |  | 兼容旧字段 |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "10000123",
  "sign": "1a2b3c4d5e6f7890abcdef1234567890",
  "name": "张三丰",
  "avatar": "https://cdn.example.com/avatar/9.png",
  "status": 1,
  "tel": "13800000099",
  "email": "zhang@example.com",
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id不能为空 | `id` 缺失 |
| 400 | id非法 | `id` 不是正整数 |
| 400 | 密码格式不正确 | `password` 长度不是 32 |
| 400 | 参数非法 | `name` 缺失 |
| 400 | 昵称格式不正确 | `name` 命中非法字符 |
| 400 | 手机格式不正确 | `tel` 非空但格式错 |
| 400 | 邮箱格式不正确 | `email` 非空但格式错 |
| 400 | 验签非法 | `sign` 校验失败 |
| 400 | 用户不存在 | uid 没有对应用户 |
| 400 | 权限非法 | 非 `admin` token 跨 appkey |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 修改密码后会清空该 uid 除当前外的所有 token，并通过 kafka `admin_topic` 通知客户端踢线
- 修改昵称/手机/邮箱也会通过 kafka 通知客户端同步本地缓存

---

### /admin/user/password 重置用户密码

#### 接口地址

`POST /admin/user/password`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `account` | string | ✓ | 后台登录账号 |
| `phone` | string |  | 关联手机号（仅记录） |
| `newPassword` | string | ✓ | 新密码 MD5 32 位小写 |
| `sms_token` | string | ✓ | 短信验证码换得的 OTPToken |

#### 请求示例

```json
{
  "account": "admin01",
  "phone": "13800000001",
  "newPassword": "5f4dcc3b5aa765d61d8327deb882cf99",
  "sms_token": "otp_3f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 参数不对 | `sms_token` 缺失 |
| 400 | 密码格式不对 | `newPassword` 长度不是 32 |
| 400 | sms_token验证失败 | OTPToken 过期或不匹配 |
| 400 | 账号不存在 | `account` 没有对应后台账号 |
| 500 | (内部 err) | DB 异常 |

#### 备注

- 本接口针对的是后台业务管理员账号（`ntype=1`），不是终端 IM 用户
- 修改成功后清空登录失败计数

---

### /admin/user/unmute 解除用户禁言

#### 接口地址

`POST /admin/user/unmute`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 用户 uid |
| `sign` | string | ✓ | 验签：`md5(md5(id + "-fuliao"))` |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `businessId` | string |  | 兼容旧字段 |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "10000123",
  "sign": "1a2b3c4d5e6f7890abcdef1234567890",
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id不能为空 | `id` 缺失 |
| 400 | id非法 | `id` 不是正整数 |
| 400 | 验签非法 | `sign` 校验失败 |
| 400 | 用户不存在 | uid 没有对应用户 |
| 400 | 权限非法 | 非 `admin` token 跨 appkey |
| 500 | (内部 err) | 解禁写入失败 |

---

### /admin/user/sensitivectrl 设置用户敏感词豁免

#### 接口地址

`POST /admin/user/sensitivectrl`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ids` | []int64 | ✓ | 用户 uid 数组；服务端会将 `SensitiveCtrl` 翻转为豁免态 |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `businessId` | string |  | 兼容旧字段 |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "ids": [10000123, 10000124],
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | ids不能为空 | `ids` 数组为空 |
| 500 | (内部 err) | DB 写入失败 |

---

### /admin/user/detail 用户详情

#### 接口地址

`POST /admin/user/detail`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 用户 uid |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "10000123",
  "appkey": "kkim_demo",
  "business_id": "B1001",
  "business_name": "示例商户"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 用户 uid |
| `name` | string | 昵称 |
| `avatar` | string | 头像 url |
| `account` | string | 登录账号 |
| `tel` | string | 手机号 |
| `email` | string | 邮箱 |
| `password` | string | 已 hash 的密码（仅 admin 查看） |
| `status` | int | `0`=禁用 `1`=启用 |
| `onlineStatus` | int | `0`=离线 `1`=在线 |
| `onlineTime` | string | 最后在线时间，格式 `YYYY-MM-DD HH:MM:SS` |
| `regTime` | string | 注册时间，同上格式 |
| `regIp` | string | 注册 IP |
| `loginIp` | string | 最近登录 IP |
| `loginVer` | string | 最近登录客户端版本 |
| `loginDevice` | string | 最近登录设备 |
| `chatNums` | int64 | 好友总数 |
| `groupNums` | int64 | 加入群总数 |
| `msgAssistant` | int | 群发助手开关：`0`=关 `1`=开 |
| `businessId` | string | 商户 id |
| `businessName` | string | 商户名称 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "10000123",
    "name": "张三",
    "avatar": "https://cdn.example.com/avatar/1.png",
    "account": "zhangsan01",
    "tel": "13800000001",
    "email": "zhangsan@example.com",
    "status": 1,
    "onlineStatus": 1,
    "onlineTime": "2026-05-26 10:30:01",
    "regTime": "2025-08-12 09:00:00",
    "regIp": "203.0.113.5",
    "loginIp": "203.0.113.5",
    "loginVer": "3.1.2",
    "loginDevice": "iOS",
    "chatNums": 36,
    "groupNums": 12,
    "msgAssistant": 0,
    "businessId": "B1001",
    "businessName": "示例商户"
  }
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 用户ID不存在 | `id` 非法或缺失 |
| 400 | 没有有效数据 | uid 查不到 user |
| 500 | (内部 err) | DB 异常 |

---

### /admin/user/list 用户列表

#### 接口地址

`POST /admin/user/list`

请求参数走 `form-data` / query string（其他接口为 JSON body）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `businessId` | string | ✓ | 商户 id |
| `businessName` | string |  | 商户名称 |
| `appkey` | string |  | 目标商户 appkey；缺省取商户默认 appkey |
| `page` | int |  | 页码，从 0 开始；默认 0 |
| `pageSize` | int |  | 每页条数；默认 100 |
| `searchKey` | string |  | 过滤字段：`name` / `account` / `tel` / `email` / `id` / `ip` / `device` |
| `searchVal` | string |  | 过滤值；`id` 须为纯数字 |

#### 请求示例

```
POST /admin/user/list
Content-Type: application/x-www-form-urlencoded

businessId=B1001&page=0&pageSize=20&searchKey=name&searchVal=张三
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 满足条件总数 |
| `list` | []object | 用户数组 |
| `list[].id` | string | 用户 uid |
| `list[].name` | string | 昵称 |
| `list[].avatar` | string | 头像 |
| `list[].account` | string | 登录账号 |
| `list[].tel` | string | 手机号 |
| `list[].email` | string | 邮箱 |
| `list[].status` | int | `0`=禁用 `1`=启用 |
| `list[].onlineStatus` | int | `0`=离线 `1`=在线 |
| `list[].onlineTime` | string | 最后在线时间 |
| `list[].regTime` | string | 注册时间 |
| `list[].regIp` | string | 注册 IP |
| `list[].loginIp` | string | 最近登录 IP |
| `list[].loginVer` | string | 客户端版本 |
| `list[].loginDevice` | string | 设备 |
| `list[].chatNums` | int64 | 好友数 |
| `list[].groupNums` | int64 | 群数 |
| `list[].msgAssistant` | int | 群发助手 |
| `list[].isMute` | bool | 是否被禁言 |
| `list[].ignoreSensitiveCtrl` | int | `1`=敏感词豁免 `0`=不豁免 |
| `list[].businessId` | string | 商户 id |
| `list[].businessName` | string | 商户名称 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 235,
    "list": [
      {
        "id": "10000123",
        "name": "张三",
        "account": "zhangsan01",
        "tel": "13800000001",
        "email": "zhangsan@example.com",
        "status": 1,
        "onlineStatus": 1,
        "onlineTime": "2026-05-26 10:30:01",
        "regTime": "2025-08-12 09:00:00",
        "chatNums": 36,
        "groupNums": 12,
        "msgAssistant": 0,
        "isMute": false,
        "ignoreSensitiveCtrl": 0,
        "businessId": "B1001",
        "businessName": "示例商户"
      }
    ]
  }
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 商户ID为空 | `businessId` 缺失 |
| 400 | 商户ID不存在 | `businessId` 查不到 |
| 400 | 用户ID是纯数字 | `searchKey=id` 时 `searchVal` 非数字 |
| 400 | 权限非法 | 非 `admin` token 跨 appkey |
| 400 | 未配置商户的调用地址 | 商户没有配置 boUrl 转发地址 |
| 500 | (内部 err) | DB / 转发异常 |

#### 备注

- 商户 appkey 不在当前节点时，会通过 `boUrl` 转发到对应业务节点

---

### /admin/user/session/list 用户会话列表

#### 接口地址

`POST /admin/user/session/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | int64(string) | ✓ | 用户 uid，JSON 用字符串表示 |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "10000123",
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

`data` 为数组，每项：

| 字段 | 类型 | 说明 |
|---|---|---|
| `ctime` | int64 | 会话/推送记录创建时间（毫秒） |
| `lang` | string | 客户端语言 |
| `version` | string | 客户端版本 |
| `deviceId` | string | 设备 id |
| `ip` | string | 登录 IP |
| `token` | string | 登录 token |
| `push_record_id` | string | 推送记录 id（按字符串返回） |
| `regis` | string | 推送注册 id |
| `alias` | string | 推送别名 |

注：仅有 token、仅有推送记录、两者都有三种情况都会出现在列表中。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "ctime": 1716700000000,
      "lang": "zh",
      "version": "3.1.2",
      "deviceId": "iPhone15-ABCD",
      "ip": "203.0.113.5",
      "token": "tok_abcdef123456",
      "push_record_id": "8801",
      "regis": "fcm_xxx",
      "alias": "10000123"
    }
  ]
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 参数错误 | `id` 为 0 |
| 500 | (内部 err) | DB 异常 |

---

### /admin/user/session/del 删除用户会话

#### 接口地址

`POST /admin/user/session/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `token` | string |  | 待删除的登录 token；空表示不动 |
| `push_record_id` | int64(string) |  | 待删除的推送记录 id；`0` 表示不动 |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

至少需要提供 `token` 或 `push_record_id` 之一。

#### 请求示例

```json
{
  "token": "tok_abcdef123456",
  "push_record_id": "8801",
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 500 | (内部 err) | 推送记录删除失败 |

#### 备注

- 删除 token 等价于让对应设备下线
- `token` 查询失败时静默忽略，不会报错

---

### /admin/user/msg/assistant 开关用户群发助手

#### 接口地址

`POST /admin/user/msg/assistant`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 用户 uid |
| `msgAssistant` | int | ✓ | `0`=关闭 `1`=开启 |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "10000123",
  "msgAssistant": 1,
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id参数非法 | `id` 缺失或非正整数 |
| 400 | msgAssistant参数非法 | 不是 0 或 1 |
| 500 | (内部 err) | DB 异常 |

#### 备注

- 变更会通过 kafka `admin_topic` 通知客户端刷新本地开关状态

---

### /admin/user/msg/translate 开关商户翻译助手

#### 接口地址

`POST /admin/user/msg/translate`

按 appkey 维度批量开关该商户下所有用户的翻译助手开关。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `appkey` | string | ✓ | 目标商户 appkey |
| `msgTranslate` | int | ✓ | `0`=关闭 `1`=开启 |
| `sign` | string | ✓ | 验签：`md5(business_id + "|" + msgTranslate + "|" + SystemMsgToken)` |
| `business_id` | string | ✓ | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "appkey": "kkim_demo",
  "msgTranslate": 1,
  "sign": "ab12cd34ef56789012ab34cd56ef7890",
  "business_id": "B1001",
  "business_name": "示例商户"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | msgTranslate非法 | 不是 0 或 1 |
| 400 | 非法参数 | `sign` 校验失败 |
| 500 | (内部 err) | DB 批量更新失败 |

#### 备注

- 同步清理 `business_<appkey>` 与 `remote_app_<appkey>` 的 Redis 缓存

---

### /admin/user/group/set/max 批量设置历史群人数上限

#### 接口地址

`POST /admin/user/group/set/max`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `appkey` | string | ✓ | 目标商户 appkey |
| `max_cnt` | int64 | ✓ | 新的群人数上限，必须 `>0` |
| `sign` | string | ✓ | 验签：`md5(business_id + "|" + max_cnt + "|" + SystemMsgToken)` |
| `business_id` | string | ✓ | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "appkey": "kkim_demo",
  "max_cnt": 500,
  "sign": "ab12cd34ef56789012ab34cd56ef7890",
  "business_id": "B1001",
  "business_name": "示例商户"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 群成员最大数非法 | `max_cnt <= 0` |
| 400 | 非法参数 | `sign` 校验失败 |
| 500 | (内部 err) | DB 批量更新失败 |

#### 备注

- 影响该 appkey 下所有已存在的群，新建群仍走业务原有上限策略

---

## 单聊会话

按"两个 uid 组成的会话"维度管理私聊。tid 形如 `s_<uidA>_<uidB>`（两个 uid 升序拼接）。

---

### /admin/privateChat/list 私聊列表

#### 接口地址

`POST /admin/privateChat/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `appkey` | string | ✓ | 目标商户 appkey |
| `page` | int64 | ✓ | 页码，从 0 开始 |
| `pageSize` | int64 | ✓ | 每页条数，默认 20 |
| `searchKey` | string |  | 过滤字段：`name`=按用户昵称搜，`userId`=按 uid 搜 |
| `searchVal` | string |  | 过滤值 |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "appkey": "kkim_demo",
  "page": 0,
  "pageSize": 20,
  "searchKey": "name",
  "searchVal": "张三",
  "business_id": "B1001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 满足条件总数 |
| `list` | []object | 会话数组 |
| `list[].tid` | string | 会话 id（`s_<uidA>_<uidB>`） |
| `list[].userIdA` | string | A 方 uid |
| `list[].nameA` | string | A 方昵称（机器人为机器人名） |
| `list[].onlineTimeA` | string | A 方最后在线时间 |
| `list[].statusA` | string | A 方禁言：`0`=未禁言 `1`=已禁言 |
| `list[].userIdB` | string | B 方 uid |
| `list[].nameB` | string | B 方昵称 |
| `list[].onlineTimeB` | string | B 方最后在线时间 |
| `list[].statusB` | string | B 方禁言：`0`=未禁言 `1`=已禁言 |
| `list[].createTime` | string | 会话/好友建立时间 |
| `list[].chatTime` | string | 最后聊天时间 |
| `list[].businessId` | string | 商户 id |
| `list[].businessName` | string | 商户名称 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 128,
    "list": [
      {
        "tid": "s_10000123_10000124",
        "userIdA": "10000123",
        "nameA": "张三",
        "onlineTimeA": "2026-05-26 10:30:01",
        "statusA": "0",
        "userIdB": "10000124",
        "nameB": "李四",
        "onlineTimeB": "2026-05-26 09:55:12",
        "statusB": "0",
        "createTime": "2025-08-12 09:30:00",
        "chatTime": "2026-05-26 10:31:45",
        "businessId": "B1001",
        "businessName": "示例商户"
      }
    ]
  }
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 没有有效数据 | 当前条件下查不到用户 |
| 500 | (内部 err) | DB 异常 |

#### 备注

- 按搜索条件命中时，搜索结果会缓存到 Redis（key `search_<searchKey>_<searchVal>`），仅在 `page=0` 时重算
- 单聊任一方处于禁用 / 删除态时会跳过该条
- 机器人助手会按机器人名展示，最后在线时间填当前时间

---

### /admin/privateChat/detail 私聊详情

#### 接口地址

`POST /admin/privateChat/detail`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "s_10000123_10000124",
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 会话 id |
| `uidA` | string | A 方 uid |
| `nameA` | string | A 方昵称 |
| `onlineTimeA` | string | A 方最后在线时间 |
| `statusA` | string | A 方禁言：`0`=未禁言 `1`=已禁言 |
| `uidB` | string | B 方 uid |
| `nameB` | string | B 方昵称 |
| `onlineTimeB` | string | B 方最后在线时间 |
| `statusB` | string | B 方禁言：`0`=未禁言 `1`=已禁言 |
| `createTime` | string | 好友建立时间 |
| `chatTime` | string | 最后聊天时间 |
| `businessId` | string | 商户 id |
| `businessName` | string | 商户名称 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "tid": "s_10000123_10000124",
    "uidA": "10000123",
    "nameA": "张三",
    "onlineTimeA": "2026-05-26 10:30:01",
    "statusA": "0",
    "uidB": "10000124",
    "nameB": "李四",
    "onlineTimeB": "2026-05-26 09:55:12",
    "statusB": "1",
    "createTime": "2025-08-12 09:30:00",
    "chatTime": "2026-05-26 10:31:45",
    "businessId": "B1001",
    "businessName": "示例商户"
  }
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | tid非法 | `tid` 长度小于 3 |
| 400 | 没有有效数据 | 双方 uid 查不到用户 |
| 400 | 已经不存在好友关系 | 两个 uid 之间没有好友关系 |
| 400 | 有账号被删除 | A/B 中有一方被禁用或删除 |
| 500 | (内部 err) | DB 异常 |

---

### /admin/privateChat/record 私聊聊天记录

#### 接口地址

`POST /admin/privateChat/record`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id |
| `appkey` | string | ✓ | 目标商户 appkey |
| `page` | int64 | ✓ | 页码，从 0 开始 |
| `pageSize` | int64 | ✓ | 每页条数，默认 20 |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "s_10000123_10000124",
  "appkey": "kkim_demo",
  "page": 0,
  "pageSize": 20,
  "business_id": "B1001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 满足条件总数 |
| `list` | []object | 消息数组（按时间倒序） |
| `list[].id` | string | 消息 id |
| `list[].mid` | string | 消息 mid（同 `id`） |
| `list[].userid` | string | 发送方 uid |
| `list[].name` | string | 发送方昵称 |
| `list[].avatar` | string | 发送方头像 |
| `list[].createTime` | string | 发送时间 |
| `list[].msgType` | string | 消息类型，枚举同 C 端 `/v1/sdk/msg/send` |
| `list[].content` | string | 消息正文（端到端加密消息已自动解密） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 312,
    "list": [
      {
        "id": "s_10000123_10000124:9001",
        "mid": "s_10000123_10000124:9001",
        "userid": "10000123",
        "name": "张三",
        "avatar": "https://cdn.example.com/avatar/1.png",
        "createTime": "2026-05-26 10:31:45",
        "msgType": "txt",
        "content": "在吗"
      }
    ]
  }
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | tid非法 | `tid` 长度小于 3 |
| 400 | 没有有效数据 | 双方 uid 查不到用户 |
| 500 | (内部 err) | DB 异常 |

#### 备注

- 商户配置 `MsgDays > 0` 时只回溯最近 `MsgDays` 个月内的消息
- 加密消息使用 `md5(md5(appkey) + "-" + cid)` 作为对称密钥解密后返回明文

---

### /admin/privateChat/suspend 私聊/群聊禁言

#### 接口地址

`POST /admin/privateChat/suspend`

兼容私聊和群聊两种 tid：私聊只能对会话两人之一禁言；群聊可以对全员或指定成员禁言。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 会话 id（私聊 `s_a_b` / 群聊群 id 都可） |
| `type` | string | ✓ | `0`=全员（私聊=双方）禁言/解禁，`1`=按 `userids` 部分禁言/解禁 |
| `status` | string | ✓ | `0`=解除禁言 `1`=设置禁言 |
| `userids` | []string |  | 当 `type=1` 必填，被操作的 uid 列表 |
| `appkey` | string |  | 目标商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "s_10000123_10000124",
  "type": "1",
  "status": "1",
  "userids": ["10000124"],
  "appkey": "kkim_demo",
  "business_id": "B1001"
}
```

#### 返回参数

仅返回 `code=0`。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | tid非法 | `tid` 长度小于 3 或私聊 tid 切分后不是 3 段 |
| 400 | type非法 | 不是 "0" / "1" |
| 400 | status非法 | 不是 "0" / "1" |
| 400 | userids非法 | `type=1` 但 `userids` 为空 |
| 400 | 有用户不是群成员 | 群聊场景下 `userids` 中有人不在群里 |
| 400 | 有用户不是私聊成员 | 私聊场景下 `userids` 中有人不在会话里 |
| 500 | (内部 err) | DB 异常 |

#### 备注

- 禁言变更后会通过 kafka `admin_topic` 推送 `MuteInfo` 通知客户端
- 群聊 `type=0` 时，被通知的 `userIds` 是全体群成员


---


## 群聊管理

---

### /admin/group/operate 群解散或删除

对指定群执行解散或删除操作：清退全部成员、移除管理员、置位群标志，并通过 `admin_topic` Kafka 推送系统通知给客户端。

#### 接口地址

```
POST /admin/group/operate
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `type` | int64 | ✓ | `1`=解散 `2`=删除 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "type": 1,
  "appkey": "10000001",
  "business_id": "B001",
  "business_name": "测试商户"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | tid 参数非法 | `tid` 为空 |
| 400 | type 参数非法 | `type` 不是 1 或 2 |
| 400 | 参数错误，群信息不存在 | 群不存在 |
| 500 | (内部 err) | 查询/更新群失败 |

#### 备注

- 群已经处于解散状态（`flag != 0`）时不再重复推送解散提示
- 操作记录会写入商户操作日志

---

### /admin/groupChat/list 群聊列表

分页拉取商户下的群列表，支持按群名称模糊搜索。

#### 接口地址

```
POST /admin/groupChat/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 0 开始 |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `searchKey` | string |  | 过滤字段，目前仅支持 `name` |
| `searchVal` | string |  | 过滤值 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "searchKey": "name",
  "searchVal": "产品",
  "appkey": "10000001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[].tid` | string | 群 id |
| `list[].name` | string | 群名称 |
| `list[].totalUsers` | string | 群成员数 |
| `list[].creatorName` | string | 群主名称（机器人返回 robot 名） |
| `list[].creatorId` | string | 群主 uid |
| `list[].creatorTime` | string | 群主最后在线时间 |
| `list[].createTime` | string | 群创建时间 |
| `list[].businessName` | string | 商户名称 |
| `list[].onlineTime` | string | 群最后活跃时间 |
| `list[].chatTime` | string | 群最后发消息时间 |
| `list[].status` | string | `0`=未禁言 `1`=已全员禁言 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "list": [
      {
        "tid": "g_10000001_5",
        "name": "产品讨论群",
        "totalUsers": "12",
        "creatorName": "Alice",
        "creatorId": "10000123",
        "creatorTime": "2026-05-20 10:00:00",
        "createTime": "2025-01-01 09:00:00",
        "businessName": "测试商户",
        "onlineTime": "2026-05-25 18:00:00",
        "chatTime": "2026-05-25 18:00:00",
        "status": "0"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 没有群数据 | 商户下无群 |
| 400 | 没有有效数据 | 群成员均无效 |
| 500 | (内部 err) | DB 查询失败 |

---

### /admin/groupChat/detail 群聊详情

按 `tid` 查询单个群的详细信息。

#### 接口地址

```
POST /admin/groupChat/detail
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "appkey": "10000001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `tid` | string | 群 id |
| `name` | string | 群名称 |
| `totalUsers` | string | 群成员数 |
| `creatorName` | string | 群主名称 |
| `creatorId` | string | 群主 uid |
| `creatorTime` | string | 群主最后在线时间 |
| `createTime` | string | 群创建时间 |
| `businessName` | string | 商户名称 |
| `chatTime` | string | 群最后发消息时间 |
| `status` | string | `0`=未禁言 `1`=已全员禁言 |
| `announcement` | string | 群公告 |
| `joinNotice` | int64 | 入群通知：`0`=开启 `1`=关闭 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "tid": "g_10000001_5",
    "name": "产品讨论群",
    "totalUsers": "12",
    "creatorName": "Alice",
    "creatorId": "10000123",
    "creatorTime": "2026-05-20 10:00:00",
    "createTime": "2025-01-01 09:00:00",
    "businessName": "测试商户",
    "chatTime": "2026-05-25 18:00:00",
    "status": "0",
    "announcement": "请勿发广告",
    "joinNotice": 0
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | tid 非法 | `tid` 长度 ≤ 2 |
| 400 | 没有有效数据 | 群主用户不存在 |
| 500 | (内部 err) | DB 查询失败 |

#### 备注

- 群不存在时返回 `code=0` 且 `data` 为空对象

---

### /admin/groupChat/record 群聊天记录

分页拉取指定群的历史消息，已加密的消息会被自动解密后返回。

#### 接口地址

```
POST /admin/groupChat/record
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `page` | int64 |  | 页码，从 0 开始 |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "page": 0,
  "pageSize": 20,
  "appkey": "10000001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[].id` | string | 消息 id |
| `list[].userid` | string | 发送方 uid |
| `list[].name` | string | 发送方名称 |
| `list[].avatar` | string | 发送方头像 |
| `list[].createTime` | string | 发送时间 |
| `list[].mid` | string | 消息 id（`tid:tmid` 拼接） |
| `list[].content` | string | 正文（已自动解密） |
| `list[].msgType` | string | 消息类型 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 100,
    "list": [
      {
        "id": "g_10000001_5:12345",
        "userid": "10000123",
        "name": "Alice",
        "avatar": "https://cdn.example.com/a.png",
        "createTime": "2026-05-25 18:00:00",
        "mid": "g_10000001_5:12345",
        "content": "hello",
        "msgType": "text"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | tid 非法 | `tid` 长度 ≤ 2 |
| 400 | 没有有效数据 | 群无成员 |
| 500 | (内部 err) | DB 查询失败 |

#### 备注

- 商户配置 `msgDays > 0` 时只返回最近 N 个月内的消息
- 群不存在时返回空列表

---

### /admin/groupUser/list 群成员列表

拉取指定群的全部成员及在线/禁言状态。

#### 接口地址

```
POST /admin/groupUser/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `appkey` | string |  | 商户 appkey，留空默认取 token 所属 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "tid": "g_10000001_5",
  "appkey": "10000001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[].tid` | string | 群 id |
| `list[].userid` | string | 成员 uid |
| `list[].name` | string | 成员名称 |
| `list[].avatar` | string | 成员头像 |
| `list[].onlineTime` | string | 最后在线时间 |
| `list[].role` | string | `creator`=群主 `admin`=管理员 `成员`=普通成员 |
| `list[].joinTime` | string | 入群时间 |
| `list[].chatTime` | string | 最后发言时间 |
| `list[].status` | string | `0`=未禁言 `1`=已禁言 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 0,
    "list": [
      {
        "tid": "g_10000001_5",
        "userid": "10000123",
        "name": "Alice",
        "avatar": "https://cdn.example.com/a.png",
        "onlineTime": "2026-05-25 18:00:00",
        "role": "creator",
        "joinTime": "2025-01-01 09:00:00",
        "chatTime": "2026-05-25 18:00:00",
        "status": "0"
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | tid 非法 | `tid` 长度 ≤ 2 |
| 400 | 没有有效数据 | 群无成员 |
| 500 | (内部 err) | DB 查询失败 |

---

## 消息管理

---

### /admin/msg/mask 消息删除或撤回

后台对一条消息进行删除（`mask=1`）或撤回（`mask=2`），写入 mongo 的 mask 位并向 `admin_topic` 推送通知给客户端。

#### 接口地址

```
POST /admin/msg/mask
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mid` | string | ✓ | 消息 id（`tid:tmid`） |
| `mask` | int32 | ✓ | `1`=删除 `2`=撤回 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "mid": "g_10000001_5:12345",
  "mask": 2,
  "appkey": "10000001",
  "business_id": "B001",
  "business_name": "测试商户"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | mid 非法 | `mid` 为空 |
| 400 | mask 非法 | `mask` 不是 1 或 2 |
| 400 | 没有账号商户信息 | 商户不存在 |
| 400 | 没有操作权限 | 商户 `permission` 未开启 `openDelRecords` |
| 500 | (内部 err) | DB 查询/更新失败 |

---

### /admin/msg/download 启动消息导出任务

按导出流水号触发一次消息批量导出任务，异步处理；同一商户同一时刻仅允许一个导出任务运行。

#### 接口地址

```
POST /admin/msg/download
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `serial` | string | ✓ | 客户端生成的导出流水号 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "serial": "exp-20260526-001",
  "appkey": "10000001"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | serial 非法 | `serial` 为空 |
| 400 | 有导出任务处理中，请十分钟再重试 | 同 appkey 已有正在执行的导出任务 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 任务在后台协程内执行，本接口仅做入队，结果通过 `/admin/msg/download/list` 查询

---

### /admin/msg/download/list 消息导出任务列表

分页拉取本商户的消息导出任务及其下载链接。

#### 接口地址

```
POST /admin/msg/download/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 0 开始 |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "appkey": "10000001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[].id` | string | 任务流水号 |
| `list[].uid` | string | 发起的管理员 uid |
| `list[].status` | int64 | `0`=进行中 `1`=已完成 |
| `list[].createTime` | string | 任务创建时间 |
| `list[].downLoadUrl` | string | 完成后的下载 url |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 1,
    "list": [
      {
        "id": "exp-20260526-001",
        "uid": "10000999",
        "status": 1,
        "createTime": "2026-05-26 10:00:00",
        "downLoadUrl": "https://cdn.example.com/export/exp-20260526-001.zip"
      }
    ]
  }
}
```

#### 错误码

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

---

## 举报

---

### /admin/report/list 举报列表

分页拉取商户下的举报记录，支持按处理状态过滤。被举报对象可以是用户也可以是群。

#### 接口地址

```
POST /admin/report/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 0 开始 |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `status` | int64 |  | `0`=未处理 `1`=已处理，缺省/非法值=全部 |
| `business_id` | string | ✓ | 商户 id（用于查 appkey） |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "status": 0,
  "business_id": "B001",
  "business_name": "测试商户"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[].id` | string | 举报记录 id |
| `list[].oppReportName` | string | 被举报对象名称（用户昵称或群名） |
| `list[].oppReportId` | string | 被举报对象 id（uid 或 tid） |
| `list[].content` | string | 举报内容标签（多个用逗号拼接） |
| `list[].status` | string | `0`=未处理 `1`=已处理 |
| `list[].createtime` | string | 举报时间 |
| `list[].report_uid` | string | 举报人 uid |
| `list[].report_name` | string | 举报人昵称 |
| `list[].deal_desc` | string | 处理描述 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 1,
    "list": [
      {
        "id": "1001",
        "oppReportName": "产品讨论群",
        "oppReportId": "g_10000001_5",
        "content": "广告,色情",
        "status": "0",
        "createtime": "2026-05-26 09:00:00",
        "report_uid": "10000123",
        "report_name": "Alice",
        "deal_desc": ""
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 没有有效数据 | 关联用户/群数据缺失 |
| 500 | (内部 err) | DB 查询失败、target 反序列化失败 |

#### 备注

- 举报对象通过 `target` 字段反序列化判定：以 `g_` 开头视为群、`s_` 开头视为单聊、`uid` 字段非 0 视为用户

---

### /admin/report/deal 处理举报

后台对一条举报进行标记处理，写入处理人和处理描述并把状态置为已处理。

#### 接口地址

```
POST /admin/report/deal
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 举报记录 id |
| `deal_desc` | string |  | 处理描述 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "1001",
  "deal_desc": "已踢出群并封禁",
  "appkey": "10000001",
  "business_id": "B001",
  "business_name": "测试商户"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | id 非法 | `id` 为空或非正整数 |
| 500 | (内部 err) | DB 更新失败 |

---

## 反馈

---

### /admin/suggest/list 反馈列表

分页拉取商户下的用户反馈，支持按处理状态过滤。

#### 接口地址

```
POST /admin/suggest/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码，从 0 开始 |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `status` | int64 |  | `0`=未处理 `1`=已处理，缺省/非法值=全部 |
| `business_id` | string | ✓ | 商户 id（用于查 appkey） |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "status": 0,
  "business_id": "B001"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[].id` | string | 反馈记录 id |
| `list[].content` | string | 反馈内容 |
| `list[].status` | string | `0`=未处理 `1`=已处理 |
| `list[].createtime` | string | 反馈时间 |
| `list[].uid` | string | 反馈人 uid |
| `list[].name` | string | 反馈人昵称 |
| `list[].deal_desc` | string | 处理描述 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 1,
    "list": [
      {
        "id": "2001",
        "content": "希望增加暗色模式",
        "status": "0",
        "createtime": "2026-05-26 09:30:00",
        "uid": "10000123",
        "name": "Alice",
        "deal_desc": ""
      }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 没有有效数据 | 反馈人用户数据缺失 |
| 500 | (内部 err) | DB 查询失败 |

---

### /admin/suggest/deal 处理反馈

后台对一条反馈进行标记处理，写入处理人和处理描述并把状态置为已处理。

#### 接口地址

```
POST /admin/suggest/deal
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 反馈记录 id |
| `deal_desc` | string |  | 处理描述 |
| `appkey` | string |  | 商户 appkey |
| `business_id` | string |  | 商户 id |
| `business_name` | string |  | 商户名称 |

#### 请求示例

```json
{
  "id": "2001",
  "deal_desc": "已记录到产品 backlog",
  "appkey": "10000001",
  "business_id": "B001",
  "business_name": "测试商户"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | id 非法 | `id` 为空或非正整数 |
| 500 | (内部 err) | DB 更新失败 |

---


## 敏感词

敏感词命中规则与全平台合规相关，**只有 P 端（系统级 appkey=admin）账号才能管理系统级敏感词**；商户管理员通过传 `business_id` 由 P 端转发，操作自己 appkey 下的词库。

写入流程为：先调用 senstivesvr RPC 广播更新内存词典，再落 MongoDB；这样即使 DB 写失败，敏感词服务也不会处于「已更新但 DB 未持久化」的中间态。

---

### /admin/sensitive/add 新增敏感词

#### 接口地址

`POST /admin/sensitive/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID；传了走商户级，不传走系统级（仅 P 端有权） |
| `business_name` | string |  | 商户名称，用于操作日志 |
| `appkey` | string |  | 商户 appkey（P 端转发场景由网关填充） |
| `name` | string | ✓ | 敏感词文本，多个用空白分隔，会去重 |
| `method` | string | ✓ | 处理方式：`0`=不展示 `1`=警告 `2`=警告两次禁言 1 小时 `3`=警告两次禁言 12 小时 `4`=警告两次禁言 24 小时 |
| `disableStatus` | string | ✓ | `0`=禁用 `1`=启用 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "business_name": "测试商户A",
  "name": "违禁词1 违禁词2",
  "method": "2",
  "disableStatus": "1"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 敏感词不能为空字符串 | `name` 拆分后为空 |
| 400 | 参数错误 | `method` 或 `disableStatus` 非法 |
| 400 | 敏感词已经存在 | 同 appkey 或系统 appkey 下已有同词 |
| 500 | (内部 err) | RPC 或 DB 失败 |

#### 备注

- `disableStatus=1` 时才会调 senstivesvr 把词加入运行时词典；`0` 仅落 DB，不影响线上拦截
- 系统级敏感词对所有商户生效，添加前会先查 `tokenData.Appkey` 和 `admin` 两个 appkey 的同名词

---

### /admin/sensitive/del 删除敏感词

#### 接口地址

`POST /admin/sensitive/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID；传了走商户级 |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `ids` | []string | ✓ | 待删除的敏感词 ID 列表 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "business_name": "测试商户A",
  "ids": ["65e0a1f2c4b8e6d9a0123456", "65e0a1f2c4b8e6d9a0123457"]
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 参数错误敏感词为空 | `ids` 为空 |
| 400 | 无法删除系统敏感词 | 非 P 端用户尝试删 `admin` appkey 下的词 |
| 500 | (内部 err) | RPC 或 DB 失败 |

---

### /admin/sensitive/edit 编辑敏感词

#### 接口地址

`POST /admin/sensitive/edit`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `id` | string | ✓ | 敏感词 ID |
| `name` | *string |  | 新词文本，传了才改 |
| `method` | *string |  | 新处理方式，传了才改 |
| `disableStatus` | *string | ✓ | `0`=禁用 `1`=启用 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "business_name": "测试商户A",
  "id": "65e0a1f2c4b8e6d9a0123456",
  "name": "新违禁词",
  "method": "3",
  "disableStatus": "1"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id不能为空 | `id` 为空 |
| 400 | 参数错误 | `disableStatus` 非法 |
| 400 | 敏感词不存在 | ID 查不到 |
| 400 | 无法修改系统敏感词 | 非 P 端用户尝试改 `admin` appkey 下的词 |
| 400 | 敏感词已经存在 | 改后的词文本与他人记录冲突 |
| 500 | (内部 err) | RPC 或 DB 失败 |

#### 备注

- 切到 `disableStatus=0` 时会从 senstivesvr 内存词典移除；切到 `1` 时会重新注册
- 旧词文本保留在 `name` 字段，仅在 `name` 显式传入非空时才更新

---

### /admin/sensitive/list 敏感词列表

#### 接口地址

`POST /admin/sensitive/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `page` | int64 |  | 页码（从 0 开始） |
| `pageSize` | int64 |  | 每页条数，默认 200 |
| `searchKey` | string |  | 过滤字段，目前支持 `name` |
| `searchVal` | string |  | 过滤值 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "page": 0,
  "pageSize": 50,
  "searchKey": "name",
  "searchVal": "违禁"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 词库总数（当前 appkey + 系统 appkey） |
| `interceptCnt` | int64 | 命中拦截总次数 |
| `list[]` | []object | 见下 |
| `list[].id` | string | 敏感词 ID |
| `list[].name` | string | 词文本 |
| `list[].showTimes` | int64 | 拦截次数 |
| `list[].disableTimes` | int64 | 禁言次数 |
| `list[].disableStatus` | string | `0`=禁用 `1`=启用 |
| `list[].method` | string | 处理方式（同新增接口枚举） |
| `list[].appkey` | string | 词所属 appkey（`admin` 即系统级） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 128,
    "interceptCnt": 3421,
    "list": [
      {
        "id": "65e0a1f2c4b8e6d9a0123456",
        "name": "违禁词1",
        "showTimes": 17,
        "disableTimes": 3,
        "disableStatus": "1",
        "method": "2",
        "appkey": "kk_business_001"
      }
    ]
  }
}
```

#### 备注

- 列表会同时返回 `tokenData.Appkey` 和系统 `admin` 两个 appkey 下的所有词，前端按 `appkey` 字段区分能否编辑
- 仅 `searchKey=name` 走精确过滤，其他值或为空时返回全量

---

## 配置

`config` 指系统配置，包含「帮助页 URL」「关于软件」「小助手文案」「服务导航栏」四类，每类按 6 种语言（zh/en/vi/ja/es/bg）存 6 条。修改小助手文案 (`type=2`) 时会通过 Kafka `admin_topic` 推 `NotifyType=11`，让 APP 实时刷新。

---

### /admin/config/init 初始化商户配置

#### 接口地址

`POST /admin/config/init`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string | ✓ | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string | ✓ | 商户 appkey |
| `sign` | string | ✓ | `MD5(business_id + "|" + SystemMsgToken)` 防伪签名 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "business_name": "测试商户A",
  "appkey": "kk_business_001",
  "sign": "9d8a7b6c5e4f3a2b1c0d9e8f7a6b5c4d"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 非法参数 | `sign` 校验失败 |
| 500 | (内部 err) | DB 查询/写入失败 |

#### 备注

- 同一 appkey 下若已有超过 2 条配置记录，本接口幂等返回成功，不会重复初始化
- 同时会写入一条 `Id=All-{appkey}` 的超级管理员权限记录，附带全菜单的 `Permission` JSON 串

---

### /admin/config/modify 修改配置

#### 接口地址

`POST /admin/config/modify`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `id` | string | ✓ | 配置记录 ID |
| `content` | *string |  | 内容文本，传了才改 |
| `url` | string |  | 跳转 URL |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "id": "1716800000",
  "content": "您好，请问需要什么帮助？",
  "url": ""
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id非法 | `id` 为空或非数字 |
| 500 | (内部 err) | DB 查询/更新失败 |

#### 备注

- 修改后会删除 Redis 缓存键 `Key_SysCfg(appkey, ntype, lang)`，下次读取会回源 DB
- `type=2`（小助手）修改时会异步给 `admin_topic` 推一条 `NotifyType=11` 的消息

---

### /admin/config/detail 配置详情

#### 接口地址

`POST /admin/config/detail`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `id` | string | ✓ | 配置记录 ID |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "id": "1716800000"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 配置 ID |
| `content` | string | 内容文本 |
| `type` | string | `0`=帮助 `1`=关于软件 `2`=小助手 `3`=服务导航栏 |
| `url` | string | 跳转 URL |
| `lang` | string | 语言标识：`zh` / `en` / `vi` / `ja` / `es` / `bg` |
| `label` | string | 分组标签，如「帮助」「关于软件」 |
| `createtime` | string | 展示用时间字符串 |
| `check_uid` | string | 最近修改人 uid |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "1716800000",
    "content": "您好，请问需要什么帮助？",
    "type": "2",
    "url": "",
    "lang": "zh",
    "label": "小助手配置",
    "createtime": "2024-05-27 10:30:00",
    "check_uid": "100023"
  }
}
```

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id非法 | `id` 为空或非数字 |
| 500 | (内部 err) | DB 查询失败 |

---

### /admin/config/list 配置列表

#### 接口地址

`POST /admin/config/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `page` | int64 |  | 页码（从 0 开始） |
| `pageSize` | int64 |  | 每页条数，默认 20 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "page": 0,
  "pageSize": 50
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 该商户配置总条数 |
| `list[]` | []object | 见下 |
| `list[].id` | string | 配置 ID |
| `list[].content` | string | 内容文本 |
| `list[].type` | string | `0`=帮助 `1`=关于软件 `2`=小助手 `3`=服务导航栏 |
| `list[].url` | string | 跳转 URL |
| `list[].lang` | string | 语言：`zh` / `en` / `vi` / `ja` / `es` / `bg` |
| `list[].label` | string | 分组标签 |
| `list[].createtime` | string | 展示用时间字符串 |
| `list[].check_uid` | string | 最近修改人 uid |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 24,
    "list": [
      {
        "id": "1716800000",
        "content": "您好，请问需要什么帮助？",
        "type": "2",
        "url": "",
        "lang": "zh",
        "label": "小助手配置",
        "createtime": "2024-05-27 10:30:00",
        "check_uid": "100023"
      }
    ]
  }
}
```

---

## 公告

公告对全 APP 在 `start_time` / `end_time` 区间内可见；只支持新增和软删（`status=deleted`），不支持文本编辑。`plat` 字段虽然请求可传，但服务端会强制改为 `ALL` 全平台。

---

### /admin/notice/add 新增公告

#### 接口地址

`POST /admin/notice/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `title` | string | ✓ | 公告标题 |
| `content` | string | ✓ | 公告正文 |
| `href` | string |  | 跳转链接 |
| `start_time` | string | ✓ | 生效起始时间（秒级或毫秒级时间戳字符串） |
| `end_time` | string | ✓ | 生效结束时间 |
| `plat` | string |  | 服务端无视入参，强制 `ALL` |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "business_name": "测试商户A",
  "appkey": "kk_business_001",
  "title": "系统升级通知",
  "content": "5月30日凌晨0:00-2:00进行系统升级，期间服务暂停",
  "href": "https://admin.example.com/notice/2024053001",
  "start_time": "1716998400",
  "end_time": "1717084800"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | start_time非法 | 时间戳解析为 0 |
| 400 | end_time非法 | 时间戳解析为 0 |
| 500 | (内部 err) | DB 写入失败 |

---

### /admin/notice/modify 修改公告

#### 接口地址

`POST /admin/notice/modify`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `id` | string | ✓ | 公告 ID |
| `status` | *string |  | 仅支持 `deleted` 软删 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "id": "65e0a1f2c4b8e6d9a0123456",
  "status": "deleted"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | status数据非法 | `status` 非空且不为 `deleted` |
| 400 | 公告信息找不到 | `id` 不存在 |
| 400 | 公告信息未更新 | 已经是目标状态 |
| 500 | (内部 err) | DB 更新失败 |

---

### /admin/notice/list 公告列表

#### 接口地址

`POST /admin/notice/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `page` | int64 |  | 页码（从 0 开始） |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `searchKey` | string |  | 过滤字段：`title` 或 `content` |
| `searchVal` | string |  | 过滤值 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "page": 0,
  "pageSize": 20,
  "searchKey": "title",
  "searchVal": "升级"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 公告总数 |
| `list[]` | []object | 见下 |
| `list[].id` | string | 公告 ID |
| `list[].title` | string | 标题 |
| `list[].content` | string | 正文 |
| `list[].href` | string | 跳转链接 |
| `list[].start_time` | string | 生效起始时间（展示字符串） |
| `list[].end_time` | string | 生效结束时间（展示字符串） |
| `list[].status` | string | `""`=正常 `deleted`=已删除 |
| `list[].uid` | int64 | 创建人 uid |
| `list[].ctime` | string | 创建时间（展示字符串） |
| `list[].utime` | string | 最后更新时间（展示字符串） |
| `list[].readCnt` | int64 | 已读用户数 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 12,
    "list": [
      {
        "id": "65e0a1f2c4b8e6d9a0123456",
        "title": "系统升级通知",
        "content": "5月30日凌晨0:00-2:00进行系统升级",
        "href": "https://admin.example.com/notice/2024053001",
        "start_time": "2024-05-30 00:00:00",
        "end_time": "2024-05-31 00:00:00",
        "status": "",
        "uid": 100023,
        "ctime": "2024-05-27 10:30:00",
        "utime": "2024-05-27 10:30:00",
        "readCnt": 532
      }
    ]
  }
}
```

---

## 广播

广播是后台主动推到 IM 客户端的系统通知，**支持定时发送**：服务端用 `time.AfterFunc` 维护进程内定时器，到点后把任务 ID 投到 Kafka `admin_topic`（`NotifyType=8`），由消费侧把广播写入用户会话。服务重启时会从 MongoDB 恢复未执行的定时任务。

`scene` 区分租户级和系统级；租户级通过 `business_id` 走 P 端转发，系统级直接走当前会话校验。

---

### /admin/broadcast/add 新增广播

#### 接口地址

`POST /admin/broadcast/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID（`scene=0/1` 时由 P 端转发用到） |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `msg` | string | ✓ | 文本内容，服务端会包装成 `[{"mtype":"txt","content":"..."}]` 入库 |
| `scene` | string |  | `0`=租户全部（默认）`1`=租户部分 `3`=系统全部 `4`=系统部分 |
| `sendTime` | int64 | ✓ | 发送时间（毫秒时间戳），必须晚于当前时间 |
| `receiveUids` | []string |  | `scene=1` 时的接收用户 uid 列表 |
| `businessInfo` | []string |  | `scene=4` 时的目标商户 ID 列表 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "business_name": "测试商户A",
  "appkey": "kk_business_001",
  "msg": "新版本v3.2.0已上线，欢迎升级体验",
  "scene": "0",
  "sendTime": 1717084800000,
  "receiveUids": []
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | msg不能为空 | `msg` 长度为 0 |
| 400 | sendTime必须是一个未来的时间戳 | `sendTime` 不晚于当前时间 |
| 400 | sendTime计算出的延迟时间为负数 | 内部计算保护 |
| 400 | Scene非法 | 系统级场景下 `scene` 不为 `3` 或 `4` |
| 500 | (内部 err) | DB 写入或 Kafka 发送失败 |

#### 备注

- `scene=3/4` 系统级广播只允许 P 端账号或拥有系统 appkey 的账号发起
- 同步会下发一份定时任务（taskId = `broadcast_<announcementId>`）；服务重启后会从 DB 中状态为「未发送且有效」的记录恢复定时器

---

### /admin/broadcast/modify 修改广播

#### 接口地址

`POST /admin/broadcast/modify`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `id` | string | ✓ | 广播 ID |
| `scene` | string |  | 与新增一致：`0`/`1`/`3`/`4`，默认 `0` |
| `status` | *int64 |  | `1`=有效 `2`=失效（会撤销定时任务） |
| `sendTime` | *int64 |  | 新的发送时间（毫秒时间戳），改了会重建定时器 |
| `businessInfo` | []string |  | `scene=4` 系统级部分的目标商户列表 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "id": "65e0a1f2c4b8e6d9a0123456",
  "scene": "0",
  "status": 2
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | status非法 | `status` 不为 `1` 或 `2` |
| 400 | Scene非法 | 系统级场景下 `scene` 不为 `3` 或 `4` |
| 500 | (内部 err) | DB 更新失败 |

#### 备注

- 把 `status` 改为 `2` 会立刻 `CancelTask`，已经发出去的消息不可撤回
- 修改 `sendTime` 时，若新时间已经过去，会立即在新协程中执行一次广播投递

---

### /admin/broadcast/list 广播列表

#### 接口地址

`POST /admin/broadcast/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `page` | int64 |  | 页码（从 0 开始） |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `searchKey` | string |  | 过滤字段：仅支持 `msg` |
| `searchVal` | string |  | 过滤值 |
| `type` | string | ✓ | `1`=商户级 其他值=系统级 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "page": 0,
  "pageSize": 20,
  "type": "1"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 广播总数 |
| `list[]` | []object | 见下 |
| `list[].id` | string | 广播 ID |
| `list[].createUid` | string | 创建人 uid |
| `list[].msg` | string | 文本内容（已解包为字符串） |
| `list[].scene` | string | `0`/`1`/`3`/`4` |
| `list[].status` | int64 | `1`=有效 `2`=失效 |
| `list[].sendStatus` | int64 | `0`=未发送 `1`=发送中 `2`=发送完成 |
| `list[].ctime` | int64 | 创建时间（毫秒） |
| `list[].utime` | int64 | 更新时间（毫秒） |
| `list[].sendTime` | int64 | 计划发送时间（毫秒） |
| `list[].startSendTime` | int64 | 实际开始发送时间（毫秒） |
| `list[].endSendTime` | int64 | 实际结束发送时间（毫秒） |
| `list[].receiveUids` | []string | `scene=1` 接收用户列表 |
| `list[].businessInfo` | []string | `scene=4` 商户 ID 列表 |
| `list[].businessNames` | []string | `scene=4` 商户名称列表（仅系统级查询时填充） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 5,
    "list": [
      {
        "id": "65e0a1f2c4b8e6d9a0123456",
        "createUid": "100023",
        "msg": "新版本v3.2.0已上线，欢迎升级体验",
        "scene": "0",
        "status": 1,
        "sendStatus": 2,
        "ctime": 1716800000000,
        "utime": 1716800000000,
        "sendTime": 1717084800000,
        "startSendTime": 1717084800123,
        "endSendTime": 1717084801456,
        "receiveUids": [],
        "businessInfo": null,
        "businessNames": null
      }
    ]
  }
}
```

#### 备注

- `type=1` 走 P 端转发查商户库；其他值走当前节点直查，并对 `scene=4` 的记录回填 `businessNames`

---

## 黑名单

后台维护的「IP / 设备」黑名单，**软删模型**：删除时把 `status` 改为已删除并清 Redis 缓存，重复添加同一条会自动复活已删除记录；网关侧用 Redis 集合 `RedisAdminBlack*` 做实时拦截。

---

### /admin/black/add 新增黑名单

#### 接口地址

`POST /admin/black/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `content` | string | ✓ | IP 字符串 (v4/v6) 或设备 ID |
| `type` | int64 | ✓ | `1`=IP `2`=设备 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "content": "203.0.113.45",
  "type": 1
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | content非法 | `content` 去空白后为空 |
| 400 | type非法 | `type` 不为 `1` 或 `2` |
| 400 | ip格式非法 | `type=1` 但 `content` 不是合法 IPv4/IPv6 |
| 400 | 已经在黑名单中，请勿重复添加 | 同 appkey+content 已有生效记录 |
| 500 | (内部 err) | DB 或 Redis 失败 |

#### 备注

- 同 (appkey, content) 已存在但 `status=deleted` 的记录会被复活而不是新建
- 新增成功后会同步写 Redis 拦截集合，无需等待消费

---

### /admin/black/del 删除黑名单

#### 接口地址

`POST /admin/black/del`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `ids` | []string | ✓ | 黑名单记录 ID 列表 |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "ids": ["65e0a1f2c4b8e6d9a0123456", "65e0a1f2c4b8e6d9a0123457"]
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 数据非法 | `ids` 为空 |
| 500 | (内部 err) | DB 或 Redis 失败 |

#### 备注

- 列表中不存在的 ID 会跳过，仅在日志中记录 `skipped`，不会中断整个请求
- 已经是 `deleted` 状态的记录幂等跳过，不会重复操作 Redis

---

### /admin/black/list 黑名单列表

#### 接口地址

`POST /admin/black/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `business_id` | string |  | 商户 ID |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | 商户 appkey |
| `page` | int64 |  | 页码（从 0 开始） |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `searchVal` | string |  | 模糊搜索 `content` |

#### 请求示例

```json
{
  "business_id": "B202401001",
  "appkey": "kk_business_001",
  "page": 0,
  "pageSize": 50,
  "searchVal": "203.0"
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 有效记录总数 |
| `list[]` | []object | 见下 |
| `list[].id` | string | 记录 ID |
| `list[].content` | string | IP 或设备 ID |
| `list[].type` | int64 | `1`=IP `2`=设备 |
| `list[].createtime` | string | 最近更新时间（展示字符串） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 23,
    "list": [
      {
        "id": "65e0a1f2c4b8e6d9a0123456",
        "content": "203.0.113.45",
        "type": 1,
        "createtime": "2024-05-27 10:30:00"
      }
    ]
  }
}
```

#### 备注

- 默认只返回 `status=active` 的有效记录，已删除条目不出现在列表中

---

## 潜客

潜客 (Potential) 是从官网留资表单进来的销售线索，独立一张 `tb_potential` 表，**未登录就能提交**（`/admin/potential/add`）；管理后台拉列表、处理、查待处理 ID 都需要登录。

---

### /admin/potential/add 提交潜客线索

#### 接口地址

`POST /admin/potential/add`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `lang` | string |  | 语言（用于错误提示本地化），默认随服务 |
| `name` | string | ✓ | 称呼 |
| `email` | string | ✓ | 邮箱 |
| `mobile` | string | ✓ | 手机号 |
| `cname` | string | ✓ | 公司名称 |
| `position` | string |  | 职位 |
| `industry` | string |  | 行业 |
| `scale` | string |  | 规模 |

#### 请求示例

```json
{
  "lang": "zh",
  "name": "张三",
  "email": "zhangsan@example.com",
  "mobile": "13800138000",
  "cname": "测试科技有限公司",
  "position": "CTO",
  "industry": "互联网",
  "scale": "100-500人"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | 姓名不能为空 | `name` 去空白后为空 |
| 400 | 邮箱不能为空 | `email` 去空白后为空 |
| 400 | 手机不能为空 | `mobile` 去空白后为空 |
| 400 | 公司名称不能为空 | `cname` 去空白后为空 |
| 1012 | 请求次数过多，请稍后再试 | 同 IP 短时间内提交过多被限流 |
| 500 | (内部 err) | DB 写入失败 |

#### 备注

- 接口不校验 token，按 IP 做频控（`AllowAccessByIP`）防止刷表
- 新记录 `status` 默认是 `0` (pending)，等待管理后台处理

---

### /admin/potential/list 潜客列表

#### 接口地址

`POST /admin/potential/list`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | int64 |  | 页码（从 0 开始） |
| `pageSize` | int64 |  | 每页条数，默认 20 |
| `status` | *int32 |  | `0`=pending（待处理）`1`=reached（已联系）；非法值会被忽略，按全量返回 |

#### 请求示例

```json
{
  "page": 0,
  "pageSize": 20,
  "status": 0
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 总数 |
| `list[]` | []object | 见下 |
| `list[].id` | string | 雪花 ID（字符串化避免精度丢失） |
| `list[].name` | string | 称呼 |
| `list[].email` | string | 邮箱 |
| `list[].mobile` | string | 手机号 |
| `list[].cname` | string | 公司名称 |
| `list[].position` | string | 职位 |
| `list[].industry` | string | 行业 |
| `list[].scale` | string | 规模 |
| `list[].status` | int32 | `0`=pending `1`=reached |
| `list[].ctime` | int64 | 创建时间（毫秒） |
| `list[].utime` | int64 | 更新时间（毫秒） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 87,
    "list": [
      {
        "id": "7234567890123456789",
        "name": "张三",
        "email": "zhangsan@example.com",
        "mobile": "13800138000",
        "cname": "测试科技有限公司",
        "position": "CTO",
        "industry": "互联网",
        "scale": "100-500人",
        "status": 0,
        "ctime": 1716800000000,
        "utime": 1716800000000
      }
    ]
  }
}
```

---

### /admin/potential/deal 处理潜客

#### 接口地址

`POST /admin/potential/deal`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 潜客记录 ID |

#### 请求示例

```json
{
  "id": "7234567890123456789"
}
```

#### 返回参数

无业务数据，`code=0` 即成功。

#### 返回示例

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

#### 错误码

| code | 含义 | 说明 |
|---|---|---|
| 400 | id非法 | `id` 为空或非数字 |
| 500 | (内部 err) | DB 更新失败 |

#### 备注

- 仅做单向状态推进 `pending -> reached`，没有反向接口

---

### /admin/potential/peek 待处理潜客 ID 列表

#### 接口地址

`POST /admin/potential/peek`

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `stime` | int64 |  | 创建时间起点（毫秒） |
| `etime` | int64 |  | 创建时间终点（毫秒） |

#### 请求示例

```json
{
  "stime": 1716800000000,
  "etime": 1717084800000
}
```

#### 返回参数

直接返回 `data` 为字符串数组，元素为 pending 状态的潜客 ID。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    "7234567890123456789",
    "7234567890123456790",
    "7234567890123456791"
  ]
}
```

#### 备注

- 不分页，直接返回区间内所有 `status=pending` 的记录 ID，主要给定时巡检/提醒脚本使用


---


## APP 版本

`/admin/appver/...` 用于客户端版本管理：录入新版本、下架、调整下载地址或描述、查询列表和最新版本。APK 文件通过 `/admin/appver/upload` 单独上传到对象存储后再走 `/admin/appver/add` 落库。

---

### /admin/appver/add 新增应用版本

录入一条新的应用版本记录。`plf`、`sn`、`url`、`desc` 都不能为空。

#### 接口地址

```
POST /admin/appver/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `plf` | string | ✓ | 平台：`ios` / `android` / `pc` |
| `sn` | string | ✓ | 版本号字符串（小写），例如 `1.2.3` |
| `build` | int32 |  | 打包号，>0 才会落库 |
| `url` | string | ✓ | 安装包下载地址 |
| `desc` | string | ✓ | 更新说明，支持 `\n` 换行 |
| `status` | int32 |  | 状态：`0`=禁用 `1`=正常 |

#### 请求示例

```json
{
  "plf": "android",
  "sn": "1.2.3",
  "build": 230,
  "url": "https://cdn.example.com/app/android/1.2.3/app.apk",
  "desc": "1. 修复登录闪退\n2. 新增暗黑模式",
  "status": 1
}
```

#### 返回参数

返回新建的 `AppVerDao`：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 雪花 id（字符串） |
| `plf` | string | 平台 |
| `sn` | string | 版本号 |
| `url` | string | 下载地址 |
| `desc` | string | 更新说明 |
| `ctime` | int64 | 创建时间（毫秒） |
| `status` | int32 | `0`=禁用 `1`=正常 |
| `sort` | int64 | 版本号排序值（由 `sn` 转换而来） |
| `build` | int32 | 打包号（为 0 时省略） |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "184629134812372992",
    "plf": "android",
    "sn": "1.2.3",
    "url": "https://cdn.example.com/app/android/1.2.3/app.apk",
    "desc": "1. 修复登录闪退\n2. 新增暗黑模式",
    "ctime": 1716700000000,
    "status": 1,
    "sort": 100020003,
    "build": 230
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 平台不能为空 | `plf` 空 |
| 400 | 平台不支持 | `plf` 不在 `ios/android/pc` 中 |
| 400 | 版本号不能为空 | `sn` 空 |
| 400 | 地址不能为空 | `url` 空 |
| 400 | 说明不能为空 | `desc` 空 |
| 500 | (内部 err) | DB 写入或 Redis 索引失败 |

---

### /admin/appver/del 删除应用版本

按 id 删除一条版本记录，同时清掉对应的 Redis 索引。

#### 接口地址

```
POST /admin/appver/del
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 版本记录 id（字符串） |

#### 请求示例

```json
{ "id": "184629134812372992" }
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | ID不能为空 | `id` 空 |
| 500 | (内部 err) | `id` 非数字 / 记录不存在 / DB 删除失败 |

---

### /admin/appver/edit 修改应用版本

修改下载地址、更新说明或状态。`plf` 与 `sn` 不允许改。三个字段都是可选指针，未传则保持不变；传了但为空则报错。

#### 接口地址

```
POST /admin/appver/edit
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 版本记录 id |
| `url` | string |  | 新下载地址，传了不能为空 |
| `desc` | string |  | 新更新说明，传了不能为空 |
| `status` | int32 |  | 新状态：`0`=禁用 `1`=正常 |

#### 请求示例

```json
{
  "id": "184629134812372992",
  "url": "https://cdn.example.com/app/android/1.2.3/app-hotfix.apk",
  "status": 0
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | ID不能为空 | `id` 空 |
| 400 | 地址不能为空 | `url` 传了但为空 |
| 400 | 说明不能为空 | `desc` 传了但为空 |
| 500 | (内部 err) | `id` 非数字 / 记录不存在 / DB 更新失败 |

---

### /admin/appver/list 应用版本列表

分页查询版本，可按平台、版本号、状态过滤。

#### 接口地址

```
POST /admin/appver/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageSize` | int64 |  | 每页条数 |
| `pageIndex` | int64 |  | 页码，从 0 开始 |
| `plf` | string |  | 平台过滤：`ios` / `android` / `pc` |
| `sn` | string |  | 版本号过滤 |
| `status` | int32 |  | 状态过滤：`0`=禁用 `1`=正常 |

#### 请求示例

```json
{
  "pageSize": 20,
  "pageIndex": 0,
  "plf": "android",
  "status": 1
}
```

#### 返回参数

`data` 直接是 `AppVerDao` 数组，结构同 `/admin/appver/add` 的返回。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "id": "184629134812372992",
      "plf": "android",
      "sn": "1.2.3",
      "url": "https://cdn.example.com/app/android/1.2.3/app.apk",
      "desc": "1. 修复登录闪退\n2. 新增暗黑模式",
      "ctime": 1716700000000,
      "status": 1,
      "sort": 100020003,
      "build": 230
    }
  ]
}
```

#### 错误码

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

---

### /admin/appver/upload 上传安装包

`multipart/form-data` 上传 APK / IPA / 安装包到阿里云 OSS，返回访问 URL。后续配合 `/admin/appver/add` 落库。

#### 接口地址

```
POST /admin/appver/upload
Content-Type: multipart/form-data
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `Key` | string |  | 业务类型（form 字段） |
| `Files` | file[] | ✓ | 一个或多个文件（form 字段） |

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `urls` | []string | 上传成功的文件 URL，顺序与上传顺序一致 |
| `errs` | []string | 失败信息；单文件失败放本地化错误文案，多文件失败带索引 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "urls": [
      "https://oss.example.com/android/yino/app-1.2.3.apk"
    ],
    "errs": []
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 参数错误 | 未上传任何文件 |
| 500 | (内部 err) | OSS 配置加载失败 |

#### 备注

- 存储桶 / 域名取自 `manage` 配置里的 `storage_*_ali` 系列字段，存储路径形如 `<rootPath>/<SysName>/<filename>`，且强制小写
- 单文件失败不会让接口报错，只会把错误项放进 `errs`

---

### /admin/appver/latest 最新版本

返回 iOS 与 Android 当前最新可用版本的下载地址。后台直接复用 SDK 同名接口，不需要 token。

#### 接口地址

```
POST /admin/appver/latest
Content-Type: application/json
```

#### 请求参数

无 body。

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `ios` | string | iOS 最新版下载地址 |
| `android` | string | Android 最新版下载地址 |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "ios": "https://cdn.example.com/app/ios/1.2.3/app.ipa",
    "android": "https://cdn.example.com/app/android/1.2.3/app.apk"
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 版本不存在 | iOS 或 Android 没有任何 `status=1` 记录 |
| 500 | (内部 err) | DB 查询失败 |

---

## 附件配置

`/admin/attachcfg/...` 管理"什么 key 下允许传哪些类型、单文件最大多少 KB"的全局规则。配置以 JSON 字符串形式存在 Redis 里，前端拿来渲染上传策略。

附件配置 JSON 结构（用于 `load` 返回 / `save` 入参）：

| 字段 | 类型 | 说明 |
|---|---|---|
| `define.rootpath` | string | 存储根路径 |
| `define.mapsets` | []object | 扩展名到分类的映射 |
| `define.mapsets[].method` | string | 分类标识，如 `image` / `video` / `doc` |
| `define.mapsets[].exts` | []string | 该分类下的扩展名集合，例如 `["jpg","png","gif"]` |
| `operators` | []object | 业务 key 对应的上传规则 |
| `operators[].key` | string | 业务 key，例如 `chat` / `avatar` |
| `operators[].handlers` | []object | 该 key 允许的分类与体积限制 |
| `operators[].handlers[].method` | string | 分类标识，对应 `define.mapsets[].method` |
| `operators[].handlers[].maxkb` | int | 该分类的单文件最大 KB |

---

### /admin/attachcfg/load 读取附件配置

返回当前生效的完整附件配置 JSON 对象。

#### 接口地址

```
POST /admin/attachcfg/load
Content-Type: application/json
```

#### 请求参数

无 body。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "define": {
      "rootpath": "uploads",
      "mapsets": [
        { "method": "image", "exts": ["jpg", "jpeg", "png", "gif", "webp"] },
        { "method": "video", "exts": ["mp4", "mov"] },
        { "method": "doc",   "exts": ["pdf", "doc", "docx", "xlsx"] }
      ]
    },
    "operators": [
      {
        "key": "chat",
        "handlers": [
          { "method": "image", "maxkb": 10240 },
          { "method": "video", "maxkb": 51200 },
          { "method": "doc",   "maxkb": 20480 }
        ]
      },
      {
        "key": "avatar",
        "handlers": [
          { "method": "image", "maxkb": 2048 }
        ]
      }
    ]
  }
}
```

#### 错误码

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

#### 备注

- Redis 里没写过配置时返回 `data: null`

---

### /admin/attachcfg/save 保存附件配置

把整段配置 JSON 字符串覆盖写到 Redis。前端通常先 `load` 再修改后 `save`。

#### 接口地址

```
POST /admin/attachcfg/save
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `json` | string | ✓ | 完整附件配置 JSON 字符串，结构同 `/admin/attachcfg/load` 的 `data` |

#### 请求示例

```json
{
  "json": "{\"define\":{\"rootpath\":\"uploads\",\"mapsets\":[{\"method\":\"image\",\"exts\":[\"jpg\",\"png\"]}]},\"operators\":[{\"key\":\"chat\",\"handlers\":[{\"method\":\"image\",\"maxkb\":10240}]}]}"
}
```

#### 返回示例

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

#### 错误码

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

#### 备注

- 服务端不校验 JSON 合法性，传错了 `load` 时会解析失败返回 `null`

---

### /admin/attachcfg/details 类型与大小汇总

把配置压平成"按业务 key 列出该 key 允许的扩展名清单和单文件 KB 上限"，给客户端做选文件器/校验。与 SDK 接口 `/v1/sdk/attachcfg/details` 同名同实现。

#### 接口地址

```
POST /admin/attachcfg/details
Content-Type: application/json
```

#### 请求参数

无 body。

#### 返回参数

`data` 是 `map[string][]item`，key 是 operator key，item 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `exts` | []string | 该分类允许的扩展名集合 |
| `maxkb` | int | 该分类单文件最大 KB |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "chat": [
      { "exts": ["jpg", "jpeg", "png", "gif", "webp"], "maxkb": 10240 },
      { "exts": ["mp4", "mov"], "maxkb": 51200 },
      { "exts": ["pdf", "doc", "docx", "xlsx"], "maxkb": 20480 }
    ],
    "avatar": [
      { "exts": ["jpg", "jpeg", "png", "gif", "webp"], "maxkb": 2048 }
    ]
  }
}
```

#### 错误码

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

---

## IP 控制

`/admin/ipctrl/...` 维护 IP 白名单。一条规则按 `(appkey, uid)` 维度落到 Redis Hash 里：`uid=0` 表示该 appkey 全局生效，`uid>0` 表示只对这个用户生效。请求到达 `manage` 入口时会按"先取 header 透传的原始 IP，再用 `(appkey, uid, ip)` 查白名单"的顺序拦截。

公共字段：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `businessId` | string |  | 商户 id；传了会按商户取 `appkey`，否则用 token 自身的 `appkey` |
| `uid` | string |  | 字符串化的用户 uid；不传或传 `"0"` 都视作 appkey 全局 |

记录结构 `IPRecord`：

| 字段 | 类型 | 说明 |
|---|---|---|
| `ip` | string | IPv4 或 IPv6 字面量 |
| `remark` | string | 备注 |

---

### /admin/ipctrl/add 新增 IP

批量新增白名单 IP。`records` 为空时直接返回成功（用于"无变更也走一次接口"的场景）。

#### 接口地址

```
POST /admin/ipctrl/add
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `businessId` | string |  | 商户 id，见公共字段 |
| `uid` | string |  | 用户 uid，见公共字段 |
| `records` | []IPRecord |  | 要新增的 IP 列表 |

#### 请求示例

```json
{
  "businessId": "B20240601001",
  "uid": "10086002",
  "records": [
    { "ip": "203.0.113.10", "remark": "上海办公室出口" },
    { "ip": "2001:db8::1",  "remark": "IPv6 内网" }
  ]
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | IP[xxx]格式非法 | 不符合 IPv4/IPv6 |
| 400 | IP[xxx]已存在 | 该 `(appkey, uid, ip)` 已存在 |
| 400 | 操作失败 | Redis 写入返回 false |
| 500 | (内部 err) | 商户查询或 Redis 异常 |

---

### /admin/ipctrl/del 删除 IP

按 IP 列表批量删除白名单。`ips` 为空直接返回成功。

#### 接口地址

```
POST /admin/ipctrl/del
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `businessId` | string |  | 商户 id，见公共字段 |
| `uid` | string |  | 用户 uid，见公共字段 |
| `ips` | []string |  | 要删除的 IP 列表 |

#### 请求示例

```json
{
  "businessId": "B20240601001",
  "uid": "10086002",
  "ips": ["203.0.113.10", "2001:db8::1"]
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | IP[xxx]格式非法 | 不符合 IPv4/IPv6 |
| 400 | IP[xxx]不存在 | 该 IP 不在白名单 |
| 500 | (内部 err) | 商户查询或 Redis 异常 |

---

### /admin/ipctrl/edit 修改 IP

把旧 IP 替换为新 IP（同时可更新备注）。旧 IP 必须存在、新 IP 必须不存在。

#### 接口地址

```
POST /admin/ipctrl/edit
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `businessId` | string |  | 商户 id，见公共字段 |
| `uid` | string |  | 用户 uid，见公共字段 |
| `record_old` | IPRecord | ✓ | 旧记录，至少 `ip` 不能为空且必须存在 |
| `record_new` | IPRecord | ✓ | 新记录，`ip` 必须合法且尚未存在 |

#### 请求示例

```json
{
  "businessId": "B20240601001",
  "uid": "10086002",
  "record_old": { "ip": "203.0.113.10", "remark": "上海办公室出口" },
  "record_new": { "ip": "203.0.113.11", "remark": "上海办公室出口(新)" }
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | IP[xxx]格式非法 | 旧或新 IP 格式不符合 IPv4/IPv6 |
| 400 | IP[xxx]不存在 | 旧 IP 在白名单中找不到 |
| 400 | IP[xxx]已存在 | 新 IP 已经在白名单 |
| 400 | 操作失败 | Redis 更新返回 false |
| 500 | (内部 err) | 商户查询或 Redis 异常 |

---

### /admin/ipctrl/list IP 列表

按 `(appkey, uid)` 分页拉白名单。`businessId` 没匹配到商户时返回空（内部把 appkey 置为 `-`）。

#### 接口地址

```
POST /admin/ipctrl/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `businessId` | string |  | 商户 id |
| `uid` | string |  | 用户 uid；不传/`"0"` 表示 appkey 全局 |
| `pageSize` | int64 |  | 每页条数 |
| `pageIndex` | int64 |  | 页码，从 0 开始 |

#### 请求示例

```json
{
  "businessId": "B20240601001",
  "uid": "0",
  "pageSize": 20,
  "pageIndex": 0
}
```

#### 返回参数

`data` 是 `IPRecord` 数组，字段同上文公共说明。

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": [
    { "ip": "203.0.113.10", "remark": "上海办公室出口" },
    { "ip": "2001:db8::1",  "remark": "IPv6 内网" }
  ]
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 500 | (内部 err) | 商户查询或 Redis 异常 |

#### 备注

- Redis 内部用 Hash `Key_AllowIP{appkey,uid}` 存储，遍历 hash 后再按 `pageSize/pageIndex` 切片，顺序与 Redis hash 迭代顺序一致（不保证稳定）

---

## 杂项

### /admin/join/group/notice 开启/关闭新成员入群消息

按群维度切换"新人入群是否广播系统消息"。是远程鉴权接口（业务侧带签名校验）。

#### 接口地址

```
POST /admin/join/group/notice
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `tid` | string | ✓ | 群 id |
| `joinNotice` | int64 | ✓ | `0`=开启 `1`=关闭 |
| `appkey` | string |  | 群所属 appkey |
| `business_id` | string |  | 商户 id（写日志用） |
| `business_name` | string |  | 商户名称（写日志用） |

#### 请求示例

```json
{
  "tid": "g_184629134812372992",
  "joinNotice": 0,
  "appkey": "yino_prod",
  "business_id": "B20240601001",
  "business_name": "Yino 正式"
}
```

#### 返回示例

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

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | status非法 | `joinNotice` 不是 0 或 1 |
| 400 | 群不存在 | `(appkey, tid)` 查不到群 |
| 500 | (内部 err) | DB 查询或更新失败 |

---

### /admin/friend/list 管理端拉取好友列表

外部业务系统（通常是"福聊"之类）凭邀请码 + 签名换回某 uid 的好友列表，用于跨系统推荐 / 导入。请求需要 `sign = MD5("{code}|{uid}|fuliao")`。

#### 接口地址

```
POST /admin/friend/list
Content-Type: application/json
```

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `code` | string | ✓ | 邀请码 |
| `uid` | int64 | ✓ | 目标用户 uid |
| `sign` | string | ✓ | `MD5("{code}|{uid}|fuliao")` |
| `page` | int64 |  | 页码，从 0 开始 |
| `pageSize` | int64 |  | 每页条数，默认 20，最大 200 |
| `business_id` | string |  | 商户 id；不传时按邀请码反查 |
| `business_name` | string |  | 商户名称 |
| `appkey` | string |  | appkey |

#### 请求示例

```json
{
  "code": "INV-8K2X",
  "uid": 10086002,
  "sign": "e3b0c44298fc1c149afbf4c8996fb924",
  "page": 0,
  "pageSize": 50
}
```

#### 返回参数

| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int64 | 好友总数 |
| `friends` | []object | 当前页好友列表 |
| `friends[].uid` | int64 | 好友 uid |
| `friends[].name` | string | 好友昵称 |
| `friends[].avatar` | string | 好友头像 url |

#### 返回示例

```json
{
  "code": 0,
  "msg": "",
  "data": {
    "total": 2,
    "friends": [
      { "uid": 10086003, "name": "Alice", "avatar": "https://oss.example.com/a/avatar/3.png" },
      { "uid": 10086004, "name": "Bob",   "avatar": "https://oss.example.com/a/avatar/4.png" }
    ]
  }
}
```

#### 错误码

| code | msg 大意 | 触发条件 |
|---|---|---|
| 400 | 邀请码非法 | `code` 空 |
| 400 | 签名非法 | `sign` 与服务端算出的不一致 |
| 400 | 邀请码不存在 | DB 查不到邀请码或对应商户 |
| 500 | (内部 err) | DB 查询失败 |

#### 备注

- 当用户没有任何好友时返回 `data: {}`（`total=0`、`friends` 字段省略）
