package baishun import ( "bytes" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "gorm.io/gorm/clause" ) // SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。 func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) { sysOrigin := normalizeAdminSysOrigin(req.SysOrigin) profile, err := s.requestProfile(ctx, sysOrigin, req.Profile) if err != nil { return nil, err } runtimeCfg, err := s.requireProviderConfigForProfile(ctx, sysOrigin, profile) if err != nil { return nil, err } games, err := s.fetchPlatformGames(ctx, runtimeCfg) 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 := utils.NextID() if err != nil { return nil, err } record := model.BaishunGameCatalog{ ID: id, SysOrigin: sysOrigin, Profile: profile, 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: utils.MustJSONString(game.GameMode, ""), Orientation: intPtr(game.GameOrientation), SafeHeight: game.SafeHeight, VenueLevelJSON: utils.MustJSONString(game.VenueLevel, ""), GSP: strconv.Itoa(runtimeCfg.gsp()), RawJSON: utils.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: "profile"}, {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 } // fetchPlatformGames 从百顺平台拉取游戏目录。 func (s *BaishunService) fetchPlatformGames(ctx context.Context, runtimeCfg baishunRuntimeConfig) ([]baishunPlatformGame, error) { timestamp := time.Now().Unix() nonce := s.randomNonce() payload := map[string]any{ "game_list_type": baishunGameListTypeGame, "app_channel": runtimeCfg.appChannel(), "app_id": runtimeCfg.AppID, "signature": signBAISHUN(runtimeCfg.AppKey, timestamp, nonce), "signature_nonce": nonce, "timestamp": timestamp, } if strings.TrimSpace(runtimeCfg.AppName) != "" { payload["app_name"] = runtimeCfg.AppName } body, _ := json.Marshal(payload) endpoint := runtimeCfg.gameListURL() req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, 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 } contentType := strings.TrimSpace(resp.Header.Get("Content-Type")) if resp.StatusCode >= http.StatusBadRequest { return nil, fmt.Errorf( "baishun platform failed: url=%s status=%d contentType=%s body=%s", endpoint, resp.StatusCode, contentType, baishunShortBody(raw), ) } if baishunLooksLikeHTML(raw, contentType) { return nil, fmt.Errorf( "baishun platform returned non-json html: url=%s contentType=%s body=%s", endpoint, contentType, baishunShortBody(raw), ) } var result baishunPlatformResponse if err := json.Unmarshal(raw, &result); err != nil { return nil, fmt.Errorf( "baishun platform returned invalid json: url=%s contentType=%s error=%v body=%s", endpoint, contentType, err, baishunShortBody(raw), ) } if result.Code != 0 { return nil, fmt.Errorf("baishun platform failed: code=%d message=%s", result.Code, result.Message) } return result.Data, nil } func baishunLooksLikeHTML(raw []byte, contentType string) bool { contentType = strings.ToLower(strings.TrimSpace(contentType)) if strings.Contains(contentType, "text/html") { return true } body := strings.ToLower(strings.TrimSpace(string(raw))) return strings.HasPrefix(body, "