Refactor config, logging, and startup structure
Introduced pkg modules for environment config, logging, and startup initialization. Replaced direct config and logging setup in main.py with modularized functions. Updated database and migration modules to use environment variables and improved DEBUG handling. Removed tool.py and migrated password utilities to pkg. Cleaned up legacy comments and unused code in models and routes.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,4 +11,6 @@ dist/
|
||||
__pycache__/
|
||||
|
||||
# VsCodeCounter Data
|
||||
.VSCodeCounter/
|
||||
.VSCodeCounter/
|
||||
|
||||
.env
|
||||
@@ -8,9 +8,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../Findreve-NiceGUI"
|
||||
},
|
||||
{
|
||||
"path": "../vein-based-iov-simulator-main"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
||||
6
app.py
6
app.py
@@ -33,18 +33,18 @@ app.include_router(object.Router)
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
if not os.path.exists("dist/index.html"):
|
||||
raise HTTPException(status_code=404, detail="Frontend not built. Please build the frontend first.")
|
||||
raise HTTPException(status_code=404)
|
||||
return FileResponse("dist/index.html")
|
||||
|
||||
# 回退路由
|
||||
@app.get("/{path:path}")
|
||||
async def serve_spa(request: Request, path: str):
|
||||
if not os.path.exists("dist/index.html"):
|
||||
raise HTTPException(status_code=404, detail="Frontend not built. Please build the frontend first.")
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
# 排除API路由
|
||||
if path.startswith("api/"):
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
# 检查是否是静态资源请求
|
||||
if path.startswith("assets/") and os.path.exists(f"dist/{path}"):
|
||||
|
||||
40
main.py
40
main.py
@@ -1,25 +1,35 @@
|
||||
# 初始化数据库
|
||||
import asyncio
|
||||
from model.database import Database
|
||||
asyncio.run(Database().init_db())
|
||||
from loguru import logger
|
||||
|
||||
# 导入库
|
||||
# 导入配置模块
|
||||
from pkg.env import load_config
|
||||
from pkg.logger import setup_logging
|
||||
from pkg.startup import startup
|
||||
|
||||
# 加载配置
|
||||
host, port, debug = load_config()
|
||||
|
||||
# 配置日志
|
||||
setup_logging(debug)
|
||||
|
||||
# 记录启动信息
|
||||
logger.info(f"Debug mode: {'enabled' if debug else 'disabled'}")
|
||||
logger.info(f"Starting Findreve on http://{host}:{port}")
|
||||
|
||||
# 导入应用实例
|
||||
from app import app
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import logging
|
||||
|
||||
# 添加静态文件目录
|
||||
try:
|
||||
# 挂载静态文件目录
|
||||
app.mount("/dist", StaticFiles(directory="dist"), name="dist")
|
||||
except RuntimeError as e:
|
||||
logging.warning(f'无法挂载静态目录: {str(e)}, 将启动纯后端模式')
|
||||
# 执行启动流程
|
||||
startup(app)
|
||||
|
||||
# 作为主程序启动时
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
|
||||
# 启动服务器
|
||||
uvicorn.run(
|
||||
'app:app',
|
||||
host='0.0.0.0',
|
||||
port=8080
|
||||
host=host,
|
||||
port=port,
|
||||
log_config=None, # 禁用 uvicorn 默认的日志配置,使用 loguru
|
||||
reload=debug, # 调试模式下启用热重载
|
||||
)
|
||||
@@ -1,6 +1,8 @@
|
||||
# ~/models/database.py
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -9,11 +11,17 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from .migration import migration
|
||||
|
||||
ASYNC_DATABASE_URL = "sqlite+aiosqlite:///data.db"
|
||||
# 加载环境变量
|
||||
load_dotenv('.env')
|
||||
|
||||
# 获取 DEBUG 配置
|
||||
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
|
||||
|
||||
ASYNC_DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///data.db")
|
||||
|
||||
engine: AsyncEngine = create_async_engine(
|
||||
ASYNC_DATABASE_URL,
|
||||
echo=True,
|
||||
echo=DEBUG, # 根据 DEBUG 配置决定是否输出 SQL 日志
|
||||
connect_args={
|
||||
"check_same_thread": False
|
||||
} if ASYNC_DATABASE_URL.startswith("sqlite") else {},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from loguru import logger
|
||||
from sqlmodel import select
|
||||
from .setting import Setting
|
||||
from pkg.password import Password
|
||||
from pkg import Password
|
||||
|
||||
default_settings: list[Setting] = [
|
||||
Setting(type='string', name='version', value='1.0.0'),
|
||||
@@ -33,56 +33,4 @@ async def migration(session):
|
||||
to_insert = [s for s in settings if s.name not in existed]
|
||||
if to_insert:
|
||||
# 使用你写好的通用新增方法(是类方法),并传入会话
|
||||
await Setting.add(session, to_insert, refresh=False)
|
||||
|
||||
"""
|
||||
# 初始化设置表数据
|
||||
async with db.execute("SELECT name FROM fr_settings WHERE name = 'version'") as cursor:
|
||||
if not await cursor.fetchone():
|
||||
await db.execute(
|
||||
"INSERT INTO fr_settings (type, name, value) VALUES (?, ?, ?)",
|
||||
('string', 'version', '1.0.0')
|
||||
)
|
||||
logging.info("插入初始版本信息: version 1.0.0")
|
||||
|
||||
async with db.execute("SELECT name FROM fr_settings WHERE name = 'ver'") as cursor:
|
||||
if not await cursor.fetchone():
|
||||
await db.execute(
|
||||
"INSERT INTO fr_settings (type, name, value) VALUES (?, ?, ?)",
|
||||
('int', 'ver', '1')
|
||||
)
|
||||
logging.info("插入初始版本号: ver 1")
|
||||
|
||||
async with db.execute("SELECT name FROM fr_settings WHERE name = 'account'") as cursor:
|
||||
if not await cursor.fetchone():
|
||||
account = 'admin@yuxiaoqiu.cn'
|
||||
await db.execute(
|
||||
"INSERT INTO fr_settings (type, name, value) VALUES (?, ?, ?)",
|
||||
('string', 'account', account)
|
||||
)
|
||||
logging.info(f"插入初始账号信息: {account}")
|
||||
print(f"账号: {account}")
|
||||
|
||||
async with db.execute("SELECT name FROM fr_settings WHERE name = 'password'") as cursor:
|
||||
if not await cursor.fetchone():
|
||||
password = tool.generate_password()
|
||||
hashed_password = tool.hash_password(password)
|
||||
await db.execute(
|
||||
"INSERT INTO fr_settings (type, name, value) VALUES (?, ?, ?)",
|
||||
('string', 'password', hashed_password)
|
||||
)
|
||||
logging.info("插入初始密码信息")
|
||||
print(f"密码(请牢记,后续不再显示): {password}")
|
||||
|
||||
async with db.execute("SELECT name FROM fr_settings WHERE name = 'SECRET_KEY'") as cursor:
|
||||
if not await cursor.fetchone():
|
||||
secret_key = tool.generate_password(64)
|
||||
await db.execute(
|
||||
"INSERT INTO fr_settings (type, name, value) VALUES (?, ?, ?)",
|
||||
('string', 'SECRET_KEY', secret_key)
|
||||
)
|
||||
logging.info("插入初始密钥信息")
|
||||
|
||||
await db.commit()
|
||||
logging.info("数据库初始化完成并提交更改")
|
||||
"""
|
||||
await Setting.add(session, to_insert, refresh=False)
|
||||
@@ -1,5 +1,3 @@
|
||||
# my_project/models/download.py
|
||||
|
||||
from typing import Literal
|
||||
from sqlmodel import Field, Column, String, DateTime
|
||||
from .base import TableBase, IdMixin
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
# model/setting.py
|
||||
from sqlmodel import Field
|
||||
from .base import TableBase
|
||||
|
||||
"""
|
||||
原表:
|
||||
CREATE TABLE IF NOT EXISTS fr_settings (
|
||||
type TEXT,
|
||||
name TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
class Setting(TableBase, table=True):
|
||||
|
||||
type: str = Field(index=True, nullable=False, description="设置类型")
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
from passlib.context import CryptContext
|
||||
|
||||
# FastAPI 鉴权模型
|
||||
# FastAPI authentication model
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: str | None = None
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
username: str | None = None
|
||||
1
pkg/__init__.py
Normal file
1
pkg/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .password import Password
|
||||
48
pkg/env.py
Normal file
48
pkg/env.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
环境变量管理模块
|
||||
|
||||
负责 .env 文件的创建和环境变量的加载
|
||||
"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def ensure_env_file(env_file_path: str = '.env') -> None:
|
||||
"""
|
||||
确保 .env 文件存在,如果不存在则创建默认配置
|
||||
|
||||
:param env_file_path: .env 文件路径
|
||||
"""
|
||||
if not os.path.exists(env_file_path):
|
||||
default_env_content = """HOST=127.0.0.1
|
||||
PORT=8167
|
||||
|
||||
DEBUG=false
|
||||
|
||||
DATABASE_URL=sqlite+aiosqlite:///data.db
|
||||
"""
|
||||
with open(env_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(default_env_content)
|
||||
print(f"Created default .env file at {env_file_path}")
|
||||
|
||||
|
||||
def load_config() -> Tuple[str, int, bool]:
|
||||
"""
|
||||
加载配置信息
|
||||
|
||||
:return: (host, port, debug) 元组
|
||||
"""
|
||||
# 确保 .env 文件存在
|
||||
ensure_env_file()
|
||||
|
||||
# 从.env文件加载环境变量
|
||||
load_dotenv('.env')
|
||||
|
||||
# 从环境变量中加载主机、端口、调试模式等配置
|
||||
host = os.getenv("HOST", "127.0.0.1")
|
||||
port = int(os.getenv("PORT", 8167))
|
||||
debug = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
|
||||
|
||||
return host, port, debug
|
||||
75
pkg/logger.py
Normal file
75
pkg/logger.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
日志配置模块
|
||||
|
||||
负责配置 loguru 和接管标准库的日志
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""
|
||||
拦截标准库的日志并转发到 loguru
|
||||
"""
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# 获取对应的 loguru 级别
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# 查找调用者的帧
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
|
||||
|
||||
def setup_logging(debug: bool = False) -> None:
|
||||
"""
|
||||
配置日志系统
|
||||
|
||||
:param debug: 是否启用调试模式
|
||||
"""
|
||||
# 配置 loguru
|
||||
# 移除默认的 handler
|
||||
logger.remove()
|
||||
|
||||
# 根据 DEBUG 模式配置不同的日志级别和格式
|
||||
if debug:
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="DEBUG"
|
||||
)
|
||||
else:
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="INFO"
|
||||
)
|
||||
|
||||
# 接管 uvicorn 的日志
|
||||
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
|
||||
|
||||
# 接管 SQLAlchemy 的日志
|
||||
logging.getLogger("sqlalchemy.engine").handlers = [InterceptHandler()]
|
||||
logging.getLogger("sqlalchemy.pool").handlers = [InterceptHandler()]
|
||||
|
||||
# 设置日志级别
|
||||
if debug:
|
||||
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.DEBUG)
|
||||
logging.getLogger("uvicorn.error").setLevel(logging.DEBUG)
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
||||
logging.getLogger("sqlalchemy.pool").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
||||
logging.getLogger("uvicorn.error").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
||||
@@ -9,7 +9,7 @@ class Password():
|
||||
|
||||
@staticmethod
|
||||
def generate(
|
||||
length: int = 8
|
||||
length: int = 8
|
||||
) -> str:
|
||||
"""
|
||||
生成指定长度的随机密码。
|
||||
|
||||
44
pkg/startup.py
Normal file
44
pkg/startup.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
应用启动模块
|
||||
|
||||
负责应用启动时的初始化工作
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from model.database import Database
|
||||
|
||||
|
||||
async def init_database() -> None:
|
||||
"""
|
||||
初始化数据库
|
||||
"""
|
||||
await Database().init_db()
|
||||
|
||||
|
||||
def mount_static_files(app: FastAPI) -> None:
|
||||
"""
|
||||
挂载静态文件目录
|
||||
|
||||
:param app: FastAPI 应用实例
|
||||
"""
|
||||
try:
|
||||
app.mount("/dist", StaticFiles(directory="dist"), name="dist")
|
||||
except RuntimeError as e:
|
||||
logger.warning(f'Unable to mount static directory: {str(e)}, starting in backend-only mode')
|
||||
|
||||
|
||||
def startup(app: FastAPI) -> None:
|
||||
"""
|
||||
执行应用启动流程
|
||||
|
||||
:param app: FastAPI 应用实例
|
||||
"""
|
||||
# 初始化数据库
|
||||
asyncio.run(init_database())
|
||||
|
||||
# 挂载静态文件
|
||||
mount_static_files(app)
|
||||
@@ -6,7 +6,7 @@ from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi import APIRouter
|
||||
import jwt, JWT
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from pkg.password import Password
|
||||
from pkg import Password
|
||||
from loguru import logger
|
||||
|
||||
from model.token import Token
|
||||
|
||||
88
tool.py
88
tool.py
@@ -1,88 +0,0 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def format_phone(
|
||||
phone: str,
|
||||
groups: list[int] | None = None,
|
||||
separator: str = " ",
|
||||
private: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
格式化中国大陆的11位手机号
|
||||
|
||||
:param phone: 手机号
|
||||
:param groups: 分组长度列表
|
||||
:param separator: 分隔符
|
||||
:param private: 是否隐藏前七位
|
||||
|
||||
:return: 格式化后的手机号
|
||||
"""
|
||||
if groups is None:
|
||||
groups = [3, 4, 4]
|
||||
|
||||
result = []
|
||||
start = 0
|
||||
for i, length in enumerate(groups):
|
||||
segment = phone[start:start + length]
|
||||
# 如果是private模式,将前两组(前7位)替换为星号
|
||||
if private and i < 2:
|
||||
segment = "*" * length
|
||||
result.append(segment)
|
||||
start += length
|
||||
|
||||
return separator.join(result)
|
||||
|
||||
def format_time_diff(
|
||||
target_time: datetime | str
|
||||
) -> str:
|
||||
"""
|
||||
计算目标时间与当前时间的差值,返回易读的中文描述
|
||||
|
||||
Args:
|
||||
target_time: 目标时间,可以是datetime对象或时间字符串
|
||||
|
||||
Returns:
|
||||
str: 格式化的时间差描述,如"一年前"、"3个月前"等
|
||||
"""
|
||||
# 如果输入是字符串,先转换为datetime对象
|
||||
if isinstance(target_time, str):
|
||||
try:
|
||||
target_time = datetime.fromisoformat(target_time)
|
||||
except ValueError:
|
||||
return "时间格式错误"
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
target_time = target_time.astimezone(timezone.utc)
|
||||
diff = now - target_time
|
||||
|
||||
# 如果是未来时间
|
||||
if diff.total_seconds() < 0:
|
||||
diff = -diff
|
||||
suffix = "后"
|
||||
else:
|
||||
suffix = "前"
|
||||
|
||||
seconds = diff.total_seconds()
|
||||
|
||||
# 定义时间间隔
|
||||
intervals = [
|
||||
(31536000, " 年"),
|
||||
(2592000, " 个月"),
|
||||
(86400, " 天"),
|
||||
(3600, " 小时"),
|
||||
(60, " 分钟"),
|
||||
(1, " 秒")
|
||||
]
|
||||
|
||||
# 计算最适合的时间单位
|
||||
for seconds_in_unit, unit in intervals:
|
||||
if seconds >= seconds_in_unit:
|
||||
value = int(seconds / seconds_in_unit)
|
||||
if unit == "个月" and value >= 12: # 超过12个月显示为年
|
||||
continue
|
||||
return f"{value}{unit}{suffix}"
|
||||
|
||||
return f"刚刚"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(format_phone("18888888888", private=True))
|
||||
print(format_phone("18888888888", private=False))
|
||||
Reference in New Issue
Block a user