项目初次提交

This commit is contained in:
2025-04-11 08:54:28 +08:00
commit 9e14a3256f
220 changed files with 15673 additions and 0 deletions

0
apps/vadmin/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,84 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/10/19 15:41
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import APIRouter, Depends
from apps.vadmin.auth.utils.current import AllUserAuth
from utils.response import SuccessResponse
from apps.vadmin.auth.utils.validation.auth import Auth
import random
app = APIRouter()
###########################################################
# 图表数据
###########################################################
@app.get("/random/number", summary="获取随机整数")
async def get_random_number(auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(random.randint(500, 20000))
@app.get("/banners", summary="轮播图")
async def get_banners(auth: Auth = Depends(AllUserAuth())):
data = [
{
"id": 1, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-14/1.jpg"
},
{
"id": 2, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-09/banner1.png"
},
{
"id": 3, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-09/banner3.png"
},
]
return SuccessResponse(data)
@app.get("/user/access/source", summary="用户来源")
async def get_user_access_source(auth: Auth = Depends(AllUserAuth())):
data = [
{"value": 1000, "name": 'analysis.directAccess'},
{"value": 310, "name": 'analysis.mailMarketing'},
{"value": 234, "name": 'analysis.allianceAdvertising'},
{"value": 135, "name": 'analysis.videoAdvertising'},
{"value": 1548, "name": 'analysis.searchEngines'}
]
return SuccessResponse(data)
@app.get("/weekly/user/activity", summary="每周用户活跃量")
async def get_weekly_user_activity(auth: Auth = Depends(AllUserAuth())):
data = [
{"value": 13253, "name": 'analysis.monday'},
{"value": 34235, "name": 'analysis.tuesday'},
{"value": 26321, "name": 'analysis.wednesday'},
{"value": 12340, "name": 'analysis.thursday'},
{"value": 24643, "name": 'analysis.friday'},
{"value": 1322, "name": 'analysis.saturday'},
{"value": 1324, "name": 'analysis.sunday'}
]
return SuccessResponse(data)
@app.get("/monthly/sales", summary="每月销售额")
async def get_monthly_sales(auth: Auth = Depends(AllUserAuth())):
data = [
{"estimate": 100, "actual": 120, "name": 'analysis.january'},
{"estimate": 120, "actual": 82, "name": 'analysis.february'},
{"estimate": 161, "actual": 91, "name": 'analysis.march'},
{"estimate": 134, "actual": 154, "name": 'analysis.april'},
{"estimate": 105, "actual": 162, "name": 'analysis.may'},
{"estimate": 160, "actual": 140, "name": 'analysis.june'},
{"estimate": 165, "actual": 145, "name": 'analysis.july'},
{"estimate": 114, "actual": 250, "name": 'analysis.august'},
{"estimate": 163, "actual": 134, "name": 'analysis.september'},
{"estimate": 185, "actual": 56, "name": 'analysis.october'},
{"estimate": 118, "actual": 99, "name": 'analysis.november'},
{"estimate": 123, "actual": 123, "name": 'analysis.december'}
]
return SuccessResponse(data)

View File

1075
apps/vadmin/auth/crud.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : __init__.py
# @IDE : PyCharm
# @desc : 简要说明
from .m2m import vadmin_auth_user_roles, vadmin_auth_role_menus, vadmin_auth_user_depts, vadmin_auth_role_depts
from .menu import VadminMenu
from .role import VadminRole
from .user import VadminUser
from .dept import VadminDept

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/10/23 13:41
# @File : dept.py
# @IDE : PyCharm
# @desc : 部门模型
from sqlalchemy.orm import Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey
class VadminDept(BaseModel):
__tablename__ = "vadmin_auth_dept"
__table_args__ = ({'comment': '部门表'})
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="部门名称")
dept_key: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="部门标识")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
order: Mapped[int | None] = mapped_column(Integer, comment="显示排序")
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
owner: Mapped[str | None] = mapped_column(String(255), comment="负责人")
phone: Mapped[str | None] = mapped_column(String(255), comment="联系电话")
email: Mapped[str | None] = mapped_column(String(255), comment="邮箱")
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("vadmin_auth_dept.id", ondelete='CASCADE'),
comment="上级部门"
)

View File

@ -0,0 +1,41 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : m2m.py
# @IDE : PyCharm
# @desc : 关联中间表
from db.db_base import Base
from sqlalchemy import ForeignKey, Column, Table, Integer
vadmin_auth_user_roles = Table(
"vadmin_auth_user_roles",
Base.metadata,
Column("user_id", Integer, ForeignKey("vadmin_auth_user.id", ondelete="CASCADE")),
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
)
vadmin_auth_role_menus = Table(
"vadmin_auth_role_menus",
Base.metadata,
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
Column("menu_id", Integer, ForeignKey("vadmin_auth_menu.id", ondelete="CASCADE")),
)
vadmin_auth_user_depts = Table(
"vadmin_auth_user_depts",
Base.metadata,
Column("user_id", Integer, ForeignKey("vadmin_auth_user.id", ondelete="CASCADE")),
Column("dept_id", Integer, ForeignKey("vadmin_auth_dept.id", ondelete="CASCADE")),
)
vadmin_auth_role_depts = Table(
"vadmin_auth_role_depts",
Base.metadata,
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
Column("dept_id", Integer, ForeignKey("vadmin_auth_dept.id", ondelete="CASCADE")),
)

View File

@ -0,0 +1,67 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : menu.py
# @IDE : PyCharm
# @desc : 菜单模型
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
class VadminMenu(BaseModel):
__tablename__ = "vadmin_auth_menu"
__table_args__ = ({'comment': '菜单表'})
title: Mapped[str] = mapped_column(String(50), comment="名称")
icon: Mapped[str | None] = mapped_column(String(50), comment="菜单图标")
redirect: Mapped[str | None] = mapped_column(String(100), comment="重定向地址")
component: Mapped[str | None] = mapped_column(String(255), comment="前端组件地址")
path: Mapped[str | None] = mapped_column(String(50), comment="前端路由地址")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
hidden: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否隐藏")
order: Mapped[int] = mapped_column(Integer, comment="排序")
menu_type: Mapped[str] = mapped_column(String(8), comment="菜单类型")
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("vadmin_auth_menu.id", ondelete='CASCADE'),
comment="父菜单"
)
perms: Mapped[str | None] = mapped_column(String(50), comment="权限标识", unique=False, index=True)
"""以下属性主要用于补全前端路由属性,"""
noCache: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则不会被 <keep-alive> 缓存(默认 false)",
default=False
)
breadcrumb: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为false则不会在breadcrumb面包屑中显示(默认 true)",
default=True
)
affix: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则会一直固定在tag项中(默认 false)",
default=False
)
noTagsView: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则不会出现在tag中(默认 false)",
default=False
)
canTo: Mapped[bool] = mapped_column(
Boolean,
comment="设置为true即使hidden为true也依然可以进行路由跳转(默认 false)",
default=False
)
alwaysShow: Mapped[bool] = mapped_column(
Boolean,
comment="""当你一个路由下面的 children 声明的路由大于1个时自动会变成嵌套的模式
只有一个时,会将那个子路由当做根路由显示在侧边栏,若你想不管路由下面的 children 声明的个数都显示你的根路由,
你可以设置 alwaysShow: true这样它就会忽略之前定义的规则一直显示根路由(默认 true)""",
default=True
)

View File

@ -0,0 +1,30 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : role.py
# @IDE : PyCharm
# @desc : 角色模型
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer
from .menu import VadminMenu
from .dept import VadminDept
from .m2m import vadmin_auth_role_menus, vadmin_auth_role_depts
class VadminRole(BaseModel):
__tablename__ = "vadmin_auth_role"
__table_args__ = ({'comment': '角色表'})
name: Mapped[str] = mapped_column(String(50), index=True, comment="名称")
role_key: Mapped[str] = mapped_column(String(50), index=True, comment="权限字符")
data_range: Mapped[int] = mapped_column(Integer, default=4, comment="数据权限范围")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
order: Mapped[int | None] = mapped_column(Integer, comment="排序")
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
is_admin: Mapped[bool] = mapped_column(Boolean, comment="是否为超级角色", default=False)
menus: Mapped[set[VadminMenu]] = relationship(secondary=vadmin_auth_role_menus)
depts: Mapped[set[VadminDept]] = relationship(secondary=vadmin_auth_role_depts)

View File

@ -0,0 +1,72 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : user.py
# @IDE : PyCharm
# @desc : 用户模型
from datetime import datetime
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, DateTime
from passlib.context import CryptContext
from .role import VadminRole
from .dept import VadminDept
from .m2m import vadmin_auth_user_roles, vadmin_auth_user_depts
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
class VadminUser(BaseModel):
__tablename__ = "vadmin_auth_user"
__table_args__ = ({'comment': '用户表'})
avatar: Mapped[str | None] = mapped_column(String(500), comment='头像')
telephone: Mapped[str] = mapped_column(String(11), nullable=False, index=True, comment="手机号", unique=False)
email: Mapped[str | None] = mapped_column(String(50), comment="邮箱地址")
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="姓名")
nickname: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="昵称")
password: Mapped[str] = mapped_column(String(255), nullable=True, comment="密码")
gender: Mapped[str | None] = mapped_column(String(8), nullable=True, comment="性别")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可用")
is_reset_password: Mapped[bool] = mapped_column(
Boolean,
default=False,
comment="是否已经重置密码,没有重置的,登陆系统后必须重置密码"
)
last_ip: Mapped[str | None] = mapped_column(String(50), comment="最后一次登录IP")
last_login: Mapped[datetime | None] = mapped_column(DateTime, comment="最近一次登录时间")
is_staff: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否为工作人员")
wx_server_openid: Mapped[str | None] = mapped_column(String(255), comment="服务端微信平台openid")
is_wx_server_openid: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否已有服务端微信平台openid")
roles: Mapped[set[VadminRole]] = relationship(secondary=vadmin_auth_user_roles)
depts: Mapped[set[VadminDept]] = relationship(secondary=vadmin_auth_user_depts)
@staticmethod
def get_password_hash(password: str) -> str:
"""
生成哈希密码
:param password: 原始密码
:return: 哈希密码
"""
return pwd_context.hash(password)
@staticmethod
def verify_password(password: str, hashed_password: str) -> bool:
"""
验证原始密码是否与哈希密码一致
:param password: 原始密码
:param hashed_password: 哈希密码
:return:
"""
return pwd_context.verify(password, hashed_password)
def is_admin(self) -> bool:
"""
获取该用户是否拥有最高权限
以最高权限为准
:return:
"""
return any([i.is_admin for i in self.roles])

View File

@ -0,0 +1,3 @@
from .user import UserParams
from .role import RoleParams
from .dept import DeptParams

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/12/18 10:19
# @File : dept.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class DeptParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
name: str | None = Query(None, title="部门名称"),
dept_key: str | None = Query(None, title="部门标识"),
disabled: bool | None = Query(None, title="是否禁用"),
params: Paging = Depends()
):
super().__init__(params)
self.name = ("like", name)
self.dept_key = ("like", dept_key)
self.disabled = disabled

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : role.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class RoleParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
name: str | None = Query(None, title="角色名称"),
role_key: str | None = Query(None, title="权限字符"),
disabled: bool | None = Query(None, title="是否禁用"),
params: Paging = Depends()
):
super().__init__(params)
self.name = ("like", name)
self.role_key = ("like", role_key)
self.disabled = disabled

View File

@ -0,0 +1,37 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : user.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class UserParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
name: str | None = Query(None, title="用户名称"),
telephone: str | None = Query(None, title="手机号"),
email: str | None = Query(None, title="邮箱"),
is_active: bool | None = Query(None, title="是否可用"),
is_staff: bool | None = Query(None, title="是否为工作人员"),
params: Paging = Depends()
):
super().__init__(params)
self.name = ("like", name)
self.telephone = ("like", telephone)
self.email = ("like", email)
self.is_active = is_active
self.is_staff = is_staff

View File

@ -0,0 +1,4 @@
from .user import UserOut, UserUpdate, User, UserIn, UserSimpleOut, ResetPwd, UserUpdateBaseInfo, UserPasswordOut
from .role import Role, RoleOut, RoleIn, RoleOptionsOut, RoleSimpleOut
from .menu import Menu, MenuSimpleOut, RouterOut, Meta, MenuTreeListOut
from .dept import Dept, DeptSimpleOut, DeptTreeListOut

View File

