From 0f00caf23a977a8033f56a0e31805fd8792a4589 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 30 Mar 2025 11:33:16 +0800 Subject: [PATCH] update --- app/api/endpoints/wecom.py | 267 +++++++++++++++++++++++++-- app/core/wecomclient.py | 297 ++++++++++++++++++++++++++++-- app/models/wecom_external_chat.py | 98 ++++++++++ jobs.sqlite | Bin 24576 -> 24576 bytes 4 files changed, 625 insertions(+), 37 deletions(-) create mode 100644 app/models/wecom_external_chat.py diff --git a/app/api/endpoints/wecom.py b/app/api/endpoints/wecom.py index 1d86d5a..516eb22 100644 --- a/app/api/endpoints/wecom.py +++ b/app/api/endpoints/wecom.py @@ -15,8 +15,9 @@ from app.models.user import UserDB from app.api.deps import get_current_user from fastapi import Depends from sqlalchemy.orm import Session -from app.core.response import error_response, success_response +from app.core.response import error_response, success_response, ResponseModel from pydantic import BaseModel +from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatInfo, WecomExternalChatMemberDB, WecomExternalChatMemberInfo router = APIRouter() @@ -109,43 +110,52 @@ async def wechat_corp_callback( # 解析解密后的XML msg_root = ET.fromstring(decrypted_msg) - print(f"企业微信回调消息:{decrypted_msg}") + logging.info(f"企业微信回调消息:{decrypted_msg}") # 解析基本信息 msg_type = msg_root.find('MsgType').text - print(f"msg_type: {msg_type}") + logging.info(f"msg_type: {msg_type}") # 处理事件消息 if msg_type == 'event': event = msg_root.find('Event').text - print(f"event: {event}") + logging.info(f"event: {event}") - # 处理进群事件 + # 处理外部群聊变更事件 if event == 'change_external_chat': chat_id = msg_root.find('ChatId').text change_type = msg_root.find('ChangeType').text update_detail = msg_root.find('UpdateDetail').text - - print(f"chat_id: {chat_id}") - print(f"change_type: {change_type}") - print(f"update_detail: {update_detail}") - # 处理进群事件 - - if update_detail == 'add_member': - print(f"发送欢迎消息") - # 发送欢迎消息 - # await wecom_client.send_welcome_message(chat_id) + join_user_id = None + if update_detail == 'add_member' and msg_root.find('JoinScene') is not None: + logging.info(f"有新成员加入群聊") + # 获取加入的成员ID + join_user_id_elem = msg_root.find('JoinUserID') + if join_user_id_elem is not None: + join_user_id = join_user_id_elem.text + logging.info(f"chat_id: {chat_id}, change_type: {change_type}, update_detail: {update_detail}, join_user_id: {join_user_id}") + + # 处理群聊变更事件 + await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type=change_type, + update_detail=update_detail, + join_user_id=join_user_id + ) + + if update_detail == 'add_member' and join_user_id: + logging.info(f"发送欢迎消息到群聊:{chat_id}") + await wecom_client.send_welcome_message(chat_id) return Response(content="success", media_type="text/plain") except Exception as e: logging.exception("处理企业微信回调消息异常") - return Response(content="success", media_type="text/plain") - + return Response(content="success", media_type="text/plain") class UnionidToExternalUseridRequest(BaseModel): unionid: str @@ -158,4 +168,225 @@ async def unionid_to_external_userid( """根据unionid获取external_userid""" result = await wecom_client.unionid_to_external_userid(request.unionid, request.openid) print(f"根据unionid获取external_userid结果: {result}") - return success_response(message="获取external_userid成功", data=result) \ No newline at end of file + return success_response(message="获取external_userid成功", data=result) + +@router.get("/external-chats", response_model=ResponseModel) +async def get_external_chats( + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取企业微信外部群聊列表""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 获取群聊列表 + chats = db.query(WecomExternalChatDB).filter( + WecomExternalChatDB.is_active == True + ).order_by(WecomExternalChatDB.update_time.desc()).all() + + # 转换为Pydantic模型 + chat_list = [WecomExternalChatInfo.model_validate(chat) for chat in chats] + + return success_response(message="获取群聊列表成功", data=chat_list) + except Exception as e: + logging.exception("获取群聊列表异常") + return error_response(code=500, message=f"获取群聊列表失败: {str(e)}") + +@router.get("/external-chats/{chat_id}/members", response_model=ResponseModel) +async def get_external_chat_members( + chat_id: str, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """获取企业微信外部群聊成员列表""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 检查群聊是否存在 + chat = db.query(WecomExternalChatDB).filter( + WecomExternalChatDB.chat_id == chat_id + ).first() + + if not chat: + return error_response(code=404, message="群聊不存在") + + # 获取成员列表 + members = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id + ).order_by(WecomExternalChatMemberDB.join_time.desc()).all() + + # 转换为Pydantic模型 + member_list = [WecomExternalChatMemberInfo.model_validate(member) for member in members] + + return success_response(message="获取群聊成员列表成功", data=member_list) + except Exception as e: + logging.exception("获取群聊成员列表异常") + return error_response(code=500, message=f"获取群聊成员列表失败: {str(e)}") + +@router.post("/sync-chat/{chat_id}", response_model=ResponseModel) +async def sync_external_chat( + chat_id: str, + db: Session = Depends(get_db), + current_user: UserDB = Depends(get_current_user) +): + """同步企业微信外部群聊信息""" + try: + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return error_response(code=403, message="权限不足") + + # 获取群聊信息 + result = await wecom_client.handle_chat_change_event( + chat_id=chat_id, + change_type="create", + update_detail="" + ) + + if not result: + return error_response(code=500, message="同步群聊信息失败") + + return success_response(message="同步群聊信息成功") + except Exception as e: + logging.exception("同步群聊信息异常") + return error_response(code=500, message=f"同步群聊信息失败: {str(e)}") + +@router.get("/chat-dashboard") +async def chat_dashboard( + current_user: UserDB = Depends(get_current_user), + db: Session = Depends(get_db) +): + """显示企业微信外部群聊信息的HTML页面""" + # 检查是否为管理员 + if current_user.userid != settings.PLATFORM_USER_ID: + return Response(content="权限不足", media_type="text/html") + + # 获取群聊列表 + chats = db.query(WecomExternalChatDB).filter( + WecomExternalChatDB.is_active == True + ).order_by(WecomExternalChatDB.update_time.desc()).all() + + # 生成HTML + html = """ + + + + 企业微信外部群聊信息 + + + + + + +
+

企业微信外部群聊信息

+ """ + + if not chats: + html += "

暂无群聊信息

" + else: + for chat in chats: + html += f""" +
+

{chat.name or '未命名群聊'} ({chat.chat_id})

+

+ 创建时间: {chat.create_time.strftime('%Y-%m-%d %H:%M:%S')}
+ 更新时间: {chat.update_time.strftime('%Y-%m-%d %H:%M:%S') if chat.update_time else '无'}
+ 成员数量: {chat.member_count}
+ 群主: {chat.owner or '未知'}
+ 公告: {chat.notice or '无'}
+

+ + +
+
+ """ + + html += """ +
+ + + """ + + return Response(content=html, media_type="text/html") \ No newline at end of file diff --git a/app/core/wecomclient.py b/app/core/wecomclient.py index b5393b6..ee32c1c 100644 --- a/app/core/wecomclient.py +++ b/app/core/wecomclient.py @@ -112,8 +112,16 @@ class WecomClient: logging.error(f"unionid_to_external_userid异常: {str(e)}") return None - async def send_welcome_message(self, chat_id: str) -> bool: - """发送欢迎消息""" + async def send_welcome_message(self, chat_id: str, user_id: str = None) -> bool: + """发送欢迎消息 + + Args: + chat_id: 群聊ID + user_id: 用户ID,如果指定则发送私信,否则发送群消息 + + Returns: + bool: 是否发送成功 + """ try: # 1. 获取 access_token access_token = await self.get_access_token() @@ -121,24 +129,37 @@ class WecomClient: logging.error("获取access_token失败") return False + welcome_text = f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。 + +‼ 微信下单,快递到家 ‼ + +🎁 新人礼包 +𝟏 赠送𝟏𝟓张【𝟑元跑腿券】 +𝟐 赠送𝟔枚鲜鸡蛋【首次下单】 +━ ━ ━ ━ ━🎊━ ━ ━ ━ ━ +↓点击↓小程序领券下单 &""" + # 2. 发送欢迎消息 - url = f"https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token={access_token}" - data = { - "chatid": chat_id, - "msgtype": "text", - "text": { - "content": f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。 - - ‼ 微信下单,快递到家 ‼ - - 🎁 新人礼包 - 𝟏 赠送𝟏𝟓张【𝟑元跑腿券】 - 𝟐 赠送𝟔枚鲜鸡蛋【首次下单】 - ━ ━ ━ ━ ━🎊━ ━ ━ ━ ━ - ↓点击↓小程序领券下单 &""" - }, - "safe": 0 - } + if user_id: + # 发送私信 + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token={access_token}" + data = { + "welcome_code": user_id, # 这里使用user_id作为临时的欢迎码 + "text": { + "content": welcome_text + } + } + else: + # 发送群消息 + url = f"https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token={access_token}" + data = { + "chatid": chat_id, + "msgtype": "text", + "text": { + "content": welcome_text + }, + "safe": 0 + } async with aiohttp.ClientSession() as session: async with session.post(url, json=data) as response: @@ -152,5 +173,243 @@ class WecomClient: except Exception as e: logging.error(f"发送欢迎消息异常: {str(e)}") return False + + async def get_external_chat_info(self, chat_id: str) -> Optional[Dict[str, Any]]: + """获取外部群聊信息 + + Args: + chat_id: 群聊ID + + Returns: + Dict: 群聊信息 + """ + try: + access_token = await self.get_access_token() + if not access_token: + logging.error("获取access_token失败") + return None + + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/groupchat/get?access_token={access_token}" + data = { + "chat_id": chat_id + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, json=data) as response: + result = await response.json() + if result.get("errcode") == 0: + return result.get("group_chat") + else: + logging.error(f"获取外部群聊信息失败: {result}") + return None + except Exception as e: + logging.error(f"获取外部群聊信息异常: {str(e)}") + return None + + async def get_external_contact_info(self, external_userid: str) -> Optional[Dict[str, Any]]: + """获取外部联系人信息 + + Args: + external_userid: 外部联系人ID + + Returns: + Dict: 外部联系人信息 + """ + try: + access_token = await self.get_access_token() + if not access_token: + logging.error("获取access_token失败") + return None + + url = f"https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token={access_token}" + params = { + "external_userid": external_userid + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params) as response: + result = await response.json() + if result.get("errcode") == 0: + return result.get("external_contact") + else: + logging.error(f"获取外部联系人信息失败: {result}") + return None + except Exception as e: + logging.error(f"获取外部联系人信息异常: {str(e)}") + return None + + async def handle_chat_change_event(self, chat_id: str, change_type: str, update_detail: str, join_user_id: str = None) -> bool: + """处理群聊变更事件 + + Args: + chat_id: 群聊ID + change_type: 变更类型 create/update/dismiss + update_detail: 变更详情 add_member/del_member/change_owner/change_name/change_notice + join_user_id: 加入的用户ID + + Returns: + bool: 处理是否成功 + """ + from app.models.wecom_external_chat import WecomExternalChatDB, WecomExternalChatMemberDB + from app.models.wecom_external_chat import WecomExternalChatCreate, WecomExternalChatMemberCreate + from app.models.database import SessionLocal + + try: + db = SessionLocal() + + # 群创建事件 + if change_type == "create": + # 获取群聊信息 + chat_info = await self.get_external_chat_info(chat_id) + if not chat_info: + return False + + # 保存群聊信息 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if not chat_db: + chat_create = WecomExternalChatCreate( + chat_id=chat_id, + name=chat_info.get("name"), + owner=chat_info.get("owner"), + member_count=len(chat_info.get("member_list", [])), + notice=chat_info.get("notice") + ) + chat_db = WecomExternalChatDB(**chat_create.dict()) + db.add(chat_db) + db.commit() + + # 保存群成员信息 + for member in chat_info.get("member_list", []): + user_id = member.get("userid") + member_type = member.get("type") + + # 检查成员是否已存在 + member_db = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id, + WecomExternalChatMemberDB.user_id == user_id + ).first() + + if not member_db: + # 获取外部联系人详情 + user_info = None + if member_type == "EXTERNAL": + user_info = await self.get_external_contact_info(user_id) + + member_create = WecomExternalChatMemberCreate( + chat_id=chat_id, + user_id=user_id, + type=member_type, + name=user_info.get("name") if user_info else None, + unionid=user_info.get("unionid") if user_info else None + ) + member_db = WecomExternalChatMemberDB(**member_create.dict()) + db.add(member_db) + + db.commit() + return True + + # 群更新事件 - 添加成员 + elif change_type == "update" and update_detail == "add_member": + if not join_user_id: + return False + + # 检查群聊是否存在 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if not chat_db: + # 获取群聊信息并创建 + chat_info = await self.get_external_chat_info(chat_id) + if chat_info: + chat_create = WecomExternalChatCreate( + chat_id=chat_id, + name=chat_info.get("name"), + owner=chat_info.get("owner"), + member_count=len(chat_info.get("member_list", [])), + notice=chat_info.get("notice") + ) + chat_db = WecomExternalChatDB(**chat_create.dict()) + db.add(chat_db) + db.commit() + + # 检查成员是否已存在 + member_db = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id, + WecomExternalChatMemberDB.user_id == join_user_id + ).first() + + if not member_db: + # 判断是内部成员还是外部联系人 + member_type = "EXTERNAL" # 默认为外部联系人 + + # 获取外部联系人详情 + user_info = await self.get_external_contact_info(join_user_id) + + member_create = WecomExternalChatMemberCreate( + chat_id=chat_id, + user_id=join_user_id, + type=member_type, + name=user_info.get("name") if user_info else None, + unionid=user_info.get("unionid") if user_info else None + ) + member_db = WecomExternalChatMemberDB(**member_create.dict()) + db.add(member_db) + db.commit() + + # 发送欢迎消息 + if member_type == "EXTERNAL": + await self.send_welcome_message(chat_id) + + # 更新发送欢迎消息状态 + member_db.welcome_sent = True + db.commit() + + # 更新群成员数量 + if chat_db: + chat_db.member_count = chat_db.member_count + 1 + db.commit() + + return True + + # 群更新事件 - 移除成员 + elif change_type == "update" and update_detail == "del_member": + if not join_user_id: + return False + + # 删除成员记录 + member_db = db.query(WecomExternalChatMemberDB).filter( + WecomExternalChatMemberDB.chat_id == chat_id, + WecomExternalChatMemberDB.user_id == join_user_id + ).first() + + if member_db: + db.delete(member_db) + db.commit() + + # 更新群成员数量 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if chat_db: + chat_db.member_count = max(0, chat_db.member_count - 1) + db.commit() + + return True + + # 群解散事件 + elif change_type == "dismiss": + # 标记群聊为非活跃 + chat_db = db.query(WecomExternalChatDB).filter(WecomExternalChatDB.chat_id == chat_id).first() + if chat_db: + chat_db.is_active = False + db.commit() + + return True + + return False + except Exception as e: + logging.error(f"处理群聊变更事件异常: {str(e)}") + if 'db' in locals(): + db.close() + return False + finally: + if 'db' in locals(): + db.close() wecom_client = WecomClient() \ No newline at end of file diff --git a/app/models/wecom_external_chat.py b/app/models/wecom_external_chat.py new file mode 100644 index 0000000..bf30312 --- /dev/null +++ b/app/models/wecom_external_chat.py @@ -0,0 +1,98 @@ +from sqlalchemy import Column, String, Integer, DateTime, JSON, Boolean, ForeignKey, Text +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any +from datetime import datetime +from .database import Base + +class WecomExternalChatDB(Base): + """企业微信外部群聊表""" + __tablename__ = "wecom_external_chats" + + id = Column(Integer, primary_key=True, autoincrement=True) + chat_id = Column(String(64), nullable=False, unique=True, index=True) # 群聊ID + name = Column(String(100), nullable=True) # 群名称 + owner = Column(String(64), nullable=True) # 群主ID + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + member_count = Column(Integer, nullable=False, default=0) # 成员数量 + notice = Column(String(500), nullable=True) # 群公告 + is_active = Column(Boolean, nullable=False, default=True) # 是否活跃 + +class WecomExternalChatMemberDB(Base): + """企业微信外部群聊成员表""" + __tablename__ = "wecom_external_chat_members" + + id = Column(Integer, primary_key=True, autoincrement=True) + chat_id = Column(String(64), nullable=False, index=True) # 群聊ID + user_id = Column(String(64), nullable=False, index=True) # 用户ID + type = Column(String(32), nullable=False) # 成员类型: INTERNAL(内部成员)、EXTERNAL(外部联系人) + join_time = Column(DateTime(timezone=True), server_default=func.now()) # 加入时间 + unionid = Column(String(64), nullable=True) # 微信unionid + name = Column(String(100), nullable=True) # 成员名称 + mobile = Column(String(20), nullable=True) # 手机号 + welcome_sent = Column(Boolean, nullable=False, default=False) # 是否已发送欢迎消息 + + # 设置联合唯一索引 + __table_args__ = ( + {"mysql_charset": "utf8mb4"}, + ) + +# Pydantic 模型 +class WecomExternalChatCreate(BaseModel): + chat_id: str + name: Optional[str] = None + owner: Optional[str] = None + member_count: int = 0 + notice: Optional[str] = None + is_active: bool = True + +class WecomExternalChatUpdate(BaseModel): + name: Optional[str] = None + owner: Optional[str] = None + member_count: Optional[int] = None + notice: Optional[str] = None + is_active: Optional[bool] = None + +class WecomExternalChatInfo(BaseModel): + id: int + chat_id: str + name: Optional[str] = None + owner: Optional[str] = None + create_time: datetime + update_time: Optional[datetime] = None + member_count: int = 0 + notice: Optional[str] = None + is_active: bool = True + + class Config: + from_attributes = True + +class WecomExternalChatMemberCreate(BaseModel): + chat_id: str + user_id: str + type: str + unionid: Optional[str] = None + name: Optional[str] = None + mobile: Optional[str] = None + welcome_sent: bool = False + +class WecomExternalChatMemberUpdate(BaseModel): + unionid: Optional[str] = None + name: Optional[str] = None + mobile: Optional[str] = None + welcome_sent: Optional[bool] = None + +class WecomExternalChatMemberInfo(BaseModel): + id: int + chat_id: str + user_id: str + type: str + join_time: datetime + unionid: Optional[str] = None + name: Optional[str] = None + mobile: Optional[str] = None + welcome_sent: bool + + class Config: + from_attributes = True \ No newline at end of file diff --git a/jobs.sqlite b/jobs.sqlite index 9902dba2428be45f7451274c7ab58c6985492600..e96a0a0c033b988d6fbca04a61dec97409b71a3a 100644 GIT binary patch delta 82 zcmZoTz}Rqrae_3X