fix(widget): 完善自动回复与新会话进线提醒
CI / Backend (go vet + test) (push) Failing after 40m21s
CI / Frontend (typecheck + lint + build) (push) Successful in 1h21m3s

This commit is contained in:
yml2213
2026-11-03 19:29:32 +08:00
parent 0eb16c1ce2
commit 24fac1bcf1
15 changed files with 275 additions and 115 deletions
+12 -8
View File
@@ -56,12 +56,12 @@ func SetupRoutes(r *gin.Engine, store storage.ObjectStorage, storageCfg config.S
authRequired := api.Group("")
authRequired.Use(middleware.AuthRequired())
{
// 当前用户
authRequired.GET("/me", auth.Me)
authRequired.GET("/me/permissions", auth.MePermissions)
authRequired.PUT("/me/status", auth.UpdatePresence)
// 令牌续期(前端定时调用,防止长挂机后过期掉线)
authRequired.POST("/auth/refresh", auth.Refresh)
// 当前用户
authRequired.GET("/me", auth.Me)
authRequired.GET("/me/permissions", auth.MePermissions)
authRequired.PUT("/me/status", auth.UpdatePresence)
// 令牌续期(前端定时调用,防止长挂机后过期掉线)
authRequired.POST("/auth/refresh", auth.Refresh)
// WebSocket
authRequired.GET("/ws", wsHandler.Connect)
@@ -143,9 +143,13 @@ func SetupRoutes(r *gin.Engine, store storage.ObjectStorage, storageCfg config.S
// 租户系统设置
settingsGroup := authRequired.Group("/settings")
settingsGroup.GET("", middleware.RequirePermission(
"settings.basic", "settings.assign_rule", "settings.auto_reply", "settings.worktime", "settings.notification",
"settings.basic", "settings.channel", "settings.staff", "settings.assign_rule",
"settings.customer_tag", "settings.auto_reply", "settings.worktime", "settings.notification",
), settings.Get)
settingsGroup.PUT("", middleware.RequirePermission("settings.basic"), settings.Update)
settingsGroup.PUT("", middleware.RequirePermission(
"settings.basic", "settings.channel", "settings.staff", "settings.assign_rule",
"settings.customer_tag", "settings.auto_reply", "settings.worktime", "settings.notification",
), settings.Update)
// 坐席账号(租户内)
staffGroup := authRequired.Group("/staff")
@@ -810,6 +810,33 @@ func TestSettingsAndExportsHonorFineGrainedPermissions(t *testing.T) {
t.Fatalf("缺少分配规则权限仍可修改设置: %d %s", settings.Code, settings.Body.String())
}
autoReplyRoleRec := httptest.NewRecorder()
autoReplyRoleBody := []byte(`{"name":"自动回复主管","permissions":["settings.auto_reply"],"data_scopes":{}}`)
router.ServeHTTP(autoReplyRoleRec, bearerRequest(t, http.MethodPost, "/api/roles", autoReplyRoleBody, admin))
if autoReplyRoleRec.Code != http.StatusOK {
t.Fatalf("创建自动回复角色失败: %d %s", autoReplyRoleRec.Code, autoReplyRoleRec.Body.String())
}
var autoReplyRoleResp struct {
Data model.Role `json:"data"`
}
if err := json.Unmarshal(autoReplyRoleRec.Body.Bytes(), &autoReplyRoleResp); err != nil {
t.Fatalf("解析自动回复角色失败: %v", err)
}
autoReplyUser := createUser(t, tenant.ID, "auto-reply-supervisor", autoReplyRoleResp.Data.Code)
autoReplySave := httptest.NewRecorder()
autoReplyBody := []byte(`{"welcome_messages":[{"type":"text","content":"主管配置的欢迎语"}],"offline_prompt":"主管配置的离线提示"}`)
router.ServeHTTP(autoReplySave, bearerRequest(t, http.MethodPut, "/api/settings", autoReplyBody, autoReplyUser))
if autoReplySave.Code != http.StatusOK {
t.Fatalf("自动回复权限应允许保存: %d %s", autoReplySave.Code, autoReplySave.Body.String())
}
basicSettingsSave := httptest.NewRecorder()
router.ServeHTTP(basicSettingsSave, bearerRequest(t, http.MethodPut, "/api/settings", []byte(`{"display_name":"越权修改"}`), autoReplyUser))
if basicSettingsSave.Code != http.StatusForbidden {
t.Fatalf("自动回复权限不应允许修改基本设置: %d %s", basicSettingsSave.Code, basicSettingsSave.Body.String())
}
customerOne := model.Customer{TenantID: tenant.ID, Name: "客户一"}
customerTwo := model.Customer{TenantID: tenant.ID, Name: "客户二"}
if err := model.DB.Create(&customerOne).Error; err != nil {
@@ -1037,6 +1064,8 @@ func TestTenantSettingsPersistAndAffectWidgetPrompt(t *testing.T) {
}
var initResp struct {
Data struct {
SessionID uint `json:"session_id"`
VisitorToken string `json:"visitor_token"`
WelcomeMessage string `json:"welcome_message"`
WelcomeMessages []struct {
Type string `json:"type"`
@@ -1058,4 +1087,40 @@ func TestTenantSettingsPersistAndAffectWidgetPrompt(t *testing.T) {
if len(initResp.Data.WelcomeMessages) != 2 {
t.Fatalf("widget 未返回多段欢迎语: %+v", initResp.Data.WelcomeMessages)
}
// 已存在的会话重新拉消息时,应同步最新的自动回复配置。
updateRec := httptest.NewRecorder()
updateBody := `{"welcome_messages":[{"type":"text","content":"刷新后的欢迎语"}],"offline_prompt":"刷新后的离线提示"}`
router.ServeHTTP(updateRec, bearerRequest(t, http.MethodPut, "/api/settings", []byte(updateBody), admin))
if updateRec.Code != http.StatusOK {
t.Fatalf("更新自动回复失败: %s", updateRec.Body.String())
}
messageReq := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/widget/messages?session_id=%d", initResp.Data.SessionID), nil)
messageReq.Header.Set("X-Visitor-Token", initResp.Data.VisitorToken)
messageRec := httptest.NewRecorder()
router.ServeHTTP(messageRec, messageReq)
if messageRec.Code != http.StatusOK {
t.Fatalf("读取已有会话消息失败: %s", messageRec.Body.String())
}
var messageResp struct {
Data struct {
WelcomeMessage string `json:"welcome_message"`
WelcomeMessages []struct {
Type string `json:"type"`
Content string `json:"content"`
} `json:"welcome_messages"`
OfflinePrompt string `json:"offline_prompt"`
AgentsOnline bool `json:"agents_online"`
} `json:"data"`
}
if err := json.Unmarshal(messageRec.Body.Bytes(), &messageResp); err != nil {
t.Fatalf("解析已有会话消息响应失败: %v", err)
}
if messageResp.Data.WelcomeMessage != "刷新后的欢迎语" ||
len(messageResp.Data.WelcomeMessages) != 1 ||
messageResp.Data.OfflinePrompt != "刷新后的离线提示" ||
messageResp.Data.AgentsOnline {
t.Fatalf("已有会话未同步最新自动回复: %+v", messageResp.Data)
}
}
+4 -4
View File
@@ -27,10 +27,6 @@ func (h *SettingsHandler) Get(c *gin.Context) {
}
func (h *SettingsHandler) Update(c *gin.Context) {
if !middleware.HasPermission(c, "settings.basic") {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅租户管理员可修改设置"})
return
}
tenantID := middleware.GetTenantID(c)
if tenantID == 0 {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅租户账号可修改设置"})
@@ -47,6 +43,10 @@ func (h *SettingsHandler) Update(c *gin.Context) {
return
}
// 设置权限按字段分区校验,避免仅拥有基本设置权限即可修改分配、工作时间或通知策略。
if req.TouchesBasic() && !middleware.HasPermission(c, "settings.basic") {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权修改基本设置"})
return
}
if req.TouchesAssignRule() && !middleware.HasPermission(c, "settings.assign_rule") {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权修改客服分配规则"})
return