Merge branch 'main' into feature/distribution-system

合入 main 的推广追踪(adtracking)模块,并解决与分销业务的 8 处冲突。

冲突解决原则:两边的字段、菜单与路由全部保留,不删任何一方。

后端:
- model/order.go:保留分销字段注释块,追加 SourceTrackCode
- model/user.go:追加 RegisterTrackCode;TokenVersion 默认值保持 1
  (与本库 users.token_version 列默认值一致)
- auth/repository.go:两处建号结构体同时写入 RegisterTrackCode 与 TokenVersion
- order/dto.go:OrderDTO 同时暴露 distribution_site_id 与 source_track_code;
  CreateRequest 同时接受 distribution_site_code 与 track_code
- order/presenter.go:同时映射 DistributionSiteID 与 SourceTrackCode

前端:
- orders/api/orders.ts:createOrder 同时携带 distribution_site_code 与 track_code
- layouts/AdminLayout.vue:分销管理菜单组与推广追踪菜单组并列保留
- router/adminRoutes.ts:分销 4 条路由与 tracking-links 路由并列保留

迁移:feature 侧 000064-000067 已在前一提交顺延为 000065-000068,
为 main 的 000064_ad_tracking 让号,合并后编号 000063→000068 连续无重复。
This commit is contained in:
yml2213
2026-09-10 13:06:18 +08:00
28 changed files with 2461 additions and 55 deletions
+9 -7
View File
@@ -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
@@ -189,7 +191,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
+41 -19
View File
@@ -3,6 +3,8 @@ package auth
import (
"context"
"errors"
"regexp"
"strings"
"time"
"hfb_sys/backend/internal/model"
@@ -11,6 +13,16 @@ import (
"gorm.io/gorm/clause"
)
var trackCodePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`)
func sanitizeTrackCode(code string) string {
s := strings.TrimSpace(code)
if len(s) == 0 || len(s) > 64 || !trackCodePattern.MatchString(s) {
return ""
}
return s
}
type UserRepository struct {
db *gorm.DB
}
@@ -57,17 +69,22 @@ 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 = sanitizeTrackCode(trackCode[0])
}
user := model.User{
Phone: phone,
Nickname: "用户" + phone[len(phone)-4:],
RealnameStatus: "unverified",
RiskStatus: "normal",
CreditScore: 100,
Status: "active",
TokenVersion: 1,
LastLoginAt: &now,
Phone: phone,
Nickname: "用户" + phone[len(phone)-4:],
RealnameStatus: "unverified",
RiskStatus: "normal",
CreditScore: 100,
Status: "active",
RegisterTrackCode: code,
TokenVersion: 1,
LastLoginAt: &now,
}
err := r.db.WithContext(ctx).Clauses(clause.OnConflict{
@@ -106,18 +123,23 @@ func (r *UserRepository) RevokeTokens(ctx context.Context, userID uint64) error
UpdateColumn("token_version", gorm.Expr("token_version + 1")).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 = sanitizeTrackCode(trackCode[0])
}
user := model.User{
Phone: phone,
PasswordHash: hash,
Nickname: "用户" + phone[len(phone)-4:],
RealnameStatus: "unverified",
RiskStatus: "normal",
CreditScore: 100,
Status: "active",
TokenVersion: 1,
LastLoginAt: &now,
Phone: phone,
PasswordHash: hash,
Nickname: "用户" + phone[len(phone)-4:],
RealnameStatus: "unverified",
RiskStatus: "normal",
CreditScore: 100,
Status: "active",
RegisterTrackCode: code,
TokenVersion: 1,
LastLoginAt: &now,
}
err := r.db.WithContext(ctx).Create(&user).Error
+4 -4
View File
@@ -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
}
@@ -349,7 +349,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
}
@@ -382,7 +382,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