feat: 优化客服会话及延迟消息逻辑

This commit is contained in:
2026-04-16 15:48:26 +08:00
parent d99481cdb0
commit 9f54f95264
10 changed files with 132 additions and 116 deletions

View File

@@ -2,13 +2,9 @@ package service
import (
"context"
"customer-server/consts/public"
"customer-server/model/dto"
"fmt"
gmq "github.com/bjang03/gmq/core/gmq"
"github.com/bjang03/gmq/mq"
"github.com/bjang03/gmq/types"
"github.com/gogf/gf/v2/frame/g"
)
@@ -18,14 +14,6 @@ var (
type accountHttpService struct{}
func (s *accountHttpService) DeleteDelayMsg(ctx context.Context) (err error) {
return gmq.GetGmq(public.GmqMsgPluginsName).GmqDeleteDelay(ctx, &mq.NatsDelMessage{
DelMessage: types.DelMessage{
Topic: public.AccountFollowupTopic,
},
})
}
func (s *accountHttpService) Connect(ctx context.Context, req *dto.AccountHttpConnectReq) (res *dto.AccountHttpConnectRes, err error) {
// 获取客服账号信息
accountInfo, err := SessionToolService.GetAccountInfo(ctx, req.AccountCode)

View File

@@ -21,6 +21,7 @@ import (
gmq "github.com/bjang03/gmq/core/gmq"
"github.com/bjang03/gmq/mq"
"github.com/bjang03/gmq/types"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
@@ -33,7 +34,7 @@ type sessionToolService struct{}
func (s *sessionToolService) PushOpeningRemark(ctx context.Context, userId string, accountInfo *dto.AccountVO, headers map[string]string) (content string, err error) {
content = ""
var sceneType = scriptedSpeech.SceneTypeOpeningRemark
var key = fmt.Sprintf("account:%s:%s:%s", accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId)
var key = fmt.Sprintf(public.AccountMsgKey, accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId)
get, err := g.Redis().Get(ctx, key)
if err != nil {
return
@@ -50,13 +51,17 @@ func (s *sessionToolService) PushOpeningRemark(ctx context.Context, userId strin
err = fmt.Errorf("数据集不存在")
return
}
var datasetDescriptions []string
var datasetDescriptions [][]string
for _, dataset := range datasetInfo.List {
datasetDescriptions = append(datasetDescriptions, dataset.Name)
datasetDescriptions = append(datasetDescriptions, []string{dataset.Name, gconv.String(dataset.Id)})
}
content = SessionToolService.BuildMenuContent(accountInfo.Greeting, datasetDescriptions, len(accountInfo.DatasetIds))
content, err = SessionToolService.BuildGreeting(ctx, userId, accountInfo.Greeting, datasetDescriptions, len(accountInfo.DatasetIds))
} else {
content = SessionToolService.BuildMenuContent(accountInfo.Greeting, accountInfo.KeywordOption, len(accountInfo.DatasetIds))
var datasetDescriptions [][]string
for _, keyword := range accountInfo.KeywordOption {
datasetDescriptions = append(datasetDescriptions, []string{keyword, gconv.String(accountInfo.DatasetIds[0])})
}
content, err = SessionToolService.BuildGreeting(ctx, userId, accountInfo.Greeting, datasetDescriptions, len(accountInfo.DatasetIds))
}
err = s.pushDelayMsg(ctx, key, sceneType.Code(), sceneType.Desc(), accountInfo.DatasetIds)
if err != nil {
@@ -67,19 +72,52 @@ func (s *sessionToolService) PushOpeningRemark(ctx context.Context, userId strin
}
func (s *sessionToolService) PushDialog(ctx context.Context, userId string, questionContent string, accountInfo *dto.AccountVO, headers map[string]string) (content string, err error) {
sceneType := scriptedSpeech.SceneTypeDialog
// 删除延迟消息
//err = s.DeleteDelayMsg(ctx)
//if err != nil {
// return nil, err
//}
content = ""
var key = fmt.Sprintf("account:%s:%s:%s", accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId)
// 删除延迟消息
if err = s.DeleteDelayMsg(ctx); err != nil {
return
}
var key = fmt.Sprintf(public.AccountMsgKey, accountInfo.AccountCode, account.GetDescByCode(accountInfo.Platform), userId)
get, err := g.Redis().Get(ctx, key)
if err != nil {
return
}
if !g.IsEmpty(get) {
sceneType := scriptedSpeech.SceneTypeDialog
var datasetIds []int64
var optionsMap *gvar.Var
optionsMap, err = g.Redis().Get(ctx, fmt.Sprintf(public.AccountGreetingOptionsKey, userId))
if err != nil {
return
}
jsonStr := gconv.String(optionsMap)
var data map[string]interface{}
if err = gconv.Scan(jsonStr, &data); err != nil {
return
}
for i, item := range data {
// 把每一项转成 map
if i == questionContent {
m := gconv.Map(item)
questionContent = gconv.String(m["datasetName"])
datasetIds = gconv.Int64s(m["datasetId"])
}
}
if g.IsEmpty(datasetIds) {
var datasetRes []int64
datasetRes, err = s.getDatasetIdsByKeywords(ctx, questionContent, headers)
if err != nil {
return
}
if len(datasetRes) > 0 {
datasetIds = datasetRes
} else {
datasetIds = accountInfo.DatasetIds
}
}
// 获取用户对话上下文
var history []*dto.Message
history, err = SessionToolService.GetUserHistory(ctx, userId)
@@ -127,10 +165,10 @@ func (s *sessionToolService) PushDialog(ctx context.Context, userId string, ques
}
}
}
if sceneType.Code() != scriptedSpeech.SceneTypeCardSend.Code() {
if *sceneType.Code() != *scriptedSpeech.SceneTypeCardSend.Code() {
// 通过HTTP调用rag服务的RAG查询接口
var ragQuery *dto.RagQueryRes
ragQuery, err = SessionToolService.GetRagQuery(ctx, questionContent, accountInfo.DatasetIds, history, headers)
ragQuery, err = SessionToolService.GetRagQuery(ctx, questionContent, datasetIds, history, headers)
if err != nil {
err = fmt.Errorf("调用rag服务的RAG查询接口失败: %w", err)
return
@@ -148,7 +186,7 @@ func (s *sessionToolService) PushDialog(ctx context.Context, userId string, ques
}
}
err = s.pushDelayMsg(ctx, key, sceneType.Code(), sceneType.Desc(), accountInfo.DatasetIds)
err = s.pushDelayMsg(ctx, key, sceneType.Code(), sceneType.Desc(), datasetIds)
if err != nil {
return
}
@@ -157,7 +195,7 @@ func (s *sessionToolService) PushDialog(ctx context.Context, userId string, ques
}
func (s *sessionToolService) pushDelayMsg(ctx context.Context, key string, sceneTypeCode scriptedSpeech.SceneType, sceneTypeDesc string, datasetIds []int64) (err error) {
err = g.Redis().SetEX(ctx, key, sceneTypeDesc, gconv.Int64(10*time.Second))
err = g.Redis().SetEX(ctx, key, sceneTypeDesc, gconv.Int64(public.DialogTimeout*time.Second))
if err != nil {
return err
}
@@ -168,19 +206,19 @@ func (s *sessionToolService) pushDelayMsg(ctx context.Context, key string, scene
if err != nil {
return fmt.Errorf("获取追问话术内容失败: %w", err)
}
if g.IsEmpty(scriptedSpeechInfo) {
if sceneTypeCode == scriptedSpeech.SceneTypeOpeningRemark.Code() {
msg = "宝子,刚才给您发的信息您有看到吗?有任何问题都能直接问我,加微信也能更方便沟通~"
} else if sceneTypeCode == scriptedSpeech.SceneTypeDialog.Code() {
msg = "看您暂时没回复,是不是还有什么疑问?加微信我详细给您说明~"
} else if sceneTypeCode == scriptedSpeech.SceneTypeCardSend.Code() {
msg = "宝子,加上没~要及时加哦,不然卡片容易失效哒✨"
}
}
msg = scriptedSpeechInfo.QuestionContent
} else {
msg = "宝子,刚才给您发的信息您有看到吗?有任何问题都能直接问我,加微信也能更方便沟通~"
}
if g.IsEmpty(msg) {
if *sceneTypeCode == *scriptedSpeech.SceneTypeOpeningRemark.Code() {
msg = public.SceneOpeningRemark
} else if *sceneTypeCode == *scriptedSpeech.SceneTypeDialog.Code() {
msg = public.SceneDialog
} else if *sceneTypeCode == *scriptedSpeech.SceneTypeCardSend.Code() {
msg = public.SceneCardSend
}
}
var msgMap = map[string]string{
"key": key,
"data": msg,
@@ -197,6 +235,14 @@ func (s *sessionToolService) pushDelayMsg(ctx context.Context, key string, scene
return
}
func (s *sessionToolService) DeleteDelayMsg(ctx context.Context) (err error) {
return gmq.GetGmq(public.GmqMsgPluginsName).GmqDeleteDelay(ctx, &mq.NatsDelMessage{
DelMessage: types.DelMessage{
Topic: public.AccountFollowupTopic,
},
})
}
// GetAccountInfo 获取客服账号信息
func (s *sessionToolService) GetAccountInfo(ctx context.Context, accountCode string) (res *dto.AccountVO, err error) {
r, err := dao.Account.GetByAccountCode(ctx, &dto.GetByAccountCodeReq{
@@ -248,31 +294,34 @@ func (s *sessionToolService) GetDatasetInfo(ctx context.Context, datasetIds []in
return
}
// BuildMenuContent 生成菜单话术内容
func (s *sessionToolService) BuildMenuContent(greeting string, options []string, datasetCount int) string {
// BuildGreeting 构建问候语
func (s *sessionToolService) BuildGreeting(ctx context.Context, userId, greeting string, options [][]string, datasetCount int) (content string, err error) {
var sb strings.Builder
// 问候语
if datasetCount > 1 {
greeting = "您好,很高兴为您服务!请问咨询什么方面问题?"
} else {
if greeting == "" {
greeting = "您好,很高兴为您服务!请问有什么可以帮您?"
}
if datasetCount > 1 || greeting == "" {
greeting = public.GreetingBegin
}
sb.WriteString(greeting)
sb.WriteByte('\n')
// 拼接选项 1、xx 2、xx...
var optionsMap = make(map[string]map[string]string, len(options))
for i, opt := range options {
optionsMap[gconv.String(i+1)] = map[string]string{
"datasetId": opt[1],
"datasetName": opt[0],
}
sb.WriteString(fmt.Sprintf("%d、%s\n", i+1, opt))
if i == len(options)-1 {
sb.WriteString(fmt.Sprintf("%s\n", "💗回复数字就好~"))
sb.WriteString(fmt.Sprintf("%s\n", public.GreetingBetween))
}
}
// 固定结尾
sb.WriteString("🌟也可直接点击下方咨询专业老师~")
sb.WriteString(public.GreetingEnd)
content = sb.String()
err = g.Redis().SetEX(ctx, fmt.Sprintf(public.AccountGreetingOptionsKey, userId), optionsMap, gconv.Int64(public.DialogTimeout*time.Second))
return sb.String()
return
}
// GetScriptedSpeechContent 获取话术内容
@@ -304,7 +353,7 @@ func (s *sessionToolService) GetRagQuery(ctx context.Context, questionContent st
// SaveUserHistory 保存用户对话历史到Redis
func (s *sessionToolService) SaveUserHistory(ctx context.Context, userKey string, newMessages []*dto.Message) (err error) {
key := fmt.Sprintf(public.AccountDialogKeyUserId, userKey)
key := fmt.Sprintf(public.AccountDialogHistoryKey, userKey)
// 1. 先读旧历史
var oldMessages []*dto.Message
@@ -327,7 +376,7 @@ func (s *sessionToolService) SaveUserHistory(ctx context.Context, userKey string
if err != nil {
return err
}
return g.Redis().SetEX(ctx, key, data, gconv.Int64(15*time.Second))
return g.Redis().SetEX(ctx, key, data, gconv.Int64(public.DialogTimeout*time.Second))
}
// GetUserHistory 从Redis获取用户历史
@@ -346,19 +395,19 @@ func (s *sessionToolService) GetUserHistory(ctx context.Context, key string) ([]
// ClearUserHistory 清空历史(可选)
func (s *sessionToolService) ClearUserHistory(ctx context.Context, userKey string) (int64, error) {
key := fmt.Sprintf(public.AccountDialogKeyUserId, userKey)
key := fmt.Sprintf(public.AccountDialogHistoryKey, userKey)
return g.Redis().Del(ctx, key)
}
// getDatasetIdsByKeywords 通过关键词查询数据集ID
func (s *sessionToolService) getDatasetIdsByKeywords(ctx context.Context, content string, headers map[string]string) (res []int64, err error) {
func (s *sessionToolService) getDatasetIdsByKeywords(ctx context.Context, questionContent string, headers map[string]string) (res []int64, err error) {
// 1. 提取关键词
keywords := s.extractKeywords(content)
keywords := s.extractKeywords(questionContent)
g.Log().Infof(ctx, "提取关键词: %v", keywords)
// 通过HTTP调用rag服务的关键词查询接口
respKeyword := &dto.RAGListKeywordRes{}
if err = http.Get(ctx, "rag/keyword/listKeyword", headers, &respKeyword, &dto.RAGListKeywordReq{
if err = http.Get(ctx, "rag/keyword/list", headers, &respKeyword, &dto.RAGListKeywordReq{
Words: keywords,
}); err != nil {
jaeger.RecordError(ctx, err, "RAG查询关键词失败")