diff --git a/backend/internal/handler/ticket.go b/backend/internal/handler/ticket.go index d4aca4c..b3f8949 100644 --- a/backend/internal/handler/ticket.go +++ b/backend/internal/handler/ticket.go @@ -138,7 +138,8 @@ func ListTickets(c *gin.Context) { model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type = 'execute'", t.ID).Count(&execCount) var isExec, isAppr int64 model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type = 'execute' AND approver_id = ?", t.ID, userID).Count(&isExec) - model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type != 'execute' AND step_type != 'submit' AND approver_id = ?", t.ID, userID).Count(&isAppr) + // Only count PENDING approvals (action IS NULL) — already-approved records should not show approve button + model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type != 'execute' AND step_type != 'submit' AND approver_id = ? AND action IS NULL", t.ID, userID).Count(&isAppr) result = append(result, TicketWithMeta{ Ticket: t, HasAuditStep: auditCount > 0, @@ -496,24 +497,14 @@ func ApproveTicket(c *gin.Context) { now := time.Now() - // Update approval record — auto-create if missing (legacy tickets) + // Find PENDING approval for this user (action IS NULL) var approval model.TicketApproval - if err := model.DB.Where("ticket_id = ? AND approver_id = ? AND action IS NULL", id, userID).First(&approval).Error; err != nil { - // No approval record — authorized role can create one - stepName := "审核" - stepType := "audit" - approverRole := userRole - approval = model.TicketApproval{ - TicketID: ticket.ID, - StepNumber: 1, - StepName: &stepName, - StepType: &stepType, - ApproverID: &userID, - ApproverRole: &approverRole, - } - model.DB.Create(&approval) + if err := model.DB.Where("ticket_id = ? AND approver_id = ? AND action IS NULL AND step_type != 'execute'", id, userID).First(&approval).Error; err != nil { + // Already approved or no pending approval for this user + c.JSON(http.StatusBadRequest, gin.H{"detail": "没有待处理的审批记录(你可能已经审批过了)"}) + return } - + approval.Action = &input.Action approval.Comment = input.Comment approval.ActualApproverID = &userID diff --git a/frontend/src/pages/TicketsPage.tsx b/frontend/src/pages/TicketsPage.tsx index 6812a97..9437d0e 100644 --- a/frontend/src/pages/TicketsPage.tsx +++ b/frontend/src/pages/TicketsPage.tsx @@ -6,7 +6,7 @@ import { ArrowUpDown, Ban, ChevronLeft, AlertTriangle, } from 'lucide-react'; import { statusMap, statusColorMap, dbTypeColorMap } from '@/lib/constants'; -import { useActionPermission } from '@/hooks/useAuth'; +import { useActionPermission, useAuth } from '@/hooks/useAuth'; import PageHeader from '@/components/PageHeader'; import EmptyState from '@/components/EmptyState'; import { ticketApi } from '@/api/ticket'; @@ -190,6 +190,7 @@ function TableRowSkeleton() { /* ------------------------------------------------------------------ */ export default function TicketsPage() { const navigate = useNavigate(); + const { user: currentUser } = useAuth(); const canCreate = useActionPermission('ticket.create'); const canApprove = useActionPermission('ticket.approve'); const canExecute = useActionPermission('ticket.execute'); @@ -455,7 +456,10 @@ export default function TicketsPage() { try { await ticketApi.review(ticket.id, { action: 'approve', comment: '通过' }); fetchTickets(); fetchStats(); - } catch { /* ignore */ } + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || '审批失败'; + alert(msg); + } }} className="dg-btn dg-btn-sm dg-btn-ghost text-green-400 hover:text-green-300" > @@ -470,7 +474,10 @@ export default function TicketsPage() { try { await ticketApi.review(ticket.id, { action: 'reject', comment: '拒绝' }); fetchTickets(); fetchStats(); - } catch { /* ignore */ } + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || '操作失败'; + alert(msg); + } }} className="dg-btn dg-btn-sm dg-btn-ghost text-red-400 hover:text-red-300" > @@ -494,7 +501,7 @@ export default function TicketsPage() { )} - {(ticket.status === 'pending' || ticket.status === 'reviewing' || ticket.status === 'approved') && ( + {currentUser?.id === ticket.applicant_id && (ticket.status === 'pending' || ticket.status === 'reviewing' || ticket.status === 'approved') && (