Compare commits
6 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
13fd7ef7c7 | 3 weeks ago |
|
|
8bbe0d2c04 | 3 weeks ago |
|
|
a83572f109 | 1 month ago |
|
|
05d7e4cc0a | 1 month ago |
|
|
050a2bea11 | 2 months ago |
|
|
c4a86c3ff5 | 2 months ago |
45 changed files with 1566 additions and 2631 deletions
@ -1,213 +1,381 @@ |
|||
# 语音数据同步服务 (dataservice-yy) |
|||
# 语音数据FTP同步服务 |
|||
|
|||
> 基于淮南 yydc-tb-server 老代码改造的 Spring Boot 服务 |
|||
> 用于从FTP服务器读取录音数据,上传至OSS并入库到人大金仓数据库 |
|||
|
|||
## 项目简介 |
|||
|
|||
语音数据同步服务用于从 EBOX 录音盒拉取录音文件,上传到 OSS,并保存通话记录到数据库。 |
|||
本服务定时从FTP服务器读取通话记录txt文件,下载对应的mp3录音文件,上传到OSS对象存储,并将通话记录保存到人大金仓数据库。**每个地市单独部署一份服务**。 |
|||
|
|||
参考老代码(淮南):`src/main/java/淮南/MineService.java`(已废弃,新代码基于其重写) |
|||
|
|||
## 技术栈 |
|||
|
|||
| 技术 | 版本 | |
|||
|-----|------| |
|||
| Spring Boot | 2.6.13 | |
|||
| MyBatis | 2.3.1 | |
|||
| 数据库 | 人大金仓 (KingbaseES) | |
|||
| 工具库 | Apache Commons Net (FTP) | |
|||
| JDK | 1.8+ | |
|||
|
|||
## 目录结构 |
|||
|
|||
``` |
|||
dataservice-yy/ |
|||
├── dataservice-yy-0.0.1-SNAPSHOT.jar # 主程序 JAR 包 |
|||
├── src/main/java/com/threecloud/dataserviceyy/ |
|||
│ ├── DataserviceYyApplication.java # 启动类 |
|||
│ ├── config/ |
|||
│ │ ├── DataSourceConfig.java # 数据源配置 |
|||
│ │ └── FtpSyncProperties.java # FTP配置 |
|||
│ ├── entity/ |
|||
│ │ └── MidVoiceCallRecord.java # 通话记录实体 |
|||
│ ├── mapper/ |
|||
│ │ └── MidVoiceCallRecordMapper.java # MyBatis Mapper |
|||
│ ├── service/ |
|||
│ │ └── FtpSyncService.java # 核心同步服务 |
|||
│ └── util/ |
|||
│ ├── FilePathUtil.java # 路径工具 |
|||
│ ├── FileUploadUtil.java # OSS上传工具 |
|||
│ ├── FtpUtil.java # FTP工具 |
|||
│ └── DateUtil.java # 日期工具 |
|||
├── src/main/resources/ |
|||
│ ├── application.yml # 内部配置 |
|||
│ ├── logback-spring.xml # 日志配置 |
|||
│ └── mapper/ |
|||
│ └── MidVoiceCallRecordMapper.xml # SQL映射 |
|||
├── config/ |
|||
│ └── application-external.yml # 外部配置文件(部署时修改) |
|||
├── vaa-recordings/ # 本地录音文件存储目录 |
|||
│ ├── 340100/ # 地市编码 |
|||
│ │ ├── 20240101/ # 日期目录 |
|||
│ │ │ └── <device_uuid>/ # 设备目录 |
|||
│ │ │ └── xxx.wav |
|||
│ │ └── 20240102/ |
|||
│ └── ... |
|||
├── logs/ |
|||
│ └── app.log # 日志文件 |
|||
└── README.md # 本说明文档 |
|||
│ └── application-external.yml # 外部配置(部署时修改) |
|||
├── sql/ |
|||
│ └── mid_voice_call_record.sql # 建表SQL |
|||
├── pom.xml # Maven配置 |
|||
└── README.md # 本文档 |
|||
``` |
|||
|
|||
## 部署步骤 |
|||
## 数据流程 |
|||
|
|||
### 1. 准备环境 |
|||
``` |
|||
┌──────────────┐ 1.连接 ┌──────────────┐ |
|||
│ 本服务 │◀───────────▶│ FTP服务器 │ |
|||
│ │ 2.下载txt │ (录音盒) │ |
|||
│ │ 3.下载mp3 │ │ |
|||
└──────┬───────┘ └──────────────┘ |
|||
│ |
|||
│ 4.上传 |
|||
▼ |
|||
┌──────────────┐ |
|||
│ OSS存储 │ http://oss地址/voice/合肥/340100/20240101/xxx.mp3 |
|||
└──────────────┘ |
|||
│ |
|||
│ 5.入库 |
|||
▼ |
|||
┌──────────────┐ |
|||
│ 人大金仓 │ mid_voice_call_record 表 |
|||
└──────────────┘ |
|||
``` |
|||
|
|||
- JDK 1.8+ |
|||
- 人大金仓数据库(已创建 mid_voice schema) |
|||
- OSS 服务(已部署并配置好上传接口) |
|||
## 数据库表 |
|||
|
|||
### mid_voice_call_record(通话记录表) |
|||
|
|||
```sql |
|||
CREATE TABLE "mid_voice"."mid_voice_call_record" ( |
|||
"id" int8 NOT NULL GENERATED ALWAYS AS IDENTITY, |
|||
"city_code" varchar(20), |
|||
"city_name" varchar(100), |
|||
"call_record_id" varchar(100) NOT NULL, -- 唯一标识,建议加唯一索引 |
|||
"call_tel" varchar(50) NOT NULL, -- 主叫 |
|||
"called_tel" varchar(200) NOT NULL, -- 被叫 |
|||
"call_start_time" timestamp(6) NOT NULL, |
|||
"call_end_time" timestamp(6) NOT NULL, |
|||
"call_duration" int4 NOT NULL, -- 秒 |
|||
"call_direction" varchar(50) NOT NULL, -- 1呼入/2呼出 |
|||
"device_no" varchar(100) NOT NULL, -- 设备编码(FTP_地市编码) |
|||
"business_scenario" varchar(100), |
|||
"recording_file_name" varchar(200) NOT NULL, |
|||
"recording_file_path" varchar(500) NOT NULL, -- OSS URL |
|||
"recording_file_size" int4, |
|||
"call_status" varchar(20), -- 1正常/2未接通 |
|||
"fail_reason" text, |
|||
"remarks" text, |
|||
"sync_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP, |
|||
"create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP, |
|||
"org_code" varchar(64), |
|||
PRIMARY KEY ("id") |
|||
) |
|||
PARTITION BY LIST ("city_code"); |
|||
``` |
|||
|
|||
### 2. 创建目录结构 |
|||
**建议加唯一索引**(防重最后一道防线): |
|||
|
|||
```bash |
|||
mkdir -p /opt/dataservice-yy/config |
|||
mkdir -p /opt/dataservice-yy/logs |
|||
```sql |
|||
CREATE UNIQUE INDEX idx_call_record_id_unique |
|||
ON mid_voice.mid_voice_call_record (call_record_id); |
|||
``` |
|||
|
|||
### 3. 复制文件 |
|||
## 部署说明 |
|||
|
|||
### 1. 准备JAR包 |
|||
|
|||
```bash |
|||
# 复制 JAR 包 |
|||
cp dataservice-yy-0.0.1-SNAPSHOT.jar /opt/dataservice-yy/ |
|||
cd /Users/wang/sanduoyun/developspace/dataservice-yy |
|||
mvn clean package -DskipTests |
|||
``` |
|||
|
|||
产物:`target/dataservice-yy-0.0.1-SNAPSHOT.jar` |
|||
|
|||
### 2. 部署目录结构 |
|||
|
|||
# 复制配置文件模板 |
|||
cp config/application-external.yml /opt/dataservice-yy/config/ |
|||
每台服务器(每地市): |
|||
|
|||
``` |
|||
/opt/dataservice-yy/ |
|||
├── dataservice-yy-0.0.1-SNAPSHOT.jar |
|||
├── config/ |
|||
│ └── application-external.yml # 修改此文件配置FTP |
|||
├── vaa-ftp-temp/ # 自动创建(临时文件) |
|||
└── logs/ # 自动创建(运行日志) |
|||
└── app.log |
|||
``` |
|||
|
|||
### 4. 修改配置 |
|||
### 3. 修改外部配置 |
|||
|
|||
编辑 `/opt/dataservice-yy/config/application-external.yml`,修改以下配置项: |
|||
编辑 `config/application-external.yml`: |
|||
|
|||
#### 数据库配置 |
|||
```yaml |
|||
spring: |
|||
datasource: |
|||
url: jdbc:kingbase8://{数据库IP}:{端口}/kingbase?currentSchema=mid_voice&clientEncoding=utf8 |
|||
username: {数据库用户名} |
|||
password: {数据库密码} |
|||
``` |
|||
url: jdbc:kingbase8://你的数据库IP:54321/kingbase?currentSchema=mid_voice |
|||
username: dcms_dev |
|||
password: 你的密码 |
|||
|
|||
ftp-sync: |
|||
cities: |
|||
- city-code: "340100" # 你的地市编码 |
|||
city-name: "合肥" |
|||
ftp-host: 10.126.129.7 # FTP服务器 |
|||
ftp-port: 9979 |
|||
ftp-username: ftpuser |
|||
ftp-password: ftppass |
|||
ftp-source-dir: /record/ # txt目录 |
|||
ftp-record-dir: /recordfile/ # mp3目录 |
|||
ftp-archive-dir: /processed/ # 归档目录 |
|||
|
|||
#### OSS 配置 |
|||
```yaml |
|||
vaa-sync: |
|||
oss: |
|||
base-url: http://{OSS服务器IP}:9090 |
|||
upload-url: http://{OSS服务器IP}:9090/apiOss/oss/fileUpload |
|||
base-url: http://你的OSS地址:9090 |
|||
upload-url: http://你的OSS地址:9090/apiOss/oss/noAuthFileUploadSingle |
|||
appcode: dataservice-yy |
|||
appid: {appid} |
|||
appsecret: {appsecret} |
|||
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. 启动服务 |
|||
### 4. 启动服务 |
|||
|
|||
```bash |
|||
cd /opt/dataservice-yy |
|||
nohup java -jar dataservice-yy-0.0.1-SNAPSHOT.jar > /dev/null 2>&1 & |
|||
``` |
|||
|
|||
### 6. 查看日志 |
|||
### 5. 验证启动 |
|||
|
|||
```bash |
|||
tail -f logs/app.log |
|||
``` |
|||
|
|||
## 配置文件说明 |
|||
看到如下日志说明启动成功: |
|||
``` |
|||
【项目启动】开始执行首次FTP录音同步 |
|||
【项目启动】首次同步完成,后续将在每2小时自动执行 |
|||
``` |
|||
|
|||
### application-external.yml |
|||
## 配置项说明 |
|||
|
|||
### 数据库配置(spring.datasource) |
|||
|
|||
| 配置项 | 必填 | 说明 | |
|||
|-------|-----|------| |
|||
| `url` | ✓ | JDBC URL(人大金仓:jdbc:kingbase8://IP:port/db) | |
|||
| `username` | ✓ | 数据库用户 | |
|||
| `password` | ✓ | 数据库密码 | |
|||
| `driver-class-name` | ✓ | com.kingbase8.Driver | |
|||
|
|||
### FTP配置(ftp-sync.cities[]) |
|||
|
|||
| 配置项 | 必填 | 说明 | |
|||
|-------|-----|------| |
|||
| `city-code` | ✓ | 地市编码(如340100),与数据库分区对应 | |
|||
| `city-name` | ✓ | 地市名称(用于OSS路径) | |
|||
| `ftp-host` | ✓ | FTP服务器IP | |
|||
| `ftp-port` | ✓ | FTP端口 | |
|||
| `ftp-username` | ✓ | FTP用户名 | |
|||
| `ftp-password` | ✓ | FTP密码 | |
|||
| `ftp-source-dir` | ✓ | txt源目录(处理后归档) | |
|||
| `ftp-record-dir` | ✓ | mp3/wav文件目录 | |
|||
| `ftp-archive-dir` | ✓ | 处理完的txt移动到此目录 | |
|||
|
|||
### FTP全局配置(ftp-sync.*) |
|||
|
|||
| 配置项 | 默认值 | 说明 | |
|||
|-------|-------|------| |
|||
| `enabled` | true | 是否启用FTP同步 | |
|||
| `sync-interval-cron` | 0 0 0/2 * * ? | 定时任务(每2小时) | |
|||
| `temp-path` | ./vaa-ftp-temp | 本地临时文件目录 | |
|||
| `retain-days` | 10 | 临时文件保留天数 | |
|||
| `txt-encoding` | GBK | txt文件编码(老系统用GBK) | |
|||
| `field-separator` | ※ | 字段分隔符(老系统用※) | |
|||
|
|||
### OSS配置(vaa-sync.oss) |
|||
|
|||
| 配置项 | 必填 | 说明 | |
|||
|-------|-----|------| |
|||
| `base-url` | ✓ | OSS访问基础URL | |
|||
| `upload-url` | ✓ | OSS上传接口URL | |
|||
| `appcode` | ✓ | 应用编码 | |
|||
| `appid` | ✓ | 应用ID | |
|||
| `appsecret` | ✓ | 应用密钥 | |
|||
|
|||
## txt文件格式 |
|||
|
|||
**编码**:GBK |
|||
**分隔符**:※ |
|||
**字段顺序**: |
|||
``` |
|||
kssj※jssj※hjls※hjzls※zjhm※bjhm※thsc※...※thfx |
|||
``` |
|||
|
|||
此文件包含所有需要部署时调整的参数,**无需重新打包 JAR**。 |
|||
| 序号 | 字段 | 说明 | 示例 | |
|||
|-----|------|------|------| |
|||
| 1 | kssj | 开始时间 | 20240101120000 | |
|||
| 2 | jssj | 结束时间 | 20240101120030 | |
|||
| 3 | hjls | 呼叫流水 | 12345 | |
|||
| 4 | hjzls | 呼叫主/被叫流水 | 67890 | |
|||
| 5 | zjhm | 主叫号码 | 055312345678 | |
|||
| 6 | bjhm | 被叫号码 | 12345678 | |
|||
| 7 | thsc | 通话时长(秒) | 30 | |
|||
| ... | ... | 中间字段 | ... | |
|||
| 10 | thfx | 通话方向 | 1=呼入/0=呼出 | |
|||
|
|||
| 配置项 | 说明 | 示例 | |
|||
|-------|------|------| |
|||
| `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` | |
|||
**录音文件**:`{ftpRecordDir}/{yyyyMMdd}/{hjls}_{hjzls}.mp3` |
|||
|
|||
## 定时任务说明 |
|||
## 防重复机制 |
|||
|
|||
同步任务默认每2小时执行一次,可通过修改 `sync-interval-cron` 调整: |
|||
| 层级 | 方式 | 说明 | |
|||
|-----|------|------| |
|||
| 1 | FTP归档 | 处理完的txt移到 `archive/` | |
|||
| 2 | 数据库前缀查重 | `call_record_id LIKE '340100_FTP_%'` | |
|||
| 3 | 内存Set去重 | 同批内不重复 | |
|||
| 4 | 数据库唯一索引 | 建议在 `call_record_id` 上加 | |
|||
|
|||
| 表达式 | 说明 | |
|||
|-------|------| |
|||
| `0 0 0/2 * * ?` | 每2小时执行一次(默认) | |
|||
| `0 0/30 * * * ?` | 每30分钟执行一次 | |
|||
| `0 0 2 * * ?` | 每天凌晨2点执行 | |
|||
| `0 0/1 * * * ?` | 每分钟执行(测试用) | |
|||
## 定时任务 |
|||
|
|||
## 数据表说明 |
|||
默认 `0 0 0/2 * * ?`(每2小时执行一次)。 |
|||
|
|||
### mid_voice_device_config |
|||
可在 `application-external.yml` 中修改: |
|||
|
|||
设备基础配置表,存储录音盒设备信息。 |
|||
```yaml |
|||
ftp-sync: |
|||
sync-interval-cron: "0 0 0/2 * * ?" |
|||
``` |
|||
|
|||
| 字段 | 说明 | |
|||
|-----|------| |
|||
| device_no | 设备编号(UUID) | |
|||
| city_code | 地市编码 | |
|||
| city_name | 地市名称 | |
|||
| ip_address | IP地址 | |
|||
| device_port | 端口号 | |
|||
| device_status | 设备状态(0在线) | |
|||
**常用 Cron 表达式**: |
|||
- 每分钟:`0 0/1 * * * ?` |
|||
- 每30分钟:`0 0/30 * * * ?` |
|||
- 每小时:`0 0 * * * ?` |
|||
- 每2小时:`0 0 0/2 * * ?` |
|||
- 每天凌晨2点:`0 0 2 * * ?` |
|||
|
|||
### mid_voice_channel_config |
|||
## 日志 |
|||
|
|||
通道配置表,存储录音盒通道绑定的电话号码。 |
|||
### 日志位置 |
|||
|
|||
| 字段 | 说明 | |
|||
|-----|------| |
|||
| device_no | 设备编号 | |
|||
| channel_no | 通道号(1-8) | |
|||
| phone_number | 绑定电话号码 | |
|||
`logs/app.log`(按天滚动,保留30天) |
|||
|
|||
### mid_voice_call_record |
|||
### 日志前缀 |
|||
|
|||
通话记录表,存储录音文件信息和OSS地址。 |
|||
| 前缀 | 含义 | |
|||
|------|------| |
|||
| 【FTP定时】 | 定时任务开始/结束 | |
|||
| 【FTP主流程】 | 同步主流程 | |
|||
| 【FTP地市】 | 单地市处理 | |
|||
| 【FTP文件】 | 单txt文件处理 | |
|||
| 【FTP行】 | 单行记录处理 | |
|||
| 【FTP防重】 | 数据库防重查询 | |
|||
| 【异常】 | 错误信息 | |
|||
|
|||
| 字段 | 说明 | |
|||
|-----|------| |
|||
| 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 "【异常】" |
|||
# 只看错误 |
|||
grep "【异常】" logs/app.log |
|||
|
|||
# 只看设备同步日志 |
|||
tail -f logs/app.log | grep "【设备】" |
|||
# 只看单地市 |
|||
grep "【FTP地市】" logs/app.log | grep "合肥" |
|||
|
|||
# 只看录音处理日志 |
|||
tail -f logs/app.log | grep "【录音】" |
|||
# 同步成功数 |
|||
grep "保存成功" logs/app.log | wc -l |
|||
``` |
|||
|
|||
## 常见问题 |
|||
|
|||
### 1. 配置文件读取失败 |
|||
### Q1: 数据库连接失败 |
|||
|
|||
检查 `application-external.yml` 的 `spring.datasource.*` 配置,确保: |
|||
- IP和端口正确 |
|||
- 用户名密码正确 |
|||
- 数据库服务已启动 |
|||
|
|||
### Q2: FTP连接失败 |
|||
|
|||
检查 `ftp-sync.cities[0].ftp-*` 配置: |
|||
- FTP服务可达:`telnet 10.126.129.7 9979` |
|||
- 用户名密码正确 |
|||
- 目录路径有读写权限 |
|||
|
|||
### Q3: 录音文件下载失败 |
|||
|
|||
检查 FTP 上是否存在对应路径的文件: |
|||
- 路径格式:`{ftp-record-dir}/{yyyyMMdd}/{hjls}_{hjzls}.mp3` |
|||
- 文件名大小写是否一致 |
|||
|
|||
### Q4: OSS上传失败 |
|||
|
|||
检查 `vaa-sync.oss.*` 配置: |
|||
- `upload-url` 接口是否可达 |
|||
- `appid` / `appsecret` 是否正确 |
|||
|
|||
检查 `config/application-external.yml` 是否在 JAR 包同级目录的 `config/` 文件夹下。 |
|||
### Q5: 数据没有入库 |
|||
|
|||
### 2. 数据库连接失败 |
|||
检查: |
|||
- 数据库 `mid_voice_call_record` 表是否存在 |
|||
- 日志中是否有 `保存成功` 字样 |
|||
- 数据库表分区是否覆盖了该地市编码 |
|||
|
|||
检查数据库URL、用户名、密码是否正确,网络是否连通。 |
|||
## 与老代码(淮南 yydc-tb-server)的差异 |
|||
|
|||
### 3. OSS上传失败 |
|||
| 项 | 老代码 | 新代码 | |
|||
|---|-------|-------| |
|||
| 数据源 | Oracle | 人大金仓 | |
|||
| 配置 | Spring XML | application.yml | |
|||
| 同步方式 | 手动运行 | 定时任务 | |
|||
| mp3处理 | 只存URL | 下载+上传OSS | |
|||
| 防重 | 不防重 | 数据库+FTP双层防重 | |
|||
| 部署 | 单机单实例 | 多地市独立部署 | |
|||
| 状态 | 已废弃 | 当前生产 | |
|||
|
|||
检查 OSS 地址、认证信息是否正确,OSS服务是否正常。 |
|||
## 维护 |
|||
|
|||
### 4. 录音盒连接失败 |
|||
| 项 | 频率 | |
|||
|---|------| |
|||
| 检查日志 | 每天 | |
|||
| 清理磁盘 | 自动(保留10天) | |
|||
| 数据库备份 | 由DBA负责 | |
|||
| 配置文件变更 | 部署时修改外部配置 | |
|||
|
|||
检查录音盒IP、端口、账号密码是否正确,网络是否连通。 |
|||
## 联系 |
|||
|
|||
- 项目维护:xxx |
|||
- 内部文档:xxx |
|||
|
|||
@ -1,76 +0,0 @@ |
|||
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,54 @@ |
|||
-- ============================================ |
|||
-- FTP已处理文件记录表(普通表) |
|||
-- 用于记录已处理过的FTP txt文件,避免重复处理 |
|||
-- ============================================ |
|||
|
|||
CREATE TABLE "mid_voice"."mid_ftp_processed_file" ( |
|||
"id" int8 NOT NULL GENERATED ALWAYS AS IDENTITY ( |
|||
INCREMENT 1 |
|||
MINVALUE 1 |
|||
MAXVALUE 9223372036854775807 |
|||
START 1 |
|||
CACHE 1 |
|||
), |
|||
"city_code" varchar(20) COLLATE "pg_catalog"."default" NOT NULL, |
|||
"city_name" varchar(100) COLLATE "pg_catalog"."default", |
|||
"file_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, |
|||
"file_size" int8, |
|||
"process_status" varchar(10) COLLATE "pg_catalog"."default" NOT NULL DEFAULT '1', |
|||
"record_count" int4 DEFAULT 0, |
|||
"success_count" int4 DEFAULT 0, |
|||
"fail_count" int4 DEFAULT 0, |
|||
"process_time" int8 DEFAULT 0, |
|||
"error_msg" text COLLATE "pg_catalog"."default", |
|||
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
|||
CONSTRAINT "mid_ftp_processed_file_pkey" PRIMARY KEY ("id"), |
|||
CONSTRAINT "uk_ftp_file_city_name" UNIQUE ("city_code", "file_name") |
|||
); |
|||
|
|||
ALTER TABLE "mid_voice"."mid_ftp_processed_file" |
|||
OWNER TO "dcms_dev"; |
|||
|
|||
-- 创建索引 |
|||
CREATE INDEX "idx_ftp_processed_file_name" ON "mid_voice"."mid_ftp_processed_file" USING btree ( |
|||
"file_name" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST |
|||
); |
|||
|
|||
CREATE INDEX "idx_ftp_processed_create_time" ON "mid_voice"."mid_ftp_processed_file" USING btree ( |
|||
"create_time" "pg_catalog"."timestamp_ops" ASC NULLS LAST |
|||
); |
|||
|
|||
-- 添加注释 |
|||
COMMENT ON TABLE "mid_voice"."mid_ftp_processed_file" IS 'FTP已处理文件记录表'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."id" IS '自增ID主键'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."city_code" IS '地市编码'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."city_name" IS '地市名称'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."file_name" IS 'FTP文件名'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."file_size" IS '文件大小(字节)'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."process_status" IS '处理状态:0失败,1成功'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."record_count" IS '记录总数'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."success_count" IS '成功入库数'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."fail_count" IS '失败数'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."process_time" IS '处理耗时(毫秒)'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."error_msg" IS '错误信息'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_ftp_processed_file"."create_time" IS '创建时间'; |
|||
@ -0,0 +1,77 @@ |
|||
-- ============================================ |
|||
-- 设备连接日志表 |
|||
-- 用于记录设备连接失败信息,便于排查问题 |
|||
-- ============================================ |
|||
|
|||
CREATE TABLE "mid_voice"."mid_voice_device_log" ( |
|||
"id" int8 NOT NULL GENERATED ALWAYS AS IDENTITY ( |
|||
INCREMENT 1 |
|||
MINVALUE 1 |
|||
MAXVALUE 9223372036854775807 |
|||
START 1 |
|||
CACHE 1 |
|||
), |
|||
"device_id" varchar(50) COLLATE "pg_catalog"."default" NOT NULL, |
|||
"device_no" varchar(100) COLLATE "pg_catalog"."default", |
|||
"city_code" varchar(20) COLLATE "pg_catalog"."default", |
|||
"city_name" varchar(100) COLLATE "pg_catalog"."default", |
|||
"ip_address" varchar(50) COLLATE "pg_catalog"."default", |
|||
"device_port" int4, |
|||
"connect_type" varchar(10) COLLATE "pg_catalog"."default" NOT NULL, |
|||
"connect_status" varchar(10) COLLATE "pg_catalog"."default" NOT NULL, |
|||
"fail_reason" text COLLATE "pg_catalog"."default", |
|||
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
|||
CONSTRAINT "mid_voice_device_log_pkey" PRIMARY KEY ("id") |
|||
) |
|||
PARTITION BY LIST ( |
|||
"city_code" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" |
|||
); |
|||
|
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" |
|||
OWNER TO "dcms_dev"; |
|||
|
|||
-- 创建索引 |
|||
CREATE INDEX "idx_device_log_device_id" ON "mid_voice"."mid_voice_device_log" USING btree ( |
|||
"device_id" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST |
|||
); |
|||
|
|||
CREATE INDEX "idx_device_log_create_time" ON "mid_voice"."mid_voice_device_log" USING btree ( |
|||
"create_time" "pg_catalog"."timestamp_ops" ASC NULLS LAST |
|||
); |
|||
|
|||
CREATE INDEX "idx_device_log_status" ON "mid_voice"."mid_voice_device_log" USING btree ( |
|||
"connect_status" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST |
|||
); |
|||
|
|||
-- 创建分区表(16个地市) |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3401" FOR VALUES IN ('340100'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3402" FOR VALUES IN ('340200'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3403" FOR VALUES IN ('340300'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3404" FOR VALUES IN ('340400'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3405" FOR VALUES IN ('340500'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3406" FOR VALUES IN ('340600'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3407" FOR VALUES IN ('340700'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3408" FOR VALUES IN ('340800'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3410" FOR VALUES IN ('341000'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3411" FOR VALUES IN ('341100'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3412" FOR VALUES IN ('341200'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3413" FOR VALUES IN ('341300'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3415" FOR VALUES IN ('341500'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3416" FOR VALUES IN ('341600'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3417" FOR VALUES IN ('341700'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_3418" FOR VALUES IN ('341800'); |
|||
ALTER TABLE "mid_voice"."mid_voice_device_log" ATTACH PARTITION "mid_voice"."mid_voice_device_log_default" DEFAULT; |
|||
|
|||
-- 添加注释 |
|||
COMMENT ON TABLE "mid_voice"."mid_voice_device_log" IS '设备连接日志表'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."id" IS '自增ID主键'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."device_id" IS '设备ID'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."device_no" IS '设备编号(UUID)'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."city_code" IS '地市编码'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."city_name" IS '地市名称'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."ip_address" IS 'IP地址'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."device_port" IS '端口号'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."connect_type" IS '连接类型:1登录,2查询录音列表,3下载文件'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."connect_status" IS '连接状态:0失败,1成功'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."fail_reason" IS '失败原因'; |
|||
COMMENT ON COLUMN "mid_voice"."mid_voice_device_log"."create_time" IS '创建时间'; |
|||
@ -1,12 +0,0 @@ |
|||
package com.threecloud.dataserviceyy.config; |
|||
|
|||
/** |
|||
* 多数据源配置 - 【方案A阶段: 已完全禁用】 |
|||
* |
|||
* 此类在方案B(多数据源模式)时会重新启用。 |
|||
* 当前阶段使用Spring Boot默认的DataSource自动配置,仿照老系统 yydc-tb-server。 |
|||
*/ |
|||
// @Configuration // 【方案A: 已禁用】
|
|||
public class DynamicDataSourceConfig { |
|||
// 所有方法已禁用,不再干扰Spring Boot的自动配置
|
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
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,129 @@ |
|||
package com.threecloud.dataserviceyy.config; |
|||
|
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* FTP同步配置 |
|||
* 对应 application-external.yml 中的 ftp-sync 配置节点 |
|||
* |
|||
* 示例: |
|||
* ftp-sync: |
|||
* enabled: true |
|||
* sync-interval-cron: "0 0 0/2 * * ?" |
|||
* cities: |
|||
* - city-code: "340100" |
|||
* city-name: "合肥" |
|||
* ftp-host: 10.126.129.7 |
|||
* ... |
|||
*/ |
|||
@Configuration |
|||
@ConfigurationProperties(prefix = "ftp-sync") |
|||
public class FtpSyncProperties { |
|||
|
|||
/** 是否启用FTP同步 */ |
|||
private boolean enabled = true; |
|||
|
|||
/** 本地临时文件目录(存放下载的mp3/wav) */ |
|||
private String tempPath = "./vaa-ftp-temp"; |
|||
|
|||
/** 文件保留天数 */ |
|||
private int retainDays = 10; |
|||
|
|||
/** txt文件编码(老系统用GBK) */ |
|||
private String txtEncoding = "GBK"; |
|||
|
|||
/** 字段分隔符(老系统用※) */ |
|||
private String fieldSeparator = "※"; |
|||
|
|||
/** OSS服务基础地址(用于mp3直接URL拼接) */ |
|||
private String ossBaseUrl; |
|||
|
|||
/** 地市FTP配置列表 */ |
|||
private List<FtpCityConfig> cities = new ArrayList<>(); |
|||
|
|||
public boolean isEnabled() { return enabled; } |
|||
public void setEnabled(boolean enabled) { this.enabled = enabled; } |
|||
|
|||
public String getTempPath() { return tempPath; } |
|||
public void setTempPath(String tempPath) { this.tempPath = tempPath; } |
|||
|
|||
public int getRetainDays() { return retainDays; } |
|||
public void setRetainDays(int retainDays) { this.retainDays = retainDays; } |
|||
|
|||
public String getTxtEncoding() { return txtEncoding; } |
|||
public void setTxtEncoding(String txtEncoding) { this.txtEncoding = txtEncoding; } |
|||
|
|||
public String getFieldSeparator() { return fieldSeparator; } |
|||
public void setFieldSeparator(String fieldSeparator) { this.fieldSeparator = fieldSeparator; } |
|||
|
|||
public String getOssBaseUrl() { return ossBaseUrl; } |
|||
public void setOssBaseUrl(String ossBaseUrl) { this.ossBaseUrl = ossBaseUrl; } |
|||
|
|||
public List<FtpCityConfig> getCities() { return cities; } |
|||
public void setCities(List<FtpCityConfig> cities) { this.cities = cities; } |
|||
|
|||
/** |
|||
* 单个地市的FTP配置 |
|||
*/ |
|||
public static class FtpCityConfig { |
|||
/** 地市编码(与mid_voice_device_config.city_code对应) */ |
|||
private String cityCode; |
|||
/** 地市名称 */ |
|||
private String cityName; |
|||
/** FTP服务器IP */ |
|||
private String ftpHost; |
|||
/** FTP服务器端口 */ |
|||
private int ftpPort = 21; |
|||
/** FTP用户名 */ |
|||
private String ftpUsername; |
|||
/** FTP密码 */ |
|||
private String ftpPassword; |
|||
/** 录音数据源目录(txt文件目录) */ |
|||
private String ftpSourceDir; |
|||
/** 录音文件存储目录(mp3/wav文件目录) */ |
|||
private String ftpRecordDir; |
|||
/** 归档目录(处理完的txt移到这里) */ |
|||
private String ftpArchiveDir; |
|||
/** |
|||
* 电话区号前缀(处理主被叫号码时去除) |
|||
* - 配正确区号(如合肥 0551):号码以 0551 开头则去除 |
|||
* - 配不存在的区号(如 "0000"):号码匹配不上,保留原始号码 |
|||
* - 留空:保留原始号码 |
|||
*/ |
|||
private String phoneAreaCode; |
|||
|
|||
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 getFtpHost() { return ftpHost; } |
|||
public void setFtpHost(String ftpHost) { this.ftpHost = ftpHost; } |
|||
|
|||
public int getFtpPort() { return ftpPort; } |
|||
public void setFtpPort(int ftpPort) { this.ftpPort = ftpPort; } |
|||
|
|||
public String getFtpUsername() { return ftpUsername; } |
|||
public void setFtpUsername(String ftpUsername) { this.ftpUsername = ftpUsername; } |
|||
|
|||
public String getFtpPassword() { return ftpPassword; } |
|||
public void setFtpPassword(String ftpPassword) { this.ftpPassword = ftpPassword; } |
|||
|
|||
public String getFtpSourceDir() { return ftpSourceDir; } |
|||
public void setFtpSourceDir(String ftpSourceDir) { this.ftpSourceDir = ftpSourceDir; } |
|||
|
|||
public String getFtpRecordDir() { return ftpRecordDir; } |
|||
public void setFtpRecordDir(String ftpRecordDir) { this.ftpRecordDir = ftpRecordDir; } |
|||
|
|||
public String getFtpArchiveDir() { return ftpArchiveDir; } |
|||
public void setFtpArchiveDir(String ftpArchiveDir) { this.ftpArchiveDir = ftpArchiveDir; } |
|||
|
|||
public String getPhoneAreaCode() { return phoneAreaCode; } |
|||
public void setPhoneAreaCode(String phoneAreaCode) { this.phoneAreaCode = phoneAreaCode; } |
|||
} |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
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; |
|||
} |
|||
@ -1,270 +0,0 @@ |
|||
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); |
|||
} |
|||
} |
|||
@ -1,157 +0,0 @@ |
|||
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 + '\'' + |
|||
'}'; |
|||
} |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
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,84 @@ |
|||
package com.threecloud.dataserviceyy.entity; |
|||
|
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* FTP已处理文件记录表 |
|||
* 对应 mid_voice.mid_ftp_processed_file |
|||
* 用于记录已处理过的FTP txt文件,避免重复处理 |
|||
*/ |
|||
public class FtpProcessedFile { |
|||
|
|||
/** 自增ID主键 */ |
|||
private Long id; |
|||
|
|||
/** 地市编码 */ |
|||
private String cityCode; |
|||
|
|||
/** 地市名称 */ |
|||
private String cityName; |
|||
|
|||
/** FTP文件名 */ |
|||
private String fileName; |
|||
|
|||
/** 文件大小(字节) */ |
|||
private Long fileSize; |
|||
|
|||
/** 处理状态:0失败,1成功 */ |
|||
private String processStatus; |
|||
|
|||
/** 记录总数 */ |
|||
private Integer recordCount; |
|||
|
|||
/** 成功入库数 */ |
|||
private Integer successCount; |
|||
|
|||
/** 失败数 */ |
|||
private Integer failCount; |
|||
|
|||
/** 处理耗时(毫秒) */ |
|||
private Long processTime; |
|||
|
|||
/** 错误信息 */ |
|||
private String errorMsg; |
|||
|
|||
/** 创建时间 */ |
|||
private Date createTime; |
|||
|
|||
// 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 getFileName() { return fileName; } |
|||
public void setFileName(String fileName) { this.fileName = fileName; } |
|||
|
|||
public Long getFileSize() { return fileSize; } |
|||
public void setFileSize(Long fileSize) { this.fileSize = fileSize; } |
|||
|
|||
public String getProcessStatus() { return processStatus; } |
|||
public void setProcessStatus(String processStatus) { this.processStatus = processStatus; } |
|||
|
|||
public Integer getRecordCount() { return recordCount; } |
|||
public void setRecordCount(Integer recordCount) { this.recordCount = recordCount; } |
|||
|
|||
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 Long getProcessTime() { return processTime; } |
|||
public void setProcessTime(Long processTime) { this.processTime = processTime; } |
|||
|
|||
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; } |
|||
} |
|||
@ -1,79 +0,0 @@ |
|||
package com.threecloud.dataserviceyy.entity; |
|||
|
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 设备连接日志表 |
|||
* 对应 mid_voice.mid_voice_device_log |
|||
* |
|||
* 用于记录设备连接失败信息,便于排查问题 |
|||
*/ |
|||
public class MidVoiceDeviceLog { |
|||
|
|||
/** 自增ID主键 */ |
|||
private Long id; |
|||
|
|||
/** 设备ID */ |
|||
private String deviceId; |
|||
|
|||
/** 设备编号 */ |
|||
private String deviceNo; |
|||
|
|||
/** 地市编码 */ |
|||
private String cityCode; |
|||
|
|||
/** 地市名称 */ |
|||
private String cityName; |
|||
|
|||
/** IP地址 */ |
|||
private String ipAddress; |
|||
|
|||
/** 端口号 */ |
|||
private Integer devicePort; |
|||
|
|||
/** 连接类型:1登录,2查询录音列表,3下载文件 */ |
|||
private String connectType; |
|||
|
|||
/** 连接状态:0失败,1成功 */ |
|||
private String connectStatus; |
|||
|
|||
/** 失败原因 */ |
|||
private String failReason; |
|||
|
|||
/** 创建时间 */ |
|||
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 getDeviceNo() { return deviceNo; } |
|||
public void setDeviceNo(String deviceNo) { this.deviceNo = deviceNo; } |
|||
|
|||
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 getIpAddress() { return ipAddress; } |
|||
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } |
|||
|
|||
public Integer getDevicePort() { return devicePort; } |
|||
public void setDevicePort(Integer devicePort) { this.devicePort = devicePort; } |
|||
|
|||
public String getConnectType() { return connectType; } |
|||
public void setConnectType(String connectType) { this.connectType = connectType; } |
|||
|
|||
public String getConnectStatus() { return connectStatus; } |
|||
public void setConnectStatus(String connectStatus) { this.connectStatus = connectStatus; } |
|||
|
|||
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; } |
|||
} |
|||
@ -1,77 +0,0 @@ |
|||
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; } |
|||
} |
|||
@ -1,125 +0,0 @@ |
|||
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; } |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
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,48 @@ |
|||
package com.threecloud.dataserviceyy.mapper; |
|||
|
|||
import com.threecloud.dataserviceyy.entity.FtpProcessedFile; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.apache.ibatis.annotations.Param; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* FTP已处理文件记录Mapper |
|||
* 用于记录已处理过的FTP txt文件,避免重复处理 |
|||
*/ |
|||
@Mapper |
|||
public interface FtpProcessedFileMapper { |
|||
|
|||
/** |
|||
* 插入处理记录 |
|||
*/ |
|||
int insert(FtpProcessedFile record); |
|||
|
|||
/** |
|||
* 根据地市编码和文件名查询(判断是否已处理) |
|||
*/ |
|||
FtpProcessedFile selectByCityAndFileName(@Param("cityCode") String cityCode, |
|||
@Param("fileName") String fileName); |
|||
|
|||
/** |
|||
* 查询指定地市下所有已处理的文件名列表 |
|||
*/ |
|||
List<String> selectProcessedFileNamesByCity(@Param("cityCode") String cityCode); |
|||
|
|||
/** |
|||
* 批量查询指定地市下多个文件的处理状态 |
|||
*/ |
|||
List<FtpProcessedFile> selectByCityAndFileNames(@Param("cityCode") String cityCode, |
|||
@Param("fileNames") List<String> fileNames); |
|||
|
|||
/** |
|||
* 根据ID更新处理结果 |
|||
*/ |
|||
int updateById(FtpProcessedFile record); |
|||
|
|||
/** |
|||
* 删除指定时间之前的记录(清理历史) |
|||
*/ |
|||
int deleteBeforeTime(@Param("cityCode") String cityCode, |
|||
@Param("beforeTime") String beforeTime); |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
package com.threecloud.dataserviceyy.mapper; |
|||
|
|||
import com.threecloud.dataserviceyy.entity.MidVoiceDeviceLog; |
|||
import org.apache.ibatis.annotations.Insert; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.apache.ibatis.annotations.Param; |
|||
import org.apache.ibatis.annotations.Select; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 设备连接日志 Mapper |
|||
*/ |
|||
@Mapper |
|||
public interface MidVoiceDeviceLogMapper { |
|||
|
|||
/** |
|||
* 插入设备连接日志 |
|||
*/ |
|||
@Insert("INSERT INTO mid_voice_device_log (" + |
|||
"device_id, device_no, city_code, city_name, ip_address, device_port, " + |
|||
"connect_type, connect_status, fail_reason, create_time" + |
|||
") VALUES (" + |
|||
"#{deviceId}, #{deviceNo}, #{cityCode}, #{cityName}, #{ipAddress}, #{devicePort}, " + |
|||
"#{connectType}, #{connectStatus}, #{failReason}, #{createTime}" + |
|||
")") |
|||
int insert(MidVoiceDeviceLog log); |
|||
|
|||
/** |
|||
* 查询设备的最近连接日志 |
|||
*/ |
|||
@Select("SELECT * FROM mid_voice_device_log " + |
|||
"WHERE device_id = #{deviceId} " + |
|||
"ORDER BY create_time DESC " + |
|||
"LIMIT #{limit}") |
|||
List<MidVoiceDeviceLog> selectRecentByDeviceId(@Param("deviceId") String deviceId, |
|||
@Param("limit") int limit); |
|||
|
|||
/** |
|||
* 查询失败的连接记录 |
|||
*/ |
|||
@Select("SELECT * FROM mid_voice_device_log " + |
|||
"WHERE connect_status = '0' " + |
|||
"AND create_time >= #{startTime} " + |
|||
"ORDER BY create_time DESC") |
|||
List<MidVoiceDeviceLog> selectFailedLogs(@Param("startTime") String startTime); |
|||
} |
|||
@ -1,55 +0,0 @@ |
|||
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); |
|||
} |
|||
@ -1,67 +0,0 @@ |
|||
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); |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
package com.threecloud.dataserviceyy.mapper; |
|||
|
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 语音设备 Mapper |
|||
* 查询 mid_voice_device_config 表 |
|||
*/ |
|||
@Mapper |
|||
public interface VoiceSyncMapper { |
|||
|
|||
/** |
|||
* 查询所有在线语音设备(含每个设备的账号密码) |
|||
*/ |
|||
List<Map<String, Object>> getAllYysb(); |
|||
} |
|||
@ -0,0 +1,512 @@ |
|||
package com.threecloud.dataserviceyy.service; |
|||
|
|||
import com.threecloud.dataserviceyy.config.FtpSyncProperties; |
|||
import com.threecloud.dataserviceyy.config.FtpSyncProperties.FtpCityConfig; |
|||
import com.threecloud.dataserviceyy.entity.FtpProcessedFile; |
|||
import com.threecloud.dataserviceyy.entity.MidVoiceCallRecord; |
|||
import com.threecloud.dataserviceyy.mapper.FtpProcessedFileMapper; |
|||
import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper; |
|||
import com.threecloud.dataserviceyy.util.DateUtil; |
|||
import com.threecloud.dataserviceyy.util.FileCleaner; |
|||
import com.threecloud.dataserviceyy.util.FilePathUtil; |
|||
import com.threecloud.dataserviceyy.util.FileUploadUtil; |
|||
import com.threecloud.dataserviceyy.util.FtpUtil; |
|||
import org.apache.commons.net.ftp.FTPClient; |
|||
import org.apache.commons.net.ftp.FTPFile; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import java.io.BufferedReader; |
|||
import java.io.ByteArrayInputStream; |
|||
import java.io.InputStreamReader; |
|||
import java.util.*; |
|||
|
|||
/** |
|||
* FTP 录音同步服务(参考淮南 yydc-tb-server 的 MineService 重写) |
|||
* |
|||
* 【功能】从FTP读取录音数据txt → 解析 → 下载录音文件 → 上传OSS → 入库 |
|||
* 【策略】增量同步、无重试、数据库批量防重、txt处理完归档 |
|||
* 【部署】每个地市单独部署一份,每个服务只处理一个地市的FTP |
|||
* |
|||
* txt文件格式(GBK编码,※分隔): |
|||
* kssj※jssj※hjls※hjzls※zjhm※bjhm※thsc※...※thfx |
|||
* |
|||
* kssj : 开始时间 yyyyMMddHHmmss |
|||
* jssj : 结束时间 yyyyMMddHHmmss |
|||
* hjls : 呼叫流水(录音文件主名) |
|||
* hjzls : 呼叫主/被叫流水 |
|||
* zjhm : 主叫号码 |
|||
* bjhm : 被叫号码 |
|||
* thsc : 通话时长(秒) |
|||
* thfx : 通话方向 1=呼入 0=呼出 |
|||
* |
|||
* 录音文件命名:{hjls}_{hjzls}.mp3 |
|||
* 录音文件路径:{ftpRecordDir}/{yyyyMMdd}/{hjls}_{hjzls}.mp3 |
|||
*/ |
|||
@Service |
|||
public class FtpSyncService { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(FtpSyncService.class); |
|||
|
|||
/** 通话状态:1=正常接通 */ |
|||
private static final String CALL_STATUS_OK = "1"; |
|||
/** 通话方向:1=呼入,2=呼出(数据库规范) */ |
|||
private static final String DIR_INCOMING = "1"; |
|||
private static final String DIR_OUTGOING = "2"; |
|||
/** 字段数最小阈值 */ |
|||
private static final int MIN_FIELD_COUNT = 10; |
|||
|
|||
@Autowired |
|||
private FtpSyncProperties properties; |
|||
@Autowired |
|||
private MidVoiceCallRecordMapper callRecordMapper; |
|||
@Autowired |
|||
private FtpProcessedFileMapper processedFileMapper; |
|||
@Autowired |
|||
private FileUploadUtil fileUploadUtil; |
|||
|
|||
/** |
|||
* 定时同步任务 |
|||
*/ |
|||
@Scheduled(cron = "${ftp-sync.sync-interval-cron:0 0 0/2 * * ?}") |
|||
public void scheduledSync() { |
|||
logger.info("【FTP定时】========== FTP录音同步开始 =========="); |
|||
long startTime = System.currentTimeMillis(); |
|||
try { |
|||
executeSync(); |
|||
} catch (Exception e) { |
|||
logger.error("【FTP定时】同步任务异常: {}", e.getMessage(), e); |
|||
} |
|||
long costTime = System.currentTimeMillis() - startTime; |
|||
logger.info("【FTP定时】========== FTP录音同步结束,耗时 {} 秒 ==========", costTime / 1000); |
|||
} |
|||
|
|||
/** |
|||
* 执行同步(主入口) |
|||
*/ |
|||
public void executeSync() { |
|||
// 清理过期临时文件
|
|||
FileCleaner.clean(properties.getTempPath(), properties.getRetainDays()); |
|||
|
|||
List<FtpCityConfig> cities = properties.getCities(); |
|||
if (cities == null || cities.isEmpty()) { |
|||
logger.warn("【FTP主流程】未配置任何地市FTP,跳过同步"); |
|||
return; |
|||
} |
|||
logger.info("【FTP主流程】配置了 {} 个地市", cities.size()); |
|||
|
|||
for (FtpCityConfig city : cities) { |
|||
try { |
|||
syncCity(city); |
|||
} catch (Exception e) { |
|||
logger.error("【FTP主流程】地市 {} 同步失败: {}", city.getCityName(), e.getMessage(), e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 同步单个地市 |
|||
*/ |
|||
private void syncCity(FtpCityConfig city) throws Exception { |
|||
logger.info("【FTP地市】═══════════════════════════════════════"); |
|||
logger.info("【FTP地市】开始同步: {}({}), FTP={}:{}", |
|||
city.getCityName(), city.getCityCode(), city.getFtpHost(), city.getFtpPort()); |
|||
|
|||
// 参数校验
|
|||
if (!StringUtils.hasText(city.getFtpHost()) || !StringUtils.hasText(city.getFtpUsername())) { |
|||
logger.warn("【FTP地市】{} FTP未配置完整,跳过", city.getCityName()); |
|||
return; |
|||
} |
|||
|
|||
// 批量查询数据库已存在的call_record_id(防重)
|
|||
String prefix = city.getCityCode() + "_FTP_"; |
|||
Set<String> existingIds = loadExistingCallRecordIds(prefix); |
|||
|
|||
FTPClient ftp = null; |
|||
try { |
|||
ftp = FtpUtil.connect(city.getFtpHost(), city.getFtpPort(), |
|||
city.getFtpUsername(), city.getFtpPassword()); |
|||
logger.info("【FTP地市】FTP连接成功"); |
|||
|
|||
FTPFile[] files = FtpUtil.listFiles(ftp, city.getFtpSourceDir()); |
|||
int txtCount = FtpUtil.countTxtFiles(files); |
|||
logger.info("【FTP地市】目录 {} 下有 {} 个txt文件", city.getFtpSourceDir(), txtCount); |
|||
|
|||
// 批量查询已处理的文件名,避免重复处理
|
|||
Set<String> processedFiles = loadProcessedFileNames(city.getCityCode()); |
|||
logger.info("【FTP地市】已处理文件记录: {} 个", processedFiles.size()); |
|||
|
|||
int fileSuccess = 0; |
|||
int fileFail = 0; |
|||
int fileSkipped = 0; |
|||
|
|||
for (FTPFile file : files) { |
|||
if (!FtpUtil.isTxtFile(file)) { |
|||
continue; |
|||
} |
|||
String fileName = file.getName(); |
|||
// 检查是否已处理过
|
|||
if (processedFiles.contains(fileName)) { |
|||
logger.debug("【FTP文件】已处理过,跳过: {}", fileName); |
|||
fileSkipped++; |
|||
continue; |
|||
} |
|||
try { |
|||
if (processTxtFile(ftp, file, city, existingIds)) { |
|||
fileSuccess++; |
|||
} else { |
|||
fileFail++; |
|||
} |
|||
} catch (Exception e) { |
|||
fileFail++; |
|||
logger.error("【FTP地市】处理文件失败: {}, 原因={}", fileName, e.getMessage()); |
|||
} |
|||
} |
|||
logger.info("【FTP地市】{} 同步完成: 成功{}个文件, 跳过{}个(已处理), 失败{}个文件", |
|||
city.getCityName(), fileSuccess, fileSkipped, fileFail); |
|||
} finally { |
|||
FtpUtil.disconnect(ftp); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理单个txt文件 |
|||
* |
|||
* @return true=成功,false=失败 |
|||
*/ |
|||
private boolean processTxtFile(FTPClient ftp, FTPFile file, FtpCityConfig city, |
|||
Set<String> existingIds) throws Exception { |
|||
String fileName = file.getName(); |
|||
String sourcePath = joinPath(city.getFtpSourceDir(), fileName); |
|||
long fileSize = file.getSize(); |
|||
long fileStartTime = System.currentTimeMillis(); |
|||
logger.info("【FTP文件】开始处理: {} ({} 字节)", fileName, fileSize); |
|||
|
|||
byte[] txtBytes = FtpUtil.downloadFile(ftp, sourcePath); |
|||
int lineCount = 0; |
|||
int successCount = 0; |
|||
int skipCount = 0; |
|||
int failCount = 0; |
|||
String errorMsg = null; |
|||
|
|||
try (BufferedReader reader = new BufferedReader( |
|||
new InputStreamReader(new ByteArrayInputStream(txtBytes), properties.getTxtEncoding()))) { |
|||
String line; |
|||
while ((line = reader.readLine()) != null) { |
|||
if (line.indexOf(properties.getFieldSeparator()) <= 0) { |
|||
continue; |
|||
} |
|||
lineCount++; |
|||
try { |
|||
if (processLine(line, city, existingIds, ftp)) { |
|||
successCount++; |
|||
} else { |
|||
skipCount++; |
|||
} |
|||
} catch (Exception e) { |
|||
failCount++; |
|||
errorMsg = e.getMessage(); |
|||
logger.error("【FTP文件】解析单行失败: {}, 原因={}", line, e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
long processTime = System.currentTimeMillis() - fileStartTime; |
|||
logger.info("【FTP文件】{} 解析完成: 共{}行, 新增{}条, 跳过{}条, 失败{}条, 耗时{}ms", |
|||
fileName, lineCount, successCount, skipCount, failCount, processTime); |
|||
|
|||
// 记录已处理的文件(避免下次重复处理)
|
|||
recordProcessedFile(city, fileName, fileSize, lineCount, successCount, failCount, processTime, errorMsg); |
|||
|
|||
// 注意:不进行归档操作,文件保留在原位置
|
|||
// 如需归档,请取消下面代码的注释
|
|||
// try {
|
|||
// String archivePath = joinPath(city.getFtpArchiveDir(), fileName);
|
|||
// FtpUtil.ensureDir(ftp, city.getFtpArchiveDir());
|
|||
// FtpUtil.rename(ftp, sourcePath, archivePath);
|
|||
// logger.info("【FTP文件】已归档: {} -> {}", sourcePath, archivePath);
|
|||
// } catch (Exception e) {
|
|||
// logger.error("【FTP文件】归档失败: {}, 原因={}", fileName, e.getMessage());
|
|||
// }
|
|||
|
|||
return failCount == 0; |
|||
} |
|||
|
|||
/** |
|||
* 处理单行记录 |
|||
* |
|||
* @return true=成功入库,false=跳过 |
|||
*/ |
|||
private boolean processLine(String line, FtpCityConfig city, Set<String> existingIds, |
|||
FTPClient ftp) throws Exception { |
|||
// 按分隔符拆分字段(老淮南FTP数据格式:※分隔)
|
|||
String[] fields = line.split(properties.getFieldSeparator(), -1); |
|||
if (fields.length < MIN_FIELD_COUNT) { |
|||
logger.debug("【FTP行】字段数不足({}<{}),跳过: {}", fields.length, MIN_FIELD_COUNT, line); |
|||
return false; |
|||
} |
|||
|
|||
// ========== 字段解析(老淮南FTP数据格式)==========
|
|||
String kssj = fields[0].trim(); // kssj : 开始时间 yyyyMMddHHmmss(如:20240101120000)
|
|||
String jssj = fields[1].trim(); // jssj : 结束时间 yyyyMMddHHmmss
|
|||
String hjls = fields[2].trim(); // hjls : 呼叫流水号(用于拼录音文件名主名)
|
|||
String hjzls = fields[3].trim(); // hjzls : 呼叫主/被叫流水号(用于拼录音文件名辅名)
|
|||
String zjhm = fields[4].trim(); // zjhm : 主叫号码(包含区号,如:055312345678)
|
|||
String bjhm = fields[5].trim(); // bjhm : 被叫号码
|
|||
String thscStr = fields[6].trim();// thsc : 通话时长(秒)
|
|||
String thfx = fields[9].trim(); // thfx : 通话方向(1=呼入,0=呼出 - 老系统特殊编码)
|
|||
|
|||
// 必填字段校验
|
|||
if (kssj.isEmpty() || jssj.isEmpty() || hjls.isEmpty() || hjzls.isEmpty()) { |
|||
return false; |
|||
} |
|||
|
|||
// 时间字符串转 Date 对象
|
|||
Date callStartTime = DateUtil.parseDate14(kssj); |
|||
Date callEndTime = DateUtil.parseDate14(jssj); |
|||
if (callStartTime == null) { |
|||
return false; |
|||
} |
|||
|
|||
int thsc = parseInt(thscStr, 0); |
|||
// thfx 转换:老系统"0"=呼出,其他(含1)=呼入
|
|||
// 数据库规范:1=呼入,2=呼出(所以 isOutgoing=true 时存"2")
|
|||
boolean isOutgoing = thfx.contains("0"); |
|||
// 录音文件名 = hjls_hjzls.mp3(如:12345_67890.mp3)
|
|||
String recordFileName = hjls + "_" + hjzls + ".mp3"; |
|||
// 唯一标识 = 地市编码_FTP_呼叫流水(防重键)
|
|||
String callRecordId = city.getCityCode() + "_FTP_" + hjls; |
|||
|
|||
// 防重复检查(内存Set比对,已存在则跳过)
|
|||
if (existingIds.contains(callRecordId)) { |
|||
logger.debug("【FTP行】已存在,跳过: {}", callRecordId); |
|||
return false; |
|||
} |
|||
|
|||
// 主叫/被叫处理:去除区号前缀
|
|||
// 配置不存在的区号(如"0000"),因为 startsWith 匹配不上,会保留原始号码
|
|||
// 后续如需去除区号,把配置改为正确的区号即可,无需重新发包
|
|||
zjhm = stripAreaCode(zjhm, city.getPhoneAreaCode()); |
|||
bjhm = stripAreaCode(bjhm, city.getPhoneAreaCode()); |
|||
|
|||
// 构建本地临时路径和OSS路径
|
|||
// 本地路径示例:./vaa-ftp-temp/340100/20240101/ftp/12345_67890.mp3
|
|||
// OSS路径示例:voice/340100/20240101/12345_67890.mp3
|
|||
String dateStr = DateUtil.formatDate(callStartTime, "yyyyMMdd"); |
|||
String ossPath = FilePathUtil.buildOssPath(city.getCityCode(), callStartTime, recordFileName); |
|||
|
|||
// 下载录音文件(FTP路径:{ftp-record-dir}/{yyyyMMdd}/{filename})
|
|||
// 先切换到录音文件根目录,避免工作目录在别处导致下载失败
|
|||
String recordDir = city.getFtpRecordDir(); |
|||
// 去掉末尾斜杠,避免双斜杠
|
|||
if (recordDir != null && recordDir.endsWith("/")) { |
|||
recordDir = recordDir.substring(0, recordDir.length() - 1); |
|||
} |
|||
logger.info("【FTP行】准备切换目录: {}", recordDir); |
|||
FtpUtil.changeDir(ftp, recordDir); |
|||
String remoteRecordPath = dateStr + "/" + recordFileName; |
|||
logger.info("【FTP行】准备下载录音: {}/{}", recordDir, remoteRecordPath); |
|||
byte[] recordBytes = FtpUtil.downloadFile(ftp, remoteRecordPath); |
|||
logger.info("【FTP行】下载录音: {} ({} 字节)", recordFileName, recordBytes.length); |
|||
|
|||
// 为了方便您测试验证链路,这里加上一段代码,把从FTP读到的录音直接存在本地硬盘
|
|||
try { |
|||
java.nio.file.Path testSaveDir = java.nio.file.Paths.get("download_test_records", dateStr); |
|||
if (!java.nio.file.Files.exists(testSaveDir)) { |
|||
java.nio.file.Files.createDirectories(testSaveDir); |
|||
} |
|||
java.nio.file.Path testSaveFile = testSaveDir.resolve(recordFileName); |
|||
java.nio.file.Files.write(testSaveFile, recordBytes); |
|||
logger.info("【FTP测试】已成功将录音保存到本地硬盘: {}", testSaveFile.toAbsolutePath()); |
|||
} catch (Exception e) { |
|||
logger.error("【FTP测试】保存本地测试文件失败: {}", e.getMessage()); |
|||
} |
|||
|
|||
// 下载完成后切回txt目录,继续处理下一个文件
|
|||
FtpUtil.changeDir(ftp, city.getFtpSourceDir()); |
|||
|
|||
// 上传到OSS(获取可访问的URL)
|
|||
String ossUrl = fileUploadUtil.uploadWav(city.getCityName(), ossPath, recordFileName, recordBytes); |
|||
logger.info("【FTP行】上传OSS: {}", ossUrl); |
|||
|
|||
// 构建实体对象并入库
|
|||
MidVoiceCallRecord rec = buildCallRecord(city, callRecordId, callStartTime, callEndTime, |
|||
thsc, isOutgoing, zjhm, bjhm, recordFileName, recordBytes.length, ossUrl); |
|||
callRecordMapper.insert(rec); |
|||
existingIds.add(callRecordId); // 防止同批内重复(本次循环内)
|
|||
logger.info("【FTP行】保存成功: id={}, callRecordId={}", rec.getId(), callRecordId); |
|||
|
|||
// 将成功信息记录到本地文件中,方便排查和核对
|
|||
try { |
|||
java.nio.file.Path logDir = java.nio.file.Paths.get("logs"); |
|||
if (!java.nio.file.Files.exists(logDir)) { |
|||
java.nio.file.Files.createDirectories(logDir); |
|||
} |
|||
java.nio.file.Path logFile = logDir.resolve("upload_success.log"); |
|||
String logLine = String.format("[%s] 成功同步录音: 流水号=%s, 文件名=%s, OSS地址=%s\n", |
|||
DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"), callRecordId, recordFileName, ossUrl); |
|||
java.nio.file.Files.write(logFile, logLine.getBytes("UTF-8"), |
|||
java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); |
|||
} catch (Exception e) { |
|||
logger.error("记录成功日志到文件失败", e); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* 构建通话记录实体 |
|||
* |
|||
* @param city 地市配置(提供编码、名称) |
|||
* @param callRecordId 唯一标识(地市编码_FTP_呼叫流水) |
|||
* @param callStartTime 通话开始时间 |
|||
* @param callEndTime 通话结束时间 |
|||
* @param duration 通话时长(秒) |
|||
* @param isOutgoing 是否呼出(true=呼出,false=呼入) |
|||
* @param zjhm 主叫号码(已去区号) |
|||
* @param bjhm 被叫号码(已去区号) |
|||
* @param fileName 录音文件名 |
|||
* @param fileSize 录音文件大小(字节) |
|||
* @param ossUrl OSS访问URL |
|||
*/ |
|||
private MidVoiceCallRecord buildCallRecord(FtpCityConfig city, String callRecordId, |
|||
Date callStartTime, Date callEndTime, |
|||
int duration, boolean isOutgoing, |
|||
String zjhm, String bjhm, |
|||
String fileName, int fileSize, String ossUrl) { |
|||
MidVoiceCallRecord rec = new MidVoiceCallRecord(); |
|||
rec.setCallRecordId(callRecordId); // 唯一标识
|
|||
rec.setDeviceNo("FTP_" + city.getCityCode()); // 设备编码(FTP_地市编码)
|
|||
rec.setCityCode(city.getCityCode()); // 地市编码(用于数据库分区)
|
|||
rec.setCityName(city.getCityName()); // 地市名称
|
|||
rec.setOrgCode(city.getCityCode()); // 单位代码(用地市编码代替)
|
|||
rec.setRecordingFileName(fileName); // 录音文件名
|
|||
rec.setCallStartTime(callStartTime); // 通话开始时间
|
|||
rec.setCallEndTime(callEndTime); // 通话结束时间
|
|||
rec.setCallDuration(duration); // 通话时长(秒)
|
|||
rec.setCallDirection(isOutgoing ? DIR_OUTGOING : DIR_INCOMING); // 通话方向:1呼入/2呼出
|
|||
rec.setCallTel(isOutgoing ? zjhm : bjhm); // 主叫号码
|
|||
rec.setCalledTel(isOutgoing ? bjhm : zjhm); // 被叫号码
|
|||
rec.setCallStatus(CALL_STATUS_OK); // 通话状态:1=正常接通
|
|||
rec.setRecordingFileSize(fileSize); // 文件大小(字节)
|
|||
rec.setRecordingFilePath(ossUrl); // OSS访问URL
|
|||
return rec; |
|||
} |
|||
|
|||
/** |
|||
* 批量加载已存在的call_record_id(防重) |
|||
*/ |
|||
private Set<String> loadExistingCallRecordIds(String prefix) { |
|||
try { |
|||
List<String> ids = callRecordMapper.selectCallRecordIdsByPrefix(prefix); |
|||
logger.info("【FTP防重】已加载 {} 条历史记录(prefix={})", ids.size(), prefix); |
|||
return new HashSet<>(ids); |
|||
} catch (Exception e) { |
|||
logger.error("【FTP防重】加载历史记录失败,将全部当作新数据: {}", e.getMessage()); |
|||
return new HashSet<>(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 批量加载已处理的文件名(避免重复处理txt文件) |
|||
*/ |
|||
private Set<String> loadProcessedFileNames(String cityCode) { |
|||
try { |
|||
List<String> fileNames = processedFileMapper.selectProcessedFileNamesByCity(cityCode); |
|||
return new HashSet<>(fileNames); |
|||
} catch (Exception e) { |
|||
logger.error("【FTP防重】加载已处理文件记录失败: {}", e.getMessage()); |
|||
return new HashSet<>(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 记录已处理的txt文件 |
|||
*/ |
|||
private void recordProcessedFile(FtpCityConfig city, String fileName, long fileSize, |
|||
int recordCount, int successCount, int failCount, |
|||
long processTime, String errorMsg) { |
|||
try { |
|||
FtpProcessedFile record = new FtpProcessedFile(); |
|||
record.setCityCode(city.getCityCode()); |
|||
record.setCityName(city.getCityName()); |
|||
record.setFileName(fileName); |
|||
record.setFileSize(fileSize); |
|||
record.setProcessStatus(failCount == 0 ? "1" : "0"); |
|||
record.setRecordCount(recordCount); |
|||
record.setSuccessCount(successCount); |
|||
record.setFailCount(failCount); |
|||
record.setProcessTime(processTime); |
|||
record.setErrorMsg(errorMsg); |
|||
processedFileMapper.insert(record); |
|||
logger.debug("【FTP文件】已记录处理结果: {}", fileName); |
|||
} catch (Exception e) { |
|||
logger.error("【FTP文件】记录处理结果失败: {}, 原因={}", fileName, e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 去除电话号码的区号前缀 |
|||
* |
|||
* @param phone 原始号码 |
|||
* @param areaCode 区号前缀(如合肥 0551),为空或配置不存在的值则不去除 |
|||
* @return 处理后的号码 |
|||
*/ |
|||
private String stripAreaCode(String phone, String areaCode) { |
|||
if (phone == null || areaCode == null || areaCode.isEmpty()) { |
|||
return phone; |
|||
} |
|||
if (phone.startsWith(areaCode)) { |
|||
return phone.substring(areaCode.length()); |
|||
} |
|||
return phone; |
|||
} |
|||
|
|||
/** |
|||
* 安全转 int |
|||
*/ |
|||
private int parseInt(String s, int defaultValue) { |
|||
if (s == null || s.isEmpty()) return defaultValue; |
|||
try { |
|||
return Integer.parseInt(s); |
|||
} catch (NumberFormatException e) { |
|||
return defaultValue; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 拼接FTP路径,自动处理末尾斜杠,避免双斜杠 |
|||
* |
|||
* @param parts 路径片段 |
|||
* @return 拼接后的路径,使用正斜杠(FTP标准路径分隔符) |
|||
*/ |
|||
private String joinPath(String... parts) { |
|||
if (parts == null || parts.length == 0) { |
|||
return ""; |
|||
} |
|||
StringBuilder sb = new StringBuilder(); |
|||
for (int i = 0; i < parts.length; i++) { |
|||
String part = parts[i]; |
|||
if (part == null || part.isEmpty()) { |
|||
continue; |
|||
} |
|||
// 去掉末尾的斜杠(除了最后一个片段且本身就是斜杠的情况)
|
|||
if (part.endsWith("/") && i < parts.length - 1) { |
|||
part = part.substring(0, part.length() - 1); |
|||
} |
|||
// 去掉开头的斜杠(除了第一个片段)
|
|||
if (i > 0 && part.startsWith("/")) { |
|||
part = part.substring(1); |
|||
} |
|||
if (sb.length() > 0) { |
|||
sb.append("/"); |
|||
} |
|||
sb.append(part); |
|||
} |
|||
return sb.toString(); |
|||
} |
|||
} |
|||
@ -1,390 +0,0 @@ |
|||
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.MidVoiceDeviceLog; |
|||
import com.threecloud.dataserviceyy.mapper.MidVoiceCallRecordMapper; |
|||
import com.threecloud.dataserviceyy.mapper.MidVoiceDeviceLogMapper; |
|||
import com.threecloud.dataserviceyy.mapper.VoiceSyncMapper; |
|||
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.net.URLEncoder; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
import java.util.*; |
|||
|
|||
/** |
|||
* VAA录音盒定时同步服务 |
|||
* |
|||
* 【功能】从EBOX录音盒拉取录音 → 下载文件 → 上传OSS → 保存通话记录 |
|||
* 【策略】增量同步、无重试、防重复、失败记录到日志表 |
|||
* 【账户】每个设备的账号密码从 mid_voice_device_config 表读取 |
|||
*/ |
|||
@Service |
|||
public class VaaSyncService { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(VaaSyncService.class); |
|||
|
|||
@Autowired |
|||
private VoiceSyncMapper voiceSyncMapper; |
|||
@Autowired |
|||
private MidVoiceCallRecordMapper callRecordMapper; |
|||
@Autowired |
|||
private MidVoiceDeviceLogMapper deviceLogMapper; |
|||
@Autowired |
|||
private VaaHttpUtil vaaHttpUtil; |
|||
@Autowired |
|||
private FileUploadUtil fileUploadUtil; |
|||
|
|||
@Value("${vaa-sync.download-path:./vaa-recordings}") |
|||
private String downloadPath; |
|||
@Value("${vaa-sync.retain-days:10}") |
|||
private int retainDays; |
|||
|
|||
/** |
|||
* 定时同步任务 |
|||
*/ |
|||
@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); |
|||
} |
|||
|
|||
/** |
|||
* 执行同步任务(主入口) |
|||
*/ |
|||
public void executeSync() { |
|||
try { |
|||
// 清理过期本地文件
|
|||
FileCleaner.cleanOldFiles(downloadPath, retainDays); |
|||
|
|||
// 查询在线设备列表(含每个设备的账号密码)
|
|||
List<Map<String, Object>> deviceList = voiceSyncMapper.getAllYysb(); |
|||
logger.info("【主流程】查询到 {} 个在线语音设备", deviceList.size()); |
|||
|
|||
int successCount = 0; |
|||
int failCount = 0; |
|||
for (int i = 0; i < deviceList.size(); i++) { |
|||
Map<String, Object> device = deviceList.get(i); |
|||
String deviceId = getStr(device, "ID"); |
|||
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); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 同步单个设备 |
|||
* |
|||
* 流程:登录 → 获取分机号码 → 查录音列表 → 逐条下载上传保存 |
|||
* 任何步骤失败直接记录日志,跳过该设备 |
|||
*/ |
|||
private void syncSingleDevice(Map<String, Object> device) throws Exception { |
|||
String deviceId = getStr(device, "ID"); |
|||
String deviceNo = getStr(device, "UUID"); |
|||
String cityName = getStr(device, "ORGAN_NAME"); |
|||
String cityCode = getStr(device, "ORGAN_ID"); |
|||
String ip = getStr(device, "IP"); |
|||
Integer port = getInt(device, "PORT", 80); |
|||
String orgCode = getStr(device, "ORG_CODE"); |
|||
// 每个设备有自己的账号密码,从配置表读取
|
|||
String username = getStr(device, "USERNAME"); |
|||
String password = getStr(device, "PASSWORD"); |
|||
|
|||
logger.info("【设备】同步设备: ID={}, 编号={}, 机构={}, IP={}:{}", deviceId, deviceNo, cityName, ip, port); |
|||
|
|||
// 参数校验
|
|||
if (!StringUtils.hasText(ip)) { |
|||
logger.warn("【设备】IP为空,跳过: ID={}", deviceId); |
|||
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "设备IP为空"); |
|||
return; |
|||
} |
|||
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { |
|||
logger.warn("【设备】账号密码为空,跳过: ID={}", deviceId); |
|||
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "设备账号密码未配置"); |
|||
return; |
|||
} |
|||
if (!StringUtils.hasText(cityName)) { |
|||
cityName = "unknown_" + deviceId; |
|||
} |
|||
|
|||
String deviceHost = buildHost(ip, port); |
|||
|
|||
// 步骤1:登录(无重试)
|
|||
String authToken; |
|||
try { |
|||
String loginUrl = String.format("http://%s/authorize?username=%s&password=%s", |
|||
deviceHost, urlEncode(username), urlEncode(password)); |
|||
authToken = vaaHttpUtil.httpLogin(loginUrl); |
|||
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "1", null); |
|||
} catch (Exception e) { |
|||
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "1", "0", "登录失败:" + e.getMessage()); |
|||
logger.error("【设备】登录失败,跳过: IP={}, 原因={}", ip, e.getMessage()); |
|||
return; |
|||
} |
|||
|
|||
// 步骤2:获取分机号码(通道→电话号码映射),用于解析主叫/被叫
|
|||
Map<String, String> extNumbers = getExtensionNumbers(deviceHost, authToken); |
|||
|
|||
// 步骤3:计算同步时间范围(增量同步)
|
|||
Date lastSyncTime = SyncTimeUtil.readLastSyncTime(downloadPath, deviceId); |
|||
Date now = new Date(); |
|||
if (lastSyncTime == null || DateUtil.getDateDoubleDiff(now, lastSyncTime) > 1.0) { |
|||
lastSyncTime = DateUtil.addDayByDate(now, -1); |
|||
} |
|||
long startEpoch = lastSyncTime.getTime() / 1000; |
|||
long endEpoch = now.getTime() / 1000; |
|||
|
|||
// 步骤4:获取录音列表(无重试)
|
|||
JSONArray records; |
|||
try { |
|||
String recordUrl = String.format("http://%s/service/record/~/time[%d,%d]", |
|||
deviceHost, startEpoch, endEpoch); |
|||
String recordData = vaaHttpUtil.httpVisit(recordUrl, authToken); |
|||
records = vaaHttpUtil.parseRecordData(recordData); |
|||
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "2", "1", null); |
|||
} catch (Exception e) { |
|||
saveDeviceLog(deviceId, deviceNo, cityCode, cityName, ip, port, "2", "0", "查询录音列表失败:" + e.getMessage()); |
|||
logger.error("【设备】获取录音列表失败: {}", e.getMessage()); |
|||
return; |
|||
} |
|||
|
|||
if (records == null || records.isEmpty()) { |
|||
logger.info("【设备】无新录音: ID={}", deviceId); |
|||
return; |
|||
} |
|||
logger.info("【设备】获取到 {} 条录音记录", records.size()); |
|||
|
|||
// 批量查询已存在的call_record_id(一次查询代替N次,防重复性能优化)
|
|||
String startTimeStr = DateUtil.formatDate(lastSyncTime, "yyyy-MM-dd HH:mm:ss"); |
|||
String endTimeStr = DateUtil.formatDate(now, "yyyy-MM-dd HH:mm:ss"); |
|||
Set<String> existingIds = new HashSet<>(callRecordMapper.selectExistingCallRecordIds( |
|||
deviceNo, startTimeStr, endTimeStr)); |
|||
logger.debug("【设备】已存在 {} 条通话记录,将跳过", existingIds.size()); |
|||
|
|||
// 步骤5:逐条处理录音
|
|||
int successCount = 0; |
|||
int failCount = 0; |
|||
Date latestCallTime = null; |
|||
|
|||
for (int i = 0; i < records.size(); i++) { |
|||
JSONObject rec = records.getJSONObject(i); |
|||
try { |
|||
Date callTime = processSingleRecord(rec, deviceNo, cityName, cityCode, orgCode, |
|||
deviceHost, authToken, extNumbers, existingIds); |
|||
if (callTime != null) { |
|||
successCount++; |
|||
if (latestCallTime == null || callTime.after(latestCallTime)) { |
|||
latestCallTime = callTime; |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
failCount++; |
|||
logger.error("【录音】处理失败: {}, 原因={}", rec.getString("id"), e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
// 步骤6:保存同步时间
|
|||
if (latestCallTime != null) { |
|||
SyncTimeUtil.writeLastSyncTime(downloadPath, deviceId, latestCallTime); |
|||
} |
|||
|
|||
logger.info("【设备】同步完成: ID={}, 成功{}条, 失败{}条", deviceId, successCount, failCount); |
|||
} |
|||
|
|||
/** |
|||
* 处理单条录音记录 |
|||
* |
|||
* @return 通话开始时间(成功时),null表示跳过 |
|||
*/ |
|||
private Date processSingleRecord(JSONObject rec, String deviceNo, String cityName, |
|||
String cityCode, String orgCode, String deviceHost, |
|||
String authToken, Map<String, String> extNumbers, |
|||
Set<String> existingIds) throws Exception { |
|||
// 解析录音信息
|
|||
String recordId = RecordParser.parseRecordId(rec); |
|||
String filePath = RecordParser.parseFilePath(rec); |
|||
Integer channel = RecordParser.parseChannel(rec); |
|||
String phone = RecordParser.parsePhone(rec); |
|||
boolean isOutgoing = RecordParser.isOutgoing(rec); |
|||
boolean isAnswered = RecordParser.isAnswered(rec); |
|||
Long begTime = RecordParser.parseBegTime(rec); |
|||
Long endTime = RecordParser.parseEndTime(rec); |
|||
|
|||
// 校验必要字段
|
|||
if (filePath == null || filePath.isEmpty()) { |
|||
return null; |
|||
} |
|||
if (begTime == null || endTime == null) { |
|||
return null; |
|||
} |
|||
|
|||
// 防重复:内存中比对已存在的call_record_id(一次查询代替N次)
|
|||
String callRecordId = deviceNo + "_" + recordId; |
|||
if (existingIds.contains(callRecordId)) { |
|||
logger.debug("【录音】已存在,跳过: {}", callRecordId); |
|||
return null; |
|||
} |
|||
|
|||
// 准备文件路径
|
|||
Date callStartTime = new Date(begTime * 1000); |
|||
String fileName = FilePathUtil.extractFileName(filePath); |
|||
String localPath = FilePathUtil.buildLocalPath(downloadPath, cityCode, callStartTime, deviceNo, fileName); |
|||
Path localFile = Paths.get(localPath); |
|||
String ossPath = FilePathUtil.buildOssPath(cityCode, callStartTime, fileName); |
|||
|
|||
// 获取通道绑定的电话号码(从EBOX分机号码配置)
|
|||
String channelPhone = ""; |
|||
if (channel != null && extNumbers != null) { |
|||
channelPhone = extNumbers.getOrDefault(String.valueOf(channel), ""); |
|||
} |
|||
|
|||
// 构建通话记录
|
|||
MidVoiceCallRecord callRecord = buildCallRecord( |
|||
callRecordId, deviceNo, cityCode, cityName, orgCode, |
|||
fileName, callStartTime, new Date(endTime * 1000), |
|||
(int) (endTime - begTime), channelPhone, phone, isOutgoing, isAnswered); |
|||
|
|||
// 下载录音文件(无重试)
|
|||
if (!Files.exists(localFile) || Files.size(localFile) == 0) { |
|||
String fileUrl = "http://" + deviceHost + filePath; |
|||
vaaHttpUtil.httpDown(fileUrl, localPath, authToken); |
|||
logger.info("【录音】下载完成: {} ({} 字节)", fileName, Files.size(localFile)); |
|||
} else { |
|||
logger.debug("【录音】文件已存在,跳过下载: {}", fileName); |
|||
} |
|||
callRecord.setRecordingFileSize((int) Files.size(localFile)); |
|||
|
|||
// 上传到OSS
|
|||
byte[] wavData = Files.readAllBytes(localFile); |
|||
String ossUrl = fileUploadUtil.uploadWav(cityName, ossPath, fileName, wavData); |
|||
callRecord.setRecordingFilePath(ossUrl); |
|||
logger.info("【录音】上传OSS成功: {}", ossUrl); |
|||
|
|||
// 保存到数据库
|
|||
callRecordMapper.insert(callRecord); |
|||
logger.info("【录音】保存成功: callRecordId={}", callRecordId); |
|||
|
|||
return callStartTime; |
|||
} |
|||
|
|||
/** |
|||
* 构建通话记录实体 |
|||
* |
|||
* 号码解析规则: |
|||
* - 呼出(state=1):主叫=本机号码(通道绑定号码),被叫=对方号码(phone字段) |
|||
* - 呼入(state=2):主叫=对方号码(phone字段),被叫=本机号码(通道绑定号码) |
|||
*/ |
|||
private MidVoiceCallRecord buildCallRecord(String callRecordId, String deviceNo, String cityCode, |
|||
String cityName, String orgCode, String fileName, |
|||
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.setCallStartTime(callStartTime); |
|||
record.setCallEndTime(callEndTime); |
|||
record.setCallDuration(duration); |
|||
record.setCallDirection(isOutgoing ? "2" : "1"); // 1呼入,2呼出
|
|||
|
|||
String localNum = channelPhone != null ? channelPhone : ""; |
|||
String remoteNum = remotePhone != null ? remotePhone : ""; |
|||
|
|||
if (isOutgoing) { |
|||
record.setCallTel(localNum); // 主叫:本机
|
|||
record.setCalledTel(remoteNum); // 被叫:对方
|
|||
} else { |
|||
record.setCallTel(remoteNum); // 主叫:对方
|
|||
record.setCalledTel(localNum); // 被叫:本机
|
|||
} |
|||
|
|||
record.setCallStatus(isAnswered ? "1" : "2"); // 1正常接通,2未接通
|
|||
return record; |
|||
} |
|||
|
|||
/** |
|||
* 获取分机号码配置 |
|||
* 调用 EBOX 接口 GET /service/ext/number |
|||
* 返回 Map<通道号, 电话号码>,如 {"1":"8001","2":"8002"} |
|||
*/ |
|||
private Map<String, String> getExtensionNumbers(String deviceHost, String authToken) { |
|||
try { |
|||
String extUrl = "http://" + deviceHost + "/service/ext/number"; |
|||
return vaaHttpUtil.getExtensionNumbers(extUrl, authToken); |
|||
} catch (Exception e) { |
|||
logger.warn("【设备】获取分机号码失败,将无法解析本机号码: {}", e.getMessage()); |
|||
return Collections.emptyMap(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 保存设备连接日志到 mid_voice_device_log 表 |
|||
*/ |
|||
private void saveDeviceLog(String deviceId, String deviceNo, String cityCode, String cityName, |
|||
String ipAddress, Integer devicePort, String connectType, |
|||
String connectStatus, String failReason) { |
|||
try { |
|||
MidVoiceDeviceLog log = new MidVoiceDeviceLog(); |
|||
log.setDeviceId(deviceId); |
|||
log.setDeviceNo(deviceNo); |
|||
log.setCityCode(cityCode); |
|||
log.setCityName(cityName); |
|||
log.setIpAddress(ipAddress); |
|||
log.setDevicePort(devicePort); |
|||
log.setConnectType(connectType); |
|||
log.setConnectStatus(connectStatus); |
|||
log.setFailReason(failReason); |
|||
log.setCreateTime(new Date()); |
|||
deviceLogMapper.insert(log); |
|||
} catch (Exception e) { |
|||
logger.error("保存设备连接日志失败: {}", e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
// ==================== 工具方法 ====================
|
|||
|
|||
private String buildHost(String ip, Integer port) { |
|||
return ip + (port != null && port != 80 ? ":" + port : ""); |
|||
} |
|||
|
|||
private String getStr(Map<String, Object> map, String key) { |
|||
Object v = map.get(key); |
|||
return v != null ? v.toString() : null; |
|||
} |
|||
|
|||
private Integer getInt(Map<String, Object> map, String key, Integer defaultVal) { |
|||
Object v = map.get(key); |
|||
if (v == null) return defaultVal; |
|||
try { return Integer.parseInt(v.toString()); } |
|||
catch (NumberFormatException e) { return defaultVal; } |
|||
} |
|||
|
|||
private String urlEncode(String value) throws Exception { |
|||
return URLEncoder.encode(value, "UTF-8"); |
|||
} |
|||
} |
|||
@ -1,76 +0,0 @@ |
|||
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()); |
|||
} |
|||
} |
|||
@ -1,177 +1,44 @@ |
|||
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 保留天数 |
|||
* @param tempDir 临时目录 |
|||
* @param retainDays 保留天数(早于今天 retainDays 天的文件将被删除) |
|||
*/ |
|||
public static void cleanOldFiles(String basePath, int retainDays) { |
|||
File baseDir = new File(basePath); |
|||
if (!baseDir.exists() || !baseDir.isDirectory()) { |
|||
logger.debug("基础目录不存在或不是目录: {}", basePath); |
|||
public static void clean(String tempDir, int retainDays) { |
|||
File dir = new File(tempDir); |
|||
if (!dir.exists()) { |
|||
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("没有需要清理的过期文件"); |
|||
} |
|||
long threshold = System.currentTimeMillis() - (long) retainDays * 24 * 60 * 60 * 1000; |
|||
deleteRecursive(dir, threshold); |
|||
} |
|||
|
|||
/** |
|||
* 清理指定地市目录下的过期日期目录 |
|||
* |
|||
* @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); |
|||
} |
|||
private static void deleteRecursive(File file, long threshold) { |
|||
if (!file.isDirectory()) { |
|||
if (file.lastModified() < threshold) { |
|||
file.delete(); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
File[] files = file.listFiles(); |
|||
if (files == null) return; |
|||
for (File f : files) { |
|||
deleteRecursive(f, threshold); |
|||
} |
|||
// 空目录也删除
|
|||
File[] remain = file.listFiles(); |
|||
if (remain == null || remain.length == 0) { |
|||
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,181 @@ |
|||
package com.threecloud.dataserviceyy.util; |
|||
|
|||
import org.apache.commons.net.ftp.FTP; |
|||
import org.apache.commons.net.ftp.FTPClient; |
|||
import org.apache.commons.net.ftp.FTPFile; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.io.ByteArrayOutputStream; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
|
|||
/** |
|||
* FTP 工具类 |
|||
* 封装FTP连接、文件列表、下载、删除、重命名等操作 |
|||
* 基于 Apache Commons Net FTPClient |
|||
*/ |
|||
public class FtpUtil { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(FtpUtil.class); |
|||
|
|||
/** FTP连接超时(毫秒) */ |
|||
private static final int CONNECT_TIMEOUT = 15000; |
|||
/** FTP读取超时(毫秒) */ |
|||
private static final int READ_TIMEOUT = 60000; |
|||
|
|||
/** |
|||
* 连接FTP服务器(一次性连接处理所有操作,最后调用 disconnect 关闭) |
|||
* |
|||
* @param host FTP服务器IP |
|||
* @param port FTP服务器端口 |
|||
* @param username 用户名 |
|||
* @param password 密码 |
|||
* @return FTPClient 连接成功返回客户端,失败抛异常 |
|||
*/ |
|||
public static FTPClient connect(String host, int port, String username, String password) throws IOException { |
|||
FTPClient ftp = new FTPClient(); |
|||
ftp.setConnectTimeout(CONNECT_TIMEOUT); |
|||
ftp.setDataTimeout(READ_TIMEOUT); |
|||
ftp.connect(host, port); |
|||
boolean loginOk = ftp.login(username, password); |
|||
if (!loginOk) { |
|||
ftp.disconnect(); |
|||
throw new IOException("FTP登录失败,请检查用户名密码"); |
|||
} |
|||
// 设置被动模式(必须,避开防火墙)
|
|||
ftp.enterLocalPassiveMode(); |
|||
// 设置二进制传输(mp3/wav必须用二进制)
|
|||
ftp.setFileType(FTP.BINARY_FILE_TYPE); |
|||
// 设置UTF-8编码(文件名有中文)
|
|||
ftp.setControlEncoding("UTF-8"); |
|||
logger.info("FTP连接成功: {}:{}", host, port); |
|||
return ftp; |
|||
} |
|||
|
|||
/** |
|||
* 断开FTP连接 |
|||
*/ |
|||
public static void disconnect(FTPClient ftp) { |
|||
if (ftp != null && ftp.isConnected()) { |
|||
try { |
|||
ftp.logout(); |
|||
} catch (IOException ignored) { |
|||
} |
|||
try { |
|||
ftp.disconnect(); |
|||
} catch (IOException ignored) { |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 列出指定目录下的所有文件 |
|||
* |
|||
* @param ftp 已连接的FTPClient |
|||
* @param dir 目录路径(相对或绝对) |
|||
* @return 文件列表,失败抛异常 |
|||
*/ |
|||
public static FTPFile[] listFiles(FTPClient ftp, String dir) throws IOException { |
|||
// 切换到目标目录
|
|||
boolean cdOk = ftp.changeWorkingDirectory(dir); |
|||
if (!cdOk) { |
|||
throw new IOException("切换FTP目录失败: " + dir); |
|||
} |
|||
FTPFile[] files = ftp.listFiles(); |
|||
logger.debug("FTP目录 [{}] 下有 {} 个文件", dir, files.length); |
|||
return files; |
|||
} |
|||
|
|||
/** |
|||
* 下载文件到本地字节数组 |
|||
* |
|||
* @param ftp 已连接的FTPClient |
|||
* @param remotePath 远程文件路径(绝对或相对于当前工作目录) |
|||
* @return 文件字节内容 |
|||
*/ |
|||
public static byte[] downloadFile(FTPClient ftp, String remotePath) throws IOException { |
|||
try (InputStream in = ftp.retrieveFileStream(remotePath); |
|||
ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
|||
if (in == null) { |
|||
throw new IOException("下载文件失败(无输入流): " + remotePath); |
|||
} |
|||
byte[] buffer = new byte[8192]; |
|||
int len; |
|||
while ((len = in.read(buffer)) != -1) { |
|||
out.write(buffer, 0, len); |
|||
} |
|||
// 必须调用 completePendingCommand,否则后续操作会失败
|
|||
boolean ok = ftp.completePendingCommand(); |
|||
if (!ok) { |
|||
throw new IOException("FTP下载未完成: " + remotePath); |
|||
} |
|||
return out.toByteArray(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 重命名/移动FTP文件(用于把处理完的txt移到归档目录) |
|||
* |
|||
* @param ftp 已连接的FTPClient |
|||
* @param fromPath 原文件路径 |
|||
* @param toPath 目标文件路径 |
|||
*/ |
|||
public static void rename(FTPClient ftp, String fromPath, String toPath) throws IOException { |
|||
boolean ok = ftp.rename(fromPath, toPath); |
|||
if (!ok) { |
|||
throw new IOException("FTP重命名失败: " + fromPath + " -> " + toPath); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除FTP文件 |
|||
*/ |
|||
public static void delete(FTPClient ftp, String path) throws IOException { |
|||
boolean ok = ftp.deleteFile(path); |
|||
if (!ok) { |
|||
throw new IOException("FTP删除文件失败: " + path); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 确保归档目录存在,不存在则创建 |
|||
*/ |
|||
public static void ensureDir(FTPClient ftp, String dir) throws IOException { |
|||
if (!ftp.changeWorkingDirectory(dir)) { |
|||
// 尝试创建目录
|
|||
boolean ok = ftp.makeDirectory(dir); |
|||
if (!ok) { |
|||
throw new IOException("创建FTP目录失败: " + dir); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 切换工作目录(不存在则创建) |
|||
*/ |
|||
public static void changeDir(FTPClient ftp, String dir) throws IOException { |
|||
if (!ftp.changeWorkingDirectory(dir)) { |
|||
throw new IOException("切换FTP目录失败: " + dir); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 判断是否为txt文件 |
|||
*/ |
|||
public static boolean isTxtFile(FTPFile file) { |
|||
return file != null && file.isFile() && file.getName().toLowerCase().endsWith(".txt"); |
|||
} |
|||
|
|||
/** |
|||
* 统计txt文件数量 |
|||
*/ |
|||
public static int countTxtFiles(FTPFile[] files) { |
|||
if (files == null) return 0; |
|||
int count = 0; |
|||
for (FTPFile f : files) { |
|||
if (isTxtFile(f)) count++; |
|||
} |
|||
return count; |
|||
} |
|||
} |
|||
@ -1,88 +0,0 @@ |
|||
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; |
|||
} |
|||
} |
|||
@ -1,70 +0,0 @@ |
|||
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()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,271 +0,0 @@ |
|||
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); |
|||
|
|||
/** 连接超时:10秒 */ |
|||
private static final int CONNECT_TIMEOUT = 10000; |
|||
/** API读取超时:30秒 */ |
|||
private static final int READ_TIMEOUT = 30000; |
|||
/** 文件下载读取超时:2分钟 */ |
|||
private static final int DOWNLOAD_READ_TIMEOUT = 120000; |
|||
|
|||
/** |
|||
* HTTP登录认证(无重试) |
|||
* @param loginUrl 登录URL |
|||
* @return 登录成功后返回Authorization Cookie值 |
|||
* @throws Exception 登录失败直接抛出异常 |
|||
*/ |
|||
public String httpLogin(String loginUrl) throws Exception { |
|||
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地址 |
|||
* @param authorization 登录后返回的Authorization Cookie值 |
|||
* @return 返回JSON字符串 |
|||
* @throws Exception 访问失败直接抛出异常 |
|||
*/ |
|||
public String httpVisit(String apiUrl, String authorization) throws Exception { |
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 下载录音文件(无重试) |
|||
* @param fileUrl 文件URL |
|||
* @param savePath 保存路径 |
|||
* @param authorization 登录后返回的Authorization Cookie值 |
|||
* @throws Exception 下载失败直接抛出异常 |
|||
*/ |
|||
public void httpDown(String fileUrl, String savePath, String authorization) throws Exception { |
|||
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; |
|||
|
|||
while ((bytesRead = inputStream.read(buffer)) != -1) { |
|||
outputStream.write(buffer, 0, bytesRead); |
|||
totalBytes += bytesRead; |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
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 |
|||
* @param extUrl 接口地址 |
|||
* @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 { |
|||
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<>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
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,75 @@ |
|||
<?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.FtpProcessedFileMapper"> |
|||
|
|||
<resultMap id="BaseResultMap" type="com.threecloud.dataserviceyy.entity.FtpProcessedFile"> |
|||
<id column="id" property="id"/> |
|||
<result column="city_code" property="cityCode"/> |
|||
<result column="city_name" property="cityName"/> |
|||
<result column="file_name" property="fileName"/> |
|||
<result column="file_size" property="fileSize"/> |
|||
<result column="process_status" property="processStatus"/> |
|||
<result column="record_count" property="recordCount"/> |
|||
<result column="success_count" property="successCount"/> |
|||
<result column="fail_count" property="failCount"/> |
|||
<result column="process_time" property="processTime"/> |
|||
<result column="error_msg" property="errorMsg"/> |
|||
<result column="create_time" property="createTime"/> |
|||
</resultMap> |
|||
|
|||
<!-- 插入处理记录 --> |
|||
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> |
|||
INSERT INTO mid_ftp_processed_file ( |
|||
city_code, city_name, file_name, file_size, process_status, |
|||
record_count, success_count, fail_count, process_time, error_msg, create_time |
|||
) VALUES ( |
|||
#{cityCode}, #{cityName}, #{fileName}, #{fileSize}, #{processStatus}, |
|||
#{recordCount}, #{successCount}, #{failCount}, #{processTime}, #{errorMsg}, NOW() |
|||
) |
|||
</insert> |
|||
|
|||
<!-- 根据地市编码和文件名查询 --> |
|||
<select id="selectByCityAndFileName" resultMap="BaseResultMap"> |
|||
SELECT * FROM mid_ftp_processed_file |
|||
WHERE city_code = #{cityCode} |
|||
AND file_name = #{fileName} |
|||
LIMIT 1 |
|||
</select> |
|||
|
|||
<!-- 查询指定地市下所有已处理的文件名列表 --> |
|||
<select id="selectProcessedFileNamesByCity" resultType="java.lang.String"> |
|||
SELECT file_name FROM mid_ftp_processed_file |
|||
WHERE city_code = #{cityCode} |
|||
AND process_status = '1' |
|||
</select> |
|||
|
|||
<!-- 批量查询指定地市下多个文件的处理状态 --> |
|||
<select id="selectByCityAndFileNames" resultMap="BaseResultMap"> |
|||
SELECT * FROM mid_ftp_processed_file |
|||
WHERE city_code = #{cityCode} |
|||
AND file_name IN |
|||
<foreach collection="fileNames" item="name" open="(" separator="," close=")"> |
|||
#{name} |
|||
</foreach> |
|||
</select> |
|||
|
|||
<!-- 根据ID更新处理结果 --> |
|||
<update id="updateById"> |
|||
UPDATE mid_ftp_processed_file |
|||
SET process_status = #{processStatus}, |
|||
record_count = #{recordCount}, |
|||
success_count = #{successCount}, |
|||
fail_count = #{failCount}, |
|||
process_time = #{processTime}, |
|||
error_msg = #{errorMsg} |
|||
WHERE id = #{id} |
|||
</update> |
|||
|
|||
<!-- 删除指定时间之前的记录 --> |
|||
<delete id="deleteBeforeTime"> |
|||
DELETE FROM mid_ftp_processed_file |
|||
WHERE city_code = #{cityCode} |
|||
AND create_time < TO_TIMESTAMP(#{beforeTime}, 'YYYY-MM-DD HH24:MI:SS') |
|||
</delete> |
|||
|
|||
</mapper> |
|||
@ -1,85 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.threecloud.dataserviceyy.mapper.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> |
|||
@ -1,117 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.threecloud.dataserviceyy.mapper.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> |
|||
@ -1,20 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.threecloud.dataserviceyy.mapper.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, |
|||
username AS USERNAME, |
|||
password AS PASSWORD |
|||
FROM mid_voice.mid_voice_device_config |
|||
WHERE device_status = '1' |
|||
</select> |
|||
|
|||
</mapper> |
|||
Loading…
Reference in new issue