first commit
This commit is contained in:
commit
4431252e30
|
|
@ -0,0 +1,13 @@
|
||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
RUN pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
RUN pip3 config set install.trusted-host mirrors.aliyun.com
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
cd venv/Scripts
|
||||||
|
call activate
|
||||||
|
cd ../../
|
||||||
|
|
||||||
|
|
||||||
|
IF EXIST "dist" (
|
||||||
|
echo "Directory exist."
|
||||||
|
) else (
|
||||||
|
md dist
|
||||||
|
echo "Directory exist created"
|
||||||
|
)
|
||||||
|
|
||||||
|
copy main_pro.py .\dist\
|
||||||
|
|
||||||
|
copy config.toml .\dist\config.toml
|
||||||
|
|
||||||
|
call nuitka --mingw64 --show-progress --product-version=1.0.0 --standalone --output-dir=dist --onefile --windows-icon-from-ico=icon.ico --remove-output --output-filename=start.exe main_pro.py
|
||||||
|
|
||||||
|
pause
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
. venv/bin/activate
|
||||||
|
cd ../../
|
||||||
|
nuitka --mingw64 --show-progress --standalone --product-version=1.0.0 --output-dir=dist --onefile --remove-output main.py
|
||||||
|
cp main.py dist/main.py
|
||||||
|
cp config.toml dist/config.toml
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 服务基础配置
|
||||||
|
[base]
|
||||||
|
# 服务端口
|
||||||
|
port=3000
|
||||||
|
reload=true
|
||||||
|
# 进程数
|
||||||
|
workers=1
|
||||||
|
|
||||||
|
[project]
|
||||||
|
# 登录有效期,单位:天, 默认为十天
|
||||||
|
tokenTIme=0.1
|
||||||
|
|
||||||
|
|
||||||
|
[mysql.DB199]
|
||||||
|
host="192.168.18.199"
|
||||||
|
port=3306
|
||||||
|
username="root"
|
||||||
|
password="Chlry#$.8"
|
||||||
|
database="db_gp_cj"
|
||||||
|
|
||||||
|
[mysql.DB150]
|
||||||
|
host="192.168.16.150"
|
||||||
|
port=3306
|
||||||
|
username="root"
|
||||||
|
password="Chlry$%.8pattern"
|
||||||
|
database="factordb_mysql"
|
||||||
|
|
||||||
|
|
||||||
|
[redis.base]
|
||||||
|
ip="192.168.18.208"
|
||||||
|
port=6379
|
||||||
|
username=""
|
||||||
|
password="wlkj2018"
|
||||||
|
db=13
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[log]
|
||||||
|
# 是否开启debug
|
||||||
|
debug=false
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#coding=utf-8
|
||||||
|
# 开发
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from src.core.app import startApp
|
||||||
|
|
||||||
|
|
||||||
|
(app, config) = startApp()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
config = config.get("base")
|
||||||
|
if config:
|
||||||
|
# ,workers=config.get("workers")
|
||||||
|
uvicorn.run(app="main_dev:app",reload=True,port=config.get("port"),workers=3 )
|
||||||
|
else:
|
||||||
|
uvicorn.run(app="main_dev:app",reload=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
#coding=utf-8
|
||||||
|
# 打包运行
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from src.core.app import startApp
|
||||||
|
|
||||||
|
|
||||||
|
(app, config) = startApp()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
config = config.get("base")
|
||||||
|
if config:
|
||||||
|
# ,workers=config.get("workers")
|
||||||
|
uvicorn.run(app="main_pro:app",port=config.get("port"),workers=3 )
|
||||||
|
else:
|
||||||
|
uvicorn.run(app="main_pro:app")
|
||||||
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,18 @@
|
||||||
|
from pydantic import BaseModel,Field, field_validator,validator
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class UserData(BaseModel):
|
||||||
|
account: str = Field(min_length=5,max_length=12,title="账号", default="admin")
|
||||||
|
password: str = '123456'
|
||||||
|
|
||||||
|
# 密码复杂度验证
|
||||||
|
@field_validator("password")
|
||||||
|
@classmethod
|
||||||
|
def validator_password(cls, value):
|
||||||
|
# if re.match(r'^.*(?=.{6,})(?=.*\d)(?=.*[a-z]|[A-Z]{1,}).*$', value): # 真正需要再用
|
||||||
|
if len(value) >= 6:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise ValueError('请输入至少包一位字母和数字的密码')
|
||||||
|
# return respone.success(code=-1, msg="请输入至少包一位字母和数字")
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
#coding=utf-8
|
||||||
|
from fastapi import Request
|
||||||
|
from datetime import datetime
|
||||||
|
from src.utils.respone import success
|
||||||
|
|
||||||
|
async def get_timestamp(request: Request):
|
||||||
|
"""
|
||||||
|
获取服务端时间戳
|
||||||
|
"""
|
||||||
|
timestamp = int(datetime.now().timestamp() * 1000) # 毫秒时间戳
|
||||||
|
return success(data={"timestamp": timestamp}, msg="获取成功")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from src.utils.respone import fail, success
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_first_value(result):
|
||||||
|
if isinstance(result, (list, tuple)) and result:
|
||||||
|
return result[0]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def mysql_ping(request: Request, name: str):
|
||||||
|
mysql_clients = getattr(request.app, "mysql", {})
|
||||||
|
if not mysql_clients:
|
||||||
|
return fail(msg="MySQL 未初始化")
|
||||||
|
|
||||||
|
client = mysql_clients.get(name)
|
||||||
|
if client is None:
|
||||||
|
return fail(msg=f"未找到名为 {name} 的 MySQL 数据源")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await client.query("SELECT 1")
|
||||||
|
return success(
|
||||||
|
data={
|
||||||
|
"name": name,
|
||||||
|
"status": "ok" if result else "empty",
|
||||||
|
"result": _extract_first_value(result),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return fail(
|
||||||
|
msg="MySQL 连接失败",
|
||||||
|
ext={"name": name, "detail": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def mysql_overview(request: Request):
|
||||||
|
mysql_clients = getattr(request.app, "mysql", {})
|
||||||
|
if not mysql_clients:
|
||||||
|
return fail(msg="MySQL 未初始化")
|
||||||
|
|
||||||
|
overview: List[dict] = []
|
||||||
|
for name, client in mysql_clients.items():
|
||||||
|
try:
|
||||||
|
result = await client.query("SELECT 1")
|
||||||
|
overview.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"status": "ok" if result else "empty",
|
||||||
|
"result": _extract_first_value(result),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
overview.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"status": "error",
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return success(data=overview)
|
||||||
|
|
||||||
|
|
||||||
|
async def redis_ping(request: Request, name: str):
|
||||||
|
redis_clients = getattr(request.app, "redis", {})
|
||||||
|
if not redis_clients:
|
||||||
|
return fail(msg="Redis 未初始化")
|
||||||
|
|
||||||
|
client = redis_clients.get(name)
|
||||||
|
if client is None:
|
||||||
|
return fail(msg=f"未找到名为 {name} 的 Redis 数据源")
|
||||||
|
|
||||||
|
try:
|
||||||
|
pong = await client.ping()
|
||||||
|
return success(
|
||||||
|
data={
|
||||||
|
"name": name,
|
||||||
|
"status": "ok" if pong else "error",
|
||||||
|
"result": pong,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return fail(
|
||||||
|
msg="Redis 连接失败",
|
||||||
|
ext={"name": name, "detail": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from logging import log
|
||||||
|
from fastapi import APIRouter,Request
|
||||||
|
from src.controller.base.data_type import UserData
|
||||||
|
from src.utils.respone import success,fail
|
||||||
|
from src.utils.token import createToken,vertify_is_login
|
||||||
|
from src.core.life import REDIS
|
||||||
|
users_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def route_name(request: Request, userData: UserData):
|
||||||
|
|
||||||
|
# res = await Users.filter(account=userData.account).first()
|
||||||
|
for k in request:
|
||||||
|
print(k)
|
||||||
|
baseRedis = request["redis"]["base"]
|
||||||
|
await baseRedis.set("long.tests.authww", '踢啊n' )
|
||||||
|
redisstr = await baseRedis.get("long.tests.authww")
|
||||||
|
|
||||||
|
print(redisstr.decode('utf8'))
|
||||||
|
res=None
|
||||||
|
if(res == None):
|
||||||
|
return fail(msg="该用户不存在")
|
||||||
|
|
||||||
|
if(res.password != userData.password):
|
||||||
|
return fail(msg="密码错误")
|
||||||
|
|
||||||
|
token = createToken({"userId": res.userId, "account": res.account})
|
||||||
|
|
||||||
|
# await REDIS["base"].hset("long.tests.auth", res.userId, token)
|
||||||
|
print(vertify_is_login("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImFjY291bnQiOiJhZG1pbiIsImV4cCI6MTcyNTE4NDgxOX0.Aa_Hb_pGDpCVrZdJrDzkBJYJMmAYBDuzymuliHN4_bQ"))
|
||||||
|
|
||||||
|
return success(data=token)
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import logging
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from src.utils.config import read_config
|
||||||
|
from src.router import register_router
|
||||||
|
from src.middleware import register_middleware
|
||||||
|
from src.core.life import lifespan
|
||||||
|
from src.middleware.exception import register_exception
|
||||||
|
from src.core.log import log
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def startApp():
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
# 静态文件服务
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
# 中间件
|
||||||
|
register_middleware(app)
|
||||||
|
|
||||||
|
# 注册路由
|
||||||
|
register_router(app)
|
||||||
|
|
||||||
|
# 注册错误捕获
|
||||||
|
register_exception(app)
|
||||||
|
|
||||||
|
config = read_config()
|
||||||
|
|
||||||
|
# 开启日志显示
|
||||||
|
if config and config.get("log") and config.get("log")["debug"]:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
return (app,config)
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# 生命周期
|
||||||
|
#coding=utf-8
|
||||||
|
from doctest import debug
|
||||||
|
from math import erf, inf
|
||||||
|
from uu import Error
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from src.utils.config import read_config
|
||||||
|
# from tortoise.contrib.fastapi import register_tortoise
|
||||||
|
# from aioredis import create_redis_pool, Redis
|
||||||
|
import aioredis
|
||||||
|
from influxdb import InfluxDBClient
|
||||||
|
from src.core.mysql_core import init_mysql
|
||||||
|
# 导入定时任务
|
||||||
|
from src.scheduler import scheduler
|
||||||
|
|
||||||
|
|
||||||
|
# 生命周期函数,监听服务启动完毕和服务结束
|
||||||
|
|
||||||
|
REDIS = {}
|
||||||
|
MYSQL = {}
|
||||||
|
INFLUXDB={}
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# Load the ML model
|
||||||
|
# 将配置文件的配置挂在在app上
|
||||||
|
config = read_config()
|
||||||
|
app.state.config = config
|
||||||
|
|
||||||
|
# redis 配置
|
||||||
|
# try:
|
||||||
|
redisConfig = config["redis"]
|
||||||
|
mysqlConfig = config["mysql"]
|
||||||
|
|
||||||
|
print(redisConfig)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# 连接redis
|
||||||
|
for redisk in redisConfig:
|
||||||
|
redis = redisConfig.get(redisk)
|
||||||
|
REDIS[redisk] = await aioredis.from_url(
|
||||||
|
f'redis://{redis["ip"]}',
|
||||||
|
username=redis["username"],
|
||||||
|
password=redis["password"],
|
||||||
|
port=redis["port"],
|
||||||
|
db=redis["db"]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\033[1;32m========= redis-{redisk} connect success !!!=======\033[1;32m")
|
||||||
|
app.redis = REDIS
|
||||||
|
|
||||||
|
# 连接mysql(OK)
|
||||||
|
for mysql in mysqlConfig:
|
||||||
|
sql = mysqlConfig.get(mysql)
|
||||||
|
MYSQL[mysql] = await init_mysql(host=sql["host"],
|
||||||
|
port=sql["port"],
|
||||||
|
user=sql["username"],
|
||||||
|
password=sql["password"],
|
||||||
|
db=sql["database"], debug=True)
|
||||||
|
print(f"\033[1;32m========= mysql-{mysql} connect success !!!=======\033[1;32m")
|
||||||
|
|
||||||
|
app.mysql = MYSQL
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
print("连接Redis失败:", IOError)
|
||||||
|
|
||||||
|
|
||||||
|
print("\033[1;32m========= app ready success !!!=======\033[1;32m")
|
||||||
|
|
||||||
|
# 启动定时任务
|
||||||
|
scheduler.start()
|
||||||
|
print("任务队开始")
|
||||||
|
|
||||||
|
yield # 在 `yield` 之前,将在应用程序启动之前执行;而 `yield` 之后的部分将在应用程序完成之后执行。
|
||||||
|
|
||||||
|
# 关闭定时任务
|
||||||
|
scheduler.shutdown()
|
||||||
|
print("任务队结束")
|
||||||
|
|
||||||
|
print("生命到头")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from os import name
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class Log:
|
||||||
|
log=None
|
||||||
|
errLog=None
|
||||||
|
sysLog=None
|
||||||
|
traceLog=None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 日志输出规范
|
||||||
|
logger_format = "{time:YYYY-MM-DD HH:mm:ss,SSS} [{thread}] - {message}"
|
||||||
|
# 日志分割时间
|
||||||
|
rotation = "00:00"
|
||||||
|
# 日志保留时间, 空格一定要有
|
||||||
|
retention="7 days"
|
||||||
|
|
||||||
|
# filter = lambda record: record
|
||||||
|
logger.add('logs/{time:YYYY-MM-DD}/err_{time:YYYY-MM-DD}.log', rotation=rotation, retention=retention, format=logger_format, filter=lambda record: record["extra"].get("name") == "err")
|
||||||
|
logger.add('logs/{time:YYYY-MM-DD}/sys_{time:YYYY-MM-DD}.log', rotation=rotation, retention=retention, format=logger_format, filter=lambda record: record["extra"].get("name") == "sys")
|
||||||
|
logger.add('logs/{time:YYYY-MM-DD}/trace_{time:YYYY-MM-DD}.log', rotation=rotation, retention=retention, format=logger_format,filter=lambda record: record["extra"].get("name") == "trace")
|
||||||
|
|
||||||
|
self.errLog = logger.bind(name="err")
|
||||||
|
self.sysLog = logger.bind(name="sys")
|
||||||
|
self.traceLog = logger.bind(name="trace")
|
||||||
|
|
||||||
|
def err(self, msg):
|
||||||
|
self.errLog.error(msg)
|
||||||
|
|
||||||
|
def sys(self, msg):
|
||||||
|
self.sysLog.info(msg)
|
||||||
|
|
||||||
|
def trace(self, msg):
|
||||||
|
self.traceLog.info(msg)
|
||||||
|
|
||||||
|
|
||||||
|
log = Log()
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
|
||||||
|
#coding=utf-8
|
||||||
|
from ast import If
|
||||||
|
from enum import Flag
|
||||||
|
from tkinter import NO, SW
|
||||||
|
from typing import Any, Union
|
||||||
|
from uu import Error
|
||||||
|
from xmlrpc.client import Boolean
|
||||||
|
import aiomysql
|
||||||
|
import re
|
||||||
|
|
||||||
|
async def init_mysql(
|
||||||
|
host: str,
|
||||||
|
port: Union[int, str],
|
||||||
|
user: str,
|
||||||
|
password: str=None,
|
||||||
|
db: str=None,
|
||||||
|
debug = False
|
||||||
|
):
|
||||||
|
mysql = Mysql(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
db=db,
|
||||||
|
debug= debug
|
||||||
|
)
|
||||||
|
await mysql.connect()
|
||||||
|
return mysql
|
||||||
|
|
||||||
|
|
||||||
|
class Obj:
|
||||||
|
replacement: dict=None
|
||||||
|
whereStr: str=None
|
||||||
|
|
||||||
|
class ReplacementError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
数据类型字符添加,如字符串增加‘’
|
||||||
|
"""
|
||||||
|
def data_type_chart(data):
|
||||||
|
if isinstance(data, str) :
|
||||||
|
return f"'{data}'"
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def str_to_sql(sql, replacement)->str:
|
||||||
|
pattern = r'\(\s*(.*?)\s*\)' # 去掉括号中间的空格
|
||||||
|
sql = re.sub(pattern, r'(\1)', sql)
|
||||||
|
sqlList = sql.split(" ")
|
||||||
|
arr = []
|
||||||
|
|
||||||
|
for data in sqlList:
|
||||||
|
|
||||||
|
if re.search(r'=:', data):
|
||||||
|
# A=:B
|
||||||
|
data = data.split("=:")
|
||||||
|
key = data[1]
|
||||||
|
|
||||||
|
if (key in replacement) == False:
|
||||||
|
raise Error(f"清检查replacement->{key}")
|
||||||
|
|
||||||
|
replacement[key] = data_type_chart(replacement[key])
|
||||||
|
data = f'{data[0]}={replacement[key]}'
|
||||||
|
elif re.search(r'^\(\:', data):
|
||||||
|
# a IN/in (:)
|
||||||
|
key = data.replace('(:', '').replace(")", '')
|
||||||
|
value = ''
|
||||||
|
|
||||||
|
if (key in replacement) == False:
|
||||||
|
raise Error(f"清检查replacement->{data}")
|
||||||
|
|
||||||
|
for v in replacement[key]:
|
||||||
|
v = data_type_chart(v)
|
||||||
|
value += f',{v}'
|
||||||
|
|
||||||
|
data = f"({value[1:]})"
|
||||||
|
|
||||||
|
arr.append(data)
|
||||||
|
|
||||||
|
sql = ' '.join(arr)
|
||||||
|
return sql
|
||||||
|
|
||||||
|
class Mysql:
|
||||||
|
|
||||||
|
client: Any = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: Union[int, str],
|
||||||
|
user: str,
|
||||||
|
password: str=None,
|
||||||
|
db: str=None,
|
||||||
|
debug = False
|
||||||
|
):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.db = db
|
||||||
|
self.host = host
|
||||||
|
self.host = host
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
|
# 连接数据库
|
||||||
|
async def connect(self):
|
||||||
|
try:
|
||||||
|
conn = await aiomysql.connect(host=self.host, port=self.port, user=self.user, password=self.password, db=self.db)
|
||||||
|
self.client = await conn.cursor()
|
||||||
|
except Error as err:
|
||||||
|
# except aiomysql.OperationalError as err:
|
||||||
|
# 连接失败
|
||||||
|
print(f"\033[1;31m连接mysql(ip:'{self.host}',db:'{self.db}')失败,ERROR: {err}\033[0m")
|
||||||
|
raise aiomysql.OperationalError("连接失败")
|
||||||
|
|
||||||
|
# 单个查询
|
||||||
|
async def query(self, sql: str=None, obj: Obj=None):
|
||||||
|
if sql is None or len(sql) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
replacement = obj.get("replacement") if isinstance(obj, dict) else None
|
||||||
|
|
||||||
|
# 带替换标签,但没有给替换值
|
||||||
|
if re.search(r"\s*:\s*", sql) and replacement is None:
|
||||||
|
raise Error("缺少replacement")
|
||||||
|
|
||||||
|
sql = str_to_sql(sql, replacement)
|
||||||
|
|
||||||
|
where_str = obj.get("whereStr") if isinstance(obj, dict) else None
|
||||||
|
if where_str:
|
||||||
|
sql += str_to_sql(where_str, replacement)
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
print(sql)
|
||||||
|
|
||||||
|
await self.client.execute(sql)
|
||||||
|
|
||||||
|
return await self.client.fetchone()
|
||||||
|
|
||||||
|
# 多个查询
|
||||||
|
async def queryList(self, sql: str=None, obj: Obj=None):
|
||||||
|
if sql is None or len(sql) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
replacement = obj.get("replacement") if isinstance(obj, dict) else None
|
||||||
|
|
||||||
|
# 带替换标签,但没有给替换值
|
||||||
|
if re.search(r"\s*:\s*", sql) and replacement is None:
|
||||||
|
raise Error("缺少replacement")
|
||||||
|
|
||||||
|
sql = str_to_sql(sql, replacement)
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
print(sql)
|
||||||
|
|
||||||
|
await self.client.execute(sql)
|
||||||
|
|
||||||
|
return await self.client.fetchall()
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
from .authMiddleware import AuthMiddleware
|
||||||
|
from fastapi import Request,FastAPI,status
|
||||||
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
# 注册中间件
|
||||||
|
def register_middleware(app: FastAPI):
|
||||||
|
|
||||||
|
# cors解决跨域
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_headers=["*"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 健全
|
||||||
|
app.add_middleware(AuthMiddleware)
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
#coding=utf-8
|
||||||
|
from fastapi import Request,FastAPI,status
|
||||||
|
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
import re
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from starlette.responses import Response
|
||||||
|
from starlette.types import ASGIApp
|
||||||
|
from src.utils.respone import fail
|
||||||
|
from src.core.life import MYSQL, REDIS
|
||||||
|
|
||||||
|
# 权限验证
|
||||||
|
class AuthMiddleware(BaseHTTPMiddleware):
|
||||||
|
#
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(app)
|
||||||
|
|
||||||
|
#
|
||||||
|
async def dispatch(self, request: Request, call_next) -> Response:
|
||||||
|
|
||||||
|
# 将数据库和redis相关挂载到requst上
|
||||||
|
request.scope["redis"] = REDIS
|
||||||
|
request.scope["mysql"] = MYSQL
|
||||||
|
|
||||||
|
api = str(request.url).replace(str(request.base_url), '')
|
||||||
|
path = api.split("?")[0]
|
||||||
|
whitelist_prefixes = ("api/users/login", "api/diagnostics/", "api/demo/")
|
||||||
|
|
||||||
|
# api过滤
|
||||||
|
if (len(path) == 0 or (len(path) > 0 and re.match(r'api/', path))) and not any(
|
||||||
|
path.startswith(prefix) for prefix in whitelist_prefixes
|
||||||
|
):
|
||||||
|
# 不在忽略内的接口需要进行token验证
|
||||||
|
print("===未登录====", api)
|
||||||
|
return fail(status_code=status.HTTP_401_UNAUTHORIZED, msg="请登录")
|
||||||
|
|
||||||
|
request.scope["userInfo"] = "天才"
|
||||||
|
# for it in request:
|
||||||
|
# print(it)
|
||||||
|
response = await call_next(request)
|
||||||
|
response.status_code = status.HTTP_200_OK
|
||||||
|
print(">>>>", )
|
||||||
|
# 字段验证出错响应修改
|
||||||
|
# if response.status_code == 422:
|
||||||
|
# return JSONResponse(
|
||||||
|
# status_code=status.HTTP_200_OK,
|
||||||
|
# content=jsonable_encoder({'masg': '权限不够'}))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# 统一异常捕获处理
|
||||||
|
#coding=utf-8
|
||||||
|
from fastapi import FastAPI, Request, status
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from src.utils.respone import fail,success
|
||||||
|
import re
|
||||||
|
|
||||||
|
def register_exception(app: FastAPI):
|
||||||
|
|
||||||
|
"""
|
||||||
|
重写捕获字段验证报错
|
||||||
|
"""
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
def handler_try_Validation(request: Request, err: RequestValidationError):
|
||||||
|
print("err.body==", err.errors())
|
||||||
|
msg = err.errors()[0].get("msg")
|
||||||
|
if msg == "field required":
|
||||||
|
msg = "请求失败,缺少必填项!"
|
||||||
|
elif msg == "value is not a valid list":
|
||||||
|
print(err.errors())
|
||||||
|
msg = f"类型错误,提交参数应该为列表!"
|
||||||
|
elif msg == "value is not a valid int":
|
||||||
|
msg = f"类型错误,提交参数应该为整数!"
|
||||||
|
elif msg == "value could not be parsed to a boolean":
|
||||||
|
msg = f"类型错误,提交参数应该为布尔值!"
|
||||||
|
elif msg == "Input should be a valid list":
|
||||||
|
msg = f"类型错误,输入应该是一个有效的列表!"
|
||||||
|
elif re.match(r'Value error', msg):
|
||||||
|
arr = re.match(r'Value error', msg)
|
||||||
|
startIndex = arr.span()[1] + 1 # 加1是为了去掉逗号
|
||||||
|
msg = msg[startIndex:].strip() # strip()去掉字符串左右空白
|
||||||
|
|
||||||
|
return fail(msg)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
#coding=utf-8
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from src.router.base import baseApi
|
||||||
|
# 注册路由
|
||||||
|
def register_router(app:FastAPI):
|
||||||
|
app.include_router(baseApi)
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#coding=utf-8
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from src.controller.base import diagnostics, users, demo
|
||||||
|
|
||||||
|
baseApi = APIRouter()
|
||||||
|
|
||||||
|
baseApi.post('/api/users/login', tags=["用户管理"], summary="登录")(users.route_name)
|
||||||
|
baseApi.get(
|
||||||
|
'/api/diagnostics/mysql',
|
||||||
|
tags=["系统诊断"],
|
||||||
|
summary="MySQL 连接概览",
|
||||||
|
)(diagnostics.mysql_overview)
|
||||||
|
baseApi.get(
|
||||||
|
'/api/diagnostics/mysql/{name}',
|
||||||
|
tags=["系统诊断"],
|
||||||
|
summary="检测单个 MySQL 数据源",
|
||||||
|
)(diagnostics.mysql_ping)
|
||||||
|
baseApi.get(
|
||||||
|
'/api/diagnostics/redis/{name}',
|
||||||
|
tags=["系统诊断"],
|
||||||
|
summary="检测单个 Redis 数据源",
|
||||||
|
)(diagnostics.redis_ping)
|
||||||
|
baseApi.get(
|
||||||
|
'/api/demo/timestamp',
|
||||||
|
tags=["Demo"],
|
||||||
|
summary="获取服务端时间戳",
|
||||||
|
)(demo.get_timestamp)
|
||||||
|
|
||||||
|
# Demo 页面路由
|
||||||
|
@baseApi.get('/demo', tags=["Demo"], summary="Demo 页面")
|
||||||
|
@baseApi.get('/', tags=["Demo"], summary="首页")
|
||||||
|
async def demo_page():
|
||||||
|
return FileResponse('static/demo.html')
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
scheduler = AsyncIOScheduler()
|
||||||
|
|
||||||
|
from .everyOneMinute import everyOneMinute
|
||||||
|
# 每分钟
|
||||||
|
"""
|
||||||
|
--------------cron------------
|
||||||
|
year (int|str) – 4-digit year
|
||||||
|
|
||||||
|
month (int|str) – month (1-12)
|
||||||
|
|
||||||
|
day (int|str) – day of month (1-31)
|
||||||
|
|
||||||
|
week (int|str) – ISO week (1-53)
|
||||||
|
|
||||||
|
day_of_week (int|str) – number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)
|
||||||
|
|
||||||
|
hour (int|str) – hour (0-23)
|
||||||
|
|
||||||
|
minute (int|str) – minute (0-59)
|
||||||
|
|
||||||
|
second (int|str) – second (0-59)
|
||||||
|
|
||||||
|
start_date - 指定开始时间 (datetime|str) – starting point for the interval calculation
|
||||||
|
|
||||||
|
"""
|
||||||
|
# 注册定时任务
|
||||||
|
scheduler.add_job(everyOneMinute , "cron", hour='*/12')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
'''
|
||||||
|
每分钟执行一次
|
||||||
|
'''
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
def everyOneMinute():
|
||||||
|
|
||||||
|
print(f"The current time is {datetime.now()}")
|
||||||
|
|
||||||
|
print("较久")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# 配置文件数据操作
|
||||||
|
#coding=utf-8
|
||||||
|
import os
|
||||||
|
|
||||||
|
import toml
|
||||||
|
|
||||||
|
_config_cache = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config():
|
||||||
|
config_path = "config.toml"
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
return toml.load(config_path)
|
||||||
|
return toml.load("default-config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(key=None):
|
||||||
|
global _config_cache
|
||||||
|
try:
|
||||||
|
if _config_cache is None:
|
||||||
|
_config_cache = _load_config()
|
||||||
|
|
||||||
|
if key is not None:
|
||||||
|
return _config_cache.get(key)
|
||||||
|
return _config_cache
|
||||||
|
except IOError as e:
|
||||||
|
print("配置读取失败,请检查是否有配置文件或者配置是否正确", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
#coding=utf-8
|
||||||
|
from fastapi import Request,FastAPI,status
|
||||||
|
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from typing import Any,Dict,Optional
|
||||||
|
|
||||||
|
"""
|
||||||
|
# 成功响应规范
|
||||||
|
# data : Any 响应主题数据
|
||||||
|
# msg : str|None 请求消息
|
||||||
|
# code : int|1 1为成功,-1或0为失败
|
||||||
|
"""
|
||||||
|
def success(data: Any=None, msg: Optional[str]="请求成功", code=1, ext: Any=None, status_code=status.HTTP_200_OK):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status_code,
|
||||||
|
content=jsonable_encoder({
|
||||||
|
'data': data,
|
||||||
|
"msg": msg,
|
||||||
|
"code": code,
|
||||||
|
"ext": ext
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# 成功响应规范
|
||||||
|
# data : Any 响应主题数据
|
||||||
|
# msg : str|None 请求消息
|
||||||
|
# code : int|1 1为成功,-1或0为失败
|
||||||
|
"""
|
||||||
|
def fail(msg: Optional[str]="请求失败", code=-1, ext: Any=None, status_code=status.HTTP_200_OK ):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status_code,
|
||||||
|
content=jsonable_encoder({
|
||||||
|
'data': None,
|
||||||
|
"msg": msg,
|
||||||
|
"code": code,
|
||||||
|
"ext": ext
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#coding=utf-8
|
||||||
|
from ast import If
|
||||||
|
from typing import Any
|
||||||
|
import jwt
|
||||||
|
from jwt.exceptions import InvalidTokenError
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from datetime import datetime, timedelta,timezone
|
||||||
|
from src.utils.config import read_config
|
||||||
|
|
||||||
|
# 密钥,用于签名和验证JWT
|
||||||
|
SECRET_KEY = 'fastapi_longyu12'
|
||||||
|
# 令牌过期时间 默认十天
|
||||||
|
EXP = datetime.now(timezone.utc)+timedelta(days=1)
|
||||||
|
project = read_config("project")
|
||||||
|
if project.get("tokenTIme"):
|
||||||
|
EXP = datetime.now(timezone.utc)+ timedelta(days=project.get("tokenTIme"))
|
||||||
|
|
||||||
|
"""
|
||||||
|
生成 token 令牌
|
||||||
|
@payload: Any 存在token里面的信息
|
||||||
|
"""
|
||||||
|
def createToken(payload: Any):
|
||||||
|
payload["exp"] = EXP
|
||||||
|
|
||||||
|
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256' )
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
def vertify_is_login(token: str):
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"], verify=False)
|
||||||
|
print(int(payload.get("exp")) , int((datetime.now(timezone.utc)).timestamp()))
|
||||||
|
if int(payload.get("exp")) < int((datetime.now(timezone.utc)).timestamp()):
|
||||||
|
print("登录已过期")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
except jwt.exceptions.ExpiredSignatureError:
|
||||||
|
print("登录已过期")
|
||||||
|
return False
|
||||||
|
except (jwt.exceptions.InvalidSignatureError, jwt.exceptions.DecodeError):
|
||||||
|
print("无效凭证")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# def getTokenInfo():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 载荷,即JWT中包含的信息
|
||||||
|
# payload = {
|
||||||
|
# 'user_id': 123,
|
||||||
|
# 'username': 'john_doe',
|
||||||
|
# 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||||
|
# }
|
||||||
|
|
||||||
|
# # 生成JWT
|
||||||
|
# token = jwt.encode(payload, secret_key, algorithm='HS256')
|
||||||
|
# print('Generated JWT:', token)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: white;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp > div:first-child {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>量化因子评价系统 - Demo</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/demo.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>量化因子评价系统</h1>
|
||||||
|
<div class="timestamp">
|
||||||
|
<div>服务端时间戳:</div>
|
||||||
|
<div class="timestamp-value" id="timestamp">
|
||||||
|
<span class="loading">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/static/js/demo.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
async function fetchTimestamp() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/demo/timestamp');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 1) {
|
||||||
|
document.getElementById('timestamp').textContent = data.data.timestamp;
|
||||||
|
} else {
|
||||||
|
document.getElementById('timestamp').textContent = '获取失败: ' + data.msg;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('timestamp').textContent = '请求失败: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取时间戳
|
||||||
|
fetchTimestamp();
|
||||||
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
not
|
||||||
|
in- 检查字段的值是否位于传递列表中
|
||||||
|
not_in
|
||||||
|
gte- 大于或等于传递的值
|
||||||
|
gt- 大于传递的值
|
||||||
|
lte- 低于或等于传递的值
|
||||||
|
lt- 低于传递值
|
||||||
|
range- 介于和给定两个值之间
|
||||||
|
isnull- 字段为空
|
||||||
|
not_isnull- 字段不为空
|
||||||
|
contains- 字段包含指定的子字符串
|
||||||
|
icontains- 不区分大小写contains
|
||||||
|
startswith- 如果字段以值开头
|
||||||
|
istartswith- 不区分大小写startswith
|
||||||
|
endswith- 如果字段以值结尾
|
||||||
|
iendswith- 不区分大小写endswith
|
||||||
|
iequals- 区分大小写等于
|
||||||
|
|
||||||
|
source_field :自定义数据库对应字段名称
|
||||||
|
generated :是否映射到数据库,
|
||||||
|
pk 是否为主键
|
||||||
|
null
|
||||||
|
default 可以为值或可调用对象
|
||||||
|
unique
|
||||||
|
index
|
||||||
|
description 描述功能,数据库注释用
|
||||||
|
|
||||||
|
# 换源
|
||||||
|
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
pip config set install.trusted-host mirrors.aliyun.com
|
||||||
|
|
||||||
|
# 安装aioredis报错:duplicate base class TimeoutError
|
||||||
|
|
||||||
|
解决方法:https://blog.csdn.net/ViniJack/article/details/131809573
|
||||||
|
|
||||||
|
# 创建虚拟环境
|
||||||
|
```
|
||||||
|
python -m venv venv
|
||||||
|
```
|
||||||
|
# 进入虚拟环境
|
||||||
|
```
|
||||||
|
venv/Scripts/activate
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
# 生成类似 packpage.json的文件
|
||||||
|
|
||||||
|
```python
|
||||||
|
pip freeze > requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装requirements.txt依赖
|
||||||
|
|
||||||
|
```python
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
++++++++++++++++Linux++++++++++++++++++++++
|
||||||
|
|
||||||
|
#创建虚拟环境
|
||||||
|
python3 -m venv venv
|
||||||
|
|
||||||
|
. venv/bin/activate
|
||||||
Loading…
Reference in New Issue