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/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/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/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/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_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/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/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/schema.py b/backend/app/api/v1/module_gencode/demo/schema.py index 49297825..1b001907 100644 --- a/backend/app/api/v1/module_gencode/demo/schema.py +++ b/backend/app/api/v1/module_gencode/demo/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 DemoCreateSchema(BaseModel): @@ -39,7 +39,6 @@ class DemoCreateSchema(BaseModel): # 描述校验:描述最大长度 if self.description and len(self.description) > 255: raise ValueError('描述长度不能超过255个字符') - return self @@ -55,27 +54,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/model.py b/backend/app/api/v1/module_generator/gencode/model.py index c3ccbfae..d36e9a40 100644 --- a/backend/app/api/v1/module_generator/gencode/model.py +++ b/backend/app/api/v1/module_generator/gencode/model.py @@ -8,7 +8,6 @@ from app.config.setting import settings from app.core.base_model import ModelMixin, UserMixin from app.utils.common_util import SqlalchemyUtil - class GenTableModel(ModelMixin, UserMixin): """ 代码生成表 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/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 34db9048..2d1e9935 100644 --- a/backend/app/api/v1/module_system/dept/model.py +++ b/backend/app/api/v1/module_system/dept/model.py @@ -4,23 +4,25 @@ 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, UserMixin 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, UserMixin): """ 部门模型 """ __tablename__: str = "sys_dept" __table_args__: dict[str, str] = ({'comment': '部门表'}) + __loader_options__: list[str] = [] - 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/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 046684da..7cd7a386 100644 --- a/backend/app/api/v1/module_system/dict/model.py +++ b/backend/app/api/v1/module_system/dict/model.py @@ -12,8 +12,9 @@ class DictTypeModel(ModelMixin): """ __tablename__: str = "sys_dict_type" __table_args__: dict[str, str] = ({'comment': '字典类型表'}) + __loader_options__: list[str] = [] - 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='字典类型') # 关系定义 @@ -26,6 +27,7 @@ class DictDataModel(ModelMixin): """ __tablename__: str = "sys_dict_data" __table_args__: dict[str, str] = ({'comment': '字典数据表'}) + __loader_options__: list[str] = [] 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/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/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/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/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..6d0275d0 100644 --- a/backend/app/api/v1/module_system/menu/model.py +++ b/backend/app/api/v1/module_system/menu/model.py @@ -27,7 +27,7 @@ class MenuModel(ModelMixin): 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/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/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 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/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 fb79bfa7..debfe3fa 100644 --- a/backend/app/api/v1/module_system/params/model.py +++ b/backend/app/api/v1/module_system/params/model.py @@ -12,8 +12,9 @@ class ParamsModel(ModelMixin): """ __tablename__: str = "sys_param" __table_args__: dict[str, str] = ({'comment': '系统参数表'}) + __loader_options__: list[str] = [] - 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/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 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/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 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/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 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..c365eb62 100644 --- a/backend/app/core/base_model.py +++ b/backend/app/core/base_model.py @@ -59,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): 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..3b45302d 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,9 @@ 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 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/dependencies.py b/backend/app/core/dependencies.py index 2a27cc11..0042f631 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -16,7 +16,6 @@ from app.core.security import OAuth2Schema, decode_access_token from app.core.logger import log from app.api.v1.module_system.user.model import UserModel -from app.api.v1.module_system.role.model import RoleModel from app.api.v1.module_system.user.crud import UserCRUD from app.api.v1.module_system.auth.schema import AuthSchema 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/app/scripts/data/sys_menu.json b/backend/app/scripts/data/sys_menu.json index 4fdc167b..88b0d27a 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,63 @@ "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": "初始化数据" } ] } @@ -1274,6 +1635,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 +1770,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 +2607,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 deleted file mode 100644 index 38439d67..00000000 --- a/backend/app/scripts/data/sys_tenant.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "name": "运行管理平台", - "code": "SYSTEM", - "status": "0", - "start_time": null, - "end_time": null, - "description": "系统内置租户,用于管理平台全局配置和所有租户" - } -] \ No newline at end of file diff --git a/backend/docs/业务逻辑.md b/backend/docs/业务逻辑.md deleted file mode 100644 index 3b72d3cb..00000000 --- a/backend/docs/业务逻辑.md +++ /dev/null @@ -1,277 +0,0 @@ -# FastAPI Admin 业务逻辑 - -## 1. 项目概述 - -FastAPI Admin 是一个基于 FastAPI 构建的企业级后台管理系统框架,提供完整的 RBAC 权限控制体系、多租户架构支持、任务调度系统和日志监控功能。 - -## 2. 技术架构 - -### 2.1 核心技术栈 - -| 技术 | 版本 | 说明 | -|------|------|------| -| FastAPI | 0.115.2 | 现代 Web 框架 | -| SQLAlchemy | 2.0.36 | ORM 框架 | -| Alembic | 1.15.1 | 数据库迁移工具 | -| Pydantic | 2.x | 数据验证与序列化 | -| APScheduler | 3.11.0 | 定时任务调度 | -| Redis | 5.2.1 | 缓存与会话存储 | -| Uvicorn | 0.30.6 | ASGI 服务器 | -| Python | 3.10+ | 运行环境 | - -### 2.2 多数据库支持 - -- MySQL -- PostgreSQL -- SQLite - -### 2.3 架构设计 - -采用经典的 MVC 分层架构: - -```sh -📦 分层架构 (MVC) -├── 🎯 Controller # 控制器层 - 处理HTTP请求 -├── 🏢 Service # 业务层 - 核心业务逻辑 -├── 💾 CRUD # 数据访问层 - 数据库操作 -└── 📊 Model # 模型层 - 数据模型定义 -``` - -## 3. 核心业务模块 - -### 3.1 系统管理模块 (module_system) - -#### 3.1.1 用户管理 - -- 支持不同类型用户:系统用户、租户管理员、租户普通用户、客户用户 -- 实现用户与角色、岗位的多对多关联 -- 提供用户状态管理、密码重置等功能 - -#### 3.1.2 角色管理 - -- 完整的 RBAC 权限体系 -- 支持菜单权限和数据权限双重控制 -- 自定义数据权限范围设置 - -#### 3.1.3 菜单管理 - -- 树形菜单结构设计 -- 支持目录、菜单、按钮/权限三种类型 -- 系统级菜单与租户级菜单隔离 - -#### 3.1.4 部门管理 - -- 树形部门结构 -- 支持无限层级嵌套 -- 部门负责人设置 - -#### 3.1.5 租户管理 - -- 多租户 SaaS 架构 -- 租户数据完全隔离 -- 系统租户管理所有普通租户 - -#### 3.1.6 客户管理 - -- 租户内部的二级业务单元 -- 客户数据隔离 -- 客户用户权限控制 - -### 3.2 系统监控模块 (module_monitor) - -- 操作日志记录与查询 -- 系统资源监控 -- 性能指标统计 - -### 3.3 AI 功能模块 (module_ai) - -- OpenAI 大模型集成 -- AI 辅助功能 - -### 3.4 定时任务模块 (module_task) - -- 基于 APScheduler 的任务调度 -- 支持多种任务类型 -- 任务执行日志记录 - -## 4. 数据模型与关系 - -### 4.1 核心数据模型 - -#### 4.1.1 UserModel (用户模型) - -- 表名:`sys_user` -- 关键字段: - - username: 用户名/登录账号 - - name: 昵称 - - dept_id: 所属部门 - -#### 4.1.2 RoleModel (角色模型) - -- 表名:`sys_role` -- 关键字段: - - name: 角色名称 - - code: 角色编码 - - data_scope: 数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义) - -#### 4.1.3 MenuModel (菜单模型) - -- 表名:`sys_menu` -- 关键字段: - - name: 菜单名称 - - type: 菜单类型(1:目录 2:菜单 3:按钮/权限 4:链接) - - permission: 权限标识 - - route_path: 路由路径 - - parent_id: 父菜单ID - -#### 4.1.4 DeptModel (部门模型) - -- 表名:`sys_dept` -- 关键字段: - - name: 部门名称 - - code: 部门编码 - - leader: 部门负责人 - - parent_id: 父级部门ID - -### 4.2 关联关系表 - -#### 4.2.1 UserRolesModel (用户角色关联) - -- 表名:`sys_user_roles` -- 关键字段:user_id, role_id - -#### 4.2.2 RoleMenusModel (角色菜单关联) - -- 表名:`sys_role_menus` -- 关键字段:role_id, menu_id - -#### 4.2.3 RoleDeptsModel (角色部门关联) - -- 表名:`sys_role_depts` -- 关键字段:role_id, dept_id - -#### 4.2.4 UserPositionsModel (用户岗位关联) - -- 表名:`sys_user_positions` -- 关键字段:user_id, position_id - -## 5. 数据隔离与权限体系 - -### 5.2 数据权限机制 - -#### 5.2.1 权限范围定义 - -- 1(仅本人): `WHERE created_id = current_user.id` -- 2(本部门): `WHERE user.dept_id = current_user.dept_id` -- 3(本部门及以下): -- 4(全部数据): -- 5(自定义): `WHERE dept_id IN (SELECT dept_id FROM role_depts WHERE role_id IN current_user.role_ids)` - -#### 5.2.2 权限叠加规则 - -- 一个用户可以有多个角色 -- 取所有角色data_scope的最大值(4>3>2>5>1) -- 5(自定义)需要合并所有角色关联的部门 - -## 6. 核心业务流程 - -### 6.1 用户登录流程 - -1. 用户提交用户名密码 -2. 系统验证用户凭据 -3. 加载用户角色和权限信息 -4. 生成JWT Token -5. 返回用户信息和权限列表 - -### 6.2 权限验证流程 - -1. 用户请求受保护的API接口 -2. 中间件验证JWT Token -3. 加载用户权限列表 -4. 验证用户是否拥有该接口的权限 -5. 根据数据权限过滤查询结果 - -### 6.3 数据权限过滤流程 - -1. 获取当前用户角色的data_scope -2. 根据data_scope构建SQL过滤条件 -3. 将过滤条件应用到数据库查询 -4. 返回符合权限要求的数据 - -## 7. 特色功能 - -### 7.1 智能代码生成 - -- 基于模板的代码生成工具 -- 支持模型、CRUD、API接口等自动生成 -- 提高开发效率 - -### 7.2 AI 集成 - -- OpenAI 大模型调用接口 -- 支持在管理系统中集成AI功能 - -### 7.3 云存储支持 - -- 阿里云 OSS 对象存储集成 -- 支持文件上传、下载、管理 - -### 7.4 任务调度系统 - -- 基于APScheduler的定时任务 -- 支持多种调度策略 -- 任务执行状态监控 - -## 8. 系统安全 - -### 8.1 认证与授权 - -- JWT Token认证 -- 细粒度的RBAC权限控制 -- 密码加密存储(bcrypt) - -### 8.2 数据安全 - -- 多级别数据隔离 -- 操作日志记录 -- 敏感数据加密存储 - -### 8.3 接口安全 - -- 请求参数验证(Pydantic) -- 接口限流保护 -- CORS配置管理 - -## 9. 部署与维护 - -### 9.1 环境要求 - -- Python 3.10+ -- 数据库: MySQL 8.0+ / PostgreSQL 13+ / SQLite 3.x -- Redis 6.0+ - -### 9.2 数据库迁移 - -- 使用Alembic管理数据库版本 -- 支持多数据库平台迁移 -- 提供初始化脚本 - -### 9.3 日志系统 - -- 操作日志记录 -- 系统日志监控 -- 异常记录与告警 - -## 10. 扩展开发 - -### 10.1 模块扩展 - -- 标准化的模块开发流程 -- 支持自定义业务模块 -- 插件化架构设计 - -### 10.2 数据模型扩展 - -- 基于SQLAlchemy 2.0的声明式模型 -- 支持模型继承与混入 -- 兼容多数据库平台 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/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/package.json b/frontend/package.json index 96a5dddc..19439bf8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,6 +63,10 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", + "@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", @@ -76,7 +80,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", @@ -96,6 +103,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/App.vue b/frontend/src/App.vue index 21f4d667..7a5f3ff6 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,36 +8,23 @@ class="wh-full" > - - - - - 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/composables/ai/useAiAction.ts b/frontend/src/composables/ai/useAiAction.ts deleted file mode 100644 index 1b505716..00000000 --- a/frontend/src/composables/ai/useAiAction.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { useRoute } from "vue-router"; -import { ElMessage, ElMessageBox } from "element-plus"; -import { onMounted, onBeforeUnmount, nextTick } from "vue"; -import AiCommandApi from "@/api/ai"; - -/** - * AI 操作处理器(简化版) - * - * 可以是简单函数,也可以是配置对象 - */ -export type AiActionHandler = - | ((args: T) => Promise | void) - | { - /** 执行函数 */ - execute: (args: T) => Promise | void; - /** 是否需要确认(默认 true) */ - needConfirm?: boolean; - /** 确认消息(支持函数或字符串) */ - confirmMessage?: string | ((args: T) => string); - /** 成功消息(支持函数或字符串) */ - successMessage?: string | ((args: T) => string); - /** 是否调用后端 API(默认 false,如果为 true 则自动调用 executeCommand) */ - callBackendApi?: boolean; - }; - -/** - * AI 操作配置 - */ -export interface UseAiActionOptions { - /** 操作映射表:函数名 -> 处理器 */ - actionHandlers?: Record; - /** 数据刷新函数(操作完成后调用) */ - onRefresh?: () => Promise | void; - /** 自动搜索处理函数 */ - onAutoSearch?: (keywords: string) => void; - /** 当前路由路径(用于执行命令时传递) */ - currentRoute?: string; -} - -/** - * AI 操作 Composable - * - * 统一处理 AI 助手传递的操作,支持: - * - 自动搜索(通过 keywords + autoSearch 参数) - * - 执行 AI 操作(通过 aiAction 参数) - * - 配置化的操作处理器 - */ -export function useAiAction(options: UseAiActionOptions = {}) { - const route = useRoute(); - const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options; - - // 用于跟踪是否已卸载,防止在卸载后执行回调 - let isUnmounted = false; - - /** - * 执行 AI 操作(统一处理确认、执行、反馈流程) - */ - async function executeAiAction(action: any) { - if (isUnmounted) return; - - // 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } } - const fnCall = action.functionCall ?? { - name: action.functionName, - arguments: action.arguments, - }; - - if (!fnCall?.name) { - ElMessage.warning("未识别的 AI 操作"); - return; - } - - // 查找对应的处理器 - const handler = actionHandlers[fnCall.name]; - if (!handler) { - ElMessage.warning(`暂不支持操作: ${fnCall.name}`); - return; - } - - try { - // 判断处理器类型(函数 or 配置对象) - const isSimpleFunction = typeof handler === "function"; - - if (isSimpleFunction) { - // 简单函数形式:直接执行 - await handler(fnCall.arguments); - } else { - // 配置对象形式:统一处理确认、执行、反馈 - const config = handler; - - // 1. 确认阶段(默认需要确认) - if (config.needConfirm !== false) { - const confirmMsg = - typeof config.confirmMessage === "function" - ? config.confirmMessage(fnCall.arguments) - : config.confirmMessage || "确认执行此操作吗?"; - - await ElMessageBox.confirm(confirmMsg, "AI 助手操作确认", { - confirmButtonText: "确认执行", - cancelButtonText: "取消", - type: "warning", - dangerouslyUseHTMLString: true, - }); - } - - // 2. 执行阶段 - if (config.callBackendApi) { - // 自动调用后端 API - await AiCommandApi.executeCommand({ - originalCommand: action.originalCommand || "", - confirmMode: "manual", - userConfirmed: true, - currentRoute, - functionCall: { - name: fnCall.name, - arguments: fnCall.arguments, - }, - }); - } else { - // 执行自定义函数 - await config.execute(fnCall.arguments); - } - - // 3. 成功反馈 - const successMsg = - typeof config.successMessage === "function" - ? config.successMessage(fnCall.arguments) - : config.successMessage || "操作执行成功"; - ElMessage.success(successMsg); - } - - // 4. 刷新数据 - if (onRefresh) { - await onRefresh(); - } - } catch (error: any) { - // 处理取消操作 - if (error === "cancel") { - ElMessage.info("已取消操作"); - return; - } - - console.error("AI 操作执行失败:", error); - ElMessage.error(error.message || "操作执行失败"); - } - } - - /** - * 执行后端命令(通用方法) - */ - async function executeCommand( - functionName: string, - args: any, - options: { - originalCommand?: string; - confirmMode?: "auto" | "manual"; - needConfirm?: boolean; - confirmMessage?: string; - } = {} - ) { - const { - originalCommand = "", - confirmMode = "manual", - needConfirm = false, - confirmMessage, - } = options; - - // 如果需要确认,先显示确认对话框 - if (needConfirm && confirmMessage) { - try { - await ElMessageBox.confirm(confirmMessage, "AI 助手操作确认", { - confirmButtonText: "确认执行", - cancelButtonText: "取消", - type: "warning", - dangerouslyUseHTMLString: true, - }); - } catch { - ElMessage.info("已取消操作"); - return; - } - } - - try { - await AiCommandApi.executeCommand({ - originalCommand, - confirmMode, - userConfirmed: true, - currentRoute, - functionCall: { - name: functionName, - arguments: args, - }, - }); - - ElMessage.success("操作执行成功"); - } catch (error: any) { - if (error !== "cancel") { - throw error; - } - } - } - - /** - * 处理自动搜索 - */ - function handleAutoSearch(keywords: string) { - if (onAutoSearch) { - onAutoSearch(keywords); - } else { - ElMessage.info(`AI 助手已为您自动搜索:${keywords}`); - } - } - - /** - * 初始化:处理 URL 参数中的 AI 操作 - * - * 注意:此方法只处理 AI 相关参数,不负责页面数据的初始加载 - * 页面数据加载应由组件的 onMounted 钩子自行处理 - */ - async function init() { - if (isUnmounted) return; - - // 检查是否有 AI 助手传递的参数 - const keywords = route.query.keywords as string; - const autoSearch = route.query.autoSearch as string; - const aiActionParam = route.query.aiAction as string; - - // 如果没有任何 AI 参数,直接返回 - if (!keywords && !autoSearch && !aiActionParam) { - return; - } - - // 在 nextTick 中执行,确保页面数据已加载 - nextTick(async () => { - if (isUnmounted) return; - - // 1. 处理自动搜索 - if (autoSearch === "true" && keywords) { - handleAutoSearch(keywords); - } - - // 2. 处理 AI 操作 - if (aiActionParam) { - try { - const aiAction = JSON.parse(decodeURIComponent(aiActionParam)); - await executeAiAction(aiAction); - } catch (error) { - console.error("解析 AI 操作失败:", error); - ElMessage.error("AI 操作参数解析失败"); - } - } - }); - } - - // 组件挂载时自动初始化 - onMounted(() => { - init(); - }); - - // 组件卸载时清理 - onBeforeUnmount(() => { - isUnmounted = true; - }); - - return { - executeAiAction, - executeCommand, - handleAutoSearch, - }; -} diff --git a/frontend/src/composables/index.ts b/frontend/src/composables/index.ts index 16baba4b..807e31be 100644 --- a/frontend/src/composables/index.ts +++ b/frontend/src/composables/index.ts @@ -1,2 +1 @@ -export { useAiAction } from "./ai/useAiAction"; -export type { UseAiActionOptions, AiActionHandler } from "./ai/useAiAction"; +// 该文件作用是:导出所有 composable 函数,作为全局函数使用 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/settings.ts b/frontend/src/settings.ts index 42b7fa61..d2d8a5a6 100644 --- a/frontend/src/settings.ts +++ b/frontend/src/settings.ts @@ -43,8 +43,6 @@ export const defaultSettings: AppSettings = { guideVisible: false, /** 是否启动引导 */ showGuide: true, - // 是否启用 AI 助手 - enableAiAssistant: false, }; // 主题色预设 - 现代化配色方案 diff --git a/frontend/src/store/modules/settings.store.ts b/frontend/src/store/modules/settings.store.ts index add6ac47..e4f3f823 100644 --- a/frontend/src/store/modules/settings.store.ts +++ b/frontend/src/store/modules/settings.store.ts @@ -13,7 +13,6 @@ interface SettingsState { showWatermark: boolean; showSettings: boolean; showGuide: boolean; // 引导功能开关 - enableAiAssistant: boolean; // 桌面端工具显示设置 showMenuSearch: boolean; @@ -51,12 +50,6 @@ export const useSettingsStore = defineStore("setting", () => { defaultSettings.showWatermark ); - // 是否启用 AI 助手 - const enableAiAssistant = useStorage( - "vea:ui:enable_ai_assistant", - defaultSettings.enableAiAssistant - ); - // 是否显示系统设置 const showSettings = useStorage( SETTINGS_KEYS.SHOW_SETTINGS, @@ -123,7 +116,6 @@ export const useSettingsStore = defineStore("setting", () => { showNotification, sidebarColorScheme, layout, - enableAiAssistant, } as const; // 🎯 监听器 - 主题变化 @@ -197,7 +189,6 @@ export const useSettingsStore = defineStore("setting", () => { showWatermark.value = defaultSettings.showWatermark; showSettings.value = defaultSettings.showSettings; showGuide.value = defaultSettings.showGuide; - enableAiAssistant.value = defaultSettings.enableAiAssistant; // 桌面端工具设置 showMenuSearch.value = defaultSettings.showMenuSearch; @@ -223,7 +214,6 @@ export const useSettingsStore = defineStore("setting", () => { showWatermark, showSettings, showGuide, - enableAiAssistant, // 🎯 桌面端工具状态 showMenuSearch, diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts index 809775e8..a1e2594a 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; } /** @@ -98,8 +99,6 @@ declare global { guideVisible: boolean; /** 是否启动引导 */ showGuide: boolean; - /** 是否启用AI助手 */ - enableAiAssistant: boolean; } /** @@ -136,24 +135,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/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_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 @@ - 查询 - 重置 + + 查询 + + + 重置 + @@ -177,7 +190,7 @@ - + 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 @@ + + + {{ data.label }} + + + + + + + + + + + + diff --git a/frontend/src/views/module_application/workflow/index.vue b/frontend/src/views/module_application/workflow/index.vue index bf1c2c42..94044b17 100644 --- a/frontend/src/views/module_application/workflow/index.vue +++ b/frontend/src/views/module_application/workflow/index.vue @@ -1,15 +1,466 @@ - - 工作流--开发中... + + + 保存 + + + + + + + + 开始 + + + 结束 + + + + + {{ item.name }} + + + + + + + + + + + + + {{ updateState === "edge" ? "连接线规则配置" : "点位规则配置" }} + + + + + + + + + + + + + + 修改 + 删除 + + + + 删除 + + + + + + 是否要删除该连线? + + + + + + + + 是否要删除该点位? + + + + - +import { ref, markRaw } from "vue"; +import { VueFlow, useVueFlow, MarkerType } from "@vue-flow/core"; +import { Background } from "@vue-flow/background"; +import { Controls } from "@vue-flow/controls"; +import { MiniMap } from "@vue-flow/minimap"; +import "@vue-flow/core/dist/style.css"; +import "@vue-flow/core/dist/theme-default.css"; +import "@vue-flow/controls/dist/style.css"; +import CustomNode from "./CustomNode.vue"; +import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage } from "element-plus"; +import "element-plus/dist/index.css"; +const { + onInit, + onNodeDragStop, + onConnect, + addEdges, + getNodes, + getEdges, + setEdges, + setNodes, + screenToFlowCoordinate, + onNodesInitialized, + updateNode, + addNodes, +} = useVueFlow(); +// 默认连线配置 +const defaultEdgeOptions = { + type: "smoothstep", // 默认边类型 + animated: true, // 是否启用动画 + markerEnd: { + type: "arrowclosed", // 默认箭头样式 + color: "black", + }, +}; +// 节点 +const nodes = ref([ + { + id: "5", + type: "input", + data: { label: "开始" }, + position: { x: 235, y: 100 }, + class: "round-start", + }, + { + id: "6", + type: "custom", // 使用自定义类型 + data: { label: "工位:流程1" }, + position: { x: 200, y: 200 }, + class: "light", + }, + { + id: "7", + type: "output", + data: { label: "结束" }, + position: { x: 235, y: 300 }, + class: "round-stop", + }, +]); +const nodeTypes = ref({ + custom: markRaw(CustomNode), // 注册自定义节点类型 +}); +// 线 +const edges = ref([ + { + id: "e4-5", + type: "straight", + source: "5", + target: "6", + sourceHandle: "top-6", + label: "测试1", + markerEnd: { + type: MarkerType.ArrowClosed, // 使用闭合箭头 + color: "black", + }, + }, + { + id: "e4-6", + type: "straight", + source: "6", + target: "7", + sourceHandle: "bottom-6", + label: "测试2", + markerEnd: { + type: MarkerType.ArrowClosed, // 使用闭合箭头 + color: "black", + }, + }, +]); +onInit((vueFlowInstance) => { + vueFlowInstance.fitView(); +}); - +onNodeDragStop(({ event, nodes, node }) => { + console.log("Node Drag Stop", { event, nodes, node }); +}); + +onConnect((connection) => { + addEdges(connection); +}); +// ----------------------------------------------- +// 拖动块 +const pointsList = ref([{ name: "测试1" }, { name: "测试2" }]); +// -------------------------------------------------------------- +const updateState = ref(""); +const selectedEdge = ref({}); // 存储选中的边 +const removeEdgeDialogVisible = ref(false); +const removeNodeDialogVisible = ref(false); +const onEdgeClick = ({ edge }) => { + selectedEdge.value = edge; // 选中边 + updateState.value = "edge"; + console.log(selectedEdge.value); +}; +function updateEdge() { + // 获取当前所有的边 + const allEdges = getEdges.value; + // 切换边类型:根据当前类型来切换 + const newType = selectedEdge.value.type === "smoothstep" ? null : "smoothstep"; + // 更新选中边的类型 + setEdges([ + ...allEdges.filter((e) => e.id !== selectedEdge.value.id), // 移除旧的边 + { ...selectedEdge.value, type: newType, label: "Node 3" }, // 更新边的类型 + ]); +} +function removeEdge() { + removeEdgeDialogVisible.value = true; +} + +function confirmRemoveEdge() { + // 获取当前所有的边 + const allEdges = getEdges.value; + // 更新选中边的类型 + setEdges([ + ...allEdges.filter((e) => e.id !== selectedEdge.value.id), // 移除边 + ]); + ElMessage.success("连线删除成功"); + updateState.value = null; + selectedEdge.value = {}; + removeEdgeDialogVisible.value = false; +} + +const selectedNode = ref({}); // 存储选中的节点 +const onNodeClick = ({ node }) => { + selectedNode.value = node; // 更新选中的节点 + updateState.value = "node"; + console.log("选中的节点:", node); +}; + +function removeNode() { + removeNodeDialogVisible.value = true; +} + +function confirmRemoveNode() { + // 获取当前所有的边 + const allNodes = getNodes.value; + // 更新选中边的类型 + setNodes([ + ...allNodes.filter((e) => e.id !== selectedNode.value.id), // 移除边 + ]); + // 获取当前所有的边 + const allEdges = getEdges.value; + setEdges([ + ...allEdges.filter( + (e) => e.source !== selectedNode.value.id && e.target !== selectedNode.value.id + ), // 移除边 + ]); + ElMessage.success("点位删除成功"); + updateState.value = null; + selectedNode.value = {}; + console.log(getEdges.value); + removeNodeDialogVisible.value = false; +} + +// 拖拽相关状态 +const dragItem = ref(null); +// 拖拽开始时设置拖拽的元素 +function onDragStart(event, state) { + dragItem.value = { + id: `node-${Date.now()}`, // 动态生成唯一 id + data: { label: state === "开始" ? "开始" : state === "结束" ? "结束" : "工位:" + state }, + type: state === "开始" ? "input" : state === "结束" ? "output" : "custom", + position: { x: event.clientX, y: event.clientY }, + animated: false, + class: state === "开始" ? "round-start" : state === "结束" ? "round-stop" : "light", + }; +} + +// 拖拽结束时清除状态 +function onDragEnd() { + dragItem.value = null; +} + +// 拖拽目标画布区域时允许放置 +function onDragOver(event) { + event.preventDefault(); +} + +function onDrop(event) { + const position = screenToFlowCoordinate({ + x: event.clientX, + y: event.clientY, + }); + + const newNode = { + ...dragItem.value, + position, + }; + const { off } = onNodesInitialized(() => { + updateNode(dragItem.value?.id, (node) => ({ + position: { + x: node.position.x - node.dimensions.width / 2, + y: node.position.y - node.dimensions.height / 2, + }, + })); + + off(); + }); + + // 更新节点数据 + // nodes.value.push(newNode) + dragItem.value = null; + addNodes(newNode); +} + + diff --git a/frontend/src/views/module_gencode/demo/index.vue b/frontend/src/views/module_gencode/demo/index.vue index e9bf14ee..f40fba2b 100644 --- a/frontend/src/views/module_gencode/demo/index.vue +++ b/frontend/src/views/module_gencode/demo/index.vue @@ -302,7 +302,7 @@ > 重置密码 @@ -291,11 +287,7 @@ link icon="edit" :disabled="scope.row.is_superuser === true" - @click=" - scope.row.is_superuser === true - ? ElMessage.warning('系统超管角色,不可操作') - : handleOpenDialog('update', scope.row.id) - " + @click="handleOpenDialog('update', scope.row.id)" > 编辑 @@ -306,11 +298,7 @@ link icon="delete" :disabled="scope.row.is_superuser === true" - @click=" - scope.row.is_superuser === true - ? ElMessage.warning('系统超管角色,不可操作') - : handleDelete([scope.row.id]) - " + @click="handleDelete([scope.row.id])" > 删除
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
${md.utils.escapeHtml(str)}
$1
工作流--开发中...