# ============================================================================= # 企微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}" )