Browse Source

优化代码

main
wang 14 hours ago
parent
commit
cd54d1220b
  1. 43
      config/application-external.yml
  2. 78
      src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceChannelConfig.java
  3. 66
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceChannelConfigMapper.java
  4. 43
      src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java
  5. 507
      src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java
  6. 48
      src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigProvider.java
  7. 115
      src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigService.java
  8. 46
      src/main/java/com/threecloud/dataserviceyy/service/channel/DatabaseChannelConfigProvider.java
  9. 138
      src/main/java/com/threecloud/dataserviceyy/service/channel/EboxChannelConfigProvider.java
  10. 99
      src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java
  11. 36
      src/main/resources/application.yml
  12. 104
      src/main/resources/mapper/MidVoiceChannelConfigMapper.xml
  13. 116
      src/main/resources/mapper/VoiceSyncMapper.xml

43
config/application-external.yml

@ -5,72 +5,47 @@
# 1. 此文件用于部署时覆盖默认配置,无需重新打包 JAR # 1. 此文件用于部署时覆盖默认配置,无需重新打包 JAR
# 2. 将此文件放在 JAR 包同级目录的 config/ 文件夹下 # 2. 将此文件放在 JAR 包同级目录的 config/ 文件夹下
# 3. 修改此文件后重启服务即可生效 # 3. 修改此文件后重启服务即可生效
# 4. 每个录音盒的账号密码在 mid_voice_device_config 表中配置
# ============================================ # ============================================
spring: spring:
# ==================== 数据库配置 ==================== # ==================== 数据库配置 ====================
datasource: datasource:
# 人大金仓数据库驱动(一般无需修改)
driver-class-name: com.kingbase8.Driver 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 url: jdbc:kingbase8://127.0.0.1:54321/kingbase?currentSchema=mid_voice&clientEncoding=utf8
# 数据库用户名
username: dcms_dev username: dcms_dev
# 数据库密码
password: your_password_here password: your_password_here
# ==================== 语音同步配置 ==================== # ==================== 语音同步配置 ====================
vaa-sync: vaa-sync:
# 本地录音文件存储路径(相对路径或绝对路径) # 本地录音文件存储路径
# 示例: ./vaa-recordings 或 /opt/dataservice-yy/vaa-recordings
download-path: ./vaa-recordings download-path: ./vaa-recordings
# 本地文件保留天数
retain-days: 10
# 同步定时任务 Cron 表达式 # 同步定时任务 Cron 表达式
# 默认每2小时执行一次: 0 0 0/2 * * ? # 每2小时: 0 0 0/2 * * ?
# 每30分钟执行: 0 0/30 * * * ? # 每30分钟: 0 0/30 * * * ?
# 每天凌晨2点执行: 0 0 2 * * ? # 每分钟(测试): 0 0/1 * * * ?
# 每分钟执行(测试用): 0 0/1 * * * ?
sync-interval-cron: "0 0 0/2 * * ?" sync-interval-cron: "0 0 0/2 * * ?"
# 录音盒登录账号密码
device-username: admin
device-password: admin
# OSS 文件上传配置 # OSS 文件上传配置
oss: oss:
# OSS 服务基础地址(用于拼接完整URL)
# 示例: http://53.1.194.59:9090
base-url: http://127.0.0.1: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 upload-url: http://127.0.0.1:9090/apiOss/oss/fileUpload
# OSS 认证信息(请向管理员索取)
appcode: dataservice-yy appcode: dataservice-yy
appid: your_appid_here appid: your_appid_here
appsecret: your_appsecret_here appsecret: your_appsecret_here
# ==================== 服务端口配置 ==================== # ==================== 服务端口 ====================
server: server:
# 服务端口
port: 8088 port: 8088
# ==================== 日志配置 ==================== # ==================== 日志配置 ====================
logging: logging:
# 日志文件路径
file: file:
name: logs/app.log name: logs/app.log
# 日志级别
level: level:
# 根日志级别: INFO(生产) / DEBUG(测试)
root: INFO root: INFO
# 本项目代码日志级别
com.threecloud.dataserviceyy: DEBUG com.threecloud.dataserviceyy: DEBUG

78
src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceChannelConfig.java

@ -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; }
}

66
src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceChannelConfigMapper.java

@ -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<MidVoiceChannelConfig> selectByDeviceNo(@Param("deviceNo") String deviceNo);
/**
* 根据电话号码查询通道
*/
MidVoiceChannelConfig selectByPhoneNumber(@Param("phoneNumber") String phoneNumber);
/**
* 查询所有通道配置
*/
List<MidVoiceChannelConfig> selectAll();
/**
* 更新通道配置
*/
int update(MidVoiceChannelConfig config);
/**
* 更新通道状态
*/
int updateStatus(@Param("id") Long id,
@Param("channelStatus") String channelStatus);
/**
* 删除通道配置
*/
int deleteById(Long id);
/**
* 批量插入
*/
int batchInsert(@Param("list") List<MidVoiceChannelConfig> list);
}

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

@ -1,46 +1,19 @@
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;
/**
* 语音设备 Mapper
* 查询 mid_voice_device_config
*/
@Mapper @Mapper
public interface VoiceSyncMapper { public interface VoiceSyncMapper {
List<Map<String, Object>> getAllYysb();
Map<String, Object> getTdByPhone(@Param("phone") String phone);
Map<String, Object> getTdByPhone2(@Param("zjhm") String zjhm, @Param("bjhm") String bjhm);
Map<String, Object> 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<String, Object> getChannelByNumberAndUuid(@Param("channel") String channel, @Param("uuid") String uuid);
void updateChannelStatus(Map<String, Object> params); /**
* 查询所有在线语音设备含每个设备的账号密码
Object getLastSyncTime(@Param("code") String code); */
List<Map<String, Object>> getAllYysb();
void saveSyncLog(Map<String, Object> params);
Object checkThjlExists(@Param("thid") String thid);
void insertThjl(Map<String, Object> params);
void updateThjl(Map<String, Object> params);
} }

