错误码与状态码
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 |
已拒绝 |
设备在线状态
通信记录状态
| 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;
}
};