xy 2 days ago
parent
commit
bd7313c538

+ 38 - 0
bin/client_msg/common.proto

@@ -390,3 +390,41 @@ message ResFriendOnLineStatus{
   UserInfo info = 3;
 }
 
+
+//聊天消息
+message ChatMessage{
+  int32 id = 1;
+  string session_id = 2;
+  string sender_id = 3;
+  string content = 4;
+  string created_at = 5;
+  string content_type = 6;
+}
+
+//发送聊天内容
+message ReqSendChatMsg{
+  string content_type = 1; //消息类型
+  string to_userid = 2; //发送给谁
+  string content = 3; //消息内容
+}
+
+message ResSendChatMsg{
+  bool success = 1;
+  MsgError err_msg = 2;
+}
+//接收聊天内容
+message RecvChatMsg{
+  ChatMessage info = 1;
+}
+//请求获取聊天历史
+message ReqChatHistory{
+  string to_userid = 1;
+  int32 last_msgid = 2; 
+}
+//响应获取聊天历史
+message ResChatHistory{
+  bool success = 1;
+  MsgError err_msg = 2;
+  repeated ChatMessage list = 3;
+}
+

+ 180 - 0
src/server/datacenter/chat_db/chat.go

@@ -0,0 +1,180 @@
+package chat_db
+
+import (
+	"encoding/json"
+	"fmt"
+	"hash/fnv"
+	"net/http"
+	mysqlmgr "server/db/mysql"
+	"server/msg"
+	"strings"
+	"time"
+	"unicode/utf8"
+
+	"github.com/godruoyi/go-snowflake"
+)
+
+type ChatMessage struct {
+	MessageID   uint64    `gorm:"column:message_id;type:bigint unsigned;primaryKey" json:"message_id"` // 雪花ID
+	SessionID   string    `gorm:"column:session_id;type:varchar(36);not null" json:"session_id"`
+	SenderID    string    `gorm:"column:sender_id;type:varchar(36);not null" json:"sender_id"`
+	ContentType string    `gorm:"column:content_type;type:enum('text','image','video','file');not null" json:"content_type"`
+	Content     string    `gorm:"column:content;type:text;not null" json:"content"`
+	CreatedAt   time.Time `gorm:"column:created_at;type:datetime(6);default:CURRENT_TIMESTAMP(6);not null" json:"created_at"`
+	Status      string    `gorm:"column:status;type:enum('sending','sent','delivered','read')" json:"status"`
+}
+
+// 根据session_id哈希选择分表
+func GetMessageTable(sessionID string) string {
+	hash := fnv.New32a()
+	hash.Write([]byte(sessionID))
+	return fmt.Sprintf("chat_messages_%02d", hash.Sum32()%64)
+}
+
+// 内容类型检测
+func detectContentType(content []byte) string {
+	contentType := http.DetectContentType(content)
+	switch {
+	case strings.HasPrefix(contentType, "image/"):
+		return "image"
+	case strings.HasPrefix(contentType, "video/"):
+		return "video"
+	case strings.HasPrefix(contentType, "application/pdf"),
+		strings.HasPrefix(contentType, "application/octet-stream"):
+		return "file"
+	case json.Valid(content):
+		return "text" // JSON is treated as text in your system
+	case utf8.Valid(content):
+		return "text"
+	default:
+		return "file" // fallback to 'file' for unknown binary data
+	}
+}
+
+func SendMessage(senderID, sessionID string, content []byte) (*msg.ChatMessage, error) {
+	// 分布式ID生成(含时间戳)
+	msgID := snowflake.ID()
+
+	// 获取分表名
+	tableName := GetMessageTable(sessionID)
+
+	// 事务操作
+	tx, _ := mysqlmgr.Begin()
+	defer tx.Rollback()
+
+	// 1. 写入消息
+	_, err := tx.Exec(fmt.Sprintf(`
+        INSERT INTO %s 
+        (message_id, session_id, sender_id, content, content_type, status)
+        VALUES (?, ?, ?, ?, ?, 'sent')`,
+		tableName),
+		msgID, sessionID, senderID, content, detectContentType(content),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// 2. 更新会话最后消息
+	tx.Exec(`
+        UPDATE chat_sessions 
+        SET last_message_id = ?
+        WHERE session_id = ?`,
+		msgID, sessionID,
+	)
+
+	// 3. 更新未读计数(排除发送者自己)
+	tx.Exec(`
+        UPDATE session_members 
+        SET unread_count = unread_count + 1
+        WHERE session_id = ? AND user_id != ?`,
+		sessionID, senderID,
+	)
+
+	tx.Commit()
+
+	return &msg.ChatMessage{
+		Id:        int32(msgID),
+		SessionId: sessionID,
+		SenderId:  senderID,
+		Content:   string(content),
+	}, nil
+}
+
+func GetHistoryMessages(sessionID string, lastMsgID int32, limit int) ([]*msg.ChatMessage, error) {
+	tableName := GetMessageTable(sessionID)
+
+	var query string
+	var args []interface{}
+
+	if lastMsgID <= 0 {
+		// 第一次加载,查询最新的消息
+		query = fmt.Sprintf(`
+            SELECT message_id, sender_id, content, created_at, content_type
+            FROM %s
+            WHERE session_id = ?
+            ORDER BY message_id DESC
+            LIMIT ?`, tableName)
+		args = []interface{}{sessionID, limit}
+	} else {
+		// 后续加载,查询比 lastMsgID 更旧的消息
+		query = fmt.Sprintf(`
+            SELECT message_id, sender_id, content, created_at, content_type
+            FROM %s
+            WHERE session_id = ? AND message_id < ?
+            ORDER BY message_id DESC
+            LIMIT ?`, tableName)
+		args = []interface{}{sessionID, lastMsgID, limit}
+	}
+
+	rows, err := mysqlmgr.Query(query, args...)
+	if err != nil {
+		return nil, fmt.Errorf("GetHistoryMessages失败: %v", err)
+	}
+	defer rows.Close()
+
+	var chatList []*msg.ChatMessage
+	for rows.Next() {
+		var chatItem ChatMessage
+		err := rows.Scan(
+			&chatItem.MessageID,
+			&chatItem.SenderID,
+			&chatItem.Content,
+			&chatItem.CreatedAt,
+			&chatItem.ContentType,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("解析聊天数据失败: %v", err)
+		}
+
+		chatList = append(chatList, &msg.ChatMessage{
+			Id:          int32(chatItem.MessageID),
+			SessionId:   chatItem.SessionID,
+			SenderId:    chatItem.SenderID,
+			Content:     chatItem.Content,
+			CreatedAt:   chatItem.CreatedAt.String(),
+			ContentType: chatItem.ContentType,
+		})
+	}
+
+	return chatList, nil
+}
+func MarkMessagesRead(userID, sessionID string, lastReadMsgID int64) error {
+	// 1. 更新已读状态
+	_, err := mysqlmgr.Insert(`
+        INSERT INTO message_read_status (message_id, user_id)
+        SELECT message_id, ? FROM chat_messages
+        WHERE session_id = ? AND message_id <= ?
+        ON DUPLICATE KEY UPDATE read_at = NOW(6)`,
+		userID, sessionID, lastReadMsgID,
+	)
+
+	// 2. 清零未读计数
+	_, err = mysqlmgr.Update(`
+        UPDATE session_members
+        SET unread_count = 0
+        WHERE user_id = ? AND session_id = ?`,
+		userID, sessionID,
+	)
+
+	return err
+}

+ 4 - 0
src/server/db/mysql/mysqlmgr.go

@@ -69,6 +69,10 @@ func Query(query string, args ...interface{}) (*sql.Rows, error) {
 	return db.Query(query, args...)
 }
 
+func Begin() (*sql.Tx, error) {
+	return db.Begin()
+}
+
 // 测试函数
 func test() {
 	// 创建用户表

+ 7 - 1
src/server/gate/router.go

@@ -13,15 +13,21 @@ func init() {
 
 	msg.Processor.SetRouter(&msg.BuyShopItem{}, hall.ChanRPC)
 	msg.Processor.SetRouter(&msg.EnterHall{}, hall.ChanRPC)
+
+	//好友
 	msg.Processor.SetRouter(&msg.ReqFriendList{}, hall.ChanRPC)
 	msg.Processor.SetRouter(&msg.ReqAddFriend{}, hall.ChanRPC)
-
 	msg.Processor.SetRouter(&msg.RecvAddFriendRequest{}, hall.ChanRPC)
 	msg.Processor.SetRouter(&msg.OptAddFriendRequest{}, hall.ChanRPC)
 	msg.Processor.SetRouter(&msg.SearchUser{}, hall.ChanRPC)
 	msg.Processor.SetRouter(&msg.ReqFriendRequestList{}, hall.ChanRPC)
 	msg.Processor.SetRouter(&msg.ReqDeleteFriend{}, hall.ChanRPC)
 
+	//聊天
+	msg.Processor.SetRouter(&msg.ReqSendChatMsg{}, hall.ChanRPC)
+	msg.Processor.SetRouter(&msg.ReqChatHistory{}, hall.ChanRPC)
+
+	//游戏
 	msg.Processor.SetRouter(&msg.ReqHeartBeat{}, game.ChanRPC)
 	msg.Processor.SetRouter(&msg.SendColorSz{}, game.ChanRPC)
 	msg.Processor.SetRouter(&msg.SendRoleMove{}, game.ChanRPC)

+ 2 - 0
src/server/go.mod

@@ -12,6 +12,7 @@ require (
 
 require (
 	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/bwmarrin/snowflake v0.3.0 // indirect
 	github.com/bytedance/sonic v1.13.3 // indirect
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
@@ -24,6 +25,7 @@ require (
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.26.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/godruoyi/go-snowflake v0.0.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.3 // indirect

+ 4 - 0
src/server/go.sum

@@ -1,5 +1,7 @@
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
+github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
 github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
 github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -29,6 +31,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/godruoyi/go-snowflake v0.0.2 h1:rN9imTkrUJ5ZjuwTOi7kTGQFEZSUI3pwPMzAb7uitk4=
+github.com/godruoyi/go-snowflake v0.0.2/go.mod h1:6JXMZzmleLpSK9pYpg4LXTcAz54mdYXTeXUvVks17+4=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=

+ 66 - 0
src/server/hall/chat/chat.go

@@ -0,0 +1,66 @@
+package chat
+
+import (
+	"fmt"
+	"server/datacenter/chat_db"
+	agentmanager "server/game/agentManager"
+	"server/msg"
+
+	"github.com/name5566/leaf/gate"
+)
+
+func SendChatMsg(args []interface{}) {
+	m := args[0].(*msg.ReqSendChatMsg)
+	a := args[1].(gate.Agent)
+	FromUserInfo := a.UserData().(*msg.UserInfo)
+	FromUserID := FromUserInfo.UserId
+	SessionID := generatePrivateSessionID(m.ToUserid, FromUserID)
+	chatMsg, err := chat_db.SendMessage(m.ToUserid, SessionID, []byte(m.Content))
+	if err != nil {
+		a.WriteMsg(&msg.ResSendChatMsg{
+			Success: false,
+			ErrMsg: &msg.MsgError{
+				ErrorCode: 101,
+				ErrorMsg:  err.Error(),
+			},
+		})
+
+	} else {
+		ToAgent := agentmanager.GetAgentByUserID(m.ToUserid)
+		if ToAgent != nil {
+			ToAgent.WriteMsg(&msg.RecvChatMsg{Info: chatMsg})
+		}
+	}
+
+}
+
+func GetChatHistory(args []interface{}) {
+	m := args[0].(*msg.ReqChatHistory)
+	a := args[1].(gate.Agent)
+	FromUserInfo := a.UserData().(*msg.UserInfo)
+	FromUserID := FromUserInfo.UserId
+	SessionID := generatePrivateSessionID(m.ToUserid, FromUserID)
+	chat_list, err := chat_db.GetHistoryMessages(SessionID, m.LastMsgid, 50)
+	if err != nil {
+		a.WriteMsg(&msg.ResChatHistory{
+			Success: false,
+			ErrMsg: &msg.MsgError{
+				ErrorCode: 101,
+				ErrorMsg:  err.Error(),
+			},
+		})
+	} else {
+		a.WriteMsg(&msg.ResChatHistory{
+			Success: true,
+			List:    chat_list,
+		})
+	}
+}
+
+// 生成私聊会话ID(保证user1和user2顺序无关)
+func generatePrivateSessionID(user1, user2 string) string {
+	if user1 > user2 {
+		return fmt.Sprintf("private_%s_%s", user2, user1)
+	}
+	return fmt.Sprintf("private_%s_%s", user1, user2)
+}

+ 20 - 0
src/server/hall/friends/friends.go

@@ -56,6 +56,26 @@ func GetFriendsListByUserId(args []interface{}) {
 }
 
 func DeleteFriend(args []interface{}) {
+	m := args[0].(*msg.ReqDeleteFriend)
+	a := args[1].(gate.Agent)
+	FromUserInfo := a.UserData().(*msg.UserInfo)
+	ToUserID := m.ToUserID
+
+	user_data, err := usercenter.GetUserByID(FromUserInfo.UserId)
+	if err == nil {
+		user_data.RemoveFriend(ToUserID)
+		a.WriteMsg(&msg.ResDeleteFriend{
+			Success: true,
+		})
+	} else {
+		a.WriteMsg(&msg.ResDeleteFriend{
+			Success: false,
+			ErrMsg: &msg.MsgError{
+				ErrorCode: 101,
+				ErrorMsg:  err.Error(),
+			},
+		})
+	}
 
 }
 

+ 5 - 0
src/server/hall/internal/handler.go

@@ -3,6 +3,7 @@ package internal
 import (
 	"reflect"
 	redismgr "server/db/redis"
+	"server/hall/chat"
 	"server/hall/friends"
 	"server/hall/shop"
 	"server/user"
@@ -30,6 +31,10 @@ func init() {
 	HandleMsg(&msg.SearchUser{}, friends.SearchUser)
 	HandleMsg(&msg.ReqFriendRequestList{}, friends.GetFriendRquestList)
 	HandleMsg(&msg.ReqDeleteFriend{}, friends.DeleteFriend)
+
+	HandleMsg(&msg.ReqSendChatMsg{}, chat.SendChatMsg)
+	HandleMsg(&msg.ReqChatHistory{}, chat.GetChatHistory)
+
 }
 
 func enterHall(args []interface{}) {

+ 401 - 8
src/server/msg/common.pb.go

@@ -3479,6 +3479,363 @@ func (x *ResFriendOnLineStatus) GetInfo() *UserInfo {
 	return nil
 }
 
+// 聊天消息
+type ChatMessage struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Id            int32                  `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+	SessionId     string                 `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+	SenderId      string                 `protobuf:"bytes,3,opt,name=sender_id,json=senderId,proto3" json:"sender_id,omitempty"`
+	Content       string                 `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+	CreatedAt     string                 `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+	ContentType   string                 `protobuf:"bytes,6,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ChatMessage) Reset() {
+	*x = ChatMessage{}
+	mi := &file_common_proto_msgTypes[53]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ChatMessage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChatMessage) ProtoMessage() {}
+
+func (x *ChatMessage) ProtoReflect() protoreflect.Message {
+	mi := &file_common_proto_msgTypes[53]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ChatMessage.ProtoReflect.Descriptor instead.
+func (*ChatMessage) Descriptor() ([]byte, []int) {
+	return file_common_proto_rawDescGZIP(), []int{53}
+}
+
+func (x *ChatMessage) GetId() int32 {
+	if x != nil {
+		return x.Id
+	}
+	return 0
+}
+
+func (x *ChatMessage) GetSessionId() string {
+	if x != nil {
+		return x.SessionId
+	}
+	return ""
+}
+
+func (x *ChatMessage) GetSenderId() string {
+	if x != nil {
+		return x.SenderId
+	}
+	return ""
+}
+
+func (x *ChatMessage) GetContent() string {
+	if x != nil {
+		return x.Content
+	}
+	return ""
+}
+
+func (x *ChatMessage) GetCreatedAt() string {
+	if x != nil {
+		return x.CreatedAt
+	}
+	return ""
+}
+
+func (x *ChatMessage) GetContentType() string {
+	if x != nil {
+		return x.ContentType
+	}
+	return ""
+}
+
+// 发送聊天内容
+type ReqSendChatMsg struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ContentType   string                 `protobuf:"bytes,1,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` //消息类型
+	ToUserid      string                 `protobuf:"bytes,2,opt,name=to_userid,json=toUserid,proto3" json:"to_userid,omitempty"`          //发送给谁
+	Content       string                 `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`                            //消息内容
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ReqSendChatMsg) Reset() {
+	*x = ReqSendChatMsg{}
+	mi := &file_common_proto_msgTypes[54]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ReqSendChatMsg) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ReqSendChatMsg) ProtoMessage() {}
+
+func (x *ReqSendChatMsg) ProtoReflect() protoreflect.Message {
+	mi := &file_common_proto_msgTypes[54]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ReqSendChatMsg.ProtoReflect.Descriptor instead.
+func (*ReqSendChatMsg) Descriptor() ([]byte, []int) {
+	return file_common_proto_rawDescGZIP(), []int{54}
+}
+
+func (x *ReqSendChatMsg) GetContentType() string {
+	if x != nil {
+		return x.ContentType
+	}
+	return ""
+}
+
+func (x *ReqSendChatMsg) GetToUserid() string {
+	if x != nil {
+		return x.ToUserid
+	}
+	return ""
+}
+
+func (x *ReqSendChatMsg) GetContent() string {
+	if x != nil {
+		return x.Content
+	}
+	return ""
+}
+
+type ResSendChatMsg struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Success       bool                   `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+	ErrMsg        *MsgError              `protobuf:"bytes,2,opt,name=err_msg,json=errMsg,proto3" json:"err_msg,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ResSendChatMsg) Reset() {
+	*x = ResSendChatMsg{}
+	mi := &file_common_proto_msgTypes[55]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ResSendChatMsg) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResSendChatMsg) ProtoMessage() {}
+
+func (x *ResSendChatMsg) ProtoReflect() protoreflect.Message {
+	mi := &file_common_proto_msgTypes[55]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResSendChatMsg.ProtoReflect.Descriptor instead.
+func (*ResSendChatMsg) Descriptor() ([]byte, []int) {
+	return file_common_proto_rawDescGZIP(), []int{55}
+}
+
+func (x *ResSendChatMsg) GetSuccess() bool {
+	if x != nil {
+		return x.Success
+	}
+	return false
+}
+
+func (x *ResSendChatMsg) GetErrMsg() *MsgError {
+	if x != nil {
+		return x.ErrMsg
+	}
+	return nil
+}
+
+// 接收聊天内容
+type RecvChatMsg struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Info          *ChatMessage           `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *RecvChatMsg) Reset() {
+	*x = RecvChatMsg{}
+	mi := &file_common_proto_msgTypes[56]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *RecvChatMsg) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RecvChatMsg) ProtoMessage() {}
+
+func (x *RecvChatMsg) ProtoReflect() protoreflect.Message {
+	mi := &file_common_proto_msgTypes[56]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RecvChatMsg.ProtoReflect.Descriptor instead.
+func (*RecvChatMsg) Descriptor() ([]byte, []int) {
+	return file_common_proto_rawDescGZIP(), []int{56}
+}
+
+func (x *RecvChatMsg) GetInfo() *ChatMessage {
+	if x != nil {
+		return x.Info
+	}
+	return nil
+}
+
+// 请求获取聊天历史
+type ReqChatHistory struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	ToUserid      string                 `protobuf:"bytes,1,opt,name=to_userid,json=toUserid,proto3" json:"to_userid,omitempty"`
+	LastMsgid     int32                  `protobuf:"varint,2,opt,name=last_msgid,json=lastMsgid,proto3" json:"last_msgid,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ReqChatHistory) Reset() {
+	*x = ReqChatHistory{}
+	mi := &file_common_proto_msgTypes[57]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ReqChatHistory) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ReqChatHistory) ProtoMessage() {}
+
+func (x *ReqChatHistory) ProtoReflect() protoreflect.Message {
+	mi := &file_common_proto_msgTypes[57]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ReqChatHistory.ProtoReflect.Descriptor instead.
+func (*ReqChatHistory) Descriptor() ([]byte, []int) {
+	return file_common_proto_rawDescGZIP(), []int{57}
+}
+
+func (x *ReqChatHistory) GetToUserid() string {
+	if x != nil {
+		return x.ToUserid
+	}
+	return ""
+}
+
+func (x *ReqChatHistory) GetLastMsgid() int32 {
+	if x != nil {
+		return x.LastMsgid
+	}
+	return 0
+}
+
+// 响应获取聊天历史
+type ResChatHistory struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	Success       bool                   `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+	ErrMsg        *MsgError              `protobuf:"bytes,2,opt,name=err_msg,json=errMsg,proto3" json:"err_msg,omitempty"`
+	List          []*ChatMessage         `protobuf:"bytes,3,rep,name=list,proto3" json:"list,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *ResChatHistory) Reset() {
+	*x = ResChatHistory{}
+	mi := &file_common_proto_msgTypes[58]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *ResChatHistory) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResChatHistory) ProtoMessage() {}
+
+func (x *ResChatHistory) ProtoReflect() protoreflect.Message {
+	mi := &file_common_proto_msgTypes[58]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResChatHistory.ProtoReflect.Descriptor instead.
+func (*ResChatHistory) Descriptor() ([]byte, []int) {
+	return file_common_proto_rawDescGZIP(), []int{58}
+}
+
+func (x *ResChatHistory) GetSuccess() bool {
+	if x != nil {
+		return x.Success
+	}
+	return false
+}
+
+func (x *ResChatHistory) GetErrMsg() *MsgError {
+	if x != nil {
+		return x.ErrMsg
+	}
+	return nil
+}
+
+func (x *ResChatHistory) GetList() []*ChatMessage {
+	if x != nil {
+		return x.List
+	}
+	return nil
+}
+
 var File_common_proto protoreflect.FileDescriptor
 
 const file_common_proto_rawDesc = "" +
@@ -3702,7 +4059,33 @@ const file_common_proto_rawDesc = "" +
 	"\x15ResFriendOnLineStatus\x12\x18\n" +
 	"\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" +
 	"\aerr_msg\x18\x02 \x01(\v2\t.MsgErrorR\x06errMsg\x12\x1d\n" +
-	"\x04info\x18\x03 \x01(\v2\t.UserInfoR\x04info*K\n" +
+	"\x04info\x18\x03 \x01(\v2\t.UserInfoR\x04info\"\xb5\x01\n" +
+	"\vChatMessage\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x05R\x02id\x12\x1d\n" +
+	"\n" +
+	"session_id\x18\x02 \x01(\tR\tsessionId\x12\x1b\n" +
+	"\tsender_id\x18\x03 \x01(\tR\bsenderId\x12\x18\n" +
+	"\acontent\x18\x04 \x01(\tR\acontent\x12\x1d\n" +
+	"\n" +
+	"created_at\x18\x05 \x01(\tR\tcreatedAt\x12!\n" +
+	"\fcontent_type\x18\x06 \x01(\tR\vcontentType\"j\n" +
+	"\x0eReqSendChatMsg\x12!\n" +
+	"\fcontent_type\x18\x01 \x01(\tR\vcontentType\x12\x1b\n" +
+	"\tto_userid\x18\x02 \x01(\tR\btoUserid\x12\x18\n" +
+	"\acontent\x18\x03 \x01(\tR\acontent\"N\n" +
+	"\x0eResSendChatMsg\x12\x18\n" +
+	"\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" +
+	"\aerr_msg\x18\x02 \x01(\v2\t.MsgErrorR\x06errMsg\"/\n" +
+	"\vRecvChatMsg\x12 \n" +
+	"\x04info\x18\x01 \x01(\v2\f.ChatMessageR\x04info\"L\n" +
+	"\x0eReqChatHistory\x12\x1b\n" +
+	"\tto_userid\x18\x01 \x01(\tR\btoUserid\x12\x1d\n" +
+	"\n" +
+	"last_msgid\x18\x02 \x01(\x05R\tlastMsgid\"p\n" +
+	"\x0eResChatHistory\x12\x18\n" +
+	"\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" +
+	"\aerr_msg\x18\x02 \x01(\v2\t.MsgErrorR\x06errMsg\x12 \n" +
+	"\x04list\x18\x03 \x03(\v2\f.ChatMessageR\x04list*K\n" +
 	"\broleType\x12\x15\n" +
 	"\x11ROLE_TYPE_UNKNOWN\x10\x00\x12\a\n" +
 	"\x03RED\x10\x01\x12\b\n" +
@@ -3753,7 +4136,7 @@ func file_common_proto_rawDescGZIP() []byte {
 }
 
 var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 7)
-var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 53)
+var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 59)
 var file_common_proto_goTypes = []any{
 	(RoleType)(0),                 // 0: roleType
 	(OptType)(0),                  // 1: OptType
@@ -3815,6 +4198,12 @@ var file_common_proto_goTypes = []any{
 	(*ReqDeleteFriend)(nil),       // 57: ReqDeleteFriend
 	(*ResDeleteFriend)(nil),       // 58: ResDeleteFriend
 	(*ResFriendOnLineStatus)(nil), // 59: ResFriendOnLineStatus
+	(*ChatMessage)(nil),           // 60: ChatMessage
+	(*ReqSendChatMsg)(nil),        // 61: ReqSendChatMsg
+	(*ResSendChatMsg)(nil),        // 62: ResSendChatMsg
+	(*RecvChatMsg)(nil),           // 63: RecvChatMsg
+	(*ReqChatHistory)(nil),        // 64: ReqChatHistory
+	(*ResChatHistory)(nil),        // 65: ResChatHistory
 }
 var file_common_proto_depIdxs = []int32{
 	0,  // 0: round.m_color:type_name -> roleType
@@ -3880,11 +4269,15 @@ var file_common_proto_depIdxs = []int32{
 	31, // 60: ResDeleteFriend.err_msg:type_name -> MsgError
 	31, // 61: ResFriendOnLineStatus.err_msg:type_name -> MsgError
 	21, // 62: ResFriendOnLineStatus.info:type_name -> UserInfo
-	63, // [63:63] is the sub-list for method output_type
-	63, // [63:63] is the sub-list for method input_type
-	63, // [63:63] is the sub-list for extension type_name
-	63, // [63:63] is the sub-list for extension extendee
-	0,  // [0:63] is the sub-list for field type_name
+	31, // 63: ResSendChatMsg.err_msg:type_name -> MsgError
+	60, // 64: RecvChatMsg.info:type_name -> ChatMessage
+	31, // 65: ResChatHistory.err_msg:type_name -> MsgError
+	60, // 66: ResChatHistory.list:type_name -> ChatMessage
+	67, // [67:67] is the sub-list for method output_type
+	67, // [67:67] is the sub-list for method input_type
+	67, // [67:67] is the sub-list for extension type_name
+	67, // [67:67] is the sub-list for extension extendee
+	0,  // [0:67] is the sub-list for field type_name
 }
 
 func init() { file_common_proto_init() }
@@ -3898,7 +4291,7 @@ func file_common_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)),
 			NumEnums:      7,
-			NumMessages:   53,
+			NumMessages:   59,
 			NumExtensions: 0,
 			NumServices:   0,
 		},

+ 6 - 0
src/server/msg/msg.go

@@ -73,6 +73,12 @@ func init() {
 
 	Processor.Register(&ResFriendOnLineStatus{})
 
+	Processor.Register(&ReqSendChatMsg{})
+	Processor.Register(&ResSendChatMsg{})
+	Processor.Register(&RecvChatMsg{})
+	Processor.Register(&ReqChatHistory{})
+	Processor.Register(&ResChatHistory{})
+
 	Processor.Range(func(id uint16, t reflect.Type) {
 		log.Debug("消息ID: %d, 消息类型: %s\n", id, t.Elem().Name())
 		msgList = append(msgList, MsgInfo{