from __future__ import annotations import re from datetime import datetime from fastapi import HTTPException from sqlalchemy import and_, delete, exists, func, or_, select from sqlalchemy.orm import Session from app.models.org import ( Department, Employee, OrgEmployeeBinding, OrgManagerBinding, Permission, Role, RolePermission, User, UserRole, ) from app.schemas.system_permissions import ( EmployeeOption, MenuPermissionNode, OrgEmployeeNode, OrgEmployeeBind, OrgNodeCreate, OrgNodeRead, OrgNodeUpdate, PermissionRead, RoleCreate, RoleRead, RoleUpdate, SystemUserCreate, SystemUserRead, ) from app.services.auth import hash_password MENU_PERMISSION_TREE = [ ("MENU_DASHBOARD", "经营驾驶舱", []), ( "MENU_MASTER_DATA", "基础资料", [ ("MENU_SUPPLIER_MANAGEMENT", "供应商名录", []), ("MENU_MATERIAL_MANAGEMENT", "原材料名录", []), ("MENU_SALES_PLANNING", "客户名录/销售订单", []), ("MENU_PRODUCT_MANAGEMENT", "产品管理", []), ("MENU_BOM_MANAGEMENT", "BOM管理", []), ("MENU_ROUTE_MANAGEMENT", "工艺路线管理", []), ("MENU_EMPLOYEE_MANAGEMENT", "人员管理", []), ], ), ( "MENU_PROCUREMENT", "采购与库存", [ ("MENU_PURCHASE_ORDER", "采购订单", []), ("MENU_PURCHASE_RECEIPT", "到货入库", []), ("MENU_QUALITY_INSPECTION", "质量检验", []), ("MENU_INVENTORY_LEDGER", "嘉恒仓库", []), ], ), ( "MENU_PRODUCTION", "生产执行", [ ("MENU_WORK_ORDER", "工单台账", []), ("MENU_OPERATION_REPORT", "工序报工", []), ("MENU_EQUIPMENT_MANAGEMENT", "设备管理", []), ], ), ("MENU_DELIVERY_MANAGEMENT", "发货概览", []), ("MENU_FINANCE_REPORT", "财务分析", []), ( "MENU_SYSTEM_MANAGEMENT", "系统管理", [ ("MENU_SYSTEM_PERMISSION", "系统权限管理", []), ("MENU_SYSTEM_EXTENSION", "系统拓展", []), ], ), ] ORG_ROOT_CODE = "ORG_ROOT" ORG_ROOT_NAME = "总公司" VALID_CHILD_NODE_TYPES = { "COMPANY": { "BRANCH": "总公司下只能新增分公司或直属部门", "DEPARTMENT": "总公司下只能新增分公司或直属部门", }, "BRANCH": {"DEPARTMENT": "分公司下只能新增部门"}, "DEPARTMENT": {"GROUP": "部门下只能新增小组"}, "GROUP": {}, } ORG_NODE_TYPE_LABELS = { "COMPANY": "总公司", "BRANCH": "分公司", "DEPARTMENT": "部门", "GROUP": "小组", } def _now() -> datetime: return datetime.now() def _normalize_phone(value: str | None) -> str: return re.sub(r"\s+", "", str(value or "").strip()) def _employee_name_sort_key(employee: Employee) -> tuple[str, int]: surname_order = { "王": "wang", "李": "li", "张": "zhang", "刘": "liu", "陈": "chen", "杨": "yang", "赵": "zhao", "黄": "huang", "周": "zhou", "吴": "wu", "徐": "xu", "孙": "sun", "胡": "hu", "朱": "zhu", "高": "gao", "林": "lin", "何": "he", "郭": "guo", "马": "ma", "罗": "luo", } name = employee.employee_name or "" return (f"{surname_order.get(name[:1], name[:1])}{name[1:]}", employee.id) def _menu_node(raw: tuple[str, str, list]) -> MenuPermissionNode: code, name, children = raw return MenuPermissionNode( permission_code=code, permission_name=name, children=[_menu_node(child) for child in children], ) def list_menu_permission_tree() -> list[MenuPermissionNode]: return [_menu_node(item) for item in MENU_PERMISSION_TREE] def list_permissions(db: Session) -> list[PermissionRead]: rows = db.scalars(select(Permission).order_by(Permission.module_code, Permission.id)).all() return [ PermissionRead( permission_id=row.id, permission_code=row.permission_code, permission_name=row.permission_name, module_code=row.module_code, action_code=row.action_code, status=row.status, ) for row in rows ] def _employee_role_map(db: Session, employee_ids: list[int]) -> tuple[dict[int, str], dict[int, list[str]]]: if not employee_ids: return {}, {} rows = db.execute( select(User.employee_id, User.username, Role.role_name) .select_from(User) .outerjoin(UserRole, UserRole.user_id == User.id) .outerjoin(Role, Role.id == UserRole.role_id) .where(User.employee_id.in_(employee_ids)) .order_by(User.employee_id, Role.id) ).all() username_map: dict[int, str] = {} role_map: dict[int, list[str]] = {} for employee_id, username, role_name in rows: if employee_id is None: continue username_map.setdefault(employee_id, username) if role_name: role_map.setdefault(employee_id, []) if role_name not in role_map[employee_id]: role_map[employee_id].append(role_name) return username_map, role_map def _ensure_org_root(db: Session) -> Department: root = db.scalar(select(Department).where(Department.dept_code == ORG_ROOT_CODE)) if root: return root now = _now() root = Department( dept_code=ORG_ROOT_CODE, dept_name=ORG_ROOT_NAME, parent_id=None, org_node_type="COMPANY", dept_type="ADMIN", manager_name=None, manager_employee_id=None, status="ACTIVE", sort_no=0, remark="系统权限管理组织脑图根节点", created_at=now, updated_at=now, ) db.add(root) db.commit() db.refresh(root) return root def _managed_department_ids(root: Department, departments: list[Department]) -> set[int]: children_by_parent: dict[int | None, list[Department]] = {} for dept in departments: children_by_parent.setdefault(dept.parent_id, []).append(dept) result: set[int] = set() def walk(node_id: int) -> None: result.add(node_id) for child in children_by_parent.get(node_id, []): walk(child.id) walk(root.id) return result def _bound_employees_by_dept(db: Session, dept_ids: set[int]) -> dict[int, list[OrgEmployeeNode]]: if not dept_ids: return {} rows = db.execute( select(OrgEmployeeBinding, Employee) .join(Employee, Employee.id == OrgEmployeeBinding.employee_id) .where( OrgEmployeeBinding.dept_id.in_(dept_ids), OrgEmployeeBinding.status == "ACTIVE", Employee.status == "ACTIVE", ) .order_by(OrgEmployeeBinding.dept_id, OrgEmployeeBinding.sort_no, OrgEmployeeBinding.id) ).all() employees = [employee for _, employee in rows] username_map, role_map = _employee_role_map(db, [employee.id for employee in employees]) employees_by_dept: dict[int, list[OrgEmployeeNode]] = {} for binding, employee in rows: employees_by_dept.setdefault(binding.dept_id, []).append( OrgEmployeeNode( employee_id=employee.id, employee_name=employee.employee_name, mobile=employee.mobile, job_title=employee.job_title, username=username_map.get(employee.id), role_names=role_map.get(employee.id, []), status=employee.status, ) ) return employees_by_dept def _active_employees_for_ids(db: Session, employee_ids: list[int]) -> list[Employee]: unique_ids = list(dict.fromkeys(employee_ids)) if not unique_ids: return [] rows = db.scalars(select(Employee).where(Employee.id.in_(unique_ids), Employee.status == "ACTIVE")).all() employee_map = {employee.id: employee for employee in rows} missing = [employee_id for employee_id in unique_ids if employee_id not in employee_map] if missing: raise HTTPException(status_code=400, detail=f"主管不存在或已停用:{missing}") return [employee_map[employee_id] for employee_id in unique_ids] def _manager_payload_ids(payload: OrgNodeCreate | OrgNodeUpdate) -> list[int]: if payload.manager_employee_ids: return list(dict.fromkeys(payload.manager_employee_ids)) return [payload.manager_employee_id] if payload.manager_employee_id else [] def _replace_org_managers(db: Session, dept: Department, manager_employee_ids: list[int]) -> None: managers = _active_employees_for_ids(db, manager_employee_ids) db.execute(delete(OrgManagerBinding).where(OrgManagerBinding.dept_id == dept.id)) now = _now() for index, manager in enumerate(managers, start=1): db.add( OrgManagerBinding( dept_id=dept.id, employee_id=manager.id, sort_no=index, status="ACTIVE", created_at=now, updated_at=now, ) ) dept.manager_employee_id = managers[0].id if managers else None dept.manager_name = "、".join(manager.employee_name for manager in managers) if managers else None def _manager_bindings_by_dept( db: Session, dept_ids: set[int], departments: list[Department], ) -> dict[int, tuple[list[int], list[str]]]: if not dept_ids: return {} rows = db.execute( select(OrgManagerBinding, Employee) .join(Employee, Employee.id == OrgManagerBinding.employee_id) .where( OrgManagerBinding.dept_id.in_(dept_ids), OrgManagerBinding.status == "ACTIVE", Employee.status == "ACTIVE", ) .order_by(OrgManagerBinding.dept_id, OrgManagerBinding.sort_no, OrgManagerBinding.id) ).all() result: dict[int, tuple[list[int], list[str]]] = {} for binding, employee in rows: manager_ids, manager_names = result.setdefault(binding.dept_id, ([], [])) manager_ids.append(employee.id) manager_names.append(employee.employee_name) legacy_ids = [dept.manager_employee_id for dept in departments if dept.id in dept_ids and dept.manager_employee_id] legacy_employees = ( {employee.id: employee for employee in db.scalars(select(Employee).where(Employee.id.in_(legacy_ids))).all()} if legacy_ids else {} ) for dept in departments: if dept.id not in dept_ids or dept.id in result: continue if dept.manager_employee_id: employee = legacy_employees.get(dept.manager_employee_id) result[dept.id] = ( [dept.manager_employee_id], [employee.employee_name if employee else dept.manager_name or "未命名主管"], ) elif dept.manager_name: result[dept.id] = ([], [dept.manager_name]) return result def build_org_tree(db: Session) -> list[OrgNodeRead]: root = _ensure_org_root(db) departments = db.scalars( select(Department).order_by(Department.parent_id.is_not(None), Department.sort_no, Department.id) ).all() managed_ids = _managed_department_ids(root, departments) departments = [dept for dept in departments if dept.id in managed_ids] employees_by_dept = _bound_employees_by_dept(db, managed_ids) managers_by_dept = _manager_bindings_by_dept(db, managed_ids, departments) nodes: dict[int, OrgNodeRead] = { dept.id: OrgNodeRead( node_id=dept.id, node_type=getattr(dept, "org_node_type", None) or "DEPARTMENT", node_label=dept.dept_name, parent_id=dept.parent_id, dept_code=dept.dept_code, dept_type=dept.dept_type, manager_employee_id=(managers_by_dept.get(dept.id, ([], []))[0] or [None])[0], manager_name="、".join(managers_by_dept.get(dept.id, ([], []))[1]) or None, manager_employee_ids=managers_by_dept.get(dept.id, ([], []))[0], manager_names=managers_by_dept.get(dept.id, ([], []))[1], status=dept.status, sort_no=dept.sort_no, remark=dept.remark, employees=employees_by_dept.get(dept.id, []), children=[], ) for dept in departments } roots: list[OrgNodeRead] = [] for dept in departments: node = nodes[dept.id] if dept.parent_id and dept.parent_id in nodes: nodes[dept.parent_id].children.append(node) elif dept.id == root.id: roots.append(node) return roots def _flatten_org_nodes(nodes: list[OrgNodeRead]) -> list[OrgNodeRead]: result: list[OrgNodeRead] = [] for node in nodes: result.append(node) result.extend(_flatten_org_nodes(node.children)) return result def _next_department_code(db: Session, node_type: str) -> str: prefix = {"COMPANY": "COMP", "BRANCH": "BR", "DEPARTMENT": "DEPT", "GROUP": "GRP"}.get(node_type, "DEPT") count = db.scalar(select(func.count(Department.id)).where(Department.dept_code.like(f"{prefix}-%"))) or 0 return f"{prefix}-{int(count) + 1:03d}" def _manager_name(db: Session, employee_id: int | None) -> str | None: if not employee_id: return None employee = db.get(Employee, employee_id) if not employee or employee.status != "ACTIVE": raise HTTPException(status_code=400, detail="负责人不存在或已停用") return employee.employee_name def create_org_node(db: Session, payload: OrgNodeCreate) -> OrgNodeRead: if not payload.parent_id: if payload.node_type != "COMPANY": raise HTTPException(status_code=400, detail="请选择上级组织节点") parent = db.get(Department, payload.parent_id) if payload.parent_id else None if payload.parent_id and not parent: raise HTTPException(status_code=404, detail="上级组织节点不存在") if parent: parent_type = getattr(parent, "org_node_type", None) or "DEPARTMENT" allowed = VALID_CHILD_NODE_TYPES.get(parent_type, {}) if payload.node_type not in allowed: message = next(iter(allowed.values()), f"{ORG_NODE_TYPE_LABELS.get(parent_type, '当前节点')}下不能新增组织节点") raise HTTPException(status_code=400, detail=message) dept_code = payload.dept_code or _next_department_code(db, payload.node_type) if db.scalar(select(Department).where(Department.dept_code == dept_code)): raise HTTPException(status_code=400, detail="组织编码已存在") now = _now() row = Department( dept_code=dept_code, dept_name=payload.node_label, parent_id=payload.parent_id, org_node_type=payload.node_type, dept_type=payload.dept_type, manager_name=None, manager_employee_id=None, status=payload.status, sort_no=payload.sort_no, remark=payload.remark, created_at=now, updated_at=now, ) db.add(row) db.flush() _replace_org_managers(db, row, _manager_payload_ids(payload)) db.add(row) db.commit() return next(node for node in _flatten_org_nodes(build_org_tree(db)) if node.node_id == row.id) def update_org_node(db: Session, node_id: int, payload: OrgNodeUpdate) -> OrgNodeRead: row = db.get(Department, node_id) if not row: raise HTTPException(status_code=404, detail="组织节点不存在") row.dept_name = payload.node_label row.dept_type = payload.dept_type _replace_org_managers(db, row, _manager_payload_ids(payload)) row.sort_no = payload.sort_no row.remark = payload.remark row.status = payload.status row.updated_at = _now() db.add(row) db.commit() return next(node for node in _flatten_org_nodes(build_org_tree(db)) if node.node_id == row.id) def delete_org_node(db: Session, node_id: int) -> None: row = db.get(Department, node_id) if not row: raise HTTPException(status_code=404, detail="组织节点不存在") child_count = db.scalar(select(func.count(Department.id)).where(Department.parent_id == node_id)) or 0 if int(child_count) > 0: raise HTTPException(status_code=400, detail="该节点存在下级节点,请先从底层开始删除") employee_count = ( db.scalar( select(func.count(OrgEmployeeBinding.id)).where( OrgEmployeeBinding.dept_id == node_id, OrgEmployeeBinding.status == "ACTIVE", ) ) or 0 ) if int(employee_count) > 0: raise HTTPException(status_code=400, detail="该节点存在人员节点,请先移除人员") if row.dept_code == ORG_ROOT_CODE: raise HTTPException(status_code=400, detail="总公司根节点不能删除") db.execute(delete(OrgManagerBinding).where(OrgManagerBinding.dept_id == node_id)) db.delete(row) db.commit() def bind_org_employee(db: Session, node_id: int, payload: OrgEmployeeBind) -> OrgNodeRead: node = db.get(Department, node_id) if not node: raise HTTPException(status_code=404, detail="组织节点不存在") node_type = getattr(node, "org_node_type", None) or "DEPARTMENT" if node_type not in {"DEPARTMENT", "GROUP"}: raise HTTPException(status_code=400, detail="只有部门或小组节点可以新增人员") employee = db.get(Employee, payload.employee_id) if not employee or employee.status != "ACTIVE": raise HTTPException(status_code=400, detail="人员不存在或已停用") existing = db.scalar( select(OrgEmployeeBinding).where( OrgEmployeeBinding.dept_id == node_id, OrgEmployeeBinding.employee_id == payload.employee_id, ) ) now = _now() if existing: existing.status = "ACTIVE" existing.sort_no = payload.sort_no existing.updated_at = now db.add(existing) else: db.add( OrgEmployeeBinding( dept_id=node_id, employee_id=payload.employee_id, sort_no=payload.sort_no, status="ACTIVE", created_at=now, updated_at=now, ) ) db.commit() return next(node for node in _flatten_org_nodes(build_org_tree(db)) if node.node_id == node_id) def unbind_org_employee(db: Session, node_id: int, employee_id: int) -> OrgNodeRead: binding = db.scalar( select(OrgEmployeeBinding).where( OrgEmployeeBinding.dept_id == node_id, OrgEmployeeBinding.employee_id == employee_id, OrgEmployeeBinding.status == "ACTIVE", ) ) if not binding: raise HTTPException(status_code=404, detail="人员节点不存在") db.delete(binding) db.commit() return next(node for node in _flatten_org_nodes(build_org_tree(db)) if node.node_id == node_id) def list_roles(db: Session) -> list[RoleRead]: roles = db.scalars(select(Role).order_by(Role.id)).all() if not roles: return [] role_ids = [role.id for role in roles] permission_rows = db.execute( select(RolePermission.role_id, Permission.permission_code) .join(Permission, Permission.id == RolePermission.permission_id) .where(RolePermission.role_id.in_(role_ids)) .order_by(RolePermission.role_id, Permission.permission_code) ).all() user_counts = dict( db.execute( select(UserRole.role_id, func.count(UserRole.user_id)) .where(UserRole.role_id.in_(role_ids)) .group_by(UserRole.role_id) ).all() ) permission_map: dict[int, list[str]] = {} for role_id, code in permission_rows: permission_map.setdefault(role_id, []).append(code) return [ RoleRead( role_id=role.id, role_code=role.role_code, role_name=role.role_name, role_scope=role.role_scope, status=role.status, remark=role.remark, permission_codes=permission_map.get(role.id, []), user_count=int(user_counts.get(role.id, 0)), created_at=role.created_at, updated_at=role.updated_at, ) for role in roles ] def _replace_role_permissions(db: Session, role_id: int, permission_codes: list[str]) -> None: permissions = _permissions_for_codes(db, permission_codes) db.execute(delete(RolePermission).where(RolePermission.role_id == role_id)) now = _now() for permission in permissions: db.add(RolePermission(role_id=role_id, permission_id=permission.id, created_at=now)) def _permissions_for_codes(db: Session, permission_codes: list[str]) -> list[Permission]: unique_codes = list(dict.fromkeys(permission_codes)) permissions = ( db.scalars(select(Permission).where(Permission.permission_code.in_(unique_codes))).all() if unique_codes else [] ) missing = sorted(set(unique_codes) - {row.permission_code for row in permissions}) if missing: raise HTTPException(status_code=400, detail=f"权限不存在:{'、'.join(missing)}") return permissions def create_role(db: Session, payload: RoleCreate) -> RoleRead: if db.scalar(select(Role).where(Role.role_code == payload.role_code)): raise HTTPException(status_code=400, detail="角色编码已存在") _permissions_for_codes(db, payload.permission_codes) now = _now() row = Role( role_code=payload.role_code, role_name=payload.role_name, role_scope=payload.role_scope, status=payload.status, remark=payload.remark, created_at=now, updated_at=now, ) db.add(row) db.flush() _replace_role_permissions(db, row.id, payload.permission_codes) db.commit() return next(role for role in list_roles(db) if role.role_id == row.id) def update_role(db: Session, role_id: int, payload: RoleUpdate) -> RoleRead: row = db.get(Role, role_id) if not row: raise HTTPException(status_code=404, detail="角色不存在") _permissions_for_codes(db, payload.permission_codes) row.role_name = payload.role_name row.role_scope = payload.role_scope row.status = payload.status row.remark = payload.remark row.updated_at = _now() db.add(row) _replace_role_permissions(db, row.id, payload.permission_codes) db.commit() return next(role for role in list_roles(db) if role.role_id == row.id) def _system_user_read(db: Session, user: User) -> SystemUserRead: employee = db.get(Employee, user.employee_id) department = db.get(Department, user.dept_id) role_rows = db.execute( select(Role.id, Role.role_name) .join(UserRole, UserRole.role_id == Role.id) .where(UserRole.user_id == user.id) .order_by(Role.id) ).all() return SystemUserRead( user_id=user.id, username=user.username, employee_id=user.employee_id, employee_name=employee.employee_name if employee else user.nickname or user.username, dept_id=user.dept_id, dept_name=department.dept_name if department else None, mobile=employee.mobile if employee else None, nickname=user.nickname, is_super_admin=bool(user.is_super_admin), status=user.status, role_ids=[row[0] for row in role_rows], role_names=[row[1] for row in role_rows], last_login_at=user.last_login_at, created_at=user.created_at, updated_at=user.updated_at, ) def list_system_users(db: Session) -> list[SystemUserRead]: rows = db.scalars(select(User).order_by(User.status, User.id)).all() return [_system_user_read(db, row) for row in rows] def _permission_by_code(db: Session, permission_code: str) -> Permission: permission = db.scalar( select(Permission).where( Permission.permission_code == permission_code, Permission.status == "ACTIVE", ) ) if not permission: raise HTTPException(status_code=400, detail=f"权限码不存在或已停用:{permission_code}") return permission def _employee_role_permission_maps( db: Session, employee_ids: list[int], ) -> tuple[dict[int, User], dict[int, list[str]], dict[int, list[str]]]: if not employee_ids: return {}, {}, {} users = db.scalars( select(User) .where( User.employee_id.in_(employee_ids), User.status == "ACTIVE", ) .order_by(User.id) ).all() user_by_employee_id = {user.employee_id: user for user in users} if not users: return {}, {}, {} rows = db.execute( select(User.employee_id, Role.role_name, Permission.permission_code) .join(UserRole, UserRole.user_id == User.id) .join(Role, Role.id == UserRole.role_id) .outerjoin(RolePermission, RolePermission.role_id == Role.id) .outerjoin( Permission, and_(Permission.id == RolePermission.permission_id, Permission.status == "ACTIVE"), ) .where( User.id.in_([user.id for user in users]), Role.status == "ACTIVE", ) .order_by(User.employee_id, Role.id, Permission.permission_code) ).all() role_map: dict[int, list[str]] = {} permission_map: dict[int, list[str]] = {} for employee_id, role_name, permission_code in rows: if role_name and role_name not in role_map.setdefault(employee_id, []): role_map[employee_id].append(role_name) if permission_code and permission_code not in permission_map.setdefault(employee_id, []): permission_map[employee_id].append(permission_code) return user_by_employee_id, role_map, permission_map def employee_has_permission(db: Session, employee_id: int, permission_code: str) -> bool: permission = _permission_by_code(db, permission_code) user = db.scalar( select(User).where( User.employee_id == employee_id, User.status == "ACTIVE", ) ) if not user: return False employee = db.get(Employee, employee_id) if not employee or employee.status != "ACTIVE": return False if bool(user.is_super_admin): return True return bool( db.scalar( select( exists() .where(UserRole.user_id == user.id) .where(UserRole.role_id == Role.id) .where(Role.status == "ACTIVE") .where(RolePermission.role_id == Role.id) .where(RolePermission.permission_id == permission.id) ) ) ) def ensure_employee_has_permission( db: Session, employee_id: int | None, permission_code: str, field_label: str, *, required: bool = False, ) -> None: if employee_id is None: if required: raise HTTPException(status_code=400, detail=f"{field_label}必填") return if employee_id <= 0: raise HTTPException(status_code=400, detail=f"{field_label}不存在或已停用") permission = _permission_by_code(db, permission_code) employee = db.get(Employee, employee_id) if not employee or employee.status != "ACTIVE": raise HTTPException(status_code=400, detail=f"{field_label}不存在或已停用") if not employee_has_permission(db, employee_id, permission_code): raise HTTPException( status_code=400, detail=f"{field_label}必须选择有“{permission.permission_name}”权限的人员", ) def list_employee_options( db: Session, permission_code: str | None = None, limit: int = 500, ) -> list[EmployeeOption]: permission = _permission_by_code(db, permission_code) if permission_code else None stmt = select(Employee).where(Employee.status == "ACTIVE") if permission: permitted_user_exists = ( exists() .where(User.employee_id == Employee.id) .where(User.status == "ACTIVE") .where( or_( User.is_super_admin == 1, exists() .where(UserRole.user_id == User.id) .where(UserRole.role_id == Role.id) .where(Role.status == "ACTIVE") .where(RolePermission.role_id == Role.id) .where(RolePermission.permission_id == permission.id), ) ) ) stmt = stmt.where(permitted_user_exists) employees = sorted( db.scalars(stmt.order_by(Employee.id)).all(), key=_employee_name_sort_key, )[:limit] dept_map = {row.id: row.dept_name for row in db.scalars(select(Department)).all()} employee_ids = [employee.id for employee in employees] user_map, role_map, permission_map = _employee_role_permission_maps(db, employee_ids) return [ EmployeeOption( employee_id=employee.id, employee_name=employee.employee_name, dept_id=employee.dept_id, dept_name=dept_map.get(employee.dept_id), mobile=employee.mobile, job_title=employee.job_title, username=user_map[employee.id].username if employee.id in user_map else None, role_names=role_map.get(employee.id, []), permission_codes=permission_map.get(employee.id, []), has_system_user=employee.id in user_map, ) for employee in employees ] def _replace_user_roles(db: Session, user_id: int, role_ids: list[int]) -> None: roles = _roles_for_ids(db, role_ids) db.execute(delete(UserRole).where(UserRole.user_id == user_id)) now = _now() for role in roles: db.add(UserRole(user_id=user_id, role_id=role.id, created_at=now)) def _roles_for_ids(db: Session, role_ids: list[int]) -> list[Role]: unique_role_ids = list(dict.fromkeys(role_ids)) roles = db.scalars(select(Role).where(Role.id.in_(unique_role_ids), Role.status == "ACTIVE")).all() missing = sorted(set(unique_role_ids) - {row.id for row in roles}) if missing: raise HTTPException(status_code=400, detail=f"角色不存在或已停用:{missing}") return roles def create_system_user(db: Session, payload: SystemUserCreate) -> SystemUserRead: employee = db.get(Employee, payload.employee_id) if not employee or employee.status != "ACTIVE": raise HTTPException(status_code=400, detail="人员不存在或已停用") username = _normalize_phone(employee.mobile) if not username: raise HTTPException(status_code=400, detail="人员没有电话号码,不能创建系统账号") if db.scalar(select(User).where(User.employee_id == employee.id)): raise HTTPException(status_code=400, detail="该人员已经有系统账号") if db.scalar(select(User).where(User.username == username)): raise HTTPException(status_code=400, detail="该电话号码已经作为系统账号存在") _roles_for_ids(db, payload.role_ids) now = _now() user = User( username=username, password_hash=hash_password(payload.password), employee_id=employee.id, dept_id=employee.dept_id, nickname=employee.employee_name, email=None, is_super_admin=0, last_login_at=None, status=payload.status, created_at=now, updated_at=now, ) db.add(user) db.flush() _replace_user_roles(db, user.id, payload.role_ids) db.commit() return _system_user_read(db, user) def update_system_user_roles(db: Session, user_id: int, role_ids: list[int]) -> SystemUserRead: user = db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="系统用户不存在") _replace_user_roles(db, user.id, role_ids) user.updated_at = _now() db.add(user) db.commit() return _system_user_read(db, user) def update_system_user_status(db: Session, user_id: int, status: str, current_user_id: int) -> SystemUserRead: user = db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="系统用户不存在") if user.id == current_user_id and status != "ACTIVE": raise HTTPException(status_code=400, detail="不能停用当前登录账号") user.status = status user.updated_at = _now() db.add(user) db.commit() return _system_user_read(db, user) def reset_system_user_password(db: Session, user_id: int, password: str) -> SystemUserRead: user = db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="系统用户不存在") user.password_hash = hash_password(password) user.updated_at = _now() db.add(user) db.commit() return _system_user_read(db, user)