Files
wecom_it_smart_desk/backend/app/api/employees.py
T

226 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微IT智能服务台 — 员工 API
# =============================================================================
# 说明:提供员工相关的管理接口
# 接口列表:
# PUT /api/employees/{employee_id}/it-level — 更新员工IT技能等级
# POST /api/employees/{employee_id}/avatar/refresh — 手动刷新员工头像
# =============================================================================
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
import redis.asyncio as aioredis
from app.utils.response import success_response
from app.schemas.employee import VALID_IT_LEVELS, VALID_LEVEL_SOURCES
from app.database import get_db
from app.config import settings
from app.models.employee import Employee
from app.dependencies import dep_redis
# 导入日志
import logging
logger = logging.getLogger(__name__)
# 创建路由器
router = APIRouter(prefix="/employees", tags=["员工管理"])
# --------------------------------------------------------------------------
# 请求 Schema
# --------------------------------------------------------------------------
class ItLevelUpdateRequest(BaseModel):
"""IT技能等级更新请求 Schema。"""
it_level: str = Field(..., description="IT技能等级: bronze/silver/gold/platinum/diamond/star/king")
source: str = Field(default="manual", description="等级来源: system/manual/assessment")
@field_validator("it_level")
@classmethod
def validate_it_level(cls, v: str) -> str:
"""校验IT等级值是否合法。"""
if v not in VALID_IT_LEVELS:
raise ValueError(f"无效的IT等级: {v},合法值为: {VALID_IT_LEVELS}")
return v
@field_validator("source")
@classmethod
def validate_source(cls, v: str) -> str:
"""校验等级来源值是否合法。"""
if v not in VALID_LEVEL_SOURCES:
raise ValueError(f"无效的等级来源: {v},合法值为: {VALID_LEVEL_SOURCES}")
return v
class ItLevelUpdateResponse(BaseModel):
"""IT技能等级更新响应 Schema。"""
employee_id: str
it_level: str
it_level_source: str
message: str
# --------------------------------------------------------------------------
# Mock 员工数据存储(IT 等级映射)
# --------------------------------------------------------------------------
# 简单的内存存储,key 为 employee_idvalue 为 it_level
MOCK_EMPLOYEE_IT_LEVELS: dict = {
"emp-001": "silver",
"emp-002": "gold",
"emp-003": "bronze",
"emp-004": "platinum",
"emp-005": "diamond",
"emp-006": "silver",
"emp-007": "star",
"emp-008": "king",
}
# --------------------------------------------------------------------------
# API 接口
# --------------------------------------------------------------------------
@router.put("/{employee_id}/it-level")
async def update_employee_it_level(
employee_id: str,
request: ItLevelUpdateRequest,
):
"""更新员工IT技能等级。
坐席可以手动调整员工的IT技能等级,等级来源标记为 manual。
更新后等级立即生效,并记录来源以便追溯。
Args:
employee_id: 员工ID
request: 等级更新请求
Returns:
更新结果
"""
# 更新内存中的等级
old_level = MOCK_EMPLOYEE_IT_LEVELS.get(employee_id, "silver")
MOCK_EMPLOYEE_IT_LEVELS[employee_id] = request.it_level
# 构造等级名称映射
level_names = {
"bronze": "青铜",
"silver": "白银",
"gold": "黄金",
"platinum": "铂金",
"diamond": "钻石",
"star": "星耀",
"king": "王者",
}
return success_response(data=ItLevelUpdateResponse(
employee_id=employee_id,
it_level=request.it_level,
it_level_source=request.source,
message=f"IT等级已从 {level_names.get(old_level, old_level)} 调整为 {level_names.get(request.it_level, request.it_level)}",
).model_dump())
# --------------------------------------------------------------------------
# 头像刷新 API
# --------------------------------------------------------------------------
class AvatarRefreshResponse(BaseModel):
"""头像刷新响应 Schema。"""
employee_id: str
avatar: str
message: str
async def get_redis() -> aioredis.Redis:
"""获取Redis客户端依赖"""
redis = await dep_redis()
if redis is None:
raise HTTPException(status_code=500, detail="Redis连接不可用")
return redis
@router.post("/{employee_id}/avatar/refresh", response_model=dict)
async def refresh_employee_avatar(
employee_id: str,
db: AsyncSession = Depends(get_db),
redis: aioredis.Redis = Depends(get_redis),
):
"""手动刷新员工头像。
调用企微通讯录API获取最新头像URL,更新数据库并刷新Redis缓存。
支持手动触发头像更新,适用于头像URL过期或需要立即更新的场景。
Args:
employee_id: 员工ID
Returns:
更新后的头像URL
"""
from datetime import datetime
from app.services.session_service import SessionService
# 1. 查找员工记录
result = await db.execute(
select(Employee).where(
Employee.employee_id == employee_id,
Employee.corp_id == settings.wecom_corp_id
)
)
employee = result.scalars().first()
if not employee:
raise HTTPException(status_code=404, detail=f"员工不存在: {employee_id}")
# 2. 使用 SessionService 从企微API获取最新头像
session_service = SessionService(db, redis_client=redis)
new_avatar = ""
try:
# 调用企微API获取最新头像
from app.services.wecom_service import WeComService
wecom_service = WeComService()
user_info = await wecom_service.get_user_info(employee_id)
new_avatar = user_info.get("avatar", "")
logger.info(f"企微API返回头像: employee_id={employee_id}, avatar={'有值(' + str(len(new_avatar)) + '字符)' if new_avatar else ''}")
# 3. 更新数据库
employee.avatar = new_avatar
employee.avatar_updated_at = datetime.utcnow()
await db.commit()
# 4. 刷新Redis缓存
cache_key = f"employee:avatar:{employee_id}"
if redis:
try:
if new_avatar:
await redis.setex(cache_key, SessionService.AVATAR_CACHE_TTL, new_avatar)
else:
# 如果头像为空,删除缓存
await redis.delete(cache_key)
except Exception as e:
logger.warning(f"刷新Redis头像缓存失败: employee_id={employee_id}, error={e}")
return success_response(data=AvatarRefreshResponse(
employee_id=employee_id,
avatar=new_avatar,
message="头像刷新成功" if new_avatar else "企微API未返回头像,已使用原头像",
).model_dump())
except Exception as e:
logger.error(f"刷新头像失败: employee_id={employee_id}, error={e}")
# 返回原头像,不阻塞流程
return success_response(data=AvatarRefreshResponse(
employee_id=employee_id,
avatar=employee.avatar,
message=f"头像刷新失败,使用原头像: {str(e)}",
).model_dump())