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, 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, 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(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 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 != "" { return value } } return defaultLaunchLang } 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[:]) }