z
增加邀请活动
This commit is contained in:
parent
42b3a4b595
commit
c4f75b4846
@ -7,6 +7,7 @@ import (
|
||||
"chatapp3-golang/internal/service/gameopen"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
"chatapp3-golang/internal/service/signinreward"
|
||||
@ -32,6 +33,7 @@ func main() {
|
||||
|
||||
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||
lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet)
|
||||
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||
@ -46,10 +48,12 @@ func main() {
|
||||
|
||||
gameProviders := gameprovider.NewRegistry(
|
||||
baishun.NewAppProvider(baishunService),
|
||||
lingxian.NewAppProvider(lingxianService),
|
||||
)
|
||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||
Invite: inviteService,
|
||||
Baishun: baishunService,
|
||||
Lingxian: lingxianService,
|
||||
GameOpen: gameOpenService,
|
||||
GameProviders: gameProviders,
|
||||
LuckyGift: luckyGiftService,
|
||||
|
||||
@ -17,6 +17,7 @@ type Config struct {
|
||||
Invite InviteConfig
|
||||
WeekStar WeekStarConfig
|
||||
Baishun BaishunConfig
|
||||
Lingxian LingxianConfig
|
||||
GameOpen GameOpenConfig
|
||||
LuckyGift LuckyGiftConfig
|
||||
}
|
||||
@ -104,6 +105,12 @@ type BaishunConfig struct {
|
||||
SSTokenTTLSeconds int
|
||||
}
|
||||
|
||||
// LingxianConfig 保存灵仙模块配置。
|
||||
type LingxianConfig struct {
|
||||
GameListURL string
|
||||
AppKey string
|
||||
}
|
||||
|
||||
// GameOpenConfig 保存统一三方游戏回调配置。
|
||||
type GameOpenConfig struct {
|
||||
PublicBaseURL string
|
||||
@ -197,6 +204,10 @@ func Load() Config {
|
||||
LaunchCodeTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_LAUNCH_CODE_TTL_SECONDS", "BAISHUN_LAUNCH_CODE_TTL_SECONDS"}, 300),
|
||||
SSTokenTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_SS_TOKEN_TTL_SECONDS", "BAISHUN_SS_TOKEN_TTL_SECONDS"}, 86400),
|
||||
},
|
||||
Lingxian: LingxianConfig{
|
||||
GameListURL: getEnvAny([]string{"CHATAPP_LINGXIAN_GAME_LIST_URL", "LINGXIAN_GAME_LIST_URL"}, "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"),
|
||||
AppKey: getEnvAny([]string{"CHATAPP_LINGXIAN_APP_KEY", "LINGXIAN_APP_KEY", "GAME_HKYS_SIGN_KEY"}, ""),
|
||||
},
|
||||
GameOpen: GameOpenConfig{
|
||||
PublicBaseURL: getEnvAny([]string{"CHATAPP_GAME_OPEN_PUBLIC_BASE_URL", "GAME_OPEN_PUBLIC_BASE_URL"}, ""),
|
||||
AppKey: getEnvAny([]string{"CHATAPP_GAME_OPEN_APP_KEY", "GAME_OPEN_APP_KEY"}, "game-open-test-key"),
|
||||
|
||||
@ -295,18 +295,51 @@ func (c *Client) GetDeviceFingerprint(ctx context.Context, userID int64) (string
|
||||
|
||||
func (c *Client) EnsureInviteCode(ctx context.Context, authorization string) (InviteCode, error) {
|
||||
endpoint := c.cfg.Java.AppBaseURL + "/activity/invite/user/my/invite/code"
|
||||
headers := http.Header{}
|
||||
headers := javaAppHeadersFromAuthorization(authorization)
|
||||
if authorization != "" {
|
||||
headers.Set("Authorization", authorization)
|
||||
}
|
||||
var resp InviteCode
|
||||
var resp resultResponse[InviteCode]
|
||||
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
|
||||
return InviteCode{}, err
|
||||
}
|
||||
if strings.TrimSpace(resp.InviteCode) == "" {
|
||||
if strings.TrimSpace(resp.Body.InviteCode) == "" {
|
||||
return InviteCode{}, fmt.Errorf("invite code response is empty")
|
||||
}
|
||||
return resp, nil
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func javaAppHeadersFromAuthorization(authorization string) http.Header {
|
||||
headers := http.Header{}
|
||||
token := trimBearerToken(authorization)
|
||||
sysOrigin := "LIKEI"
|
||||
var userID int64
|
||||
if credential, err := parseUserCredentialToken(token); err == nil {
|
||||
userID = int64(credential.UserID)
|
||||
if strings.TrimSpace(credential.SysOrigin) != "" {
|
||||
sysOrigin = strings.TrimSpace(credential.SysOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
headers.Set("Req-Sys-Origin", "origin="+sysOrigin+";originChild="+sysOrigin)
|
||||
headers.Set("Req-Client", "Android")
|
||||
headers.Set("Req-App-Intel", "version=1.0.0;build=100;channel=local;model=go-service;sysVersion=go")
|
||||
headers.Set("Req-Zone", "Asia/Shanghai")
|
||||
headers.Set("Req-Imei", "go-service")
|
||||
if userID > 0 {
|
||||
headers.Set("Req-Inner-Internal", fmt.Sprintf("time=%d;uid=%d;", time.Now().UnixMilli(), userID))
|
||||
} else {
|
||||
headers.Set("Req-Inner-Internal", fmt.Sprintf("time=%d;", time.Now().UnixMilli()))
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func trimBearerToken(authorization string) string {
|
||||
token := strings.TrimSpace(authorization)
|
||||
if len(token) >= 7 && strings.EqualFold(token[:7], "Bearer ") {
|
||||
return strings.TrimSpace(token[7:])
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (c *Client) GrantGold(ctx context.Context, req GrantGoldRequest) error {
|
||||
|
||||
@ -102,6 +102,44 @@ type BaishunGameCatalog struct {
|
||||
// TableName 返回百顺目录表名。
|
||||
func (BaishunGameCatalog) TableName() string { return "baishun_game_catalog" }
|
||||
|
||||
// LingxianProviderConfig 保存每个系统的灵仙接入配置。
|
||||
type LingxianProviderConfig struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_lingxian_provider_sys_origin_profile,priority:1;index:idx_lingxian_provider_active,priority:1"`
|
||||
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_lingxian_provider_sys_origin_profile,priority:2"`
|
||||
Active bool `gorm:"column:active;index:idx_lingxian_provider_active,priority:2"`
|
||||
GameListURL string `gorm:"column:game_list_url;size:1024"`
|
||||
AppKey string `gorm:"column:app_key;size:255"`
|
||||
TestUID string `gorm:"column:test_uid;size:64"`
|
||||
TestToken string `gorm:"column:test_token;size:1024"`
|
||||
CreateTime time.Time `gorm:"column:create_time"`
|
||||
UpdateTime time.Time `gorm:"column:update_time;index:idx_lingxian_provider_active,priority:3"`
|
||||
}
|
||||
|
||||
// TableName 返回灵仙配置表名。
|
||||
func (LingxianProviderConfig) TableName() string { return "lingxian_provider_config" }
|
||||
|
||||
// LingxianGameCatalog 是灵仙平台同步到本地的游戏目录。
|
||||
type LingxianGameCatalog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"`
|
||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_lingxian_catalog_sys_profile_vendor_game,priority:1;index:idx_lingxian_catalog_profile_internal_game,priority:1"`
|
||||
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_lingxian_catalog_sys_profile_vendor_game,priority:2;index:idx_lingxian_catalog_profile_internal_game,priority:2"`
|
||||
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_lingxian_catalog_profile_internal_game,priority:3"`
|
||||
VendorGameID string `gorm:"column:vendor_game_id;size:64;uniqueIndex:uk_lingxian_catalog_sys_profile_vendor_game,priority:3"`
|
||||
Name string `gorm:"column:name;size:128"`
|
||||
Cover string `gorm:"column:cover;size:1024"`
|
||||
PreviewURL string `gorm:"column:preview_url;size:1024"`
|
||||
DownloadURL string `gorm:"column:download_url;size:1024"`
|
||||
PackageVersion string `gorm:"column:package_version;size:32"`
|
||||
RawJSON string `gorm:"column:raw_json;type:longtext"`
|
||||
Status string `gorm:"column:status;size:32;index:idx_lingxian_catalog_profile_internal_game,priority:4"`
|
||||
CreateTime time.Time `gorm:"column:create_time"`
|
||||
UpdateTime time.Time `gorm:"column:update_time"`
|
||||
}
|
||||
|
||||
// TableName 返回灵仙目录表名。
|
||||
func (LingxianGameCatalog) TableName() string { return "lingxian_game_catalog" }
|
||||
|
||||
// BaishunLaunchSession 保存一次房间内游戏启动会话。
|
||||
type BaishunLaunchSession struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 会话主键
|
||||
|
||||
@ -38,6 +38,19 @@ func registerInviteRoutes(engine *gin.Engine, javaClient authGateway, inviteServ
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
internalGroup.POST("/register-bind-code", func(c *gin.Context) {
|
||||
var req invite.RegisterBindCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, invite.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := inviteService.BindRegisteredCode(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
|
||||
appGroup := engine.Group("/app/h5/invite-campaign")
|
||||
appGroup.Use(authMiddleware(javaClient))
|
||||
|
||||
110
internal/router/lingxian_routes.go
Normal file
110
internal/router/lingxian_routes.go
Normal file
@ -0,0 +1,110 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chatapp3-golang/internal/common"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerLingxianRoutes(engine *gin.Engine, javaClient authGateway, service *lingxian.Service) {
|
||||
if service == nil {
|
||||
return
|
||||
}
|
||||
consoleGroup := engine.Group("/operate/lingxian-game")
|
||||
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
||||
consoleGroup.GET("/config", func(c *gin.Context) {
|
||||
resp, err := service.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
consoleGroup.POST("/config", func(c *gin.Context) {
|
||||
var req lingxian.SaveProviderConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.SaveProviderConfig(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
consoleGroup.POST("/config/activate", func(c *gin.Context) {
|
||||
var req lingxian.ActivateProviderProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.ActivateProviderProfile(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
consoleGroup.GET("/catalog/page", func(c *gin.Context) {
|
||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||
resp, err := service.PageCatalog(
|
||||
c.Request.Context(),
|
||||
c.Query("sysOrigin"),
|
||||
c.Query("profile"),
|
||||
c.Query("keyword"),
|
||||
cursor,
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
consoleGroup.POST("/catalog/fetch", func(c *gin.Context) {
|
||||
var req lingxian.SyncCatalogRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.FetchAdminCatalog(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
consoleGroup.POST("/import-catalog", func(c *gin.Context) {
|
||||
var req lingxian.SyncCatalogRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
consoleGroup.POST("/sync", func(c *gin.Context) {
|
||||
var req lingxian.SyncCatalogRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.SyncAdminGames(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
}
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"chatapp3-golang/internal/service/gameopen"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
"chatapp3-golang/internal/service/signinreward"
|
||||
@ -22,6 +23,7 @@ import (
|
||||
type Services struct {
|
||||
Invite *invite.InviteService
|
||||
Baishun *baishun.BaishunService
|
||||
Lingxian *lingxian.Service
|
||||
GameOpen *gameopen.GameOpenService
|
||||
GameProviders *gameprovider.Registry
|
||||
LuckyGift *luckygift.LuckyGiftService
|
||||
@ -48,6 +50,7 @@ func NewRouter(
|
||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||
|
||||
return engine
|
||||
|
||||
@ -15,6 +15,25 @@ import (
|
||||
|
||||
// BindCode 处理邀请码绑定,并写入邀请关系、风控状态和双方即时奖励。
|
||||
func (s *InviteService) BindCode(ctx context.Context, user AuthUser, clientIP string, req BindCodeRequest) (*BindCodeResponse, error) {
|
||||
return s.bindCode(ctx, user, clientIP, req, "")
|
||||
}
|
||||
|
||||
// BindRegisteredCode 处理 Java 注册链路传入的邀请码,复用 H5 绑码口径。
|
||||
func (s *InviteService) BindRegisteredCode(ctx context.Context, req RegisterBindCodeRequest) (*BindCodeResponse, error) {
|
||||
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
||||
if sysOrigin == "" {
|
||||
return nil, NewAppError(400, "invalid_sys_origin", "sysOrigin is required")
|
||||
}
|
||||
if req.InviteeUserID <= 0 {
|
||||
return nil, NewAppError(400, "invalid_invitee_user_id", "inviteeUserId is required")
|
||||
}
|
||||
return s.bindCode(ctx, AuthUser{
|
||||
UserID: req.InviteeUserID,
|
||||
SysOrigin: sysOrigin,
|
||||
}, req.ClientIP, BindCodeRequest{InviteCode: req.InviteCode}, req.DeviceFingerprint)
|
||||
}
|
||||
|
||||
func (s *InviteService) bindCode(ctx context.Context, user AuthUser, clientIP string, req BindCodeRequest, deviceFingerprint string) (*BindCodeResponse, error) {
|
||||
inviteCode := strings.TrimSpace(req.InviteCode)
|
||||
if inviteCode == "" {
|
||||
return nil, NewAppError(400, "invalid_invite_code", "inviteCode is required")
|
||||
@ -46,9 +65,11 @@ func (s *InviteService) BindCode(ctx context.Context, user AuthUser, clientIP st
|
||||
return nil, NewAppError(400, "sys_origin_mismatch", "invite code does not belong to the current system")
|
||||
}
|
||||
|
||||
fingerprint := ""
|
||||
if value, fpErr := s.java.GetDeviceFingerprint(ctx, user.UserID); fpErr == nil {
|
||||
fingerprint = strings.TrimSpace(value)
|
||||
fingerprint := strings.TrimSpace(deviceFingerprint)
|
||||
if fingerprint == "" {
|
||||
if value, fpErr := s.java.GetDeviceFingerprint(ctx, user.UserID); fpErr == nil {
|
||||
fingerprint = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// 绑码链路同时锁住用户以及可选的 IP/设备维度,避免并发穿透风控。
|
||||
lockKeys := []string{
|
||||
|
||||
@ -205,6 +205,67 @@ func TestClaimTaskUsesInviteCountForInviteCountTasks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindRegisteredCodeUsesRequestDeviceFingerprint(t *testing.T) {
|
||||
service, db, cleanup := newTestInviteService(t, nil)
|
||||
defer cleanup()
|
||||
|
||||
now := time.Now()
|
||||
if err := db.Create(&model.InviteCampaignConfig{
|
||||
ID: 101,
|
||||
SysOrigin: "LIKEI",
|
||||
Enabled: true,
|
||||
Timezone: "Asia/Shanghai",
|
||||
AntiCheatSameIPOnce: false,
|
||||
AntiCheatSameDeviceOnce: true,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed config: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.InviteCodeMapping{UserID: 9001, InviteCode: "INV9001", Account: "u9001"}).Error; err != nil {
|
||||
t.Fatalf("seed invite code: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.UserBaseInfo{ID: 9001, Account: "u9001", UserNickname: "Inviter", OriginSys: "LIKEI", CreateTime: now}).Error; err != nil {
|
||||
t.Fatalf("seed inviter profile: %v", err)
|
||||
}
|
||||
|
||||
resp, err := service.BindRegisteredCode(context.Background(), RegisterBindCodeRequest{
|
||||
InviteCode: "INV9001",
|
||||
SysOrigin: "LIKEI",
|
||||
InviteeUserID: 9101,
|
||||
ClientIP: "10.0.0.1",
|
||||
DeviceFingerprint: "device-a",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BindRegisteredCode() error = %v", err)
|
||||
}
|
||||
if !resp.Eligible || resp.InviterUserID != 9001 {
|
||||
t.Fatalf("BindRegisteredCode() response = %+v, want eligible inviter 9001", resp)
|
||||
}
|
||||
|
||||
var relation model.InviteCampaignRelation
|
||||
if err := db.Where("invitee_user_id = ?", 9101).First(&relation).Error; err != nil {
|
||||
t.Fatalf("load relation: %v", err)
|
||||
}
|
||||
if relation.DeviceFingerprint != "device-a" {
|
||||
t.Fatalf("relation device fingerprint = %q, want device-a", relation.DeviceFingerprint)
|
||||
}
|
||||
|
||||
duplicateResp, err := service.BindRegisteredCode(context.Background(), RegisterBindCodeRequest{
|
||||
InviteCode: "INV9001",
|
||||
SysOrigin: "LIKEI",
|
||||
InviteeUserID: 9102,
|
||||
ClientIP: "10.0.0.2",
|
||||
DeviceFingerprint: "device-a",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BindRegisteredCode() duplicate device error = %v", err)
|
||||
}
|
||||
if duplicateResp.Eligible || duplicateResp.BlockReason != blockReasonDuplicateDevice {
|
||||
t.Fatalf("duplicate response = %+v, want blocked duplicate device", duplicateResp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRankUsesLifetimeInviteAndRewardStats(t *testing.T) {
|
||||
service, db, cleanup := newTestInviteService(t, nil)
|
||||
defer cleanup()
|
||||
|
||||
@ -136,6 +136,15 @@ type BindCodeRequest struct {
|
||||
InviteCode string `json:"inviteCode"`
|
||||
}
|
||||
|
||||
// RegisterBindCodeRequest 是 Java 注册成功后同步邀请活动关系的内部入参。
|
||||
type RegisterBindCodeRequest struct {
|
||||
InviteCode string `json:"inviteCode"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
InviteeUserID int64 `json:"inviteeUserId"`
|
||||
ClientIP string `json:"clientIp"`
|
||||
DeviceFingerprint string `json:"deviceFingerprint"`
|
||||
}
|
||||
|
||||
// BindCodeResponse 是绑定邀请码后的返回结果。
|
||||
type BindCodeResponse struct {
|
||||
RelationID int64 `json:"relationId"`
|
||||
|
||||
255
internal/service/lingxian/app_provider.go
Normal file
255
internal/service/lingxian/app_provider.go
Normal file
@ -0,0 +1,255 @@
|
||||
package lingxian
|
||||
|
||||
import (
|
||||
"chatapp3-golang/internal/common"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (p *AppProvider) Key() string {
|
||||
return vendorType
|
||||
}
|
||||
|
||||
func (p *AppProvider) DisplayName() string {
|
||||
return "灵仙"
|
||||
}
|
||||
|
||||
func (p *AppProvider) SupportsLaunch(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) {
|
||||
_, err := p.service.findGameRow(ctx, user.SysOrigin, req)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
var appErr *common.AppError
|
||||
if errors.As(err, &appErr) && appErr.Code == "game_not_found" {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (p *AppProvider) ListShortcutGames(ctx context.Context, user gameprovider.AuthUser, roomID string) ([]gameprovider.RoomGameListItem, error) {
|
||||
resp, err := p.ListRoomGames(ctx, user, roomID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Items) > 5 {
|
||||
resp.Items = resp.Items[:5]
|
||||
}
|
||||
return resp.Items, nil
|
||||
}
|
||||
|
||||
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
||||
items, err := p.service.listRoomGames(ctx, user.SysOrigin, category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gameprovider.RoomGameListResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (p *AppProvider) GetRoomState(ctx context.Context, user gameprovider.AuthUser, roomID string) (*gameprovider.RoomStateResponse, error) {
|
||||
return &gameprovider.RoomStateResponse{RoomID: roomID, State: "IDLE", Provider: vendorType}, nil
|
||||
}
|
||||
|
||||
func (p *AppProvider) LaunchGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest, clientIP string) (*gameprovider.LaunchResponse, error) {
|
||||
row, err := p.service.findGameRow(ctx, user.SysOrigin, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtimeCfg, err := p.service.resolveRuntimeConfigForProfile(ctx, normalizeSysOrigin(user.SysOrigin), row.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entryURL := buildLaunchURL(row.entryURL(), runtimeCfg, user, req, row.VendorGameID)
|
||||
sessionID := fmt.Sprintf("lx_%d_%s", user.UserID, row.VendorGameID)
|
||||
return &gameprovider.LaunchResponse{
|
||||
ID: row.ConfigID,
|
||||
GameSessionID: sessionID,
|
||||
Provider: vendorType,
|
||||
GameID: row.InternalGameID,
|
||||
ProviderGameID: row.VendorGameID,
|
||||
Entry: gameprovider.LaunchEntry{
|
||||
LaunchMode: defaultIfBlank(row.LaunchMode, launchModeH5),
|
||||
EntryURL: entryURL,
|
||||
PreviewURL: row.previewURL(),
|
||||
DownloadURL: row.entryURL(),
|
||||
PackageVersion: row.packageVersion(),
|
||||
},
|
||||
LaunchConfig: map[string]any{
|
||||
"uid": launchUID(runtimeCfg, user),
|
||||
"token": launchToken(runtimeCfg, user),
|
||||
"gameId": row.VendorGameID,
|
||||
"roomId": req.RoomID,
|
||||
"sign": launchSign(runtimeCfg, row.VendorGameID, launchUID(runtimeCfg, user), launchToken(runtimeCfg, user), req.RoomID),
|
||||
},
|
||||
RoomState: gameprovider.RoomStateResponse{
|
||||
RoomID: req.RoomID,
|
||||
State: "PLAYING",
|
||||
Provider: vendorType,
|
||||
GameSessionID: sessionID,
|
||||
CurrentGameID: row.InternalGameID,
|
||||
CurrentProviderGameID: row.VendorGameID,
|
||||
CurrentGameName: row.name(),
|
||||
CurrentGameCover: row.cover(),
|
||||
HostUserID: user.UserID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AppProvider) CloseGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
||||
return &gameprovider.RoomStateResponse{RoomID: req.RoomID, State: "IDLE", Provider: vendorType}, nil
|
||||
}
|
||||
|
||||
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, category string) ([]gameprovider.RoomGameListItem, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []gameRow
|
||||
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, vendorType)
|
||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), roomCategory) {
|
||||
query = query.Where("cfg.category = ?", category)
|
||||
}
|
||||
if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]gameprovider.RoomGameListItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toListItem())
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) findGameRow(ctx context.Context, sysOrigin string, req gameprovider.LaunchRequest) (gameRow, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
return gameRow{}, err
|
||||
}
|
||||
var row gameRow
|
||||
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
||||
Where("cfg.sys_origin = ? AND ext.vendor_type = ?", sysOrigin, vendorType)
|
||||
if req.ID > 0 {
|
||||
query = query.Where("cfg.id = ?", req.ID)
|
||||
} else {
|
||||
query = query.Where("cfg.game_id = ? OR ext.vendor_game_id = ?", strings.TrimSpace(req.GameID), strings.TrimSpace(req.GameID))
|
||||
}
|
||||
if err := query.Limit(1).Scan(&row).Error; err != nil {
|
||||
return gameRow{}, err
|
||||
}
|
||||
if row.ConfigID > 0 {
|
||||
return row, nil
|
||||
}
|
||||
return gameRow{}, common.NewAppError(http.StatusNotFound, "game_not_found", "lingxian game config not found")
|
||||
}
|
||||
|
||||
func (s *Service) baseGameQuery(ctx context.Context, sysOrigin, profile string) *gorm.DB {
|
||||
return s.db.WithContext(ctx).
|
||||
Table("sys_game_list_config AS cfg").
|
||||
Select(`
|
||||
cfg.id AS config_id,
|
||||
cfg.sys_origin AS sys_origin,
|
||||
ext.profile AS profile,
|
||||
cfg.game_id AS internal_game_id,
|
||||
cfg.name AS name,
|
||||
cfg.category AS category,
|
||||
cfg.cover AS cover,
|
||||
cfg.sort AS sort,
|
||||
cfg.full_screen AS full_screen,
|
||||
ext.vendor_game_id AS vendor_game_id,
|
||||
ext.launch_mode AS launch_mode,
|
||||
ext.package_version AS ext_package_version,
|
||||
ext.preview_url AS ext_preview_url,
|
||||
ext.package_url AS package_url,
|
||||
ext.extra_json AS ext_extra_json,
|
||||
cat.name AS catalog_name,
|
||||
cat.cover AS catalog_cover,
|
||||
cat.preview_url AS catalog_preview_url,
|
||||
cat.download_url AS catalog_download_url,
|
||||
cat.package_version AS catalog_package_version,
|
||||
cat.status AS catalog_status
|
||||
`).
|
||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
|
||||
Joins("LEFT JOIN lingxian_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND cat.vendor_game_id = ext.vendor_game_id")
|
||||
}
|
||||
|
||||
func (r gameRow) toListItem() gameprovider.RoomGameListItem {
|
||||
return gameprovider.RoomGameListItem{
|
||||
ID: r.ConfigID,
|
||||
GameID: r.InternalGameID,
|
||||
GameType: vendorType,
|
||||
Provider: vendorType,
|
||||
ProviderGameID: r.VendorGameID,
|
||||
Name: r.name(),
|
||||
Cover: r.cover(),
|
||||
Category: r.Category,
|
||||
Sort: r.Sort,
|
||||
LaunchMode: defaultIfBlank(r.LaunchMode, launchModeH5),
|
||||
FullScreen: r.FullScreen,
|
||||
GameMode: 3,
|
||||
PackageVersion: r.packageVersion(),
|
||||
Status: defaultIfBlank(r.CatalogStatus, catalogEnabled),
|
||||
LaunchParams: map[string]any{
|
||||
"gameType": vendorType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r gameRow) name() string {
|
||||
return defaultIfBlank(r.Name, r.CatalogName)
|
||||
}
|
||||
|
||||
func (r gameRow) cover() string {
|
||||
return defaultIfBlank(r.Cover, defaultIfBlank(r.CatalogCover, r.previewURL()))
|
||||
}
|
||||
|
||||
func (r gameRow) previewURL() string {
|
||||
return defaultIfBlank(r.ExtPreviewURL, r.CatalogPreviewURL)
|
||||
}
|
||||
|
||||
func (r gameRow) entryURL() string {
|
||||
return defaultIfBlank(r.PackageURL, defaultIfBlank(r.CatalogDownloadURL, r.ExtPreviewURL))
|
||||
}
|
||||
|
||||
func (r gameRow) packageVersion() string {
|
||||
return defaultIfBlank(r.ExtPackageVersion, r.CatalogPackageVersion)
|
||||
}
|
||||
|
||||
func buildLaunchURL(entry string, cfg runtimeConfig, user gameprovider.AuthUser, req gameprovider.LaunchRequest, gameID string) string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(entry))
|
||||
if err != nil {
|
||||
return strings.TrimSpace(entry)
|
||||
}
|
||||
uid := launchUID(cfg, user)
|
||||
token := launchToken(cfg, user)
|
||||
query := parsed.Query()
|
||||
query.Set("uid", uid)
|
||||
query.Set("token", token)
|
||||
query.Set("gameId", gameID)
|
||||
query.Set("roomId", req.RoomID)
|
||||
query.Set("sign", launchSign(cfg, gameID, uid, token, req.RoomID))
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func launchUID(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
||||
return defaultIfBlank(cfg.TestUID, fmt.Sprintf("%d", user.UserID))
|
||||
}
|
||||
|
||||
func launchToken(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
||||
return defaultIfBlank(cfg.TestToken, user.Token)
|
||||
}
|
||||
|
||||
func launchSign(cfg runtimeConfig, gameID, uid, token, roomID string) string {
|
||||
sum := md5.Sum([]byte(gameID + uid + token + roomID + strings.TrimSpace(cfg.AppKey)))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
391
internal/service/lingxian/catalog.go
Normal file
391
internal/service/lingxian/catalog.go
Normal file
@ -0,0 +1,391 @@
|
||||
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) (*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))
|
||||
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)
|
||||
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{}) (*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()
|
||||
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 {
|
||||
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: true,
|
||||
Sort: parseSort(vendorGameID),
|
||||
FullScreen: true,
|
||||
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 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 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)
|
||||
}
|
||||
71
internal/service/lingxian/catalog_test.go
Normal file
71
internal/service/lingxian/catalog_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package lingxian
|
||||
|
||||
import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/model"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestSyncAdminGamesFetchesCatalogAndImportsUnifiedList(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)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[{"gameId":"1","name":"Fruit Party","title":"水果奇缘","ver":11,"full_url":"https://example.test/full.html","half_url":"https://example.test/half.html"}]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
now := time.Now()
|
||||
if err := db.Create(&model.LingxianProviderConfig{
|
||||
ID: 1,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
Active: true,
|
||||
GameListURL: server.URL,
|
||||
AppKey: "test-key",
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed provider: %v", err)
|
||||
}
|
||||
|
||||
service := NewService(config.Config{HTTP: config.HTTPConfig{Timeout: time.Second}}, db, nil)
|
||||
resp, err := service.SyncAdminGames(ctx, SyncCatalogRequest{SysOrigin: "LIKEI", Profile: profileProd})
|
||||
if err != nil {
|
||||
t.Fatalf("SyncAdminGames: %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)
|
||||
}
|
||||
var ext model.SysGameListVendorExt
|
||||
if err := db.Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", cfg.ID, vendorType, profileProd).First(&ext).Error; err != nil {
|
||||
t.Fatalf("vendor ext missing: %v", err)
|
||||
}
|
||||
if ext.VendorGameID != "1" || ext.PackageURL != "https://example.test/full.html" {
|
||||
t.Fatalf("vendor ext = %#v", ext)
|
||||
}
|
||||
}
|
||||
203
internal/service/lingxian/config.go
Normal file
203
internal/service/lingxian/config.go
Normal file
@ -0,0 +1,203 @@
|
||||
package lingxian
|
||||
|
||||
import (
|
||||
"chatapp3-golang/internal/common"
|
||||
"chatapp3-golang/internal/model"
|
||||
"chatapp3-golang/internal/utils"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (s *Service) defaultRuntimeConfig(sysOrigin string) runtimeConfig {
|
||||
return runtimeConfig{
|
||||
SysOrigin: normalizeSysOrigin(sysOrigin),
|
||||
Profile: profileProd,
|
||||
GameListURL: defaultIfBlank(s.cfg.Lingxian.GameListURL, defaultListURL),
|
||||
AppKey: strings.TrimSpace(s.cfg.Lingxian.AppKey),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) activeProfile(ctx context.Context, sysOrigin string) (string, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
var row model.LingxianProviderConfig
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
||||
Order("update_time DESC").
|
||||
Limit(1).
|
||||
First(&row).Error
|
||||
if err == nil {
|
||||
return normalizeProfile(row.Profile), nil
|
||||
}
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return profileProd, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (s *Service) requestProfile(ctx context.Context, sysOrigin, profile string) (string, error) {
|
||||
if strings.TrimSpace(profile) != "" {
|
||||
return normalizeProfile(profile), nil
|
||||
}
|
||||
return s.activeProfile(ctx, sysOrigin)
|
||||
}
|
||||
|
||||
func (s *Service) resolveRuntimeConfigForProfile(ctx context.Context, sysOrigin, profile string) (runtimeConfig, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
profile = normalizeProfile(profile)
|
||||
base := s.defaultRuntimeConfig(sysOrigin)
|
||||
base.Profile = profile
|
||||
|
||||
var row model.LingxianProviderConfig
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||
Limit(1).
|
||||
First(&row).Error
|
||||
if err == nil {
|
||||
return configToRuntime(base, row), nil
|
||||
}
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return base, nil
|
||||
}
|
||||
return runtimeConfig{}, err
|
||||
}
|
||||
|
||||
func (s *Service) requireRuntimeConfigForProfile(ctx context.Context, sysOrigin, profile string) (runtimeConfig, error) {
|
||||
cfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return runtimeConfig{}, err
|
||||
}
|
||||
if strings.TrimSpace(cfg.GameListURL) == "" || strings.TrimSpace(cfg.AppKey) == "" {
|
||||
return runtimeConfig{}, common.NewAppError(http.StatusBadRequest, "lingxian_config_missing", "lingxian config is missing")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetProviderConfig(ctx context.Context, sysOrigin, profile string) (*ProviderConfigItem, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
activeProfile, err := s.activeProfile(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile, err = s.requestProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var row model.LingxianProviderConfig
|
||||
_ = s.db.WithContext(ctx).
|
||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||
Limit(1).
|
||||
First(&row).Error
|
||||
|
||||
return &ProviderConfigItem{
|
||||
ID: row.ID,
|
||||
SysOrigin: runtimeCfg.SysOrigin,
|
||||
Profile: runtimeCfg.Profile,
|
||||
ActiveProfile: activeProfile,
|
||||
Active: runtimeCfg.Profile == activeProfile,
|
||||
GameListURL: defaultIfBlank(runtimeCfg.GameListURL, defaultListURL),
|
||||
AppKey: runtimeCfg.AppKey,
|
||||
TestUID: runtimeCfg.TestUID,
|
||||
TestToken: runtimeCfg.TestToken,
|
||||
UpdateTime: formatTime(row.UpdateTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) SaveProviderConfig(ctx context.Context, req SaveProviderConfigRequest) (*ProviderConfigItem, error) {
|
||||
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
||||
profile := normalizeProfile(req.Profile)
|
||||
gameListURL := defaultIfBlank(req.GameListURL, defaultListURL)
|
||||
appKey := strings.TrimSpace(req.AppKey)
|
||||
if gameListURL == "" {
|
||||
return nil, common.NewAppError(http.StatusBadRequest, "game_list_url_required", "gameListUrl is required")
|
||||
}
|
||||
if appKey == "" {
|
||||
return nil, common.NewAppError(http.StatusBadRequest, "app_key_required", "appKey is required")
|
||||
}
|
||||
|
||||
id, err := utils.NextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
row := model.LingxianProviderConfig{
|
||||
ID: id,
|
||||
SysOrigin: sysOrigin,
|
||||
Profile: profile,
|
||||
GameListURL: gameListURL,
|
||||
AppKey: appKey,
|
||||
TestUID: strings.TrimSpace(req.TestUID),
|
||||
TestToken: strings.TrimSpace(req.TestToken),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
var activeCount int64
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.LingxianProviderConfig{}).
|
||||
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
||||
Count(&activeCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.Active = activeCount == 0
|
||||
|
||||
if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"game_list_url", "app_key", "test_uid", "test_token", "update_time",
|
||||
}),
|
||||
}).Create(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.Active {
|
||||
s.clearJavaGameListCache(ctx)
|
||||
}
|
||||
return s.GetProviderConfig(ctx, sysOrigin, profile)
|
||||
}
|
||||
|
||||
func (s *Service) ActivateProviderProfile(ctx context.Context, req ActivateProviderProfileRequest) (*ProviderConfigItem, error) {
|
||||
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
||||
profile := normalizeProfile(req.Profile)
|
||||
var row model.LingxianProviderConfig
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||
Limit(1).
|
||||
First(&row).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, common.NewAppError(http.StatusBadRequest, "profile_config_missing", "profile config is missing")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.LingxianProviderConfig{}).
|
||||
Where("sys_origin = ?", sysOrigin).
|
||||
Updates(map[string]any{"active": false, "update_time": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.LingxianProviderConfig{}).
|
||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||
Updates(map[string]any{"active": true, "update_time": now}).Error
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.clearJavaGameListCache(ctx)
|
||||
return s.GetProviderConfig(ctx, sysOrigin, profile)
|
||||
}
|
||||
|
||||
func (s *Service) clearJavaGameListCache(ctx context.Context) {
|
||||
if s.cache == nil {
|
||||
return
|
||||
}
|
||||
_ = s.cache.Del(ctx, "GAME_LIST").Err()
|
||||
}
|
||||
227
internal/service/lingxian/types.go
Normal file
227
internal/service/lingxian/types.go
Normal file
@ -0,0 +1,227 @@
|
||||
package lingxian
|
||||
|
||||
import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/model"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
vendorType = "LINGXIAN"
|
||||
profileProd = "PROD"
|
||||
profileTest = "TEST"
|
||||
launchModeH5 = "H5_REMOTE"
|
||||
catalogEnabled = "ENABLED"
|
||||
roomCategory = "CHAT_ROOM"
|
||||
defaultListURL = "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"
|
||||
)
|
||||
|
||||
type dbHandle interface {
|
||||
WithContext(ctx context.Context) *gorm.DB
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfg config.Config
|
||||
db dbHandle
|
||||
cache redis.Cmdable
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewService(cfg config.Config, db dbHandle, cache redis.Cmdable) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
cache: cache,
|
||||
httpClient: &http.Client{
|
||||
Timeout: cfg.HTTP.Timeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type runtimeConfig struct {
|
||||
SysOrigin string
|
||||
Profile string
|
||||
GameListURL string
|
||||
AppKey string
|
||||
TestUID string
|
||||
TestToken string
|
||||
}
|
||||
|
||||
type ProviderConfigItem struct {
|
||||
ID int64 `json:"id"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
ActiveProfile string `json:"activeProfile"`
|
||||
Active bool `json:"active"`
|
||||
GameListURL string `json:"gameListUrl"`
|
||||
AppKey string `json:"appKey"`
|
||||
TestUID string `json:"testUid"`
|
||||
TestToken string `json:"testToken"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
type SaveProviderConfigRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
GameListURL string `json:"gameListUrl"`
|
||||
AppKey string `json:"appKey"`
|
||||
TestUID string `json:"testUid"`
|
||||
TestToken string `json:"testToken"`
|
||||
}
|
||||
|
||||
type ActivateProviderProfileRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
}
|
||||
|
||||
type SyncCatalogRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
VendorGameIDs []string `json:"vendorGameIds"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
type SyncCatalogResponse struct {
|
||||
Inserted int `json:"inserted"`
|
||||
Updated int `json:"updated"`
|
||||
Skipped int `json:"skipped"`
|
||||
}
|
||||
|
||||
type ImportCatalogResponse struct {
|
||||
Existing int `json:"existing"`
|
||||
Imported int `json:"imported"`
|
||||
}
|
||||
|
||||
type SyncAdminCatalogResponse struct {
|
||||
Inserted int `json:"inserted"`
|
||||
Updated int `json:"updated"`
|
||||
Skipped int `json:"skipped"`
|
||||
Existing int `json:"existing"`
|
||||
Imported int `json:"imported"`
|
||||
}
|
||||
|
||||
type CatalogItem struct {
|
||||
VendorGameID string `json:"vendorGameId"`
|
||||
Profile string `json:"profile"`
|
||||
GameID string `json:"gameId"`
|
||||
Name string `json:"name"`
|
||||
Cover string `json:"cover"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
PackageVersion string `json:"packageVersion"`
|
||||
Status string `json:"status"`
|
||||
Added bool `json:"added"`
|
||||
Showcase bool `json:"showcase"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
type CatalogPageResponse struct {
|
||||
Cursor int `json:"cursor"`
|
||||
Limit int `json:"limit"`
|
||||
Records []CatalogItem `json:"records"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type platformGame struct {
|
||||
GameID string `json:"gameId"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Version int `json:"ver"`
|
||||
FullURL string `json:"full_url"`
|
||||
HalfURL string `json:"half_url"`
|
||||
}
|
||||
|
||||
type gameRow struct {
|
||||
ConfigID int64
|
||||
SysOrigin string
|
||||
Profile string
|
||||
InternalGameID string
|
||||
Name string
|
||||
Category string
|
||||
Cover string
|
||||
Sort int64
|
||||
FullScreen bool
|
||||
VendorGameID string
|
||||
LaunchMode string
|
||||
ExtPackageVersion string
|
||||
ExtPreviewURL string
|
||||
PackageURL string
|
||||
ExtExtraJSON string
|
||||
CatalogName string
|
||||
CatalogCover string
|
||||
CatalogPreviewURL string
|
||||
CatalogDownloadURL string
|
||||
CatalogPackageVersion string
|
||||
CatalogStatus string
|
||||
}
|
||||
|
||||
var _ gameprovider.Provider = (*AppProvider)(nil)
|
||||
|
||||
type AppProvider struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewAppProvider(service *Service) *AppProvider {
|
||||
if service == nil {
|
||||
return nil
|
||||
}
|
||||
return &AppProvider{service: service}
|
||||
}
|
||||
|
||||
func normalizeSysOrigin(sysOrigin string) string {
|
||||
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||||
if sysOrigin == "" {
|
||||
return "LIKEI"
|
||||
}
|
||||
return sysOrigin
|
||||
}
|
||||
|
||||
func normalizeProfile(profile string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(profile)) {
|
||||
case "", "PROD", "PRODUCT", "PRODUCTION", "ONLINE", "OFFICIAL", "FORMAL":
|
||||
return profileProd
|
||||
case "TEST", "TESTING", "SANDBOX", "DEV":
|
||||
return profileTest
|
||||
default:
|
||||
return strings.ToUpper(strings.TrimSpace(profile))
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func defaultIfBlank(value, fallback string) string {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
func catalogName(game platformGame) string {
|
||||
return defaultIfBlank(game.Title, game.Name)
|
||||
}
|
||||
|
||||
func internalGameID(vendorGameID string) string {
|
||||
return strings.TrimSpace(vendorGameID)
|
||||
}
|
||||
|
||||
func configToRuntime(base runtimeConfig, row model.LingxianProviderConfig) runtimeConfig {
|
||||
base.SysOrigin = normalizeSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
|
||||
base.Profile = normalizeProfile(defaultIfBlank(row.Profile, base.Profile))
|
||||
base.GameListURL = defaultIfBlank(row.GameListURL, base.GameListURL)
|
||||
base.AppKey = defaultIfBlank(row.AppKey, base.AppKey)
|
||||
base.TestUID = strings.TrimSpace(row.TestUID)
|
||||
base.TestToken = strings.TrimSpace(row.TestToken)
|
||||
return base
|
||||
}
|
||||
33
migrations/014_lingxian_provider_config.sql
Normal file
33
migrations/014_lingxian_provider_config.sql
Normal file
@ -0,0 +1,33 @@
|
||||
CREATE TABLE IF NOT EXISTS lingxian_provider_config (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
sys_origin VARCHAR(32) NOT NULL,
|
||||
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
game_list_url VARCHAR(1024) NOT NULL DEFAULT '',
|
||||
app_key VARCHAR(255) NOT NULL DEFAULT '',
|
||||
test_uid VARCHAR(64) DEFAULT NULL,
|
||||
test_token VARCHAR(1024) DEFAULT NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_lingxian_provider_sys_origin_profile (sys_origin, profile),
|
||||
KEY idx_lingxian_provider_active (sys_origin, active, update_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lingxian_game_catalog (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
sys_origin VARCHAR(32) NOT NULL,
|
||||
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
|
||||
internal_game_id VARCHAR(64) NOT NULL,
|
||||
vendor_game_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
cover VARCHAR(1024) DEFAULT NULL,
|
||||
preview_url VARCHAR(1024) DEFAULT NULL,
|
||||
download_url VARCHAR(1024) DEFAULT NULL,
|
||||
package_version VARCHAR(32) DEFAULT NULL,
|
||||
raw_json LONGTEXT DEFAULT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'ENABLED',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_lingxian_catalog_sys_profile_vendor_game (sys_origin, profile, vendor_game_id),
|
||||
KEY idx_lingxian_catalog_profile_internal_game (sys_origin, profile, internal_game_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
Loading…
x
Reference in New Issue
Block a user