Browse Source

first commit

main
wang 1 week ago
commit
20252cfba3
  1. BIN
      .DS_Store
  2. 33
      .gitignore
  3. 216
      README.md
  4. 244
      VAA_SYNC_README.md
  5. 76
      config/application-external.yml
  6. BIN
      logs/.DS_Store
  7. 76
      logs/app.log
  8. BIN
      logs/app.log.2026-05-21.0.gz
  9. BIN
      logs/app.log.2026-05-22.0.gz
  10. BIN
      logs/app.log.2026-05-23.0.gz
  11. BIN
      logs/app.log.2026-05-25.0.gz
  12. BIN
      logs/server.jar
  13. 192
      pom.xml
  14. BIN
      src/.DS_Store
  15. BIN
      src/main/.DS_Store
  16. BIN
      src/main/java/.DS_Store
  17. 44
      src/main/java/com/threecloud/dataserviceyy/DataserviceYyApplication.java
  18. 63
      src/main/java/com/threecloud/dataserviceyy/config/DataSourceConfig.java
  19. 12
      src/main/java/com/threecloud/dataserviceyy/config/DynamicDataSourceConfig.java
  20. 16
      src/main/java/com/threecloud/dataserviceyy/config/FileUploadConfig.java
  21. 13
      src/main/java/com/threecloud/dataserviceyy/config/SyncTargetConfig.java
  22. 270
      src/main/java/com/threecloud/dataserviceyy/controller/VoiceBoxController.java
  23. 157
      src/main/java/com/threecloud/dataserviceyy/entity/CallRecord.java
  24. 93
      src/main/java/com/threecloud/dataserviceyy/entity/DeviceMatchResult.java
  25. 143
      src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceCallRecord.java
  26. 78
      src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceChannelConfig.java
  27. 140
      src/main/java/com/threecloud/dataserviceyy/entity/ResultEntity.java
  28. 77
      src/main/java/com/threecloud/dataserviceyy/entity/SyncLog.java
  29. 125
      src/main/java/com/threecloud/dataserviceyy/entity/VoiceRecord.java
  30. 22
      src/main/java/com/threecloud/dataserviceyy/enums/SyncStatus.java
  31. 69
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java
  32. 66
      src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceChannelConfigMapper.java
  33. 55
      src/main/java/com/threecloud/dataserviceyy/mapper/SyncLogMapper.java
  34. 67
      src/main/java/com/threecloud/dataserviceyy/mapper/VoiceRecordMapper.java
  35. 46
      src/main/java/com/threecloud/dataserviceyy/mapper/VoiceSyncMapper.java
  36. 547
      src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java
  37. 48
      src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigProvider.java
  38. 115
      src/main/java/com/threecloud/dataserviceyy/service/channel/ChannelConfigService.java
  39. 46
      src/main/java/com/threecloud/dataserviceyy/service/channel/DatabaseChannelConfigProvider.java
  40. 138
      src/main/java/com/threecloud/dataserviceyy/service/channel/EboxChannelConfigProvider.java
  41. 76
      src/main/java/com/threecloud/dataserviceyy/util/DataUtil.java
  42. 57
      src/main/java/com/threecloud/dataserviceyy/util/DateUtil.java
  43. 177
      src/main/java/com/threecloud/dataserviceyy/util/FileCleaner.java
  44. 97
      src/main/java/com/threecloud/dataserviceyy/util/FilePathUtil.java
  45. 290
      src/main/java/com/threecloud/dataserviceyy/util/FileUploadUtil.java
  46. 88
      src/main/java/com/threecloud/dataserviceyy/util/RecordParser.java
  47. 70
      src/main/java/com/threecloud/dataserviceyy/util/SyncTimeUtil.java
  48. 332
      src/main/java/com/threecloud/dataserviceyy/util/VaaHttpUtil.java
  49. 72
      src/main/java/com/threecloud/dataserviceyy/util/fileUtil.java
  50. 70
      src/main/resources/application.yml
  51. 127
      src/main/resources/mapper/MidVoiceCallRecordMapper.xml
  52. 104
      src/main/resources/mapper/MidVoiceChannelConfigMapper.xml
  53. 85
      src/main/resources/mapper/SyncLogMapper.xml
  54. 117
      src/main/resources/mapper/VoiceRecordMapper.xml
  55. 124
      src/main/resources/mapper/VoiceSyncMapper.xml
  56. 16
      src/test/java/com/threecloud/dataserviceyy/DataserviceYyApplicationTests.java

BIN
.DS_Store

Binary file not shown.

33
.gitignore

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

216
README.md

@ -0,0 +1,216 @@
# 语音数据同步服务 (dataservice-yy)
## 项目简介
语音数据同步服务用于从 EBOX 录音盒拉取录音文件,上传到 OSS,并保存通话记录到数据库。
## 目录结构
```
dataservice-yy/
├── dataservice-yy-0.0.1-SNAPSHOT.jar # 主程序 JAR 包
├── config/
│ └── application-external.yml # 外部配置文件(部署时修改)
├── vaa-recordings/ # 本地录音文件存储目录
│ ├── 340100/ # 地市编码
│ │ ├── 20240101/ # 日期目录
│ │ │ └── <device_uuid>/ # 设备目录
│ │ │ └── xxx.wav
│ │ └── 20240102/
│ └── ...
├── logs/
│ └── app.log # 日志文件
└── README.md # 本说明文档
```
## 部署步骤
### 1. 准备环境
- JDK 1.8+
- 人大金仓数据库(已创建 mid_voice schema)
- OSS 服务(已部署并配置好上传接口)
### 2. 创建目录结构
```bash
mkdir -p /opt/dataservice-yy/config
mkdir -p /opt/dataservice-yy/logs
```
### 3. 复制文件
```bash
# 复制 JAR 包
cp dataservice-yy-0.0.1-SNAPSHOT.jar /opt/dataservice-yy/
# 复制配置文件模板
cp config/application-external.yml /opt/dataservice-yy/config/
```
### 4. 修改配置
编辑 `/opt/dataservice-yy/config/application-external.yml`,修改以下配置项:
#### 数据库配置
```yaml
spring:
datasource:
url: jdbc:kingbase8://{数据库IP}:{端口}/kingbase?currentSchema=mid_voice&clientEncoding=utf8
username: {数据库用户名}
password: {数据库密码}
```
#### OSS 配置
```yaml
vaa-sync:
oss:
base-url: http://{OSS服务器IP}:9090
upload-url: http://{OSS服务器IP}:9090/apiOss/oss/fileUpload
appcode: dataservice-yy
appid: {appid}
appsecret: {appsecret}
```
#### 其他配置
```yaml
vaa-sync:
download-path: ./vaa-recordings # 本地录音文件存储路径
sync-interval-cron: "0 0 0/2 * * ?" # 同步间隔(默认每2小时)
device-username: admin # 录音盒登录账号
device-password: admin # 录音盒登录密码
server:
port: 8088 # 服务端口
```
### 5. 启动服务
```bash
cd /opt/dataservice-yy
nohup java -jar dataservice-yy-0.0.1-SNAPSHOT.jar > /dev/null 2>&1 &
```
### 6. 查看日志
```bash
tail -f logs/app.log
```
## 配置文件说明
### application-external.yml
此文件包含所有需要部署时调整的参数,**无需重新打包 JAR**。
| 配置项 | 说明 | 示例 |
|-------|------|------|
| `spring.datasource.url` | 数据库连接URL | `jdbc:kingbase8://53.1.194.60:54321/kingbase?currentSchema=mid_voice&clientEncoding=utf8` |
| `spring.datasource.username` | 数据库用户名 | `dcms_dev` |
| `spring.datasource.password` | 数据库密码 | `sdy@2025#dc$ks` |
| `vaa-sync.oss.base-url` | OSS基础地址 | `http://53.1.194.59:9090` |
| `vaa-sync.oss.upload-url` | OSS上传接口 | `http://53.1.194.59:9090/apiOss/oss/fileUpload` |
| `vaa-sync.oss.appcode` | OSS应用编码 | `dataservice-yy` |
| `vaa-sync.oss.appid` | OSS应用ID | `371a3368-e28e-4ba3-95a3-c31c19cf0ad0` |
| `vaa-sync.oss.appsecret` | OSS应用密钥 | `06a6a80e-f9d2-4b3b-acc0-8d182c876074` |
| `vaa-sync.download-path` | 本地录音存储路径 | `./vaa-recordings` |
| `vaa-sync.sync-interval-cron` | 同步定时任务 | `0 0 0/2 * * ?`(每2小时) |
| `vaa-sync.device-username` | 录音盒登录账号 | `admin` |
| `vaa-sync.device-password` | 录音盒登录密码 | `admin` |
| `server.port` | 服务端口 | `8088` |
## 定时任务说明
同步任务默认每2小时执行一次,可通过修改 `sync-interval-cron` 调整:
| 表达式 | 说明 |
|-------|------|
| `0 0 0/2 * * ?` | 每2小时执行一次(默认) |
| `0 0/30 * * * ?` | 每30分钟执行一次 |
| `0 0 2 * * ?` | 每天凌晨2点执行 |
| `0 0/1 * * * ?` | 每分钟执行(测试用) |
## 数据表说明
### mid_voice_device_config
设备基础配置表,存储录音盒设备信息。
| 字段 | 说明 |
|-----|------|
| device_no | 设备编号(UUID) |
| city_code | 地市编码 |
| city_name | 地市名称 |
| ip_address | IP地址 |
| device_port | 端口号 |
| device_status | 设备状态(0在线) |
### mid_voice_channel_config
通道配置表,存储录音盒通道绑定的电话号码。
| 字段 | 说明 |
|-----|------|
| device_no | 设备编号 |
| channel_no | 通道号(1-8) |
| phone_number | 绑定电话号码 |
### mid_voice_call_record
通话记录表,存储录音文件信息和OSS地址。
| 字段 | 说明 |
|-----|------|
| call_record_id | 通话记录ID(device_no + record_id) |
| device_no | 设备编号 |
| city_code | 地市编码 |
| city_name | 地市名称 |
| call_tel | 主叫号码 |
| called_tel | 被叫号码 |
| call_start_time | 通话开始时间 |
| call_end_time | 通话结束时间 |
| call_duration | 通话时长(秒) |
| call_direction | 通话方向(1呼入,2呼出) |
| recording_file_name | 录音文件名 |
| recording_file_path | OSS完整访问URL |
| recording_file_size | 文件大小(字节) |
## 日志说明
日志文件位于 `logs/app.log`,可通过以下命令查看:
```bash
# 查看实时日志
tail -f logs/app.log
# 只看错误日志
tail -f logs/app.log | grep "【异常】"
# 只看设备同步日志
tail -f logs/app.log | grep "【设备】"
# 只看录音处理日志
tail -f logs/app.log | grep "【录音】"
```
## 常见问题
### 1. 配置文件读取失败
检查 `config/application-external.yml` 是否在 JAR 包同级目录的 `config/` 文件夹下。
### 2. 数据库连接失败
检查数据库URL、用户名、密码是否正确,网络是否连通。
### 3. OSS上传失败
检查 OSS 地址、认证信息是否正确,OSS服务是否正常。
### 4. 录音盒连接失败
检查录音盒IP、端口、账号密码是否正确,网络是否连通。
## 技术支持
如有问题,请联系开发人员。

244
VAA_SYNC_README.md