@ -0,0 +1,39 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/10/25 12:19
# @File : dept.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class Dept(BaseModel):
name: str
dept_key: str
disabled: bool = False
order: int | None = None
desc: str | None = None
owner: str | None = None
phone: str | None = None
email: str | None = None
parent_id: int | None = None
class DeptSimpleOut(Dept):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class DeptTreeListOut(DeptSimpleOut):
model_config = ConfigDict(from_attributes=True)
children: list[dict] = []

View File

@ -0,0 +1,66 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : role.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class Menu(BaseModel):
title: str
icon: str | None = None
component: str | None = None
redirect: str | None = None
path: str | None = None
disabled: bool = False
hidden: bool = False
order: int | None = None
perms: str | None = None
parent_id: int | None = None
menu_type: str
alwaysShow: bool | None = True
noCache: bool | None = False
class MenuSimpleOut(Menu):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class Meta(BaseModel):
title: str
icon: str | None = None
hidden: bool = False
noCache: bool | None = False
breadcrumb: bool | None = True
affix: bool | None = False
noTagsView: bool | None = False
canTo: bool | None = False
alwaysShow: bool | None = True
# 路由展示
class RouterOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str | None = None
component: str | None = None
path: str
redirect: str | None = None
meta: Meta | None = None
order: int | None = None
children: list[dict] = []
class MenuTreeListOut(MenuSimpleOut):
model_config = ConfigDict(from_attributes=True)
children: list[dict] = []

View File

@ -0,0 +1,52 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : role.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict, Field
from core.data_types import DatetimeStr
from .menu import MenuSimpleOut
from .dept import DeptSimpleOut
class Role(BaseModel):
name: str
disabled: bool = False
order: int | None = None
desc: str | None = None
data_range: int = 4
role_key: str
is_admin: bool = False
class RoleSimpleOut(Role):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class RoleOut(RoleSimpleOut):
model_config = ConfigDict(from_attributes=True)
menus: list[MenuSimpleOut] = []
depts: list[DeptSimpleOut] = []
class RoleIn(Role):
menu_ids: list[int] = []
dept_ids: list[int] = []
class RoleOptionsOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
label: str = Field(alias='name')
value: int = Field(alias='id')
disabled: bool

View File

@ -0,0 +1,98 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : user.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict, field_validator
from pydantic_core.core_schema import FieldValidationInfo
from core.data_types import Telephone, DatetimeStr, Email
from .role import RoleSimpleOut
from .dept import DeptSimpleOut
class User(BaseModel):
name: str
telephone: Telephone
email: Email | None = None
nickname: str | None = None
avatar: str | None = None
is_active: bool | None = True
is_staff: bool | None = True
gender: str | None = "0"
is_wx_server_openid: bool | None = False
class UserIn(User):
"""
创建用户
"""
role_ids: list[int] = []
dept_ids: list[int] = []
password: str | None = ""
class UserUpdateBaseInfo(BaseModel):
"""
更新用户基本信息
"""
name: str
telephone: Telephone
email: Email | None = None
nickname: str | None = None
gender: str | None = "0"
class UserUpdate(User):
"""
更新用户详细信息
"""
name: str | None = None
telephone: Telephone
email: Email | None = None
nickname: str | None = None
avatar: str | None = None
is_active: bool | None = True
is_staff: bool | None = False
gender: str | None = "0"
role_ids: list[int] = []
dept_ids: list[int] = []
class UserSimpleOut(User):
model_config = ConfigDict(from_attributes=True)
id: int
update_datetime: DatetimeStr
create_datetime: DatetimeStr
is_reset_password: bool | None = None
last_login: DatetimeStr | None = None
last_ip: str | None = None
class UserPasswordOut(UserSimpleOut):
model_config = ConfigDict(from_attributes=True)
password: str
class UserOut(UserSimpleOut):
model_config = ConfigDict(from_attributes=True)
roles: list[RoleSimpleOut] = []
depts: list[DeptSimpleOut] = []
class ResetPwd(BaseModel):
password: str
password_two: str
@field_validator('password_two')
def check_passwords_match(cls, v, info: FieldValidationInfo):
if 'password' in info.data and v != info.data['password']:
raise ValueError('两次密码不一致!')
return v

View File

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/8/8 11:02
# @File : __init__.py
# @IDE : PyCharm
# @desc : 简要说明

View File

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : current.py
# @IDE : PyCharm
# @desc : 获取认证后的信息工具
from typing import Annotated
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from apps.vadmin.auth.crud import UserDal
from apps.vadmin.auth.models import VadminUser, VadminRole
from core.exception import CustomException
from utils import status
from .validation import AuthValidation
from fastapi import Request, Depends
from application import settings
from core.database import db_getter
from .validation.auth import Auth
class OpenAuth(AuthValidation):
"""
开放认证,无认证也可以访问
认证了以后可以获取到用户信息,无认证则获取不到
"""
async def __call__(
self,
request: Request,
token: Annotated[str, Depends(settings.oauth2_scheme)],
db: AsyncSession = Depends(db_getter)
):
"""
每次调用依赖此类的接口会执行该方法
"""
if not settings.OAUTH_ENABLE:
return Auth(db=db)
try:
telephone, password = self.validate_token(request, token)
user = await UserDal(db).get_data(telephone=telephone, password=password, v_return_none=True)
return await self.validate_user(request, user, db, is_all=True)
except CustomException:
return Auth(db=db)
class AllUserAuth(AuthValidation):
"""
支持所有用户认证
获取用户基本信息
"""
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
db: AsyncSession = Depends(db_getter)
):
"""
每次调用依赖此类的接口会执行该方法
"""
if not settings.OAUTH_ENABLE:
return Auth(db=db)
telephone, password = self.validate_token(request, token)
user = await UserDal(db).get_data(telephone=telephone, password=password, v_return_none=True)
return await self.validate_user(request, user, db, is_all=True)
class FullAdminAuth(AuthValidation):
"""
只支持员工用户认证
获取员工用户完整信息
如果有权限,那么会验证该用户是否包括权限列表中的其中一个权限
"""
def __init__(self, permissions: list[str] | None = None):
if permissions:
self.permissions = set(permissions)
else:
self.permissions = None
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
db: AsyncSession = Depends(db_getter)
) -> Auth:
"""
每次调用依赖此类的接口会执行该方法
"""
if not settings.OAUTH_ENABLE:
return Auth(db=db)
telephone, password = self.validate_token(request, token)
options = [
joinedload(VadminUser.roles).subqueryload(VadminRole.menus),
joinedload(VadminUser.roles).subqueryload(VadminRole.depts),
joinedload(VadminUser.depts)
]
user = await UserDal(db).get_data(
telephone=telephone,
password=password,
v_return_none=True,
v_options=options,
is_staff=True
)
result = await self.validate_user(request, user, db, is_all=False)
permissions = self.get_user_permissions(user)
if permissions != {'*.*.*'} and self.permissions:
if not (self.permissions & permissions):
raise CustomException(msg="无权限操作", code=status.HTTP_403_FORBIDDEN)
return result

