2026-06-26 16:20:58 +08:00

922 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package levelconfig
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/integration/walletclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"github.com/gin-gonic/gin"
)
const (
statusActive = "active"
statusDisabled = "disabled"
longBadgeResourceIDKey = "long_badge_resource_id"
avatarFrameResourceIDKey = "avatar_frame_resource_id"
shortBadgeResourceIDKey = "short_badge_resource_id"
)
type Handler struct {
activity activityclient.Client
wallet walletclient.Client
audit shared.OperationLogger
}
func New(activity activityclient.Client, wallet walletclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{activity: activity, wallet: wallet, audit: audit}
}
type levelConfigDTO struct {
Tracks []trackDTO `json:"tracks"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type trackDTO struct {
Track string `json:"track"`
Name string `json:"name"`
Status string `json:"status"`
SortOrder int32 `json:"sort_order"`
DisplayConfigJSON string `json:"display_config_json"`
Rules []ruleDTO `json:"rules"`
Tiers []tierDTO `json:"tiers"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type ruleDTO struct {
Track string `json:"track"`
Level int32 `json:"level"`
RequiredValue int64 `json:"required_value"`
Name string `json:"name"`
Status string `json:"status"`
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
SortOrder int32 `json:"sort_order"`
LongBadgeResourceID int64 `json:"long_badge_resource_id"`
AvatarFrameResourceID int64 `json:"avatar_frame_resource_id"`
ShortBadgeResourceID int64 `json:"short_badge_resource_id"`
DisplayConfigJSON string `json:"display_config_json"`
CreatedByAdminID int64 `json:"created_by_admin_id"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type tierDTO struct {
TierID int64 `json:"tier_id"`
Track string `json:"track"`
MinLevel int32 `json:"min_level"`
MaxLevel int32 `json:"max_level"`
Name string `json:"name"`
DisplayAvatarFrameResourceID int64 `json:"display_avatar_frame_resource_id"`
DisplayBadgeResourceID int64 `json:"display_badge_resource_id"`
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
Status string `json:"status"`
DisplayConfigJSON string `json:"display_config_json"`
CreatedByAdminID int64 `json:"created_by_admin_id"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type trackRequest struct {
Name *string `json:"name"`
Status *string `json:"status"`
SortOrder *int32 `json:"sort_order"`
DisplayConfigJSON *string `json:"display_config_json"`
}
type ruleRequest struct {
RequiredValue *int64 `json:"required_value"`
Name *string `json:"name"`
Status *string `json:"status"`
RewardResourceGroupID *int64 `json:"reward_resource_group_id"`
SortOrder *int32 `json:"sort_order"`
LongBadgeResourceID *int64 `json:"long_badge_resource_id"`
AvatarFrameResourceID *int64 `json:"avatar_frame_resource_id"`
ShortBadgeResourceID *int64 `json:"short_badge_resource_id"`
DisplayConfigJSON *string `json:"display_config_json"`
}
type tierRequest struct {
Track *string `json:"track"`
MinLevel *int32 `json:"min_level"`
MaxLevel *int32 `json:"max_level"`
Name *string `json:"name"`
DisplayAvatarFrameResourceID *int64 `json:"display_avatar_frame_resource_id"`
DisplayBadgeResourceID *int64 `json:"display_badge_resource_id"`
RewardResourceGroupID *int64 `json:"reward_resource_group_id"`
Status *string `json:"status"`
DisplayConfigJSON *string `json:"display_config_json"`
}
// ListLevelConfig 返回用户管理 > 等级配置页面需要的三条等级轨道完整配置。
func (h *Handler) ListLevelConfig(c *gin.Context) {
track := normalizeTrack(c.Query("track"))
status := normalizeStatusFilter(c.Query("status"))
if track != "" && !validTrack(track) {
response.BadRequest(c, "等级类型不正确")
return
}
if status != "" && !validStatus(status) {
response.BadRequest(c, "状态参数不正确")
return
}
resp, err := h.activity.ListLevelConfig(c.Request.Context(), &activityv1.ListLevelConfigRequest{
Meta: h.meta(c),
Track: track,
Status: status,
})
if err != nil {
response.ServerError(c, "获取等级配置失败")
return
}
response.OK(c, configFromProto(resp))
}
// UpdateTrack 更新 wealth/game/charm 三条固定等级轨道的基础展示配置。
func (h *Handler) UpdateTrack(c *gin.Context) {
track := normalizeTrack(c.Param("track"))
if !validTrack(track) {
response.BadRequest(c, "等级类型不正确")
return
}
var req trackRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "等级轨道参数不正确")
return
}
current, ok, err := h.currentTrack(c, track)
if err != nil {
response.ServerError(c, "读取等级轨道失败")
return
}
name := current.Name
if req.Name != nil {
name = strings.TrimSpace(*req.Name)
}
if name == "" {
name = defaultTrackName(track)
}
status := current.Status
if req.Status != nil {
status = normalizeStatus(*req.Status)
}
if status == "" {
status = statusActive
}
if !validStatus(status) {
response.BadRequest(c, "状态参数不正确")
return
}
sortOrder := current.SortOrder
if !ok {
sortOrder = defaultTrackSort(track)
}
if req.SortOrder != nil {
sortOrder = *req.SortOrder
}
displayConfig := current.DisplayConfigJSON
if req.DisplayConfigJSON != nil {
displayConfig = strings.TrimSpace(*req.DisplayConfigJSON)
}
displayConfig, err = normalizeJSONObject(displayConfig)
if err != nil {
response.BadRequest(c, "展示配置 JSON 不正确")
return
}
resp, err := h.activity.UpsertLevelTrack(c.Request.Context(), &activityv1.UpsertLevelTrackRequest{
Meta: h.meta(c),
Track: track,
Name: name,
Status: status,
SortOrder: sortOrder,
DisplayConfigJson: displayConfig,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := trackFromProto(resp.GetTrack(), nil, nil)
h.auditLog(c, "update-level-track", "growth_level_tracks", track, fmt.Sprintf("status=%s", item.Status))
response.OK(c, item)
}
// UpsertRule 配置某条轨道某一级的升级阈值、等级奖励资源组和每级长徽章素材。
func (h *Handler) UpsertRule(c *gin.Context) {
track := normalizeTrack(c.Param("track"))
level, ok := parseNonNegativeInt32(c.Param("level"))
if !ok || !validTrack(track) {
response.BadRequest(c, "等级参数不正确")
return
}
var req ruleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "等级规则参数不正确")
return
}
config, err := h.loadConfig(c, track, "")
if err != nil {
response.ServerError(c, "读取等级配置失败")
return
}
current, exists := findRule(config.GetRules(), track, level)
trackName := defaultTrackName(track)
if currentTrack, found := findTrack(config.GetTracks(), track); found {
trackName = currentTrack.GetName()
}
requiredValue := current.GetRequiredValue()
if req.RequiredValue != nil {
requiredValue = *req.RequiredValue
}
if requiredValue < 0 {
response.BadRequest(c, "升级所需数值不能小于 0")
return
}
name := current.GetName()
if req.Name != nil {
name = strings.TrimSpace(*req.Name)
}
if name == "" {
name = fmt.Sprintf("%s Lv.%d", trackName, level)
}
status := current.GetStatus()
if req.Status != nil {
status = normalizeStatus(*req.Status)
}
if status == "" {
status = statusActive
}
if !validStatus(status) {
response.BadRequest(c, "状态参数不正确")
return
}
rewardResourceGroupID := current.GetRewardResourceGroupId()
if req.RewardResourceGroupID != nil {
rewardResourceGroupID = *req.RewardResourceGroupID
}
if rewardResourceGroupID < 0 {
response.BadRequest(c, "奖励资源组 ID 不正确")
return
}
sortOrder := current.GetSortOrder()
if !exists {
sortOrder = level
}
if req.SortOrder != nil {
sortOrder = *req.SortOrder
}
displayConfig := current.GetDisplayConfigJson()
if req.DisplayConfigJSON != nil {
displayConfig = strings.TrimSpace(*req.DisplayConfigJSON)
}
displayConfig, err = mergeRuleDisplayResources(displayConfig, req.LongBadgeResourceID, req.AvatarFrameResourceID, req.ShortBadgeResourceID)
if err != nil {
response.BadRequest(c, "展示配置 JSON 不正确")
return
}
if badgeID := longBadgeResourceID(displayConfig); badgeID > 0 {
if err := h.validateLevelLongBadgeResource(c, track, badgeID); err != nil {
response.BadRequest(c, err.Error())
return
}
}
if avatarFrameID := avatarFrameResourceID(displayConfig); avatarFrameID > 0 {
if err := h.validateLevelAvatarFrameResource(c, track, avatarFrameID); err != nil {
response.BadRequest(c, err.Error())
return
}
}
if shortBadgeID := shortBadgeResourceID(displayConfig); shortBadgeID > 0 {
if err := h.validateLevelShortBadgeResource(c, track, shortBadgeID); err != nil {
response.BadRequest(c, err.Error())
return
}
}
resp, err := h.activity.UpsertLevelRule(c.Request.Context(), &activityv1.UpsertLevelRuleRequest{
Meta: h.meta(c),
Track: track,
Level: level,
RequiredValue: requiredValue,
Name: name,
Status: status,
RewardResourceGroupId: rewardResourceGroupID,
SortOrder: sortOrder,
DisplayConfigJson: displayConfig,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := ruleFromProto(resp.GetRule())
h.auditLog(c, "upsert-level-rule", "growth_level_rules", fmt.Sprintf("%s:%d", track, level), fmt.Sprintf("required_value=%d long_badge_resource_id=%d avatar_frame_resource_id=%d short_badge_resource_id=%d", item.RequiredValue, item.LongBadgeResourceID, item.AvatarFrameResourceID, item.ShortBadgeResourceID))
response.OK(c, item)
}
func (h *Handler) CreateTier(c *gin.Context) {
h.upsertTier(c, 0)
}
func (h *Handler) UpdateTier(c *gin.Context) {
tierID, ok := parsePositiveInt64(c.Param("tier_id"))
if !ok {
response.BadRequest(c, "等级段 ID 不正确")
return
}
h.upsertTier(c, tierID)
}
// upsertTier 复用已有等级段配置能力,保持 1-10、11-20 这类展示徽章仍由 activity-service 拥有。
func (h *Handler) upsertTier(c *gin.Context, tierID int64) {
var req tierRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "等级段参数不正确")
return
}
var current *activityv1.LevelTier
if tierID > 0 {
config, err := h.loadConfig(c, "", "")
if err != nil {
response.ServerError(c, "读取等级段配置失败")
return
}
var found bool
current, found = findTier(config.GetTiers(), tierID)
if !found {
response.NotFound(c, "等级段不存在")
return
}
}
track := ""
if current != nil {
track = current.GetTrack()
}
if req.Track != nil {
track = normalizeTrack(*req.Track)
}
if !validTrack(track) {
response.BadRequest(c, "等级类型不正确")
return
}
minLevel := int32(0)
if current != nil {
minLevel = current.GetMinLevel()
}
if req.MinLevel != nil {
minLevel = *req.MinLevel
}
maxLevel := int32(0)
if current != nil {
maxLevel = current.GetMaxLevel()
}
if req.MaxLevel != nil {
maxLevel = *req.MaxLevel
}
if minLevel < 0 || maxLevel < minLevel {
response.BadRequest(c, "等级段范围不正确")
return
}
name := ""
if current != nil {
name = current.GetName()
}
if req.Name != nil {
name = strings.TrimSpace(*req.Name)
}
if name == "" {
name = fmt.Sprintf("%s Lv.%d-%d", defaultTrackName(track), minLevel, maxLevel)
}
status := statusActive
if current != nil {
status = current.GetStatus()
}
if req.Status != nil {
status = normalizeStatus(*req.Status)
}
if !validStatus(status) {
response.BadRequest(c, "状态参数不正确")
return
}
avatarFrameResourceID := int64(0)
if current != nil {
avatarFrameResourceID = current.GetDisplayAvatarFrameResourceId()
}
if req.DisplayAvatarFrameResourceID != nil {
avatarFrameResourceID = *req.DisplayAvatarFrameResourceID
}
badgeResourceID := int64(0)
if current != nil {
badgeResourceID = current.GetDisplayBadgeResourceId()
}
if req.DisplayBadgeResourceID != nil {
badgeResourceID = *req.DisplayBadgeResourceID
}
rewardResourceGroupID := int64(0)
if current != nil {
rewardResourceGroupID = current.GetRewardResourceGroupId()
}
if req.RewardResourceGroupID != nil {
rewardResourceGroupID = *req.RewardResourceGroupID
}
if avatarFrameResourceID < 0 || badgeResourceID < 0 || rewardResourceGroupID < 0 {
response.BadRequest(c, "等级段资源 ID 不正确")
return
}
displayConfig := "{}"
if current != nil {
displayConfig = current.GetDisplayConfigJson()
}
if req.DisplayConfigJSON != nil {
displayConfig = strings.TrimSpace(*req.DisplayConfigJSON)
}
var err error
displayConfig, err = normalizeJSONObject(displayConfig)
if err != nil {
response.BadRequest(c, "展示配置 JSON 不正确")
return
}
resp, err := h.activity.UpsertLevelTier(c.Request.Context(), &activityv1.UpsertLevelTierRequest{
Meta: h.meta(c),
TierId: tierID,
Track: track,
MinLevel: minLevel,
MaxLevel: maxLevel,
Name: name,
DisplayAvatarFrameResourceId: avatarFrameResourceID,
DisplayBadgeResourceId: badgeResourceID,
RewardResourceGroupId: rewardResourceGroupID,
Status: status,
DisplayConfigJson: displayConfig,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := tierFromProto(resp.GetTier())
h.auditLog(c, "upsert-level-tier", "growth_level_tiers", strconv.FormatInt(item.TierID, 10), fmt.Sprintf("%s:%d-%d", item.Track, item.MinLevel, item.MaxLevel))
response.OK(c, item)
}
func (h *Handler) loadConfig(c *gin.Context, track string, status string) (*activityv1.ListLevelConfigResponse, error) {
return h.activity.ListLevelConfig(c.Request.Context(), &activityv1.ListLevelConfigRequest{
Meta: h.meta(c),
Track: track,
Status: status,
})
}
func (h *Handler) validateLevelLongBadgeResource(c *gin.Context, track string, resourceID int64) error {
if resourceID <= 0 {
return nil
}
if h.wallet == nil {
return fmt.Errorf("资源服务不可用")
}
resp, err := h.wallet.GetResource(c.Request.Context(), &walletv1.GetResourceRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ResourceId: resourceID,
})
if err != nil {
return fmt.Errorf("等级徽章资源不存在")
}
resource := resp.GetResource()
if resource.GetResourceType() != "badge" {
return fmt.Errorf("等级长徽章必须选择徽章资源")
}
metadata := levelBadgeMetadataFromJSON(resource.GetMetadataJson())
if metadata.BadgeForm != "strip" {
return fmt.Errorf("等级长徽章必须选择长徽章素材")
}
if metadata.BadgeKind != "level" {
return fmt.Errorf("等级长徽章必须选择等级徽章素材")
}
if metadata.LevelTrack != track {
return fmt.Errorf("等级长徽章所属等级不匹配")
}
return nil
}
func (h *Handler) validateLevelAvatarFrameResource(c *gin.Context, track string, resourceID int64) error {
if resourceID <= 0 {
return nil
}
if h.wallet == nil {
return fmt.Errorf("资源服务不可用")
}
resp, err := h.wallet.GetResource(c.Request.Context(), &walletv1.GetResourceRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ResourceId: resourceID,
})
if err != nil {
return fmt.Errorf("等级头像框资源不存在")
}
resource := resp.GetResource()
if resource.GetResourceType() != "avatar_frame" {
return fmt.Errorf("等级头像框必须选择头像框资源")
}
metadata := levelAvatarFrameMetadataFromJSON(resource.GetMetadataJson())
if metadata.AvatarFrameKind != "level" {
return fmt.Errorf("等级头像框必须选择等级头像框素材")
}
if metadata.LevelTrack != track {
return fmt.Errorf("等级头像框所属等级不匹配")
}
return nil
}
func (h *Handler) validateLevelShortBadgeResource(c *gin.Context, track string, resourceID int64) error {
if resourceID <= 0 {
return nil
}
if h.wallet == nil {
return fmt.Errorf("资源服务不可用")
}
resp, err := h.wallet.GetResource(c.Request.Context(), &walletv1.GetResourceRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ResourceId: resourceID,
})
if err != nil {
return fmt.Errorf("等级短徽章资源不存在")
}
resource := resp.GetResource()
if resource.GetResourceType() != "badge" {
return fmt.Errorf("等级短徽章必须选择徽章资源")
}
metadata := levelBadgeMetadataFromJSON(resource.GetMetadataJson())
if metadata.BadgeForm != "tile" {
return fmt.Errorf("等级短徽章必须选择短徽章素材")
}
if metadata.BadgeKind != "level" {
return fmt.Errorf("等级短徽章必须选择等级徽章素材")
}
if metadata.LevelTrack != track {
return fmt.Errorf("等级短徽章所属等级不匹配")
}
return nil
}
func (h *Handler) currentTrack(c *gin.Context, track string) (trackDTO, bool, error) {
config, err := h.loadConfig(c, track, "")
if err != nil {
return trackDTO{}, false, err
}
item, ok := findTrack(config.GetTracks(), track)
if !ok {
return trackDTO{}, false, nil
}
return trackFromProto(item, nil, nil), true, nil
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UnixMilli(),
}
}
func (h *Handler) auditLog(c *gin.Context, action string, resource string, resourceID string, detail string) {
shared.OperationLogWithResourceID(c, h.audit, action, resource, resourceID, "success", detail)
}
func configFromProto(resp *activityv1.ListLevelConfigResponse) levelConfigDTO {
if resp == nil {
return levelConfigDTO{Tracks: []trackDTO{}}
}
rulesByTrack := make(map[string][]ruleDTO)
for _, rule := range resp.GetRules() {
item := ruleFromProto(rule)
rulesByTrack[item.Track] = append(rulesByTrack[item.Track], item)
}
tiersByTrack := make(map[string][]tierDTO)
for _, tier := range resp.GetTiers() {
item := tierFromProto(tier)
tiersByTrack[item.Track] = append(tiersByTrack[item.Track], item)
}
tracks := make([]trackDTO, 0, len(resp.GetTracks()))
for _, track := range resp.GetTracks() {
code := track.GetTrack()
tracks = append(tracks, trackFromProto(track, rulesByTrack[code], tiersByTrack[code]))
}
return levelConfigDTO{Tracks: tracks, ServerTimeMS: resp.GetServerTimeMs()}
}
func trackFromProto(item *activityv1.LevelTrack, rules []ruleDTO, tiers []tierDTO) trackDTO {
if item == nil {
return trackDTO{Rules: []ruleDTO{}, Tiers: []tierDTO{}}
}
if rules == nil {
rules = []ruleDTO{}
}
if tiers == nil {
tiers = []tierDTO{}
}
return trackDTO{
Track: item.GetTrack(),
Name: item.GetName(),
Status: item.GetStatus(),
SortOrder: item.GetSortOrder(),
DisplayConfigJSON: item.GetDisplayConfigJson(),
Rules: rules,
Tiers: tiers,
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func ruleFromProto(item *activityv1.LevelRule) ruleDTO {
if item == nil {
return ruleDTO{}
}
return ruleDTO{
Track: item.GetTrack(),
Level: item.GetLevel(),
RequiredValue: item.GetRequiredValue(),
Name: item.GetName(),
Status: item.GetStatus(),
RewardResourceGroupID: item.GetRewardResourceGroupId(),
SortOrder: item.GetSortOrder(),
LongBadgeResourceID: longBadgeResourceID(item.GetDisplayConfigJson()),
AvatarFrameResourceID: avatarFrameResourceID(item.GetDisplayConfigJson()),
ShortBadgeResourceID: shortBadgeResourceID(item.GetDisplayConfigJson()),
DisplayConfigJSON: item.GetDisplayConfigJson(),
CreatedByAdminID: item.GetCreatedByAdminId(),
UpdatedByAdminID: item.GetUpdatedByAdminId(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func tierFromProto(item *activityv1.LevelTier) tierDTO {
if item == nil {
return tierDTO{}
}
return tierDTO{
TierID: item.GetTierId(),
Track: item.GetTrack(),
MinLevel: item.GetMinLevel(),
MaxLevel: item.GetMaxLevel(),
Name: item.GetName(),
DisplayAvatarFrameResourceID: item.GetDisplayAvatarFrameResourceId(),
DisplayBadgeResourceID: item.GetDisplayBadgeResourceId(),
RewardResourceGroupID: item.GetRewardResourceGroupId(),
Status: item.GetStatus(),
DisplayConfigJSON: item.GetDisplayConfigJson(),
CreatedByAdminID: item.GetCreatedByAdminId(),
UpdatedByAdminID: item.GetUpdatedByAdminId(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func findTrack(items []*activityv1.LevelTrack, track string) (*activityv1.LevelTrack, bool) {
for _, item := range items {
if item.GetTrack() == track {
return item, true
}
}
return nil, false
}
func findRule(items []*activityv1.LevelRule, track string, level int32) (*activityv1.LevelRule, bool) {
for _, item := range items {
if item.GetTrack() == track && item.GetLevel() == level {
return item, true
}
}
return nil, false
}
func findTier(items []*activityv1.LevelTier, tierID int64) (*activityv1.LevelTier, bool) {
for _, item := range items {
if item.GetTierId() == tierID {
return item, true
}
}
return nil, false
}
func mergeRuleDisplayResources(raw string, longBadgeResourceID *int64, avatarFrameResourceID *int64, shortBadgeResourceID *int64) (string, error) {
payload, err := objectFromJSON(raw)
if err != nil {
return "", err
}
if longBadgeResourceID != nil {
if *longBadgeResourceID < 0 {
return "", fmt.Errorf("long badge resource id is invalid")
}
// 长徽章是每一级展示素材;沿用 display_config_json 可避免为展示扩展反复改表。
payload[longBadgeResourceIDKey] = *longBadgeResourceID
}
if avatarFrameResourceID != nil {
if *avatarFrameResourceID < 0 {
return "", fmt.Errorf("avatar frame resource id is invalid")
}
// 等级头像框也是每一级独立物料;和长徽章共用 display_config_json避免为等级展示资源反复扩表。
payload[avatarFrameResourceIDKey] = *avatarFrameResourceID
}
if shortBadgeResourceID != nil {
if *shortBadgeResourceID < 0 {
return "", fmt.Errorf("short badge resource id is invalid")
}
// 短徽章是每一级发放给用户的等级奖励;放在 display_config_json 内保持规则表结构稳定。
payload[shortBadgeResourceIDKey] = *shortBadgeResourceID
}
data, err := json.Marshal(payload)
if err != nil {
return "", err
}
return string(data), nil
}
func normalizeJSONObject(raw string) (string, error) {
payload, err := objectFromJSON(raw)
if err != nil {
return "", err
}
data, err := json.Marshal(payload)
if err != nil {
return "", err
}
return string(data), nil
}
func objectFromJSON(raw string) (map[string]any, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
raw = "{}"
}
decoder := json.NewDecoder(bytes.NewBufferString(raw))
decoder.UseNumber()
var payload map[string]any
if err := decoder.Decode(&payload); err != nil {
return nil, err
}
if payload == nil {
payload = map[string]any{}
}
return payload, nil
}
func longBadgeResourceID(raw string) int64 {
return positiveInt64FromDisplayConfig(raw, longBadgeResourceIDKey)
}
func avatarFrameResourceID(raw string) int64 {
return positiveInt64FromDisplayConfig(raw, avatarFrameResourceIDKey)
}
func shortBadgeResourceID(raw string) int64 {
return positiveInt64FromDisplayConfig(raw, shortBadgeResourceIDKey)
}
func positiveInt64FromDisplayConfig(raw string, key string) int64 {
payload, err := objectFromJSON(raw)
if err != nil {
return 0
}
value, ok := payload[key]
if !ok {
return 0
}
switch typed := value.(type) {
case json.Number:
parsed, err := typed.Int64()
if err == nil && parsed > 0 {
return parsed
}
case float64:
if typed > 0 {
return int64(typed)
}
case string:
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
if err == nil && parsed > 0 {
return parsed
}
}
return 0
}
type levelBadgeMetadata struct {
BadgeForm string `json:"badge_form"`
BadgeKind string `json:"badge_kind"`
LevelTrack string `json:"level_track"`
}
type levelAvatarFrameMetadata struct {
AvatarFrameKind string `json:"avatar_frame_kind"`
LevelTrack string `json:"level_track"`
}
func levelBadgeMetadataFromJSON(raw string) levelBadgeMetadata {
payload := levelBadgeMetadata{}
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
return levelBadgeMetadata{}
}
payload.BadgeForm = strings.ToLower(strings.TrimSpace(payload.BadgeForm))
payload.BadgeKind = strings.ToLower(strings.TrimSpace(payload.BadgeKind))
payload.LevelTrack = normalizeTrack(payload.LevelTrack)
return payload
}
func levelAvatarFrameMetadataFromJSON(raw string) levelAvatarFrameMetadata {
payload := levelAvatarFrameMetadata{}
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
return levelAvatarFrameMetadata{}
}
payload.AvatarFrameKind = strings.ToLower(strings.TrimSpace(payload.AvatarFrameKind))
payload.LevelTrack = normalizeTrack(payload.LevelTrack)
return payload
}
func parseNonNegativeInt32(raw string) (int32, bool) {
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 32)
if err != nil || value < 0 {
return 0, false
}
return int32(value), true
}
func parsePositiveInt64(raw string) (int64, bool) {
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil || value <= 0 {
return 0, false
}
return value, true
}
func normalizeTrack(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeStatus(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeStatusFilter(value string) string {
value = normalizeStatus(value)
if value == "all" {
return ""
}
return value
}
func validTrack(value string) bool {
switch value {
case "wealth", "game", "charm":
return true
default:
return false
}
}
func validStatus(value string) bool {
return value == statusActive || value == statusDisabled
}
func defaultTrackName(track string) string {
switch track {
case "wealth":
return "财富等级"
case "game":
return "游戏等级"
case "charm":
return "魅力等级"
default:
return "等级"
}
}
func defaultTrackSort(track string) int32 {
switch track {
case "wealth":
return 10
case "game":
return 20
case "charm":
return 30
default:
return 0
}
}