项目初次提交

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

7
apps/__init__.py Normal file
View File

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

View File

View File

View File

@ -0,0 +1,47 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:30
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据访问层
from sqlalchemy.ext.asyncio import AsyncSession
from core.crud import DalBase
from . import schemas, models
class ProjectDetectDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectDetectDal, self).__init__()
self.db = db
self.model = models.ProjectDetect
self.schema = schemas.ProjectDetectSimpleOut
class ProjectDetectImgDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectDetectImgDal, self).__init__()
self.db = db
self.model = models.ProjectDetectImg
self.schema = schemas.ProjectDetectImgSimpleOut
class ProjectDetectLogDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectDetectLogDal, self).__init__()
self.db = db
self.model = models.ProjectDetectLog
self.schema = schemas.ProjectDetectLogSimpleOut
class ProjectDetectLogImgDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectDetectLogImgDal, self).__init__()
self.db = db
self.model = models.ProjectDetectLogImg
self.schema = schemas.ProjectDetectLogImgSimpleOut

View File

View File

@ -0,0 +1,67 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer
from db.db_base import BaseModel
class ProjectDetect(BaseModel):
"""
项目推理集合
"""
__tablename__ = "project_detect"
__table_args__ = ({'comment': '项目推理集合'})
project_id: Mapped[int] = mapped_column(Integer, nullable=False)
detect_name: Mapped[str] = mapped_column(String(64), nullable=False)
detect_version: Mapped[int] = mapped_column(Integer)
detect_no: Mapped[str] = mapped_column(String(32))
detect_status: Mapped[int] = mapped_column(Integer)
file_type: Mapped[str] = mapped_column(String(10))
folder_url: Mapped[str] = mapped_column(String(255))
rtsp_url: Mapped[str] = mapped_column(String(255))
user_id: Mapped[int] = mapped_column(Integer, nullable=False)
class ProjectDetectImg(BaseModel):
"""
待推理图片
"""
__tablename__ = "project_detect_img"
__table_args__ = ({'comment': '待推理图片'})
detect_id: Mapped[int] = mapped_column(Integer, nullable=False)
file_name: Mapped[str] = mapped_column(String(64), nullable=False)
image_url: Mapped[str] = mapped_column(String(255), nullable=False)
thumb_image_url: Mapped[str] = mapped_column(String(255), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, nullable=False)
class ProjectDetectLog(BaseModel):
"""
项目推理记录
"""
__tablename__ = "project_detect_log"
__table_args__ = ({'comment': '项目推理记录'})
detect_id: Mapped[int] = mapped_column(Integer, nullable=False)
detect_version: Mapped[str] = mapped_column(String(10))
detect_name: Mapped[str] = mapped_column(String(64), nullable=False)
train_id: Mapped[int] = mapped_column(Integer, nullable=False)
train_version: Mapped[str] = mapped_column(String(10))
pt_type: Mapped[str] = mapped_column(String(10))
pt_url: Mapped[str] = mapped_column(String(255))
folder_url: Mapped[str] = mapped_column(String(255))
detect_folder_url: Mapped[str] = mapped_column(String(255))
user_id: Mapped[int] = mapped_column(Integer, nullable=False)
class ProjectDetectLogImg(BaseModel):
"""
推理完成的图片
"""
__tablename__ = "project_detect_log_img"
__table_args__ = ({'comment': '项目训练版本信息表'})
log_id: Mapped[int] = mapped_column(Integer, nullable=False)
file_name: Mapped[str] = mapped_column(String(64), nullable=False)
image_url: Mapped[str] = mapped_column(String(255), nullable=False)

View File

@ -0,0 +1,4 @@
from .project_detect import ProjectDetectParams
from .project_detect_img import ProjectDetectImgParams
from .project_detect_log import ProjectDetectLogParams
from .project_detect_log_img import ProjectDetectLogImgParams

View File

@ -0,0 +1,15 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:30
# @File : project_detect.py
# @IDE : PyCharm
# @desc : 项目推理集合信息
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ProjectDetectParams(QueryParams):
def __init__(self, params: Paging = Depends()):
super().__init__(params)

View File

@ -0,0 +1,15 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:30
# @File : project_detect_img.py
# @IDE : PyCharm
# @desc : 项目推理集合图片信息
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ProjectDetectImgParams(QueryParams):
def __init__(self, params: Paging = Depends()):
super().__init__(params)

View File