@ -0,0 +1,244 @@
# VAA录音盒HTTP同步服务
## 📋 概述
基于合肥实现方案,通过HTTP API从VAA录音盒设备定时拉取录音数据并上传到OSS。
## 🔧 核心特性
1. **HTTP API通信** - 使用录音盒提供的RESTful API
2. **定时同步** - 每2小时自动执行一次同步任务
3. **增量同步** - 根据上次同步时间,只拉取新增录音
4. **自动登录** - 每次同步前自动认证
5. **通道状态更新** - 同步时更新设备通道状态
6. **OSS上传** - 录音文件自动上传到对象存储
7. **完整日志** - 记录每次同步的结果
## 📊 数据流程
```
┌──────────────┐
│ 定时触发 │ (每2小时)
└──────┬───────┘
┌──────────────┐
│ 查询设备列表 │ (YYDC_YYSB表)
└──────┬───────┘
┌──────────────┐
│ HTTP登录认证 │ (/authorize)
└──────┬───────┘
┌──────────────┐
│ 获取通道状态 │ (/service/running/channel)
└──────┬───────┘
┌──────────────┐
│ 获取录音列表 │ (/service/record/~/time[开始,结束])
└──────┬───────┘
┌──────────────┐
│ 下载录音文件 │ (HTTP下载)
└──────┬───────┘
┌──────────────┐
│ 上传到OSS │ (FileUploadUtil)
└──────┬───────┘
┌──────────────┐
│ 保存到数据库 │ (YYDC_YYTH表)
└──────┬───────┘
┌──────────────┐
│ 记录同步日志 │ (YYDC_TBRZ表)
└──────────────┘
```
## 🗄️ 数据库表要求
### YYDC_YYSB (语音设备表)
必需字段:
- `ID` - 设备ID
- `UUID` - 设备UUID
- `ORGAN_NAME` - 机构名称
- `ORGAN_ID` - 机构ID
- `IP` - 设备IP地址
- `PORT` - 设备端口(默认80)
- `SBYH` - 设备用户名
- `SBMM` - 设备密码
- `SFYX_ST` - 是否有效('1':有效)
### YYDC_SBTD (设备通道表)
必需字段:
- `ID` - 通道ID
- `UUID` - 关联设备UUID
- `TDHM` - 通道号码
- `PHONE` - 电话号码
- `TDZT` - 通道状态
- `SFYX_ST` - 是否有效
### YYDC_YYTH (通话记录表)
必需字段:
- `ID` - 通话ID(格式:设备ID_录音ID_开始时间戳)
- `ORGAN_NAME`, `ORGAN_ID` - 机构信息
- `YYSB_ID` - 语音设备ID
- `SBTD_ID` - 设备通道ID
- `THFX` - 通话方向('1':呼出, '0':呼入)
- `PHONE`, `ZJHM`, `BJHM` - 号码信息
- `THSC` - 通话时长(秒)
- `KSSJ`, `JSSJ` - 开始/结束时间
- `LYDZ` - 录音地址(OSS URL)
- `LYMC` - 录音文件名
### YYDC_TBRZ (同步日志表)
必需字段:
- `CODE` - 同步代码(格式:YYDC.AUDIO(设备ID))
- `KSSJ` - 开始时间
- `JSSJ` - 结束时间
- `GXSJ` - 更新时间
- `TBBZ` - 同步标志('1':成功, '0':失败)
- `TBSM` - 同步说明
## ⚙️ 配置说明
### application.yml
```yaml
# 本地IP(用于生成录音访问URL)
server:
local-ip: 127.0.0.1
# VAA同步配置
vaa-sync:
download-path: /tmp/vaa-recordings # 临时下载目录
sync-interval-cron: "0 0 0/2 * * ?" # 每2小时同步
```
## 🚀 使用方法
### 1. 配置设备信息
在数据库中插入设备信息:
```sql
INSERT INTO YYDC_YYSB (ID, UUID, ORGAN_NAME, ORGAN_ID, IP, PORT, SBYH, SBMM, SFYX_ST)
VALUES ('device-001', 'uuid-001', '黄山市局', 1001, '192.168.1.100', 80, 'admin', 'admin123', '1');
```
### 2. 配置通道信息
```sql
INSERT INTO YYDC_SBTD (ID, UUID, TDHM, PHONE, SFYX_ST)
VALUES ('channel-001', 'uuid-001', '1', '13800138000', '1');
```
### 3. 启动应用
```bash
mvn spring-boot:run
```
应用启动后,定时任务会自动执行。
### 4. 手动触发(可选)
可以通过注入 `VaaSyncService` 并调用 `executeSync()` 方法手动触发同步。
## 📝 VAA录音盒API说明
### 1. 登录认证
```
GET http://{IP}/authorize?username={用户}&password={密码}
```
### 2. 获取通道状态
```
GET http://{IP}/service/running/channel
```
返回示例:
```json
[
{"ch": "1", "state": "1"},
{"ch": "2", "state": "0"}
]
```
### 3. 获取录音列表
```
GET http://{IP}/service/record/~/time[开始时间戳,结束时间戳]
```
返回示例:
```json
[
{
"id": "12345",
"channel": "1",
"phone": "13800138000",
"file": "/recordings/20260520/xxx.wav",
"begtime": 1716192000,
"endtime": 1716192120,
"state": "1"
}
]
```
### 4. 下载录音文件
```
GET http://{IP}{文件路径}
```
## 🔍 日志说明
同步过程会输出详细日志:
```
========== 定时任务触发: VAA录音盒同步 ==========
开始同步设备: ID=device-001, 机构=黄山市局, IP=192.168.1.100:80
正在登录设备...
登录成功
获取录音列表: http://192.168.1.100/service/record/~/time[1716105600,1716192000]
获取到 5 条录音记录
开始下载录音: /recordings/20260520/xxx.wav
录音下载完成: /tmp/vaa-recordings/20260520/uuid-001/xxx.wav
录音上传OSS: https://oss.example.com/voice/黄山市局/20260520/xxx.wav
插入通话记录: device-001_12345_1716192000
设备同步完成: ID=device-001, 成功5条
========== VAA录音盒同步任务完成 ==========
```
## ⚠️ 注意事项
1. **网络连接** - 确保服务器能访问录音盒设备的IP和端口
2. **存储空间** - 确保 `/tmp/vaa-recordings` 有足够的空间
3. **时间同步** - 确保服务器时间与录音盒时间一致
4. **并发控制** - 每个设备独立执行,不会相互阻塞
5. **错误处理** - 单个设备失败不影响其他设备
## 🔄 与FTP方式的对比
| 特性 | HTTP API方式 | FTP方式 |
|------|-------------|---------|
| 连接方式 | HTTP RESTful API | FTP协议 |
| 认证方式 | URL参数认证 | 用户名密码 |
| 数据获取 | JSON API | 解析.txt索引文件 |
| 文件下载 | HTTP GET | FTP RETR |
| 实时性 | 更好(API直接查询) | 依赖文件生成 |
| 复杂度 | 较低 | 较高 |
## 📞 技术支持
如有问题,请检查:
1. 设备IP、端口、用户名、密码是否正确
2. 网络是否能访问设备
3. 数据库表结构是否正确
4. 日志中的错误信息

76
config/application-external.yml

@ -0,0 +1,76 @@
# ============================================
# 语音数据同步服务 - 外部配置文件
# ============================================
# 【说明】
# 1. 此文件用于部署时覆盖默认配置,无需重新打包 JAR
# 2. 将此文件放在 JAR 包同级目录的 config/ 文件夹下
# 3. 修改此文件后重启服务即可生效
# ============================================
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
# 同步定时任务 Cron 表达式
# 默认每2小时执行一次: 0 0 0/2 * * ?
# 每30分钟执行: 0 0/30 * * * ?
# 每天凌晨2点执行: 0 0 2 * * ?
# 每分钟执行(测试用): 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

BIN
logs/.DS_Store

Binary file not shown.

76
logs/app.log

@ -0,0 +1,76 @@
2026-05-26 16:26:25.778 [main] INFO c.t.d.DataserviceYyApplication - Starting DataserviceYyApplication using Java 1.8.0_492 on MswangdeMacBook-Air.local with PID 89751 (/Users/wang/sanduoyun/developspace/dataservice-yy/target/classes started by wang in /Users/wang/sanduoyun/developspace/dataservice-yy)
2026-05-26 16:26:25.782 [main] DEBUG c.t.d.DataserviceYyApplication - Running with Spring Boot v2.6.13, Spring v5.3.23
2026-05-26 16:26:25.783 [main] INFO c.t.d.DataserviceYyApplication - No active profile set, falling back to 1 default profile: "default"
2026-05-26 16:26:26.305 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8088 (http)
2026-05-26 16:26:26.310 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2026-05-26 16:26:26.310 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.68]
2026-05-26 16:26:26.357 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2026-05-26 16:26:26.357 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 546 ms
2026-05-26 16:26:26.601 [main] INFO c.t.d.util.FileUploadUtil - 文件上传工具初始化完成, 上传接口: http://127.0.0.1/apiOss/oss/fileUpload
2026-05-26 16:26:26.877 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8088 (http) with context path ''
2026-05-26 16:26:26.886 [main] INFO c.t.d.DataserviceYyApplication - Started DataserviceYyApplication in 1.294 seconds (JVM running for 2.135)
2026-05-26 16:26:26.888 [main] INFO c.t.d.service.VaaSyncService - 开始执行VAA录音盒同步任务
2026-05-26 16:26:26.906 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2026-05-26 16:26:26.913 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2026-05-26 16:26:50.548 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2026-05-26 16:26:55.382 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2026-05-26 16:32:47.381 [main] INFO c.t.d.DataserviceYyApplication - Starting DataserviceYyApplication using Java 1.8.0_492 on MswangdeMacBook-Air.local with PID 90117 (/Users/wang/sanduoyun/developspace/dataservice-yy/target/classes started by wang in /Users/wang/sanduoyun/developspace/dataservice-yy)
2026-05-26 16:32:47.382 [main] DEBUG c.t.d.DataserviceYyApplication - Running with Spring Boot v2.6.13, Spring v5.3.23
2026-05-26 16:32:47.383 [main] INFO c.t.d.DataserviceYyApplication - No active profile set, falling back to 1 default profile: "default"
2026-05-26 16:32:47.870 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8088 (http)
2026-05-26 16:32:47.876 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2026-05-26 16:32:47.876 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.68]
2026-05-26 16:32:47.929 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2026-05-26 16:32:47.929 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 519 ms
2026-05-26 16:32:48.166 [main] INFO c.t.d.util.FileUploadUtil - 文件上传工具初始化完成, 上传接口: http://127.0.0.1/apiOss/oss/fileUpload
2026-05-26 16:32:48.431 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8088 (http) with context path ''
2026-05-26 16:32:48.440 [main] INFO c.t.d.DataserviceYyApplication - Started DataserviceYyApplication in 1.277 seconds (JVM running for 1.712)
2026-05-26 16:32:48.441 [main] INFO c.t.d.service.VaaSyncService - 开始执行VAA录音盒同步任务
2026-05-26 16:32:48.459 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2026-05-26 16:32:48.465 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2026-05-26 16:32:56.383 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2026-05-26 16:32:59.216 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2026-05-26 16:35:12.104 [main] INFO c.t.d.DataserviceYyApplication - Starting DataserviceYyApplication using Java 1.8.0_492 on MswangdeMacBook-Air.local with PID 90436 (/Users/wang/sanduoyun/developspace/dataservice-yy/target/classes started by wang in /Users/wang/sanduoyun/developspace/dataservice-yy)
2026-05-26 16:35:12.108 [main] DEBUG c.t.d.DataserviceYyApplication - Running with Spring Boot v2.6.13, Spring v5.3.23
2026-05-26 16:35:12.108 [main] INFO c.t.d.DataserviceYyApplication - No active profile set, falling back to 1 default profile: "default"
2026-05-26 16:35:12.935 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8088 (http)
2026-05-26 16:35:12.945 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2026-05-26 16:35:12.946 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.68]
2026-05-26 16:35:13.022 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2026-05-26 16:35:13.023 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 849 ms
2026-05-26 16:35:13.428 [main] INFO c.t.d.util.FileUploadUtil - 文件上传工具初始化完成, 上传接口: http://127.0.0.1/apiOss/oss/fileUpload
2026-05-26 16:35:13.831 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8088 (http) with context path ''
2026-05-26 16:35:13.842 [main] INFO c.t.d.DataserviceYyApplication - Started DataserviceYyApplication in 2.227 seconds (JVM running for 3.504)
2026-05-26 16:35:13.843 [main] INFO c.t.d.service.VaaSyncService - 开始执行VAA录音盒同步任务
2026-05-26 16:35:13.864 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2026-05-26 16:35:13.873 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2026-05-26 16:35:14.560 [main] INFO c.t.d.service.VaaSyncService - 查询到 0 个语音设备
2026-05-26 16:35:14.560 [main] INFO c.t.d.service.VaaSyncService - ========== VAA录音盒同步任务完成 ==========
2026-05-26 16:40:10.590 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2026-05-26 16:40:10.591 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2026-05-26 16:40:15.223 [main] INFO c.t.d.DataserviceYyApplication - Starting DataserviceYyApplication using Java 1.8.0_492 on MswangdeMacBook-Air.local with PID 90706 (/Users/wang/sanduoyun/developspace/dataservice-yy/target/classes started by wang in /Users/wang/sanduoyun/developspace/dataservice-yy)
2026-05-26 16:40:15.225 [main] DEBUG c.t.d.DataserviceYyApplication - Running with Spring Boot v2.6.13, Spring v5.3.23
2026-05-26 16:40:15.226 [main] INFO c.t.d.DataserviceYyApplication - No active profile set, falling back to 1 default profile: "default"
2026-05-26 16:40:15.760 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8088 (http)
2026-05-26 16:40:15.766 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2026-05-26 16:40:15.766 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.68]
2026-05-26 16:40:15.818 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2026-05-26 16:40:15.818 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 563 ms
2026-05-26 16:40:16.062 [main] INFO c.t.d.util.FileUploadUtil - 文件上传工具初始化完成, 上传接口: http://127.0.0.1/apiOss/oss/fileUpload
2026-05-26 16:40:16.347 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8088 (http) with context path ''
2026-05-26 16:40:16.356 [main] INFO c.t.d.DataserviceYyApplication - Started DataserviceYyApplication in 1.395 seconds (JVM running for 1.897)
2026-05-26 16:40:16.358 [main] INFO c.t.d.service.VaaSyncService - 开始执行VAA录音盒同步任务
2026-05-26 16:40:16.375 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2026-05-26 16:40:16.381 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2026-05-26 16:40:17.269 [main] INFO c.t.d.service.VaaSyncService - 查询到 216 个语音设备
2026-05-26 16:40:17.269 [main] INFO c.t.d.service.VaaSyncService - ────────────────────────────────────────
2026-05-26 16:40:17.269 [main] INFO c.t.d.service.VaaSyncService - 开始同步设备: ID=15, 机构=阜阳市, IP=53.162.212.190:80
2026-05-26 16:40:17.271 [main] INFO c.t.d.service.VaaSyncService - 正在登录设备...
2026-05-26 16:40:17.271 [main] DEBUG c.t.dataserviceyy.util.VaaHttpUtil - 正在登录录音盒: http://53.162.212.190/authorize?username=admin&password=admin
2026-05-26 16:40:32.779 [main] ERROR c.t.d.service.VaaSyncService - 设备登录失败: IP=53.162.212.190, url=http://53.162.212.190/authorize?username=admin&password=admin, 原因=登录失败,HTTP状态码: 502
2026-05-26 16:40:32.780 [main] INFO c.t.d.service.VaaSyncService - ────────────────────────────────────────
2026-05-26 16:40:32.780 [main] INFO c.t.d.service.VaaSyncService - 开始同步设备: ID=16, 机构=阜阳市, IP=10.127.89.93:80
2026-05-26 16:40:32.780 [main] INFO c.t.d.service.VaaSyncService - 正在登录设备...
2026-05-26 16:40:32.780 [main] DEBUG c.t.dataserviceyy.util.VaaHttpUtil - 正在登录录音盒: http://10.127.89.93/authorize?username=admin&password=admin
2026-05-26 16:40:33.777 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2026-05-26 16:40:33.778 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.

