2026-07-02 16:55:34 +08:00
|
|
|
|
// 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 {
|
2026-07-03 21:34:51 +08:00
|
|
|
|
rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error){
|
2026-07-02 16:55:34 +08:00
|
|
|
|
"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"
|
|
|
|
|
|
}
|