一些检测仍在等待运行
Docs Build / build-and-deploy (push) Waiting to run
- 移除 ConfigManager 配置管理器类 - 移除 GameManager 全局单例管理器类 - 移除 NetworkManager 网络连接管理器类 - 移除 CharacterData 和 ItemData 数据模型类 - 移除 BagScene、BattleScene、LobbyScene 等场景脚本 - 移除 EncounterBubble 和 EventFeedPanel UI组件脚本 - 更新代理邀请文档中的服务器连接方式 - 更新同步状态表格中的代理任务分配信息 - 添加 MiMo 任务完成总结和审查修复记录
491 行
14 KiB
Go
491 行
14 KiB
Go
// Package modules - 佣兵大厅系统模块
|
||
// 对齐GDD-13 佣兵大厅与悬赏系统
|
||
package modules
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"time"
|
||
|
||
"github.com/heroiclabs/nakama-common/runtime"
|
||
)
|
||
|
||
// RegisterMercenary 注册佣兵相关 RPC。
|
||
func RegisterMercenary(initializer runtime.Initializer) error {
|
||
rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error){
|
||
"MercenaryService/GetContracts": getContracts,
|
||
"MercenaryService/AcceptContract": acceptContract,
|
||
"MercenaryService/CompleteContract": completeContract,
|
||
"MercenaryService/PostBounty": postBounty,
|
||
"MercenaryService/GetBountyInfo": getBountyInfo,
|
||
"MercenaryService/GetCreditScore": getCreditScore,
|
||
}
|
||
for path, fn := range rpcs {
|
||
if err := initializer.RegisterRpc(path, fn); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type getContractsReq struct {
|
||
ContractType string `json:"contract_type"` // mercenary/limited_time/bounty
|
||
Difficulty int32 `json:"difficulty"` // 1-6星
|
||
Page int32 `json:"page"`
|
||
PageSize int32 `json:"page_size"`
|
||
}
|
||
|
||
type acceptContractReq struct {
|
||
ContractID string `json:"contract_id"`
|
||
PartyMembers []string `json:"party_members"` // 组队成员ID(可选)
|
||
UseDisciples bool `json:"use_disciples"` // 是否使用弟子代派
|
||
}
|
||
|
||
type completeContractReq struct {
|
||
ContractID string `json:"contract_id"`
|
||
}
|
||
|
||
type postBountyReq struct {
|
||
BountyType string `json:"bounty_type"` // official_bounty/private_bounty
|
||
TargetID string `json:"target_id"` // 目标玩家ID
|
||
RewardAmount float64 `json:"reward_amount"` // 赏金金额
|
||
Duration int32 `json:"duration"` // 有效天数
|
||
}
|
||
|
||
type getBountyInfoReq struct {
|
||
BountyID string `json:"bounty_id"`
|
||
}
|
||
|
||
type getCreditScoreReq struct {
|
||
CharacterID string `json:"character_id"`
|
||
}
|
||
|
||
type contractData struct {
|
||
ID string `json:"id"`
|
||
ContractType string `json:"contract_type"`
|
||
Difficulty int32 `json:"difficulty"`
|
||
RewardCurrency string `json:"reward_currency"`
|
||
BaseReward float64 `json:"base_reward"`
|
||
MaxParticipants int32 `json:"max_participants"`
|
||
Status string `json:"status"`
|
||
ValidUntil time.Time `json:"valid_until"`
|
||
Description interface{} `json:"description"`
|
||
}
|
||
|
||
type bountyData struct {
|
||
ContractID string `json:"contract_id"`
|
||
BountyType string `json:"bounty_type"`
|
||
TargetID string `json:"target_id"`
|
||
TargetName string `json:"target_name"`
|
||
TargetWorldTier int32 `json:"target_world_tier"`
|
||
RewardAmount float64 `json:"reward_amount"`
|
||
Status string `json:"status"`
|
||
Clues interface{} `json:"clues"`
|
||
}
|
||
|
||
type creditScoreData struct {
|
||
CharacterID string `json:"character_id"`
|
||
Score int32 `json:"score"` // 0-1000
|
||
Grade string `json:"grade"` // S/A/B/C/D
|
||
AcceptLimit int32 `json:"accept_limit"` // 每日接单上限
|
||
BannedUntil *time.Time `json:"banned_until"`
|
||
}
|
||
|
||
// 信用评级配置
|
||
var creditGradeConfig = map[string]struct {
|
||
MinScore int32
|
||
MaxScore int32
|
||
AcceptLimit int32
|
||
BanDays int32
|
||
}{
|
||
"S": {900, 1000, 7, 0},
|
||
"A": {700, 899, 5, 0},
|
||
"B": {500, 699, 3, 0},
|
||
"C": {300, 499, 2, 0},
|
||
"D": {0, 299, 0, 14}, // D级禁止接单14天
|
||
}
|
||
|
||
// 星级难度配置
|
||
var difficultyConfig = map[int32]struct {
|
||
MinRealm int32
|
||
BaseReward float64
|
||
Currency string
|
||
Description string
|
||
}{
|
||
1: {1, 100, "copper", "采集边境灵草"},
|
||
2: {2, 500, "copper", "讨伐野外妖兽群"},
|
||
3: {2, 2000, "copper", "击杀精英BOSS"},
|
||
4: {3, 30, "spirit_stone_low", "深入腹地探索遗迹"},
|
||
5: {4, 150, "spirit_stone_mid", "秘境寻宝"},
|
||
6: {5, 800, "soul_crystal", "混沌之渊探索"},
|
||
}
|
||
|
||
func getContracts(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 getContractsReq
|
||
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 c.id, c.contract_type, c.difficulty, c.currency_code, c.base_reward,
|
||
c.max_participants, c.status, c.valid_until
|
||
FROM contracts c
|
||
WHERE c.status = 'active' AND c.valid_until > NOW()
|
||
`
|
||
args := []interface{}{}
|
||
|
||
if req.ContractType != "" {
|
||
query += " AND c.contract_type = $1"
|
||
args = append(args, req.ContractType)
|
||
}
|
||
if req.Difficulty > 0 {
|
||
query += " AND c.difficulty = $2"
|
||
args = append(args, req.Difficulty)
|
||
}
|
||
|
||
query += " ORDER BY c.difficulty ASC, c.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("get contracts failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
defer rows.Close()
|
||
|
||
var contracts []contractData
|
||
for rows.Next() {
|
||
var c contractData
|
||
if err := rows.Scan(&c.ID, &c.ContractType, &c.Difficulty, &c.RewardCurrency,
|
||
&c.BaseReward, &c.MaxParticipants, &c.Status, &c.ValidUntil); err != nil {
|
||
continue
|
||
}
|
||
contracts = append(contracts, c)
|
||
}
|
||
|
||
return okResp(map[string]interface{}{
|
||
"contracts": contracts,
|
||
"count": len(contracts),
|
||
"page": req.Page,
|
||
"page_size": req.PageSize,
|
||
}, 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(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
// 获取角色ID
|
||
var charID 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(&charID)
|
||
if err != nil {
|
||
return errResp(4002, "character not found", traceID)
|
||
}
|
||
|
||
// 检查信用评级
|
||
var grade string
|
||
var score int32
|
||
err = hhdbPool.QueryRow(ctx, `
|
||
SELECT COALESCE(mercenary_score, 0) FROM characters WHERE id = $1
|
||
`, charID).Scan(&score)
|
||
if err != nil {
|
||
score = 500
|
||
}
|
||
|
||
grade = "B"
|
||
for g, config := range creditGradeConfig {
|
||
if score >= config.MinScore && score <= config.MaxScore {
|
||
grade = g
|
||
break
|
||
}
|
||
}
|
||
|
||
if grade == "D" {
|
||
return errResp(8101, "credit score too low, banned from accepting contracts", traceID)
|
||
}
|
||
|
||
// 检查每日接单限制
|
||
var todayAccepted int32
|
||
err = hhdbPool.QueryRow(ctx, `
|
||
SELECT COUNT(*) FROM contract_participants
|
||
WHERE character_id = $1 AND accepted_at >= CURRENT_DATE
|
||
`, charID).Scan(&todayAccepted)
|
||
if err != nil {
|
||
todayAccepted = 0
|
||
}
|
||
|
||
acceptLimit := creditGradeConfig[grade].AcceptLimit
|
||
if todayAccepted >= acceptLimit {
|
||
return errResp(8102, "daily accept limit reached", traceID)
|
||
}
|
||
|
||
// 接受委托
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
INSERT INTO contract_participants (contract_id, character_id, participant_type, status, accepted_at)
|
||
VALUES ($1, $2, 'hunter', 'accepted', NOW())
|
||
ON CONFLICT DO NOTHING
|
||
`, req.ContractID, charID)
|
||
if err != nil {
|
||
logger.Error("accept contract failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
|
||
// 更新委托状态
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
UPDATE contracts SET status = 'accepted' WHERE id = $1 AND status = 'active'
|
||
`, req.ContractID)
|
||
if err != nil {
|
||
logger.Error("update contract status failed: %v", err)
|
||
}
|
||
|
||
return okResp(map[string]interface{}{
|
||
"success": true,
|
||
"contract_id": req.ContractID,
|
||
"credit_grade": grade,
|
||
"message": "委托接受成功",
|
||
}, traceID)
|
||
}
|
||
|
||
func completeContract(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 completeContractReq
|
||
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
||
return errResp(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
// 获取角色ID
|
||
var charID 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(&charID)
|
||
if err != nil {
|
||
return errResp(4002, "character not found", traceID)
|
||
}
|
||
|
||
// 检查委托是否被接受
|
||
var participantStatus string
|
||
err = hhdbPool.QueryRow(ctx, `
|
||
SELECT status FROM contract_participants
|
||
WHERE contract_id = $1 AND character_id = $2
|
||
`, req.ContractID, charID).Scan(&participantStatus)
|
||
if err != nil {
|
||
return errResp(8103, "not accepted this contract", traceID)
|
||
}
|
||
|
||
if participantStatus != "accepted" {
|
||
return errResp(8104, "contract already completed or failed", traceID)
|
||
}
|
||
|
||
// 完成委托
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
UPDATE contract_participants
|
||
SET status = 'completed', completed_at = NOW()
|
||
WHERE contract_id = $1 AND character_id = $2
|
||
`, req.ContractID, charID)
|
||
if err != nil {
|
||
logger.Error("complete contract failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
|
||
// 发放奖励(简化版,实际应查询委托详情)
|
||
reward := 100.0
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
UPDATE characters
|
||
SET mercenary_score = mercenary_score + 10,
|
||
updated_at = NOW()
|
||
WHERE id = $1
|
||
`, charID)
|
||
if err != nil {
|
||
logger.Error("update mercenary score failed: %v", err)
|
||
}
|
||
|
||
// 更新委托状态
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
UPDATE contracts SET status = 'completed', completed_at = NOW()
|
||
WHERE id = $1
|
||
`, req.ContractID)
|
||
if err != nil {
|
||
logger.Error("update contract status failed: %v", err)
|
||
}
|
||
|
||
return okResp(map[string]interface{}{
|
||
"success": true,
|
||
"contract_id": req.ContractID,
|
||
"reward": reward,
|
||
"score_gain": 10,
|
||
"message": "委托完成,奖励已发放",
|
||
}, traceID)
|
||
}
|
||
|
||
func postBounty(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 postBountyReq
|
||
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
||
return errResp(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
// 获取发布者角色ID
|
||
var publisherID 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(&publisherID)
|
||
if err != nil {
|
||
return errResp(4002, "character not found", traceID)
|
||
}
|
||
|
||
// 检查目标玩家
|
||
var targetName string
|
||
var targetWorldTier int32
|
||
err = hhdbPool.QueryRow(ctx, `
|
||
SELECT name, world_tier FROM characters WHERE id = $1 AND status = 'active'
|
||
`, req.TargetID).Scan(&targetName, &targetWorldTier)
|
||
if err != nil {
|
||
return errResp(8105, "target not found", traceID)
|
||
}
|
||
|
||
// 检查赏金金额
|
||
if req.RewardAmount <= 0 {
|
||
return errResp(8106, "invalid reward amount", traceID)
|
||
}
|
||
|
||
// 创建悬赏委托
|
||
contractID := genUUID()
|
||
validUntil := time.Now().Add(time.Duration(req.Duration) * 24 * time.Hour)
|
||
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
INSERT INTO contracts (id, contract_type, publisher_id, difficulty, currency_code,
|
||
base_reward, max_participants, status, valid_until)
|
||
VALUES ($1, 'bounty', $2, 5, 'spirit_stone_low', $3, 1, 'active', $4)
|
||
`, contractID, publisherID, req.RewardAmount, validUntil)
|
||
if err != nil {
|
||
logger.Error("post bounty contract failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
|
||
// 创建悬赏记录
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
INSERT INTO bounties (contract_id, bounty_type, target_id, target_world_tier,
|
||
reward_amount, fee_amount, status)
|
||
VALUES ($1, $2, $3, $4, $5, $6, 'active')
|
||
`, contractID, req.BountyType, req.TargetID, targetWorldTier,
|
||
req.RewardAmount, req.RewardAmount*0.1) // 10%手续费
|
||
if err != nil {
|
||
logger.Error("post bounty record failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
|
||
return okResp(map[string]interface{}{
|
||
"success": true,
|
||
"bounty_id": contractID,
|
||
"target_name": targetName,
|
||
"reward": req.RewardAmount,
|
||
"fee": req.RewardAmount * 0.1,
|
||
"valid_until": validUntil,
|
||
"message": "悬赏发布成功",
|
||
}, traceID)
|
||
}
|
||
|
||
func getBountyInfo(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
||
traceID := newTraceID()
|
||
|
||
var req getBountyInfoReq
|
||
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
||
return errResp(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
var bounty bountyData
|
||
err := hhdbPool.QueryRow(ctx, `
|
||
SELECT b.contract_id, b.bounty_type, b.target_id, c.name, b.target_world_tier,
|
||
b.reward_amount, b.status, b.clues
|
||
FROM bounties b
|
||
JOIN characters c ON b.target_id = c.id
|
||
WHERE b.contract_id = $1
|
||
`, req.BountyID).Scan(
|
||
&bounty.ContractID, &bounty.BountyType, &bounty.TargetID,
|
||
&bounty.TargetName, &bounty.TargetWorldTier, &bounty.RewardAmount,
|
||
&bounty.Status, &bounty.Clues,
|
||
)
|
||
if err != nil {
|
||
return errResp(8107, "bounty not found", traceID)
|
||
}
|
||
|
||
return okResp(bounty, traceID)
|
||
}
|
||
|
||
func getCreditScore(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 getCreditScoreReq
|
||
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
||
return errResp(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
var score int32
|
||
err := hhdbPool.QueryRow(ctx, `
|
||
SELECT COALESCE(mercenary_score, 0) FROM characters WHERE id = $1
|
||
`, req.CharacterID).Scan(&score)
|
||
if err != nil {
|
||
score = 500
|
||
}
|
||
|
||
grade := "B"
|
||
acceptLimit := int32(3)
|
||
for g, config := range creditGradeConfig {
|
||
if score >= config.MinScore && score <= config.MaxScore {
|
||
grade = g
|
||
acceptLimit = config.AcceptLimit
|
||
break
|
||
}
|
||
}
|
||
|
||
return okResp(creditScoreData{
|
||
CharacterID: req.CharacterID,
|
||
Score: score,
|
||
Grade: grade,
|
||
AcceptLimit: acceptLimit,
|
||
}, traceID)
|
||
}
|