package com.mydreamjob.auth.identity; import com.mydreamjob.audit.AuditContext; import com.mydreamjob.audit.AuditService; import com.mydreamjob.auth.*; import com.mydreamjob.auth.company.CompanyRegistrationProperties; import com.mydreamjob.auth.company.CompanyRegistrationRequest.CompanyDetails; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyRepository; import com.mydreamjob.company.CompanyOnboardingService; import com.mydreamjob.deletion.DeletionStatus; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.deletion.AccountDeletionRepository; import jakarta.validation.Validator; import lombok.RequiredArgsConstructor; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @Service @RequiredArgsConstructor public class IdentitySelectionService { private final UserRepository users; private final CompanyRepository companies; private final AuthSessionService sessions; private final AuditService audit; private final AccountDeletionRepository deletions; private final CompanyRegistrationProperties properties; private final CompanyOnboardingService onboarding; private final Validator validator; private final Clock clock; @Transactional public IdentitySelectionResponse select(Long userId, String sessionId, IdentitySelectionRequest request, AuditContext context) { if (request == null || !validator.validate(request).isEmpty()) { throw new BusinessException(ErrorCode.BAD_REQUEST); } try { // 与刷新/密码变更相同:users -> auth_tokens;锁内重新确认会话,关闭 TOCTOU。 User user = users.findByIdForUpdate(userId).orElseThrow(InvalidTokenException::new); Instant now = clock.instant(); if (!Boolean.TRUE.equals(user.getActive()) || !sessions.isSessionActive(userId, sessionId, now)) { throw new InvalidTokenException(); } if (deletions.findByUserId(userId).filter(d -> d.getStatus() != DeletionStatus.NONE).isPresent()) { throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT); } User.Role target = request.getIdentity() == IdentitySelectionRequest.Identity.STUDENT ? User.Role.STUDENT : User.Role.COMPANY; if (user.getRole() != User.Role.UNASSIGNED) { if (user.getRole() != target || user.getRoles() == null || !user.getRoles().equals(Set.of(target))) { throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT); } Company company = target == User.Role.COMPANY ? companies.findByOwnerUserId(userId) .orElseThrow(() -> new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT)) : null; // 不重复更改资料/颁发会话/审计;客户端可使用已有凭证或重新登录。 return response(user, company, null, true); } if (user.getRoles() == null || !user.getRoles().isEmpty() || companies.existsByOwnerUserId(userId)) { throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT); } if (target == User.Role.COMPANY && !properties.isEnabled()) { throw new BusinessException(ErrorCode.COMPANY_REGISTRATION_DISABLED); } user.setRole(target); user.setRoles(new HashSet<>(Set.of(target))); users.saveAndFlush(user); Company company = null; if (target == User.Role.COMPANY) { CompanyDetails d = request.getCompany(); company = companies.saveAndFlush(Company.builder().ownerUserId(userId).name(d.getName()) .logoUrl(d.getLogoUrl()).industry(d.getIndustry()).description(d.getDescription()) .website(d.getWebsite()).address(d.getAddress()).verified(false).build()); } // modifying 会清理 persistence context;上面的角色与资料已 flush,锁仍持有。 sessions.revokeAllUserSessions(userId, "IDENTITY_SELECTED", now); AuthSessionService.SessionTokens tokens = sessions.createSession(user, now, context); audit.logRequired(userId, "IDENTITY_SELECTED", "USER", userId, "{\"result\":\"SUCCESS\"}", context, LocalDateTime.ofInstant(now, AuthSessionService.BUSINESS_ZONE)); return response(user, company, tokens, false); } catch (DataIntegrityViolationException e) { throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT); } catch (DataAccessException e) { throw new BusinessException(ErrorCode.IDENTITY_SELECTION_DEPENDENCY_UNAVAILABLE); } } private IdentitySelectionResponse response(User user, Company company, AuthSessionService.SessionTokens tokens, boolean reauthenticate) { return new IdentitySelectionResponse(user.getId(), user.getRole().name(), false, company == null ? null : company.getId(), company == null ? null : (tokens == null ? onboarding.get(user.getId()).onboardingStatus() : "UNVERIFIED"), tokens == null ? null : tokens.accessToken(), tokens == null ? null : tokens.refreshToken(), tokens == null ? null : AuthService.EXPIRES_IN_SECONDS, reauthenticate); } }