diff --git a/backend/app/admin/api/v1/sys/__init__.py b/backend/app/admin/api/v1/sys/__init__.py index d757bf1a..00451851 100644 --- a/backend/app/admin/api/v1/sys/__init__.py +++ b/backend/app/admin/api/v1/sys/__init__.py @@ -4,6 +4,7 @@ from backend.app.admin.api.v1.sys.api import router as api_router from backend.app.admin.api.v1.sys.casbin import router as casbin_router +from backend.app.admin.api.v1.sys.config import router as config_router from backend.app.admin.api.v1.sys.dept import router as dept_router from backend.app.admin.api.v1.sys.dict_data import router as dict_data_router from backend.app.admin.api.v1.sys.dict_type import router as dict_type_router @@ -15,6 +16,7 @@ router.include_router(api_router, prefix='/apis', tags=['系统API']) router.include_router(casbin_router, prefix='/casbin', tags=['系统Casbin权限']) +router.include_router(config_router, prefix='/configs', tags=['系统配置']) router.include_router(dept_router, prefix='/depts', tags=['系统部门']) router.include_router(dict_data_router, prefix='/dict_datas', tags=['系统字典数据']) router.include_router(dict_type_router, prefix='/dict_types', tags=['系统字典类型']) diff --git a/backend/app/admin/api/v1/sys/config.py b/backend/app/admin/api/v1/sys/config.py new file mode 100644 index 00000000..cf3cf95f --- /dev/null +++ b/backend/app/admin/api/v1/sys/config.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Annotated + +from fastapi import APIRouter, Depends, Path, Query + +from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam +from backend.app.admin.service.config_service import config_service +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth +from backend.common.security.permission import RequestPermission +from backend.common.security.rbac import DependsRBAC + +router = APIRouter() + + +@router.get('', summary='获取系统配置详情', dependencies=[DependsJwtAuth]) +async def get_config() -> ResponseModel: + config = await config_service.get() + return await response_base.success(data=config) + + +@router.post( + '', + summary='创建系统配置', + dependencies=[ + Depends(RequestPermission('sys:config:add')), + DependsRBAC, + ], +) +async def create_config(obj: CreateConfigParam) -> ResponseModel: + await config_service.create(obj=obj) + return await response_base.success() + + +@router.put( + '/{pk}', + summary='更新系统配置', + dependencies=[ + Depends(RequestPermission('sys:config:edit')), + DependsRBAC, + ], +) +async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -> ResponseModel: + count = await config_service.update(pk=pk, obj=obj) + if count > 0: + return await response_base.success() + return await response_base.fail() + + +@router.delete( + '', + summary='(批量)删除系统配置', + dependencies=[ + Depends(RequestPermission('sys:config:del')), + DependsRBAC, + ], +) +async def delete_config(pk: Annotated[list[int], Query(...)]) -> ResponseModel: + count = await config_service.delete(pk=pk) + if count > 0: + return await response_base.success() + return await response_base.fail() diff --git a/backend/app/admin/conf.py b/backend/app/admin/conf.py index 439902e1..8d19e9d7 100644 --- a/backend/app/admin/conf.py +++ b/backend/app/admin/conf.py @@ -30,6 +30,9 @@ class AdminSettings(BaseSettings): CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha' CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒 + # Config + CONFIG_REDIS_KEY: str = 'fba_config' + @lru_cache def get_admin_settings() -> AdminSettings: diff --git a/backend/app/admin/crud/crud_config.py b/backend/app/admin/crud/crud_config.py new file mode 100644 index 00000000..92f041b7 --- /dev/null +++ b/backend/app/admin/crud/crud_config.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Sequence + +from sqlalchemy import delete, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy_crud_plus import CRUDPlus + +from backend.app.admin.model import Config +from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam + + +class CRUDConfig(CRUDPlus[Config]): + async def get_one(self, db: AsyncSession) -> Config | None: + """ + 获取 Config + + :param db: + :return: + """ + query = await db.execute(select(self.model).limit(1)) + return query.scalars().first() + + async def get_all(self, db: AsyncSession) -> Sequence[Config]: + """ + 获取所有 Config + + :param db: + :return: + """ + return await self.select_models(db) + + async def create(self, db: AsyncSession, obj_in: CreateConfigParam) -> None: + """ + 创建 Config + + :param db: + :param obj_in: + :return: + """ + await self.create_model(db, obj_in) + + async def update(self, db: AsyncSession, pk: int, obj_in: UpdateConfigParam) -> int: + """ + 更新 Config + + :param db: + :param pk: + :param obj_in: + :return: + """ + return await self.update_model(db, pk, obj_in) + + async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除 Config + + :param db: + :param pk: + :return: + """ + configs = await db.execute(delete(self.model).where(self.model.id.in_(pk))) + return configs.rowcount + + +config_dao: CRUDConfig = CRUDConfig(Config) diff --git a/backend/app/admin/model/__init__.py b/backend/app/admin/model/__init__.py index fa2b0ccb..345990fd 100644 --- a/backend/app/admin/model/__init__.py +++ b/backend/app/admin/model/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from backend.app.admin.model.sys_api import Api from backend.app.admin.model.sys_casbin_rule import CasbinRule +from backend.app.admin.model.sys_config import Config from backend.app.admin.model.sys_dept import Dept from backend.app.admin.model.sys_dict_data import DictData from backend.app.admin.model.sys_dict_type import DictType diff --git a/backend/app/admin/model/sys_config.py b/backend/app/admin/model/sys_config.py new file mode 100644 index 00000000..2da68102 --- /dev/null +++ b/backend/app/admin/model/sys_config.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from sqlalchemy import String +from sqlalchemy.dialects.mysql import LONGTEXT +from sqlalchemy.orm import Mapped, mapped_column + +from backend.common.model import Base, id_key + + +class Config(Base): + """系统配置表""" + + __tablename__ = 'sys_config' + + id: Mapped[id_key] = mapped_column(init=False) + login_title: Mapped[str] = mapped_column(String(20), default='登陆 FBA', comment='登陆页面标题') + login_sub_title: Mapped[str] = mapped_column( + String(50), default='fastapi_best_architecture', comment='登陆页面子标题' + ) + footer: Mapped[str] = mapped_column(String(50), default='FBA', comment='页脚标题') + logo: Mapped[str] = mapped_column(LONGTEXT, default='Arco', comment='Logo') + system_title: Mapped[str] = mapped_column(String(20), default='Arco', comment='系统标题') + system_comment: Mapped[str] = mapped_column( + LONGTEXT, + default='基于 FastAPI 构建的前后端分离 RBAC 权限控制系统,采用独特的伪三层架构模型设计,' + '内置 fastapi-admin 基本实现,并作为模板库免费开源', + comment='系统描述', + ) diff --git a/backend/app/admin/schema/config.py b/backend/app/admin/schema/config.py new file mode 100644 index 00000000..f9439c09 --- /dev/null +++ b/backend/app/admin/schema/config.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime + +from pydantic import ConfigDict, Field + +from backend.common.schema import SchemaBase + + +class ConfigSchemaBase(SchemaBase): + login_title: str = Field(default='登陆 FBA') + login_sub_title: str = Field(default='fastapi_best_architecture') + footer: str = Field(default='FBA') + logo: str = Field(default='Arco') + system_title: str = Field(default='Arco') + system_comment: str = Field( + default='基于 FastAPI 构建的前后端分离 RBAC 权限控制系统,采用独特的伪三层架构模型设计,' + '内置 fastapi-admin 基本实现,并作为模板库免费开源' + ) + + +class CreateConfigParam(ConfigSchemaBase): + pass + + +class UpdateConfigParam(ConfigSchemaBase): + pass + + +class GetConfigListDetails(ConfigSchemaBase): + model_config = ConfigDict(from_attributes=True) + + id: int + created_time: datetime + updated_time: datetime | None = None diff --git a/backend/app/admin/service/config_service.py b/backend/app/admin/service/config_service.py new file mode 100644 index 00000000..070483c8 --- /dev/null +++ b/backend/app/admin/service/config_service.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from backend.app.admin.conf import admin_settings +from backend.app.admin.crud.crud_config import config_dao +from backend.app.admin.model import Config +from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam +from backend.common.exception import errors +from backend.database.db_mysql import async_db_session +from backend.database.db_redis import redis_client +from backend.utils.serializers import select_as_dict + + +class ConfigService: + @staticmethod + async def get() -> Config | dict: + async with async_db_session() as db: + cache_config = await redis_client.hgetall(admin_settings.CONFIG_REDIS_KEY) + if not cache_config: + config = await config_dao.get_one(db) + if not config: + raise errors.NotFoundError(msg='系统配置不存在') + data_map = await select_as_dict(config) + del data_map['created_time'] + del data_map['updated_time'] + await redis_client.hset(admin_settings.CONFIG_REDIS_KEY, mapping=data_map) + return config + else: + return cache_config + + @staticmethod + async def create(*, obj: CreateConfigParam) -> None: + async with async_db_session.begin() as db: + config = await config_dao.get_one(db) + if config: + raise errors.ForbiddenError(msg='系统配置已存在') + await config_dao.create(db, obj) + await redis_client.hset(admin_settings.CONFIG_REDIS_KEY, mapping=obj.model_dump()) + + @staticmethod + async def update(*, pk: int, obj: UpdateConfigParam) -> int: + async with async_db_session.begin() as db: + count = await config_dao.update(db, pk, obj) + await redis_client.hset(admin_settings.CONFIG_REDIS_KEY, mapping=obj.model_dump()) + return count + + @staticmethod + async def delete(*, pk: list[int]) -> int: + async with async_db_session.begin() as db: + configs = await config_dao.get_all(db) + if len(configs) == 1: + raise errors.ForbiddenError(msg='系统配置无法彻底删除') + count = await config_dao.delete(db, pk) + await redis_client.delete(admin_settings.CONFIG_REDIS_KEY) + return count + + +config_service = ConfigService() diff --git a/backend/templates/py/crud.jinja b/backend/templates/py/crud.jinja index bd2bcc8d..c5e5fed0 100644 --- a/backend/templates/py/crud.jinja +++ b/backend/templates/py/crud.jinja @@ -21,7 +21,7 @@ class CRUD{{ table_name_class }}(CRUDPlus[{{ schema_name }}]): """ return await self.select_model_by_id(db, pk) - async def get_all(self, db: AsyncSession) -> Sequence[{{ schema_name }}]: + async def get_all(self, db: AsyncSession) -> Sequence[{{ table_name_class }}]: """ 获取所有 {{ schema_name }} @@ -63,4 +63,4 @@ class CRUD{{ table_name_class }}(CRUDPlus[{{ schema_name }}]): return {{ table_name_en }}s.rowcount -{{ table_name_en }}_dao: CRUD{{ schema_name }} = CRUD{{ schema_name }}({{ schema_name }}) +{{ table_name_en }}_dao: CRUD{{ table_name_class }} = CRUD{{ table_name_class }}({{ table_name_class }}) diff --git a/backend/templates/py/service.jinja b/backend/templates/py/service.jinja index 664c034f..e392041a 100644 --- a/backend/templates/py/service.jinja +++ b/backend/templates/py/service.jinja @@ -9,13 +9,13 @@ from backend.common.exception import errors from backend.database.db_mysql import async_db_session -class {{ schema_name }}Service: +class {{ table_name_class }}Service: @staticmethod async def get(*, pk: int) -> {{ table_name_class }}: async with async_db_session() as db: {{ table_name_en }} = await {{ table_name_en }}_dao.get(db, pk) if not {{ table_name_en }}: - raise errors.NotFoundError(msg='接口不存在') + raise errors.NotFoundError(msg='{{ table_simple_name_zh }}不存在') return {{ table_name_en }} @staticmethod @@ -45,4 +45,4 @@ class {{ schema_name }}Service: return count -{{ table_name_en }}_service = {{ schema_name }}Service() +{{ table_name_en }}_service = {{ table_name_class }}Service() diff --git a/backend/utils/gen_template.py b/backend/utils/gen_template.py index 9efd5c46..90ceee24 100644 --- a/backend/utils/gen_template.py +++ b/backend/utils/gen_template.py @@ -80,7 +80,7 @@ def get_vars(business: GenBusiness, models: list[GenModel]) -> dict: 'table_comment': business.table_comment, 'schema_name': to_pascal(business.schema_name), 'have_datetime_column': business.have_datetime_column, - 'permission_sign': str(business.__tablename__.replace('_', ':')), + 'permission_sign': str(business.table_name_en.replace('_', ':')), 'models': models, }