refactor(ticket): rename TicketNoService to TicketNoSequenceService and update references
This commit is contained in:
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -11,29 +11,29 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var TicketNoService = newTicketNoService()
|
var TicketNoSequenceService = newTicketNoSequenceService()
|
||||||
|
|
||||||
var ticketNoSQLiteMu sync.Mutex
|
func newTicketNoSequenceService() *ticketNoSequenceService {
|
||||||
|
return &ticketNoSequenceService{}
|
||||||
func newTicketNoService() *ticketNoService {
|
|
||||||
return &ticketNoService{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if tx == nil {
|
||||||
return "", fmt.Errorf("ticket number transaction is required")
|
return "", fmt.Errorf("ticket number transaction is required")
|
||||||
}
|
}
|
||||||
if tx.Dialector.Name() == "sqlite" {
|
if tx.Dialector.Name() == "sqlite" {
|
||||||
ticketNoSQLiteMu.Lock()
|
s.ticketNoSQLiteMu.Lock()
|
||||||
defer ticketNoSQLiteMu.Unlock()
|
defer s.ticketNoSQLiteMu.Unlock()
|
||||||
return s.nextWithRetry(tx, now)
|
return s.nextWithRetry(tx, now)
|
||||||
}
|
}
|
||||||
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")
|
dateKey := now.Format("20060102")
|
||||||
for attempt := 0; attempt < 100; attempt++ {
|
for attempt := 0; attempt < 100; attempt++ {
|
||||||
current, err := repositories.TicketNoSequenceRepository.GetByDateKeyForUpdate(tx, dateKey)
|
current, err := repositories.TicketNoSequenceRepository.GetByDateKeyForUpdate(tx, dateKey)
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *
|
|||||||
|
|
||||||
if err := withSQLiteTicketCreateLock(sqls.DB(), func() error {
|
if err := withSQLiteTicketCreateLock(sqls.DB(), func() error {
|
||||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ func TestTicketServiceTicketNoNextConcurrent(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
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 { toast } from "sonner"
|
||||||
|
|
||||||
|
import { type CustomerFormSavePayload } from "@/components/customer-form"
|
||||||
|
import { CustomerFormDialog } from "@/components/customer-form-dialog"
|
||||||
import { ProjectDialog } from "@/components/project-dialog"
|
import { ProjectDialog } from "@/components/project-dialog"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { saveCustomerProfile } from "@/lib/api/customer"
|
||||||
import {
|
import {
|
||||||
changeTicketStatus,
|
changeTicketStatus,
|
||||||
createTicketProgress,
|
createTicketProgress,
|
||||||
@@ -69,6 +72,8 @@ export function TicketDetailDialog({
|
|||||||
const [assignOpen, setAssignOpen] = useState(false)
|
const [assignOpen, setAssignOpen] = useState(false)
|
||||||
const [editOpen, setEditOpen] = useState(false)
|
const [editOpen, setEditOpen] = useState(false)
|
||||||
const [editSaving, setEditSaving] = useState(false)
|
const [editSaving, setEditSaving] = useState(false)
|
||||||
|
const [customerEditOpen, setCustomerEditOpen] = useState(false)
|
||||||
|
const [customerEditSaving, setCustomerEditSaving] = useState(false)
|
||||||
const loadSeqRef = useRef(0)
|
const loadSeqRef = useRef(0)
|
||||||
const dialogSeqRef = useRef(0)
|
const dialogSeqRef = useRef(0)
|
||||||
const currentTicketIdRef = useRef<number | null>(null)
|
const currentTicketIdRef = useRef<number | null>(null)
|
||||||
@@ -115,8 +120,10 @@ export function TicketDetailDialog({
|
|||||||
setStatusSaving(null)
|
setStatusSaving(null)
|
||||||
setProgressSaving(false)
|
setProgressSaving(false)
|
||||||
setEditSaving(false)
|
setEditSaving(false)
|
||||||
|
setCustomerEditSaving(false)
|
||||||
setAssignOpen(false)
|
setAssignOpen(false)
|
||||||
setEditOpen(false)
|
setEditOpen(false)
|
||||||
|
setCustomerEditOpen(false)
|
||||||
setProgressContent("")
|
setProgressContent("")
|
||||||
}, [open, ticketId])
|
}, [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
|
const ticket = detail?.ticket
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -332,13 +375,30 @@ export function TicketDetailDialog({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-3 rounded-md border p-3 text-sm sm:grid-cols-2">
|
<section className="space-y-3 rounded-md border p-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="font-medium text-muted-foreground">客户信息</div>
|
||||||
|
{ticket.customerId > 0 ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 shrink-0 gap-1 px-2 text-xs"
|
||||||
|
onClick={() => setCustomerEditOpen(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="size-3.5" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<MetadataItem label="客户" value={ticket.customer?.name || ticket.customerId} />
|
<MetadataItem label="客户" value={ticket.customer?.name || ticket.customerId} />
|
||||||
<MetadataItem label="联系方式" value={ticket.customer?.primaryMobile || ticket.customer?.primaryEmail} />
|
<MetadataItem label="联系方式" value={ticket.customer?.primaryMobile || ticket.customer?.primaryEmail} />
|
||||||
<MetadataItem label="来源" value={sourceLabel(ticket.source)} />
|
<MetadataItem label="来源" value={sourceLabel(ticket.source)} />
|
||||||
<MetadataItem label="渠道" value={ticket.channel} />
|
<MetadataItem label="渠道" value={ticket.channel} />
|
||||||
<MetadataItem label="会话 ID" value={ticket.conversationId || undefined} />
|
<MetadataItem label="会话 ID" value={ticket.conversationId || undefined} />
|
||||||
<MetadataItem label="最后更新" value={ticket.updatedAt ? formatDateTime(ticket.updatedAt) : undefined} />
|
<MetadataItem label="最后更新" value={ticket.updatedAt ? formatDateTime(ticket.updatedAt) : undefined} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -413,6 +473,13 @@ export function TicketDetailDialog({
|
|||||||
onOpenChange={setEditOpen}
|
onOpenChange={setEditOpen}
|
||||||
onSubmit={handleUpdateTicket}
|
onSubmit={handleUpdateTicket}
|
||||||
/>
|
/>
|
||||||
|
<CustomerFormDialog
|
||||||
|
open={customerEditOpen}
|
||||||
|
onOpenChange={setCustomerEditOpen}
|
||||||
|
saving={customerEditSaving}
|
||||||
|
itemId={ticket?.customerId ? ticket.customerId : null}
|
||||||
|
onSave={handleUpdateCustomer}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user