This commit is contained in:
hy001 2026-04-29 19:29:12 +08:00
parent c16c6a7bd9
commit 65b0558e03
6 changed files with 264 additions and 38 deletions

View File

@ -270,6 +270,7 @@ func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, f
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 {
@ -279,6 +280,13 @@ func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, f
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
}
@ -302,7 +310,7 @@ func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, f
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
Showcase: defaultShowcase,
Sort: parseSort(vendorGameID),
FullScreen: true,
FullScreen: fullScreen,
ClientOrigin: "COMMON",
GameMode: "[3]",
}
@ -387,6 +395,19 @@ func parseSort(value string) int64 {
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

View File

@ -138,6 +138,164 @@ func TestImportCatalogGamesCanDefaultToHidden(t *testing.T) {
}
}
func TestImportCatalogGamesMarksHalfScreenFromCatalog(t *testing.T) {
ctx := context.Background()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(
&model.LingxianProviderConfig{},
&model.LingxianGameCatalog{},
&model.SysGameListConfig{},
&model.SysGameListVendorExt{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
now := time.Now()
if err := db.Create(&model.LingxianProviderConfig{
ID: 31,
SysOrigin: "LIKEI",
Profile: profileProd,
Active: true,
AppKey: "test-key",
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed provider: %v", err)
}
if err := db.Create(&model.LingxianGameCatalog{
ID: 32,
SysOrigin: "LIKEI",
Profile: profileProd,
InternalGameID: "1",
VendorGameID: "1",
Name: "Half Lingxian",
PreviewURL: "https://example.test/fruit_party_half/index.html",
DownloadURL: "https://example.test/fruit_party_half/index.html",
RawJSON: `{"gameId":"1","half_url":"https://example.test/fruit_party_half/index.html","full_url":""}`,
Status: catalogEnabled,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed catalog: %v", err)
}
service := NewService(config.Config{HTTP: config.HTTPConfig{Timeout: time.Second}}, db, nil)
resp, err := service.ImportCatalogGames(ctx, "LIKEI", profileProd, []string{"1"})
if err != nil {
t.Fatalf("ImportCatalogGames: %v", err)
}
if resp.Imported != 1 {
t.Fatalf("imported = %d, want 1", resp.Imported)
}
var cfg model.SysGameListConfig
if err := db.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", "LIKEI", vendorType, "1").First(&cfg).Error; err != nil {
t.Fatalf("unified config missing: %v", err)
}
if cfg.FullScreen {
t.Fatalf("fullScreen = true, want false")
}
}
func TestImportCatalogGamesRepairsExistingHalfScreenConfig(t *testing.T) {
ctx := context.Background()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(
&model.LingxianProviderConfig{},
&model.LingxianGameCatalog{},
&model.SysGameListConfig{},
&model.SysGameListVendorExt{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
now := time.Now()
if err := db.Create(&model.LingxianProviderConfig{
ID: 41,
SysOrigin: "LIKEI",
Profile: profileProd,
Active: true,
AppKey: "test-key",
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed provider: %v", err)
}
if err := db.Create(&model.LingxianGameCatalog{
ID: 42,
SysOrigin: "LIKEI",
Profile: profileProd,
InternalGameID: "1",
VendorGameID: "1",
Name: "Half Lingxian",
PreviewURL: "https://example.test/fruit_party_half/index.html",
DownloadURL: "https://example.test/fruit_party_half/index.html",
RawJSON: `{"gameId":"1","half_url":"https://example.test/fruit_party_half/index.html","full_url":""}`,
Status: catalogEnabled,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed catalog: %v", err)
}
if err := db.Create(&model.SysGameListConfig{
ID: 43,
SysOrigin: "LIKEI",
GameOrigin: vendorType,
GameID: "1",
Name: "Half Lingxian",
Category: roomCategory,
GameCode: "1",
Showcase: true,
Sort: 1,
FullScreen: true,
ClientOrigin: "COMMON",
GameMode: "[3]",
}).Error; err != nil {
t.Fatalf("seed config: %v", err)
}
if err := db.Create(&model.SysGameListVendorExt{
ID: 44,
GameListConfigID: 43,
SysOrigin: "LIKEI",
Profile: profileProd,
VendorType: vendorType,
VendorGameID: "1",
LaunchMode: launchModeH5,
PackageURL: "https://example.test/fruit_party_half/index.html",
PreviewURL: "https://example.test/fruit_party_half/index.html",
BridgeSchemaVersion: "1.0",
ExtraJSON: `{"gameType":"LINGXIAN"}`,
Enabled: true,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed vendor ext: %v", err)
}
service := NewService(config.Config{HTTP: config.HTTPConfig{Timeout: time.Second}}, db, nil)
resp, err := service.ImportCatalogGames(ctx, "LIKEI", profileProd, []string{"1"})
if err != nil {
t.Fatalf("ImportCatalogGames: %v", err)
}
if resp.Existing != 1 {
t.Fatalf("existing = %d, want 1", resp.Existing)
}
var cfg model.SysGameListConfig
if err := db.First(&cfg, 43).Error; err != nil {
t.Fatalf("config missing: %v", err)
}
if cfg.FullScreen {
t.Fatalf("fullScreen = true, want false after repair")
}
}
func TestSaveProviderConfigPersistsTestRoomID(t *testing.T) {
ctx := context.Background()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})

View File

@ -241,7 +241,7 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque
return nil, err
}
if err := s.java.ChangeGoldBalance(ctx, buildWalletCommand(user.UserID, sysOrigin, order)); err != nil {
appErr := NewAppError(http.StatusBadGateway, "vip_wallet_deduct_failed", err.Error())
appErr := mapWalletPurchaseError(err)
if saveErr := s.markOrderFailed(tx, &order, err.Error()); saveErr != nil {
return nil, saveErr
}
@ -487,6 +487,17 @@ func expireState(state *model.UserVipState, now time.Time) {
state.UpdateTime = now
}
func mapWalletPurchaseError(err error) *AppError {
message := err.Error()
normalized := strings.ToLower(message)
if strings.Contains(normalized, "insufficient_balance") ||
strings.Contains(normalized, "balance not made") ||
strings.Contains(normalized, `"errorcode":5000`) {
return NewAppError(http.StatusNotAcceptable, "vip_insufficient_balance", "insufficient balance")
}
return NewAppError(http.StatusBadGateway, "vip_wallet_deduct_failed", message)
}
func buildWalletCommand(userID int64, sysOrigin string, order model.VipOrderRecord) integration.GoldReceiptCommand {
return integration.GoldReceiptCommand{
ReceiptType: walletReceiptExpenditure,

View File

@ -136,6 +136,16 @@ func normalizeResource(resource VipResourcePayload) VipResourcePayload {
}
resource.Name = strings.TrimSpace(resource.Name)
resource.URL = strings.TrimSpace(resource.URL)
resource.SourceURL = strings.TrimSpace(resource.SourceURL)
resource.ResourceURL = strings.TrimSpace(resource.ResourceURL)
if resource.URL == "" {
resource.URL = resource.SourceURL
}
if resource.URL == "" {
resource.URL = resource.ResourceURL
}
resource.SourceURL = resource.URL
resource.ResourceURL = resource.URL
return resource
}

View File

@ -2,6 +2,8 @@ package vip
import (
"encoding/json"
"errors"
"net/http"
"testing"
"time"
@ -104,22 +106,53 @@ func TestBuildPurchasePlanRejectsDowngrade(t *testing.T) {
func TestVipResourcePayloadAcceptsStringResourceID(t *testing.T) {
var payload VipResourcePayload
if err := json.Unmarshal([]byte(`{"resourceId":"2045029471274201089","name":"vip","url":"https://example.com/a.svga"}`), &payload); err != nil {
if err := json.Unmarshal([]byte(`{"resourceId":"2045029471274201089","name":"vip","sourceUrl":"https://example.com/a.svga"}`), &payload); err != nil {
t.Fatalf("unmarshal resource payload: %v", err)
}
payload = normalizeResource(payload)
if payload.ResourceID.Int64() != 2045029471274201089 {
t.Fatalf("resource id = %d, want 2045029471274201089", payload.ResourceID.Int64())
}
if payload.URL != "https://example.com/a.svga" || payload.SourceURL != payload.URL || payload.ResourceURL != payload.URL {
t.Fatalf("resource urls = url:%q sourceUrl:%q resourceUrl:%q", payload.URL, payload.SourceURL, payload.ResourceURL)
}
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource payload: %v", err)
}
if got := string(body); got != `{"resourceId":"2045029471274201089","name":"vip","url":"https://example.com/a.svga"}` {
if got := string(body); got != `{"resourceId":"2045029471274201089","name":"vip","url":"https://example.com/a.svga","sourceUrl":"https://example.com/a.svga","resourceUrl":"https://example.com/a.svga"}` {
t.Fatalf("json = %s", got)
}
}
func TestMapWalletPurchaseErrorInsufficientBalance(t *testing.T) {
err := errors.New(`java api failed: status=406 body={"errorCode":5000,"errorCodeName":"INSUFFICIENT_BALANCE","errorMsg":"balance not made"}`)
appErr := mapWalletPurchaseError(err)
if appErr.Status != http.StatusNotAcceptable {
t.Fatalf("status = %d, want %d", appErr.Status, http.StatusNotAcceptable)
}
if appErr.Code != "vip_insufficient_balance" {
t.Fatalf("code = %s, want vip_insufficient_balance", appErr.Code)
}
if appErr.Message != "insufficient balance" {
t.Fatalf("message = %s, want insufficient balance", appErr.Message)
}
}
func TestMapWalletPurchaseErrorGatewayFailure(t *testing.T) {
err := errors.New("java api failed: status=500 body=boom")
appErr := mapWalletPurchaseError(err)
if appErr.Status != http.StatusBadGateway {
t.Fatalf("status = %d, want %d", appErr.Status, http.StatusBadGateway)
}
if appErr.Code != "vip_wallet_deduct_failed" {
t.Fatalf("code = %s, want vip_wallet_deduct_failed", appErr.Code)
}
}
func vipTestConfigs() map[int]model.VipLevelConfig {
configs := make(map[int]model.VipLevelConfig, levelCount)
for level := 1; level <= levelCount; level++ {

View File

@ -88,9 +88,11 @@ type VipLevelPayload struct {
}
type VipResourcePayload struct {
ResourceID ResourceID `json:"resourceId"`
Name string `json:"name"`
URL string `json:"url"`
ResourceID ResourceID `json:"resourceId"`
Name string `json:"name"`
URL string `json:"url"`
SourceURL string `json:"sourceUrl,omitempty"`
ResourceURL string `json:"resourceUrl,omitempty"`
}
type ResourceID int64
@ -293,6 +295,17 @@ func defaultLevelPayload(level int) VipLevelPayload {
}
}
func resourcePayload(resourceID int64, name string, url string) VipResourcePayload {
url = strings.TrimSpace(url)
return VipResourcePayload{
ResourceID: ResourceID(resourceID),
Name: strings.TrimSpace(name),
URL: url,
SourceURL: url,
ResourceURL: url,
}
}
func payloadFromConfig(row model.VipLevelConfig) VipLevelPayload {
duration := row.DurationDays
if duration <= 0 {
@ -314,32 +327,12 @@ func payloadFromConfig(row model.VipLevelConfig) VipLevelPayload {
Enabled: row.Enabled,
DurationDays: duration,
PriceGold: row.PriceGold,
Badge: VipResourcePayload{
ResourceID: ResourceID(row.BadgeResourceID),
Name: row.BadgeName,
URL: row.BadgeURL,
},
AvatarFrame: VipResourcePayload{
ResourceID: ResourceID(row.AvatarFrameResourceID),
Name: row.AvatarFrameName,
URL: row.AvatarFrameURL,
},
EntryEffect: VipResourcePayload{
ResourceID: ResourceID(row.EntryEffectResourceID),
Name: row.EntryEffectName,
URL: row.EntryEffectURL,
},
ChatBubble: VipResourcePayload{
ResourceID: ResourceID(row.ChatBubbleResourceID),
Name: row.ChatBubbleName,
URL: row.ChatBubbleURL,
},
FloatPicture: VipResourcePayload{
ResourceID: ResourceID(row.FloatPictureResourceID),
Name: row.FloatPictureName,
URL: row.FloatPictureURL,
},
UpdateTime: formatTime(row.UpdateTime),
Badge: resourcePayload(row.BadgeResourceID, row.BadgeName, row.BadgeURL),
AvatarFrame: resourcePayload(row.AvatarFrameResourceID, row.AvatarFrameName, row.AvatarFrameURL),
EntryEffect: resourcePayload(row.EntryEffectResourceID, row.EntryEffectName, row.EntryEffectURL),
ChatBubble: resourcePayload(row.ChatBubbleResourceID, row.ChatBubbleName, row.ChatBubbleURL),
FloatPicture: resourcePayload(row.FloatPictureResourceID, row.FloatPictureName, row.FloatPictureURL),
UpdateTime: formatTime(row.UpdateTime),
}
}
@ -368,11 +361,11 @@ func statePayloadFromModel(row *model.UserVipState, now time.Time) UserVipStateP
payload.DisplayName = ""
return payload
}
payload.Badge = VipResourcePayload{ResourceID: ResourceID(row.BadgeResourceID), Name: row.BadgeName, URL: row.BadgeURL}
payload.AvatarFrame = VipResourcePayload{ResourceID: ResourceID(row.AvatarFrameResourceID), Name: row.AvatarFrameName, URL: row.AvatarFrameURL}
payload.EntryEffect = VipResourcePayload{ResourceID: ResourceID(row.EntryEffectResourceID), Name: row.EntryEffectName, URL: row.EntryEffectURL}
payload.ChatBubble = VipResourcePayload{ResourceID: ResourceID(row.ChatBubbleResourceID), Name: row.ChatBubbleName, URL: row.ChatBubbleURL}
payload.FloatPicture = VipResourcePayload{ResourceID: ResourceID(row.FloatPictureResourceID), Name: row.FloatPictureName, URL: row.FloatPictureURL}
payload.Badge = resourcePayload(row.BadgeResourceID, row.BadgeName, row.BadgeURL)
payload.AvatarFrame = resourcePayload(row.AvatarFrameResourceID, row.AvatarFrameName, row.AvatarFrameURL)
payload.EntryEffect = resourcePayload(row.EntryEffectResourceID, row.EntryEffectName, row.EntryEffectURL)
payload.ChatBubble = resourcePayload(row.ChatBubbleResourceID, row.ChatBubbleName, row.ChatBubbleURL)
payload.FloatPicture = resourcePayload(row.FloatPictureResourceID, row.FloatPictureName, row.FloatPictureURL)
return payload
}