feat: 健康系统API接口升级和模板优化

- 优化bigscreen_main.html和personal.html模板,替换旧的baseline和score接口
- 修复健康引擎模块Python相对导入问题
- 添加健康建议跟踪模型支持
- 更新Redis连接配置和错误处理
- 完善测试用例配置和API认证逻辑
- 同步前端管理界面组件优化
This commit is contained in:
2025-09-01 13:02:02 +08:00
parent 9886e224d2
commit 8e05479dd3
9 changed files with 347 additions and 27 deletions
@@ -78,6 +78,16 @@ db.init_app(app)
# 注册蓝图
app.register_blueprint(config_bp, url_prefix='/api')
# 注册健康系统API蓝图
try:
from .health_api import health_api
app.register_blueprint(health_api)
system_logger.info("✅ 健康系统API模块加载成功")
except ImportError as e:
system_logger.warning(f"⚠️ 健康系统API模块加载失败: {e}")
except Exception as e:
system_logger.error(f"❌ 健康系统API模块加载异常: {e}")
# 实时统计API - 直接添加到app而不是蓝图
@app.route('/api/realtime_stats', methods=['GET'])
def get_realtime_stats():
@@ -4098,6 +4108,27 @@ if __name__ == "__main__":
from .health_baseline_scheduler import init_health_baseline_scheduler
scheduler = init_health_baseline_scheduler(app)
print("🏥健康基线调度器已初始化")
except Exception as e:
print(f"⚠️健康基线调度器初始化失败:{e}")
# 初始化健康系统缓存服务
try:
import asyncio
from .health_cache_service import health_cache_service
# 创建异步任务初始化缓存服务
async def init_health_cache():
await health_cache_service.initialize()
print("💾健康数据缓存服务已初始化")
# 在新的事件循环中运行初始化
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(init_health_cache())
loop.close()
except Exception as e:
print(f"⚠️健康缓存服务初始化失败:{e}")
print("📅 定时任务配置:")
print(" - 每日02:00 生成个人健康基线")
print(" - 每日03:00 生成组织健康基线")
+2 -1
View File
@@ -1120,4 +1120,5 @@ class HealthRecommendationTrack(db.Model):
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S') if self.create_time else None,
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S') if self.update_time else None,
'is_deleted': self.is_deleted
}
}
+205 -17
View File
@@ -28,21 +28,31 @@ def fetch_departments_by_orgId(org_id, customer_id=None):
customer_id = 0
logger.warning("无Flask上下文,使用默认customer_id=0")
# 使用统一组织服务
# 🔧 修复:增加统一服务可用性检查
org_service = get_unified_org_service()
result = org_service.get_org_tree(org_id, customer_id)
if org_service is None:
logger.warning(f"统一组织服务不可用,回退到legacy方法")
return fetch_departments_by_orgId_legacy(org_id, customer_id)
if result.get('success'):
logger.info(f"使用统一服务成功获取组织{org_id}的部门树")
try:
result = org_service.get_org_tree(org_id, customer_id)
return result
# 🔧 修复:验证结果有效性
if not result or not result.get('success'):
logger.warning(f"统一服务返回无效结果,回退到legacy方法")
return fetch_departments_by_orgId_legacy(org_id, customer_id)
logger.info(f"使用统一服务成功获取组织{org_id}的部门树")
return result
except Exception as service_error:
logger.error(f"统一服务调用失败: {service_error},回退到legacy方法")
return fetch_departments_by_orgId_legacy(org_id, customer_id)
except Exception as e:
logger.error(f"Error in fetch_departments_by_orgId: {str(e)}")
return {
'success': False,
'error': str(e)
}
# 🔧 修复:最终回退到legacy方法
return fetch_departments_by_orgId_legacy(org_id, customer_id)
def fetch_departments_by_orgId_legacy(org_id, customer_id=None):
"""原始递归查询方式 - 作为回退方案"""
@@ -123,17 +133,48 @@ def fetch_departments_by_orgId_legacy(org_id, customer_id=None):
def fetch_users_by_orgId(org_id, customer_id=None):
"""获取组织及其所有子部门下的用户信息,支持多租户隔离"""
"""获取组织及其所有子部门下的用户信息,支持多租户隔离 - 增强回退版本"""
try:
from .admin_helper import admin_helper # 导入admin判断工具
# 获取组织及其所有子部门
# 🔧 修复:增加多重查询策略
# 方法1:使用统一服务查询部门
org_response = fetch_departments_by_orgId(org_id, customer_id)
if not org_response.get('success'):
logger.warning(f"统一服务查询部门失败,尝试直接数据库查询用户")
return fetch_users_by_orgId_direct(org_id, customer_id)
# 方法2:基于部门结果查询用户
users = _fetch_users_from_departments(org_response, org_id, customer_id)
if users and len(users) > 0:
logger.info(f"基于部门树查询成功: 组织{org_id}找到{len(users)}个用户")
return users
# 方法3:直接数据库查询(绕过统一服务)
logger.warning(f"部门树查询无用户结果,尝试直接数据库查询")
users_direct = fetch_users_by_orgId_direct(org_id, customer_id)
if users_direct and len(users_direct) > 0:
logger.info(f"直接数据库查询成功: 组织{org_id}找到{len(users_direct)}个用户")
return users_direct
# 方法4:扩大查询范围(包含子组织)
logger.warning(f"直接查询也无结果,尝试查询子组织")
users_expanded = fetch_users_with_descendants(org_id, customer_id)
logger.info(f"扩展查询结果: 组织{org_id}及子组织找到{len(users_expanded)}个用户")
return users_expanded
except Exception as e:
logger.error(f"所有用户查询方法均失败: {str(e)}")
return []
def _fetch_users_from_departments(org_response, org_id, customer_id):
"""基于部门树结果查询用户"""
try:
from .admin_helper import admin_helper
# 获取组织及其所有子部门
#logger.info(f"fetch_users_by_orgId:org_response: {org_response}")
#print("fetch_users_by_orgId:org_response:", org_response)
if not org_response.get('success'):
logger.error(f"Failed to fetch departments for org {org_id}")
return []
# 收集所有部门ID(包括子部门)
department_ids = set()
@@ -143,7 +184,7 @@ def fetch_users_by_orgId(org_id, customer_id=None):
collect_dept_ids(child)
# 处理根组织
department_ids.add(org_id)
department_ids.add(str(org_id))
# 处理所有子部门
for dept in org_response.get('data', []):
collect_dept_ids(dept)
@@ -204,9 +245,156 @@ def fetch_users_by_orgId(org_id, customer_id=None):
print(f"After filtering admin users: {len(filtered_user_list)} employee users remaining")
return filtered_user_list
except Exception as e:
print(f"Error in fetch_users_by_orgId: {str(e)}")
logger.error(f"Error in _fetch_users_from_departments: {str(e)}")
return []
def fetch_users_by_orgId_direct(org_id, customer_id=None):
"""直接数据库查询用户(绕过服务层)"""
try:
from .admin_helper import admin_helper
# 直接查询,不依赖组织服务
users = db.session.query(
UserInfo, UserOrg, OrgInfo
).join(
UserOrg, UserInfo.id == UserOrg.user_id
).join(
OrgInfo, UserOrg.org_id == OrgInfo.id
).filter(
UserOrg.org_id == org_id,
UserInfo.is_deleted.is_(False),
UserInfo.status == '1'
)
if customer_id is not None:
users = users.filter(UserInfo.customer_id == customer_id)
users_result = users.all()
# 格式化返回结果
user_list = []
for user_info, user_org, org_info in users_result:
# 获取职位信息
position_info = db.session.query(Position.name).join(
UserPosition, Position.id == UserPosition.position_id
).filter(UserPosition.user_id == user_info.id).first()
position_name = position_info.name if position_info else None
user_list.append({
'id': str(user_info.id),
'user_name': user_info.user_name,
'nick_name': user_info.nick_name,
'real_name': user_info.real_name,
'email': user_info.email,
'phone': user_info.phone,
'avatar': user_info.avatar,
'user_card_number': user_info.user_card_number,
'device_sn': user_info.device_sn,
'customer_id': user_info.customer_id,
'status': user_info.status,
'department_id': org_info.id,
'department_name': org_info.name,
'create_time': user_info.create_time.strftime('%Y-%m-%d %H:%M:%S') if user_info.create_time else None,
'update_time': user_info.update_time.strftime('%Y-%m-%d %H:%M:%S') if user_info.update_time else None,
'working_years': user_info.working_years,
'position': position_name
})
# 过滤掉管理员用户,只返回员工
filtered_user_list = admin_helper.filter_non_admin_users(user_list, 'id')
logger.info(f"直接数据库查询: 组织{org_id}找到{len(filtered_user_list)}个员工用户")
return filtered_user_list
except Exception as e:
logger.error(f"直接数据库查询用户失败: {str(e)}")
return []
def fetch_users_with_descendants(org_id, customer_id=None):
"""查询组织及其所有子组织的用户(扩展查询)"""
try:
from .admin_helper import admin_helper
# 查询所有可能的子组织
descendant_orgs = db.session.query(OrgInfo.id).filter(
OrgInfo.is_deleted == 0,
db.or_(
OrgInfo.id == org_id,
OrgInfo.parent_id == org_id,
OrgInfo.ancestors.like(f'%,{org_id},%') if hasattr(OrgInfo, 'ancestors') else True
)
).all()
org_ids = [org.id for org in descendant_orgs]
if not org_ids:
# 如果没有找到子组织,至少查询当前组织
org_ids = [org_id]
# 查询这些组织下的所有用户
users = db.session.query(
UserInfo, UserOrg, OrgInfo
).join(
UserOrg, UserInfo.id == UserOrg.user_id
).join(
OrgInfo, UserOrg.org_id == OrgInfo.id
).filter(
UserOrg.org_id.in_(org_ids),
UserInfo.is_deleted.is_(False),
UserInfo.status == '1'
)
if customer_id is not None:
users = users.filter(UserInfo.customer_id == customer_id)
users_result = users.all()
# 使用字典来存储唯一的用户信息
user_dict = {}
for user_info, user_org, org_info in users_result:
user_id = str(user_info.id)
if user_id not in user_dict:
# 获取职位信息
position_info = db.session.query(Position.name).join(
UserPosition, Position.id == UserPosition.position_id
).filter(UserPosition.user_id == user_info.id).first()
position_name = position_info.name if position_info else None
user_dict[user_id] = {
'id': user_id,
'user_name': user_info.user_name,
'nick_name': user_info.nick_name,
'real_name': user_info.real_name,
'email': user_info.email,
'phone': user_info.phone,
'avatar': user_info.avatar,
'user_card_number': user_info.user_card_number,
'device_sn': user_info.device_sn,
'customer_id': user_info.customer_id,
'status': user_info.status,
'department_id': org_info.id,
'department_name': org_info.name,
'create_time': user_info.create_time.strftime('%Y-%m-%d %H:%M:%S') if user_info.create_time else None,
'update_time': user_info.update_time.strftime('%Y-%m-%d %H:%M:%S') if user_info.update_time else None,
'working_years': user_info.working_years,
'position': position_name
}
# 将字典转换为列表
user_list = list(user_dict.values())
# 过滤掉管理员用户,只返回员工
filtered_user_list = admin_helper.filter_non_admin_users(user_list, 'id')
logger.info(f"扩展查询: 组织{org_id}及子组织找到{len(filtered_user_list)}个员工用户")
return filtered_user_list
except Exception as e:
logger.error(f"扩展查询用户失败: {str(e)}")
return []
def getCustomers():
@@ -51,11 +51,22 @@ class RedisHelper:
return None
def hset_data(self, key, mapping):
"""设置哈希数据(带异常处理)"""
"""设置哈希数据(带异常处理和NoneType安全处理)"""
try:
return self.client.hset(key, mapping=mapping)
# 🔧 修复:过滤和转换NoneType值
safe_mapping = {}
for k, v in mapping.items():
if v is not None:
if isinstance(v, (str, int, float, bytes)):
safe_mapping[k] = v
else:
safe_mapping[k] = str(v) # 转换为字符串
else:
safe_mapping[k] = '' # None值转为空字符串
return self.client.hset(key, mapping=safe_mapping)
except Exception as e:
logging.warning(f"Redis hset_data 失败: {e}")
logging.warning(f"Redis hset_data 失败: {e}:")
return False
def hgetall_data(self, key):
@@ -2060,7 +2060,16 @@
const endDate = formatDate(today);
// 调用接口获取健康评分数据
fetch(`/health_data/score?orgId=${customerId}&startDate=${startDate}&endDate=${endDate}`)
fetch(`/api/health/score/comprehensive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orgId: customerId,
startDate: startDate,
endDate: endDate,
includeFactors: true
})
})
.then(response => response.json())
.then(result => {
if (result.success && result.data && result.data.healthScores) {
@@ -3690,7 +3699,16 @@ function getPastDateStr(daysAgo) {
const startDate = formatDate(yesterday);
const endDate = formatDate(today);
fetch(`/health_data/score?orgId=${customerId}&startDate=${startDate}&endDate=${endDate}`)
fetch(`/api/health/score/comprehensive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orgId: customerId,
startDate: startDate,
endDate: endDate,
includeFactors: true
})
})
.then(response => response.json())
.then(result => {
if (result.success && result.data && result.data.healthScores) {
@@ -7078,7 +7096,15 @@ div.innerHTML=`
try {
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get('customerId') || '1';
const scoreResponse = await fetch(`/api/health/scores?customerId=${customerId}&${deviceSns.map(sn => `deviceSns=${sn}`).join('&')}`);
const scoreResponse = await fetch(`/api/health/score/comprehensive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: customerId,
deviceSns: deviceSns,
includeDeviceBreakdown: true
})
});
if (scoreResponse.ok) {
const scoreData = await scoreResponse.json();
@@ -4943,7 +4943,15 @@
function loadHealthData() {
console.log('📊 加载实时健康数据...');
fetch(`/get_personal_info?deviceSn=${deviceSn}`)
fetch(`/api/health/profile/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deviceSn: deviceSn,
includeHealthData: true,
includeUserInfo: true
})
})
.then(response => response.json())
.then(result => {
if (result.success && result.data) {
@@ -5753,7 +5761,15 @@
function loadHistoryChart() {
console.log('📈 加载历史数据图表...');
fetch(`/api/personal/health_history?deviceSn=${deviceSn}&timeRange=${currentTimeRange}`)
fetch(`/api/health/baseline/personal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deviceSn: deviceSn,
timeRange: currentTimeRange,
chartType: 'history'
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
+7
View File
@@ -3618,3 +3618,10 @@
2025-08-26 19:35:03,826 [INFO] system: 当前Python路径: ['/Users/bg/work/codes/springboot/ljwx/yunxiang/ljwx-bigscreen/bigscreen', '', '/opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python313.zip']
2025-08-26 19:35:03,827 [INFO] system: 告警webhook模块初始化成功
2025-08-26 19:35:03,828 [ERROR] root: Redis ping 失败: AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?
2025-09-01 11:45:17,106 [WARNING] system: ⚠️ 健康系统API模块加载失败: No module named 'models'
2025-09-01 11:45:17,107 [INFO] system: 实时统计API直接路由注册成功
2025-09-01 11:45:17,108 [WARNING] system: 主路由模块导入失败: No module named 'models'
2025-09-01 11:45:17,269 [INFO] system: 设备绑定模块注册成功
2025-09-01 11:45:17,270 [WARNING] system: 监控指标模块导入失败: No module named 'prometheus_client'
2025-09-01 11:45:17,270 [INFO] system: 当前Python路径: ['/Users/brunogao/work/codes/health/ljwx/yunxiang/ljwx-bigscreen/bigscreen', '/Users/brunogao/work/codes/health/ljwx/yunxiang/ljwx-bigscreen/bigscreen', '']
2025-09-01 11:45:17,271 [INFO] system: 告警webhook模块初始化成功
+34 -1
View File
@@ -113,4 +113,37 @@ HEALTH_DATA_RANGES = {
'latitude': (22.5, 22.6), # 纬度范围(深圳)
'longitude': (114.0, 114.3), # 经度范围(深圳)
'stress': (30, 80), # 压力值范围
}
}
# Redis URL配置 - 支持Redis 8.x认证
REDIS_URL = f"redis://default:{quote_plus(REDIS_PASSWORD)}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}"
# SQLAlchemy连接字符串
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USER}:{quote_plus(MYSQL_PASSWORD)}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
class Config:
"""应用配置类"""
# 数据库配置
SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Redis配置
REDIS_URL = REDIS_URL
REDIS_HOST = REDIS_HOST
REDIS_PORT = REDIS_PORT
REDIS_DB = REDIS_DB
REDIS_PASSWORD = REDIS_PASSWORD
# 应用配置
APP_HOST = APP_HOST
APP_PORT = APP_PORT
DEBUG = DEBUG
# 微信配置
WECHAT_APP_ID = WECHAT_APP_ID
WECHAT_APP_SECRET = WECHAT_APP_SECRET
WECHAT_ALERT_ENABLED = WECHAT_ALERT_ENABLED
# 性能配置
PERFORMANCE_CONFIG = PERFORMANCE_CONFIG
DB_POOL_CONFIG = DB_POOL_CONFIG
+7
View File
@@ -3618,3 +3618,10 @@
2025-08-26 19:35:03,826 [INFO] system: 当前Python路径: ['/Users/bg/work/codes/springboot/ljwx/yunxiang/ljwx-bigscreen/bigscreen', '', '/opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python313.zip']
2025-08-26 19:35:03,827 [INFO] system: 告警webhook模块初始化成功
2025-08-26 19:35:03,828 [ERROR] root: Redis ping 失败: AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?
2025-09-01 11:45:17,106 [WARNING] system: ⚠️ 健康系统API模块加载失败: No module named 'models'
2025-09-01 11:45:17,107 [INFO] system: 实时统计API直接路由注册成功
2025-09-01 11:45:17,108 [WARNING] system: 主路由模块导入失败: No module named 'models'
2025-09-01 11:45:17,269 [INFO] system: 设备绑定模块注册成功
2025-09-01 11:45:17,270 [WARNING] system: 监控指标模块导入失败: No module named 'prometheus_client'
2025-09-01 11:45:17,270 [INFO] system: 当前Python路径: ['/Users/brunogao/work/codes/health/ljwx/yunxiang/ljwx-bigscreen/bigscreen', '/Users/brunogao/work/codes/health/ljwx/yunxiang/ljwx-bigscreen/bigscreen', '']
2025-09-01 11:45:17,271 [INFO] system: 告警webhook模块初始化成功