2026-06-11 18:14:04 +08:00

1045 lines
44 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package grpc
import (
"context"
"strings"
"time"
gamev1 "hyapp.local/api/proto/game/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
dicedomain "hyapp/services/game-service/internal/domain/dice"
gamedomain "hyapp/services/game-service/internal/domain/game"
diceservice "hyapp/services/game-service/internal/service/dice"
gameservice "hyapp/services/game-service/internal/service/game"
roomrpsservice "hyapp/services/game-service/internal/service/roomrps"
)
// Server 把 game-service 用例层适配为 gRPC 契约。
type Server struct {
gamev1.UnimplementedGameAppServiceServer
gamev1.UnimplementedGameCallbackServiceServer
gamev1.UnimplementedGameAdminServiceServer
gamev1.UnimplementedGameCronServiceServer
svc *gameservice.Service
diceSvc *diceservice.Service
roomRPSSvc *roomrpsservice.Service
}
func NewServer(svc *gameservice.Service, extras ...any) *Server {
// extras 用类型分派保留旧测试构造方式;生产 app 会同时传入骰子服务和房内猜拳服务。
var (
diceSvc *diceservice.Service
roomRPSSvc *roomrpsservice.Service
)
for _, extra := range extras {
switch typed := extra.(type) {
case *diceservice.Service:
diceSvc = typed
case *roomrpsservice.Service:
roomRPSSvc = typed
}
}
return &Server{svc: svc, diceSvc: diceSvc, roomRPSSvc: roomRPSSvc}
}
func (s *Server) ListGames(ctx context.Context, req *gamev1.ListGamesRequest) (*gamev1.ListGamesResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, serverTimeMS, err := s.svc.ListGames(ctx, gameservice.ListGamesQuery{
AppCode: req.GetMeta().GetAppCode(),
UserID: req.GetUserId(),
Scene: req.GetScene(),
RoomID: req.GetRoomId(),
RegionID: req.GetRegionId(),
Language: req.GetLanguage(),
ClientPlatform: req.GetClientPlatform(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListGamesResponse{ServerTimeMs: serverTimeMS, Games: make([]*gamev1.AppGame, 0, len(items))}
for _, item := range items {
resp.Games = append(resp.Games, appGameToProto(item))
}
return resp, nil
}
func (s *Server) ListRecentGames(ctx context.Context, req *gamev1.ListRecentGamesRequest) (*gamev1.ListGamesResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, serverTimeMS, err := s.svc.ListRecentGames(ctx, gameservice.ListRecentGamesQuery{
AppCode: req.GetMeta().GetAppCode(),
UserID: req.GetUserId(),
Scene: req.GetScene(),
RoomID: req.GetRoomId(),
RegionID: req.GetRegionId(),
Language: req.GetLanguage(),
ClientPlatform: req.GetClientPlatform(),
PageSize: req.GetPageSize(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListGamesResponse{ServerTimeMs: serverTimeMS, Games: make([]*gamev1.AppGame, 0, len(items))}
for _, item := range items {
resp.Games = append(resp.Games, appGameToProto(item))
}
return resp, nil
}
func (s *Server) ListExploreWinners(ctx context.Context, req *gamev1.ListExploreWinnersRequest) (*gamev1.ListExploreWinnersResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, serverTimeMS, err := s.diceSvc.ListExploreWinners(ctx, req.GetMeta().GetAppCode(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListExploreWinnersResponse{ServerTimeMs: serverTimeMS, Winners: make([]*gamev1.ExploreWinner, 0, len(items))}
for _, item := range items {
resp.Winners = append(resp.Winners, exploreWinnerToProto(item))
}
return resp, nil
}
func (s *Server) GetBridgeScript(ctx context.Context, req *gamev1.GetBridgeScriptRequest) (*gamev1.GetBridgeScriptResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
result, err := s.svc.GetBridgeScript(ctx, req.GetMeta().GetAppCode())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.GetBridgeScriptResponse{
Config: &gamev1.GameBridgeScriptConfig{
BridgeScriptUrl: result.URL,
BridgeScriptVersion: result.Version,
BridgeScriptSha256: result.SHA256,
},
ServerTimeMs: result.ServerTimeMS,
}, nil
}
func (s *Server) LaunchGame(ctx context.Context, req *gamev1.LaunchGameRequest) (*gamev1.LaunchGameResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
result, err := s.svc.LaunchGame(ctx, gameservice.LaunchCommand{
AppCode: req.GetMeta().GetAppCode(),
RequestID: req.GetMeta().GetRequestId(),
UserID: req.GetUserId(),
DisplayUserID: req.GetDisplayUserId(),
GameID: req.GetGameId(),
Scene: req.GetScene(),
RoomID: req.GetRoomId(),
AccessToken: req.GetAccessToken(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.LaunchGameResponse{
SessionId: result.SessionID,
LaunchUrl: result.LaunchURL,
ExpiresAtMs: result.ExpiresAtMS,
Orientation: result.Orientation,
SafeHeight: result.SafeHeight,
ServerTimeMs: result.ServerTimeMS,
}, nil
}
func (s *Server) GetDiceConfig(ctx context.Context, req *gamev1.GetDiceConfigRequest) (*gamev1.DiceConfigResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
config, serverTimeMS, err := s.diceSvc.GetConfig(ctx, req.GetMeta().GetAppCode(), req.GetGameId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceConfigResponse{Config: diceConfigToProto(config), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) MatchDice(ctx context.Context, req *gamev1.MatchDiceRequest) (*gamev1.DiceMatchResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
match, serverTimeMS, err := s.diceSvc.Match(ctx, diceservice.MatchCommand{
AppCode: req.GetMeta().GetAppCode(),
RequestID: req.GetMeta().GetRequestId(),
UserID: req.GetUserId(),
GameID: req.GetGameId(),
RoomID: req.GetRoomId(),
RegionID: req.GetRegionId(),
StakeCoin: req.GetStakeCoin(),
RPSGesture: req.GetRpsGesture(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) CreateDiceMatch(ctx context.Context, req *gamev1.CreateDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
// gRPC 层只把 proto 字段转成 command人数、金额、游戏状态和钱包逻辑都留给 service 层验证。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
match, serverTimeMS, err := s.diceSvc.CreateMatch(ctx, diceservice.CreateMatchCommand{
AppCode: req.GetMeta().GetAppCode(),
RequestID: req.GetMeta().GetRequestId(),
UserID: req.GetUserId(),
GameID: req.GetGameId(),
RoomID: req.GetRoomId(),
RegionID: req.GetRegionId(),
StakeCoin: req.GetStakeCoin(),
MinPlayers: req.GetMinPlayers(),
MaxPlayers: req.GetMaxPlayers(),
RPSGesture: req.GetRpsGesture(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) JoinDiceMatch(ctx context.Context, req *gamev1.JoinDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
// user_id 由 gateway 从登录态注入transport 不允许用 actor_user_id 推导业务用户,避免调用方混淆身份字段。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
match, serverTimeMS, err := s.diceSvc.JoinMatch(ctx, diceservice.JoinMatchCommand{
AppCode: req.GetMeta().GetAppCode(),
RequestID: req.GetMeta().GetRequestId(),
UserID: req.GetUserId(),
MatchID: req.GetMatchId(),
GameID: req.GetGameId(),
RPSGesture: req.GetRpsGesture(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) GetDiceMatch(ctx context.Context, req *gamev1.GetDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
// 查询接口仍要求 user_id后续如果要做观战/隐私控制,可以在 service 层基于同一 command 加规则。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
match, serverTimeMS, err := s.diceSvc.GetMatch(ctx, diceservice.GetMatchCommand{
AppCode: req.GetMeta().GetAppCode(),
UserID: req.GetUserId(),
MatchID: req.GetMatchId(),
GameID: req.GetGameId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) RollDiceMatch(ctx context.Context, req *gamev1.RollDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
// roll 是唯一会触发钱包改账的骰子 RPCgRPC 层不拆分扣款/派奖,避免客户端绕过局状态机。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
match, serverTimeMS, err := s.diceSvc.RollMatch(ctx, diceservice.RollMatchCommand{
AppCode: req.GetMeta().GetAppCode(),
RequestID: req.GetMeta().GetRequestId(),
UserID: req.GetUserId(),
MatchID: req.GetMatchId(),
GameID: req.GetGameId(),
RPSGesture: req.GetRpsGesture(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) CancelDiceMatch(ctx context.Context, req *gamev1.CancelDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
match, serverTimeMS, err := s.diceSvc.CancelMatch(ctx, diceservice.CancelMatchCommand{
AppCode: req.GetMeta().GetAppCode(),
RequestID: req.GetMeta().GetRequestId(),
UserID: req.GetUserId(),
MatchID: req.GetMatchId(),
GameID: req.GetGameId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) GetRoomRPSConfig(ctx context.Context, req *gamev1.GetRoomRPSConfigRequest) (*gamev1.RoomRPSConfigResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 配置只按 meta.app_code 读取App/Admin 都不能在 body 里传 app_code 读写其它租户配置。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
config, serverTimeMS, err := s.roomRPSSvc.GetConfig(ctx, req.GetMeta().GetAppCode())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSConfigResponse{Config: config, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) ListRoomRPSChallenges(ctx context.Context, req *gamev1.ListRoomRPSChallengesRequest) (*gamev1.ListRoomRPSChallengesResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 列表参数完整透传给 serviceroom_id、状态、用户筛选、时间窗和游标必须在同一个事实源内判断。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, nextCursor, pageSize, serverTimeMS, err := s.roomRPSSvc.ListChallenges(ctx, req)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.ListRoomRPSChallengesResponse{Challenges: items, NextCursor: nextCursor, PageSize: pageSize, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) CreateRoomRPSChallenge(ctx context.Context, req *gamev1.CreateRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 发起人身份、房间、礼物和手势全部交给 service 原子校验transport 不在这里复制状态规则。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
challenge, serverTimeMS, err := s.roomRPSSvc.CreateChallenge(ctx, req)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSChallengeResponse{Challenge: challenge, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) AcceptRoomRPSChallenge(ctx context.Context, req *gamev1.AcceptRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 应战入口只做 proto 到 service 的调用pending 锁定、本人不能应战、超时收敛和胜负结算都在 service 内完成。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
challenge, serverTimeMS, err := s.roomRPSSvc.AcceptChallenge(ctx, req)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSChallengeResponse{Challenge: challenge, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) GetRoomRPSChallenge(ctx context.Context, req *gamev1.GetRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 详情是 IM 事件后的权威补拉接口transport 不根据 user_id 推断展示内容,统一交给 service 返回事实。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
challenge, serverTimeMS, err := s.roomRPSSvc.GetChallenge(ctx, req.GetMeta().GetAppCode(), req.GetChallengeId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSChallengeResponse{Challenge: challenge, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) UpdateRoomRPSConfig(ctx context.Context, req *gamev1.UpdateRoomRPSConfigRequest) (*gamev1.RoomRPSConfigResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 后台写配置只落 room_rps 独立配置域;固定四档礼物、默认开启和倒计时默认值由 service 统一归一。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
config, serverTimeMS, err := s.roomRPSSvc.UpdateConfig(ctx, req.GetMeta().GetAppCode(), req.GetConfig())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSConfigResponse{Config: config, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) RetryRoomRPSSettlement(ctx context.Context, req *gamev1.RetryRoomRPSSettlementRequest) (*gamev1.RoomRPSChallengeResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 结算重试只能从 settlement_failed 进入transport 不允许 admin 传目标状态,避免后台绕过状态机。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
challenge, serverTimeMS, err := s.roomRPSSvc.RetrySettlement(ctx, req.GetMeta().GetAppCode(), req.GetChallengeId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSChallengeResponse{Challenge: challenge, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) ExpireRoomRPSChallenge(ctx context.Context, req *gamev1.ExpireRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
if s == nil || s.roomRPSSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "room rps service is not configured"))
}
// 手动过期只允许 still-pending 且无人应战的挑战;服务端重查事实,不能相信后台按钮状态。
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
challenge, serverTimeMS, err := s.roomRPSSvc.ExpireChallenge(ctx, req.GetMeta().GetAppCode(), req.GetChallengeId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.RoomRPSChallengeResponse{Challenge: challenge, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) HandleCallback(ctx context.Context, req *gamev1.CallbackRequest) (*gamev1.CallbackResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
raw, contentType, err := s.svc.HandleCallback(ctx, req)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.CallbackResponse{RawBody: raw, ContentType: contentType}, nil
}
// ProcessLevelEventOutboxBatch 只由 cron-service 调用,批量接管并投递游戏消耗等级事件。
func (s *Server) ProcessLevelEventOutboxBatch(ctx context.Context, req *gamev1.CronBatchRequest) (*gamev1.CronBatchResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
claimed, processed, success, failure, hasMore, err := s.svc.ProcessLevelEventOutboxBatch(ctx, req.GetRunId(), req.GetWorkerId(), int(req.GetBatchSize()), durationFromMillis(req.GetLockTtlMs()))
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.CronBatchResponse{
ClaimedCount: claimed,
ProcessedCount: processed,
SuccessCount: success,
FailureCount: failure,
HasMore: hasMore,
}, nil
}
func (s *Server) ListPlatforms(ctx context.Context, req *gamev1.ListPlatformsRequest) (*gamev1.ListPlatformsResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, err := s.svcRepository().ListPlatforms(ctx, req.GetMeta().GetAppCode(), req.GetStatus())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListPlatformsResponse{ServerTimeMs: time.Now().UnixMilli(), Platforms: make([]*gamev1.GamePlatform, 0, len(items))}
for _, item := range items {
resp.Platforms = append(resp.Platforms, platformToProto(item))
}
return resp, nil
}
func (s *Server) UpsertPlatform(ctx context.Context, req *gamev1.UpsertPlatformRequest) (*gamev1.PlatformResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
platform := platformFromProto(req.GetPlatform())
platform.AppCode = req.GetMeta().GetAppCode()
normalized, err := normalizePlatform(platform)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
saved, err := s.svcRepository().UpsertPlatform(ctx, normalized)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.PlatformResponse{Platform: platformToProto(saved), ServerTimeMs: time.Now().UnixMilli()}, nil
}
func (s *Server) ListCatalog(ctx context.Context, req *gamev1.ListCatalogRequest) (*gamev1.ListCatalogResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, next, err := s.svcRepository().ListCatalog(ctx, gameservice.ListCatalogQuery{
AppCode: req.GetMeta().GetAppCode(),
PlatformCode: req.GetPlatformCode(),
Status: req.GetStatus(),
PageSize: req.GetPageSize(),
Cursor: req.GetCursor(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListCatalogResponse{NextCursor: next, ServerTimeMs: time.Now().UnixMilli(), Games: make([]*gamev1.GameCatalogItem, 0, len(items))}
for _, item := range items {
resp.Games = append(resp.Games, catalogToProto(item))
}
return resp, nil
}
func (s *Server) UpsertCatalog(ctx context.Context, req *gamev1.UpsertCatalogRequest) (*gamev1.CatalogResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
item := catalogFromProto(req.GetGame())
item.AppCode = req.GetMeta().GetAppCode()
normalized, err := normalizeCatalog(item)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
saved, err := s.svcRepository().UpsertCatalog(ctx, normalized)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.CatalogResponse{Game: catalogToProto(saved), ServerTimeMs: time.Now().UnixMilli()}, nil
}
func (s *Server) SetGameStatus(ctx context.Context, req *gamev1.SetGameStatusRequest) (*gamev1.CatalogResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
status := normalizeStatus(req.GetStatus())
if status == "" {
return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "status is invalid"))
}
item, err := s.svcRepository().SetGameStatus(ctx, req.GetMeta().GetAppCode(), req.GetGameId(), status, time.Now().UnixMilli())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.CatalogResponse{Game: catalogToProto(item), ServerTimeMs: time.Now().UnixMilli()}, nil
}
func (s *Server) DeleteCatalog(ctx context.Context, req *gamev1.DeleteCatalogRequest) (*gamev1.DeleteCatalogResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
if err := s.svcRepository().DeleteCatalog(ctx, req.GetMeta().GetAppCode(), req.GetGameId()); err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DeleteCatalogResponse{ServerTimeMs: time.Now().UnixMilli()}, nil
}
func (s *Server) ListSelfGames(ctx context.Context, req *gamev1.ListSelfGamesRequest) (*gamev1.ListSelfGamesResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, serverTimeMS, err := s.diceSvc.ListSelfGames(ctx, req.GetMeta().GetAppCode())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListSelfGamesResponse{ServerTimeMs: serverTimeMS, Games: make([]*gamev1.DiceConfig, 0, len(items))}
for _, item := range items {
resp.Games = append(resp.Games, diceConfigToProto(item))
}
return resp, nil
}
func (s *Server) UpdateDiceConfig(ctx context.Context, req *gamev1.UpdateDiceConfigRequest) (*gamev1.DiceConfigResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
config := diceConfigFromProto(req.GetConfig())
config.AppCode = req.GetMeta().GetAppCode()
saved, serverTimeMS, err := s.diceSvc.UpdateConfig(ctx, config)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceConfigResponse{Config: diceConfigToProto(saved), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) AdjustDicePool(ctx context.Context, req *gamev1.AdjustDicePoolRequest) (*gamev1.AdjustDicePoolResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
adjustment, config, serverTimeMS, err := s.diceSvc.AdjustPool(ctx, dicedomain.PoolAdjustment{
AppCode: req.GetMeta().GetAppCode(),
GameID: req.GetGameId(),
Direction: req.GetDirection(),
AmountCoin: req.GetAmountCoin(),
Reason: req.GetReason(),
CreatedBy: req.GetMeta().GetActorUserId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.AdjustDicePoolResponse{Adjustment: dicePoolAdjustmentToProto(adjustment), Config: diceConfigToProto(config), ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) ListDiceRobots(ctx context.Context, req *gamev1.ListDiceRobotsRequest) (*gamev1.ListDiceRobotsResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, next, serverTimeMS, err := s.diceSvc.ListRobots(ctx, req.GetMeta().GetAppCode(), req.GetGameId(), req.GetStatus(), req.GetPageSize(), req.GetCursor())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.ListDiceRobotsResponse{NextCursor: next, ServerTimeMs: serverTimeMS, Robots: make([]*gamev1.DiceRobot, 0, len(items))}
for _, item := range items {
resp.Robots = append(resp.Robots, diceRobotToProto(item))
}
return resp, nil
}
func (s *Server) RegisterDiceRobots(ctx context.Context, req *gamev1.RegisterDiceRobotsRequest) (*gamev1.RegisterDiceRobotsResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, serverTimeMS, err := s.diceSvc.RegisterRobots(ctx, req.GetMeta().GetAppCode(), req.GetGameId(), req.GetUserIds(), req.GetMeta().GetActorUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &gamev1.RegisterDiceRobotsResponse{ServerTimeMs: serverTimeMS, Robots: make([]*gamev1.DiceRobot, 0, len(items))}
for _, item := range items {
resp.Robots = append(resp.Robots, diceRobotToProto(item))
}
return resp, nil
}
func (s *Server) SetDiceRobotStatus(ctx context.Context, req *gamev1.SetDiceRobotStatusRequest) (*gamev1.DiceRobotResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
item, serverTimeMS, err := s.diceSvc.SetRobotStatus(ctx, req.GetMeta().GetAppCode(), req.GetGameId(), req.GetUserId(), req.GetStatus())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceRobotResponse{Robot: diceRobotToProto(item), ServerTimeMs: serverTimeMS}, nil
}
// DeleteDiceRobot 只对管理端暴露机器人池删除能力,删除范围由 app_code、game_id、user_id 三元组限定。
func (s *Server) DeleteDiceRobot(ctx context.Context, req *gamev1.DeleteDiceRobotRequest) (*gamev1.DeleteDiceRobotResponse, error) {
if s == nil || s.diceSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "dice service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
serverTimeMS, err := s.diceSvc.DeleteRobot(ctx, req.GetMeta().GetAppCode(), req.GetGameId(), req.GetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DeleteDiceRobotResponse{Deleted: true, ServerTimeMs: serverTimeMS}, nil
}
func (s *Server) svcRepository() gameservice.Repository {
if s == nil || s.svc == nil || s.svc.Repository() == nil {
return unavailableRepository{}
}
return s.svc.Repository()
}
type unavailableRepository struct{}
func (unavailableRepository) Ping(context.Context) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListGames(context.Context, gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListRecentGames(context.Context, gameservice.ListRecentGamesQuery) ([]gamedomain.AppGame, error) {
return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetLaunchableGame(context.Context, string, string) (gamedomain.LaunchableGame, error) {
return gamedomain.LaunchableGame{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetPlatform(context.Context, string, string) (gamedomain.Platform, error) {
return gamedomain.Platform{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) CreateLaunchSession(context.Context, gamedomain.LaunchSession) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetLaunchSessionByToken(context.Context, string, string) (gamedomain.LaunchSession, error) {
return gamedomain.LaunchSession{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetLaunchSessionByTokenScope(context.Context, string, string, string, string) (gamedomain.LaunchSession, error) {
return gamedomain.LaunchSession{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) InsertCallbackLog(context.Context, gamedomain.CallbackLog) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) CreateGameOrder(context.Context, gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
return gamedomain.GameOrder{}, false, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkOrderSucceeded(context.Context, string, string, string, int64, int64) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkOrderFailed(context.Context, string, string, string, string, string, int64) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) CreateRepairOrder(context.Context, gamedomain.RepairOrder) (gamedomain.RepairOrder, bool, error) {
return gamedomain.RepairOrder{}, false, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ClaimPendingLevelEvents(context.Context, string, int64, time.Duration, int) ([]gamedomain.LevelEventOutbox, error) {
return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkLevelEventDelivered(context.Context, string, int64) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkLevelEventFailed(context.Context, string, string, int64, int64) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListPlatforms(context.Context, string, string) ([]gamedomain.Platform, error) {
return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) UpsertPlatform(context.Context, gamedomain.Platform) (gamedomain.Platform, error) {
return gamedomain.Platform{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListCatalog(context.Context, gameservice.ListCatalogQuery) ([]gamedomain.CatalogItem, string, error) {
return nil, "", xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) UpsertCatalog(context.Context, gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
return gamedomain.CatalogItem{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) SetGameStatus(context.Context, string, string, string, int64) (gamedomain.CatalogItem, error) {
return gamedomain.CatalogItem{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) DeleteCatalog(context.Context, string, string) error {
return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func appGameToProto(item gamedomain.AppGame) *gamev1.AppGame {
return &gamev1.AppGame{
GameId: item.GameID,
PlatformCode: item.PlatformCode,
NameKey: item.NameKey,
Name: item.Name,
IconUrl: item.IconURL,
CoverUrl: item.CoverURL,
Category: item.Category,
LaunchMode: item.LaunchMode,
Orientation: item.Orientation,
SafeHeight: item.SafeHeight,
MinCoin: item.MinCoin,
Enabled: item.Enabled,
Maintenance: item.Maintenance,
SortOrder: item.SortOrder,
}
}
func diceMatchToProto(match dicedomain.Match) *gamev1.DiceMatch {
return diceMatchToProtoAt(match, time.Now().UnixMilli())
}
func diceMatchToProtoAt(match dicedomain.Match, nowMS int64) *gamev1.DiceMatch {
participants := make([]*gamev1.DiceParticipant, 0, len(match.Participants))
for _, participant := range match.Participants {
participants = append(participants, diceParticipantToProto(participant))
}
phase, phaseDeadlineMS := dicePhase(match, nowMS)
// proto 只暴露客户端需要的局事实;订单 ID 和 provider 快照留在 MySQL避免把账务内部键泄漏到 H5。
return &gamev1.DiceMatch{
AppCode: match.AppCode,
MatchId: match.MatchID,
GameId: match.GameID,
RoomId: match.RoomID,
RegionId: match.RegionID,
MinPlayers: match.MinPlayers,
MaxPlayers: match.MaxPlayers,
CurrentPlayers: match.CurrentPlayers,
StakeCoin: match.StakeCoin,
RoundNo: match.RoundNo,
Status: match.Status,
Result: match.Result,
Participants: participants,
JoinDeadlineMs: match.JoinDeadlineMS,
CreatedAtMs: match.CreatedAtMS,
UpdatedAtMs: match.UpdatedAtMS,
SettledAtMs: match.SettledAtMS,
Phase: phase,
PhaseDeadlineMs: phaseDeadlineMS,
FeeBps: match.FeeBPS,
PoolBps: match.PoolBPS,
MatchMode: match.MatchMode,
ForcedResult: match.ForcedResult,
ReadyAtMs: match.ReadyAtMS,
CanceledAtMs: match.CanceledAtMS,
PoolDeltaCoin: match.PoolDeltaCoin,
}
}
func diceParticipantToProto(participant dicedomain.Participant) *gamev1.DiceParticipant {
// balance_after 是最近一次钱包操作后的快照,供 H5 结算弹层刷新余额;不是局内可计算字段。
return &gamev1.DiceParticipant{
UserId: participant.UserID,
SeatNo: participant.SeatNo,
Status: participant.Status,
StakeCoin: participant.StakeCoin,
DicePoints: participant.DicePoints,
Result: participant.Result,
PayoutCoin: participant.PayoutCoin,
BalanceAfter: participant.BalanceAfter,
JoinedAtMs: participant.JoinedAtMS,
UpdatedAtMs: participant.UpdatedAtMS,
ParticipantType: participant.ParticipantType,
IsRobot: participant.ParticipantType == dicedomain.ParticipantTypeRobot,
RpsGesture: participant.RPSGesture,
}
}
func diceConfigToProto(config dicedomain.Config) *gamev1.DiceConfig {
options := make([]*gamev1.DiceStakeOption, 0, len(config.StakeOptions))
for _, option := range config.StakeOptions {
options = append(options, &gamev1.DiceStakeOption{StakeCoin: option.StakeCoin, Enabled: option.Enabled, SortOrder: option.SortOrder})
}
return &gamev1.DiceConfig{
AppCode: config.AppCode,
GameId: config.GameID,
Status: config.Status,
StakeOptions: options,
FeeBps: config.FeeBPS,
PoolBps: config.PoolBPS,
MinPlayers: config.MinPlayers,
MaxPlayers: config.MaxPlayers,
RobotEnabled: config.RobotEnabled,
RobotMatchWaitMs: config.RobotMatchWaitMS,
PoolBalanceCoin: config.PoolBalanceCoin,
CreatedAtMs: config.CreatedAtMS,
UpdatedAtMs: config.UpdatedAtMS,
}
}
func diceConfigFromProto(config *gamev1.DiceConfig) dicedomain.Config {
if config == nil {
return dicedomain.Config{}
}
options := make([]dicedomain.StakeOption, 0, len(config.GetStakeOptions()))
for _, option := range config.GetStakeOptions() {
options = append(options, dicedomain.StakeOption{StakeCoin: option.GetStakeCoin(), Enabled: option.GetEnabled(), SortOrder: option.GetSortOrder()})
}
return dicedomain.Config{
AppCode: config.GetAppCode(),
GameID: config.GetGameId(),
Status: config.GetStatus(),
StakeOptions: options,
FeeBPS: config.GetFeeBps(),
PoolBPS: config.GetPoolBps(),
MinPlayers: config.GetMinPlayers(),
MaxPlayers: config.GetMaxPlayers(),
RobotEnabled: config.GetRobotEnabled(),
RobotMatchWaitMS: config.GetRobotMatchWaitMs(),
}
}
func dicePoolAdjustmentToProto(adjustment dicedomain.PoolAdjustment) *gamev1.DicePoolAdjustment {
return &gamev1.DicePoolAdjustment{
AdjustmentId: adjustment.AdjustmentID,
GameId: adjustment.GameID,
AmountCoin: adjustment.AmountCoin,
Direction: adjustment.Direction,
Reason: adjustment.Reason,
BalanceAfter: adjustment.BalanceAfter,
CreatedAtMs: adjustment.CreatedAtMS,
}
}
func diceRobotToProto(robot dicedomain.Robot) *gamev1.DiceRobot {
return &gamev1.DiceRobot{
AppCode: robot.AppCode,
GameId: robot.GameID,
UserId: robot.UserID,
Status: robot.Status,
CreatedByAdminId: robot.CreatedByAdminID,
CreatedAtMs: robot.CreatedAtMS,
UpdatedAtMs: robot.UpdatedAtMS,
}
}
func exploreWinnerToProto(item dicedomain.ExploreWinner) *gamev1.ExploreWinner {
return &gamev1.ExploreWinner{
WinId: item.WinID,
GameCode: item.GameCode,
GameId: item.GameID,
UserId: item.UserID,
DisplayName: item.DisplayName,
AvatarUrl: item.AvatarURL,
CoinAmount: item.CoinAmount,
WonAtMs: item.WonAtMS,
}
}
func dicePhase(match dicedomain.Match, nowMS int64) (string, int64) {
switch match.Status {
case dicedomain.MatchStatusCanceled:
return dicedomain.MatchPhaseCanceled, match.CanceledAtMS
case dicedomain.MatchStatusFailed:
return dicedomain.MatchPhaseFailed, match.UpdatedAtMS
case dicedomain.MatchStatusSettled:
return dicedomain.MatchPhaseSettled, match.SettledAtMS + dicedomain.SettlementShowMillis
}
if match.Result == dicedomain.ParticipantResultDraw {
// 和局不是终局,只给客户端一个 2 秒比点展示窗口,窗口结束后 H5 可以继续调用 roll。
return dicedomain.MatchPhaseComparing, match.UpdatedAtMS + dicedomain.DrawRerollMillis
}
if match.ReadyAtMS <= 0 {
return dicedomain.MatchPhaseWaiting, match.JoinDeadlineMS
}
countdownEnd := match.ReadyAtMS + dicedomain.CountdownMillis
rollingEnd := countdownEnd + dicedomain.RollingMillis
comparingEnd := rollingEnd + dicedomain.ComparingMillis
if nowMS < countdownEnd {
return dicedomain.MatchPhaseCountdown, countdownEnd
}
if nowMS < rollingEnd {
return dicedomain.MatchPhaseRolling, rollingEnd
}
return dicedomain.MatchPhaseComparing, comparingEnd
}
func platformToProto(item gamedomain.Platform) *gamev1.GamePlatform {
return &gamev1.GamePlatform{
AppCode: item.AppCode,
PlatformCode: item.PlatformCode,
PlatformName: item.PlatformName,
Status: item.Status,
ApiBaseUrl: item.APIBaseURL,
SortOrder: item.SortOrder,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
AdapterType: item.AdapterType,
// admin 后台要把当前厂商 key 提供给运营/对接人员核对;更新接口仍以空值表示保留旧密钥。
CallbackSecret: item.CallbackSecretCiphertext,
CallbackSecretSet: item.CallbackSecretConfigured,
CallbackIpWhitelist: item.CallbackIPWhitelist,
AdapterConfigJson: item.AdapterConfigJSON,
}
}
func platformFromProto(item *gamev1.GamePlatform) gamedomain.Platform {
if item == nil {
return gamedomain.Platform{}
}
return gamedomain.Platform{
AppCode: item.GetAppCode(),
PlatformCode: item.GetPlatformCode(),
PlatformName: item.GetPlatformName(),
Status: item.GetStatus(),
APIBaseURL: item.GetApiBaseUrl(),
AdapterType: item.GetAdapterType(),
// 更新接口只有在 callback_secret 非空时才会覆盖旧密钥,空值表示保留原密钥。
CallbackSecretCiphertext: item.GetCallbackSecret(),
CallbackSecretConfigured: item.GetCallbackSecretSet(),
CallbackIPWhitelist: item.GetCallbackIpWhitelist(),
AdapterConfigJSON: item.GetAdapterConfigJson(),
SortOrder: item.GetSortOrder(),
}
}
func catalogToProto(item gamedomain.CatalogItem) *gamev1.GameCatalogItem {
return &gamev1.GameCatalogItem{
AppCode: item.AppCode,
GameId: item.GameID,
PlatformCode: item.PlatformCode,
ProviderGameId: item.ProviderGameID,
GameName: item.GameName,
Category: item.Category,
IconUrl: item.IconURL,
CoverUrl: item.CoverURL,
LaunchMode: item.LaunchMode,
Orientation: item.Orientation,
SafeHeight: item.SafeHeight,
MinCoin: item.MinCoin,
Status: item.Status,
SortOrder: item.SortOrder,
Tags: item.Tags,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
func catalogFromProto(item *gamev1.GameCatalogItem) gamedomain.CatalogItem {
if item == nil {
return gamedomain.CatalogItem{}
}
return gamedomain.CatalogItem{
AppCode: item.GetAppCode(),
GameID: item.GetGameId(),
PlatformCode: item.GetPlatformCode(),
ProviderGameID: item.GetProviderGameId(),
GameName: item.GetGameName(),
Category: item.GetCategory(),
IconURL: item.GetIconUrl(),
CoverURL: item.GetCoverUrl(),
LaunchMode: item.GetLaunchMode(),
Orientation: item.GetOrientation(),
SafeHeight: item.GetSafeHeight(),
MinCoin: item.GetMinCoin(),
Status: item.GetStatus(),
SortOrder: item.GetSortOrder(),
Tags: item.GetTags(),
}
}
func normalizePlatform(item gamedomain.Platform) (gamedomain.Platform, error) {
// gRPC 边界只做通用字段清洗,厂商 JSON 的具体语义留给 adapter 解析。
item.PlatformCode = strings.TrimSpace(item.PlatformCode)
item.PlatformName = strings.TrimSpace(item.PlatformName)
item.Status = normalizeStatus(item.Status)
item.APIBaseURL = strings.TrimSpace(item.APIBaseURL)
item.AdapterType = normalizeAdapterType(item.AdapterType)
item.CallbackSecretCiphertext = strings.TrimSpace(item.CallbackSecretCiphertext)
item.CallbackIPWhitelist = compactStrings(item.CallbackIPWhitelist)
item.AdapterConfigJSON = strings.TrimSpace(item.AdapterConfigJSON)
if item.AdapterConfigJSON == "" {
item.AdapterConfigJSON = "{}"
}
if item.PlatformCode == "" || item.PlatformName == "" || item.Status == "" {
return gamedomain.Platform{}, xerr.New(xerr.InvalidArgument, "platform is incomplete")
}
if item.AdapterType == "" {
return gamedomain.Platform{}, xerr.New(xerr.InvalidArgument, "platform adapter_type is invalid")
}
return item, nil
}
func normalizeCatalog(item gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
item.GameID = strings.TrimSpace(item.GameID)
item.PlatformCode = strings.TrimSpace(item.PlatformCode)
item.ProviderGameID = strings.TrimSpace(item.ProviderGameID)
item.GameName = strings.TrimSpace(item.GameName)
item.Status = normalizeStatus(item.Status)
if item.LaunchMode == "" {
item.LaunchMode = gamedomain.LaunchModeFullScreen
}
if item.Orientation == "" {
item.Orientation = "portrait"
}
if item.SafeHeight < 0 {
item.SafeHeight = 0
}
if item.GameID == "" || item.PlatformCode == "" || item.ProviderGameID == "" || item.GameName == "" || item.Status == "" {
return gamedomain.CatalogItem{}, xerr.New(xerr.InvalidArgument, "game is incomplete")
}
return item, nil
}
func normalizeStatus(status string) string {
switch strings.ToLower(strings.TrimSpace(status)) {
case gamedomain.StatusActive:
return gamedomain.StatusActive
case gamedomain.StatusMaintenance:
return gamedomain.StatusMaintenance
case gamedomain.StatusDisabled:
return gamedomain.StatusDisabled
default:
return ""
}
}
func normalizeAdapterType(adapterType string) string {
// 允许的 adapter 必须在这里登记,后台传错值时直接拒绝,避免落库后回调走错协议。
switch strings.ToLower(strings.TrimSpace(adapterType)) {
case gamedomain.AdapterYomiV4:
return gamedomain.AdapterYomiV4
case gamedomain.AdapterLeaderCCV1:
return gamedomain.AdapterLeaderCCV1
case gamedomain.AdapterZeeOneV1:
return gamedomain.AdapterZeeOneV1
case gamedomain.AdapterBaishunV1:
return gamedomain.AdapterBaishunV1
case gamedomain.AdapterVivaGamesV1:
return gamedomain.AdapterVivaGamesV1
case gamedomain.AdapterReyouV1:
return gamedomain.AdapterReyouV1
default:
return ""
}
}
func compactStrings(values []string) []string {
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}
func durationFromMillis(value int64) time.Duration {
if value <= 0 {
return 0
}
return time.Duration(value) * time.Millisecond
}