264 lines
8.7 KiB
Go

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 strings.TrimSpace(req.GameID) == "" {
return nil, NewAppError(http.StatusBadRequest, "missing_game_id", "gameId is required")
}
row, err := s.findGameRow(ctx, user.SysOrigin, req.GameID)
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(s.cfg.Baishun.LaunchCodeTTLSeconds) * time.Second),
Language: s.resolveLanguage(ctx, user.UserID),
GSP: s.resolveGSP(row),
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{
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: s.cfg.Baishun.AppName,
AppChannel: defaultIfBlank(s.cfg.Baishun.AppChannel, "skychat"),
AppID: s.cfg.Baishun.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
}
// 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
}