View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : views.py
# @IDE : PyCharm
# @desc : 安全认证视图
"""
JWT 表示 「JSON Web Tokens」。https://jwt.io/
它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准。
通过这种方式,你可以创建一个有效期为 1 周的令牌。然后当用户第二天使用令牌重新访问时,你知道该用户仍然处于登入状态。
一周后令牌将会过期,用户将不会通过认证,必须再次登录才能获得一个新令牌。
我们需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌pip install python-jose[cryptography]
PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。
推荐的算法是 「Bcrypt」pip install passlib[bcrypt]
"""
from datetime import timedelta
from redis.asyncio import Redis
from fastapi import APIRouter, Depends, Request, Body
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import db_getter, redis_getter
from core.exception import CustomException
from utils import status
from utils.response import SuccessResponse, ErrorResponse
from application import settings
from .login_manage import LoginManage
from .validation import LoginForm, WXLoginForm
from apps.vadmin.record.models import VadminLoginRecord
from apps.vadmin.auth.crud import MenuDal, UserDal
from apps.vadmin.auth.models import VadminUser
from .current import FullAdminAuth
from .validation.auth import Auth
from utils.wx.oauth import WXOAuth
import jwt
app = APIRouter()
@app.post("/api/login", summary="API 手机号密码登录", description="Swagger API 文档登录认证")
async def api_login_for_access_token(
request: Request,
data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(db_getter)
):
user = await UserDal(db).get_data(telephone=data.username, v_return_none=True)
error_code = status.HTTP_401_UNAUTHORIZED
if not user:
raise CustomException(status_code=error_code, code=error_code, msg="该手机号不存在")
result = VadminUser.verify_password(data.password, user.password)
if not result:
raise CustomException(status_code=error_code, code=error_code, msg="手机号或密码错误")
if not user.is_active:
raise CustomException(status_code=error_code, code=error_code, msg="此手机号已被冻结")
elif not user.is_staff:
raise CustomException(status_code=error_code, code=error_code, msg="此手机号无权限")
access_token = LoginManage.create_token({"sub": user.telephone, "password": user.password})
record = LoginForm(platform='2', method='0', telephone=data.username, password=data.password)
resp = {"access_token": access_token, "token_type": "bearer"}
await VadminLoginRecord.create_login_record(db, record, True, request, resp)
return resp
@app.post("/login", summary="手机号密码登录", description="员工登录通道限制最多输错次数达到最大值后将is_active=False")
async def login_for_access_token(
request: Request,
data: LoginForm,
manage: LoginManage = Depends(),
db: AsyncSession = Depends(db_getter)
):
try:
result = await manage.password_login(data, db, request)
if not result.status:
raise ValueError(result.msg)
access_token = LoginManage.create_token(
{"sub": result.user.telephone, "is_refresh": False, "password": result.user.password}
)
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = LoginManage.create_token(
payload={"sub": result.user.telephone, "is_refresh": True, "password": result.user.password},
expires=expires
)
resp = {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"is_reset_password": result.user.is_reset_password,
"is_wx_server_openid": result.user.is_wx_server_openid
}
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
return SuccessResponse(resp)
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
@app.post("/wx/login", summary="微信服务端一键登录", description="员工登录通道")
async def wx_login_for_access_token(
request: Request,
data: WXLoginForm,
db: AsyncSession = Depends(db_getter),
rd: Redis = Depends(redis_getter)
):
try:
if data.platform != "1" or data.method != "2":
raise ValueError("无效参数")
wx = WXOAuth(rd, 0)
telephone = await wx.parsing_phone_number(data.code)
if not telephone:
raise ValueError("无效Code")
data.telephone = telephone
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
if not user:
raise ValueError("手机号不存在")
elif not user.is_active:
raise ValueError("手机号已被冻结")
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
# 更新登录时间
await UserDal(db).update_login_info(user, request.client.host)
# 登录成功创建 token
access_token = LoginManage.create_token({"sub": user.telephone, "is_refresh": False, "password": user.password})
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = LoginManage.create_token(
payload={"sub": user.telephone, "is_refresh": True, "password": user.password},
expires=expires
)
resp = {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"is_reset_password": user.is_reset_password,
"is_wx_server_openid": user.is_wx_server_openid
}
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
return SuccessResponse(resp)
@app.get("/getMenuList", summary="获取当前用户菜单树")
async def get_menu_list(auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await MenuDal(auth.db).get_routers(auth.user))
@app.post("/token/refresh", summary="刷新Token")
async def token_refresh(refresh: str = Body(..., title="刷新Token")):
error_code = status.HTTP_401_UNAUTHORIZED
try:
payload = jwt.decode(refresh, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
is_refresh: bool = payload.get("is_refresh")
password: str = payload.get("password")
if not telephone or not is_refresh or not password:
return ErrorResponse("未认证,请您重新登录", code=error_code, status=error_code)
except jwt.exceptions.InvalidSignatureError:
return ErrorResponse("无效认证,请您重新登录", code=error_code, status=error_code)
except jwt.exceptions.ExpiredSignatureError:
return ErrorResponse("登录已超时,请您重新登录", code=error_code, status=error_code)
access_token = LoginManage.create_token({"sub": telephone, "is_refresh": False, "password": password})
expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = LoginManage.create_token(
payload={"sub": telephone, "is_refresh": True, "password": password},
expires=expires
)
resp = {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer"
}
return SuccessResponse(resp)

View File

@ -0,0 +1,62 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/8/8 11:02
# @File : auth_util.py
# @IDE : PyCharm
# @desc : 简要说明
from datetime import datetime, timedelta
from fastapi import Request
from application import settings
import jwt
from apps.vadmin.auth import models
from core.database import redis_getter
from utils.sms.code import CodeSMS
from .validation import LoginValidation, LoginForm, LoginResult
class LoginManage:
"""
登录认证工具
"""
@LoginValidation
async def password_login(self, data: LoginForm, user: models.VadminUser, **kwargs) -> LoginResult:
"""
验证用户密码
"""
result = models.VadminUser.verify_password(data.password, user.password)
if result:
return LoginResult(status=True, msg="验证成功")
return LoginResult(status=False, msg="手机号或密码错误")
@LoginValidation
async def sms_login(self, data: LoginForm, request: Request, **kwargs) -> LoginResult:
"""
验证用户短信验证码
"""
rd = redis_getter(request)
sms = CodeSMS(data.telephone, rd)
result = await sms.check_sms_code(data.password)
if result:
return LoginResult(status=True, msg="验证成功")
return LoginResult(status=False, msg="验证码错误")
@staticmethod
def create_token(payload: dict, expires: timedelta = None):
"""
创建一个生成新的访问令牌的工具函数。
pyjwthttps://github.com/jpadilla/pyjwt/blob/master/docs/usage.rst
jwt 博客https://geek-docs.com/python/python-tutorial/j_python-jwt.html
#TODO 传入的时间为UTC时间datetime.datetime类型但是在解码时获取到的是本机时间的时间戳
"""
if expires:
expire = datetime.utcnow() + expires
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
payload.update({"exp": expire})
encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt

View File

@ -0,0 +1,10 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/11/9 10:14
# @File : __init__.py.py
# @IDE : PyCharm
# @desc : 简要说明
from .auth import Auth, AuthValidation
from .login import LoginValidation, LoginForm, LoginResult, WXLoginForm

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : auth.py
# @IDE : PyCharm
# @desc : 用户凭证验证装饰器
from fastapi import Request
import jwt
from pydantic import BaseModel
from application import settings
from sqlalchemy.ext.asyncio import AsyncSession
from apps.vadmin.auth.models import VadminUser
from core.exception import CustomException
from utils import status
from datetime import timedelta, datetime
from apps.vadmin.auth.crud import UserDal
class Auth(BaseModel):
user: VadminUser = None
db: AsyncSession
data_range: int | None = None
dept_ids: list | None = []
class Config:
# 接收任意类型
arbitrary_types_allowed = True
class AuthValidation:
"""
用于用户每次调用接口时验证用户提交的token是否正确并从token中获取用户信息
"""
# status_code = 401 时表示强制要求重新登录因账号已冻结账号已过期手机号码错误刷新token无效等问题导致
# 只有 code = 401 时,表示 token 过期,要求刷新 token
# 只有 code = 错误值时,只是报错,不重新登陆
error_code = status.HTTP_401_UNAUTHORIZED
warning_code = status.HTTP_ERROR
# status_code = 403 时,表示强制要求重新登录,因无系统权限,而进入到系统访问等问题导致
@classmethod
def validate_token(cls, request: Request, token: str | None) -> tuple[str, bool]:
"""
验证用户 token
"""
if not token:
raise CustomException(
msg="请您先登录!",
code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
exp: int = payload.get("exp")
is_refresh: bool = payload.get("is_refresh")
password: bool = payload.get("password")
if not telephone or is_refresh or not password:
raise CustomException(
msg="未认证,请您重新登录",
code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN
)
# 计算当前时间 + 缓冲时间是否大于等于 JWT 过期时间
buffer_time = (datetime.now() + timedelta(minutes=settings.ACCESS_TOKEN_CACHE_MINUTES)).timestamp()
# print("过期时间", exp, datetime.fromtimestamp(exp))
# print("当前时间", buffer_time, datetime.fromtimestamp(buffer_time))
# print("剩余时间", exp - buffer_time)
if buffer_time >= exp:
request.scope["if-refresh"] = 1
else:
request.scope["if-refresh"] = 0
except (jwt.exceptions.InvalidSignatureError, jwt.exceptions.DecodeError):
raise CustomException(
msg="无效认证,请您重新登录",
code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN
)
except jwt.exceptions.ExpiredSignatureError:
raise CustomException(msg="认证已失效,请您重新登录", code=cls.error_code, status_code=cls.error_code)
return telephone, password
@classmethod
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession, is_all: bool = True) -> Auth:
"""
验证用户信息
:param request:
:param user:
:param db:
:param is_all: 是否所有人访问,不加权限
:return:
"""
if user is None:
raise CustomException(msg="未认证,请您重新登陆", code=cls.error_code, status_code=cls.error_code)
elif not user.is_active:
raise CustomException(msg="用户已被冻结!", code=cls.error_code, status_code=cls.error_code)
request.scope["telephone"] = user.telephone
request.scope["user_id"] = user.id
request.scope["user_name"] = user.name
try:
request.scope["body"] = await request.body()
except RuntimeError:
request.scope["body"] = "获取失败"
if is_all:
return Auth(user=user, db=db)
data_range, dept_ids = await cls.get_user_data_range(user, db)
return Auth(user=user, db=db, data_range=data_range, dept_ids=dept_ids)
@classmethod
def get_user_permissions(cls, user: VadminUser) -> set:
"""
获取员工用户所有权限列表
:param user: 用户实例
:return:
"""
if user.is_admin():
return {'*.*.*'}
permissions = set()
for role_obj in user.roles:
for menu in role_obj.menus:
if menu.perms and not menu.disabled:
permissions.add(menu.perms)
return permissions
@classmethod
async def get_user_data_range(cls, user: VadminUser, db: AsyncSession) -> tuple:
"""
获取用户数据范围
0 仅本人数据权限 create_user_id 查询
1 本部门数据权限 部门 id 左连接查询
2 本部门及以下数据权限 部门 id 左连接查询
3 自定义数据权限 部门 id 左连接查询
4 全部数据权限 无
:param user:
:param db:
:return:
"""
if user.is_admin():
return 4, ["*"]
data_range = max([i.data_range for i in user.roles])
dept_ids = set()
if data_range == 0:
pass
elif data_range == 1:
for dept in user.depts:
dept_ids.add(dept.id)
elif data_range == 2:
# 递归获取部门列表
dept_ids = await UserDal(db).recursion_get_dept_ids(user)
elif data_range == 3:
for role_obj in user.roles:
for dept in role_obj.depts:
dept_ids.add(dept.id)
elif data_range == 4:
dept_ids.add("*")
return data_range, list(dept_ids)

View File

@ -0,0 +1,92 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/11/9 10:15
# @File : login.py
# @IDE : PyCharm
# @desc : 登录验证装饰器
from fastapi import Request
from pydantic import BaseModel, field_validator
from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER, DEMO, REDIS_DB_ENABLE
from apps.vadmin.auth import crud, schemas
from core.database import redis_getter
from core.validator import vali_telephone
from utils.count import Count
class LoginForm(BaseModel):
telephone: str
password: str
method: str = '0' # 认证方式0密码登录1短信登录2微信一键登录
platform: str = '0' # 登录平台0PC端管理系统1移动端管理系统
# 重用验证器https://docs.pydantic.dev/dev-v2/usage/validators/#reuse-validators
normalize_telephone = field_validator('telephone')(vali_telephone)
class WXLoginForm(BaseModel):
telephone: str | None = None
code: str
method: str = '2' # 认证方式0密码登录1短信登录2微信一键登录
platform: str = '1' # 登录平台0PC端管理系统1移动端管理系统
class LoginResult(BaseModel):
status: bool | None = False
user: schemas.UserPasswordOut | None = None
msg: str | None = None
class Config:
arbitrary_types_allowed = True
class LoginValidation:
"""
验证用户登录时提交的数据是否有效
"""
def __init__(self, func):
self.func = func
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
self.result = LoginResult()
if data.platform not in ["0", "1"] or data.method not in ["0", "1"]:
self.result.msg = "无效参数"
return self.result
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True)
if not user:
self.result.msg = "该手机号不存在!"
return self.result
result = await self.func(self, data=data, user=user, request=request)
if REDIS_DB_ENABLE:
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
count = Count(redis_getter(request), count_key)
else:
count = None
if not result.status:
self.result.msg = result.msg
if not DEMO and count:
number = await count.add(ex=86400)
if number >= DEFAULT_AUTH_ERROR_MAX_NUMBER:
await count.reset()
# 如果等于最大次数,那么就将用户 is_active=False
user.is_active = False
await db.flush()
elif not user.is_active:
self.result.msg = "此手机号已被冻结!"
elif data.platform in ["0", "1"] and not user.is_staff:
self.result.msg = "此手机号无权限!"
else:
if not DEMO and count:
await count.delete()
self.result.msg = "OK"
self.result.status = True
self.result.user = schemas.UserPasswordOut.model_validate(user)
await crud.UserDal(db).update_login_info(user, request.client.host)
return self.result

307
apps/vadmin/auth/views.py Normal file
View File

