Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
部门模块
|
||||
"""
|
||||
from core.dept.model import Dept
|
||||
from core.dept.service import DeptService
|
||||
|
||||
__all__ = ["Dept", "DeptService"]
|
||||
@@ -0,0 +1,431 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Dept API - 部门管理接口
|
||||
提供部门的 CRUD 操作和树形结构查询
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from core.dept.schema import (
|
||||
DeptCreate, DeptUpdate, DeptResponse, DeptTreeNode, DeptSimple,
|
||||
DeptBatchDeleteIn, DeptBatchDeleteOut, DeptBatchUpdateStatusIn, DeptBatchUpdateStatusOut,
|
||||
DeptPathOut, DeptUserSchema, DeptUserIn, DeptStatsResponse, DeptMoveRequest, DeptSearchRequest
|
||||
)
|
||||
from core.dept.service import DeptService
|
||||
|
||||
router = APIRouter(prefix="/dept", tags=["部门管理"])
|
||||
|
||||
|
||||
@router.post("", response_model=DeptResponse, summary="创建部门")
|
||||
async def create_dept(data: DeptCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建部门"""
|
||||
# 编码唯一性校验
|
||||
if data.code:
|
||||
if not await DeptService.check_unique(db, field="code", value=data.code):
|
||||
raise HTTPException(status_code=400, detail="部门编码已存在")
|
||||
|
||||
# 检查父部门是否存在
|
||||
if data.parent_id:
|
||||
parent = await DeptService.get_by_id(db, data.parent_id)
|
||||
if not parent:
|
||||
raise HTTPException(status_code=400, detail="父部门不存在")
|
||||
|
||||
dept = await DeptService.create(db=db, data=data)
|
||||
return _build_dept_response(dept)
|
||||
|
||||
|
||||
@router.get("/tree", response_model=List[DeptTreeNode], summary="获取部门树")
|
||||
async def get_dept_tree(
|
||||
parent_id: Optional[str] = Query(None, alias="parentId", description="父部门ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取部门树形结构"""
|
||||
return await DeptService.get_tree(db, parent_id)
|
||||
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[DeptResponse], summary="获取部门列表")
|
||||
async def get_dept_list(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
name: Optional[str] = Query(None, description="部门名称"),
|
||||
code: Optional[str] = Query(None, description="部门编码"),
|
||||
status: Optional[bool] = Query(None, description="部门状态"),
|
||||
dept_type: Optional[str] = Query(None, alias="deptType", description="部门类型"),
|
||||
parent_id: Optional[str] = Query(None, alias="parentId", description="父部门ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取部门列表(分页)"""
|
||||
from core.dept.model import Dept
|
||||
|
||||
filters = []
|
||||
if name:
|
||||
filters.append(Dept.name.ilike(f"%{name}%"))
|
||||
if code:
|
||||
filters.append(Dept.code.ilike(f"%{code}%"))
|
||||
if status is not None:
|
||||
filters.append(Dept.status == status)
|
||||
if dept_type:
|
||||
filters.append(Dept.dept_type == dept_type)
|
||||
if parent_id:
|
||||
filters.append(Dept.parent_id == parent_id)
|
||||
|
||||
items, total = await DeptService.get_list(db, page=page, page_size=page_size, filters=filters)
|
||||
return PaginatedResponse(
|
||||
items=[_build_dept_response(item) for item in items],
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/simple", response_model=List[DeptSimple], summary="获取部门简单列表")
|
||||
async def get_dept_simple_list(
|
||||
status: Optional[bool] = Query(None, description="部门状态"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取部门简单列表(用于选择器)"""
|
||||
from core.dept.model import Dept
|
||||
|
||||
filters = []
|
||||
if status is not None:
|
||||
filters.append(Dept.status == status)
|
||||
|
||||
items, _ = await DeptService.get_list(db, page=1, page_size=1000, filters=filters)
|
||||
return [DeptSimple.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@router.get("/export/excel", summary="导出部门Excel")
|
||||
async def export_dept_excel(db: AsyncSession = Depends(get_db)):
|
||||
"""导出部门到Excel"""
|
||||
output = await DeptService.export_to_excel(db)
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=depts.xlsx"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/import/template", summary="下载部门导入模板")
|
||||
async def download_dept_template():
|
||||
"""下载部门导入模板"""
|
||||
output = DeptService.get_import_template()
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=dept_template.xlsx"}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import/excel", response_model=ResponseModel, summary="导入部门Excel")
|
||||
async def import_dept_excel(
|
||||
file: UploadFile = File(..., description="Excel文件(.xlsx)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""从Excel导入部门"""
|
||||
if not file.filename.endswith(".xlsx"):
|
||||
raise HTTPException(status_code=400, detail="只支持.xlsx格式")
|
||||
|
||||
content = await file.read()
|
||||
success, fail = await DeptService.import_from_excel(db, content)
|
||||
return ResponseModel(message=f"成功{success}条,失败{fail}条", data={"success": success, "fail": fail})
|
||||
|
||||
|
||||
@router.get("/check/unique", response_model=ResponseModel, summary="检查部门唯一性")
|
||||
async def check_dept_unique(
|
||||
field: str = Query(..., description="字段名"),
|
||||
value: str = Query(..., description="字段值"),
|
||||
exclude_id: Optional[str] = Query(None, alias="excludeId", description="排除ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""检查部门字段唯一性"""
|
||||
allowed_fields = ["code", "name"]
|
||||
if field not in allowed_fields:
|
||||
raise HTTPException(status_code=400, detail=f"不支持检查字段: {field}")
|
||||
|
||||
is_unique = await DeptService.check_unique(db, field=field, value=value, exclude_id=exclude_id)
|
||||
return ResponseModel(message="可用" if is_unique else "已存在", data={"unique": is_unique})
|
||||
|
||||
|
||||
@router.post("/batch/delete", response_model=DeptBatchDeleteOut, summary="批量删除部门")
|
||||
async def batch_delete_depts(
|
||||
data: DeptBatchDeleteIn,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量删除部门"""
|
||||
count, failed_ids = await DeptService.batch_delete(db, data.ids, hard=hard)
|
||||
return DeptBatchDeleteOut(count=count, failed_ids=failed_ids)
|
||||
|
||||
|
||||
@router.post("/batch/status", response_model=DeptBatchUpdateStatusOut, summary="批量更新部门状态")
|
||||
async def batch_update_dept_status(
|
||||
data: DeptBatchUpdateStatusIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量更新部门状态"""
|
||||
count = await DeptService.batch_update_status(db, data.ids, data.status)
|
||||
return DeptBatchUpdateStatusOut(count=count)
|
||||
|
||||
|
||||
@router.get("/{dept_id}/children", response_model=List[DeptResponse], summary="获取子部门")
|
||||
async def get_dept_children(
|
||||
dept_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取直接子部门列表"""
|
||||
children = await DeptService.get_children(db, dept_id)
|
||||
return [_build_dept_response(item) for item in children]
|
||||
|
||||
|
||||
@router.get("/{dept_id}/descendants", response_model=List[DeptResponse], summary="获取所有后代部门")
|
||||
async def get_dept_descendants(
|
||||
dept_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取所有后代部门"""
|
||||
descendants = await DeptService.get_descendants(db, dept_id)
|
||||
return [_build_dept_response(item) for item in descendants]
|
||||
|
||||
|
||||
@router.get("/{dept_id}/ancestors", response_model=List[DeptResponse], summary="获取所有祖先部门")
|
||||
async def get_dept_ancestors(
|
||||
dept_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取所有祖先部门"""
|
||||
ancestors = await DeptService.get_ancestors(db, dept_id)
|
||||
return [_build_dept_response(item) for item in ancestors]
|
||||
|
||||
|
||||
@router.get("/{dept_id}", response_model=DeptResponse, summary="获取部门详情")
|
||||
async def get_dept_by_id(dept_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取部门详情"""
|
||||
dept = await DeptService.get_by_id(db, dept_id)
|
||||
if dept is None:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
return _build_dept_response(dept)
|
||||
|
||||
|
||||
@router.put("/{dept_id}", response_model=DeptResponse, summary="更新部门")
|
||||
async def update_dept(dept_id: str, data: DeptUpdate, db: AsyncSession = Depends(get_db)):
|
||||
"""更新部门"""
|
||||
# 编码唯一性校验
|
||||
if data.code:
|
||||
if not await DeptService.check_unique(db, field="code", value=data.code, exclude_id=dept_id):
|
||||
raise HTTPException(status_code=400, detail="部门编码已存在")
|
||||
|
||||
# 检查父部门是否存在
|
||||
if data.parent_id:
|
||||
if data.parent_id == dept_id:
|
||||
raise HTTPException(status_code=400, detail="不能将自己设为父部门")
|
||||
parent = await DeptService.get_by_id(db, data.parent_id)
|
||||
if not parent:
|
||||
raise HTTPException(status_code=400, detail="父部门不存在")
|
||||
|
||||
dept = await DeptService.update(db, record_id=dept_id, data=data)
|
||||
if dept is None:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
return _build_dept_response(dept)
|
||||
|
||||
|
||||
@router.delete("/{dept_id}", response_model=ResponseModel, summary="删除部门")
|
||||
async def delete_dept(
|
||||
dept_id: str,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除部门"""
|
||||
can_del, reason = await DeptService.can_delete(db, dept_id)
|
||||
if not can_del:
|
||||
raise HTTPException(status_code=400, detail=reason)
|
||||
|
||||
success = await DeptService.delete(db, record_id=dept_id, hard=hard)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
@router.get("/by/parent/{parent_id}", response_model=List[dict], summary="根据父部门ID获取子部门")
|
||||
async def get_dept_by_parent(
|
||||
parent_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
根据父部门ID获取直接子部门
|
||||
|
||||
- parent_id="null" 获取根部门
|
||||
"""
|
||||
if parent_id == "null":
|
||||
parent_id = None
|
||||
|
||||
return await DeptService.get_by_parent(db, parent_id)
|
||||
|
||||
|
||||
@router.post("/search", response_model=List[dict], summary="搜索部门")
|
||||
async def search_dept(
|
||||
data: DeptSearchRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
搜索部门(模糊匹配部门名称或编码)
|
||||
|
||||
返回匹配部门及其完整的层级路径
|
||||
"""
|
||||
return await DeptService.search(db, data.keyword)
|
||||
|
||||
|
||||
@router.get("/by/ids", response_model=List[dict], summary="根据ID列表获取部门")
|
||||
async def get_depts_by_ids(
|
||||
ids: str = Query(..., description="部门ID列表,逗号分隔"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
根据部门ID列表批量获取部门信息(包含完整的层级路径)
|
||||
"""
|
||||
dept_ids = [id.strip() for id in ids.split(',') if id.strip()]
|
||||
return await DeptService.get_by_ids(db, dept_ids)
|
||||
|
||||
|
||||
@router.get("/path/{dept_id}", response_model=DeptPathOut, summary="获取部门路径")
|
||||
async def get_dept_path(
|
||||
dept_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取部门的完整路径(从根到当前部门)"""
|
||||
dept = await DeptService.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
|
||||
# 获取所有祖先
|
||||
ancestors = await DeptService.get_ancestors(db, dept_id)
|
||||
|
||||
path = []
|
||||
for ancestor in reversed(ancestors):
|
||||
path.append(DeptSimple(
|
||||
id=ancestor.id,
|
||||
name=ancestor.name,
|
||||
code=ancestor.code,
|
||||
parent_id=ancestor.parent_id,
|
||||
level=ancestor.level,
|
||||
status=ancestor.status,
|
||||
))
|
||||
|
||||
# 添加当前部门
|
||||
path.append(DeptSimple(
|
||||
id=dept.id,
|
||||
name=dept.name,
|
||||
code=dept.code,
|
||||
parent_id=dept.parent_id,
|
||||
level=dept.level,
|
||||
status=dept.status,
|
||||
))
|
||||
|
||||
return DeptPathOut(
|
||||
dept_id=dept.id,
|
||||
dept_name=dept.name,
|
||||
path=path
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=DeptStatsResponse, summary="获取部门统计信息")
|
||||
async def get_dept_stats(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取部门统计信息"""
|
||||
stats = await DeptService.get_stats(db)
|
||||
return DeptStatsResponse(**stats)
|
||||
|
||||
|
||||
@router.post("/move", response_model=ResponseModel, summary="移动部门")
|
||||
async def move_dept(
|
||||
data: DeptMoveRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""移动部门到新的父部门下"""
|
||||
success, message = await DeptService.move(db, data.dept_id, data.new_parent_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return ResponseModel(message=message)
|
||||
|
||||
|
||||
@router.get("/users/{dept_id}", response_model=List[DeptUserSchema], summary="获取部门用户列表")
|
||||
async def get_dept_users(
|
||||
dept_id: str,
|
||||
include_children: bool = Query(False, alias="includeChildren", description="是否包含子部门用户"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取部门下的用户列表"""
|
||||
dept = await DeptService.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
|
||||
users = await DeptService.get_dept_users(db, dept_id, include_children)
|
||||
return [DeptUserSchema.model_validate(user) for user in users]
|
||||
|
||||
|
||||
@router.post("/users/{dept_id}", response_model=ResponseModel, summary="为部门添加用户")
|
||||
async def add_user_to_dept(
|
||||
dept_id: str,
|
||||
data: DeptUserIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""将用户添加到部门"""
|
||||
dept = await DeptService.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
|
||||
if not data.user_ids:
|
||||
raise HTTPException(status_code=400, detail="用户ID列表不能为空")
|
||||
|
||||
added_count = await DeptService.add_users_to_dept(db, dept_id, data.user_ids)
|
||||
return ResponseModel(message=f"成功添加 {added_count} 个用户")
|
||||
|
||||
|
||||
@router.delete("/users/{dept_id}", response_model=ResponseModel, summary="从部门中移除用户")
|
||||
async def remove_user_from_dept(
|
||||
dept_id: str,
|
||||
data: DeptUserIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""从部门中移除用户(支持批量删除)"""
|
||||
dept = await DeptService.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
raise HTTPException(status_code=404, detail="部门不存在")
|
||||
|
||||
# 优先使用 user_ids(批量),如果没有则使用 user_id(单个)
|
||||
user_ids_to_remove = data.user_ids if data.user_ids else ([data.user_id] if data.user_id else [])
|
||||
|
||||
if not user_ids_to_remove:
|
||||
raise HTTPException(status_code=400, detail="用户ID不能为空")
|
||||
|
||||
removed_count = await DeptService.remove_users_from_dept(db, dept_id, user_ids_to_remove)
|
||||
return ResponseModel(message=f"成功移除 {removed_count} 个用户")
|
||||
|
||||
|
||||
def _build_dept_response(dept) -> DeptResponse:
|
||||
"""构建部门响应"""
|
||||
return DeptResponse(
|
||||
id=dept.id,
|
||||
name=dept.name,
|
||||
code=dept.code,
|
||||
dept_type=dept.dept_type,
|
||||
dept_type_display=dept.get_dept_type_display(),
|
||||
phone=dept.phone,
|
||||
email=dept.email,
|
||||
status=dept.status,
|
||||
description=dept.description,
|
||||
parent_id=dept.parent_id,
|
||||
lead_id=dept.lead_id,
|
||||
lead_name=dept.lead.name if dept.lead else None,
|
||||
level=dept.level,
|
||||
path=dept.path,
|
||||
sort=dept.sort,
|
||||
is_deleted=dept.is_deleted,
|
||||
sys_create_datetime=dept.sys_create_datetime,
|
||||
sys_update_datetime=dept.sys_update_datetime,
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Dept Model - 部门模型
|
||||
用于管理组织架构中的部门信息
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Boolean, Integer
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class Dept(BaseModel):
|
||||
"""
|
||||
部门模型 - 用于组织架构管理
|
||||
|
||||
字段说明:
|
||||
- name: 部门名称
|
||||
- code: 部门编码(唯一)
|
||||
- dept_type: 部门类型(company/department/team/other)
|
||||
- phone: 部门电话
|
||||
- email: 部门邮箱
|
||||
- status: 部门状态(启用/禁用)
|
||||
- description: 部门描述
|
||||
- parent_id: 父部门ID
|
||||
- lead_id: 部门领导ID
|
||||
- level: 部门层级(0为顶层)
|
||||
- path: 部门路径(便于查询,格式:/id1/id2/)
|
||||
"""
|
||||
__tablename__ = "core_dept"
|
||||
|
||||
# 部门名称
|
||||
name = Column(String(64), nullable=False, index=True, comment="部门名称")
|
||||
|
||||
# 部门编码
|
||||
code = Column(String(32), unique=True, nullable=True, index=True, comment="部门编码")
|
||||
|
||||
# 部门类型:company-公司, department-部门, team-小组, other-其他
|
||||
dept_type = Column(String(20), default="department", index=True, comment="部门类型")
|
||||
|
||||
# 部门电话
|
||||
phone = Column(String(20), nullable=True, comment="部门电话")
|
||||
|
||||
# 部门邮箱
|
||||
email = Column(String(64), nullable=True, comment="部门邮箱")
|
||||
|
||||
# 部门状态
|
||||
status = Column(Boolean, default=True, index=True, comment="部门状态")
|
||||
|
||||
# 部门描述
|
||||
description = Column(Text, nullable=True, comment="部门描述")
|
||||
|
||||
# 父部门ID(逻辑外键,不创建数据库约束)
|
||||
parent_id = Column(String(21), nullable=True, index=True, comment="父部门ID")
|
||||
|
||||
# 部门领导ID(逻辑外键,不创建数据库约束)
|
||||
lead_id = Column(String(21), nullable=True, comment="部门领导ID")
|
||||
|
||||
# 部门层级(0为顶层)
|
||||
level = Column(Integer, default=0, index=True, comment="部门层级")
|
||||
|
||||
# 部门路径(便于查询,格式:/id1/id2/)
|
||||
path = Column(String(500), nullable=True, index=True, comment="部门路径")
|
||||
|
||||
# 钉钉部门ID(用于组织架构同步映射)
|
||||
dingtalk_dept_id = Column(String(64), unique=True, nullable=True, index=True, comment="钉钉部门ID")
|
||||
|
||||
# 企业微信部门ID(用于组织架构同步映射)
|
||||
wecom_dept_id = Column(String(64), unique=True, nullable=True, index=True, comment="企业微信部门ID")
|
||||
|
||||
# 飞书部门ID(用于组织架构同步映射,open_department_id 格式)
|
||||
feishu_dept_id = Column(String(128), unique=True, nullable=True, index=True, comment="飞书部门ID")
|
||||
|
||||
# 关系定义(使用primaryjoin指定逻辑关联,lazy='selectin'支持异步加载)
|
||||
parent = relationship("Dept", remote_side="Dept.id", backref="children", foreign_keys="Dept.parent_id", primaryjoin="Dept.parent_id == Dept.id", lazy="selectin")
|
||||
lead = relationship("User", foreign_keys="Dept.lead_id", primaryjoin="Dept.lead_id == User.id", backref="leading_depts", lazy="selectin")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Dept {self.name} ({self.code or 'N/A'})>"
|
||||
|
||||
def get_dept_type_display(self) -> str:
|
||||
"""获取部门类型的显示名称"""
|
||||
type_map = {
|
||||
"company": "公司",
|
||||
"department": "部门",
|
||||
"team": "小组",
|
||||
"other": "其他",
|
||||
}
|
||||
return type_map.get(self.dept_type, "未知")
|
||||
|
||||
def get_full_name(self) -> str:
|
||||
"""获取部门全名(包含父部门)"""
|
||||
if self.parent:
|
||||
return f"{self.parent.get_full_name()} / {self.name}"
|
||||
return self.name
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Dept Schema - 部门数据验证模式
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class DeptBase(BaseModel):
|
||||
"""部门基础Schema"""
|
||||
name: str = Field(..., min_length=1, max_length=64, description="部门名称")
|
||||
code: Optional[str] = Field(None, max_length=32, description="部门编码")
|
||||
dept_type: str = Field(default="department", description="部门类型")
|
||||
phone: Optional[str] = Field(None, max_length=20, description="部门电话")
|
||||
email: Optional[str] = Field(None, max_length=64, description="部门邮箱")
|
||||
status: bool = Field(default=True, description="部门状态")
|
||||
description: Optional[str] = Field(None, description="部门描述")
|
||||
parent_id: Optional[str] = Field(None, description="父部门ID")
|
||||
lead_id: Optional[str] = Field(None, description="部门领导ID")
|
||||
sort: int = Field(default=0, description="排序")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v):
|
||||
"""验证部门编码格式"""
|
||||
if v and not v.replace("-", "").replace("_", "").isalnum():
|
||||
raise ValueError("部门编码只能包含字母、数字、下划线和横线")
|
||||
return v
|
||||
|
||||
@field_validator("dept_type")
|
||||
@classmethod
|
||||
def validate_dept_type(cls, v):
|
||||
"""验证部门类型"""
|
||||
valid_types = ["company", "department", "team", "other"]
|
||||
if v not in valid_types:
|
||||
raise ValueError(f"部门类型必须是以下之一: {', '.join(valid_types)}")
|
||||
return v
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def validate_phone(cls, v):
|
||||
"""验证电话格式"""
|
||||
if v and not all(c.isdigit() or c in "-+() " for c in v):
|
||||
raise ValueError("电话号码格式不正确")
|
||||
return v
|
||||
|
||||
|
||||
class DeptCreate(DeptBase):
|
||||
"""部门创建Schema"""
|
||||
pass
|
||||
|
||||
|
||||
class DeptUpdate(BaseModel):
|
||||
"""部门更新Schema - 所有字段可选"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=64, description="部门名称")
|
||||
code: Optional[str] = Field(None, max_length=32, description="部门编码")
|
||||
dept_type: Optional[str] = Field(None, description="部门类型")
|
||||
phone: Optional[str] = Field(None, max_length=20, description="部门电话")
|
||||
email: Optional[str] = Field(None, max_length=64, description="部门邮箱")
|
||||
status: Optional[bool] = Field(None, description="部门状态")
|
||||
description: Optional[str] = Field(None, description="部门描述")
|
||||
parent_id: Optional[str] = Field(None, description="父部门ID")
|
||||
lead_id: Optional[str] = Field(None, description="部门领导ID")
|
||||
sort: Optional[int] = Field(None, description="排序")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v):
|
||||
"""验证部门编码格式"""
|
||||
if v is not None and v and not v.replace("-", "").replace("_", "").isalnum():
|
||||
raise ValueError("部门编码只能包含字母、数字、下划线和横线")
|
||||
return v
|
||||
|
||||
@field_validator("dept_type")
|
||||
@classmethod
|
||||
def validate_dept_type(cls, v):
|
||||
"""验证部门类型"""
|
||||
if v is not None:
|
||||
valid_types = ["company", "department", "team", "other"]
|
||||
if v not in valid_types:
|
||||
raise ValueError(f"部门类型必须是以下之一: {', '.join(valid_types)}")
|
||||
return v
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def validate_phone(cls, v):
|
||||
"""验证电话格式"""
|
||||
if v is not None and v and not all(c.isdigit() or c in "-+() " for c in v):
|
||||
raise ValueError("电话号码格式不正确")
|
||||
return v
|
||||
|
||||
|
||||
class DeptResponse(BaseModel):
|
||||
"""部门响应Schema"""
|
||||
id: str
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
dept_type: str
|
||||
dept_type_display: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
status: bool
|
||||
description: Optional[str] = None
|
||||
parent_id: Optional[str] = None
|
||||
lead_id: Optional[str] = None
|
||||
lead_name: Optional[str] = None
|
||||
level: int = 0
|
||||
path: Optional[str] = None
|
||||
sort: int = 0
|
||||
is_deleted: bool = False
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DeptTreeNode(BaseModel):
|
||||
"""部门树形节点"""
|
||||
id: str
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
parent_id: Optional[str] = None
|
||||
dept_type: str
|
||||
status: bool
|
||||
level: int
|
||||
sort: int = 0
|
||||
children: List["DeptTreeNode"] = []
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DeptSimple(BaseModel):
|
||||
"""部门简单输出(用于选择器)"""
|
||||
id: str
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
parent_id: Optional[str] = None
|
||||
level: int
|
||||
status: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DeptBatchDeleteIn(BaseModel):
|
||||
"""批量删除部门输入"""
|
||||
ids: List[str] = Field(..., description="要删除的部门ID列表")
|
||||
|
||||
|
||||
class DeptBatchDeleteOut(BaseModel):
|
||||
"""批量删除部门输出"""
|
||||
count: int = Field(..., description="删除的记录数")
|
||||
failed_ids: List[str] = Field(default=[], description="删除失败的ID列表")
|
||||
|
||||
|
||||
class DeptBatchUpdateStatusIn(BaseModel):
|
||||
"""批量更新部门状态输入"""
|
||||
ids: List[str] = Field(..., description="部门ID列表")
|
||||
status: bool = Field(..., description="部门状态")
|
||||
|
||||
|
||||
class DeptBatchUpdateStatusOut(BaseModel):
|
||||
"""批量更新部门状态输出"""
|
||||
count: int = Field(..., description="更新的记录数")
|
||||
|
||||
|
||||
class DeptPathOut(BaseModel):
|
||||
"""部门路径响应"""
|
||||
dept_id: str = Field(..., description="部门ID")
|
||||
dept_name: str = Field(..., description="部门名称")
|
||||
path: List[DeptSimple] = Field(..., description="从根到当前部门的路径")
|
||||
|
||||
|
||||
class DeptUserSchema(BaseModel):
|
||||
"""部门用户Schema"""
|
||||
id: str
|
||||
username: str
|
||||
name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
mobile: Optional[str] = None
|
||||
dept_id: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DeptUserIn(BaseModel):
|
||||
"""部门用户输入"""
|
||||
user_id: Optional[str] = Field(None, description="单个用户ID")
|
||||
user_ids: List[str] = Field(default=[], description="用户ID列表")
|
||||
|
||||
|
||||
class DeptStatsResponse(BaseModel):
|
||||
"""部门统计响应"""
|
||||
total_count: int = Field(..., description="总部门数")
|
||||
active_count: int = Field(..., description="启用部门数")
|
||||
inactive_count: int = Field(..., description="禁用部门数")
|
||||
root_count: int = Field(..., description="根部门数")
|
||||
type_stats: Dict[str, int] = Field(..., description="按类型统计")
|
||||
max_level: int = Field(..., description="最大层级")
|
||||
|
||||
|
||||
class DeptMoveRequest(BaseModel):
|
||||
"""移动部门请求"""
|
||||
dept_id: str = Field(..., description="要移动的部门ID")
|
||||
new_parent_id: Optional[str] = Field(None, description="新父部门ID,为空表示移动到根节点")
|
||||
|
||||
|
||||
class DeptSearchRequest(BaseModel):
|
||||
"""搜索部门请求"""
|
||||
keyword: str = Field(..., description="搜索关键词")
|
||||
@@ -0,0 +1,712 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Dept Service - 部门服务层
|
||||
"""
|
||||
from io import BytesIO
|
||||
from typing import Tuple, Dict, Any, Optional, List
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.base_service import BaseService
|
||||
from core.dept.model import Dept
|
||||
from core.dept.schema import DeptCreate, DeptUpdate, DeptTreeNode
|
||||
|
||||
|
||||
class DeptService(BaseService[Dept, DeptCreate, DeptUpdate]):
|
||||
"""
|
||||
部门服务层
|
||||
继承BaseService,自动获得增删改查功能
|
||||
"""
|
||||
|
||||
model = Dept
|
||||
|
||||
# Excel导入导出配置
|
||||
excel_columns = {
|
||||
"name": "部门名称",
|
||||
"code": "部门编码",
|
||||
"dept_type": "部门类型",
|
||||
"phone": "部门电话",
|
||||
"email": "部门邮箱",
|
||||
"status": "状态",
|
||||
"description": "描述",
|
||||
}
|
||||
excel_sheet_name = "部门列表"
|
||||
|
||||
@classmethod
|
||||
def _export_converter(cls, item: Any) -> Dict[str, Any]:
|
||||
"""导出数据转换器"""
|
||||
return {
|
||||
"name": item.name,
|
||||
"code": item.code or "",
|
||||
"dept_type": item.get_dept_type_display(),
|
||||
"phone": item.phone or "",
|
||||
"email": item.email or "",
|
||||
"status": "启用" if item.status else "禁用",
|
||||
"description": item.description or "",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _import_processor(cls, row: Dict[str, Any]) -> Optional[Dept]:
|
||||
"""导入数据处理器"""
|
||||
name = row.get("name")
|
||||
if not name:
|
||||
return None
|
||||
|
||||
# 部门类型映射
|
||||
type_map = {"公司": "company", "部门": "department", "小组": "team", "其他": "other"}
|
||||
dept_type_str = row.get("dept_type", "部门")
|
||||
dept_type = type_map.get(dept_type_str, "department")
|
||||
|
||||
status_str = row.get("status", "启用")
|
||||
status = status_str in ("启用", "true", "True", "1", True)
|
||||
|
||||
return Dept(
|
||||
name=str(name),
|
||||
code=str(row.get("code") or "") or None,
|
||||
dept_type=dept_type,
|
||||
phone=str(row.get("phone") or "") or None,
|
||||
email=str(row.get("email") or "") or None,
|
||||
status=status,
|
||||
description=str(row.get("description") or "") or None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def export_to_excel(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
data_converter: Any = None
|
||||
) -> BytesIO:
|
||||
"""导出到Excel"""
|
||||
return await super().export_to_excel(db, cls._export_converter)
|
||||
|
||||
@classmethod
|
||||
async def import_from_excel(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
file_content: bytes,
|
||||
row_processor: Any = None
|
||||
) -> Tuple[int, int]:
|
||||
"""从Excel导入"""
|
||||
return await super().import_from_excel(db, file_content, cls._import_processor)
|
||||
|
||||
@classmethod
|
||||
async def create(cls, db: AsyncSession, data: DeptCreate) -> Dept:
|
||||
"""
|
||||
创建部门,自动计算层级和路径
|
||||
"""
|
||||
dept_data = data.model_dump()
|
||||
|
||||
# 计算层级和路径
|
||||
parent_id = dept_data.get("parent_id")
|
||||
if parent_id:
|
||||
parent = await cls.get_by_id(db, parent_id)
|
||||
if parent:
|
||||
dept_data["level"] = parent.level + 1
|
||||
dept_data["path"] = f"{parent.path or '/'}{parent.id}/"
|
||||
else:
|
||||
dept_data["level"] = 0
|
||||
dept_data["path"] = "/"
|
||||
else:
|
||||
dept_data["level"] = 0
|
||||
dept_data["path"] = "/"
|
||||
|
||||
db_obj = Dept(**dept_data)
|
||||
db.add(db_obj)
|
||||
await db.commit()
|
||||
await db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
@classmethod
|
||||
async def update(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
record_id: str,
|
||||
data: DeptUpdate
|
||||
) -> Optional[Dept]:
|
||||
"""
|
||||
更新部门,如果父部门变化则重新计算层级和路径
|
||||
"""
|
||||
db_obj = await cls.get_by_id(db, record_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# 如果父部门变化,重新计算层级和路径
|
||||
if "parent_id" in update_data:
|
||||
parent_id = update_data["parent_id"]
|
||||
if parent_id:
|
||||
parent = await cls.get_by_id(db, parent_id)
|
||||
if parent:
|
||||
update_data["level"] = parent.level + 1
|
||||
update_data["path"] = f"{parent.path or '/'}{parent.id}/"
|
||||
else:
|
||||
update_data["level"] = 0
|
||||
update_data["path"] = "/"
|
||||
else:
|
||||
update_data["level"] = 0
|
||||
update_data["path"] = "/"
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(db_obj, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
@classmethod
|
||||
async def get_tree(cls, db: AsyncSession, parent_id: Optional[str] = None) -> List[DeptTreeNode]:
|
||||
"""
|
||||
获取部门树形结构
|
||||
|
||||
:param db: 数据库会话
|
||||
:param parent_id: 父部门ID,None表示获取所有顶级部门
|
||||
:return: 部门树形列表
|
||||
"""
|
||||
# 获取所有未删除的部门
|
||||
result = await db.execute(
|
||||
select(Dept)
|
||||
.where(Dept.is_deleted == False) # noqa: E712
|
||||
.order_by(Dept.sort.desc(), Dept.sys_create_datetime)
|
||||
)
|
||||
all_depts = result.scalars().all()
|
||||
|
||||
# 构建部门字典
|
||||
dept_dict = {dept.id: dept for dept in all_depts}
|
||||
|
||||
# 构建树形结构
|
||||
def build_tree(parent_id: Optional[str]) -> List[DeptTreeNode]:
|
||||
children = []
|
||||
for dept in all_depts:
|
||||
if dept.parent_id == parent_id:
|
||||
node = DeptTreeNode(
|
||||
id=dept.id,
|
||||
name=dept.name,
|
||||
code=dept.code,
|
||||
parent_id=dept.parent_id,
|
||||
dept_type=dept.dept_type,
|
||||
status=dept.status,
|
||||
level=dept.level,
|
||||
sort=dept.sort,
|
||||
children=build_tree(dept.id)
|
||||
)
|
||||
children.append(node)
|
||||
return children
|
||||
|
||||
return build_tree(parent_id)
|
||||
|
||||
@classmethod
|
||||
async def get_children(cls, db: AsyncSession, parent_id: str) -> List[Dept]:
|
||||
"""
|
||||
获取直接子部门列表
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Dept)
|
||||
.where(
|
||||
Dept.parent_id == parent_id,
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
.order_by(Dept.sort.desc(), Dept.sys_create_datetime)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_descendants(cls, db: AsyncSession, dept_id: str) -> List[Dept]:
|
||||
"""
|
||||
获取所有后代部门(通过path字段查询)
|
||||
"""
|
||||
dept = await cls.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
return []
|
||||
|
||||
# 使用path字段进行模糊查询
|
||||
search_path = f"{dept.path or '/'}{dept.id}/"
|
||||
result = await db.execute(
|
||||
select(Dept)
|
||||
.where(
|
||||
Dept.path.like(f"{search_path}%"),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
.order_by(Dept.level, Dept.sort.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_ancestors(cls, db: AsyncSession, dept_id: str) -> List[Dept]:
|
||||
"""
|
||||
获取所有祖先部门
|
||||
"""
|
||||
ancestors = []
|
||||
current = await cls.get_by_id(db, dept_id)
|
||||
|
||||
while current and current.parent_id:
|
||||
parent = await cls.get_by_id(db, current.parent_id)
|
||||
if parent:
|
||||
ancestors.append(parent)
|
||||
current = parent
|
||||
else:
|
||||
break
|
||||
|
||||
return ancestors
|
||||
|
||||
@classmethod
|
||||
async def can_delete(cls, db: AsyncSession, dept_id: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
检查部门是否可以删除
|
||||
|
||||
:return: (是否可删除, 原因)
|
||||
"""
|
||||
# 检查是否有子部门
|
||||
children = await cls.get_children(db, dept_id)
|
||||
if children:
|
||||
return False, "该部门下存在子部门,无法删除"
|
||||
|
||||
# 检查是否有用户(需要导入User模型后才能检查)
|
||||
# 这里暂时返回True,后续可以添加用户检查
|
||||
return True, ""
|
||||
|
||||
@classmethod
|
||||
async def batch_update_status(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
ids: List[str],
|
||||
status: bool
|
||||
) -> int:
|
||||
"""
|
||||
批量更新部门状态
|
||||
|
||||
:return: 更新的记录数
|
||||
"""
|
||||
count = 0
|
||||
for dept_id in ids:
|
||||
dept = await cls.get_by_id(db, dept_id)
|
||||
if dept:
|
||||
dept.status = status
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
await db.commit()
|
||||
|
||||
return count
|
||||
|
||||
@classmethod
|
||||
async def batch_delete(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
ids: List[str],
|
||||
hard: bool = False
|
||||
) -> Tuple[int, List[str]]:
|
||||
"""
|
||||
批量删除部门
|
||||
|
||||
:return: (删除成功数, 删除失败的ID列表)
|
||||
"""
|
||||
success_count = 0
|
||||
failed_ids = []
|
||||
|
||||
for dept_id in ids:
|
||||
can_del, reason = await cls.can_delete(db, dept_id)
|
||||
if can_del:
|
||||
if await cls.delete(db, dept_id, hard=hard):
|
||||
success_count += 1
|
||||
else:
|
||||
failed_ids.append(dept_id)
|
||||
else:
|
||||
failed_ids.append(dept_id)
|
||||
|
||||
return success_count, failed_ids
|
||||
|
||||
@classmethod
|
||||
async def get_user_count(cls, db: AsyncSession, dept_id: str) -> int:
|
||||
"""获取部门下的用户数量"""
|
||||
from core.user.model import User
|
||||
result = await db.execute(
|
||||
select(func.count(User.id)).where(
|
||||
User.dept_id == dept_id,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@classmethod
|
||||
async def get_child_count(cls, db: AsyncSession, dept_id: str) -> int:
|
||||
"""获取直接子部门数量"""
|
||||
result = await db.execute(
|
||||
select(func.count(Dept.id)).where(
|
||||
Dept.parent_id == dept_id,
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@classmethod
|
||||
async def search(cls, db: AsyncSession, keyword: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
搜索部门(模糊匹配部门名称或编码)
|
||||
返回匹配部门及其完整的层级路径
|
||||
"""
|
||||
if not keyword:
|
||||
return []
|
||||
|
||||
# 搜索部门
|
||||
result = await db.execute(
|
||||
select(Dept).where(
|
||||
(Dept.name.ilike(f"%{keyword}%") | Dept.code.ilike(f"%{keyword}%")),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
matched_depts = list(result.scalars().all())
|
||||
|
||||
# 收集所有需要的部门ID(包括匹配部门和其所有祖先)
|
||||
dept_ids_to_include = set()
|
||||
for dept in matched_depts:
|
||||
dept_ids_to_include.add(dept.id)
|
||||
ancestors = await cls.get_ancestors(db, dept.id)
|
||||
for ancestor in ancestors:
|
||||
dept_ids_to_include.add(ancestor.id)
|
||||
|
||||
# 获取所有需要的部门
|
||||
result = await db.execute(
|
||||
select(Dept).where(
|
||||
Dept.id.in_(dept_ids_to_include),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
all_depts = list(result.scalars().all())
|
||||
|
||||
# 构建部门字典
|
||||
dept_dict_map = {}
|
||||
for dept in all_depts:
|
||||
child_count = await cls.get_child_count(db, dept.id)
|
||||
user_count = await cls.get_user_count(db, dept.id)
|
||||
dept_dict = {
|
||||
'id': dept.id,
|
||||
'name': dept.name,
|
||||
'code': dept.code,
|
||||
'dept_type': dept.dept_type,
|
||||
'dept_type_display': dept.get_dept_type_display(),
|
||||
'status': dept.status,
|
||||
'level': dept.level,
|
||||
'path': dept.path,
|
||||
'parent_id': dept.parent_id,
|
||||
'lead_id': dept.lead_id,
|
||||
'phone': dept.phone,
|
||||
'email': dept.email,
|
||||
'description': dept.description,
|
||||
'sort': dept.sort,
|
||||
'child_count': child_count,
|
||||
'user_count': user_count,
|
||||
}
|
||||
dept_dict_map[dept.id] = dept_dict
|
||||
|
||||
# 构建树形结构
|
||||
roots = []
|
||||
for dept_id, dept in dept_dict_map.items():
|
||||
parent_id = dept['parent_id']
|
||||
if parent_id is None:
|
||||
roots.append(dept)
|
||||
elif parent_id in dept_dict_map:
|
||||
parent = dept_dict_map[parent_id]
|
||||
if 'children' not in parent:
|
||||
parent['children'] = []
|
||||
parent['children'].append(dept)
|
||||
|
||||
return roots
|
||||
|
||||
@classmethod
|
||||
async def get_by_ids(cls, db: AsyncSession, ids: List[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
根据ID列表批量获取部门信息(包含完整的层级路径)
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
# 收集所有需要的部门ID
|
||||
dept_ids_to_include = set()
|
||||
result = await db.execute(
|
||||
select(Dept).where(
|
||||
Dept.id.in_(ids),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
target_depts = list(result.scalars().all())
|
||||
|
||||
for dept in target_depts:
|
||||
dept_ids_to_include.add(dept.id)
|
||||
ancestors = await cls.get_ancestors(db, dept.id)
|
||||
for ancestor in ancestors:
|
||||
dept_ids_to_include.add(ancestor.id)
|
||||
|
||||
# 获取所有需要的部门
|
||||
result = await db.execute(
|
||||
select(Dept).where(
|
||||
Dept.id.in_(dept_ids_to_include),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
all_depts = list(result.scalars().all())
|
||||
|
||||
# 构建字典和树形结构
|
||||
dept_dict_map = {}
|
||||
for dept in all_depts:
|
||||
child_count = await cls.get_child_count(db, dept.id)
|
||||
user_count = await cls.get_user_count(db, dept.id)
|
||||
dept_dict = {
|
||||
'id': dept.id,
|
||||
'name': dept.name,
|
||||
'code': dept.code,
|
||||
'dept_type': dept.dept_type,
|
||||
'status': dept.status,
|
||||
'level': dept.level,
|
||||
'parent_id': dept.parent_id,
|
||||
'child_count': child_count,
|
||||
'user_count': user_count,
|
||||
}
|
||||
dept_dict_map[dept.id] = dept_dict
|
||||
|
||||
roots = []
|
||||
for dept_id, dept in dept_dict_map.items():
|
||||
parent_id = dept['parent_id']
|
||||
if parent_id is None:
|
||||
roots.append(dept)
|
||||
elif parent_id in dept_dict_map:
|
||||
parent = dept_dict_map[parent_id]
|
||||
if 'children' not in parent:
|
||||
parent['children'] = []
|
||||
parent['children'].append(dept)
|
||||
|
||||
return roots
|
||||
|
||||
@classmethod
|
||||
async def get_stats(cls, db: AsyncSession) -> Dict[str, Any]:
|
||||
"""获取部门统计信息"""
|
||||
# 总数
|
||||
total_result = await db.execute(
|
||||
select(func.count(Dept.id)).where(Dept.is_deleted == False) # noqa: E712
|
||||
)
|
||||
total_count = total_result.scalar() or 0
|
||||
|
||||
# 启用数
|
||||
active_result = await db.execute(
|
||||
select(func.count(Dept.id)).where(
|
||||
Dept.status == True, # noqa: E712
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
active_count = active_result.scalar() or 0
|
||||
|
||||
# 根部门数
|
||||
root_result = await db.execute(
|
||||
select(func.count(Dept.id)).where(
|
||||
Dept.parent_id.is_(None),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
root_count = root_result.scalar() or 0
|
||||
|
||||
# 按类型统计
|
||||
type_stats = {}
|
||||
type_choices = [
|
||||
('company', '公司'),
|
||||
('department', '部门'),
|
||||
('team', '小组'),
|
||||
('other', '其他'),
|
||||
]
|
||||
for type_code, type_name in type_choices:
|
||||
count_result = await db.execute(
|
||||
select(func.count(Dept.id)).where(
|
||||
Dept.dept_type == type_code,
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
type_stats[type_name] = count_result.scalar() or 0
|
||||
|
||||
# 最大层级
|
||||
max_level_result = await db.execute(
|
||||
select(func.max(Dept.level)).where(Dept.is_deleted == False) # noqa: E712
|
||||
)
|
||||
max_level = max_level_result.scalar() or 0
|
||||
|
||||
return {
|
||||
'total_count': total_count,
|
||||
'active_count': active_count,
|
||||
'inactive_count': total_count - active_count,
|
||||
'root_count': root_count,
|
||||
'type_stats': type_stats,
|
||||
'max_level': max_level,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def move(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
dept_id: str,
|
||||
new_parent_id: Optional[str]
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
移动部门到新的父部门下
|
||||
|
||||
:return: (是否成功, 消息)
|
||||
"""
|
||||
dept = await cls.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
return False, "部门不存在"
|
||||
|
||||
# 检查新父部门
|
||||
if new_parent_id:
|
||||
if new_parent_id == dept_id:
|
||||
return False, "不能将自己设置为父部门"
|
||||
|
||||
new_parent = await cls.get_by_id(db, new_parent_id)
|
||||
if not new_parent:
|
||||
return False, "父部门不存在"
|
||||
|
||||
# 检查是否会形成循环引用
|
||||
ancestors = await cls.get_ancestors(db, new_parent_id)
|
||||
ancestor_ids = [a.id for a in ancestors]
|
||||
if dept.id in ancestor_ids or dept.id == new_parent.id:
|
||||
return False, "不能移动到自己或子部门下"
|
||||
|
||||
dept.parent_id = new_parent_id
|
||||
dept.level = new_parent.level + 1
|
||||
dept.path = f"{new_parent.path or '/'}{new_parent.id}/"
|
||||
else:
|
||||
dept.parent_id = None
|
||||
dept.level = 0
|
||||
dept.path = "/"
|
||||
|
||||
await db.commit()
|
||||
return True, "移动成功"
|
||||
|
||||
@classmethod
|
||||
async def get_dept_users(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
dept_id: str,
|
||||
include_children: bool = False
|
||||
) -> List[Any]:
|
||||
"""获取部门下的用户列表"""
|
||||
from core.user.model import User
|
||||
|
||||
if include_children:
|
||||
# 获取部门及其所有子部门的用户
|
||||
descendants = await cls.get_descendants(db, dept_id)
|
||||
dept_ids = [dept_id] + [d.id for d in descendants]
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.dept_id.in_(dept_ids),
|
||||
User.user_status == 1,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 只获取当前部门的用户
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.dept_id == dept_id,
|
||||
User.user_status == 1,
|
||||
User.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def add_users_to_dept(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
dept_id: str,
|
||||
user_ids: List[str]
|
||||
) -> int:
|
||||
"""将用户添加到部门"""
|
||||
from core.user.model import User
|
||||
|
||||
dept = await cls.get_by_id(db, dept_id)
|
||||
if not dept:
|
||||
return 0
|
||||
|
||||
added_count = 0
|
||||
for user_id in user_ids:
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user and user.dept_id != dept_id:
|
||||
user.dept_id = dept_id
|
||||
added_count += 1
|
||||
|
||||
if added_count > 0:
|
||||
await db.commit()
|
||||
|
||||
return added_count
|
||||
|
||||
@classmethod
|
||||
async def remove_users_from_dept(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
dept_id: str,
|
||||
user_ids: List[str]
|
||||
) -> int:
|
||||
"""从部门中移除用户"""
|
||||
from core.user.model import User
|
||||
|
||||
removed_count = 0
|
||||
for user_id in user_ids:
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user and user.dept_id == dept_id:
|
||||
user.dept_id = None
|
||||
removed_count += 1
|
||||
|
||||
if removed_count > 0:
|
||||
await db.commit()
|
||||
|
||||
return removed_count
|
||||
|
||||
@classmethod
|
||||
async def get_by_parent(cls, db: AsyncSession, parent_id: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""根据父部门ID获取直接子部门"""
|
||||
if parent_id:
|
||||
query = select(Dept).where(
|
||||
Dept.parent_id == parent_id,
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
).order_by(Dept.sort.desc())
|
||||
else:
|
||||
query = select(Dept).where(
|
||||
Dept.parent_id.is_(None),
|
||||
Dept.is_deleted == False # noqa: E712
|
||||
).order_by(Dept.sort.desc())
|
||||
|
||||
result = await db.execute(query)
|
||||
depts = list(result.scalars().all())
|
||||
|
||||
dept_list = []
|
||||
for dept in depts:
|
||||
child_count = await cls.get_child_count(db, dept.id)
|
||||
user_count = await cls.get_user_count(db, dept.id)
|
||||
dept_dict = {
|
||||
'id': dept.id,
|
||||
'name': dept.name,
|
||||
'code': dept.code,
|
||||
'dept_type': dept.dept_type,
|
||||
'dept_type_display': dept.get_dept_type_display(),
|
||||
'status': dept.status,
|
||||
'level': dept.level,
|
||||
'path': dept.path,
|
||||
'parent_id': dept.parent_id,
|
||||
'lead_id': dept.lead_id,
|
||||
'phone': dept.phone,
|
||||
'email': dept.email,
|
||||
'description': dept.description,
|
||||
'sort': dept.sort,
|
||||
'child_count': child_count,
|
||||
'user_count': user_count,
|
||||
}
|
||||
dept_list.append(dept_dict)
|
||||
|
||||
return dept_list
|
||||
Reference in New Issue
Block a user