// Package modules - 聊天系统模块 // 对齐GDD-29 聊天与信息传递系统设计 package modules import ( "context" "database/sql" "encoding/json" "time" "github.com/heroiclabs/nakama-common/runtime" ) // RegisterChat 注册聊天相关 RPC。 func RegisterChat(initializer runtime.Initializer) error { rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error){ "ChatService/SendMessage": sendMessage, "ChatService/GetMessages": getMessages, "ChatService/GetChatChannels": getChatChannels, "ChatService/JoinChannel": joinChannel, "ChatService/LeaveChannel": leaveChannel, } for path, fn := range rpcs { if err := initializer.RegisterRpc(path, fn); err != nil { return err } } return nil } type sendMessageReq struct { ChannelID string `json:"channel_id"` // channel_id 或 player_id(私聊) Content string `json:"content"` MsgType string `json:"msg_type"` // text/system/emote } type getMessagesReq struct { ChannelID string `json:"channel_id"` Limit int32 `json:"limit"` Before string `json:"before"` // 分页游标 } type getChatChannelsReq struct { CharacterID string `json:"character_id"` } type joinChannelReq struct { ChannelID string `json:"channel_id"` } type leaveChannelReq struct { ChannelID string `json:"channel_id"` } type chatMessageData struct { MessageID string `json:"message_id"` SenderID string `json:"sender_id"` SenderName string `json:"sender_name"` SenderRace string `json:"sender_race"` ChannelID string `json:"channel_id"` ChannelType string `json:"channel_type"` // world/region/guild/party/private/system Content string `json:"content"` MsgType string `json:"msg_type"` // text/system/emote Timestamp time.Time `json:"timestamp"` } type chatChannelData struct { ChannelID string `json:"channel_id"` ChannelType string `json:"channel_type"` Name string `json:"name"` MemberCount int32 `json:"member_count"` IsMuted bool `json:"is_muted"` } // 频道配置 var channelConfig = map[string]chatChannelData{ "world": { ChannelID: "world", ChannelType: "world", Name: "世界频道", MemberCount: 0, }, "region_1": { ChannelID: "region_1", ChannelType: "region", Name: "凡界频道", MemberCount: 0, }, "region_2": { ChannelID: "region_2", ChannelType: "region", Name: "灵界频道", MemberCount: 0, }, "region_3": { ChannelID: "region_3", ChannelType: "region", Name: "道界频道", MemberCount: 0, }, "region_4": { ChannelID: "region_4", ChannelType: "region", Name: "圣界频道", MemberCount: 0, }, } func sendMessage(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req sendMessageReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取发送者信息 var charID string var charName string var raceID string err := hhdbPool.QueryRow(ctx, ` SELECT id, name, race_id FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID, &charName, &raceID) if err != nil { return errResp(4002, "character not found", traceID) } // 检查消息长度 if len(req.Content) > 500 { return errResp(8801, "message too long", traceID) } // 发送消息(简化版,实际应使用Nakama实时多人游戏API) message := chatMessageData{ MessageID: genUUID(), SenderID: charID, SenderName: charName, SenderRace: raceID, ChannelID: req.ChannelID, ChannelType: getChannelType(req.ChannelID), Content: req.Content, MsgType: req.MsgType, Timestamp: time.Now(), } logger.Info("Chat message: sender=%s channel=%s content=%s", charName, req.ChannelID, req.Content) return okResp(message, traceID) } func getMessages(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req getMessagesReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } if req.Limit < 1 || req.Limit > 100 { req.Limit = 50 } // 返回空消息列表(实际应从数据库/缓存读取) return okResp(map[string]interface{}{ "channel_id": req.ChannelID, "messages": []chatMessageData{}, "count": 0, "has_more": false, }, traceID) } func getChatChannels(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req getChatChannelsReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 返回可用频道列表 var channels []chatChannelData for _, ch := range channelConfig { channels = append(channels, ch) } return okResp(map[string]interface{}{ "channels": channels, "count": len(channels), }, traceID) } func joinChannel(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req joinChannelReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 加入频道(简化版) logger.Info("Join channel: channel=%s", req.ChannelID) return okResp(map[string]interface{}{ "success": true, "channel_id": req.ChannelID, "message": "已加入频道", }, traceID) } func leaveChannel(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req leaveChannelReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 离开频道(简化版) logger.Info("Leave channel: channel=%s", req.ChannelID) return okResp(map[string]interface{}{ "success": true, "channel_id": req.ChannelID, "message": "已离开频道", }, traceID) } // 辅助函数 func getChannelType(channelID string) string { if channelID == "world" { return "world" } if len(channelID) > 6 && channelID[:6] == "region" { return "region" } return "other" }