You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
343 lines
13 KiB
Python
343 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from passlib.context import CryptContext
|
|
from sqlalchemy import (
|
|
JSON,
|
|
BigInteger,
|
|
Boolean,
|
|
Column,
|
|
DateTime,
|
|
Float,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
Table,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from iti.db import AuditMixin, Base, IdMixin, TimestampMixin
|
|
|
|
from iti_system.enums import GenderEnum, LogType, MenuTypeEnum, StatusEnum
|
|
|
|
|
|
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
|
|
|
|
|
sys_user_role = Table(
|
|
"sys_user_role",
|
|
Base.metadata,
|
|
Column("user_id", String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True),
|
|
Column("role_id", String(36), ForeignKey("sys_role.id", ondelete="CASCADE"), primary_key=True),
|
|
)
|
|
|
|
|
|
sys_role_menu = Table(
|
|
"sys_role_menu",
|
|
Base.metadata,
|
|
Column("role_id", String(36), ForeignKey("sys_role.id", ondelete="CASCADE"), primary_key=True),
|
|
Column("menu_id", String(36), ForeignKey("sys_menu.id", ondelete="CASCADE"), primary_key=True),
|
|
)
|
|
|
|
|
|
sys_user_dept = Table(
|
|
"sys_user_dept",
|
|
Base.metadata,
|
|
Column("user_id", String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), primary_key=True),
|
|
Column("dept_id", String(36), ForeignKey("sys_dept.id", ondelete="CASCADE"), primary_key=True),
|
|
)
|
|
|
|
|
|
class User(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_user"
|
|
|
|
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
phone: Mapped[str | None] = mapped_column(String(32), unique=True, nullable=True)
|
|
email: Mapped[str | None] = mapped_column(String(255), unique=True, nullable=True)
|
|
password_hash: Mapped[str] = mapped_column(String(255))
|
|
realname: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
avatar: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
gender: Mapped[str] = mapped_column(String(16), default=GenderEnum.SECURE.value)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
roles: Mapped[list["Role"]] = relationship(
|
|
secondary=sys_user_role,
|
|
back_populates="users",
|
|
lazy="selectin",
|
|
)
|
|
depts: Mapped[list["SysDept"]] = relationship(
|
|
secondary=sys_user_dept,
|
|
back_populates="users",
|
|
lazy="selectin",
|
|
)
|
|
attributes: Mapped[list["SysUserAttribute"]] = relationship(
|
|
back_populates="user",
|
|
lazy="selectin",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
def set_password(self, value: str) -> None:
|
|
self.password_hash = pwd_context.hash(value)
|
|
|
|
def check_password(self, value: str) -> bool:
|
|
return pwd_context.verify(value, self.password_hash)
|
|
|
|
@property
|
|
def permissions(self) -> list[str]:
|
|
codes: set[str] = set()
|
|
for role in self.roles:
|
|
for menu in role.menus:
|
|
if menu.status == StatusEnum.ENABLED.value and menu.auth_code:
|
|
codes.add(menu.auth_code)
|
|
return sorted(codes)
|
|
|
|
@property
|
|
def role_codes(self) -> list[str]:
|
|
return [role.code for role in self.roles]
|
|
|
|
@property
|
|
def dept_ids(self) -> list[str]:
|
|
return [dept.id for dept in self.depts]
|
|
|
|
@property
|
|
def attribute_map(self) -> dict[str, dict[str, Any]]:
|
|
result: dict[str, dict[str, Any]] = {}
|
|
for item in self.attributes:
|
|
result.setdefault(item.attr_group, {})[item.attr_key] = item.typed_value
|
|
return result
|
|
|
|
|
|
class Role(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_role"
|
|
|
|
name: Mapped[str] = mapped_column(String(64))
|
|
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
users: Mapped[list[User]] = relationship(
|
|
secondary=sys_user_role,
|
|
back_populates="roles",
|
|
lazy="selectin",
|
|
)
|
|
menus: Mapped[list["SysMenu"]] = relationship(
|
|
secondary=sys_role_menu,
|
|
back_populates="roles",
|
|
lazy="selectin",
|
|
)
|
|
|
|
@property
|
|
def permissions(self) -> list[str]:
|
|
return [menu.id for menu in self.menus]
|
|
|
|
|
|
class SysMenu(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_menu"
|
|
|
|
name: Mapped[str] = mapped_column(String(255), unique=True)
|
|
type: Mapped[str] = mapped_column(String(32), default=MenuTypeEnum.MENU.value)
|
|
path: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
component: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
redirect: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
auth_code: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
|
|
meta: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
parent_id: Mapped[str | None] = mapped_column(
|
|
String(36),
|
|
ForeignKey("sys_menu.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
)
|
|
|
|
parent: Mapped["SysMenu | None"] = relationship(remote_side="SysMenu.id", back_populates="children")
|
|
children: Mapped[list["SysMenu"]] = relationship(back_populates="parent", lazy="selectin")
|
|
roles: Mapped[list[Role]] = relationship(
|
|
secondary=sys_role_menu,
|
|
back_populates="menus",
|
|
lazy="selectin",
|
|
)
|
|
|
|
|
|
class SysDept(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_dept"
|
|
|
|
name: Mapped[str] = mapped_column(String(255))
|
|
parent_id: Mapped[str | None] = mapped_column(
|
|
String(36),
|
|
ForeignKey("sys_dept.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
leader_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
parent: Mapped["SysDept | None"] = relationship(remote_side="SysDept.id", back_populates="children")
|
|
children: Mapped[list["SysDept"]] = relationship(back_populates="parent", lazy="selectin")
|
|
users: Mapped[list[User]] = relationship(
|
|
secondary=sys_user_dept,
|
|
back_populates="depts",
|
|
lazy="selectin",
|
|
)
|
|
|
|
|
|
class SysConfig(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_config"
|
|
__table_args__ = (UniqueConstraint("type", "code", name="uk_sys_config_type_code"),)
|
|
|
|
type: Mapped[str] = mapped_column(String(64), default="SYSTEM")
|
|
name: Mapped[str] = mapped_column(String(255))
|
|
code: Mapped[str] = mapped_column(String(128), index=True)
|
|
value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
|
|
class SysDictType(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_dict_type"
|
|
|
|
type_name: Mapped[str] = mapped_column(String(255))
|
|
type_code: Mapped[str] = mapped_column(String(128), unique=True, index=True)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
data_list: Mapped[list["SysDictData"]] = relationship(
|
|
back_populates="type",
|
|
lazy="selectin",
|
|
cascade="all, delete-orphan",
|
|
primaryjoin="SysDictType.type_code == foreign(SysDictData.type_code)",
|
|
)
|
|
|
|
|
|
class SysDictData(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_dict_data"
|
|
__table_args__ = (UniqueConstraint("type_code", "code", name="uk_sys_dict_data_type_code_code"),)
|
|
|
|
type_code: Mapped[str] = mapped_column(String(128), index=True)
|
|
label: Mapped[str] = mapped_column(String(255))
|
|
code: Mapped[str] = mapped_column(String(128))
|
|
value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
type: Mapped[SysDictType | None] = relationship(
|
|
back_populates="data_list",
|
|
primaryjoin="foreign(SysDictData.type_code) == SysDictType.type_code",
|
|
)
|
|
|
|
|
|
class SysFile(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_file"
|
|
|
|
filename: Mapped[str] = mapped_column(String(255))
|
|
file_key: Mapped[str] = mapped_column(String(512), unique=True, index=True)
|
|
file_hash: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
|
|
mime_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
file_size: Mapped[int] = mapped_column(BigInteger, default=0)
|
|
extension: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
storage_type: Mapped[str] = mapped_column(String(32), default="local")
|
|
storage_info: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
|
directory_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
|
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
|
|
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
deleted_by: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
share_code: Mapped[str | None] = mapped_column(String(64), unique=True, nullable=True)
|
|
share_password: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
share_expire_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
share_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
status: Mapped[str] = mapped_column(String(16), default=StatusEnum.ENABLED.value)
|
|
|
|
|
|
class SysLog(IdMixin, TimestampMixin, Base):
|
|
__tablename__ = "sys_log"
|
|
|
|
name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
method: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
|
user_id: Mapped[str | None] = mapped_column(String(36), index=True, nullable=True)
|
|
path: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
ip: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
headers: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
query_params: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
body_params: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
execution_time: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
response: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
exception: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
success: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
|
desc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
type: Mapped[str] = mapped_column(String(32), default=LogType.OPERATION.value)
|
|
|
|
|
|
class SysUserAttribute(IdMixin, TimestampMixin, AuditMixin, Base):
|
|
__tablename__ = "sys_user_attribute"
|
|
__table_args__ = (
|
|
UniqueConstraint("user_id", "attr_group", "attr_key", name="uk_user_group_key"),
|
|
)
|
|
|
|
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("sys_user.id", ondelete="CASCADE"), index=True)
|
|
attr_group: Mapped[str] = mapped_column(String(64), index=True)
|
|
attr_key: Mapped[str] = mapped_column(String(128))
|
|
attr_value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
attr_type: Mapped[str] = mapped_column(String(32), default="string")
|
|
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
sort: Mapped[int] = mapped_column(Integer, default=0)
|
|
|
|
user: Mapped[User] = relationship(back_populates="attributes")
|
|
|
|
@property
|
|
def typed_value(self) -> Any:
|
|
if self.attr_value is None:
|
|
return None
|
|
if self.attr_type == "int":
|
|
return int(self.attr_value)
|
|
if self.attr_type == "float":
|
|
return float(self.attr_value)
|
|
if self.attr_type == "bool":
|
|
return self.attr_value.lower() in {"true", "1", "yes", "on"}
|
|
if self.attr_type == "json":
|
|
import json
|
|
|
|
return json.loads(self.attr_value)
|
|
if self.attr_type == "encrypted":
|
|
return "******"
|
|
return self.attr_value
|
|
|
|
def set_typed_value(self, value: Any) -> None:
|
|
if value is None:
|
|
self.attr_value = None
|
|
elif self.attr_type == "json":
|
|
import json
|
|
|
|
self.attr_value = json.dumps(value, ensure_ascii=False)
|
|
elif self.attr_type == "bool":
|
|
self.attr_value = "true" if value else "false"
|
|
else:
|
|
self.attr_value = str(value)
|
|
|
|
|
|
__all__ = [
|
|
"Role",
|
|
"SysConfig",
|
|
"SysDept",
|
|
"SysDictData",
|
|
"SysDictType",
|
|
"SysFile",
|
|
"SysLog",
|
|
"SysMenu",
|
|
"SysUserAttribute",
|
|
"User",
|
|
"sys_role_menu",
|
|
"sys_user_dept",
|
|
"sys_user_role",
|
|
]
|