// Package modules - 稀有宝物流转系统模块 // 对齐GDD-14 稀有宝物流转与拍卖系统 package modules import ( "context" "database/sql" "encoding/json" "math/rand" "time" "github.com/heroiclabs/nakama-common/runtime" "github.com/jackc/pgx/v5" ) // RegisterRareTreasure 注册稀有宝物相关 RPC。 func RegisterRareTreasure(initializer runtime.Initializer) error { rpcs := map[string]func(runtime.Initializer) error{ "RareTreasureService/GetTreasureInfo": getTreasureInfo, "RareTreasureService/ListAuctions": listAuctions, "RareTreasureService/PlaceBid": placeBid, "RareTreasureService/GetAuctionDetail": getAuctionDetail, "RareTreasureService/ReportLocation": reportLocation, } for path, fn := range rpcs { if err := initializer.RegisterRpc(path, fn); err != nil { return err } } return nil } type getTreasureInfoReq struct { TreasureID string `json:"treasure_id"` } type listAuctionsReq struct { AuctionType string `json:"auction_type"` // official/organization Category string `json:"category"` // rare_bloodline/rare_manual/jade_slip/secret_material/artifact/material Page int32 `json:"page"` PageSize int32 `json:"page_size"` } type placeBidReq struct { AuctionID string `json:"auction_id"` Amount float64 `json:"amount"` IsAutoBid bool `json:"is_auto_bid"` MaxAmount float64 `json:"max_amount"` } type getAuctionDetailReq struct { AuctionID string `json:"auction_id"` } type reportLocationReq struct { TreasureID string `json:"treasure_id"` LocationX float64 `json:"location_x"` LocationY float64 `json:"location_y"` } type treasureInfoData struct { ID string `json:"id"` Type string `json:"type"` // bloodline/manual Race string `json:"race"` Name string `json:"name"` Description string `json:"description"` HolderID string `json:"holder_id"` HolderName string `json:"holder_name"` IsTraded bool `json:"is_traded"` AcquiredAt time.Time `json:"acquired_at"` BroadcastTriggered bool `json:"broadcast_triggered"` } type auctionData struct { ID string `json:"id"` AuctionType string `json:"auction_type"` Category string `json:"category"` ItemID string `json:"item_id"` ItemName string `json:"item_name"` SellerID string `json:"seller_id"` SellerName string `json:"seller_name"` CurrencyCode string `json:"currency_code"` StartPrice float64 `json:"start_price"` CurrentPrice float64 `json:"current_price"` BidCount int32 `json:"bid_count"` MinIncrement float64 `json:"min_increment"` TaxRate float64 `json:"tax_rate"` Status string `json:"status"` StartedAt *time.Time `json:"started_at"` EndedAt *time.Time `json:"ended_at"` IsAnonymous bool `json:"is_anonymous"` } type auctionDetailData struct { Auction auctionData `json:"auction"` Bids []bidData `json:"bids"` ItemData interface{} `json:"item_data"` } type bidData struct { BidderID string `json:"bidder_id"` BidderName string `json:"bidder_name"` Amount float64 `json:"amount"` IsAuto bool `json:"is_auto"` BidAt time.Time `json:"bid_at"` } // 稀有宝物配置 var rareTreasureConfig = map[string]struct { Name string Type string Race string Description string BroadcastRate float64 }{ "dragon_blood_crystal": { Name: "龙族真血晶", Type: "bloodline", Race: "dragon", Description: "龙族血脉碎片,使用后可触发龙族转换", BroadcastRate: 0.08, }, "chaos_source_core": { Name: "混沌本源核", Type: "bloodline", Race: "chaos", Description: "混沌裔血脉碎片,使用后可触发混沌裔转换", BroadcastRate: 0.10, }, "titan_fire_stone": { Name: "泰坦火种石", Type: "bloodline", Race: "giant", Description: "巨人族血脉碎片,使用后可触发巨人族转换", BroadcastRate: 0.07, }, "fallen_feather_core": { Name: "堕羽圣核", Type: "bloodline", Race: "demon", Description: "堕天使裔血脉碎片,使用后可触发堕天使裔转换", BroadcastRate: 0.09, }, "dragon_supreme_manual": { Name: "《九龙镇世诀》玉简", Type: "manual", Race: "dragon", Description: "龙族传承功法,转换后功法起点显著更高", BroadcastRate: 0.10, }, "chaos_supreme_manual": { Name: "《太初混沌经》玉简", Type: "manual", Race: "chaos", Description: "混沌裔传承功法,转换后功法起点显著更高", BroadcastRate: 0.10, }, } func getTreasureInfo(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req getTreasureInfoReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } config, ok := rareTreasureConfig[req.TreasureID] if !ok { return errResp(8201, "treasure not found", traceID) } return okResp(treasureInfoData{ ID: req.TreasureID, Type: config.Type, Race: config.Race, Name: config.Name, Description: config.Description, }, traceID) } func listAuctions(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req listAuctionsReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } if req.Page < 1 { req.Page = 1 } if req.PageSize < 1 || req.PageSize > 50 { req.PageSize = 20 } offset := (req.Page - 1) * req.PageSize // 查询拍卖列表 query := ` SELECT a.id, a.auction_type, a.category, a.item_id, i.name, a.seller_id, c.name, a.currency_code, a.start_price, (SELECT COALESCE(MAX(amount), a.start_price) FROM auction_bids WHERE auction_id = a.id), (SELECT COUNT(*) FROM auction_bids WHERE auction_id = a.id), a.min_increment_rate, a.tax_rate, a.status, a.started_at, a.ended_at, a.is_anonymous FROM auctions a JOIN items i ON a.item_id = i.id JOIN characters c ON a.seller_id = c.id WHERE a.status IN ('active', 'extended') ` args := []interface{}{} if req.AuctionType != "" { query += " AND a.auction_type = $1" args = append(args, req.AuctionType) } if req.Category != "" { query += " AND a.category = $2" args = append(args, req.Category) } query += " ORDER BY a.created_at DESC LIMIT $3 OFFSET $4" args = append(args, req.PageSize, offset) rows, err := hhdbPool.Query(ctx, query, args...) if err != nil { logger.Error("list auctions failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() var auctions []auctionData for rows.Next() { var a auctionData if err := rows.Scan( &a.ID, &a.AuctionType, &a.Category, &a.ItemID, &a.ItemName, &a.SellerID, &a.SellerName, &a.CurrencyCode, &a.StartPrice, &a.CurrentPrice, &a.BidCount, &a.MinIncrement, &a.TaxRate, &a.Status, &a.StartedAt, &a.EndedAt, &a.IsAnonymous, ); err != nil { continue } auctions = append(auctions, a) } return okResp(map[string]interface{}{ "auctions": auctions, "count": len(auctions), "page": req.Page, "page_size": req.PageSize, }, traceID) } func placeBid(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 placeBidReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取竞拍者角色ID var bidderID 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(&bidderID) if err != nil { return errResp(4002, "character not found", traceID) } // 查询拍卖信息 var auction auctionData err = hhdbPool.QueryRow(ctx, ` SELECT a.id, a.start_price, a.status, a.seller_id, a.min_increment_rate FROM auctions a WHERE a.id = $1 `, req.AuctionID).Scan( &auction.ID, &auction.StartPrice, &auction.Status, &auction.SellerID, &auction.MinIncrement, ) if err != nil { return errResp(8202, "auction not found", traceID) } if auction.Status != "active" && auction.Status != "extended" { return errResp(8203, "auction not active", traceID) } // 检查是否是卖家自己 if bidderID == auction.SellerID { return errResp(8204, "cannot bid on own auction", traceID) } // 获取当前最高出价 var currentPrice float64 err = hhdbPool.QueryRow(ctx, ` SELECT COALESCE(MAX(amount), $1) FROM auction_bids WHERE auction_id = $2 `, auction.StartPrice, req.AuctionID).Scan(¤tPrice) if err != nil { currentPrice = auction.StartPrice } // 检查出价是否足够 minBid := currentPrice * (1 + auction.MinIncrement) if req.Amount < minBid { return errResp(8205, "bid too low", traceID) } // 记录出价 _, err = hhdbPool.Exec(ctx, ` INSERT INTO auction_bids (auction_id, bidder_id, amount, is_auto_bid, auto_max_amount, bid_at) VALUES ($1, $2, $3, $4, $5, NOW()) `, req.AuctionID, bidderID, req.Amount, req.IsAutoBid, req.MaxAmount) if err != nil { logger.Error("place bid failed: %v", err) return errResp(9002, "internal error", traceID) } return okResp(map[string]interface{}{ "success": true, "auction_id": req.AuctionID, "bid_amount": req.Amount, "current_price": req.Amount, "message": "出价成功", }, traceID) } func getAuctionDetail(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req getAuctionDetailReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 查询拍卖详情 var auction auctionData err := hhdbPool.QueryRow(ctx, ` SELECT a.id, a.auction_type, a.category, a.item_id, i.name, a.seller_id, c.name, a.currency_code, a.start_price, (SELECT COALESCE(MAX(amount), a.start_price) FROM auction_bids WHERE auction_id = a.id), (SELECT COUNT(*) FROM auction_bids WHERE auction_id = a.id), a.min_increment_rate, a.tax_rate, a.status, a.started_at, a.ended_at, a.is_anonymous FROM auctions a JOIN items i ON a.item_id = i.id JOIN characters c ON a.seller_id = c.id WHERE a.id = $1 `, req.AuctionID).Scan( &auction.ID, &auction.AuctionType, &auction.Category, &auction.ItemID, &auction.ItemName, &auction.SellerID, &auction.SellerName, &auction.CurrencyCode, &auction.StartPrice, &auction.CurrentPrice, &auction.BidCount, &auction.MinIncrement, &auction.TaxRate, &auction.Status, &auction.StartedAt, &auction.EndedAt, &auction.IsAnonymous, ) if err != nil { return errResp(8202, "auction not found", traceID) } // 查询出价记录 rows, err := hhdbPool.Query(ctx, ` SELECT b.bidder_id, c.name, b.amount, b.is_auto_bid, b.bid_at FROM auction_bids b JOIN characters c ON b.bidder_id = c.id WHERE b.auction_id = $1 ORDER BY b.amount DESC LIMIT 10 `, req.AuctionID) if err != nil { logger.Error("get auction bids failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() var bids []bidData for rows.Next() { var b bidData if err := rows.Scan(&b.BidderID, &b.BidderName, &b.Amount, &b.IsAuto, &b.BidAt); err != nil { continue } bids = append(bids, b) } return okResp(auctionDetailData{ Auction: auction, Bids: bids, }, traceID) } func reportLocation(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 reportLocationReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取报告者角色ID var reporterID 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(&reporterID) if err != nil { return errResp(4002, "character not found", traceID) } // 记录位置报告(用于追杀令系统) logger.Info("Location reported: treasure=%s reporter=%s x=%.2f y=%.2f", req.TreasureID, reporterID, req.LocationX, req.LocationY) return okResp(map[string]interface{}{ "success": true, "reporter": reporterID, "treasure": req.TreasureID, "location_x": req.LocationX, "location_y": req.LocationY, "message": "位置报告已提交", }, traceID) }