You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

141 lines
6.2 KiB

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;
/**
* 学生隐私设置服务。
*
* <p>身份由调用方从 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:委托给失败关闭的授权策略)。
*
* <p>判定需要同时满足:投递存在、投递学生与传入 {@code studentUserId} 一致、
* <strong>投递所属岗位属于 {@code companyId}</strong>,再按隐私设置决定:</p>
*
* <ul>
* <li>无记录 / null / PRIVATE:不可见</li>
* <li>APPLIED_COMPANIES:岗位属于该企业且投递状态为
* SUBMITTED/VIEWED/INTERVIEW/HIRED</li>
* <li>VERIFIED_COMPANIES:该企业 {@code verified=true} 且拥有该投递岗位</li>
* </ul>
*/
public boolean canCompanyViewResume(Long studentUserId, Long companyId, Long applicationId) {
return companyResumeAccessPolicy.isVisible(studentUserId, companyId, applicationId);
}
private void validate(PrivacySettingsUpdateRequest req) {
if (req.getResumeVisibility() == null || req.getContactVisible() == null) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
}
private PrivacySettingsResponse toResponse(StudentPrivacySettings s, Long userId) {
if (s == null) {
return PrivacySettingsResponse.builder()
.studentUserId(userId)
.resumeVisibility(ResumeVisibility.PRIVATE)
.contactVisible(false)
.version(0L)
.build();
}
return PrivacySettingsResponse.builder()
.id(s.getId())
.studentUserId(s.getStudentUserId())
.resumeVisibility(s.getResumeVisibility())
.contactVisible(s.getContactVisible())
.version(s.getVersion())
.createdAt(s.getCreatedAt())
.updatedAt(s.getUpdatedAt())
.build();
}
}