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 grantVipRequest struct { CommandID string `json:"commandId"` TargetUserID any `json:"targetUserId"` Level int32 `json:"level"` Reason string `json:"reason"` } type configDTO struct { Levels []vipLevelDTO `json:"levels"` ServerTimeMS int64 `json:"serverTimeMs"` } type grantVipDTO struct { TransactionID string `json:"transactionId"` VIP userVipDTO `json:"vip"` RewardItems []vipRewardItemDTO `json:"rewardItems"` 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"` } 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"` } 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) 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()), 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) 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 { 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, }) } 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()), 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(), }) } return levels } 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(), } } 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 } }