项目初次搭建

This commit is contained in:
2025-02-13 16:29:28 +08:00
parent feef37cbd7
commit 3cb2a4c507
121 changed files with 19550 additions and 0 deletions

View File

@ -0,0 +1,19 @@
from typing import List
from fastapi import APIRouter
from fastapi import UploadFile
import os
upload = APIRouter()
@upload.post("/")
def upload_file(files: List[UploadFile]):
paths = []
for file in files:
path = os.path.join("images", file.filename)
with open(path, "wb") as f:
for line in file.file:
f.write(line)
paths.append(path)
return paths

34
app/api/sys/login_api.py Normal file
View File

@ -0,0 +1,34 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.db_session import get_db
from app.model.schemas.sys_user_schemas import SysUserLogin
from app.common import reponse_code as rc
from app.model.crud import sys_user_crud as us
from app.common.bcrypt_pw import verify_password
from app.common.jwt_check import generate_token
from app.common.redis_cli import redis_conn
"""
用户登录统一接口
"""
login = APIRouter()
@login.post("/")
def login_check(user: SysUserLogin, session: Session = Depends(get_db)):
"""
登录验证并返回token
:param session:
:param user: 登录参数信息
:return: response
"""
query_user = us.get_user_by_username(username=user.username, session=session)
if query_user is None:
return rc.response_error('您输入的用户名不正确')
if not verify_password(user.password, query_user.password):
return rc.response_error("您输入的密码不正确")
if not query_user.user_status == '0':
return rc.response_error(msg="登录失败,账号已被禁用,请联系管理员")
token = generate_token(query_user.id, query_user.username)
redis_conn.set(query_user.id, token)
return rc.response_success(msg="登录成功", data=token)

View File

@ -0,0 +1,84 @@
from fastapi import APIRouter, Depends
from app.model.schemas.sys_user_schemas import SysUserOut, SysUserIN, SysUserPager
from app.common import reponse_code as rc
from app.model.crud import sys_user_crud as us
from app.model.model import SysUser
from app.common.redis_cli import redis_conn
from sqlalchemy.orm import Session
from app.db.db_session import get_db
"""
用户管理模块
"""
user = APIRouter()
@user.post("/pager")
def user_pager(user: SysUserPager, session: Session = Depends(get_db)):
pager = us.user_pager(user, session)
return rc.response_success_pager(pager)
@user.post("/")
def add_user(user: SysUserIN, session: Session = Depends(get_db)):
"""
新增用户
:param session:
:param user: 用户信息
:return:
"""
if us.check_username(user.username, session):
return rc.response_error(msg="该用户名已存在!")
else:
user_in= SysUser(**user.dict())
user_in.user_status = '0'
if us.add_user(user_in, session):
return rc.response_success(msg="保存成功")
else:
return rc.response_error(msg="保存失败")
@user.get("/{id}")
def get_user(id: int, session: Session = Depends(get_db)):
"""
根据用户id获取用户信息
:param session:
:param id: 用户id
:return: 用户信息
"""
user = us.get_user_by_id(id, session)
if user is None:
return rc.response_success(data=None)
user_out = SysUserOut(**dict(user))
return rc.response_success(data=user_out.dict())
@user.post("/stop/{id}")
def stop_user(id: int, session: Session = Depends(get_db)):
"""
停用用户。修改用户状态并删除保存过的登录redis
:param session:
:param id:
:return:
"""
user = us.get_user_by_id(id, session)
if user is None:
return rc.response_error("用户查询错误,请稍后再试")
us.stop_user(user)
redis_conn.delete(id)
return rc.response_success("停用用户成功")
@user.post("/start/{id}")
def start_user(id: int, session: Session = Depends(get_db)):
"""
启用用户。修改用户状态
:param session:
:param id:
:return:
"""
user = us.get_user_by_id(id, session)
if user is None:
return rc.response_error("用户查询错误,请稍后再试")
us.start_user(user)
return rc.response_success("启用用户成功")

23
app/application/app.py Normal file
View File

@ -0,0 +1,23 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.application.token_middleware import TokenMiddleware
from app.application.logger_middleware import LoggerMiddleware
my_app = FastAPI()
'''
添加CROS中间件允许跨域请求
'''
my_app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
#注意中间的顺序,这个地方是倒序执行的
my_app.add_middleware(LoggerMiddleware)
my_app.add_middleware(TokenMiddleware)

