Browse Source

feat (1.0) 增加同步单个录音的接口

main
hwrdt 3 weeks ago
parent
commit
a5b21d99a8
  1. 36
      src/main/java/com/threecloud/dataserviceyy/controller/VoiceSyncController.java
  2. 61
      src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceResyncLog.java
  3. 5
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java
  4. 24
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceResyncLogMapper.java
  5. 6
      src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java
  6. 147
      src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java
  7. 6
      src/main/resources/mapper/MidVoiceCallRecordMapper.xml
  8. 16
      src/main/resources/mapper/VoiceSyncMapper.xml
  9. 25
      src/main/resources/sql/mid_voice_resync_log.sql

36
src/main/java/com/threecloud/dataserviceyy/controller/VoiceSyncController.java

@ -0,0 +1,36 @@
package com.threecloud.dataserviceyy.controller;
import com.threecloud.dataserviceyy.entity.ResultEntity;
import com.threecloud.dataserviceyy.service.VaaSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 语音同步接口
*/
@RestController
@RequestMapping("/voice")
public class VoiceSyncController {
@Autowired
private VaaSyncService vaaSyncService;
/**
* 根据通话记录编号重新同步录音文件
*
* @param callRecordId 通话记录编号
* @return 操作结果
*/
@GetMapping("/resync")
public ResultEntity resync(@RequestParam("callRecordId") String callRecordId) {
try {
String msg = vaaSyncService.resyncByCallRecordId(callRecordId);
return new ResultEntity(ResultEntity.StatusCode.SUCCESS.getCode(), msg, null);
} catch (Exception e) {
return new ResultEntity(ResultEntity.StatusCode.FAILURE.getCode(), e.getMessage(), null);
}
}
}

61
src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceResyncLog.java

@ -0,0 +1,61 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* 录音文件重新同步操作记录表
* 对应 mid_voice.mid_voice_resync_log
*
* 用于记录按通话记录编号重新同步录音文件的操作便于追溯排查
*/
public class MidVoiceResyncLog {
/** 自增ID主键 */
private Long id;
/** 通话记录编号 */
private String callRecordId;
/** 设备编号 */
private String deviceNo;
/** 录音文件名 */
private String recordingFileName;
/** 重新上传后的OSS文件路径 */
private String ossFilePath;
/** 同步状态:1成功,0失败 */
private String resyncStatus;
/** 失败原因 */
private String failReason;
/** 创建时间 */
private Date createTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCallRecordId() { return callRecordId; }
public void setCallRecordId(String callRecordId) { this.callRecordId = callRecordId; }
public String getDeviceNo() { return deviceNo; }
public void setDeviceNo(String deviceNo) { this.deviceNo = deviceNo; }
public String getRecordingFileName() { return recordingFileName; }
public void setRecordingFileName(String recordingFileName) { this.recordingFileName = recordingFileName; }
public String getOssFilePath() { return ossFilePath; }
public void setOssFilePath(String ossFilePath) { this.ossFilePath = ossFilePath; }
public String getResyncStatus() { return resyncStatus; }
public void setResyncStatus(String resyncStatus) { this.resyncStatus = resyncStatus; }
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; }
}

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

@ -24,4 +24,9 @@ public interface MidVoiceCallRecordMapper {
List<String> selectExistingCallRecordIds(@Param("deviceNo") String deviceNo, List<String> selectExistingCallRecordIds(@Param("deviceNo") String deviceNo,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime); @Param("endTime") String endTime);
/**
* 根据通话记录编号查询单条通话记录
*/
MidVoiceCallRecord selectByCallRecordId(@Param("callRecordId") String callRecordId);
} }

24
src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceResyncLogMapper.java

@ -0,0 +1,24 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.MidVoiceResyncLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
/**
* 录音文件重新同步操作记录 Mapper
*/
@Mapper
public interface MidVoiceResyncLogMapper {
/**
* 插入重新同步操作记录
*/
@Insert("INSERT INTO mid_voice_resync_log (" +
"call_record_id, device_no, recording_file_name, oss_file_path, " +
"resync_status, fail_reason, create_time" +
") VALUES (" +
"#{callRecordId}, #{deviceNo}, #{recordingFileName}, #{ossFilePath}, " +
"#{resyncStatus}, #{failReason}, #{createTime}" +
")")
int insert(MidVoiceResyncLog log);
}

6
src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java

@ -1,6 +1,7 @@
package com.threecloud.dataserviceyy.mapper; package com.threecloud.dataserviceyy.mapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -16,4 +17,9 @@ public interface VoiceSyncMapper {
* 查询所有在线语音设备含每个设备的账号密码 * 查询所有在线语音设备含每个设备的账号密码
*/ */
List<Map<String, Object>> getAllYysb(); List<Map<String, Object>> getAllYysb();
/**
* 根据设备编号查询单个语音设备含账号密码
*/
Map<String, Object> getDeviceByNo(@Param("deviceNo") String deviceNo);
} }

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

