922 lines
28 KiB
Go
922 lines
28 KiB
Go
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
|
||
}
|
||
}
|