Browse Source

优化语音盒子拉取服务代码

main
wang 14 hours ago
parent
commit
62efde45d0
  1. 30
      config/application-external.yml
  2. 63
      src/main/java/com/threecloud/dataserviceyy/config/DataSourceConfig.java
  3. 12
      src/main/java/com/threecloud/dataserviceyy/config/DynamicDataSourceConfig.java
  4. 13
      src/main/java/com/threecloud/dataserviceyy/config/SyncTargetConfig.java
  5. 265
      src/main/java/com/threecloud/dataserviceyy/controller/VoiceBoxController.java
  6. 157
      src/main/java/com/threecloud/dataserviceyy/entity/CallRecord.java
  7. 93
      src/main/java/com/threecloud/dataserviceyy/entity/DeviceMatchResult.java
  8. 84
      src/main/java/com/threecloud/dataserviceyy/entity/FtpProcessedFile.java
  9. 77
      src/main/java/com/threecloud/dataserviceyy/entity/SyncLog.java
  10. 125
      src/main/java/com/threecloud/dataserviceyy/entity/VoiceRecord.java
  11. 48
      src/main/java/com/threecloud/dataserviceyy/mapper/FtpProcessedFileMapper.java
  12. 54
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java
  13. 25
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceDeviceLogMapper.java
  14. 55
      src/main/java/com/threecloud/dataserviceyy/mapper/SyncLogMapper.java
  15. 67
      src/main/java/com/threecloud/dataserviceyy/mapper/VoiceRecordMapper.java
  16. 96
      src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java
  17. 76
      src/main/java/com/threecloud/dataserviceyy/util/DataUtil.java
  18. 75
      src/main/java/com/threecloud/dataserviceyy/util/OssFileService.java
  19. 10
      src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java
  20. 45
      src/main/resources/logback-spring.xml
  21. 75
      src/main/resources/mapper/FtpProcessedFileMapper.xml
  22. 85
      src/main/resources/mapper/MidVoiceCallRecordMapper.xml
  23. 85
      src/main/resources/mapper/SyncLogMapper.xml
  24. 117
      src/main/resources/mapper/VoiceRecordMapper.xml

30
config/application-external.yml

@ -16,33 +16,6 @@ spring:
username: dcms_dev
password: your_password_here
# ==================== FTP同步配置 ====================
ftp-sync:
# 临时文件存储路径
temp-path: ./vaa-ftp-temp
# 临时文件保留天数
retain-days: 10
# txt文件编码
txt-encoding: GBK
# 字段分隔符
field-separator: "※"
# 地市配置(单地市部署,只配当前地市)
cities:
- city-code: "340100"
city-name: "huainan"
ftp-host: 10.126.129.7
ftp-port: 9979
ftp-username: yyfile
ftp-password: yyfile
ftp-source-dir: /voice_record/
ftp-record-dir: /rec/
ftp-archive-dir: /processed/
phone-area-code: "0000"
# ==================== 服务端口 ====================
server:
port: 8088
@ -52,10 +25,11 @@ oss:
base-url: http://53.1.211.7/apiOss
upload-path: voice/record/
# ==================== VAA同步配置 ====================
# ==================== VAA语音盒同步配置 ====================
vaa-sync:
download-path: ./vaa-recordings
retain-days: 10
sync-interval-cron: "0 0 0/8 * * ?"
# ==================== 日志配置 ====================
logging:

63
src/main/java/com/threecloud/dataserviceyy/config/DataSourceConfig.java

@ -1,63 +0,0 @@
package com.threecloud.dataserviceyy.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Bean
public DataSource dataSource() {
System.out.println("========================================");
System.out.println("【DataSourceConfig】创建数据源");
System.out.println("Driver: " + driverClassName);
System.out.println("URL: " + url);
System.out.println("Username: " + username);
System.out.println("Password length: " + (password != null ? password.length() : 0));
System.out.println("========================================");
HikariDataSource datasource = new HikariDataSource();
datasource.setJdbcUrl(url);
datasource.setDriverClassName(driverClassName);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setMinimumIdle(0);
datasource.setMaximumPoolSize(10);
datasource.setConnectionTimeout(30000);
datasource.setValidationTimeout(5000);
// 根据数据库类型设置不同的验证SQL
if (url.contains("kingbase")) {
datasource.setConnectionTestQuery("SELECT 1");
} else {
datasource.setConnectionTestQuery("SELECT 1 FROM DUAL");
datasource.addDataSourceProperty("oracle.jdbc.timezoneAsRegion", "false");
datasource.addDataSourceProperty("oracle.net.CONNECT_TIMEOUT", "10000");
datasource.addDataSourceProperty("oracle.net.READ_TIMEOUT", "60000");
}
// 关键:初始化时不测试连接,即使数据库暂时连不上也能启动
datasource.setInitializationFailTimeout(-1);
System.out.println("【DataSourceConfig】数据源创建完成(允许延迟连接)");
return datasource;
}
}

12
src/main/java/com/threecloud/dataserviceyy/config/DynamicDataSourceConfig.java

@ -1,12 +0,0 @@
package com.threecloud.dataserviceyy.config;
/**
* 多数据源配置 - 方案A阶段: 已完全禁用
*
* 此类在方案B(多数据源模式)时会重新启用
* 当前阶段使用Spring Boot默认的DataSource自动配置仿照老系统 yydc-tb-server
*/
// @Configuration // 【方案A: 已禁用】
public class DynamicDataSourceConfig {
// 所有方法已禁用,不再干扰Spring Boot的自动配置
}

13
src/main/java/com/threecloud/dataserviceyy/config/SyncTargetConfig.java

@ -1,13 +0,0 @@
package com.threecloud.dataserviceyy.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "server.target")
public class SyncTargetConfig {
private String syncLogCode;
private String syncLogTable;
}

265
src/main/java/com/threecloud/dataserviceyy/controller/VoiceBoxController.java

