优化快捷回复联想与个人调用统计,数据统计支持自定义日期

/ 按输入码与个人频次建议(最多10条,唯一自动填入),正文关键字联想;统计页可日历选区间并统一中文日历。
This commit is contained in:
yml2213
2026-07-19 01:10:16 +08:00
parent c6532f2ee3
commit 1a3b32868c
9 changed files with 488 additions and 114 deletions
+112 -24
View File
@@ -3,6 +3,7 @@ package handler
import (
"net/http"
"sort"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -22,9 +23,60 @@ func requireStatisticsAccess(c *gin.Context) bool {
return false
}
func loadStatisticsData(tenantID uint) ([]model.Session, []model.Message, error) {
// statsRange 统计时间窗:[From, To) 本地日界
type statsRange struct {
From time.Time
To time.Time // exclusive
}
// parseStatsRange 支持 from&to=YYYY-MM-DD,或 period=today|week|month(默认 week
func parseStatsRange(c *gin.Context) (statsRange, error) {
loc := time.Now().Location()
now := time.Now().In(loc)
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
fromStr := strings.TrimSpace(c.Query("from"))
toStr := strings.TrimSpace(c.Query("to"))
if fromStr != "" && toStr != "" {
fromDay, err1 := time.ParseInLocation("2006-01-02", fromStr, loc)
toDay, err2 := time.ParseInLocation("2006-01-02", toStr, loc)
if err1 != nil || err2 != nil {
return statsRange{}, errStr("日期格式应为 YYYY-MM-DD")
}
if toDay.Before(fromDay) {
return statsRange{}, errStr("结束日期不能早于开始日期")
}
// 最多 366 天
if toDay.Sub(fromDay) > 366*24*time.Hour {
return statsRange{}, errStr("日期区间最长 366 天")
}
return statsRange{From: fromDay, To: toDay.AddDate(0, 0, 1)}, nil
}
period := strings.TrimSpace(c.DefaultQuery("period", "week"))
switch period {
case "today":
return statsRange{From: today, To: today.AddDate(0, 0, 1)}, nil
case "month":
// 本自然月
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
nextMonth := monthStart.AddDate(0, 1, 0)
return statsRange{From: monthStart, To: nextMonth}, nil
case "week", "day":
// 近 7 天(含今天)
return statsRange{From: today.AddDate(0, 0, -6), To: today.AddDate(0, 0, 1)}, nil
default:
return statsRange{From: today.AddDate(0, 0, -6), To: today.AddDate(0, 0, 1)}, nil
}
}
func loadStatisticsData(tenantID uint, r statsRange) ([]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 {
q := model.DB.Where("tenant_id = ?", tenantID)
if !r.From.IsZero() {
q = q.Where("created_at >= ? AND created_at < ?", r.From, r.To)
}
if err := q.Order("created_at asc").Find(&sessions).Error; err != nil {
return nil, nil, err
}
if len(sessions) == 0 {
@@ -101,7 +153,12 @@ func (h *StatisticsHandler) KPIs(c *gin.Context) {
if !requireStatisticsAccess(c) {
return
}
sessions, messages, err := loadStatisticsData(middleware.GetTenantID(c))
r, err := parseStatsRange(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
sessions, messages, err := loadStatisticsData(middleware.GetTenantID(c), r)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询统计数据失败"})
return
@@ -141,23 +198,30 @@ func (h *StatisticsHandler) SessionTrend(c *gin.Context) {
if !requireStatisticsAccess(c) {
return
}
period := c.DefaultQuery("period", "day")
r, err := parseStatsRange(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// 是否有自定义区间:有 from/to 时按日粒度;本月预置可按月展示近 6 月
hasCustom := strings.TrimSpace(c.Query("from")) != "" && strings.TrimSpace(c.Query("to")) != ""
period := strings.TrimSpace(c.DefaultQuery("period", "week"))
var sessions []model.Session
if err := model.DB.Where("tenant_id = ?", middleware.GetTenantID(c)).Find(&sessions).Error; err != nil {
q := model.DB.Where("tenant_id = ?", middleware.GetTenantID(c))
// 趋势:自定义/今日/本周用区间内数据;本月预置仍看近 6 个月走势
if hasCustom || period == "today" || period == "week" || period == "day" {
q = q.Where("created_at >= ? AND created_at < ?", r.From, r.To)
}
if err := q.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" {
if !hasCustom && period == "month" {
// 近 6 个自然月
now := time.Now()
counts := make(map[string]int)
for _, session := range sessions {
counts[session.CreatedAt.Format("2006-01")]++
@@ -168,14 +232,19 @@ func (h *StatisticsHandler) SessionTrend(c *gin.Context) {
data = append(data, gin.H{"date": key, "count": counts[key]})
}
} else {
// 按日:从 From 到 To(不含)
counts := make(map[string]int)
for _, session := range sessions {
counts[session.CreatedAt.Format("01-02")]++
counts[session.CreatedAt.Format("2006-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]})
for d := r.From; d.Before(r.To); d = d.AddDate(0, 0, 1) {
key := d.Format("2006-01-02")
label := d.Format("01-02")
// 跨年区间显示完整日期
if r.To.Sub(r.From) > 180*24*time.Hour || d.Year() != time.Now().Year() {
label = d.Format("2006-01-02")
}
data = append(data, gin.H{"date": label, "count": counts[key]})
}
}
middleware.JSON(c, data)
@@ -185,7 +254,12 @@ func (h *StatisticsHandler) ResponseDistribution(c *gin.Context) {
if !requireStatisticsAccess(c) {
return
}
_, messages, err := loadStatisticsData(middleware.GetTenantID(c))
r, err := parseStatsRange(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
_, messages, err := loadStatisticsData(middleware.GetTenantID(c), r)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询响应时长失败"})
return
@@ -216,8 +290,13 @@ func (h *StatisticsHandler) AgentPerformance(c *gin.Context) {
if !requireStatisticsAccess(c) {
return
}
r, err := parseStatsRange(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
tenantID := middleware.GetTenantID(c)
sessions, messages, err := loadStatisticsData(tenantID)
sessions, messages, err := loadStatisticsData(tenantID, r)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询客服绩效失败"})
return
@@ -280,10 +359,19 @@ func (h *StatisticsHandler) ChannelDistribution(c *gin.Context) {
if !requireStatisticsAccess(c) {
return
}
r, err := parseStatsRange(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
tenantID := middleware.GetTenantID(c)
var sessions []model.Session
sessions, _, err := loadStatisticsData(tenantID, r)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询渠道分布失败"})
return
}
var channels []model.Channel
if err := model.DB.Where("tenant_id = ?", tenantID).Find(&sessions).Error; err != nil {
if err := model.DB.Where("tenant_id = ?", tenantID).Find(&channels).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询渠道分布失败"})
return
}