@ -0,0 +1,15 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:31
# @File : project_detect_log.py
# @IDE : PyCharm
# @desc : 项目推理记录信息
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ProjectDetectLogParams(QueryParams):
def __init__(self, params: Paging = Depends()):
super().__init__(params)

View File

@ -0,0 +1,15 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:31
# @File : project_detect_log_img.py
# @IDE : PyCharm
# @desc : 项目推理记录图片信息
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ProjectDetectLogImgParams(QueryParams):
def __init__(self, params: Paging = Depends()):
super().__init__(params)

View File

@ -0,0 +1,4 @@
from .project_detect import ProjectDetect, ProjectDetectSimpleOut
from .project_detect_img import ProjectDetectImg, ProjectDetectImgSimpleOut
from .project_detect_log import ProjectDetectLog, ProjectDetectLogSimpleOut
from .project_detect_log_img import ProjectDetectLogImg, ProjectDetectLogImgSimpleOut

View File

@ -0,0 +1,30 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:30
# @File : project_detect.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
class ProjectDetect(BaseModel):
project_id: int = Field(..., title="None")
detect_name: str = Field(..., title="None")
detect_version: int = Field(..., title="None")
detect_no: str = Field(..., title="None")
detect_status: int = Field(..., title="None")
file_type: str = Field(..., title="None")
folder_url: str = Field(..., title="None")
rtsp_url: str = Field(..., title="None")
user_id: int = Field(..., title="None")
class ProjectDetectSimpleOut(ProjectDetect):
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., title="编号")
create_datetime: DatetimeStr = Field(..., title="创建时间")
update_datetime: DatetimeStr = Field(..., title="更新时间")

View File

@ -0,0 +1,26 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:30
# @File : project_detect_img.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
class ProjectDetectImg(BaseModel):
detect_id: int = Field(..., title="None")
file_name: str = Field(..., title="None")
image_url: str = Field(..., title="None")
thumb_image_url: str = Field(..., title="None")
user_id: int = Field(..., title="None")
class ProjectDetectImgSimpleOut(ProjectDetectImg):
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., title="编号")
create_datetime: DatetimeStr = Field(..., title="创建时间")
update_datetime: DatetimeStr = Field(..., title="更新时间")

View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:31
# @File : project_detect_log.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
class ProjectDetectLog(BaseModel):
detect_id: int = Field(..., title="None")
detect_version: str = Field(..., title="None")
detect_name: str = Field(..., title="None")
train_id: int = Field(..., title="None")
train_version: str = Field(..., title="None")
pt_type: str = Field(..., title="None")
pt_url: str = Field(..., title="None")
folder_url: str = Field(..., title="None")
detect_folder_url: str = Field(..., title="None")
user_id: int = Field(..., title="None")
class ProjectDetectLogSimpleOut(ProjectDetectLog):
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., title="编号")
create_datetime: DatetimeStr = Field(..., title="创建时间")
update_datetime: DatetimeStr = Field(..., title="更新时间")

View File

@ -0,0 +1,24 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:31
# @File : project_detect_log_img.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
class ProjectDetectLogImg(BaseModel):
log_id: int = Field(..., title="None")
file_name: str = Field(..., title="None")
image_url: str = Field(..., title="None")
class ProjectDetectLogImgSimpleOut(ProjectDetectLogImg):
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., title="编号")
create_datetime: DatetimeStr = Field(..., title="创建时间")
update_datetime: DatetimeStr = Field(..., title="更新时间")

View File

