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

/ 按输入码与个人频次建议(最多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
+91 -11
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -213,13 +214,22 @@ func (h *QuickReplyHandler) List(c *gin.Context) {
middleware.JSONList(c, list, total, page, pageSize)
}
// Suggest GET /api/quick-replies/suggest?prefix=
// 供输入框 / 触发:匹配 shortcut 前缀或标题
const suggestLimit = 10
// Suggest GET /api/quick-replies/suggest
// mode=slash&prefix= → / 调用:按输入码前缀过滤;空前缀返回个人调用频次最高的 10 条
// mode=keyword&q= → 普通输入:按标题/内容/输入码关键字匹配
// 排序一律按当前坐席个人调用次数(非全租户)
func (h *QuickReplyHandler) Suggest(c *gin.Context) {
tenantID := middleware.GetTenantID(c)
uid := middleware.GetUserID(c)
mode := strings.TrimSpace(c.Query("mode"))
if mode == "" {
mode = "slash"
}
prefix := strings.TrimSpace(c.Query("prefix"))
prefix = strings.TrimPrefix(strings.ToLower(prefix), "/")
q := strings.TrimSpace(c.Query("q"))
db := model.DB.Model(&model.QuickReply{}).Where("tenant_id = ?", tenantID).Where(
"(scope = ? AND status = ?) OR (scope = ? AND owner_user_id = ? AND status = ?)",
@@ -227,19 +237,64 @@ func (h *QuickReplyHandler) Suggest(c *gin.Context) {
quickReplyScopePersonal, uid, quickReplyStatusPub,
)
if prefix != "" {
like := prefix + "%"
titleLike := "%" + prefix + "%"
db = db.Where("shortcut LIKE ? OR title LIKE ?", like, titleLike)
switch mode {
case "slash":
if prefix != "" {
// 仅匹配输入码前缀(/n → shortcut 以 n 开头)
db = db.Where("shortcut <> '' AND LOWER(shortcut) LIKE ?", prefix+"%")
}
case "keyword":
if utf8.RuneCountInString(q) < 1 {
middleware.JSON(c, []model.QuickReply{})
return
}
like := "%" + q + "%"
db = db.Where("title LIKE ? OR content LIKE ? OR shortcut LIKE ?", like, like, like)
default:
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "mode 无效"})
return
}
// 多取一些再按个人频次排序截断(避免漏掉个人高频但全局低的条目)
var list []model.QuickReply
// 无前缀时返回高频
order := "usage_count desc, sort_order asc, id desc"
if err := db.Order(order).Limit(20).Find(&list).Error; err != nil {
if err := db.Order("usage_count desc, sort_order asc, id desc").Limit(200).Find(&list).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "搜索失败"})
return
}
if len(list) == 0 {
middleware.JSON(c, []model.QuickReply{})
return
}
ids := make([]uint, len(list))
for i, item := range list {
ids[i] = item.ID
}
var usages []model.QuickReplyUserUsage
_ = model.DB.Where("user_id = ? AND tenant_id = ? AND quick_reply_id IN ?", uid, tenantID, ids).Find(&usages).Error
myMap := map[uint]int{}
for _, u := range usages {
myMap[u.QuickReplyID] = u.UsageCount
}
for i := range list {
list[i].MyUsageCount = myMap[list[i].ID]
}
// 个人调用次数优先
sort.SliceStable(list, func(i, j int) bool {
if list[i].MyUsageCount != list[j].MyUsageCount {
return list[i].MyUsageCount > list[j].MyUsageCount
}
if list[i].UsageCount != list[j].UsageCount {
return list[i].UsageCount > list[j].UsageCount
}
if list[i].SortOrder != list[j].SortOrder {
return list[i].SortOrder < list[j].SortOrder
}
return list[i].ID > list[j].ID
})
if len(list) > suggestLimit {
list = list[:suggestLimit]
}
middleware.JSON(c, list)
}
@@ -428,7 +483,7 @@ func (h *QuickReplyHandler) Unpublish(c *gin.Context) {
middleware.JSON(c, item)
}
// Use POST /api/quick-replies/:id/use 使用计数 +1
// Use POST /api/quick-replies/:id/use 个人调用 +1(排序用),并累计全局 usage_count
func (h *QuickReplyHandler) Use(c *gin.Context) {
id64, _ := strconv.ParseUint(c.Param("id"), 10, 64)
item, ok := loadQuickReply(c, uint(id64))
@@ -437,6 +492,7 @@ func (h *QuickReplyHandler) Use(c *gin.Context) {
}
// 可见性:团队已发布,或本人个人
uid := middleware.GetUserID(c)
tenantID := middleware.GetTenantID(c)
if item.Scope == quickReplyScopeTeam && item.Status != quickReplyStatusPub {
if !middleware.HasAnyRole(c, "admin", "supervisor") {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "该话术尚未发布"})
@@ -447,8 +503,32 @@ func (h *QuickReplyHandler) Use(c *gin.Context) {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权使用"})
return
}
// 全局累计(管理页)
_ = model.DB.Model(item).UpdateColumn("usage_count", item.UsageCount+1).Error
middleware.JSON(c, gin.H{"ok": true, "usage_count": item.UsageCount + 1})
// 个人累计(建议排序)
var usage model.QuickReplyUserUsage
err := model.DB.Where("user_id = ? AND quick_reply_id = ?", uid, item.ID).First(&usage).Error
myCount := 1
if err != nil {
usage = model.QuickReplyUserUsage{
TenantID: tenantID,
UserID: uid,
QuickReplyID: item.ID,
UsageCount: 1,
}
_ = model.DB.Create(&usage).Error
} else {
myCount = usage.UsageCount + 1
_ = model.DB.Model(&usage).UpdateColumn("usage_count", myCount).Error
}
middleware.JSON(c, gin.H{
"ok": true,
"usage_count": item.UsageCount + 1,
"my_usage_count": myCount,
})
}
// 快捷回复 CSV 固定四列(与导入模板一致)