修正默认客服分配逻辑

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
@@ -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
}