mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Refactor the backend architecture (#299)
* define the basic architecture * Update script and deployment file locations * Update the route registration * Fix CI download dependencies * Updated ruff to 0.3.3 * Update app subdirectory naming * Update the model import * fix pre-commit pdm lock * Update the service directory naming * Add CRUD method documents * Fix the issue of circular import * Update the README document * Update the SQL statement for create tables * Update docker scripts and documentation * Fix docker scripts * Update the backend README.md * Add the security folder and move the redis client * Update the configuration item * Fix environment configuration reads * Update the default configuration * Updated README description * Updated the user registration API * Fix test cases * Update the celery configuration * Update and fix celery configuration * Updated the celery structure * Update celery tasks and api * Add celery flower * Update the import style * Update contributors
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend.common.msd.model import MappedBase # noqa: I001
|
||||
from backend.app.admin.model.sys_api import Api
|
||||
from backend.app.admin.model.sys_casbin_rule import CasbinRule
|
||||
from backend.app.admin.model.sys_dept import Dept
|
||||
from backend.app.admin.model.sys_dict_data import DictData
|
||||
from backend.app.admin.model.sys_dict_type import DictType
|
||||
from backend.app.admin.model.sys_login_log import LoginLog
|
||||
from backend.app.admin.model.sys_menu import Menu
|
||||
from backend.app.admin.model.sys_opera_log import OperaLog
|
||||
from backend.app.admin.model.sys_role import Role
|
||||
from backend.app.admin.model.sys_user import User
|
||||
from backend.app.admin.model.sys_user_social import UserSocial
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class Api(Base):
|
||||
"""系统api"""
|
||||
|
||||
__tablename__ = 'sys_api'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(50), unique=True, comment='api名称')
|
||||
method: Mapped[str] = mapped_column(String(16), comment='请求方法')
|
||||
path: Mapped[str] = mapped_column(String(500), comment='api路径')
|
||||
remark: Mapped[str | None] = mapped_column(LONGTEXT, comment='备注')
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.msd.model import MappedBase, id_key
|
||||
|
||||
|
||||
class CasbinRule(MappedBase):
|
||||
"""重写 casbin 中的 CasbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题"""
|
||||
|
||||
__tablename__ = 'sys_casbin_rule'
|
||||
|
||||
id: Mapped[id_key]
|
||||
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p / g')
|
||||
v0: Mapped[str] = mapped_column(String(255), comment='角色ID / 用户uuid')
|
||||
v1: Mapped[str] = mapped_column(LONGTEXT, comment='api路径 / 角色名称')
|
||||
v2: Mapped[str | None] = mapped_column(String(255), comment='请求方法')
|
||||
v3: Mapped[str | None] = mapped_column(String(255))
|
||||
v4: Mapped[str | None] = mapped_column(String(255))
|
||||
v5: Mapped[str | None] = mapped_column(String(255))
|
||||
|
||||
def __str__(self):
|
||||
arr = [self.ptype]
|
||||
for v in (self.v0, self.v1, self.v2, self.v3, self.v4, self.v5):
|
||||
if v is None:
|
||||
break
|
||||
arr.append(v)
|
||||
return ', '.join(arr)
|
||||
|
||||
def __repr__(self):
|
||||
return '<CasbinRule {}: "{}">'.format(self.id, str(self))
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class Dept(Base):
|
||||
"""部门表"""
|
||||
|
||||
__tablename__ = 'sys_dept'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(50), comment='部门名称')
|
||||
level: Mapped[int] = mapped_column(default=0, comment='部门层级')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
leader: Mapped[str | None] = mapped_column(String(20), default=None, comment='负责人')
|
||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机')
|
||||
email: Mapped[str | None] = mapped_column(String(50), 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(
|
||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
|
||||
)
|
||||
parent: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||
children: Mapped[list['Dept'] | None] = relationship(init=False, back_populates='parent')
|
||||
# 部门用户一对多
|
||||
users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class DictData(Base):
|
||||
"""字典数据"""
|
||||
|
||||
__tablename__ = 'sys_dict_data'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
label: Mapped[str] = mapped_column(String(32), unique=True, comment='字典标签')
|
||||
value: Mapped[str] = mapped_column(String(32), unique=True, comment='字典值')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
||||
remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注')
|
||||
# 字典类型一对多
|
||||
type_id: Mapped[int] = mapped_column(ForeignKey('sys_dict_type.id'), default=None, comment='字典类型关联ID')
|
||||
type: Mapped['DictType'] = relationship(init=False, back_populates='datas') # noqa: F821
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class DictType(Base):
|
||||
"""字典类型"""
|
||||
|
||||
__tablename__ = 'sys_dict_type'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(32), unique=True, comment='字典类型名称')
|
||||
code: Mapped[str] = mapped_column(String(32), unique=True, comment='字典类型编码')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
||||
remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注')
|
||||
# 字典类型一对多
|
||||
datas: Mapped[list['DictData']] = relationship(init=False, back_populates='type') # noqa: F821
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.msd.model import DataClassBase, id_key
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class LoginLog(DataClassBase):
|
||||
"""登录日志表"""
|
||||
|
||||
__tablename__ = 'sys_login_log'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
user_uuid: Mapped[str] = mapped_column(String(50), comment='用户UUID')
|
||||
username: Mapped[str] = mapped_column(String(20), comment='用户名')
|
||||
status: Mapped[int] = mapped_column(insert_default=0, comment='登录状态(0失败 1成功)')
|
||||
ip: Mapped[str] = mapped_column(String(50), comment='登录IP地址')
|
||||
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
||||
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
||||
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
||||
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
|
||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
||||
msg: Mapped[str] = mapped_column(LONGTEXT, comment='提示消息')
|
||||
login_time: Mapped[datetime] = mapped_column(comment='登录时间')
|
||||
created_time: Mapped[datetime] = mapped_column(init=False, default_factory=timezone.now, comment='创建时间')
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.sys_role_menu import sys_role_menu
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class Menu(Base):
|
||||
"""菜单表"""
|
||||
|
||||
__tablename__ = 'sys_menu'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
|
||||
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
||||
level: Mapped[int] = mapped_column(default=0, comment='菜单层级')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
|
||||
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址')
|
||||
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
|
||||
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
||||
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
||||
show: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
|
||||
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
|
||||
remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注')
|
||||
# 父级菜单一对多
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
|
||||
)
|
||||
parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||
children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent')
|
||||
# 菜单角色多对多
|
||||
roles: Mapped[list['Role']] = relationship( # noqa: F821
|
||||
init=False, secondary=sys_role_menu, back_populates='menus'
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.msd.model import DataClassBase, id_key
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class OperaLog(DataClassBase):
|
||||
"""操作日志表"""
|
||||
|
||||
__tablename__ = 'sys_opera_log'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
username: Mapped[str | None] = mapped_column(String(20), comment='用户名')
|
||||
method: Mapped[str] = mapped_column(String(20), comment='请求类型')
|
||||
title: Mapped[str] = mapped_column(String(255), comment='操作模块')
|
||||
path: Mapped[str] = mapped_column(String(500), comment='请求路径')
|
||||
ip: Mapped[str] = mapped_column(String(50), comment='IP地址')
|
||||
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
||||
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
||||
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
||||
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
|
||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
||||
args: Mapped[str | None] = mapped_column(JSON(), comment='请求参数')
|
||||
status: Mapped[int] = mapped_column(comment='操作状态(0异常 1正常)')
|
||||
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
|
||||
msg: Mapped[str | None] = mapped_column(LONGTEXT, comment='提示消息')
|
||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时ms')
|
||||
opera_time: Mapped[datetime] = mapped_column(comment='操作时间')
|
||||
created_time: Mapped[datetime] = mapped_column(init=False, default_factory=timezone.now, comment='创建时间')
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.sys_role_menu import sys_role_menu
|
||||
from backend.app.admin.model.sys_user_role import sys_user_role
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class Role(Base):
|
||||
"""角色表"""
|
||||
|
||||
__tablename__ = 'sys_role'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
||||
data_scope: Mapped[int | None] = mapped_column(default=2, comment='权限范围(1:全部数据权限 2:自定义数据权限)')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
||||
remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注')
|
||||
# 角色用户多对多
|
||||
users: Mapped[list['User']] = relationship( # noqa: F821
|
||||
init=False, secondary=sys_user_role, back_populates='roles'
|
||||
)
|
||||
# 角色菜单多对多
|
||||
menus: Mapped[list['Menu']] = relationship( # noqa: F821
|
||||
init=False, secondary=sys_role_menu, back_populates='roles'
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
|
||||
|
||||
from backend.common.msd.model import MappedBase
|
||||
|
||||
sys_role_menu = Table(
|
||||
'sys_role_menu',
|
||||
MappedBase.metadata,
|
||||
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||
Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.sys_user_role import sys_user_role
|
||||
from backend.common.msd.model import Base, id_key
|
||||
from backend.database.db_mysql import uuid4_str
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
|
||||
__tablename__ = 'sys_user'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
||||
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
||||
nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称')
|
||||
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
||||
salt: Mapped[str | None] = mapped_column(String(5), comment='加密盐')
|
||||
email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱')
|
||||
is_superuser: Mapped[bool] = mapped_column(default=False, comment='超级权限(0否 1是)')
|
||||
is_staff: Mapped[bool] = mapped_column(default=False, comment='后台管理登陆(0否 1是)')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='用户账号状态(0停用 1正常)')
|
||||
is_multi_login: Mapped[bool] = mapped_column(default=False, comment='是否重复登陆(0否 1是)')
|
||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
||||
join_time: Mapped[datetime] = mapped_column(init=False, default_factory=timezone.now, comment='注册时间')
|
||||
last_login_time: Mapped[datetime | None] = mapped_column(init=False, onupdate=timezone.now, comment='上次登录')
|
||||
# 部门用户一对多
|
||||
dept_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
|
||||
)
|
||||
dept: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='users') # noqa: F821
|
||||
# 用户角色多对多
|
||||
roles: Mapped[list['Role']] = relationship( # noqa: F821
|
||||
init=False, secondary=sys_user_role, back_populates='users'
|
||||
)
|
||||
# 用户 OAuth2 一对多
|
||||
socials: Mapped[list['UserSocial']] = relationship(init=False, back_populates='user') # noqa: F821
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
|
||||
|
||||
from backend.common.msd.model import MappedBase
|
||||
|
||||
sys_user_role = Table(
|
||||
'sys_user_role',
|
||||
MappedBase.metadata,
|
||||
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
|
||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.common.msd.model import Base, id_key
|
||||
|
||||
|
||||
class UserSocial(Base):
|
||||
"""用户社交表(OAuth2)"""
|
||||
|
||||
__tablename__ = 'sys_user_social'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
|
||||
open_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 open id')
|
||||
uid: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 ID')
|
||||
union_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 union id')
|
||||
scope: Mapped[str | None] = mapped_column(String(120), default=None, comment='第三方用户授予的权限')
|
||||
code: Mapped[str | None] = mapped_column(String(50), default=None, comment='用户的授权 code')
|
||||
# 用户 OAuth2 一对多
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
|
||||
)
|
||||
user: Mapped[Union['User', None]] = relationship(init=False, back_populates='socials') # noqa: F821
|
||||
Reference in New Issue
Block a user