@ -0,0 +1,307 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/2/24 17:02
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from redis.asyncio import Redis
from fastapi import APIRouter, Depends, Body, UploadFile, Request
from sqlalchemy.orm import joinedload
from core.database import redis_getter
from utils.response import SuccessResponse, ErrorResponse
from . import schemas, crud, models
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth, OpenAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import UserParams, RoleParams, DeptParams
app = APIRouter()
###########################################################
# 接口测试
###########################################################
@app.get("/test", summary="接口测试")
async def test(auth: Auth = Depends(OpenAuth())):
return SuccessResponse(await crud.TestDal(auth.db).relationship_where_operations_has())
###########################################################
# 用户管理
###########################################################
@app.get("/users", summary="获取用户列表")
async def get_users(
params: UserParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.list"]))
):
model = models.VadminUser
options = [joinedload(model.roles), joinedload(model.depts)]
schema = schemas.UserOut
datas, count = await crud.UserDal(auth.db).get_datas(
**params.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@app.post("/users", summary="创建用户")
async def create_user(data: schemas.UserIn, auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.create"]))):
return SuccessResponse(await crud.UserDal(auth.db).create_data(data=data))
@app.delete("/users", summary="批量删除用户", description="软删除,删除后清空所关联的角色")
async def delete_users(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.delete"]))):
if auth.user.id in ids.ids:
return ErrorResponse("不能删除当前登录用户")
elif 1 in ids.ids:
return ErrorResponse("不能删除超级管理员用户")
await crud.UserDal(auth.db).delete_datas(ids=ids.ids, v_soft=True, is_active=False)
return SuccessResponse("删除成功")
@app.put("/users/{data_id}", summary="更新用户信息")
async def put_user(
data_id: int,
data: schemas.UserUpdate,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.update"]))
):
return SuccessResponse(await crud.UserDal(auth.db).put_data(data_id, data))
@app.get("/users/{data_id}", summary="获取用户信息")
async def get_user(
data_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.view", "auth.user.update"]))
):
model = models.VadminUser
options = [joinedload(model.roles), joinedload(model.depts)]
schema = schemas.UserOut
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
@app.post("/user/current/reset/password", summary="重置当前用户密码")
async def user_current_reset_password(data: schemas.ResetPwd, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).reset_current_password(auth.user, data))
@app.post("/user/current/update/info", summary="更新当前用户基本信息")
async def post_user_current_update_info(data: schemas.UserUpdateBaseInfo, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).update_current_info(auth.user, data))
@app.post("/user/current/update/avatar", summary="更新当前用户头像")
async def post_user_current_update_avatar(file: UploadFile, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).update_current_avatar(auth.user, file))
@app.get("/user/admin/current/info", summary="获取当前管理员信息")
async def get_user_admin_current_info(auth: Auth = Depends(FullAdminAuth())):
result = schemas.UserOut.model_validate(auth.user).model_dump()
result["permissions"] = list(FullAdminAuth.get_user_permissions(auth.user))
return SuccessResponse(result)
@app.post("/user/export/query/list/to/excel", summary="导出用户查询列表为excel")
async def post_user_export_query_list(
header: list = Body(..., title="表头与对应字段"),
params: UserParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.export"]))
):
return SuccessResponse(await crud.UserDal(auth.db).export_query_list(header, params))
@app.get("/user/download/import/template", summary="下载最新批量导入用户模板")
async def get_user_download_new_import_template(auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.UserDal(auth.db).download_import_template())
@app.post("/import/users", summary="批量导入用户")
async def post_import_users(file: UploadFile, auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.import"]))):
return SuccessResponse(await crud.UserDal(auth.db).import_users(file))
@app.post("/users/init/password/send/sms", summary="初始化所选用户密码并发送通知短信")
async def post_users_init_password(
request: Request,
ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.reset"])),
rd: Redis = Depends(redis_getter)
):
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_sms(ids.ids, rd))
@app.post("/users/init/password/send/email", summary="初始化所选用户密码并发送通知邮件")
async def post_users_init_password_send_email(
request: Request,
ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.reset"])),
rd: Redis = Depends(redis_getter)
):
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_email(ids.ids, rd))
@app.put("/users/wx/server/openid", summary="更新当前用户服务端微信平台openid")
async def put_user_wx_server_openid(code: str, auth: Auth = Depends(AllUserAuth()), rd: Redis = Depends(redis_getter)):
result = await crud.UserDal(auth.db).update_wx_server_openid(code, auth.user, rd)
return SuccessResponse(result)
###########################################################
# 角色管理
###########################################################
@app.get("/roles", summary="获取角色列表")
async def get_roles(
params: RoleParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.list"]))
):
datas, count = await crud.RoleDal(auth.db).get_datas(**params.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/roles", summary="创建角色信息")
async def create_role(role: schemas.RoleIn, auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create"]))):
return SuccessResponse(await crud.RoleDal(auth.db).create_data(data=role))
@app.delete("/roles", summary="批量删除角色", description="硬删除, 如果存在用户关联则无法删除")
async def delete_roles(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.delete"]))):
if 1 in ids.ids:
return ErrorResponse("不能删除管理员角色")
await crud.RoleDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/roles/{data_id}", summary="更新角色信息")
async def put_role(
data_id: int,
data: schemas.RoleIn,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.update"]))
):
if 1 == data_id:
return ErrorResponse("不能修改管理员角色")
return SuccessResponse(await crud.RoleDal(auth.db).put_data(data_id, data))
@app.get("/roles/options", summary="获取角色选择项")
async def get_role_options(auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.create", "auth.user.update"]))):
return SuccessResponse(await crud.RoleDal(auth.db).get_select_datas())
@app.get("/roles/{data_id}", summary="获取角色信息")
async def get_role(
data_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.view", "auth.role.update"]))
):
model = models.VadminRole
options = [joinedload(model.menus), joinedload(model.depts)]
schema = schemas.RoleOut
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
###########################################################
# 菜单管理
###########################################################
@app.get("/menus", summary="获取菜单列表")
async def get_menus(auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.list"]))):
datas = await crud.MenuDal(auth.db).get_tree_list(mode=1)
return SuccessResponse(datas)
@app.get("/menus/tree/options", summary="获取菜单树选择项,添加/修改菜单时使用")
async def get_menus_options(auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.create", "auth.menu.update"]))):
datas = await crud.MenuDal(auth.db).get_tree_list(mode=2)
return SuccessResponse(datas)
@app.get("/menus/role/tree/options", summary="获取菜单列表树信息,角色权限使用")
async def get_menus_treeselect(
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create", "auth.role.update"]))
):
return SuccessResponse(await crud.MenuDal(auth.db).get_tree_list(mode=3))
@app.post("/menus", summary="创建菜单信息")
async def create_menu(menu: schemas.Menu, auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.create"]))):
if menu.parent_id:
menu.alwaysShow = False
return SuccessResponse(await crud.MenuDal(auth.db).create_data(data=menu))
@app.delete("/menus", summary="批量删除菜单", description="硬删除, 如果存在角色关联则无法删除")
async def delete_menus(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.delete"]))):
await crud.MenuDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/menus/{data_id}", summary="更新菜单信息")
async def put_menus(
data_id: int,
data: schemas.Menu, auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.update"]))
):
return SuccessResponse(await crud.MenuDal(auth.db).put_data(data_id, data))
@app.get("/menus/{data_id}", summary="获取菜单信息")
async def get_menus(
data_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
):
schema = schemas.MenuSimpleOut
return SuccessResponse(await crud.MenuDal(auth.db).get_data(data_id, v_schema=schema))
@app.get("/role/menus/tree/{role_id}", summary="获取菜单列表树信息以及角色菜单权限ID角色权限使用")
async def get_role_menu_tree(
role_id: int,
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.create", "auth.role.update"]))
):
tree_data = await crud.MenuDal(auth.db).get_tree_list(mode=3)
role_menu_tree = await crud.RoleDal(auth.db).get_role_menu_tree(role_id)
return SuccessResponse({"role_menu_tree": role_menu_tree, "menus": tree_data})
###########################################################
# 部门管理
###########################################################
@app.get("/depts", summary="获取部门列表")
async def get_depts(
params: DeptParams = Depends(),
auth: Auth = Depends(FullAdminAuth())
):
datas = await crud.DeptDal(auth.db).get_tree_list(1)
return SuccessResponse(datas)
@app.get("/dept/tree/options", summary="获取部门树选择项,添加/修改部门时使用")
async def get_dept_options(auth: Auth = Depends(FullAdminAuth())):
datas = await crud.DeptDal(auth.db).get_tree_list(mode=2)
return SuccessResponse(datas)
@app.get("/dept/user/tree/options", summary="获取部门树选择项,添加/修改用户时使用")
async def get_dept_treeselect(auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.DeptDal(auth.db).get_tree_list(mode=3))
@app.post("/depts", summary="创建部门信息")
async def create_dept(data: schemas.Dept, auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.DeptDal(auth.db).create_data(data=data))
@app.delete("/depts", summary="批量删除部门", description="硬删除, 如果存在用户关联则无法删除")
async def delete_depts(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth())):
await crud.DeptDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/depts/{data_id}", summary="更新部门信息")
async def put_dept(
data_id: int,
data: schemas.Dept,
auth: Auth = Depends(FullAdminAuth())
):
return SuccessResponse(await crud.DeptDal(auth.db).put_data(data_id, data))

View File

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023-02-15 20:03:49
# @File : __init__.py
# @IDE : PyCharm
# @desc : 帮助中心

37
apps/vadmin/help/crud.py Normal file
View File

@ -0,0 +1,37 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023-02-15 20:03:49
# @File : crud.py
# @IDE : PyCharm
# @desc : 帮助中心 - 增删改查
from sqlalchemy.ext.asyncio import AsyncSession
from core.crud import DalBase
from . import models, schemas
class IssueDal(DalBase):
def __init__(self, db: AsyncSession):
super(IssueDal, self).__init__()
self.db = db
self.model = models.VadminIssue
self.schema = schemas.IssueSimpleOut
async def add_view_number(self, data_id: int) -> None:
"""
更新常见问题查看次数+1
"""
obj: models.VadminIssue = await self.get_data(data_id)
obj.view_number = obj.view_number + 1 if obj.view_number else 1
await self.flush(obj)
class IssueCategoryDal(DalBase):
def __init__(self, db: AsyncSession):
super(IssueCategoryDal, self).__init__()
self.db = db
self.model = models.VadminIssueCategory
self.schema = schemas.IssueCategorySimpleOut

View File

@ -0,0 +1,10 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023-02-15 20:03:49
# @File : __init__.py
# @IDE : PyCharm
# @desc : 初始化文件
from .issue import VadminIssue, VadminIssueCategory

View File

@ -0,0 +1,54 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : issue.py
# @IDE : PyCharm
# @desc : 常见问题
from sqlalchemy.orm import relationship, Mapped, mapped_column
from apps.vadmin.auth.models import VadminUser
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey, Text
class VadminIssueCategory(BaseModel):
__tablename__ = "vadmin_help_issue_category"
__table_args__ = ({'comment': '常见问题类别表'})
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="类别名称")
platform: Mapped[str] = mapped_column(String(8), index=True, nullable=False, comment="展示平台")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见")
issues: Mapped[list["VadminIssue"]] = relationship(back_populates='category')
create_user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_auth_user.id", ondelete='RESTRICT'),
comment="创建人"
)
create_user: Mapped[VadminUser] = relationship(foreign_keys=create_user_id)
class VadminIssue(BaseModel):
__tablename__ = "vadmin_help_issue"
__table_args__ = ({'comment': '常见问题记录表'})
category_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_help_issue_category.id", ondelete='CASCADE'),
comment="类别"
)
category: Mapped[list["VadminIssueCategory"]] = relationship(foreign_keys=category_id, back_populates='issues')
title: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="标题")
content: Mapped[str] = mapped_column(Text, comment="内容")
view_number: Mapped[int] = mapped_column(Integer, default=0, comment="查看次数")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见")
create_user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_auth_user.id", ondelete='RESTRICT'),
comment="创建人"
)
create_user: Mapped[VadminUser] = relationship(foreign_keys=create_user_id)

View File

@ -0,0 +1,10 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023-02-15 20:03:49
# @File : __init__.py
# @IDE : PyCharm
# @desc : 初始化文件
from .issue import IssueParams, IssueCategoryParams

View File

@ -0,0 +1,51 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : issue.py
# @IDE : PyCharm
# @desc : 常见问题
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class IssueParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
params: Paging = Depends(),
is_active: bool = None,
title: str = None,
category_id: int = None
):
super().__init__(params)
self.v_order = "desc"
self.v_order_field = "create_datetime"
self.is_active = is_active
self.category_id = category_id
self.title = ("like", title)
class IssueCategoryParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
params: Paging = Depends(),
is_active: bool = None,
platform: str = None,
name: str = None
):
super().__init__(params)
self.v_order = "desc"
self.v_order_field = "create_datetime"
self.is_active = is_active
self.platform = platform
self.name = ("like", name)

View File

@ -0,0 +1,12 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023-02-15 20:03:49
# @File : __init__.py
# @IDE : PyCharm
# @desc : 初始化文件
from .issue import Issue, IssueSimpleOut, IssueListOut
from .issue_category import IssueCategory, IssueCategorySimpleOut, IssueCategoryListOut, IssueCategoryOptionsOut
from .issue_m2m import IssueCategoryPlatformOut

View File

@ -0,0 +1,38 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : issue.py
# @IDE : PyCharm
# @desc : 常见问题
from typing import Optional
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
from apps.vadmin.auth.schemas import UserSimpleOut
from .issue_category import IssueCategorySimpleOut
class Issue(BaseModel):
category_id: int | None = None
create_user_id: int | None = None
title: str | None = None
content: str | None = None
view_number: int | None = None
is_active: bool | None = None
class IssueSimpleOut(Issue):
model_config = ConfigDict(from_attributes=True)
id: int
update_datetime: DatetimeStr
create_datetime: DatetimeStr
class IssueListOut(IssueSimpleOut):
model_config = ConfigDict(from_attributes=True)
create_user: UserSimpleOut
category: IssueCategorySimpleOut

View File

@ -0,0 +1,43 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : issue_category.py
# @IDE : PyCharm
# @desc : 常见问题类别
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
from apps.vadmin.auth.schemas import UserSimpleOut
class IssueCategory(BaseModel):
name: str | None = None
platform: str | None = None
is_active: bool | None = None
create_user_id: int | None = None
class IssueCategorySimpleOut(IssueCategory):
model_config = ConfigDict(from_attributes=True)
id: int
update_datetime: DatetimeStr
create_datetime: DatetimeStr
class IssueCategoryListOut(IssueCategorySimpleOut):
model_config = ConfigDict(from_attributes=True)
create_user: UserSimpleOut
class IssueCategoryOptionsOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
label: str = Field(alias='name')
value: int = Field(alias='id')

View File

@ -0,0 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/2/17 15:18
# @File : issue_m2m.py.py
# @IDE : PyCharm
# @desc : 简要说明
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
from .issue import IssueSimpleOut
class IssueCategoryPlatformOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str | None = None
platform: str | None = None
is_active: bool | None = None
create_user_id: int | None = None
id: int
update_datetime: DatetimeStr
create_datetime: DatetimeStr
issues: list[IssueSimpleOut] = None

125
apps/vadmin/help/views.py Normal file
View File

