1045 lines
44 KiB
Go
1045 lines
44 KiB
Go
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 是唯一会触发钱包改账的骰子 RPC;gRPC 层不拆分扣款/派奖,避免客户端绕过局状态机。
|
||
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"))
|
||
}
|
||
// 列表参数完整透传给 service;room_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
|
||
}
|