2026-07-04 02:17:43 +08:00

250 lines
8.1 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 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
}
}