1409 lines
47 KiB
Go
1409 lines
47 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"chatapp3-golang/internal/config"
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/repo"
|
|
"chatapp3-golang/internal/util"
|
|
"context"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const (
|
|
baishunVendorType = "BAISHUN"
|
|
baishunLaunchModeRemote = "H5_REMOTE"
|
|
baishunLaunchModeLocalZip = "H5_ZIP_LOCAL"
|
|
baishunCatalogEnabled = "ENABLED"
|
|
baishunRoomStateIdle = "IDLE"
|
|
baishunRoomStatePlaying = "PLAYING"
|
|
baishunSessionInit = "INIT"
|
|
baishunSessionCodeIssued = "CODE_ISSUED"
|
|
baishunSessionTokenIssued = "TOKEN_ISSUED"
|
|
baishunSessionActive = "ACTIVE"
|
|
baishunSessionClosed = "CLOSED"
|
|
baishunSessionExpired = "EXPIRED"
|
|
baishunOrderStatusInit = "INIT"
|
|
baishunOrderStatusSuccess = "SUCCESS"
|
|
baishunOrderStatusFailed = "FAILED"
|
|
baishunCallbackStatusSuccess = "SUCCESS"
|
|
baishunCallbackStatusFailed = "FAILED"
|
|
baishunCodeInsufficientAssets = 1008
|
|
baishunCodeTokenExpired = 1009
|
|
baishunCodeSignatureError = 1010
|
|
baishunCodeRepeatUnknown = 1011
|
|
baishunCodeServerError = 1999
|
|
)
|
|
|
|
type BaishunService struct {
|
|
cfg config.Config
|
|
repo *repo.Repository
|
|
java *integration.Client
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewBaishunService(cfg config.Config, repository *repo.Repository, javaClient *integration.Client) *BaishunService {
|
|
return &BaishunService{
|
|
cfg: cfg,
|
|
repo: repository,
|
|
java: javaClient,
|
|
httpClient: &http.Client{
|
|
Timeout: cfg.Timeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
type RoomGameListItem struct {
|
|
GameID string `json:"gameId"`
|
|
VendorType string `json:"vendorType"`
|
|
VendorGameID int `json:"vendorGameId"`
|
|
Name string `json:"name"`
|
|
Cover string `json:"cover"`
|
|
Category string `json:"category,omitempty"`
|
|
Sort int64 `json:"sort,omitempty"`
|
|
LaunchMode string `json:"launchMode"`
|
|
FullScreen bool `json:"fullScreen"`
|
|
GameMode int `json:"gameMode"`
|
|
SafeHeight int `json:"safeHeight"`
|
|
Orientation int `json:"orientation"`
|
|
PackageVersion string `json:"packageVersion,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
type RoomGameListResponse struct {
|
|
Items []RoomGameListItem `json:"items"`
|
|
}
|
|
|
|
type BaishunRoomStateResponse struct {
|
|
RoomID string `json:"roomId"`
|
|
State string `json:"state"`
|
|
GameSessionID string `json:"gameSessionId,omitempty"`
|
|
CurrentGameID string `json:"currentGameId,omitempty"`
|
|
CurrentVendorGameID int `json:"currentVendorGameId,omitempty"`
|
|
CurrentGameName string `json:"currentGameName,omitempty"`
|
|
CurrentGameCover string `json:"currentGameCover,omitempty"`
|
|
HostUserID int64 `json:"hostUserId,omitempty"`
|
|
}
|
|
|
|
type BaishunLaunchAppRequest struct {
|
|
RoomID string `json:"roomId"`
|
|
GameID string `json:"gameId"`
|
|
SceneMode int `json:"sceneMode"`
|
|
ClientOrigin string `json:"clientOrigin"`
|
|
}
|
|
|
|
type BaishunCloseAppRequest struct {
|
|
RoomID string `json:"roomId"`
|
|
GameSessionID string `json:"gameSessionId"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type BaishunGameConfig struct {
|
|
SceneMode int `json:"sceneMode"`
|
|
CurrencyIcon string `json:"currencyIcon"`
|
|
}
|
|
|
|
type BaishunBridgeConfig struct {
|
|
AppName string `json:"appName"`
|
|
AppChannel string `json:"appChannel"`
|
|
AppID int64 `json:"appId"`
|
|
UserID string `json:"userId"`
|
|
Code string `json:"code"`
|
|
RoomID string `json:"roomId"`
|
|
GameMode string `json:"gameMode"`
|
|
Language string `json:"language"`
|
|
GSP int `json:"gsp"`
|
|
GameConfig BaishunGameConfig `json:"gameConfig"`
|
|
}
|
|
|
|
type BaishunLaunchEntry struct {
|
|
LaunchMode string `json:"launchMode"`
|
|
EntryURL string `json:"entryUrl"`
|
|
PreviewURL string `json:"previewUrl"`
|
|
DownloadURL string `json:"downloadUrl"`
|
|
PackageVersion string `json:"packageVersion"`
|
|
Orientation int `json:"orientation"`
|
|
SafeHeight int `json:"safeHeight"`
|
|
}
|
|
|
|
type BaishunLaunchAppResponse struct {
|
|
GameSessionID string `json:"gameSessionId"`
|
|
VendorType string `json:"vendorType"`
|
|
GameID string `json:"gameId"`
|
|
VendorGameID int `json:"vendorGameId"`
|
|
Entry BaishunLaunchEntry `json:"entry"`
|
|
BridgeConfig BaishunBridgeConfig `json:"bridgeConfig"`
|
|
RoomState BaishunRoomStateResponse `json:"roomState"`
|
|
}
|
|
|
|
type SyncCatalogRequest struct {
|
|
SysOrigin string `json:"sysOrigin"`
|
|
VendorGameIDs []int `json:"vendorGameIds"`
|
|
Force bool `json:"force"`
|
|
}
|
|
|
|
type SyncCatalogResponse struct {
|
|
Inserted int `json:"inserted"`
|
|
Updated int `json:"updated"`
|
|
Skipped int `json:"skipped"`
|
|
}
|
|
|
|
type BaishunTokenRequest struct {
|
|
AppID int64 `json:"app_id"`
|
|
UserID string `json:"user_id"`
|
|
Code string `json:"code"`
|
|
Signature string `json:"signature"`
|
|
SignatureNonce string `json:"signature_nonce"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
type BaishunProfileRequest struct {
|
|
AppID int64 `json:"app_id"`
|
|
UserID string `json:"user_id"`
|
|
SSToken string `json:"ss_token"`
|
|
ClientIP string `json:"client_ip"`
|
|
GameID int `json:"game_id"`
|
|
Signature string `json:"signature"`
|
|
SignatureNonce string `json:"signature_nonce"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
type BaishunUpdateTokenRequest struct {
|
|
AppID int64 `json:"app_id"`
|
|
UserID string `json:"user_id"`
|
|
SSToken string `json:"ss_token"`
|
|
GameID int `json:"game_id"`
|
|
Signature string `json:"signature"`
|
|
SignatureNonce string `json:"signature_nonce"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
type BaishunChangeBalanceRequest struct {
|
|
AppID int64 `json:"app_id"`
|
|
UserID string `json:"user_id"`
|
|
SSToken string `json:"ss_token"`
|
|
CurrencyDiff int64 `json:"currency_diff"`
|
|
DiffMsg string `json:"diff_msg"`
|
|
GameID int `json:"game_id"`
|
|
GameRoundID string `json:"game_round_id"`
|
|
RoomID string `json:"room_id"`
|
|
ChangeTimeAt int64 `json:"change_time_at"`
|
|
OrderID string `json:"order_id"`
|
|
Extend string `json:"extend"`
|
|
MsgType string `json:"msg_type"`
|
|
CurrencyType *int `json:"currency_type"`
|
|
Signature string `json:"signature"`
|
|
SignatureNonce string `json:"signature_nonce"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
type BaishunBalanceInfoRequest struct {
|
|
UserID string `json:"user_id"`
|
|
AppID int64 `json:"app_id"`
|
|
AppChannel string `json:"app_channel"`
|
|
Signature string `json:"signature"`
|
|
SignatureNonce string `json:"signature_nonce"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
type baishunStandardResponse struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
UniqueID string `json:"unique_id,omitempty"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
UserInfo interface{} `json:"user_info,omitempty"`
|
|
}
|
|
|
|
type baishunBalanceInfoResponse struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
type gameListRow struct {
|
|
ConfigID int64
|
|
SysOrigin string
|
|
InternalGameID string
|
|
Name string
|
|
Category string
|
|
Cover string
|
|
Sort int64
|
|
FullScreen bool
|
|
GameModeRaw string
|
|
VendorType string
|
|
VendorGameID string
|
|
LaunchMode string
|
|
ExtPackageVersion string
|
|
ExtPreviewURL string
|
|
PackageURL string
|
|
ExtOrientation *int
|
|
ExtSafeHeight int
|
|
CurrencyIcon string
|
|
ExtGSP string
|
|
CatalogName string
|
|
CatalogCover string
|
|
CatalogPreviewURL string
|
|
CatalogDownloadURL string
|
|
CatalogPackageVersion string
|
|
CatalogOrientation *int
|
|
CatalogSafeHeight int
|
|
CatalogStatus string
|
|
}
|
|
|
|
type baishunPlatformResponse struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data []baishunPlatformGame `json:"data"`
|
|
}
|
|
|
|
type baishunPlatformOneGameResponse struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data baishunPlatformGame `json:"data"`
|
|
}
|
|
|
|
type baishunPlatformGame struct {
|
|
GameID int `json:"game_id"`
|
|
Name string `json:"name"`
|
|
PreviewURL string `json:"preview_url"`
|
|
GameVersion string `json:"game_version"`
|
|
DownloadURL string `json:"download_url"`
|
|
GameMode []int `json:"game_mode"`
|
|
GameOrientation int `json:"game_orientation"`
|
|
SafeHeight int `json:"safe_height"`
|
|
VenueLevel []int `json:"venue_level"`
|
|
}
|
|
|
|
func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
|
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(items) > 5 {
|
|
items = items[:5]
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *BaishunService) ListRoomGames(ctx context.Context, user AuthUser, roomID string, category string) (*RoomGameListResponse, error) {
|
|
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &RoomGameListResponse{Items: items}, nil
|
|
}
|
|
|
|
func (s *BaishunService) GetRoomState(ctx context.Context, user AuthUser, roomID string) (*BaishunRoomStateResponse, error) {
|
|
var state model.BaishunRoomState
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Where("sys_origin = ? AND room_id = ?", user.SysOrigin, roomID).
|
|
First(&state).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return &BaishunRoomStateResponse{RoomID: roomID, State: baishunRoomStateIdle}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
|
|
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 := util.NextID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sessionDBID, err := util.NextID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
roomStateID, err := util.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.BaishunLaunchCodeTTLSeconds) * 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.BaishunAppName,
|
|
AppChannel: defaultIfBlank(s.cfg.BaishunAppChannel, "skychat"),
|
|
AppID: s.cfg.BaishunAppID,
|
|
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) 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
|
|
}
|
|
|
|
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
|
if strings.TrimSpace(s.cfg.BaishunPlatformBaseURL) == "" || s.cfg.BaishunAppID == 0 {
|
|
return nil, NewAppError(http.StatusBadRequest, "baishun_config_missing", "baishun platform config is missing")
|
|
}
|
|
sysOrigin := defaultIfBlank(req.SysOrigin, "LIKEI")
|
|
if strings.TrimSpace(sysOrigin) == "" {
|
|
sysOrigin = "LIKEI"
|
|
}
|
|
|
|
games, err := s.fetchPlatformGames(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filter := make(map[int]struct{}, len(req.VendorGameIDs))
|
|
for _, id := range req.VendorGameIDs {
|
|
filter[id] = struct{}{}
|
|
}
|
|
|
|
resp := &SyncCatalogResponse{}
|
|
for _, game := range games {
|
|
if len(filter) > 0 {
|
|
if _, ok := filter[game.GameID]; !ok {
|
|
resp.Skipped++
|
|
continue
|
|
}
|
|
}
|
|
id, err := util.NextID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
record := model.BaishunGameCatalog{
|
|
ID: id,
|
|
SysOrigin: sysOrigin,
|
|
InternalGameID: fmt.Sprintf("bs_%d", game.GameID),
|
|
VendorGameID: game.GameID,
|
|
Name: game.Name,
|
|
Cover: defaultIfBlank(game.PreviewURL, game.DownloadURL),
|
|
PreviewURL: game.PreviewURL,
|
|
DownloadURL: game.DownloadURL,
|
|
PackageVersion: game.GameVersion,
|
|
GameModeJSON: mustJSONString(game.GameMode),
|
|
Orientation: intPtr(game.GameOrientation),
|
|
SafeHeight: game.SafeHeight,
|
|
VenueLevelJSON: mustJSONString(game.VenueLevel),
|
|
GSP: strconv.Itoa(s.cfg.BaishunGSP),
|
|
RawJSON: mustJSONString(game),
|
|
Status: baishunCatalogEnabled,
|
|
CreateTime: time.Now(),
|
|
UpdateTime: time.Now(),
|
|
}
|
|
err = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "vendor_game_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"internal_game_id", "name", "cover", "preview_url", "download_url",
|
|
"package_version", "game_mode_json", "orientation", "safe_height",
|
|
"venue_level_json", "gsp", "raw_json", "status", "update_time",
|
|
}),
|
|
}).Create(&record).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Force {
|
|
resp.Updated++
|
|
} else {
|
|
resp.Inserted++
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenRequest, rawJSON string) baishunStandardResponse {
|
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
|
}
|
|
var session model.BaishunLaunchSession
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Where("launch_code = ?", strings.TrimSpace(req.Code)).
|
|
First(&session).Error; err != nil {
|
|
resp := s.failStandard(baishunCodeTokenExpired, "code invalid")
|
|
s.saveCallbackLog(ctx, "token", rawJSON, mustJSONString(resp), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
if session.Status != baishunSessionCodeIssued || time.Now().After(session.LaunchCodeExpireTime) {
|
|
resp := s.failStandard(baishunCodeTokenExpired, "code expired")
|
|
s.saveCallbackLog(ctx, "token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
if strings.TrimSpace(req.UserID) != "" && strconv.FormatInt(session.UserID, 10) != strings.TrimSpace(req.UserID) {
|
|
resp := s.failStandard(baishunCodeTokenExpired, "code invalid")
|
|
s.saveCallbackLog(ctx, "token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
ssToken, err := s.randomToken(32)
|
|
if err != nil {
|
|
resp := s.failStandard(baishunCodeServerError, "token create failed")
|
|
s.saveCallbackLog(ctx, "token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
expire := time.Now().Add(time.Duration(s.cfg.BaishunSSTokenTTLSeconds) * time.Second)
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Model(&model.BaishunLaunchSession{}).
|
|
Where("id = ?", session.ID).
|
|
Updates(map[string]any{
|
|
"ss_token": ssToken,
|
|
"ss_token_expire_time": expire,
|
|
"status": baishunSessionTokenIssued,
|
|
"update_time": time.Now(),
|
|
}).Error; err != nil {
|
|
resp := s.failStandard(baishunCodeServerError, "session update failed")
|
|
s.saveCallbackLog(ctx, "token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
userInfo := s.readUserInfo(ctx, session.UserID)
|
|
balance := s.readUserBalance(ctx, session.UserID)
|
|
resp := baishunStandardResponse{
|
|
Code: 0,
|
|
Message: "succeed",
|
|
UniqueID: s.uniqueID(),
|
|
Data: map[string]any{
|
|
"ss_token": ssToken,
|
|
"expire_date": expire.UnixMilli(),
|
|
},
|
|
UserInfo: map[string]any{
|
|
"user_id": req.UserID,
|
|
"user_name": userInfo.UserNickname,
|
|
"user_avatar": userInfo.UserAvatar,
|
|
"balance": balance,
|
|
},
|
|
}
|
|
s.saveCallbackLog(ctx, "token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRequest, rawJSON string) baishunStandardResponse {
|
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
|
}
|
|
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
|
if !ok {
|
|
resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid")
|
|
s.saveCallbackLog(ctx, "profile", rawJSON, mustJSONString(resp), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
userInfo := s.readUserInfo(ctx, session.UserID)
|
|
balance := s.readUserBalance(ctx, session.UserID)
|
|
resp := baishunStandardResponse{
|
|
Code: 0,
|
|
Message: "succeed",
|
|
UniqueID: s.uniqueID(),
|
|
Data: map[string]any{
|
|
"user_id": req.UserID,
|
|
"user_name": userInfo.UserNickname,
|
|
"user_avatar": userInfo.UserAvatar,
|
|
"balance": balance,
|
|
"balance_list": []map[string]any{{"name": "Gold", "currency_type": 0, "currency_amount": balance}},
|
|
"user_type": 1,
|
|
},
|
|
}
|
|
_ = s.repo.DB.WithContext(ctx).
|
|
Model(&model.BaishunLaunchSession{}).
|
|
Where("id = ?", session.ID).
|
|
Updates(map[string]any{"status": baishunSessionActive, "update_time": time.Now()}).Error
|
|
s.saveCallbackLog(ctx, "profile", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdateTokenRequest, rawJSON string) baishunStandardResponse {
|
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
|
}
|
|
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
|
if !ok {
|
|
resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid")
|
|
s.saveCallbackLog(ctx, "update-token", rawJSON, mustJSONString(resp), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
newToken, err := s.randomToken(32)
|
|
if err != nil {
|
|
resp := s.failStandard(baishunCodeServerError, "token create failed")
|
|
s.saveCallbackLog(ctx, "update-token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
expire := time.Now().Add(time.Duration(s.cfg.BaishunSSTokenTTLSeconds) * time.Second)
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Model(&model.BaishunLaunchSession{}).
|
|
Where("id = ?", session.ID).
|
|
Updates(map[string]any{
|
|
"ss_token": newToken,
|
|
"ss_token_expire_time": expire,
|
|
"status": baishunSessionActive,
|
|
"update_time": time.Now(),
|
|
}).Error; err != nil {
|
|
resp := s.failStandard(baishunCodeServerError, "session update failed")
|
|
s.saveCallbackLog(ctx, "update-token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
resp := baishunStandardResponse{
|
|
Code: 0,
|
|
Message: "succeed",
|
|
UniqueID: s.uniqueID(),
|
|
Data: map[string]any{
|
|
"ss_token": newToken,
|
|
"expire_date": expire.UnixMilli(),
|
|
},
|
|
}
|
|
s.saveCallbackLog(ctx, "update-token", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunChangeBalanceRequest, rawJSON string) baishunStandardResponse {
|
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
|
}
|
|
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
|
if !ok {
|
|
resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid")
|
|
s.saveCallbackLog(ctx, "change-balance", rawJSON, mustJSONString(resp), "", req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
if req.CurrencyDiff == 0 {
|
|
resp := s.failStandard(baishunCodeServerError, "currency_diff invalid")
|
|
s.saveCallbackLog(ctx, "change-balance", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
lockKey := fmt.Sprintf("bs:lock:user:%s:%s", session.SysOrigin, req.UserID)
|
|
lockOK, err := s.repo.Redis.SetNX(ctx, lockKey, "1", 5*time.Second).Result()
|
|
if err == nil && lockOK {
|
|
defer s.repo.Redis.Del(context.Background(), lockKey)
|
|
}
|
|
|
|
var order model.BaishunOrderIdempotency
|
|
err = s.repo.DB.WithContext(ctx).
|
|
Where("sys_origin = ? AND order_id = ?", session.SysOrigin, req.OrderID).
|
|
First(&order).Error
|
|
if err == nil && order.Status == baishunOrderStatusSuccess && strings.TrimSpace(order.ResponseJSON) != "" {
|
|
var resp baishunStandardResponse
|
|
if json.Unmarshal([]byte(order.ResponseJSON), &resp) == nil {
|
|
s.saveCallbackLog(ctx, "change-balance", rawJSON, order.ResponseJSON, session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message)
|
|
return resp
|
|
}
|
|
}
|
|
|
|
walletEventID := fmt.Sprintf("BAISHUN:%s:%s", session.SysOrigin, req.OrderID)
|
|
exists, existsErr := s.java.ExistsGoldEvent(ctx, walletEventID)
|
|
if existsErr == nil && !exists {
|
|
changeErr := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
|
ReceiptType: receiptTypeFromDiff(req.CurrencyDiff),
|
|
UserID: session.UserID,
|
|
SysOrigin: session.SysOrigin,
|
|
EventID: walletEventID,
|
|
Remark: defaultIfBlank(req.DiffMsg, req.MsgType),
|
|
Amount: abs64(req.CurrencyDiff),
|
|
CloseDelayAsset: false,
|
|
OpUserType: "APP",
|
|
CustomizeOrigin: fmt.Sprintf("BAISHUN_GAME_%d", req.GameID),
|
|
CustomizeOriginDesc: fmt.Sprintf("BAISHUN GAME[%d]", req.GameID),
|
|
})
|
|
if changeErr != nil {
|
|
message := changeErr.Error()
|
|
code := baishunCodeServerError
|
|
if strings.Contains(strings.ToLower(message), "insufficient") || strings.Contains(message, "5000") {
|
|
code = baishunCodeInsufficientAssets
|
|
}
|
|
resp := s.failStandard(code, message)
|
|
s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusFailed, rawJSON, mustJSONString(resp), nil)
|
|
s.saveCallbackLog(ctx, "change-balance", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
|
return resp
|
|
}
|
|
}
|
|
|
|
balance := s.readUserBalance(ctx, session.UserID)
|
|
resp := baishunStandardResponse{
|
|
Code: 0,
|
|
Message: "succeed",
|
|
UniqueID: s.uniqueID(),
|
|
Data: map[string]any{
|
|
"currency_balance": balance,
|
|
},
|
|
}
|
|
s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusSuccess, rawJSON, mustJSONString(resp), nil)
|
|
s.saveCallbackLog(ctx, "change-balance", rawJSON, mustJSONString(resp), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
func (s *BaishunService) HandleReport(ctx context.Context, payload map[string]any, rawJSON string) baishunStandardResponse {
|
|
userID := toString(payload["user_id"])
|
|
orderID := toString(payload["order_id"])
|
|
gameRoundID := toString(payload["game_round_id"])
|
|
resp := baishunStandardResponse{
|
|
Code: 0,
|
|
Message: "succeed",
|
|
UniqueID: s.uniqueID(),
|
|
Data: map[string]any{},
|
|
}
|
|
s.saveCallbackLog(ctx, "report", rawJSON, mustJSONString(resp), "", userID, stringPtr(orderID), stringPtr(gameRoundID), baishunCallbackStatusSuccess, 0, resp.Message)
|
|
return resp
|
|
}
|
|
|
|
func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalanceInfoRequest, rawJSON string) baishunBalanceInfoResponse {
|
|
if !s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp) {
|
|
return baishunBalanceInfoResponse{Code: baishunCodeSignatureError, Msg: "signature error"}
|
|
}
|
|
userID, _ := strconv.ParseInt(req.UserID, 10, 64)
|
|
balance := s.readUserBalance(ctx, userID)
|
|
resp := baishunBalanceInfoResponse{
|
|
Code: 0,
|
|
Msg: "success",
|
|
Data: map[string]any{"cur_coin": balance},
|
|
}
|
|
s.saveCallbackLog(ctx, "balance-info", rawJSON, mustJSONString(resp), "", req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Msg)
|
|
return resp
|
|
}
|
|
|
|
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, category string) ([]RoomGameListItem, error) {
|
|
var rows []gameListRow
|
|
query := s.repo.DB.WithContext(ctx).
|
|
Table("sys_game_list_config AS cfg").
|
|
Select(`
|
|
cfg.id AS config_id,
|
|
cfg.sys_origin AS sys_origin,
|
|
cfg.game_id AS internal_game_id,
|
|
cfg.name AS name,
|
|
cfg.category AS category,
|
|
cfg.cover AS cover,
|
|
cfg.sort AS sort,
|
|
cfg.full_screen AS full_screen,
|
|
cfg.game_mode AS game_mode_raw,
|
|
ext.vendor_type AS vendor_type,
|
|
ext.vendor_game_id AS vendor_game_id,
|
|
ext.launch_mode AS launch_mode,
|
|
ext.package_version AS ext_package_version,
|
|
ext.preview_url AS ext_preview_url,
|
|
ext.package_url AS package_url,
|
|
ext.orientation AS ext_orientation,
|
|
ext.safe_height AS ext_safe_height,
|
|
ext.currency_icon AS currency_icon,
|
|
ext.gsp AS ext_gsp,
|
|
cat.name AS catalog_name,
|
|
cat.cover AS catalog_cover,
|
|
cat.preview_url AS catalog_preview_url,
|
|
cat.download_url AS catalog_download_url,
|
|
cat.package_version AS catalog_package_version,
|
|
cat.orientation AS catalog_orientation,
|
|
cat.safe_height AS catalog_safe_height,
|
|
cat.status AS catalog_status
|
|
`).
|
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1").
|
|
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
|
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
|
|
if strings.TrimSpace(category) != "" {
|
|
query = query.Where("cfg.category = ?", category)
|
|
}
|
|
if strings.TrimSpace(roomID) != "" {
|
|
query = query.Where("cfg.game_mode LIKE ?", "%CHAT_ROOM%")
|
|
}
|
|
if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make([]RoomGameListItem, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, row.toListItem())
|
|
}
|
|
if len(items) > 0 {
|
|
return items, nil
|
|
}
|
|
|
|
var catalogs []model.BaishunGameCatalog
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Where("sys_origin = ? AND status = ?", sysOrigin, baishunCatalogEnabled).
|
|
Order("vendor_game_id ASC").
|
|
Find(&catalogs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, catalog := range catalogs {
|
|
gameID := defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID))
|
|
items = append(items, RoomGameListItem{
|
|
GameID: gameID,
|
|
VendorType: baishunVendorType,
|
|
VendorGameID: catalog.VendorGameID,
|
|
Name: catalog.Name,
|
|
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
|
Category: "CHAT_ROOM",
|
|
Sort: int64(catalog.VendorGameID),
|
|
LaunchMode: baishunLaunchModeRemote,
|
|
FullScreen: true,
|
|
GameMode: pickFirstInt(catalog.GameModeJSON, 3),
|
|
SafeHeight: catalog.SafeHeight,
|
|
Orientation: derefInt(catalog.Orientation),
|
|
PackageVersion: catalog.PackageVersion,
|
|
Status: catalog.Status,
|
|
})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
if items[i].Sort == items[j].Sort {
|
|
return items[i].VendorGameID < items[j].VendorGameID
|
|
}
|
|
return items[i].Sort > items[j].Sort
|
|
})
|
|
return items, nil
|
|
}
|
|
|
|
func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID string) (gameListRow, error) {
|
|
var row gameListRow
|
|
err := s.repo.DB.WithContext(ctx).
|
|
Table("sys_game_list_config AS cfg").
|
|
Select(`
|
|
cfg.id AS config_id,
|
|
cfg.sys_origin AS sys_origin,
|
|
cfg.game_id AS internal_game_id,
|
|
cfg.name AS name,
|
|
cfg.category AS category,
|
|
cfg.cover AS cover,
|
|
cfg.sort AS sort,
|
|
cfg.full_screen AS full_screen,
|
|
cfg.game_mode AS game_mode_raw,
|
|
ext.vendor_type AS vendor_type,
|
|
ext.vendor_game_id AS vendor_game_id,
|
|
ext.launch_mode AS launch_mode,
|
|
ext.package_version AS ext_package_version,
|
|
ext.preview_url AS ext_preview_url,
|
|
ext.package_url AS package_url,
|
|
ext.orientation AS ext_orientation,
|
|
ext.safe_height AS ext_safe_height,
|
|
ext.currency_icon AS currency_icon,
|
|
ext.gsp AS ext_gsp,
|
|
cat.name AS catalog_name,
|
|
cat.cover AS catalog_cover,
|
|
cat.preview_url AS catalog_preview_url,
|
|
cat.download_url AS catalog_download_url,
|
|
cat.package_version AS catalog_package_version,
|
|
cat.orientation AS catalog_orientation,
|
|
cat.safe_height AS catalog_safe_height,
|
|
cat.status AS catalog_status
|
|
`).
|
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1").
|
|
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
|
Where("cfg.sys_origin = ? AND cfg.game_id = ? AND ext.vendor_type = ?", sysOrigin, gameID, baishunVendorType).
|
|
Limit(1).
|
|
Scan(&row).Error
|
|
if err != nil {
|
|
return gameListRow{}, err
|
|
}
|
|
if strings.TrimSpace(row.InternalGameID) != "" {
|
|
return row, nil
|
|
}
|
|
|
|
var catalog model.BaishunGameCatalog
|
|
if err := s.repo.DB.WithContext(ctx).
|
|
Where("sys_origin = ? AND internal_game_id = ?", sysOrigin, gameID).
|
|
First(&catalog).Error; err == nil {
|
|
return gameListRow{
|
|
SysOrigin: sysOrigin,
|
|
InternalGameID: catalog.InternalGameID,
|
|
Name: catalog.Name,
|
|
Cover: catalog.Cover,
|
|
VendorType: baishunVendorType,
|
|
VendorGameID: strconv.Itoa(catalog.VendorGameID),
|
|
LaunchMode: baishunLaunchModeRemote,
|
|
CatalogCover: catalog.Cover,
|
|
CatalogPreviewURL: catalog.PreviewURL,
|
|
CatalogDownloadURL: catalog.DownloadURL,
|
|
CatalogPackageVersion: catalog.PackageVersion,
|
|
CatalogSafeHeight: catalog.SafeHeight,
|
|
CatalogOrientation: catalog.Orientation,
|
|
CatalogStatus: catalog.Status,
|
|
GameModeRaw: catalog.GameModeJSON,
|
|
}, nil
|
|
}
|
|
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
|
}
|
|
|
|
func (s *BaishunService) fetchPlatformGames(ctx context.Context) ([]baishunPlatformGame, error) {
|
|
timestamp := time.Now().Unix()
|
|
nonce := s.randomNonce()
|
|
payload := map[string]any{
|
|
"app_channel": s.cfg.BaishunAppChannel,
|
|
"app_id": s.cfg.BaishunAppID,
|
|
"signature": s.signBAISHUN(timestamp, nonce),
|
|
"signature_nonce": nonce,
|
|
"timestamp": timestamp,
|
|
}
|
|
if strings.TrimSpace(s.cfg.BaishunAppName) != "" {
|
|
payload["app_name"] = s.cfg.BaishunAppName
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(s.cfg.BaishunPlatformBaseURL, "/")+"/v1/api/gamelist", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
|
return nil, fmt.Errorf("baishun platform failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
|
}
|
|
var result baishunPlatformResponse
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
if result.Code != 0 {
|
|
return nil, fmt.Errorf("baishun platform failed: code=%d message=%s", result.Code, result.Message)
|
|
}
|
|
return result.Data, nil
|
|
}
|
|
|
|
func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) bool {
|
|
if s.cfg.BaishunAppID != 0 && appID != 0 && appID != s.cfg.BaishunAppID {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(appChannel) != "" && s.cfg.BaishunAppChannel != "" && appChannel != s.cfg.BaishunAppChannel {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(s.cfg.BaishunAppKey) == "" {
|
|
return false
|
|
}
|
|
if abs64(time.Now().Unix()-timestamp) > 15 {
|
|
return false
|
|
}
|
|
expected := s.signBAISHUN(timestamp, nonce)
|
|
if !strings.EqualFold(expected, signature) {
|
|
return false
|
|
}
|
|
return s.rememberNonce(ctx, nonce)
|
|
}
|
|
|
|
func (s *BaishunService) rememberNonce(ctx context.Context, nonce string) bool {
|
|
ok, err := s.repo.Redis.SetNX(ctx, "bs:sign:nonce:"+nonce, "1", 15*time.Second).Result()
|
|
return err == nil && ok
|
|
}
|
|
|
|
func (s *BaishunService) signBAISHUN(timestamp int64, nonce string) string {
|
|
sum := md5.Sum([]byte(nonce + s.cfg.BaishunAppKey + strconv.FormatInt(timestamp, 10)))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (s *BaishunService) randomNonce() string {
|
|
token, err := s.randomToken(8)
|
|
if err != nil {
|
|
return fmt.Sprintf("%d", time.Now().UnixNano())
|
|
}
|
|
return token
|
|
}
|
|
|
|
func (s *BaishunService) randomToken(size int) (string, error) {
|
|
buf := make([]byte, size)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(buf), nil
|
|
}
|
|
|
|
func (s *BaishunService) uniqueID() string {
|
|
id, err := util.NextID()
|
|
if err != nil {
|
|
return strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
}
|
|
return strconv.FormatInt(id, 10)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *BaishunService) persistOrderRecord(ctx context.Context, sysOrigin string, req BaishunChangeBalanceRequest, walletEventID, status, requestJSON, responseJSON string, assetRecordID *int64) {
|
|
id, err := util.NextID()
|
|
if err != nil {
|
|
return
|
|
}
|
|
record := model.BaishunOrderIdempotency{
|
|
ID: id,
|
|
SysOrigin: sysOrigin,
|
|
OrderID: req.OrderID,
|
|
GameRoundID: req.GameRoundID,
|
|
RoomID: req.RoomID,
|
|
UserID: parseInt64Default(req.UserID),
|
|
VendorGameID: req.GameID,
|
|
CurrencyDiff: req.CurrencyDiff,
|
|
DiffMsg: req.DiffMsg,
|
|
MsgType: req.MsgType,
|
|
CurrencyType: req.CurrencyType,
|
|
WalletEventID: walletEventID,
|
|
WalletAssetRecordID: assetRecordID,
|
|
Status: status,
|
|
RequestJSON: requestJSON,
|
|
ResponseJSON: responseJSON,
|
|
CreateTime: time.Now(),
|
|
UpdateTime: time.Now(),
|
|
}
|
|
_ = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "order_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"wallet_event_id", "wallet_asset_record_id", "status", "request_json", "response_json", "update_time",
|
|
}),
|
|
}).Create(&record).Error
|
|
}
|
|
|
|
func (s *BaishunService) saveCallbackLog(ctx context.Context, requestType, requestJSON, responseJSON, sysOrigin, userID string, orderID, gameRoundID *string, status string, bizCode int, bizMessage string) {
|
|
id, err := util.NextID()
|
|
if err != nil {
|
|
return
|
|
}
|
|
logRecord := model.BaishunCallbackLog{
|
|
ID: id,
|
|
SysOrigin: sysOrigin,
|
|
RequestType: requestType,
|
|
RequestJSON: requestJSON,
|
|
ResponseJSON: responseJSON,
|
|
BizCode: strconv.Itoa(bizCode),
|
|
BizMessage: bizMessage,
|
|
Status: status,
|
|
CreateTime: time.Now(),
|
|
UpdateTime: time.Now(),
|
|
}
|
|
if orderID != nil {
|
|
logRecord.OrderID = *orderID
|
|
}
|
|
if gameRoundID != nil {
|
|
logRecord.GameRoundID = *gameRoundID
|
|
}
|
|
if strings.TrimSpace(userID) != "" {
|
|
parsed := parseInt64Default(userID)
|
|
logRecord.UserID = &parsed
|
|
}
|
|
_ = s.repo.DB.WithContext(ctx).Create(&logRecord).Error
|
|
}
|
|
|
|
func (s *BaishunService) failStandard(code int, message string) baishunStandardResponse {
|
|
return baishunStandardResponse{
|
|
Code: code,
|
|
Message: defaultIfBlank(message, "failed"),
|
|
UniqueID: s.uniqueID(),
|
|
}
|
|
}
|
|
|
|
func (r gameListRow) toListItem() RoomGameListItem {
|
|
return RoomGameListItem{
|
|
GameID: r.InternalGameID,
|
|
VendorType: defaultIfBlank(r.VendorType, baishunVendorType),
|
|
VendorGameID: r.vendorGameIDInt(),
|
|
Name: r.name(),
|
|
Cover: r.cover(),
|
|
Category: r.Category,
|
|
Sort: r.Sort,
|
|
LaunchMode: r.launchMode(),
|
|
FullScreen: r.FullScreen,
|
|
GameMode: r.gameMode(),
|
|
SafeHeight: r.safeHeight(),
|
|
Orientation: r.orientation(),
|
|
PackageVersion: r.packageVersion(),
|
|
Status: defaultIfBlank(r.CatalogStatus, baishunCatalogEnabled),
|
|
}
|
|
}
|
|
|
|
func (r gameListRow) name() string {
|
|
return defaultIfBlank(r.Name, r.CatalogName)
|
|
}
|
|
|
|
func (r gameListRow) cover() string {
|
|
return defaultIfBlank(r.Cover, defaultIfBlank(r.CatalogCover, r.previewURL()))
|
|
}
|
|
|
|
func (r gameListRow) previewURL() string {
|
|
return defaultIfBlank(r.ExtPreviewURL, r.CatalogPreviewURL)
|
|
}
|
|
|
|
func (r gameListRow) downloadURL() string {
|
|
return defaultIfBlank(r.PackageURL, r.CatalogDownloadURL)
|
|
}
|
|
|
|
func (r gameListRow) entryURL() string {
|
|
switch r.launchMode() {
|
|
case baishunLaunchModeLocalZip:
|
|
if strings.TrimSpace(r.PackageURL) != "" {
|
|
return r.PackageURL
|
|
}
|
|
}
|
|
if strings.TrimSpace(r.CatalogDownloadURL) != "" {
|
|
return r.CatalogDownloadURL
|
|
}
|
|
return defaultIfBlank(r.PackageURL, defaultIfBlank(r.ExtPreviewURL, "mock://baishun"))
|
|
}
|
|
|
|
func (r gameListRow) packageVersion() string {
|
|
return defaultIfBlank(r.ExtPackageVersion, r.CatalogPackageVersion)
|
|
}
|
|
|
|
func (r gameListRow) safeHeight() int {
|
|
if r.ExtSafeHeight > 0 {
|
|
return r.ExtSafeHeight
|
|
}
|
|
return r.CatalogSafeHeight
|
|
}
|
|
|
|
func (r gameListRow) orientation() int {
|
|
if r.ExtOrientation != nil {
|
|
return *r.ExtOrientation
|
|
}
|
|
return derefInt(r.CatalogOrientation)
|
|
}
|
|
|
|
func (r gameListRow) launchMode() string {
|
|
return defaultIfBlank(r.LaunchMode, baishunLaunchModeRemote)
|
|
}
|
|
|
|
func (r gameListRow) gameMode() int {
|
|
if parsed := pickFirstInt(r.GameModeRaw, 3); parsed > 0 {
|
|
return parsed
|
|
}
|
|
return 3
|
|
}
|
|
|
|
func (r gameListRow) vendorGameIDInt() int {
|
|
parsed, _ := strconv.Atoi(strings.TrimSpace(r.VendorGameID))
|
|
return parsed
|
|
}
|
|
|
|
func (r gameListRow) currencyIcon() string {
|
|
return r.CurrencyIcon
|
|
}
|
|
|
|
func (s *BaishunService) resolveGSP(row gameListRow) int {
|
|
if parsed, err := strconv.Atoi(strings.TrimSpace(row.ExtGSP)); err == nil && parsed > 0 {
|
|
return parsed
|
|
}
|
|
if s.cfg.BaishunGSP > 0 {
|
|
return s.cfg.BaishunGSP
|
|
}
|
|
return 101
|
|
}
|
|
|
|
func defaultIfBlank(value, fallback string) string {
|
|
if strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func pickFirstInt(raw string, fallback int) int {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
var list []int
|
|
if strings.HasPrefix(raw, "[") {
|
|
if json.Unmarshal([]byte(raw), &list) == nil && len(list) > 0 {
|
|
return list[len(list)-1]
|
|
}
|
|
}
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
|
return parsed
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func receiptTypeFromDiff(diff int64) string {
|
|
if diff >= 0 {
|
|
return "INCOME"
|
|
}
|
|
return "EXPENDITURE"
|
|
}
|
|
|
|
func mustJSONString(value interface{}) string {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func abs64(value int64) int64 {
|
|
if value < 0 {
|
|
return -value
|
|
}
|
|
return value
|
|
}
|
|
|
|
func parseInt64Default(value string) int64 {
|
|
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
|
return parsed
|
|
}
|
|
|
|
func sanitizeRoomID(roomID string) string {
|
|
roomID = strings.ReplaceAll(strings.TrimSpace(roomID), " ", "_")
|
|
if roomID == "" {
|
|
return "room"
|
|
}
|
|
return roomID
|
|
}
|
|
|
|
func intPtr(value int) *int {
|
|
return &value
|
|
}
|
|
|
|
func derefInt(value *int) int {
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func derefTime(value *time.Time) time.Time {
|
|
if value == nil {
|
|
return time.Unix(0, 0)
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func stringPtr(value string) *string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func toString(value any) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return typed
|
|
case json.Number:
|
|
return typed.String()
|
|
default:
|
|
return fmt.Sprint(typed)
|
|
}
|
|
}
|