package chat import ( "encoding/json" "errors" "fmt" "strconv" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/chathub" "golang.org/x/crypto/bcrypt" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB hub *chathub.Hub } const ( defaultSupportUsername = "admin" defaultSupportPassword = "admin123456" defaultSupportNickname = "超级管理员" ) func NewRepository(db *gorm.DB, hub *chathub.Hub) *Repository { return &Repository{db: db, hub: hub} } func EnsureOrderConversation(tx *gorm.DB, order model.RentalOrder) (*model.ChatConversation, error) { var existing model.ChatConversation err := tx.Where("order_id = ?", order.ID).First(&existing).Error if err == nil { return &existing, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } now := time.Now() conversation := model.ChatConversation{ OrderID: order.ID, Type: "order_group", Title: orderConversationTitle(order), Status: "active", } if err := tx.Create(&conversation).Error; err != nil { return nil, err } participants := []model.ChatParticipant{ { ConversationID: conversation.ID, ParticipantType: "user", ParticipantID: order.RenterID, Role: "renter", JoinedAt: now, }, { ConversationID: conversation.ID, ParticipantType: "user", ParticipantID: order.OwnerID, Role: "owner", JoinedAt: now, }, } if supportID := defaultSupportAdminID(tx); supportID > 0 { participants = append(participants, model.ChatParticipant{ ConversationID: conversation.ID, ParticipantType: "admin", ParticipantID: supportID, Role: "support", JoinedAt: now, }) } for _, participant := range participants { if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&participant).Error; err != nil { return nil, err } } // 获取自动话术 autoMessage := "订单已支付,群聊已创建。租客、号主和客服可在这里沟通交接与结账问题。" var cfg model.SystemConfig if err := tx.Where("`key` = ?", "chat.auto_welcome_message").First(&cfg).Error; err == nil && cfg.Value != "" { autoMessage = cfg.Value } message := model.ChatMessage{ ConversationID: conversation.ID, SenderType: "system", SenderRole: "system", ContentType: "system", Content: autoMessage, AttachmentURLS: emptyJSONList(), } if err := tx.Create(&message).Error; err != nil { return nil, err } conversation.LastMessageID = &message.ID conversation.LastMessagePreview = truncatePreview(message.Content) conversation.LastMessageAt = &message.CreatedAt if err := tx.Save(&conversation).Error; err != nil { return nil, err } return &conversation, nil } // NotifyNewConversation 推送群聊创建事件给会话参与者。应在事务提交后调用。 func (r *Repository) NotifyNewConversation(conversationID uint64) { if r.hub == nil { return } r.hub.NotifyConversation(conversationID, &chathub.ChatEvent{ Type: "conversation_updated", ConversationID: conversationID, }) } func (r *Repository) ListConversations(principal Principal, page, pageSize int) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) var total int64 countDB := r.db.Table("chat_conversations AS c"). Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id"). Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) if err := countDB.Count(&total).Error; err != nil { return nil, err } var rows []conversationRow offset := (page - 1) * pageSize err := r.conversationQuery(principal). Order("COALESCE(c.last_message_at, c.created_at) DESC, c.id DESC"). Offset(offset). Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } items := make([]ConversationDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO(nil)) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) FindConversation(principal Principal, id uint64) (*ConversationDTO, error) { var row conversationRow err := r.conversationQuery(principal).Where("c.id = ?", id).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrConversationNotFound } return nil, err } participants, err := r.participants(row.ID) if err != nil { return nil, err } dto := row.toDTO(participants) return &dto, nil } func (r *Repository) FindOrderConversation(userID uint64, orderID uint64) (*ConversationDTO, error) { var row conversationRow principal := Principal{Type: "user", ID: userID} err := r.conversationQuery(principal).Where("c.order_id = ?", orderID).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrConversationNotFound } return nil, err } participants, err := r.participants(row.ID) if err != nil { return nil, err } dto := row.toDTO(participants) return &dto, nil } func (r *Repository) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil { return nil, err } var total int64 if err := r.db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var rows []model.ChatMessage if err := r.db.Where("conversation_id = ?", conversationID). Order("id ASC"). Offset(offset). Limit(pageSize). Find(&rows).Error; err != nil { return nil, err } items, err := r.toMessageDTOs(principal, rows) if err != nil { return nil, err } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) SendMessage(principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) { var messageID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { participant, err := r.findParticipant(tx, principal, conversationID, true) if err != nil { return err } var conversation model.ChatConversation if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil { return err } if conversation.Status != "active" { return ErrPermissionDenied } message := model.ChatMessage{ ConversationID: conversation.ID, SenderType: principal.Type, SenderID: principal.ID, SenderRole: participant.Role, ContentType: "text", Content: req.Content, AttachmentURLS: emptyJSONList(), } if err := tx.Create(&message).Error; err != nil { return err } conversation.LastMessageID = &message.ID conversation.LastMessagePreview = truncatePreview(message.Content) conversation.LastMessageAt = &message.CreatedAt if err := tx.Save(&conversation).Error; err != nil { return err } now := time.Now() if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil { return err } messageID = message.ID return nil }) if err != nil { return nil, err } var message model.ChatMessage if err := r.db.First(&message, messageID).Error; err != nil { return nil, err } items, err := r.toMessageDTOs(principal, []model.ChatMessage{message}) if err != nil { return nil, err } if len(items) == 0 { return nil, ErrConversationNotFound } // 推送新消息事件给会话中的在线参与者 if r.hub != nil { msg := items[0] r.hub.NotifyConversation(conversationID, &chathub.ChatEvent{ Type: "new_message", ConversationID: conversationID, Message: &chathub.MessageData{ ID: msg.ID, ConversationID: msg.ConversationID, SenderType: msg.SenderType, SenderID: msg.SenderID, SenderRole: msg.SenderRole, SenderName: msg.SenderName, ContentType: msg.ContentType, Content: msg.Content, AttachmentURLS: msg.AttachmentURLS, CreatedAt: msg.CreatedAt.Format(time.RFC3339), }, }) } return &items[0], nil } func (r *Repository) MarkRead(principal Principal, conversationID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { participant, err := r.findParticipant(tx, principal, conversationID, true) if err != nil { return err } now := time.Now() return tx.Model(participant).Update("last_read_at", now).Error }) } func (r *Repository) conversationQuery(principal Principal) *gorm.DB { return r.db.Table("chat_conversations AS c"). Select(`c.id, c.order_id, c.type, c.title, c.status, c.last_message_id, c.last_message_preview, c.last_message_at, c.created_at, c.updated_at, cp.role, ( SELECT COUNT(1) FROM chat_messages AS cm WHERE cm.conversation_id = c.id AND NOT (cm.sender_type = ? AND cm.sender_id = ?) AND (cp.last_read_at IS NULL OR cm.created_at > cp.last_read_at) ) AS unread_count`, principal.Type, principal.ID). Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id"). Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } func (r *Repository) findParticipant(tx *gorm.DB, principal Principal, conversationID uint64, lock bool) (*model.ChatParticipant, error) { var participant model.ChatParticipant db := tx if lock { db = db.Clauses(clause.Locking{Strength: "UPDATE"}) } err := db.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", conversationID, principal.Type, principal.ID). First(&participant).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrPermissionDenied } return nil, err } return &participant, nil } func (r *Repository) participants(conversationID uint64) ([]ParticipantDTO, error) { var rows []model.ChatParticipant if err := r.db.Where("conversation_id = ?", conversationID).Order("id ASC").Find(&rows).Error; err != nil { return nil, err } userNames, userAvatars, adminNames, err := r.participantNames(rows) if err != nil { return nil, err } items := make([]ParticipantDTO, 0, len(rows)) for _, row := range rows { name := "系统" avatar := "" if row.ParticipantType == "user" { name = userNames[row.ParticipantID] avatar = userAvatars[row.ParticipantID] } if row.ParticipantType == "admin" { name = adminNames[row.ParticipantID] } items = append(items, ParticipantDTO{ ID: row.ID, ConversationID: row.ConversationID, ParticipantType: row.ParticipantType, ParticipantID: row.ParticipantID, Role: row.Role, DisplayName: fallbackName(row.ParticipantType, row.ParticipantID, name), AvatarURL: avatar, LastReadAt: row.LastReadAt, JoinedAt: row.JoinedAt, }) } return items, nil } func (r *Repository) toMessageDTOs(principal Principal, rows []model.ChatMessage) ([]MessageDTO, error) { userIDs := make([]uint64, 0) adminIDs := make([]uint64, 0) for _, row := range rows { if row.SenderType == "user" && row.SenderID > 0 { userIDs = append(userIDs, row.SenderID) } if row.SenderType == "admin" && row.SenderID > 0 { adminIDs = append(adminIDs, row.SenderID) } } userNames, userAvatars, err := r.userNames(userIDs) if err != nil { return nil, err } adminNames, err := r.adminNames(adminIDs) if err != nil { return nil, err } items := make([]MessageDTO, 0, len(rows)) for _, row := range rows { name := "系统" avatar := "" if row.SenderType == "user" { name = userNames[row.SenderID] avatar = userAvatars[row.SenderID] } if row.SenderType == "admin" { name = adminNames[row.SenderID] } items = append(items, MessageDTO{ ID: row.ID, ConversationID: row.ConversationID, SenderType: row.SenderType, SenderID: row.SenderID, SenderRole: row.SenderRole, SenderName: fallbackName(row.SenderType, row.SenderID, name), SenderAvatar: avatar, IsSelf: row.SenderType == principal.Type && row.SenderID == principal.ID, ContentType: row.ContentType, Content: row.Content, AttachmentURLS: decodeStringList(row.AttachmentURLS), CreatedAt: row.CreatedAt, }) } return items, nil } func (r *Repository) participantNames(rows []model.ChatParticipant) (map[uint64]string, map[uint64]string, map[uint64]string, error) { userIDs := make([]uint64, 0) adminIDs := make([]uint64, 0) for _, row := range rows { if row.ParticipantType == "user" { userIDs = append(userIDs, row.ParticipantID) } if row.ParticipantType == "admin" { adminIDs = append(adminIDs, row.ParticipantID) } } userNames, userAvatars, err := r.userNames(userIDs) if err != nil { return nil, nil, nil, err } adminNames, err := r.adminNames(adminIDs) if err != nil { return nil, nil, nil, err } return userNames, userAvatars, adminNames, nil } func (r *Repository) userNames(ids []uint64) (map[uint64]string, map[uint64]string, error) { names := map[uint64]string{} avatars := map[uint64]string{} if len(ids) == 0 { return names, avatars, nil } var users []model.User if err := r.db.Where("id IN ?", uniqueIDs(ids)).Find(&users).Error; err != nil { return nil, nil, err } for _, user := range users { name := user.Nickname if name == "" { name = user.Phone } names[user.ID] = name avatars[user.ID] = user.AvatarURL } return names, avatars, nil } func (r *Repository) adminNames(ids []uint64) (map[uint64]string, error) { names := map[uint64]string{} if len(ids) == 0 { return names, nil } var admins []model.AdminUser if err := r.db.Where("id IN ?", uniqueIDs(ids)).Find(&admins).Error; err != nil { return nil, err } for _, admin := range admins { name := admin.Nickname if name == "" { name = admin.Username } names[admin.ID] = name } return names, nil } type conversationRow struct { ID uint64 OrderID uint64 Type string Title string Status string Role string LastMessageID *uint64 LastMessagePreview string LastMessageAt *time.Time UnreadCount int64 CreatedAt time.Time UpdatedAt time.Time } func (row conversationRow) toDTO(participants []ParticipantDTO) ConversationDTO { return ConversationDTO{ ID: row.ID, OrderID: row.OrderID, Type: row.Type, Title: row.Title, Status: row.Status, Role: row.Role, Participants: participants, LastMessageID: row.LastMessageID, LastMessagePreview: row.LastMessagePreview, LastMessageAt: row.LastMessageAt, UnreadCount: row.UnreadCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func defaultSupportAdminID(tx *gorm.DB) uint64 { // 查询所有有 chat:view 权限且状态为 active 的管理员 var adminIDs []uint64 err := tx.Table("admin_users AS au"). Joins("JOIN admin_user_roles AS aur ON aur.admin_user_id = au.id"). Joins("JOIN role_permissions AS rp ON rp.role_id = aur.role_id"). Joins("JOIN permissions AS p ON p.id = rp.permission_id"). Where("au.status = ? AND p.code = ?", "active", "chat:view"). Distinct("au.id"). Pluck("au.id", &adminIDs).Error if err != nil || len(adminIDs) == 0 { // 回退到原有逻辑 return fallbackSupportAdminID(tx) } // 统计每个客服当前负责的会话数,选择负载最少的 type adminLoad struct { AdminID uint64 Count int64 } var loads []adminLoad tx.Table("chat_participants AS cp"). Select("cp.participant_id AS admin_id, COUNT(*) AS count"). Where("cp.participant_type = ? AND cp.role = ? AND cp.participant_id IN ?", "admin", "support", adminIDs). Group("cp.participant_id"). Scan(&loads) loadMap := make(map[uint64]int64) for _, l := range loads { loadMap[l.AdminID] = l.Count } // 找到负载最少的客服 var minLoad int64 = -1 var selectedID uint64 for _, id := range adminIDs { count := loadMap[id] if minLoad < 0 || count < minLoad { minLoad = count selectedID = id } } if selectedID > 0 { return selectedID } return fallbackSupportAdminID(tx) } func fallbackSupportAdminID(tx *gorm.DB) uint64 { var cfg model.SystemConfig if err := tx.Where("`key` = ?", "chat.default_support_admin_id").First(&cfg).Error; err == nil { id, parseErr := strconv.ParseUint(cfg.Value, 10, 64) if parseErr == nil && id > 0 && adminActive(tx, id) { return id } } var admin model.AdminUser if err := tx.Where("status = ?", "active").Order("id ASC").First(&admin).Error; err != nil { return ensureDefaultSupportAdmin(tx) } return admin.ID } func ensureDefaultSupportAdmin(tx *gorm.DB) uint64 { var count int64 if err := tx.Model(&model.AdminUser{}).Count(&count).Error; err != nil || count > 0 { return 0 } hash, err := bcrypt.GenerateFromPassword([]byte(defaultSupportPassword), bcrypt.DefaultCost) if err != nil { return 0 } admin := model.AdminUser{ Username: defaultSupportUsername, PasswordHash: string(hash), Nickname: defaultSupportNickname, Status: "active", } if err := tx.Create(&admin).Error; err != nil { return 0 } return admin.ID } func adminActive(tx *gorm.DB, id uint64) bool { var count int64 if err := tx.Model(&model.AdminUser{}).Where("id = ? AND status = ?", id, "active").Count(&count).Error; err != nil { return false } return count > 0 } func orderConversationTitle(order model.RentalOrder) string { if order.OrderNo == "" { return fmt.Sprintf("订单群聊 #%d", order.ID) } return "订单群聊 " + order.OrderNo } func normalizePagination(page, pageSize int) (int, int) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } return page, pageSize } func truncatePreview(content string) string { runes := []rune(content) if len(runes) <= 80 { return content } return string(runes[:80]) } // TransferConversation 转接会话给其他客服 func (r *Repository) TransferConversation(principal Principal, conversationID uint64, toAdminID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { // 验证当前操作者是会话参与者 if _, err := r.findParticipant(tx, principal, conversationID, false); err != nil { return err } // 验证目标客服存在且活跃 if !adminActive(tx, toAdminID) { return fmt.Errorf("目标客服不存在或已禁用") } // 检查目标客服是否已有该会话 var count int64 if err := tx.Model(&model.ChatParticipant{}). Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", conversationID, "admin", toAdminID). Count(&count).Error; err != nil { return err } if count > 0 { return fmt.Errorf("该客服已在会话中") } // 删除原客服参与者 if err := tx.Where("conversation_id = ? AND participant_type = ? AND role = ?", conversationID, "admin", "support"). Delete(&model.ChatParticipant{}).Error; err != nil { return err } // 添加新客服参与者 participant := model.ChatParticipant{ ConversationID: conversationID, ParticipantType: "admin", ParticipantID: toAdminID, Role: "support", JoinedAt: time.Now(), } if err := tx.Create(&participant).Error; err != nil { return err } // 添加系统消息记录转接 message := model.ChatMessage{ ConversationID: conversationID, SenderType: "system", SenderRole: "system", ContentType: "system", Content: "会话已转接给其他客服", AttachmentURLS: emptyJSONList(), } if err := tx.Create(&message).Error; err != nil { return err } return nil }) } // GetAvailableSupportAdmins 获取可用客服列表及其会话数 func (r *Repository) GetAvailableSupportAdmins() ([]SupportAdminDTO, error) { // 查询所有有 chat:view 权限且状态为 active 的管理员 type adminRow struct { ID uint64 Nickname string } var admins []adminRow err := r.db.Table("admin_users AS au"). Select("DISTINCT au.id, COALESCE(NULLIF(au.nickname, ''), au.username) AS nickname"). Joins("JOIN admin_user_roles AS aur ON aur.admin_user_id = au.id"). Joins("JOIN role_permissions AS rp ON rp.role_id = aur.role_id"). Joins("JOIN permissions AS p ON p.id = rp.permission_id"). Where("au.status = ? AND p.code = ?", "active", "chat:view"). Scan(&admins).Error if err != nil { return nil, err } // 统计每个客服的会话数 type loadRow struct { AdminID uint64 Count int64 } var loads []loadRow adminIDs := make([]uint64, len(admins)) for i, a := range admins { adminIDs[i] = a.ID } if len(adminIDs) > 0 { r.db.Table("chat_participants"). Select("participant_id AS admin_id, COUNT(*) AS count"). Where("participant_type = ? AND role = ? AND participant_id IN ?", "admin", "support", adminIDs). Group("participant_id"). Scan(&loads) } loadMap := make(map[uint64]int64) for _, l := range loads { loadMap[l.AdminID] = l.Count } result := make([]SupportAdminDTO, len(admins)) for i, a := range admins { result[i] = SupportAdminDTO{ ID: a.ID, Nickname: a.Nickname, ChatCount: loadMap[a.ID], } } return result, nil } // ListConversationsWithFilter 支持筛选的会话列表 func (r *Repository) ListConversationsWithFilter(principal Principal, page, pageSize int, filter string) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) var total int64 countDB := r.db.Table("chat_conversations AS c"). Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id") switch filter { case "mine": // 只看我的会话 countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) case "unassigned": // 未分配客服的会话 countDB = countDB.Where("c.id NOT IN (?)", r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support")) default: // 全部会话(admin 可以看所有) if principal.Type == "admin" { // 管理员看所有会话 } else { countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } } if err := countDB.Count(&total).Error; err != nil { return nil, err } var rows []conversationRow offset := (page - 1) * pageSize queryDB := r.conversationQuery(principal) switch filter { case "mine": queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) case "unassigned": queryDB = queryDB.Where("c.id NOT IN (?)", r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support")) default: if principal.Type == "admin" { // 管理员看所有会话 } else { queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } } err := queryDB. Order("COALESCE(c.last_message_at, c.created_at) DESC, c.id DESC"). Offset(offset). Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } items := make([]ConversationDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO(nil)) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func fallbackName(participantType string, id uint64, name string) string { if name != "" { return name } switch participantType { case "admin": return "客服" case "system": return "系统" default: return fmt.Sprintf("用户%d", id) } } func uniqueIDs(ids []uint64) []uint64 { seen := map[uint64]bool{} result := make([]uint64, 0, len(ids)) for _, id := range ids { if id == 0 || seen[id] { continue } seen[id] = true result = append(result, id) } return result } func emptyJSONList() datatypes.JSON { raw, _ := json.Marshal([]string{}) return datatypes.JSON(raw) } // UpdateRemark 更新会话备注 func (r *Repository) UpdateRemark(principal Principal, conversationID uint64, remark string) error { return r.db.Model(&model.ChatParticipant{}). Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", conversationID, principal.Type, principal.ID). Update("remark", remark).Error } // ListQuickReplies 获取快捷回复列表(个人 + 全局) func (r *Repository) ListQuickReplies(adminID uint64) ([]QuickReplyDTO, error) { var replies []model.ChatQuickReply err := r.db.Where("admin_user_id = ? OR admin_user_id = 0", adminID). Order("admin_user_id DESC, sort_order ASC, id ASC"). Find(&replies).Error if err != nil { return nil, err } result := make([]QuickReplyDTO, len(replies)) for i, reply := range replies { result[i] = QuickReplyDTO{ ID: reply.ID, AdminUserID: reply.AdminUserID, Title: reply.Title, Content: reply.Content, SortOrder: reply.SortOrder, IsGlobal: reply.AdminUserID == 0, } } return result, nil } // CreateQuickReply 创建快捷回复 func (r *Repository) CreateQuickReply(adminID uint64, req CreateQuickReplyRequest) (*QuickReplyDTO, error) { reply := model.ChatQuickReply{ AdminUserID: adminID, Title: req.Title, Content: req.Content, SortOrder: req.SortOrder, } if err := r.db.Create(&reply).Error; err != nil { return nil, err } return &QuickReplyDTO{ ID: reply.ID, AdminUserID: reply.AdminUserID, Title: reply.Title, Content: reply.Content, SortOrder: reply.SortOrder, IsGlobal: false, }, nil } // UpdateQuickReply 更新快捷回复 func (r *Repository) UpdateQuickReply(adminID uint64, replyID uint64, req UpdateQuickReplyRequest) error { query := r.db.Model(&model.ChatQuickReply{}).Where("id = ? AND (admin_user_id = ? OR admin_user_id = 0)", replyID, adminID) updates := map[string]interface{}{} if req.Title != "" { updates["title"] = req.Title } if req.Content != "" { updates["content"] = req.Content } if req.SortOrder != nil { updates["sort_order"] = *req.SortOrder } if len(updates) == 0 { return nil } return query.Updates(updates).Error } // DeleteQuickReply 删除快捷回复 func (r *Repository) DeleteQuickReply(adminID uint64, replyID uint64) error { return r.db.Where("id = ? AND admin_user_id = ?", replyID, adminID). Delete(&model.ChatQuickReply{}).Error } // GetAutoWelcomeMessage 获取建群自动话术 func (r *Repository) GetAutoWelcomeMessage() string { var cfg model.SystemConfig if err := r.db.Where("`key` = ?", "chat.auto_welcome_message").First(&cfg).Error; err != nil { return "欢迎加入订单群聊!如有任何问题,请随时沟通。" } return cfg.Value } // UpdateAutoWelcomeMessage 更新建群自动话术 func (r *Repository) UpdateAutoWelcomeMessage(message string) error { return r.db.Model(&model.SystemConfig{}). Where("`key` = ?", "chat.auto_welcome_message"). Update("value", message).Error } func decodeStringList(raw datatypes.JSON) []string { if len(raw) == 0 { return []string{} } var items []string if err := json.Unmarshal(raw, &items); err != nil { return []string{} } return items }