Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DictItem Module - 字典项模块
|
||||
"""
|
||||
@@ -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="删除成功")
|
||||
@@ -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})"
|
||||
@@ -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="搜索关键词")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user