Files
wecom_it_smart_desk/backend/app/services/avatar_service.py
T
Simon bea288e414 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
2026-07-11 23:13:10 +08:00

118 lines
4.5 KiB
Python

# =============================================================================
# 企微IT智能服务台 — 员工头像同步服务
# =============================================================================
# 说明:集中处理"从企微拿到头像 URL 后,更新 employee 表 + 清 Redis 缓存"的逻辑,
# 确保所有登录/认证路径(H5 OAuth、坐席密码、扫码确认、dev 登录)行为一致,
# 避免部分路径漏清缓存导致前端仍是旧图(要求 C:全路径一致性)。
#
# 设计原则:
# 1. 头像更新失败绝不阻塞登录主流程 → 所有异常内部吞掉并记录 warning。
# 2. 复用已有写法:先查 Employee,有则 update 字段,无则跳过(创建由各自登录逻辑负责)。
# 3. 统一清理 Redis 缓存 key = employee:avatar:{employee_id},强制后续读库取最新。
# 4. 清理企微头像 URL 的多余查询参数,保留稳定部分,降低 404 / 过期概率(要求 B)。
# =============================================================================
import logging
from datetime import datetime
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.employee import Employee
logger = logging.getLogger(__name__)
def clean_avatar_url(url: str) -> str:
"""清理企微头像 URL,去掉查询字符串(? 及其之后),保留稳定部分。
企微头像 URL 形如 https://wework.qpic.cn/...png?...,常带尺寸 / 时效参数,
去参后链接更稳定、不易因时效参数失效而 404。
Args:
url: 原始头像 URL
Returns:
str: 去参后的稳定 URL;空输入返回空串。
"""
if not url:
return ""
# 确保 HTTPS(企微头像可能返回 http://,HTTPS 页面不允许加载)
if url.startswith("http://"):
url = "https://" + url[7:]
return url.split("?", 1)[0]
def wrap_avatar_url(url: str) -> str:
"""将企微头像 URL 包装为后端代理 URL,解决 COEP/Mixed Content/CSP 跨域问题。
如果 URL 包含 wework.qpic.cn,转换为 /api/avatar/proxy?url=... 格式。
其他 URL(如 /duckula.webp)原样返回。
Args:
url: 清理后的头像 URL
Returns:
str: 包装后的代理 URL 或原始 URL
"""
if not url:
return ""
if "wework.qpic.cn" in url:
# 确保 HTTPS(代理端点也会校验,这里做一次以防万一)
if url.startswith("http://"):
url = "https://" + url[7:]
from urllib.parse import quote
return f"/api/avatar/proxy?url={quote(url, safe='')}"
return url
async def sync_employee_avatar(
db: AsyncSession,
redis_client: Optional[object],
employee_id: str,
avatar: str,
) -> None:
"""用企微返回的头像 URL 同步 employee 表并清缓存。
任一异常都内部处理,不向上抛出(头像更新失败绝不能阻塞登录)。
Args:
db: 数据库会话(本函数内部会 commit 一次以落库)
redis_client: Redis 客户端(可为 None,此时跳过清缓存)
employee_id: 企微 userid
avatar: 企微返回的头像 URL(空字符串表示无头像,不更新)
"""
# 清理多余查询参数,保留稳定部分
avatar = clean_avatar_url(avatar)
if not avatar:
# 企微未返回头像时不做任何写入,避免把已有头像清空
return
try:
stmt = select(Employee).where(
Employee.employee_id == employee_id,
Employee.corp_id == settings.wecom_corp_id,
)
result = await db.execute(stmt)
employee = result.scalars().first()
if employee:
employee.avatar = avatar
employee.avatar_updated_at = datetime.utcnow()
await db.commit()
logger.info(f"同步员工头像: employee_id={employee_id}")
else:
logger.debug(f"员工不存在,跳过头像同步: employee_id={employee_id}")
# 删除 Redis 头像缓存,强制后续读取数据库最新头像
if redis_client is not None:
try:
await redis_client.delete(f"employee:avatar:{employee_id}")
except Exception as e:
logger.warning(f"删除头像Redis缓存失败: employee_id={employee_id}, error={e}")
except Exception as e:
logger.warning(
f"同步员工头像失败(不阻塞登录): employee_id={employee_id}, error={e}"
)