init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .controller import TicketRouter
|
||||
|
||||
__all__ = ["TicketRouter"]
|
||||
@@ -0,0 +1,121 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
|
||||
from .schema import TicketBatchSchema, TicketCommentCreateSchema, TicketCommentOutSchema, TicketCreateSchema, TicketOutSchema, TicketQueryParam, TicketUpdateSchema
|
||||
from .service import TicketCommentService, TicketService
|
||||
|
||||
TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=["工单管理"])
|
||||
|
||||
|
||||
@TicketRouter.get("/list", summary="工单列表", response_model=ResponseSchema[PageResultSchema[TicketOutSchema]])
|
||||
async def ticket_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[TicketQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result = await TicketService(auth, db).page(
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
return SuccessResponse(data=result, msg="查询成功")
|
||||
|
||||
|
||||
@TicketRouter.get("/detail/{id}", summary="获取工单详情", response_model=ResponseSchema[TicketOutSchema])
|
||||
async def ticket_detail_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="工单ID")],
|
||||
) -> JSONResponse:
|
||||
result = await TicketService(auth, db).detail(id=id)
|
||||
return SuccessResponse(data=result, msg="查询成功")
|
||||
|
||||
|
||||
@TicketRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建工单", response_model=ResponseSchema[TicketOutSchema])
|
||||
async def ticket_create_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:create"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
data: Annotated[TicketCreateSchema, Body(description="工单创建参数")],
|
||||
) -> JSONResponse:
|
||||
result = await TicketService(auth, db).create(data=data)
|
||||
return SuccessResponse(data=result, msg="创建成功")
|
||||
|
||||
|
||||
@TicketRouter.put("/update/{id}", summary="更新工单", response_model=ResponseSchema[TicketOutSchema])
|
||||
async def ticket_update_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:update"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="工单ID", ge=1)],
|
||||
data: Annotated[TicketUpdateSchema, Body(description="工单更新参数")],
|
||||
) -> JSONResponse:
|
||||
result = await TicketService(auth, db).update(id=id, data=data)
|
||||
return SuccessResponse(data=result, msg="更新成功")
|
||||
|
||||
|
||||
@TicketRouter.put("/batch", summary="批量更新工单", response_model=ResponseSchema)
|
||||
async def ticket_batch_update_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:update"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
data: Annotated[TicketBatchSchema, Body(description="工单批量更新参数")],
|
||||
) -> JSONResponse:
|
||||
await TicketService(auth, db).batch(data=data)
|
||||
return SuccessResponse(msg="批量操作成功")
|
||||
|
||||
|
||||
@TicketRouter.delete("/delete", summary="删除工单", response_model=ResponseSchema[None])
|
||||
async def ticket_delete_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:delete"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
ids: Annotated[list[int], Body(description="工单ID列表")],
|
||||
) -> JSONResponse:
|
||||
await TicketService(auth, db).delete(ids=ids)
|
||||
return SuccessResponse(msg="删除成功")
|
||||
|
||||
|
||||
@TicketRouter.post("/export", summary="导出工单")
|
||||
async def ticket_export_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:export"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
search: Annotated[TicketQueryParam, Body()],
|
||||
) -> StreamingResponse:
|
||||
ticket_list = await TicketService(auth, db).get_list(search=search)
|
||||
export_result = TicketService.export_list(ticket_list=[item.model_dump() for item in ticket_list])
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=ticket.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@TicketRouter.get("/{ticket_id}/comments", summary="工单评论列表", response_model=ResponseSchema[PageResultSchema[TicketCommentOutSchema]])
|
||||
async def ticket_comment_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
ticket_id: Annotated[int, Path(description="工单ID")],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
) -> JSONResponse:
|
||||
result = await TicketCommentService(auth, db).page(ticket_id=ticket_id, page_no=page.page_no, page_size=page.page_size)
|
||||
return SuccessResponse(data=result, msg="查询成功")
|
||||
|
||||
|
||||
@TicketRouter.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED, summary="创建评论", response_model=ResponseSchema[TicketCommentOutSchema])
|
||||
async def ticket_comment_create_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:ticket:detail"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
ticket_id: Annotated[int, Path(description="工单ID")],
|
||||
data: Annotated[TicketCommentCreateSchema, Body(description="评论内容")],
|
||||
) -> JSONResponse:
|
||||
result = await TicketCommentService(auth, db).create(ticket_id=ticket_id, data=data)
|
||||
return SuccessResponse(data=result, msg="评论成功")
|
||||
@@ -0,0 +1,23 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import TicketCommentModel, TicketModel
|
||||
from .schema import TicketCommentCreateSchema, TicketCreateSchema, TicketUpdateSchema
|
||||
|
||||
|
||||
class TicketCRUD(CRUDBase[TicketModel, TicketCreateSchema, TicketUpdateSchema]):
|
||||
"""工单 CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
super().__init__(model=TicketModel, auth=auth, db=db)
|
||||
|
||||
|
||||
class TicketCommentCRUD(CRUDBase[TicketCommentModel, TicketCommentCreateSchema, Any]):
|
||||
"""工单评论 CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
super().__init__(model=TicketCommentModel, auth=auth, db=db)
|
||||
@@ -0,0 +1,51 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
|
||||
class TicketModel(ModelMixin, UserMixin):
|
||||
"""工单模型 — 用户提交的建议和反馈
|
||||
status: 0=待处理 1=处理中 2=已完成 3=已关闭
|
||||
"""
|
||||
|
||||
__tablename__: str = "sys_ticket"
|
||||
__table_args__: dict[str, str] = {"comment": "工单表"}
|
||||
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, index=True, comment="标题")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:待处理 1:处理中 2:已完成 3:已关闭)")
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
ticket_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment="工单内容(富文本)")
|
||||
summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="工单内容(纯文本摘要)")
|
||||
ticket_type: Mapped[str] = mapped_column(String(20), nullable=False, default="suggestion", comment="工单类型(suggestion:建议 bug:缺陷 optimize:优化 other:其他)")
|
||||
images: Mapped[str | None] = mapped_column(Text, nullable=True, comment="图片URL列表(JSON数组)")
|
||||
reply: Mapped[str | None] = mapped_column(Text, nullable=True, comment="回复内容")
|
||||
assigned_id: Mapped[int | None] = mapped_column(ForeignKey("sys_user.id", ondelete="SET NULL", onupdate="CASCADE"), nullable=True, index=True, comment="处理人ID")
|
||||
|
||||
assigned_by: Mapped["UserModel | None"] = relationship("UserModel", foreign_keys=[assigned_id], uselist=False)
|
||||
|
||||
@validates("title")
|
||||
def validate_title(self, key: str, title: str) -> str:
|
||||
if not title or not title.strip():
|
||||
raise ValueError("工单标题不能为空")
|
||||
return title.strip()
|
||||
|
||||
@validates("summary", "ticket_content")
|
||||
def validate_content(self, key: str, content: str | None) -> str | None:
|
||||
if content and content.strip():
|
||||
return content.strip()
|
||||
return content
|
||||
|
||||
|
||||
class TicketCommentModel(ModelMixin, UserMixin):
|
||||
"""工单评论模型"""
|
||||
__tablename__: str = "sys_ticket_comment"
|
||||
__table_args__: dict[str, str] = {"comment": "工单评论表"}
|
||||
|
||||
ticket_id: Mapped[int] = mapped_column(ForeignKey("sys_ticket.id", ondelete="CASCADE"), nullable=False, index=True, comment="工单ID")
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, comment="评论内容(富文本)")
|
||||
@@ -0,0 +1,103 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.common.enums import TicketTypeEnum
|
||||
from app.core.base_schema import (
|
||||
BaseQueryParam,
|
||||
BaseSchema,
|
||||
CommonSchema,
|
||||
UserByQueryParam,
|
||||
UserBySchema,
|
||||
)
|
||||
|
||||
|
||||
class TicketCreateSchema(BaseModel):
|
||||
"""创建工单"""
|
||||
|
||||
title: str = Field(..., min_length=1, max_length=200, description="工单标题")
|
||||
ticket_content: str = Field(default="", description="工单内容(富文本)")
|
||||
summary: str | None = Field(default=None, description="工单内容(纯文本摘要)")
|
||||
ticket_type: TicketTypeEnum = Field(default=TicketTypeEnum.SUGGESTION, description="工单类型(suggestion/bug/optimize/other)")
|
||||
images: str | None = Field(default=None, description="图片URL列表(JSON数组)")
|
||||
description: str | None = Field(default=None, max_length=255, description="工单描述")
|
||||
|
||||
@field_validator("title")
|
||||
@classmethod
|
||||
def _validate_title(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("工单标题不能为空")
|
||||
return v
|
||||
|
||||
|
||||
class TicketUpdateSchema(BaseModel):
|
||||
"""更新工单"""
|
||||
|
||||
title: str | None = Field(default=None, max_length=200, description="工单标题")
|
||||
ticket_content: str | None = Field(default=None, description="工单内容(富文本)")
|
||||
summary: str | None = Field(default=None, description="工单内容(纯文本摘要)")
|
||||
ticket_type: TicketTypeEnum | None = Field(default=None, description="工单类型")
|
||||
status: int | None = Field(default=None, ge=0, le=3, description="状态(0:待处理 1:处理中 2:已完成 3:已关闭)")
|
||||
reply: str | None = Field(default=None, description="回复内容")
|
||||
assigned_id: int | None = Field(default=None, gt=0, description="处理人ID")
|
||||
description: str | None = Field(default=None, max_length=255, description="工单描述")
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _validate_status(cls, v: int | None) -> int | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v not in {0, 1, 2, 3}:
|
||||
raise ValueError("工单状态仅支持 0(待处理)、1(处理中)、2(已完成)、3(已关闭)")
|
||||
return v
|
||||
|
||||
|
||||
class TicketOutSchema(BaseSchema, UserBySchema):
|
||||
"""工单响应"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
title: str = Field(..., description="工单标题")
|
||||
ticket_content: str | None = Field(default=None, description="工单内容")
|
||||
summary: str | None = Field(default=None, description="摘要")
|
||||
ticket_type: TicketTypeEnum = Field(..., description="工单类型")
|
||||
status: int = Field(..., description="状态(0:待处理 1:处理中 2:已完成 3:已关闭)")
|
||||
images: str | None = Field(default=None, description="图片")
|
||||
reply: str | None = Field(default=None, description="回复内容")
|
||||
assigned_id: int | None = Field(default=None, description="指派人ID")
|
||||
assigned_by: CommonSchema | None = Field(default=None, description="指派人")
|
||||
|
||||
|
||||
class TicketBatchSchema(BaseModel):
|
||||
"""批量更新工单"""
|
||||
|
||||
ids: list[int] = Field(..., min_length=1, description="工单ID列表")
|
||||
status: int = Field(..., ge=0, le=3, description="状态(0:待处理 1:处理中 2:已完成 3:已关闭)")
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _validate_status(cls, v: int) -> int:
|
||||
if v not in {0, 1, 2, 3}:
|
||||
raise ValueError("工单状态仅支持 0(待处理)、1(处理中)、2(已完成)、3(已关闭)")
|
||||
return v
|
||||
|
||||
|
||||
class TicketQueryParam(BaseQueryParam, UserByQueryParam):
|
||||
"""工单查询参数"""
|
||||
|
||||
title: str | None = Field(None, description="工单标题", json_schema_extra={"q": "like"})
|
||||
ticket_type: str | None = Field(None, description="工单类型", json_schema_extra={"q": "eq"})
|
||||
assigned_id: int | None = Field(None, description="处理人ID", json_schema_extra={"q": "eq"})
|
||||
status: int | None = Field(None, ge=0, le=3, description="状态(0:待处理 1:处理中 2:已完成 3:已关闭)", json_schema_extra={"q": "eq"})
|
||||
|
||||
|
||||
class TicketCommentCreateSchema(BaseModel):
|
||||
"""创建评论"""
|
||||
content: str = Field(..., min_length=1, description="评论内容")
|
||||
|
||||
|
||||
class TicketCommentOutSchema(BaseSchema, UserBySchema):
|
||||
"""评论响应"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
ticket_id: int
|
||||
content: str
|
||||
created_by_name: str | None = None
|
||||
@@ -0,0 +1,201 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.common_util import search_to_dict
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
|
||||
from .crud import TicketCommentCRUD, TicketCRUD
|
||||
from .schema import (
|
||||
TicketBatchSchema,
|
||||
TicketCommentCreateSchema,
|
||||
TicketCommentOutSchema,
|
||||
TicketCreateSchema,
|
||||
TicketOutSchema,
|
||||
TicketQueryParam,
|
||||
TicketUpdateSchema,
|
||||
)
|
||||
|
||||
_TICKET_STATUS_TRANSITIONS = {
|
||||
0: {1, 3},
|
||||
1: {2, 3},
|
||||
2: {3},
|
||||
3: {0},
|
||||
}
|
||||
|
||||
_TICKET_STATUS_LABELS = {
|
||||
0: "待处理",
|
||||
1: "处理中",
|
||||
2: "已完成",
|
||||
3: "已关闭",
|
||||
}
|
||||
|
||||
|
||||
_TICKET_PRELOAD = ["assigned_by"]
|
||||
|
||||
|
||||
class TicketService:
|
||||
"""工单管理服务"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
self.auth = auth
|
||||
self.db = db
|
||||
|
||||
def _validate_status_transition(self, ticket, new_status: int) -> None:
|
||||
old_status = ticket.status if ticket.status is not None else 0
|
||||
old_label = _TICKET_STATUS_LABELS.get(old_status, str(old_status))
|
||||
new_label = _TICKET_STATUS_LABELS.get(new_status, str(new_status))
|
||||
|
||||
if new_status not in _TICKET_STATUS_TRANSITIONS.get(old_status, set()):
|
||||
raise CustomException(msg=f"不允许从{old_label}转换为{new_label}")
|
||||
|
||||
user = self.auth.user
|
||||
is_super = user.is_superuser if user else False
|
||||
is_creator = user and user.id and ticket.created_id == user.id
|
||||
is_assignee = user and user.id and ticket.assigned_id == user.id
|
||||
|
||||
if new_status == 0:
|
||||
if not is_super:
|
||||
raise CustomException(msg="仅超管可以重新打开已关闭的工单")
|
||||
elif old_status == 0 and new_status == 1:
|
||||
if not (is_super or is_creator or is_assignee):
|
||||
raise CustomException(msg="仅创建人、处理人或超管可以受理工单")
|
||||
elif old_status == 0 and new_status == 3:
|
||||
if not (is_super or is_creator):
|
||||
raise CustomException(msg="仅创建人或超管可以取消工单")
|
||||
elif old_status == 1 and new_status == 2:
|
||||
if not (is_super or is_assignee):
|
||||
raise CustomException(msg="仅处理人或超管可以将工单标记为已完成")
|
||||
elif old_status == 1 and new_status == 3:
|
||||
if not (is_super or is_creator or is_assignee):
|
||||
raise CustomException(msg="仅创建人、处理人或超管可以关闭工单")
|
||||
elif old_status == 2 and new_status == 3:
|
||||
if not (is_super or is_creator):
|
||||
raise CustomException(msg="仅创建人或超管可以确认关闭工单")
|
||||
|
||||
async def page(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: TicketQueryParam | None = None,
|
||||
order_by: list | None = None,
|
||||
) -> PageResultSchema[TicketOutSchema]:
|
||||
return await TicketCRUD(self.auth, self.db).page(
|
||||
offset=(page_no - 1) * page_size,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"created_time": "desc"}],
|
||||
search=search_to_dict(search),
|
||||
out_schema=TicketOutSchema,
|
||||
preload=_TICKET_PRELOAD,
|
||||
)
|
||||
|
||||
async def detail(self, id: int) -> TicketOutSchema:
|
||||
obj = await TicketCRUD(self.auth, self.db).get_or_404(id=id, preload=_TICKET_PRELOAD)
|
||||
return TicketOutSchema.model_validate(obj)
|
||||
|
||||
async def create(self, data: TicketCreateSchema) -> TicketOutSchema:
|
||||
obj = await TicketCRUD(self.auth, self.db).create(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建工单失败")
|
||||
return await self.detail(id=obj.id)
|
||||
|
||||
async def update(self, id: int, data: TicketUpdateSchema) -> TicketOutSchema:
|
||||
obj = await TicketCRUD(self.auth, self.db).get_or_404(id=id, msg="工单不存在")
|
||||
|
||||
if data.status is not None:
|
||||
self._validate_status_transition(obj, data.status)
|
||||
|
||||
if data.assigned_id is not None:
|
||||
user_stmt = select(UserModel).where(
|
||||
UserModel.id == data.assigned_id,
|
||||
UserModel.is_deleted.is_(False),
|
||||
)
|
||||
user_result = await self.db.execute(user_stmt)
|
||||
assigned_user = user_result.scalar_one_or_none()
|
||||
if not assigned_user:
|
||||
raise CustomException(msg="指定的处理人不存在")
|
||||
|
||||
updated = await TicketCRUD(self.auth, self.db).update(id=id, data=data)
|
||||
if not updated:
|
||||
raise CustomException(msg="工单不存在")
|
||||
|
||||
return await self.detail(id=updated.id)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="删除对象不能为空")
|
||||
await TicketCRUD(self.auth, self.db).delete(ids=ids)
|
||||
|
||||
async def batch(self, data: TicketBatchSchema) -> None:
|
||||
if not data.ids:
|
||||
raise CustomException(msg="请选择要操作的工单")
|
||||
|
||||
tickets = await TicketCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)})
|
||||
ticket_map = {t.id: t for t in tickets}
|
||||
for tid in data.ids:
|
||||
obj = ticket_map.get(tid)
|
||||
if not obj:
|
||||
raise CustomException(msg=f"工单[{tid}]不存在")
|
||||
self._validate_status_transition(obj, data.status)
|
||||
await TicketCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
||||
|
||||
async def get_list(
|
||||
self,
|
||||
search: TicketQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[TicketOutSchema]:
|
||||
obj_list = await TicketCRUD(self.auth, self.db).get_list(
|
||||
search=search_to_dict(search),
|
||||
order_by=order_by or [{"created_time": "desc"}],
|
||||
preload=_TICKET_PRELOAD,
|
||||
)
|
||||
return [TicketOutSchema.model_validate(obj) for obj in obj_list]
|
||||
|
||||
@staticmethod
|
||||
def export_list(ticket_list: list[dict[str, Any]]) -> bytes:
|
||||
"""导出工单列表"""
|
||||
mapping_dict = {
|
||||
"id": "工单编号",
|
||||
"title": "工单标题",
|
||||
"ticket_type": "工单类型",
|
||||
"summary": "工单摘要",
|
||||
"status": "工单状态",
|
||||
"description": "备注",
|
||||
"created_time": "创建时间",
|
||||
"updated_time": "更新时间",
|
||||
}
|
||||
return ExcelUtil.export_list2excel(list_data=ticket_list, mapping_dict=mapping_dict)
|
||||
|
||||
|
||||
class TicketCommentService:
|
||||
"""工单评论服务"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
self.auth = auth
|
||||
self.db = db
|
||||
|
||||
async def page(self, ticket_id: int, page_no: int, page_size: int) -> PageResultSchema[TicketCommentOutSchema]:
|
||||
return await TicketCommentCRUD(self.auth, self.db).page(
|
||||
offset=(page_no - 1) * page_size,
|
||||
limit=page_size,
|
||||
order_by=[{"created_time": "desc"}],
|
||||
search={"ticket_id": ("eq", ticket_id)},
|
||||
out_schema=TicketCommentOutSchema,
|
||||
)
|
||||
|
||||
async def create(self, ticket_id: int, data: TicketCommentCreateSchema) -> TicketCommentOutSchema:
|
||||
# 验证工单存在
|
||||
await TicketCRUD(self.auth, self.db).get_or_404(id=ticket_id, msg="工单不存在")
|
||||
create_data = data.model_dump() | {"ticket_id": ticket_id}
|
||||
obj = await TicketCommentCRUD(self.auth, self.db).create(data=create_data) # type: ignore[arg-type]
|
||||
if not obj:
|
||||
raise CustomException(msg="评论失败")
|
||||
return TicketCommentOutSchema.model_validate(obj)
|
||||
|
||||
async def delete(self, comment_id: int) -> None:
|
||||
await TicketCommentCRUD(self.auth, self.db).get_or_404(id=comment_id, msg="评论不存在")
|
||||
await TicketCommentCRUD(self.auth, self.db).delete(ids=[comment_id])
|
||||
Reference in New Issue
Block a user