欢迎语支持多段配置与图片
租户可配置有序文本/图片欢迎语;设置页支持增删排序与上传,Widget 空会话按序展示;兼容旧 welcome_message 字段。
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"kefu-cloud/server/internal/middleware"
|
||||
@@ -22,10 +23,99 @@ const (
|
||||
defaultWorktimePrompt = "当前为非工作时间,我们会在工作时段尽快回复您。"
|
||||
defaultAgentNickname = "在线客服"
|
||||
defaultTimezone = "Asia/Shanghai"
|
||||
|
||||
maxWelcomeSegments = 10
|
||||
maxWelcomeTextLen = 500
|
||||
)
|
||||
|
||||
// WelcomeSegment 单段欢迎语:文本或图片(content 为正文或对象存储 URL)。
|
||||
type WelcomeSegment struct {
|
||||
Type string `json:"type"` // text | image
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
var weekdayKeys = []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}
|
||||
|
||||
func defaultWelcomeSegments() []WelcomeSegment {
|
||||
return []WelcomeSegment{{Type: "text", Content: defaultWelcomeMessage}}
|
||||
}
|
||||
|
||||
func defaultWelcomeMessagesJSON() string {
|
||||
b, _ := json.Marshal(defaultWelcomeSegments())
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func firstWelcomeText(segs []WelcomeSegment) string {
|
||||
for _, s := range segs {
|
||||
if s.Type == "text" && strings.TrimSpace(s.Content) != "" {
|
||||
return s.Content
|
||||
}
|
||||
}
|
||||
return defaultWelcomeMessage
|
||||
}
|
||||
|
||||
// resolveWelcomeSegments 解析多段欢迎语;兼容仅有旧 welcome_message 的数据。
|
||||
func resolveWelcomeSegments(setting *model.TenantSetting) []WelcomeSegment {
|
||||
if setting == nil {
|
||||
return defaultWelcomeSegments()
|
||||
}
|
||||
raw := strings.TrimSpace(setting.WelcomeMessagesJSON)
|
||||
if raw != "" {
|
||||
var segs []WelcomeSegment
|
||||
if err := json.Unmarshal([]byte(raw), &segs); err == nil {
|
||||
out := make([]WelcomeSegment, 0, len(segs))
|
||||
for _, s := range segs {
|
||||
t := strings.TrimSpace(s.Type)
|
||||
c := strings.TrimSpace(s.Content)
|
||||
if (t == "text" || t == "image") && c != "" {
|
||||
out = append(out, WelcomeSegment{Type: t, Content: c})
|
||||
}
|
||||
}
|
||||
if len(out) > 0 {
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg := strings.TrimSpace(setting.WelcomeMessage); msg != "" {
|
||||
return []WelcomeSegment{{Type: "text", Content: msg}}
|
||||
}
|
||||
return defaultWelcomeSegments()
|
||||
}
|
||||
|
||||
// normalizeWelcomeSegments 校验并规范化欢迎语段落;返回规范化列表与兼容用首段文本。
|
||||
func normalizeWelcomeSegments(input []WelcomeSegment) ([]WelcomeSegment, string, error) {
|
||||
if len(input) == 0 {
|
||||
return nil, "", fmt.Errorf("至少保留一段欢迎语")
|
||||
}
|
||||
if len(input) > maxWelcomeSegments {
|
||||
return nil, "", fmt.Errorf("欢迎语最多 %d 段", maxWelcomeSegments)
|
||||
}
|
||||
out := make([]WelcomeSegment, 0, len(input))
|
||||
for i, seg := range input {
|
||||
t := strings.ToLower(strings.TrimSpace(seg.Type))
|
||||
c := strings.TrimSpace(seg.Content)
|
||||
switch t {
|
||||
case "text":
|
||||
if c == "" {
|
||||
return nil, "", fmt.Errorf("第 %d 段文本不能为空", i+1)
|
||||
}
|
||||
if utf8.RuneCountInString(c) > maxWelcomeTextLen {
|
||||
return nil, "", fmt.Errorf("第 %d 段文本不能超过 %d 字", i+1, maxWelcomeTextLen)
|
||||
}
|
||||
out = append(out, WelcomeSegment{Type: "text", Content: c})
|
||||
case "image":
|
||||
validated, err := validateMessageContent("image", c)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("第 %d 段图片:%s", i+1, err.Error())
|
||||
}
|
||||
out = append(out, WelcomeSegment{Type: "image", Content: validated})
|
||||
default:
|
||||
return nil, "", fmt.Errorf("第 %d 段类型无效,请使用 text 或 image", i+1)
|
||||
}
|
||||
}
|
||||
return out, firstWelcomeText(out), nil
|
||||
}
|
||||
|
||||
func defaultWorkHoursJSON() string {
|
||||
// 默认全周全天可接待,租户可在设置中收紧为工作日时段
|
||||
hours := map[string]string{
|
||||
@@ -50,19 +140,20 @@ func getOrCreateTenantSettings(tenantID uint) (*model.TenantSetting, error) {
|
||||
displayName = "在线客服"
|
||||
}
|
||||
setting = model.TenantSetting{
|
||||
TenantID: tenantID,
|
||||
DisplayName: displayName,
|
||||
AgentNickname: defaultAgentNickname,
|
||||
Timezone: defaultTimezone,
|
||||
WelcomeMessage: defaultWelcomeMessage,
|
||||
OfflinePrompt: defaultOfflinePrompt,
|
||||
WorkHoursJSON: defaultWorkHoursJSON(),
|
||||
WorktimePrompt: defaultWorktimePrompt,
|
||||
NotifyNewSession: true,
|
||||
NotifyOfflineLeave: true,
|
||||
NotifyDailyReport: false,
|
||||
AssignStrategy: assignStrategyLeastLoad,
|
||||
MaxActivePerAgent: 0,
|
||||
TenantID: tenantID,
|
||||
DisplayName: displayName,
|
||||
AgentNickname: defaultAgentNickname,
|
||||
Timezone: defaultTimezone,
|
||||
WelcomeMessage: defaultWelcomeMessage,
|
||||
WelcomeMessagesJSON: defaultWelcomeMessagesJSON(),
|
||||
OfflinePrompt: defaultOfflinePrompt,
|
||||
WorkHoursJSON: defaultWorkHoursJSON(),
|
||||
WorktimePrompt: defaultWorktimePrompt,
|
||||
NotifyNewSession: true,
|
||||
NotifyOfflineLeave: true,
|
||||
NotifyDailyReport: false,
|
||||
AssignStrategy: assignStrategyLeastLoad,
|
||||
MaxActivePerAgent: 0,
|
||||
}
|
||||
if err := model.DB.Create(&setting).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -173,12 +264,14 @@ func settingsResponse(setting *model.TenantSetting) gin.H {
|
||||
if maxActive < 0 {
|
||||
maxActive = 0
|
||||
}
|
||||
welcomeSegs := resolveWelcomeSegments(setting)
|
||||
return gin.H{
|
||||
"tenant_id": setting.TenantID,
|
||||
"display_name": setting.DisplayName,
|
||||
"agent_nickname": setting.AgentNickname,
|
||||
"timezone": setting.Timezone,
|
||||
"welcome_message": setting.WelcomeMessage,
|
||||
"welcome_message": firstWelcomeText(welcomeSegs),
|
||||
"welcome_messages": welcomeSegs,
|
||||
"offline_prompt": setting.OfflinePrompt,
|
||||
"work_hours": parseWorkHours(setting.WorkHoursJSON),
|
||||
"worktime_prompt": setting.WorktimePrompt,
|
||||
@@ -209,6 +302,7 @@ type updateSettingsReq struct {
|
||||
AgentNickname *string `json:"agent_nickname"`
|
||||
Timezone *string `json:"timezone"`
|
||||
WelcomeMessage *string `json:"welcome_message"`
|
||||
WelcomeMessages *[]WelcomeSegment `json:"welcome_messages"`
|
||||
OfflinePrompt *string `json:"offline_prompt"`
|
||||
WorkHours map[string]string `json:"work_hours"`
|
||||
WorktimePrompt *string `json:"worktime_prompt"`
|
||||
@@ -275,13 +369,35 @@ func (h *SettingsHandler) Update(c *gin.Context) {
|
||||
}
|
||||
updates["timezone"] = tz
|
||||
}
|
||||
if req.WelcomeMessage != nil {
|
||||
// 优先多段欢迎语;若仅传旧 welcome_message 则同步为单段文本
|
||||
if req.WelcomeMessages != nil {
|
||||
segs, firstText, err := normalizeWelcomeSegments(*req.WelcomeMessages)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
b, err := json.Marshal(segs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "欢迎语序列化失败"})
|
||||
return
|
||||
}
|
||||
updates["welcome_messages_json"] = string(b)
|
||||
updates["welcome_message"] = firstText
|
||||
} else if req.WelcomeMessage != nil {
|
||||
msg := strings.TrimSpace(*req.WelcomeMessage)
|
||||
if len([]rune(msg)) > 500 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "欢迎语不能超过 500 字"})
|
||||
return
|
||||
}
|
||||
updates["welcome_message"] = msg
|
||||
// 同步为单段,避免旧字段与 JSON 不一致
|
||||
if msg == "" {
|
||||
updates["welcome_messages_json"] = defaultWelcomeMessagesJSON()
|
||||
updates["welcome_message"] = defaultWelcomeMessage
|
||||
} else {
|
||||
b, _ := json.Marshal([]WelcomeSegment{{Type: "text", Content: msg}})
|
||||
updates["welcome_messages_json"] = string(b)
|
||||
}
|
||||
}
|
||||
if req.OfflinePrompt != nil {
|
||||
msg := strings.TrimSpace(*req.OfflinePrompt)
|
||||
|
||||
Reference in New Issue
Block a user