ForgeFlow-ERP/backend/app/services/system_permissions.py
2026-06-15 04:24:46 +08:00

909 lines
32 KiB
Python

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)