@ -0,0 +1,125 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023-02-15 20:03:49
# @File : views.py
# @IDE : PyCharm
# @desc : 帮助中心视图
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from core.database import db_getter
from utils.response import SuccessResponse
from . import schemas, crud, params, models
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth
from apps.vadmin.auth.utils.validation.auth import Auth
app = APIRouter()
###########################################################
# 类别管理
###########################################################
@app.get("/issue/categorys", summary="获取类别列表")
async def get_issue_categorys(p: params.IssueCategoryParams = Depends(), auth: Auth = Depends(AllUserAuth())):
model = models.VadminIssueCategory
options = [joinedload(model.create_user)]
schema = schemas.IssueCategoryListOut
datas, count = await crud.IssueCategoryDal(auth.db).get_datas(
**p.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@app.get("/issue/categorys/options", summary="获取类别选择项")
async def get_issue_categorys_options(auth: Auth = Depends(AllUserAuth())):
schema = schemas.IssueCategoryOptionsOut
return SuccessResponse(await crud.IssueCategoryDal(auth.db).get_datas(limit=0, is_active=True, v_schema=schema))
@app.post("/issue/categorys", summary="创建类别")
async def create_issue_category(data: schemas.IssueCategory, auth: Auth = Depends(AllUserAuth())):
data.create_user_id = auth.user.id
return SuccessResponse(await crud.IssueCategoryDal(auth.db).create_data(data=data))
@app.delete("/issue/categorys", summary="批量删除类别", description="硬删除")
async def delete_issue_categorys(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.IssueCategoryDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/issue/categorys/{data_id}", summary="更新类别信息")
async def put_issue_category(data_id: int, data: schemas.IssueCategory, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.IssueCategoryDal(auth.db).put_data(data_id, data))
@app.get("/issue/categorys/{data_id}", summary="获取类别信息")
async def get_issue_category(data_id: int, auth: Auth = Depends(AllUserAuth())):
schema = schemas.IssueCategorySimpleOut
return SuccessResponse(await crud.IssueCategoryDal(auth.db).get_data(data_id, v_schema=schema))
@app.get("/issue/categorys/platform/{platform}", summary="获取平台中的常见问题类别列表")
async def get_issue_category_platform(platform: str, db: AsyncSession = Depends(db_getter)):
model = models.VadminIssueCategory
options = [joinedload(model.issues)]
schema = schemas.IssueCategoryPlatformOut
result = await crud.IssueCategoryDal(db).get_datas(
limit=0,
platform=platform,
is_active=True,
v_schema=schema,
v_options=options
)
return SuccessResponse(result)
###########################################################
# 问题管理
###########################################################
@app.get("/issues", summary="获取问题列表")
async def get_issues(p: params.IssueParams = Depends(), auth: Auth = Depends(AllUserAuth())):
model = models.VadminIssue
options = [joinedload(model.create_user), joinedload(model.category)]
schema = schemas.IssueListOut
datas, count = await crud.IssueDal(auth.db).get_datas(
**p.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@app.post("/issues", summary="创建问题")
async def create_issue(data: schemas.Issue, auth: Auth = Depends(AllUserAuth())):
data.create_user_id = auth.user.id
return SuccessResponse(await crud.IssueDal(auth.db).create_data(data=data))
@app.delete("/issues", summary="批量删除问题", description="硬删除")
async def delete_issues(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.IssueDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/issues/{data_id}", summary="更新问题信息")
async def put_issue(data_id: int, data: schemas.Issue, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.IssueDal(auth.db).put_data(data_id, data))
@app.get("/issues/{data_id}", summary="获取问题信息")
async def get_issue(data_id: int, db: AsyncSession = Depends(db_getter)):
schema = schemas.IssueSimpleOut
return SuccessResponse(await crud.IssueDal(db).get_data(data_id, v_schema=schema))
@app.get("/issues/add/view/number/{data_id}", summary="更新常见问题查看次数+1")
async def issue_add_view_number(data_id: int, db: AsyncSession = Depends(db_getter)):
return SuccessResponse(await crud.IssueDal(db).add_view_number(data_id))

View File

View File

@ -0,0 +1,88 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:18
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据库 增删改查操作
import random
from motor.motor_asyncio import AsyncIOMotorDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from . import models, schemas
from core.crud import DalBase
from core.mongo_manage import MongoManage
class LoginRecordDal(DalBase):
def __init__(self, db: AsyncSession):
super(LoginRecordDal, self).__init__()
self.db = db
self.model = models.VadminLoginRecord
self.schema = schemas.LoginRecordSimpleOut
async def get_user_distribute(self) -> list[dict]:
"""
获取用户登录分布情况
高德经纬度查询https://lbs.amap.com/tools/picker
{
name: '北京',
center: [116.407394, 39.904211],
total: 20
}
:return: List[dict]
"""
result = [{
"name": '北京',
"center": [116.407394, 39.904211],
},
{
"name": '重庆',
"center": [106.551643, 29.562849],
},
{
"name": '郑州',
"center": [113.778584, 34.759197],
},
{
"name": '南京',
"center": [118.796624, 32.059344],
},
{
"name": '武汉',
"center": [114.304569, 30.593354],
},
{
"name": '乌鲁木齐',
"center": [87.616824, 43.825377],
},
{
"name": '新乡',
"center": [113.92679, 35.303589],
}]
for data in result:
assert isinstance(data, dict)
data["total"] = random.randint(2, 80)
return result
class SMSSendRecordDal(DalBase):
def __init__(self, db: AsyncSession):
super(SMSSendRecordDal, self).__init__()
self.db = db
self.model = models.VadminSMSSendRecord
self.schema = schemas.SMSSendRecordSimpleOut
class OperationRecordDal(MongoManage):
def __init__(self, db: AsyncIOMotorDatabase):
super(OperationRecordDal, self).__init__()
self.db = db
self.collection = db["operation_record"]
self.schema = schemas.OperationRecordSimpleOut
self.is_object_id = True

View File

@ -0,0 +1,11 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/2/14 21:11
# @File : __init__.py.py
# @IDE : PyCharm
# @desc : xpath例子
from .login import VadminLoginRecord
from .sms import VadminSMSSendRecord

View File

@ -0,0 +1,86 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : login.py
# @IDE : PyCharm
# @desc : 登录记录模型
import json
from sqlalchemy.orm import Mapped, mapped_column
from application.settings import LOGIN_LOG_RECORD
from apps.vadmin.auth.utils.validation import LoginForm, WXLoginForm
from utils.ip_manage import IPManage
from sqlalchemy.ext.asyncio import AsyncSession
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Text
from fastapi import Request
from starlette.requests import Request as StarletteRequest
from user_agents import parse
class VadminLoginRecord(BaseModel):
__tablename__ = "vadmin_record_login"
__table_args__ = ({'comment': '登录记录表'})
telephone: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="手机号")
status: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否登录成功")
platform: Mapped[str] = mapped_column(String(8), comment="登陆平台")
login_method: Mapped[str] = mapped_column(String(8), comment="认证方式")
ip: Mapped[str | None] = mapped_column(String(50), comment="登陆地址")
address: Mapped[str | None] = mapped_column(String(255), comment="登陆地点")
country: Mapped[str | None] = mapped_column(String(255), comment="国家")
province: Mapped[str | None] = mapped_column(String(255), comment="")
city: Mapped[str | None] = mapped_column(String(255), comment="城市")
county: Mapped[str | None] = mapped_column(String(255), comment="区/县")
operator: Mapped[str | None] = mapped_column(String(255), comment="运营商")
postal_code: Mapped[str | None] = mapped_column(String(255), comment="邮政编码")
area_code: Mapped[str | None] = mapped_column(String(255), comment="地区区号")
browser: Mapped[str | None] = mapped_column(String(50), comment="浏览器")
system: Mapped[str | None] = mapped_column(String(50), comment="操作系统")
response: Mapped[str | None] = mapped_column(Text, comment="响应信息")
request: Mapped[str | None] = mapped_column(Text, comment="请求信息")
@classmethod
async def create_login_record(
cls,
db: AsyncSession,
data: LoginForm | WXLoginForm,
status: bool,
req: Request | StarletteRequest,
resp: dict
):
"""
创建登录记录
:return:
"""
if not LOGIN_LOG_RECORD:
return None
header = {}
for k, v in req.headers.items():
header[k] = v
if isinstance(req, StarletteRequest):
form = (await req.form()).multi_items()
params = json.dumps({"form": form, "headers": header})
else:
body = json.loads((await req.body()).decode())
params = json.dumps({"body": body, "headers": header})
user_agent = parse(req.headers.get("user-agent"))
system = f"{user_agent.os.family} {user_agent.os.version_string}"
browser = f"{user_agent.browser.family} {user_agent.browser.version_string}"
ip = IPManage(req.client.host)
location = await ip.parse()
obj = VadminLoginRecord(
**location.dict(),
telephone=data.telephone if data.telephone else data.code,
status=status,
browser=browser,
system=system,
response=json.dumps(resp),
request=params,
platform=data.platform,
login_method=data.method
)
db.add(obj)
await db.flush()

View File

@ -0,0 +1,23 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/3/21 17:36
# @File : sms.py
# @IDE : PyCharm
# @desc : 短信发送记录模型
from sqlalchemy.orm import Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Integer, String, Boolean, ForeignKey
class VadminSMSSendRecord(BaseModel):
__tablename__ = "vadmin_record_sms_send"
__table_args__ = ({'comment': '短信发送记录表'})
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("vadmin_auth_user.id", ondelete='CASCADE'), comment="操作人")
status: Mapped[bool] = mapped_column(Boolean, default=True, comment="发送状态")
content: Mapped[str] = mapped_column(String(255), comment="发送内容")
telephone: Mapped[str] = mapped_column(String(11), comment="目标手机号")
desc: Mapped[str | None] = mapped_column(String(255), comment="失败描述")
scene: Mapped[str | None] = mapped_column(String(50), comment="发送场景")

View File

@ -0,0 +1,3 @@
from .login import LoginParams
from .operation import OperationParams
from .sms import SMSParams

View File

@ -0,0 +1,35 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : login.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class LoginParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
ip: str = None,
address: str = None,
telephone: str = None,
status: bool = None,
platform: str = None,
params: Paging = Depends()
):
super().__init__(params)
self.ip = ("like", ip)
self.telephone = ("like", telephone)
self.address = ("like", address)
self.status = status
self.platform = platform
self.v_order = "desc"

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : operation.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class OperationParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
summary: str = None,
telephone: str = None,
request_method: str = None,
params: Paging = Depends()
):
super().__init__(params)
self.summary = ("like", summary)
self.telephone = ("like", telephone)
self.request_method = request_method
self.v_order = "desc"

View File

@ -0,0 +1,22 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : sms.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class SMSParams(QueryParams):
"""
列表分页
"""
def __init__(self, telephone: str = None, params: Paging = Depends()):
super().__init__(params)
self.telephone = ("like", telephone)

View File

@ -0,0 +1,3 @@
from .login import LoginRecord, LoginRecordSimpleOut
from .sms import SMSSendRecord, SMSSendRecordSimpleOut
from .operation import OperationRecord, OperationRecordSimpleOut

View File

@ -0,0 +1,38 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : login.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class LoginRecord(BaseModel):
telephone: str
status: bool
ip: str | None = None
address: str | None = None
browser: str | None = None
system: str | None = None
response: str | None = None
request: str | None = None
postal_code: str | None = None
area_code: str | None = None
country: str | None = None
province: str | None = None
city: str | None = None
county: str | None = None
operator: str | None = None
platform: str | None = None
login_method: str | None = None
class LoginRecordSimpleOut(LoginRecord):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr

View File

@ -0,0 +1,35 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : operation.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class OperationRecord(BaseModel):
telephone: str | None = None
user_id: int | None = None
user_name: str | None = None
status_code: int | None = None
client_ip: str | None = None
request_method: str | None = None
api_path: str | None = None
system: str | None = None
browser: str | None = None
summary: str | None = None
route_name: str | None = None
description: str | None = None
tags: list[str] | None = None
process_time: float | None = None
params: str | None = None
class OperationRecordSimpleOut(OperationRecord):
model_config = ConfigDict(from_attributes=True)
create_datetime: DatetimeStr

View File

