package managercenter import ( "context" "errors" "fmt" "strconv" "strings" "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"` } 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"` 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"` } func (userBaseInfoRow) TableName() string { return "user_base_info" } 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) { if err := s.ensureManager(ctx, user); err != nil { return nil, err } view, err := s.loadUserViewByID(ctx, user.UserID) if err != nil { return nil, err } return &ProfileResponse{Manager: view}, 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 } 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.ensureManager(ctx, user); 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.ensureManager(ctx, user); 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.ensureManager(ctx, user); 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 } func (s *Service) ensureManager(ctx context.Context, user AuthUser) error { if user.UserID <= 0 { return unauthorized("invalid_user", "authenticated user is required") } if s.db == nil { return serviceUnavailable("database_unavailable", "database is unavailable") } var count int64 err := s.db.WithContext(ctx). Table("team_manager_base_info"). Where("user_id = ?", user.UserID). Count(&count).Error if err != nil { return serverError("manager_query_failed", err.Error()) } if count == 0 { return forbidden("not_manager", "user is not manager") } return nil } 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) 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, 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, 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 profile.OriginSys != "" { view.OriginSys = profile.OriginSys } if profile.AccountStatus != "" { view.AccountStatus = profile.AccountStatus if isUnbannableAccountStatus(profile.AccountStatus) { view.CanBan = false } } } 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, } }