forked from baskbuddy/zhangshun
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.
76 lines
3.4 KiB
76 lines
3.4 KiB
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 企业读取候选人简历文件。
|
|
*
|
|
* <p>本类只负责「授权已通过之后」的文件读取与响应构建:
|
|
* 授权全部由 {@link CompanyResumeAccessPolicy} 完成,**任何拒绝路径都不会走到
|
|
* {@link ResumeStorageService#read}</strong>。</p>
|
|
*
|
|
* <p>简历正文、存储路径、原始文件名与学生标识都不写入日志、审计或异常。</p>
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class CompanyResumeService {
|
|
|
|
/** Content-Type 白名单:只能来自上传时已校验的三种简历类型。 */
|
|
static final Set<String> 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;
|
|
}
|
|
}
|
|
|