88 lines
3.4 KiB
Go

package user
import (
"context"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
invitedomain "hyapp/services/user-service/internal/domain/invite"
userdomain "hyapp/services/user-service/internal/domain/user"
)
// SearchInviteReferrer 通过邀请码搜索可绑定邀请人。
// 邀请归因按 app 和区域隔离;国家只是用户资料快照,不能把同区域的跨国家邀请误判为不存在。
func (s *Service) SearchInviteReferrer(ctx context.Context, userID int64, inviteCode string) (userdomain.User, error) {
inviteCode = invitedomain.NormalizeCode(inviteCode)
if userID <= 0 {
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if !invitedomain.ValidCode(inviteCode) {
// 对外搜索入口只接受合法邀请码;非法输入直接拒绝,不落入 NotFound 分支。
return userdomain.User{}, xerr.New(xerr.InvalidInviteCode, "invalid invite code")
}
if s.userRepository == nil {
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
current, err := s.userRepository.GetUser(ctx, userID)
if err != nil {
return userdomain.User{}, err
}
if !current.ProfileCompleted {
// H5 绑定入口只服务已完成资料用户;注册资料页绑定走 CompleteOnboarding 的原子写链路。
return userdomain.User{}, xerr.New(xerr.ProfileRequired, "profile required")
}
inviter, err := s.userRepository.FindInviteReferrerByCode(ctx, appcode.FromContext(ctx), inviteCode)
if err != nil {
return userdomain.User{}, err
}
if err := validateInviteReferrerPair(current, inviter); err != nil {
return userdomain.User{}, err
}
return s.refreshExpiredUser(ctx, inviter, "")
}
// BindInviteReferrer 原子绑定当前用户的邀请人。
// H5 只能传用户输入的邀请码;归属用户由 repository 在事务内重新解析并复核。
func (s *Service) BindInviteReferrer(ctx context.Context, userID int64, inviteCode string, requestID string) (userdomain.User, error) {
inviteCode = invitedomain.NormalizeCode(inviteCode)
if userID <= 0 {
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if !invitedomain.ValidCode(inviteCode) {
return userdomain.User{}, xerr.New(xerr.InvalidInviteCode, "invalid invite code")
}
if s.userRepository == nil {
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.BindInviteReferrer(ctx, userdomain.BindInviteReferrerCommand{
AppCode: appcode.FromContext(ctx),
InvitedUserID: userID,
InviteCode: inviteCode,
RequestID: strings.TrimSpace(requestID),
BoundAtMs: s.now().UnixMilli(),
})
}
func validateInviteReferrerPair(invited userdomain.User, inviter userdomain.User) error {
if invited.UserID <= 0 || inviter.UserID <= 0 {
return xerr.New(xerr.NotFound, "inviter not found")
}
if invited.UserID == inviter.UserID {
// 自邀不进入邀请码解析,避免把当前用户自己的邀请码可用性暴露出去。
return xerr.New(xerr.InvalidInviteCode, "invalid invite code")
}
if !inviter.ProfileCompleted || !inviter.CanLogin() {
return xerr.New(xerr.NotFound, "inviter not found")
}
if invited.RegionID != inviter.RegionID {
// 产品边界是同区域邀请;跨区域统一按未找到处理,避免 H5 用搜索接口枚举其他区域用户。
return xerr.New(xerr.NotFound, "inviter not found")
}
return nil
}