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