diff --git a/backend/apps/tickets/migrations/0003_repair_closed_resolved_flags.py b/backend/apps/tickets/migrations/0003_repair_closed_resolved_flags.py new file mode 100644 index 0000000..3a2bce3 --- /dev/null +++ b/backend/apps/tickets/migrations/0003_repair_closed_resolved_flags.py @@ -0,0 +1,21 @@ +from django.db import migrations + + +def repair_closed_resolved_flags(apps, schema_editor): + Ticket = apps.get_model("tickets", "Ticket") + Ticket.objects.filter( + status="closed", + resolved_at__isnull=False, + resolution_summary__gt="", + is_resolved=False, + ).update(is_resolved=True) + + +class Migration(migrations.Migration): + dependencies = [ + ("tickets", "0002_ticket_context_and_standard_categories"), + ] + + operations = [ + migrations.RunPython(repair_closed_resolved_flags, migrations.RunPython.noop), + ] diff --git a/backend/apps/tickets/services.py b/backend/apps/tickets/services.py index 9cbaff0..89e03a8 100644 --- a/backend/apps/tickets/services.py +++ b/backend/apps/tickets/services.py @@ -95,7 +95,7 @@ def transition_ticket( if to_status == "resolved": ticket.resolved_at = timezone.now() ticket.is_resolved = True if is_resolved is None else is_resolved - elif is_resolved is not None: + elif to_status != "closed" and is_resolved is not None: ticket.is_resolved = is_resolved if to_status == "closed": ticket.closed_at = timezone.now() diff --git a/backend/apps/tickets/tests/test_ticket_api.py b/backend/apps/tickets/tests/test_ticket_api.py index dfa320c..8b10663 100644 --- a/backend/apps/tickets/tests/test_ticket_api.py +++ b/backend/apps/tickets/tests/test_ticket_api.py @@ -192,6 +192,28 @@ def test_technical_ticket_cannot_be_resolved_directly(admin_client, db): assert response.status_code == 400 +def test_closing_resolved_ticket_keeps_resolved_flag(admin_client, db): + ticket = Ticket.objects.create( + ticket_no="TK202607010106", + title="已解决待关闭", + description="A", + status="resolved", + is_resolved=True, + resolution_summary="已完成处理", + ) + + response = admin_client.post( + f"/api/tickets/{ticket.id}/status", + data={"status": "closed", "remark": "已回复", "is_resolved": False}, + content_type="application/json", + ) + + assert response.status_code == 200 + ticket.refresh_from_db() + assert ticket.status == "closed" + assert ticket.is_resolved is True + + def test_get_ticket_events_returns_real_timeline_with_operator(admin_client, db): user_model = get_user_model() operator = user_model.objects.get(username="admin") diff --git a/frontend/src/features/tickets/TicketDetailPage.tsx b/frontend/src/features/tickets/TicketDetailPage.tsx index 735e141..bc80417 100644 --- a/frontend/src/features/tickets/TicketDetailPage.tsx +++ b/frontend/src/features/tickets/TicketDetailPage.tsx @@ -90,7 +90,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { values: { remark: string resolution: string - is_resolved: boolean + is_resolved?: boolean replied_to_customer: boolean } ) { diff --git a/frontend/src/features/tickets/TicketTransitionDialog.tsx b/frontend/src/features/tickets/TicketTransitionDialog.tsx index c4a9360..c005bbf 100644 --- a/frontend/src/features/tickets/TicketTransitionDialog.tsx +++ b/frontend/src/features/tickets/TicketTransitionDialog.tsx @@ -32,7 +32,7 @@ type TicketTransitionDialogProps = { values: { remark: string resolution: string - is_resolved: boolean + is_resolved?: boolean replied_to_customer: boolean } ) => Promise @@ -70,7 +70,7 @@ export function TicketTransitionDialog({ await onSubmit(status, { remark: remark.trim(), resolution: resolution.trim(), - is_resolved: status === 'resolved', + is_resolved: status === 'resolved' ? true : undefined, replied_to_customer: repliedToCustomer }) onOpenChange(false)