From fb592d4f1b03aa9d9647ef0dde65fc7dbc84d2f0 Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Sun, 21 Dec 2025 23:14:02 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E3=80=81=E5=AE=A2=E6=88=B7=E5=92=8C=E4=BB=A4=E7=89=8C?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 重构用户、角色、部门等模块的字段长度和校验逻辑 fix: 修复前端接口类型定义和字段显示问题 style: 优化代码格式和注释 perf: 提升数据库连接性能和查询效率 docs: 更新README和配置文档 test: 添加字段校验和数据库连接测试 chore: 更新依赖包和配置文件 --- README.en.md | 2 +- README.md | 2 +- .../alembic/versions/8767328f6bc3_迁移脚本.py | 37 + .../api/v1/module_application/ai/schema.py | 1 - .../api/v1/module_application/job/schema.py | 12 +- .../api/v1/module_application/myapp/schema.py | 1 - .../api/v1/module_common/health/controller.py | 2 +- .../app/api/v1/module_gencode/demo/schema.py | 22 +- .../gencode/templates/ts/api.ts.j2 | 4 +- .../api/v1/module_system/customer/__init__.py | 2 + .../v1/module_system/customer/controller.py | 215 +++++ .../app/api/v1/module_system/customer/crud.py | 125 +++ .../api/v1/module_system/customer/model.py | 38 + .../api/v1/module_system/customer/schema.py | 82 ++ .../api/v1/module_system/customer/service.py | 312 +++++++ .../app/api/v1/module_system/dept/model.py | 4 +- .../app/api/v1/module_system/dept/schema.py | 6 +- .../app/api/v1/module_system/dict/model.py | 2 +- .../app/api/v1/module_system/dict/schema.py | 2 +- .../app/api/v1/module_system/log/schema.py | 28 +- .../app/api/v1/module_system/menu/schema.py | 27 +- .../app/api/v1/module_system/notice/model.py | 4 +- .../app/api/v1/module_system/notice/schema.py | 27 +- .../app/api/v1/module_system/params/model.py | 2 +- .../app/api/v1/module_system/params/schema.py | 15 +- .../api/v1/module_system/position/model.py | 2 +- .../api/v1/module_system/position/schema.py | 22 +- .../app/api/v1/module_system/role/model.py | 6 +- .../app/api/v1/module_system/role/schema.py | 30 +- .../api/v1/module_system/tenant/__init__.py | 2 + .../api/v1/module_system/tenant/controller.py | 215 +++++ .../app/api/v1/module_system/tenant/crud.py | 125 +++ .../app/api/v1/module_system/tenant/model.py | 33 + .../app/api/v1/module_system/tenant/schema.py | 78 ++ .../api/v1/module_system/tenant/service.py | 479 ++++++++++ .../api/v1/module_system/token/__init__.py | 2 + .../api/v1/module_system/token/controller.py | 148 +++ .../app/api/v1/module_system/token/crud.py | 124 +++ .../app/api/v1/module_system/token/model.py | 17 + .../app/api/v1/module_system/token/schema.py | 82 ++ .../app/api/v1/module_system/token/service.py | 312 +++++++ .../app/api/v1/module_system/user/model.py | 2 +- .../app/api/v1/module_system/user/schema.py | 39 +- .../app/api/v1/module_system/user/service.py | 9 +- backend/app/config/setting.py | 14 +- backend/app/core/base_crud.py | 40 +- backend/app/core/base_model.py | 61 ++ backend/app/core/base_params.py | 49 +- backend/app/core/base_schema.py | 29 +- backend/app/core/database.py | 34 +- backend/app/core/validator.py | 21 + backend/env/.env.dev | 2 +- frontend/.env.development | 2 +- frontend/package.json | 2 + frontend/src/api/module_application/job.ts | 4 +- frontend/src/api/module_application/myapp.ts | 4 +- frontend/src/api/module_gencode/demo.ts | 4 +- frontend/src/api/module_generator/gencode.ts | 8 +- frontend/src/api/module_system/customer.ts | 101 ++ frontend/src/api/module_system/log.ts | 4 +- frontend/src/api/module_system/notice.ts | 4 +- frontend/src/api/module_system/position.ts | 4 +- frontend/src/api/module_system/tenant.ts | 102 ++ frontend/src/api/module_system/token.ts | 101 ++ frontend/src/api/module_system/user.ts | 4 +- .../src/components/Notification/index.vue | 2 +- frontend/src/types/global.d.ts | 18 - .../module_application/workflow/index.vue | 873 +++++++++++++++++- .../views/module_system/customer/index.vue | 745 +++++++++++++++ .../src/views/module_system/tenant/index.vue | 757 +++++++++++++++ .../src/views/module_system/token/index.vue | 793 ++++++++++++++++ 71 files changed, 6274 insertions(+), 209 deletions(-) create mode 100644 backend/app/alembic/versions/8767328f6bc3_迁移脚本.py create mode 100644 backend/app/api/v1/module_system/customer/__init__.py create mode 100644 backend/app/api/v1/module_system/customer/controller.py create mode 100644 backend/app/api/v1/module_system/customer/crud.py create mode 100644 backend/app/api/v1/module_system/customer/model.py create mode 100644 backend/app/api/v1/module_system/customer/schema.py create mode 100644 backend/app/api/v1/module_system/customer/service.py create mode 100644 backend/app/api/v1/module_system/tenant/__init__.py create mode 100644 backend/app/api/v1/module_system/tenant/controller.py create mode 100644 backend/app/api/v1/module_system/tenant/crud.py create mode 100644 backend/app/api/v1/module_system/tenant/model.py create mode 100644 backend/app/api/v1/module_system/tenant/schema.py create mode 100644 backend/app/api/v1/module_system/tenant/service.py create mode 100644 backend/app/api/v1/module_system/token/__init__.py create mode 100644 backend/app/api/v1/module_system/token/controller.py create mode 100644 backend/app/api/v1/module_system/token/crud.py create mode 100644 backend/app/api/v1/module_system/token/model.py create mode 100644 backend/app/api/v1/module_system/token/schema.py create mode 100644 backend/app/api/v1/module_system/token/service.py create mode 100644 frontend/src/api/module_system/customer.ts create mode 100644 frontend/src/api/module_system/tenant.ts create mode 100644 frontend/src/api/module_system/token.ts create mode 100644 frontend/src/views/module_system/customer/index.vue create mode 100644 frontend/src/views/module_system/tenant/index.vue create mode 100644 frontend/src/views/module_system/token/index.vue diff --git a/README.en.md b/README.en.md index b200b1b8..9672091a 100644 --- a/README.en.md +++ b/README.en.md @@ -110,7 +110,7 @@ FastapiAdmin | Type | Technology Stack | Version | |------|------------------|---------| -| Backend | Python | ≥ 3.10 | +| Backend | Python | 3.12 ≥ 3.10 | | Backend | FastAPI | 0.109+ | | Frontend | Node.js | ≥ 20.0 | | Frontend | Vue3 | 3.3+ | diff --git a/README.md b/README.md index 73dfae62..ba7d931c 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ FastapiAdmin | 类型 | 技术栈 | 版本 | |------|--------|------| -| 后端 | Python | ≥ 3.10 | +| 后端 | Python | 3.12 ≥ 3.10 | | 后端 | FastAPI | 0.109+ | | 前端 | Node.js | ≥ 20.0 | | 前端 | Vue3 | 3.3+ | diff --git a/backend/app/alembic/versions/8767328f6bc3_迁移脚本.py b/backend/app/alembic/versions/8767328f6bc3_迁移脚本.py new file mode 100644 index 00000000..b7a01e6d --- /dev/null +++ b/backend/app/alembic/versions/8767328f6bc3_迁移脚本.py @@ -0,0 +1,37 @@ +"""迁移脚本 + +Revision ID: 8767328f6bc3 +Revises: +Create Date: 2025-12-21 19:47:11.533199 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8767328f6bc3' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_apscheduler_jobs_next_run_time', table_name='apscheduler_jobs') + op.drop_table('apscheduler_jobs') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('apscheduler_jobs', + sa.Column('id', sa.VARCHAR(length=191), nullable=False), + sa.Column('next_run_time', sa.FLOAT(), nullable=True), + sa.Column('job_state', sa.BLOB(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_apscheduler_jobs_next_run_time', 'apscheduler_jobs', ['next_run_time'], unique=False) + # ### end Alembic commands ### diff --git a/backend/app/api/v1/module_application/ai/schema.py b/backend/app/api/v1/module_application/ai/schema.py index 32304792..528df217 100644 --- a/backend/app/api/v1/module_application/ai/schema.py +++ b/backend/app/api/v1/module_application/ai/schema.py @@ -48,7 +48,6 @@ class McpQueryParam: created_id: int | None = Query(None, description="创建人"), updated_id: int | None = Query(None, description="更新人"), ) -> None: - # 模糊查询字段 self.name = ("like", name) if name else None diff --git a/backend/app/api/v1/module_application/job/schema.py b/backend/app/api/v1/module_application/job/schema.py index 024fb05b..c22ee5e2 100644 --- a/backend/app/api/v1/module_application/job/schema.py +++ b/backend/app/api/v1/module_application/job/schema.py @@ -126,12 +126,12 @@ class JobLogQueryParam: """定时任务查询参数""" def __init__( - self, - job_id: int | None = Query(None, description="定时任务ID"), - job_name: str | None = Query(None, description="任务名称"), - status: str | None = Query(None, description="状态: 正常,失败"), - created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + self, + job_id: int | None = Query(None, description="定时任务ID"), + job_name: str | None = Query(None, description="任务名称"), + status: str | None = Query(None, description="状态: 正常,失败"), + created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), ) -> None: # 定时任务ID查询 self.job_id = job_id diff --git a/backend/app/api/v1/module_application/myapp/schema.py b/backend/app/api/v1/module_application/myapp/schema.py index 389c7ce1..26f58bec 100644 --- a/backend/app/api/v1/module_application/myapp/schema.py +++ b/backend/app/api/v1/module_application/myapp/schema.py @@ -63,7 +63,6 @@ class ApplicationQueryParam: created_id: int | None = Query(None, description="创建人"), updated_id: int | None = Query(None, description="更新人"), ) -> None: - # 模糊查询字段 self.name = ("like", name) if name else None diff --git a/backend/app/api/v1/module_common/health/controller.py b/backend/app/api/v1/module_common/health/controller.py index acf1f6b4..b7dd796b 100644 --- a/backend/app/api/v1/module_common/health/controller.py +++ b/backend/app/api/v1/module_common/health/controller.py @@ -13,4 +13,4 @@ async def health_check() -> JSONResponse: 返回: - JSONResponse: 包含健康状态的JSON响应 """ - return JSONResponse(content={"msg": "Healthy"}, status_code=200) + return JSONResponse(content={"msg": True}, status_code=200) diff --git a/backend/app/api/v1/module_gencode/demo/schema.py b/backend/app/api/v1/module_gencode/demo/schema.py index 49297825..ae49c4fe 100644 --- a/backend/app/api/v1/module_gencode/demo/schema.py +++ b/backend/app/api/v1/module_gencode/demo/schema.py @@ -3,8 +3,9 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from fastapi import Query -from app.core.validator import DateTimeStr +from app.core.base_params import BaseQueryParam from app.core.base_schema import BaseSchema, UserBySchema +from app.core.validator import DateTimeStr class DemoCreateSchema(BaseModel): @@ -39,7 +40,6 @@ class DemoCreateSchema(BaseModel): # 描述校验:描述最大长度 if self.description and len(self.description) > 255: raise ValueError('描述长度不能超过255个字符') - return self @@ -55,27 +55,33 @@ class DemoOutSchema(DemoCreateSchema, BaseSchema, UserBySchema): class DemoQueryParam: """示例查询参数""" - def __init__( self, name: str | None = Query(None, description="名称"), + description: str | None = Query(None, description="描述"), status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), created_id: int | None = Query(None, description="创建人"), - updated_id: int | None = Query(None, description="更新人"), + updated_id: int | None = Query(None, description="更新人") ) -> None: - # 模糊查询字段 self.name = ("like", name) + if description: + self.description = ("like", description) # 精确查询字段 - self.created_id = created_id - self.updated_id = updated_id - self.status = status + if status: + self.status = ("eq", status) # 时间范围查询 if created_time and len(created_time) == 2: self.created_time = ("between", (created_time[0], created_time[1])) if updated_time and len(updated_time) == 2: self.updated_time = ("between", (updated_time[0], updated_time[1])) + + # 关联查询字段 + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) diff --git a/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 b/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 index 57b28ce3..6fa51524 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 @@ -119,8 +119,8 @@ export interface {{ class_name }}Table extends BaseType{ }}; {% endif %} {% endfor %} - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } // 新增/修改/详情表单参数 diff --git a/backend/app/api/v1/module_system/customer/__init__.py b/backend/app/api/v1/module_system/customer/__init__.py new file mode 100644 index 00000000..633f8661 --- /dev/null +++ b/backend/app/api/v1/module_system/customer/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- + diff --git a/backend/app/api/v1/module_system/customer/controller.py b/backend/app/api/v1/module_system/customer/controller.py new file mode 100644 index 00000000..a74813f4 --- /dev/null +++ b/backend/app/api/v1/module_system/customer/controller.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- + +from fastapi import APIRouter, Body, Depends, Path, UploadFile +from fastapi.responses import JSONResponse, StreamingResponse +import urllib.parse + +from app.common.response import StreamResponse, SuccessResponse +from app.utils.common_util import bytes2file_response +from app.core.base_params import PaginationQueryParam +from app.core.dependencies import AuthPermission +from app.core.router_class import OperationLogRoute +from app.core.base_schema import BatchSetAvailable +from app.core.logger import log + +from app.api.v1.module_system.auth.schema import AuthSchema +from .service import CustomerService +from .schema import ( + CustomerCreateSchema, + CustomerUpdateSchema, + CustomerQueryParam +) + + +CustomerRouter = APIRouter(route_class=OperationLogRoute, prefix="/customer", tags=["客户模块"]) + +@CustomerRouter.get("/detail/{id}", summary="获取客户详情", description="获取客户详情") +async def get_obj_detail_controller( + id: int = Path(..., description="客户ID"), + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:query"])) +) -> JSONResponse: + """ + 获取客户详情 + + 参数: + - id (int): 客户ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含客户详情的JSON响应 + """ + result_dict = await CustomerService.detail_service(id=id, auth=auth) + log.info(f"获取客户详情成功 {id}") + return SuccessResponse(data=result_dict, msg="获取客户详情成功") + +@CustomerRouter.get("/list", summary="查询客户列表", description="查询客户列表") +async def get_obj_list_controller( + page: PaginationQueryParam = Depends(), + search: CustomerQueryParam = Depends(), + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:query"])) +) -> JSONResponse: + """ + 查询客户列表 + + 参数: + - page (PaginationQueryParam): 分页查询参数 + - search (CustomerQueryParam): 查询参数 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含客户列表分页信息的JSON响应 + """ + # 使用数据库分页而不是应用层分页 + result_dict = await CustomerService.page_service( + auth=auth, + page_no=page.page_no if page.page_no is not None else 1, + page_size=page.page_size if page.page_size is not None else 10, + search=search, + order_by=page.order_by + ) + log.info("查询客户列表成功") + return SuccessResponse(data=result_dict, msg="查询客户列表成功") + +@CustomerRouter.post("/create", summary="创建客户", description="创建客户") +async def create_obj_controller( + data: CustomerCreateSchema, + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:create"])) +) -> JSONResponse: + """ + 创建客户 + + 参数: + - data (CustomerCreateSchema): 客户创建模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含创建客户详情的JSON响应 + """ + result_dict = await CustomerService.create_service(auth=auth, data=data) + log.info(f"创建客户成功: {result_dict.get('name')}") + return SuccessResponse(data=result_dict, msg="创建客户成功") + +@CustomerRouter.put("/update/{id}", summary="修改客户", description="修改客户") +async def update_obj_controller( + data: CustomerUpdateSchema, + id: int = Path(..., description="客户ID"), + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:update"])) +) -> JSONResponse: + """ + 修改客户 + + 参数: + - data (CustomerUpdateSchema): 客户更新模型 + - id (int): 客户ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含修改客户详情的JSON响应 + """ + result_dict = await CustomerService.update_service(auth=auth, id=id, data=data) + log.info(f"修改客户成功: {result_dict.get('name')}") + return SuccessResponse(data=result_dict, msg="修改客户成功") + +@CustomerRouter.delete("/delete", summary="删除客户", description="删除客户") +async def delete_obj_controller( + ids: list[int] = Body(..., description="ID列表"), + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:delete"])) +) -> JSONResponse: + """ + 删除客户 + + 参数: + - ids (list[int]): 客户ID列表 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含删除客户详情的JSON响应 + """ + await CustomerService.delete_service(auth=auth, ids=ids) + log.info(f"删除客户成功: {ids}") + return SuccessResponse(msg="删除客户成功") + +@CustomerRouter.patch("/available/setting", summary="批量修改客户状态", description="批量修改客户状态") +async def batch_set_available_obj_controller( + data: BatchSetAvailable, + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:patch"])) +) -> JSONResponse: + """ + 批量修改客户状态 + + 参数: + - data (BatchSetAvailable): 批量修改客户状态模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含批量修改客户状态详情的JSON响应 + """ + await CustomerService.set_available_service(auth=auth, data=data) + log.info(f"批量修改客户状态成功: {data.ids}") + return SuccessResponse(msg="批量修改客户状态成功") + +@CustomerRouter.post('/export', summary="导出客户", description="导出客户") +async def export_obj_list_controller( + search: CustomerQueryParam = Depends(), + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:export"])) +) -> StreamingResponse: + """ + 导出客户 + + 参数: + - search (CustomerQueryParam): 查询参数 + - auth (AuthSchema): 认证信息模型 + + 返回: + - StreamingResponse: 包含客户列表的Excel文件流响应 + """ + result_dict_list = await CustomerService.list_service(search=search, auth=auth) + export_result = await CustomerService.batch_export_service(obj_list=result_dict_list) + log.info('导出客户成功') + + return StreamResponse( + data=bytes2file_response(export_result), + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + headers={ + 'Content-Disposition': 'attachment; filename=example.xlsx' + } + ) + +@CustomerRouter.post('/import', summary="导入客户", description="导入客户") +async def import_obj_list_controller( + file: UploadFile, + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:import"])) +) -> JSONResponse: + """ + 导入租户 + + 参数: + - file (UploadFile): 导入的Excel文件 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含导入客户详情的JSON响应 + """ + batch_import_result = await CustomerService.batch_import_service(file=file, auth=auth, update_support=True) + log.info(f"导入客户成功: {batch_import_result}") + return SuccessResponse(data=batch_import_result, msg="导入租户成功") + +@CustomerRouter.post('/download/template', summary="获取客户导入模板", description="获取客户导入模板", dependencies=[Depends(AuthPermission(["module_system:customer:download"]))]) +async def export_obj_template_controller() -> StreamingResponse: + """ + 获取租户导入模板 + + 返回: + - StreamingResponse: 包含租户导入模板的Excel文件流响应 + """ + example_import_template_result = await CustomerService.import_template_download_service() + log.info('获取客户导入模板成功') + + return StreamResponse( + data=bytes2file_response(example_import_template_result), + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + headers={ + 'Content-Disposition': f'attachment; filename={urllib.parse.quote("客户导入模板.xlsx")}', + 'Access-Control-Expose-Headers': 'Content-Disposition' + } + ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/customer/crud.py b/backend/app/api/v1/module_system/customer/crud.py new file mode 100644 index 00000000..c32112c7 --- /dev/null +++ b/backend/app/api/v1/module_system/customer/crud.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +from typing import Dict, List, Optional, Sequence, Union, Any + +from app.core.base_crud import CRUDBase + +from app.api.v1.module_system.auth.schema import AuthSchema +from .model import CustomerModel +from .schema import CustomerCreateSchema, CustomerUpdateSchema, CustomerOutSchema + + +class CustomerCRUD(CRUDBase[CustomerModel, CustomerCreateSchema, CustomerUpdateSchema]): + """客户数据层""" + + def __init__(self, auth: AuthSchema) -> None: + """ + 初始化CRUD数据层 + + 参数: + - auth (AuthSchema): 认证信息模型 + """ + super().__init__(model=CustomerModel, auth=auth) + + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[CustomerModel]: + """ + 详情 + + 参数: + - id (int): 客户ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Optional[CustomerModel]: 客户模型实例或None + """ + return await self.get(id=id, preload=preload) + + async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[CustomerModel]: + """ + 列表查询 + + 参数: + - search (Optional[Dict]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Sequence[TenantModel]: 租户模型实例序列 + """ + return await self.list(search=search, order_by=order_by, preload=preload) + + async def create_crud(self, data: CustomerCreateSchema) -> Optional[CustomerModel]: + """ + 创建 + + 参数: + - data (CustomerCreateSchema): 客户创建模型 + + 返回: + - Optional[CustomerModel]: 客户模型实例或None + """ + return await self.create(data=data) + + async def update_crud(self, id: int, data: CustomerUpdateSchema) -> Optional[CustomerModel]: + """ + 更新 + + 参数: + - id (int): 客户ID + - data (CustomerUpdateSchema): 客户更新模型 + + 返回: + - Optional[CustomerModel]: 客户模型实例或None + """ + return await self.update(id=id, data=data) + + async def delete_crud(self, ids: List[int]) -> None: + """ + 批量删除 + + 参数: + - ids (List[int]): 客户ID列表 + + 返回: + - None + """ + return await self.delete(ids=ids) + + async def set_available_crud(self, ids: List[int], status: str) -> None: + """ + 批量设置可用状态 + + 参数: + - ids (List[int]): 客户ID列表 + - status (bool): 可用状态 + + 返回: + - None + """ + return await self.set(ids=ids, status=status) + + async def page_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict: + """ + 分页查询 + + 参数: + - offset (int): 偏移量 + - limit (int): 每页数量 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - search (Optional[Dict]): 查询参数 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Dict: 分页数据 + """ + order_by_list = order_by or [{'id': 'asc'}] + search_dict = search or {} + + return await self.page( + offset=offset, + limit=limit, + order_by=order_by_list, + search=search_dict, + out_schema=CustomerOutSchema, + preload=preload + ) diff --git a/backend/app/api/v1/module_system/customer/model.py b/backend/app/api/v1/module_system/customer/model.py new file mode 100644 index 00000000..8b9d8ed4 --- /dev/null +++ b/backend/app/api/v1/module_system/customer/model.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from typing import TYPE_CHECKING +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column, relationship, validates + +from app.core.base_model import ModelMixin, UserMixin, TenantMixin +if TYPE_CHECKING: + from app.api.v1.module_system.user.model import UserModel + + +class CustomerModel(ModelMixin, UserMixin): + """ + 客户表 + """ + __tablename__: str = 'sys_customer' + __table_args__: dict[str, str] = ({'comment': '客户表'}) + __loader_options__: list[str] = ["created_by", "updated_by"] + + name: Mapped[str] = mapped_column(String(64), nullable=False, comment='客户名称') + code: Mapped[str] = mapped_column(String(20), nullable=False, index=True, comment='客户编码') + + + @validates('name') + def validate_name(self, key: str, name: str) -> str: + """验证名称不为空""" + if not name or not name.strip(): + raise ValueError('名称不能为空') + return name + + @validates('code') + def validate_code(self, key: str, code: str) -> str: + """验证编码格式校验""" + if not code or not code.strip(): + raise ValueError('编码不能为空') + if not code.isalnum(): + raise ValueError('编码只能包含字母和数字') + return code diff --git a/backend/app/api/v1/module_system/customer/schema.py b/backend/app/api/v1/module_system/customer/schema.py new file mode 100644 index 00000000..915ca09b --- /dev/null +++ b/backend/app/api/v1/module_system/customer/schema.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +from typing import Optional +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from fastapi import Query + +from app.core.base_schema import BaseSchema, UserBySchema, TenantSchema, CustomerSchema +from app.core.validator import DateTimeStr + + +class CustomerCreateSchema(BaseModel): + """新增模型""" + name: str = Field(..., description='客户名称') + code: Optional[str] = Field(default=None, description='客户编码') + status: str = Field(default="0", description="是否启用(0:启用 1:禁用)") + description: Optional[str] = Field(default=None, description="描述") + + @field_validator('name') + @classmethod + def _validate_name(cls, v: str) -> str: + v = v.strip() + if not v: + raise ValueError('名称不能为空') + return v + + @model_validator(mode='after') + def _after_validation(self): + """ + 核心业务规则校验 + """ + # 长度校验:名称最小长度 + if len(self.name) < 2 or len(self.name) > 64: + raise ValueError('名称长度必须在2-50个字符之间') + # 格式校验:名称只能包含字母、数字、下划线和中划线 + if not self.name.isalnum() and not all(c in '-_' for c in self.name): + raise ValueError('名称只能包含字母、数字、下划线和中划线') + return self + + +class CustomerUpdateSchema(CustomerCreateSchema): + """更新模型""" + ... + + +class CustomerOutSchema(CustomerCreateSchema, BaseSchema, UserBySchema): + """响应模型""" + model_config = ConfigDict(from_attributes=True) + + +class CustomerQueryParam: + """客户查询参数""" + + def __init__( + self, + name: str | None = Query(None, description="名称"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), + created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + created_id: int | None = Query(None, description="创建人"), + updated_id: int | None = Query(None, description="更新人") + ) -> None: + # 模糊查询字段 + self.name = ("like", name) + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) + + # 时间范围查询 + if created_time and len(created_time) == 2: + self.created_time = ("between", (created_time[0], created_time[1])) + if updated_time and len(updated_time) == 2: + self.updated_time = ("between", (updated_time[0], updated_time[1])) + + # 关联查询字段 + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) diff --git a/backend/app/api/v1/module_system/customer/service.py b/backend/app/api/v1/module_system/customer/service.py new file mode 100644 index 00000000..70f0372f --- /dev/null +++ b/backend/app/api/v1/module_system/customer/service.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- + +import io +from typing import Any, List, Dict, Optional +from fastapi import UploadFile +import pandas as pd + +from app.api.v1.module_system.tenant.crud import TenantCRUD +from app.core.base_schema import BatchSetAvailable +from app.core.exceptions import CustomException +from app.utils.excel_util import ExcelUtil +from app.core.logger import log + +from app.api.v1.module_system.auth.schema import AuthSchema +from .schema import CustomerCreateSchema, CustomerUpdateSchema, CustomerOutSchema, CustomerQueryParam +from .crud import CustomerCRUD + + +class CustomerService: + """ + 客户管理模块服务层 + """ + + @classmethod + async def detail_service(cls, auth: AuthSchema, id: int) -> Dict: + """ + 详情 + + 参数: + - auth (AuthSchema): 认证信息模型 + - id (int): 客户ID + + 返回: + - Dict: 客户模型实例字典 + """ + obj = await CustomerCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg="该数据不存在") + return CustomerOutSchema.model_validate(obj).model_dump() + + @classmethod + async def list_service(cls, auth: AuthSchema, search: Optional[CustomerQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]: + """ + 列表查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - search (Optional[CustomerQueryParam]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + + 返回: + - List[Dict]: 客户模型实例字典列表 + """ + search_dict = search.__dict__ if search else None + obj_list = await CustomerCRUD(auth).list_crud(search=search_dict, order_by=order_by) + return [CustomerOutSchema.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: Optional[CustomerQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict: + """ + 分页查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - page_no (int): 页码 + - page_size (int): 每页数量 + - search (Optional[CustomerQueryParam]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + + 返回: + - Dict: 分页数据 + """ + search_dict = search.__dict__ if search else {} + order_by_list = order_by or [{'id': 'asc'}] + offset = (page_no - 1) * page_size + + result = await CustomerCRUD(auth).page_crud( + offset=offset, + limit=page_size, + order_by=order_by_list, + search=search_dict + ) + return result + + @classmethod + async def create_service(cls, auth: AuthSchema, data: CustomerCreateSchema) -> Dict: + """ + 创建 + + 参数: + - auth (AuthSchema): 认证信息模型 + - data (CustomerCreateSchema): 客户创建模型 + + 返回: + - Dict: 客户模型实例字典 + """ + obj = await CustomerCRUD(auth).get(name=data.name) + if obj: + raise CustomException(msg='创建失败,名称已存在') + obj = await CustomerCRUD(auth).get(code=data.code) + if obj: + raise CustomException(msg='创建失败,编码已存在') + obj = await CustomerCRUD(auth).create_crud(data=data) + return CustomerOutSchema.model_validate(obj).model_dump() + + @classmethod + async def update_service(cls, auth: AuthSchema, id: int, data: CustomerUpdateSchema) -> Dict: + """ + 更新 + + 参数: + - auth (AuthSchema): 认证信息模型 + - id (int): 客户ID + - data (CustomerUpdateSchema): 客户更新模型 + + 返回: + - Dict: 客户模型实例字典 + """ + # 检查数据是否存在 + obj = await CustomerCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg='更新失败,该数据不存在') + + # 检查名称是否重复 + exist_obj = await CustomerCRUD(auth).get(name=data.name) + if exist_obj and exist_obj.id != id: + raise CustomException(msg='更新失败,名称重复') + + obj = await CustomerCRUD(auth).update_crud(id=id, data=data) + return CustomerOutSchema.model_validate(obj).model_dump() + + @classmethod + async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None: + """ + 删除 + + 参数: + - auth (AuthSchema): 认证信息模型 + - ids (List[int]): 客户ID列表 + + 返回: + - None + """ + if len(ids) < 1: + raise CustomException(msg='删除失败,删除对象不能为空') + + # 检查所有要删除的数据是否存在 + for id in ids: + obj = await CustomerCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') + + await CustomerCRUD(auth).delete_crud(ids=ids) + + @classmethod + async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + """ + 批量设置状态 + + 参数: + - auth (AuthSchema): 认证信息模型 + - data (BatchSetAvailable): 批量设置状态模型 + + 返回: + - None + """ + await CustomerCRUD(auth).set_available_crud(ids=data.ids, status=data.status) + + @classmethod + async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes: + """ + 批量导出 + + 参数: + - obj_list (List[Dict[str, Any]]): 客户模型实例字典列表 + + 返回: + - bytes: Excel文件字节流 + """ + mapping_dict = { + 'id': '编号', + 'name': '名称', + 'code': '编码', + 'status': '状态', + 'description': '备注', + 'created_time': '创建时间', + 'updated_time': '更新时间', + 'creator': '创建者', + 'code': '编码', + } + + # 复制数据并转换状态 + data = obj_list.copy() + for item in data: + # 处理状态 + item['status'] = '启用' if item.get('status') == '0' else '停用' + # 处理创建者 + creator_info = item.get('creator') + if isinstance(creator_info, dict): + item['creator'] = creator_info.get('name', '未知') + else: + item['creator'] = '未知' + + 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: + """ + 批量导入 + + 参数: + - auth (AuthSchema): 认证信息模型 + - file (UploadFile): 上传的Excel文件 + - update_support (bool): 是否支持更新存在数据 + + 返回: + - str: 导入结果信息 + """ + + header_dict = { + '名称': 'name', + '状态': 'status', + '描述': 'description' + } + + try: + # 读取Excel文件 + contents = await file.read() + df = pd.read_excel(io.BytesIO(contents)) + await file.close() + + if df.empty: + raise CustomException(msg="导入文件为空") + + # 检查表头是否完整 + missing_headers = [header for header in header_dict.keys() if header not in df.columns] + if missing_headers: + raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") + + # 重命名列名 + df.rename(columns=header_dict, inplace=True) + + # 验证必填字段 + required_fields = ['name', 'status'] + for field in required_fields: + missing_rows = df[df[field].isnull()].index.tolist() + raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}行") + + error_msgs = [] + success_count = 0 + count = 0 + + # 处理每一行数据 + for index, row in df.iterrows(): + count += 1 + try: + # 数据转换前的类型检查 + try: + status = True if row['status'] == '正常' else False + except ValueError: + error_msgs.append(f"第{count}行: 状态必须是'正常'或'停用'") + continue + + # 构建用户数据 + data = { + "name": str(row['name']), + "status": status, + "description": str(row['description']), + } + + # 处理用户导入 + exists_obj = await TenantCRUD(auth).get(name=data["name"]) + if exists_obj: + if update_support: + await TenantCRUD(auth).update(id=exists_obj.id, data=data) + success_count += 1 + else: + error_msgs.append(f"第{count}行: 对象 {data['name']} 已存在") + else: + await TenantCRUD(auth).create(data=data) + success_count += 1 + + except Exception as e: + error_msgs.append(f"第{count}行: {str(e)}") + continue + + # 返回详细的导入结果 + result = f"成功导入 {success_count} 条数据" + if error_msgs: + result += "\n错误信息:\n" + "\n".join(error_msgs) + return result + + except Exception as e: + log.error(f"批量导入用户失败: {str(e)}") + raise CustomException(msg=f"导入失败: {str(e)}") + + @classmethod + async def import_template_download_service(cls) -> bytes: + """ + 下载导入模板 + + 返回: + - bytes: Excel文件字节流 + """ + header_list = ['名称', '状态', '描述'] + selector_header_list = ['状态'] + option_list = [{'状态': ['正常', '停用']}] + return ExcelUtil.get_excel_template( + header_list=header_list, + selector_header_list=selector_header_list, + option_list=option_list + ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/dept/model.py b/backend/app/api/v1/module_system/dept/model.py index 34db9048..7a4717a5 100644 --- a/backend/app/api/v1/module_system/dept/model.py +++ b/backend/app/api/v1/module_system/dept/model.py @@ -18,9 +18,9 @@ class DeptModel(ModelMixin): __tablename__: str = "sys_dept" __table_args__: dict[str, str] = ({'comment': '部门表'}) - name: Mapped[str] = mapped_column(String(40), nullable=False, comment="部门名称") + name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称") order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序") - code: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="部门编码") + code: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True, comment="部门编码") leader: Mapped[str | None] = mapped_column(String(32), default=None, comment='部门负责人') phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机') email: Mapped[str | None] = mapped_column(String(64), default=None, comment='邮箱') diff --git a/backend/app/api/v1/module_system/dept/schema.py b/backend/app/api/v1/module_system/dept/schema.py index 4611a7df..11265067 100644 --- a/backend/app/api/v1/module_system/dept/schema.py +++ b/backend/app/api/v1/module_system/dept/schema.py @@ -9,10 +9,10 @@ from app.core.base_schema import BaseSchema class DeptCreateSchema(BaseModel): """部门创建模型""" - name: str = Field(..., max_length=40, description="部门名称") + name: str = Field(..., max_length=64, description="部门名称") order: int = Field(default=1, ge=0, description="显示顺序") - code: str | None = Field(default=None, max_length=60, description="部门编码") - leader: str | None = Field(default=None, max_length=20, description="部门负责人") + code: str | None = Field(default=None, max_length=16, description="部门编码") + leader: str | None = Field(default=None, max_length=32, description="部门负责人") phone: str | None = Field(default=None, max_length=11, description="手机") email: str | None = Field(default=None, max_length=64, description="邮箱") parent_id: int | None = Field(default=None, ge=0, description="父部门ID") diff --git a/backend/app/api/v1/module_system/dict/model.py b/backend/app/api/v1/module_system/dict/model.py index 046684da..93a5a2bf 100644 --- a/backend/app/api/v1/module_system/dict/model.py +++ b/backend/app/api/v1/module_system/dict/model.py @@ -13,7 +13,7 @@ class DictTypeModel(ModelMixin): __tablename__: str = "sys_dict_type" __table_args__: dict[str, str] = ({'comment': '字典类型表'}) - dict_name: Mapped[str] = mapped_column(String(255), nullable=False, comment='字典名称') + dict_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='字典名称') dict_type: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, comment='字典类型') # 关系定义 diff --git a/backend/app/api/v1/module_system/dict/schema.py b/backend/app/api/v1/module_system/dict/schema.py index 466d310c..28082fe4 100644 --- a/backend/app/api/v1/module_system/dict/schema.py +++ b/backend/app/api/v1/module_system/dict/schema.py @@ -14,7 +14,7 @@ class DictTypeCreateSchema(BaseModel): """ dict_name: str = Field(..., min_length=1, max_length=64, description='字典名称') - dict_type: str = Field(..., min_length=1, max_length=100, description='字典类型') + dict_type: str = Field(..., min_length=1, max_length=64, description='字典类型') status: str = Field(default='0', description='状态(0正常 1停用)') description: str | None = Field(default=None, max_length=255, description="描述") diff --git a/backend/app/api/v1/module_system/log/schema.py b/backend/app/api/v1/module_system/log/schema.py index 9781dde8..835a4077 100644 --- a/backend/app/api/v1/module_system/log/schema.py +++ b/backend/app/api/v1/module_system/log/schema.py @@ -4,8 +4,8 @@ import re from pydantic import BaseModel, ConfigDict, Field, field_validator from fastapi import Query -from app.core.validator import DateTimeStr from app.core.base_schema import BaseSchema, UserBySchema +from app.core.validator import DateTimeStr class OperationLogCreateSchema(BaseModel): @@ -72,26 +72,36 @@ class OperationLogQueryParam: request_method: str | None = Query(None, description="请求方法"), request_ip: str | None = Query(None, description="请求IP"), response_code: int | None = Query(None, description="响应状态码"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), created_id: int | None = Query(None, description="创建人"), - updated_id: int | None = Query(None, description="更新人"), + updated_id: int | None = Query(None, description="更新人") ) -> None: - # 模糊查询字段 self.request_path = ("like", f"%{request_path}%") if request_path else None - # 精确查询字段 - self.created_id = created_id - self.updated_id = updated_id self.request_method = request_method self.request_ip = request_ip self.response_code = response_code self.type = type - - # 时间范围查询 - 增加对单个时间参数的处理 + # 模糊查询字段 + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) + + # 时间范围查询 if created_time and len(created_time) == 2: self.created_time = ("between", (created_time[0], created_time[1])) if updated_time and len(updated_time) == 2: self.updated_time = ("between", (updated_time[0], updated_time[1])) - \ No newline at end of file + + # 关联查询字段 + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) diff --git a/backend/app/api/v1/module_system/menu/schema.py b/backend/app/api/v1/module_system/menu/schema.py index b0bd6b49..da0cc1a7 100644 --- a/backend/app/api/v1/module_system/menu/schema.py +++ b/backend/app/api/v1/module_system/menu/schema.py @@ -4,8 +4,7 @@ from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator from fastapi import Query -from app.core.validator import DateTimeStr -from app.core.validator import menu_request_validator +from app.core.validator import DateTimeStr, menu_request_validator from app.core.base_schema import BaseSchema @@ -83,20 +82,36 @@ class MenuQueryParam: component_path: str | None = Query(None, description="组件路径"), type: Literal[1,2,3,4] | None = Query(None, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)"), permission: str | None = Query(None, description="权限标识"), - status: str | None = Query(None, description="菜单状态(0:启用 1:禁用)"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + created_id: int | None = Query(None, description="创建人"), + updated_id: int | None = Query(None, description="更新人") ) -> None: - # 模糊查询字段 self.name = ("like", name) self.route_path = ("like", route_path) self.component_path = ("like", component_path) self.permission = ("like", permission) - # 精确查询字段 self.type = type - self.status = status + # 模糊查询字段 + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) # 时间范围查询 if created_time and len(created_time) == 2: self.created_time = ("between", (created_time[0], created_time[1])) + if updated_time and len(updated_time) == 2: + self.updated_time = ("between", (updated_time[0], updated_time[1])) + + # 关联查询字段 + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) diff --git a/backend/app/api/v1/module_system/notice/model.py b/backend/app/api/v1/module_system/notice/model.py index 1b7c80ea..8241e144 100644 --- a/backend/app/api/v1/module_system/notice/model.py +++ b/backend/app/api/v1/module_system/notice/model.py @@ -14,6 +14,6 @@ class NoticeModel(ModelMixin, UserMixin): __table_args__: dict[str, str] = ({'comment': '通知公告表'}) __loader_options__: list[str] = ["created_by", "updated_by"] - notice_title: Mapped[str] = mapped_column(String(50), nullable=False, comment='公告标题') - notice_type: Mapped[str] = mapped_column(String(50), nullable=False, comment='公告类型(1通知 2公告)') + notice_title: Mapped[str] = mapped_column(String(64), nullable=False, comment='公告标题') + notice_type: Mapped[str] = mapped_column(String(1), nullable=False, comment='公告类型(1通知 2公告)') notice_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment='公告内容') diff --git a/backend/app/api/v1/module_system/notice/schema.py b/backend/app/api/v1/module_system/notice/schema.py index 24388919..4707e7f3 100644 --- a/backend/app/api/v1/module_system/notice/schema.py +++ b/backend/app/api/v1/module_system/notice/schema.py @@ -3,8 +3,8 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from fastapi import Query -from app.core.validator import DateTimeStr from app.core.base_schema import BaseSchema, UserBySchema +from app.core.validator import DateTimeStr class NoticeCreateSchema(BaseModel): @@ -48,25 +48,34 @@ class NoticeQueryParam: self, notice_title: str | None = Query(None, description="公告标题"), notice_type: str | None = Query(None, description="公告类型"), - status: str | None = Query(None, description="是否可用"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), created_id: int | None = Query(None, description="创建人"), - updated_id: int | None = Query(None, description="更新人"), + updated_id: int | None = Query(None, description="更新人") ) -> None: - # 模糊查询字段 self.notice_title = ("like", notice_title) + # 精确查询字段 + self.notice_type = notice_type + # 模糊查询字段 + if description: + self.description = ("like", description) # 精确查询字段 - self.created_id = created_id - self.updated_id = updated_id - self.status = status - self.notice_type = notice_type + if status: + self.status = ("eq", status) # 时间范围查询 if created_time and len(created_time) == 2: self.created_time = ("between", (created_time[0], created_time[1])) if updated_time and len(updated_time) == 2: self.updated_time = ("between", (updated_time[0], updated_time[1])) - + + # 关联查询字段 + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) + diff --git a/backend/app/api/v1/module_system/params/model.py b/backend/app/api/v1/module_system/params/model.py index fb79bfa7..b6ff6aff 100644 --- a/backend/app/api/v1/module_system/params/model.py +++ b/backend/app/api/v1/module_system/params/model.py @@ -13,7 +13,7 @@ class ParamsModel(ModelMixin): __tablename__: str = "sys_param" __table_args__: dict[str, str] = ({'comment': '系统参数表'}) - config_name: Mapped[str] = mapped_column(String(500), nullable=False, comment='参数名称') + config_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='参数名称') config_key: Mapped[str] = mapped_column(String(500), nullable=False, comment='参数键名') config_value: Mapped[str | None] = mapped_column(String(500), comment='参数键值') config_type: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True, comment="系统内置(True:是 False:否)") diff --git a/backend/app/api/v1/module_system/params/schema.py b/backend/app/api/v1/module_system/params/schema.py index fa2fa90e..e9f27fa6 100644 --- a/backend/app/api/v1/module_system/params/schema.py +++ b/backend/app/api/v1/module_system/params/schema.py @@ -3,8 +3,8 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator from fastapi import Query -from app.core.validator import DateTimeStr from app.core.base_schema import BaseSchema +from app.core.validator import DateTimeStr class ParamsCreateSchema(BaseModel): @@ -44,16 +44,23 @@ class ParamsQueryParam: config_name: str | None = Query(None, description="配置名称"), config_key: str | None = Query(None, description="配置键名"), config_type: bool | None = Query(None, description="系统内置((True:是 False:否))"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]) ) -> None: - + # 模糊查询字段 # 模糊查询字段 self.config_name = ("like", config_name) self.config_key = ("like", config_key) - # 精确查询字段 self.config_type = config_type + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) # 时间范围查询 if created_time and len(created_time) == 2: diff --git a/backend/app/api/v1/module_system/position/model.py b/backend/app/api/v1/module_system/position/model.py index c6f95e63..46a8362a 100644 --- a/backend/app/api/v1/module_system/position/model.py +++ b/backend/app/api/v1/module_system/position/model.py @@ -19,7 +19,7 @@ class PositionModel(ModelMixin, UserMixin): __table_args__: dict[str, str] = ({'comment': '岗位表'}) __loader_options__: list[str] = ["users", "created_by", "updated_by"] - name: Mapped[str] = mapped_column(String(40), nullable=False, comment="岗位名称") + name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位名称") order: Mapped[int] = mapped_column(Integer, nullable=False, default=1, comment="显示排序") # 关联关系 diff --git a/backend/app/api/v1/module_system/position/schema.py b/backend/app/api/v1/module_system/position/schema.py index cf1fab39..d51f7b12 100644 --- a/backend/app/api/v1/module_system/position/schema.py +++ b/backend/app/api/v1/module_system/position/schema.py @@ -41,24 +41,30 @@ class PositionQueryParam: def __init__( self, name: Optional[str] = Query(None, description="岗位名称"), - status: Optional[str] = Query(None, description="是否可用"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), created_id: int | None = Query(None, description="创建人"), - updated_id: int | None = Query(None, description="更新人"), + updated_id: int | None = Query(None, description="更新人") ) -> None: - # 模糊查询字段 self.name = ("like", name) + if description: + self.description = ("like", description) # 精确查询字段 - self.created_id = created_id - self.updated_id = updated_id - self.status = status - + if status: + self.status = ("eq", status) + # 时间范围查询 if created_time and len(created_time) == 2: self.created_time = ("between", (created_time[0], created_time[1])) if updated_time and len(updated_time) == 2: self.updated_time = ("between", (updated_time[0], updated_time[1])) - \ No newline at end of file + + # 关联查询字段 + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/role/model.py b/backend/app/api/v1/module_system/role/model.py index 6bbb8b71..309ded13 100644 --- a/backend/app/api/v1/module_system/role/model.py +++ b/backend/app/api/v1/module_system/role/model.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from sqlalchemy import String, Integer, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column -from app.core.base_model import MappedBase, ModelMixin, UserMixin +from app.core.base_model import MappedBase, ModelMixin if TYPE_CHECKING: from app.api.v1.module_system.menu.model import MenuModel @@ -67,8 +67,8 @@ class RoleModel(ModelMixin): __table_args__: dict[str, str] = ({'comment': '角色表'}) __loader_options__: list[str] = ["menus", "depts"] - name: Mapped[str] = mapped_column(String(40), nullable=False, comment="角色名称") - code: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="角色编码") + name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称") + code: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True, comment="角色编码") order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序") data_scope: Mapped[int] = mapped_column(Integer, default=1, nullable=False, comment="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)") diff --git a/backend/app/api/v1/module_system/role/schema.py b/backend/app/api/v1/module_system/role/schema.py index a93eab1e..51ba45d9 100644 --- a/backend/app/api/v1/module_system/role/schema.py +++ b/backend/app/api/v1/module_system/role/schema.py @@ -3,9 +3,8 @@ from fastapi import Query from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator -from app.core.validator import DateTimeStr from app.core.base_schema import BaseSchema -from app.core.validator import role_permission_request_validator +from app.core.validator import DateTimeStr, code_validator, role_permission_request_validator from ..dept.schema import DeptOutSchema from ..menu.schema import MenuOutSchema @@ -13,8 +12,8 @@ from ..menu.schema import MenuOutSchema class RoleCreateSchema(BaseModel): """角色创建模型""" - name: str = Field(..., max_length=40, description="角色名称") - code: str | None = Field(default=None, max_length=40, description="角色编码") + name: str = Field(..., max_length=64, description="角色名称") + code: str | None = Field(default=None, max_length=16, description="角色编码") order: int | None = Field(default=1, ge=1, description='显示排序') data_scope: int | None = Field(default=1, description='数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)') status: str = Field(default="0", description="是否启用") @@ -23,13 +22,7 @@ class RoleCreateSchema(BaseModel): @field_validator("code") @classmethod def validate_code(cls, value: str | None): - if value is None: - return value - import re - v = value.strip() - if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,39}$", v): - raise ValueError("角色编码需字母开头,允许字母/数字/下划线,长度2-40") - return v + return code_validator(value) class RolePermissionSettingSchema(BaseModel): @@ -64,21 +57,22 @@ class RoleQueryParam: def __init__( self, name: str | None = Query(None, description="角色名称"), - status: str | None = Query(None, description="是否可用"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]) ) -> None: - # 模糊查询字段 self.name = ("like", name) + if description: + self.description = ("like", description) # 精确查询字段 - self.status = status - + if status: + self.status = ("eq", status) + # 时间范围查询 if created_time and len(created_time) == 2: self.created_time = ("between", (created_time[0], created_time[1])) - if updated_time and len(updated_time) == 2: self.updated_time = ("between", (updated_time[0], updated_time[1])) - diff --git a/backend/app/api/v1/module_system/tenant/__init__.py b/backend/app/api/v1/module_system/tenant/__init__.py new file mode 100644 index 00000000..633f8661 --- /dev/null +++ b/backend/app/api/v1/module_system/tenant/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- + diff --git a/backend/app/api/v1/module_system/tenant/controller.py b/backend/app/api/v1/module_system/tenant/controller.py new file mode 100644 index 00000000..e7eccf23 --- /dev/null +++ b/backend/app/api/v1/module_system/tenant/controller.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- + +from fastapi import APIRouter, Body, Depends, Path, UploadFile +from fastapi.responses import JSONResponse, StreamingResponse +import urllib.parse + +from app.common.response import StreamResponse, SuccessResponse +from app.utils.common_util import bytes2file_response +from app.core.base_params import PaginationQueryParam +from app.core.dependencies import AuthPermission +from app.core.router_class import OperationLogRoute +from app.core.base_schema import BatchSetAvailable +from app.core.logger import log + +from app.api.v1.module_system.auth.schema import AuthSchema +from .service import TenantService +from .schema import ( + TenantCreateSchema, + TenantUpdateSchema, + TenantQueryParam +) + + +TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=["租户模块"]) + +@TenantRouter.get("/detail/{id}", summary="获取租户详情", description="获取租户详情") +async def get_obj_detail_controller( + id: int = Path(..., description="租户ID"), + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:query"])) +) -> JSONResponse: + """ + 获取租户详情 + + 参数: + - id (int): 租户ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含租户详情的JSON响应 + """ + result_dict = await TenantService.detail_service(id=id, auth=auth) + log.info(f"获取租户详情成功 {id}") + return SuccessResponse(data=result_dict, msg="获取租户详情成功") + +@TenantRouter.get("/list", summary="查询租户列表", description="查询租户列表") +async def get_obj_list_controller( + page: PaginationQueryParam = Depends(), + search: TenantQueryParam = Depends(), + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:query"])) +) -> JSONResponse: + """ + 查询租户列表 + + 参数: + - page (PaginationQueryParam): 分页查询参数 + - search (TenantQueryParam): 查询参数 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含租户列表分页信息的JSON响应 + """ + # 使用数据库分页而不是应用层分页 + result_dict = await TenantService.page_service( + auth=auth, + page_no=page.page_no if page.page_no is not None else 1, + page_size=page.page_size if page.page_size is not None else 10, + search=search, + order_by=page.order_by + ) + log.info("查询租户列表成功") + return SuccessResponse(data=result_dict, msg="查询租户列表成功") + +@TenantRouter.post("/create", summary="创建租户", description="创建租户") +async def create_obj_controller( + data: TenantCreateSchema, + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:create"])) +) -> JSONResponse: + """ + 创建租户 + + 参数: + - data (TenantCreateSchema): 租户创建模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含创建租户详情的JSON响应 + """ + result_dict = await TenantService.create_service(auth=auth, data=data) + log.info(f"创建租户成功: {result_dict.get('name')}") + return SuccessResponse(data=result_dict, msg="创建租户成功") + +@TenantRouter.put("/update/{id}", summary="修改租户", description="修改租户") +async def update_obj_controller( + data: TenantUpdateSchema, + id: int = Path(..., description="租户ID"), + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:update"])) +) -> JSONResponse: + """ + 修改租户 + + 参数: + - data (TenantUpdateSchema): 租户更新模型 + - id (int): 租户ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含修改租户详情的JSON响应 + """ + result_dict = await TenantService.update_service(auth=auth, id=id, data=data) + log.info(f"修改租户成功: {result_dict.get('name')}") + return SuccessResponse(data=result_dict, msg="修改租户成功") + +@TenantRouter.delete("/delete", summary="删除租户", description="删除租户") +async def delete_obj_controller( + ids: list[int] = Body(..., description="ID列表"), + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:delete"])) +) -> JSONResponse: + """ + 删除租户 + + 参数: + - ids (list[int]): 租户ID列表 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含删除租户详情的JSON响应 + """ + await TenantService.delete_service(auth=auth, ids=ids) + log.info(f"删除租户成功: {ids}") + return SuccessResponse(msg="删除租户成功") + +@TenantRouter.patch("/available/setting", summary="批量修改租户状态", description="批量修改租户状态") +async def batch_set_available_obj_controller( + data: BatchSetAvailable, + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:patch"])) +) -> JSONResponse: + """ + 批量修改租户状态 + + 参数: + - data (BatchSetAvailable): 批量修改租户状态模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含批量修改租户状态详情的JSON响应 + """ + await TenantService.set_available_service(auth=auth, data=data) + log.info(f"批量修改租户状态成功: {data.ids}") + return SuccessResponse(msg="批量修改租户状态成功") + +@TenantRouter.post('/export', summary="导出租户", description="导出租户") +async def export_obj_list_controller( + search: TenantQueryParam = Depends(), + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:export"])) +) -> StreamingResponse: + """ + 导出租户 + + 参数: + - search (TenantQueryParam): 查询参数 + - auth (AuthSchema): 认证信息模型 + + 返回: + - StreamingResponse: 包含租户列表的Excel文件流响应 + """ + result_dict_list = await TenantService.list_service(search=search, auth=auth) + export_result = await TenantService.batch_export_service(obj_list=result_dict_list) + log.info('导出租户成功') + + return StreamResponse( + data=bytes2file_response(export_result), + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + headers={ + 'Content-Disposition': 'attachment; filename=example.xlsx' + } + ) + +@TenantRouter.post('/import', summary="导入租户", description="导入租户") +async def import_obj_list_controller( + file: UploadFile, + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:import"])) +) -> JSONResponse: + """ + 导入租户 + + 参数: + - file (UploadFile): 导入的Excel文件 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含导入租户详情的JSON响应 + """ + batch_import_result = await TenantService.batch_import_service(file=file, auth=auth, update_support=True) + log.info(f"导入租户成功: {batch_import_result}") + return SuccessResponse(data=batch_import_result, msg="导入租户成功") + +@TenantRouter.post('/download/template', summary="获取租户导入模板", description="获取租户导入模板", dependencies=[Depends(AuthPermission(["module_system:tenant:download"]))]) +async def export_obj_template_controller() -> StreamingResponse: + """ + 获取租户导入模板 + + 返回: + - StreamingResponse: 包含租户导入模板的Excel文件流响应 + """ + example_import_template_result = await TenantService.import_template_download_service() + log.info('获取租户导入模板成功') + + return StreamResponse( + data=bytes2file_response(example_import_template_result), + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + headers={ + 'Content-Disposition': f'attachment; filename={urllib.parse.quote("租户导入模板.xlsx")}', + 'Access-Control-Expose-Headers': 'Content-Disposition' + } + ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/tenant/crud.py b/backend/app/api/v1/module_system/tenant/crud.py new file mode 100644 index 00000000..269104e0 --- /dev/null +++ b/backend/app/api/v1/module_system/tenant/crud.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +from typing import Dict, List, Optional, Sequence, Union, Any + +from app.core.base_crud import CRUDBase + +from app.api.v1.module_system.auth.schema import AuthSchema +from .model import TenantModel +from .schema import TenantCreateSchema, TenantUpdateSchema, TenantOutSchema + + +class TenantCRUD(CRUDBase[TenantModel, TenantCreateSchema, TenantUpdateSchema]): + """租户数据层""" + + def __init__(self, auth: AuthSchema) -> None: + """ + 初始化CRUD数据层 + + 参数: + - auth (AuthSchema): 认证信息模型 + """ + super().__init__(model=TenantModel, auth=auth) + + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[TenantModel]: + """ + 详情 + + 参数: + - id (int): 租户ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Optional[TenantModel]: 租户模型实例或None + """ + return await self.get(id=id, preload=preload) + + async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[TenantModel]: + """ + 列表查询 + + 参数: + - search (Optional[Dict]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Sequence[TenantModel]: 租户模型实例序列 + """ + return await self.list(search=search, order_by=order_by, preload=preload) + + async def create_crud(self, data: TenantCreateSchema) -> Optional[TenantModel]: + """ + 创建 + + 参数: + - data (TenantCreateSchema): 租户创建模型 + + 返回: + - Optional[TenantModel]: 租户模型实例或None + """ + return await self.create(data=data) + + async def update_crud(self, id: int, data: TenantUpdateSchema) -> Optional[TenantModel]: + """ + 更新 + + 参数: + - id (int): 租户ID + - data (TenantUpdateSchema): 租户更新模型 + + 返回: + - Optional[TenantModel]: 租户模型实例或None + """ + return await self.update(id=id, data=data) + + async def delete_crud(self, ids: List[int]) -> None: + """ + 批量删除 + + 参数: + - ids (List[int]): 租户ID列表 + + 返回: + - None + """ + return await self.delete(ids=ids) + + async def set_available_crud(self, ids: List[int], status: str) -> None: + """ + 批量设置可用状态 + + 参数: + - ids (List[int]): 租户ID列表 + - status (bool): 可用状态 + + 返回: + - None + """ + return await self.set(ids=ids, status=status) + + async def page_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict: + """ + 分页查询 + + 参数: + - offset (int): 偏移量 + - limit (int): 每页数量 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - search (Optional[Dict]): 查询参数 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Dict: 分页数据 + """ + order_by_list = order_by or [{'id': 'asc'}] + search_dict = search or {} + + return await self.page( + offset=offset, + limit=limit, + order_by=order_by_list, + search=search_dict, + out_schema=TenantOutSchema, + preload=preload + ) diff --git a/backend/app/api/v1/module_system/tenant/model.py b/backend/app/api/v1/module_system/tenant/model.py new file mode 100644 index 00000000..f2e9e106 --- /dev/null +++ b/backend/app/api/v1/module_system/tenant/model.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column, validates + +from app.core.base_model import ModelMixin + + +class TenantModel(ModelMixin): + """ + 租户模型 + """ + __tablename__: str = 'sys_tenant' + __table_args__: dict[str, str] = {'comment': '租户表'} + + name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, comment='租户名称') + code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True, comment='租户编码') + + @validates('name') + def validate_name(self, key: str, name: str) -> str: + """验证名称不为空""" + if not name or not name.strip(): + raise ValueError('名称不能为空') + return name + + @validates('code') + def validate_code(self, key: str, code: str) -> str: + """验证编码格式校验""" + if not code or not code.strip(): + raise ValueError('编码不能为空') + if not code.isalnum(): + raise ValueError('编码只能包含字母和数字') + return code diff --git a/backend/app/api/v1/module_system/tenant/schema.py b/backend/app/api/v1/module_system/tenant/schema.py new file mode 100644 index 00000000..2a95fda9 --- /dev/null +++ b/backend/app/api/v1/module_system/tenant/schema.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +from typing import Optional +from fastapi import Query +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from app.core.base_schema import BaseSchema +from app.core.validator import DateTimeStr + + +class TenantCreateSchema(BaseModel): + """新增模型""" + name: str = Field(..., description='租户名称') + code: Optional[str] = Field(default=None, description='租户编码') + status: str = Field(default="0", description="是否启用(0:启用 1:禁用)") + description: Optional[str] = Field(default=None, description="描述") + + @field_validator('name') + @classmethod + def _validate_name(cls, v: str) -> str: + v = v.strip() + if not v: + raise ValueError('名称不能为空') + return v + + @model_validator(mode='after') + def _after_validation(self): + """ + 核心业务规则校验 + """ + # 长度校验:名称最小长度 + if len(self.name) < 2 or len(self.name) > 64: + raise ValueError('名称长度必须在2-50个字符之间') + # 格式校验:名称只能包含字母、数字、下划线和中划线 + if not self.name.isalnum() and not all(c in '-_' for c in self.name): + raise ValueError('名称只能包含字母、数字、下划线和中划线') + + return self + + +class TenantUpdateSchema(TenantCreateSchema): + """更新模型""" + ... + +class TenantOutSchema(TenantCreateSchema, BaseSchema): + """响应模型""" + model_config = ConfigDict(from_attributes=True) + + +class TenantQueryParam: + """租户查询参数""" + + def __init__( + self, + name: str | None = Query(None, description="名称"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), + created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + created_id: int | None = Query(None, description="创建人"), + updated_id: int | None = Query(None, description="更新人") + ) -> None: + # 模糊查询字段 + self.name = ("like", name) + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) + + # 时间范围查询 + if created_time and len(created_time) == 2: + self.created_time = ("between", (created_time[0], created_time[1])) + if updated_time and len(updated_time) == 2: + self.updated_time = ("between", (updated_time[0], updated_time[1])) + + diff --git a/backend/app/api/v1/module_system/tenant/service.py b/backend/app/api/v1/module_system/tenant/service.py new file mode 100644 index 00000000..ffc19a8c --- /dev/null +++ b/backend/app/api/v1/module_system/tenant/service.py @@ -0,0 +1,479 @@ +# -*- coding: utf-8 -*- + +import io +import random +import string +from typing import Any, List, Dict, Optional +from fastapi import UploadFile +import pandas as pd + +from app.core.base_schema import BatchSetAvailable +from app.core.exceptions import CustomException +from app.utils.excel_util import ExcelUtil +from app.core.logger import log +from app.utils.hash_bcrpy_util import PwdUtil + +from app.api.v1.module_system.auth.schema import AuthSchema +from app.api.v1.module_system.user.crud import UserCRUD +from .schema import TenantCreateSchema, TenantUpdateSchema, TenantOutSchema, TenantQueryParam +from .crud import TenantCRUD + + +class TenantService: + """ + 租户管理模块服务层 + """ + + @classmethod + async def detail_service(cls, auth: AuthSchema, id: int) -> Dict: + """ + 详情 + + 参数: + - auth (AuthSchema): 认证信息模型 + - id (int): 租户ID + + 返回: + - Dict: 租户模型实例字典 + """ + obj = await TenantCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg="该数据不存在") + + # 获取租户详情基础数据 + result = TenantOutSchema.model_validate(obj).model_dump() + + return result + + @classmethod + async def list_service(cls, auth: AuthSchema, search: Optional[TenantQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]: + """ + 列表查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - search (Optional[TenantQueryParam]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + + 返回: + - List[Dict]: 租户模型实例字典列表 + """ + search_dict = search.__dict__ if search else None + obj_list = await TenantCRUD(auth).list_crud(search=search_dict, order_by=order_by) + return [TenantOutSchema.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: Optional[TenantQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict: + """ + 分页查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - page_no (int): 页码 + - page_size (int): 每页数量 + - search (Optional[TenantQueryParam]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + + 返回: + - Dict: 分页数据 + """ + search_dict = search.__dict__ if search else {} + order_by_list = order_by or [{'id': 'asc'}] + offset = (page_no - 1) * page_size + + result = await TenantCRUD(auth).page_crud( + offset=offset, + limit=page_size, + order_by=order_by_list, + search=search_dict + ) + return result + + @classmethod + async def create_service(cls, auth: AuthSchema, data: TenantCreateSchema) -> Dict: + """ + 创建 + + 参数: + - auth (AuthSchema): 认证信息模型 + - data (TenantCreateSchema): 租户创建模型 + + 返回: + - Dict: 租户模型实例字典 + """ + obj = await TenantCRUD(auth).get(name=data.name) + if obj: + raise CustomException(msg='创建失败,名称已存在') + obj = await TenantCRUD(auth).get(code=data.code) + if obj: + raise CustomException(msg='创建失败,编码已存在') + + # 创建租户 + tenant_obj = await TenantCRUD(auth).create_crud(data=data) + + # 自动创建租户初始管理员用户 + await cls._create_tenant_admin_user(auth, tenant_obj) + + return TenantOutSchema.model_validate(tenant_obj).model_dump() + + @classmethod + async def _create_tenant_admin_user(cls, auth: AuthSchema, tenant_obj) -> None: + """ + 为新创建的租户自动创建初始管理员用户 + + 参数: + - auth (AuthSchema): 认证信息模型 + - tenant_obj: 租户对象 + + 返回: + - None + """ + try: + # 生成初始管理员用户名(使用租户编码) + username = f"{tenant_obj.code}_admin" + + # 生成随机密码 + password_length = 12 + characters = string.ascii_letters + string.digits + "!@#$%^&*" + password = ''.join(random.choice(characters) for _ in range(password_length)) + + # 创建管理员用户数据 + admin_user_data = { + "username": username, + "password": PwdUtil.set_password_hash(password=password), + "name": f"{tenant_obj.name}管理员", + "tenant_id": tenant_obj.id, + "user_type": "1", # 租户管理员类型 + "status": True, + "created_id": auth.user.id if auth.user else None + } + + # 创建用户 + new_user = await UserCRUD(auth).create(data=admin_user_data) + + # 记录日志,包含临时密码信息(仅开发环境记录,生产环境应避免) + log.info(f"为租户[{tenant_obj.name}]创建初始管理员用户成功,用户名: {username},临时密码: {password}") + + except Exception as e: + log.error(f"为租户[{tenant_obj.name}]创建初始管理员用户失败: {str(e)}") + # 不中断租户创建流程,仅记录错误 + pass + + @classmethod + async def update_service(cls, auth: AuthSchema, id: int, data: TenantUpdateSchema) -> Dict: + """ + 更新 + + 参数: + - auth (AuthSchema): 认证信息模型 + - id (int): 租户ID + - data (TenantUpdateSchema): 租户更新模型 + + 返回: + - Dict: 租户模型实例字典 + """ + # 系统租户特殊处理 + if id == 1: + obj = await TenantCRUD(auth).update_crud(id=id, data=data) + log.info(f"系统租户配额设置已更新") + + return TenantOutSchema.model_validate(obj).model_dump() + + # 检查数据是否存在 + obj = await TenantCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg='更新失败,该数据不存在') + + # 检查名称是否重复 + exist_obj = await TenantCRUD(auth).get(name=data.name) + if exist_obj and exist_obj.id != id: + raise CustomException(msg='更新失败,名称重复') + + obj = await TenantCRUD(auth).update_crud(id=id, data=data) + return TenantOutSchema.model_validate(obj).model_dump() + + @classmethod + async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None: + """ + 删除 + + 参数: + - auth (AuthSchema): 认证信息模型 + - ids (List[int]): 租户ID列表 + + 返回: + - None + """ + if len(ids) < 1: + raise CustomException(msg='删除失败,删除对象不能为空') + + # 系统租户保护:不允许删除系统租户(id=1) + if 1 in ids: + raise CustomException(msg='系统租户不允许删除') + + # 检查所有要删除的数据是否存在 + for id in ids: + obj = await TenantCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') + + await TenantCRUD(auth).delete_crud(ids=ids) + + @classmethod + async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + """ + 批量设置状态 + + 参数: + - auth (AuthSchema): 认证信息模型 + - data (BatchSetAvailable): 批量设置状态模型 + + 返回: + - None + """ + # 系统租户保护:不允许禁用系统租户(id=1) + if data.status is False and 1 in data.ids: + raise CustomException(msg='系统租户不允许禁用') + + await TenantCRUD(auth).set_available_crud(ids=data.ids, status=data.status) + + @classmethod + async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes: + """ + 批量导出 + + 参数: + - obj_list (List[Dict[str, Any]]): 租户模型实例字典列表 + + 返回: + - bytes: Excel文件字节流 + """ + mapping_dict = { + 'id': '编号', + 'name': '名称', + 'code': '编码', + 'status': '状态', + 'description': '备注', + 'start_time': '开始时间', + 'end_time': '结束时间', + 'created_time': '创建时间', + 'updated_time': '更新时间', + 'created_id': '创建者', + } + + # 复制数据并转换状态 + data = obj_list.copy() + for item in data: + # 系统租户特殊标记 + if item.get('id') == 1: + item['name'] = f"{item.get('name')} [系统租户]" + + # 处理状态 + item['status'] = '启用' if item.get('status') == '0' else '停用' + + # 处理创建者 + creator_info = item.get('created_id') + if isinstance(creator_info, dict): + item['created_id'] = creator_info.get('name', '未知') + else: + item['created_id'] = '未知' + + # 限制导出数量,防止大数据量导出 + max_export_count = 1000 + if len(data) > max_export_count: + data = data[:max_export_count] + log.warning(f'导出数据超过{max_export_count}条限制,仅导出前{max_export_count}条') + + 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: + """ + 批量导入 + + 参数: + - auth (AuthSchema): 认证信息模型 + - file (UploadFile): 上传的Excel文件 + - update_support (bool): 是否支持更新存在数据 + + 返回: + - str: 导入结果信息 + """ + + header_dict = { + '名称': 'name', + '编码': 'code', + '状态': 'status', + '描述': 'description', + '开始时间': 'start_time', + '结束时间': 'end_time' + } + + try: + # 读取Excel文件 + contents = await file.read() + df = pd.read_excel(io.BytesIO(contents)) + await file.close() + + # 验证导入数量限制 + max_import_count = 100 + if len(df) > max_import_count: + raise CustomException(msg=f"单次导入不能超过{max_import_count}条数据") + + if df.empty: + raise CustomException(msg="导入文件为空") + + # 检查表头是否完整 + missing_headers = [header for header in header_dict.keys() if header not in df.columns] + if missing_headers: + raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") + + # 重命名列名 + df.rename(columns=header_dict, inplace=True) + + # 验证必填字段 + required_fields = ['name', 'code', 'status'] + for field in required_fields: + missing_rows = df[df[field].isnull()].index.tolist() + if missing_rows: + field_name = [k for k,v in header_dict.items() if v == field][0] + error_rows = [i+1 for i in missing_rows] + raise CustomException(msg=f"{field_name}不能为空,第{error_rows}行") + + error_msgs = [] + success_count = 0 + count = 0 + processed_names = set() # 用于检测重复名称 + processed_codes = set() # 用于检测重复编码 + + # 处理每一行数据 + for index, row in df.iterrows(): + count += 1 + try: + # 数据转换前的类型检查 + try: + status = True if str(row['status']).strip() == '正常' else False + except ValueError: + error_msgs.append(f"第{count}行: 状态必须是'正常'或'停用'") + continue + + # 字段格式验证 + name = str(row['name']).strip() + if len(name) < 2 or len(name) > 64: + error_msgs.append(f"第{count}行: 租户名称长度必须在2-64个字符之间") + continue + + # 检查名称是否只包含允许的字符 + if not all(c.isalnum() or c in '-_' for c in name.replace(' ', '')): + error_msgs.append(f"第{count}行: 租户名称只能包含字母、数字、下划线、中划线和空格") + continue + + # 检查导入文件内的重复名称 + if name in processed_names: + error_msgs.append(f"第{count}行: 租户名称 '{name}' 在文件中重复") + continue + processed_names.add(name) + + # 处理编码 + code = str(row['code']).strip() + if code in processed_codes: + error_msgs.append(f"第{count}行: 租户编码 '{code}' 在文件中重复") + continue + processed_codes.add(code) + + # 构建租户数据 + data = { + "name": name, + "code": code, + "status": status, + "description": str(row['description']).strip(), + } + + + # 检查时间有效性 + if 'start_time' in data and 'end_time' in data and data['start_time'] > data['end_time']: + error_msgs.append(f"第{count}行: 开始时间不能晚于结束时间") + continue + + # 处理租户导入 + exists_obj = await TenantCRUD(auth).get(name=data["name"]) + if exists_obj: + # 系统租户保护 + if exists_obj.id == 1: + error_msgs.append(f"第{count}行: 系统租户不允许修改") + continue + + if update_support: + await TenantCRUD(auth).update(id=exists_obj.id, data=data) + success_count += 1 + else: + error_msgs.append(f"第{count}行: 租户 {data['name']} 已存在") + else: + # 检查编码是否已存在 + exists_code = await TenantCRUD(auth).get(code=data["code"]) + if exists_code: + error_msgs.append(f"第{count}行: 租户编码 '{data['code']}' 已存在") + continue + + # 创建租户 + new_tenant = await TenantCRUD(auth).create(data=data) + success_count += 1 + + # 自动创建租户管理员(如果导入数量不是特别大) + if success_count < 10: # 限制自动创建管理员的数量 + await cls._create_tenant_admin_user(auth, new_tenant) + else: + log.info(f"批量导入超过10个租户,跳过自动创建管理员用户") + + except Exception as e: + error_msgs.append(f"第{count}行: {str(e)}") + continue + + # 返回详细的导入结果 + result = f"成功导入 {success_count} 条数据" + if error_msgs: + result += "\n错误信息:\n" + "\n".join(error_msgs) + # 记录错误详情到日志 + log.error(f"租户批量导入错误详情: {error_msgs}") + + log.info(f"租户批量导入完成: 成功{success_count}条, 失败{len(error_msgs)}条") + return result + + except CustomException: + raise + except Exception as e: + log.error(f"批量导入租户失败: {str(e)}") + raise CustomException(msg=f"导入失败: {str(e)}") + + @classmethod + async def import_template_download_service(cls) -> bytes: + """ + 下载导入模板 + + 返回: + - bytes: Excel文件字节流 + """ + header_list = ['名称', '编码', '状态', '描述', '开始时间', '结束时间'] + selector_header_list = ['状态'] + option_list = [{'状态': ['正常', '停用']}] + + # 添加示例数据和说明 + sample_data = [ + ['测试租户1', 'TEST001', '正常', '这是一个测试租户', '', ''], + ['测试租户2', 'TEST002', '正常', '这是另一个测试租户', '', ''] + ] + + # 添加说明文本 + description = """导入说明: +1. 名称和编码为必填项,名称长度2-64个字符 +2. 编码如果不填写,系统会自动生成 +3. 状态只能选择'正常'或'停用' +4. 时间格式:YYYY-MM-DD HH:MM:SS或YYYY-MM-DD +5. 单次导入最多支持100条数据 +""" + + return ExcelUtil.get_excel_template( + header_list=header_list, + selector_header_list=selector_header_list, + option_list=option_list + ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/token/__init__.py b/backend/app/api/v1/module_system/token/__init__.py new file mode 100644 index 00000000..633f8661 --- /dev/null +++ b/backend/app/api/v1/module_system/token/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- + diff --git a/backend/app/api/v1/module_system/token/controller.py b/backend/app/api/v1/module_system/token/controller.py new file mode 100644 index 00000000..197501cb --- /dev/null +++ b/backend/app/api/v1/module_system/token/controller.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +from fastapi import APIRouter, Body, Depends, Path, UploadFile +from fastapi.responses import JSONResponse, StreamingResponse +import urllib.parse + +from app.common.response import StreamResponse, SuccessResponse +from app.core.router_class import OperationLogRoute +from app.utils.common_util import bytes2file_response +from app.core.base_params import PaginationQueryParam +from app.core.dependencies import AuthPermission +from app.core.base_schema import BatchSetAvailable +from app.core.logger import log +from app.api.v1.module_system.auth.schema import AuthSchema +from .service import TokenService +from .schema import ( + TokenCreateSchema, + TokenUpdateSchema, + TokenQueryParam +) + + +TokenRouter = APIRouter(route_class=OperationLogRoute, prefix="/token", tags=["令牌模块"]) + +@TokenRouter.get("/detail/{id}", summary="获取令牌详情", description="获取令牌详情") +async def get_obj_detail_controller( + id: int = Path(..., description="令牌ID"), + auth: AuthSchema = Depends(AuthPermission(["module_system:demo:query"])) +) -> JSONResponse: + """ + 获取示例详情 + + 参数: + - id (int): 示例ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含示例详情的JSON响应 + """ + result_dict = await TokenService.detail_service(id=id, auth=auth) + log.info(f"获取令牌详情成功 {id}") + return SuccessResponse(data=result_dict, msg="获取令牌详情成功") + +@TokenRouter.get("/list", summary="查询令牌列表", description="查询令牌列表") +async def get_obj_list_controller( + page: PaginationQueryParam = Depends(), + search: TokenQueryParam = Depends(), + auth: AuthSchema = Depends(AuthPermission(["module_system:token:query"])) +) -> JSONResponse: + """ + 查询令牌列表 + + 参数: + - page (PaginationQueryParam): 分页查询参数 + - search (TokenQueryParam): 查询参数 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含令牌列表分页信息的JSON响应 + """ + # 使用数据库分页而不是应用层分页 + result_dict = await TokenService.page_service( + auth=auth, + page_no=page.page_no, + page_size=page.page_size, + search=search, + order_by=page.order_by + ) + log.info("查询令牌列表成功") + return SuccessResponse(data=result_dict, msg="查询令牌列表成功") + +@TokenRouter.post("/create", summary="创建令牌", description="创建令牌") +async def create_obj_controller( + data: TokenCreateSchema, + auth: AuthSchema = Depends(AuthPermission(["module_system:token:create"])) +) -> JSONResponse: + """ + 创建令牌 + + 参数: + - data (TokenCreateSchema): 令牌创建模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含创建令牌详情的JSON响应 + """ + result_dict = await TokenService.create_service(auth=auth, data=data) + log.info(f"创建令牌成功: {result_dict.get('name')}") + return SuccessResponse(data=result_dict, msg="创建令牌成功") + +@TokenRouter.put("/update/{id}", summary="修改令牌", description="修改令牌") +async def update_obj_controller( + data: TokenUpdateSchema, + id: int = Path(..., description="令牌ID"), + auth: AuthSchema = Depends(AuthPermission(["module_system:token:update"])) +) -> JSONResponse: + """ + 修改令牌 + + 参数: + - data (TokenUpdateSchema): 令牌更新模型 + - id (int): 令牌ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含修改令牌详情的JSON响应 + """ + result_dict = await TokenService.update_service(auth=auth, id=id, data=data) + log.info(f"修改令牌成功: {result_dict.get('name')}") + return SuccessResponse(data=result_dict, msg="修改令牌成功") + +@TokenRouter.delete("/delete", summary="删除令牌", description="删除令牌") +async def delete_obj_controller( + ids: list[int] = Body(..., description="ID列表"), + auth: AuthSchema = Depends(AuthPermission(["module_system:token:delete"])) +) -> JSONResponse: + """ + 删除令牌 + + 参数: + - ids (list[int]): 令牌ID列表 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含删除令牌详情的JSON响应 + """ + await TokenService.delete_service(auth=auth, ids=ids) + log.info(f"删除令牌成功: {ids}") + return SuccessResponse(msg="删除令牌成功") + +@TokenRouter.patch("/available/setting", summary="批量修改令牌状态", description="批量修改令牌状态") +async def batch_set_available_obj_controller( + data: BatchSetAvailable, + auth: AuthSchema = Depends(AuthPermission(["module_system:token:patch"])) +) -> JSONResponse: + """ + 批量修改令牌状态 + + 参数: + - data (BatchSetAvailable): 批量修改令牌状态模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含批量修改令牌状态详情的JSON响应 + """ + await TokenService.set_available_service(auth=auth, data=data) + log.info(f"批量修改令牌状态成功: {data.ids}") + return SuccessResponse(msg="批量修改令牌状态成功") diff --git a/backend/app/api/v1/module_system/token/crud.py b/backend/app/api/v1/module_system/token/crud.py new file mode 100644 index 00000000..1a1b75d7 --- /dev/null +++ b/backend/app/api/v1/module_system/token/crud.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +from collections.abc import Sequence +from app.core.base_crud import CRUDBase + +from app.api.v1.module_system.auth.schema import AuthSchema +from .model import TokenModel +from .schema import TokenCreateSchema, TokenUpdateSchema, TokenOutSchema + + +class TokenCRUD(CRUDBase[TokenModel, TokenCreateSchema, TokenUpdateSchema]): + """令牌数据层""" + + def __init__(self, auth: AuthSchema) -> None: + """ + 初始化CRUD数据层 + + 参数: + - auth (AuthSchema): 认证信息模型 + """ + super().__init__(model=TokenModel, auth=auth) + + async def get_by_id_crud(self, id: int, preload: list[str] | None = None) -> TokenModel | None: + """ + 详情 + + 参数: + - id (int): 令牌ID + - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 + + 返回: + - TokenModel | None: 令牌模型实例或None + """ + return await self.get(id=id, preload=preload) + + async def list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list[str] | None = None) -> Sequence[TokenModel]: + """ + 列表查询 + + 参数: + - search (dict | None): 查询参数 + - order_by (list[dict] | None): 排序参数 + - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 + + 返回: + - Sequence[DemoModel]: 示例模型实例序列 + """ + return await self.list(search=search, order_by=order_by, preload=preload) + + async def create_crud(self, data: TokenCreateSchema) -> TokenModel | None: + """ + 创建 + + 参数: + - data (TokenCreateSchema): 令牌创建模型 + + 返回: + - TokenModel | None: 令牌模型实例或None + """ + return await self.create(data=data) + + async def update_crud(self, id: int, data: TokenUpdateSchema) -> TokenModel | None: + """ + 更新 + + 参数: + - id (int): 令牌ID + - data (TokenUpdateSchema): 令牌更新模型 + + 返回: + - TokenModel | None: 令牌模型实例或None + """ + return await self.update(id=id, data=data) + + async def delete_crud(self, ids: list[int]) -> None: + """ + 批量删除 + + 参数: + - ids (list[int]): 令牌ID列表 + + 返回: + - None + """ + return await self.delete(ids=ids) + + async def set_available_crud(self, ids: list[int], status: str) -> None: + """ + 批量设置可用状态 + + 参数: + - ids (list[int]): 令牌ID列表 + - status (str): 可用状态 + + 返回: + - None + """ + return await self.set(ids=ids, status=status) + + async def page_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict: + """ + 分页查询 + + 参数: + - offset (int): 偏移量 + - limit (int): 每页数量 + - order_by (list[dict] | None): 排序参数 + - search (dict | None): 查询参数 + - preload (list | None): 预加载关系,未提供时使用模型默认项 + + 返回: + - dict: 分页数据 + """ + order_by_list = order_by or [{'id': 'asc'}] + search_dict = search or {} + + return await self.page( + offset=offset, + limit=limit, + order_by=order_by_list, + search=search_dict, + out_schema=TokenOutSchema, + preload=preload + ) diff --git a/backend/app/api/v1/module_system/token/model.py b/backend/app/api/v1/module_system/token/model.py new file mode 100644 index 00000000..c1028411 --- /dev/null +++ b/backend/app/api/v1/module_system/token/model.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.base_model import ModelMixin, UserMixin + + +class TokenModel(ModelMixin, UserMixin): + """ + 令牌表 + """ + __tablename__: str = 'sys_token' + __table_args__: dict[str, str] = ({'comment': '令牌表'}) + __loader_options__: list[str] = ["created_by", "updated_by"] + + name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='名称') diff --git a/backend/app/api/v1/module_system/token/schema.py b/backend/app/api/v1/module_system/token/schema.py new file mode 100644 index 00000000..84c56b94 --- /dev/null +++ b/backend/app/api/v1/module_system/token/schema.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from fastapi import Query + +from app.core.base_schema import BaseSchema, UserBySchema +from app.core.validator import DateTimeStr + + +class TokenCreateSchema(BaseModel): + """新增模型""" + name: str = Field(..., description='名称') + status: str = Field(default="0", description="是否启用(0:启用 1:禁用)") + description: str | None = Field(default=None, description="描述") + + @field_validator('name') + @classmethod + def validate_name(cls, v: str) -> str: + """验证名称字段的格式和内容""" + # 去除首尾空格 + v = v.strip() + if not v: + raise ValueError('名称不能为空') + return v + + @model_validator(mode='after') + def _after_validation(self): + """ + 核心业务规则校验 + """ + # 长度校验:名称最小长度 + if len(self.name) < 2 or len(self.name) > 50: + raise ValueError('名称长度必须在2-50个字符之间') + # 格式校验:名称只能包含字母、数字、下划线和中划线 + if not self.name.isalnum() and not all(c in '-_' for c in self.name): + raise ValueError('名称只能包含字母、数字、下划线和中划线') + if self.status not in ["0", "1"]: + raise ValueError('是否启用必须为0或1') + # 描述校验:描述最大长度 + if self.description and len(self.description) > 255: + raise ValueError('描述长度不能超过255个字符') + + return self + + +class TokenUpdateSchema(TokenCreateSchema): + """更新模型""" + ... + + +class TokenOutSchema(TokenCreateSchema, BaseSchema, UserBySchema): + """响应模型""" + model_config = ConfigDict(from_attributes=True) + + +class TokenQueryParam: + """令牌查询参数""" + + def __init__( + self, + name: str | None = Query(None, description="名称"), + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), + created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + created_id: int | None = Query(None, description="创建人"), + updated_id: int | None = Query(None, description="更新人") + ) -> None: + # 模糊查询字段 + self.name = ("like", name) + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) + + # 时间范围查询 + if created_time and len(created_time) == 2: + self.created_time = ("between", (created_time[0], created_time[1])) + if updated_time and len(updated_time) == 2: + self.updated_time = ("between", (updated_time[0], updated_time[1])) diff --git a/backend/app/api/v1/module_system/token/service.py b/backend/app/api/v1/module_system/token/service.py new file mode 100644 index 00000000..e95a75ec --- /dev/null +++ b/backend/app/api/v1/module_system/token/service.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- + +import io +from typing import Any +from fastapi import UploadFile +import pandas as pd + +from app.core.base_schema import BatchSetAvailable +from app.core.exceptions import CustomException +from app.utils.excel_util import ExcelUtil +from app.core.logger import log + +from app.api.v1.module_system.auth.schema import AuthSchema +from .schema import TokenCreateSchema, TokenUpdateSchema, TokenOutSchema, TokenQueryParam +from .crud import TokenCRUD + + +class TokenService: + """ + 令牌管理模块服务层 + """ + + @classmethod + async def detail_service(cls, auth: AuthSchema, id: int) -> dict: + """ + 详情 + + 参数: + - auth (AuthSchema): 认证信息模型 + - id (int): 令牌ID + + 返回: + - dict: 令牌模型实例字典 + """ + obj = await TokenCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg="该数据不存在") + return TokenOutSchema.model_validate(obj).model_dump() + + @classmethod + async def list_service(cls, auth: AuthSchema, search: TokenQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]: + """ + 列表查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - search (TokenQueryParam | None): 查询参数 + - order_by (list[dict[str, str]] | None): 排序参数 + + 返回: + - list[dict]: 令牌模型实例字典列表 + """ + search_dict = search.__dict__ if search else None + obj_list = await TokenCRUD(auth).list_crud(search=search_dict, order_by=order_by) + return [TokenOutSchema.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: TokenQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> dict: + """ + 分页查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - page_no (int): 页码 + - page_size (int): 每页数量 + - search (TokenQueryParam | None): 查询参数 + - order_by (list[dict[str, str]] | None): 排序参数 + + 返回: + - dict: 分页数据 + """ + search_dict = search.__dict__ if search else {} + order_by_list = order_by or [{'id': 'asc'}] + offset = (page_no - 1) * page_size + + result = await TokenCRUD(auth).page_crud( + offset=offset, + limit=page_size, + order_by=order_by_list, + search=search_dict + ) + return result + + @classmethod + async def create_service(cls, auth: AuthSchema, data: TokenCreateSchema) -> dict: + """ + 创建 + + 参数: + - auth (AuthSchema): 认证信息模型 + - data (TokenCreateSchema): 令牌创建模型 + + 返回: + - dict: 令牌模型实例字典 + """ + obj = await TokenCRUD(auth).get(name=data.name) + if obj: + raise CustomException(msg='创建失败,名称已存在') + obj = await TokenCRUD(auth).create_crud(data=data) + return TokenOutSchema.model_validate(obj).model_dump() + + @classmethod + async def update_service(cls, auth: AuthSchema, id: int, data: TokenUpdateSchema) -> dict: + """ + 更新 + + 参数: + - auth (AuthSchema): 认证信息模型 + - id (int): 令牌ID + - data (TokenUpdateSchema): 令牌更新模型 + + 返回: + - dict: 令牌模型实例字典 + """ + # 检查数据是否存在 + obj = await TokenCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg='更新失败,该数据不存在') + + # 检查名称是否重复 + exist_obj = await TokenCRUD(auth).get(name=data.name) + if exist_obj and exist_obj.id != id: + raise CustomException(msg='更新失败,名称重复') + + obj = await TokenCRUD(auth).update_crud(id=id, data=data) + return TokenOutSchema.model_validate(obj).model_dump() + + @classmethod + async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: + """ + 删除 + + 参数: + - auth (AuthSchema): 认证信息模型 + - ids (list[int]): 令牌ID列表 + + 返回: + - None + """ + if len(ids) < 1: + raise CustomException(msg='删除失败,删除对象不能为空') + + # 检查所有要删除的数据是否存在 + for id in ids: + obj = await TokenCRUD(auth).get_by_id_crud(id=id) + if not obj: + raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') + + await TokenCRUD(auth).delete_crud(ids=ids) + + @classmethod + async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + """ + 批量设置状态 + + 参数: + - auth (AuthSchema): 认证信息模型 + - data (BatchSetAvailable): 批量设置状态模型 + + 返回: + - None + """ + await TokenCRUD(auth).set_available_crud(ids=data.ids, status=data.status) + + @classmethod + async def batch_export_service(cls, obj_list: list[dict[str, Any]]) -> bytes: + """ + 批量导出 + + 参数: + - obj_list (list[dict[str, Any]]): 令牌模型实例字典列表 + + 返回: + - bytes: Excel文件字节流 + """ + mapping_dict = { + 'id': '编号', + 'name': '名称', + 'status': '状态', + 'description': '备注', + 'created_time': '创建时间', + 'updated_time': '更新时间', + 'created_id': '创建者', + } + + # 复制数据并转换状态 + data = obj_list.copy() + for item in data: + # 处理状态 + item['status'] = '启用' if item.get('status') == '0' else '停用' + # 处理创建者 + creator_info = item.get('created_id') + if isinstance(creator_info, dict): + item['created_id'] = creator_info.get('name', '未知') + else: + item['created_id'] = '未知' + + 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: + """ + 批量导入 + + 参数: + - auth (AuthSchema): 认证信息模型 + - file (UploadFile): 上传的Excel文件 + - update_support (bool): 是否支持更新存在数据 + + 返回: + - str: 导入结果信息 + """ + + header_dict = { + '名称': 'name', + '状态': 'status', + '描述': 'description' + } + + try: + # 读取Excel文件 + contents = await file.read() + df = pd.read_excel(io.BytesIO(contents)) + await file.close() + + if df.empty: + raise CustomException(msg="导入文件为空") + + # 检查表头是否完整 + missing_headers = [header for header in header_dict.keys() if header not in df.columns] + if missing_headers: + raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") + + # 重命名列名 + df.rename(columns=header_dict, inplace=True) + + # 验证必填字段 + required_fields = ['name', 'status'] + errors = [] + for field in required_fields: + missing_rows = df[df[field].isnull()].index.tolist() + if missing_rows: + field_name = [k for k,v in header_dict.items() if v == field][0] + rows_str = "、".join([str(i+1) for i in missing_rows]) + errors.append(f"{field_name}不能为空,第{rows_str}行") + if errors: + raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}") + + error_msgs = [] + success_count = 0 + count = 0 + + # 处理每一行数据 + for index, row in df.iterrows(): + count += 1 + try: + # 数据转换前的类型检查 + try: + status = True if row['status'] == '正常' else False + except ValueError: + error_msgs.append(f"第{count}行: 状态必须是'正常'或'停用'") + continue + + # 构建用户数据 + data = { + "name": str(row['name']), + "status": status, + "description": str(row['description']), + } + + # 处理用户导入 + exists_obj = await TokenCRUD(auth).get(name=data["name"]) + if exists_obj: + if update_support: + await TokenCRUD(auth).update(id=exists_obj.id, data=data) + success_count += 1 + else: + error_msgs.append(f"第{count}行: 对象 {data['name']} 已存在") + else: + await TokenCRUD(auth).create(data=data) + success_count += 1 + + except Exception as e: + error_msgs.append(f"第{count}行: {str(e)}") + continue + + # 返回详细的导入结果 + result = f"成功导入 {success_count} 条数据" + if error_msgs: + result += "\n错误信息:\n" + "\n".join(error_msgs) + return result + + except Exception as e: + log.error(f"批量导入用户失败: {str(e)}") + raise CustomException(msg=f"导入失败: {str(e)}") + + @classmethod + async def import_template_download_service(cls) -> bytes: + """ + 下载导入模板 + + 返回: + - bytes: Excel文件字节流 + """ + header_list = ['名称', '状态', '描述'] + selector_header_list = ['状态'] + option_list = [{'状态': ['正常', '停用']}] + return ExcelUtil.get_excel_template( + header_list=header_list, + selector_header_list=selector_header_list, + option_list=option_list + ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/user/model.py b/backend/app/api/v1/module_system/user/model.py index 0bd4833a..78074250 100644 --- a/backend/app/api/v1/module_system/user/model.py +++ b/backend/app/api/v1/module_system/user/model.py @@ -67,7 +67,7 @@ class UserModel(ModelMixin, UserMixin): __table_args__: dict[str, str] = ({'comment': '用户表'}) __loader_options__: list[str] = ["dept", "roles", "positions", "created_by", "updated_by"] - username: Mapped[str] = mapped_column(String(32), nullable=False, unique=True, comment="用户名/登录账号") + username: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, comment="用户名/登录账号") password: Mapped[str] = mapped_column(String(255), nullable=False, comment="密码哈希") name: Mapped[str] = mapped_column(String(32), nullable=False, comment="昵称") mobile: Mapped[str | None] = mapped_column(String(11), nullable=True, unique=True, comment="手机号") diff --git a/backend/app/api/v1/module_system/user/schema.py b/backend/app/api/v1/module_system/user/schema.py index 55589f66..06e6bab9 100644 --- a/backend/app/api/v1/module_system/user/schema.py +++ b/backend/app/api/v1/module_system/user/schema.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from fastapi import Query -from pydantic import BaseModel, ConfigDict, Field, EmailStr, field_validator +from pydantic import BaseModel, ConfigDict, Field, EmailStr, field_validator, model_validator from urllib.parse import urlparse -from app.core.validator import DateTimeStr, mobile_validator +from app.core.validator import DateTimeStr, email_validator, mobile_validator from app.core.base_schema import BaseSchema, CommonSchema, UserBySchema from app.core.validator import DateTimeStr from app.api.v1.module_system.menu.schema import MenuOutSchema @@ -13,7 +13,7 @@ from app.api.v1.module_system.role.schema import RoleOutSchema class CurrentUserUpdateSchema(BaseModel): """基础用户信息""" - name: str | None = Field(default=None, max_length=32, description="名称") + name: str | None = Field(default=None, description="名称") mobile: str | None = Field(default=None, description="手机号") email: EmailStr | None = Field(default=None, description="邮箱") gender: str | None = Field(default=None, description="性别") @@ -23,6 +23,13 @@ class CurrentUserUpdateSchema(BaseModel): @classmethod def validate_mobile(cls, value: str | None): return mobile_validator(value) + + @field_validator("email") + @classmethod + def validate_email(cls, value: str | None): + if not value: + return value + return email_validator(value) @field_validator("avatar") @classmethod @@ -33,14 +40,20 @@ class CurrentUserUpdateSchema(BaseModel): if parsed.scheme in ("http", "https") and parsed.netloc: return value raise ValueError("头像地址需为有效的HTTP/HTTPS URL") + + @model_validator(mode="after") + def check_model(self): + if self.name and len(self.name) > 32: + raise ValueError("名称长度不能超过32个字符") + return self class UserRegisterSchema(BaseModel): """注册""" - name: str | None = Field(default=None, max_length=32, description="名称") + name: str | None = Field(default=None, description="名称") mobile: str | None = Field(default=None, description="手机号") - username: str = Field(..., max_length=32, description="账号") - password: str = Field(..., max_length=128, description="密码哈希值") + username: str = Field(..., description="账号") + password: str = Field(..., description="密码哈希值") role_ids: list[int] | None = Field(default=[1], description='角色ID') created_id: int | None = Field(default=1, description='创建人ID') description: str | None = Field(default=None, max_length=255, description="备注") @@ -49,7 +62,7 @@ class UserRegisterSchema(BaseModel): @classmethod def validate_mobile(cls, value: str | None): return mobile_validator(value) - + @field_validator("username") @classmethod def validate_username(cls, value: str): @@ -61,6 +74,18 @@ class UserRegisterSchema(BaseModel): if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v): raise ValueError("账号需字母开头,3-32位,仅含字母/数字/_ . -") return v + + @model_validator(mode="after") + def check_model(self): + if self.name and len(self.name) > 32: + raise ValueError("名称长度不能超过32个字符") + if self.username and len(self.username) > 32: + raise ValueError("账号长度不能超过32个字符") + if self.description and len(self.description) > 255: + raise ValueError("备注长度不能超过255个字符") + if self.password and len(self.password) > 128: + raise ValueError("密码长度不能超过128个字符") + return self class UserForgetPasswordSchema(BaseModel): diff --git a/backend/app/api/v1/module_system/user/service.py b/backend/app/api/v1/module_system/user/service.py index 2d71c0ad..cd2eb212 100644 --- a/backend/app/api/v1/module_system/user/service.py +++ b/backend/app/api/v1/module_system/user/service.py @@ -505,9 +505,16 @@ class UserService: # 验证必填字段 required_fields = ['username', 'name', 'dept_id'] + errors = [] for field in required_fields: missing_rows = df[df[field].isnull()].index.tolist() - raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}行") + if missing_rows: + field_name = [k for k,v in header_dict.items() if v == field][0] + rows_str = "、".join([str(i+1) for i in missing_rows]) + errors.append(f"{field_name}不能为空,第{rows_str}行") + + if errors: + raise CustomException(msg=";".join(errors)) error_msgs = [] success_count = 0 diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index 3b3be36a..9e6897c3 100755 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -49,7 +49,7 @@ class Settings(BaseSettings): CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域 # ALLOW_ORIGINS: List[str] = ["*"] # 允许的域名列表 ALLOW_ORIGINS: List[str] = [ - 'http://127.0.0.1:8001', + 'http://localhost:8001', 'http://localhost:5180', ] # 允许的域名列表 ALLOW_METHODS: List[str] = ["*"] # 允许的HTTP方法 @@ -87,7 +87,7 @@ class Settings(BaseSettings): EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期 # 数据库类型 - DATABASE_TYPE: Literal['mysql', 'postgres'] = 'mysql' + DATABASE_TYPE: Literal['mysql', 'postgres', 'sqlite', 'dm'] = 'mysql' # MySQL/PostgreSQL数据库连接 @@ -197,11 +197,9 @@ class Settings(BaseSettings): elif self.DATABASE_TYPE == "postgres": return f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" elif self.DATABASE_TYPE == "sqlite": - return f"sqlite+aiosqlite:///{self.DATABASE_NAME}" - elif self.DATABASE_TYPE == "dm": - return f"dm+dmPython://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" + return f"sqlite+aiosqlite:///{self.DATABASE_NAME}.db" else: - raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 请选择 请选择 mysql、postgres") + raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 异步数据库请选择 mysql、postgres、sqlite") @property def DB_URI(self) -> str: @@ -211,11 +209,11 @@ class Settings(BaseSettings): elif self.DATABASE_TYPE == "postgres": return f"postgresql+psycopg2://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" elif self.DATABASE_TYPE == "sqlite": - return f"sqlite+pysqlite:///{self.DATABASE_NAME}" + return f"sqlite+pysqlite:///{self.DATABASE_NAME}.db" elif self.DATABASE_TYPE == "dm": return f"dm+dmPython://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" else: - raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 请选择 请选择 mysql、postgres") + raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 同步数据库请选择 mysql、postgres、sqlite、dm") @property def REDIS_URI(self) -> str: diff --git a/backend/app/core/base_crud.py b/backend/app/core/base_crud.py index 2ff36a94..59f297f4 100644 --- a/backend/app/core/base_crud.py +++ b/backend/app/core/base_crud.py @@ -275,18 +275,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - CustomException: 删除失败时抛出异常 """ try: - # 先查询确认权限,避免删除无权限的数据 - objs = await self.list(search={"id": ("in", ids)}) - accessible_ids = [obj.id for obj in objs] - - # 检查是否所有ID都有权限访问 - inaccessible_count = len(ids) - len(accessible_ids) - if inaccessible_count > 0: - raise CustomException(msg=f"无权限删除{inaccessible_count}条数据") - - if not accessible_ids: - return # 没有可删除的数据 - mapper = sa_inspect(self.model) pk_cols = list(getattr(mapper, "primary_key", [])) if not pk_cols: @@ -295,7 +283,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): raise CustomException(msg="暂不支持复合主键的批量删除") # 只删除有权限的数据 - sql = delete(self.model).where(pk_cols[0].in_(accessible_ids)) + sql = delete(self.model).where(pk_cols[0].in_(ids)) await self.auth.db.execute(sql) await self.auth.db.flush() except Exception as e: @@ -327,18 +315,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - CustomException: 更新失败时抛出异常 """ try: - # 先查询确认权限,避免更新无权限的数据 - objs = await self.list(search={"id": ("in", ids)}) - accessible_ids = [obj.id for obj in objs] - - # 检查是否所有ID都有权限访问 - inaccessible_count = len(ids) - len(accessible_ids) - if inaccessible_count > 0: - raise CustomException(msg=f"无权限更新{inaccessible_count}条数据") - - if not accessible_ids: - return # 没有可更新的数据 - mapper = sa_inspect(self.model) pk_cols = list(getattr(mapper, "primary_key", [])) if not pk_cols: @@ -347,7 +323,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): raise CustomException(msg="暂不支持复合主键的批量更新") # 只更新有权限的数据 - sql = update(self.model).where(pk_cols[0].in_(accessible_ids)).values(**kwargs) + sql = update(self.model).where(pk_cols[0].in_(ids)).values(**kwargs) await self.auth.db.execute(sql) await self.auth.db.flush() except CustomException: @@ -400,17 +376,17 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): conditions.append(attr.in_(val)) elif seq == "between" and isinstance(val, (list, tuple)) and len(val) == 2: conditions.append(attr.between(val[0], val[1])) - elif seq == "!=" and val: + elif seq == "!=" or seq == "ne" and val: conditions.append(attr != val) - elif seq == ">" and val: + elif seq == ">" or seq == "gt" and val: conditions.append(attr > val) - elif seq == ">=" and val: + elif seq == ">=" or seq == "ge" and val: conditions.append(attr >= val) - elif seq == "<" and val: + elif seq == "<" or seq == "lt"and val: conditions.append(attr < val) - elif seq == "<=" and val: + elif seq == "<=" or seq == "le" and val: conditions.append(attr <= val) - elif seq == "==" and val: + elif seq == "==" or seq == "eq" and val: conditions.append(attr == val) else: conditions.append(attr == value) diff --git a/backend/app/core/base_model.py b/backend/app/core/base_model.py index 027acecf..a8547e29 100644 --- a/backend/app/core/base_model.py +++ b/backend/app/core/base_model.py @@ -9,6 +9,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from app.api.v1.module_system.user.model import UserModel + from app.api.v1.module_system.customer.model import CustomerModel + from app.api.v1.module_system.tenant.model import TenantModel from app.utils.common_util import uuid4_str @@ -116,3 +118,62 @@ class UserMixin(MappedBase): foreign_keys=lambda: cls.updated_id, uselist=False ) + + +class TenantMixin(MappedBase): + """ + 租户字段 Mixin + """ + __abstract__: bool = True + + tenant_id: Mapped[int | None] = mapped_column( + Integer, + ForeignKey('sys_tenant.id', ondelete="CASCADE", onupdate="CASCADE"), + nullable=False, + index=True, + comment="所属租户ID" + ) + + @declared_attr + def tenant(cls) -> Mapped["TenantModel"]: + """ + 租户关联关系(延迟加载,避免循环依赖) + """ + return relationship( + "TenantModel", + primaryjoin=f"{cls.__name__}.tenant_id == TenantModel.id", + lazy="selectin", + foreign_keys=lambda: [cls.tenant_id], + viewonly=True, + uselist=False + ) + + +class CustomerMixin(MappedBase): + """ + 客户隔离字段 Mixin + """ + __abstract__: bool = True + + customer_id: Mapped[int | None] = mapped_column( + Integer, + ForeignKey('sys_customer.id', ondelete="CASCADE", onupdate="CASCADE"), + default=None, + nullable=True, + index=True, + comment="所属客户ID(NULL表示租户级数据,>0表示客户级数据)" + ) + + @declared_attr + def customer(cls) -> Mapped["CustomerModel"]: + """ + 客户关联关系(延迟加载,避免循环依赖) + """ + return relationship( + "CustomerModel", + primaryjoin=f"{cls.__name__}.customer_id == CustomerModel.id", + lazy="selectin", + foreign_keys=lambda: [cls.customer_id], + viewonly=True, + uselist=False + ) \ No newline at end of file diff --git a/backend/app/core/base_params.py b/backend/app/core/base_params.py index 7ec539c3..3b0ee25b 100644 --- a/backend/app/core/base_params.py +++ b/backend/app/core/base_params.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +import json from fastapi import Query +from app.core.validator import DateTimeStr + class PaginationQueryParam: """分页查询参数基类""" @@ -10,7 +13,7 @@ class PaginationQueryParam: self, page_no: int = Query(default=1, description="当前页码", ge=1), page_size: int = Query(default=10, description="每页数量", ge=1, le=100), - order_by: str | None = Query(default=None, description="排序字段,格式:field1,asc;field2,desc"), + order_by: str | None = Query(default=None, description="排序字段,格式:[{'field1': 'asc'}, {'field2': 'desc'}]"), ) -> None: """ 初始化分页查询参数。 @@ -28,14 +31,48 @@ class PaginationQueryParam: # 将字符串格式的order_by转换为服务层需要的List[Dict[str, str]]格式 if order_by: try: - self.order_by = [] - for item in order_by.split(';'): - if item.strip(): - field, direction = item.split(',', 1) - self.order_by.append({field.strip(): direction.strip().lower()}) + self.order_by = json.loads(order_by) except ValueError: # 如果解析失败,使用默认排序 self.order_by = [{'updated_time': 'desc'}] else: self.order_by = [{'updated_time': 'desc'}] + +class BaseQueryParam: + """公共查询参数""" + def __init__( + self, + description: str | None = Query(None, description="描述"), + status: str | None = Query(None, description="是否启用"), + created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), + *args, + **kwargs + ) -> None: + # 模糊查询字段 + if description: + self.description = ("like", description) + + # 精确查询字段 + if status: + self.status = ("eq", status) + + # 时间范围查询 + if created_time and len(created_time) == 2: + self.created_time = ("between", (created_time[0], created_time[1])) + if updated_time and len(updated_time) == 2: + self.updated_time = ("between", (updated_time[0], updated_time[1])) + + +class CommonQueryParam: + """根据用户查询参数""" + def __init__( + self, + created_id: int | None = Query(None, description="创建人"), + updated_id: int | None = Query(None, description="更新人") + ) -> None: + if created_id: + self.created_id = ("eq", created_id) + if updated_id: + self.updated_id = ("eq", updated_id) diff --git a/backend/app/core/base_schema.py b/backend/app/core/base_schema.py index 600fa621..b984f34e 100644 --- a/backend/app/core/base_schema.py +++ b/backend/app/core/base_schema.py @@ -5,15 +5,6 @@ from pydantic import BaseModel, ConfigDict, Field from app.core.validator import DateTimeStr -class UserInfoSchema(BaseModel): - """用户信息模型""" - model_config = ConfigDict(from_attributes=True) - - id: int | None = Field(default=None, description="用户ID") - name: str | None = Field(default=None, description="用户姓名") - username: str | None = Field(default=None, description="用户名") - - class CommonSchema(BaseModel): """通用信息模型""" model_config = ConfigDict(from_attributes=True) @@ -39,9 +30,25 @@ class UserBySchema(BaseModel): model_config = ConfigDict(from_attributes=True) created_id: int | None = Field(default=None, description="创建人ID") - created_by: UserInfoSchema | None = Field(default=None, description="创建人信息") + created_by: CommonSchema | None = Field(default=None, description="创建人信息") updated_id: int | None = Field(default=None, description="更新人ID") - updated_by: UserInfoSchema | None = Field(default=None, description="更新人信息") + updated_by: CommonSchema | None = Field(default=None, description="更新人信息") + + +class TenantSchema(BaseModel): + """租户模型""" + model_config = ConfigDict(from_attributes=True) + + tenant_id: int | None = Field(default=None, description="所属租户ID") + tenant: CommonSchema | None = Field(default=None, description="租户信息") + + +class CustomerSchema(BaseModel): + """客户模型""" + model_config = ConfigDict(from_attributes=True) + + customer_id: int | None = Field(default=None, description="所属客户ID") + customer: CommonSchema | None = Field(default=None, description="客户信息") class BatchSetAvailable(BaseModel): diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 4ceddc5e..60eb645a 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -55,18 +55,28 @@ def create_async_engine_and_session( if not settings.SQL_DB_ENABLE: raise CustomException(msg="请先开启数据库连接", data="请启用 app/config/setting.py: SQL_DB_ENABLE") # 异步数据库引擎 - async_engine: AsyncEngine = create_async_engine( - url=db_url, - echo=settings.DATABASE_ECHO, - echo_pool=settings.ECHO_POOL, - pool_pre_ping=settings.POOL_PRE_PING, - future=settings.FUTURE, - pool_recycle=settings.POOL_RECYCLE, - pool_size=settings.POOL_SIZE, - max_overflow=settings.MAX_OVERFLOW, - pool_timeout=settings.POOL_TIMEOUT, - pool_use_lifo=settings.POOL_USE_LIFO, - ) + if settings.DATABASE_TYPE == 'sqlite': + async_engine = create_async_engine( + url=db_url, + echo=settings.DATABASE_ECHO, + echo_pool=settings.ECHO_POOL, + pool_pre_ping=settings.POOL_PRE_PING, + future=settings.FUTURE, + pool_recycle=settings.POOL_RECYCLE, + ) + else: + async_engine = create_async_engine( + url=db_url, + echo=settings.DATABASE_ECHO, + echo_pool=settings.ECHO_POOL, + pool_pre_ping=settings.POOL_PRE_PING, + future=settings.FUTURE, + pool_recycle=settings.POOL_RECYCLE, + pool_size=settings.POOL_SIZE, + max_overflow=settings.MAX_OVERFLOW, + pool_timeout=settings.POOL_TIMEOUT, + pool_use_lifo=settings.POOL_USE_LIFO, + ) except Exception as e: log.error(f'❌ 数据库连接失败 {e}') raise diff --git a/backend/app/core/validator.py b/backend/app/core/validator.py index 6c033147..9d5b111a 100644 --- a/backend/app/core/validator.py +++ b/backend/app/core/validator.py @@ -107,6 +107,27 @@ def mobile_validator(value: str | None) -> str | None: return value +def code_validator(value: str | None) -> str | None: + """ + 编码验证器。 + + 参数: + - value (str | None): 编码。 + + 返回: + - str | None: 验证后的编码。 + + 异常: + - CustomException: 编码格式无效时抛出。 + """ + if not value: + return value + v = value.strip() + if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,15}$", v): + raise CustomException(code=RET.ERROR.code, msg="编码需字母开头,允许字母/数字/下划线,长度2-16") + return v + + def menu_request_validator(data): """ 菜单请求数据验证器。 diff --git a/backend/env/.env.dev b/backend/env/.env.dev index 5260ef49..6b633ed2 100644 --- a/backend/env/.env.dev +++ b/backend/env/.env.dev @@ -22,7 +22,7 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi DEMO_ENABLE = False # 数据库配置 -DATABASE_TYPE = "mysql" # mysql、postgres、[qlite、dm这俩种不支持代码生成] +DATABASE_TYPE = "sqlite" # mysql、postgres、[qlite、dm这俩种不支持代码生成] # 数据库配置 DATABASE_HOST = "localhost" diff --git a/frontend/.env.development b/frontend/.env.development index 2152d301..adcb3357 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -5,7 +5,7 @@ VITE_APP_ENV=development VITE_APP_TITLE=fastapiadmin # 网络请求公用地址 -VITE_API_BASE_URL=http://127.0.0.1:8001 +VITE_API_BASE_URL=https://service.fastapiadmin.com # 代理前缀 VITE_APP_BASE_API=/api/v1 diff --git a/frontend/package.json b/frontend/package.json index 96a5dddc..1a21314b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,6 +63,8 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", + "@logicflow/core": "2.2.0-alpha.3", + "@logicflow/extension": "2.2.0-alpha.3", "@vueuse/core": "^13.5.0", "@wangeditor-next/editor": "^5.6.49", "@wangeditor-next/editor-for-vue": "^5.1.14", diff --git a/frontend/src/api/module_application/job.ts b/frontend/src/api/module_application/job.ts index da2ea06a..a52db669 100644 --- a/frontend/src/api/module_application/job.ts +++ b/frontend/src/api/module_application/job.ts @@ -164,8 +164,8 @@ export interface JobTable extends BaseType { trigger_args?: string; start_date?: string; end_date?: string; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } export interface JobForm extends BaseFormType { diff --git a/frontend/src/api/module_application/myapp.ts b/frontend/src/api/module_application/myapp.ts index 640a47d2..e7460e3a 100644 --- a/frontend/src/api/module_application/myapp.ts +++ b/frontend/src/api/module_application/myapp.ts @@ -97,8 +97,8 @@ export interface ApplicationInfo extends BaseType { name?: string; access_url?: string; icon_url?: string; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } /** diff --git a/frontend/src/api/module_gencode/demo.ts b/frontend/src/api/module_gencode/demo.ts index 81c8ac4f..cc679d28 100644 --- a/frontend/src/api/module_gencode/demo.ts +++ b/frontend/src/api/module_gencode/demo.ts @@ -92,8 +92,8 @@ export interface DemoPageQuery extends PageQuery { export interface DemoTable extends BaseType { name?: string; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } export interface DemoForm extends BaseFormType { diff --git a/frontend/src/api/module_generator/gencode.ts b/frontend/src/api/module_generator/gencode.ts index db008e19..c90eedf0 100644 --- a/frontend/src/api/module_generator/gencode.ts +++ b/frontend/src/api/module_generator/gencode.ts @@ -166,8 +166,8 @@ export interface GenTableSchema extends BaseType { sub_table?: GenTableSchema; /** 是否为子表 */ sub?: boolean; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } /** 代码生成业务表列模型 */ @@ -212,6 +212,6 @@ export interface GenTableColumnSchema extends BaseType { dict_type?: string; /** 排序 */ sort?: number; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } diff --git a/frontend/src/api/module_system/customer.ts b/frontend/src/api/module_system/customer.ts new file mode 100644 index 00000000..b19cc95a --- /dev/null +++ b/frontend/src/api/module_system/customer.ts @@ -0,0 +1,101 @@ +import request from "@/utils/request"; + +const API_PATH = "/system/customer"; + +const CustomerAPI = { + listCustomer(query: CustomerPageQuery) { + return request>>({ + url: `${API_PATH}/list`, + method: "get", + params: query, + }); + }, + + detailCustomer(query: number) { + return request>({ + url: `${API_PATH}/detail/${query}`, + method: "get", + }); + }, + + createCustomer(body: CustomerForm) { + return request({ + url: `${API_PATH}/create`, + method: "post", + data: body, + }); + }, + + updateCustomer(id: number, body: CustomerForm) { + return request({ + url: `${API_PATH}/update/${id}`, + method: "put", + data: body, + }); + }, + + deleteCustomer(body: number[]) { + return request({ + url: `${API_PATH}/delete`, + method: "delete", + data: body, + }); + }, + + batchCustomer(body: BatchType) { + return request({ + url: `${API_PATH}/available/setting`, + method: "patch", + data: body, + }); + }, + + exportCustomer(body: CustomerPageQuery) { + return request({ + url: `${API_PATH}/export`, + method: "post", + data: body, + responseType: "blob", + }); + }, + + downloadCustomer() { + return request({ + url: `${API_PATH}/download/template`, + method: "post", + responseType: "blob", + }); + }, + + importCustomer(body: FormData) { + return request({ + url: `${API_PATH}/import`, + method: "post", + data: body, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, +}; + +export default CustomerAPI; + +export interface CustomerPageQuery extends PageQuery { + name?: string; + status?: string; + created_time?: string[]; +} + +export interface CustomerTable extends BaseType { + name?: string; + code?: string; + created_by?: CommonType; + updated_by?: CommonType; + tenant?: CommonType; +} + +export interface CustomerForm extends BaseFormType { + name?: string; + code?: string; +} diff --git a/frontend/src/api/module_system/log.ts b/frontend/src/api/module_system/log.ts index 33908915..85e3083e 100644 --- a/frontend/src/api/module_system/log.ts +++ b/frontend/src/api/module_system/log.ts @@ -60,6 +60,6 @@ export interface LogTable extends BaseType { request_payload?: string; response_json?: string; process_time?: string; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } diff --git a/frontend/src/api/module_system/notice.ts b/frontend/src/api/module_system/notice.ts index f5bc8702..0b829d68 100644 --- a/frontend/src/api/module_system/notice.ts +++ b/frontend/src/api/module_system/notice.ts @@ -83,8 +83,8 @@ export interface NoticeTable extends BaseType { notice_title?: string; notice_type?: string; notice_content?: string; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } export interface NoticeForm extends BaseFormType { diff --git a/frontend/src/api/module_system/position.ts b/frontend/src/api/module_system/position.ts index cd884c40..cfde13d8 100644 --- a/frontend/src/api/module_system/position.ts +++ b/frontend/src/api/module_system/position.ts @@ -73,8 +73,8 @@ export interface PositionPageQuery extends PageQuery { export interface PositionTable extends BaseType { name?: string; order?: number; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } export interface PositionForm extends BaseFormType { diff --git a/frontend/src/api/module_system/tenant.ts b/frontend/src/api/module_system/tenant.ts new file mode 100644 index 00000000..bc6be9a9 --- /dev/null +++ b/frontend/src/api/module_system/tenant.ts @@ -0,0 +1,102 @@ +import request from "@/utils/request"; + +const API_PATH = "/system/tenant"; + +const TenantAPI = { + listTenant(query: TenantPageQuery) { + return request>>({ + url: `${API_PATH}/list`, + method: "get", + params: query, + }); + }, + + detailTenant(query: number) { + return request>({ + url: `${API_PATH}/detail/${query}`, + method: "get", + }); + }, + + createTenant(body: TenantForm) { + return request({ + url: `${API_PATH}/create`, + method: "post", + data: body, + }); + }, + + updateTenant(id: number, body: TenantForm) { + return request({ + url: `${API_PATH}/update/${id}`, + method: "put", + data: body, + }); + }, + + deleteTenant(body: number[]) { + return request({ + url: `${API_PATH}/delete`, + method: "delete", + data: body, + }); + }, + + batchTenant(body: BatchType) { + return request({ + url: `${API_PATH}/available/setting`, + method: "patch", + data: body, + }); + }, + + exportTenant(body: TenantPageQuery) { + return request({ + url: `${API_PATH}/export`, + method: "post", + data: body, + responseType: "blob", + }); + }, + + downloadTenant() { + return request({ + url: `${API_PATH}/download/template`, + method: "post", + responseType: "blob", + }); + }, + + importTenant(body: FormData) { + return request({ + url: `${API_PATH}/import`, + method: "post", + data: body, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, +}; + +export default TenantAPI; + +export interface TenantPageQuery extends PageQuery { + name?: string; + status?: string; + created_time?: string[]; +} + +export interface TenantTable extends BaseType { + name?: string; + code?: string; + start_time?: string; + end_time?: string; +} + +export interface TenantForm extends BaseFormType { + name?: string; + code?: string; + start_time?: string; + end_time?: string; +} diff --git a/frontend/src/api/module_system/token.ts b/frontend/src/api/module_system/token.ts new file mode 100644 index 00000000..50712ad5 --- /dev/null +++ b/frontend/src/api/module_system/token.ts @@ -0,0 +1,101 @@ +import request from "@/utils/request"; + +const API_PATH = "/system/token"; + +const TokenAPI = { + getTokenList(query: TokenPageQuery) { + return request>>({ + url: `${API_PATH}/list`, + method: "get", + params: query, + }); + }, + + getTokenDetail(query: number) { + return request>({ + url: `${API_PATH}/detail/${query}`, + method: "get", + }); + }, + + createToken(body: TokenForm) { + return request({ + url: `${API_PATH}/create`, + method: "post", + data: body, + }); + }, + + updateToken(id: number, body: TokenForm) { + return request({ + url: `${API_PATH}/update/${id}`, + method: "put", + data: body, + }); + }, + + deleteToken(body: number[]) { + return request({ + url: `${API_PATH}/delete`, + method: "delete", + data: body, + }); + }, + + batchToken(body: BatchType) { + return request({ + url: `${API_PATH}/available/setting`, + method: "patch", + data: body, + }); + }, + + exportToken(body: TokenPageQuery) { + return request({ + url: `${API_PATH}/export`, + method: "post", + data: body, + responseType: "blob", + }); + }, + + downloadTemplateToken() { + return request({ + url: `${API_PATH}/download/template`, + method: "post", + responseType: "blob", + }); + }, + + importToken(body: FormData) { + return request({ + url: `${API_PATH}/import`, + method: "post", + data: body, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, +}; + +export default TokenAPI; + +export interface TokenPageQuery extends PageQuery { + name?: string; + status?: string; + created_time?: string[]; + updated_time?: string[]; + created_id?: number; + updated_id?: number; +} + +export interface TokenTable extends BaseType { + name?: string; + created_by?: CommonType; + updated_by?: CommonType; +} + +export interface TokenForm extends BaseFormType { + name?: string; +} diff --git a/frontend/src/api/module_system/user.ts b/frontend/src/api/module_system/user.ts index da4476f1..827af565 100644 --- a/frontend/src/api/module_system/user.ts +++ b/frontend/src/api/module_system/user.ts @@ -188,8 +188,8 @@ export interface UserInfo extends BaseType { position_ids?: positionSelectorType["id"][]; is_superuser?: boolean; last_login?: string; - created_by?: creatorType; - updated_by?: updatorType; + created_by?: CommonType; + updated_by?: CommonType; } export interface deptTreeType { diff --git a/frontend/src/components/Notification/index.vue b/frontend/src/components/Notification/index.vue index 4cd3ca84..49937a11 100644 --- a/frontend/src/components/Notification/index.vue +++ b/frontend/src/components/Notification/index.vue @@ -67,7 +67,7 @@ - {{ noticeDetail.created_by?.username }} + {{ noticeDetail.created_by?.name }} diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts index 809775e8..ccc29a34 100644 --- a/frontend/src/types/global.d.ts +++ b/frontend/src/types/global.d.ts @@ -136,24 +136,6 @@ declare global { name?: string; } - /** - * 创建人 - */ - interface creatorType { - id?: number; - name?: string; - username?: string; - } - - /** - * 更新人 - */ - interface updatorType { - id?: number; - name?: string; - username?: string; - } - /** * 基础类型 */ diff --git a/frontend/src/views/module_application/workflow/index.vue b/frontend/src/views/module_application/workflow/index.vue index bf1c2c42..3fc1dffc 100644 --- a/frontend/src/views/module_application/workflow/index.vue +++ b/frontend/src/views/module_application/workflow/index.vue @@ -1,15 +1,882 @@ - - + diff --git a/frontend/src/views/module_system/customer/index.vue b/frontend/src/views/module_system/customer/index.vue new file mode 100644 index 00000000..d7cfc73d --- /dev/null +++ b/frontend/src/views/module_system/customer/index.vue @@ -0,0 +1,745 @@ + + + + + + diff --git a/frontend/src/views/module_system/tenant/index.vue b/frontend/src/views/module_system/tenant/index.vue new file mode 100644 index 00000000..0287fa2c --- /dev/null +++ b/frontend/src/views/module_system/tenant/index.vue @@ -0,0 +1,757 @@ + + + + + + diff --git a/frontend/src/views/module_system/token/index.vue b/frontend/src/views/module_system/token/index.vue new file mode 100644 index 00000000..b75d8c21 --- /dev/null +++ b/frontend/src/views/module_system/token/index.vue @@ -0,0 +1,793 @@ + + + + + + From 057776c35facfa7fa0826aadf8c89a9f4115634a Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Mon, 22 Dec 2025 22:21:52 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix(backend):=20=E4=BF=AE=E5=A4=8DWebSocket?= =?UTF-8?q?=E5=92=8CAI=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(frontend): 添加markdown支持并改进AI聊天界面显示 refactor(backend): 优化定时任务参数处理和日志记录 style(frontend): 格式化代码和修复样式问题 chore(frontend): 添加markdown相关依赖 --- .../api/v1/module_application/ai/service.py | 8 +- .../v1/module_application/ai/tools/ai_util.py | 21 ++++- .../app/api/v1/module_application/ai/ws.py | 7 +- .../job/tools/ap_scheduler.py | 20 +++- .../app/api/v1/module_gencode/demo/schema.py | 1 - frontend/.env.development | 2 +- frontend/package.json | 4 + .../src/views/module_application/ai/index.vue | 94 ++++++++++++++++--- .../views/module_system/customer/index.vue | 6 +- .../src/views/module_system/tenant/index.vue | 7 +- 10 files changed, 144 insertions(+), 26 deletions(-) diff --git a/backend/app/api/v1/module_application/ai/service.py b/backend/app/api/v1/module_application/ai/service.py index 73b5bff8..bdd69c78 100644 --- a/backend/app/api/v1/module_application/ai/service.py +++ b/backend/app/api/v1/module_application/ai/service.py @@ -4,6 +4,7 @@ from typing import Any, AsyncGenerator from app.core.exceptions import CustomException from app.api.v1.module_system.auth.schema import AuthSchema +from app.core.logger import log from .tools.ai_util import AIClient from .schema import McpCreateSchema, McpUpdateSchema, McpOutSchema, ChatQuerySchema, McpQueryParam from .crud import McpCRUD @@ -124,5 +125,8 @@ class McpService: async for response in mcp_client.process(query.message): yield response finally: - # 确保关闭客户端连接 - await mcp_client.close() + # 确保关闭客户端连接,即使在事件循环关闭时也能安全处理 + try: + await mcp_client.close() + except Exception as e: + log.debug(f"关闭AIClient时发生异常(预期行为,服务可能正在关闭): {str(e)}") diff --git a/backend/app/api/v1/module_application/ai/tools/ai_util.py b/backend/app/api/v1/module_application/ai/tools/ai_util.py index fc318d64..4d8f69b1 100644 --- a/backend/app/api/v1/module_application/ai/tools/ai_util.py +++ b/backend/app/api/v1/module_application/ai/tools/ai_util.py @@ -110,7 +110,24 @@ class AIClient: """ 关闭客户端连接 """ + import asyncio + + # 安全关闭OpenAI客户端 if hasattr(self, 'client'): - await self.client.close() + try: + # 检查事件循环是否仍在运行 + loop = asyncio.get_event_loop() + if loop.is_running(): + await self.client.close() + except Exception as e: + log.debug(f"关闭OpenAI客户端时发生异常: {str(e)}") + + # 安全关闭HTTP客户端 if hasattr(self, 'http_client'): - await self.http_client.aclose() \ No newline at end of file + try: + # 检查事件循环是否仍在运行 + loop = asyncio.get_event_loop() + if loop.is_running(): + await self.http_client.aclose() + except Exception as e: + log.debug(f"关闭HTTP客户端时发生异常: {str(e)}") \ No newline at end of file diff --git a/backend/app/api/v1/module_application/ai/ws.py b/backend/app/api/v1/module_application/ai/ws.py index ff16857f..53aea913 100644 --- a/backend/app/api/v1/module_application/ai/ws.py +++ b/backend/app/api/v1/module_application/ai/ws.py @@ -32,4 +32,9 @@ async def websocket_chat_controller( except Exception as e: log.error(f"WebSocket聊天出错: {str(e)}") finally: - await websocket.close() \ No newline at end of file + try: + # 检查WebSocket连接状态,避免重复关闭已关闭的连接 + if websocket.client_state != websocket.client_state.DISCONNECTED: + await websocket.close() + except Exception as e: + log.debug(f"WebSocket关闭时发生异常(预期行为,服务可能正在关闭): {str(e)}") \ No newline at end of file diff --git a/backend/app/api/v1/module_application/job/tools/ap_scheduler.py b/backend/app/api/v1/module_application/job/tools/ap_scheduler.py index 3a5e98d8..5545fda6 100644 --- a/backend/app/api/v1/module_application/job/tools/ap_scheduler.py +++ b/backend/app/api/v1/module_application/job/tools/ap_scheduler.py @@ -292,8 +292,15 @@ class SchedulerUtil: return await func(*args, **kwargs) else: # 对于同步函数,使用线程池执行 - loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, func, *args, **kwargs) + log.info(f"任务 {job_id} 开始执行同步函数: {func.__name__}, 参数: {args}-{kwargs}") + try: + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, func, *args, **kwargs) + log.info(f"任务 {job_id} 同步函数执行完成,结果: {result}") + return result + except Exception as e: + log.error(f"任务 {job_id} 同步函数执行失败: {str(e)}") + raise else: # 获取锁失败,记录日志 log.info(f"任务 {job_id} 获取执行锁失败,跳过本次执行") @@ -398,10 +405,17 @@ class SchedulerUtil: raise ValueError("无效的 trigger 触发器") # 5. 添加任务(使用包装器函数) + # 处理任务参数,确保空参数时返回空列表 + job_args = [] + if job_info.args: + args_str = str(job_info.args).strip() + if args_str: + job_args = args_str.split(',') + job = scheduler.add_job( func=cls._task_wrapper, trigger=trigger, - args=[str(job_info.id), job_func] + (str(job_info.args).split(',') if job_info.args else []), + args=[job_func, str(job_info.id)] + job_args, kwargs=json.loads(job_info.kwargs) if job_info.kwargs else {}, id=str(job_info.id), name=job_info.name, diff --git a/backend/app/api/v1/module_gencode/demo/schema.py b/backend/app/api/v1/module_gencode/demo/schema.py index ae49c4fe..1b001907 100644 --- a/backend/app/api/v1/module_gencode/demo/schema.py +++ b/backend/app/api/v1/module_gencode/demo/schema.py @@ -3,7 +3,6 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from fastapi import Query -from app.core.base_params import BaseQueryParam from app.core.base_schema import BaseSchema, UserBySchema from app.core.validator import DateTimeStr diff --git a/frontend/.env.development b/frontend/.env.development index adcb3357..2152d301 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -5,7 +5,7 @@ VITE_APP_ENV=development VITE_APP_TITLE=fastapiadmin # 网络请求公用地址 -VITE_API_BASE_URL=https://service.fastapiadmin.com +VITE_API_BASE_URL=http://127.0.0.1:8001 # 代理前缀 VITE_APP_BASE_API=/api/v1 diff --git a/frontend/package.json b/frontend/package.json index 1a21314b..f48d0ec6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -78,7 +78,10 @@ "element-plus": "^2.10.4", "exceljs": "^4.4.0", "file-saver": "^2.0.5", + "highlight.js": "^11.11.1", "js-beautify": "^1.15.4", + "markdown-it": "^14.1.0", + "markdown-it-highlightjs": "^4.2.0", "nprogress": "^0.2.0", "path-browserify": "^1.0.1", "path-to-regexp": "^8.2.0", @@ -98,6 +101,7 @@ "@iconify/utils": "^2.3.0", "@types/codemirror": "^5.60.16", "@types/file-saver": "^2.0.7", + "@types/markdown-it": "^14.1.2", "@types/node": "^22.16.5", "@types/nprogress": "^0.2.3", "@types/path-browserify": "^1.0.3", diff --git a/frontend/src/views/module_application/ai/index.vue b/frontend/src/views/module_application/ai/index.vue index 52963bed..92a4530b 100644 --- a/frontend/src/views/module_application/ai/index.vue +++ b/frontend/src/views/module_application/ai/index.vue @@ -81,8 +81,11 @@
+ +
+
@@ -91,7 +94,6 @@
-
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}`; + } catch { + // 忽略错误,使用默认渲染 + } + } + return `
${md.utils.escapeHtml(str)}
`; + }, +}).use(markdownItHighlightjs); + +// 配置链接在新窗口打开 +const defaultRender = + md.renderer.rules.link_open || + function (tokens: any[], idx: number, options: any, env: any, self: any) { + return self.renderToken(tokens, idx, options, env, self); + }; + +md.renderer.rules.link_open = function ( + tokens: any[], + idx: number, + options: any, + env: any, + self: any +) { + // 添加target="_blank"和rel="noopener noreferrer"属性 + tokens[idx].attrPush(["target", "_blank"]); + tokens[idx].attrPush(["rel", "noopener noreferrer"]); + return defaultRender(tokens, idx, options, env, self); +}; + // 响应式数据 const messages = ref([]); const inputMessage = ref(""); @@ -238,6 +282,13 @@ const connectWebSocket = () => { console.log("WebSocket 连接已关闭", event.code, event.reason); isConnected.value = false; connectionStatus.value = "disconnected"; + + // 结束所有加载中的助手消息 + messages.value.forEach((message) => { + if (message.type === "assistant" && message.loading) { + message.loading = false; + } + }); }; ws.onerror = (error) => { @@ -245,6 +296,13 @@ const connectWebSocket = () => { isConnected.value = false; connectionStatus.value = "disconnected"; ElMessage.error("连接失败,请检查服务器状态"); + + // 结束所有加载中的助手消息 + messages.value.forEach((message) => { + if (message.type === "assistant" && message.loading) { + message.loading = false; + } + }); }; } catch (err) { console.error("创建 WebSocket 连接失败:", err); @@ -261,6 +319,13 @@ const disconnectWebSocket = () => { } isConnected.value = false; connectionStatus.value = "disconnected"; + + // 结束所有加载中的助手消息 + messages.value.forEach((message) => { + if (message.type === "assistant" && message.loading) { + message.loading = false; + } + }); }; // 切换连接状态 @@ -279,11 +344,14 @@ const handleWebSocketMessage = (data: any) => { const lastMessage = messages.value[messages.value.length - 1]; if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) { - // 更新加载中的消息 - lastMessage.content = data.content || data.message || "收到回复"; - lastMessage.loading = false; + // 累积流式响应内容,而不是替换 + lastMessage.content += data.content || data.message || ""; + + // 保持加载状态,直到收到完整响应 + // 注意:如果后端会发送特定的结束信号,需要根据实际情况调整 + // 例如:if (data.finish_reason || data.is_complete) { lastMessage.loading = false; } } else { - // 添加新的助手消息 + // 添加新的助手消息(仅当没有加载中的助手消息时) addMessage("assistant", data.content || data.message || "收到回复"); } @@ -297,6 +365,12 @@ const sendMessage = async () => { return; } + // 结束上一条助手消息的加载状态(如果存在) + const lastMessage = messages.value[messages.value.length - 1]; + if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) { + lastMessage.loading = false; + } + // 添加用户消息 addMessage("user", message); inputMessage.value = ""; @@ -398,12 +472,8 @@ const scrollToBottom = () => { const formatMessage = (content: string) => { if (!content) return ""; - // 简单的 Markdown 支持 - return content - .replace(/\*\*(.*?)\*\*/g, "$1") - .replace(/\*(.*?)\*/g, "$1") - .replace(/`(.*?)`/g, "$1") - .replace(/\n/g, "
"); + // 使用markdown-it进行完整的Markdown渲染 + return md.render(content); }; // 生成唯一ID diff --git a/frontend/src/views/module_system/customer/index.vue b/frontend/src/views/module_system/customer/index.vue index d7cfc73d..ec7d2302 100644 --- a/frontend/src/views/module_system/customer/index.vue +++ b/frontend/src/views/module_system/customer/index.vue @@ -427,7 +427,11 @@ import DatePicker from "@/components/DatePicker/index.vue"; import type { IContentConfig } from "@/components/CURD/types"; import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue"; import { formatToDateTime } from "@/utils/dateUtil"; -import CustomerAPI, { CustomerTable, CustomerForm, CustomerPageQuery } from "@/api/module_system/customer"; +import CustomerAPI, { + CustomerTable, + CustomerForm, + CustomerPageQuery, +} from "@/api/module_system/customer"; const visible = ref(true); const queryFormRef = ref(); diff --git a/frontend/src/views/module_system/tenant/index.vue b/frontend/src/views/module_system/tenant/index.vue index 0287fa2c..3b16af47 100644 --- a/frontend/src/views/module_system/tenant/index.vue +++ b/frontend/src/views/module_system/tenant/index.vue @@ -518,7 +518,8 @@ const queryFormData = reactive({ page_size: 10, name: undefined, status: undefined, - created_time: undefined, created_id: undefined, + created_time: undefined, + created_id: undefined, }); // 编辑表单 @@ -526,7 +527,7 @@ const formData = reactive({ id: undefined, name: "", code: "", - status: '0', + status: "0", description: undefined, }); @@ -605,7 +606,7 @@ const initialFormData: TenantForm = { id: undefined, name: "", code: "", - status: '0', + status: "0", description: "", }; From 97c35b0780746c05dd3f299d92056cbc7257dc6f Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Wed, 24 Dec 2025 01:01:34 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(workflow):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=BC=96=E6=8E=92=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8VueFlow=E6=9B=BF=E6=8D=A2LogicFlow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 统一详情接口权限标识从query改为detail feat(tenant): 为租户模型添加domain字段支持 fix(cron): 移除定时任务触发参数只读限制 perf(menu): 优化菜单权限标识注释说明 refactor(model): 为多个模型添加租户关联支持 style(main): 调整CSS导入顺序和格式 chore(package): 替换前端流程图库为@vue-flow系列 fix(token): 修正令牌详情接口权限标识 docs(sql): 更新菜单权限标识注释为query格式 --- .../app/api/v1/module_application/ai/model.py | 6 +- .../v1/module_application/job/controller.py | 2 +- .../api/v1/module_application/job/model.py | 10 +- .../v1/module_application/myapp/controller.py | 2 +- .../api/v1/module_application/myapp/model.py | 6 +- .../api/v1/module_gencode/demo/controller.py | 2 +- .../app/api/v1/module_gencode/demo/model.py | 2 +- .../api/v1/module_generator/gencode/model.py | 7 +- .../v1/module_system/customer/controller.py | 2 +- .../api/v1/module_system/customer/model.py | 4 +- .../api/v1/module_system/dept/controller.py | 2 +- .../app/api/v1/module_system/dept/model.py | 6 +- .../api/v1/module_system/dict/controller.py | 4 +- .../app/api/v1/module_system/dict/model.py | 8 +- .../api/v1/module_system/log/controller.py | 2 +- backend/app/api/v1/module_system/log/model.py | 6 +- .../api/v1/module_system/menu/controller.py | 6 +- .../app/api/v1/module_system/menu/model.py | 8 +- .../api/v1/module_system/notice/controller.py | 2 +- .../app/api/v1/module_system/notice/model.py | 6 +- .../api/v1/module_system/params/controller.py | 2 +- .../app/api/v1/module_system/params/model.py | 5 +- .../v1/module_system/position/controller.py | 2 +- .../api/v1/module_system/position/model.py | 6 +- .../api/v1/module_system/role/controller.py | 2 +- .../app/api/v1/module_system/role/model.py | 6 +- .../api/v1/module_system/tenant/controller.py | 2 +- .../app/api/v1/module_system/tenant/model.py | 3 +- .../app/api/v1/module_system/tenant/schema.py | 6 +- .../api/v1/module_system/token/controller.py | 16 +- .../app/api/v1/module_system/token/crud.py | 2 +- .../app/api/v1/module_system/token/model.py | 6 +- .../api/v1/module_system/user/controller.py | 4 +- .../app/api/v1/module_system/user/model.py | 6 +- backend/app/scripts/data/sys_menu.json | 1004 ++++++++++++- backend/app/scripts/data/sys_tenant.json | 1 + .../mysql/fastapiadmin_2025-12-04_221332.sql | 2 +- .../fastapiadmin_2025-12-04_221155.sql | 2 +- frontend/.env.development | 3 +- frontend/package.json | 6 +- frontend/src/main.ts | 1 + .../views/module_application/job/index.vue | 8 +- .../workflow/CustomNode.vue | 47 + .../module_application/workflow/index.vue | 1260 ++++++----------- 44 files changed, 1561 insertions(+), 934 deletions(-) create mode 100644 frontend/src/views/module_application/workflow/CustomNode.vue diff --git a/backend/app/api/v1/module_application/ai/model.py b/backend/app/api/v1/module_application/ai/model.py index 262f3aff..63effddc 100644 --- a/backend/app/api/v1/module_application/ai/model.py +++ b/backend/app/api/v1/module_application/ai/model.py @@ -3,10 +3,10 @@ from sqlalchemy import JSON, String, Integer from sqlalchemy.orm import Mapped, mapped_column -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin -class McpModel(ModelMixin, UserMixin): +class McpModel(ModelMixin, TenantMixin, UserMixin): """ MCP 服务器表 MCP类型: @@ -15,7 +15,7 @@ class McpModel(ModelMixin, UserMixin): """ __tablename__: str = 'app_ai_mcp' __table_args__: dict[str, str] = ({'comment': 'MCP 服务器表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] name: Mapped[str] = mapped_column(String(50), comment='MCP 名称') type: Mapped[int] = mapped_column(Integer, default=0, comment='MCP 类型(0:stdio 1:sse)') diff --git a/backend/app/api/v1/module_application/job/controller.py b/backend/app/api/v1/module_application/job/controller.py index d00f57e3..f46923eb 100644 --- a/backend/app/api/v1/module_application/job/controller.py +++ b/backend/app/api/v1/module_application/job/controller.py @@ -27,7 +27,7 @@ JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/job", tags=["定 @JobRouter.get("/detail/{id}", summary="获取定时任务详情", description="获取定时任务详情") async def get_obj_detail_controller( id: int = Path(..., description="定时任务ID"), - auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_application:job:detail"])) ) -> JSONResponse: """ 获取定时任务详情 diff --git a/backend/app/api/v1/module_application/job/model.py b/backend/app/api/v1/module_application/job/model.py index aa473ea4..1376bd64 100644 --- a/backend/app/api/v1/module_application/job/model.py +++ b/backend/app/api/v1/module_application/job/model.py @@ -3,10 +3,10 @@ from sqlalchemy import Boolean, String, Integer, Text, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin -class JobModel(ModelMixin, UserMixin): +class JobModel(ModelMixin, TenantMixin, UserMixin): """ 定时任务调度表 - 0: 运行中 @@ -14,7 +14,7 @@ class JobModel(ModelMixin, UserMixin): """ __tablename__: str = 'app_job' __table_args__: dict[str, str] = ({'comment': '定时任务调度表'}) - __loader_options__: list[str] = ["job_logs", "created_by", "updated_by"] + __loader_options__: list[str] = ["job_logs", "created_by", "updated_by", "tenant"] name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='任务名称') jobstore: Mapped[str | None] = mapped_column(String(64), nullable=True, default='default', comment='存储器') @@ -36,13 +36,13 @@ class JobModel(ModelMixin, UserMixin): ) -class JobLogModel(ModelMixin): +class JobLogModel(ModelMixin, TenantMixin): """ 定时任务调度日志表 """ __tablename__: str = 'app_job_log' __table_args__: dict[str, str] = ({'comment': '定时任务调度日志表'}) - __loader_options__: list[str] = ["job"] + __loader_options__: list[str] = ["job", "tenant"] job_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='任务名称') job_group: Mapped[str] = mapped_column(String(64), nullable=False, comment='任务组名') diff --git a/backend/app/api/v1/module_application/myapp/controller.py b/backend/app/api/v1/module_application/myapp/controller.py index d69281b8..538de3ed 100644 --- a/backend/app/api/v1/module_application/myapp/controller.py +++ b/backend/app/api/v1/module_application/myapp/controller.py @@ -25,7 +25,7 @@ MyAppRouter = APIRouter(route_class=OperationLogRoute, prefix="/myapp", tags=[" @MyAppRouter.get("/detail/{id}", summary="获取应用详情", description="获取应用详情") async def get_obj_detail_controller( id: int = Path(..., description="应用ID"), - auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:detail"])) ) -> JSONResponse: """ 获取应用详情 diff --git a/backend/app/api/v1/module_application/myapp/model.py b/backend/app/api/v1/module_application/myapp/model.py index 5cfa8d51..6a5ad20b 100644 --- a/backend/app/api/v1/module_application/myapp/model.py +++ b/backend/app/api/v1/module_application/myapp/model.py @@ -3,16 +3,16 @@ from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin -class ApplicationModel(ModelMixin, UserMixin): +class ApplicationModel(ModelMixin, TenantMixin, UserMixin): """ 应用系统表 """ __tablename__: str = 'app_myapp' __table_args__: dict[str, str] = ({'comment': '应用系统表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] name: Mapped[str] = mapped_column(String(64), nullable=False, comment='应用名称') access_url: Mapped[str] = mapped_column(String(500), nullable=False, comment='访问地址') diff --git a/backend/app/api/v1/module_gencode/demo/controller.py b/backend/app/api/v1/module_gencode/demo/controller.py index 800a2ca3..c117c304 100644 --- a/backend/app/api/v1/module_gencode/demo/controller.py +++ b/backend/app/api/v1/module_gencode/demo/controller.py @@ -25,7 +25,7 @@ DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["示 @DemoRouter.get("/detail/{id}", summary="获取示例详情", description="获取示例详情") async def get_obj_detail_controller( id: int = Path(..., description="示例ID"), - auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:detail"])) ) -> JSONResponse: """ 获取示例详情 diff --git a/backend/app/api/v1/module_gencode/demo/model.py b/backend/app/api/v1/module_gencode/demo/model.py index a8ce30c0..0e9ddbde 100644 --- a/backend/app/api/v1/module_gencode/demo/model.py +++ b/backend/app/api/v1/module_gencode/demo/model.py @@ -12,6 +12,6 @@ class DemoModel(ModelMixin, UserMixin): """ __tablename__: str = 'gen_demo' __table_args__: dict[str, str] = ({'comment': '示例表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='名称') diff --git a/backend/app/api/v1/module_generator/gencode/model.py b/backend/app/api/v1/module_generator/gencode/model.py index c3ccbfae..f99f5e29 100644 --- a/backend/app/api/v1/module_generator/gencode/model.py +++ b/backend/app/api/v1/module_generator/gencode/model.py @@ -5,17 +5,16 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from sqlalchemy.sql import expression from app.config.setting import settings -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin from app.utils.common_util import SqlalchemyUtil - -class GenTableModel(ModelMixin, UserMixin): +class GenTableModel(ModelMixin, TenantMixin, UserMixin): """ 代码生成表 """ __tablename__: str = 'gen_table' __table_args__: dict[str, str] = ({'comment': '代码生成表'}) - __loader_options__: list[str] = ["columns", "created_by", "updated_by"] + __loader_options__: list[str] = ["columns", "created_by", "updated_by", "tenant"] table_name: Mapped[str] = mapped_column(String(200), nullable=False, default='', comment='表名称') table_comment: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='表描述') diff --git a/backend/app/api/v1/module_system/customer/controller.py b/backend/app/api/v1/module_system/customer/controller.py index a74813f4..e47da8cc 100644 --- a/backend/app/api/v1/module_system/customer/controller.py +++ b/backend/app/api/v1/module_system/customer/controller.py @@ -26,7 +26,7 @@ CustomerRouter = APIRouter(route_class=OperationLogRoute, prefix="/customer", ta @CustomerRouter.get("/detail/{id}", summary="获取客户详情", description="获取客户详情") async def get_obj_detail_controller( id: int = Path(..., description="客户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:customer:detail"])) ) -> JSONResponse: """ 获取客户详情 diff --git a/backend/app/api/v1/module_system/customer/model.py b/backend/app/api/v1/module_system/customer/model.py index 8b9d8ed4..0ebf8f13 100644 --- a/backend/app/api/v1/module_system/customer/model.py +++ b/backend/app/api/v1/module_system/customer/model.py @@ -9,13 +9,13 @@ if TYPE_CHECKING: from app.api.v1.module_system.user.model import UserModel -class CustomerModel(ModelMixin, UserMixin): +class CustomerModel(ModelMixin, TenantMixin, UserMixin): """ 客户表 """ __tablename__: str = 'sys_customer' __table_args__: dict[str, str] = ({'comment': '客户表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] name: Mapped[str] = mapped_column(String(64), nullable=False, comment='客户名称') code: Mapped[str] = mapped_column(String(20), nullable=False, index=True, comment='客户编码') diff --git a/backend/app/api/v1/module_system/dept/controller.py b/backend/app/api/v1/module_system/dept/controller.py index e6d48e54..d0f79d71 100644 --- a/backend/app/api/v1/module_system/dept/controller.py +++ b/backend/app/api/v1/module_system/dept/controller.py @@ -48,7 +48,7 @@ async def get_dept_tree_controller( @DeptRouter.get("/detail/{id}", summary="查询部门详情", description="查询部门详情") async def get_obj_detail_controller( id: int = Path(..., description="部门ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:dept:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:dept:detail"])) ) -> JSONResponse: """ 查询部门详情 diff --git a/backend/app/api/v1/module_system/dept/model.py b/backend/app/api/v1/module_system/dept/model.py index 7a4717a5..9bd603ef 100644 --- a/backend/app/api/v1/module_system/dept/model.py +++ b/backend/app/api/v1/module_system/dept/model.py @@ -4,19 +4,21 @@ from typing import TYPE_CHECKING from sqlalchemy import String, Integer, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column -from app.core.base_model import ModelMixin +from app.core.base_model import ModelMixin, TenantMixin if TYPE_CHECKING: from app.api.v1.module_system.role.model import RoleModel from app.api.v1.module_system.user.model import UserModel -class DeptModel(ModelMixin): +class DeptModel(ModelMixin, TenantMixin): """ 部门模型 """ __tablename__: str = "sys_dept" __table_args__: dict[str, str] = ({'comment': '部门表'}) + __loader_options__: list[str] = ["tenant"] + name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称") order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序") diff --git a/backend/app/api/v1/module_system/dict/controller.py b/backend/app/api/v1/module_system/dict/controller.py index 103ae2b6..636ad5bf 100644 --- a/backend/app/api/v1/module_system/dict/controller.py +++ b/backend/app/api/v1/module_system/dict/controller.py @@ -30,7 +30,7 @@ DictRouter = APIRouter(route_class=OperationLogRoute, prefix="/dict", tags=["字 @DictRouter.get("/type/detail/{id}", summary="获取字典类型详情", description="获取字典类型详情") async def get_type_detail_controller( id: int = Path(..., description="字典类型ID", ge=1), - auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:dict_type:detail"])) ) -> JSONResponse: """ 获取字典类型详情 @@ -224,7 +224,7 @@ async def export_type_list_controller( @DictRouter.get("/data/detail/{id}", summary="获取字典数据详情", description="获取字典数据详情") async def get_data_detail_controller( id: int = Path(..., description="字典数据ID", ge=1), - auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:dict_data:detail"])) ) -> JSONResponse: """ 获取字典数据详情 diff --git a/backend/app/api/v1/module_system/dict/model.py b/backend/app/api/v1/module_system/dict/model.py index 93a5a2bf..6c08b7cf 100644 --- a/backend/app/api/v1/module_system/dict/model.py +++ b/backend/app/api/v1/module_system/dict/model.py @@ -3,15 +3,16 @@ from sqlalchemy import String, Integer, Boolean, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from app.core.base_model import ModelMixin +from app.core.base_model import ModelMixin, TenantMixin -class DictTypeModel(ModelMixin): +class DictTypeModel(ModelMixin, TenantMixin): """ 字典类型表 """ __tablename__: str = "sys_dict_type" __table_args__: dict[str, str] = ({'comment': '字典类型表'}) + __loader_options__: list[str] = ["tenant"] dict_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='字典名称') dict_type: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, comment='字典类型') @@ -20,12 +21,13 @@ class DictTypeModel(ModelMixin): dict_data_list: Mapped[list["DictDataModel"]] = relationship("DictDataModel", back_populates="dict_type_obj", cascade="all, delete-orphan") -class DictDataModel(ModelMixin): +class DictDataModel(ModelMixin, TenantMixin): """ 字典数据表 """ __tablename__: str = "sys_dict_data" __table_args__: dict[str, str] = ({'comment': '字典数据表'}) + __loader_options__: list[str] = ["tenant"] dict_sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment='字典排序') dict_label: Mapped[str] = mapped_column(String(255), nullable=False, comment='字典标签') diff --git a/backend/app/api/v1/module_system/log/controller.py b/backend/app/api/v1/module_system/log/controller.py index 5274bb37..979be19b 100644 --- a/backend/app/api/v1/module_system/log/controller.py +++ b/backend/app/api/v1/module_system/log/controller.py @@ -48,7 +48,7 @@ async def get_obj_list_controller( @LogRouter.get("/detail/{id}", summary="日志详情", description="日志详情") async def get_obj_detail_controller( id: int = Path(..., description="操作日志ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:log:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:log:detail"])) ) -> JSONResponse: """ 获取日志详情 diff --git a/backend/app/api/v1/module_system/log/model.py b/backend/app/api/v1/module_system/log/model.py index 0bf8bcd3..931270c8 100644 --- a/backend/app/api/v1/module_system/log/model.py +++ b/backend/app/api/v1/module_system/log/model.py @@ -3,10 +3,10 @@ from sqlalchemy import String, Integer, Text from sqlalchemy.orm import Mapped, mapped_column -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin -class OperationLogModel(ModelMixin, UserMixin): +class OperationLogModel(ModelMixin, TenantMixin, UserMixin): """ 系统日志模型 日志类型: @@ -15,7 +15,7 @@ class OperationLogModel(ModelMixin, UserMixin): """ __tablename__: str = "sys_log" __table_args__: dict[str, str] = ({'comment': '系统日志表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] type: Mapped[int] = mapped_column(Integer, comment="日志类型(1登录日志 2操作日志)") request_path: Mapped[str] = mapped_column(String(255), comment="请求路径") diff --git a/backend/app/api/v1/module_system/menu/controller.py b/backend/app/api/v1/module_system/menu/controller.py index 65f3c13d..7d039339 100644 --- a/backend/app/api/v1/module_system/menu/controller.py +++ b/backend/app/api/v1/module_system/menu/controller.py @@ -43,7 +43,7 @@ async def get_menu_tree_controller( @MenuRouter.get("/detail/{id}", summary="查询菜单详情", description="查询菜单详情") async def get_obj_detail_controller( id: int = Path(..., description="菜单ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:menu:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:menu:detail"])) ) -> JSONResponse: """ 查询菜单详情。 @@ -55,8 +55,8 @@ async def get_obj_detail_controller( - JSONResponse: 包含菜单详情的 JSON 响应。 """ result_dict = await MenuService.get_menu_detail_service(id=id, auth=auth) - log.info(f"查询菜单情成功 {id}") - return SuccessResponse(data=result_dict, msg="获取菜单成功") + log.info(f"查询菜单详情成功 {id}") + return SuccessResponse(data=result_dict, msg="查询菜单详情成功") @MenuRouter.post("/create", summary="创建菜单", description="创建菜单") diff --git a/backend/app/api/v1/module_system/menu/model.py b/backend/app/api/v1/module_system/menu/model.py index 5d3c0653..9cded92e 100644 --- a/backend/app/api/v1/module_system/menu/model.py +++ b/backend/app/api/v1/module_system/menu/model.py @@ -4,13 +4,13 @@ from typing import TYPE_CHECKING from sqlalchemy import Boolean, String, Integer, JSON, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column -from app.core.base_model import ModelMixin +from app.core.base_model import ModelMixin, TenantMixin if TYPE_CHECKING: from app.api.v1.module_system.role.model import RoleModel -class MenuModel(ModelMixin): +class MenuModel(ModelMixin, TenantMixin): """ 菜单表 - 用于存储系统菜单信息 @@ -22,12 +22,12 @@ class MenuModel(ModelMixin): """ __tablename__: str = "sys_menu" __table_args__: dict[str, str] = ({'comment': '菜单表'}) - __loader_options__: list[str] = ["roles"] + __loader_options__: list[str] = ["roles", "tenant"] name: Mapped[str] = mapped_column(String(50), nullable=False, comment='菜单名称') type: Mapped[int] = mapped_column(Integer, nullable=False, default=2, comment='菜单类型(1:目录 2:菜单 3:按钮/权限 4:链接)') order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment='显示排序') - permission: Mapped[str | None] = mapped_column(String(100), comment='权限标识(如:module_system:user:list)') + permission: Mapped[str | None] = mapped_column(String(100), comment='权限标识(如:module_system:user:query)') icon: Mapped[str | None] = mapped_column(String(50), comment='菜单图标') route_name: Mapped[str | None] = mapped_column(String(100), comment='路由名称') route_path: Mapped[str | None] = mapped_column(String(200), comment='路由路径') diff --git a/backend/app/api/v1/module_system/notice/controller.py b/backend/app/api/v1/module_system/notice/controller.py index caa8ed83..de05688b 100644 --- a/backend/app/api/v1/module_system/notice/controller.py +++ b/backend/app/api/v1/module_system/notice/controller.py @@ -26,7 +26,7 @@ NoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=[ @NoticeRouter.get("/detail/{id}", summary="获取公告详情", description="获取公告详情") async def get_obj_detail_controller( id: int = Path(..., description="公告ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:notice:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:notice:detail"])) ) -> JSONResponse: """ 获取公告详情。 diff --git a/backend/app/api/v1/module_system/notice/model.py b/backend/app/api/v1/module_system/notice/model.py index 8241e144..93358c1b 100644 --- a/backend/app/api/v1/module_system/notice/model.py +++ b/backend/app/api/v1/module_system/notice/model.py @@ -3,16 +3,16 @@ from sqlalchemy import String, Text from sqlalchemy.orm import Mapped, mapped_column -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin -class NoticeModel(ModelMixin, UserMixin): +class NoticeModel(ModelMixin, TenantMixin, UserMixin): """ 通知公告表 """ __tablename__: str = "sys_notice" __table_args__: dict[str, str] = ({'comment': '通知公告表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] notice_title: Mapped[str] = mapped_column(String(64), nullable=False, comment='公告标题') notice_type: Mapped[str] = mapped_column(String(1), nullable=False, comment='公告类型(1通知 2公告)') diff --git a/backend/app/api/v1/module_system/params/controller.py b/backend/app/api/v1/module_system/params/controller.py index c100f6e0..d888a02c 100644 --- a/backend/app/api/v1/module_system/params/controller.py +++ b/backend/app/api/v1/module_system/params/controller.py @@ -22,7 +22,7 @@ ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=[" @ParamsRouter.get("/detail/{id}", summary="获取参数详情", description="获取参数详情") async def get_type_detail_controller( id: int = Path(..., description="参数ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:param:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:param:detail"])) ) -> JSONResponse: """ 获取参数详情 diff --git a/backend/app/api/v1/module_system/params/model.py b/backend/app/api/v1/module_system/params/model.py index b6ff6aff..a5bfe420 100644 --- a/backend/app/api/v1/module_system/params/model.py +++ b/backend/app/api/v1/module_system/params/model.py @@ -3,15 +3,16 @@ from sqlalchemy import String, Boolean from sqlalchemy.orm import Mapped, mapped_column -from app.core.base_model import ModelMixin +from app.core.base_model import ModelMixin, TenantMixin -class ParamsModel(ModelMixin): +class ParamsModel(ModelMixin, TenantMixin): """ 参数配置表 """ __tablename__: str = "sys_param" __table_args__: dict[str, str] = ({'comment': '系统参数表'}) + __loader_options__: list[str] = ["tenant"] config_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='参数名称') config_key: Mapped[str] = mapped_column(String(500), nullable=False, comment='参数键名') diff --git a/backend/app/api/v1/module_system/position/controller.py b/backend/app/api/v1/module_system/position/controller.py index 52883837..bad1733e 100644 --- a/backend/app/api/v1/module_system/position/controller.py +++ b/backend/app/api/v1/module_system/position/controller.py @@ -53,7 +53,7 @@ async def get_obj_list_controller( @PositionRouter.get("/detail/{id}", summary="查询岗位详情", description="查询岗位详情") async def get_obj_detail_controller( id: int = Path(..., description="岗位ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:position:query"])), + auth: AuthSchema = Depends(AuthPermission(["module_system:position:detail"])), ) -> JSONResponse: """ 查询岗位详情 diff --git a/backend/app/api/v1/module_system/position/model.py b/backend/app/api/v1/module_system/position/model.py index 46a8362a..0fe21d3f 100644 --- a/backend/app/api/v1/module_system/position/model.py +++ b/backend/app/api/v1/module_system/position/model.py @@ -4,20 +4,20 @@ from typing import TYPE_CHECKING from sqlalchemy import String, Integer from sqlalchemy.orm import relationship, Mapped, mapped_column -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin if TYPE_CHECKING: from app.api.v1.module_system.user.model import UserModel -class PositionModel(ModelMixin, UserMixin): +class PositionModel(ModelMixin, TenantMixin, UserMixin): """ 岗位模型 """ __tablename__: str = "sys_position" __table_args__: dict[str, str] = ({'comment': '岗位表'}) - __loader_options__: list[str] = ["users", "created_by", "updated_by"] + __loader_options__: list[str] = ["users", "created_by", "updated_by", "tenant"] name: Mapped[str] = mapped_column(String(64), nullable=False, comment="岗位名称") order: Mapped[int] = mapped_column(Integer, nullable=False, default=1, comment="显示排序") diff --git a/backend/app/api/v1/module_system/role/controller.py b/backend/app/api/v1/module_system/role/controller.py index e573b6c3..6423a2f1 100644 --- a/backend/app/api/v1/module_system/role/controller.py +++ b/backend/app/api/v1/module_system/role/controller.py @@ -54,7 +54,7 @@ async def get_obj_list_controller( @RoleRouter.get("/detail/{id}", summary="查询角色详情", description="查询角色详情") async def get_obj_detail_controller( id: int = Path(..., description="角色ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:role:query"])), + auth: AuthSchema = Depends(AuthPermission(["module_system:role:detail"])), ) -> JSONResponse: """ 查询角色详情 diff --git a/backend/app/api/v1/module_system/role/model.py b/backend/app/api/v1/module_system/role/model.py index 309ded13..d6c4b848 100644 --- a/backend/app/api/v1/module_system/role/model.py +++ b/backend/app/api/v1/module_system/role/model.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from sqlalchemy import String, Integer, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column -from app.core.base_model import MappedBase, ModelMixin +from app.core.base_model import MappedBase, ModelMixin, TenantMixin if TYPE_CHECKING: from app.api.v1.module_system.menu.model import MenuModel @@ -59,13 +59,13 @@ class RoleDeptsModel(MappedBase): ) -class RoleModel(ModelMixin): +class RoleModel(ModelMixin, TenantMixin): """ 角色模型 """ __tablename__: str = "sys_role" __table_args__: dict[str, str] = ({'comment': '角色表'}) - __loader_options__: list[str] = ["menus", "depts"] + __loader_options__: list[str] = ["menus", "depts", "tenant"] name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称") code: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True, comment="角色编码") diff --git a/backend/app/api/v1/module_system/tenant/controller.py b/backend/app/api/v1/module_system/tenant/controller.py index e7eccf23..b54e005a 100644 --- a/backend/app/api/v1/module_system/tenant/controller.py +++ b/backend/app/api/v1/module_system/tenant/controller.py @@ -26,7 +26,7 @@ TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=[ @TenantRouter.get("/detail/{id}", summary="获取租户详情", description="获取租户详情") async def get_obj_detail_controller( id: int = Path(..., description="租户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:detail"])) ) -> JSONResponse: """ 获取租户详情 diff --git a/backend/app/api/v1/module_system/tenant/model.py b/backend/app/api/v1/module_system/tenant/model.py index f2e9e106..e4e61c2b 100644 --- a/backend/app/api/v1/module_system/tenant/model.py +++ b/backend/app/api/v1/module_system/tenant/model.py @@ -15,7 +15,8 @@ class TenantModel(ModelMixin): name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, comment='租户名称') code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True, comment='租户编码') - + domain: Mapped[str | None] = mapped_column(String(100), unique=True, comment="租户域名") + @validates('name') def validate_name(self, key: str, name: str) -> str: """验证名称不为空""" diff --git a/backend/app/api/v1/module_system/tenant/schema.py b/backend/app/api/v1/module_system/tenant/schema.py index 2a95fda9..5371be9d 100644 --- a/backend/app/api/v1/module_system/tenant/schema.py +++ b/backend/app/api/v1/module_system/tenant/schema.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from typing import Optional from fastapi import Query from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -11,9 +10,10 @@ from app.core.validator import DateTimeStr class TenantCreateSchema(BaseModel): """新增模型""" name: str = Field(..., description='租户名称') - code: Optional[str] = Field(default=None, description='租户编码') + code: str | None = Field(default=None, description='租户编码') + domain: str | None = Field(default=None, description='租户域名') status: str = Field(default="0", description="是否启用(0:启用 1:禁用)") - description: Optional[str] = Field(default=None, description="描述") + description: str | None = Field(default=None, description="描述") @field_validator('name') @classmethod diff --git a/backend/app/api/v1/module_system/token/controller.py b/backend/app/api/v1/module_system/token/controller.py index 197501cb..0ddc8f28 100644 --- a/backend/app/api/v1/module_system/token/controller.py +++ b/backend/app/api/v1/module_system/token/controller.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -from fastapi import APIRouter, Body, Depends, Path, UploadFile -from fastapi.responses import JSONResponse, StreamingResponse -import urllib.parse +from fastapi import APIRouter, Body, Depends, Path +from fastapi.responses import JSONResponse -from app.common.response import StreamResponse, SuccessResponse +from app.common.response import SuccessResponse from app.core.router_class import OperationLogRoute -from app.utils.common_util import bytes2file_response from app.core.base_params import PaginationQueryParam from app.core.dependencies import AuthPermission from app.core.base_schema import BatchSetAvailable @@ -25,17 +23,17 @@ TokenRouter = APIRouter(route_class=OperationLogRoute, prefix="/token", tags=[" @TokenRouter.get("/detail/{id}", summary="获取令牌详情", description="获取令牌详情") async def get_obj_detail_controller( id: int = Path(..., description="令牌ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:demo:query"])) + auth: AuthSchema = Depends(AuthPermission(["module_system:token:detail"])) ) -> JSONResponse: """ - 获取示例详情 + 获取令牌详情 参数: - - id (int): 示例ID + - id (int): 令牌ID - auth (AuthSchema): 认证信息模型 返回: - - JSONResponse: 包含示例详情的JSON响应 + - JSONResponse: 包含令牌详情的JSON响应 """ result_dict = await TokenService.detail_service(id=id, auth=auth) log.info(f"获取令牌详情成功 {id}") diff --git a/backend/app/api/v1/module_system/token/crud.py b/backend/app/api/v1/module_system/token/crud.py index 1a1b75d7..23bf6eee 100644 --- a/backend/app/api/v1/module_system/token/crud.py +++ b/backend/app/api/v1/module_system/token/crud.py @@ -43,7 +43,7 @@ class TokenCRUD(CRUDBase[TokenModel, TokenCreateSchema, TokenUpdateSchema]): - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 返回: - - Sequence[DemoModel]: 示例模型实例序列 + - Sequence[TokenModel]: 示例模型实例序列 """ return await self.list(search=search, order_by=order_by, preload=preload) diff --git a/backend/app/api/v1/module_system/token/model.py b/backend/app/api/v1/module_system/token/model.py index c1028411..ced6b285 100644 --- a/backend/app/api/v1/module_system/token/model.py +++ b/backend/app/api/v1/module_system/token/model.py @@ -3,15 +3,15 @@ from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column -from app.core.base_model import ModelMixin, UserMixin +from app.core.base_model import ModelMixin, TenantMixin, UserMixin -class TokenModel(ModelMixin, UserMixin): +class TokenModel(ModelMixin, TenantMixin, UserMixin): """ 令牌表 """ __tablename__: str = 'sys_token' __table_args__: dict[str, str] = ({'comment': '令牌表'}) - __loader_options__: list[str] = ["created_by", "updated_by"] + __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='名称') diff --git a/backend/app/api/v1/module_system/user/controller.py b/backend/app/api/v1/module_system/user/controller.py index f2fc9c21..4a68f5e9 100644 --- a/backend/app/api/v1/module_system/user/controller.py +++ b/backend/app/api/v1/module_system/user/controller.py @@ -195,7 +195,7 @@ async def get_obj_list_controller( @UserRouter.get("/detail/{id}", summary="查询用户详情", description="查询用户详情") async def get_obj_detail_controller( id: int = Path(..., description="用户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:user:query"])), + auth: AuthSchema = Depends(AuthPermission(["module_system:user:detail"])), ) -> JSONResponse: """ 查询用户详情 @@ -298,7 +298,7 @@ async def batch_set_available_obj_controller( return SuccessResponse(msg="批量修改用户状态成功") -@UserRouter.post('/import/template', summary="获取用户导入模板", description="获取用户导入模板", dependencies=[Depends(AuthPermission(["module_system:user:import"]))]) +@UserRouter.post('/import/template', summary="获取用户导入模板", description="获取用户导入模板", dependencies=[Depends(AuthPermission(["module_system:user:download"]))]) async def export_obj_template_controller()-> StreamingResponse: """ 获取用户导入模板 diff --git a/backend/app/api/v1/module_system/user/model.py b/backend/app/api/v1/module_system/user/model.py index 78074250..ac9f423d 100644 --- a/backend/app/api/v1/module_system/user/model.py +++ b/backend/app/api/v1/module_system/user/model.py @@ -5,7 +5,7 @@ from datetime import datetime from sqlalchemy import Boolean, String, Integer, DateTime, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column -from app.core.base_model import MappedBase, ModelMixin, UserMixin +from app.core.base_model import MappedBase, ModelMixin, TenantMixin, UserMixin if TYPE_CHECKING: from app.api.v1.module_system.dept.model import DeptModel @@ -59,13 +59,13 @@ class UserPositionsModel(MappedBase): ) -class UserModel(ModelMixin, UserMixin): +class UserModel(ModelMixin, TenantMixin, UserMixin): """ 用户模型 """ __tablename__: str = "sys_user" __table_args__: dict[str, str] = ({'comment': '用户表'}) - __loader_options__: list[str] = ["dept", "roles", "positions", "created_by", "updated_by"] + __loader_options__: list[str] = ["dept", "roles", "positions", "created_by", "updated_by", "tenant"] username: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, comment="用户名/登录账号") password: Mapped[str] = mapped_column(String(255), nullable=False, comment="密码哈希") diff --git a/backend/app/scripts/data/sys_menu.json b/backend/app/scripts/data/sys_menu.json index 4fdc167b..228bb526 100644 --- a/backend/app/scripts/data/sys_menu.json +++ b/backend/app/scripts/data/sys_menu.json @@ -152,6 +152,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情改菜", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_system:menu:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情改菜", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询菜单", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:menu:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询菜单", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -249,6 +287,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情部门", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_system:dept:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情部门", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询部门", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:dept:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询部门", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -367,11 +443,11 @@ "description": "初始化数据" }, { - "name": "设置角色权限", + "name": "详情岗位", "type": 3, "icon": null, - "order": 8, - "permission": "module_system:role:permission", + "order": 6, + "permission": "module_system:position:detail", "route_name": null, "route_path": null, "component_path": null, @@ -379,7 +455,26 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "设置角色权限", + "title": "详情岗位", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询岗位", + "type": 3, + "icon": null, + "order": 7, + "permission": "module_system:position:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询岗位", "params": null, "affix": false, "redirect": null, @@ -486,7 +581,7 @@ "name": "角色导出", "type": 3, "icon": null, - "order": 6, + "order": 5, "permission": "module_system:role:export", "route_name": null, "route_path": null, @@ -500,6 +595,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情角色", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:role:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情角色", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询角色", + "type": 3, + "icon": null, + "order": 7, + "permission": "module_system:role:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询角色", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -635,6 +768,63 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "下载用户导入模板", + "type": 3, + "icon": null, + "order": 7, + "permission": "module_system:user:download", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "下载用户导入模板", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "详情用户", + "type": 3, + "icon": null, + "order": 8, + "permission": "module_system:user:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情用户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询用户", + "type": 3, + "icon": null, + "order": 9, + "permission": "module_system:user:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询用户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -694,6 +884,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "日志详情", + "type": 3, + "icon": null, + "order": 3, + "permission": "module_system:log:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "日志详情", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询日志", + "type": 3, + "icon": null, + "order": 4, + "permission": "module_system:log:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询日志", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -810,6 +1038,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "公告详情", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:notice:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "公告详情", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询公告", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_system:notice:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询公告", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -926,6 +1192,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "参数详情", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:param:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "参数详情", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询参数", + "type": 3, + "icon": null, + "order": 7, + "permission": "module_system:param:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询参数", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -1156,6 +1460,582 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情字典类型", + "type": 3, + "icon": null, + "order": 12, + "permission": "module_system:dict_type:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情字典类型", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询字典类型", + "type": 3, + "icon": null, + "order": 13, + "permission": "module_system:dict_type:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询字典类型", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "详情字典数据", + "type": 3, + "icon": null, + "order": 14, + "permission": "module_system:dict_data:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情字典数据", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + } + ] + }, + { + "name": "租户管理", + "type": 2, + "icon": "el-icon-Avatar", + "order": 10, + "permission": "module_system:tenant:query", + "route_name": "Tenant", + "route_path": "/system/tenant", + "component_path": "module_system/tenant/index", + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "租户管理", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据", + "children": [ + { + "name": "查询租户", + "type": 3, + "icon": null, + "order": 1, + "permission": "module_system:tenant:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询租户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "创建租户", + "type": 3, + "icon": null, + "order": 2, + "permission": "module_system:tenant:create", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "创建租户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "更新租户", + "type": 3, + "icon": null, + "order": 3, + "permission": "module_system:tenant:update", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "更新租户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "删除租户", + "type": 3, + "icon": null, + "order": 4, + "permission": "module_system:tenant:delete", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "删除租户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "批量修改租户状态", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_system:tenant:patch", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "批量修改租户状态", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "导出租户", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:tenant:export", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "导出租户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "导入租户", + "type": 3, + "icon": null, + "order": 7, + "permission": "module_system:tenant:import", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "导入租户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "下载导入租户模版", + "type": 3, + "icon": null, + "order": 8, + "permission": "module_system:tenant:download", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "下载导入租户模版", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "租户详情", + "type": 3, + "icon": null, + "order": 9, + "permission": "module_system:tenant:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "租户详情", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + } + ] + }, + { + "name": "客户管理", + "type": 2, + "icon": "el-icon-Avatar", + "order": 11, + "permission": "module_system:customer:query", + "route_name": "Customer", + "route_path": "/system/customer", + "component_path": "module_system/customer/index", + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "客户管理", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据", + "children": [ + { + "name": "查询客户", + "type": 3, + "icon": null, + "order": 1, + "permission": "module_system:customer:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询客户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "创建客户", + "type": 3, + "icon": null, + "order": 2, + "permission": "module_system:customer:create", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "创建客户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "更新客户", + "type": 3, + "icon": null, + "order": 3, + "permission": "module_system:customer:update", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "更新客户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "删除客户", + "type": 3, + "icon": null, + "order": 4, + "permission": "module_system:customer:delete", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "删除客户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "批量修改客户状态", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_system:customer:patch", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "批量修改客户状态", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "导出客户", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:customer:export", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "导出客户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "导入客户", + "type": 3, + "icon": null, + "order": 7, + "permission": "module_system:customer:import", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "导入客户", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "下载导入客户模版", + "type": 3, + "icon": null, + "order": 8, + "permission": "module_system:customer:download", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "下载导入客户模版", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "客户详情", + "type": 3, + "icon": null, + "order": 9, + "permission": "module_system:customer:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "客户详情", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + } + ] + }, + { + "name": "令牌管理", + "type": 2, + "icon": "el-icon-Avatar", + "order": 12, + "permission": "module_system:token:query", + "route_name": "Token", + "route_path": "/system/token", + "component_path": "module_system/token/index", + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "令牌管理", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据", + "children": [ + { + "name": "查询令牌", + "type": 3, + "icon": null, + "order": 1, + "permission": "module_system:token:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询令牌", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "创建令牌", + "type": 3, + "icon": null, + "order": 2, + "permission": "module_system:token:create", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "创建令牌", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "更新令牌", + "type": 3, + "icon": null, + "order": 3, + "permission": "module_system:token:update", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "更新令牌", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "删除令牌", + "type": 3, + "icon": null, + "order": 4, + "permission": "module_system:token:delete", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "删除令牌", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "批量修改令牌状态", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_system:token:patch", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "批量修改令牌状态", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "详情令牌", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_system:token:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情令牌", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] } @@ -1274,6 +2154,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情应用", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_application:myapp:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情应用", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询应用", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_application:myapp:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询应用", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -1371,6 +2289,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情定时任务", + "type": 3, + "icon": null, + "order": 5, + "permission": "module_application:job:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情任务", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询定时任务", + "type": 3, + "icon": null, + "order": 6, + "permission": "module_application:job:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询定时任务", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] }, @@ -2170,6 +3126,44 @@ "affix": false, "redirect": null, "description": "初始化数据" + }, + { + "name": "详情示例", + "type": 3, + "icon": null, + "order": 8, + "permission": "module_gencode:demo:detail", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "详情示例", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" + }, + { + "name": "查询示例", + "type": 3, + "icon": null, + "order": 9, + "permission": "module_gencode:demo:query", + "route_name": null, + "route_path": null, + "component_path": null, + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "查询示例", + "params": null, + "affix": false, + "redirect": null, + "description": "初始化数据" } ] } diff --git a/backend/app/scripts/data/sys_tenant.json b/backend/app/scripts/data/sys_tenant.json index 38439d67..d9425597 100644 --- a/backend/app/scripts/data/sys_tenant.json +++ b/backend/app/scripts/data/sys_tenant.json @@ -2,6 +2,7 @@ { "name": "运行管理平台", "code": "SYSTEM", + "domain": null, "status": "0", "start_time": null, "end_time": null, diff --git a/backend/sql/mysql/fastapiadmin_2025-12-04_221332.sql b/backend/sql/mysql/fastapiadmin_2025-12-04_221332.sql index f889883a..987345cc 100644 --- a/backend/sql/mysql/fastapiadmin_2025-12-04_221332.sql +++ b/backend/sql/mysql/fastapiadmin_2025-12-04_221332.sql @@ -480,7 +480,7 @@ CREATE TABLE `sys_menu` ( `name` varchar(50) NOT NULL COMMENT '菜单名称', `type` int NOT NULL COMMENT '菜单类型(1:目录 2:菜单 3:按钮/权限 4:链接)', `order` int NOT NULL COMMENT '显示排序', - `permission` varchar(100) DEFAULT NULL COMMENT '权限标识(如:module_system:user:list)', + `permission` varchar(100) DEFAULT NULL COMMENT '权限标识(如:module_system:user:query)', `icon` varchar(50) DEFAULT NULL COMMENT '菜单图标', `route_name` varchar(100) DEFAULT NULL COMMENT '路由名称', `route_path` varchar(200) DEFAULT NULL COMMENT '路由路径', diff --git a/backend/sql/postgres/fastapiadmin_2025-12-04_221155.sql b/backend/sql/postgres/fastapiadmin_2025-12-04_221155.sql index 29ec9648..b3acd491 100644 --- a/backend/sql/postgres/fastapiadmin_2025-12-04_221155.sql +++ b/backend/sql/postgres/fastapiadmin_2025-12-04_221155.sql @@ -1887,7 +1887,7 @@ COMMENT ON COLUMN public.sys_menu."order" IS '显示排序'; -- Name: COLUMN sys_menu.permission; Type: COMMENT; Schema: public; Owner: tao -- -COMMENT ON COLUMN public.sys_menu.permission IS '权限标识(如:module_system:user:list)'; +COMMENT ON COLUMN public.sys_menu.permission IS '权限标识(如:module_system:user:query)'; -- diff --git a/frontend/.env.development b/frontend/.env.development index 2152d301..a2945046 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -5,7 +5,8 @@ VITE_APP_ENV=development VITE_APP_TITLE=fastapiadmin # 网络请求公用地址 -VITE_API_BASE_URL=http://127.0.0.1:8001 +# VITE_API_BASE_URL=http://127.0.0.1:8001 +VITE_API_BASE_URL=https://service.fastapiadmin.com # 代理前缀 VITE_APP_BASE_API=/api/v1 diff --git a/frontend/package.json b/frontend/package.json index f48d0ec6..19439bf8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,8 +63,10 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", - "@logicflow/core": "2.2.0-alpha.3", - "@logicflow/extension": "2.2.0-alpha.3", + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "@vue-flow/minimap": "^1.5.4", "@vueuse/core": "^13.5.0", "@wangeditor-next/editor": "^5.6.49", "@wangeditor-next/editor-for-vue": "^5.1.14", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 08404a19..37178684 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -8,6 +8,7 @@ import "element-plus/dist/index.css"; // 暗黑模式自定义变量 import "@/styles/dark/css-vars.css"; import "@/styles/index.scss"; + import "uno.css"; // 过渡动画 diff --git a/frontend/src/views/module_application/job/index.vue b/frontend/src/views/module_application/job/index.vue index e44a406e..2dbd5661 100644 --- a/frontend/src/views/module_application/job/index.vue +++ b/frontend/src/views/module_application/job/index.vue @@ -545,16 +545,10 @@ - + diff --git a/frontend/src/views/module_application/workflow/CustomNode.vue b/frontend/src/views/module_application/workflow/CustomNode.vue new file mode 100644 index 00000000..0aa32d0c --- /dev/null +++ b/frontend/src/views/module_application/workflow/CustomNode.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/frontend/src/views/module_application/workflow/index.vue b/frontend/src/views/module_application/workflow/index.vue index 3fc1dffc..94044b17 100644 --- a/frontend/src/views/module_application/workflow/index.vue +++ b/frontend/src/views/module_application/workflow/index.vue @@ -1,882 +1,466 @@ - + From c7798e5eebecb3c1be9277374dc5ab259d5e8cc6 Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Thu, 25 Dec 2025 01:30:44 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor(=E6=9D=83=E9=99=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86):=20=E7=BB=9F=E4=B8=80=E8=AF=A6=E6=83=85=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A0=87=E8=AF=86=E4=BB=8Equery=E6=94=B9=E4=B8=BAdeta?= =?UTF-8?q?il?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除租户和客户管理相关模块代码 - 更新多个视图文件的权限标识 - 修改全局响应类型添加success字段 - 优化基础模型字段索引配置 --- .../api/v1/module_system/customer/__init__.py | 2 - .../v1/module_system/customer/controller.py | 215 ----- .../app/api/v1/module_system/customer/crud.py | 125 --- .../api/v1/module_system/customer/model.py | 38 - .../api/v1/module_system/customer/schema.py | 82 -- .../api/v1/module_system/customer/service.py | 312 ------- .../api/v1/module_system/tenant/__init__.py | 2 - .../api/v1/module_system/tenant/controller.py | 215 ----- .../app/api/v1/module_system/tenant/crud.py | 125 --- .../app/api/v1/module_system/tenant/model.py | 34 - .../app/api/v1/module_system/tenant/schema.py | 78 -- .../api/v1/module_system/tenant/service.py | 479 ----------- backend/app/core/base_model.py | 73 +- backend/app/scripts/data/sys_menu.json | 386 +-------- backend/app/scripts/data/sys_tenant.json | 11 - frontend/src/api/module_system/customer.ts | 101 --- frontend/src/api/module_system/tenant.ts | 102 --- frontend/src/types/global.d.ts | 1 + .../job/components/JobLogDrawer.vue | 19 +- .../views/module_application/job/index.vue | 1 + .../src/views/module_gencode/demo/index.vue | 2 +- .../views/module_system/customer/index.vue | 749 ----------------- .../src/views/module_system/dept/index.vue | 2 +- .../dict/components/DataDrawer.vue | 2 +- .../src/views/module_system/dict/index.vue | 2 +- .../src/views/module_system/log/index.vue | 2 +- .../src/views/module_system/menu/index.vue | 2 +- .../src/views/module_system/notice/index.vue | 2 +- .../src/views/module_system/param/index.vue | 2 +- .../views/module_system/position/index.vue | 2 +- .../src/views/module_system/role/index.vue | 2 +- .../src/views/module_system/tenant/index.vue | 758 ------------------ .../src/views/module_system/token/index.vue | 2 +- 33 files changed, 36 insertions(+), 3894 deletions(-) delete mode 100644 backend/app/api/v1/module_system/customer/__init__.py delete mode 100644 backend/app/api/v1/module_system/customer/controller.py delete mode 100644 backend/app/api/v1/module_system/customer/crud.py delete mode 100644 backend/app/api/v1/module_system/customer/model.py delete mode 100644 backend/app/api/v1/module_system/customer/schema.py delete mode 100644 backend/app/api/v1/module_system/customer/service.py delete mode 100644 backend/app/api/v1/module_system/tenant/__init__.py delete mode 100644 backend/app/api/v1/module_system/tenant/controller.py delete mode 100644 backend/app/api/v1/module_system/tenant/crud.py delete mode 100644 backend/app/api/v1/module_system/tenant/model.py delete mode 100644 backend/app/api/v1/module_system/tenant/schema.py delete mode 100644 backend/app/api/v1/module_system/tenant/service.py delete mode 100644 backend/app/scripts/data/sys_tenant.json delete mode 100644 frontend/src/api/module_system/customer.ts delete mode 100644 frontend/src/api/module_system/tenant.ts delete mode 100644 frontend/src/views/module_system/customer/index.vue delete mode 100644 frontend/src/views/module_system/tenant/index.vue diff --git a/backend/app/api/v1/module_system/customer/__init__.py b/backend/app/api/v1/module_system/customer/__init__.py deleted file mode 100644 index 633f8661..00000000 --- a/backend/app/api/v1/module_system/customer/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- - diff --git a/backend/app/api/v1/module_system/customer/controller.py b/backend/app/api/v1/module_system/customer/controller.py deleted file mode 100644 index e47da8cc..00000000 --- a/backend/app/api/v1/module_system/customer/controller.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- - -from fastapi import APIRouter, Body, Depends, Path, UploadFile -from fastapi.responses import JSONResponse, StreamingResponse -import urllib.parse - -from app.common.response import StreamResponse, SuccessResponse -from app.utils.common_util import bytes2file_response -from app.core.base_params import PaginationQueryParam -from app.core.dependencies import AuthPermission -from app.core.router_class import OperationLogRoute -from app.core.base_schema import BatchSetAvailable -from app.core.logger import log - -from app.api.v1.module_system.auth.schema import AuthSchema -from .service import CustomerService -from .schema import ( - CustomerCreateSchema, - CustomerUpdateSchema, - CustomerQueryParam -) - - -CustomerRouter = APIRouter(route_class=OperationLogRoute, prefix="/customer", tags=["客户模块"]) - -@CustomerRouter.get("/detail/{id}", summary="获取客户详情", description="获取客户详情") -async def get_obj_detail_controller( - id: int = Path(..., description="客户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:detail"])) -) -> JSONResponse: - """ - 获取客户详情 - - 参数: - - id (int): 客户ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含客户详情的JSON响应 - """ - result_dict = await CustomerService.detail_service(id=id, auth=auth) - log.info(f"获取客户详情成功 {id}") - return SuccessResponse(data=result_dict, msg="获取客户详情成功") - -@CustomerRouter.get("/list", summary="查询客户列表", description="查询客户列表") -async def get_obj_list_controller( - page: PaginationQueryParam = Depends(), - search: CustomerQueryParam = Depends(), - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:query"])) -) -> JSONResponse: - """ - 查询客户列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (CustomerQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含客户列表分页信息的JSON响应 - """ - # 使用数据库分页而不是应用层分页 - result_dict = await CustomerService.page_service( - auth=auth, - page_no=page.page_no if page.page_no is not None else 1, - page_size=page.page_size if page.page_size is not None else 10, - search=search, - order_by=page.order_by - ) - log.info("查询客户列表成功") - return SuccessResponse(data=result_dict, msg="查询客户列表成功") - -@CustomerRouter.post("/create", summary="创建客户", description="创建客户") -async def create_obj_controller( - data: CustomerCreateSchema, - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:create"])) -) -> JSONResponse: - """ - 创建客户 - - 参数: - - data (CustomerCreateSchema): 客户创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建客户详情的JSON响应 - """ - result_dict = await CustomerService.create_service(auth=auth, data=data) - log.info(f"创建客户成功: {result_dict.get('name')}") - return SuccessResponse(data=result_dict, msg="创建客户成功") - -@CustomerRouter.put("/update/{id}", summary="修改客户", description="修改客户") -async def update_obj_controller( - data: CustomerUpdateSchema, - id: int = Path(..., description="客户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:update"])) -) -> JSONResponse: - """ - 修改客户 - - 参数: - - data (CustomerUpdateSchema): 客户更新模型 - - id (int): 客户ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改客户详情的JSON响应 - """ - result_dict = await CustomerService.update_service(auth=auth, id=id, data=data) - log.info(f"修改客户成功: {result_dict.get('name')}") - return SuccessResponse(data=result_dict, msg="修改客户成功") - -@CustomerRouter.delete("/delete", summary="删除客户", description="删除客户") -async def delete_obj_controller( - ids: list[int] = Body(..., description="ID列表"), - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:delete"])) -) -> JSONResponse: - """ - 删除客户 - - 参数: - - ids (list[int]): 客户ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除客户详情的JSON响应 - """ - await CustomerService.delete_service(auth=auth, ids=ids) - log.info(f"删除客户成功: {ids}") - return SuccessResponse(msg="删除客户成功") - -@CustomerRouter.patch("/available/setting", summary="批量修改客户状态", description="批量修改客户状态") -async def batch_set_available_obj_controller( - data: BatchSetAvailable, - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:patch"])) -) -> JSONResponse: - """ - 批量修改客户状态 - - 参数: - - data (BatchSetAvailable): 批量修改客户状态模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量修改客户状态详情的JSON响应 - """ - await CustomerService.set_available_service(auth=auth, data=data) - log.info(f"批量修改客户状态成功: {data.ids}") - return SuccessResponse(msg="批量修改客户状态成功") - -@CustomerRouter.post('/export', summary="导出客户", description="导出客户") -async def export_obj_list_controller( - search: CustomerQueryParam = Depends(), - auth: AuthSchema = Depends(AuthPermission(["module_system:customer:export"])) -) -> StreamingResponse: - """ - 导出客户 - - 参数: - - search (CustomerQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 包含客户列表的Excel文件流响应 - """ - result_dict_list = await CustomerService.list_service(search=search, auth=auth) - export_result = await CustomerService.batch_export_service(obj_list=result_dict_list) - log.info('导出客户成功') - - return StreamResponse( - data=bytes2file_response(export_result), - media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - headers={ - 'Content-Disposition': 'attachment; filename=example.xlsx' - } - ) - -@CustomerRouter.post('/import', summary="导入客户", description="导入客户") -async def import_obj_list_controller( - file: UploadFile, - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:import"])) -) -> JSONResponse: - """ - 导入租户 - - 参数: - - file (UploadFile): 导入的Excel文件 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含导入客户详情的JSON响应 - """ - batch_import_result = await CustomerService.batch_import_service(file=file, auth=auth, update_support=True) - log.info(f"导入客户成功: {batch_import_result}") - return SuccessResponse(data=batch_import_result, msg="导入租户成功") - -@CustomerRouter.post('/download/template', summary="获取客户导入模板", description="获取客户导入模板", dependencies=[Depends(AuthPermission(["module_system:customer:download"]))]) -async def export_obj_template_controller() -> StreamingResponse: - """ - 获取租户导入模板 - - 返回: - - StreamingResponse: 包含租户导入模板的Excel文件流响应 - """ - example_import_template_result = await CustomerService.import_template_download_service() - log.info('获取客户导入模板成功') - - return StreamResponse( - data=bytes2file_response(example_import_template_result), - media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - headers={ - 'Content-Disposition': f'attachment; filename={urllib.parse.quote("客户导入模板.xlsx")}', - 'Access-Control-Expose-Headers': 'Content-Disposition' - } - ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/customer/crud.py b/backend/app/api/v1/module_system/customer/crud.py deleted file mode 100644 index c32112c7..00000000 --- a/backend/app/api/v1/module_system/customer/crud.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import Dict, List, Optional, Sequence, Union, Any - -from app.core.base_crud import CRUDBase - -from app.api.v1.module_system.auth.schema import AuthSchema -from .model import CustomerModel -from .schema import CustomerCreateSchema, CustomerUpdateSchema, CustomerOutSchema - - -class CustomerCRUD(CRUDBase[CustomerModel, CustomerCreateSchema, CustomerUpdateSchema]): - """客户数据层""" - - def __init__(self, auth: AuthSchema) -> None: - """ - 初始化CRUD数据层 - - 参数: - - auth (AuthSchema): 认证信息模型 - """ - super().__init__(model=CustomerModel, auth=auth) - - async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[CustomerModel]: - """ - 详情 - - 参数: - - id (int): 客户ID - - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 - - 返回: - - Optional[CustomerModel]: 客户模型实例或None - """ - return await self.get(id=id, preload=preload) - - async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[CustomerModel]: - """ - 列表查询 - - 参数: - - search (Optional[Dict]): 查询参数 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 - - 返回: - - Sequence[TenantModel]: 租户模型实例序列 - """ - return await self.list(search=search, order_by=order_by, preload=preload) - - async def create_crud(self, data: CustomerCreateSchema) -> Optional[CustomerModel]: - """ - 创建 - - 参数: - - data (CustomerCreateSchema): 客户创建模型 - - 返回: - - Optional[CustomerModel]: 客户模型实例或None - """ - return await self.create(data=data) - - async def update_crud(self, id: int, data: CustomerUpdateSchema) -> Optional[CustomerModel]: - """ - 更新 - - 参数: - - id (int): 客户ID - - data (CustomerUpdateSchema): 客户更新模型 - - 返回: - - Optional[CustomerModel]: 客户模型实例或None - """ - return await self.update(id=id, data=data) - - async def delete_crud(self, ids: List[int]) -> None: - """ - 批量删除 - - 参数: - - ids (List[int]): 客户ID列表 - - 返回: - - None - """ - return await self.delete(ids=ids) - - async def set_available_crud(self, ids: List[int], status: str) -> None: - """ - 批量设置可用状态 - - 参数: - - ids (List[int]): 客户ID列表 - - status (bool): 可用状态 - - 返回: - - None - """ - return await self.set(ids=ids, status=status) - - async def page_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict: - """ - 分页查询 - - 参数: - - offset (int): 偏移量 - - limit (int): 每页数量 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - search (Optional[Dict]): 查询参数 - - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 - - 返回: - - Dict: 分页数据 - """ - order_by_list = order_by or [{'id': 'asc'}] - search_dict = search or {} - - return await self.page( - offset=offset, - limit=limit, - order_by=order_by_list, - search=search_dict, - out_schema=CustomerOutSchema, - preload=preload - ) diff --git a/backend/app/api/v1/module_system/customer/model.py b/backend/app/api/v1/module_system/customer/model.py deleted file mode 100644 index 0ebf8f13..00000000 --- a/backend/app/api/v1/module_system/customer/model.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import TYPE_CHECKING -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column, relationship, validates - -from app.core.base_model import ModelMixin, UserMixin, TenantMixin -if TYPE_CHECKING: - from app.api.v1.module_system.user.model import UserModel - - -class CustomerModel(ModelMixin, TenantMixin, UserMixin): - """ - 客户表 - """ - __tablename__: str = 'sys_customer' - __table_args__: dict[str, str] = ({'comment': '客户表'}) - __loader_options__: list[str] = ["created_by", "updated_by", "tenant"] - - name: Mapped[str] = mapped_column(String(64), nullable=False, comment='客户名称') - code: Mapped[str] = mapped_column(String(20), nullable=False, index=True, comment='客户编码') - - - @validates('name') - def validate_name(self, key: str, name: str) -> str: - """验证名称不为空""" - if not name or not name.strip(): - raise ValueError('名称不能为空') - return name - - @validates('code') - def validate_code(self, key: str, code: str) -> str: - """验证编码格式校验""" - if not code or not code.strip(): - raise ValueError('编码不能为空') - if not code.isalnum(): - raise ValueError('编码只能包含字母和数字') - return code diff --git a/backend/app/api/v1/module_system/customer/schema.py b/backend/app/api/v1/module_system/customer/schema.py deleted file mode 100644 index 915ca09b..00000000 --- a/backend/app/api/v1/module_system/customer/schema.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import Optional -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator -from fastapi import Query - -from app.core.base_schema import BaseSchema, UserBySchema, TenantSchema, CustomerSchema -from app.core.validator import DateTimeStr - - -class CustomerCreateSchema(BaseModel): - """新增模型""" - name: str = Field(..., description='客户名称') - code: Optional[str] = Field(default=None, description='客户编码') - status: str = Field(default="0", description="是否启用(0:启用 1:禁用)") - description: Optional[str] = Field(default=None, description="描述") - - @field_validator('name') - @classmethod - def _validate_name(cls, v: str) -> str: - v = v.strip() - if not v: - raise ValueError('名称不能为空') - return v - - @model_validator(mode='after') - def _after_validation(self): - """ - 核心业务规则校验 - """ - # 长度校验:名称最小长度 - if len(self.name) < 2 or len(self.name) > 64: - raise ValueError('名称长度必须在2-50个字符之间') - # 格式校验:名称只能包含字母、数字、下划线和中划线 - if not self.name.isalnum() and not all(c in '-_' for c in self.name): - raise ValueError('名称只能包含字母、数字、下划线和中划线') - return self - - -class CustomerUpdateSchema(CustomerCreateSchema): - """更新模型""" - ... - - -class CustomerOutSchema(CustomerCreateSchema, BaseSchema, UserBySchema): - """响应模型""" - model_config = ConfigDict(from_attributes=True) - - -class CustomerQueryParam: - """客户查询参数""" - - def __init__( - self, - name: str | None = Query(None, description="名称"), - description: str | None = Query(None, description="描述"), - status: str | None = Query(None, description="是否启用"), - created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - created_id: int | None = Query(None, description="创建人"), - updated_id: int | None = Query(None, description="更新人") - ) -> None: - # 模糊查询字段 - self.name = ("like", name) - if description: - self.description = ("like", description) - - # 精确查询字段 - if status: - self.status = ("eq", status) - - # 时间范围查询 - if created_time and len(created_time) == 2: - self.created_time = ("between", (created_time[0], created_time[1])) - if updated_time and len(updated_time) == 2: - self.updated_time = ("between", (updated_time[0], updated_time[1])) - - # 关联查询字段 - if created_id: - self.created_id = ("eq", created_id) - if updated_id: - self.updated_id = ("eq", updated_id) diff --git a/backend/app/api/v1/module_system/customer/service.py b/backend/app/api/v1/module_system/customer/service.py deleted file mode 100644 index 70f0372f..00000000 --- a/backend/app/api/v1/module_system/customer/service.py +++ /dev/null @@ -1,312 +0,0 @@ -# -*- coding: utf-8 -*- - -import io -from typing import Any, List, Dict, Optional -from fastapi import UploadFile -import pandas as pd - -from app.api.v1.module_system.tenant.crud import TenantCRUD -from app.core.base_schema import BatchSetAvailable -from app.core.exceptions import CustomException -from app.utils.excel_util import ExcelUtil -from app.core.logger import log - -from app.api.v1.module_system.auth.schema import AuthSchema -from .schema import CustomerCreateSchema, CustomerUpdateSchema, CustomerOutSchema, CustomerQueryParam -from .crud import CustomerCRUD - - -class CustomerService: - """ - 客户管理模块服务层 - """ - - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> Dict: - """ - 详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 客户ID - - 返回: - - Dict: 客户模型实例字典 - """ - obj = await CustomerCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg="该数据不存在") - return CustomerOutSchema.model_validate(obj).model_dump() - - @classmethod - async def list_service(cls, auth: AuthSchema, search: Optional[CustomerQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]: - """ - 列表查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (Optional[CustomerQueryParam]): 查询参数 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - 返回: - - List[Dict]: 客户模型实例字典列表 - """ - search_dict = search.__dict__ if search else None - obj_list = await CustomerCRUD(auth).list_crud(search=search_dict, order_by=order_by) - return [CustomerOutSchema.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: Optional[CustomerQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict: - """ - 分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (Optional[CustomerQueryParam]): 查询参数 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - 返回: - - Dict: 分页数据 - """ - search_dict = search.__dict__ if search else {} - order_by_list = order_by or [{'id': 'asc'}] - offset = (page_no - 1) * page_size - - result = await CustomerCRUD(auth).page_crud( - offset=offset, - limit=page_size, - order_by=order_by_list, - search=search_dict - ) - return result - - @classmethod - async def create_service(cls, auth: AuthSchema, data: CustomerCreateSchema) -> Dict: - """ - 创建 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (CustomerCreateSchema): 客户创建模型 - - 返回: - - Dict: 客户模型实例字典 - """ - obj = await CustomerCRUD(auth).get(name=data.name) - if obj: - raise CustomException(msg='创建失败,名称已存在') - obj = await CustomerCRUD(auth).get(code=data.code) - if obj: - raise CustomException(msg='创建失败,编码已存在') - obj = await CustomerCRUD(auth).create_crud(data=data) - return CustomerOutSchema.model_validate(obj).model_dump() - - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: CustomerUpdateSchema) -> Dict: - """ - 更新 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 客户ID - - data (CustomerUpdateSchema): 客户更新模型 - - 返回: - - Dict: 客户模型实例字典 - """ - # 检查数据是否存在 - obj = await CustomerCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg='更新失败,该数据不存在') - - # 检查名称是否重复 - exist_obj = await CustomerCRUD(auth).get(name=data.name) - if exist_obj and exist_obj.id != id: - raise CustomException(msg='更新失败,名称重复') - - obj = await CustomerCRUD(auth).update_crud(id=id, data=data) - return CustomerOutSchema.model_validate(obj).model_dump() - - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None: - """ - 删除 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (List[int]): 客户ID列表 - - 返回: - - None - """ - if len(ids) < 1: - raise CustomException(msg='删除失败,删除对象不能为空') - - # 检查所有要删除的数据是否存在 - for id in ids: - obj = await CustomerCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') - - await CustomerCRUD(auth).delete_crud(ids=ids) - - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 批量设置状态 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (BatchSetAvailable): 批量设置状态模型 - - 返回: - - None - """ - await CustomerCRUD(auth).set_available_crud(ids=data.ids, status=data.status) - - @classmethod - async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes: - """ - 批量导出 - - 参数: - - obj_list (List[Dict[str, Any]]): 客户模型实例字典列表 - - 返回: - - bytes: Excel文件字节流 - """ - mapping_dict = { - 'id': '编号', - 'name': '名称', - 'code': '编码', - 'status': '状态', - 'description': '备注', - 'created_time': '创建时间', - 'updated_time': '更新时间', - 'creator': '创建者', - 'code': '编码', - } - - # 复制数据并转换状态 - data = obj_list.copy() - for item in data: - # 处理状态 - item['status'] = '启用' if item.get('status') == '0' else '停用' - # 处理创建者 - creator_info = item.get('creator') - if isinstance(creator_info, dict): - item['creator'] = creator_info.get('name', '未知') - else: - item['creator'] = '未知' - - 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: - """ - 批量导入 - - 参数: - - auth (AuthSchema): 认证信息模型 - - file (UploadFile): 上传的Excel文件 - - update_support (bool): 是否支持更新存在数据 - - 返回: - - str: 导入结果信息 - """ - - header_dict = { - '名称': 'name', - '状态': 'status', - '描述': 'description' - } - - try: - # 读取Excel文件 - contents = await file.read() - df = pd.read_excel(io.BytesIO(contents)) - await file.close() - - if df.empty: - raise CustomException(msg="导入文件为空") - - # 检查表头是否完整 - missing_headers = [header for header in header_dict.keys() if header not in df.columns] - if missing_headers: - raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") - - # 重命名列名 - df.rename(columns=header_dict, inplace=True) - - # 验证必填字段 - required_fields = ['name', 'status'] - for field in required_fields: - missing_rows = df[df[field].isnull()].index.tolist() - raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}行") - - error_msgs = [] - success_count = 0 - count = 0 - - # 处理每一行数据 - for index, row in df.iterrows(): - count += 1 - try: - # 数据转换前的类型检查 - try: - status = True if row['status'] == '正常' else False - except ValueError: - error_msgs.append(f"第{count}行: 状态必须是'正常'或'停用'") - continue - - # 构建用户数据 - data = { - "name": str(row['name']), - "status": status, - "description": str(row['description']), - } - - # 处理用户导入 - exists_obj = await TenantCRUD(auth).get(name=data["name"]) - if exists_obj: - if update_support: - await TenantCRUD(auth).update(id=exists_obj.id, data=data) - success_count += 1 - else: - error_msgs.append(f"第{count}行: 对象 {data['name']} 已存在") - else: - await TenantCRUD(auth).create(data=data) - success_count += 1 - - except Exception as e: - error_msgs.append(f"第{count}行: {str(e)}") - continue - - # 返回详细的导入结果 - result = f"成功导入 {success_count} 条数据" - if error_msgs: - result += "\n错误信息:\n" + "\n".join(error_msgs) - return result - - except Exception as e: - log.error(f"批量导入用户失败: {str(e)}") - raise CustomException(msg=f"导入失败: {str(e)}") - - @classmethod - async def import_template_download_service(cls) -> bytes: - """ - 下载导入模板 - - 返回: - - bytes: Excel文件字节流 - """ - header_list = ['名称', '状态', '描述'] - selector_header_list = ['状态'] - option_list = [{'状态': ['正常', '停用']}] - return ExcelUtil.get_excel_template( - header_list=header_list, - selector_header_list=selector_header_list, - option_list=option_list - ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/tenant/__init__.py b/backend/app/api/v1/module_system/tenant/__init__.py deleted file mode 100644 index 633f8661..00000000 --- a/backend/app/api/v1/module_system/tenant/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- - diff --git a/backend/app/api/v1/module_system/tenant/controller.py b/backend/app/api/v1/module_system/tenant/controller.py deleted file mode 100644 index b54e005a..00000000 --- a/backend/app/api/v1/module_system/tenant/controller.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- - -from fastapi import APIRouter, Body, Depends, Path, UploadFile -from fastapi.responses import JSONResponse, StreamingResponse -import urllib.parse - -from app.common.response import StreamResponse, SuccessResponse -from app.utils.common_util import bytes2file_response -from app.core.base_params import PaginationQueryParam -from app.core.dependencies import AuthPermission -from app.core.router_class import OperationLogRoute -from app.core.base_schema import BatchSetAvailable -from app.core.logger import log - -from app.api.v1.module_system.auth.schema import AuthSchema -from .service import TenantService -from .schema import ( - TenantCreateSchema, - TenantUpdateSchema, - TenantQueryParam -) - - -TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=["租户模块"]) - -@TenantRouter.get("/detail/{id}", summary="获取租户详情", description="获取租户详情") -async def get_obj_detail_controller( - id: int = Path(..., description="租户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:detail"])) -) -> JSONResponse: - """ - 获取租户详情 - - 参数: - - id (int): 租户ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含租户详情的JSON响应 - """ - result_dict = await TenantService.detail_service(id=id, auth=auth) - log.info(f"获取租户详情成功 {id}") - return SuccessResponse(data=result_dict, msg="获取租户详情成功") - -@TenantRouter.get("/list", summary="查询租户列表", description="查询租户列表") -async def get_obj_list_controller( - page: PaginationQueryParam = Depends(), - search: TenantQueryParam = Depends(), - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:query"])) -) -> JSONResponse: - """ - 查询租户列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (TenantQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含租户列表分页信息的JSON响应 - """ - # 使用数据库分页而不是应用层分页 - result_dict = await TenantService.page_service( - auth=auth, - page_no=page.page_no if page.page_no is not None else 1, - page_size=page.page_size if page.page_size is not None else 10, - search=search, - order_by=page.order_by - ) - log.info("查询租户列表成功") - return SuccessResponse(data=result_dict, msg="查询租户列表成功") - -@TenantRouter.post("/create", summary="创建租户", description="创建租户") -async def create_obj_controller( - data: TenantCreateSchema, - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:create"])) -) -> JSONResponse: - """ - 创建租户 - - 参数: - - data (TenantCreateSchema): 租户创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建租户详情的JSON响应 - """ - result_dict = await TenantService.create_service(auth=auth, data=data) - log.info(f"创建租户成功: {result_dict.get('name')}") - return SuccessResponse(data=result_dict, msg="创建租户成功") - -@TenantRouter.put("/update/{id}", summary="修改租户", description="修改租户") -async def update_obj_controller( - data: TenantUpdateSchema, - id: int = Path(..., description="租户ID"), - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:update"])) -) -> JSONResponse: - """ - 修改租户 - - 参数: - - data (TenantUpdateSchema): 租户更新模型 - - id (int): 租户ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改租户详情的JSON响应 - """ - result_dict = await TenantService.update_service(auth=auth, id=id, data=data) - log.info(f"修改租户成功: {result_dict.get('name')}") - return SuccessResponse(data=result_dict, msg="修改租户成功") - -@TenantRouter.delete("/delete", summary="删除租户", description="删除租户") -async def delete_obj_controller( - ids: list[int] = Body(..., description="ID列表"), - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:delete"])) -) -> JSONResponse: - """ - 删除租户 - - 参数: - - ids (list[int]): 租户ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除租户详情的JSON响应 - """ - await TenantService.delete_service(auth=auth, ids=ids) - log.info(f"删除租户成功: {ids}") - return SuccessResponse(msg="删除租户成功") - -@TenantRouter.patch("/available/setting", summary="批量修改租户状态", description="批量修改租户状态") -async def batch_set_available_obj_controller( - data: BatchSetAvailable, - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:patch"])) -) -> JSONResponse: - """ - 批量修改租户状态 - - 参数: - - data (BatchSetAvailable): 批量修改租户状态模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量修改租户状态详情的JSON响应 - """ - await TenantService.set_available_service(auth=auth, data=data) - log.info(f"批量修改租户状态成功: {data.ids}") - return SuccessResponse(msg="批量修改租户状态成功") - -@TenantRouter.post('/export', summary="导出租户", description="导出租户") -async def export_obj_list_controller( - search: TenantQueryParam = Depends(), - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:export"])) -) -> StreamingResponse: - """ - 导出租户 - - 参数: - - search (TenantQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 包含租户列表的Excel文件流响应 - """ - result_dict_list = await TenantService.list_service(search=search, auth=auth) - export_result = await TenantService.batch_export_service(obj_list=result_dict_list) - log.info('导出租户成功') - - return StreamResponse( - data=bytes2file_response(export_result), - media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - headers={ - 'Content-Disposition': 'attachment; filename=example.xlsx' - } - ) - -@TenantRouter.post('/import', summary="导入租户", description="导入租户") -async def import_obj_list_controller( - file: UploadFile, - auth: AuthSchema = Depends(AuthPermission(["module_system:tenant:import"])) -) -> JSONResponse: - """ - 导入租户 - - 参数: - - file (UploadFile): 导入的Excel文件 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含导入租户详情的JSON响应 - """ - batch_import_result = await TenantService.batch_import_service(file=file, auth=auth, update_support=True) - log.info(f"导入租户成功: {batch_import_result}") - return SuccessResponse(data=batch_import_result, msg="导入租户成功") - -@TenantRouter.post('/download/template', summary="获取租户导入模板", description="获取租户导入模板", dependencies=[Depends(AuthPermission(["module_system:tenant:download"]))]) -async def export_obj_template_controller() -> StreamingResponse: - """ - 获取租户导入模板 - - 返回: - - StreamingResponse: 包含租户导入模板的Excel文件流响应 - """ - example_import_template_result = await TenantService.import_template_download_service() - log.info('获取租户导入模板成功') - - return StreamResponse( - data=bytes2file_response(example_import_template_result), - media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - headers={ - 'Content-Disposition': f'attachment; filename={urllib.parse.quote("租户导入模板.xlsx")}', - 'Access-Control-Expose-Headers': 'Content-Disposition' - } - ) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/tenant/crud.py b/backend/app/api/v1/module_system/tenant/crud.py deleted file mode 100644 index 269104e0..00000000 --- a/backend/app/api/v1/module_system/tenant/crud.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import Dict, List, Optional, Sequence, Union, Any - -from app.core.base_crud import CRUDBase - -from app.api.v1.module_system.auth.schema import AuthSchema -from .model import TenantModel -from .schema import TenantCreateSchema, TenantUpdateSchema, TenantOutSchema - - -class TenantCRUD(CRUDBase[TenantModel, TenantCreateSchema, TenantUpdateSchema]): - """租户数据层""" - - def __init__(self, auth: AuthSchema) -> None: - """ - 初始化CRUD数据层 - - 参数: - - auth (AuthSchema): 认证信息模型 - """ - super().__init__(model=TenantModel, auth=auth) - - async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[TenantModel]: - """ - 详情 - - 参数: - - id (int): 租户ID - - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 - - 返回: - - Optional[TenantModel]: 租户模型实例或None - """ - return await self.get(id=id, preload=preload) - - async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[TenantModel]: - """ - 列表查询 - - 参数: - - search (Optional[Dict]): 查询参数 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 - - 返回: - - Sequence[TenantModel]: 租户模型实例序列 - """ - return await self.list(search=search, order_by=order_by, preload=preload) - - async def create_crud(self, data: TenantCreateSchema) -> Optional[TenantModel]: - """ - 创建 - - 参数: - - data (TenantCreateSchema): 租户创建模型 - - 返回: - - Optional[TenantModel]: 租户模型实例或None - """ - return await self.create(data=data) - - async def update_crud(self, id: int, data: TenantUpdateSchema) -> Optional[TenantModel]: - """ - 更新 - - 参数: - - id (int): 租户ID - - data (TenantUpdateSchema): 租户更新模型 - - 返回: - - Optional[TenantModel]: 租户模型实例或None - """ - return await self.update(id=id, data=data) - - async def delete_crud(self, ids: List[int]) -> None: - """ - 批量删除 - - 参数: - - ids (List[int]): 租户ID列表 - - 返回: - - None - """ - return await self.delete(ids=ids) - - async def set_available_crud(self, ids: List[int], status: str) -> None: - """ - 批量设置可用状态 - - 参数: - - ids (List[int]): 租户ID列表 - - status (bool): 可用状态 - - 返回: - - None - """ - return await self.set(ids=ids, status=status) - - async def page_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict: - """ - 分页查询 - - 参数: - - offset (int): 偏移量 - - limit (int): 每页数量 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - search (Optional[Dict]): 查询参数 - - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 - - 返回: - - Dict: 分页数据 - """ - order_by_list = order_by or [{'id': 'asc'}] - search_dict = search or {} - - return await self.page( - offset=offset, - limit=limit, - order_by=order_by_list, - search=search_dict, - out_schema=TenantOutSchema, - preload=preload - ) diff --git a/backend/app/api/v1/module_system/tenant/model.py b/backend/app/api/v1/module_system/tenant/model.py deleted file mode 100644 index e4e61c2b..00000000 --- a/backend/app/api/v1/module_system/tenant/model.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- - -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column, validates - -from app.core.base_model import ModelMixin - - -class TenantModel(ModelMixin): - """ - 租户模型 - """ - __tablename__: str = 'sys_tenant' - __table_args__: dict[str, str] = {'comment': '租户表'} - - name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, comment='租户名称') - code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True, comment='租户编码') - domain: Mapped[str | None] = mapped_column(String(100), unique=True, comment="租户域名") - - @validates('name') - def validate_name(self, key: str, name: str) -> str: - """验证名称不为空""" - if not name or not name.strip(): - raise ValueError('名称不能为空') - return name - - @validates('code') - def validate_code(self, key: str, code: str) -> str: - """验证编码格式校验""" - if not code or not code.strip(): - raise ValueError('编码不能为空') - if not code.isalnum(): - raise ValueError('编码只能包含字母和数字') - return code diff --git a/backend/app/api/v1/module_system/tenant/schema.py b/backend/app/api/v1/module_system/tenant/schema.py deleted file mode 100644 index 5371be9d..00000000 --- a/backend/app/api/v1/module_system/tenant/schema.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- - -from fastapi import Query -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator - -from app.core.base_schema import BaseSchema -from app.core.validator import DateTimeStr - - -class TenantCreateSchema(BaseModel): - """新增模型""" - name: str = Field(..., description='租户名称') - code: str | None = Field(default=None, description='租户编码') - domain: str | None = Field(default=None, description='租户域名') - status: str = Field(default="0", description="是否启用(0:启用 1:禁用)") - description: str | None = Field(default=None, description="描述") - - @field_validator('name') - @classmethod - def _validate_name(cls, v: str) -> str: - v = v.strip() - if not v: - raise ValueError('名称不能为空') - return v - - @model_validator(mode='after') - def _after_validation(self): - """ - 核心业务规则校验 - """ - # 长度校验:名称最小长度 - if len(self.name) < 2 or len(self.name) > 64: - raise ValueError('名称长度必须在2-50个字符之间') - # 格式校验:名称只能包含字母、数字、下划线和中划线 - if not self.name.isalnum() and not all(c in '-_' for c in self.name): - raise ValueError('名称只能包含字母、数字、下划线和中划线') - - return self - - -class TenantUpdateSchema(TenantCreateSchema): - """更新模型""" - ... - -class TenantOutSchema(TenantCreateSchema, BaseSchema): - """响应模型""" - model_config = ConfigDict(from_attributes=True) - - -class TenantQueryParam: - """租户查询参数""" - - def __init__( - self, - name: str | None = Query(None, description="名称"), - description: str | None = Query(None, description="描述"), - status: str | None = Query(None, description="是否启用"), - created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - created_id: int | None = Query(None, description="创建人"), - updated_id: int | None = Query(None, description="更新人") - ) -> None: - # 模糊查询字段 - self.name = ("like", name) - if description: - self.description = ("like", description) - - # 精确查询字段 - if status: - self.status = ("eq", status) - - # 时间范围查询 - if created_time and len(created_time) == 2: - self.created_time = ("between", (created_time[0], created_time[1])) - if updated_time and len(updated_time) == 2: - self.updated_time = ("between", (updated_time[0], updated_time[1])) - - diff --git a/backend/app/api/v1/module_system/tenant/service.py b/backend/app/api/v1/module_system/tenant/service.py deleted file mode 100644 index ffc19a8c..00000000 --- a/backend/app/api/v1/module_system/tenant/service.py +++ /dev/null @@ -1,479 +0,0 @@ -# -*- coding: utf-8 -*- - -import io -import random -import string -from typing import Any, List, Dict, Optional -from fastapi import UploadFile -import pandas as pd - -from app.core.base_schema import BatchSetAvailable -from app.core.exceptions import CustomException -from app.utils.excel_util import ExcelUtil -from app.core.logger import log -from app.utils.hash_bcrpy_util import PwdUtil - -from app.api.v1.module_system.auth.schema import AuthSchema -from app.api.v1.module_system.user.crud import UserCRUD -from .schema import TenantCreateSchema, TenantUpdateSchema, TenantOutSchema, TenantQueryParam -from .crud import TenantCRUD - - -class TenantService: - """ - 租户管理模块服务层 - """ - - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> Dict: - """ - 详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 租户ID - - 返回: - - Dict: 租户模型实例字典 - """ - obj = await TenantCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg="该数据不存在") - - # 获取租户详情基础数据 - result = TenantOutSchema.model_validate(obj).model_dump() - - return result - - @classmethod - async def list_service(cls, auth: AuthSchema, search: Optional[TenantQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]: - """ - 列表查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (Optional[TenantQueryParam]): 查询参数 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - 返回: - - List[Dict]: 租户模型实例字典列表 - """ - search_dict = search.__dict__ if search else None - obj_list = await TenantCRUD(auth).list_crud(search=search_dict, order_by=order_by) - return [TenantOutSchema.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: Optional[TenantQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict: - """ - 分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (Optional[TenantQueryParam]): 查询参数 - - order_by (Optional[List[Dict[str, str]]]): 排序参数 - - 返回: - - Dict: 分页数据 - """ - search_dict = search.__dict__ if search else {} - order_by_list = order_by or [{'id': 'asc'}] - offset = (page_no - 1) * page_size - - result = await TenantCRUD(auth).page_crud( - offset=offset, - limit=page_size, - order_by=order_by_list, - search=search_dict - ) - return result - - @classmethod - async def create_service(cls, auth: AuthSchema, data: TenantCreateSchema) -> Dict: - """ - 创建 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (TenantCreateSchema): 租户创建模型 - - 返回: - - Dict: 租户模型实例字典 - """ - obj = await TenantCRUD(auth).get(name=data.name) - if obj: - raise CustomException(msg='创建失败,名称已存在') - obj = await TenantCRUD(auth).get(code=data.code) - if obj: - raise CustomException(msg='创建失败,编码已存在') - - # 创建租户 - tenant_obj = await TenantCRUD(auth).create_crud(data=data) - - # 自动创建租户初始管理员用户 - await cls._create_tenant_admin_user(auth, tenant_obj) - - return TenantOutSchema.model_validate(tenant_obj).model_dump() - - @classmethod - async def _create_tenant_admin_user(cls, auth: AuthSchema, tenant_obj) -> None: - """ - 为新创建的租户自动创建初始管理员用户 - - 参数: - - auth (AuthSchema): 认证信息模型 - - tenant_obj: 租户对象 - - 返回: - - None - """ - try: - # 生成初始管理员用户名(使用租户编码) - username = f"{tenant_obj.code}_admin" - - # 生成随机密码 - password_length = 12 - characters = string.ascii_letters + string.digits + "!@#$%^&*" - password = ''.join(random.choice(characters) for _ in range(password_length)) - - # 创建管理员用户数据 - admin_user_data = { - "username": username, - "password": PwdUtil.set_password_hash(password=password), - "name": f"{tenant_obj.name}管理员", - "tenant_id": tenant_obj.id, - "user_type": "1", # 租户管理员类型 - "status": True, - "created_id": auth.user.id if auth.user else None - } - - # 创建用户 - new_user = await UserCRUD(auth).create(data=admin_user_data) - - # 记录日志,包含临时密码信息(仅开发环境记录,生产环境应避免) - log.info(f"为租户[{tenant_obj.name}]创建初始管理员用户成功,用户名: {username},临时密码: {password}") - - except Exception as e: - log.error(f"为租户[{tenant_obj.name}]创建初始管理员用户失败: {str(e)}") - # 不中断租户创建流程,仅记录错误 - pass - - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: TenantUpdateSchema) -> Dict: - """ - 更新 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 租户ID - - data (TenantUpdateSchema): 租户更新模型 - - 返回: - - Dict: 租户模型实例字典 - """ - # 系统租户特殊处理 - if id == 1: - obj = await TenantCRUD(auth).update_crud(id=id, data=data) - log.info(f"系统租户配额设置已更新") - - return TenantOutSchema.model_validate(obj).model_dump() - - # 检查数据是否存在 - obj = await TenantCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg='更新失败,该数据不存在') - - # 检查名称是否重复 - exist_obj = await TenantCRUD(auth).get(name=data.name) - if exist_obj and exist_obj.id != id: - raise CustomException(msg='更新失败,名称重复') - - obj = await TenantCRUD(auth).update_crud(id=id, data=data) - return TenantOutSchema.model_validate(obj).model_dump() - - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None: - """ - 删除 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (List[int]): 租户ID列表 - - 返回: - - None - """ - if len(ids) < 1: - raise CustomException(msg='删除失败,删除对象不能为空') - - # 系统租户保护:不允许删除系统租户(id=1) - if 1 in ids: - raise CustomException(msg='系统租户不允许删除') - - # 检查所有要删除的数据是否存在 - for id in ids: - obj = await TenantCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') - - await TenantCRUD(auth).delete_crud(ids=ids) - - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 批量设置状态 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (BatchSetAvailable): 批量设置状态模型 - - 返回: - - None - """ - # 系统租户保护:不允许禁用系统租户(id=1) - if data.status is False and 1 in data.ids: - raise CustomException(msg='系统租户不允许禁用') - - await TenantCRUD(auth).set_available_crud(ids=data.ids, status=data.status) - - @classmethod - async def batch_export_service(cls, obj_list: List[Dict[str, Any]]) -> bytes: - """ - 批量导出 - - 参数: - - obj_list (List[Dict[str, Any]]): 租户模型实例字典列表 - - 返回: - - bytes: Excel文件字节流 - """ - mapping_dict = { - 'id': '编号', - 'name': '名称', - 'code': '编码', - 'status': '状态', - 'description': '备注', - 'start_time': '开始时间', - 'end_time': '结束时间', - 'created_time': '创建时间', - 'updated_time': '更新时间', - 'created_id': '创建者', - } - - # 复制数据并转换状态 - data = obj_list.copy() - for item in data: - # 系统租户特殊标记 - if item.get('id') == 1: - item['name'] = f"{item.get('name')} [系统租户]" - - # 处理状态 - item['status'] = '启用' if item.get('status') == '0' else '停用' - - # 处理创建者 - creator_info = item.get('created_id') - if isinstance(creator_info, dict): - item['created_id'] = creator_info.get('name', '未知') - else: - item['created_id'] = '未知' - - # 限制导出数量,防止大数据量导出 - max_export_count = 1000 - if len(data) > max_export_count: - data = data[:max_export_count] - log.warning(f'导出数据超过{max_export_count}条限制,仅导出前{max_export_count}条') - - 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: - """ - 批量导入 - - 参数: - - auth (AuthSchema): 认证信息模型 - - file (UploadFile): 上传的Excel文件 - - update_support (bool): 是否支持更新存在数据 - - 返回: - - str: 导入结果信息 - """ - - header_dict = { - '名称': 'name', - '编码': 'code', - '状态': 'status', - '描述': 'description', - '开始时间': 'start_time', - '结束时间': 'end_time' - } - - try: - # 读取Excel文件 - contents = await file.read() - df = pd.read_excel(io.BytesIO(contents)) - await file.close() - - # 验证导入数量限制 - max_import_count = 100 - if len(df) > max_import_count: - raise CustomException(msg=f"单次导入不能超过{max_import_count}条数据") - - if df.empty: - raise CustomException(msg="导入文件为空") - - # 检查表头是否完整 - missing_headers = [header for header in header_dict.keys() if header not in df.columns] - if missing_headers: - raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") - - # 重命名列名 - df.rename(columns=header_dict, inplace=True) - - # 验证必填字段 - required_fields = ['name', 'code', 'status'] - for field in required_fields: - missing_rows = df[df[field].isnull()].index.tolist() - if missing_rows: - field_name = [k for k,v in header_dict.items() if v == field][0] - error_rows = [i+1 for i in missing_rows] - raise CustomException(msg=f"{field_name}不能为空,第{error_rows}行") - - error_msgs = [] - success_count = 0 - count = 0 - processed_names = set() # 用于检测重复名称 - processed_codes = set() # 用于检测重复编码 - - # 处理每一行数据 - for index, row in df.iterrows(): - count += 1 - try: - # 数据转换前的类型检查 - try: - status = True if str(row['status']).strip() == '正常' else False - except ValueError: - error_msgs.append(f"第{count}行: 状态必须是'正常'或'停用'") - continue - - # 字段格式验证 - name = str(row['name']).strip() - if len(name) < 2 or len(name) > 64: - error_msgs.append(f"第{count}行: 租户名称长度必须在2-64个字符之间") - continue - - # 检查名称是否只包含允许的字符 - if not all(c.isalnum() or c in '-_' for c in name.replace(' ', '')): - error_msgs.append(f"第{count}行: 租户名称只能包含字母、数字、下划线、中划线和空格") - continue - - # 检查导入文件内的重复名称 - if name in processed_names: - error_msgs.append(f"第{count}行: 租户名称 '{name}' 在文件中重复") - continue - processed_names.add(name) - - # 处理编码 - code = str(row['code']).strip() - if code in processed_codes: - error_msgs.append(f"第{count}行: 租户编码 '{code}' 在文件中重复") - continue - processed_codes.add(code) - - # 构建租户数据 - data = { - "name": name, - "code": code, - "status": status, - "description": str(row['description']).strip(), - } - - - # 检查时间有效性 - if 'start_time' in data and 'end_time' in data and data['start_time'] > data['end_time']: - error_msgs.append(f"第{count}行: 开始时间不能晚于结束时间") - continue - - # 处理租户导入 - exists_obj = await TenantCRUD(auth).get(name=data["name"]) - if exists_obj: - # 系统租户保护 - if exists_obj.id == 1: - error_msgs.append(f"第{count}行: 系统租户不允许修改") - continue - - if update_support: - await TenantCRUD(auth).update(id=exists_obj.id, data=data) - success_count += 1 - else: - error_msgs.append(f"第{count}行: 租户 {data['name']} 已存在") - else: - # 检查编码是否已存在 - exists_code = await TenantCRUD(auth).get(code=data["code"]) - if exists_code: - error_msgs.append(f"第{count}行: 租户编码 '{data['code']}' 已存在") - continue - - # 创建租户 - new_tenant = await TenantCRUD(auth).create(data=data) - success_count += 1 - - # 自动创建租户管理员(如果导入数量不是特别大) - if success_count < 10: # 限制自动创建管理员的数量 - await cls._create_tenant_admin_user(auth, new_tenant) - else: - log.info(f"批量导入超过10个租户,跳过自动创建管理员用户") - - except Exception as e: - error_msgs.append(f"第{count}行: {str(e)}") - continue - - # 返回详细的导入结果 - result = f"成功导入 {success_count} 条数据" - if error_msgs: - result += "\n错误信息:\n" + "\n".join(error_msgs) - # 记录错误详情到日志 - log.error(f"租户批量导入错误详情: {error_msgs}") - - log.info(f"租户批量导入完成: 成功{success_count}条, 失败{len(error_msgs)}条") - return result - - except CustomException: - raise - except Exception as e: - log.error(f"批量导入租户失败: {str(e)}") - raise CustomException(msg=f"导入失败: {str(e)}") - - @classmethod - async def import_template_download_service(cls) -> bytes: - """ - 下载导入模板 - - 返回: - - bytes: Excel文件字节流 - """ - header_list = ['名称', '编码', '状态', '描述', '开始时间', '结束时间'] - selector_header_list = ['状态'] - option_list = [{'状态': ['正常', '停用']}] - - # 添加示例数据和说明 - sample_data = [ - ['测试租户1', 'TEST001', '正常', '这是一个测试租户', '', ''], - ['测试租户2', 'TEST002', '正常', '这是另一个测试租户', '', ''] - ] - - # 添加说明文本 - description = """导入说明: -1. 名称和编码为必填项,名称长度2-64个字符 -2. 编码如果不填写,系统会自动生成 -3. 状态只能选择'正常'或'停用' -4. 时间格式:YYYY-MM-DD HH:MM:SS或YYYY-MM-DD -5. 单次导入最多支持100条数据 -""" - - return ExcelUtil.get_excel_template( - header_list=header_list, - selector_header_list=selector_header_list, - option_list=option_list - ) \ No newline at end of file diff --git a/backend/app/core/base_model.py b/backend/app/core/base_model.py index a8547e29..c365eb62 100644 --- a/backend/app/core/base_model.py +++ b/backend/app/core/base_model.py @@ -9,8 +9,6 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from app.api.v1.module_system.user.model import UserModel - from app.api.v1.module_system.customer.model import CustomerModel - from app.api.v1.module_system.tenant.model import TenantModel from app.utils.common_util import uuid4_str @@ -61,12 +59,12 @@ class ModelMixin(MappedBase): __abstract__: bool = True # 基础字段 - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID') - uuid: Mapped[str] = mapped_column(String(64), default=uuid4_str, nullable=False, unique=True, comment='UUID全局唯一标识') - status: Mapped[str] = mapped_column(String(10), default='0', nullable=False, comment="是否启用(0:启用 1:禁用)") - description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注/描述") - created_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False, comment='创建时间') - updated_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间') + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID', index=True) + uuid: Mapped[str] = mapped_column(String(64), default=uuid4_str, nullable=False, unique=True, comment='UUID全局唯一标识', index=True) + status: Mapped[str] = mapped_column(String(10), default='0', nullable=False, comment="是否启用(0:启用 1:禁用)", index=True) + description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注/描述", index=True) + created_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=False, comment='创建时间', index=True) + updated_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间', index=True) class UserMixin(MappedBase): @@ -118,62 +116,3 @@ class UserMixin(MappedBase): foreign_keys=lambda: cls.updated_id, uselist=False ) - - -class TenantMixin(MappedBase): - """ - 租户字段 Mixin - """ - __abstract__: bool = True - - tenant_id: Mapped[int | None] = mapped_column( - Integer, - ForeignKey('sys_tenant.id', ondelete="CASCADE", onupdate="CASCADE"), - nullable=False, - index=True, - comment="所属租户ID" - ) - - @declared_attr - def tenant(cls) -> Mapped["TenantModel"]: - """ - 租户关联关系(延迟加载,避免循环依赖) - """ - return relationship( - "TenantModel", - primaryjoin=f"{cls.__name__}.tenant_id == TenantModel.id", - lazy="selectin", - foreign_keys=lambda: [cls.tenant_id], - viewonly=True, - uselist=False - ) - - -class CustomerMixin(MappedBase): - """ - 客户隔离字段 Mixin - """ - __abstract__: bool = True - - customer_id: Mapped[int | None] = mapped_column( - Integer, - ForeignKey('sys_customer.id', ondelete="CASCADE", onupdate="CASCADE"), - default=None, - nullable=True, - index=True, - comment="所属客户ID(NULL表示租户级数据,>0表示客户级数据)" - ) - - @declared_attr - def customer(cls) -> Mapped["CustomerModel"]: - """ - 客户关联关系(延迟加载,避免循环依赖) - """ - return relationship( - "CustomerModel", - primaryjoin=f"{cls.__name__}.customer_id == CustomerModel.id", - lazy="selectin", - foreign_keys=lambda: [cls.customer_id], - viewonly=True, - uselist=False - ) \ No newline at end of file diff --git a/backend/app/scripts/data/sys_menu.json b/backend/app/scripts/data/sys_menu.json index 228bb526..3001067a 100644 --- a/backend/app/scripts/data/sys_menu.json +++ b/backend/app/scripts/data/sys_menu.json @@ -1520,395 +1520,11 @@ } ] }, - { - "name": "租户管理", - "type": 2, - "icon": "el-icon-Avatar", - "order": 10, - "permission": "module_system:tenant:query", - "route_name": "Tenant", - "route_path": "/system/tenant", - "component_path": "module_system/tenant/index", - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "租户管理", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据", - "children": [ - { - "name": "查询租户", - "type": 3, - "icon": null, - "order": 1, - "permission": "module_system:tenant:query", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "查询租户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "创建租户", - "type": 3, - "icon": null, - "order": 2, - "permission": "module_system:tenant:create", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "创建租户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "更新租户", - "type": 3, - "icon": null, - "order": 3, - "permission": "module_system:tenant:update", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "更新租户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "删除租户", - "type": 3, - "icon": null, - "order": 4, - "permission": "module_system:tenant:delete", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "删除租户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "批量修改租户状态", - "type": 3, - "icon": null, - "order": 5, - "permission": "module_system:tenant:patch", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "批量修改租户状态", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "导出租户", - "type": 3, - "icon": null, - "order": 6, - "permission": "module_system:tenant:export", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "导出租户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "导入租户", - "type": 3, - "icon": null, - "order": 7, - "permission": "module_system:tenant:import", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "导入租户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "下载导入租户模版", - "type": 3, - "icon": null, - "order": 8, - "permission": "module_system:tenant:download", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "下载导入租户模版", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "租户详情", - "type": 3, - "icon": null, - "order": 9, - "permission": "module_system:tenant:detail", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "租户详情", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - } - ] - }, - { - "name": "客户管理", - "type": 2, - "icon": "el-icon-Avatar", - "order": 11, - "permission": "module_system:customer:query", - "route_name": "Customer", - "route_path": "/system/customer", - "component_path": "module_system/customer/index", - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "客户管理", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据", - "children": [ - { - "name": "查询客户", - "type": 3, - "icon": null, - "order": 1, - "permission": "module_system:customer:query", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "查询客户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "创建客户", - "type": 3, - "icon": null, - "order": 2, - "permission": "module_system:customer:create", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "创建客户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "更新客户", - "type": 3, - "icon": null, - "order": 3, - "permission": "module_system:customer:update", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "更新客户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "删除客户", - "type": 3, - "icon": null, - "order": 4, - "permission": "module_system:customer:delete", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "删除客户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "批量修改客户状态", - "type": 3, - "icon": null, - "order": 5, - "permission": "module_system:customer:patch", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "批量修改客户状态", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "导出客户", - "type": 3, - "icon": null, - "order": 6, - "permission": "module_system:customer:export", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "导出客户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "导入客户", - "type": 3, - "icon": null, - "order": 7, - "permission": "module_system:customer:import", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "导入客户", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "下载导入客户模版", - "type": 3, - "icon": null, - "order": 8, - "permission": "module_system:customer:download", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "下载导入客户模版", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "客户详情", - "type": 3, - "icon": null, - "order": 9, - "permission": "module_system:customer:detail", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "客户详情", - "params": null, - "affix": false, - "redirect": null, - "description": "初始化数据" - } - ] - }, { "name": "令牌管理", "type": 2, "icon": "el-icon-Avatar", - "order": 12, + "order": 10, "permission": "module_system:token:query", "route_name": "Token", "route_path": "/system/token", diff --git a/backend/app/scripts/data/sys_tenant.json b/backend/app/scripts/data/sys_tenant.json deleted file mode 100644 index d9425597..00000000 --- a/backend/app/scripts/data/sys_tenant.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "name": "运行管理平台", - "code": "SYSTEM", - "domain": null, - "status": "0", - "start_time": null, - "end_time": null, - "description": "系统内置租户,用于管理平台全局配置和所有租户" - } -] \ No newline at end of file diff --git a/frontend/src/api/module_system/customer.ts b/frontend/src/api/module_system/customer.ts deleted file mode 100644 index b19cc95a..00000000 --- a/frontend/src/api/module_system/customer.ts +++ /dev/null @@ -1,101 +0,0 @@ -import request from "@/utils/request"; - -const API_PATH = "/system/customer"; - -const CustomerAPI = { - listCustomer(query: CustomerPageQuery) { - return request>>({ - url: `${API_PATH}/list`, - method: "get", - params: query, - }); - }, - - detailCustomer(query: number) { - return request>({ - url: `${API_PATH}/detail/${query}`, - method: "get", - }); - }, - - createCustomer(body: CustomerForm) { - return request({ - url: `${API_PATH}/create`, - method: "post", - data: body, - }); - }, - - updateCustomer(id: number, body: CustomerForm) { - return request({ - url: `${API_PATH}/update/${id}`, - method: "put", - data: body, - }); - }, - - deleteCustomer(body: number[]) { - return request({ - url: `${API_PATH}/delete`, - method: "delete", - data: body, - }); - }, - - batchCustomer(body: BatchType) { - return request({ - url: `${API_PATH}/available/setting`, - method: "patch", - data: body, - }); - }, - - exportCustomer(body: CustomerPageQuery) { - return request({ - url: `${API_PATH}/export`, - method: "post", - data: body, - responseType: "blob", - }); - }, - - downloadCustomer() { - return request({ - url: `${API_PATH}/download/template`, - method: "post", - responseType: "blob", - }); - }, - - importCustomer(body: FormData) { - return request({ - url: `${API_PATH}/import`, - method: "post", - data: body, - headers: { - "Content-Type": "multipart/form-data", - }, - }); - }, -}; - -export default CustomerAPI; - -export interface CustomerPageQuery extends PageQuery { - name?: string; - status?: string; - created_time?: string[]; -} - -export interface CustomerTable extends BaseType { - name?: string; - code?: string; - created_by?: CommonType; - updated_by?: CommonType; - tenant?: CommonType; -} - -export interface CustomerForm extends BaseFormType { - name?: string; - code?: string; -} diff --git a/frontend/src/api/module_system/tenant.ts b/frontend/src/api/module_system/tenant.ts deleted file mode 100644 index bc6be9a9..00000000 --- a/frontend/src/api/module_system/tenant.ts +++ /dev/null @@ -1,102 +0,0 @@ -import request from "@/utils/request"; - -const API_PATH = "/system/tenant"; - -const TenantAPI = { - listTenant(query: TenantPageQuery) { - return request>>({ - url: `${API_PATH}/list`, - method: "get", - params: query, - }); - }, - - detailTenant(query: number) { - return request>({ - url: `${API_PATH}/detail/${query}`, - method: "get", - }); - }, - - createTenant(body: TenantForm) { - return request({ - url: `${API_PATH}/create`, - method: "post", - data: body, - }); - }, - - updateTenant(id: number, body: TenantForm) { - return request({ - url: `${API_PATH}/update/${id}`, - method: "put", - data: body, - }); - }, - - deleteTenant(body: number[]) { - return request({ - url: `${API_PATH}/delete`, - method: "delete", - data: body, - }); - }, - - batchTenant(body: BatchType) { - return request({ - url: `${API_PATH}/available/setting`, - method: "patch", - data: body, - }); - }, - - exportTenant(body: TenantPageQuery) { - return request({ - url: `${API_PATH}/export`, - method: "post", - data: body, - responseType: "blob", - }); - }, - - downloadTenant() { - return request({ - url: `${API_PATH}/download/template`, - method: "post", - responseType: "blob", - }); - }, - - importTenant(body: FormData) { - return request({ - url: `${API_PATH}/import`, - method: "post", - data: body, - headers: { - "Content-Type": "multipart/form-data", - }, - }); - }, -}; - -export default TenantAPI; - -export interface TenantPageQuery extends PageQuery { - name?: string; - status?: string; - created_time?: string[]; -} - -export interface TenantTable extends BaseType { - name?: string; - code?: string; - start_time?: string; - end_time?: string; -} - -export interface TenantForm extends BaseFormType { - name?: string; - code?: string; - start_time?: string; - end_time?: string; -} diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts index ccc29a34..d0fd58f3 100644 --- a/frontend/src/types/global.d.ts +++ b/frontend/src/types/global.d.ts @@ -7,6 +7,7 @@ declare global { data: T; msg: string; status_code: number; + success: boolean; } /** diff --git a/frontend/src/views/module_application/job/components/JobLogDrawer.vue b/frontend/src/views/module_application/job/components/JobLogDrawer.vue index 52168274..7f170947 100644 --- a/frontend/src/views/module_application/job/components/JobLogDrawer.vue +++ b/frontend/src/views/module_application/job/components/JobLogDrawer.vue @@ -31,8 +31,21 @@ - 查询 - 重置 + + 查询 + + + 重置 +