// Package modules - 好友组队系统模块 // 对齐GDD-34 好友与组队系统设计 package modules import ( "context" "database/sql" "encoding/json" "time" "github.com/heroiclabs/nakama-common/runtime" "github.com/jackc/pgx/v5" ) // RegisterFriendParty 注册好友组队相关 RPC。 func RegisterFriendParty(initializer runtime.Initializer) error { rpcs := map[string]func(runtime.Initializer) error{ "FriendPartyService/AddFriend": addFriend, "FriendPartyService/RemoveFriend": removeFriend, "FriendPartyService/GetFriends": getFriends, "FriendPartyService/AcceptFriend": acceptFriend, "FriendPartyService/CreateParty": createParty, "FriendPartyService/JoinParty": joinParty, "FriendPartyService/LeaveParty": leaveParty, "FriendPartyService/GetPartyInfo": getPartyInfo, } for path, fn := range rpcs { if err := initializer.RegisterRpc(path, fn); err != nil { return err } } return nil } type addFriendReq struct { TargetCharacterID string `json:"target_character_id"` } type removeFriendReq struct { FriendID string `json:"friend_id"` } type getFriendsReq struct { CharacterID string `json:"character_id"` } type acceptFriendReq struct { RelationID string `json:"relation_id"` } type createPartyReq struct { MaxMembers int32 `json:"max_members"` PartyName string `json:"party_name"` } type joinPartyReq struct { PartyID string `json:"party_id"` } type leavePartyReq struct { PartyID string `json:"party_id"` } type getPartyInfoReq struct { PartyID string `json:"party_id"` } type friendData struct { CharacterID string `json:"character_id"` Name string `json:"name"` RaceID string `json:"race_id"` RealmTier int32 `json:"realm_tier"` IsOnline bool `json:"is_online"` LastSeen time.Time `json:"last_seen"` Intimacy int32 `json:"intimacy"` Status string `json:"status"` // active/cooldown/dissolved } type partyData struct { PartyID string `json:"party_id"` LeaderID string `json:"leader_id"` LeaderName string `json:"leader_name"` Members interface{} `json:"members"` MaxMembers int32 `json:"max_members"` PartyName string `json:"party_name"` WorldTier int32 `json:"world_tier"` RealmTier int32 `json:"realm_tier"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` } type partyMemberData struct { CharacterID string `json:"character_id"` Name string `json:"name"` RaceID string `json:"race_id"` RealmTier int32 `json:"realm_tier"` Role string `json:"role"` // leader/member IsReady bool `json:"is_ready"` } func addFriend(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 addFriendReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取发起者角色ID var charID string err := hhdbPool.QueryRow(ctx, ` SELECT id FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(4002, "character not found", traceID) } // 检查目标是否存在 var targetName string err = hhdbPool.QueryRow(ctx, ` SELECT name FROM characters WHERE id = $1 AND status = 'active' `, req.TargetCharacterID).Scan(&targetName) if err != nil { return errResp(8401, "target not found", traceID) } // 检查是否已是好友 var existingCount int32 err = hhdbPool.QueryRow(ctx, ` SELECT COUNT(*) FROM social_relations WHERE ((character_a_id = $1 AND character_b_id = $2) OR (character_a_id = $2 AND character_b_id = $1)) AND relation_type = 'friend' AND status = 'active' `, charID, req.TargetCharacterID).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(8402, "already friends", traceID) } // 创建好友关系(待接受) _, err = hhdbPool.Exec(ctx, ` INSERT INTO social_relations (character_a_id, character_b_id, relation_type, intimacy, status) VALUES ($1, $2, 'friend', 0, 'active') ON CONFLICT DO NOTHING `, charID, req.TargetCharacterID) if err != nil { logger.Error("add friend failed: %v", err) return errResp(9002, "internal error", traceID) } // 发送通知给目标玩家 _, err = nk.NotificationsSend(ctx, []runtime.NotificationRequest{ { Code: 3001, Content: map[string]interface{}{"type": "friend_request", "from_id": charID}, Persistent: true, Subject: "好友请求", }, }, []string{req.TargetCharacterID}) if err != nil { logger.Error("send friend notification failed: %v", err) } return okResp(map[string]interface{}{ "success": true, "target_name": targetName, "message": "好友请求已发送", }, traceID) } func removeFriend(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 removeFriendReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取角色ID var charID string err := hhdbPool.QueryRow(ctx, ` SELECT id FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(4002, "character not found", traceID) } // 删除好友关系 _, err = hhdbPool.Exec(ctx, ` UPDATE social_relations SET status = 'dissolved', dissolved_at = NOW() WHERE ((character_a_id = $1 AND character_b_id = $2) OR (character_a_id = $2 AND character_b_id = $1)) AND relation_type = 'friend' AND status = 'active' `, charID, req.FriendID) if err != nil { logger.Error("remove friend failed: %v", err) return errResp(9002, "internal error", traceID) } return okResp(map[string]interface{}{ "success": true, "message": "好友已删除", }, traceID) } func getFriends(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 getFriendsReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 查询好友列表 rows, err := hhdbPool.Query(ctx, ` SELECT c.id, c.name, c.race_id, c.realm_tier, c.last_online_at, sr.intimacy, sr.status FROM social_relations sr JOIN characters c ON ( CASE WHEN sr.character_a_id = $1 THEN sr.character_b_id ELSE sr.character_a_id END ) = c.id WHERE (sr.character_a_id = $1 OR sr.character_b_id = $1) AND sr.relation_type = 'friend' AND sr.status = 'active' ORDER BY sr.intimacy DESC `, req.CharacterID) if err != nil { logger.Error("get friends failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() var friends []friendData for rows.Next() { var f friendData if err := rows.Scan(&f.CharacterID, &f.Name, &f.RaceID, &f.RealmTier, &f.LastSeen, &f.Intimacy, &f.Status); err != nil { continue } // 简单判断在线状态(5分钟内有活动) f.IsOnline = time.Since(f.LastSeen) < 5*time.Minute friends = append(friends, f) } return okResp(map[string]interface{}{ "friends": friends, "count": len(friends), }, traceID) } func acceptFriend(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 acceptFriendReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 简化版:接受好友请求 logger.Info("Accept friend request: relation_id=%s", req.RelationID) return okResp(map[string]interface{}{ "success": true, "message": "好友请求已接受", }, traceID) } func createParty(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 createPartyReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取队长角色ID var charID string var charName string var worldTier, realmTier int32 err := hhdbPool.QueryRow(ctx, ` SELECT id, name, world_tier, realm_tier FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID, &charName, &worldTier, &realmTier) if err != nil { return errResp(4002, "character not found", traceID) } // 创建队伍(简化版,实际应存储到数据库) partyID := genUUID() return okResp(partyData{ PartyID: partyID, LeaderID: charID, LeaderName: charName, MaxMembers: req.MaxMembers, PartyName: req.PartyName, WorldTier: worldTier, RealmTier: realmTier, Status: "forming", CreatedAt: time.Now(), }, traceID) } func joinParty(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 joinPartyReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取角色信息 var charID string var charName string var worldTier, realmTier int32 err := hhdbPool.QueryRow(ctx, ` SELECT id, name, world_tier, realm_tier FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID, &charName, &worldTier, &realmTier) if err != nil { return errResp(4002, "character not found", traceID) } // 简化版:加入队伍 logger.Info("Join party: party=%s player=%s", req.PartyID, charName) return okResp(map[string]interface{}{ "success": true, "party_id": req.PartyID, "character_id": charID, "character_name": charName, "message": "成功加入队伍", }, traceID) } func leaveParty(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 leavePartyReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 简化版:离开队伍 logger.Info("Leave party: party=%s", req.PartyID) return okResp(map[string]interface{}{ "success": true, "message": "已离开队伍", }, traceID) } func getPartyInfo(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req getPartyInfoReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 简化版:返回队伍信息 return okResp(map[string]interface{}{ "party_id": req.PartyID, "members": []partyMemberData{}, "max_members": 5, "status": "forming", }, traceID) }