@ -0,0 +1,150 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:30
# @File : views.py
# @IDE : PyCharm
# @desc : 路由,视图文件
from core.dependencies import IdList
from apps.vadmin.auth.utils.validation.auth import Auth
from sqlalchemy.ext.asyncio import AsyncSession
from apps.vadmin.auth.utils.current import AllUserAuth
from core.database import db_getter
from . import schemas, crud, models, params
from fastapi import Depends, APIRouter
from utils.response import SuccessResponse
app = APIRouter()
###########################################################
# 项目推理集合信息
###########################################################
@app.get("/project/detect", summary="获取项目推理集合信息列表", tags=["项目推理集合信息"])
async def get_project_detect_list(p: params.ProjectDetectParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.ProjectDetectDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/project/detect", summary="创建项目推理集合信息", tags=["项目推理集合信息"])
async def create_project_detect(data: schemas.ProjectDetect, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectDal(auth.db).create_data(data=data))
@app.delete("/project/detect", summary="删除项目推理集合信息", description="硬删除", tags=["项目推理集合信息"])
async def delete_project_detect_list(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.ProjectDetectDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/project/detect/{data_id}", summary="更新项目推理集合信息", tags=["项目推理集合信息"])
async def put_project_detect(data_id: int, data: schemas.ProjectDetect, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectDal(auth.db).put_data(data_id, data))
@app.get("/project/detect/{data_id}", summary="获取项目推理集合信息信息", tags=["项目推理集合信息"])
async def get_project_detect(data_id: int, db: AsyncSession = Depends(db_getter)):
schema = schemas.ProjectDetectSimpleOut
return SuccessResponse(await crud.ProjectDetectDal(db).get_data(data_id, v_schema=schema))
###########################################################
# 项目推理集合图片信息
###########################################################
@app.get("/project/detect/img", summary="获取项目推理集合图片信息列表", tags=["项目推理集合图片信息"])
async def get_project_detect_img_list(p: params.ProjectDetectImgParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.ProjectDetectImgDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/project/detect/img", summary="创建项目推理集合图片信息", tags=["项目推理集合图片信息"])
async def create_project_detect_img(data: schemas.ProjectDetectImg, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectImgDal(auth.db).create_data(data=data))
@app.delete("/project/detect/img", summary="删除项目推理集合图片信息", description="硬删除", tags=["项目推理集合图片信息"])
async def delete_project_detect_img_list(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.ProjectDetectImgDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/project/detect/img/{data_id}", summary="更新项目推理集合图片信息", tags=["项目推理集合图片信息"])
async def put_project_detect_img(data_id: int, data: schemas.ProjectDetectImg, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectImgDal(auth.db).put_data(data_id, data))
@app.get("/project/detect/img/{data_id}", summary="获取项目推理集合图片信息信息", tags=["项目推理集合图片信息"])
async def get_project_detect_img(data_id: int, db: AsyncSession = Depends(db_getter)):
schema = schemas.ProjectDetectImgSimpleOut
return SuccessResponse(await crud.ProjectDetectImgDal(db).get_data(data_id, v_schema=schema))
###########################################################
# 项目推理记录信息
###########################################################
@app.get("/project/detect/log", summary="获取项目推理记录信息列表", tags=["项目推理记录信息"])
async def get_project_detect_log_list(p: params.ProjectDetectLogParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.ProjectDetectLogDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/project/detect/log", summary="创建项目推理记录信息", tags=["项目推理记录信息"])
async def create_project_detect_log(data: schemas.ProjectDetectLog, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectLogDal(auth.db).create_data(data=data))
@app.delete("/project/detect/log", summary="删除项目推理记录信息", description="硬删除", tags=["项目推理记录信息"])
async def delete_project_detect_log_list(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.ProjectDetectLogDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/project/detect/log/{data_id}", summary="更新项目推理记录信息", tags=["项目推理记录信息"])
async def put_project_detect_log(data_id: int, data: schemas.ProjectDetectLog, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectLogDal(auth.db).put_data(data_id, data))
@app.get("/project/detect/log/{data_id}", summary="获取项目推理记录信息信息", tags=["项目推理记录信息"])
async def get_project_detect_log(data_id: int, db: AsyncSession = Depends(db_getter)):
schema = schemas.ProjectDetectLogSimpleOut
return SuccessResponse(await crud.ProjectDetectLogDal(db).get_data(data_id, v_schema=schema))
###########################################################
# 项目推理记录图片信息
###########################################################
@app.get("/project/detect/log/img", summary="获取项目推理记录图片信息列表", tags=["项目推理记录图片信息"])
async def get_project_detect_log_img_list(p: params.ProjectDetectLogImgParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.ProjectDetectLogImgDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/project/detect/log/img", summary="创建项目推理记录图片信息", tags=["项目推理记录图片信息"])
async def create_project_detect_log_img(data: schemas.ProjectDetectLogImg, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectLogImgDal(auth.db).create_data(data=data))
@app.delete("/project/detect/log/img", summary="删除项目推理记录图片信息", description="硬删除", tags=["项目推理记录图片信息"])
async def delete_project_detect_log_img_list(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.ProjectDetectLogImgDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/project/detect/log/img/{data_id}", summary="更新项目推理记录图片信息", tags=["项目推理记录图片信息"])
async def put_project_detect_log_img(data_id: int, data: schemas.ProjectDetectLogImg, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectDetectLogImgDal(auth.db).put_data(data_id, data))
@app.get("/project/detect/log/img/{data_id}", summary="获取项目推理记录图片信息信息", tags=["项目推理记录图片信息"])
async def get_project_detect_log_img(data_id: int, db: AsyncSession = Depends(db_getter)):
schema = schemas.ProjectDetectLogImgSimpleOut
return SuccessResponse(await crud.ProjectDetectLogImgDal(db).get_data(data_id, v_schema=schema))

View File

View File

@ -0,0 +1,156 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:25
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据访问层
import application.settings
from . import schemas, models, params
from apps.vadmin.auth.utils.validation.auth import Auth
from utils import os_utils as os, random_utils as ru
if application.settings.DEBUG:
from application.config.development import datasets_url, runs_url, detect_url, yolo_url, images_url
else:
from application.config.production import datasets_url, runs_url, detect_url, yolo_url, images_url
from typing import Any, List
from core.crud import DalBase
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, case, and_
class ProjectInfoDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectInfoDal, self).__init__()
self.db = db
self.model = models.ProjectInfo
self.schema = schemas.ProjectInfoOut
async def get_project_pager(self, project: params.ProjectInfoParams, auth: Auth):
"""
分页查询项目列表
"""
# 定义子查询
subquery = (
select(
models.ProjectImage.project_id,
func.sum(case((models.ProjectImgLeafer.id.is_(None), 1), else_=0)).label('no_mark_count'),
func.sum(case((models.ProjectImgLeafer.id.isnot(None), 1), else_=0)).label('mark_count')
)
.outerjoin(models.ProjectImgLeafer, models.ProjectImage.id == models.ProjectImgLeafer.image_id)
.group_by(models.ProjectImage.project_id)
.subquery()
)
full_query = select(
models.ProjectInfo,
func.ifnull(subquery.c.mark_count, 0).label("mark_count"),
func.ifnull(subquery.c.no_mark_count, 0).label("no_mark_count")
).select_from(models.ProjectInfo).join(
subquery, models.ProjectInfo.id == subquery.c.project_id, isouter=True
)
v_where = [models.ProjectInfo.is_delete.is_(False)]
if '*' in auth.dept_ids:
v_where.append(models.ProjectInfo.dept_id.isnot(None))
else:
v_where.append(models.ProjectInfo.dept_id.in_(auth.dept_ids))
sql = await self.filter_core(
v_start_sql=full_query,
v_where=v_where,
v_return_sql=True,
v_order=project.v_order,
v_order_field=project.v_order_field
)
count = await self.get_count_sql(sql)
if project.limit != 0:
sql = sql.offset((project.page - 1) * project.limit).limit(project.limit)
queryset = await self.db.execute(sql)
result = queryset.all()
datas = []
for result in result:
data = schemas.ProjectInfoPagerOut.model_validate(result[0])
data.mark_count = int(result[1])
data.no_mark_count = int(result[2])
datas.append(data.model_dump())
return datas, count
async def check_name(self, project_name: str):
"""
校验项目名称是否重名
"""
count = await self.get_count(v_where=[models.ProjectInfo.project_name == project_name,
models.ProjectInfo.is_delete is False])
return count > 0
async def add_project(
self,
project: schemas.ProjectInfoIn,
auth: Auth
) -> Any:
obj = self.model(**project.model_dump())
obj.user_id = auth.user.id
obj.project_no = ru.random_str(6)
obj.project_status = "0"
obj.train_version = 0
obj.user_id = auth.user.id
if '*' in auth.dept_ids:
obj.dept_id = 0
else:
obj.dept_id = auth.dept_ids[0]
os.create_folder(datasets_url, obj.project_no)
os.create_folder(runs_url, obj.project_no)
await self.flush(obj)
return await self.out_dict(obj, None, False, schemas.ProjectInfoOut)
class ProjectImageDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectImageDal, self).__init__()
self.db = db
self.model = models.ProjectImage
self.schema = schemas.ProjectImageSimpleOut
class ProjectLabelDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectLabelDal, self).__init__()
self.db = db
self.model = models.ProjectLabel
self.schema = schemas.ProjectLabel
async def check_label_name(
self,
name: str,
pro_id: int,
label_id: int = None
):
wheres = [
models.ProjectLabel.project_id == pro_id,
models.ProjectLabel.label_name == name
]
if label_id:
wheres.append(models.ProjectLabel.id != label_id)
count = await self.get_count(v_where=wheres)
return count > 0
class ProjectImgLabelDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectImgLabelDal, self).__init__()
self.db = db
self.model = models.ProjectImgLabel
self.schema = schemas.ProjectImgLabelSimpleOut
class ProjectImgLeaferDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectImgLeaferDal, self).__init__()
self.db = db
self.model = models.ProjectImgLeafer
self.schema = schemas.ProjectImgLeaferSimpleOut

View File

@ -0,0 +1 @@
from .project import ProjectLabel, ProjectInfo, ProjectImgLabel, ProjectImgLeafer, ProjectImage

View File

@ -0,0 +1,73 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer, JSON
from db.db_base import BaseModel
class ProjectInfo(BaseModel):
"""
项目信息表
"""
__tablename__ = "project_info"
__table_args__ = ({'comment': '项目类别表 - 标识项目的类型目前存在的目标识别OCR识别瑕疵检测图像分类'})
project_no: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
project_name: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
type_code: Mapped[str] = mapped_column(String(10))
description: Mapped[str] = mapped_column(String(255))
project_status: Mapped[str] = mapped_column(String(10))
user_id: Mapped[int] = mapped_column(Integer)
dept_id: Mapped[int] = mapped_column(Integer)
train_version: Mapped[int] = mapped_column(Integer)
class ProjectLabel(BaseModel):
"""
项目标签表
"""
__tablename__ = "project_label"
__table_args__ = ({'comment': '项目标签表'})
label_name: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
project_id: Mapped[int] = mapped_column(Integer, nullable=False)
meta: Mapped[dict] = mapped_column(JSON)
class ProjectImage(BaseModel):
"""
项目图片表
"""
__tablename__ = "project_image"
__table_args__ = ({'comment': '项目图片表'})
img_type: Mapped[str] = mapped_column(String(10))
file_name: Mapped[str] = mapped_column(String(64), nullable=False)
image_url: Mapped[str] = mapped_column(String(255), nullable=False)
thumb_image_url: Mapped[str] = mapped_column(String(255), nullable=False)
project_id: Mapped[int] = mapped_column(Integer)
class ProjectImgLeafer(BaseModel):
"""
项目图片leafer表
"""
__tablename__ = "project_img_leafer"
__table_args__ = ({'comment': '项目图片leafer表'})
image_id: Mapped[int] = mapped_column(Integer, nullable=False)
leafer: Mapped[dict] = mapped_column(JSON)
class ProjectImgLabel(BaseModel):
"""
项目图片标签对应表一张图片对应多个label
"""
__tablename__ = "project_img_label"
__table_args__ = ({'comment': '项目图片标签对应表一张图片对应多个label'})
image_id: Mapped[int] = mapped_column(Integer, nullable=False)
label_id: Mapped[int] = mapped_column(Integer, nullable=False)
mark_center_x: Mapped[str] = mapped_column(String(64), nullable=False)
mark_center_y: Mapped[str] = mapped_column(String(64), nullable=False)
mark_width: Mapped[str] = mapped_column(String(64), nullable=False)
mark_height: Mapped[str] = mapped_column(String(64), nullable=False)

View File

@ -0,0 +1,5 @@
from .project_info import ProjectInfoParams
from .project_image import ProjectImageParams
from .project_label import ProjectLabelParams
from .project_img_label import ProjectImgLabelParams
from .project_img_leafer import ProjectImgLeaferParams

View File

@ -0,0 +1,25 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:27
# @File : project_image.py
# @IDE : PyCharm
# @desc : 项目图片信息
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class ProjectImageParams(QueryParams):
def __init__(
self,
img_type: str | None = Query(None, title="图片类别"),
project_id: int | None = Query(None, title="项目id"),
file_name: str | None = Query(None, title="文件名称"),
params: Paging = Depends()
):
super().__init__(params)
self.img_type = img_type
self.project_id = project_id
self.file_name = file_name

View File

@ -0,0 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:29
# @File : project_img_label.py
# @IDE : PyCharm
# @desc : 项目图片标签信息
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class ProjectImgLabelParams(QueryParams):
def __init__(
self,
image_id: int | None = Query(None, title="图片id"),
params: Paging = Depends()
):
super().__init__(params)
self.image_id = image_id

View File

@ -0,0 +1,21 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:29
# @File : project_img_leafer.py
# @IDE : PyCharm
# @desc : 项目图片leafer信息
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class ProjectImgLeaferParams(QueryParams):
def __init__(
self,
image_id: int | None = Query(None, title="图片id"),
params: Paging = Depends()
):
super().__init__(params)
self.image_id = image_id

View File

@ -0,0 +1,28 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:25
# @File : project_info.py
# @IDE : PyCharm
# @desc : 项目信息
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class ProjectInfoParams(QueryParams):
def __init__(
self,
project_name: str | None = Query(None, title="项目名称"),
type_code: str | None = Query(None, title="项目类别"),
dept_id: str | None = Query(None, title="部门id"),
user_id: str | None = Query(None, title="用户id"),
params: Paging = Depends()
):
super().__init__(params)
self.project_name = ("like", project_name)
self.type_code = type_code
self.dept_id = dept_id
self.user_id = user_id

View File

@ -0,0 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:28
# @File : project_label.py
# @IDE : PyCharm
# @desc : 项目标签信息
from fastapi import Depends, Query
from core.dependencies import Paging, QueryParams
class ProjectLabelParams(QueryParams):
def __init__(
self,
project_id: int | None = Query(None, title="项目id"),
params: Paging = Depends()
):
super().__init__(params)
self.project_id = project_id

View File

@ -0,0 +1,5 @@
from .project_info import ProjectInfoIn, ProjectInfoOut, ProjectInfoPagerOut
from .project_image import ProjectImage, ProjectImagePager, ProjectImageOut
from .project_label import ProjectLabel
from .project_img_label import ProjectImgLabelIn
from .project_img_leafer import ProjectImgLeaferLabel, ProjectImgLeaferOut

View File

@ -0,0 +1,38 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:27
# @File : project_image.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
from typing import Optional
class ProjectImage(BaseModel):
id: Optional[int] = Field(None, description="id")
project_id: Optional[int] = Field(..., description="项目id")
img_type: Optional[str] = Field(None, description="图片类别")
file_name: Optional[str] = Field(None, description="文件名称")
create_time: DatetimeStr
model_config = ConfigDict(from_attributes=True)
class ProjectImageOut(BaseModel):
id: Optional[int] = Field(None, description="id")
project_id: Optional[int] = Field(..., description="项目id")
file_name: Optional[str] = Field(None, description="文件名称")
create_time: DatetimeStr
label_count: Optional[int]
model_config = ConfigDict(from_attributes=True)
class ProjectImagePager(BaseModel):
project_id: Optional[int] = Field(..., description="项目id")
img_type: Optional[str] = Field(None, description="图片类别")
pagerNum: Optional[int] = Field(None, description="当前页码")
pagerSize: Optional[int] = Field(None, description="每页数量")

View File

@ -0,0 +1,18 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:29
# @File : project_img_label.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel
class ProjectImgLabelIn(BaseModel):
label_id: int
mark_center_x: str
mark_center_y: str
mark_width: str
mark_height: str

View File

@ -0,0 +1,25 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:29
# @File : project_img_leafer.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
from . import ProjectImgLabelIn
class ProjectImgLeaferLabel(BaseModel):
image_id: Optional[int] = Field(..., description="图片id")
leafer: Optional[dict] = Field(..., description="保存的leafer")
label_infos: List[ProjectImgLabelIn] = Field(..., description="标签框选信息")
class ProjectImgLeaferOut(BaseModel):
image_id: Optional[int] = Field(..., description="图片id")
leafer: Optional[dict] = Field(..., description="保存的leafer")
model_config = ConfigDict(from_attributes=True)

View File

@ -0,0 +1,47 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:25
# @File : project_info.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
class ProjectInfoIn(BaseModel):
"""项目信息输入"""
project_name: Optional[str] = Field(..., description="项目名称")
type_code: Optional[str] = Field(..., description="项目类型编码")
description: Optional[str] = Field(None, description="项目描述")
model_config = ConfigDict(from_attributes=True)
class ProjectInfoOut(BaseModel):
"""项目信息输出"""
id: Optional[int] = Field(None, description="项目id")
project_no: Optional[str] = Field(..., description="项目编号")
project_name: Optional[str] = Field(..., description="项目名称")
type_code: Optional[str] = Field(..., description="项目类型编码")
description: Optional[str] = Field(None, description="项目描述")
train_version: Optional[int] = Field(None, description="训练版本号")
project_status: Optional[str] = Field(None, description="项目状态")
model_config = ConfigDict(from_attributes=True)
class ProjectInfoPagerOut(BaseModel):
"""项目信息输出"""
id: Optional[int] = Field(None, description="项目id")
project_no: Optional[str] = Field(None, description="项目编号")
project_name: Optional[str] = Field(None, description="项目名称")
type_code: Optional[str] = Field(None, description="项目类型编码")
description: Optional[str] = Field(None, description="项目描述")
train_version: Optional[int] = Field(None, description="训练版本号")
project_status: Optional[str] = Field(None, description="项目状态")
mark_count: Optional[int] = Field(0, description="已标记数量")
no_mark_count: Optional[int] = Field(0, description="未标记数量")
model_config = ConfigDict(from_attributes=True)

View File

@ -0,0 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:28
# @File : project_label.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
class ProjectLabel(BaseModel):
"""项目标签输入输出"""
id: Optional[int] = Field(None, description="id")
project_id: Optional[int] = Field(None, description="项目id")
label_name: Optional[str] = Field(..., description="标签名称")
meta: Optional[dict] = Field(None, description="label属性")
model_config = ConfigDict(from_attributes=True)

View File

@ -0,0 +1,107 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:25
# @File : views.py
# @IDE : PyCharm
# @desc : 路由,视图文件
from utils.response import SuccessResponse, ErrorResponse
from . import params, schemas, crud, models
from core.dependencies import IdList
from fastapi import APIRouter, Depends
from apps.vadmin.auth.utils.current import FullAdminAuth
from apps.vadmin.auth.utils.validation.auth import Auth
app = APIRouter()
###########################################################
# 项目信息
###########################################################
@app.get("/list", summary="获取项目信息列表")
async def project_pager(
param: params.ProjectInfoParams = Depends(),
auth: Auth = Depends(FullAdminAuth())
):
datas, count = await crud.ProjectInfoDal(auth.db).get_project_pager(project=param, auth=auth)
return SuccessResponse(datas, count=count)
@app.post("/info", summary="新建项目")
async def add_project(
pro_in: schemas.ProjectInfoIn,
auth: Auth = Depends(FullAdminAuth())
):
check = await crud.ProjectInfoDal(auth.db).check_name(project_name=pro_in.project_name)
if check:
return ErrorResponse(msg="存在相同名称的项目,不能创建")
result = await crud.ProjectInfoDal(auth.db).add_project(pro_in, auth)
return SuccessResponse(data=result)
@app.get("/info/{pro_id}", summary="查询项目信息")
async def project(
pro_id: int,
auth: Auth = Depends(FullAdminAuth())
):
result = await crud.ProjectInfoDal(auth.db).get_data(data_id=pro_id, v_schema=schemas.ProjectInfoOut)
return SuccessResponse(data=result)
@app.delete("/info", summary="删除项目")
async def del_project(
pro_ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth())
):
await crud.ProjectInfoDal(auth.db).delete_datas(ids=pro_ids.ids, v_soft=True)
return SuccessResponse(msg="删除成功")
@app.get("/label/{pro_id}", summary="查询标签列表")
async def label_list(
pro_id: int,
auth: Auth = Depends(FullAdminAuth())
):
result = await crud.ProjectLabelDal(auth.db).get_datas(v_where=[models.ProjectLabel.project_id == pro_id],
v_schema=schemas.ProjectLabel)
return SuccessResponse(data=result)
@app.post("/label", summary="新建标签")
async def add_label(
label_in: schemas.ProjectLabel,
auth: Auth = Depends(FullAdminAuth())
):
check = await crud.ProjectLabelDal(auth.db).check_label_name(label_in.label_name, label_in.project_id)
if check:
return ErrorResponse(msg="存在相同名称的标签,不能新建")
await crud.ProjectLabelDal(auth.db).create_data(data=label_in)
return SuccessResponse(msg="新建成功")
@app.put("/label", summary="修改标签")
async def update_label(
label_in: schemas.ProjectLabel,
auth: Auth = Depends(FullAdminAuth())
):
check = await crud.ProjectLabelDal(auth.db).check_label_name(label_in.label_name, label_in.project_id, label_in.id)
if check:
return ErrorResponse(msg="存在相同名称的标签,不能修改")
await crud.ProjectLabelDal(auth.db).put_data(data_id=label_in.id, data=label_in)
return SuccessResponse(msg="修改成功")
@app.delete("/label", summary="删除标签")
async def delete_label(
label_ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth())
):
await crud.ProjectLabelDal(auth.db).delete_datas(label_ids.ids)
return SuccessResponse(msg="删除成功")

View File

View File

@ -0,0 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:32
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据访问层
from sqlalchemy.ext.asyncio import AsyncSession
from core.crud import DalBase
from . import models, schemas
class ProjectTrainDal(DalBase):
def __init__(self, db: AsyncSession):
super(ProjectTrainDal, self).__init__()
self.db = db
self.model = models.ProjectTrain
self.schema = schemas.ProjectTrainSimpleOut

View File

View File

@ -0,0 +1,24 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer
from db.db_base import BaseModel
class ProjectTrain(BaseModel):
"""
项目训练版本信息表
"""
__tablename__ = "project_train"
__table_args__ = ({'comment': '项目训练版本信息表'})
project_id: Mapped[int] = mapped_column(Integer, nullable=False)
train_version: Mapped[str] = mapped_column(String(32), nullable=False)
train_url: Mapped[str] = mapped_column(String(255), nullable=False)
train_data: Mapped[str] = mapped_column(String(255), nullable=False)
weights_id: Mapped[int] = mapped_column(Integer)
weights_name: Mapped[str] = mapped_column(String(32))
epochs: Mapped[int] = mapped_column(Integer)
patience: Mapped[int] = mapped_column(Integer)
best_pt: Mapped[str] = mapped_column(String(255), nullable=False)
last_pt: Mapped[str] = mapped_column(String(255), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, nullable=False)

View File

@ -0,0 +1 @@
from .project_train import ProjectTrainParams

View File

@ -0,0 +1,15 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:32
# @File : project_train.py
# @IDE : PyCharm
# @desc : 项目巡逻片信息
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ProjectTrainParams(QueryParams):
def __init__(self, params: Paging = Depends()):
super().__init__(params)

View File

@ -0,0 +1 @@
from .project_train import ProjectTrain, ProjectTrainSimpleOut

View File

@ -0,0 +1,32 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:32
# @File : project_train.py
# @IDE : PyCharm
# @desc : pydantic 模型,用于数据库序列化操作
from pydantic import BaseModel, Field, ConfigDict
from core.data_types import DatetimeStr
class ProjectTrain(BaseModel):
project_id: int = Field(..., title="None")
train_version: str = Field(..., title="None")
train_url: str = Field(..., title="None")
train_data: str = Field(..., title="None")
weights_id: int = Field(..., title="None")
weights_name: str = Field(..., title="None")
epochs: int = Field(..., title="None")
patience: int = Field(..., title="None")
best_pt: str = Field(..., title="None")
last_pt: str = Field(..., title="None")
user_id: int = Field(..., title="None")
class ProjectTrainSimpleOut(ProjectTrain):
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., title="编号")
create_datetime: DatetimeStr = Field(..., title="创建时间")
update_datetime: DatetimeStr = Field(..., title="更新时间")

