跳转至

错误码与状态码

HTTP 状态码

标准状态码

状态码 说明 使用场景
200 OK 请求成功
201 Created 资源创建成功
400 Bad Request 请求参数错误
401 Unauthorized 未认证或token无效
403 Forbidden 无权限访问
404 Not Found 资源不存在
409 Conflict 资源冲突(如重复绑定)
422 Unprocessable Entity 请求格式正确但语义错误
429 Too Many Requests 请求频率超限
500 Internal Server Error 服务器内部错误

统一响应格式

{
  "code": 200,
  "message": "成功",
  "data": {}
}

错误响应格式

{
  "code": 400,
  "message": "请求参数错误",
  "data": {
    "field": "username",
    "detail": "用户名长度不能少于3个字符"
  }
}

业务错误码

认证相关 (1xxx)

错误码 说明 详细信息
1001 用户名或密码错误 请检查用户名和密码
1002 账号已被禁用 联系管理员
1003 账号未审核 等待管理员审核
1004 账号已被拒绝 查看拒绝原因
1005 Token已过期 请重新登录
1006 Token无效 请重新登录
1007 Refresh Token无效 请重新登录
1008 验证码错误 请重新输入
1009 验证码已过期 请重新获取
1010 邮箱已被注册 使用其他邮箱
1011 用户名已被占用 使用其他用户名
1012 呼号已被注册 联系管理员
1013 SSO认证失败 重试或联系管理员
1014 SSO账号已绑定 解绑后重试

设备相关 (2xxx)

错误码 说明 详细信息
2001 设备不存在 检查设备ID
2002 设备已绑定 不能重复绑定
2003 动态码无效 请重新获取
2004 动态码已过期 请重新获取
2005 SSID不可用 选择其他SSID
2006 SSID已被占用 选择其他SSID
2007 设备型号不支持 检查设备型号
2008 MAC地址无效 检查MAC格式
2009 设备密码错误 检查设备密码
2010 设备已被禁用 联系管理员
2011 配置同步失败 设备可能离线
2012 固件版本无效 使用semver格式

群组相关 (3xxx)

错误码 说明 详细信息
3001 群组不存在 检查群组ID
3002 群组密码错误 输入正确密码
3003 已在群组中 不能重复加入
3004 不在群组中 先加入群组
3005 无权操作群组 需要群主或管理员权限
3006 群组已满 联系管理员
3007 不能加入虚拟互联组 仅管理员可操作

用户相关 (4xxx)

错误码 说明 详细信息
4001 用户不存在 检查用户ID
4002 无权修改用户 需要管理员或本人权限
4003 密码强度不足 至少8位,包含字母和数字
4004 旧密码错误 输入正确旧密码
4005 操作证已提交 等待审核
4006 文件上传失败 检查文件大小和格式

通信相关 (5xxx)

错误码 说明 详细信息
5001 通信记录不存在 检查记录ID
5002 音频文件不存在 可能已删除
5003 通联日志不存在 检查日志ID
5004 录制未启用 联系管理员开启

WebSocket 相关 (6xxx)

错误码 说明 详细信息
6001 认证失败 请重新登录
6002 Token过期 刷新token
6003 幽灵能力不完整 必须提供实例 ID 并声明多收和来源频道能力
6004 群组不存在 切换群组
6005 服务器错误 重连

UDP 协议相关 (7xxx)

错误码 说明 详细信息
7001 认证失败 检查用户名和密码
7002 用户不存在 检查用户名
7003 用户已禁用 联系管理员
7004 用户未审核 等待审核
7005 无效设备型号 检查DevModel
7006 幽灵协议版本不支持 使用版本化 JSON 认证和 Session tag
7007 IP被封禁 等待解封

JWT 认证响应状态码

Type=1 (JWTAuth) DATA[0] 状态码

值 说明
0 认证成功
1 Token无效或过期
2 用户不存在
3 用户已禁用
4 用户未审核
5 无效设备型号(非101-104)

响应格式

DATA[0] = 状态码 (0=成功, 非0=失败)
DATA[1:] = 错误消息 (UTF-8, 失败时)

UDP 幽灵认证成功以 DATA[0] = 0 开头,后续 DATA 为版本化 JSON,Reserved 同时携带 session_tag:

{
  "version": 1,
  "session_id": "7fbbbd37-9207-4df2-b79d-e400e0a09fd0",
  "session_tag": 270544961,
  "client_instance_id": "41a065d7-f9e1-4785-bbcb-22c3ca8784ad",
  "tx_group_id": 1001,
  "rx_group_ids": [1001, 1002]
}

旧 raw-JWT 或无版本化 JSON 的客户端返回 ghost_protocol_upgrade_required;缺少必需能力返回 ghost_capabilities_required。账号达到会话上限或 Session 注册失败时也复用状态 1,并在 DATA[1:] 返回机器可读原因;达到上限时格式为 ghost_session_limit active=<当前数> limit=<上限>。客户端不得把所有状态 1 都显示成“Token 过期”。

幽灵 Session 与路由错误

PUT /api/radio/sessions/:session_id/routing 的响应除 HTTP code 外,还可能返回稳定的 error 标识:

