|
|
|
@ -13,114 +13,49 @@ import org.springframework.util.MultiValueMap; |
|
|
|
import org.springframework.web.client.RestTemplate; |
|
|
|
|
|
|
|
import javax.annotation.PostConstruct; |
|
|
|
import javax.net.ssl.HttpsURLConnection; |
|
|
|
import javax.net.ssl.*; |
|
|
|
import java.io.IOException; |
|
|
|
import java.net.HttpURLConnection; |
|
|
|
import java.security.cert.X509Certificate; |
|
|
|
|
|
|
|
/** |
|
|
|
* 文件上传工具类 |
|
|
|
* 负责上传录音文件到 OSS,并返回完整的访问 URL |
|
|
|
* |
|
|
|
* 【配置说明】 |
|
|
|
* vaa-sync.oss.base-url: OSS服务基础地址,用于拼接完整URL |
|
|
|
* vaa-sync.oss.upload-url: OSS上传接口地址 |
|
|
|
* vaa-sync.oss.appcode/appid/appsecret: OSS认证信息 |
|
|
|
* 【接口】POST /oss/upload (multipart: file + path) |
|
|
|
* 【响应】{fileName, fileUrl, fileKey, fileSize, fileType} |
|
|
|
*/ |
|
|
|
@Component |
|
|
|
public class FileUploadUtil { |
|
|
|
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(FileUploadUtil.class); |
|
|
|
|
|
|
|
// OSS配置(从配置文件读取)
|
|
|
|
@Value("${ftp-sync.oss.base-url:http://53.1.194.59:9090}") |
|
|
|
@Value("${ftp-sync.oss.base-url:http://53.1.211.7/apiOss}") |
|
|
|
private String ossBaseUrl; |
|
|
|
|
|
|
|
@Value("${ftp-sync.oss.upload-url:http://53.1.194.59:9090/apiOss/oss/fileUpload}") |
|
|
|
private String ossUploadUrl; |
|
|
|
|
|
|
|
@Value("${ftp-sync.oss.appcode:dataservice-yy}") |
|
|
|
private String ossAppcode; |
|
|
|
|
|
|
|
@Value("${ftp-sync.oss.appid:371a3368-e28e-4ba3-95a3-c31c19cf0ad0}") |
|
|
|
private String ossAppid; |
|
|
|
|
|
|
|
@Value("${ftp-sync.oss.appsecret:06a6a80e-f9d2-4b3b-acc0-8d182c876074}") |
|
|
|
private String ossAppsecret; |
|
|
|
|
|
|
|
private RestTemplate restTemplate; |
|
|
|
|
|
|
|
@PostConstruct |
|
|
|
public void init() { |
|
|
|
restTemplate = createRestTemplate(); |
|
|
|
logger.info("文件上传工具初始化完成"); |
|
|
|
logger.info(" OSS基础地址: {}", ossBaseUrl); |
|
|
|
logger.info(" 上传接口: {}", ossUploadUrl); |
|
|
|
logger.info("文件上传工具初始化完成, OSS基础地址: {}", ossBaseUrl); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 上传WAV录音文件到OSS |
|
|
|
* 上传录音文件到OSS |
|
|
|
* |
|
|
|
* 存储路径格式: voice/{cityName}/{cityCode}/{date}/{filename} |
|
|
|
* 示例: voice/合肥/340100/20240101/IN-xxx.wav |
|
|
|
* 存储路径格式: voice/{ossPath} |
|
|
|
* 示例: voice/340400/20240101/12345_67890.mp3 |
|
|
|
* |
|
|
|
* 返回完整URL格式: {ossBaseUrl}/voice/{cityName}/{cityCode}/{date}/{filename} |
|
|
|
* 示例: http://53.1.194.59:9090/voice/合肥/340100/20240101/IN-xxx.wav
|
|
|
|
* |
|
|
|
* @param cityName 地市名称(如 合肥) |
|
|
|
* @param ossPath OSS存储路径(格式: cityCode/date/filename) |
|
|
|
* @param wavFileName WAV文件名 |
|
|
|
* @param wavData 文件字节数组 |
|
|
|
* @param fileName 文件名 |
|
|
|
* @param fileData 文件字节数组 |
|
|
|
* @return 完整的文件访问URL |
|
|
|
*/ |
|
|
|
public String uploadWav(String cityName, String ossPath, String wavFileName, byte[] wavData) { |
|
|
|
// 构建OSS对象名: voice/{cityName}/{cityCode}/{date}/{filename}
|
|
|
|
String objectName = "voice/" + cityName + "/" + ossPath; |
|
|
|
logger.debug("构建OSS对象名: {}", objectName); |
|
|
|
|
|
|
|
// 上传文件
|
|
|
|
String relativeUrl = uploadFile(objectName, wavData); |
|
|
|
public String uploadWav(String ossPath, String fileName, byte[] fileData) { |
|
|
|
String path = "voice/" + ossPath; |
|
|
|
logger.debug("上传文件到OSS, path={}, 大小={} bytes", path, fileData.length); |
|
|
|
|
|
|
|
// 返回完整URL
|
|
|
|
String fullUrl = buildFullUrl(relativeUrl); |
|
|
|
logger.info("文件上传完成,完整URL: {}", fullUrl); |
|
|
|
|
|
|
|
return fullUrl; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 构建完整的文件访问URL |
|
|
|
* |
|
|
|
* 如果返回的是相对路径,拼接 base-url |
|
|
|
* 如果返回的已经是完整URL,直接返回 |
|
|
|
* |
|
|
|
* @param url 从OSS返回的URL(可能是相对路径或完整URL) |
|
|
|
* @return 完整的访问URL |
|
|
|
*/ |
|
|
|
private String buildFullUrl(String url) { |
|
|
|
if (url == null || url.isEmpty()) { |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果已经是完整URL,直接返回
|
|
|
|
if (url.startsWith("http://") || url.startsWith("https://")) { |
|
|
|
return url; |
|
|
|
} |
|
|
|
|
|
|
|
// 拼接基础URL
|
|
|
|
String baseUrl = ossBaseUrl.endsWith("/") ? ossBaseUrl.substring(0, ossBaseUrl.length() - 1) : ossBaseUrl; |
|
|
|
String relativePath = url.startsWith("/") ? url : "/" + url; |
|
|
|
|
|
|
|
return baseUrl + relativePath; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 上传文件到OSS |
|
|
|
* |
|
|
|
* @param fileName 文件在OSS中的路径(如 voice/合肥/340100/...) |
|
|
|
* @param fileData 文件字节数组 |
|
|
|
* @return OSS返回的文件路径或URL |
|
|
|
*/ |
|
|
|
private String uploadFile(String fileName, byte[] fileData) { |
|
|
|
try { |
|
|
|
HttpHeaders headers = new HttpHeaders(); |
|
|
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA); |
|
|
|
@ -133,16 +68,13 @@ public class FileUploadUtil { |
|
|
|
}; |
|
|
|
|
|
|
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|
|
|
body.add("files", byteResource); |
|
|
|
body.set("appcode", ossAppcode); |
|
|
|
body.set("appid", ossAppid); |
|
|
|
body.set("appsecret", ossAppsecret); |
|
|
|
body.add("file", byteResource); |
|
|
|
body.add("path", path); |
|
|
|
|
|
|
|
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers); |
|
|
|
|
|
|
|
logger.debug("开始上传文件到OSS: {}, 大小: {} bytes", fileName, fileData.length); |
|
|
|
ResponseEntity<ResultEntity> response = restTemplate.exchange( |
|
|
|
ossUploadUrl, |
|
|
|
ossBaseUrl + "/oss/upload", |
|
|
|
HttpMethod.POST, |
|
|
|
requestEntity, |
|
|
|
ResultEntity.class |
|
|
|
@ -150,17 +82,18 @@ public class FileUploadUtil { |
|
|
|
|
|
|
|
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { |
|
|
|
ResultEntity result = response.getBody(); |
|
|
|
logger.debug("OSS返回结果: {}", result.toString()); |
|
|
|
if (result.getCode() == ResultEntity.StatusCode.SUCCESS.getCode()) { |
|
|
|
String url = parseUrlFromContent(result.getContent()); |
|
|
|
logger.debug("OSS返回URL: {}", url); |
|
|
|
return url; |
|
|
|
String fileUrl = parseFileUrl(result.getContent()); |
|
|
|
logger.info("文件上传完成, URL: {}", fileUrl); |
|
|
|
return fileUrl; |
|
|
|
} else { |
|
|
|
throw new RuntimeException("上传失败, code=" + result.getCode() + ", msg=" + result.getMsg()); |
|
|
|
} |
|
|
|
} else { |
|
|
|
throw new RuntimeException("上传失败, HTTP状态码: " + response.getStatusCode()); |
|
|
|
} |
|
|
|
} catch (RuntimeException e) { |
|
|
|
throw e; |
|
|
|
} catch (Exception e) { |
|
|
|
logger.error("文件上传异常: {}", fileName, e); |
|
|
|
throw new RuntimeException("文件上传失败: " + e.getMessage(), e); |
|
|
|
@ -168,124 +101,90 @@ public class FileUploadUtil { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 从 ResultEntity.content 中解析文件URL |
|
|
|
* |
|
|
|
* 支持多种返回格式: |
|
|
|
* 1. Map 对象: {"url": "http://xxx", "fileName": "xxx.wav"} |
|
|
|
* 2. JSON 字符串: "{\"url\":\"http://xxx\"}" |
|
|
|
* 3. 纯 URL 字符串: "http://xxx/xxx.wav" |
|
|
|
* 4. 数组格式: [{"url": "http://xxx"}] |
|
|
|
* 5. 相对路径: "voice/xxx/xxx.wav" |
|
|
|
* 从响应 content 中解析 fileUrl |
|
|
|
* |
|
|
|
* @param content OSS 返回的内容 |
|
|
|
* @return 解析后的文件路径或URL |
|
|
|
* 支持格式: |
|
|
|
* 1. Map: {"fileUrl": "http://xxx", "fileName": "xxx.mp3"} |
|
|
|
* 2. JSON字符串: "{\"fileUrl\":\"http://xxx\"}" |
|
|
|
* 3. 直接URL字符串 |
|
|
|
*/ |
|
|
|
private String parseUrlFromContent(Object content) { |
|
|
|
private String parseFileUrl(Object content) { |
|
|
|
if (content == null) { |
|
|
|
logger.warn("OSS 返回 content 为空"); |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
// 情况1:Map 对象
|
|
|
|
if (content instanceof java.util.Map) { |
|
|
|
java.util.Map<?, ?> map = (java.util.Map<?, ?>) content; |
|
|
|
Object url = map.get("url"); |
|
|
|
Object url = map.get("fileUrl"); |
|
|
|
if (url != null) { |
|
|
|
logger.debug("从 Map 解析到 URL: {}", url); |
|
|
|
return url.toString(); |
|
|
|
} |
|
|
|
// 尝试其他可能的 key
|
|
|
|
Object fileUrl = map.get("fileUrl"); |
|
|
|
if (fileUrl != null) { |
|
|
|
logger.debug("从 Map 解析到 fileUrl: {}", fileUrl); |
|
|
|
return fileUrl.toString(); |
|
|
|
} |
|
|
|
Object path = map.get("path"); |
|
|
|
if (path != null) { |
|
|
|
logger.debug("从 Map 解析到 path: {}", path); |
|
|
|
return path.toString(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 情况2:List/Array 对象(批量上传返回)
|
|
|
|
if (content instanceof java.util.List) { |
|
|
|
java.util.List<?> list = (java.util.List<?>) content; |
|
|
|
if (!list.isEmpty()) { |
|
|
|
Object first = list.get(0); |
|
|
|
if (first instanceof java.util.Map) { |
|
|
|
Object url = ((java.util.Map<?, ?>) first).get("url"); |
|
|
|
url = map.get("url"); |
|
|
|
if (url != null) { |
|
|
|
logger.debug("从 List 第一个元素解析到 URL: {}", url); |
|
|
|
return url.toString(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 情况3:JSON 字符串
|
|
|
|
String contentStr = content.toString().trim(); |
|
|
|
if (contentStr.startsWith("{") || contentStr.startsWith("[")) { |
|
|
|
try { |
|
|
|
// 尝试解析为 JSON
|
|
|
|
Object parsed = com.alibaba.fastjson2.JSON.parse(contentStr); |
|
|
|
if (parsed instanceof java.util.Map) { |
|
|
|
return parseUrlFromContent(parsed); // 递归解析
|
|
|
|
} |
|
|
|
if (parsed instanceof java.util.List) { |
|
|
|
return parseUrlFromContent(parsed); // 递归解析
|
|
|
|
return parseFileUrl(parsed); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
logger.debug("解析JSON失败: {}", e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 情况4:直接是 URL 字符串(以 http:// 或 https:// 开头)
|
|
|
|
if (contentStr.startsWith("http://") || contentStr.startsWith("https://")) { |
|
|
|
logger.debug("直接返回 URL 字符串: {}", contentStr); |
|
|
|
return contentStr; |
|
|
|
} |
|
|
|
|
|
|
|
// 情况5:相对路径(以 voice/ 开头)
|
|
|
|
if (contentStr.startsWith("voice/")) { |
|
|
|
logger.debug("返回相对路径: {}", contentStr); |
|
|
|
return contentStr; |
|
|
|
// 相对路径,拼接 base-url
|
|
|
|
if (contentStr.startsWith("/")) { |
|
|
|
String base = ossBaseUrl.endsWith("/") ? ossBaseUrl.substring(0, ossBaseUrl.length() - 1) : ossBaseUrl; |
|
|
|
return base + contentStr; |
|
|
|
} |
|
|
|
|
|
|
|
// 情况6:尝试从字符串中提取 URL
|
|
|
|
if (contentStr.contains("\"url\"")) { |
|
|
|
try { |
|
|
|
int start = contentStr.indexOf("\"url\":\"") + 7; |
|
|
|
int end = contentStr.indexOf("\"", start); |
|
|
|
if (start > 6 && end > start) { |
|
|
|
String url = contentStr.substring(start, end); |
|
|
|
logger.debug("从字符串提取 URL: {}", url); |
|
|
|
return url; |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
logger.warn("从字符串提取 URL 失败: {}", e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 兜底:直接返回字符串
|
|
|
|
logger.warn("无法解析 URL,直接返回 content: {}", contentStr); |
|
|
|
return contentStr; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 创建支持自定义SSL配置的RestTemplate |
|
|
|
* 针对HTTPS请求,配置信任所有证书并关闭Hostname验证,以兼容自签名证书 |
|
|
|
* 创建支持跳过SSL验证的RestTemplate |
|
|
|
*/ |
|
|
|
private RestTemplate createRestTemplate() { |
|
|
|
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() { |
|
|
|
try { |
|
|
|
TrustManager[] trustAll = new TrustManager[]{ |
|
|
|
new X509TrustManager() { |
|
|
|
@Override |
|
|
|
public X509Certificate[] getAcceptedIssuers() { return null; } |
|
|
|
@Override |
|
|
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {} |
|
|
|
@Override |
|
|
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {} |
|
|
|
} |
|
|
|
}; |
|
|
|
SSLContext sslContext = SSLContext.getInstance("TLS"); |
|
|
|
sslContext.init(null, trustAll, new java.security.SecureRandom()); |
|
|
|
|
|
|
|
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() { |
|
|
|
@Override |
|
|
|
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { |
|
|
|
if (ossUploadUrl.startsWith("https") && connection instanceof HttpsURLConnection) { |
|
|
|
// HTTPS 特殊配置(如需要)
|
|
|
|
if (connection instanceof HttpsURLConnection) { |
|
|
|
((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory()); |
|
|
|
((HttpsURLConnection) connection).setHostnameVerifier((hostname, session) -> true); |
|
|
|
} |
|
|
|
super.prepareConnection(connection, httpMethod); |
|
|
|
} |
|
|
|
}; |
|
|
|
requestFactory.setBufferRequestBody(false); |
|
|
|
return new RestTemplate(requestFactory); |
|
|
|
factory.setBufferRequestBody(false); |
|
|
|
factory.setConnectTimeout(15000); |
|
|
|
factory.setReadTimeout(300000); |
|
|
|
return new RestTemplate(factory); |
|
|
|
} catch (Exception e) { |
|
|
|
logger.error("创建SSL RestTemplate失败,使用默认配置", e); |
|
|
|
return new RestTemplate(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|