lawless/server/modules/social.go

867 行
27 KiB
Go

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": acceptContract,
"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 string `json:"base_reward"`
TargetParams interface{} `json:"target_params"`
ValidUntil string `json:"valid_until"`
}
type acceptContractReq 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 string `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"`
MemberLimit int32 `json:"member_limit"`
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 contractData struct {
ContractID string `json:"contract_id"`
ContractType string `json:"contract_type"`
Status string `json:"status"`
PublisherID string `json:"publisher_id"`
BaseReward string `json:"base_reward"`
CurrencyCode string `json:"currency_code"`
ValidUntil string `json:"valid_until"`
}
type bountyData struct {
BountyID string `json:"bounty_id"`
BountyType string `json:"bounty_type"`
TargetID string `json:"target_id"`
RewardAmount string `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(&currentMembers)
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.NotificationRequest{
{
Code: 3002,
Content: map[string]interface{}{"type": "relation_request", "from_id": charID, "relation_type": req.RelationType},
Persistent: true,
Subject: "关系请求",
},
}, []string{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(contractData{
ContractID: contractID,
ContractType: req.ContractType,
Status: "active",
PublisherID: charID,
BaseReward: req.BaseReward,
CurrencyCode: req.CurrencyCode,
}, traceID)
}
func acceptContract(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(contractData{
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, 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(bountyData{
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 bountyData
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(bountyData{
BountyID: req.BountyID,
Status: "accepted",
}, traceID)
}