2026-04-24 13:04:06 +08:00

177 lines
5.0 KiB
Go

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 := strings.TrimRight(runtimeCfg.PlatformBaseURL, "/") + "/v1/api/gamelist"
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, "<!doctype html") ||
strings.HasPrefix(body, "<!doctypehtml") ||
strings.HasPrefix(body, "<html")
}
func baishunShortBody(raw []byte) string {
body := strings.TrimSpace(string(raw))
const max = 512
if len(body) <= max {
return body
}
return body[:max] + "..."
}