lawless/server/modules/friend_party.go

389 行
11 KiB
Go

// Package modules - 好友组队系统模块
// 对齐GDD-34 好友与组队系统设计
package modules
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/heroiclabs/nakama-common/runtime"
)
// RegisterFriendParty 注册好友组队相关 RPC。
func RegisterFriendParty(initializer runtime.Initializer) error {
rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, 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)
}
// 发送通知给目标玩家(简化版:仅记录日志)
logger.Info("Friend request sent from %s to %s", charID, req.TargetCharacterID)
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)
}