fix(adtracking): 收紧成交口径、点击去重与生产落地页白名单

成交 GMV 排除已关闭订单,避免未支付关单被计入;点击按访客 5 秒、IP 1 秒双重去重;生产禁止 localhost 落地页,失效短码改用 QueryEscape。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-10 11:27:43 +08:00
co-authored by Cursor
parent 6a3611c1a8
commit ee0e70ccad
6 changed files with 73 additions and 52 deletions
+35 -20
View File
@@ -28,7 +28,7 @@ var reservedCodes = map[string]bool{
"pay": true, "notice": true, "chat": true, "dashboard": true,
}
func validateTargetURL(rawURL string) (string, error) {
func validateTargetURL(rawURL string, allowLocal bool) (string, error) {
targetURL := strings.TrimSpace(rawURL)
if targetURL == "" {
return "https://hao858.com/", nil
@@ -40,25 +40,37 @@ func validateTargetURL(rawURL string) (string, error) {
targetURL = "https://" + targetURL
}
u, err := url.Parse(targetURL)
if err != nil {
if err != nil || u.Host == "" {
return "", errors.New("目标落地页地址不合法")
}
scheme := strings.ToLower(u.Scheme)
if scheme != "http" && scheme != "https" {
return "", errors.New("目标落地页地址不合法")
}
host := strings.ToLower(u.Hostname())
if host != "hao858.com" && !strings.HasSuffix(host, ".hao858.com") && host != "localhost" && host != "127.0.0.1" {
if host == "localhost" || host == "127.0.0.1" {
if !allowLocal {
return "", ErrDisallowedTargetDomain
}
return targetURL, nil
}
if host != "hao858.com" && !strings.HasSuffix(host, ".hao858.com") {
return "", ErrDisallowedTargetDomain
}
return targetURL, nil
}
type Service struct {
repo *Repository
redis *redis.Client
repo *Repository
redis *redis.Client
allowLocalTargets bool
}
func NewService(repo *Repository, redisClient *redis.Client) *Service {
func NewService(repo *Repository, redisClient *redis.Client, allowLocalTargets bool) *Service {
return &Service{
repo: repo,
redis: redisClient,
repo: repo,
redis: redisClient,
allowLocalTargets: allowLocalTargets,
}
}
@@ -77,7 +89,7 @@ func (s *Service) CreateLink(ctx context.Context, adminID uint64, req CreateTrac
return nil, ErrCodeAlreadyExists
}
targetURL, err := validateTargetURL(req.TargetURL)
targetURL, err := validateTargetURL(req.TargetURL, s.allowLocalTargets)
if err != nil {
return nil, err
}
@@ -113,7 +125,7 @@ func (s *Service) UpdateLink(ctx context.Context, id uint64, req UpdateTrackingL
return ErrLinkNotFound
}
targetURL, err := validateTargetURL(req.TargetURL)
targetURL, err := validateTargetURL(req.TargetURL, s.allowLocalTargets)
if err != nil {
return err
}
@@ -179,18 +191,13 @@ func (s *Service) ResolveRedirect(ctx context.Context, code, ip, ua, referer, vi
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 防刷频控:同一个访客/IP在 5 秒内重复点击相同短链,不重复记录流水
// 防刷:同访客 5 秒去重;同 IP 1 秒去重,避免不带 Cookie 轮换 visitorID 刷库
if s.redis != nil {
dedupID := visitorID
if dedupID == "" {
dedupID = ip
if isDuplicateClick(bgCtx, s.redis, "adtrack:dedup:v:"+link.Code+":"+visitorID, 5*time.Second) {
return
}
if dedupID != "" {
dedupKey := "adtrack:dedup:" + link.Code + ":" + dedupID
ok, rErr := s.redis.SetNX(bgCtx, dedupKey, "1", 5*time.Second).Result()
if rErr == nil && !ok {
return
}
if isDuplicateClick(bgCtx, s.redis, "adtrack:dedup:ip:"+link.Code+":"+ip, time.Second) {
return
}
}
@@ -214,6 +221,14 @@ func (s *Service) ResolveRedirect(ctx context.Context, code, ip, ua, referer, vi
return finalURL, nil
}
func isDuplicateClick(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) bool {
if rdb == nil || key == "" || strings.HasSuffix(key, ":") {
return false
}
ok, err := rdb.SetNX(ctx, key, "1", ttl).Result()
return err == nil && !ok
}
func appendTrackCode(targetURL, code string) string {
u, err := url.Parse(targetURL)
if err != nil {