177 lines
5.0 KiB
Go
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] + "..."
|
|
}
|