@ -0,0 +1,28 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/3/21 17:54
# @File : sms.py
# @IDE : PyCharm
# @desc : 简要说明
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class SMSSendRecord(BaseModel):
telephone: str
status: bool = True
user_id: int | None = None
content: str | None = None
desc: str | None = None
scene: str | None = None
class SMSSendRecordSimpleOut(SMSSendRecord):
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
model_config = ConfigDict(from_attributes=True)

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : views.py
# @IDE : PyCharm
# @desc : 主要接口文件
from fastapi import APIRouter, Depends
from motor.motor_asyncio import AsyncIOMotorDatabase
from utils.response import SuccessResponse
from . import crud
from apps.vadmin.auth.utils.current import AllUserAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import LoginParams, OperationParams, SMSParams
from core.database import mongo_getter
app = APIRouter()
###########################################################
# 日志管理
###########################################################
@app.get("/logins", summary="获取登录日志列表")
async def get_record_login(p: LoginParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.LoginRecordDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.get("/operations", summary="获取操作日志列表")
async def get_record_operation(
p: OperationParams = Depends(),
db: AsyncIOMotorDatabase = Depends(mongo_getter),
auth: Auth = Depends(AllUserAuth())
):
count = await crud.OperationRecordDal(db).get_count(**p.to_count())
datas = await crud.OperationRecordDal(db).get_datas(**p.dict())
return SuccessResponse(datas, count=count)
@app.get("/sms/send/list", summary="获取短信发送列表")
async def get_sms_send_list(p: SMSParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.SMSSendRecordDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
###########################################################
# 日志分析
###########################################################
@app.get("/analysis/user/login/distribute", summary="获取用户登录分布情况列表")
async def get_user_login_distribute(auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.LoginRecordDal(auth.db).get_user_distribute())

View File

View File

@ -0,0 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 13:15
# @File : crud.py
# @IDE : PyCharm
# @desc : 简要说明
from sqlalchemy.ext.asyncio import AsyncSession
from core.crud import DalBase
from . import models, schemas
class ImagesDal(DalBase):
def __init__(self, db: AsyncSession):
super(ImagesDal, self).__init__()
self.db = db
self.model = models.VadminImages
self.schema = schemas.ImagesSimpleOut

View File

@ -0,0 +1 @@
from .images import VadminImages

View File

@ -0,0 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 13:41
# @File : images.py
# @IDE : PyCharm
# @desc : 图片素材表
from sqlalchemy.orm import relationship, Mapped, mapped_column
from apps.vadmin.auth.models import VadminUser
from db.db_base import BaseModel
from sqlalchemy import String, ForeignKey, Integer
class VadminImages(BaseModel):
__tablename__ = "vadmin_resource_images"
__table_args__ = ({'comment': '图片素材表'})
filename: Mapped[str] = mapped_column(String(255), nullable=False, comment="原图片名称")
image_url: Mapped[str] = mapped_column(String(500), nullable=False, comment="图片链接")
create_user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_auth_user.id", ondelete='RESTRICT'),
comment="创建人"
)
create_user: Mapped[VadminUser] = relationship(foreign_keys=create_user_id)

View File

@ -0,0 +1 @@
from .images import ImagesParams

View File

@ -0,0 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 14:59
# @File : images.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ImagesParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
filename: str = None,
params: Paging = Depends()
):
super().__init__(params)
self.filename = ('like', filename)
self.v_order = "desc"
self.v_order_field = "create_datetime"

View File

@ -0,0 +1 @@
from .images import Images, ImagesOut, ImagesSimpleOut

View File

@ -0,0 +1,33 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 14:49
# @File : images.py
# @IDE : PyCharm
# @desc : 简要说明
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
from apps.vadmin.auth.schemas import UserSimpleOut
class Images(BaseModel):
filename: str
image_url: str
create_user_id: int
class ImagesSimpleOut(Images):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class ImagesOut(ImagesSimpleOut):
model_config = ConfigDict(from_attributes=True)
create_user: UserSimpleOut

View File

@ -0,0 +1,60 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 9:29
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import APIRouter, Depends, UploadFile
from sqlalchemy.orm import joinedload
from core.dependencies import IdList
from utils.file.aliyun_oss import AliyunOSS, BucketConf
from utils.response import SuccessResponse
from . import schemas, crud, params, models
from apps.vadmin.auth.utils.current import FullAdminAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from application.settings import ALIYUN_OSS
app = APIRouter()
###########################################################
# 图片资源管理
###########################################################
@app.get("/images", summary="获取图片列表")
async def get_images_list(p: params.ImagesParams = Depends(), auth: Auth = Depends(FullAdminAuth())):
model = models.VadminImages
v_options = [joinedload(model.create_user)]
v_schema = schemas.ImagesOut
datas, count = await crud.ImagesDal(auth.db).get_datas(
**p.dict(),
v_options=v_options,
v_schema=v_schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@app.post("/images", summary="创建图片")
async def create_images(file: UploadFile, auth: Auth = Depends(FullAdminAuth())):
filepath = f"/resource/images/"
result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_image(filepath, file)
data = schemas.Images(
filename=file.filename,
image_url=result,
create_user_id=auth.user.id
)
return SuccessResponse(await crud.ImagesDal(auth.db).create_data(data=data))
@app.delete("/images", summary="删除图片", description="硬删除")
async def delete_images(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth())):
await crud.ImagesDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.get("/images/{data_id}", summary="获取图片信息")
async def get_images(data_id: int, auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.ImagesDal(auth.db).get_data(data_id, v_schema=schemas.ImagesSimpleOut))

View File

508
apps/vadmin/system/crud.py Normal file
View File

@ -0,0 +1,508 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:18
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据库 增删改查操作
import json
import os
from enum import Enum
from typing import Any
from redis.asyncio import Redis
from fastapi.encoders import jsonable_encoder
from motor.motor_asyncio import AsyncIOMotorDatabase
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from application.settings import STATIC_ROOT, SUBSCRIBE, REDIS_DB_ENABLE
from core.database import redis_getter
from core.mongo_manage import MongoManage
from utils.file.file_manage import FileManage
from . import models, schemas
from core.crud import DalBase
from core.exception import CustomException
from utils import status
from fastapi import Request
class DictTypeDal(DalBase):
def __init__(self, db: AsyncSession):
super(DictTypeDal, self).__init__()
self.db = db
self.model = models.VadminDictType
self.schema = schemas.DictTypeSimpleOut
async def get_dicts_details(self, dict_types: list[str]) -> dict:
"""
获取多个字典类型下的字典元素列表
"""
data = {}
options = [joinedload(self.model.details)]
objs = await DictTypeDal(self.db).get_datas(
limit=0,
v_return_objs=True,
v_options=options,
dict_type=("in", dict_types)
)
for obj in objs:
if not obj:
data[obj.dict_type] = []
continue
else:
data[obj.dict_type] = [schemas.DictDetailsSimpleOut.model_validate(i).model_dump() for i in obj.details]
return data
async def get_select_datas(self) -> list:
"""获取选择数据,全部数据"""
sql = select(self.model)
queryset = await self.db.execute(sql)
return [schemas.DictTypeOptionsOut.model_validate(i).model_dump() for i in queryset.scalars().all()]
class DictDetailsDal(DalBase):
def __init__(self, db: AsyncSession):
super(DictDetailsDal, self).__init__()
self.db = db
self.model = models.VadminDictDetails
self.schema = schemas.DictDetailsSimpleOut
class SettingsDal(DalBase):
def __init__(self, db: AsyncSession):
super(SettingsDal, self).__init__()
self.db = db
self.model = models.VadminSystemSettings
self.schema = schemas.SettingsSimpleOut
async def get_tab_values(self, tab_id: int) -> dict:
"""
获取系统配置标签下的信息
"""
datas = await self.get_datas(limit=0, tab_id=tab_id, v_return_objs=True)
result = {}
for data in datas:
if not data.disabled:
result[data.config_key] = data.config_value
return result
async def update_datas(self, datas: dict, request: Request) -> None:
"""
更新系统配置信息
更新ico图标步骤先将文件上传到本地然后点击提交后获取到文件地址将上传的新文件覆盖原有文件
原因ico图标的路径是在前端的index.html中固定的所以目前只能改变图片不改变路径
"""
for key, value in datas.items():
if key == "web_ico":
continue
elif key == "web_ico_local_path":
if not value:
continue
ico = await self.get_data(config_key="web_ico", tab_id=1)
web_ico = datas.get("web_ico")
if ico.config_value == web_ico:
continue
# 将上传的ico路径替换到static/system/favicon.ico文件
await FileManage.async_copy_file(value, os.path.join(STATIC_ROOT, "system/favicon.ico"))
sql = update(self.model).where(self.model.config_key == "web_ico").values(config_value=web_ico)
await self.db.execute(sql)
else:
sql = update(self.model).where(self.model.config_key == str(key)).values(config_value=value)
await self.db.execute(sql)
if "wx_server_app_id" in datas and REDIS_DB_ENABLE:
rd = redis_getter(request)
await rd.client().set("wx_server", json.dumps(datas))
elif "sms_access_key" in datas and REDIS_DB_ENABLE:
rd = redis_getter(request)
await rd.client().set('aliyun_sms', json.dumps(datas))
async def get_base_config(self) -> dict:
"""
获取系统基本信息
"""
ignore_configs = ["wx_server_app_id", "wx_server_app_secret"]
datas = await self.get_datas(limit=0, tab_id=("in", ["1", "9"]), disabled=False, v_return_objs=True)
result = {}
for config in datas:
if config.config_key not in ignore_configs:
result[config.config_key] = config.config_value
return result
class SettingsTabDal(DalBase):
def __init__(self, db: AsyncSession):
super(SettingsTabDal, self).__init__(db, models.VadminSystemSettingsTab, schemas.SettingsTabSimpleOut)
async def get_classify_tab_values(self, classify: list[str], hidden: bool | None = False) -> dict:
"""
获取系统配置分类下的标签信息
"""
model = models.VadminSystemSettingsTab
options = [joinedload(model.settings)]
datas = await self.get_datas(
limit=0,
v_options=options,
classify=("in", classify),
disabled=False,
v_return_objs=True,
hidden=hidden
)
return self.__generate_values(datas)
async def get_tab_name_values(self, tab_names: list[str], hidden: bool | None = False) -> dict:
"""
获取系统配置标签下的标签信息
"""
model = models.VadminSystemSettingsTab
options = [joinedload(model.settings)]
datas = await self.get_datas(
limit=0,
v_options=options,
tab_name=("in", tab_names),
disabled=False,
v_return_objs=True,
hidden=hidden
)
return self.__generate_values(datas)
@classmethod
def __generate_values(cls, datas: list[models.VadminSystemSettingsTab]) -> dict:
"""
生成字典值
"""
return {
tab.tab_name: {
item.config_key: item.config_value
for item in tab.settings
if not item.disabled
}
for tab in datas
}
class TaskDal(MongoManage):
class JobOperation(Enum):
add = "add_job"
def __init__(self, db: AsyncIOMotorDatabase):
super(TaskDal, self).__init__(db, "vadmin_system_task", schemas.TaskSimpleOut)
async def get_task(
self,
_id: str = None,
v_return_none: bool = False,
v_schema: Any = None,
**kwargs
) -> dict | None:
"""
获取单个数据,默认使用 ID 查询,否则使用关键词查询
包括临时字段 last_run_datetimeis_active
is_active: 只有在 scheduler_task_jobs 任务运行表中存在相同 _id 才表示任务添加成功,任务状态才为 True
last_run_datetime: 在 scheduler_task_record 中获取该任务最近一次执行完成的时间
:param _id: 数据 ID
:param v_return_none: 是否返回空 None否则抛出异常默认抛出异常
:param v_schema: 指定使用的序列化对象
"""
if _id:
kwargs["_id"] = ("ObjectId", _id)
params = self.filter_condition(**kwargs)
pipeline = [
{
'$addFields': {
'str_id': {'$toString': '$_id'}
}
},
{
'$lookup': {
'from': 'scheduler_task_jobs',
'localField': 'str_id',
'foreignField': '_id',
'as': 'matched_jobs'
}
},
{
'$lookup': {
'from': 'scheduler_task_record',
'localField': 'str_id',
'foreignField': 'job_id',
'as': 'matched_records'
}
},
{
'$addFields': {
'is_active': {
'$cond': {
'if': {'$ne': ['$matched_jobs', []]},
'then': True,
'else': False
}
},
'last_run_datetime': {
'$ifNull': [
{'$arrayElemAt': ['$matched_records.create_datetime', -1]},
None
]
}
}
},
{
'$business': {
'matched_records': 0,
'matched_jobs': 0
}
},
{
'$match': params
},
{
'$facet': {
'documents': [
{'$limit': 1},
]
}
}
]
# 执行聚合查询
cursor = self.collection.aggregate(pipeline)
result = await cursor.to_list(length=None)
data = result[0]['documents']
if not data and v_return_none:
return None
elif not data:
raise CustomException("未查找到对应数据", code=status.HTTP_404_NOT_FOUND)
data = data[0]
if data and v_schema:
return jsonable_encoder(v_schema(**data))
return data
async def get_tasks(
self,
page: int = 1,
limit: int = 10,
v_schema: Any = None,
v_order: str = None,
v_order_field: str = None,
**kwargs
) -> tuple:
"""
获取任务信息列表
添加了两个临时字段
is_active: 只有在 scheduler_task_jobs 任务运行表中存在相同 _id 才表示任务添加成功,任务状态才为 True
last_run_datetime: 在 scheduler_task_record 中获取该任务最近一次执行完成的时间
"""
v_order_field = v_order_field if v_order_field else 'create_datetime'
v_order = -1 if v_order in self.ORDER_FIELD else 1
params = self.filter_condition(**kwargs)
pipeline = [
{
'$addFields': {
'str_id': {'$toString': '$_id'}
}
},
{
'$lookup': {
'from': 'scheduler_task_jobs',
'localField': 'str_id',
'foreignField': '_id',
'as': 'matched_jobs'
}
},
{
'$lookup': {
'from': 'scheduler_task_record',
'localField': 'str_id',
'foreignField': 'job_id',
'as': 'matched_records'
}
},
{
'$addFields': {
'is_active': {
'$cond': {
'if': {'$ne': ['$matched_jobs', []]},
'then': True,
'else': False
}
},
'last_run_datetime': {
'$ifNull': [
{'$arrayElemAt': ['$matched_records.create_datetime', -1]},
None
]
}
}
},
{
'$business': {
'matched_records': 0,
'matched_jobs': 0
}
},
{
'$match': params
},
{
'$facet': {
'documents': [
{'$sort': {v_order_field: v_order}},
{'$limit': limit},
{'$skip': (page - 1) * limit}
],
'count': [{'$count': 'total'}]
}
}
]
# 执行聚合查询
cursor = self.collection.aggregate(pipeline)
result = await cursor.to_list(length=None)
datas = result[0]['documents']
count = result[0]['count'][0]['total'] if result[0]['count'] else 0
if count == 0:
return [], 0
elif v_schema:
datas = [jsonable_encoder(v_schema(**data)) for data in datas]
elif self.schema:
datas = [jsonable_encoder(self.schema(**data)) for data in datas]
return datas, count
async def add_task(self, rd: Redis, data: dict) -> int:
"""
添加任务到消息队列
使用消息无保留策略:无保留是指当发送者向某个频道发送消息时,如果没有订阅该频道的调用方,就直接将该消息丢弃。
:param rd: redis 对象
:param data: 行数据字典
:return: 接收到消息的订阅者数量。
"""
exec_strategy = data.get("exec_strategy")
job_params = {
"name": data.get("_id"),
"job_class": data.get("job_class"),
"expression": data.get("expression")
}
if exec_strategy == "interval" or exec_strategy == "cron":
job_params["start_date"] = data.get("start_date")
job_params["end_date"] = data.get("end_date")
message = {
"operation": self.JobOperation.add.value,
"task": {
"exec_strategy": data.get("exec_strategy"),
"job_params": job_params
}
}
return await rd.publish(SUBSCRIBE, json.dumps(message).encode('utf-8'))
async def create_task(self, rd: Redis, data: schemas.Task) -> dict:
"""
创建任务
"""
data_dict = data.model_dump()
is_active = data_dict.pop('is_active')
insert_result = await super().create_data(data_dict)
obj = await self.get_task(insert_result.inserted_id, v_schema=schemas.TaskSimpleOut)
# 如果分组不存在则新增分组
group = await TaskGroupDal(self.db).get_data(value=data.group, v_return_none=True)
if not group:
await TaskGroupDal(self.db).create_data({"value": data.group})
result = {
"subscribe_number": 0,
"is_active": is_active
}
if is_active:
# 创建任务成功后, 如果任务状态为 True则向消息队列中发送任务
result['subscribe_number'] = await self.add_task(rd, obj)
return result
async def put_task(self, rd: Redis, _id: str, data: schemas.Task) -> dict:
"""
更新任务
"""
data_dict = data.model_dump()
is_active = data_dict.pop('is_active')
await super(TaskDal, self).put_data(_id, data)
obj: dict = await self.get_task(_id, v_schema=schemas.TaskSimpleOut)
# 如果分组不存在则新增分组
group = await TaskGroupDal(self.db).get_data(value=data.group, v_return_none=True)
if not group:
await TaskGroupDal(self.db).create_data({"value": data.group})
try:
# 删除正在运行中的 Job
await SchedulerTaskJobsDal(self.db).delete_data(_id)
except CustomException as e:
pass
result = {
"subscribe_number": 0,
"is_active": is_active
}
if is_active:
# 更新任务成功后, 如果任务状态为 True则向消息队列中发送任务
result['subscribe_number'] = await self.add_task(rd, obj)
return result
async def delete_task(self, _id: str) -> bool:
"""
删除任务
"""
result = await super(TaskDal, self).delete_data(_id)
try:
# 删除正在运行中的 Job
await SchedulerTaskJobsDal(self.db).delete_data(_id)
except CustomException as e:
pass
return result
async def run_once_task(self, rd: Redis, _id: str) -> int:
"""
执行一次任务
"""
obj: dict = await self.get_data(_id, v_schema=schemas.TaskSimpleOut)
message = {
"operation": self.JobOperation.add.value,
"task": {
"exec_strategy": "once",
"job_params": {
"name": obj.get("_id"),
"job_class": obj.get("job_class")
}
}
}
return await rd.publish(SUBSCRIBE, json.dumps(message).encode('utf-8'))
class TaskGroupDal(MongoManage):
def __init__(self, db: AsyncIOMotorDatabase):
super(TaskGroupDal, self).__init__(db, "vadmin_system_task_group")
class TaskRecordDal(MongoManage):
def __init__(self, db: AsyncIOMotorDatabase):
super(TaskRecordDal, self).__init__(db, "scheduler_task_record")
class SchedulerTaskJobsDal(MongoManage):
def __init__(self, db: AsyncIOMotorDatabase):
super(SchedulerTaskJobsDal, self).__init__(db, "scheduler_task_jobs", is_object_id=False)

