909 lines
32 KiB
Python
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)
|