Files
wecom_it_smart_desk/backend/app/services/avatar_service.py
T

92 lines
3.6 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 ""
return url.split("?", 1)[0]
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}"
)