diff --git a/src/main/java/com/threecloud/dataserviceyy/controller/VoiceSyncController.java b/src/main/java/com/threecloud/dataserviceyy/controller/VoiceSyncController.java new file mode 100644 index 0000000..849e434 --- /dev/null +++ b/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); + } + } +} diff --git a/src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceResyncLog.java b/src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceResyncLog.java new file mode 100644 index 0000000..8f78830 --- /dev/null +++ b/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; } +} diff --git a/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java b/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java index 46ffe5f..456baee 100644 --- a/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java +++ b/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java @@ -24,4 +24,9 @@ public interface MidVoiceCallRecordMapper { List selectExistingCallRecordIds(@Param("deviceNo") String deviceNo, @Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 根据通话记录编号查询单条通话记录 + */ + MidVoiceCallRecord selectByCallRecordId(@Param("callRecordId") String callRecordId); } \ No newline at end of file diff --git a/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceResyncLogMapper.java b/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceResyncLogMapper.java new file mode 100644 index 0000000..ada6fa7 --- /dev/null +++ b/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); +} diff --git a/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java b/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java index ea0942b..ad18915 100644 --- a/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java +++ b/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java @@ -1,6 +1,7 @@ package com.threecloud.dataserviceyy.mapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; @@ -16,4 +17,9 @@ public interface VoiceSyncMapper { * 查询所有在线语音设备(含每个设备的账号密码) */ List> getAllYysb(); + + /** + * 根据设备编号查询单个语音设备(含账号密码) + */ + Map getDeviceByNo(@Param("deviceNo") String deviceNo); } diff --git a/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java b/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java index 6d012da..535ed5b 100644 --- a/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java +++ b/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.threecloud.dataserviceyy.entity.MidVoiceCallRecord; import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog; +import com.threecloud.dataserviceyy.entity.MidVoiceResyncLog; import com.threecloud.dataserviceyy.entity.OssFileResponse; import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper; import com.threecloud.dataserviceyy.mapper.MidVoiceDeviceLogMapper; +import com.threecloud.dataserviceyy.mapper.MidVoiceResyncLogMapper; import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper; import com.threecloud.dataserviceyy.util.*; import org.slf4j.Logger; @@ -45,6 +47,8 @@ public class VaaSyncService { @Autowired private MidVoiceDeviceLogMapper deviceLogMapper; @Autowired + private MidVoiceResyncLogMapper resyncLogMapper; + @Autowired private VaaHttpUtil vaaHttpUtil; @Autowired 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 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()); + } + } + /** * 同步单个设备 * diff --git a/src/main/resources/mapper/MidVoiceCallRecordMapper.xml b/src/main/resources/mapper/MidVoiceCallRecordMapper.xml index 111e7a2..25695af 100644 --- a/src/main/resources/mapper/MidVoiceCallRecordMapper.xml +++ b/src/main/resources/mapper/MidVoiceCallRecordMapper.xml @@ -49,4 +49,10 @@ AND call_start_time <= TO_TIMESTAMP(#{endTime}, 'YYYY-MM-DD HH24:MI:SS') + + + \ No newline at end of file diff --git a/src/main/resources/mapper/VoiceSyncMapper.xml b/src/main/resources/mapper/VoiceSyncMapper.xml index eb49c32..8c932a3 100644 --- a/src/main/resources/mapper/VoiceSyncMapper.xml +++ b/src/main/resources/mapper/VoiceSyncMapper.xml @@ -17,4 +17,20 @@ WHERE device_status = '1' + + + diff --git a/src/main/resources/sql/mid_voice_resync_log.sql b/src/main/resources/sql/mid_voice_resync_log.sql new file mode 100644 index 0000000..be4fd9e --- /dev/null +++ b/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 '操作时间';