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.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 void 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); } 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) { Application app = findApplication(applicationId); Job job = findJob(app); assertCompanyOwner(userId, job); 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) { Application app = findApplication(applicationId); Job job = findJob(app); // 企业(按 userId 解析归属企业后比较企业 id)或学生任一匹配即可查看 boolean isStudent = app.getStudentUserId().equals(userId); boolean isCompany = companyRepository.findByOwnerUserId(userId) .map(c -> c.getId().equals(job.getCompanyId())) .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) { Application app = findApplication(applicationId); Job job = findJob(app); assertCompanyOwner(userId, job); 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) { Application app = findApplication(applicationId); Job job = findJob(app); assertCompanyOwner(userId, job); 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 list) { if (list == null) return null; try { return mapper.writeValueAsString(list); } catch (JsonProcessingException e) { throw new RuntimeException("JSON error", e); } } private List fromJson(String json) { if (json == null || json.isBlank()) return List.of(); try { return mapper.readValue(json, new TypeReference>() {}); } catch (JsonProcessingException e) { return List.of(); } } }