@ -1,265 +0,0 @@
package com.threecloud.dataserviceyy.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class VoiceBoxController {
private static final Logger logger = LoggerFactory.getLogger(VoiceBoxController.class);
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
/**
* 事件上传接口 - GET方式
* 语音盒来电/去电时主动推送事件信息
*
* 参数说明来自VAA录音仪开发文档
* - event_type: 事件类型 (3=状态提机, 4=提机, 6=开始去电录音, 7=来电接听开始录音,
* 8=去电完成挂机, 9=新的去电录音产生, 10=新的来电录音产生, 11=未接来电, 14=发送来电号码)
* - line: 设备端口
* - device_id: 设备编号
* - duration: 录音时长()
* - TimeLong: 通话时长()
* - date: 来电日期(格式: 2022-04-21 15:48:40)
* - caller: 来电号码(9,10,14事件中有值)
* - FilePath: 录音文件名称(9,10事件中有值)
* - voltage: 电压
* - RingCnt: 振铃次数
* - TotalStore: 设备存储容量
* - TotalFreeStore: 设备剩余存储容量
* - TotalMem: 内存使用率
* - CPU: CPU使用率
* - calloutId: websocket回传的calloutId
*
* 返回值成功返回 "0000"否则录音仪客户端日志会显示失败
*/
@GetMapping("/event")
public String handleEvent(
@RequestParam(required = false) String event_type,
@RequestParam(required = false) String line,
@RequestParam(required = false) String device_id,
@RequestParam(required = false) String duration,
@RequestParam(required = false) String TimeLong,
@RequestParam(required = false) String date,
@RequestParam(required = false) String caller,
@RequestParam(required = false) String FilePath,
@RequestParam(required = false) String voltage,
@RequestParam(required = false) String RingCnt,
@RequestParam(required = false) String TotalStore,
@RequestParam(required = false) String TotalFreeStore,
@RequestParam(required = false) String TotalMem,
@RequestParam(required = false) String CPU,
@RequestParam(required = false) String calloutId) {
try {
logger.info("========================================");
logger.info("[事件上传] 收到语音盒事件推送");
logger.info("[事件类型] event_type={} ({})", event_type, getEventTypeDesc(event_type));
logger.info("[设备信息] device_id={}, line={}", device_id, line);
if (date != null) {
logger.info("[通话时间] date={}", date);
}
if (caller != null && !caller.isEmpty()) {
logger.info("[来电号码] caller={}", caller);
}
if (FilePath != null && !FilePath.isEmpty()) {
logger.info("[文件路径] FilePath={}", FilePath);
}
if (duration != null) {
logger.info("[录音时长] duration={}秒", duration);
}
if (TimeLong != null) {
logger.info("[通话时长] TimeLong={}秒", TimeLong);
}
Map<String, String> eventData = new HashMap<>();
eventData.put("event_type", event_type);
eventData.put("line", line);
eventData.put("device_id", device_id);
eventData.put("date", date);
eventData.put("caller", caller);
eventData.put("FilePath", FilePath);
saveEventToDatabase(eventData);
logger.info("[事件上传] 处理完成");
logger.info("========================================");
return "0000";
} catch (Exception e) {
logger.error("[事件上传] 处理异常: {}", e.getMessage(), e);
return "0000";
}
}
/**
* 文件上传接口 - POST方式
* 语音盒主动上传录音文件先保存到本地临时目录再由定时任务上传到OSS
*
* 参数说明来自VAA录音仪开发文档
* - files: 文件上传信息(文件名称与事件中的FilePath字段对应)
* 文件名称格式: 设备名称-年月日时分秒-(O|I)-L(端口号)-EN-电话号码.wav
* O=去电, I=来电
* - event_type: 事件类型(9=去电录音, 10=来电录音)
* - device_id: 设备编号
* - line: 端口编号
* - date: 来电日期
* - caller: 来电号码
* - FilePath: 录音文件名称
* - duration: 录音时长()
* - TimeLong: 通话时长()
*
* 返回值成功返回 "0000"否则录音仪客户端日志会显示失败
*/
@PostMapping("/file")
public String handleFileUpload(
@RequestParam("files") MultipartFile file,
@RequestParam(required = false) String event_type,
@RequestParam(required = false) String device_id,
@RequestParam(required = false) String line,
@RequestParam(required = false) String date,
@RequestParam(required = false) String caller,
@RequestParam(required = false) String FilePath,
@RequestParam(required = false) String duration,
@RequestParam(required = false) String TimeLong) {
try {
logger.info("========================================");
logger.info("[文件上传] 收到语音盒文件上传请求");
logger.info("[事件类型] event_type={} ({})", event_type, getEventTypeDesc(event_type));
logger.info("[设备信息] device_id={}, line={}", device_id, line);
if (file != null && !file.isEmpty()) {
String originalFilename = file.getOriginalFilename();
long fileSize = file.getSize();
logger.info("[文件信息] fileName={}, size={} bytes", originalFilename, fileSize);
}
if (FilePath != null && !FilePath.isEmpty()) {
logger.info("[文件路径] FilePath={}", FilePath);
}
if (caller != null && !caller.isEmpty()) {
logger.info("[来电号码] caller={}", caller);
}
if (file != null && !file.isEmpty()) {
String fileName = file.getOriginalFilename();
String tempDir = "/tmp/voice_upload/";
File tempDirFile = new File(tempDir);
if (!tempDirFile.exists()) {
tempDirFile.mkdirs();
}
String tempFilePath = tempDir + fileName;
file.transferTo(new File(tempFilePath));
logger.info("[保存本地] 文件已保存到临时目录: {}", tempFilePath);
saveRecordToDatabase(device_id, line, date, caller, fileName, duration, TimeLong, tempFilePath, "0");
}
logger.info("[文件上传] 处理完成");
logger.info("========================================");
return "0000";
} catch (IOException e) {
logger.error("[文件上传] 文件保存异常: {}", e.getMessage(), e);
return "0000";
} catch (Exception e) {
logger.error("[文件上传] 处理异常: {}", e.getMessage(), e);
return "0000";
}
}
/**
* 获取事件类型描述
*/
private String getEventTypeDesc(String eventType) {
if (eventType == null) return "未知";
switch (eventType) {
case "3": return "状态提机(ON HOOK)";
case "4": return "提机(OFF HOOK)";
case "6": return "开始去电录音";
case "7": return "来电接听开始录音";
case "8": return "去电完成挂机(ON HOOK)";
case "9": return "新的去电录音产生";
case "10": return "新的来电录音产生";
case "11": return "未接来电";
case "14": return "发送来电号码";
default: return "其他(" + eventType + ")";
}
}
/**
* 从文件名提取设备名称
* 文件格式: 设备名称-年月日时分秒-(O|I)-L(端口号)-EN-电话号码.wav
*/
private String extractDeviceName(String fileName) {
if (fileName == null || !fileName.contains("-")) {
return "unknown";
}
return fileName.substring(0, fileName.indexOf("-"));
}
/**
* 从文件名提取日期
* 文件格式: 设备名称-年月日时分秒-(O|I)-L(端口号)-EN-电话号码.wav
*/
private String extractDateFromFileName(String fileName) {
if (fileName == null) {
return LocalDateTime.now().format(DATE_FORMAT);
}
String datePattern = "\\d{8}";
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(datePattern);
java.util.regex.Matcher matcher = pattern.matcher(fileName);
if (matcher.find()) {
return matcher.group();
}
return LocalDateTime.now().format(DATE_FORMAT);
}
/**
* 从文件名提取通话类型
* O=去电, I=来电
*/
private String extractCallType(String fileName) {
if (fileName == null) return "0";
if (fileName.contains("-O-")) return "1";
if (fileName.contains("-I-")) return "0";
return "0";
}
/**
* 保存事件到数据库示例方法
*/
private void saveEventToDatabase(Map<String, String> eventData) {
logger.debug("[事件存储] device_id={}, event_type={}",
eventData.get("device_id"), eventData.get("event_type"));
}
/**
* 保存通话记录到数据库示例方法
*/
private void saveRecordToDatabase(String deviceId, String line, String date, String caller,
String fileName, String duration, String timeLong,
String ossUrl, String callType) {
logger.debug("[记录存储] device_id={}, fileName={}, ossUrl={}", deviceId, fileName, ossUrl);
}
}

157
src/main/java/com/threecloud/dataserviceyy/entity/CallRecord.java

