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
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();
|
|
}
|
|
}
|