package com.threecloud.dataserviceyy.util; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.Map; /** * VAA录音盒HTTP工具类 * 用于与录音盒设备进行HTTP通信 * 参考文档: ebox_developer_guide.html (EBOX-8108 电话录音仪开发手册) */ @Component public class VaaHttpUtil { private static final Logger logger = LoggerFactory.getLogger(VaaHttpUtil.class); /** 连接超时:30秒,设备可能在远端网络 */ private static final int CONNECT_TIMEOUT = 30000; /** API读取超时:60秒 */ private static final int READ_TIMEOUT = 60000; /** 文件下载读取超时:5分钟,音频文件可能较大 */ private static final int DOWNLOAD_READ_TIMEOUT = 300000; /** 重试次数 */ private static final int MAX_RETRY = 3; /** 重试间隔基数(毫秒) */ private static final int RETRY_BASE_DELAY = 2000; /** * HTTP登录认证 * @param loginUrl 登录URL,例如:http://192.168.1.100/authorize?username=admin&password=admin123 * @return 登录成功后返回Authorization Cookie值 */ public String httpLogin(String loginUrl) throws Exception { return executeWithRetry(() -> { logger.debug("正在登录录音盒: {}", loginUrl); HttpURLConnection conn = null; try { URL url = new URL(loginUrl); conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(false); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.setRequestMethod("GET"); int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { String authorization = getAuthorizationCookie(conn); if (authorization == null || authorization.isEmpty()) { throw new RuntimeException("登录失败,录音盒未返回Authorization Cookie"); } logger.info("录音盒登录成功"); return authorization; } throw new RuntimeException("登录失败,HTTP状态码: " + responseCode); } finally { if (conn != null) { conn.disconnect(); } } }, "登录"); } /** * HTTP访问API获取数据 * @param apiUrl API地址,例如:http://192.168.1.100/service/running/channel * @param authorization 登录后返回的Authorization Cookie值 * @return 返回JSON字符串 */ public String httpVisit(String apiUrl, String authorization) throws Exception { return executeWithRetry(() -> { logger.debug("访问API: {}", apiUrl); HttpURLConnection conn = null; try { URL url = new URL(apiUrl); conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.setRequestMethod("GET"); addAuthorization(conn, authorization); int responseCode = conn.getResponseCode(); if (responseCode == 200) { BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8") ); StringBuilder result = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { result.append(line); } reader.close(); String jsonResult = result.toString(); logger.debug("API响应: {}", jsonResult); if (isLoginPage(jsonResult)) { throw new RuntimeException("录音盒认证失效,返回登录页面"); } return jsonResult; } else { throw new RuntimeException("API访问失败,HTTP状态码: " + responseCode); } } finally { if (conn != null) { conn.disconnect(); } } }, "API访问"); } /** * 下载录音文件(带重试) * @param fileUrl 文件URL,例如:http://192.168.1.100/record/2026/05/20/OUT-xxx.wav * @param savePath 保存路径 * @param authorization 登录后返回的Authorization Cookie值 */ public void httpDown(String fileUrl, String savePath, String authorization) throws Exception { executeWithRetry(() -> { logger.info("开始下载录音文件: {} -> {}", fileUrl, savePath); // 确保目录存在 File saveFile = new File(savePath); if (!saveFile.getParentFile().exists()) { saveFile.getParentFile().mkdirs(); } HttpURLConnection conn = null; InputStream inputStream = null; FileOutputStream outputStream = null; try { URL url = new URL(fileUrl); conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(DOWNLOAD_READ_TIMEOUT); conn.setRequestMethod("GET"); addAuthorization(conn, authorization); int responseCode = conn.getResponseCode(); if (responseCode == 200) { inputStream = conn.getInputStream(); outputStream = new FileOutputStream(savePath); byte[] buffer = new byte[8192]; int bytesRead; long totalBytes = 0; long lastLogTime = System.currentTimeMillis(); while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); totalBytes += bytesRead; // 每10秒打印一次进度 long now = System.currentTimeMillis(); if (now - lastLogTime > 10000) { logger.info("下载进度: {} MB", totalBytes / 1024 / 1024); lastLogTime = now; } } outputStream.flush(); logger.info("录音文件下载完成: {}, 大小: {} bytes ({} MB)", savePath, totalBytes, totalBytes / 1024 / 1024); } else { throw new RuntimeException("文件下载失败,HTTP状态码: " + responseCode); } } finally { closeQuietly(inputStream); closeQuietly(outputStream); if (conn != null) { conn.disconnect(); } } return null; }, "文件下载"); } /** * 带重试的执行器 */ private T executeWithRetry(RetryableTask task, String operationName) throws Exception { Exception lastException = null; for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { try { return task.execute(); } catch (java.net.SocketTimeoutException e) { lastException = e; if (attempt < MAX_RETRY) { long delay = RETRY_BASE_DELAY * attempt; logger.warn("{}(第{}次)超时,{}ms后重试: {}", operationName, attempt, delay, e.getMessage()); Thread.sleep(delay); } } catch (java.net.ConnectException e) { lastException = e; if (attempt < MAX_RETRY) { long delay = RETRY_BASE_DELAY * attempt; logger.warn("{}(第{}次)连接失败,{}ms后重试: {}", operationName, attempt, delay, e.getMessage()); Thread.sleep(delay); } } catch (IOException e) { lastException = e; if (attempt < MAX_RETRY && isRetryable(e)) { long delay = RETRY_BASE_DELAY * attempt; logger.warn("{}(第{}次)IO异常,{}ms后重试: {}", operationName, attempt, delay, e.getMessage()); Thread.sleep(delay); } else { throw e; } } } throw new RuntimeException(operationName + "失败,已重试" + MAX_RETRY + "次", lastException); } private boolean isRetryable(IOException e) { String msg = e.getMessage(); if (msg == null) return false; return msg.contains("timed out") || msg.contains("connection") || msg.contains("reset"); } @FunctionalInterface private interface RetryableTask { T execute() throws Exception; } private void closeQuietly(Closeable c) { if (c != null) { try { c.close(); } catch (IOException ignored) {} } } private void addAuthorization(HttpURLConnection conn, String authorization) { if (authorization != null && !authorization.isEmpty()) { conn.setRequestProperty("Cookie", "Authorization=" + authorization); } } private boolean isLoginPage(String responseBody) { if (responseBody == null) { return false; } String body = responseBody.trim().toLowerCase(); return body.startsWith("> entry : conn.getHeaderFields().entrySet()) { if (entry.getKey() == null || !"Set-Cookie".equalsIgnoreCase(entry.getKey())) { continue; } String authorization = getAuthorizationValue(entry.getValue()); if (authorization != null && !authorization.isEmpty()) { return authorization; } } return null; } private String getAuthorizationValue(List cookies) { if (cookies == null || cookies.isEmpty()) { return null; } for (String cookie : cookies) { String prefix = "Authorization="; int start = cookie.indexOf(prefix); if (start < 0) { continue; } start += prefix.length(); int end = cookie.indexOf(';', start); return end >= 0 ? cookie.substring(start, end) : cookie.substring(start); } return null; } /** * 解析通道状态JSON */ public JSONArray parseChannelData(String jsonData) { if (jsonData == null || jsonData.isEmpty() || "[]".equals(jsonData)) { return new JSONArray(); } return JSON.parseArray(jsonData); } /** * 解析录音记录JSON */ public JSONArray parseRecordData(String jsonData) { if (jsonData == null || jsonData.isEmpty() || "[]".equals(jsonData)) { return new JSONArray(); } return JSON.parseArray(jsonData); } /** * 获取分机号码配置 * EBOX接口: GET /service/ext/number * 返回每条线路的分机号码,如: {"1":"8001","2":"8002",...} * @param extUrl 接口地址,例如:http://192.168.1.100/service/ext/number * @param authorization 登录后返回的Authorization Cookie值 * @return 通道-分机号映射 Map */ public Map getExtensionNumbers(String extUrl, String authorization) throws Exception { String jsonData = httpVisit(extUrl, authorization); if (jsonData == null || jsonData.isEmpty() || "[]".equals(jsonData) || "{}".equals(jsonData)) { return new java.util.HashMap<>(); } try { // EBOX返回的是JSON对象,key是通道号(1-8),value是分机号码 com.alibaba.fastjson2.JSONObject jsonObj = JSON.parseObject(jsonData); Map result = new java.util.HashMap<>(); for (String key : jsonObj.keySet()) { String value = jsonObj.getString(key); result.put(key, value != null ? value : ""); } logger.info("获取到分机号码配置: {} 条", result.size()); return result; } catch (Exception e) { logger.warn("解析分机号码配置失败: {}", e.getMessage()); return new java.util.HashMap<>(); } } }