Files
Your Name cf46b27610 fix: P0-1 RateLimiter并发写安全 + P0-2工单操作错误码区分 + P1 rows.Close修复
P0-1 (limits.go): Allow()方法改为全程使用写锁保护counters map读写,避免RLock写入时的data race
P0-2 (ticket_workflow.go+ticket_handler.go): Assign/Resolve/Close操作先查询ticket存在性和状态,返回明确的CS_TICKET_4001/CS_TKT_4002/CS_TICKET_4092/CS_TICKET_4093错误码,handler根据错误前缀路由HTTP状态码
P1-1 (ticket_store.go): 移除GetStats中3处手动rows.Close(),只保留defer Close()
2026-05-01 20:56:25 +08:00

97 lines
2.3 KiB
Go

package memory
import (
"context"
"sync"
"github.com/bridge/ai-customer-service/internal/domain/ticket"
"github.com/bridge/ai-customer-service/internal/domain/ticketstats"
)
type TicketStore struct {
mu sync.RWMutex
tickets []ticket.Ticket
}
func NewTicketStore() *TicketStore {
return &TicketStore{tickets: make([]ticket.Ticket, 0, 8)}
}
func (s *TicketStore) Create(_ context.Context, t *ticket.Ticket) error {
s.mu.Lock()
defer s.mu.Unlock()
s.tickets = append(s.tickets, *t)
return nil
}
func (s *TicketStore) List() []ticket.Ticket {
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]ticket.Ticket, len(s.tickets))
copy(items, s.tickets)
return items
}
func (s *TicketStore) ListAll(_ context.Context) ([]ticket.Ticket, error) {
return s.List(), nil
}
func (s *TicketStore) GetByID(_ context.Context, id string) (*ticket.Ticket, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for i := range s.tickets {
if s.tickets[i].ID == id {
return &s.tickets[i], nil
}
}
return nil, nil
}
// GetStats aggregates ticket statistics in memory.
func (s *TicketStore) GetStats(_ context.Context) (ticketstats.Stats, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var stats ticketstats.Stats
stats.ByChannel = make(map[string]int)
stats.ByPriority = make(map[string]int)
for _, t := range s.tickets {
stats.Total++
// Count by status
switch t.Status {
case ticket.StatusOpen, ticket.StatusAssigned, ticket.StatusProcessing:
stats.Open++
case ticket.StatusResolved:
stats.Resolved++
case ticket.StatusClosed:
stats.Closed++
}
// Count by priority
stats.ByPriority[string(t.Priority)]++
// Channel from context snapshot
if ch, ok := t.ContextSnapshot["channel"].(string); ok {
stats.ByChannel[ch]++
}
// Handoff count
if t.HandoffReason != "" {
stats.HandoffCount++
}
// Resolution time
if t.ResolvedAt != nil {
diff := t.ResolvedAt.Sub(t.CreatedAt).Seconds()
stats.AvgResolutionTimeMinutes += diff / 60.0
}
}
// Compute average resolution time
resolvedCount := stats.Resolved + stats.Closed
if resolvedCount > 0 {
stats.AvgResolutionTimeMinutes /= float64(resolvedCount)
}
return stats, nil
}
// Assign, Resolve, Close, ListOpen are defined in ticket_workflow.go
// to match the handlers.TicketService interface signature.