feat(adtracking): 新增推广链接追踪与数据大盘,支持自定义短码、注册与下单归因及二维码物料生成
1. 数据库新增 000064_ad_tracking.sql 迁移,创建 ad_tracking_links 和 ad_tracking_clicks 表,并在 users、rental_orders 增加归因字段 2. 后端新增 adtracking 模块,挂载 GET /r/:code 302 智能重定向、点击流水异步记录、转化率/GMV聚合统计及管理接口 3. 用户认证 (auth) 与订单创建 (order) 链路贯通,实现注册用户与订单成交推广短码全链路归因 4. 前端管理后台左侧新增【推广追踪】目录,支持大盘指标总览、推广链接 CRUD、状态启停、一键复制短链及二维码下载 (PNG)
This commit is contained in:
@@ -21,8 +21,9 @@ type SendSMSRequest struct {
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Code string `json:"code" binding:"required,len=6"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Code string `json:"code" binding:"required,len=6"`
|
||||
TrackCode string `json:"track_code"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
@@ -40,9 +41,10 @@ type SetPasswordRequest struct {
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Code string `json:"code" binding:"required,len=6"`
|
||||
Password string `json:"password" binding:"required,min=8,max=20"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Code string `json:"code" binding:"required,len=6"`
|
||||
Password string `json:"password" binding:"required,min=8,max=20"`
|
||||
TrackCode string `json:"track_code"`
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
@@ -106,7 +108,7 @@ func (h *Handler) Login(c *gin.Context) {
|
||||
response.BadRequest(c, "手机号和验证码不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.service.LoginWithSMS(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code))
|
||||
result, err := h.service.LoginWithSMS(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code), strings.TrimSpace(req.TrackCode))
|
||||
if err != nil {
|
||||
writeAuthError(c, err)
|
||||
return
|
||||
@@ -176,7 +178,7 @@ func (h *Handler) Register(c *gin.Context) {
|
||||
response.BadRequest(c, "手机号、验证码和密码不能为空,密码长度 8-20 位")
|
||||
return
|
||||
}
|
||||
result, err := h.service.RegisterWithPassword(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code), req.Password)
|
||||
result, err := h.service.RegisterWithPassword(c.Request.Context(), strings.TrimSpace(req.Phone), strings.TrimSpace(req.Code), req.Password, strings.TrimSpace(req.TrackCode))
|
||||
if err != nil {
|
||||
writeAuthError(c, err)
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -57,16 +58,21 @@ func (r *UserRepository) UpdateProfile(ctx context.Context, id uint64, nickname
|
||||
return r.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindOrCreateByPhone(ctx context.Context, phone string) (*model.User, error) {
|
||||
func (r *UserRepository) FindOrCreateByPhone(ctx context.Context, phone string, trackCode ...string) (*model.User, error) {
|
||||
now := time.Now()
|
||||
var code string
|
||||
if len(trackCode) > 0 {
|
||||
code = strings.TrimSpace(trackCode[0])
|
||||
}
|
||||
user := model.User{
|
||||
Phone: phone,
|
||||
Nickname: "用户" + phone[len(phone)-4:],
|
||||
RealnameStatus: "unverified",
|
||||
RiskStatus: "normal",
|
||||
CreditScore: 100,
|
||||
Status: "active",
|
||||
LastLoginAt: &now,
|
||||
Phone: phone,
|
||||
Nickname: "用户" + phone[len(phone)-4:],
|
||||
RealnameStatus: "unverified",
|
||||
RiskStatus: "normal",
|
||||
CreditScore: 100,
|
||||
Status: "active",
|
||||
RegisterTrackCode: code,
|
||||
LastLoginAt: &now,
|
||||
}
|
||||
|
||||
err := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
@@ -96,17 +102,22 @@ func (r *UserRepository) SetPassword(ctx context.Context, userID uint64, hash st
|
||||
return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", userID).Update("password_hash", hash).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) RegisterWithPassword(ctx context.Context, phone string, hash string) (*model.User, error) {
|
||||
func (r *UserRepository) RegisterWithPassword(ctx context.Context, phone string, hash string, trackCode ...string) (*model.User, error) {
|
||||
now := time.Now()
|
||||
var code string
|
||||
if len(trackCode) > 0 {
|
||||
code = strings.TrimSpace(trackCode[0])
|
||||
}
|
||||
user := model.User{
|
||||
Phone: phone,
|
||||
PasswordHash: hash,
|
||||
Nickname: "用户" + phone[len(phone)-4:],
|
||||
RealnameStatus: "unverified",
|
||||
RiskStatus: "normal",
|
||||
CreditScore: 100,
|
||||
Status: "active",
|
||||
LastLoginAt: &now,
|
||||
Phone: phone,
|
||||
PasswordHash: hash,
|
||||
Nickname: "用户" + phone[len(phone)-4:],
|
||||
RealnameStatus: "unverified",
|
||||
RiskStatus: "normal",
|
||||
CreditScore: 100,
|
||||
Status: "active",
|
||||
RegisterTrackCode: code,
|
||||
LastLoginAt: &now,
|
||||
}
|
||||
|
||||
err := r.db.WithContext(ctx).Create(&user).Error
|
||||
|
||||
@@ -157,7 +157,7 @@ func (s *Service) SendSMSCode(ctx context.Context, phone string, captchaID strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) (LoginResult, error) {
|
||||
func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string, trackCode ...string) (LoginResult, error) {
|
||||
if s.redis == nil || s.users == nil {
|
||||
return LoginResult{}, ErrDependencyUnavailable
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) (
|
||||
return LoginResult{}, err
|
||||
}
|
||||
|
||||
user, err := s.users.FindOrCreateByPhone(ctx, phone)
|
||||
user, err := s.users.FindOrCreateByPhone(ctx, phone, trackCode...)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
@@ -342,7 +342,7 @@ func (s *Service) ResetPassword(ctx context.Context, phone, code, newPassword st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) RegisterWithPassword(ctx context.Context, phone, code, password string) (LoginResult, error) {
|
||||
func (s *Service) RegisterWithPassword(ctx context.Context, phone, code, password string, trackCode ...string) (LoginResult, error) {
|
||||
if s.redis == nil || s.users == nil {
|
||||
return LoginResult{}, ErrDependencyUnavailable
|
||||
}
|
||||
@@ -375,7 +375,7 @@ func (s *Service) RegisterWithPassword(ctx context.Context, phone, code, passwor
|
||||
return LoginResult{}, err
|
||||
}
|
||||
|
||||
user, err := s.users.RegisterWithPassword(ctx, phone, string(hash))
|
||||
user, err := s.users.RegisterWithPassword(ctx, phone, string(hash), trackCode...)
|
||||
if err != nil {
|
||||
// 并发场景下仍可能因唯一索引冲突,统一按"已注册"处理
|
||||
return LoginResult{}, ErrUserAlreadyExists
|
||||
|
||||
Reference in New Issue
Block a user