package modules import ( "context" "database/sql" "encoding/json" "github.com/heroiclabs/nakama-common/runtime" ) // RegisterSocial 注册社交/组织/悬赏相关 RPC。 func RegisterSocial(initializer runtime.Initializer) error { rpcs := map[string]func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, string) (string, error){ "SocialService/CreateOrganization": createOrganization, "SocialService/GetOrganization": getOrganization, "SocialService/JoinOrganization": joinOrganization, "SocialService/LeaveOrganization": leaveOrganization, "SocialService/UpdateMemberRole": updateMemberRole, "SocialService/SendRelationRequest": sendRelationRequest, "SocialService/RespondRelationRequest": respondRelationRequest, "SocialService/PublishContract": publishContract, "SocialService/AcceptContract": socialAcceptContract, "SocialService/PublishBounty": publishBounty, "SocialService/AcceptBounty": acceptBounty, } for name, fn := range rpcs { if err := initializer.RegisterRpc(name, fn); err != nil { return err } } return nil } type createOrgReq struct { OrgType string `json:"org_type"` Name string `json:"name"` RegionID string `json:"region_id"` FoundingMembers []string `json:"founding_members"` } type orgIDReq struct { GuildID string `json:"guild_id"` } type orgMemberReq struct { GuildID string `json:"guild_id"` } type updateRoleReq struct { GuildID string `json:"guild_id"` CharacterID string `json:"character_id"` Role string `json:"role"` Action string `json:"action"` } type relationReq struct { RelationType string `json:"relation_type"` TargetCharacterID string `json:"target_character_id"` VowPath string `json:"vow_path"` } type relationResponseReq struct { RelationType string `json:"relation_type"` RequestID string `json:"request_id"` Action string `json:"action"` } type publishContractReq struct { ContractType string `json:"contract_type"` Difficulty int32 `json:"difficulty"` CurrencyCode string `json:"currency_code"` BaseReward int32 `json:"base_reward"` MaxParticipants int32 `json:"max_participants"` TargetParams interface{} `json:"target_params"` ValidUntil string `json:"valid_until"` } type socialAcceptContractReq struct { ContractID string `json:"contract_id"` AcceptType string `json:"accept_type"` DiscipleID string `json:"disciple_id"` PartyMembers []string `json:"party_members"` } type publishBountyReq struct { BountyType string `json:"bounty_type"` TargetCharacterID string `json:"target_character_id"` RewardAmount int32 `json:"reward_amount"` CurrencyCode string `json:"currency_code"` ValidDays int32 `json:"valid_days"` IsAnonymous bool `json:"is_anonymous"` } type orgData struct { GuildID string `json:"guild_id"` Name string `json:"name"` OrgType string `json:"org_type"` LeaderID string `json:"leader_id"` Level int32 `json:"level"` Members int32 `json:"members"` Reputation int32 `json:"reputation"` MemberLimit int32 `json:"member_limit"` Territories interface{} `json:"territories"` CreatedAt string `json:"created_at"` } type orgMemberData struct { GuildID string `json:"guild_id"` CharacterID string `json:"character_id"` Role string `json:"role"` JoinedAt string `json:"joined_at"` } type relationData struct { RequestID string `json:"request_id"` RelationType string `json:"relation_type"` FromCharacterID string `json:"from_character_id"` ToCharacterID string `json:"to_character_id"` Status string `json:"status"` } type socialContractData struct { ContractID string `json:"contract_id"` ContractType string `json:"contract_type"` Status string `json:"status"` PublisherID string `json:"publisher_id"` BaseReward int32 `json:"base_reward"` CurrencyCode string `json:"currency_code"` ValidUntil string `json:"valid_until"` } type socialBountyData struct { BountyID string `json:"bounty_id"` BountyType string `json:"bounty_type"` TargetID string `json:"target_id"` RewardAmount int32 `json:"reward_amount"` Status string `json:"status"` ValidUntil string `json:"valid_until"` } func createOrganization(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 createOrgReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7001, "invalid payload", traceID) } // 校验参数 if req.Name == "" || len(req.Name) > 64 { return errResp(7002, "invalid name", traceID) } if req.OrgType == "" { req.OrgType = "guild" } // 获取角色ID var charID string var realmTier int32 err := db.QueryRowContext(ctx, ` SELECT id, realm_tier FROM characters WHERE player_id = (SELECT id FROM players WHERE nakama_user_id = $1) AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID, &realmTier) if err != nil { return errResp(7003, "character not found", traceID) } // 检查是否已在同类组织中 var existingCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM guild_members gm JOIN guilds g ON gm.guild_id = g.id WHERE gm.character_id = $1 AND g.org_type = $2 `, charID, req.OrgType).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(7004, "already in same type organization", traceID) } // 创建组织 var guildID string err = db.QueryRowContext(ctx, ` INSERT INTO guilds (name, org_type, world_tier, leader_id, level, reputation, member_limit, tax_rate, status) VALUES ($1, $2, $3, $4, 1, 0, 50, 0.20, 'active') RETURNING id::text `, req.Name, req.OrgType, realmTier, charID).Scan(&guildID) if err != nil { logger.Error("create organization failed: %v", err) return errResp(9002, "internal error", traceID) } // 添加创建者为领袖 _, err = db.ExecContext(ctx, ` INSERT INTO guild_members (guild_id, character_id, role, joined_at, contribution, daily_quota) VALUES ($1, $2, 'leader', NOW(), '{}', '{}') `, guildID, charID) if err != nil { logger.Error("add leader failed: %v", err) } logger.Info("SocialService/CreateOrganization success: guild_id=%s name=%s", guildID, req.Name) return okResp(orgData{ GuildID: guildID, Name: req.Name, OrgType: req.OrgType, LeaderID: charID, Level: 1, Members: 1, }, traceID) } func getOrganization(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req orgIDReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7005, "invalid payload", traceID) } // 查询组织信息 var name, orgType, leaderID string var level int32 var reputation, memberLimit int32 err := db.QueryRowContext(ctx, ` SELECT name, org_type, leader_id, level, reputation, member_limit FROM guilds WHERE id = $1 AND status = 'active' `, req.GuildID).Scan(&name, &orgType, &leaderID, &level, &reputation, &memberLimit) if err != nil { return errResp(7006, "organization not found", traceID) } // 查询成员数量 var memberCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM guild_members WHERE guild_id = $1 `, req.GuildID).Scan(&memberCount) if err != nil { memberCount = 0 } // 查询领地信息 rows, err := db.QueryContext(ctx, ` SELECT id, territory_type, level FROM guild_territories WHERE guild_id = $1 `, req.GuildID) if err != nil { logger.Error("get territories failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() type territoryInfo struct { ID string `json:"id"` TerritoryType string `json:"territory_type"` Level int32 `json:"level"` } var territories []territoryInfo for rows.Next() { var t territoryInfo if err := rows.Scan(&t.ID, &t.TerritoryType, &t.Level); err != nil { continue } territories = append(territories, t) } logger.Info("SocialService/GetOrganization success: guild_id=%s name=%s", req.GuildID, name) return okResp(orgData{ GuildID: req.GuildID, Name: name, OrgType: orgType, LeaderID: leaderID, Level: level, Reputation: reputation, Members: memberCount, MemberLimit: memberLimit, Territories: territories, }, traceID) } func joinOrganization(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 orgMemberReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7005, "invalid payload", traceID) } // 获取角色ID var charID string err := db.QueryRowContext(ctx, ` SELECT id FROM characters WHERE player_id = (SELECT id FROM players WHERE nakama_user_id = $1) AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(7003, "character not found", traceID) } // 检查组织是否存在 var memberLimit int32 var orgType string err = db.QueryRowContext(ctx, ` SELECT member_limit, org_type FROM guilds WHERE id = $1 AND status = 'active' `, req.GuildID).Scan(&memberLimit, &orgType) if err != nil { return errResp(7006, "organization not found", traceID) } // 检查人数上限 var currentMembers int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM guild_members WHERE guild_id = $1 `, req.GuildID).Scan(¤tMembers) if err == nil && currentMembers >= memberLimit { return errResp(7007, "organization is full", traceID) } // 检查是否已在同类组织中 var existingCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM guild_members gm JOIN guilds g ON gm.guild_id = g.id WHERE gm.character_id = $1 AND g.org_type = $2 `, charID, orgType).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(7008, "already in same type organization", traceID) } // 加入组织 _, err = db.ExecContext(ctx, ` INSERT INTO guild_members (guild_id, character_id, role, joined_at, contribution, daily_quota) VALUES ($1, $2, 'member', NOW(), '{}', '{}') ON CONFLICT (guild_id, character_id) DO NOTHING `, req.GuildID, charID) if err != nil { logger.Error("join organization failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/JoinOrganization success: guild_id=%s char_id=%s", req.GuildID, charID) return okResp(orgMemberData{ GuildID: req.GuildID, CharacterID: charID, Role: "member", }, traceID) } func leaveOrganization(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 orgIDReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7005, "invalid payload", traceID) } // 获取角色ID var charID string err := db.QueryRowContext(ctx, ` SELECT id FROM characters WHERE player_id = (SELECT id FROM players WHERE nakama_user_id = $1) AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(7003, "character not found", traceID) } // 检查是否是领袖 var role string err = db.QueryRowContext(ctx, ` SELECT role FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.GuildID, charID).Scan(&role) if err != nil { return errResp(7009, "not a member", traceID) } if role == "leader" { return errResp(7010, "leader cannot leave", traceID) } // 离开组织 _, err = db.ExecContext(ctx, ` DELETE FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.GuildID, charID) if err != nil { logger.Error("leave organization failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/LeaveOrganization success: guild_id=%s char_id=%s", req.GuildID, charID) return okResp(orgMemberData{ GuildID: req.GuildID, CharacterID: charID, }, traceID) } func updateMemberRole(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 updateRoleReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7009, "invalid payload", traceID) } // 获取操作者角色ID var operatorID string err := db.QueryRowContext(ctx, ` SELECT id FROM characters WHERE player_id = (SELECT id FROM players WHERE nakama_user_id = $1) AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&operatorID) if err != nil { return errResp(7003, "character not found", traceID) } // 检查操作者是否是领袖 var operatorRole string err = db.QueryRowContext(ctx, ` SELECT role FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.GuildID, operatorID).Scan(&operatorRole) if err != nil { return errResp(7010, "not a member", traceID) } if operatorRole != "leader" && operatorRole != "vice" { return errResp(7011, "insufficient permissions", traceID) } // 检查目标是否是成员 var targetRole string err = db.QueryRowContext(ctx, ` SELECT role FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.GuildID, req.CharacterID).Scan(&targetRole) if err != nil { return errResp(7012, "target not a member", traceID) } // 检查职位操作权限 if targetRole == "leader" && req.Action == "demote" { return errResp(7013, "cannot demote leader", traceID) } // 更新职位 _, err = db.ExecContext(ctx, ` UPDATE guild_members SET role = $1, updated_at = NOW() WHERE guild_id = $2 AND character_id = $3 `, req.Role, req.GuildID, req.CharacterID) if err != nil { logger.Error("update member role failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/UpdateMemberRole success: guild=%s target=%s role=%s", req.GuildID, req.CharacterID, req.Role) return okResp(orgMemberData{ GuildID: req.GuildID, CharacterID: req.CharacterID, Role: req.Role, }, traceID) } func sendRelationRequest(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 relationReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7011, "invalid payload", traceID) } // 获取发起者角色ID var charID string err := db.QueryRowContext(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(7003, "character not found", traceID) } // 检查目标是否存在 var targetExists bool err = db.QueryRowContext(ctx, ` SELECT EXISTS(SELECT 1 FROM characters WHERE id = $1 AND status = 'active') `, req.TargetCharacterID).Scan(&targetExists) if err != nil || !targetExists { return errResp(7012, "target not found", traceID) } // 检查是否已有关系 var existingCount int32 err = db.QueryRowContext(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 = $3 AND status = 'active' `, charID, req.TargetCharacterID, req.RelationType).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(7013, "relation already exists", traceID) } // 创建关系请求 var requestID string err = db.QueryRowContext(ctx, ` INSERT INTO social_relations (character_a_id, character_b_id, relation_type, intimacy, status) VALUES ($1, $2, $3, 0, 'active') RETURNING id::text `, charID, req.TargetCharacterID, req.RelationType).Scan(&requestID) if err != nil { logger.Error("create relation request failed: %v", err) return errResp(9002, "internal error", traceID) } // 发送通知给目标玩家 err = nk.NotificationsSend(ctx, []*runtime.NotificationSend{ { Code: 3002, Content: map[string]interface{}{"type": "relation_request", "from_id": charID, "relation_type": req.RelationType}, Persistent: true, Subject: "关系请求", UserID: req.TargetCharacterID, }, }) if err != nil { logger.Error("send relation notification failed: %v", err) } logger.Info("SocialService/SendRelationRequest success: request_id=%s", requestID) return okResp(relationData{ RequestID: requestID, RelationType: req.RelationType, FromCharacterID: charID, ToCharacterID: req.TargetCharacterID, Status: "active", }, traceID) } func respondRelationRequest(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 relationResponseReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7011, "invalid payload", traceID) } // 获取响应者角色ID var charID string err := db.QueryRowContext(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(7003, "character not found", traceID) } // 检查关系请求是否存在 var fromID, relationType, status string err = db.QueryRowContext(ctx, ` SELECT character_a_id, relation_type, status FROM social_relations WHERE id = $1 AND character_b_id = $2 `, req.RequestID, charID).Scan(&fromID, &relationType, &status) if err != nil { return errResp(7014, "request not found", traceID) } if status != "active" { return errResp(7015, "request already processed", traceID) } // 处理响应 if req.Action == "accept" { // 保持关系为active logger.Info("Relation request accepted: request=%s", req.RequestID) } else if req.Action == "reject" { // 更新关系状态为dissolved _, err = db.ExecContext(ctx, ` UPDATE social_relations SET status = 'dissolved', dissolved_at = NOW() WHERE id = $1 `, req.RequestID) if err != nil { logger.Error("reject relation failed: %v", err) return errResp(9002, "internal error", traceID) } } else if req.Action == "dissolve" { // 解除关系 _, err = db.ExecContext(ctx, ` UPDATE social_relations SET status = 'dissolved', dissolved_at = NOW() WHERE id = $1 `, req.RequestID) if err != nil { logger.Error("dissolve relation failed: %v", err) return errResp(9002, "internal error", traceID) } } logger.Info("SocialService/RespondRelationRequest success: request=%s action=%s", req.RequestID, req.Action) return okResp(relationData{ RequestID: req.RequestID, RelationType: relationType, FromCharacterID: fromID, ToCharacterID: charID, Status: req.Action, }, traceID) } func publishContract(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 publishContractReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7015, "invalid payload", traceID) } // 获取发布者角色ID var charID string err := db.QueryRowContext(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(7003, "character not found", traceID) } // 校验委托类型 validTypes := map[string]bool{"mercenary": true, "limited_time": true, "bounty": true} if !validTypes[req.ContractType] { return errResp(7016, "invalid contract type", traceID) } // 校验赏金金额 if req.BaseReward <= 0 { return errResp(7017, "invalid reward amount", traceID) } // 创建委托 var contractID string err = db.QueryRowContext(ctx, ` INSERT INTO contracts (contract_type, publisher_id, difficulty, currency_code, base_reward, max_participants, status, valid_until) VALUES ($1, $2, $3, $4, $5, $6, 'active', NOW() + INTERVAL '7 days') RETURNING id::text `, req.ContractType, charID, req.Difficulty, req.CurrencyCode, req.BaseReward, req.MaxParticipants).Scan(&contractID) if err != nil { logger.Error("publish contract failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/PublishContract success: contract_id=%s", contractID) return okResp(socialContractData{ ContractID: contractID, ContractType: req.ContractType, Status: "active", PublisherID: charID, BaseReward: req.BaseReward, CurrencyCode: req.CurrencyCode, }, traceID) } func socialAcceptContract(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 acceptContractReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7018, "invalid payload", traceID) } // 获取接取者角色ID var charID string err := db.QueryRowContext(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(7003, "character not found", traceID) } // 检查委托是否存在且可接取 var contractStatus string var maxParticipants int32 err = db.QueryRowContext(ctx, ` SELECT status, max_participants FROM contracts WHERE id = $1 `, req.ContractID).Scan(&contractStatus, &maxParticipants) if err != nil { return errResp(7019, "contract not found", traceID) } if contractStatus != "active" { return errResp(7020, "contract not available", traceID) } // 检查是否已接取 var existingCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM contract_participants WHERE contract_id = $1 AND character_id = $2 `, req.ContractID, charID).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(7021, "already accepted this contract", traceID) } // 检查人数上限 var participantCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM contract_participants WHERE contract_id = $1 `, req.ContractID).Scan(&participantCount) if err == nil && participantCount >= maxParticipants { return errResp(7022, "contract is full", traceID) } // 创建参与记录 _, err = db.ExecContext(ctx, ` INSERT INTO contract_participants (contract_id, character_id, participant_type, status, accepted_at) VALUES ($1, $2, 'hunter', 'accepted', NOW()) `, req.ContractID, charID) if err != nil { logger.Error("accept contract failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/AcceptContract success: contract_id=%s", req.ContractID) return okResp(socialContractData{ ContractID: req.ContractID, Status: "accepted", }, traceID) } func publishBounty(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 publishBountyReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7022, "invalid payload", traceID) } // 获取发布者角色ID var charID string err := db.QueryRowContext(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(7003, "character not found", traceID) } // 校验目标是否存在 var targetExists bool err = db.QueryRowContext(ctx, ` SELECT EXISTS(SELECT 1 FROM characters WHERE id = $1 AND status = 'active') `, req.TargetCharacterID).Scan(&targetExists) if err != nil || !targetExists { return errResp(7023, "target not found", traceID) } // 校验赏金金额 if req.RewardAmount <= 0 { return errResp(7024, "invalid reward amount", traceID) } // 创建悬赏委托 var contractID string err = db.QueryRowContext(ctx, ` INSERT INTO contracts (contract_type, publisher_id, difficulty, currency_code, base_reward, max_participants, status, valid_until) VALUES ('bounty', $1, 5, $2, $3, 1, 'active', NOW() + INTERVAL '7 days') RETURNING id::text `, charID, req.CurrencyCode, req.RewardAmount).Scan(&contractID) if err != nil { logger.Error("publish bounty contract failed: %v", err) return errResp(9002, "internal error", traceID) } // 创建悬赏记录 var bountyID string err = db.QueryRowContext(ctx, ` INSERT INTO bounties (contract_id, bounty_type, target_id, target_world_tier, reward_amount, fee_amount, status) VALUES ($1, $2, $3, 1, $4, $5, 'active') RETURNING contract_id `, contractID, req.BountyType, req.TargetCharacterID, req.RewardAmount, float64(req.RewardAmount)*0.1).Scan(&bountyID) if err != nil { logger.Error("publish bounty record failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/PublishBounty success: bounty_id=%s", bountyID) return okResp(socialBountyData{ BountyID: bountyID, BountyType: req.BountyType, TargetID: req.TargetCharacterID, RewardAmount: req.RewardAmount, Status: "active", }, traceID) } func acceptBounty(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 socialBountyData if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(7026, "invalid payload", traceID) } // 获取接取者角色ID var charID string err := db.QueryRowContext(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(7003, "character not found", traceID) } // 检查悬赏是否存在且可接取 var bountyStatus string err = db.QueryRowContext(ctx, ` SELECT status FROM bounties WHERE contract_id = $1 `, req.BountyID).Scan(&bountyStatus) if err != nil { return errResp(7027, "bounty not found", traceID) } if bountyStatus != "active" { return errResp(7028, "bounty not available", traceID) } // 检查是否已接取 var existingCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM contract_participants WHERE contract_id = $1 AND character_id = $2 `, req.BountyID, charID).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(7029, "already accepted this bounty", traceID) } // 创建参与记录 _, err = db.ExecContext(ctx, ` INSERT INTO contract_participants (contract_id, character_id, participant_type, status, accepted_at) VALUES ($1, $2, 'hunter', 'accepted', NOW()) `, req.BountyID, charID) if err != nil { logger.Error("accept bounty failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("SocialService/AcceptBounty success: bounty_id=%s", req.BountyID) return okResp(socialBountyData{ BountyID: req.BountyID, Status: "accepted", }, traceID) }