package com.mydreamjob.privacy; import com.mydreamjob.application.CompanyResumeAccessPolicy; import com.mydreamjob.audit.AuditService; import com.mydreamjob.auth.User; import com.mydreamjob.auth.UserRepository; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.privacy.dto.PrivacySettingsResponse; import com.mydreamjob.privacy.dto.PrivacySettingsUpdateRequest; import com.mydreamjob.security.UserRoleGuard; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 学生隐私设置服务。 * *
身份由调用方从 JWT/SecurityContext 解析后传入 userId,本服务负责校验 STUDENT 角色; * 不接受客户端指定的 userId 身份参数。联系方式的可见性策略方法供企业侧脱敏接入使用。 */ @Slf4j @Service @RequiredArgsConstructor public class PrivacySettingsService { private final com.mydreamjob.security.UserRoleGuard userRoleGuard; private final StudentPrivacySettingsRepository privacyRepository; private final UserRepository userRepository; private final AuditService auditService; /** * S03-B:企业简历可见性统一由失败关闭的授权策略判定。 * 策略强制校验「投递所属岗位属于该企业」,修复了此前只校验学生与状态、 * 不校验企业归属的缺陷。 */ private final CompanyResumeAccessPolicy companyResumeAccessPolicy; /** 校验当前用户必须为 STUDENT,否则抛 FORBIDDEN。 */ public void assertStudentRole(Long userId) { userRoleGuard.requireStudent(userId); } /** * 获取当前学生的隐私设置;无记录时返回默认对象(PRIVATE / contactVisible=false / version=0), * 不自动创建数据库记录。 */ @Transactional(readOnly = true) public PrivacySettingsResponse getCurrentUserSettings(Long userId) { assertStudentRole(userId); StudentPrivacySettings settings = privacyRepository.findByStudentUserId(userId).orElse(null); return toResponse(settings, userId); } /** * 更新当前学生的隐私设置,执行 upsert,确保同一学生至多一行。 * version 不匹配时抛 PRIVACY_SETTINGS_CONCURRENT_UPDATE(9201 → HTTP 409)。 */ @Transactional public PrivacySettingsResponse updateCurrentUserSettings(Long userId, PrivacySettingsUpdateRequest req) { assertStudentRole(userId); validate(req); StudentPrivacySettings existing = privacyRepository.findByStudentUserId(userId).orElse(null); if (existing != null && req.getVersion() != null) { long expected = req.getVersion(); long current = existing.getVersion() == null ? 0L : existing.getVersion(); if (current != expected) { throw new BusinessException(ErrorCode.PRIVACY_SETTINGS_CONCURRENT_UPDATE); } } try { StudentPrivacySettings saved; if (existing == null) { saved = privacyRepository.saveAndFlush(StudentPrivacySettings.builder() .studentUserId(userId) .resumeVisibility(req.getResumeVisibility()) .contactVisible(req.getContactVisible()) .version(0L) .build()); } else { existing.setResumeVisibility(req.getResumeVisibility()); existing.setContactVisible(req.getContactVisible()); saved = privacyRepository.saveAndFlush(existing); } // 审计:仅记录事件,不写入 phone/email/password/jwt/resumeUrl 等敏感信息 auditService.log(userId, "UPDATE_PRIVACY_SETTINGS", "STUDENT_PRIVACY_SETTINGS", saved.getId(), "privacy settings updated", null); return toResponse(saved, userId); } catch (ObjectOptimisticLockingFailureException | DataIntegrityViolationException e) { log.warn("Concurrent privacy settings update conflict for userId={}", userId, e); throw new BusinessException(ErrorCode.PRIVACY_SETTINGS_CONCURRENT_UPDATE); } } /** * 企业是否可查看某学生的简历(S03-B:委托给失败关闭的授权策略)。 * *
判定需要同时满足:投递存在、投递学生与传入 {@code studentUserId} 一致、 * 投递所属岗位属于 {@code companyId},再按隐私设置决定:
* *