diff --git a/README.md b/README.md index 3d0f92f..d10ba53 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# simple_ams -简单的资产管理系统 +# Fastapi Admin +前后端分离项目实现的一个后端管理框架 +* 前端:vue3 + element plus +* 后端:fastapi + sqlmodel **个人学习项目,只会对明显的bug进行修复,不会进行过多新功能的更新** @@ -17,103 +19,106 @@ uvicorn server.main:app --reload ``` +## 约束 +1. 后端数据库对于布尔值的传递统一数据库设置为tinyint,0为假,1为真 +2. 前端所有bool都0为假,1为真 # 功能实现介绍 -## 权限分配 -实力有限,实现简化的权限分配功能,思路如下: + +## 分页查询实现 ### 前端 -1. 页面下的按钮需要权限管控的按钮,单独列出,再对这个按钮做权限分配 -2. 在“菜单管理”中对应页面下添加“按钮”,**路径**字段需要跟到时候做权限判断时对应 -3. 进入“角色管理”页面,分配对应的角色,可选择页面的按钮 - **NOTICE:**上级页面需要手动勾选,不然登录无法带出来 -4. 给按钮增加**v-if**判断语法,用法如下: - ``` - v-if="$route.meta.import === true" - ``` +```js + +import usePagination from '@/composables/usePagination' + + +const searchForm = { + name: null, + email: null, + enable: null + } + + + const { + search, + tableData, + currentPage, + pageSize, + orderModel, + total, + freshCurrentPage, + handleSearch + } = usePagination('/api/users/search', searchForm) +``` ### 后端 -* 用户请求权限,返回子页面带meta元素,示例如下: - ``` - [ - { - "id": 1, - "component": 'Layout, - "parent_id": null, - "url": null, - "path": "/assets", - "name": "资产管理", - "type": "page", - "enable": 1, - "children": [ - { - "id": 2, - "parent_id": 1, - "url": null, - "path": "add", - "name": "增加资产", - "type": "page", - "enable": 1 - }, - { - "id": 11, - "parent_id": 1, - "url": null, - "path": "system", - "name": "系统", - "type": "page", - "enable": 1, - - "meta": { - "import": true, - "output": true - } - } - ] - }, - { - "id": 3, - "component": "Layout", - "parent_id": null, - "url": null, - "path": "/system", - "name": "系统管理", - "type": "page", - "enable": 1, - "children": [ - { - "id": 4, - "parent_id": 3, - "url": null, - "path": "user", - "name": "用户管理", - "type": "page", - "enable": 1 - }, - { - "id": 6, - "parent_id": 3, - "url": null, - "path": "roles", - "name": "角色管理", - "type": "page", - "enable": 1 - }, - { - "id": 7, - "parent_id": 3, - "url": null, - "path": "menus", - "name": "菜单管理", - "type": "page", - "enable": 1 - } - ] - } - ] - ``` - -## 资产信息的动态字段设计 -充分利用MySQL的JSON字段特性,把动态字段放入JSON数据类型中。 -### 导入细节 -批量导入时,分为两类:1. 固定字段的列,2. 动态字段的列。 -对于**动态字段的列**,通过pandas函数拼接成str,然后进行格式转换导入。 +定义了一个分页模型 +```python +from typing import Optional, Generic, TypeVar +from pydantic import BaseModel + +T = TypeVar('T') + + +class Pagination(BaseModel, Generic[T]): + search: T + page: Optional[int] = 1 + page_size: Optional[int] = 10 + model: Optional[str] = 'asc' +``` + +使用 +```python +@router.post('/dict/item/search', summary="字典列表查询", response_model=ApiResponse[SearchResponse[DictRead]]) +async def search_items(search: Pagination[DictItemSearch], session: Session = Depends(get_session)): + # 需要定义一个filter_type,用于区分各个字段的匹配形式,可用为:l_like、r_like、like、eq、ne、lt、le、gt、ge + filter_type = DictItemSearchFilter(dict_id='eq', label='like', enable='eq', value='like') + total = crud.internal.dict_item.search_total(session, search.search, filter_type.dict()) + items: List[DictRead] = crud.internal.dict_item.search(session, search, filter_type.dict()) + # 转义下数据类型,不然在执行return的时候,会去获取外键、关联字段相关的内容,容易造成数据量过多等问题 + item_list = [DictRead.from_orm(item) for item in items] + return ApiResponse( + data={ + 'total': total, + 'data': item_list + } + + ) +``` + +## 权限管控 +通过casbin实现简化的权限管控功能,思路如下: +1. 对于不需要token验证的,写入settings的APISettings.NO_VERIFY_URL中 +2. 对于需要权限管控的接口,写入casbin中,并且对需要权限验证的接口使用casbin验证 +3. 前端通过权限字段,进行显示 +4. 只能对按钮级别的功能实现权限管控 +5. 页面管控,只是后端返回菜单列表,前端根据菜单列表进行显示,后端没有对页面进行权限管控 + +### 前端 +v-permission定义了权限标识,当拥有权限时,可以页面上能显示按钮,同时,后端也会进行权限的判断。 +```js + 编辑 + +``` +### 后端 +```python +@router.put('/roles', summary="更新角色", response_model=ApiResponse[Role], + dependencies=[Depends(Authority('role:update'))]) +async def update_roles(role_info: RoleUpdate, session: Session = Depends(get_session)): + print(role_info) + if role_info.name == 'admin': + ApiResponse(code=status.HTTP_400_BAD_REQUEST, message='admin权限组无法更新信息') + db_obj = crud.internal.role.get(session, role_info.id) + enable_menus = role_info.menus + delattr(role_info, 'menus') + db_obj = crud.internal.role.update(session, db_obj, role_info) + crud.internal.role.update_menus(session, db_obj, enable_menus) + return ApiResponse( + data=db_obj + ) +``` + + +### 参考项目: +* https://github.com/xingxingzaixian/FastAPI-MySQL-Tortoise-Casbin \ No newline at end of file diff --git "a/server/alembic/versions/001da528b756_\346\267\273\345\212\240\346\225\260\346\215\256\345\255\227\345\205\270.py" "b/server/alembic/versions/001da528b756_\346\267\273\345\212\240\346\225\260\346\215\256\345\255\227\345\205\270.py" new file mode 100644 index 0000000..90a94d4 --- /dev/null +++ "b/server/alembic/versions/001da528b756_\346\267\273\345\212\240\346\225\260\346\215\256\345\255\227\345\205\270.py" @@ -0,0 +1,47 @@ +"""添加数据字典 + +Revision ID: 001da528b756 +Revises: +Create Date: 2022-11-08 10:11:55.957440 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '001da528b756' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('data_dict', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='字典名称'), + sa.Column('code', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='字典编号'), + sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True, comment='描述'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('dict_item', + sa.Column('enable', sa.Boolean(), nullable=True, comment='是否启用'), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='名称'), + sa.Column('data', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='数据值'), + sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True, comment='描述'), + sa.Column('sort', sa.Integer(), nullable=True, comment='排序值,越小越靠前'), + sa.Column('dict_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['dict_id'], ['data_dict.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('dict_item') + op.drop_table('data_dict') + # ### end Alembic commands ### diff --git "a/server/alembic/versions/02cae67f3be4_\350\217\234\345\215\225\346\267\273\345\212\240icon\345\233\276\346\240\207\345\255\227\346\256\265.py" "b/server/alembic/versions/02cae67f3be4_\350\217\234\345\215\225\346\267\273\345\212\240icon\345\233\276\346\240\207\345\255\227\346\256\265.py" deleted file mode 100644 index 5842287..0000000 --- "a/server/alembic/versions/02cae67f3be4_\350\217\234\345\215\225\346\267\273\345\212\240icon\345\233\276\346\240\207\345\255\227\346\256\265.py" +++ /dev/null @@ -1,29 +0,0 @@ -"""菜单添加icon图标字段 - -Revision ID: 02cae67f3be4 -Revises: b703662d9cf9 -Create Date: 2022-11-02 13:54:41.361623 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '02cae67f3be4' -down_revision = 'b703662d9cf9' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('menu', sa.Column('icon', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='Icon图标')) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('menu', 'icon') - # ### end Alembic commands ### diff --git a/server/alembic/versions/334040db4d77_delete_sysapi.py b/server/alembic/versions/334040db4d77_delete_sysapi.py deleted file mode 100644 index 0ecc8da..0000000 --- a/server/alembic/versions/334040db4d77_delete_sysapi.py +++ /dev/null @@ -1,53 +0,0 @@ -"""delete sysapi - -Revision ID: 334040db4d77 -Revises: a21539d7fbb7 -Create Date: 2022-10-28 15:00:57.698249 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '334040db4d77' -down_revision = 'a21539d7fbb7' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('menu_apis') - op.drop_table('sys_api') - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('menu_apis', - sa.Column('menu_id', mysql.INTEGER(), autoincrement=False, nullable=False), - sa.Column('api_id', mysql.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['api_id'], ['sys_api.id'], name='menu_apis_ibfk_2'), - sa.ForeignKeyConstraint(['menu_id'], ['menu.id'], name='menu_apis_ibfk_1'), - sa.PrimaryKeyConstraint('menu_id', 'api_id'), - mysql_collate='utf8mb4_general_ci', - mysql_default_charset='utf8mb4', - mysql_engine='InnoDB', - mysql_row_format='DYNAMIC' - ) - op.create_table('sys_api', - sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False), - sa.Column('tags', mysql.VARCHAR(collation='utf8mb4_general_ci', length=10), nullable=False, comment='标签'), - sa.Column('path', mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), nullable=False, comment='API路径'), - sa.Column('method', mysql.VARCHAR(collation='utf8mb4_general_ci', length=10), nullable=False, comment='HTTP方法'), - sa.Column('summary', mysql.VARCHAR(collation='utf8mb4_general_ci', length=20), nullable=False, comment='描述'), - sa.Column('deprecated', mysql.TINYINT(display_width=1), server_default=sa.text("'0'"), autoincrement=False, nullable=True, comment='是否废弃'), - sa.PrimaryKeyConstraint('id'), - mysql_collate='utf8mb4_general_ci', - mysql_default_charset='utf8mb4', - mysql_engine='InnoDB', - mysql_row_format='DYNAMIC' - ) - # ### end Alembic commands ### diff --git "a/server/alembic/versions/9edc223ae20a_update\346\225\260\346\215\256\345\255\227\345\205\270.py" "b/server/alembic/versions/9edc223ae20a_update\346\225\260\346\215\256\345\255\227\345\205\270.py" new file mode 100644 index 0000000..1a30376 --- /dev/null +++ "b/server/alembic/versions/9edc223ae20a_update\346\225\260\346\215\256\345\255\227\345\205\270.py" @@ -0,0 +1,43 @@ +"""update数据字典 + +Revision ID: 9edc223ae20a +Revises: 001da528b756 +Create Date: 2022-11-11 10:57:04.750421 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '9edc223ae20a' +down_revision = '001da528b756' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('dict_item', sa.Column('label', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='名称')) + op.add_column('dict_item', sa.Column('value', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='数据值')) + op.drop_column('dict_item', 'data') + op.drop_column('dict_item', 'name') + op.alter_column('menu', 'icon', + existing_type=mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), + nullable=True, + existing_comment='Icon图标') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('menu', 'icon', + existing_type=mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), + nullable=False, + existing_comment='Icon图标') + op.add_column('dict_item', sa.Column('name', mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), nullable=False, comment='名称')) + op.add_column('dict_item', sa.Column('data', mysql.VARCHAR(collation='utf8mb4_general_ci', length=100), nullable=False, comment='数据值')) + op.drop_column('dict_item', 'value') + op.drop_column('dict_item', 'label') + # ### end Alembic commands ### diff --git a/server/alembic/versions/a21539d7fbb7_init.py b/server/alembic/versions/a21539d7fbb7_init.py deleted file mode 100644 index b9ff371..0000000 --- a/server/alembic/versions/a21539d7fbb7_init.py +++ /dev/null @@ -1,35 +0,0 @@ -"""init - -Revision ID: a21539d7fbb7 -Revises: -Create Date: 2022-10-28 10:38:17.100830 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = 'a21539d7fbb7' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user_roles', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('user_id', 'role_id') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user_roles') - # ### end Alembic commands ### diff --git "a/server/alembic/versions/a5e2cea226d4_\346\267\273\345\212\240\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" "b/server/alembic/versions/a5e2cea226d4_\346\267\273\345\212\240\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" deleted file mode 100644 index b4b27c0..0000000 --- "a/server/alembic/versions/a5e2cea226d4_\346\267\273\345\212\240\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" +++ /dev/null @@ -1,29 +0,0 @@ -"""添加菜单排序字段 - -Revision ID: a5e2cea226d4 -Revises: 334040db4d77 -Create Date: 2022-10-31 09:44:30.289049 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = 'a5e2cea226d4' -down_revision = '334040db4d77' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('menu', sa.Column('sort', sa.Integer(), nullable=True, comment='菜单排序')) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('menu', 'sort') - # ### end Alembic commands ### diff --git "a/server/alembic/versions/b703662d9cf9_\346\233\264\346\226\260\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" "b/server/alembic/versions/b703662d9cf9_\346\233\264\346\226\260\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" deleted file mode 100644 index 1f582ba..0000000 --- "a/server/alembic/versions/b703662d9cf9_\346\233\264\346\226\260\350\217\234\345\215\225\346\216\222\345\272\217\345\255\227\346\256\265.py" +++ /dev/null @@ -1,37 +0,0 @@ -"""更新菜单排序字段 - -Revision ID: b703662d9cf9 -Revises: a5e2cea226d4 -Create Date: 2022-10-31 10:22:22.613017 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = 'b703662d9cf9' -down_revision = 'a5e2cea226d4' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('menu', 'sort', - existing_type=mysql.INTEGER(), - type_=sa.Float(), - existing_comment='菜单排序', - existing_nullable=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('menu', 'sort', - existing_type=sa.Float(), - type_=mysql.INTEGER(), - existing_comment='菜单排序', - existing_nullable=True) - # ### end Alembic commands ### diff --git a/server/common/response_code.py b/server/common/response_code.py index 24497eb..30584ee 100644 --- a/server/common/response_code.py +++ b/server/common/response_code.py @@ -1,8 +1,9 @@ -from typing import Generic, TypeVar, Optional +from typing import Generic, TypeVar, Optional, List from pydantic import Field from pydantic.generics import GenericModel T = TypeVar("T") +DATA = TypeVar("DATA") class ApiResponse(GenericModel, Generic[T]): @@ -12,3 +13,8 @@ class ApiResponse(GenericModel, Generic[T]): code: int = Field(default=200, description="返回码") message: str = Field(default="success", description="消息内容") data: Optional[T] + + +class SearchResponse(GenericModel, Generic[DATA]): + total: int + data: List[DATA] = [] diff --git a/server/crud/base.py b/server/crud/base.py index b02521a..2f3aedb 100644 --- a/server/crud/base.py +++ b/server/crud/base.py @@ -42,19 +42,19 @@ def delete(self, db: Session, id: int): db.commit() return obj - def _make_search(self, sql, search: Optional[Dict[str, Any]] = None): + def _make_search(self, sql, search: Optional[Dict[str, Any]] = None, filter_type: Optional[Dict[str, str]] = None): """ 用于构建专用的sql查询语句,子类需要重写此方法 :param sql: :param search: + :param filter_type:指定的各属性值判断形式 :return: """ if search is None: return sql q = deepcopy(search) - filter_type: Dict[str, Any] = q.pop('type', None) for key in q: - if (q[key] is not None) and (q[key]): + if (key in filter_type.keys()) and (q[key] is not None): if filter_type[key] == 'l_like': sql = sql.where(getattr(self.model, key).like(f'%{q[key]}')) elif filter_type[key] == 'r_like': @@ -75,11 +75,13 @@ def _make_search(self, sql, search: Optional[Dict[str, Any]] = None): sql = sql.where(getattr(self.model, key) >= q[key]) return sql - def search(self, session: Session, search: Pagination, columns: Optional[List] = None): + def search(self, session: Session, search: Pagination, filter_type: Optional[Dict[str, str]] = None, + columns: Optional[List] = None): """ 分页查询方法 :param session: :param search: Pagination实例对象,包含各搜索参数 + :param filter_type: 指定的各属性值判断形式 :param columns: 查询返回指定columns :return: """ @@ -87,9 +89,9 @@ def search(self, session: Session, search: Pagination, columns: Optional[List] = sql = select(self.model) else: sql = select(*columns) - sql = self._make_search(sql, search.search) + sql = self._make_search(sql, search.search, filter_type) subquery = select(self.model.id) - subquery = self._make_search(subquery, search.search) + subquery = self._make_search(subquery, search.search, filter_type) if search.model == 'desc': subquery = subquery.order_by(desc(self.model.id)) else: @@ -104,14 +106,15 @@ def search(self, session: Session, search: Pagination, columns: Optional[List] = results = session.exec(sql).all() return results - def search_total(self, session: Session, q: Dict[str, Any]): + def search_total(self, session: Session, q: Dict[str, Any], filter_type: Optional[Dict[str, str]] = None): """ 每次进行分页查询的时候,都需要返回一个total值,表示对应搜索,现阶段数据库有多少内容,便于前端分页数 :param session: :param q: + :param filter_type: 字段过滤形式 :return: """ sql = select(func.count(self.model.id)) - sql = self._make_search(sql, q) - print(sql) + sql = self._make_search(sql, q, filter_type) + print(str(sql)) return session.execute(sql).scalar() diff --git a/server/crud/internal/__init__.py b/server/crud/internal/__init__.py index a7c086d..51bdf03 100644 --- a/server/crud/internal/__init__.py +++ b/server/crud/internal/__init__.py @@ -1,3 +1,4 @@ from .user import user from .roles import role from .menu import menu +from .dictonary import data_dict, dict_item diff --git a/server/crud/internal/dictonary.py b/server/crud/internal/dictonary.py new file mode 100644 index 0000000..21b1e9c --- /dev/null +++ b/server/crud/internal/dictonary.py @@ -0,0 +1,22 @@ +from typing import Union +from sqlmodel import select, Session +from ...models.internal.dictonary import DataDict, DictItem +from ..base import CRUDBase +from ...schemas.internal.user import UserInfo, UserLogin +from .roles import role + + +class CRUDDict(CRUDBase[DataDict]): + pass + + +class CRUDItem(CRUDBase[DictItem]): + def get_items_by_code(self, db: Session, code: str): + dict_id = select(DataDict.id).where(DataDict.code == code).subquery() + sql = select(self.model).where(self.model.dict_id == dict_id).where(self.model.enable == 1).order_by( + self.model.id) + return db.exec(sql).all() + + +data_dict = CRUDDict(DataDict) +dict_item = CRUDItem(DictItem) diff --git a/server/crud/internal/menu.py b/server/crud/internal/menu.py index 1429fbb..10d1ddd 100644 --- a/server/crud/internal/menu.py +++ b/server/crud/internal/menu.py @@ -5,10 +5,8 @@ class CRUDMenu(CRUDBase[Menu]): - def search(self, session: Session, q: Optional = None) -> List[Menu]: + def search_menus(self, session: Session) -> List[Menu]: sql = select(self.model) - if q is not None: - sql = sql.where(self.model.name.like(f'%{q}%')) sql = sql.order_by(self.model.sort) return session.exec(sql).all() diff --git a/server/crud/internal/user.py b/server/crud/internal/user.py index 64744f9..b16d154 100644 --- a/server/crud/internal/user.py +++ b/server/crud/internal/user.py @@ -17,12 +17,6 @@ def check_name(self, session: Session, name: str): sql = select(self.model).where(self.model.name == name) return session.exec(sql).one() - # 重写父类的查询构建命令 - def _make_search(self, sql, q: Union[int, str]): - if q is not None: - sql = sql.where(self.model.name.like(f'%{q}%')) - return sql - def insert(self, session: Session, user_info: UserInfo) -> User: updated_user = User(**user_info.user.dict()) user_roles = role.get_roles_by_id(session, user_info.roles) diff --git a/server/main.py b/server/main.py index a7b9bbf..7fca15c 100644 --- a/server/main.py +++ b/server/main.py @@ -1,6 +1,6 @@ from sqlmodel import Session from fastapi import FastAPI, Depends -from .routers.internal import login, user, menu, roles +from .routers.internal import login, user, menu, roles, dictonary from .common.security import auth_check from .settings import engine @@ -11,6 +11,7 @@ app.include_router(user.router, tags=['用户管理']) app.include_router(menu.router, tags=['菜单管理']) app.include_router(roles.router, tags=['角色管理']) +app.include_router(dictonary.router, tags=['数据字典']) @app.on_event("startup") diff --git a/server/models/internal/__init__.py b/server/models/internal/__init__.py index efb0daf..95cd37a 100644 --- a/server/models/internal/__init__.py +++ b/server/models/internal/__init__.py @@ -1,3 +1,4 @@ from .user import User from .menu import Menu from .role import Role, RoleMenu +from .dictonary import DataDict, DictItem diff --git a/server/models/internal/dictonary.py b/server/models/internal/dictonary.py new file mode 100644 index 0000000..1b8da49 --- /dev/null +++ b/server/models/internal/dictonary.py @@ -0,0 +1,57 @@ +from typing import Optional, List +from sqlmodel import SQLModel, Field, Relationship, Column, Boolean, Integer + + +class DataDictBase(SQLModel): + name: str = Field(max_length=50, sa_column_kwargs={'comment': '字典名称'}) + code: str = Field(max_length=100, sa_column_kwargs={'comment': '字典编号'}) + desc: Optional[str] = Field(max_length=100, sa_column_kwargs={'comment': '描述'}) + + +class DataDict(DataDictBase, table=True): + __tablename__ = 'data_dict' + id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True)) + dict_items: List["DictItem"] = Relationship(back_populates="dict") + + +class DataDictSearch(SQLModel): + name: Optional[str] + code: Optional[str] + + +class DictBase(SQLModel): + label: str = Field(max_length=50, sa_column_kwargs={'comment': '名称'}) + value: str = Field(max_length=100, sa_column_kwargs={'comment': '数据值'}) + desc: Optional[str] = Field(max_length=100, sa_column_kwargs={'comment': '描述'}) + sort: Optional[int] = Field(sa_column_kwargs={'comment': '排序值,越小越靠前'}) + enable: bool = Field(default=True, sa_column=Column(Boolean, comment='是否启用')) + dict_id: Optional[int] = Field(foreign_key="data_dict.id") + + +class DictItem(DictBase, table=True): + __tablename__ = 'dict_item' + + id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True)) + dict: Optional[DataDict] = Relationship(back_populates='dict_items') + + +class DictRead(DictBase): + id: int + + +class DictUpdate(DictBase): + id: Optional[int] + + +class DictItemSearch(SQLModel): + dict_id: int + name: Optional[str] + data: Optional[str] + enable: Optional[bool] + + +class DictItemSearchFilter(SQLModel): + dict_id: str + label: str + value: str + enable: str diff --git a/server/routers/internal/dictonary.py b/server/routers/internal/dictonary.py new file mode 100644 index 0000000..c622c4f --- /dev/null +++ b/server/routers/internal/dictonary.py @@ -0,0 +1,78 @@ +from typing import Optional, List +from fastapi import APIRouter, Depends, status, HTTPException +from sqlmodel import Session +from ...schemas.internal.pagination import Pagination +from ...models.internal.dictonary import DataDict, DictRead, DictUpdate, DictItem, DataDictSearch, \ + DictItemSearch, DictItemSearchFilter +from ...common.response_code import ApiResponse, SearchResponse +from ...common.database import get_session +from ... import crud + +router = APIRouter(prefix='/api') + + +@router.post('/dict/item/search', summary="字典列表查询", response_model=ApiResponse[SearchResponse[DictRead]]) +async def search_items(search: Pagination[DictItemSearch], session: Session = Depends(get_session)): + filter_type = DictItemSearchFilter(dict_id='eq', label='like', enable='eq', value='like') + total = crud.internal.dict_item.search_total(session, search.search, filter_type.dict()) + items: List[DictRead] = crud.internal.dict_item.search(session, search, filter_type.dict()) + item_list = [DictRead.from_orm(item) for item in items] + return ApiResponse( + data={ + 'total': total, + 'data': item_list + } + + ) + + +@router.post('/dict/item', summary="添加字典字段", response_model=ApiResponse[DictRead]) +async def add_dict_item(dict_item: DictUpdate, session: Session = Depends(get_session)): + new_item = crud.internal.dict_item.insert(session, DictItem(**dict_item.dict())) + return ApiResponse( + data=DictRead.from_orm(new_item) + ) + + +@router.put('/dict/item', summary="更新字典元素", response_model=ApiResponse) +async def update_dict_item(dict_item: DictUpdate, session: Session = Depends(get_session)): + db_obj = crud.internal.dict_item.get(session, dict_item.id) + crud.internal.dict_item.update(session, db_obj, dict_item) + return ApiResponse() + + +@router.delete('/dict/item/{item_id}', summary="删除字典元素", ) +async def del_dict_item(item_id: int, session: Session = Depends(get_session)): + crud.internal.dict_item.delete(session, item_id) + return ApiResponse() + + +@router.get("/dict/{dict_code}", summary="获取数据字典", response_model=ApiResponse[List[DictRead]], + response_model_exclude={'data': {'__all__': {'desc', 'sort', 'enable'}}}) +async def get_dict(dict_code: str, session: Session = Depends(get_session)): + dict_items: List[DictItem] = crud.internal.dict_item.get_items_by_code(session, dict_code) + return ApiResponse( + data=[DictRead.from_orm(item) for item in dict_items] + ) + + +@router.post("/dict", summary="新建数据字典", response_model=ApiResponse[DataDict]) +async def add_dict(data_dict: DataDict, session: Session = Depends(get_session)): + obj = crud.internal.data_dict.insert(session, data_dict) + return ApiResponse( + data=obj + ) + + +@router.post('/dict/search', + summary="查询数据字典") +async def get_dicts(search: Pagination[DataDictSearch], session: Session = Depends(get_session)): + filter_type = DataDictSearch(name='like', code='like') + total = crud.internal.data_dict.search_total(session, search.search, filter_type.dict()) + dicts: List[DataDict] = crud.internal.data_dict.search(session, search, filter_type.dict()) + return ApiResponse( + data={ + 'total': total, + 'data': dicts + } + ) diff --git a/server/routers/internal/menu.py b/server/routers/internal/menu.py index cf912be..268bdca 100644 --- a/server/routers/internal/menu.py +++ b/server/routers/internal/menu.py @@ -12,10 +12,10 @@ router = APIRouter(prefix='/api') -@router.get('/menus', summary="查询菜单", response_model=ApiResponse[List[MenusWithChild]]) -async def get_all_menu(q: Optional[str] = None, session: Session = Depends(get_session)): +@router.get('/menus', summary="列出菜单", response_model=ApiResponse[List[MenusWithChild]]) +async def get_all_menu(session: Session = Depends(get_session)): # 复用crud.get_menu_list,默认role为admin就是返回所有的菜单列表 - menu_list: List[Menu] = crud.menu.search(session, q) + menu_list: List[Menu] = crud.menu.search_menus(session) user_menus = utils.menu_convert(menu_list) return ApiResponse( data=user_menus diff --git a/server/routers/internal/roles.py b/server/routers/internal/roles.py index 64f00e6..5b90e4c 100644 --- a/server/routers/internal/roles.py +++ b/server/routers/internal/roles.py @@ -7,9 +7,8 @@ from ...common.database import get_session from ... import crud from ...models.internal import Role, Menu -from ...models.internal.role import RoleWithMenus, RoleInsert, RoleUpdate +from ...models.internal.role import RoleBase, RoleWithMenus, RoleInsert, RoleUpdate from ...schemas.internal.pagination import Pagination -from ...schemas.internal.roles import RoleSearch from ...common.utils import menu_convert router = APIRouter(prefix='/api') @@ -37,10 +36,10 @@ async def get_role_menus(id: Optional[int] = None, session: Session = Depends(ge @router.post('/roles/search', summary="查询角色") -async def get_roles(search: Pagination[RoleSearch], session: Session = Depends(get_session)): - total = crud.internal.role.search_total(session, search.search) +async def get_roles(search: Pagination[RoleBase], session: Session = Depends(get_session)): + total = crud.internal.role.search_total(session, search.search, {'name': 'like', 'enable': 'eq'}) print(total) - roles: List[Role] = crud.internal.role.search(session, search) + roles: List[Role] = crud.internal.role.search(session, search, {'name': 'like', 'enable': 'eq'}) role_with_menus: List[RoleWithMenus] = [] for role in roles: new_role = RoleWithMenus(**role.dict(), menus=role.menus) diff --git a/server/routers/internal/user.py b/server/routers/internal/user.py index 2ea1814..4605bc2 100644 --- a/server/routers/internal/user.py +++ b/server/routers/internal/user.py @@ -65,9 +65,9 @@ async def get_all_user(search: Pagination[UserWithOutPasswd], :param session: :return: """ - total = crud.internal.user.search_total(session, search.search) + total = crud.internal.user.search_total(session, search.search, {'name': 'like', 'enable': 'eq'}) print(total) - users = crud.internal.user.search(session, search) + users = crud.internal.user.search(session, search, {'name': 'like', 'enable': 'eq'}) users_list = [user.dict(exclude={"password"}) for user in users] print(users_list) return ApiResponse( @@ -97,11 +97,11 @@ async def update_user(user_info: UserCreateWithRoles, session: Session = Depends @router.put('/users/password', summary='重置密码', dependencies=[Depends(Authority('user:reset'))]) async def update_password(user: UserUpdatePassword, session: Session = Depends(get_session)): crud.internal.user.update_passwd(session, uid=user.id, passwd=user.password) + return ApiResponse() @router.put('/users/{uid}', - summary='更新用户', dependencies=[Depends(Authority("user:update"))], - status_code=status.HTTP_204_NO_CONTENT) + summary='更新用户', dependencies=[Depends(Authority("user:update"))]) async def update_user(uid: int, user_info: UserUpdateWithRoles, session: Session = Depends(get_session)): """ 更新用户信息的所有操作,可涉及更新用户名、密码、角色等 @@ -110,18 +110,16 @@ async def update_user(uid: int, user_info: UserUpdateWithRoles, session: Session :param session: :return: """ - print('update...') print(user_info.dict(exclude_unset=True, exclude_none=True)) user = crud.internal.user.update(session, uid, user_info) new_roles = [role.id for role in user.roles] casbin_enforcer.delete_roles_for_user(f'uid_{user.id}') for role in new_roles: casbin_enforcer.add_role_for_user(f'uid_{user.id}', f'role_{role}') - return user + return ApiResponse() -@router.delete('/users/{uid}', summary='删除用户', dependencies=[Depends(Authority("user:del"))], - status_code=status.HTTP_204_NO_CONTENT) +@router.delete('/users/{uid}', summary='删除用户', dependencies=[Depends(Authority("user:del"))]) async def delete_user(uid: int, session: Session = Depends(get_session)): try: user = session.exec(select(User).where(User.id == uid)).one() @@ -130,3 +128,4 @@ async def delete_user(uid: int, session: Session = Depends(get_session)): casbin_enforcer.delete_roles_for_user(f'uid_{user.id}') session.delete(user) session.commit() + return ApiResponse() diff --git a/server/schemas/internal/dictonary.py b/server/schemas/internal/dictonary.py new file mode 100644 index 0000000..9a548ef --- /dev/null +++ b/server/schemas/internal/dictonary.py @@ -0,0 +1,16 @@ +from typing import TypeVar, Generic +from pydantic import BaseModel +from .pagination import SearchBase +from ...models.internal.dictonary import DataDict, DictItem, DictBase + + +class DictSearch(DataDict, SearchBase): + pass + + +class DictItemSearch(DictItem, SearchBase): + pass + + +class DictItem(DictBase): + dict_id: int diff --git a/server/schemas/internal/pagination.py b/server/schemas/internal/pagination.py index af0a638..d26e106 100644 --- a/server/schemas/internal/pagination.py +++ b/server/schemas/internal/pagination.py @@ -1,4 +1,4 @@ -from typing import Optional, Generic, TypeVar +from typing import Optional, Generic, TypeVar, Dict from pydantic import BaseModel T = TypeVar('T') @@ -9,3 +9,7 @@ class Pagination(BaseModel, Generic[T]): page: Optional[int] = 1 page_size: Optional[int] = 10 model: Optional[str] = 'asc' + + +class SearchBase(BaseModel): + type: Dict[str, str] diff --git a/server/schemas/internal/roles.py b/server/schemas/internal/roles.py deleted file mode 100644 index bd6647d..0000000 --- a/server/schemas/internal/roles.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Dict -from ...models.internal.role import RoleBase - - -class RoleSearch(RoleBase): - type: Dict[str, str] \ No newline at end of file diff --git a/server/schemas/internal/sysapi.py b/server/schemas/internal/sysapi.py deleted file mode 100644 index b402b3d..0000000 --- a/server/schemas/internal/sysapi.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Dict -from ...models.internal.api import ApiBase - - -class ApiSearch(ApiBase): - type: Dict[str, str] diff --git a/www/src/api/dictonary.js b/www/src/api/dictonary.js new file mode 100644 index 0000000..0ccaf58 --- /dev/null +++ b/www/src/api/dictonary.js @@ -0,0 +1,7 @@ +import {GET, POST, PUT, DELETE} from '@/utils/request' + +export const PostNewDict = (dict) => POST('/api/dict', dict) +export const PostNewDictItem = (item) => POST('/api/dict/item', item) +export const PutDictItem = (item) => PUT('/api/dict/item', item) +export const DelDictItem = (id) => DELETE('/api/dict/item/' + id) +export const GetDictItems = (code) => GET('/api/dict/'+ code) diff --git a/www/src/api/menus.js b/www/src/api/menus.js index 9f34511..25935d0 100644 --- a/www/src/api/menus.js +++ b/www/src/api/menus.js @@ -1,8 +1,7 @@ import { GET, POST, PUT, DELETE } from '@/utils/request' // 菜单接口 -export const GetAllMenus = (q) => GET('/api/menus', { q }) +export const GetAllMenus = () => GET('/api/menus') export const PostNewMenu = (menu) => POST('/api/menus', menu ) export const PutMenu = (menu) => PUT('/api/menus', menu) -export const DeleteMenu = (id) => DELETE('/api/menus/' + id) -export const GetMenuApis = (id) => GET('/api/menus/' + id + '/apis') \ No newline at end of file +export const DeleteMenu = (id) => DELETE('/api/menus/' + id) \ No newline at end of file diff --git a/www/src/components/AutoDict.vue b/www/src/components/AutoDict.vue new file mode 100644 index 0000000..01987a5 --- /dev/null +++ b/www/src/components/AutoDict.vue @@ -0,0 +1,37 @@ + + + + + \ No newline at end of file diff --git a/www/src/views/system/dictonary/AddDict.vue b/www/src/views/system/dictonary/AddDict.vue new file mode 100644 index 0000000..bc3d3a9 --- /dev/null +++ b/www/src/views/system/dictonary/AddDict.vue @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/www/src/views/system/dictonary/AddItem.vue b/www/src/views/system/dictonary/AddItem.vue new file mode 100644 index 0000000..a39c715 --- /dev/null +++ b/www/src/views/system/dictonary/AddItem.vue @@ -0,0 +1,58 @@ + + + + + \ No newline at end of file diff --git a/www/src/views/system/dictonary/DictItem.vue b/www/src/views/system/dictonary/DictItem.vue new file mode 100644 index 0000000..7fd4a5d --- /dev/null +++ b/www/src/views/system/dictonary/DictItem.vue @@ -0,0 +1,152 @@ + + + + + \ No newline at end of file diff --git a/www/src/views/system/dictonary/index.vue b/www/src/views/system/dictonary/index.vue new file mode 100644 index 0000000..2ced786 --- /dev/null +++ b/www/src/views/system/dictonary/index.vue @@ -0,0 +1,153 @@ + + + + + \ No newline at end of file diff --git a/www/src/views/system/menus/ButtonForm.vue b/www/src/views/system/menus/ButtonForm.vue index 010527b..90e18a4 100644 --- a/www/src/views/system/menus/ButtonForm.vue +++ b/www/src/views/system/menus/ButtonForm.vue @@ -12,16 +12,14 @@ - - 禁用 - 启用 - +