错误码与状态码
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 |
同账号冲突 |
关闭其他页面 |
| 6004 |
群组不存在 |
切换群组 |
| 6005 |
服务器错误 |
重连 |
UDP 协议相关 (7xxx)
| 错误码 |
说明 |
详细信息 |
| 7001 |
认证失败 |
检查用户名和密码 |
| 7002 |
用户不存在 |
检查用户名 |
| 7003 |
用户已禁用 |
联系管理员 |
| 7004 |
用户未审核 |
等待审核 |
| 7005 |
无效设备型号 |
检查DevModel |
| 7006 |
同平台在线冲突 |
等待旧连接断开 |
| 7007 |
IP被封禁 |
等待解封 |
JWT 认证响应状态码
Type=1 (JWTAuth) DATA[0] 状态码
| 值 |
说明 |
| 0 |
认证成功 |
| 1 |
Token无效或过期 |
| 2 |
用户不存在 |
| 3 |
用户已禁用 |
| 4 |
用户未审核 |
| 5 |
无效设备型号(非101-104) |
| 6 |
同平台已有在线幽灵设备 |
响应格式
DATA[0] = 状态码 (0=成功, 非0=失败)
DATA[1:] = 错误消息 (UTF-8, 失败时)
心跳响应状态码
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次 |
业务状态码
用户审核状态
| 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 4003:
showToast('同账号冲突,请关闭其他页面', 'warning');
break;
case 1006:
showToast('连接异常断开,正在重连...', 'info');
reconnect();
break;
}
};