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:
Wu Clan
2024-03-22 18:16:15 +08:00
committed by GitHub
parent a98621b40d
commit 5e438c685d
190 changed files with 2408 additions and 1494 deletions
+14
View File
@@ -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
+19
View File
@@ -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))
+32
View File
@@ -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
+23
View File
@@ -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
+21
View File
@@ -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
+32
View File
@@ -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='创建时间')
+41
View File
@@ -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'
)
+37
View File
@@ -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='创建时间')
+29
View File
@@ -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'
)
+13
View File
@@ -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'),
)
+45
View File
@@ -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
+13
View File
@@ -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