package com.mydreamjob.application;
import com.mydreamjob.application.dto.CompanyResumeFile;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.profile.StudentProfile;
import com.mydreamjob.profile.StudentProfileRepository;
import com.mydreamjob.profile.storage.ResumeFilenameSanitizer;
import com.mydreamjob.profile.storage.ResumeStorageService;
import com.mydreamjob.profile.storage.ResumeOwnershipVerifier;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Set;
/**
* S03-B 企业读取候选人简历文件。
*
*
本类只负责「授权已通过之后」的文件读取与响应构建:
* 授权全部由 {@link CompanyResumeAccessPolicy} 完成,**任何拒绝路径都不会走到
* {@link ResumeStorageService#read}。
*
* 简历正文、存储路径、原始文件名与学生标识都不写入日志、审计或异常。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CompanyResumeService {
/** Content-Type 白名单:只能来自上传时已校验的三种简历类型。 */
static final Set ALLOWED_CONTENT_TYPES = Set.of(
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document");
static final String FALLBACK_CONTENT_TYPE = "application/octet-stream";
private final CompanyResumeAccessPolicy accessPolicy;
private final StudentProfileRepository profileRepository;
private final ResumeStorageService resumeStorageService;
private final ResumeOwnershipVerifier resumeOwnership;
/**
* 读取该投递对应候选人的简历。
*
* @throws BusinessException 授权失败({@code FORBIDDEN} / {@code APPLICATION_NOT_FOUND}),
* 或授权通过但学生没有简历({@code PROFILE_NOT_FOUND})
*/
@Transactional(readOnly = true)
public CompanyResumeFile load(Long companyUserId, Long applicationId) {
// 先授权,后读文件:拒绝路径绝不触碰存储。
CompanyResumeAccessPolicy.Decision decision =
accessPolicy.authorizeForDownload(companyUserId, applicationId);
StudentProfile profile = profileRepository.findByUserId(decision.studentUserId())
.orElseThrow(() -> new BusinessException(ErrorCode.PROFILE_NOT_FOUND));
String storagePath = profile.getResumeUrl();
if (storagePath == null || storagePath.isBlank()) {
throw new BusinessException(ErrorCode.PROFILE_NOT_FOUND);
}
// 企业获得的是该候选人的读取授权,不能将历史污染引用当作文件归属证明。
resumeOwnership.assertOwnedBy(storagePath, decision.studentUserId());
byte[] content = resumeStorageService.read(storagePath);
String filename = ResumeFilenameSanitizer.forDownload(profile.getResumeOriginalName(), "resume");
String contentType = resolveContentType(profile.getResumeContentType());
return new CompanyResumeFile(content, filename, contentType);
}
private String resolveContentType(String stored) {
return (stored != null && ALLOWED_CONTENT_TYPES.contains(stored)) ? stored : FALLBACK_CONTENT_TYPE;
}
}