View File

@ -0,0 +1,23 @@
from starlette.middleware.base import BaseHTTPMiddleware
from urllib.request import Request
from app.common.jwt_check import check_token
from app.common.logger_config import logger_http
class LoggerMiddleware(BaseHTTPMiddleware):
def __init__(self, app):
super().__init__(app)
async def dispatch(self, request: Request, call_next):
method = request.method
path = request.url.path
token = request.headers.get("Authorization")
user_id = None
if token:
decoded_payload = check_token(token)
user_id = decoded_payload['user_id']
logger_http.info(f"Path: {path},UserId: {user_id}, Method: {method}")
response = await call_next(request)
return response

View File

@ -0,0 +1,33 @@
from fastapi import status
from starlette.middleware.base import BaseHTTPMiddleware
from urllib.request import Request
from jwt import PyJWTError
from common import reponse_code as rc
from common import jwt_check as jc
class TokenMiddleware(BaseHTTPMiddleware):
def __init__(self, app):
super().__init__(app)
async def dispatch(self, request: Request, call_next):
"""
验证token中间件
:param request: Request请求
:param call_next:
:return:
"""
token = request.headers.get('Authorization')
path = request.url.path
if '/login' in path:
response = await call_next(request)
return response
if not token:
return rc.response_code_view(status.HTTP_401_UNAUTHORIZED, "缺少Token请重新验证")
try:
jc.check_token(token)
return await call_next(request)
except PyJWTError as error:
return rc.response_code_view(status.HTTP_401_UNAUTHORIZED, "Token错误或失效请重新验证")

13
app/common/bcrypt_pw.py Normal file
View File

@ -0,0 +1,13 @@
import bcrypt
#使用bcrypt对密码进行加密
def hash_password(password):
# 生成盐值并使用 bcrypt 加密密码
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed
def verify_password(provided_password, stored_password):
# 验证提供的密码是否与存储的哈希值匹配
return bcrypt.checkpw(provided_password.encode('utf-8'), stored_password.encode('utf-8'))

50
app/common/jwt_check.py Normal file
View File

@ -0,0 +1,50 @@
import datetime
import jwt
from app.common.redis_cli import redis_conn
# 过期时间单位S
exp = 6000
# 加密秘钥
secret_key = 'syg15684712291'
def generate_token(user_id: int, username: str):
"""
根据用户id和用户名生成一个token
:param user_id: 用户id
:param username: 用户名
:return: token
"""
payload = {
'user_id': user_id,
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(exp)
}
# 生成token
token = jwt.encode(payload, secret_key, algorithm='HS256')
return token
def check_token(token: str):
"""
验证token
:param token: token
:return: True or False
"""
try:
decoded_payload = jwt.decode(token, secret_key, algorithms=['HS256'])
user_id = decoded_payload['user_id']
token_redis = redis_conn.get(user_id)
if token_redis is None:
raise jwt.ExpiredSignatureError("Expired Token")
if token_redis != token:
raise jwt.ExpiredSignatureError("Invalid Token")
return decoded_payload
except jwt.ExpiredSignatureError:
raise jwt.ExpiredSignatureError("Expired Token")
except jwt.InvalidTokenError:
raise jwt.InvalidTokenError("Invalid Token")

View File

@ -0,0 +1,33 @@
import logging
import os
from logging.handlers import TimedRotatingFileHandler
from app.config.config_reader import log_dir
file_suffix = "%Y-%m-%d"
os.makedirs(log_dir, exist_ok=True)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
#所有http请求的日志
logger_http = logging.getLogger("api_log")
logger_http.setLevel(logging.DEBUG)
# 创建一个TimedRotatingFileHandler指定日志文件名、轮转周期和备份数量
api_log_file = os.path.join(log_dir, 'api.log')
api_handler = TimedRotatingFileHandler(api_log_file, when="midnight", interval=1, backupCount=30)
api_handler.setFormatter(formatter)
api_handler.suffix = file_suffix # 日志文件的后缀为日期格式
logger_http.addHandler(api_handler)
#所有sqlalchemy打印的日志
logger_sql = logging.getLogger("sqlalchemy.engine.Engine")
logger_sql.setLevel(logging.DEBUG)
# 创建文件处理器并设置级别
sql_log_file = os.path.join(log_dir, 'sql.log')
sql_handler = TimedRotatingFileHandler(sql_log_file, when="midnight", interval=1, backupCount=30)
sql_handler.setFormatter(formatter)
sql_handler.suffix = file_suffix # 日志文件的后缀为日期格式
logger_sql.addHandler(sql_handler)

