feat: 客服聊天

This commit is contained in:
徐俊杰 2025-06-12 17:07:49 +08:00
parent 88333063d7
commit 47c77fcdbb
27 changed files with 8621 additions and 1827 deletions

File diff suppressed because it is too large Load Diff

View File

@ -18,9 +18,9 @@
syntax = "proto3";
package accountFiee;
import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";
option go_package = "./;accountFiee";
//protoc -I . -I C:\Users\lenovo\go\src --go_out=. --go-triple_out=. ./accountFiee.proto
service AccountFiee {
rpc Login (LoginRequest) returns (TokenInfo) {}
rpc RefreshToken (RefreshTokenRequest) returns (TokenInfo) {} //token
@ -62,6 +62,37 @@ service AccountFiee {
rpc VerifySliderCaptcha(VerifySliderCaptchaRequest) returns (VerifySliderCaptchaResponse) {}//
rpc SendNationMsg (SendNationMsgRequest) returns (SendMsgStatusResponse) {} // --
rpc VerifySliderStatus(VerifySliderStatusRequest) returns (VerifySliderStatusResponse) {}//
// submit info
rpc SaveSubmitInfo(SubmitInfoRequest) returns (CommonResponse);
//-------------------------------------------------------------
rpc CreateChatUser ( ChatUserData )returns( CreateChatUserResp ){} //
rpc UpdateChatUser ( ChatUserData )returns( CommonMsg ){} //
rpc SaveChatUser ( ChatUserData )returns( CommonMsg ){} //
rpc DeleteChatUser ( DeleteChatUserRequest )returns( CommonMsg ){} //
rpc GetChatUserDetail ( GetChatUserByIdRequest )returns( ChatUserData ){} //
rpc GetChatUserList ( GetChatUserListRequest )returns( GetChatUserListResp ){} //
rpc GetChatUserList2 ( GetChatUserListRequest2 )returns( GetChatUserListResp2 ){} //2
rpc RegisterWaiter ( RegisterWaiterRequest )returns( RegisterWaiterResp ){} //
rpc CreateChatRecord ( ChatRecordData )returns( CreateChatRecordResp ){} //ChatRecord
rpc UpdateChatRecord ( ChatRecordData )returns( CommonMsg ){} //ChatRecord
rpc SaveChatRecord ( ChatRecordData )returns( CommonMsg ){} //ChatRecord
rpc DeleteChatRecord ( DeleteChatRecordRequest )returns( CommonMsg ){} //ChatRecord
rpc GetChatRecordDetail ( GetChatRecordByIdRequest )returns( ChatRecordData ){} //ChatRecord详情
rpc GetChatRecordList ( GetChatRecordListRequest )returns( GetChatRecordListResp ){} //ChatRecord列表
rpc CreateChatMedia ( ChatMediaData )returns( CreateChatMediaResp ){} //ChatMedia
rpc UpdateChatMedia ( ChatMediaData )returns( CommonMsg ){} //ChatMedia
rpc SaveChatMedia ( ChatMediaData )returns( CommonMsg ){} //ChatMedia
rpc DeleteChatMedia ( DeleteChatMediaRequest )returns( CommonMsg ){} //ChatMedia
rpc GetChatMediaDetail ( GetChatMediaByIdRequest )returns( ChatMediaData ){} //ChatMedia详情
rpc GetChatMediaList ( GetChatMediaListRequest )returns( GetChatMediaListResp ){} //ChatMedia列表
rpc CreateChatAutoReplyRuler ( ChatAutoReplyRulerData )returns( CreateChatAutoReplyRulerResp ){} //
rpc UpdateChatAutoReplyRuler ( ChatAutoReplyRulerData )returns( CommonMsg ){} //
rpc SaveChatAutoReplyRuler ( ChatAutoReplyRulerData )returns( CommonMsg ){} //
rpc DeleteChatAutoReplyRuler ( DeleteChatAutoReplyRulerRequest )returns( CommonMsg ){} //
rpc GetChatAutoReplyRulerDetail ( GetChatAutoReplyRulerByIdRequest )returns( ChatAutoReplyRulerData ){} //
rpc GetChatAutoReplyRulerList ( GetChatAutoReplyRulerListRequest )returns( GetChatAutoReplyRulerListResp ){} //
}
message VerifySliderStatusRequest {
@ -817,4 +848,207 @@ message ClockLogReq{
message ClockLogListResponse{
repeated ClockLogInfo data =1;
uint64 count = 2;
}
message SubmitInfoRequest{
string firstName = 1;
string lastName = 2;
string email = 3;
string company = 4;
string phone = 5;
}
message CommonMsg{
string msg = 1;
}
enum MsgType{
UnknownMsgType = 0 ;//
TextMsgType = 1 ;//
ImageMsgType = 2 ;//
AudioMsgType = 3 ;//
VideoMsgType = 4 ;//
FileType = 5 ;//
}
message ChatRecordData{
int64 ID=1;
string createdAt=2;
string updatedAt=3;
int64 deletedAt=4;
string sessionId = 5; //UID
int64 userId = 6; //ID
string name = 7; //
string avatar = 8; //
MsgType msgType = 9; //
string content = 10; //
repeated ChatMediaData medias = 11; //
int32 waiterRead=12;// 1= 2=
int64 localStamp = 13; //
string domain =14;//
}
message CreateChatRecordResp{
ChatRecordData data=1;
string msg=2;
}
message DeleteChatRecordRequest{
int64 id=1; //id
repeated int64 ids=2;//id列表
}
message GetChatRecordByIdRequest{
int64 id=1; //id
}
message GetChatRecordListRequest{
ChatRecordData query =1;
int64 page=2;
int64 pageSize=3;
string where=4;
string order=5;
}
message GetChatRecordListResp{
repeated ChatRecordData list=1;
int64 page=2;
int64 pageSize=3;
int64 Total=4;
}
message RegisterWaiterRequest{
string origin=1; //
int64 originId=2; //ID
string nickName=3; //
string avatar=4; //
string telNum=5; //
string invitationCode=6; //
string account=7;
}
message RegisterWaiterResp{
int64 userId=1;
}
message ChatMediaData{
int64 ID=1;
string createdAt=2;
string updatedAt=3;
int64 deletedAt=4;
string url = 5; //url
string md5 = 6; //md5值
string size = 7; //
string ext = 8; //
string convText=9; //
int64 duration=10;//
}
message CreateChatMediaResp{
ChatMediaData data=1;
string msg=2;
}
message DeleteChatMediaRequest{
int64 id=1; //id
repeated int64 ids=2;//id列表
}
message GetChatMediaByIdRequest{
int64 id=1; //id
}
message GetChatMediaListRequest{
ChatMediaData query =1;
int64 page=2;
int64 pageSize=3;
string where=4;
string order=5;
}
message GetChatMediaListResp{
repeated ChatMediaData list=1;
int64 page=2;
int64 pageSize=3;
int64 Total=4;
}
message GetChatUserListRequest2{
int64 page=1;
int64 pageSize=2;
string where=3;
string name=4;
repeated int64 userIdIn=5;
}
message ChatUser2{
int64 userId=1;
string name=2;
string avatar=3;
string origin=4;
string originId=5;
}
message GetChatUserListResp2{
repeated ChatUser2 list=1;
int64 page=2;
int64 pageSize=3;
int64 Total=4;
string where=5;
}
message ChatAutoReplyRulerData{
int64 ID = 1; //
string createdAt = 2; //
string updatedAt = 3; //
int64 deletedAt = 4; //
string title = 5; //
string ruler = 6; //
int32 rulerStatus = 7; //: 1= 2=
}
message CreateChatAutoReplyRulerResp{
ChatAutoReplyRulerData data=1;
string msg=2;
}
message DeleteChatAutoReplyRulerRequest{
int64 id=1; //id
repeated int64 ids=2;//id列表
}
message GetChatAutoReplyRulerByIdRequest{
int64 id=1; //id
}
message GetChatAutoReplyRulerListRequest{
ChatAutoReplyRulerData query =1;
int64 page=2;
int64 pageSize=3;
string where=4;
string order=5;
}
message GetChatAutoReplyRulerListResp{
repeated ChatAutoReplyRulerData list=1;
int64 page=2;
int64 pageSize=3;
int64 Total=4;
}
message ChatUserData{
int64 ID = 1; //
string createdAt = 2; //
string updatedAt = 3; //
int64 deletedAt = 4; //
string nickName = 5; //
string account = 6; //
int32 role = 7; // 1= 2=
string origin = 8; //
int64 originId = 9; //ID
string avatar = 10; //
}
message CreateChatUserResp{
ChatUserData data=1;
string msg=2;
}
message DeleteChatUserRequest{
int64 id=1; //id
repeated int64 ids=2;//id列表
}
message GetChatUserByIdRequest{
int64 id=1; //id
}
message GetChatUserListRequest{
ChatUserData query =1;
int64 page=2;
int64 pageSize=3;
string where=4;
string order=5;
}
message GetChatUserListResp{
repeated ChatUserData list=1;
int64 page=2;
int64 pageSize=3;
int64 Total=4;
}

View File

@ -522,3 +522,6 @@ func (this *ClockLogListResponse) Validate() error {
}
return nil
}
func (this *SubmitInfoRequest) Validate() error {
return nil
}

File diff suppressed because it is too large Load Diff

1
go.mod
View File

@ -6,6 +6,7 @@ replace (
github.com/fonchain_enterprise/utils/aes => ../utils/aes
github.com/fonchain_enterprise/utils/objstorage => ../utils/objstorage
//github.com/fonchain_enterprise/utils/objstorage => ../../tyfon-/utils/objstorage
github.com/fonchain/utils/voice => ../utils/voice
)
//

2
pkg/cache/common.go vendored
View File

@ -17,7 +17,7 @@ type RedisConfig struct {
RedisDbName string
}
//LoadRedis 在中间件中初始化redis链接
// LoadRedis 在中间件中初始化redis链接
func LoadRedis(configEnv RedisConfig) {
db, _ := strconv.ParseUint(configEnv.RedisDbName, 10, 64)
client := redis.NewClient(&redis.Options{

178
pkg/common/ws/base.go Normal file
View File

@ -0,0 +1,178 @@
// Package ws -----------------------------
// @file : hertzWSUpgrade.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/6/28 14:14
// -------------------------------------------
package ws
import (
"encoding/json"
"fonchain-fiee/pkg/e"
"fonchain-fiee/pkg/serializer"
"strings"
)
type WsType int
const (
RegisterType WsType = iota
ErrorType
TestType
ChatType
NewChatMsgType //新消息通知
AuthorizationType //token校验通知
)
// 消息结构
type WSMessage struct {
Type string `json:"type"`
Data string `json:"data"`
}
// websocket消息内容
type WsInfo struct {
Type WsType `json:"type"` //消息类型
Content interface{} `json:"content"` //消息内容
From string `json:"from"` //发送者 0为服务端,客户端填写clientId
To string `json:"to"` //接收者 接收消息的用户id
Mark string `json:"mark"`
//Conn *websocket.Conn `json:"-"` //客户端发送消息使用
}
type WsSessionInfo struct {
Type WsType `json:"type"` //消息类型
//SessionId string `json:"sessionId"` //会话Id
Content interface{} `json:"content"` //消息内容
}
// 身份认证消息
type AuthorizationInfo struct {
Type WsType `json:"type"` //消息类型
Content AuthInfo `json:"content"`
}
type AuthInfo struct {
Auth string `json:"auth"`
Domain string `json:"domain"`
}
// 注册消息
type WsRegisterInfo struct {
Type WsType `json:"type"` //消息类型
Content UserInfo `json:"content"` //消息内容
From string `json:"from"` //发送者 0为服务端,客户端填写clientId
To string `json:"to"` //接收者 接收消息的用户id
//Conn *websocket.Conn `json:"-"` //客户端发送消息使用
}
type UserInfo struct {
Uuid string `json:"uuid"` //画家uid
UserId int64 `json:"userId"` //用户id
ClientId string `json:"clientId,omitempty"` //服务端临时签发的客户端uid
}
type TempClientInfo struct {
ClientId string `json:"clientId"`
}
func WsMessageRegisterCallback(clientId string) []byte {
var errMsg = WsInfo{
Type: RegisterType,
Content: map[string]string{
"clientId": clientId,
},
From: "0",
To: clientId,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}
func WsErrorMessage(wsType WsType, clientId string, code e.ErrorCodeType, err error) []byte {
var ers string
if err != nil {
ers = err.Error()
}
var content = serializer.Response{
Code: code,
Err: ers,
Msg: code.String(),
}
var errMsg = WsInfo{
Type: wsType,
Content: content,
From: "0",
To: clientId,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}
func WsErrorPermissionDenied(wsType WsType, clientId string) []byte {
var content = serializer.Response{
Code: e.PermissionDenied,
Err: "Permission Denied",
Msg: "拒绝访问",
}
var errMsg = WsInfo{
Type: wsType,
Content: content,
From: "0",
To: clientId,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}
func WsErrorInvalidDataFormat(clientId string) []byte {
var content = serializer.Response{
Status: e.Failed,
Code: e.Failed,
Err: "Invalid Data Format",
Msg: "发送失败",
}
var errMsg = WsInfo{
Type: ErrorType,
Content: content,
From: "0",
To: clientId,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}
func WsErrorUnknownMessageType(clientId string) []byte {
var errMsg = WsInfo{
Type: ErrorType,
Content: "Unknown notice type",
From: "0",
To: clientId,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}
func WsErrorConnection(clientId string, err string, marks ...string) []byte {
mark := ""
if marks != nil {
mark = strings.Join(marks, ";")
}
var errMsg = WsInfo{
Type: ErrorType,
Content: "Connection error:" + err,
From: "0",
To: clientId,
Mark: mark,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}
func WsChatMessage(clientId string, targetClientId string, msg string) []byte {
var errMsg = WsInfo{
Type: ChatType,
Content: msg,
From: clientId,
To: targetClientId,
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}

315
pkg/common/ws/chatRoom.go Normal file
View File

@ -0,0 +1,315 @@
// Package ws -----------------------------
// @file : chatRoom.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/10/21 18:17:17
// -------------------------------------------
package ws
import (
"encoding/json"
"fmt"
"fonchain-fiee/pkg/utils"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"log"
"runtime"
"strconv"
"sync"
"time"
)
const (
// Time allowed to write a notice to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong notice from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum notice size allowed from peer.
maxMessageSize = 1024
)
func NewChatRoom() *ChatRoom {
var room = ChatRoom{
clientsRwLocker: &sync.RWMutex{},
clients: make(map[int64]map[string]*Client),
register: make(clientChan),
UnRegister: make(clientChan),
broadcast: make(broadcastChan),
}
go room.Run()
return &room
}
type broadcastMessage struct {
UserIds []int64
message []byte
}
type (
// []byte类型管道 用于客户端接收消息数据
messageChan chan []byte
//
wsConnChan chan *websocket.Conn
// Client类型数据管道
clientChan chan *Client
broadcastChan chan *broadcastMessage
)
type ChatRoom struct {
clientsRwLocker *sync.RWMutex
//clients 客户端信息存储
//// 支持多客户端连接 map[userId]map[clientId]*Client
clients map[int64]map[string]*Client
//会话 map[sessionId][]*Client
Session map[string][]*Client
//register register 注册管道
register clientChan
//unRegister 注销管道 接收需要注销的客户端
UnRegister clientChan
broadcast broadcastChan
}
func (o *ChatRoom) Run() {
//消息分发
for {
select {
// 注册事件
case newClient := <-o.register:
////删除临时map中的客户户端
//delete(o.tempClient, client.clientId)
o.clientsRwLocker.Lock()
//添加到客户端集合中
if o.clients[newClient.UserId] == nil {
o.clients[newClient.UserId] = make(map[string]*Client)
}
o.clients[newClient.UserId][newClient.ClientId] = newClient
//添加到会话集合中
if o.Session == nil {
o.Session = make(map[string][]*Client)
}
//if _, ok := o.Session[newClient.SessionId]; ok {
// for i, client := range o.Session[newClient.SessionId] {
// if client.ClientId == newClient.ClientId {
// //将之前的客户端注销
// o.UnRegister <- client
// }
// o.Session[newClient.SessionId][i] = newClient
// }
//}
if newClient.Waiter {
//客服人员没有默认会话窗口,而是自动加入所有用户的会话
for sessionId, _ := range o.Session {
sessionId := sessionId
if sessionId != newClient.SessionId {
for _, client := range o.clients[newClient.UserId] {
o.Session[sessionId] = append(o.Session[sessionId], client)
}
}
}
} else {
//画家添加会话的逻辑
_, ok := o.Session[newClient.SessionId]
if !ok {
o.Session[newClient.SessionId] = make([]*Client, 0)
//把客服拉入会话
for userId, clientInfo := range o.clients {
if userId == newClient.UserId {
continue
}
for i, client := range clientInfo {
if client != nil && client.Waiter {
//把客服人员客户端加入会话中
o.Session[newClient.SessionId] = append(o.Session[newClient.SessionId], clientInfo[i])
}
}
}
}
//再把自己的客户端加入会话
o.Session[newClient.SessionId] = append(o.Session[newClient.SessionId], newClient)
}
o.clientsRwLocker.Unlock()
//注销事件
case client := <-o.UnRegister:
//panic 恢复
defer func() {
if r := recover(); r != "" {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
log.Fatal("close webosocket connection occured panic , recovered!", zap.Any("client", client), zap.Error(err), zap.String("stack", string(buf)))
}
}()
fmt.Println("ws客户端注销事件触发")
//从客户端集合中删除
if _, ok := o.clients[client.UserId]; ok {
if client != nil && client.Conn != nil {
//_ = client.Conn.WriteMessage(websocket.CloseMessage, []byte{})
_ = client.Conn.Close()
}
o.clients[client.UserId][client.ClientId] = nil
delete(o.clients[client.UserId], client.ClientId)
fmt.Printf("ws客户端%s 被注销\n", client.ClientId)
}
// 消息群发事件
case messageInfo := <-o.broadcast:
o.Broadcast(messageInfo.message, messageInfo.UserIds...)
}
}
}
func (o *ChatRoom) Register(c *Client) (sessionId string) {
if c.SessionId == "" && !c.Waiter {
//这里的c经常拿不到sessionId所以使用userId固定死
//c.SessionId = fmt.Sprintf("%d-%d", c.UserId, time.Now().Unix())
c.SessionId = fmt.Sprintf("%d", c.UserId)
}
o.register <- c
return c.SessionId
}
// SendSessionMessage
// sendUserId: 发送消息的用户id消息提醒时此用户将会被排除
// sessionId 会话id
// msgType 消息类型
// message: 消息内容
func (o *ChatRoom) SendSessionMessage(sendUserId int64, sessionId string, msgType WsType, message any) (userIdInSession []int64, err error) {
o.clientsRwLocker.Lock()
defer o.clientsRwLocker.Unlock()
var msg = WsSessionInfo{
Type: msgType,
Content: message,
}
msgBytes, _ := json.Marshal(msg)
if o.Session[sessionId] == nil {
err = fmt.Errorf("该会话不存在或已失效")
return
}
fmt.Println("ChatRoom.SendSessionMessage - 1")
usableClients := []*Client{}
fmt.Printf("sessionId:[%s],客户端数量%d\n", sessionId, len(o.Session[sessionId]))
for i, client := range o.Session[sessionId] {
if client != nil {
_, exist := o.clients[client.UserId][client.ClientId]
if exist {
usableClients = append(usableClients, o.Session[sessionId][i])
}
}
fmt.Printf("client:%+v\n", client)
if client != nil && client.UserId != sendUserId {
client.Send <- msgBytes
userIdInSession = append(userIdInSession, client.UserId)
}
//client.Send <- msgBytes
}
o.Session[sessionId] = usableClients
fmt.Printf("sessionId:[%s],客户端数量%d\n", sessionId, len(o.Session[sessionId]))
fmt.Println("userIdInSession", userIdInSession)
return
}
func (o *ChatRoom) GetUserIdInSession(sessionId string, withoutUserId ...int64) (userIds []int64) {
fmt.Printf("sessionId:%s withoutUserId:%d\n", sessionId, withoutUserId)
//o.clientsRwLocker.RLock()
//defer o.clientsRwLocker.RUnlock()
fmt.Println("GetUserIdInSession 1")
if o.Session[sessionId] != nil {
fmt.Printf("GetUserIdInSession 2,o.Session[sessionId]:%+v", o.Session[sessionId])
for _, client := range o.Session[sessionId] {
fmt.Println("session one of userId", client.UserId)
var jump bool
if withoutUserId != nil {
for _, userId := range withoutUserId {
if client.UserId == userId {
jump = true
continue
}
}
}
if !jump {
fmt.Println("ADD USER", client.UserId)
userId := client.UserId
userIds = append(userIds, userId)
}
}
}
//针对app没有连接上websocket(聊天室没有检查到用户的客户端此时websocket无法发送通知)但是需要推送app通知给用户的情况进行优化
fmt.Println("GetUserIdInSession 3,userIds:", userIds)
if len(userIds) == 0 {
sessionUserId, _ := strconv.Atoi(sessionId)
add := true
if sessionUserId != 0 {
for _, v := range withoutUserId {
if v == int64(sessionUserId) {
add = false
break
}
}
}
if add {
userIds = append(userIds, int64(sessionUserId))
}
fmt.Println("GetUserIdInSession 4,userIds:", userIds)
}
userIds = utils.Unique(userIds)
fmt.Println("GetUserIdInSession 5,userIds:", userIds)
return
}
// func (o ChatRoom) RegisterClient(c *Client) {
// o.register <- c
// }
//
// func (o ChatRoom) DeleteClient(c *Client) {
// o.unRegister <- c
// }
func (o ChatRoom) Broadcast(message []byte, userIds ...int64) {
// 如果userIds为空则群发,否则找到这个用户的ws对象
if userIds == nil {
for _, userClients := range o.clients {
for _, cli := range userClients {
if cli == nil {
o.UnRegister <- cli
continue
}
go func() {
err := cli.Conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
o.UnRegister <- cli
}
}()
}
}
} else {
for _, userId := range userIds {
userClients, ok := o.clients[userId]
if ok == false {
return
}
for _, cli := range userClients {
if cli == nil {
o.UnRegister <- cli
continue
}
go func() {
err := cli.Conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
o.UnRegister <- cli
}
}()
}
}
}
}

111
pkg/common/ws/client.go Normal file
View File

@ -0,0 +1,111 @@
// Package ws -----------------------------
// @file : client.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/10/21 18:18:05
// -------------------------------------------
package ws
import (
"context"
"fmt"
"github.com/gorilla/websocket"
uuid "github.com/satori/go.uuid"
"go.uber.org/zap"
"log"
"strings"
"time"
)
var (
// 注册事件最大等待时间
limitRegisterWaitTime = time.Second * 6
limitReadTime = time.Second * 5
)
// NewClient 创建客户端实例
//
// param userId 用户id
// param uid 用户uuid
// param conn 客户端websocket连接对象
// return *Client
func NewClient(userId int64, uid string, conn *websocket.Conn, room *ChatRoom) *Client {
uidobj, _ := uuid.NewV4()
var v = &Client{
Room: room,
UserId: userId,
Uuid: uid,
ClientId: strings.Replace(uidobj.String(), "-", "", -1),
Conn: conn,
Send: make(chan []byte, 500),
}
return v
}
type Client struct {
Room *ChatRoom `json:"-" `
UserId int64 `json:"userId" ` //用户id
Uuid string `json:"uuid"` //画家uid
ClientId string `json:"clientId"` //为用户不同设备分配不同的客户端ID
Conn *websocket.Conn `json:"-"`
Send chan []byte
SessionId string `json:"sessionId"` //会话ID同一个用户多客户端登录会话ID相同
Waiter bool `json:"waiter"` //是否是客服
}
func (c *Client) Reading(ctx context.Context, handleFunc ...func(sourceData []byte, cli *Client)) {
defer func() {
c.Room.UnRegister <- c
ctx.Done()
return
}()
//c.Conn.SetReadLimit(maxMessageSize)
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
//接收到ping命令后更新读取时间
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
msgType, byteData, err := c.Conn.ReadMessage()
if msgType == -1 {
return
}
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Println("ws连接已关闭", zap.Error(err))
}
break
}
if handleFunc != nil {
handleFunc[0](byteData, c)
} else {
HandleMessage(byteData, c)
}
}
}
func (c *Client) WriteWait() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case msg, ok := <-c.Send:
fmt.Printf("发送消息:%+v\n", string(msg))
c.Conn.WriteMessage(websocket.TextMessage, msg)
fmt.Println("发送消息结束")
if !ok {
// 聊天室关闭了管道
c.Conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(writeWait))
return
}
case <-ticker.C:
if err := c.Conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(pongWait)); err != nil {
return
}
}
}
}

View File

@ -0,0 +1,21 @@
// Package utils -----------------------------
// @file : hertzWSUpgrade.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/6/28 14:19
// -------------------------------------------
package ws
import (
"github.com/gorilla/websocket"
"net/http"
)
var UpGrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// 检查请求的来源是否允许websocket连接可根据需求自行实现
return true
},
}

