426 行
13 KiB
Go
426 行
13 KiB
Go
// 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)
|
|
}
|