2026-06-14 16:49:18 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 企微IT智能服务台 — 员工 API
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 说明:提供员工相关的管理接口
|
|
|
|
|
|
# 接口列表:
|
|
|
|
|
|
# PUT /api/employees/{employee_id}/it-level — 更新员工IT技能等级
|
2026-07-05 17:03:36 +08:00
|
|
|
|
# POST /api/employees/{employee_id}/avatar/refresh — 手动刷新员工头像
|
2026-06-14 16:49:18 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
2026-06-14 16:49:18 +08:00
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
2026-07-05 17:03:36 +08:00
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
import redis.asyncio as aioredis
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
from app.utils.response import success_response
|
|
|
|
|
|
from app.schemas.employee import VALID_IT_LEVELS, VALID_LEVEL_SOURCES
|
2026-07-05 17:03:36 +08:00
|
|
|
|
from app.database import get_db
|
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
from app.models.employee import Employee
|
|
|
|
|
|
from app.dependencies import dep_redis
|
|
|
|
|
|
|
|
|
|
|
|
# 导入日志
|
|
|
|
|
|
import logging
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
|
|
|
|
|
# 创建路由器
|
|
|
|
|
|
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_id,value 为 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())
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 头像刷新 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())
|