BIN
logs/app.log.2026-05-21.0.gz

Binary file not shown.

BIN
logs/app.log.2026-05-22.0.gz

Binary file not shown.

BIN
logs/app.log.2026-05-23.0.gz

Binary file not shown.

BIN
logs/app.log.2026-05-25.0.gz

Binary file not shown.

BIN
logs/server.jar

Binary file not shown.

192
pom.xml

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.threecloud</groupId>
<artifactId>dataservice-yy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dataservice-yy</name>
<description>数据同步服务</description>
<properties>
<java.version>8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- 核心框架版本 -->
<spring-boot.version>2.6.13</spring-boot.version>
<!-- 数据库相关版本 -->
<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>
<ojdbc.version>21.9.0.0</ojdbc.version>
<!-- 工具库版本 -->
<lombok.version>1.18.24</lombok.version>
<guava.version>31.1-jre</guava.version>
<hutool.version>5.8.18</hutool.version>
<fastjson2.version>2.0.25</fastjson2.version>
<commons-codec.version>1.15</commons-codec.version>
<commons-io.version>2.11.0</commons-io.version>
<commons-pool2.version>2.11.1</commons-pool2.version>
<commons-net.version>3.9.0</commons-net.version>
<!-- 测试 -->
<junit.version>5.8.2</junit.version>
</properties>
<!-- 依赖版本管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot 核心启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<version>${kingbase.version}</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>${ojdbc.version}</version>
</dependency>
<!-- 数据库访问 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 常用工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
<excludes>
<exclude>**/老代码/**</exclude>
</excludes>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.threecloud.dataserviceyy.DataserviceYyApplication</mainClass>
<skip>false</skip>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

BIN
src/.DS_Store

Binary file not shown.

BIN
src/main/.DS_Store

Binary file not shown.

BIN
src/main/java/.DS_Store

Binary file not shown.

44
src/main/java/com/threecloud/dataserviceyy/DataserviceYyApplication.java

@ -0,0 +1,44 @@
package com.threecloud.dataserviceyy;
import com.threecloud.dataserviceyy.service.VaaSyncService;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(exclude = {
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class,
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration.class
})
@MapperScan("com.threecloud.dataserviceyy.mapper")
@EnableScheduling
public class DataserviceYyApplication {
@Autowired
private VaaSyncService vaaSyncService;
public static void main(String[] args) {
SpringApplication.run(DataserviceYyApplication.class, args);
}
@Bean
public CommandLineRunner startupRunner() {
return args -> {
System.out.println("========================================");
System.out.println("【项目启动】开始执行首次语音数据同步");
System.out.println("========================================");
try {
vaaSyncService.executeSync();
} catch (Exception e) {
System.err.println("【项目启动】首次同步失败: " + e.getMessage());
e.printStackTrace();
}
System.out.println("========================================");
System.out.println("【项目启动】首次同步完成,后续将在每2小时自动执行");
System.out.println("========================================");
};
}
}

63
src/main/java/com/threecloud/dataserviceyy/config/DataSourceConfig.java

@ -0,0 +1,63 @@
package com.threecloud.dataserviceyy.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Bean
public DataSource dataSource() {
System.out.println("========================================");
System.out.println("【DataSourceConfig】创建数据源");
System.out.println("Driver: " + driverClassName);
System.out.println("URL: " + url);
System.out.println("Username: " + username);
System.out.println("Password length: " + (password != null ? password.length() : 0));
System.out.println("========================================");
HikariDataSource datasource = new HikariDataSource();
datasource.setJdbcUrl(url);
datasource.setDriverClassName(driverClassName);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setMinimumIdle(0);
datasource.setMaximumPoolSize(10);
datasource.setConnectionTimeout(30000);
datasource.setValidationTimeout(5000);
// 根据数据库类型设置不同的验证SQL
if (url.contains("kingbase")) {
datasource.setConnectionTestQuery("SELECT 1");
} else {
datasource.setConnectionTestQuery("SELECT 1 FROM DUAL");
datasource.addDataSourceProperty("oracle.jdbc.timezoneAsRegion", "false");
datasource.addDataSourceProperty("oracle.net.CONNECT_TIMEOUT", "10000");
datasource.addDataSourceProperty("oracle.net.READ_TIMEOUT", "60000");
}
// 关键:初始化时不测试连接,即使数据库暂时连不上也能启动
datasource.setInitializationFailTimeout(-1);
System.out.println("【DataSourceConfig】数据源创建完成(允许延迟连接)");
return datasource;
}
}

12
src/main/java/com/threecloud/dataserviceyy/config/DynamicDataSourceConfig.java

@ -0,0 +1,12 @@
package com.threecloud.dataserviceyy.config;
/**
* 多数据源配置 - 方案A阶段: 已完全禁用
*
* 此类在方案B(多数据源模式)时会重新启用
* 当前阶段使用Spring Boot默认的DataSource自动配置仿照老系统 yydc-tb-server
*/
// @Configuration // 【方案A: 已禁用】
public class DynamicDataSourceConfig {
// 所有方法已禁用,不再干扰Spring Boot的自动配置
}

16
src/main/java/com/threecloud/dataserviceyy/config/FileUploadConfig.java

@ -0,0 +1,16 @@
package com.threecloud.dataserviceyy.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Data
@Component
@ConfigurationProperties(prefix = "file-upload")
public class FileUploadConfig {
private String uploadUrl;
private Map<String, String> extraParams = new HashMap<>();
}

13
src/main/java/com/threecloud/dataserviceyy/config/SyncTargetConfig.java

@ -0,0 +1,13 @@
package com.threecloud.dataserviceyy.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "server.target")
public class SyncTargetConfig {
private String syncLogCode;
private String syncLogTable;
}

270
src/main/java/com/threecloud/dataserviceyy/controller/VoiceBoxController.java

@ -0,0 +1,270 @@
package com.threecloud.dataserviceyy.controller;
import com.threecloud.dataserviceyy.util.FileUploadUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class VoiceBoxController {
private static final Logger logger = LoggerFactory.getLogger(VoiceBoxController.class);
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
@Autowired
private FileUploadUtil fileUploadUtil;
/**
* 事件上传接口 - GET方式
* 语音盒来电/去电时主动推送事件信息
*
* 参数说明来自VAA录音仪开发文档
* - event_type: 事件类型 (3=状态提机, 4=提机, 6=开始去电录音, 7=来电接听开始录音,
* 8=去电完成挂机, 9=新的去电录音产生, 10=新的来电录音产生, 11=未接来电, 14=发送来电号码)
* - line: 设备端口
* - device_id: 设备编号
* - duration: 录音时长()
* - TimeLong: 通话时长()
* - date: 来电日期(格式: 2022-04-21 15:48:40)
* - caller: 来电号码(9,10,14事件中有值)
* - FilePath: 录音文件名称(9,10事件中有值)
* - voltage: 电压
* - RingCnt: 振铃次数
* - TotalStore: 设备存储容量
* - TotalFreeStore: 设备剩余存储容量
* - TotalMem: 内存使用率
* - CPU: CPU使用率
* - calloutId: websocket回传的calloutId
*
* 返回值成功返回 "0000"否则录音仪客户端日志会显示失败
*/
@GetMapping("/event")
public String handleEvent(
@RequestParam(required = false) String event_type,
@RequestParam(required = false) String line,
@RequestParam(required = false) String device_id,
@RequestParam(required = false) String duration,
@RequestParam(required = false) String TimeLong,
@RequestParam(required = false) String date,
@RequestParam(required = false) String caller,
@RequestParam(required = false) String FilePath,
@RequestParam(required = false) String voltage,
@RequestParam(required = false) String RingCnt,
@RequestParam(required = false) String TotalStore,
@RequestParam(required = false) String TotalFreeStore,
@RequestParam(required = false) String TotalMem,
@RequestParam(required = false) String CPU,
@RequestParam(required = false) String calloutId) {
try {
logger.info("========================================");
logger.info("[事件上传] 收到语音盒事件推送");
logger.info("[事件类型] event_type={} ({})", event_type, getEventTypeDesc(event_type));
logger.info("[设备信息] device_id={}, line={}", device_id, line);
if (date != null) {
logger.info("[通话时间] date={}", date);
}
if (caller != null && !caller.isEmpty()) {
logger.info("[来电号码] caller={}", caller);
}
if (FilePath != null && !FilePath.isEmpty()) {
logger.info("[文件路径] FilePath={}", FilePath);
}
if (duration != null) {
logger.info("[录音时长] duration={}秒", duration);
}
if (TimeLong != null) {
logger.info("[通话时长] TimeLong={}秒", TimeLong);
}
Map<String, String> eventData = new HashMap<>();
eventData.put("event_type", event_type);
eventData.put("line", line);
eventData.put("device_id", device_id);
eventData.put("date", date);
eventData.put("caller", caller);
eventData.put("FilePath", FilePath);
saveEventToDatabase(eventData);
logger.info("[事件上传] 处理完成");
logger.info("========================================");
return "0000";
} catch (Exception e) {
logger.error("[事件上传] 处理异常: {}", e.getMessage(), e);
return "0000";
}
}
/**
* 文件上传接口 - POST方式
* 语音盒主动上传录音文件先保存到本地临时目录再由定时任务上传到OSS
*
* 参数说明来自VAA录音仪开发文档
* - files: 文件上传信息(文件名称与事件中的FilePath字段对应)
* 文件名称格式: 设备名称-年月日时分秒-(O|I)-L(端口号)-EN-电话号码.wav
* O=去电, I=来电
* - event_type: 事件类型(9=去电录音, 10=来电录音)
* - device_id: 设备编号
* - line: 端口编号
* - date: 来电日期
* - caller: 来电号码
* - FilePath: 录音文件名称
* - duration: 录音时长()
* - TimeLong: 通话时长()
*
* 返回值成功返回 "0000"否则录音仪客户端日志会显示失败
*/
@PostMapping("/file")
public String handleFileUpload(
@RequestParam("files") MultipartFile file,
@RequestParam(required = false) String event_type,
@RequestParam(required = false) String device_id,
@RequestParam(required = false) String line,
@RequestParam(required = false) String date,
@RequestParam(required = false) String caller,
@RequestParam(required = false) String FilePath,
@RequestParam(required = false) String duration,
@RequestParam(required = false) String TimeLong) {
try {
logger.info("========================================");
logger.info("[文件上传] 收到语音盒文件上传请求");
logger.info("[事件类型] event_type={} ({})", event_type, getEventTypeDesc(event_type));
logger.info("[设备信息] device_id={}, line={}", device_id, line);
if (file != null && !file.isEmpty()) {
String originalFilename = file.getOriginalFilename();
long fileSize = file.getSize();
logger.info("[文件信息] fileName={}, size={} bytes", originalFilename, fileSize);
}
if (FilePath != null && !FilePath.isEmpty()) {
logger.info("[文件路径] FilePath={}", FilePath);
}
if (caller != null && !caller.isEmpty()) {
logger.info("[来电号码] caller={}", caller);
}
if (file != null && !file.isEmpty()) {
String fileName = file.getOriginalFilename();
String tempDir = "/tmp/voice_upload/";
File tempDirFile = new File(tempDir);
if (!tempDirFile.exists()) {
tempDirFile.mkdirs();
}
String tempFilePath = tempDir + fileName;
file.transferTo(new File(tempFilePath));
logger.info("[保存本地] 文件已保存到临时目录: {}", tempFilePath);
saveRecordToDatabase(device_id, line, date, caller, fileName, duration, TimeLong, tempFilePath, "0");
}
logger.info("[文件上传] 处理完成");
logger.info("========================================");
return "0000";
} catch (IOException e) {
logger.error("[文件上传] 文件保存异常: {}", e.getMessage(), e);
return "0000";
} catch (Exception e) {
logger.error("[文件上传] 处理异常: {}", e.getMessage(), e);
return "0000";
}
}
/**
* 获取事件类型描述
*/
private String getEventTypeDesc(String eventType) {
if (eventType == null) return "未知";
switch (eventType) {
case "3": return "状态提机(ON HOOK)";
case "4": return "提机(OFF HOOK)";
case "6": return "开始去电录音";
case "7": return "来电接听开始录音";
case "8": return "去电完成挂机(ON HOOK)";
case "9": return "新的去电录音产生";
case "10": return "新的来电录音产生";
case "11": return "未接来电";
case "14": return "发送来电号码";
default: return "其他(" + eventType + ")";
}
}
/**
* 从文件名提取设备名称
* 文件格式: 设备名称-年月日时分秒-(O|I)-L(端口号)-EN-电话号码.wav
*/
private String extractDeviceName(String fileName) {
if (fileName == null || !fileName.contains("-")) {
return "unknown";
}
return fileName.substring(0, fileName.indexOf("-"));
}
/**
* 从文件名提取日期
* 文件格式: 设备名称-年月日时分秒-(O|I)-L(端口号)-EN-电话号码.wav
*/
private String extractDateFromFileName(String fileName) {
if (fileName == null) {
return LocalDateTime.now().format(DATE_FORMAT);
}
String datePattern = "\\d{8}";
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(datePattern);
java.util.regex.Matcher matcher = pattern.matcher(fileName);
if (matcher.find()) {
return matcher.group();
}
return LocalDateTime.now().format(DATE_FORMAT);
}
/**
* 从文件名提取通话类型
* O=去电, I=来电
*/
private String extractCallType(String fileName) {
if (fileName == null) return "0";
if (fileName.contains("-O-")) return "1";
if (fileName.contains("-I-")) return "0";
return "0";
}
/**
* 保存事件到数据库示例方法
*/
private void saveEventToDatabase(Map<String, String> eventData) {
logger.debug("[事件存储] device_id={}, event_type={}",
eventData.get("device_id"), eventData.get("event_type"));
}
/**
* 保存通话记录到数据库示例方法
*/
private void saveRecordToDatabase(String deviceId, String line, String date, String caller,
String fileName, String duration, String timeLong,
String ossUrl, String callType) {
logger.debug("[记录存储] device_id={}, fileName={}, ossUrl={}", deviceId, fileName, ossUrl);
}
}

