From b53313f62ddcdf91cb062332c7b41e70258346a8 Mon Sep 17 00:00:00 2001 From: hy001 Date: Thu, 28 May 2026 15:07:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A6=96=E5=86=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/first_recharge_reward_models.go | 13 ++- .../router/first_recharge_reward_routes.go | 33 ++++++- .../service/firstrechargereward/config.go | 27 ++++-- internal/service/firstrechargereward/event.go | 52 ++++++---- internal/service/firstrechargereward/home.go | 7 +- .../service/firstrechargereward/mapper.go | 33 ++++--- .../service/firstrechargereward/records.go | 1 + .../firstrechargereward/service_test.go | 91 +++++++++++++++++- internal/service/firstrechargereward/types.go | 95 +++++++++++-------- .../service/firstrechargereward/version.go | 10 +- migrations/049_first_recharge_reward.sql | 5 + ...recharge_reward_version_google_product.sql | 71 ++++++++++++++ 12 files changed, 346 insertions(+), 92 deletions(-) create mode 100644 migrations/050_first_recharge_reward_version_google_product.sql diff --git a/internal/model/first_recharge_reward_models.go b/internal/model/first_recharge_reward_models.go index cc62945..e5c8e55 100644 --- a/internal/model/first_recharge_reward_models.go +++ b/internal/model/first_recharge_reward_models.go @@ -4,11 +4,12 @@ import "time" // FirstRechargeRewardConfig stores first recharge reward config per system. type FirstRechargeRewardConfig struct { - ID int64 `gorm:"column:id;primaryKey"` - SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_first_recharge_reward_sys_origin"` - Enabled bool `gorm:"column:enabled;index:idx_first_recharge_reward_enabled"` - CreateTime time.Time `gorm:"column:create_time"` - UpdateTime time.Time `gorm:"column:update_time"` + ID int64 `gorm:"column:id;primaryKey"` + SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_first_recharge_reward_sys_origin"` + Enabled bool `gorm:"column:enabled;index:idx_first_recharge_reward_enabled"` + MinAppVersion string `gorm:"column:min_app_version;size:32"` + CreateTime time.Time `gorm:"column:create_time"` + UpdateTime time.Time `gorm:"column:update_time"` } func (FirstRechargeRewardConfig) TableName() string { return "first_recharge_reward_config" } @@ -19,6 +20,7 @@ type FirstRechargeRewardLevel struct { ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_first_recharge_reward_level,priority:1;uniqueIndex:uk_first_recharge_reward_amount,priority:1;index:idx_first_recharge_reward_level_config"` Level int `gorm:"column:level;uniqueIndex:uk_first_recharge_reward_level,priority:2"` RechargeAmountCents int64 `gorm:"column:recharge_amount_cents;uniqueIndex:uk_first_recharge_reward_amount,priority:2"` + GoogleProductID string `gorm:"column:google_product_id;size:128;index:idx_first_recharge_reward_level_google_product"` RewardGroupID int64 `gorm:"column:reward_group_id"` RewardGroupName string `gorm:"column:reward_group_name;size:255"` Enabled bool `gorm:"column:enabled"` @@ -46,6 +48,7 @@ type FirstRechargeRewardGrantRecord struct { Level int `gorm:"column:level"` PayPlatform string `gorm:"column:pay_platform;size:64"` PaymentMethod string `gorm:"column:payment_method;size:32"` + GoogleProductID string `gorm:"column:google_product_id;size:128;index:idx_first_recharge_reward_grant_google_product"` SourceOrderID string `gorm:"column:source_order_id;size:128;index:idx_first_recharge_reward_order"` RewardGroupID int64 `gorm:"column:reward_group_id"` RewardGroupName string `gorm:"column:reward_group_name;size:255"` diff --git a/internal/router/first_recharge_reward_routes.go b/internal/router/first_recharge_reward_routes.go index 3dea949..9d12787 100644 --- a/internal/router/first_recharge_reward_routes.go +++ b/internal/router/first_recharge_reward_routes.go @@ -2,6 +2,7 @@ package router import ( "net/http" + "strings" "chatapp3-golang/internal/service/firstrechargereward" @@ -60,7 +61,7 @@ func registerFirstRechargeRewardRoutes(engine *gin.Engine, javaClient authGatewa func registerFirstRechargeRewardAppGroup(group *gin.RouterGroup, javaClient authGateway, service *firstrechargereward.Service) { group.Use(authMiddleware(javaClient)) group.GET("/home", func(c *gin.Context) { - resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c)) + resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c), firstRechargeRewardRequestVersion(c)) if err != nil { writeError(c, err) return @@ -68,3 +69,33 @@ func registerFirstRechargeRewardAppGroup(group *gin.RouterGroup, javaClient auth writeOK(c, resp) }) } + +func firstRechargeRewardRequestVersion(c *gin.Context) string { + return firstNonEmptyString( + c.Query("appVersion"), + c.Query("version"), + c.GetHeader("req-version"), + c.GetHeader("Req-Version"), + versionFromReqAppIntel(c.GetHeader("req-app-intel")), + versionFromReqAppIntel(c.GetHeader("Req-App-Intel")), + ) +} + +func versionFromReqAppIntel(value string) string { + for _, item := range strings.Split(value, ";") { + parts := strings.SplitN(item, "=", 2) + if len(parts) == 2 && strings.EqualFold(strings.TrimSpace(parts[0]), "version") { + return strings.TrimSpace(parts[1]) + } + } + return "" +} + +func firstNonEmptyString(values ...string) string { + for _, value := range values { + if text := strings.TrimSpace(value); text != "" { + return text + } + } + return "" +} diff --git a/internal/service/firstrechargereward/config.go b/internal/service/firstrechargereward/config.go index c526b39..f9982c1 100644 --- a/internal/service/firstrechargereward/config.go +++ b/internal/service/firstrechargereward/config.go @@ -37,12 +37,13 @@ func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigRespo } rewardItems := s.loadRewardItemsMap(ctx, levels) return &ConfigResponse{ - Configured: true, - ID: configRow.ID, - SysOrigin: configRow.SysOrigin, - Enabled: configRow.Enabled, - UpdateTime: formatDateTime(configRow.UpdateTime), - LevelConfigs: buildLevelPayloads(levels, rewardItems, 0, 0, true), + Configured: true, + ID: configRow.ID, + SysOrigin: configRow.SysOrigin, + Enabled: configRow.Enabled, + MinAppVersion: strings.TrimSpace(configRow.MinAppVersion), + UpdateTime: formatDateTime(configRow.UpdateTime), + LevelConfigs: buildLevelPayloads(levels, rewardItems, 0, 0, true), }, nil } @@ -56,6 +57,10 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*Confi if req.Enabled && !hasEnabledLevel(levels) { return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "enabled config must contain at least one enabled level") } + minAppVersion := strings.TrimSpace(req.MinAppVersion) + if req.Enabled && minAppVersion == "" { + return nil, NewAppError(http.StatusBadRequest, "invalid_min_app_version", "minAppVersion is required when enabled") + } var savedConfigID int64 if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { @@ -91,6 +96,7 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*Confi configRow.SysOrigin = sysOrigin configRow.Enabled = req.Enabled + configRow.MinAppVersion = minAppVersion configRow.UpdateTime = now if configRow.CreateTime.IsZero() { configRow.CreateTime = now @@ -126,6 +132,7 @@ func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechar } seenLevels := make(map[int]struct{}, len(inputs)) seenAmounts := make(map[int64]struct{}, len(inputs)) + seenGoogleProductIDs := make(map[string]struct{}, len(inputs)) rows := make([]model.FirstRechargeRewardLevel, 0, len(inputs)) for _, item := range inputs { if item.Level <= 0 { @@ -148,6 +155,13 @@ func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechar if _, exists := seenAmounts[amountCents]; exists { return nil, NewAppError(http.StatusBadRequest, "duplicate_recharge_amount", "levelConfigs must not contain duplicate rechargeAmount") } + googleProductID := normalizeGoogleProductID(item.GoogleProductID) + if googleProductID != "" { + if _, exists := seenGoogleProductIDs[googleProductID]; exists { + return nil, NewAppError(http.StatusBadRequest, "duplicate_google_product_id", "levelConfigs must not contain duplicate googleProductId") + } + seenGoogleProductIDs[googleProductID] = struct{}{} + } seenLevels[item.Level] = struct{}{} seenAmounts[amountCents] = struct{}{} id, err := utils.NextID() @@ -158,6 +172,7 @@ func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechar ID: id, Level: item.Level, RechargeAmountCents: amountCents, + GoogleProductID: googleProductID, RewardGroupID: rewardGroupID, RewardGroupName: strings.TrimSpace(item.RewardGroupName), Enabled: item.Enabled, diff --git a/internal/service/firstrechargereward/event.go b/internal/service/firstrechargereward/event.go index 3204986..685656b 100644 --- a/internal/service/firstrechargereward/event.go +++ b/internal/service/firstrechargereward/event.go @@ -41,9 +41,6 @@ func (s *Service) ProcessRechargeEvent(ctx context.Context, event RechargeEvent) } return nil, err } - if !isFirstRechargeRewardAppVersionSupported(normalized.AppVersion) { - return &ProcessRechargeResponse{Processed: false, Reason: "app_version_not_supported"}, nil - } if !isAcceptedPaymentMethod(normalized.PaymentMethod, normalized.PayPlatform) { return &ProcessRechargeResponse{Processed: false, Reason: "unsupported_payment_method"}, nil } @@ -61,9 +58,12 @@ func (s *Service) ProcessRechargeEvent(ctx context.Context, event RechargeEvent) if !bundle.Config.Enabled { return &ProcessRechargeResponse{Processed: false, Reason: "config_disabled"}, nil } - matched, ok := pickMatchedLevel(bundle.Levels, normalized.AmountCents) + if !isFirstRechargeRewardAppVersionSupported(normalized.AppVersion, bundle.Config.MinAppVersion) { + return &ProcessRechargeResponse{Processed: false, Reason: "app_version_not_supported"}, nil + } + matched, ok := pickMatchedLevel(bundle.Levels, normalized.AmountCents, normalized.GoogleProductID) if !ok { - return &ProcessRechargeResponse{Processed: false, Reason: "amount_not_reached"}, nil + return &ProcessRechargeResponse{Processed: false, Reason: "level_not_matched"}, nil } record, terminal, err := s.getOrCreateGrantRecord(ctx, normalized, bundle.Config, matched) @@ -112,13 +112,14 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg if amountCents <= 0 { amountCents = firstPositiveAmountCents(event.Amount.String(), event.AmountUSD.String(), event.USDAmount.String()) } - if amountCents <= 0 { - return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_amount", "amountCents or amount is required") - } payPlatform := strings.ToUpper(strings.TrimSpace(event.PayPlatform)) paymentMethod := strings.ToUpper(strings.TrimSpace(event.PaymentMethod)) appVersion := normalizeRechargeAppVersion(event) + googleProductID := normalizeRechargeGoogleProductID(event) + if amountCents <= 0 && googleProductID == "" { + return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_recharge_identity", "amountCents, amount, or googleProductId is required") + } sourceOrderID := firstNonEmpty(event.SourceOrderID.String(), event.OrderID.String()) eventID := strings.TrimSpace(event.EventID) if eventID == "" && sourceOrderID != "" { @@ -132,16 +133,17 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg occurredAt = time.Now() } return normalizedRechargeEvent{ - EventID: eventID, - SysOrigin: sysOrigin, - UserID: userID, - AmountCents: amountCents, - Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"), - PayPlatform: payPlatform, - PaymentMethod: paymentMethod, - AppVersion: appVersion, - SourceOrderID: sourceOrderID, - OccurredAt: occurredAt, + EventID: eventID, + SysOrigin: sysOrigin, + UserID: userID, + AmountCents: amountCents, + Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"), + PayPlatform: payPlatform, + PaymentMethod: paymentMethod, + AppVersion: appVersion, + GoogleProductID: googleProductID, + SourceOrderID: sourceOrderID, + OccurredAt: occurredAt, }, nil } @@ -187,6 +189,7 @@ func (s *Service) getOrCreateGrantRecord( Level: level.Level, PayPlatform: event.PayPlatform, PaymentMethod: event.PaymentMethod, + GoogleProductID: event.GoogleProductID, SourceOrderID: event.SourceOrderID, RewardGroupID: level.RewardGroupID, RewardGroupName: level.RewardGroupName, @@ -290,6 +293,7 @@ func (s *Service) pushRewardNotice(ctx context.Context, record *model.FirstRecha "rewardGroupName": record.RewardGroupName, "payPlatform": record.PayPlatform, "paymentMethod": record.PaymentMethod, + "googleProductId": record.GoogleProductID, "noticeType": firstRechargeRewardNoticeType, } return s.java.SendOfficialNoticeCustomize(ctx, integration.OfficialNoticeCustomizeRequest{ @@ -372,6 +376,18 @@ func normalizeRechargeAppVersion(event RechargeEvent) string { ) } +func normalizeRechargeGoogleProductID(event RechargeEvent) string { + return normalizeGoogleProductID(firstNonEmpty( + event.GoogleProductID, + event.ProductID.String(), + rechargePayloadString(event.Payload, "googleProductId", "productId", "product_id"), + )) +} + +func normalizeGoogleProductID(value string) string { + return strings.TrimSpace(value) +} + func rechargePayloadString(payload json.RawMessage, keys ...string) string { if len(payload) == 0 { return "" diff --git a/internal/service/firstrechargereward/home.go b/internal/service/firstrechargereward/home.go index a748044..0685fbc 100644 --- a/internal/service/firstrechargereward/home.go +++ b/internal/service/firstrechargereward/home.go @@ -20,7 +20,7 @@ const ( ) // GetHome returns app-facing first recharge reward config and the user's grant state. -func (s *Service) GetHome(ctx context.Context, user AuthUser) (*HomeResponse, error) { +func (s *Service) GetHome(ctx context.Context, user AuthUser, appVersion string) (*HomeResponse, error) { sysOrigin, err := requireUser(user) if err != nil { return nil, err @@ -52,12 +52,17 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*HomeResponse, er ActivityStatus: activityStatusOngoing, UserID: user.UserID, SysOrigin: bundle.Config.SysOrigin, + MinAppVersion: bundle.Config.MinAppVersion, LevelConfigs: buildLevelPayloads(bundle.Levels, rewardItems, 0, 0, false), HasRewardRecord: false, } if !bundle.Config.Enabled { resp.ActivityStatus = activityStatusDisabled } + if bundle.Config.Enabled && !isFirstRechargeRewardAppVersionSupported(appVersion, bundle.Config.MinAppVersion) { + resp.Enabled = false + resp.ActivityStatus = activityStatusDisabled + } record, ok, err := s.loadUserGrantRecord(ctx, bundle.Config.SysOrigin, user.UserID) if err != nil { diff --git a/internal/service/firstrechargereward/mapper.go b/internal/service/firstrechargereward/mapper.go index d59c6f6..dfa9a94 100644 --- a/internal/service/firstrechargereward/mapper.go +++ b/internal/service/firstrechargereward/mapper.go @@ -12,10 +12,11 @@ import ( func configSnapshotFromModel(row model.FirstRechargeRewardConfig) configSnapshot { return configSnapshot{ - ID: row.ID, - SysOrigin: strings.ToUpper(strings.TrimSpace(row.SysOrigin)), - Enabled: row.Enabled, - UpdateTime: row.UpdateTime, + ID: row.ID, + SysOrigin: strings.ToUpper(strings.TrimSpace(row.SysOrigin)), + Enabled: row.Enabled, + MinAppVersion: strings.TrimSpace(row.MinAppVersion), + UpdateTime: row.UpdateTime, } } @@ -24,6 +25,7 @@ func levelSnapshotFromModel(row model.FirstRechargeRewardLevel) levelSnapshot { ID: row.ID, Level: row.Level, RechargeAmountCents: row.RechargeAmountCents, + GoogleProductID: normalizeGoogleProductID(row.GoogleProductID), RewardGroupID: row.RewardGroupID, RewardGroupName: strings.TrimSpace(row.RewardGroupName), Enabled: row.Enabled, @@ -51,6 +53,7 @@ func buildLevelPayloads( Level: level.Level, RechargeAmount: formatAmountCents(level.RechargeAmountCents), RechargeAmountCents: level.RechargeAmountCents, + GoogleProductID: level.GoogleProductID, RewardGroupID: level.RewardGroupID, RewardGroupName: level.RewardGroupName, RewardItems: rewardItems[level.RewardGroupID], @@ -62,19 +65,27 @@ func buildLevelPayloads( return payloads } -func pickMatchedLevel(levels []levelSnapshot, rechargeAmountCents int64) (levelSnapshot, bool) { - var matched levelSnapshot - ok := false +func pickMatchedLevel(levels []levelSnapshot, rechargeAmountCents int64, googleProductID string) (levelSnapshot, bool) { + googleProductID = normalizeGoogleProductID(googleProductID) + if googleProductID != "" { + for _, level := range levels { + if !level.Enabled || level.RewardGroupID <= 0 || level.GoogleProductID == "" { + continue + } + if level.GoogleProductID == googleProductID { + return level, true + } + } + } for _, level := range levels { if !level.Enabled || level.RechargeAmountCents <= 0 || level.RewardGroupID <= 0 { continue } - if rechargeAmountCents >= level.RechargeAmountCents && (!ok || level.RechargeAmountCents > matched.RechargeAmountCents) { - matched = level - ok = true + if rechargeAmountCents == level.RechargeAmountCents { + return level, true } } - return matched, ok + return levelSnapshot{}, false } func sortLevels(levels []levelSnapshot) { diff --git a/internal/service/firstrechargereward/records.go b/internal/service/firstrechargereward/records.go index 665bfae..13e5f56 100644 --- a/internal/service/firstrechargereward/records.go +++ b/internal/service/firstrechargereward/records.go @@ -92,6 +92,7 @@ func grantRecordViewFromModel(row model.FirstRechargeRewardGrantRecord, rewardIt Level: row.Level, PayPlatform: row.PayPlatform, PaymentMethod: row.PaymentMethod, + GoogleProductID: row.GoogleProductID, SourceOrderID: row.SourceOrderID, RewardGroupID: row.RewardGroupID, RewardGroupName: row.RewardGroupName, diff --git a/internal/service/firstrechargereward/service_test.go b/internal/service/firstrechargereward/service_test.go index aad750f..819aeb3 100644 --- a/internal/service/firstrechargereward/service_test.go +++ b/internal/service/firstrechargereward/service_test.go @@ -20,8 +20,9 @@ func TestSaveConfigReturnsRewardItems(t *testing.T) { req := mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, + "minAppVersion": "1.5.0", "levelConfigs": [ - {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} + {"level": 1, "rechargeAmount": "9.99", "googleProductId": "gold_999", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`) resp, err := service.SaveConfig(context.Background(), req) @@ -38,6 +39,9 @@ func TestSaveConfigReturnsRewardItems(t *testing.T) { if level.RechargeAmountCents != 999 || level.RewardGroupID != 1001 { t.Fatalf("level payload = %+v", level) } + if level.GoogleProductID != "gold_999" { + t.Fatalf("google product id = %q", level.GoogleProductID) + } if len(level.RewardItems) != 1 || level.RewardItems[0].Name != "首冲礼包奖励" { t.Fatalf("reward items = %+v", level.RewardItems) } @@ -66,6 +70,7 @@ func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) { resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, + "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "2001", "rewardGroupName": "VIP礼包", "enabled": true} ] @@ -115,6 +120,7 @@ func TestRewardItemsReturnConcretePropsType(t *testing.T) { resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, + "minAppVersion": "1.5.0", "levelConfigs": [ {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "3001", "rewardGroupName": "道具礼包", "enabled": true} ] @@ -147,8 +153,9 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, + "minAppVersion": "1.5.0", "levelConfigs": [ - {"level": 1, "rechargeAmount": "5.00", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} + {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`)) if err != nil { @@ -185,7 +192,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { if err != nil { t.Fatalf("low amount ProcessRechargeEvent() error = %v", err) } - if lowAmount.Processed || lowAmount.Reason != "amount_not_reached" { + if lowAmount.Processed || lowAmount.Reason != "level_not_matched" { t.Fatalf("low amount response = %+v", lowAmount) } @@ -292,8 +299,9 @@ func TestProcessRechargeEventStartsCountingAtSupportedAppVersion(t *testing.T) { _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ "sysOrigin": "LIKEI", "enabled": true, + "minAppVersion": "1.5.0", "levelConfigs": [ - {"level": 1, "rechargeAmount": "5.00", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} + {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} ] }`)) if err != nil { @@ -380,13 +388,86 @@ func TestFirstRechargeRewardAppVersionSupported(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion); got != tt.want { + if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion, "1.5.0"); got != tt.want { t.Fatalf("isFirstRechargeRewardAppVersionSupported(%q) = %v, want %v", tt.appVersion, got, tt.want) } }) } } +func TestHomeRequiresConfiguredMinAppVersion(t *testing.T) { + service, gateway, _ := newTestService(t) + gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") + + _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ + "sysOrigin": "LIKEI", + "enabled": true, + "minAppVersion": "2.0.0", + "levelConfigs": [ + {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} + ] + }`)) + if err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + oldVersion, err := service.GetHome(context.Background(), AuthUser{UserID: 123, SysOrigin: "LIKEI"}, "1.9.9") + if err != nil { + t.Fatalf("old version GetHome() error = %v", err) + } + if oldVersion.Enabled || oldVersion.ActivityStatus != activityStatusDisabled { + t.Fatalf("old version home = %+v", oldVersion) + } + + supported, err := service.GetHome(context.Background(), AuthUser{UserID: 123, SysOrigin: "LIKEI"}, "2.0.0") + if err != nil { + t.Fatalf("supported GetHome() error = %v", err) + } + if !supported.Enabled || supported.ActivityStatus != activityStatusOngoing { + t.Fatalf("supported home = %+v", supported) + } +} + +func TestProcessRechargeEventMatchesGoogleProductIDBeforeAmount(t *testing.T) { + service, gateway, _ := newTestService(t) + gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") + gateway.groups[1002] = testRewardGroup(1002, "Google 首冲礼包") + + _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ + "sysOrigin": "LIKEI", + "enabled": true, + "minAppVersion": "1.5.0", + "levelConfigs": [ + {"level": 1, "rechargeAmount": "9.99", "googleProductId": "gold_999", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}, + {"level": 2, "rechargeAmount": "19.99", "googleProductId": "first_recharge_google_1999", "rewardGroupId": "1002", "rewardGroupName": "Google 首冲礼包", "enabled": true} + ] + }`)) + if err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + granted, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ + "eventId": "RECHARGE_SUCCESS:GOOGLE:PRODUCT_ID", + "sysOrigin": "LIKEI", + "userId": "123", + "amountCents": "999", + "payPlatform": "GOOGLE", + "paymentMethod": "GOOGLE", + "googleProductId": "first_recharge_google_1999", + "appVersion": "1.5.0", + "sourceOrderId": "PRODUCT_ID" + }`)) + if err != nil { + t.Fatalf("product id ProcessRechargeEvent() error = %v", err) + } + if !granted.Processed || granted.Status != statusSuccess || granted.Level != 2 { + t.Fatalf("product id response = %+v", granted) + } + if len(gateway.rewardRequests) != 1 || gateway.rewardRequests[0].SourceGroupID != 1002 { + t.Fatalf("reward requests = %+v", gateway.rewardRequests) + } +} + func newTestService(t *testing.T) (*Service, *fakeGateway, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) diff --git a/internal/service/firstrechargereward/types.go b/internal/service/firstrechargereward/types.go index 80e9b36..105db3d 100644 --- a/internal/service/firstrechargereward/types.go +++ b/internal/service/firstrechargereward/types.go @@ -65,6 +65,7 @@ type LevelPayload struct { Level int `json:"level"` RechargeAmount string `json:"rechargeAmount"` RechargeAmountCents int64 `json:"rechargeAmountCents"` + GoogleProductID string `json:"googleProductId,omitempty"` RewardGroupID int64 `json:"rewardGroupId,string"` RewardGroupName string `json:"rewardGroupName,omitempty"` RewardItems []RewardItem `json:"rewardItems"` @@ -78,25 +79,28 @@ type LevelInput struct { Level int `json:"level"` RechargeAmount flexibleAmount `json:"rechargeAmount"` RechargeAmountCents int64 `json:"rechargeAmountCents"` + GoogleProductID string `json:"googleProductId"` RewardGroupID flexibleInt64 `json:"rewardGroupId"` RewardGroupName string `json:"rewardGroupName"` Enabled bool `json:"enabled"` } type ConfigResponse struct { - Configured bool `json:"configured"` - ID int64 `json:"id,string"` - SysOrigin string `json:"sysOrigin"` - Enabled bool `json:"enabled"` - UpdateTime string `json:"updateTime,omitempty"` - LevelConfigs []LevelPayload `json:"levelConfigs"` + Configured bool `json:"configured"` + ID int64 `json:"id,string"` + SysOrigin string `json:"sysOrigin"` + Enabled bool `json:"enabled"` + MinAppVersion string `json:"minAppVersion,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` + LevelConfigs []LevelPayload `json:"levelConfigs"` } type SaveConfigRequest struct { - ID flexibleInt64 `json:"id"` - SysOrigin string `json:"sysOrigin"` - Enabled bool `json:"enabled"` - LevelConfigs []LevelInput `json:"levelConfigs"` + ID flexibleInt64 `json:"id"` + SysOrigin string `json:"sysOrigin"` + Enabled bool `json:"enabled"` + MinAppVersion string `json:"minAppVersion"` + LevelConfigs []LevelInput `json:"levelConfigs"` } type HomeResponse struct { @@ -108,6 +112,7 @@ type HomeResponse struct { HasRewardRecord bool `json:"hasRewardRecord"` RewardStatus string `json:"rewardStatus,omitempty"` Rewarded bool `json:"rewarded"` + MinAppVersion string `json:"minAppVersion,omitempty"` MatchedLevel int `json:"matchedLevel,omitempty"` RechargeAmount string `json:"rechargeAmount,omitempty"` RechargeAmountCents int64 `json:"rechargeAmountCents,omitempty"` @@ -141,6 +146,7 @@ type GrantRecordView struct { Level int `json:"level"` PayPlatform string `json:"payPlatform"` PaymentMethod string `json:"paymentMethod"` + GoogleProductID string `json:"googleProductId,omitempty"` SourceOrderID string `json:"sourceOrderId,omitempty"` RewardGroupID int64 `json:"rewardGroupId,string"` RewardGroupName string `json:"rewardGroupName,omitempty"` @@ -166,24 +172,26 @@ type RewardItem struct { } type RechargeEvent struct { - EventID string `json:"eventId"` - SysOrigin string `json:"sysOrigin"` - UserID flexibleInt64 `json:"userId"` - AmountCents flexibleInt64 `json:"amountCents"` - Amount flexibleString `json:"amount"` - AmountUSD flexibleString `json:"amountUsd"` - USDAmount flexibleString `json:"usdAmount"` - Currency string `json:"currency"` - PayPlatform string `json:"payPlatform"` - PaymentMethod string `json:"paymentMethod"` - AppVersion flexibleString `json:"appVersion"` - ClientVersion flexibleString `json:"clientVersion"` - Version flexibleString `json:"version"` - ReqVersion flexibleString `json:"reqVersion"` - SourceOrderID flexibleString `json:"sourceOrderId"` - OrderID flexibleString `json:"orderId"` - OccurredAt flexibleString `json:"occurredAt"` - Payload json.RawMessage `json:"payload,omitempty"` + EventID string `json:"eventId"` + SysOrigin string `json:"sysOrigin"` + UserID flexibleInt64 `json:"userId"` + AmountCents flexibleInt64 `json:"amountCents"` + Amount flexibleString `json:"amount"` + AmountUSD flexibleString `json:"amountUsd"` + USDAmount flexibleString `json:"usdAmount"` + Currency string `json:"currency"` + PayPlatform string `json:"payPlatform"` + PaymentMethod string `json:"paymentMethod"` + GoogleProductID string `json:"googleProductId"` + ProductID flexibleString `json:"productId"` + AppVersion flexibleString `json:"appVersion"` + ClientVersion flexibleString `json:"clientVersion"` + Version flexibleString `json:"version"` + ReqVersion flexibleString `json:"reqVersion"` + SourceOrderID flexibleString `json:"sourceOrderId"` + OrderID flexibleString `json:"orderId"` + OccurredAt flexibleString `json:"occurredAt"` + Payload json.RawMessage `json:"payload,omitempty"` } type ProcessRechargeResponse struct { @@ -200,32 +208,35 @@ type configBundle struct { } type configSnapshot struct { - ID int64 - SysOrigin string - Enabled bool - UpdateTime time.Time + ID int64 + SysOrigin string + Enabled bool + MinAppVersion string + UpdateTime time.Time } type levelSnapshot struct { ID int64 Level int RechargeAmountCents int64 + GoogleProductID string RewardGroupID int64 RewardGroupName string Enabled bool } type normalizedRechargeEvent struct { - EventID string - SysOrigin string - UserID int64 - AmountCents int64 - Currency string - PayPlatform string - PaymentMethod string - AppVersion string - SourceOrderID string - OccurredAt time.Time + EventID string + SysOrigin string + UserID int64 + AmountCents int64 + Currency string + PayPlatform string + PaymentMethod string + AppVersion string + GoogleProductID string + SourceOrderID string + OccurredAt time.Time } type flexibleInt64 int64 diff --git a/internal/service/firstrechargereward/version.go b/internal/service/firstrechargereward/version.go index 0c7c31a..1a2a3aa 100644 --- a/internal/service/firstrechargereward/version.go +++ b/internal/service/firstrechargereward/version.go @@ -5,14 +5,18 @@ import ( "strings" ) -const firstRechargeRewardMinAppVersion = "1.5.0" +const defaultFirstRechargeRewardMinAppVersion = "1.5.0" -func isFirstRechargeRewardAppVersionSupported(appVersion string) bool { +func isFirstRechargeRewardAppVersionSupported(appVersion string, minAppVersion string) bool { appVersion = strings.TrimSpace(appVersion) + minAppVersion = strings.TrimSpace(minAppVersion) + if minAppVersion == "" { + minAppVersion = defaultFirstRechargeRewardMinAppVersion + } if appVersion == "" { return false } - return compareAppVersion(appVersion, firstRechargeRewardMinAppVersion) >= 0 + return compareAppVersion(appVersion, minAppVersion) >= 0 } func compareAppVersion(left, right string) int { diff --git a/migrations/049_first_recharge_reward.sql b/migrations/049_first_recharge_reward.sql index f332df5..9c68ade 100644 --- a/migrations/049_first_recharge_reward.sql +++ b/migrations/049_first_recharge_reward.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_config` ( `id` bigint NOT NULL COMMENT '主键ID', `sys_origin` varchar(32) NOT NULL COMMENT '来源系统', `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用', + `min_app_version` varchar(32) NOT NULL DEFAULT '1.5.0' COMMENT '最低生效App版本', `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间', `update_time` datetime(3) DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), @@ -14,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_level` ( `config_id` bigint NOT NULL COMMENT '首冲奖励主配置ID', `level` int NOT NULL COMMENT '档位', `recharge_amount_cents` bigint NOT NULL DEFAULT '0' COMMENT '充值金额门槛,单位为分', + `google_product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Google商品ID', `reward_group_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组ID', `reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励资源组名称快照', `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用', @@ -22,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_level` ( PRIMARY KEY (`id`), UNIQUE KEY `uk_first_recharge_reward_level` (`config_id`, `level`), UNIQUE KEY `uk_first_recharge_reward_amount` (`config_id`, `recharge_amount_cents`), + KEY `idx_first_recharge_reward_level_google_product` (`google_product_id`), KEY `idx_first_recharge_reward_level_config` (`config_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励充值档位'; @@ -42,6 +45,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_grant_record` ( `level` int NOT NULL COMMENT '命中档位', `pay_platform` varchar(64) NOT NULL DEFAULT '' COMMENT '支付平台:GOOGLE/MIFA_PAY/PAYER_MAX等', `payment_method` varchar(32) NOT NULL DEFAULT '' COMMENT '充值方式:GOOGLE/THIRD_PARTY', + `google_product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Google商品ID', `source_order_id` varchar(128) NOT NULL DEFAULT '' COMMENT '来源订单ID', `reward_group_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组ID', `reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励资源组名称快照', @@ -58,5 +62,6 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_grant_record` ( UNIQUE KEY `uk_first_recharge_reward_event` (`event_id`), UNIQUE KEY `uk_first_recharge_reward_user` (`sys_origin`, `user_id`), KEY `idx_first_recharge_reward_status` (`sys_origin`, `user_id`, `status`), + KEY `idx_first_recharge_reward_grant_google_product` (`google_product_id`), KEY `idx_first_recharge_reward_order` (`source_order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励发放记录'; diff --git a/migrations/050_first_recharge_reward_version_google_product.sql b/migrations/050_first_recharge_reward_version_google_product.sql new file mode 100644 index 0000000..30af296 --- /dev/null +++ b/migrations/050_first_recharge_reward_version_google_product.sql @@ -0,0 +1,71 @@ +SET @current_schema := DATABASE(); + +SET @add_first_recharge_reward_min_app_version_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'first_recharge_reward_config' + AND column_name = 'min_app_version' + ), + 'ALTER TABLE `first_recharge_reward_config` ADD COLUMN `min_app_version` varchar(32) NOT NULL DEFAULT ''1.5.0'' COMMENT ''最低生效App版本'' AFTER `enabled`', + 'SELECT 1' +); +PREPARE add_first_recharge_reward_min_app_version_stmt FROM @add_first_recharge_reward_min_app_version_sql; +EXECUTE add_first_recharge_reward_min_app_version_stmt; +DEALLOCATE PREPARE add_first_recharge_reward_min_app_version_stmt; + +SET @add_first_recharge_reward_level_google_product_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'first_recharge_reward_level' + AND column_name = 'google_product_id' + ), + 'ALTER TABLE `first_recharge_reward_level` ADD COLUMN `google_product_id` varchar(128) NOT NULL DEFAULT '''' COMMENT ''Google商品ID'' AFTER `recharge_amount_cents`', + 'SELECT 1' +); +PREPARE add_first_recharge_reward_level_google_product_stmt FROM @add_first_recharge_reward_level_google_product_sql; +EXECUTE add_first_recharge_reward_level_google_product_stmt; +DEALLOCATE PREPARE add_first_recharge_reward_level_google_product_stmt; + +SET @add_first_recharge_reward_record_google_product_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'first_recharge_reward_grant_record' + AND column_name = 'google_product_id' + ), + 'ALTER TABLE `first_recharge_reward_grant_record` ADD COLUMN `google_product_id` varchar(128) NOT NULL DEFAULT '''' COMMENT ''Google商品ID'' AFTER `payment_method`', + 'SELECT 1' +); +PREPARE add_first_recharge_reward_record_google_product_stmt FROM @add_first_recharge_reward_record_google_product_sql; +EXECUTE add_first_recharge_reward_record_google_product_stmt; +DEALLOCATE PREPARE add_first_recharge_reward_record_google_product_stmt; + +SET @add_first_recharge_reward_level_google_product_idx_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = @current_schema + AND table_name = 'first_recharge_reward_level' + AND index_name = 'idx_first_recharge_reward_level_google_product' + ), + 'ALTER TABLE `first_recharge_reward_level` ADD INDEX `idx_first_recharge_reward_level_google_product` (`google_product_id`)', + 'SELECT 1' +); +PREPARE add_first_recharge_reward_level_google_product_idx_stmt FROM @add_first_recharge_reward_level_google_product_idx_sql; +EXECUTE add_first_recharge_reward_level_google_product_idx_stmt; +DEALLOCATE PREPARE add_first_recharge_reward_level_google_product_idx_stmt; + +SET @add_first_recharge_reward_record_google_product_idx_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = @current_schema + AND table_name = 'first_recharge_reward_grant_record' + AND index_name = 'idx_first_recharge_reward_grant_google_product' + ), + 'ALTER TABLE `first_recharge_reward_grant_record` ADD INDEX `idx_first_recharge_reward_grant_google_product` (`google_product_id`)', + 'SELECT 1' +); +PREPARE add_first_recharge_reward_record_google_product_idx_stmt FROM @add_first_recharge_reward_record_google_product_idx_sql; +EXECUTE add_first_recharge_reward_record_google_product_idx_stmt; +DEALLOCATE PREPARE add_first_recharge_reward_record_google_product_idx_stmt;