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 9902dba..e96a0a0 100644
Binary files a/jobs.sqlite and b/jobs.sqlite differ