You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
332 lines
13 KiB
332 lines
13 KiB
|
3 months ago
|
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> T executeWithRetry(RetryableTask<T> 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> {
|
||
|
|
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("<!doctype") || body.startsWith("<html")
|
||
|
|
|| (body.contains("<form") && body.contains("password"));
|
||
|
|
}
|
||
|
|
|
||
|
|
private String getAuthorizationCookie(HttpURLConnection conn) {
|
||
|
|
for (Map.Entry<String, List<String>> 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<String> 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<channel, phoneNumber>
|
||
|
|
*/
|
||
|
|
public Map<String, String> 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<String, String> 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<>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|