View File

@ -0,0 +1,51 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2025/04/03 10:32
# @File : views.py
# @IDE : PyCharm
# @desc : 路由,视图文件
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, Depends
from . import models, schemas, crud, params
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth
from utils.response import SuccessResponse
from apps.vadmin.auth.utils.validation.auth import Auth
from core.database import db_getter
app = APIRouter()
###########################################################
# 项目巡逻片信息
###########################################################
@app.get("/project/train", summary="获取项目巡逻片信息列表", tags=["项目巡逻片信息"])
async def get_project_train_list(p: params.ProjectTrainParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas, count = await crud.ProjectTrainDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@app.post("/project/train", summary="创建项目巡逻片信息", tags=["项目巡逻片信息"])
async def create_project_train(data: schemas.ProjectTrain, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectTrainDal(auth.db).create_data(data=data))
@app.delete("/project/train", summary="删除项目巡逻片信息", description="硬删除", tags=["项目巡逻片信息"])
async def delete_project_train_list(ids: IdList = Depends(), auth: Auth = Depends(AllUserAuth())):
await crud.ProjectTrainDal(auth.db).delete_datas(ids=ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.put("/project/train/{data_id}", summary="更新项目巡逻片信息", tags=["项目巡逻片信息"])
async def put_project_train(data_id: int, data: schemas.ProjectTrain, auth: Auth = Depends(AllUserAuth())):
return SuccessResponse(await crud.ProjectTrainDal(auth.db).put_data(data_id, data))
@app.get("/project/train/{data_id}", summary="获取项目巡逻片信息信息", tags=["项目巡逻片信息"])
async def get_project_train(data_id: int, db: AsyncSession = Depends(db_getter)):
schema = schemas.ProjectTrainSimpleOut
return SuccessResponse(await crud.ProjectTrainDal(db).get_data(data_id, v_schema=schema))

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)

Some files were not shown because too many files have changed in this diff Show More