diff --git a/backend/internal/modules/chat/repository.go b/backend/internal/modules/chat/repository.go index b695320..b2d4e8a 100644 --- a/backend/internal/modules/chat/repository.go +++ b/backend/internal/modules/chat/repository.go @@ -11,7 +11,6 @@ import ( "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" @@ -23,9 +22,7 @@ type Repository struct { } const ( - defaultSupportUsername = "admin" - defaultSupportPassword = "admin123456" - defaultSupportNickname = "超级管理员" + defaultSupportRoleCode = "cs" ) func NewRepository(db *gorm.DB, hub *chathub.Hub) *Repository { @@ -689,21 +686,16 @@ func (row conversationRow) toDTO(participants []ParticipantDTO) ConversationDTO } 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) + if supportID := configuredDefaultSupportAdminID(tx); supportID > 0 { + return supportID } - // 统计每个客服当前负责的会话数,选择负载最少的 + adminIDs, err := supportAdminIDs(tx) + if err != nil || len(adminIDs) == 0 { + return 0 + } + + // 未配置默认客服时,按当前会话负载选择客服角色中最空闲的一位。 type adminLoad struct { AdminID uint64 Count int64 @@ -733,48 +725,38 @@ func defaultSupportAdminID(tx *gorm.DB) uint64 { if selectedID > 0 { return selectedID } - return fallbackSupportAdminID(tx) + return 0 } -func fallbackSupportAdminID(tx *gorm.DB) uint64 { +func configuredDefaultSupportAdminID(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) { + if parseErr == nil && id > 0 && adminIsSupport(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 + return 0 } -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 supportAdminIDs(tx *gorm.DB) ([]uint64, error) { + 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 roles AS r ON r.id = aur.role_id"). + Where("au.status = ? AND r.code = ?", "active", defaultSupportRoleCode). + Order("CASE au.support_status WHEN 'online' THEN 0 WHEN 'busy' THEN 1 ELSE 2 END, au.id ASC"). + Pluck("au.id", &adminIDs).Error + return adminIDs, err } -func adminActive(tx *gorm.DB, id uint64) bool { +func adminIsSupport(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 { + if err := tx.Table("admin_users AS au"). + Joins("JOIN admin_user_roles AS aur ON aur.admin_user_id = au.id"). + Joins("JOIN roles AS r ON r.id = aur.role_id"). + Where("au.id = ? AND au.status = ? AND r.code = ?", id, "active", defaultSupportRoleCode). + Count(&count).Error; err != nil { return false } return count > 0 @@ -826,9 +808,9 @@ func (r *Repository) TransferConversation(principal Principal, conversationID ui if _, err := r.findParticipant(tx, principal, conversationID, false); err != nil { return err } - // 验证目标客服存在且活跃 - if !adminActive(tx, toAdminID) { - return fmt.Errorf("目标客服不存在或已禁用") + // 验证目标客服存在、活跃且拥有客服角色,避免转接给超级管理员。 + if !adminIsSupport(tx, toAdminID) { + return fmt.Errorf("目标客服不存在、已禁用或不是客服角色") } // 检查目标客服是否已有该会话 var count int64 @@ -874,7 +856,7 @@ func (r *Repository) TransferConversation(principal Principal, conversationID ui // GetAvailableSupportAdmins 获取可用客服列表及其会话数 func (r *Repository) GetAvailableSupportAdmins() ([]SupportAdminDTO, error) { - // 查询所有有 chat:view 权限且状态为 active 的管理员 + // 仅展示客服角色管理员,超级管理员即使有 chat:view 权限也不作为客服候选。 type adminRow struct { ID uint64 Nickname string @@ -882,11 +864,11 @@ func (r *Repository) GetAvailableSupportAdmins() ([]SupportAdminDTO, error) { } var admins []adminRow err := r.db.Table("admin_users AS au"). - Select("DISTINCT au.id, COALESCE(NULLIF(au.nickname, ''), au.username) AS nickname, au.support_status"). + Select("au.id, COALESCE(NULLIF(au.nickname, ''), au.username) AS nickname, au.support_status"). 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"). + Joins("JOIN roles AS r ON r.id = aur.role_id"). + Where("au.status = ? AND r.code = ?", "active", defaultSupportRoleCode). + Order("CASE au.support_status WHEN 'online' THEN 0 WHEN 'busy' THEN 1 ELSE 2 END, au.id ASC"). Scan(&admins).Error if err != nil { return nil, err diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 945f0dd..a28763d 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -34,7 +34,7 @@ var defaultConfigs = []defaultConfig{ {Key: listingPublishAgreementsConfigKey, Value: defaultListingPublishAgreementsConfigValue(), Description: "发布账号前协议配置 JSON"}, {Key: orderAgreementsConfigKey, Value: defaultOrderAgreementsConfigValue(), Description: "下单前协议配置 JSON"}, {Key: postRentalNoticeConfigKey, Value: defaultPostRentalNoticeConfigValue(), Description: "租后须知配置 JSON"}, - {Key: "chat.default_support_admin_id", Value: "1", Description: "订单群聊回退客服 ID(仅当无可用客服时使用)"}, + {Key: "chat.default_support_admin_id", Value: "2", Description: "默认客服 ID(必须是启用状态的客服角色)"}, {Key: "chat.auto_welcome_message", Value: "欢迎加入订单群聊!如有任何问题,请随时沟通。", Description: "建群后自动发送的欢迎话术"}, {Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"}, {Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"}, @@ -150,7 +150,7 @@ func (r *Repository) ensureDefaults() error { return err } if item.Key == "chat.default_support_admin_id" { - if err := updateDefaultDescription(tx, item); err != nil { + if err := updateDefaultSupportConfig(tx, item); err != nil { return err } } @@ -164,15 +164,23 @@ func (r *Repository) ensureDefaults() error { }) } -func updateDefaultDescription(tx *gorm.DB, item defaultConfig) error { +func updateDefaultSupportConfig(tx *gorm.DB, item defaultConfig) error { var row model.SystemConfig if err := tx.Where("`key` = ?", item.Key).First(&row).Error; err != nil { return err } - if row.Description == item.Description { + changed := false + if strings.TrimSpace(row.Value) == "1" { + row.Value = item.Value + changed = true + } + if row.Description != item.Description { + row.Description = item.Description + changed = true + } + if !changed { return nil } - row.Description = item.Description return tx.Save(&row).Error } diff --git a/backend/migrations/000001_init.sql b/backend/migrations/000001_init.sql index 246f3b2..d692f9a 100644 --- a/backend/migrations/000001_init.sql +++ b/backend/migrations/000001_init.sql @@ -658,7 +658,7 @@ WHERE au.username IN ('kf1', 'kf2') AND r.code = 'cs'; INSERT INTO system_configs (`key`, `value`, description) VALUES ('listing.review_required', 'true', '商品是否需要审核'), ('order.pending_payment_timeout_minutes', '15', '待支付超时时间(分钟)'), -('chat.default_support_admin_id', '1', '订单群聊回退客服 ID(仅当无可用客服时使用)'), +('chat.default_support_admin_id', '2', '默认客服 ID(必须是启用状态的客服角色)'), ('chat.auto_welcome_message', '欢迎加入订单群聊!如有任何问题,请随时沟通。', '建群后自动发送的欢迎话术') ON DUPLICATE KEY UPDATE `value` = VALUES(`value`),