package user import ( "context" "encoding/json" "fmt" "sort" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/kutt" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/phone" "github.com/perfect-panel/server/pkg/tool" ) type QueryUserInfoLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } // Query User Info func NewQueryUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserInfoLogic { return &QueryUserInfoLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { resp = &types.User{} u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } tool.DeepCopy(resp, u) // 临时调试日志:打印原始 AuthMethods fmt.Println("========================================") fmt.Printf("UserID: %d, Original AuthMethods Count: %d\n", u.Id, len(u.AuthMethods)) for i, m := range u.AuthMethods { fmt.Printf(" [%d] Type: %s, Identifier: %s\n", i, m.AuthType, m.AuthIdentifier) } fmt.Println("========================================") var userMethods []types.UserAuthMethod for _, method := range u.AuthMethods { item := types.UserAuthMethod{ AuthType: method.AuthType, Verified: method.Verified, AuthIdentifier: method.AuthIdentifier, } switch method.AuthType { case "mobile": item.AuthIdentifier = phone.MaskPhoneNumber(method.AuthIdentifier) case "email": // No masking for email case "device": // No masking for device identifier default: item.AuthIdentifier = maskOpenID(method.AuthIdentifier) } userMethods = append(userMethods, item) } // 按照指定顺序排序:email第一位,mobile第二位,其他按原顺序 sort.Slice(userMethods, func(i, j int) bool { return getAuthTypePriority(userMethods[i].AuthType) < getAuthTypePriority(userMethods[j].AuthType) }) // 临时调试日志:打印处理后的 AuthMethods fmt.Println("========================================") fmt.Printf("UserID: %d, Sorted Response AuthMethods Count: %d\n", u.Id, len(userMethods)) for i, m := range userMethods { fmt.Printf(" [%d] Type: %s, Identifier: %s\n", i, m.AuthType, m.AuthIdentifier) } fmt.Println("========================================") resp.AuthMethods = userMethods // 生成邀请短链接 if l.svcCtx.Config.Kutt.Enable && resp.ReferCode != "" { shortLink := l.generateInviteShortLink(resp.ReferCode) if shortLink != "" { resp.ShareLink = shortLink } } return resp, nil } // customData 用于解析 SiteConfig.CustomData JSON 字段 // 包含从自定义数据中提取所需的配置项 type customData struct { ShareUrl string `json:"shareUrl"` // 分享链接前缀 URL(目标落地页) Domain string `json:"domain"` // 短链接域名 } // getShareUrl 从 SiteConfig.CustomData 中获取 shareUrl // // 返回: // - string: 分享链接前缀 URL,如果获取失败则返回 Kutt.TargetURL 作为 fallback func (l *QueryUserInfoLogic) getShareUrl() string { siteConfig := l.svcCtx.Config.Site if siteConfig.CustomData != "" { var data customData if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil { if data.ShareUrl != "" { return data.ShareUrl } } } // fallback 到 Kutt.TargetURL return l.svcCtx.Config.Kutt.TargetURL } // getDomain 从 SiteConfig.CustomData 中获取短链接域名 // // 返回: // - string: 短链接域名,如果获取失败则返回 Kutt.Domain 作为 fallback func (l *QueryUserInfoLogic) getDomain() string { siteConfig := l.svcCtx.Config.Site if siteConfig.CustomData != "" { var data customData if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil { if data.Domain != "" { return data.Domain } } } // fallback 到 Kutt.Domain return l.svcCtx.Config.Kutt.Domain } // generateInviteShortLink 生成邀请短链接(带 Redis 缓存) // // 参数: // - inviteCode: 邀请码 // // 返回: // - string: 短链接 URL,失败时返回空字符串 func (l *QueryUserInfoLogic) generateInviteShortLink(inviteCode string) string { cfg := l.svcCtx.Config.Kutt shareUrl := l.getShareUrl() domain := l.getDomain() // 检查必要配置 if cfg.ApiURL == "" || cfg.ApiKey == "" { l.Sloww("Kutt config incomplete", logger.Field("api_url", cfg.ApiURL != ""), logger.Field("api_key", cfg.ApiKey != "")) return "" } if shareUrl == "" { l.Sloww("ShareUrl not configured in CustomData or Kutt.TargetURL") return "" } // Redis 缓存 key cacheKey := "cache:invite:short_link:" + inviteCode // 1. 尝试从 Redis 缓存读取 cachedLink, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() if err == nil && cachedLink != "" { l.Debugw("Hit cache for invite short link", logger.Field("invite_code", inviteCode), logger.Field("short_link", cachedLink)) return cachedLink } // 2. 缓存未命中,调用 Kutt API 创建短链接 client := kutt.NewClient(cfg.ApiURL, cfg.ApiKey) shortLink, err := client.CreateInviteShortLink(l.ctx, shareUrl, inviteCode, domain) if err != nil { l.Errorw("Failed to create short link", logger.Field("error", err.Error()), logger.Field("invite_code", inviteCode), logger.Field("share_url", shareUrl)) return "" } // 3. 写入 Redis 缓存(永不过期,因为邀请码不变短链接也不会变) if err := l.svcCtx.Redis.Set(l.ctx, cacheKey, shortLink, 0).Err(); err != nil { l.Errorw("Failed to cache short link", logger.Field("error", err.Error()), logger.Field("invite_code", inviteCode)) // 缓存失败不影响返回 } l.Infow("Created and cached invite short link", logger.Field("invite_code", inviteCode), logger.Field("short_link", shortLink), logger.Field("share_url", shareUrl)) return shortLink } // getAuthTypePriority 获取认证类型的排序优先级 // email: 1 (第一位) // mobile: 2 (第二位) // 其他类型: 100+ (后续位置) func getAuthTypePriority(authType string) int { switch authType { case "email": return 1 case "mobile": return 2 default: return 100 } } // maskOpenID 脱敏 OpenID,只保留前 3 和后 3 位 func maskOpenID(openID string) string { length := len(openID) if length <= 6 { return "***" // 如果 ID 太短,直接返回 "***" } // 计算中间需要被替换的 `*` 数量 maskLength := length - 6 mask := make([]byte, maskLength) for i := range mask { mask[i] = '*' } // 组合脱敏后的 OpenID return openID[:3] + string(mask) + openID[length-3:] }