Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
DictItem Module - 字典项模块
"""
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
DictItem API - 字典项管理接口
提供字典项的 CRUD 操作
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
from fastapi.responses import StreamingResponse
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.dict_item.model import DictItem
from core.dict_item.schema import (
DictItemCreate, DictItemUpdate, DictItemResponse, DictItemSimple,
DictItemBatchDeleteIn, DictItemBatchDeleteOut,
DictItemBatchUpdateStatusIn, DictItemBatchUpdateStatusOut,
DictItemSearchRequest
)
from core.dict_item.service import DictItemService
router = APIRouter(prefix="/dict_item", tags=["字典项管理"])
async def _build_dict_item_response(db: AsyncSession, item: DictItem) -> DictItemResponse:
"""构建字典项响应"""
dict_code = None
dict_name = None
if item.dict_id:
from core.dict.service import DictService
dict_obj = await DictService.get_by_id(db, item.dict_id)
if dict_obj:
dict_code = dict_obj.code
dict_name = dict_obj.name
return DictItemResponse(
id=item.id,
dict_id=item.dict_id,
dict_code=dict_code,
dict_name=dict_name,
label=item.label,
value=item.value,
icon=item.icon,
status=item.status,
remark=item.remark,
sort=item.sort,
is_deleted=item.is_deleted,
sys_create_datetime=item.sys_create_datetime,
sys_update_datetime=item.sys_update_datetime,
)
@router.post("", response_model=DictItemResponse, summary="创建字典项")
async def create_dict_item(data: DictItemCreate, db: AsyncSession = Depends(get_db)):
"""创建字典项"""
# 验证字典是否存在
from core.dict.service import DictService
dict_obj = await DictService.get_by_id(db, data.dict_id)
if not dict_obj:
raise HTTPException(status_code=400, detail=f"字典不存在: {data.dict_id}")
item = await DictItemService.create(db=db, data=data)
return await _build_dict_item_response(db, item)
@router.get("/all", response_model=List[DictItemSimple], summary="获取所有字典项(简化版)")
async def get_all_dict_items(db: AsyncSession = Depends(get_db)):
"""获取所有启用的字典项(用于选择器)"""
items = await DictItemService.get_all_active(db)
return items
@router.get("", response_model=PaginatedResponse[DictItemResponse], summary="获取字典项列表")
async def get_dict_item_list(
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="每页数量"),
dict_id: Optional[str] = Query(default=None, alias="dict_id", description="字典ID"),
label: Optional[str] = Query(default=None, description="显示名称"),
value: Optional[str] = Query(default=None, description="实际值"),
status: Optional[bool] = Query(default=None, description="状态"),
db: AsyncSession = Depends(get_db)
):
"""获取字典项列表(分页)"""
filters = []
if dict_id:
filters.append(DictItem.dict_id == dict_id)
if label:
filters.append(DictItem.label.ilike(f"%{label}%"))
if value:
filters.append(DictItem.value.ilike(f"%{value}%"))
if status is not None:
filters.append(DictItem.status == status)
items, total = await DictItemService.get_list(db, page=page, page_size=page_size, filters=filters)
response_items = [await _build_dict_item_response(db, item) for item in items]
return PaginatedResponse(items=response_items, total=total)
@router.post("/batch/delete", response_model=DictItemBatchDeleteOut, summary="批量删除字典项")
async def batch_delete_dict_items(
data: DictItemBatchDeleteIn,
db: AsyncSession = Depends(get_db)
):
"""批量删除字典项"""
count, failed_ids = await DictItemService.batch_delete(db, data.ids)
return DictItemBatchDeleteOut(count=count, failed_ids=failed_ids)
@router.post("/batch/update_status", response_model=DictItemBatchUpdateStatusOut, summary="批量更新字典项状态")
async def batch_update_dict_item_status(
data: DictItemBatchUpdateStatusIn,
db: AsyncSession = Depends(get_db)
):
"""批量更新字典项状态"""
count = await DictItemService.batch_update_status(db, data.ids, data.status)
return DictItemBatchUpdateStatusOut(count=count)
@router.post("/search", response_model=PaginatedResponse[DictItemResponse], summary="搜索字典项")
async def search_dict_items(
data: DictItemSearchRequest,
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="每页数量"),
db: AsyncSession = Depends(get_db)
):
"""搜索字典项"""
items, total = await DictItemService.search(db, data.keyword, page, page_size)
response_items = [await _build_dict_item_response(db, item) for item in items]
return PaginatedResponse(items=response_items, total=total)
@router.get("/by/dict_id/{dict_id}", response_model=List[DictItemSimple], summary="根据字典ID获取字典项")
async def get_dict_items_by_dict_id(dict_id: str, db: AsyncSession = Depends(get_db)):
"""根据字典ID获取字典项列表"""
items = await DictItemService.get_by_dict_id(db, dict_id)
return items
@router.get("/by/dict_code/{dict_code}", response_model=List[DictItemSimple], summary="根据字典编码获取字典项")
async def get_dict_items_by_dict_code(dict_code: str, db: AsyncSession = Depends(get_db)):
"""根据字典编码获取字典项列表"""
items = await DictItemService.get_by_dict_code(db, dict_code)
if not items:
# 检查字典是否存在
from core.dict.service import DictService
dict_obj = await DictService.get_by_code(db, dict_code)
if not dict_obj:
raise HTTPException(status_code=404, detail=f"字典编码不存在: {dict_code}")
return items
@router.get("/export/excel", summary="导出字典项Excel")
async def export_dict_item_excel(db: AsyncSession = Depends(get_db)):
"""导出字典项到Excel"""
output = await DictItemService.export_to_excel(db)
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=dict_item_export.xlsx"}
)
@router.get("/import/template", summary="下载字典项导入模板")
async def download_dict_item_template():
"""下载字典项导入模板"""
output = DictItemService.get_import_template()
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=dict_item_template.xlsx"}
)
@router.post("/import/excel", response_model=ResponseModel, summary="导入字典项Excel")
async def import_dict_item_excel(
file: UploadFile = File(..., description="Excel文件(.xlsx)"),
db: AsyncSession = Depends(get_db)
):
"""从Excel导入字典项"""
if not file.filename.endswith(".xlsx"):
raise HTTPException(status_code=400, detail="只支持.xlsx格式")
content = await file.read()
success, fail = await DictItemService.import_from_excel(db, content)
return ResponseModel(message=f"成功{success}条,失败{fail}", data={"success": success, "fail": fail})
@router.get("/{item_id}", response_model=DictItemResponse, summary="获取字典项详情")
async def get_dict_item_by_id(item_id: str, db: AsyncSession = Depends(get_db)):
"""获取字典项详情"""
item = await DictItemService.get_by_id(db, item_id)
if item is None:
raise HTTPException(status_code=404, detail="字典项不存在")
return await _build_dict_item_response(db, item)
@router.put("/{item_id}", response_model=DictItemResponse, summary="更新字典项")
async def update_dict_item(item_id: str, data: DictItemUpdate, db: AsyncSession = Depends(get_db)):
"""更新字典项"""
# 验证字典是否存在
if data.dict_id:
from core.dict.service import DictService
dict_obj = await DictService.get_by_id(db, data.dict_id)
if not dict_obj:
raise HTTPException(status_code=400, detail=f"字典不存在: {data.dict_id}")
item = await DictItemService.update(db, record_id=item_id, data=data)
if item is None:
raise HTTPException(status_code=404, detail="字典项不存在")
return await _build_dict_item_response(db, item)
@router.delete("/{item_id}", response_model=ResponseModel, summary="删除字典项")
async def delete_dict_item(
item_id: str,
hard: bool = Query(default=False, description="是否物理删除"),
db: AsyncSession = Depends(get_db)
):
"""删除字典项"""
success = await DictItemService.delete(db, record_id=item_id, hard=hard)
if not success:
raise HTTPException(status_code=404, detail="字典项不存在")
return ResponseModel(message="删除成功")
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
DictItem Model - 字典项模型
用于管理字典中的各个选项
"""
from sqlalchemy import Column, String, Boolean, Text
from app.base_model import BaseModel
class DictItem(BaseModel):
"""
系统字典项表
字段说明:
- dict_id: 字典ID(逻辑外键)
- label: 显示名称
- value: 实际值
- icon: 图标
- status: 状态
- remark: 备注
"""
__tablename__ = "core_dict_item"
# 字典ID(逻辑外键)
dict_id = Column(String(36), nullable=False, index=True, comment="字典ID")
# 显示名称
label = Column(String(100), nullable=True, index=True, comment="显示名称")
# 实际值
value = Column(String(100), nullable=True, index=True, comment="实际值")
# 图标
icon = Column(String(100), nullable=True, comment="图标")
# 状态
status = Column(Boolean, default=True, index=True, comment="状态")
# 备注
remark = Column(Text, nullable=True, comment="备注")
def __str__(self):
return f"{self.label} ({self.value})"
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
DictItem Schema - 字典项数据验证模式
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, ConfigDict, Field
from app.base_schema import CSTDatetime
class DictItemBase(BaseModel):
"""字典项基础Schema"""
dict_id: str = Field(..., description="字典ID")
label: Optional[str] = Field(None, max_length=100, description="显示名称")
value: Optional[str] = Field(None, max_length=100, description="实际值")
icon: Optional[str] = Field(None, max_length=100, description="图标")
status: bool = Field(default=True, description="状态")
remark: Optional[str] = Field(None, description="备注")
sort: int = Field(default=0, description="排序")
class DictItemCreate(DictItemBase):
"""字典项创建Schema"""
pass
class DictItemUpdate(BaseModel):
"""字典项更新Schema - 所有字段可选"""
dict_id: Optional[str] = Field(None, description="字典ID")
label: Optional[str] = Field(None, max_length=100, description="显示名称")
value: Optional[str] = Field(None, max_length=100, description="实际值")
icon: Optional[str] = Field(None, max_length=100, description="图标")
status: Optional[bool] = Field(None, description="状态")
remark: Optional[str] = Field(None, description="备注")
sort: Optional[int] = Field(None, description="排序")
class DictItemResponse(BaseModel):
"""字典项响应Schema"""
id: str
dict_id: str
dict_code: Optional[str] = None
dict_name: Optional[str] = None
label: Optional[str] = None
value: Optional[str] = None
icon: Optional[str] = None
status: bool
remark: Optional[str] = None
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 DictItemSimple(BaseModel):
"""字典项简单输出(用于选择器)"""
id: str
label: Optional[str] = None
value: Optional[str] = None
icon: Optional[str] = None
status: bool
model_config = ConfigDict(from_attributes=True)
class DictItemBatchDeleteIn(BaseModel):
"""批量删除字典项输入"""
ids: List[str] = Field(..., description="要删除的字典项ID列表")
class DictItemBatchDeleteOut(BaseModel):
"""批量删除字典项输出"""
count: int = Field(..., description="删除的记录数")
failed_ids: List[str] = Field(default=[], description="删除失败的ID列表")
class DictItemBatchUpdateStatusIn(BaseModel):
"""批量更新字典项状态输入"""
ids: List[str] = Field(..., description="字典项ID列表")
status: bool = Field(..., description="状态")
class DictItemBatchUpdateStatusOut(BaseModel):
"""批量更新字典项状态输出"""
count: int = Field(..., description="更新的记录数")
class DictItemSearchRequest(BaseModel):
"""搜索字典项请求"""
keyword: str = Field(..., description="搜索关键词")
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
DictItem Service - 字典项服务层
"""
from io import BytesIO
from typing import Tuple, Dict, Any, Optional, List
from sqlalchemy import select, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_service import BaseService
from core.dict_item.model import DictItem
from core.dict_item.schema import DictItemCreate, DictItemUpdate
class DictItemService(BaseService[DictItem, DictItemCreate, DictItemUpdate]):
"""
字典项服务层
继承BaseService,自动获得增删改查功能
"""
model = DictItem
# Excel导入导出配置
excel_columns = {
"label": "显示名称",
"value": "实际值",
"icon": "图标",
"status": "状态",
"remark": "备注",
}
excel_sheet_name = "字典项列表"
@classmethod
def _export_converter(cls, item: Any) -> Dict[str, Any]:
"""导出数据转换器"""
return {
"label": item.label or "",
"value": item.value or "",
"icon": item.icon or "",
"status": "启用" if item.status else "禁用",
"remark": item.remark or "",
}
@classmethod
def _import_processor(cls, row: Dict[str, Any]) -> Optional[DictItem]:
"""导入数据处理器"""
label = row.get("label")
value = row.get("value")
if not label and not value:
return None
status_str = row.get("status", "启用")
status = status_str in ("启用", "true", "True", "1", True)
return DictItem(
label=str(label) if label else None,
value=str(value) if value else None,
icon=str(row.get("icon") or "") if row.get("icon") else None,
status=status,
remark=str(row.get("remark") or "") if row.get("remark") else None,
)
@classmethod
async def export_to_excel(
cls,
db: AsyncSession,
data_converter: Any = None
) -> BytesIO:
"""导出到Excel"""
return await super().export_to_excel(db, cls._export_converter)
@classmethod
async def import_from_excel(
cls,
db: AsyncSession,
file_content: bytes,
row_processor: Any = None
) -> Tuple[int, int]:
"""从Excel导入"""
return await super().import_from_excel(db, file_content, cls._import_processor)
@classmethod
async def get_by_dict_id(cls, db: AsyncSession, dict_id: str) -> List[DictItem]:
"""根据字典ID获取字典项列表"""
result = await db.execute(
select(DictItem).where(
DictItem.dict_id == dict_id,
DictItem.is_deleted == False # noqa: E712
).order_by(DictItem.sort)
)
return list(result.scalars().all())
@classmethod
async def get_by_dict_code(cls, db: AsyncSession, dict_code: str) -> List[DictItem]:
"""根据字典编码获取字典项列表"""
from core.dict.model import Dict as DictModel
# 先获取字典
dict_result = await db.execute(
select(DictModel).where(
DictModel.code == dict_code,
DictModel.is_deleted == False # noqa: E712
)
)
dict_obj = dict_result.scalar_one_or_none()
if not dict_obj:
return []
# 获取字典项
return await cls.get_by_dict_id(db, dict_obj.id)
@classmethod
async def get_all_active(cls, db: AsyncSession) -> List[DictItem]:
"""获取所有启用的字典项"""
result = await db.execute(
select(DictItem).where(
DictItem.status == True, # noqa: E712
DictItem.is_deleted == False # noqa: E712
).order_by(DictItem.sort)
)
return list(result.scalars().all())
@classmethod
async def search(
cls,
db: AsyncSession,
keyword: str,
page: int = 1,
page_size: int = 20
) -> Tuple[List[DictItem], int]:
"""搜索字典项"""
filters = [
or_(
DictItem.label.ilike(f"%{keyword}%"),
DictItem.value.ilike(f"%{keyword}%"),
)
]
return await cls.get_list(db, page=page, page_size=page_size, filters=filters)
@classmethod
async def batch_delete(
cls,
db: AsyncSession,
ids: List[str],
hard: bool = False
) -> Tuple[int, List[str]]:
"""
批量删除字典项
:return: (成功数量, 失败的ID列表)
"""
success_count = 0
failed_ids = []
for item_id in ids:
try:
success = await cls.delete(db, record_id=item_id, hard=hard)
if success:
success_count += 1
else:
failed_ids.append(item_id)
except Exception:
failed_ids.append(item_id)
return success_count, failed_ids
@classmethod
async def batch_update_status(
cls,
db: AsyncSession,
ids: List[str],
status: bool
) -> int:
"""批量更新字典项状态"""
count = 0
for item_id in ids:
item = await cls.get_by_id(db, item_id)
if item:
item.status = status
await db.commit()
count += 1
return count