Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
应用管理模块
|
||||
"""
|
||||
from core.application.model import Application
|
||||
from core.application.service import ApplicationService
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
应用管理API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from core.application.schema import (
|
||||
ApplicationCreate,
|
||||
ApplicationUpdate,
|
||||
ApplicationResponse,
|
||||
ApplicationListResponse,
|
||||
)
|
||||
from core.application.service import ApplicationService
|
||||
|
||||
router = APIRouter(prefix="/applications", tags=["应用管理"])
|
||||
|
||||
|
||||
@router.post("/", response_model=ApplicationResponse, summary="创建应用")
|
||||
async def create_application(
|
||||
data: ApplicationCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
创建新应用
|
||||
- **name**: 应用名称
|
||||
- **code**: 应用编码(唯一标识,用于URL路由,只能包含字母、数字、下划线和短横线)
|
||||
- **description**: 应用描述(可选)
|
||||
- **icon**: 应用图标(可选)
|
||||
- **app_type**: 应用类型(可选,默认mixed)
|
||||
"""
|
||||
# 检查编码唯一性
|
||||
if not await ApplicationService.check_unique(db, field="code", value=data.code):
|
||||
raise HTTPException(status_code=400, detail="应用编码已存在")
|
||||
|
||||
# 检查名称唯一性
|
||||
if not await ApplicationService.check_unique(db, field="name", value=data.name):
|
||||
raise HTTPException(status_code=400, detail="应用名称已存在")
|
||||
|
||||
return await ApplicationService.create(db=db, data=data)
|
||||
|
||||
|
||||
@router.get("/", response_model=PaginatedResponse[ApplicationListResponse], summary="获取应用列表")
|
||||
async def get_applications(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
keyword: str = Query(default=None, description="搜索关键词"),
|
||||
app_type: str = Query(default=None, alias="appType", description="应用类型"),
|
||||
status: str = Query(default=None, description="状态"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取应用列表(分页)
|
||||
支持按关键词、类型、状态筛选
|
||||
"""
|
||||
items, total = await ApplicationService.search(
|
||||
db,
|
||||
keyword=keyword,
|
||||
app_type=app_type,
|
||||
status=status,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
return PaginatedResponse(items=items, total=total)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=ResponseModel, summary="获取应用统计")
|
||||
async def get_stats(db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
获取应用统计信息
|
||||
"""
|
||||
stats = await ApplicationService.get_stats(db)
|
||||
return ResponseModel(message="获取成功", data=stats)
|
||||
|
||||
|
||||
@router.get("/check/unique", response_model=ResponseModel, summary="检查字段唯一性")
|
||||
async def check_unique(
|
||||
field: str = Query(..., description="字段名:code 或 name"),
|
||||
value: str = Query(..., description="字段值"),
|
||||
exclude_id: str = Query(default=None, alias="excludeId", description="排除的记录ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
检查字段值是否唯一
|
||||
"""
|
||||
allowed_fields = ["code", "name"]
|
||||
if field not in allowed_fields:
|
||||
raise HTTPException(status_code=400, detail=f"不支持检查字段: {field}")
|
||||
|
||||
is_unique = await ApplicationService.check_unique(db, field=field, value=value, exclude_id=exclude_id)
|
||||
return ResponseModel(
|
||||
message="可用" if is_unique else "已存在",
|
||||
data={"unique": is_unique}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/code/{code}", response_model=ApplicationResponse, summary="根据编码获取应用")
|
||||
async def get_application_by_code(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
根据应用编码获取应用详情
|
||||
"""
|
||||
db_obj = await ApplicationService.get_by_code(db, code=code)
|
||||
if db_obj is None:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
return db_obj
|
||||
|
||||
|
||||
@router.get("/{record_id}", response_model=ApplicationResponse, summary="获取应用详情")
|
||||
async def get_application(
|
||||
record_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
根据ID获取应用详情
|
||||
"""
|
||||
db_obj = await ApplicationService.get_by_id(db, record_id=record_id)
|
||||
if db_obj is None:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
return db_obj
|
||||
|
||||
|
||||
@router.put("/{record_id}", response_model=ApplicationResponse, summary="更新应用")
|
||||
async def update_application(
|
||||
record_id: str,
|
||||
data: ApplicationUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
更新应用信息
|
||||
"""
|
||||
# 检查编码唯一性(排除自身)
|
||||
if data.code and not await ApplicationService.check_unique(db, field="code", value=data.code, exclude_id=record_id):
|
||||
raise HTTPException(status_code=400, detail="应用编码已存在")
|
||||
|
||||
# 检查名称唯一性(排除自身)
|
||||
if data.name and not await ApplicationService.check_unique(db, field="name", value=data.name, exclude_id=record_id):
|
||||
raise HTTPException(status_code=400, detail="应用名称已存在")
|
||||
|
||||
db_obj = await ApplicationService.update(db, record_id=record_id, data=data)
|
||||
if db_obj is None:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 清除该应用相关的菜单缓存
|
||||
from core.menu.service import MenuService
|
||||
await MenuService.invalidate_app_menu_cache(db_obj.code)
|
||||
|
||||
return db_obj
|
||||
|
||||
|
||||
@router.post("/{record_id}/publish", response_model=ApplicationResponse, summary="发布应用")
|
||||
async def publish_application(
|
||||
record_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
发布应用;已停用状态也可调用本接口重新启用(状态变为已发布)
|
||||
"""
|
||||
db_obj = await ApplicationService.publish(db, record_id=record_id)
|
||||
if db_obj is None:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
return db_obj
|
||||
|
||||
|
||||
@router.post("/{record_id}/disable", response_model=ApplicationResponse, summary="停用应用")
|
||||
async def disable_application(
|
||||
record_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
停用应用
|
||||
"""
|
||||
db_obj = await ApplicationService.disable(db, record_id=record_id)
|
||||
if db_obj is None:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
return db_obj
|
||||
|
||||
|
||||
@router.delete("/{record_id}", response_model=ResponseModel, summary="删除应用")
|
||||
async def delete_application(
|
||||
record_id: str,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
删除应用
|
||||
- **hard=false**: 逻辑删除(默认)
|
||||
- **hard=true**: 物理删除
|
||||
"""
|
||||
success = await ApplicationService.delete(db, record_id=record_id, hard=hard)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
return ResponseModel(message="删除成功")
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
应用管理数据模型
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Integer, JSON
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class Application(BaseModel):
|
||||
"""应用模型 - 低代码平台的顶层容器"""
|
||||
__tablename__ = "core_application"
|
||||
|
||||
name = Column(String(100), nullable=False, comment="应用名称")
|
||||
code = Column(String(100), unique=True, nullable=False, index=True, comment="应用编码(唯一标识,用于URL路由)")
|
||||
description = Column(Text, default="", comment="应用描述")
|
||||
icon = Column(String(100), default="", comment="应用图标")
|
||||
cover = Column(String(500), default="", comment="应用封面图URL")
|
||||
|
||||
# 应用类型:form-表单应用, workflow-流程应用, dashboard-数据应用, screen-大屏应用, mixed-混合应用, ai-AI应用
|
||||
app_type = Column(String(20), default="mixed", index=True, comment="应用类型")
|
||||
|
||||
# 状态:draft-开发中, published-已发布, disabled-已停用
|
||||
status = Column(String(20), default="draft", index=True, comment="应用状态")
|
||||
|
||||
# 应用首页路径
|
||||
home_path = Column(String(200), nullable=True, comment="应用首页路径")
|
||||
|
||||
# 版本号
|
||||
version = Column(Integer, default=1, comment="版本号")
|
||||
|
||||
# 应用配置(JSON格式,可存储主题、权限、自定义配置等)
|
||||
config = Column(JSON, default=dict, comment="应用配置")
|
||||
|
||||
# 所有者(逻辑外键关联 core_user)
|
||||
owner_id = Column(String(21), nullable=True, index=True, comment="所有者ID")
|
||||
|
||||
# 团队成员ID列表(JSON数组)
|
||||
team_ids = Column(JSON, default=list, comment="团队成员ID列表")
|
||||
|
||||
# 开发模式下显示的系统菜单ID列表(JSON数组)
|
||||
# 如果为空或None,则显示所有系统菜单;否则只显示选中的系统菜单
|
||||
system_menu_ids = Column(JSON, default=list, comment="开发模式下显示的系统菜单ID列表")
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
应用管理Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class ApplicationBase(BaseModel):
|
||||
"""应用基础Schema"""
|
||||
name: str = Field(..., min_length=1, max_length=100, description="应用名称")
|
||||
code: str = Field(..., min_length=1, max_length=100, pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*$", description="应用编码")
|
||||
description: Optional[str] = Field(default="", description="应用描述")
|
||||
icon: Optional[str] = Field(default="", description="应用图标")
|
||||
cover: Optional[str] = Field(default="", description="应用封面图URL")
|
||||
app_type: Optional[str] = Field(default="mixed", description="应用类型: form/workflow/dashboard/screen/mixed/ai")
|
||||
home_path: Optional[str] = Field(default=None, description="应用首页路径")
|
||||
config: Optional[Dict[str, Any]] = Field(default_factory=dict, description="应用配置")
|
||||
system_menu_ids: Optional[List[str]] = Field(default_factory=list, description="开发模式下显示的系统菜单ID列表")
|
||||
|
||||
|
||||
class ApplicationCreate(ApplicationBase):
|
||||
"""创建应用Schema"""
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationUpdate(BaseModel):
|
||||
"""更新应用Schema - 所有字段可选"""
|
||||
name: Optional[str] = Field(default=None, min_length=1, max_length=100, description="应用名称")
|
||||
code: Optional[str] = Field(default=None, min_length=1, max_length=100, pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*$", description="应用编码")
|
||||
description: Optional[str] = Field(default=None, description="应用描述")
|
||||
icon: Optional[str] = Field(default=None, description="应用图标")
|
||||
cover: Optional[str] = Field(default=None, description="应用封面图URL")
|
||||
app_type: Optional[str] = Field(default=None, description="应用类型")
|
||||
home_path: Optional[str] = Field(default=None, description="应用首页路径")
|
||||
status: Optional[str] = Field(default=None, description="应用状态: draft/published/disabled")
|
||||
config: Optional[Dict[str, Any]] = Field(default=None, description="应用配置")
|
||||
team_ids: Optional[List[str]] = Field(default=None, description="团队成员ID列表")
|
||||
system_menu_ids: Optional[List[str]] = Field(default=None, description="开发模式下显示的系统菜单ID列表")
|
||||
|
||||
|
||||
class ApplicationResponse(ApplicationBase):
|
||||
"""应用响应Schema"""
|
||||
id: str
|
||||
status: str = "draft"
|
||||
home_path: Optional[str] = None
|
||||
version: int = 1
|
||||
owner_id: Optional[str] = None
|
||||
team_ids: List[str] = []
|
||||
system_menu_ids: Optional[List[str]] = []
|
||||
sort: int = 0
|
||||
is_deleted: bool = False
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ApplicationListResponse(BaseModel):
|
||||
"""应用列表响应Schema(简化版)"""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
description: Optional[str] = ""
|
||||
icon: Optional[str] = ""
|
||||
cover: Optional[str] = ""
|
||||
app_type: str = "mixed"
|
||||
status: str = "draft"
|
||||
home_path: Optional[str] = None
|
||||
version: int = 1
|
||||
owner_id: Optional[str] = None
|
||||
system_menu_ids: Optional[List[str]] = []
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ApplicationStatsResponse(BaseModel):
|
||||
"""应用统计响应Schema"""
|
||||
id: str
|
||||
name: str
|
||||
code: str
|
||||
form_count: int = 0
|
||||
page_count: int = 0
|
||||
workflow_count: int = 0
|
||||
screen_count: int = 0
|
||||
data_source_count: int = 0
|
||||
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
应用管理服务层
|
||||
"""
|
||||
from typing import Optional, List, Tuple, Any
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_service import BaseService
|
||||
from core.application.model import Application
|
||||
from core.application.schema import ApplicationCreate, ApplicationUpdate
|
||||
|
||||
|
||||
class ApplicationService(BaseService[Application, ApplicationCreate, ApplicationUpdate]):
|
||||
"""
|
||||
应用服务层
|
||||
继承BaseService,自动获得增删改查功能
|
||||
"""
|
||||
|
||||
model = Application
|
||||
|
||||
# Excel导入导出配置
|
||||
excel_columns = {
|
||||
"name": "应用名称",
|
||||
"code": "应用编码",
|
||||
"description": "描述",
|
||||
"app_type": "应用类型",
|
||||
"status": "状态",
|
||||
}
|
||||
excel_sheet_name = "应用列表"
|
||||
|
||||
@classmethod
|
||||
async def get_by_code(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
code: str
|
||||
) -> Optional[Application]:
|
||||
"""
|
||||
根据应用编码获取应用
|
||||
|
||||
:param db: 数据库会话
|
||||
:param code: 应用编码
|
||||
:return: 应用或None
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(cls.model).where(
|
||||
cls.model.code == code,
|
||||
cls.model.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def get_list_by_owner(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
owner_id: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[List[Application], int]:
|
||||
"""
|
||||
获取指定用户拥有的应用列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param owner_id: 所有者ID
|
||||
:param page: 页码
|
||||
:param page_size: 每页数量
|
||||
:return: (应用列表, 总数)
|
||||
"""
|
||||
filters = [cls.model.owner_id == owner_id]
|
||||
return await cls.get_list(db, page, page_size, filters)
|
||||
|
||||
@classmethod
|
||||
async def get_list_by_status(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
status: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[List[Application], int]:
|
||||
"""
|
||||
根据状态获取应用列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param status: 状态
|
||||
:param page: 页码
|
||||
:param page_size: 每页数量
|
||||
:return: (应用列表, 总数)
|
||||
"""
|
||||
filters = [cls.model.status == status]
|
||||
return await cls.get_list(db, page, page_size, filters)
|
||||
|
||||
@classmethod
|
||||
async def get_list_by_type(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
app_type: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[List[Application], int]:
|
||||
"""
|
||||
根据类型获取应用列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param app_type: 应用类型
|
||||
:param page: 页码
|
||||
:param page_size: 每页数量
|
||||
:return: (应用列表, 总数)
|
||||
"""
|
||||
filters = [cls.model.app_type == app_type]
|
||||
return await cls.get_list(db, page, page_size, filters)
|
||||
|
||||
@classmethod
|
||||
async def search(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
keyword: Optional[str] = None,
|
||||
app_type: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
owner_id: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[List[Application], int]:
|
||||
"""
|
||||
搜索应用
|
||||
|
||||
:param db: 数据库会话
|
||||
:param keyword: 关键词(搜索名称和描述)
|
||||
:param app_type: 应用类型
|
||||
:param status: 状态
|
||||
:param owner_id: 所有者ID
|
||||
:param page: 页码
|
||||
:param page_size: 每页数量
|
||||
:return: (应用列表, 总数)
|
||||
"""
|
||||
filters = []
|
||||
|
||||
if keyword:
|
||||
filters.append(
|
||||
(cls.model.name.ilike(f"%{keyword}%")) |
|
||||
(cls.model.description.ilike(f"%{keyword}%")) |
|
||||
(cls.model.code.ilike(f"%{keyword}%"))
|
||||
)
|
||||
|
||||
if app_type:
|
||||
filters.append(cls.model.app_type == app_type)
|
||||
|
||||
if status:
|
||||
filters.append(cls.model.status == status)
|
||||
|
||||
if owner_id:
|
||||
filters.append(cls.model.owner_id == owner_id)
|
||||
|
||||
return await cls.get_list(db, page, page_size, filters)
|
||||
|
||||
@classmethod
|
||||
async def publish(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
record_id: str,
|
||||
auto_commit: bool = True
|
||||
) -> Optional[Application]:
|
||||
"""
|
||||
发布应用(草稿 -> 已发布;已停用 -> 重新启用为已发布)
|
||||
|
||||
:param db: 数据库会话
|
||||
:param record_id: 应用ID
|
||||
:param auto_commit: 是否自动提交
|
||||
:return: 更新后的应用或None
|
||||
"""
|
||||
db_obj = await cls.get_by_id(db, record_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = "published"
|
||||
db_obj.version += 1
|
||||
|
||||
if auto_commit:
|
||||
await db.commit()
|
||||
await db.refresh(db_obj)
|
||||
else:
|
||||
await db.flush()
|
||||
await db.refresh(db_obj)
|
||||
|
||||
return db_obj
|
||||
|
||||
@classmethod
|
||||
async def disable(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
record_id: str,
|
||||
auto_commit: bool = True
|
||||
) -> Optional[Application]:
|
||||
"""
|
||||
停用应用
|
||||
|
||||
:param db: 数据库会话
|
||||
:param record_id: 应用ID
|
||||
:param auto_commit: 是否自动提交
|
||||
:return: 更新后的应用或None
|
||||
"""
|
||||
db_obj = await cls.get_by_id(db, record_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
db_obj.status = "disabled"
|
||||
|
||||
if auto_commit:
|
||||
await db.commit()
|
||||
await db.refresh(db_obj)
|
||||
else:
|
||||
await db.flush()
|
||||
await db.refresh(db_obj)
|
||||
|
||||
return db_obj
|
||||
|
||||
@classmethod
|
||||
async def get_stats(
|
||||
cls,
|
||||
db: AsyncSession
|
||||
) -> dict:
|
||||
"""
|
||||
获取应用统计信息
|
||||
|
||||
:param db: 数据库会话
|
||||
:return: 统计信息
|
||||
"""
|
||||
# 总数
|
||||
total_result = await db.execute(
|
||||
select(func.count()).select_from(cls.model).where(
|
||||
cls.model.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 按状态统计
|
||||
status_result = await db.execute(
|
||||
select(cls.model.status, func.count()).where(
|
||||
cls.model.is_deleted == False # noqa: E712
|
||||
).group_by(cls.model.status)
|
||||
)
|
||||
status_stats = {row[0]: row[1] for row in status_result.all()}
|
||||
|
||||
# 按类型统计
|
||||
type_result = await db.execute(
|
||||
select(cls.model.app_type, func.count()).where(
|
||||
cls.model.is_deleted == False # noqa: E712
|
||||
).group_by(cls.model.app_type)
|
||||
)
|
||||
type_stats = {row[0]: row[1] for row in type_result.all()}
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"by_status": status_stats,
|
||||
"by_type": type_stats
|
||||
}
|
||||
Reference in New Issue
Block a user