400 行
11 KiB
Go
400 行
11 KiB
Go
// 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)
|
||
}
|