lawless/server/modules/rare_treasure.go

424 行
13 KiB
Go

// Package modules - 稀有宝物流转系统模块
// 对齐GDD-14 稀有宝物流转与拍卖系统
package modules
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/heroiclabs/nakama-common/runtime"
)
// RegisterRareTreasure 注册稀有宝物相关 RPC。
func RegisterRareTreasure(initializer runtime.Initializer) error {
rpcs := map[string]func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, string) (string, 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 []treasureBidData `json:"bids"`
ItemData interface{} `json:"item_data"`
}
type treasureBidData 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 listTreasureAuctions(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(&currentPrice)
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 []treasureBidData
for rows.Next() {
var b treasureBidData
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)
}