157
src/main/java/com/threecloud/dataserviceyy/entity/CallRecord.java

@ -0,0 +1,157 @@
package com.threecloud.dataserviceyy.entity;
/**
* 通话记录实体类
*
* 封装从FTP数据文件中解析出的单条通话记录信息
* 数据来源FTP服务器上的.txt文件字段间用"※"分隔
*
* 字段说明
* - kssj: 开始时间 (格式: yyyy-MM-dd HH:mm:ss)
* - jssj: 结束时间 (格式: yyyy-MM-dd HH:mm:ss)
* - hjls: 呼机流水号
* - hjzls: 呼机总流水号
* - zjhm: 主叫号码
* - bjhm: 被叫号码
* - thsc: 通话时长()
* - thfx: 通话方向(1=主叫方向, 0=被叫方向)
* - dateDir: 录音文件所在日期目录(yyyyMMdd)
* - wavFileName: 对应的WAV录音文件名(格式: 呼机流水号_呼机总流水号.wav)
*/
public class CallRecord {
/** 开始时间 */
private String kssj;
/** 结束时间 */
private String jssj;
/** 呼机流水号 */
private String hjls;
/** 呼机总流水号 */
private String hjzls;
/** 主叫号码(可能包含区号前缀如0553) */
private String zjhm;
/** 被叫号码(可能包含区号前缀如0553) */
private String bjhm;
/** 通话时长(秒) */
private Long thsc;
/** 通话方向(1=主叫, 0=被叫) */
private String thfx;
/** 日期目录(yyyyMMdd格式,从kssj中提取) */
private String dateDir;
/** WAV录音文件名(格式: 呼机流水号_呼机总流水号.wav) */
private String wavFileName;
public CallRecord() {
}
/**
* 获取通话记录摘要信息
* 用于日志输出显示主被叫号码
* @return 格式化的摘要字符串主叫=13800138000, 被叫=13900139000
*/
public String getSummary() {
return "主叫=" + zjhm + ", 被叫=" + bjhm;
}
public String getKssj() {
return kssj;
}
public void setKssj(String kssj) {
this.kssj = kssj;
}
public String getJssj() {
return jssj;
}
public void setJssj(String jssj) {
this.jssj = jssj;
}
public String getHjls() {
return hjls;
}
public void setHjls(String hjls) {
this.hjls = hjls;
}
public String getHjzls() {
return hjzls;
}
public void setHjzls(String hjzls) {
this.hjzls = hjzls;
}
public String getZjhm() {
return zjhm;
}
public void setZjhm(String zjhm) {
this.zjhm = zjhm;
}
public String getBjhm() {
return bjhm;
}
public void setBjhm(String bjhm) {
this.bjhm = bjhm;
}
public Long getThsc() {
return thsc;
}
public void setThsc(Long thsc) {
this.thsc = thsc;
}
public String getThfx() {
return thfx;
}
public void setThfx(String thfx) {
this.thfx = thfx;
}
public String getDateDir() {
return dateDir;
}
public void setDateDir(String dateDir) {
this.dateDir = dateDir;
}
public String getWavFileName() {
return wavFileName;
}
public void setWavFileName(String wavFileName) {
this.wavFileName = wavFileName;
}
@Override
public String toString() {
return "CallRecord{" +
"kssj='" + kssj + '\'' +
", jssj='" + jssj + '\'' +
", zjhm='" + zjhm + '\'' +
", bjhm='" + bjhm + '\'' +
", thsc=" + thsc +
", thfx='" + thfx + '\'' +
", wavFileName='" + wavFileName + '\'' +
'}';
}
}

93
src/main/java/com/threecloud/dataserviceyy/entity/DeviceMatchResult.java

@ -0,0 +1,93 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Map;
/**
* 设备匹配结果实体类
*
* 封装通话记录与设备通道表匹配后的结果信息
* 匹配流程
* 1. 根据电话号码在设备通道表(YYDC_SBTD)中查找对应设备
* 2. 根据设备的UUID在语音盒表(YYDC_YYSB)中查找所属地市信息
* 3. 返回完整的匹配结果
*/
public class DeviceMatchResult {
/** 是否匹配成功 */
private boolean success;
/** 设备通道信息(ID, UUID, PHONE, THFX等字段) */
private Map<String, Object> deviceChannel;
/** 语音盒信息(ID, ORGAN_NAME, ORGAN_ID等字段) */
private Map<String, Object> voiceBox;
/** 匹配时使用的电话号码(可能是主叫或被叫) */
private String phone;
/** 失败时的错误描述信息 */
private String errorMessage;
public DeviceMatchResult() {
}
/**
* 创建匹配成功的结果对象
* @param deviceChannel 设备通道表中的记录信息
* @param voiceBox 语音盒表中的记录信息
* @param phone 匹配到的有效电话号码
* @return 成功状态的DeviceMatchResult实例
*/
public static DeviceMatchResult success(Map<String, Object> deviceChannel,
Map<String, Object> voiceBox,
String phone) {
DeviceMatchResult result = new DeviceMatchResult();
result.success = true;
result.deviceChannel = deviceChannel;
result.voiceBox = voiceBox;
result.phone = phone;
return result;
}
/**
* 创建匹配失败的结果对象
* @param errorMessage 失败原因描述
* @return 失败状态的DeviceMatchResult实例
*/
public static DeviceMatchResult fail(String errorMessage) {
DeviceMatchResult result = new DeviceMatchResult();
result.success = false;
result.errorMessage = errorMessage;
return result;
}
public boolean isSuccess() {
return success;
}
public Map<String, Object> getDeviceChannel() {
return deviceChannel;
}
public Map<String, Object> getVoiceBox() {
return voiceBox;
}
public String getPhone() {
return phone;
}
public String getErrorMessage() {
return errorMessage;
}
@Override
public String toString() {
if (success) {
return "DeviceMatchResult{success=true, phone=" + phone +
", organName=" + (voiceBox != null ? voiceBox.get("ORGAN_NAME") : "null") + "}";
} else {
return "DeviceMatchResult{success=false, error='" + errorMessage + "'}";
}
}
}

143
src/main/java/com/threecloud/dataserviceyy/entity/MidVoiceCallRecord.java

@ -0,0 +1,143 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* 语音通话记录表
* 对应 mid_voice.mid_voice_call_record
*/
public class MidVoiceCallRecord {
/** 自增ID主键 */
private Long id;
/** 地市编码 */
private String cityCode;
/** 地市名称 */
private String cityName;
/** 通话记录ID */
private String callRecordId;
/** 主叫号码 */
private String callTel;
/** 被叫号码 */
private String calledTel;
/** 通话开始时间 */
private Date callStartTime;
/** 通话结束时间 */
private Date callEndTime;
/** 通话时长(秒) */
private Integer callDuration;
/** 通话类型: 1呼入,2呼出 */
private String callDirection;
/** 设备编码 */
private String deviceNo;
/** 场景:110接报警、执法办案沟通、窗口服务咨询 */
private String businessScenario;
/** 文件名称 */
private String recordingFileName;
/** 文件地址(OSS路径) */
private String recordingFilePath;
/** 文件大小 */
private Integer recordingFileSize;
/** 通话状态:1正常接通,2未接通,3中途挂断,4通话失败 */
private String callStatus;
/** 失败原因 */
private String failReason;
/** 备注 */
private String remarks;
/** 数据同步时间 */
private Date syncTime;
/** 创建时间 */
private Date createTime;
/** 单位代码 */
private String orgCode;
/** 本地存储路径(非数据库字段,仅用于业务处理) */
private String localPath;
// 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 getCallRecordId() { return callRecordId; }
public void setCallRecordId(String callRecordId) { this.callRecordId = callRecordId; }
public String getCallTel() { return callTel; }
public void setCallTel(String callTel) { this.callTel = callTel; }
public String getCalledTel() { return calledTel; }
public void setCalledTel(String calledTel) { this.calledTel = calledTel; }
public Date getCallStartTime() { return callStartTime; }
public void setCallStartTime(Date callStartTime) { this.callStartTime = callStartTime; }
public Date getCallEndTime() { return callEndTime; }
public void setCallEndTime(Date callEndTime) { this.callEndTime = callEndTime; }
public Integer getCallDuration() { return callDuration; }
public void setCallDuration(Integer callDuration) { this.callDuration = callDuration; }
public String getCallDirection() { return callDirection; }
public void setCallDirection(String callDirection) { this.callDirection = callDirection; }
public String getDeviceNo() { return deviceNo; }
public void setDeviceNo(String deviceNo) { this.deviceNo = deviceNo; }
public String getBusinessScenario() { return businessScenario; }
public void setBusinessScenario(String businessScenario) { this.businessScenario = businessScenario; }
public String getRecordingFileName() { return recordingFileName; }
public void setRecordingFileName(String recordingFileName) { this.recordingFileName = recordingFileName; }
public String getRecordingFilePath() { return recordingFilePath; }
public void setRecordingFilePath(String recordingFilePath) { this.recordingFilePath = recordingFilePath; }
public Integer getRecordingFileSize() { return recordingFileSize; }
public void setRecordingFileSize(Integer recordingFileSize) { this.recordingFileSize = recordingFileSize; }
public String getCallStatus() { return callStatus; }
public void setCallStatus(String callStatus) { this.callStatus = callStatus; }
public String getFailReason() { return failReason; }
public void setFailReason(String failReason) { this.failReason = failReason; }
public String getRemarks() { return remarks; }
public void setRemarks(String remarks) { this.remarks = remarks; }
public Date getSyncTime() { return syncTime; }
public void setSyncTime(Date syncTime) { this.syncTime = syncTime; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public String getOrgCode() { return orgCode; }
public void setOrgCode(String orgCode) { this.orgCode = orgCode; }
public String getLocalPath() { return localPath; }
public void setLocalPath(String localPath) { this.localPath = localPath; }
}

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

@ -0,0 +1,78 @@
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; }
}

140
src/main/java/com/threecloud/dataserviceyy/entity/ResultEntity.java

