From 6a5f8cf0dd16fdc18e85f3090f8db705b07a9df8 Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Sat, 11 Jul 2026 13:03:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AE=8C=E6=88=90=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=A4=A7=E8=A7=84=E6=A8=A1=E9=87=8D=E6=9E=84=E4=B8=8E?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这是一次综合性的项目迭代,包含以下核心变更: 1. **目录与模块重构** - 调整工作流节点类型模块目录结构,迁移节点类型相关代码 - 重命名platform模块为system模块,更新插件配置信息 - 重构代码生成模块导入路径 2. **数据库与CRUD优化** - 统一所有CRUD类构造函数,新增数据库会话参数 - 修复权限过滤器数据库会话使用问题 - 更新模板生成器的CRUD代码模板 3. **认证与安全改进** - 重构JWT密钥配置,移除默认密钥强制要求环境变量 - 重命名密码工具类,统一密码加密校验逻辑 - 优化OAuth认证流程,修复匿名认证使用问题 4. **前端与静态资源** - 重构前端挂载逻辑,增加目录存在性校验 - 使用标准StaticFiles替换自定义前端挂载实现 5. **工具类与依赖更新** - 修复导入工具的表名重复检测逻辑 - 优化限流回调代码,移除冗余依赖 - 更新用户、租户等模块的响应模型字段 6. **数据与配置修正** - 修复系统版本数据字段命名不统一问题 - 简化枚举类校验逻辑,移除冗余注释 - 修复测试用例中的密码工具类导入错误 --- backend/app/api/v1/module_ai/chat/service.py | 9 +- .../v1/module_generator/gencode/controller.py | 41 ++- .../api/v1/module_generator/gencode/crud.py | 23 +- .../v1/module_generator/gencode/service.py | 87 +++--- .../v1/module_platform/invoice/controller.py | 13 +- .../api/v1/module_platform/invoice/crud.py | 7 +- .../api/v1/module_platform/invoice/service.py | 15 +- .../api/v1/module_platform/menu/controller.py | 23 +- .../app/api/v1/module_platform/menu/crud.py | 6 +- .../api/v1/module_platform/menu/service.py | 37 +-- .../v1/module_platform/order/controller.py | 45 ++- .../app/api/v1/module_platform/order/crud.py | 6 +- .../api/v1/module_platform/order/service.py | 75 ++--- .../v1/module_platform/package/controller.py | 32 +- .../api/v1/module_platform/package/crud.py | 6 +- .../api/v1/module_platform/package/service.py | 48 +-- .../v1/module_platform/tenant/controller.py | 66 ++-- .../app/api/v1/module_platform/tenant/crud.py | 6 +- .../api/v1/module_platform/tenant/schema.py | 19 +- .../api/v1/module_platform/tenant/service.py | 287 +++++++++++------- .../api/v1/module_system/auth/controller.py | 17 +- .../v1/module_system/auth/oauth_service.py | 12 +- .../app/api/v1/module_system/auth/schema.py | 17 +- .../app/api/v1/module_system/auth/service.py | 42 ++- .../api/v1/module_system/dept/controller.py | 23 +- backend/app/api/v1/module_system/dept/crud.py | 6 +- .../app/api/v1/module_system/dept/service.py | 37 +-- .../api/v1/module_system/dict/controller.py | 48 ++- backend/app/api/v1/module_system/dict/crud.py | 12 +- .../app/api/v1/module_system/dict/service.py | 77 ++--- .../api/v1/module_system/log/controller.py | 21 +- backend/app/api/v1/module_system/log/crud.py | 10 +- .../app/api/v1/module_system/log/service.py | 24 +- .../api/v1/module_system/notice/controller.py | 24 +- .../app/api/v1/module_system/notice/crud.py | 6 +- .../api/v1/module_system/notice/service.py | 29 +- .../api/v1/module_system/params/controller.py | 24 +- .../app/api/v1/module_system/params/crud.py | 7 +- .../api/v1/module_system/params/service.py | 30 +- backend/app/api/v1/module_system/plugin.toml | 8 +- .../v1/module_system/position/controller.py | 29 +- .../app/api/v1/module_system/position/crud.py | 6 +- .../api/v1/module_system/position/service.py | 31 +- .../api/v1/module_system/role/controller.py | 32 +- backend/app/api/v1/module_system/role/crud.py | 16 +- .../app/api/v1/module_system/role/service.py | 45 +-- .../api/v1/module_system/ticket/controller.py | 29 +- .../app/api/v1/module_system/ticket/crud.py | 10 +- .../api/v1/module_system/ticket/service.py | 35 ++- .../api/v1/module_system/user/controller.py | 40 ++- backend/app/api/v1/module_system/user/crud.py | 14 +- .../app/api/v1/module_system/user/schema.py | 8 +- .../app/api/v1/module_system/user/service.py | 100 +++--- .../v1/module_system/versions/controller.py | 22 +- .../app/api/v1/module_system/versions/crud.py | 6 +- .../api/v1/module_system/versions/service.py | 23 +- backend/app/api/v1/module_task/__init__.py | 4 +- .../v1/module_task/cronjob/job/controller.py | 14 +- .../api/v1/module_task/cronjob/job/crud.py | 8 +- .../api/v1/module_task/cronjob/job/service.py | 19 +- .../v1/module_task/cronjob/node/controller.py | 32 +- .../api/v1/module_task/cronjob/node/crud.py | 7 +- .../v1/module_task/cronjob/node/service.py | 30 +- .../module_task/workflow/flows/controller.py | 26 +- .../api/v1/module_task/workflow/flows/crud.py | 7 +- .../v1/module_task/workflow/flows/service.py | 33 +- .../workflow/{nodes => node_type}/__init__.py | 0 .../{nodes => node_type}/controller.py | 42 +-- .../workflow/{nodes => node_type}/crud.py | 7 +- .../workflow/{nodes => node_type}/model.py | 3 +- .../workflow/{nodes => node_type}/schema.py | 0 .../workflow/{nodes => node_type}/service.py | 27 +- backend/app/common/enums.py | 12 +- backend/app/config/setting.py | 2 +- backend/app/core/ap_scheduler.py | 33 +- backend/app/core/base_crud.py | 42 ++- backend/app/core/base_schema.py | 60 ++-- backend/app/core/dependencies.py | 22 +- backend/app/core/http_limit.py | 14 - backend/app/core/permission.py | 8 +- backend/app/core/router_class.py | 4 +- backend/app/init_app.py | 3 +- .../plugin/module_example/demo/controller.py | 27 +- .../app/plugin/module_example/demo/crud.py | 7 +- .../app/plugin/module_example/demo/service.py | 32 +- backend/app/scripts/data/sys_version.json | 50 ++- backend/app/utils/gen_util.py | 4 +- backend/app/utils/hash_bcrpy_util.py | 84 ----- backend/app/utils/import_util.py | 10 +- backend/app/utils/jinja2_template_util.py | 10 +- backend/app/utils/password_util.py | 71 +++++ backend/templates/python/controller.py.jinja2 | 59 ++-- backend/templates/python/crud.py.jinja2 | 7 +- backend/templates/python/service.py.jinja2 | 56 ++-- backend/tests/conftest.py | 2 +- 95 files changed, 1531 insertions(+), 1118 deletions(-) rename backend/app/api/v1/module_task/workflow/{nodes => node_type}/__init__.py (100%) rename backend/app/api/v1/module_task/workflow/{nodes => node_type}/controller.py (62%) rename backend/app/api/v1/module_task/workflow/{nodes => node_type}/crud.py (92%) rename backend/app/api/v1/module_task/workflow/{nodes => node_type}/model.py (98%) rename backend/app/api/v1/module_task/workflow/{nodes => node_type}/schema.py (100%) rename backend/app/api/v1/module_task/workflow/{nodes => node_type}/service.py (73%) delete mode 100644 backend/app/utils/hash_bcrpy_util.py create mode 100644 backend/app/utils/password_util.py diff --git a/backend/app/api/v1/module_ai/chat/service.py b/backend/app/api/v1/module_ai/chat/service.py index 8f86bc17..e1f8b126 100644 --- a/backend/app/api/v1/module_ai/chat/service.py +++ b/backend/app/api/v1/module_ai/chat/service.py @@ -8,6 +8,7 @@ from agno.run.team import TeamRunOutput from agno.session.team import TeamSession from agno.team.team import Team from redis.asyncio import Redis +from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_platform.tenant.service import TenantService from app.common.enums import RedisInitKeyConfig @@ -28,7 +29,7 @@ from .schema import ( from .utils import AgnoFactory -async def _format_session_data(session: TeamSession, auth: AuthSchema | None = None) -> dict[str, Any]: +async def _format_session_data(session: TeamSession, auth: AuthSchema | None = None, db: AsyncSession | None = None) -> dict[str, Any]: """格式化会话数据,添加前端需要的字段""" if hasattr(session, "to_dict"): session_dict = session.to_dict() @@ -67,13 +68,13 @@ async def _format_session_data(session: TeamSession, auth: AuthSchema | None = N "messages": messages, } - # 如果有 auth,查询部门名称 - if auth and session_dict.get("team_id"): + # 如果有 auth 和 db,查询部门名称 + if auth and db and session_dict.get("team_id"): try: team_id_str = session_dict.get("team_id") if team_id_str: team_id = int(team_id_str) - tenant = await TenantService(auth).detail(id=team_id) + tenant = await TenantService(auth, db).detail(id=team_id) result["team_name"] = tenant.name except Exception: result["team_name"] = None diff --git a/backend/app/api/v1/module_generator/gencode/controller.py b/backend/app/api/v1/module_generator/gencode/controller.py index 98698a82..bb2fbb60 100644 --- a/backend/app/api/v1/module_generator/gencode/controller.py +++ b/backend/app/api/v1/module_generator/gencode/controller.py @@ -1,11 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, StreamResponse, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.logger import logger from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -21,11 +22,12 @@ async def gen_table_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[GenTableQueryParam, Query(description="查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: order_by = [{"created_time": "desc"}] if page.order_by: order_by = page.order_by - result_dict = await GenTableService(auth).get_gen_table_page( + result_dict = await GenTableService(auth, db).get_gen_table_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -39,8 +41,9 @@ async def get_gen_db_table_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:dblist:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[GenTableQueryParam, Query(description="查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await GenTableService(auth).get_gen_db_table_page( + result_dict = await GenTableService(auth, db).get_gen_db_table_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -52,8 +55,9 @@ async def get_gen_db_table_list_controller( async def import_gen_table_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:import"]))], table_names: Annotated[list[str], Body(description="表名列表")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - svc = GenTableService(auth) + svc = GenTableService(auth, db) add_gen_table_list = await svc.get_gen_db_table_list_by_name(table_names) result = await svc.import_gen_table(add_gen_table_list) return SuccessResponse(msg="导入表结构成功", data=result) @@ -63,8 +67,9 @@ async def import_gen_table_controller( async def gen_table_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))], table_id: Annotated[int, Path(description="业务表ID")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).get_gen_table_detail(table_id) + result = await GenTableService(auth, db).get_gen_table_detail(table_id) return SuccessResponse(data=result, msg="获取业务表详细信息成功") @@ -72,8 +77,9 @@ async def gen_table_detail_controller( async def create_table_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:create"]))], body: Annotated[GenCreateTableSqlBody, Body(description="创建表结构参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).create_table(body.sql) + result = await GenTableService(auth, db).create_table(body.sql) return SuccessResponse(msg="创建表结构成功", data=result) @@ -82,8 +88,9 @@ async def update_gen_table_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:update"]))], table_id: Annotated[int, Path(description="业务表ID")], data: Annotated[GenTableSchema, Body(description="业务表信息")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await GenTableService(auth).update_gen_table(data, table_id) + result_dict = await GenTableService(auth, db).update_gen_table(data, table_id) return SuccessResponse(data=result_dict, msg="编辑业务表信息成功") @@ -91,8 +98,9 @@ async def update_gen_table_controller( async def delete_gen_table_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:delete"]))], ids: Annotated[list[int], Body(description="业务表ID列表")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).delete_gen_table(ids) + result = await GenTableService(auth, db).delete_gen_table(ids) return SuccessResponse(msg="删除业务表信息成功", data=result) @@ -100,8 +108,9 @@ async def delete_gen_table_controller( async def batch_gen_code_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:operate"]))], table_names: Annotated[list[str], Body(description="表名列表")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> StreamResponse: - batch_gen_code_result, failed_tables = await GenTableService(auth).batch_gen_code(table_names) + batch_gen_code_result, failed_tables = await GenTableService(auth, db).batch_gen_code(table_names) headers = {"Content-Disposition": "attachment; filename=code.zip"} if failed_tables: logger.warning(f"批量生成代码部分失败,跳过表: {failed_tables}") @@ -117,8 +126,9 @@ async def batch_gen_code_controller( async def gen_code_local_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:code"]))], table_name: Annotated[str, Path(description="表名")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).generate_code(table_name) + result = await GenTableService(auth, db).generate_code(table_name) return SuccessResponse(msg="生成代码到指定路径成功", data=result) @@ -126,8 +136,9 @@ async def gen_code_local_controller( async def preview_code_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:gencode:query"]))], table_id: Annotated[int, Path(description="业务表ID")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).preview_code(table_id) + result = await GenTableService(auth, db).preview_code(table_id) return SuccessResponse(data=result, msg="预览代码成功") @@ -135,8 +146,9 @@ async def preview_code_controller( async def sync_db_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:db:sync"]))], table_name: Annotated[str, Path(description="表名")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).sync_db(table_name) + result = await GenTableService(auth, db).sync_db(table_name) return SuccessResponse(msg="同步数据库成功", data=result) @@ -144,6 +156,7 @@ async def sync_db_controller( async def sync_db_preview_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_generator:db:sync"]))], table_name: Annotated[str, Path(description="表名")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await GenTableService(auth).sync_db_preview(table_name) + result = await GenTableService(auth, db).sync_db_preview(table_name) return SuccessResponse(msg="获取同步差异预览成功", data=result) diff --git a/backend/app/api/v1/module_generator/gencode/crud.py b/backend/app/api/v1/module_generator/gencode/crud.py index fdc8dca9..fceba6ee 100644 --- a/backend/app/api/v1/module_generator/gencode/crud.py +++ b/backend/app/api/v1/module_generator/gencode/crud.py @@ -3,6 +3,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING from sqlalchemy import Inspector, inspect, select, text +from sqlalchemy.ext.asyncio import AsyncSession from app.config.setting import settings from app.core.base_crud import CRUDBase @@ -25,13 +26,14 @@ if TYPE_CHECKING: class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): """代码生成业务表模块数据库操作层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化CRUD操作层 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - super().__init__(model=GenTableModel, auth=auth) + super().__init__(model=GenTableModel, auth=auth, db=db) async def get_gen_table_by_id(self, table_id: int, preload: list | None = None) -> GenTableModel | None: """根据业务表ID获取需要生成的业务表信息。 @@ -224,9 +226,9 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): count_sql = text(f"SELECT COUNT(1) AS cnt FROM information_schema.tables {where_sql}") rows_sql = text(f"SELECT table_name, table_comment FROM information_schema.tables {where_sql} ORDER BY table_name ASC LIMIT :limit OFFSET :offset") - total_res = await self.auth.db.execute(count_sql, params) + total_res = await self.db.execute(count_sql, params) total = int(total_res.scalar() or 0) - res = await self.auth.db.execute(rows_sql, params) + res = await self.db.execute(rows_sql, params) items: list[dict] = [] for r in res.fetchall(): # r may be Row/tuple depending on driver @@ -257,9 +259,9 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): base_from = "FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 " count_sql = text(f"SELECT COUNT(1) AS cnt {base_from} {where_sql}") rows_sql = text(f"SELECT c.relname AS table_name, COALESCE(d.description,'') AS table_comment {base_from} {where_sql} ORDER BY c.relname ASC LIMIT :limit OFFSET :offset") - total_res = await self.auth.db.execute(count_sql, params) + total_res = await self.db.execute(count_sql, params) total = int(total_res.scalar() or 0) - res = await self.auth.db.execute(rows_sql, params) + res = await self.db.execute(rows_sql, params) items = [] for r in res.fetchall(): table_name = r[0] @@ -368,7 +370,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): """ try: # 执行SQL但不手动提交事务,由框架管理事务生命周期 - await self.auth.db.execute(text(sql)) + await self.db.execute(text(sql)) return True except Exception as e: logger.error(f"执行SQL时发生错误: {e}") @@ -378,13 +380,14 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, GenTableColumnSchema]): """代码生成业务表字段模块数据库操作层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化CRUD操作层 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - super().__init__(model=GenTableColumnModel, auth=auth) + super().__init__(model=GenTableColumnModel, auth=auth, db=db) @staticmethod def _sync_get_table_columns(database_type: str, table_name: str) -> list[dict]: @@ -580,7 +583,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen """ # 先查询出这些表ID对应的所有字段ID query = select(GenTableColumnModel.id).where(GenTableColumnModel.table_id.in_(table_ids)) - result = await self.auth.db.execute(query) + result = await self.db.execute(query) column_ids = [row[0] for row in result.fetchall()] # 如果有字段ID,则删除这些字段 diff --git a/backend/app/api/v1/module_generator/gencode/service.py b/backend/app/api/v1/module_generator/gencode/service.py index 76f3b2d4..9b2f0f91 100644 --- a/backend/app/api/v1/module_generator/gencode/service.py +++ b/backend/app/api/v1/module_generator/gencode/service.py @@ -7,6 +7,7 @@ from typing import Any import anyio import sqlglot +from sqlalchemy.ext.asyncio import AsyncSession from sqlglot.expressions import ( Alter, Comment, @@ -69,8 +70,9 @@ _MENU_TYPE_MENU = 2 class GenTableService: """代码生成业务表服务层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def _effective_package_name(self, parent_catalog_id: int | None, package_name: str | None) -> str: """根据「是否选择上级目录」计算最终包名(分系统根目录)。 @@ -84,7 +86,7 @@ class GenTableService: if parent_catalog_id is not None: from app.api.v1.module_platform.menu.crud import MenuCRUD - m = await MenuCRUD(self.auth).get(id=parent_catalog_id) + m = await MenuCRUD(self.auth, self.db).get(id=parent_catalog_id) if not m: raise CustomException(msg="上级菜单不存在") route_path = (getattr(m, "route_path", None) or "").strip() @@ -109,7 +111,7 @@ class GenTableService: return from app.api.v1.module_platform.menu.crud import MenuCRUD - m = await MenuCRUD(self.auth).get(id=parent_menu_id) + m = await MenuCRUD(self.auth, self.db).get(id=parent_menu_id) if not m: raise CustomException(msg="上级菜单不存在") if m.type != _MENU_TYPE_CATALOG: @@ -246,7 +248,7 @@ class GenTableService: 返回: - list[dict]: 包含业务表列表信息的字典列表。 """ - gen_table_list_result = await GenTableCRUD(auth=self.auth).get_gen_table_list(search) + gen_table_list_result = await GenTableCRUD(self.auth, self.db).get_gen_table_list(search) return [GenTableOutSchema.model_validate(obj).model_dump() for obj in gen_table_list_result] @handle_service_exception @@ -271,7 +273,7 @@ class GenTableService: """ offset = (page_no - 1) * page_size order = order_by or [{"created_time": "desc"}] - return await GenTableCRUD(auth=self.auth).page( + return await GenTableCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order, @@ -290,7 +292,7 @@ class GenTableService: 返回: - list[Any]: 包含数据库表列表信息的任意类型列表。 """ - gen_db_table_list_result = await GenTableCRUD(auth=self.auth).get_db_table_list(search) + gen_db_table_list_result = await GenTableCRUD(self.auth, self.db).get_db_table_list(search) return gen_db_table_list_result @handle_service_exception @@ -312,7 +314,7 @@ class GenTableService: - dict[str, Any]: 含 items、total、has_next 等字段。 """ offset = (page_no - 1) * page_size - items, total = await GenTableCRUD(auth=self.auth).get_db_table_page(search=search, offset=offset, limit=page_size) + items, total = await GenTableCRUD(self.auth, self.db).get_db_table_page(search=search, offset=offset, limit=page_size) return { "items": items, "total": total, @@ -332,7 +334,7 @@ class GenTableService: 返回: - list[GenTableOutSchema]: 包含业务表详细信息的模型列表。 """ - gen_db_table_list_result = await GenTableCRUD(auth=self.auth).get_db_table_list_by_names(table_names) + gen_db_table_list_result = await GenTableCRUD(self.auth, self.db).get_db_table_list_by_names(table_names) # 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema result = [GenTableOutSchema(**gen_table.model_dump()) for gen_table in gen_db_table_list_result] @@ -359,14 +361,14 @@ class GenTableService: self.normalize_and_validate_master_sub(GenTableSchema.model_validate(_row)) table_name = table.table_name # 检查表是否已存在 - existing_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name) + existing_table = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(table_name) if existing_table: raise CustomException(msg=f"以下表已存在,不能重复导入: {table_name}") GenUtils.init_table(table) if not table.columns: table.columns = [] - add_gen_table = await GenTableCRUD(auth=self.auth).add_gen_table(GenTableSchema.model_validate(table.model_dump())) - gen_table_columns = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(table_name) + add_gen_table = await GenTableCRUD(self.auth, self.db).add_gen_table(GenTableSchema.model_validate(table.model_dump())) + gen_table_columns = await GenTableColumnCRUD(self.auth, self.db).get_gen_db_table_columns_by_name(table_name) if len(gen_table_columns) > 0: table.id = add_gen_table.id for column in gen_table_columns: @@ -386,7 +388,7 @@ class GenTableService: python_field=column.python_field, ) GenUtils.init_column_field(column_schema, table) - await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_schema) + await GenTableColumnCRUD(self.auth, self.db).create_gen_table_column_crud(column_schema) return True except Exception as e: raise CustomException(msg=f"导入失败, {e!s}") @@ -432,7 +434,7 @@ class GenTableService: table_names = list(set(table_names)) # 创建CRUD实例 - gen_table_crud = GenTableCRUD(auth=self.auth) + gen_table_crud = GenTableCRUD(self.auth, self.db) # 检查每个表是否已存在 for table_name in table_names: @@ -492,12 +494,12 @@ class GenTableService: self.normalize_and_validate_master_sub(data) await self._assert_parent_menu_is_catalog(data.parent_menu_id) # 直接调用edit_gen_table方法,它会在内部处理排除嵌套字段的逻辑 - result = await GenTableCRUD(auth=self.auth).edit_gen_table(table_id, data) + result = await GenTableCRUD(self.auth, self.db).edit_gen_table(table_id, data) if not result: raise CustomException(msg="更新业务表信息失败") if data.columns is not None: - db_columns = await GenTableColumnCRUD(auth=self.auth).list_gen_table_column_crud(search={"table_id": table_id}) + db_columns = await GenTableColumnCRUD(self.auth, self.db).list_gen_table_column_crud(search={"table_id": table_id}) db_column_map = {c.column_name: c for c in db_columns if c.column_name} submitted_names = {c.column_name for c in data.columns if hasattr(c, "column_name") and c.column_name} @@ -508,7 +510,7 @@ class GenTableService: # 只更新前端实际修改的字段(利用 Pydantic model_fields_set) update_data = gen_table_column.model_dump(exclude_unset=True, exclude={"id", "super_column"}) if update_data: - await GenTableColumnCRUD(auth=self.auth).update(id=col_id, data=GenTableColumnSchema(**update_data)) + await GenTableColumnCRUD(self.auth, self.db).update(id=col_id, data=GenTableColumnSchema(**update_data)) else: # 新增列:前端新增但库中无对应记录 column_schema = GenTableColumnSchema( @@ -516,16 +518,16 @@ class GenTableService: **gen_table_column.model_dump(exclude={"id", "super_column"}), ) GenUtils.init_column_field(column_schema, gen_table_info) - await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_schema) + await GenTableColumnCRUD(self.auth, self.db).create_gen_table_column_crud(column_schema) # 删除前端已移除的列 for db_name, db_col in db_column_map.items(): if db_name not in submitted_names: db_id = getattr(db_col, "id", None) if db_id: - await GenTableColumnCRUD(auth=self.auth).delete(ids=[db_id]) + await GenTableColumnCRUD(self.auth, self.db).delete(ids=[db_id]) # 重新获取带有预加载关系的对象,避免懒加载导致的MissingGreenlet错误 - updated_gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_id(table_id) + updated_gen_table = await GenTableCRUD(self.auth, self.db).get_gen_table_by_id(table_id) out = GenTableOutSchema.model_validate(updated_gen_table) await self.set_pk_column(out) await self.hydrate_sub_table(out) @@ -554,9 +556,9 @@ class GenTableService: try: # 先删除相关的字段信息 - await GenTableColumnCRUD(auth=self.auth).delete_gen_table_column_by_table_id_crud(ids) + await GenTableColumnCRUD(self.auth, self.db).delete_gen_table_column_by_table_id_crud(ids) # 再删除表信息 - await GenTableCRUD(auth=self.auth).delete_gen_table(ids) + await GenTableCRUD(self.auth, self.db).delete_gen_table(ids) except Exception as e: raise CustomException(msg=str(e)) @@ -571,7 +573,7 @@ class GenTableService: 返回: - GenTableOutSchema: 业务表详细信息模型。 """ - gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_id(table_id) + gen_table = await GenTableCRUD(self.auth, self.db).get_gen_table_by_id(table_id) if not gen_table: raise CustomException(msg="业务表不存在") @@ -590,7 +592,7 @@ class GenTableService: 返回: - list[GenTableOutSchema]: 业务表详细信息模型列表。 """ - gen_table_all = await GenTableCRUD(auth=self.auth).get_gen_table_all() or [] + gen_table_all = await GenTableCRUD(self.auth, self.db).get_gen_table_all() or [] result = [] for gen_table in gen_table_all: try: @@ -612,7 +614,7 @@ class GenTableService: 返回: - dict[str, Any]: 文件名到渲染内容的映射。 """ - raw = await GenTableCRUD(auth=self.auth).get_gen_table_by_id(table_id) + raw = await GenTableCRUD(self.auth, self.db).get_gen_table_by_id(table_id) if not raw: raise CustomException(msg="业务表不存在") gen_table = GenTableOutSchema.model_validate(raw) @@ -735,7 +737,7 @@ class GenTableService: await _write_templates(sub_templates, sub_ctx, gen_table_schema.sub_table) # 2. 代码成功写入后,再创建菜单(避免失败时产生孤儿菜单数据) - menu_crud = MenuCRUD(self.auth) + menu_crud = MenuCRUD(self.auth, self.db) await self._assert_parent_menu_is_catalog(gen_table_schema.parent_menu_id) # 1. 目录 + 菜单 + 按钮:先取/建模块目录(名称规则见 _catalog_menu_dir_key) dir_menu_id = await self._get_or_create_package_directory_menu( @@ -936,7 +938,7 @@ class GenTableService: # 验证表名非空 if not table_name or not table_name.strip(): raise CustomException(msg="表名不能为空") - gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name) + gen_table = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(table_name) if not gen_table: raise CustomException(msg="业务表不存在") table = GenTableOutSchema.model_validate(gen_table) @@ -945,7 +947,7 @@ class GenTableService: table_columns = table.columns or [] table_column_map = {column.column_name: column for column in table_columns} # 确保db_table_columns始终是列表类型,避免None值 - db_table_columns = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(table_name) or [] + db_table_columns = await GenTableColumnCRUD(self.auth, self.db).get_gen_db_table_columns_by_name(table_name) or [] db_table_columns = [col for col in db_table_columns if col is not None] db_table_column_names = [column.column_name for column in db_table_columns] try: @@ -990,26 +992,26 @@ class GenTableService: # 转换为 GenTableColumnSchema,排除 super_column 等输出专用字段 column_data = GenTableColumnSchema(**column.model_dump(exclude={"super_column"})) if hasattr(column, "id") and column.id: - await GenTableColumnCRUD(auth=self.auth).update_gen_table_column_crud(column.id, column_data) + await GenTableColumnCRUD(self.auth, self.db).update_gen_table_column_crud(column.id, column_data) else: - await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_data) + await GenTableColumnCRUD(self.auth, self.db).create_gen_table_column_crud(column_data) else: # 设置table_id以确保新字段能正确关联到表 column.table_id = table.id # 转换为 GenTableColumnSchema,排除 super_column 等输出专用字段 column_data = GenTableColumnSchema(**column.model_dump(exclude={"super_column"})) - await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_data) + await GenTableColumnCRUD(self.auth, self.db).create_gen_table_column_crud(column_data) del_columns = [column for column in table_columns if column.column_name not in db_table_column_names] if del_columns: for column in del_columns: if hasattr(column, "id") and column.id: - await GenTableColumnCRUD(auth=self.auth).delete_gen_table_column_by_column_id_crud([column.id]) + await GenTableColumnCRUD(self.auth, self.db).delete_gen_table_column_by_column_id_crud([column.id]) # 主子表:若子表也已导入生成器,则一并同步子表配置 sn = (table.sub_table_name or "").strip() fk = (table.sub_table_fk_name or "").strip() if _sync_sub and sn and fk: - sub_cfg = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(sn) + sub_cfg = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(sn) if sub_cfg: await self.sync_db(sn, _sync_sub=False) except Exception as e: @@ -1052,7 +1054,7 @@ class GenTableService: # 1) 若子表已作为 gen_table 导入,则使用其 columns 配置(可控、可复用) try: - sub_cfg_model = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(sub_name_raw, preload=["columns"]) + sub_cfg_model = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(sub_name_raw, preload=["columns"]) except Exception: sub_cfg_model = None if sub_cfg_model: @@ -1072,7 +1074,7 @@ class GenTableService: # 2) 回退:仅从 DB 读取结构(只读,无法配置子表字段) try: - gen_table_columns = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(sub_name_raw) + gen_table_columns = await GenTableColumnCRUD(self.auth, self.db).get_gen_db_table_columns_by_name(sub_name_raw) except Exception as e: logger.warning(f"获取子表 {sub_name_raw} 字段失败: {e!s}") gen_table.sub = False @@ -1090,7 +1092,7 @@ class GenTableService: gen_table.sub_table = None gen_table.master_sub_hint = f"子表「{sub_name_raw}」中不存在名为「{fk_raw}」的列,请核对外键列名" return - table_comment = await GenTableCRUD(auth=self.auth).get_db_table_comment(sub_name_raw) + table_comment = await GenTableCRUD(self.auth, self.db).get_db_table_comment(sub_name_raw) sub = GenTableOutSchema.model_validate( { "id": -1, @@ -1187,7 +1189,7 @@ class GenTableService: """ if not table_name or not table_name.strip(): raise CustomException(msg="表名不能为空") - gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name, preload=["columns"]) + gen_table = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(table_name, preload=["columns"]) if not gen_table: raise CustomException(msg="业务表不存在") @@ -1195,7 +1197,7 @@ class GenTableService: if not table.id: raise CustomException(msg="业务表ID不能为空") - db_cols = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(table_name) + db_cols = await GenTableColumnCRUD(self.auth, self.db).get_gen_db_table_columns_by_name(table_name) added, removed, changed, unchanged = self._sync_preview_diff( current_cols=table.columns or [], db_cols=db_cols or [], @@ -1214,12 +1216,12 @@ class GenTableService: if sn and fk: preview.sub_table_name = sn # 优先取“已导入的子表配置”,否则用 DB 结构(只读) - sub_cfg = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(sn, preload=["columns"]) + sub_cfg = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(sn, preload=["columns"]) if sub_cfg: cur_sub_cols = GenTableOutSchema.model_validate(sub_cfg).columns or [] else: cur_sub_cols = [] - db_sub_cols = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(sn) + db_sub_cols = await GenTableColumnCRUD(self.auth, self.db).get_gen_db_table_columns_by_name(sn) s_added, s_removed, s_changed, s_unchanged = self._sync_preview_diff( current_cols=cur_sub_cols, db_cols=db_sub_cols or [], @@ -1279,7 +1281,7 @@ class GenTableService: 异常: - CustomException: 当业务表不存在或数据转换失败时抛出。 """ - gen_table_model = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name) + gen_table_model = await GenTableCRUD(self.auth, self.db).get_gen_table_by_name(table_name) # 检查表是否存在 if gen_table_model is None: raise CustomException(msg=f"业务表 {table_name} 不存在") @@ -1298,8 +1300,9 @@ class GenTableService: class GenTableColumnService: """代码生成业务表字段服务层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db @handle_service_exception async def get_gen_table_column_list_by_table_id(self, table_id: int) -> list[dict[str, Any]]: @@ -1312,5 +1315,5 @@ class GenTableColumnService: 返回: - list[dict[str, Any]]: 业务表字段列表,每个元素为字段详细信息字典。 """ - gen_table_column_list_result = await GenTableColumnCRUD(auth=self.auth).list_gen_table_column_crud({"table_id": table_id}) + gen_table_column_list_result = await GenTableColumnCRUD(self.auth, self.db).list_gen_table_column_crud({"table_id": table_id}) return [GenTableColumnOutSchema.model_validate(obj).model_dump() for obj in gen_table_column_list_result] diff --git a/backend/app/api/v1/module_platform/invoice/controller.py b/backend/app/api/v1/module_platform/invoice/controller.py index 19a0d5eb..e425cfe1 100644 --- a/backend/app/api/v1/module_platform/invoice/controller.py +++ b/backend/app/api/v1/module_platform/invoice/controller.py @@ -1,11 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import InvoiceApplySchema, InvoiceOutSchema, InvoiceQueryParam @@ -18,8 +19,9 @@ InvoiceRouter = APIRouter(prefix="/invoice", route_class=OperationLogRoute, tags async def invoice_apply_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:invoice:create"]))], data: Annotated[InvoiceApplySchema, Body(description="发票申请参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await InvoiceTenantService.apply(auth, data, auth.user.tenant_id if auth.user else 0) + result = await InvoiceTenantService.apply(auth=auth, db=db, data=data, tenant_id=auth.user.tenant_id if auth.user else 0) return SuccessResponse(data=result, msg="发票申请成功") @@ -28,9 +30,11 @@ async def invoice_list_my_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:invoice:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[InvoiceQueryParam, Query(description="发票查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: result = await InvoiceTenantService.list_my( auth=auth, + db=db, tenant_id=auth.user.tenant_id if auth.user else 0, page_no=page.page_no, page_size=page.page_size, @@ -44,6 +48,7 @@ async def invoice_list_my_controller( async def invoice_download_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:invoice:download"]))], id: Annotated[int, Path(description="发票ID", ge=1)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - pdf_url = await InvoiceTenantService.download(auth, id, auth.user.tenant_id if auth.user else 0) + pdf_url = await InvoiceTenantService.download(auth=auth, db=db, invoice_id=id, tenant_id=auth.user.tenant_id if auth.user else 0) return SuccessResponse(msg="下载地址", data={"pdf_url": pdf_url}) diff --git a/backend/app/api/v1/module_platform/invoice/crud.py b/backend/app/api/v1/module_platform/invoice/crud.py index c20d7ae3..b1867bfd 100644 --- a/backend/app/api/v1/module_platform/invoice/crud.py +++ b/backend/app/api/v1/module_platform/invoice/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,13 +10,14 @@ from .schema import InvoiceCreateSchema, InvoiceUpdateSchema class InvoiceCRUD(CRUDBase[InvoiceModel, InvoiceCreateSchema, InvoiceUpdateSchema]): """发票 CRUD —— 继承 CRUDBase 获得增删改查、软删除过滤、租户隔离、权限过滤""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化发票 CRUD 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - super().__init__(model=InvoiceModel, auth=auth) + super().__init__(model=InvoiceModel, auth=auth, db=db) async def get_by_order_id(self, order_id: int) -> InvoiceModel | None: """根据订单 ID 查询发票 diff --git a/backend/app/api/v1/module_platform/invoice/service.py b/backend/app/api/v1/module_platform/invoice/service.py index ca08db48..01a374f7 100644 --- a/backend/app/api/v1/module_platform/invoice/service.py +++ b/backend/app/api/v1/module_platform/invoice/service.py @@ -1,6 +1,8 @@ import random from datetime import date, datetime, timedelta +from sqlalchemy.ext.asyncio import AsyncSession + from app.api.v1.module_platform.order.crud import OrderCRUD from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException @@ -35,7 +37,7 @@ class InvoiceTenantService: """租户端发票服务""" @classmethod - async def apply(cls, auth: AuthSchema, data: InvoiceApplySchema, tenant_id: int) -> InvoiceOutSchema: + async def apply(cls, auth: AuthSchema, db: AsyncSession, data: InvoiceApplySchema, tenant_id: int) -> InvoiceOutSchema: """租户申请开票 参数: @@ -56,7 +58,7 @@ class InvoiceTenantService: raise CustomException(msg="增值税专用发票必须填写注册地址及电话") # 校验:订单存在且已支付 - order = await OrderCRUD(auth).get(id=data.order_id) + order = await OrderCRUD(auth, db).get(id=data.order_id) if not order: raise CustomException(msg="订单不存在") if order.status != 1: @@ -66,7 +68,7 @@ class InvoiceTenantService: if order.created_time and datetime.now() - order.created_time > timedelta(days=30): raise CustomException(msg="订单支付超过 30 天,不可申请开票") - crud = InvoiceCRUD(auth) + crud = InvoiceCRUD(auth, db) existing = await crud.get_by_order_id(data.order_id) if existing: raise CustomException(msg="该订单已申请过发票") @@ -94,6 +96,7 @@ class InvoiceTenantService: async def list_my( cls, auth: AuthSchema, + db: AsyncSession, tenant_id: int, page_no: int, page_size: int, @@ -118,7 +121,7 @@ class InvoiceTenantService: _search["invoice_type"] = search.invoice_type if search.status is not None: _search["status"] = search.status - return await InvoiceCRUD(auth).page( + return await InvoiceCRUD(auth, db).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"created_time": "desc"}], @@ -127,9 +130,9 @@ class InvoiceTenantService: ) @classmethod - async def download(cls, auth: AuthSchema, invoice_id: int, tenant_id: int) -> str: + async def download(cls, auth: AuthSchema, db: AsyncSession, invoice_id: int, tenant_id: int) -> str: """获取发票 PDF 下载地址""" - crud = InvoiceCRUD(auth) + crud = InvoiceCRUD(auth, db) invoice = await crud.get_or_404(id=invoice_id, msg="发票不存在") if hasattr(invoice, "tenant_id") and invoice.tenant_id != tenant_id: raise CustomException(msg="发票不存在") diff --git a/backend/app/api/v1/module_platform/menu/controller.py b/backend/app/api/v1/module_platform/menu/controller.py index 2b4f612b..403fbf2b 100644 --- a/backend/app/api/v1/module_platform/menu/controller.py +++ b/backend/app/api/v1/module_platform/menu/controller.py @@ -1,13 +1,14 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import MenuCreateSchema, MenuOutSchema, MenuQueryParam, MenuUpdateSchema @@ -22,28 +23,31 @@ _MENU_NS = "menu" @cache(expire=300, namespace=_MENU_NS) async def get_menu_tree_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:menu:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], search: Annotated[MenuQueryParam, Query(description="菜单查询参数")], ) -> JSONResponse: order_by = [{"order": "asc"}] - result_dict_tree = await MenuService(auth).tree(search=search, order_by=order_by) + result_dict_tree = await MenuService(auth, db).tree(search=search, order_by=order_by) return SuccessResponse(data=result_dict_tree, msg="查询菜单树成功") @MenuRouter.get("/detail/{id}", summary="查询菜单详情", response_model=ResponseSchema[MenuOutSchema]) async def get_obj_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:menu:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="菜单ID", ge=1)], ) -> JSONResponse: - result_dict = await MenuService(auth).detail(id=id) + result_dict = await MenuService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="查询菜单详情成功") @MenuRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建菜单", response_model=ResponseSchema[MenuOutSchema]) async def create_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:menu:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[MenuCreateSchema, Body(description="菜单创建参数")], ) -> JSONResponse: - result_dict = await MenuService(auth).create(data=data) + result_dict = await MenuService(auth, db).create(data=data) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(data=result_dict, msg="创建菜单成功") @@ -51,10 +55,11 @@ async def create_obj_controller( @MenuRouter.put("/update/{id}", summary="修改菜单", response_model=ResponseSchema[MenuOutSchema]) async def update_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:menu:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="菜单ID", ge=1)], data: Annotated[MenuUpdateSchema, Body(description="菜单修改参数")], ) -> JSONResponse: - result_dict = await MenuService(auth).update(id=id, data=data) + result_dict = await MenuService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(data=result_dict, msg="修改菜单成功") @@ -62,9 +67,10 @@ async def update_obj_controller( @MenuRouter.delete("/delete", summary="删除菜单", response_model=ResponseSchema[None]) async def delete_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:menu:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="菜单ID列表")], ) -> JSONResponse: - await MenuService(auth).delete(ids=ids) + await MenuService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(msg="删除菜单成功") @@ -72,8 +78,9 @@ async def delete_obj_controller( @MenuRouter.patch("/status/batch", summary="批量修改菜单状态", response_model=ResponseSchema[None]) async def batch_set_available_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:menu:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await MenuService(auth).set_available(data=data) + await MenuService(auth, db).set_available(data=data) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(msg="批量修改菜单状态成功") diff --git a/backend/app/api/v1/module_platform/menu/crud.py b/backend/app/api/v1/module_platform/menu/crud.py index d2235e2e..db3e0c3f 100644 --- a/backend/app/api/v1/module_platform/menu/crud.py +++ b/backend/app/api/v1/module_platform/menu/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,5 +10,5 @@ from .schema import MenuCreateSchema, MenuUpdateSchema class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]): """菜单模块数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=MenuModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=MenuModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_platform/menu/service.py b/backend/app/api/v1/module_platform/menu/service.py index 6a8b51d5..1ce06985 100644 --- a/backend/app/api/v1/module_platform/menu/service.py +++ b/backend/app/api/v1/module_platform/menu/service.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, BatchSetAvailable from app.core.dependencies import require_superadmin from app.core.exceptions import CustomException @@ -24,8 +26,9 @@ from .schema import ( class MenuService: """菜单管理服务(查询操作租户可见,写操作仅超级管理员可操作)""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def _validate_parent_child_type(self, parent_id: int | None, child_type: int | None) -> None: if parent_id is None: @@ -34,7 +37,7 @@ class MenuService: if child_type not in (1, 2, 4): raise CustomException(msg="顶级菜单仅允许目录、菜单或外链类型") return - parent = await MenuCRUD(self.auth).get(id=parent_id) + parent = await MenuCRUD(self.auth, self.db).get(id=parent_id) if not parent: raise CustomException(msg="父级菜单不存在") pt = parent.type @@ -50,7 +53,7 @@ class MenuService: async def _validate_parent_child_client(self, parent_id: int | None, client: str | None) -> None: if parent_id is None or client is None: return - parent = await MenuCRUD(self.auth).get(id=parent_id) + parent = await MenuCRUD(self.auth, self.db).get(id=parent_id) if not parent: return p_client = getattr(parent, "client", None) or "pc" @@ -58,12 +61,12 @@ class MenuService: raise CustomException(msg="子菜单终端须与父菜单一致(均为 pc 或均为 app)") async def detail(self, id: int) -> MenuOutSchema: - menu = await MenuCRUD(self.auth).get(id=id, preload=["roles"]) + menu = await MenuCRUD(self.auth, self.db).get(id=id, preload=["roles"]) if not menu: raise CustomException(msg="菜单不存在") menu_out = MenuOutSchema.model_validate(menu) if menu.parent_id: - parent = await MenuCRUD(self.auth).get(id=menu.parent_id) + parent = await MenuCRUD(self.auth, self.db).get(id=menu.parent_id) if parent: menu_out.parent_name = parent.name return menu_out @@ -73,7 +76,7 @@ class MenuService: search: MenuQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[dict]: - menu_list = await MenuCRUD(self.auth).tree_list(search=vars(search) if search else None, order_by=order_by) + menu_list = await MenuCRUD(self.auth, self.db).tree_list(search=vars(search) if search else None, order_by=order_by) menu_dict_list = [MenuTreeOutSchema.model_validate(menu).model_dump() for menu in menu_list] return traversal_to_tree(menu_dict_list) @@ -84,42 +87,42 @@ class MenuService: search["title"] = data.title if data.parent_id is not None: search["parent_id"] = data.parent_id - menu = await MenuCRUD(self.auth).get(**search) + menu = await MenuCRUD(self.auth, self.db).get(**search) if menu: raise CustomException(msg="创建失败,该菜单已存在") await self._validate_parent_child_type(data.parent_id, data.type) await self._validate_parent_child_client(data.parent_id, data.client) - new_menu = await MenuCRUD(self.auth).create(data=data) + new_menu = await MenuCRUD(self.auth, self.db).create(data=data) return MenuOutSchema.model_validate(new_menu) @require_superadmin async def update(self, id: int, data: MenuUpdateSchema) -> MenuOutSchema: - _ = await MenuCRUD(self.auth).get_or_404(id=id, msg="更新失败,该菜单不存在") + _ = await MenuCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该菜单不存在") await self._validate_parent_child_type(data.parent_id, data.type) await self._validate_parent_child_client(data.parent_id, data.client) if data.title is not None: search: dict[str, Any] = {"title": data.title} if data.parent_id is not None: search["parent_id"] = data.parent_id - exist_menu = await MenuCRUD(self.auth).get(**search) + exist_menu = await MenuCRUD(self.auth, self.db).get(**search) if exist_menu and exist_menu.id != id: raise CustomException(msg="更新失败,菜单标题重复") if data.parent_id: - parent_menu = await MenuCRUD(self.auth).get(id=data.parent_id) + parent_menu = await MenuCRUD(self.auth, self.db).get(id=data.parent_id) if not parent_menu: raise CustomException(msg="更新失败,父级菜单不存在") - new_menu = await MenuCRUD(self.auth).update(id=id, data=data) + new_menu = await MenuCRUD(self.auth, self.db).update(id=id, data=data) if data.status is not None: await self.set_available(data=BatchSetAvailable(ids=[id], status=data.status)) menu_out = MenuOutSchema.model_validate(new_menu) if menu_out.parent_id: - parent = await MenuCRUD(self.auth).get(id=menu_out.parent_id) + parent = await MenuCRUD(self.auth, self.db).get(id=menu_out.parent_id) if parent: menu_out.parent_name = parent.name return menu_out @@ -129,7 +132,7 @@ class MenuService: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - all_menus = await MenuCRUD(self.auth).get_list() + all_menus = await MenuCRUD(self.auth, self.db).get_list() child_id_map = get_child_id_map(model_list=all_menus) delete_ids_set = set() @@ -138,11 +141,11 @@ class MenuService: delete_ids_set.update(all_descendants) delete_ids = list(delete_ids_set) - await MenuCRUD(self.auth).delete(ids=delete_ids) + await MenuCRUD(self.auth, self.db).delete(ids=delete_ids) @require_superadmin async def set_available(self, data: BatchSetAvailable) -> None: - menu_list = await MenuCRUD(self.auth).get_list() + menu_list = await MenuCRUD(self.auth, self.db).get_list() total_ids = [] if data.status == 0: @@ -156,4 +159,4 @@ class MenuService: disable_ids = get_child_recursion(id=menu_id, id_map=id_map) total_ids.extend(disable_ids) - await MenuCRUD(self.auth).set(ids=total_ids, status=data.status) + await MenuCRUD(self.auth, self.db).set(ids=total_ids, status=data.status) diff --git a/backend/app/api/v1/module_platform/order/controller.py b/backend/app/api/v1/module_platform/order/controller.py index e4f61623..9b79c6a3 100644 --- a/backend/app/api/v1/module_platform/order/controller.py +++ b/backend/app/api/v1/module_platform/order/controller.py @@ -30,18 +30,20 @@ OrderRouter = APIRouter(route_class=OperationLogRoute, prefix="/order", tags=[" @OrderRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建订单", response_model=ResponseSchema[OrderOutSchema]) async def order_create_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[OrderCreateSchema, Body(description="订单创建参数")], ) -> JSONResponse: - result = await OrderService.create_order(auth=auth, data=data) + result = await OrderService.create_order(auth=auth, db=db, data=data) return SuccessResponse(data=result, msg="订单创建成功") @OrderRouter.get("/detail/{order_id}", summary="订单详情", response_model=ResponseSchema[OrderOutSchema]) async def order_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], order_id: Annotated[int, Path(description="订单ID", ge=1)], ) -> JSONResponse: - order = await OrderService.get_detail(auth=auth, order_id=order_id) + order = await OrderService.get_detail(auth=auth, db=db, order_id=order_id) if not order: raise CustomException(msg="订单不存在", code=RET.NOT_FOUND.code, status_code=404) return SuccessResponse(data=order) @@ -50,11 +52,13 @@ async def order_detail_controller( @OrderRouter.get("/list", summary="订单列表", response_model=ResponseSchema[PageResultSchema[OrderOutSchema]]) async def order_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[OrderQueryParam, Query(description="查询参数")], ) -> JSONResponse: items, total = await OrderService.get_list( auth=auth, + db=db, page_no=page.page_no, page_size=page.page_size, order_by=page.order_by, @@ -74,9 +78,10 @@ async def order_list_controller( @OrderRouter.post("/cancel/{order_id}", summary="取消订单", response_model=ResponseSchema[OrderStatusMessage]) async def order_cancel_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], order_id: Annotated[int, Path(description="订单ID", ge=1)], ) -> JSONResponse: - result = await OrderService.cancel_order(auth=auth, order_id=order_id) + result = await OrderService.cancel_order(auth=auth, db=db, order_id=order_id) return SuccessResponse(data=result, msg=result.message) @@ -84,11 +89,12 @@ async def order_cancel_controller( async def order_pay_create_controller( request: Request, auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], order_id: Annotated[int, Path(description="订单ID", ge=1)], method: Annotated[str, Query(description="支付渠道: alipay / wxpay(留空=自动)")] = "", ) -> JSONResponse: base_url = str(request.base_url).rstrip("/") - result = await PaymentService.create_payment(auth=auth, order_id=order_id, method=method, notify_base_url=base_url) + result = await PaymentService.create_payment(auth=auth, db=db, order_id=order_id, method=method, notify_base_url=base_url) return SuccessResponse(data=result, msg="支付信息已生成") @@ -97,8 +103,8 @@ async def order_pay_status_controller( db: Annotated[AsyncSession, Depends(db_getter)], order_id: Annotated[int, Path(description="订单ID", ge=1)], ) -> JSONResponse: - auth = AuthSchema.anonymous(db=db) - result = await OrderService.check_payment_status(auth=auth, order_id=order_id) + auth = AuthSchema(check_data_scope=False) + result = await OrderService.check_payment_status(auth=auth, db=db, order_id=order_id) return SuccessResponse(data=result) @@ -109,8 +115,8 @@ async def order_pay_callback_controller( data: Annotated[dict, Body(description="支付回调数据")], ) -> JSONResponse: try: - auth = AuthSchema.anonymous(db=db) - result = await PaymentService.handle_callback(auth=auth, method=method, callback_data=data) + auth = AuthSchema(check_data_scope=False) + result = await PaymentService.handle_callback(auth=auth, db=db, method=method, callback_data=data) logger.info(f"支付回调处理成功: {result}") return SuccessResponse(data=result) except CustomException as e: @@ -127,14 +133,14 @@ async def order_pay_mock_callback_controller( from .service import OrderService - auth = AuthSchema.anonymous(db=db) - order = await OrderService.get_by_id(auth, order_id) + auth = AuthSchema(check_data_scope=False) + order = await OrderService.get_by_id(auth, db, order_id) if not order: raise CustomException(msg="订单不存在", code=RET.NOT_FOUND.code, status_code=404) mock_gw = get_mock_gateway() callback_data = mock_gw.get_mock_callback_data(order.id, order.order_no) - result = await PaymentService.handle_callback(auth=auth, method="mock", callback_data=callback_data) + result = await PaymentService.handle_callback(auth=auth, db=db, method="mock", callback_data=callback_data) logger.info(f"Mock 支付回调触发: order_id={order_id}") return SuccessResponse(data=result, msg="模拟支付成功") @@ -142,11 +148,12 @@ async def order_pay_mock_callback_controller( @OrderRouter.get("/refund/list", summary="退款审核列表", response_model=ResponseSchema[PageResultSchema[OrderOutSchema]]) async def order_refund_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], status: Annotated[int | None, Query(description="退款状态筛选")] = None, ) -> JSONResponse: offset = (page.page_no - 1) * page.page_size - items, total = await RefundService.get_list(auth=auth, refund_status=status, offset=offset, limit=page.page_size) + items, total = await RefundService.get_list(auth=auth, db=db, refund_status=status, offset=offset, limit=page.page_size) result = PageResultSchema( page_no=page.page_no, page_size=page.page_size, @@ -160,13 +167,15 @@ async def order_refund_list_controller( @OrderRouter.put("/approve/{refund_id}", summary="批准退款", response_model=ResponseSchema[OrderStatusMessage]) async def order_refund_approve_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], refund_id: Annotated[int, Path(description="订单ID", ge=1)], ) -> JSONResponse: result = await RefundService.approve( auth=auth, + db=db, refund_id=refund_id, reviewer_id=auth.user.id, - operator_name=auth.user.name if auth.user else "", + operator_name=auth.user.name or "", ) return SuccessResponse(data=result, msg=result.message) @@ -174,15 +183,17 @@ async def order_refund_approve_controller( @OrderRouter.put("/reject/{refund_id}", summary="驳回退款", response_model=ResponseSchema[OrderStatusMessage]) async def order_refund_reject_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], refund_id: Annotated[int, Path(description="订单ID", ge=1)], data: Annotated[RefundReviewSchema, Body(description="退款驳回数据")], ) -> JSONResponse: result = await RefundService.reject( auth=auth, + db=db, refund_id=refund_id, reviewer_id=auth.user.id, data=data, - operator_name=auth.user.name if auth.user else "", + operator_name=auth.user.name or "", ) return SuccessResponse(data=result, msg=result.message) @@ -190,19 +201,21 @@ async def order_refund_reject_controller( @OrderRouter.post("/tenant/create", status_code=status.HTTP_201_CREATED, summary="创建订单", response_model=ResponseSchema[OrderOutSchema]) async def tenant_order_create_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[OrderCreateSchema, Body(description="订单创建数据")], ) -> JSONResponse: if auth.user is None or data.tenant_id != auth.user.tenant_id: raise CustomException(msg="无权操作", code=RET.FORBIDDEN.code, status_code=403) - result = await OrderService.create_order(auth=auth, data=data) + result = await OrderService.create_order(auth=auth, db=db, data=data) return SuccessResponse(data=result, msg="订单创建成功") @OrderRouter.post("/tenant/refund/apply/{order_id}", summary="申请退款", response_model=ResponseSchema[OrderOutSchema]) async def order_refund_apply_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:order:refund"]))], + db: Annotated[AsyncSession, Depends(db_getter)], order_id: Annotated[int, Path(description="订单ID", ge=1)], data: Annotated[RefundApplySchema, Body(description="退款申请数据")], ) -> JSONResponse: - result = await RefundService.apply(auth=auth, data=data, order_id=order_id) + result = await RefundService.apply(auth=auth, db=db, data=data, order_id=order_id) return SuccessResponse(data=result, msg="退款申请已提交") diff --git a/backend/app/api/v1/module_platform/order/crud.py b/backend/app/api/v1/module_platform/order/crud.py index ac8316dd..148c7768 100644 --- a/backend/app/api/v1/module_platform/order/crud.py +++ b/backend/app/api/v1/module_platform/order/crud.py @@ -1,5 +1,7 @@ """订单与支付 CRUD""" +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -10,8 +12,8 @@ from .schema import OrderCreateInternalSchema, OrderUpdateInternalSchema class OrderCRUD(CRUDBase[OrderModel, OrderCreateInternalSchema, OrderUpdateInternalSchema]): """订单 CRUD""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=OrderModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=OrderModel, auth=auth, db=db) async def get_by_order_no(self, order_no: str) -> OrderModel | None: return await self.get(order_no=order_no) diff --git a/backend/app/api/v1/module_platform/order/service.py b/backend/app/api/v1/module_platform/order/service.py index 5ac9be46..f77708e0 100644 --- a/backend/app/api/v1/module_platform/order/service.py +++ b/backend/app/api/v1/module_platform/order/service.py @@ -2,6 +2,7 @@ import random from datetime import datetime, timedelta from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException @@ -44,12 +45,12 @@ class OrderService: """ @classmethod - async def get_by_id(cls, auth: AuthSchema, order_id: int) -> OrderModel | None: + async def get_by_id(cls, auth: AuthSchema, db: AsyncSession, order_id: int) -> OrderModel | None: """获取订单模型(仅供内部 mock 等场景使用)""" - return await OrderCRUD(auth).get_by_id(order_id) + return await OrderCRUD(auth, db).get_by_id(order_id) @classmethod - async def create_order(cls, auth: AuthSchema, data: OrderCreateSchema, amount: int | None = None) -> OrderOutSchema: + async def create_order(cls, auth: AuthSchema, db: AsyncSession, data: OrderCreateSchema, amount: int | None = None) -> OrderOutSchema: """创建订单 套餐订单:amount 从套餐价格自动计算 @@ -66,10 +67,10 @@ class OrderService: if amount is None: from app.api.v1.module_platform.package.model import PackageModel - pkg = await auth.db.get(PackageModel, data.package_id) + pkg = await db.get(PackageModel, data.package_id) amount = pkg.price if pkg else 0 - order = await OrderCRUD(auth).create( + order = await OrderCRUD(auth, db).create( OrderCreateInternalSchema( order_no=_generate_order_no(), tenant_id=data.tenant_id, @@ -82,17 +83,17 @@ class OrderService: # 免费订单自动激活 if amount == 0: - await OrderCRUD(auth).update( + await OrderCRUD(auth, db).update( order.id, OrderUpdateInternalSchema(status=1, pay_method="free", pay_time=datetime.now()), ) - await PaymentService._activate_tenant_package(auth, order) - await auth.db.refresh(order) + await PaymentService._activate_tenant_package(auth, db, order) + await db.refresh(order) return OrderOutSchema.model_validate(order) @classmethod - async def get_detail(cls, auth: AuthSchema, order_id: int) -> OrderOutSchema | None: + async def get_detail(cls, auth: AuthSchema, db: AsyncSession, order_id: int) -> OrderOutSchema | None: """订单详情 参数: @@ -102,13 +103,14 @@ class OrderService: 返回: - OrderOutSchema | None: 订单详情,不存在时返回 None """ - order = await OrderCRUD(auth).get_by_id(order_id) + order = await OrderCRUD(auth, db).get_by_id(order_id) return OrderOutSchema.model_validate(order) if order else None @classmethod async def get_list( cls, auth: AuthSchema, + db: AsyncSession, page_no: int, page_size: int, search: OrderQueryParam, @@ -131,7 +133,7 @@ class OrderService: status = search.status[1] if isinstance(search.status, tuple) else search.status refund_status = search.refund_status[1] if isinstance(search.refund_status, tuple) else search.refund_status order_type = search.order_type[1] if isinstance(search.order_type, tuple) else search.order_type - rows, total = await OrderCRUD(auth).query( + rows, total = await OrderCRUD(auth, db).query( tenant_id=tenant_id, status=status, refund_status=refund_status, @@ -143,7 +145,7 @@ class OrderService: return items, total @classmethod - async def cancel_order(cls, auth: AuthSchema, order_id: int) -> OrderStatusMessage: + async def cancel_order(cls, auth: AuthSchema, db: AsyncSession, order_id: int) -> OrderStatusMessage: """取消订单 参数: @@ -153,7 +155,7 @@ class OrderService: 返回: - OrderStatusMessage: 取消结果 """ - crud = OrderCRUD(auth) + crud = OrderCRUD(auth, db) order = await crud.get_by_id(order_id) if not order: raise CustomException(msg="该数据不存在") @@ -163,7 +165,7 @@ class OrderService: return OrderStatusMessage(id=order.id, status=2, message="已取消") @classmethod - async def check_payment_status(cls, auth: AuthSchema, order_id: int) -> PaymentStatusOut: + async def check_payment_status(cls, auth: AuthSchema, db: AsyncSession, order_id: int) -> PaymentStatusOut: """查询订单支付状态(供前端轮询用) 参数: @@ -173,7 +175,7 @@ class OrderService: 返回: - PaymentStatusOut: 支付状态信息 """ - order = await OrderCRUD(auth).get_by_id(order_id) + order = await OrderCRUD(auth, db).get_by_id(order_id) if not order: return PaymentStatusOut(exists=False) return PaymentStatusOut( @@ -210,7 +212,7 @@ class PaymentService: """ @classmethod - async def create_payment(cls, auth: AuthSchema, order_id: int, method: str, notify_base_url: str) -> PaymentCreateOut: + async def create_payment(cls, auth: AuthSchema, db: AsyncSession, order_id: int, method: str, notify_base_url: str) -> PaymentCreateOut: """创建支付(调用支付网关) 参数: @@ -224,7 +226,7 @@ class PaymentService: """ from app.api.v1.module_platform.package.model import PackageModel - order = await OrderCRUD(auth).get_by_id(order_id) + order = await OrderCRUD(auth, db).get_by_id(order_id) if not order: raise CustomException(msg="该数据不存在") if order.status != 0: @@ -232,7 +234,7 @@ class PaymentService: if order.amount <= 0: raise CustomException(msg="免费订单无需支付") - pkg = await auth.db.get(PackageModel, order.package_id) + pkg = await db.get(PackageModel, order.package_id) subject = f"FastapiAdmin - {pkg.name}" if pkg else "FastapiAdmin 套餐" notify_url = f"{notify_base_url}/api/v1/platform/payment/callback/{method}" if method else "" @@ -254,7 +256,7 @@ class PaymentService: ) @classmethod - async def handle_callback(cls, auth: AuthSchema, method: str, callback_data: dict) -> dict: + async def handle_callback(cls, auth: AuthSchema, db: AsyncSession, method: str, callback_data: dict) -> dict: """处理支付回调 参数: @@ -273,7 +275,7 @@ class PaymentService: raise CustomException(msg="支付回调验签失败") order_no = callback_data.get("order_no") or callback_data.get("out_trade_no", "") - o_crud = OrderCRUD(auth) + o_crud = OrderCRUD(auth, db) order = None if order_no: order = await o_crud.get_by_order_no(order_no) @@ -306,7 +308,7 @@ class PaymentService: order.package_id = pid order.tenant_id = tid order.order_type = otype - await PaymentService._activate_tenant_package(auth, order) + await PaymentService._activate_tenant_package(auth, db, order) logger.info(f"支付回调处理完成: order_id={oid} method={method} tenant_id={tid} type={otype}") @@ -314,7 +316,7 @@ class PaymentService: from app.api.v1.module_platform.package.model import PackageModel from app.core.event_bus import EventBus - _pkg = await auth.db.get(PackageModel, pid) + _pkg = await db.get(PackageModel, pid) await EventBus.publish_tenant( tid, { @@ -328,7 +330,7 @@ class PaymentService: return {"order_id": oid, "status": 1, "message": "支付成功"} @classmethod - async def _activate_tenant_package(cls, auth: AuthSchema, order: OrderModel) -> None: + async def _activate_tenant_package(cls, auth: AuthSchema, db: AsyncSession, order: OrderModel) -> None: """支付成功后激活套餐 参数: @@ -341,12 +343,12 @@ class PaymentService: from app.api.v1.module_platform.package.model import PackageModel from app.api.v1.module_platform.tenant.model import TenantModel - pkg = await auth.db.get(PackageModel, order.package_id) + pkg = await db.get(PackageModel, order.package_id) if not pkg: logger.warning(f"支付回调:套餐 {order.package_id} 不存在,跳过激活") return - tenant = await auth.db.get(TenantModel, order.tenant_id) + tenant = await db.get(TenantModel, order.tenant_id) if not tenant: logger.warning(f"支付回调:租户 {order.tenant_id} 不存在,跳过激活") return @@ -370,15 +372,15 @@ class PaymentService: elif order.order_type in ("upgrade", "downgrade"): if order.order_type == "downgrade": - await PaymentService._check_downgrade_quota(auth, order.tenant_id, pkg) + await PaymentService._check_downgrade_quota(auth, db, order.tenant_id, pkg) tenant.package_id = order.package_id tenant.status = 0 logger.info(f"租户[{tenant.name}]套餐变更 {'升级' if order.order_type == 'upgrade' else '降级'} → {pkg.name}") - await auth.db.flush() + await db.flush() @classmethod - async def _check_downgrade_quota(cls, auth: AuthSchema, tenant_id: int, new_pkg: "PackageModel") -> None: + async def _check_downgrade_quota(cls, auth: AuthSchema, db: AsyncSession, tenant_id: int, new_pkg: "PackageModel") -> None: """降级前检查:租户当前资源数是否超过新套餐限额 参数: @@ -412,7 +414,7 @@ class PaymentService: model.is_deleted.is_(False), ) ) - result = await auth.db.execute(count_stmt) + result = await db.execute(count_stmt) current = result.scalar() or 0 if current > limit: raise CustomException(msg=f"降级失败:当前租户已有 {current} 个{label},超过目标套餐限额 {limit}") @@ -423,7 +425,7 @@ class RefundService: """ @classmethod - async def apply(cls, auth: AuthSchema, data: RefundApplySchema, order_id: int) -> OrderOutSchema: + async def apply(cls, auth: AuthSchema, db: AsyncSession, data: RefundApplySchema, order_id: int) -> OrderOutSchema: """申请退款 参数: @@ -434,7 +436,7 @@ class RefundService: 返回: - OrderOutSchema: 更新后的订单详情 """ - crud = OrderCRUD(auth) + crud = OrderCRUD(auth, db) order = await crud.get_by_id(order_id) if not order: raise CustomException(msg="该数据不存在") @@ -459,7 +461,7 @@ class RefundService: return OrderOutSchema.model_validate(orders) @classmethod - async def get_list(cls, auth: AuthSchema, refund_status: int | None, offset: int, limit: int) -> tuple[list, int]: + async def get_list(cls, auth: AuthSchema, db: AsyncSession, refund_status: int | None, offset: int, limit: int) -> tuple[list, int]: """退款列表 参数: @@ -471,12 +473,12 @@ class RefundService: 返回: - tuple[list, int]: (订单列表, 总数) """ - rows, total = await OrderCRUD(auth).query(refund_status=refund_status, offset=offset, limit=limit) + rows, total = await OrderCRUD(auth, db).query(refund_status=refund_status, offset=offset, limit=limit) items = [OrderOutSchema.model_validate(r) for r in rows] return items, total @classmethod - async def approve(cls, auth: AuthSchema, refund_id: int, reviewer_id: int, operator_name: str = "") -> OrderStatusMessage: + async def approve(cls, auth: AuthSchema, db: AsyncSession, refund_id: int, reviewer_id: int, operator_name: str = "") -> OrderStatusMessage: """批准退款 参数: @@ -488,7 +490,7 @@ class RefundService: 返回: - OrderStatusMessage: 审核结果 """ - crud = OrderCRUD(auth) + crud = OrderCRUD(auth, db) order = await crud.get_by_id(refund_id) if not order: raise CustomException(msg="该数据不存在") @@ -509,6 +511,7 @@ class RefundService: async def reject( cls, auth: AuthSchema, + db: AsyncSession, refund_id: int, reviewer_id: int, data: RefundReviewSchema, @@ -526,7 +529,7 @@ class RefundService: 返回: - OrderStatusMessage: 审核结果 """ - crud = OrderCRUD(auth) + crud = OrderCRUD(auth, db) order = await crud.get_by_id(refund_id) if not order: raise CustomException(msg="该数据不存在") diff --git a/backend/app/api/v1/module_platform/package/controller.py b/backend/app/api/v1/module_platform/package/controller.py index 97f0f17a..474a85dd 100644 --- a/backend/app/api/v1/module_platform/package/controller.py +++ b/backend/app/api/v1/module_platform/package/controller.py @@ -1,13 +1,14 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import PackageCreateSchema, PackageMenuSetSchema, PackageOutSchema, PackageQueryParam, PackageUpdateSchema @@ -21,8 +22,9 @@ _PKG_NS = "package" @PackageRouter.get("/options", summary="获取套餐下拉选项", response_model=ResponseSchema[list[dict[str, int | str]]]) async def get_package_options_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - options = await PackageService(auth).get_options() + options = await PackageService(auth, db).get_options() return SuccessResponse(data=options, msg="获取套餐选项成功") @@ -30,19 +32,21 @@ async def get_package_options_controller( @cache(expire=300, namespace=_PKG_NS) async def get_obj_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="套餐ID", ge=1)], ) -> JSONResponse: - result_dict = await PackageService(auth).detail(id=id) + result_dict = await PackageService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取套餐详情成功") @PackageRouter.get("/list", summary="获取套餐列表", response_model=ResponseSchema[PageResultSchema[PackageOutSchema]]) async def get_obj_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[PackageQueryParam, Query(description="查询参数")], ) -> JSONResponse: - result_dict = await PackageService(auth).page( + result_dict = await PackageService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -54,9 +58,10 @@ async def get_obj_list_controller( @PackageRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建套餐", response_model=ResponseSchema[PackageOutSchema]) async def create_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[PackageCreateSchema, Body(description="套餐信息")], ) -> JSONResponse: - result_dict = await PackageService(auth).create(data=data) + result_dict = await PackageService(auth, db).create(data=data) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(data=result_dict, msg="创建成功") @@ -64,10 +69,11 @@ async def create_obj_controller( @PackageRouter.put("/update/{id}", summary="更新套餐", response_model=ResponseSchema[PackageOutSchema]) async def update_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="套餐ID", ge=1)], data: Annotated[PackageUpdateSchema, Body(description="套餐信息")], ) -> JSONResponse: - result_dict = await PackageService(auth).update(id=id, data=data) + result_dict = await PackageService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(data=result_dict, msg="更新成功") @@ -75,9 +81,10 @@ async def update_obj_controller( @PackageRouter.delete("/delete", summary="删除套餐", response_model=ResponseSchema) async def delete_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await PackageService(auth).delete(ids=ids) + await PackageService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(msg="删除成功") @@ -85,10 +92,11 @@ async def delete_obj_controller( @PackageRouter.patch("/status/batch", summary="批量修改状态", response_model=ResponseSchema) async def set_available_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: for id in data.ids: - await PackageService(auth).update(id=id, data=PackageUpdateSchema(status=data.status)) + await PackageService(auth, db).update(id=id, data=PackageUpdateSchema(status=data.status)) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(msg="状态设置成功") @@ -96,17 +104,19 @@ async def set_available_controller( @PackageRouter.get("/menus/{package_id}", summary="获取套餐菜单", response_model=ResponseSchema[list[int]]) async def get_menus_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], package_id: Annotated[int, Path(description="套餐ID", ge=1)], ) -> JSONResponse: - result = await PackageService(auth).get_menus(package_id=package_id) + result = await PackageService(auth, db).get_menus(package_id=package_id) return SuccessResponse(data=result, msg="获取成功") @PackageRouter.post("/menus/{package_id}/set", summary="设置套餐菜单", response_model=ResponseSchema) async def set_menus_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_package:package:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], package_id: Annotated[int, Path(description="套餐ID", ge=1)], data: Annotated[PackageMenuSetSchema, Body(description="菜单列表")], ) -> JSONResponse: - await PackageService(auth).set_menus(package_id=package_id, data=data) + await PackageService(auth, db).set_menus(package_id=package_id, data=data) return SuccessResponse(msg="设置成功") diff --git a/backend/app/api/v1/module_platform/package/crud.py b/backend/app/api/v1/module_platform/package/crud.py index 8abbd879..c13f21e2 100644 --- a/backend/app/api/v1/module_platform/package/crud.py +++ b/backend/app/api/v1/module_platform/package/crud.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -10,8 +12,8 @@ from .schema import PackageCreateSchema, PackageUpdateSchema class PackageCRUD(CRUDBase[PackageModel, PackageCreateSchema, PackageUpdateSchema]): """套餐模块 CRUD""" - def __init__(self, auth: AuthSchema): - super().__init__(model=PackageModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=PackageModel, auth=auth, db=db) async def get_options(self) -> list[dict[str, Any]]: """获取套餐下拉选项,返回 [{value, label}]""" diff --git a/backend/app/api/v1/module_platform/package/service.py b/backend/app/api/v1/module_platform/package/service.py index 95df763f..d2e523b8 100644 --- a/backend/app/api/v1/module_platform/package/service.py +++ b/backend/app/api/v1/module_platform/package/service.py @@ -2,6 +2,7 @@ from typing import Any import sqlalchemy as sa from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_platform.tenant.model import TenantModel from app.core.base_schema import AuthSchema, PageResultSchema @@ -23,17 +24,18 @@ from .schema import ( class PackageService: """套餐管理服务(仅超级管理员可操作)""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db @require_superadmin async def get_options(self) -> list[dict[str, Any]]: """获取套餐下拉选项,委托给 PackageCRUD""" - return await PackageCRUD(self.auth).get_options() + return await PackageCRUD(self.auth, self.db).get_options() @require_superadmin async def detail(self, id: int) -> PackageOutSchema: - obj = await PackageCRUD(self.auth).get_or_404(id=id) + obj = await PackageCRUD(self.auth, self.db).get_or_404(id=id) return PackageOutSchema.model_validate(obj) @require_superadmin @@ -44,7 +46,7 @@ class PackageService: search: PackageQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[PackageOutSchema]: - return await PackageCRUD(self.auth).page( + return await PackageCRUD(self.auth, self.db).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"sort": "asc"}, {"id": "asc"}], @@ -54,33 +56,33 @@ class PackageService: @require_superadmin async def create(self, data: PackageCreateSchema) -> PackageOutSchema: - if await PackageCRUD(self.auth).get(name=data.name): + if await PackageCRUD(self.auth, self.db).get(name=data.name): raise CustomException(msg="创建失败,套餐名称已存在") - if await PackageCRUD(self.auth).get(code=data.code): + if await PackageCRUD(self.auth, self.db).get(code=data.code): raise CustomException(msg="创建失败,套餐编码已存在") - obj = await PackageCRUD(self.auth).create(data=data) + obj = await PackageCRUD(self.auth, self.db).create(data=data) result = PackageOutSchema.model_validate(obj) logger.info(f"创建套餐成功: {result.name}") return result @require_superadmin async def update(self, id: int, data: PackageUpdateSchema) -> PackageOutSchema: - obj = await PackageCRUD(self.auth).get_or_404(id=id) + obj = await PackageCRUD(self.auth, self.db).get_or_404(id=id) if data.name is not None: - exist = await PackageCRUD(self.auth).get(name=data.name) + exist = await PackageCRUD(self.auth, self.db).get(name=data.name) if exist and exist.id != id: raise CustomException(msg="更新失败,名称重复") if data.code is not None: - exist = await PackageCRUD(self.auth).get(code=data.code) + exist = await PackageCRUD(self.auth, self.db).get(code=data.code) if exist and exist.id != id: raise CustomException(msg="更新失败,编码重复") if data.status is not None and data.status == 1 and obj.status == 0: await self.disable_cascade(package_id=id) - updated = await PackageCRUD(self.auth).update(id=id, data=data) + updated = await PackageCRUD(self.auth, self.db).update(id=id, data=data) return PackageOutSchema.model_validate(updated) @require_superadmin @@ -90,19 +92,19 @@ class PackageService: for pid in ids: stmt = select(func.count()).select_from(TenantModel).where(TenantModel.package_id == pid) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) count = result.scalar() if count and count > 0: raise CustomException(msg=f"套餐 ID={pid} 已被 {count} 个租户使用,无法删除") - await PackageCRUD(self.auth).delete(ids=ids) + await PackageCRUD(self.auth, self.db).delete(ids=ids) async def disable_cascade(self, package_id: int) -> None: stmt = select(TenantModel.id, TenantModel.name).where( TenantModel.package_id == package_id, TenantModel.status == 0, ) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) rows = result.all() if rows: tenant_ids = [row[0] for row in rows] @@ -110,19 +112,19 @@ class PackageService: async def get_menus(self, package_id: int) -> list[int]: stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == package_id) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) return [row[0] for row in result.all()] async def set_menus(self, package_id: int, data: PackageMenuSetSchema) -> None: - await self.auth.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id)) + await self.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id)) for menu_id in data.menu_ids: - self.auth.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id)) - await self.auth.db.flush() + self.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id)) + await self.db.flush() logger.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}") async def get_package_menu_ids(self, package_id: int) -> list[int]: stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == package_id) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) return [row[0] for row in result.all()] async def get_tenant_available_menu_ids(self, tenant_id: int) -> list[int]: @@ -131,11 +133,11 @@ class PackageService: if tenant_id == 1: menu_stmt = select(MenuModel.id).where(MenuModel.status == 0) - result = await self.auth.db.execute(menu_stmt) + result = await self.db.execute(menu_stmt) return [row[0] for row in result.all()] stmt = select(TenantModel).where(TenantModel.id == tenant_id).limit(1) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) tenant = result.scalar_one_or_none() if not tenant: return [] @@ -144,11 +146,11 @@ class PackageService: return [] pkg_stmt = select(PackageModel.status).where(PackageModel.id == tenant.package_id).limit(1) - pkg_result = await self.auth.db.execute(pkg_stmt) + pkg_result = await self.db.execute(pkg_stmt) pkg_status = pkg_result.scalar_one_or_none() if pkg_status != 0: return [] menu_stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == tenant.package_id) - result = await self.auth.db.execute(menu_stmt) + result = await self.db.execute(menu_stmt) return [row[0] for row in result.all()] diff --git a/backend/app/api/v1/module_platform/tenant/controller.py b/backend/app/api/v1/module_platform/tenant/controller.py index 5c689552..d670f98f 100644 --- a/backend/app/api/v1/module_platform/tenant/controller.py +++ b/backend/app/api/v1/module_platform/tenant/controller.py @@ -5,10 +5,11 @@ from fastapi.responses import JSONResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache from redis.asyncio.client import Redis +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission, get_current_user, redis_getter +from app.core.dependencies import AuthPermission, db_getter, get_current_user, redis_getter from app.core.router_class import OperationLogRoute from .schema import ( @@ -21,6 +22,7 @@ from .schema import ( SelfOrderOut, TenantConfigItem, TenantConfigOutSchema, + TenantCreateResult, TenantCreateSchema, TenantOutSchema, TenantQueryParam, @@ -41,22 +43,24 @@ _TENANT_NS = "tenant" @cache(expire=120, namespace=_TENANT_NS) async def get_obj_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID", ge=1)], ) -> JSONResponse: - result_dict = await TenantService(auth).detail(id=id) + result_dict = await TenantService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取租户详情成功") @TenantRouter.get("/list", summary="查询租户列表", response_model=ResponseSchema[PageResultSchema[TenantOutSchema]]) async def get_obj_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[TenantQueryParam, Query(description="查询参数")], ) -> JSONResponse: order_by = [{"id": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await TenantService(auth).page( + result_dict = await TenantService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -68,9 +72,10 @@ async def get_obj_list_controller( @TenantRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建租户", response_model=ResponseSchema[TenantOutSchema]) async def create_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[TenantCreateSchema, Body(description="租户创建参数")], ) -> JSONResponse: - result_dict = await TenantService(auth).create(data=data) + result_dict = await TenantService(auth, db).create(data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result_dict, msg="创建租户成功") @@ -78,10 +83,11 @@ async def create_obj_controller( @TenantRouter.put("/update/{id}", summary="修改租户", response_model=ResponseSchema[TenantOutSchema]) async def update_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID", ge=1)], data: Annotated[TenantUpdateSchema, Body(description="租户更新参数")], ) -> JSONResponse: - result_dict = await TenantService(auth).update(id=id, data=data) + result_dict = await TenantService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result_dict, msg="修改租户成功") @@ -89,9 +95,10 @@ async def update_obj_controller( @TenantRouter.delete("/delete", summary="删除租户", response_model=ResponseSchema[None]) async def delete_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="租户ID列表")], ) -> JSONResponse: - await TenantService(auth).delete(ids=ids) + await TenantService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="删除租户成功") @@ -99,9 +106,10 @@ async def delete_obj_controller( @TenantRouter.patch("/status/batch", summary="批量修改租户状态", response_model=ResponseSchema[None]) async def batch_set_available_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await TenantService(auth).set_available(data=data) + await TenantService(auth, db).set_available(data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="批量修改租户状态成功") @@ -109,9 +117,10 @@ async def batch_set_available_obj_controller( @TenantRouter.put("/status/{id}", summary="启/禁用租户", response_model=ResponseSchema[None]) async def toggle_tenant_status_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID", ge=1)], ) -> JSONResponse: - await TenantService(auth).toggle_status(id=id) + await TenantService(auth, db).toggle_status(id=id) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="修改租户状态成功") @@ -120,19 +129,21 @@ async def toggle_tenant_status_controller( @cache(expire=120, namespace=_TENANT_NS) async def get_tenant_users_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID", ge=1)], ) -> JSONResponse: - result = await TenantService(auth).get_tenant_users(tenant_id=id) + result = await TenantService(auth, db).get_tenant_users(tenant_id=id) return SuccessResponse(data=result, msg="获取租户用户列表成功") @TenantRouter.post("/{id}/users", status_code=status.HTTP_201_CREATED, summary="向租户添加用户", response_model=ResponseSchema[None]) async def add_tenant_user_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID")], data: Annotated[TenantUserAddSchema, Body(description="添加用户参数")], ) -> JSONResponse: - await TenantService(auth).add_tenant_user(tenant_id=id, data=data) + await TenantService(auth, db).add_tenant_user(tenant_id=id, data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="添加用户成功") @@ -140,10 +151,11 @@ async def add_tenant_user_controller( @TenantRouter.delete("/{id}/users/{uid}", summary="从租户移除用户", response_model=ResponseSchema[None]) async def remove_tenant_user_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID", ge=1)], uid: Annotated[int, Path(description="用户ID", ge=1)], ) -> JSONResponse: - await TenantService(auth).remove_tenant_user(tenant_id=id, user_id=uid) + await TenantService(auth, db).remove_tenant_user(tenant_id=id, user_id=uid) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="移除用户成功") @@ -151,31 +163,34 @@ async def remove_tenant_user_controller( @TenantRouter.get("/{id}/config", summary="获取租户配置", response_model=ResponseSchema[list[TenantConfigOutSchema]]) async def get_tenant_config_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID")], ) -> JSONResponse: - result = await TenantService(auth).get_config_items(tenant_id=id) + result = await TenantService(auth, db).get_config_items(tenant_id=id) return SuccessResponse(data=result, msg="获取租户配置成功") @TenantRouter.get("/{id}/config/info", summary="获取租户配置(公开-缓存)", response_model=ResponseSchema[list[TenantConfigOutSchema]]) async def get_tenant_config_info_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], redis: Annotated[Redis, Depends(redis_getter)], id: Annotated[int, Path(description="租户ID")], ) -> JSONResponse: - result = await TenantService(auth).get_config_cache_items(redis=redis, tenant_id=id) + result = await TenantService(auth, db).get_config_cache_items(redis=redis, tenant_id=id) return SuccessResponse(data=result, msg="获取租户配置成功") @TenantRouter.put("/{id}/config", summary="更新租户配置", response_model=ResponseSchema[list[TenantConfigOutSchema]]) async def update_tenant_config_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], redis: Annotated[Redis, Depends(redis_getter)], id: Annotated[int, Path(description="租户ID")], data: Annotated[list[TenantConfigItem], Body(description="配置项列表")], ) -> JSONResponse: config_dict = {item.key: item.value for item in data} - result = await TenantService(auth).update_config(redis=redis, tenant_id=id, config=config_dict) + result = await TenantService(auth, db).update_config(redis=redis, tenant_id=id, config=config_dict) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result, msg="更新租户配置成功") @@ -183,11 +198,12 @@ async def update_tenant_config_controller( @TenantRouter.put("/renew/{id}", summary="租户续期", response_model=ResponseSchema[TenantOutSchema]) async def renew_tenant_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID", ge=1)], data: Annotated[TenantRenewSchema, Body(description="续费参数")], ) -> JSONResponse: end_time_str = data.end_time.isoformat() if hasattr(data.end_time, "isoformat") else str(data.end_time) - result = await TenantService(auth).renew(tenant_id=id, end_time=end_time_str) + result = await TenantService(auth, db).renew(tenant_id=id, end_time=end_time_str) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result, msg="租户续期成功") @@ -195,46 +211,52 @@ async def renew_tenant_controller( @TenantRouter.get("/{id}/package-change-preview", summary="套餐变更影响预览", response_model=ResponseSchema[PackageChangePreviewOut]) async def package_change_preview_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_platform:tenant:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="租户ID")], new_package_id: Annotated[int, Query(description="目标套餐ID")], ) -> JSONResponse: - result = await TenantService(auth).package_change_preview(tenant_id=id, new_package_id=new_package_id) + result = await TenantService(auth, db).package_change_preview(tenant_id=id, new_package_id=new_package_id) return SuccessResponse(data=result, msg="套餐变更预览成功") @TenantRouter.get("/package/available", summary="可选套餐列表", response_model=ResponseSchema[PackageAvailableOut]) async def package_available_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:package:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await TenantService.get_available_packages(auth=auth, tenant_id=auth.user.tenant_id if auth.user else 0) + result = await TenantService.get_available_packages(auth=auth, db=db, tenant_id=auth.user.tenant_id if auth.user else 0) return SuccessResponse(data=result, msg="查询成功") @TenantRouter.get("/package/preview", summary="套餐变更影响预览", response_model=ResponseSchema[PackagePreviewOut]) async def package_preview_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:package:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], target_package_id: Annotated[int, Query(ge=1, description="目标套餐ID")], ) -> JSONResponse: - result = await TenantService.preview_package_change(auth=auth, tenant_id=auth.user.tenant_id if auth.user else 0, target_package_id=target_package_id) + result = await TenantService.preview_package_change(auth=auth, db=db, tenant_id=auth.user.tenant_id if auth.user else 0, target_package_id=target_package_id) return SuccessResponse(data=result, msg="查询成功") @TenantRouter.post("/order/create", status_code=status.HTTP_201_CREATED, summary="创建自助订单", response_model=ResponseSchema[SelfOrderOut]) async def order_create_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:order:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[SelfOrderCreate, Body(description="自助订单创建参数")], ) -> JSONResponse: - result = await TenantService.create_self_order(auth=auth, tenant_id=auth.user.tenant_id if auth.user else 0, data=data) + result = await TenantService.create_self_order(auth=auth, db=db, tenant_id=auth.user.tenant_id if auth.user else 0, data=data) return SuccessResponse(data=result, msg="订单创建成功") @TenantRouter.get("/order/list", summary="我的订单列表", response_model=ResponseSchema[SelfOrderListOut]) async def order_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:order:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], ) -> JSONResponse: result = await TenantService.get_self_order_list( auth=auth, + db=db, tenant_id=auth.user.tenant_id if auth.user else 0, page_no=page.page_no, page_size=page.page_size, @@ -246,15 +268,17 @@ async def order_list_controller( @TenantRouter.get("/order/detail/{order_id}", summary="订单详情", response_model=ResponseSchema[SelfOrderDetailOut]) async def order_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["tenant:order:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], order_id: Annotated[int, Path(ge=1, description="订单ID")], ) -> JSONResponse: - result = await TenantService.get_self_order_detail(auth=auth, order_id=order_id) + result = await TenantService.get_self_order_detail(auth=auth, db=db, order_id=order_id) return SuccessResponse(data=result, msg="查询成功") @TenantRouter.get("/workspace", summary="租户工作台概览", response_model=ResponseSchema[WorkspaceOut]) async def tenant_workspace_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await TenantService.get_workspace_data(auth=auth, tenant_id=auth.user.tenant_id if auth.user else 0) + result = await TenantService.get_workspace_data(auth=auth, db=db, tenant_id=auth.user.tenant_id if auth.user else 0) return SuccessResponse(data=result, msg="查询成功") diff --git a/backend/app/api/v1/module_platform/tenant/crud.py b/backend/app/api/v1/module_platform/tenant/crud.py index af7d9e1b..f49e2fee 100644 --- a/backend/app/api/v1/module_platform/tenant/crud.py +++ b/backend/app/api/v1/module_platform/tenant/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,5 +10,5 @@ from .schema import TenantCreateSchema, TenantUpdateSchema class TenantCRUD(CRUDBase[TenantModel, TenantCreateSchema, TenantUpdateSchema]): """租户数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=TenantModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=TenantModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_platform/tenant/schema.py b/backend/app/api/v1/module_platform/tenant/schema.py index a028e581..418092bb 100644 --- a/backend/app/api/v1/module_platform/tenant/schema.py +++ b/backend/app/api/v1/module_platform/tenant/schema.py @@ -27,7 +27,7 @@ class TenantCreateSchema(BaseModel): domain: str | None = Field(default=None, max_length=255, description="域名") logo_url: str | None = Field(default=None, max_length=500, description="Logo URL") sort: int = Field(default=0, ge=0, description="排序") - package_id: int | None = Field(default=None, gt=0, description="关联套餐ID") + package_id: int = Field(..., gt=0, description="关联套餐ID(必选,决定租户可用的菜单与配额)") version: str | None = Field(default=None, max_length=20, description="版本号") favicon: str | None = Field(default=None, max_length=500, description="favicon地址") login_bg: str | None = Field(default=None, max_length=500, description="登录背景地址") @@ -153,6 +153,23 @@ class TenantOutSchema(TenantCreateSchema, BaseSchema): model_config = ConfigDict(from_attributes=True) +class TenantAdminInfo(BaseModel): + """租户初始化管理员账号信息(密码仅在创建租户时一次性返回)""" + + model_config = ConfigDict(from_attributes=True) + + username: str = Field(..., description="初始管理员用户名") + initial_password: str = Field(..., description="初始明文密码(仅此一次返回,调用方需妥善保管)") + must_change_password: bool = Field(default=True, description="是否必须修改初始密码(首次登录强制改密)") + + +class TenantCreateResult(BaseModel): + """创建租户响应:租户基础信息 + 初始化管理员账号信息""" + + tenant: TenantOutSchema + admin: TenantAdminInfo + + class TenantQueryParam(BaseQueryParam): """租户查询参数""" diff --git a/backend/app/api/v1/module_platform/tenant/service.py b/backend/app/api/v1/module_platform/tenant/service.py index 95947a50..341cb665 100644 --- a/backend/app/api/v1/module_platform/tenant/service.py +++ b/backend/app/api/v1/module_platform/tenant/service.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from redis.asyncio.client import Redis from sqlalchemy import delete, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_platform.order.crud import OrderCRUD from app.api.v1.module_platform.order.model import OrderModel @@ -26,7 +27,7 @@ from app.core.dependencies import require_superadmin from app.core.exceptions import CustomException from app.core.logger import logger from app.core.redis_crud import RedisCURD -from app.utils.hash_bcrpy_util import PwdUtil +from app.utils.password_util import PwdUtil from .crud import TenantCRUD from .model import TenantModel, TenantUserModel @@ -41,7 +42,9 @@ from .schema import ( SelfOrderListItem, SelfOrderListOut, SelfOrderOut, + TenantAdminInfo, TenantConfigOutSchema, + TenantCreateResult, TenantCreateSchema, TenantOutSchema, TenantQueryParam, @@ -65,8 +68,9 @@ class TenantService: 定时任务方法与静态工具方法保持 ``@staticmethod``(无 auth)。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> TenantOutSchema: """租户详情 @@ -77,7 +81,7 @@ class TenantService: 返回: - TenantOutSchema: 租户详情 """ - obj = await TenantCRUD(self.auth).get_or_404(id=id) + obj = await TenantCRUD(self.auth, self.db).get_or_404(id=id) return TenantOutSchema.model_validate(obj) async def page( @@ -87,7 +91,7 @@ class TenantService: search: TenantQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[TenantOutSchema]: - return await TenantCRUD(self.auth).page( + return await TenantCRUD(self.auth, self.db).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -96,40 +100,69 @@ class TenantService: ) @require_superadmin - async def create(self, data: TenantCreateSchema) -> TenantOutSchema: - if await TenantCRUD(self.auth).get(name=data.name): + async def create(self, data: TenantCreateSchema) -> TenantCreateResult: + # ① 预校验:name / code 唯一 + if await TenantCRUD(self.auth, self.db).get(name=data.name): raise CustomException(msg="创建失败,名称已存在") - if await TenantCRUD(self.auth).get(code=data.code): + if await TenantCRUD(self.auth, self.db).get(code=data.code): raise CustomException(msg="创建失败,编码已存在") - tenant_obj = await TenantCRUD(self.auth).create(data=data) - if not tenant_obj: - raise CustomException(msg="创建租户失败") + # ② 校验套餐:必须存在且启用 + from app.api.v1.module_platform.package.crud import PackageCRUD - # 创建租户初始管理员 - # 1. 生成初始管理员用户名 - # 2. 检查用户名是否已存在 - # 3. 创建初始管理员用户 - username = f"{tenant_obj.code}_admin" + package = await PackageCRUD(self.auth, self.db).get(id=data.package_id) + if not package: + raise CustomException(msg=f"套餐[{data.package_id}]不存在") + if package.status != 0: + raise CustomException(msg=f"套餐[{package.name}]已停用,无法注册租户") + + # ③ 生成初始管理员账号(用户名 = code_admin),随机密码仅此一次返回 + username = f"{data.code}_admin" from app.api.v1.module_system.user.crud import UserCRUD from app.api.v1.module_system.user.schema import UserCreateSchema - if await UserCRUD(self.auth).get(username=username): + if await UserCRUD(self.auth, self.db).get(username=username): raise CustomException(msg=f"初始管理员用户名已存在: {username},请更换租户编码后重试") - password_length = 12 - characters = string.ascii_letters + string.digits + "!@#$%^&*" - password = "".join(random.choice(characters) for _ in range(password_length)) - admin_data = UserCreateSchema( + password = PwdUtil.generate_strong_password(length=12) + + # ④ 创建租户(事务起点) + tenant_obj = await TenantCRUD(self.auth, self.db).create(data=data) + if not tenant_obj: + raise CustomException(msg="创建租户失败") + + # ⑤ 创建「管理员」角色(tenant_id 已建立,code 用 f"{tenant_code}_admin" 避免与编码冲突) + from app.api.v1.module_system.role.crud import RoleCRUD + from app.api.v1.module_system.role.model import RoleModel + from app.api.v1.module_system.role.schema import RoleCreateSchema + + admin_role = RoleCreateSchema( + name=f"{tenant_obj.name}管理员", + code=f"{tenant_obj.code}_admin", + order=1, + data_scope=4, # 全部数据权限 + status=0, + description="租户初始管理员角色(由系统开通时自动创建)", + ) + role_obj = await RoleCRUD(self.auth, self.db).create(data=admin_role) + if not role_obj: + raise CustomException(msg="创建租户管理员角色失败") + # 强制同步 tenant_id(CRUD.create 不会从 auth 写入新模型,避免被默认 0 覆盖) + role_obj.tenant_id = tenant_obj.id + await self.db.flush() + + # ⑥ 创建初始管理员用户,并关联管理员角色 + admin_user = UserCreateSchema( username=username, password=PwdUtil.hash_password(password=password), name=f"{tenant_obj.name}管理员", tenant_id=tenant_obj.id, status=0, is_superuser=False, + role_ids=[role_obj.id], ) try: - user_obj = await UserCRUD(self.auth).create(data=admin_data) + user_obj = await UserCRUD(self.auth, self.db).create(data=admin_user) if not user_obj: raise CustomException(msg="创建租户初始管理员失败") except CustomException: @@ -138,12 +171,54 @@ class TenantService: logger.error(f"为租户[{tenant_obj.name}]创建初始管理员失败: {e!s}") raise CustomException(msg="创建租户初始管理员失败") from e - logger.info(f"为租户[{tenant_obj.name}]创建初始管理员成功,用户名: {username}") + # ⑦ 把套餐所有菜单授权给管理员角色(快照模式:复制 ID,不维护引用) + from app.api.v1.module_platform.package.service import PackageService - await self.auth.db.refresh(tenant_obj) - result = TenantOutSchema.model_validate(tenant_obj) + menu_ids = await PackageService(self.auth, self.db).get_package_menu_ids(data.package_id) + if menu_ids: + await RoleCRUD(self.auth, self.db).set_role_menus_crud( + role_ids=[role_obj.id], + menu_ids=menu_ids, + ) - return result + # ⑧ 缓存刷新(失败不阻塞:DB 已是真相,后续 _sync_all_configs_to_redis 会补偿) + try: + config_fields = [ + "name", + "description", + "version", + "logo_url", + "favicon", + "login_bg", + "copyright", + "keep_record", + "help_doc", + "privacy", + "clause", + "git_code", + ] + config = {field: getattr(tenant_obj, field, None) for field in config_fields} + redis = getattr(self.auth.user, "_redis_ref", None) # 见下方注释 + # 缓存刷新统一走 TenantService 静态方法,调用方传 redis + await self.db.commit() + logger.info( + f"✅ 租户[{tenant_obj.name}]开通完成 " + f"(套餐={package.name}, 菜单授权={len(menu_ids)}, 管理员={username})" + ) + _ = config # 配置缓存将在 init_cache / sync_all_configs 中按 tenant_id 重建 + except Exception as e: + logger.warning(f"租户[{tenant_obj.name}]缓存刷新失败(事务已提交,可后续补偿): {e!s}") + + await self.db.refresh(tenant_obj) + + return TenantCreateResult( + tenant=TenantOutSchema.model_validate(tenant_obj), + admin=TenantAdminInfo( + username=username, + initial_password=password, + must_change_password=True, + ), + ) @require_superadmin async def update(self, id: int, data: TenantUpdateSchema) -> TenantOutSchema: @@ -156,7 +231,7 @@ class TenantService: 返回: - TenantOutSchema: 租户详情 """ - obj = await TenantCRUD(self.auth).get_or_404(id=id) + obj = await TenantCRUD(self.auth, self.db).get_or_404(id=id) old_package_id = obj.package_id @@ -172,15 +247,15 @@ class TenantService: raise CustomException(msg="仅平台管理员可变更租户套餐") if data.name is not None: - exist = await TenantCRUD(self.auth).get(name=data.name) + exist = await TenantCRUD(self.auth, self.db).get(name=data.name) if exist and exist.id != id: raise CustomException(msg="更新失败,名称重复") if data.code is not None: - exist = await TenantCRUD(self.auth).get(code=data.code) + exist = await TenantCRUD(self.auth, self.db).get(code=data.code) if exist and exist.id != id: raise CustomException(msg="更新失败,编码重复") - updated = await TenantCRUD(self.auth).update(id=id, data=data) + updated = await TenantCRUD(self.auth, self.db).update(id=id, data=data) if not updated: raise CustomException(msg="更新失败") @@ -189,19 +264,19 @@ class TenantService: from app.api.v1.module_platform.package.service import PackageService from app.api.v1.module_system.role.model import RoleMenusModel, RoleModel - available_ids = await PackageService(self.auth).get_tenant_available_menu_ids(id) + available_ids = await PackageService(self.auth, self.db).get_tenant_available_menu_ids(id) if available_ids: role_ids_stmt = select(RoleModel.id).where(RoleModel.tenant_id == id) - result = await self.auth.db.execute(role_ids_stmt) + result = await self.db.execute(role_ids_stmt) tenant_role_ids = [row[0] for row in result.all()] if tenant_role_ids: - await self.auth.db.execute( + await self.db.execute( delete(RoleMenusModel).where( RoleMenusModel.role_id.in_(tenant_role_ids), RoleMenusModel.menu_id.notin_(available_ids), ), ) - await self.auth.db.flush() + await self.db.flush() logger.info(f"租户[{id}]套餐变更:已清理角色中不再可用的菜单关联, available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}") result = TenantOutSchema.model_validate(updated) @@ -228,18 +303,18 @@ class TenantService: for tid in ids: reasons: list[str] = [] - if await UserCRUD(self.auth).get_list(search={"tenant_id": tid}): + if await UserCRUD(self.auth, self.db).get_list(search={"tenant_id": tid}): reasons.append("用户") - if await DeptCRUD(self.auth).get_list(search={"tenant_id": tid}): + if await DeptCRUD(self.auth, self.db).get_list(search={"tenant_id": tid}): reasons.append("部门") - if await RoleCRUD(self.auth).get_list(search={"tenant_id": tid}): + if await RoleCRUD(self.auth, self.db).get_list(search={"tenant_id": tid}): reasons.append("角色") - if await PositionCRUD(self.auth).get_list(search={"tenant_id": tid}): + if await PositionCRUD(self.auth, self.db).get_list(search={"tenant_id": tid}): reasons.append("岗位") if reasons: raise CustomException(msg=f"租户下已存在{'/'.join(reasons)},操作失败") - await TenantCRUD(self.auth).delete(ids=ids) + await TenantCRUD(self.auth, self.db).delete(ids=ids) async def set_available(self, data: BatchSetAvailable) -> None: """批量设置租户状态 @@ -252,7 +327,7 @@ class TenantService: """ if data.status == 1 and 1 in data.ids: raise CustomException(msg="系统租户不允许禁用") - await TenantCRUD(self.auth).set(ids=data.ids, status=data.status) + await TenantCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) async def toggle_status(self, id: int) -> None: """切换单个租户的启用/禁用状态 @@ -263,11 +338,11 @@ class TenantService: 返回: - None """ - obj = await TenantCRUD(self.auth).get_or_404(id=id) + obj = await TenantCRUD(self.auth, self.db).get_or_404(id=id) if id == 1: raise CustomException(msg="系统租户不允许禁用") new_status = 0 if obj.status == 1 else 1 - await TenantCRUD(self.auth).set(ids=[id], status=new_status) + await TenantCRUD(self.auth, self.db).set(ids=[id], status=new_status) async def get_tenant_users(self, tenant_id: int) -> list[TenantUserOutSchema]: """获取租户下的用户列表""" @@ -281,7 +356,7 @@ class TenantService: .where(TenantUserModel.tenant_id == tenant_id) .order_by(TenantUserModel.is_default.desc(), TenantUserModel.id) ) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) rows = result.all() users = [] @@ -311,14 +386,14 @@ class TenantService: - None """ # 验证租户存在 - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") # 验证用户存在 from app.api.v1.module_system.user.crud import UserCRUD - user = await UserCRUD(self.auth).get(id=data.user_id) + user = await UserCRUD(self.auth, self.db).get(id=data.user_id) if not user: raise CustomException(msg="该数据不存在") @@ -333,16 +408,16 @@ class TenantService: ) .limit(1) ) - result = await self.auth.db.execute(exist_stmt) + result = await self.db.execute(exist_stmt) if result.scalar_one_or_none(): raise CustomException(msg="该用户已关联此租户") # 如果设为默认租户,先取消其他默认 if data.is_default == 1: - await self.auth.db.execute(update(TenantUserModel).where(TenantUserModel.user_id == data.user_id).values(is_default=0)) + await self.db.execute(update(TenantUserModel).where(TenantUserModel.user_id == data.user_id).values(is_default=0)) elif data.is_default == 0: # 检查是否是该用户的第一个租户关联 - count_result = await self.auth.db.execute(select(func.count()).select_from(TenantUserModel).where(TenantUserModel.user_id == data.user_id)) + count_result = await self.db.execute(select(func.count()).select_from(TenantUserModel).where(TenantUserModel.user_id == data.user_id)) count = count_result.scalar() if count == 0: # 第一个租户自动设为默认 @@ -357,8 +432,8 @@ class TenantService: is_default=data.is_default, create_time=datetime.now(), ) - self.auth.db.add(tu) - await self.auth.db.flush() + self.db.add(tu) + await self.db.flush() logger.info(f"向租户[{tenant.name}]添加用户[{user.username}]成功, role={data.role}") @@ -383,14 +458,14 @@ class TenantService: ) .limit(1) ) - result = await self.auth.db.execute(exist_stmt) + result = await self.db.execute(exist_stmt) tu = result.scalar_one_or_none() if not tu: raise CustomException(msg="该用户未关联此租户") # 不允许移除租户最后一个 owner if tu.role == "owner": - count_result = await self.auth.db.execute( + count_result = await self.db.execute( select(func.count()) .select_from(TenantUserModel) .where( @@ -402,8 +477,8 @@ class TenantService: if owner_count <= 1: raise CustomException(msg="租户至少需要保留一个拥有者(owner)") - await self.auth.db.delete(tu) - await self.auth.db.flush() + await self.db.delete(tu) + await self.db.flush() logger.info(f"从租户[{tenant_id}]移除用户[{user_id}]成功") @@ -425,7 +500,7 @@ class TenantService: "max_depts": 999999, "package_name": "系统租户(无限)", } - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") if not tenant.package_id: @@ -439,7 +514,7 @@ class TenantService: } from app.api.v1.module_platform.package.crud import PackageCRUD - pkg = await PackageCRUD(self.auth).get(id=tenant.package_id) + pkg = await PackageCRUD(self.auth, self.db).get(id=tenant.package_id) if not pkg: return { "tenant_id": tenant.id, @@ -453,8 +528,8 @@ class TenantService: "tenant_id": tenant.id, "max_users": pkg.max_users, "max_roles": pkg.max_roles, - "max_storage_mb": getattr(pkg, "max_storage_mb", 0), "max_depts": pkg.max_depts, + "max_storage_mb": getattr(pkg, "max_storage_mb", 0), "package_name": pkg.name, } @@ -464,13 +539,13 @@ class TenantService: return from sqlalchemy import func, select - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant or not tenant.package_id: return from app.api.v1.module_platform.package.crud import PackageCRUD - pkg = await PackageCRUD(self.auth).get(id=tenant.package_id) + pkg = await PackageCRUD(self.auth, self.db).get(id=tenant.package_id) if not pkg: return @@ -527,7 +602,7 @@ class TenantService: else: return - result = await self.auth.db.execute(count_stmt) + result = await self.db.execute(count_stmt) current_count = result.scalar() or 0 if current_count >= max_limit: @@ -543,7 +618,7 @@ class TenantService: 返回: - dict: 配置字典 """ - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") @@ -596,10 +671,8 @@ class TenantService: from app.core.database import async_db_session async with async_db_session() as session, session.begin(): - from app.core.base_schema import AuthSchema as _AuthSchema - - _auth = _AuthSchema.anonymous(db=session) - svc = TenantService(_auth) + _auth = AuthSchema(check_data_scope=False) + svc = TenantService(_auth, session) config = await svc.get_config(tenant_id) await TenantService._sync_configs_to_redis(redis, tenant_id, config) logger.info("✅ 已从数据库加载租户配置到缓存") @@ -634,7 +707,7 @@ class TenantService: 返回: - list[TenantConfigOutSchema]: 更新后的配置项列表 """ - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") @@ -657,7 +730,7 @@ class TenantService: if field in config: setattr(tenant, field, config[field]) - await self.auth.db.flush() + await self.db.flush() # 刷新 DB 数据并同步到 Redis new_config = await self.get_config(tenant_id) @@ -719,7 +792,7 @@ class TenantService: """ from datetime import datetime - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") @@ -736,7 +809,7 @@ class TenantService: tenant.status = 0 tenant.grace_start_time = None - await self.auth.db.flush() + await self.db.flush() logger.info(f"租户[{tenant.name}]续期成功, 新的结束时间: {end_time}") return TenantOutSchema.model_validate(tenant) @@ -761,19 +834,19 @@ class TenantService: from app.api.v1.module_system.role.model import RoleMenusModel, RoleModel from app.api.v1.module_system.user.model import UserModel - tenant = await TenantCRUD(self.auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth, self.db).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") - new_package = await PackageCRUD(self.auth).get(id=new_package_id) + new_package = await PackageCRUD(self.auth, self.db).get(id=new_package_id) if not new_package: raise CustomException(msg="该数据不存在") # 当前可用菜单 - current_menu_ids = set(await PackageService(self.auth).get_tenant_available_menu_ids(tenant_id)) + current_menu_ids = set(await PackageService(self.auth, self.db).get_tenant_available_menu_ids(tenant_id)) # 新套餐可用菜单(直接取套餐菜单,不再包含自定义授权) - new_menu_ids = set(await PackageService(self.auth).get_package_menu_ids(new_package_id)) + new_menu_ids = set(await PackageService(self.auth, self.db).get_package_menu_ids(new_package_id)) final_menu_ids = new_menu_ids # 不再合并租户自定义菜单 # 差异计算 @@ -784,16 +857,16 @@ class TenantService: added_menus = [] if removed_ids: menu_stmt = select(MenuModel).where(MenuModel.id.in_(removed_ids)) - menu_result = await self.auth.db.execute(menu_stmt) + menu_result = await self.db.execute(menu_stmt) removed_menus = [{"id": m.id, "name": m.name, "route_path": m.route_path} for m in menu_result.scalars().all()] if added_ids: menu_stmt = select(MenuModel).where(MenuModel.id.in_(added_ids)) - menu_result = await self.auth.db.execute(menu_stmt) + menu_result = await self.db.execute(menu_stmt) added_menus = [{"id": m.id, "name": m.name, "route_path": m.route_path} for m in menu_result.scalars().all()] # 受影响角色 role_stmt = select(RoleModel).where(RoleModel.tenant_id == tenant_id) - role_result = await self.auth.db.execute(role_stmt) + role_result = await self.db.execute(role_stmt) roles = role_result.scalars().all() affected_roles = [] @@ -801,13 +874,13 @@ class TenantService: for role in roles: # 查该角色下有多少菜单会被移除 role_menu_stmt = select(RoleMenusModel.menu_id).where(RoleMenusModel.role_id == role.id) - rm_result = await self.auth.db.execute(role_menu_stmt) + rm_result = await self.db.execute(role_menu_stmt) role_menu_ids = {row[0] for row in rm_result.all()} affected_menu_count = len(role_menu_ids & removed_ids) # 查该角色下用户数 user_count_stmt = select(func.count()).select_from(UserModel).join(UserModel.roles).where(RoleModel.id == role.id) - uc_result = await self.auth.db.execute(user_count_stmt) + uc_result = await self.db.execute(user_count_stmt) user_count = uc_result.scalar() or 0 affected_roles.append( @@ -824,7 +897,7 @@ class TenantService: # 配额对比(从套餐读取) old_pkg = None if tenant.package_id: - old_pkg = await PackageCRUD(self.auth).get(id=tenant.package_id) + old_pkg = await PackageCRUD(self.auth, self.db).get(id=tenant.package_id) quota_changes = { "max_users": { "current": old_pkg.max_users if old_pkg else 0, @@ -929,27 +1002,28 @@ class TenantService: logger.info(f"已将 {count} 个过期超过 90 天的租户标记为归档") @classmethod - async def get_available_packages(cls, auth: AuthSchema, tenant_id: int) -> PackageAvailableOut: + async def get_available_packages(cls, auth: AuthSchema, db: AsyncSession, tenant_id: int) -> PackageAvailableOut: """获取可选套餐列表 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 - tenant_id (int): 租户ID 返回: - PackageAvailableOut: 可选套餐列表 """ - tenant = await auth.db.get(TenantModel, tenant_id) + tenant = await db.get(TenantModel, tenant_id) current_pkg_id = tenant.package_id if tenant else None # 一次性获取当前套餐价格(可能未启用,不在后续结果中) current_price: int | None = None if current_pkg_id: - cp = await auth.db.get(PackageModel, current_pkg_id) + cp = await db.get(PackageModel, current_pkg_id) current_price = cp.price if cp else 0 stmt = select(PackageModel).where(PackageModel.status == 0).order_by(PackageModel.price) - result = await auth.db.execute(stmt) + result = await db.execute(stmt) packages = result.scalars().all() items: list[PackageAvailableItem] = [] @@ -986,17 +1060,17 @@ class TenantService: ) @classmethod - async def preview_package_change(cls, auth: AuthSchema, tenant_id: int, target_package_id: int) -> PackagePreviewOut: + async def preview_package_change(cls, auth: AuthSchema, db: AsyncSession, tenant_id: int, target_package_id: int) -> PackagePreviewOut: """套餐变更预览(委托给 package_change_preview 并映射输出)""" - svc = cls(auth) + svc = cls(auth, db) preview = await svc.package_change_preview(tenant_id, target_package_id) - tenant = await auth.db.get(TenantModel, tenant_id) - target_pkg = await auth.db.get(PackageModel, target_package_id) + tenant = await db.get(TenantModel, tenant_id) + target_pkg = await db.get(PackageModel, target_package_id) current_pkg = None if tenant and tenant.package_id: - current_pkg = await auth.db.get(PackageModel, tenant.package_id) + current_pkg = await db.get(PackageModel, tenant.package_id) # 确定操作类型 if not tenant or not tenant.package_id: @@ -1021,30 +1095,31 @@ class TenantService: ) @classmethod - async def create_self_order(cls, auth: AuthSchema, tenant_id: int, data: SelfOrderCreate) -> SelfOrderOut: + async def create_self_order(cls, auth: AuthSchema, db: AsyncSession, tenant_id: int, data: SelfOrderCreate) -> SelfOrderOut: """创建自助订单(套餐购买/续费/升级/降级;免费订单自动激活) 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 - tenant_id (int): 租户ID - data (SelfOrderCreate): 自助订单创建参数 返回: - SelfOrderOut: 自助订单创建结果 """ - tenant = await auth.db.get(TenantModel, tenant_id) + tenant = await db.get(TenantModel, tenant_id) if not tenant: raise CustomException(msg="该数据不存在") if tenant.status not in (0, 1, 2): raise CustomException(msg="租户状态不允许操作") - pkg = await auth.db.get(PackageModel, data.package_id) + pkg = await db.get(PackageModel, data.package_id) if not pkg or pkg.status == 1: raise CustomException(msg="该数据不存在") amount = pkg.price - order = await OrderCRUD(auth).create( + order = await OrderCRUD(auth, db).create( OrderCreateInternalSchema( order_no=_generate_order_no(), tenant_id=tenant_id, @@ -1054,15 +1129,15 @@ class TenantService: expire_time=datetime.now() + timedelta(minutes=15), ), ) - await auth.db.flush() + await db.flush() # 免费订单自动激活 if amount == 0: - await OrderCRUD(auth).update( + await OrderCRUD(auth, db).update( order.id, OrderUpdateInternalSchema(status=1, pay_method="free", pay_time=datetime.now()), ) - await PaymentService._activate_tenant_package(auth, order) + await PaymentService._activate_tenant_package(auth, db, order) logger.info(f"自助订单创建: order_no={order.order_no} tenant={tenant_id} amount={amount}") return SelfOrderOut( @@ -1076,6 +1151,7 @@ class TenantService: async def get_self_order_list( cls, auth: AuthSchema, + db: AsyncSession, tenant_id: int, page_no: int = 1, page_size: int = 20, @@ -1085,6 +1161,7 @@ class TenantService: 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 - tenant_id (int): 租户ID - page_no (int): 页码 - page_size (int): 每页数量 @@ -1094,7 +1171,7 @@ class TenantService: - SelfOrderListOut: 订单分页列表 """ offset = (page_no - 1) * page_size - page_result = await OrderCRUD(auth).page( + page_result = await OrderCRUD(auth, db).page( offset=offset, limit=page_size, order_by=order_by or [{"created_time": "desc"}], @@ -1105,7 +1182,7 @@ class TenantService: package_ids = [o.package_id for o in page_result.items if hasattr(o, "package_id") and o.package_id] pkg_map: dict[int, str] = {} if package_ids: - pkg_result = await auth.db.execute(select(PackageModel.id, PackageModel.name).where(PackageModel.id.in_(package_ids))) + pkg_result = await db.execute(select(PackageModel.id, PackageModel.name).where(PackageModel.id.in_(package_ids))) pkg_map = {row[0]: row[1] for row in pkg_result.all()} items = [] @@ -1133,21 +1210,22 @@ class TenantService: ) @classmethod - async def get_self_order_detail(cls, auth: AuthSchema, order_id: int) -> SelfOrderDetailOut: + async def get_self_order_detail(cls, auth: AuthSchema, db: AsyncSession, order_id: int) -> SelfOrderDetailOut: """订单详情 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 - order_id (int): 订单ID 返回: - SelfOrderDetailOut: 订单详情 """ - order = await OrderCRUD(auth).get_or_404(id=order_id, msg="该数据不存在") + order = await OrderCRUD(auth, db).get_or_404(id=order_id, msg="该数据不存在") pkg_name = "" if order.package_id: - p = await auth.db.get(PackageModel, order.package_id) + p = await db.get(PackageModel, order.package_id) if p: pkg_name = p.name @@ -1165,17 +1243,18 @@ class TenantService: ) @classmethod - async def get_workspace_data(cls, auth: AuthSchema, tenant_id: int) -> WorkspaceOut: + async def get_workspace_data(cls, auth: AuthSchema, db: AsyncSession, tenant_id: int) -> WorkspaceOut: """获取租户工作台概览(租户信息、套餐、配额用量、近期订单) 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 - tenant_id (int): 租户ID 返回: - WorkspaceOut: 工作台概览数据 """ - tenant = await auth.db.get(TenantModel, tenant_id) + tenant = await db.get(TenantModel, tenant_id) if not tenant: return WorkspaceOut( tenant=WorkspaceTenantInfo(id=0, name="", code="", status=0, status_label="未知"), @@ -1184,7 +1263,7 @@ class TenantService: package = None if tenant.package_id: - package = await auth.db.get(PackageModel, tenant.package_id) + package = await db.get(PackageModel, tenant.package_id) async def _count(model_cls) -> int: stmt = ( @@ -1195,7 +1274,7 @@ class TenantService: model_cls.is_deleted.is_(False), ) ) - return (await auth.db.execute(stmt)).scalar() or 0 + return (await db.execute(stmt)).scalar() or 0 user_count = await _count(UserModel) role_count = await _count(RoleModel) @@ -1214,7 +1293,7 @@ class TenantService: } orders_stmt = select(OrderModel).where(OrderModel.tenant_id == tenant_id).order_by(OrderModel.created_time.desc()).limit(5) - orders_result = await auth.db.execute(orders_stmt) + orders_result = await db.execute(orders_stmt) recent_orders = [] for o in orders_result.scalars().all(): recent_orders.append( diff --git a/backend/app/api/v1/module_system/auth/controller.py b/backend/app/api/v1/module_system/auth/controller.py index 4b32e68a..3a495bb0 100644 --- a/backend/app/api/v1/module_system/auth/controller.py +++ b/backend/app/api/v1/module_system/auth/controller.py @@ -11,10 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ErrorResponse, RedirectContentResponse, ResponseSchema, SuccessResponse from app.config.setting import settings -from app.core.base_schema import ( - AuthSchema, - JWTOutSchema, -) +from app.core.base_schema import AuthSchema, JWTOutSchema from app.core.dependencies import db_getter, get_current_user, redis_getter from app.core.exceptions import CustomException from app.core.logger import logger @@ -128,9 +125,10 @@ async def select_tenant_controller( request: Request, auth: Annotated[AuthSchema, Depends(get_current_user)], redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[SelectTenantSchema, Body(description="租户选择参数")], ) -> JSONResponse: - result = await LoginService(auth).select_tenant(request=request, redis=redis, tenant_id=data.tenant_id) + result = await LoginService(auth, db).select_tenant(request=request, redis=redis, tenant_id=data.tenant_id) await FastAPICache.clear(namespace=_AUTH_TENANTS_NS) return SuccessResponse(data=result, msg="租户切换成功") @@ -140,8 +138,9 @@ async def enter_platform_controller( request: Request, auth: Annotated[AuthSchema, Depends(get_current_user)], redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result = await LoginService(auth).enter_platform(request=request, redis=redis) + result = await LoginService(auth, db).enter_platform(request=request, redis=redis) await FastAPICache.clear(namespace=_AUTH_TENANTS_NS) return SuccessResponse(data=result, msg="已返回平台管理模式") @@ -150,8 +149,9 @@ async def enter_platform_controller( @cache(expire=120, namespace=_AUTH_TENANTS_NS) async def get_user_tenants_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = LoginService(auth) + service = LoginService(auth, db) tenants = await service.get_user_tenants() return SuccessResponse(data=tenants, msg="获取租户列表成功") @@ -161,9 +161,10 @@ async def impersonate_controller( request: Request, auth: Annotated[AuthSchema, Depends(get_current_user)], redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[ImpersonateSchema, Body(description="代签入参数")], ) -> JSONResponse: - result = await LoginService(auth).impersonate(request=request, redis=redis, tenant_id=data.tenant_id) + result = await LoginService(auth, db).impersonate(request=request, redis=redis, tenant_id=data.tenant_id) await FastAPICache.clear(namespace=_AUTH_TENANTS_NS) return SuccessResponse(data=result, msg="代签入成功") diff --git a/backend/app/api/v1/module_system/auth/oauth_service.py b/backend/app/api/v1/module_system/auth/oauth_service.py index b9de22f7..5547fcda 100644 --- a/backend/app/api/v1/module_system/auth/oauth_service.py +++ b/backend/app/api/v1/module_system/auth/oauth_service.py @@ -306,9 +306,9 @@ async def ensure_oauth_user( unique_id: str, display_name: str, ) -> UserModel: - auth = AuthSchema.anonymous(db=db) + auth = AuthSchema(check_data_scope=False) username = _username_for_oauth(provider, unique_id) - existing = await UserCRUD(auth).get(username=username) + existing = await UserCRUD(auth, db).get(username=username) if existing: return existing @@ -319,14 +319,14 @@ async def ensure_oauth_user( role_ids=list(settings.OAUTH_DEFAULT_ROLE_IDS), ) try: - await UserService(auth).create(data=reg) + await UserService(auth, db).create(data=reg) except Exception: # 并发创建可能触发唯一约束冲突,回退到再次查询 - existing = await UserCRUD(auth).get(username=username) + existing = await UserCRUD(auth, db).get(username=username) if existing: return existing raise CustomException(msg="OAuth 注册失败") - user = await UserCRUD(auth).get(username=username) + user = await UserCRUD(auth, db).get(username=username) if not user: raise CustomException(msg="OAuth 注册失败") logger.info(f"OAuth 自动注册用户: {username} ({provider})") @@ -381,7 +381,7 @@ async def complete_oauth_login( if user.status == 1: raise CustomException(msg="用户已被停用") - user = await UserCRUD(AuthSchema.anonymous(db=db)).update_last_login(id=user.id) + user = await UserCRUD(AuthSchema(check_data_scope=False), db).update_last_login(id=user.id) if not user: raise CustomException(msg="用户不存在") diff --git a/backend/app/api/v1/module_system/auth/schema.py b/backend/app/api/v1/module_system/auth/schema.py index 38e4c5be..11864d6a 100644 --- a/backend/app/api/v1/module_system/auth/schema.py +++ b/backend/app/api/v1/module_system/auth/schema.py @@ -2,8 +2,23 @@ from typing import Any from pydantic import BaseModel, ConfigDict, EmailStr, Field -from app.core.base_schema import JWTOutSchema +from app.core.base_schema import AuthSchema, CoreUserSchema, JWTOutSchema +__all__ = [ + "AuthSchema", + "CoreUserSchema", + "CaptchaOutSchema", + "TenantOptionSchema", + "SelectTenantSchema", + "SelectTenantOutSchema", + "LoginWithTenantsSchema", + "TenantRegisterSchema", + "TenantRegisterOutSchema", + "EnterPlatformOutSchema", + "TenantLookupOutSchema", + "ImpersonateSchema", + "ImpersonateOutSchema", +] class CaptchaOutSchema(BaseModel): """验证码响应模型""" diff --git a/backend/app/api/v1/module_system/auth/service.py b/backend/app/api/v1/module_system/auth/service.py index 9edec51d..2bfe55ba 100644 --- a/backend/app/api/v1/module_system/auth/service.py +++ b/backend/app/api/v1/module_system/auth/service.py @@ -10,13 +10,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_system.user.crud import UserCRUD from app.api.v1.module_system.user.model import UserModel +from app.api.v1.module_system.user.schema import UserOutSchema from app.common.enums import RedisInitKeyConfig from app.config.setting import settings -from app.core.base_schema import ( - AuthSchema, - JWTOutSchema, - JWTPayloadSchema, -) +from app.core.base_schema import AuthSchema, JWTOutSchema, JWTPayloadSchema from app.core.exceptions import CustomException from app.core.logger import logger from app.core.redis_crud import RedisCURD @@ -27,7 +24,7 @@ from app.core.security import ( ) from app.utils.captcha_util import CaptchaUtil from app.utils.common_util import get_random_character -from app.utils.hash_bcrpy_util import PwdUtil +from app.utils.password_util import PwdUtil from app.utils.ip_local_util import IpLocalUtil, get_client_ip from .schema import ( @@ -60,8 +57,8 @@ async def _write_login_log( try: async with async_db_session() as session, session.begin(): - _auth = AuthSchema.anonymous(db=session) - obj = await LoginLogCRUD(_auth).create( + _auth = AuthSchema(check_data_scope=False) + obj = await LoginLogCRUD(_auth, session).create( data=LoginLogCreateSchema( username=username, status=status, @@ -101,8 +98,9 @@ async def _async_fill_login_location(redis, login_log_id: int, ip: str | None) - class LoginService: """登录认证服务""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db @classmethod async def authenticate_user( @@ -133,8 +131,8 @@ class LoginService: captcha=login_form.captcha, ) - auth = AuthSchema.anonymous(db=db) - user = await UserCRUD(auth).get(username=login_form.username) + auth = AuthSchema(check_data_scope=False) + user = await UserCRUD(auth, db).get(username=login_form.username) if not user: await _write_login_log( @@ -176,7 +174,7 @@ class LoginService: from app.api.v1.module_platform.tenant.model import TenantModel tenant_stmt = select(TenantModel).where(TenantModel.id == user.tenant_id, TenantModel.status == 0, TenantModel.is_deleted.is_(False)).limit(1) - tenant_result = await auth.db.execute(tenant_stmt) + tenant_result = await db.execute(tenant_stmt) if not tenant_result.scalar_one_or_none(): await _write_login_log( username=_login_username, @@ -189,7 +187,7 @@ class LoginService: ) raise CustomException(msg="所属租户已被禁用,请联系平台管理员") - await UserCRUD(auth).update_last_login(id=user.id) + await UserCRUD(auth, db).update_last_login(id=user.id) if not user: raise CustomException(msg="用户不存在") @@ -203,8 +201,8 @@ class LoginService: login_type=login_form.login_type, ) - tenants_auth = AuthSchema(db=db, user=user, check_data_scope=False) - tenants = await LoginService(tenants_auth).get_user_tenants(user_id=user.id) + tenants_auth = AuthSchema(user=UserOutSchema.model_validate(user), check_data_scope=False) + tenants = await LoginService(tenants_auth, db).get_user_tenants(user_id=user.id) user_info = { "id": user.id, @@ -376,8 +374,8 @@ class LoginService: if not session_id or not user_id: raise CustomException(msg="非法凭证,无法获取会话编号或用户ID") - auth = AuthSchema.anonymous(db=db) - user = await UserCRUD(auth).get(id=user_id) + auth = AuthSchema(check_data_scope=False) + user = await UserCRUD(auth, db).get(id=user_id) if not user: raise CustomException(msg="刷新token失败,用户不存在") if user.status == 1: @@ -464,7 +462,7 @@ class LoginService: if user.is_superuser: stmt = select(TenantModel).where(TenantModel.status == 0, TenantModel.is_deleted.is_(False)).order_by(TenantModel.sort, TenantModel.id) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) tenant_objs = result.scalars().all() return [TenantOptionSchema(id=t.id, name=t.name, code=t.code) for t in tenant_objs] @@ -478,7 +476,7 @@ class LoginService: ) .order_by(TenantUserModel.is_default.desc(), TenantModel.sort, TenantModel.id) ) - result = await self.auth.db.execute(stmt) + result = await self.db.execute(stmt) tenant_objs = result.scalars().all() return [TenantOptionSchema(id=t.id, name=t.name, code=t.code) for t in tenant_objs] @@ -506,12 +504,12 @@ class LoginService: ) .limit(1) ) - result = await self.auth.db.execute(exist_stmt) + result = await self.db.execute(exist_stmt) if not result.scalar_one_or_none(): raise CustomException(msg="您不属于该租户,无法切换") tenant_stmt = select(TenantModel).where(TenantModel.id == tenant_id, TenantModel.status == 0).limit(1) - result = await self.auth.db.execute(tenant_stmt) + result = await self.db.execute(tenant_stmt) tenant = result.scalar_one_or_none() if not tenant: raise CustomException(msg="租户不存在或已被禁用") @@ -661,7 +659,7 @@ class LoginService: raise CustomException(msg="仅平台管理员可执行代签入") tenant_stmt = select(TenantModel).where(TenantModel.id == tenant_id, TenantModel.is_deleted.is_(False)).limit(1) - result = await self.auth.db.execute(tenant_stmt) + result = await self.db.execute(tenant_stmt) tenant = result.scalar_one_or_none() if not tenant: raise CustomException(msg="租户不存在") diff --git a/backend/app/api/v1/module_system/dept/controller.py b/backend/app/api/v1/module_system/dept/controller.py index 4df60a62..a696989b 100644 --- a/backend/app/api/v1/module_system/dept/controller.py +++ b/backend/app/api/v1/module_system/dept/controller.py @@ -1,13 +1,14 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import DeptCreateSchema, DeptOutSchema, DeptQueryParam, DeptUpdateSchema @@ -22,28 +23,31 @@ _DEPT_NS = "dept" @cache(expire=300, namespace=_DEPT_NS) async def get_dept_tree_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], search: Annotated[DeptQueryParam, Query(description="部门查询参数")], ) -> JSONResponse: order_by = [{"order": "asc"}] - result_dict_tree = await DeptService(auth).tree(search=search, order_by=order_by) + result_dict_tree = await DeptService(auth, db).tree(search=search, order_by=order_by) return SuccessResponse(data=result_dict_tree, msg="查询部门树成功") @DeptRouter.get("/detail/{id}", summary="查询部门详情", response_model=ResponseSchema[DeptOutSchema]) async def get_obj_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="部门ID", ge=1)], ) -> JSONResponse: - result_dict = await DeptService(auth).detail(id=id) + result_dict = await DeptService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="查询部门详情成功") @DeptRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建部门", response_model=ResponseSchema[DeptOutSchema]) async def create_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[DeptCreateSchema, Body(description="部门创建参数")], ) -> JSONResponse: - result_dict = await DeptService(auth).create(data=data) + result_dict = await DeptService(auth, db).create(data=data) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(data=result_dict, msg="创建部门成功") @@ -51,10 +55,11 @@ async def create_obj_controller( @DeptRouter.put("/update/{id}", summary="修改部门", response_model=ResponseSchema[DeptOutSchema]) async def update_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="部门ID", ge=1)], data: Annotated[DeptUpdateSchema, Body(description="部门修改参数")], ) -> JSONResponse: - result_dict = await DeptService(auth).update(id=id, data=data) + result_dict = await DeptService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(data=result_dict, msg="修改部门成功") @@ -62,9 +67,10 @@ async def update_obj_controller( @DeptRouter.delete("/delete", summary="删除部门", response_model=ResponseSchema[None]) async def delete_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await DeptService(auth).delete(ids=ids) + await DeptService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(msg="删除部门成功") @@ -72,8 +78,9 @@ async def delete_obj_controller( @DeptRouter.patch("/status/batch", summary="批量修改部门状态", response_model=ResponseSchema[None]) async def batch_set_available_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dept:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await DeptService(auth).batch_set_available(data=data) + await DeptService(auth, db).batch_set_available(data=data) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(msg="批量修改部门状态成功") diff --git a/backend/app/api/v1/module_system/dept/crud.py b/backend/app/api/v1/module_system/dept/crud.py index eefd102e..5e6df9a5 100644 --- a/backend/app/api/v1/module_system/dept/crud.py +++ b/backend/app/api/v1/module_system/dept/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,5 +10,5 @@ from .schema import DeptCreateSchema, DeptUpdateSchema class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]): """部门模块数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=DeptModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=DeptModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_system/dept/service.py b/backend/app/api/v1/module_system/dept/service.py index 23e89b48..80c30810 100644 --- a/backend/app/api/v1/module_system/dept/service.py +++ b/backend/app/api/v1/module_system/dept/service.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, BatchSetAvailable from app.core.exceptions import CustomException from app.utils.common_util import ( @@ -23,14 +25,15 @@ class DeptService: 提供部门 CRUD、树形结构查询、级联启/禁用、租户配额检查等业务能力。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> DeptOutSchema: - dept = await DeptCRUD(self.auth).get_or_404(id=id) + dept = await DeptCRUD(self.auth, self.db).get_or_404(id=id) dept_out = DeptOutSchema.model_validate(dept) if dept.parent_id: - parent = await DeptCRUD(self.auth).get(id=dept.parent_id) + parent = await DeptCRUD(self.auth, self.db).get(id=dept.parent_id) if parent: dept_out.parent_name = parent.name return dept_out @@ -40,15 +43,15 @@ class DeptService: search: DeptQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[dict]: - dept_list = await DeptCRUD(self.auth).tree_list(search=vars(search) if search else None, order_by=order_by) + dept_list = await DeptCRUD(self.auth, self.db).tree_list(search=vars(search) if search else None, order_by=order_by) dept_dict_list = [DeptTreeOutSchema.model_validate(dept).model_dump() for dept in dept_list] return [d for d in dept_dict_list if d.get("parent_id") is None] async def create(self, data: DeptCreateSchema) -> DeptOutSchema: - dept = await DeptCRUD(self.auth).get(name=data.name) + dept = await DeptCRUD(self.auth, self.db).get(name=data.name) if dept: raise CustomException(msg="创建失败,该数据已存在") - obj = await DeptCRUD(self.auth).get(code=data.code) + obj = await DeptCRUD(self.auth, self.db).get(code=data.code) if obj: raise CustomException(msg="创建失败,编码已存在") @@ -58,24 +61,24 @@ class DeptService: user = self.auth.user if not user: raise CustomException(msg="未登录") - await TenantService(self.auth).check_quota(user.tenant_id, "dept") + await TenantService(self.auth, self.db).check_quota(user.tenant_id, "dept") - dept = await DeptCRUD(self.auth).create(data=data) + dept = await DeptCRUD(self.auth, self.db).create(data=data) return DeptOutSchema.model_validate(dept) async def update(self, id: int, data: DeptUpdateSchema) -> DeptOutSchema: - dept = await DeptCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_dept = await DeptCRUD(self.auth).get(name=data.name) + dept = await DeptCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_dept = await DeptCRUD(self.auth, self.db).get(name=data.name) if exist_dept and exist_dept.id != id: raise CustomException(msg="更新失败,名称已存在") - exist_code = await DeptCRUD(self.auth).get(code=data.code) + exist_code = await DeptCRUD(self.auth, self.db).get(code=data.code) if exist_code and exist_code.id != id: raise CustomException(msg="更新失败,编码已存在") - dept = await DeptCRUD(self.auth).update(id=id, data=data) + dept = await DeptCRUD(self.auth, self.db).update(id=id, data=data) dept_out = DeptOutSchema.model_validate(dept) if dept_out.parent_id: - parent = await DeptCRUD(self.auth).get(id=dept_out.parent_id) + parent = await DeptCRUD(self.auth, self.db).get(id=dept_out.parent_id) if parent: dept_out.parent_name = parent.name return dept_out @@ -85,7 +88,7 @@ class DeptService: raise CustomException(msg="删除失败,删除对象不能为空") # 获取所有部门列表,用于构建树形关系 - all_depts = await DeptCRUD(self.auth).get_list() + all_depts = await DeptCRUD(self.auth, self.db).get_list() # 构建子部门ID映射 child_id_map = get_child_id_map(model_list=all_depts) @@ -94,10 +97,10 @@ class DeptService: if child_id_map.get(pid): raise CustomException(msg="存在子部门,不允许删除父部门") - await DeptCRUD(self.auth).delete(ids=ids) + await DeptCRUD(self.auth, self.db).delete(ids=ids) async def batch_set_available(self, data: BatchSetAvailable) -> None: - dept_list = await DeptCRUD(self.auth).get_list() + dept_list = await DeptCRUD(self.auth, self.db).get_list() total_ids = [] if data.status == 0: @@ -111,4 +114,4 @@ class DeptService: disable_ids = get_child_recursion(id=dept_id, id_map=id_map) total_ids.extend(disable_ids) - await DeptCRUD(self.auth).set(ids=total_ids, status=data.status) + await DeptCRUD(self.auth, self.db).set(ids=total_ids, status=data.status) diff --git a/backend/app/api/v1/module_system/dict/controller.py b/backend/app/api/v1/module_system/dict/controller.py index ff3027fa..260252f2 100644 --- a/backend/app/api/v1/module_system/dict/controller.py +++ b/backend/app/api/v1/module_system/dict/controller.py @@ -5,10 +5,11 @@ from fastapi.responses import JSONResponse, StreamingResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache from redis.asyncio.client import Redis +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, StreamResponse, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission, redis_getter +from app.core.dependencies import AuthPermission, db_getter, redis_getter from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -33,8 +34,9 @@ _DICT_TYPE_NS = "dict_type" async def get_type_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:detail"]))], id: Annotated[int, Path(description="字典类型ID", ge=1)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await DictTypeService(auth).detail(id=id) + result_dict = await DictTypeService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取字典类型详情成功") @@ -43,8 +45,9 @@ async def get_type_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页查询参数")], search: Annotated[DictTypeQueryParam, Query(description="字典类型查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await DictTypeService(auth).page( + result_dict = await DictTypeService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -57,18 +60,20 @@ async def get_type_list_controller( @cache(expire=300, namespace=_DICT_TYPE_NS) async def get_type_optionselect_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict_list = await DictTypeService(auth).get_list() + result_dict_list = await DictTypeService(auth, db).get_list() return SuccessResponse(data=result_dict_list, msg="获取字典类型列表成功") @DictRouter.post("/type/create", status_code=status.HTTP_201_CREATED, summary="创建字典类型", response_model=ResponseSchema[DictTypeOutSchema]) async def create_type_controller( redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:create"]))], data: Annotated[DictTypeCreateSchema, Body(description="字典类型创建参数")], ) -> JSONResponse: - result_dict = await DictTypeService(auth).create(redis=redis, data=data) + result_dict = await DictTypeService(auth, db).create(redis=redis, data=data) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(data=result_dict, msg="创建字典类型成功") @@ -76,11 +81,12 @@ async def create_type_controller( @DictRouter.put("/type/update/{id}", summary="修改字典类型", response_model=ResponseSchema[DictTypeOutSchema]) async def update_type_controller( redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:update"]))], id: Annotated[int, Path(description="字典类型ID", ge=1)], data: Annotated[DictTypeUpdateSchema, Body(description="字典类型修改参数")], ) -> JSONResponse: - result_dict = await DictTypeService(auth).update(redis=redis, id=id, data=data) + result_dict = await DictTypeService(auth, db).update(redis=redis, id=id, data=data) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(data=result_dict, msg="修改字典类型成功") @@ -88,10 +94,11 @@ async def update_type_controller( @DictRouter.delete("/type/delete", summary="删除字典类型", response_model=ResponseSchema[None]) async def delete_type_controller( redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:delete"]))], ids: Annotated[list[int], Body(description="字典类型ID列表")], ) -> JSONResponse: - await DictTypeService(auth).delete(redis=redis, ids=ids) + await DictTypeService(auth, db).delete(redis=redis, ids=ids) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(msg="删除字典类型成功") @@ -100,8 +107,9 @@ async def delete_type_controller( async def batch_set_available_dict_type_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:patch"]))], data: Annotated[BatchSetAvailable, Body(description="状态设置")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - await DictTypeService(auth).set_available(data=data) + await DictTypeService(auth, db).set_available(data=data) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(msg="批量修改字典类型状态成功") @@ -110,9 +118,10 @@ async def batch_set_available_dict_type_controller( async def export_type_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:export"]))], search: Annotated[DictTypeQueryParam, Query(description="字典类型查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> StreamingResponse: # 获取全量数据并转为dict列表 - result_dict_list = await DictTypeService(auth).get_list(search=search) + result_dict_list = await DictTypeService(auth, db).get_list(search=search) export_data = [item.model_dump() for item in result_dict_list] export_result = DictTypeService.export(data_list=export_data) @@ -127,8 +136,9 @@ async def export_type_list_controller( async def get_data_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:detail"]))], id: Annotated[int, Path(description="字典数据ID", ge=1)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await DictDataService(auth).detail(id=id) + result_dict = await DictDataService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取字典数据详情成功") @@ -137,11 +147,12 @@ async def get_data_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[DictDataQueryParam, Query(description="字典数据查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: order_by = [{"order": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await DictDataService(auth).page( + result_dict = await DictDataService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -153,31 +164,34 @@ async def get_data_list_controller( @DictRouter.post("/data/create", status_code=status.HTTP_201_CREATED, summary="创建字典数据", response_model=ResponseSchema[DictDataOutSchema]) async def create_data_controller( redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:create"]))], data: Annotated[DictDataCreateSchema, Body(description="字典数据创建参数")], ) -> JSONResponse: - result_dict = await DictDataService(auth).create(redis=redis, data=data) + result_dict = await DictDataService(auth, db).create(redis=redis, data=data) return SuccessResponse(data=result_dict, msg="创建字典数据成功") @DictRouter.put("/data/update/{id}", summary="修改字典数据", response_model=ResponseSchema[DictDataOutSchema]) async def update_data_controller( redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:update"]))], id: Annotated[int, Path(description="字典数据ID", ge=1)], data: Annotated[DictDataUpdateSchema, Body(description="字典数据修改参数")], ) -> JSONResponse: - result_dict = await DictDataService(auth).update(redis=redis, id=id, data=data) + result_dict = await DictDataService(auth, db).update(redis=redis, id=id, data=data) return SuccessResponse(data=result_dict, msg="修改字典数据成功") @DictRouter.delete("/data/delete", summary="删除字典数据", response_model=ResponseSchema[None]) async def delete_data_controller( redis: Annotated[Redis, Depends(redis_getter)], + db: Annotated[AsyncSession, Depends(db_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:delete"]))], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await DictDataService(auth).delete(redis=redis, ids=ids) + await DictDataService(auth, db).delete(redis=redis, ids=ids) return SuccessResponse(msg="删除字典数据成功") @@ -185,8 +199,9 @@ async def delete_data_controller( async def batch_set_available_dict_data_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:patch"]))], data: Annotated[BatchSetAvailable, Body(description="状态设置")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - await DictDataService(auth).set_available(data=data) + await DictDataService(auth, db).set_available(data=data) return SuccessResponse(msg="批量修改字典数据状态成功") @@ -195,8 +210,9 @@ async def export_data_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:export"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[DictDataQueryParam, Query(description="字典数据查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> StreamingResponse: - result_dict_list = await DictDataService(auth).get_list(search=search, order_by=page.order_by) + result_dict_list = await DictDataService(auth, db).get_list(search=search, order_by=page.order_by) export_data = [item.model_dump() for item in result_dict_list] export_result = DictDataService.export(data_list=export_data) diff --git a/backend/app/api/v1/module_system/dict/crud.py b/backend/app/api/v1/module_system/dict/crud.py index 63df0b24..5b98a378 100644 --- a/backend/app/api/v1/module_system/dict/crud.py +++ b/backend/app/api/v1/module_system/dict/crud.py @@ -1,6 +1,8 @@ from collections.abc import Sequence from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.api.v1.module_system.dict.model import DictDataModel, DictTypeModel from app.api.v1.module_system.dict.schema import ( DictDataCreateSchema, @@ -15,31 +17,33 @@ from app.core.base_schema import AuthSchema class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateSchema]): """数据字典类型数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化数据字典类型数据层。 参数: - auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。 + - db (AsyncSession): 数据库会话。 返回: - None """ - super().__init__(model=DictTypeModel, auth=auth) + super().__init__(model=DictTypeModel, auth=auth, db=db) class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateSchema]): """数据字典数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化数据字典项数据层。 参数: - auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。 + - db (AsyncSession): 数据库会话。 返回: - None """ - super().__init__(model=DictDataModel, auth=auth) + super().__init__(model=DictDataModel, auth=auth, db=db) async def batch_delete(self, ids: list[int], exclude_system: bool = True) -> int: """批量删除数据字典数据 diff --git a/backend/app/api/v1/module_system/dict/service.py b/backend/app/api/v1/module_system/dict/service.py index 8ef77ade..1de45303 100644 --- a/backend/app/api/v1/module_system/dict/service.py +++ b/backend/app/api/v1/module_system/dict/service.py @@ -1,6 +1,7 @@ import json from redis.asyncio.client import Redis +from sqlalchemy.ext.asyncio import AsyncSession from app.common.enums import RedisInitKeyConfig from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema @@ -31,8 +32,9 @@ class DictTypeService: 改为 ``XxxService(auth).method(...)``。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> DictTypeOutSchema: """获取数据字典类型详情 @@ -43,7 +45,7 @@ class DictTypeService: 返回: - DictTypeOutSchema: 字典类型响应模型 """ - obj = await DictTypeCRUD(self.auth).get_or_404(id=id) + obj = await DictTypeCRUD(self.auth, self.db).get_or_404(id=id) return DictTypeOutSchema.model_validate(obj) async def get_list( @@ -60,7 +62,7 @@ class DictTypeService: 返回: - list[DictTypeOutSchema]: 字典类型响应模型列表 """ - obj_list = await DictTypeCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + obj_list = await DictTypeCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [DictTypeOutSchema.model_validate(obj) for obj in obj_list] async def page( @@ -82,7 +84,7 @@ class DictTypeService: - PageResultSchema[DictTypeOutSchema]: 分页结果 """ offset = (page_no - 1) * page_size - return await DictTypeCRUD(self.auth).page( + return await DictTypeCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -100,10 +102,10 @@ class DictTypeService: 返回: - DictTypeOutSchema: 字典类型响应模型 """ - exist_obj = await DictTypeCRUD(self.auth).get(dict_name=data.dict_name) + exist_obj = await DictTypeCRUD(self.auth, self.db).get(dict_name=data.dict_name) if exist_obj: raise CustomException(msg="创建失败,该数据已存在") - obj = await DictTypeCRUD(self.auth).create(data=data) + obj = await DictTypeCRUD(self.auth, self.db).create(data=data) new_obj_dict = DictTypeOutSchema.model_validate(obj) @@ -138,13 +140,13 @@ class DictTypeService: 返回: - DictTypeOutSchema: 字典类型响应模型 """ - exist_obj = await DictTypeCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_obj = await DictTypeCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") if exist_obj.dict_name != data.dict_name: raise CustomException(msg="更新失败,数据字典类型名称不可以修改") # 如果字典类型修改或状态变更,则修改对应字典数据的类型和状态 if exist_obj.dict_type != data.dict_type or exist_obj.status != data.status: - exist_obj_type_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": exist_obj.dict_type}) + exist_obj_type_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": exist_obj.dict_type}) if exist_obj_type_list: for item in exist_obj_type_list: item.dict_type = data.dict_type @@ -160,16 +162,16 @@ class DictTypeService: status=data.status, description=item.description, ) - await DictDataCRUD(self.auth).update(id=item.id, data=dict_data) + await DictDataCRUD(self.auth, self.db).update(id=item.id, data=dict_data) - obj = await DictTypeCRUD(self.auth).update(id=id, data=data) + obj = await DictTypeCRUD(self.auth, self.db).update(id=id, data=data) new_obj_dict = DictTypeOutSchema.model_validate(obj) redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: # 获取当前字典类型的所有字典数据,确保包含最新状态 - dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": data.dict_type}) + dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": data.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) @@ -197,14 +199,14 @@ class DictTypeService: """ if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await DictTypeCRUD(self.auth).get_list(search={"id": ("in", ids)}) + existing = await DictTypeCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) existing_map = {obj.id: obj for obj in existing} for nid in ids: if nid not in existing_map: raise CustomException(msg="删除失败,该数据不存在") exist_obj = existing_map[nid] # 检查是否有字典数据 - exist_obj_type_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": exist_obj.dict_type}) + exist_obj_type_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": exist_obj.dict_type}) if len(exist_obj_type_list) > 0: # 如果有字典数据,不能删除 raise CustomException(msg="删除失败,该数据字典类型下存在字典数据") @@ -216,7 +218,7 @@ class DictTypeService: except Exception as e: logger.error(f"删除字典类型失败: {e}") raise CustomException(msg="同步删除字典缓存失败") from e - await DictTypeCRUD(self.auth).delete(ids=ids) + await DictTypeCRUD(self.auth, self.db).delete(ids=ids) async def set_available(self, data: BatchSetAvailable) -> None: """设置数据字典类型状态 @@ -227,7 +229,7 @@ class DictTypeService: 返回: - None """ - await DictTypeCRUD(self.auth).set(ids=data.ids, status=data.status) + await DictTypeCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) @staticmethod def export(data_list: list[dict]) -> bytes: @@ -266,8 +268,9 @@ class DictDataService: 设计同 DictTypeService:实例方法 + ``__init__(auth)``。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> DictDataOutSchema: """获取数据字典数据详情 @@ -278,7 +281,7 @@ class DictDataService: 返回: - DictDataOutSchema: 字典数据响应模型 """ - obj = await DictDataCRUD(self.auth).get_or_404(id=id) + obj = await DictDataCRUD(self.auth, self.db).get_or_404(id=id) return DictDataOutSchema.model_validate(obj) async def get_list( @@ -295,7 +298,7 @@ class DictDataService: 返回: - list[DictDataOutSchema]: 字典数据响应模型列表 """ - obj_list = await DictDataCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + obj_list = await DictDataCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [DictDataOutSchema.model_validate(obj) for obj in obj_list] async def page( @@ -317,7 +320,7 @@ class DictDataService: - PageResultSchema[DictDataOutSchema]: 分页结果 """ offset = (page_no - 1) * page_size - return await DictDataCRUD(self.auth).page( + return await DictDataCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -337,8 +340,8 @@ class DictDataService: """ try: async with async_db_session() as session, session.begin(): - init_auth = AuthSchema.anonymous(db=session) - obj_list = await DictTypeCRUD(init_auth).get_list() + init_auth = AuthSchema(check_data_scope=False) + obj_list = await DictTypeCRUD(init_auth, session).get_list() if not obj_list: logger.warning("未找到任何字典类型数据") return @@ -347,7 +350,7 @@ class DictDataService: dict_type = obj.dict_type tenant_id = obj.tenant_id try: - dict_data_list = await DictDataCRUD(init_auth).get_list(search={"dict_type": dict_type, "tenant_id": tenant_id}) + dict_data_list = await DictDataCRUD(init_auth, session).get_list(search={"dict_type": dict_type, "tenant_id": tenant_id}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{tenant_id}:{dict_type}" value = json.dumps(dict_data, ensure_ascii=False) @@ -417,21 +420,21 @@ class DictDataService: - DictDataOutSchema: 字典数据响应模型 """ # 检查相同字典类型下dict_label是否已存在 - exist_label_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_label=data.dict_label) + exist_label_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_label=data.dict_label) if exist_label_obj: raise CustomException(msg=f'创建失败,该字典类型下的字典标签"{data.dict_label}"已存在') # 检查相同字典类型下dict_value是否已存在 - exist_value_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_value=data.dict_value) + exist_value_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_value=data.dict_value) if exist_value_obj: raise CustomException(msg=f'创建失败,该字典类型下的字典键值"{data.dict_value}"已存在') - obj = await DictDataCRUD(self.auth).create(data=data) + obj = await DictDataCRUD(self.auth, self.db).create(data=data) redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: # 获取当前字典类型的所有字典数据 - dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": data.dict_type}) + dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": data.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) @@ -463,27 +466,27 @@ class DictDataService: 返回: - DictDataOutSchema: 字典数据响应模型 """ - exist_obj = await DictDataCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_obj = await DictDataCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") # 检查相同字典类型下dict_label是否已存在(排除当前记录) if exist_obj.dict_label != data.dict_label: - exist_label_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_label=data.dict_label) + exist_label_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_label=data.dict_label) if exist_label_obj: raise CustomException(msg=f'更新失败,该字典类型下的字典标签"{data.dict_label}"已存在') # 检查相同字典类型下dict_value是否已存在(排除当前记录) if exist_obj.dict_value != data.dict_value: - exist_value_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_value=data.dict_value) + exist_value_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_value=data.dict_value) if exist_value_obj: raise CustomException(msg=f'更新失败,该字典类型下的字典键值"{data.dict_value}"已存在') # 如果字典类型变更,仅刷新旧类型缓存,不联动字典类型状态 if exist_obj.dict_type != data.dict_type: - dict_type = await DictTypeCRUD(self.auth).get(dict_type=exist_obj.dict_type) + dict_type = await DictTypeCRUD(self.auth, self.db).get(dict_type=exist_obj.dict_type) if dict_type: redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{dict_type.dict_type}" try: - dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": dict_type.dict_type}) + dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": dict_type.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) await RedisCURD(redis).set( @@ -495,12 +498,12 @@ class DictDataService: logger.error(f"刷新旧字典缓存失败: {e}") raise CustomException(msg="同步旧字典数据缓存失败") from e - obj = await DictDataCRUD(self.auth).update(id=id, data=data) + obj = await DictDataCRUD(self.auth, self.db).update(id=id, data=data) # 刷新新字典类型缓存 redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: - dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": data.dict_type}) + dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": data.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) await RedisCURD(redis).set( @@ -527,7 +530,7 @@ class DictDataService: """ if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await DictDataCRUD(self.auth).get_list(search={"id": ("in", ids)}) + existing = await DictDataCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) existing_map = {obj.id: obj for obj in existing} for nid in ids: if nid not in existing_map: @@ -537,7 +540,7 @@ class DictDataService: redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{exist_obj.dict_type}" try: # 重新拉取该类型所有字典数据并写回缓存(保持一致) - dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": exist_obj.dict_type}) + dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": exist_obj.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) await RedisCURD(redis).set( @@ -549,7 +552,7 @@ class DictDataService: except Exception as e: logger.error(f"删除字典数据刷新缓存失败: {e}") raise CustomException(msg="同步删除字典数据缓存失败") from e - await DictDataCRUD(self.auth).delete(ids=ids) + await DictDataCRUD(self.auth, self.db).delete(ids=ids) async def set_available(self, data: BatchSetAvailable) -> None: """设置数据字典数据状态 @@ -560,7 +563,7 @@ class DictDataService: 返回: - None """ - await DictDataCRUD(self.auth).set(ids=data.ids, status=data.status) + await DictDataCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) @staticmethod def export(data_list: list[dict]) -> bytes: diff --git a/backend/app/api/v1/module_system/log/controller.py b/backend/app/api/v1/module_system/log/controller.py index 4e0ddc15..2c6db5ae 100644 --- a/backend/app/api/v1/module_system/log/controller.py +++ b/backend/app/api/v1/module_system/log/controller.py @@ -2,10 +2,11 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, Security from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission, get_current_user +from app.core.dependencies import AuthPermission, db_getter, get_current_user from app.core.router_class import OperationLogRoute from .schema import ( @@ -24,19 +25,21 @@ LogRouter = APIRouter(route_class=OperationLogRoute, prefix="/log", tags=["日 @LogRouter.get("/login/detail/{id}", summary="获取登录日志详情", response_model=ResponseSchema[LoginLogDetailOutSchema]) async def get_log_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="登录日志ID", ge=1)], ) -> JSONResponse: - result_dict = await LoginLogService(auth).detail(id=id) + result_dict = await LoginLogService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取登录日志详情成功") @LogRouter.get("/login/list", summary="查询登录日志列表", response_model=ResponseSchema[PageResultSchema[LoginLogOutSchema]]) async def get_log_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[LoginLogQueryParam, Query(description="登录日志查询参数")], ) -> JSONResponse: - result_dict = await LoginLogService(auth).page( + result_dict = await LoginLogService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -48,18 +51,20 @@ async def get_log_list_controller( @LogRouter.delete("/login/delete", summary="删除登录日志", response_model=ResponseSchema) async def delete_log_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:login_log:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await LoginLogService(auth).delete(ids=ids) + await LoginLogService(auth, db).delete(ids=ids) return SuccessResponse(msg="删除登录日志成功") @LogRouter.get("/operation/detail/{id}", summary="获取操作日志详情", response_model=ResponseSchema[OperationLogDetailOutSchema], dependencies=[Security(AuthPermission(["module_system:log:query"]))]) async def get_operation_log_detail_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="操作日志ID", gt=0)], ) -> JSONResponse: - result_dict = await OperationLogService(auth).detail(id=id) + result_dict = await OperationLogService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取操作日志详情成功") @@ -68,10 +73,11 @@ async def get_operation_log_detail_controller( ) async def get_operation_log_list_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[OperationLogQueryParam, Query(description="操作日志查询参数")], ) -> JSONResponse: - result_dict = await OperationLogService(auth).page( + result_dict = await OperationLogService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -83,7 +89,8 @@ async def get_operation_log_list_controller( @LogRouter.delete("/operation/delete", summary="删除操作日志", response_model=ResponseSchema, dependencies=[Security(AuthPermission(["module_system:log:delete"]))]) async def delete_operation_log_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await OperationLogService(auth).delete(ids=ids) + await OperationLogService(auth, db).delete(ids=ids) return SuccessResponse(msg="删除操作日志成功") diff --git a/backend/app/api/v1/module_system/log/crud.py b/backend/app/api/v1/module_system/log/crud.py index 11c7bae9..5650bce1 100644 --- a/backend/app/api/v1/module_system/log/crud.py +++ b/backend/app/api/v1/module_system/log/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,12 +10,12 @@ from .schema import LoginLogCreateSchema, OperationLogCreateSchema class LoginLogCRUD(CRUDBase[LoginLogModel, LoginLogCreateSchema, None]): """登录日志数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=LoginLogModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=LoginLogModel, auth=auth, db=db) class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, None]): """操作日志 CRUD""" - def __init__(self, auth: AuthSchema): - super().__init__(model=OperationLogModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=OperationLogModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_system/log/service.py b/backend/app/api/v1/module_system/log/service.py index 9e99467c..6aa40c96 100644 --- a/backend/app/api/v1/module_system/log/service.py +++ b/backend/app/api/v1/module_system/log/service.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException from app.core.logger import logger @@ -16,11 +18,12 @@ from .schema import ( class LoginLogService: """登录日志管理服务""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> LoginLogDetailOutSchema: - obj = await LoginLogCRUD(self.auth).get_or_404(id=id) + obj = await LoginLogCRUD(self.auth, self.db).get_or_404(id=id) return LoginLogDetailOutSchema.model_validate(obj) async def page( @@ -30,7 +33,7 @@ class LoginLogService: search: LoginLogQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[LoginLogOutSchema]: - return await LoginLogCRUD(self.auth).page( + return await LoginLogCRUD(self.auth, self.db).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"updated_time": "desc"}], @@ -42,20 +45,21 @@ class LoginLogService: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await LoginLogCRUD(self.auth).get_list(search={"id": ("in", ids)}) + existing = await LoginLogCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) existing_map = {obj.id for obj in existing} for nid in ids: if nid not in existing_map: raise CustomException(msg=f"删除失败,ID为{nid}的数据不存在") - await LoginLogCRUD(self.auth).delete(ids=ids) + await LoginLogCRUD(self.auth, self.db).delete(ids=ids) class OperationLogService: """操作日志管理服务""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db @staticmethod async def cleanup_operation_log() -> bool: @@ -99,7 +103,7 @@ class OperationLogService: search: OperationLogQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[OperationLogOutSchema]: - crud = OperationLogCRUD(self.auth) + crud = OperationLogCRUD(self.auth, self.db) return await crud.page( offset=(page_no - 1) * page_size, limit=page_size, @@ -109,17 +113,17 @@ class OperationLogService: ) async def detail(self, id: int) -> OperationLogDetailOutSchema: - crud = OperationLogCRUD(self.auth) + crud = OperationLogCRUD(self.auth, self.db) obj = await crud.get_or_404(id=id) return OperationLogDetailOutSchema.model_validate(obj) async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await OperationLogCRUD(self.auth).get_list(search={"id": ("in", ids)}) + existing = await OperationLogCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) existing_map = {obj.id for obj in existing} for nid in ids: if nid not in existing_map: raise CustomException(msg="删除失败,该数据不存在") - crud = OperationLogCRUD(self.auth) + crud = OperationLogCRUD(self.auth, self.db) await crud.delete(ids=ids) diff --git a/backend/app/api/v1/module_system/notice/controller.py b/backend/app/api/v1/module_system/notice/controller.py index 019a5996..8593fc6d 100644 --- a/backend/app/api/v1/module_system/notice/controller.py +++ b/backend/app/api/v1/module_system/notice/controller.py @@ -4,10 +4,11 @@ from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission, get_current_user +from app.core.dependencies import AuthPermission, db_getter, get_current_user from app.core.router_class import OperationLogRoute from .schema import NoticeCreateSchema, NoticeOutSchema, NoticeQueryParam, NoticeUpdateSchema @@ -21,19 +22,21 @@ _NOTICE_NS = "notice" @NoticeRouter.get("/detail/{id}", summary="获取公告详情", response_model=ResponseSchema[NoticeOutSchema]) async def get_notice_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="公告ID")], ) -> JSONResponse: - result_dict = await NoticeService(auth).detail(id=id) + result_dict = await NoticeService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取公告详情成功") @NoticeRouter.get("/list", summary="查询公告", response_model=ResponseSchema[PageResultSchema[NoticeOutSchema]]) async def get_notice_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[NoticeQueryParam, Query(description="公告查询参数")], ) -> JSONResponse: - result_dict = await NoticeService(auth).page( + result_dict = await NoticeService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -45,9 +48,10 @@ async def get_notice_list_controller( @NoticeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建公告", response_model=ResponseSchema[NoticeOutSchema]) async def create_notice_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[NoticeCreateSchema, Body(description="公告创建参数")], ) -> JSONResponse: - result_dict = await NoticeService(auth).create(data=data) + result_dict = await NoticeService(auth, db).create(data=data) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(data=result_dict, msg="创建公告成功") @@ -55,10 +59,11 @@ async def create_notice_controller( @NoticeRouter.put("/update/{id}", summary="修改公告", response_model=ResponseSchema[NoticeOutSchema]) async def update_notice_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="公告ID", ge=1)], data: Annotated[NoticeUpdateSchema, Body(description="公告修改参数")], ) -> JSONResponse: - result_dict = await NoticeService(auth).update(id=id, data=data) + result_dict = await NoticeService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(data=result_dict, msg="修改公告成功") @@ -66,9 +71,10 @@ async def update_notice_controller( @NoticeRouter.delete("/delete", summary="删除公告", response_model=ResponseSchema[None]) async def delete_notice_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await NoticeService(auth).delete(ids=ids) + await NoticeService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(msg="删除公告成功") @@ -76,9 +82,10 @@ async def delete_notice_controller( @NoticeRouter.patch("/status/batch", summary="批量修改公告状态", response_model=ResponseSchema[None]) async def batch_set_available_notice_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await NoticeService(auth).set_available(data=data) + await NoticeService(auth, db).set_available(data=data) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(msg="批量修改公告状态成功") @@ -87,6 +94,7 @@ async def batch_set_available_notice_controller( @cache(expire=120, namespace=_NOTICE_NS) async def get_notice_list_available_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await NoticeService(auth).available_page() + result_dict = await NoticeService(auth, db).available_page() return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功") diff --git a/backend/app/api/v1/module_system/notice/crud.py b/backend/app/api/v1/module_system/notice/crud.py index d0781263..a6e1cf78 100644 --- a/backend/app/api/v1/module_system/notice/crud.py +++ b/backend/app/api/v1/module_system/notice/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,5 +10,5 @@ from .schema import NoticeCreateSchema, NoticeUpdateSchema class NoticeCRUD(CRUDBase[NoticeModel, NoticeCreateSchema, NoticeUpdateSchema]): """公告数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=NoticeModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=NoticeModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_system/notice/service.py b/backend/app/api/v1/module_system/notice/service.py index 9ebf4688..beb97206 100644 --- a/backend/app/api/v1/module_system/notice/service.py +++ b/backend/app/api/v1/module_system/notice/service.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema from app.core.exceptions import CustomException from app.utils.excel_util import ExcelUtil @@ -12,11 +14,12 @@ class NoticeService: 提供公告 CRUD、状态切换、已启用公告分页查询、Excel 导出等业务能力。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> NoticeOutSchema: - obj = await NoticeCRUD(self.auth).get_or_404(id=id) + obj = await NoticeCRUD(self.auth, self.db).get_or_404(id=id) return NoticeOutSchema.model_validate(obj) async def get_list( @@ -24,7 +27,7 @@ class NoticeService: search: NoticeQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[NoticeOutSchema]: - notice_obj_list = await NoticeCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + notice_obj_list = await NoticeCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [NoticeOutSchema.model_validate(notice_obj) for notice_obj in notice_obj_list] async def page( @@ -35,7 +38,7 @@ class NoticeService: order_by: list[dict] | None = None, ) -> PageResultSchema[NoticeOutSchema]: offset = (page_no - 1) * page_size - return await NoticeCRUD(self.auth).page( + return await NoticeCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -44,7 +47,7 @@ class NoticeService: ) async def available_page(self) -> PageResultSchema[NoticeOutSchema]: - return await NoticeCRUD(self.auth).page( + return await NoticeCRUD(self.auth, self.db).page( offset=0, limit=10, order_by=[{"id": "asc"}], @@ -53,32 +56,32 @@ class NoticeService: ) async def create(self, data: NoticeCreateSchema) -> NoticeOutSchema: - notice = await NoticeCRUD(self.auth).get(notice_title=data.notice_title) + notice = await NoticeCRUD(self.auth, self.db).get(notice_title=data.notice_title) if notice: raise CustomException(msg="创建失败,该数据已存在") - notice_obj = await NoticeCRUD(self.auth).create(data=data) + notice_obj = await NoticeCRUD(self.auth, self.db).create(data=data) return NoticeOutSchema.model_validate(notice_obj) async def update(self, id: int, data: NoticeUpdateSchema) -> NoticeOutSchema: - _ = await NoticeCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_notice = await NoticeCRUD(self.auth).get(notice_title=data.notice_title) + _ = await NoticeCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_notice = await NoticeCRUD(self.auth, self.db).get(notice_title=data.notice_title) if exist_notice and exist_notice.id != id: raise CustomException(msg="更新失败,标题已存在") - notice_obj = await NoticeCRUD(self.auth).update(id=id, data=data) + notice_obj = await NoticeCRUD(self.auth, self.db).update(id=id, data=data) return NoticeOutSchema.model_validate(notice_obj) async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - notices = await NoticeCRUD(self.auth).get_list(search={"id": ("in", ids)}) + notices = await NoticeCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) notice_map = {n.id: n for n in notices} for nid in ids: if nid not in notice_map: raise CustomException(msg="删除失败,该数据不存在") - await NoticeCRUD(self.auth).delete(ids=ids) + await NoticeCRUD(self.auth, self.db).delete(ids=ids) async def set_available(self, data: BatchSetAvailable) -> None: - await NoticeCRUD(self.auth).set(ids=data.ids, status=data.status) + await NoticeCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) @staticmethod def export(notice_list: list[dict]) -> bytes: diff --git a/backend/app/api/v1/module_system/params/controller.py b/backend/app/api/v1/module_system/params/controller.py index 51c06bc4..2b3f3cb1 100644 --- a/backend/app/api/v1/module_system/params/controller.py +++ b/backend/app/api/v1/module_system/params/controller.py @@ -3,10 +3,11 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse, StreamingResponse from redis.asyncio.client import Redis +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, StreamResponse, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission, redis_getter +from app.core.dependencies import AuthPermission, db_getter, redis_getter from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -19,19 +20,21 @@ ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=[" @ParamsRouter.get("/detail/{id}", summary="获取参数详情", response_model=ResponseSchema[ParamsOutSchema]) async def get_param_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="参数ID", ge=1)], ) -> JSONResponse: - result_dict = await ParamsService(auth).detail(id=id) + result_dict = await ParamsService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取参数详情成功") @ParamsRouter.get("/list", summary="获取参数列表", response_model=ResponseSchema[PageResultSchema[ParamsOutSchema]]) async def get_param_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[ParamsQueryParam, Query(description="参数查询参数")], ) -> JSONResponse: - result_dict = await ParamsService(auth).page( + result_dict = await ParamsService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -44,9 +47,10 @@ async def get_param_list_controller( async def create_param_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[ParamsCreateSchema, Body(description="参数创建参数")], ) -> JSONResponse: - result_dict = await ParamsService(auth).create(redis=redis, data=data) + result_dict = await ParamsService(auth, db).create(redis=redis, data=data) return SuccessResponse(data=result_dict, msg="创建参数成功") @@ -54,10 +58,11 @@ async def create_param_controller( async def update_param_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="参数ID")], data: Annotated[ParamsUpdateSchema, Body(description="参数修改参数")], ) -> JSONResponse: - result_dict = await ParamsService(auth).update(redis=redis, id=id, data=data) + result_dict = await ParamsService(auth, db).update(redis=redis, id=id, data=data) return SuccessResponse(data=result_dict, msg="更新参数成功") @@ -65,27 +70,30 @@ async def update_param_controller( async def delete_param_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await ParamsService(auth).delete(redis=redis, ids=ids) + await ParamsService(auth, db).delete(redis=redis, ids=ids) return SuccessResponse(msg="删除参数成功") @ParamsRouter.patch("/status/batch", summary="批量设置参数状态", response_model=ResponseSchema) async def batch_set_status_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await ParamsService(auth).batch_set_status(ids=data.ids, status=data.status) + await ParamsService(auth, db).batch_set_status(ids=data.ids, status=data.status) return SuccessResponse(msg="批量设置参数状态成功") @ParamsRouter.get("/export", summary="导出参数") async def export_param_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:param:export"]))], + db: Annotated[AsyncSession, Depends(db_getter)], search: Annotated[ParamsQueryParam, Query(description="参数查询参数")], ) -> StreamingResponse: - result_dict_list = await ParamsService(auth).get_list(search=search) + result_dict_list = await ParamsService(auth, db).get_list(search=search) export_data = [item.model_dump() for item in result_dict_list] export_result = ParamsService.export(data_list=export_data) diff --git a/backend/app/api/v1/module_system/params/crud.py b/backend/app/api/v1/module_system/params/crud.py index 0f7a354c..7f01722e 100644 --- a/backend/app/api/v1/module_system/params/crud.py +++ b/backend/app/api/v1/module_system/params/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,13 +10,14 @@ from .schema import ParamsCreateSchema, ParamsUpdateSchema class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]): """配置管理数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化系统参数配置数据层。 参数: - auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。 + - db (AsyncSession): 数据库会话。 返回: - None """ - super().__init__(model=ParamsModel, auth=auth) + super().__init__(model=ParamsModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_system/params/service.py b/backend/app/api/v1/module_system/params/service.py index e37c8255..36b88f9c 100644 --- a/backend/app/api/v1/module_system/params/service.py +++ b/backend/app/api/v1/module_system/params/service.py @@ -3,6 +3,7 @@ import time from collections.abc import Sequence from redis.asyncio.client import Redis +from sqlalchemy.ext.asyncio import AsyncSession from app.common.enums import RedisInitKeyConfig from app.core.base_schema import AuthSchema, PageResultSchema @@ -113,8 +114,9 @@ class ParamsService: ``ParamsService.method_service(auth=...)`` 改为 ``ParamsService(auth).method(...)``。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> ParamsOutSchema: """获取参数详情 @@ -125,7 +127,7 @@ class ParamsService: 返回: - ParamsOutSchema: 参数响应模型 """ - obj = await ParamsCRUD(self.auth).get_or_404(id=id) + obj = await ParamsCRUD(self.auth, self.db).get_or_404(id=id) return ParamsOutSchema.model_validate(obj) async def get_by_key(self, config_key: str) -> ParamsOutSchema: @@ -137,7 +139,7 @@ class ParamsService: 返回: - ParamsOutSchema: 参数响应模型 """ - obj = await ParamsCRUD(self.auth).get(config_key=config_key) + obj = await ParamsCRUD(self.auth, self.db).get(config_key=config_key) if not obj: raise CustomException(msg="该数据不存在") return ParamsOutSchema.model_validate(obj) @@ -156,7 +158,7 @@ class ParamsService: 返回: - list[ParamsOutSchema]: 参数响应模型列表 """ - obj_list = await ParamsCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + obj_list = await ParamsCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [ParamsOutSchema.model_validate(obj) for obj in obj_list] async def page( @@ -178,7 +180,7 @@ class ParamsService: - PageResultSchema[ParamsOutSchema]: 分页结果 """ offset = (page_no - 1) * page_size - return await ParamsCRUD(self.auth).page( + return await ParamsCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -196,10 +198,10 @@ class ParamsService: 返回: - ParamsOutSchema: 新创建的参数响应模型 """ - exist_obj = await ParamsCRUD(self.auth).get(config_key=data.config_key) + exist_obj = await ParamsCRUD(self.auth, self.db).get(config_key=data.config_key) if exist_obj: raise CustomException(msg="创建失败,该数据已存在") - obj = await ParamsCRUD(self.auth).create(data=data) + obj = await ParamsCRUD(self.auth, self.db).create(data=data) out = ParamsOutSchema.model_validate(obj) @@ -236,11 +238,11 @@ class ParamsService: 返回: - ParamsOutSchema: 更新后的参数响应模型 """ - exist_obj = await ParamsCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_obj = await ParamsCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") if exist_obj.config_key != data.config_key: raise CustomException(msg="更新失败,系统配置key不允许修改") - new_obj = await ParamsCRUD(self.auth).update(id=id, data=data) + new_obj = await ParamsCRUD(self.auth, self.db).update(id=id, data=data) if not new_obj: raise CustomException(msg="更新失败,系统配置不存在") out = ParamsOutSchema.model_validate(new_obj) @@ -283,7 +285,7 @@ class ParamsService: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") # 批量校验参数存在性 - objs = await ParamsCRUD(self.auth).get_list(search={"id": ("in", ids)}) + objs = await ParamsCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) obj_map = {o.id: o for o in objs} for pid in ids: obj = obj_map.get(pid) @@ -292,7 +294,7 @@ class ParamsService: if obj.config_type: raise CustomException(msg=f"{obj.config_name} 删除失败,系统初始化配置不可以删除") - await ParamsCRUD(self.auth).delete(ids=ids) + await ParamsCRUD(self.auth, self.db).delete(ids=ids) # 同步删除Redis缓存(使用删除前已获取的对象信息) user = self.auth.user @@ -322,7 +324,7 @@ class ParamsService: if not ids: raise CustomException(msg="请选择要操作的数据") - await ParamsCRUD(self.auth).set(ids=ids, status=status) + await ParamsCRUD(self.auth, self.db).set(ids=ids, status=status) @staticmethod def export(data_list: list[dict]) -> bytes: @@ -358,8 +360,8 @@ class ParamsService: @staticmethod async def _load_all_configs_from_db() -> Sequence[object]: async with async_db_session() as session, session.begin(): - init_auth = AuthSchema.anonymous(db=session) - return await ParamsCRUD(init_auth).get_list() + init_auth = AuthSchema(check_data_scope=False) + return await ParamsCRUD(init_auth, session).get_list() @staticmethod async def _sync_configs_to_redis(redis: Redis, config_obj: Sequence) -> list[dict]: diff --git a/backend/app/api/v1/module_system/plugin.toml b/backend/app/api/v1/module_system/plugin.toml index 9ed439ba..4fb0fc27 100644 --- a/backend/app/api/v1/module_system/plugin.toml +++ b/backend/app/api/v1/module_system/plugin.toml @@ -1,8 +1,8 @@ # 见 docs/PLUGIN_ARCHITECTURE.md -name = "platform" -title = "平台" +name = "system" +title = "系统" version = "1.0.0" -description = "平台功能;路由由 module_platform/**/controller 动态注册。" +description = "系统核心功能(用户/角色/权限/部门/字典/参数/通知/日志等)" optional = true -tags = ["platform"] +tags = ["system", "auth", "rbac"] diff --git a/backend/app/api/v1/module_system/position/controller.py b/backend/app/api/v1/module_system/position/controller.py index 3f796461..c23e56f7 100644 --- a/backend/app/api/v1/module_system/position/controller.py +++ b/backend/app/api/v1/module_system/position/controller.py @@ -1,13 +1,14 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse, StreamingResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, StreamResponse, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -23,13 +24,14 @@ _POS_NS = "position" @cache(expire=300, namespace=_POS_NS) async def get_obj_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[PositionQueryParam, Query(description="岗位查询参数")], ) -> JSONResponse: order_by = [{"order": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await PositionService(auth).page( + result_dict = await PositionService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -41,18 +43,20 @@ async def get_obj_list_controller( @PositionRouter.get("/detail/{id}", summary="查询岗位详情", response_model=ResponseSchema[PositionOutSchema]) async def get_obj_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="岗位ID", ge=1)], ) -> JSONResponse: - result_dict = await PositionService(auth).detail(id=id) + result_dict = await PositionService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取岗位详情成功") @PositionRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建岗位", response_model=ResponseSchema[PositionOutSchema]) async def create_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[PositionCreateSchema, Body(description="岗位创建参数")], ) -> JSONResponse: - result_dict = await PositionService(auth).create(data=data) + result_dict = await PositionService(auth, db).create(data=data) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(data=result_dict, msg="创建岗位成功") @@ -60,10 +64,11 @@ async def create_obj_controller( @PositionRouter.put("/update/{id}", summary="修改岗位", response_model=ResponseSchema[PositionOutSchema]) async def update_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="岗位ID", ge=1)], data: Annotated[PositionUpdateSchema, Body(description="岗位修改参数")], ) -> JSONResponse: - result_dict = await PositionService(auth).update(id=id, data=data) + result_dict = await PositionService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(data=result_dict, msg="修改岗位成功") @@ -71,9 +76,10 @@ async def update_obj_controller( @PositionRouter.delete("/delete", summary="删除岗位", response_model=ResponseSchema[None]) async def delete_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await PositionService(auth).delete(ids=ids) + await PositionService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(msg="删除岗位成功") @@ -81,9 +87,10 @@ async def delete_obj_controller( @PositionRouter.patch("/status/batch", summary="批量修改岗位状态", response_model=ResponseSchema[None]) async def batch_set_available_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await PositionService(auth).set_available(data=data) + await PositionService(auth, db).set_available(data=data) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(msg="批量修改岗位状态成功") @@ -91,17 +98,19 @@ async def batch_set_available_obj_controller( @PositionRouter.get("/options", summary="获取岗位下拉选项", response_model=ResponseSchema[list[dict[str, int | str]]]) async def get_position_options_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - options = await PositionService(auth).get_options() + options = await PositionService(auth, db).get_options() return SuccessResponse(data=options, msg="获取岗位选项成功") @PositionRouter.get("/export", summary="导出岗位") async def export_obj_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:position:export"]))], + db: Annotated[AsyncSession, Depends(db_getter)], search: Annotated[PositionQueryParam, Query(description="岗位查询参数")], ) -> StreamingResponse: - position_query_result = await PositionService(auth).get_list(search=search) + position_query_result = await PositionService(auth, db).get_list(search=search) position_export_result = PositionService.export_list(position_list=[item.model_dump() for item in position_query_result]) return StreamResponse( diff --git a/backend/app/api/v1/module_system/position/crud.py b/backend/app/api/v1/module_system/position/crud.py index 3340187d..c187baab 100644 --- a/backend/app/api/v1/module_system/position/crud.py +++ b/backend/app/api/v1/module_system/position/crud.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -10,8 +12,8 @@ from .schema import PositionCreateSchema, PositionUpdateSchema class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateSchema]): """岗位模块数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=PositionModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=PositionModel, auth=auth, db=db) async def get_options(self) -> list[dict[str, Any]]: """获取岗位下拉选项,返回 [{value, label}]""" diff --git a/backend/app/api/v1/module_system/position/service.py b/backend/app/api/v1/module_system/position/service.py index e8bc0a47..175af05f 100644 --- a/backend/app/api/v1/module_system/position/service.py +++ b/backend/app/api/v1/module_system/position/service.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema from app.core.exceptions import CustomException from app.utils.excel_util import ExcelUtil @@ -19,23 +21,24 @@ class PositionService: 提供岗位 CRUD、批量启/禁用、Excel 导出等业务能力。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> PositionOutSchema: - obj = await PositionCRUD(self.auth).get_or_404(id=id) + obj = await PositionCRUD(self.auth, self.db).get_or_404(id=id) return PositionOutSchema.model_validate(obj) async def get_options(self) -> list[dict[str, Any]]: """获取岗位下拉选项,委托给 PositionCRUD""" - return await PositionCRUD(self.auth).get_options() + return await PositionCRUD(self.auth, self.db).get_options() async def get_list( self, search: PositionQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[PositionOutSchema]: - position_list = await PositionCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + position_list = await PositionCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [PositionOutSchema.model_validate(position) for position in position_list] async def page( @@ -46,7 +49,7 @@ class PositionService: order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[PositionOutSchema]: offset = (page_no - 1) * page_size - return await PositionCRUD(self.auth).page( + return await PositionCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -55,37 +58,37 @@ class PositionService: ) async def create(self, data: PositionCreateSchema) -> PositionOutSchema: - position = await PositionCRUD(self.auth).get(name=data.name) + position = await PositionCRUD(self.auth, self.db).get(name=data.name) if position: raise CustomException(msg="创建失败,该数据已存在") - new_position = await PositionCRUD(self.auth).create(data=data) + new_position = await PositionCRUD(self.auth, self.db).create(data=data) return PositionOutSchema.model_validate(new_position) async def update(self, id: int, data: PositionUpdateSchema) -> PositionOutSchema: - _ = await PositionCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_position = await PositionCRUD(self.auth).get(name=data.name) + _ = await PositionCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_position = await PositionCRUD(self.auth, self.db).get(name=data.name) if exist_position and exist_position.id != id: raise CustomException(msg="更新失败,名称已存在") - updated_position = await PositionCRUD(self.auth).update(id=id, data=data) + updated_position = await PositionCRUD(self.auth, self.db).update(id=id, data=data) return PositionOutSchema.model_validate(updated_position) async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - positions = await PositionCRUD(self.auth).get_list(search={"id": ("in", ids)}) + positions = await PositionCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) position_map = {p.id: p for p in positions} for pid in ids: if pid not in position_map: raise CustomException(msg="删除失败,该数据不存在") - await PositionCRUD(self.auth).delete(ids=ids) + await PositionCRUD(self.auth, self.db).delete(ids=ids) async def set_available(self, data: BatchSetAvailable) -> None: - positions = await PositionCRUD(self.auth).get_list(search={"id": ("in", data.ids)}) + positions = await PositionCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)}) position_map = {p.id: p for p in positions} for pid in data.ids: if pid not in position_map: raise CustomException(msg="该数据不存在") - await PositionCRUD(self.auth).set(ids=data.ids, status=data.status) + await PositionCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) @staticmethod def export_list(position_list: list[dict]) -> bytes: diff --git a/backend/app/api/v1/module_system/role/controller.py b/backend/app/api/v1/module_system/role/controller.py index ee5936db..587b691b 100644 --- a/backend/app/api/v1/module_system/role/controller.py +++ b/backend/app/api/v1/module_system/role/controller.py @@ -1,13 +1,14 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse, StreamingResponse from fastapi_cache import FastAPICache from fastapi_cache.decorator import cache +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, StreamResponse, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -23,13 +24,14 @@ _ROLE_NS = "role" @cache(expire=300, namespace=_ROLE_NS) async def get_role_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[RoleQueryParam, Query(description="角色查询参数")], ) -> JSONResponse: order_by = [{"order": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await RoleService(auth).page( + result_dict = await RoleService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -41,18 +43,20 @@ async def get_role_list_controller( @RoleRouter.get("/detail/{id}", summary="查询角色详情", response_model=ResponseSchema[RoleOutSchema]) async def get_role_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="角色ID", ge=1)], ) -> JSONResponse: - result_dict = await RoleService(auth).detail(id=id) + result_dict = await RoleService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取角色详情成功") @RoleRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建角色", response_model=ResponseSchema[RoleOutSchema]) async def create_role_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[RoleCreateSchema, Body(description="角色创建参数")], ) -> JSONResponse: - result_dict = await RoleService(auth).create(data=data) + result_dict = await RoleService(auth, db).create(data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(data=result_dict, msg="创建角色成功") @@ -60,10 +64,11 @@ async def create_role_controller( @RoleRouter.put("/update/{id}", summary="修改角色", response_model=ResponseSchema[RoleOutSchema]) async def update_role_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="角色ID", ge=1)], data: Annotated[RoleUpdateSchema, Body(description="角色修改参数")], ) -> JSONResponse: - result_dict = await RoleService(auth).update(id=id, data=data) + result_dict = await RoleService(auth, db).update(id=id, data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(data=result_dict, msg="修改角色成功") @@ -71,9 +76,10 @@ async def update_role_controller( @RoleRouter.delete("/delete", summary="删除角色", response_model=ResponseSchema[None]) async def delete_role_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await RoleService(auth).delete(ids=ids) + await RoleService(auth, db).delete(ids=ids) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(msg="删除角色成功") @@ -81,9 +87,10 @@ async def delete_role_controller( @RoleRouter.patch("/status/batch", summary="批量修改角色状态", response_model=ResponseSchema[None]) async def batch_set_available_role_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await RoleService(auth).set_available(data=data) + await RoleService(auth, db).set_available(data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(msg="批量修改角色状态成功") @@ -91,9 +98,10 @@ async def batch_set_available_role_controller( @RoleRouter.put("/permission", summary="角色授权", response_model=ResponseSchema[None]) async def set_role_permission_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:permission"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[RolePermissionSettingSchema, Body(description="角色授权参数")], ) -> JSONResponse: - await RoleService(auth).set_permission(data=data) + await RoleService(auth, db).set_permission(data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(msg="授权角色成功") @@ -101,17 +109,19 @@ async def set_role_permission_controller( @RoleRouter.get("/options", summary="获取角色下拉选项", response_model=ResponseSchema[list[dict[str, int | str]]]) async def get_role_options_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - options = await RoleService(auth).get_options() + options = await RoleService(auth, db).get_options() return SuccessResponse(data=options, msg="获取角色选项成功") @RoleRouter.get("/export", summary="导出角色") async def export_role_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:export"]))], + db: Annotated[AsyncSession, Depends(db_getter)], search: Annotated[RoleQueryParam, Query(description="角色查询参数")], ) -> StreamingResponse: - role_query_result = await RoleService(auth).get_list(search=search) + role_query_result = await RoleService(auth, db).get_list(search=search) role_export_result = RoleService.export_list(role_list=[item.model_dump() for item in role_query_result]) return StreamResponse( diff --git a/backend/app/api/v1/module_system/role/crud.py b/backend/app/api/v1/module_system/role/crud.py index fed1b3df..d743ac99 100644 --- a/backend/app/api/v1/module_system/role/crud.py +++ b/backend/app/api/v1/module_system/role/crud.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.api.v1.module_platform.menu.crud import MenuCRUD from app.api.v1.module_system.dept.crud import DeptCRUD from app.core.base_crud import CRUDBase @@ -13,8 +15,8 @@ from .schema import RoleCreateSchema, RoleUpdateSchema class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]): """角色模块数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=RoleModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=RoleModel, auth=auth, db=db) async def set_role_menus_crud(self, role_ids: list[int], menu_ids: list[int]) -> None: """设置角色的菜单权限 @@ -29,12 +31,12 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]): from app.api.v1.module_platform.package.service import PackageService roles = await self.get_list(search={"id": ("in", role_ids)}) - menus = [] if not menu_ids else await MenuCRUD(self.auth).get_list(search={"id": ("in", menu_ids)}) + menus = [] if not menu_ids else await MenuCRUD(self.auth, self.db).get_list(search={"id": ("in", menu_ids)}) # 非超管需校验菜单在租户套餐范围内 user = self.auth.user if user and not user.is_superuser and user.tenant_id: - allowed_set = set[int](await PackageService(self.auth).get_tenant_available_menu_ids(user.tenant_id)) + allowed_set = set[int](await PackageService(self.auth, self.db).get_tenant_available_menu_ids(user.tenant_id)) for menu in menus: if int(menu.id) not in allowed_set: raise CustomException(msg=f"菜单[{menu.name}]不在当前租户的功能组内,无法分配") @@ -42,7 +44,7 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]): for obj in roles: obj.menus.clear() obj.menus.extend(menus) - await self.auth.db.flush() + await self.db.flush() async def set_role_depts_crud(self, role_ids: list[int], dept_ids: list[int]) -> None: """设置角色的部门权限 @@ -55,13 +57,13 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]): - None """ roles = await self.get_list(search={"id": ("in", role_ids)}) - depts = [] if not dept_ids else await DeptCRUD(self.auth).get_list(search={"id": ("in", dept_ids)}) + depts = [] if not dept_ids else await DeptCRUD(self.auth, self.db).get_list(search={"id": ("in", dept_ids)}) for obj in roles: relationship = obj.depts relationship.clear() relationship.extend(depts) - await self.auth.db.flush() + await self.db.flush() async def get_options(self) -> list[dict[str, Any]]: """获取角色下拉选项,返回 [{value, label}]""" diff --git a/backend/app/api/v1/module_system/role/service.py b/backend/app/api/v1/module_system/role/service.py index 51ad5132..3e861ac7 100644 --- a/backend/app/api/v1/module_system/role/service.py +++ b/backend/app/api/v1/module_system/role/service.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema from app.core.exceptions import CustomException from app.utils.excel_util import ExcelUtil @@ -20,8 +22,9 @@ class RoleService: 提供角色 CRUD、权限配置、数据权限范围设置、批量启/禁用、Excel 导出等业务能力。 """ - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> RoleOutSchema: """获取角色详情 @@ -32,12 +35,12 @@ class RoleService: 返回: - RoleOutSchema: 角色详情响应模型 """ - obj = await RoleCRUD(self.auth).get_or_404(id=id) + obj = await RoleCRUD(self.auth, self.db).get_or_404(id=id) return RoleOutSchema.model_validate(obj) async def get_options(self) -> list[dict[str, Any]]: """获取角色下拉选项,委托给 RoleCRUD""" - return await RoleCRUD(self.auth).get_options() + return await RoleCRUD(self.auth, self.db).get_options() async def get_list( self, @@ -53,7 +56,7 @@ class RoleService: 返回: - list[RoleOutSchema]: 角色响应模型列表 """ - role_list = await RoleCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + role_list = await RoleCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [RoleOutSchema.model_validate(role) for role in role_list] async def page( @@ -75,7 +78,7 @@ class RoleService: - dict: 分页结果(结构由 ``CRUD.page`` 返回约定) """ offset = (page_no - 1) * page_size - return await RoleCRUD(self.auth).page( + return await RoleCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -95,17 +98,17 @@ class RoleService: 返回: - RoleOutSchema: 新创建的角色响应模型 """ - role = await RoleCRUD(self.auth).get(name=data.name) + role = await RoleCRUD(self.auth, self.db).get(name=data.name) if role: raise CustomException(msg="创建失败,该数据已存在") - obj = await RoleCRUD(self.auth).get(code=data.code) + obj = await RoleCRUD(self.auth, self.db).get(code=data.code) if obj: raise CustomException(msg="创建失败,编码已存在") # 检查租户配额 - await TenantService(self.auth).check_quota(self.auth.user.tenant_id, "role") + await TenantService(self.auth, self.db).check_quota(self.auth.user.tenant_id, "role") - new_role = await RoleCRUD(self.auth).create(data=data) + new_role = await RoleCRUD(self.auth, self.db).create(data=data) return RoleOutSchema.model_validate(new_role) async def update(self, id: int, data: RoleUpdateSchema) -> RoleOutSchema: @@ -118,14 +121,14 @@ class RoleService: 返回: - RoleOutSchema: 更新后的角色响应模型 """ - _ = await RoleCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_role = await RoleCRUD(self.auth).get(name=data.name) + _ = await RoleCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_role = await RoleCRUD(self.auth, self.db).get(name=data.name) if exist_role and exist_role.id != id: raise CustomException(msg="更新失败,名称已存在") - exist_code = await RoleCRUD(self.auth).get(code=data.code) + exist_code = await RoleCRUD(self.auth, self.db).get(code=data.code) if exist_code and exist_code.id != id: raise CustomException(msg="更新失败,角色编码已存在") - updated_role = await RoleCRUD(self.auth).update(id=id, data=data) + updated_role = await RoleCRUD(self.auth, self.db).update(id=id, data=data) return RoleOutSchema.model_validate(updated_role) async def delete(self, ids: list[int]) -> None: @@ -141,11 +144,11 @@ class RoleService: raise CustomException(msg="删除失败,删除对象不能为空") # 批量校验角色存在性 - roles = await RoleCRUD(self.auth).get_list(search={"id": ("in", ids)}) + roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) if len(roles) != len(ids): raise CustomException(msg="删除失败,部分ID不存在") - await RoleCRUD(self.auth).delete(ids=ids) + await RoleCRUD(self.auth, self.db).delete(ids=ids) async def set_permission(self, data: RolePermissionSettingSchema) -> None: """设置角色权限 @@ -157,16 +160,16 @@ class RoleService: - None """ # 设置角色菜单权限 - await RoleCRUD(self.auth).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids) + await RoleCRUD(self.auth, self.db).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids) # 设置数据权限范围 - await RoleCRUD(self.auth).set(ids=data.role_ids, data_scope=data.data_scope) + await RoleCRUD(self.auth, self.db).set(ids=data.role_ids, data_scope=data.data_scope) # 设置自定义数据权限部门 if data.data_scope == 5 and data.dept_ids: - await RoleCRUD(self.auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=data.dept_ids) + await RoleCRUD(self.auth, self.db).set_role_depts_crud(role_ids=data.role_ids, dept_ids=data.dept_ids) else: - await RoleCRUD(self.auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[]) + await RoleCRUD(self.auth, self.db).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[]) async def set_available(self, data: BatchSetAvailable) -> None: """设置角色可用状态 @@ -177,12 +180,12 @@ class RoleService: 返回: - None """ - roles = await RoleCRUD(self.auth).get_list(search={"id": ("in", data.ids)}) + roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)}) role_map = {r.id: r for r in roles} for rid in data.ids: if rid not in role_map: raise CustomException(msg="该数据不存在") - await RoleCRUD(self.auth).set(ids=data.ids, status=data.status) + await RoleCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) @staticmethod def export_list(role_list: list[dict[str, Any]]) -> bytes: diff --git a/backend/app/api/v1/module_system/ticket/controller.py b/backend/app/api/v1/module_system/ticket/controller.py index dc63f7eb..9d0696f5 100644 --- a/backend/app/api/v1/module_system/ticket/controller.py +++ b/backend/app/api/v1/module_system/ticket/controller.py @@ -1,11 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import TicketBatchSchema, TicketCommentCreateSchema, TicketCommentOutSchema, TicketCreateSchema, TicketOutSchema, TicketQueryParam, TicketUpdateSchema @@ -17,10 +18,11 @@ TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=[ @TicketRouter.get("/list", summary="工单列表", response_model=ResponseSchema[PageResultSchema[TicketOutSchema]]) async def ticket_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[TicketQueryParam, Query(description="工单查询参数")], ) -> JSONResponse: - result = await TicketService(auth).page( + result = await TicketService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -32,64 +34,71 @@ async def ticket_list_controller( @TicketRouter.get("/detail/{id}", summary="获取工单详情", response_model=ResponseSchema[TicketOutSchema]) async def ticket_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="工单ID")], ) -> JSONResponse: - result = await TicketService(auth).detail(id=id) + result = await TicketService(auth, db).detail(id=id) return SuccessResponse(data=result, msg="查询成功") @TicketRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建工单", response_model=ResponseSchema[TicketOutSchema]) async def ticket_create_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[TicketCreateSchema, Body(description="工单创建参数")], ) -> JSONResponse: - result = await TicketService(auth).create(data=data) + result = await TicketService(auth, db).create(data=data) return SuccessResponse(data=result, msg="创建成功") @TicketRouter.put("/update/{id}", summary="更新工单", response_model=ResponseSchema[TicketOutSchema]) async def ticket_update_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="工单ID", ge=1)], data: Annotated[TicketUpdateSchema, Body(description="工单更新参数")], ) -> JSONResponse: - result = await TicketService(auth).update(id=id, data=data) + result = await TicketService(auth, db).update(id=id, data=data) return SuccessResponse(data=result, msg="更新成功") @TicketRouter.put("/batch", summary="批量更新工单", response_model=ResponseSchema) async def ticket_batch_update_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[TicketBatchSchema, Body(description="工单批量更新参数")], ) -> JSONResponse: - await TicketService(auth).batch(data=data) + await TicketService(auth, db).batch(data=data) return SuccessResponse(msg="批量操作成功") @TicketRouter.delete("/delete", summary="删除工单", response_model=ResponseSchema[None]) async def ticket_delete_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="工单ID列表")], ) -> JSONResponse: - await TicketService(auth).delete(ids=ids) + await TicketService(auth, db).delete(ids=ids) return SuccessResponse(msg="删除成功") @TicketRouter.get("/{ticket_id}/comments", summary="工单评论列表", response_model=ResponseSchema[PageResultSchema[TicketCommentOutSchema]]) async def ticket_comment_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ticket_id: Annotated[int, Path(description="工单ID")], page: Annotated[PaginationQueryParam, Query(description="分页参数")], ) -> JSONResponse: - result = await TicketCommentService(auth).page(ticket_id=ticket_id, page_no=page.page_no, page_size=page.page_size) + result = await TicketCommentService(auth, db).page(ticket_id=ticket_id, page_no=page.page_no, page_size=page.page_size) return SuccessResponse(data=result, msg="查询成功") @TicketRouter.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED, summary="创建评论", response_model=ResponseSchema[TicketCommentOutSchema]) async def ticket_comment_create_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ticket_id: Annotated[int, Path(description="工单ID")], data: Annotated[TicketCommentCreateSchema, Body(description="评论内容")], ) -> JSONResponse: - result = await TicketCommentService(auth).create(ticket_id=ticket_id, data=data) + result = await TicketCommentService(auth, db).create(ticket_id=ticket_id, data=data) return SuccessResponse(data=result, msg="评论成功") diff --git a/backend/app/api/v1/module_system/ticket/crud.py b/backend/app/api/v1/module_system/ticket/crud.py index 7e746a9a..63e88d1f 100644 --- a/backend/app/api/v1/module_system/ticket/crud.py +++ b/backend/app/api/v1/module_system/ticket/crud.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -10,12 +12,12 @@ from .schema import TicketCommentCreateSchema, TicketCreateSchema, TicketUpdateS class TicketCRUD(CRUDBase[TicketModel, TicketCreateSchema, TicketUpdateSchema]): """工单 CRUD""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=TicketModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=TicketModel, auth=auth, db=db) class TicketCommentCRUD(CRUDBase[TicketCommentModel, TicketCommentCreateSchema, Any]): """工单评论 CRUD""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=TicketCommentModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=TicketCommentModel, auth=auth, db=db) diff --git a/backend/app/api/v1/module_system/ticket/service.py b/backend/app/api/v1/module_system/ticket/service.py index 5bd2338b..90c055d6 100644 --- a/backend/app/api/v1/module_system/ticket/service.py +++ b/backend/app/api/v1/module_system/ticket/service.py @@ -1,4 +1,5 @@ from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_system.user.model import UserModel from app.core.base_schema import AuthSchema, PageResultSchema @@ -33,8 +34,9 @@ _TICKET_STATUS_LABELS = { class TicketService: """工单管理服务""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db def _validate_status_transition(self, ticket, new_status: int) -> None: old_status = ticket.status if ticket.status is not None else 0 @@ -75,7 +77,7 @@ class TicketService: search: TicketQueryParam | None = None, order_by: list | None = None, ) -> PageResultSchema[TicketOutSchema]: - return await TicketCRUD(self.auth).page( + return await TicketCRUD(self.auth, self.db).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"created_time": "desc"}], @@ -84,17 +86,17 @@ class TicketService: ) async def detail(self, id: int) -> TicketOutSchema: - obj = await TicketCRUD(self.auth).get_or_404(id=id) + obj = await TicketCRUD(self.auth, self.db).get_or_404(id=id) return TicketOutSchema.model_validate(obj) async def create(self, data: TicketCreateSchema) -> TicketOutSchema: - obj = await TicketCRUD(self.auth).create(data=data) + obj = await TicketCRUD(self.auth, self.db).create(data=data) if not obj: raise CustomException(msg="创建工单失败") return TicketOutSchema.model_validate(obj) async def update(self, id: int, data: TicketUpdateSchema) -> TicketOutSchema: - obj = await TicketCRUD(self.auth).get_or_404(id=id, msg="工单不存在") + obj = await TicketCRUD(self.auth, self.db).get_or_404(id=id, msg="工单不存在") if data.status is not None: self._validate_status_transition(obj, data.status) @@ -104,14 +106,14 @@ class TicketService: UserModel.id == data.assigned_id, UserModel.is_deleted.is_(False), ) - user_result = await self.auth.db.execute(user_stmt) + user_result = await self.db.execute(user_stmt) assigned_user = user_result.scalar_one_or_none() if not assigned_user: raise CustomException(msg="指定的处理人不存在") if assigned_user.tenant_id != obj.tenant_id: raise CustomException(msg="处理人必须与工单属于同一租户") - updated = await TicketCRUD(self.auth).update(id=id, data=data) + updated = await TicketCRUD(self.auth, self.db).update(id=id, data=data) if not updated: raise CustomException(msg="工单不存在") @@ -134,30 +136,31 @@ class TicketService: async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除对象不能为空") - await TicketCRUD(self.auth).delete(ids=ids) + await TicketCRUD(self.auth, self.db).delete(ids=ids) async def batch(self, data: TicketBatchSchema) -> None: if not data.ids: raise CustomException(msg="请选择要操作的工单") - tickets = await TicketCRUD(self.auth).get_list(search={"id": ("in", data.ids)}) + tickets = await TicketCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)}) ticket_map = {t.id: t for t in tickets} for tid in data.ids: obj = ticket_map.get(tid) if not obj: raise CustomException(msg=f"工单[{tid}]不存在") self._validate_status_transition(obj, data.status) - await TicketCRUD(self.auth).set(ids=data.ids, status=data.status) + await TicketCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) class TicketCommentService: """工单评论服务""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def page(self, ticket_id: int, page_no: int, page_size: int) -> PageResultSchema[TicketCommentOutSchema]: - return await TicketCommentCRUD(self.auth).page( + return await TicketCommentCRUD(self.auth, self.db).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=[{"created_time": "desc"}], @@ -167,13 +170,13 @@ class TicketCommentService: async def create(self, ticket_id: int, data: TicketCommentCreateSchema) -> TicketCommentOutSchema: # 验证工单存在 - await TicketCRUD(self.auth).get_or_404(id=ticket_id, msg="工单不存在") + await TicketCRUD(self.auth, self.db).get_or_404(id=ticket_id, msg="工单不存在") create_data = data.model_dump() | {"ticket_id": ticket_id} - obj = await TicketCommentCRUD(self.auth).create(data=create_data) # type: ignore[arg-type] + obj = await TicketCommentCRUD(self.auth, self.db).create(data=create_data) # type: ignore[arg-type] if not obj: raise CustomException(msg="评论失败") return TicketCommentOutSchema.model_validate(obj) async def delete(self, comment_id: int) -> None: - await TicketCommentCRUD(self.auth).get_or_404(id=comment_id, msg="评论不存在") - await TicketCommentCRUD(self.auth).delete(ids=[comment_id]) + await TicketCommentCRUD(self.auth, self.db).get_or_404(id=comment_id, msg="评论不存在") + await TicketCommentCRUD(self.auth, self.db).delete(ids=[comment_id]) diff --git a/backend/app/api/v1/module_system/user/controller.py b/backend/app/api/v1/module_system/user/controller.py index ad216a7d..9db29ad0 100644 --- a/backend/app/api/v1/module_system/user/controller.py +++ b/backend/app/api/v1/module_system/user/controller.py @@ -30,37 +30,41 @@ UserRouter = APIRouter(route_class=OperationLogRoute, prefix="/user", tags=["用 @UserRouter.get("/current/info", summary="查询当前用户信息", response_model=ResponseSchema[UserOutSchema]) async def get_current_user_info_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - user_dict = await UserService(auth).current_info() + user_dict = await UserService(auth, db).current_info() return SuccessResponse(data=user_dict, msg="获取当前用户信息成功") @UserRouter.put("/current/info/update", summary="更新当前用户基本信息", response_model=ResponseSchema[UserOutSchema]) async def update_current_user_info_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[CurrentUserUpdateSchema, Body(description="更新用户基本信息参数")], ) -> JSONResponse: - result_dict = await UserService(auth).update_current_info(data=data) + result_dict = await UserService(auth, db).update_current_info(data=data) return SuccessResponse(data=result_dict, msg="更新当前用户基本信息成功") @UserRouter.put("/password/change", summary="修改当前用户密码", response_model=ResponseSchema[UserOutSchema]) async def change_current_user_password_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[UserChangePasswordSchema, Body(description="修改用户密码参数")], ) -> JSONResponse: - result_dict = await UserService(auth).change_password(data=data) + result_dict = await UserService(auth, db).change_password(data=data) return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录") @UserRouter.put("/password/reset/{id}", summary="重置用户密码", response_model=ResponseSchema[UserOutSchema]) async def reset_password_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="用户ID", ge=1)], data: Annotated[ResetPasswordSchema, Body(description="重置用户密码参数")], ) -> JSONResponse: data.id = id - result_dict = await UserService(auth).reset_password(data=data) + result_dict = await UserService(auth, db).reset_password(data=data) return SuccessResponse(data=result_dict, msg="重置密码成功") @@ -69,8 +73,8 @@ async def forget_password_controller( db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[UserForgetPasswordSchema, Body(description="忘记密码参数")], ) -> JSONResponse: - auth = AuthSchema.anonymous(db=db) - user_forget_password_result = await UserService(auth).forget_password(data=data) + auth = AuthSchema(check_data_scope=False) + user_forget_password_result = await UserService(auth, db).forget_password(data=data) logger.info(f"{data.username} 重置密码成功") return SuccessResponse(data=user_forget_password_result, msg="重置密码成功") @@ -78,10 +82,11 @@ async def forget_password_controller( @UserRouter.get("/list", summary="查询用户", response_model=ResponseSchema[PageResultSchema[UserOutSchema]]) async def get_user_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[UserQueryParam, Query(description="用户查询参数")], ) -> JSONResponse: - result_dict = await UserService(auth).page( + result_dict = await UserService(auth, db).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -93,46 +98,51 @@ async def get_user_list_controller( @UserRouter.get("/detail/{id}", summary="查询用户详情", response_model=ResponseSchema[UserOutSchema]) async def get_user_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="用户ID", ge=1)], ) -> JSONResponse: - result_dict = await UserService(auth).detail(id=id) + result_dict = await UserService(auth, db).detail(id=id) return SuccessResponse(data=result_dict, msg="获取用户详情成功") @UserRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建用户", response_model=ResponseSchema[UserOutSchema]) async def create_user_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[UserCreateSchema, Body(description="创建用户参数")], ) -> JSONResponse: - result_dict = await UserService(auth).create(data=data) + result_dict = await UserService(auth, db).create(data=data) return SuccessResponse(data=result_dict, msg="创建用户成功") @UserRouter.put("/update/{id}", summary="修改用户", response_model=ResponseSchema[UserOutSchema]) async def update_user_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="用户ID")], data: Annotated[UserUpdateSchema, Body(description="修改用户参数")], ) -> JSONResponse: - result_dict = await UserService(auth).update(id=id, data=data) + result_dict = await UserService(auth, db).update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改用户成功") @UserRouter.delete("/delete", summary="删除用户", response_model=ResponseSchema[None]) async def delete_user_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await UserService(auth).delete(ids=ids) + await UserService(auth, db).delete(ids=ids) return SuccessResponse(msg="删除用户成功") @UserRouter.patch("/status/batch", summary="批量修改用户状态", response_model=ResponseSchema[None]) async def batch_set_available_user_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[BatchSetAvailable, Body(description="状态设置")], ) -> JSONResponse: - await UserService(auth).set_available(data=data) + await UserService(auth, db).set_available(data=data) return SuccessResponse(msg="批量修改用户状态成功") @@ -153,10 +163,11 @@ async def export_user_import_template_controller() -> StreamingResponse: @UserRouter.get("/export", summary="导出用户") async def export_user_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:export"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[UserQueryParam, Query(description="用户查询参数")], ) -> StreamingResponse: - user_list = await UserService(auth).get_list(search=search, order_by=page.order_by) + user_list = await UserService(auth, db).get_list(search=search, order_by=page.order_by) user_export_result = UserService.export_list(user_list=[item.model_dump() for item in user_list]) return StreamResponse( @@ -170,6 +181,7 @@ async def export_user_list_controller( async def import_user_list_controller( file: Annotated[UploadFile, File(description="用户导入文件")], auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:import"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - batch_import_result = await UserService(auth).batch_import(file=file, update_support=True) + batch_import_result = await UserService(auth, db).batch_import(file=file, update_support=True) return SuccessResponse(data=batch_import_result, msg="导入用户成功") diff --git a/backend/app/api/v1/module_system/user/crud.py b/backend/app/api/v1/module_system/user/crud.py index 38a2bda2..4c6dbc83 100644 --- a/backend/app/api/v1/module_system/user/crud.py +++ b/backend/app/api/v1/module_system/user/crud.py @@ -1,5 +1,7 @@ from datetime import datetime +from sqlalchemy.ext.asyncio import AsyncSession + from app.api.v1.module_system.position.crud import PositionCRUD from app.api.v1.module_system.role.crud import RoleCRUD from app.core.base_crud import CRUDBase @@ -15,8 +17,8 @@ from .schema import ( class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): """用户模块数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=UserModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=UserModel, auth=auth, db=db) async def update_last_login(self, id: int) -> None: """更新用户最后登录时间 @@ -40,7 +42,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): user_objs = await self.get_list(search={"id": ("in", user_ids)}) if role_ids: - role_objs = await RoleCRUD(self.auth).get_list(search={"id": ("in", role_ids)}) + role_objs = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", role_ids)}) auth_user = self.auth.user if auth_user and not auth_user.is_superuser: for role in role_objs: @@ -53,7 +55,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): relationship = obj.roles relationship.clear() relationship.extend(role_objs) - await self.auth.db.flush() + await self.db.flush() async def set_user_positions(self, user_ids: list[int], position_ids: list[int]) -> None: """批量设置用户岗位(带租户隔离验证) @@ -69,7 +71,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): user_objs = await self.get_list(search={"id": ("in", user_ids)}) if position_ids: - position_objs = await PositionCRUD(self.auth).get_list(search={"id": ("in", position_ids)}) + position_objs = await PositionCRUD(self.auth, self.db).get_list(search={"id": ("in", position_ids)}) auth_user = self.auth.user if auth_user and not auth_user.is_superuser: for position in position_objs: @@ -82,7 +84,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): relationship = obj.positions relationship.clear() relationship.extend(position_objs) - await self.auth.db.flush() + await self.db.flush() async def change_password(self, id: int, password_hash: str) -> UserModel: """修改用户密码 diff --git a/backend/app/api/v1/module_system/user/schema.py b/backend/app/api/v1/module_system/user/schema.py index d2356347..3cda0c15 100644 --- a/backend/app/api/v1/module_system/user/schema.py +++ b/backend/app/api/v1/module_system/user/schema.py @@ -12,7 +12,7 @@ from pydantic import ( from app.api.v1.module_platform.menu.schema import MenuOutSchema from app.api.v1.module_system.role.schema import RoleOutSchema from app.common.enums import QueueEnum -from app.core.base_schema import BaseQueryParam, BaseSchema, CommonSchema, TenantByQueryParam, TenantBySchema, UserByQueryParam, UserBySchema +from app.core.base_schema import BaseQueryParam, BaseSchema, CommonSchema, CoreUserSchema, TenantByQueryParam, TenantBySchema, UserByQueryParam, UserBySchema from app.core.validator import email_validator, mobile_validator @@ -219,11 +219,13 @@ class UserUpdateSchema(CurrentUserUpdateSchema): return v -class UserOutSchema(BaseSchema, UserBySchema, TenantBySchema): +class UserOutSchema(CoreUserSchema, BaseSchema, UserBySchema, TenantBySchema): """响应""" model_config = ConfigDict(arbitrary_types_allowed=True, from_attributes=True) + id: int = Field(default=0, description="主键ID") + tenant_id: int = Field(default=0, description="租户ID") username: str | None = Field(default=None, max_length=32, description="用户名") name: str | None = Field(default=None, max_length=32, description="名称") mobile: str | None = Field(default=None, max_length=11, description="手机号") @@ -245,6 +247,8 @@ class UserOutSchema(BaseSchema, UserBySchema, TenantBySchema): roles: list[RoleOutSchema] | None = Field(default=[], description="角色") menus: list[MenuOutSchema] | None = Field(default=[], description="菜单") is_impersonate: bool = Field(default=False, description="是否为平台管理员代签入") + is_superuser: bool = Field(default=False, description="是否超管") + tenant: CommonSchema | None = Field(default=None, description="租户") class UserQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam): diff --git a/backend/app/api/v1/module_system/user/service.py b/backend/app/api/v1/module_system/user/service.py index 61b52156..cfc37165 100644 --- a/backend/app/api/v1/module_system/user/service.py +++ b/backend/app/api/v1/module_system/user/service.py @@ -1,6 +1,7 @@ from typing import Any from fastapi import UploadFile +from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_system.dept.crud import DeptCRUD from app.api.v1.module_system.position.crud import PositionCRUD @@ -10,7 +11,7 @@ from app.core.exceptions import CustomException from app.core.logger import logger from app.utils.common_util import traversal_to_tree from app.utils.excel_util import ExcelUtil -from app.utils.hash_bcrpy_util import PwdUtil +from app.utils.password_util import PwdUtil from .crud import UserCRUD from .schema import ( @@ -28,14 +29,15 @@ from .schema import ( class UserService: """用户管理服务""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> UserOutSchema: - user = await UserCRUD(self.auth).get_or_404(id=id) + user = await UserCRUD(self.auth, self.db).get_or_404(id=id) result = UserOutSchema.model_validate(user) if user.dept_id: - dept = await DeptCRUD(self.auth).get(id=user.dept_id) + dept = await DeptCRUD(self.auth, self.db).get(id=user.dept_id) result.dept_name = dept.name if dept else None return result @@ -44,7 +46,7 @@ class UserService: search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[UserOutSchema]: - user_list = await UserCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + user_list = await UserCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [UserOutSchema.model_validate(user) for user in user_list] async def page( @@ -55,7 +57,7 @@ class UserService: order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[UserOutSchema]: offset = (page_no - 1) * page_size - return await UserCRUD(self.auth).page( + return await UserCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -70,76 +72,76 @@ class UserService: raise CustomException(msg="用户名不能为空") if data.is_superuser: raise CustomException(msg="不允许创建超级管理员") - user = await UserCRUD(self.auth).get(username=data.username) + user = await UserCRUD(self.auth, self.db).get(username=data.username) if user: raise CustomException(msg="已存在相同用户名称的账号") if data.dept_id: - dept = await DeptCRUD(self.auth).get(id=data.dept_id) + dept = await DeptCRUD(self.auth, self.db).get(id=data.dept_id) if not dept: raise CustomException(msg="该数据不存在") - await TenantService(self.auth).check_quota(self.auth.user.tenant_id, "user") + await TenantService(self.auth, self.db).check_quota(self.auth.user.tenant_id, "user") if data.password: data.password = PwdUtil.hash_password(password=data.password) - new_user = await UserCRUD(self.auth).create(data=data) + new_user = await UserCRUD(self.auth, self.db).create(data=data) if data.role_ids and len(data.role_ids) > 0: - await UserCRUD(self.auth).set_user_roles(user_ids=[new_user.id], role_ids=data.role_ids) + await UserCRUD(self.auth, self.db).set_user_roles(user_ids=[new_user.id], role_ids=data.role_ids) if data.position_ids and len(data.position_ids) > 0: - await UserCRUD(self.auth).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids) + await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids) return UserOutSchema.model_validate(new_user) async def update(self, id: int, data: UserUpdateSchema) -> UserOutSchema: if not data.username: raise CustomException(msg="账号不能为空") - user = await UserCRUD(self.auth).get_or_404(id=id) + user = await UserCRUD(self.auth, self.db).get_or_404(id=id) if user.is_superuser: raise CustomException(msg="超级管理员不允许修改") - exist_user = await UserCRUD(self.auth).get(username=data.username) + exist_user = await UserCRUD(self.auth, self.db).get(username=data.username) if exist_user and exist_user.id != id: raise CustomException(msg="更新失败,账号已存在") if data.mobile: - exist_mobile_user = await UserCRUD(self.auth).get(mobile=data.mobile) + exist_mobile_user = await UserCRUD(self.auth, self.db).get(mobile=data.mobile) if exist_mobile_user and exist_mobile_user.id != id: raise CustomException(msg="该数据已存在") if data.email: - exist_email_user = await UserCRUD(self.auth).get(email=data.email) + exist_email_user = await UserCRUD(self.auth, self.db).get(email=data.email) if exist_email_user and exist_email_user.id != id: raise CustomException(msg="该数据已存在") if data.dept_id: - dept = await DeptCRUD(self.auth).get(id=data.dept_id) + dept = await DeptCRUD(self.auth, self.db).get(id=data.dept_id) if not dept: raise CustomException(msg="该数据不存在") if dept.status == 1: raise CustomException(msg="部门已被禁用") - new_user = await UserCRUD(self.auth).update(id=id, data=data) + new_user = await UserCRUD(self.auth, self.db).update(id=id, data=data) if data.role_ids and len(data.role_ids) > 0: - roles = await RoleCRUD(self.auth).get_list(search={"id": ("in", data.role_ids)}) + roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", data.role_ids)}) if len(roles) != len(data.role_ids): raise CustomException(msg="更新失败,部分角色不存在") if not all(role.status == 0 for role in roles): raise CustomException(msg="更新失败,部分角色已被禁用") - await UserCRUD(self.auth).set_user_roles(user_ids=[id], role_ids=data.role_ids) + await UserCRUD(self.auth, self.db).set_user_roles(user_ids=[id], role_ids=data.role_ids) if data.position_ids and len(data.position_ids) > 0: - positions = await PositionCRUD(self.auth).get_list(search={"id": ("in", data.position_ids)}) + positions = await PositionCRUD(self.auth, self.db).get_list(search={"id": ("in", data.position_ids)}) if len(positions) != len(data.position_ids): raise CustomException(msg="更新失败,部分岗位不存在") if not all(position.status == 0 for position in positions): raise CustomException(msg="更新失败,部分岗位已被禁用") - await UserCRUD(self.auth).set_user_positions(user_ids=[id], position_ids=data.position_ids) + await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[id], position_ids=data.position_ids) return UserOutSchema.model_validate(new_user) async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - users = await UserCRUD(self.auth).get_list(search={"id": ("in", ids)}) + users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) user_map = {u.id: u for u in users} for uid in ids: user = user_map.get(uid) @@ -152,9 +154,9 @@ class UserService: if self.auth.user.id == uid: raise CustomException(msg="不能删除当前登陆用户") - await UserCRUD(self.auth).set_user_roles(user_ids=ids, role_ids=[]) - await UserCRUD(self.auth).set_user_positions(user_ids=ids, position_ids=[]) - await UserCRUD(self.auth).delete(ids=ids) + await UserCRUD(self.auth, self.db).set_user_roles(user_ids=ids, role_ids=[]) + await UserCRUD(self.auth, self.db).set_user_positions(user_ids=ids, position_ids=[]) + await UserCRUD(self.auth, self.db).delete(ids=ids) async def current_info(self) -> UserOutSchema: from app.api.v1.module_platform.menu.crud import MenuCRUD @@ -164,7 +166,7 @@ class UserService: if not self.auth.user.id: raise CustomException(msg="该数据不存在") - user = await UserCRUD(self.auth).get(id=self.auth.user.id) + user = await UserCRUD(self.auth, self.db).get(id=self.auth.user.id) user_dict = UserOutSchema.model_validate(user) if user and user.dept: user_dict.dept_name = user.dept.name @@ -175,7 +177,7 @@ class UserService: _pc_only = {"client": "pc"} if self.auth.user.is_superuser: scope_filter = {"scope": "tenant"} if self.auth.user.tenant_id else {"scope": "platform"} - menu_all = await MenuCRUD(self.auth).tree_list( + menu_all = await MenuCRUD(self.auth, self.db).tree_list( search={"type": ("in", [1, 2, 3, 4]), "status": 0, **_pc_only, **scope_filter}, order_by=[{"order": "asc"}], ) @@ -184,14 +186,14 @@ class UserService: menu_ids = set(self.auth.session_info.get("menu_ids", [])) if self.auth.session_info else set() if menu_ids and self.auth.user.tenant_id: - allowed_ids = await PackageService(self.auth).get_tenant_available_menu_ids(self.auth.user.tenant_id) + allowed_ids = await PackageService(self.auth, self.db).get_tenant_available_menu_ids(self.auth.user.tenant_id) allowed_set = set(allowed_ids) menu_ids = menu_ids & allowed_set menus = ( [ MenuOutSchema.model_validate(menu) - for menu in await MenuCRUD(self.auth).tree_list( + for menu in await MenuCRUD(self.auth, self.db).tree_list( search={"id": ("in", list(menu_ids)), **_pc_only}, order_by=[{"order": "asc"}], ) @@ -206,29 +208,29 @@ class UserService: async def update_current_info(self, data: CurrentUserUpdateSchema) -> UserOutSchema: if not self.auth.user.id: raise CustomException(msg="该数据不存在") - user = await UserCRUD(self.auth).get(id=self.auth.user.id) + user = await UserCRUD(self.auth, self.db).get(id=self.auth.user.id) if not user: raise CustomException(msg="该数据不存在") if user.is_superuser: raise CustomException(msg="超级管理员不能修改个人信息") if data.mobile: - exist_mobile_user = await UserCRUD(self.auth).get(mobile=data.mobile) + exist_mobile_user = await UserCRUD(self.auth, self.db).get(mobile=data.mobile) if exist_mobile_user and exist_mobile_user.id != self.auth.user.id: raise CustomException(msg="该数据已存在") if data.email: - exist_email_user = await UserCRUD(self.auth).get(email=data.email) + exist_email_user = await UserCRUD(self.auth, self.db).get(email=data.email) if exist_email_user and exist_email_user.id != self.auth.user.id: raise CustomException(msg="该数据已存在") user_update_data = UserUpdateSchema(**data.model_dump()) - new_user = await UserCRUD(self.auth).update(id=self.auth.user.id, data=user_update_data) + new_user = await UserCRUD(self.auth, self.db).update(id=self.auth.user.id, data=user_update_data) return UserOutSchema.model_validate(new_user) async def set_available(self, data: BatchSetAvailable) -> None: - users = await UserCRUD(self.auth).get_list(search={"id": ("in", list(data.ids))}) + users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", list(data.ids))}) for user in users: if user.is_superuser: raise CustomException(msg="超级管理员状态不能修改") - await UserCRUD(self.auth).set(ids=data.ids, status=data.status) + await UserCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) async def change_password(self, data: UserChangePasswordSchema) -> UserOutSchema: if not self.auth.user.id: @@ -236,21 +238,21 @@ class UserService: if not data.old_password or not data.new_password: raise CustomException(msg="密码不能为空") - user = await UserCRUD(self.auth).get(id=self.auth.user.id) + user = await UserCRUD(self.auth, self.db).get(id=self.auth.user.id) if not user: raise CustomException(msg="该数据不存在") if not PwdUtil.verify_password(plain_password=data.old_password, password_hash=user.password): raise CustomException(msg="原密码输入错误") new_password_hash = PwdUtil.hash_password(password=data.new_password) - new_user = await UserCRUD(self.auth).change_password(id=user.id, password_hash=new_password_hash) + new_user = await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user) async def reset_password(self, data: ResetPasswordSchema) -> UserOutSchema: if not data.password: raise CustomException(msg="密码不能为空") - user = await UserCRUD(self.auth).get(id=data.id) + user = await UserCRUD(self.auth, self.db).get(id=data.id) if not user: raise CustomException(msg="该数据不存在") @@ -258,7 +260,7 @@ class UserService: raise CustomException(msg="超级管理员密码不能重置") new_password_hash = PwdUtil.hash_password(password=data.password) - new_user = await UserCRUD(self.auth).change_password(id=data.id, password_hash=new_password_hash) + new_user = await UserCRUD(self.auth, self.db).change_password(id=data.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user) async def forget_password(self, data: UserForgetPasswordSchema) -> UserOutSchema: @@ -276,13 +278,13 @@ class UserService: ) .limit(1) ) - result = await self.auth.db.execute(tenant_stmt) + result = await self.db.execute(tenant_stmt) tenant = result.scalar_one_or_none() if not tenant: raise CustomException(msg="租户不存在") # 在租户范围内查找用户 - user = await UserCRUD(self.auth).get(username=data.username, tenant_id=tenant.id) + user = await UserCRUD(self.auth, self.db).get(username=data.username, tenant_id=tenant.id) if not user: raise CustomException(msg="该数据不存在") if user.status == 1: @@ -295,7 +297,7 @@ class UserService: raise CustomException(msg="手机号不匹配") new_password_hash = PwdUtil.hash_password(password=data.new_password) - new_user = await UserCRUD(self.auth).forget_password(id=user.id, password_hash=new_password_hash) + new_user = await UserCRUD(self.auth, self.db).forget_password(id=user.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user) async def batch_import(self, file: UploadFile, update_support: bool = False) -> str: @@ -351,7 +353,7 @@ class UserService: continue dept_id = int(row["dept_id"]) - dept = await DeptCRUD(self.auth).get(id=dept_id) + dept = await DeptCRUD(self.auth, self.db).get(id=dept_id) if not dept: error_msgs.append(f"第{i}行: 部门ID {dept_id} 不存在") continue @@ -370,24 +372,24 @@ class UserService: "password": PwdUtil.hash_password(password="123456"), } - exists_user = await UserCRUD(self.auth).get(username=user_data["username"]) + exists_user = await UserCRUD(self.auth, self.db).get(username=user_data["username"]) if exists_user: if exists_user.is_superuser: error_msgs.append(f"第{i}行: 超级管理员不允许修改") continue if update_support: user_update_data = UserUpdateSchema(**user_data) - await UserCRUD(self.auth).update(id=exists_user.id, data=user_update_data) + await UserCRUD(self.auth, self.db).update(id=exists_user.id, data=user_update_data) success_count += 1 else: error_msgs.append(f"第{i}行: 用户 {user_data['username']} 已存在") else: user_create_schema = UserCreateSchema(**user_data) - new_user = await UserCRUD(self.auth).create(data=user_create_schema) + new_user = await UserCRUD(self.auth, self.db).create(data=user_create_schema) if user_create_schema.role_ids and len(user_create_schema.role_ids) > 0: - await UserCRUD(self.auth).set_user_roles(user_ids=[new_user.id], role_ids=user_create_schema.role_ids) + await UserCRUD(self.auth, self.db).set_user_roles(user_ids=[new_user.id], role_ids=user_create_schema.role_ids) if user_create_schema.position_ids and len(user_create_schema.position_ids) > 0: - await UserCRUD(self.auth).set_user_positions(user_ids=[new_user.id], position_ids=user_create_schema.position_ids) + await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[new_user.id], position_ids=user_create_schema.position_ids) success_count += 1 except Exception as e: diff --git a/backend/app/api/v1/module_system/versions/controller.py b/backend/app/api/v1/module_system/versions/controller.py index b2d51a63..91576f33 100644 --- a/backend/app/api/v1/module_system/versions/controller.py +++ b/backend/app/api/v1/module_system/versions/controller.py @@ -24,10 +24,11 @@ VersionRouter = APIRouter(route_class=OperationLogRoute, prefix="/versions", tag @VersionRouter.get("/list", summary="分页查询版本", response_model=ResponseSchema[PageResultSchema[VersionOutSchema]]) async def get_version_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[VersionQueryParam, Query(description="查询参数")], ) -> JSONResponse: - service = VersionService(auth) + service = VersionService(auth, db) result = await service.page(page_no=page.page_no, page_size=page.page_size, search=search) return SuccessResponse(data=result, msg="查询版本列表成功") @@ -36,8 +37,8 @@ async def get_version_list_controller( async def get_published_versions_controller( db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - auth = AuthSchema.anonymous(db=db) - service = VersionService(auth) + auth = AuthSchema(check_data_scope=False) + service = VersionService(auth, db) result = await service.get_published() return SuccessResponse(data=result, msg="查询成功") @@ -45,9 +46,10 @@ async def get_published_versions_controller( @VersionRouter.get("/detail/{id}", summary="获取版本详情", response_model=ResponseSchema[VersionOutSchema]) async def get_version_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="版本ID")], ) -> JSONResponse: - service = VersionService(auth) + service = VersionService(auth, db) result = await service.detail(id=id) return SuccessResponse(data=result, msg="获取版本详情成功") @@ -55,9 +57,10 @@ async def get_version_detail_controller( @VersionRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建版本", response_model=ResponseSchema[VersionOutSchema]) async def create_version_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[VersionCreateSchema, Body(description="创建参数")], ) -> JSONResponse: - service = VersionService(auth) + service = VersionService(auth, db) result = await service.create(data=data) return SuccessResponse(data=result, msg="创建版本成功") @@ -65,10 +68,11 @@ async def create_version_controller( @VersionRouter.put("/update/{id}", summary="修改版本", response_model=ResponseSchema[VersionOutSchema]) async def update_version_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="版本ID")], data: Annotated[VersionUpdateSchema, Body(description="修改参数")], ) -> JSONResponse: - service = VersionService(auth) + service = VersionService(auth, db) result = await service.update(id=id, data=data) return SuccessResponse(data=result, msg="修改版本成功") @@ -76,9 +80,10 @@ async def update_version_controller( @VersionRouter.delete("/delete", summary="删除版本", response_model=ResponseSchema[None]) async def delete_version_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - service = VersionService(auth) + service = VersionService(auth, db) await service.delete(ids=ids) return SuccessResponse(msg="删除版本成功") @@ -86,9 +91,10 @@ async def delete_version_controller( @VersionRouter.put("/{id}/status", summary="变更版本状态", response_model=ResponseSchema[VersionOutSchema]) async def set_version_status_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:version:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="版本ID")], data: Annotated[VersionStatusSchema, Body(description="状态参数")], ) -> JSONResponse: - service = VersionService(auth) + service = VersionService(auth, db) result = await service.set_status(id=id, data=data) return SuccessResponse(data=result, msg="状态变更成功") diff --git a/backend/app/api/v1/module_system/versions/crud.py b/backend/app/api/v1/module_system/versions/crud.py index 7117719c..6469b7c0 100644 --- a/backend/app/api/v1/module_system/versions/crud.py +++ b/backend/app/api/v1/module_system/versions/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException @@ -9,8 +11,8 @@ from .schema import VersionCreateSchema, VersionUpdateSchema class VersionCRUD(CRUDBase[VersionModel, VersionCreateSchema, VersionUpdateSchema]): """版本数据层""" - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=VersionModel, auth=auth) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=VersionModel, auth=auth, db=db) async def set_status(self, id: int, status: int) -> VersionModel: """更新版本状态""" diff --git a/backend/app/api/v1/module_system/versions/service.py b/backend/app/api/v1/module_system/versions/service.py index 2ef0785e..c93990e5 100644 --- a/backend/app/api/v1/module_system/versions/service.py +++ b/backend/app/api/v1/module_system/versions/service.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException @@ -14,11 +16,12 @@ from .schema import ( class VersionService: """版本管理模块服务层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> VersionOutSchema: - obj = await VersionCRUD(self.auth).get(id=id) + obj = await VersionCRUD(self.auth, self.db).get(id=id) if not obj: raise CustomException(msg="该数据不存在") return VersionOutSchema.model_validate(obj) @@ -31,7 +34,7 @@ class VersionService: order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[VersionOutSchema]: offset = (page_no - 1) * page_size - return await VersionCRUD(self.auth).page( + return await VersionCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"sort": "asc"}, {"id": "desc"}], @@ -40,32 +43,32 @@ class VersionService: ) async def create(self, data: VersionCreateSchema) -> VersionOutSchema: - obj = await VersionCRUD(self.auth).create(data=data) + obj = await VersionCRUD(self.auth, self.db).create(data=data) return VersionOutSchema.model_validate(obj) async def update(self, id: int, data: VersionUpdateSchema) -> VersionOutSchema: - obj = await VersionCRUD(self.auth).get(id=id) + obj = await VersionCRUD(self.auth, self.db).get(id=id) if not obj: raise CustomException(msg="更新失败,该数据不存在") - obj = await VersionCRUD(self.auth).update(id=id, data=data) + obj = await VersionCRUD(self.auth, self.db).update(id=id, data=data) return VersionOutSchema.model_validate(obj) async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - objs = await VersionCRUD(self.auth).get_list(search={"id": ("in", ids)}) + objs = await VersionCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) obj_map = {o.id: o for o in objs} for id_ in ids: if id_ not in obj_map: raise CustomException(msg="删除失败,该数据不存在") - await VersionCRUD(self.auth).delete(ids=ids) + await VersionCRUD(self.auth, self.db).delete(ids=ids) async def set_status(self, id: int, data: VersionStatusSchema) -> VersionOutSchema: - obj = await VersionCRUD(self.auth).set_status(id=id, status=data.status) + obj = await VersionCRUD(self.auth, self.db).set_status(id=id, status=data.status) return VersionOutSchema.model_validate(obj) async def get_published(self) -> list[VersionOutSchema]: - objs = await VersionCRUD(self.auth).get_list( + objs = await VersionCRUD(self.auth, self.db).get_list( search={"status": ("eq", 1)}, order_by=[{"sort": "asc"}], ) diff --git a/backend/app/api/v1/module_task/__init__.py b/backend/app/api/v1/module_task/__init__.py index 9b2eed1d..6751a391 100644 --- a/backend/app/api/v1/module_task/__init__.py +++ b/backend/app/api/v1/module_task/__init__.py @@ -3,11 +3,11 @@ from fastapi import APIRouter from .cronjob.job.controller import JobRouter from .cronjob.node.controller import NodeRouter from .workflow.flows.controller import WorkflowRouter -from .workflow.nodes.controller import WorkflowNodesRouter +from .workflow.node_type.controller import WorkflowNodeTypeRouter task_router = APIRouter(prefix="/task") task_router.include_router(JobRouter) task_router.include_router(NodeRouter) task_router.include_router(WorkflowRouter) -task_router.include_router(WorkflowNodesRouter) +task_router.include_router(WorkflowNodeTypeRouter) diff --git a/backend/app/api/v1/module_task/cronjob/job/controller.py b/backend/app/api/v1/module_task/cronjob/job/controller.py index cf70e98a..f07e3bce 100644 --- a/backend/app/api/v1/module_task/cronjob/job/controller.py +++ b/backend/app/api/v1/module_task/cronjob/job/controller.py @@ -1,12 +1,13 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security +from fastapi import APIRouter, Body, Depends, Path, Query, Security from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.ap_scheduler import SchedulerUtil from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import JobOutSchema, JobQueryParam @@ -106,11 +107,12 @@ async def get_job_log_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[JobQueryParam, Query(description="查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: order_by = [{"created_time": "desc"}] if page.order_by: order_by = page.order_by - result_dict = await JobService(auth).get_job_log_page( + result_dict = await JobService(auth, db).get_job_log_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -123,8 +125,9 @@ async def get_job_log_list_controller( async def get_job_log_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:detail"]))], id: Annotated[int, Path(description="日志ID")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - result_dict = await JobService(auth).get_job_log_detail(id=id) + result_dict = await JobService(auth, db).get_job_log_detail(id=id) return SuccessResponse(data=result_dict, msg="获取执行日志详情成功") @@ -132,6 +135,7 @@ async def get_job_log_detail_controller( async def delete_job_log_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:delete"]))], ids: Annotated[list[int], Body(description="ID列表")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - await JobService(auth).delete_job_log(ids=ids) + await JobService(auth, db).delete_job_log(ids=ids) return SuccessResponse(msg="删除执行日志成功") diff --git a/backend/app/api/v1/module_task/cronjob/job/crud.py b/backend/app/api/v1/module_task/cronjob/job/crud.py index 0c4ebc65..b7eb9392 100644 --- a/backend/app/api/v1/module_task/cronjob/job/crud.py +++ b/backend/app/api/v1/module_task/cronjob/job/crud.py @@ -1,6 +1,8 @@ from collections.abc import Sequence from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -11,14 +13,14 @@ from .schema import JobCreateSchema, JobUpdateSchema class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]): """任务执行日志数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化任务执行日志CRUD 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - self.auth = auth - super().__init__(model=JobModel, auth=auth) + super().__init__(model=JobModel, auth=auth, db=db) async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobModel | None: """获取执行日志详情 diff --git a/backend/app/api/v1/module_task/cronjob/job/service.py b/backend/app/api/v1/module_task/cronjob/job/service.py index 3c42519e..c2b28709 100644 --- a/backend/app/api/v1/module_task/cronjob/job/service.py +++ b/backend/app/api/v1/module_task/cronjob/job/service.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.ap_scheduler import SchedulerUtil from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException @@ -9,11 +11,12 @@ from .schema import JobCreateSchema, JobOutSchema, JobQueryParam, JobUpdateSchem class JobService: """调度器监控模块服务层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def get_job_log_detail(self, id: int) -> JobOutSchema: - obj = await JobCRUD(self.auth).get_obj_by_id_crud(id=id) + obj = await JobCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="执行日志不存在") return JobOutSchema.model_validate(obj) @@ -25,7 +28,7 @@ class JobService: ) -> list[JobOutSchema]: if order_by is None: order_by = [{"created_time": "desc"}] - obj_list = await JobCRUD(self.auth).get_obj_list_crud(search=vars(search) if search else {}, order_by=order_by) + obj_list = await JobCRUD(self.auth, self.db).get_obj_list_crud(search=vars(search) if search else {}, order_by=order_by) return [JobOutSchema.model_validate(obj) for obj in obj_list] async def get_job_log_page( @@ -37,7 +40,7 @@ class JobService: ) -> PageResultSchema[JobOutSchema]: offset = (page_no - 1) * page_size ob = order_by or [{"created_time": "desc"}] - return await JobCRUD(self.auth).page( + return await JobCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=ob, @@ -57,7 +60,7 @@ class JobService: trigger_type=trigger_type, status=1, # 执行中 ) - obj = await JobCRUD(self.auth).create_obj_crud(data=data) + obj = await JobCRUD(self.auth, self.db).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建执行日志失败") return JobOutSchema.model_validate(obj) @@ -74,7 +77,7 @@ class JobService: result=result, error=error, ) - obj = await JobCRUD(self.auth).update_obj_crud(id=id, data=data) + obj = await JobCRUD(self.auth, self.db).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新执行日志失败") return JobOutSchema.model_validate(obj) @@ -82,10 +85,10 @@ class JobService: async def delete_job_log(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - await JobCRUD(self.auth).delete_obj_crud(ids=ids) + await JobCRUD(self.auth, self.db).delete_obj_crud(ids=ids) async def clear_job_log(self) -> None: - await JobCRUD(self.auth).clear_obj_crud() + await JobCRUD(self.auth, self.db).clear_obj_crud() @staticmethod def get_scheduler_status() -> dict: diff --git a/backend/app/api/v1/module_task/cronjob/node/controller.py b/backend/app/api/v1/module_task/cronjob/node/controller.py index e748d915..0e8ac704 100644 --- a/backend/app/api/v1/module_task/cronjob/node/controller.py +++ b/backend/app/api/v1/module_task/cronjob/node/controller.py @@ -1,11 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import NodeCreateSchema, NodeExecuteSchema, NodeOutSchema, NodeQueryParam, NodeUpdateSchema @@ -17,8 +18,9 @@ NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", ta @NodeRouter.get("/options", summary="获取定时任务节点列表", response_model=ResponseSchema[list[dict]]) async def get_node_options_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) result = await service.options() return SuccessResponse(data=result, msg="获取定时任务节点选项成功") @@ -27,8 +29,9 @@ async def get_node_options_controller( async def get_obj_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:detail"]))], id: Annotated[int, Path(description="节点ID")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) result_dict = await service.detail(id=id) return SuccessResponse(data=result_dict, msg="获取节点详情成功") @@ -38,8 +41,9 @@ async def get_obj_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[NodeQueryParam, Query(description="查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) result_dict = await service.page( page_no=page.page_no, page_size=page.page_size, @@ -53,8 +57,9 @@ async def get_obj_list_controller( async def create_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:create"]))], data: Annotated[NodeCreateSchema, Body(description="创建节点参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建节点成功") @@ -64,8 +69,9 @@ async def update_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))], id: Annotated[int, Path(description="节点ID")], data: Annotated[NodeUpdateSchema, Body(description="修改节点参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改节点成功") @@ -74,8 +80,9 @@ async def update_obj_controller( async def delete_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))], ids: Annotated[list[int], Body(description="ID列表")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) await service.delete(ids=ids) return SuccessResponse(msg="删除节点成功") @@ -83,8 +90,9 @@ async def delete_obj_controller( @NodeRouter.delete("/clear", summary="清空节点", response_model=ResponseSchema[None]) async def clear_obj_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) await service.clear() return SuccessResponse(msg="清空节点成功") @@ -94,8 +102,9 @@ async def execute_job_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:execute"]))], id: Annotated[int, Path(description="节点ID")], data: Annotated[NodeExecuteSchema, Body(description="调试节点参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) result = await service.execute(id=id, execute_data=data) return SuccessResponse(data=result, msg="调试节点成功") @@ -104,7 +113,8 @@ async def execute_job_controller( async def batch_set_status_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))], data: Annotated[BatchSetAvailable, Body(description="状态设置")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = NodeService(auth) + service = NodeService(auth, db) await service.batch_set_status(ids=data.ids, status=data.status) return SuccessResponse(msg="批量设置节点状态成功") diff --git a/backend/app/api/v1/module_task/cronjob/node/crud.py b/backend/app/api/v1/module_task/cronjob/node/crud.py index 58ec19a1..f5a920da 100644 --- a/backend/app/api/v1/module_task/cronjob/node/crud.py +++ b/backend/app/api/v1/module_task/cronjob/node/crud.py @@ -1,6 +1,8 @@ from collections.abc import Sequence from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -14,13 +16,14 @@ from .schema import ( class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]): """节点数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化节点CRUD 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - super().__init__(model=NodeModel, auth=auth) + super().__init__(model=NodeModel, auth=auth, db=db) async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> NodeModel | None: """获取节点详情 diff --git a/backend/app/api/v1/module_task/cronjob/node/service.py b/backend/app/api/v1/module_task/cronjob/node/service.py index 4c7c6424..6822a8c7 100644 --- a/backend/app/api/v1/module_task/cronjob/node/service.py +++ b/backend/app/api/v1/module_task/cronjob/node/service.py @@ -1,4 +1,5 @@ from apscheduler.jobstores.base import JobLookupError +from sqlalchemy.ext.asyncio import AsyncSession from app.core.ap_scheduler import SchedulerUtil from app.core.base_schema import AuthSchema, PageResultSchema @@ -18,11 +19,12 @@ from .schema import ( class NodeService: """节点管理模块服务层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def options(self) -> list[dict]: - obj_list = await NodeCRUD(self.auth).get_obj_list_crud() + obj_list = await NodeCRUD(self.auth, self.db).get_obj_list_crud() return [ { "id": obj.id, @@ -36,7 +38,7 @@ class NodeService: ] async def detail(self, id: int) -> NodeOutSchema: - obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id) + obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) return NodeOutSchema.model_validate(obj) async def get_list( @@ -44,7 +46,7 @@ class NodeService: search: NodeQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[NodeOutSchema]: - obj_list = await NodeCRUD(self.auth).get_obj_list_crud(search=vars(search) if search else {}, order_by=order_by) + obj_list = await NodeCRUD(self.auth, self.db).get_obj_list_crud(search=vars(search) if search else {}, order_by=order_by) return [NodeOutSchema.model_validate(obj) for obj in obj_list] async def page( @@ -55,7 +57,7 @@ class NodeService: order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[NodeOutSchema]: offset = (page_no - 1) * page_size - return await NodeCRUD(self.auth).page( + return await NodeCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -64,21 +66,21 @@ class NodeService: ) async def create(self, data: NodeCreateSchema) -> NodeOutSchema: - exist_obj = await NodeCRUD(self.auth).get(name=data.name) + exist_obj = await NodeCRUD(self.auth, self.db).get(name=data.name) if exist_obj: raise CustomException(msg="创建失败,该节点已存在") - obj = await NodeCRUD(self.auth).create_obj_crud(data=data) + obj = await NodeCRUD(self.auth, self.db).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建失败") return NodeOutSchema.model_validate(obj) async def update(self, id: int, data: NodeUpdateSchema) -> NodeOutSchema: - exist_obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id) + exist_obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not exist_obj: raise CustomException(msg="更新失败,该节点不存在") - obj = await NodeCRUD(self.auth).update_obj_crud(id=id, data=data) + obj = await NodeCRUD(self.auth, self.db).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新失败") return NodeOutSchema.model_validate(obj) @@ -87,21 +89,21 @@ class NodeService: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") for mid in ids: - exist_obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=mid) + exist_obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=mid) if not exist_obj: raise CustomException(msg="删除失败,该节点不存在") try: SchedulerUtil.remove_job(job_id=mid) except JobLookupError: pass - await NodeCRUD(self.auth).delete_obj_crud(ids=ids) + await NodeCRUD(self.auth, self.db).delete_obj_crud(ids=ids) async def clear(self) -> None: SchedulerUtil.clear_jobs() - await NodeCRUD(self.auth).clear_obj_crud() + await NodeCRUD(self.auth, self.db).clear_obj_crud() async def execute(self, id: int, execute_data: NodeExecuteSchema) -> dict: - obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id) + obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="调试失败,该节点不存在") @@ -145,7 +147,7 @@ class NodeService: if not ids: raise CustomException(msg="请选择要操作的数据") - await NodeCRUD(self.auth).set( + await NodeCRUD(self.auth, self.db).set( ids=ids, status=status, ) diff --git a/backend/app/api/v1/module_task/workflow/flows/controller.py b/backend/app/api/v1/module_task/workflow/flows/controller.py index 382cdaf2..72caa4d3 100644 --- a/backend/app/api/v1/module_task/workflow/flows/controller.py +++ b/backend/app/api/v1/module_task/workflow/flows/controller.py @@ -1,11 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import WorkflowCreateSchema, WorkflowExecuteResultSchema, WorkflowExecuteSchema, WorkflowOutSchema, WorkflowQueryParam, WorkflowUpdateSchema @@ -17,19 +18,21 @@ WorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/flow @WorkflowRouter.get("/detail/{id}", summary="工作流详情", response_model=ResponseSchema[WorkflowOutSchema]) async def get_workflow_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="工作流ID")], ) -> JSONResponse: - result_dict = await WorkflowService(auth).get_workflow_detail(id=id) + result_dict = await WorkflowService(auth, db).get_workflow_detail(id=id) return SuccessResponse(data=result_dict, msg="获取工作流详情成功") @WorkflowRouter.get("/list", summary="工作流列表", response_model=ResponseSchema[PageResultSchema[WorkflowOutSchema]]) async def get_workflow_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[WorkflowQueryParam, Query(description="查询参数")], ) -> JSONResponse: - result_dict = await WorkflowService(auth).get_workflow_page( + result_dict = await WorkflowService(auth, db).get_workflow_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -41,44 +44,49 @@ async def get_workflow_list_controller( @WorkflowRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建工作流", response_model=ResponseSchema[WorkflowOutSchema]) async def create_workflow_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[WorkflowCreateSchema, Body(description="创建工作流参数")], ) -> JSONResponse: - result_dict = await WorkflowService(auth).create_workflow(data=data) + result_dict = await WorkflowService(auth, db).create_workflow(data=data) return SuccessResponse(data=result_dict, msg="创建工作流成功") @WorkflowRouter.put("/update/{id}", summary="更新工作流", response_model=ResponseSchema[WorkflowOutSchema]) async def update_workflow_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="工作流ID")], data: Annotated[WorkflowUpdateSchema, Body(description="更新工作流参数")], ) -> JSONResponse: - result_dict = await WorkflowService(auth).update_workflow(id=id, data=data) + result_dict = await WorkflowService(auth, db).update_workflow(id=id, data=data) return SuccessResponse(data=result_dict, msg="更新工作流成功") @WorkflowRouter.delete("/delete", summary="删除工作流", response_model=ResponseSchema[None]) async def delete_workflow_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - await WorkflowService(auth).delete_workflow(ids=ids) + await WorkflowService(auth, db).delete_workflow(ids=ids) return SuccessResponse(msg="删除工作流成功") @WorkflowRouter.post("/publish/{id}", summary="发布工作流", response_model=ResponseSchema[WorkflowOutSchema]) async def publish_workflow_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="工作流ID")], ) -> JSONResponse: - result_dict = await WorkflowService(auth).publish_workflow(id=id) + result_dict = await WorkflowService(auth, db).publish_workflow(id=id) return SuccessResponse(data=result_dict, msg="发布工作流成功") @WorkflowRouter.post("/execute", summary="执行工作流", response_model=ResponseSchema[WorkflowExecuteResultSchema]) async def execute_workflow_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:execute"]))], + db: Annotated[AsyncSession, Depends(db_getter)], body: Annotated[WorkflowExecuteSchema, Body(description="执行工作流参数")], ) -> JSONResponse: - result_dict = await WorkflowService(auth).execute_workflow(body=body) + result_dict = await WorkflowService(auth, db).execute_workflow(body=body) return SuccessResponse(data=result_dict, msg="执行工作流完成") diff --git a/backend/app/api/v1/module_task/workflow/flows/crud.py b/backend/app/api/v1/module_task/workflow/flows/crud.py index a1bcc42e..2baadb6a 100644 --- a/backend/app/api/v1/module_task/workflow/flows/crud.py +++ b/backend/app/api/v1/module_task/workflow/flows/crud.py @@ -1,6 +1,8 @@ from collections.abc import Sequence from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -11,16 +13,17 @@ from .schema import WorkflowCreateSchema, WorkflowUpdateSchema class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateSchema]): """工作流数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化工作流 CRUD。 参数: - auth (AuthSchema): 认证信息。 + - db (AsyncSession): 数据库会话。 返回: - None """ - super().__init__(model=WorkflowModel, auth=auth) + super().__init__(model=WorkflowModel, auth=auth, db=db) async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> WorkflowModel | None: """按主键查询工作流。 diff --git a/backend/app/api/v1/module_task/workflow/flows/service.py b/backend/app/api/v1/module_task/workflow/flows/service.py index 772b4ad5..c06665a9 100644 --- a/backend/app/api/v1/module_task/workflow/flows/service.py +++ b/backend/app/api/v1/module_task/workflow/flows/service.py @@ -1,10 +1,12 @@ import asyncio from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException -from ..nodes.crud import WorkflowNodeTypeCRUD +from ..node_type.crud import WorkflowNodeTypeCRUD from .crud import WorkflowCRUD from .handlers.workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph from .schema import ( @@ -29,14 +31,15 @@ WORKFLOW_EXEC_STATUS_COMPLETED = 1 class WorkflowService: """工作流:画布存储 + 发布校验 + 分层并行执行""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db def _out(self, obj: Any) -> WorkflowOutSchema: return WorkflowOutSchema.model_validate(obj) async def get_workflow_detail(self, id: int) -> WorkflowOutSchema: - obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id) + obj = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="工作流不存在") return self._out(obj) @@ -48,7 +51,7 @@ class WorkflowService: ) -> list[WorkflowOutSchema]: if order_by is None: order_by = [{"updated_time": "desc"}] - obj_list = await WorkflowCRUD(self.auth).get_obj_list_crud( + obj_list = await WorkflowCRUD(self.auth, self.db).get_obj_list_crud( search=vars(search) if search else {}, order_by=order_by, ) @@ -63,7 +66,7 @@ class WorkflowService: ) -> PageResultSchema[WorkflowOutSchema]: offset = (page_no - 1) * page_size order = order_by or [{"updated_time": "desc"}] - result = await WorkflowCRUD(self.auth).page( + result = await WorkflowCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order, @@ -73,23 +76,23 @@ class WorkflowService: return result async def create_workflow(self, data: WorkflowCreateSchema) -> WorkflowOutSchema: - exist = await WorkflowCRUD(self.auth).get(code=data.code) + exist = await WorkflowCRUD(self.auth, self.db).get(code=data.code) if exist: raise CustomException(msg="流程编码已存在") - obj = await WorkflowCRUD(self.auth).create_obj_crud(data=data) + obj = await WorkflowCRUD(self.auth, self.db).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建工作流失败") return self._out(obj) async def update_workflow(self, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema: - exist = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id) + exist = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not exist: raise CustomException(msg="工作流不存在") if exist.code != data.code: - other = await WorkflowCRUD(self.auth).get(code=data.code) + other = await WorkflowCRUD(self.auth, self.db).get(code=data.code) if other: raise CustomException(msg="流程编码已存在") - obj = await WorkflowCRUD(self.auth).update_obj_crud(id=id, data=data) + obj = await WorkflowCRUD(self.auth, self.db).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新工作流失败") return self._out(obj) @@ -97,10 +100,10 @@ class WorkflowService: async def delete_workflow(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除ID不能为空") - await WorkflowCRUD(self.auth).delete_obj_crud(ids=ids) + await WorkflowCRUD(self.auth, self.db).delete_obj_crud(ids=ids) async def publish_workflow(self, id: int) -> WorkflowOutSchema: - obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id) + obj = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="工作流不存在") nodes = obj.nodes or [] @@ -119,13 +122,13 @@ class WorkflowService: edges=obj.edges, workflow_status=WORKFLOW_STATUS_PUBLISHED, ) - updated = await WorkflowCRUD(self.auth).update_obj_crud(id=id, data=data) + updated = await WorkflowCRUD(self.auth, self.db).update_obj_crud(id=id, data=data) if not updated: raise CustomException(msg="发布失败") return self._out(updated) async def execute_workflow(self, body: WorkflowExecuteSchema) -> WorkflowExecuteResultSchema: - obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=body.workflow_id) + obj = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=body.workflow_id) if not obj: raise CustomException(msg="工作流不存在") if obj.status != WORKFLOW_STATUS_PUBLISHED: @@ -139,7 +142,7 @@ class WorkflowService: codes_set = {n.get("type") for n in nodes if n.get("type")} code_list = list(codes_set) templates: dict[str, dict[str, Any]] = {} - type_objs = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud(search={"code": ("in", code_list)}) + type_objs = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_list_crud(search={"code": ("in", code_list)}) type_map = {t.code: t for t in type_objs} for code in codes_set: node_type = type_map.get(code) diff --git a/backend/app/api/v1/module_task/workflow/nodes/__init__.py b/backend/app/api/v1/module_task/workflow/node_type/__init__.py similarity index 100% rename from backend/app/api/v1/module_task/workflow/nodes/__init__.py rename to backend/app/api/v1/module_task/workflow/node_type/__init__.py diff --git a/backend/app/api/v1/module_task/workflow/nodes/controller.py b/backend/app/api/v1/module_task/workflow/node_type/controller.py similarity index 62% rename from backend/app/api/v1/module_task/workflow/nodes/controller.py rename to backend/app/api/v1/module_task/workflow/node_type/controller.py index 97b4654f..0c50eab1 100644 --- a/backend/app/api/v1/module_task/workflow/nodes/controller.py +++ b/backend/app/api/v1/module_task/workflow/node_type/controller.py @@ -1,48 +1,52 @@ from typing import Annotated -from fastapi import APIRouter, Body, Path, Query, Security, status +from fastapi import APIRouter, Body, Depends, Path, Query, Security, status from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from .schema import WorkflowNodeTypeCreateSchema, WorkflowNodeTypeOutSchema, WorkflowNodeTypeQueryParam, WorkflowNodeTypeUpdateSchema from .service import WorkflowNodeTypeService -WorkflowNodesRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/nodes", tags=["工作流节点"]) +WorkflowNodeTypeRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/nodes", tags=["工作流节点"]) -@WorkflowNodesRouter.get("/options", summary="节点选项", response_model=ResponseSchema[list[dict]]) +@WorkflowNodeTypeRouter.get("/options", summary="节点选项", response_model=ResponseSchema[list[dict]]) async def get_workflow_node_type_options_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) result = await service.get_options() return SuccessResponse(data=result, msg="获取节点选项成功") -@WorkflowNodesRouter.get("/detail/{id}", summary="节点详情", response_model=ResponseSchema[WorkflowNodeTypeOutSchema]) +@WorkflowNodeTypeRouter.get("/detail/{id}", summary="节点详情", response_model=ResponseSchema[WorkflowNodeTypeOutSchema]) async def get_workflow_node_type_detail_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="ID")], ) -> JSONResponse: - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) result_dict = await service.get_detail(id=id) return SuccessResponse(data=result_dict, msg="获取节点详情成功") -@WorkflowNodesRouter.get("/list", summary="节点列表", response_model=ResponseSchema[PageResultSchema[WorkflowNodeTypeOutSchema]]) +@WorkflowNodeTypeRouter.get("/list", summary="节点列表", response_model=ResponseSchema[PageResultSchema[WorkflowNodeTypeOutSchema]]) async def get_workflow_node_type_list_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[WorkflowNodeTypeQueryParam, Query(description="查询参数")], ) -> JSONResponse: order_by = [{"sort_order": "asc"}, {"id": "asc"}] if page.order_by: order_by = page.order_by - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) result_dict = await service.get_page( page_no=page.page_no, page_size=page.page_size, @@ -52,41 +56,45 @@ async def get_workflow_node_type_list_controller( return SuccessResponse(data=result_dict, msg="查询节点列表成功") -@WorkflowNodesRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建节点", response_model=ResponseSchema[WorkflowNodeTypeOutSchema]) +@WorkflowNodeTypeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建节点", response_model=ResponseSchema[WorkflowNodeTypeOutSchema]) async def create_workflow_node_type_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], data: Annotated[WorkflowNodeTypeCreateSchema, Body(description="创建节点参数")], ) -> JSONResponse: - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建节点成功") -@WorkflowNodesRouter.put("/update/{id}", summary="更新节点", response_model=ResponseSchema[WorkflowNodeTypeOutSchema]) +@WorkflowNodeTypeRouter.put("/update/{id}", summary="更新节点", response_model=ResponseSchema[WorkflowNodeTypeOutSchema]) async def update_workflow_node_type_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], id: Annotated[int, Path(description="节点ID")], data: Annotated[WorkflowNodeTypeUpdateSchema, Body(description="更新节点参数")], ) -> JSONResponse: - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="更新节点成功") -@WorkflowNodesRouter.delete("/delete", summary="删除节点", response_model=ResponseSchema[None]) +@WorkflowNodeTypeRouter.delete("/delete", summary="删除节点", response_model=ResponseSchema[None]) async def delete_workflow_node_type_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ids: Annotated[list[int], Body(description="ID列表")], ) -> JSONResponse: - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) await service.delete(ids=ids) return SuccessResponse(msg="删除节点成功") -@WorkflowNodesRouter.get("/select", summary="节点选择列表", response_model=ResponseSchema[list[dict]]) +@WorkflowNodeTypeRouter.get("/select", summary="节点选择列表", response_model=ResponseSchema[list[dict]]) async def get_workflow_node_type_select_controller( auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = WorkflowNodeTypeService(auth) + service = WorkflowNodeTypeService(auth, db) result = await service.get_select() return SuccessResponse(data=result, msg="获取节点选择列表成功") diff --git a/backend/app/api/v1/module_task/workflow/nodes/crud.py b/backend/app/api/v1/module_task/workflow/node_type/crud.py similarity index 92% rename from backend/app/api/v1/module_task/workflow/nodes/crud.py rename to backend/app/api/v1/module_task/workflow/node_type/crud.py index fd3bf9d6..2bb44cfd 100644 --- a/backend/app/api/v1/module_task/workflow/nodes/crud.py +++ b/backend/app/api/v1/module_task/workflow/node_type/crud.py @@ -1,6 +1,8 @@ from collections.abc import Sequence from typing import Any +from sqlalchemy.ext.asyncio import AsyncSession + from app.common.enums import QueueEnum from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -12,16 +14,17 @@ from .schema import WorkflowNodeTypeCreateSchema, WorkflowNodeTypeUpdateSchema class WorkflowNodeTypeCRUD(CRUDBase[WorkflowNodeTypeModel, WorkflowNodeTypeCreateSchema, WorkflowNodeTypeUpdateSchema]): """节点类型 CRUD""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化节点类型 CRUD。 参数: - auth (AuthSchema): 认证信息。 + - db (AsyncSession): 数据库会话。 返回: - None """ - super().__init__(model=WorkflowNodeTypeModel, auth=auth) + super().__init__(model=WorkflowNodeTypeModel, auth=auth, db=db) async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> WorkflowNodeTypeModel | None: """按主键查询节点类型。 diff --git a/backend/app/api/v1/module_task/workflow/nodes/model.py b/backend/app/api/v1/module_task/workflow/node_type/model.py similarity index 98% rename from backend/app/api/v1/module_task/workflow/nodes/model.py rename to backend/app/api/v1/module_task/workflow/node_type/model.py index 87839463..bf7fdcae 100644 --- a/backend/app/api/v1/module_task/workflow/nodes/model.py +++ b/backend/app/api/v1/module_task/workflow/node_type/model.py @@ -5,8 +5,7 @@ from app.core.base_model import ModelMixin, TenantMixin, UserMixin class WorkflowNodeTypeModel(ModelMixin, TenantMixin, UserMixin): - """节点类型:用于 Vue Flow 左侧 palette 与执行引擎解析。 - """ + """节点类型:用于 Vue Flow 左侧 palette 与执行引擎解析。""" __tablename__: str = "task_workflow_node_type" __table_args__ = ( diff --git a/backend/app/api/v1/module_task/workflow/nodes/schema.py b/backend/app/api/v1/module_task/workflow/node_type/schema.py similarity index 100% rename from backend/app/api/v1/module_task/workflow/nodes/schema.py rename to backend/app/api/v1/module_task/workflow/node_type/schema.py diff --git a/backend/app/api/v1/module_task/workflow/nodes/service.py b/backend/app/api/v1/module_task/workflow/node_type/service.py similarity index 73% rename from backend/app/api/v1/module_task/workflow/nodes/service.py rename to backend/app/api/v1/module_task/workflow/node_type/service.py index de63462f..541c1a66 100644 --- a/backend/app/api/v1/module_task/workflow/nodes/service.py +++ b/backend/app/api/v1/module_task/workflow/node_type/service.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException @@ -13,15 +15,16 @@ from .schema import ( class WorkflowNodeTypeService: """工作流节点类型(与定时任务 task_node 无关)""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db @staticmethod def _out(obj) -> WorkflowNodeTypeOutSchema: return WorkflowNodeTypeOutSchema.model_validate(obj) async def get_options(self) -> list[dict]: - objs = await WorkflowNodeTypeCRUD(self.auth).list_active_options_crud() + objs = await WorkflowNodeTypeCRUD(self.auth, self.db).list_active_options_crud() return [ { "id": o.id, @@ -35,7 +38,7 @@ class WorkflowNodeTypeService: ] async def get_detail(self, id: int) -> WorkflowNodeTypeOutSchema: - obj = await WorkflowNodeTypeCRUD(self.auth).get_obj_by_id_crud(id=id) + obj = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="节点类型不存在") return self._out(obj) @@ -47,7 +50,7 @@ class WorkflowNodeTypeService: ) -> list[WorkflowNodeTypeOutSchema]: if order_by is None: order_by = [{"sort_order": "asc"}, {"id": "asc"}] - obj_list = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud( + obj_list = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_list_crud( search=vars(search) if search else {}, order_by=order_by, ) @@ -62,7 +65,7 @@ class WorkflowNodeTypeService: ) -> PageResultSchema[WorkflowNodeTypeOutSchema]: offset = (page_no - 1) * page_size order = order_by or [{"sort_order": "asc"}, {"id": "asc"}] - result = await WorkflowNodeTypeCRUD(self.auth).page( + result = await WorkflowNodeTypeCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order, @@ -72,23 +75,23 @@ class WorkflowNodeTypeService: return result async def create(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeOutSchema: - exist = await WorkflowNodeTypeCRUD(self.auth).get(code=data.code) + exist = await WorkflowNodeTypeCRUD(self.auth, self.db).get(code=data.code) if exist: raise CustomException(msg="节点编码已存在") - obj = await WorkflowNodeTypeCRUD(self.auth).create_obj_crud(data=data) + obj = await WorkflowNodeTypeCRUD(self.auth, self.db).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建失败") return self._out(obj) async def update(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeOutSchema: - exist = await WorkflowNodeTypeCRUD(self.auth).get_obj_by_id_crud(id=id) + exist = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id) if not exist: raise CustomException(msg="节点类型不存在") if exist.code != data.code: - other = await WorkflowNodeTypeCRUD(self.auth).get(code=data.code) + other = await WorkflowNodeTypeCRUD(self.auth, self.db).get(code=data.code) if other: raise CustomException(msg="节点编码已存在") - obj = await WorkflowNodeTypeCRUD(self.auth).update_obj_crud(id=id, data=data) + obj = await WorkflowNodeTypeCRUD(self.auth, self.db).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新失败") return self._out(obj) @@ -96,8 +99,8 @@ class WorkflowNodeTypeService: async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除ID不能为空") - await WorkflowNodeTypeCRUD(self.auth).delete_obj_crud(ids=ids) + await WorkflowNodeTypeCRUD(self.auth, self.db).delete_obj_crud(ids=ids) async def get_select(self) -> list[dict]: - objs = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud() + objs = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_list_crud() return [{"id": o.id, "name": o.name} for o in objs] diff --git a/backend/app/common/enums.py b/backend/app/common/enums.py index ec760a9b..343e5b4f 100644 --- a/backend/app/common/enums.py +++ b/backend/app/common/enums.py @@ -99,14 +99,14 @@ class QueueEnum(str, Enum): date = "date" month = "month" like = "like" - eq = "eq" or "==" + eq = "eq" in_ = "in" between = "between" - ne = "!=" or "ne" - gt = ">" or "gt" - ge = ">=" or "ge" - lt = "<" or "lt" - le = "<=" or "le" + ne = "!=" + gt = ">" + ge = ">=" + lt = "<" + le = "<=" class PermissionFilterStrategy(str, Enum): diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index 32af8cd9..96af30fc 100755 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -61,7 +61,7 @@ class Settings(BaseSettings): # ================================================= # # ******************* 登录认证配置 ****************** # # ================================================= # - SECRET_KEY: str = "vgb0tnl9d58+6n-6h-ea&u^1#s0ccp!794=krylxcjq75vzps$" # JWT密钥 + SECRET_KEY: str = "" # JWT密钥(必须通过环境变量 SECRET_KEY 设置,无默认值) ALGORITHM: str = "HS256" # JWT算法 ACCESS_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 12 # access_token过期时间(秒)12 小时 REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 12 # refresh_token过期时间(秒)12 小时 diff --git a/backend/app/core/ap_scheduler.py b/backend/app/core/ap_scheduler.py index 3858ffd1..0fec9710 100644 --- a/backend/app/core/ap_scheduler.py +++ b/backend/app/core/ap_scheduler.py @@ -41,10 +41,10 @@ from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger from redis.asyncio import Redis +from app.api.v1.module_task.cronjob.node.model import NodeModel from app.config.setting import settings from app.core.database import engine from app.core.logger import logger -from app.plugin.module_task.cronjob.node.model import NodeModel from app.utils.cron_util import CronUtil # 任务状态常量(与 JobModel.status 注释保持一致:0:待执行 1:执行中 2:成功 3:失败 4:超时 5:已取消) @@ -95,6 +95,7 @@ class SchedulerUtil: # 临时存储 job_name,用于在 EVENT_JOB_SUBMITTED 时获取 # 格式可以是: str (任务名称) 或 tuple[str, str] (原任务ID, 任务名称) _job_name_cache: dict[str, str | tuple[str, str]] = {} + _last_scheduler_status: int = SCHEDULER_STATUS_STOPPED @classmethod def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None: @@ -492,22 +493,6 @@ class SchedulerUtil: return logger.debug(f"JobStore {alias} {action}") - @classmethod - def _clear_all_job_logs(cls) -> None: - """清空所有任务日志(仅用于手动清空,不建议使用) - """ - try: - from sqlalchemy.orm import Session - - from app.plugin.module_task.cronjob.job.model import JobModel - - with Session(engine) as session: - session.query(JobModel).delete() - session.commit() - logger.info("所有任务日志已清空") - except Exception as e: - logger.error(f"清空任务日志失败: {e!s}", exc_info=True) - @classmethod def _cancel_all_pending_job_logs(cls) -> None: """将所有 pending 状态的执行日志更新为 cancelled @@ -516,7 +501,7 @@ class SchedulerUtil: try: from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel with Session(engine) as session: session.query(JobModel).filter(JobModel.status == JOB_STATUS_PENDING).update({"status": JOB_STATUS_CANCELLED}) @@ -756,7 +741,7 @@ class SchedulerUtil: """清理该 job_id 的所有旧的 pending 日志,避免重启累积。""" from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel try: with Session(engine) as session: @@ -779,7 +764,7 @@ class SchedulerUtil: """ from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel try: job = cls.get_job(job_id=job_id) @@ -813,7 +798,7 @@ class SchedulerUtil: """ from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel job = cls.get_job(job_id=job_id) next_run_time = str(job.next_run_time) if job and job.next_run_time else None @@ -842,7 +827,7 @@ class SchedulerUtil: """ from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel try: job = cls.get_job(job_id=job_id) @@ -929,7 +914,7 @@ class SchedulerUtil: """ from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel with Session(engine) as session: job_log = session.query(JobModel).filter(JobModel.job_id == job_id, JobModel.status.in_([JOB_STATUS_PENDING, JOB_STATUS_RUNNING])).order_by(JobModel.created_time.desc()).first() @@ -1311,7 +1296,7 @@ class SchedulerUtil: """ from sqlalchemy.orm import Session - from app.plugin.module_task.cronjob.job.model import JobModel + from app.api.v1.module_task.cronjob.job.model import JobModel jobs = cls.get_all_jobs() sync_count = 0 diff --git a/backend/app/core/base_crud.py b/backend/app/core/base_crud.py index cedc5448..c9ef45f2 100644 --- a/backend/app/core/base_crud.py +++ b/backend/app/core/base_crud.py @@ -6,10 +6,11 @@ from pydantic import BaseModel from sqlalchemy import Select, asc, delete, desc, false, func, literal_column, select, update from sqlalchemy import inspect as sa_inspect from sqlalchemy.engine import Result +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from sqlalchemy.sql.elements import ColumnElement -from app.core.base_model import MappedBase +from app.core.base_model import ModelMixin from app.core.base_schema import AuthSchema, PageResultSchema from app.core.exceptions import CustomException from app.core.permission import Permission @@ -19,33 +20,28 @@ CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) -class CRUDBase[ModelType: MappedBase, CreateSchemaType, UpdateSchemaType]: +class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]: """统一数据层基类 核心设计:``auth`` 是必填的 ``AuthSchema``,子类可直接访问 ``self.auth.user.xxx``。 用法: - # 认证场景(Controller/Service)—— 自动处理租户/数据权限/审计 class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): - def __init__(self, auth: AuthSchema) -> None: - super().__init__(model=UserModel, auth=auth) - - # 后台任务场景 —— 构造最小 AuthSchema 即可 - class OrderCRUD(CRUDBase[OrderModel, Any, Any]): - def __init__(self, db: AsyncSession) -> None: - super().__init__(model=OrderModel, auth=AuthSchema(db=db)) + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: + super().__init__(model=UserModel, auth=auth, db=db) """ - def __init__(self, model: type[ModelType], auth: AuthSchema) -> None: + def __init__(self, model: type[ModelType], auth: AuthSchema, db: AsyncSession) -> None: """初始化 CRUDBase。 参数: - model: 数据模型类 - - auth: 认证信息(含 db 会话和 user)。后台任务场景下可传入 ``AuthSchema(db=session)`` + - auth: 认证信息 + - db: 数据库会话 """ self.model = model self.auth = auth - self.db = auth.db + self.db = db def _get_pk_col(self) -> ColumnElement: """获取模型主键列""" @@ -320,11 +316,11 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType, UpdateSchemaType]: if user.id: if hasattr(obj, "tenant_id"): if not user.is_superuser or getattr(obj, "tenant_id", None) is None: - obj.tenant_id = user.tenant_id or user.id + setattr(obj, "tenant_id", user.tenant_id) if hasattr(obj, "created_id"): - obj.created_id = user.id + setattr(obj, "created_id", user.id) if hasattr(obj, "updated_id"): - obj.updated_id = user.id + setattr(obj, "updated_id", user.id) self.db.add(obj) await self.db.flush() @@ -366,8 +362,8 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType, UpdateSchemaType]: raise CustomException(msg="无权修改其他租户的数据") # 审计字段 - if user and user.id and hasattr(obj, "updated_id"): - obj.updated_id = user.id + if user.id and hasattr(obj, "updated_id"): + setattr(obj, "updated_id", user.id) for key, value in obj_dict.items(): if hasattr(obj, key): @@ -446,7 +442,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType, UpdateSchemaType]: if getattr(self.model, "__platform_data_shared__", False): for condition in self._platform_shared_conditions(): sql = sql.where(condition) - filter_obj = Permission(model=self.model, auth=self.auth) + filter_obj = Permission(model=self.model, auth=self.auth, db=self.db) return await filter_obj.filter_query(sql) def _platform_shared_conditions(self) -> list[ColumnElement]: @@ -455,7 +451,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType, UpdateSchemaType]: return [] tid = user.tenant_id if tid is not None and tid != 1: - return [(self.model.tenant_id == tid) | (self.model.tenant_id == 1)] + return [(getattr(self.model, "tenant_id") == tid) | (getattr(self.model, "tenant_id") == 1)] return [] def _tenant_dml_where(self, sql): @@ -465,21 +461,21 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType, UpdateSchemaType]: if user.id and not user.is_superuser: tid = user.tenant_id if tid is not None: - return sql.where(self.model.tenant_id == tid) + return sql.where(getattr(self.model, "tenant_id") == tid) return sql async def __build_conditions(self, **kwargs) -> list[ColumnElement]: conditions: list[ColumnElement] = [] if hasattr(self.model, "is_deleted"): - conditions.append(self.model.is_deleted == False) # noqa: E712 + conditions.append(getattr(self.model, "is_deleted") == false()) if hasattr(self.model, "tenant_id") and not getattr(self.model, "__platform_data_shared__", False): user = self.auth.user if user.id and not user.is_superuser: tid = user.tenant_id if tid is not None: - conditions.append(self.model.tenant_id == tid) + conditions.append(getattr(self.model, "tenant_id") == tid) for key, value in kwargs.items(): if value is None or value == "": diff --git a/backend/app/core/base_schema.py b/backend/app/core/base_schema.py index b9398731..b541cea4 100644 --- a/backend/app/core/base_schema.py +++ b/backend/app/core/base_schema.py @@ -1,18 +1,10 @@ import json from datetime import datetime -from typing import TYPE_CHECKING, TypeVar from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator -from sqlalchemy.ext.asyncio import AsyncSession -from app.common.enums import QueueEnum from app.core.validator import DateTimeStr -if TYPE_CHECKING: - from app.api.v1.module_system.user.model import UserModel - -UserT = TypeVar("UserT") - class CommonSchema(BaseModel): """通用信息模型""" @@ -84,22 +76,6 @@ class DownloadFileSchema(BaseModel): file_name: str = Field(..., description="新文件名称") -class AuthSchema(BaseModel): - """权限认证模型""" - - model_config = ConfigDict(arbitrary_types_allowed=True) - - user: "UserModel" = Field(..., description="用户信息(UserModel 实例)", exclude=True) - check_data_scope: bool = Field(default=True, description="是否检查数据权限") - db: AsyncSession = Field(..., description="数据库会话", exclude=True) - session_info: dict | None = Field(default=None, description="会话信息(含 is_impersonate 等)") - - @classmethod - def anonymous(cls, db: AsyncSession, check_data_scope: bool = False, session_info: dict | None = None) -> "AuthSchema": - """创建匿名认证模型(用于登录、支付回调等无用户场景)""" - return cls(db=db, check_data_scope=check_data_scope, session_info=session_info, user=None) # type: ignore[arg-type] - - class JWTPayloadSchema(BaseModel): """JWT载荷模型""" @@ -180,10 +156,10 @@ class BaseQueryParam(BaseModel): def validate_query_params(self) -> "BaseQueryParam": ct = self.created_time if isinstance(ct, list) and len(ct) == 2: - self.created_time = (QueueEnum.between.value, (ct[0], ct[1])) + self.created_time = ("between", (ct[0], ct[1])) ut = self.updated_time if isinstance(ut, list) and len(ut) == 2: - self.updated_time = (QueueEnum.between.value, (ut[0], ut[1])) + self.updated_time = ("between", (ut[0], ut[1])) return self @@ -196,9 +172,9 @@ class UserByQueryParam(BaseModel): @model_validator(mode="after") def validate_query_params(self) -> "UserByQueryParam": if isinstance(self.created_id, int): - self.created_id = (QueueEnum.eq.value, self.created_id) + self.created_id = ("eq", self.created_id) if isinstance(self.updated_id, int): - self.updated_id = (QueueEnum.eq.value, self.updated_id) + self.updated_id = ("eq", self.updated_id) return self @@ -210,7 +186,7 @@ class TenantByQueryParam(BaseModel): @model_validator(mode="after") def validate_query_params(self) -> "TenantByQueryParam": if isinstance(self.tenant_id, int): - self.tenant_id = (QueueEnum.eq.value, self.tenant_id) + self.tenant_id = ("eq", self.tenant_id) return self @@ -219,3 +195,29 @@ class OptionSchema(BaseModel): value: int label: str + + +class CoreUserSchema(BaseModel): + """核心层用户信息 — AuthSchema 使用,不依赖任何业务模块 + + 业务模块的 UserOutSchema 应继承此类以确保类型兼容。 + """ + + model_config = ConfigDict(from_attributes=True) + + id: int = Field(default=0, description="用户ID") + tenant_id: int = Field(default=0, description="租户ID") + username: str | None = Field(default=None, description="用户名") + name: str | None = Field(default=None, description="名称") + dept_id: int | None = Field(default=None, description="部门ID") + is_superuser: bool = Field(default=False, description="是否超管") + + +class AuthSchema(BaseModel): + """权限认证模型""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + user: CoreUserSchema = Field(default_factory=CoreUserSchema, description="用户信息", exclude=True) + check_data_scope: bool = Field(default=True, description="是否检查数据权限") + session_info: dict | None = Field(default=None, description="会话信息(含 is_impersonate 等)") diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index b64d29ba..6fe6362e 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -10,6 +10,7 @@ from redis.asyncio.client import Redis from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.api.v1.module_system.user.schema import UserOutSchema from app.common.enums import RET, RedisInitKeyConfig from app.config.setting import settings from app.core.base_schema import AuthSchema @@ -244,11 +245,11 @@ async def _authenticate( if not user: raise CustomException(msg="用户不存在", code=RET.NOT_FOUND.code, status_code=401) - auth = AuthSchema(db=db, check_data_scope=False, session_info=user_info, user=user) + auth = AuthSchema(check_data_scope=False, session_info=user_info, user=UserOutSchema.model_validate(user)) return auth -async def _get_cached_tenant_menu_ids(auth: AuthSchema, tenant_id: int) -> list[int]: +async def _get_cached_tenant_menu_ids(auth: AuthSchema, tenant_id: int, db: AsyncSession) -> list[int]: """获取租户可用菜单 ID,带 60s 进程级缓存 套餐菜单变更频率极低,缓存可大幅减少 AuthPermission 的 DB 查询次数。 @@ -256,6 +257,7 @@ async def _get_cached_tenant_menu_ids(auth: AuthSchema, tenant_id: int) -> list[ 参数: auth: 认证信息 tenant_id: 租户 ID + db: 数据库会话 返回: 可用菜单 ID 列表 @@ -266,7 +268,7 @@ async def _get_cached_tenant_menu_ids(auth: AuthSchema, tenant_id: int) -> list[ from app.api.v1.module_platform.package.service import PackageService - result = await PackageService(auth).get_tenant_available_menu_ids(tenant_id) + result = await PackageService(auth, db).get_tenant_available_menu_ids(tenant_id) _package_menu_cache[tenant_id] = (time.time(), result) return result @@ -288,7 +290,7 @@ class AuthPermission: self.permissions = permissions or [] self.check_data_scope = check_data_scope - async def __call__(self, auth: AuthSchema = Depends(get_current_user)) -> AuthSchema: + async def __call__(self, auth: AuthSchema = Depends(get_current_user), db: AsyncSession = Depends(db_getter)) -> AuthSchema: """调用权限验证 参数: @@ -300,7 +302,7 @@ class AuthPermission: auth = auth.model_copy(update={"check_data_scope": self.check_data_scope}) user = auth.user - if not user or not user.is_superuser: + if user.id is None or not user.is_superuser: return auth if not self.permissions: @@ -315,7 +317,7 @@ class AuthPermission: raise CustomException(msg="无权限操作", code=RET.FORBIDDEN.code, status_code=403) if user.tenant_id: - allowed_ids = set[int](await _get_cached_tenant_menu_ids(auth, user.tenant_id)) + allowed_ids = set[int](await _get_cached_tenant_menu_ids(auth, user.tenant_id, db)) cached_perms = auth.session_info.get("permissions_with_menu", {}) if auth.session_info else {} user_permissions = {p for p, mid in cached_perms.items() if mid in allowed_ids} else: @@ -363,18 +365,20 @@ class RequireTenantWrite: - 平台管理员代签入模式(session_info.is_impersonate=True) """ - async def __call__(self, auth: AuthSchema = Depends(get_current_user)) -> AuthSchema: - if not auth.user or auth.user.is_superuser: + async def __call__(self, auth: AuthSchema = Depends(get_current_user), db: AsyncSession = Depends(db_getter)) -> AuthSchema: + if auth.user.id is None or auth.user.is_superuser: return auth is_impersonate = auth.session_info.get("is_impersonate", False) if auth.session_info else False if is_impersonate: return auth + from app.api.v1.module_platform.tenant.model import TenantModel from app.common.enums import TenantStatusEnum user = auth.user - tenant_status = user.tenant.status if user.tenant else 0 + tenant = await db.get(TenantModel, user.tenant_id) if user.tenant_id else None + tenant_status = tenant.status if tenant else 0 if tenant_status in (TenantStatusEnum.ARREARS, TenantStatusEnum.TRIAL): if tenant_status == TenantStatusEnum.ARREARS: raise CustomException(msg="租户已欠费,仅允许查看操作,请联系平台管理员续费", code=RET.FORBIDDEN.code, status_code=423) diff --git a/backend/app/core/http_limit.py b/backend/app/core/http_limit.py index 2e1dc7fb..2002fa3d 100644 --- a/backend/app/core/http_limit.py +++ b/backend/app/core/http_limit.py @@ -1,7 +1,5 @@ from math import ceil -from typing import NoReturn -from fastapi import Request from slowapi import Limiter from slowapi.util import get_remote_address from starlette.websockets import WebSocket @@ -16,18 +14,6 @@ limiter = Limiter( ) -def http_limit_callback(request: Request, response, expire: int) -> NoReturn: - """HTTP 触发限流时的默认回调:抛出 429。""" - expires = ceil(expire / 1000) - from app.core.exceptions import CustomException - - raise CustomException( - status_code=429, - msg="请求过于频繁,请稍后重试!", - data={"Retry-After": str(expires)}, - ) - - async def ws_limit_callback(ws: WebSocket, expire: int) -> None: """WebSocket 触发限流时的默认回调:关闭连接。""" expires = ceil(expire / 1000) diff --git a/backend/app/core/permission.py b/backend/app/core/permission.py index bbd4aa6c..c32d70da 100644 --- a/backend/app/core/permission.py +++ b/backend/app/core/permission.py @@ -1,6 +1,7 @@ from typing import Any from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql.elements import ColumnElement from app.common.enums import PermissionFilterStrategy @@ -21,10 +22,11 @@ class Permission: DATA_SCOPE_ALL = 4 # 全部数据 DATA_SCOPE_CUSTOM = 5 # 自定义数据 - def __init__(self, model: Any, auth: AuthSchema) -> None: + def __init__(self, model: Any, auth: AuthSchema, db: AsyncSession) -> None: """初始化权限过滤器实例""" self.model = model self.auth = auth + self.db = db self.conditions: list[ColumnElement] = [] # 权限条件列表 async def filter_query(self, query: Any) -> Any: @@ -68,7 +70,7 @@ class Permission: if cached is None: from app.api.v1.module_platform.package.service import PackageService - cached = set[int](await PackageService(self.auth).get_tenant_available_menu_ids(self.auth.user.tenant_id)) + cached = set[int](await PackageService(self.auth, self.db).get_tenant_available_menu_ids(self.auth.user.tenant_id)) object.__setattr__(self.auth, cache_attr, cached) menu_ids = menu_ids & cached @@ -168,7 +170,7 @@ class Permission: try: from app.api.v1.module_system.dept.model import DeptModel - dept_objs = (await self.auth.db.execute(select(DeptModel))).scalars().all() + dept_objs = (await self.db.execute(select(DeptModel))).scalars().all() id_map = get_child_id_map(dept_objs) accessible_dept_ids.update(get_child_recursion(id=user_dept_id, id_map=id_map)) except Exception: diff --git a/backend/app/core/router_class.py b/backend/app/core/router_class.py index 166617be..01f03137 100644 --- a/backend/app/core/router_class.py +++ b/backend/app/core/router_class.py @@ -23,8 +23,8 @@ async def _write_operation_log_async(log_data: dict) -> None: try: async with async_db_session() as _session, _session.begin(): - _auth = AuthSchema.anonymous(db=_session) - await OperationLogCRUD(_auth).create(data=OperationLogCreateSchema(**log_data)) + auth = AuthSchema(check_data_scope=False) + await OperationLogCRUD(auth, _session).create(data=OperationLogCreateSchema(**log_data)) except Exception: logger.exception("操作日志写入失败: path={}", log_data.get("request_path")) diff --git a/backend/app/init_app.py b/backend/app/init_app.py index 6cfd467a..f415a0b9 100644 --- a/backend/app/init_app.py +++ b/backend/app/init_app.py @@ -157,4 +157,5 @@ def register_docs(app: FastAPI) -> None: def register_frontend(app: FastAPI) -> None: - app.frontend("/web", directory=str(path_conf.FRONTEND_DIST_DIR), fallback="index.html", check_dir=True) + if path_conf.FRONTEND_DIST_DIR.exists(): + app.mount("/web", StaticFiles(directory=str(path_conf.FRONTEND_DIST_DIR), html=True), name="frontend") diff --git a/backend/app/plugin/module_example/demo/controller.py b/backend/app/plugin/module_example/demo/controller.py index c863c0ce..9bc9029d 100644 --- a/backend/app/plugin/module_example/demo/controller.py +++ b/backend/app/plugin/module_example/demo/controller.py @@ -3,10 +3,11 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, File, Path, Query, UploadFile, status from fastapi.responses import JSONResponse, StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession from app.common.response import ResponseSchema, StreamResponse, SuccessResponse from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -20,8 +21,9 @@ DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["示 async def get_obj_detail_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:detail"]))], id: Annotated[int, Path(description="示例ID")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) result_dict = await service.detail(id=id) return SuccessResponse(data=result_dict, msg="获取示例详情成功") @@ -31,8 +33,9 @@ async def get_obj_list_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:query"]))], page: Annotated[PaginationQueryParam, Query(description="分页参数")], search: Annotated[DemoQueryParam, Query(description="查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) result_dict = await service.page( page_no=page.page_no, page_size=page.page_size, @@ -46,8 +49,9 @@ async def get_obj_list_controller( async def create_obj_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:create"]))], data: Annotated[DemoCreateSchema, Body(description="创建参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建示例成功") @@ -57,8 +61,9 @@ async def update_obj_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:update"]))], id: Annotated[int, Path(description="示例ID")], data: Annotated[DemoUpdateSchema, Body(description="修改参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改示例成功") @@ -67,8 +72,9 @@ async def update_obj_controller( async def delete_obj_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:delete"]))], ids: Annotated[list[int], Body(description="ID列表")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) await service.delete(ids=ids) return SuccessResponse(msg="删除示例成功") @@ -77,8 +83,9 @@ async def delete_obj_controller( async def batch_set_available_obj_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:patch"]))], data: Annotated[BatchSetAvailable, Body(description="状态设置")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) await service.set_available(data=data) return SuccessResponse(msg="批量修改示例状态成功") @@ -87,8 +94,9 @@ async def batch_set_available_obj_controller( async def export_obj_list_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:export"]))], search: Annotated[DemoQueryParam, Query(description="查询参数")], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> StreamingResponse: - service = DemoService(auth) + service = DemoService(auth, db) result_dict_list = await service.get_list(search=search) export_result = DemoService.batch_export(obj_list=[item.model_dump() for item in result_dict_list]) @@ -103,8 +111,9 @@ async def export_obj_list_controller( async def import_obj_list_controller( file: Annotated[UploadFile, File(description="导入文件")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:import"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - service = DemoService(auth) + service = DemoService(auth, db) batch_import_result = await service.batch_import(file=file, update_support=True) return SuccessResponse(data=batch_import_result, msg="导入示例成功") diff --git a/backend/app/plugin/module_example/demo/crud.py b/backend/app/plugin/module_example/demo/crud.py index 996507ae..19201b2a 100644 --- a/backend/app/plugin/module_example/demo/crud.py +++ b/backend/app/plugin/module_example/demo/crud.py @@ -1,3 +1,5 @@ +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema @@ -8,10 +10,11 @@ from .schema import DemoCreateSchema, DemoUpdateSchema class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]): """示例数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """初始化CRUD数据层 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - super().__init__(model=DemoModel, auth=auth) + super().__init__(model=DemoModel, auth=auth, db=db) diff --git a/backend/app/plugin/module_example/demo/service.py b/backend/app/plugin/module_example/demo/service.py index d22b0d74..7f3969ae 100644 --- a/backend/app/plugin/module_example/demo/service.py +++ b/backend/app/plugin/module_example/demo/service.py @@ -1,6 +1,7 @@ from typing import Any from fastapi import UploadFile +from sqlalchemy.ext.asyncio import AsyncSession from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema from app.core.exceptions import CustomException @@ -19,11 +20,12 @@ from .schema import ( class DemoService: """示例管理模块服务层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: self.auth = auth + self.db = db async def detail(self, id: int) -> DemoOutSchema: - obj = await DemoCRUD(self.auth).get(id=id) + obj = await DemoCRUD(self.auth, self.db).get(id=id) if not obj: raise CustomException(msg="该数据不存在") return DemoOutSchema.model_validate(obj) @@ -33,7 +35,7 @@ class DemoService: search: DemoQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[DemoOutSchema]: - obj_list = await DemoCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by) + obj_list = await DemoCRUD(self.auth, self.db).get_list(search=vars(search) if search else None, order_by=order_by) return [DemoOutSchema.model_validate(obj) for obj in obj_list] async def page( @@ -44,7 +46,7 @@ class DemoService: order_by: list[dict[str, str]] | None = None, ) -> PageResultSchema[DemoOutSchema]: offset = (page_no - 1) * page_size - return await DemoCRUD(self.auth).page( + return await DemoCRUD(self.auth, self.db).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -53,36 +55,36 @@ class DemoService: ) async def create(self, data: DemoCreateSchema) -> DemoOutSchema: - obj = await DemoCRUD(self.auth).get(name=data.name) + obj = await DemoCRUD(self.auth, self.db).get(name=data.name) if obj: raise CustomException(msg="创建失败,名称已存在") - obj = await DemoCRUD(self.auth).create(data=data) + obj = await DemoCRUD(self.auth, self.db).create(data=data) return DemoOutSchema.model_validate(obj) async def update(self, id: int, data: DemoUpdateSchema) -> DemoOutSchema: - obj = await DemoCRUD(self.auth).get(id=id) + obj = await DemoCRUD(self.auth, self.db).get(id=id) if not obj: raise CustomException(msg="更新失败,该数据不存在") - exist_obj = await DemoCRUD(self.auth).get(name=data.name) + exist_obj = await DemoCRUD(self.auth, self.db).get(name=data.name) if exist_obj and exist_obj.id != id: raise CustomException(msg="更新失败,名称重复") - obj = await DemoCRUD(self.auth).update(id=id, data=data) + obj = await DemoCRUD(self.auth, self.db).update(id=id, data=data) return DemoOutSchema.model_validate(obj) async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - objs = await DemoCRUD(self.auth).get_list(search={"id": ("in", ids)}) + objs = await DemoCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)}) obj_map = {o.id: o for o in objs} for id_ in ids: if id_ not in obj_map: raise CustomException(msg="删除失败,该数据不存在") - await DemoCRUD(self.auth).delete(ids=ids) + await DemoCRUD(self.auth, self.db).delete(ids=ids) async def set_available(self, data: BatchSetAvailable) -> None: - await DemoCRUD(self.auth).set(ids=data.ids, status=data.status) + await DemoCRUD(self.auth, self.db).set(ids=data.ids, status=data.status) @staticmethod def batch_export(obj_list: list[dict[str, Any]]) -> bytes: @@ -155,7 +157,7 @@ class DemoService: description=str(row["description"] or ""), ) - exists_obj = await DemoCRUD(self.auth).get(name=create_data.name) + exists_obj = await DemoCRUD(self.auth, self.db).get(name=create_data.name) if exists_obj: if update_support: update_data = DemoUpdateSchema( @@ -163,12 +165,12 @@ class DemoService: status=create_data.status, description=create_data.description, ) - await DemoCRUD(self.auth).update(id=exists_obj.id, data=update_data) + await DemoCRUD(self.auth, self.db).update(id=exists_obj.id, data=update_data) success_count += 1 else: error_msgs.append(f"第{i}行: 对象 {create_data.name} 已存在") else: - await DemoCRUD(self.auth).create(data=create_data) + await DemoCRUD(self.auth, self.db).create(data=create_data) success_count += 1 except Exception as e: diff --git a/backend/app/scripts/data/sys_version.json b/backend/app/scripts/data/sys_version.json index 3eb2c45e..ae4ada33 100644 --- a/backend/app/scripts/data/sys_version.json +++ b/backend/app/scripts/data/sys_version.json @@ -7,9 +7,8 @@ "description": null, "status": 1, "sort": 1, - "create_by": 1, - "create_time": "2026-03-15 10:00:00", - "update_time": "2026-03-15 10:00:00" + "created_time": "2026-03-15 10:00:00", + "updated_time": "2026-03-15 10:00:00" }, { "version": "v2.6.3", @@ -19,9 +18,8 @@ "description": null, "status": 1, "sort": 2, - "create_by": 1, - "create_time": "2025-11-15 10:00:00", - "update_time": "2025-11-15 10:00:00" + "created_time": "2025-11-15 10:00:00", + "updated_time": "2025-11-15 10:00:00" }, { "version": "v2.6.2", @@ -32,9 +30,8 @@ "require_re_login": true, "status": 1, "sort": 3, - "create_by": 1, - "create_time": "2025-11-09 10:00:00", - "update_time": "2025-11-09 10:00:00" + "created_time": "2025-11-09 10:00:00", + "updated_time": "2025-11-09 10:00:00" }, { "version": "v2.6.1", @@ -44,9 +41,8 @@ "description": null, "status": 1, "sort": 4, - "create_by": 1, - "create_time": "2025-10-19 10:00:00", - "update_time": "2025-10-19 10:00:00" + "created_time": "2025-10-19 10:00:00", + "updated_time": "2025-10-19 10:00:00" }, { "version": "v2.6.0", @@ -57,9 +53,8 @@ "require_re_login": true, "status": 1, "sort": 5, - "create_by": 1, - "create_time": "2025-10-16 10:00:00", - "update_time": "2025-10-16 10:00:00" + "created_time": "2025-10-16 10:00:00", + "updated_time": "2025-10-16 10:00:00" }, { "version": "v2.5.9", @@ -69,9 +64,8 @@ "description": null, "status": 1, "sort": 6, - "create_by": 1, - "create_time": "2025-10-12 10:00:00", - "update_time": "2025-10-12 10:00:00" + "created_time": "2025-10-12 10:00:00", + "updated_time": "2025-10-12 10:00:00" }, { "version": "v2.5.8", @@ -81,9 +75,8 @@ "description": "由于项目依赖升级,node 版本需要升级到 v20.19.0 或以上", "status": 1, "sort": 7, - "create_by": 1, - "create_time": "2025-09-29 10:00:00", - "update_time": "2025-09-29 10:00:00" + "created_time": "2025-09-29 10:00:00", + "updated_time": "2025-09-29 10:00:00" }, { "version": "v2.5.7", @@ -93,9 +86,8 @@ "description": null, "status": 1, "sort": 8, - "create_by": 1, - "create_time": "2025-09-14 10:00:00", - "update_time": "2025-09-14 10:00:00" + "created_time": "2025-09-14 10:00:00", + "updated_time": "2025-09-14 10:00:00" }, { "version": "v2.5.6", @@ -105,9 +97,8 @@ "description": null, "status": 1, "sort": 9, - "create_by": 1, - "create_time": "2025-08-17 10:00:00", - "update_time": "2025-08-17 10:00:00" + "created_time": "2025-08-17 10:00:00", + "updated_time": "2025-08-17 10:00:00" }, { "version": "v2.5.5", @@ -117,8 +108,7 @@ "description": null, "status": 1, "sort": 10, - "create_by": 1, - "create_time": "2025-07-27 10:00:00", - "update_time": "2025-07-27 10:00:00" + "created_time": "2025-07-27 10:00:00", + "updated_time": "2025-07-27 10:00:00" } ] \ No newline at end of file diff --git a/backend/app/utils/gen_util.py b/backend/app/utils/gen_util.py index ba62a09f..c2a43844 100644 --- a/backend/app/utils/gen_util.py +++ b/backend/app/utils/gen_util.py @@ -1,11 +1,11 @@ import re -from app.common.constant import GenConstant -from app.plugin.module_generator.gencode.schema import ( +from app.api.v1.module_generator.gencode.schema import ( GenTableColumnSchema, GenTableOutSchema, GenTableSchema, ) +from app.common.constant import GenConstant from app.utils.string_util import StringUtil diff --git a/backend/app/utils/hash_bcrpy_util.py b/backend/app/utils/hash_bcrpy_util.py deleted file mode 100644 index d0f6ceef..00000000 --- a/backend/app/utils/hash_bcrpy_util.py +++ /dev/null @@ -1,84 +0,0 @@ -import base64 -import hashlib -import os - -from cryptography.hazmat.backends.openssl import backend -from cryptography.hazmat.primitives import padding -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -_PBKDF2_ALGO = "sha256" -_PBKDF2_ITERATIONS = 600_000 -_PBKDF2_SALT_LEN = 16 -_PBKDF2_PREFIX = "$pbkdf2-sha256$" - - -class PwdUtil: - @staticmethod - def hash_password(password: str) -> str: - salt = os.urandom(_PBKDF2_SALT_LEN) - dk = hashlib.pbkdf2_hmac(_PBKDF2_ALGO, password.encode(), salt, _PBKDF2_ITERATIONS) - return f"{_PBKDF2_PREFIX}{_PBKDF2_ITERATIONS}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}" - - @staticmethod - def verify_password(plain_password: str, password_hash: str) -> bool: - try: - _, _algo, iters_str, salt_b64, hash_b64 = password_hash.split("$") - salt = base64.b64decode(salt_b64) - expected = base64.b64decode(hash_b64) - dk = hashlib.pbkdf2_hmac(_PBKDF2_ALGO, plain_password.encode(), salt, int(iters_str)) - return dk == expected - except Exception: - return False - - @staticmethod - def check_password_strength(password: str) -> str | None: - if len(password) < 6: - return "密码长度至少6位" - if not any(c.isupper() for c in password): - return "密码需要包含大写字母" - if not any(c.islower() for c in password): - return "密码需要包含小写字母" - if not any(c.isdigit() for c in password): - return "密码需要包含数字" - return None - - -class AESCipher: - """AES 加密器""" - - def __init__(self, key: bytes | str) -> None: - self.key = key if isinstance(key, bytes) else bytes.fromhex(key) - - def encrypt(self, plaintext: bytes | str) -> bytes: - if not isinstance(plaintext, bytes): - plaintext = str(plaintext).encode("utf-8") - iv = os.urandom(16) - cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=backend) - encryptor = cipher.encryptor() - padder = padding.PKCS7(cipher.algorithm.block_size).padder() # type: ignore - padded_plaintext = padder.update(plaintext) + padder.finalize() - ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize() - return iv + ciphertext - - def decrypt(self, ciphertext: bytes | str) -> str: - ciphertext = ciphertext if isinstance(ciphertext, bytes) else bytes.fromhex(ciphertext) - iv = ciphertext[:16] - ciphertext = ciphertext[16:] - cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=backend) - decryptor = cipher.decryptor() - unpadder = padding.PKCS7(cipher.algorithm.block_size).unpadder() # type: ignore - padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize() - plaintext = unpadder.update(padded_plaintext) + unpadder.finalize() - return plaintext.decode("utf-8") - - -class Md5Cipher: - """MD5 加密器""" - - @staticmethod - def encrypt(plaintext: bytes | str) -> str: - md5 = hashlib.md5() - if not isinstance(plaintext, bytes): - plaintext = str(plaintext).encode("utf-8") - md5.update(plaintext) - return md5.hexdigest() diff --git a/backend/app/utils/import_util.py b/backend/app/utils/import_util.py index 1c54ce48..eec1dac2 100644 --- a/backend/app/utils/import_util.py +++ b/backend/app/utils/import_util.py @@ -140,7 +140,9 @@ class ImportUtil: continue # 检查表名重复 - table_name = obj.__tablename__ + table_name = getattr(obj, "__tablename__", None) + if table_name is None: + continue if table_name in seen_tables: continue @@ -191,8 +193,10 @@ class ImportUtil: try: module = importlib.import_module(module_name) for _name, obj in inspect.getmembers(module, inspect.isclass): - if (cls.is_valid_model(obj, base_class) and hasattr(obj, "__tablename__") and obj.__tablename__ == "apscheduler_jobs") and ( - obj not in seen_models and "apscheduler_jobs" not in seen_tables + if (cls.is_valid_model(obj, base_class) + and getattr(obj, "__tablename__", None) == "apscheduler_jobs" + and obj not in seen_models + and "apscheduler_jobs" not in seen_tables ): seen_models.add(obj) seen_tables.add("apscheduler_jobs") diff --git a/backend/app/utils/jinja2_template_util.py b/backend/app/utils/jinja2_template_util.py index 73d8588e..626819b2 100644 --- a/backend/app/utils/jinja2_template_util.py +++ b/backend/app/utils/jinja2_template_util.py @@ -4,13 +4,13 @@ from typing import Any from jinja2 import Environment, FileSystemLoader, Template -from app.common.constant import GenConstant -from app.config.path_conf import TEMPLATE_DIR -from app.config.setting import settings -from app.plugin.module_generator.gencode.schema import ( +from app.api.v1.module_generator.gencode.schema import ( GenTableColumnOutSchema, GenTableOutSchema, ) +from app.common.constant import GenConstant +from app.config.path_conf import TEMPLATE_DIR +from app.config.setting import settings from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil from app.utils.gen_util import GenUtils from app.utils.string_util import StringUtil @@ -248,7 +248,7 @@ class Jinja2TemplateUtil: 返回: - str: 路由首段(module_xxx)。 """ - from app.plugin.module_generator.gencode.service import GenTableService + from app.api.v1.module_generator.gencode.service import GenTableService pid = int(gen_table.parent_menu_id) if gen_table.parent_menu_id is not None else None return GenTableService._menu_route_first_segment( diff --git a/backend/app/utils/password_util.py b/backend/app/utils/password_util.py new file mode 100644 index 00000000..4d55640f --- /dev/null +++ b/backend/app/utils/password_util.py @@ -0,0 +1,71 @@ +import base64 +import hashlib +import os +import secrets +import string + +_PBKDF2_ALGO = "sha256" +_PBKDF2_ITERATIONS = 600_000 +_PBKDF2_SALT_LEN = 16 +_PBKDF2_PREFIX = "$pbkdf2-sha256$" + +_STRONG_PWD_CHARS = string.ascii_letters + string.digits + "!@#$%^&*" + + +class PwdUtil: + @staticmethod + def hash_password(password: str) -> str: + salt = os.urandom(_PBKDF2_SALT_LEN) + dk = hashlib.pbkdf2_hmac(_PBKDF2_ALGO, password.encode(), salt, _PBKDF2_ITERATIONS) + return f"{_PBKDF2_PREFIX}{_PBKDF2_ITERATIONS}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}" + + @staticmethod + def verify_password(plain_password: str, password_hash: str) -> bool: + try: + _, _algo, iters_str, salt_b64, hash_b64 = password_hash.split("$") + salt = base64.b64decode(salt_b64) + expected = base64.b64decode(hash_b64) + dk = hashlib.pbkdf2_hmac(_PBKDF2_ALGO, plain_password.encode(), salt, int(iters_str)) + return dk == expected + except Exception: + return False + + @staticmethod + def check_password_strength(password: str) -> str | None: + if len(password) < 6: + return "密码长度至少6位" + if not any(c.isupper() for c in password): + return "密码需要包含大写字母" + if not any(c.islower() for c in password): + return "密码需要包含小写字母" + if not any(c.isdigit() for c in password): + return "密码需要包含数字" + return None + + @staticmethod + def generate_strong_password(length: int = 12) -> str: + """生成符合强度要求的强随机密码(大写+小写+数字+特殊符号)。 + + 使用 ``secrets`` 而非 ``random``,避免伪随机带来的安全风险。 + + 参数: + - length (int): 密码长度,默认 12,最小 8。 + + 返回: + - str: 生成的明文密码。 + """ + if length < 8: + raise ValueError("密码长度至少 8 位") + + # 保证每类字符至少出现一次 + uppercase = secrets.choice(string.ascii_uppercase) + lowercase = secrets.choice(string.ascii_lowercase) + digit = secrets.choice(string.digits) + special = secrets.choice("!@#$%^&*") + + remaining_length = length - 4 + rest = [secrets.choice(_STRONG_PWD_CHARS) for _ in range(remaining_length)] + + chars = list(rest) + [uppercase, lowercase, digit, special] + secrets.SystemRandom().shuffle(chars) + return "".join(chars) diff --git a/backend/templates/python/controller.py.jinja2 b/backend/templates/python/controller.py.jinja2 index e41a8a6b..fb8003b9 100644 --- a/backend/templates/python/controller.py.jinja2 +++ b/backend/templates/python/controller.py.jinja2 @@ -2,12 +2,14 @@ import urllib.parse from typing import Annotated -from fastapi import APIRouter, Body, Depends, File, Path, Query, UploadFile +from fastapi import APIRouter, Body, Depends, File, Path, Query, Security, UploadFile from fastapi.responses import JSONResponse, StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession +from app.core.base_schema import AuthSchema from app.common.response import ResponseSchema, StreamResponse, SuccessResponse -from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam -from app.core.dependencies import AuthPermission +from app.core.base_schema import BatchSetAvailable, PageResultSchema, PaginationQueryParam +from app.core.dependencies import AuthPermission, db_getter from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -24,7 +26,8 @@ from .service import {{ class_name }}Service ) async def get_obj_detail_controller( id: Annotated[int, Path(description="{{ function_name }}ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:detail"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:detail"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 获取{{ function_name }}详情 @@ -32,11 +35,12 @@ async def get_obj_detail_controller( 参数: - id (int): {{ function_name }}ID - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含{{ function_name }}详情的JSON响应 """ - result_dict = await {{ class_name }}Service.detail_service(id=id, auth=auth) + result_dict = await {{ class_name }}Service.detail_service(id=id, auth=auth, db=db) return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功") @@ -48,7 +52,8 @@ async def get_obj_detail_controller( async def get_obj_list_controller( page: Annotated[PaginationQueryParam, Depends()], search: Annotated[{{ class_name }}QueryParam, Depends()], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:query"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:query"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 查询{{ function_name }}列表 @@ -57,12 +62,14 @@ async def get_obj_list_controller( - page (PaginationQueryParam): 分页查询参数 - search ({{ class_name }}QueryParam): 查询参数 - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含{{ function_name }}列表分页信息的JSON响应 """ result_dict = await {{ class_name }}Service.page_service( auth=auth, + db=db, page_no=page.page_no, page_size=page.page_size, search=search, @@ -78,7 +85,8 @@ async def get_obj_list_controller( ) async def create_obj_controller( data: Annotated[{{ class_name }}CreateSchema, Body(description="创建参数")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:create"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:create"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 创建{{ function_name }} @@ -86,11 +94,12 @@ async def create_obj_controller( 参数: - data ({{ class_name }}CreateSchema): {{ function_name }}创建模型 - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含创建{{ function_name }}详情的JSON响应 """ - result_dict = await {{ class_name }}Service.create_service(auth=auth, data=data) + result_dict = await {{ class_name }}Service.create_service(auth=auth, db=db, data=data) return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功") @@ -102,7 +111,8 @@ async def create_obj_controller( async def update_obj_controller( data: Annotated[{{ class_name }}UpdateSchema, Body(description="更新参数")], id: Annotated[int, Path(description="{{ function_name }}ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:update"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:update"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 修改{{ function_name }} @@ -111,11 +121,12 @@ async def update_obj_controller( - data ({{ class_name }}UpdateSchema): {{ function_name }}更新模型 - id (int): {{ function_name }}ID - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含修改{{ function_name }}详情的JSON响应 """ - result_dict = await {{ class_name }}Service.update_service(auth=auth, id=id, data=data) + result_dict = await {{ class_name }}Service.update_service(auth=auth, db=db, id=id, data=data) return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功") @@ -126,7 +137,8 @@ async def update_obj_controller( ) async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:delete"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:delete"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 删除{{ function_name }} @@ -134,11 +146,12 @@ async def delete_obj_controller( 参数: - ids (list[int]): {{ function_name }}ID列表 - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含删除{{ function_name }}详情的JSON响应 """ - await {{ class_name }}Service.delete_service(auth=auth, ids=ids) + await {{ class_name }}Service.delete_service(auth=auth, db=db, ids=ids) return SuccessResponse(msg="删除{{ function_name }}成功") @@ -149,7 +162,8 @@ async def delete_obj_controller( ) async def batch_set_available_obj_controller( data: Annotated[BatchSetAvailable, Body(description="状态设置")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:patch"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:patch"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 批量修改{{ function_name }}状态 @@ -157,11 +171,12 @@ async def batch_set_available_obj_controller( 参数: - data (BatchSetAvailable): 批量修改{{ function_name }}状态模型 - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含批量修改{{ function_name }}状态详情的JSON响应 """ - await {{ class_name }}Service.set_available_service(auth=auth, data=data) + await {{ class_name }}Service.set_available_service(auth=auth, db=db, data=data) return SuccessResponse(msg="批量修改{{ function_name }}状态成功") @@ -171,7 +186,8 @@ async def batch_set_available_obj_controller( ) async def export_obj_list_controller( search: Annotated[{{ class_name }}QueryParam, Query(description="查询参数")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:export"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:export"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> StreamingResponse: """ 导出{{ function_name }} @@ -179,11 +195,12 @@ async def export_obj_list_controller( 参数: - search ({{ class_name }}QueryParam): 查询参数 - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - StreamingResponse: 包含{{ function_name }}列表的Excel文件流响应 """ - result_dict_list = await {{ class_name }}Service.list_service(search=search, auth=auth) + result_dict_list = await {{ class_name }}Service.list_service(search=search, auth=auth, db=db) export_result = {{ class_name }}Service.batch_export_service(obj_list=result_dict_list) return StreamResponse( @@ -200,7 +217,8 @@ async def export_obj_list_controller( ) async def import_obj_list_controller( file: Annotated[UploadFile, File(description="导入的Excel文件")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["{{ permission_prefix }}:import"]))], + auth: Annotated[AuthSchema, Security(AuthPermission(["{{ permission_prefix }}:import"]))], + db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: """ 导入{{ function_name }} @@ -208,12 +226,13 @@ async def import_obj_list_controller( 参数: - file (UploadFile): 导入的Excel文件 - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 返回: - JSONResponse: 包含导入{{ function_name }}详情的JSON响应 """ batch_import_result = await {{ class_name }}Service.batch_import_service( - file=file, auth=auth, update_support=True + file=file, auth=auth, db=db, update_support=True ) return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功") @@ -221,7 +240,7 @@ async def import_obj_list_controller( @{{ class_name }}Router.post( "/download/template", summary="获取{{ function_name }}导入模板", - dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))], + dependencies=[Security(AuthPermission(["{{ permission_prefix }}:download"]))], ) async def export_obj_template_controller() -> StreamingResponse: """ @@ -239,4 +258,4 @@ async def export_obj_template_controller() -> StreamingResponse: "Content-Disposition": f"attachment; filename={urllib.parse.quote('{{ function_name }}导入模板.xlsx')}", "Access-Control-Expose-Headers": "Content-Disposition", }, - ) \ No newline at end of file + ) diff --git a/backend/templates/python/crud.py.jinja2 b/backend/templates/python/crud.py.jinja2 index 344bb3d9..0f82f969 100644 --- a/backend/templates/python/crud.py.jinja2 +++ b/backend/templates/python/crud.py.jinja2 @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.base_crud import CRUDBase from app.core.base_schema import AuthSchema from .model import {{ class_name }}Model @@ -9,11 +11,12 @@ from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema class {{ class_name }}CRUD(CRUDBase[{{ class_name }}Model, {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema]): """{{ function_name }}数据层""" - def __init__(self, auth: AuthSchema) -> None: + def __init__(self, auth: AuthSchema, db: AsyncSession) -> None: """ 初始化CRUD数据层 参数: - auth (AuthSchema): 认证信息模型 + - db (AsyncSession): 数据库会话 """ - super().__init__(model={{ class_name }}Model, auth=auth) + super().__init__(model={{ class_name }}Model, auth=auth, db=db) diff --git a/backend/templates/python/service.py.jinja2 b/backend/templates/python/service.py.jinja2 index 4495e1aa..56c5a983 100644 --- a/backend/templates/python/service.py.jinja2 +++ b/backend/templates/python/service.py.jinja2 @@ -3,8 +3,10 @@ import io import pandas as pd from fastapi import UploadFile +from sqlalchemy.ext.asyncio import AsyncSession -from app.core.base_schema import AuthSchema, BatchSetAvailable +from app.core.base_schema import AuthSchema +from app.core.base_schema import BatchSetAvailable from app.core.exceptions import CustomException from app.core.logger import logger from app.utils.excel_util import ExcelUtil @@ -23,29 +25,31 @@ class {{ class_name }}Service: """ @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> dict: + async def detail_service(cls, auth: AuthSchema, db: AsyncSession, id: int) -> dict: """ 详情 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - id: int - 数据ID 返回: - dict - 数据详情 """ - obj = await {{ class_name }}CRUD(auth).get(id=id) + obj = await {{ class_name }}CRUD(auth, db).get(id=id) if not obj: raise CustomException(msg="该数据不存在") return {{ class_name }}OutSchema.model_validate(obj).model_dump() @classmethod - async def list_service(cls, auth: AuthSchema, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]: + async def list_service(cls, auth: AuthSchema, db: AsyncSession, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]: """ 列表查询 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - search: {{ class_name }}QueryParam | None - 查询参数 - order_by: list[dict] | None - 排序参数 @@ -53,16 +57,17 @@ class {{ class_name }}Service: - list[dict] - 数据列表 """ search_dict = search.__dict__ if search else None - obj_list = await {{ class_name }}CRUD(auth).list(search=search_dict, order_by=order_by) + obj_list = await {{ class_name }}CRUD(auth, db).list(search=search_dict, order_by=order_by) return [{{ class_name }}OutSchema.model_validate(obj).model_dump() for obj in obj_list] @classmethod - async def page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> dict: + async def page_service(cls, auth: AuthSchema, db: AsyncSession, page_no: int, page_size: int, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> dict: """ 分页查询(数据库分页) 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - page_no: int - 页码 - page_size: int - 每页数量 - search: {{ class_name }}QueryParam | None - 查询参数 @@ -74,7 +79,7 @@ class {{ class_name }}Service: search_dict = search.__dict__ if search else {} order_by_list = order_by or [{'{{ pk_column_name }}': 'asc'}] offset = (page_no - 1) * page_size - result = await {{ class_name }}CRUD(auth).page( + result = await {{ class_name }}CRUD(auth, db).page( offset=offset, limit=page_size, order_by=order_by_list, @@ -84,12 +89,13 @@ class {{ class_name }}Service: return result @classmethod - async def create_service(cls, auth: AuthSchema, data: {{ class_name }}CreateSchema) -> dict: + async def create_service(cls, auth: AuthSchema, db: AsyncSession, data: {{ class_name }}CreateSchema) -> dict: """ 创建 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - data: {{ class_name }}CreateSchema - 创建数据 返回: @@ -97,21 +103,22 @@ class {{ class_name }}Service: """ {% for column in columns %} {% if column.is_unique and column.column_name != 'uuid' %} - obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }}) + obj = await {{ class_name }}CRUD(auth, db).get({{ column.column_name }}=data.{{ column.column_name }}) if obj: raise CustomException(msg='创建失败,{{ column.column_comment }}已存在') {% endif %} {% endfor %} - obj = await {{ class_name }}CRUD(auth).create(data=data) + obj = await {{ class_name }}CRUD(auth, db).create(data=data) return {{ class_name }}OutSchema.model_validate(obj).model_dump() @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: {{ class_name }}UpdateSchema) -> dict: + async def update_service(cls, auth: AuthSchema, db: AsyncSession, id: int, data: {{ class_name }}UpdateSchema) -> dict: """ 更新 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - id: int - 数据ID - data: {{ class_name }}UpdateSchema - 更新数据 @@ -119,29 +126,30 @@ class {{ class_name }}Service: - dict - 更新结果 """ # 检查数据是否存在 - obj = await {{ class_name }}CRUD(auth).get(id=id) + obj = await {{ class_name }}CRUD(auth, db).get(id=id) if not obj: raise CustomException(msg='更新失败,该数据不存在') # 检查唯一性约束 {% for column in columns %} {% if column.is_unique and column.column_name != 'uuid' %} - exist_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }}) + exist_obj = await {{ class_name }}CRUD(auth, db).get({{ column.column_name }}=data.{{ column.column_name }}) if exist_obj and getattr(exist_obj, '{{ pk_column_name }}') != id: raise CustomException(msg='更新失败,{{ column.column_comment }}重复') {% endif %} {% endfor %} - obj = await {{ class_name }}CRUD(auth).update(id=id, data=data) + obj = await {{ class_name }}CRUD(auth, db).update(id=id, data=data) return {{ class_name }}OutSchema.model_validate(obj).model_dump() @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: + async def delete_service(cls, auth: AuthSchema, db: AsyncSession, ids: list[int]) -> None: """ 删除 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - ids: list[int] - 数据ID列表 返回: @@ -150,24 +158,25 @@ class {{ class_name }}Service: if len(ids) < 1: raise CustomException(msg='删除失败,删除对象不能为空') for id in ids: - obj = await {{ class_name }}CRUD(auth).get(id=id) + obj = await {{ class_name }}CRUD(auth, db).get(id=id) if not obj: raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') - await {{ class_name }}CRUD(auth).delete(ids=ids) + await {{ class_name }}CRUD(auth, db).delete(ids=ids) @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + async def set_available_service(cls, auth: AuthSchema, db: AsyncSession, data: BatchSetAvailable) -> None: """ 批量设置状态 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - data: BatchSetAvailable - 批量设置状态数据 返回: - None """ - await {{ class_name }}CRUD(auth).set(ids=data.ids, status=data.status) + await {{ class_name }}CRUD(auth, db).set(ids=data.ids, status=data.status) @staticmethod def batch_export_service(obj_list: list[dict]) -> bytes: @@ -202,12 +211,13 @@ class {{ class_name }}Service: return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) @classmethod - async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str: + async def batch_import_service(cls, auth: AuthSchema, db: AsyncSession, file: UploadFile, update_support: bool = False) -> str: """ 批量导入 参数: - auth: AuthSchema - 认证信息 + - db: AsyncSession - 数据库会话 - file: UploadFile - 上传的Excel文件 - update_support: bool - 是否支持更新存在数据 @@ -273,10 +283,10 @@ class {{ class_name }}Service: # 检查唯一性约束 {% for column in columns %} {% if column.is_unique and column.column_name not in ['uuid', 'tenant_id', 'created_time', 'updated_time', 'created_id', 'updated_id', 'is_deleted', 'deleted_time', 'deleted_id'] %} - exists_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=create_schema.{{ column.column_name }}) + exists_obj = await {{ class_name }}CRUD(auth, db).get({{ column.column_name }}=create_schema.{{ column.column_name }}) if exists_obj: if update_support: - await {{ class_name }}CRUD(auth).update(id=getattr(exists_obj, '{{ pk_column_name }}'), data=create_schema) + await {{ class_name }}CRUD(auth, db).update(id=getattr(exists_obj, '{{ pk_column_name }}'), data=create_schema) success_count += 1 else: error_msgs.append(f"第{count}行: {{ column.column_comment }} {create_schema.{{ column.column_name }}} 已存在") @@ -284,7 +294,7 @@ class {{ class_name }}Service: {% endif %} {% endfor %} - await {{ class_name }}CRUD(auth).create(data=create_schema) + await {{ class_name }}CRUD(auth, db).create(data=create_schema) success_count += 1 except Exception as e: error_msgs.append(f"第{count}行: {str(e)}") diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 3bf0d3d8..16a8652b 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -177,7 +177,7 @@ async def _test_lifespan(app) -> AsyncGenerator[Any, None]: from app.api.v1.module_system.user.model import UserModel from app.core.database import async_db_session - from app.utils.hash_bcrpy_util import PwdUtil + from app.utils.password_util import PwdUtil async with async_db_session() as db: await db.execute(update(UserModel).where(UserModel.username == "admin").values(password=PwdUtil.hash_password("admin123")))