diff --git a/config/application-external.yml b/config/application-external.yml index c220ce8..243c6cc 100644 --- a/config/application-external.yml +++ b/config/application-external.yml @@ -5,72 +5,47 @@ # 1. 此文件用于部署时覆盖默认配置,无需重新打包 JAR # 2. 将此文件放在 JAR 包同级目录的 config/ 文件夹下 # 3. 修改此文件后重启服务即可生效 +# 4. 每个录音盒的账号密码在 mid_voice_device_config 表中配置 # ============================================ spring: # ==================== 数据库配置 ==================== datasource: - # 人大金仓数据库驱动(一般无需修改) driver-class-name: com.kingbase8.Driver - - # 数据库连接URL - # 格式: jdbc:kingbase8://{host}:{port}/{database}?currentSchema={schema}&clientEncoding=utf8 - # 示例: jdbc:kingbase8://53.1.194.60:54321/kingbase?currentSchema=mid_voice&clientEncoding=utf8 url: jdbc:kingbase8://127.0.0.1:54321/kingbase?currentSchema=mid_voice&clientEncoding=utf8 - - # 数据库用户名 username: dcms_dev - - # 数据库密码 password: your_password_here # ==================== 语音同步配置 ==================== vaa-sync: - # 本地录音文件存储路径(相对路径或绝对路径) - # 示例: ./vaa-recordings 或 /opt/dataservice-yy/vaa-recordings + # 本地录音文件存储路径 download-path: ./vaa-recordings - + + # 本地文件保留天数 + retain-days: 10 + # 同步定时任务 Cron 表达式 - # 默认每2小时执行一次: 0 0 0/2 * * ? - # 每30分钟执行: 0 0/30 * * * ? - # 每天凌晨2点执行: 0 0 2 * * ? - # 每分钟执行(测试用): 0 0/1 * * * ? + # 每2小时: 0 0 0/2 * * ? + # 每30分钟: 0 0/30 * * * ? + # 每分钟(测试): 0 0/1 * * * ? sync-interval-cron: "0 0 0/2 * * ?" - - # 录音盒登录账号密码 - device-username: admin - device-password: admin - + # OSS 文件上传配置 oss: - # OSS 服务基础地址(用于拼接完整URL) - # 示例: http://53.1.194.59:9090 base-url: http://127.0.0.1:9090 - - # OSS 上传接口完整地址 - # 示例: http://53.1.194.59:9090/apiOss/oss/fileUpload upload-url: http://127.0.0.1:9090/apiOss/oss/fileUpload - - # OSS 认证信息(请向管理员索取) appcode: dataservice-yy appid: your_appid_here appsecret: your_appsecret_here -# ==================== 服务端口配置 ==================== +# ==================== 服务端口 ==================== server: - # 服务端口 port: 8088 # ==================== 日志配置 ==================== logging: - # 日志文件路径 file: name: logs/app.log - - # 日志级别 level: - # 根日志级别: INFO(生产) / DEBUG(测试) root: INFO - - # 本项目代码日志级别 com.threecloud.dataserviceyy: DEBUG diff --git a/src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceChannelConfig.java b/src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceChannelConfig.java deleted file mode 100644 index b8d7515..0000000 --- a/src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceChannelConfig.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.threecloud.dataserviceyy.entity; - -import java.util.Date; - -/** - * 语音设备通道配置表 - * 对应 mid_voice.mid_voice_channel_config - * 用于维护每个录音盒的通道信息(通道号、绑定号码等) - */ -public class MidVoiceChannelConfig { - - /** 自增ID主键 */ - private Long id; - - /** 地市编码 */ - private String cityCode; - - /** 地市名称 */ - private String cityName; - - /** 设备编码(关联 mid_voice_device_config.device_no) */ - private String deviceNo; - - /** 通道号 1-8 */ - private Integer channelNo; - - /** 通道绑定的电话号码 */ - private String phoneNumber; - - /** 通道名称/描述 */ - private String channelName; - - /** 通道状态:0离线,1在线,2故障 */ - private String channelStatus; - - /** 创建时间 */ - private Date createTime; - - /** 更新时间 */ - private Date updateTime; - - /** 备注 */ - private String remarks; - - // 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 getDeviceNo() { return deviceNo; } - public void setDeviceNo(String deviceNo) { this.deviceNo = deviceNo; } - - public Integer getChannelNo() { return channelNo; } - public void setChannelNo(Integer channelNo) { this.channelNo = channelNo; } - - public String getPhoneNumber() { return phoneNumber; } - public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } - - public String getChannelName() { return channelName; } - public void setChannelName(String channelName) { this.channelName = channelName; } - - public String getChannelStatus() { return channelStatus; } - public void setChannelStatus(String channelStatus) { this.channelStatus = channelStatus; } - - 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; } - - public String getRemarks() { return remarks; } - public void setRemarks(String remarks) { this.remarks = remarks; } -} diff --git a/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceChannelConfigMapper.java b/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceChannelConfigMapper.java deleted file mode 100644 index 643d080..0000000 --- a/src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceChannelConfigMapper.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.threecloud.dataserviceyy.mapper; - -import com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * 语音设备通道配置Mapper - */ -@Mapper -public interface MidVoiceChannelConfigMapper { - - /** - * 插入通道配置 - */ - int insert(MidVoiceChannelConfig config); - - /** - * 根据ID查询 - */ - MidVoiceChannelConfig selectById(Long id); - - /** - * 根据设备编码和通道号查询 - */ - MidVoiceChannelConfig selectByDeviceAndChannel(@Param("deviceNo") String deviceNo, - @Param("channelNo") Integer channelNo); - - /** - * 查询设备的所有通道 - */ - List selectByDeviceNo(@Param("deviceNo") String deviceNo); - - /** - * 根据电话号码查询通道 - */ - MidVoiceChannelConfig selectByPhoneNumber(@Param("phoneNumber") String phoneNumber); - - /** - * 查询所有通道配置 - */ - List selectAll(); - - /** - * 更新通道配置 - */ - int update(MidVoiceChannelConfig config); - - /** - * 更新通道状态 - */ - int updateStatus(@Param("id") Long id, - @Param("channelStatus") String channelStatus); - - /** - * 删除通道配置 - */ - int deleteById(Long id); - - /** - * 批量插入 - */ - int batchInsert(@Param("list") List list); -} diff --git a/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java b/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java index a9fc1bc..ea0942b 100644 --- a/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java +++ b/src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java @@ -1,46 +1,19 @@ 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; +/** + * 语音设备 Mapper + * 查询 mid_voice_device_config 表 + */ @Mapper public interface VoiceSyncMapper { - List> getAllYysb(); - - Map getTdByPhone(@Param("phone") String phone); - - Map getTdByPhone2(@Param("zjhm") String zjhm, @Param("bjhm") String bjhm); - - Map getYysbByUuid(@Param("uuid") String uuid); - - void saveThjl(@Param("sbtdId") String sbtdId, - @Param("yysbId") String yysbId, - @Param("organId") Long organId, - @Param("organName") String organName, - @Param("phone") String phone, - @Param("kssj") String kssj, - @Param("jssj") String jssj, - @Param("lydz") String lydz, - @Param("zjhm") String zjhm, - @Param("bjhm") String bjhm, - @Param("thfx") String thfx, - @Param("thsc") Long thsc); - - // VAA同步相关方法 - Map getChannelByNumberAndUuid(@Param("channel") String channel, @Param("uuid") String uuid); - void updateChannelStatus(Map params); - - Object getLastSyncTime(@Param("code") String code); - - void saveSyncLog(Map params); - - Object checkThjlExists(@Param("thid") String thid); - - void insertThjl(Map params); - - void updateThjl(Map params); + /** + * 查询所有在线语音设备(含每个设备的账号密码) + */ + List> getAllYysb(); } diff --git a/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java b/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java index a1ecbe2..737e7f0 100644 --- a/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java +++ b/src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java @@ -3,10 +3,10 @@ package com.threecloud.dataserviceyy.service; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.threecloud.dataserviceyy.entity.MidVoiceCallRecord; -import com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig; +import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog; import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper; +import com.threecloud.dataserviceyy.mapper.MidVoiceDeviceLogMapper; import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper; -import com.threecloud.dataserviceyy.service.channel.ChannelConfigService; import com.threecloud.dataserviceyy.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,93 +16,42 @@ 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.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; /** * VAA录音盒定时同步服务 - * - * 【功能说明】 - * 1. 定时从 EBOX 录音盒拉取录音记录 - * 2. 下载录音文件到本地(保留10天) - * 3. 上传录音文件到 OSS - * 4. 保存通话记录到 mid_voice_call_record 表 - * - * 【数据来源】 - * - 设备列表:mid_voice_device_config 表 - * - 通道配置:优先从 EBOX API 获取,失败则从数据库读取 - * - * 【同步策略】 - * - 每2小时执行一次(cron: 0 0 0/2 * * ?) - * - 增量同步:根据上次同步时间只拉取新录音 - * - 防重复:根据 device_no + record_id 判断 - * - * 【目录结构】 - * vaa-recordings/ - * ├── 20240101/ # 按日期分目录 - * │ └── / # 按设备分目录 - * │ └── xxx.wav - * └── .sync-marker/ # 同步时间标记 - * └── .time + * + * 【功能】从EBOX录音盒拉取录音 → 下载文件 → 上传OSS → 保存通话记录 + * 【策略】增量同步、无重试、防重复、失败记录到日志表 + * 【账户】每个设备的账号密码从 mid_voice_device_config 表读取 */ @Service public class VaaSyncService { private static final Logger logger = LoggerFactory.getLogger(VaaSyncService.class); - // ==================== 依赖注入 ==================== - @Autowired private VoiceSyncMapper voiceSyncMapper; - @Autowired private MidVoiceCallRecordMapper callRecordMapper; - @Autowired - private ChannelConfigService channelConfigService; - + private MidVoiceDeviceLogMapper deviceLogMapper; @Autowired private VaaHttpUtil vaaHttpUtil; - @Autowired private FileUploadUtil fileUploadUtil; - // ==================== 配置参数 ==================== - - /** 本地录音文件存储路径 */ @Value("${vaa-sync.download-path:./vaa-recordings}") private String downloadPath; - - /** 录音盒登录用户名 */ - @Value("${vaa-sync.device-username:admin}") - private String deviceUsername; - - /** 录音盒登录密码 */ - @Value("${vaa-sync.device-password:admin}") - private String devicePassword; - - /** 本地文件保留天数 */ @Value("${vaa-sync.retain-days:10}") private int retainDays; - // ==================== 定时任务 ==================== - /** - * 定时同步任务 - 每2小时执行一次 - * - * cron表达式说明:0 0 0/2 * * ? - * - 秒:0 - * - 分:0 - * - 时:0/2 表示从0点开始,每2小时 - * - 日:* 每天 - * - 月:* 每月 - * - 周:? 不指定 + * 定时同步任务 */ @Scheduled(cron = "${vaa-sync.sync-interval-cron:0 0 0/2 * * ?}") public void scheduledSync() { @@ -113,36 +62,23 @@ public class VaaSyncService { logger.info("【定时任务】========== VAA录音盒同步结束,耗时 {} 秒 ==========", costTime / 1000); } - // ==================== 核心同步逻辑 ==================== - /** * 执行同步任务(主入口) - * - * 执行流程: - * 1. 清理过期本地文件 - * 2. 查询所有在线设备 - * 3. 逐个设备同步 */ public void executeSync() { - logger.info("【主流程】开始执行VAA录音盒同步任务"); - try { - // 步骤1:清理过期本地文件 - logger.debug("【步骤1】清理 {} 天前的本地录音文件", retainDays); + // 清理过期本地文件 FileCleaner.cleanOldFiles(downloadPath, retainDays); - // 步骤2:查询设备列表 - logger.debug("【步骤2】查询在线设备列表"); + // 查询在线设备列表(含每个设备的账号密码) List> deviceList = voiceSyncMapper.getAllYysb(); - logger.info("【主流程】查询到 {} 个语音设备", deviceList.size()); + logger.info("【主流程】查询到 {} 个在线语音设备", deviceList.size()); - // 步骤3:逐个设备同步 int successCount = 0; int failCount = 0; for (int i = 0; i < deviceList.size(); i++) { Map device = deviceList.get(i); - String deviceId = getStringValue(device, "ID"); - logger.debug("【步骤3】处理第 {}/{} 个设备: ID={}", i + 1, deviceList.size(), deviceId); + String deviceId = getStr(device, "ID"); try { syncSingleDevice(device); successCount++; @@ -151,8 +87,7 @@ public class VaaSyncService { logger.error("【异常】设备同步失败: ID={}, 原因={}", deviceId, e.getMessage()); } } - - logger.info("【主流程】同步完成,成功 {} 个设备,失败 {} 个设备", successCount, failCount); + logger.info("【主流程】同步完成,成功 {} 个,失败 {} 个", successCount, failCount); } catch (Exception e) { logger.error("【异常】同步任务执行失败: {}", e.getMessage(), e); } @@ -160,388 +95,288 @@ public class VaaSyncService { /** * 同步单个设备 - * - * @param device 设备信息Map,包含: - * - ID: 设备ID - * - UUID: 设备编号(device_no) - * - ORGAN_NAME: 地市名称 - * - ORGAN_ID: 地市编码 - * - IP: IP地址 - * - PORT: 端口号 - * - ORG_CODE: 单位代码 + * + * 流程:登录 → 获取分机号码 → 查录音列表 → 逐条下载上传保存 + * 任何步骤失败直接记录日志,跳过该设备 */ private void syncSingleDevice(Map device) throws Exception { - // 提取设备信息 - String deviceId = getStringValue(device, "ID"); - String deviceNo = getStringValue(device, "UUID"); - String cityName = getStringValue(device, "ORGAN_NAME"); - String cityCode = getStringValue(device, "ORGAN_ID"); - String ip = getStringValue(device, "IP"); - Integer port = getIntValue(device, "PORT", 80); - String orgCode = getStringValue(device, "ORG_CODE"); - - logger.info("【设备】────────────────────────────────────────"); - logger.info("【设备】开始同步设备: ID={}, 编号={}, 机构={}, IP={}:{}", - deviceId, deviceNo, cityName, ip, port); + String deviceId = getStr(device, "ID"); + String deviceNo = getStr(device, "UUID"); + String cityName = getStr(device, "ORGAN_NAME"); + String cityCode = getStr(device, "ORGAN_ID"); + 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"); + + logger.info("【设备】同步设备: ID={}, 编号={}, 机构={}, IP={}:{}", deviceId, deviceNo, cityName, ip, port); // 参数校验 if (!StringUtils.hasText(ip)) { - logger.warn("【设备】设备IP为空,跳过同步: ID={}", deviceId); - return; - } - - // 机构名称兜底 - if (!StringUtils.hasText(cityName)) { - cityName = "unknown_" + deviceId; - } - - // 构造设备访问地址 - String deviceHost = buildDeviceHost(ip, port); - - // 步骤1:登录认证 - logger.debug("【设备-步骤1】登录设备: {}", deviceHost); - String authToken = loginDevice(deviceHost); - if (authToken == null) { - logger.error("【设备-异常】设备登录失败,跳过同步: IP={}", ip); + logger.warn("【设备】IP为空,跳过: ID={}", deviceId); + saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "设备IP为空"); return; } - logger.debug("【设备-步骤1】登录成功,获取到认证令牌"); - - // 步骤2:获取时间范围 - TimeRange timeRange = calculateSyncTimeRange(deviceId); - logger.debug("【设备-步骤2】同步时间范围: {} 至 {}", timeRange.startTime, timeRange.endTime); - - // 步骤3:获取录音列表 - logger.debug("【设备-步骤3】获取录音列表..."); - JSONArray records = fetchRecordList(deviceHost, authToken, timeRange); - if (records == null || records.isEmpty()) { - logger.info("【设备】设备 {} 没有新的录音记录", deviceId); + if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { + logger.warn("【设备】账号密码为空,跳过: ID={}", deviceId); + saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "设备账号密码未配置"); return; } - logger.info("【设备】获取到 {} 条录音记录", records.size()); - - // 步骤4:处理每条录音 - SyncResult result = processRecords(records, deviceNo, cityName, cityCode, orgCode, deviceHost, authToken); - - // 步骤5:保存同步时间 - if (result.latestCallTime != null) { - SyncTimeUtil.writeLastSyncTime(downloadPath, deviceId, result.latestCallTime); - logger.debug("【设备-步骤5】保存同步时间: {}", result.latestCallTime); + if (!StringUtils.hasText(cityName)) { + cityName = "unknown_" + deviceId; } - logger.info("【设备】同步完成: ID={}, 成功{}条, 失败{}条", deviceId, result.successCount, result.failCount); - } - - // ==================== 私有辅助方法 ==================== + String deviceHost = buildHost(ip, port); - /** - * 登录设备获取认证令牌 - * - * @param deviceHost 设备访问地址(如 192.168.1.100:80) - * @return 认证令牌(Cookie),失败返回null - */ - private String loginDevice(String deviceHost) { + // 步骤1:登录(无重试) + String authToken; try { String loginUrl = String.format("http://%s/authorize?username=%s&password=%s", - deviceHost, urlEncode(deviceUsername), urlEncode(devicePassword)); - - logger.info("正在登录设备: {}", deviceHost); - String authToken = vaaHttpUtil.httpLogin(loginUrl); - logger.info("登录成功"); - return authToken; + deviceHost, urlEncode(username), urlEncode(password)); + authToken = vaaHttpUtil.httpLogin(loginUrl); + saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "1", null); } catch (Exception e) { - logger.error("设备登录失败: {}, 原因={}", deviceHost, e.getMessage()); - return null; + saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "登录失败:" + e.getMessage()); + logger.error("【设备】登录失败,跳过: IP={}, 原因={}", ip, e.getMessage()); + return; } - } - /** - * 计算同步时间范围 - * - * @param deviceId 设备ID - * @return 时间范围(Unix时间戳,秒) - */ - private TimeRange calculateSyncTimeRange(String deviceId) { + // 步骤2:获取分机号码(通道→电话号码映射),用于解析主叫/被叫 + Map extNumbers = getExtensionNumbers(deviceHost, authToken); + + // 步骤3:计算同步时间范围(增量同步) Date lastSyncTime = SyncTimeUtil.readLastSyncTime(downloadPath, deviceId); Date now = new Date(); - - // 如果没有上次同步时间,或超过1天,则从昨天开始 if (lastSyncTime == null || DateUtil.getDateDoubleDiff(now, lastSyncTime) > 1.0) { lastSyncTime = DateUtil.addDayByDate(now, -1); } + long startEpoch = lastSyncTime.getTime() / 1000; + long endEpoch = now.getTime() / 1000; - return new TimeRange(lastSyncTime.getTime() / 1000, now.getTime() / 1000); - } - - /** - * 获取录音列表 - * - * @param deviceHost 设备访问地址 - * @param authToken 认证令牌 - * @param timeRange 时间范围 - * @return 录音记录JSON数组 - */ - private JSONArray fetchRecordList(String deviceHost, String authToken, TimeRange timeRange) { + // 步骤4:获取录音列表(无重试) + JSONArray records; try { String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]", - deviceHost, timeRange.startTime, timeRange.endTime); - - logger.debug("获取录音列表: {}", recordUrl); + deviceHost, startEpoch, endEpoch); String recordData = vaaHttpUtil.httpVisit(recordUrl, authToken); - return vaaHttpUtil.parseRecordData(recordData); + records = vaaHttpUtil.parseRecordData(recordData); + saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "2", "1", null); } catch (Exception e) { - logger.error("获取录音列表失败: {}", e.getMessage()); - return null; + saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "2", "0", "查询录音列表失败:" + e.getMessage()); + logger.error("【设备】获取录音列表失败: {}", e.getMessage()); + return; } - } - /** - * 批量处理录音记录 - */ - private SyncResult processRecords(JSONArray records, String deviceNo, String cityName, - String cityCode, String orgCode, String deviceHost, String authToken) { - SyncResult result = new SyncResult(); + if (records == null || records.isEmpty()) { + logger.info("【设备】无新录音: ID={}", deviceId); + return; + } + logger.info("【设备】获取到 {} 条录音记录", records.size()); + + // 步骤5:逐条处理录音 + int successCount = 0; + int failCount = 0; + Date latestCallTime = null; for (int i = 0; i < records.size(); i++) { - JSONObject record = records.getJSONObject(i); + JSONObject rec = records.getJSONObject(i); try { - boolean success = processSingleRecord(record, deviceNo, cityName, cityCode, orgCode, deviceHost, authToken); - if (success) { - result.successCount++; - // 跟踪最新的通话时间 - Date callTime = parseCallTime(record); - if (callTime != null && (result.latestCallTime == null || callTime.after(result.latestCallTime))) { - result.latestCallTime = callTime; + Date callTime = processSingleRecord(rec, deviceNo, cityName, cityCode, orgCode, + deviceHost, authToken, extNumbers); + if (callTime != null) { + successCount++; + if (latestCallTime == null || callTime.after(latestCallTime)) { + latestCallTime = callTime; } - } else { - result.failCount++; } } catch (Exception e) { - logger.error("处理录音记录失败[{}]: {}", i, e.getMessage()); - result.failCount++; + failCount++; + logger.error("【录音】处理失败: {}, 原因={}", rec.getString("id"), e.getMessage()); } } - return result; + // 步骤6:保存同步时间 + if (latestCallTime != null) { + SyncTimeUtil.writeLastSyncTime(downloadPath, deviceId, latestCallTime); + } + + logger.info("【设备】同步完成: ID={}, 成功{}条, 失败{}条", deviceId, successCount, failCount); } /** * 处理单条录音记录 - * - * 处理流程: - * 1. 解析录音信息 - * 2. 检查是否已存在(防重复) - * 3. 查询通道配置(获取本机号码) - * 4. 下载录音文件(如不存在) - * 5. 上传到OSS - * 6. 保存到数据库 + * + * @return 通话开始时间(成功时),null表示跳过 */ - private boolean processSingleRecord(JSONObject record, String deviceNo, String cityName, - String cityCode, String orgCode, String deviceHost, String authToken) throws Exception { - // ========== 步骤1:解析录音信息 ========== - String recordId = RecordParser.parseRecordId(record); - String filePath = RecordParser.parseFilePath(record); - Integer channel = RecordParser.parseChannel(record); - String phone = RecordParser.parsePhone(record); - boolean isOutgoing = RecordParser.isOutgoing(record); - boolean isAnswered = RecordParser.isAnswered(record); - Long begTime = RecordParser.parseBegTime(record); - Long endTime = RecordParser.parseEndTime(record); - - logger.debug("【录音】处理记录: recordId={}, channel={}, phone={}, direction={}", - recordId, channel, phone, isOutgoing ? "呼出" : "呼入"); - - // 参数校验 + private Date processSingleRecord(JSONObject rec, String deviceNo, String cityName, + String cityCode, String orgCode, String deviceHost, + String authToken, Map extNumbers) throws Exception { + // 解析录音信息 + String recordId = RecordParser.parseRecordId(rec); + String filePath = RecordParser.parseFilePath(rec); + Integer channel = RecordParser.parseChannel(rec); + String phone = RecordParser.parsePhone(rec); + boolean isOutgoing = RecordParser.isOutgoing(rec); + boolean isAnswered = RecordParser.isAnswered(rec); + Long begTime = RecordParser.parseBegTime(rec); + Long endTime = RecordParser.parseEndTime(rec); + + // 校验必要字段 if (filePath == null || filePath.isEmpty()) { - logger.debug("【录音】录音文件路径为空,跳过: recordId={}", recordId); - return false; + return null; } if (begTime == null || endTime == null) { - logger.warn("【录音-异常】录音时间信息缺失,跳过: recordId={}", recordId); - return false; + return null; } - // ========== 步骤2:防重复检查 ========== + // 防重复:根据 device_no + record_id 判断 String callRecordId = deviceNo + "_" + recordId; if (callRecordMapper.selectByCallRecordId(callRecordId) != null) { - logger.debug("【录音】通话记录已存在,跳过: {}", callRecordId); - return true; + logger.debug("【录音】已存在,跳过: {}", callRecordId); + return null; } - // ========== 步骤3:准备文件路径(按地市分文件夹) ========== + // 准备文件路径 Date callStartTime = new Date(begTime * 1000); String fileName = FilePathUtil.extractFileName(filePath); - // 本地路径: {basePath}/{cityCode}/{date}/{uuid}/{fileName} String localPath = FilePathUtil.buildLocalPath(downloadPath, cityCode, callStartTime, deviceNo, fileName); Path localFile = Paths.get(localPath); - // OSS路径: {cityCode}/{date}/{fileName} String ossPath = FilePathUtil.buildOssPath(cityCode, callStartTime, fileName); - logger.debug("【录音-步骤3】路径信息: localPath={}, ossPath={}", localPath, ossPath); - logger.debug("【录音】文件信息: fileName={}, localPath={}", fileName, localPath); - - // ========== 步骤4:查询通道配置 ========== - logger.debug("【录音-步骤4】查询通道配置: deviceNo={}, channel={}", deviceNo, channel); - MidVoiceChannelConfig channelConfig = channelConfigService.getChannelConfig( - deviceNo, channel, deviceHost, "EBOX-8108", authToken); - String channelPhone = channelConfig != null ? channelConfig.getPhoneNumber() : null; - logger.debug("【录音-步骤4】通道配置: channelPhone={}", channelPhone); + // 获取通道绑定的电话号码(从EBOX分机号码配置) + String channelPhone = ""; + if (channel != null && extNumbers != null) { + channelPhone = extNumbers.getOrDefault(String.valueOf(channel), ""); + } - // ========== 步骤5:构建通话记录实体 ========== + // 构建通话记录 MidVoiceCallRecord callRecord = buildCallRecord( callRecordId, deviceNo, cityCode, cityName, orgCode, - fileName, localPath, callStartTime, new Date(endTime * 1000), + fileName, callStartTime, new Date(endTime * 1000), (int) (endTime - begTime), channelPhone, phone, isOutgoing, isAnswered); - // ========== 步骤6:下载录音文件 ========== + // 下载录音文件(无重试) if (!Files.exists(localFile) || Files.size(localFile) == 0) { - logger.info("【录音-步骤6】开始下载录音: {}", fileName); String fileUrl = "http://" + deviceHost + filePath; vaaHttpUtil.httpDown(fileUrl, localPath, authToken); - logger.info("【录音-步骤6】录音下载完成: {} ({} 字节)", localPath, Files.size(localFile)); + logger.info("【录音】下载完成: {} ({} 字节)", fileName, Files.size(localFile)); } else { - logger.debug("【录音-步骤6】录音文件已存在本地,跳过下载: {} ({} 字节)", - fileName, Files.size(localFile)); + logger.debug("【录音】文件已存在,跳过下载: {}", fileName); } callRecord.setRecordingFileSize((int) Files.size(localFile)); - // ========== 步骤7:上传到OSS ========== - logger.debug("【录音-步骤7】开始上传OSS: cityName={}, ossPath={}", cityName, ossPath); + // 上传到OSS byte[] wavData = Files.readAllBytes(localFile); String ossUrl = fileUploadUtil.uploadWav(cityName, ossPath, fileName, wavData); - logger.info("【录音-步骤7】录音上传OSS成功: {}", ossUrl); callRecord.setRecordingFilePath(ossUrl); + logger.info("【录音】上传OSS成功: {}", ossUrl); - // ========== 步骤8:保存到数据库 ========== - logger.debug("【录音-步骤8】保存通话记录到数据库..."); + // 保存到数据库 callRecordMapper.insert(callRecord); - logger.info("【录音-步骤8】通话记录保存成功: id={}, callRecordId={}", callRecord.getId(), callRecordId); + logger.info("【录音】保存成功: callRecordId={}", callRecordId); - return true; + return callStartTime; } /** * 构建通话记录实体 + * + * 号码解析规则: + * - 呼出(state=1):主叫=本机号码(通道绑定号码),被叫=对方号码(phone字段) + * - 呼入(state=2):主叫=对方号码(phone字段),被叫=本机号码(通道绑定号码) */ private MidVoiceCallRecord buildCallRecord(String callRecordId, String deviceNo, String cityCode, String cityName, String orgCode, String fileName, - String localPath, Date callStartTime, Date callEndTime, + Date callStartTime, Date callEndTime, int duration, String channelPhone, String remotePhone, boolean isOutgoing, boolean isAnswered) { MidVoiceCallRecord record = new MidVoiceCallRecord(); - - // 基础信息 record.setCallRecordId(callRecordId); record.setDeviceNo(deviceNo); record.setCityCode(cityCode); record.setCityName(cityName); record.setOrgCode(orgCode); - - // 文件信息 record.setRecordingFileName(fileName); - record.setLocalPath(localPath); - - // 时间信息 record.setCallStartTime(callStartTime); record.setCallEndTime(callEndTime); record.setCallDuration(duration); + record.setCallDirection(isOutgoing ? "2" : "1"); // 1呼入,2呼出 - // 通话方向:1呼入,2呼出 - record.setCallDirection(isOutgoing ? "2" : "1"); - - // 主叫/被叫号码 - String localNumber = channelPhone != null && !channelPhone.isEmpty() ? channelPhone : ""; - String remoteNumber = remotePhone != null ? remotePhone : ""; + String localNum = channelPhone != null ? channelPhone : ""; + String remoteNum = remotePhone != null ? remotePhone : ""; if (isOutgoing) { - // 呼出:本机打给对方 - record.setCallTel(localNumber); // 主叫:本机号码 - record.setCalledTel(remoteNumber); // 被叫:对方号码 + record.setCallTel(localNum); // 主叫:本机 + record.setCalledTel(remoteNum); // 被叫:对方 } else { - // 呼入:对方打给本机 - record.setCallTel(remoteNumber); // 主叫:对方号码 - record.setCalledTel(localNumber); // 被叫:本机号码 + record.setCallTel(remoteNum); // 主叫:对方 + record.setCalledTel(localNum); // 被叫:本机 } - // 通话状态 record.setCallStatus(isAnswered ? "1" : "2"); // 1正常接通,2未接通 - return record; } - // ==================== 工具方法 ==================== - - /** - * 构建设备访问地址 - */ - private String buildDeviceHost(String ip, Integer port) { - return ip + (port != null && port != 80 ? ":" + port : ""); - } - /** - * 解析通话时间 + * 获取分机号码配置 + * 调用 EBOX 接口 GET /service/ext/number + * 返回 Map<通道号, 电话号码>,如 {"1":"8001","2":"8002"} */ - private Date parseCallTime(JSONObject record) { - Long begTime = RecordParser.parseBegTime(record); - return begTime != null ? new Date(begTime * 1000) : null; - } - - /** - * 从Map中获取字符串值 - */ - private String getStringValue(Map map, String key) { - Object value = map.get(key); - return value != null ? value.toString() : null; - } - - /** - * 从Map中获取整数值 - */ - private Integer getIntValue(Map map, String key, Integer defaultValue) { - Object value = map.get(key); - if (value == null) { - return defaultValue; - } + private Map getExtensionNumbers(String deviceHost, String authToken) { try { - return Integer.parseInt(value.toString()); - } catch (NumberFormatException e) { - return defaultValue; + String extUrl = "http://" + deviceHost + "/service/ext/number"; + return vaaHttpUtil.getExtensionNumbers(extUrl, authToken); + } catch (Exception e) { + logger.warn("【设备】获取分机号码失败,将无法解析本机号码: {}", e.getMessage()); + return Collections.emptyMap(); } } /** - * URL编码 + * 保存设备连接日志到 mid_voice_device_log 表 */ - private String urlEncode(String value) throws Exception { - return URLEncoder.encode(value, "UTF-8"); + private void saveDeviceLog(String deviceId, String deviceNo, String cityCode, String cityName, + String ipAddress, Integer devicePort, String connectType, + String connectStatus, String failReason) { + try { + MidVoiceDeviceLog log = new MidVoiceDeviceLog(); + log.setDeviceId(deviceId); + log.setDeviceNo(deviceNo); + log.setCityCode(cityCode); + log.setCityName(cityName); + log.setIpAddress(ipAddress); + log.setDevicePort(devicePort); + log.setConnectType(connectType); + log.setConnectStatus(connectStatus); + log.setFailReason(failReason); + log.setCreateTime(new Date()); + deviceLogMapper.insert(log); + } catch (Exception e) { + logger.error("保存设备连接日志失败: {}", e.getMessage()); + } } - // ==================== 内部类 ==================== + // ==================== 工具方法 ==================== - /** - * 时间范围 - */ - private static class TimeRange { - final long startTime; - final long endTime; + private String buildHost(String ip, Integer port) { + return ip + (port != null && port != 80 ? ":" + port : ""); + } - TimeRange(long startTime, long endTime) { - this.startTime = startTime; - this.endTime = endTime; - } + private String getStr(Map map, String key) { + Object v = map.get(key); + return v != null ? v.toString() : null; + } - @Override - public String toString() { - return String.format("[%d, %d]", startTime, endTime); - } + private Integer getInt(Map map, String key, Integer defaultVal) { + Object v = map.get(key); + if (v == null) return defaultVal; + try { return Integer.parseInt(v.toString()); } + catch (NumberFormatException e) { return defaultVal; } } - /** - * 同步结果 - */ - private static class SyncResult { - int successCount = 0; - int failCount = 0; - Date latestCallTime = null; + private String urlEncode(String value) throws Exception { + return URLEncoder.encode(value, "UTF-8"); } } diff --git a/src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigProvider.java b/src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigProvider.java deleted file mode 100644 index 073653e..0000000 --- a/src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.threecloud.dataserviceyy.service.channel; - -import com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig; - -import java.util.List; - -/** - * 通道配置提供者接口 - * 定义获取通道配置的通用方法,支持多种实现(EBOX API、数据库、手动配置等) - */ -public interface ChannelConfigProvider { - - /** - * 获取指定设备的所有通道配置 - * - * @param deviceNo 设备编码 - * @param deviceIp 设备IP地址 - * @param authToken 认证令牌(部分实现需要) - * @return 通道配置列表 - */ - List getChannelConfigs(String deviceNo, String deviceIp, String authToken); - - /** - * 获取指定设备的单个通道配置 - * - * @param deviceNo 设备编码 - * @param channelNo 通道号 - * @param deviceIp 设备IP地址 - * @param authToken 认证令牌 - * @return 通道配置,不存在返回null - */ - MidVoiceChannelConfig getChannelConfig(String deviceNo, Integer channelNo, String deviceIp, String authToken); - - /** - * 是否支持该设备类型 - * - * @param deviceModel 设备型号 - * @return true表示支持 - */ - boolean supports(String deviceModel); - - /** - * 提供者名称 - * - * @return 名称标识 - */ - String getProviderName(); -} diff --git a/src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigService.java b/src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigService.java deleted file mode 100644 index 4fa9855..0000000 --- a/src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigService.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.threecloud.dataserviceyy.service.channel; - -import com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.List; - -/** - * 通道配置服务 - * 管理多个 ChannelConfigProvider,根据设备类型选择合适的提供者 - */ -@Service -public class ChannelConfigService { - - private static final Logger logger = LoggerFactory.getLogger(ChannelConfigService.class); - - @Autowired - private List providers = new ArrayList<>(); - - @Autowired - private DatabaseChannelConfigProvider databaseProvider; - - @PostConstruct - public void init() { - logger.info("初始化通道配置服务,注册 {} 个提供者", providers.size()); - for (ChannelConfigProvider provider : providers) { - logger.info("注册通道配置提供者: {}", provider.getProviderName()); - } - } - - /** - * 获取通道配置 - * 优先使用设备特定的提供者,如果没有则使用数据库提供者 - * - * @param deviceNo 设备编码 - * @param channelNo 通道号 - * @param deviceIp 设备IP - * @param deviceModel 设备型号 - * @param authToken 认证令牌 - * @return 通道配置 - */ - public MidVoiceChannelConfig getChannelConfig(String deviceNo, Integer channelNo, - String deviceIp, String deviceModel, String authToken) { - // 1. 先尝试使用设备特定的提供者(如 EBOX API) - ChannelConfigProvider specificProvider = findProvider(deviceModel); - if (specificProvider != null && !specificProvider.getProviderName().equals("DATABASE")) { - try { - MidVoiceChannelConfig config = specificProvider.getChannelConfig(deviceNo, channelNo, deviceIp, authToken); - if (config != null) { - logger.debug("使用 {} 获取到通道配置", specificProvider.getProviderName()); - return config; - } - } catch (Exception e) { - logger.warn("{} 获取通道配置失败,尝试使用数据库: {}", specificProvider.getProviderName(), e.getMessage()); - } - } - - // 2. 使用数据库提供者(兜底) - return databaseProvider.getChannelConfig(deviceNo, channelNo, deviceIp, authToken); - } - - /** - * 获取设备的所有通道配置 - * 优先从 API 获取并同步到数据库,失败则从数据库读取 - * - * @param deviceNo 设备编码 - * @param deviceIp 设备IP - * @param deviceModel 设备型号 - * @param authToken 认证令牌 - * @return 通道配置列表 - */ - public List getChannelConfigs(String deviceNo, String deviceIp, - String deviceModel, String authToken) { - // 1. 先尝试使用设备特定的提供者(如 EBOX API) - ChannelConfigProvider specificProvider = findProvider(deviceModel); - if (specificProvider != null && !specificProvider.getProviderName().equals("DATABASE")) { - try { - List configs = specificProvider.getChannelConfigs(deviceNo, deviceIp, authToken); - if (!configs.isEmpty()) { - logger.info("从 {} 获取到 {} 条通道配置", specificProvider.getProviderName(), configs.size()); - return configs; - } - } catch (Exception e) { - logger.warn("{} 获取通道配置失败,尝试使用数据库: {}", specificProvider.getProviderName(), e.getMessage()); - } - } - - // 2. 使用数据库提供者(兜底) - return databaseProvider.getChannelConfigs(deviceNo, deviceIp, authToken); - } - - /** - * 根据设备型号查找合适的提供者 - */ - private ChannelConfigProvider findProvider(String deviceModel) { - if (deviceModel == null) { - return databaseProvider; - } - - // 优先找特定的提供者 - for (ChannelConfigProvider provider : providers) { - if (provider.supports(deviceModel)) { - return provider; - } - } - - // 默认使用数据库提供者 - return databaseProvider; - } -} diff --git a/src/main/java/com/threecloud/dataserviceyy/service/channel/DatabaseChannelConfigProvider.java b/src/main/java/com/threecloud/dataserviceyy/service/channel/DatabaseChannelConfigProvider.java deleted file mode 100644 index 5a3d3d2..0000000 --- a/src/main/java/com/threecloud/dataserviceyy/service/channel/DatabaseChannelConfigProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.threecloud.dataserviceyy.service.channel; - -import com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig; -import com.threecloud.dataserviceyy.mapper.MidVoiceChannelConfigMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** - * 数据库通道配置提供者 - * 从数据库读取通道配置,作为兜底方案 - */ -@Component -public class DatabaseChannelConfigProvider implements ChannelConfigProvider { - - private static final Logger logger = LoggerFactory.getLogger(DatabaseChannelConfigProvider.class); - - @Autowired - private MidVoiceChannelConfigMapper channelConfigMapper; - - @Override - public List getChannelConfigs(String deviceNo, String deviceIp, String authToken) { - List configs = channelConfigMapper.selectByDeviceNo(deviceNo); - logger.debug("从数据库获取到 {} 条通道配置: deviceNo={}", configs.size(), deviceNo); - return configs; - } - - @Override - public MidVoiceChannelConfig getChannelConfig(String deviceNo, Integer channelNo, String deviceIp, String authToken) { - return channelConfigMapper.selectByDeviceAndChannel(deviceNo, channelNo); - } - - @Override - public boolean supports(String deviceModel) { - // 支持所有设备类型(兜底方案) - return true; - } - - @Override - public String getProviderName() { - return "DATABASE"; - } -} diff --git a/src/main/java/com/threecloud/dataserviceyy/service/channel/EboxChannelConfigProvider.java b/src/main/java/com/threecloud/dataserviceyy/service/channel/EboxChannelConfigProvider.java deleted file mode 100644 index 045f6df..0000000 --- a/src/main/java/com/threecloud/dataserviceyy/service/channel/EboxChannelConfigProvider.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.threecloud.dataserviceyy.service.channel; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; -import com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig; -import com.threecloud.dataserviceyy.mapper.MidVoiceChannelConfigMapper; -import com.threecloud.dataserviceyy.util.VaaHttpUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -/** - * EBOX 录音盒通道配置提供者 - * 通过 EBOX HTTP API 获取分机号码配置 - */ -@Component -public class EboxChannelConfigProvider implements ChannelConfigProvider { - - private static final Logger logger = LoggerFactory.getLogger(EboxChannelConfigProvider.class); - - @Autowired - private VaaHttpUtil vaaHttpUtil; - - @Autowired - private MidVoiceChannelConfigMapper channelConfigMapper; - - @Override - public List getChannelConfigs(String deviceNo, String deviceIp, String authToken) { - List configs = new ArrayList<>(); - - try { - // 调用 EBOX API 获取分机号码 - String extUrl = "http://" + deviceIp + "/service/ext/number"; - String response = vaaHttpUtil.httpVisit(extUrl, authToken); - - if (response == null || response.isEmpty()) { - logger.warn("EBOX 返回空的分机号码配置: deviceNo={}", deviceNo); - return configs; - } - - // 解析 JSON 响应 - JSONObject jsonObj = JSON.parseObject(response); - - for (String key : jsonObj.keySet()) { - try { - Integer channelNo = Integer.parseInt(key); - String phoneNumber = jsonObj.getString(key); - - if (phoneNumber != null && !phoneNumber.isEmpty()) { - MidVoiceChannelConfig config = new MidVoiceChannelConfig(); - config.setDeviceNo(deviceNo); - config.setChannelNo(channelNo); - config.setPhoneNumber(phoneNumber); - config.setChannelName("通道" + channelNo); - config.setChannelStatus("1"); // 在线 - - configs.add(config); - } - } catch (NumberFormatException e) { - logger.warn("解析通道号失败: key={}", key); - } - } - - logger.info("从 EBOX 获取到 {} 条通道配置: deviceNo={}", configs.size(), deviceNo); - - // 同步到数据库(更新或插入) - syncToDatabase(configs); - - } catch (Exception e) { - logger.error("从 EBOX 获取通道配置失败: deviceNo={}, error={}", deviceNo, e.getMessage()); - } - - return configs; - } - - @Override - public MidVoiceChannelConfig getChannelConfig(String deviceNo, Integer channelNo, String deviceIp, String authToken) { - // 先查数据库 - MidVoiceChannelConfig config = channelConfigMapper.selectByDeviceAndChannel(deviceNo, channelNo); - if (config != null) { - return config; - } - - // 数据库没有,从 EBOX 获取 - List configs = getChannelConfigs(deviceNo, deviceIp, authToken); - for (MidVoiceChannelConfig c : configs) { - if (c.getChannelNo().equals(channelNo)) { - return c; - } - } - - return null; - } - - @Override - public boolean supports(String deviceModel) { - // 支持 EBOX 系列设备 - return deviceModel != null && deviceModel.toUpperCase().contains("EBOX"); - } - - @Override - public String getProviderName() { - return "EBOX_API"; - } - - /** - * 将获取到的配置同步到数据库 - */ - private void syncToDatabase(List configs) { - for (MidVoiceChannelConfig config : configs) { - try { - MidVoiceChannelConfig existing = channelConfigMapper.selectByDeviceAndChannel( - config.getDeviceNo(), config.getChannelNo()); - - if (existing != null) { - // 更新电话号码(如果变化了) - if (!config.getPhoneNumber().equals(existing.getPhoneNumber())) { - existing.setPhoneNumber(config.getPhoneNumber()); - channelConfigMapper.update(existing); - logger.info("更新通道配置: deviceNo={}, channelNo={}, phone={}", - config.getDeviceNo(), config.getChannelNo(), config.getPhoneNumber()); - } - } else { - // 插入新配置 - channelConfigMapper.insert(config); - logger.info("新增通道配置: deviceNo={}, channelNo={}, phone={}", - config.getDeviceNo(), config.getChannelNo(), config.getPhoneNumber()); - } - } catch (Exception e) { - logger.error("同步通道配置到数据库失败: {}", e.getMessage()); - } - } - } -} diff --git a/src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java b/src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java index 9b03521..a290b0f 100644 --- a/src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java +++ b/src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java @@ -16,218 +16,159 @@ import java.util.Map; * VAA录音盒HTTP工具类 * 用于与录音盒设备进行HTTP通信 * 参考文档: ebox_developer_guide.html (EBOX-8108 电话录音仪开发手册) + * + * 【说明】 + * 所有方法都不重试,一次失败直接抛出异常,由调用方处理 */ @Component public class VaaHttpUtil { private static final Logger logger = LoggerFactory.getLogger(VaaHttpUtil.class); - /** 连接超时:30秒,设备可能在远端网络 */ - private static final int CONNECT_TIMEOUT = 30000; - /** API读取超时:60秒 */ - private static final int READ_TIMEOUT = 60000; - /** 文件下载读取超时:5分钟,音频文件可能较大 */ - private static final int DOWNLOAD_READ_TIMEOUT = 300000; - /** 重试次数 */ - private static final int MAX_RETRY = 3; - /** 重试间隔基数(毫秒) */ - private static final int RETRY_BASE_DELAY = 2000; + /** 连接超时:10秒 */ + private static final int CONNECT_TIMEOUT = 10000; + /** API读取超时:30秒 */ + private static final int READ_TIMEOUT = 30000; + /** 文件下载读取超时:2分钟 */ + private static final int DOWNLOAD_READ_TIMEOUT = 120000; /** - * HTTP登录认证 - * @param loginUrl 登录URL,例如:http://192.168.1.100/authorize?username=admin&password=admin123 + * HTTP登录认证(无重试) + * @param loginUrl 登录URL * @return 登录成功后返回Authorization Cookie值 + * @throws Exception 登录失败直接抛出异常 */ public String httpLogin(String loginUrl) throws Exception { - return executeWithRetry(() -> { - logger.debug("正在登录录音盒: {}", loginUrl); + logger.debug("正在登录录音盒: {}", loginUrl); - HttpURLConnection conn = null; - try { - URL url = new URL(loginUrl); - conn = (HttpURLConnection) url.openConnection(); - conn.setInstanceFollowRedirects(false); - conn.setConnectTimeout(CONNECT_TIMEOUT); - conn.setReadTimeout(READ_TIMEOUT); - conn.setRequestMethod("GET"); + HttpURLConnection conn = null; + try { + URL url = new URL(loginUrl); + conn = (HttpURLConnection) url.openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setRequestMethod("GET"); - int responseCode = conn.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { - String authorization = getAuthorizationCookie(conn); - if (authorization == null || authorization.isEmpty()) { - throw new RuntimeException("登录失败,录音盒未返回Authorization Cookie"); - } - logger.info("录音盒登录成功"); - return authorization; - } - throw new RuntimeException("登录失败,HTTP状态码: " + responseCode); - } finally { - if (conn != null) { - conn.disconnect(); + int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { + String authorization = getAuthorizationCookie(conn); + if (authorization == null || authorization.isEmpty()) { + throw new RuntimeException("登录失败,录音盒未返回Authorization Cookie"); } + logger.info("录音盒登录成功"); + return authorization; + } + throw new RuntimeException("登录失败,HTTP状态码: " + responseCode); + } finally { + if (conn != null) { + conn.disconnect(); } - }, "登录"); + } } /** - * HTTP访问API获取数据 - * @param apiUrl API地址,例如:http://192.168.1.100/service/running/channel + * HTTP访问API获取数据(无重试) + * @param apiUrl API地址 * @param authorization 登录后返回的Authorization Cookie值 * @return 返回JSON字符串 + * @throws Exception 访问失败直接抛出异常 */ public String httpVisit(String apiUrl, String authorization) throws Exception { - return executeWithRetry(() -> { - logger.debug("访问API: {}", apiUrl); - - HttpURLConnection conn = null; - try { - URL url = new URL(apiUrl); - conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(CONNECT_TIMEOUT); - conn.setReadTimeout(READ_TIMEOUT); - conn.setRequestMethod("GET"); - addAuthorization(conn, authorization); + logger.debug("访问API: {}", apiUrl); - int responseCode = conn.getResponseCode(); - if (responseCode == 200) { - BufferedReader reader = new BufferedReader( - new InputStreamReader(conn.getInputStream(), "UTF-8") - ); - StringBuilder result = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - result.append(line); - } - reader.close(); + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setRequestMethod("GET"); + addAuthorization(conn, authorization); - String jsonResult = result.toString(); - logger.debug("API响应: {}", jsonResult); - if (isLoginPage(jsonResult)) { - throw new RuntimeException("录音盒认证失效,返回登录页面"); - } - return jsonResult; - } else { - throw new RuntimeException("API访问失败,HTTP状态码: " + responseCode); + int responseCode = conn.getResponseCode(); + if (responseCode == 200) { + BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), "UTF-8") + ); + StringBuilder result = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + result.append(line); } - } finally { - if (conn != null) { - conn.disconnect(); + reader.close(); + + String jsonResult = result.toString(); + logger.debug("API响应: {}", jsonResult); + if (isLoginPage(jsonResult)) { + throw new RuntimeException("录音盒认证失效,返回登录页面"); } + return jsonResult; + } else { + throw new RuntimeException("API访问失败,HTTP状态码: " + responseCode); + } + } finally { + if (conn != null) { + conn.disconnect(); } - }, "API访问"); + } } /** - * 下载录音文件(带重试) - * @param fileUrl 文件URL,例如:http://192.168.1.100/record/2026/05/20/OUT-xxx.wav + * 下载录音文件(无重试) + * @param fileUrl 文件URL * @param savePath 保存路径 * @param authorization 登录后返回的Authorization Cookie值 + * @throws Exception 下载失败直接抛出异常 */ public void httpDown(String fileUrl, String savePath, String authorization) throws Exception { - executeWithRetry(() -> { - logger.info("开始下载录音文件: {} -> {}", fileUrl, savePath); + logger.info("开始下载录音文件: {} -> {}", fileUrl, savePath); - // 确保目录存在 - File saveFile = new File(savePath); - if (!saveFile.getParentFile().exists()) { - saveFile.getParentFile().mkdirs(); - } - - HttpURLConnection conn = null; - InputStream inputStream = null; - FileOutputStream outputStream = null; + // 确保目录存在 + File saveFile = new File(savePath); + if (!saveFile.getParentFile().exists()) { + saveFile.getParentFile().mkdirs(); + } - try { - URL url = new URL(fileUrl); - conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(CONNECT_TIMEOUT); - conn.setReadTimeout(DOWNLOAD_READ_TIMEOUT); - conn.setRequestMethod("GET"); - addAuthorization(conn, authorization); + HttpURLConnection conn = null; + InputStream inputStream = null; + FileOutputStream outputStream = null; - int responseCode = conn.getResponseCode(); - if (responseCode == 200) { - inputStream = conn.getInputStream(); - outputStream = new FileOutputStream(savePath); + try { + URL url = new URL(fileUrl); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(DOWNLOAD_READ_TIMEOUT); + conn.setRequestMethod("GET"); + addAuthorization(conn, authorization); - byte[] buffer = new byte[8192]; - int bytesRead; - long totalBytes = 0; - long lastLogTime = System.currentTimeMillis(); + int responseCode = conn.getResponseCode(); + if (responseCode == 200) { + inputStream = conn.getInputStream(); + outputStream = new FileOutputStream(savePath); - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - totalBytes += bytesRead; - // 每10秒打印一次进度 - long now = System.currentTimeMillis(); - if (now - lastLogTime > 10000) { - logger.info("下载进度: {} MB", totalBytes / 1024 / 1024); - lastLogTime = now; - } - } + byte[] buffer = new byte[8192]; + int bytesRead; + long totalBytes = 0; - outputStream.flush(); - logger.info("录音文件下载完成: {}, 大小: {} bytes ({} MB)", - savePath, totalBytes, totalBytes / 1024 / 1024); - } else { - throw new RuntimeException("文件下载失败,HTTP状态码: " + responseCode); + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + totalBytes += bytesRead; } - } finally { - closeQuietly(inputStream); - closeQuietly(outputStream); - if (conn != null) { - conn.disconnect(); - } - } - return null; - }, "文件下载"); - } - /** - * 带重试的执行器 - */ - private T executeWithRetry(RetryableTask task, String operationName) throws Exception { - Exception lastException = null; - for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { - try { - return task.execute(); - } catch (java.net.SocketTimeoutException e) { - lastException = e; - if (attempt < MAX_RETRY) { - long delay = RETRY_BASE_DELAY * attempt; - logger.warn("{}(第{}次)超时,{}ms后重试: {}", operationName, attempt, delay, e.getMessage()); - Thread.sleep(delay); - } - } catch (java.net.ConnectException e) { - lastException = e; - if (attempt < MAX_RETRY) { - long delay = RETRY_BASE_DELAY * attempt; - logger.warn("{}(第{}次)连接失败,{}ms后重试: {}", operationName, attempt, delay, e.getMessage()); - Thread.sleep(delay); - } - } catch (IOException e) { - lastException = e; - if (attempt < MAX_RETRY && isRetryable(e)) { - long delay = RETRY_BASE_DELAY * attempt; - logger.warn("{}(第{}次)IO异常,{}ms后重试: {}", operationName, attempt, delay, e.getMessage()); - Thread.sleep(delay); - } else { - throw e; - } + outputStream.flush(); + logger.info("录音文件下载完成: {}, 大小: {} bytes ({} MB)", + savePath, totalBytes, totalBytes / 1024 / 1024); + } else { + throw new RuntimeException("文件下载失败,HTTP状态码: " + responseCode); + } + } finally { + closeQuietly(inputStream); + closeQuietly(outputStream); + if (conn != null) { + conn.disconnect(); } } - throw new RuntimeException(operationName + "失败,已重试" + MAX_RETRY + "次", lastException); - } - - private boolean isRetryable(IOException e) { - String msg = e.getMessage(); - if (msg == null) return false; - return msg.contains("timed out") || msg.contains("connection") || msg.contains("reset"); - } - - @FunctionalInterface - private interface RetryableTask { - T execute() throws Exception; } private void closeQuietly(Closeable c) { @@ -304,8 +245,7 @@ public class VaaHttpUtil { /** * 获取分机号码配置 * EBOX接口: GET /service/ext/number - * 返回每条线路的分机号码,如: {"1":"8001","2":"8002",...} - * @param extUrl 接口地址,例如:http://192.168.1.100/service/ext/number + * @param extUrl 接口地址 * @param authorization 登录后返回的Authorization Cookie值 * @return 通道-分机号映射 Map */ @@ -315,7 +255,6 @@ public class VaaHttpUtil { return new java.util.HashMap<>(); } try { - // EBOX返回的是JSON对象,key是通道号(1-8),value是分机号码 com.alibaba.fastjson2.JSONObject jsonObj = JSON.parseObject(jsonData); Map result = new java.util.HashMap<>(); for (String key : jsonObj.keySet()) { @@ -329,4 +268,4 @@ public class VaaHttpUtil { return new java.util.HashMap<>(); } } -} \ No newline at end of file +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0c30310..30d11c9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,6 @@ spring: application: name: dataservice-yy # 引入外部配置文件(部署时修改 config/application-external.yml) - # 注意:路径是相对于 JAR 包所在目录的 config: import: optional:file:./config/application-external.yml @@ -13,58 +12,23 @@ mybatis: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl -# ==================== 日志配置 ==================== -# 【内网测试专用】详细日志级别配置 logging: level: - # 根日志级别:INFO(生产环境建议WARN) root: INFO - - # 本项目代码:DEBUG(内网测试时开启,生产建议INFO) com.threecloud.dataserviceyy: DEBUG - - # 同步服务:DEBUG(关键业务流程) com.threecloud.dataserviceyy.service.VaaSyncService: DEBUG - - # 通道配置服务:DEBUG - com.threecloud.dataserviceyy.service.channel: DEBUG - - # HTTP工具:DEBUG(查看API请求响应) com.threecloud.dataserviceyy.util.VaaHttpUtil: DEBUG - - # 文件上传:INFO com.threecloud.dataserviceyy.util.FileUploadUtil: INFO - - # MyBatis SQL日志:DEBUG(查看执行的SQL) com.threecloud.dataserviceyy.mapper: DEBUG - - # Spring框架:WARN(减少框架日志噪音) org.springframework: WARN - org.springframework.web: WARN - - # Hikari连接池:INFO com.zaxxer.hikari: INFO - - # Tomcat:INFO - org.apache.catalina: INFO - - # 日志文件配置 file: name: logs/app.log - - # 日志格式 pattern: - # 文件格式:带时间、线程、级别、类名 file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" - # 控制台格式:简化版,方便开发查看 console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - %msg%n" - - # 日志文件滚动配置 logback: rollingpolicy: - # 单个文件最大10MB max-file-size: 10MB - # 保留30天 max-history: 30 - # 总大小不超过1GB total-size-cap: 1GB diff --git a/src/main/resources/mapper/MidVoiceChannelConfigMapper.xml b/src/main/resources/mapper/MidVoiceChannelConfigMapper.xml deleted file mode 100644 index e4703ab..0000000 --- a/src/main/resources/mapper/MidVoiceChannelConfigMapper.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - INSERT INTO mid_voice_channel_config ( - city_code, city_name, device_no, channel_no, phone_number, - channel_name, channel_status, create_time, update_time, remarks - ) VALUES ( - #{cityCode}, #{cityName}, #{deviceNo}, #{channelNo}, #{phoneNumber}, - #{channelName}, #{channelStatus}, NOW(), NOW(), #{remarks} - ) - - - - - - - - - - - - - - - - - - - - UPDATE mid_voice_channel_config - SET city_code = #{cityCode}, - city_name = #{cityName}, - device_no = #{deviceNo}, - channel_no = #{channelNo}, - phone_number = #{phoneNumber}, - channel_name = #{channelName}, - channel_status = #{channelStatus}, - remarks = #{remarks}, - update_time = NOW() - WHERE id = #{id} - - - - - UPDATE mid_voice_channel_config - SET channel_status = #{channelStatus}, - update_time = NOW() - WHERE id = #{id} - - - - - DELETE FROM mid_voice_channel_config WHERE id = #{id} - - - - - INSERT INTO mid_voice_channel_config ( - city_code, city_name, device_no, channel_no, phone_number, - channel_name, channel_status, create_time, update_time, remarks - ) VALUES - - ( - #{item.cityCode}, #{item.cityName}, #{item.deviceNo}, #{item.channelNo}, #{item.phoneNumber}, - #{item.channelName}, #{item.channelStatus}, NOW(), NOW(), #{item.remarks} - ) - - - - diff --git a/src/main/resources/mapper/VoiceSyncMapper.xml b/src/main/resources/mapper/VoiceSyncMapper.xml index 2b6130b..4647249 100644 --- a/src/main/resources/mapper/VoiceSyncMapper.xml +++ b/src/main/resources/mapper/VoiceSyncMapper.xml @@ -1,6 +1,8 @@ + + - - - - - - - - - INSERT INTO YYDC_YYTH(ID, ORGAN_NAME, ORGAN_ID, YYSB_ID, SBTD_ID, THFX, PHONE, ZJHM, - BJHM, THSC, KSSJ, JSSJ, LYZT, LYDZ, TBSJ, CJSJ, CJR_ID, XGSJ, XGR_ID, SFYX_ST) - VALUES (SYS_GUID(), #{organName}, #{organId}, #{yysbId}, #{sbtdId}, #{thfx}, #{phone}, #{zjhm}, #{bjhm}, - #{thsc}, TO_DATE(#{kssj}, 'YYYY-MM-DD HH24:MI:SS'), TO_DATE(#{jssj}, 'YYYY-MM-DD HH24:MI:SS'), '1', - #{lydz}, SYSDATE, SYSDATE, 0, SYSDATE, 0, '1') - - - - - - - UPDATE YYDC_SBTD - SET TDZT = #{TDZT}, - TBSJ = #{TBSJ}, - XGR_ID = 0, - XGSJ = #{TBSJ} - WHERE SFYX_ST = '1' - AND TDHM = #{TDHM} - AND UUID = #{UUID} - - - - - INSERT INTO YYDC_TBRZ (CODE, KSSJ, JSSJ, GXSJ, TBBZ, TBSM) - VALUES (#{CODE}, #{KSSJ}, #{JSSJ}, #{GXSJ}, #{TBBZ}, #{TBSM}) - - - - - - INSERT INTO YYDC_YYTH - (ID, ORGAN_NAME, ORGAN_ID, YYSB_ID, SBTD_ID, THFX, PHONE, ZJHM, BJHM, - THSC, KSSJ, JSSJ, LYZT, LYDZ, LYMC, YJZT, YCZT, XCZT, DBZT, - TBSJ, CJR_ID, CJSJ, XGR_ID, XGSJ, SFYX_ST) - VALUES - (#{THID}, #{ORGAN_NAME}, #{ORGAN_ID}, #{YYSB_ID}, #{SBTD_ID}, #{THFX}, #{PHONE}, - #{ZJHM}, #{BJHM}, #{THSC}, TO_DATE(#{KSSJ}, 'YYYY-MM-DD HH24:MI:SS'), - TO_DATE(#{JSSJ}, 'YYYY-MM-DD HH24:MI:SS'), '1', #{LYDZ}, #{LYMC}, - '0', '0', '0', '0', #{TBSJ}, 0, #{TBSJ}, 0, #{TBSJ}, '1') - - - - UPDATE YYDC_YYTH - SET ORGAN_NAME = #{ORGAN_NAME}, - ORGAN_ID = #{ORGAN_ID}, - YYSB_ID = #{YYSB_ID}, - SBTD_ID = #{SBTD_ID}, - THFX = #{THFX}, - PHONE = #{PHONE}, - ZJHM = #{ZJHM}, - BJHM = #{BJHM}, - THSC = #{THSC}, - KSSJ = TO_DATE(#{KSSJ}, 'YYYY-MM-DD HH24:MI:SS'), - JSSJ = TO_DATE(#{JSSJ}, 'YYYY-MM-DD HH24:MI:SS'), - LYZT = '1', - LYDZ = #{LYDZ}, - LYMC = #{LYMC}, - TBSJ = #{TBSJ}, - XGR_ID = 0, - XGSJ = #{TBSJ} - WHERE ID = #{THID} -