@ -0,0 +1,140 @@
package com.threecloud.dataserviceyy.entity;
import java.io.Serializable;
/**
* 接口请求返回实体类
*
* @author Jonny
* @version 1.0.0
*/
public class ResultEntity implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
/**状态(1:成功,0:失败)*/
private Integer code;
/**返回信息码*/
private String msgno;
/**返回信息*/
private String msg;
/**总数*/
private Long totalcount;
/**当前记录数*/
private Integer datacount;
/**是否存在下一页*/
private Integer pagestate;
/**每页记录数*/
private Integer pagesize;
/**返回内容*/
private Object content;
public ResultEntity() {}
public ResultEntity(Integer code, Object content) {
super();
this.code = code;
this.content = content;
}
public ResultEntity(Integer code, String msg, Object content) {
super();
this.code = code;
this.msg = msg;
this.content = content;
}
public ResultEntity(Integer code, String msgno, String msg, Object content) {
super();
this.code = code;
this.msgno = msgno;
this.msg = msg;
this.content = content;
}
public ResultEntity(Integer code, String msg, Object content, Long totalcount, Integer pagesize) {
super();
this.code = code;
this.msg = msg;
this.content = content;
this.totalcount = totalcount;
this.pagesize = pagesize;
}
public ResultEntity(Integer code, String msgno, String msg, Long totalcount, Integer datacount, Integer pagestate,
Integer pagesize, Object content) {
super();
this.code = code;
this.msgno = msgno;
this.msg = msg;
this.totalcount = totalcount;
this.datacount = datacount;
this.pagestate = pagestate;
this.pagesize = pagesize;
this.content = content;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsgno() {
return msgno;
}
public void setMsgno(String msgno) {
this.msgno = msgno;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Long getTotalcount() {
return totalcount;
}
public void setTotalcount(Long totalcount) {
this.totalcount = totalcount;
}
public Integer getDatacount() {
return datacount;
}
public void setDatacount(Integer datacount) {
this.datacount = datacount;
}
public Integer getPagestate() {
return pagestate;
}
public void setPagestate(Integer pagestate) {
this.pagestate = pagestate;
}
public Integer getPagesize() {
return pagesize;
}
public void setPagesize(Integer pagesize) {
this.pagesize = pagesize;
}
public Object getContent() {
return content;
}
public void setContent(Object content) {
this.content = content;
}
public enum StatusCode{
/**失败*/
FAILURE(1),
/**成功*/
SUCCESS(0);
private Integer code;
private StatusCode(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
}

77
src/main/java/com/threecloud/dataserviceyy/entity/SyncLog.java

@ -0,0 +1,77 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* 同步日志表
* 记录每次同步任务的执行情况
*/
public class SyncLog {
/** 主键ID */
private Long id;
/** 设备ID */
private String deviceId;
/** 设备名称 */
private String deviceName;
/** 同步开始时间 */
private Date startTime;
/** 同步结束时间 */
private Date endTime;
/** 查询到的记录数 */
private Integer totalCount;
/** 成功处理数 */
private Integer successCount;
/** 失败数 */
private Integer failCount;
/** 同步状态:0失败,1成功,2部分成功 */
private Integer status;
/** 错误信息 */
private String errorMsg;
/** 创建时间 */
private Date createTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getDeviceName() { return deviceName; }
public void setDeviceName(String deviceName) { this.deviceName = deviceName; }
public Date getStartTime() { return startTime; }
public void setStartTime(Date startTime) { this.startTime = startTime; }
public Date getEndTime() { return endTime; }
public void setEndTime(Date endTime) { this.endTime = endTime; }
public Integer getTotalCount() { return totalCount; }
public void setTotalCount(Integer totalCount) { this.totalCount = totalCount; }
public Integer getSuccessCount() { return successCount; }
public void setSuccessCount(Integer successCount) { this.successCount = successCount; }
public Integer getFailCount() { return failCount; }
public void setFailCount(Integer failCount) { this.failCount = failCount; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getErrorMsg() { return errorMsg; }
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}

125
src/main/java/com/threecloud/dataserviceyy/entity/VoiceRecord.java

@ -0,0 +1,125 @@
package com.threecloud.dataserviceyy.entity;
import java.util.Date;
/**
* 录音记录表
* 保存每次从录音盒下载并上传到OSS的录音文件信息
*/
public class VoiceRecord {
/** 主键ID */
private Long id;
/** 设备ID */
private String deviceId;
/** 设备UUID */
private String deviceUuid;
/** 设备名称/机构名称 */
private String deviceName;
/** 录音盒原始记录ID */
private String recordId;
/** 录音文件名 */
private String fileName;
/** 录音文件在录音盒中的路径 */
private String filePath;
/** OSS访问地址 */
private String ossUrl;
/** 本地存储路径 */
private String localPath;
/** 通话开始时间 */
private Date callStartTime;
/** 通话结束时间 */
private Date callEndTime;
/** 通话时长(秒) */
private Integer duration;
/** 通道号 1-8 */
private Integer channel;
/** 对方电话号码 */
private String phone;
/** 通话方向:1呼出,2呼入 */
private Integer direction;
/** 上传状态:0失败,1成功 */
private Integer status;
/** 失败原因 */
private String failReason;
/** 创建时间 */
private Date createTime;
/** 更新时间 */
private Date updateTime;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getDeviceUuid() { return deviceUuid; }
public void setDeviceUuid(String deviceUuid) { this.deviceUuid = deviceUuid; }
public String getDeviceName() { return deviceName; }
public void setDeviceName(String deviceName) { this.deviceName = deviceName; }
public String getRecordId() { return recordId; }
public void setRecordId(String recordId) { this.recordId = recordId; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getOssUrl() { return ossUrl; }
public void setOssUrl(String ossUrl) { this.ossUrl = ossUrl; }
public String getLocalPath() { return localPath; }
public void setLocalPath(String localPath) { this.localPath = localPath; }
public Date getCallStartTime() { return callStartTime; }
public void setCallStartTime(Date callStartTime) { this.callStartTime = callStartTime; }
public Date getCallEndTime() { return callEndTime; }
public void setCallEndTime(Date callEndTime) { this.callEndTime = callEndTime; }
public Integer getDuration() { return duration; }
public void setDuration(Integer duration) { this.duration = duration; }
public Integer getChannel() { return channel; }
public void setChannel(Integer channel) { this.channel = channel; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public Integer getDirection() { return direction; }
public void setDirection(Integer direction) { this.direction = direction; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getFailReason() { return failReason; }
public void setFailReason(String failReason) { this.failReason = failReason; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public Date getUpdateTime() { return updateTime; }
public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; }
}

22
src/main/java/com/threecloud/dataserviceyy/enums/SyncStatus.java

@ -0,0 +1,22 @@
package com.threecloud.dataserviceyy.enums;
public enum SyncStatus {
ERR("0", "失败"),
SUC("1", "成功");
private final String code;
private final String name;
SyncStatus(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

69
src/main/java/com/threecloud/dataserviceyy/mapper/MidVoiceCallRecordMapper.java

@ -0,0 +1,69 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.MidVoiceCallRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 语音通话记录Mapper
*/
@Mapper
public interface MidVoiceCallRecordMapper {
/**
* 插入通话记录
*/
int insert(MidVoiceCallRecord record);
/**
* 根据ID查询
*/
MidVoiceCallRecord selectById(Long id);
/**
* 根据通话记录ID查询防止重复插入
*/
MidVoiceCallRecord selectByCallRecordId(@Param("callRecordId") String callRecordId);
/**
* 根据设备编码查询
*/
List<MidVoiceCallRecord> selectByDeviceNo(@Param("deviceNo") String deviceNo);
/**
* 分页查询
*/
List<MidVoiceCallRecord> selectList(@Param("cityCode") String cityCode,
@Param("deviceNo") String deviceNo,
@Param("callTel") String callTel,
@Param("calledTel") String calledTel,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计总数
*/
int count(@Param("cityCode") String cityCode,
@Param("deviceNo") String deviceNo,
@Param("callTel") String callTel,
@Param("calledTel") String calledTel,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 更新文件路径
*/
int updateFilePath(@Param("id") Long id,
@Param("recordingFilePath") String recordingFilePath);
/**
* 更新状态和失败原因
*/
int updateStatus(@Param("id") Long id,
@Param("callStatus") String callStatus,
@Param("failReason") String failReason);
}

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

@ -0,0 +1,66 @@
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);
}

55
src/main/java/com/threecloud/dataserviceyy/mapper/SyncLogMapper.java

@ -0,0 +1,55 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.SyncLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 同步日志Mapper
*/
@Mapper
public interface SyncLogMapper {
/**
* 插入同步日志
*/
int insert(SyncLog log);
/**
* 根据ID查询
*/
SyncLog selectById(Long id);
/**
* 查询设备的同步日志
*/
List<SyncLog> selectByDeviceId(@Param("deviceId") String deviceId);
/**
* 分页查询同步日志
*/
List<SyncLog> selectList(@Param("deviceId") String deviceId,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计总数
*/
int count(@Param("deviceId") String deviceId,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 更新同步结果
*/
int updateResult(@Param("id") Long id,
@Param("endTime") java.util.Date endTime,
@Param("successCount") Integer successCount,
@Param("failCount") Integer failCount,
@Param("status") Integer status,
@Param("errorMsg") String errorMsg);
}

67
src/main/java/com/threecloud/dataserviceyy/mapper/VoiceRecordMapper.java

@ -0,0 +1,67 @@
package com.threecloud.dataserviceyy.mapper;
import com.threecloud.dataserviceyy.entity.VoiceRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 录音记录Mapper
*/
@Mapper
public interface VoiceRecordMapper {
/**
* 插入录音记录
*/
int insert(VoiceRecord record);
/**
* 根据ID查询
*/
VoiceRecord selectById(Long id);
/**
* 根据设备ID和录音ID查询防止重复插入
*/
VoiceRecord selectByDeviceAndRecordId(@Param("deviceId") String deviceId,
@Param("recordId") String recordId);
/**
* 查询设备的所有录音记录
*/
List<VoiceRecord> selectByDeviceId(@Param("deviceId") String deviceId);
/**
* 分页查询录音记录
*/
List<VoiceRecord> selectList(@Param("deviceId") String deviceId,
@Param("deviceName") String deviceName,
@Param("phone") String phone,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/**
* 统计总数
*/
int count(@Param("deviceId") String deviceId,
@Param("deviceName") String deviceName,
@Param("phone") String phone,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 更新OSS地址如果上传后需要更新
*/
int updateOssUrl(@Param("id") Long id, @Param("ossUrl") String ossUrl);
/**
* 更新状态
*/
int updateStatus(@Param("id") Long id,
@Param("status") Integer status,
@Param("failReason") String failReason);
}

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

@ -0,0 +1,46 @@
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
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);
void saveSyncLog(Map<String, Object> params);
Object checkThjlExists(@Param("thid") String thid);
void insertThjl(Map<String, Object> params);
void updateThjl(Map<String, Object> params);
}

547
src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java

@ -0,0 +1,547 @@
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.mapper.MidVoiceCallRecordMapper;
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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;
/**
* 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/ # 按日期分目录
* <device_uuid>/ # 按设备分目录
* xxx.wav
* .sync-marker/ # 同步时间标记
* <device_id>.time
*/
@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;
@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() {
logger.info("【定时任务】========== VAA录音盒同步开始 ==========");
long startTime = System.currentTimeMillis();
executeSync();
long costTime = System.currentTimeMillis() - startTime;
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<Map<String, Object>> deviceList = voiceSyncMapper.getAllYysb();
logger.info("【主流程】查询到 {} 个语音设备", deviceList.size());
// 步骤3:逐个设备同步
int successCount = 0;
int failCount = 0;
for (int i = 0; i < deviceList.size(); i++) {
Map<String, Object> device = deviceList.get(i);
String deviceId = getStringValue(device, "ID");
logger.debug("【步骤3】处理第 {}/{} 个设备: ID={}", i + 1, deviceList.size(), deviceId);
try {
syncSingleDevice(device);
successCount++;
} catch (Exception e) {
failCount++;
logger.error("【异常】设备同步失败: ID={}, 原因={}", deviceId, e.getMessage());
}
}
logger.info("【主流程】同步完成,成功 {} 个设备,失败 {} 个设备", successCount, failCount);
} catch (Exception e) {
logger.error("【异常】同步任务执行失败: {}", e.getMessage(), e);
}
}
/**
* 同步单个设备
*
* @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 {
// 提取设备信息
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);
// 参数校验
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);
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);
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);
}
logger.info("【设备】同步完成: ID={}, 成功{}条, 失败{}条", deviceId, result.successCount, result.failCount);
}
// ==================== 私有辅助方法 ====================
/**
* 登录设备获取认证令牌
*
* @param deviceHost 设备访问地址 192.168.1.100:80
* @return 认证令牌Cookie失败返回null
*/
private String loginDevice(String deviceHost) {
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;
} catch (Exception e) {
logger.error("设备登录失败: {}, 原因={}", deviceHost, e.getMessage());
return null;
}
}
/**
* 计算同步时间范围
*
* @param deviceId 设备ID
* @return 时间范围Unix时间戳
*/
private TimeRange calculateSyncTimeRange(String deviceId) {
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);
}
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) {
try {
String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]",
deviceHost, timeRange.startTime, timeRange.endTime);
logger.debug("获取录音列表: {}", recordUrl);
String recordData = vaaHttpUtil.httpVisit(recordUrl, authToken);
return vaaHttpUtil.parseRecordData(recordData);
} catch (Exception e) {
logger.error("获取录音列表失败: {}", e.getMessage());
return null;
}
}
/**
* 批量处理录音记录
*/
private SyncResult processRecords(JSONArray records, String deviceNo, String cityName,
String cityCode, String orgCode, String deviceHost, String authToken) {
SyncResult result = new SyncResult();
for (int i = 0; i < records.size(); i++) {
JSONObject record = 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;
}
} else {
result.failCount++;
}
} catch (Exception e) {
logger.error("处理录音记录失败[{}]: {}", i, e.getMessage());
result.failCount++;
}
}
return result;
}
/**
* 处理单条录音记录
*
* 处理流程
* 1. 解析录音信息
* 2. 检查是否已存在防重复
* 3. 查询通道配置获取本机号码
* 4. 下载录音文件如不存在
* 5. 上传到OSS
* 6. 保存到数据库
*/
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 ? "呼出" : "呼入");
// 参数校验
if (filePath == null || filePath.isEmpty()) {
logger.debug("【录音】录音文件路径为空,跳过: recordId={}", recordId);
return false;
}
if (begTime == null || endTime == null) {
logger.warn("【录音-异常】录音时间信息缺失,跳过: recordId={}", recordId);
return false;
}
// ========== 步骤2:防重复检查 ==========
String callRecordId = deviceNo + "_" + recordId;
if (callRecordMapper.selectByCallRecordId(callRecordId) != null) {
logger.debug("【录音】通话记录已存在,跳过: {}", callRecordId);
return true;
}
// ========== 步骤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);
// ========== 步骤5:构建通话记录实体 ==========
MidVoiceCallRecord callRecord = buildCallRecord(
callRecordId, deviceNo, cityCode, cityName, orgCode,
fileName, localPath, 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));
} else {
logger.debug("【录音-步骤6】录音文件已存在本地,跳过下载: {} ({} 字节)",
fileName, Files.size(localFile));
}
callRecord.setRecordingFileSize((int) Files.size(localFile));
// ========== 步骤7:上传到OSS ==========
logger.debug("【录音-步骤7】开始上传OSS: cityName={}, ossPath={}", cityName, ossPath);
byte[] wavData = Files.readAllBytes(localFile);
String ossUrl = fileUploadUtil.uploadWav(cityName, ossPath, fileName, wavData);
logger.info("【录音-步骤7】录音上传OSS成功: {}", ossUrl);
callRecord.setRecordingFilePath(ossUrl);
// ========== 步骤8:保存到数据库 ==========
logger.debug("【录音-步骤8】保存通话记录到数据库...");
callRecordMapper.insert(callRecord);
logger.info("【录音-步骤8】通话记录保存成功: id={}, callRecordId={}", callRecord.getId(), callRecordId);
return true;
}
/**
* 构建通话记录实体
*/
private MidVoiceCallRecord buildCallRecord(String callRecordId, String deviceNo, String cityCode,
String cityName, String orgCode, String fileName,
String localPath, 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);
// 通话方向:1呼入,2呼出
record.setCallDirection(isOutgoing ? "2" : "1");
// 主叫/被叫号码
String localNumber = channelPhone != null && !channelPhone.isEmpty() ? channelPhone : "";
String remoteNumber = remotePhone != null ? remotePhone : "";
if (isOutgoing) {
// 呼出:本机打给对方
record.setCallTel(localNumber); // 主叫:本机号码
record.setCalledTel(remoteNumber); // 被叫:对方号码
} else {
// 呼入:对方打给本机
record.setCallTel(remoteNumber); // 主叫:对方号码
record.setCalledTel(localNumber); // 被叫:本机号码
}
// 通话状态
record.setCallStatus(isAnswered ? "1" : "2"); // 1正常接通,2未接通
return record;
}
// ==================== 工具方法 ====================
/**
* 构建设备访问地址
*/
private String buildDeviceHost(String ip, Integer port) {
return ip + (port != null && port != 80 ? ":" + port : "");
}
/**
* 解析通话时间
*/
private Date parseCallTime(JSONObject record) {
Long begTime = RecordParser.parseBegTime(record);
return begTime != null ? new Date(begTime * 1000) : null;
}
/**
* 从Map中获取字符串值
*/
private String getStringValue(Map<String, Object> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : null;
}
/**
* 从Map中获取整数值
*/
private Integer getIntValue(Map<String, Object> map, String key, Integer defaultValue) {
Object value = map.get(key);
if (value == null) {
return defaultValue;
}
try {
return Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* 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) {
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public String toString() {
return String.format("[%d, %d]", startTime, endTime);
}
}
/**
* 同步结果
*/
private static class SyncResult {
int successCount = 0;
int failCount = 0;
Date latestCallTime = null;
}
}

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

