304 lines
11 KiB
Go
304 lines
11 KiB
Go
package gamemanagement
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/middleware"
|
||
gamev1 "hyapp.local/api/proto/game/v1"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type platformRequest struct {
|
||
PlatformCode string `json:"platformCode"`
|
||
PlatformName string `json:"platformName"`
|
||
Status string `json:"status"`
|
||
APIBaseURL string `json:"apiBaseUrl"`
|
||
// AdapterType 只允许服务端白名单中的值,避免后台误填导致回调走错协议。
|
||
AdapterType string `json:"adapterType"`
|
||
// CallbackSecret 只有提交非空时才覆盖旧密钥,编辑其它字段时前端传空即可。
|
||
CallbackSecret string `json:"callbackSecret"`
|
||
CallbackIPWhitelist []string `json:"callbackIpWhitelist"`
|
||
// AdapterConfigJSON 是厂商扩展配置,保持 JSON 字符串透传给 game-service。
|
||
AdapterConfigJSON string `json:"adapterConfigJson"`
|
||
SortOrder int32 `json:"sortOrder"`
|
||
}
|
||
|
||
type catalogRequest struct {
|
||
GameID string `json:"gameId"`
|
||
PlatformCode string `json:"platformCode"`
|
||
ProviderGameID string `json:"providerGameId"`
|
||
GameName string `json:"gameName"`
|
||
Category string `json:"category"`
|
||
IconURL string `json:"iconUrl"`
|
||
CoverURL string `json:"coverUrl"`
|
||
LaunchMode string `json:"launchMode"`
|
||
Orientation string `json:"orientation"`
|
||
SafeHeight int32 `json:"safeHeight"`
|
||
MinCoin int64 `json:"minCoin"`
|
||
Status string `json:"status"`
|
||
SortOrder int32 `json:"sortOrder"`
|
||
Tags []string `json:"tags"`
|
||
}
|
||
|
||
type statusRequest struct {
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
type diceConfigRequest struct {
|
||
Status string `json:"status"`
|
||
StakeOptions []diceStakeReq `json:"stakeOptions"`
|
||
FeeBPS int32 `json:"feeBps"`
|
||
PoolBPS int32 `json:"poolBps"`
|
||
MinPlayers int32 `json:"minPlayers"`
|
||
MaxPlayers int32 `json:"maxPlayers"`
|
||
RobotEnabled bool `json:"robotEnabled"`
|
||
RobotMatchWaitMS int64 `json:"robotMatchWaitMs"`
|
||
}
|
||
|
||
type diceStakeReq struct {
|
||
StakeCoin int64 `json:"stakeCoin"`
|
||
Enabled bool `json:"enabled"`
|
||
SortOrder int32 `json:"sortOrder"`
|
||
}
|
||
|
||
type dicePoolAdjustRequest struct {
|
||
AmountCoin int64 `json:"amountCoin"`
|
||
Direction string `json:"direction"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
|
||
type selfGameStakePoolRequest struct {
|
||
TargetBalanceCoin int64 `json:"targetBalanceCoin"`
|
||
SafeBalanceCoin int64 `json:"safeBalanceCoin"`
|
||
WarningBalanceCoin int64 `json:"warningBalanceCoin"`
|
||
DangerBalanceCoin int64 `json:"dangerBalanceCoin"`
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
type selfGameNewUserPolicyRequest struct {
|
||
Enabled bool `json:"enabled"`
|
||
ProtectionRounds int32 `json:"protectionRounds"`
|
||
ProtectionHours int32 `json:"protectionHours"`
|
||
MaxStakeCoin int64 `json:"maxStakeCoin"`
|
||
LifetimeSubsidyQuotaCoin int64 `json:"lifetimeSubsidyQuotaCoin"`
|
||
Day1SubsidyQuotaCoin int64 `json:"day1SubsidyQuotaCoin"`
|
||
SingleRoundSubsidyCapCoin int64 `json:"singleRoundSubsidyCapCoin"`
|
||
StrategyLevel string `json:"strategyLevel"`
|
||
LoseStreakProtectionEnabled bool `json:"loseStreakProtectionEnabled"`
|
||
LoseStreakTrigger int32 `json:"loseStreakTrigger"`
|
||
NormalPhaseTargetWinRatePercent int32 `json:"normalPhaseTargetWinRatePercent"`
|
||
BlackPoolRobotForceWinEnabled bool `json:"blackPoolRobotForceWinEnabled"`
|
||
}
|
||
|
||
type diceGenerateRobotsRequest struct {
|
||
Count int32 `json:"count"`
|
||
NicknamePrefix string `json:"nicknamePrefix"`
|
||
NicknameLanguage string `json:"nicknameLanguage"`
|
||
AvatarURLs []string `json:"avatarUrls"`
|
||
Country string `json:"country"`
|
||
Gender string `json:"gender"`
|
||
}
|
||
|
||
type diceRobotStatusRequest struct {
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
type roomRPSConfigRequest struct {
|
||
Status string `json:"status"`
|
||
ChallengeTimeoutMS int64 `json:"challengeTimeoutMs"`
|
||
RevealCountdownMS int64 `json:"revealCountdownMs"`
|
||
StakeGifts []roomRPSStakeGiftReq `json:"stakeGifts"`
|
||
}
|
||
|
||
type roomRPSStakeGiftReq struct {
|
||
GiftID string `json:"giftId"`
|
||
Enabled bool `json:"enabled"`
|
||
SortOrder int32 `json:"sortOrder"`
|
||
}
|
||
|
||
func (r platformRequest) toProto(platformCode string) *gamev1.GamePlatform {
|
||
if platformCode == "" {
|
||
platformCode = r.PlatformCode
|
||
}
|
||
return &gamev1.GamePlatform{
|
||
PlatformCode: strings.TrimSpace(platformCode),
|
||
PlatformName: strings.TrimSpace(r.PlatformName),
|
||
Status: strings.TrimSpace(r.Status),
|
||
ApiBaseUrl: strings.TrimSpace(r.APIBaseURL),
|
||
AdapterType: strings.TrimSpace(r.AdapterType),
|
||
CallbackSecret: strings.TrimSpace(r.CallbackSecret),
|
||
CallbackIpWhitelist: compactTags(r.CallbackIPWhitelist),
|
||
AdapterConfigJson: strings.TrimSpace(r.AdapterConfigJSON),
|
||
SortOrder: r.SortOrder,
|
||
}
|
||
}
|
||
|
||
func (r catalogRequest) toProto(gameID string) *gamev1.GameCatalogItem {
|
||
if gameID == "" {
|
||
gameID = r.GameID
|
||
}
|
||
return &gamev1.GameCatalogItem{
|
||
GameId: strings.TrimSpace(gameID),
|
||
PlatformCode: strings.TrimSpace(r.PlatformCode),
|
||
ProviderGameId: strings.TrimSpace(r.ProviderGameID),
|
||
GameName: strings.TrimSpace(r.GameName),
|
||
Category: strings.TrimSpace(r.Category),
|
||
IconUrl: strings.TrimSpace(r.IconURL),
|
||
CoverUrl: strings.TrimSpace(r.CoverURL),
|
||
LaunchMode: strings.TrimSpace(r.LaunchMode),
|
||
Orientation: strings.TrimSpace(r.Orientation),
|
||
SafeHeight: r.SafeHeight,
|
||
MinCoin: r.MinCoin,
|
||
Status: strings.TrimSpace(r.Status),
|
||
SortOrder: r.SortOrder,
|
||
Tags: compactTags(r.Tags),
|
||
}
|
||
}
|
||
|
||
func (r diceConfigRequest) toProto(gameID string) *gamev1.DiceConfig {
|
||
options := make([]*gamev1.DiceStakeOption, 0, len(r.StakeOptions))
|
||
for _, option := range r.StakeOptions {
|
||
options = append(options, &gamev1.DiceStakeOption{
|
||
StakeCoin: option.StakeCoin,
|
||
Enabled: option.Enabled,
|
||
SortOrder: option.SortOrder,
|
||
})
|
||
}
|
||
return &gamev1.DiceConfig{
|
||
GameId: strings.TrimSpace(gameID),
|
||
Status: strings.TrimSpace(r.Status),
|
||
StakeOptions: options,
|
||
FeeBps: r.FeeBPS,
|
||
PoolBps: r.PoolBPS,
|
||
MinPlayers: r.MinPlayers,
|
||
MaxPlayers: r.MaxPlayers,
|
||
RobotEnabled: r.RobotEnabled,
|
||
RobotMatchWaitMs: r.RobotMatchWaitMS,
|
||
}
|
||
}
|
||
|
||
func (r selfGameNewUserPolicyRequest) toProto(gameID string) *gamev1.SelfGameNewUserPolicy {
|
||
return &gamev1.SelfGameNewUserPolicy{
|
||
GameId: strings.TrimSpace(gameID),
|
||
Enabled: r.Enabled,
|
||
ProtectionRounds: r.ProtectionRounds,
|
||
ProtectionHours: r.ProtectionHours,
|
||
MaxStakeCoin: r.MaxStakeCoin,
|
||
LifetimeSubsidyQuotaCoin: r.LifetimeSubsidyQuotaCoin,
|
||
Day1SubsidyQuotaCoin: r.Day1SubsidyQuotaCoin,
|
||
SingleRoundSubsidyCapCoin: r.SingleRoundSubsidyCapCoin,
|
||
StrategyLevel: strings.TrimSpace(r.StrategyLevel),
|
||
LoseStreakProtectionEnabled: r.LoseStreakProtectionEnabled,
|
||
LoseStreakTrigger: r.LoseStreakTrigger,
|
||
NormalPhaseTargetWinRatePercent: r.NormalPhaseTargetWinRatePercent,
|
||
BlackPoolRobotForceWinEnabled: r.BlackPoolRobotForceWinEnabled,
|
||
}
|
||
}
|
||
|
||
func (r selfGameStakePoolRequest) toProto(gameID string, stakeCoin int64) *gamev1.SelfGameStakePool {
|
||
return &gamev1.SelfGameStakePool{
|
||
GameId: strings.TrimSpace(gameID),
|
||
StakeCoin: stakeCoin,
|
||
TargetBalanceCoin: r.TargetBalanceCoin,
|
||
SafeBalanceCoin: r.SafeBalanceCoin,
|
||
WarningBalanceCoin: r.WarningBalanceCoin,
|
||
DangerBalanceCoin: r.DangerBalanceCoin,
|
||
Status: strings.TrimSpace(r.Status),
|
||
}
|
||
}
|
||
|
||
func (r roomRPSConfigRequest) toProto() (*gamev1.RoomRPSConfig, error) {
|
||
status := strings.TrimSpace(r.Status)
|
||
if status == "" {
|
||
// 业务功能配置默认打开;后台没传 status 时不能默认为关闭,否则新环境上线后 App 入口会被静默隐藏。
|
||
status = "active"
|
||
}
|
||
timeoutMS := r.ChallengeTimeoutMS
|
||
if timeoutMS <= 0 {
|
||
// 无人应战超时使用产品默认 10 分钟;0 或负数按“未填写”处理,不允许保存成立刻过期。
|
||
timeoutMS = 10 * 60 * 1000
|
||
}
|
||
revealMS := r.RevealCountdownMS
|
||
if revealMS <= 0 {
|
||
// 揭晓倒计时使用产品默认 3 秒;客户端动画和 IM 倒计时都依赖这个稳定默认值。
|
||
revealMS = 3 * 1000
|
||
}
|
||
if len(r.StakeGifts) != 4 {
|
||
// 房内猜拳礼物档位是产品固定四档,少一档会导致 Flutter 面板缺位,多一档会导致后台和客户端展示不一致。
|
||
return nil, fmt.Errorf("房内猜拳礼物档位必须配置 4 档")
|
||
}
|
||
gifts := make([]*gamev1.RoomRPSStakeGift, 0, len(r.StakeGifts))
|
||
seen := make(map[string]struct{}, len(r.StakeGifts))
|
||
for index, gift := range r.StakeGifts {
|
||
giftID := strings.TrimSpace(gift.GiftID)
|
||
if giftID == "" {
|
||
// gift_id 是后台礼物表的真实主键,当前礼物体系允许字符串;空值不能表示“空档位”,空档位应该通过 enabled=false 表达。
|
||
return nil, fmt.Errorf("第 %d 档礼物 ID 不正确", index+1)
|
||
}
|
||
if _, ok := seen[giftID]; ok {
|
||
// 同一个礼物不能重复配置成多档,否则结算金额和排序展示会变成同一个 gift_id 的多份事实。
|
||
return nil, fmt.Errorf("第 %d 档礼物 ID 重复", index+1)
|
||
}
|
||
seen[giftID] = struct{}{}
|
||
sortOrder := gift.SortOrder
|
||
if sortOrder <= 0 {
|
||
// 未显式给排序时按提交顺序落库,保证前端拖拽顺序和运行时展示顺序一致。
|
||
sortOrder = int32(index + 1)
|
||
}
|
||
gifts = append(gifts, &gamev1.RoomRPSStakeGift{
|
||
GiftId: legacyRoomRPSGiftID(giftID),
|
||
GiftIdText: giftID,
|
||
Enabled: gift.Enabled,
|
||
SortOrder: sortOrder,
|
||
})
|
||
}
|
||
return &gamev1.RoomRPSConfig{
|
||
// 房内猜拳和独立猜拳必须用不同 game_id;这里固定 room_rps,防止后台误把配置写到独立猜拳。
|
||
GameId: "room_rps",
|
||
Status: status,
|
||
ChallengeTimeoutMs: timeoutMS,
|
||
RevealCountdownMs: revealMS,
|
||
StakeGifts: gifts,
|
||
}, nil
|
||
}
|
||
|
||
func legacyRoomRPSGiftID(giftID string) int64 {
|
||
value, err := strconv.ParseInt(strings.TrimSpace(giftID), 10, 64)
|
||
if err != nil || value <= 0 {
|
||
return 0
|
||
}
|
||
return value
|
||
}
|
||
|
||
func compactTags(tags []string) []string {
|
||
out := make([]string, 0, len(tags))
|
||
seen := make(map[string]struct{}, len(tags))
|
||
for _, tag := range tags {
|
||
value := strings.TrimSpace(tag)
|
||
if value == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[value]; ok {
|
||
continue
|
||
}
|
||
seen[value] = struct{}{}
|
||
out = append(out, value)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func requestMeta(c *gin.Context) *gamev1.RequestMeta {
|
||
return &gamev1.RequestMeta{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
Caller: "admin-server",
|
||
ActorUserId: int64(middleware.CurrentUserID(c)),
|
||
SentAtMs: time.Now().UnixMilli(),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
}
|
||
}
|