From 0d3a116820ce5118e3e759756a5be20bce57c73d Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Fri, 29 May 2026 12:37:37 +0800 Subject: [PATCH] f --- .../dto/responses/subordinate_responses.go | 2 +- .../subordinate_application_service_impl.go | 38 +++++++++++++------ .../subordinate/entities/invitation.go | 3 +- .../subordinate_repository_interface.go | 1 + .../gorm_subordinate_repository.go | 16 ++++++++ 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/internal/application/subordinate/dto/responses/subordinate_responses.go b/internal/application/subordinate/dto/responses/subordinate_responses.go index 0aaefd7..c019b13 100644 --- a/internal/application/subordinate/dto/responses/subordinate_responses.go +++ b/internal/application/subordinate/dto/responses/subordinate_responses.go @@ -4,7 +4,7 @@ import "time" // CreateInvitationResponse 创建邀请 type CreateInvitationResponse struct { - InviteToken string `json:"invite_token" description:"仅返回一次,请转达被邀请人"` + InviteToken string `json:"invite_token" description:"主账号固定邀请码,可重复使用"` InviteURL string `json:"invite_url" description:"子站注册完整链接"` ExpiresAt time.Time `json:"expires_at"` InvitationID string `json:"invitation_id"` diff --git a/internal/application/subordinate/subordinate_application_service_impl.go b/internal/application/subordinate/subordinate_application_service_impl.go index 62050f8..16659f7 100644 --- a/internal/application/subordinate/subordinate_application_service_impl.go +++ b/internal/application/subordinate/subordinate_application_service_impl.go @@ -81,8 +81,15 @@ func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Contex if inv == nil { return fmt.Errorf("邀请码无效") } - if inv.Status != subentities.InvitationStatusPending { - return fmt.Errorf("邀请码已使用或已失效") + switch inv.Status { + case subentities.InvitationStatusRevoked: + return fmt.Errorf("邀请码已失效") + case subentities.InvitationStatusConsumed: + return fmt.Errorf("邀请码已失效,请联系主账号获取新邀请码") + case subentities.InvitationStatusPending: + // 固定邀请码可重复使用,注册后不核销 + default: + return fmt.Errorf("邀请码无效") } now := time.Now() if now.After(inv.ExpiresAt) { @@ -105,15 +112,6 @@ func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Contex 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} return nil }) @@ -123,8 +121,19 @@ func (s *SubordinateApplicationServiceImpl) RegisterSubPortal(ctx context.Contex return resp, nil } -// CreateInvitation 主账号发邀请 +// CreateInvitation 获取或创建主账号固定邀请码(可重复使用) 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 if hours <= 0 { // 永久有效:设置100年后过期 @@ -136,6 +145,7 @@ func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context } inv := &subentities.SubordinateInvitation{ ParentUserID: cmd.ParentUserID, + Token: raw, TokenHash: hash, ExpiresAt: time.Now().Add(time.Duration(hours) * time.Hour), Status: subentities.InvitationStatusPending, @@ -143,6 +153,10 @@ func (s *SubordinateApplicationServiceImpl) CreateInvitation(ctx context.Context if err := s.subRepo.CreateInvitation(ctx, inv); err != nil { 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")) if base == "" { base = s.cfg.App.SubPortalBaseURL diff --git a/internal/domains/subordinate/entities/invitation.go b/internal/domains/subordinate/entities/invitation.go index cb4d2fe..6ad0a39 100644 --- a/internal/domains/subordinate/entities/invitation.go +++ b/internal/domains/subordinate/entities/invitation.go @@ -16,10 +16,11 @@ const ( InvitationStatusRevoked InvitationStatus = "revoked" ) -// SubordinateInvitation 主账号邀请记录(存 token 哈希) +// SubordinateInvitation 主账号邀请记录(主账号固定邀请码,可重复使用) type SubordinateInvitation struct { 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"` + Token string `gorm:"type:char(6)" json:"-" comment:"邀请码明文(6位)"` TokenHash string `gorm:"type:varchar(64);not null;uniqueIndex" json:"-" comment:"邀请码的SHA256(十六进制)"` ExpiresAt time.Time `gorm:"not null;index" json:"expires_at" comment:"过期时间"` Status InvitationStatus `gorm:"type:varchar(20);not null;default:pending" json:"status" comment:"状态"` diff --git a/internal/domains/subordinate/repositories/subordinate_repository_interface.go b/internal/domains/subordinate/repositories/subordinate_repository_interface.go index 8af8bfc..04982a7 100644 --- a/internal/domains/subordinate/repositories/subordinate_repository_interface.go +++ b/internal/domains/subordinate/repositories/subordinate_repository_interface.go @@ -12,6 +12,7 @@ type SubordinateRepository interface { CreateInvitation(ctx context.Context, inv *entities.SubordinateInvitation) error FindInvitationByTokenHash(ctx context.Context, tokenHash 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 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) diff --git a/internal/infrastructure/database/repositories/subordinate/gorm_subordinate_repository.go b/internal/infrastructure/database/repositories/subordinate/gorm_subordinate_repository.go index 07accf9..655d33e 100644 --- a/internal/infrastructure/database/repositories/subordinate/gorm_subordinate_repository.go +++ b/internal/infrastructure/database/repositories/subordinate/gorm_subordinate_repository.go @@ -52,6 +52,22 @@ func (r *GormSubordinateRepository) FindInvitationByTokenHash(ctx context.Contex 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 func (r *GormSubordinateRepository) FindInvitationByID(ctx context.Context, id string) (*entities.SubordinateInvitation, error) { var inv entities.SubordinateInvitation