commit
20252cfba3
56 changed files with 5189 additions and 0 deletions
Binary file not shown.
@ -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/ |
|||
@ -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、端口、账号密码是否正确,网络是否连通。 |
|||
|
|||
## 技术支持 |
|||
|
|||
如有问题,请联系开发人员。 |
|||
@ -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. 日志中的错误信息 |
|||
@ -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 |
|||
Binary file not shown.
@ -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. |
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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> |
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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("========================================"); |
|||
}; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.threecloud.dataserviceyy.config; |
|||
|
|||
/** |
|||
* 多数据源配置 - 【方案A阶段: 已完全禁用】 |
|||
* |
|||
* 此类在方案B(多数据源模式)时会重新启用。 |
|||
* 当前阶段使用Spring Boot默认的DataSource自动配置,仿照老系统 yydc-tb-server。 |
|||
*/ |
|||
// @Configuration // 【方案A: 已禁用】
|
|||
public class DynamicDataSourceConfig { |
|||
// 所有方法已禁用,不再干扰Spring Boot的自动配置
|
|||
} |
|||
@ -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<>(); |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 + '\'' + |
|||
'}'; |
|||
} |
|||
} |
|||
@ -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 + "'}"; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
@ -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<>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 |
|||
@ -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 >= #{startTime} |
|||
</if> |
|||
<if test="endTime != null and endTime != ''"> |
|||
AND call_start_time <= #{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 >= #{startTime} |
|||
</if> |
|||
<if test="endTime != null and endTime != ''"> |
|||
AND call_start_time <= #{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> |
|||
@ -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> |
|||
@ -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 >= #{startTime} |
|||
</if> |
|||
<if test="endTime != null and endTime != ''"> |
|||
AND start_time <= #{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 >= #{startTime} |
|||
</if> |
|||
<if test="endTime != null and endTime != ''"> |
|||
AND start_time <= #{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> |
|||
@ -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 >= #{startTime} |
|||
</if> |
|||
<if test="endTime != null and endTime != ''"> |
|||
AND call_start_time <= #{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 >= #{startTime} |
|||
</if> |
|||
<if test="endTime != null and endTime != ''"> |
|||
AND call_start_time <= #{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> |
|||
@ -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> |
|||
@ -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…
Reference in new issue