507
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.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.MidVoiceChannelConfig; import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog;
import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper; import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper;
import com.threecloud.dataserviceyy.mapper.MidVoiceDeviceLogMapper;
import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper; import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper;
import com.threecloud.dataserviceyy.service.channel.ChannelConfigService;
import com.threecloud.dataserviceyy.util.*; import com.threecloud.dataserviceyy.util.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -16,93 +16,42 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.io.File;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Date; import java.util.*;
import java.util.List;
import java.util.Map;
/** /**
* VAA录音盒定时同步服务 * VAA录音盒定时同步服务
* *
* 功能说明 * 功能从EBOX录音盒拉取录音 下载文件 上传OSS 保存通话记录
* 1. 定时从 EBOX 录音盒拉取录音记录 * 策略增量同步无重试防重复失败记录到日志表
* 2. 下载录音文件到本地保留10天 * 账户每个设备的账号密码从 mid_voice_device_config 表读取
* 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/ # 按日期分目录
* <device_uuid>/ # 按设备分目录
* xxx.wav
* .sync-marker/ # 同步时间标记
* <device_id>.time
*/ */
@Service @Service
public class VaaSyncService { public class VaaSyncService {
private static final Logger logger = LoggerFactory.getLogger(VaaSyncService.class); private static final Logger logger = LoggerFactory.getLogger(VaaSyncService.class);
// ==================== 依赖注入 ====================
@Autowired @Autowired
private VoiceSyncMapper voiceSyncMapper; private VoiceSyncMapper voiceSyncMapper;
@Autowired @Autowired
private MidVoiceCallRecordMapper callRecordMapper; private MidVoiceCallRecordMapper callRecordMapper;
@Autowired @Autowired
private ChannelConfigService channelConfigService; private MidVoiceDeviceLogMapper deviceLogMapper;
@Autowired @Autowired
private VaaHttpUtil vaaHttpUtil; private VaaHttpUtil vaaHttpUtil;
@Autowired @Autowired
private FileUploadUtil fileUploadUtil; private FileUploadUtil fileUploadUtil;
// ==================== 配置参数 ====================
/** 本地录音文件存储路径 */
@Value("${vaa-sync.download-path:./vaa-recordings}") @Value("${vaa-sync.download-path:./vaa-recordings}")
private String downloadPath; 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}") @Value("${vaa-sync.retain-days:10}")
private int retainDays; 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 * * ?}") @Scheduled(cron = "${vaa-sync.sync-interval-cron:0 0 0/2 * * ?}")
public void scheduledSync() { public void scheduledSync() {
@ -113,36 +62,23 @@ public class VaaSyncService {
logger.info("【定时任务】========== VAA录音盒同步结束,耗时 {} 秒 ==========", costTime / 1000); logger.info("【定时任务】========== VAA录音盒同步结束,耗时 {} 秒 ==========", costTime / 1000);
} }
// ==================== 核心同步逻辑 ====================
/** /**
* 执行同步任务主入口 * 执行同步任务主入口
*
* 执行流程
* 1. 清理过期本地文件
* 2. 查询所有在线设备
* 3. 逐个设备同步
*/ */
public void executeSync() { public void executeSync() {
logger.info("【主流程】开始执行VAA录音盒同步任务");
try { try {
// 步骤1:清理过期本地文件 // 清理过期本地文件
logger.debug("【步骤1】清理 {} 天前的本地录音文件", retainDays);
FileCleaner.cleanOldFiles(downloadPath, retainDays); FileCleaner.cleanOldFiles(downloadPath, retainDays);
// 步骤2:查询设备列表 // 查询在线设备列表(含每个设备的账号密码)
logger.debug("【步骤2】查询在线设备列表");
List<Map<String, Object>> deviceList = voiceSyncMapper.getAllYysb(); List<Map<String, Object>> deviceList = voiceSyncMapper.getAllYysb();
logger.info("【主流程】查询到 {} 个语音设备", deviceList.size()); logger.info("【主流程】查询到 {} 个在线语音设备", deviceList.size());
// 步骤3:逐个设备同步
int successCount = 0; int successCount = 0;
int failCount = 0; int failCount = 0;
for (int i = 0; i < deviceList.size(); i++) { for (int i = 0; i < deviceList.size(); i++) {
Map<String, Object> device = deviceList.get(i); Map<String, Object> device = deviceList.get(i);
String deviceId = getStringValue(device, "ID"); String deviceId = getStr(device, "ID");
logger.debug("【步骤3】处理第 {}/{} 个设备: ID={}", i + 1, deviceList.size(), deviceId);
try { try {
syncSingleDevice(device); syncSingleDevice(device);
successCount++; successCount++;
@ -151,8 +87,7 @@ public class VaaSyncService {
logger.error("【异常】设备同步失败: ID={}, 原因={}", deviceId, e.getMessage()); logger.error("【异常】设备同步失败: ID={}, 原因={}", deviceId, e.getMessage());
} }
} }
logger.info("【主流程】同步完成,成功 {} 个,失败 {} 个", successCount, failCount);
logger.info("【主流程】同步完成,成功 {} 个设备,失败 {} 个设备", successCount, failCount);
} catch (Exception e) { } catch (Exception e) {
logger.error("【异常】同步任务执行失败: {}", e.getMessage(), e); logger.error("【异常】同步任务执行失败: {}", e.getMessage(), e);
} }
@ -161,387 +96,287 @@ public class VaaSyncService {
/** /**
* 同步单个设备 * 同步单个设备
* *
* @param device 设备信息Map包含 * 流程登录 获取分机号码 查录音列表 逐条下载上传保存
* - ID: 设备ID * 任何步骤失败直接记录日志跳过该设备
* - UUID: 设备编号device_no
* - ORGAN_NAME: 地市名称
* - ORGAN_ID: 地市编码
* - IP: IP地址
* - PORT: 端口号
* - ORG_CODE: 单位代码
*/ */
private void syncSingleDevice(Map<String, Object> device) throws Exception { private void syncSingleDevice(Map<String, Object> device) throws Exception {
// 提取设备信息 String deviceId = getStr(device, "ID");
String deviceId = getStringValue(device, "ID"); String deviceNo = getStr(device, "UUID");
String deviceNo = getStringValue(device, "UUID"); String cityName = getStr(device, "ORGAN_NAME");
String cityName = getStringValue(device, "ORGAN_NAME"); String cityCode = getStr(device, "ORGAN_ID");
String cityCode = getStringValue(device, "ORGAN_ID"); String ip = getStr(device, "IP");
String ip = getStringValue(device, "IP"); Integer port = getInt(device, "PORT", 80);
Integer port = getIntValue(device, "PORT", 80); String orgCode = getStr(device, "ORG_CODE");
String orgCode = getStringValue(device, "ORG_CODE"); // 每个设备有自己的账号密码,从配置表读取
String username = getStr(device, "USERNAME");
logger.info("【设备】────────────────────────────────────────"); String password = getStr(device, "PASSWORD");
logger.info("【设备】开始同步设备: ID={}, 编号={}, 机构={}, IP={}:{}",
deviceId, deviceNo, cityName, ip, port); logger.info("【设备】同步设备: ID={}, 编号={}, 机构={}, IP={}:{}", deviceId, deviceNo, cityName, ip, port);
// 参数校验 // 参数校验
if (!StringUtils.hasText(ip)) { if (!StringUtils.hasText(ip)) {
logger.warn("【设备】设备IP为空,跳过同步: ID={}", deviceId); logger.warn("【设备】IP为空,跳过: ID={}", deviceId);
return; saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "设备IP为空");
}
// 机构名称兜底
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);
return; return;
} }
logger.debug("【设备-步骤1】登录成功,获取到认证令牌"); if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
logger.warn("【设备】账号密码为空,跳过: ID={}", deviceId);
// 步骤2:获取时间范围 saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "设备账号密码未配置");
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);
return; return;
} }
logger.info("【设备】获取到 {} 条录音记录", records.size()); if (!StringUtils.hasText(cityName)) {
cityName = "unknown_" + deviceId;
// 步骤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);
}
logger.info("【设备】同步完成: ID={}, 成功{}条, 失败{}条", deviceId, result.successCount, result.failCount);
} }
// ==================== 私有辅助方法 ==================== String deviceHost = buildHost(ip, port);
/** // 步骤1:登录(无重试)
* 登录设备获取认证令牌 String authToken;
*
* @param deviceHost 设备访问地址 192.168.1.100:80
* @return 认证令牌Cookie失败返回null
*/
private String loginDevice(String deviceHost) {
try { try {
String loginUrl = String.format("http://%s/authorize?username=%s&password=%s", String loginUrl = String.format("http://%s/authorize?username=%s&password=%s",
deviceHost, urlEncode(deviceUsername), urlEncode(devicePassword)); deviceHost, urlEncode(username), urlEncode(password));
authToken = vaaHttpUtil.httpLogin(loginUrl);
logger.info("正在登录设备: {}", deviceHost); saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "1", null);
String authToken = vaaHttpUtil.httpLogin(loginUrl);
logger.info("登录成功");
return authToken;
} catch (Exception e) { } catch (Exception e) {
logger.error("设备登录失败: {}, 原因={}", deviceHost, e.getMessage()); saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "登录失败:" + e.getMessage());
return null; logger.error("【设备】登录失败,跳过: IP={}, 原因={}", ip, e.getMessage());
} return;
} }
/** // 步骤2:获取分机号码(通道→电话号码映射),用于解析主叫/被叫
* 计算同步时间范围 Map<String, String> extNumbers = getExtensionNumbers(deviceHost, authToken);
*
* @param deviceId 设备ID // 步骤3:计算同步时间范围(增量同步)
* @return 时间范围Unix时间戳
*/
private TimeRange calculateSyncTimeRange(String deviceId) {
Date lastSyncTime = SyncTimeUtil.readLastSyncTime(downloadPath, deviceId); Date lastSyncTime = SyncTimeUtil.readLastSyncTime(downloadPath, deviceId);
Date now = new Date(); Date now = new Date();
// 如果没有上次同步时间,或超过1天,则从昨天开始
if (lastSyncTime == null || DateUtil.getDateDoubleDiff(now, lastSyncTime) > 1.0) { if (lastSyncTime == null || DateUtil.getDateDoubleDiff(now, lastSyncTime) > 1.0) {
lastSyncTime = DateUtil.addDayByDate(now, -1); lastSyncTime = DateUtil.addDayByDate(now, -1);
} }
long startEpoch = lastSyncTime.getTime() / 1000;
long endEpoch = now.getTime() / 1000;
return new TimeRange(lastSyncTime.getTime() / 1000, now.getTime() / 1000); // 步骤4:获取录音列表(无重试)
} JSONArray records;
/**
* 获取录音列表
*
* @param deviceHost 设备访问地址
* @param authToken 认证令牌
* @param timeRange 时间范围
* @return 录音记录JSON数组
*/
private JSONArray fetchRecordList(String deviceHost, String authToken, TimeRange timeRange) {
try { try {
String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]", String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]",
deviceHost, timeRange.startTime, timeRange.endTime); deviceHost, startEpoch, endEpoch);
logger.debug("获取录音列表: {}", recordUrl);
String recordData = vaaHttpUtil.httpVisit(recordUrl, authToken); 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) { } catch (Exception e) {
logger.error("获取录音列表失败: {}", e.getMessage()); saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "2", "0", "查询录音列表失败:" + e.getMessage());
return null; logger.error("【设备】获取录音列表失败: {}", e.getMessage());
return;
} }
if (records == null || records.isEmpty()) {
logger.info("【设备】无新录音: ID={}", deviceId);
return;
} }
logger.info("【设备】获取到 {} 条录音记录", records.size());
/** // 步骤5:逐条处理录音
* 批量处理录音记录 int successCount = 0;
*/ int failCount = 0;
private SyncResult processRecords(JSONArray records, String deviceNo, String cityName, Date latestCallTime = null;
String cityCode, String orgCode, String deviceHost, String authToken) {
SyncResult result = new SyncResult();
for (int i = 0; i < records.size(); i++) { for (int i = 0; i < records.size(); i++) {
JSONObject record = records.getJSONObject(i); JSONObject rec = records.getJSONObject(i);
try { try {
boolean success = processSingleRecord(record, deviceNo, cityName, cityCode, orgCode, deviceHost, authToken); Date callTime = processSingleRecord(rec, deviceNo, cityName, cityCode, orgCode,
if (success) { deviceHost, authToken, extNumbers);
result.successCount++; if (callTime != null) {
// 跟踪最新的通话时间 successCount++;
Date callTime = parseCallTime(record); if (latestCallTime == null || callTime.after(latestCallTime)) {
if (callTime != null && (result.latestCallTime == null || callTime.after(result.latestCallTime))) { latestCallTime = callTime;
result.latestCallTime = callTime;
} }
} else {
result.failCount++;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("处理录音记录失败[{}]: {}", i, e.getMessage()); failCount++;
result.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);
} }
/** /**
* 处理单条录音记录 * 处理单条录音记录
* *
* 处理流程 * @return 通话开始时间成功时null表示跳过
* 1. 解析录音信息
* 2. 检查是否已存在防重复
* 3. 查询通道配置获取本机号码
* 4. 下载录音文件如不存在
* 5. 上传到OSS
* 6. 保存到数据库
*/ */
private boolean processSingleRecord(JSONObject record, String deviceNo, String cityName, private Date processSingleRecord(JSONObject rec, String deviceNo, String cityName,
String cityCode, String orgCode, String deviceHost, String authToken) throws Exception { String cityCode, String orgCode, String deviceHost,
// ========== 步骤1:解析录音信息 ========== String authToken, Map<String, String> extNumbers) throws Exception {
String recordId = RecordParser.parseRecordId(record); // 解析录音信息
String filePath = RecordParser.parseFilePath(record); String recordId = RecordParser.parseRecordId(rec);
Integer channel = RecordParser.parseChannel(record); String filePath = RecordParser.parseFilePath(rec);
String phone = RecordParser.parsePhone(record); Integer channel = RecordParser.parseChannel(rec);
boolean isOutgoing = RecordParser.isOutgoing(record); String phone = RecordParser.parsePhone(rec);
boolean isAnswered = RecordParser.isAnswered(record); boolean isOutgoing = RecordParser.isOutgoing(rec);
Long begTime = RecordParser.parseBegTime(record); boolean isAnswered = RecordParser.isAnswered(rec);
Long endTime = RecordParser.parseEndTime(record); Long begTime = RecordParser.parseBegTime(rec);
Long endTime = RecordParser.parseEndTime(rec);
logger.debug("【录音】处理记录: recordId={}, channel={}, phone={}, direction={}",
recordId, channel, phone, isOutgoing ? "呼出" : "呼入"); // 校验必要字段
// 参数校验
if (filePath == null || filePath.isEmpty()) { if (filePath == null || filePath.isEmpty()) {
logger.debug("【录音】录音文件路径为空,跳过: recordId={}", recordId); return null;
return false;
} }
if (begTime == null || endTime == null) { if (begTime == null || endTime == null) {
logger.warn("【录音-异常】录音时间信息缺失,跳过: recordId={}", recordId); return null;
return false;
} }
// ========== 步骤2:防重复检查 ========== // 防重复:根据 device_no + record_id 判断
String callRecordId = deviceNo + "_" + recordId; String callRecordId = deviceNo + "_" + recordId;
if (callRecordMapper.selectByCallRecordId(callRecordId) != null) { if (callRecordMapper.selectByCallRecordId(callRecordId) != null) {
logger.debug("【录音】通话记录已存在,跳过: {}", callRecordId); logger.debug("【录音】已存在,跳过: {}", callRecordId);
return true; return null;
} }
// ========== 步骤3:准备文件路径(按地市分文件夹) ========== // 准备文件路径
Date callStartTime = new Date(begTime * 1000); Date callStartTime = new Date(begTime * 1000);
String fileName = FilePathUtil.extractFileName(filePath); String fileName = FilePathUtil.extractFileName(filePath);
// 本地路径: {basePath}/{cityCode}/{date}/{uuid}/{fileName}
String localPath = FilePathUtil.buildLocalPath(downloadPath, cityCode, callStartTime, deviceNo, fileName); String localPath = FilePathUtil.buildLocalPath(downloadPath, cityCode, callStartTime, deviceNo, fileName);
Path localFile = Paths.get(localPath); Path localFile = Paths.get(localPath);
// OSS路径: {cityCode}/{date}/{fileName}
String ossPath = FilePathUtil.buildOssPath(cityCode, callStartTime, fileName); String ossPath = FilePathUtil.buildOssPath(cityCode, callStartTime, fileName);
logger.debug("【录音-步骤3】路径信息: localPath={}, ossPath={}", localPath, ossPath);
logger.debug("【录音】文件信息: fileName={}, localPath={}", fileName, localPath); // 获取通道绑定的电话号码(从EBOX分机号码配置)
String channelPhone = "";
// ========== 步骤4:查询通道配置 ========== if (channel != null && extNumbers != null) {
logger.debug("【录音-步骤4】查询通道配置: deviceNo={}, channel={}", deviceNo, channel); channelPhone = extNumbers.getOrDefault(String.valueOf(channel), "");
MidVoiceChannelConfig channelConfig = channelConfigService.getChannelConfig( }
deviceNo, channel, deviceHost, "EBOX-8108", authToken);
String channelPhone = channelConfig != null ? channelConfig.getPhoneNumber() : null;
logger.debug("【录音-步骤4】通道配置: channelPhone={}", channelPhone);
// ========== 步骤5:构建通话记录实体 ========== // 构建通话记录
MidVoiceCallRecord callRecord = buildCallRecord( MidVoiceCallRecord callRecord = buildCallRecord(
callRecordId, deviceNo, cityCode, cityName, orgCode, 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); (int) (endTime - begTime), channelPhone, phone, isOutgoing, isAnswered);
// ========== 步骤6:下载录音文件 ========== // 下载录音文件(无重试)
if (!Files.exists(localFile) || Files.size(localFile) == 0) { if (!Files.exists(localFile) || Files.size(localFile) == 0) {
logger.info("【录音-步骤6】开始下载录音: {}", fileName);
String fileUrl = "http://" + deviceHost + filePath; String fileUrl = "http://" + deviceHost + filePath;
vaaHttpUtil.httpDown(fileUrl, localPath, authToken); vaaHttpUtil.httpDown(fileUrl, localPath, authToken);
logger.info("【录音-步骤6录音下载完成: {} ({} 字节)", localPath, Files.size(localFile)); logger.info("【录音】下载完成: {} ({} 字节)", fileName, Files.size(localFile));
} else { } else {
logger.debug("【录音-步骤6】录音文件已存在本地,跳过下载: {} ({} 字节)", logger.debug("【录音】文件已存在,跳过下载: {}", fileName);
fileName, Files.size(localFile));
} }
callRecord.setRecordingFileSize((int) Files.size(localFile)); callRecord.setRecordingFileSize((int) Files.size(localFile));
// ========== 步骤7:上传到OSS ========== // 上传到OSS
logger.debug("【录音-步骤7】开始上传OSS: cityName={}, ossPath={}", cityName, ossPath);
byte[] wavData = Files.readAllBytes(localFile); byte[] wavData = Files.readAllBytes(localFile);
String ossUrl = fileUploadUtil.uploadWav(cityName, ossPath, fileName, wavData); String ossUrl = fileUploadUtil.uploadWav(cityName, ossPath, fileName, wavData);
logger.info("【录音-步骤7】录音上传OSS成功: {}", ossUrl);
callRecord.setRecordingFilePath(ossUrl); callRecord.setRecordingFilePath(ossUrl);
logger.info("【录音】上传OSS成功: {}", ossUrl);
// ========== 步骤8:保存到数据库 ========== // 保存到数据库
logger.debug("【录音-步骤8】保存通话记录到数据库...");
callRecordMapper.insert(callRecord); 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, private MidVoiceCallRecord buildCallRecord(String callRecordId, String deviceNo, String cityCode,
String cityName, String orgCode, String fileName, String cityName, String orgCode, String fileName,
String localPath, Date callStartTime, Date callEndTime, Date callStartTime, Date callEndTime,
int duration, String channelPhone, String remotePhone, int duration, String channelPhone, String remotePhone,
boolean isOutgoing, boolean isAnswered) { boolean isOutgoing, boolean isAnswered) {
MidVoiceCallRecord record = new MidVoiceCallRecord(); MidVoiceCallRecord record = new MidVoiceCallRecord();
// 基础信息
record.setCallRecordId(callRecordId); record.setCallRecordId(callRecordId);
record.setDeviceNo(deviceNo); record.setDeviceNo(deviceNo);
record.setCityCode(cityCode); record.setCityCode(cityCode);
record.setCityName(cityName); record.setCityName(cityName);
record.setOrgCode(orgCode); record.setOrgCode(orgCode);
// 文件信息
record.setRecordingFileName(fileName); record.setRecordingFileName(fileName);
record.setLocalPath(localPath);
// 时间信息
record.setCallStartTime(callStartTime); record.setCallStartTime(callStartTime);
record.setCallEndTime(callEndTime); record.setCallEndTime(callEndTime);
record.setCallDuration(duration); record.setCallDuration(duration);
record.setCallDirection(isOutgoing ? "2" : "1"); // 1呼入,2呼出
// 通话方向:1呼入,2呼出 String localNum = channelPhone != null ? channelPhone : "";
record.setCallDirection(isOutgoing ? "2" : "1"); String remoteNum = remotePhone != null ? remotePhone : "";
// 主叫/被叫号码
String localNumber = channelPhone != null && !channelPhone.isEmpty() ? channelPhone : "";
String remoteNumber = remotePhone != null ? remotePhone : "";
if (isOutgoing) { if (isOutgoing) {
// 呼出:本机打给对方 record.setCallTel(localNum); // 主叫:本机
record.setCallTel(localNumber); // 主叫:本机号码 record.setCalledTel(remoteNum); // 被叫:对方
record.setCalledTel(remoteNumber); // 被叫:对方号码
} else { } else {
// 呼入:对方打给本机 record.setCallTel(remoteNum); // 主叫:对方
record.setCallTel(remoteNumber); // 主叫:对方号码 record.setCalledTel(localNum); // 被叫:本机
record.setCalledTel(localNumber); // 被叫:本机号码
} }
// 通话状态
record.setCallStatus(isAnswered ? "1" : "2"); // 1正常接通,2未接通 record.setCallStatus(isAnswered ? "1" : "2"); // 1正常接通,2未接通
return record; 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) { private Map<String, String> getExtensionNumbers(String deviceHost, String authToken) {
Long begTime = RecordParser.parseBegTime(record); try {
return begTime != null ? new Date(begTime * 1000) : null; String extUrl = "http://" + deviceHost + "/service/ext/number";
return vaaHttpUtil.getExtensionNumbers(extUrl, authToken);
} catch (Exception e) {
logger.warn("【设备】获取分机号码失败,将无法解析本机号码: {}", e.getMessage());
return Collections.emptyMap();
} }
/**
* 从Map中获取字符串值
*/
private String getStringValue(Map<String, Object> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : null;
} }
/** /**
* 从Map中获取整数值 * 保存设备连接日志到 mid_voice_device_log
*/ */
private Integer getIntValue(Map<String, Object> map, String key, Integer defaultValue) { private void saveDeviceLog(String deviceId, String deviceNo, String cityCode, String cityName,
Object value = map.get(key); String ipAddress, Integer devicePort, String connectType,
if (value == null) { String connectStatus, String failReason) {
return defaultValue;
}
try { try {
return Integer.parseInt(value.toString()); MidVoiceDeviceLog log = new MidVoiceDeviceLog();
} catch (NumberFormatException e) { log.setDeviceId(deviceId);
return defaultValue; 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());
} }
} }
/** // ==================== 工具方法 ====================
* URL编码
*/
private String urlEncode(String value) throws Exception {
return URLEncoder.encode(value, "UTF-8");
}
// ==================== 内部类 ====================
/**
* 时间范围
*/
private static class TimeRange {
final long startTime;
final long endTime;
TimeRange(long startTime, long endTime) { private String buildHost(String ip, Integer port) {
this.startTime = startTime; return ip + (port != null && port != 80 ? ":" + port : "");
this.endTime = endTime;
} }
@Override private String getStr(Map<String, Object> map, String key) {
public String toString() { Object v = map.get(key);
return String.format("[%d, %d]", startTime, endTime); return v != null ? v.toString() : null;
} }
private Integer getInt(Map<String, Object> 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 String urlEncode(String value) throws Exception {
* 同步结果 return URLEncoder.encode(value, "UTF-8");
*/
private static class SyncResult {
int successCount = 0;
int failCount = 0;
Date latestCallTime = null;
} }
} }

48
src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigProvider.java

@ -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<MidVoiceChannelConfig> 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();
}

115
src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigService.java

@ -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<ChannelConfigProvider> 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<MidVoiceChannelConfig> 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<MidVoiceChannelConfig> 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;
}
}

46
src/main/java/com/threecloud/dataserviceyy/service/channel/DatabaseChannelConfigProvider.java

@ -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<MidVoiceChannelConfig> getChannelConfigs(String deviceNo, String deviceIp, String authToken) {
List<MidVoiceChannelConfig> 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";
}
}