@ -1,157 +0,0 @@
package com.threecloud.dataserviceyy.entity;
/**
* 通话记录实体类
*
* 封装从FTP数据文件中解析出的单条通话记录信息
* 数据来源FTP服务器上的.txt文件字段间用"※"分隔
*
* 字段说明
* - kssj: 开始时间 (格式: yyyy-MM-dd HH:mm:ss)
* - jssj: 结束时间 (格式: yyyy-MM-dd HH:mm:ss)
* - hjls: 呼机流水号
* - hjzls: 呼机总流水号
* - zjhm: 主叫号码
* - bjhm: 被叫号码
* - thsc: 通话时长()
* - thfx: 通话方向(1=主叫方向, 0=被叫方向)
* - dateDir: 录音文件所在日期目录(yyyyMMdd)
* - wavFileName: 对应的WAV录音文件名(格式: 呼机流水号_呼机总流水号.wav)
*/
public class CallRecord {
/** 开始时间 */
private String kssj;
/** 结束时间 */
private String jssj;
/** 呼机流水号 */
private String hjls;
/** 呼机总流水号 */
private String hjzls;
/** 主叫号码(可能包含区号前缀如0553) */
private String zjhm;
/** 被叫号码(可能包含区号前缀如0553) */
private String bjhm;
/** 通话时长(秒) */
private Long thsc;
/** 通话方向(1=主叫, 0=被叫) */
private String thfx;
/** 日期目录(yyyyMMdd格式,从kssj中提取) */
private String dateDir;
/** WAV录音文件名(格式: 呼机流水号_呼机总流水号.wav) */
private String wavFileName;
public CallRecord() {
}
/**
* 获取通话记录摘要信息
* 用于日志输出显示主被叫号码
* @return 格式化的摘要字符串主叫=13800138000, 被叫=13900139000
*/
public String getSummary() {
return "主叫=" + zjhm + ", 被叫=" + bjhm;
}
public String getKssj() {
return kssj;
}
public void setKssj(String kssj) {
this.kssj = kssj;
}
public String getJssj() {
return jssj;
}
public void setJssj(String jssj) {
this.jssj = jssj;
}
public String getHjls() {
return hjls;
}
public void setHjls(String hjls) {
this.hjls = hjls;
}
public String getHjzls() {
return hjzls;
}
public void setHjzls(String hjzls) {
this.hjzls = hjzls;
}
public String getZjhm() {
return zjhm;
}
public void setZjhm(String zjhm) {
this.zjhm = zjhm;
}
public String getBjhm() {
return bjhm;
}
public void setBjhm(String bjhm) {
this.bjhm = bjhm;
}
public Long getThsc() {
return thsc;
}
public void setThsc(Long thsc) {
this.thsc = thsc;
}
public String getThfx() {
return thfx;
}
public void setThfx(String thfx) {
this.thfx = thfx;
}
public String getDateDir() {
return dateDir;
}
public void setDateDir(String dateDir) {
this.dateDir = dateDir;
}
public String getWavFileName() {
return wavFileName;
}
public void setWavFileName(String wavFileName) {
this.wavFileName = wavFileName;
}
@Override
public String toString() {
return "CallRecord{" +
"kssj='" + kssj + '\'' +
", jssj='" + jssj + '\'' +
", zjhm='" + zjhm + '\'' +
", bjhm='" + bjhm + '\'' +
", thsc=" + thsc +
", thfx='" + thfx + '\'' +
", wavFileName='" + wavFileName + '\'' +
'}';
}
}

93
src/main/java/com/threecloud/dataserviceyy/entity/DeviceMatchResult.java

@ -1,93 +0,0 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Map;
/**
* 设备匹配结果实体类
*
* 封装通话记录与设备通道表匹配后的结果信息
* 匹配流程
* 1. 根据电话号码在设备通道表(YYDC_SBTD)中查找对应设备
* 2. 根据设备的UUID在语音盒表(YYDC_YYSB)中查找所属地市信息
* 3. 返回完整的匹配结果
*/
public class DeviceMatchResult {
/** 是否匹配成功 */
private boolean success;
/** 设备通道信息(ID, UUID, PHONE, THFX等字段) */
private Map<String, Object> deviceChannel;
/** 语音盒信息(ID, ORGAN_NAME, ORGAN_ID等字段) */
private Map<String, Object> voiceBox;
/** 匹配时使用的电话号码(可能是主叫或被叫) */
private String phone;
/** 失败时的错误描述信息 */
private String errorMessage;
public DeviceMatchResult() {
}
/**
* 创建匹配成功的结果对象
* @param deviceChannel 设备通道表中的记录信息
* @param voiceBox 语音盒表中的记录信息
* @param phone 匹配到的有效电话号码
* @return 成功状态的DeviceMatchResult实例
*/
public static DeviceMatchResult success(Map<String, Object> deviceChannel,
Map<String, Object> voiceBox,
String phone) {
DeviceMatchResult result = new DeviceMatchResult();
result.success = true;
result.deviceChannel = deviceChannel;
result.voiceBox = voiceBox;
result.phone = phone;
return result;
}
/**
* 创建匹配失败的结果对象
* @param errorMessage 失败原因描述
* @return 失败状态的DeviceMatchResult实例
*/
public static DeviceMatchResult fail(String errorMessage) {
DeviceMatchResult result = new DeviceMatchResult();
result.success = false;
result.errorMessage = errorMessage;
return result;
}
public boolean isSuccess() {
return success;
}
public Map<String, Object> getDeviceChannel() {
return deviceChannel;
}
public Map<String, Object> getVoiceBox() {
return voiceBox;
}
public String getPhone() {
return phone;
}
public String getErrorMessage() {
return errorMessage;
}
@Override
public String toString() {
if (success) {
return "DeviceMatchResult{success=true, phone=" + phone +
", organName=" + (voiceBox != null ? voiceBox.get("ORGAN_NAME") : "null") + "}";
} else {
return "DeviceMatchResult{success=false, error='" + errorMessage + "'}";
}
}
}

84
src/main/java/com/threecloud/dataserviceyy/entity/FtpProcessedFile.java

