mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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='运算符(0:and、1:or)')
|
||||
|
||||
@@ -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正常)')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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='备注')
|
||||
|
||||
@@ -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正常)')
|
||||
|
||||
@@ -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}')
|
||||
|
||||
Reference in New Issue
Block a user