441 lines
15 KiB
Go
441 lines
15 KiB
Go
package hotgame
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"chatapp3-golang/internal/model"
|
||
"chatapp3-golang/internal/utils"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
var defaultCatalogGames = []defaultCatalogGame{
|
||
{
|
||
GameID: "1",
|
||
Name: "幸运77(Lucky77)",
|
||
TestURL: "https://hk.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}",
|
||
ProdURL: "https://cdn-res.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}",
|
||
},
|
||
{
|
||
GameID: "13",
|
||
Name: "海盗捕鱼(PirateFishing)",
|
||
TestURL: "https://hk.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
ProdURL: "https://cdn-res.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
},
|
||
{
|
||
GameID: "19",
|
||
Name: "FortuneSlot (宝石solt)",
|
||
TestURL: "https://hk.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
ProdURL: "https://cdn-res.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
},
|
||
{
|
||
GameID: "15",
|
||
Name: "美味摩天轮(Delicious)",
|
||
TestURL: "https://hk.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
ProdURL: "https://cdn-res.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
},
|
||
{
|
||
GameID: "20",
|
||
Name: "水果派对(FruitParty)",
|
||
TestURL: "https://hk.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
ProdURL: "https://cdn-res.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
},
|
||
{
|
||
GameID: "16",
|
||
Name: "太空摩天轮(GreedyStar)",
|
||
TestURL: "https://hk.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
ProdURL: "https://cdn-res.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
||
},
|
||
}
|
||
|
||
// SyncCatalog 把表格里确认过的热游可用游戏同步到本地目录表。
|
||
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
|
||
}
|
||
if _, err := s.requireRuntimeConfigForProfile(ctx, sysOrigin, profile); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
filter := buildVendorGameIDFilter(req.VendorGameIDs)
|
||
resp := &SyncCatalogResponse{}
|
||
for _, game := range defaultCatalogGames {
|
||
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()
|
||
launchURL := hotgameLaunchURLForProfile(game, profile)
|
||
record := model.HotgameGameCatalog{
|
||
ID: id,
|
||
SysOrigin: sysOrigin,
|
||
Profile: profile,
|
||
InternalGameID: internalGameID(vendorGameID),
|
||
VendorGameID: vendorGameID,
|
||
Name: game.Name,
|
||
Cover: "",
|
||
PreviewURL: launchURL,
|
||
DownloadURL: launchURL,
|
||
PackageVersion: "2026",
|
||
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
|
||
}
|
||
|
||
// PageCatalog 返回热游目录,并标记每个目录游戏是否已经进入统一游戏列表。
|
||
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("CAST(cat.vendor_game_id AS UNSIGNED) 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("hotgame_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
|
||
}
|
||
|
||
// FetchAdminCatalog 供后台按钮调用,热游没有远程目录接口,因此读取内置对接表目录。
|
||
func (s *Service) FetchAdminCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
||
return s.SyncCatalog(ctx, req)
|
||
}
|
||
|
||
// ImportCatalogGames 把热游目录导入统一游戏列表,app 侧列表随后会通过统一 provider 返回。
|
||
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
|
||
}
|
||
|
||
// SyncAdminGames 一次完成目录同步和统一游戏列表导入,便于后台首次配置。
|
||
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.HotgameGameCatalog
|
||
if err := query.Order("CAST(vendor_game_id AS UNSIGNED) 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 := false
|
||
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 {
|
||
if err := s.refreshExistingGame(tx, ext.GameListConfigID, catalog, fullScreen); err != nil {
|
||
return nil, err
|
||
}
|
||
resp.Existing++
|
||
continue
|
||
}
|
||
|
||
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: `{"launchParams":{"gameType":"HOTGAME","screenMode":"seven"}}`,
|
||
Enabled: true,
|
||
CreateTime: now,
|
||
UpdateTime: now,
|
||
}
|
||
if err := tx.Create(&newExt).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
resp.Imported++
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func (s *Service) refreshExistingGame(tx *gorm.DB, configID int64, catalog model.HotgameGameCatalog, fullScreen bool) error {
|
||
// 目录链接来自热游正式表格,重新同步时要同时刷新主表展示信息和扩展表启动入口,避免旧 URL 继续下发给 app。
|
||
if err := tx.Model(&model.SysGameListConfig{}).
|
||
Where("id = ? AND game_origin = ?", configID, vendorType).
|
||
Updates(map[string]any{
|
||
"name": catalog.Name,
|
||
"cover": defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||
"full_screen": fullScreen,
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
return tx.Model(&model.SysGameListVendorExt{}).
|
||
Where("game_list_config_id = ? AND vendor_type = ?", configID, vendorType).
|
||
Updates(map[string]any{
|
||
"package_version": catalog.PackageVersion,
|
||
"package_url": catalog.DownloadURL,
|
||
"preview_url": catalog.PreviewURL,
|
||
"extra_json": `{"launchParams":{"gameType":"HOTGAME","screenMode":"seven"}}`,
|
||
"update_time": time.Now(),
|
||
}).Error
|
||
}
|
||
|
||
func hotgameLaunchURLForProfile(game defaultCatalogGame, profile string) string {
|
||
if normalizeProfile(profile) == profileTest {
|
||
return strings.TrimSpace(game.TestURL)
|
||
}
|
||
return strings.TrimSpace(game.ProdURL)
|
||
}
|
||
|
||
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 catalogRawMode(rawJSON string) string {
|
||
var raw struct {
|
||
Mode string `json:"mode"`
|
||
}
|
||
_ = json.Unmarshal([]byte(strings.TrimSpace(rawJSON)), &raw)
|
||
return strings.TrimSpace(raw.Mode)
|
||
}
|