commit for 9.7
This commit is contained in:
@@ -15,7 +15,7 @@ SERVER_IP=192.168.1.83
|
||||
# 各服务外部端口配置
|
||||
MYSQL_EXTERNAL_PORT=3306
|
||||
REDIS_EXTERNAL_PORT=6379
|
||||
LJWX_BOOT_EXTERNAL_PORT=9998
|
||||
LJWX_BOOT_EXTERNAL_PORT=9999
|
||||
LJWX_BIGSCREEN_EXTERNAL_PORT=8001
|
||||
LJWX_ADMIN_EXTERNAL_PORT=8088
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# 智能穿戴系统客户现场部署使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
`deploy-client.sh` 是智能穿戴系统的客户现场部署脚本,支持使用预构建镜像进行快速部署,采用Docker命名卷实现数据持久化。该脚本提供完整的自动化部署流程,包括配置验证、镜像管理、服务启动、定制化配置等功能。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- ✅ **智能部署模式**: 支持在线/离线两种部署模式
|
||||
- ✅ **数据持久化**: 使用Docker命名卷确保数据安全
|
||||
- ✅ **配置验证**: 自动验证配置文件一致性
|
||||
- ✅ **智能备份**: 升级前自动备份现有数据
|
||||
- ✅ **定制化支持**: 自动应用客户定制配置
|
||||
- ✅ **健康检查**: 完整的服务启动状态监控
|
||||
- ✅ **跨平台兼容**: 支持CentOS等多种Linux发行版
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 服务组件
|
||||
- **ljwx-mysql**: 数据库服务 (端口: 3306)
|
||||
- **ljwx-redis**: 缓存服务 (端口: 6379)
|
||||
- **ljwx-boot**: 后端API服务 (端口: 9998)
|
||||
- **ljwx-bigscreen**: 数据大屏服务 (端口: 8001)
|
||||
- **ljwx-admin**: 管理端服务 (端口: 8088)
|
||||
|
||||
### 数据持久化
|
||||
- `client-deployment_mysql_data`: MySQL数据卷
|
||||
- `client-deployment_redis_data`: Redis数据卷
|
||||
- `client-deployment_ljwx_boot_data`: 后端数据卷
|
||||
- `client-deployment_ljwx_bigscreen_data`: 大屏数据卷
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本语法
|
||||
```bash
|
||||
./deploy-client.sh [配置文件名] [offline]
|
||||
```
|
||||
|
||||
### 部署模式
|
||||
|
||||
#### 在线部署 (推荐)
|
||||
```bash
|
||||
# 使用默认配置文件
|
||||
./deploy-client.sh
|
||||
|
||||
# 使用指定配置文件
|
||||
./deploy-client.sh my-config.env
|
||||
```
|
||||
|
||||
#### 离线部署
|
||||
```bash
|
||||
# 离线模式 + 默认配置
|
||||
./deploy-client.sh offline
|
||||
|
||||
# 离线模式 + 指定配置
|
||||
./deploy-client.sh my-config.env offline
|
||||
```
|
||||
|
||||
## 部署前准备
|
||||
|
||||
### 1. 环境要求
|
||||
- Docker Engine (已安装并运行)
|
||||
- docker-compose (已安装)
|
||||
- 网络连接 (在线模式需要)
|
||||
|
||||
### 2. 必需文件检查
|
||||
```
|
||||
client-deployment/
|
||||
├── deploy-client.sh # 主部署脚本
|
||||
├── custom-config.env # 配置文件
|
||||
├── custom-config.py # Python自定义配置
|
||||
├── custom-admin-config.js # 前端自定义配置
|
||||
├── docker-compose.yml # Docker编排文件
|
||||
├── client-data.sql # 客户数据(首次部署)
|
||||
└── custom-assets/ # 自定义资源目录
|
||||
└── logo.svg # 自定义logo文件
|
||||
```
|
||||
|
||||
### 3. 配置文件准备
|
||||
|
||||
复制并编辑配置文件:
|
||||
```bash
|
||||
cp custom-config.env my-client-config.env
|
||||
```
|
||||
|
||||
关键配置项:
|
||||
```env
|
||||
# 服务器地址
|
||||
SERVER_IP=192.168.1.83
|
||||
|
||||
# 端口配置
|
||||
LJWX_ADMIN_EXTERNAL_PORT=8088
|
||||
LJWX_BIGSCREEN_EXTERNAL_PORT=8001
|
||||
LJWX_BOOT_EXTERNAL_PORT=9998
|
||||
|
||||
# 定制化配置
|
||||
VITE_APP_TITLE=客户定制管理平台
|
||||
BIGSCREEN_TITLE=客户数据大屏
|
||||
COMPANY_NAME=客户公司名称
|
||||
VITE_CUSTOM_LOGO=true
|
||||
|
||||
# 镜像版本
|
||||
LJWX_ADMIN_VERSION=2.0.1
|
||||
LJWX_BIGSCREEN_VERSION=2.0.1
|
||||
LJWX_BOOT_VERSION=2.0.1
|
||||
```
|
||||
|
||||
## 部署流程详解
|
||||
|
||||
### 第一阶段:环境检查与配置验证
|
||||
1. **系统兼容性检查**: 自动检测CentOS并处理文件格式
|
||||
2. **依赖检查**: 验证Docker和docker-compose安装
|
||||
3. **配置文件验证**: 加载并验证配置文件格式
|
||||
4. **必需文件检查**: 确认所有必需的配置文件存在
|
||||
|
||||
### 第二阶段:数据策略选择
|
||||
根据现有数据卷情况自动判断部署类型:
|
||||
|
||||
#### 首次部署
|
||||
- 创建新的Docker命名卷
|
||||
- 使用 `client-data.sql` 进行数据初始化
|
||||
- 无需备份操作
|
||||
|
||||
#### 升级部署
|
||||
- 检测现有命名卷
|
||||
- 提供备份策略选择:
|
||||
1. **在线备份**: 完整SQL导出 (推荐)
|
||||
2. **离线卷备份**: 直接备份Docker卷
|
||||
3. **跳过备份**: 风险自担
|
||||
4. **退出部署**: 手动处理
|
||||
|
||||
### 第三阶段:镜像管理
|
||||
1. **镜像版本检查**: 验证配置文件中的镜像版本
|
||||
2. **本地镜像检查**: 扫描本地已有镜像
|
||||
3. **镜像拉取策略**:
|
||||
- 在线模式:自动登录阿里云镜像仓库并拉取缺失镜像
|
||||
- 离线模式:仅使用本地镜像,缺失时给出警告
|
||||
|
||||
### 第四阶段:服务部署
|
||||
1. **数据初始化配置**: 首次部署时动态添加数据初始化挂载
|
||||
2. **容器清理**: 清理可能冲突的旧容器
|
||||
3. **目录权限设置**: 确保日志和备份目录权限正确
|
||||
4. **服务启动**: 根据部署模式启动所有服务
|
||||
5. **启动等待**: 等待服务完全启动
|
||||
|
||||
### 第五阶段:健康检查
|
||||
对每个服务进行健康状态验证:
|
||||
- **ljwx-boot**: 检查 `/actuator/health` 端点
|
||||
- **ljwx-bigscreen**: 检查 `/health` 端点
|
||||
- **ljwx-admin**: 检查首页访问
|
||||
- 超时时间:每个服务120秒
|
||||
|
||||
### 第六阶段:定制化配置
|
||||
1. **大屏URL更新**: 替换前端中的大屏访问地址
|
||||
2. **浏览器缓存清理**: 清除可能的缓存问题
|
||||
3. **自定义Logo应用**:
|
||||
- 检查 `VITE_CUSTOM_LOGO=true` 配置
|
||||
- 调用 `customize-admin-logo.sh` 进行logo替换
|
||||
4. **挂载配置验证**: 验证容器挂载是否正确
|
||||
|
||||
## 定制化功能详解
|
||||
|
||||
### 自定义Logo功能
|
||||
脚本支持自动替换管理端Logo:
|
||||
|
||||
1. **启用条件**: `VITE_CUSTOM_LOGO=true`
|
||||
2. **支持格式**: png, jpg, jpeg, svg
|
||||
3. **文件位置**: `custom-assets/logo.*` 或包含"logo"的图片文件
|
||||
4. **实现机制**:
|
||||
- 复制logo文件到容器 `/usr/share/nginx/html/logo-custom.*`
|
||||
- 生成JavaScript配置文件
|
||||
- 在index.html中注入配置脚本
|
||||
- 重新加载nginx配置
|
||||
|
||||
### 大屏URL配置
|
||||
自动更新前端应用中的大屏跳转链接:
|
||||
- 读取 `VITE_BIGSCREEN_URL` 配置
|
||||
- 在ljwx-admin容器中替换相关配置
|
||||
- 确保管理端可正确跳转到数据大屏
|
||||
|
||||
## 错误处理和故障排除
|
||||
|
||||
### 常见问题解决
|
||||
|
||||
#### 1. Docker登录失败
|
||||
```bash
|
||||
❌ Docker登录失败,请检查用户名和密码
|
||||
```
|
||||
**解决方案**: 检查网络连接和镜像仓库访问权限
|
||||
|
||||
#### 2. 镜像拉取失败
|
||||
```bash
|
||||
❌ 警告: 无法拉取 xxx 镜像
|
||||
```
|
||||
**解决方案**:
|
||||
- 检查网络连接
|
||||
- 确认镜像版本号正确
|
||||
- 考虑使用离线部署模式
|
||||
|
||||
#### 3. 服务启动超时
|
||||
```bash
|
||||
⚠️ 后端服务启动超时,请检查日志
|
||||
```
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查看详细日志
|
||||
docker-compose logs ljwx-boot
|
||||
|
||||
# 检查容器状态
|
||||
docker ps -a
|
||||
|
||||
# 查看文件日志
|
||||
tail -f logs/ljwx-boot/*.log
|
||||
```
|
||||
|
||||
#### 4. 端口冲突
|
||||
确保以下端口未被占用:
|
||||
- 3306 (MySQL)
|
||||
- 6379 (Redis)
|
||||
- 9998 (后端API)
|
||||
- 8001 (数据大屏)
|
||||
- 8088 (管理端)
|
||||
|
||||
### 日志查看工具
|
||||
|
||||
#### 实时日志监控
|
||||
```bash
|
||||
# 使用内置日志查看器(推荐)
|
||||
./logs-viewer.sh
|
||||
|
||||
# 查看特定服务日志
|
||||
docker-compose logs -f ljwx-boot
|
||||
docker-compose logs -f ljwx-admin
|
||||
|
||||
# 查看文件日志
|
||||
tail -f logs/ljwx-boot/*.log
|
||||
tail -f logs/mysql/slow.log
|
||||
```
|
||||
|
||||
#### 日志目录结构
|
||||
```
|
||||
logs/
|
||||
├── mysql/ # MySQL数据库日志
|
||||
├── redis/ # Redis缓存日志
|
||||
├── ljwx-boot/ # 后端服务日志
|
||||
├── ljwx-bigscreen/ # 数据大屏日志
|
||||
└── ljwx-admin/ # 管理端日志
|
||||
```
|
||||
|
||||
## 部署后管理
|
||||
|
||||
### 服务管理命令
|
||||
```bash
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 重启特定服务
|
||||
docker-compose restart ljwx-admin
|
||||
|
||||
# 停止所有服务(保留数据)
|
||||
docker-compose down
|
||||
|
||||
# 完全清理(⚠️删除数据卷)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### 数据备份恢复
|
||||
```bash
|
||||
# 在线备份(推荐)
|
||||
./auto-backup.sh
|
||||
|
||||
# 数据恢复
|
||||
./restore-backup.sh
|
||||
|
||||
# 跨平台备份
|
||||
./auto-backup-crossplatform.sh
|
||||
```
|
||||
|
||||
### 访问地址
|
||||
部署完成后的访问地址:
|
||||
- **管理端**: `http://服务器IP:8088`
|
||||
- **数据大屏**: `http://服务器IP:8001`
|
||||
- **后端API**: `http://服务器IP:9998`
|
||||
|
||||
默认登录账号:`admin / 123456`
|
||||
|
||||
## 最佳实践建议
|
||||
|
||||
### 1. 部署前准备
|
||||
- 备份现有数据和配置
|
||||
- 确认服务器资源充足 (CPU: 4核, 内存: 8GB, 磁盘: 50GB)
|
||||
- 准备好客户定制化资源文件
|
||||
|
||||
### 2. 配置管理
|
||||
- 为每个客户创建独立的配置文件
|
||||
- 定期备份配置文件到版本控制系统
|
||||
- 记录客户特殊需求和配置变更
|
||||
|
||||
### 3. 运维监控
|
||||
- 定期检查服务健康状态
|
||||
- 监控磁盘空间使用情况
|
||||
- 设置定时备份任务
|
||||
|
||||
### 4. 安全注意事项
|
||||
- 修改默认管理员密码
|
||||
- 配置防火墙规则限制端口访问
|
||||
- 定期更新系统和Docker版本
|
||||
|
||||
## 技术支持
|
||||
|
||||
如在部署过程中遇到问题,请:
|
||||
|
||||
1. 查看详细错误日志
|
||||
2. 检查系统资源使用情况
|
||||
3. 参考故障排除章节
|
||||
4. 联系技术支持团队
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v2.0.1
|
||||
**更新时间**: 2025-09-06
|
||||
**适用版本**: deploy-client.sh v2.0+
|
||||
@@ -0,0 +1,688 @@
|
||||
# 灵境万象健康数据分析算法设计方案
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档详细描述了灵境万象(LJWX)健康监控系统中的核心算法设计,包括健康趋势分析、健康预测和健康画像三大核心功能的技术实现方案。
|
||||
|
||||
### 1.1 系统版本信息
|
||||
- **版本**: v2.0.1
|
||||
- **创建日期**: 2025年9月
|
||||
- **更新日期**: 2025年9月
|
||||
- **适用范围**: LJWX BigScreen个人健康监控大屏
|
||||
|
||||
### 1.2 核心算法模块
|
||||
1. **健康数据趋势分析算法** - 基于时间序列分析的健康指标趋势识别
|
||||
2. **健康状态预测算法** - 使用线性回归的健康状态预测模型
|
||||
3. **健康画像生成算法** - 综合评分与个性化建议生成系统
|
||||
|
||||
---
|
||||
|
||||
## 2. 健康数据趋势分析算法
|
||||
|
||||
### 2.1 算法概述
|
||||
|
||||
健康数据趋势分析算法(`calculate_health_trends`)是一个综合性的时间序列分析系统,用于识别用户健康指标的变化趋势、异常检测和统计分析。
|
||||
|
||||
### 2.2 数据预处理模块
|
||||
|
||||
#### 2.2.1 数据清洗算法
|
||||
```python
|
||||
def clean_data(values):
|
||||
"""
|
||||
四分位数异常值检测算法
|
||||
- 移除None值和零值
|
||||
- 使用IQR方法检测异常值
|
||||
- 过滤离群点数据
|
||||
"""
|
||||
# 1. 基础数据过滤
|
||||
clean_values = [v for v in values if v is not None and v > 0]
|
||||
|
||||
# 2. 四分位数计算
|
||||
q1 = clean_values[len(clean_values)//4]
|
||||
q3 = clean_values[3*len(clean_values)//4]
|
||||
iqr = q3 - q1
|
||||
|
||||
# 3. 异常值边界计算
|
||||
lower_bound = q1 - 1.5 * iqr
|
||||
upper_bound = q3 + 1.5 * iqr
|
||||
|
||||
# 4. 异常值过滤
|
||||
return [v for v in clean_values if lower_bound <= v <= upper_bound]
|
||||
```
|
||||
|
||||
**关键特性**:
|
||||
- **鲁棒性**: 使用四分位数方法,对极值不敏感
|
||||
- **自适应**: 根据数据分布自动调整异常值阈值
|
||||
- **效率**: O(n)时间复杂度
|
||||
|
||||
### 2.3 时间序列格式化
|
||||
|
||||
#### 2.3.1 ECharts兼容格式转换
|
||||
```python
|
||||
def format_time_series(timestamps, values, default_value=0):
|
||||
"""
|
||||
将原始数据转换为ECharts时间序列格式
|
||||
格式: [[timestamp, value], [timestamp, value], ...]
|
||||
"""
|
||||
series_data = []
|
||||
for i, timestamp in enumerate(timestamps):
|
||||
if i < len(values) and values[i] is not None:
|
||||
series_data.append([timestamp, values[i]])
|
||||
else:
|
||||
series_data.append([timestamp, default_value])
|
||||
return series_data
|
||||
```
|
||||
|
||||
**输出数据结构**:
|
||||
```json
|
||||
{
|
||||
"heartRate": [["2025-09-07T10:00:00", 75], ["2025-09-07T10:30:00", 78]],
|
||||
"bloodOxygen": [["2025-09-07T10:00:00", 98], ["2025-09-07T10:30:00", 97]],
|
||||
"temperature": [["2025-09-07T10:00:00", 36.5], ["2025-09-07T10:30:00", 36.6]]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 趋势分析算法
|
||||
|
||||
#### 2.4.1 统计指标计算
|
||||
```python
|
||||
def calculate_stats(values):
|
||||
"""
|
||||
计算健康指标的统计信息和趋势
|
||||
"""
|
||||
# 基础统计量
|
||||
avg = sum(values) / len(values)
|
||||
min_val = min(values)
|
||||
max_val = max(values)
|
||||
|
||||
# 趋势分析 - 分段比较法
|
||||
if len(values) >= 4:
|
||||
mid_point = len(values) // 2
|
||||
first_half_avg = sum(values[:mid_point]) / mid_point
|
||||
second_half_avg = sum(values[mid_point:]) / (len(values) - mid_point)
|
||||
change_percent = ((second_half_avg - first_half_avg) / first_half_avg) * 100
|
||||
|
||||
# 趋势分类
|
||||
if change_percent > 5:
|
||||
trend = 'rising' # 上升趋势
|
||||
elif change_percent < -5:
|
||||
trend = 'falling' # 下降趋势
|
||||
else:
|
||||
trend = 'stable' # 稳定趋势
|
||||
|
||||
return {
|
||||
'avg': round(avg, 2),
|
||||
'min': min_val,
|
||||
'max': max_val,
|
||||
'trend': trend,
|
||||
'change': round(change_percent, 2)
|
||||
}
|
||||
```
|
||||
|
||||
**趋势判定标准**:
|
||||
- **上升趋势**: 后半段均值比前半段高5%以上
|
||||
- **下降趋势**: 后半段均值比前半段低5%以上
|
||||
- **稳定趋势**: 变化幅度在±5%以内
|
||||
|
||||
### 2.5 健康状态评估算法
|
||||
|
||||
#### 2.5.1 综合评分系统
|
||||
```python
|
||||
def assess_health_status(stats):
|
||||
"""
|
||||
基于医学标准的健康状态评估算法
|
||||
总分100分,各指标权重分配:
|
||||
- 心率: 15分
|
||||
- 血氧: 20分
|
||||
- 体温: 10分
|
||||
- 血压: 15分
|
||||
- 运动: 10分
|
||||
"""
|
||||
score = 100
|
||||
warnings = []
|
||||
|
||||
# 心率评估 (60-100 bpm正常)
|
||||
hr_avg = stats['heartRate']['avg']
|
||||
if hr_avg > 100 or hr_avg < 60:
|
||||
score -= 15
|
||||
warnings.append(f"心率异常: 平均{hr_avg}bpm")
|
||||
|
||||
# 血氧评估 (>95%正常)
|
||||
spo2_avg = stats['bloodOxygen']['avg']
|
||||
if spo2_avg < 95:
|
||||
score -= 20
|
||||
warnings.append(f"血氧偏低: 平均{spo2_avg}%")
|
||||
|
||||
# 体温评估 (36.0-37.5°C正常)
|
||||
temp_avg = stats['temperature']['avg']
|
||||
if temp_avg > 37.5 or temp_avg < 36.0:
|
||||
score -= 10
|
||||
warnings.append(f"体温异常: 平均{temp_avg}°C")
|
||||
|
||||
return {
|
||||
'overallScore': max(0, score),
|
||||
'status': get_health_level(score),
|
||||
'warnings': warnings
|
||||
}
|
||||
```
|
||||
|
||||
**健康等级分类**:
|
||||
- **优秀** (90-100分): excellent
|
||||
- **良好** (75-89分): good
|
||||
- **一般** (60-74分): fair
|
||||
- **需关注** (<60分): poor
|
||||
|
||||
---
|
||||
|
||||
## 3. 健康状态预测算法
|
||||
|
||||
### 3.1 线性回归预测模型
|
||||
|
||||
#### 3.1.1 预测算法实现
|
||||
```python
|
||||
def predict_trends(values, periods=5):
|
||||
"""
|
||||
使用简单线性回归预测未来健康指标趋势
|
||||
|
||||
参数:
|
||||
- values: 历史数据点列表
|
||||
- periods: 预测未来的时间点数量
|
||||
|
||||
返回:
|
||||
- predictions: 预测值列表
|
||||
"""
|
||||
if len(values) < 3:
|
||||
return [] # 数据不足,无法预测
|
||||
|
||||
# 准备回归数据
|
||||
n = len(values)
|
||||
x_vals = list(range(n)) # 时间序列 [0, 1, 2, ...]
|
||||
y_vals = values # 健康指标值
|
||||
|
||||
# 计算回归系数
|
||||
x_mean = sum(x_vals) / n
|
||||
y_mean = sum(y_vals) / n
|
||||
|
||||
# 最小二乘法计算斜率和截距
|
||||
numerator = sum((x_vals[i] - x_mean) * (y_vals[i] - y_mean) for i in range(n))
|
||||
denominator = sum((x_vals[i] - x_mean) ** 2 for i in range(n))
|
||||
|
||||
if denominator == 0:
|
||||
slope = 0 # 避免除零错误
|
||||
else:
|
||||
slope = numerator / denominator
|
||||
|
||||
intercept = y_mean - slope * x_mean
|
||||
|
||||
# 生成预测值
|
||||
predictions = []
|
||||
for i in range(periods):
|
||||
pred_x = n + i # 未来时间点
|
||||
pred_y = slope * pred_x + intercept
|
||||
predictions.append(round(pred_y, 2))
|
||||
|
||||
return predictions
|
||||
```
|
||||
|
||||
### 3.2 预测准确性评估
|
||||
|
||||
#### 3.2.1 置信度计算
|
||||
预测置信度基于以下因素:
|
||||
- **数据点数量**: 数据点越多,置信度越高
|
||||
- **数据稳定性**: 波动越小,置信度越高
|
||||
- **趋势一致性**: 趋势越明显,置信度越高
|
||||
|
||||
```python
|
||||
def calculate_confidence(values, predictions):
|
||||
"""
|
||||
计算预测置信度
|
||||
"""
|
||||
if len(values) < 5:
|
||||
return 'low'
|
||||
elif len(values) < 20:
|
||||
return 'medium'
|
||||
else:
|
||||
return 'high'
|
||||
```
|
||||
|
||||
### 3.3 预测应用场景
|
||||
|
||||
1. **短期预测** (1-5个时间点): 用于实时健康监控
|
||||
2. **中期预测** (5-20个时间点): 用于健康趋势分析
|
||||
3. **长期预测** (20+时间点): 用于健康风险评估
|
||||
|
||||
---
|
||||
|
||||
## 4. 健康画像生成算法
|
||||
|
||||
### 4.1 综合评分算法
|
||||
|
||||
#### 4.1.1 权重分配策略
|
||||
```python
|
||||
def calculate_health_score(heart_rates, blood_oxygen, temperatures, steps):
|
||||
"""
|
||||
综合健康评分算法
|
||||
|
||||
权重分配:
|
||||
- 心率: 30% (最重要的生命体征)
|
||||
- 血氧: 25% (呼吸系统健康指标)
|
||||
- 体温: 20% (基础代谢指标)
|
||||
- 运动: 25% (生活方式指标)
|
||||
"""
|
||||
score = 0
|
||||
factors = []
|
||||
|
||||
# 心率评分 (30分满分)
|
||||
if heart_rates:
|
||||
avg_hr = sum(heart_rates) / len(heart_rates)
|
||||
if 60 <= avg_hr <= 100: # 正常范围
|
||||
hr_score = 30
|
||||
elif 50 <= avg_hr < 60 or 100 < avg_hr <= 110: # 轻微异常
|
||||
hr_score = 25
|
||||
else: # 明显异常
|
||||
hr_score = 15
|
||||
score += hr_score
|
||||
factors.append(f'心率: {avg_hr:.1f}bpm')
|
||||
|
||||
# 血氧评分 (25分满分)
|
||||
if blood_oxygen:
|
||||
avg_spo2 = sum(blood_oxygen) / len(blood_oxygen)
|
||||
if avg_spo2 >= 95: # 正常
|
||||
spo2_score = 25
|
||||
elif avg_spo2 >= 90: # 轻度缺氧
|
||||
spo2_score = 20
|
||||
else: # 重度缺氧
|
||||
spo2_score = 10
|
||||
score += spo2_score
|
||||
factors.append(f'血氧: {avg_spo2:.1f}%')
|
||||
|
||||
return {
|
||||
'overall': min(100, score),
|
||||
'category': get_score_category(score),
|
||||
'factors': factors
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 个性化建议生成算法
|
||||
|
||||
#### 4.2.1 规则引擎实现
|
||||
```python
|
||||
def generate_health_advice(heart_rates, blood_oxygen, temperatures, steps):
|
||||
"""
|
||||
基于规则引擎的个性化健康建议生成
|
||||
"""
|
||||
advice = []
|
||||
|
||||
# 心率相关建议
|
||||
if heart_rates:
|
||||
avg_hr = sum(heart_rates) / len(heart_rates)
|
||||
if avg_hr > 100:
|
||||
advice.append('您的平均心率偏高,建议减少剧烈运动,保持放松心态')
|
||||
elif avg_hr < 60:
|
||||
advice.append('您的心率偏低,建议适当增加有氧运动')
|
||||
else:
|
||||
advice.append('您的心率正常,请继续保持良好的运动习惯')
|
||||
|
||||
# 血氧相关建议
|
||||
if blood_oxygen:
|
||||
avg_spo2 = sum(blood_oxygen) / len(blood_oxygen)
|
||||
if avg_spo2 < 95:
|
||||
advice.append('血氧饱和度偏低,建议多做深呼吸练习,必要时咨询医生')
|
||||
else:
|
||||
advice.append('血氧水平良好,请保持规律的呼吸练习')
|
||||
|
||||
# 运动相关建议
|
||||
if steps:
|
||||
avg_steps = sum(steps) / len(steps)
|
||||
if avg_steps < 5000:
|
||||
advice.append('日常活动量不足,建议每天至少步行8000步')
|
||||
elif avg_steps > 15000:
|
||||
advice.append('运动量很充足,注意适度休息避免过度疲劳')
|
||||
else:
|
||||
advice.append('运动量适中,请继续保持')
|
||||
|
||||
# 通用健康建议
|
||||
advice.extend([
|
||||
'保持规律作息,确保充足睡眠',
|
||||
'注意饮食均衡,多吃蔬菜水果',
|
||||
'定期监测健康数据,及时发现异常'
|
||||
])
|
||||
|
||||
return advice[:5] # 最多返回5条建议
|
||||
```
|
||||
|
||||
### 4.3 健康画像数据结构
|
||||
|
||||
#### 4.3.1 标准输出格式
|
||||
```json
|
||||
{
|
||||
"healthScore": {
|
||||
"overall": 82,
|
||||
"category": "良好",
|
||||
"factors": [
|
||||
"心率: 75.3bpm",
|
||||
"血氧: 97.8%",
|
||||
"体温: 36.5°C",
|
||||
"日均步数: 7820步"
|
||||
]
|
||||
},
|
||||
"healthPrediction": {
|
||||
"trend": "心率呈稳定趋势",
|
||||
"confidence": "high",
|
||||
"details": "基于30天数据分析,共156个数据点"
|
||||
},
|
||||
"healthAdvice": [
|
||||
"您的心率正常,请继续保持良好的运动习惯",
|
||||
"血氧水平良好,请保持规律的呼吸练习",
|
||||
"运动量适中,请继续保持",
|
||||
"保持规律作息,确保充足睡眠",
|
||||
"注意饮食均衡,多吃蔬菜水果"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口设计规范
|
||||
|
||||
### 5.1 健康趋势分析接口
|
||||
|
||||
#### 5.1.1 接口定义
|
||||
```
|
||||
GET /api/health/trends/analysis
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
- `deviceSn` (string, required): 设备序列号
|
||||
- `timeRange` (string, optional): 时间范围 [1h, 6h, 24h, 7d, 30d]
|
||||
|
||||
**响应格式**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deviceSn": "CRFTQ23409001890",
|
||||
"timeRange": "24h",
|
||||
"dataPoints": 48,
|
||||
"chartData": {
|
||||
"heartRate": [["2025-09-07T10:00:00", 75]],
|
||||
"bloodOxygen": [["2025-09-07T10:00:00", 98]],
|
||||
"temperature": [["2025-09-07T10:00:00", 36.5]]
|
||||
},
|
||||
"statistics": {
|
||||
"heartRate": {
|
||||
"avg": 75.2,
|
||||
"min": 68,
|
||||
"max": 85,
|
||||
"trend": "stable",
|
||||
"change": 1.2
|
||||
}
|
||||
},
|
||||
"healthAssessment": {
|
||||
"overallScore": 85,
|
||||
"status": "good",
|
||||
"warnings": []
|
||||
},
|
||||
"predictions": {
|
||||
"heartRate": [75, 76, 74, 75, 77]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 综合健康分析接口
|
||||
|
||||
#### 5.2.1 接口定义
|
||||
```
|
||||
GET /api/health/analysis/comprehensive
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
- `deviceSn` (string, required): 设备序列号
|
||||
- `days` (integer, optional): 分析天数,默认30天
|
||||
|
||||
**响应格式**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deviceSn": "CRFTQ23409001890",
|
||||
"analysisDate": "2025-09-07T10:30:00Z",
|
||||
"dataPoints": 156,
|
||||
"analysisPeriod": "30天",
|
||||
"healthScore": {
|
||||
"overall": 82,
|
||||
"category": "良好"
|
||||
},
|
||||
"healthPrediction": {
|
||||
"trend": "心率保持稳定",
|
||||
"confidence": "high"
|
||||
},
|
||||
"healthAdvice": [
|
||||
"您的心率正常,请继续保持良好的运动习惯"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化策略
|
||||
|
||||
### 6.1 数据缓存策略
|
||||
|
||||
#### 6.1.1 Redis缓存设计
|
||||
```python
|
||||
# 缓存键设计
|
||||
CACHE_KEY_PATTERN = "health:analysis:{deviceSn}:{timeRange}:{date}"
|
||||
|
||||
# 缓存过期时间
|
||||
CACHE_TTL = {
|
||||
'1h': 300, # 5分钟
|
||||
'6h': 900, # 15分钟
|
||||
'24h': 1800, # 30分钟
|
||||
'7d': 3600, # 1小时
|
||||
'30d': 7200 # 2小时
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 数据库查询优化
|
||||
|
||||
#### 6.2.1 索引优化策略
|
||||
```sql
|
||||
-- 健康数据查询优化索引
|
||||
CREATE INDEX idx_health_device_time ON t_user_health_data (device_sn, timestamp DESC);
|
||||
CREATE INDEX idx_health_user_time ON t_user_health_data (user_id, timestamp DESC);
|
||||
CREATE INDEX idx_health_org_time ON t_user_health_data (org_id, timestamp DESC);
|
||||
```
|
||||
|
||||
### 6.3 算法性能指标
|
||||
|
||||
#### 6.3.1 性能基准
|
||||
- **数据处理速度**: >1000条记录/秒
|
||||
- **趋势分析延迟**: <500ms
|
||||
- **预测计算时间**: <200ms
|
||||
- **内存占用**: <100MB (1000用户并发)
|
||||
- **缓存命中率**: >90%
|
||||
|
||||
---
|
||||
|
||||
## 7. 质量保证与测试
|
||||
|
||||
### 7.1 算法准确性验证
|
||||
|
||||
#### 7.1.1 回归测试用例
|
||||
```python
|
||||
def test_trend_calculation_accuracy():
|
||||
"""测试趋势计算准确性"""
|
||||
# 上升趋势数据
|
||||
rising_data = [70, 72, 74, 76, 78, 80, 82, 84]
|
||||
result = calculate_stats(rising_data)
|
||||
assert result['trend'] == 'rising'
|
||||
assert result['change'] > 5
|
||||
|
||||
# 稳定趋势数据
|
||||
stable_data = [75, 74, 76, 75, 74, 76, 75, 74]
|
||||
result = calculate_stats(stable_data)
|
||||
assert result['trend'] == 'stable'
|
||||
assert -5 <= result['change'] <= 5
|
||||
```
|
||||
|
||||
### 7.2 异常处理机制
|
||||
|
||||
#### 7.2.1 容错设计
|
||||
```python
|
||||
def safe_calculate_trends(health_data, timeRange):
|
||||
"""
|
||||
安全的趋势计算,包含完整的异常处理
|
||||
"""
|
||||
try:
|
||||
return analyze_health_trends_comprehensive(health_data, timeRange)
|
||||
except ZeroDivisionError:
|
||||
logger.warning("除零错误,返回默认趋势数据")
|
||||
return generate_fallback_trends_data(timeRange)
|
||||
except ValueError as e:
|
||||
logger.error(f"数值错误: {e}")
|
||||
return generate_fallback_trends_data(timeRange)
|
||||
except Exception as e:
|
||||
logger.error(f"未知错误: {e}")
|
||||
return generate_fallback_trends_data(timeRange)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 部署与运维
|
||||
|
||||
### 8.1 监控指标
|
||||
|
||||
#### 8.1.1 关键监控点
|
||||
```python
|
||||
# 算法性能监控
|
||||
MONITORING_METRICS = {
|
||||
'algorithm_execution_time': {
|
||||
'trends_analysis': 'histogram',
|
||||
'health_prediction': 'histogram',
|
||||
'health_scoring': 'histogram'
|
||||
},
|
||||
'algorithm_accuracy': {
|
||||
'prediction_accuracy': 'gauge',
|
||||
'trend_detection_rate': 'gauge'
|
||||
},
|
||||
'system_health': {
|
||||
'memory_usage': 'gauge',
|
||||
'cpu_utilization': 'gauge',
|
||||
'cache_hit_rate': 'gauge'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 日志记录规范
|
||||
|
||||
#### 8.2.1 结构化日志
|
||||
```python
|
||||
import structlog
|
||||
|
||||
health_logger = structlog.get_logger("health_algorithm")
|
||||
|
||||
def log_algorithm_execution(algorithm_name, execution_time, data_points):
|
||||
"""记录算法执行日志"""
|
||||
health_logger.info(
|
||||
"algorithm_executed",
|
||||
algorithm=algorithm_name,
|
||||
execution_time_ms=execution_time,
|
||||
data_points=data_points,
|
||||
timestamp=datetime.now().isoformat()
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 未来优化方向
|
||||
|
||||
### 9.1 机器学习增强
|
||||
|
||||
#### 9.1.1 深度学习模型集成
|
||||
- **LSTM网络**: 用于长期健康趋势预测
|
||||
- **CNN网络**: 用于健康数据模式识别
|
||||
- **Transformer**: 用于多模态健康数据融合
|
||||
|
||||
### 9.2 个性化算法优化
|
||||
|
||||
#### 9.2.1 用户画像学习
|
||||
```python
|
||||
class PersonalizedHealthModel:
|
||||
"""个性化健康模型"""
|
||||
|
||||
def __init__(self, user_profile):
|
||||
self.age = user_profile.get('age')
|
||||
self.gender = user_profile.get('gender')
|
||||
self.medical_history = user_profile.get('medical_history')
|
||||
|
||||
def calculate_personalized_score(self, health_data):
|
||||
"""基于个人特征计算个性化健康评分"""
|
||||
base_score = calculate_health_score(health_data)
|
||||
|
||||
# 年龄调整
|
||||
age_factor = self.get_age_adjustment_factor()
|
||||
adjusted_score = base_score * age_factor
|
||||
|
||||
return adjusted_score
|
||||
```
|
||||
|
||||
### 9.3 实时预警系统
|
||||
|
||||
#### 9.3.1 异常检测算法
|
||||
```python
|
||||
def real_time_anomaly_detection(current_value, historical_data):
|
||||
"""
|
||||
实时异常检测算法
|
||||
使用统计方法识别健康数据异常
|
||||
"""
|
||||
mean = sum(historical_data) / len(historical_data)
|
||||
std_dev = calculate_std_deviation(historical_data)
|
||||
|
||||
# 3σ原则检测异常
|
||||
threshold = 3 * std_dev
|
||||
|
||||
if abs(current_value - mean) > threshold:
|
||||
return {
|
||||
'is_anomaly': True,
|
||||
'severity': 'high' if abs(current_value - mean) > 4 * std_dev else 'medium',
|
||||
'deviation': abs(current_value - mean)
|
||||
}
|
||||
|
||||
return {'is_anomaly': False}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 总结
|
||||
|
||||
本文档详细描述了灵境万象健康监控系统的核心算法设计,包括:
|
||||
|
||||
1. **健康趋势分析**: 基于时间序列分析和统计学方法的趋势识别系统
|
||||
2. **健康状态预测**: 使用线性回归的预测模型,支持短中长期预测
|
||||
3. **健康画像生成**: 综合评分与个性化建议的智能生成系统
|
||||
|
||||
### 10.1 技术优势
|
||||
|
||||
- **科学性**: 基于医学标准和统计学原理
|
||||
- **准确性**: 多层异常检测和数据清洗
|
||||
- **实用性**: 面向实际应用场景设计
|
||||
- **可扩展性**: 模块化设计,支持算法升级
|
||||
- **高性能**: 优化的数据处理和缓存策略
|
||||
|
||||
### 10.2 应用价值
|
||||
|
||||
- **个人健康监控**: 提供科学的健康状态评估
|
||||
- **预防医学**: 早期健康风险识别和预警
|
||||
- **生活方式指导**: 个性化的健康改善建议
|
||||
- **企业健康管理**: 员工群体健康状况分析
|
||||
|
||||
通过这套完整的算法体系,灵境万象健康监控系统能够为用户提供专业、准确、个性化的健康分析服务,助力用户实现主动健康管理。
|
||||
|
||||
---
|
||||
|
||||
**文档版权**: © 2025 灵境万象科技有限公司
|
||||
**技术支持**: LJWX技术团队
|
||||
**更新周期**: 季度更新
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NPopconfirm, NUpload, NModal, NCard, NTag, NDescriptions, NDescriptionsItem, NAlert, NCollapse, NCollapseItem, NList, NListItem, NIcon, NSpace, type UploadFileInfo } from 'naive-ui';
|
||||
import type { Ref } from 'vue';
|
||||
import { ref, h } from 'vue';
|
||||
import { ref, h, computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
@@ -25,6 +25,11 @@ const authStore = useAuthStore();
|
||||
const customerId = authStore.userInfo?.customerId;
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
// 检查是否为管理员用户
|
||||
const isAdmin = computed(() => {
|
||||
return authStore.userInfo?.userName === 'admin';
|
||||
});
|
||||
|
||||
const { dictTag } = useDict();
|
||||
|
||||
const editingData: Ref<Api.Customer.CustomerConfig | null> = ref(null);
|
||||
@@ -114,7 +119,8 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
render: row => {
|
||||
const buttons = [];
|
||||
|
||||
if (hasAuth('t:customer:config:update')) {
|
||||
// 编辑按钮 - 只有admin可见
|
||||
if (hasAuth('t:customer:config:update') && isAdmin.value) {
|
||||
buttons.push(
|
||||
h(NButton, {
|
||||
type: 'primary',
|
||||
@@ -125,7 +131,8 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAuth('t:customer:config:license:status')) {
|
||||
// 许可证按钮 - 只有admin可见
|
||||
if (hasAuth('t:customer:config:license:status') && isAdmin.value) {
|
||||
buttons.push(
|
||||
h(NButton, {
|
||||
type: 'info',
|
||||
@@ -136,7 +143,16 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAuth('t:customer:config:delete')) {
|
||||
// 如果不是admin,显示提示信息
|
||||
if (!isAdmin.value && buttons.length === 0) {
|
||||
buttons.push(
|
||||
h('span', {
|
||||
class: 'text-gray-400 text-xs px-2 py-1'
|
||||
}, '仅管理员可操作')
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAuth('t:customer:config:delete') && isAdmin.value) {
|
||||
buttons.push(
|
||||
h(NPopconfirm, {
|
||||
onPositiveClick: () => handleDelete(row.id)
|
||||
@@ -160,17 +176,29 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
const { drawerVisible, openDrawer, checkedRowKeys, onDeleted, onBatchDeleted } = useTableOperate(data, getData);
|
||||
|
||||
function handleAdd() {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能添加租户配置');
|
||||
return;
|
||||
}
|
||||
operateType.value = 'add';
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
function edit(item: Api.Customer.CustomerConfig) {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能编辑租户配置');
|
||||
return;
|
||||
}
|
||||
operateType.value = 'edit';
|
||||
editingData.value = { ...item };
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能删除租户配置');
|
||||
return;
|
||||
}
|
||||
// request
|
||||
const { error, data: result } = await fetchDeleteCustomerConfig(transDeleteParams([id]));
|
||||
if (!error && result) {
|
||||
@@ -179,6 +207,10 @@ async function handleDelete(id: string) {
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能批量删除租户配置');
|
||||
return;
|
||||
}
|
||||
// request
|
||||
const { error, data: result } = await fetchDeleteCustomerConfig(transDeleteParams(checkedRowKeys.value));
|
||||
if (!error && result) {
|
||||
@@ -212,6 +244,10 @@ const licenseUploading = ref(false);
|
||||
const manualExpanded = ref<string[]>([]);
|
||||
|
||||
async function viewLicense(customer: Api.Customer.CustomerConfig) {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能查看许可证信息');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { error, data } = await request<any>({
|
||||
url: `/t_customer_config/license/status/${customer.id}`,
|
||||
@@ -230,6 +266,11 @@ async function viewLicense(customer: Api.Customer.CustomerConfig) {
|
||||
}
|
||||
|
||||
async function uploadLicense(options: { file: UploadFileInfo }) {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能上传许可证');
|
||||
return false;
|
||||
}
|
||||
|
||||
licenseUploading.value = true;
|
||||
|
||||
try {
|
||||
@@ -445,8 +486,8 @@ function formatDateTime(dateTime: string | null): string {
|
||||
v-model:columns="columnChecks"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
:loading="loading"
|
||||
add-auth="t:customer:config:add"
|
||||
delete-auth="t:customer:config:delete"
|
||||
:add-auth="isAdmin ? 't:customer:config:add' : ''"
|
||||
:delete-auth="isAdmin ? 't:customer:config:delete' : ''"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
@@ -541,16 +582,17 @@ function formatDateTime(dateTime: string | null): string {
|
||||
:show-file-list="true"
|
||||
accept=".lic"
|
||||
:max="1"
|
||||
:disabled="licenseUploading"
|
||||
:disabled="licenseUploading || !isAdmin"
|
||||
>
|
||||
<NButton :loading="licenseUploading" type="primary" size="small">
|
||||
{{ licenseUploading ? '上传中...' : '选择许可证文件' }}
|
||||
<NButton :loading="licenseUploading" :disabled="!isAdmin" type="primary" size="small">
|
||||
{{ licenseUploading ? '上传中...' : (isAdmin ? '选择许可证文件' : '仅管理员可操作') }}
|
||||
</NButton>
|
||||
</NUpload>
|
||||
<div class="text-sm text-gray-500">
|
||||
<p>• 支持 .lic 格式的许可证文件</p>
|
||||
<p>• 上传新许可证后将自动重新验证</p>
|
||||
<p>• 建议在更新前备份当前配置</p>
|
||||
<p v-if="!isAdmin" class="text-orange-600">⚠️ 许可证上传功能仅限管理员使用</p>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
@@ -2047,6 +2047,601 @@ def health_data_chart_baseline():
|
||||
api_logger.error(f"健康基线图表接口错误: {str(e)}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/health/analysis/comprehensive', methods=['GET'])
|
||||
def get_comprehensive_health_analysis():
|
||||
"""综合健康分析接口:包含健康预测、评分和建议"""
|
||||
try:
|
||||
deviceSn = request.args.get('deviceSn')
|
||||
days = int(request.args.get('days', 30)) # 分析天数,默认30天
|
||||
|
||||
if not deviceSn:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'deviceSn参数是必需的'
|
||||
}), 400
|
||||
|
||||
# 获取用户ID和组织ID
|
||||
from .device import fetch_user_org_by_deviceSn
|
||||
userId, orgId = fetch_user_org_by_deviceSn(deviceSn)
|
||||
|
||||
if not userId or not orgId:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '未找到设备对应的用户信息'
|
||||
}), 404
|
||||
|
||||
# 计算日期范围
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# 使用get_all_health_data_optimized获取健康数据
|
||||
health_result = get_all_health_data_optimized(
|
||||
orgId=orgId,
|
||||
userId=userId,
|
||||
startDate=start_date.strftime('%Y-%m-%d'),
|
||||
endDate=end_date.strftime('%Y-%m-%d'),
|
||||
latest_only=False
|
||||
)
|
||||
|
||||
if not health_result.get('success'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '获取健康数据失败'
|
||||
}), 500
|
||||
|
||||
health_data = health_result.get('data', {}).get('healthData', [])
|
||||
|
||||
# 分析健康数据并生成预测、评分和建议
|
||||
analysis_result = analyze_health_comprehensive(health_data, days)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'deviceSn': deviceSn,
|
||||
'userId': str(userId),
|
||||
'orgId': str(orgId),
|
||||
'analysisDate': datetime.now().isoformat(),
|
||||
'dataPoints': len(health_data),
|
||||
'analysisPeriod': f'{days}天',
|
||||
**analysis_result
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"综合健康分析接口错误: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
def analyze_health_comprehensive(health_data, analysis_days):
|
||||
"""分析健康数据并生成预测、评分和建议"""
|
||||
if not health_data:
|
||||
return {
|
||||
'healthScore': {'overall': 60, 'category': '一般'},
|
||||
'healthPrediction': {'trend': '数据不足', 'confidence': 'low'},
|
||||
'healthAdvice': ['请保持正常作息', '定期进行健康检查']
|
||||
}
|
||||
|
||||
# 计算各项健康指标的统计数据
|
||||
heart_rates = [float(item.get('heart_rate', 0)) for item in health_data if item.get('heart_rate')]
|
||||
blood_oxygen = [float(item.get('blood_oxygen', 0)) for item in health_data if item.get('blood_oxygen')]
|
||||
temperatures = [float(item.get('temperature', 0)) for item in health_data if item.get('temperature')]
|
||||
steps = [int(item.get('step', 0)) for item in health_data if item.get('step')]
|
||||
|
||||
# 计算健康评分
|
||||
health_score = calculate_health_score(heart_rates, blood_oxygen, temperatures, steps)
|
||||
|
||||
# 生成健康预测
|
||||
health_prediction = generate_health_prediction(heart_rates, blood_oxygen, temperatures, analysis_days)
|
||||
|
||||
# 生成健康建议
|
||||
health_advice = generate_health_advice(heart_rates, blood_oxygen, temperatures, steps)
|
||||
|
||||
return {
|
||||
'healthScore': health_score,
|
||||
'healthPrediction': health_prediction,
|
||||
'healthAdvice': health_advice
|
||||
}
|
||||
|
||||
def calculate_health_score(heart_rates, blood_oxygen, temperatures, steps):
|
||||
"""计算综合健康评分"""
|
||||
score = 0
|
||||
factors = []
|
||||
|
||||
# 心率评分 (30分)
|
||||
if heart_rates:
|
||||
avg_hr = sum(heart_rates) / len(heart_rates)
|
||||
if 60 <= avg_hr <= 100:
|
||||
hr_score = 30
|
||||
elif 50 <= avg_hr < 60 or 100 < avg_hr <= 110:
|
||||
hr_score = 25
|
||||
else:
|
||||
hr_score = 15
|
||||
score += hr_score
|
||||
factors.append(f'心率: {avg_hr:.1f}bpm')
|
||||
|
||||
# 血氧评分 (25分)
|
||||
if blood_oxygen:
|
||||
avg_spo2 = sum(blood_oxygen) / len(blood_oxygen)
|
||||
if avg_spo2 >= 95:
|
||||
spo2_score = 25
|
||||
elif avg_spo2 >= 90:
|
||||
spo2_score = 20
|
||||
else:
|
||||
spo2_score = 10
|
||||
score += spo2_score
|
||||
factors.append(f'血氧: {avg_spo2:.1f}%')
|
||||
|
||||
# 体温评分 (20分)
|
||||
if temperatures:
|
||||
avg_temp = sum(temperatures) / len(temperatures)
|
||||
if 36.0 <= avg_temp <= 37.5:
|
||||
temp_score = 20
|
||||
else:
|
||||
temp_score = 15
|
||||
score += temp_score
|
||||
factors.append(f'体温: {avg_temp:.1f}°C')
|
||||
|
||||
# 运动评分 (25分)
|
||||
if steps:
|
||||
avg_steps = sum(steps) / len(steps)
|
||||
if avg_steps >= 8000:
|
||||
step_score = 25
|
||||
elif avg_steps >= 5000:
|
||||
step_score = 20
|
||||
elif avg_steps >= 3000:
|
||||
step_score = 15
|
||||
else:
|
||||
step_score = 10
|
||||
score += step_score
|
||||
factors.append(f'日均步数: {avg_steps:.0f}步')
|
||||
|
||||
# 确定健康等级
|
||||
if score >= 85:
|
||||
category = '优秀'
|
||||
elif score >= 70:
|
||||
category = '良好'
|
||||
elif score >= 55:
|
||||
category = '一般'
|
||||
else:
|
||||
category = '需关注'
|
||||
|
||||
return {
|
||||
'overall': min(100, score),
|
||||
'category': category,
|
||||
'factors': factors
|
||||
}
|
||||
|
||||
def generate_health_prediction(heart_rates, blood_oxygen, temperatures, days):
|
||||
"""生成健康趋势预测"""
|
||||
if not heart_rates or len(heart_rates) < 3:
|
||||
return {
|
||||
'trend': '数据不足',
|
||||
'confidence': 'low',
|
||||
'details': '需要更多数据进行趋势分析'
|
||||
}
|
||||
|
||||
# 简单的趋势分析
|
||||
recent_data = heart_rates[-7:] if len(heart_rates) >= 7 else heart_rates
|
||||
early_data = heart_rates[:7] if len(heart_rates) >= 14 else heart_rates[:len(heart_rates)//2]
|
||||
|
||||
if not early_data:
|
||||
early_data = recent_data[:len(recent_data)//2]
|
||||
|
||||
recent_avg = sum(recent_data) / len(recent_data)
|
||||
early_avg = sum(early_data) / len(early_data)
|
||||
|
||||
if recent_avg > early_avg + 5:
|
||||
trend = '心率呈上升趋势'
|
||||
confidence = 'medium'
|
||||
elif recent_avg < early_avg - 5:
|
||||
trend = '心率呈下降趋势'
|
||||
confidence = 'medium'
|
||||
else:
|
||||
trend = '心率保持稳定'
|
||||
confidence = 'high'
|
||||
|
||||
return {
|
||||
'trend': trend,
|
||||
'confidence': confidence,
|
||||
'details': f'基于{days}天数据分析,共{len(heart_rates)}个数据点'
|
||||
}
|
||||
|
||||
def generate_health_advice(heart_rates, blood_oxygen, temperatures, steps):
|
||||
"""生成个性化健康建议"""
|
||||
advice = []
|
||||
|
||||
# 心率建议
|
||||
if heart_rates:
|
||||
avg_hr = sum(heart_rates) / len(heart_rates)
|
||||
if avg_hr > 100:
|
||||
advice.append('您的平均心率偏高,建议减少剧烈运动,保持放松心态')
|
||||
elif avg_hr < 60:
|
||||
advice.append('您的心率偏低,建议适当增加有氧运动')
|
||||
else:
|
||||
advice.append('您的心率正常,请继续保持良好的运动习惯')
|
||||
|
||||
# 血氧建议
|
||||
if blood_oxygen:
|
||||
avg_spo2 = sum(blood_oxygen) / len(blood_oxygen)
|
||||
if avg_spo2 < 95:
|
||||
advice.append('血氧饱和度偏低,建议多做深呼吸练习,必要时咨询医生')
|
||||
else:
|
||||
advice.append('血氧水平良好,请保持规律的呼吸练习')
|
||||
|
||||
# 运动建议
|
||||
if steps:
|
||||
avg_steps = sum(steps) / len(steps)
|
||||
if avg_steps < 5000:
|
||||
advice.append('日常活动量不足,建议每天至少步行8000步')
|
||||
elif avg_steps > 15000:
|
||||
advice.append('运动量很充足,注意适度休息避免过度疲劳')
|
||||
else:
|
||||
advice.append('运动量适中,请继续保持')
|
||||
|
||||
# 通用建议
|
||||
advice.extend([
|
||||
'保持规律作息,确保充足睡眠',
|
||||
'注意饮食均衡,多吃蔬菜水果',
|
||||
'定期监测健康数据,及时发现异常'
|
||||
])
|
||||
|
||||
return advice[:5] # 最多返回5条建议
|
||||
|
||||
@app.route('/api/health/trends/analysis', methods=['GET'])
|
||||
def get_health_trends_analysis():
|
||||
"""健康数据趋势分析接口:提供前端图表所需的结构化数据"""
|
||||
try:
|
||||
deviceSn = request.args.get('deviceSn')
|
||||
timeRange = request.args.get('timeRange', '24h') # 1h, 6h, 24h, 7d, 30d
|
||||
|
||||
if not deviceSn:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'deviceSn参数是必需的'
|
||||
}), 400
|
||||
|
||||
# 获取用户ID和组织ID
|
||||
from .device import fetch_user_org_by_deviceSn
|
||||
userId, orgId = fetch_user_org_by_deviceSn(deviceSn)
|
||||
|
||||
if not userId or not orgId:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '未找到设备对应的用户信息'
|
||||
}), 404
|
||||
|
||||
# 根据时间范围计算起止时间
|
||||
end_date = datetime.now()
|
||||
time_ranges = {
|
||||
'1h': timedelta(hours=1),
|
||||
'6h': timedelta(hours=6),
|
||||
'24h': timedelta(hours=24),
|
||||
'7d': timedelta(days=7),
|
||||
'30d': timedelta(days=30)
|
||||
}
|
||||
|
||||
start_date = end_date - time_ranges.get(timeRange, timedelta(hours=24))
|
||||
|
||||
# 使用get_all_health_data_optimized获取健康数据
|
||||
health_result = get_all_health_data_optimized(
|
||||
orgId=orgId,
|
||||
userId=userId,
|
||||
startDate=start_date.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
endDate=end_date.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
latest_only=False
|
||||
)
|
||||
|
||||
if not health_result.get('success'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '获取健康数据失败'
|
||||
}), 500
|
||||
|
||||
health_data = health_result.get('data', {}).get('healthData', [])
|
||||
|
||||
# 进行趋势分析并格式化为前端所需的数据结构
|
||||
trends_analysis = analyze_health_trends_comprehensive(health_data, timeRange)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'deviceSn': deviceSn,
|
||||
'timeRange': timeRange,
|
||||
'startDate': start_date.isoformat(),
|
||||
'endDate': end_date.isoformat(),
|
||||
'dataPoints': len(health_data),
|
||||
**trends_analysis
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"健康趋势分析接口错误: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
def analyze_health_trends_comprehensive(health_data, timeRange):
|
||||
"""综合健康趋势分析算法"""
|
||||
if not health_data:
|
||||
return generate_fallback_trends_data(timeRange)
|
||||
|
||||
# 按时间排序数据
|
||||
health_data.sort(key=lambda x: x.get('timestamp', ''))
|
||||
|
||||
# 提取各项指标的时间序列数据
|
||||
heart_rates = []
|
||||
blood_oxygen = []
|
||||
temperatures = []
|
||||
blood_pressure_high = []
|
||||
blood_pressure_low = []
|
||||
steps = []
|
||||
timestamps = []
|
||||
|
||||
for item in health_data:
|
||||
timestamp = item.get('timestamp')
|
||||
if timestamp:
|
||||
timestamps.append(timestamp)
|
||||
heart_rates.append(float(item.get('heart_rate', 0)) if item.get('heart_rate') else None)
|
||||
blood_oxygen.append(float(item.get('blood_oxygen', 0)) if item.get('blood_oxygen') else None)
|
||||
temperatures.append(float(item.get('temperature', 0)) if item.get('temperature') else None)
|
||||
blood_pressure_high.append(float(item.get('pressure_high', 0)) if item.get('pressure_high') else None)
|
||||
blood_pressure_low.append(float(item.get('pressure_low', 0)) if item.get('pressure_low') else None)
|
||||
steps.append(int(item.get('step', 0)) if item.get('step') else None)
|
||||
|
||||
# 执行趋势分析算法
|
||||
trends_result = calculate_health_trends(
|
||||
timestamps, heart_rates, blood_oxygen, temperatures,
|
||||
blood_pressure_high, blood_pressure_low, steps, timeRange
|
||||
)
|
||||
|
||||
return trends_result
|
||||
|
||||
def calculate_health_trends(timestamps, heart_rates, blood_oxygen, temperatures,
|
||||
bp_high, bp_low, steps, timeRange):
|
||||
"""标准健康数据趋势分析算法"""
|
||||
|
||||
# 1. 数据预处理和清洗
|
||||
def clean_data(values):
|
||||
"""清洗数据,移除None值并进行异常值检测"""
|
||||
clean_values = [v for v in values if v is not None and v > 0]
|
||||
if len(clean_values) < 2:
|
||||
return clean_values
|
||||
|
||||
# 使用四分位数方法检测异常值
|
||||
clean_values.sort()
|
||||
q1 = clean_values[len(clean_values)//4]
|
||||
q3 = clean_values[3*len(clean_values)//4]
|
||||
iqr = q3 - q1
|
||||
lower_bound = q1 - 1.5 * iqr
|
||||
upper_bound = q3 + 1.5 * iqr
|
||||
|
||||
return [v for v in clean_values if lower_bound <= v <= upper_bound]
|
||||
|
||||
# 清洗各项数据
|
||||
clean_hr = clean_data(heart_rates)
|
||||
clean_spo2 = clean_data(blood_oxygen)
|
||||
clean_temp = clean_data(temperatures)
|
||||
clean_bp_h = clean_data(bp_high)
|
||||
clean_bp_l = clean_data(bp_low)
|
||||
clean_steps = clean_data(steps)
|
||||
|
||||
# 2. 时间序列数据格式化(ECharts格式)
|
||||
def format_time_series(timestamps, values, default_value=0):
|
||||
"""格式化为ECharts时间序列数据"""
|
||||
series_data = []
|
||||
for i, timestamp in enumerate(timestamps):
|
||||
if i < len(values) and values[i] is not None:
|
||||
series_data.append([timestamp, values[i]])
|
||||
else:
|
||||
series_data.append([timestamp, default_value])
|
||||
return series_data
|
||||
|
||||
chart_data = {
|
||||
'heartRate': format_time_series(timestamps, heart_rates, 75),
|
||||
'bloodOxygen': format_time_series(timestamps, blood_oxygen, 98),
|
||||
'temperature': format_time_series(timestamps, temperatures, 36.5),
|
||||
'bloodPressureHigh': format_time_series(timestamps, bp_high, 120),
|
||||
'bloodPressureLow': format_time_series(timestamps, bp_low, 80),
|
||||
'steps': format_time_series(timestamps, steps, 0)
|
||||
}
|
||||
|
||||
# 3. 统计分析
|
||||
def calculate_stats(values):
|
||||
"""计算基本统计指标"""
|
||||
if not values:
|
||||
return {'avg': 0, 'min': 0, 'max': 0, 'trend': 'stable', 'change': 0}
|
||||
|
||||
avg = sum(values) / len(values)
|
||||
min_val = min(values)
|
||||
max_val = max(values)
|
||||
|
||||
# 趋势计算:比较前半段和后半段的平均值
|
||||
if len(values) >= 4:
|
||||
mid_point = len(values) // 2
|
||||
first_half_avg = sum(values[:mid_point]) / mid_point
|
||||
second_half_avg = sum(values[mid_point:]) / (len(values) - mid_point)
|
||||
change_percent = ((second_half_avg - first_half_avg) / first_half_avg) * 100
|
||||
|
||||
if change_percent > 5:
|
||||
trend = 'rising'
|
||||
elif change_percent < -5:
|
||||
trend = 'falling'
|
||||
else:
|
||||
trend = 'stable'
|
||||
else:
|
||||
trend = 'stable'
|
||||
change_percent = 0
|
||||
|
||||
return {
|
||||
'avg': round(avg, 2),
|
||||
'min': min_val,
|
||||
'max': max_val,
|
||||
'trend': trend,
|
||||
'change': round(change_percent, 2)
|
||||
}
|
||||
|
||||
# 计算各项指标的统计信息
|
||||
stats = {
|
||||
'heartRate': calculate_stats(clean_hr),
|
||||
'bloodOxygen': calculate_stats(clean_spo2),
|
||||
'temperature': calculate_stats(clean_temp),
|
||||
'bloodPressureHigh': calculate_stats(clean_bp_h),
|
||||
'bloodPressureLow': calculate_stats(clean_bp_l),
|
||||
'steps': calculate_stats(clean_steps)
|
||||
}
|
||||
|
||||
# 4. 健康状态评估
|
||||
def assess_health_status(stats):
|
||||
"""根据统计数据评估健康状态"""
|
||||
score = 100
|
||||
warnings = []
|
||||
|
||||
# 心率评估
|
||||
hr_avg = stats['heartRate']['avg']
|
||||
if hr_avg > 100 or hr_avg < 60:
|
||||
score -= 15
|
||||
warnings.append(f"心率异常: 平均{hr_avg}bpm")
|
||||
|
||||
# 血氧评估
|
||||
spo2_avg = stats['bloodOxygen']['avg']
|
||||
if spo2_avg < 95:
|
||||
score -= 20
|
||||
warnings.append(f"血氧偏低: 平均{spo2_avg}%")
|
||||
|
||||
# 体温评估
|
||||
temp_avg = stats['temperature']['avg']
|
||||
if temp_avg > 37.5 or temp_avg < 36.0:
|
||||
score -= 10
|
||||
warnings.append(f"体温异常: 平均{temp_avg}°C")
|
||||
|
||||
# 血压评估
|
||||
bp_h_avg = stats['bloodPressureHigh']['avg']
|
||||
bp_l_avg = stats['bloodPressureLow']['avg']
|
||||
if bp_h_avg > 140 or bp_l_avg > 90:
|
||||
score -= 15
|
||||
warnings.append(f"血压偏高: {bp_h_avg}/{bp_l_avg}mmHg")
|
||||
|
||||
# 运动评估
|
||||
steps_avg = stats['steps']['avg']
|
||||
if steps_avg < 5000:
|
||||
score -= 10
|
||||
warnings.append(f"运动不足: 日均{steps_avg}步")
|
||||
|
||||
return {
|
||||
'overallScore': max(0, score),
|
||||
'status': 'excellent' if score >= 90 else 'good' if score >= 75 else 'fair' if score >= 60 else 'poor',
|
||||
'warnings': warnings
|
||||
}
|
||||
|
||||
health_assessment = assess_health_status(stats)
|
||||
|
||||
# 5. 趋势预测(简单线性回归)
|
||||
def predict_trends(values, periods=5):
|
||||
"""使用简单线性回归预测未来趋势"""
|
||||
if len(values) < 3:
|
||||
return []
|
||||
|
||||
# 简单的线性回归预测
|
||||
n = len(values)
|
||||
x_vals = list(range(n))
|
||||
y_vals = values
|
||||
|
||||
# 计算回归系数
|
||||
x_mean = sum(x_vals) / n
|
||||
y_mean = sum(y_vals) / n
|
||||
|
||||
numerator = sum((x_vals[i] - x_mean) * (y_vals[i] - y_mean) for i in range(n))
|
||||
denominator = sum((x_vals[i] - x_mean) ** 2 for i in range(n))
|
||||
|
||||
if denominator == 0:
|
||||
slope = 0
|
||||
else:
|
||||
slope = numerator / denominator
|
||||
|
||||
intercept = y_mean - slope * x_mean
|
||||
|
||||
# 预测未来值
|
||||
predictions = []
|
||||
for i in range(periods):
|
||||
pred_x = n + i
|
||||
pred_y = slope * pred_x + intercept
|
||||
predictions.append(round(pred_y, 2))
|
||||
|
||||
return predictions
|
||||
|
||||
predictions = {
|
||||
'heartRate': predict_trends(clean_hr),
|
||||
'bloodOxygen': predict_trends(clean_spo2),
|
||||
'temperature': predict_trends(clean_temp)
|
||||
}
|
||||
|
||||
return {
|
||||
'chartData': chart_data,
|
||||
'statistics': stats,
|
||||
'healthAssessment': health_assessment,
|
||||
'predictions': predictions,
|
||||
'timeRange': timeRange,
|
||||
'analysisTimestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def generate_fallback_trends_data(timeRange):
|
||||
"""生成备用趋势数据"""
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 根据时间范围生成相应数量的数据点
|
||||
data_points = {
|
||||
'1h': 12, # 每5分钟一个点
|
||||
'6h': 36, # 每10分钟一个点
|
||||
'24h': 48, # 每30分钟一个点
|
||||
'7d': 168, # 每小时一个点
|
||||
'30d': 720 # 每小时一个点
|
||||
}
|
||||
|
||||
points = data_points.get(timeRange, 48)
|
||||
now = datetime.now()
|
||||
|
||||
# 生成模拟时间序列数据
|
||||
chart_data = {
|
||||
'heartRate': [],
|
||||
'bloodOxygen': [],
|
||||
'temperature': [],
|
||||
'bloodPressureHigh': [],
|
||||
'bloodPressureLow': [],
|
||||
'steps': []
|
||||
}
|
||||
|
||||
for i in range(points):
|
||||
timestamp = (now - timedelta(minutes=30*i)).isoformat()
|
||||
chart_data['heartRate'].append([timestamp, random.randint(65, 90)])
|
||||
chart_data['bloodOxygen'].append([timestamp, random.randint(96, 100)])
|
||||
chart_data['temperature'].append([timestamp, round(random.uniform(36.2, 37.0), 1)])
|
||||
chart_data['bloodPressureHigh'].append([timestamp, random.randint(110, 130)])
|
||||
chart_data['bloodPressureLow'].append([timestamp, random.randint(70, 85)])
|
||||
chart_data['steps'].append([timestamp, random.randint(100, 500)])
|
||||
|
||||
return {
|
||||
'chartData': chart_data,
|
||||
'statistics': {
|
||||
'heartRate': {'avg': 75, 'min': 65, 'max': 90, 'trend': 'stable', 'change': 0},
|
||||
'bloodOxygen': {'avg': 98, 'min': 96, 'max': 100, 'trend': 'stable', 'change': 0},
|
||||
'temperature': {'avg': 36.6, 'min': 36.2, 'max': 37.0, 'trend': 'stable', 'change': 0}
|
||||
},
|
||||
'healthAssessment': {
|
||||
'overallScore': 85,
|
||||
'status': 'good',
|
||||
'warnings': []
|
||||
},
|
||||
'predictions': {
|
||||
'heartRate': [75, 76, 74, 75, 77],
|
||||
'bloodOxygen': [98, 98, 99, 98, 98],
|
||||
'temperature': [36.6, 36.7, 36.5, 36.6, 36.8]
|
||||
}
|
||||
}
|
||||
|
||||
@app.route('/health_data/page', methods=['GET']) #健康数据分页接口
|
||||
def health_data_page():
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
home = /opt/homebrew/opt/[email protected]/bin
|
||||
home = /opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.13.5
|
||||
executable = /opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13
|
||||
command = /opt/homebrew/opt/[email protected]/bin/python3.13 -m venv /Users/brunogao/work/codes/93/release/ljwx-bigscreen/bigscreen/bigscreen
|
||||
command = /Users/brunogao/work/codes/93/release/ljwx-bigscreen/bigscreen/bigScreen/bin/python3 -m venv /Users/brunogao/work/codes/93/release/ljwx-bigscreen/bigscreen/bigscreen
|
||||
|
||||
-5
@@ -13,33 +13,28 @@ import java.sql.*;
|
||||
public class GeoJsonTypeHandler extends BaseTypeHandler<String> {
|
||||
|
||||
public GeoJsonTypeHandler() {
|
||||
System.out.println("GeoJsonTypeHandler has been initialized.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||||
System.out.println("setNonNullParameter called with parameter: " + parameter);
|
||||
ps.setString(i, parameter); // 如果需要插入数据,传递 GeoJSON 字符串
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String result = rs.getString(columnName);
|
||||
System.out.println("getNullableResult called with columnName: " + columnName + ", result: " + result);
|
||||
return result; // 从数据库读取 GeoJSON 数据
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String result = rs.getString(columnIndex);
|
||||
System.out.println("getNullableResult called with columnIndex: " + columnIndex + ", result: " + result);
|
||||
return result; // 从数据库读取 GeoJSON 数据
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String result = cs.getString(columnIndex);
|
||||
System.out.println("getNullableResult called with columnIndex: " + columnIndex + ", result: " + result);
|
||||
return result; // 从数据库读取 GeoJSON 数据
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# API 自动化测试工具
|
||||
|
||||
用于测试 `upload_health_data`、`upload_device_info`、`upload_common_event` 三个接口的功能和性能。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 🔗 从数据库读取真实的用户和设备数据
|
||||
- 🧪 功能测试:验证接口的基本功能
|
||||
- ⚡ 性能测试:并发测试,统计响应时间
|
||||
- 📊 详细的测试报告生成
|
||||
- 🎯 支持自定义测试参数
|
||||
|
||||
## 环境要求
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
### 1. 数据库配置
|
||||
|
||||
修改 `db_config.json` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"database": "ljwx",
|
||||
"user": "root",
|
||||
"password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. API 地址
|
||||
|
||||
默认测试地址:`http://192.168.1.83:5001`
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 简单运行
|
||||
|
||||
```bash
|
||||
python run_tests.py
|
||||
```
|
||||
|
||||
### 高级用法
|
||||
|
||||
```bash
|
||||
# 仅功能测试
|
||||
python api_tester.py --mode functional
|
||||
|
||||
# 仅性能测试
|
||||
python api_tester.py --mode performance --concurrent 10 --requests 20
|
||||
|
||||
# 指定 API 地址
|
||||
python api_tester.py --url http://192.168.1.83:5001 --mode both
|
||||
|
||||
# 指定输出文件
|
||||
python api_tester.py --output my_test_report.txt
|
||||
```
|
||||
|
||||
### 命令行参数
|
||||
|
||||
- `--url`: API 基础地址 (默认: http://192.168.1.83:5001)
|
||||
- `--mode`: 测试模式 (functional|performance|both, 默认: both)
|
||||
- `--concurrent`: 性能测试并发用户数 (默认: 5)
|
||||
- `--requests`: 每个用户的请求数 (默认: 10)
|
||||
- `--output`: 输出报告文件名
|
||||
|
||||
## 测试接口
|
||||
|
||||
### 1. upload_health_data
|
||||
健康数据上传接口,测试数据包括:
|
||||
- 心率、血氧、体温、步数等健康指标
|
||||
- GPS 位置信息
|
||||
- 血压数据
|
||||
- 时间戳等
|
||||
|
||||
### 2. upload_device_info
|
||||
设备信息上传接口,测试数据包括:
|
||||
- 设备系统版本
|
||||
- 网络地址信息
|
||||
- 电池状态
|
||||
- 设备状态等
|
||||
|
||||
### 3. upload_common_event
|
||||
通用事件上传接口,测试数据包括:
|
||||
- 各种设备事件类型
|
||||
- 事件值和设备序列号
|
||||
- 位置信息
|
||||
- 关联的健康数据
|
||||
|
||||
## 测试报告
|
||||
|
||||
测试完成后会生成详细报告,包含:
|
||||
|
||||
- 📈 测试概要(成功率、响应时间等)
|
||||
- 🎯 各接口详细统计
|
||||
- ⏱️ 响应时间分析(平均值、最值、中位数)
|
||||
- ❌ 错误详情
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `api_tester.py` - 主测试工具
|
||||
- `db_config.py` - 数据库连接模块
|
||||
- `run_tests.py` - 简化运行脚本
|
||||
- `db_config.json` - 数据库配置文件
|
||||
- `requirements.txt` - Python 依赖包
|
||||
|
||||
## 数据来源
|
||||
|
||||
- 用户数据:从 `sys_user` 表获取
|
||||
- 设备数据:从 `t_device_info` 表获取
|
||||
- 自动生成符合接口规范的测试数据
|
||||
@@ -0,0 +1,78 @@
|
||||
================================================================================
|
||||
API 测试报告
|
||||
================================================================================
|
||||
测试时间: 2025-09-07T18:01:43.827257
|
||||
测试地址: http://192.168.1.83:5001
|
||||
测试接口数: 6
|
||||
总请求数: 60
|
||||
成功率: 51.67%
|
||||
平均响应时间: 14.53ms
|
||||
|
||||
接口详情:
|
||||
--------------------------------------------------
|
||||
|
||||
upload_health_data:
|
||||
总请求数: 5
|
||||
成功请求数: 5
|
||||
成功率: 100.00%
|
||||
平均响应时间: 12.43ms
|
||||
最小响应时间: 9.10ms
|
||||
最大响应时间: 13.76ms
|
||||
中位数响应时间: 13.39ms
|
||||
|
||||
upload_device_info:
|
||||
总请求数: 5
|
||||
成功请求数: 5
|
||||
成功率: 100.00%
|
||||
平均响应时间: 8.83ms
|
||||
最小响应时间: 6.03ms
|
||||
最大响应时间: 10.78ms
|
||||
中位数响应时间: 9.58ms
|
||||
|
||||
upload_common_event:
|
||||
总请求数: 5
|
||||
成功请求数: 0
|
||||
成功率: 0.00%
|
||||
平均响应时间: 14.76ms
|
||||
最小响应时间: 9.88ms
|
||||
最大响应时间: 25.84ms
|
||||
中位数响应时间: 12.67ms
|
||||
错误数: 5
|
||||
错误 1: Unknown error
|
||||
错误 2: Unknown error
|
||||
错误 3: Unknown error
|
||||
|
||||
upload_health_data_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 5
|
||||
成功率: 33.33%
|
||||
平均响应时间: 18.53ms
|
||||
最小响应时间: 7.92ms
|
||||
最大响应时间: 112.56ms
|
||||
中位数响应时间: 11.17ms
|
||||
错误数: 10
|
||||
错误 1: Unknown error
|
||||
错误 2: Unknown error
|
||||
错误 3: Unknown error
|
||||
|
||||
upload_device_info_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 15
|
||||
成功率: 100.00%
|
||||
平均响应时间: 12.20ms
|
||||
最小响应时间: 5.85ms
|
||||
最大响应时间: 44.16ms
|
||||
中位数响应时间: 10.11ms
|
||||
|
||||
upload_common_event_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 1
|
||||
成功率: 6.67%
|
||||
平均响应时间: 15.39ms
|
||||
最小响应时间: 3.99ms
|
||||
最大响应时间: 72.56ms
|
||||
中位数响应时间: 9.77ms
|
||||
错误数: 14
|
||||
错误 1: Unknown error
|
||||
错误 2: Unknown error
|
||||
错误 3: Unknown error
|
||||
@@ -0,0 +1,109 @@
|
||||
================================================================================
|
||||
API 测试报告
|
||||
================================================================================
|
||||
测试时间: 2025-09-07T18:03:36.894309
|
||||
测试地址: http://192.168.1.83:5001
|
||||
测试接口数: 6
|
||||
总请求数: 60
|
||||
成功率: 55.00%
|
||||
平均响应时间: 15.66ms
|
||||
|
||||
接口详情:
|
||||
--------------------------------------------------
|
||||
|
||||
upload_health_data:
|
||||
总请求数: 5
|
||||
成功请求数: 4
|
||||
成功率: 80.00%
|
||||
平均响应时间: 13.78ms
|
||||
最小响应时间: 9.33ms
|
||||
最大响应时间: 21.51ms
|
||||
中位数响应时间: 11.48ms
|
||||
错误数: 1
|
||||
错误 1: HTTP 404: {
|
||||
"message": "\u8bbe\u5907\u5bf9\u5e94\u7528\u6237\u672a\u627e\u5230",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
|
||||
upload_device_info:
|
||||
总请求数: 5
|
||||
成功请求数: 5
|
||||
成功率: 100.00%
|
||||
平均响应时间: 9.53ms
|
||||
最小响应时间: 7.82ms
|
||||
最大响应时间: 12.13ms
|
||||
中位数响应时间: 8.93ms
|
||||
|
||||
upload_common_event:
|
||||
总请求数: 5
|
||||
成功请求数: 4
|
||||
成功率: 80.00%
|
||||
平均响应时间: 47.52ms
|
||||
最小响应时间: 17.66ms
|
||||
最大响应时间: 79.72ms
|
||||
中位数响应时间: 42.09ms
|
||||
错误数: 1
|
||||
错误 1: HTTP 400: {
|
||||
"message": "\u672a\u627e\u5230TEST_DEVICE_003\u7684\u7ec4\u7ec7\u6216\u7528\u6237",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
|
||||
upload_health_data_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 0
|
||||
成功率: 0.00%
|
||||
平均响应时间: 10.56ms
|
||||
最小响应时间: 5.54ms
|
||||
最大响应时间: 19.06ms
|
||||
中位数响应时间: 8.85ms
|
||||
错误数: 15
|
||||
错误 1: HTTP 404: {
|
||||
"message": "\u8bbe\u5907\u5bf9\u5e94\u7528\u6237\u672a\u627e\u5230",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
错误 2: HTTP 404: {
|
||||
"message": "\u8bbe\u5907\u5bf9\u5e94\u7528\u6237\u672a\u627e\u5230",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
错误 3: HTTP 404: {
|
||||
"message": "\u8bbe\u5907\u5bf9\u5e94\u7528\u6237\u672a\u627e\u5230",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
|
||||
upload_device_info_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 15
|
||||
成功率: 100.00%
|
||||
平均响应时间: 9.95ms
|
||||
最小响应时间: 5.62ms
|
||||
最大响应时间: 16.48ms
|
||||
中位数响应时间: 9.51ms
|
||||
|
||||
upload_common_event_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 5
|
||||
成功率: 33.33%
|
||||
平均响应时间: 18.52ms
|
||||
最小响应时间: 4.63ms
|
||||
最大响应时间: 60.33ms
|
||||
中位数响应时间: 8.43ms
|
||||
错误数: 10
|
||||
错误 1: HTTP 400: {
|
||||
"message": "\u672a\u627e\u5230TEST_DEVICE_003\u7684\u7ec4\u7ec7\u6216\u7528\u6237",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
错误 2: HTTP 400: {
|
||||
"message": "\u672a\u627e\u5230TEST_DEVICE_003\u7684\u7ec4\u7ec7\u6216\u7528\u6237",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
错误 3: HTTP 400: {
|
||||
"message": "\u672a\u627e\u5230TEST_DEVICE_003\u7684\u7ec4\u7ec7\u6216\u7528\u6237",
|
||||
"status": "error"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
================================================================================
|
||||
API 测试报告
|
||||
================================================================================
|
||||
测试时间: 2025-09-07T18:05:33.085746
|
||||
测试地址: http://192.168.1.83:5001
|
||||
测试接口数: 6
|
||||
总请求数: 57
|
||||
成功率: 100.00%
|
||||
平均响应时间: 25.38ms
|
||||
|
||||
接口详情:
|
||||
--------------------------------------------------
|
||||
|
||||
upload_health_data:
|
||||
总请求数: 4
|
||||
成功请求数: 4
|
||||
成功率: 100.00%
|
||||
平均响应时间: 12.39ms
|
||||
最小响应时间: 8.65ms
|
||||
最大响应时间: 14.58ms
|
||||
中位数响应时间: 13.16ms
|
||||
|
||||
upload_device_info:
|
||||
总请求数: 4
|
||||
成功请求数: 4
|
||||
成功率: 100.00%
|
||||
平均响应时间: 7.63ms
|
||||
最小响应时间: 6.97ms
|
||||
最大响应时间: 8.52ms
|
||||
中位数响应时间: 7.53ms
|
||||
|
||||
upload_common_event:
|
||||
总请求数: 4
|
||||
成功请求数: 4
|
||||
成功率: 100.00%
|
||||
平均响应时间: 53.50ms
|
||||
最小响应时间: 28.43ms
|
||||
最大响应时间: 62.55ms
|
||||
中位数响应时间: 61.52ms
|
||||
|
||||
upload_health_data_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 15
|
||||
成功率: 100.00%
|
||||
平均响应时间: 22.60ms
|
||||
最小响应时间: 7.78ms
|
||||
最大响应时间: 90.82ms
|
||||
中位数响应时间: 17.25ms
|
||||
|
||||
upload_device_info_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 15
|
||||
成功率: 100.00%
|
||||
平均响应时间: 11.88ms
|
||||
最小响应时间: 5.48ms
|
||||
最大响应时间: 52.32ms
|
||||
中位数响应时间: 7.67ms
|
||||
|
||||
upload_common_event_performance:
|
||||
总请求数: 15
|
||||
成功请求数: 15
|
||||
成功率: 100.00%
|
||||
平均响应时间: 42.34ms
|
||||
最小响应时间: 12.13ms
|
||||
最大响应时间: 85.21ms
|
||||
中位数响应时间: 43.93ms
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
import threading
|
||||
import statistics
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import argparse
|
||||
from db_config import load_db_config
|
||||
|
||||
class APITester:
|
||||
def __init__(self, base_url: str = "http://192.168.1.83:5001"):
|
||||
self.base_url = base_url
|
||||
self.db_config = load_db_config()
|
||||
self.results = []
|
||||
|
||||
def generate_health_data(self, device_sn: str) -> Dict[str, Any]:
|
||||
now = datetime.now()
|
||||
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"deviceSn": device_sn,
|
||||
"heart_rate": random.randint(60, 120),
|
||||
"blood_oxygen": random.randint(95, 100) if random.random() > 0.3 else 0,
|
||||
"body_temperature": f"{random.uniform(36.0, 37.5):.1f}",
|
||||
"step": random.randint(0, 15000),
|
||||
"distance": f"{random.uniform(0, 10):.1f}",
|
||||
"calorie": f"{random.uniform(0, 500):.1f}",
|
||||
"latitude": f"{random.uniform(22.5, 22.6):.6f}",
|
||||
"longitude": f"{random.uniform(114.0, 114.1):.6f}",
|
||||
"altitude": f"{random.uniform(0, 100):.1f}",
|
||||
"stress": random.randint(0, 100),
|
||||
"upload_method": random.choice(["wifi", "4g", "bluetooth"]),
|
||||
"blood_pressure_systolic": random.randint(110, 140),
|
||||
"blood_pressure_diastolic": random.randint(70, 90),
|
||||
"sleepData": "null",
|
||||
"exerciseDailyData": "null",
|
||||
"exerciseWeekData": "null",
|
||||
"scientificSleepData": "null",
|
||||
"workoutData": "null",
|
||||
"timestamp": timestamp
|
||||
}
|
||||
}
|
||||
|
||||
def generate_device_info(self, device_sn: str) -> Dict[str, Any]:
|
||||
now = datetime.now()
|
||||
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
return {
|
||||
"System Software Version": f"GLL-AL30BCN {random.randint(3,5)}.0.0.{random.randint(800,999)}",
|
||||
"Wifi Address": ":".join([f"{random.randint(0,255):02x}" for _ in range(6)]),
|
||||
"Bluetooth Address": ":".join([f"{random.randint(0,255):02X}" for _ in range(6)]),
|
||||
"IP Address": f"192.168.1.{random.randint(100, 254)}",
|
||||
"Network Access Mode": random.choice([1, 2, 3]),
|
||||
"SerialNumber": device_sn,
|
||||
"Device Name": f"HUAWEI WATCH B7-{random.randint(500,600)}-BF{random.randint(0,9)}",
|
||||
"IMEI": f"86615206{random.randint(10000000, 99999999)}",
|
||||
"batteryLevel": random.randint(10, 100),
|
||||
"voltage": random.randint(3500, 4500),
|
||||
"chargingStatus": random.choice(["NONE", "CHARGING", "FULL"]),
|
||||
"status": random.choice(["ACTIVE", "INACTIVE", "SLEEP"]),
|
||||
"timestamp": timestamp,
|
||||
"wearState": random.choice([0, 1])
|
||||
}
|
||||
|
||||
def generate_common_event(self, device_sn: str) -> Dict[str, Any]:
|
||||
now = datetime.now()
|
||||
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
health_data = self.generate_health_data(device_sn)
|
||||
|
||||
event_types = [
|
||||
'com.tdtech.ohos.action.WEAR_STATUS_CHANGED'
|
||||
]
|
||||
|
||||
return {
|
||||
'eventType': random.choice(event_types),
|
||||
'eventValue': str(random.choice([0, 1])),
|
||||
'deviceSn': device_sn,
|
||||
'latitude': round(random.uniform(22.5, 22.6), 6),
|
||||
'longitude': round(random.uniform(114.0, 114.1), 6),
|
||||
'altitude': random.randint(0, 100),
|
||||
'timestamp': timestamp,
|
||||
'healthData': health_data
|
||||
}
|
||||
|
||||
def make_request(self, endpoint: str, data: Dict[str, Any], timeout: int = 30) -> Dict[str, Any]:
|
||||
url = f"{self.base_url}/{endpoint}"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'API-Tester/1.0'
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=timeout)
|
||||
end_time = time.time()
|
||||
|
||||
result = {
|
||||
'endpoint': endpoint,
|
||||
'status_code': response.status_code,
|
||||
'response_time': round((end_time - start_time) * 1000, 2),
|
||||
'success': response.status_code == 200,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'request_size': len(json.dumps(data).encode('utf-8')),
|
||||
'response_size': len(response.content)
|
||||
}
|
||||
|
||||
try:
|
||||
result['response_data'] = response.json()
|
||||
except:
|
||||
result['response_data'] = response.text
|
||||
|
||||
if response.status_code != 200:
|
||||
result['error'] = f"HTTP {response.status_code}: {response.text[:200]}"
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return {
|
||||
'endpoint': endpoint,
|
||||
'status_code': 0,
|
||||
'response_time': timeout * 1000,
|
||||
'success': False,
|
||||
'error': 'Request timeout',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'endpoint': endpoint,
|
||||
'status_code': 0,
|
||||
'response_time': 0,
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def test_single_api(self, endpoint: str, device_sn: str) -> Dict[str, Any]:
|
||||
if endpoint == 'upload_health_data':
|
||||
data = self.generate_health_data(device_sn)
|
||||
elif endpoint == 'upload_device_info':
|
||||
data = self.generate_device_info(device_sn)
|
||||
elif endpoint == 'upload_common_event':
|
||||
data = self.generate_common_event(device_sn)
|
||||
else:
|
||||
raise ValueError(f"Unknown endpoint: {endpoint}")
|
||||
|
||||
return self.make_request(endpoint, data)
|
||||
|
||||
def performance_test(self, endpoint: str, device_sns: List[str],
|
||||
concurrent_users: int = 5, requests_per_user: int = 10) -> List[Dict[str, Any]]:
|
||||
results = []
|
||||
|
||||
def worker(device_sn: str, request_count: int) -> List[Dict[str, Any]]:
|
||||
worker_results = []
|
||||
for i in range(request_count):
|
||||
result = self.test_single_api(endpoint, device_sn)
|
||||
result['user_id'] = threading.current_thread().ident
|
||||
result['request_number'] = i + 1
|
||||
worker_results.append(result)
|
||||
time.sleep(random.uniform(0.1, 0.5))
|
||||
return worker_results
|
||||
|
||||
with ThreadPoolExecutor(max_workers=concurrent_users) as executor:
|
||||
futures = []
|
||||
for i in range(concurrent_users):
|
||||
device_sn = random.choice(device_sns)
|
||||
future = executor.submit(worker, device_sn, requests_per_user)
|
||||
futures.append(future)
|
||||
|
||||
for future in as_completed(futures):
|
||||
results.extend(future.result())
|
||||
|
||||
return results
|
||||
|
||||
def functional_test(self, device_sns: List[str]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
endpoints = ['upload_health_data', 'upload_device_info', 'upload_common_event']
|
||||
results = {}
|
||||
|
||||
for endpoint in endpoints:
|
||||
print(f"Testing {endpoint}...")
|
||||
endpoint_results = []
|
||||
|
||||
for device_sn in device_sns[:5]:
|
||||
result = self.test_single_api(endpoint, device_sn)
|
||||
endpoint_results.append(result)
|
||||
time.sleep(0.1)
|
||||
|
||||
results[endpoint] = endpoint_results
|
||||
|
||||
return results
|
||||
|
||||
def generate_report(self, results: Dict[str, Any], output_file: str = None):
|
||||
report = {
|
||||
'test_summary': {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'base_url': self.base_url,
|
||||
'total_endpoints_tested': 0,
|
||||
'total_requests': 0,
|
||||
'success_rate': 0,
|
||||
'average_response_time': 0
|
||||
},
|
||||
'endpoint_details': {},
|
||||
'performance_metrics': {},
|
||||
'errors': []
|
||||
}
|
||||
|
||||
all_results = []
|
||||
for endpoint, endpoint_results in results.items():
|
||||
if isinstance(endpoint_results, list):
|
||||
all_results.extend(endpoint_results)
|
||||
|
||||
successful_requests = [r for r in endpoint_results if r.get('success', False)]
|
||||
response_times = [r['response_time'] for r in endpoint_results if 'response_time' in r]
|
||||
|
||||
endpoint_stats = {
|
||||
'total_requests': len(endpoint_results),
|
||||
'successful_requests': len(successful_requests),
|
||||
'success_rate': (len(successful_requests) / len(endpoint_results)) * 100 if endpoint_results else 0,
|
||||
'average_response_time': statistics.mean(response_times) if response_times else 0,
|
||||
'min_response_time': min(response_times) if response_times else 0,
|
||||
'max_response_time': max(response_times) if response_times else 0,
|
||||
'median_response_time': statistics.median(response_times) if response_times else 0,
|
||||
'errors': [r for r in endpoint_results if not r.get('success', False)]
|
||||
}
|
||||
|
||||
report['endpoint_details'][endpoint] = endpoint_stats
|
||||
|
||||
if all_results:
|
||||
successful_results = [r for r in all_results if r.get('success', False)]
|
||||
all_response_times = [r['response_time'] for r in all_results if 'response_time' in r]
|
||||
|
||||
report['test_summary'].update({
|
||||
'total_endpoints_tested': len(results),
|
||||
'total_requests': len(all_results),
|
||||
'success_rate': (len(successful_results) / len(all_results)) * 100,
|
||||
'average_response_time': statistics.mean(all_response_times) if all_response_times else 0
|
||||
})
|
||||
|
||||
report_text = self.format_report(report)
|
||||
|
||||
if output_file:
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(report_text)
|
||||
print(f"Report saved to: {output_file}")
|
||||
|
||||
print(report_text)
|
||||
return report
|
||||
|
||||
def format_report(self, report: Dict[str, Any]) -> str:
|
||||
lines = []
|
||||
lines.append("=" * 80)
|
||||
lines.append("API 测试报告")
|
||||
lines.append("=" * 80)
|
||||
lines.append(f"测试时间: {report['test_summary']['timestamp']}")
|
||||
lines.append(f"测试地址: {report['test_summary']['base_url']}")
|
||||
lines.append(f"测试接口数: {report['test_summary']['total_endpoints_tested']}")
|
||||
lines.append(f"总请求数: {report['test_summary']['total_requests']}")
|
||||
lines.append(f"成功率: {report['test_summary']['success_rate']:.2f}%")
|
||||
lines.append(f"平均响应时间: {report['test_summary']['average_response_time']:.2f}ms")
|
||||
lines.append("")
|
||||
|
||||
lines.append("接口详情:")
|
||||
lines.append("-" * 50)
|
||||
for endpoint, stats in report['endpoint_details'].items():
|
||||
lines.append(f"\n{endpoint}:")
|
||||
lines.append(f" 总请求数: {stats['total_requests']}")
|
||||
lines.append(f" 成功请求数: {stats['successful_requests']}")
|
||||
lines.append(f" 成功率: {stats['success_rate']:.2f}%")
|
||||
lines.append(f" 平均响应时间: {stats['average_response_time']:.2f}ms")
|
||||
lines.append(f" 最小响应时间: {stats['min_response_time']:.2f}ms")
|
||||
lines.append(f" 最大响应时间: {stats['max_response_time']:.2f}ms")
|
||||
lines.append(f" 中位数响应时间: {stats['median_response_time']:.2f}ms")
|
||||
|
||||
if stats['errors']:
|
||||
lines.append(f" 错误数: {len(stats['errors'])}")
|
||||
for i, error in enumerate(stats['errors'][:3]):
|
||||
lines.append(f" 错误 {i+1}: {error.get('error', 'Unknown error')}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='API Testing Tool')
|
||||
parser.add_argument('--url', default='http://192.168.1.83:5001', help='Base URL for API')
|
||||
parser.add_argument('--mode', choices=['functional', 'performance', 'both'],
|
||||
default='both', help='Test mode')
|
||||
parser.add_argument('--concurrent', type=int, default=5, help='Concurrent users for performance test')
|
||||
parser.add_argument('--requests', type=int, default=10, help='Requests per user for performance test')
|
||||
parser.add_argument('--output', help='Output file for report')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tester = APITester(args.url)
|
||||
|
||||
if not tester.db_config.connect():
|
||||
print("数据库连接失败,请检查配置")
|
||||
return
|
||||
|
||||
devices = tester.db_config.get_devices(50)
|
||||
if not devices:
|
||||
print("未找到设备数据,请检查数据库")
|
||||
return
|
||||
|
||||
device_sns = [device['device_sn'] for device in devices]
|
||||
print(f"从数据库获取到 {len(device_sns)} 个设备序列号")
|
||||
|
||||
results = {}
|
||||
|
||||
if args.mode in ['functional', 'both']:
|
||||
print("\n开始功能测试...")
|
||||
functional_results = tester.functional_test(device_sns)
|
||||
results.update(functional_results)
|
||||
|
||||
if args.mode in ['performance', 'both']:
|
||||
print(f"\n开始性能测试 (并发用户: {args.concurrent}, 每用户请求: {args.requests})...")
|
||||
endpoints = ['upload_health_data', 'upload_device_info', 'upload_common_event']
|
||||
|
||||
for endpoint in endpoints:
|
||||
print(f"Testing {endpoint} performance...")
|
||||
perf_results = tester.performance_test(endpoint, device_sns,
|
||||
args.concurrent, args.requests)
|
||||
results[f"{endpoint}_performance"] = perf_results
|
||||
|
||||
if args.output:
|
||||
output_file = args.output
|
||||
else:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_file = f"api_test_report_{timestamp}.txt"
|
||||
|
||||
tester.generate_report(results, output_file)
|
||||
tester.db_config.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+200
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from continuous_uploader import ContinuousUploader
|
||||
|
||||
class BackgroundUploader(ContinuousUploader):
|
||||
def __init__(self, base_url: str = "http://192.168.1.83:5001", interval: int = 300):
|
||||
super().__init__(base_url, interval)
|
||||
self.progress_file = Path("progress_state.json")
|
||||
self.state = self.load_progress()
|
||||
|
||||
def load_progress(self) -> dict:
|
||||
"""加载进度状态"""
|
||||
if self.progress_file.exists():
|
||||
try:
|
||||
with open(self.progress_file, 'r', encoding='utf-8') as f:
|
||||
state = json.load(f)
|
||||
# 转换时间字符串回datetime对象
|
||||
if 'last_completed_time' in state and state['last_completed_time']:
|
||||
state['last_completed_time'] = datetime.fromisoformat(state['last_completed_time'])
|
||||
if 'start_time' in state and state['start_time']:
|
||||
state['start_time'] = datetime.fromisoformat(state['start_time'])
|
||||
return state
|
||||
except Exception as e:
|
||||
self.logger.warning(f"加载进度文件失败: {e}")
|
||||
|
||||
return {
|
||||
'mode': None,
|
||||
'total_days': 30,
|
||||
'completed_times': [],
|
||||
'last_completed_time': None,
|
||||
'start_time': None,
|
||||
'total_operations': 0,
|
||||
'completed_operations': 0
|
||||
}
|
||||
|
||||
def save_progress(self):
|
||||
"""保存进度状态"""
|
||||
try:
|
||||
state_to_save = self.state.copy()
|
||||
# 转换datetime对象为字符串
|
||||
if state_to_save.get('last_completed_time'):
|
||||
state_to_save['last_completed_time'] = state_to_save['last_completed_time'].isoformat()
|
||||
if state_to_save.get('start_time'):
|
||||
state_to_save['start_time'] = state_to_save['start_time'].isoformat()
|
||||
|
||||
with open(self.progress_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(state_to_save, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存进度失败: {e}")
|
||||
|
||||
def resume_historical_upload(self, days: int = 30):
|
||||
"""恢复历史数据上传"""
|
||||
devices = self.get_devices_with_users()
|
||||
if not devices:
|
||||
self.logger.error("未找到设备数据")
|
||||
return
|
||||
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
# 检查是否有进度可以恢复
|
||||
resume_from = None
|
||||
if self.state.get('mode') == 'historical' and self.state.get('last_completed_time'):
|
||||
resume_from = self.state['last_completed_time'] + timedelta(minutes=5)
|
||||
self.logger.info(f"🔄 恢复上传,从时间点: {resume_from}")
|
||||
else:
|
||||
resume_from = start_time
|
||||
self.state.update({
|
||||
'mode': 'historical',
|
||||
'total_days': days,
|
||||
'start_time': datetime.now(),
|
||||
'completed_times': [],
|
||||
'completed_operations': 0
|
||||
})
|
||||
self.logger.info(f"🚀 开始新的历史数据上传")
|
||||
|
||||
# 计算需要处理的时间点
|
||||
time_points = []
|
||||
current_time = resume_from
|
||||
while current_time <= end_time:
|
||||
time_points.append(current_time)
|
||||
current_time += timedelta(minutes=5)
|
||||
|
||||
total_operations = len(devices) * len(time_points) * 3
|
||||
self.state['total_operations'] = len(devices) * ((end_time - start_time).total_seconds() / 300) * 3
|
||||
|
||||
self.logger.info(f"剩余时间点: {len(time_points)}")
|
||||
self.logger.info(f"剩余操作数: {total_operations}")
|
||||
|
||||
# 开始处理
|
||||
for i, time_point in enumerate(time_points):
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
self.logger.info(f"📅 处理时间点: {time_point.strftime('%Y-%m-%d %H:%M:%S')} ({i+1}/{len(time_points)})")
|
||||
|
||||
for device in devices:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
device_sn = device['device_sn']
|
||||
user_name = device['user_name']
|
||||
|
||||
self.upload_data_for_time(device_sn, user_name, time_point)
|
||||
self.state['completed_operations'] += 3
|
||||
|
||||
# 更新进度
|
||||
self.state['last_completed_time'] = time_point
|
||||
self.state['completed_times'].append(time_point.isoformat())
|
||||
|
||||
# 每10个时间点保存一次进度
|
||||
if (i + 1) % 10 == 0:
|
||||
self.save_progress()
|
||||
progress = ((i + 1) / len(time_points)) * 100
|
||||
self.logger.info(f"📊 进度: {progress:.1f}% - 已保存检查点")
|
||||
|
||||
if self.running:
|
||||
time.sleep(0.1) # 避免过快请求
|
||||
|
||||
# 完成后清理进度文件
|
||||
if self.running: # 正常完成
|
||||
self.state['mode'] = 'completed'
|
||||
self.save_progress()
|
||||
self.logger.info("🎉 历史数据上传完成!")
|
||||
else: # 被中断
|
||||
self.save_progress()
|
||||
self.logger.info("⏸️ 上传已暂停,进度已保存")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "start":
|
||||
days = int(sys.argv[2]) if len(sys.argv) > 2 else 30
|
||||
print(f"🚀 开始/恢复历史数据上传 ({days}天)")
|
||||
|
||||
uploader = BackgroundUploader()
|
||||
uploader.running = True
|
||||
|
||||
try:
|
||||
uploader.resume_historical_upload(days)
|
||||
except KeyboardInterrupt:
|
||||
uploader.logger.info("收到中断信号,保存进度...")
|
||||
uploader.running = False
|
||||
uploader.save_progress()
|
||||
|
||||
elif command == "status":
|
||||
uploader = BackgroundUploader()
|
||||
state = uploader.state
|
||||
|
||||
print("📊 当前状态:")
|
||||
print(f"模式: {state.get('mode', 'None')}")
|
||||
|
||||
if state.get('start_time'):
|
||||
print(f"开始时间: {state['start_time']}")
|
||||
|
||||
if state.get('last_completed_time'):
|
||||
print(f"最后完成时间: {state['last_completed_time']}")
|
||||
|
||||
if state.get('total_operations', 0) > 0:
|
||||
completed = state.get('completed_operations', 0)
|
||||
total = state['total_operations']
|
||||
progress = (completed / total) * 100
|
||||
print(f"进度: {completed}/{total} ({progress:.1f}%)")
|
||||
|
||||
elif command == "reset":
|
||||
progress_file = Path("progress_state.json")
|
||||
if progress_file.exists():
|
||||
progress_file.unlink()
|
||||
print("🗑️ 进度已重置")
|
||||
else:
|
||||
print("ℹ️ 没有进度文件需要重置")
|
||||
|
||||
elif command == "continuous":
|
||||
print("🔄 开始持续上传模式")
|
||||
uploader = BackgroundUploader()
|
||||
uploader.start_continuous()
|
||||
|
||||
else:
|
||||
print("用法:")
|
||||
print(" python background_uploader.py start [天数] - 开始/恢复历史数据上传")
|
||||
print(" python background_uploader.py status - 查看进度状态")
|
||||
print(" python background_uploader.py reset - 重置进度")
|
||||
print(" python background_uploader.py continuous - 持续上传模式")
|
||||
|
||||
else:
|
||||
print("📋 后台数据上传工具")
|
||||
print("==================")
|
||||
print("用法:")
|
||||
print(" python background_uploader.py start [天数] - 开始/恢复历史数据上传")
|
||||
print(" python background_uploader.py status - 查看进度状态")
|
||||
print(" python background_uploader.py reset - 重置进度")
|
||||
print(" python background_uploader.py continuous - 持续上传模式")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
from db_config import load_db_config
|
||||
|
||||
def check_table_structure():
|
||||
db_config = load_db_config()
|
||||
|
||||
if not db_config.connect():
|
||||
print("数据库连接失败")
|
||||
return
|
||||
|
||||
try:
|
||||
cursor = db_config.connection.cursor()
|
||||
|
||||
# 查看所有表
|
||||
print("=== 查看数据库中的所有表 ===")
|
||||
cursor.execute("SHOW TABLES")
|
||||
tables = cursor.fetchall()
|
||||
print(f"找到 {len(tables)} 个表:")
|
||||
for table in tables:
|
||||
print(f" - {table[0]}")
|
||||
|
||||
# 查看 sys_user 表结构(如果存在)
|
||||
user_table = None
|
||||
device_table = None
|
||||
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
if 'user' in table_name.lower():
|
||||
user_table = table_name
|
||||
print(f"\n=== {table_name} 表结构 ===")
|
||||
cursor.execute(f"DESCRIBE {table_name}")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[0]} - {col[1]} - {col[2]} - {col[3]}")
|
||||
|
||||
if 'device' in table_name.lower():
|
||||
device_table = table_name
|
||||
print(f"\n=== {table_name} 表结构 ===")
|
||||
cursor.execute(f"DESCRIBE {table_name}")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[0]} - {col[1]} - {col[2]} - {col[3]}")
|
||||
|
||||
# 查看表中的数据量
|
||||
if user_table:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {user_table}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"\n{user_table} 表中有 {count} 条记录")
|
||||
|
||||
if count > 0:
|
||||
cursor.execute(f"SELECT * FROM {user_table} LIMIT 3")
|
||||
rows = cursor.fetchall()
|
||||
print("前3条记录:")
|
||||
for row in rows:
|
||||
print(f" {row}")
|
||||
|
||||
if device_table:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {device_table}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"\n{device_table} 表中有 {count} 条记录")
|
||||
|
||||
if count > 0:
|
||||
cursor.execute(f"SELECT * FROM {device_table} LIMIT 3")
|
||||
rows = cursor.fetchall()
|
||||
print("前3条记录:")
|
||||
for row in rows:
|
||||
print(f" {row}")
|
||||
|
||||
cursor.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"查询错误: {e}")
|
||||
finally:
|
||||
db_config.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_table_structure()
|
||||
Executable
+345
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
import threading
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from api_tester import APITester
|
||||
from db_config import load_db_config
|
||||
import argparse
|
||||
|
||||
class ContinuousUploader:
|
||||
def __init__(self, base_url: str = "http://192.168.1.83:5001", interval: int = 300):
|
||||
self.base_url = base_url
|
||||
self.interval = interval # seconds between uploads
|
||||
self.api_tester = APITester(base_url)
|
||||
self.db_config = load_db_config()
|
||||
self.running = False
|
||||
self.upload_thread = None
|
||||
|
||||
# Setup logging
|
||||
self.setup_logging()
|
||||
|
||||
# Statistics
|
||||
self.stats = {
|
||||
'total_uploads': 0,
|
||||
'successful_uploads': 0,
|
||||
'failed_uploads': 0,
|
||||
'start_time': None,
|
||||
'current_date': None,
|
||||
'devices_processed': set()
|
||||
}
|
||||
|
||||
# Signal handler for graceful shutdown
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
def setup_logging(self):
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
log_file = log_dir / f"continuous_upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info(f"日志文件: {log_file}")
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
self.logger.info(f"接收到信号 {signum},开始优雅关闭...")
|
||||
self.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def get_devices_with_users(self) -> List[Dict[str, Any]]:
|
||||
if not self.db_config.connect():
|
||||
self.logger.error("数据库连接失败")
|
||||
return []
|
||||
|
||||
user_devices = self.db_config.get_user_devices(50)
|
||||
if not user_devices:
|
||||
devices = self.db_config.get_devices(50)
|
||||
return [{'device_sn': d['device_sn'], 'user_name': 'unknown'} for d in devices if d.get('device_sn')]
|
||||
|
||||
return [{'device_sn': ud['device_sn'], 'user_name': ud['user_name']} for ud in user_devices]
|
||||
|
||||
def generate_historical_data(self, device_sn: str, target_time: datetime) -> Dict[str, Any]:
|
||||
"""为指定时间生成历史数据"""
|
||||
# 保存原来的时间生成逻辑,用指定时间替换
|
||||
original_generate_health = self.api_tester.generate_health_data
|
||||
original_generate_device = self.api_tester.generate_device_info
|
||||
original_generate_event = self.api_tester.generate_common_event
|
||||
|
||||
timestamp_str = target_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 重写生成方法以使用指定时间
|
||||
def generate_health_with_time(device_sn):
|
||||
data = original_generate_health(device_sn)
|
||||
data['data']['timestamp'] = timestamp_str
|
||||
return data
|
||||
|
||||
def generate_device_with_time(device_sn):
|
||||
data = original_generate_device(device_sn)
|
||||
data['timestamp'] = timestamp_str
|
||||
return data
|
||||
|
||||
def generate_event_with_time(device_sn):
|
||||
data = original_generate_event(device_sn)
|
||||
data['timestamp'] = timestamp_str
|
||||
data['healthData']['data']['timestamp'] = timestamp_str
|
||||
return data
|
||||
|
||||
return {
|
||||
'health_data': generate_health_with_time(device_sn),
|
||||
'device_info': generate_device_with_time(device_sn),
|
||||
'common_event': generate_event_with_time(device_sn)
|
||||
}
|
||||
|
||||
def upload_data_for_time(self, device_sn: str, user_name: str, target_time: datetime) -> Dict[str, bool]:
|
||||
"""为指定时间上传数据"""
|
||||
results = {
|
||||
'upload_health_data': False,
|
||||
'upload_device_info': False,
|
||||
'upload_common_event': False
|
||||
}
|
||||
|
||||
historical_data = self.generate_historical_data(device_sn, target_time)
|
||||
|
||||
endpoints = [
|
||||
('upload_health_data', historical_data['health_data']),
|
||||
('upload_device_info', historical_data['device_info']),
|
||||
('upload_common_event', historical_data['common_event'])
|
||||
]
|
||||
|
||||
for endpoint, data in endpoints:
|
||||
try:
|
||||
result = self.api_tester.make_request(endpoint, data, timeout=30)
|
||||
success = result.get('success', False)
|
||||
results[endpoint] = success
|
||||
|
||||
if success:
|
||||
self.logger.debug(f"✅ {endpoint} 成功 - 设备: {device_sn}, 时间: {target_time}")
|
||||
else:
|
||||
error_msg = result.get('error', 'Unknown error')
|
||||
self.logger.warning(f"❌ {endpoint} 失败 - 设备: {device_sn}, 错误: {error_msg}")
|
||||
|
||||
self.stats['total_uploads'] += 1
|
||||
if success:
|
||||
self.stats['successful_uploads'] += 1
|
||||
else:
|
||||
self.stats['failed_uploads'] += 1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ {endpoint} 异常 - 设备: {device_sn}, 异常: {e}")
|
||||
results[endpoint] = False
|
||||
self.stats['total_uploads'] += 1
|
||||
self.stats['failed_uploads'] += 1
|
||||
|
||||
return results
|
||||
|
||||
def upload_historical_data(self, days: int = 30):
|
||||
"""上传过去指定天数的历史数据"""
|
||||
devices = self.get_devices_with_users()
|
||||
if not devices:
|
||||
self.logger.error("未找到设备数据")
|
||||
return
|
||||
|
||||
self.logger.info(f"开始上传过去 {days} 天的历史数据")
|
||||
self.logger.info(f"找到 {len(devices)} 个设备")
|
||||
|
||||
# 计算时间范围
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
self.stats['start_time'] = start_time
|
||||
|
||||
# 每5分钟一个时间点
|
||||
time_points = []
|
||||
current_time = start_time
|
||||
while current_time <= end_time:
|
||||
time_points.append(current_time)
|
||||
current_time += timedelta(minutes=5)
|
||||
|
||||
total_operations = len(devices) * len(time_points) * 3 # 3个接口
|
||||
self.logger.info(f"总共需要上传 {total_operations} 次数据")
|
||||
self.logger.info(f"预计耗时: {total_operations * 0.5 / 60:.1f} 分钟")
|
||||
|
||||
# 开始上传
|
||||
completed_operations = 0
|
||||
|
||||
for time_point in time_points:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
self.stats['current_date'] = time_point.strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.logger.info(f"📅 处理时间点: {self.stats['current_date']}")
|
||||
|
||||
for device in devices:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
device_sn = device['device_sn']
|
||||
user_name = device['user_name']
|
||||
|
||||
results = self.upload_data_for_time(device_sn, user_name, time_point)
|
||||
|
||||
self.stats['devices_processed'].add(device_sn)
|
||||
completed_operations += 3
|
||||
|
||||
# 显示进度
|
||||
progress = (completed_operations / total_operations) * 100
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads'] * 100) if self.stats['total_uploads'] > 0 else 0
|
||||
|
||||
if completed_operations % 30 == 0: # 每30次操作显示一次进度
|
||||
self.logger.info(f"📊 进度: {progress:.1f}% | 成功: {self.stats['successful_uploads']} | 失败: {self.stats['failed_uploads']} | 成功率: {success_rate:.1f}%")
|
||||
|
||||
# 每个时间点之间稍微延迟,避免过快请求
|
||||
if self.running:
|
||||
time.sleep(0.1)
|
||||
|
||||
self.print_final_statistics()
|
||||
|
||||
def continuous_upload(self):
|
||||
"""持续上传当前数据(每5分钟)"""
|
||||
devices = self.get_devices_with_users()
|
||||
if not devices:
|
||||
self.logger.error("未找到设备数据")
|
||||
return
|
||||
|
||||
self.logger.info(f"开始持续数据上传,间隔: {self.interval}秒")
|
||||
self.logger.info(f"找到 {len(devices)} 个设备")
|
||||
|
||||
while self.running:
|
||||
current_time = datetime.now()
|
||||
self.stats['current_date'] = current_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
self.logger.info(f"🔄 开始上传数据 - 时间: {self.stats['current_date']}")
|
||||
|
||||
for device in devices:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
device_sn = device['device_sn']
|
||||
user_name = device['user_name']
|
||||
|
||||
results = self.upload_data_for_time(device_sn, user_name, current_time)
|
||||
|
||||
self.stats['devices_processed'].add(device_sn)
|
||||
|
||||
# 显示结果
|
||||
success_count = sum(1 for v in results.values() if v)
|
||||
self.logger.info(f"📱 设备 {device_sn} - 成功: {success_count}/3")
|
||||
|
||||
# 显示统计
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads'] * 100) if self.stats['total_uploads'] > 0 else 0
|
||||
self.logger.info(f"📊 累计 - 总上传: {self.stats['total_uploads']}, 成功: {self.stats['successful_uploads']}, 失败: {self.stats['failed_uploads']}, 成功率: {success_rate:.1f}%")
|
||||
|
||||
# 等待下次上传
|
||||
if self.running:
|
||||
self.logger.info(f"⏰ 等待 {self.interval} 秒后进行下次上传...")
|
||||
time.sleep(self.interval)
|
||||
|
||||
def print_final_statistics(self):
|
||||
"""打印最终统计信息"""
|
||||
self.logger.info("=" * 60)
|
||||
self.logger.info("📊 最终统计报告")
|
||||
self.logger.info("=" * 60)
|
||||
|
||||
if self.stats['start_time']:
|
||||
duration = datetime.now() - self.stats['start_time']
|
||||
self.logger.info(f"运行时长: {duration}")
|
||||
|
||||
self.logger.info(f"总上传次数: {self.stats['total_uploads']}")
|
||||
self.logger.info(f"成功次数: {self.stats['successful_uploads']}")
|
||||
self.logger.info(f"失败次数: {self.stats['failed_uploads']}")
|
||||
|
||||
if self.stats['total_uploads'] > 0:
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads']) * 100
|
||||
self.logger.info(f"成功率: {success_rate:.2f}%")
|
||||
|
||||
self.logger.info(f"处理设备数: {len(self.stats['devices_processed'])}")
|
||||
self.logger.info(f"设备列表: {list(self.stats['devices_processed'])}")
|
||||
|
||||
def start_historical(self, days: int = 30):
|
||||
"""启动历史数据上传"""
|
||||
self.running = True
|
||||
self.stats['start_time'] = datetime.now()
|
||||
self.logger.info("🚀 开始历史数据上传...")
|
||||
|
||||
try:
|
||||
self.upload_historical_data(days)
|
||||
except Exception as e:
|
||||
self.logger.error(f"历史数据上传出错: {e}")
|
||||
finally:
|
||||
self.running = False
|
||||
|
||||
def start_continuous(self):
|
||||
"""启动持续上传"""
|
||||
self.running = True
|
||||
self.stats['start_time'] = datetime.now()
|
||||
self.logger.info("🚀 开始持续数据上传...")
|
||||
|
||||
self.upload_thread = threading.Thread(target=self.continuous_upload)
|
||||
self.upload_thread.daemon = True
|
||||
self.upload_thread.start()
|
||||
|
||||
try:
|
||||
self.upload_thread.join()
|
||||
except KeyboardInterrupt:
|
||||
self.logger.info("收到中断信号,正在停止...")
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
"""停止上传"""
|
||||
self.logger.info("🛑 正在停止数据上传...")
|
||||
self.running = False
|
||||
|
||||
if self.upload_thread and self.upload_thread.is_alive():
|
||||
self.upload_thread.join(timeout=5)
|
||||
|
||||
self.db_config.disconnect()
|
||||
self.print_final_statistics()
|
||||
self.logger.info("✅ 上传已停止")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='持续数据上传工具')
|
||||
parser.add_argument('--url', default='http://192.168.1.83:5001', help='API基础地址')
|
||||
parser.add_argument('--mode', choices=['historical', 'continuous', 'both'],
|
||||
default='historical', help='运行模式')
|
||||
parser.add_argument('--days', type=int, default=30, help='历史数据天数')
|
||||
parser.add_argument('--interval', type=int, default=300, help='持续上传间隔(秒)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
uploader = ContinuousUploader(args.url, args.interval)
|
||||
|
||||
try:
|
||||
if args.mode == 'historical':
|
||||
uploader.start_historical(args.days)
|
||||
elif args.mode == 'continuous':
|
||||
uploader.start_continuous()
|
||||
elif args.mode == 'both':
|
||||
print("先执行历史数据上传...")
|
||||
uploader.start_historical(args.days)
|
||||
print("\n开始持续上传...")
|
||||
uploader.start_continuous()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
uploader.logger.info("收到中断信号")
|
||||
uploader.stop()
|
||||
except Exception as e:
|
||||
uploader.logger.error(f"程序异常: {e}")
|
||||
uploader.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
"database": "test",
|
||||
"user": "root",
|
||||
"password": "123456"
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
class DatabaseConfig:
|
||||
def __init__(self, host: str = 'localhost', port: int = 3306,
|
||||
database: str = 'ljwx', user: str = 'root', password: str = ''):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.database = database
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.connection = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
try:
|
||||
self.connection = mysql.connector.connect(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
database=self.database,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
charset='utf8mb4',
|
||||
collation='utf8mb4_unicode_ci'
|
||||
)
|
||||
return True
|
||||
except Error as e:
|
||||
print(f"数据库连接错误: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
if self.connection and self.connection.is_connected():
|
||||
self.connection.close()
|
||||
|
||||
def get_users(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
if not self.connection or not self.connection.is_connected():
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
try:
|
||||
cursor = self.connection.cursor(dictionary=True)
|
||||
query = "SELECT id, user_name, phone FROM sys_user WHERE is_deleted = 0 LIMIT %s"
|
||||
cursor.execute(query, (limit,))
|
||||
users = cursor.fetchall()
|
||||
cursor.close()
|
||||
return users
|
||||
except Error as e:
|
||||
print(f"查询用户错误: {e}")
|
||||
return []
|
||||
|
||||
def get_devices(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
if not self.connection or not self.connection.is_connected():
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
try:
|
||||
cursor = self.connection.cursor(dictionary=True)
|
||||
query = "SELECT serial_number as device_sn, device_name, user_id FROM t_device_info WHERE is_deleted = 0 LIMIT %s"
|
||||
cursor.execute(query, (limit,))
|
||||
devices = cursor.fetchall()
|
||||
cursor.close()
|
||||
return devices
|
||||
except Error as e:
|
||||
print(f"查询设备错误: {e}")
|
||||
return []
|
||||
|
||||
def get_user_devices(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
if not self.connection or not self.connection.is_connected():
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
try:
|
||||
cursor = self.connection.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT u.id as user_id, u.user_name, u.phone, d.serial_number as device_sn, d.device_name
|
||||
FROM sys_user u
|
||||
INNER JOIN t_device_info d ON u.id = d.user_id
|
||||
WHERE u.is_deleted = 0 AND d.is_deleted = 0 AND d.user_id IS NOT NULL
|
||||
LIMIT %s
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
user_devices = cursor.fetchall()
|
||||
cursor.close()
|
||||
return user_devices
|
||||
except Error as e:
|
||||
print(f"查询用户设备关联错误: {e}")
|
||||
return []
|
||||
|
||||
def load_db_config() -> DatabaseConfig:
|
||||
config_file = os.path.join(os.path.dirname(__file__), 'db_config.json')
|
||||
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
return DatabaseConfig(**config)
|
||||
else:
|
||||
default_config = {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"database": "ljwx",
|
||||
"user": "root",
|
||||
"password": ""
|
||||
}
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, indent=2, ensure_ascii=False)
|
||||
print(f"已创建默认数据库配置文件: {config_file}")
|
||||
print("请根据实际情况修改数据库连接参数")
|
||||
return DatabaseConfig(**default_config)
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
from api_tester import APITester
|
||||
from db_config import load_db_config
|
||||
|
||||
def debug_single_request():
|
||||
print("Debug API 请求")
|
||||
print("=" * 50)
|
||||
|
||||
# 获取测试数据
|
||||
tester = APITester("http://192.168.1.83:5001")
|
||||
|
||||
db_config = load_db_config()
|
||||
if not db_config.connect():
|
||||
print("数据库连接失败")
|
||||
return
|
||||
|
||||
devices = db_config.get_devices(1)
|
||||
if not devices:
|
||||
print("未找到设备数据")
|
||||
return
|
||||
|
||||
device_sn = devices[0]['device_sn']
|
||||
print(f"使用设备序列号: {device_sn}")
|
||||
|
||||
# 测试每个接口
|
||||
endpoints = [
|
||||
('upload_health_data', tester.generate_health_data(device_sn)),
|
||||
('upload_device_info', tester.generate_device_info(device_sn)),
|
||||
('upload_common_event', tester.generate_common_event(device_sn))
|
||||
]
|
||||
|
||||
for endpoint, test_data in endpoints:
|
||||
print(f"\n--- 测试 {endpoint} ---")
|
||||
url = f"http://192.168.1.83:5001/{endpoint}"
|
||||
|
||||
print(f"URL: {url}")
|
||||
print(f"数据: {json.dumps(test_data, indent=2, ensure_ascii=False)[:500]}...")
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=test_data, timeout=10)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应头: {dict(response.headers)}")
|
||||
print(f"响应内容: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ 请求成功")
|
||||
else:
|
||||
print(f"❌ 请求失败: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 请求异常: {e}")
|
||||
|
||||
db_config.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_single_request()
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from api_tester import APITester
|
||||
from db_config import load_db_config
|
||||
import random
|
||||
import queue
|
||||
|
||||
class FastUploader:
|
||||
def __init__(self, base_url: str = "http://192.168.1.83:5001"):
|
||||
self.base_url = base_url
|
||||
self.db_config = load_db_config()
|
||||
self.running = False
|
||||
|
||||
# 高并发设置
|
||||
self.max_workers = 20 # 线程池大小
|
||||
self.session_pool = self._create_api_testers(self.max_workers)
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
'total_uploads': 0,
|
||||
'successful_uploads': 0,
|
||||
'failed_uploads': 0,
|
||||
'start_time': None,
|
||||
'devices_count': 0,
|
||||
'time_points_completed': 0,
|
||||
'total_time_points': 0
|
||||
}
|
||||
|
||||
# 线程安全的锁
|
||||
self.stats_lock = threading.Lock()
|
||||
|
||||
# 设置日志
|
||||
self.setup_logging()
|
||||
|
||||
# 信号处理
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
def _create_api_testers(self, count: int) -> queue.Queue:
|
||||
"""创建API测试器池"""
|
||||
tester_queue = queue.Queue()
|
||||
for _ in range(count):
|
||||
tester_queue.put(APITester(self.base_url))
|
||||
return tester_queue
|
||||
|
||||
def setup_logging(self):
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
log_file = log_dir / f"fast_upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info(f"日志文件: {log_file}")
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
self.logger.info(f"接收到信号 {signum},停止上传...")
|
||||
self.running = False
|
||||
sys.exit(0)
|
||||
|
||||
def generate_data_for_time(self, device_sn: str, target_time: datetime) -> Dict[str, Any]:
|
||||
"""为指定时间生成数据"""
|
||||
timestamp_str = target_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 健康数据
|
||||
health_data = {
|
||||
"data": {
|
||||
"deviceSn": device_sn,
|
||||
"heart_rate": random.randint(60, 120),
|
||||
"blood_oxygen": random.randint(95, 100) if random.random() > 0.3 else 0,
|
||||
"body_temperature": f"{random.uniform(36.0, 37.5):.1f}",
|
||||
"step": random.randint(0, 15000),
|
||||
"distance": f"{random.uniform(0, 10):.1f}",
|
||||
"calorie": f"{random.uniform(0, 500):.1f}",
|
||||
"latitude": f"{random.uniform(22.5, 22.6):.6f}",
|
||||
"longitude": f"{random.uniform(114.0, 114.1):.6f}",
|
||||
"altitude": f"{random.uniform(0, 100):.1f}",
|
||||
"stress": random.randint(0, 100),
|
||||
"upload_method": random.choice(["wifi", "4g", "bluetooth"]),
|
||||
"blood_pressure_systolic": random.randint(110, 140),
|
||||
"blood_pressure_diastolic": random.randint(70, 90),
|
||||
"sleepData": "null",
|
||||
"exerciseDailyData": "null",
|
||||
"exerciseWeekData": "null",
|
||||
"scientificSleepData": "null",
|
||||
"workoutData": "null",
|
||||
"timestamp": timestamp_str
|
||||
}
|
||||
}
|
||||
|
||||
# 设备信息
|
||||
device_info = {
|
||||
"System Software Version": f"GLL-AL30BCN {random.randint(3,5)}.0.0.{random.randint(800,999)}",
|
||||
"Wifi Address": ":".join([f"{random.randint(0,255):02x}" for _ in range(6)]),
|
||||
"Bluetooth Address": ":".join([f"{random.randint(0,255):02X}" for _ in range(6)]),
|
||||
"IP Address": f"192.168.1.{random.randint(100, 254)}",
|
||||
"Network Access Mode": random.choice([1, 2, 3]),
|
||||
"SerialNumber": device_sn,
|
||||
"Device Name": f"HUAWEI WATCH B7-{random.randint(500,600)}-BF{random.randint(0,9)}",
|
||||
"IMEI": f"86615206{random.randint(10000000, 99999999)}",
|
||||
"batteryLevel": random.randint(10, 100),
|
||||
"voltage": random.randint(3500, 4500),
|
||||
"chargingStatus": random.choice(["NONE", "CHARGING", "FULL"]),
|
||||
"status": random.choice(["ACTIVE", "INACTIVE", "SLEEP"]),
|
||||
"timestamp": timestamp_str,
|
||||
"wearState": random.choice([0, 1])
|
||||
}
|
||||
|
||||
# 通用事件
|
||||
common_event = {
|
||||
'eventType': 'com.tdtech.ohos.action.WEAR_STATUS_CHANGED',
|
||||
'eventValue': str(random.choice([0, 1])),
|
||||
'deviceSn': device_sn,
|
||||
'latitude': round(random.uniform(22.5, 22.6), 6),
|
||||
'longitude': round(random.uniform(114.0, 114.1), 6),
|
||||
'altitude': random.randint(0, 100),
|
||||
'timestamp': timestamp_str,
|
||||
'healthData': health_data
|
||||
}
|
||||
|
||||
return {
|
||||
'upload_health_data': health_data,
|
||||
'upload_device_info': device_info,
|
||||
'upload_common_event': common_event
|
||||
}
|
||||
|
||||
def upload_device_data(self, device_sn: str, target_time: datetime) -> List[Dict[str, Any]]:
|
||||
"""上传单个设备在指定时间的所有数据"""
|
||||
# 获取API测试器
|
||||
api_tester = self.session_pool.get()
|
||||
|
||||
try:
|
||||
data_set = self.generate_data_for_time(device_sn, target_time)
|
||||
results = []
|
||||
|
||||
for endpoint, data in data_set.items():
|
||||
try:
|
||||
result = api_tester.make_request(endpoint, data, timeout=30)
|
||||
result['device_sn'] = device_sn
|
||||
result['endpoint'] = endpoint
|
||||
results.append(result)
|
||||
|
||||
# 更新统计
|
||||
with self.stats_lock:
|
||||
self.stats['total_uploads'] += 1
|
||||
if result.get('success', False):
|
||||
self.stats['successful_uploads'] += 1
|
||||
else:
|
||||
self.stats['failed_uploads'] += 1
|
||||
|
||||
except Exception as e:
|
||||
error_result = {
|
||||
'device_sn': device_sn,
|
||||
'endpoint': endpoint,
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
results.append(error_result)
|
||||
|
||||
with self.stats_lock:
|
||||
self.stats['total_uploads'] += 1
|
||||
self.stats['failed_uploads'] += 1
|
||||
|
||||
return results
|
||||
|
||||
finally:
|
||||
# 归还API测试器到池中
|
||||
self.session_pool.put(api_tester)
|
||||
|
||||
def upload_historical_data_fast(self, days: int = 30):
|
||||
"""高速上传历史数据"""
|
||||
if not self.db_config.connect():
|
||||
self.logger.error("数据库连接失败")
|
||||
return
|
||||
|
||||
# 获取设备数据
|
||||
user_devices = self.db_config.get_user_devices(100)
|
||||
if not user_devices:
|
||||
devices = self.db_config.get_devices(100)
|
||||
device_list = [d['device_sn'] for d in devices if d.get('device_sn')]
|
||||
else:
|
||||
device_list = [ud['device_sn'] for ud in user_devices]
|
||||
|
||||
if not device_list:
|
||||
self.logger.error("未找到设备数据")
|
||||
return
|
||||
|
||||
self.stats['devices_count'] = len(device_list)
|
||||
|
||||
# 计算时间范围 - 每分钟一次
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
# 生成时间点(每分钟)
|
||||
time_points = []
|
||||
current_time = start_time
|
||||
while current_time <= end_time:
|
||||
time_points.append(current_time)
|
||||
current_time += timedelta(minutes=1)
|
||||
|
||||
self.stats['total_time_points'] = len(time_points)
|
||||
total_operations = len(device_list) * len(time_points) * 3
|
||||
|
||||
self.logger.info("🚀 高速数据上传开始")
|
||||
self.logger.info(f"设备数量: {len(device_list)}")
|
||||
self.logger.info(f"时间范围: {start_time.strftime('%Y-%m-%d %H:%M')} 到 {end_time.strftime('%Y-%m-%d %H:%M')}")
|
||||
self.logger.info(f"时间点数量: {len(time_points)} (每分钟)")
|
||||
self.logger.info(f"总操作数: {total_operations}")
|
||||
self.logger.info(f"线程池大小: {self.max_workers}")
|
||||
self.logger.info(f"预计耗时: {total_operations/100/60:.1f} 分钟 (假设100次/分钟)")
|
||||
|
||||
self.running = True
|
||||
self.stats['start_time'] = datetime.now()
|
||||
|
||||
# 使用线程池并行处理
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
# 创建所有任务
|
||||
futures = []
|
||||
for time_idx, time_point in enumerate(time_points):
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
for device_sn in device_list:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
future = executor.submit(self.upload_device_data, device_sn, time_point)
|
||||
futures.append((future, time_point, device_sn, time_idx))
|
||||
|
||||
self.logger.info(f"已创建 {len(futures)} 个上传任务")
|
||||
|
||||
# 等待所有任务完成并显示进度
|
||||
completed_count = 0
|
||||
for future, time_point, device_sn, time_idx in futures:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
try:
|
||||
results = future.result(timeout=60)
|
||||
completed_count += 1
|
||||
|
||||
# 每完成10个任务显示进度
|
||||
if completed_count % 10 == 0 or completed_count == len(futures):
|
||||
elapsed = datetime.now() - self.stats['start_time']
|
||||
with self.stats_lock:
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads'] * 100) if self.stats['total_uploads'] > 0 else 0
|
||||
upload_speed = self.stats['total_uploads'] / elapsed.total_seconds() if elapsed.total_seconds() > 0 else 0
|
||||
|
||||
progress = completed_count / len(futures) * 100
|
||||
time_str = time_point.strftime('%H:%M')
|
||||
self.logger.info(f"📊 进度: {progress:.1f}% | {completed_count}/{len(futures)} | "
|
||||
f"时间: {time_str} | 设备: {device_sn[:15]} | "
|
||||
f"成功率: {success_rate:.1f}% | 速度: {upload_speed:.1f} 次/秒")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"任务处理错误 - 设备: {device_sn}, 时间: {time_point}: {e}")
|
||||
completed_count += 1
|
||||
|
||||
self.db_config.disconnect()
|
||||
self.print_final_stats()
|
||||
|
||||
def print_final_stats(self):
|
||||
"""打印最终统计"""
|
||||
self.logger.info("=" * 80)
|
||||
self.logger.info("📊 高速上传完成统计")
|
||||
self.logger.info("=" * 80)
|
||||
|
||||
if self.stats['start_time']:
|
||||
duration = datetime.now() - self.stats['start_time']
|
||||
self.logger.info(f"总耗时: {duration}")
|
||||
|
||||
# 计算速度
|
||||
if self.stats['total_uploads'] > 0 and duration.total_seconds() > 0:
|
||||
upload_speed = self.stats['total_uploads'] / duration.total_seconds()
|
||||
self.logger.info(f"平均上传速度: {upload_speed:.2f} 次/秒")
|
||||
self.logger.info(f"峰值处理能力: {upload_speed * 60:.0f} 次/分钟")
|
||||
|
||||
self.logger.info(f"设备数量: {self.stats['devices_count']}")
|
||||
self.logger.info(f"总操作数: {self.stats['total_uploads']}")
|
||||
self.logger.info(f"成功次数: {self.stats['successful_uploads']}")
|
||||
self.logger.info(f"失败次数: {self.stats['failed_uploads']}")
|
||||
|
||||
if self.stats['total_uploads'] > 0:
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads']) * 100
|
||||
self.logger.info(f"成功率: {success_rate:.2f}%")
|
||||
|
||||
def main():
|
||||
print("🚀 高速数据上传工具 - 模拟客户手表每分钟上传")
|
||||
print("=" * 60)
|
||||
|
||||
days = 30
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
days = float(sys.argv[1])
|
||||
except ValueError:
|
||||
print("❌ 无效的天数参数")
|
||||
return
|
||||
|
||||
print(f"📅 准备上传过去 {days} 天的数据")
|
||||
print("⚡ 使用高并发模式,最大化上传速度")
|
||||
print("💡 按 Ctrl+C 可以安全停止")
|
||||
print()
|
||||
|
||||
confirm = input("确认开始上传? (y/N): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
print("已取消")
|
||||
return
|
||||
|
||||
uploader = FastUploader()
|
||||
|
||||
try:
|
||||
uploader.upload_historical_data_fast(days)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⏸️ 用户中断操作")
|
||||
uploader.running = False
|
||||
except Exception as e:
|
||||
print(f"\n❌ 程序错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
|
||||
import multiprocessing
|
||||
import signal
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from api_tester import APITester
|
||||
from db_config import load_db_config
|
||||
import random
|
||||
|
||||
class HighSpeedUploader:
|
||||
def __init__(self, base_url: str = "http://192.168.1.83:5001"):
|
||||
self.base_url = base_url
|
||||
self.api_tester = APITester(base_url)
|
||||
self.db_config = load_db_config()
|
||||
self.running = False
|
||||
|
||||
# 高并发设置
|
||||
self.max_workers = min(50, multiprocessing.cpu_count() * 10) # 最大并发数
|
||||
self.batch_size = 20 # 批次大小
|
||||
|
||||
# Setup logging
|
||||
self.setup_logging()
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
'total_uploads': 0,
|
||||
'successful_uploads': 0,
|
||||
'failed_uploads': 0,
|
||||
'start_time': None,
|
||||
'devices_count': 0,
|
||||
'time_points_completed': 0,
|
||||
'total_time_points': 0
|
||||
}
|
||||
|
||||
# 信号处理
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
def setup_logging(self):
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
log_file = log_dir / f"high_speed_upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info(f"日志文件: {log_file}")
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
self.logger.info(f"接收到信号 {signum},开始停止...")
|
||||
self.running = False
|
||||
sys.exit(0)
|
||||
|
||||
def generate_data_for_time(self, device_sn: str, target_time: datetime) -> Dict[str, Any]:
|
||||
"""为指定时间生成所有类型的数据"""
|
||||
timestamp_str = target_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 生成健康数据
|
||||
health_data = {
|
||||
"data": {
|
||||
"deviceSn": device_sn,
|
||||
"heart_rate": random.randint(60, 120),
|
||||
"blood_oxygen": random.randint(95, 100) if random.random() > 0.3 else 0,
|
||||
"body_temperature": f"{random.uniform(36.0, 37.5):.1f}",
|
||||
"step": random.randint(0, 15000),
|
||||
"distance": f"{random.uniform(0, 10):.1f}",
|
||||
"calorie": f"{random.uniform(0, 500):.1f}",
|
||||
"latitude": f"{random.uniform(22.5, 22.6):.6f}",
|
||||
"longitude": f"{random.uniform(114.0, 114.1):.6f}",
|
||||
"altitude": f"{random.uniform(0, 100):.1f}",
|
||||
"stress": random.randint(0, 100),
|
||||
"upload_method": random.choice(["wifi", "4g", "bluetooth"]),
|
||||
"blood_pressure_systolic": random.randint(110, 140),
|
||||
"blood_pressure_diastolic": random.randint(70, 90),
|
||||
"sleepData": "null",
|
||||
"exerciseDailyData": "null",
|
||||
"exerciseWeekData": "null",
|
||||
"scientificSleepData": "null",
|
||||
"workoutData": "null",
|
||||
"timestamp": timestamp_str
|
||||
}
|
||||
}
|
||||
|
||||
# 生成设备信息
|
||||
device_info = {
|
||||
"System Software Version": f"GLL-AL30BCN {random.randint(3,5)}.0.0.{random.randint(800,999)}",
|
||||
"Wifi Address": ":".join([f"{random.randint(0,255):02x}" for _ in range(6)]),
|
||||
"Bluetooth Address": ":".join([f"{random.randint(0,255):02X}" for _ in range(6)]),
|
||||
"IP Address": f"192.168.1.{random.randint(100, 254)}",
|
||||
"Network Access Mode": random.choice([1, 2, 3]),
|
||||
"SerialNumber": device_sn,
|
||||
"Device Name": f"HUAWEI WATCH B7-{random.randint(500,600)}-BF{random.randint(0,9)}",
|
||||
"IMEI": f"86615206{random.randint(10000000, 99999999)}",
|
||||
"batteryLevel": random.randint(10, 100),
|
||||
"voltage": random.randint(3500, 4500),
|
||||
"chargingStatus": random.choice(["NONE", "CHARGING", "FULL"]),
|
||||
"status": random.choice(["ACTIVE", "INACTIVE", "SLEEP"]),
|
||||
"timestamp": timestamp_str,
|
||||
"wearState": random.choice([0, 1])
|
||||
}
|
||||
|
||||
# 生成通用事件
|
||||
common_event = {
|
||||
'eventType': 'com.tdtech.ohos.action.WEAR_STATUS_CHANGED',
|
||||
'eventValue': str(random.choice([0, 1])),
|
||||
'deviceSn': device_sn,
|
||||
'latitude': round(random.uniform(22.5, 22.6), 6),
|
||||
'longitude': round(random.uniform(114.0, 114.1), 6),
|
||||
'altitude': random.randint(0, 100),
|
||||
'timestamp': timestamp_str,
|
||||
'healthData': health_data
|
||||
}
|
||||
|
||||
return {
|
||||
'health_data': health_data,
|
||||
'device_info': device_info,
|
||||
'common_event': common_event
|
||||
}
|
||||
|
||||
def upload_single_request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""上传单个请求"""
|
||||
try:
|
||||
result = self.api_tester.make_request(endpoint, data, timeout=30)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {
|
||||
'endpoint': endpoint,
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def process_device_batch(self, device_batch: List[Dict[str, Any]], time_point: datetime) -> List[Dict[str, Any]]:
|
||||
"""并行处理一批设备的数据上传"""
|
||||
results = []
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
futures = []
|
||||
|
||||
for device in device_batch:
|
||||
device_sn = device['device_sn']
|
||||
data_set = self.generate_data_for_time(device_sn, time_point)
|
||||
|
||||
# 为每个设备的三个接口创建任务
|
||||
endpoints = [
|
||||
('upload_health_data', data_set['health_data']),
|
||||
('upload_device_info', data_set['device_info']),
|
||||
('upload_common_event', data_set['common_event'])
|
||||
]
|
||||
|
||||
for endpoint, data in endpoints:
|
||||
future = executor.submit(self.upload_single_request, endpoint, data)
|
||||
futures.append((future, device_sn, endpoint))
|
||||
|
||||
# 收集结果
|
||||
for future, device_sn, endpoint in futures:
|
||||
try:
|
||||
result = future.result(timeout=60)
|
||||
result['device_sn'] = device_sn
|
||||
results.append(result)
|
||||
|
||||
# 更新统计
|
||||
self.stats['total_uploads'] += 1
|
||||
if result.get('success', False):
|
||||
self.stats['successful_uploads'] += 1
|
||||
else:
|
||||
self.stats['failed_uploads'] += 1
|
||||
|
||||
except Exception as e:
|
||||
error_result = {
|
||||
'device_sn': device_sn,
|
||||
'endpoint': endpoint,
|
||||
'success': False,
|
||||
'error': f"Future error: {str(e)}",
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
results.append(error_result)
|
||||
self.stats['total_uploads'] += 1
|
||||
self.stats['failed_uploads'] += 1
|
||||
|
||||
return results
|
||||
|
||||
def upload_historical_data_fast(self, days: int = 30):
|
||||
"""高速上传历史数据"""
|
||||
if not self.db_config.connect():
|
||||
self.logger.error("数据库连接失败")
|
||||
return
|
||||
|
||||
# 获取设备数据
|
||||
user_devices = self.db_config.get_user_devices(100)
|
||||
if not user_devices:
|
||||
devices = self.db_config.get_devices(100)
|
||||
device_list = [{'device_sn': d['device_sn'], 'user_name': 'unknown'}
|
||||
for d in devices if d.get('device_sn')]
|
||||
else:
|
||||
device_list = [{'device_sn': ud['device_sn'], 'user_name': ud['user_name']}
|
||||
for ud in user_devices]
|
||||
|
||||
if not device_list:
|
||||
self.logger.error("未找到设备数据")
|
||||
return
|
||||
|
||||
self.stats['devices_count'] = len(device_list)
|
||||
self.logger.info(f"找到 {len(device_list)} 个设备")
|
||||
|
||||
# 计算时间范围 - 每分钟一次
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
# 生成时间点(每分钟)
|
||||
time_points = []
|
||||
current_time = start_time
|
||||
while current_time <= end_time:
|
||||
time_points.append(current_time)
|
||||
current_time += timedelta(minutes=1) # 改为每分钟
|
||||
|
||||
self.stats['total_time_points'] = len(time_points)
|
||||
total_operations = len(device_list) * len(time_points) * 3
|
||||
|
||||
self.logger.info(f"时间范围: {start_time} 到 {end_time}")
|
||||
self.logger.info(f"时间点数量: {len(time_points)} (每分钟)")
|
||||
self.logger.info(f"总操作数: {total_operations}")
|
||||
self.logger.info(f"最大并发: {self.max_workers}")
|
||||
|
||||
self.running = True
|
||||
self.stats['start_time'] = datetime.now()
|
||||
|
||||
# 分批处理设备
|
||||
device_batches = [device_list[i:i + self.batch_size]
|
||||
for i in range(0, len(device_list), self.batch_size)]
|
||||
|
||||
self.logger.info(f"设备分为 {len(device_batches)} 个批次,每批 {self.batch_size} 个")
|
||||
|
||||
# 处理每个时间点
|
||||
for time_idx, time_point in enumerate(time_points):
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
time_str = time_point.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 并行处理所有设备批次
|
||||
batch_futures = []
|
||||
with ProcessPoolExecutor(max_workers=min(len(device_batches), multiprocessing.cpu_count())) as process_executor:
|
||||
for batch in device_batches:
|
||||
if not self.running:
|
||||
break
|
||||
future = process_executor.submit(self.process_device_batch_worker, batch, time_point)
|
||||
batch_futures.append(future)
|
||||
|
||||
# 等待所有批次完成
|
||||
for future in as_completed(batch_futures):
|
||||
if not self.running:
|
||||
break
|
||||
try:
|
||||
results = future.result(timeout=120)
|
||||
# 结果已在worker中统计,这里不需要额外处理
|
||||
except Exception as e:
|
||||
self.logger.error(f"批次处理错误: {e}")
|
||||
|
||||
self.stats['time_points_completed'] += 1
|
||||
|
||||
# 显示进度
|
||||
progress = (time_idx + 1) / len(time_points) * 100
|
||||
elapsed = datetime.now() - self.stats['start_time']
|
||||
remaining_points = len(time_points) - (time_idx + 1)
|
||||
if time_idx > 0:
|
||||
avg_time_per_point = elapsed.total_seconds() / (time_idx + 1)
|
||||
eta = timedelta(seconds=avg_time_per_point * remaining_points)
|
||||
else:
|
||||
eta = timedelta(0)
|
||||
|
||||
if (time_idx + 1) % 10 == 0 or time_idx == 0: # 每10个时间点显示一次
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads'] * 100) if self.stats['total_uploads'] > 0 else 0
|
||||
self.logger.info(f"🚀 进度: {progress:.1f}% | 时间: {time_str} | "
|
||||
f"成功: {self.stats['successful_uploads']} | 失败: {self.stats['failed_uploads']} | "
|
||||
f"成功率: {success_rate:.1f}% | ETA: {eta}")
|
||||
|
||||
self.db_config.disconnect()
|
||||
self.print_final_stats()
|
||||
|
||||
def process_device_batch_worker(self, device_batch: List[Dict[str, Any]], time_point: datetime) -> List[Dict[str, Any]]:
|
||||
"""工作进程中处理设备批次"""
|
||||
# 在新进程中重新初始化必要的组件
|
||||
api_tester = APITester(self.base_url)
|
||||
results = []
|
||||
|
||||
for device in device_batch:
|
||||
device_sn = device['device_sn']
|
||||
data_set = self.generate_data_for_time(device_sn, time_point)
|
||||
|
||||
endpoints = [
|
||||
('upload_health_data', data_set['health_data']),
|
||||
('upload_device_info', data_set['device_info']),
|
||||
('upload_common_event', data_set['common_event'])
|
||||
]
|
||||
|
||||
for endpoint, data in endpoints:
|
||||
try:
|
||||
result = api_tester.make_request(endpoint, data, timeout=30)
|
||||
result['device_sn'] = device_sn
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
error_result = {
|
||||
'device_sn': device_sn,
|
||||
'endpoint': endpoint,
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
results.append(error_result)
|
||||
|
||||
return results
|
||||
|
||||
def print_final_stats(self):
|
||||
"""打印最终统计"""
|
||||
self.logger.info("=" * 80)
|
||||
self.logger.info("📊 高速上传完成统计")
|
||||
self.logger.info("=" * 80)
|
||||
|
||||
if self.stats['start_time']:
|
||||
duration = datetime.now() - self.stats['start_time']
|
||||
self.logger.info(f"总耗时: {duration}")
|
||||
|
||||
# 计算速度
|
||||
total_ops = self.stats['total_uploads']
|
||||
if total_ops > 0 and duration.total_seconds() > 0:
|
||||
ops_per_sec = total_ops / duration.total_seconds()
|
||||
self.logger.info(f"上传速度: {ops_per_sec:.2f} 次/秒")
|
||||
|
||||
self.logger.info(f"设备数量: {self.stats['devices_count']}")
|
||||
self.logger.info(f"时间点完成: {self.stats['time_points_completed']}/{self.stats['total_time_points']}")
|
||||
self.logger.info(f"总上传次数: {self.stats['total_uploads']}")
|
||||
self.logger.info(f"成功次数: {self.stats['successful_uploads']}")
|
||||
self.logger.info(f"失败次数: {self.stats['failed_uploads']}")
|
||||
|
||||
if self.stats['total_uploads'] > 0:
|
||||
success_rate = (self.stats['successful_uploads'] / self.stats['total_uploads']) * 100
|
||||
self.logger.info(f"成功率: {success_rate:.2f}%")
|
||||
|
||||
def main():
|
||||
print("🚀 高速历史数据上传工具")
|
||||
print("=" * 50)
|
||||
print("• 模拟客户手表每分钟上传一次数据")
|
||||
print("• 最大并发处理")
|
||||
print("• 上传过去30天的历史数据")
|
||||
print()
|
||||
|
||||
days = 30
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
days = int(sys.argv[1])
|
||||
except ValueError:
|
||||
print("❌ 无效的天数参数")
|
||||
return
|
||||
|
||||
uploader = HighSpeedUploader()
|
||||
|
||||
try:
|
||||
print(f"开始上传过去 {days} 天的数据...")
|
||||
uploader.upload_historical_data_fast(days)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 用户中断操作")
|
||||
uploader.running = False
|
||||
except Exception as e:
|
||||
print(f"\n❌ 程序错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
requests>=2.28.0
|
||||
mysql-connector-python>=8.0.0
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 自动化数据上传脚本 - 上传过去30天的数据,每5分钟一次
|
||||
# 作者: Claude Code AI Assistant
|
||||
# 日期: 2025-09-07
|
||||
|
||||
echo "🚀 自动化数据上传工具 - 30天历史数据"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# 检查Python环境
|
||||
if [ ! -d "test_env" ]; then
|
||||
echo "❌ 虚拟环境不存在,请先运行 python3 -m venv test_env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
source test_env/bin/activate
|
||||
|
||||
echo "📊 即将开始上传过去30天的数据:"
|
||||
echo " • 时间间隔: 每5分钟一次"
|
||||
echo " • 预计操作数: ~25,920 次"
|
||||
echo " • 预计耗时: 3-4 小时"
|
||||
echo " • 支持中断恢复"
|
||||
echo ""
|
||||
|
||||
# 检查是否有进度文件
|
||||
if [ -f "progress_state.json" ]; then
|
||||
echo "🔄 发现进度文件,将从上次中断处继续"
|
||||
python background_uploader.py status
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "选择运行方式:"
|
||||
echo "1. 前台运行(可以看到实时日志)"
|
||||
echo "2. 后台运行(nohup方式)"
|
||||
echo "3. 查看当前进度状态"
|
||||
echo "4. 重置进度(重新开始)"
|
||||
echo "5. 开始持续上传(当前数据)"
|
||||
echo ""
|
||||
|
||||
read -p "请选择 (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
echo "🖥️ 前台运行模式"
|
||||
echo "💡 按 Ctrl+C 可以安全中断并保存进度"
|
||||
echo ""
|
||||
sleep 2
|
||||
python background_uploader.py start 30
|
||||
;;
|
||||
2)
|
||||
echo "📡 后台运行模式"
|
||||
nohup python background_uploader.py start 30 > upload_30days.log 2>&1 &
|
||||
PID=$!
|
||||
echo "✅ 后台任务已启动 (PID: $PID)"
|
||||
echo "📄 日志文件: upload_30days.log"
|
||||
echo "📊 查看进度: python background_uploader.py status"
|
||||
echo "🛑 停止任务: kill $PID"
|
||||
echo ""
|
||||
echo "监控命令:"
|
||||
echo " tail -f upload_30days.log # 实时查看日志"
|
||||
echo " python background_uploader.py status # 查看进度"
|
||||
;;
|
||||
3)
|
||||
echo "📊 当前进度状态:"
|
||||
python background_uploader.py status
|
||||
;;
|
||||
4)
|
||||
echo "🗑️ 重置进度..."
|
||||
python background_uploader.py reset
|
||||
echo "✅ 进度已重置,可以重新开始"
|
||||
;;
|
||||
5)
|
||||
echo "🔄 持续上传模式(每5分钟上传当前数据)"
|
||||
echo "💡 按 Ctrl+C 停止"
|
||||
python background_uploader.py continuous
|
||||
;;
|
||||
*)
|
||||
echo "❌ 无效选择"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "✅ 操作完成"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
from api_tester import APITester
|
||||
from datetime import datetime
|
||||
|
||||
def main():
|
||||
print("API 接口自动化测试工具")
|
||||
print("=" * 50)
|
||||
|
||||
tester = APITester("http://192.168.1.83:5001")
|
||||
|
||||
print("连接数据库...")
|
||||
if not tester.db_config.connect():
|
||||
print("❌ 数据库连接失败,请检查 db_config.json 配置")
|
||||
return
|
||||
|
||||
print("✅ 数据库连接成功")
|
||||
|
||||
devices = tester.db_config.get_devices(20)
|
||||
users = tester.db_config.get_users(10)
|
||||
user_devices = tester.db_config.get_user_devices(15)
|
||||
|
||||
print(f"📊 数据统计:")
|
||||
print(f" - 设备数量: {len(devices)}")
|
||||
print(f" - 用户数量: {len(users)}")
|
||||
print(f" - 用户设备关联: {len(user_devices)}")
|
||||
|
||||
if not devices:
|
||||
print("❌ 未找到设备数据,请检查数据库表 t_device_info")
|
||||
return
|
||||
|
||||
# 优先使用有用户关联的设备
|
||||
if user_devices:
|
||||
device_sns = [ud['device_sn'] for ud in user_devices]
|
||||
print(f"✅ 获取有用户关联的设备序列号: {device_sns[:3]}...")
|
||||
else:
|
||||
device_sns = [device['device_sn'] for device in devices if device['device_sn']]
|
||||
print(f"✅ 获取设备序列号: {device_sns[:3]}...")
|
||||
|
||||
print("\n🧪 开始功能测试...")
|
||||
functional_results = tester.functional_test(device_sns)
|
||||
|
||||
print("\n⚡ 开始性能测试...")
|
||||
endpoints = ['upload_health_data', 'upload_device_info', 'upload_common_event']
|
||||
performance_results = {}
|
||||
|
||||
for endpoint in endpoints:
|
||||
print(f" 测试 {endpoint}...")
|
||||
perf_results = tester.performance_test(endpoint, device_sns,
|
||||
concurrent_users=3,
|
||||
requests_per_user=5)
|
||||
performance_results[f"{endpoint}_performance"] = perf_results
|
||||
|
||||
all_results = {**functional_results, **performance_results}
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_file = f"api_test_report_{timestamp}.txt"
|
||||
|
||||
print(f"\n📋 生成测试报告: {output_file}")
|
||||
tester.generate_report(all_results, output_file)
|
||||
|
||||
tester.db_config.disconnect()
|
||||
print("✅ 测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from fast_uploader import FastUploader
|
||||
|
||||
def main():
|
||||
print("🚀 30天历史数据高速上传")
|
||||
print("=" * 60)
|
||||
print("🎯 模拟客户手表每分钟上传一次数据")
|
||||
print("⚡ 高并发模式,最大化上传速度")
|
||||
print("📊 预计统计:")
|
||||
print(" • 时间范围: 过去30天")
|
||||
print(" • 上传频率: 每分钟1次")
|
||||
print(" • 时间点数: 43,200个 (30天 × 24小时 × 60分钟)")
|
||||
print(" • 总操作数: ~130,000次 (假设3个设备)")
|
||||
print(" • 预计速度: 600+ 次/秒")
|
||||
print(" • 预计耗时: 3-5分钟")
|
||||
print()
|
||||
|
||||
# 检查虚拟环境
|
||||
if not os.path.exists("test_env/bin/activate"):
|
||||
print("❌ 虚拟环境不存在,请先运行:")
|
||||
print(" python3 -m venv test_env")
|
||||
print(" source test_env/bin/activate")
|
||||
print(" pip install -r requirements.txt")
|
||||
return
|
||||
|
||||
# 显示当前配置
|
||||
print("🔧 当前配置:")
|
||||
print(" • API地址: http://192.168.1.83:5001")
|
||||
print(" • 数据库: 127.0.0.1:3306/test")
|
||||
print(" • 线程池: 20个并发线程")
|
||||
print()
|
||||
|
||||
# 确认开始
|
||||
print("⚠️ 注意事项:")
|
||||
print(" • 这将生成大量测试数据")
|
||||
print(" • 请确保API服务器能承受高并发")
|
||||
print(" • 可以随时按 Ctrl+C 安全停止")
|
||||
print(" • 建议在测试环境中运行")
|
||||
print()
|
||||
|
||||
choice = input("选择运行方式:\n1. 完整30天上传\n2. 测试模式(1小时数据)\n3. 自定义天数\n请选择 (1-3): ").strip()
|
||||
|
||||
days = 30
|
||||
if choice == "2":
|
||||
days = 0.042 # 1小时
|
||||
print(f"🧪 测试模式 - 上传1小时数据")
|
||||
elif choice == "3":
|
||||
try:
|
||||
days = float(input("请输入天数: ").strip())
|
||||
print(f"📅 自定义模式 - 上传{days}天数据")
|
||||
except ValueError:
|
||||
print("❌ 无效输入")
|
||||
return
|
||||
elif choice == "1":
|
||||
print(f"🚀 完整模式 - 上传30天数据")
|
||||
else:
|
||||
print("❌ 无效选择")
|
||||
return
|
||||
|
||||
print()
|
||||
confirm = input("确认开始上传? (y/N): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
print("已取消")
|
||||
return
|
||||
|
||||
print()
|
||||
print("🚀 开始高速上传...")
|
||||
print("=" * 60)
|
||||
|
||||
# 开始上传
|
||||
uploader = FastUploader()
|
||||
|
||||
try:
|
||||
uploader.upload_historical_data_fast(days)
|
||||
print("✅ 上传完成!")
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⏸️ 用户中断上传")
|
||||
uploader.running = False
|
||||
except Exception as e:
|
||||
print(f"\n❌ 上传错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from continuous_uploader import ContinuousUploader
|
||||
|
||||
def main():
|
||||
print("🚀 数据上传工具")
|
||||
print("=" * 60)
|
||||
|
||||
print("请选择运行模式:")
|
||||
print("1. 上传过去1个月的历史数据(每5分钟一次)")
|
||||
print("2. 持续上传当前数据(每5分钟)")
|
||||
print("3. 先上传历史数据,然后持续上传")
|
||||
print("4. 测试模式 - 上传过去1小时数据")
|
||||
print("5. 自定义天数的历史数据")
|
||||
|
||||
try:
|
||||
choice = input("\n请输入选择 (1-5): ").strip()
|
||||
|
||||
uploader = ContinuousUploader("http://192.168.1.83:5001", 300)
|
||||
|
||||
if choice == "1":
|
||||
print("\n📅 开始上传过去30天的历史数据...")
|
||||
print("⚠️ 这将花费较长时间,请耐心等待")
|
||||
print("📊 预计操作数: 约 25,920 次上传 (30天 × 288次/天 × 3个接口)")
|
||||
print("⏱️ 预计时间: 约 3-4 小时")
|
||||
|
||||
confirm = input("确认开始? (y/N): ").strip().lower()
|
||||
if confirm == 'y':
|
||||
uploader.start_historical(30)
|
||||
else:
|
||||
print("已取消")
|
||||
|
||||
elif choice == "2":
|
||||
print("\n🔄 开始持续数据上传...")
|
||||
print("📡 每5分钟上传一次当前数据")
|
||||
print("💡 按 Ctrl+C 停止")
|
||||
uploader.start_continuous()
|
||||
|
||||
elif choice == "3":
|
||||
print("\n📅 先上传历史数据,然后持续上传...")
|
||||
confirm = input("确认开始历史数据上传? (y/N): ").strip().lower()
|
||||
if confirm == 'y':
|
||||
uploader.start_historical(30)
|
||||
print("\n🔄 历史数据完成,开始持续上传...")
|
||||
uploader = ContinuousUploader("http://192.168.1.83:5001", 300)
|
||||
uploader.start_continuous()
|
||||
else:
|
||||
print("已取消")
|
||||
|
||||
elif choice == "4":
|
||||
print("\n🧪 测试模式 - 上传过去1小时数据...")
|
||||
uploader.start_historical(days=0.042) # 1小时
|
||||
|
||||
elif choice == "5":
|
||||
try:
|
||||
days = int(input("请输入天数: ").strip())
|
||||
if days <= 0:
|
||||
print("天数必须大于0")
|
||||
return
|
||||
|
||||
operations = days * 288 * 3 # 每天288个时间点,3个接口
|
||||
hours = operations * 0.5 / 3600 # 预计每次操作0.5秒
|
||||
|
||||
print(f"\n📊 预计操作数: {operations}")
|
||||
print(f"⏱️ 预计时间: {hours:.1f} 小时")
|
||||
|
||||
confirm = input("确认开始? (y/N): ").strip().lower()
|
||||
if confirm == 'y':
|
||||
uploader.start_historical(days)
|
||||
else:
|
||||
print("已取消")
|
||||
|
||||
except ValueError:
|
||||
print("无效的天数")
|
||||
|
||||
else:
|
||||
print("无效的选择")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 用户取消操作")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 程序错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
from continuous_uploader import ContinuousUploader
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
def test_historical_short():
|
||||
"""测试短期历史数据上传(1小时)"""
|
||||
print("🧪 测试历史数据上传功能")
|
||||
print("=" * 50)
|
||||
|
||||
uploader = ContinuousUploader("http://192.168.1.83:5001", 300)
|
||||
|
||||
# 测试1小时的历史数据(12个时间点)
|
||||
uploader.start_historical(days=0.042) # 1小时 = 1/24天
|
||||
|
||||
print("✅ 历史数据上传测试完成")
|
||||
|
||||
def test_continuous_short():
|
||||
"""测试持续上传功能(3次上传)"""
|
||||
print("\n🔄 测试持续上传功能")
|
||||
print("=" * 50)
|
||||
|
||||
uploader = ContinuousUploader("http://192.168.1.83:5001", 10) # 10秒间隔
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
# 运行3次后自动停止
|
||||
def auto_stop():
|
||||
time.sleep(35) # 让它运行3次(10秒间隔 * 3 + 一些缓冲)
|
||||
uploader.stop()
|
||||
|
||||
stop_thread = threading.Thread(target=auto_stop)
|
||||
stop_thread.daemon = True
|
||||
stop_thread.start()
|
||||
|
||||
uploader.start_continuous()
|
||||
|
||||
print("✅ 持续上传测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "historical":
|
||||
test_historical_short()
|
||||
elif sys.argv[1] == "continuous":
|
||||
test_continuous_short()
|
||||
else:
|
||||
print("用法: python test_continuous.py [historical|continuous]")
|
||||
else:
|
||||
# 默认测试历史数据
|
||||
test_historical_short()
|
||||
Reference in New Issue
Block a user