modify: 集成文件上传

This commit is contained in:
2026-04-20 17:11:31 +08:00
parent 69d1c5fc67
commit 55463a9d07
25 changed files with 736 additions and 44 deletions
+4 -1
View File
@@ -4,6 +4,8 @@ app.version = "chongming"
app.description = "基于 FastAPI 的 chongming 打包脚手架"
app.debug = false
env = "development"
prefix = "/api/v1"
upload_path = "uploads"
[database]
type = "sqlite"
@@ -12,6 +14,7 @@ generate_schemas = true # 生产环境使用迁移工具
[database.sqlite]
echo = true
echo_pool = true
pool_size = 20
max_overflow = 10
pool_timeout = 30
@@ -82,4 +85,4 @@ path = ["resources/app.mbank", "resources/plugins.mbank"]
[production.file_system]
type = "svfs"
path = "resources/application.vfs"
path = "static.svfs"
+5 -2
View File
@@ -4,6 +4,8 @@ app.version = "0.1.0"
app.description = "基于 FastAPI 的 chongming 打包脚手架"
app.debug = false
env = "production"
prefix = "/api/v1"
upload_path = "uploads"
[database]
type = "sqlite"
@@ -11,7 +13,8 @@ url = "sqlite+aiosqlite:///./database.db"
generate_schemas = true # 生产环境使用迁移工具
[database.sqlite]
echo = true
echo = false
echo_pool = false
pool_size = 20
max_overflow = 10
pool_timeout = 30
@@ -82,4 +85,4 @@ path = ["resources/app.mbank", "resources/plugins.mbank"]
[production.file_system]
type = "svfs"
path = "resources/application.vfs"
path = "static.svfs"
+2
View File
@@ -27,6 +27,7 @@ def app_run(app_config: dict, default_config: dict):
server_config = app_config.get("server", {})
host = server_config.get("host", "0.0.0.0")
port = server_config.get("port", 8000)
timeout_keep_alive = server_config.get("timeout_keep_alive", 5)
# 打包环境强制禁用重载
is_frozen = getattr(sys, "frozen", False)
@@ -54,6 +55,7 @@ def app_run(app_config: dict, default_config: dict):
workers=workers,
log_level="debug" if app_config.get("debug", False) else "info",
access_log=True,
timeout_keep_alive=timeout_keep_alive,
)
+2
View File
@@ -9,6 +9,7 @@ description = "基于 modules-bank 的 fastapi 打包脚手架"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"aiofiles>=24.1.0",
"aiosqlite",
"apscheduler",
"bcrypt==4.0.1",
@@ -17,6 +18,7 @@ dependencies = [
"module-bank",
"passlib[bcrypt]",
"pyjwt",
"python-multipart>=0.0.20",
"sqlite-vfs",
"sqlmodel[aiosqlite]",
"tomli",
+21 -7
View File
@@ -1,9 +1,10 @@
import os
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlmodel import SQLModel, text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.exc import OperationalError
@@ -26,6 +27,7 @@ class TaskRequest(BaseModel):
config = get_config()
logger = get_logger("app")
try:
env = config["default"]["env"]
except KeyError: # pragma: no cover
@@ -70,6 +72,7 @@ async def lifespan(app: FastAPI):
if "already exists" in str(e):
raise
# 启动时:初始化任务服务
task_service = get_task_service_instance(async_session_maker)
app.state.task_service = task_service
await task_service.start(init_tasks_callback)
@@ -82,9 +85,8 @@ async def lifespan(app: FastAPI):
get_cache().close()
cors = config[env].get("cors", {})
# 创建 FastAPI 应用
cors = config[env].get("cors", {})
app = FastAPI(
title=config["default"]["app"]["name"],
version=config["default"]["app"]["version"],
@@ -94,9 +96,12 @@ app = FastAPI(
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.include_router(api_router, prefix=config["default"]["prefix"])
# 根路由
@@ -107,7 +112,7 @@ async def root():
"message": f"欢迎使用 {config['default']['app']['name']}",
"version": config["default"]["app"]["version"],
"docs": "/docs" if config[env]["debug"] else None,
"health": "/api/v1/health",
"health": f"{config['default']['prefix']}/health",
}
@@ -124,10 +129,19 @@ async def root():
# )
# return {"status": "success", "job_id": job.id}
# 挂载静态文件
config = get_config()
IMAGE_DIR = Path(f"./{config['default']['upload_path']}/images")
app.mount(
f"{config['default']['prefix']}/images",
StaticFiles(directory=f"./{config['default']['upload_path']}/images"),
name="images",
)
if os.path.exists("static.svfs"):
# 挂载前端静态文件
if vfs_db_path := config[env].get("file_system", {}).get("path", None):
logger.info("静态文件已存在,将使用静态文件服务")
static_files_handler = SVFSStaticFiles(directory="/")
static_files_handler = SVFSStaticFiles(directory="/", vfs_db_path=vfs_db_path)
app.mount("/static", static_files_handler, name="static")
+2 -1
View File
@@ -4,7 +4,7 @@ API 路由
from fastapi import APIRouter
from .routers import todo, auth
from .routers import todo, auth, upload
# 创建主路由
api_router = APIRouter()
@@ -12,6 +12,7 @@ api_router = APIRouter()
# 注册子路由
api_router.include_router(todo.router, prefix="/todos", tags=["todo"])
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(upload.router, prefix="/upload", tags=["upload"])
# 导出路由
+3 -1
View File
@@ -1,3 +1,5 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
@@ -113,7 +115,7 @@ 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), session)
user = await UserService.get_user_by_id(UUID(current_user.user_id), session)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
+35
View File
@@ -0,0 +1,35 @@
import os
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends
from pydantic import BaseModel
from ..deps import get_current_user
from ...service.uploads import UploadService
from ...core.config import get_config
config = get_config()
router = APIRouter(dependencies=[Depends(get_current_user)])
upload_image_service = UploadService(
config["default"]["upload_path"], 1024 * 1024, "images"
)
class UploadResponse(BaseModel):
url: str
filename: str
file_type: str
@router.post("/image", response_model=UploadResponse)
async def upload_image(file: UploadFile = File(...)):
if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="Invalid file type")
if file.filename is None:
raise HTTPException(status_code=400, detail="Invalid file name")
ext = os.path.splitext(file.filename)[1].lower()
if ext not in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
raise HTTPException(status_code=400, detail="Invalid file type")
url, filename = await upload_image_service.upload(file)
return UploadResponse(url=url, filename=filename, file_type=ext)
+3 -1
View File
@@ -23,6 +23,8 @@ class DefaultApp(TypedDict):
class Default(TypedDict):
app: DefaultApp
env: Literal["production", "development"]
prefix: str
upload_path: str
class SqliteConnectionArgs(TypedDict, total=False):
@@ -86,7 +88,7 @@ class ModuleSystem(TypedDict, total=False):
class FileSystem(TypedDict, total=False):
type: str
path: List[str]
path: str
class Env(TypedDict, total=False):
+277
View File
@@ -0,0 +1,277 @@
class PermissionConstant:
"""
权限常量类 - 按业务模块分组管理所有系统权限
该类定义了系统中各个业务模块的权限标识符,采用分层结构组织:
- 每个业务域作为一个内部类(如 Sales, Purchase, Inventory 等)
- 每个权限使用 "资源:操作" 格式定义
- 支持 CRUD 操作及业务特定操作(如 approve, export 等)
使用示例:
>>> Permission.Sales.ORDER_CREATE # 获取销售订单创建权限
'sales_order:create'
>>> Permission.get_all() # 获取所有权限列表
['sales_order:create', 'sales_order:read', ...]
"""
class Sales:
"""
销售域权限常量
包含销售相关的权限定义:
- 销售订单管理(ORDER_*)
- 销售出库管理(OUTBOUND_*)
- 客户管理(CUSTOMER_*)
权限格式: sales_order:create, customer:export 等
"""
ORDER_CREATE = "sales_order:create" # 创建销售订单
ORDER_READ = "sales_order:read" # 读取销售订单
ORDER_UPDATE = "sales_order:update" # 修改销售订单
ORDER_DELETE = "sales_order:delete" # 删除销售订单
OUTBOUND_CREATE = "sales_outbound:create" # 创建销售出库
OUTBOUND_READ = "sales_outbound:read" # 读取销售出库
OUTBOUND_UPDATE = "sales_outbound:update" # 修改销售出库
OUTBOUND_DELETE = "sales_outbound:delete" # 删除销售出库
OUTBOUND_APPROVE = "sales_outbound:approve" # 审批销售出库
CUSTOMER_CREATE = "customer:create" # 创建客户
CUSTOMER_READ = "customer:read" # 读取客户
CUSTOMER_UPDATE = "customer:update" # 修改客户
CUSTOMER_DELETE = "customer:delete" # 删除客户
CUSTOMER_EXPORT = "customer:export" # 导出客户
class Purchase:
"""
采购域权限常量
包含采购相关的权限定义:
- 采购订单管理(ORDER_*)
- 采购入库管理(INBOUND_*),包含审批和取消操作
- 供应商管理(SUPPLIER_*)
权限格式: purchase_order:create, supplier:update 等
"""
ORDER_CREATE = "purchase_order:create" # 创建采购订单
ORDER_READ = "purchase_order:read" # 读取采购订单
ORDER_UPDATE = "purchase_order:update" # 更新采购订单
ORDER_DELETE = "purchase_order:delete" # 删除采购订单
INBOUND_CREATE = "purchase_inbound:create" # 创建采购入库
INBOUND_READ = "purchase_inbound:read" # 读取采购入库
INBOUND_UPDATE = "purchase_inbound:update" # 更新采购入库
INBOUND_DELETE = "purchase_inbound:delete" # 删除采购入库
INBOUND_APPROVE = "purchase_inbound:approve" # 审批采购入库
INBOUND_CANCEL = "purchase_inbound:cancel" # 取消采购入库
SUPPLIER_CREATE = "supplier:create" # 创建供应商
SUPPLIER_READ = "supplier:read" # 读取供应商
SUPPLIER_UPDATE = "supplier:update" # 更新供应商
SUPPLIER_DELETE = "supplier:delete" # 删除供应商
class Inventory:
"""
库存域权限常量
包含库存管理相关的权限定义:
- 库存管理(STOCK_*)
- 仓库管理(WAREHOUSE_*)
- 其他出入库单据(OTHER_INBOUND_*, OTHER_OUTBOUND_*)
- 调拨单管理(TRANSFER_*)
- 盘点单管理(STOCK_CHECK_*)
特点: 大部分单据支持审批流程(APPROVE 权限)
权限格式: stock:read, transfer:approve 等
"""
STOCK_CREATE = "stock:create" # 创建库存
STOCK_READ = "stock:read" # 读取库存
STOCK_UPDATE = "stock:update" # 更新库存
STOCK_DELETE = "stock:delete" # 删除库存
WAREHOUSE_CREATE = "warehouse:create" # 创建仓库
WAREHOUSE_READ = "warehouse:read" # 读取仓库
WAREHOUSE_UPDATE = "warehouse:update" # 更新仓库
WAREHOUSE_DELETE = "warehouse:delete" # 删除仓库
# 库存单据
OTHER_INBOUND_CREATE = "other_inbound:create" # 创建其他入库
OTHER_INBOUND_READ = "other_inbound:read" # 读取其他入库
OTHER_INBOUND_UPDATE = "other_inbound:update" # 更新其他入库
OTHER_INBOUND_DELETE = "other_inbound:delete" # 删除其他入库
OTHER_INBOUND_APPROVE = "other_inbound:approve" # 审核其他入库
OTHER_OUTBOUND_CREATE = "other_outbound:create" # 创建其他出库
OTHER_OUTBOUND_READ = "other_outbound:read" # 读取其他出库
OTHER_OUTBOUND_UPDATE = "other_outbound:update" # 更新其他出库
OTHER_OUTBOUND_DELETE = "other_outbound:delete" # 删除其他出库
OTHER_OUTBOUND_APPROVE = "other_outbound:approve" # 审核其他出库
TRANSFER_CREATE = "transfer:create" # 创建调拨
TRANSFER_READ = "transfer:read" # 读取调拨
TRANSFER_UPDATE = "transfer:update" # 更新调拨
TRANSFER_DELETE = "transfer:delete" # 删除调拨
TRANSFER_APPROVE = "transfer:approve" # 审核调拨
STOCK_CHECK_CREATE = "stock_check:create" # 创建盘点
STOCK_CHECK_READ = "stock_check:read" # 读取盘点
STOCK_CHECK_UPDATE = "stock_check:update" # 更新盘点
STOCK_CHECK_DELETE = "stock_check:delete" # 删除盘点
STOCK_CHECK_APPROVE = "stock_check:approve" # 审核盘点
class Product:
"""
产品主数据权限常量
包含产品和基础数据管理的权限定义:
- 产品管理(PRODUCT_*),支持导入导出
- 产品分类管理(CATEGORY_*)
- 计量单位管理(UNIT_*)
权限格式: product:import, product_category:read 等
"""
PRODUCT_CREATE = "product:create" # 创建产品
PRODUCT_READ = "product:read" # 读取产品
PRODUCT_UPDATE = "product:update" # 更新产品
PRODUCT_DELETE = "product:delete" # 删除产品
PRODUCT_IMPORT = "product:import" # 导入产品
PRODUCT_EXPORT = "product:export" # 导出产品
CATEGORY_CREATE = "product_category:create" # 创建产品分类
CATEGORY_READ = "product_category:read" # 读取产品分类
CATEGORY_UPDATE = "product_category:update" # 更新产品分类
CATEGORY_DELETE = "product_category:delete" # 删除产品分类
UNIT_CREATE = "unit:create" # 创建单位
UNIT_READ = "unit:read" # 读取单位
UNIT_UPDATE = "unit:update" # 更新单位
UNIT_DELETE = "unit:delete" # 删除单位
class Finance:
"""
财务域权限常量
包含财务管理相关的权限定义:
- 应收应付查询(RECEIVABLE_READ, PAYABLE_READ)
- 付款管理(PAYMENT_*),包含审批流程
- 发票管理(INVOICE_*)
注意: 财务域以只读权限为主,写操作需要审批
权限格式: payment:approve, invoice:create 等
"""
RECEIVABLE_READ = "receivable:read" # 读取应收应付
PAYABLE_READ = "payable:read" # 读取应付
PAYMENT_CREATE = "payment:create" # 创建付款
PAYMENT_READ = "payment:read" # 读取付款
PAYMENT_APPROVE = "payment:approve" # 审批付款
INVOICE_CREATE = "invoice:create" # 创建发票
INVOICE_READ = "invoice:read" # 读取发票
class Report:
"""
报表与分析权限常量
包含各类报表查看权限:
- 销售报表(SALES_VIEW)
- 采购报表(PURCHASE_VIEW)
- 库存报表(STOCK_VIEW)
- 财务报表(FINANCE_VIEW)
- 仪表盘(DASHBOARD_VIEW)
特点: 所有权限均为只读查看权限
权限格式: report:sales:view, dashboard:view 等
"""
SALES_VIEW = "report_sales:view" # 销售报表
PURCHASE_VIEW = "report_purchase:view" # 采购报表
STOCK_VIEW = "report_stock:view" # 库存报表
FINANCE_VIEW = "report_finance:view" # 财务报表
DASHBOARD_VIEW = "dashboard:view" # 仪表盘
class System:
"""
系统管理权限常量
包含系统级管理功能的权限定义:
- 人员管理(PERSONNEL_*)
- 角色管理(ROLE_*),包含角色分配权限
- 系统配置(CONFIG_*)
- 审计日志(AUDIT_LOG_READ)
- 数据备份恢复(BACKUP_*)
注意: 这些是最高权限级别的操作,应谨慎分配
权限格式: role:assign, backup:restore 等
"""
PERSONNEL_CREATE = "personnel:create" # 创建人员
PERSONNEL_READ = "personnel:read" # 读取人员
PERSONNEL_UPDATE = "personnel:update" # 更新人员
PERSONNEL_DELETE = "personnel:delete" # 删除人员
ROLE_CREATE = "role:create" # 创建角色
ROLE_READ = "role:read" # 读取角色
ROLE_UPDATE = "role:update" # 更新角色
ROLE_DELETE = "role:delete" # 删除角色
ASSIGN_PERMISSION = "permission:assign" # 分配权限
ASSIGN_ROLE = "role:assign" # 分配角色
CONFIG_READ = "system_config:read" # 读取系统配置
CONFIG_UPDATE = "system_config:update" # 更新系统配置
AUDIT_LOG_READ = "audit_log:read" # 读取审计日志
BACKUP_CREATE = "backup:create" # 创建备份
BACKUP_RESTORE = "backup:restore" # 恢复备份
@classmethod
def get_all(cls):
"""
获取系统中定义的所有权限字符串列表
该方法通过反射机制自动收集所有内部类中定义的权限常量,
用于数据库同步、权限初始化等场景。
工作流程:
1. 遍历 Permission 类的所有属性
2. 跳过私有属性和非类属性
3. 对每个内部类,遍历其所有字符串类型的权限常量
4. 过滤出符合 "resource:action" 格式的权限标识
Returns:
list[str]: 所有权限字符串的列表,例如:
[
'sales_order:create',
'sales_order:read',
'customer:export',
...
]
使用示例:
>>> all_perms = Permission.get_all()
>>> len(all_perms) # 获取权限总数
85
>>> 'sales_order:create' in all_perms # 检查权限是否存在
True
Note:
- 该方法会自动排除以下划线开头的私有属性
- 只收集包含冒号的字符串值,确保是有效的权限标识
- 返回的列表顺序取决于 dir() 函数的返回顺序
"""
perms = []
for attr_name in dir(cls):
attr = getattr(cls, attr_name)
if attr_name.startswith("_") or not hasattr(attr, "__dict__"):
continue
# 遍历内部类的属性
for sub_attr in dir(attr):
if sub_attr.startswith("_"):
continue
val = getattr(attr, sub_attr)
if isinstance(val, str) and ":" in val:
perms.append(val)
return perms
+1
View File
@@ -21,6 +21,7 @@ class ExcludeWatchFilesFilter(logging.Filter):
"apscheduler",
"sqlalchemy",
"sqlitedict",
"sqlalchemy.engine.Engine",
)
# 重写过滤方法
View File
View File
+2 -1
View File
@@ -1,5 +1,5 @@
from .user import User
from .role import Role
from .role import Role, RoleUpdate
from .permission import Permission
from .relationship_table import UserRole, RolePermission
from .todo import Todo
@@ -8,6 +8,7 @@ from .todo import Todo
__all__ = [
"User",
"Role",
"RoleUpdate",
"Permission",
"UserRole",
"RolePermission",
+6
View File
@@ -1,6 +1,7 @@
from typing import Optional, List, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from pydantic import BaseModel
from .relationship_table import UserRole, RolePermission
@@ -19,3 +20,8 @@ class Role(SQLModel, table=True):
permissions: List["Permission"] = Relationship(
back_populates="roles", link_model=RolePermission
)
class RoleUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
+1 -1
View File
@@ -112,7 +112,7 @@ class AuthService:
user_id = validation["user_id"]
# 获取用户
user = await UserService.get_user_by_id(int(user_id), session)
user = await UserService.get_user_by_id(user_id, session)
if not user:
return None
+112
View File
@@ -0,0 +1,112 @@
from typing import Optional
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from ..model import Permission, RolePermission, Role, User
from .user import UserService
from .role import RoleService
class PermissionService:
@classmethod
async def create_permission(
cls,
session: AsyncSession,
name: str,
resource: str,
action: str,
description: Optional[str] = None,
) -> Permission:
permission = Permission(
name=name, resource=resource, action=action, description=description
)
session.add(permission)
await session.commit()
await session.refresh(permission)
return permission
@classmethod
async def get_permission_by_id(
cls, session: AsyncSession, permission_id: int
) -> Optional[Permission]:
return await session.get(Permission, permission_id)
@classmethod
async def get_permission_by_name(
cls, session: AsyncSession, name: str
) -> Optional[Permission]:
result = await session.execute(
select(Permission).where(Permission.name == name)
)
return result.scalar_one_or_none()
@classmethod
async def assign_permission_to_role(
cls, session: AsyncSession, role_id: int, permission_id: int
) -> RolePermission:
role_permission = RolePermission(role_id=role_id, permission_id=permission_id)
session.add(role_permission)
await session.commit()
await session.refresh(role_permission)
return role_permission
@classmethod
async def assign_permission_to_user(
cls,
session: AsyncSession,
user_id: UUID,
permission_id: int,
) -> User:
permission = await cls.get_permission_by_id(session, permission_id)
if not permission:
raise HTTPException(status_code=404, detail="Permission not found")
user = await UserService.get_user_by_id(user_id, session)
if user is None:
raise HTTPException(status_code=404, detail="用户不存在")
role = await RoleService.get_role_by_name(session, str(user_id))
if role is None:
role = await RoleService.create_role(
session, str(user_id), is_only_user=True
)
if role.id is None:
raise HTTPException(status_code=500, detail="角色ID无效")
if permission.id is None:
raise HTTPException(status_code=500, detail="权限ID无效")
await cls.assign_permission_to_role(session, role.id, permission.id)
user.roles.append(role)
await session.commit()
await session.refresh(user)
return user
@classmethod
async def unbind_permission_from_role(
cls, session: AsyncSession, role_id: int, permission_id: int
) -> Role:
role = await RoleService.get_role_by_id(session, role_id)
if role is None:
raise HTTPException(status_code=404, detail="Role not found")
role.permissions = [p for p in role.permissions if p.id != permission_id]
await session.commit()
await session.refresh(role)
return role
@classmethod
async def unbind_permission_from_user(
cls, session: AsyncSession, user_id: UUID, permission_id: int
):
user = await UserService.get_user_by_id(user_id, session)
if not user:
raise HTTPException(status_code=404, detail="User not found")
role = await RoleService.get_role_by_name(session, str(user.id))
if role is None:
raise HTTPException(status_code=404, detail="Role not found")
role.permissions = [
permission
for permission in role.permissions
if permission.id != permission_id
]
await session.commit()
await session.refresh(role)
+97
View File
@@ -0,0 +1,97 @@
from typing import Optional
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from ..model import Role, RoleUpdate, UserRole
from ..service.user import UserService
class RoleService:
@staticmethod
async def create_role(
session: AsyncSession,
name: str,
description: Optional[str] = None,
is_only_user: bool = False,
) -> Role:
role = Role(name=name, description=description, is_only_user=is_only_user)
session.add(role)
await session.commit()
await session.refresh(role)
return role
@staticmethod
async def assign_role_to_user(
session: AsyncSession,
user_id: UUID,
role_id: int,
):
user_role = UserRole(user_id=user_id, role_id=role_id)
session.add(user_role)
await session.commit()
await session.refresh(user_role)
@classmethod
async def get_role_by_name(cls, session: AsyncSession, name: str) -> Optional[Role]:
result = await session.execute(select(Role).where(Role.name == name))
return result.scalar_one_or_none()
@classmethod
async def get_role_by_id(
cls, session: AsyncSession, role_id: int
) -> Optional[Role]:
result = await session.get(Role, role_id)
return result
@staticmethod
async def unbind_role_from_user(
session: AsyncSession,
user_id: UUID,
role_id: int,
):
user = await UserService.get_user_by_id(user_id, session)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
user.roles = [role for role in user.roles if role.id != role_id]
await session.commit()
await session.refresh(user)
return user
@staticmethod
async def delete_role(
role_id: int,
session: AsyncSession,
) -> bool:
result = await session.execute(
select(UserRole).where(UserRole.role_id == role_id)
)
if result.scalars().all():
raise HTTPException(status_code=400, detail="Role is not empty")
role = await RoleService.get_role_by_id(session, role_id)
if role is None:
raise HTTPException(status_code=404, detail="Role not found")
await session.delete(role)
await session.commit()
return True
@classmethod
async def update_role(
cls,
role_id: int,
role_update: RoleUpdate,
session: AsyncSession,
) -> Optional[Role]:
role = await cls.get_role_by_id(session, role_id)
if role is None:
raise HTTPException(status_code=404, detail="Role not found")
role.name = role_update.name if role_update.name else role.name
role.description = (
role_update.description if role_update.description else role.description
)
await session.commit()
await session.refresh(role)
return role
+43
View File
@@ -0,0 +1,43 @@
import os
import time
import uuid
from pathlib import Path
from fastapi.exceptions import HTTPException
from fastapi import UploadFile
import aiofiles
from ..core.config import get_config
config = get_config()
class UploadService:
def __init__(self, upload_dir: str, chunk_size: int, file_type: str):
self.upload_dir = upload_dir
self.file_type = file_type
self.chunk_size = chunk_size
UPLOAD_DIR = Path(f"{self.upload_dir}/{self.file_type}")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
async def upload(self, file_data: UploadFile):
if file_data.filename is None:
raise HTTPException(status_code=400, detail="Invalid file name")
ext = os.path.splitext(file_data.filename)[1].lower()
new_filename = f"{uuid.uuid4().hex}{ext}"
file_path = os.path.join(self.upload_dir, self.file_type, new_filename)
try:
async with aiofiles.open(file_path, "wb") as out_file:
while chunk := await file_data.read(self.chunk_size):
await out_file.write(chunk)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
await file_data.close()
return (
f"{config['default']['prefix']}/{self.file_type}/{new_filename}".replace(
"\\", "/"
),
new_filename,
)
+2 -1
View File
@@ -1,4 +1,5 @@
from typing import Optional
from uuid import UUID
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +15,7 @@ class UserService:
return result.scalar_one_or_none()
@staticmethod
async def get_user_by_id(user_id: int, session: AsyncSession) -> Optional[User]:
async def get_user_by_id(user_id: UUID, session: AsyncSession) -> Optional[User]:
return await session.get(User, user_id)
@staticmethod
+8 -4
View File
@@ -1,12 +1,16 @@
from typing import Callable, Optional
from ..core.scheduler import TaskService
from .execute_background import dev_init_admin
from ..core.config import get_config
from .dev_init_db import init_db
config = get_config()
async def init_tasks_callback(task_service: TaskService):
if config["default"]["env"] == "development":
await task_service.add_date_job(
dev_init_admin,
init_db,
run_date=None,
args=[],
job_id="task_name",
+39
View File
@@ -0,0 +1,39 @@
from ..core.scheduler import logger
from ..core.scheduler import get_task_service_instance
from ..core.constant import PermissionConstant
from ..service.user import UserService
from ..service.permission import PermissionService
async def dev_init_admin(session):
admin_user = await UserService.get_user_by_email("admin", session)
if admin_user: # type: ignore
logger.info("管理员用户已存在") # type: ignore
return
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
async def init_permission(session):
for permission in PermissionConstant.get_all():
parts = permission.split(":", 1)
resource = parts[0]
action = parts[1] if len(parts) > 1 else ""
existing_permission = await PermissionService.get_permission_by_name(
session, permission
)
if existing_permission:
logger.debug(f"权限已存在: {permission}")
continue
await PermissionService.create_permission(session, permission, resource, action)
logger.info(f"创建权限: {permission}")
async def init_db():
task_service = get_task_service_instance()
if task_service.async_session_maker is None:
return
async with task_service.async_session_maker() as session:
await dev_init_admin(session)
await init_permission(session)
@@ -1,15 +0,0 @@
from ..core.scheduler import logger
from ..service.user import UserService
from ..core.scheduler import get_task_service_instance
async def dev_init_admin():
task_service = get_task_service_instance()
if task_service.async_session_maker is None:
return
async with task_service.async_session_maker() as session:
admin_user = await UserService.get_user_by_email("admin", session)
if admin_user: # type: ignore
logger.info("管理员用户已存在") # type: ignore
return
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
+10 -4
View File
@@ -115,6 +115,8 @@ def run_pyinstaller():
"--hidden-import",
"aiosqlite",
"--hidden-import",
"aiofiles",
"--hidden-import",
"apscheduler",
"--hidden-import",
"diskcache",
@@ -127,6 +129,8 @@ def run_pyinstaller():
"--hidden-import",
"sqlite_vfs",
"--hidden-import",
"python_multipart",
"--hidden-import",
"sqlite_vfs.core",
"--hidden-import",
"jwt",
@@ -154,11 +158,12 @@ def run_pyinstaller():
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
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from ..app.service.user import UserService
from ..app.core.config import get_config
from ..app.task.dev_init_db import init_permission, dev_init_admin
database = get_config()["database"]
database_type = database["type"]
@@ -187,7 +192,9 @@ async def init_database():
raise
# 添加管理员用户
async with async_session_maker() as session:
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
# await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
await init_permission(session)
await dev_init_admin(session)
async def main():
@@ -256,7 +263,6 @@ async def main():
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("构建完成")
Generated
+56
View File
@@ -7,6 +7,31 @@ resolution-markers = [
"python_full_version < '3.9'",
]
[[package]]
name = "aiofiles"
version = "24.1.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" },
]
[[package]]
name = "aiofiles"
version = "25.1.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" },
]
[[package]]
name = "aiosqlite"
version = "0.20.0"
@@ -337,6 +362,8 @@ name = "chongming"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiofiles", version = "24.1.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "aiofiles", version = "25.1.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
{ name = "aiosqlite", version = "0.20.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "aiosqlite", version = "0.22.1", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
{ name = "apscheduler" },
@@ -349,6 +376,8 @@ dependencies = [
{ name = "passlib", extra = ["bcrypt"] },
{ name = "pyjwt", version = "2.9.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pyjwt", version = "2.11.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
{ name = "python-multipart", version = "0.0.20", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.10'" },
{ name = "python-multipart", version = "0.0.26", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.10'" },
{ name = "sqlite-vfs" },
{ name = "sqlmodel", version = "0.0.29", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "sqlmodel", version = "0.0.34", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version == '3.9.*'" },
@@ -370,6 +399,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "aiosqlite" },
{ name = "apscheduler" },
{ name = "bcrypt", specifier = "==4.0.1" },
@@ -378,6 +408,7 @@ requires-dist = [
{ name = "module-bank" },
{ name = "passlib", extras = ["bcrypt"] },
{ name = "pyjwt" },
{ name = "python-multipart", specifier = ">=0.0.20" },
{ name = "sqlite-vfs" },
{ name = "sqlmodel", extras = ["aiosqlite"] },
{ name = "tomli" },
@@ -1334,6 +1365,31 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.26"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"