轻松聘AI开发
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.
 
 
 

184 lines
8.7 KiB

package com.mydreamjob.feedback;
import com.mydreamjob.application.Application;
import com.mydreamjob.application.ApplicationRepository;
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.company.VerifiedCompanyGuard;
import com.mydreamjob.feedback.dto.CandidateFeedbackRequest;
import com.mydreamjob.feedback.dto.CandidateFeedbackResponse;
import com.mydreamjob.job.Job;
import com.mydreamjob.job.JobService;
import com.mydreamjob.candidatescore.refresh.CandidateScoringRefreshRequestService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class CandidateFeedbackService {
private final CandidateFeedbackRepository feedbackRepository;
private final ApplicationRepository applicationRepository;
private final JobService jobService;
/** R01-D4-1:候选人反馈创建后登记评分快照刷新(INTEGRITY 维度)。 */
private final CandidateScoringRefreshRequestService scoringRefreshRequestService;
private final CompanyRepository companyRepository;
private static final ObjectMapper mapper = new ObjectMapper();
private Application findApplication(Long appId) {
return applicationRepository.findById(appId)
.orElseThrow(() -> new BusinessException(ErrorCode.APPLICATION_NOT_FOUND));
}
private Job findJob(Application app) {
return jobService.getJob(app.getJobId());
}
/**
* 企业归属校验:先按当前登录 userId 解析其拥有的企业,
* 再将企业 id 与 job.companyId 比较(userId 与 companyId 属不同命名空间,不可直接比较)。
*/
private Company assertCompanyOwner(Long userId, Job job) {
Company company = companyRepository.findByOwnerUserId(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.FORBIDDEN));
if (!company.getId().equals(job.getCompanyId()))
throw new BusinessException(ErrorCode.FORBIDDEN);
return company;
}
private void requireOwnedCompanyVerifiedIfPresent(Long userId) {
companyRepository.findByOwnerUserId(userId).ifPresent(VerifiedCompanyGuard::requireVerified);
}
private void assertStudentOwner(Long studentUserId, Application app) {
if (!app.getStudentUserId().equals(studentUserId))
throw new BusinessException(ErrorCode.FORBIDDEN);
}
// ── 创建 ──────────────────────────────────────────────────
@Transactional
public CandidateFeedbackResponse createFeedback(Long userId, Long applicationId, CandidateFeedbackRequest req) {
requireOwnedCompanyVerifiedIfPresent(userId);
Application app = findApplication(applicationId);
Job job = findJob(app);
Company company = assertCompanyOwner(userId, job);
VerifiedCompanyGuard.requireVerified(company);
if (feedbackRepository.existsByApplicationId(applicationId))
throw new BusinessException(ErrorCode.DUPLICATE_FEEDBACK);
CandidateFeedback fb = CandidateFeedback.builder()
.applicationId(applicationId)
.companyUserId(userId)
.studentUserId(app.getStudentUserId())
.rating(req.getRating()).content(req.getContent())
.tags(toJson(req.getTags()))
.build();
CandidateFeedback saved = feedbackRepository.save(fb);
// R01-D4-1:候选人反馈计数/均值影响 INTEGRITY 维度 → 登记当前周期刷新。
scoringRefreshRequestService.requestRefresh(app.getStudentUserId(), java.time.LocalDateTime.now());
return toResponse(saved);
}
// ── 查询 ──────────────────────────────────────────────────
public CandidateFeedbackResponse getFeedback(Long userId, Long applicationId) {
requireOwnedCompanyVerifiedIfPresent(userId);
Application app = findApplication(applicationId);
Job job = findJob(app);
// 企业(按 userId 解析归属企业后比较企业 id)或学生任一匹配即可查看
boolean isStudent = app.getStudentUserId().equals(userId);
boolean isCompany = companyRepository.findByOwnerUserId(userId)
.map(c -> {
if (!c.getId().equals(job.getCompanyId())) return false;
VerifiedCompanyGuard.requireVerified(c);
return true;
}).orElse(false);
if (!isCompany && !isStudent)
throw new BusinessException(ErrorCode.FORBIDDEN);
CandidateFeedback fb = feedbackRepository.findByApplicationId(applicationId)
.orElseThrow(() -> new BusinessException(ErrorCode.FEEDBACK_NOT_FOUND));
return toResponse(fb);
}
// ── 修改 ──────────────────────────────────────────────────
@Transactional
public CandidateFeedbackResponse updateFeedback(Long userId, Long applicationId, CandidateFeedbackRequest req) {
requireOwnedCompanyVerifiedIfPresent(userId);
Application app = findApplication(applicationId);
Job job = findJob(app);
Company company = assertCompanyOwner(userId, job);
VerifiedCompanyGuard.requireVerified(company);
CandidateFeedback fb = feedbackRepository.findByApplicationId(applicationId)
.orElseThrow(() -> new BusinessException(ErrorCode.FEEDBACK_NOT_FOUND));
if (req.getRating() != null) fb.setRating(req.getRating());
if (req.getContent() != null) fb.setContent(req.getContent());
if (req.getTags() != null) fb.setTags(toJson(req.getTags()));
CandidateFeedback updated = feedbackRepository.save(fb);
// R01-D4-2 F2:修改 rating 会改变 AVG(rating) → 与写操作同事务登记刷新。
scoringRefreshRequestService.requestRefresh(app.getStudentUserId(), java.time.LocalDateTime.now());
return toResponse(updated);
}
// ── 删除 ──────────────────────────────────────────────────
@Transactional
public void deleteFeedback(Long userId, Long applicationId) {
requireOwnedCompanyVerifiedIfPresent(userId);
Application app = findApplication(applicationId);
Job job = findJob(app);
Company company = assertCompanyOwner(userId, job);
VerifiedCompanyGuard.requireVerified(company);
CandidateFeedback fb = feedbackRepository.findByApplicationId(applicationId)
.orElseThrow(() -> new BusinessException(ErrorCode.FEEDBACK_NOT_FOUND));
// R01-D4-2 F2:删除前先固定候选人标识,删除后仍能正确登记。
Long studentUserId = app.getStudentUserId();
feedbackRepository.delete(fb);
// COUNT(*) 与 AVG(rating) 同时变化 → 与删除操作同事务登记刷新。
scoringRefreshRequestService.requestRefresh(studentUserId, java.time.LocalDateTime.now());
}
// ── 视图 ──────────────────────────────────────────────────
public CandidateFeedbackResponse toResponse(CandidateFeedback fb) {
if (fb == null) return null;
return CandidateFeedbackResponse.builder()
.id(fb.getId())
.applicationId(fb.getApplicationId())
.rating(fb.getRating())
.content(fb.getContent())
.tags(fromJson(fb.getTags()))
.createdAt(fb.getCreatedAt())
.updatedAt(fb.getUpdatedAt())
.build();
}
private String toJson(List<String> list) {
if (list == null) return null;
try { return mapper.writeValueAsString(list); }
catch (JsonProcessingException e) { throw new RuntimeException("JSON error", e); }
}
private List<String> fromJson(String json) {
if (json == null || json.isBlank()) return List.of();
try { return mapper.readValue(json, new TypeReference<List<String>>() {}); }
catch (JsonProcessingException e) { return List.of(); }
}
}