163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
# =============================================================================
|
||
# 企微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:agent,TTL 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)}")
|