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
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(); }
|
|
}
|
|
}
|
|
|