@ -1,84 +0,0 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* FTP已处理文件记录表
* 对应 mid_voice.mid_ftp_processed_file
* 用于记录已处理过的FTP txt文件避免重复处理
*/
public class FtpProcessedFile {
/** 自增ID主键 */
private Long id;
/** 地市编码 */
private String cityCode;
/** 地市名称 */
private String cityName;
/** FTP文件名 */
private String fileName;
/** 文件大小(字节) */
private Long fileSize;
/** 处理状态:0失败,1成功 */
private String processStatus;
/** 记录总数 */
private Integer recordCount;
/** 成功入库数 */
private Integer successCount;
/** 失败数 */
private Integer failCount;
/** 处理耗时(毫秒) */
private Long processTime;
/** 错误信息 */
private String errorMsg;
/** 创建时间 */
private Date createTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCityCode() { return cityCode; }
public void setCityCode(String cityCode) { this.cityCode = cityCode; }
public String getCityName() { return cityName; }
public void setCityName(String cityName) { this.cityName = cityName; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getProcessStatus() { return processStatus; }
public void setProcessStatus(String processStatus) { this.processStatus = processStatus; }
public Integer getRecordCount() { return recordCount; }
public void setRecordCount(Integer recordCount) { this.recordCount = recordCount; }
public Integer getSuccessCount() { return successCount; }
public void setSuccessCount(Integer successCount) { this.successCount = successCount; }
public Integer getFailCount() { return failCount; }
public void setFailCount(Integer failCount) { this.failCount = failCount; }
public Long getProcessTime() { return processTime; }
public void setProcessTime(Long processTime) { this.processTime = processTime; }
public String getErrorMsg() { return errorMsg; }
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}

77
src/main/java/com/threecloud/dataserviceyy/entity/SyncLog.java

@ -1,77 +0,0 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* 同步日志表
* 记录每次同步任务的执行情况
*/
public class SyncLog {
/** 主键ID */
private Long id;
/** 设备ID */
private String deviceId;
/** 设备名称 */
private String deviceName;
/** 同步开始时间 */
private Date startTime;
/** 同步结束时间 */
private Date endTime;
/** 查询到的记录数 */
private Integer totalCount;
/** 成功处理数 */
private Integer successCount;
/** 失败数 */
private Integer failCount;
/** 同步状态:0失败,1成功,2部分成功 */
private Integer status;
/** 错误信息 */
private String errorMsg;
/** 创建时间 */
private Date createTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getDeviceName() { return deviceName; }
public void setDeviceName(String deviceName) { this.deviceName = deviceName; }
public Date getStartTime() { return startTime; }
public void setStartTime(Date startTime) { this.startTime = startTime; }
public Date getEndTime() { return endTime; }
public void setEndTime(Date endTime) { this.endTime = endTime; }
public Integer getTotalCount() { return totalCount; }
public void setTotalCount(Integer totalCount) { this.totalCount = totalCount; }
public Integer getSuccessCount() { return successCount; }
public void setSuccessCount(Integer successCount) { this.successCount = successCount; }
public Integer getFailCount() { return failCount; }
public void setFailCount(Integer failCount) { this.failCount = failCount; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getErrorMsg() { return errorMsg; }
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}

125
src/main/java/com/threecloud/dataserviceyy/entity/VoiceRecord.java

@ -1,125 +0,0 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* 录音记录表
* 保存每次从录音盒下载并上传到OSS的录音文件信息
*/
public class VoiceRecord {
/** 主键ID */
private Long id;
/** 设备ID */
private String deviceId;
/** 设备UUID */
private String deviceUuid;
/** 设备名称/机构名称 */
private String deviceName;
/** 录音盒原始记录ID */
private String recordId;
/** 录音文件名 */
private String fileName;
/** 录音文件在录音盒中的路径 */
private String filePath;
/** OSS访问地址 */
private String ossUrl;
/** 本地存储路径 */
private String localPath;
/** 通话开始时间 */
private Date callStartTime;
/** 通话结束时间 */
private Date callEndTime;
/** 通话时长(秒) */
private Integer duration;
/** 通道号 1-8 */
private Integer channel;
/** 对方电话号码 */
private String phone;
/** 通话方向:1呼出,2呼入 */
private Integer direction;
/** 上传状态:0失败,1成功 */
private Integer status;
/** 失败原因 */
private String failReason;
/** 创建时间 */
private Date createTime;
/** 更新时间 */
private Date updateTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getDeviceUuid() { return deviceUuid; }
public void setDeviceUuid(String deviceUuid) { this.deviceUuid = deviceUuid; }
public String getDeviceName() { return deviceName; }
public void setDeviceName(String deviceName) { this.deviceName = deviceName; }
public String getRecordId() { return recordId; }
public void setRecordId(String recordId) { this.recordId = recordId; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getOssUrl() { return ossUrl; }
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
public String getLocalPath() { return localPath; }
public void setLocalPath(String localPath) { this.localPath = localPath; }
public Date getCallStartTime() { return callStartTime; }
public void setCallStartTime(Date callStartTime) { this.callStartTime = callStartTime; }
public Date getCallEndTime() { return callEndTime; }
public void setCallEndTime(Date callEndTime) { this.callEndTime = callEndTime; }
public Integer getDuration() { return duration; }
public void setDuration(Integer duration) { this.duration = duration; }
public Integer getChannel() { return channel; }
public void setChannel(Integer channel) { this.channel = channel; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public Integer getDirection() { return direction; }
public void setDirection(Integer direction) { this.direction = direction; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getFailReason() { return failReason; }
public void setFailReason(String failReason) { this.failReason = failReason; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public Date getUpdateTime() { return updateTime; }
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
}

48
src/main/java/com/threecloud/dataserviceyy/mapper/FtpProcessedFileMapper.java

@ -1,48 +0,0 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.FtpProcessedFile;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* FTP已处理文件记录Mapper
* 用于记录已处理过的FTP txt文件避免重复处理
*/
@Mapper
public interface FtpProcessedFileMapper {
/**
* 插入处理记录
*/
int insert(FtpProcessedFile record);
/**
* 根据地市编码和文件名查询判断是否已处理
*/
FtpProcessedFile selectByCityAndFileName(@Param("cityCode") String cityCode,
@Param("fileName") String fileName);
/**
* 查询指定地市下所有已处理的文件名列表
*/
List<String> selectProcessedFileNamesByCity(@Param("cityCode") String cityCode);
/**
* 批量查询指定地市下多个文件的处理状态
*/
List<FtpProcessedFile> selectByCityAndFileNames(@Param("cityCode") String cityCode,
@Param("fileNames") List<String> fileNames);
/**
* 根据ID更新处理结果
*/
int updateById(FtpProcessedFile record);
/**
* 删除指定时间之前的记录清理历史
*/
int deleteBeforeTime(@Param("cityCode") String cityCode,
@Param("beforeTime") String beforeTime);
}

54
src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java

@ -11,21 +11,11 @@ import java.util.List;
*/
@Mapper
public interface MidVoiceCallRecordMapper {
/**
* 插入通话记录
*/
int insert(MidVoiceCallRecord record);
/**
* 根据ID查询
*/
MidVoiceCallRecord selectById(Long id);
/**
* 根据通话记录ID查询防止重复插入
*/
MidVoiceCallRecord selectByCallRecordId(@Param("callRecordId") String callRecordId);
/**
* 批量查询某设备在时间范围内的已存在call_record_id
@ -34,44 +24,4 @@ public interface MidVoiceCallRecordMapper {
List<String> selectExistingCallRecordIds(@Param("deviceNo") String deviceNo,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 根据设备编码查询
*/
List<MidVoiceCallRecord> selectByDeviceNo(@Param("deviceNo") String deviceNo);
/**
* 分页查询
*/
List<MidVoiceCallRecord> selectList(@Param("cityCode") String cityCode,
@Param("deviceNo") String deviceNo,
@Param("callTel") String callTel,
@Param("calledTel") String calledTel,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计总数
*/
int count(@Param("cityCode") String cityCode,
@Param("deviceNo") String deviceNo,
@Param("callTel") String callTel,
@Param("calledTel") String calledTel,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 更新文件路径
*/
int updateFilePath(@Param("id") Long id,
@Param("recordingFilePath") String recordingFilePath);
/**
* 更新状态和失败原因
*/
int updateStatus(@Param("id") Long id,
@Param("callStatus") String callStatus,
@Param("failReason") String failReason);
}
}

25
src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceDeviceLogMapper.java

@ -3,10 +3,6 @@ package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 设备连接日志 Mapper
@ -25,23 +21,4 @@ public interface MidVoiceDeviceLogMapper {
"#{connectType}, #{connectStatus}, #{failReason}, #{createTime}" +
")")
int insert(MidVoiceDeviceLog log);
/**
* 查询设备的最近连接日志
*/
@Select("SELECT * FROM mid_voice_device_log " +
"WHERE device_id = #{deviceId} " +
"ORDER BY create_time DESC " +
"LIMIT #{limit}")
List<MidVoiceDeviceLog> selectRecentByDeviceId(@Param("deviceId") String deviceId,
@Param("limit") int limit);
/**
* 查询失败的连接记录
*/
@Select("SELECT * FROM mid_voice_device_log " +
"WHERE connect_status = '0' " +
"AND create_time >= #{startTime} " +
"ORDER BY create_time DESC")
List<MidVoiceDeviceLog> selectFailedLogs(@Param("startTime") String startTime);
}
}

55
src/main/java/com/threecloud/dataserviceyy/mapper/SyncLogMapper.java

@ -1,55 +0,0 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.SyncLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 同步日志Mapper
*/
@Mapper
public interface SyncLogMapper {
/**
* 插入同步日志
*/
int insert(SyncLog log);
/**
* 根据ID查询
*/
SyncLog selectById(Long id);
/**
* 查询设备的同步日志
*/
List<SyncLog> selectByDeviceId(@Param("deviceId") String deviceId);
/**
* 分页查询同步日志
*/
List<SyncLog> selectList(@Param("deviceId") String deviceId,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计总数
*/
int count(@Param("deviceId") String deviceId,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 更新同步结果
*/
int updateResult(@Param("id") Long id,
@Param("endTime") java.util.Date endTime,
@Param("successCount") Integer successCount,
@Param("failCount") Integer failCount,
@Param("status") Integer status,
@Param("errorMsg") String errorMsg);
}

67
src/main/java/com/threecloud/dataserviceyy/mapper/VoiceRecordMapper.java

@ -1,67 +0,0 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.VoiceRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 录音记录Mapper
*/
@Mapper
public interface VoiceRecordMapper {
/**
* 插入录音记录
*/
int insert(VoiceRecord record);
/**
* 根据ID查询
*/
VoiceRecord selectById(Long id);
/**
* 根据设备ID和录音ID查询防止重复插入
*/
VoiceRecord selectByDeviceAndRecordId(@Param("deviceId") String deviceId,
@Param("recordId") String recordId);
/**
* 查询设备的所有录音记录
*/
List<VoiceRecord> selectByDeviceId(@Param("deviceId") String deviceId);
/**
* 分页查询录音记录
*/
List<VoiceRecord> selectList(@Param("deviceId") String deviceId,
@Param("deviceName") String deviceName,
@Param("phone") String phone,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计总数
*/
int count(@Param("deviceId") String deviceId,
@Param("deviceName") String deviceName,
@Param("phone") String phone,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 更新OSS地址如果上传后需要更新
*/
int updateOssUrl(@Param("id") Long id, @Param("ossUrl") String ossUrl);
/**
* 更新状态
*/
int updateStatus(@Param("id") Long id,
@Param("status") Integer status,
@Param("failReason") String failReason);
}

96
src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java

@ -17,10 +17,12 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.File;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;
/**
@ -29,6 +31,7 @@ import java.util.*;
* 功能从EBOX录音盒拉取录音 下载文件 上传OSS 保存通话记录
* 策略增量同步无重试防重复失败记录到日志表
* 账户每个设备的账号密码从 mid_voice_device_config 表读取
* OSS存储路径voice/{地市编码}/{yyyy-MM-dd}/{文件名}
*/
@Service
public class VaaSyncService {
@ -108,7 +111,6 @@ public class VaaSyncService {
String ip = getStr(device, "IP");
Integer port = getInt(device, "PORT", 80);
String orgCode = getStr(device, "ORG_CODE");
// 每个设备有自己的账号密码,从配置表读取
String username = getStr(device, "USERNAME");
String password = getStr(device, "PASSWORD");
@ -131,12 +133,10 @@ public class VaaSyncService {
String deviceHost = buildHost(ip, port);
// 步骤1:登录(无重试)
// 步骤1:登录
String authToken;
try {
String loginUrl = String.format("http://%s/authorize?username=%s&password=%s",
deviceHost, URLEncoder.encode(username, "UTF-8"), URLEncoder.encode(password, "UTF-8"));
authToken = vaaHttpUtil.httpLogin(loginUrl);
authToken = loginDevice(deviceHost, username, password);
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "1", null);
} catch (Exception e) {
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "登录失败:" + e.getMessage());
@ -144,10 +144,10 @@ public class VaaSyncService {
return;
}
// 步骤2:获取分机号码(通道→电话号码映射),用于解析主叫/被叫
// 步骤2:获取分机号码
Map<String, String> extNumbers = getExtensionNumbers(deviceHost, authToken);
// 步骤3:计算同步时间范围(增量同步)
// 步骤3:计算同步时间范围
Date lastSyncTime = SyncTimeUtil.readLastSyncTime(downloadPath, deviceId);
Date now = new Date();
if (lastSyncTime == null || DateUtil.getDateDoubleDiff(now, lastSyncTime) > 1.0) {
@ -156,12 +156,12 @@ public class VaaSyncService {
long startEpoch = lastSyncTime.getTime() / 1000;
long endEpoch = now.getTime() / 1000;
// 步骤4:获取录音列表(无重试
// 步骤4:获取录音列表(Token过期时自动重登录重试一次
JSONArray records;
try {
String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]",
deviceHost, startEpoch, endEpoch);
String recordData = vaaHttpUtil.httpVisit(recordUrl, authToken);
String recordData = httpVisitWithReauth(recordUrl, authToken, deviceHost, username, password);
records = vaaHttpUtil.parseRecordData(recordData);
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "2", "1", null);
} catch (Exception e) {
@ -176,7 +176,7 @@ public class VaaSyncService {
}
logger.info("【设备】获取到 {} 条录音记录", records.size());
// 批量查询已存在的call_record_id(一次查询代替N次,防重复性能优化)
// 批量查询已存在的call_record_id
String startTimeStr = DateUtil.formatDate(lastSyncTime, "yyyy-MM-dd HH:mm:ss");
String endTimeStr = DateUtil.formatDate(now, "yyyy-MM-dd HH:mm:ss");
Set<String> existingIds = new HashSet<>(callRecordMapper.selectExistingCallRecordIds(
@ -186,18 +186,27 @@ public class VaaSyncService {
// 步骤5:逐条处理录音
int successCount = 0;
int failCount = 0;
int skipCount = 0;
Date latestCallTime = null;
for (int i = 0; i < records.size(); i++) {
JSONObject rec = records.getJSONObject(i);
try {
// 解析时间用于更新同步进度(即使跳过也要更新,防止重复拉取)
Long endTime = RecordParser.parseEndTime(rec);
if (endTime != null) {
Date recEndTime = new Date(endTime * 1000);
if (latestCallTime == null || recEndTime.after(latestCallTime)) {
latestCallTime = recEndTime;
}
}
Date callTime = processSingleRecord(rec, deviceNo, cityName, cityCode, orgCode,
deviceHost, authToken, extNumbers, existingIds);
deviceHost, authToken, extNumbers, existingIds, username, password);
if (callTime != null) {
successCount++;
if (latestCallTime == null || callTime.after(latestCallTime)) {
latestCallTime = callTime;
}
} else {
skipCount++;
}
} catch (Exception e) {
failCount++;
@ -205,7 +214,7 @@ public class VaaSyncService {
}
}
// 步骤6:保存同步时间
// 步骤6:保存同步时间(用API返回的最大时间,而非仅新处理的时间)
if (latestCallTime != null) {
SyncTimeUtil.writeLastSyncTime(downloadPath, deviceId, latestCallTime);
}
@ -216,12 +225,15 @@ public class VaaSyncService {
/**
* 处理单条录音记录
*
* OSS存储路径: voice/{cityCode}/{yyyy-MM-dd}/{fileName}
* 上传失败不阻塞记录仍保存到数据库文件路径留空
*
* @return 通话开始时间成功时null表示跳过
*/
private Date processSingleRecord(JSONObject rec, String deviceNo, String cityName,
String cityCode, String orgCode, String deviceHost,
String authToken, Map<String, String> extNumbers,
Set<String> existingIds) throws Exception {
Set<String> existingIds, String username, String password) throws Exception {
// 解析录音信息
String recordId = RecordParser.parseRecordId(rec);
String filePath = RecordParser.parseFilePath(rec);
@ -232,7 +244,6 @@ public class VaaSyncService {
Long begTime = RecordParser.parseBegTime(rec);
Long endTime = RecordParser.parseEndTime(rec);
// 校验必要字段
if (filePath == null || filePath.isEmpty()) {
return null;
}
@ -240,7 +251,7 @@ public class VaaSyncService {
return null;
}
// 防重复:内存中比对已存在的call_record_id(一次查询代替N次)
// 防重复
String callRecordId = deviceNo + "_" + recordId;
if (existingIds.contains(callRecordId)) {
logger.debug("【录音】已存在,跳过: {}", callRecordId);
@ -253,7 +264,7 @@ public class VaaSyncService {
String localPath = FilePathUtil.buildLocalPath(downloadPath, cityCode, callStartTime, deviceNo, fileName);
Path localFile = Paths.get(localPath);
// 获取通道绑定的电话号码(从EBOX分机号码配置)
// 获取通道绑定的电话号码
String channelPhone = "";
if (channel != null && extNumbers != null) {
channelPhone = extNumbers.getOrDefault(String.valueOf(channel), "");
@ -275,12 +286,17 @@ public class VaaSyncService {
}
callRecord.setRecordingFileSize((int) Files.size(localFile));
// 上传到OSS并获取预览URL
OssFileResponse ossResponse = ossFileService.uploadFile(
Files.readAllBytes(localFile), fileName, "voice/" + cityCode + "/");
String previewUrl = ossResponse.getFileUrl();
callRecord.setRecordingFilePath(previewUrl);
logger.info("【录音】OSS预览URL: {}", previewUrl);
// 上传到OSS(地市+日期目录),上传失败不阻塞,仍保存通话记录
String ossPath = "voice/" + cityCode + "/" + new SimpleDateFormat("yyyy-MM-dd").format(callStartTime) + "/";
try {
OssFileResponse ossResponse = ossFileService.uploadFile(localFile.toFile(), fileName, ossPath);
String previewUrl = ossResponse.getFileUrl();
callRecord.setRecordingFilePath(previewUrl);
logger.info("【录音】OSS上传成功: {}", previewUrl);
} catch (Exception e) {
logger.error("【录音】OSS上传失败,通话记录仍将保存: fileName={}, 原因={}", fileName, e.getMessage());
callRecord.setRecordingFilePath(null);
}
// 保存到数据库
callRecordMapper.insert(callRecord);
@ -289,6 +305,32 @@ public class VaaSyncService {
return callStartTime;
}
/**
* 登录录音盒设备
*/
private String loginDevice(String deviceHost, String username, String password) throws Exception {
String loginUrl = String.format("http://%s/authorize?username=%s&password=%s",
deviceHost, URLEncoder.encode(username, "UTF-8"), URLEncoder.encode(password, "UTF-8"));
return vaaHttpUtil.httpLogin(loginUrl);
}
/**
* HTTP访问APIToken过期时自动重新登录并重试一次
*/
private String httpVisitWithReauth(String apiUrl, String authToken,
String deviceHost, String username, String password) throws Exception {
try {
return vaaHttpUtil.httpVisit(apiUrl, authToken);
} catch (Exception e) {
if (e.getMessage() != null && e.getMessage().contains("认证失效")) {
logger.warn("【设备】Token过期,自动重新登录并重试");
String newToken = loginDevice(deviceHost, username, password);
return vaaHttpUtil.httpVisit(apiUrl, newToken);
}
throw e;
}
}
/**
* 构建通话记录实体
*
@ -330,8 +372,6 @@ public class VaaSyncService {
/**
* 获取分机号码配置
* 调用 EBOX 接口 GET /service/ext/number
* 返回 Map<通道号, 电话号码> {"1":"8001","2":"8002"}
*/
private Map<String, String> getExtensionNumbers(String deviceHost, String authToken) {
try {
@ -384,4 +424,4 @@ public class VaaSyncService {
try { return Integer.parseInt(v.toString()); }
catch (NumberFormatException e) { return defaultVal; }
}
}
}

76
src/main/java/com/threecloud/dataserviceyy/util/DataUtil.java

@ -1,76 +0,0 @@
package com.threecloud.dataserviceyy.util;
import java.util.Collection;
import java.util.Map;
public class DataUtil {
public static boolean isNotNull(Object obj) {
return obj != null;
}
public static boolean isNotNull(Map map) {
return map != null && !map.isEmpty();
}
public static boolean isNotNull(Collection collection) {
return collection != null && !collection.isEmpty();
}
public static boolean isNotNull(String str) {
return str != null && !str.trim().isEmpty();
}
public static boolean isNumber(Object obj) {
if (obj == null) return false;
try {
Double.parseDouble(obj.toString());
return true;
} catch (NumberFormatException e) {
return false;
}
}
public static Long objToLong(Object obj) {
if (obj == null) return null;
if (obj instanceof Long) return (Long) obj;
if (obj instanceof Number) return ((Number) obj).longValue();
if (isNumber(obj)) return Long.parseLong(obj.toString());
return null;
}
public static String objToValue(Object obj) {
if (obj == null) return null;
return obj.toString();
}
public static Integer objToInteger(Object obj) {
if (obj == null) return null;
if (obj instanceof Integer) return (Integer) obj;
if (obj instanceof Number) return ((Number) obj).intValue();
if (isNumber(obj)) return Integer.parseInt(obj.toString());
return null;
}
public static Float objToFloat(Object obj) {
if (obj == null) return null;
if (obj instanceof Float) return (Float) obj;
if (obj instanceof Number) return ((Number) obj).floatValue();
if (isNumber(obj)) return Float.parseFloat(obj.toString());
return null;
}
public static Double objToDouble(Object obj) {
if (obj == null) return null;
if (obj instanceof Double) return (Double) obj;
if (obj instanceof Number) return ((Number) obj).doubleValue();
if (isNumber(obj)) return Double.parseDouble(obj.toString());
return null;
}
public static boolean objToBoolean(Object obj) {
if (obj == null) return false;
if (obj instanceof Boolean) return (Boolean) obj;
return Boolean.parseBoolean(obj.toString());
}
}

75
src/main/java/com/threecloud/dataserviceyy/util/OssFileService.java

@ -4,7 +4,7 @@ import com.threecloud.dataserviceyy.entity.OssFileResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
@ -13,18 +13,25 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
/**
* OSS文件存储服务客户端
*
* 基于新OSS文件存储服务API对接提供文件上传预览等功能
* API文档地址: file:sanduoyun/developspace/dataservice-yy/OSS文件存储服务-技术对接文档.md
*
* 配置项:
* oss.base-url: OSS服务基础地址 http://53.1.211.7)
* oss.base-url: OSS服务基础地址 http://53.1.211.7/apiOss
* oss.upload-path: 上传存储路径 voice/record/
*/
@Component
@ -32,7 +39,7 @@ public class OssFileService {
private static final Logger logger = LoggerFactory.getLogger(OssFileService.class);
@Value("${oss.base-url:http://53.1.211.7}")
@Value("${oss.base-url:http://53.1.211.7/apiOss}")
private String ossBaseUrl;
@Value("${oss.upload-path:voice/record/}")
@ -48,29 +55,28 @@ public class OssFileService {
}
/**
* 上传文件到OSS
* 上传文件到OSS通过文件路径避免大文件内存溢出
*
* 调用 POST /oss/upload 接口
* 存储路径格式: {uploadPath}/{fileName}
* 示例: voice/record/xxx.wav
*
* @param fileData 文件字节数组
* @param file 本地文件
* @param fileName 文件名 xxx.wav
* @return OssFileResponse 包含 fileUrl预览地址 fileKey文件标识
*/
public OssFileResponse uploadFile(byte[] fileData, String fileName) {
return uploadFile(fileData, fileName, defaultUploadPath);
public OssFileResponse uploadFile(File file, String fileName) {
return uploadFile(file, fileName, defaultUploadPath);
}
/**
* 上传文件到OSS指定存储路径
*
* @param fileData 文件字节数组
* @param file 本地文件
* @param fileName 文件名
* @param uploadPath 存储路径 voice/record/
* @return OssFileResponse
*/
public OssFileResponse uploadFile(byte[] fileData, String fileName, String uploadPath) {
public OssFileResponse uploadFile(File file, String fileName, String uploadPath) {
String url = ossBaseUrl + "/oss/upload";
String path = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
@ -78,12 +84,7 @@ public class OssFileService {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
ByteArrayResource resource = new ByteArrayResource(fileData) {
@Override
public String getFilename() {
return fileName;
}
};
FileSystemResource resource = new FileSystemResource(file);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", resource);
@ -91,7 +92,8 @@ public class OssFileService {
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
logger.debug("上传文件到OSS: url={}, path={}, fileName={}, size={} bytes", url, path, fileName, fileData.length);
logger.debug("上传文件到OSS: url={}, path={}, fileName={}, size={} bytes",
url, path, fileName, file.length());
ResponseEntity<OssFileResponse> response = restTemplate.exchange(
url,
HttpMethod.POST,
@ -131,26 +133,45 @@ public class OssFileService {
}
/**
* 创建支持自定义SSL配置的RestTemplate
* 创建支持自定义SSL配置和超时设置的RestTemplate
*/
private RestTemplate createRestTemplate() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
// 超时配置
connection.setConnectTimeout(15000); // 连接超时15秒
connection.setReadTimeout(300000); // 读取超时5分钟(大文件上传)
// SSL信任配置
if (ossBaseUrl.startsWith("https") && connection instanceof HttpsURLConnection) {
HttpsURLConnection httpsConn = (HttpsURLConnection) connection;
try {
javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[]{
new javax.net.ssl.X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
TrustManager[] trustAllCerts = 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) {
}
}
};
javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new SecureRandom());
httpsConn.setSSLSocketFactory(sc.getSocketFactory());
httpsConn.setHostnameVerifier((hostname, session) -> true);
httpsConn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
} catch (Exception e) {
logger.warn("SSL配置失败: {}", e.getMessage());
}

10
src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java

@ -144,6 +144,9 @@ public class VaaHttpUtil {
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
// 获取服务器声明的文件大小,用于完整性校验
long expectedSize = conn.getContentLengthLong();
inputStream = conn.getInputStream();
outputStream = new FileOutputStream(savePath);
@ -159,6 +162,13 @@ public class VaaHttpUtil {
outputStream.flush();
logger.info("录音文件下载完成: {}, 大小: {} bytes ({} MB)",
savePath, totalBytes, totalBytes / 1024 / 1024);
// 完整性校验:如果服务器返回了Content-Length,校验实际下载大小
if (expectedSize > 0 && totalBytes != expectedSize) {
saveFile.delete();
throw new RuntimeException(String.format(
"文件下载不完整: 期望 %d bytes, 实际 %d bytes", expectedSize, totalBytes));
}
} else {
throw new RuntimeException("文件下载失败,HTTP状态码: " + responseCode);
}

45
src/main/resources/logback-spring.xml

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志目录 -->
<property name="LOG_PATH" value="logs"/>
<!-- 日志文件名 -->
<property name="LOG_FILE" value="app"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - %msg%n</pattern>
</encoder>
</appender>
<!-- 每天一个日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天生成一个日志文件 -->
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留最近30天的日志 -->
<maxHistory>30</maxHistory>
<!-- 所有日志文件总大小上限 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别 -->
<logger name="com.threecloud.dataserviceyy" level="DEBUG"/>
<logger name="com.threecloud.dataserviceyy.service.VaaSyncService" level="DEBUG"/>
<logger name="com.threecloud.dataserviceyy.util.VaaHttpUtil" level="DEBUG"/>
<logger name="com.threecloud.dataserviceyy.mapper" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

75
src/main/resources/mapper/FtpProcessedFileMapper.xml

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.threecloud.dataserviceyy.mapper.FtpProcessedFileMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.FtpProcessedFile">
<id column="id" property="id"/>
<result column="city_code" property="cityCode"/>
<result column="city_name" property="cityName"/>
<result column="file_name" property="fileName"/>
<result column="file_size" property="fileSize"/>
<result column="process_status" property="processStatus"/>
<result column="record_count" property="recordCount"/>
<result column="success_count" property="successCount"/>
<result column="fail_count" property="failCount"/>
<result column="process_time" property="processTime"/>
<result column="error_msg" property="errorMsg"/>
<result column="create_time" property="createTime"/>
</resultMap>
<!-- 插入处理记录 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO mid_ftp_processed_file (
city_code, city_name, file_name, file_size, process_status,
record_count, success_count, fail_count, process_time, error_msg, create_time
) VALUES (
#{cityCode}, #{cityName}, #{fileName}, #{fileSize}, #{processStatus},
#{recordCount}, #{successCount}, #{failCount}, #{processTime}, #{errorMsg}, NOW()
)
</insert>
<!-- 根据地市编码和文件名查询 -->
<select id="selectByCityAndFileName" resultMap="BaseResultMap">
SELECT * FROM mid_ftp_processed_file
WHERE city_code = #{cityCode}
AND file_name = #{fileName}
LIMIT 1
</select>
<!-- 查询指定地市下所有已处理的文件名列表 -->
<select id="selectProcessedFileNamesByCity" resultType="java.lang.String">
SELECT file_name FROM mid_ftp_processed_file
WHERE city_code = #{cityCode}
AND process_status = '1'
</select>
<!-- 批量查询指定地市下多个文件的处理状态 -->
<select id="selectByCityAndFileNames" resultMap="BaseResultMap">
SELECT * FROM mid_ftp_processed_file
WHERE city_code = #{cityCode}
AND file_name IN
<foreach collection="fileNames" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</select>
<!-- 根据ID更新处理结果 -->
<update id="updateById">
UPDATE mid_ftp_processed_file
SET process_status = #{processStatus},
record_count = #{recordCount},
success_count = #{successCount},
fail_count = #{failCount},
process_time = #{processTime},
error_msg = #{errorMsg}
WHERE id = #{id}
</update>
<!-- 删除指定时间之前的记录 -->
<delete id="deleteBeforeTime">
DELETE FROM mid_ftp_processed_file
WHERE city_code = #{cityCode}
AND create_time &lt; TO_TIMESTAMP(#{beforeTime}, 'YYYY-MM-DD HH24:MI:SS')
</delete>
</mapper>

85
src/main/resources/mapper/MidVoiceCallRecordMapper.xml

@ -41,18 +41,6 @@
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record WHERE id = #{id}
</select>
<!-- 根据通话记录ID查询(防止重复) -->
<select id="selectByCallRecordId" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE call_record_id = #{callRecordId}
LIMIT 1
</select>
<!-- 批量查询某设备在时间范围内已存在的call_record_id(防重复,一次查询代替N次) -->
<select id="selectExistingCallRecordIds" resultType="java.lang.String">
SELECT call_record_id FROM mid_voice_call_record
@ -61,75 +49,4 @@
AND call_start_time &lt;= TO_TIMESTAMP(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
</select>
<!-- 根据设备编码查询 -->
<select id="selectByDeviceNo" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE device_no = #{deviceNo}
ORDER BY call_start_time DESC
</select>
<!-- 分页查询 -->
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE 1=1
<if test="cityCode != null and cityCode != ''">
AND city_code = #{cityCode}
</if>
<if test="deviceNo != null and deviceNo != ''">
AND device_no = #{deviceNo}
</if>
<if test="callTel != null and callTel != ''">
AND call_tel LIKE CONCAT('%', #{callTel}, '%')
</if>
<if test="calledTel != null and calledTel != ''">
AND called_tel LIKE CONCAT('%', #{calledTel}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
ORDER BY call_start_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 统计总数 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM mid_voice_call_record
WHERE 1=1
<if test="cityCode != null and cityCode != ''">
AND city_code = #{cityCode}
</if>
<if test="deviceNo != null and deviceNo != ''">
AND device_no = #{deviceNo}
</if>
<if test="callTel != null and callTel != ''">
AND call_tel LIKE CONCAT('%', #{callTel}, '%')
</if>
<if test="calledTel != null and calledTel != ''">
AND called_tel LIKE CONCAT('%', #{calledTel}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
</select>
<!-- 更新文件路径 -->
<update id="updateFilePath">
UPDATE mid_voice_call_record
SET recording_file_path = #{recordingFilePath}, sync_time = NOW()
WHERE id = #{id}
</update>
<!-- 更新状态 -->
<update id="updateStatus">
UPDATE mid_voice_call_record
SET call_status = #{callStatus}, fail_reason = #{failReason}, sync_time = NOW()
WHERE id = #{id}
</update>
</mapper>
</mapper>

85
src/main/resources/mapper/SyncLogMapper.xml

@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.threecloud.dataserviceyy.mapper.SyncLogMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.SyncLog">
<id column="id" property="id"/>
<result column="device_id" property="deviceId"/>
<result column="device_name" property="deviceName"/>
<result column="start_time" property="startTime"/>
<result column="end_time" property="endTime"/>
<result column="total_count" property="totalCount"/>
<result column="success_count" property="successCount"/>
<result column="fail_count" property="failCount"/>
<result column="status" property="status"/>
<result column="error_msg" property="errorMsg"/>
<result column="create_time" property="createTime"/>
</resultMap>
<!-- 插入同步日志 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO sync_log (
device_id, device_name, start_time, end_time, total_count,
success_count, fail_count, status, error_msg, create_time
) VALUES (
#{deviceId}, #{deviceName}, #{startTime}, #{endTime}, #{totalCount},
#{successCount}, #{failCount}, #{status}, #{errorMsg}, NOW()
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM sync_log WHERE id = #{id}
</select>
<!-- 查询设备的同步日志 -->
<select id="selectByDeviceId" resultMap="BaseResultMap">
SELECT * FROM sync_log
WHERE device_id = #{deviceId}
ORDER BY create_time DESC
</select>
<!-- 分页查询 -->
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM sync_log
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="startTime != null and startTime != ''">
AND start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND start_time &lt;= #{endTime}
</if>
ORDER BY create_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 统计总数 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM sync_log
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="startTime != null and startTime != ''">
AND start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND start_time &lt;= #{endTime}
</if>
</select>
<!-- 更新同步结果 -->
<update id="updateResult">
UPDATE sync_log
SET end_time = #{endTime},
success_count = #{successCount},
fail_count = #{failCount},
status = #{status},
error_msg = #{errorMsg}
WHERE id = #{id}
</update>
</mapper>

117
src/main/resources/mapper/VoiceRecordMapper.xml

@ -1,117 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.threecloud.dataserviceyy.mapper.VoiceRecordMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.VoiceRecord">
<id column="id" property="id"/>
<result column="device_id" property="deviceId"/>
<result column="device_uuid" property="deviceUuid"/>
<result column="device_name" property="deviceName"/>
<result column="record_id" property="recordId"/>
<result column="file_name" property="fileName"/>
<result column="file_path" property="filePath"/>
<result column="oss_url" property="ossUrl"/>
<result column="local_path" property="localPath"/>
<result column="call_start_time" property="callStartTime"/>
<result column="call_end_time" property="callEndTime"/>
<result column="duration" property="duration"/>
<result column="channel" property="channel"/>
<result column="phone" property="phone"/>
<result column="direction" property="direction"/>
<result column="status" property="status"/>
<result column="fail_reason" property="failReason"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<!-- 插入录音记录 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO voice_record (
device_id, device_uuid, device_name, record_id, file_name, file_path,
oss_url, local_path, call_start_time, call_end_time, duration,
channel, phone, direction, status, fail_reason, create_time, update_time
) VALUES (
#{deviceId}, #{deviceUuid}, #{deviceName}, #{recordId}, #{fileName}, #{filePath},
#{ossUrl}, #{localPath}, #{callStartTime}, #{callEndTime}, #{duration},
#{channel}, #{phone}, #{direction}, #{status}, #{failReason}, NOW(), NOW()
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM voice_record WHERE id = #{id}
</select>
<!-- 根据设备ID和录音ID查询(防止重复) -->
<select id="selectByDeviceAndRecordId" resultMap="BaseResultMap">
SELECT * FROM voice_record
WHERE device_id = #{deviceId} AND record_id = #{recordId}
LIMIT 1
</select>
<!-- 查询设备的所有录音 -->
<select id="selectByDeviceId" resultMap="BaseResultMap">
SELECT * FROM voice_record
WHERE device_id = #{deviceId}
ORDER BY call_start_time DESC
</select>
<!-- 分页查询 -->
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM voice_record
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="deviceName != null and deviceName != ''">
AND device_name LIKE CONCAT('%', #{deviceName}, '%')
</if>
<if test="phone != null and phone != ''">
AND phone LIKE CONCAT('%', #{phone}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
ORDER BY call_start_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 统计总数 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM voice_record
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="deviceName != null and deviceName != ''">
AND device_name LIKE CONCAT('%', #{deviceName}, '%')
</if>
<if test="phone != null and phone != ''">
AND phone LIKE CONCAT('%', #{phone}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
</select>
<!-- 更新OSS地址 -->
<update id="updateOssUrl">
UPDATE voice_record
SET oss_url = #{ossUrl}, update_time = NOW()
WHERE id = #{id}
</update>
<!-- 更新状态 -->
<update id="updateStatus">
UPDATE voice_record
SET status = #{status}, fail_reason = #{failReason}, update_time = NOW()
WHERE id = #{id}
</update>
</mapper>
Loading…
Cancel
Save