427 lines
12 KiB
Go
427 lines
12 KiB
Go
package lingxian
|
|
|
|
import (
|
|
"chatapp3-golang/internal/common"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func (s *Service) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
|
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
|
profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runtimeCfg, err := s.requireRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
games, err := s.fetchPlatformGames(ctx, runtimeCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filter := buildVendorGameIDFilter(req.VendorGameIDs)
|
|
resp := &SyncCatalogResponse{}
|
|
for _, game := range games {
|
|
vendorGameID := strings.TrimSpace(game.GameID)
|
|
if vendorGameID == "" {
|
|
resp.Skipped++
|
|
continue
|
|
}
|
|
if len(filter) > 0 {
|
|
if _, ok := filter[vendorGameID]; !ok {
|
|
resp.Skipped++
|
|
continue
|
|
}
|
|
}
|
|
id, err := utils.NextID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
record := model.LingxianGameCatalog{
|
|
ID: id,
|
|
SysOrigin: sysOrigin,
|
|
Profile: profile,
|
|
InternalGameID: internalGameID(vendorGameID),
|
|
VendorGameID: vendorGameID,
|
|
Name: catalogName(game),
|
|
Cover: "",
|
|
PreviewURL: strings.TrimSpace(game.HalfURL),
|
|
DownloadURL: defaultIfBlank(game.FullURL, game.HalfURL),
|
|
PackageVersion: fmt.Sprintf("%d", game.Version),
|
|
RawJSON: utils.MustJSONString(game, ""),
|
|
Status: catalogEnabled,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := s.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", "raw_json", "status", "update_time",
|
|
}),
|
|
}).Create(&record).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Force {
|
|
resp.Updated++
|
|
} else {
|
|
resp.Inserted++
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) fetchPlatformGames(ctx context.Context, runtimeCfg runtimeConfig) ([]platformGame, error) {
|
|
endpoint := strings.TrimSpace(runtimeCfg.GameListURL)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
|
return nil, fmt.Errorf("lingxian platform failed: url=%s status=%d body=%s", endpoint, resp.StatusCode, shortBody(raw))
|
|
}
|
|
var games []platformGame
|
|
if err := json.Unmarshal(raw, &games); err != nil {
|
|
return nil, fmt.Errorf("lingxian platform returned invalid json: url=%s error=%v body=%s", endpoint, err, shortBody(raw))
|
|
}
|
|
return games, nil
|
|
}
|
|
|
|
func (s *Service) PageCatalog(ctx context.Context, sysOrigin, profile, keyword string, cursor, limit int) (*CatalogPageResponse, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cursor = normalizePageCursor(cursor)
|
|
limit = normalizePageLimit(limit)
|
|
|
|
countQuery := s.buildCatalogQuery(ctx, sysOrigin, profile, keyword)
|
|
var total int64
|
|
if err := countQuery.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
type row struct {
|
|
VendorGameID string
|
|
Profile string
|
|
InternalGameID string
|
|
Name string
|
|
Cover string
|
|
PreviewURL string
|
|
DownloadURL string
|
|
PackageVersion string
|
|
Status string
|
|
AddedConfigID *int64
|
|
Showcase *bool
|
|
UpdateTime string
|
|
}
|
|
var rows []row
|
|
if err := s.buildCatalogQuery(ctx, sysOrigin, profile, keyword).
|
|
Select(`
|
|
cat.vendor_game_id AS vendor_game_id,
|
|
cat.profile AS profile,
|
|
cat.internal_game_id AS internal_game_id,
|
|
cat.name AS name,
|
|
cat.cover AS cover,
|
|
cat.preview_url AS preview_url,
|
|
cat.download_url AS download_url,
|
|
cat.package_version AS package_version,
|
|
cat.status AS status,
|
|
cfg.id AS added_config_id,
|
|
cfg.is_showcase AS showcase,
|
|
DATE_FORMAT(cat.update_time, '%Y-%m-%d %H:%i:%s') AS update_time
|
|
`).
|
|
Order("cat.vendor_game_id ASC").
|
|
Limit(limit).
|
|
Offset((cursor - 1) * limit).
|
|
Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items := make([]CatalogItem, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, CatalogItem{
|
|
VendorGameID: row.VendorGameID,
|
|
Profile: normalizeProfile(row.Profile),
|
|
GameID: row.InternalGameID,
|
|
Name: row.Name,
|
|
Cover: row.Cover,
|
|
PreviewURL: row.PreviewURL,
|
|
DownloadURL: row.DownloadURL,
|
|
PackageVersion: row.PackageVersion,
|
|
Status: row.Status,
|
|
Added: row.AddedConfigID != nil && *row.AddedConfigID > 0,
|
|
Showcase: row.Showcase != nil && *row.Showcase,
|
|
UpdateTime: row.UpdateTime,
|
|
})
|
|
}
|
|
return &CatalogPageResponse{Cursor: cursor, Limit: limit, Records: items, Total: total}, nil
|
|
}
|
|
|
|
func (s *Service) buildCatalogQuery(ctx context.Context, sysOrigin, profile, keyword string) *gorm.DB {
|
|
query := s.db.WithContext(ctx).
|
|
Table("lingxian_game_catalog AS cat").
|
|
Joins(`
|
|
LEFT JOIN sys_game_list_vendor_ext ext
|
|
ON ext.sys_origin = cat.sys_origin
|
|
AND ext.profile = cat.profile
|
|
AND ext.vendor_type = ?
|
|
AND ext.vendor_game_id = cat.vendor_game_id
|
|
AND ext.enabled = 1
|
|
`, vendorType).
|
|
Joins(`
|
|
LEFT JOIN sys_game_list_config cfg
|
|
ON cfg.id = ext.game_list_config_id
|
|
AND cfg.sys_origin = cat.sys_origin
|
|
AND cfg.game_origin = ?
|
|
`, vendorType).
|
|
Where("cat.sys_origin = ? AND cat.profile = ?", sysOrigin, profile)
|
|
keyword = strings.TrimSpace(keyword)
|
|
if keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
query = query.Where("cat.name LIKE ? OR cat.internal_game_id LIKE ? OR cat.vendor_game_id LIKE ?", like, like, like)
|
|
}
|
|
return query
|
|
}
|
|
|
|
func (s *Service) ImportCatalogGames(ctx context.Context, sysOrigin, profile string, vendorGameIDs []string, defaultShowcase ...bool) (*ImportCatalogResponse, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tx := s.db.WithContext(ctx).Begin()
|
|
if tx.Error != nil {
|
|
return nil, tx.Error
|
|
}
|
|
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs), resolveImportDefaultShowcase(defaultShowcase...))
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit().Error; err != nil {
|
|
return nil, err
|
|
}
|
|
s.clearJavaGameListCache(ctx)
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) SyncAdminGames(ctx context.Context, req SyncCatalogRequest) (*SyncAdminCatalogResponse, error) {
|
|
syncResp, err := s.SyncCatalog(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs, resolveBoolPtrDefault(req.DefaultShowcase, true))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &SyncAdminCatalogResponse{
|
|
Inserted: syncResp.Inserted,
|
|
Updated: syncResp.Updated,
|
|
Skipped: syncResp.Skipped,
|
|
Existing: importResp.Existing,
|
|
Imported: importResp.Imported,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, filter map[string]struct{}, defaultShowcase bool) (*ImportCatalogResponse, error) {
|
|
query := tx.Where("sys_origin = ? AND profile = ? AND status = ?", sysOrigin, profile, catalogEnabled)
|
|
if len(filter) > 0 {
|
|
ids := make([]string, 0, len(filter))
|
|
for id := range filter {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
query = query.Where("vendor_game_id IN ?", ids)
|
|
}
|
|
|
|
var catalogs []model.LingxianGameCatalog
|
|
if err := query.Order("vendor_game_id ASC").Find(&catalogs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp := &ImportCatalogResponse{}
|
|
for _, catalog := range catalogs {
|
|
vendorGameID := strings.TrimSpace(catalog.VendorGameID)
|
|
if vendorGameID == "" {
|
|
continue
|
|
}
|
|
now := time.Now()
|
|
fullScreen := catalogFullScreen(catalog)
|
|
var ext model.SysGameListVendorExt
|
|
err := tx.Where("sys_origin = ? AND profile = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, profile, vendorType, vendorGameID).First(&ext).Error
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
|
return nil, err
|
|
}
|
|
if err == nil && ext.GameListConfigID > 0 {
|
|
var cfg model.SysGameListConfig
|
|
cfgErr := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, sysOrigin, vendorType).First(&cfg).Error
|
|
if cfgErr == nil {
|
|
if cfg.FullScreen != fullScreen {
|
|
if err := tx.Model(&model.SysGameListConfig{}).
|
|
Where("id = ?", cfg.ID).
|
|
Update("full_screen", fullScreen).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
resp.Existing++
|
|
continue
|
|
}
|
|
if cfgErr != gorm.ErrRecordNotFound {
|
|
return nil, cfgErr
|
|
}
|
|
}
|
|
|
|
nextCfgID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return nil, idErr
|
|
}
|
|
cfg := model.SysGameListConfig{
|
|
ID: nextCfgID,
|
|
SysOrigin: sysOrigin,
|
|
GameOrigin: vendorType,
|
|
GameID: defaultIfBlank(catalog.InternalGameID, vendorGameID),
|
|
Name: catalog.Name,
|
|
Category: roomCategory,
|
|
GameCode: vendorGameID,
|
|
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
|
Showcase: defaultShowcase,
|
|
Sort: parseSort(vendorGameID),
|
|
FullScreen: fullScreen,
|
|
ClientOrigin: "COMMON",
|
|
GameMode: "[3]",
|
|
}
|
|
if err := tx.Create(&cfg).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nextID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return nil, idErr
|
|
}
|
|
newExt := model.SysGameListVendorExt{
|
|
ID: nextID,
|
|
GameListConfigID: cfg.ID,
|
|
SysOrigin: sysOrigin,
|
|
Profile: profile,
|
|
VendorType: vendorType,
|
|
VendorGameID: vendorGameID,
|
|
LaunchMode: launchModeH5,
|
|
PackageVersion: catalog.PackageVersion,
|
|
PackageURL: catalog.DownloadURL,
|
|
PreviewURL: catalog.PreviewURL,
|
|
BridgeSchemaVersion: "1.0",
|
|
ExtraJSON: `{"gameType":"LINGXIAN"}`,
|
|
Enabled: true,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := tx.Create(&newExt).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Imported++
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func resolveImportDefaultShowcase(values ...bool) bool {
|
|
if len(values) == 0 {
|
|
return true
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
func resolveBoolPtrDefault(value *bool, fallback bool) bool {
|
|
if value == nil {
|
|
return fallback
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func buildVendorGameIDFilter(vendorGameIDs []string) map[string]struct{} {
|
|
filter := make(map[string]struct{}, len(vendorGameIDs))
|
|
for _, id := range vendorGameIDs {
|
|
id = strings.TrimSpace(id)
|
|
if id != "" {
|
|
filter[id] = struct{}{}
|
|
}
|
|
}
|
|
return filter
|
|
}
|
|
|
|
func normalizePageCursor(cursor int) int {
|
|
if cursor <= 0 {
|
|
return 1
|
|
}
|
|
return cursor
|
|
}
|
|
|
|
func normalizePageLimit(limit int) int {
|
|
if limit <= 0 {
|
|
return 20
|
|
}
|
|
if limit > 200 {
|
|
return 200
|
|
}
|
|
return limit
|
|
}
|
|
|
|
func parseSort(value string) int64 {
|
|
var sortValue int64
|
|
_, _ = fmt.Sscanf(strings.TrimSpace(value), "%d", &sortValue)
|
|
return sortValue
|
|
}
|
|
|
|
func catalogFullScreen(catalog model.LingxianGameCatalog) bool {
|
|
type rawCatalog struct {
|
|
FullURL string `json:"full_url"`
|
|
HalfURL string `json:"half_url"`
|
|
}
|
|
var raw rawCatalog
|
|
_ = json.Unmarshal([]byte(strings.TrimSpace(catalog.RawJSON)), &raw)
|
|
if strings.TrimSpace(raw.FullURL) == "" && strings.TrimSpace(raw.HalfURL) != "" {
|
|
return false
|
|
}
|
|
return !strings.Contains(strings.ToLower(defaultIfBlank(catalog.PreviewURL, catalog.DownloadURL)), "_half")
|
|
}
|
|
|
|
func shortBody(raw []byte) string {
|
|
body := strings.TrimSpace(string(raw))
|
|
const max = 512
|
|
if len(body) <= max {
|
|
return body
|
|
}
|
|
return body[:max] + "..."
|
|
}
|
|
|
|
func (s *Service) FetchAdminCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
|
return s.SyncCatalog(ctx, req)
|
|
}
|
|
|
|
func badRequest(code, message string) error {
|
|
return common.NewAppError(http.StatusBadRequest, code, message)
|
|
}
|