Browse Source

降级处理,语音异常判断增加

ftpmain
wang 2 weeks ago
parent
commit
261330156d
  1. 14
      config/application-external.yml
  2. 5
      pom.xml
  3. 6
      src/main/java/com/threecloud/dataserviceyy/config/FtpSyncProperties.java
  4. 136
      src/main/java/com/threecloud/dataserviceyy/service/FtpSyncService.java
  5. BIN
      src/main/resources/kingbase8-8.2.0.jar
  6. 2
      src/main/resources/mapper/FtpProcessedFileMapper.xml
  7. 2
      src/main/resources/mapper/MidVoiceCallRecordMapper.xml

14
config/application-external.yml

@ -22,6 +22,16 @@ ftp-sync:
# 是否启用FTP同步
enabled: true
# ==================== 定时任务执行周期 (Cron表达式) ====================
# 常用配置示例:
# "0 0/5 * * * ?" 每 5 分钟执行一次(测试推荐)
# "0 0/10 * * * ?" 每 10 分钟执行一次
# "0 0/30 * * * ?" 每 30 分钟执行一次
# "0 0 0/1 * * ?" 每 1 小时执行一次
# "0 0 0/2 * * ?" 每 2 小时执行一次(默认)
# "0 0 2 * * ?" 每天凌晨 2:00 执行一次
sync-interval-cron: "0 0 0/2 * * ?"
# 本地临时文件目录
temp-path: ./vaa-ftp-temp
@ -43,7 +53,7 @@ ftp-sync:
cities:
# ===== 淮南 =====
- city-code: "340400"
city-name: "淮南"
city-name: "淮南"
ftp-host: 10.126.129.7
ftp-port: 9979
ftp-username: yyfile
@ -56,7 +66,7 @@ ftp-sync:
# ===== 蚌埠 =====
- city-code: "340300"
city-name: "蚌埠"
city-name: "蚌埠"
ftp-host: 10.126.54.104
ftp-port: 9989
ftp-username: ftpadmin

5
pom.xml

@ -20,7 +20,7 @@
<mysql.version>8.0.28</mysql.version>
<mybatis.version>2.2.2</mybatis.version>
<pagehelper.version>1.4.6</pagehelper.version>
<kingbase.version>8.6.0</kingbase.version>
<kingbase.version>8.2.0</kingbase.version>
<ojdbc.version>21.9.0.0</ojdbc.version>
<!-- 工具库版本 -->
<lombok.version>1.18.24</lombok.version>
@ -71,6 +71,8 @@
<groupId>cn.com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<version>${kingbase.version}</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/kingbase8-8.2.0.jar</systemPath>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
@ -177,6 +179,7 @@
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.threecloud.dataserviceyy.DataserviceYyApplication</mainClass>
<includeSystemScope>true</includeSystemScope>
<skip>false</skip>
</configuration>
<executions>

6
src/main/java/com/threecloud/dataserviceyy/config/FtpSyncProperties.java

