2026-06-24 15:39:42 +08:00

958 lines
29 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 managercenter
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"chatapp3-golang/internal/integration"
"chatapp3-golang/internal/model"
"gorm.io/gorm"
)
type teamManagerInfoRow struct {
ID int64 `gorm:"column:id;primaryKey"`
UserID int64 `gorm:"column:user_id"`
Origin string `gorm:"column:sys_origin"`
Contact string `gorm:"column:contact"`
RegionID string `gorm:"column:region_id"`
CanAddBDLeader *bool `gorm:"column:can_add_bd_leader"`
CanGiftProps *bool `gorm:"column:can_gift_props"`
CanBanUser *bool `gorm:"column:can_ban_user"`
CanUnbanUser *bool `gorm:"column:can_unban_user"`
CanChangeCountry *bool `gorm:"column:can_change_country"`
CreateUser int64 `gorm:"column:create_user"`
UpdateUser int64 `gorm:"column:update_user"`
}
func (teamManagerInfoRow) TableName() string {
return "team_manager_base_info"
}
type userBaseInfoRow struct {
ID int64 `gorm:"column:id;primaryKey"`
Account string `gorm:"column:account"`
UserAvatar string `gorm:"column:user_avatar"`
UserNickname string `gorm:"column:user_nickname"`
CountryID int64 `gorm:"column:country_id"`
CountryCode string `gorm:"column:country_code"`
CountryName string `gorm:"column:country_name"`
OriginSys string `gorm:"column:origin_sys"`
AccountStatus string `gorm:"column:account_status"`
IsDel bool `gorm:"column:is_del"`
UpdateTime time.Time `gorm:"column:update_time"`
UpdateUser int64 `gorm:"column:update_user"`
}
func (userBaseInfoRow) TableName() string {
return "user_base_info"
}
type sysCountryCodeRow struct {
ID int64 `gorm:"column:id;primaryKey"`
AlphaTwo string `gorm:"column:alpha_two"`
CountryName string `gorm:"column:country_name"`
NationalFlag string `gorm:"column:national_flag"`
PhonePrefix int `gorm:"column:phone_prefix"`
Open bool `gorm:"column:is_open"`
Sort int `gorm:"column:sort"`
}
func (sysCountryCodeRow) TableName() string {
return "sys_country_code"
}
type propsSourceRecordRow struct {
ID int64 `gorm:"column:id;primaryKey"`
Type string `gorm:"column:type"`
Code string `gorm:"column:code"`
Name string `gorm:"column:name"`
Cover string `gorm:"column:cover"`
SourceURL string `gorm:"column:source_url"`
Expand string `gorm:"column:expand"`
AdminFree bool `gorm:"column:admin_free"`
IsDel bool `gorm:"column:is_del"`
}
func (propsSourceRecordRow) TableName() string {
return "props_source_record"
}
type propsNobleVIPAbilityRow struct {
ID int64 `gorm:"column:id;primaryKey"`
VIPLevel int `gorm:"column:vip_level"`
AvatarFrameID int64 `gorm:"column:avatar_frame_id"`
CarID int64 `gorm:"column:car_id"`
ChatBubbleID int64 `gorm:"column:chat_bubble_id"`
DataCardID int64 `gorm:"column:data_card_id"`
}
func (propsNobleVIPAbilityRow) TableName() string {
return "props_noble_vip_ability"
}
func (s *Service) GetProfile(ctx context.Context, user AuthUser) (*ProfileResponse, error) {
manager, err := s.requireManagerInfo(ctx, user)
if err != nil {
return nil, err
}
view, err := s.loadUserViewByID(ctx, user.UserID)
if err != nil {
return nil, err
}
return &ProfileResponse{Manager: view, Features: managerFeaturesFromRow(manager)}, nil
}
func (s *Service) SearchUser(ctx context.Context, user AuthUser, account string) (*UserSearchResponse, error) {
if err := s.ensureManager(ctx, user); err != nil {
return nil, err
}
account = strings.TrimSpace(account)
if account == "" {
return nil, badRequest("account_required", "account is required")
}
view, err := s.findUserView(ctx, account)
if err != nil {
return nil, err
}
return &UserSearchResponse{User: view}, nil
}
// ListCountries 给经理中心返回当前开放国家H5 只展示可绑定目标,避免经理手输无效国家码。
func (s *Service) ListCountries(ctx context.Context, user AuthUser) (*CountryListResponse, error) {
if err := s.ensureManager(ctx, user); err != nil {
return nil, err
}
if s.db == nil {
return nil, serviceUnavailable("database_unavailable", "database is unavailable")
}
var rows []sysCountryCodeRow
if err := s.db.WithContext(ctx).
Table("sys_country_code").
Select("id, alpha_two, country_name, national_flag, phone_prefix, is_open, sort").
Where("is_open = ?", true).
Order("sort ASC, country_name ASC, id ASC").
Find(&rows).Error; err != nil {
return nil, serverError("country_query_failed", err.Error())
}
records := make([]CountryView, 0, len(rows))
for _, row := range rows {
if view, ok := countryRowToView(row); ok {
records = append(records, view)
}
}
return &CountryListResponse{Records: records}, nil
}
func (s *Service) ListProps(ctx context.Context, user AuthUser, propsType string) (*PropsListResponse, error) {
if err := s.ensureManager(ctx, user); err != nil {
return nil, err
}
if s.db == nil {
return nil, serviceUnavailable("database_unavailable", "database is unavailable")
}
propsType = normalizePropsType(propsType)
if propsType == propsTypeNobleVIP {
return s.listVIPProps(ctx, user)
}
query := s.db.WithContext(ctx).
Table("props_source_record").
Select("id, type, code, name, cover, source_url, expand, admin_free, is_del").
Where("is_del = ? AND admin_free = ?", false, true)
if propsType != "" {
query = query.Where("type = ?", propsType)
}
var rows []propsSourceRecordRow
if err := query.Order("type ASC, id ASC").Find(&rows).Error; err != nil {
return nil, serverError("props_query_failed", err.Error())
}
vipLevels, err := s.loadVIPLevels(ctx, rows)
if err != nil {
return nil, err
}
records := make([]PropsView, 0, len(rows))
for _, row := range rows {
view, ok := propsRowToView(row, vipLevels)
if !ok {
continue
}
records = append(records, view)
}
return &PropsListResponse{
Type: propsType,
Records: records,
}, nil
}
func (s *Service) SendProps(ctx context.Context, user AuthUser, req SendPropsRequest) (*SendPropsResponse, error) {
if _, err := s.requireManagerFeature(ctx, user, managerFeatureGiftProps); err != nil {
return nil, err
}
if req.AcceptUserID.Int64() <= 0 {
return nil, badRequest("accept_user_required", "acceptUserId is required")
}
if req.PropsID.Int64() <= 0 {
return nil, badRequest("props_required", "propsId is required")
}
if s.java == nil {
return nil, serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
}
target, err := s.loadUserRowByID(ctx, req.AcceptUserID.Int64())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, notFound("user_not_found", "user not found")
}
return nil, serverError("user_query_failed", err.Error())
}
prop, err := s.loadGiftableProps(ctx, req.PropsID.Int64())
if err != nil {
if isAppErrorCode(err, "prop_not_giftable") {
if resp, vipErr := s.sendVIPGift(ctx, user, target, req.PropsID.Int64()); vipErr != nil {
return nil, vipErr
} else if resp != nil {
return resp, nil
}
}
return nil, err
}
days := propsGiftDays(prop.Type)
if prop.Type == propsTypeBadge {
if err := s.java.ActivateTemporaryBadge(ctx, req.AcceptUserID.Int64(), prop.ID, days); err != nil {
return nil, serverError("send_badge_failed", err.Error())
}
return &SendPropsResponse{
Success: true,
AcceptUserID: req.AcceptUserID,
PropsID: req.PropsID,
Type: prop.Type,
Days: days,
}, nil
}
grants := []propsGrant{{
propsID: prop.ID,
typ: prop.Type,
days: days,
}}
if prop.Type == propsTypeNobleVIP {
if _, err := s.loadGiftableNobleVIPAbility(ctx, prop.ID); err != nil {
return nil, err
}
ability, err := s.java.GetNobleVIPAbility(ctx, prop.ID)
if err != nil {
return nil, serverError("vip_ability_query_failed", err.Error())
}
grants = append(grants, nobleVIPAccessoryGrants(ability)...)
}
for _, grant := range grants {
if err := s.java.GivePropsBackpack(ctx, integration.GivePropsBackpackRequest{
AcceptUserID: req.AcceptUserID.Int64(),
PropsID: grant.propsID,
Type: grant.typ,
Origin: adminFreeOrigin,
OriginDesc: adminFreeOriginDesc,
Days: grant.days,
UseProps: true,
AllowGive: false,
OpUser: user.UserID,
}); err != nil {
return nil, serverError("send_props_failed", err.Error())
}
}
if prop.Type == propsTypeNobleVIP {
if err := s.switchMaxNobleVIP(ctx, req.AcceptUserID.Int64()); err != nil {
return nil, err
}
}
return &SendPropsResponse{
Success: true,
AcceptUserID: req.AcceptUserID,
PropsID: req.PropsID,
Type: prop.Type,
Days: days,
}, nil
}
func (s *Service) BanUser(ctx context.Context, user AuthUser, req BanUserRequest) (*BanUserResponse, error) {
if _, err := s.requireManagerFeature(ctx, user, managerFeatureBanUser); err != nil {
return nil, err
}
if req.UserID.Int64() <= 0 {
return nil, badRequest("user_required", "userId is required")
}
if s.java == nil {
return nil, serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
}
target, err := s.loadUserRowByID(ctx, req.UserID.Int64())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, notFound("user_not_found", "user not found")
}
return nil, serverError("user_query_failed", err.Error())
}
if isUnbannableAccountStatus(target.AccountStatus) {
return nil, badRequest("user_already_banned", "user is already banned")
}
hasRecharge, err := s.userHasRecharge(ctx, target.ID)
if err != nil {
return nil, err
}
if hasRecharge {
return nil, badRequest("user_recharged", "only users without recharge can be banned")
}
description := buildBanDescription(user.UserID, req.Reason)
if err := s.java.PunishAccount(ctx, integration.PunishAccountRequest{
BeOptUserID: target.ID,
OptUserID: user.UserID,
Status: accountStatusArchive,
Description: description,
}); err != nil {
return nil, serverError("ban_user_failed", err.Error())
}
return &BanUserResponse{
Success: true,
UserID: req.UserID,
AccountStatus: accountStatusArchive,
}, nil
}
func (s *Service) UnbanUser(ctx context.Context, user AuthUser, req UnbanUserRequest) (*UnbanUserResponse, error) {
if _, err := s.requireManagerFeature(ctx, user, managerFeatureUnbanUser); err != nil {
return nil, err
}
if req.UserID.Int64() <= 0 {
return nil, badRequest("user_required", "userId is required")
}
if s.java == nil {
return nil, serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
}
target, err := s.loadUserRowByID(ctx, req.UserID.Int64())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, notFound("user_not_found", "user not found")
}
return nil, serverError("user_query_failed", err.Error())
}
if !isUnbannableAccountStatus(target.AccountStatus) {
return nil, badRequest("user_not_banned", "only banned or frozen users can be unbanned")
}
description := buildUnbanDescription(user.UserID, req.Reason)
if err := s.java.UnblockAccount(ctx, integration.UnblockAccountRequest{
BeOptUserID: target.ID,
OptUserID: user.UserID,
Description: description,
}); err != nil {
return nil, serverError("unban_user_failed", err.Error())
}
return &UnbanUserResponse{
Success: true,
UserID: req.UserID,
AccountStatus: accountStatusNormal,
}, nil
}
// ChangeUserCountry 只允许 admin 开启改国家权限的经理移动普通用户国家;目标用户已有业务身份时拒绝,避免跨国家后团队、币商和主播归属数据不一致。
func (s *Service) ChangeUserCountry(ctx context.Context, user AuthUser, req ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
if _, err := s.requireManagerFeature(ctx, user, managerFeatureChangeCountry); err != nil {
return nil, err
}
if req.UserID.Int64() <= 0 {
return nil, badRequest("user_required", "userId is required")
}
if req.CountryID.Int64() <= 0 {
return nil, badRequest("country_required", "countryId is required")
}
if s.db == nil {
return nil, serviceUnavailable("database_unavailable", "database is unavailable")
}
target, err := s.loadUserRowByID(ctx, req.UserID.Int64())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, notFound("user_not_found", "user not found")
}
return nil, serverError("user_query_failed", err.Error())
}
country, err := s.loadOpenCountryByID(ctx, req.CountryID.Int64())
if err != nil {
return nil, err
}
if err := s.ensureUserCountryMovable(ctx, target.ID); err != nil {
return nil, err
}
// 只改国家三元组,不触碰昵称、头像等资料审核字段;清理 Java 资料缓存后 App/H5 会重新读到新国家。
updates := map[string]any{
"country_id": country.ID,
"country_code": strings.ToUpper(strings.TrimSpace(country.AlphaTwo)),
"country_name": strings.TrimSpace(country.CountryName),
"update_time": time.Now(),
"update_user": user.UserID,
}
if err := s.db.WithContext(ctx).
Table("user_base_info").
Where("id = ? AND is_del = ?", target.ID, false).
Updates(updates).Error; err != nil {
return nil, serverError("user_country_update_failed", err.Error())
}
if s.java != nil {
if err := s.java.RemoveUserProfileCacheAll(ctx, target.ID); err != nil {
return nil, serverError("remove_profile_cache_failed", err.Error())
}
}
target.CountryID = country.ID
target.CountryCode = strings.ToUpper(strings.TrimSpace(country.AlphaTwo))
target.CountryName = strings.TrimSpace(country.CountryName)
view, err := s.userRowToView(ctx, target)
if err != nil {
return nil, err
}
return &ChangeUserCountryResponse{Success: true, User: view}, nil
}
func (s *Service) ensureManager(ctx context.Context, user AuthUser) error {
_, err := s.requireManagerInfo(ctx, user)
return err
}
// requireManagerFeature 先确认调用人仍然是团队经理,再检查 admin 给这个经理打开的单项 H5 操作权限。
// 这样前端隐藏按钮和后端写操作使用同一张 team_manager_base_info 表做判断H5 被绕过时也会被服务层拦截。
func (s *Service) requireManagerFeature(ctx context.Context, user AuthUser, feature managerFeature) (teamManagerInfoRow, error) {
manager, err := s.requireManagerInfo(ctx, user)
if err != nil {
return teamManagerInfoRow{}, err
}
features := managerFeaturesFromRow(manager)
if !features.enabled(feature) {
return teamManagerInfoRow{}, forbidden("manager_feature_disabled", "manager feature is disabled")
}
return manager, nil
}
func managerFeaturesFromRow(row teamManagerInfoRow) ManagerFeatures {
return ManagerFeatures{
AddBDLeader: defaultEnabled(row.CanAddBDLeader),
GiftProps: defaultEnabled(row.CanGiftProps),
BanUser: defaultEnabled(row.CanBanUser),
UnbanUser: defaultEnabled(row.CanUnbanUser),
ChangeCountry: defaultEnabled(row.CanChangeCountry),
}
}
func defaultEnabled(value *bool) bool {
return value == nil || *value
}
func (features ManagerFeatures) enabled(feature managerFeature) bool {
switch feature {
case managerFeatureAddBDLeader:
return features.AddBDLeader
case managerFeatureGiftProps:
return features.GiftProps
case managerFeatureBanUser:
return features.BanUser
case managerFeatureUnbanUser:
return features.UnbanUser
case managerFeatureChangeCountry:
return features.ChangeCountry
default:
return false
}
}
func (s *Service) findUserView(ctx context.Context, account string) (UserView, error) {
if userID, err := strconv.ParseInt(account, 10, 64); err == nil && userID > 0 {
if row, err := s.loadUserRowByID(ctx, userID); err == nil {
return s.userRowToView(ctx, row)
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return UserView{}, serverError("user_query_failed", err.Error())
}
}
if s.java != nil {
profile, err := s.java.GetUserProfileByAccountOne(ctx, account)
if err == nil && int64(profile.ID) > 0 {
if row, rowErr := s.loadUserRowByID(ctx, int64(profile.ID)); rowErr == nil {
view, viewErr := s.userRowToView(ctx, row)
if viewErr != nil {
return UserView{}, viewErr
}
overlayJavaProfile(&view, profile)
return view, nil
} else if !errors.Is(rowErr, gorm.ErrRecordNotFound) {
return UserView{}, serverError("user_query_failed", rowErr.Error())
}
return s.javaProfileToView(ctx, profile)
}
}
row, err := s.loadUserRowByAccount(ctx, account)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return UserView{}, notFound("user_not_found", "user not found")
}
return UserView{}, serverError("user_query_failed", err.Error())
}
return s.userRowToView(ctx, row)
}
func (s *Service) loadUserViewByID(ctx context.Context, userID int64) (UserView, error) {
row, err := s.loadUserRowByID(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return UserView{}, notFound("user_not_found", "user not found")
}
return UserView{}, serverError("user_query_failed", err.Error())
}
return s.userRowToView(ctx, row)
}
func (s *Service) loadUserRowByID(ctx context.Context, userID int64) (userBaseInfoRow, error) {
var row userBaseInfoRow
err := s.db.WithContext(ctx).
Table("user_base_info").
Where("id = ? AND is_del = ?", userID, false).
First(&row).Error
return row, err
}
func (s *Service) loadUserRowByAccount(ctx context.Context, account string) (userBaseInfoRow, error) {
var row userBaseInfoRow
err := s.db.WithContext(ctx).
Table("user_base_info").
Where("account = ? AND is_del = ?", account, false).
First(&row).Error
return row, err
}
func (s *Service) loadOpenCountryByID(ctx context.Context, countryID int64) (sysCountryCodeRow, error) {
var row sysCountryCodeRow
err := s.db.WithContext(ctx).
Table("sys_country_code").
Select("id, alpha_two, country_name, national_flag, phone_prefix, is_open, sort").
Where("id = ? AND is_open = ?", countryID, true).
First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return sysCountryCodeRow{}, notFound("country_not_found", "country not found")
}
if err != nil {
return sysCountryCodeRow{}, serverError("country_query_failed", err.Error())
}
if strings.TrimSpace(row.AlphaTwo) == "" {
return sysCountryCodeRow{}, badRequest("country_code_required", "country code is required")
}
return row, nil
}
// ensureUserCountryMovable 串行检查所有会绑定归属关系的身份;任一命中都返回同一个业务错误,前端显示统一提示。
func (s *Service) ensureUserCountryMovable(ctx context.Context, userID int64) error {
locked, err := s.userHasLockedHistoryIdentity(ctx, userID)
if err != nil {
return err
}
if locked {
return badRequest("user_identity_locked", "user has host, agency, BD, BD leader or coin seller identity and cannot be moved")
}
locked, err = s.userIsBDLeader(ctx, userID)
if err != nil {
return err
}
if locked {
return badRequest("user_identity_locked", "user has host, agency, BD, BD leader or coin seller identity and cannot be moved")
}
if s.java == nil {
return serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
}
locked, err = s.java.ExistsFreightSeller(ctx, userID)
if err != nil {
return serverError("coin_seller_query_failed", err.Error())
}
if locked {
return badRequest("user_identity_locked", "user has host, agency, BD, BD leader or coin seller identity and cannot be moved")
}
return nil
}
func (s *Service) userHasLockedHistoryIdentity(ctx context.Context, userID int64) (bool, error) {
var count int64
if err := s.db.WithContext(ctx).
Table("user_history_identity").
Where("user_id = ? AND (is_host = ? OR is_agent = ? OR is_bd = ?)", userID, true, true, true).
Count(&count).Error; err != nil {
return false, serverError("history_identity_query_failed", err.Error())
}
return count > 0, nil
}
func (s *Service) userRowToView(ctx context.Context, row userBaseInfoRow) (UserView, error) {
hasRecharge, canBan := s.userRechargeEligibility(ctx, row.ID)
canBan = canBan && !isUnbannableAccountStatus(row.AccountStatus)
return UserView{
UserID: ID(row.ID),
Account: row.Account,
UserAvatar: row.UserAvatar,
UserNickname: row.UserNickname,
CountryID: ID(row.CountryID),
CountryCode: row.CountryCode,
CountryName: row.CountryName,
OriginSys: row.OriginSys,
AccountStatus: row.AccountStatus,
HasRecharge: hasRecharge,
CanBan: canBan,
}, nil
}
func (s *Service) javaProfileToView(ctx context.Context, profile integration.UserProfile) (UserView, error) {
hasRecharge, canBan := s.userRechargeEligibility(ctx, int64(profile.ID))
canBan = canBan && !isUnbannableAccountStatus(profile.AccountStatus)
return UserView{
UserID: ID(profile.ID),
Account: profile.Account,
UserAvatar: profile.UserAvatar,
UserNickname: profile.UserNickname,
CountryID: ID(profile.CountryID),
CountryCode: profile.CountryCode,
CountryName: profile.CountryName,
OriginSys: profile.OriginSys,
AccountStatus: profile.AccountStatus,
HasRecharge: hasRecharge,
CanBan: canBan,
}, nil
}
func overlayJavaProfile(view *UserView, profile integration.UserProfile) {
if profile.Account != "" {
view.Account = profile.Account
}
if profile.UserAvatar != "" {
view.UserAvatar = profile.UserAvatar
}
if profile.UserNickname != "" {
view.UserNickname = profile.UserNickname
}
if profile.CountryCode != "" {
view.CountryCode = profile.CountryCode
}
if profile.CountryName != "" {
view.CountryName = profile.CountryName
}
if int64(profile.CountryID) > 0 {
view.CountryID = ID(profile.CountryID)
}
if profile.OriginSys != "" {
view.OriginSys = profile.OriginSys
}
if profile.AccountStatus != "" {
view.AccountStatus = profile.AccountStatus
if isUnbannableAccountStatus(profile.AccountStatus) {
view.CanBan = false
}
}
}
func countryRowToView(row sysCountryCodeRow) (CountryView, bool) {
code := strings.ToUpper(strings.TrimSpace(row.AlphaTwo))
name := strings.TrimSpace(row.CountryName)
if row.ID <= 0 || code == "" || name == "" {
return CountryView{}, false
}
return CountryView{
ID: ID(row.ID),
CountryCode: code,
CountryName: name,
NationalFlag: strings.TrimSpace(row.NationalFlag),
PhonePrefix: row.PhonePrefix,
}, true
}
func (s *Service) userHasRecharge(ctx context.Context, userID int64) (bool, error) {
if userID <= 0 {
return false, nil
}
if s.java == nil {
return false, nil
}
recharges, err := s.java.MapUserTotalRecharge(ctx, []int64{userID})
if err != nil {
return false, serverError("recharge_query_failed", err.Error())
}
for _, item := range recharges[userID] {
raw := strings.TrimSpace(string(item.Amount))
if raw == "" {
continue
}
amount, err := strconv.ParseFloat(raw, 64)
if err != nil {
return false, serverError("recharge_query_failed", err.Error())
}
if amount > 0 {
return true, nil
}
}
return false, nil
}
func (s *Service) userRechargeEligibility(ctx context.Context, userID int64) (bool, bool) {
hasRecharge, err := s.userHasRecharge(ctx, userID)
if err != nil {
return false, false
}
return hasRecharge, !hasRecharge
}
func (s *Service) loadGiftableProps(ctx context.Context, propsID int64) (propsSourceRecordRow, error) {
var row propsSourceRecordRow
err := s.db.WithContext(ctx).
Table("props_source_record").
Select("id, type, code, name, cover, source_url, expand, admin_free, is_del").
Where("id = ? AND is_del = ? AND admin_free = ?", propsID, false, true).
First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return propsSourceRecordRow{}, badRequest("prop_not_giftable", "props is not admin-free giftable")
}
if err != nil {
return propsSourceRecordRow{}, serverError("props_query_failed", err.Error())
}
return row, nil
}
func (s *Service) loadGiftableNobleVIPAbility(ctx context.Context, propsID int64) (propsNobleVIPAbilityRow, error) {
var row propsNobleVIPAbilityRow
err := s.db.WithContext(ctx).
Table("props_noble_vip_ability").
Where("id = ?", propsID).
First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return propsNobleVIPAbilityRow{}, badRequest("vip_not_giftable", "noble VIP ability is not giftable")
}
if err != nil {
return propsNobleVIPAbilityRow{}, serverError("vip_ability_query_failed", err.Error())
}
if row.VIPLevel > maxGiftableVIPLevel {
return propsNobleVIPAbilityRow{}, badRequest("vip_not_giftable", "only noble VIP level 1-3 can be gifted")
}
return row, nil
}
func (s *Service) loadVIPLevels(ctx context.Context, rows []propsSourceRecordRow) (map[int64]int, error) {
ids := make([]int64, 0)
for _, row := range rows {
if row.Type == propsTypeNobleVIP {
ids = append(ids, row.ID)
}
}
if len(ids) == 0 {
return nil, nil
}
var abilities []propsNobleVIPAbilityRow
if err := s.db.WithContext(ctx).
Table("props_noble_vip_ability").
Select("id, vip_level").
Where("id IN ?", ids).
Find(&abilities).Error; err != nil {
return nil, serverError("vip_ability_query_failed", err.Error())
}
levels := make(map[int64]int, len(abilities))
for _, ability := range abilities {
levels[ability.ID] = ability.VIPLevel
}
return levels, nil
}
type propsGrant struct {
propsID int64
typ string
days int
}
func nobleVIPAccessoryGrants(ability integration.PropsNobleVIPAbility) []propsGrant {
grants := make([]propsGrant, 0, 4)
appendIfPositive := func(propsID int64, typ string) {
if propsID <= 0 {
return
}
grants = append(grants, propsGrant{propsID: propsID, typ: typ, days: defaultVIPGiftDays})
}
appendIfPositive(int64(ability.CarID), propsTypeRide)
appendIfPositive(int64(ability.AvatarFrameID), propsTypeAvatarFrame)
appendIfPositive(int64(ability.ChatBubbleID), propsTypeChatBubble)
appendIfPositive(int64(ability.DataCardID), propsTypeDataCard)
return grants
}
func (s *Service) switchMaxNobleVIP(ctx context.Context, userID int64) error {
ability, err := s.java.GetUserMaxNobleVIPAbility(ctx, userID)
if err != nil {
return serverError("vip_ability_query_failed", err.Error())
}
ids := []int64{
int64(ability.ID),
int64(ability.AvatarFrameID),
int64(ability.DataCardID),
int64(ability.CarID),
int64(ability.ChatBubbleID),
}
for _, propsID := range compactPositiveIDs(ids) {
if err := s.java.SwitchUseProps(ctx, userID, propsID); err != nil {
return serverError("switch_props_failed", err.Error())
}
}
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
return serverError("remove_profile_cache_failed", err.Error())
}
return nil
}
func compactPositiveIDs(ids []int64) []int64 {
seen := make(map[int64]struct{}, len(ids))
out := make([]int64, 0, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
func propsRowToView(row propsSourceRecordRow, vipLevels map[int64]int) (PropsView, bool) {
level := 0
if row.Type == propsTypeNobleVIP {
var exists bool
level, exists = vipLevels[row.ID]
if !exists || level > 3 {
return PropsView{}, false
}
}
return PropsView{
ID: ID(row.ID),
Type: row.Type,
Code: row.Code,
Name: row.Name,
Cover: row.Cover,
SourceURL: row.SourceURL,
Expand: row.Expand,
Days: propsGiftDays(row.Type),
VIPLevel: level,
}, true
}
func propsGiftDays(propsType string) int {
if propsType == propsTypeNobleVIP {
return defaultVIPGiftDays
}
return defaultGiftDays
}
func normalizePropsType(propsType string) string {
return strings.ToUpper(strings.TrimSpace(propsType))
}
func buildBanDescription(managerID int64, reason string) string {
reason = strings.TrimSpace(reason)
if len([]rune(reason)) > 200 {
runes := []rune(reason)
reason = string(runes[:200])
}
if reason == "" {
return fmt.Sprintf("manager center ban non-recharged user, manager=%d", managerID)
}
return fmt.Sprintf("manager center ban non-recharged user, manager=%d, reason=%s", managerID, reason)
}
func buildUnbanDescription(managerID int64, reason string) string {
reason = strings.TrimSpace(reason)
if len([]rune(reason)) > 200 {
runes := []rune(reason)
reason = string(runes[:200])
}
if reason == "" {
return fmt.Sprintf("manager center unban user, manager=%d", managerID)
}
return fmt.Sprintf("manager center unban user, manager=%d, reason=%s", managerID, reason)
}
func isUnbannableAccountStatus(status string) bool {
switch strings.ToUpper(strings.TrimSpace(status)) {
case accountStatusFreeze, accountStatusArchive, accountStatusArchiveDevice:
return true
default:
return false
}
}
func isAppErrorCode(err error, code string) bool {
var appErr *AppError
return errors.As(err, &appErr) && appErr.Code == code
}
func normalizeSysOrigin(sysOrigin string) string {
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
if sysOrigin == "" {
return "LIKEI"
}
return sysOrigin
}
func vipPropsViewFromConfig(row model.VipLevelConfig) PropsView {
days := row.DurationDays
if days <= 0 {
days = defaultVIPGiftDays
}
name := strings.TrimSpace(row.DisplayName)
if name == "" {
name = fmt.Sprintf("VIP %d", row.Level)
}
code := strings.TrimSpace(row.LevelCode)
if code == "" {
code = fmt.Sprintf("VIP%d", row.Level)
}
return PropsView{
ID: ID(row.ID),
Type: propsTypeNobleVIP,
Code: code,
Name: name,
Cover: row.BadgeURL,
SourceURL: row.BadgeURL,
Days: days,
VIPLevel: row.Level,
}
}