一些检测仍在等待运行
Docs Build / build-and-deploy (push) Waiting to run
- 移除 ConfigManager 配置管理器类 - 移除 GameManager 全局单例管理器类 - 移除 NetworkManager 网络连接管理器类 - 移除 CharacterData 和 ItemData 数据模型类 - 移除 BagScene、BattleScene、LobbyScene 等场景脚本 - 移除 EncounterBubble 和 EventFeedPanel UI组件脚本 - 更新代理邀请文档中的服务器连接方式 - 更新同步状态表格中的代理任务分配信息 - 添加 MiMo 任务完成总结和审查修复记录
872 行
27 KiB
Go
872 行
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": 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)
|
|
}
|