4
app/common/redis_cli.py Normal file
View File

@ -0,0 +1,4 @@
import redis
from app.config.config_reader import redis_db, redis_password, redis_port, redis_host
redis_conn = redis.Redis(host=redis_host, port=redis_port, db=redis_db, password=redis_password, decode_responses=True)

View File

@ -0,0 +1,46 @@
from fastapi.responses import JSONResponse, Response
from fastapi import status
from app.db.page_util import Pager
def response_code_view(code: int,msg: str) -> Response:
return JSONResponse(
status_code=code,
content={
'code': code,
'msg': msg
}
)
def response_success(msg: str = "查询成功", data: object = None):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
'code': 200,
'msg': msg,
'data': data,
}
)
def response_success_pager(pager: Pager):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
'code': 200,
'msg': "查询成功",
'data': pager.data,
'total': pager.total
}
)
def response_error(msg:str):
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
'code': 500,
'msg': msg,
'data': None,
}
)

View File

@ -0,0 +1,11 @@
[mysql]
database_url = mysql+pymysql://root:root@localhost:3306/aicheckv2
[redis]
host = localhost
port = 6379
db = 0
password = sdust2020
[log]
dir = D:\syg\workspace\logs

View File

@ -0,0 +1,11 @@
[mysql]
database_url = mysql+pymysql://root:root@localhost:3306/sun
[redis]
host = localhost
port = 6379
db = 0
password = 123456
[log]
dir = /Users/macbookpro/sunyg/workspace/logs

View File

@ -0,0 +1,21 @@
import configparser
import os
env = "dev"
script_directory = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_directory, f'application_config_{env}.ini')
config = configparser.ConfigParser()
config.read(config_path)
database_url = config['mysql']['database_url']
redis_host = config.get('redis', 'host')
redis_port = config.get('redis', 'port')
redis_db = config.get('redis', 'db')
redis_password = config.get('redis', 'password')
log_dir = config.get('log', 'dir')

20
app/db/db_base.py Normal file
View File

@ -0,0 +1,20 @@
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import Mapped, mapped_column
import datetime
Base = declarative_base()
class DbCommon(Base):
__abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True)
create_time: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow())
update_time: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow(), onupdate=datetime.datetime.utcnow())
def keys(self):
return ["id", "create_time", "update_time"]
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()

9
app/db/db_engine.py Normal file
View File

@ -0,0 +1,9 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
from app.config.config_reader import database_url
db_engine = create_engine(database_url, echo=True, echo_pool=True)
db_session = sessionmaker(bind=db_engine, autoflush=False, autocommit=False, expire_on_commit=True)
session = scoped_session(db_session)

9
app/db/db_session.py Normal file
View File

@ -0,0 +1,9 @@
from app.db.db_engine import db_session
def get_db():
session = db_session()
try:
yield session
finally:
session.close()

26
app/db/page_util.py Normal file
View File

@ -0,0 +1,26 @@
from sqlalchemy.orm.query import Query
def get_pager(query: Query, pagerNum: int, pagerSize: int, ):
total = query.count()
data = query.limit(pagerSize).offset((pagerNum - 1) * pagerSize).all()
pager = Pager(total, data)
return pager
class Pager:
def __init__(self, total: int, data: object):
self.total = total
self.data = data
def keys(self):
keys = ["total", "data"]
return keys
def __getitem__(self, item):
'''
内置方法, 当使用obj['name']的形式的时候, 将调用这个方法, 这里返回的结果就是值
:param item:
:return:
'''
return getattr(self, item, None)

5
app/main.py Normal file
View File

@ -0,0 +1,5 @@
import uvicorn
from app.application.app import my_app
if __name__ == '__main__':
uvicorn.run("main:my_app", port=8080, reload=True)

