package baishun import ( "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "fmt" "net/http" "strconv" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) // LaunchGame 创建会话、拉起百顺游戏并更新房间状态。 func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req BaishunLaunchAppRequest, clientIP string) (*BaishunLaunchAppResponse, error) { if strings.TrimSpace(req.RoomID) == "" { return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required") } if req.ID <= 0 && strings.TrimSpace(req.GameID) == "" { return nil, NewAppError(http.StatusBadRequest, "missing_game_id", "id or gameId is required") } row, err := s.resolveLaunchGameRow(ctx, user.SysOrigin, req) if err != nil { return nil, err } runtimeCfg, err := s.requireProviderConfig(ctx, user.SysOrigin) if err != nil { return nil, err } sessionID, err := utils.NextID() if err != nil { return nil, err } sessionDBID, err := utils.NextID() if err != nil { return nil, err } roomStateID, err := utils.NextID() if err != nil { return nil, err } launchCode, err := s.randomToken(24) if err != nil { return nil, err } now := time.Now() session := model.BaishunLaunchSession{ ID: sessionDBID, SysOrigin: user.SysOrigin, RoomID: req.RoomID, UserID: user.UserID, InternalGameID: row.InternalGameID, VendorGameID: row.vendorGameIDInt(), GameSessionID: fmt.Sprintf("bs_%s_%d", sanitizeRoomID(req.RoomID), sessionID), LaunchCode: launchCode, LaunchCodeExpireTime: now.Add(time.Duration(runtimeCfg.launchCodeTTLSeconds()) * time.Second), Language: s.resolveLanguage(ctx, user.UserID), GSP: s.resolveGSP(row, runtimeCfg), CurrencyType: 0, CurrencyIcon: row.currencyIcon(), SceneMode: req.SceneMode, GameMode: row.gameMode(), PackageVersion: row.packageVersion(), ClientIP: clientIP, ClientOrigin: defaultIfBlank(req.ClientOrigin, "COMMON"), Status: baishunSessionCodeIssued, CreateTime: now, UpdateTime: now, } roomState := model.BaishunRoomState{ ID: roomStateID, SysOrigin: user.SysOrigin, RoomID: req.RoomID, HostUserID: user.UserID, CurrentGameID: row.InternalGameID, CurrentGameName: row.name(), CurrentGameCover: row.cover(), GameSessionID: session.GameSessionID, LaunchUserID: &user.UserID, State: baishunRoomStatePlaying, StartTime: &now, CreateTime: now, UpdateTime: now, } vendorGameID := row.vendorGameIDInt() roomState.CurrentVendorGameID = &vendorGameID if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(&session).Error; err != nil { return err } return tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "sys_origin"}, {Name: "room_id"}}, DoUpdates: clause.AssignmentColumns([]string{ "host_user_id", "current_game_id", "current_vendor_game_id", "current_game_name", "current_game_cover", "game_session_id", "launch_user_id", "state", "start_time", "end_time", "update_time", }), }).Create(&roomState).Error }); err != nil { return nil, err } return &BaishunLaunchAppResponse{ ID: row.ConfigID, GameSessionID: session.GameSessionID, VendorType: baishunVendorType, GameID: row.InternalGameID, VendorGameID: row.vendorGameIDInt(), Entry: BaishunLaunchEntry{ LaunchMode: row.launchMode(), EntryURL: row.entryURL(), PreviewURL: row.previewURL(), DownloadURL: row.downloadURL(), PackageVersion: row.packageVersion(), Orientation: row.orientation(), SafeHeight: row.safeHeight(), }, BridgeConfig: BaishunBridgeConfig{ AppName: runtimeCfg.AppName, AppChannel: runtimeCfg.appChannel(), AppID: runtimeCfg.AppID, UserID: strconv.FormatInt(user.UserID, 10), Code: launchCode, RoomID: req.RoomID, GameMode: strconv.Itoa(row.gameMode()), Language: session.Language, GSP: session.GSP, GameConfig: BaishunGameConfig{ SceneMode: req.SceneMode, CurrencyIcon: session.CurrencyIcon, }, }, RoomState: BaishunRoomStateResponse{ RoomID: req.RoomID, State: baishunRoomStatePlaying, GameSessionID: session.GameSessionID, CurrentGameID: row.InternalGameID, CurrentVendorGameID: row.vendorGameIDInt(), CurrentGameName: row.name(), CurrentGameCover: row.cover(), HostUserID: user.UserID, }, }, nil } func (s *BaishunService) resolveLaunchGameRow(ctx context.Context, sysOrigin string, req BaishunLaunchAppRequest) (gameListRow, error) { if req.ID > 0 { return s.findGameRowByConfigID(ctx, sysOrigin, req.ID) } return s.findGameRow(ctx, sysOrigin, req.GameID) } // CloseGame 关闭房间内当前游戏会话并回写房间状态。 func (s *BaishunService) CloseGame(ctx context.Context, user AuthUser, req BaishunCloseAppRequest) (*BaishunRoomStateResponse, error) { if strings.TrimSpace(req.RoomID) == "" { return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required") } now := time.Now() if strings.TrimSpace(req.GameSessionID) != "" { if err := s.repo.DB.WithContext(ctx). Model(&model.BaishunLaunchSession{}). Where("sys_origin = ? AND room_id = ? AND game_session_id = ?", user.SysOrigin, req.RoomID, req.GameSessionID). Updates(map[string]any{ "status": baishunSessionClosed, "closed_reason": defaultIfBlank(req.Reason, "user_exit"), "update_time": now, }).Error; err != nil { return nil, err } } if err := s.repo.DB.WithContext(ctx). Model(&model.BaishunRoomState{}). Where("sys_origin = ? AND room_id = ?", user.SysOrigin, req.RoomID). Updates(map[string]any{ "state": baishunRoomStateIdle, "end_time": now, "game_session_id": "", "current_game_id": "", "current_vendor_game_id": nil, "current_game_name": "", "current_game_cover": "", "update_time": now, }).Error; err != nil { return nil, err } return &BaishunRoomStateResponse{ RoomID: req.RoomID, State: baishunRoomStateIdle, }, nil } // RefreshRoomState 根据最新会话状态刷新房间当前挂载的游戏信息。 func (s *BaishunService) RefreshRoomState(ctx context.Context, sysOrigin, roomID string) (*BaishunRoomStateResponse, error) { var state model.BaishunRoomState if err := s.repo.DB.WithContext(ctx). Where("sys_origin = ? AND room_id = ?", sysOrigin, roomID). First(&state).Error; err != nil { if err == gorm.ErrRecordNotFound { return &BaishunRoomStateResponse{RoomID: roomID, State: baishunRoomStateIdle}, nil } return nil, err } if strings.TrimSpace(state.GameSessionID) == "" { state.State = baishunRoomStateIdle _ = s.repo.DB.WithContext(ctx).Model(&model.BaishunRoomState{}). Where("id = ?", state.ID). Update("state", baishunRoomStateIdle).Error } resp := &BaishunRoomStateResponse{ RoomID: roomID, State: defaultIfBlank(state.State, baishunRoomStateIdle), GameSessionID: state.GameSessionID, CurrentGameID: state.CurrentGameID, CurrentGameName: state.CurrentGameName, CurrentGameCover: state.CurrentGameCover, HostUserID: state.HostUserID, } if state.CurrentVendorGameID != nil { resp.CurrentVendorGameID = *state.CurrentVendorGameID } return resp, nil } // readUserInfo 读取用户资料,用于回调和启动配置。 func (s *BaishunService) readUserInfo(ctx context.Context, userID int64) integration.UserProfile { profile, err := s.java.GetUserProfile(ctx, userID) if err != nil { return integration.UserProfile{ID: integration.Int64Value(userID)} } return profile } // readUserBalance 读取用户金币余额。 func (s *BaishunService) readUserBalance(ctx context.Context, userID int64) int64 { balanceMap, err := s.java.MapGoldBalance(ctx, []int64{userID}) if err != nil { return 0 } return balanceMap[userID] } // resolveLanguage 解析用户语言,缺省回退英文。 func (s *BaishunService) resolveLanguage(ctx context.Context, userID int64) string { language, err := s.java.GetUserLanguage(ctx, userID) if err != nil || strings.TrimSpace(language) == "" { return "0" } return language } // findSessionByToken 按 ss_token 和用户查询会话。 func (s *BaishunService) findSessionByToken(ctx context.Context, ssToken, userID string) (model.BaishunLaunchSession, bool) { var session model.BaishunLaunchSession err := s.repo.DB.WithContext(ctx). Where("ss_token = ?", strings.TrimSpace(ssToken)). First(&session).Error if err != nil || time.Now().After(derefTime(session.SSTokenExpireTime)) { return model.BaishunLaunchSession{}, false } if strings.TrimSpace(userID) != "" && strconv.FormatInt(session.UserID, 10) != strings.TrimSpace(userID) { return model.BaishunLaunchSession{}, false } return session, true }