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.
132 lines
6.0 KiB
132 lines
6.0 KiB
package com.mydreamjob.application;
|
|
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.job.Job;
|
|
import com.mydreamjob.job.JobRepository;
|
|
import com.mydreamjob.privacy.ResumeVisibility;
|
|
import com.mydreamjob.privacy.StudentPrivacySettings;
|
|
import com.mydreamjob.privacy.StudentPrivacySettingsRepository;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* S03-B 企业简历访问授权(失败关闭)。
|
|
*
|
|
* <p>授权顺序严格按设计 §4.2:当前 COMPANY 用户 → 当前 Company → Application+Job →
|
|
* Job.companyId → Student → privacy setting。归属信息全部由服务端推导,
|
|
* <strong>不信任任何客户端传入的 companyId / studentUserId</strong>。</p>
|
|
*
|
|
* <p>决策表(设计 §4.3):</p>
|
|
* <ul>
|
|
* <li>无设置记录 / null / PRIVATE:拒绝(按 PRIVATE 处理)</li>
|
|
* <li>APPLIED_COMPANIES:岗位必须属于当前企业,且投递状态为
|
|
* SUBMITTED/VIEWED/INTERVIEW/HIRED;WITHDRAWN/REJECTED 拒绝</li>
|
|
* <li>VERIFIED_COMPANIES:当前企业 {@code verified=true} <strong>且</strong>拥有该投递岗位;
|
|
* 投递状态不额外限制</li>
|
|
* </ul>
|
|
*
|
|
* <p>本类修复了此前 {@code PrivacySettingsService.canCompanyViewResume} 只校验学生与状态、
|
|
* 不校验企业归属的缺陷。</p>
|
|
*/
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
public class CompanyResumeAccessPolicy {
|
|
|
|
/**
|
|
* {@code APPLIED_COMPANIES} 允许的投递状态:**严格白名单**。
|
|
*
|
|
* <p>不得改为「不是拒绝状态就允许」的黑名单逻辑 —— 那会让 {@code null}、空白、
|
|
* 未知状态或大小写变体(如 {@code "submitted"})失败开放。状态比较区分大小写。</p>
|
|
*/
|
|
static final Set<String> APPLIED_COMPANY_VISIBLE_STATUSES =
|
|
Set.of("SUBMITTED", "VIEWED", "INTERVIEW", "HIRED");
|
|
|
|
private final CompanyRepository companyRepository;
|
|
private final ApplicationRepository applicationRepository;
|
|
private final JobRepository jobRepository;
|
|
private final StudentPrivacySettingsRepository privacyRepository;
|
|
|
|
/** 授权通过后返回候选人用户 ID;这是后续读取简历的唯一依据。 */
|
|
public record Decision(Long studentUserId) {
|
|
}
|
|
|
|
/**
|
|
* 企业下载接口使用的授权入口:仅凭当前登录公司用户与路径中的 applicationId 完成全部判定。
|
|
*
|
|
* @throws BusinessException 未拥有企业资料 / 跨企业 / 隐私拒绝 → {@code FORBIDDEN(2002)};
|
|
* 投递不存在 → {@code APPLICATION_NOT_FOUND(8002)}
|
|
*/
|
|
public Decision authorizeForDownload(Long companyUserId, Long applicationId) {
|
|
if (companyUserId == null || applicationId == null) {
|
|
throw new BusinessException(ErrorCode.FORBIDDEN);
|
|
}
|
|
Company company = companyRepository.findByOwnerUserId(companyUserId).orElse(null);
|
|
if (company == null) {
|
|
throw new BusinessException(ErrorCode.FORBIDDEN);
|
|
}
|
|
Application application = applicationRepository.findById(applicationId)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.APPLICATION_NOT_FOUND));
|
|
|
|
if (!isVisibleToCompanyInternal(application, company.getId())) {
|
|
throw new BusinessException(ErrorCode.FORBIDDEN);
|
|
}
|
|
return new Decision(application.getStudentUserId());
|
|
}
|
|
|
|
/**
|
|
* 既有布尔语义的授权判定(供 {@code PrivacySettingsService} 委托)。
|
|
*
|
|
* <p>调用方提供的 {@code studentUserId} 只用于一致性校验:必须与投递记录一致,
|
|
* 否则视为不可信并拒绝。</p>
|
|
*/
|
|
public boolean isVisible(Long studentUserId, Long companyId, Long applicationId) {
|
|
if (studentUserId == null || companyId == null || applicationId == null) {
|
|
return false;
|
|
}
|
|
Application application = applicationRepository.findById(applicationId).orElse(null);
|
|
if (application == null || !studentUserId.equals(application.getStudentUserId())) {
|
|
return false;
|
|
}
|
|
return isVisibleToCompanyInternal(application, companyId);
|
|
}
|
|
|
|
/**
|
|
* 核心判定:投递所属岗位必须属于该企业,再按学生隐私设置决定可见性。
|
|
*/
|
|
private boolean isVisibleToCompanyInternal(Application application, Long companyId) {
|
|
Job job = jobRepository.findById(application.getJobId()).orElse(null);
|
|
if (job == null || !companyId.equals(job.getCompanyId())) {
|
|
// 岗位不存在或不属于当前企业:一律拒绝(含跨企业投递)。
|
|
return false;
|
|
}
|
|
|
|
ResumeVisibility visibility = visibilityOf(application.getStudentUserId());
|
|
switch (visibility) {
|
|
case APPLIED_COMPANIES:
|
|
// 白名单判定(失败关闭):null / 空白 / 未知 / 大小写变体一律拒绝。
|
|
// 先做 null 判断,避免 Set.of(...).contains(null) 抛 NPE。
|
|
return application.getStatus() != null
|
|
&& APPLIED_COMPANY_VISIBLE_STATUSES.contains(application.getStatus());
|
|
case VERIFIED_COMPANIES:
|
|
Company company = companyRepository.findById(companyId).orElse(null);
|
|
return company != null && Boolean.TRUE.equals(company.getVerified());
|
|
case PRIVATE:
|
|
default:
|
|
// 无设置记录、null 或 PRIVATE:按 PRIVATE 处理。
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private ResumeVisibility visibilityOf(Long studentUserId) {
|
|
StudentPrivacySettings settings = privacyRepository.findByStudentUserId(studentUserId).orElse(null);
|
|
if (settings == null || settings.getResumeVisibility() == null) {
|
|
return ResumeVisibility.PRIVATE;
|
|
}
|
|
return settings.getResumeVisibility();
|
|
}
|
|
}
|
|
|