2026-06-11 01:02:16 +08:00

893 lines
36 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"
)
// Server 把 game-service 用例层适配为 gRPC 契约。
type Server struct {
gamev1.UnimplementedGameAppServiceServer
gamev1.UnimplementedGameCallbackServiceServer
gamev1.UnimplementedGameAdminServiceServer
gamev1.UnimplementedGameCronServiceServer
svc *gameservice.Service
diceSvc *diceservice.Service
}
func NewServer(svc *gameservice.Service, diceServices ...*diceservice.Service) *Server {
// diceSvc 用可选参数接入,保留旧测试和非骰子场景的构造方式;生产 app 会显式传入骰子服务。
var diceSvc *diceservice.Service
if len(diceServices) > 0 {
diceSvc = diceServices[0]
}
return &Server{svc: svc, diceSvc: diceSvc}
}
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) 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(),
})
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(),
})
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(),
})
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(),
})
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(),
})
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(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &gamev1.DiceMatchResponse{Match: diceMatchToProtoAt(match, serverTimeMS), 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,
}
}
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 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
}