fix: normalize Yumi gift challenge rewards

This commit is contained in:
hy001 2026-07-20 21:08:44 +08:00
parent 489aa40a50
commit 5cc47b90f6

View File

@ -4,12 +4,15 @@ import (
"context"
"errors"
"fmt"
"math"
"math/big"
"net/http"
"regexp"
"sort"
"strings"
"time"
"chatapp3-golang/internal/integration"
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/utils"
@ -607,8 +610,9 @@ func (s *Service) buildRewardSnapshots(ctx context.Context, activity model.YumiG
})
for index, item := range detail.RewardConfigList {
rewardConfigID := int64(item.ID)
if rewardConfigID <= 0 || strings.TrimSpace(item.Type) == "" || int64(item.Quantity) <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_item", fmt.Sprintf("reward group %d contains an invalid item", groupID))
normalizedItem, normalizeErr := normalizeFrozenRewardItem(groupID, index, item)
if normalizeErr != nil {
return nil, normalizeErr
}
key := fmt.Sprintf("%d:%d", groupID, rewardConfigID)
if _, exists := seen[key]; exists {
@ -626,8 +630,8 @@ func (s *Service) buildRewardSnapshots(ctx context.Context, activity model.YumiG
displayName := truncate(firstNonBlank(item.Name, item.BadgeName, item.Remark), 500)
rows = append(rows, model.YumiGiftChallengeRewardSnapshot{
ID: id, ActivityID: activity.ID, ResourceGroupID: groupID, RewardConfigID: rewardConfigID,
RewardType: strings.ToUpper(strings.TrimSpace(item.Type)), DetailType: strings.TrimSpace(item.DetailType),
Content: strings.TrimSpace(item.Content), Quantity: int64(item.Quantity), SortOrder: sortOrder,
RewardType: normalizedItem.rewardType, DetailType: normalizedItem.detailType,
Content: normalizedItem.content, Quantity: normalizedItem.quantity, SortOrder: sortOrder,
Remark: strings.TrimSpace(item.Remark), Cover: strings.TrimSpace(item.Cover),
SourceURL: strings.TrimSpace(item.SourceURL), DisplayName: displayName, CreateTime: now,
})
@ -639,6 +643,71 @@ func (s *Service) buildRewardSnapshots(ctx context.Context, activity model.YumiG
return rows, nil
}
type frozenRewardItem struct {
rewardType string
detailType string
content string
quantity int64
}
func normalizeFrozenRewardItem(groupID int64, index int, item integration.RewardGroupItem) (frozenRewardItem, error) {
result := frozenRewardItem{
rewardType: strings.ToUpper(strings.TrimSpace(item.Type)),
detailType: strings.TrimSpace(item.DetailType),
content: strings.TrimSpace(item.Content),
quantity: int64(item.Quantity),
}
itemLabel := fmt.Sprintf("reward group %d item %d (id=%d, type=%s, quantity=%d)",
groupID, index+1, int64(item.ID), result.rewardType, result.quantity)
if int64(item.ID) <= 0 {
return frozenRewardItem{}, NewAppError(http.StatusBadRequest, "invalid_reward_item", itemLabel+" has an invalid id")
}
if result.rewardType == "" {
return frozenRewardItem{}, NewAppError(http.StatusBadRequest, "invalid_reward_item", itemLabel+" has an empty type")
}
// 奖励组详情为了展示会把原始 PROPS 改写成 AVATAR_FRAME/RIDE 等子类型,
// 但冻结发奖接口只按基础类型分流;这里还原 PROPS同时保留子类型用于展示和通知。
switch result.rewardType {
case "AVATAR_FRAME", "RIDE", "NOBLE_VIP", "THEME", "LAYOUT", "CHAT_BUBBLE",
"FLOAT_PICTURE", "DATA_CARD", "VIP_EFFECT_IMAGE", "RED_PACKET":
if result.detailType == "" || strings.EqualFold(result.detailType, "PROPS") {
result.detailType = result.rewardType
}
result.rewardType = "PROPS"
case "ROOM_BADGE", "HONOR_ACTIVITY":
if result.detailType == "" || strings.EqualFold(result.detailType, "BADGE") {
result.detailType = result.rewardType
}
result.rewardType = "BADGE"
}
if result.quantity < 0 || result.quantity > math.MaxInt32 {
return frozenRewardItem{}, NewAppError(http.StatusBadRequest, "invalid_reward_item", itemLabel+" has an out-of-range quantity")
}
// 所有当前可发奖类型的 content 都是正整数资源 ID 或整数币值。启用时先拒绝空值、
// 非数字和零值,避免活动完成后才在 Java 发奖链路留下 UNKNOWN 记录。
contentNumber, contentOK := new(big.Int).SetString(result.content, 10)
if !contentOK || contentNumber.Sign() <= 0 {
return frozenRewardItem{}, NewAppError(http.StatusBadRequest, "invalid_reward_item", itemLabel+" has invalid content")
}
switch result.rewardType {
case "GOLD", "DIAMOND":
// 币值由 content 承载,旧奖励组按契约把 quantity 保存为 0不能把币值复制到
// quantity否则既破坏冻结事实也会与 Java 实际按 content 发币的逻辑不一致。
case "BADGE":
// quantity=0 是 Java 的永久徽章语义,正数才表示临时徽章天数。
case "PROPS", "GIFT", "EMOJI", "FRAGMENTS", "CUSTOMIZE", "PROP_COUPON":
if result.quantity == 0 {
return frozenRewardItem{}, NewAppError(http.StatusBadRequest, "invalid_reward_item", itemLabel+" requires a positive quantity")
}
default:
return frozenRewardItem{}, NewAppError(http.StatusBadRequest, "unsupported_reward_item", itemLabel+" is not supported by the reward delivery service")
}
return result, nil
}
func firstNonBlank(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {