322 lines
10 KiB
Go
322 lines
10 KiB
Go
package lingxian
|
|
|
|
import (
|
|
"chatapp3-golang/internal/common"
|
|
"chatapp3-golang/internal/service/gameprovider"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func (p *AppProvider) Key() string {
|
|
return vendorType
|
|
}
|
|
|
|
func (p *AppProvider) DisplayName() string {
|
|
return "灵仙"
|
|
}
|
|
|
|
func (p *AppProvider) SupportsLaunch(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) {
|
|
_, err := p.service.findGameRow(ctx, user.SysOrigin, req)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
var appErr *common.AppError
|
|
if errors.As(err, &appErr) && appErr.Code == "game_not_found" {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (p *AppProvider) ListShortcutGames(ctx context.Context, user gameprovider.AuthUser, roomID string) ([]gameprovider.RoomGameListItem, error) {
|
|
resp, err := p.ListRoomGames(ctx, user, roomID, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(resp.Items) > 5 {
|
|
resp.Items = resp.Items[:5]
|
|
}
|
|
return resp.Items, nil
|
|
}
|
|
|
|
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
|
items, err := p.service.listRoomGames(ctx, user.SysOrigin, user.RegionID, category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &gameprovider.RoomGameListResponse{Items: items}, nil
|
|
}
|
|
|
|
func (p *AppProvider) GetRoomState(ctx context.Context, user gameprovider.AuthUser, roomID string) (*gameprovider.RoomStateResponse, error) {
|
|
return &gameprovider.RoomStateResponse{RoomID: roomID, State: "IDLE", Provider: vendorType}, nil
|
|
}
|
|
|
|
func (p *AppProvider) LaunchGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest, clientIP string) (*gameprovider.LaunchResponse, error) {
|
|
row, err := p.service.findGameRow(ctx, user.SysOrigin, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runtimeCfg, err := p.service.resolveRuntimeConfigForProfile(ctx, normalizeSysOrigin(user.SysOrigin), row.Profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uid := launchUID(runtimeCfg, user)
|
|
token := launchToken(runtimeCfg, user)
|
|
roomID := launchRoomID(runtimeCfg, req)
|
|
lang := launchLang(req)
|
|
sign := launchSign(runtimeCfg, row.VendorGameID, uid, token, roomID)
|
|
entryURL := buildLaunchURL(row.entryURL(), runtimeCfg, user, req, row.VendorGameID)
|
|
sessionID := fmt.Sprintf("lx_%d_%s", user.UserID, row.VendorGameID)
|
|
return &gameprovider.LaunchResponse{
|
|
ID: row.ConfigID,
|
|
GameSessionID: sessionID,
|
|
Provider: vendorType,
|
|
GameID: row.InternalGameID,
|
|
ProviderGameID: row.VendorGameID,
|
|
Entry: gameprovider.LaunchEntry{
|
|
LaunchMode: defaultIfBlank(row.LaunchMode, launchModeH5),
|
|
EntryURL: entryURL,
|
|
PreviewURL: row.previewURL(),
|
|
DownloadURL: row.entryURL(),
|
|
PackageVersion: row.packageVersion(),
|
|
Orientation: row.orientation(),
|
|
SafeHeight: row.safeHeight(),
|
|
},
|
|
LaunchConfig: map[string]any{
|
|
"uid": uid,
|
|
"token": token,
|
|
"lang": lang,
|
|
"gameId": row.VendorGameID,
|
|
"roomId": roomID,
|
|
"roomid": roomID,
|
|
"sign": sign,
|
|
},
|
|
RoomState: gameprovider.RoomStateResponse{
|
|
RoomID: req.RoomID,
|
|
State: "PLAYING",
|
|
Provider: vendorType,
|
|
GameSessionID: sessionID,
|
|
CurrentGameID: row.InternalGameID,
|
|
CurrentProviderGameID: row.VendorGameID,
|
|
CurrentGameName: row.name(),
|
|
CurrentGameCover: row.cover(),
|
|
HostUserID: user.UserID,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *AppProvider) CloseGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
|
return &gameprovider.RoomStateResponse{RoomID: req.RoomID, State: "IDLE", Provider: vendorType}, nil
|
|
}
|
|
|
|
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, regionID, category string) ([]gameprovider.RoomGameListItem, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var rows []gameRow
|
|
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
|
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, vendorType)
|
|
if strings.TrimSpace(regionID) != "" {
|
|
// regions 为空表示全区域;有值时按用户区域 ID 精确匹配,避免土耳其用户看到其他区域游戏。
|
|
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
|
}
|
|
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), roomCategory) {
|
|
query = query.Where("cfg.category = ?", category)
|
|
}
|
|
if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]gameprovider.RoomGameListItem, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, row.toListItem())
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *Service) findGameRow(ctx context.Context, sysOrigin string, req gameprovider.LaunchRequest) (gameRow, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
|
if err != nil {
|
|
return gameRow{}, err
|
|
}
|
|
var row gameRow
|
|
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
|
Where("cfg.sys_origin = ? AND ext.vendor_type = ?", sysOrigin, vendorType)
|
|
if req.ID > 0 {
|
|
query = query.Where("cfg.id = ?", req.ID)
|
|
} else {
|
|
query = query.Where("cfg.game_id = ? OR ext.vendor_game_id = ?", strings.TrimSpace(req.GameID), strings.TrimSpace(req.GameID))
|
|
}
|
|
if err := query.Limit(1).Scan(&row).Error; err != nil {
|
|
return gameRow{}, err
|
|
}
|
|
if row.ConfigID > 0 {
|
|
return row, nil
|
|
}
|
|
return gameRow{}, common.NewAppError(http.StatusNotFound, "game_not_found", "lingxian game config not found")
|
|
}
|
|
|
|
func (s *Service) baseGameQuery(ctx context.Context, sysOrigin, profile string) *gorm.DB {
|
|
return s.db.WithContext(ctx).
|
|
Table("sys_game_list_config AS cfg").
|
|
Select(`
|
|
cfg.id AS config_id,
|
|
cfg.sys_origin AS sys_origin,
|
|
ext.profile AS profile,
|
|
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,
|
|
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.extra_json AS ext_extra_json,
|
|
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.status AS catalog_status
|
|
`).
|
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
|
|
Joins("LEFT JOIN lingxian_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND cat.vendor_game_id = ext.vendor_game_id")
|
|
}
|
|
|
|
func (r gameRow) toListItem() gameprovider.RoomGameListItem {
|
|
return gameprovider.RoomGameListItem{
|
|
ID: r.ConfigID,
|
|
GameID: r.InternalGameID,
|
|
GameType: vendorType,
|
|
Provider: vendorType,
|
|
ProviderGameID: r.VendorGameID,
|
|
Name: r.name(),
|
|
Cover: r.cover(),
|
|
Category: r.Category,
|
|
Sort: r.Sort,
|
|
LaunchMode: defaultIfBlank(r.LaunchMode, launchModeH5),
|
|
FullScreen: r.FullScreen,
|
|
GameMode: 3,
|
|
SafeHeight: r.safeHeight(),
|
|
Orientation: r.orientation(),
|
|
PackageVersion: r.packageVersion(),
|
|
Status: defaultIfBlank(r.CatalogStatus, catalogEnabled),
|
|
LaunchParams: map[string]any{
|
|
"gameType": vendorType,
|
|
"lang": defaultLaunchLang,
|
|
"screenMode": r.screenMode(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r gameRow) name() string {
|
|
return defaultIfBlank(r.Name, r.CatalogName)
|
|
}
|
|
|
|
func (r gameRow) cover() string {
|
|
return defaultIfBlank(r.Cover, defaultIfBlank(r.CatalogCover, r.previewURL()))
|
|
}
|
|
|
|
func (r gameRow) previewURL() string {
|
|
return defaultIfBlank(r.ExtPreviewURL, r.CatalogPreviewURL)
|
|
}
|
|
|
|
func (r gameRow) entryURL() string {
|
|
if !r.FullScreen {
|
|
if halfURL := r.previewURL(); halfURL != "" {
|
|
return halfURL
|
|
}
|
|
}
|
|
return defaultIfBlank(r.PackageURL, defaultIfBlank(r.CatalogDownloadURL, r.ExtPreviewURL))
|
|
}
|
|
|
|
func (r gameRow) packageVersion() string {
|
|
return defaultIfBlank(r.ExtPackageVersion, r.CatalogPackageVersion)
|
|
}
|
|
|
|
func (r gameRow) orientation() int {
|
|
if r.ExtOrientation != nil {
|
|
return *r.ExtOrientation
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (r gameRow) safeHeight() int {
|
|
return r.ExtSafeHeight
|
|
}
|
|
|
|
func (r gameRow) screenMode() string {
|
|
if r.FullScreen {
|
|
return "full"
|
|
}
|
|
return "half"
|
|
}
|
|
|
|
func buildLaunchURL(entry string, cfg runtimeConfig, user gameprovider.AuthUser, req gameprovider.LaunchRequest, gameID string) string {
|
|
parsed, err := url.Parse(strings.TrimSpace(entry))
|
|
if err != nil {
|
|
return strings.TrimSpace(entry)
|
|
}
|
|
uid := launchUID(cfg, user)
|
|
token := launchToken(cfg, user)
|
|
roomID := launchRoomID(cfg, req)
|
|
lang := launchLang(req)
|
|
query := parsed.Query()
|
|
query.Set("uid", uid)
|
|
query.Set("token", token)
|
|
query.Set("lang", lang)
|
|
query.Set("gameId", gameID)
|
|
query.Set("roomId", roomID)
|
|
query.Set("roomid", roomID)
|
|
query.Set("sign", launchSign(cfg, gameID, uid, token, roomID))
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String()
|
|
}
|
|
|
|
func launchUID(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
|
return defaultIfBlank(cfg.TestUID, fmt.Sprintf("%d", user.UserID))
|
|
}
|
|
|
|
func launchToken(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
|
return trimTokenPadding(defaultIfBlank(cfg.TestToken, user.Token))
|
|
}
|
|
|
|
func launchRoomID(cfg runtimeConfig, req gameprovider.LaunchRequest) string {
|
|
return defaultIfBlank(cfg.TestRoomID, req.RoomID)
|
|
}
|
|
|
|
func launchLang(req gameprovider.LaunchRequest) string {
|
|
for _, key := range []string{"lang", "language", "locale"} {
|
|
if req.Params == nil {
|
|
break
|
|
}
|
|
if value := strings.TrimSpace(fmt.Sprint(req.Params[key])); value != "" && value != "<nil>" {
|
|
return value
|
|
}
|
|
}
|
|
return defaultLaunchLang
|
|
}
|
|
|
|
func trimTokenPadding(token string) string {
|
|
return strings.TrimRight(strings.TrimSpace(token), "=")
|
|
}
|
|
|
|
func launchSign(cfg runtimeConfig, gameID, uid, token, roomID string) string {
|
|
sum := md5.Sum([]byte(gameID + uid + token + roomID + strings.TrimSpace(cfg.AppKey)))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|