From f1b781e3be23892074a8d165b75b49b010f10e57 Mon Sep 17 00:00:00 2001 From: hy001 Date: Mon, 25 May 2026 23:20:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8C=83=E5=9B=B4=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E9=A6=96=E5=85=85=E7=89=88=E6=9C=AC=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/integration/java.go | 17 +- internal/service/firstrechargereward/event.go | 33 +++ .../service/firstrechargereward/mapper.go | 25 ++- .../firstrechargereward/service_test.go | 191 ++++++++++++++++-- internal/service/firstrechargereward/types.go | 5 + .../service/firstrechargereward/version.go | 75 +++++++ 6 files changed, 323 insertions(+), 23 deletions(-) create mode 100644 internal/service/firstrechargereward/version.go diff --git a/internal/integration/java.go b/internal/integration/java.go index 1d06cfd..8c02054 100644 --- a/internal/integration/java.go +++ b/internal/integration/java.go @@ -235,14 +235,15 @@ type OfficialNoticeCustomizeRequest struct { } type RewardGroupItem struct { - ID Int64Value `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Content string `json:"content"` - Quantity Int64Value `json:"quantity"` - Cover string `json:"cover"` - SourceURL string `json:"sourceUrl"` - Remark string `json:"remark"` + ID Int64Value `json:"id"` + Type string `json:"type"` + DetailType string `json:"detailType"` + Name string `json:"name"` + Content string `json:"content"` + Quantity Int64Value `json:"quantity"` + Cover string `json:"cover"` + SourceURL string `json:"sourceUrl"` + Remark string `json:"remark"` } type RewardGroupDetail struct { diff --git a/internal/service/firstrechargereward/event.go b/internal/service/firstrechargereward/event.go index b64f928..3204986 100644 --- a/internal/service/firstrechargereward/event.go +++ b/internal/service/firstrechargereward/event.go @@ -41,6 +41,9 @@ 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 } @@ -115,6 +118,7 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg payPlatform := strings.ToUpper(strings.TrimSpace(event.PayPlatform)) paymentMethod := strings.ToUpper(strings.TrimSpace(event.PaymentMethod)) + appVersion := normalizeRechargeAppVersion(event) sourceOrderID := firstNonEmpty(event.SourceOrderID.String(), event.OrderID.String()) eventID := strings.TrimSpace(event.EventID) if eventID == "" && sourceOrderID != "" { @@ -135,6 +139,7 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"), PayPlatform: payPlatform, PaymentMethod: paymentMethod, + AppVersion: appVersion, SourceOrderID: sourceOrderID, OccurredAt: occurredAt, }, nil @@ -357,6 +362,34 @@ func isAcceptedPaymentMethod(paymentMethod string, _ string) bool { return paymentMethod == paymentMethodGoogle || paymentMethod == paymentMethodThirdParty } +func normalizeRechargeAppVersion(event RechargeEvent) string { + return firstNonEmpty( + event.AppVersion.String(), + event.ClientVersion.String(), + event.Version.String(), + event.ReqVersion.String(), + rechargePayloadString(event.Payload, "appVersion", "clientVersion", "version", "reqVersion"), + ) +} + +func rechargePayloadString(payload json.RawMessage, keys ...string) string { + if len(payload) == 0 { + return "" + } + var values map[string]any + if err := json.Unmarshal(payload, &values); err != nil { + return "" + } + for _, key := range keys { + if value, ok := values[key]; ok { + if text := strings.TrimSpace(fmt.Sprint(value)); text != "" && text != "" { + return text + } + } + } + return "" +} + func firstPositiveAmountCents(values ...string) int64 { for _, value := range values { cents, err := parseAmountCents(value) diff --git a/internal/service/firstrechargereward/mapper.go b/internal/service/firstrechargereward/mapper.go index 2182a28..d59c6f6 100644 --- a/internal/service/firstrechargereward/mapper.go +++ b/internal/service/firstrechargereward/mapper.go @@ -120,7 +120,7 @@ func rewardItemsFromGroup(detail integration.RewardGroupDetail) []RewardItem { for _, item := range detail.RewardConfigList { items = append(items, RewardItem{ ID: int64(item.ID), - Type: strings.TrimSpace(item.Type), + Type: rewardItemDisplayType(item), Name: strings.TrimSpace(item.Name), Content: strings.TrimSpace(item.Content), Quantity: int64(item.Quantity), @@ -131,6 +131,29 @@ func rewardItemsFromGroup(detail integration.RewardGroupDetail) []RewardItem { return items } +func rewardItemDisplayType(item integration.RewardGroupItem) string { + itemType := strings.ToUpper(strings.TrimSpace(item.Type)) + detailType := strings.ToUpper(strings.TrimSpace(item.DetailType)) + if itemType == "PROPS" { + if detailType != "" && detailType != "PROPS" { + return detailType + } + if isVipRewardItem(item) { + return "NOBLE_VIP" + } + } + return itemType +} + +func isVipRewardItem(item integration.RewardGroupItem) bool { + name := strings.ToUpper(strings.TrimSpace(item.Name)) + remark := strings.ToUpper(strings.TrimSpace(item.Remark)) + return strings.Contains(name, "VIP") || + strings.Contains(name, "NOBLE") || + strings.Contains(remark, "VIP") || + strings.Contains(remark, "NOBLE") +} + func formatDateTime(t time.Time) string { if t.IsZero() { return "" diff --git a/internal/service/firstrechargereward/service_test.go b/internal/service/firstrechargereward/service_test.go index 1650c5b..aad750f 100644 --- a/internal/service/firstrechargereward/service_test.go +++ b/internal/service/firstrechargereward/service_test.go @@ -50,14 +50,15 @@ func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) { Name: "VIP礼包", RewardConfigList: []integration.RewardGroupItem{ { - ID: integration.Int64Value(11), - Type: "PROPS", - Name: "VIP 1", - Content: "2049402425177018400", - Quantity: integration.Int64Value(30), - Cover: "", - SourceURL: "https://example.com/vip-short-badge.png", - Remark: "30 天", + ID: integration.Int64Value(11), + Type: "PROPS", + DetailType: "NOBLE_VIP", + Name: "VIP 1", + Content: "2049402425177018400", + Quantity: integration.Int64Value(30), + Cover: "", + SourceURL: "https://example.com/vip-short-badge.png", + Remark: "30 天", }, }, } @@ -78,6 +79,59 @@ func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) { if got := resp.LevelConfigs[0].RewardItems[0].Cover; got != "https://example.com/vip-short-badge.png" { t.Fatalf("vip cover = %q", got) } + if got := resp.LevelConfigs[0].RewardItems[0].Type; got != "NOBLE_VIP" { + t.Fatalf("vip type = %q", got) + } +} + +func TestRewardItemsReturnConcretePropsType(t *testing.T) { + service, gateway, _ := newTestService(t) + gateway.groups[3001] = integration.RewardGroupDetail{ + ID: integration.Int64Value(3001), + Name: "道具礼包", + RewardConfigList: []integration.RewardGroupItem{ + { + ID: integration.Int64Value(21), + Type: "PROPS", + DetailType: "RIDE", + Name: "Luxury sports car", + Content: "2044620024357908482", + Quantity: integration.Int64Value(7), + Cover: "https://example.com/ride.png", + Remark: "Luxury sports car", + }, + { + ID: integration.Int64Value(22), + Type: "PROPS", + Name: "VIP 3", + Content: "2050200000000000003", + Quantity: integration.Int64Value(3), + Cover: "https://example.com/vip3.png", + Remark: "VIP 3", + }, + }, + } + + resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ + "sysOrigin": "LIKEI", + "enabled": true, + "levelConfigs": [ + {"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "3001", "rewardGroupName": "道具礼包", "enabled": true} + ] + }`)) + if err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + items := resp.LevelConfigs[0].RewardItems + if len(items) != 2 { + t.Fatalf("reward items = %+v", items) + } + if items[0].Type != "RIDE" { + t.Fatalf("ride type = %q", items[0].Type) + } + if items[1].Type != "NOBLE_VIP" { + t.Fatalf("vip fallback type = %q", items[1].Type) + } } func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { @@ -108,6 +162,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { "amountCents": "999", "payPlatform": "APPLE", "paymentMethod": "APPLE", + "appVersion": "1.5.0", "sourceOrderId": "1" }`)) if err != nil { @@ -124,6 +179,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { "amountCents": "499", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", + "appVersion": "1.5.0", "sourceOrderId": "LOW" }`)) if err != nil { @@ -140,6 +196,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", + "appVersion": "1.5.0", "sourceOrderId": "OK", "occurredAt": "2026-05-21T12:00:00Z" }`)) @@ -166,6 +223,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { "amountCents": "999", "payPlatform": "GOOGLE", "paymentMethod": "GOOGLE", + "appVersion": "1.5.0", "sourceOrderId": "OK" }`)) if err != nil { @@ -185,6 +243,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { "amountCents": "999", "payPlatform": "CUSTOM_PAY", "paymentMethod": "THIRD_PARTY", + "appVersion": "1.5.1", "sourceOrderId": "CUSTOM" }`)) if err != nil { @@ -204,6 +263,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { "amountCents": "999", "payPlatform": "CUSTOM_GOOGLE_GATEWAY", "paymentMethod": "GOOGLE", + "appVersion": "1.5.1", "sourceOrderId": "CUSTOM_GOOGLE" }`)) if err != nil { @@ -225,6 +285,108 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) { } } +func TestProcessRechargeEventStartsCountingAtSupportedAppVersion(t *testing.T) { + service, gateway, db := newTestService(t) + gateway.groups[1001] = testRewardGroup(1001, "首冲礼包") + + _, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{ + "sysOrigin": "LIKEI", + "enabled": true, + "levelConfigs": [ + {"level": 1, "rechargeAmount": "5.00", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true} + ] + }`)) + if err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + oldVersion, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ + "eventId": "RECHARGE_SUCCESS:GOOGLE:OLD_VERSION", + "sysOrigin": "LIKEI", + "userId": "123", + "amountCents": "999", + "payPlatform": "GOOGLE", + "paymentMethod": "GOOGLE", + "appVersion": "1.4.9", + "sourceOrderId": "OLD_VERSION" + }`)) + if err != nil { + t.Fatalf("old version ProcessRechargeEvent() error = %v", err) + } + if oldVersion.Processed || oldVersion.Reason != "app_version_not_supported" { + t.Fatalf("old version response = %+v", oldVersion) + } + + missingVersion, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ + "eventId": "RECHARGE_SUCCESS:GOOGLE:MISSING_VERSION", + "sysOrigin": "LIKEI", + "userId": "123", + "amountCents": "999", + "payPlatform": "GOOGLE", + "paymentMethod": "GOOGLE", + "sourceOrderId": "MISSING_VERSION" + }`)) + if err != nil { + t.Fatalf("missing version ProcessRechargeEvent() error = %v", err) + } + if missingVersion.Processed || missingVersion.Reason != "app_version_not_supported" { + t.Fatalf("missing version response = %+v", missingVersion) + } + + var count int64 + if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil { + t.Fatalf("count grant records after unsupported versions: %v", err) + } + if count != 0 { + t.Fatalf("grant record count after unsupported versions = %d", count) + } + + supported, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{ + "eventId": "RECHARGE_SUCCESS:GOOGLE:SUPPORTED_VERSION", + "sysOrigin": "LIKEI", + "userId": "123", + "amountCents": "999", + "payPlatform": "GOOGLE", + "paymentMethod": "GOOGLE", + "appVersion": "1.5.0", + "sourceOrderId": "SUPPORTED_VERSION" + }`)) + if err != nil { + t.Fatalf("supported version ProcessRechargeEvent() error = %v", err) + } + if !supported.Processed || supported.Status != statusSuccess { + t.Fatalf("supported version response = %+v", supported) + } + if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil { + t.Fatalf("count grant records after supported version: %v", err) + } + if count != 1 { + t.Fatalf("grant record count after supported version = %d", count) + } +} + +func TestFirstRechargeRewardAppVersionSupported(t *testing.T) { + tests := []struct { + name string + appVersion string + want bool + }{ + {name: "missing", appVersion: "", want: false}, + {name: "below", appVersion: "1.4.9", want: false}, + {name: "equal", appVersion: "1.5.0", want: true}, + {name: "short equal", appVersion: "1.5", want: true}, + {name: "above", appVersion: "1.5.1", want: true}, + {name: "suffix", appVersion: "1.5.0+100", want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion); got != tt.want { + t.Fatalf("isFirstRechargeRewardAppVersionSupported(%q) = %v, want %v", tt.appVersion, got, tt.want) + } + }) + } +} + 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{}) @@ -274,12 +436,13 @@ func testRewardGroup(id int64, name string) integration.RewardGroupDetail { Name: name, RewardConfigList: []integration.RewardGroupItem{ { - ID: integration.Int64Value(11), - Type: "PROPS", - Name: name + "奖励", - Content: "10001", - Quantity: integration.Int64Value(1), - Cover: "https://example.com/reward.png", + ID: integration.Int64Value(11), + Type: "PROPS", + DetailType: "RIDE", + Name: name + "奖励", + Content: "10001", + Quantity: integration.Int64Value(1), + Cover: "https://example.com/reward.png", }, }, } diff --git a/internal/service/firstrechargereward/types.go b/internal/service/firstrechargereward/types.go index e32e521..80e9b36 100644 --- a/internal/service/firstrechargereward/types.go +++ b/internal/service/firstrechargereward/types.go @@ -176,6 +176,10 @@ type RechargeEvent struct { 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"` @@ -219,6 +223,7 @@ type normalizedRechargeEvent struct { Currency string PayPlatform string PaymentMethod string + AppVersion string SourceOrderID string OccurredAt time.Time } diff --git a/internal/service/firstrechargereward/version.go b/internal/service/firstrechargereward/version.go new file mode 100644 index 0000000..0c7c31a --- /dev/null +++ b/internal/service/firstrechargereward/version.go @@ -0,0 +1,75 @@ +package firstrechargereward + +import ( + "strconv" + "strings" +) + +const firstRechargeRewardMinAppVersion = "1.5.0" + +func isFirstRechargeRewardAppVersionSupported(appVersion string) bool { + appVersion = strings.TrimSpace(appVersion) + if appVersion == "" { + return false + } + return compareAppVersion(appVersion, firstRechargeRewardMinAppVersion) >= 0 +} + +func compareAppVersion(left, right string) int { + leftParts := parseAppVersion(left) + rightParts := parseAppVersion(right) + maxLen := len(leftParts) + if len(rightParts) > maxLen { + maxLen = len(rightParts) + } + for i := 0; i < maxLen; i++ { + var lv, rv int + if i < len(leftParts) { + lv = leftParts[i] + } + if i < len(rightParts) { + rv = rightParts[i] + } + if lv > rv { + return 1 + } + if lv < rv { + return -1 + } + } + return 0 +} + +func parseAppVersion(version string) []int { + version = strings.TrimSpace(version) + if version == "" { + return []int{0} + } + parts := strings.Split(version, ".") + result := make([]int, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + result = append(result, 0) + continue + } + digits := strings.Builder{} + for _, r := range part { + if r < '0' || r > '9' { + break + } + digits.WriteRune(r) + } + if digits.Len() == 0 { + result = append(result, 0) + continue + } + value, err := strconv.Atoi(digits.String()) + if err != nil { + result = append(result, 0) + continue + } + result = append(result, value) + } + return result +}