This commit is contained in:
aaron 2025-03-30 11:33:16 +08:00
parent 5448a04be4
commit 0f00caf23a
4 changed files with 625 additions and 37 deletions

View File

@ -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,36 +110,46 @@ 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}")
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}")
if update_detail == 'add_member':
print(f"发送欢迎消息")
# 发送欢迎消息
# await wecom_client.send_welcome_message(chat_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")
@ -146,7 +157,6 @@ async def wechat_corp_callback(
logging.exception("处理企业微信回调消息异常")
return Response(content="success", media_type="text/plain")
class UnionidToExternalUseridRequest(BaseModel):
unionid: str
openid: str
@ -159,3 +169,224 @@ async def unionid_to_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)
@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 = """
<!DOCTYPE html>
<html>
<head>
<title>企业微信外部群聊信息</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
h1, h2 { color: #333; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
tr:nth-child(even) { background-color: #f9f9f9; }
.container { max-width: 1200px; margin: 0 auto; }
.btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
color: #fff;
background-color: #337ab7;
text-decoration: none;
}
.btn:hover { background-color: #286090; }
</style>
<script>
function loadMembers(chatId) {
fetch(`/api/wecom/external-chats/${chatId}/members`)
.then(response => response.json())
.then(data => {
if (data.code === 0) {
const members = data.data;
let html = '<table>';
html += '<tr><th>ID</th><th>用户ID</th><th>类型</th><th>姓名</th><th>加入时间</th><th>是否已发送欢迎</th></tr>';
members.forEach(member => {
html += `<tr>
<td>${member.id}</td>
<td>${member.user_id}</td>
<td>${member.type}</td>
<td>${member.name || '未知'}</td>
<td>${new Date(member.join_time).toLocaleString()}</td>
<td>${member.welcome_sent ? '' : ''}</td>
</tr>`;
});
html += '</table>';
document.getElementById('members-' + chatId).innerHTML = html;
} else {
alert('获取成员列表失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('获取成员列表失败');
});
}
function syncChat(chatId) {
fetch(`/api/wecom/sync-chat/${chatId}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
alert('同步成功');
location.reload();
} else {
alert('同步失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('同步失败');
});
}
</script>
</head>
<body>
<div class="container">
<h1>企业微信外部群聊信息</h1>
"""
if not chats:
html += "<p>暂无群聊信息</p>"
else:
for chat in chats:
html += f"""
<div style="margin-bottom: 30px; border: 1px solid #ddd; padding: 15px; border-radius: 5px;">
<h2>{chat.name or '未命名群聊'} ({chat.chat_id})</h2>
<p>
<strong>创建时间:</strong> {chat.create_time.strftime('%Y-%m-%d %H:%M:%S')}<br>
<strong>更新时间:</strong> {chat.update_time.strftime('%Y-%m-%d %H:%M:%S') if chat.update_time else ''}<br>
<strong>成员数量:</strong> {chat.member_count}<br>
<strong>群主:</strong> {chat.owner or '未知'}<br>
<strong>公告:</strong> {chat.notice or ''}<br>
</p>
<button class="btn" onclick="loadMembers('{chat.chat_id}')">查看成员</button>
<button class="btn" onclick="syncChat('{chat.chat_id}')">同步群信息</button>
<div id="members-{chat.chat_id}" style="margin-top: 15px;"></div>
</div>
"""
html += """
</div>
</body>
</html>
"""
return Response(content=html, media_type="text/html")

View File

@ -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,21 +129,34 @@ class WecomClient:
logging.error("获取access_token失败")
return False
welcome_text = f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。
微信下单快递到家
🎁 新人礼包
𝟏 赠送𝟏𝟓张𝟑元跑腿券
𝟐 赠送𝟔枚鲜鸡蛋首次下单
🎊
点击小程序领券下单 &"""
# 2. 发送欢迎消息
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": f"""🥳 欢迎您进群,在群内可以享受📦【代取快递】跑腿服务。
微信下单快递到家
🎁 新人礼包
𝟏 赠送𝟏𝟓张𝟑元跑腿券
𝟐 赠送𝟔枚鲜鸡蛋首次下单
🎊
点击小程序领券下单 &"""
"content": welcome_text
},
"safe": 0
}
@ -153,4 +174,242 @@ class WecomClient:
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()

View File

@ -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

Binary file not shown.