modify: jwt 验证

This commit is contained in:
2026-02-02 13:36:57 +08:00
parent f58181fbd3
commit 82097e12b5
33 changed files with 1438 additions and 2287 deletions
-1953
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -11,8 +11,6 @@
uv 环境 uv 环境
```shell ```shell
uv sync
uv pip install -e .
uv run serve uv run serve
``` ```
@@ -49,6 +47,7 @@ build
│   ├── plugins.mbank │   ├── plugins.mbank
│   └── app.mbank │   └── app.mbank
├── config.toml ├── config.toml
├── db.sqlite3
└── {{ cookiecutter.project_name }} # windows {{ cookiecutter.project_name }}.exe └── {{ cookiecutter.project_name }} # windows {{ cookiecutter.project_name }}.exe
``` ```
@@ -1,7 +1,7 @@
[default] [default]
app.name = "{{ cookiecutter.project_name }}" app.name = "{{ cookiecutter.project_name }}"
app.version = "{{ cookiecutter.project_name }}" app.version = "{{ cookiecutter.project_name }}"
app.description = "基于 FastAPI 和 Tortoise-ORM 的 chongming 打包脚手架" app.description = "基于 FastAPI 和 Tortoise-ORM 的 {{ cookiecutter.project_name }} 打包脚手架"
app.debug = false app.debug = false
env = "production" env = "production"
@@ -29,6 +29,13 @@ readonly = false
secret_key = "your-secret-key-change-in-production" secret_key = "your-secret-key-change-in-production"
algorithm = "HS256" algorithm = "HS256"
access_token_expire_minutes = 30 access_token_expire_minutes = 30
refresh_token_expire_days = 7
[jwt_cache]
enabled = true
db_path = "./cache"
cleanup_interval_hours = 24
max_sessions_per_user = 5
[development] [development]
env = "development" env = "development"
@@ -1,14 +1,15 @@
# main.py - 修复 reload 问题 # main.py - 修复 reload 问题
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
FastAPI + Tortoise-ORM + Chongming 示例 FastAPI + Tortoise-ORM + {{ cookiecutter.project_name }} 示例
主入口文件,适配 chongming 打包系统 主入口文件,适配 {{ cookiecutter.project_name }} 打包系统
""" """
import sys import sys
import os import os
from utils.launch import launch from utils.launch import launch
from multiprocessing import freeze_support from multiprocessing import freeze_support
def app_run(app_config: dict, default_config: dict): def app_run(app_config: dict, default_config: dict):
""" """
应用运行函数 - 适配模块银行环境 应用运行函数 - 适配模块银行环境
@@ -23,14 +24,13 @@ def app_run(app_config: dict, default_config: dict):
setup_logging(app_config["logging"]) setup_logging(app_config["logging"])
logger = get_logger("app") logger = get_logger("app")
# 获取服务器配置 # 获取服务器配置
server_config = app_config.get("server", {}) server_config = app_config.get("server", {})
host = server_config.get("host", "0.0.0.0") host = server_config.get("host", "0.0.0.0")
port = server_config.get("port", 8000) port = server_config.get("port", 8000)
# 打包环境强制禁用重载 # 打包环境强制禁用重载
is_frozen = getattr(sys, 'frozen', False) is_frozen = getattr(sys, "frozen", False)
reload = False if is_frozen else server_config.get("reload", False) reload = False if is_frozen else server_config.get("reload", False)
workers = server_config.get("workers", 4) workers = server_config.get("workers", 4)
@@ -59,10 +59,11 @@ def app_run(app_config: dict, default_config: dict):
access_log=True, access_log=True,
) )
def main(): def main():
freeze_support() freeze_support()
# 如果是打包后的可执行文件,调整路径 # 如果是打包后的可执行文件,调整路径
if getattr(sys, 'frozen', False): if getattr(sys, "frozen", False):
exe_dir = os.path.dirname(sys.executable) exe_dir = os.path.dirname(sys.executable)
if exe_dir not in sys.path: if exe_dir not in sys.path:
sys.path.insert(0, exe_dir) sys.path.insert(0, exe_dir)
@@ -70,5 +71,6 @@ def main():
# 启动应用 # 启动应用
launch(app_run, "development") launch(app_run, "development")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -5,9 +5,11 @@ import os
from module_bank import PythonToSQLite from module_bank import PythonToSQLite
from typing import Callable from typing import Callable
# 检查是否在 PyInstaller 子进程中 # 检查是否在 PyInstaller 子进程中
def is_pyinstaller_child(): def is_pyinstaller_child():
return len(sys.argv) > 1 and ('_child.py' in sys.argv[0] or sys.argv[1].isdigit()) return len(sys.argv) > 1 and ("_child.py" in sys.argv[0] or sys.argv[1].isdigit())
def parse_args(): def parse_args():
"""安全的参数解析函数""" """安全的参数解析函数"""
@@ -15,13 +17,16 @@ def parse_args():
# PyInstaller 子进程,使用默认参数 # PyInstaller 子进程,使用默认参数
return argparse.Namespace(config="config.toml") return argparse.Namespace(config="config.toml")
parser = argparse.ArgumentParser(description="chongming 应用启动脚本") parser = argparse.ArgumentParser(
description="{{ cookiecutter.project_name }} 应用启动脚本"
)
parser.add_argument("--config", default="config.toml", help="配置文件路径") parser.add_argument("--config", default="config.toml", help="配置文件路径")
return parser.parse_args() return parser.parse_args()
args = parse_args() args = parse_args()
def init_module_bank(config): def init_module_bank(config):
MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"] MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"]
for module_path in MODULE_BANK_PATH_LIST: for module_path in MODULE_BANK_PATH_LIST:
@@ -35,6 +40,7 @@ def init_module_bank(config):
print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}") print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}")
packer.install_importer() packer.install_importer()
def launch(run_fun: Callable[[dict, dict], None], run_mode: str): def launch(run_fun: Callable[[dict, dict], None], run_mode: str):
config = ConfigLoader(args.config) config = ConfigLoader(args.config)
+14 -11
View File
@@ -1,3 +1,7 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project] [project]
name = "{{ cookiecutter.project_name }}" name = "{{ cookiecutter.project_name }}"
version = "0.1.0" version = "0.1.0"
@@ -5,33 +9,32 @@ description = "基于 modules-bank 的 fastapi 打包脚手架"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [ dependencies = [
"aerich",
"fastapi[uvicorn]", "fastapi[uvicorn]",
"module-bank", "module-bank",
"passlib", "passlib[bcrypt]",
"pydantic-settings", "pydantic-settings",
"pydantic[email]", "pydantic[email]",
"sqlite-vfs", "sqlite-vfs",
"toml", "toml",
"tortoise-orm", "tortoise-orm",
"uvicorn[standard]", "uvicorn[standard]",
"sqlitedict",
"pyjwt",
"bcrypt==4.0.1",
] ]
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true
[[tool.uv.index]] [[tool.uv.index]]
url = "https://mirrors.ustc.edu.cn/pypi/web/simple/" url = "https://mirrors.ustc.edu.cn/pypi/web/simple/"
[tool.uv.workspace]
members = [
".",
]
[tool.uv.sources]
{{ cookiecutter.project_name }} = { workspace = true }
[dependency-groups] [dependency-groups]
dev = [ dev = [
"{{ cookiecutter.project_name }}", "aerich",
"pyinstaller>=6.18.0", "pyinstaller>=6.18.0",
"requests>=2.32.4",
] ]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
@@ -8,3 +8,9 @@ sqlite-vfs
toml toml
tortoise-orm tortoise-orm
uvicorn[standard] uvicorn[standard]
sqlitedict
pyjwt
bcrypt==4.0.1
pyinstaller
aerich
requests
@@ -1,14 +1,15 @@
# main.py - 修复 reload 问题 # main.py - 修复 reload 问题
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
FastAPI + Tortoise-ORM + Chongming 示例 FastAPI + Tortoise-ORM + {{ cookiecutter.project_name }} 示例
主入口文件,适配 chongming 打包系统 主入口文件,适配 {{ cookiecutter.project_name }} 打包系统
""" """
import sys import sys
import os import os
from .utils.launch import launch from .utils.launch import launch
from multiprocessing import freeze_support from multiprocessing import freeze_support
def app_run(app_config: dict, default_config: dict): def app_run(app_config: dict, default_config: dict):
""" """
应用运行函数 - 适配模块银行环境 应用运行函数 - 适配模块银行环境
@@ -23,14 +24,13 @@ def app_run(app_config: dict, default_config: dict):
setup_logging(app_config["logging"]) setup_logging(app_config["logging"])
logger = get_logger("app") logger = get_logger("app")
# 获取服务器配置 # 获取服务器配置
server_config = app_config.get("server", {}) server_config = app_config.get("server", {})
host = server_config.get("host", "0.0.0.0") host = server_config.get("host", "0.0.0.0")
port = server_config.get("port", 8000) port = server_config.get("port", 8000)
# 打包环境强制禁用重载 # 打包环境强制禁用重载
is_frozen = getattr(sys, 'frozen', False) is_frozen = getattr(sys, "frozen", False)
reload = False if is_frozen else server_config.get("reload", False) reload = False if is_frozen else server_config.get("reload", False)
workers = server_config.get("workers", 4) workers = server_config.get("workers", 4)
@@ -59,10 +59,11 @@ def app_run(app_config: dict, default_config: dict):
access_log=True, access_log=True,
) )
def main(): def main():
freeze_support() freeze_support()
# 如果是打包后的可执行文件,调整路径 # 如果是打包后的可执行文件,调整路径
if getattr(sys, 'frozen', False): if getattr(sys, "frozen", False):
exe_dir = os.path.dirname(sys.executable) exe_dir = os.path.dirname(sys.executable)
if exe_dir not in sys.path: if exe_dir not in sys.path:
sys.path.insert(0, exe_dir) sys.path.insert(0, exe_dir)
@@ -70,5 +71,15 @@ def main():
# 启动应用 # 启动应用
launch(app_run, "development") launch(app_run, "development")
if __name__ == "__main__":
def serve():
import asyncio
from .scripts import create_admin
print("🔍 正在执行 create_admin 脚本...")
success = asyncio.run(create_admin.main()) # type: ignore
if success:
print("✅ create_admin 脚本执行成功")
else:
print("⚠️ create_admin 脚本执行失败或跳过(例如,管理员已存在)")
main() main()
@@ -1,8 +1,9 @@
""" """
API 路由 API 路由
""" """
from fastapi import APIRouter from fastapi import APIRouter
from .routers import health, items, users from .routers import health, items, users, auth
# 创建主路由 # 创建主路由
api_router = APIRouter() api_router = APIRouter()
@@ -11,6 +12,7 @@ api_router = APIRouter()
api_router.include_router(health.router, prefix="/health", tags=["health"]) api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(items.router, prefix="/items", tags=["items"]) api_router.include_router(items.router, prefix="/items", tags=["items"])
api_router.include_router(users.router, prefix="/users", tags=["users"]) api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
# 导出路由 # 导出路由
__all__ = ["api_router"] __all__ = ["api_router"]
@@ -1,11 +1,20 @@
""" """
API 依赖项 API 依赖项
""" """
from typing import AsyncGenerator from typing import AsyncGenerator
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from tortoise.exceptions import DBConnectionError, DoesNotExist from tortoise.exceptions import DBConnectionError, DoesNotExist
from tortoise import Tortoise from tortoise import Tortoise
from ..services.auth_service import get_auth_service
from ..core.jwt_cache import TokenData
# 安全组件
security = HTTPBearer()
async def get_db() -> AsyncGenerator[None, None]: async def get_db() -> AsyncGenerator[None, None]:
"""获取数据库连接依赖""" """获取数据库连接依赖"""
try: try:
@@ -16,16 +25,61 @@ async def get_db() -> AsyncGenerator[None, None]:
yield yield
except DBConnectionError as e: except DBConnectionError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="数据库连接失败"
detail="数据库连接失败"
) )
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"数据库错误: {str(e)}" detail=f"数据库错误: {str(e)}",
) )
def get_current_user():
"""获取当前用户(示例)""" async def get_current_user(
# 这里可以实现 JWT 验证等逻辑 credentials: HTTPAuthorizationCredentials = Depends(security),
pass ) -> TokenData:
"""
获取当前用户依赖项
验证访问令牌并返回用户信息
"""
auth_service = get_auth_service()
token = credentials.credentials
token_data = await auth_service.validate_access_token(token)
if token_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的访问令牌",
headers={"WWW-Authenticate": "Bearer"},
)
return token_data
async def get_current_active_user(
current_user: TokenData = Depends(get_current_user),
) -> TokenData:
"""
获取当前活跃用户
检查用户是否被禁用
"""
# 这里可以添加额外的检查,比如用户是否被禁用
# 由于 TokenData 目前不包含用户状态,需要查询数据库
# 为简化起见,这里直接返回
return current_user
async def get_current_superuser(
current_user: TokenData = Depends(get_current_user),
) -> TokenData:
"""
获取当前超级用户
检查用户是否具有超级用户权限
"""
# 这里需要查询数据库检查用户权限
# 为简化起见,这里直接返回
# 实际应用中应该检查用户的权限
return current_user
@@ -0,0 +1,135 @@
"""
认证路由 - 处理登录、注册、Token 刷新等
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from ...schemas.user import UserCreate, UserLogin, UserResponse
from ...services.auth_service import get_auth_service, TokenResponse
from ...services.user_service import UserService
from ...core.jwt_cache import TokenData
from ..dependencies import get_current_user
router = APIRouter(tags=["authentication"])
# 安全组件
security = HTTPBearer()
@router.post("/register", response_model=UserResponse, summary="用户注册")
async def register(user_data: UserCreate, request: Request):
"""用户注册"""
# 检查用户名是否已存在
existing_user = await UserService.get_user_by_username(user_data.username)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在"
)
# 检查邮箱是否已存在
existing_email = await UserService.get_user_by_email(user_data.email)
if existing_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="邮箱已存在"
)
# 创建用户
user = await UserService.create_user(user_data)
return user
@router.post("/login", response_model=TokenResponse, summary="用户登录")
async def login(user_data: UserLogin, request: Request):
"""用户登录并获取令牌"""
auth_service = get_auth_service()
# 验证用户凭据
user = await auth_service.authenticate_user(user_data.username, user_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 检查用户是否激活
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="用户已被禁用"
)
# 创建令牌
tokens = await auth_service.create_tokens(
user, request, device_id=user_data.device_id
)
return tokens
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
async def refresh_token(
request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)
):
"""使用刷新令牌获取新的访问令牌"""
auth_service = get_auth_service()
refresh_token = credentials.credentials
new_access_token = await auth_service.refresh_access_token(refresh_token, request)
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的刷新令牌",
headers={"WWW-Authenticate": "Bearer"},
)
return {
"access_token": new_access_token,
"token_type": "bearer",
"expires_in": get_auth_service().settings.access_token_expire_minutes * 60,
}
@router.post("/logout", summary="用户登出")
async def logout(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""用户登出,使令牌失效"""
auth_service = get_auth_service()
token = credentials.credentials
await auth_service.logout(token)
return {"message": "登出成功"}
@router.post("/logout-all", summary="登出所有设备")
async def logout_all(current_user: TokenData = Depends(get_current_user)):
"""使用户在所有设备的令牌失效"""
auth_service = get_auth_service()
await auth_service.logout_all(current_user.user_id)
return {"message": "所有设备已登出"}
@router.get("/sessions", summary="获取用户会话")
async def get_sessions(current_user: TokenData = Depends(get_current_user)):
"""获取用户的所有活跃会话"""
auth_service = get_auth_service()
sessions = await auth_service.get_user_sessions(current_user.user_id)
return {
"user_id": current_user.user_id,
"sessions": sessions,
"count": len(sessions),
}
@router.get("/me", response_model=UserResponse, summary="获取当前用户信息")
async def get_current_user_info(current_user: TokenData = Depends(get_current_user)):
"""获取当前登录用户的信息"""
user = await UserService.get_user_by_id(int(current_user.user_id))
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return user
@@ -1,14 +1,16 @@
""" """
健康检查路由 健康检查路由
""" """
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ...core.config import get_settings from ...core.config import get_settings
from ..dependencies import get_db from ..dependencies import get_db
router = APIRouter() router = APIRouter()
@router.get("/", summary="健康检查") @router.get("/", summary="健康检查")
async def health_check(db = Depends(get_db)): async def health_check(db=Depends(get_db)):
"""健康检查端点""" """健康检查端点"""
settings = get_settings() settings = get_settings()
@@ -17,17 +19,15 @@ async def health_check(db = Depends(get_db)):
"service": settings.app_name, "service": settings.app_name,
"version": settings.app_version, "version": settings.app_version,
"environment": settings.env, "environment": settings.env,
"timestamp": "now" # 实际使用中可以用 datetime.now() "timestamp": "now", # 实际使用中可以用 datetime.now()
} }
@router.get("/ready", summary="就绪检查") @router.get("/ready", summary="就绪检查")
async def readiness_check(): async def readiness_check():
"""就绪检查端点""" """就绪检查端点"""
return { return {"status": "ready", "database": "connected", "api": "available"}
"status": "ready",
"database": "connected",
"api": "available"
}
@router.get("/info", summary="服务信息") @router.get("/info", summary="服务信息")
async def service_info(): async def service_info():
@@ -40,5 +40,4 @@ async def service_info():
"description": settings.description, "description": settings.description,
"environment": settings.env, "environment": settings.env,
"debug": settings.debug, "debug": settings.debug,
"database": settings.database_url.split("://")[0] if "://" in settings.database_url else "unknown"
} }
@@ -1,6 +1,7 @@
""" """
Item 路由 Item 路由
""" """
from typing import List from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from tortoise.exceptions import DoesNotExist from tortoise.exceptions import DoesNotExist
@@ -10,16 +11,14 @@ from ..dependencies import get_db
router = APIRouter() router = APIRouter()
@router.get("/", response_model=List[ItemResponse], summary="获取物品列表") @router.get("/", response_model=List[ItemResponse], summary="获取物品列表")
async def read_items( async def read_items(skip: int = 0, limit: int = 100, db=Depends(get_db)):
skip: int = 0,
limit: int = 100,
db=Depends(get_db)
):
"""获取物品列表""" """获取物品列表"""
items = await Item.all().offset(skip).limit(limit) items = await Item.all().offset(skip).limit(limit)
return items return items
@router.get("/{item_id}", response_model=ItemResponse, summary="获取物品详情") @router.get("/{item_id}", response_model=ItemResponse, summary="获取物品详情")
async def read_item(item_id: int, db=Depends(get_db)): async def read_item(item_id: int, db=Depends(get_db)):
"""根据ID获取物品""" """根据ID获取物品"""
@@ -28,11 +27,16 @@ async def read_item(item_id: int, db=Depends(get_db)):
return item return item
except DoesNotExist: except DoesNotExist:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND, detail=f"物品 ID {item_id} 不存在"
detail=f"物品 ID {item_id} 不存在"
) )
@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED, summary="创建物品")
@router.post(
"/",
response_model=ItemResponse,
status_code=status.HTTP_201_CREATED,
summary="创建物品",
)
async def create_item(item: ItemCreate, db=Depends(get_db)): async def create_item(item: ItemCreate, db=Depends(get_db)):
"""创建新物品""" """创建新物品"""
# 检查名称是否已存在 # 检查名称是否已存在
@@ -40,7 +44,7 @@ async def create_item(item: ItemCreate, db=Depends(get_db)):
if existing_item: if existing_item:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"物品名称 '{item.name}' 已存在" detail=f"物品名称 '{item.name}' 已存在",
) )
# 创建物品 # 创建物品
@@ -48,6 +52,7 @@ async def create_item(item: ItemCreate, db=Depends(get_db)):
new_item = await Item.create(**item_dict) new_item = await Item.create(**item_dict)
return new_item return new_item
@router.put("/{item_id}", response_model=ItemResponse, summary="更新物品") @router.put("/{item_id}", response_model=ItemResponse, summary="更新物品")
async def update_item(item_id: int, item_update: ItemUpdate, db=Depends(get_db)): async def update_item(item_id: int, item_update: ItemUpdate, db=Depends(get_db)):
"""更新物品信息""" """更新物品信息"""
@@ -59,13 +64,13 @@ async def update_item(item_id: int, item_update: ItemUpdate, db=Depends(get_db))
# 如果更新名称,检查是否与其他物品重名 # 如果更新名称,检查是否与其他物品重名
if "name" in update_data: if "name" in update_data:
existing_item = await Item.filter( existing_item = (
name=update_data["name"] await Item.filter(name=update_data["name"]).exclude(id=item_id).first()
).exclude(id=item_id).first() )
if existing_item: if existing_item:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"物品名称 '{update_data['name']}' 已存在" detail=f"物品名称 '{update_data['name']}' 已存在",
) )
# 应用更新 # 应用更新
@@ -75,10 +80,10 @@ async def update_item(item_id: int, item_update: ItemUpdate, db=Depends(get_db))
return item return item
except DoesNotExist: except DoesNotExist:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND, detail=f"物品 ID {item_id} 不存在"
detail=f"物品 ID {item_id} 不存在"
) )
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除物品") @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除物品")
async def delete_item(item_id: int, db=Depends(get_db)): async def delete_item(item_id: int, db=Depends(get_db)):
"""删除物品""" """删除物品"""
@@ -88,6 +93,5 @@ async def delete_item(item_id: int, db=Depends(get_db)):
return None return None
except DoesNotExist: except DoesNotExist:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND, detail=f"物品 ID {item_id} 不存在"
detail=f"物品 ID {item_id} 不存在"
) )
@@ -1,6 +1,7 @@
""" """
User 路由 User 路由
""" """
from typing import List from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from tortoise.exceptions import DoesNotExist, IntegrityError from tortoise.exceptions import DoesNotExist, IntegrityError
@@ -8,29 +9,44 @@ from tortoise.exceptions import DoesNotExist, IntegrityError
from ...models.user import User from ...models.user import User
from ...schemas.user import UserCreate, UserUpdate, UserResponse from ...schemas.user import UserCreate, UserUpdate, UserResponse
from ...services.user_service import UserService from ...services.user_service import UserService
from ..dependencies import get_db from ..dependencies import (
get_db,
get_current_user,
get_current_active_user,
get_current_superuser,
)
from ...core.jwt_cache import TokenData
router = APIRouter() router = APIRouter()
@router.get("/", response_model=List[UserResponse], summary="获取用户列表")
async def read_users( @router.get(
skip: int = 0, "/",
limit: int = 100, response_model=List[UserResponse],
db=Depends(get_db) summary="获取用户列表",
): dependencies=[Depends(get_current_superuser)],
"""获取用户列表""" ) # 需要超级用户权限
async def read_users(skip: int = 0, limit: int = 100, db=Depends(get_db)):
"""获取用户列表(需要管理员权限)"""
users = await UserService.get_all_users(skip, limit) users = await UserService.get_all_users(skip, limit)
return users return users
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED, summary="创建用户")
@router.post(
"/",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
summary="创建用户",
dependencies=[Depends(get_current_superuser)],
) # 需要超级用户权限
async def create_user(user_data: UserCreate, db=Depends(get_db)): async def create_user(user_data: UserCreate, db=Depends(get_db)):
"""创建新用户""" """创建新用户(需要管理员权限)"""
# 检查用户名是否已存在 # 检查用户名是否已存在
existing_user = await UserService.get_user_by_username(user_data.username) existing_user = await UserService.get_user_by_username(user_data.username)
if existing_user: if existing_user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"用户名 '{user_data.username}' 已存在" detail=f"用户名 '{user_data.username}' 已存在",
) )
# 检查邮箱是否已存在 # 检查邮箱是否已存在
@@ -38,7 +54,7 @@ async def create_user(user_data: UserCreate, db=Depends(get_db)):
if existing_email: if existing_email:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"邮箱 '{user_data.email}' 已存在" detail=f"邮箱 '{user_data.email}' 已存在",
) )
# 创建用户 # 创建用户
@@ -47,39 +63,94 @@ async def create_user(user_data: UserCreate, db=Depends(get_db)):
return user return user
except IntegrityError: except IntegrityError:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST, detail="创建用户失败"
detail="创建用户失败"
) )
@router.get("/{user_id}", response_model=UserResponse, summary="获取用户详情")
@router.get("/me", response_model=UserResponse, summary="获取当前用户信息")
async def read_current_user(current_user: TokenData = Depends(get_current_active_user)):
"""获取当前登录用户的信息"""
user = await UserService.get_user_by_id(int(current_user.user_id))
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"用户不存在")
return user
@router.get(
"/{user_id}",
response_model=UserResponse,
summary="获取用户详情",
dependencies=[Depends(get_current_superuser)],
) # 需要超级用户权限
async def read_user(user_id: int, db=Depends(get_db)): async def read_user(user_id: int, db=Depends(get_db)):
"""根据ID获取用户""" """根据ID获取用户(需要管理员权限)"""
user = await UserService.get_user_by_id(user_id) user = await UserService.get_user_by_id(user_id)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND, detail=f"用户 ID {user_id} 不存在"
detail=f"用户 ID {user_id} 不存在"
) )
return user return user
@router.put("/{user_id}", response_model=UserResponse, summary="更新用户")
@router.put(
"/{user_id}",
response_model=UserResponse,
summary="更新用户",
dependencies=[Depends(get_current_superuser)],
) # 需要超级用户权限
async def update_user(user_id: int, user_update: UserUpdate, db=Depends(get_db)): async def update_user(user_id: int, user_update: UserUpdate, db=Depends(get_db)):
"""更新用户信息""" """更新用户信息(需要管理员权限)"""
user = await UserService.update_user(user_id, user_update) user = await UserService.update_user(user_id, user_update)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND, detail=f"用户 ID {user_id} 不存在"
detail=f"用户 ID {user_id} 不存在"
) )
return user return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除用户")
@router.delete(
"/{user_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="删除用户",
dependencies=[Depends(get_current_superuser)],
) # 需要超级用户权限
async def delete_user(user_id: int, db=Depends(get_db)): async def delete_user(user_id: int, db=Depends(get_db)):
"""删除用户""" """删除用户(需要管理员权限)"""
deleted = await UserService.delete_user(user_id) deleted = await UserService.delete_user(user_id)
if not deleted: if not deleted:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND, detail=f"用户 ID {user_id} 不存在"
detail=f"用户 ID {user_id} 不存在"
) )
return None return None
@router.post(
"/{user_id}/deactivate",
response_model=UserResponse,
summary="停用用户",
dependencies=[Depends(get_current_superuser)],
) # 需要超级用户权限
async def deactivate_user(user_id: int, db=Depends(get_db)):
"""停用用户(需要管理员权限)"""
user = await UserService.deactivate_user(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"用户 ID {user_id} 不存在"
)
return user
@router.post(
"/{user_id}/activate",
response_model=UserResponse,
summary="激活用户",
dependencies=[Depends(get_current_superuser)],
) # 需要超级用户权限
async def activate_user(user_id: int, db=Depends(get_db)):
"""激活用户(需要管理员权限)"""
user = await UserService.activate_user(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"用户 ID {user_id} 不存在"
)
return user
@@ -97,6 +97,13 @@ class Settings(BaseSettings):
secret_key: str = "your-secret-key-change-in-production" secret_key: str = "your-secret-key-change-in-production"
algorithm: str = "HS256" algorithm: str = "HS256"
access_token_expire_minutes: int = 30 access_token_expire_minutes: int = 30
refresh_token_expire_days: int = 7
# JWT 缓存配置
jwt_cache_enabled: bool = True
jwt_cache_db_path: str = "jwt_cache.sqlite"
jwt_cache_cleanup_interval_hours: int = 24
jwt_cache_max_sessions_per_user: int = 5
# 模块系统 # 模块系统
module_system_type: str = "local" module_system_type: str = "local"
@@ -176,6 +183,18 @@ class Settings(BaseSettings):
if database_config: if database_config:
config_dict["sqlite_config"] = SQLiteConfig(**database_config) config_dict["sqlite_config"] = SQLiteConfig(**database_config)
# 处理JWT缓存
if "jwt_cache" in config_data:
jwt_cache_config = config_data["jwt_cache"]
for key, value in jwt_cache_config.items():
config_dict[f"jwt_cache_{key}"] = value
# 处理安全配置
if "security" in config_dict:
security_config = config_dict["security"]
for key, value in security_config.items():
config_dict[key] = value
return cls(**config_dict) return cls(**config_dict)
except Exception as e: except Exception as e:
@@ -31,7 +31,10 @@ async def init_db(run_mode: str):
"connections": db_config, "connections": db_config,
"apps": { "apps": {
"models": { "models": {
"models": ["{{ cookiecutter.project_name }}.app.models", "aerich.models"], "models": [
"{{ cookiecutter.project_name }}.app.models",
"aerich.models",
],
"default_connection": "default", "default_connection": "default",
} }
}, },
@@ -63,7 +66,11 @@ async def close_db():
def register_db(app, run_mode: str): def register_db(app, run_mode: str):
"""将数据库注册到 FastAPI 应用""" """将数据库注册到 FastAPI 应用"""
modules_path = "app.models" if run_mode == "production" else "{{ cookiecutter.project_name }}.app.models" modules_path = (
"app.models"
if run_mode == "production"
else "{{ cookiecutter.project_name }}.app.models"
)
file_path = getattr(settings, f"{settings.database_type}_config").file_path file_path = getattr(settings, f"{settings.database_type}_config").file_path
register_tortoise( register_tortoise(
app, app,
@@ -76,31 +83,26 @@ def register_db(app, run_mode: str):
return app return app
# 在 app/core/database.py 中添加 # 为 Aerich 提供配置
from aerich import Command def get_tortoise_config():
"""获取 Tortoise ORM 配置 - 供 Aerich 使用"""
db_config = getattr(settings, f"{settings.database_type}_config").tortoise_config
return {
async def run_migrations(): "connections": db_config,
"""运行数据库迁移"""
try:
command = Command(
tortoise_config={
"connections": getattr(
settings, f"{settings.database_type}_config"
).tortoise_config,
"apps": { "apps": {
"models": { "models": {
"models": ["app.models", "aerich.models"], "models": [
"{{ cookiecutter.project_name }}.app.models",
"aerich.models",
],
"default_connection": "default", "default_connection": "default",
} }
}, },
}, "use_tz": False,
app="models", "timezone": "UTC",
) }
# 检查并应用迁移
await command.init() # 保存配置引用
await command.upgrade() TORTOISE_CONFIG = get_tortoise_config()
print("✅ 数据库迁移完成")
except Exception as e:
print(f"⚠️ 数据库迁移失败: {e}")
@@ -0,0 +1,464 @@
import jwt
import json
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from sqlitedict import SqliteDict
from dataclasses import dataclass, asdict
from pydantic import BaseModel
import hashlib
import threading
from pathlib import Path
from .config import get_settings
@dataclass
class TokenCacheData:
"""Token 缓存数据结构"""
token_hash: str
user_id: str
device_id: Optional[str] = None
user_agent: Optional[str] = None
ip_address: Optional[str] = None
created_at: Optional[str] = None
expires_at: Optional[str] = None
last_used: Optional[str] = None
is_active: bool = True
payload_data: Optional[Dict[str, Any]] = None
token_type: str = "access" # access 或 refresh
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
if self.last_used is None:
self.last_used = datetime.utcnow().isoformat()
class JWTCache:
"""JWT Token 缓存管理器"""
def __init__(self, db_path: Optional[str] = None):
self.settings = get_settings()
if db_path is None:
db_path = (
self.settings.jwt_cache_db_path
if hasattr(self.settings, "jwt_cache_db_path")
else "jwt_cache.sqlite"
)
db_file_path = Path(db_path)
db_file_path.mkdir(parents=True, exist_ok=True)
self.lock = threading.RLock()
# 主 Token 缓存
self.token_db = SqliteDict(
rf"{db_path}/tokens.cache",
tablename="tokens",
autocommit=True,
encode=self._json_encode,
decode=self._json_decode,
journal_mode="WAL",
)
# 黑名单缓存
self.blacklist_db = SqliteDict(
rf"{db_path}/blacklist.cache",
tablename="blacklist",
autocommit=True,
encode=json.dumps,
decode=json.loads,
journal_mode="WAL",
)
# 用户会话索引
self.user_sessions_db = SqliteDict(
rf"{db_path}/user_sessions.cache",
tablename="user_sessions",
autocommit=True,
encode=json.dumps,
decode=json.loads,
journal_mode="WAL",
)
self.secret_key = self.settings.secret_key
self.algorithm = self.settings.algorithm
def _json_encode(self, obj):
"""自定义 JSON 编码器,处理 datetime 对象"""
def convert_datetime(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError(
f"Object of type {o.__class__.__name__} is not JSON serializable"
)
return json.dumps(obj, default=convert_datetime)
def _json_decode(self, s):
"""自定义 JSON 解码器"""
return json.loads(s)
def _hash_token(self, token: str) -> str:
"""Token 哈希化(避免存储明文 Token)"""
return hashlib.sha256(token.encode()).hexdigest()
def create_token(
self,
user_id: str,
user_data: Dict[str, Any],
token_type: str = "access",
device_id: Optional[str] = None,
user_agent: Optional[str] = None,
ip_address: Optional[str] = None,
) -> str:
"""
创建并缓存 JWT Token
Args:
user_id: 用户ID
user_data: 用户数据
token_type: Token 类型(access 或 refresh)
device_id: 设备ID
user_agent: 用户代理
ip_address: IP地址
Returns:
JWT Token 字符串
"""
with self.lock:
# 计算过期时间
if token_type == "access":
expire_minutes = self.settings.access_token_expire_minutes
expires_delta = timedelta(minutes=expire_minutes)
else:
expire_days = (
self.settings.refresh_token_expire_days
if hasattr(self.settings, "refresh_token_expire_days")
else 7
)
expires_delta = timedelta(days=expire_days)
expire = datetime.utcnow() + expires_delta
# 构建 payload
payload = {
"sub": user_id,
"exp": expire,
"iat": datetime.utcnow(),
"type": token_type,
"data": user_data,
"device_id": device_id,
"jti": self._generate_jti(), # JWT ID,唯一标识
}
# 生成 JWT Token
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
if isinstance(token, bytes):
token = token.decode("utf-8")
# 创建缓存数据
token_hash = self._hash_token(token)
cache_data = TokenCacheData(
token_hash=token_hash,
user_id=user_id,
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
expires_at=expire.isoformat(), # 转换为字符串
payload_data=payload,
token_type=token_type,
)
# 存储到缓存
self.token_db[token_hash] = asdict(cache_data)
# 更新用户会话索引
self._update_user_session_index(user_id, token_hash, token_type)
# 检查用户会话数限制
self._enforce_session_limit(user_id)
return token
def _generate_jti(self) -> str:
"""生成唯一的 JWT ID"""
import uuid
return str(uuid.uuid4())
def _update_user_session_index(
self, user_id: str, token_hash: str, token_type: str
):
"""更新用户会话索引"""
with self.lock:
key = f"{user_id}:{token_type}"
user_tokens = self.user_sessions_db.get(key, [])
# 添加新 token
user_tokens.append(token_hash)
# 只保留最近的一定数量的 token
max_tokens = 100 # 每个用户每种类型最多保留100个 token
if len(user_tokens) > max_tokens:
user_tokens = user_tokens[-max_tokens:]
self.user_sessions_db[key] = user_tokens
def _enforce_session_limit(self, user_id: str):
"""强制用户会话限制"""
max_sessions = getattr(self.settings, "max_sessions_per_user", 5)
access_key = f"{user_id}:access"
refresh_key = f"{user_id}:refresh"
# 检查 access token 数量
access_tokens = self.user_sessions_db.get(access_key, [])
if len(access_tokens) > max_sessions:
# 删除最早的 token
tokens_to_remove = access_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if token_hash in self.token_db:
del self.token_db[token_hash]
# 更新索引
self.user_sessions_db[access_key] = access_tokens[-max_sessions:]
# 检查 refresh token 数量
refresh_tokens = self.user_sessions_db.get(refresh_key, [])
if len(refresh_tokens) > max_sessions:
tokens_to_remove = refresh_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if token_hash in self.token_db:
del self.token_db[token_hash]
self.user_sessions_db[refresh_key] = refresh_tokens[-max_sessions:]
def validate_token(
self, token: str, token_type: str = "access"
) -> Optional[Dict[str, Any]]:
"""
验证 JWT Token
Args:
token: JWT Token
token_type: 期望的 Token 类型
Returns:
验证成功返回用户数据,失败返回 None
"""
try:
# 解码验证 Token
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# 检查 Token 类型
if payload.get("type") != token_type:
return None
user_id = payload.get("sub")
jti = payload.get("jti")
if not user_id or not jti:
return None
# 检查黑名单
token_hash = self._hash_token(token)
if self._is_blacklisted(token_hash):
return None
# 检查缓存
with self.lock:
cache_data = self.token_db.get(token_hash)
if not cache_data:
return None
# 检查 Token 是否激活
if not cache_data.get("is_active", True):
return None
# 检查是否过期
expires_at = datetime.fromisoformat(cache_data["expires_at"])
if datetime.utcnow() > expires_at:
# 标记为不活跃
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
return None
# 更新最后使用时间
cache_data["last_used"] = datetime.utcnow().isoformat()
self.token_db[token_hash] = cache_data
return {
"user_id": user_id,
"device_id": cache_data.get("device_id"),
"payload": payload,
"cache_data": cache_data,
}
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
except Exception:
return None
def add_to_blacklist(self, token: str, reason: str = "logout"):
"""添加 Token 到黑名单"""
token_hash = self._hash_token(token)
expire_time = datetime.utcnow() + timedelta(days=7) # 黑名单保留7天
self.blacklist_db[token_hash] = {
"reason": reason,
"blacklisted_at": datetime.utcnow().isoformat(),
"expires_at": expire_time.isoformat(),
}
def _is_blacklisted(self, token_hash: str) -> bool:
"""检查 Token 是否在黑名单中"""
with self.lock:
if token_hash not in self.blacklist_db:
return False
blacklist_data = self.blacklist_db[token_hash]
expires_at = datetime.fromisoformat(blacklist_data["expires_at"])
# 如果黑名单条目已过期,删除它
if datetime.utcnow() > expires_at:
del self.blacklist_db[token_hash]
return False
return True
def invalidate_token(self, token: str):
"""使单个 Token 失效"""
with self.lock:
token_hash = self._hash_token(token)
# 添加到黑名单
self.add_to_blacklist(token)
# 标记为不活跃
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
def invalidate_user_tokens(self, user_id: str, token_type: Optional[str] = None):
"""使用户的所有 Token 失效"""
with self.lock:
if token_type:
keys = [f"{user_id}:{token_type}"]
else:
keys = [f"{user_id}:access", f"{user_id}:refresh"]
for key in keys:
token_hashes = self.user_sessions_db.get(key, [])
for token_hash in token_hashes:
# 添加到黑名单
# 注意:这里我们只有 token_hash,没有原始 token
# 所以只能标记为不活跃
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
# 清空用户会话索引
self.user_sessions_db[key] = []
def get_user_sessions(self, user_id: str) -> List[Dict[str, Any]]:
"""获取用户的所有活跃会话"""
with self.lock:
sessions = []
for token_type in ["access", "refresh"]:
key = f"{user_id}:{token_type}"
token_hashes = self.user_sessions_db.get(key, [])
for token_hash in token_hashes:
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
# 检查是否活跃且未过期
if cache_data.get("is_active", True):
expires_at = datetime.fromisoformat(
cache_data["expires_at"]
)
if datetime.utcnow() <= expires_at:
sessions.append(
{
"token_type": token_type,
"device_id": cache_data.get("device_id"),
"user_agent": cache_data.get("user_agent"),
"ip_address": cache_data.get("ip_address"),
"created_at": cache_data["created_at"],
"last_used": cache_data["last_used"],
"expires_at": cache_data["expires_at"],
}
)
return sessions
def cleanup_expired(self):
"""清理过期数据"""
with self.lock:
cleaned_count = 0
current_time = datetime.utcnow()
# 清理过期 Token
for token_hash, data in list(self.token_db.items()):
expires_at = datetime.fromisoformat(data["expires_at"])
if current_time > expires_at:
del self.token_db[token_hash]
cleaned_count += 1
# 清理过期黑名单
for token_hash, data in list(self.blacklist_db.items()):
expires_at = datetime.fromisoformat(data["expires_at"])
if current_time > expires_at:
del self.blacklist_db[token_hash]
cleaned_count += 1
return cleaned_count
def close(self):
with self.lock:
"""关闭所有数据库连接"""
self.token_db.close()
self.blacklist_db.close()
self.user_sessions_db.close()
# 全局缓存实例
_jwt_cache = None
def get_jwt_cache() -> JWTCache:
"""获取 JWT 缓存实例(单例模式)"""
global _jwt_cache
if _jwt_cache is None:
_jwt_cache = JWTCache()
return _jwt_cache
# Token 响应模型
class TokenResponse(BaseModel):
"""Token 响应模型"""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int
refresh_expires_in: int
# Token 数据模型
class TokenData(BaseModel):
"""Token 数据模型"""
user_id: str
username: Optional[str] = None
email: Optional[str] = None
scopes: List[str] = []
@@ -1,14 +1,16 @@
import logging import logging
from pathlib import Path from pathlib import Path
class ExcludeWatchFilesFilter(logging.Filter): class ExcludeWatchFilesFilter(logging.Filter):
def filter(self, record): def filter(self, record):
# 如果日志记录来自 watchfiles 模块,则不记录(返回 False) # 如果日志记录来自 watchfiles 模块,则不记录(返回 False)
return not record.name.startswith('watchfiles') return not record.name.startswith("watchfiles")
def setup_logging(log_config: dict): def setup_logging(log_config: dict):
log_level = getattr(logging, log_config["level"], logging.INFO) log_level = getattr(logging, log_config["level"], logging.INFO)
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 创建文件夹 # 创建文件夹
log_file_path = Path(log_config["file"]) log_file_path = Path(log_config["file"])
log_file_dir = log_file_path.parent log_file_dir = log_file_path.parent
@@ -39,7 +41,10 @@ def setup_logging(log_config: dict):
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler) root_logger.addHandler(console_handler)
print(f"日志系统已配置 - 级别: {logging.getLevelName(log_level)}, 文件: {log_config['file']}") print(
f"日志系统已配置 - 级别: {logging.getLevelName(log_level)}, 文件: {log_config['file']}"
)
def get_logger(name: str) -> logging.Logger: def get_logger(name: str) -> logging.Logger:
logger = logging.getLogger(name) logger = logging.getLogger(name)
@@ -1,6 +1,7 @@
""" """
数据库模型 数据库模型
""" """
from .item import Item from .item import Item
from .user import User from .user import User
@@ -1,17 +1,21 @@
""" """
Item 数据模型 Item 数据模型
""" """
from tortoise.models import Model from tortoise.models import Model
from tortoise import fields from tortoise import fields
from datetime import datetime from datetime import datetime
class Item(Model): class Item(Model):
"""物品模型""" """物品模型"""
id = fields.IntField(pk=True, description="主键ID") id = fields.IntField(pk=True, description="主键ID")
name = fields.CharField(max_length=100, description="名称") name = fields.CharField(max_length=100, description="名称")
description = fields.TextField(null=True, description="描述") description = fields.TextField(null=True, description="描述")
price = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00, description="价格") price = fields.DecimalField(
max_digits=10, decimal_places=2, default=0.00, description="价格"
)
is_active = fields.BooleanField(default=True, description="是否激活") is_active = fields.BooleanField(default=True, description="是否激活")
# 关联用户(可选) # 关联用户(可选)
@@ -20,7 +24,7 @@ class Item(Model):
related_name="items", related_name="items",
null=True, null=True,
on_delete=fields.SET_NULL, on_delete=fields.SET_NULL,
description="所有者" description="所有者",
) )
# 时间戳 # 时间戳
@@ -30,7 +34,7 @@ class Item(Model):
def __str__(self): def __str__(self):
return f"Item(id={self.id}, name={self.name}, price={self.price})" return f"Item(id={self.id}, name={self.name}, price={self.price})"
class Meta: class Meta: # type: ignore
table = "items" table = "items"
table_description = "物品表" table_description = "物品表"
ordering = ["-created_at"] ordering = ["-created_at"]
@@ -1,11 +1,13 @@
""" """
User 数据模型 User 数据模型
""" """
from tortoise.models import Model from tortoise.models import Model
from tortoise import fields from tortoise import fields
from datetime import datetime from datetime import datetime
from .item import Item from .item import Item
class User(Model): class User(Model):
"""用户模型""" """用户模型"""
@@ -26,7 +28,7 @@ class User(Model):
def __str__(self): def __str__(self):
return f"User(id={self.id}, username={self.username})" return f"User(id={self.id}, username={self.username})"
class Meta: class Meta: # type: ignore
table = "users" table = "users"
table_description = "用户表" table_description = "用户表"
ordering = ["-created_at"] ordering = ["-created_at"]
@@ -1,11 +1,7 @@
""" """
Pydantic 模型 Pydantic 模型
""" """
from .item import ItemCreate, ItemUpdate, ItemResponse, ItemInDB from .item import ItemCreate, ItemUpdate, ItemResponse, ItemInDB
__all__ = [ __all__ = ["ItemCreate", "ItemUpdate", "ItemResponse", "ItemInDB"]
"ItemCreate",
"ItemUpdate",
"ItemResponse",
"ItemInDB"
]
@@ -1,29 +1,40 @@
""" """
Item Pydantic 模型 Item Pydantic 模型
""" """
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from decimal import Decimal from decimal import Decimal
class ItemBase(BaseModel): class ItemBase(BaseModel):
"""Item 基础模型""" """Item 基础模型"""
name: str = Field(..., min_length=1, max_length=100, description="物品名称") name: str = Field(..., min_length=1, max_length=100, description="物品名称")
description: Optional[str] = Field(None, description="物品描述") description: Optional[str] = Field(None, description="物品描述")
price: Decimal = Field(Decimal(0.00), ge=0, description="物品价格") price: Decimal = Field(Decimal(0.00), ge=0, description="物品价格")
class ItemCreate(ItemBase): class ItemCreate(ItemBase):
"""创建 Item 模型""" """创建 Item 模型"""
pass pass
class ItemUpdate(BaseModel): class ItemUpdate(BaseModel):
"""更新 Item 模型""" """更新 Item 模型"""
name: Optional[str] = Field(None, min_length=1, max_length=100, description="物品名称")
name: Optional[str] = Field(
None, min_length=1, max_length=100, description="物品名称"
)
description: Optional[str] = Field(None, description="物品描述") description: Optional[str] = Field(None, description="物品描述")
price: Optional[Decimal] = Field(None, ge=0, description="物品价格") price: Optional[Decimal] = Field(None, ge=0, description="物品价格")
class ItemInDB(ItemBase): class ItemInDB(ItemBase):
"""数据库中的 Item 模型""" """数据库中的 Item 模型"""
id: int id: int
is_active: bool is_active: bool
created_at: datetime created_at: datetime
@@ -32,6 +43,8 @@ class ItemInDB(ItemBase):
class Config: class Config:
from_attributes = True from_attributes = True
class ItemResponse(ItemInDB): class ItemResponse(ItemInDB):
"""Item 响应模型""" """Item 响应模型"""
pass pass
@@ -3,26 +3,39 @@ from pydantic import BaseModel, EmailStr
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
class UserBase(BaseModel): class UserBase(BaseModel):
username: str username: str
email: EmailStr email: EmailStr
class UserCreate(UserBase): class UserCreate(UserBase):
password: str password: str
class UserUpdate(BaseModel): class UserUpdate(BaseModel):
username: Optional[str] = None username: Optional[str] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
password: Optional[str] = None password: Optional[str] = None
class UserInDB(UserBase): class UserInDB(UserBase):
id: int id: int
is_active: bool is_active: bool
is_superuser: bool
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
class Config: class Config:
from_attributes = True from_attributes = True
class UserResponse(UserInDB): class UserResponse(UserInDB):
pass pass
# 新增登录模型
class UserLogin(BaseModel):
username: str
password: str
device_id: Optional[str] = None # 设备ID,用于多设备登录
@@ -0,0 +1,163 @@
"""
认证服务 - 处理用户认证和 Token 管理
"""
from typing import Optional, Dict, Any
from datetime import datetime, timedelta
from fastapi import HTTPException, status, Request
from passlib.context import CryptContext
from ..models.user import User
from ..schemas.user import UserCreate, UserLogin
from .user_service import UserService
from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData
from ..core.config import get_settings
class AuthService:
"""认证服务"""
def __init__(self):
self.jwt_cache = get_jwt_cache()
self.settings = get_settings()
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def authenticate_user(self, username: str, password: str) -> Optional[User]:
"""验证用户凭据"""
user = await UserService.get_user_by_username(username)
if not user:
return None
if not UserService.verify_password(password, user.hashed_password):
return None
return user
async def create_tokens(
self,
user: User,
request: Optional[Request] = None,
device_id: Optional[str] = None,
) -> TokenResponse:
"""为用户创建访问和刷新令牌"""
# 获取用户数据
user_data = {
"username": user.username,
"email": user.email,
"is_superuser": user.is_superuser,
}
# 获取设备信息
user_agent = None
ip_address = None
if request:
user_agent = request.headers.get("user-agent")
ip_address = request.client.host if request.client else None
# 创建访问令牌
access_token = self.jwt_cache.create_token(
user_id=str(user.id),
user_data=user_data,
token_type="access",
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
)
# 创建刷新令牌
refresh_token = self.jwt_cache.create_token(
user_id=str(user.id),
user_data=user_data,
token_type="refresh",
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
)
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
expires_in=self.settings.access_token_expire_minutes * 60,
refresh_expires_in=getattr(self.settings, "refresh_token_expire_days", 7)
* 24
* 60
* 60,
)
async def validate_access_token(self, token: str) -> Optional[TokenData]:
"""验证访问令牌"""
validation = self.jwt_cache.validate_token(token, token_type="access")
if not validation:
return None
return TokenData(
user_id=validation["user_id"],
username=validation["payload"]["data"].get("username"),
email=validation["payload"]["data"].get("email"),
scopes=[], # 可以根据需要添加权限范围
)
async def refresh_access_token(
self, refresh_token: str, request: Optional[Request] = None
) -> Optional[str]:
"""使用刷新令牌获取新的访问令牌"""
validation = self.jwt_cache.validate_token(refresh_token, token_type="refresh")
if not validation:
return None
user_id = validation["user_id"]
# 获取用户
user = await UserService.get_user_by_id(int(user_id))
if not user:
return None
# 创建新的访问令牌
user_data = {
"username": user.username,
"email": user.email,
"is_superuser": user.is_superuser,
}
device_id = validation.get("device_id")
user_agent = None
ip_address = None
if request:
user_agent = request.headers.get("user-agent")
ip_address = request.client.host if request.client else None
new_access_token = self.jwt_cache.create_token(
user_id=user_id,
user_data=user_data,
token_type="access",
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
)
return new_access_token
async def logout(self, token: str):
"""用户登出 - 使令牌失效"""
self.jwt_cache.invalidate_token(token)
async def logout_all(self, user_id: str):
"""使用户的所有令牌失效"""
self.jwt_cache.invalidate_user_tokens(str(user_id))
async def get_user_sessions(self, user_id: str):
"""获取用户的活跃会话"""
return self.jwt_cache.get_user_sessions(str(user_id))
# 全局认证服务实例
_auth_service = None
def get_auth_service() -> AuthService:
"""获取认证服务实例(单例模式)"""
global _auth_service
if _auth_service is None:
_auth_service = AuthService()
return _auth_service
@@ -3,12 +3,11 @@ from typing import Optional, List
from ..models.item import Item from ..models.item import Item
from ..schemas.item import ItemCreate, ItemUpdate from ..schemas.item import ItemCreate, ItemUpdate
class ItemService: class ItemService:
@staticmethod @staticmethod
async def get_all_items( async def get_all_items(
skip: int = 0, skip: int = 0, limit: int = 100, owner_id: Optional[int] = None
limit: int = 100,
owner_id: Optional[int] = None
) -> List[Item]: ) -> List[Item]:
query = Item.all() query = Item.all()
if owner_id: if owner_id:
@@ -20,11 +19,10 @@ class ItemService:
return await Item.filter(id=item_id).first().prefetch_related("owner") return await Item.filter(id=item_id).first().prefetch_related("owner")
@staticmethod @staticmethod
async def create_item(item_data: ItemCreate, owner_id: Optional[int] = None) -> Item: async def create_item(
item = await Item.create( item_data: ItemCreate, owner_id: Optional[int] = None
**item_data.model_dump(), ) -> Item:
owner_id=owner_id item = await Item.create(**item_data.model_dump(), owner_id=owner_id)
)
return item return item
@staticmethod @staticmethod
@@ -6,6 +6,7 @@ from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class UserService: class UserService:
@staticmethod @staticmethod
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
@@ -37,7 +38,7 @@ class UserService:
user = await User.create( user = await User.create(
username=user_data.username, username=user_data.username,
email=user_data.email, email=user_data.email,
hashed_password=hashed_password hashed_password=hashed_password,
) )
return user return user
@@ -65,3 +66,23 @@ class UserService:
await user.delete() await user.delete()
return True return True
return False return False
@staticmethod
async def deactivate_user(user_id: int) -> Optional[User]:
"""停用用户"""
user = await UserService.get_user_by_id(user_id)
if user:
user.is_active = False
await user.save()
return user
return None
@staticmethod
async def activate_user(user_id: int) -> Optional[User]:
"""激活用户"""
user = await UserService.get_user_by_id(user_id)
if user:
user.is_active = True
await user.save()
return user
return None
@@ -5,50 +5,112 @@ import shutil
import subprocess import subprocess
import sys import sys
def create_admin():
try:
cmd = [
sys.executable,
"../src/{{ cookiecutter.project_name }}/scripts/create_admin.py",
]
print(f"正在运行 create_admin.py: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
if result.returncode == 0:
print("创建管理员用户成功")
print(result.stdout)
else:
print("创建管理员用户失败")
print("错误信息:", result.stderr)
except Exception as e:
print("创建管理员用户失败")
def run_pyinstaller(): def run_pyinstaller():
"""运行 PyInstaller 打包成可执行文件""" """运行 PyInstaller 打包成可执行文件"""
try: try:
# 构建 PyInstaller 命令 # 构建 PyInstaller 命令
cmd = [ cmd = [
sys.executable, "-m", "PyInstaller", sys.executable,
"-m",
"PyInstaller",
"--onefile", "--onefile",
"--name", "{{ cookiecutter.project_name }}", "--name",
"--distpath", ".", "{{ cookiecutter.project_name }}",
"--workpath", "./build_temp", "--distpath",
"--specpath", "./specs", ".",
"--workpath",
"./build_temp",
"--specpath",
"./specs",
"--clean", "--clean",
# 隐藏导入 # 隐藏导入
"--hidden-import", "passlib", "--hidden-import",
"--hidden-import", "passlib.context", "passlib",
"--hidden-import", "passlib.handlers.bcrypt", "--hidden-import",
"--hidden-import", "fastapi", "passlib.context",
"--hidden-import", "fastapi.middleware.cors", "--hidden-import",
"--hidden-import", "pydantic_settings", "passlib.handlers.bcrypt",
"--hidden-import", "tortoise", "--hidden-import",
"--hidden-import", "tortoise.contrib.fastapi", "fastapi",
"--hidden-import", "aerich", "--hidden-import",
"--hidden-import", "uvicorn", "fastapi.middleware.cors",
"--hidden-import",
"pydantic_settings",
"--hidden-import",
"tortoise",
"--hidden-import",
"tortoise.contrib.fastapi",
"--hidden-import",
"aerich",
"--hidden-import",
"uvicorn",
"--hidden-import",
"jwt",
"--hidden-import",
"PyJWT",
"--hidden-import",
"sqlitedict",
# 复制包元数据 - 解决 importlib.metadata 问题 # 复制包元数据 - 解决 importlib.metadata 问题
"--copy-metadata", "tortoise-orm", "--copy-metadata",
"--copy-metadata", "aerich", "tortoise-orm",
"--copy-metadata", "fastapi", "--copy-metadata",
"--copy-metadata", "uvicorn", "aerich",
"--copy-metadata", "pydantic", "--copy-metadata",
"--copy-metadata", "pydantic-settings", "fastapi",
"--copy-metadata", "module-bank", "--copy-metadata",
"--copy-metadata", "passlib", "uvicorn",
"--copy-metadata", "sqlite-vfs", "--copy-metadata",
"--copy-metadata", "toml", "pydantic",
"--copy-metadata",
"pydantic-settings",
"--copy-metadata",
"module-bank",
"--copy-metadata",
"passlib",
"--copy-metadata",
"sqlite-vfs",
"--copy-metadata",
"toml",
"--copy-metadata",
"PyJWT",
"--copy-metadata",
"sqlitedict",
# 收集整个包 # 收集整个包
"--collect-all", "tortoise-orm", "--collect-all",
"--collect-all", "aerich", "tortoise-orm",
"--collect-all",
"aerich",
"--collect-all",
"PyJWT",
"--collect-all",
"sqlitedict",
# 禁用多进程优化 # 禁用多进程优化
# "--multiprocessing-fork", # 明确指定使用 fork 模式 # "--multiprocessing-fork", # 明确指定使用 fork 模式
# 添加数据文件 # 添加数据文件
# "--add-data", "resources:resources", # "--add-data", "resources:resources",
# 其他选项 # 其他选项
"--icon", "../../public/{{ cookiecutter.project_name }}.ico", "--icon",
"main.py" "../../public/{{ cookiecutter.project_name }}.ico",
"main.py",
] ]
print(f"正在运行 PyInstaller: {' '.join(cmd)}") print(f"正在运行 PyInstaller: {' '.join(cmd)}")
@@ -66,6 +128,7 @@ def run_pyinstaller():
except Exception as e: except Exception as e:
print(f"PyInstaller 执行出错: {e}") print(f"PyInstaller 执行出错: {e}")
def main(): def main():
# 打包模块 # 打包模块
resources_path = Path("resources") resources_path = Path("resources")
@@ -74,12 +137,14 @@ def main():
app_packer = PythonToSQLite("./resources/app.mbank") app_packer = PythonToSQLite("./resources/app.mbank")
app_packer.pack_directory("src/{{ cookiecutter.project_name }}/app", "app") app_packer.pack_directory("src/{{ cookiecutter.project_name }}/app", "app")
app_packer.verify_package_structure() app_packer.verify_package_structure()
# app_packer.delete_source_code(None) app_packer.delete_source_code(None)
plugin_packer = PythonToSQLite("./resources/plugins.mbank") plugin_packer = PythonToSQLite("./resources/plugins.mbank")
plugin_packer.pack_directory("src/{{ cookiecutter.project_name }}/plugins", "plugins") plugin_packer.pack_directory(
"src/{{ cookiecutter.project_name }}/plugins", "plugins"
)
plugin_packer.verify_package_structure() plugin_packer.verify_package_structure()
# plugin_packer.delete_source_code(None) plugin_packer.delete_source_code(None)
# 创建 build 目录 # 创建 build 目录
build_path = Path("build") build_path = Path("build")
@@ -114,6 +179,9 @@ def main():
# 运行 PyInstaller # 运行 PyInstaller
run_pyinstaller() run_pyinstaller()
# 创建管理员
create_admin()
# 清理临时文件 # 清理临时文件
shutil.rmtree(r"build/build_temp", ignore_errors=True) shutil.rmtree(r"build/build_temp", ignore_errors=True)
shutil.rmtree(r"build/specs", ignore_errors=True) shutil.rmtree(r"build/specs", ignore_errors=True)
@@ -123,5 +191,6 @@ def main():
print("构建完成") print("构建完成")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
创建管理员用户脚本
"""
import asyncio
import sys
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent.parent.parent
sys.path.insert(0, str(project_root))
async def main():
try:
# 首先初始化 Tortoise-ORM
from tortoise import Tortoise
from {{ cookiecutter.project_name }}.app.core.config import get_settings
settings = get_settings()
db_config = getattr(
settings, f"{settings.database_type}_config"
).tortoise_config
# Tortoise-ORM 配置
db_config = {
"connections": db_config,
"apps": {
"models": {
"models": ["{{ cookiecutter.project_name }}.app.models", "aerich.models"],
"default_connection": "default",
}
},
"use_tz": False,
"timezone": "UTC",
}
print("🔧 初始化 Tortoise-ORM...")
await Tortoise.init(db_config)
# 生成数据库表
print("🗃️ 生成数据库表结构...")
await Tortoise.generate_schemas()
# 导入模型和服务
from {{ cookiecutter.project_name }}.app.models.user import User
from {{ cookiecutter.project_name }}.app.services.user_service import UserService
from {{ cookiecutter.project_name }}.app.schemas.user import UserCreate
# 检查是否已存在管理员
admin = await User.filter(username="admin").first()
if admin:
print("✅ 管理员用户已存在")
print(f" 用户名: {admin.username}")
print(f" 邮箱: {admin.email}")
await Tortoise.close_connections()
return
# 创建管理员用户
user_data = UserCreate(
username="admin",
email="admin@example.com",
password="admin123", # 生产环境应使用复杂密码
)
admin = await UserService.create_user(user_data)
admin.is_superuser = True
await admin.save()
print(f"✅ 管理员用户创建成功")
print(f" 用户名: {admin.username}")
print(f" 邮箱: {admin.email}")
print(f" 密码: {user_data.password}")
print("\n⚠️ 警告:请在生产环境中修改默认密码!")
# 关闭数据库连接
await Tortoise.close_connections()
print("✅ 数据库连接已关闭")
except Exception as e:
print(f"❌ 创建管理员失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())
@@ -1,61 +0,0 @@
#!/usr/bin/env python3
"""
数据库迁移脚本
"""
import asyncio
import sys
import os
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
async def main():
from app.core.config import get_settings
from aerich import Command
settings = get_settings()
# Aerich 配置
TORTOISE_ORM = {
"connections": {
"default": settings.database_url
},
"apps": {
"models": {
"models": ["app.models", "aerich.models"],
"default_connection": "default",
}
},
"use_tz": False,
"timezone": "UTC",
}
command = Command(tortoise_config=TORTOISE_ORM, app="models")
# 初始化 Aerich(首次运行)
if not os.path.exists("./migrations"):
print("🔧 初始化数据库迁移...")
await command.init()
print("✅ 迁移初始化完成")
# 生成迁移文件
print("📝 生成迁移文件...")
try:
migration_name = sys.argv[1] if len(sys.argv) > 1 else "update"
await command.migrate(name=migration_name)
print(f"✅ 迁移文件生成成功: {migration_name}")
except Exception as e:
print(f"⚠️ 生成迁移文件失败: {e}")
# 尝试升级
pass
# 应用迁移
print("🔄 应用数据库迁移...")
try:
await command.upgrade()
print("✅ 迁移应用成功")
except Exception as e:
print(f"❌ 迁移应用失败: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -5,6 +5,7 @@ import os
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
class ConfigLoader: class ConfigLoader:
"""TOML配置文件加载器""" """TOML配置文件加载器"""
@@ -29,7 +30,7 @@ class ConfigLoader:
"config.toml", "config.toml",
"config/config.toml", "config/config.toml",
"~/.config/myapp/config.toml", "~/.config/myapp/config.toml",
"/etc/myapp/config.toml" "/etc/myapp/config.toml",
] ]
for path in possible_paths: for path in possible_paths:
@@ -44,12 +45,12 @@ class ConfigLoader:
if not self.config_path.exists(): if not self.config_path.exists():
raise FileNotFoundError(f"配置文件不存在: {self.config_path}") raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
with open(self.config_path, 'r', encoding='utf-8') as f: with open(self.config_path, "r", encoding="utf-8") as f:
self._config = toml.load(f) self._config = toml.load(f)
def get(self, key: str, default: Any = None) -> Any: def get(self, key: str, default: Any = None) -> Any:
"""获取配置值,支持点号分隔的路径""" """获取配置值,支持点号分隔的路径"""
keys = key.split('.') keys = key.split(".")
value = self._config value = self._config
try: try:
@@ -79,4 +80,3 @@ class ConfigLoader:
def all(self) -> Dict[str, Any]: def all(self) -> Dict[str, Any]:
"""获取所有配置""" """获取所有配置"""
return self._config return self._config
@@ -5,9 +5,11 @@ import os
from module_bank import PythonToSQLite from module_bank import PythonToSQLite
from typing import Callable from typing import Callable
# 检查是否在 PyInstaller 子进程中 # 检查是否在 PyInstaller 子进程中
def is_pyinstaller_child(): def is_pyinstaller_child():
return len(sys.argv) > 1 and ('_child.py' in sys.argv[0] or sys.argv[1].isdigit()) return len(sys.argv) > 1 and ("_child.py" in sys.argv[0] or sys.argv[1].isdigit())
def parse_args(): def parse_args():
"""安全的参数解析函数""" """安全的参数解析函数"""
@@ -15,13 +17,16 @@ def parse_args():
# PyInstaller 子进程,使用默认参数 # PyInstaller 子进程,使用默认参数
return argparse.Namespace(config="config.toml") return argparse.Namespace(config="config.toml")
parser = argparse.ArgumentParser(description="chongming 应用启动脚本") parser = argparse.ArgumentParser(
description="{{ cookiecutter.project_name }} 应用启动脚本"
)
parser.add_argument("--config", default="config.toml", help="配置文件路径") parser.add_argument("--config", default="config.toml", help="配置文件路径")
return parser.parse_args() return parser.parse_args()
args = parse_args() args = parse_args()
def init_module_bank(config): def init_module_bank(config):
MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"] MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"]
for module_path in MODULE_BANK_PATH_LIST: for module_path in MODULE_BANK_PATH_LIST:
@@ -35,6 +40,7 @@ def init_module_bank(config):
print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}") print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}")
packer.install_importer() packer.install_importer()
def launch(run_fun: Callable[[dict, dict], None], run_mode: str): def launch(run_fun: Callable[[dict, dict], None], run_mode: str):
config = ConfigLoader(args.config) config = ConfigLoader(args.config)