diff --git a/internal/service/yumigiftchallenge/config.go b/internal/service/yumigiftchallenge/config.go index c701308..70a38be 100644 --- a/internal/service/yumigiftchallenge/config.go +++ b/internal/service/yumigiftchallenge/config.go @@ -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 != "" {