@ -42,6 +42,9 @@ public class FtpSyncProperties {
/** OSS服务基础地址(用于mp3直接URL拼接) */
private String ossBaseUrl;
/** 定时同步Cron表达式(默认每2小时执行一次) */
private String syncIntervalCron = "0 0 0/2 * * ?";
/** 地市FTP配置列表 */
private List<FtpCityConfig> cities = new ArrayList<>();
@ -63,6 +66,9 @@ public class FtpSyncProperties {
public String getOssBaseUrl() { return ossBaseUrl; }
public void setOssBaseUrl(String ossBaseUrl) { this.ossBaseUrl = ossBaseUrl; }
public String getSyncIntervalCron() { return syncIntervalCron; }
public void setSyncIntervalCron(String syncIntervalCron) { this.syncIntervalCron = syncIntervalCron; }
public List<FtpCityConfig> getCities() { return cities; }
public void setCities(List<FtpCityConfig> cities) { this.cities = cities; }

136
src/main/java/com/threecloud/dataserviceyy/service/FtpSyncService.java

@ -148,27 +148,39 @@ public class FtpSyncService {
int fileSuccess = 0;
int fileFail = 0;
int fileSkipped = 0;
int filePending = 0;
String todayStr = DateUtil.formatDate(new Date(), "yyyyMMdd");
logger.info("【FTP地市】当前同步日期: {} (仅处理今日数据文件)", todayStr);
for (FTPFile file : files) {
if (!FtpUtil.isTxtFile(file)) {
continue;
}
String fileName = file.getName();
// 检查是否为有效的数据TXT文件(支持跨天、一天内多个切片文件;排除空文件或未封口占位文件)
// 1. 只处理今天的数据文件(例如 20260819*.txt)
if (!fileName.startsWith(todayStr)) {
logger.debug("【FTP文件】非今日数据文件,跳过: {}", fileName);
continue;
}
// 2. 检查是否为有效的数据TXT文件
if (!isValidDataTxtFile(file)) {
logger.debug("【FTP文件】非有效数据文件,跳过: {} ({} 字节)",
fileName, file.getSize());
continue;
}
// 检查是否已处理过
// 3. 检查是否已处理过
if (processedFiles.contains(fileName)) {
logger.debug("【FTP文件】已处理过,跳过: {}", fileName);
fileSkipped++;
continue;
}
try {
if (processTxtFile(ftp, file, city, existingIds)) {
int txtResult = processTxtFile(ftp, file, city, existingIds);
if (txtResult == 1) {
fileSuccess++;
} else if (txtResult == 2) {
filePending++; // 录音未就绪,留待下次定时任务
} else {
fileFail++;
}
@ -181,8 +193,11 @@ public class FtpSyncService {
logger.info("【FTP地市】重新连接 FTP 成功,正在重试处理刚才失败的文件: {}", fileName);
// 重新尝试处理文件
if (processTxtFile(ftp, file, city, existingIds)) {
int txtResult = processTxtFile(ftp, file, city, existingIds);
if (txtResult == 1) {
fileSuccess++;
} else if (txtResult == 2) {
filePending++;
} else {
fileFail++;
}
@ -194,8 +209,8 @@ public class FtpSyncService {
}
}
}
logger.info("【FTP地市】{} 同步完成: 成功{}个文件, 跳过{}个(已处理), 失败{}个文件",
city.getCityName(), fileSuccess, fileSkipped, fileFail);
logger.info("【FTP地市】{} 同步完成: 成功{}个文件, 待就绪(等待下次同步){}个, 跳过{}个(已处理), 失败{}个文件",
city.getCityName(), fileSuccess, filePending, fileSkipped, fileFail);
} finally {
FtpUtil.disconnect(ftp);
}
@ -204,9 +219,9 @@ public class FtpSyncService {
/**
* 处理单个txt文件
*
* @return true=成功false=失败
* @return 1=全部成功处理完毕, 2=有录音暂未就绪(下次重试), 0=解析失败
*/
private boolean processTxtFile(FTPClient ftp, FTPFile file, FtpCityConfig city,
private int processTxtFile(FTPClient ftp, FTPFile file, FtpCityConfig city,
Set<String> existingIds) throws Exception {
String fileName = file.getName();
String sourcePath = joinPath(city.getFtpSourceDir(), fileName);
@ -217,7 +232,9 @@ public class FtpSyncService {
byte[] txtBytes = FtpUtil.downloadFile(ftp, sourcePath);
int lineCount = 0;
int successCount = 0;
int skipCount = 0;
int existCount = 0;
int pendingCount = 0;
int invalidCount = 0;
int failCount = 0;
String errorMsg = null;
@ -230,10 +247,20 @@ public class FtpSyncService {
}
lineCount++;
try {
if (processLine(line, city, existingIds, ftp)) {
int result = processLine(line, city, existingIds, ftp);
switch (result) {
case 1:
successCount++;
} else {
skipCount++;
break;
case 2:
existCount++;
break;
case 3:
pendingCount++;
break;
default:
invalidCount++;
break;
}
} catch (Exception e) {
// 如果是网络连接重置或其它 IO 严重异常,不在此吞掉,直接向上抛出以触发外部重连重试
@ -252,39 +279,40 @@ public class FtpSyncService {
}
long processTime = System.currentTimeMillis() - fileStartTime;
logger.info("【FTP文件】{} 解析完成: 共{}行, 新增{}条, 跳过{}条, 失败{}条, 耗时{}ms",
fileName, lineCount, successCount, skipCount, failCount, processTime);
// 记录已处理的文件(避免下次重复处理)
recordProcessedFile(city, fileName, fileSize, lineCount, successCount, failCount, processTime, errorMsg);
// 注意:不进行归档操作,文件保留在原位置
// 如需归档,请取消下面代码的注释
// try {
// String archivePath = joinPath(city.getFtpArchiveDir(), fileName);
// FtpUtil.ensureDir(ftp, city.getFtpArchiveDir());
// FtpUtil.rename(ftp, sourcePath, archivePath);
// logger.info("【FTP文件】已归档: {} -> {}", sourcePath, archivePath);
// } catch (Exception e) {
// logger.error("【FTP文件】归档失败: {}, 原因={}", fileName, e.getMessage());
// }
return failCount == 0;
logger.info("【FTP文件】{} 处理结果: 共{}行, 新增入库{}条, 已存在{}条, 录音待就绪{}条, 失败{}条, 耗时{}ms",
fileName, lineCount, successCount, existCount, pendingCount, failCount, processTime);
// 关键逻辑:只有当该文件里所有录音均已处理完成(无待就绪、无失败)时,才标记该TXT文件为已处理
if (pendingCount == 0 && failCount == 0) {
recordProcessedFile(city, fileName, fileSize, lineCount, successCount, failCount, processTime, null);
logger.info("【FTP文件】{} 全部记录已处理完成,标记为已完成", fileName);
return 1;
} else if (pendingCount > 0 && failCount == 0) {
logger.info("【FTP文件】{} 仍有 {} 条录音在FTP未就绪,暂不标记完成,下次定时任务将继续自动重试", fileName, pendingCount);
return 2;
} else {
logger.warn("【FTP文件】{} 存在 {} 行解析失败", fileName, failCount);
return 0;
}
}
/**
* 处理单行记录
* 处理单行数据
*
* @return true=成功入库false=跳过
* @param line 单行文本内容
* @param city 地市配置
* @param existingIds 已存在的通话记录ID集合用于防重
* @param ftp FTP客户端连接
* @return 1=成功入库, 2=已存在跳过, 3=录音未就绪跳过, 0=无效行
*/
private boolean processLine(String line, FtpCityConfig city, Set<String> existingIds,
private int processLine(String line, FtpCityConfig city, Set<String> existingIds,
FTPClient ftp) throws Exception {
// 按分隔符拆分字段(*分隔,需转义正则特殊字符)
String separator = java.util.regex.Pattern.quote(properties.getFieldSeparator());
String[] fields = line.split(separator, -1);
if (fields.length < MIN_FIELD_COUNT) {
logger.debug("【FTP行】字段数不足({}<{}),跳过: {}", fields.length, MIN_FIELD_COUNT, line);
return false;
return 0;
}
// ========== 字段解析(老淮南FTP数据格式)==========
@ -299,14 +327,14 @@ public class FtpSyncService {
// 必填字段校验
if (kssj.isEmpty() || jssj.isEmpty() || hjls.isEmpty() || hjzls.isEmpty()) {
return false;
return 0;
}
// 时间字符串转 Date 对象
Date callStartTime = DateUtil.parseDate14(kssj);
Date callEndTime = DateUtil.parseDate14(jssj);
if (callStartTime == null) {
return false;
return 0;
}
int thsc = parseInt(thscStr, 0);
@ -320,24 +348,19 @@ public class FtpSyncService {
// 防重复检查(内存Set比对,已存在则跳过)
if (existingIds.contains(callRecordId)) {
logger.debug("【FTP行】已存在,跳过: {}", callRecordId);
return false;
logger.debug("【FTP行】已存在数据库中,跳过: {}", callRecordId);
return 2;
}
// 主叫/被叫处理:去除区号前缀
// 配置不存在的区号(如"0000"),因为 startsWith 匹配不上,会保留原始号码
// 后续如需去除区号,把配置改为正确的区号即可,无需重新发包
zjhm = stripAreaCode(zjhm, city.getPhoneAreaCode());
bjhm = stripAreaCode(bjhm, city.getPhoneAreaCode());
// 构建本地临时路径和OSS路径
// 本地路径示例:./vaa-ftp-temp/340100/20240101/ftp/12345_67890.mp3
// OSS路径示例:voice/340100/20240101/12345_67890.mp3
String dateStr = DateUtil.formatDate(callStartTime, "yyyyMMdd");
String ossPath = FilePathUtil.buildOssPath(city.getCityCode(), callStartTime, recordFileName);
// 下载录音文件(FTP路径:{ftp-record-dir}/{yyyyMMdd}/{filename})
// 使用绝对路径下载,避免切换目录导致的问题
String recordDir = city.getFtpRecordDir();
if (recordDir != null && recordDir.endsWith("/")) {
recordDir = recordDir.substring(0, recordDir.length() - 1);
@ -345,26 +368,27 @@ public class FtpSyncService {
String remoteRecordPath = recordDir + "/" + dateStr + "/" + recordFileName;
logger.info("【FTP行】准备下载录音: {}", remoteRecordPath);
byte[] recordBytes = FtpUtil.downloadFile(ftp, remoteRecordPath);
String ossUrl = null;
int fileSize = 0;
if (recordBytes != null && recordBytes.length > 0) {
fileSize = recordBytes.length;
logger.info("【FTP行】下载录音: {} ({} 字节)", recordFileName, fileSize);
// 上传到OSS(获取可访问的URL)
ossUrl = fileUploadUtil.uploadWav(ossPath, recordFileName, recordBytes);
logger.info("【FTP行】上传OSS: {}", ossUrl);
} else {
logger.warn("【FTP行】录音文件不存在或为空,跳过OSS上传: {}", remoteRecordPath);
// 如果录音文件在 FTP 上暂未生成或为空:不插入数据库(绝不放空值),等待下次定时任务
if (recordBytes == null || recordBytes.length == 0) {
logger.info("【FTP行】录音文件在FTP暂未就绪,跳过本次入库,等待下次定时同步: {}", remoteRecordPath);
return 3;
}
// 录音文件存在:上传到OSS
int fileSize = recordBytes.length;
logger.info("【FTP行】下载录音成功: {} ({} 字节)", recordFileName, fileSize);
String ossUrl = fileUploadUtil.uploadWav(ossPath, recordFileName, recordBytes);
logger.info("【FTP行】上传OSS成功: {}", ossUrl);
// 构建实体对象并入库
MidVoiceCallRecord rec = buildCallRecord(city, callRecordId, callStartTime, callEndTime,
thsc, isOutgoing, zjhm, bjhm, recordFileName, fileSize, ossUrl);
callRecordMapper.insert(rec);
existingIds.add(callRecordId); // 防止同批内重复(本次循环内)
logger.info("【FTP行】保存成功: id={}, callRecordId={}", rec.getId(), callRecordId);
existingIds.add(callRecordId); // 防止同批内重复
logger.info("【FTP行】保存成功: callRecordId={}", callRecordId);
return true;
return 1;
}
/**

BIN
src/main/resources/kingbase8-8.2.0.jar

Binary file not shown.

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

@ -18,7 +18,7 @@
</resultMap>
<!-- 插入处理记录 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
<insert id="insert">
INSERT INTO mid_ftp_processed_file (
city_code, city_name, file_name, file_size, process_status,
record_count, success_count, fail_count, process_time, error_msg, create_time

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

@ -27,7 +27,7 @@
</resultMap>
<!-- 插入通话记录 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
<insert id="insert">
INSERT INTO mid_voice_call_record (
city_code, city_name, call_record_id, call_tel, called_tel,
call_start_time, call_end_time, call_duration, call_direction,

Loading…
Cancel
Save