first commit
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
logs/
|
||||
resources/
|
||||
output.md
|
||||
@@ -0,0 +1 @@
|
||||
3.11
|
||||
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 chakcy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,58 @@
|
||||
# chongming
|
||||
|
||||
[TOC]
|
||||
|
||||
作者: chakcy
|
||||
|
||||
版本: 0.1.0
|
||||
|
||||
## 运行项目
|
||||
|
||||
uv 环境
|
||||
|
||||
```shell
|
||||
uv run serve
|
||||
```
|
||||
|
||||
纯 python 环境
|
||||
|
||||
```shell
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # windwows 命令为 .venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
serve
|
||||
```
|
||||
|
||||
## 打包项目
|
||||
|
||||
uv 环境
|
||||
|
||||
```shell
|
||||
uv run build
|
||||
```
|
||||
|
||||
纯 python 环境
|
||||
```shell
|
||||
source .venv/bin/activate # windwows 命令为 .venv\Scripts\activate
|
||||
build
|
||||
```
|
||||
|
||||
打包出来的文件在 build 目录下
|
||||
|
||||
```text
|
||||
build
|
||||
├── resources
|
||||
│ ├── chongming.ico
|
||||
│ ├── plugins.mbank
|
||||
│ └── app.mbank
|
||||
├── config.toml
|
||||
├── db.sqlite3
|
||||
└── chongming # windows chongming.exe
|
||||
```
|
||||
|
||||
运行打包后的项目
|
||||
```shell
|
||||
cd build
|
||||
chongming # windows chongming.exe
|
||||
```
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
[default]
|
||||
app.name = "chongming"
|
||||
app.version = "chongming"
|
||||
app.description = "基于 FastAPI 和 Tortoise-ORM 的 chongming 打包脚手架"
|
||||
app.debug = false
|
||||
env = "development"
|
||||
|
||||
[database]
|
||||
type = "sqlite"
|
||||
url = "sqlite+aiosqlite:///./database.db"
|
||||
generate_schemas = true # 生产环境使用迁移工具
|
||||
|
||||
[database.sqlite]
|
||||
echo = true
|
||||
pool_size = 20
|
||||
max_overflow = 10
|
||||
pool_timeout = 30
|
||||
pool_recycle = 300
|
||||
pool_pre_ping = true
|
||||
connect_args.timeout = 15
|
||||
connect_args.check_same_thread = false
|
||||
|
||||
[security]
|
||||
secret_key = "your-secret-key-change-in-production"
|
||||
algorithm = "HS256"
|
||||
access_token_expire_minutes = 30
|
||||
refresh_token_expire_days = 7
|
||||
jwt_cache_db_path = "jwt_cache"
|
||||
max_sessions_per_user = 5
|
||||
|
||||
[development]
|
||||
env = "development"
|
||||
debug = true
|
||||
|
||||
[development.server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
reload = true
|
||||
|
||||
[development.logging]
|
||||
level = "DEBUG"
|
||||
file = "logs/dev.log"
|
||||
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
[development.cors]
|
||||
allow_origins = ["*"]
|
||||
allow_credentials = true
|
||||
allow_methods = ["*"]
|
||||
allow_headers = ["*"]
|
||||
|
||||
[production]
|
||||
env = "production"
|
||||
debug = false
|
||||
|
||||
[production.server]
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
reload = false
|
||||
workers = 4
|
||||
|
||||
[production.logging]
|
||||
level = "INFO"
|
||||
file = "logs/prod.log"
|
||||
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
max_size = 10485760 # 10MB
|
||||
backup_count = 5 # 最多保存 10 个日志文件
|
||||
|
||||
[production.cors]
|
||||
allow_origins = ["https://yourdomain.com", "http://localhost:8000", "http://127.0.0.1:8000"]
|
||||
allow_credentials = true
|
||||
allow_methods = ["GET", "POST", "PUT", "DELETE"]
|
||||
allow_headers = ["Authorization", "Content-Type"]
|
||||
|
||||
[production.module_system]
|
||||
type = "module-bank"
|
||||
path = ["resources/app.mbank", "resources/plugins.mbank"]
|
||||
|
||||
[production.file_system]
|
||||
type = "svfs"
|
||||
path = "resources/application.vfs"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
@@ -0,0 +1,78 @@
|
||||
[default]
|
||||
app.name = "chongming"
|
||||
app.version = "0.1.0"
|
||||
app.description = "基于 FastAPI 的 chongming 打包脚手架"
|
||||
app.debug = false
|
||||
env = "production"
|
||||
|
||||
[database]
|
||||
type = "sqlite"
|
||||
url = "sqlite+aiosqlite:///./database.db"
|
||||
generate_schemas = true # 生产环境使用迁移工具
|
||||
|
||||
[database.sqlite]
|
||||
echo = true
|
||||
pool_size = 20
|
||||
max_overflow = 10
|
||||
pool_timeout = 30
|
||||
pool_recycle = 300
|
||||
pool_pre_ping = true
|
||||
connect_args.timeout = 15
|
||||
connect_args.check_same_thread = false
|
||||
|
||||
[security]
|
||||
secret_key = "your-secret-key-change-in-production"
|
||||
algorithm = "HS256"
|
||||
access_token_expire_minutes = 30
|
||||
refresh_token_expire_days = 7
|
||||
jwt_cache_db_path = "jwt_cache"
|
||||
max_sessions_per_user = 5
|
||||
|
||||
[development]
|
||||
env = "development"
|
||||
debug = true
|
||||
|
||||
[development.server]
|
||||
host = "127.0.0.1"
|
||||
port = 8001
|
||||
reload = true
|
||||
|
||||
[development.logging]
|
||||
level = "DEBUG"
|
||||
file = "logs/dev.log"
|
||||
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
[development.cors]
|
||||
allow_origins = ["*"]
|
||||
allow_credentials = true
|
||||
allow_methods = ["*"]
|
||||
allow_headers = ["*"]
|
||||
|
||||
[production]
|
||||
env = "production"
|
||||
debug = false
|
||||
|
||||
[production.server]
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
reload = false
|
||||
workers = 4
|
||||
|
||||
[production.logging]
|
||||
level = "INFO"
|
||||
file = "logs/prod.log"
|
||||
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
[production.cors]
|
||||
allow_origins = ["https://yourdomain.com", "http://localhost:8000", "http://127.0.0.1:8000"]
|
||||
allow_credentials = true
|
||||
allow_methods = ["GET", "POST", "PUT", "DELETE"]
|
||||
allow_headers = ["Authorization", "Content-Type"]
|
||||
|
||||
[production.module_system]
|
||||
type = "module-bank"
|
||||
path = ["resources/app.mbank", "resources/plugins.mbank"]
|
||||
|
||||
[production.file_system]
|
||||
type = "svfs"
|
||||
path = "resources/application.vfs"
|
||||
@@ -0,0 +1,67 @@
|
||||
# main.py - 修复 reload 问题
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastAPI + Tortoise-ORM + chongming 示例
|
||||
主入口文件,适配 chongming 打包系统
|
||||
"""
|
||||
import sys
|
||||
from utils.launch import launch
|
||||
from multiprocessing import freeze_support
|
||||
import server
|
||||
|
||||
|
||||
def app_run(app_config: dict, default_config: dict):
|
||||
"""
|
||||
应用运行函数 - 适配模块银行环境
|
||||
|
||||
Args:
|
||||
app_config: 应用配置
|
||||
default_config: 默认配置
|
||||
"""
|
||||
import uvicorn
|
||||
from app.core.logger import setup_logging, get_logger # type: ignore
|
||||
|
||||
setup_logging(app_config["logging"])
|
||||
logger = get_logger("app")
|
||||
|
||||
# 获取服务器配置
|
||||
server_config = app_config.get("server", {})
|
||||
host = server_config.get("host", "0.0.0.0")
|
||||
port = server_config.get("port", 8000)
|
||||
|
||||
# 打包环境强制禁用重载
|
||||
is_frozen = getattr(sys, "frozen", False)
|
||||
reload = False if is_frozen else server_config.get("reload", False)
|
||||
workers = server_config.get("workers", 4)
|
||||
|
||||
# 显示启动信息
|
||||
env = app_config.get("env", "development")
|
||||
logger.info("=" * 50)
|
||||
logger.info(f"🚀 启动 {default_config.get('app.name', 'FastAPI 应用')}")
|
||||
logger.info(f"📦 版本: {default_config.get('app.version', '1.0.0')}")
|
||||
logger.info(f"🌍 环境: {env}")
|
||||
logger.info(f"📍 地址: http://{host}:{port}")
|
||||
logger.info(f"📚 文档: http://{host}:{port}/docs")
|
||||
logger.info(f"🔧 调试: {'启用' if app_config.get('debug', False) else '禁用'}")
|
||||
logger.info(f"🔄 重载: {'启用' if reload else '禁用'}")
|
||||
logger.info(f"👥 Workers: {workers}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
uvicorn.run(
|
||||
"server:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=reload,
|
||||
workers=workers,
|
||||
log_level="debug" if app_config.get("debug", False) else "info",
|
||||
access_log=True,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
freeze_support()
|
||||
launch(app_run, "production")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
# server.py
|
||||
import sys
|
||||
from utils.launch import init_module_bank
|
||||
from utils.config import ConfigLoader
|
||||
|
||||
config = ConfigLoader("config.toml")
|
||||
init_module_bank(config)
|
||||
|
||||
# 从数据库导入真正的 app 模块
|
||||
try:
|
||||
import app # type: ignore # 此时会从 SQLite 数据库加载
|
||||
|
||||
app_instance = app.app # 获取 FastAPI 实例
|
||||
except ImportError as e:
|
||||
print(f"无法从数据库导入 app 模块: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 暴露 ASGI 应用变量,供 uvicorn 使用
|
||||
app = app_instance
|
||||
@@ -0,0 +1,88 @@
|
||||
# config_loader.py - 读取config.toml的示例代码
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""TOML配置文件加载器"""
|
||||
|
||||
def __init__(self, config_path: Optional[str] = None):
|
||||
"""
|
||||
初始化配置加载器
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径,如果为None则自动查找
|
||||
"""
|
||||
if config_path is None:
|
||||
# 自动查找配置文件
|
||||
config_path = self._find_config_file()
|
||||
|
||||
self.config_path = Path(config_path)
|
||||
self._config: Dict[str, Any] = {}
|
||||
self.load()
|
||||
|
||||
def _find_config_file(self) -> str:
|
||||
"""自动查找配置文件"""
|
||||
possible_paths = [
|
||||
"config.toml",
|
||||
"config/config.toml",
|
||||
"~/.config/myapp/config.toml",
|
||||
"/etc/myapp/config.toml",
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
expanded_path = os.path.expanduser(path)
|
||||
if os.path.exists(expanded_path):
|
||||
return expanded_path
|
||||
|
||||
raise FileNotFoundError("未找到配置文件")
|
||||
|
||||
def load(self) -> None:
|
||||
"""加载配置文件"""
|
||||
if not self.config_path.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
||||
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self._config = tomllib.loads(f.read())
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置值,支持点号分隔的路径"""
|
||||
keys = key.split(".")
|
||||
value = self._config
|
||||
|
||||
try:
|
||||
for k in keys:
|
||||
value = value[k]
|
||||
return value
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
def get_section(self, section: str) -> Dict[str, Any]:
|
||||
"""获取整个配置节"""
|
||||
return self._config.get(section, {})
|
||||
|
||||
def reload(self) -> None:
|
||||
"""重新加载配置文件"""
|
||||
self.load()
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
"""支持字典式访问"""
|
||||
return self.get(key)
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
"""检查配置是否存在"""
|
||||
return self.get(key) is not None
|
||||
|
||||
@property
|
||||
def all(self) -> Dict[str, Any]:
|
||||
"""获取所有配置"""
|
||||
return self._config
|
||||
@@ -0,0 +1,32 @@
|
||||
from .config import ConfigLoader
|
||||
import os
|
||||
from module_bank import PythonToSQLite
|
||||
from typing import Callable
|
||||
|
||||
key = None
|
||||
|
||||
|
||||
def init_module_bank(config):
|
||||
MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"]
|
||||
for module_path in MODULE_BANK_PATH_LIST:
|
||||
if os.path.exists(module_path) == False:
|
||||
raise ValueError(f"模块路径不存在: {module_path}")
|
||||
|
||||
packer_list = [
|
||||
PythonToSQLite(module_path, key=key) for module_path in MODULE_BANK_PATH_LIST
|
||||
]
|
||||
for packer in packer_list:
|
||||
modules = packer.list_modules()
|
||||
for module in modules:
|
||||
print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}")
|
||||
packer.install_importer(key=key)
|
||||
|
||||
|
||||
def launch(run_fun: Callable[[dict, dict], None], run_mode: str):
|
||||
config = ConfigLoader("config.toml")
|
||||
|
||||
# 获取对应运行模式的配置
|
||||
default_config = config.get("default", {})
|
||||
app_config = config.get(run_mode, {})
|
||||
|
||||
run_fun(app_config, default_config)
|
||||
@@ -0,0 +1,45 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "chongming"
|
||||
version = "0.1.0"
|
||||
description = "基于 modules-bank 的 fastapi 打包脚手架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"aiosqlite",
|
||||
"bcrypt==4.0.1",
|
||||
"fastapi[uvicorn]",
|
||||
"module-bank",
|
||||
"passlib[bcrypt]",
|
||||
"pyjwt",
|
||||
"sqlite-vfs",
|
||||
"sqlitedict",
|
||||
"sqlmodel[aiosqlite]",
|
||||
"tomli",
|
||||
"typing-extensions",
|
||||
"uvicorn",
|
||||
]
|
||||
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
default = true
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://mirrors.ustc.edu.cn/pypi/web/simple/"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyarmor",
|
||||
"pyinstaller",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/chongming"]
|
||||
|
||||
[project.scripts]
|
||||
build = "chongming.scripts.build:build"
|
||||
serve = "chongming:serve"
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"venvPath": ".",
|
||||
"venv": ".venv",
|
||||
"pythonVersion": "3.11",
|
||||
"extraPaths": [
|
||||
"./",
|
||||
"./.venv/lib/python3.12/site-packages"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
aerich
|
||||
fastapi[uvicorn]
|
||||
module-bank==0.1.1
|
||||
passlib
|
||||
pydantic-settings
|
||||
pydantic[email]
|
||||
sqlite-vfs
|
||||
toml
|
||||
tortoise-orm
|
||||
uvicorn[standard]
|
||||
sqlitedict
|
||||
pyjwt
|
||||
bcrypt==4.0.1
|
||||
pyinstaller
|
||||
aerich
|
||||
requests
|
||||
@@ -0,0 +1,19 @@
|
||||
import uvicorn
|
||||
from .app.core.config import load_config
|
||||
from .app.core.logger import setup_logging, get_logger
|
||||
|
||||
|
||||
def serve():
|
||||
config = load_config()
|
||||
env = config["default"]["env"]
|
||||
server_config = config[env]["server"]
|
||||
log_config = config[env]["logging"]
|
||||
setup_logging(log_config)
|
||||
logget = get_logger("app")
|
||||
logget.info(f"启动服务: {server_config}")
|
||||
logget.info(f"启动日志: {log_config}")
|
||||
uvicorn.run("chongming.app:app", **server_config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
serve()
|
||||
@@ -0,0 +1,105 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlmodel import SQLModel, text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from .core.config import get_config
|
||||
from .core.jwt_cache import get_jwt_cache
|
||||
from .core.logger import get_logger
|
||||
from .api import api_router
|
||||
|
||||
config = get_config()
|
||||
logger = get_logger("app")
|
||||
|
||||
try:
|
||||
env = config["default"]["env"]
|
||||
except KeyError: # pragma: no cover
|
||||
raise ValueError("配置不存在")
|
||||
|
||||
|
||||
# 生命周期管理
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
from plugins import hello
|
||||
|
||||
hello()
|
||||
database = config["database"]
|
||||
database_type = database["type"]
|
||||
database_config = database.get(database_type, None)
|
||||
if database_config is None:
|
||||
raise ValueError("配置不存在")
|
||||
|
||||
# 启动时:创建引擎和会话工厂,初始化数据库表
|
||||
engine = create_async_engine(database["url"], **database_config)
|
||||
async_session_maker = async_sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
# 将资源存储到 app.state
|
||||
app.state.engine = engine
|
||||
app.state.async_session_maker = async_session_maker
|
||||
|
||||
# 创建表(异步方式)
|
||||
if env == "development":
|
||||
async with engine.begin() as conn:
|
||||
if database_type == "sqlite":
|
||||
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
||||
await conn.execute(text("PRAGMA synchronous=NORMAL"))
|
||||
try:
|
||||
await conn.run_sync(
|
||||
lambda conn: SQLModel.metadata.create_all(
|
||||
bind=conn, checkfirst=True
|
||||
)
|
||||
)
|
||||
except OperationalError as e:
|
||||
if "already exists" in str(e):
|
||||
raise
|
||||
|
||||
yield # 应用运行期间
|
||||
|
||||
# 关闭时:释放引擎
|
||||
await engine.dispose()
|
||||
get_jwt_cache().close()
|
||||
|
||||
|
||||
cors = config[env].get("cors", {})
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
app = FastAPI(
|
||||
title=config["default"]["app"]["name"],
|
||||
version=config["default"]["app"]["version"],
|
||||
description=config["default"]["app"]["description"],
|
||||
docs_url="/docs" if config[env]["debug"] else None,
|
||||
redoc_url="/redoc" if config[env]["debug"] else None,
|
||||
openapi_url="/openapi.json" if config[env]["debug"] else None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
# 添加 CORS 中间件
|
||||
app.add_middleware(CORSMiddleware, **cors)
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
|
||||
# 根路由
|
||||
@app.get("/", summary="根路径")
|
||||
async def root():
|
||||
"""根路径,返回应用信息"""
|
||||
return {
|
||||
"message": f"欢迎使用 {config["default"]["app"]["name"]}",
|
||||
"version": config["default"]["app"]["version"],
|
||||
"docs": "/docs" if config[env]["debug"] else None,
|
||||
"health": "/api/v1/health",
|
||||
}
|
||||
|
||||
|
||||
# 全局异常处理器
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request, exc):
|
||||
"""全局异常处理器"""
|
||||
logger.error(f"全局异常: {exc}")
|
||||
return {
|
||||
"error": "内部服务器错误",
|
||||
"detail": str(exc) if config[env]["debug"] else "请查看服务器日志",
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
API 路由
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .routers import todo, auth
|
||||
|
||||
# 创建主路由
|
||||
api_router = APIRouter()
|
||||
|
||||
# 注册子路由
|
||||
api_router.include_router(todo.router, prefix="/todos", tags=["todo"])
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||
|
||||
|
||||
# 导出路由
|
||||
__all__ = ["api_router"]
|
||||
@@ -0,0 +1,31 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
|
||||
from ..service.auth import get_auth_service
|
||||
from ..core.jwt_cache import TokenData
|
||||
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
) -> 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
|
||||
@@ -0,0 +1,134 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ...core.database import get_session
|
||||
from ...core.jwt_cache import get_jwt_cache, TokenData
|
||||
from ...core.security import verify_password
|
||||
from ...service.user import UserService
|
||||
from ...service.auth import get_auth_service
|
||||
from ...model.user import UserCreate, UserRead, UserLogin
|
||||
from ..deps import get_current_user
|
||||
|
||||
|
||||
router = APIRouter(tags=["authentication"])
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserRead)
|
||||
async def register(
|
||||
user_data: UserCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
print(user_data.password)
|
||||
|
||||
existing = await UserService.get_user_by_email(user_data.email, session)
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="邮箱已被注册")
|
||||
|
||||
user = await UserService.create_user(
|
||||
session=session,
|
||||
email=user_data.email,
|
||||
password=user_data.password,
|
||||
username=user_data.username,
|
||||
full_name=user_data.full_name,
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
user_data: UserLogin,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
user = await UserService.get_user_by_email(user_data.email, session)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="邮箱或密码错误")
|
||||
if not verify_password(user_data.password, user.hashed_password):
|
||||
raise HTTPException(status_code=400, detail="邮箱或密码错误")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="用户被禁用")
|
||||
|
||||
cache = get_jwt_cache()
|
||||
user_data_dict = {
|
||||
"sub": str(user.id),
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
}
|
||||
token = cache.create_token(
|
||||
user_id=str(user.id),
|
||||
user_data=user_data_dict,
|
||||
token_type="access",
|
||||
device_id=None,
|
||||
user_agent=request.headers.get("user-agent") if request else None,
|
||||
ip_address=request.client.host if request else None, # type: ignore
|
||||
)
|
||||
|
||||
return {"access_token": token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
|
||||
async def refresh_token(
|
||||
request: Request,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
auth_service = get_auth_service()
|
||||
|
||||
refresh_token = credentials.credentials
|
||||
new_access_token = await auth_service.refresh_access_token(
|
||||
refresh_token, session, 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"}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
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=UserRead, summary="获取当前用户信息")
|
||||
async def get_current_user_info(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: TokenData = Depends(get_current_user),
|
||||
):
|
||||
"""获取当前登录用户的信息"""
|
||||
user = await UserService.get_user_by_id(int(current_user.user_id), session)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
|
||||
return user
|
||||
@@ -0,0 +1,59 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ...model.todo import TodoCreate, TodoUpdate, TodoRead
|
||||
from ...core.database import get_session
|
||||
from ...service.todo import TodoService
|
||||
from ...model.user import User
|
||||
from ..deps import get_current_user
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# -------------------- 路由 --------------------
|
||||
@router.post("/", response_model=TodoRead, dependencies=[Depends(get_current_user)])
|
||||
async def create_todo(
|
||||
todo: TodoCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
return await TodoService.create_todo(todo, session)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/", response_model=List[TodoRead], dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
async def read_todos(
|
||||
offset: int = 0, limit: int = 100, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
return await TodoService.get_todos(offset, limit, session)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{todo_id}", response_model=TodoRead, dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
|
||||
todo = await TodoService.get_todo_by_id(todo_id, session)
|
||||
if not todo:
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
return todo
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{todo_id}", response_model=TodoRead, dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
async def update_todo(
|
||||
todo_id: int, todo_update: TodoUpdate, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
todo = await TodoService.update_todo(todo_id, todo_update, session)
|
||||
return todo
|
||||
|
||||
|
||||
@router.delete("/{todo_id}", dependencies=[Depends(get_current_user)])
|
||||
async def delete_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
|
||||
is_deleted = await TodoService.delete_todo(todo_id, session)
|
||||
if not is_deleted:
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,125 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TypedDict, Literal, Optional, cast
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
from typing import Required, NotRequired
|
||||
else:
|
||||
import tomli as tomllib
|
||||
from typing_extensions import Required, NotRequired
|
||||
|
||||
# 设置 env 为 development 或 production
|
||||
|
||||
|
||||
class DefaultApp(TypedDict):
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
debug: bool
|
||||
|
||||
|
||||
class Default(TypedDict):
|
||||
app: DefaultApp
|
||||
env: Literal["production", "development"]
|
||||
|
||||
|
||||
class SqliteConnectionArgs(TypedDict, total=False):
|
||||
timeout: int
|
||||
check_same_thread: bool
|
||||
|
||||
|
||||
class DatabaseSqlite(TypedDict, total=False):
|
||||
echo: bool
|
||||
pool_size: int
|
||||
max_overflow: int
|
||||
pool_timeout: int
|
||||
pool_recycle: int
|
||||
pool_pre_ping: bool
|
||||
connect_args: SqliteConnectionArgs
|
||||
|
||||
|
||||
class Database(TypedDict, total=False):
|
||||
type: Required[Literal["sqlite", "mysql", "postgresql"]]
|
||||
url: Required[str]
|
||||
generate_schemas: bool
|
||||
sqlite: DatabaseSqlite
|
||||
mysql: dict
|
||||
postgresql: dict
|
||||
|
||||
|
||||
class Security(TypedDict):
|
||||
secret_key: str
|
||||
algorithm: Literal["HS256"]
|
||||
access_token_expire_minutes: int
|
||||
jwt_cache_db_path: str
|
||||
refresh_token_expire_days: int
|
||||
max_sessions_per_user: int
|
||||
|
||||
|
||||
class Server(TypedDict, total=False):
|
||||
host: Required[str]
|
||||
port: Required[int]
|
||||
reload: bool
|
||||
workers: int
|
||||
|
||||
|
||||
class Logging(TypedDict):
|
||||
level: str
|
||||
file: str
|
||||
format: str
|
||||
max_size: int
|
||||
backup_count: int
|
||||
|
||||
|
||||
class Cors(TypedDict, total=False):
|
||||
allow_origins: list[str]
|
||||
allow_credentials: bool
|
||||
allow_methods: list[str]
|
||||
allow_headers: list[str]
|
||||
|
||||
|
||||
class ModuleSystem(TypedDict, total=False):
|
||||
type: str
|
||||
path: list[str]
|
||||
|
||||
|
||||
class FileSystem(TypedDict, total=False):
|
||||
type: str
|
||||
path: list[str]
|
||||
|
||||
|
||||
class Env(TypedDict, total=False):
|
||||
debug: Required[bool]
|
||||
server: Required[Server]
|
||||
logging: Required[Logging]
|
||||
cors: Cors
|
||||
module_system: ModuleSystem
|
||||
file_system: FileSystem
|
||||
|
||||
|
||||
class Config(TypedDict):
|
||||
default: Default
|
||||
database: Database
|
||||
security: Security
|
||||
development: Env
|
||||
production: Env
|
||||
|
||||
|
||||
def load_config(config_path: Path = Path("config.toml")) -> Config:
|
||||
"""从 TOML 文件加载配置"""
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_path}")
|
||||
with open(config_path, "rb") as f:
|
||||
return cast(Config, tomllib.load(f))
|
||||
|
||||
|
||||
_config: Optional[Config] = None
|
||||
|
||||
|
||||
def get_config(config_path: str = "config.toml") -> Config:
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = load_config(Path(config_path))
|
||||
return _config
|
||||
@@ -0,0 +1,11 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
# 获取数据库会话的依赖
|
||||
async def get_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
|
||||
async_session_maker = request.app.state.async_session_maker
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,459 @@
|
||||
import jwt
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any, List, TypedDict
|
||||
from sqlitedict import SqliteDict
|
||||
from pydantic import BaseModel
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
# from pydantic import BaseModel
|
||||
import hashlib
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from .config import get_config
|
||||
|
||||
|
||||
@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.config = get_config()
|
||||
|
||||
if db_path is None:
|
||||
db_path = self.config["security"]["jwt_cache_db_path"]
|
||||
|
||||
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.config["security"]["secret_key"]
|
||||
self.algorithm = self.config["security"]["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.config["security"]["access_token_expire_minutes"]
|
||||
expires_delta = timedelta(minutes=expire_minutes)
|
||||
else:
|
||||
expire_days = self.config["security"]["refresh_token_expire_days"]
|
||||
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 = self.config["security"]["max_sessions_per_user"]
|
||||
|
||||
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 类型
|
||||
print(f"payload: {payload}")
|
||||
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] = []
|
||||
@@ -0,0 +1,64 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from .config import Logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
|
||||
class ExcludeWatchFilesFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
# 如果日志记录来自 watchfiles 模块,则不记录(返回 False)
|
||||
return not record.name.startswith("watchfiles")
|
||||
|
||||
|
||||
def setup_logging(log_config: Logging):
|
||||
log_level = getattr(logging, log_config["level"], logging.INFO)
|
||||
log_format = log_config["format"]
|
||||
# 创建文件夹
|
||||
log_file_path = Path(log_config["file"])
|
||||
log_file_dir = log_file_path.parent
|
||||
if log_file_dir != Path("."):
|
||||
log_file_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置特定日志级别的过滤
|
||||
logging.getLogger("watchfiles").setLevel(logging.WARNING)
|
||||
|
||||
# 配置根日志记录器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(log_level)
|
||||
|
||||
# 创建格式化器
|
||||
formatter = logging.Formatter(log_format)
|
||||
|
||||
# 创建轮转文件处理器 - 限制文件大小为 10MB,保留 5 个备份文件
|
||||
max_bytes = log_config.get("max_size", 10 * 1024 * 1024) # 默认 10MB
|
||||
backup_count = log_config.get("backup_count", 5) # 默认保留 5 个备份
|
||||
|
||||
file_handler = RotatingFileHandler(
|
||||
log_config["file"],
|
||||
encoding="utf-8",
|
||||
maxBytes=max_bytes,
|
||||
backupCount=backup_count,
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.addFilter(ExcludeWatchFilesFilter())
|
||||
|
||||
# 创建控制台处理器并添加过滤器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
console_handler.addFilter(ExcludeWatchFilesFilter())
|
||||
|
||||
# 添加处理器到根日志记录器
|
||||
root_logger.addHandler(file_handler)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
print(
|
||||
f"日志系统已配置 - 级别: {logging.getLevelName(log_level)}, 文件: {log_config['file']} "
|
||||
f"(最大:{max_bytes / 1024 / 1024:.1f}MB, 备份:{backup_count})"
|
||||
)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
logger = logging.getLogger(name)
|
||||
# 为每个logger添加过滤器
|
||||
logger.addFilter(ExcludeWatchFilesFilter())
|
||||
return logger
|
||||
@@ -0,0 +1,14 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""密码哈希"""
|
||||
return pwd_context.hash(password)
|
||||
@@ -0,0 +1,28 @@
|
||||
from typing import Optional
|
||||
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
|
||||
# 定义模型
|
||||
class TodoBase(SQLModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
completed: bool = False
|
||||
|
||||
|
||||
class Todo(TodoBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
|
||||
|
||||
class TodoCreate(TodoBase):
|
||||
pass
|
||||
|
||||
|
||||
class TodoRead(TodoBase):
|
||||
id: int
|
||||
|
||||
|
||||
class TodoUpdate(SQLModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
completed: Optional[bool] = None
|
||||
@@ -0,0 +1,36 @@
|
||||
from typing import Optional
|
||||
from sqlmodel import SQLModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
email: str = Field(unique=True, index=True)
|
||||
username: Optional[str] = Field(default=None, max_length=50)
|
||||
full_name: Optional[str] = Field(default=None, max_length=100)
|
||||
is_active: bool = Field(default=True)
|
||||
is_superuser: bool = Field(default=False)
|
||||
created_at: Optional[str] = Field(default=None) # 可自动填充,此处略
|
||||
|
||||
|
||||
class User(UserBase, table=True):
|
||||
"""用户模型"""
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
hashed_password: str
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""用户创建模型"""
|
||||
|
||||
password: str
|
||||
|
||||
|
||||
class UserRead(UserBase):
|
||||
"""用户读取模型"""
|
||||
|
||||
id: int
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
认证服务 - 处理用户认证和 Token 管理
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import Request
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..model.user import User
|
||||
from .user import UserService
|
||||
from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData
|
||||
from ..core.config import get_config
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""认证服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.jwt_cache = get_jwt_cache()
|
||||
self.settings = get_config()
|
||||
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
async def authenticate_user(
|
||||
self, email: str, password: str, session: AsyncSession
|
||||
) -> Optional[User]:
|
||||
"""验证用户凭据"""
|
||||
user = await UserService.get_user_by_email(email, session)
|
||||
if not user:
|
||||
return None
|
||||
if not self.pwd_context.verify(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["security"]["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,
|
||||
session: AsyncSession,
|
||||
request: Optional[Request] = None,
|
||||
) -> Optional[str]:
|
||||
"""使用刷新令牌获取新的访问令牌"""
|
||||
validation = self.jwt_cache.validate_token(refresh_token, token_type="access")
|
||||
if not validation:
|
||||
return None
|
||||
|
||||
user_id = validation["user_id"]
|
||||
|
||||
# 获取用户
|
||||
user = await UserService.get_user_by_id(int(user_id), session)
|
||||
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
|
||||
@@ -0,0 +1,65 @@
|
||||
from typing import Sequence, Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from ..model.todo import TodoCreate, Todo, TodoUpdate
|
||||
|
||||
|
||||
class TodoService:
|
||||
@staticmethod
|
||||
async def create_todo(todo: TodoCreate, session: AsyncSession) -> Todo:
|
||||
db_todo = Todo.model_validate(todo)
|
||||
session.add(db_todo)
|
||||
await session.commit()
|
||||
await session.refresh(db_todo)
|
||||
return db_todo
|
||||
|
||||
@staticmethod
|
||||
async def get_todos(
|
||||
offset: int,
|
||||
limit: int,
|
||||
session: AsyncSession,
|
||||
) -> Sequence[Todo]:
|
||||
result = await session.execute(select(Todo).offset(offset).limit(limit))
|
||||
todos = result.scalars().all()
|
||||
return todos
|
||||
|
||||
@staticmethod
|
||||
async def get_todo_by_id(
|
||||
todo_id: int,
|
||||
session: AsyncSession,
|
||||
) -> Optional[Todo]:
|
||||
todo = await session.get(Todo, todo_id)
|
||||
return todo
|
||||
|
||||
@staticmethod
|
||||
async def update_todo(
|
||||
todo_id: int,
|
||||
todo_update: TodoUpdate,
|
||||
session: AsyncSession,
|
||||
) -> Optional[Todo]:
|
||||
todo = await session.get(Todo, todo_id)
|
||||
if not todo:
|
||||
return None
|
||||
update_data = todo_update.model_dump(exclude_unset=True)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(todo, key, value)
|
||||
|
||||
session.add(todo)
|
||||
await session.commit()
|
||||
await session.refresh(todo)
|
||||
return todo
|
||||
|
||||
@staticmethod
|
||||
async def delete_todo(
|
||||
todo_id: int,
|
||||
session: AsyncSession,
|
||||
) -> bool:
|
||||
todo = await session.get(Todo, todo_id)
|
||||
if not todo:
|
||||
return False
|
||||
await session.delete(todo)
|
||||
await session.commit()
|
||||
return True
|
||||
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
|
||||
from ..model.user import User
|
||||
from ..core.security import get_password_hash
|
||||
|
||||
|
||||
class UserService:
|
||||
@staticmethod
|
||||
async def get_user_by_email(email: str, session: AsyncSession) -> Optional[User]:
|
||||
result = await session.execute(select(User).where(User.email == email)) # type: ignore
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_id(user_id: int, session: AsyncSession) -> Optional[User]:
|
||||
return await session.get(User, user_id)
|
||||
|
||||
@staticmethod
|
||||
async def create_user(
|
||||
session: AsyncSession, email: str, password: str, **kwargs
|
||||
) -> User:
|
||||
hashed = get_password_hash(password)
|
||||
user = User(email=email, hashed_password=hashed, **kwargs)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
@@ -0,0 +1,253 @@
|
||||
from module_bank import PythonToSQLite
|
||||
from module_bank.encryption import Encryption
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
|
||||
def create_admin():
|
||||
try:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"../src/chongming/scripts/create_admin.py",
|
||||
]
|
||||
print(f"正在运行 create_admin.py: {' '.join(cmd)}")
|
||||
|
||||
# 创建包含 UTF-8 编码的环境变量
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
env["PYTHONUTF8"] = "1"
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd="./build",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8", # 明确指定编码
|
||||
errors="replace", # 替换无法解码的字符
|
||||
env=env, # 传递修改后的环境变量
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("创建管理员用户成功")
|
||||
print(result.stdout)
|
||||
else:
|
||||
print("创建管理员用户失败")
|
||||
# 安全地打印错误信息,避免编码问题
|
||||
try:
|
||||
print("错误信息:", result.stderr)
|
||||
except UnicodeEncodeError:
|
||||
print(
|
||||
"错误信息:", result.stderr.encode("ascii", "ignore").decode("ascii")
|
||||
)
|
||||
except Exception as e:
|
||||
print("创建管理员用户失败")
|
||||
print(f"异常: {e}")
|
||||
|
||||
|
||||
def run_pyarmor_obfuscate():
|
||||
# pyarmor obfuscate --output=obfuscated_script.py original_script.py
|
||||
obfuscate_files = [
|
||||
("main.py", "./"),
|
||||
("server.py", "./"),
|
||||
("utils/launch.py", "./utils"),
|
||||
("utils/config.py", "./utils"),
|
||||
]
|
||||
for file, output in obfuscate_files:
|
||||
cmd = [
|
||||
"pyarmor",
|
||||
"gen",
|
||||
f"--output={output}",
|
||||
file,
|
||||
]
|
||||
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"打包文件: {file} 失败")
|
||||
print(result.stderr)
|
||||
return
|
||||
else:
|
||||
print(result.stdout)
|
||||
print(f"打包文件: {file}")
|
||||
|
||||
|
||||
def run_pyinstaller():
|
||||
"""运行 PyInstaller 打包成可执行文件"""
|
||||
try:
|
||||
# 构建 PyInstaller 命令
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"PyInstaller",
|
||||
"--onefile",
|
||||
"--name",
|
||||
"chongming",
|
||||
"--distpath",
|
||||
".",
|
||||
"--workpath",
|
||||
"./build_temp",
|
||||
"--specpath",
|
||||
"./specs",
|
||||
"--clean",
|
||||
# 隐藏导入
|
||||
"--hidden-import",
|
||||
"utils",
|
||||
"--hidden-import",
|
||||
"utils.launch",
|
||||
"--hidden-import",
|
||||
"utils.config",
|
||||
"--hidden-import",
|
||||
"server",
|
||||
"--hidden-import",
|
||||
"module_bank",
|
||||
"--hidden-import",
|
||||
"fastapi",
|
||||
"--hidden-import",
|
||||
"fastapi.middleware.cors",
|
||||
"--hidden-import",
|
||||
"uvicorn",
|
||||
"--hidden-import",
|
||||
"sqlmodel",
|
||||
"--hidden-import",
|
||||
"aiosqlite",
|
||||
"--hidden-import",
|
||||
"pyjwt",
|
||||
"--hidden-import",
|
||||
"sqlitedict",
|
||||
"--hidden-import",
|
||||
"passlib",
|
||||
"--hidden-import",
|
||||
"passlib.context",
|
||||
"--hidden-import",
|
||||
"passlib.handlers.bcrypt",
|
||||
"--hidden-import",
|
||||
"jwt",
|
||||
# 其他选项
|
||||
"--icon",
|
||||
"../../public/chongming.ico",
|
||||
"main.py",
|
||||
]
|
||||
|
||||
print(f"正在运行 PyInstaller: {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("PyInstaller 打包成功!")
|
||||
print(result.stdout)
|
||||
else:
|
||||
print("PyInstaller 打包失败!")
|
||||
print("错误信息:", result.stderr)
|
||||
|
||||
except FileNotFoundError:
|
||||
print("错误:未找到 PyInstaller,请先安装 PyInstaller:pip install pyinstaller")
|
||||
except Exception as e:
|
||||
print(f"PyInstaller 执行出错: {e}")
|
||||
|
||||
|
||||
async def init_database():
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from ..app.core.config import get_config
|
||||
from sqlmodel import SQLModel, text
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
database = get_config()["database"]
|
||||
database_type = database["type"]
|
||||
database_config = database.get(database_type, None)
|
||||
if database_config is None:
|
||||
raise ValueError("配置不存在")
|
||||
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///./build/database.db", **database_config
|
||||
)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
if database_type == "sqlite":
|
||||
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
||||
await conn.execute(text("PRAGMA synchronous=NORMAL"))
|
||||
try:
|
||||
await conn.run_sync(
|
||||
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
|
||||
)
|
||||
except OperationalError as e:
|
||||
if "already exists" in str(e):
|
||||
raise
|
||||
|
||||
|
||||
async def main():
|
||||
# 打包模块
|
||||
key = Encryption.generate_key()
|
||||
resources_path = Path("resources")
|
||||
if not resources_path.exists():
|
||||
resources_path.mkdir()
|
||||
app_packer = PythonToSQLite("./resources/app.mbank", key)
|
||||
app_packer.pack_directory("src/chongming/app", "app")
|
||||
app_packer.verify_package_structure()
|
||||
app_packer.delete_source_code(None)
|
||||
|
||||
plugin_packer = PythonToSQLite("./resources/plugins.mbank", key)
|
||||
plugin_packer.pack_directory("src/plugins", "plugins")
|
||||
plugin_packer.verify_package_structure()
|
||||
plugin_packer.delete_source_code(None)
|
||||
|
||||
# 创建 build 目录
|
||||
build_path = Path("build")
|
||||
if build_path.exists() and build_path.is_dir():
|
||||
shutil.rmtree("build")
|
||||
|
||||
os.makedirs("build", exist_ok=True)
|
||||
|
||||
# 复制 utils 文件夹到 build 文件夹
|
||||
utils_path = Path("utils")
|
||||
if utils_path.exists():
|
||||
target_utils_path = Path("build") / "utils"
|
||||
shutil.copytree(utils_path, target_utils_path, dirs_exist_ok=True)
|
||||
print(f"已复制 utils 文件夹到 {target_utils_path}")
|
||||
|
||||
# 复制 resources 文件夹到 build 文件夹
|
||||
resources_path = Path("resources")
|
||||
if resources_path.exists():
|
||||
target_resources_path = Path("build") / "resources"
|
||||
shutil.copytree(resources_path, target_resources_path, dirs_exist_ok=True)
|
||||
print(f"已复制 resources 文件夹到 {target_resources_path}")
|
||||
|
||||
print("模块打包完成")
|
||||
|
||||
# 复制 main.py 到 build 文件夹
|
||||
shutil.copy("public/main.py", "build")
|
||||
shutil.copy("public/server.py", "build")
|
||||
# 复制 config.toml 到 build 文件夹
|
||||
shutil.copy("public/config.toml", "build")
|
||||
# 复制 utils 文件夹到 build 文件夹
|
||||
shutil.copytree("public/utils", "build/utils")
|
||||
|
||||
with open("./build/utils/launch.py", "r", encoding="utf-8") as f:
|
||||
context = f.read()
|
||||
context = context.replace("key = None", f"key = '{key}'")
|
||||
with open("./build/utils/launch.py", "w", encoding="utf-8") as f:
|
||||
f.write(context)
|
||||
|
||||
# 混淆
|
||||
run_pyarmor_obfuscate()
|
||||
|
||||
# 运行 PyInstaller
|
||||
run_pyinstaller()
|
||||
|
||||
# 清理临时文件
|
||||
shutil.rmtree(r"build/build_temp", ignore_errors=True)
|
||||
shutil.rmtree(r"build/specs", ignore_errors=True)
|
||||
shutil.rmtree(r"build/utils", ignore_errors=True)
|
||||
shutil.rmtree(r"build/pyarmor_runtime_000000", ignore_errors=True)
|
||||
os.remove(r"build/main.py")
|
||||
os.remove(r"build/server.py")
|
||||
# os.remove(r"build/config.toml")
|
||||
|
||||
print("构建完成")
|
||||
|
||||
# 初始化数据库
|
||||
await init_database()
|
||||
|
||||
|
||||
def build():
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
import shutil
|
||||
# 需要遍历的目录
|
||||
root_dir = "./"
|
||||
# 遍历目录
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
if "__pycache__" in dirnames:
|
||||
# 获取 __pycache__ 目录的全路径
|
||||
pycache_dir = os.path.join(dirpath, "__pycache__")
|
||||
# 删除目录
|
||||
shutil.rmtree(pycache_dir)
|
||||
print(f"Removed: {pycache_dir}")
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def generate_markdown_from_py_files(directory, output_file):
|
||||
with open(output_file, "w", encoding="utf-8") as md_file:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# 排除 venv 目录
|
||||
dirs[:] = [d for d in dirs if d != ".venv"]
|
||||
dirs[:] = [d for d in dirs if d != ".vscode"]
|
||||
# dirs[:] = [d for d in dirs if d != "scripts"]
|
||||
dirs[:] = [d for d in dirs if d != "build"]
|
||||
for file in files:
|
||||
if (
|
||||
file.endswith(".py")
|
||||
or file.endswith(".rs")
|
||||
or file.endswith(".toml")
|
||||
or file.endswith(".md")
|
||||
):
|
||||
file_path = os.path.join(root, file)
|
||||
if file == "output.md":
|
||||
continue
|
||||
md_file.write(f"`{file_path}`\n")
|
||||
md_file.write("```python\n")
|
||||
with open(file_path, "r", encoding="utf-8") as py_file:
|
||||
md_file.write(py_file.read())
|
||||
md_file.write("\n```\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 指定目录和输出文件名
|
||||
target_directory = sys.argv[1] # 替换为你的目标目录
|
||||
output_markdown_file = "output.md" # 输出的 Markdown 文件名
|
||||
|
||||
generate_markdown_from_py_files(target_directory, output_markdown_file)
|
||||
print(f"Markdown 文件已生成:{output_markdown_file}")
|
||||
@@ -0,0 +1,2 @@
|
||||
def hello():
|
||||
print("Hello World")
|
||||
Reference in New Issue
Block a user