mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
1. add TicketStatsSchema for work status aggregate 2. add stats method to TicketService to count pending/processing/done tickets 3. add ticket stats api endpoint 4. optimize server local ip detection with fallback logic
210 lines
8.2 KiB
Python
210 lines
8.2 KiB
Python
from typing import Any
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.base_schema import AuthSchema, PageResultSchema
|
|
from app.core.exceptions import CustomException
|
|
from app.modules.system.user.crud import UserCRUD
|
|
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,
|
|
TicketStatsSchema,
|
|
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:
|
|
# 走 UserCRUD.exists:软删过滤与数据权限由 CRUDBase 统一生效,不再直读整行
|
|
if not await UserCRUD(self.auth, self.db).exists(id=data.assigned_id):
|
|
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]
|
|
|
|
async def stats(self) -> TicketStatsSchema:
|
|
"""按状态聚合工单数量(待处理/处理中/已完成,已关闭不计入)。
|
|
|
|
走 CRUDBase.count 以统一继承软删过滤与数据权限;同一 AsyncSession
|
|
不能并发执行,三次 COUNT 顺序发起。
|
|
"""
|
|
crud = TicketCRUD(self.auth, self.db)
|
|
return TicketStatsSchema(
|
|
pending=await crud.count(status=("eq", 0)),
|
|
processing=await crud.count(status=("eq", 1)),
|
|
done=await crud.count(status=("eq", 2)),
|
|
)
|
|
|
|
@staticmethod
|
|
async def export_list(ticket_list: list[dict[str, Any]]) -> bytes:
|
|
"""导出工单列表"""
|
|
mapping_dict = {
|
|
"id": "工单编号",
|
|
"title": "工单标题",
|
|
"ticket_type": "工单类型",
|
|
"summary": "工单摘要",
|
|
"status": "工单状态",
|
|
"description": "备注",
|
|
"created_time": "创建时间",
|
|
"updated_time": "更新时间",
|
|
}
|
|
return await ExcelUtil.aexport_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])
|