修复奖励

This commit is contained in:
hy001 2026-05-26 20:06:23 +08:00
parent 675f45bf44
commit c09433bf29
9 changed files with 285 additions and 12 deletions

View File

@ -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)

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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)
}