修正默认客服分配逻辑

This commit is contained in:
yml2213
2026-06-08 06:35:56 +08:00
parent ebf9b95f5a
commit 9d6b1d945c
3 changed files with 50 additions and 60 deletions
+36 -54
View File
@@ -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