HTTP error 含义 客户端处理
400 invalid_routing 发送频道无效,或接收集合含非正整数 修正请求后重试
403 group_forbidden 至少一个频道当前不可访问 移除无权限频道
404 session_not_found Session 不存在或不属于当前账号 刷新会话列表并重新连接
404 group_not_found 至少一个频道不存在 刷新频道列表
409 ptt_active 当前 Session 仍处于半双工租约内 等待约 900ms 后重试切换发送频道
422 subscription_limit 规范化后超过 GhostSessions.MaxSubscriptionsPerSession 根据响应中的 requested 与 limit 减少接收频道
500 routing_update_failed 持久化或运行时投影失败 保留旧路由并稍后重试

DELETE /api/radio/sessions/:session_id 对不存在、已断开或不属于当前账号的 Session 返回 404 session_not_found。路由更新是原子的:失败时服务端应继续使用更新前的完整路由。

WebSocket 握手与关闭

协议升级前可能返回以下纯文本 HTTP 错误:

HTTP 响应文本 含义
401 token_required 缺少 ws_token Cookie
401 invalid_token / user_not_found / user_disabled / user_not_approved 账号认证失败
401 ghost_protocol_upgrade_required 未提供现代幽灵实例协议参数
401 invalid_client_instance_id 实例 ID 不是有效 UUID
401 ghost_capabilities_required 缺少 multi_receive_v1 或 source_group_v1
401 client_preference_unavailable 无法加载实例路由偏好

Session 注册在 WebSocket 升级后失败时,服务端使用标准关闭码 1008,关闭原因是稳定错误码,例如 ghost_session_limit。客户端应展示原因并停止无休止重连。

频道消息 API 查询保护

HTTP error 含义 客户端处理
400 invalid_message_limit limit 超出部署配置范围 使用服务端允许的页大小
429 message_api_user_rate_limited 当前账号一分钟查询次数超限 按 Retry-After 等待
429 message_api_ip_rate_limited 当前 IP 一分钟查询次数超限 按 Retry-After 等待
503 message_api_busy 消息查询并发槽已满 至少等待 Retry-After 后重试

心跳响应状态码

Type=2 (Heartbeat) 响应

普通设备心跳认证成功后,服务器会在响应中填充 CallSign。

成功响应:

Header:
  CallSign: "BG1AAA" (服务器填充)

DATA: 原样返回设备发送的DATA

失败响应: 服务器直接丢弃心跳包,不返回响应。

设备认证失败处理

阶梯封禁机制

失败次数    封禁时长    封禁Key
1-3        无          -
4          10秒        IP + Username
5          30秒        IP + Username
6          60秒        IP + Username
7+         300秒       IP + Username

封禁响应

被封禁的设备发送心跳包时,服务器直接丢弃,不返回响应。

设备可通过以下方式判断是否被封禁: 1. 发送心跳后长时间无响应 2. 尝试其他用户名(如果封禁是基于IP+Username)

HTTP 限流响应

429 Too Many Requests

{
  "code": 429,
  "message": "请求过于频繁,请稍后重试",
  "data": {
    "retry_after": 9
  }
}

限流规则

接口 限流策略
/api/auth/login IP每秒5次
/api/auth/send-code IP每分钟2次
/api/auth/register IP每小时5次
/api/device/pre-check IP每秒1次,MAC每分钟5次
/api/device/request-code IP每10秒1次,MAC每分钟1次
/api/device/confirm-bind MAC每5秒1次
/api/device/bind 用户每分钟5次
/api/device/submit-config 用户每分钟10次
/api/public/relays IP每分钟10次
/api/public/client-resources/manifest 与 /api/public/client-resources/artifacts/:artifact_id/download 共享 IP 每分钟30次

业务状态码

用户审核状态

approval_status 说明
0 待审核
1 已通过
2 已拒绝

设备在线状态

is_online 说明
0 离线
1 在线

通信记录状态

status 说明
0 录制中
1 待上传
2 已完成
3 上传失败

操作证审核状态

status 说明
0 待审核
1 已通过
2 已拒绝

错误处理最佳实践

客户端错误处理

async function apiRequest(url, options) {
  try {
    const response = await fetch(url, options);
    const data = await response.json();

    if (response.ok) {
      return data;
    }

    switch (response.status) {
      case 401:
        // Token过期,尝试刷新
        const refreshed = await refreshToken();
        if (refreshed) {
          // 重试原请求
          return apiRequest(url, options);
        }
        // 刷新失败,跳转登录
        window.location.href = '/login';
        break;

      case 403:
        showToast('无权限访问', 'error');
        break;

      case 404:
        showToast('资源不存在', 'error');
        break;

      case 429:
        const retryAfter = data.data?.retry_after || 5;
        showToast(`请求过于频繁,${retryAfter}秒后重试`, 'warning');
        await sleep(retryAfter * 1000);
        return apiRequest(url, options);

      default:
        showToast(data.message || '请求失败', 'error');
    }

    throw new Error(data.message);
  } catch (error) {
    if (error.name === 'TypeError') {
      showToast('网络错误,请检查连接', 'error');
    }
    throw error;
  }
}

WebSocket 错误处理

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
  showToast('连接错误', 'error');
};

ws.onclose = (event) => {
  switch (event.code) {
    case 4001:
      showToast('认证失败', 'error');
      break;
    case 1008:
      showToast(event.reason || '当前会话不符合服务端策略', 'warning');
      break;
    case 1006:
      showToast('连接异常断开,正在重连...', 'info');
      reconnect();
      break;
  }
};