@ -0,0 +1,48 @@
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

@ -0,0 +1,115 @@
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

@ -0,0 +1,46 @@
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

@ -0,0 +1,138 @@
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());
}
}
}
}

76
src/main/java/com/threecloud/dataserviceyy/util/DataUtil.java

@ -0,0 +1,76 @@
package com.threecloud.dataserviceyy.util;
import java.util.Collection;
import java.util.Map;
public class DataUtil {
public static boolean isNotNull(Object obj) {
return obj != null;
}
public static boolean isNotNull(Map map) {
return map != null && !map.isEmpty();
}
public static boolean isNotNull(Collection collection) {
return collection != null && !collection.isEmpty();
}
public static boolean isNotNull(String str) {
return str != null && !str.trim().isEmpty();
}
public static boolean isNumber(Object obj) {
if (obj == null) return false;
try {
Double.parseDouble(obj.toString());
return true;
} catch (NumberFormatException e) {
return false;
}
}
public static Long objToLong(Object obj) {
if (obj == null) return null;
if (obj instanceof Long) return (Long) obj;
if (obj instanceof Number) return ((Number) obj).longValue();
if (isNumber(obj)) return Long.parseLong(obj.toString());
return null;
}
public static String objToValue(Object obj) {
if (obj == null) return null;
return obj.toString();
}
public static Integer objToInteger(Object obj) {
if (obj == null) return null;
if (obj instanceof Integer) return (Integer) obj;
if (obj instanceof Number) return ((Number) obj).intValue();
if (isNumber(obj)) return Integer.parseInt(obj.toString());
return null;
}
public static Float objToFloat(Object obj) {
if (obj == null) return null;
if (obj instanceof Float) return (Float) obj;
if (obj instanceof Number) return ((Number) obj).floatValue();
if (isNumber(obj)) return Float.parseFloat(obj.toString());
return null;
}
public static Double objToDouble(Object obj) {
if (obj == null) return null;
if (obj instanceof Double) return (Double) obj;
if (obj instanceof Number) return ((Number) obj).doubleValue();
if (isNumber(obj)) return Double.parseDouble(obj.toString());
return null;
}
public static boolean objToBoolean(Object obj) {
if (obj == null) return false;
if (obj instanceof Boolean) return (Boolean) obj;
return Boolean.parseBoolean(obj.toString());
}
}

57
src/main/java/com/threecloud/dataserviceyy/util/DateUtil.java

@ -0,0 +1,57 @@
package com.threecloud.dataserviceyy.util;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 日期工具类
*/
public class DateUtil {
public static final SimpleDateFormat dateFmt4 = new SimpleDateFormat("yyyyMMdd");
/**
* 计算两个日期之间的天数差
* @param date1 日期1
* @param date2 日期2
* @return 天数差
*/
public static double getDateDoubleDiff(Date date1, Date date2) {
if (date1 == null || date2 == null) {
return 0;
}
long diff = date1.getTime() - date2.getTime();
return diff / (1000.0 * 60 * 60 * 24);
}
/**
* 日期加减天数
* @param date 基准日期
* @param days 天数正数加负数减
* @return 计算后的日期
*/
public static Date addDayByDate(Date date, int days) {
if (date == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, days);
return calendar.getTime();
}
/**
* 格式化日期
* @param date 日期
* @param pattern 格式
* @return 格式化后的字符串
*/
public static String formatDate(Date date, String pattern) {
if (date == null) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.format(date);
}
}

177
src/main/java/com/threecloud/dataserviceyy/util/FileCleaner.java

@ -0,0 +1,177 @@
package com.threecloud.dataserviceyy.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 文件清理工具类
* 负责清理过期的本地录音文件
*
* 目录结构
* vaa-recordings/
* 340100/ # 地市编码
* 20240101/ # 日期目录
* <device_uuid>/ # 设备目录
* xxx.wav
* 20240102/
* ...
* 340200/
* .sync-marker/
*/
public class FileCleaner {
private static final Logger logger = LoggerFactory.getLogger(FileCleaner.class);
/**
* 默认保留天数
*/
private static final int DEFAULT_RETAIN_DAYS = 10;
/**
* 日期格式
*/
private static final String DATE_FORMAT = "yyyyMMdd";
/**
* 地市编码正则6位数字
*/
private static final String CITY_CODE_PATTERN = "\\d{6}";
/**
* 清理指定天数前的本地录音文件
*
* 清理逻辑
* 1. 遍历 basePath 下地市目录 340100, 340200
* 2. 遍历每个地市目录下的日期目录 20240101
* 3. 删除早于 cutoffDate 的日期目录
*
* @param basePath 基础路径
* @param retainDays 保留天数
*/
public static void cleanOldFiles(String basePath, int retainDays) {
File baseDir = new File(basePath);
if (!baseDir.exists() || !baseDir.isDirectory()) {
logger.debug("基础目录不存在或不是目录: {}", basePath);
return;
}
// 获取地市目录列表
File[] cityDirs = baseDir.listFiles(File::isDirectory);
if (cityDirs == null || cityDirs.length == 0) {
logger.debug("目录下没有地市子目录: {}", basePath);
return;
}
String cutoffDate = calculateCutoffDate(retainDays);
int totalDeletedCount = 0;
for (File cityDir : cityDirs) {
String cityCode = cityDir.getName();
// 跳过非地市目录(如 .sync-marker)
if (!cityCode.matches(CITY_CODE_PATTERN)) {
logger.debug("跳过非地市目录: {}", cityCode);
continue;
}
// 清理该地市下的过期日期目录
int deletedCount = cleanCityDirectory(cityDir, cutoffDate);
totalDeletedCount += deletedCount;
// 如果地市目录为空,删除地市目录
if (deletedCount > 0 && isEmptyDirectory(cityDir)) {
cityDir.delete();
logger.info("已删除空地市目录: {}", cityDir.getAbsolutePath());
}
}
if (totalDeletedCount > 0) {
logger.info("清理完成,共删除 {} 个过期日期目录", totalDeletedCount);
} else {
logger.debug("没有需要清理的过期文件");
}
}
/**
* 清理指定地市目录下的过期日期目录
*
* @param cityDir 地市目录
* @param cutoffDate 截止日期
* @return 删除的目录数
*/
private static int cleanCityDirectory(File cityDir, String cutoffDate) {
File[] dateDirs = cityDir.listFiles(File::isDirectory);
if (dateDirs == null || dateDirs.length == 0) {
return 0;
}
int deletedCount = 0;
for (File dateDir : dateDirs) {
String dirName = dateDir.getName();
// 只处理日期格式的目录(如 20240101)
if (dirName.matches("\\d{8}") && dirName.compareTo(cutoffDate) < 0) {
if (deleteDirectory(dateDir)) {
deletedCount++;
logger.info("已清理过期录音目录: {} (早于 {})", dateDir.getAbsolutePath(), cutoffDate);
}
}
}
return deletedCount;
}
/**
* 检查目录是否为空不包含任何文件和子目录
*
* @param dir 目录
* @return 是否为空
*/
private static boolean isEmptyDirectory(File dir) {
File[] files = dir.listFiles();
return files == null || files.length == 0;
}
/**
* 清理默认天数10天前的文件
*
* @param basePath 基础路径
*/
public static void cleanOldFiles(String basePath) {
cleanOldFiles(basePath, DEFAULT_RETAIN_DAYS);
}
/**
* 递归删除目录
*
* @param dir 要删除的目录
* @return 是否成功删除
*/
private static boolean deleteDirectory(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
}
return dir.delete();
}
/**
* 计算截止日期
*
* @param retainDays 保留天数
* @return 截止日期字符串yyyyMMdd
*/
private static String calculateCutoffDate(int retainDays) {
Date cutoffDate = DateUtil.addDayByDate(new Date(), -retainDays);
return new SimpleDateFormat(DATE_FORMAT).format(cutoffDate);
}
}

97
src/main/java/com/threecloud/dataserviceyy/util/FilePathUtil.java

@ -0,0 +1,97 @@
package com.threecloud.dataserviceyy.util;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 文件路径工具类
* 统一管理本地文件存储路径生成逻辑
*/
public class FilePathUtil {
/**
* 日期格式yyyyMMdd
*/
private static final String DATE_FORMAT = "yyyyMMdd";
/**
* 生成本地文件存储路径按地市分文件夹
*
* 路径格式: {basePath}/{cityCode}/{date}/{uuid}/{fileName}
* 示例: ./vaa-recordings/340100/20240101/uuid-xxx/xxx.wav
*
* @param basePath 基础路径 ./vaa-recordings
* @param cityCode 地市编码 340100
* @param date 日期
* @param uuid 设备UUID
* @param fileName 文件名
* @return 完整路径
*/
public static String buildLocalPath(String basePath, String cityCode, Date date, String uuid, String fileName) {
String dateDir = new SimpleDateFormat(DATE_FORMAT).format(date);
return basePath + File.separator + cityCode + File.separator + dateDir + File.separator + uuid + File.separator + fileName;
}
/**
* 从文件路径中提取文件名
*
* @param filePath 文件路径 /record/2024/01/01/test.wav
* @return 文件名 test.wav
*/
public static String extractFileName(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return "";
}
int lastSlash = filePath.lastIndexOf("/");
return lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
}
/**
* 生成OSS存储路径按地市分文件夹
*
* 路径格式: {cityCode}/{date}/{fileName}
* 示例: 340100/20240101/xxx.wav
*
* @param cityCode 地市编码 340100
* @param date 日期
* @param fileName 文件名
* @return OSS存储路径
*/
public static String buildOssPath(String cityCode, Date date, String fileName) {
String dateDir = new SimpleDateFormat(DATE_FORMAT).format(date);
return cityCode + "/" + dateDir + "/" + fileName;
}
/**
* 生成OSS存储的日期目录已废弃请使用 buildOssPath
*
* @param date 日期
* @return 日期目录 20240101
*/
@Deprecated
public static String formatDateDir(Date date) {
return new SimpleDateFormat(DATE_FORMAT).format(date);
}
/**
* 构建同步标记文件路径
*
* @param basePath 基础路径
* @param deviceId 设备ID
* @return 标记文件路径
*/
public static String buildSyncMarkerPath(String basePath, String deviceId) {
return basePath + File.separator + ".sync-marker" + File.separator + deviceId + ".time";
}
/**
* 构建同步标记目录路径
*
* @param basePath 基础路径
* @return 标记目录路径
*/
public static String buildSyncMarkerDir(String basePath) {
return basePath + File.separator + ".sync-marker";
}
}

290
src/main/java/com/threecloud/dataserviceyy/util/FileUploadUtil.java

