fix(adtracking): 修复生产网关反代路由、成交态统计口径与安全加固

This commit is contained in:
yml2213
2026-09-10 11:17:40 +08:00
parent 2b11d70ebe
commit 6a3611c1a8
10 changed files with 140 additions and 28 deletions
+56 -16
View File
@@ -13,12 +13,13 @@ import (
)
var (
ErrLinkNotFound = errors.New("推广链接不存在")
ErrLinkDisabled = errors.New("该推广链接已被停用")
ErrCodeAlreadyExists = errors.New("该自定义后缀已被占用,请更换")
ErrInvalidCodeFormat = errors.New("后缀格式不正确,仅支持2-64位英文、数字、下划线和连字符")
ErrReservedCode = errors.New("该后缀为系统保留字,不可使用")
codePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]{2,64}$`)
ErrLinkNotFound = errors.New("推广链接不存在")
ErrLinkDisabled = errors.New("该推广链接已被停用")
ErrCodeAlreadyExists = errors.New("该自定义后缀已被占用,请更换")
ErrInvalidCodeFormat = errors.New("后缀格式不正确,仅支持2-64位英文、数字、下划线和连字符")
ErrReservedCode = errors.New("该后缀为系统保留字,不可使用")
ErrDisallowedTargetDomain = errors.New("目标落地页仅允许为 hao858.com 及其子域名或站内路径")
codePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]{2,64}$`)
)
var reservedCodes = map[string]bool{
@@ -27,6 +28,28 @@ var reservedCodes = map[string]bool{
"pay": true, "notice": true, "chat": true, "dashboard": true,
}
func validateTargetURL(rawURL string) (string, error) {
targetURL := strings.TrimSpace(rawURL)
if targetURL == "" {
return "https://hao858.com/", nil
}
if strings.HasPrefix(targetURL, "/") {
return "https://hao858.com" + targetURL, nil
}
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
targetURL = "https://" + targetURL
}
u, err := url.Parse(targetURL)
if err != nil {
return "", errors.New("目标落地页地址不合法")
}
host := strings.ToLower(u.Hostname())
if host != "hao858.com" && !strings.HasSuffix(host, ".hao858.com") && host != "localhost" && host != "127.0.0.1" {
return "", ErrDisallowedTargetDomain
}
return targetURL, nil
}
type Service struct {
repo *Repository
redis *redis.Client
@@ -54,11 +77,9 @@ func (s *Service) CreateLink(ctx context.Context, adminID uint64, req CreateTrac
return nil, ErrCodeAlreadyExists
}
targetURL := strings.TrimSpace(req.TargetURL)
if targetURL == "" {
targetURL = "https://hao858.com/"
} else if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
targetURL = "https://" + targetURL
targetURL, err := validateTargetURL(req.TargetURL)
if err != nil {
return nil, err
}
channelType := strings.TrimSpace(req.ChannelType)
@@ -92,11 +113,9 @@ func (s *Service) UpdateLink(ctx context.Context, id uint64, req UpdateTrackingL
return ErrLinkNotFound
}
targetURL := strings.TrimSpace(req.TargetURL)
if targetURL == "" {
targetURL = "https://hao858.com/"
} else if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
targetURL = "https://" + targetURL
targetURL, err := validateTargetURL(req.TargetURL)
if err != nil {
return err
}
channelType := strings.TrimSpace(req.ChannelType)
@@ -151,9 +170,30 @@ func (s *Service) ResolveRedirect(ctx context.Context, code, ip, ua, referer, vi
// 异步记录点击流水,防止写库阻塞跳转
deviceType, os, browser := parseUserAgent(ua)
go func() {
defer func() {
if r := recover(); r != nil {
// 保护主进程,防止异步协程 panic
}
}()
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 防刷频控:同一个访客/IP在 5 秒内重复点击相同短链,不重复记录流水
if s.redis != nil {
dedupID := visitorID
if dedupID == "" {
dedupID = ip
}
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
}
}
}
click := &AdTrackingClick{
LinkID: link.ID,
Code: link.Code,