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) }