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

163 lines
6.2 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
# =============================================================================
# 说明:为坐席端"邀请参与者"弹窗提供员工搜索和组织架构树接口
# 1. GET /agent/employees/search?keyword=xxx — 模糊搜索员工(姓名/工号)
# 2. GET /agent/org/tree — 获取组织架构树(部门→员工层级)
# 两个端点均需坐席认证(get_current_agent),复用 employee_directory 服务
#
# 路由说明:
# 后端路由不带 /api 前缀(nginx / Vite proxy 负责 strip /api),
# 实际对外 URL 为 /api/agent/employees/search 和 /api/agent/org/tree
# =============================================================================
import logging
from typing import Any, Dict, List, Optional
import redis.asyncio as aioredis
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.agents import get_current_agent
from app.database import get_db
from app.dependencies import dep_redis
from app.models.agent import Agent
from app.services.employee_directory import (
count_tree_employees,
get_org_directory,
get_org_tree_cached,
)
from app.utils.response import AppException, success_response
logger = logging.getLogger(__name__)
# 创建路由器(与 agents.py 一致:路由直接挂在根路径,不加额外 prefix)
router = APIRouter()
# --------------------------------------------------------------------------
# GET /agent/employees/search — 模糊搜索员工
# --------------------------------------------------------------------------
@router.get("/agent/employees/search")
async def search_employees(
keyword: str = Query("", description="搜索关键词(姓名或工号,模糊匹配)"),
current_agent: Agent = Depends(get_current_agent),
db: AsyncSession = Depends(get_db),
redis: Optional[aioredis.Redis] = Depends(dep_redis),
):
"""模糊搜索员工(按姓名或工号)。
复用 get_org_directory() 获取组织目录(优先企微全组织,降级本地 employees 表),
在内存中做大小写不敏感的模糊匹配。排除当前登录坐席自己。
Args:
keyword: 搜索关键词(为空时返回空列表,避免无意义全量返回)
current_agent: 当前坐席(通过 get_current_agent 认证依赖注入)
db: 数据库会话
redis: Redis 客户端(可选,用于企微目录缓存读取)
Returns:
Dict: 统一响应格式,data 为员工列表
[{ "id": "userid", "name": "姓名", "department": "部门" }, ...]
"""
# 去除首尾空格,空关键词直接返回空列表
kw = (keyword or "").strip()
if not kw:
return success_response(data=[])
try:
# 获取组织目录(含 30 分钟 Redis 缓存 + 本地降级)
directory, _ = await get_org_directory(db, redis)
kw_lower = kw.lower()
results: List[Dict[str, Any]] = []
for emp in directory:
# 排除当前坐席自己(避免邀请自己加入会话)
if emp.get("employee_id") == current_agent.user_id:
continue
name = emp.get("name", "") or ""
emp_id = emp.get("employee_id", "") or ""
# 模糊匹配:姓名 或 工号(employee_id)包含关键词(大小写不敏感)
if kw_lower in name.lower() or kw_lower in emp_id.lower():
results.append({
"id": emp_id,
"name": name,
"department": emp.get("department", ""),
})
logger.info(f"员工搜索: keyword='{kw}', 命中 {len(results)}")
return success_response(data=results)
except AppException:
# 业务异常直接抛出(如认证失败)
raise
except Exception as e:
logger.error(f"员工搜索异常: {e}", exc_info=True)
raise AppException(1005, f"搜索失败: {str(e)}")
# --------------------------------------------------------------------------
# GET /agent/org/tree — 获取组织架构树
# --------------------------------------------------------------------------
@router.get("/agent/org/tree")
async def get_org_tree(
current_agent: Agent = Depends(get_current_agent),
db: AsyncSession = Depends(get_db),
redis: Optional[aioredis.Redis] = Depends(dep_redis),
):
"""获取组织架构树(部门层级 + 每个部门下的员工列表)。
利用企微 department/list 返回的 parentid 字段构建真正的层级树,
不再按部门名扁平分组。部门ID加 ``dept_`` 前缀作为唯一 key
避免同名部门合并。员工可以出现在其所属的所有部门下。
树结构示例(多层级):
[
{
"id": "dept_1",
"label": "公司",
"dept_id": 1,
"parentid": 0,
"children": [
{
"id": "dept_2",
"label": "研发一部",
"dept_id": 2,
"parentid": 1,
"children": [
{"id": "zhangsan", "label": "张三", "isLeaf": true, "department": "研发一部"}
]
}
]
}
]
性能优化:
- 树构建结果独立缓存(key: wecom:org_tree:agentTTL 30 分钟)
- 缓存中包含所有员工,读取后过滤掉当前登录坐席自己
Args:
current_agent: 当前坐席
db: 数据库会话
redis: Redis 客户端
Returns:
Dict: 统一响应格式,data 为树节点列表
"""
try:
# 获取组织架构树(含独立缓存 + 排除当前坐席)
tree = await get_org_tree_cached(db, redis, "agent", current_agent.user_id)
total_employees = count_tree_employees(tree)
logger.info(f"组织架构树: {len(tree)} 个顶层节点, 共 {total_employees}")
return success_response(data=tree)
except AppException:
raise
except Exception as e:
logger.error(f"组织架构树获取异常: {e}", exc_info=True)
raise AppException(1005, f"获取组织架构树失败: {str(e)}")