53
pkg/common/ws/readme.md Normal file
View File

@ -0,0 +1,53 @@
# wsscoket 对接说明
## 客户端对接测试页面
[{{服务端地址}}/ws/client](http://127.0.0.1:8088/ws/client)
## 客户端对接websocket流程
### websocket的连接
1. 客户端登录后获取uuid
2. 连接服务端websocket后在10s内发送一下格式的数据否则websocket连接将断开。
uuid请添加登录后获取的如果uuid不正确连接也会断开
```json
{
"type": "register",
"from": "",
"to": "0",
"content": {
"uuid":"用户的uuid"
}
}
```
注册成功后服务端将返回客户端临时id
```json
{"clientId":"02de5759-3f0a-47fa-a79f-afe61c39c5aa"}
```
### weboscket 数据发送测试
消息类型`type="test"`时,客户端将会把`content`内容原路返回,以此来测试最基本的通讯功能。
```json
{
"type": "test",
"from": "用户clientId",
"to": "0",
"content": {
"demo":"testdemo"
}
}
```
### websocket消息类型说明
#### 错误消息
在websocket通讯过程中服务端会对客户端发送过来的消息进行验证。
| type字段 | content字段 | 说明 |
|--------|----------------------|------------------------------|
| Error | Permission denied | 拒绝访问。 此报错一般出现在首次连接验证uuid的时候 |
| Error | Invalid data format | 无效的数据格式。消息内容未按照指定格式书写 |
| Error | Unknown message type | 未知的消息类型。接收到了未定义的type |
**错误消息示例:**
```json
{"type":"Error","content":"Permission denied","from":"0","to":"tempId"}
//{"type":"Error","content":"Invalid data format","from":"0","to":""}
//{"type":"Error","content":"Unknown notice type","from":"0","to":"0"}
```

View File

@ -0,0 +1,144 @@
// Package ws -----------------------------
// @file : handler.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/10/23 11:13:43
// -------------------------------------------
package ws
import (
"context"
"encoding/json"
"fonchain-fiee/api/account"
"fonchain-fiee/api/accountFiee"
"fonchain-fiee/pkg/config"
"fonchain-fiee/pkg/e"
"fonchain-fiee/pkg/service"
"fonchain-fiee/pkg/utils/secret"
)
func AuthorizationVerify(sourceData []byte) (userInfo *accountFiee.ChatUserData, ok bool, err error) {
var msg AuthorizationInfo
err = json.Unmarshal(sourceData, &msg)
if err != nil {
return
}
if msg.Type != AuthorizationType {
return
}
if msg.Content.Auth == "" {
return
}
var ctx = context.Background()
var accountInfo accountFiee.ChatUserData
switch msg.Content.Domain {
case config.Domain:
//fiee token校验
msg.Content.Auth, err = secret.GetJwtFromStr(msg.Content.Auth)
if err != nil {
return
}
var fieeJwtInfo *accountFiee.DecryptJwtResponse
fieeJwtInfo, err = service.AccountFieeProvider.DecryptJwt(ctx, &accountFiee.DecryptJwtRequest{Token: msg.Content.Auth, Domain: msg.Content.Domain})
if err != nil || fieeJwtInfo.IsOffline {
return
}
accountInfo.Origin = msg.Content.Domain
accountInfo.OriginId = int64(fieeJwtInfo.ID)
accountInfo.Account = fieeJwtInfo.Account
accountInfo.NickName = fieeJwtInfo.NickName
case "fontree":
//erp token校验
msg.Content.Auth, err = secret.GetJwtFromStr(msg.Content.Auth)
if err != nil {
return
}
var fontreeJwtInfo *account.DecryptJwtResponse
fontreeJwtInfo, err = service.AccountProvider.DecryptJwt(ctx, &account.DecryptJwtRequest{Token: msg.Content.Auth, Domain: msg.Content.Domain})
if err != nil || fontreeJwtInfo.IsOffline {
return
}
accountInfo.Origin = msg.Content.Domain
accountInfo.OriginId = int64(fontreeJwtInfo.ID)
accountInfo.Account = fontreeJwtInfo.Account
accountInfo.NickName = fontreeJwtInfo.NickName
}
//查询是否已经注册
var chatUserQuery *accountFiee.GetChatUserListResp
chatUserQuery, err = service.AccountFieeProvider.GetChatUserList(ctx, &accountFiee.GetChatUserListRequest{
Query: &accountFiee.ChatUserData{OriginId: int64(accountInfo.ID), Origin: msg.Content.Domain},
Page: 1,
PageSize: 1,
})
//如果找不到聊天用户则创建
if err != nil || chatUserQuery.Total > 0 {
//注册客服
createUserRes, err := service.AccountFieeProvider.CreateChatUser(ctx, &accountFiee.ChatUserData{
NickName: accountInfo.NickName,
Account: accountInfo.Account,
Role: 2,
Origin: msg.Content.Domain,
OriginId: int64(accountInfo.ID),
})
if err != nil {
return
}
userInfo = createUserRes.GetData()
}
ok = true
return
}
func HandleMessage(sourceData []byte, cli *Client) {
var msg WsInfo
err := json.Unmarshal(sourceData, &msg)
if err != nil {
cli.Send <- WsErrorInvalidDataFormat(msg.From)
return
}
switch msg.Type {
default:
cli.Send <- WsErrorUnknownMessageType(msg.From)
case TestType:
var newMsg = WsInfo{
Type: TestType,
Content: msg.Content,
From: "0",
To: msg.From,
}
byteMsg, _ := json.Marshal(newMsg)
cli.Send <- byteMsg
case ChatType:
if msg.From == "" {
//客户端id不能为空
cli.Send <- WsErrorMessage(ChatType, "null", e.ErrInvalidClientId, nil)
return
}
var chatInfo ChatInfo
_ = json.Unmarshal(sourceData, &chatInfo)
//解析Content
if clients, ok := cli.Room.clients[chatInfo.Content.TargetUserId]; ok {
for _, targetObj := range clients {
if targetObj != nil {
targetObj.Send <- WsChatMessage(msg.From, chatInfo.Content.TargetClientId, chatInfo.Content.Msg)
}
}
} else {
//对方不在线
cli.Send <- WsErrorMessage(ChatType, msg.From, e.ErrTargetOutLine, nil)
}
}
}
type ChatInfo struct {
Type WsType `json:"type"` //消息类型
Content ChatContent `json:"content"` //消息内容
From string `json:"from"` //发送者 0为服务端,客户端填写clientId
To string `json:"to"` //接收者 接收消息的用户id
}
type ChatContent struct {
TargetUuid string `json:"targetUuid"`
TargetUserId int64 `json:"targetUserId"`
TargetClientId string `json:"targetClientId"`
Msg string `json:"msg"`
}

127
pkg/common/ws/wsRoom.html Normal file
View File

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chat Example</title>
<script type="text/javascript">
window.onload = function () {
var conn;
var msg = document.getElementById("msg");
var log = document.getElementById("log");
function appendLog(item) {
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
//时间格式化
Date.prototype.Format = function (fmt) { // author: meizz
var o = {
"M+": this.getMonth() + 1, // 月份
"d+": this.getDate(), // 日
"h+": this.getHours(), // 小时
"m+": this.getMinutes(), // 分
"s+": this.getSeconds(), // 秒
"q+": Math.floor((this.getMonth() + 3) / 3), // 季度
"S": this.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
document.getElementById("form").onsubmit = function () {
if (!conn) {
return false;
}
if (!msg.value) {
return false;
}
conn.send(msg.value);
var item = document.createElement("div");
var now = new Date().Format("yyyy-MM-dd hh:mm:ss:S")
item.innerText = "客户端发送消息:\t"+now+"\n\t\t"+msg.value+"\n\n";
appendLog(item);
msg.value = "";
return false;
};
if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/ws");
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
conn.onmessage = function (evt) {
var messages = evt.data.split('\n');
var now = new Date().Format("yyyy-MM-dd hh:mm:ss:S")
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div");
item.innerText = "服务端回复消息:\t"+now+"\n\t\t"+messages[i]+"\n\n";
appendLog(item);
}
};
} else {
var item = document.createElement("div");
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
appendLog(item);
}
};
</script>
<style type="text/css">
html {
overflow: hidden;
}
body {
overflow: hidden;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: gray;
}
#log {
background: white;
margin: 0;
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 0.5em;
left: 0.5em;
right: 0.5em;
bottom: 3em;
overflow: auto;
}
#form {
padding: 0 0.5em 0 0.5em;
margin: 0;
position: absolute;
bottom: 1em;
left: 0px;
width: 100%;
overflow: hidden;
}
input{
height: 50px;
font-size: larger;
}
</style>
</head>
<body>
<div id="log"></div>
<form id="form">
<input type="submit" value="Send" />
<input type="text" id="msg" size="64" autofocus />
</form>
</body>
</html>