@ -4,9 +4,11 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.threecloud.dataserviceyy.entity.MidVoiceCallRecord; import com.threecloud.dataserviceyy.entity.MidVoiceCallRecord;
import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog; import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog;
import com.threecloud.dataserviceyy.entity.MidVoiceResyncLog;
import com.threecloud.dataserviceyy.entity.OssFileResponse; import com.threecloud.dataserviceyy.entity.OssFileResponse;
import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper; import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper;
import com.threecloud.dataserviceyy.mapper.MidVoiceDeviceLogMapper; import com.threecloud.dataserviceyy.mapper.MidVoiceDeviceLogMapper;
import com.threecloud.dataserviceyy.mapper.MidVoiceResyncLogMapper;
import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper; import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper;
import com.threecloud.dataserviceyy.util.*; import com.threecloud.dataserviceyy.util.*;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -45,6 +47,8 @@ public class VaaSyncService {
@Autowired @Autowired
private MidVoiceDeviceLogMapper deviceLogMapper; private MidVoiceDeviceLogMapper deviceLogMapper;
@Autowired @Autowired
private MidVoiceResyncLogMapper resyncLogMapper;
@Autowired
private VaaHttpUtil vaaHttpUtil; private VaaHttpUtil vaaHttpUtil;
@Autowired @Autowired
private OssFileService ossFileService; private OssFileService ossFileService;
@ -97,6 +101,149 @@ public class VaaSyncService {
} }
} }
/**
* 根据通话记录编号重新同步录音文件
*
* 说明只重新下载录音文件并上传OSS不修改 mid_voice_call_record 业务数据
* 流程查通话记录 查设备 登录 按时间范围查录音列表 匹配记录 下载 上传OSS 记录操作日志
*
* @param callRecordId 通话记录编号
* @return 成功提示信息
* @throws RuntimeException 重新同步失败已记录操作日志
*/
public String resyncByCallRecordId(String callRecordId) {
String deviceNo = null;
String fileName = null;
String ossFilePath = null;
try {
if (!StringUtils.hasText(callRecordId)) {
throw new IllegalArgumentException("通话记录编号为空");
}
// 1. 查询通话记录
MidVoiceCallRecord record = callRecordMapper.selectByCallRecordId(callRecordId);
if (record == null) {
throw new IllegalArgumentException("通话记录不存在: " + callRecordId);
}
deviceNo = record.getDeviceNo();
fileName = record.getRecordingFileName();
Date callStartTime = record.getCallStartTime();
Date callEndTime = record.getCallEndTime();
if (callStartTime == null) {
throw new IllegalStateException("通话记录缺少开始时间: " + callRecordId);
}
// 2. 查询设备
Map<String, Object> device = voiceSyncMapper.getDeviceByNo(deviceNo);
if (device == null || device.isEmpty()) {
throw new IllegalStateException("语音设备不存在或未配置: " + deviceNo);
}
String cityCode = getStr(device, "ORGAN_ID");
String ip = getStr(device, "IP");
Integer port = getInt(device, "PORT", 80);
String username = getStr(device, "USERNAME");
String password = getStr(device, "PASSWORD");
if (!StringUtils.hasText(ip)) {
throw new IllegalStateException("设备IP为空: " + deviceNo);
}
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
throw new IllegalStateException("设备账号密码未配置: " + deviceNo);
}
String deviceHost = buildHost(ip, port);
// 3. 登录
String authToken = loginDevice(deviceHost, username, password);
// 4. 反推录音盒上的录音ID
String recordId = extractRecordId(callRecordId, deviceNo);
// 5. 按通话时间前后各扩展1小时查询录音列表
long startEpoch = callStartTime.getTime() / 1000 - 3600;
long endEpoch = (callEndTime != null ? callEndTime.getTime() : callStartTime.getTime()) / 1000 + 3600;
String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]",
deviceHost, startEpoch, endEpoch);
String recordData = httpVisitWithReauth(recordUrl, authToken, deviceHost, username, password);
JSONArray records = vaaHttpUtil.parseRecordData(recordData);
// 6. 匹配录音记录
JSONObject matched = null;
for (int i = 0; i < records.size(); i++) {
JSONObject rec = records.getJSONObject(i);
if (recordId.equals(RecordParser.parseRecordId(rec))) {
matched = rec;
break;
}
}
if (matched == null) {
throw new IllegalStateException("录音盒上未找到该录音: " + recordId);
}
String filePath = RecordParser.parseFilePath(matched);
if (filePath == null || filePath.isEmpty()) {
throw new IllegalStateException("录音文件路径为空: " + recordId);
}
// 7. 强制重新下载录音文件
String localPath = FilePathUtil.buildLocalPath(downloadPath, cityCode, callStartTime, deviceNo, fileName);
Path localFile = Paths.get(localPath);
Files.deleteIfExists(localFile);
String fileUrl = "http://" + deviceHost + filePath;
vaaHttpUtil.httpDown(fileUrl, localPath, authToken);
logger.info("【重新同步】录音下载完成: {}", fileName);
// 8. 上传OSS(路径与原记录一致,覆盖同名文件,业务表无需更新)
String ossPath = "voice/" + cityCode + "/" + new SimpleDateFormat("yyyy-MM-dd").format(callStartTime) + "/";
OssFileResponse ossResponse = ossFileService.uploadFile(localFile.toFile(), fileName, ossPath);
String previewUrl = ossResponse.getFileUrl();
if (previewUrl == null || previewUrl.trim().isEmpty()) {
throw new IllegalStateException("OSS上传成功但未返回文件地址: " + fileName);
}
ossFilePath = previewUrl;
// 9. 记录成功操作日志
saveResyncLog(callRecordId, deviceNo, fileName, previewUrl, "1", null);
logger.info("【重新同步】成功: callRecordId={}, ossUrl={}", callRecordId, previewUrl);
return "重新同步成功: " + callRecordId + " -> " + previewUrl;
} catch (Exception e) {
logger.error("【重新同步】失败: callRecordId={}, 原因={}", callRecordId, e.getMessage(), e);
saveResyncLog(callRecordId, deviceNo, fileName, ossFilePath, "0", e.getMessage());
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException("重新同步失败: " + e.getMessage(), e);
}
}
/**
* call_record_id格式设备编号_录音ID中提取录音盒上的录音ID
*/
private String extractRecordId(String callRecordId, String deviceNo) {
if (deviceNo != null && callRecordId.startsWith(deviceNo + "_")) {
return callRecordId.substring(deviceNo.length() + 1);
}
return callRecordId;
}
/**
* 保存重新同步操作记录到 mid_voice_resync_log
*/
private void saveResyncLog(String callRecordId, String deviceNo, String fileName,
String ossFilePath, String resyncStatus, String failReason) {
try {
MidVoiceResyncLog log = new MidVoiceResyncLog();
log.setCallRecordId(callRecordId);
log.setDeviceNo(deviceNo);
log.setRecordingFileName(fileName);
log.setOssFilePath(ossFilePath);
log.setResyncStatus(resyncStatus);
log.setFailReason(failReason);
log.setCreateTime(new Date());
resyncLogMapper.insert(log);
} catch (Exception e) {
logger.error("保存重新同步操作日志失败: callRecordId={}, 原因={}", callRecordId, e.getMessage());
}
}
/** /**
* 同步单个设备 * 同步单个设备
* *

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

@ -49,4 +49,10 @@
AND call_start_time &lt;= TO_TIMESTAMP(#{endTime}, 'YYYY-MM-DD HH24:MI:SS') AND call_start_time &lt;= TO_TIMESTAMP(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
</select> </select>
<!-- 根据通话记录编号查询单条通话记录 -->
<select id="selectByCallRecordId" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE call_record_id = #{callRecordId}
</select>
</mapper> </mapper>

16
src/main/resources/mapper/VoiceSyncMapper.xml

@ -17,4 +17,20 @@
WHERE device_status = '1' WHERE device_status = '1'
</select> </select>
<!-- 根据设备编号查询单个语音设备(含账号密码) -->
<select id="getDeviceByNo" resultType="java.util.Map">
SELECT id AS ID,
device_no AS UUID,
city_name AS ORGAN_NAME,
city_code AS ORGAN_ID,
ip_address AS IP,
device_port AS PORT,
org_code AS ORG_CODE,
username AS USERNAME,
password AS PASSWORD
FROM mid_voice.mid_voice_device_config
WHERE device_no = #{deviceNo}
LIMIT 1
</select>
</mapper> </mapper>

25
src/main/resources/sql/mid_voice_resync_log.sql

@ -0,0 +1,25 @@
-- ============================================
-- 录音文件重新同步操作记录表
-- 用于记录按通话记录编号重新同步录音文件的操作
-- 数据库:Kingbase8(兼容 PostgreSQL)
-- ============================================
CREATE TABLE IF NOT EXISTS mid_voice.mid_voice_resync_log (
id BIGSERIAL PRIMARY KEY, -- 自增主键
call_record_id VARCHAR(128), -- 通话记录编号
device_no VARCHAR(64), -- 设备编号
recording_file_name VARCHAR(255), -- 录音文件名
oss_file_path VARCHAR(512), -- 重新上传后的OSS文件路径
resync_status VARCHAR(2), -- 同步状态:1成功,0失败
fail_reason VARCHAR(512), -- 失败原因
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 操作时间
);
COMMENT ON TABLE mid_voice.mid_voice_resync_log IS '录音文件重新同步操作记录表';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.call_record_id IS '通话记录编号';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.device_no IS '设备编号';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.recording_file_name IS '录音文件名';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.oss_file_path IS '重新上传后的OSS文件路径';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.resync_status IS '同步状态:1成功,0失败';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.fail_reason IS '失败原因';
COMMENT ON COLUMN mid_voice.mid_voice_resync_log.create_time IS '操作时间';
Loading…
Cancel
Save