hi-server/internal/logic/common/inviteLinkResolver.go
shanshanzhong 4349a7ea2f
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m16s
家庭组 权益修改
2026-03-04 22:02:42 -08:00

290 lines
7.0 KiB
Go

package common
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
"sync"
"time"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/kutt"
)
const inviteShortLinkCachePrefix = "cache:invite:short_link:"
type inviteLinkCustomData struct {
ShareURL string `json:"shareUrl"`
Domain string `json:"domain"`
}
type InviteLinkResolver struct {
ctx context.Context
svcCtx *svc.ServiceContext
createShortLink func(ctx context.Context, targetURL, domain string) (string, error)
}
func NewInviteLinkResolver(ctx context.Context, svcCtx *svc.ServiceContext) *InviteLinkResolver {
resolver := &InviteLinkResolver{
ctx: ctx,
svcCtx: svcCtx,
}
resolver.createShortLink = func(ctx context.Context, targetURL, domain string) (string, error) {
client := kutt.NewClient(svcCtx.Config.Kutt.ApiURL, svcCtx.Config.Kutt.ApiKey)
link, err := client.CreateShortLink(ctx, &kutt.CreateLinkRequest{
Target: targetURL,
Reuse: true,
Domain: domain,
})
if err != nil {
return "", err
}
shortLink := strings.TrimSpace(link.Link)
if strings.HasPrefix(shortLink, "http://") {
shortLink = strings.Replace(shortLink, "http://", "https://", 1)
}
return shortLink, nil
}
return resolver
}
func (r *InviteLinkResolver) ResolveInviteLink(referCode string) string {
normalizedCode := strings.TrimSpace(referCode)
if normalizedCode == "" {
return ""
}
longLink := r.buildLongInviteLink(normalizedCode)
if !r.canUseKutt() || longLink == "" {
return longLink
}
if cached := r.getCachedShortLink(normalizedCode); cached != "" {
return cached
}
shortLink, err := r.generateShortLinkWithTimeout(normalizedCode, 1500*time.Millisecond)
if err != nil || strings.TrimSpace(shortLink) == "" {
return longLink
}
r.cacheShortLink(normalizedCode, shortLink)
return shortLink
}
func (r *InviteLinkResolver) ResolveInviteLinksBatch(referCodes []string, maxGenerate, maxConcurrency int, timeout time.Duration) map[string]string {
result := make(map[string]string)
uniqueCodes := uniqueReferCodes(referCodes)
if len(uniqueCodes) == 0 {
return result
}
for _, referCode := range uniqueCodes {
result[referCode] = r.buildLongInviteLink(referCode)
}
if !r.canUseKutt() {
return result
}
toGenerate := make([]string, 0, len(uniqueCodes))
for _, referCode := range uniqueCodes {
if cached := r.getCachedShortLink(referCode); cached != "" {
result[referCode] = cached
continue
}
toGenerate = append(toGenerate, referCode)
}
if maxGenerate > 0 && len(toGenerate) > maxGenerate {
toGenerate = toGenerate[:maxGenerate]
}
if len(toGenerate) == 0 {
return result
}
if maxConcurrency <= 0 {
maxConcurrency = 1
}
if timeout <= 0 {
timeout = 1500 * time.Millisecond
}
limiter := make(chan struct{}, maxConcurrency)
var waitGroup sync.WaitGroup
var mutex sync.Mutex
for _, referCode := range toGenerate {
waitGroup.Add(1)
currentCode := referCode
go func() {
defer waitGroup.Done()
limiter <- struct{}{}
defer func() { <-limiter }()
shortLink, err := r.generateShortLinkWithTimeout(currentCode, timeout)
if err != nil || strings.TrimSpace(shortLink) == "" {
return
}
mutex.Lock()
result[currentCode] = shortLink
mutex.Unlock()
r.cacheShortLink(currentCode, shortLink)
}()
}
waitGroup.Wait()
return result
}
func (r *InviteLinkResolver) canUseKutt() bool {
if r == nil || r.svcCtx == nil {
return false
}
if !r.svcCtx.Config.Kutt.Enable {
return false
}
if strings.TrimSpace(r.svcCtx.Config.Kutt.ApiURL) == "" || strings.TrimSpace(r.svcCtx.Config.Kutt.ApiKey) == "" {
return false
}
return r.createShortLink != nil
}
func (r *InviteLinkResolver) resolveShareURLAndDomain() (string, string) {
if r == nil || r.svcCtx == nil {
return "", ""
}
shareURL := strings.TrimSpace(r.svcCtx.Config.Kutt.TargetURL)
domain := strings.TrimSpace(r.svcCtx.Config.Kutt.Domain)
customData := strings.TrimSpace(r.svcCtx.Config.Site.CustomData)
if customData == "" {
return shareURL, domain
}
var parsedData inviteLinkCustomData
if err := json.Unmarshal([]byte(customData), &parsedData); err != nil {
return shareURL, domain
}
if strings.TrimSpace(parsedData.ShareURL) != "" {
shareURL = strings.TrimSpace(parsedData.ShareURL)
}
if strings.TrimSpace(parsedData.Domain) != "" {
domain = strings.TrimSpace(parsedData.Domain)
}
return shareURL, domain
}
func (r *InviteLinkResolver) buildLongInviteLink(referCode string) string {
normalizedCode := strings.TrimSpace(referCode)
if normalizedCode == "" {
return ""
}
shareURL, _ := r.resolveShareURLAndDomain()
if shareURL == "" {
return ""
}
parsedURL, err := url.Parse(shareURL)
if err != nil {
return fallbackLongInviteLink(shareURL, normalizedCode)
}
queryValues := parsedURL.Query()
queryValues.Set("ic", normalizedCode)
parsedURL.RawQuery = queryValues.Encode()
return parsedURL.String()
}
func (r *InviteLinkResolver) generateShortLinkWithTimeout(referCode string, timeout time.Duration) (string, error) {
longLink := r.buildLongInviteLink(referCode)
if longLink == "" {
return "", nil
}
_, domain := r.resolveShareURLAndDomain()
requestCtx := r.ctx
var cancel context.CancelFunc
if timeout > 0 {
requestCtx, cancel = context.WithTimeout(r.ctx, timeout)
defer cancel()
}
shortLink, err := r.createShortLink(requestCtx, longLink, domain)
if err != nil {
return "", err
}
return strings.TrimSpace(shortLink), nil
}
func (r *InviteLinkResolver) getCachedShortLink(referCode string) string {
if r == nil || r.svcCtx == nil || r.svcCtx.Redis == nil {
return ""
}
cacheKey := inviteShortLinkCachePrefix + referCode
shortLink, err := r.svcCtx.Redis.Get(r.ctx, cacheKey).Result()
if err != nil {
return ""
}
return strings.TrimSpace(shortLink)
}
func (r *InviteLinkResolver) cacheShortLink(referCode, shortLink string) {
if r == nil || r.svcCtx == nil || r.svcCtx.Redis == nil {
return
}
if strings.TrimSpace(referCode) == "" || strings.TrimSpace(shortLink) == "" {
return
}
cacheKey := inviteShortLinkCachePrefix + referCode
_ = r.svcCtx.Redis.Set(r.ctx, cacheKey, shortLink, 0).Err()
}
func uniqueReferCodes(referCodes []string) []string {
uniqueCodes := make([]string, 0, len(referCodes))
seen := make(map[string]struct{}, len(referCodes))
for _, referCode := range referCodes {
normalizedCode := strings.TrimSpace(referCode)
if normalizedCode == "" {
continue
}
if _, exists := seen[normalizedCode]; exists {
continue
}
seen[normalizedCode] = struct{}{}
uniqueCodes = append(uniqueCodes, normalizedCode)
}
return uniqueCodes
}
func fallbackLongInviteLink(baseURL, referCode string) string {
normalizedBase := strings.TrimSpace(baseURL)
normalizedCode := strings.TrimSpace(referCode)
if normalizedBase == "" || normalizedCode == "" {
return ""
}
separator := "?"
if strings.Contains(normalizedBase, "?") {
separator = "&"
}
trimmedBase := strings.TrimRight(normalizedBase, "?&")
return fmt.Sprintf("%s%sic=%s", trimmedBase, separator, url.QueryEscape(normalizedCode))
}