3. 响应格式与错误处理
V5 API 的所有调用均返回 类型 DTO 或 boolean/long。失败时将返回如下安全默认值。
3.1 V5 响应模式
| 方法模式 | 成功返回 | 失败返回 |
|---|---|---|
create(req) / update(id, req) / get(id) | *ResponseV5 实例 | null |
list() 系列 | List<*ResponseV5> | 空列表 (绝对非 null) |
delete(id) | true | false |
exists(id) | true (存在) | false (不存在或错误) |
count() 系列 | long (≥0) | 0 |
Order 生命周期 (start/end/pause/resume/abort) | true (成功) | false (无法转移等) |
不会抛出异常。 HTTP 错误、服务器 ERROR、JSON 解析失败将全部被上述表格中的安全默认值吸收。错误原因可通过调试日志或 envelope 的
_code/_message确认。
3.2 内部 wire format (参考)
V5 服务器以如下 envelope 形式响应。由于客户端库会自动解析,一般使用时无需关注。
// 단건 성공
{ "_status": "OK", "data": { ... } }
// 목록 성공
{ "_status": "OK", "data": [ { ... }, { ... } ] }
// 목록 — items 키 안에 배열
{ "_status": "OK", "data": { "items": [ ... ] } }
// 에러
{
"_status": "ERROR",
"_code": "E1002",
"_message": "validation failed: ...",
"_http_status": 400
}
3.3 V5 ErrorCode 映射
当服务器发送 ERROR 响应时,_code 字段中包含以下代码。权威定义为产品的 plantpulse.core.exception.ErrorCode,且 HTTP 状态由该枚举值共同决定 —— 代码与状态为配对关系,仅根据其中之一进行分支处理即可。
| 代码 | HTTP | 名称 | 常见原因 |
|---|---|---|---|
E1000 | 400 | INVALID_REQUEST | 语法错误、类型不匹配 |
E1001 | 400 | MISSING_PARAMETER | 缺失必要参数 |
E1002 | 422 | VALIDATION_FAILED | 违反值范围·格式·域规则 (如 ID 前缀等) |
E1100 | 401 | UNAUTHORIZED | api_key 缺失或无效 |
E1101 | 429 | AUTH_TOO_MANY_ATTEMPTS | 同一 IP 认证失败累积 — 参见 Retry-After |
E1102 | 401 | TOKEN_EXPIRED | 令牌过期 — 可通过重新发行恢复 |
E1103 | 401 | TOKEN_REVOKED | 令牌撤回 — 同一令牌无法恢复 |
E1104 | 401 | TOKEN_IP_MISMATCH | 令牌绑定的 IP 与调用 IP 不一致 |
E1200 | 403 | FORBIDDEN | 已认证但无权限 |
E1300 | 404 | NOT_FOUND | 该 ID 不存在 |
E1400 | 405 | METHOD_NOT_ALLOWED | 不允许的方法 |
E1500 | 409 | CONFLICT | 资源冲突 |
E1501 | 409 | DUPLICATE_ENTITY | 实体已存在 |
E1600 | 413 | PAYLOAD_TOO_LARGE | 请求正文过大 |
E1700 | 415 | UNSUPPORTED_MEDIA_TYPE | 不支持的媒体类型 |
E1800 | 422 | IDEMPOTENCY_CONFLICT | 相同 Idempotency-Key + 不同 body |
E1801 | 409 | IDEMPOTENCY_IN_PROGRESS | 相同键处理中 — 稍后重试 |
E1900 | 429 | RATE_LIMIT_EXCEEDED | 超过速率限制 |
E5000 | 500 | INTERNAL_ERROR | 服务器内部错误 |
E5100 | 500 | SERVICE_ERROR | 服务处理错误 |
E5200 | 500 | DAO_ERROR | 数据访问错误 |
E5300 | 500 | ENGINE_ERROR | 引擎处理错误 |
E5400 | 502 | EXTERNAL_SERVICE_ERROR | 外部系统调用失败 |
E5500 | 503 | SERVICE_UNAVAILABLE | 服务不可用 — 非判定结果,而是「无法执行」。 参见 Retry-After |
E5503 | 503 | SHUTTING_DOWN | 服务器关闭中 — 切换至其他节点或等待 Retry-After 时长 |
E5600 | 504 | TIMEOUT | 服务器侧请求超时 |
E1900 (速率限制) 通过 降低速度 可解除。E1101 (认证尝试过多) 通过降低速度无法解除 — 凭据本身错误,且该 IP 将被锁定直至窗口关闭。若将两者绑定在相同的退避策略中,后者将陷入永久重试。
400 表示请求无法被 «解析》,422 表示已解析但 «内容» 不符合规则 (自 2026-08-31 起)。通过状态码区分应修改格式还是修改数值 — 因为人类阅读消息,但代理(Agent)根据状态码决定下次尝试方案。
常见拒绝原因
| HTTP | reason | 含义 |
|---|---|---|
| 422 | asset_statement.eql.missing_columns | 语句 EQL 未提供该种类的必要项 — 设备语句 |
| 422 | asset_statement.eql.unknown_type | 不存在该语句种类 |
| 422 | alarm.invalid.alarm_config_id | 创建报警设置时输入了自定义 ID。请通过 GET /alarm/config/new-id 接收 |
| 409 | — | 试图在同一标签上创建第二个频段报警设置。每个标签仅限一个 |
3.4 错误信息提取
V5 服务方法不直接公开 envelope,因此可通过以下两种方式确认错误详情。
方法 1 — 调试模式
APIClient_V5 client = new APIClient_V5(proto, host, port, user, token, true);
client.connect();
SiteResponseV5 result = client.site().create(req);
if (result == null) {
// 콘솔 로그에서 envelope 확인 가능
System.err.println("등록 실패 — 디버그 로그 확인");
}
控制台输出:
[V5] POST /api/v5/site body={...}
[V5] response: 400 {"_status":"ERROR","_code":"E1002","_message":"site_id must start with SITE_"}
方法 2 — 使用 BaseServiceV5 静态方法 (自定义扩展)
BaseServiceV5 的辅助工具从 JSONObject 响应中提取错误信息。适用于在扩展库以创建捕捉 envelope 的自定义服务时。
import plantpulse.api.v5.service.BaseServiceV5;
import plantpulse.json.JSONObject;
// 환경 (envelope) JSONObject 가 있다면
String code = BaseServiceV5.getErrorCode(env); // 예: "E1002"
String message = BaseServiceV5.getErrorMessage(env); // 예: "validation failed: ..."
int httpStatus = BaseServiceV5.getHttpStatus(env); // 예: 400
3.5 安全调用模式
CRUD 调用
import plantpulse.api.v5.dto.request.SiteRequestV5;
import plantpulse.api.v5.dto.response.SiteResponseV5;
SiteRequestV5 req = new SiteRequestV5();
req.setSite_id("SITE_DJ");
req.setSite_name("대전공장");
SiteResponseV5 created = client.site().create(req);
if (created == null) {
// 실패 처리 — 예: 알람, 재시도, 다른 경로 시도
log.warn("사이트 생성 실패");
return;
}
System.out.println("등록 완료: " + created.getSite_id());
列表调用
List<SiteResponseV5> sites = client.site().list();
// 실패해도 null 이 아님 — 그대로 순회 가능
for (SiteResponseV5 s : sites) {
System.out.println(s.getSite_id());
}
if (sites.isEmpty()) {
log.info("등록된 사이트 없음");
}
存在确认 → 查询
if (client.site().exists("SITE_DJ")) {
SiteResponseV5 site = client.site().get("SITE_DJ");
// ...
}
Order 生命周期 (返回 boolean)
String orderId = "ORDER_000001";
if (!client.order().start(orderId)) {
// 전이 불가 (예: 이미 START 상태)
log.warn("작업 시작 실패 — 현재 상태 확인");
return;
}
// 작업 진행 ...
if (!client.order().end(orderId)) {
log.warn("작업 종료 실패");
}
3.6 重试模式
V5 不执行自动重试。如需应对临时性网络错误,请自行实现重试逻辑。
import java.util.function.Supplier;
public static <T> T callWithRetry(Supplier<T> apiCall, int maxRetries, long baseDelayMs) {
int attempt = 0;
while (true) {
T result = apiCall.get();
if (result != null) return result; // 성공
if (++attempt >= maxRetries) return null; // 포기
try {
Thread.sleep(baseDelayMs * attempt); // 지수 백오프
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return null;
}
}
}
// 사용
SiteResponseV5 site = callWithRetry(
() -> client.site().get("SITE_DJ"),
3,
1_000L);
503 · 429 与 Retry-After
因重启或过载导致当前无法接收请求时,会随附 Retry-After (秒级整数)。Java 客户端将遵守此标头,但默认等待时间不超过 30 秒。直接调用时请遵循以下规则:
| 场景 | 响应 |
|---|---|
| Web 层关闭 (重启 · 重新部署) — 代理响应 | 503 E5500, Retry-After: 15 |
| 服务器关闭进行中 | 503 E5503, Retry-After: 15 |
| 后端存储临时不可用 (DB 重启等) | 503 E5500, Retry-After: 15 |
| 每个令牌请求额度超限 | 429 E1900, Retry-After 计算额度 |
| 流并发数超限 | 429 E1900 或 503 E5500, Retry-After |
- 读取操作:等待相应时长后再次调用。
- 写入操作:使用 相同
Idempotency-Key再次调用。因重启而中断的请求可能已被执行。 - 500 为故障,502
E5400为外部调用失败。两者均无Retry-After。
3.7 空响应与 null 响应区分
在 V5 中,null 或 false 同时包含以下两种含义:
- 服务器无数据 (
NOT_FOUND,E1300) - 调用失败 (网络错误、验证失败等)
如需区分,请配合使用 exists()。
if (!client.site().exists("SITE_DJ")) {
log.info("사이트가 등록되어 있지 않습니다");
// 신규 등록 로직
} else {
SiteResponseV5 site = client.site().get("SITE_DJ");
if (site == null) {
log.error("사이트는 존재하지만 조회 실패 — 일시적 오류 가능");
}
}