diff --git a/internal/services/ticket_no_sequence_service.go b/internal/services/ticket_no_sequence_service.go deleted file mode 100644 index c322768..0000000 --- a/internal/services/ticket_no_sequence_service.go +++ /dev/null @@ -1,73 +0,0 @@ -package services - -import ( - "time" - - "cs-agent/internal/models" - "cs-agent/internal/repositories" - - "github.com/mlogclub/simple/sqls" - "github.com/mlogclub/simple/web/params" - "gorm.io/gorm" -) - -var TicketNoSequenceService = newTicketNoSequenceService() - -func newTicketNoSequenceService() *ticketNoSequenceService { - return &ticketNoSequenceService{} -} - -type ticketNoSequenceService struct { -} - -func (s *ticketNoSequenceService) Get(id int64) *models.TicketNoSequence { - return repositories.TicketNoSequenceRepository.Get(sqls.DB(), id) -} - -func (s *ticketNoSequenceService) Take(where ...any) *models.TicketNoSequence { - return repositories.TicketNoSequenceRepository.Take(sqls.DB(), where...) -} - -func (s *ticketNoSequenceService) Find(cnd *sqls.Cnd) []models.TicketNoSequence { - return repositories.TicketNoSequenceRepository.Find(sqls.DB(), cnd) -} - -func (s *ticketNoSequenceService) FindOne(cnd *sqls.Cnd) *models.TicketNoSequence { - return repositories.TicketNoSequenceRepository.FindOne(sqls.DB(), cnd) -} - -func (s *ticketNoSequenceService) FindPageByParams(params *params.QueryParams) (list []models.TicketNoSequence, paging *sqls.Paging) { - return repositories.TicketNoSequenceRepository.FindPageByParams(sqls.DB(), params) -} - -func (s *ticketNoSequenceService) FindPageByCnd(cnd *sqls.Cnd) (list []models.TicketNoSequence, paging *sqls.Paging) { - return repositories.TicketNoSequenceRepository.FindPageByCnd(sqls.DB(), cnd) -} - -func (s *ticketNoSequenceService) Count(cnd *sqls.Cnd) int64 { - return repositories.TicketNoSequenceRepository.Count(sqls.DB(), cnd) -} - -func (s *ticketNoSequenceService) Create(t *models.TicketNoSequence) error { - return repositories.TicketNoSequenceRepository.Create(sqls.DB(), t) -} - -func (s *ticketNoSequenceService) Update(t *models.TicketNoSequence) error { - return repositories.TicketNoSequenceRepository.Update(sqls.DB(), t) -} - -func (s *ticketNoSequenceService) Updates(id int64, columns map[string]any) error { - return repositories.TicketNoSequenceRepository.Updates(sqls.DB(), id, columns) -} - -func (s *ticketNoSequenceService) UpdateColumn(id int64, name string, value any) error { - return repositories.TicketNoSequenceRepository.UpdateColumn(sqls.DB(), id, name, value) -} - -func (s *ticketNoSequenceService) Delete(id int64) { - repositories.TicketNoSequenceRepository.Delete(sqls.DB(), id) -} - -func (s *ticketNoSequenceService) Next(db *gorm.DB, now time.Time) (string, error) { - return TicketNoService.Next(db, now) -} diff --git a/internal/services/ticket_no_service.go b/internal/services/ticket_no_service.go index 9bfcf5b..7979886 100644 --- a/internal/services/ticket_no_service.go +++ b/internal/services/ticket_no_service.go @@ -11,29 +11,29 @@ import ( "gorm.io/gorm" ) -var TicketNoService = newTicketNoService() +var TicketNoSequenceService = newTicketNoSequenceService() -var ticketNoSQLiteMu sync.Mutex - -func newTicketNoService() *ticketNoService { - return &ticketNoService{} +func newTicketNoSequenceService() *ticketNoSequenceService { + return &ticketNoSequenceService{} } -type ticketNoService struct{} +type ticketNoSequenceService struct { + ticketNoSQLiteMu sync.Mutex +} -func (s *ticketNoService) Next(tx *gorm.DB, now time.Time) (string, error) { +func (s *ticketNoSequenceService) Next(tx *gorm.DB, now time.Time) (string, error) { if tx == nil { return "", fmt.Errorf("ticket number transaction is required") } if tx.Dialector.Name() == "sqlite" { - ticketNoSQLiteMu.Lock() - defer ticketNoSQLiteMu.Unlock() + s.ticketNoSQLiteMu.Lock() + defer s.ticketNoSQLiteMu.Unlock() return s.nextWithRetry(tx, now) } return s.nextWithRetry(tx, now) } -func (s *ticketNoService) nextWithRetry(tx *gorm.DB, now time.Time) (string, error) { +func (s *ticketNoSequenceService) nextWithRetry(tx *gorm.DB, now time.Time) (string, error) { dateKey := now.Format("20060102") for attempt := 0; attempt < 100; attempt++ { current, err := repositories.TicketNoSequenceRepository.GetByDateKeyForUpdate(tx, dateKey) diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index 325a722..5a88f5d 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -199,7 +199,7 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator * if err := withSQLiteTicketCreateLock(sqls.DB(), func() error { return sqls.WithTransaction(func(ctx *sqls.TxContext) error { - ticketNo, err := TicketNoService.nextWithRetry(ctx.Tx, now) + ticketNo, err := TicketNoSequenceService.nextWithRetry(ctx.Tx, now) if err != nil { return err } diff --git a/internal/services/ticket_service_test.go b/internal/services/ticket_service_test.go index b92a792..402d032 100644 --- a/internal/services/ticket_service_test.go +++ b/internal/services/ticket_service_test.go @@ -391,7 +391,7 @@ func TestTicketServiceTicketNoNextConcurrent(t *testing.T) { go func() { defer wg.Done() err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { - ticketNo, err := services.TicketNoService.Next(ctx.Tx, time.Now()) + ticketNo, err := services.TicketNoSequenceService.Next(ctx.Tx, time.Now()) if err != nil { return err } diff --git a/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx b/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx index a3338f3..58a8f7d 100644 --- a/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx +++ b/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx @@ -1,14 +1,17 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" -import { MessageSquareTextIcon, RefreshCcwIcon, SendIcon, UserRoundIcon } from "lucide-react" +import { MessageSquareTextIcon, PencilIcon, RefreshCcwIcon, SendIcon, UserRoundIcon } from "lucide-react" import { toast } from "sonner" +import { type CustomerFormSavePayload } from "@/components/customer-form" +import { CustomerFormDialog } from "@/components/customer-form-dialog" import { ProjectDialog } from "@/components/project-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { Textarea } from "@/components/ui/textarea" +import { saveCustomerProfile } from "@/lib/api/customer" import { changeTicketStatus, createTicketProgress, @@ -69,6 +72,8 @@ export function TicketDetailDialog({ const [assignOpen, setAssignOpen] = useState(false) const [editOpen, setEditOpen] = useState(false) const [editSaving, setEditSaving] = useState(false) + const [customerEditOpen, setCustomerEditOpen] = useState(false) + const [customerEditSaving, setCustomerEditSaving] = useState(false) const loadSeqRef = useRef(0) const dialogSeqRef = useRef(0) const currentTicketIdRef = useRef(null) @@ -115,8 +120,10 @@ export function TicketDetailDialog({ setStatusSaving(null) setProgressSaving(false) setEditSaving(false) + setCustomerEditSaving(false) setAssignOpen(false) setEditOpen(false) + setCustomerEditOpen(false) setProgressContent("") }, [open, ticketId]) @@ -237,6 +244,42 @@ export function TicketDetailDialog({ } } + async function handleUpdateCustomer(payload: CustomerFormSavePayload) { + if (!ticket?.id || !ticket.customerId) { + toast.error("当前工单未关联客户") + return + } + if (customerEditSaving) { + return + } + const activeTicketId = ticket.id + const activeCustomerId = ticket.customerId + const activeDialogSeq = dialogSeqRef.current + setCustomerEditSaving(true) + try { + await saveCustomerProfile({ ...payload, id: activeCustomerId }) + if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { + return + } + toast.success("已保存") + setCustomerEditOpen(false) + await loadDetail(activeTicketId, activeDialogSeq) + if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { + return + } + onChanged() + } catch (error) { + if (!isCurrentOperation(activeTicketId, activeDialogSeq)) { + return + } + toast.error(error instanceof Error ? error.message : "保存失败") + } finally { + if (isCurrentOperation(activeTicketId, activeDialogSeq)) { + setCustomerEditSaving(false) + } + } + } + const ticket = detail?.ticket return ( @@ -332,13 +375,30 @@ export function TicketDetailDialog({ )} -
- - - - - - +
+
+
客户信息
+ {ticket.customerId > 0 ? ( + + ) : null} +
+
+ + + + + + +
@@ -413,6 +473,13 @@ export function TicketDetailDialog({ onOpenChange={setEditOpen} onSubmit={handleUpdateTicket} /> + ) }