feat: init commint
This commit is contained in:
@@ -316,6 +316,113 @@ def create_ticket_from_fields(
|
||||
return ticket
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_follow_up_ticket(
|
||||
source_ticket: Ticket,
|
||||
customer_feedback: str,
|
||||
*,
|
||||
operator=None,
|
||||
) -> Ticket:
|
||||
"""为已关闭工单的新负向反馈创建唯一活动后续工单。
|
||||
|
||||
已关闭记录保持不可变;后续问题继承必要上下文但不继承处理人,自动化未就绪时
|
||||
仍可按普通工单人工接管。
|
||||
"""
|
||||
|
||||
source_ticket = (
|
||||
Ticket.objects.select_for_update(of=("self",))
|
||||
.select_related("category", "requester", "created_by")
|
||||
.get(id=source_ticket.id)
|
||||
)
|
||||
if source_ticket.status != TicketStatus.CLOSED:
|
||||
raise TicketStatusError("只有已关闭工单才能创建后续工单")
|
||||
existing = (
|
||||
source_ticket.follow_up_tickets.exclude(status=TicketStatus.CLOSED)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
if existing is not None:
|
||||
return existing
|
||||
feedback = customer_feedback.strip()
|
||||
if not feedback:
|
||||
raise TicketDataError("客户反馈不能为空")
|
||||
actor = operator or source_ticket.requester or source_ticket.created_by
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no=next_ticket_no(),
|
||||
title=f"后续问题:{feedback[:170]}"[:200],
|
||||
description=feedback,
|
||||
category=source_ticket.category,
|
||||
status=TicketStatus.PENDING,
|
||||
priority=source_ticket.priority,
|
||||
problem_performance=feedback,
|
||||
problem_object=source_ticket.problem_object,
|
||||
submitter_name=source_ticket.submitter_name,
|
||||
submitter_org=source_ticket.submitter_org,
|
||||
impact_scope=source_ticket.impact_scope,
|
||||
environment=source_ticket.environment,
|
||||
related_system=source_ticket.related_system,
|
||||
product_name=source_ticket.product_name,
|
||||
module_name=source_ticket.module_name,
|
||||
tags=source_ticket.tags,
|
||||
attempted_actions=source_ticket.attempted_actions,
|
||||
error_information=source_ticket.error_information,
|
||||
customer_request=source_ticket.customer_request,
|
||||
current_result=f"客户在原工单关闭后反馈:{feedback}",
|
||||
requester=source_ticket.requester,
|
||||
created_by=actor,
|
||||
follow_up_of=source_ticket,
|
||||
customer_last_message_at=timezone.now(),
|
||||
)
|
||||
TicketEvent.objects.create(
|
||||
ticket=ticket,
|
||||
event_type="created",
|
||||
to_value={
|
||||
"status": ticket.status,
|
||||
"category_code": ticket.category.code if ticket.category_id else None,
|
||||
},
|
||||
operator=operator,
|
||||
source=TicketActorSource.CHANNEL,
|
||||
metadata={
|
||||
"creation_mode": "closed_ticket_follow_up",
|
||||
"source_ticket_id": source_ticket.id,
|
||||
},
|
||||
)
|
||||
TicketEvent.objects.create(
|
||||
ticket=source_ticket,
|
||||
event_type="follow_up_created",
|
||||
to_value={"follow_up_ticket_id": ticket.id, "ticket_no": ticket.ticket_no},
|
||||
operator=operator,
|
||||
source=TicketActorSource.SYSTEM,
|
||||
metadata={"customer_feedback": feedback[:1000]},
|
||||
)
|
||||
for field_name, value in {
|
||||
"description": feedback,
|
||||
"problem_performance": feedback,
|
||||
"current_result": ticket.current_result,
|
||||
}.items():
|
||||
record_field_evidence(
|
||||
ticket,
|
||||
field_name,
|
||||
value,
|
||||
source=TicketActorSource.CHANNEL,
|
||||
operator=operator,
|
||||
evidence={"source_ticket_id": source_ticket.id},
|
||||
)
|
||||
from apps.tickets.similarity import schedule_ticket_similarity_refresh
|
||||
|
||||
schedule_ticket_similarity_refresh(ticket)
|
||||
ticket_id = ticket.id
|
||||
|
||||
def start_follow_up_automation() -> None:
|
||||
from apps.tickets.automation import start_ticket_automation
|
||||
|
||||
follow_up = Ticket.objects.get(id=ticket_id)
|
||||
start_ticket_automation(follow_up, initiated_by=actor)
|
||||
|
||||
transaction.on_commit(start_follow_up_automation)
|
||||
return ticket
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_ticket_from_intake(
|
||||
fields: dict,
|
||||
@@ -828,6 +935,58 @@ def transition_ticket(
|
||||
return ticket
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def reopen_ticket(
|
||||
ticket: Ticket,
|
||||
*,
|
||||
operator=None,
|
||||
remark: str = "客户反馈问题仍未解决,恢复处理",
|
||||
source: str = TicketActorSource.SYSTEM,
|
||||
) -> Ticket:
|
||||
"""在保留既有事件和解决记录的前提下开启新一轮处理。"""
|
||||
|
||||
ticket = Ticket.objects.select_for_update(of=("self",)).get(id=ticket.id)
|
||||
if ticket.status != TicketStatus.RESOLVED:
|
||||
raise TicketStatusError("只有已解决且尚未关闭的工单可以恢复处理")
|
||||
from_status = ticket.status
|
||||
ticket.status = TicketStatus.IN_PROGRESS
|
||||
ticket.resolved_at = None
|
||||
ticket.closed_at = None
|
||||
ticket.replied_to_customer = False
|
||||
ticket.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"resolved_at",
|
||||
"closed_at",
|
||||
"replied_to_customer",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
from apps.workitems.services import sync_ticket_close_work_item
|
||||
|
||||
sync_ticket_close_work_item(ticket, actor=operator)
|
||||
TicketEvent.objects.create(
|
||||
ticket=ticket,
|
||||
event_type="ticket_reopened",
|
||||
from_value={"status": from_status},
|
||||
to_value={"status": TicketStatus.IN_PROGRESS},
|
||||
operator=operator,
|
||||
source=source,
|
||||
remark=remark,
|
||||
)
|
||||
OperationAuditLog.objects.create(
|
||||
user=operator,
|
||||
module="tickets",
|
||||
action="ticket_reopened",
|
||||
target_type="ticket",
|
||||
target_id=str(ticket.id),
|
||||
request_snapshot={"source": source, "remark": remark},
|
||||
result="success",
|
||||
)
|
||||
schedule_ticket_search_index(ticket.id)
|
||||
return ticket
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def adopt_solution_for_ticket(
|
||||
ticket: Ticket,
|
||||
|
||||
Reference in New Issue
Block a user