@ -0,0 +1,290 @@
package com.threecloud.dataserviceyy.util;
import com.threecloud.dataserviceyy.entity.ResultEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.net.HttpURLConnection;
/**
* 文件上传工具类
* 负责上传录音文件到 OSS并返回完整的访问 URL
*
* 配置说明
* vaa-sync.oss.base-url: OSS服务基础地址用于拼接完整URL
* vaa-sync.oss.upload-url: OSS上传接口地址
* vaa-sync.oss.appcode/appid/appsecret: OSS认证信息
*/
@Component
public class FileUploadUtil {
private static final Logger logger = LoggerFactory.getLogger(FileUploadUtil.class);
// OSS配置(从配置文件读取)
@Value("${vaa-sync.oss.base-url:http://53.1.194.59:9090}")
private String ossBaseUrl;
@Value("${vaa-sync.oss.upload-url:http://53.1.194.59:9090/apiOss/oss/fileUpload}")
private String ossUploadUrl;
@Value("${vaa-sync.oss.appcode:dataservice-yy}")
private String ossAppcode;
@Value("${vaa-sync.oss.appid:371a3368-e28e-4ba3-95a3-c31c19cf0ad0}")
private String ossAppid;
@Value("${vaa-sync.oss.appsecret:06a6a80e-f9d2-4b3b-acc0-8d182c876074}")
private String ossAppsecret;
private RestTemplate restTemplate;
@PostConstruct
public void init() {
restTemplate = createRestTemplate();
logger.info("文件上传工具初始化完成");
logger.info(" OSS基础地址: {}", ossBaseUrl);
logger.info(" 上传接口: {}", ossUploadUrl);
}
/**
* 上传WAV录音文件到OSS
*
* 存储路径格式: voice/{cityName}/{cityCode}/{date}/{filename}
* 示例: voice/合肥/340100/20240101/IN-xxx.wav
*
* 返回完整URL格式: {ossBaseUrl}/voice/{cityName}/{cityCode}/{date}/{filename}
* 示例: http://53.1.194.59:9090/voice/合肥/340100/20240101/IN-xxx.wav
*
* @param cityName 地市名称 合肥
* @param ossPath OSS存储路径格式: cityCode/date/filename
* @param wavFileName WAV文件名
* @param wavData 文件字节数组
* @return 完整的文件访问URL
*/
public String uploadWav(String cityName, String ossPath, String wavFileName, byte[] wavData) {
// 构建OSS对象名: voice/{cityName}/{cityCode}/{date}/{filename}
String objectName = "voice/" + cityName + "/" + ossPath;
logger.debug("构建OSS对象名: {}", objectName);
// 上传文件
String relativeUrl = uploadFile(objectName, wavData);
// 返回完整URL
String fullUrl = buildFullUrl(relativeUrl);
logger.info("文件上传完成,完整URL: {}", fullUrl);
return fullUrl;
}
/**
* 构建完整的文件访问URL
*
* 如果返回的是相对路径拼接 base-url
* 如果返回的已经是完整URL直接返回
*
* @param url 从OSS返回的URL可能是相对路径或完整URL
* @return 完整的访问URL
*/
private String buildFullUrl(String url) {
if (url == null || url.isEmpty()) {
return "";
}
// 如果已经是完整URL,直接返回
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
// 拼接基础URL
String baseUrl = ossBaseUrl.endsWith("/") ? ossBaseUrl.substring(0, ossBaseUrl.length() - 1) : ossBaseUrl;
String relativePath = url.startsWith("/") ? url : "/" + url;
return baseUrl + relativePath;
}
/**
* 上传文件到OSS
*
* @param fileName 文件在OSS中的路径 voice/合肥/340100/...
* @param fileData 文件字节数组
* @return OSS返回的文件路径或URL
*/
private String uploadFile(String fileName, byte[] fileData) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
ByteArrayResource byteResource = new ByteArrayResource(fileData) {
@Override
public String getFilename() {
return fileName;
}
};
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("files", byteResource);
body.set("appcode", ossAppcode);
body.set("appid", ossAppid);
body.set("appsecret", ossAppsecret);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
logger.debug("开始上传文件到OSS: {}, 大小: {} bytes", fileName, fileData.length);
ResponseEntity<ResultEntity> response = restTemplate.exchange(
ossUploadUrl,
HttpMethod.POST,
requestEntity,
ResultEntity.class
);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
ResultEntity result = response.getBody();
if (result.getCode() == ResultEntity.StatusCode.SUCCESS.getCode()) {
String url = parseUrlFromContent(result.getContent());
logger.debug("OSS返回URL: {}", url);
return url;
} else {
throw new RuntimeException("上传失败, code=" + result.getCode() + ", msg=" + result.getMsg());
}
} else {
throw new RuntimeException("上传失败, HTTP状态码: " + response.getStatusCode());
}
} catch (Exception e) {
logger.error("文件上传异常: {}", fileName, e);
throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
}
}
/**
* ResultEntity.content 中解析文件URL
*
* 支持多种返回格式
* 1. Map 对象: {"url": "http://xxx", "fileName": "xxx.wav"}
* 2. JSON 字符串: "{\"url\":\"http://xxx\"}"
* 3. URL 字符串: "http://xxx/xxx.wav"
* 4. 数组格式: [{"url": "http://xxx"}]
* 5. 相对路径: "voice/xxx/xxx.wav"
*
* @param content OSS 返回的内容
* @return 解析后的文件路径或URL
*/
private String parseUrlFromContent(Object content) {
if (content == null) {
logger.warn("OSS 返回 content 为空");
return "";
}
// 情况1:Map 对象
if (content instanceof java.util.Map) {
java.util.Map<?, ?> map = (java.util.Map<?, ?>) content;
Object url = map.get("url");
if (url != null) {
logger.debug("从 Map 解析到 URL: {}", url);
return url.toString();
}
// 尝试其他可能的 key
Object fileUrl = map.get("fileUrl");
if (fileUrl != null) {
logger.debug("从 Map 解析到 fileUrl: {}", fileUrl);
return fileUrl.toString();
}
Object path = map.get("path");
if (path != null) {
logger.debug("从 Map 解析到 path: {}", path);
return path.toString();
}
}
// 情况2:List/Array 对象(批量上传返回)
if (content instanceof java.util.List) {
java.util.List<?> list = (java.util.List<?>) content;
if (!list.isEmpty()) {
Object first = list.get(0);
if (first instanceof java.util.Map) {
Object url = ((java.util.Map<?, ?>) first).get("url");
if (url != null) {
logger.debug("从 List 第一个元素解析到 URL: {}", url);
return url.toString();
}
}
}
}
// 情况3:JSON 字符串
String contentStr = content.toString().trim();
if (contentStr.startsWith("{") || contentStr.startsWith("[")) {
try {
// 尝试解析为 JSON
Object parsed = com.alibaba.fastjson2.JSON.parse(contentStr);
if (parsed instanceof java.util.Map) {
return parseUrlFromContent(parsed); // 递归解析
}
if (parsed instanceof java.util.List) {
return parseUrlFromContent(parsed); // 递归解析
}
} catch (Exception e) {
logger.debug("解析 JSON 失败: {}", e.getMessage());
}
}
// 情况4:直接是 URL 字符串(以 http:// 或 https:// 开头)
if (contentStr.startsWith("http://") || contentStr.startsWith("https://")) {
logger.debug("直接返回 URL 字符串: {}", contentStr);
return contentStr;
}
// 情况5:相对路径(以 voice/ 开头)
if (contentStr.startsWith("voice/")) {
logger.debug("返回相对路径: {}", contentStr);
return contentStr;
}
// 情况6:尝试从字符串中提取 URL
if (contentStr.contains("\"url\"")) {
try {
int start = contentStr.indexOf("\"url\":\"") + 7;
int end = contentStr.indexOf("\"", start);
if (start > 6 && end > start) {
String url = contentStr.substring(start, end);
logger.debug("从字符串提取 URL: {}", url);
return url;
}
} catch (Exception e) {
logger.warn("从字符串提取 URL 失败: {}", e.getMessage());
}
}
// 兜底:直接返回字符串
logger.warn("无法解析 URL,直接返回 content: {}", contentStr);
return contentStr;
}
/**
* 创建支持自定义SSL配置的RestTemplate
* 针对HTTPS请求配置信任所有证书并关闭Hostname验证以兼容自签名证书
*/
private RestTemplate createRestTemplate() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
if (ossUploadUrl.startsWith("https") && connection instanceof HttpsURLConnection) {
// HTTPS 特殊配置(如需要)
}
super.prepareConnection(connection, httpMethod);
}
};
requestFactory.setBufferRequestBody(false);
return new RestTemplate(requestFactory);
}
}

88
src/main/java/com/threecloud/dataserviceyy/util/RecordParser.java

@ -0,0 +1,88 @@
package com.threecloud.dataserviceyy.util;
import com.alibaba.fastjson2.JSONObject;
/**
* 录音记录解析工具类
* 封装从 EBOX API 返回的 JSON 中提取字段的逻辑
*/
public class RecordParser {
/**
* 解析录音记录ID
*/
public static String parseRecordId(JSONObject record) {
return record.getString("id");
}
/**
* 解析文件路径
*/
public static String parseFilePath(JSONObject record) {
return record.getString("file");
}
/**
* 解析通道号
*/
public static Integer parseChannel(JSONObject record) {
Object channelObj = record.get("channel");
return channelObj instanceof Number ? ((Number) channelObj).intValue() : null;
}
/**
* 解析对方电话号码
*/
public static String parsePhone(JSONObject record) {
return record.getString("phone");
}
/**
* 解析通话状态方向
* 1=呼出其他=呼入
*/
public static Integer parseState(JSONObject record) {
Object stateObj = record.get("state");
return stateObj instanceof Number ? ((Number) stateObj).intValue() : null;
}
/**
* 解析开始时间Unix时间戳
*/
public static Long parseBegTime(JSONObject record) {
Object begtimeObj = record.get("begtime");
return begtimeObj instanceof Number ? ((Number) begtimeObj).longValue() : null;
}
/**
* 解析结束时间Unix时间戳
*/
public static Long parseEndTime(JSONObject record) {
Object endtimeObj = record.get("endtime");
return endtimeObj instanceof Number ? ((Number) endtimeObj).longValue() : null;
}
/**
* 解析是否接听
* 1=接听0=未接听
*/
public static Integer parseAnswer(JSONObject record) {
Object answerObj = record.get("answer");
return answerObj instanceof Number ? ((Number) answerObj).intValue() : 0;
}
/**
* 判断是否为呼出
*/
public static boolean isOutgoing(JSONObject record) {
Integer state = parseState(record);
return state != null && state == 1;
}
/**
* 判断是否已接听
*/
public static boolean isAnswered(JSONObject record) {
return parseAnswer(record) == 1;
}
}

70
src/main/java/com/threecloud/dataserviceyy/util/SyncTimeUtil.java

@ -0,0 +1,70 @@
package com.threecloud.dataserviceyy.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.util.Date;
/**
* 同步时间工具类
* 管理设备上次同步时间的本地文件存储
*/
public class SyncTimeUtil {
private static final Logger logger = LoggerFactory.getLogger(SyncTimeUtil.class);
/**
* 从本地文件读取上次同步时间
*
* @param basePath 基础路径
* @param deviceId 设备ID
* @return 上次同步时间不存在返回null
*/
public static Date readLastSyncTime(String basePath, String deviceId) {
String markerPath = FilePathUtil.buildSyncMarkerPath(basePath, deviceId);
File markerFile = new File(markerPath);
if (!markerFile.exists()) {
logger.debug("同步标记文件不存在: deviceId={}", deviceId);
return null;
}
try {
byte[] bytes = Files.readAllBytes(markerFile.toPath());
long millis = Long.parseLong(new String(bytes).trim());
Date syncTime = new Date(millis);
logger.debug("读取到上次同步时间: deviceId={}, time={}", deviceId, syncTime);
return syncTime;
} catch (Exception e) {
logger.warn("读取同步时间失败: deviceId={}, error={}", deviceId, e.getMessage());
return null;
}
}
/**
* 保存同步时间到本地文件
*
* @param basePath 基础路径
* @param deviceId 设备ID
* @param time 同步时间
*/
public static void writeLastSyncTime(String basePath, String deviceId, Date time) {
String markerDir = FilePathUtil.buildSyncMarkerDir(basePath);
File dir = new File(markerDir);
if (!dir.exists()) {
dir.mkdirs();
}
String markerPath = FilePathUtil.buildSyncMarkerPath(basePath, deviceId);
File markerFile = new File(markerPath);
try {
Files.write(markerFile.toPath(), String.valueOf(time.getTime()).getBytes());
logger.debug("保存同步时间成功: deviceId={}, time={}", deviceId, time);
} catch (Exception e) {
logger.warn("保存同步时间失败: deviceId={}, error={}", deviceId, e.getMessage());
}
}
}

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

@ -0,0 +1,332 @@
package com.threecloud.dataserviceyy.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
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;
/**
* HTTP登录认证
* @param loginUrl 登录URL例如http://192.168.1.100/authorize?username=admin&password=admin123
* @return 登录成功后返回Authorization Cookie值
*/
public String httpLogin(String loginUrl) throws Exception {
return executeWithRetry(() -> {
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");
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
* @param authorization 登录后返回的Authorization Cookie值
* @return 返回JSON字符串
*/
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);
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();
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 savePath 保存路径
* @param authorization 登录后返回的Authorization Cookie值
*/
public void httpDown(String fileUrl, String savePath, String authorization) throws Exception {
executeWithRetry(() -> {
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;
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);
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
inputStream = conn.getInputStream();
outputStream = new FileOutputStream(savePath);
byte[] buffer = new byte[8192];
int bytesRead;
long totalBytes = 0;
long lastLogTime = System.currentTimeMillis();
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;
}
}
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();
}
}
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) {
if (c != null) {
try { c.close(); } catch (IOException ignored) {}
}
}
private void addAuthorization(HttpURLConnection conn, String authorization) {
if (authorization != null && !authorization.isEmpty()) {
conn.setRequestProperty("Cookie", "Authorization=" + authorization);
}
}
private boolean isLoginPage(String responseBody) {
if (responseBody == null) {
return false;
}
String body = responseBody.trim().toLowerCase();
return body.startsWith("<!doctype") || body.startsWith("<html")
|| (body.contains("<form") && body.contains("password"));
}
private String getAuthorizationCookie(HttpURLConnection conn) {
for (Map.Entry<String, List<String>> entry : conn.getHeaderFields().entrySet()) {
if (entry.getKey() == null || !"Set-Cookie".equalsIgnoreCase(entry.getKey())) {
continue;
}
String authorization = getAuthorizationValue(entry.getValue());
if (authorization != null && !authorization.isEmpty()) {
return authorization;
}
}
return null;
}
private String getAuthorizationValue(List<String> cookies) {
if (cookies == null || cookies.isEmpty()) {
return null;
}
for (String cookie : cookies) {
String prefix = "Authorization=";
int start = cookie.indexOf(prefix);
if (start < 0) {
continue;
}
start += prefix.length();
int end = cookie.indexOf(';', start);
return end >= 0 ? cookie.substring(start, end) : cookie.substring(start);
}
return null;
}
/**
* 解析通道状态JSON
*/
public JSONArray parseChannelData(String jsonData) {
if (jsonData == null || jsonData.isEmpty() || "[]".equals(jsonData)) {
return new JSONArray();
}
return JSON.parseArray(jsonData);
}
/**
* 解析录音记录JSON
*/
public JSONArray parseRecordData(String jsonData) {
if (jsonData == null || jsonData.isEmpty() || "[]".equals(jsonData)) {
return new JSONArray();
}
return JSON.parseArray(jsonData);
}
/**
* 获取分机号码配置
* EBOX接口: GET /service/ext/number
* 返回每条线路的分机号码: {"1":"8001","2":"8002",...}
* @param extUrl 接口地址例如http://192.168.1.100/service/ext/number
* @param authorization 登录后返回的Authorization Cookie值
* @return 通道-分机号映射 Map<channel, phoneNumber>
*/
public Map<String, String> getExtensionNumbers(String extUrl, String authorization) throws Exception {
String jsonData = httpVisit(extUrl, authorization);
if (jsonData == null || jsonData.isEmpty() || "[]".equals(jsonData) || "{}".equals(jsonData)) {
return new java.util.HashMap<>();
}
try {
// EBOX返回的是JSON对象,key是通道号(1-8),value是分机号码
com.alibaba.fastjson2.JSONObject jsonObj = JSON.parseObject(jsonData);
Map<String, String> result = new java.util.HashMap<>();
for (String key : jsonObj.keySet()) {
String value = jsonObj.getString(key);
result.put(key, value != null ? value : "");
}
logger.info("获取到分机号码配置: {} 条", result.size());
return result;
} catch (Exception e) {
logger.warn("解析分机号码配置失败: {}", e.getMessage());
return new java.util.HashMap<>();
}
}
}

