package vipconfig import ( "strconv" "strings" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/walletclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/gin-gonic/gin" ) type Handler struct { wallet walletclient.Client audit shared.OperationLogger } func New(wallet walletclient.Client, audit shared.OperationLogger) *Handler { return &Handler{wallet: wallet, audit: audit} } type configRequest struct { Levels []vipLevelDTO `json:"levels"` } type programRequest struct { Config *vipProgramConfigDTO `json:"config"` } type grantVipRequest struct { CommandID string `json:"commandId"` TargetUserID any `json:"targetUserId"` Level int32 `json:"level"` Reason string `json:"reason"` } type grantVipTrialCardRequest struct { CommandID string `json:"commandId"` TargetUserID any `json:"targetUserId"` Level int32 `json:"level"` DurationMS int64 `json:"durationMs"` ResourceID *int64 `json:"resourceId"` Reason string `json:"reason"` } type configDTO struct { Levels []vipLevelDTO `json:"levels"` ProgramConfig vipProgramConfigDTO `json:"programConfig"` ServerTimeMS int64 `json:"serverTimeMs"` } type programDTO struct { Config vipProgramConfigDTO `json:"config"` Benefits []vipBenefitDTO `json:"benefits"` ServerTimeMS int64 `json:"serverTimeMs"` } type grantVipDTO struct { TransactionID string `json:"transactionId"` VIP userVipDTO `json:"vip"` RewardItems []vipRewardItemDTO `json:"rewardItems"` ServerTimeMS int64 `json:"serverTimeMs"` } type grantVipTrialCardDTO struct { TrialCard vipTrialCardDTO `json:"trialCard"` State vipStateDTO `json:"state"` ServerTimeMS int64 `json:"serverTimeMs"` } type userVipDTO struct { UserID string `json:"userId"` Level int32 `json:"level"` Name string `json:"name"` Active bool `json:"active"` StartedAtMS int64 `json:"startedAtMs"` ExpiresAtMS int64 `json:"expiresAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` ProgramType string `json:"programType"` ConfigVersion int64 `json:"configVersion"` } type vipLevelDTO struct { Level int32 `json:"level"` Name string `json:"name"` Status string `json:"status"` PriceCoin int64 `json:"priceCoin"` DurationMS int64 `json:"durationMs"` RewardResourceGroupID int64 `json:"rewardResourceGroupId"` SortOrder int32 `json:"sortOrder"` RechargeGateRequired bool `json:"rechargeGateRequired"` RequiredRechargeCoinAmount int64 `json:"requiredRechargeCoinAmount"` UserRechargeCoinAmount int64 `json:"userRechargeCoinAmount"` PurchaseLockedReason string `json:"purchaseLockedReason"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` Benefits []vipBenefitDTO `json:"benefits"` ConfigVersion int64 `json:"configVersion"` } type vipProgramConfigDTO struct { AppCode string `json:"appCode"` ProgramType string `json:"programType"` LevelCount int32 `json:"levelCount"` SameLevelExpiryPolicy string `json:"sameLevelExpiryPolicy"` UpgradeExpiryPolicy string `json:"upgradeExpiryPolicy"` DowngradePurchasePolicy string `json:"downgradePurchasePolicy"` BenefitInheritancePolicy string `json:"benefitInheritancePolicy"` GrantMode string `json:"grantMode"` TrialCardEnabled bool `json:"trialCardEnabled"` Status string `json:"status"` ConfigVersion int64 `json:"configVersion"` UpdatedByAdminID int64 `json:"updatedByAdminId"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` } type vipBenefitDTO struct { BenefitCode string `json:"benefitCode"` Name string `json:"name"` BenefitType string `json:"benefitType"` UnlockLevel int32 `json:"unlockLevel"` Status string `json:"status"` TrialEnabled bool `json:"trialEnabled"` ResourceID int64 `json:"resourceId"` ResourceType string `json:"resourceType"` ExecutionScope string `json:"executionScope"` AutoEquip bool `json:"autoEquip"` SortOrder int32 `json:"sortOrder"` MetadataJSON string `json:"metadataJson"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` } type vipTrialCardDTO struct { TrialCardID string `json:"trialCardId"` EntitlementID string `json:"entitlementId"` ResourceID int64 `json:"resourceId"` UserID string `json:"userId"` Level int32 `json:"level"` Name string `json:"name"` Status string `json:"status"` Equipped bool `json:"equipped"` DurationMS int64 `json:"durationMs"` EffectiveAtMS int64 `json:"effectiveAtMs"` ExpiresAtMS int64 `json:"expiresAtMs"` RemainingDurationMS int64 `json:"remainingDurationMs"` GrantSource string `json:"grantSource"` SourceGrantID string `json:"sourceGrantId"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` } type vipStateDTO struct { PaidVIP userVipDTO `json:"paidVip"` EquippedTrialCard vipTrialCardDTO `json:"equippedTrialCard"` EffectiveVIP userVipDTO `json:"effectiveVip"` EffectiveSource string `json:"effectiveSource"` EffectiveBenefits []vipBenefitDTO `json:"effectiveBenefits"` EvaluatedAtMS int64 `json:"evaluatedAtMs"` ProgramConfig vipProgramConfigDTO `json:"programConfig"` } type vipRewardItemDTO struct { ResourceID string `json:"resourceId"` ResourceCode string `json:"resourceCode"` ResourceType string `json:"resourceType"` Name string `json:"name"` Quantity int64 `json:"quantity"` ExpiresAtMS int64 `json:"expiresAtMs"` } func (h *Handler) GetProgram(c *gin.Context) { resp, err := h.wallet.GetVipProgramConfig(c.Request.Context(), &walletv1.GetVipProgramConfigRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), }) if err != nil { response.ServerError(c, "获取 VIP 体系失败") return } // 体系规则与权益目录同次返回,保证后台切换 App 后不会把上一 App 的权益矩阵误带入当前表单。 response.OK(c, programDTO{ Config: vipProgramConfigFromProto(resp.GetConfig()), Benefits: vipBenefitsFromProto(resp.GetBenefits()), ServerTimeMS: resp.GetServerTimeMs(), }) } func (h *Handler) UpdateProgram(c *gin.Context) { var req programRequest if err := c.ShouldBindJSON(&req); err != nil || req.Config == nil { response.BadRequest(c, "VIP 体系参数不正确") return } config := req.Config.toProto() if config.GetLevelCount() <= 0 || strings.TrimSpace(config.GetProgramType()) == "" || strings.TrimSpace(config.GetStatus()) == "" { response.BadRequest(c, "VIP 体系参数不正确") return } appCode := appctx.FromContext(c.Request.Context()) // AppCode、版本和审计操作者只取服务端上下文;即使客户端提交同名字段也不能跨 App 或伪造审计信息。 config.AppCode = appCode config.ConfigVersion = 0 config.UpdatedByAdminId = 0 config.CreatedAtMs = 0 config.UpdatedAtMs = 0 resp, err := h.wallet.UpdateAdminVipProgramConfig(c.Request.Context(), &walletv1.UpdateAdminVipProgramConfigRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appCode, Config: config, OperatorUserId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "update-vip-program", "vip_program_configs", appCode, "success", resp.GetConfig().GetProgramType()) response.OK(c, programDTO{ Config: vipProgramConfigFromProto(resp.GetConfig()), Benefits: []vipBenefitDTO{}, ServerTimeMS: resp.GetServerTimeMs(), }) } func (h *Handler) ListLevels(c *gin.Context) { resp, err := h.wallet.ListAdminVipLevels(c.Request.Context(), &walletv1.ListAdminVipLevelsRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), }) if err != nil { response.ServerError(c, "获取 VIP 配置失败") return } response.OK(c, configDTO{ Levels: vipLevelsFromProto(resp.GetLevels()), ProgramConfig: vipProgramConfigFromProto(resp.GetProgramConfig()), ServerTimeMS: resp.GetServerTimeMs(), }) } func (h *Handler) GrantVIP(c *gin.Context) { var req grantVipRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "VIP 赠送参数不正确") return } targetUserID, ok := parseFlexibleUserID(req.TargetUserID) if !ok || req.Level <= 0 { response.BadRequest(c, "VIP 赠送参数不正确") return } reason := strings.TrimSpace(req.Reason) if reason == "" { response.BadRequest(c, "赠送原因不能为空") return } commandID := strings.TrimSpace(req.CommandID) if commandID == "" { commandID = "admin_vip_grant:" + middleware.CurrentRequestID(c) } resp, err := h.wallet.GrantVip(c.Request.Context(), &walletv1.GrantVipRequest{ CommandId: commandID, AppCode: appctx.FromContext(c.Request.Context()), TargetUserId: targetUserID, Level: req.Level, GrantSource: "admin_grant", OperatorUserId: int64(middleware.CurrentUserID(c)), Reason: reason, }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "grant-vip", "user_vip_memberships", strconv.FormatInt(targetUserID, 10), "success", "level="+strconv.Itoa(int(req.Level))) response.Created(c, grantVipFromProto(resp)) } func (h *Handler) GrantVIPTrialCard(c *gin.Context) { var req grantVipTrialCardRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "VIP 体验卡赠送参数不正确") return } targetUserID, ok := parseFlexibleUserID(req.TargetUserID) if !ok || req.Level <= 0 || req.DurationMS <= 0 || (req.ResourceID != nil && *req.ResourceID <= 0) { response.BadRequest(c, "VIP 体验卡赠送参数不正确") return } reason := strings.TrimSpace(req.Reason) if reason == "" { response.BadRequest(c, "赠送原因不能为空") return } commandID := strings.TrimSpace(req.CommandID) if commandID == "" { commandID = "admin_vip_trial_card_grant:" + middleware.CurrentRequestID(c) } resp, err := h.wallet.GrantVipTrialCard(c.Request.Context(), &walletv1.GrantVipTrialCardRequest{ CommandId: commandID, AppCode: appctx.FromContext(c.Request.Context()), TargetUserId: targetUserID, Level: req.Level, DurationMs: req.DurationMS, ResourceId: req.ResourceID, GrantSource: "admin_grant", OperatorUserId: int64(middleware.CurrentUserID(c)), Reason: reason, }) if err != nil { response.BadRequest(c, err.Error()) return } // 体验卡是独立背包实例,审计目标使用 trial_card_id,后续切换佩戴不会覆盖本次赠送记录。 shared.OperationLogWithResourceID(c, h.audit, "grant-vip-trial-card", "user_vip_trial_cards", resp.GetTrialCard().GetTrialCardId(), "success", "target_user_id="+strconv.FormatInt(targetUserID, 10)) response.Created(c, grantVipTrialCardFromProto(resp)) } func (h *Handler) UpdateLevels(c *gin.Context) { var req configRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "VIP 配置参数不正确") return } input := make([]*walletv1.AdminVipLevelInput, 0, len(req.Levels)) for _, level := range req.Levels { benefits := make([]*walletv1.VipBenefit, 0, len(level.Benefits)) for _, benefit := range level.Benefits { benefits = append(benefits, benefit.toProto()) } input = append(input, &walletv1.AdminVipLevelInput{ Level: level.Level, Name: strings.TrimSpace(level.Name), Status: strings.TrimSpace(level.Status), PriceCoin: level.PriceCoin, DurationMs: level.DurationMS, RewardResourceGroupId: level.RewardResourceGroupID, // VIP 累充门槛已下线;后台旧请求即使携带旧字段,也统一写 0,避免旧页面或缓存把门槛重新保存回来。 RequiredRechargeCoinAmount: 0, SortOrder: level.SortOrder, Benefits: benefits, }) } resp, err := h.wallet.UpdateAdminVipLevels(c.Request.Context(), &walletv1.UpdateAdminVipLevelsRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), Levels: input, OperatorUserId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "update-vip-config", "vip_levels", appctx.FromContext(c.Request.Context()), "success", strconv.Itoa(len(resp.GetLevels()))) response.OK(c, configDTO{ Levels: vipLevelsFromProto(resp.GetLevels()), ProgramConfig: vipProgramConfigFromProto(resp.GetProgramConfig()), ServerTimeMS: resp.GetServerTimeMs(), }) } func vipLevelsFromProto(items []*walletv1.VipLevel) []vipLevelDTO { levels := make([]vipLevelDTO, 0, len(items)) for _, item := range items { if item == nil { continue } levels = append(levels, vipLevelDTO{ Level: item.GetLevel(), Name: item.GetName(), Status: item.GetStatus(), PriceCoin: item.GetPriceCoin(), DurationMS: item.GetDurationMs(), RewardResourceGroupID: item.GetRewardResourceGroupId(), SortOrder: item.GetSortOrder(), // 响应仍保留兼容字段,但后台配置页不再展示或依赖累充资格,统一返回无门槛。 RechargeGateRequired: false, RequiredRechargeCoinAmount: 0, UserRechargeCoinAmount: 0, PurchaseLockedReason: item.GetPurchaseLockedReason(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), Benefits: vipBenefitsFromProto(item.GetBenefits()), ConfigVersion: item.GetConfigVersion(), }) } return levels } func vipProgramConfigFromProto(item *walletv1.VipProgramConfig) vipProgramConfigDTO { if item == nil { return vipProgramConfigDTO{} } return vipProgramConfigDTO{ AppCode: item.GetAppCode(), ProgramType: item.GetProgramType(), LevelCount: item.GetLevelCount(), SameLevelExpiryPolicy: item.GetSameLevelExpiryPolicy(), UpgradeExpiryPolicy: item.GetUpgradeExpiryPolicy(), DowngradePurchasePolicy: item.GetDowngradePurchasePolicy(), BenefitInheritancePolicy: item.GetBenefitInheritancePolicy(), GrantMode: item.GetGrantMode(), TrialCardEnabled: item.GetTrialCardEnabled(), Status: item.GetStatus(), ConfigVersion: item.GetConfigVersion(), UpdatedByAdminID: item.GetUpdatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func (item vipProgramConfigDTO) toProto() *walletv1.VipProgramConfig { return &walletv1.VipProgramConfig{ ProgramType: strings.TrimSpace(item.ProgramType), LevelCount: item.LevelCount, SameLevelExpiryPolicy: strings.TrimSpace(item.SameLevelExpiryPolicy), UpgradeExpiryPolicy: strings.TrimSpace(item.UpgradeExpiryPolicy), DowngradePurchasePolicy: strings.TrimSpace(item.DowngradePurchasePolicy), BenefitInheritancePolicy: strings.TrimSpace(item.BenefitInheritancePolicy), GrantMode: strings.TrimSpace(item.GrantMode), TrialCardEnabled: item.TrialCardEnabled, Status: strings.TrimSpace(item.Status), } } func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitDTO { benefits := make([]vipBenefitDTO, 0, len(items)) for _, item := range items { if item == nil { continue } benefits = append(benefits, vipBenefitDTO{ BenefitCode: item.GetBenefitCode(), Name: item.GetName(), BenefitType: item.GetBenefitType(), UnlockLevel: item.GetUnlockLevel(), Status: item.GetStatus(), TrialEnabled: item.GetTrialEnabled(), ResourceID: item.GetResourceId(), ResourceType: item.GetResourceType(), ExecutionScope: item.GetExecutionScope(), AutoEquip: item.GetAutoEquip(), SortOrder: item.GetSortOrder(), MetadataJSON: item.GetMetadataJson(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), }) } return benefits } func (item vipBenefitDTO) toProto() *walletv1.VipBenefit { return &walletv1.VipBenefit{ BenefitCode: strings.TrimSpace(item.BenefitCode), Name: strings.TrimSpace(item.Name), BenefitType: strings.TrimSpace(item.BenefitType), UnlockLevel: item.UnlockLevel, Status: strings.TrimSpace(item.Status), TrialEnabled: item.TrialEnabled, ResourceId: item.ResourceID, ResourceType: strings.TrimSpace(item.ResourceType), ExecutionScope: strings.TrimSpace(item.ExecutionScope), AutoEquip: item.AutoEquip, SortOrder: item.SortOrder, MetadataJson: strings.TrimSpace(item.MetadataJSON), } } func grantVipFromProto(resp *walletv1.GrantVipResponse) grantVipDTO { if resp == nil { return grantVipDTO{} } return grantVipDTO{ TransactionID: resp.GetTransactionId(), VIP: userVipFromProto(resp.GetVip()), RewardItems: vipRewardItemsFromProto(resp.GetRewardItems()), ServerTimeMS: resp.GetServerTimeMs(), } } func userVipFromProto(item *walletv1.UserVip) userVipDTO { if item == nil { return userVipDTO{} } return userVipDTO{ UserID: strconv.FormatInt(item.GetUserId(), 10), Level: item.GetLevel(), Name: item.GetName(), Active: item.GetActive(), StartedAtMS: item.GetStartedAtMs(), ExpiresAtMS: item.GetExpiresAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), ProgramType: item.GetProgramType(), ConfigVersion: item.GetConfigVersion(), } } func grantVipTrialCardFromProto(resp *walletv1.GrantVipTrialCardResponse) grantVipTrialCardDTO { if resp == nil { return grantVipTrialCardDTO{} } return grantVipTrialCardDTO{ TrialCard: vipTrialCardFromProto(resp.GetTrialCard()), State: vipStateFromProto(resp.GetState()), ServerTimeMS: resp.GetServerTimeMs(), } } func vipTrialCardFromProto(item *walletv1.VipTrialCard) vipTrialCardDTO { if item == nil { return vipTrialCardDTO{} } return vipTrialCardDTO{ TrialCardID: item.GetTrialCardId(), EntitlementID: item.GetEntitlementId(), ResourceID: item.GetResourceId(), UserID: strconv.FormatInt(item.GetUserId(), 10), Level: item.GetLevel(), Name: item.GetName(), Status: item.GetStatus(), Equipped: item.GetEquipped(), DurationMS: item.GetDurationMs(), EffectiveAtMS: item.GetEffectiveAtMs(), ExpiresAtMS: item.GetExpiresAtMs(), RemainingDurationMS: item.GetRemainingDurationMs(), GrantSource: item.GetGrantSource(), SourceGrantID: item.GetSourceGrantId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func vipStateFromProto(item *walletv1.VipState) vipStateDTO { if item == nil { return vipStateDTO{EffectiveBenefits: []vipBenefitDTO{}} } return vipStateDTO{ PaidVIP: userVipFromProto(item.GetPaidVip()), EquippedTrialCard: vipTrialCardFromProto(item.GetEquippedTrialCard()), EffectiveVIP: userVipFromProto(item.GetEffectiveVip()), EffectiveSource: item.GetEffectiveSource(), EffectiveBenefits: vipBenefitsFromProto(item.GetEffectiveBenefits()), EvaluatedAtMS: item.GetEvaluatedAtMs(), ProgramConfig: vipProgramConfigFromProto(item.GetProgramConfig()), } } func vipRewardItemsFromProto(items []*walletv1.VipRewardItem) []vipRewardItemDTO { rewards := make([]vipRewardItemDTO, 0, len(items)) for _, item := range items { if item == nil { continue } rewards = append(rewards, vipRewardItemDTO{ ResourceID: strconv.FormatInt(item.GetResourceId(), 10), ResourceCode: item.GetResourceCode(), ResourceType: item.GetResourceType(), Name: item.GetName(), Quantity: item.GetQuantity(), ExpiresAtMS: item.GetExpiresAtMs(), }) } return rewards } func parseFlexibleUserID(value any) (int64, bool) { switch typed := value.(type) { case float64: userID := int64(typed) return userID, userID > 0 && float64(userID) == typed case string: userID, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64) return userID, err == nil && userID > 0 default: return 0, false } }