feat: 添加客服账号管理及WebSocket功能
This commit is contained in:
92
service/account_service.go
Normal file
92
service/account_service.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Package service - 客服账号服务
|
||||
// 功能:客服账号的增删改查、状态切换
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/dao"
|
||||
"customer-server/model/dto"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
AccountService = new(accountService)
|
||||
)
|
||||
|
||||
type accountService struct{}
|
||||
|
||||
// Add 添加客服账号
|
||||
// 参数: ctx - 上下文,req - 添加客服账号请求
|
||||
// 返回: res - 添加成功后的客服账号ID,err - 错误信息
|
||||
// 功能: 创建新的客服账号记录
|
||||
func (s *accountService) Add(ctx context.Context, req *dto.AddAccountReq) (res *dto.AddAccountRes, err error) {
|
||||
// 检查账号名称是否已存在
|
||||
count, err := dao.Account.Count(ctx, &dto.ListAccountReq{
|
||||
AccountName: req.AccountName,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
err = gerror.Newf("客服账号名称已存在:%s", req.AccountName)
|
||||
return
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.Account.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.AddAccountRes{Id: id}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新客服账号
|
||||
// 参数: ctx - 上下文,req - 更新客服账号请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 更新客服账号信息
|
||||
func (s *accountService) Update(ctx context.Context, req *dto.UpdateAccountReq) (err error) {
|
||||
_, err = dao.Account.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除客服账号
|
||||
// 参数: ctx - 上下文,req - 删除客服账号请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 逻辑删除客服账号记录
|
||||
func (s *accountService) Delete(ctx context.Context, req *dto.DeleteAccountReq) (err error) {
|
||||
_, err = dao.Account.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Get 获取单个客服账号
|
||||
// 参数: ctx - 上下文,req - 获取客服账号请求
|
||||
// 返回: res - 客服账号信息,err - 错误信息
|
||||
// 功能: 根据ID获取单个客服账号详情
|
||||
func (s *accountService) Get(ctx context.Context, req *dto.GetAccountReq) (res *dto.AccountVO, err error) {
|
||||
r, err := dao.Account.GetById(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gconv.Struct(r, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取客服账号列表
|
||||
// 参数: ctx - 上下文,req - 列表查询请求
|
||||
// 返回: res - 客服账号列表及分页信息,err - 错误信息
|
||||
// 功能: 分页查询客服账号记录
|
||||
func (s *accountService) List(ctx context.Context, req *dto.ListAccountReq) (res *dto.ListAccountRes, err error) {
|
||||
list, total, err := dao.Account.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListAccountRes{
|
||||
Total: total,
|
||||
}
|
||||
err = gconv.Struct(list, &res.List)
|
||||
|
||||
return
|
||||
}
|
||||
187
service/account_websocket_service.go
Normal file
187
service/account_websocket_service.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/consts/account"
|
||||
"customer-server/dao"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/model/entity"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.com/red-future/common/jaeger"
|
||||
"github.com/gogf/gf/v2/container/gmap"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// AccountWebSocket 全局单例
|
||||
var AccountWebSocket = &accountWebsocketService{
|
||||
connections: gmap.NewStrAnyMap(true),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // 允许跨域
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type accountWebsocketService struct {
|
||||
connections *gmap.StrAnyMap
|
||||
upgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
// key: userId_platform
|
||||
// accountWsConnection WebSocket 连接信息
|
||||
type accountWsConnection struct {
|
||||
UserId string
|
||||
Platform account.Platform
|
||||
TenantId uint64
|
||||
AccountName string // 客服账号ID
|
||||
Conn *websocket.Conn
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
// Connect 建立 WebSocket 连接
|
||||
func (s *accountWebsocketService) Connect(ctx context.Context, r *ghttp.Request, accountName string, platform account.Platform) error {
|
||||
// 使用原生upgrader升级WebSocket连接
|
||||
ws, err := s.upgrader.Upgrade(r.Response.Writer, r.Request, nil)
|
||||
if err != nil {
|
||||
jaeger.RecordError(ctx, err, "WebSocket 升级失败")
|
||||
return err
|
||||
}
|
||||
defer ws.Close()
|
||||
if g.IsEmpty(accountName) {
|
||||
return errors.New("accountName is empty")
|
||||
}
|
||||
res, err := s.getGreeting(ctx, accountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if g.IsEmpty(&res) {
|
||||
return errors.New("account is empty")
|
||||
}
|
||||
if !g.IsEmpty(res.Greeting) {
|
||||
s.writeJSON(ws, &dto.WebSocketPushMsg{
|
||||
Type: "message",
|
||||
Message: res.Greeting,
|
||||
})
|
||||
glog.Infof(ctx, "已发送开场白 - 用户: %v, 客服账号: %s, 长度: %d", res.Id, accountName, len(res.Greeting))
|
||||
} else {
|
||||
glog.Warningf(ctx, "客服账号未配置开场白 - accountName: %s, tenantId: %v", accountName, res.TenantId)
|
||||
}
|
||||
|
||||
// key格式: tenantId:userId_platform (确保租户隔离)
|
||||
key := gconv.String(res.TenantId) + ":" + gconv.String(res.Creator) + ":" + gconv.String(platform)
|
||||
|
||||
// 关闭旧连接
|
||||
if old := s.connections.Get(key); old != nil {
|
||||
old.(*accountWsConnection).Conn.Close()
|
||||
}
|
||||
|
||||
// 注册新连接(携带 TenantId 和 AccountName)
|
||||
s.connections.Set(key, &accountWsConnection{
|
||||
UserId: res.Creator,
|
||||
Platform: platform,
|
||||
TenantId: res.TenantId,
|
||||
AccountName: accountName,
|
||||
Conn: ws,
|
||||
CreatedAt: gtime.Now().Timestamp(),
|
||||
})
|
||||
|
||||
// 处理消息(阻塞)
|
||||
s.handleConnection(ctx, key, ws)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConnection 处理 WebSocket 连接
|
||||
func (s *accountWebsocketService) handleConnection(ctx context.Context, key string, conn *websocket.Conn) {
|
||||
defer func() {
|
||||
s.connections.Remove(key)
|
||||
conn.Close()
|
||||
glog.Infof(ctx, "WebSocket 连接断开 - %s", key)
|
||||
}()
|
||||
|
||||
for {
|
||||
msgType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
// 排除正常关闭情况:正常关闭、离开页面、无状态码关闭
|
||||
if websocket.IsUnexpectedCloseError(err,
|
||||
websocket.CloseNormalClosure,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseNoStatusReceived,
|
||||
) {
|
||||
jaeger.RecordError(ctx, err, "WebSocket 读取错误")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if msgType != websocket.TextMessage {
|
||||
continue
|
||||
}
|
||||
|
||||
content := gconv.String(message)
|
||||
glog.Infof(ctx, "收到 WebSocket 消息 - %s: %s", key, content)
|
||||
|
||||
// 解析 userId
|
||||
connInfo := s.connections.Get(key)
|
||||
if connInfo == nil {
|
||||
break
|
||||
}
|
||||
wsConn := connInfo.(*accountWsConnection)
|
||||
|
||||
// 先检查对话轮数,>5 则只发卡片,跳过话术
|
||||
//checkCardBeforeProcess 已推送卡片消息,无需ack
|
||||
//if handled, err := checkCardBeforeProcess(ctx, wsConn.TenantId, wsConn.UserId, wsConn.Platform); err != nil {
|
||||
// jaeger.RecordError(ctx, err, "卡片检查失败")
|
||||
//} else if handled {
|
||||
// continue
|
||||
//}
|
||||
|
||||
// 话术匹配并发布响应
|
||||
// status 暂时为空,表示任意行为匹配
|
||||
// isPushed=true表示已直接推送响应(话术匹配),无需ack
|
||||
// isPushed=false表示转发到RAGFlow,需要ack告知用户正在处理
|
||||
|
||||
// 创建带有accountName的context,供GetTenantInfo使用
|
||||
//newCtx := ctx
|
||||
//if wsConn.AccountName != "" {
|
||||
// newCtx = context.WithValue(ctx, "accountName", wsConn.AccountName)
|
||||
//}
|
||||
|
||||
//isPushed, err := Speechcraft.ProcessAndPublish(newCtx, wsConn.UserId, wsConn.Platform, wsConn.TenantId, content, "", wsConn.AccountName)
|
||||
//if err != nil {
|
||||
// jaeger.RecordError(ctx, err, "话术处理失败")
|
||||
// s.writeJSON(conn, &dto.WebSocketPushMsg{Type: "error", Message: "消息处理失败"})
|
||||
// continue
|
||||
//}
|
||||
|
||||
// 只在转发到RAGFlow时发送ack(Go直接返回的不需要ack)
|
||||
//if !isPushed {
|
||||
// s.writeJSON(conn, &dto.WebSocketPushMsg{Type: "ack", Message: "消息已接收,正在处理..."})
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
// writeJSON 发送 JSON 消息
|
||||
func (s *accountWebsocketService) writeJSON(conn *websocket.Conn, data interface{}) {
|
||||
jsonBytes, _ := gjson.Encode(data)
|
||||
conn.WriteMessage(websocket.TextMessage, jsonBytes)
|
||||
}
|
||||
|
||||
// getGreeting 获取客服账号的开场白
|
||||
func (s *accountWebsocketService) getGreeting(ctx context.Context, accountName string) (res *entity.Account, err error) {
|
||||
res, err = dao.Account.GetByAccountName(ctx, &dto.GetByAccountNameReq{
|
||||
AccountName: accountName,
|
||||
})
|
||||
if err != nil {
|
||||
jaeger.RecordError(ctx, err, "查询客服账号开场白失败")
|
||||
glog.Errorf(ctx, "查询开场白失败: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
90
service/scripted_speech_service.go
Normal file
90
service/scripted_speech_service.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Package service - 预制话术服务
|
||||
// 功能:预制话术的增删改查
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/dao"
|
||||
"customer-server/model/dto"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ScriptedSpeech = new(scriptedSpeech)
|
||||
)
|
||||
|
||||
type scriptedSpeech struct{}
|
||||
|
||||
// Add 添加预制话术
|
||||
// 参数: ctx - 上下文,req - 添加预制话术请求
|
||||
// 返回: res - 添加成功后的预制话术ID,err - 错误信息
|
||||
// 功能: 创建新的预制话术记录
|
||||
func (s *scriptedSpeech) Add(ctx context.Context, req *dto.AddScriptedSpeechReq) (res *dto.AddScriptedSpeechRes, err error) {
|
||||
// 检查账号是否存在
|
||||
account, err := dao.Account.GetById(ctx, &dto.GetAccountReq{Id: req.AccountId})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if account == nil {
|
||||
err = gerror.New("客服账号不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.ScriptedSpeech.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.AddScriptedSpeechRes{Id: id}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新预制话术
|
||||
// 参数: ctx - 上下文,req - 更新预制话术请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 更新预制话术信息
|
||||
func (s *scriptedSpeech) Update(ctx context.Context, req *dto.UpdateScriptedSpeechReq) (err error) {
|
||||
_, err = dao.ScriptedSpeech.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除预制话术
|
||||
// 参数: ctx - 上下文,req - 删除预制话术请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 逻辑删除预制话术记录
|
||||
func (s *scriptedSpeech) Delete(ctx context.Context, req *dto.DeleteScriptedSpeechReq) (err error) {
|
||||
_, err = dao.ScriptedSpeech.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Get 获取单个预制话术
|
||||
// 参数: ctx - 上下文,req - 获取预制话术请求
|
||||
// 返回: res - 预制话术信息,err - 错误信息
|
||||
// 功能: 根据ID获取单个预制话术详情
|
||||
func (s *scriptedSpeech) Get(ctx context.Context, req *dto.GetScriptedSpeechReq) (res *dto.ScriptedSpeechVO, err error) {
|
||||
r, err := dao.ScriptedSpeech.GetById(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gconv.Struct(r, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取预制话术列表
|
||||
// 参数: ctx - 上下文,req - 列表查询请求
|
||||
// 返回: res - 预制话术列表及分页信息,err - 错误信息
|
||||
// 功能: 分页查询预制话术记录
|
||||
func (s *scriptedSpeech) List(ctx context.Context, req *dto.ListScriptedSpeechReq) (res *dto.ListScriptedSpeechRes, err error) {
|
||||
list, total, err := dao.ScriptedSpeech.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListScriptedSpeechRes{
|
||||
Total: total,
|
||||
}
|
||||
err = gconv.Struct(list, &res.List)
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user