View File

@ -0,0 +1,2 @@
from .dict import VadminDictType, VadminDictDetails
from .settings import VadminSystemSettings, VadminSystemSettingsTab

View File

@ -0,0 +1,40 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : user.py
# @IDE : PyCharm
# @desc : 系统字典模型
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, ForeignKey, Integer
class VadminDictType(BaseModel):
__tablename__ = "vadmin_system_dict_type"
__table_args__ = ({'comment': '字典类型表'})
dict_name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典名称")
dict_type: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典类型")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="字典状态,是否禁用")
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
details: Mapped[list["VadminDictDetails"]] = relationship(back_populates="dict_type")
class VadminDictDetails(BaseModel):
__tablename__ = "vadmin_system_dict_details"
__table_args__ = ({'comment': '字典详情表'})
label: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典标签")
value: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典键值")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="字典状态,是否禁用")
is_default: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否默认")
order: Mapped[int] = mapped_column(Integer, comment="字典排序")
dict_type_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_system_dict_type.id", ondelete='CASCADE'),
comment="关联字典类型"
)
dict_type: Mapped[VadminDictType] = relationship(foreign_keys=dict_type_id, back_populates="details")
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")

View File

@ -0,0 +1,42 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : settings.py
# @IDE : PyCharm
# @desc : 系统字典模型
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import String, Integer, ForeignKey, Boolean, Text
class VadminSystemSettingsTab(BaseModel):
__tablename__ = "vadmin_system_settings_tab"
__table_args__ = ({'comment': '系统配置分类表'})
title: Mapped[str] = mapped_column(String(255), comment="标题")
classify: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="分类键")
tab_label: Mapped[str] = mapped_column(String(255), comment="tab标题")
tab_name: Mapped[str] = mapped_column(String(255), index=True, nullable=False, unique=True, comment="tab标识符")
hidden: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否隐藏")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
settings: Mapped[list["VadminSystemSettings"]] = relationship(back_populates="tab")
class VadminSystemSettings(BaseModel):
__tablename__ = "vadmin_system_settings"
__table_args__ = ({'comment': '系统配置表'})
config_label: Mapped[str] = mapped_column(String(255), comment="配置表标签")
config_key: Mapped[str] = mapped_column(String(255), index=True, nullable=False, unique=True, comment="配置表键")
config_value: Mapped[str | None] = mapped_column(Text, comment="配置表内容")
remark: Mapped[str | None] = mapped_column(String(255), comment="备注信息")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
tab_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_system_settings_tab.id", ondelete='CASCADE'),
comment="关联tab标签"
)
tab: Mapped[VadminSystemSettingsTab] = relationship(foreign_keys=tab_id, back_populates="settings")

View File

@ -0,0 +1,3 @@
from .dict_type import DictTypeParams
from .dict_detail import DictDetailParams
from .task import TaskParams

View File

@ -0,0 +1,23 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : dict_type.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class DictDetailParams(QueryParams):
"""
列表分页
"""
def __init__(self, dict_type_id: int = None, label: str = None, params: Paging = Depends()):
super().__init__(params)
self.dict_type_id = dict_type_id
self.label = ("like", label)

View File

@ -0,0 +1,23 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : dict_type.py
# @IDE : PyCharm
# @desc : 查询参数-类依赖项
"""
类依赖项-官方文档https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/
"""
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class DictTypeParams(QueryParams):
"""
列表分页
"""
def __init__(self, dict_name: str = None, dict_type: str = None, params: Paging = Depends()):
super().__init__(params)
self.dict_name = ("like", dict_name)
self.dict_type = ("like", dict_type)

View File

@ -0,0 +1,32 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/6/25 14:50
# @File : task.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class TaskParams(QueryParams):
"""
列表分页
"""
def __init__(self, name: str = None, _id: str = None, group: str = None, params: Paging = Depends()):
super().__init__(params)
self.name = ("like", name)
self.group = group
self._id = ("ObjectId", _id)
self.v_order = "desc"
class TaskRecordParams(QueryParams):
"""
列表分页
"""
def __init__(self, job_id: str = None, name: str = None, params: Paging = Depends()):
super().__init__(params)
self.job_id = ("like", job_id)
self.name = ("like", name)
self.v_order = "desc"

View File

@ -0,0 +1,4 @@
from .dict import DictType, DictDetails, DictTypeSimpleOut, DictDetailsSimpleOut, DictTypeOptionsOut
from .settings_tab import SettingsTab, SettingsTabSimpleOut
from .settings import Settings, SettingsSimpleOut
from .task import Task, TaskSimpleOut

View File

@ -0,0 +1,53 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : dict.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict, Field
from core.data_types import DatetimeStr
class DictType(BaseModel):
dict_name: str
dict_type: str
disabled: bool | None = False
remark: str | None = None
class DictTypeSimpleOut(DictType):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class DictTypeOptionsOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
label: str = Field(alias='dict_name')
value: int = Field(alias='id')
disabled: bool
class DictDetails(BaseModel):
label: str
value: str
disabled: bool | None = False
is_default: bool | None = False
remark: str | None = None
order: int | None = None
dict_type_id: int
class DictDetailsSimpleOut(DictDetails):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr

View File

@ -0,0 +1,29 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : settings.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class Settings(BaseModel):
config_label: str | None = None
config_key: str
config_value: str | None = None
remark: str | None = None
disabled: bool | None = None
tab_id: int
class SettingsSimpleOut(Settings):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr

View File

@ -0,0 +1,28 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @File : settings_tab.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
class SettingsTab(BaseModel):
title: str
classify: str
tab_label: str
tab_name: str
hidden: bool
class SettingsTabSimpleOut(SettingsTab):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr

View File

@ -0,0 +1,32 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/6/25 15:08
# @File : task.py
# @IDE : PyCharm
# @desc : 简要说明
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr, ObjectIdStr
class Task(BaseModel):
name: str
group: str | None = None
job_class: str
exec_strategy: str
expression: str
is_active: bool | None = True # 临时字段,不在表中创建
remark: str | None = None
start_date: DatetimeStr | None = None
end_date: DatetimeStr | None = None
class TaskSimpleOut(Task):
model_config = ConfigDict(from_attributes=True)
id: ObjectIdStr = Field(..., alias='_id')
create_datetime: DatetimeStr
update_datetime: DatetimeStr
last_run_datetime: DatetimeStr | None = None # 临时字段,不在表中创建

263
apps/vadmin/system/views.py Normal file
View File