61
pkg/e/chatCode.go Normal file
View File

@ -0,0 +1,61 @@
// Package e -----------------------------
// @file : chatCode.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2025/6/12 16:57
// -------------------------------------------
package e
import "fmt"
type ErrorCodeType int
func (e ErrorCodeType) String() string {
return GetCodeMsg(e)
}
func (e ErrorCodeType) Error() string {
return GetCodeMsg(e)
}
func (e ErrorCodeType) Int() int {
return int(e)
}
func GetCodeMsg(e ErrorCodeType) string {
v, ok := msgFlags[e]
if !ok {
return fmt.Sprintf("未知错误:[%d]", e)
}
return v
}
var msgFlags = map[ErrorCodeType]string{
SUCCESS: "操作成功",
UpdatePasswordSuccess: "修改密码成功",
NotExistInentifier: "该第三方账号未绑定",
ERROR: "fail",
InvalidParams: "请求参数错误",
BindError: "参数绑定错误,类型不一致",
JsonUnmarshal: "Json解析错误",
ErrorDatabase: "数据库操作出错,请重试",
ErrorOss: "OSS配置错误",
InvalidToken: "Token验证失败",
ErrorUploadFile: "上传失败",
ErrorUploadVideoCover: "视频截取封面错误",
ErrorUploadValidParam: "上传参数非法",
ErrorFileReadErr: "读取文件错误",
ErrorFileNotExists: "文件不存在",
ErrorChunkNotGt: "分块数量不一致",
ErrorChunk: "读取分块错误",
ErrorUploadBos: "上传bos错误",
ErrorFileCreate: "文件创建错误",
ErrInvalidDataFormat: "无效的数据格式",
ErrInvalidClientId: "无效的客户端ID",
ErrRegisterFailed: "注册失败",
ErrUnRegistered: "未注册客户端",
PermissionDenied: "拒绝访问",
ErrChatSendErr: "消息发送失败",
}

