From c09433bf29a43dafe6b9f672eb302c8ac1c0a5bc Mon Sep 17 00:00:00 2001 From: hy001 Date: Tue, 26 May 2026 20:06:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A5=96=E5=8A=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/integration/gateways.go | 10 ++ internal/integration/java.go | 127 +++++++++++++++++- internal/integration/java_test.go | 63 +++++++++ .../service/rechargereward/claim_records.go | 2 +- internal/service/rechargereward/config.go | 24 +++- .../service/rechargereward/config_test.go | 49 +++++++ internal/service/rechargereward/home.go | 2 +- internal/service/rechargereward/mapper.go | 19 ++- internal/service/rechargereward/types.go | 1 + 9 files changed, 285 insertions(+), 12 deletions(-) diff --git a/internal/integration/gateways.go b/internal/integration/gateways.go index 4f2abf5..0187bdb 100644 --- a/internal/integration/gateways.go +++ b/internal/integration/gateways.go @@ -131,6 +131,11 @@ func (g *Gateways) GetRewardGroupDetail(ctx context.Context, groupID int64) (Rew return g.RewardQuery.GetRewardGroupDetail(ctx, groupID) } +// FindRewardGroupDetail 透传到奖励查询网关,按名称查找奖励组详情。 +func (g *Gateways) FindRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) { + return g.RewardQuery.FindRewardGroupDetail(ctx, sysOrigin, name) +} + // GetUserProfile 透传到资料网关。 func (g *Gateways) GetUserProfile(ctx context.Context, userID int64) (UserProfile, error) { return g.Profile.GetUserProfile(ctx, userID) @@ -365,6 +370,11 @@ func (g *RewardQueryGateway) GetRewardGroupDetail(ctx context.Context, groupID i return g.client.GetRewardGroupDetail(ctx, groupID) } +// FindRewardGroupDetail 按名称查询奖励组详情。 +func (g *RewardQueryGateway) FindRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) { + return g.client.FindRewardGroupDetail(ctx, sysOrigin, name) +} + // GetNobleVIPAbility 查询贵族 VIP 能力配置。 func (g *RewardQueryGateway) GetNobleVIPAbility(ctx context.Context, sourceID int64) (PropsNobleVIPAbility, error) { return g.client.GetNobleVIPAbility(ctx, sourceID) diff --git a/internal/integration/java.go b/internal/integration/java.go index 8c02054..6a1d2a3 100644 --- a/internal/integration/java.go +++ b/internal/integration/java.go @@ -253,6 +253,13 @@ type RewardGroupDetail struct { RewardConfigList []RewardGroupItem `json:"rewardConfigList"` } +type rewardGroupPage struct { + Records []RewardGroupDetail `json:"records"` + Total int64 `json:"total"` + Current int `json:"current"` + Size int `json:"size"` +} + type UserTotalRecharge struct { ID Int64Value `json:"id"` UserID Int64Value `json:"userId"` @@ -336,6 +343,7 @@ type resultResponse[T any] struct { Code int `json:"code"` Message string `json:"message"` Body T `json:"body"` + Data T `json:"data"` Success *bool `json:"success"` } @@ -698,15 +706,128 @@ func (c *Client) SendOfficialNoticeCustomize(ctx context.Context, req OfficialNo } func (c *Client) GetRewardGroupDetail(ctx context.Context, groupID int64) (RewardGroupDetail, error) { - endpoint := c.cfg.Java.OtherBaseURL + "/props-activity/reward/group/client/getByGroupId?id=" + url.QueryEscape(strconv.FormatInt(groupID, 10)) + detail, err := c.getInnerRewardGroupDetail(ctx, groupID) + if err == nil && rewardGroupDetailHasContent(detail) { + return detail, nil + } + if fallback, fallbackErr := c.getConsoleRewardGroupDetail(ctx, groupID); fallbackErr == nil && rewardGroupDetailHasContent(fallback) { + return fallback, nil + } else if err == nil && fallbackErr != nil { + return detail, nil + } + return detail, err +} + +func (c *Client) getInnerRewardGroupDetail(ctx context.Context, groupID int64) (RewardGroupDetail, error) { + endpoint := strings.TrimRight(c.cfg.Java.OtherBaseURL, "/") + "/props-activity/reward/group/client/getByGroupId?id=" + url.QueryEscape(strconv.FormatInt(groupID, 10)) var resp resultResponse[RewardGroupDetail] if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil { return RewardGroupDetail{}, err } - if int64(resp.Body.ID) == 0 { + detail := resp.Body + if int64(detail.ID) == 0 { + detail = resp.Data + } + if int64(detail.ID) == 0 { return RewardGroupDetail{}, fmt.Errorf("empty reward group response") } - return resp.Body, nil + return detail, nil +} + +func (c *Client) getConsoleRewardGroupDetail(ctx context.Context, groupID int64) (RewardGroupDetail, error) { + baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/") + if baseURL == "" { + return RewardGroupDetail{}, fmt.Errorf("console base url is empty") + } + endpoint := baseURL + "/props/activity/reward/group/" + url.PathEscape(strconv.FormatInt(groupID, 10)) + var detail RewardGroupDetail + if err := c.getJSON(ctx, endpoint, nil, &detail); err != nil { + return RewardGroupDetail{}, err + } + if int64(detail.ID) == 0 { + return RewardGroupDetail{}, fmt.Errorf("empty reward group response") + } + return detail, nil +} + +func rewardGroupDetailHasContent(detail RewardGroupDetail) bool { + return int64(detail.ID) > 0 && len(detail.RewardConfigList) > 0 +} + +func (c *Client) FindRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) { + groupName := strings.TrimSpace(name) + if groupName == "" { + return RewardGroupDetail{}, fmt.Errorf("reward group name is empty") + } + if detail, err := c.findInnerRewardGroupDetail(ctx, sysOrigin, groupName); err == nil && rewardGroupDetailHasContent(detail) { + return detail, nil + } + return c.findConsoleRewardGroupDetail(ctx, sysOrigin, groupName) +} + +func (c *Client) findInnerRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) { + endpoint := strings.TrimRight(c.cfg.Java.OtherBaseURL, "/") + "/props-activity/reward/group/client/page" + payload := map[string]any{ + "current": 1, + "cursor": 1, + "limit": 10, + "name": name, + "shelfStatus": true, + "size": 10, + } + if origin := strings.TrimSpace(sysOrigin); origin != "" { + payload["sysOrigin"] = origin + } + var resp resultResponse[rewardGroupPage] + if err := c.postJSON(ctx, endpoint, payload, nil, &resp); err != nil { + return RewardGroupDetail{}, err + } + page := resp.Body + if len(page.Records) == 0 { + page = resp.Data + } + return pickRewardGroupDetailByName(page.Records, name) +} + +func (c *Client) findConsoleRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) { + baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/") + if baseURL == "" { + return RewardGroupDetail{}, fmt.Errorf("console base url is empty") + } + values := url.Values{} + values.Set("current", "1") + values.Set("cursor", "1") + values.Set("limit", "10") + values.Set("name", name) + values.Set("shelfStatus", "true") + values.Set("size", "10") + if origin := strings.TrimSpace(sysOrigin); origin != "" { + values.Set("sysOrigin", origin) + } + endpoint := baseURL + "/props/activity/reward/group/page?" + values.Encode() + var page rewardGroupPage + if err := c.getJSON(ctx, endpoint, nil, &page); err != nil { + return RewardGroupDetail{}, err + } + return pickRewardGroupDetailByName(page.Records, name) +} + +func pickRewardGroupDetailByName(records []RewardGroupDetail, name string) (RewardGroupDetail, error) { + if len(records) == 0 { + return RewardGroupDetail{}, fmt.Errorf("empty reward group page response") + } + target := strings.TrimSpace(name) + for _, record := range records { + if strings.TrimSpace(record.Name) == target && rewardGroupDetailHasContent(record) { + return record, nil + } + } + for _, record := range records { + if rewardGroupDetailHasContent(record) { + return record, nil + } + } + return records[0], nil } func (c *Client) MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]UserTotalRecharge, error) { diff --git a/internal/integration/java_test.go b/internal/integration/java_test.go index 88e72b9..5b558f8 100644 --- a/internal/integration/java_test.go +++ b/internal/integration/java_test.go @@ -77,3 +77,66 @@ func TestResolveRegionCodeByCountryCodeUsesRegionConfig(t *testing.T) { t.Fatalf("region config calls = %d, want cached 1", calls) } } + +func TestGetRewardGroupDetailAcceptsDataWrapper(t *testing.T) { + groupID := int64(2056981527522242600) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/props-activity/reward/group/client/getByGroupId" { + t.Fatalf("request = %s %s, want GET /props-activity/reward/group/client/getByGroupId", r.Method, r.URL.Path) + } + if r.URL.Query().Get("id") != "2056981527522242600" { + t.Fatalf("id query = %q", r.URL.Query().Get("id")) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"id":"2056981527522242600","name":"累计充值10美金","rewardConfigList":[{"id":"1","type":"PROPS","detailType":"AVATAR_FRAME","name":"Frame","quantity":"1","sourceUrl":"https://example.com/frame.svga"}]}}`)) + })) + defer server.Close() + + client := New(config.Config{ + HTTP: config.HTTPConfig{Timeout: time.Second}, + Java: config.JavaConfig{OtherBaseURL: server.URL}, + }) + got, err := client.GetRewardGroupDetail(context.Background(), groupID) + if err != nil { + t.Fatalf("GetRewardGroupDetail() error = %v", err) + } + if int64(got.ID) != groupID || len(got.RewardConfigList) != 1 { + t.Fatalf("detail = %+v, want group with one reward item", got) + } +} + +func TestGetRewardGroupDetailFallsBackToConsoleEndpoint(t *testing.T) { + groupID := int64(2056981527522242600) + var innerCalls, consoleCalls int + mux := http.NewServeMux() + mux.HandleFunc("/props-activity/reward/group/client/getByGroupId", func(w http.ResponseWriter, r *http.Request) { + innerCalls++ + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"body":{"id":"2056981527522242600","name":"累计充值10美金","rewardConfigList":[]}}`)) + }) + mux.HandleFunc("/console/props/activity/reward/group/2056981527522242600", func(w http.ResponseWriter, r *http.Request) { + consoleCalls++ + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"2056981527522242600","name":"累计充值10美金","rewardConfigList":[{"id":"1","type":"GIFT","name":"Gift","quantity":"1","cover":"https://example.com/gift.png"}]}`)) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client := New(config.Config{ + HTTP: config.HTTPConfig{Timeout: time.Second}, + Java: config.JavaConfig{ + OtherBaseURL: server.URL, + ConsoleBaseURL: server.URL + "/console", + }, + }) + got, err := client.GetRewardGroupDetail(context.Background(), groupID) + if err != nil { + t.Fatalf("GetRewardGroupDetail() error = %v", err) + } + if innerCalls != 1 || consoleCalls != 1 { + t.Fatalf("calls inner=%d console=%d, want 1/1", innerCalls, consoleCalls) + } + if len(got.RewardConfigList) != 1 || got.RewardConfigList[0].Name != "Gift" { + t.Fatalf("detail = %+v, want fallback reward item", got) + } +} diff --git a/internal/service/rechargereward/claim_records.go b/internal/service/rechargereward/claim_records.go index ddf6c1f..8aaa2a7 100644 --- a/internal/service/rechargereward/claim_records.go +++ b/internal/service/rechargereward/claim_records.go @@ -117,7 +117,7 @@ ORDER BY claims.last_claim_time DESC, claims.user_id ASC` if err != nil { return nil, err } - rewardItems := s.loadRewardItemsMap(ctx, claimDetailLevels(details)) + rewardItems := s.loadRewardItemsMap(ctx, sysOrigin, claimDetailLevels(details)) records = make([]RechargeRewardClaimRecordView, 0, len(rows)) for _, row := range rows { records = append(records, rechargeRewardClaimRecordView(row, displayMonth, rechargeDate, details[row.UserID], rewardItems)) diff --git a/internal/service/rechargereward/config.go b/internal/service/rechargereward/config.go index 93be59d..ad4b6ee 100644 --- a/internal/service/rechargereward/config.go +++ b/internal/service/rechargereward/config.go @@ -39,7 +39,7 @@ func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*RechargeRew if err != nil { return nil, err } - rewardItems := s.loadRewardItemsMap(ctx, levels) + rewardItems := s.loadRewardItemsMap(ctx, configRow.SysOrigin, levels) return &RechargeRewardConfigResponse{ Configured: true, ID: configRow.ID, @@ -60,7 +60,7 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveRechargeRewardConfigRe return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error()) } - levels, err := s.normalizeLevelInputs(req.LevelConfigs) + levels, err := s.normalizeLevelInputs(ctx, sysOrigin, req.LevelConfigs) if err != nil { return nil, err } @@ -133,7 +133,7 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveRechargeRewardConfigRe return s.GetConfig(ctx, rechargeRewardSysOriginBySavedID(ctx, s.db, savedConfigID, sysOrigin)) } -func (s *Service) normalizeLevelInputs(inputs []RechargeRewardLevelInput) ([]model.RechargeRewardLevel, error) { +func (s *Service) normalizeLevelInputs(ctx context.Context, sysOrigin string, inputs []RechargeRewardLevelInput) ([]model.RechargeRewardLevel, error) { if len(inputs) == 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "levelConfigs is required") } @@ -152,7 +152,7 @@ func (s *Service) normalizeLevelInputs(inputs []RechargeRewardLevelInput) ([]mod if amountCents <= 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_recharge_amount", "rechargeAmount must be greater than 0") } - rewardGroupID := item.RewardGroupID.Int64Ptr() + rewardGroupID := s.resolveRewardGroupID(ctx, sysOrigin, item.RewardGroupID.Int64Ptr(), item.RewardGroupName) if item.RewardGold <= 0 && (rewardGroupID == nil || *rewardGroupID <= 0) { return nil, NewAppError(http.StatusBadRequest, "invalid_reward", "rewardGold or rewardGroupId is required") } @@ -188,6 +188,22 @@ func (s *Service) normalizeLevelInputs(inputs []RechargeRewardLevelInput) ([]mod return rows, nil } +func (s *Service) resolveRewardGroupID(ctx context.Context, sysOrigin string, rewardGroupID *int64, rewardGroupName string) *int64 { + normalized := normalizeRewardGroupID(rewardGroupID) + if normalized == nil || s.java == nil || strings.TrimSpace(rewardGroupName) == "" { + return normalized + } + if detail, err := s.java.GetRewardGroupDetail(ctx, *normalized); err == nil && rewardGroupDetailHasItems(detail) { + return normalized + } + detail, err := s.java.FindRewardGroupDetail(ctx, sysOrigin, rewardGroupName) + if err != nil || int64(detail.ID) <= 0 { + return normalized + } + resolved := int64(detail.ID) + return &resolved +} + func (s *Service) loadConfigBundle(ctx context.Context, sysOrigin string) (*rechargeRewardConfigBundle, error) { sysOrigin = s.normalizeSysOrigin(sysOrigin) diff --git a/internal/service/rechargereward/config_test.go b/internal/service/rechargereward/config_test.go index 7d1073d..e7472f2 100644 --- a/internal/service/rechargereward/config_test.go +++ b/internal/service/rechargereward/config_test.go @@ -224,6 +224,46 @@ INSERT INTO likei_wallet.user_freight_balance_running_water (accept_user_id, sys } } +func TestGetHomeFallsBackToRewardGroupNameWhenStoredIDLostPrecision(t *testing.T) { + actualGroupID := int64(2056723577461338114) + roundedGroupID := int64(2056723577461338000) + service, _ := newTestService(t, rechargeRewardTestGateway{ + groups: map[int64]integration.RewardGroupDetail{ + actualGroupID: { + ID: integration.Int64Value(actualGroupID), + Name: "累充活动$50", + RewardConfigList: []integration.RewardGroupItem{ + {ID: integration.Int64Value(1), Type: "BADGE", Name: "recharge50$", Quantity: integration.Int64Value(30), Cover: "https://example.com/badge.png"}, + }, + }, + }, + }) + if _, err := service.SaveConfig(context.Background(), SaveRechargeRewardConfigRequest{ + SysOrigin: "LIKEI", + Enabled: true, + Timezone: "Asia/Riyadh", + LevelConfigs: []RechargeRewardLevelInput{ + {Level: 1, RechargeAmount: mustAmount(t, "50"), RewardGroupID: ptrFlexibleInt64(roundedGroupID), RewardGroupName: "累充活动$50", Enabled: true}, + }, + }); err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + resp, err := service.GetConfig(context.Background(), "LIKEI") + if err != nil { + t.Fatalf("GetConfig() error = %v", err) + } + if len(resp.LevelConfigs) != 1 || len(resp.LevelConfigs[0].RewardItems) != 1 { + t.Fatalf("reward items = %+v, want fallback by reward group name", resp.LevelConfigs) + } + if resp.LevelConfigs[0].RewardGroupID == nil || *resp.LevelConfigs[0].RewardGroupID != actualGroupID { + t.Fatalf("rewardGroupId = %v, want resolved actual group id %d", resp.LevelConfigs[0].RewardGroupID, actualGroupID) + } + if got := resp.LevelConfigs[0].RewardItems[0].Name; got != "recharge50$" { + t.Fatalf("reward item name = %q, want recharge50$", got) + } +} + func TestPageClaimRecordsUsesActualReceiveRecords(t *testing.T) { groupID := int64(9001) service, db := newTestService(t, rechargeRewardTestGateway{ @@ -408,6 +448,15 @@ func (g rechargeRewardTestGateway) GetRewardGroupDetail(_ context.Context, group return integration.RewardGroupDetail{}, fmt.Errorf("group not found") } +func (g rechargeRewardTestGateway) FindRewardGroupDetail(_ context.Context, _ string, name string) (integration.RewardGroupDetail, error) { + for _, detail := range g.groups { + if detail.Name == name { + return detail, nil + } + } + return integration.RewardGroupDetail{}, fmt.Errorf("group not found") +} + func (g rechargeRewardTestGateway) MapUserTotalRecharge(context.Context, []int64) (map[int64][]integration.UserTotalRecharge, error) { return g.total, nil } diff --git a/internal/service/rechargereward/home.go b/internal/service/rechargereward/home.go index 371d215..6f06352 100644 --- a/internal/service/rechargereward/home.go +++ b/internal/service/rechargereward/home.go @@ -83,7 +83,7 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*RechargeRewardHo resp.NextRechargeAmount = formatAmountCents(next.RechargeAmountCents) resp.NextRechargeAmountCents = next.RechargeAmountCents } - rewardItems := s.loadRewardItemsMap(ctx, bundle.Levels) + rewardItems := s.loadRewardItemsMap(ctx, resp.SysOrigin, bundle.Levels) matchedLevel := 0 if matchedOK { matchedLevel = matched.Level diff --git a/internal/service/rechargereward/mapper.go b/internal/service/rechargereward/mapper.go index f092b16..4915974 100644 --- a/internal/service/rechargereward/mapper.go +++ b/internal/service/rechargereward/mapper.go @@ -131,6 +131,7 @@ func sortLevels(levels []rechargeRewardLevelSnapshot) { func (s *Service) loadRewardItemsMap( ctx context.Context, + sysOrigin string, levels []rechargeRewardLevelSnapshot, ) map[int64][]RechargeRewardRewardItem { if s.java == nil { @@ -138,10 +139,14 @@ func (s *Service) loadRewardItemsMap( } groupIDs := make([]int64, 0) seen := make(map[int64]struct{}) + groupNames := make(map[int64]string) for _, level := range levels { if level.RewardGroupID == nil || *level.RewardGroupID <= 0 { continue } + if strings.TrimSpace(level.RewardGroupName) != "" { + groupNames[*level.RewardGroupID] = level.RewardGroupName + } if _, exists := seen[*level.RewardGroupID]; exists { continue } @@ -153,15 +158,23 @@ func (s *Service) loadRewardItemsMap( result := make(map[int64][]RechargeRewardRewardItem, len(groupIDs)) for _, groupID := range groupIDs { detail, err := s.java.GetRewardGroupDetail(ctx, groupID) - if err != nil { - result[groupID] = []RechargeRewardRewardItem{} - continue + if err != nil || !rewardGroupDetailHasItems(detail) { + if fallback, fallbackErr := s.java.FindRewardGroupDetail(ctx, sysOrigin, groupNames[groupID]); fallbackErr == nil && rewardGroupDetailHasItems(fallback) { + detail = fallback + } else { + result[groupID] = []RechargeRewardRewardItem{} + continue + } } result[groupID] = rewardItemsFromGroup(detail) } return result } +func rewardGroupDetailHasItems(detail integration.RewardGroupDetail) bool { + return len(detail.RewardConfigList) > 0 +} + func rewardItemsFromGroup(detail integration.RewardGroupDetail) []RechargeRewardRewardItem { items := make([]RechargeRewardRewardItem, 0, len(detail.RewardConfigList)) for _, item := range detail.RewardConfigList { diff --git a/internal/service/rechargereward/types.go b/internal/service/rechargereward/types.go index d0bcd48..9218728 100644 --- a/internal/service/rechargereward/types.go +++ b/internal/service/rechargereward/types.go @@ -35,6 +35,7 @@ type rechargeRewardDB interface { type rechargeRewardJavaGateway interface { GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error) GetRewardGroupDetail(ctx context.Context, groupID int64) (integration.RewardGroupDetail, error) + FindRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (integration.RewardGroupDetail, error) MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]integration.UserTotalRecharge, error) GetThisMonthTotalPersonalRecharge(ctx context.Context, userID int64) (integration.DecimalString, error) }