@ -0,0 +1,263 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/24 16:44
# @File : views.py
# @IDE : PyCharm
# @desc : 主要接口文件
from redis.asyncio import Redis
from fastapi import APIRouter, Depends, Body, UploadFile, Form, Request
from motor.motor_asyncio import AsyncIOMotorDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import ALIYUN_OSS
from core.database import db_getter, redis_getter, mongo_getter
from utils.file.aliyun_oss import AliyunOSS, BucketConf
from utils.file.file_manage import FileManage
from utils.response import SuccessResponse, ErrorResponse
from utils.sms.code import CodeSMS
from . import schemas, crud
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth, OpenAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import DictTypeParams, DictDetailParams, TaskParams
from apps.vadmin.auth import crud as vadmin_auth_crud
from .params.task import TaskRecordParams
app = APIRouter()
###########################################################
# 字典类型管理
###########################################################
@app.get("/dict/types", summary="获取字典类型列表")
async def get_dict_types(p: DictTypeParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.DictTypeDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/dict/types", summary="创建字典类型")
async def create_dict_types(data: schemas.DictType, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.DictTypeDal(auth.db).create_data(data=data))
@app.delete("/dict/types", summary="批量删除字典类型")
async def delete_dict_types(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.DictTypeDal(auth.db).delete_datas(ids=ids.ids)
return SuccessResponse("删除成功")
@app.post("/dict/types/details", summary="获取多个字典类型下的字典元素列表")
async def post_dicts_details(
auth: Auth = Depends(AllUserAuth()),
dict_types: list[str] = Body(None, title="字典元素列表", description="查询字典元素列表")
):
datas = await crud.DictTypeDal(auth.db).get_dicts_details(dict_types)
return SuccessResponse(datas)
@app.get("/dict/types/options", summary="获取字典类型选择项")
async def get_dicts_options(auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.DictTypeDal(auth.db).get_select_datas())
@app.put("/dict/types/{data_id}", summary="更新字典类型")
async def put_dict_types(data_id: int, data: schemas.DictType, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.DictTypeDal(auth.db).put_data(data_id, data))
@app.get("/dict/types/{data_id}", summary="获取字典类型详细")
async def get_dict_type(data_id: int, auth: Auth = Depends(AllUserAuth())):
schema = schemas.DictTypeSimpleOut
return SuccessResponse(await crud.DictTypeDal(auth.db).get_data(data_id, v_schema=schema))
###########################################################
# 字典元素管理
###########################################################
@app.post("/dict/details", summary="创建字典元素")
async def create_dict_details(data: schemas.DictDetails, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.DictDetailsDal(auth.db).create_data(data=data))
@app.get("/dict/details", summary="获取单个字典类型下的字典元素列表,分页")
async def get_dict_details(params: DictDetailParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.DictDetailsDal(auth.db).get_datas(**params.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.delete("/dict/details", summary="批量删除字典元素", description="硬删除")
async def delete_dict_details(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.DictDetailsDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/dict/details/{data_id}", summary="更新字典元素")
async def put_dict_details(data_id: int, data: schemas.DictDetails, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.DictDetailsDal(auth.db).put_data(data_id, data))
@app.get("/dict/details/{data_id}", summary="获取字典元素详情")
async def get_dict_detail(data_id: int, auth: Auth = Depends(AllUserAuth())):
schema = schemas.DictDetailsSimpleOut
return SuccessResponse(await crud.DictDetailsDal(auth.db).get_data(data_id, v_schema=schema))
###########################################################
# 文件上传管理
###########################################################
@app.post("/upload/image/to/oss", summary="上传图片到阿里云OSS")
async def upload_image_to_oss(file: UploadFile, path: str = Form(...)):
result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_image(path, file)
return SuccessResponse(result)
@app.post("/upload/video/to/oss", summary="上传视频到阿里云OSS")
async def upload_video_to_oss(file: UploadFile, path: str = Form(...)):
result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_video(path, file)
return SuccessResponse(result)
@app.post("/upload/file/to/oss", summary="上传文件到阿里云OSS")
async def upload_file_to_oss(file: UploadFile, path: str = Form(...)):
result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_file(path, file)
return SuccessResponse(result)
@app.post("/upload/image/to/local", summary="上传图片到本地")
async def upload_image_to_local(file: UploadFile, path: str = Form(...)):
manage = FileManage(file, path)
path = await manage.save_image_local()
return SuccessResponse(path)
###########################################################
# 短信服务管理
###########################################################
@app.post("/sms/send", summary="发送短信验证码(阿里云服务)")
async def sms_send(telephone: str, rd: Redis = Depends(redis_getter), auth: Auth = Depends(OpenAuth())):
user = await vadmin_auth_crud.UserDal(auth.db).get_data(telephone=telephone, v_return_none=True)
if not user:
return ErrorResponse("手机号不存在!")
sms = CodeSMS(telephone, rd)
return SuccessResponse(await sms.main_async())
###########################################################
# 系统配置管理
###########################################################
@app.post("/settings/tabs", summary="获取系统配置标签列表")
async def get_settings_tabs(classifys: list[str] = Body(...), auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.SettingsTabDal(auth.db).get_datas(limit=0, classify=("in", classifys)))
@app.get("/settings/tabs/values", summary="获取系统配置标签下的信息")
async def get_settings_tabs_values(tab_id: int, auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.SettingsDal(auth.db).get_tab_values(tab_id=tab_id))
@app.put("/settings/tabs/values", summary="更新系统配置信息")
async def put_settings_tabs_values(
request: Request,
datas: dict = Body(...),
auth: Auth = Depends(FullAdminAuth())
):
return SuccessResponse(await crud.SettingsDal(auth.db).update_datas(datas, request))
@app.get("/settings/base/config", summary="获取系统基础配置", description="每次进入系统中时使用")
async def get_setting_base_config(db: AsyncSession = Depends(db_getter)):
return SuccessResponse(await crud.SettingsDal(db).get_base_config())
@app.get("/settings/privacy", summary="获取隐私协议")
async def get_settings_privacy(auth: Auth = Depends(OpenAuth())):
return SuccessResponse((await crud.SettingsDal(auth.db).get_data(config_key="web_privacy")).config_value)
@app.get("/settings/agreement", summary="获取用户协议")
async def get_settings_agreement(auth: Auth = Depends(OpenAuth())):
return SuccessResponse((await crud.SettingsDal(auth.db).get_data(config_key="web_agreement")).config_value)
###########################################################
# 定时任务管理
###########################################################
@app.get("/tasks", summary="获取定时任务列表")
async def get_tasks(
p: TaskParams = Depends(),
db: AsyncIOMotorDatabase = Depends(mongo_getter),
auth: Auth = Depends(AllUserAuth())
):
datas, count = await crud.TaskDal(db).get_tasks(**p.dict())
return SuccessResponse(datas, count=count)
@app.post("/tasks", summary="添加定时任务")
async def post_tasks(
data: schemas.Task,
db: AsyncIOMotorDatabase = Depends(mongo_getter),
rd: Redis = Depends(redis_getter),
auth: Auth = Depends(AllUserAuth())
):
return SuccessResponse(await crud.TaskDal(db).create_task(rd, data))
@app.put("/tasks", summary="更新定时任务")
async def put_tasks(
_id: str,
data: schemas.Task,
db: AsyncIOMotorDatabase = Depends(mongo_getter),
rd: Redis = Depends(redis_getter),
auth: Auth = Depends(AllUserAuth())
):
return SuccessResponse(await crud.TaskDal(db).put_task(rd, _id, data))
@app.delete("/tasks", summary="删除单个定时任务")
async def delete_task(
_id: str,
db: AsyncIOMotorDatabase = Depends(mongo_getter),
auth: Auth = Depends(AllUserAuth())
):
return SuccessResponse(await crud.TaskDal(db).delete_task(_id))
@app.get("/task", summary="获取定时任务详情")
async def get_task(
_id: str,
db: AsyncIOMotorDatabase = Depends(mongo_getter),
auth: Auth = Depends(AllUserAuth())
):
return SuccessResponse(await crud.TaskDal(db).get_task(_id, v_schema=schemas.TaskSimpleOut))
@app.post("/task", summary="执行一次定时任务")
async def run_once_task(
_id: str,
db: AsyncIOMotorDatabase = Depends(mongo_getter),
rd: Redis = Depends(redis_getter),
auth: Auth = Depends(AllUserAuth())
):
return SuccessResponse(await crud.TaskDal(db).run_once_task(rd, _id))
###########################################################
# 定时任务分组管理
###########################################################
@app.get("/task/group/options", summary="获取定时任务分组选择项列表")
async def get_task_group_options(db: AsyncIOMotorDatabase = Depends(mongo_getter), auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.TaskGroupDal(db).get_datas(limit=0))
###########################################################
# 定时任务调度日志
###########################################################
@app.get("/task/records", summary="获取定时任务调度日志列表")
async def get_task_records(
p: TaskRecordParams = Depends(),
db: AsyncIOMotorDatabase = Depends(mongo_getter),
auth: Auth = Depends(AllUserAuth())
):
count = await crud.TaskRecordDal(db).get_count(**p.to_count())
datas = await crud.TaskRecordDal(db).get_datas(**p.dict())
return SuccessResponse(datas, count=count)

View File

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/10/19 15:41
# @File : __init__.py.py
# @IDE : PyCharm
# @desc : 简要说明

View File

@ -0,0 +1,159 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/10/19 15:41
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import APIRouter, Depends
from apps.vadmin.auth.utils.current import AllUserAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from utils.response import SuccessResponse
import datetime
from apps.vadmin.record.crud import LoginRecordDal
app = APIRouter()
###########################################################
# 工作区管理
###########################################################
@app.get("/project", summary="获取项目")
async def get_project():
data = [
{
"name": 'Mysql',
"icon": 'vscode-icons:file-type-mysql',
"message": '最流行的关系型数据库管理系统',
"personal": 'kinit',
"link": "https://www.mysql.com/",
"time": datetime.datetime.now().strftime("%Y-%m-%d")
},
{
"name": 'FastAPI',
"icon": 'simple-icons:fastapi',
"message": '一个现代、快速(高性能)的 web 框架',
"personal": 'kinit',
"link": "https://fastapi.tiangolo.com/zh/",
"time": datetime.datetime.now().strftime("%Y-%m-%d")
},
{
"name": 'Vue',
"icon": 'logos:vue',
"message": '渐进式 JavaScript 框架',
"personal": 'kinit',
"link": "https://cn.vuejs.org/",
"time": datetime.datetime.now().strftime("%Y-%m-%d")
},
{
"name": 'Element-plus',
"icon": 'logos:element',
"message": '面向设计师和开发者的组件库',
"personal": 'kinit',
"link": "https://element-plus.org/zh-CN/",
"time": datetime.datetime.now().strftime("%Y-%m-%d")
},
{
"name": 'Typescript',
"icon": 'vscode-icons:file-type-typescript-official',
"message": 'TypeScript是JavaScript类型的超集',
"personal": 'kinit',
"link": "https://www.typescriptlang.org/",
"time": datetime.datetime.now().strftime("%Y-%m-%d")
},
{
"name": 'Vite',
"icon": 'vscode-icons:file-type-vite',
"message": 'Vite 下一代的前端工具链',
"personal": 'kinit',
"link": "https://cn.vitejs.dev/",
"time": datetime.datetime.now().strftime("%Y-%m-%d")
}
]
return SuccessResponse(data)
@app.get("/dynamic", summary="获取动态")
async def get_dynamic():
data = [
{
"keys": ['workplace.push', 'Github'],
"time": datetime.datetime.now().strftime("%Y-%m-%d")
},
{
"keys": ['workplace.push', 'Github'],
"time": datetime.datetime.now().strftime("%Y-%m-%d")
}
]
return SuccessResponse(data)
@app.get("/team", summary="获取团队信息")
async def get_team():
data = [
{
"name": 'Mysql',
"icon": 'vscode-icons:file-type-mysql'
},
{
"name": 'Vue',
"icon": 'logos:vue'
},
{
"name": 'Element-plus',
"icon": 'logos:element'
},
{
"name": 'Fastapi',
"icon": 'simple-icons:fastapi'
},
{
"name": 'Typescript',
"icon": 'vscode-icons:file-type-typescript-official'
},
{
"name": 'Vite',
"icon": 'vscode-icons:file-type-vite'
}
]
return SuccessResponse(data)
@app.get("/shortcuts", summary="获取快捷操作")
async def get_shortcuts():
data = [
{
"name": "Gitee 项目仓库",
"link": "https://gitee.com/ktianc/kinit"
},
{
"name": "GitHub 项目仓库",
"link": "https://github.com/vvandk/kinit"
},
{
"name": "前端文档",
"link": "https://element-plus-admin-doc.cn/"
},
{
"name": "Swagger UI 接口文档",
"link": "http://kinit.ktianc.top/api/docs"
},
{
"name": "Redoc 接口文档",
"link": "http://kinit.ktianc.top/api/redoc"
},
{
"name": "UnoCSS 中文文档",
"link": "https://unocss.nodejs.cn/guide/"
},
{
"name": "Iconify 文档",
"link": "https://icon-sets.iconify.design/"
},
{
"name": "echarts 文档",
"link": "https://echarts.apache.org/zh/index.html"
},
]
return SuccessResponse(data)