vip
This commit is contained in:
parent
c16c6a7bd9
commit
65b0558e03
@ -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
|
||||
|
||||
@ -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{})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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++ {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user