2026-04-29 20:14:53 +08:00

760 lines
23 KiB
Go

package vip
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"chatapp3-golang/internal/integration"
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// GetHome returns VIP configuration plus the current user's purchasable state.
func (s *Service) GetHome(ctx context.Context, user AuthUser) (*HomeResponse, error) {
sysOrigin, err := normalizeUser(user)
if err != nil {
return nil, err
}
configs, err := s.loadConfigMap(ctx, sysOrigin)
if err != nil {
return nil, err
}
now := time.Now()
state, err := s.loadUserState(ctx, sysOrigin, user.UserID, now)
if err != nil {
return nil, err
}
activeState := activeStateOrNil(state, now)
resourceIDs := resourceIDsFromConfigMap(configs)
resourceIDs = appendResourceIDsFromState(resourceIDs, state)
resources, err := s.loadResourceLookup(ctx, resourceIDs)
if err != nil {
return nil, err
}
levels := make([]VipLevelHomePayload, 0, levelCount)
for level := 1; level <= levelCount; level++ {
payload := defaultLevelPayload(level)
config, exists := configs[level]
if exists {
payload = payloadFromConfigWithResources(config, resources)
}
item := VipLevelHomePayload{VipLevelPayload: payload}
if exists && config.Enabled {
plan, planErr := buildPurchasePlan(now, activeState, config, configs)
item.CanPurchase = planErr == nil
if planErr == nil {
item.PayableGold = plan.PayableGold
}
}
levels = append(levels, item)
}
statePayload := statePayloadFromModelWithResources(state, now, resources)
statePayload.UserID = user.UserID
statePayload.SysOrigin = sysOrigin
return &HomeResponse{
UserID: user.UserID,
SysOrigin: sysOrigin,
Configured: len(configs) > 0,
State: statePayload,
Levels: levels,
}, nil
}
// GetStatus returns only the current user's VIP state.
func (s *Service) GetStatus(ctx context.Context, user AuthUser) (*UserVipStatePayload, error) {
sysOrigin, err := normalizeUser(user)
if err != nil {
return nil, err
}
now := time.Now()
state, err := s.loadUserState(ctx, sysOrigin, user.UserID, now)
if err != nil {
return nil, err
}
resources, err := s.loadResourceLookup(ctx, appendResourceIDsFromState(nil, state))
if err != nil {
return nil, err
}
payload := statePayloadFromModelWithResources(state, now, resources)
payload.UserID = user.UserID
payload.SysOrigin = sysOrigin
return &payload, nil
}
// PreviewPurchase calculates the cost and resulting period before purchase.
func (s *Service) PreviewPurchase(ctx context.Context, user AuthUser, req PurchaseRequest) (*PurchasePreviewResponse, error) {
sysOrigin, err := normalizeUser(user)
if err != nil {
return nil, err
}
targetLevel := parseTargetLevel(req)
if err := validateLevel(targetLevel); err != nil {
return nil, err
}
configs, err := s.loadConfigMap(ctx, sysOrigin)
if err != nil {
return nil, err
}
targetConfig, err := resolveTargetConfig(configs, targetLevel)
if err != nil {
return nil, err
}
now := time.Now()
state, err := s.loadUserState(ctx, sysOrigin, user.UserID, now)
if err != nil {
return nil, err
}
plan, err := buildPurchasePlan(now, activeStateOrNil(state, now), targetConfig, configs)
if err != nil {
return nil, err
}
resources, err := s.loadResourceLookup(ctx, appendResourceIDsFromConfig(nil, plan.TargetConfig))
if err != nil {
return nil, err
}
return &PurchasePreviewResponse{
UserID: user.UserID,
SysOrigin: sysOrigin,
OrderType: plan.OrderType,
CurrentLevel: plan.CurrentLevel,
TargetLevel: plan.TargetLevel,
DurationDays: plan.DurationDays,
OriginalPriceGold: plan.OriginalPriceGold,
PayableGold: plan.PayableGold,
StartAt: formatTime(plan.StartAt),
ExpireAt: formatTime(plan.ExpireAt),
TargetConfig: payloadFromConfigWithResources(plan.TargetConfig, resources),
}, nil
}
// Purchase buys, renews, or upgrades a 30-day VIP package.
func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseRequest) (*PurchaseResponse, error) {
sysOrigin, err := normalizeUser(user)
if err != nil {
return nil, err
}
targetLevel := parseTargetLevel(req)
if err := validateLevel(targetLevel); err != nil {
return nil, err
}
idempotentKey := normalizeIdempotentKey(req.BizIdempotentKey)
if idempotentKey != "" {
eventID := buildVIPEventID(sysOrigin, user.UserID, idempotentKey)
existing, err := s.loadOrderByEventID(ctx, eventID)
if err != nil {
return nil, err
}
if existing != nil {
if existing.Status != orderStatusSuccess {
return nil, NewAppError(http.StatusConflict, "vip_order_in_progress", "vip order is already submitted")
}
now := time.Now()
state, stateErr := s.loadUserState(ctx, sysOrigin, user.UserID, now)
if stateErr != nil {
return nil, stateErr
}
if err := s.ensureStateResourcesGranted(ctx, user.UserID, state, now); err != nil {
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
}
resources, resourceErr := s.loadResourceLookup(ctx, appendResourceIDsFromState(nil, state))
if resourceErr != nil {
return nil, resourceErr
}
statePayload := statePayloadFromModelWithResources(state, now, resources)
statePayload.UserID = user.UserID
statePayload.SysOrigin = sysOrigin
return &PurchaseResponse{
Success: true,
Order: orderPayloadFromModel(*existing),
State: statePayload,
}, nil
}
}
configs, err := s.loadConfigMap(ctx, sysOrigin)
if err != nil {
return nil, err
}
targetConfig, err := resolveTargetConfig(configs, targetLevel)
if err != nil {
return nil, err
}
now := time.Now()
orderID, err := utils.NextID()
if err != nil {
return nil, err
}
eventKey := idempotentKey
if eventKey == "" {
eventKey = strconv.FormatInt(orderID, 10)
}
tx := s.db.WithContext(ctx).Begin()
if tx.Error != nil {
return nil, tx.Error
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback().Error
}
}()
state, err := s.loadOrCreateLockedState(tx, sysOrigin, user.UserID, now)
if err != nil {
return nil, err
}
activeState := activeStateOrNil(state, now)
plan, err := buildPurchasePlan(now, activeState, targetConfig, configs)
if err != nil {
return nil, err
}
order := model.VipOrderRecord{
ID: orderID,
EventID: buildVIPEventID(sysOrigin, user.UserID, eventKey),
SysOrigin: sysOrigin,
UserID: user.UserID,
OrderType: plan.OrderType,
FromLevel: plan.CurrentLevel,
ToLevel: plan.TargetLevel,
DurationDays: plan.DurationDays,
OriginalPriceGold: plan.OriginalPriceGold,
PaidGold: plan.PayableGold,
Status: orderStatusInit,
StartAt: plan.StartAt,
ExpireAt: plan.ExpireAt,
CreateTime: now,
UpdateTime: now,
}
if err := tx.Create(&order).Error; err != nil {
return nil, err
}
if plan.PayableGold > 0 {
if s.java == nil {
err := NewAppError(http.StatusServiceUnavailable, "wallet_unavailable", "wallet gateway is unavailable")
if saveErr := s.markOrderFailed(tx, &order, err.Error()); saveErr != nil {
return nil, saveErr
}
if commitErr := tx.Commit().Error; commitErr != nil {
return nil, commitErr
}
committed = true
return nil, err
}
if err := s.java.ChangeGoldBalance(ctx, buildWalletCommand(user.UserID, sysOrigin, order)); err != nil {
appErr := mapWalletPurchaseError(err)
if saveErr := s.markOrderFailed(tx, &order, err.Error()); saveErr != nil {
return nil, saveErr
}
if commitErr := tx.Commit().Error; commitErr != nil {
return nil, commitErr
}
committed = true
return nil, appErr
}
}
applyPlanToState(state, plan, now)
order.Status = orderStatusSuccess
order.ErrorMessage = ""
order.UpdateTime = now
if err := tx.Save(state).Error; err != nil {
return nil, err
}
if err := tx.Save(&order).Error; err != nil {
return nil, err
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
committed = true
if err := s.ensurePlanResourcesGranted(ctx, user.UserID, plan, now); err != nil {
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
}
resources, err := s.loadResourceLookup(ctx, appendResourceIDsFromState(nil, state))
if err != nil {
return nil, err
}
payload := statePayloadFromModelWithResources(state, now, resources)
payload.UserID = user.UserID
payload.SysOrigin = sysOrigin
return &PurchaseResponse{
Success: true,
Order: orderPayloadFromModel(order),
State: payload,
}, nil
}
func normalizeUser(user AuthUser) (string, error) {
if user.UserID <= 0 {
return "", NewAppError(http.StatusUnauthorized, "invalid_user", "user is required")
}
sysOrigin := normalizeSysOrigin(user.SysOrigin)
if sysOrigin == "" {
return "", NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
}
return sysOrigin, nil
}
func resolveTargetConfig(configs map[int]model.VipLevelConfig, targetLevel int) (model.VipLevelConfig, error) {
config, exists := configs[targetLevel]
if !exists {
return model.VipLevelConfig{}, NewAppError(http.StatusBadRequest, "vip_not_configured", "target vip level is not configured")
}
if !config.Enabled {
return model.VipLevelConfig{}, NewAppError(http.StatusBadRequest, "vip_level_disabled", "target vip level is disabled")
}
if config.PriceGold < 0 {
return model.VipLevelConfig{}, NewAppError(http.StatusBadRequest, "invalid_price", "target vip level price is invalid")
}
if config.DurationDays <= 0 {
config.DurationDays = vipDurationDay
}
return config, nil
}
func buildPurchasePlan(
now time.Time,
currentState *model.UserVipState,
targetConfig model.VipLevelConfig,
configs map[int]model.VipLevelConfig,
) (purchasePlan, error) {
targetLevel := targetConfig.Level
if err := validateLevel(targetLevel); err != nil {
return purchasePlan{}, err
}
if !targetConfig.Enabled {
return purchasePlan{}, NewAppError(http.StatusBadRequest, "vip_level_disabled", "target vip level is disabled")
}
durationDays := targetConfig.DurationDays
if durationDays <= 0 {
durationDays = vipDurationDay
}
currentLevel := 0
startAt := now
expireAt := now.AddDate(0, 0, durationDays)
orderType := orderTypePurchase
payableGold := targetConfig.PriceGold
if isActiveState(currentState, now) {
currentLevel = currentState.Level
if targetLevel < currentLevel {
return purchasePlan{}, NewAppError(http.StatusBadRequest, "vip_downgrade_not_allowed", "vip downgrade is not allowed")
}
startAt = currentState.StartAt
if startAt.IsZero() {
startAt = now
}
if targetLevel == currentLevel {
orderType = orderTypeRenew
expireAt = currentState.ExpireAt.AddDate(0, 0, durationDays)
payableGold = targetConfig.PriceGold
} else {
orderType = orderTypeUpgrade
expireAt = currentState.ExpireAt
currentPrice := int64(0)
if currentConfig, exists := configs[currentLevel]; exists {
currentPrice = currentConfig.PriceGold
}
payableGold = targetConfig.PriceGold - currentPrice
if payableGold < 0 {
payableGold = 0
}
}
}
return purchasePlan{
OrderType: orderType,
CurrentLevel: currentLevel,
TargetLevel: targetLevel,
DurationDays: durationDays,
OriginalPriceGold: targetConfig.PriceGold,
PayableGold: payableGold,
StartAt: startAt,
ExpireAt: expireAt,
TargetConfig: targetConfig,
}, nil
}
func (s *Service) loadUserState(ctx context.Context, sysOrigin string, userID int64, now time.Time) (*model.UserVipState, error) {
var row model.UserVipState
err := s.db.WithContext(ctx).
Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).
First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
if row.Status == statusActive && !row.ExpireAt.After(now) {
expireState(&row, now)
if err := s.db.WithContext(ctx).Save(&row).Error; err != nil {
return nil, err
}
}
return &row, nil
}
func (s *Service) loadOrCreateLockedState(tx *gorm.DB, sysOrigin string, userID int64, now time.Time) (*model.UserVipState, error) {
var row model.UserVipState
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).
First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
nextID, idErr := utils.NextID()
if idErr != nil {
return nil, idErr
}
row = model.UserVipState{
ID: nextID,
SysOrigin: sysOrigin,
UserID: userID,
Status: statusExpired,
StartAt: now,
ExpireAt: now,
CreateTime: now,
UpdateTime: now,
}
if err := tx.Create(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
if err != nil {
return nil, err
}
if row.Status == statusActive && !row.ExpireAt.After(now) {
expireState(&row, now)
if err := tx.Save(&row).Error; err != nil {
return nil, err
}
}
return &row, nil
}
func activeStateOrNil(state *model.UserVipState, now time.Time) *model.UserVipState {
if isActiveState(state, now) {
return state
}
return nil
}
func applyPlanToState(state *model.UserVipState, plan purchasePlan, now time.Time) {
state.Level = plan.TargetLevel
state.LevelCode = levelCode(plan.TargetLevel)
state.DisplayName = strings.TrimSpace(plan.TargetConfig.DisplayName)
if state.DisplayName == "" {
state.DisplayName = levelDisplayName(plan.TargetLevel)
}
state.Status = statusActive
state.StartAt = plan.StartAt
state.ExpireAt = plan.ExpireAt
state.BadgeResourceID = plan.TargetConfig.BadgeResourceID
state.BadgeName = ""
state.BadgeURL = ""
state.AvatarFrameResourceID = plan.TargetConfig.AvatarFrameResourceID
state.AvatarFrameName = ""
state.AvatarFrameURL = ""
state.EntryEffectResourceID = plan.TargetConfig.EntryEffectResourceID
state.EntryEffectName = ""
state.EntryEffectURL = ""
state.ChatBubbleResourceID = plan.TargetConfig.ChatBubbleResourceID
state.ChatBubbleName = ""
state.ChatBubbleURL = ""
state.FloatPictureResourceID = plan.TargetConfig.FloatPictureResourceID
state.FloatPictureName = ""
state.FloatPictureURL = ""
state.UpdateTime = now
if state.CreateTime.IsZero() {
state.CreateTime = now
}
}
func expireState(state *model.UserVipState, now time.Time) {
state.Level = 0
state.LevelCode = ""
state.DisplayName = ""
state.Status = statusExpired
state.BadgeResourceID = 0
state.BadgeName = ""
state.BadgeURL = ""
state.AvatarFrameResourceID = 0
state.AvatarFrameName = ""
state.AvatarFrameURL = ""
state.EntryEffectResourceID = 0
state.EntryEffectName = ""
state.EntryEffectURL = ""
state.ChatBubbleResourceID = 0
state.ChatBubbleName = ""
state.ChatBubbleURL = ""
state.FloatPictureResourceID = 0
state.FloatPictureName = ""
state.FloatPictureURL = ""
state.UpdateTime = now
}
func (s *Service) ensurePlanResourcesGranted(ctx context.Context, userID int64, plan purchasePlan, now time.Time) error {
return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromConfig(plan.TargetConfig), plan.ExpireAt, plan.DurationDays, now)
}
func (s *Service) ensureStateResourcesGranted(ctx context.Context, userID int64, state *model.UserVipState, now time.Time) error {
if !isActiveState(state, now) {
return nil
}
return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromState(state), state.ExpireAt, vipGrantDaysUntil(now, state.ExpireAt), now)
}
func (s *Service) ensureVIPResourcesGranted(ctx context.Context, userID int64, resources []vipGrantResource, expireAt time.Time, days int, now time.Time) error {
if len(resources) == 0 {
return nil
}
if s.java == nil {
return fmt.Errorf("java gateway is unavailable")
}
if days <= 0 {
days = 1
}
for _, resource := range resources {
if resource.ResourceID <= 0 {
continue
}
exists, err := s.vipResourceExists(ctx, userID, resource, expireAt)
if err != nil {
return err
}
if !exists {
if err := s.grantVIPResource(ctx, userID, resource, days); err != nil {
return err
}
}
if !resource.Badge {
if err := s.java.SwitchUseProps(ctx, userID, resource.ResourceID); err != nil {
return fmt.Errorf("switch vip resource %s:%d: %w", resource.PropsType, resource.ResourceID, err)
}
}
if err := s.alignVIPResourceExpireAt(ctx, userID, resource, expireAt, now); err != nil {
return err
}
}
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
return fmt.Errorf("remove vip user profile cache: %w", err)
}
return nil
}
func (s *Service) grantVIPResource(ctx context.Context, userID int64, resource vipGrantResource, days int) error {
if resource.Badge {
if err := s.java.ActivateTemporaryBadge(ctx, userID, resource.ResourceID, days); err != nil {
return fmt.Errorf("grant vip badge %d: %w", resource.ResourceID, err)
}
return nil
}
req := integration.GivePropsBackpackRequest{
AcceptUserID: userID,
PropsID: resource.ResourceID,
Type: resource.PropsType,
Origin: walletOrigin,
OriginDesc: walletOriginDesc,
Days: days,
UseProps: true,
AllowGive: false,
}
if err := s.java.GivePropsBackpack(ctx, req); err != nil {
return fmt.Errorf("grant vip resource %s:%d: %w", resource.PropsType, resource.ResourceID, err)
}
return nil
}
func (s *Service) vipResourceExists(ctx context.Context, userID int64, resource vipGrantResource, expireAt time.Time) (bool, error) {
var count int64
threshold := expireAt.Add(-time.Second)
query := s.db.WithContext(ctx)
if resource.Badge {
query = query.Table("user_badge_backpack").
Where("user_id = ? AND badge_id = ? AND expire_time >= ?", userID, resource.ResourceID, threshold)
} else {
query = query.Table("user_props_backpack").
Where("user_id = ? AND props_id = ? AND type = ? AND expire_time >= ?", userID, resource.ResourceID, resource.PropsType, threshold)
}
if err := query.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (s *Service) alignVIPResourceExpireAt(ctx context.Context, userID int64, resource vipGrantResource, expireAt time.Time, now time.Time) error {
var result *gorm.DB
if resource.Badge {
result = s.db.WithContext(ctx).Exec(
"UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id = ?",
"TEMPORARY", expireAt, true, now, userID, resource.ResourceID,
)
} else {
result = s.db.WithContext(ctx).Exec(
"UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id = ? AND type = ?",
expireAt, true, false, now, userID, resource.ResourceID, resource.PropsType,
)
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
exists, err := s.vipResourceExists(ctx, userID, resource, expireAt)
if err != nil {
return err
}
if !exists {
if resource.Badge {
return fmt.Errorf("vip badge backpack row not found after grant: %d", resource.ResourceID)
}
return fmt.Errorf("vip props backpack row not found after grant: %s:%d", resource.PropsType, resource.ResourceID)
}
}
return nil
}
func vipGrantResourcesFromConfig(config model.VipLevelConfig) []vipGrantResource {
return compactVIPGrantResources([]vipGrantResource{
{ResourceID: config.BadgeResourceID, Badge: true},
{ResourceID: config.AvatarFrameResourceID, PropsType: vipPropsTypeAvatarFrame},
{ResourceID: config.EntryEffectResourceID, PropsType: vipPropsTypeRide},
{ResourceID: config.ChatBubbleResourceID, PropsType: vipPropsTypeChatBubble},
{ResourceID: config.FloatPictureResourceID, PropsType: vipPropsTypeFloatPicture},
})
}
func vipGrantResourcesFromState(state *model.UserVipState) []vipGrantResource {
if state == nil {
return nil
}
return compactVIPGrantResources([]vipGrantResource{
{ResourceID: state.BadgeResourceID, Badge: true},
{ResourceID: state.AvatarFrameResourceID, PropsType: vipPropsTypeAvatarFrame},
{ResourceID: state.EntryEffectResourceID, PropsType: vipPropsTypeRide},
{ResourceID: state.ChatBubbleResourceID, PropsType: vipPropsTypeChatBubble},
{ResourceID: state.FloatPictureResourceID, PropsType: vipPropsTypeFloatPicture},
})
}
func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource {
compacted := make([]vipGrantResource, 0, len(resources))
for _, resource := range resources {
if resource.ResourceID > 0 {
compacted = append(compacted, resource)
}
}
return compacted
}
func vipGrantDaysUntil(now time.Time, expireAt time.Time) int {
if !expireAt.After(now) {
return 1
}
duration := expireAt.Sub(now)
days := int(duration / (24 * time.Hour))
if duration%(24*time.Hour) != 0 {
days++
}
if days < 1 {
return 1
}
return days
}
func mapWalletPurchaseError(err error) *AppError {
message := err.Error()
normalized := strings.ToLower(message)
if strings.Contains(normalized, "insufficient_balance") ||
strings.Contains(normalized, "balance not made") ||
strings.Contains(normalized, `"errorcode":5000`) {
return NewAppError(http.StatusNotAcceptable, "vip_insufficient_balance", "insufficient balance")
}
return NewAppError(http.StatusBadGateway, "vip_wallet_deduct_failed", message)
}
func buildWalletCommand(userID int64, sysOrigin string, order model.VipOrderRecord) integration.GoldReceiptCommand {
return integration.GoldReceiptCommand{
ReceiptType: walletReceiptExpenditure,
UserID: userID,
SysOrigin: sysOrigin,
EventID: order.EventID,
Remark: fmt.Sprintf("VIP %s from level %d to %d, %d days", order.OrderType, order.FromLevel, order.ToLevel, order.DurationDays),
Amount: integration.NewPennyAmountPayloadFromDollar(order.PaidGold),
CloseDelayAsset: false,
OpUserType: "APP",
CustomizeOrigin: walletOrigin,
CustomizeOriginDesc: walletOriginDesc,
}
}
func (s *Service) markOrderFailed(tx *gorm.DB, order *model.VipOrderRecord, message string) error {
order.Status = orderStatusFailed
order.ErrorMessage = trimErrorMessage(message)
order.UpdateTime = time.Now()
return tx.Save(order).Error
}
func (s *Service) loadOrderByEventID(ctx context.Context, eventID string) (*model.VipOrderRecord, error) {
var row model.VipOrderRecord
err := s.db.WithContext(ctx).Where("event_id = ?", eventID).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &row, nil
}
func buildVIPEventID(sysOrigin string, userID int64, key string) string {
sysPart := normalizeSysOrigin(sysOrigin)
if len(sysPart) > 8 {
sysPart = sysPart[:8]
}
sum := sha1.Sum([]byte(fmt.Sprintf("%s:%d:%s", sysOrigin, userID, key)))
return fmt.Sprintf("VIP:%s:%d:%s", sysPart, userID, hex.EncodeToString(sum[:])[:16])
}
func normalizeIdempotentKey(key string) string {
key = strings.TrimSpace(key)
if len(key) > 64 {
key = key[:64]
}
return key
}
func trimErrorMessage(message string) string {
message = strings.TrimSpace(message)
if len(message) > 512 {
return message[:512]
}
return message
}