138
src/main/java/com/threecloud/dataserviceyy/service/channel/EboxChannelConfigProvider.java

@ -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<MidVoiceChannelConfig> getChannelConfigs(String deviceNo, String deviceIp, String authToken) {
List<MidVoiceChannelConfig> 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<MidVoiceChannelConfig> 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<MidVoiceChannelConfig> 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());
}
}
}
}

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

@ -16,30 +16,29 @@ import java.util.Map;
* VAA录音盒HTTP工具类 * VAA录音盒HTTP工具类
* 用于与录音盒设备进行HTTP通信 * 用于与录音盒设备进行HTTP通信
* 参考文档: ebox_developer_guide.html (EBOX-8108 电话录音仪开发手册) * 参考文档: ebox_developer_guide.html (EBOX-8108 电话录音仪开发手册)
*
* 说明
* 所有方法都不重试一次失败直接抛出异常由调用方处理
*/ */
@Component @Component
public class VaaHttpUtil { public class VaaHttpUtil {
private static final Logger logger = LoggerFactory.getLogger(VaaHttpUtil.class); private static final Logger logger = LoggerFactory.getLogger(VaaHttpUtil.class);
/** 连接超时:30秒,设备可能在远端网络 */ /** 连接超时:10秒 */
private static final int CONNECT_TIMEOUT = 30000; private static final int CONNECT_TIMEOUT = 10000;
/** API读取超时:60秒 */ /** API读取超时:30秒 */
private static final int READ_TIMEOUT = 60000; private static final int READ_TIMEOUT = 30000;
/** 文件下载读取超时:5分钟,音频文件可能较大 */ /** 文件下载读取超时:2分钟 */
private static final int DOWNLOAD_READ_TIMEOUT = 300000; private static final int DOWNLOAD_READ_TIMEOUT = 120000;
/** 重试次数 */
private static final int MAX_RETRY = 3;
/** 重试间隔基数(毫秒) */
private static final int RETRY_BASE_DELAY = 2000;
/** /**
* HTTP登录认证 * HTTP登录认证无重试
* @param loginUrl 登录URL例如http://192.168.1.100/authorize?username=admin&password=admin123 * @param loginUrl 登录URL
* @return 登录成功后返回Authorization Cookie值 * @return 登录成功后返回Authorization Cookie值
* @throws Exception 登录失败直接抛出异常
*/ */
public String httpLogin(String loginUrl) throws Exception { public String httpLogin(String loginUrl) throws Exception {
return executeWithRetry(() -> {
logger.debug("正在登录录音盒: {}", loginUrl); logger.debug("正在登录录音盒: {}", loginUrl);
HttpURLConnection conn = null; HttpURLConnection conn = null;
@ -66,17 +65,16 @@ public class VaaHttpUtil {
conn.disconnect(); conn.disconnect();
} }
} }
}, "登录");
} }
/** /**
* HTTP访问API获取数据 * HTTP访问API获取数据无重试
* @param apiUrl API地址例如http://192.168.1.100/service/running/channel * @param apiUrl API地址
* @param authorization 登录后返回的Authorization Cookie值 * @param authorization 登录后返回的Authorization Cookie值
* @return 返回JSON字符串 * @return 返回JSON字符串
* @throws Exception 访问失败直接抛出异常
*/ */
public String httpVisit(String apiUrl, String authorization) throws Exception { public String httpVisit(String apiUrl, String authorization) throws Exception {
return executeWithRetry(() -> {
logger.debug("访问API: {}", apiUrl); logger.debug("访问API: {}", apiUrl);
HttpURLConnection conn = null; HttpURLConnection conn = null;
@ -114,17 +112,16 @@ public class VaaHttpUtil {
conn.disconnect(); conn.disconnect();
} }
} }
}, "API访问");
} }
/** /**
* 下载录音文件重试 * 下载录音文件重试
* @param fileUrl 文件URL例如http://192.168.1.100/record/2026/05/20/OUT-xxx.wav * @param fileUrl 文件URL
* @param savePath 保存路径 * @param savePath 保存路径
* @param authorization 登录后返回的Authorization Cookie值 * @param authorization 登录后返回的Authorization Cookie值
* @throws Exception 下载失败直接抛出异常
*/ */
public void httpDown(String fileUrl, String savePath, String authorization) throws Exception { public void httpDown(String fileUrl, String savePath, String authorization) throws Exception {
executeWithRetry(() -> {
logger.info("开始下载录音文件: {} -> {}", fileUrl, savePath); logger.info("开始下载录音文件: {} -> {}", fileUrl, savePath);
// 确保目录存在 // 确保目录存在
@ -153,17 +150,10 @@ public class VaaHttpUtil {
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int bytesRead; int bytesRead;
long totalBytes = 0; long totalBytes = 0;
long lastLogTime = System.currentTimeMillis();
while ((bytesRead = inputStream.read(buffer)) != -1) { while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead); outputStream.write(buffer, 0, bytesRead);
totalBytes += bytesRead; totalBytes += bytesRead;
// 每10秒打印一次进度
long now = System.currentTimeMillis();
if (now - lastLogTime > 10000) {
logger.info("下载进度: {} MB", totalBytes / 1024 / 1024);
lastLogTime = now;
}
} }
outputStream.flush(); outputStream.flush();
@ -179,55 +169,6 @@ public class VaaHttpUtil {
conn.disconnect(); conn.disconnect();
} }
} }
return null;
}, "文件下载");
}
/**
* 带重试的执行器
*/
private <T> T executeWithRetry(RetryableTask<T> 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;
}
}
}
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> {
T execute() throws Exception;
} }
private void closeQuietly(Closeable c) { private void closeQuietly(Closeable c) {
@ -304,8 +245,7 @@ public class VaaHttpUtil {
/** /**
* 获取分机号码配置 * 获取分机号码配置
* EBOX接口: GET /service/ext/number * EBOX接口: GET /service/ext/number
* 返回每条线路的分机号码: {"1":"8001","2":"8002",...} * @param extUrl 接口地址
* @param extUrl 接口地址例如http://192.168.1.100/service/ext/number
* @param authorization 登录后返回的Authorization Cookie值 * @param authorization 登录后返回的Authorization Cookie值
* @return 通道-分机号映射 Map<channel, phoneNumber> * @return 通道-分机号映射 Map<channel, phoneNumber>
*/ */
@ -315,7 +255,6 @@ public class VaaHttpUtil {
return new java.util.HashMap<>(); return new java.util.HashMap<>();
} }
try { try {
// EBOX返回的是JSON对象,key是通道号(1-8),value是分机号码
com.alibaba.fastjson2.JSONObject jsonObj = JSON.parseObject(jsonData); com.alibaba.fastjson2.JSONObject jsonObj = JSON.parseObject(jsonData);
Map<String, String> result = new java.util.HashMap<>(); Map<String, String> result = new java.util.HashMap<>();
for (String key : jsonObj.keySet()) { for (String key : jsonObj.keySet()) {

36
src/main/resources/application.yml

@ -2,7 +2,6 @@ spring:
application: application:
name: dataservice-yy name: dataservice-yy
# 引入外部配置文件(部署时修改 config/application-external.yml) # 引入外部配置文件(部署时修改 config/application-external.yml)
# 注意:路径是相对于 JAR 包所在目录的
config: config:
import: optional:file:./config/application-external.yml import: optional:file:./config/application-external.yml
@ -13,58 +12,23 @@ mybatis:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# ==================== 日志配置 ====================
# 【内网测试专用】详细日志级别配置
logging: logging:
level: level:
# 根日志级别:INFO(生产环境建议WARN)
root: INFO root: INFO
# 本项目代码:DEBUG(内网测试时开启,生产建议INFO)
com.threecloud.dataserviceyy: DEBUG com.threecloud.dataserviceyy: DEBUG
# 同步服务:DEBUG(关键业务流程)
com.threecloud.dataserviceyy.service.VaaSyncService: DEBUG com.threecloud.dataserviceyy.service.VaaSyncService: DEBUG
# 通道配置服务:DEBUG
com.threecloud.dataserviceyy.service.channel: DEBUG
# HTTP工具:DEBUG(查看API请求响应)
com.threecloud.dataserviceyy.util.VaaHttpUtil: DEBUG com.threecloud.dataserviceyy.util.VaaHttpUtil: DEBUG
# 文件上传:INFO
com.threecloud.dataserviceyy.util.FileUploadUtil: INFO com.threecloud.dataserviceyy.util.FileUploadUtil: INFO
# MyBatis SQL日志:DEBUG(查看执行的SQL)
com.threecloud.dataserviceyy.mapper: DEBUG com.threecloud.dataserviceyy.mapper: DEBUG
# Spring框架:WARN(减少框架日志噪音)
org.springframework: WARN org.springframework: WARN
org.springframework.web: WARN
# Hikari连接池:INFO
com.zaxxer.hikari: INFO com.zaxxer.hikari: INFO
# Tomcat:INFO
org.apache.catalina: INFO
# 日志文件配置
file: file:
name: logs/app.log name: logs/app.log
# 日志格式
pattern: pattern:
# 文件格式:带时间、线程、级别、类名
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" 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" console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - %msg%n"
# 日志文件滚动配置
logback: logback:
rollingpolicy: rollingpolicy:
# 单个文件最大10MB
max-file-size: 10MB max-file-size: 10MB
# 保留30天
max-history: 30 max-history: 30
# 总大小不超过1GB
total-size-cap: 1GB total-size-cap: 1GB

104
src/main/resources/mapper/MidVoiceChannelConfigMapper.xml

@ -1,104 +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.MidVoiceChannelConfigMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.MidVoiceChannelConfig">
<id column="id" property="id"/>
<result column="city_code" property="cityCode"/>
<result column="city_name" property="cityName"/>
<result column="device_no" property="deviceNo"/>
<result column="channel_no" property="channelNo"/>
<result column="phone_number" property="phoneNumber"/>
<result column="channel_name" property="channelName"/>
<result column="channel_status" property="channelStatus"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="remarks" property="remarks"/>
</resultMap>
<!-- 插入通道配置 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="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 (
#{cityCode}, #{cityName}, #{deviceNo}, #{channelNo}, #{phoneNumber},
#{channelName}, #{channelStatus}, NOW(), NOW(), #{remarks}
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM mid_voice_channel_config WHERE id = #{id}
</select>
<!-- 根据设备编码和通道号查询 -->
<select id="selectByDeviceAndChannel" resultMap="BaseResultMap">
SELECT * FROM mid_voice_channel_config
WHERE device_no = #{deviceNo} AND channel_no = #{channelNo}
LIMIT 1
</select>
<!-- 查询设备的所有通道 -->
<select id="selectByDeviceNo" resultMap="BaseResultMap">
SELECT * FROM mid_voice_channel_config
WHERE device_no = #{deviceNo}
ORDER BY channel_no
</select>
<!-- 根据电话号码查询通道 -->
<select id="selectByPhoneNumber" resultMap="BaseResultMap">
SELECT * FROM mid_voice_channel_config
WHERE phone_number = #{phoneNumber}
LIMIT 1
</select>
<!-- 查询所有通道配置 -->
<select id="selectAll" resultMap="BaseResultMap">
SELECT * FROM mid_voice_channel_config
ORDER BY device_no, channel_no
</select>
<!-- 更新通道配置 -->
<update id="update">
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>
<!-- 更新通道状态 -->
<update id="updateStatus">
UPDATE mid_voice_channel_config
SET channel_status = #{channelStatus},
update_time = NOW()
WHERE id = #{id}
</update>
<!-- 删除通道配置 -->
<delete id="deleteById">
DELETE FROM mid_voice_channel_config WHERE id = #{id}
</delete>
<!-- 批量插入 -->
<insert id="batchInsert">
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
<foreach collection="list" item="item" separator=",">
(
#{item.cityCode}, #{item.cityName}, #{item.deviceNo}, #{item.channelNo}, #{item.phoneNumber},
#{item.channelName}, #{item.channelStatus}, NOW(), NOW(), #{item.remarks}
)
</foreach>
</insert>
</mapper>

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

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.threecloud.dataserviceyy.mapper.VoiceSyncMapper"> <mapper namespace="com.threecloud.dataserviceyy.mapper.VoiceSyncMapper">
<!-- 查询所有在线语音设备(含每个设备的账号密码) -->
<select id="getAllYysb" resultType="java.util.Map"> <select id="getAllYysb" resultType="java.util.Map">
SELECT id AS ID, SELECT id AS ID,
device_no AS UUID, device_no AS UUID,
@ -8,117 +10,11 @@
city_code AS ORGAN_ID, city_code AS ORGAN_ID,
ip_address AS IP, ip_address AS IP,
device_port AS PORT, device_port AS PORT,
org_code AS ORG_CODE org_code AS ORG_CODE,
username AS USERNAME,
password AS PASSWORD
FROM mid_voice.mid_voice_device_config FROM mid_voice.mid_voice_device_config
WHERE device_status = '0' and city_code = '340100' WHERE device_status = '0'
</select>
<select id="getTdByPhone" resultType="java.util.Map">
SELECT ID, UUID
FROM YYDC_SBTD
WHERE SFYX_ST = '1'
AND PHONE = #{phone}
AND ROWNUM = 1
</select>
<select id="getTdByPhone2" resultType="java.util.Map">
SELECT * FROM (
SELECT ID, UUID, PHONE, '1' THFX
FROM YYDC_SBTD
WHERE SFYX_ST = '1'
AND PHONE = #{zjhm}
UNION ALL
SELECT ID, UUID, PHONE, '0' THFX
FROM YYDC_SBTD
WHERE SFYX_ST = '1'
AND PHONE = #{bjhm}
) WHERE ROWNUM = 1
</select>
<select id="getYysbByUuid" resultType="java.util.Map">
SELECT ORGAN_NAME, ORGAN_ID, ID
FROM YYDC_YYSB
WHERE SFYX_ST = '1'
AND UUID = #{uuid}
</select>
<insert id="saveThjl">
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')
</insert>
<!-- VAA同步相关SQL -->
<select id="getChannelByNumberAndUuid" resultType="java.util.Map">
SELECT ID AS TDID, PHONE
FROM YYDC_SBTD
WHERE SFYX_ST = '1'
AND TDHM = #{channel}
AND UUID = #{uuid}
AND ROWNUM = 1
</select>
<update id="updateChannelStatus">
UPDATE YYDC_SBTD
SET TDZT = #{TDZT},
TBSJ = #{TBSJ},
XGR_ID = 0,
XGSJ = #{TBSJ}
WHERE SFYX_ST = '1'
AND TDHM = #{TDHM}
AND UUID = #{UUID}
</update>
<select id="getLastSyncTime" resultType="java.util.Date">
SELECT MAX(GXSJ)
FROM YYDC_TBRZ
WHERE TBBZ = '1'
AND CODE = #{code}
</select> </select>
<insert id="saveSyncLog">
INSERT INTO YYDC_TBRZ (CODE, KSSJ, JSSJ, GXSJ, TBBZ, TBSM)
VALUES (#{CODE}, #{KSSJ}, #{JSSJ}, #{GXSJ}, #{TBBZ}, #{TBSM})
</insert>
<select id="checkThjlExists" resultType="java.lang.String">
SELECT ID FROM YYDC_YYTH WHERE ID = #{thid}
</select>
<insert id="insertThjl">
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')
</insert>
<update id="updateThjl">
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}
</update>
</mapper> </mapper>

Loading…
Cancel
Save