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