View File

@ -139,6 +139,15 @@ const (
ERROR_Text_Irregularity = 90018
ERROR_Text_Length = 90019
ERROR_NoPermission = 90020
//聊天室
ErrInvalidDataFormat = 80100 //无效的数据格式
ErrInvalidClientId = 80101 //无效的客户端id
ErrRegisterFailed = 80102 //注册失败
ErrUnRegistered = 80103 //未注册
PermissionDenied = 80104 //拒绝访问
ErrChatSendErr = 80105 //聊天记录发送失败
ErrTargetOutLine = 80106 //目标离线
)
const (
Push = 1

View File

@ -146,6 +146,8 @@ var MsgFlags = map[int]string{
ERROR_Text_Irregularity: "文字内容不合规",
ERROR_Text_Length: "文本长度超出限制",
ERROR_NoPermission: "您暂无权限,请联系客服",
ErrInvalidClientId: "无效的客户端ID",
}
const (

View File

@ -4,6 +4,7 @@ import (
"fonchain-fiee/pkg/middleware"
"fonchain-fiee/pkg/service"
"fonchain-fiee/pkg/service/account"
"fonchain-fiee/pkg/service/asChat"
"fonchain-fiee/pkg/service/auth"
"fonchain-fiee/pkg/service/lang"
"fonchain-fiee/pkg/service/qr"
@ -22,7 +23,10 @@ import (
func NewRouter() *gin.Engine {
//使用默认gin路由
r := gin.Default()
wsGroup := r.Group("api/fiee")
wsGroup.Use(
middleware.GinRecovery(true),
)
r.Use(gzip.Gzip(gzip.BestSpeed)) // 中间件占用绝大部分内存
//加入日志中间件,跨域中间件
r.Use(middleware.NewLogger(), middleware.Cors(), middleware.GinRecovery(true))
@ -107,6 +111,18 @@ func NewRouter() *gin.Engine {
redirectRoute.POST("sdk/down/v2", auth.DownImgV2)
redirectRoute.POST("sdk/down/v3", auth.DownImgV3)
}
//========================================================================================
// 客户聊天
{
// websocket数据接收
wsGroup.GET("aschat/ws", asChat.ChatHandlerIns.Connection)
v1.POST("aschat/message/new", asChat.ChatHandlerIns.NewMessage)
v1.POST("aschat/media/upload", asChat.ChatHandlerIns.Upload)
v1.POST("aschat/message/list", asChat.ChatHandlerIns.MessageList)
v1.POST("aschat/user/stat", asChat.ChatHandlerIns.UserMessageStat)
v1.POST("aschat/voicetotext", asChat.ChatHandlerIns.VoiceToText)
v1.POST("aschat/artistDetail", asChat.ChatHandlerIns.ArtistDetail)
}
//静态文件
r.StaticFS("/api/static", http.Dir("./runtime"))

View File

@ -1,17 +1,19 @@
package serializer
import "fonchain-fiee/pkg/e"
// Response 基础序列化器
type Response struct {
Status int `json:"status"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
Code int `json:"code"`
Error error `json:"error"`
Err string `json:"err"`
Keys []string `json:"keys"`
Mark string `json:"mark,omitempty"`
Page *PageInfo `json:"page,omitempty"`
Positions interface{} `json:"positions"`
Status int `json:"status"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
Code e.ErrorCodeType `json:"code"`
Error error `json:"error"`
Err string `json:"err"`
Keys []string `json:"keys"`
Mark string `json:"mark,omitempty"`
Page *PageInfo `json:"page,omitempty"`
Positions interface{} `json:"positions"`
}
type PageInfo struct {
Page int32 `json:"page" query:"page"`

241
pkg/service/asChat/cache.go Normal file
View File

@ -0,0 +1,241 @@
// Package asChat -----------------------------
// @file : cache.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2024/9/11 下午5:18
// -------------------------------------------
package asChat
import (
"context"
"errors"
"fmt"
"fonchain-fiee/api/accountFiee"
"fonchain-fiee/pkg/cache"
"github.com/go-redis/redis"
"github.com/goccy/go-json"
"go.uber.org/zap"
"log"
"strings"
"sync"
"time"
)
const CacheChatRecordKey = "chatRecord"
const CacheSessionKey = "chatSession"
const CacheNewMsgStatKey = "newMsgStat"
var chatCacheLocker sync.RWMutex
type ChatCache struct {
newMessageStatExpireAfter time.Duration //消息统计的数据过期时间
}
// ------------------------------存储用户的会话ID--------------------------------
func (cr ChatCache) GetUserSessionCacheKey(userId int64) string {
return fmt.Sprintf("%s:%d", CacheSessionKey, userId)
}
func (cr ChatCache) SaveUserSession(userId int64, sessionId string) {
chatCacheLocker.Lock()
defer chatCacheLocker.Unlock()
////var c = context.Background()
err := cache.RedisClient.Set(cr.GetUserSessionCacheKey(userId), sessionId, 0).Err()
if err != nil {
log.Fatal("保存用户会话失败", zap.Error(err))
}
}
func (cr ChatCache) GetUserSession(userId int64) (sessionId string) {
fmt.Println("GetUserSession-1")
chatCacheLocker.RLock()
defer chatCacheLocker.RUnlock()
//var c = context.Background()
sessionId, err := cache.RedisClient.Get(cr.GetUserSessionCacheKey(userId)).Result()
fmt.Println("GetUserSession-2")
if err != nil {
if err.Error() == "redis: nil" {
err = nil
} else {
log.Fatal("获取用户会话失败", zap.Error(err))
}
}
fmt.Println("GetUserSession-3, sessionId:", sessionId)
return
}
// ------------------------------存储会话的聊天记录--------------------------------
func (cr ChatCache) GetChatRecordCacheKey(sessionId string) string {
return fmt.Sprintf("%s:%s", CacheChatRecordKey, sessionId)
}
func (cr ChatCache) AddChatRecord(sessionId string, data ...*accountFiee.ChatRecordData) (err error) {
////var c = context.Background()
messages := cr.GetChatRecord(sessionId)
fmt.Printf("AddChatRecord add data:%+v\n", data)
messages = append(messages, data...)
cacheBytes, _ := json.Marshal(messages)
fmt.Println("Marshal result", string(cacheBytes))
err = cache.RedisClient.Set(cr.GetChatRecordCacheKey(sessionId), cacheBytes, 2*time.Hour).Err()
return
}
func (cr ChatCache) CoverChatRecord(sessionId string, data []*accountFiee.ChatRecordData) (err error) {
chatCacheLocker.Lock()
defer chatCacheLocker.Unlock()
//var c = context.Background()
cacheBytes, _ := json.Marshal(data)
err = cache.RedisClient.Set(cr.GetChatRecordCacheKey(sessionId), cacheBytes, 2*time.Hour).Err()
return
}
func (cr ChatCache) GetChatRecord(sessionId string) (data []*accountFiee.ChatRecordData) {
chatCacheLocker.RLock()
defer chatCacheLocker.RUnlock()
data = make([]*accountFiee.ChatRecordData, 0)
//var c = context.Background()
messages, err := cache.RedisClient.Get(cr.GetChatRecordCacheKey(sessionId)).Bytes()
if err != nil {
if err.Error() == "redis: nil" {
err = nil
}
//log.Fatal("获取聊天记录失败", zap.Error(err))
return
}
fmt.Printf("cache data: %+v", string(messages))
if len(messages) > 0 {
_ = json.Unmarshal(messages, &data)
}
return
}
// ------------------------------存储新消息统计--------------------------------
func (cr ChatCache) GetNewMsgStatCacheKey(ownerId int64) string {
return fmt.Sprintf("%s:%d", CacheNewMsgStatKey, ownerId)
}
// 消息数量自增
func (cr ChatCache) IncreaseNewMessageTotal(ownerId int64, sessionId string) (err error) {
chatCacheLocker.Lock()
defer chatCacheLocker.Unlock()
ctx := context.Background()
data := cr.GetNewMessageStat(ctx, ownerId)
if len(data) > 0 {
foundIndex := -1
for i, v := range data {
if v.SessionId == sessionId {
foundIndex = i
break
}
}
if foundIndex > -1 {
data[foundIndex].Total += 1
}
//将foundIndex之后的所有元素右移动一位
if foundIndex > 0 {
elementToMove := data[foundIndex]
copy(data[1:], data[0:foundIndex])
data[0] = elementToMove
} else if foundIndex == -1 {
data = append([]UserMsgStatic{{SessionId: sessionId, Total: 1}}, data...)
}
} else {
data = []UserMsgStatic{{SessionId: sessionId, Total: 1}}
}
return cr.coverOwnerNewMessageStat(ctx, ownerId, data)
}
// 重置新消息数量
func (cr ChatCache) ResetNewMessageTotal(ownerId int64, sessionId string, total ...int64) error {
chatCacheLocker.Lock()
defer chatCacheLocker.Unlock()
var tl int64
if len(total) > 0 {
tl = total[0]
}
ctx := context.Background()
data := cr.GetNewMessageStat(ctx, ownerId)
found := false
for i, v := range data {
if v.SessionId == sessionId {
found = true
data[i].Total = tl
break
}
}
if !found {
data = append(data, UserMsgStatic{
SessionId: sessionId,
Total: tl,
})
}
return cr.coverOwnerNewMessageStat(ctx, ownerId, data)
}
func (cr ChatCache) RecountNewMessageTotal(ownerId int64) {
//var c = context.Background()
var keys []string
var err error
keys, err = cache.RedisClient.Keys(CacheChatRecordKey + "*").Result()
if err != nil {
log.Fatal("获取聊天记录所有缓存KEY失败", zap.Error(err))
return
}
var countMap = make(map[string]int)
for _, key := range keys {
var messages []byte
var data []*accountFiee.ChatRecordData
messages, err = cache.RedisClient.Get(key).Bytes()
if err != nil {
if err.Error() == "redis: nil" {
err = nil
}
log.Fatal("获取聊天记录失败", zap.Error(err))
data = make([]*accountFiee.ChatRecordData, 0)
continue
}
if len(messages) > 0 {
_ = json.Unmarshal(messages, &data)
}
var sessionId = strings.Split(key, ":")[1]
countMap[sessionId] = 0
for _, v := range data {
if v.WaiterRead == 2 { //统计未读消息数量
countMap[sessionId]++
}
}
}
for sessionId, count := range countMap {
err = cr.ResetNewMessageTotal(ownerId, sessionId, int64(count))
if err != nil {
log.Fatal("重置新消息数量统计",
zap.String("function", "RecountNewMessageTotal"),
zap.Int64("ownerId", ownerId),
zap.String("sessionId", sessionId),
zap.Int("count", count),
zap.Error(err),
)
}
}
return
}
// erp获取最新的消息统计
func (cr ChatCache) GetNewMessageStat(ctx context.Context, ownerId int64) (result []UserMsgStatic) {
//chatCacheLocker.RLock()
//defer chatCacheLocker.RUnlock()
result = make([]UserMsgStatic, 0)
vals, err := cache.RedisClient.Get(cr.GetNewMsgStatCacheKey(ownerId)).Bytes()
if err != nil && errors.Is(err, redis.Nil) {
log.Fatal("从缓存获取新消息统计失败", zap.Error(err), zap.Int64("ownerId", ownerId))
return
}
if vals != nil {
_ = json.Unmarshal(vals, &result)
}
return
}
// 覆盖指定erp用户的新消息统计
func (cr ChatCache) coverOwnerNewMessageStat(ctx context.Context, ownerId int64, data []UserMsgStatic) (err error) {
value, _ := json.Marshal(data)
//err = cache.RedisClient.Set(ctx, cr.GetNewMsgStatCacheKey(ownerId), value, cr.newMessageStatExpireAfter).Err()
err = cache.RedisClient.Set(cr.GetNewMsgStatCacheKey(ownerId), value, 0).Err()
return
}

View File

@ -0,0 +1,33 @@
// package asChat -----------------------------
// @file : chatRoom.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/10/21 18:17:17
// -------------------------------------------
package asChat
import (
"encoding/json"
"fonchain-fiee/pkg/common/ws"
)
var (
ChatRoom = ws.NewChatRoom()
)
type WsInfo struct {
Type ws.WsType `json:"type"` //消息类型
Content any `json:"content"`
}
func WsMessageRegisterCallback(clientId string, sessionId string) []byte {
var errMsg = WsInfo{
Type: ws.RegisterType,
Content: map[string]string{
//"clientId": clientId,
"sessionId": sessionId,
},
}
byteMsg, _ := json.Marshal(errMsg)
return byteMsg
}

130
pkg/service/asChat/dto.go Normal file
View File

@ -0,0 +1,130 @@
// Package asChat -----------------------------
// @file : dto.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2024/9/10 下午6:28
// -------------------------------------------
package asChat
import (
"encoding/json"
"fonchain-fiee/api/accountFiee"
"time"
)
type Message struct {
MsgType accountFiee.MsgType `json:"msgType"`
Text string `json:"text"` //文本内容
Media []MessageMedia `json:"media"`
LocalStamp int64 `json:"localStamp"`
}
type MessageMedia struct {
MediaId int64 `json:"mediaId"` //媒体文件id
MediaSize string `json:"mediaSize"` //媒体文件大小
Ext string `json:"ext"` //后缀格式
Url string `json:"url"` //文件地址
ConvText string `json:"convText"` //语音转文字内容,需要调用语音转文字接口后才会有值
Duration int64 `json:"duration"` //时长 单位:毫秒
}
// 客户端发送消息请求使用api发送消息
type NewMessageRequest struct {
Waiter bool `json:"waiter"` //是否是客服发送,客服没有userId
SessionId string `json:"sessionId"`
Message
}
// 服务端接收到消息后使用websocket发送给userId关联的客户端通知客户端有新消息然后调用接口获取消息
type NewMessageNotice struct {
Name string `json:"name"` //名字
UserId int64 `json:"userId"` //用户id
SessionId string `json:"sessionId"`
MessageId int64 `json:"messageId"` //消息id
//NewMsgTotal int64 `json:"newMsgTotal"` //新消息数量
//Active bool `json:"active"` //是否在线
}
// 获取会话列表
type SessionType struct {
NewMessageNotice
RecentMessage []*Message `json:"recentMessage"` //最近消息
}
type MessageListType struct {
ID int64 `json:"ID"`
CreatedAt string `json:"createdAt"`
UserId int64 `json:"userId"`
Name string `json:"name"`
Message Message `json:"message"`
}
func (m *MessageListType) BuildMessage(data *accountFiee.ChatRecordData) {
m.ID = data.ID
m.CreatedAt = data.CreatedAt
m.UserId = data.UserId
m.Name = data.Name
switch data.MsgType {
case accountFiee.MsgType_TextMsgType:
m.Message = Message{
MsgType: accountFiee.MsgType_TextMsgType,
Text: data.Content,
Media: []MessageMedia{},
LocalStamp: data.LocalStamp,
}
case accountFiee.MsgType_ImageMsgType, accountFiee.MsgType_AudioMsgType, accountFiee.MsgType_VideoMsgType:
m.Message.MsgType = data.MsgType
m.Message.Text = data.Content
m.Message.LocalStamp = data.LocalStamp
if data.Medias != nil {
for _, media := range data.Medias {
m.Message.Media = append(m.Message.Media, MessageMedia{
MediaId: media.ID,
MediaSize: media.Size,
Ext: media.Ext,
Url: media.Url,
ConvText: media.ConvText,
Duration: media.Duration,
})
}
}
}
}
func (m *MessageListType) ToJson() string {
jsonBytes, _ := json.Marshal(m)
return string(jsonBytes)
}
type UserMsgStatic struct {
UserId int64 `json:"userId"` //用户id
Name string `json:"name"` //名称
ArtistUid string `json:"artistUid"`
SessionId string `json:"sessionId"` //会话id
Total int64 `json:"total"` //新消息数量
//NewMessageTime string `json:"newMessageTime"` //最新消息的创建时间
}
type MessageListRequest struct {
SessionId string `json:"sessionId"` //不传则获取自己的会话消息里列表
CurrentId int64 `json:"currentId"` //组合查询条件1基于某个消息id向前或向后查找。两种组合条件不能同时使用
Direction int `json:"direction"` //组合查询条件1方向 1=向前查找 2=向后查找
Recent bool `json:"recent"` //组合查询条件2查找最新的若干条消息。两种组合条件不能同时使用
InHour time.Duration `json:"inHour"` //组合查询条件2可选查询指定小时内的数据
PageSize int64 `json:"pageSize"` //查找数量
}
type VoiceToTextRequest struct {
MediaId int64 `json:"mediaId"`
}
type ArtistInfoRequest struct {
UserId int64 `json:"userId"`
}
type ArtistInfo struct {
Tnum string `json:"tnum"`
ArtistName string `json:"artistName"`
Age int64 `json:"age"`
Sex string `json:"sex"`
NativePlace string `json:"nativePlace"`
TelNum string `json:"telNum"`
RecentPhoto string `json:"recentPhoto"`
}

View File

@ -0,0 +1,577 @@
// package asChat -----------------------------
// @file : handler.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2022/10/23 11:13:43
// -------------------------------------------
package asChat
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"fonchain-fiee/api/account"
"fonchain-fiee/api/accountFiee"
"fonchain-fiee/pkg/common/ws"
"fonchain-fiee/pkg/e"
"fonchain-fiee/pkg/logic"
"fonchain-fiee/pkg/service"
"fonchain-fiee/pkg/service/upload"
"fonchain-fiee/pkg/utils"
"github.com/fonchain/utils/voice"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
uuid "github.com/satori/go.uuid"
"go.uber.org/zap"
"io"
"log"
"path"
"slices"
"sort"
"strconv"
"strings"
"time"
)
var ChatHandlerIns = ChatHandler{
cache: ChatCache{newMessageStatExpireAfter: 10 * time.Minute},
}
type ChatHandler struct {
cache ChatCache
}
func (cr ChatHandler) Connection(c *gin.Context) {
conn, err := ws.UpGrader.Upgrade(c.Writer, c.Request, nil)
conn.SetReadDeadline(time.Now().Add(time.Second * 10))
if err != nil {
log.Fatal("无法升级为websocket连接", zap.Error(err))
c.String(500, "无法转为websocket连接")
return
}
defer func() {
if conn != nil {
conn.Close()
}
}()
_, byteData, err := conn.ReadMessage()
if err != nil {
_ = conn.WriteMessage(websocket.TextMessage, ws.WsErrorConnection("null", err.Error(), "conn.ReadMessag1"))
return
}
fmt.Println("22222222222222,AuthorizationVerify")
var ok bool
var userInfo *accountFiee.ChatUserData
userInfo, ok, err = ws.AuthorizationVerify(byteData)
if err != nil {
_ = conn.WriteMessage(websocket.TextMessage, ws.WsErrorConnection("null", err.Error(), "AuthorizationVerify2"))
return
}
if !ok {
_ = conn.WriteMessage(websocket.TextMessage, ws.WsErrorConnection("null", "登录状态失效", "AuthorizationVerify2.1"))
return
}
fmt.Println("33333333333333,RecountNewMessageTotal")
conn.SetReadDeadline(time.Time{})
go cr.cache.RecountNewMessageTotal(userInfo.ID)
fmt.Println("44444444444444,ws.NewClient")
//注册ws客户端并发送clientId给ws客户端
var cli = ws.NewClient(userInfo.ID, "", conn, ChatRoom)
cli.Waiter = userInfo.Role == 2
fmt.Println("55555555555555,GetUserSession")
//查询是否有历史的sessionId
cli.SessionId = cr.cache.GetUserSession(userInfo.ID)
ChatRoom.Register(cli)
cr.cache.SaveUserSession(userInfo.ID, cli.SessionId)
fmt.Println("66666666666666666666666666")
go cli.WriteWait()
cli.Send <- WsMessageRegisterCallback(cli.ClientId, cli.SessionId)
fmt.Println("777777777777777777777777")
// 处理websocket连接的逻辑
ctx, _ := context.WithCancel(context.Background())
cli.Reading(ctx, HandleMessage)
fmt.Println("88888888888888888888888888")
select {
case <-ctx.Done():
return
}
}
func (cr ChatHandler) NewMessage(c *gin.Context) {
var request NewMessageRequest
if err := c.ShouldBindJSON(&request); err != nil {
service.ErrorMsgI18n(c, e.InvalidParams, err.Error(), e.I18N_INVALID_PARAM)
return
}
if request.SessionId == "" {
service.ErrorMsgI18n(c, e.InvalidParams, "sessionId不能为空", e.I18N_INVALID_PARAM)
return
}
if request.MsgType == 0 {
service.ErrorMsgI18n(c, e.InvalidParams, "msgType不能为空", e.I18N_INVALID_PARAM)
return
}
fmt.Println("NewMessage 1111111111111111111111111111111")
//获取用户信息
tokenResult := asAccount.GetUserInfoWithTokenV2(c)
if tokenResult.Err != nil {
service.ErrorMsgI18n(c, e.NotLogin, tokenResult.Err.Error(), e.I18N_NOTLOGIN)
return
}
fmt.Println("NewMessage 22222222222222222222222222222222222")
//存储入库
var userName = "未知"
if request.Waiter {
accountDetail, err := service.AccountProvider.Info(c, &account.InfoRequest{ID: uint64(tokenResult.UserInfo.MgmtAccId), Domain: config.Domain})
trace.AddTrace(c, "AccountProvider.Info", err)
if err != nil {
service.ErrorMsgI18n(c, e.Failed, err.Error(), e.I18N_USER_FOUND_FAILED)
return
}
userName = accountDetail.Info.NickName
} else {
if tokenResult.UserInfo.RealNameInfo != nil {
userName = tokenResult.UserInfo.RealNameInfo.Name
}
}
fmt.Println("NewMessage 3333333333333333333333333333333333")
var data = accountFiee.ChatRecordData{
SessionId: request.SessionId,
UserId: tokenResult.UserInfo.ID,
Name: userName,
Avatar: "",
MsgType: request.MsgType,
Content: request.Message.Text,
LocalStamp: request.LocalStamp,
Medias: nil,
}
if len(request.Message.Media) > 0 {
for _, media := range request.Message.Media {
data.Medias = append(data.Medias, &accountFiee.ChatMediaData{
ID: media.MediaId,
})
}
}
fmt.Println("NewMessage 4444444444444444444444444444444444")
resp, err := service.AccountFieeProvider.CreateChatRecord(c, &data)
if err != nil {
service.Error(c, errors.New("创建失败"))
return
}
fmt.Printf("CreateChatRecord resp:%+v\n", resp)
//录入缓存
err = cr.cache.AddChatRecord(request.SessionId, resp.Data)
if err != nil {
service.Error(c, errors.New("创建失败"))
return
}
fmt.Println("NewMessage 5 消息数量+1")
//新消息数量统计+1
noticeUserId := ChatRoom.GetUserIdInSession(request.SessionId, tokenResult.UserInfo.ID)
fmt.Println("NewMessage 5.1 消息数量配置结束")
fmt.Printf("noticeUserId %+v\n", noticeUserId)
for _, userId := range noticeUserId {
fmt.Println("userId")
cr.cache.IncreaseNewMessageTotal(userId, request.SessionId)
}
fmt.Println("NewMessage 6")
//发送websocket消息提醒通知
var notice = MessageListType{}
notice.BuildMessage(resp.Data)
fmt.Printf("ws消息提醒:%+v\n", notice)
_, err = ChatRoom.SendSessionMessage(tokenResult.UserInfo.ID, request.SessionId, ws.NewChatMsgType, notice)
if err != nil {
log.Fatal("发送新消息通知失败", zap.Error(err), zap.Any("notice", notice))
}
fmt.Println("NewMessage 7 -end")
//发送app推送(无横幅推送)
go func() {
omitMessage := ""
switch request.MsgType {
case accountFiee.MsgType_TextMsgType:
runMsg := []rune(request.Text)
if len(runMsg) > 15 {
omitMessage = string(runMsg[:15]) + "..."
} else {
omitMessage = request.Text
}
case accountFiee.MsgType_ImageMsgType:
omitMessage = "[图片]"
case accountFiee.MsgType_AudioMsgType:
omitMessage = "[音频]"
case accountFiee.MsgType_VideoMsgType:
omitMessage = "[视频]"
default:
omitMessage = "新消息请查收"
}
for _, userId := range noticeUserId {
_ = asPusher.NewArtistinfoUniPush().NewChatMessageNotice(userId, omitMessage)
}
}()
service.Success(c)
}
func (cr ChatHandler) MessageList(c *gin.Context) {
var request MessageListRequest
if err := c.ShouldBindJSON(&request); err != nil {
service.Error(c, err)
return
}
domain := c.GetHeader("domain")
if (request.Direction == 0 && request.Recent == false) || (request.Direction > 0 && request.Recent == true) {
service.Error(c, errors.New("组合条件校验失败"))
return
}
if request.SessionId == "" {
service.Error(c, errors.New("sessionId不能为空"))
return
}
if request.PageSize < -1 {
service.Error(c, errors.New("pageSize校验错误"))
return
}
var resp = make([]*MessageListType, 0)
if request.CurrentId == 0 && request.Direction == 1 {
service.Success(c, resp)
return
}
tokenResult := asAccount.GetUserInfoWithTokenV2(c)
if tokenResult.Err != nil {
service.ErrorWeb(c, e.NotLogin, tokenResult.Err.Error())
return
}
//if request.SessionId == "" {
// request.SessionId = cr.cache.GetUserSession(tokenResult.UserInfo.ID)
// if request.SessionId == "" {
// service.Success(c, resp)
// return
// }
//}
//messages := cr.cache.GetChatRecord(request.SessionId)
messages := []*accountFiee.ChatRecordData{}
var returnDataIdList = make([]int64, 0)
defer func() {
//获取最新数据时,重置新消息数量统计
if request.Direction == 2 || request.Recent {
cr.cache.ResetNewMessageTotal(tokenResult.UserInfo.ID, request.SessionId)
}
//设置消息已被客服阅读,当客服重新通过通过websocket连接时这些消息将不被纳入新消息数量统计
if len(returnDataIdList) > 0 && domain == "fontree" {
for _, hasReadId := range returnDataIdList {
for i, message := range messages {
if message.ID == hasReadId {
messages[i].WaiterRead = 1
}
}
}
err := cr.cache.CoverChatRecord(request.SessionId, messages)
if err != nil {
log.Fatal("设置消息已读失败", zap.Error(err))
}
for _, v := range messages {
_, err = service.AccountFieeProvider.SaveChatRecord(context.Background(), v)
if err != nil {
log.Fatal("设置消息已读失败", zap.Error(err))
}
}
}
}()
if len(messages) == 0 {
//从数据库获取
recordResp, err := service.AccountFieeProvider.GetChatRecordList(c, &accountFiee.GetChatRecordListRequest{
Query: &accountFiee.ChatRecordData{SessionId: request.SessionId},
Page: 1,
PageSize: -1,
//Where: fmt.Sprintf("id %s %d", utils.IfGec(request.Direction == 1, "<", ">"), request.CurrentId),
})
if err != nil {
service.Error(c, err)
return
}
messages = recordResp.List
err = cr.cache.CoverChatRecord(request.SessionId, messages)
if err != nil {
log.Fatal("覆盖聊天记录失败", zap.Error(err))
}
}
if request.Recent {
if int64(len(messages)) >= request.PageSize {
messages = messages[len(messages)-int(request.PageSize):]
}
var now = time.Now()
for _, message := range messages {
if request.InHour > 0 {
messageCreatedAt, _ := stime.StringToTime(message.CreatedAt)
if now.Sub(*messageCreatedAt) >= request.InHour*time.Hour {
continue
}
}
returnDataIdList = append(returnDataIdList, message.ID)
var msg = &MessageListType{}
msg.BuildMessage(message)
resp = append(resp, msg)
}
} else {
sort.Slice(messages, func(i, j int) bool {
if request.Direction == 1 {
return messages[i].ID < messages[j].ID
} else {
return messages[i].ID > messages[j].ID
}
})
fmt.Printf("data is %+v\n", messages)
total := 0
for i, message := range messages {
switch request.Direction {
case 1: //向下查找找比CurrentId大的数据
if message.ID <= request.CurrentId {
continue
}
case 2: //向上查找找比CurrentId小的数据
if message.ID >= request.CurrentId {
continue
}
}
message := message
fmt.Println(i, message.ID)
if request.PageSize != -1 && int64(total+1) > request.PageSize {
continue
}
total++
returnDataIdList = append(returnDataIdList, message.ID)
var msg = &MessageListType{}
msg.BuildMessage(message)
resp = append(resp, msg)
}
}
//二次排序
sort.Slice(resp, func(i, j int) bool {
return resp[i].ID < resp[j].ID
})
//优化空列表
for i, v := range resp {
if v.Message.Media == nil {
resp[i].Message.Media = []MessageMedia{}
}
}
service.Success(c, resp)
}
func (cr ChatHandler) Upload(c *gin.Context) {
fmt.Println("111111111111")
//获取用户信息
accInfo := asAccount.GetUserInfoWithTokenV2(c)
if accInfo.Err != nil {
service.Error(c, accInfo.Err.Error())
return
}
//获取文件对象
file, err := c.FormFile("file")
if err != nil {
log.Fatal("ERROR: upload file failed. ", zap.Error(err))
return
}
duration := c.PostForm("duration")
fmt.Println(duration)
ext := c.PostForm("ext")
fileExt := strings.ToLower(path.Ext(file.Filename))
if ext != "" {
fileExt = ext
}
fileType := e.DetectFileTypeByExtension(fileExt)
if fileType == e.Audio {
if !slices.Contains([]string{".mp4", ".aac", ".mp3", ".opus", ".wav"}, fileExt) {
service.Error(c, errors.New("不支持的格式"))
return
}
}
//计算md5
tmp, err := file.Open()
if err != nil {
service.Error(c, errors.New("上传失败"))
return
}
fileContent, err := io.ReadAll(tmp)
if err != nil {
service.Error(c, errors.New("文件读取失败"))
return
}
hash := md5.New()
_, err = hash.Write(fileContent)
if err != nil {
service.Error(c, errors.New("文件读取失败"))
return
}
md5Bytes := hash.Sum(nil) // 获取 MD5 字节切片
md5String := hex.EncodeToString(md5Bytes) // 转换为十六进制字符串表示
//检查文件是否存在
checkResp, err := service.AccountFieeProvider.GetChatMediaList(c, &accountFiee.GetChatMediaListRequest{Query: &accountFiee.ChatMediaData{Md5: md5String}, Page: 1, PageSize: 1})
if err != nil {
log.Fatal("md5查询附件失败", zap.Error(err))
}
if checkResp.Total > 0 {
service.Success(c, checkResp.List[0])
return
}
//文件不存在则上传文件
filename, _ := uuid.NewV4()
defer tmp.Close()
fileBuffer := bytes.NewBuffer(fileContent)
var bosUrl string
bosUrl, err = upload.UploadWithBuffer(fileBuffer, fmt.Sprintf("%d/%v%v", accInfo.UserInfo.MgmtAccId, filename, fileExt))
if err != nil {
service.Error(c, err)
return
}
//存到数据库
var durationInt64, _ = strconv.ParseInt(duration, 10, 64)
var mediaData = accountFiee.ChatMediaData{
Url: bosUrl,
Md5: md5String,
Size: fmt.Sprintf("%d", file.Size),
Ext: fileExt,
Duration: durationInt64,
}
resp, err := service.AccountFieeProvider.CreateChatMedia(c, &mediaData)
if err != nil {
service.Error(c, err)
return
}
service.Success(c, resp.Data)
}
func (cr ChatHandler) UserMessageStat(c *gin.Context) {
//获取用户信息
tokenResult := asAccount.GetUserInfoWithTokenV2(c)
if tokenResult.Err != nil {
service.ErrorMsgI18n(c, e.NotLogin, tokenResult.Err.Error(), e.I18N_NOTLOGIN)
return
}
result := cr.cache.GetNewMessageStat(c, tokenResult.UserInfo.ID)
if len(result) == 0 {
service.Success(c, result)
return
}
fmt.Printf("cache stat:%+v\n", result)
//获取实名信息
var protoReq = accountFiee.GetChatUserListRequest{
Page: 1,
PageSize: int64(len(result)),
}
for i, item := range result {
if item.UserId == 0 {
sessionId, _ := strconv.Atoi(item.SessionId)
item.UserId = int64(sessionId)
result[i].UserId = int64(sessionId)
}
protoReq.UserIdIn = append(protoReq.UserIdIn, item.UserId)
}
fmt.Printf("protoReq.UserIdIn:%+v\n", protoReq.UserIdIn)
listRes, err := service.AccountFieeProvider.GetChatUserList(c, &protoReq)
if err != nil {
service.Error(c, err)
return
}
fmt.Printf("GetChatUserList:%+v\n", listRes)
for i, item := range result {
for _, user := range listRes.List {
if item.UserId == user.UserId {
user := user
result[i].Name = user.Name
result[i].ArtistUid = user.ArtistUid
break
}
}
if result[i].Name == "" {
result[i].Name = beautifulZeroName(result[i].Name, result[i].UserId)
}
}
reverse(result)
service.Success(c, result)
}
func reverse(slice []UserMsgStatic) {
for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
slice[i], slice[j] = slice[j], slice[i]
}
}
func (cr ChatHandler) VoiceToText(c *gin.Context) {
var req VoiceToTextRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
detail, err := service.AccountFieeProvider.GetChatMediaDetail(c, &accountFiee.GetChatMediaByIdRequest{Id: req.MediaId})
if err != nil {
service.Error(c, err)
return
}
if detail.ConvText != "" {
service.Success(c, map[string]string{"convText": detail.ConvText})
return
}
voiceApi := voice.NewVoiceApi()
detail.ConvText, err = voiceApi.ToTextFromUrl(detail.Url)
if err != nil {
service.Error(c, errors.New("语音转文字失败"))
return
}
defer func() {
service.AccountFieeProvider.UpdateChatMedia(context.Background(), detail)
}()
service.Success(c, map[string]string{"convText": detail.ConvText})
}
func (cr ChatHandler) ArtistDetail(c *gin.Context) {
var req ArtistInfoRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
if req.UserId == 0 {
service.Success(c, ArtistInfo{})
return
}
detail, err := service.GrpcArtistInfoUserImpl.FindUsersUserView(c, &artistInfoUser.FindUsersRequest{UserId: req.UserId})
if err != nil {
service.Error(c, err)
return
}
var (
tnum string
artistName string
age int64
sex string
nativePlace string
telNum string
recentPhoto string
)
if len(detail.Data) > 0 {
tnum = detail.Data[0].Tnum
artistName = beautifulZeroName(detail.Data[0].RealName, req.UserId)
age = detail.Data[0].Age
sex = detail.Data[0].Sex
nativePlace = detail.Data[0].NativePlace
telNum = detail.Data[0].TelNum
recentPhoto = detail.Data[0].Photo
}
resp := ArtistInfo{
Tnum: tnum,
ArtistName: artistName,
Age: age,
Sex: sex,
NativePlace: nativePlace,
TelNum: telNum,
RecentPhoto: recentPhoto,
}
service.Success(c, resp)
}
// 对没有名字的name进行优化
func beautifulZeroName(name string, userId int64) string {
return utils.IfGec(name == "", fmt.Sprintf("未实名用户:%d", userId), name)
}

View File

@ -0,0 +1,26 @@
# asChat 客服聊天
## 聊天室主要流程与功能描述
1. 用户通过画家包登录
2. 打开客服页面。 画家宝客户端自动进行websocket连接后台会自动创建一个默认聊天室聊天室携带一个SessionId
3. 用户调用api接口发送消息。 服务端接收到消息后会通过websocket通知聊天室里面所有用户。
4. erp首次打开客服菜单时会进行websocket连接并调用一次api接口刷新消息列表。后续通过websocket接收消息推送收到消息时应主动调用一次消息列表刷新接口。
5. erp客服端发送消息时加入到此聊天室。
6. 用户端调用api接口获取新消息列表。
## 客户端应具备的其它功能
1. weboscket断开自动重连
2. 当通过websocket接收到错误类型的消息应具备对应的错误处理机制<p>
错误消息示例
```json
{"type":1,"content":"Connection error:登录状态失效","from":"0","to":"null"}
```
## 服务端应具备的功能
1. 通过redis缓存聊天消息
2. 通过redis缓存用户的sessionId避免ws断开后找不到之前的sessionId
3. 客服端由于不是画家宝用户没有userId。在websocket连接时如果找不到userId应该为其在画家宝创建一个账号。且经纪人不可见。
4. 由于没有创建聊天室的需求,所以每个用户使用一个聊天室即可。客服与之对话时,就自动加入用户端的聊天室
5. 新消息统计
- 当发送消息时,该聊天室中除了发信者以外,其它用户的新消息数都+1录入缓存。
- 当新客服人员加入时,没有新消息统计的缓存。~~他的新消息数量应该从创建时间开始计算~~,所以都是0。

View File

@ -0,0 +1,46 @@
// Package asChat -----------------------------
// @file : service.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2024/9/10 下午7:05
// -------------------------------------------
package asChat
import (
"encoding/json"
"fonchain-fiee/pkg/common/ws"
)
func HandleMessage(sourceData []byte, cli *ws.Client) {
var msg map[string]any
err := json.Unmarshal(sourceData, &msg)
if err != nil {
cli.Send <- ws.WsErrorInvalidDataFormat(cli.ClientId)
return
}
switch msg["type"] {
default:
cli.Send <- ws.WsErrorUnknownMessageType(cli.ClientId)
case ws.TestType:
var newMsg = ws.WsInfo{
Type: ws.TestType,
Content: msg["content"],
}
byteMsg, _ := json.Marshal(newMsg)
cli.Send <- byteMsg
//case ws.ChatType:
// var chatInfo ChatInfo
// _ = json.Unmarshal(sourceData, &chatInfo)
// //解析Content
// if clients, ok := cli.Room.clients[chatInfo.Content.TargetUserId]; ok {
// for _, targetObj := range clients {
// if targetObj != nil {
// targetObj.Send <- WsChatMessage(msg.From, chatInfo.Content.TargetClientId, chatInfo.Content.Msg)
// }
// }
// } else {
// //对方不在线
// cli.Send <- WsErrorMessage(ChatType, msg.From, e.ErrTargetOutLine, nil)
// }
}
}

View File

@ -329,3 +329,18 @@ func GetSnapshot(videoPath, snapshotPath string, frameNum int) (snapshotName str
snapshotName = names[len(names)-1] + "." + PngType
return
}
func UploadWithBuffer(fileBuffer *bytes.Buffer, cloudStoreSubPath string) (url string, err error) {
Client, err := objstorage.NewOSS(config.ConfigData.Oss.AccessKeyId, config.ConfigData.Oss.AccessKeySecret, config.ConfigData.Oss.Endpoint)
if err != nil {
err = errors.New(fmt.Sprintf("云存储初始化失败:%s", err.Error()))
return
}
cloudStoreSubPath = getEnvDir(cloudStoreSubPath)
_, err = Client.PutObjectFromBytes(config.ConfigData.Oss.BucketName, cloudStoreSubPath, fileBuffer.Bytes())
return
}
func getEnvDir(cloudStoreSubPath string) (ep string) {
ep, _ = url.JoinPath("fiee", cloudStoreSubPath)
return ep
}

47
pkg/utils/if.go Normal file
View File

@ -0,0 +1,47 @@
/*
* @FileName: if.go
* @Author: JJXu
* @CreateTime: 2022/3/31 下午10:34
* @Description:
*/
package utils
import "strings"
func If(condition bool, trueVal, falseVal interface{}) interface{} {
if condition {
return trueVal
}
return falseVal
}
func IfGec[T ~string | ~int | ~int32 | ~int64 | ~bool | ~float32 | ~float64](condition bool, trueVal, falseVal T) T {
if condition {
return trueVal
}
return falseVal
}
// IsValueInList 值是否在列表中
// value:查询的值
// list: 列表
// disableStrictCase: 禁用严格大小写检查。默认是严格大小写
func IsValueInList(value string, list []string, disableStrictCase ...bool) bool {
var disStrictCase bool
if disableStrictCase != nil {
disStrictCase = disableStrictCase[0]
}
for _, v := range list {
var listValue string
if disStrictCase {
listValue = strings.ToLower(v)
value = strings.ToLower(v)
} else {
listValue = v
}
if listValue == value {
return true
}
}
return false
}

19
pkg/utils/unqiue.go Normal file
View File

@ -0,0 +1,19 @@
// Package utils -----------------------------
// @file : unqiue.go
// @author : JJXu
// @contact : wavingbear@163.com
// @time : 2024/9/12 下午5:03
// -------------------------------------------
package utils
func Unique[T int | int8 | int32 | int64 | string](slice []T) []T {
seen := make(map[T]bool) // 创建一个 map 来跟踪已经看到的元素
unique := make([]T, 0) // 创建一个新的切片来存储唯一的元素
for _, v := range slice {
if _, ok := seen[v]; !ok {
seen[v] = true // 标记元素为已见
unique = append(unique, v) // 将元素添加到唯一元素切片中
}
}
return unique
}