Refactor physical deletion to logical deletion (#1191)

* Refactor physical deletion to logical deletion

* Update the usage of unique indexes

* Update scheduler update and delete

* Fix lint

* Add some missing logic

* Optimize codegen get by id
This commit is contained in:
Wu Clan
2026-05-30 15:02:00 +08:00
committed by GitHub
parent 1897b3198e
commit a5e9a0e805
43 changed files with 612 additions and 263 deletions
+18 -7
View File
@@ -6,6 +6,7 @@ from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule
from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRuleParam
from backend.utils.timezone import timezone
class CRUDDataRule(CRUDPlus[DataRule]):
@@ -19,7 +20,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param pk: 规则 ID
:return:
"""
return await self.select_model(db, pk)
return await self.select_model(db, pk, deleted=0)
async def get_select(self, name: str | None) -> Select:
"""
@@ -28,7 +29,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param name: 规则名称
:return:
"""
filters = {}
filters = {'deleted': 0}
if name is not None:
filters['name__like'] = f'%{name}%'
@@ -43,7 +44,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param name: 规则名称
:return:
"""
return await self.select_model_by_column(db, name=name)
return await self.select_model_by_column(db, name=name, deleted=0)
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
"""
@@ -52,7 +53,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
return await self.select_models(db, deleted=0)
async def get_all_by_ids(self, db: AsyncSession, pks: list[int]) -> Sequence[DataRule]:
"""
@@ -62,7 +63,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param pks: 规则 ID 列表
:return:
"""
return await self.select_models(db, id__in=pks)
return await self.select_models(db, id__in=pks, deleted=0)
async def create(self, db: AsyncSession, obj: CreateDataRuleParam) -> None:
"""
@@ -83,7 +84,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param obj: 更新规则参数
:return:
"""
return await self.update_model(db, pk, obj)
return await self.update_model_by_column(db, obj, id=pk, deleted=0)
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
"""
@@ -93,7 +94,17 @@ class CRUDDataRule(CRUDPlus[DataRule]):
:param pks: 规则 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
return await self.delete_model_by_column(
db,
allow_multiple=True,
logical_deletion=True,
deleted_flag_column='deleted',
deleted_flag_value=self.model.id,
deleted_at_column='deleted_time',
deleted_at_factory=timezone.now(),
id__in=pks,
deleted=0,
)
data_rule_dao: CRUDDataRule = CRUDDataRule(DataRule)
+25 -9
View File
@@ -1,7 +1,7 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy import Select, delete, insert
from sqlalchemy import Select, and_, delete, insert
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
@@ -13,6 +13,7 @@ from backend.app.admin.schema.data_scope import (
UpdateDataScopeRuleParam,
)
from backend.utils.serializers import select_join_serialize
from backend.utils.timezone import timezone
class CRUDDataScope(CRUDPlus[DataScope]):
@@ -26,7 +27,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param pk: 范围 ID
:return:
"""
return await self.select_model(db, pk)
return await self.select_model(db, pk, deleted=0)
async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None:
"""
@@ -36,7 +37,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param name: 范围名称
:return:
"""
return await self.select_model_by_column(db, name=name)
return await self.select_model_by_column(db, name=name, deleted=0)
async def get_join(self, db: AsyncSession, pk: int) -> Any:
"""
@@ -49,9 +50,14 @@ class CRUDDataScope(CRUDPlus[DataScope]):
result = await self.select_models(
db,
id=pk,
deleted=0,
join_conditions=[
JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == self.model.id),
JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True),
JoinConfig(
model=DataRule,
join_on=and_(DataRule.id == data_scope_rule.c.data_rule_id, DataRule.deleted == 0),
fill_result=True,
),
],
)
@@ -64,7 +70,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
return await self.select_models(db, deleted=0)
async def get_all_by_ids(self, db: AsyncSession, pks: list[int]) -> Sequence[DataScope]:
"""
@@ -74,7 +80,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param pks: 范围 ID 列表
:return:
"""
return await self.select_models(db, id__in=pks)
return await self.select_models(db, id__in=pks, deleted=0)
async def get_select(self, name: str | None, status: int | None) -> Select:
"""
@@ -84,7 +90,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param status: 范围状态
:return:
"""
filters = {}
filters = {'deleted': 0}
if name is not None:
filters['name__like'] = f'%{name}%'
@@ -112,7 +118,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param obj: 更新数据范围参数
:return:
"""
return await self.update_model(db, pk, obj)
return await self.update_model_by_column(db, obj, id=pk, deleted=0)
@staticmethod
async def update_rules(db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
@@ -145,7 +151,17 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param pks: 范围 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
return await self.delete_model_by_column(
db,
allow_multiple=True,
logical_deletion=True,
deleted_flag_column='deleted',
deleted_flag_value=self.model.id,
deleted_at_column='deleted_time',
deleted_at_factory=timezone.now(),
id__in=pks,
deleted=0,
)
data_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
+25 -8
View File
@@ -1,13 +1,14 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy import ColumnElement, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
from backend.app.admin.model import Dept, User
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.utils.serializers import select_join_serialize
from backend.utils.timezone import timezone
class CRUDDept(CRUDPlus[Dept]):
@@ -21,7 +22,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param dept_id: 部门 ID
:return:
"""
return await self.select_model_by_column(db, id=dept_id, del_flag=False)
return await self.select_model_by_column(db, id=dept_id, deleted=0)
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
"""
@@ -31,7 +32,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param name: 部门名称
:return:
"""
return await self.select_model_by_column(db, name=name, del_flag=False)
return await self.select_model_by_column(db, name=name, deleted=0)
async def get_all(
self,
@@ -53,7 +54,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param status: 部门状态
:return:
"""
filters = {'del_flag': False}
filters = {'deleted': 0}
if name is not None:
filters['name__like'] = f'%{name}%'
@@ -85,7 +86,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param obj: 更新部门参数
:return:
"""
return await self.update_model(db, dept_id, obj)
return await self.update_model_by_column(db, obj, id=dept_id, deleted=0)
async def delete(self, db: AsyncSession, dept_id: int) -> int:
"""
@@ -95,7 +96,16 @@ class CRUDDept(CRUDPlus[Dept]):
:param dept_id: 部门 ID
:return:
"""
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
return await self.delete_model_by_column(
db,
logical_deletion=True,
deleted_flag_column='deleted',
deleted_flag_value=self.model.id,
deleted_at_column='deleted_time',
deleted_at_factory=timezone.now(),
id=dept_id,
deleted=0,
)
async def get_join(self, db: AsyncSession, dept_id: int) -> Any | None:
"""
@@ -108,7 +118,14 @@ class CRUDDept(CRUDPlus[Dept]):
result = await self.select_model(
db,
dept_id,
join_conditions=[JoinConfig(model=User, join_on=User.dept_id == self.model.id, fill_result=True)],
deleted=0,
join_conditions=[
JoinConfig(
model=User,
join_on=and_(User.dept_id == self.model.id, User.deleted == 0),
fill_result=True,
)
],
)
return select_join_serialize(result, relationships=['Dept-o2m-User'])
@@ -120,7 +137,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param dept_id: 部门 ID
:return:
"""
return await self.select_models(db, parent_id=dept_id, del_flag=False)
return await self.select_models(db, parent_id=dept_id, deleted=0)
dept_dao: CRUDDept = CRUDDept(Dept)
+18 -8
View File
@@ -6,6 +6,7 @@ from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Menu, role_menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.utils.timezone import timezone
class CRUDMenu(CRUDPlus[Menu]):
@@ -19,7 +20,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param menu_id: 菜单 ID
:return:
"""
return await self.select_model(db, menu_id)
return await self.select_model(db, menu_id, deleted=0)
async def get_by_title(self, db: AsyncSession, title: str) -> Menu | None:
"""
@@ -29,7 +30,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param title: 菜单标题
:return:
"""
return await self.select_model_by_column(db, title=title, type__ne=2)
return await self.select_model_by_column(db, title=title, type__ne=2, deleted=0)
async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
"""
@@ -40,7 +41,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param status: 菜单状态
:return:
"""
filters = {}
filters = {'deleted': 0}
if title is not None:
filters['title__like'] = f'%{title}%'
@@ -57,7 +58,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param menu_ids: 菜单 ID 列表
:return:
"""
filters = {'type__in': [0, 1, 3, 4]}
filters = {'type__in': [0, 1, 3, 4], 'deleted': 0}
if menu_ids:
filters['id__in'] = menu_ids
@@ -72,7 +73,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param menu_ids: 菜单 ID 列表
:return:
"""
return await self.select_models(db, id__in=menu_ids)
return await self.select_models(db, id__in=menu_ids, deleted=0)
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
"""
@@ -93,7 +94,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param obj: 更新菜单参数
:return:
"""
return await self.update_model(db, menu_id, obj)
return await self.update_model_by_column(db, obj, id=menu_id, deleted=0)
async def delete(self, db: AsyncSession, menu_id: int) -> int:
"""
@@ -106,7 +107,16 @@ class CRUDMenu(CRUDPlus[Menu]):
role_menu_stmt = delete(role_menu).where(role_menu.c.menu_id == menu_id)
await db.execute(role_menu_stmt)
return await self.delete_model(db, menu_id)
return await self.delete_model_by_column(
db,
logical_deletion=True,
deleted_flag_column='deleted',
deleted_flag_value=self.model.id,
deleted_at_column='deleted_time',
deleted_at_factory=timezone.now(),
id=menu_id,
deleted=0,
)
async def get_children(self, db: AsyncSession, menu_id: int) -> Sequence[Menu | None]:
"""
@@ -116,7 +126,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param menu_id: 菜单 ID
:return:
"""
return await self.select_models(db, parent_id=menu_id)
return await self.select_models(db, parent_id=menu_id, deleted=0)
menu_dao: CRUDMenu = CRUDMenu(Menu)
+35 -11
View File
@@ -1,7 +1,7 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy import Select, delete, insert, select
from sqlalchemy import Select, and_, delete, insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
@@ -15,6 +15,7 @@ from backend.app.admin.schema.role import (
UpdateRoleScopeParam,
)
from backend.utils.serializers import select_join_serialize
from backend.utils.timezone import timezone
class CRUDRole(CRUDPlus[Role]):
@@ -28,7 +29,7 @@ class CRUDRole(CRUDPlus[Role]):
:param role_id: 角色 ID
:return:
"""
return await self.select_model(db, role_id)
return await self.select_model(db, role_id, deleted=0)
@staticmethod
async def get_menus(db: AsyncSession, role_id: int) -> Sequence[Menu] | None:
@@ -39,7 +40,11 @@ class CRUDRole(CRUDPlus[Role]):
:param role_id: 角色 ID
:return:
"""
menu_stmt = select(Menu).join(role_menu, Menu.id == role_menu.c.menu_id).where(role_menu.c.role_id == role_id)
menu_stmt = (
select(Menu)
.join(role_menu, Menu.id == role_menu.c.menu_id)
.where(role_menu.c.role_id == role_id, Menu.deleted == 0)
)
result = await db.execute(menu_stmt)
return result.scalars().all()
@@ -54,11 +59,20 @@ class CRUDRole(CRUDPlus[Role]):
result = await self.select_models(
db,
id=role_id,
deleted=0,
join_conditions=[
JoinConfig(model=role_menu, join_on=role_menu.c.role_id == self.model.id),
JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True),
JoinConfig(
model=Menu,
join_on=and_(Menu.id == role_menu.c.menu_id, Menu.deleted == 0),
fill_result=True,
),
JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == self.model.id),
JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True),
JoinConfig(
model=DataScope,
join_on=and_(DataScope.id == role_data_scope.c.data_scope_id, DataScope.deleted == 0),
fill_result=True,
),
],
)
@@ -71,7 +85,7 @@ class CRUDRole(CRUDPlus[Role]):
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
return await self.select_models(db, deleted=0)
async def get_all_by_ids(self, db: AsyncSession, role_ids: list[int]) -> Sequence[Role]:
"""
@@ -81,7 +95,7 @@ class CRUDRole(CRUDPlus[Role]):
:param role_ids: 角色 ID 列表
:return:
"""
return await self.select_models(db, id__in=role_ids)
return await self.select_models(db, id__in=role_ids, deleted=0)
async def get_select(self, name: str | None, status: int | None) -> Select:
"""
@@ -92,7 +106,7 @@ class CRUDRole(CRUDPlus[Role]):
:return:
"""
filters = {}
filters = {'deleted': 0}
if name is not None:
filters['name__like'] = f'%{name}%'
@@ -109,7 +123,7 @@ class CRUDRole(CRUDPlus[Role]):
:param name: 角色名称
:return:
"""
return await self.select_model_by_column(db, name=name)
return await self.select_model_by_column(db, name=name, deleted=0)
async def create(self, db: AsyncSession, obj: CreateRoleParam) -> None:
"""
@@ -130,7 +144,7 @@ class CRUDRole(CRUDPlus[Role]):
:param obj: 更新角色参数
:return:
"""
return await self.update_model(db, role_id, obj)
return await self.update_model_by_column(db, obj, id=role_id, deleted=0)
@staticmethod
async def update_menus(db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
@@ -185,7 +199,17 @@ class CRUDRole(CRUDPlus[Role]):
:param role_ids: 角色 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_ids)
return await self.delete_model_by_column(
db,
allow_multiple=True,
logical_deletion=True,
deleted_flag_column='deleted',
deleted_flag_value=self.model.id,
deleted_at_column='deleted_time',
deleted_at_factory=timezone.now(),
id__in=role_ids,
deleted=0,
)
role_dao: CRUDRole = CRUDRole(Role)
+69 -30
View File
@@ -3,7 +3,7 @@ from typing import Any
import bcrypt
from sqlalchemy import Select, delete, insert, select
from sqlalchemy import Select, and_, delete, insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
@@ -44,7 +44,7 @@ class CRUDUser(CRUDPlus[User]):
:param user_id: 用户 ID
:return:
"""
return await self.select_model(db, user_id)
return await self.select_model(db, user_id, deleted=0)
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
"""
@@ -54,7 +54,7 @@ class CRUDUser(CRUDPlus[User]):
:param username: 用户名
:return:
"""
return await self.select_model_by_column(db, username=username)
return await self.select_model_by_column(db, username=username, deleted=0)
async def get_all_by_usernames(self, db: AsyncSession, usernames: list[str]) -> Sequence[User]:
"""
@@ -64,7 +64,7 @@ class CRUDUser(CRUDPlus[User]):
:param usernames: 用户名列表
:return:
"""
return await self.select_models(db, username__in=usernames)
return await self.select_models(db, username__in=usernames, deleted=0)
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
"""
@@ -74,7 +74,7 @@ class CRUDUser(CRUDPlus[User]):
:param nickname: 用户昵称
:return:
"""
return await self.select_model_by_column(db, nickname=nickname)
return await self.select_model_by_column(db, nickname=nickname, deleted=0)
async def check_email(self, db: AsyncSession, email: str) -> User | None:
"""
@@ -84,7 +84,7 @@ class CRUDUser(CRUDPlus[User]):
:param email: 电子邮箱
:return:
"""
return await self.select_model_by_column(db, email=email)
return await self.select_model_by_column(db, email=email, deleted=0)
async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
"""
@@ -96,7 +96,7 @@ class CRUDUser(CRUDPlus[User]):
:param status: 用户状态
:return:
"""
filters = {}
filters = {'deleted': 0}
if dept:
filters['dept_id'] = dept
@@ -111,9 +111,17 @@ class CRUDUser(CRUDPlus[User]):
'id',
'desc',
join_conditions=[
JoinConfig(model=Dept, join_on=Dept.id == self.model.dept_id, fill_result=True),
JoinConfig(
model=Dept,
join_on=and_(Dept.id == self.model.dept_id, Dept.deleted == 0),
fill_result=True,
),
JoinConfig(model=user_role, join_on=user_role.c.user_id == self.model.id),
JoinConfig(model=Role, join_on=Role.id == user_role.c.role_id, fill_result=True),
JoinConfig(
model=Role,
join_on=and_(Role.id == user_role.c.role_id, Role.deleted == 0),
fill_result=True,
),
],
**filters,
)
@@ -136,7 +144,7 @@ class CRUDUser(CRUDPlus[User]):
await db.flush()
if obj.roles:
role_stmt = select(Role).where(Role.id.in_(obj.roles))
role_stmt = select(Role).where(Role.id.in_(obj.roles), Role.deleted == 0)
result = await db.execute(role_stmt)
roles = result.scalars().all()
@@ -158,7 +166,7 @@ class CRUDUser(CRUDPlus[User]):
db.add(new_user)
await db.flush()
role_stmt = select(Role).where(Role.status == StatusType.enable)
role_stmt = select(Role).where(Role.status == StatusType.enable, Role.deleted == 0)
result = await db.execute(role_stmt)
role = result.scalars().first() # 默认绑定第一个角色
if role is None:
@@ -179,13 +187,13 @@ class CRUDUser(CRUDPlus[User]):
role_ids = obj.roles
del obj.roles
count = await self.update_model(db, user_id, obj)
count = await self.update_model_by_column(db, obj, id=user_id, deleted=0)
user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id)
await db.execute(user_role_stmt)
if role_ids:
role_stmt = select(Role).where(Role.id.in_(role_ids))
role_stmt = select(Role).where(Role.id.in_(role_ids), Role.deleted == 0)
result = await db.execute(role_stmt)
roles = result.scalars().all()
@@ -203,7 +211,7 @@ class CRUDUser(CRUDPlus[User]):
:param username: 用户名
:return:
"""
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username, deleted=0)
async def update_password_changed_time(self, db: AsyncSession, user_id: int) -> int:
"""
@@ -213,7 +221,9 @@ class CRUDUser(CRUDPlus[User]):
:param user_id: 用户 ID
:return:
"""
return await self.update_model(db, user_id, {'last_password_changed_time': timezone.now()})
return await self.update_model_by_column(
db, {'last_password_changed_time': timezone.now()}, id=user_id, deleted=0
)
async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int:
"""
@@ -224,7 +234,7 @@ class CRUDUser(CRUDPlus[User]):
:param nickname: 用户昵称
:return:
"""
return await self.update_model(db, user_id, {'nickname': nickname})
return await self.update_model_by_column(db, {'nickname': nickname}, id=user_id, deleted=0)
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
"""
@@ -235,7 +245,7 @@ class CRUDUser(CRUDPlus[User]):
:param avatar: 头像地址
:return:
"""
return await self.update_model(db, user_id, {'avatar': avatar})
return await self.update_model_by_column(db, {'avatar': avatar}, id=user_id, deleted=0)
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
"""
@@ -246,7 +256,7 @@ class CRUDUser(CRUDPlus[User]):
:param email: 邮箱
:return:
"""
return await self.update_model(db, user_id, {'email': email})
return await self.update_model_by_column(db, {'email': email}, id=user_id, deleted=0)
async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
"""
@@ -259,7 +269,7 @@ class CRUDUser(CRUDPlus[User]):
"""
salt = bcrypt.gensalt()
new_pwd = get_hash_password(password, salt)
return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt}, flush=True)
return await self.update_model_by_column(db, {'password': new_pwd, 'salt': salt}, flush=True, id=pk, deleted=0)
async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int:
"""
@@ -270,7 +280,7 @@ class CRUDUser(CRUDPlus[User]):
:param is_super: 是否超级管理员
:return:
"""
return await self.update_model(db, user_id, {'is_superuser': is_super})
return await self.update_model_by_column(db, {'is_superuser': is_super}, id=user_id, deleted=0)
async def set_staff(self, db: AsyncSession, user_id: int, *, is_staff: bool) -> int:
"""
@@ -281,7 +291,7 @@ class CRUDUser(CRUDPlus[User]):
:param is_staff: 是否可登录后台
:return:
"""
return await self.update_model(db, user_id, {'is_staff': is_staff})
return await self.update_model_by_column(db, {'is_staff': is_staff}, id=user_id, deleted=0)
async def set_status(self, db: AsyncSession, user_id: int, status: int) -> int:
"""
@@ -292,7 +302,7 @@ class CRUDUser(CRUDPlus[User]):
:param status: 状态
:return:
"""
return await self.update_model(db, user_id, {'status': status})
return await self.update_model_by_column(db, {'status': status}, id=user_id, deleted=0)
async def set_multi_login(self, db: AsyncSession, user_id: int, *, multi_login: bool) -> int:
"""
@@ -303,7 +313,7 @@ class CRUDUser(CRUDPlus[User]):
:param multi_login: 是否允许多端登录
:return:
"""
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
return await self.update_model_by_column(db, {'is_multi_login': multi_login}, id=user_id, deleted=0)
async def delete(self, db: AsyncSession, user_id: int) -> int:
"""
@@ -324,7 +334,16 @@ class CRUDUser(CRUDPlus[User]):
user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id)
await db.execute(user_role_stmt)
return await self.delete_model(db, user_id)
return await self.delete_model_by_column(
db,
logical_deletion=True,
deleted_flag_column='deleted',
deleted_flag_value=self.model.id,
deleted_at_column='deleted_time',
deleted_at_factory=timezone.now(),
id=user_id,
deleted=0,
)
async def get_join(
self,
@@ -341,7 +360,7 @@ class CRUDUser(CRUDPlus[User]):
:param username: 用户名
:return:
"""
filters = {}
filters = {'deleted': 0}
if user_id:
filters['id'] = user_id
@@ -351,15 +370,35 @@ class CRUDUser(CRUDPlus[User]):
result = await self.select_models(
db,
join_conditions=[
JoinConfig(model=Dept, join_on=Dept.id == self.model.dept_id, fill_result=True),
JoinConfig(
model=Dept,
join_on=and_(Dept.id == self.model.dept_id, Dept.deleted == 0),
fill_result=True,
),
JoinConfig(model=user_role, join_on=user_role.c.user_id == self.model.id),
JoinConfig(model=Role, join_on=Role.id == user_role.c.role_id, fill_result=True),
JoinConfig(
model=Role,
join_on=and_(Role.id == user_role.c.role_id, Role.deleted == 0),
fill_result=True,
),
JoinConfig(model=role_menu, join_on=role_menu.c.role_id == Role.id),
JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True),
JoinConfig(
model=Menu,
join_on=and_(Menu.id == role_menu.c.menu_id, Menu.deleted == 0),
fill_result=True,
),
JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == Role.id),
JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True),
JoinConfig(
model=DataScope,
join_on=and_(DataScope.id == role_data_scope.c.data_scope_id, DataScope.deleted == 0),
fill_result=True,
),
JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == DataScope.id),
JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True),
JoinConfig(
model=DataRule,
join_on=and_(DataRule.id == data_scope_rule.c.data_rule_id, DataRule.deleted == 0),
fill_result=True,
),
],
**filters,
)
+5 -1
View File
@@ -9,9 +9,13 @@ class DataRule(Base):
"""数据规则表"""
__tablename__ = 'sys_data_rule'
__table_args__ = (
sa.UniqueConstraint('name', 'deleted', name='uk_sys_data_rule_name_deleted'),
{'comment': '数据规则表'},
)
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(sa.String(512), unique=True, comment='名称')
name: Mapped[str] = mapped_column(sa.String(512), comment='名称')
model: Mapped[str] = mapped_column(sa.String(64), comment='模型名称')
column: Mapped[str] = mapped_column(sa.String(32), comment='模型字段名')
operator: Mapped[int] = mapped_column(comment='运算符(0and、1or')
+5 -1
View File
@@ -9,7 +9,11 @@ class DataScope(Base):
"""数据范围表"""
__tablename__ = 'sys_data_scope'
__table_args__ = (
sa.UniqueConstraint('name', 'deleted', name='uk_sys_data_scope_name_deleted'),
{'comment': '数据范围表'},
)
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(sa.String(64), unique=True, comment='名称')
name: Mapped[str] = mapped_column(sa.String(64), comment='名称')
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
+4 -1
View File
@@ -9,6 +9,10 @@ class Dept(Base):
"""部门表"""
__tablename__ = 'sys_dept'
__table_args__ = (
sa.UniqueConstraint('name', 'deleted', name='uk_sys_dept_name_deleted'),
{'comment': '部门表'},
)
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(sa.String(64), comment='部门名称')
@@ -17,7 +21,6 @@ class Dept(Base):
phone: Mapped[str | None] = mapped_column(sa.String(11), default=None, comment='手机')
email: Mapped[str | None] = mapped_column(sa.String(64), default=None, comment='邮箱')
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在)')
# 父级部门
parent_id: Mapped[int | None] = mapped_column(sa.BigInteger, default=None, index=True, comment='父部门ID')
+5 -1
View File
@@ -9,9 +9,13 @@ class Role(Base):
"""角色表"""
__tablename__ = 'sys_role'
__table_args__ = (
sa.UniqueConstraint('name', 'deleted', name='uk_sys_role_name_deleted'),
{'comment': '角色表'},
)
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(sa.String(32), unique=True, comment='角色名称')
name: Mapped[str] = mapped_column(sa.String(32), comment='角色名称')
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
is_filter_scopes: Mapped[bool] = mapped_column(default=True, comment='过滤数据权限(0否 1是)')
remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注')
+7 -2
View File
@@ -13,14 +13,19 @@ class User(Base):
"""用户表"""
__tablename__ = 'sys_user'
__table_args__ = (
sa.UniqueConstraint('username', 'deleted', name='uk_sys_user_username_deleted'),
sa.UniqueConstraint('email', 'deleted', name='uk_sys_user_email_deleted'),
{'comment': '用户表'},
)
id: Mapped[id_key] = mapped_column(init=False)
uuid: Mapped[str] = mapped_column(sa.String(64), init=False, default_factory=uuid4_str, unique=True)
username: Mapped[str] = mapped_column(sa.String(64), unique=True, index=True, comment='用户名')
username: Mapped[str] = mapped_column(sa.String(64), index=True, comment='用户名')
nickname: Mapped[str] = mapped_column(sa.String(64), comment='昵称')
password: Mapped[str | None] = mapped_column(sa.String(256), comment='密码')
salt: Mapped[bytes | None] = mapped_column(sa.LargeBinary(255), comment='加密盐')
email: Mapped[str | None] = mapped_column(sa.String(256), default=None, unique=True, index=True, comment='邮箱')
email: Mapped[str | None] = mapped_column(sa.String(256), default=None, index=True, comment='邮箱')
phone: Mapped[str | None] = mapped_column(sa.String(11), default=None, comment='手机号')
avatar: Mapped[str | None] = mapped_column(sa.String(256), default=None, comment='头像')
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
+2 -1
View File
@@ -32,9 +32,10 @@ class GetDeptDetail(DeptSchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='部门 ID')
del_flag: bool = Field(description='是否删除')
deleted: int = Field(description='是否删除0:否;id:是)')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
deleted_time: datetime | None = Field(None, description='删除时间')
class GetDeptTree(GetDeptDetail):
@@ -90,6 +90,8 @@ class UserService:
"""
if await user_dao.get_by_username(db, obj.username):
raise errors.ConflictError(msg='用户名已注册')
if obj.email and await user_dao.check_email(db, obj.email):
raise errors.ConflictError(msg='邮箱已被绑定')
if not obj.password:
raise errors.RequestError(msg='密码不允许为空')
if not await dept_dao.get(db, obj.dept_id):
@@ -116,6 +118,10 @@ class UserService:
raise errors.NotFoundError(msg='用户不存在')
if obj.username != user.username and await user_dao.get_by_username(db, obj.username):
raise errors.ConflictError(msg='用户名已注册')
if obj.email and obj.email != user.email:
email_user = await user_dao.check_email(db, obj.email)
if email_user:
raise errors.ConflictError(msg='邮箱已被绑定')
if obj.dept_id and obj.dept_id != user.dept_id and not await dept_dao.get(db, dept_id=obj.dept_id):
raise errors.NotFoundError(msg='部门不存在')
if obj.roles:
@@ -256,6 +262,9 @@ class UserService:
raise errors.RequestError(msg='验证码已失效,请重新获取')
if captcha != captcha_code:
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
email_user = await user_dao.check_email(db, email)
if email_user and email_user.id != user_id:
raise errors.ConflictError(msg='邮箱已被绑定')
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
count = await user_dao.update_email(db, user_id, email)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}')