commit for 9.7

This commit is contained in:
BrunoGao
2025-09-07 20:03:14 +08:00
parent e4a255c454
commit 2f49ad81a1
26 changed files with 4262 additions and 17 deletions
+1 -1
View File
@@ -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技术团队
**更新周期**: 季度更新
+51 -9
View File
@@ -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
@@ -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
View File
@@ -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` 表获取
- 自动生成符合接口规范的测试数据
+78
View File
@@ -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
+109
View File
@@ -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"
}
+66
View File
@@ -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
+337
View File
@@ -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()
+200
View File
@@ -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()
+77
View File
@@ -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()
+345
View File
@@ -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()
+7
View File
@@ -0,0 +1,7 @@
{
"host": "127.0.0.1",
"port": 3306,
"database": "test",
"user": "root",
"password": "123456"
}
+110
View File
@@ -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)
+58
View File
@@ -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()
+337
View File
@@ -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()
+385
View File
@@ -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()
+2
View File
@@ -0,0 +1,2 @@
requests>=2.28.0
mysql-connector-python>=8.0.0
+86
View File
@@ -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 "✅ 操作完成"
+65
View File
@@ -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()
+88
View File
@@ -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()
+87
View File
@@ -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()
+51
View File
@@ -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()