refactor(server): 会话/访客/设置业务逻辑下沉 internal/service 层,handler 瘦身

- handler 只保留请求绑定、权限检查与响应映射;业务校验顺序、状态码与文案逐字保留
- 新增 service.Error 语义错误,由 handler 统一映射 HTTP 响应
- session 875→361 行、widget 745→321 行、settings 540→73 行
- 自动分配/租户设置(工作时间、欢迎语)/消息校验/黑名单检查/联系方式提取随迁 service 包
- 相关单元测试随迁 service 包,go test ./... 全部通过
This commit is contained in:
yml2213
2026-09-05 14:39:34 +08:00
parent ba3b1534f8
commit 8b9f3d710f
24 changed files with 2158 additions and 1963 deletions
+75 -499
View File
@@ -1,17 +1,13 @@
package handler
import (
"errors"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"kefu-cloud/server/internal/middleware"
"kefu-cloud/server/internal/model"
"kefu-cloud/server/internal/service"
"kefu-cloud/server/internal/ws"
)
@@ -65,10 +61,6 @@ type WidgetLeaveReq struct {
VisitorToken string `json:"visitor_token"`
}
var channelKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{3,64}$`)
var phonePattern = regexp.MustCompile(`^1[3-9]\d{9}$`)
var emailPattern = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
func (h *WidgetHandler) Init(c *gin.Context) {
var req WidgetInitReq
if err := c.ShouldBindQuery(&req); err != nil {
@@ -84,185 +76,39 @@ func (h *WidgetHandler) Init(c *gin.Context) {
if req.ChannelKey == "" {
req.ChannelKey = c.Query("channel_key")
}
if !channelKeyPattern.MatchString(req.ChannelKey) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "渠道标识无效"})
return
}
var channel model.Channel
if err := model.DB.
Where("type = ? AND status = ? AND script_code LIKE ?", "web", "enabled", "%data-id=\""+req.ChannelKey+"\"%").
First(&channel).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "渠道不存在或已关闭"})
return
}
name := strings.TrimSpace(req.VisitorName)
if name == "" {
name = "访客"
}
// 创建或查找客户
var customer model.Customer
model.DB.Where("tenant_id = ? AND name = ? AND phone = ''", channel.TenantID, name).First(&customer)
if customer.ID == 0 {
customer = model.Customer{
TenantID: channel.TenantID,
Name: name,
Source: "网页",
Status: "online",
}
model.DB.Create(&customer)
}
visitorToken, visitorTokenHash, err := model.NewVisitorToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "创建访客会话失败"})
return
}
visitorIP, visitorRegion, userAgent, _ := captureVisitorMeta(c)
deviceKey := normalizeDeviceKey(req.DeviceID, userAgent)
// 黑名单拦截(IP / 设备)
if blocked, blKind, blReason := checkVisitorBlacklist(channel.TenantID, visitorIP, deviceKey); blocked {
kindLabel := "IP"
if blKind == "device" {
kindLabel = "设备"
}
msg := "您暂时无法使用在线客服"
if strings.TrimSpace(blReason) != "" {
msg = msg + "(" + kindLabel + "限制)"
}
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": msg})
return
}
pageURL := sanitizePageURL(req.PageURL)
// 宿主页信息:body 优先,query 兜底
pageURL := service.SanitizePageURL(req.PageURL)
if pageURL == "" {
// query 兜底
pageURL = sanitizePageURL(c.Query("page_url"))
pageURL = service.SanitizePageURL(c.Query("page_url"))
}
pageTitle := sanitizePageTitle(req.PageTitle)
pageTitle := service.SanitizePageTitle(req.PageTitle)
if pageTitle == "" {
pageTitle = sanitizePageTitle(c.Query("page_title"))
pageTitle = service.SanitizePageTitle(c.Query("page_title"))
}
referrer := sanitizePageURL(req.Referrer)
referrer := service.SanitizePageURL(req.Referrer)
if referrer == "" {
referrer = sanitizePageURL(c.Query("referrer"))
referrer = service.SanitizePageURL(c.Query("referrer"))
}
now := time.Now()
// 创建会话
session := model.Session{
TenantID: channel.TenantID,
ChannelID: channel.ID,
CustomerID: customer.ID,
VisitorTokenHash: visitorTokenHash,
VisitorIP: visitorIP,
VisitorRegion: visitorRegion,
UserAgent: userAgent,
DeviceKey: deviceKey,
LandingURL: pageURL,
LandingTitle: pageTitle,
Referrer: referrer,
CurrentURL: pageURL,
CurrentTitle: pageTitle,
LastSeenAt: &now,
Status: "waiting",
Priority: "normal",
}
if err := model.DB.Create(&session).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "创建会话失败"})
return
}
// 首条浏览轨迹
if pageURL != "" {
_ = model.DB.Create(&model.VisitorPageView{
SessionID: session.ID,
TenantID: channel.TenantID,
URL: pageURL,
Title: pageTitle,
EnteredAt: now,
}).Error
}
model.DB.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", customer.ID, channel.TenantID).
Updates(map[string]interface{}{"conversation_count": gorm.Expr("conversation_count + 1"), "last_contact_at": time.Now()})
onlineCount, err := countOnlineAgents(channel.TenantID)
ip, region, userAgent, _ := captureVisitorMeta(c)
resp, err := service.WidgetInit(service.WidgetInitInput{
ChannelKey: req.ChannelKey,
VisitorName: req.VisitorName,
DeviceID: req.DeviceID,
PageURL: pageURL,
PageTitle: pageTitle,
Referrer: referrer,
Meta: service.VisitorMeta{
IP: ip,
Region: region,
UserAgent: userAgent,
},
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询在线客服失败"})
respondServiceError(c, err)
return
}
setting, _ := getOrCreateTenantSettings(channel.TenantID)
withinWorkHours := isWithinWorkHours(setting, time.Now())
// 非工作时间不自动分配,走留言/排队提示
canServeNow := onlineCount > 0 && withinWorkHours
var assignedAgent *model.User
if canServeNow {
assignedAgent, err = tryAutoAssign(&session)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "自动分配失败"})
return
}
}
if payload, err := ws.NewEvent("session_created", session.ID, session); err == nil {
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
}
welcomeSegs := resolveWelcomeSegments(setting)
welcome := firstWelcomeText(welcomeSegs)
offlinePrompt := offlineLeavePrompt
agentNickname := defaultAgentNickname
displayName := "在线客服"
worktimePrompt := defaultWorktimePrompt
if setting != nil {
if setting.OfflinePrompt != "" {
offlinePrompt = setting.OfflinePrompt
}
if setting.AgentNickname != "" {
agentNickname = setting.AgentNickname
}
if setting.DisplayName != "" {
displayName = setting.DisplayName
}
if setting.WorktimePrompt != "" {
worktimePrompt = setting.WorktimePrompt
}
}
// 非工作时间优先展示非工作时间提示
if !withinWorkHours {
offlinePrompt = worktimePrompt
}
resp := gin.H{
"session_id": session.ID,
"customer_id": customer.ID,
"channel_id": channel.ID,
"tenant_id": channel.TenantID,
"visitor_token": visitorToken,
"agents_online": canServeNow,
"online_agent_count": onlineCount,
"within_work_hours": withinWorkHours,
"session_status": session.Status,
"welcome_message": welcome,
"welcome_messages": welcomeSegs,
"display_name": displayName,
"agent_nickname": agentNickname,
}
if !canServeNow {
resp["offline_prompt"] = offlinePrompt
}
if assignedAgent != nil {
resp["agent_id"] = assignedAgent.ID
resp["agent_name"] = assignedAgent.Nickname
} else if canServeNow {
resp["agent_name"] = agentNickname
}
resp["landing_url"] = session.LandingURL
resp["current_url"] = session.CurrentURL
c.JSON(http.StatusOK, gin.H{"code": 0, "data": resp})
}
@@ -273,101 +119,17 @@ func (h *WidgetHandler) PageView(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
token := visitorTokenFromRequest(c, req.VisitorToken)
session, ok := loadVisitorSession(c, req.SessionID, token)
if !ok {
session, err := service.WidgetSession(req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if err != nil {
respondServiceError(c, err)
return
}
if session.Status == "ended" || session.Status == "archived" {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "会话已结束"})
data, err := service.WidgetPageView(session, service.SanitizePageURL(req.PageURL), service.SanitizePageTitle(req.PageTitle))
if err != nil {
respondServiceError(c, err)
return
}
pageURL := sanitizePageURL(req.PageURL)
if pageURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "页面地址无效"})
return
}
pageTitle := sanitizePageTitle(req.PageTitle)
now := time.Now()
// 与当前页相同:只刷新活跃时间/标题,不插重复轨迹
if strings.TrimSpace(session.CurrentURL) == pageURL {
updates := map[string]interface{}{
"last_seen_at": now,
}
titleFixed := false
if pageTitle != "" && pageTitle != strings.TrimSpace(session.CurrentTitle) {
updates["current_title"] = pageTitle
// 同步最新一条轨迹的标题(纠正「慢一页」时写入的旧 title)
var last model.VisitorPageView
if err := model.DB.Where("session_id = ? AND url = ?", session.ID, pageURL).
Order("entered_at desc").First(&last).Error; err == nil {
_ = model.DB.Model(&last).Update("title", pageTitle).Error
titleFixed = true
if payload, err := ws.NewEvent("page_view", session.ID, gin.H{
"id": last.ID,
"session_id": last.SessionID,
"url": pageURL,
"title": pageTitle,
"entered_at": last.EnteredAt,
"title_fix": true,
}); err == nil {
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
}
}
}
_ = model.DB.Model(session).Updates(updates).Error
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"deduped": true, "title_updated": titleFixed}})
return
}
pv := model.VisitorPageView{
SessionID: session.ID,
TenantID: session.TenantID,
URL: pageURL,
Title: pageTitle,
EnteredAt: now,
}
if err := model.DB.Create(&pv).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "记录失败"})
return
}
_ = model.DB.Model(session).Updates(map[string]interface{}{
"current_url": pageURL,
"current_title": pageTitle,
"last_seen_at": now,
}).Error
// 控制单会话条数
var count int64
model.DB.Model(&model.VisitorPageView{}).Where("session_id = ?", session.ID).Count(&count)
if count > maxPageViewsPerSession {
var oldest []model.VisitorPageView
model.DB.Where("session_id = ?", session.ID).Order("entered_at asc").
Limit(int(count - maxPageViewsPerSession)).Find(&oldest)
ids := make([]uint, 0, len(oldest))
for _, o := range oldest {
ids = append(ids, o.ID)
}
if len(ids) > 0 {
model.DB.Where("id IN ?", ids).Delete(&model.VisitorPageView{})
}
}
if payload, err := ws.NewEvent("page_view", session.ID, gin.H{
"id": pv.ID,
"session_id": pv.SessionID,
"url": pv.URL,
"title": pv.Title,
"entered_at": pv.EnteredAt,
}); err == nil {
// 排队中 AgentID 为空时,BroadcastToSessionStaff 只会推给 admin/supervisor,
// 一线坐席看不到实时轨迹;轨迹对租户内工作人员统一广播。
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": pv})
c.JSON(http.StatusOK, gin.H{"code": 0, "data": data})
}
// Heartbeat POST /api/widget/heartbeat — 刷新 last_seen,供在线读秒
@@ -377,18 +139,21 @@ func (h *WidgetHandler) Heartbeat(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
token := visitorTokenFromRequest(c, req.VisitorToken)
session, ok := loadVisitorSession(c, req.SessionID, token)
if !ok {
session, err := service.WidgetSession(req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if err != nil {
respondServiceError(c, err)
return
}
if session.Status == "ended" || session.Status == "archived" {
ended, lastSeen, err := service.WidgetHeartbeat(session)
if err != nil {
respondServiceError(c, err)
return
}
if ended {
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"ok": true, "ended": true}})
return
}
now := time.Now()
_ = model.DB.Model(session).Update("last_seen_at", now).Error
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"ok": true, "last_seen_at": now}})
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"ok": true, "last_seen_at": lastSeen}})
}
func visitorTokenFromRequest(c *gin.Context, bodyToken string) string {
@@ -398,19 +163,6 @@ func visitorTokenFromRequest(c *gin.Context, bodyToken string) string {
return bodyToken
}
func loadVisitorSession(c *gin.Context, sessionID uint, token string) (*model.Session, bool) {
var session model.Session
if err := model.DB.First(&session, sessionID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "会话不存在"})
return nil, false
}
if !model.VerifyVisitorToken(&session, token) {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "访客会话凭证无效"})
return nil, false
}
return &session, true
}
func (h *WidgetHandler) SendMessage(c *gin.Context) {
var req WidgetMessageReq
if err := c.ShouldBindJSON(&req); err != nil {
@@ -420,70 +172,22 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
if req.Type == "" {
req.Type = "text"
}
content, err := validateMessageContent(req.Type, req.Content)
// 先校验内容,再加载会话(与原校验顺序一致)
content, err := service.ValidateMessageContent(req.Type, req.Content)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
session, ok := loadVisitorSession(c, req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if !ok {
session, err := service.WidgetSession(req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if err != nil {
respondServiceError(c, err)
return
}
if session.Status == "ended" || session.Status == "archived" {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "会话已结束,请重新发起咨询"})
msg, err := service.WidgetSendMessage(session, req.Type, content)
if err != nil {
respondServiceError(c, err)
return
}
custID := session.CustomerID
msg := model.Message{
SessionID: session.ID,
SenderType: "visitor",
SenderID: &custID,
Content: content,
Type: req.Type,
SentAt: time.Now(),
}
if err := model.CreateMessage(&msg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "发送失败"})
return
}
// 发送后清空草稿
_ = model.DB.Model(session).Updates(map[string]interface{}{
"draft_text": "",
"draft_updated_at": time.Now(),
}).Error
model.DB.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", session.CustomerID, session.TenantID).Update("last_contact_at", time.Now())
// 从已发送消息再提取一次联系方式
if req.Type == "text" {
go onVisitorDraftContacts(session.TenantID, session.CustomerID, session.ID, content)
}
// 仍在排队时尝试自动分配(客服刚上线的场景)
if session.Status == "waiting" && session.AgentID == nil {
if _, err := tryAutoAssign(session); err == nil {
_ = model.DB.First(session, session.ID)
}
}
if payload, err := ws.NewEvent("message", session.ID, msg); err == nil {
if session.AgentID == nil {
// 等待会话的消息要通知全部客服,便于任一在线客服及时领取。
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
} else {
ws.DefaultHub.BroadcastToSession(session.TenantID, session.ID, session.AgentID, payload)
}
}
// 通知坐席清空草稿预览
if clearPayload, err := ws.NewEvent("input_draft", session.ID, map[string]interface{}{
"from": "visitor",
"text": "",
}); err == nil {
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, clearPayload)
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": msg})
}
@@ -494,7 +198,7 @@ func (h *WidgetHandler) LeaveMessage(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
return
}
content, err := validateMessageContent("text", req.Content)
content, err := service.ValidateMessageContent("text", req.Content)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
@@ -502,104 +206,27 @@ func (h *WidgetHandler) LeaveMessage(c *gin.Context) {
name := strings.TrimSpace(req.Name)
phone := strings.TrimSpace(req.Phone)
email := strings.TrimSpace(req.Email)
if phone == "" && email == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请至少填写手机号或邮箱,便于我们回复"})
return
}
if phone != "" && !phonePattern.MatchString(phone) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "手机号格式不正确"})
return
}
if email != "" && !emailPattern.MatchString(email) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "邮箱格式不正确"})
return
}
if name != "" && (len([]rune(name)) < 2 || len([]rune(name)) > 50) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "姓名需为 2 至 50 个字符"})
if err := service.ValidateLeaveContact(name, phone, email); err != nil {
respondServiceError(c, err)
return
}
session, ok := loadVisitorSession(c, req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if !ok {
session, err := service.WidgetSession(req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if err != nil {
respondServiceError(c, err)
return
}
if session.Status == "ended" || session.Status == "archived" {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "会话已结束,请重新发起咨询"})
result, err := service.WidgetLeaveMessage(session, content, name, phone, email)
if err != nil {
respondServiceError(c, err)
return
}
// 若此刻已有客服在线,直接走自动分配,留言仍作为普通消息入库
onlineCount, _ := countOnlineAgents(session.TenantID)
if onlineCount > 0 && session.Status == "waiting" {
if _, err := tryAutoAssign(session); err == nil {
_ = model.DB.First(session, session.ID)
}
}
updates := map[string]interface{}{"last_contact_at": time.Now()}
if name != "" {
updates["name"] = name
}
if phone != "" {
updates["phone"] = phone
}
if email != "" {
updates["email"] = email
}
if err := model.DB.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", session.CustomerID, session.TenantID).
Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "更新联系方式失败"})
return
}
custID := session.CustomerID
msg := model.Message{
SessionID: session.ID,
SenderType: "visitor",
SenderID: &custID,
Content: content,
Type: "text",
SentAt: time.Now(),
}
if err := model.CreateMessage(&msg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "留言失败"})
return
}
contactBits := make([]string, 0, 3)
if name != "" {
contactBits = append(contactBits, "姓名:"+name)
}
if phone != "" {
contactBits = append(contactBits, "手机:"+phone)
}
if email != "" {
contactBits = append(contactBits, "邮箱:"+email)
}
model.DB.Create(&model.SessionEvent{
SessionID: session.ID,
OperatorID: 0,
Action: "offline_leave",
Detail: "离线留言 · " + strings.Join(contactBits, " · "),
})
if payload, err := ws.NewEvent("message", session.ID, msg); err == nil {
if session.AgentID == nil {
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
} else {
ws.DefaultHub.BroadcastToSession(session.TenantID, session.ID, session.AgentID, payload)
}
}
if payload, err := ws.NewEvent("session_updated", session.ID, session); err == nil {
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"message": msg,
"session_status": session.Status,
"agents_online": onlineCount > 0,
"message": result.Message,
"session_status": result.SessionStatus,
"agents_online": result.AgentsOnline,
},
})
}
@@ -618,8 +245,9 @@ func (h *WidgetHandler) Connect(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "会话参数错误"})
return
}
session, ok := loadVisitorSession(c, uint(sessionID), visitorTokenFromWebSocket(c))
if !ok {
session, err := service.WidgetSession(uint(sessionID), visitorTokenFromWebSocket(c))
if err != nil {
respondServiceError(c, err)
return
}
client, err := ws.UpgradeVisitor(c.Writer, c.Request, session.TenantID, session.ID)
@@ -640,42 +268,15 @@ func (h *WidgetHandler) SubmitRating(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "评分应为 1 至 5 星"})
return
}
session, ok := loadVisitorSession(c, req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if !ok {
session, err := service.WidgetSession(req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
if err != nil {
respondServiceError(c, err)
return
}
if session.Status != "ended" {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "会话结束后才能评价"})
if err := service.WidgetSubmitRating(session, req.Score, req.Text); err != nil {
respondServiceError(c, err)
return
}
if session.SatisfactionScore != nil {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "该会话已评价"})
return
}
if err := model.DB.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Session{}).Where("id = ? AND satisfaction_score IS NULL", session.ID).
Updates(map[string]interface{}{"satisfaction_score": req.Score, "satisfaction_text": req.Text})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return tx.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", session.CustomerID, session.TenantID).
Updates(map[string]interface{}{
"satisfaction_sum": gorm.Expr("satisfaction_sum + ?", req.Score),
"satisfaction_count": gorm.Expr("satisfaction_count + 1"),
}).Error
}); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": "该会话已评价"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "提交评价失败"})
return
}
middleware.JSON(c, gin.H{"message": "感谢您的评价"})
}
@@ -685,8 +286,9 @@ func (h *WidgetHandler) GetMessages(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "会话参数错误"})
return
}
session, ok := loadVisitorSession(c, uint(sessionID), visitorTokenFromRequest(c, ""))
if !ok {
session, err := service.WidgetSession(uint(sessionID), visitorTokenFromRequest(c, ""))
if err != nil {
respondServiceError(c, err)
return
}
@@ -710,36 +312,10 @@ func (h *WidgetHandler) GetMessages(c *gin.Context) {
}
}
query := model.DB.Where("session_id = ?", sessionID)
if afterSeq > 0 {
query = query.Where("seq > ?", afterSeq)
}
var messages []model.Message
if err := query.Order("seq asc").Limit(limit).Find(&messages).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询消息失败"})
data, err := service.WidgetMessages(session, afterSeq, limit)
if err != nil {
respondServiceError(c, err)
return
}
var maxSeq int
if err := model.DB.Model(&model.Message{}).
Where("session_id = ?", sessionID).
Select("COALESCE(MAX(seq), 0)").
Scan(&maxSeq).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询消息失败"})
return
}
hasMore := len(messages) >= limit && (len(messages) == 0 || messages[len(messages)-1].Seq < maxSeq)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"messages": messages,
"after_seq": afterSeq,
"max_seq": maxSeq,
"has_more": hasMore,
"session_status": session.Status,
"satisfaction_score": session.SatisfactionScore,
},
})
c.JSON(http.StatusOK, gin.H{"code": 0, "data": data})
}