f
This commit is contained in:
@@ -4,7 +4,7 @@ import "time"
|
|||||||
|
|
||||||
// CreateInvitationResponse 创建邀请
|
// CreateInvitationResponse 创建邀请
|
||||||
type CreateInvitationResponse struct {
|
type CreateInvitationResponse struct {
|
||||||
InviteToken string `json:"invite_token" description:"仅返回一次,请转达被邀请人"`
|
InviteToken string `json:"invite_token" description:"主账号固定邀请码,可重复使用"`
|
||||||
InviteURL string `json:"invite_url" description:"子站注册完整链接"`
|
InviteURL string `json:"invite_url" description:"子站注册完整链接"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
InvitationID string `json:"invitation_id"`
|
InvitationID string `json:"invitation_id"`
|
||||||
|
|||||||
@@ -81,8 +81,15 @@ func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Contex
|
|||||||
if inv == nil {
|
if inv == nil {
|
||||||
return fmt.Errorf("邀请码无效")
|
return fmt.Errorf("邀请码无效")
|
||||||
}
|
}
|
||||||
if inv.Status != subentities.InvitationStatusPending {
|
switch inv.Status {
|
||||||
return fmt.Errorf("邀请码已使用或已失效")
|
case subentities.InvitationStatusRevoked:
|
||||||
|
return fmt.Errorf("邀请码已失效")
|
||||||
|
case subentities.InvitationStatusConsumed:
|
||||||
|
return fmt.Errorf("邀请码已失效,请联系主账号获取新邀请码")
|
||||||
|
case subentities.InvitationStatusPending:
|
||||||
|
// 固定邀请码可重复使用,注册后不核销
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("邀请码无效")
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.After(inv.ExpiresAt) {
|
if now.After(inv.ExpiresAt) {
|
||||||
@@ -105,15 +112,6 @@ func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Contex
|
|||||||
return fmt.Errorf("注册失败,请重试或联系主账号")
|
return fmt.Errorf("注册失败,请重试或联系主账号")
|
||||||
}
|
}
|
||||||
|
|
||||||
consumed, consumeErr := s.subRepo.ConsumeInvitation(txCtx, inv.ID, u.ID, now)
|
|
||||||
if consumeErr != nil {
|
|
||||||
s.logger.Error("核销邀请失败", zap.Error(consumeErr), zap.String("user_id", u.ID))
|
|
||||||
return fmt.Errorf("注册失败,请重试或联系主账号")
|
|
||||||
}
|
|
||||||
if !consumed {
|
|
||||||
return fmt.Errorf("邀请码已使用或已失效")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = &responses.SubPortalRegisterResponse{ID: u.ID, Phone: u.Phone}
|
resp = &responses.SubPortalRegisterResponse{ID: u.ID, Phone: u.Phone}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -123,8 +121,19 @@ func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Contex
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateInvitation 主账号发邀请
|
// CreateInvitation 获取或创建主账号固定邀请码(可重复使用)
|
||||||
func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context, cmd *commands.CreateInvitationCommand) (*responses.CreateInvitationResponse, error) {
|
func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context, cmd *commands.CreateInvitationCommand) (*responses.CreateInvitationResponse, error) {
|
||||||
|
existing, err := s.subRepo.FindActiveInvitationByParent(ctx, cmd.ParentUserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing != nil {
|
||||||
|
if raw := strings.TrimSpace(existing.Token); raw != "" {
|
||||||
|
return s.buildInvitationResponse(existing, raw)
|
||||||
|
}
|
||||||
|
// 历史 pending 记录未存明文,无法找回,继续创建新的固定邀请码
|
||||||
|
}
|
||||||
|
|
||||||
hours := cmd.ExpiresInHours
|
hours := cmd.ExpiresInHours
|
||||||
if hours <= 0 {
|
if hours <= 0 {
|
||||||
// 永久有效:设置100年后过期
|
// 永久有效:设置100年后过期
|
||||||
@@ -136,6 +145,7 @@ func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context
|
|||||||
}
|
}
|
||||||
inv := &subentities.SubordinateInvitation{
|
inv := &subentities.SubordinateInvitation{
|
||||||
ParentUserID: cmd.ParentUserID,
|
ParentUserID: cmd.ParentUserID,
|
||||||
|
Token: raw,
|
||||||
TokenHash: hash,
|
TokenHash: hash,
|
||||||
ExpiresAt: time.Now().Add(time.Duration(hours) * time.Hour),
|
ExpiresAt: time.Now().Add(time.Duration(hours) * time.Hour),
|
||||||
Status: subentities.InvitationStatusPending,
|
Status: subentities.InvitationStatusPending,
|
||||||
@@ -143,6 +153,10 @@ func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context
|
|||||||
if err := s.subRepo.CreateInvitation(ctx, inv); err != nil {
|
if err := s.subRepo.CreateInvitation(ctx, inv); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return s.buildInvitationResponse(inv, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubordinateApplicationServiceImpl) buildInvitationResponse(inv *subentities.SubordinateInvitation, raw string) (*responses.CreateInvitationResponse, error) {
|
||||||
base := strings.TrimSpace(os.Getenv("SUB_PORTAL_BASE_URL"))
|
base := strings.TrimSpace(os.Getenv("SUB_PORTAL_BASE_URL"))
|
||||||
if base == "" {
|
if base == "" {
|
||||||
base = s.cfg.App.SubPortalBaseURL
|
base = s.cfg.App.SubPortalBaseURL
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ const (
|
|||||||
InvitationStatusRevoked InvitationStatus = "revoked"
|
InvitationStatusRevoked InvitationStatus = "revoked"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SubordinateInvitation 主账号邀请记录(存 token 哈希)
|
// SubordinateInvitation 主账号邀请记录(主账号固定邀请码,可重复使用)
|
||||||
type SubordinateInvitation struct {
|
type SubordinateInvitation struct {
|
||||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"唯一标识"`
|
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"唯一标识"`
|
||||||
ParentUserID string `gorm:"type:varchar(36);not null;index" json:"parent_user_id" comment:"主账号用户ID"`
|
ParentUserID string `gorm:"type:varchar(36);not null;index" json:"parent_user_id" comment:"主账号用户ID"`
|
||||||
|
Token string `gorm:"type:char(6)" json:"-" comment:"邀请码明文(6位)"`
|
||||||
TokenHash string `gorm:"type:varchar(64);not null;uniqueIndex" json:"-" comment:"邀请码的SHA256(十六进制)"`
|
TokenHash string `gorm:"type:varchar(64);not null;uniqueIndex" json:"-" comment:"邀请码的SHA256(十六进制)"`
|
||||||
ExpiresAt time.Time `gorm:"not null;index" json:"expires_at" comment:"过期时间"`
|
ExpiresAt time.Time `gorm:"not null;index" json:"expires_at" comment:"过期时间"`
|
||||||
Status InvitationStatus `gorm:"type:varchar(20);not null;default:pending" json:"status" comment:"状态"`
|
Status InvitationStatus `gorm:"type:varchar(20);not null;default:pending" json:"status" comment:"状态"`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type SubordinateRepository interface {
|
|||||||
CreateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error
|
CreateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error
|
||||||
FindInvitationByTokenHash(ctx context.Context, tokenHash string) (*entities.SubordinateInvitation, error)
|
FindInvitationByTokenHash(ctx context.Context, tokenHash string) (*entities.SubordinateInvitation, error)
|
||||||
FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error)
|
FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error)
|
||||||
|
FindActiveInvitationByParent(ctx context.Context, parentUserID string) (*entities.SubordinateInvitation, error)
|
||||||
UpdateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error
|
UpdateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error
|
||||||
ConsumeInvitation(ctx context.Context, invitationID, childUserID string, consumedAt time.Time) (bool, error)
|
ConsumeInvitation(ctx context.Context, invitationID, childUserID string, consumedAt time.Time) (bool, error)
|
||||||
ListInvitationsByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.SubordinateInvitation, int64, error)
|
ListInvitationsByParent(ctx context.Context, parentUserID string, limit, offset int) ([]*entities.SubordinateInvitation, int64, error)
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ func (r *GormSubordinateRepository) FindInvitationByTokenHash(ctx context.Contex
|
|||||||
return &inv, nil
|
return &inv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindActiveInvitationByParent 查询主账号当前可用的固定邀请码(pending 且未过期)
|
||||||
|
func (r *GormSubordinateRepository) FindActiveInvitationByParent(ctx context.Context, parentUserID string) (*entities.SubordinateInvitation, error) {
|
||||||
|
var inv entities.SubordinateInvitation
|
||||||
|
err := r.withCtx(ctx).
|
||||||
|
Where("parent_user_id = ? AND status = ? AND expires_at > ?", parentUserID, entities.InvitationStatusPending, time.Now()).
|
||||||
|
Order("created_at ASC").
|
||||||
|
First(&inv).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &inv, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FindInvitationByID 按ID
|
// FindInvitationByID 按ID
|
||||||
func (r *GormSubordinateRepository) FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error) {
|
func (r *GormSubordinateRepository) FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error) {
|
||||||
var inv entities.SubordinateInvitation
|
var inv entities.SubordinateInvitation
|
||||||
|
|||||||
Reference in New Issue
Block a user