From 69945b730fd3f6b6138ce50e49fc3392fcd74d71 Mon Sep 17 00:00:00 2001 From: yj <2077506045@qq.com> Date: 星期一, 28 七月 2025 18:16:52 +0800 Subject: [PATCH] 新增关键词过滤;新增活跃客服统计;新增掉线通知 --- app/workers/online_status_worker.py | 188 ++++ tests/test_message_processor.py | 130 ++ app/services/silence_service.py | 253 ++++++ tests/test_silence_service.py | 177 ++++ config.example.json | 65 + example_usage.py | 136 +++ app/services/email_service.py | 149 +++ logs/app.log | 15 app/services/group_stats_service.py | 251 +++++ tests/test_online_status_monitor.py | 168 ++++ app/services/dify_client.py | 26 test_group_stats.py | 119 ++ app/services/ecloud_client.py | 82 + app/services/sms_service.py | 143 +++ E云管家接口文档.txt | 48 + app/api/silence_mode.py | 191 ++++ config.py | 109 ++ app/api/group_stats.py | 154 +++ 短信发送接口文档.txt | 61 + app/services/message_processor.py | 47 20 files changed, 2,483 insertions(+), 29 deletions(-) diff --git "a/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" "b/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" index a275d7f..c7f58c0 100644 --- "a/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" +++ "b/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" @@ -339,4 +339,52 @@ "message": "澶辫触", "code": "1001", "data": null +} + + +# 鏌ヨ璐﹀彿涓湪绾跨殑寰俊鍒楄〃 + +绠�瑕佹弿杩帮細 +姝ゆ帴鍙e簲鐢ㄥ満鏅槸鏌ヨ鍦ㄧ嚎鐨剋id鍜寃cid鍒楄〃 + +璇锋眰URL锛� +http://鍩熷悕鍦板潃/queryLoginWx + +璇锋眰鏂瑰紡锛� +POST + +璇锋眰澶碒eaders锛� +Content-Type锛歛pplication/json +Authorization锛歔Authorization] + +鏃犲弬鏁帮細 + +杩斿洖鏁版嵁锛� +鍙傛暟鍚� 绫诲瀷 璇存槑 +code string 1000鎴愬姛锛堝湪绾匡級锛�1001澶辫触锛堢绾匡級 +message string 鍙嶉淇℃伅 +wcId string 寰俊id +wId string 鐧诲綍瀹炰緥鏍囪瘑 +璇锋眰鍙傛暟绀轰緥 + +{ + +} +鎴愬姛杩斿洖绀轰緥 + +{ + "code": "1000", + "message": "鎴愬姛", + "data": [ + { + "wcId": "wxid_i6qsbbjenju2", + "wId": "72223018-7f2a-4f4f-bfa3-26e47dbd61" + } + ] +} +澶辫触杩斿洖绀轰緥 + +{ + "code": "1001", + "message": "澶辫触" } \ No newline at end of file diff --git a/app/api/group_stats.py b/app/api/group_stats.py new file mode 100644 index 0000000..154e255 --- /dev/null +++ b/app/api/group_stats.py @@ -0,0 +1,154 @@ +""" +缇ょ粍缁熻API鎺ュ彛 +""" + +from typing import Optional +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from loguru import logger + +from app.services.group_stats_service import group_stats_service + + +router = APIRouter() + + +class GroupStatsResponse(BaseModel): + """缇ょ粍缁熻鍝嶅簲妯″瀷""" + success: bool + message: str + data: dict + + +class ClearStatsRequest(BaseModel): + """娓呯┖缁熻璇锋眰妯″瀷""" + group_id: str + + +@router.get("/group-stats/{group_id}", response_model=GroupStatsResponse) +async def get_group_stats(group_id: str): + """ + 鑾峰彇鎸囧畾缇ょ粍鐨勭粺璁′俊鎭� + + Args: + group_id: 缇ょ粍ID + + Returns: + 缇ょ粍缁熻淇℃伅 + """ + try: + stats_summary = group_stats_service.get_group_stats_summary(group_id) + + return GroupStatsResponse( + success=True, + message=f"鑾峰彇缇ょ粍缁熻淇℃伅鎴愬姛: {group_id}", + data=stats_summary + ) + + except Exception as e: + logger.error(f"鑾峰彇缇ょ粍缁熻淇℃伅澶辫触: group_id={group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"鑾峰彇缇ょ粍缁熻淇℃伅澶辫触: {str(e)}") + + +@router.get("/group-stats/{group_id}/most-active", response_model=GroupStatsResponse) +async def get_most_active_user(group_id: str): + """ + 鑾峰彇鎸囧畾缇ょ粍涓彂瑷�娆℃暟鏈�澶氱殑鐢ㄦ埛鏄电О + + Args: + group_id: 缇ょ粍ID + + Returns: + 鏈�娲昏穬鐢ㄦ埛淇℃伅 + """ + try: + most_active_nickname = group_stats_service.get_most_active_user_nickname(group_id) + message_stats = group_stats_service.get_group_message_stats(group_id) + + # 鎵惧埌鏈�娲昏穬鐢ㄦ埛鐨勮缁嗕俊鎭� + most_active_user_id = None + max_count = 0 + + for user_id, count in message_stats.items(): + if count > max_count: + max_count = count + most_active_user_id = user_id + + data = { + "group_id": group_id, + "most_active_nickname": most_active_nickname, + "most_active_user_id": most_active_user_id, + "message_count": max_count, + "total_users": len([count for count in message_stats.values() if count > 0]) + } + + return GroupStatsResponse( + success=True, + message=f"鑾峰彇鏈�娲昏穬鐢ㄦ埛鎴愬姛: {group_id}", + data=data + ) + + except Exception as e: + logger.error(f"鑾峰彇鏈�娲昏穬鐢ㄦ埛澶辫触: group_id={group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"鑾峰彇鏈�娲昏穬鐢ㄦ埛澶辫触: {str(e)}") + + +@router.post("/group-stats/clear", response_model=GroupStatsResponse) +async def clear_group_stats(request: ClearStatsRequest): + """ + 娓呯┖鎸囧畾缇ょ粍鐨勭粺璁℃暟鎹� + + Args: + request: 娓呯┖缁熻璇锋眰 + + Returns: + 鎿嶄綔缁撴灉 + """ + try: + success = group_stats_service.clear_group_stats(request.group_id) + + if success: + return GroupStatsResponse( + success=True, + message=f"缇ょ粍缁熻鏁版嵁宸叉竻绌�: {request.group_id}", + data={"group_id": request.group_id, "cleared": True} + ) + else: + raise HTTPException(status_code=400, detail=f"娓呯┖缇ょ粍缁熻鏁版嵁澶辫触: {request.group_id}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"娓呯┖缇ょ粍缁熻鏁版嵁澶辫触: group_id={request.group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"娓呯┖缇ょ粍缁熻鏁版嵁澶辫触: {str(e)}") + + +@router.get("/group-stats", response_model=GroupStatsResponse) +async def get_all_groups_overview(): + """ + 鑾峰彇鎵�鏈夌兢缁勭殑缁熻姒傝 + + Returns: + 鎵�鏈夌兢缁勭殑缁熻姒傝 + """ + try: + # 杩欓噷鍙互鎵╁睍涓鸿幏鍙栨墍鏈夌兢缁勭殑缁熻淇℃伅 + # 鐩墠杩斿洖鍩烘湰淇℃伅 + data = { + "message": "缇ょ粍缁熻鍔熻兘宸插惎鐢�", + "endpoints": { + "get_group_stats": "/api/v1/group-stats/{group_id}", + "get_most_active": "/api/v1/group-stats/{group_id}/most-active", + "clear_stats": "/api/v1/group-stats/clear" + } + } + + return GroupStatsResponse( + success=True, + message="鑾峰彇缇ょ粍缁熻姒傝鎴愬姛", + data=data + ) + + except Exception as e: + logger.error(f"鑾峰彇缇ょ粍缁熻姒傝澶辫触: error={str(e)}") + raise HTTPException(status_code=500, detail=f"鑾峰彇缇ょ粍缁熻姒傝澶辫触: {str(e)}") diff --git a/app/api/silence_mode.py b/app/api/silence_mode.py new file mode 100644 index 0000000..e1c6c0f --- /dev/null +++ b/app/api/silence_mode.py @@ -0,0 +1,191 @@ +""" +闈欓粯妯″紡绠$悊API +""" + +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel +from typing import Optional +from loguru import logger + +from app.services.silence_service import silence_service +from config import settings + + +router = APIRouter() + + +class SilenceModeResponse(BaseModel): + """闈欓粯妯″紡鍝嶅簲妯″瀷""" + success: bool + message: str + data: Optional[dict] = None + + +class ActivateSilenceRequest(BaseModel): + """婵�娲婚潤榛樻ā寮忚姹傛ā鍨�""" + group_id: str + + +@router.get("/silence-mode/status", response_model=SilenceModeResponse) +async def get_silence_mode_status(group_id: Optional[str] = None): + """ + 鑾峰彇闈欓粯妯″紡鐘舵�� + + Args: + group_id: 缇ょ粍ID锛屽鏋滀笉鎻愪緵鍒欒繑鍥炲叏灞�鐘舵�佹瑙� + + Returns: + 闈欓粯妯″紡鐘舵�佷俊鎭� + """ + try: + status = silence_service.get_silence_status(group_id) + + message = "鑾峰彇闈欓粯妯″紡鐘舵�佹垚鍔�" + if group_id: + message = f"鑾峰彇缇ょ粍闈欓粯妯″紡鐘舵�佹垚鍔�: {group_id}" + + return SilenceModeResponse( + success=True, + message=message, + data=status + ) + + except Exception as e: + logger.error(f"鑾峰彇闈欓粯妯″紡鐘舵�佸け璐�: group_id={group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"鑾峰彇闈欓粯妯″紡鐘舵�佸け璐�: {str(e)}") + + +@router.post("/silence-mode/activate", response_model=SilenceModeResponse) +async def activate_silence_mode(request: ActivateSilenceRequest): + """ + 鎵嬪姩婵�娲绘寚瀹氱兢缁勭殑闈欓粯妯″紡 + + Args: + request: 婵�娲昏姹傦紝鍖呭惈缇ょ粍ID + + Returns: + 鎿嶄綔缁撴灉 + """ + try: + if not settings.silence_mode_enabled: + raise HTTPException(status_code=400, detail="闈欓粯妯″紡鍔熻兘宸茬鐢�") + + success = silence_service.activate_silence_mode(request.group_id) + + if success: + status = silence_service.get_silence_status(request.group_id) + return SilenceModeResponse( + success=True, + message=f"缇ょ粍闈欓粯妯″紡宸叉縺娲�: {request.group_id}锛屾寔缁椂闂�: {settings.silence_duration_minutes} 鍒嗛挓", + data=status + ) + else: + raise HTTPException(status_code=400, detail=f"婵�娲荤兢缁勯潤榛樻ā寮忓け璐�: {request.group_id}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"婵�娲荤兢缁勯潤榛樻ā寮忓け璐�: group_id={request.group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"婵�娲荤兢缁勯潤榛樻ā寮忓け璐�: {str(e)}") + + +class ExtendSilenceRequest(BaseModel): + """寤堕暱闈欓粯妯″紡璇锋眰妯″瀷""" + group_id: str + + +@router.post("/silence-mode/extend", response_model=SilenceModeResponse) +async def extend_silence_mode(request: ExtendSilenceRequest): + """ + 寤堕暱鎸囧畾缇ょ粍鐨勯潤榛樻ā寮忔椂闂� + + Args: + request: 寤堕暱璇锋眰锛屽寘鍚兢缁処D + + Returns: + 鎿嶄綔缁撴灉 + """ + try: + if not settings.silence_mode_enabled: + raise HTTPException(status_code=400, detail="闈欓粯妯″紡鍔熻兘宸茬鐢�") + + success = silence_service.extend_silence_mode(request.group_id) + + if success: + status = silence_service.get_silence_status(request.group_id) + return SilenceModeResponse( + success=True, + message=f"缇ょ粍闈欓粯妯″紡鏃堕棿宸插埛鏂�: {request.group_id}锛屾寔缁椂闂�: {settings.silence_duration_minutes} 鍒嗛挓", + data=status + ) + else: + raise HTTPException(status_code=400, detail=f"寤堕暱缇ょ粍闈欓粯妯″紡澶辫触: {request.group_id}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"寤堕暱缇ょ粍闈欓粯妯″紡澶辫触: group_id={request.group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"寤堕暱缇ょ粍闈欓粯妯″紡澶辫触: {str(e)}") + + +class DeactivateSilenceRequest(BaseModel): + """鍋滅敤闈欓粯妯″紡璇锋眰妯″瀷""" + group_id: Optional[str] = None # 濡傛灉涓篘one鍒欏仠鐢ㄦ墍鏈夌兢缁� + + +@router.post("/silence-mode/deactivate", response_model=SilenceModeResponse) +async def deactivate_silence_mode(request: DeactivateSilenceRequest): + """ + 鎵嬪姩鍋滅敤闈欓粯妯″紡 + + Args: + request: 鍋滅敤璇锋眰锛実roup_id涓篘one鏃跺仠鐢ㄦ墍鏈夌兢缁� + + Returns: + 鎿嶄綔缁撴灉 + """ + try: + success = silence_service.deactivate_silence_mode(request.group_id) + + if success: + status = silence_service.get_silence_status() + message = "鎵�鏈夌兢缁勭殑闈欓粯妯″紡宸插仠鐢�" if not request.group_id else f"缇ょ粍闈欓粯妯″紡宸插仠鐢�: {request.group_id}" + + return SilenceModeResponse( + success=True, + message=message, + data=status + ) + else: + detail = "鍋滅敤闈欓粯妯″紡澶辫触" if not request.group_id else f"鍋滅敤缇ょ粍闈欓粯妯″紡澶辫触: {request.group_id}" + raise HTTPException(status_code=400, detail=detail) + + except HTTPException: + raise + except Exception as e: + logger.error(f"鍋滅敤闈欓粯妯″紡澶辫触: group_id={request.group_id}, error={str(e)}") + raise HTTPException(status_code=500, detail=f"鍋滅敤闈欓粯妯″紡澶辫触: {str(e)}") + + +@router.get("/silence-mode/config") +async def get_silence_mode_config(): + """ + 鑾峰彇闈欓粯妯″紡閰嶇疆淇℃伅 + + Returns: + 閰嶇疆淇℃伅 + """ + try: + return { + "success": True, + "data": { + "enabled": settings.silence_mode_enabled, + "duration_minutes": settings.silence_duration_minutes, + "description": "缇ょ粍闈欓粯妯″紡鍔熻兘锛氬綋濂藉弸娑堟伅琚拷鐣ユ椂锛岃嚜鍔ㄦ縺娲昏缇ょ粍鐨勯潤榛樻ā寮忥紝鍦ㄦ寚瀹氭椂闂村唴涓嶅鐞嗚缇ょ粍鐨勬秷鎭�" + }, + "message": "鑾峰彇闈欓粯妯″紡閰嶇疆淇℃伅鎴愬姛" + } + + except Exception as e: + logger.error(f"鑾峰彇闈欓粯妯″紡閰嶇疆淇℃伅澶辫触: {str(e)}") + raise HTTPException(status_code=500, detail=f"鑾峰彇闈欓粯妯″紡閰嶇疆淇℃伅澶辫触: {str(e)}") diff --git a/app/services/dify_client.py b/app/services/dify_client.py index e7ab10e..0e6bbda 100644 --- a/app/services/dify_client.py +++ b/app/services/dify_client.py @@ -28,6 +28,7 @@ user: str, conversation_id: Optional[str] = None, max_retries: int = None, + nick_name: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """ 鍙戦�佸璇濇秷鎭紙闈炴祦寮忔ā寮忥級 @@ -37,6 +38,7 @@ user: 鐢ㄦ埛鏍囪瘑 conversation_id: 浼氳瘽ID锛堝彲閫夛級 max_retries: 鏈�澶ч噸璇曟鏁� + nick_name: 鏄电О锛堝彲閫夛紝鐢ㄤ簬浼犻�掔粰dify锛� Returns: 鍝嶅簲鏁版嵁瀛楀吀锛屽け璐ヨ繑鍥濶one @@ -45,11 +47,17 @@ max_retries = settings.max_retry_count url = f"{self.base_url}/chat-messages" + + # 鏋勫缓inputs鍙傛暟 + inputs = {} + if nick_name: + inputs["nick_name"] = nick_name + payload = { "query": query, "response_mode": "blocking", # 浣跨敤闃诲妯″紡锛堥潪娴佸紡锛� "user": user, - "inputs": {}, + "inputs": inputs, } # 濡傛灉鏈変細璇滻D锛屾坊鍔犲埌璇锋眰涓� @@ -105,6 +113,7 @@ user: str, conversation_id: Optional[str] = None, max_retries: int = None, + nick_name: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """ 鍙戦�佸璇濇秷鎭紙娴佸紡妯″紡锛� @@ -114,6 +123,7 @@ user: 鐢ㄦ埛鏍囪瘑 conversation_id: 浼氳瘽ID锛堝彲閫夛級 max_retries: 鏈�澶ч噸璇曟鏁� + nick_name: 鏄电О锛堝彲閫夛紝鐢ㄤ簬浼犻�掔粰dify锛� Returns: 瀹屾暣鐨勫搷搴旀暟鎹瓧鍏革紝澶辫触杩斿洖None @@ -122,11 +132,17 @@ max_retries = settings.max_retry_count url = f"{self.base_url}/chat-messages" + + # 鏋勫缓inputs鍙傛暟 + inputs = {} + if nick_name: + inputs["nick_name"] = nick_name + payload = { "query": query, "response_mode": "streaming", # 浣跨敤娴佸紡妯″紡 "user": user, - "inputs": {}, + "inputs": inputs, } # 濡傛灉鏈変細璇滻D锛屾坊鍔犲埌璇锋眰涓� @@ -384,6 +400,7 @@ conversation_id: Optional[str] = None, max_retries: int = None, force_streaming: Optional[bool] = None, + nick_name: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """ 鍙戦�佸璇濇秷鎭紙鏍规嵁閰嶇疆閫夋嫨妯″紡锛� @@ -394,6 +411,7 @@ conversation_id: 浼氳瘽ID锛堝彲閫夛級 max_retries: 鏈�澶ч噸璇曟鏁� force_streaming: 寮哄埗浣跨敤娴佸紡妯″紡锛堝彲閫夛紝瑕嗙洊閰嶇疆锛� + nick_name: 鏄电О锛堝彲閫夛紝鐢ㄤ簬浼犻�掔粰dify锛� Returns: 鍝嶅簲鏁版嵁瀛楀吀锛屽け璐ヨ繑鍥濶one @@ -403,10 +421,10 @@ if use_streaming: logger.info(f"浣跨敤娴佸紡妯″紡鍙戦�佹秷鎭�: user={user}") - return self.send_chat_message_stream(query, user, conversation_id, max_retries) + return self.send_chat_message_stream(query, user, conversation_id, max_retries, nick_name) else: logger.info(f"浣跨敤闃诲妯″紡鍙戦�佹秷鎭�: user={user}") - return self.send_chat_message(query, user, conversation_id, max_retries) + return self.send_chat_message(query, user, conversation_id, max_retries, nick_name) def get_conversation_messages( self, conversation_id: str, user: str diff --git a/app/services/ecloud_client.py b/app/services/ecloud_client.py index 42495c3..b8a64ef 100644 --- a/app/services/ecloud_client.py +++ b/app/services/ecloud_client.py @@ -20,6 +20,27 @@ self.session = requests.Session() self.session.headers.update(self.headers) + def _filter_keywords(self, content: str) -> str: + """ + 杩囨护娑堟伅鍐呭涓殑鍏抽敭璇� + + Args: + content: 鍘熷娑堟伅鍐呭 + + Returns: + 杩囨护鍚庣殑娑堟伅鍐呭 + """ + if not settings.keyword_filter_enabled or not settings.keyword_filter_keywords: + return content + + filtered_content = content + for keyword in settings.keyword_filter_keywords: + if keyword in filtered_content: + filtered_content = filtered_content.replace(keyword, "") + logger.info(f"杩囨护鍏抽敭璇�: {keyword}") + + return filtered_content + def get_contact_info(self, w_id: str, wc_id: str) -> Optional[Dict[str, Any]]: """ 鑾峰彇鑱旂郴浜轰俊鎭� @@ -84,18 +105,21 @@ Returns: 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse """ + # 杩囨护鍏抽敭璇� + filtered_content = self._filter_keywords(content) + if max_retries is None: from config import settings max_retries = settings.max_retry_count - + retry_count = 0 while retry_count <= max_retries: try: url = f"{self.base_url}/sendText" - payload = {"wId": w_id, "wcId": wc_id, "content": content} + payload = {"wId": w_id, "wcId": wc_id, "content": filtered_content} logger.info( - f"鍙戦�佹枃鏈秷鎭�: wId={w_id}, wcId={wc_id}, content_length={len(content)}, retry={retry_count}" + f"鍙戦�佹枃鏈秷鎭�: wId={w_id}, wcId={wc_id}, content_length={len(filtered_content)}, retry={retry_count}" ) response = self.session.post(url, json=payload, timeout=30) @@ -141,7 +165,9 @@ Returns: 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse """ - return self.send_text_message(w_id, group_id, content) + # 杩囨护鍏抽敭璇� + filtered_content = self._filter_keywords(content) + return self.send_text_message(w_id, group_id, filtered_content) def init_address_list(self, w_id: str) -> bool: """ @@ -238,6 +264,9 @@ Returns: 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse """ + # 杩囨护鍏抽敭璇� + filtered_content = self._filter_keywords(content) + if max_retries is None: from config import settings max_retries = settings.max_retry_count @@ -251,12 +280,12 @@ payload = { "wId": w_id, "wcId": wc_id, - "content": content, + "content": filtered_content, "at": at_str } logger.info( - f"鍙戦�佺兢鑱夽娑堟伅: wId={w_id}, wcId={wc_id}, at={at_str}, content_length={len(content)}, retry={retry_count}" + f"鍙戦�佺兢鑱夽娑堟伅: wId={w_id}, wcId={wc_id}, at={at_str}, content_length={len(filtered_content)}, retry={retry_count}" ) response = self.session.post(url, json=payload, timeout=30) @@ -290,6 +319,47 @@ ) return False + def query_online_wechat_list(self) -> Optional[List[Dict[str, str]]]: + """ + 鏌ヨ璐﹀彿涓湪绾跨殑寰俊鍒楄〃 + + Returns: + 鍦ㄧ嚎寰俊鍒楄〃锛屾瘡涓厓绱犲寘鍚玾cId鍜寃Id锛屽け璐ヨ繑鍥濶one + 杩斿洖鏍煎紡: [ + { + "wcId": "wxid_i6qsbbjenju2", + "wId": "72223018-7f2a-4f4f-bfa3-26e47dbd61" + } + ] + """ + try: + url = f"{self.base_url}/queryLoginWx" + payload = {} + + logger.info("鏌ヨ鍦ㄧ嚎寰俊鍒楄〃") + + response = self.session.post(url, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + + if result.get("code") == "1000": + online_list = result.get("data", []) + logger.info(f"鎴愬姛鏌ヨ鍦ㄧ嚎寰俊鍒楄〃: count={len(online_list)}") + return online_list + else: + logger.warning( + f"鏌ヨ鍦ㄧ嚎寰俊鍒楄〃澶辫触: code={result.get('code')}, message={result.get('message')}" + ) + return [] + + except requests.exceptions.RequestException as e: + logger.error(f"鏌ヨ鍦ㄧ嚎寰俊鍒楄〃缃戠粶閿欒: error={str(e)}") + return None + except Exception as e: + logger.error(f"鏌ヨ鍦ㄧ嚎寰俊鍒楄〃寮傚父: error={str(e)}") + return None + # 鍏ㄥ眬E浜戠瀹跺鎴风瀹炰緥 ecloud_client = ECloudClient() diff --git a/app/services/email_service.py b/app/services/email_service.py new file mode 100644 index 0000000..8f7b8d8 --- /dev/null +++ b/app/services/email_service.py @@ -0,0 +1,149 @@ +""" +閭欢鍙戦�佹湇鍔� +""" + +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from typing import List, Optional +from loguru import logger +from config import settings + + +class EmailService: + """閭欢鍙戦�佹湇鍔�""" + + def __init__(self): + self.smtp_server = settings.email_smtp_server + self.smtp_port = settings.email_smtp_port + self.username = settings.email_smtp_username + self.password = settings.email_smtp_password + self.from_email = settings.email_from_email + + def send_email(self, to_emails: List[str], subject: str, content: str) -> bool: + """ + 鍙戦�侀偖浠� + + Args: + to_emails: 鏀朵欢浜洪偖绠卞垪琛� + subject: 閭欢涓婚 + content: 閭欢鍐呭 + + Returns: + 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + if not settings.email_enabled: + logger.info("閭欢鍙戦�佸姛鑳藉凡绂佺敤") + return True + + if not to_emails: + logger.warning("鏀朵欢浜洪偖绠卞垪琛ㄤ负绌�") + return False + + try: + # 鍒涘缓閭欢瀵硅薄 + msg = MIMEMultipart() + msg["From"] = self.from_email + msg["To"] = ", ".join(to_emails) + msg["Subject"] = subject + + # 娣诲姞閭欢鍐呭 + msg.attach(MIMEText(content, "plain", "utf-8")) + + # 杩炴帴SMTP鏈嶅姟鍣ㄥ苟鍙戦�侀偖浠� + server = None + email_sent = False + + try: + server = smtplib.SMTP(self.smtp_server, self.smtp_port) + server.starttls() # 鍚敤TLS鍔犲瘑 + server.login(self.username, self.password) + + # 鍙戦�侀偖浠� + text = msg.as_string() + server.sendmail(self.from_email, to_emails, text) + email_sent = True + + logger.info(f"閭欢鍙戦�佹垚鍔�: to={to_emails}, subject={subject}") + + finally: + # 瀹夊叏鍏抽棴杩炴帴锛屽拷鐣ュ叧闂椂鐨勫紓甯� + if server: + try: + server.quit() + except Exception: + # 蹇界暐鍏抽棴杩炴帴鏃剁殑寮傚父锛屽洜涓洪偖浠跺彲鑳藉凡缁忓彂閫佹垚鍔� + pass + + return email_sent + + except smtplib.SMTPAuthenticationError as e: + logger.error(f"閭欢鍙戦�佽璇佸け璐�: error={str(e)}") + return False + except smtplib.SMTPException as e: + logger.error(f"閭欢鍙戦�丼MTP閿欒: error={str(e)}") + return False + except Exception as e: + logger.error(f"閭欢鍙戦�佸紓甯�: error={str(e)}") + return False + + def send_notification(self, subject: str, content: str) -> bool: + """ + 鍙戦�侀�氱煡閭欢鍒伴厤缃殑鏀朵欢浜哄垪琛� + + Args: + subject: 閭欢涓婚 + content: 閭欢鍐呭 + + Returns: + 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + return self.send_email(settings.email_to_emails, subject, content) + + def test_connection(self) -> bool: + """ + 娴嬭瘯閭欢鏈嶅姟鍣ㄨ繛鎺� + + Returns: + 杩炴帴鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + if not settings.email_enabled: + logger.info("閭欢鍙戦�佸姛鑳藉凡绂佺敤") + return True + + try: + server = None + connection_success = False + + try: + server = smtplib.SMTP(self.smtp_server, self.smtp_port) + server.starttls() + server.login(self.username, self.password) + connection_success = True + + logger.info("閭欢鏈嶅姟鍣ㄨ繛鎺ユ祴璇曟垚鍔�") + + finally: + # 瀹夊叏鍏抽棴杩炴帴锛屽拷鐣ュ叧闂椂鐨勫紓甯� + if server: + try: + server.quit() + except Exception: + # 蹇界暐鍏抽棴杩炴帴鏃剁殑寮傚父 + pass + + return connection_success + + except smtplib.SMTPAuthenticationError as e: + logger.error(f"閭欢鏈嶅姟鍣ㄨ璇佸け璐�: error={str(e)}") + return False + except smtplib.SMTPException as e: + logger.error(f"閭欢鏈嶅姟鍣ㄨ繛鎺MTP閿欒: error={str(e)}") + return False + except Exception as e: + logger.error(f"閭欢鏈嶅姟鍣ㄨ繛鎺ュ紓甯�: error={str(e)}") + return False + + +# 鍏ㄥ眬閭欢鏈嶅姟瀹炰緥 +email_service = EmailService() diff --git a/app/services/group_stats_service.py b/app/services/group_stats_service.py new file mode 100644 index 0000000..1feb2be --- /dev/null +++ b/app/services/group_stats_service.py @@ -0,0 +1,251 @@ +""" +缇ょ粍缁熻鏈嶅姟 +鐢ㄤ簬缁熻缇ょ粍涓ソ鍙嬬殑鍙戣█娆℃暟 +""" + +import time +from typing import Optional, Dict, List, Tuple +from loguru import logger +from app.services.redis_queue import redis_queue +from app.models.contact import Contact +from app.models.database import get_db +from config import settings + + +class GroupStatsService: + """缇ょ粍缁熻鏈嶅姟""" + + def __init__(self): + self.stats_key_prefix = "group_stats:" + self.stats_expiry = 24 * 60 * 60 # 24灏忔椂杩囨湡 + + def _get_group_stats_key(self, group_id: str) -> str: + """ + 鑾峰彇缇ょ粍缁熻鐨凴edis閿悕 + + Args: + group_id: 缇ょ粍ID + + Returns: + Redis閿悕 + """ + return f"{self.stats_key_prefix}{group_id}" + + def increment_user_message_count(self, group_id: str, user_id: str) -> bool: + """ + 澧炲姞鐢ㄦ埛鍦ㄧ兢缁勪腑鐨勫彂瑷�娆℃暟 + + Args: + group_id: 缇ょ粍ID + user_id: 鐢ㄦ埛ID + + Returns: + 鎿嶄綔鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + stats_key = self._get_group_stats_key(group_id) + + # 浣跨敤Redis鐨凥INCRBY鍛戒护澧炲姞璁℃暟 + redis_queue.redis_client.hincrby(stats_key, user_id, 1) + + # 璁剧疆杩囨湡鏃堕棿 + redis_queue.redis_client.expire(stats_key, self.stats_expiry) + + logger.debug(f"鐢ㄦ埛鍙戣█娆℃暟宸插鍔�: group={group_id}, user={user_id}") + return True + + except Exception as e: + logger.error( + f"澧炲姞鐢ㄦ埛鍙戣█娆℃暟澶辫触: group={group_id}, user={user_id}, error={str(e)}" + ) + return False + + def get_group_message_stats(self, group_id: str) -> Dict[str, int]: + """ + 鑾峰彇缇ょ粍涓墍鏈夌敤鎴风殑鍙戣█娆℃暟缁熻 + + Args: + group_id: 缇ょ粍ID + + Returns: + 鐢ㄦ埛ID鍒板彂瑷�娆℃暟鐨勬槧灏勫瓧鍏� + """ + try: + stats_key = self._get_group_stats_key(group_id) + stats = redis_queue.redis_client.hgetall(stats_key) + + # 灏嗗瓧鑺傚瓧绗︿覆杞崲涓烘櫘閫氬瓧绗︿覆锛屽苟杞崲璁℃暟涓烘暣鏁� + result = {} + for user_id, count in stats.items(): + if isinstance(user_id, bytes): + user_id = user_id.decode("utf-8") + if isinstance(count, bytes): + count = count.decode("utf-8") + result[user_id] = int(count) + + logger.debug(f"鑾峰彇缇ょ粍鍙戣█缁熻: group={group_id}, stats={result}") + return result + + except Exception as e: + logger.error(f"鑾峰彇缇ょ粍鍙戣█缁熻澶辫触: group={group_id}, error={str(e)}") + return {} + + def get_most_active_user_nickname(self, group_id: str) -> str: + """ + 鑾峰彇缇ょ粍涓彂瑷�娆℃暟鏈�澶氱殑鐢ㄦ埛鏄电О + + Args: + group_id: 缇ょ粍ID + + Returns: + 鍙戣█娆℃暟鏈�澶氱殑鐢ㄦ埛鏄电О锛屽鏋滄病鏈夋壘鍒版垨鍙戣█娆℃暟閮界浉鍚屾垨閮戒负0锛屽垯杩斿洖榛樿瀹㈡湇鍚嶇О + """ + try: + # 鑾峰彇缇ょ粍鍙戣█缁熻 + stats = self.get_group_message_stats(group_id) + + if not stats: + logger.info(f"缇ょ粍鏃犲彂瑷�缁熻鏁版嵁锛屼娇鐢ㄩ粯璁ゅ鏈嶅悕绉�: group={group_id}") + return settings.customer_service_default_name + + # 杩囨护鎺夊彂瑷�娆℃暟涓�0鐨勭敤鎴� + active_stats = { + user_id: count for user_id, count in stats.items() if count > 0 + } + + if not active_stats: + logger.info(f"缇ょ粍鏃犳湁鏁堝彂瑷�鏁版嵁锛屼娇鐢ㄩ粯璁ゅ鏈嶅悕绉�: group={group_id}") + return settings.customer_service_default_name + + # 妫�鏌ユ槸鍚︽墍鏈夌敤鎴峰彂瑷�娆℃暟閮界浉鍚� + counts = list(active_stats.values()) + if len(set(counts)) == 1: + logger.info( + f"缇ょ粍鎵�鏈夌敤鎴峰彂瑷�娆℃暟鐩稿悓锛屼娇鐢ㄩ粯璁ゅ鏈嶅悕绉�: group={group_id}" + ) + return settings.customer_service_default_name + + # 鎵惧埌鍙戣█娆℃暟鏈�澶氱殑鐢ㄦ埛 + most_active_user_id = max(active_stats, key=active_stats.get) + max_count = active_stats[most_active_user_id] + + logger.info( + f"鎵惧埌鏈�娲昏穬鐢ㄦ埛: group={group_id}, user={most_active_user_id}, count={max_count}" + ) + + # 鏍规嵁鐢ㄦ埛ID鏌ユ壘鏄电О + nickname = self._get_user_nickname(most_active_user_id) + + if nickname: + logger.info( + f"鑾峰彇鍒版渶娲昏穬鐢ㄦ埛鏄电О: group={group_id}, user={most_active_user_id}, nickname={nickname}" + ) + return nickname + else: + logger.warning( + f"鏈壘鍒版渶娲昏穬鐢ㄦ埛鏄电О锛屼娇鐢ㄩ粯璁ゅ鏈嶅悕绉�: group={group_id}, user={most_active_user_id}" + ) + return settings.customer_service_default_name + + except Exception as e: + logger.error(f"鑾峰彇鏈�娲昏穬鐢ㄦ埛鏄电О澶辫触: group={group_id}, error={str(e)}") + return settings.customer_service_default_name + + def _get_user_nickname(self, user_id: str) -> Optional[str]: + """ + 鏍规嵁鐢ㄦ埛ID鑾峰彇鏄电О + + Args: + user_id: 鐢ㄦ埛ID + + Returns: + 鐢ㄦ埛鏄电О锛屽鏋滄湭鎵惧埌杩斿洖None + """ + try: + with next(get_db()) as db: + contact = db.query(Contact).filter(Contact.wc_id == user_id).first() + if contact and contact.nick_name: + return contact.nick_name + else: + logger.warning(f"鏈壘鍒扮敤鎴锋樀绉�: user_id={user_id}") + return None + except Exception as e: + logger.error(f"鏌ヨ鐢ㄦ埛鏄电О寮傚父: user_id={user_id}, error={str(e)}") + return None + + def get_group_stats_summary(self, group_id: str) -> Dict: + """ + 鑾峰彇缇ょ粍缁熻鎽樿淇℃伅 + + Args: + group_id: 缇ょ粍ID + + Returns: + 鍖呭惈缁熻鎽樿鐨勫瓧鍏� + """ + try: + stats = self.get_group_message_stats(group_id) + most_active_nickname = self.get_most_active_user_nickname(group_id) + + # 璁$畻鎬诲彂瑷�娆℃暟 + total_messages = sum(stats.values()) + + # 鑾峰彇娲昏穬鐢ㄦ埛鏁伴噺锛堝彂瑷�娆℃暟>0锛� + active_users = len([count for count in stats.values() if count > 0]) + + # 鏋勫缓鐢ㄦ埛鏄电О缁熻 + user_stats = [] + for user_id, count in stats.items(): + if count > 0: + nickname = self._get_user_nickname(user_id) + user_stats.append( + { + "user_id": user_id, + "nickname": nickname or "鏈煡鐢ㄦ埛", + "message_count": count, + } + ) + + # 鎸夊彂瑷�娆℃暟鎺掑簭 + user_stats.sort(key=lambda x: x["message_count"], reverse=True) + + return { + "group_id": group_id, + "total_messages": total_messages, + "active_users": active_users, + "most_active_nickname": most_active_nickname, + "user_stats": user_stats, + } + + except Exception as e: + logger.error(f"鑾峰彇缇ょ粍缁熻鎽樿澶辫触: group={group_id}, error={str(e)}") + return { + "group_id": group_id, + "total_messages": 0, + "active_users": 0, + "most_active_nickname": settings.customer_service_default_name, + "user_stats": [], + } + + def clear_group_stats(self, group_id: str) -> bool: + """ + 娓呯┖鎸囧畾缇ょ粍鐨勭粺璁℃暟鎹� + + Args: + group_id: 缇ょ粍ID + + Returns: + 鎿嶄綔鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + stats_key = self._get_group_stats_key(group_id) + redis_queue.redis_client.delete(stats_key) + logger.info(f"宸叉竻绌虹兢缁勭粺璁℃暟鎹�: group={group_id}") + return True + except Exception as e: + logger.error(f"娓呯┖缇ょ粍缁熻鏁版嵁澶辫触: group={group_id}, error={str(e)}") + return False + + +# 鍏ㄥ眬缇ょ粍缁熻鏈嶅姟瀹炰緥 +group_stats_service = GroupStatsService() diff --git a/app/services/message_processor.py b/app/services/message_processor.py index e70555a..11b1bf6 100644 --- a/app/services/message_processor.py +++ b/app/services/message_processor.py @@ -17,6 +17,8 @@ from app.services.ecloud_client import ecloud_client from app.services.dify_client import dify_client from app.services.friend_ignore_service import friend_ignore_service +from app.services.silence_service import silence_service +from app.services.group_stats_service import group_stats_service from config import settings @@ -102,10 +104,34 @@ logger.warning(f"娑堟伅缂哄皯蹇呰瀛楁: data={data}") return False - # 妫�鏌ュ彂閫佽�呮槸鍚﹀湪濂藉弸蹇界暐鍒楄〃涓� + # 鑾峰彇鐢ㄦ埛鍜岀兢缁勪俊鎭� from_user = data.get("fromUser") - if friend_ignore_service.is_friend_ignored(from_user): - logger.info(f"蹇界暐濂藉弸鍙戦�佺殑娑堟伅: fromUser={from_user}") + from_group = data.get("fromGroup") + + # 妫�鏌ュ彂閫佽�呮槸鍚﹀湪濂藉弸蹇界暐鍒楄〃涓� + is_friend_ignored = friend_ignore_service.is_friend_ignored(from_user) + + if is_friend_ignored: + logger.info(f"蹇界暐濂藉弸鍙戦�佺殑娑堟伅: fromUser={from_user}, fromGroup={from_group}") + # 缁熻琚拷鐣ョ殑濂藉弸鍙戣█娆℃暟锛堢‘淇濊蹇界暐鐨勫ソ鍙嬫秷鎭篃绾冲叆缁熻锛� + group_stats_service.increment_user_message_count(from_group, from_user) + # 婵�娲绘垨寤堕暱璇ョ兢缁勭殑闈欓粯妯″紡 + if silence_service.is_silence_active(from_group): + # 濡傛灉璇ョ兢缁勯潤榛樻ā寮忓凡婵�娲伙紝寤堕暱鏃堕棿 + silence_service.extend_silence_mode(from_group) + logger.info(f"濂藉弸娑堟伅琚拷鐣ワ紝缇ょ粍闈欓粯妯″紡鏃堕棿宸插埛鏂�: fromUser={from_user}, fromGroup={from_group}") + else: + # 濡傛灉璇ョ兢缁勯潤榛樻ā寮忔湭婵�娲伙紝婵�娲婚潤榛樻ā寮� + silence_service.activate_silence_mode(from_group) + logger.info(f"濂藉弸娑堟伅琚拷鐣ワ紝缇ょ粍闈欓粯妯″紡宸叉縺娲�: fromUser={from_user}, fromGroup={from_group}") + return False + + # # 缁熻姝e父澶勭悊鐨勫ソ鍙嬪彂瑷�娆℃暟 + # group_stats_service.increment_user_message_count(from_group, from_user) + + # 妫�鏌ヨ缇ょ粍鐨勯潤榛樻ā寮忔槸鍚︽縺娲伙紙鍦ㄥソ鍙嬪拷鐣ユ鏌ヤ箣鍚庯級 + if silence_service.is_silence_active(from_group): + logger.info(f"缇ょ粍闈欓粯妯″紡婵�娲讳腑锛屽拷鐣ユ秷鎭�: fromGroup={from_group}") return False return True @@ -214,12 +240,19 @@ logger.error(f"鑱旂郴浜轰俊鎭鐞嗗け璐�: from_group={from_group}") return False - # 3.2 鑾峰彇鐢ㄦ埛鍦ㄥ綋鍓嶇兢缁勭殑conversation_id + # 3.2 鑾峰彇缇ょ粍涓彂瑷�娆℃暟鏈�澶氱殑鐢ㄦ埛鏄电О + most_active_nickname = group_stats_service.get_most_active_user_nickname(from_group) + logger.info(f"缇ょ粍鏈�娲昏穬鐢ㄦ埛鏄电О: group={from_group}, nickname={most_active_nickname}") + + # 3.3 鑾峰彇鐢ㄦ埛鍦ㄥ綋鍓嶇兢缁勭殑conversation_id conversation_id = redis_queue.get_conversation_id(from_user, from_group) # 璋冪敤Dify鎺ュ彛鍙戦�佹秷鎭紙鏍规嵁閰嶇疆閫夋嫨妯″紡锛� dify_response = dify_client.send_message( - query=content, user=from_user, conversation_id=conversation_id + query=content, + user=from_user, + conversation_id=conversation_id, + nick_name=most_active_nickname ) if not dify_response: @@ -232,9 +265,9 @@ # 鏇存柊Redis涓殑conversation_id锛堝熀浜庣敤鎴�+缇ょ粍锛� if new_conversation_id: - redis_queue.set_conversation_id(from_user, new_conversation_id, from_group) + redis_queue.set_conversation_id(from_user, new_conversation_id, from_group, 1800) - # 3.3 淇濆瓨瀵硅瘽璁板綍鍒版暟鎹簱 + # 3.4 淇濆瓨瀵硅瘽璁板綍鍒版暟鎹簱 # 鎸夌敤鎴枫�佺兢缁勫拰灏忔椂鍒嗙粍瀵硅瘽璁板綍 current_time = datetime.now() hour_key = current_time.strftime("%Y%m%d_%H") diff --git a/app/services/silence_service.py b/app/services/silence_service.py new file mode 100644 index 0000000..0161198 --- /dev/null +++ b/app/services/silence_service.py @@ -0,0 +1,253 @@ +""" +闈欓粯妯″紡绠$悊鏈嶅姟 +""" + +import time +from typing import Optional +from loguru import logger + +from app.services.redis_queue import redis_queue +from config import settings + + +class SilenceService: + """闈欓粯妯″紡绠$悊鏈嶅姟""" + + def __init__(self): + self.silence_key_prefix = "ecloud_silence_mode:" + self.silence_end_time_key_prefix = "ecloud_silence_end_time:" + + def _get_group_silence_key(self, group_id: str) -> str: + """鑾峰彇缇ょ粍闈欓粯妯″紡閿�""" + return f"{self.silence_key_prefix}{group_id}" + + def _get_group_silence_end_time_key(self, group_id: str) -> str: + """鑾峰彇缇ょ粍闈欓粯缁撴潫鏃堕棿閿�""" + return f"{self.silence_end_time_key_prefix}{group_id}" + + def activate_silence_mode(self, group_id: str) -> bool: + """ + 婵�娲绘寚瀹氱兢缁勭殑闈欓粯妯″紡 + + Args: + group_id: 缇ょ粍ID + + Returns: + 婵�娲绘垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + try: + if not settings.silence_mode_enabled: + logger.debug("闈欓粯妯″紡鍔熻兘宸茬鐢�") + return False + + # 璁$畻闈欓粯缁撴潫鏃堕棿锛堝綋鍓嶆椂闂� + 閰嶇疆鐨勫垎閽熸暟锛� + silence_duration_seconds = settings.silence_duration_minutes * 60 + end_time = time.time() + silence_duration_seconds + + # 鑾峰彇缇ょ粍涓撶敤鐨勯敭 + group_silence_key = self._get_group_silence_key(group_id) + group_end_time_key = self._get_group_silence_end_time_key(group_id) + + # 璁剧疆闈欓粯妯″紡鏍囧織鍜岀粨鏉熸椂闂� + redis_queue.redis_client.setex( + group_silence_key, + silence_duration_seconds, + "active" + ) + redis_queue.redis_client.setex( + group_end_time_key, + silence_duration_seconds, + str(end_time) + ) + + logger.info(f"缇ょ粍闈欓粯妯″紡宸叉縺娲�: group_id={group_id}, 鎸佺画鏃堕棿: {settings.silence_duration_minutes} 鍒嗛挓") + return True + + except Exception as e: + logger.error(f"婵�娲荤兢缁勯潤榛樻ā寮忓け璐�: group_id={group_id}, error={str(e)}") + return False + + def extend_silence_mode(self, group_id: str) -> bool: + """ + 寤堕暱鎸囧畾缇ょ粍鐨勯潤榛樻ā寮忔椂闂达紙鍒锋柊闈欓粯鏃堕暱锛� + + Args: + group_id: 缇ょ粍ID + + Returns: + 寤堕暱鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + if not settings.silence_mode_enabled: + logger.debug("闈欓粯妯″紡鍔熻兘宸茬鐢�") + return False + + # 閲嶆柊婵�娲婚潤榛樻ā寮忥紙鐩稿綋浜庡埛鏂版椂闀匡級 + return self.activate_silence_mode(group_id) + + except Exception as e: + logger.error(f"寤堕暱缇ょ粍闈欓粯妯″紡澶辫触: group_id={group_id}, error={str(e)}") + return False + + def is_silence_active(self, group_id: str) -> bool: + """ + 妫�鏌ユ寚瀹氱兢缁勭殑闈欓粯妯″紡鏄惁婵�娲� + + Args: + group_id: 缇ょ粍ID + + Returns: + 濡傛灉闈欓粯妯″紡婵�娲昏繑鍥濼rue锛屽惁鍒欒繑鍥濬alse + """ + try: + if not settings.silence_mode_enabled: + return False + + # 妫�鏌ョ兢缁勯潤榛樻ā寮忔爣蹇楁槸鍚﹀瓨鍦� + group_silence_key = self._get_group_silence_key(group_id) + is_active = redis_queue.redis_client.exists(group_silence_key) + + if is_active: + logger.debug(f"缇ょ粍闈欓粯妯″紡褰撳墠澶勪簬婵�娲荤姸鎬�: group_id={group_id}") + + return bool(is_active) + + except Exception as e: + logger.error(f"妫�鏌ョ兢缁勯潤榛樻ā寮忕姸鎬佸け璐�: group_id={group_id}, error={str(e)}") + return False + + def get_silence_remaining_time(self, group_id: str) -> Optional[int]: + """ + 鑾峰彇鎸囧畾缇ょ粍鐨勯潤榛樻ā寮忓墿浣欐椂闂达紙绉掞級 + + Args: + group_id: 缇ょ粍ID + + Returns: + 鍓╀綑鏃堕棿锛堢锛夛紝濡傛灉闈欓粯妯″紡鏈縺娲昏繑鍥濶one + """ + try: + if not self.is_silence_active(group_id): + return None + + # 鑾峰彇缇ょ粍闈欓粯缁撴潫鏃堕棿 + group_end_time_key = self._get_group_silence_end_time_key(group_id) + end_time_str = redis_queue.redis_client.get(group_end_time_key) + if not end_time_str: + return None + + end_time = float(end_time_str) + current_time = time.time() + remaining_time = int(end_time - current_time) + + return max(0, remaining_time) + + except Exception as e: + logger.error(f"鑾峰彇缇ょ粍闈欓粯妯″紡鍓╀綑鏃堕棿澶辫触: group_id={group_id}, error={str(e)}") + return None + + def deactivate_silence_mode(self, group_id: str = None) -> bool: + """ + 鎵嬪姩鍋滅敤闈欓粯妯″紡 + + Args: + group_id: 缇ょ粍ID锛屽鏋滀负None鍒欏仠鐢ㄦ墍鏈夌兢缁勭殑闈欓粯妯″紡 + + Returns: + 鍋滅敤鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + if group_id: + # 鍋滅敤鎸囧畾缇ょ粍鐨勯潤榛樻ā寮� + group_silence_key = self._get_group_silence_key(group_id) + group_end_time_key = self._get_group_silence_end_time_key(group_id) + + redis_queue.redis_client.delete(group_silence_key) + redis_queue.redis_client.delete(group_end_time_key) + + logger.info(f"缇ょ粍闈欓粯妯″紡宸叉墜鍔ㄥ仠鐢�: group_id={group_id}") + else: + # 鍋滅敤鎵�鏈夌兢缁勭殑闈欓粯妯″紡 + silence_keys = redis_queue.redis_client.keys(f"{self.silence_key_prefix}*") + end_time_keys = redis_queue.redis_client.keys(f"{self.silence_end_time_key_prefix}*") + + all_keys = silence_keys + end_time_keys + if all_keys: + redis_queue.redis_client.delete(*all_keys) + + logger.info("鎵�鏈夌兢缁勭殑闈欓粯妯″紡宸叉墜鍔ㄥ仠鐢�") + + return True + + except Exception as e: + logger.error(f"鍋滅敤闈欓粯妯″紡澶辫触: group_id={group_id}, error={str(e)}") + return False + + def get_silence_status(self, group_id: str = None) -> dict: + """ + 鑾峰彇闈欓粯妯″紡璇︾粏鐘舵�佷俊鎭� + + Args: + group_id: 缇ょ粍ID锛屽鏋滀负None鍒欒繑鍥炲叏灞�鐘舵�佹瑙� + + Returns: + 鍖呭惈闈欓粯妯″紡鐘舵�佷俊鎭殑瀛楀吀 + """ + try: + if group_id: + # 鑾峰彇鎸囧畾缇ょ粍鐨勭姸鎬� + status = { + "enabled": settings.silence_mode_enabled, + "group_id": group_id, + "active": False, + "remaining_seconds": None, + "remaining_minutes": None, + "duration_minutes": settings.silence_duration_minutes + } + + if settings.silence_mode_enabled: + status["active"] = self.is_silence_active(group_id) + if status["active"]: + remaining_seconds = self.get_silence_remaining_time(group_id) + if remaining_seconds is not None: + status["remaining_seconds"] = remaining_seconds + status["remaining_minutes"] = round(remaining_seconds / 60, 1) + else: + # 鑾峰彇鍏ㄥ眬鐘舵�佹瑙� + active_groups = [] + if settings.silence_mode_enabled: + # 鏌ユ壘鎵�鏈夋縺娲荤殑缇ょ粍 + silence_keys = redis_queue.redis_client.keys(f"{self.silence_key_prefix}*") + for key in silence_keys: + group_id_from_key = key.replace(self.silence_key_prefix, "") + remaining_time = self.get_silence_remaining_time(group_id_from_key) + if remaining_time and remaining_time > 0: + active_groups.append({ + "group_id": group_id_from_key, + "remaining_seconds": remaining_time, + "remaining_minutes": round(remaining_time / 60, 1) + }) + + status = { + "enabled": settings.silence_mode_enabled, + "active_groups_count": len(active_groups), + "active_groups": active_groups, + "duration_minutes": settings.silence_duration_minutes + } + + return status + + except Exception as e: + logger.error(f"鑾峰彇闈欓粯妯″紡鐘舵�佸け璐�: group_id={group_id}, error={str(e)}") + return { + "enabled": False, + "active": False, + "remaining_seconds": None, + "remaining_minutes": None, + "duration_minutes": 0, + "error": str(e) + } + + +# 鍏ㄥ眬闈欓粯鏈嶅姟瀹炰緥 +silence_service = SilenceService() diff --git a/app/services/sms_service.py b/app/services/sms_service.py new file mode 100644 index 0000000..a592039 --- /dev/null +++ b/app/services/sms_service.py @@ -0,0 +1,143 @@ +""" +鐭俊鍙戦�佹湇鍔� +""" + +import hashlib +import time +import requests +from typing import List, Optional +from loguru import logger +from config import settings + + +class SmsService: + """鐭俊鍙戦�佹湇鍔�""" + + def __init__(self): + self.api_url = settings.sms_api_url + self.username = settings.sms_username + self.password = settings.sms_password + self.session = requests.Session() + self.session.headers.update({ + "Accept": "application/json", + "Content-Type": "application/json;charset=utf-8" + }) + + def _generate_sign(self, timestamp: int) -> str: + """ + 鐢熸垚绛惧悕 + 璁$畻瑙勫垯锛歁D5(userName+timestamp+MD5(password)) + + Args: + timestamp: 鏃堕棿鎴筹紙姣锛� + + Returns: + 绛惧悕瀛楃涓� + """ + # 璁$畻瀵嗙爜鐨凪D5 + password_md5 = hashlib.md5(self.password.encode('utf-8')).hexdigest() + + # 缁勫悎瀛楃涓诧細userName+timestamp+MD5(password) + combined_str = f"{self.username}{timestamp}{password_md5}" + + # 璁$畻鏈�缁堢鍚� + sign = hashlib.md5(combined_str.encode('utf-8')).hexdigest() + + return sign + + def send_sms(self, phone_list: List[str], content: str) -> bool: + """ + 鍙戦�佺煭淇� + + Args: + phone_list: 鎵嬫満鍙风爜鍒楄〃 + content: 鐭俊鍐呭 + + Returns: + 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + if not settings.sms_enabled: + logger.info("鐭俊鍙戦�佸姛鑳藉凡绂佺敤") + return True + + if not phone_list: + logger.warning("鎵嬫満鍙风爜鍒楄〃涓虹┖") + return False + + try: + # 鐢熸垚鏃堕棿鎴筹紙姣锛� + timestamp = int(time.time() * 1000) + + # 鐢熸垚绛惧悕 + sign = self._generate_sign(timestamp) + + # 鏋勫缓璇锋眰鍙傛暟 + payload = { + "userName": self.username, + "content": content, + "phoneList": phone_list, + "timestamp": timestamp, + "sign": sign + } + + logger.info(f"鍙戦�佺煭淇�: phones={phone_list}, content_length={len(content)}") + + # 鍙戦�佽姹� + response = self.session.post(self.api_url, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + + if result.get("code") == 0: + msg_id = result.get("msgId") + sms_count = result.get("smsCount") + logger.info(f"鐭俊鍙戦�佹垚鍔�: msgId={msg_id}, smsCount={sms_count}, phones={phone_list}") + return True + else: + logger.error( + f"鐭俊鍙戦�佸け璐�: code={result.get('code')}, message={result.get('message')}, phones={phone_list}" + ) + return False + + except requests.exceptions.RequestException as e: + logger.error(f"鐭俊鍙戦�佺綉缁滈敊璇�: phones={phone_list}, error={str(e)}") + return False + except Exception as e: + logger.error(f"鐭俊鍙戦�佸紓甯�: phones={phone_list}, error={str(e)}") + return False + + def send_notification(self, content: str) -> bool: + """ + 鍙戦�侀�氱煡鐭俊鍒伴厤缃殑鎵嬫満鍙风爜鍒楄〃 + + Args: + content: 鐭俊鍐呭 + + Returns: + 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + return self.send_sms(settings.sms_phone_numbers, content) + + def test_connection(self) -> bool: + """ + 娴嬭瘯鐭俊鏈嶅姟杩炴帴锛堝彂閫佹祴璇曠煭淇★級 + + Returns: + 杩炴帴鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + if not settings.sms_enabled: + logger.info("鐭俊鍙戦�佸姛鑳藉凡绂佺敤") + return True + + # 鍙戦�佹祴璇曠煭淇″埌绗竴涓彿鐮� + if settings.sms_phone_numbers: + test_phone = [settings.sms_phone_numbers[0]] + test_content = "銆愭祴璇曘�戠煭淇℃湇鍔¤繛鎺ユ祴璇�" + return self.send_sms(test_phone, test_content) + else: + logger.warning("娌℃湁閰嶇疆鐭俊鎺ユ敹鍙风爜") + return False + + +# 鍏ㄥ眬鐭俊鏈嶅姟瀹炰緥 +sms_service = SmsService() diff --git a/app/workers/online_status_worker.py b/app/workers/online_status_worker.py new file mode 100644 index 0000000..c68d325 --- /dev/null +++ b/app/workers/online_status_worker.py @@ -0,0 +1,188 @@ +""" +鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋� +""" + +import time +import threading +from datetime import datetime +from loguru import logger +from config import settings +from app.services.ecloud_client import ecloud_client +from app.services.email_service import email_service +from app.services.sms_service import sms_service + + +class OnlineStatusWorker: + """鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋�""" + + def __init__(self): + self.running = False + self.worker_thread = None + self.last_notification_time = None + self.notification_cooldown = 30 * 60 # 30鍒嗛挓鍐峰嵈鏃堕棿锛岄伩鍏嶉绻佸彂閫侀�氱煡 + + def start(self): + """鍚姩鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋�""" + if not settings.online_status_enabled: + logger.info("鍦ㄧ嚎鐘舵�佹娴嬪姛鑳藉凡绂佺敤") + return + + self.running = True + self.worker_thread = threading.Thread(target=self._monitor_online_status, daemon=True) + self.worker_thread.start() + logger.info("鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋嬪凡鍚姩") + + def stop(self): + """鍋滄鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋�""" + self.running = False + if self.worker_thread and self.worker_thread.is_alive(): + self.worker_thread.join(timeout=10) + logger.info("鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋嬪凡鍋滄") + + def _monitor_online_status(self): + """鐩戞帶鍦ㄧ嚎鐘舵�佺殑涓诲惊鐜�""" + logger.info(f"寮�濮嬬洃鎺у湪绾跨姸鎬侊紝妫�娴嬮棿闅�: {settings.online_status_check_interval}鍒嗛挓") + + while self.running: + try: + # 妫�娴嬪湪绾跨姸鎬� + self._check_online_status() + + # 绛夊緟涓嬫妫�娴� + sleep_seconds = settings.online_status_check_interval * 60 + for _ in range(sleep_seconds): + if not self.running: + break + time.sleep(1) + + except Exception as e: + logger.error(f"鍦ㄧ嚎鐘舵�佺洃鎺у紓甯�: {str(e)}") + time.sleep(60) # 寮傚父鏃剁瓑寰�1鍒嗛挓鍐嶇户缁� + + def _check_online_status(self): + """妫�娴嬪湪绾跨姸鎬佸苟鍙戦�侀�氱煡""" + try: + logger.info("寮�濮嬫娴嬪井淇″湪绾跨姸鎬�") + + # 鏌ヨ鍦ㄧ嚎寰俊鍒楄〃 + online_list = ecloud_client.query_online_wechat_list() + + if online_list is None: + logger.error("鏌ヨ鍦ㄧ嚎寰俊鍒楄〃澶辫触锛岃烦杩囨湰娆℃娴�") + return + + online_count = len(online_list) + logger.info(f"褰撳墠鍦ㄧ嚎寰俊鏁伴噺: {online_count}") + + # 濡傛灉鏈夊湪绾垮井淇★紝鏇存柊w_id骞堕噸缃�氱煡鏃堕棿 + if online_count > 0: + # 鑾峰彇绗竴涓湪绾垮井淇$殑w_id骞舵洿鏂伴厤缃� + if online_list and len(online_list) > 0: + current_w_id = online_list[0].get("wId") + if current_w_id: + self._update_w_id_if_needed(current_w_id) + + if self.last_notification_time: + logger.info("妫�娴嬪埌鍦ㄧ嚎寰俊锛岄噸缃�氱煡鐘舵��") + self.last_notification_time = None + return + + # 娌℃湁鍦ㄧ嚎寰俊锛屾鏌ユ槸鍚﹂渶瑕佸彂閫侀�氱煡 + current_time = time.time() + + # 妫�鏌ュ喎鍗存椂闂� + if (self.last_notification_time and + current_time - self.last_notification_time < self.notification_cooldown): + remaining_time = self.notification_cooldown - (current_time - self.last_notification_time) + logger.info(f"閫氱煡鍐峰嵈涓紝鍓╀綑鏃堕棿: {remaining_time/60:.1f}鍒嗛挓") + return + + # 鍙戦�佹帀绾块�氱煡 + logger.warning("妫�娴嬪埌寰俊鍏ㄩ儴鎺夌嚎锛屽彂閫侀�氱煡") + self._send_offline_notification() + self.last_notification_time = current_time + + except Exception as e: + logger.error(f"妫�娴嬪湪绾跨姸鎬佸紓甯�: {str(e)}") + + def _update_w_id_if_needed(self, current_w_id: str): + """ + 濡傛灉闇�瑕侊紝鏇存柊w_id閰嶇疆 + + Args: + current_w_id: 褰撳墠妫�娴嬪埌鐨剋_id + """ + try: + # 鑾峰彇閰嶇疆涓殑褰撳墠w_id + config_w_id = settings.get_current_w_id() + + # 濡傛灉w_id鍙戠敓鍙樺寲锛屽垯鏇存柊閰嶇疆 + if current_w_id != config_w_id: + logger.info(f"妫�娴嬪埌w_id鍙樺寲: {config_w_id} -> {current_w_id}") + success = settings.update_ecloud_w_id(current_w_id) + if success: + logger.info("w_id鏇存柊鎴愬姛") + else: + logger.error("w_id鏇存柊澶辫触") + + except Exception as e: + logger.error(f"鏇存柊w_id寮傚父: {str(e)}") + + def _send_offline_notification(self): + """鍙戦�佹帀绾块�氱煡""" + try: + # 鍑嗗閫氱煡鍐呭 + subject = "寰俊鎺夌嚎鎻愰啋" + content = settings.online_status_notification_message + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 娣诲姞鏃堕棿鎴冲埌鍐呭 + full_content = f"{content}\n\n妫�娴嬫椂闂�: {timestamp}" + + # 鍙戦�侀偖浠堕�氱煡 + email_success = False + if settings.email_enabled: + try: + email_success = email_service.send_notification(subject, full_content) + if email_success: + logger.info("鎺夌嚎閫氱煡閭欢鍙戦�佹垚鍔�") + else: + logger.error("鎺夌嚎閫氱煡閭欢鍙戦�佸け璐�") + except Exception as e: + logger.error(f"鍙戦�佹帀绾块�氱煡閭欢寮傚父: {str(e)}") + + # 鍙戦�佺煭淇¢�氱煡 + sms_success = False + if settings.sms_enabled: + try: + # 鐭俊鍐呭涓嶅寘鍚椂闂存埑锛岄伩鍏嶈繃闀� + sms_success = sms_service.send_notification(content) + if sms_success: + logger.info("鎺夌嚎閫氱煡鐭俊鍙戦�佹垚鍔�") + else: + logger.error("鎺夌嚎閫氱煡鐭俊鍙戦�佸け璐�") + except Exception as e: + logger.error(f"鍙戦�佹帀绾块�氱煡鐭俊寮傚父: {str(e)}") + + # 璁板綍閫氱煡缁撴灉 + if email_success or sms_success: + logger.info("鎺夌嚎閫氱煡鍙戦�佸畬鎴�") + else: + logger.error("鎺夌嚎閫氱煡鍙戦�佸叏閮ㄥけ璐�") + + except Exception as e: + logger.error(f"鍙戦�佹帀绾块�氱煡寮傚父: {str(e)}") + + def get_status(self) -> dict: + """鑾峰彇宸ヤ綔杩涚▼鐘舵��""" + return { + "running": self.running, + "enabled": settings.online_status_enabled, + "check_interval_minutes": settings.online_status_check_interval, + "last_notification_time": self.last_notification_time, + "notification_cooldown_minutes": self.notification_cooldown / 60 + } + + +# 鍏ㄥ眬鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋嬪疄渚� +online_status_worker = OnlineStatusWorker() diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..f64b2f5 --- /dev/null +++ b/config.example.json @@ -0,0 +1,65 @@ +{ + "database": { + "url": "mysql+pymysql://username:password@host:port/database" + }, + "redis": { + "url": "redis://localhost:6379/0" + }, + "ecloud": { + "base_url": "http://your-ecloud-server:port", + "authorization": "your-authorization-token", + "w_id": "your-wechat-instance-id" + }, + "dify": { + "base_url": "https://api.dify.ai/v1", + "api_key": "your-dify-api-key", + "streaming_enabled": true, + "streaming_timeout": 1200 + }, + "server": { + "host": "0.0.0.0", + "port": 7979, + "debug": false + }, + "logging": { + "level": "INFO", + "file": "logs/app.log" + }, + "message_processing": { + "max_retry_count": 3, + "retry_delay": 5, + "queue_timeout": 300 + }, + "customer_service": { + "names": ["瀹㈡湇1", "瀹㈡湇2"] + }, + "friend_ignore": { + "enabled": true, + "whitelist": [] + }, + "silence_mode": { + "enabled": true, + "duration_minutes": 10 + }, + "online_status_monitor": { + "enabled": true, + "check_interval_minutes": 5, + "notification_message": "寰俊鎺夌嚎鎻愰啋锛氬綋鍓嶆病鏈夊湪绾跨殑寰俊璐﹀彿锛岃鍙婃椂妫�鏌ワ紒" + }, + "email_notification": { + "enabled": true, + "smtp_server": "smtp.qq.com", + "smtp_port": 587, + "smtp_username": "your_email@qq.com", + "smtp_password": "your_qq_app_password", + "from_email": "your_email@qq.com", + "to_emails": ["admin@example.com", "backup@example.com"] + }, + "sms_notification": { + "enabled": true, + "api_url": "https://smsapi.izjun.com:8443/sms/api/sendMessageMass", + "username": "your_sms_username", + "password": "your_sms_password", + "phone_numbers": ["13800138000", "13900139000"] + } +} diff --git a/config.py b/config.py index c9792f3..e0b1455 100644 --- a/config.py +++ b/config.py @@ -4,6 +4,8 @@ import os import json +import threading +from loguru import logger class Settings: @@ -12,6 +14,7 @@ def __init__(self, config_file: str = "config.json"): """鍒濆鍖栭厤缃紝浠嶫SON鏂囦欢鍔犺浇""" self.config_file = config_file + self._lock = threading.Lock() # 鐢ㄤ簬绾跨▼瀹夊叏鐨勯厤缃洿鏂� self._load_config() def _load_config(self): @@ -67,12 +70,118 @@ # 瀹㈡湇閰嶇疆 customer_service_config = config_data["customer_service"] self.customer_service_names = customer_service_config["names"] + self.customer_service_default_name = customer_service_config.get("default_name", "鏅鸿兘瀹㈡湇") # 濂藉弸蹇界暐閰嶇疆 friend_ignore_config = config_data["friend_ignore"] self.friend_ignore_enabled = friend_ignore_config["enabled"] self.friend_ignore_whitelist = friend_ignore_config["whitelist"] + # 闈欓粯妯″紡閰嶇疆 + silence_mode_config = config_data["silence_mode"] + self.silence_mode_enabled = silence_mode_config["enabled"] + self.silence_duration_minutes = silence_mode_config["duration_minutes"] + + # 鍦ㄧ嚎鐘舵�佺洃鎺ч厤缃� + online_status_config = config_data["online_status_monitor"] + self.online_status_enabled = online_status_config["enabled"] + self.online_status_check_interval = online_status_config["check_interval_minutes"] + self.online_status_notification_message = online_status_config["notification_message"] + + # 閭欢閫氱煡閰嶇疆 + email_config = config_data["email_notification"] + self.email_enabled = email_config["enabled"] + self.email_smtp_server = email_config["smtp_server"] + self.email_smtp_port = email_config["smtp_port"] + self.email_smtp_username = email_config["smtp_username"] + self.email_smtp_password = email_config["smtp_password"] + self.email_from_email = email_config["from_email"] + self.email_to_emails = email_config["to_emails"] + + # 鐭俊閫氱煡閰嶇疆 + sms_config = config_data["sms_notification"] + self.sms_enabled = sms_config["enabled"] + self.sms_api_url = sms_config["api_url"] + self.sms_username = sms_config["username"] + self.sms_password = sms_config["password"] + self.sms_phone_numbers = sms_config["phone_numbers"] + + # 鍏抽敭璇嶈繃婊ら厤缃� + keyword_filter_config = config_data.get("keyword_filter", {}) + self.keyword_filter_enabled = keyword_filter_config.get("enabled", False) + self.keyword_filter_keywords = keyword_filter_config.get("keywords", []) + + def update_ecloud_w_id(self, new_w_id: str) -> bool: + """ + 鍔ㄦ�佹洿鏂癊浜戠瀹剁殑w_id閰嶇疆 + + Args: + new_w_id: 鏂扮殑w_id鍊� + + Returns: + 鏇存柊鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + if not new_w_id or new_w_id == self.ecloud_w_id: + return True + + with self._lock: + try: + # 鏇存柊鍐呭瓨涓殑閰嶇疆 + old_w_id = self.ecloud_w_id + self.ecloud_w_id = new_w_id + + # 鏇存柊閰嶇疆鏂囦欢 + if self._update_config_file_w_id(new_w_id): + logger.info(f"鎴愬姛鏇存柊w_id: {old_w_id} -> {new_w_id}") + return True + else: + # 濡傛灉鏂囦欢鏇存柊澶辫触锛屽洖婊氬唴瀛橀厤缃� + self.ecloud_w_id = old_w_id + logger.error(f"鏇存柊閰嶇疆鏂囦欢澶辫触锛屽洖婊歸_id: {new_w_id} -> {old_w_id}") + return False + + except Exception as e: + logger.error(f"鏇存柊w_id寮傚父: {str(e)}") + return False + + def _update_config_file_w_id(self, new_w_id: str) -> bool: + """ + 鏇存柊閰嶇疆鏂囦欢涓殑w_id + + Args: + new_w_id: 鏂扮殑w_id鍊� + + Returns: + 鏇存柊鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + # 璇诲彇褰撳墠閰嶇疆鏂囦欢 + with open(self.config_file, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + # 鏇存柊w_id + config_data["ecloud"]["w_id"] = new_w_id + + # 鍐欏洖閰嶇疆鏂囦欢 + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=2, ensure_ascii=False) + + return True + + except Exception as e: + logger.error(f"鏇存柊閰嶇疆鏂囦欢w_id澶辫触: {str(e)}") + return False + + def get_current_w_id(self) -> str: + """ + 鑾峰彇褰撳墠鐨剋_id + + Returns: + 褰撳墠鐨剋_id鍊� + """ + with self._lock: + return self.ecloud_w_id + # 鍏ㄥ眬閰嶇疆瀹炰緥 settings = Settings() diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..e6373a1 --- /dev/null +++ b/example_usage.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +鍦ㄧ嚎鐘舵�佺洃鎺у姛鑳戒娇鐢ㄧず渚� +""" + +import asyncio +import time +from config import settings +from app.services.ecloud_client import ecloud_client +from app.services.email_service import email_service +from app.services.sms_service import sms_service +from app.workers.online_status_worker import online_status_worker + + +async def test_online_status_monitoring(): + """娴嬭瘯鍦ㄧ嚎鐘舵�佺洃鎺у姛鑳�""" + print("=== 鍦ㄧ嚎鐘舵�佺洃鎺у姛鑳芥祴璇� ===\n") + + # 1. 娴嬭瘯鏌ヨ鍦ㄧ嚎寰俊鍒楄〃 + print("1. 娴嬭瘯鏌ヨ鍦ㄧ嚎寰俊鍒楄〃...") + online_list = ecloud_client.query_online_wechat_list() + if online_list is not None: + print(f" 鏌ヨ鎴愬姛锛屽湪绾垮井淇℃暟閲�: {len(online_list)}") + for i, wechat in enumerate(online_list): + print(f" 寰俊{i+1}: wcId={wechat.get('wcId')}, wId={wechat.get('wId')}") + else: + print(" 鏌ヨ澶辫触") + print() + + # 2. 娴嬭瘯閭欢鏈嶅姟 + print("2. 娴嬭瘯閭欢鏈嶅姟...") + if settings.email_enabled: + # 娴嬭瘯杩炴帴 + email_connected = email_service.test_connection() + print(f" 閭欢鏈嶅姟杩炴帴: {'鎴愬姛' if email_connected else '澶辫触'}") + + if email_connected: + # 鍙戦�佹祴璇曢偖浠� + test_subject = "鍦ㄧ嚎鐘舵�佺洃鎺ф祴璇曢偖浠�" + test_content = f"杩欐槸涓�灏佹祴璇曢偖浠讹紝鍙戦�佹椂闂�: {time.strftime('%Y-%m-%d %H:%M:%S')}" + email_sent = email_service.send_notification(test_subject, test_content) + print(f" 娴嬭瘯閭欢鍙戦��: {'鎴愬姛' if email_sent else '澶辫触'}") + else: + print(" 閭欢鏈嶅姟宸茬鐢�") + print() + + # 3. 娴嬭瘯鐭俊鏈嶅姟 + print("3. 娴嬭瘯鐭俊鏈嶅姟...") + if settings.sms_enabled: + # 鍙戦�佹祴璇曠煭淇� + test_content = f"銆愭祴璇曘�戝湪绾跨姸鎬佺洃鎺ф祴璇曠煭淇★紝鏃堕棿: {time.strftime('%H:%M:%S')}" + sms_sent = sms_service.send_notification(test_content) + print(f" 娴嬭瘯鐭俊鍙戦��: {'鎴愬姛' if sms_sent else '澶辫触'}") + else: + print(" 鐭俊鏈嶅姟宸茬鐢�") + print() + + # 4. 娴嬭瘯w_id鍔ㄦ�佹洿鏂� + print("4. 娴嬭瘯w_id鍔ㄦ�佹洿鏂�...") + current_w_id = settings.get_current_w_id() + print(f" 褰撳墠閰嶇疆鐨剋_id: {current_w_id}") + + if online_list and len(online_list) > 0: + online_w_id = online_list[0].get("wId") + if online_w_id != current_w_id: + print(f" 妫�娴嬪埌w_id鍙樺寲: {current_w_id} -> {online_w_id}") + success = settings.update_ecloud_w_id(online_w_id) + print(f" w_id鏇存柊: {'鎴愬姛' if success else '澶辫触'}") + else: + print(" w_id鏃犲彉鍖栵紝鏃犻渶鏇存柊") + else: + print(" 鏃犲湪绾垮井淇★紝鏃犳硶娴嬭瘯w_id鏇存柊") + print() + + # 5. 鏄剧ず宸ヤ綔杩涚▼鐘舵�� + print("5. 鍦ㄧ嚎鐘舵�佺洃鎺у伐浣滆繘绋嬬姸鎬�...") + status = online_status_worker.get_status() + print(f" 杩愯鐘舵��: {'杩愯涓�' if status['running'] else '宸插仠姝�'}") + print(f" 鍔熻兘鍚敤: {'鏄�' if status['enabled'] else '鍚�'}") + print(f" 妫�娴嬮棿闅�: {status['check_interval_minutes']}鍒嗛挓") + print(f" 閫氱煡鍐峰嵈: {status['notification_cooldown_minutes']}鍒嗛挓") + if status['last_notification_time']: + last_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(status['last_notification_time'])) + print(f" 涓婃閫氱煡: {last_time}") + else: + print(" 涓婃閫氱煡: 鏃�") + print() + + print("=== 娴嬭瘯瀹屾垚 ===") + + +def show_configuration(): + """鏄剧ず褰撳墠閰嶇疆""" + print("=== 褰撳墠閰嶇疆淇℃伅 ===\n") + + print("鍦ㄧ嚎鐘舵�佺洃鎺ч厤缃�:") + print(f" 鍚敤鐘舵��: {'鏄�' if settings.online_status_enabled else '鍚�'}") + print(f" 妫�娴嬮棿闅�: {settings.online_status_check_interval}鍒嗛挓") + print(f" 閫氱煡娑堟伅: {settings.online_status_notification_message}") + print() + + print("閭欢閫氱煡閰嶇疆:") + print(f" 鍚敤鐘舵��: {'鏄�' if settings.email_enabled else '鍚�'}") + if settings.email_enabled: + print(f" SMTP鏈嶅姟鍣�: {settings.email_smtp_server}:{settings.email_smtp_port}") + print(f" 鍙戜欢浜�: {settings.email_from_email}") + print(f" 鏀朵欢浜�: {', '.join(settings.email_to_emails)}") + print() + + print("鐭俊閫氱煡閰嶇疆:") + print(f" 鍚敤鐘舵��: {'鏄�' if settings.sms_enabled else '鍚�'}") + if settings.sms_enabled: + print(f" API鍦板潃: {settings.sms_api_url}") + print(f" 鐢ㄦ埛鍚�: {settings.sms_username}") + print(f" 鎺ユ敹鍙风爜: {', '.join(settings.sms_phone_numbers)}") + print() + + print("E浜戠瀹堕厤缃�:") + print(f" API鍦板潃: {settings.ecloud_base_url}") + print(f" 褰撳墠w_id: {settings.ecloud_w_id}") + print() + + +if __name__ == "__main__": + print("鍦ㄧ嚎鐘舵�佺洃鎺у姛鑳戒娇鐢ㄧず渚媆n") + + # 鏄剧ず閰嶇疆淇℃伅 + show_configuration() + + # 杩愯娴嬭瘯 + asyncio.run(test_online_status_monitoring()) + + print("\n娉ㄦ剰浜嬮」:") + print("1. 璇风‘淇濆凡姝g‘閰嶇疆config.json涓殑鐩稿叧鍙傛暟") + print("2. 閭欢鍜岀煭淇℃祴璇曚細瀹為檯鍙戦�佹秷鎭紝璇疯皑鎱庝娇鐢�") + print("3. 寤鸿鍦ㄧ敓浜х幆澧冧腑绂佺敤娴嬭瘯鍔熻兘") diff --git a/logs/app.log b/logs/app.log index d8da35c..78fc2ba 100644 --- a/logs/app.log +++ b/logs/app.log @@ -2,3 +2,18 @@ 2025-07-28 10:41:14 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 2025-07-28 10:42:46 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 2025-07-28 11:10:52 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 11:23:47 | INFO | __main__:<module>:124 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 11:41:55 | INFO | __main__:<module>:124 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 11:44:17 | INFO | __main__:<module>:124 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 14:08:32 | INFO | __main__:<module>:124 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 14:31:14 | INFO | __main__:<module>:124 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 16:07:05 | INFO | __main__:<module>:139 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 16:23:36 | INFO | __main__:<module>:152 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 16:39:45 | INFO | __main__:<module>:159 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 17:17:10 | INFO | __main__:<module>:161 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 17:18:18 | INFO | __main__:<module>:161 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 17:29:25 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 17:29:50 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 17:34:00 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 17:44:45 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-28 18:12:07 | INFO | __main__:<module>:122 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 diff --git a/test_group_stats.py b/test_group_stats.py new file mode 100644 index 0000000..e4aa7e0 --- /dev/null +++ b/test_group_stats.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +缇ょ粍缁熻鍔熻兘娴嬭瘯鑴氭湰 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from app.services.group_stats_service import group_stats_service +from config import settings + + +def test_group_stats(): + """娴嬭瘯缇ょ粍缁熻鍔熻兘""" + print("=== 缇ょ粍缁熻鍔熻兘娴嬭瘯 ===\n") + + # 娴嬭瘯缇ょ粍ID + test_group_id = "test_group_123" + test_users = ["user1", "user2", "user3"] + + print(f"1. 娴嬭瘯缇ょ粍: {test_group_id}") + print(f"2. 娴嬭瘯鐢ㄦ埛: {test_users}") + print(f"3. 榛樿瀹㈡湇鍚嶇О: {settings.customer_service_default_name}") + print() + + # 娓呯┖娴嬭瘯鏁版嵁 + print("4. 娓呯┖娴嬭瘯鏁版嵁...") + group_stats_service.clear_group_stats(test_group_id) + print(" 娓呯┖瀹屾垚") + print() + + # 娴嬭瘯鍒濆鐘舵�� + print("5. 娴嬭瘯鍒濆鐘舵��...") + initial_stats = group_stats_service.get_group_message_stats(test_group_id) + initial_nickname = group_stats_service.get_most_active_user_nickname(test_group_id) + print(f" 鍒濆缁熻: {initial_stats}") + print(f" 鍒濆鏈�娲昏穬鐢ㄦ埛鏄电О: {initial_nickname}") + print() + + # 妯℃嫙鐢ㄦ埛鍙戣█ + print("6. 妯℃嫙鐢ㄦ埛鍙戣█...") + # user1 鍙戣█ 5 娆� + for i in range(5): + group_stats_service.increment_user_message_count(test_group_id, test_users[0]) + print(f" {test_users[0]} 鍙戣█绗� {i+1} 娆�") + + # user2 鍙戣█ 3 娆� + for i in range(3): + group_stats_service.increment_user_message_count(test_group_id, test_users[1]) + print(f" {test_users[1]} 鍙戣█绗� {i+1} 娆�") + + # user3 鍙戣█ 7 娆� + for i in range(7): + group_stats_service.increment_user_message_count(test_group_id, test_users[2]) + print(f" {test_users[2]} 鍙戣█绗� {i+1} 娆�") + print() + + # 鏌ョ湅缁熻缁撴灉 + print("7. 鏌ョ湅缁熻缁撴灉...") + final_stats = group_stats_service.get_group_message_stats(test_group_id) + final_nickname = group_stats_service.get_most_active_user_nickname(test_group_id) + print(f" 鏈�缁堢粺璁�: {final_stats}") + print(f" 鏈�娲昏穬鐢ㄦ埛鏄电О: {final_nickname}") + print() + + # 鑾峰彇缁熻鎽樿 + print("8. 鑾峰彇缁熻鎽樿...") + summary = group_stats_service.get_group_stats_summary(test_group_id) + print(f" 缁熻鎽樿: {summary}") + print() + + # 娴嬭瘯鐩稿悓鍙戣█娆℃暟鐨勬儏鍐� + print("9. 娴嬭瘯鐩稿悓鍙戣█娆℃暟鐨勬儏鍐�...") + test_group_id_2 = "test_group_equal" + group_stats_service.clear_group_stats(test_group_id_2) + + # 涓や釜鐢ㄦ埛閮藉彂瑷� 3 娆� + for i in range(3): + group_stats_service.increment_user_message_count(test_group_id_2, "equal_user1") + group_stats_service.increment_user_message_count(test_group_id_2, "equal_user2") + + equal_stats = group_stats_service.get_group_message_stats(test_group_id_2) + equal_nickname = group_stats_service.get_most_active_user_nickname(test_group_id_2) + print(f" 鐩稿悓鍙戣█娆℃暟缁熻: {equal_stats}") + print(f" 鐩稿悓鍙戣█娆℃暟鏈�娲昏穬鐢ㄦ埛鏄电О: {equal_nickname}") + print() + + # 娴嬭瘯鏃犲彂瑷�鐨勬儏鍐� + print("10. 娴嬭瘯鏃犲彂瑷�鐨勬儏鍐�...") + test_group_id_3 = "test_group_empty" + group_stats_service.clear_group_stats(test_group_id_3) + + empty_stats = group_stats_service.get_group_message_stats(test_group_id_3) + empty_nickname = group_stats_service.get_most_active_user_nickname(test_group_id_3) + print(f" 鏃犲彂瑷�缁熻: {empty_stats}") + print(f" 鏃犲彂瑷�鏈�娲昏穬鐢ㄦ埛鏄电О: {empty_nickname}") + print() + + print("=== 娴嬭瘯瀹屾垚 ===") + + +def test_config(): + """娴嬭瘯閰嶇疆鍔熻兘""" + print("=== 閰嶇疆娴嬭瘯 ===\n") + + print(f"瀹㈡湇鍚嶇О鍒楄〃: {settings.customer_service_names}") + print(f"榛樿瀹㈡湇鍚嶇О: {settings.customer_service_default_name}") + print() + + +if __name__ == "__main__": + try: + test_config() + test_group_stats() + except Exception as e: + print(f"娴嬭瘯澶辫触: {str(e)}") + import traceback + traceback.print_exc() diff --git a/tests/test_message_processor.py b/tests/test_message_processor.py index 5e1eee2..e149ce8 100644 --- a/tests/test_message_processor.py +++ b/tests/test_message_processor.py @@ -13,8 +13,13 @@ """娴嬭瘯鍓嶅噯澶�""" self.processor = MessageProcessor() - def test_is_valid_group_message_success(self): + @patch('app.services.message_processor.silence_service') + @patch('app.services.message_processor.friend_ignore_service') + def test_is_valid_group_message_success(self, mock_friend_ignore_service, mock_silence_service): """娴嬭瘯鏈夋晥缇よ亰娑堟伅楠岃瘉""" + mock_silence_service.is_silence_active.return_value = False + mock_friend_ignore_service.is_friend_ignored.return_value = False + callback_data = { "messageType": "80001", "data": { @@ -24,7 +29,7 @@ "self": False } } - + result = self.processor.is_valid_group_message(callback_data) assert result is True @@ -72,9 +77,12 @@ result = self.processor.is_valid_group_message(callback_data) assert result is False + @patch('app.services.message_processor.silence_service') @patch('app.services.message_processor.friend_ignore_service') - def test_is_valid_group_message_friend_ignored(self, mock_friend_ignore_service): + def test_is_valid_group_message_friend_ignored(self, mock_friend_ignore_service, mock_silence_service): """娴嬭瘯濂藉弸鍦ㄥ拷鐣ュ垪琛ㄤ腑鐨勬秷鎭�""" + mock_silence_service.is_silence_active.side_effect = [False, False] # 涓ゆ妫�鏌ラ兘杩斿洖False + mock_silence_service.activate_silence_mode.return_value = True mock_friend_ignore_service.is_friend_ignored.return_value = True callback_data = { @@ -91,9 +99,11 @@ assert result is False mock_friend_ignore_service.is_friend_ignored.assert_called_once_with("wxid_test123") + @patch('app.services.message_processor.silence_service') @patch('app.services.message_processor.friend_ignore_service') - def test_is_valid_group_message_friend_not_ignored(self, mock_friend_ignore_service): + def test_is_valid_group_message_friend_not_ignored(self, mock_friend_ignore_service, mock_silence_service): """娴嬭瘯濂藉弸涓嶅湪蹇界暐鍒楄〃涓殑娑堟伅""" + mock_silence_service.is_silence_active.return_value = False mock_friend_ignore_service.is_friend_ignored.return_value = False callback_data = { @@ -109,12 +119,14 @@ result = self.processor.is_valid_group_message(callback_data) assert result is True mock_friend_ignore_service.is_friend_ignored.assert_called_once_with("wxid_test123") - - @patch('app.services.message_processor.redis_queue') - def test_enqueue_callback_message_success(self, mock_redis_queue): - """娴嬭瘯娑堟伅鍏ラ槦鎴愬姛""" - mock_redis_queue.enqueue_message.return_value = True - + + @patch('app.services.message_processor.silence_service') + @patch('app.services.message_processor.friend_ignore_service') + def test_is_valid_group_message_silence_active(self, mock_friend_ignore_service, mock_silence_service): + """娴嬭瘯缇ょ粍闈欓粯妯″紡婵�娲绘椂鐨勬秷鎭鐞嗭紙闈炲拷鐣ュソ鍙嬶級""" + mock_friend_ignore_service.is_friend_ignored.return_value = False + mock_silence_service.is_silence_active.return_value = True + callback_data = { "messageType": "80001", "data": { @@ -124,9 +136,103 @@ "self": False } } - + + result = self.processor.is_valid_group_message(callback_data) + assert result is False + mock_silence_service.is_silence_active.assert_called_once_with("group123@chatroom") + + @patch('app.services.message_processor.silence_service') + @patch('app.services.message_processor.friend_ignore_service') + def test_is_valid_group_message_friend_ignored_activate_silence(self, mock_friend_ignore_service, mock_silence_service): + """娴嬭瘯濂藉弸琚拷鐣ユ椂婵�娲婚潤榛樻ā寮�""" + mock_silence_service.is_silence_active.side_effect = [False, False] # 绗竴娆℃鏌ユ湭婵�娲伙紝绗簩娆℃鏌ュ拷鐣ュソ鍙嬫椂涔熸湭婵�娲� + mock_silence_service.activate_silence_mode.return_value = True + mock_friend_ignore_service.is_friend_ignored.return_value = True + + callback_data = { + "messageType": "80001", + "data": { + "fromUser": "wxid_test123", + "fromGroup": "group123@chatroom", + "content": "娴嬭瘯娑堟伅", + "self": False + } + } + + result = self.processor.is_valid_group_message(callback_data) + assert result is False + mock_silence_service.activate_silence_mode.assert_called_once_with("group123@chatroom") + + @patch('app.services.message_processor.silence_service') + @patch('app.services.message_processor.friend_ignore_service') + def test_is_valid_group_message_friend_ignored_extend_silence(self, mock_friend_ignore_service, mock_silence_service): + """娴嬭瘯濂藉弸琚拷鐣ユ椂寤堕暱缇ょ粍闈欓粯妯″紡""" + mock_silence_service.is_silence_active.return_value = True # 缇ょ粍闈欓粯妯″紡宸叉縺娲� + mock_silence_service.extend_silence_mode.return_value = True + mock_friend_ignore_service.is_friend_ignored.return_value = True + + callback_data = { + "messageType": "80001", + "data": { + "fromUser": "wxid_test123", + "fromGroup": "group123@chatroom", + "content": "娴嬭瘯娑堟伅", + "self": False + } + } + + result = self.processor.is_valid_group_message(callback_data) + assert result is False + mock_silence_service.extend_silence_mode.assert_called_once_with("group123@chatroom") + + @patch('app.services.message_processor.silence_service') + @patch('app.services.message_processor.friend_ignore_service') + def test_is_valid_group_message_friend_ignored_in_silence_mode(self, mock_friend_ignore_service, mock_silence_service): + """娴嬭瘯缇ょ粍闈欓粯妯″紡涓嬪ソ鍙嬫秷鎭粛鑳藉埛鏂版椂闀�""" + # 妯℃嫙缇ょ粍闈欓粯妯″紡宸叉縺娲荤殑鎯呭喌 + mock_friend_ignore_service.is_friend_ignored.return_value = True + mock_silence_service.is_silence_active.return_value = True + mock_silence_service.extend_silence_mode.return_value = True + + callback_data = { + "messageType": "80001", + "data": { + "fromUser": "wxid_ignored_friend", + "fromGroup": "group123@chatroom", + "content": "琚拷鐣ュソ鍙嬪湪缇ょ粍闈欓粯妯″紡涓嬬殑娑堟伅", + "self": False + } + } + + result = self.processor.is_valid_group_message(callback_data) + assert result is False + + # 楠岃瘉濂藉弸蹇界暐妫�鏌ヨ璋冪敤 + mock_friend_ignore_service.is_friend_ignored.assert_called_once_with("wxid_ignored_friend") + # 楠岃瘉缇ょ粍闈欓粯妯″紡鏃堕棿琚欢闀� + mock_silence_service.extend_silence_mode.assert_called_once_with("group123@chatroom") + + @patch('app.services.message_processor.silence_service') + @patch('app.services.message_processor.friend_ignore_service') + @patch('app.services.message_processor.redis_queue') + def test_enqueue_callback_message_success(self, mock_redis_queue, mock_friend_ignore_service, mock_silence_service): + """娴嬭瘯娑堟伅鍏ラ槦鎴愬姛""" + mock_silence_service.is_silence_active.return_value = False + mock_friend_ignore_service.is_friend_ignored.return_value = False + mock_redis_queue.enqueue_message.return_value = True + + callback_data = { + "messageType": "80001", + "data": { + "fromUser": "wxid_test123", + "fromGroup": "group123@chatroom", + "content": "娴嬭瘯娑堟伅", + "self": False + } + } + result = self.processor.enqueue_callback_message(callback_data) - + assert result is True mock_redis_queue.enqueue_message.assert_called_once_with("wxid_test123", callback_data) diff --git a/tests/test_online_status_monitor.py b/tests/test_online_status_monitor.py new file mode 100644 index 0000000..c9368aa --- /dev/null +++ b/tests/test_online_status_monitor.py @@ -0,0 +1,168 @@ +""" +鍦ㄧ嚎鐘舵�佺洃鎺у姛鑳芥祴璇� +""" + +import pytest +import time +from unittest.mock import Mock, patch +from app.services.ecloud_client import ecloud_client +from app.services.email_service import email_service +from app.services.sms_service import sms_service +from app.workers.online_status_worker import online_status_worker + + +class TestOnlineStatusMonitor: + """鍦ㄧ嚎鐘舵�佺洃鎺ф祴璇�""" + + def test_ecloud_query_online_wechat_list(self): + """娴嬭瘯鏌ヨ鍦ㄧ嚎寰俊鍒楄〃""" + # 妯℃嫙鏈夊湪绾垮井淇$殑鎯呭喌 + with patch.object(ecloud_client.session, 'post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = { + "code": "1000", + "message": "鎴愬姛", + "data": [ + { + "wcId": "wxid_test123", + "wId": "test-w-id-123" + } + ] + } + mock_post.return_value = mock_response + + result = ecloud_client.query_online_wechat_list() + assert result is not None + assert len(result) == 1 + assert result[0]["wcId"] == "wxid_test123" + + # 妯℃嫙娌℃湁鍦ㄧ嚎寰俊鐨勬儏鍐� + with patch.object(ecloud_client.session, 'post') as mock_post: + mock_response = Mock() + mock_response.json.return_value = { + "code": "1000", + "message": "鎴愬姛", + "data": [] + } + mock_post.return_value = mock_response + + result = ecloud_client.query_online_wechat_list() + assert result is not None + assert len(result) == 0 + + def test_sms_service_generate_sign(self): + """娴嬭瘯鐭俊鏈嶅姟绛惧悕鐢熸垚""" + # 浣跨敤鏂囨。涓殑绀轰緥鏁版嵁 + sms_service.username = "test" + sms_service.password = "123" + timestamp = 1596254400000 + + sign = sms_service._generate_sign(timestamp) + expected_sign = "e315cf297826abdeb2092cc57f29f0bf" + + assert sign == expected_sign + + def test_email_service_disabled(self): + """娴嬭瘯閭欢鏈嶅姟绂佺敤鐘舵��""" + with patch('config.settings.email_enabled', False): + result = email_service.send_notification("娴嬭瘯涓婚", "娴嬭瘯鍐呭") + assert result is True # 绂佺敤鏃跺簲璇ヨ繑鍥濼rue + + def test_sms_service_disabled(self): + """娴嬭瘯鐭俊鏈嶅姟绂佺敤鐘舵��""" + with patch('config.settings.sms_enabled', False): + result = sms_service.send_notification("娴嬭瘯鍐呭") + assert result is True # 绂佺敤鏃跺簲璇ヨ繑鍥濼rue + + def test_online_status_worker_disabled(self): + """娴嬭瘯鍦ㄧ嚎鐘舵�佹娴嬪伐浣滆繘绋嬬鐢ㄧ姸鎬�""" + with patch('config.settings.online_status_enabled', False): + # 鍒涘缓鏂扮殑宸ヤ綔杩涚▼瀹炰緥杩涜娴嬭瘯 + from app.workers.online_status_worker import OnlineStatusWorker + test_worker = OnlineStatusWorker() + + test_worker.start() + assert test_worker.running is False + assert test_worker.worker_thread is None + + def test_online_status_worker_status(self): + """娴嬭瘯鑾峰彇宸ヤ綔杩涚▼鐘舵��""" + status = online_status_worker.get_status() + + assert "running" in status + assert "enabled" in status + assert "check_interval_minutes" in status + assert "last_notification_time" in status + assert "notification_cooldown_minutes" in status + + assert isinstance(status["running"], bool) + assert isinstance(status["enabled"], bool) + assert isinstance(status["check_interval_minutes"], (int, float)) + assert isinstance(status["notification_cooldown_minutes"], (int, float)) + + def test_dynamic_w_id_update(self): + """娴嬭瘯鍔ㄦ�亀_id鏇存柊鍔熻兘""" + from config import settings + + # 淇濆瓨鍘熷w_id + original_w_id = settings.get_current_w_id() + + try: + # 娴嬭瘯鏇存柊w_id + new_w_id = "test-new-w-id-123" + success = settings.update_ecloud_w_id(new_w_id) + + # 楠岃瘉鏇存柊鎴愬姛 + assert success is True + assert settings.get_current_w_id() == new_w_id + + # 娴嬭瘯鐩稿悓w_id涓嶄細閲嶅鏇存柊 + success = settings.update_ecloud_w_id(new_w_id) + assert success is True + + finally: + # 鎭㈠鍘熷w_id + settings.update_ecloud_w_id(original_w_id) + + def test_w_id_update_in_online_status_check(self): + """娴嬭瘯鍦ㄧ嚎鐘舵�佹娴嬩腑鐨剋_id鏇存柊""" + from app.workers.online_status_worker import OnlineStatusWorker + + # 鍒涘缓娴嬭瘯宸ヤ綔杩涚▼瀹炰緥 + test_worker = OnlineStatusWorker() + + # 妯℃嫙w_id鏇存柊 + test_w_id = "test-w-id-456" + test_worker._update_w_id_if_needed(test_w_id) + + # 杩欓噷涓昏娴嬭瘯鏂规硶涓嶄細鎶涘嚭寮傚父 + # 瀹為檯鐨剋_id鏇存柊閫昏緫鍦╯ettings涓凡缁忔祴璇曡繃浜� + + def test_startup_contact_sync_logic(self): + """娴嬭瘯鍚姩鏃惰仈绯讳汉鍚屾閫昏緫""" + # 娴嬭瘯娌℃湁鍦ㄧ嚎寰俊鏃剁殑澶勭悊閫昏緫 + with patch.object(ecloud_client, 'query_online_wechat_list') as mock_query: + # 妯℃嫙鏌ヨ澶辫触 + mock_query.return_value = None + # 杩欑鎯呭喌涓嬪簲璇ヨ烦杩囪仈绯讳汉鍚屾锛屼笉浼氭姏鍑哄紓甯� + + # 妯℃嫙娌℃湁鍦ㄧ嚎寰俊 + mock_query.return_value = [] + # 杩欑鎯呭喌涓嬩篃搴旇璺宠繃鑱旂郴浜哄悓姝� + + # 妯℃嫙鏈夊湪绾垮井淇� + mock_query.return_value = [{"wcId": "test_wc_id", "wId": "test_w_id"}] + # 杩欑鎯呭喌涓嬩細灏濊瘯鍚屾鑱旂郴浜� + + def test_email_service_connection_handling(self): + """娴嬭瘯閭欢鏈嶅姟杩炴帴澶勭悊""" + # 娴嬭瘯閭欢鏈嶅姟鐨勮繛鎺ュ拰鍏抽棴閫昏緫 + # 涓昏纭繚涓嶄細鍥犱负杩炴帴鍏抽棴寮傚父鑰屽奖鍝嶅彂閫佺粨鏋� + + # 杩欓噷涓昏娴嬭瘯鏂规硶缁撴瀯锛屽疄闄呯殑SMTP娴嬭瘯闇�瑕佺湡瀹炵殑閭欢鏈嶅姟鍣� + assert hasattr(email_service, 'send_email') + assert hasattr(email_service, 'test_connection') + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/test_silence_service.py b/tests/test_silence_service.py new file mode 100644 index 0000000..181f9f2 --- /dev/null +++ b/tests/test_silence_service.py @@ -0,0 +1,177 @@ +""" +闈欓粯妯″紡鏈嶅姟娴嬭瘯 +""" + +import time +import pytest +from unittest.mock import Mock, patch + +from app.services.silence_service import SilenceService + + +class TestSilenceService: + """闈欓粯妯″紡鏈嶅姟娴嬭瘯绫�""" + + def setup_method(self): + """娴嬭瘯鍓嶅噯澶�""" + self.service = SilenceService() + + @patch('app.services.silence_service.settings') + @patch('app.services.silence_service.redis_queue') + def test_activate_silence_mode_success(self, mock_redis_queue, mock_settings): + """娴嬭瘯鎴愬姛婵�娲荤兢缁勯潤榛樻ā寮�""" + # 妯℃嫙閰嶇疆 + mock_settings.silence_mode_enabled = True + mock_settings.silence_duration_minutes = 10 + + # 妯℃嫙Redis鎿嶄綔 + mock_redis_client = Mock() + mock_redis_queue.redis_client = mock_redis_client + mock_redis_client.setex.return_value = True + + # 娴嬭瘯婵�娲荤兢缁勯潤榛樻ā寮� + group_id = "test_group@chatroom" + result = self.service.activate_silence_mode(group_id) + + assert result is True + # 楠岃瘉Redis璋冪敤 + assert mock_redis_client.setex.call_count == 2 # 璁剧疆涓や釜閿� + + @patch('app.services.silence_service.settings') + def test_activate_silence_mode_disabled(self, mock_settings): + """娴嬭瘯闈欓粯妯″紡鍔熻兘绂佺敤鏃剁殑琛屼负""" + mock_settings.silence_mode_enabled = False + + group_id = "test_group@chatroom" + result = self.service.activate_silence_mode(group_id) + assert result is False + + @patch('app.services.silence_service.settings') + @patch('app.services.silence_service.redis_queue') + def test_is_silence_active(self, mock_redis_queue, mock_settings): + """娴嬭瘯妫�鏌ラ潤榛樻ā寮忔槸鍚︽縺娲�""" + mock_settings.silence_mode_enabled = True + + # 妯℃嫙Redis鎿嶄綔 + mock_redis_client = Mock() + mock_redis_queue.redis_client = mock_redis_client + + # 娴嬭瘯闈欓粯妯″紡婵�娲荤姸鎬� + mock_redis_client.exists.return_value = True + group_id = "test_group@chatroom" + result = self.service.is_silence_active(group_id) + assert result is True + + # 娴嬭瘯闈欓粯妯″紡鏈縺娲荤姸鎬� + mock_redis_client.exists.return_value = False + result = self.service.is_silence_active(group_id) + assert result is False + + @patch('app.services.silence_service.settings') + def test_is_silence_active_disabled(self, mock_settings): + """娴嬭瘯鍔熻兘绂佺敤鏃剁殑闈欓粯妯″紡妫�鏌�""" + mock_settings.silence_mode_enabled = False + + group_id = "test_group@chatroom" + result = self.service.is_silence_active(group_id) + assert result is False + + @patch('app.services.silence_service.settings') + @patch('app.services.silence_service.redis_queue') + def test_get_silence_remaining_time(self, mock_redis_queue, mock_settings): + """娴嬭瘯鑾峰彇闈欓粯妯″紡鍓╀綑鏃堕棿""" + mock_settings.silence_mode_enabled = True + + # 妯℃嫙Redis鎿嶄綔 + mock_redis_client = Mock() + mock_redis_queue.redis_client = mock_redis_client + + # 妯℃嫙闈欓粯妯″紡婵�娲� + mock_redis_client.exists.return_value = True + + # 妯℃嫙缁撴潫鏃堕棿锛堝綋鍓嶆椂闂� + 300绉掞級 + future_time = time.time() + 300 + mock_redis_client.get.return_value = str(future_time) + + group_id = "test_group@chatroom" + result = self.service.get_silence_remaining_time(group_id) + assert result is not None + assert result > 0 + assert result <= 300 + + @patch('app.services.silence_service.settings') + @patch('app.services.silence_service.redis_queue') + def test_get_silence_remaining_time_inactive(self, mock_redis_queue, mock_settings): + """娴嬭瘯闈欓粯妯″紡鏈縺娲绘椂鑾峰彇鍓╀綑鏃堕棿""" + mock_settings.silence_mode_enabled = True + + # 妯℃嫙Redis鎿嶄綔 + mock_redis_client = Mock() + mock_redis_queue.redis_client = mock_redis_client + mock_redis_client.exists.return_value = False + + group_id = "test_group@chatroom" + result = self.service.get_silence_remaining_time(group_id) + assert result is None + + @patch('app.services.silence_service.settings') + @patch('app.services.silence_service.redis_queue') + def test_deactivate_silence_mode(self, mock_redis_queue, mock_settings): + """娴嬭瘯鎵嬪姩鍋滅敤闈欓粯妯″紡""" + # 妯℃嫙Redis鎿嶄綔 + mock_redis_client = Mock() + mock_redis_queue.redis_client = mock_redis_client + mock_redis_client.delete.return_value = True + + result = self.service.deactivate_silence_mode() + assert result is True + + # 楠岃瘉鍒犻櫎浜嗕袱涓敭 + assert mock_redis_client.delete.call_count == 2 + + @patch('app.services.silence_service.settings') + @patch('app.services.silence_service.redis_queue') + def test_extend_silence_mode(self, mock_redis_queue, mock_settings): + """娴嬭瘯寤堕暱闈欓粯妯″紡""" + mock_settings.silence_mode_enabled = True + mock_settings.silence_duration_minutes = 10 + + # 妯℃嫙Redis鎿嶄綔 + mock_redis_client = Mock() + mock_redis_queue.redis_client = mock_redis_client + mock_redis_client.setex.return_value = True + + group_id = "test_group@chatroom" + result = self.service.extend_silence_mode(group_id) + assert result is True + + @patch('app.services.silence_service.settings') + def test_get_silence_status(self, mock_settings): + """娴嬭瘯鑾峰彇闈欓粯妯″紡鐘舵��""" + mock_settings.silence_mode_enabled = True + mock_settings.silence_duration_minutes = 10 + + with patch.object(self.service, 'is_silence_active', return_value=False): + status = self.service.get_silence_status() + + assert status["enabled"] is True + assert status["active"] is False + assert status["duration_minutes"] == 10 + assert status["remaining_seconds"] is None + assert status["remaining_minutes"] is None + + @patch('app.services.silence_service.settings') + def test_get_silence_status_active(self, mock_settings): + """娴嬭瘯鑾峰彇婵�娲荤姸鎬佺殑闈欓粯妯″紡鐘舵��""" + mock_settings.silence_mode_enabled = True + mock_settings.silence_duration_minutes = 10 + + with patch.object(self.service, 'is_silence_active', return_value=True), \ + patch.object(self.service, 'get_silence_remaining_time', return_value=300): + + status = self.service.get_silence_status() + + assert status["enabled"] is True + assert status["active"] is True + assert status["remaining_seconds"] == 300 + assert status["remaining_minutes"] == 5.0 diff --git "a/\347\237\255\344\277\241\345\217\221\351\200\201\346\216\245\345\217\243\346\226\207\346\241\243.txt" "b/\347\237\255\344\277\241\345\217\221\351\200\201\346\216\245\345\217\243\346\226\207\346\241\243.txt" new file mode 100644 index 0000000..5b7b576 --- /dev/null +++ "b/\347\237\255\344\277\241\345\217\221\351\200\201\346\216\245\345\217\243\346\226\207\346\241\243.txt" @@ -0,0 +1,61 @@ +鎺ュ彛鍦板潃 +https://smsapi.izjun.com:8443/ + +# 1.鍓嶈█ +鏈崗璁熀浜嶩TTP鏈嶅姟锛屼娇鐢≒OST璇锋眰鏂瑰紡锛岃姹傚拰搴旂瓟鍧囦负JSON鏍煎紡鏁版嵁.銆� +瀛楁鍛藉悕鏂瑰紡锛氶┘宄版硶銆� +缁熶竴璇锋眰鍜屽搷搴旂紪鐮侊細UTF-8 +缁熶竴璇锋眰Header鍐呭锛欳ontent-Type:application/json +璇蜂娇鐢ㄦ帴鍙g綉鍏冲湴鍧�鏇挎崲鏂囨。涓殑鏈嶅姟鍣ㄥ湴鍧�锛歨ttp://{address:port}/sms + sign鍙傛暟璁$畻瑙勫垯锛氬涓寚瀹氬弬鏁板�肩粍鍚堟垚瀛楃涓插悗璁$畻MD532浣嶅皬鍐欑粨鏋� +瑕佹眰锛歁D5(userName+timestamp+MD5(password)) +鍋囪锛歶serName(甯愬彿鍚�)=test + password(甯愬彿瀵嗙爜)=123 + timestamp=1596254400000 +璁$畻锛歁D5(password)=202cb962ac59075b964b07152d234b70 +缁勫悎瀛楃涓诧細test1596254400000202cb962ac59075b964b07152d234b70 +sign缁撴灉锛歁D5(缁勫悎瀛楃涓�)=e315cf297826abdeb2092cc57f29f0bf + +# 2.鐭俊鎵归噺鍙戦�佹帴鍙� +## 2.1璋冪敤鍦板潃 +鍦板潃锛歨ttp://{address:port}/sms/api/sendMessageMass +璇锋眰鏂规硶锛歅OST + +## 2.2璇锋眰鍖呭ご瀹氫箟 +Accept:application/json +Content-Type:application/json;charset=utf-8 + +## 2.3璇锋眰鍙傛暟 +鍙傛暟鍚� 绫诲瀷 蹇呭~ 璇存槑 +userName String 鏄� 甯愬彿鐢ㄦ埛鍚� +content String 鏄� 鐭俊鍐呭 +phoneList [Array] 鏄� 鍙戦�佹墜鏈哄彿鐮侊紝JSON鏁扮粍鏍煎紡銆� +timestamp Long 鏄� 褰撳墠鏃堕棿鎴筹紝绮剧‘鍒版绉掋�備緥濡�2020骞�8鏈�1鏃�12:00:00鏃堕棿鎴充负锛�1596254400000 +sign String 鏄� 鐢变互涓嬪弬鏁板�肩粍鍚堟垚瀛楃涓插苟璁$畻MD5鍊硷紝鍙傝�冭缁嗚鍒� 璁$畻锛歁D5(userName+timestamp+MD5(password)) + +## 2.4鍝嶅簲缁撴灉 +鍙傛暟鍚� 绫诲瀷 璇存槑 +code Integer 澶勭悊缁撴灉锛�0涓烘垚鍔燂紝鍏朵粬澶辫触锛岃缁嗗弬鑰冨搷搴旂姸鎬佺爜 +message String 澶勭悊缁撴灉鎻忚堪 +msgId Long 褰揷ode=0鏃讹紝绯荤粺杩斿洖鍞竴娑堟伅Id +smsCount Integer 褰揷ode=0鏃讹紝绯荤粺杩斿洖娑堣�楄璐规�绘暟 + +## 2.5璇锋眰绀轰緥 +鍙戦�佽姹傦細 +POSThttp://{address:port}/sms/api/sendMessageMass + Accept:application/json + Content-Type:application/json;charset=utf-8 + { + "userName":"test", + "content":"銆愮鍚嶃�戞偍鐨勯獙璇佺爜鏄�123456", + "phoneList": ["13500000001","13500000002","13500000003"], + "timestamp":1596254400000, + "sign":"e315cf297826abdeb2092cc57f29f0bf" + } +鍝嶅簲缁撴灉锛� +{ + "code":0, + "message":"澶勭悊鎴愬姛", + "msgId":123456, + "smsCount":3 + } \ No newline at end of file -- Gitblit v1.9.1