diff --git a/config/application-external.yml b/config/application-external.yml
index 50b2a4f..6a473ce 100644
--- a/config/application-external.yml
+++ b/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
diff --git a/pom.xml b/pom.xml
index 84b1019..33e7df2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
8.0.28
2.2.2
1.4.6
- 8.6.0
+ 8.2.0
21.9.0.0
1.18.24
@@ -71,6 +71,8 @@
cn.com.kingbase
kingbase8
${kingbase.version}
+ system
+ ${project.basedir}/src/main/resources/kingbase8-8.2.0.jar
com.oracle.database.jdbc
@@ -177,6 +179,7 @@
${spring-boot.version}
com.threecloud.dataserviceyy.DataserviceYyApplication
+ true
false
diff --git a/src/main/java/com/threecloud/dataserviceyy/config/FtpSyncProperties.java b/src/main/java/com/threecloud/dataserviceyy/config/FtpSyncProperties.java
index bd6f7bf..f43ec43 100644
--- a/src/main/java/com/threecloud/dataserviceyy/config/FtpSyncProperties.java
+++ b/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 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 getCities() { return cities; }
public void setCities(List cities) { this.cities = cities; }
diff --git a/src/main/java/com/threecloud/dataserviceyy/service/FtpSyncService.java b/src/main/java/com/threecloud/dataserviceyy/service/FtpSyncService.java
index d83b436..b44caca 100644
--- a/src/main/java/com/threecloud/dataserviceyy/service/FtpSyncService.java
+++ b/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,10 +219,10 @@ public class FtpSyncService {
/**
* 处理单个txt文件
*
- * @return true=成功,false=失败
+ * @return 1=全部成功处理完毕, 2=有录音暂未就绪(下次重试), 0=解析失败
*/
- private boolean processTxtFile(FTPClient ftp, FTPFile file, FtpCityConfig city,
- Set existingIds) throws Exception {
+ private int processTxtFile(FTPClient ftp, FTPFile file, FtpCityConfig city,
+ Set existingIds) throws Exception {
String fileName = file.getName();
String sourcePath = joinPath(city.getFtpSourceDir(), fileName);
long fileSize = file.getSize();
@@ -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)) {
- successCount++;
- } else {
- skipCount++;
+ int result = processLine(line, city, existingIds, ftp);
+ switch (result) {
+ case 1:
+ successCount++;
+ 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 existingIds,
- FTPClient ftp) throws Exception {
+ private int processLine(String line, FtpCityConfig city, Set 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;
}
/**
diff --git a/src/main/resources/kingbase8-8.2.0.jar b/src/main/resources/kingbase8-8.2.0.jar
new file mode 100644
index 0000000..aa46cbc
Binary files /dev/null and b/src/main/resources/kingbase8-8.2.0.jar differ
diff --git a/src/main/resources/mapper/FtpProcessedFileMapper.xml b/src/main/resources/mapper/FtpProcessedFileMapper.xml
index 0823425..d389f21 100644
--- a/src/main/resources/mapper/FtpProcessedFileMapper.xml
+++ b/src/main/resources/mapper/FtpProcessedFileMapper.xml
@@ -18,7 +18,7 @@
-
+
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
diff --git a/src/main/resources/mapper/MidVoiceCallRecordMapper.xml b/src/main/resources/mapper/MidVoiceCallRecordMapper.xml
index 5feb9fc..f143050 100644
--- a/src/main/resources/mapper/MidVoiceCallRecordMapper.xml
+++ b/src/main/resources/mapper/MidVoiceCallRecordMapper.xml
@@ -27,7 +27,7 @@
-
+
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,