package handler import ( "net/http" "sort" "time" "github.com/gin-gonic/gin" "kefu-cloud/server/internal/middleware" "kefu-cloud/server/internal/model" ) type StatisticsHandler struct{} func NewStatisticsHandler() *StatisticsHandler { return &StatisticsHandler{} } func requireStatisticsAccess(c *gin.Context) bool { if middleware.HasAnyRole(c, "admin", "supervisor") { return true } c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可查看统计"}) return false } func loadStatisticsData(tenantID uint) ([]model.Session, []model.Message, error) { var sessions []model.Session if err := model.DB.Where("tenant_id = ?", tenantID).Order("created_at asc").Find(&sessions).Error; err != nil { return nil, nil, err } if len(sessions) == 0 { return sessions, []model.Message{}, nil } ids := make([]uint, 0, len(sessions)) for _, session := range sessions { ids = append(ids, session.ID) } var messages []model.Message if err := model.DB.Where("session_id IN ?", ids).Order("session_id asc, seq asc").Find(&messages).Error; err != nil { return nil, nil, err } return sessions, messages, nil } func firstResponseSeconds(messages []model.Message) map[uint]float64 { visitorFirst := make(map[uint]time.Time) responseSeconds := make(map[uint]float64) for _, message := range messages { if message.SenderType == "visitor" { if _, exists := visitorFirst[message.SessionID]; !exists { visitorFirst[message.SessionID] = message.SentAt } continue } if message.SenderType != "agent" { continue } firstVisitorAt, hasVisitor := visitorFirst[message.SessionID] if !hasVisitor || !message.SentAt.After(firstVisitorAt) { continue } if _, exists := responseSeconds[message.SessionID]; !exists { responseSeconds[message.SessionID] = message.SentAt.Sub(firstVisitorAt).Seconds() } } return responseSeconds } func firstResolveRate(sessions []model.Session) float64 { endedCount := 0 resolvedCount := 0 for index, session := range sessions { if session.Status != "ended" { continue } endedCount++ if session.EndReason != "resolved" && session.EndReason != "已解决" { continue } endedAt := session.CreatedAt if session.EndedAt != nil { endedAt = *session.EndedAt } reopened := false for _, later := range sessions[index+1:] { if later.CustomerID == session.CustomerID && later.CreatedAt.After(endedAt) && later.CreatedAt.Sub(endedAt) <= 24*time.Hour { reopened = true break } } if !reopened { resolvedCount++ } } if endedCount == 0 { return 0 } return float64(resolvedCount) * 100 / float64(endedCount) } func (h *StatisticsHandler) KPIs(c *gin.Context) { if !requireStatisticsAccess(c) { return } sessions, messages, err := loadStatisticsData(middleware.GetTenantID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询统计数据失败"}) return } responses := firstResponseSeconds(messages) var responseTotal float64 for _, seconds := range responses { responseTotal += seconds } var satisfactionTotal float64 satisfactionCount := 0 for _, session := range sessions { if session.SatisfactionScore != nil { satisfactionTotal += float64(*session.SatisfactionScore) satisfactionCount++ } } avgResponse := float64(0) if len(responses) > 0 { avgResponse = responseTotal / float64(len(responses)) } satisfactionAvg := float64(0) if satisfactionCount > 0 { satisfactionAvg = satisfactionTotal / float64(satisfactionCount) } middleware.JSON(c, gin.H{ "total_sessions": len(sessions), "avg_response_time": avgResponse, "satisfaction_avg": satisfactionAvg, "first_resolve_rate": firstResolveRate(sessions), "total_messages": len(messages), }) } func (h *StatisticsHandler) SessionTrend(c *gin.Context) { if !requireStatisticsAccess(c) { return } period := c.DefaultQuery("period", "day") var sessions []model.Session if err := model.DB.Where("tenant_id = ?", middleware.GetTenantID(c)).Find(&sessions).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询会话趋势失败"}) return } now := time.Now() data := make([]gin.H, 0) if period == "today" { count := 0 for _, session := range sessions { if session.CreatedAt.Format("2006-01-02") == now.Format("2006-01-02") { count++ } } data = append(data, gin.H{"date": now.Format("01-02"), "count": count}) } else if period == "month" { counts := make(map[string]int) for _, session := range sessions { counts[session.CreatedAt.Format("2006-01")]++ } for offset := 5; offset >= 0; offset-- { month := time.Date(now.Year(), now.Month()-time.Month(offset), 1, 0, 0, 0, 0, now.Location()) key := month.Format("2006-01") data = append(data, gin.H{"date": key, "count": counts[key]}) } } else { counts := make(map[string]int) for _, session := range sessions { counts[session.CreatedAt.Format("01-02")]++ } for offset := 6; offset >= 0; offset-- { day := now.AddDate(0, 0, -offset) key := day.Format("01-02") data = append(data, gin.H{"date": key, "count": counts[key]}) } } middleware.JSON(c, data) } func (h *StatisticsHandler) ResponseDistribution(c *gin.Context) { if !requireStatisticsAccess(c) { return } _, messages, err := loadStatisticsData(middleware.GetTenantID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询响应时长失败"}) return } counts := map[string]int{"0-10s": 0, "10-30s": 0, "30-60s": 0, "1-3min": 0, ">3min": 0} for _, seconds := range firstResponseSeconds(messages) { switch { case seconds <= 10: counts["0-10s"]++ case seconds <= 30: counts["10-30s"]++ case seconds <= 60: counts["30-60s"]++ case seconds <= 180: counts["1-3min"]++ default: counts[">3min"]++ } } data := []gin.H{} for _, key := range []string{"0-10s", "10-30s", "30-60s", "1-3min", ">3min"} { data = append(data, gin.H{"range": key, "count": counts[key]}) } middleware.JSON(c, data) } func (h *StatisticsHandler) AgentPerformance(c *gin.Context) { if !requireStatisticsAccess(c) { return } tenantID := middleware.GetTenantID(c) sessions, messages, err := loadStatisticsData(tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询客服绩效失败"}) return } var agents []model.User if err := model.DB.Where("tenant_id = ? AND role = ?", tenantID, "agent").Find(&agents).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询客服失败"}) return } responses := firstResponseSeconds(messages) type performance struct { ID uint Name string Conversations int ResponseTotal float64 ResponseCount int Satisfaction float64 RatingCount int } items := make(map[uint]*performance) for _, agent := range agents { items[agent.ID] = &performance{ID: agent.ID, Name: agent.Nickname} } for _, session := range sessions { if session.AgentID == nil || items[*session.AgentID] == nil { continue } item := items[*session.AgentID] item.Conversations++ if seconds, exists := responses[session.ID]; exists { item.ResponseTotal += seconds item.ResponseCount++ } if session.SatisfactionScore != nil { item.Satisfaction += float64(*session.SatisfactionScore) item.RatingCount++ } } values := make([]*performance, 0, len(items)) for _, item := range items { values = append(values, item) } sort.Slice(values, func(i, j int) bool { return values[i].Conversations > values[j].Conversations }) data := make([]gin.H, 0, len(values)) for _, item := range values { avgResponse := float64(0) if item.ResponseCount > 0 { avgResponse = item.ResponseTotal / float64(item.ResponseCount) } satisfaction := float64(0) if item.RatingCount > 0 { satisfaction = item.Satisfaction / float64(item.RatingCount) } data = append(data, gin.H{"name": item.Name, "conversations": item.Conversations, "avg_response": avgResponse, "satisfaction": satisfaction}) } middleware.JSON(c, data) } func (h *StatisticsHandler) ChannelDistribution(c *gin.Context) { if !requireStatisticsAccess(c) { return } tenantID := middleware.GetTenantID(c) var sessions []model.Session var channels []model.Channel if err := model.DB.Where("tenant_id = ?", tenantID).Find(&sessions).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询渠道分布失败"}) return } if err := model.DB.Where("tenant_id = ?", tenantID).Find(&channels).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询渠道分布失败"}) return } channelTypes := make(map[uint]string) for _, channel := range channels { channelTypes[channel.ID] = channel.Type } labels := map[string]string{"web": "网页", "wechat": "微信", "app": "APP", "phone": "电话工单", "email": "邮件"} counts := make(map[string]int) for _, session := range sessions { channelType := channelTypes[session.ChannelID] if channelType == "" { channelType = "unknown" } counts[channelType]++ } keys := make([]string, 0, len(counts)) for key := range counts { keys = append(keys, key) } sort.Strings(keys) data := make([]gin.H, 0, len(keys)) for _, key := range keys { label := labels[key] if label == "" { label = key } data = append(data, gin.H{"type": label, "value": counts[key]}) } middleware.JSON(c, data) }