diff --git a/ljwx-bigscreen/bigscreen/bigScreen/bigScreen.py b/ljwx-bigscreen/bigscreen/bigScreen/bigScreen.py index a4560b8..bc619e3 100644 --- a/ljwx-bigscreen/bigscreen/bigScreen/bigScreen.py +++ b/ljwx-bigscreen/bigscreen/bigScreen/bigScreen.py @@ -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 生成组织健康基线") diff --git a/ljwx-bigscreen/bigscreen/bigScreen/models.py b/ljwx-bigscreen/bigscreen/bigScreen/models.py index c064acd..28ab830 100644 --- a/ljwx-bigscreen/bigscreen/bigScreen/models.py +++ b/ljwx-bigscreen/bigscreen/bigScreen/models.py @@ -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 - } \ No newline at end of file + } + diff --git a/ljwx-bigscreen/bigscreen/bigScreen/org.py b/ljwx-bigscreen/bigscreen/bigScreen/org.py index be8cbfb..876b902 100644 --- a/ljwx-bigscreen/bigscreen/bigScreen/org.py +++ b/ljwx-bigscreen/bigscreen/bigScreen/org.py @@ -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(): diff --git a/ljwx-bigscreen/bigscreen/bigScreen/redis_helper.py b/ljwx-bigscreen/bigscreen/bigScreen/redis_helper.py index baf7bd8..c68ca60 100644 --- a/ljwx-bigscreen/bigscreen/bigScreen/redis_helper.py +++ b/ljwx-bigscreen/bigscreen/bigScreen/redis_helper.py @@ -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): diff --git a/ljwx-bigscreen/bigscreen/bigScreen/templates/bigscreen_main.html b/ljwx-bigscreen/bigscreen/bigScreen/templates/bigscreen_main.html index 78f88af..9e7cbb1 100644 --- a/ljwx-bigscreen/bigscreen/bigScreen/templates/bigscreen_main.html +++ b/ljwx-bigscreen/bigscreen/bigScreen/templates/bigscreen_main.html @@ -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(); diff --git a/ljwx-bigscreen/bigscreen/bigScreen/templates/personal.html b/ljwx-bigscreen/bigscreen/bigScreen/templates/personal.html index 064b199..82f1f06 100644 --- a/ljwx-bigscreen/bigscreen/bigScreen/templates/personal.html +++ b/ljwx-bigscreen/bigscreen/bigScreen/templates/personal.html @@ -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) { diff --git a/ljwx-bigscreen/bigscreen/bigscreen.log b/ljwx-bigscreen/bigscreen/bigscreen.log index f3c1479..7c0f58f 100644 --- a/ljwx-bigscreen/bigscreen/bigscreen.log +++ b/ljwx-bigscreen/bigscreen/bigscreen.log @@ -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/python@3.13/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 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模块初始化成功 diff --git a/ljwx-bigscreen/bigscreen/config.py b/ljwx-bigscreen/bigscreen/config.py index 2efccf0..ff0c3e0 100644 --- a/ljwx-bigscreen/bigscreen/config.py +++ b/ljwx-bigscreen/bigscreen/config.py @@ -113,4 +113,37 @@ HEALTH_DATA_RANGES = { 'latitude': (22.5, 22.6), # 纬度范围(深圳) 'longitude': (114.0, 114.3), # 经度范围(深圳) 'stress': (30, 80), # 压力值范围 -} \ No newline at end of file +} + +# 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 \ No newline at end of file diff --git a/ljwx-bigscreen/bigscreen/system.log b/ljwx-bigscreen/bigscreen/system.log index f3c1479..7c0f58f 100644 --- a/ljwx-bigscreen/bigscreen/system.log +++ b/ljwx-bigscreen/bigscreen/system.log @@ -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/python@3.13/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 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模块初始化成功