View File

@ -0,0 +1,67 @@
from app.model.model import SysUser
from app.model.schemas.sys_user_schemas import SysUserPager, SysUserOut
from app.common.bcrypt_pw import hash_password
from app.db.page_util import get_pager
from sqlalchemy import and_, desc
from sqlalchemy.orm import Session
def user_pager(user: SysUserPager, session: Session):
query = session.query(SysUser).order_by(desc(SysUser.id))
filters = []
if user.username is not None:
filters.append(SysUser.username.ilike(f"%{user.username}%"))
if user.dept_id is not None:
filters.append(SysUser.dept_id == user.dept_id)
if len(filters) > 0:
query.filter(and_(*filters))
pager = get_pager(query, user.pagerNum, user.pagerSize)
pager.data = [SysUserOut.from_orm(user) for user in pager.data]
return pager
def add_user(user: SysUser, session: Session):
user.password = hash_password(user.password)
session.add(user)
session.commit()
return user
def get_user_by_id(id: int, session: Session):
user = session.query(SysUser).filter(SysUser.id == id).first()
return user
def stop_user(user: SysUser, session: Session):
user.user_status = "1"
session.commit();
return user
def start_user(user: SysUser, session: Session):
user.user_status = "0"
session.commit();
return user
def get_user_by_username(username: str, session: Session):
user = session.query(SysUser).filter(SysUser.username == username).first()
return user
# 验证username的唯一性
def check_username(username: str, session: Session):
count = session.query(SysUser).filter(SysUser.username == username).count()
if count > 0:
return True
else:
return False
# 修改密码
def update_pw(user: SysUser, session: Session):
session.query(SysUser).filter_by(id=user.id).update({
"password": user.password
})
session.commit()

29
app/model/model.py Normal file
View File

@ -0,0 +1,29 @@
from app.db.db_base import DbCommon
from sqlalchemy import String,Integer
from sqlalchemy.orm import Mapped, mapped_column
class SysUser(DbCommon):
__tablename__ = "sys_user"
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(255))
dept_id: Mapped[int] = mapped_column(Integer)
login_name: Mapped[str] = mapped_column(String(255))
avatar: Mapped[str] = mapped_column(String(255))
user_status: Mapped[str] = mapped_column(String(10))
def keys(self):
keys = ["username", "password", "dept_id", "login_name", "avatar", "user_status"]
keys.extend(super().keys())
return keys
def __getitem__(self, item):
'''
内置方法, 当使用obj['name']的形式的时候, 将调用这个方法, 这里返回的结果就是值
:param item:
:return:
'''
return getattr(self, item, None)

View File

@ -0,0 +1,43 @@
from pydantic import BaseModel, Field
from typing import Optional
# 用户相关的原型
class SysUserIN(BaseModel):
username: Optional[str] = Field(..., description="用户名", max_length=50)
password: Optional[str] = Field(..., description="密码", max_length=30, min_length=6)
dept_id: Optional[str] = Field(None, description="部门id")
login_name: Optional[str] = Field(None, description="昵称", max_length=20)
class SysUserLogin(BaseModel):
username: Optional[str] = Field(..., description="用户名", max_length=50)
password: Optional[str] = Field(..., description="密码", max_length=30, min_length=6)
class SysUserOut(BaseModel):
id: Optional[int] = Field(..., description="id")
username: Optional[str] = Field(..., description="用户名")
dept_id: Optional[str] = Field(None, description="部门id")
dept_name: Optional[str] = Field(None, description="部门名称")
login_name: Optional[str] = Field(None, description="昵称")
class Config:
orm_mode = True
class SysUserUpdatePw(BaseModel):
id: Optional[int] = Field(..., description="id")
new_password: Optional[str] = Field(..., description="新密码", max_length=30, min_length=8)
original_password: Optional[str] = Field(..., description="旧密码", max_length=30, min_length=8)
class SysUserPager(BaseModel):
username: Optional[str] = Field(None, description="用户名")
dept_id: Optional[str] = Field(None, description="部门id")
login_name: Optional[str] = Field(None, description="昵称")
pagerNum: Optional[int] = Field(1, description="当前页码")
pagerSize: Optional[int] = Field(10, description="每页数量")
class Config:
orm_mode = True