72
src/main/java/com/threecloud/dataserviceyy/util/fileUtil.java

@ -0,0 +1,72 @@
package com.threecloud.dataserviceyy.util;
import com.threecloud.dataserviceyy.entity.ResultEntity;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.net.HttpURLConnection;
public class fileUtil {
/**
* 通过oss上传文件
* @param mf
* @return ResultEntity
* @throws IOException
*/
private ResultEntity uploadFilesByOss(MultipartFile mf) throws IOException{
String ossUrl = "http://127.0.0.1/apiOss";//baseOssConfigService.readOSsUrl();// OSS管理服务地址
String ossPreviewUrl = "http://127.0.0.1/apiOssPreview/onlinePreview";//baseOssConfigService.readOSsPreviewUrl();// OSS预览服务地址
String ossAppid = "371a3368-e28e-4ba3-95a3-c31c19cf0ad0";//baseOssConfigService.readOSsAppid();// OSS应用ID
String ossAppsecret = "06a6a80e-f9d2-4b3b-acc0-8d182c876074";//baseOssConfigService.readOSsAppsecret();// OSS应用密钥
String ossAppcode = "dataservice-yy";//baseOssConfigService.readOSsAppcode();// OSS应用别名
ResponseEntity<ResultEntity> responseEntity = null;
RestTemplate restTemplate = createRestTemplate(ossUrl);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> files = new LinkedMultiValueMap<String, Object>();
//for (MultipartFile file : multipartFiles) {
ByteArrayResource byteResource = new ByteArrayResource(mf.getBytes()){
@Override
public String getFilename() {
return mf.getOriginalFilename();
}
};
files.add("files", byteResource);//上传文件
//}
files.add("appcode", ossAppcode);//项目编号
files.set("appcode", ossAppcode);
files.set("appid", ossAppid);
files.set("appsecret", ossAppsecret);
HttpEntity<MultiValueMap<String, Object>> httpEntity =
new HttpEntity<MultiValueMap<String, Object>>(files,headers);
responseEntity = restTemplate.exchange(ossUrl + "/oss/fileUpload", HttpMethod.POST, httpEntity, ResultEntity.class);
return responseEntity.getBody();
}
/**
* 创建支持自定义SSL配置的RestTemplate
* 针对HTTPS请求配置信任所有证书并关闭Hostname验证以兼容自签名证书
*/
private RestTemplate createRestTemplate(String ossUrl) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
if (ossUrl != null && ossUrl.startsWith("https") && connection instanceof HttpsURLConnection) {
}
super.prepareConnection(connection, httpMethod);
}
};
requestFactory.setBufferRequestBody(false);
return new RestTemplate(requestFactory);
}
}

70
src/main/resources/application.yml

@ -0,0 +1,70 @@
spring:
application:
name: dataservice-yy
# 引入外部配置文件(部署时修改 config/application-external.yml)
# 注意:路径是相对于 JAR 包所在目录的
config:
import: optional:file:./config/application-external.yml
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.threecloud.dataserviceyy
configuration:
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

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

@ -0,0 +1,127 @@
<?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.MidVoiceCallRecordMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.MidVoiceCallRecord">
<id column="id" property="id"/>
<result column="city_code" property="cityCode"/>
<result column="city_name" property="cityName"/>
<result column="call_record_id" property="callRecordId"/>
<result column="call_tel" property="callTel"/>
<result column="called_tel" property="calledTel"/>
<result column="call_start_time" property="callStartTime"/>
<result column="call_end_time" property="callEndTime"/>
<result column="call_duration" property="callDuration"/>
<result column="call_direction" property="callDirection"/>
<result column="device_no" property="deviceNo"/>
<result column="business_scenario" property="businessScenario"/>
<result column="recording_file_name" property="recordingFileName"/>
<result column="recording_file_path" property="recordingFilePath"/>
<result column="recording_file_size" property="recordingFileSize"/>
<result column="call_status" property="callStatus"/>
<result column="fail_reason" property="failReason"/>
<result column="remarks" property="remarks"/>
<result column="sync_time" property="syncTime"/>
<result column="create_time" property="createTime"/>
<result column="org_code" property="orgCode"/>
</resultMap>
<!-- 插入通话记录 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
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,
device_no, business_scenario, recording_file_name, recording_file_path,
recording_file_size, call_status, fail_reason, remarks, sync_time, create_time, org_code
) VALUES (
#{cityCode}, #{cityName}, #{callRecordId}, #{callTel}, #{calledTel},
#{callStartTime}, #{callEndTime}, #{callDuration}, #{callDirection},
#{deviceNo}, #{businessScenario}, #{recordingFileName}, #{recordingFilePath},
#{recordingFileSize}, #{callStatus}, #{failReason}, #{remarks}, NOW(), NOW(), #{orgCode}
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record WHERE id = #{id}
</select>
<!-- 根据通话记录ID查询(防止重复) -->
<select id="selectByCallRecordId" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE call_record_id = #{callRecordId}
LIMIT 1
</select>
<!-- 根据设备编码查询 -->
<select id="selectByDeviceNo" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE device_no = #{deviceNo}
ORDER BY call_start_time DESC
</select>
<!-- 分页查询 -->
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM mid_voice_call_record
WHERE 1=1
<if test="cityCode != null and cityCode != ''">
AND city_code = #{cityCode}
</if>
<if test="deviceNo != null and deviceNo != ''">
AND device_no = #{deviceNo}
</if>
<if test="callTel != null and callTel != ''">
AND call_tel LIKE CONCAT('%', #{callTel}, '%')
</if>
<if test="calledTel != null and calledTel != ''">
AND called_tel LIKE CONCAT('%', #{calledTel}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
ORDER BY call_start_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 统计总数 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM mid_voice_call_record
WHERE 1=1
<if test="cityCode != null and cityCode != ''">
AND city_code = #{cityCode}
</if>
<if test="deviceNo != null and deviceNo != ''">
AND device_no = #{deviceNo}
</if>
<if test="callTel != null and callTel != ''">
AND call_tel LIKE CONCAT('%', #{callTel}, '%')
</if>
<if test="calledTel != null and calledTel != ''">
AND called_tel LIKE CONCAT('%', #{calledTel}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
</select>
<!-- 更新文件路径 -->
<update id="updateFilePath">
UPDATE mid_voice_call_record
SET recording_file_path = #{recordingFilePath}, sync_time = NOW()
WHERE id = #{id}
</update>
<!-- 更新状态 -->
<update id="updateStatus">
UPDATE mid_voice_call_record
SET call_status = #{callStatus}, fail_reason = #{failReason}, sync_time = NOW()
WHERE id = #{id}
</update>
</mapper>

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

@ -0,0 +1,104 @@
<?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>

85
src/main/resources/mapper/SyncLogMapper.xml

@ -0,0 +1,85 @@
<?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.SyncLogMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.SyncLog">
<id column="id" property="id"/>
<result column="device_id" property="deviceId"/>
<result column="device_name" property="deviceName"/>
<result column="start_time" property="startTime"/>
<result column="end_time" property="endTime"/>
<result column="total_count" property="totalCount"/>
<result column="success_count" property="successCount"/>
<result column="fail_count" property="failCount"/>
<result column="status" property="status"/>
<result column="error_msg" property="errorMsg"/>
<result column="create_time" property="createTime"/>
</resultMap>
<!-- 插入同步日志 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO sync_log (
device_id, device_name, start_time, end_time, total_count,
success_count, fail_count, status, error_msg, create_time
) VALUES (
#{deviceId}, #{deviceName}, #{startTime}, #{endTime}, #{totalCount},
#{successCount}, #{failCount}, #{status}, #{errorMsg}, NOW()
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM sync_log WHERE id = #{id}
</select>
<!-- 查询设备的同步日志 -->
<select id="selectByDeviceId" resultMap="BaseResultMap">
SELECT * FROM sync_log
WHERE device_id = #{deviceId}
ORDER BY create_time DESC
</select>
<!-- 分页查询 -->
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM sync_log
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="startTime != null and startTime != ''">
AND start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND start_time &lt;= #{endTime}
</if>
ORDER BY create_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 统计总数 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM sync_log
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="startTime != null and startTime != ''">
AND start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND start_time &lt;= #{endTime}
</if>
</select>
<!-- 更新同步结果 -->
<update id="updateResult">
UPDATE sync_log
SET end_time = #{endTime},
success_count = #{successCount},
fail_count = #{failCount},
status = #{status},
error_msg = #{errorMsg}
WHERE id = #{id}
</update>
</mapper>

117
src/main/resources/mapper/VoiceRecordMapper.xml

@ -0,0 +1,117 @@
<?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.VoiceRecordMapper">
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.VoiceRecord">
<id column="id" property="id"/>
<result column="device_id" property="deviceId"/>
<result column="device_uuid" property="deviceUuid"/>
<result column="device_name" property="deviceName"/>
<result column="record_id" property="recordId"/>
<result column="file_name" property="fileName"/>
<result column="file_path" property="filePath"/>
<result column="oss_url" property="ossUrl"/>
<result column="local_path" property="localPath"/>
<result column="call_start_time" property="callStartTime"/>
<result column="call_end_time" property="callEndTime"/>
<result column="duration" property="duration"/>
<result column="channel" property="channel"/>
<result column="phone" property="phone"/>
<result column="direction" property="direction"/>
<result column="status" property="status"/>
<result column="fail_reason" property="failReason"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<!-- 插入录音记录 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO voice_record (
device_id, device_uuid, device_name, record_id, file_name, file_path,
oss_url, local_path, call_start_time, call_end_time, duration,
channel, phone, direction, status, fail_reason, create_time, update_time
) VALUES (
#{deviceId}, #{deviceUuid}, #{deviceName}, #{recordId}, #{fileName}, #{filePath},
#{ossUrl}, #{localPath}, #{callStartTime}, #{callEndTime}, #{duration},
#{channel}, #{phone}, #{direction}, #{status}, #{failReason}, NOW(), NOW()
)
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM voice_record WHERE id = #{id}
</select>
<!-- 根据设备ID和录音ID查询(防止重复) -->
<select id="selectByDeviceAndRecordId" resultMap="BaseResultMap">
SELECT * FROM voice_record
WHERE device_id = #{deviceId} AND record_id = #{recordId}
LIMIT 1
</select>
<!-- 查询设备的所有录音 -->
<select id="selectByDeviceId" resultMap="BaseResultMap">
SELECT * FROM voice_record
WHERE device_id = #{deviceId}
ORDER BY call_start_time DESC
</select>
<!-- 分页查询 -->
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM voice_record
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="deviceName != null and deviceName != ''">
AND device_name LIKE CONCAT('%', #{deviceName}, '%')
</if>
<if test="phone != null and phone != ''">
AND phone LIKE CONCAT('%', #{phone}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
ORDER BY call_start_time DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 统计总数 -->
<select id="count" resultType="int">
SELECT COUNT(*) FROM voice_record
WHERE 1=1
<if test="deviceId != null and deviceId != ''">
AND device_id = #{deviceId}
</if>
<if test="deviceName != null and deviceName != ''">
AND device_name LIKE CONCAT('%', #{deviceName}, '%')
</if>
<if test="phone != null and phone != ''">
AND phone LIKE CONCAT('%', #{phone}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND call_start_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND call_start_time &lt;= #{endTime}
</if>
</select>
<!-- 更新OSS地址 -->
<update id="updateOssUrl">
UPDATE voice_record
SET oss_url = #{ossUrl}, update_time = NOW()
WHERE id = #{id}
</update>
<!-- 更新状态 -->
<update id="updateStatus">
UPDATE voice_record
SET status = #{status}, fail_reason = #{failReason}, update_time = NOW()
WHERE id = #{id}
</update>
</mapper>

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

@ -0,0 +1,124 @@
<?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.VoiceSyncMapper">
<select id="getAllYysb" resultType="java.util.Map">
SELECT id AS ID,
device_no AS UUID,
city_name AS ORGAN_NAME,
city_code AS ORGAN_ID,
ip_address AS IP,
device_port AS PORT,
org_code AS ORG_CODE
FROM mid_voice.mid_voice_device_config
WHERE device_status = '0' and city_code = '340100'
</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>
<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>

16
src/test/java/com/threecloud/dataserviceyy/DataserviceYyApplicationTests.java

@ -0,0 +1,16 @@
package com.threecloud.dataserviceyy;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Disabled // 【方案A: 暂时禁用测试, 因为Oracle在当前环境不可达]
class DataserviceYyApplicationTests {
@Test
@Disabled // 禁用单个测试方法
void contextLoads() {
}
}
Loading…
Cancel
Save