#!/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