feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -46,6 +46,10 @@ class WecomService:
|
||||
)
|
||||
# 内存缓存(Redis 不可用时的降级方案)
|
||||
self._token_cache: Optional[str] = None
|
||||
# 通讯录同步 token 的内存缓存(与应用 token 区分,避免互相覆盖)
|
||||
self._contact_token_cache: Optional[str] = None
|
||||
# 会议室 API token 的内存缓存(使用独立 secret,与应用/通讯录 token 区分)
|
||||
self._meetingroom_token_cache: Optional[str] = None
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# access_token 管理
|
||||
@@ -123,9 +127,446 @@ class WecomService:
|
||||
logger.error(f"获取 access_token 网络错误: {e}")
|
||||
raise Exception(f"企微API网络错误: {e}") from e
|
||||
|
||||
async def get_contact_access_token(self) -> str:
|
||||
"""获取企微通讯录同步 access_token。
|
||||
|
||||
使用通讯录同步专用 Secret(wecom_contact_secret)获取 access_token,
|
||||
该 Secret 拥有完整的通讯录读取权限(部门列表、成员列表等)。
|
||||
|
||||
当 wecom_contact_secret 未配置时,降级使用普通应用 access_token
|
||||
(此时通讯录相关 API 可能因权限不足返回 errcode 60011)。
|
||||
|
||||
对应企微API:
|
||||
GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=CONTACT_SECRET
|
||||
|
||||
Returns:
|
||||
str: access_token 字符串
|
||||
|
||||
Raises:
|
||||
Exception: 获取 access_token 失败
|
||||
"""
|
||||
# 降级:未配置通讯录同步 Secret,使用普通应用 token
|
||||
if not settings.wecom_contact_secret:
|
||||
logger.debug("未配置 wecom_contact_secret,降级使用普通 access_token")
|
||||
return await self.get_access_token()
|
||||
|
||||
# Redis 缓存 key(与应用 token 区分,避免互相覆盖)
|
||||
cache_key = "wecom:contact_access_token"
|
||||
|
||||
# 1. 尝试从 Redis 缓存获取
|
||||
if self.redis:
|
||||
try:
|
||||
cached_token = await self.redis.get(cache_key)
|
||||
if cached_token:
|
||||
logger.debug("从缓存获取通讯录 access_token")
|
||||
return cached_token.decode("utf-8") if isinstance(cached_token, bytes) else cached_token
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis 读取通讯录 token 失败(降级): {e}")
|
||||
|
||||
# 1b. 尝试从内存缓存获取
|
||||
if self._contact_token_cache:
|
||||
logger.debug("从内存缓存获取通讯录 access_token")
|
||||
return self._contact_token_cache
|
||||
|
||||
# 2. 缓存未命中,调用企微 API 获取
|
||||
logger.info("缓存未命中,调用企微API获取通讯录 access_token")
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
params = {
|
||||
"corpid": settings.wecom_corp_id,
|
||||
"corpsecret": settings.wecom_contact_secret,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.client.get(url, params=params)
|
||||
result = response.json()
|
||||
|
||||
# 检查企微API返回码
|
||||
if result.get("errcode") != 0:
|
||||
error_msg = result.get("errmsg", "未知错误")
|
||||
logger.error(f"获取通讯录 access_token 失败: errcode={result.get('errcode')}, errmsg={error_msg}")
|
||||
raise Exception(f"企微API错误: {error_msg}")
|
||||
|
||||
access_token = result["access_token"]
|
||||
expires_in = result.get("expires_in", 7200)
|
||||
|
||||
# 3. 缓存到 Redis,TTL = 有效期 - 300秒(提前刷新)
|
||||
buffer_seconds = 300
|
||||
cache_ttl = max(expires_in - buffer_seconds, 60) # 至少缓存 60 秒
|
||||
if self.redis:
|
||||
try:
|
||||
await self.redis.setex(cache_key, cache_ttl, access_token)
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis 写入通讯录 token 失败(降级): {e}")
|
||||
|
||||
# 3b. 同时缓存到内存
|
||||
self._contact_token_cache = access_token
|
||||
|
||||
logger.info(f"通讯录 access_token 获取成功,缓存 TTL={cache_ttl}秒")
|
||||
return access_token
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"获取通讯录 access_token 网络错误: {e}")
|
||||
raise Exception(f"企微API网络错误: {e}") from e
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 发送文本消息
|
||||
# 会议室 API access_token 管理
|
||||
# --------------------------------------------------------------------------
|
||||
async def get_meetingroom_access_token(self) -> str:
|
||||
"""获取企微会议室 API 专用 access_token。
|
||||
|
||||
企微会议室API需要独立的"会议室"secret获取token,
|
||||
不同于普通应用secret和通讯录secret。
|
||||
|
||||
当 wecom_meetingroom_secret 未配置时,降级使用普通应用 access_token
|
||||
(此时会议室API可能因权限不足返回错误)。
|
||||
|
||||
Redis缓存key: wecom:meetingroom_access_token
|
||||
TTL: 有效期 - 300秒(提前刷新),与 get_access_token() 模式一致。
|
||||
|
||||
Returns:
|
||||
str: access_token 字符串
|
||||
|
||||
Raises:
|
||||
Exception: 获取 access_token 失败
|
||||
"""
|
||||
# 降级:未配置会议室专用 Secret,使用普通应用 token
|
||||
if not settings.wecom_meetingroom_secret:
|
||||
logger.debug("未配置 wecom_meetingroom_secret,降级使用普通 access_token")
|
||||
return await self.get_access_token()
|
||||
|
||||
cache_key = "wecom:meetingroom_access_token"
|
||||
|
||||
# 1. 尝试从 Redis 缓存获取
|
||||
if self.redis:
|
||||
try:
|
||||
cached_token = await self.redis.get(cache_key)
|
||||
if cached_token:
|
||||
logger.debug("从缓存获取会议室 access_token")
|
||||
return cached_token.decode("utf-8") if isinstance(cached_token, bytes) else cached_token
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis 读取会议室 token 失败(降级): {e}")
|
||||
|
||||
# 1b. 尝试从内存缓存获取
|
||||
if self._meetingroom_token_cache:
|
||||
logger.debug("从内存缓存获取会议室 access_token")
|
||||
return self._meetingroom_token_cache
|
||||
|
||||
# 2. 缓存未命中,调用企微 API 获取
|
||||
logger.info("缓存未命中,调用企微API获取会议室 access_token")
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
params = {
|
||||
"corpid": settings.wecom_corp_id,
|
||||
"corpsecret": settings.wecom_meetingroom_secret,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.client.get(url, params=params)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") != 0:
|
||||
error_msg = result.get("errmsg", "未知错误")
|
||||
logger.error(f"获取会议室 access_token 失败: errcode={result.get('errcode')}, errmsg={error_msg}")
|
||||
raise Exception(f"企微API错误: {error_msg}")
|
||||
|
||||
access_token = result["access_token"]
|
||||
expires_in = result.get("expires_in", 7200)
|
||||
|
||||
# 3. 缓存到 Redis
|
||||
buffer_seconds = 300
|
||||
cache_ttl = max(expires_in - buffer_seconds, 60)
|
||||
if self.redis:
|
||||
try:
|
||||
await self.redis.setex(cache_key, cache_ttl, access_token)
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis 写入会议室 token 失败(降级): {e}")
|
||||
|
||||
# 3b. 同时缓存到内存
|
||||
self._meetingroom_token_cache = access_token
|
||||
|
||||
logger.info(f"会议室 access_token 获取成功,缓存 TTL={cache_ttl}秒")
|
||||
return access_token
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"获取会议室 access_token 网络错误: {e}")
|
||||
raise Exception(f"企微API网络错误: {e}") from e
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 会议室管理 API
|
||||
# --------------------------------------------------------------------------
|
||||
async def get_meetingroom_list(
|
||||
self,
|
||||
city: Optional[str] = None,
|
||||
building: Optional[str] = None,
|
||||
floor: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取会议室列表。
|
||||
|
||||
对应企微API:
|
||||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/list
|
||||
|
||||
Args:
|
||||
city: 城市名称(可选过滤)
|
||||
building: 楼宇名称(可选过滤)
|
||||
floor: 楼层名称(可选过滤)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 会议室列表,每项含 meetingroom_id/name/capacity/location等
|
||||
|
||||
Raises:
|
||||
Exception: 获取失败
|
||||
"""
|
||||
access_token = await self.get_meetingroom_access_token()
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/list?access_token={access_token}"
|
||||
|
||||
# 构建请求体(仅包含非空过滤条件)
|
||||
payload: Dict[str, Any] = {}
|
||||
if city:
|
||||
payload["city"] = city
|
||||
if building:
|
||||
payload["building"] = building
|
||||
if floor:
|
||||
payload["floor"] = floor
|
||||
|
||||
try:
|
||||
response = await self.client.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
logger.error(
|
||||
f"获取会议室列表失败: errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
raise Exception(f"获取会议室列表失败: {result.get('errmsg')}")
|
||||
|
||||
meetingroom_list = result.get("meetingroom_list", [])
|
||||
logger.info(f"获取会议室列表成功: count={len(meetingroom_list)}")
|
||||
return meetingroom_list
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"获取会议室列表网络错误: {e}")
|
||||
raise Exception(f"获取会议室列表网络错误: {e}") from e
|
||||
|
||||
async def get_booking_info(
|
||||
self,
|
||||
meetingroom_id: int,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取会议室预定状态。
|
||||
|
||||
对应企微API:
|
||||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/get_booking_info
|
||||
|
||||
企微API要求传入时间范围(时间戳),不支持跨天查询。
|
||||
|
||||
Args:
|
||||
meetingroom_id: 企微会议室ID
|
||||
start_time: 查询开始时间(ISO 8601 格式,如 "2026-07-15T00:00:00+08:00")
|
||||
end_time: 查询结束时间(ISO 8601 格式)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 预定记录列表
|
||||
|
||||
Raises:
|
||||
Exception: 获取失败
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
access_token = await self.get_meetingroom_access_token()
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/get_booking_info?access_token={access_token}"
|
||||
|
||||
# 将 ISO 8601 时间转为时间戳(秒)
|
||||
start_dt = datetime.fromisoformat(start_time)
|
||||
end_dt = datetime.fromisoformat(end_time)
|
||||
start_ts = int(start_dt.timestamp())
|
||||
end_ts = int(end_dt.timestamp())
|
||||
|
||||
payload = {
|
||||
"meetingroom_id": meetingroom_id,
|
||||
"start_time": start_ts,
|
||||
"end_time": end_ts,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.client.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
logger.error(
|
||||
f"获取预定状态失败: meetingroom_id={meetingroom_id}, "
|
||||
f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
raise Exception(f"获取预定状态失败: {result.get('errmsg')}")
|
||||
|
||||
booking_list = result.get("booking_list", [])
|
||||
logger.info(f"获取预定状态成功: meetingroom_id={meetingroom_id}, count={len(booking_list)}")
|
||||
return booking_list
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"获取预定状态网络错误: meetingroom_id={meetingroom_id}, error={e}")
|
||||
raise Exception(f"获取预定状态网络错误: {e}") from e
|
||||
|
||||
async def book_meetingroom(
|
||||
self,
|
||||
meetingroom_id: int,
|
||||
subject: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
booker: str,
|
||||
attendees: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""预定会议室。
|
||||
|
||||
对应企微API:
|
||||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/book
|
||||
|
||||
时间需按30分钟取整(15:15→15:00, 15:45→16:00)。
|
||||
仅可预定无需审批的会议室。
|
||||
|
||||
Args:
|
||||
meetingroom_id: 企微会议室ID
|
||||
subject: 会议主题
|
||||
start_time: 开始时间(ISO 8601 格式)
|
||||
end_time: 结束时间(ISO 8601 格式)
|
||||
booker: 预定人userid
|
||||
attendees: 参与人userid列表(可选)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 预定结果,含 booking_id
|
||||
|
||||
Raises:
|
||||
Exception: 预定失败
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
access_token = await self.get_meetingroom_access_token()
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/book?access_token={access_token}"
|
||||
|
||||
# 将 ISO 8601 时间转为时间戳(秒)
|
||||
start_dt = datetime.fromisoformat(start_time)
|
||||
end_dt = datetime.fromisoformat(end_time)
|
||||
start_ts = int(start_dt.timestamp())
|
||||
end_ts = int(end_dt.timestamp())
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"meetingroom_id": meetingroom_id,
|
||||
"subject": subject,
|
||||
"start_time": start_ts,
|
||||
"end_time": end_ts,
|
||||
"booker": booker,
|
||||
}
|
||||
if attendees:
|
||||
payload["attendees"] = attendees
|
||||
|
||||
try:
|
||||
response = await self.client.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
logger.error(
|
||||
f"预定会议室失败: meetingroom_id={meetingroom_id}, subject={subject}, "
|
||||
f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
raise Exception(f"预定会议室失败: {result.get('errmsg')}")
|
||||
|
||||
booking_id = result.get("booking_id", "")
|
||||
logger.info(f"预定会议室成功: meetingroom_id={meetingroom_id}, booking_id={booking_id}")
|
||||
return result
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"预定会议室网络错误: meetingroom_id={meetingroom_id}, error={e}")
|
||||
raise Exception(f"预定会议室网络错误: {e}") from e
|
||||
|
||||
async def cancel_booking(
|
||||
self,
|
||||
booking_id: str,
|
||||
meetingroom_id: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""取消会议室预定。
|
||||
|
||||
对应企微API:
|
||||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/cancel_book
|
||||
|
||||
Args:
|
||||
booking_id: 企微预定ID
|
||||
meetingroom_id: 企微会议室ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 取消结果
|
||||
|
||||
Raises:
|
||||
Exception: 取消失败
|
||||
"""
|
||||
access_token = await self.get_meetingroom_access_token()
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/cancel_book?access_token={access_token}"
|
||||
|
||||
payload = {
|
||||
"meetingroom_id": meetingroom_id,
|
||||
"booking_id": booking_id,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.client.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
logger.error(
|
||||
f"取消预定失败: booking_id={booking_id}, meetingroom_id={meetingroom_id}, "
|
||||
f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
raise Exception(f"取消预定失败: {result.get('errmsg')}")
|
||||
|
||||
logger.info(f"取消预定成功: booking_id={booking_id}, meetingroom_id={meetingroom_id}")
|
||||
return result
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"取消预定网络错误: booking_id={booking_id}, error={e}")
|
||||
raise Exception(f"取消预定网络错误: {e}") from e
|
||||
|
||||
async def get_booking_detail(
|
||||
self,
|
||||
meetingroom_id: int,
|
||||
booking_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取预定详情。
|
||||
|
||||
对应企微API:
|
||||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/bookinfo/get
|
||||
|
||||
Args:
|
||||
meetingroom_id: 企微会议室ID
|
||||
booking_id: 企微预定ID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 预定详情,含 subject/booker/attendees/start_time/end_time
|
||||
|
||||
Raises:
|
||||
Exception: 获取失败
|
||||
"""
|
||||
access_token = await self.get_meetingroom_access_token()
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/bookinfo/get?access_token={access_token}"
|
||||
|
||||
payload = {
|
||||
"meetingroom_id": meetingroom_id,
|
||||
"booking_id": booking_id,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.client.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
logger.error(
|
||||
f"获取预定详情失败: booking_id={booking_id}, meetingroom_id={meetingroom_id}, "
|
||||
f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
raise Exception(f"获取预定详情失败: {result.get('errmsg')}")
|
||||
|
||||
logger.info(f"获取预定详情成功: booking_id={booking_id}")
|
||||
return result
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"获取预定详情网络错误: booking_id={booking_id}, error={e}")
|
||||
raise Exception(f"获取预定详情网络错误: {e}") from e
|
||||
async def send_text_message(
|
||||
self, user_id: str, content: str
|
||||
) -> Dict[str, Any]:
|
||||
@@ -249,6 +690,156 @@ class WecomService:
|
||||
logger.error(f"发送卡片消息网络错误: user_id={user_id}, error={e}")
|
||||
raise Exception(f"发送消息网络错误: {e}") from e
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 发送模板卡片消息 (text_notice)
|
||||
# --------------------------------------------------------------------------
|
||||
async def send_template_card_message(
|
||||
self,
|
||||
user_id: str,
|
||||
main_title: str,
|
||||
main_title_desc: str = "",
|
||||
sub_title_text: str = "",
|
||||
emphasis_title: str = "",
|
||||
emphasis_desc: str = "",
|
||||
jump_list: Optional[List[Dict[str, Any]]] = None,
|
||||
card_action_url: str = "",
|
||||
source_icon: str = "",
|
||||
source_desc: str = "",
|
||||
horizontal_content_list: Optional[List[Dict[str, str]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""向员工发送模板卡片消息(text_notice类型)。
|
||||
|
||||
对应企微API:
|
||||
POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=TOKEN
|
||||
|
||||
请求体 (text_notice):
|
||||
{
|
||||
"touser": "UserID",
|
||||
"msgtype": "template_card",
|
||||
"agentid": 1000002,
|
||||
"template_card": {
|
||||
"card_type": "text_notice",
|
||||
"source": {
|
||||
"icon_url": "来源图标URL",
|
||||
"desc": "来源描述"
|
||||
},
|
||||
"main_title": {
|
||||
"title": "一级标题",
|
||||
"desc": "标题辅助信息"
|
||||
},
|
||||
"sub_title_text": "二级普通文本",
|
||||
"emphasis_content": {
|
||||
"title": "关键数据",
|
||||
"desc": "描述"
|
||||
},
|
||||
"horizontal_content_list": [
|
||||
{"keyname": "标题", "value": "内容"}
|
||||
],
|
||||
"jump_list": [
|
||||
{
|
||||
"type": 1,
|
||||
"title": "跳转链接文案",
|
||||
"url": "https://..."
|
||||
}
|
||||
],
|
||||
"card_action": {
|
||||
"type": 1,
|
||||
"url": "https://..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Args:
|
||||
user_id: 员工的企微 UserID
|
||||
main_title: 主标题(一级标题)
|
||||
main_title_desc: 主标题辅助信息
|
||||
sub_title_text: 二级普通文本
|
||||
emphasis_title: 关键数据标题(如剩余时间)
|
||||
emphasis_desc: 关键数据描述
|
||||
jump_list: 跳转按钮列表(每项含 type/title/url)
|
||||
card_action_url: 卡片整体点击跳转链接
|
||||
source_icon: 来源图标URL(可选,如企微agent头像)
|
||||
source_desc: 来源描述(如"IT智能服务台")
|
||||
horizontal_content_list: 关键信息列表 [{"keyname": "标题", "value": "内容"}]
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 企微API返回结果
|
||||
"""
|
||||
access_token = await self.get_access_token()
|
||||
url_api = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
|
||||
|
||||
# 构建 template_card 结构
|
||||
template_card: Dict[str, Any] = {
|
||||
"card_type": "text_notice",
|
||||
"main_title": {
|
||||
"title": main_title,
|
||||
}
|
||||
}
|
||||
|
||||
# 添加来源信息(可选)
|
||||
if source_icon or source_desc:
|
||||
template_card["source"] = {}
|
||||
if source_icon:
|
||||
template_card["source"]["icon_url"] = source_icon
|
||||
if source_desc:
|
||||
template_card["source"]["desc"] = source_desc
|
||||
|
||||
# 添加主标题辅助信息(可选)
|
||||
if main_title_desc:
|
||||
template_card["main_title"]["desc"] = main_title_desc
|
||||
|
||||
# 添加二级普通文本(可选)
|
||||
if sub_title_text:
|
||||
template_card["sub_title_text"] = sub_title_text
|
||||
|
||||
# 添加关键数据高亮(可选)
|
||||
if emphasis_title:
|
||||
template_card["emphasis_content"] = {
|
||||
"title": emphasis_title,
|
||||
}
|
||||
if emphasis_desc:
|
||||
template_card["emphasis_content"]["desc"] = emphasis_desc
|
||||
|
||||
# 添加关键信息列表(可选)
|
||||
if horizontal_content_list:
|
||||
template_card["horizontal_content_list"] = horizontal_content_list
|
||||
|
||||
# 添加跳转按钮列表(可选)
|
||||
if jump_list:
|
||||
template_card["jump_list"] = jump_list
|
||||
|
||||
# 添加卡片整体点击事件(可选)
|
||||
if card_action_url:
|
||||
template_card["card_action"] = {
|
||||
"type": 1, # 跳转URL
|
||||
"url": card_action_url,
|
||||
}
|
||||
|
||||
payload = {
|
||||
"touser": user_id,
|
||||
"msgtype": "template_card",
|
||||
"agentid": int(settings.wecom_agent_id),
|
||||
"template_card": template_card,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self.client.post(url_api, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") != 0:
|
||||
logger.error(
|
||||
f"发送模板卡片消息失败: user_id={user_id}, "
|
||||
f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
else:
|
||||
logger.info(f"发送模板卡片消息成功: user_id={user_id}")
|
||||
|
||||
return result
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"发送模板卡片消息网络错误: user_id={user_id}, error={e}")
|
||||
raise Exception(f"发送消息网络错误: {e}") from e
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 发送图片消息
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -436,7 +1027,7 @@ class WecomService:
|
||||
Raises:
|
||||
Exception: 获取失败
|
||||
"""
|
||||
access_token = await self.get_access_token()
|
||||
access_token = await self.get_contact_access_token()
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/user/list"
|
||||
params = {
|
||||
"access_token": access_token,
|
||||
@@ -463,6 +1054,60 @@ class WecomService:
|
||||
logger.error(f"获取部门成员网络错误: dept_id={department_id}, error={e}")
|
||||
raise Exception(f"获取部门成员网络错误: {e}") from e
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 获取部门列表
|
||||
# --------------------------------------------------------------------------
|
||||
async def get_department_list(self, department_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""获取部门列表。
|
||||
|
||||
对应企微API:
|
||||
GET https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=TOKEN&id=ID
|
||||
|
||||
返回部门列表,每个部门包含 id(部门ID)、name(部门名称)、parentid(父部门ID)。
|
||||
不传 department_id 时返回全量部门列表。
|
||||
|
||||
用于将 user/list 返回的成员 department ID 列表(如 [1,2])解析为可读的部门名称。
|
||||
|
||||
需要企微通讯录只读权限。
|
||||
|
||||
Args:
|
||||
department_id: 父部门ID(可选)。不传则返回全量部门列表;
|
||||
传入时返回该部门及其子部门。
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 部门列表,每项含 id/name/parentid
|
||||
|
||||
Raises:
|
||||
Exception: 获取失败
|
||||
"""
|
||||
access_token = await self.get_contact_access_token()
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/department/list"
|
||||
params: Dict[str, Any] = {
|
||||
"access_token": access_token,
|
||||
}
|
||||
# department_id 为 None 时不传,企微返回全量部门列表
|
||||
if department_id is not None:
|
||||
params["id"] = department_id
|
||||
|
||||
try:
|
||||
response = await self.client.get(url, params=params)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
logger.error(
|
||||
f"获取部门列表失败: dept_id={department_id}, "
|
||||
f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
|
||||
)
|
||||
raise Exception(f"获取部门列表失败: {result.get('errmsg')}")
|
||||
|
||||
department_list = result.get("department", [])
|
||||
logger.info(f"获取部门列表成功: count={len(department_list)}")
|
||||
return department_list
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"获取部门列表网络错误: dept_id={department_id}, error={e}")
|
||||
raise Exception(f"获取部门列表网络错误: {e}") from e
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# JS-SDK 票据 (v0.5.4:应急页身份检测用)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user