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

135 lines
6.5 KiB

package com.mydreamjob.preference;
import com.mydreamjob.auth.User;
import com.mydreamjob.auth.UserRepository;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.candidatescore.refresh.CandidateScoringRefreshRequestService;
import com.mydreamjob.preference.dto.StudentPreferenceRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mydreamjob.security.UserRoleGuard;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class PreferenceService {
private final com.mydreamjob.security.UserRoleGuard userRoleGuard;
private final StudentPreferenceRepository preferenceRepository;
private final UserRepository userRepository;
/** R01-D4-1:求职偏好更新后登记评分快照刷新(COMPLIANCE 维度)。 */
private final CandidateScoringRefreshRequestService scoringRefreshRequestService;
private final com.mydreamjob.onlineresume.OnlineResumeWriteGuard onlineResumeWriteGuard;
private static final ObjectMapper mapper = new ObjectMapper();
// ── 角色校验 ──────────────────────────────────────────────
public void assertStudentRole(Long userId) {
userRoleGuard.requireStudent(userId);
}
// ── 查询 ──────────────────────────────────────────────────
public Map<String, Object> getPreferences(Long studentUserId) {
assertStudentRole(studentUserId);
return preferenceRepository.findByStudentUserId(studentUserId)
.map(this::toView)
.orElse(emptyView());
}
// ── 保存/更新 ────────────────────────────────────────────
@Transactional
public Map<String, Object> savePreferences(Long studentUserId, StudentPreferenceRequest req) {
assertStudentRole(studentUserId);
var onlineResume = onlineResumeWriteGuard.beforeLegacyWrite(studentUserId, req.getRevision());
StudentPreference pref = preferenceRepository.lockByStudentUserId(studentUserId)
.orElse(StudentPreference.builder()
.studentUserId(studentUserId)
.remoteAllowed(false)
.build());
// 部分更新必须校验合并后的区间,且在修改任何受管实体字段前拒绝,避免部分写入。
Integer salaryMin = req.getExpectedSalaryMin() == null
? pref.getExpectedSalaryMin() : req.getExpectedSalaryMin();
Integer salaryMax = req.getExpectedSalaryMax() == null
? pref.getExpectedSalaryMax() : req.getExpectedSalaryMax();
if (salaryMin != null && salaryMin < 0 || salaryMax != null && salaryMax < 0
|| salaryMin != null && salaryMax != null && salaryMin > salaryMax) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "invalid merged salary range");
}
if (req.getExpectedSalaryMin() != null) pref.setExpectedSalaryMin(req.getExpectedSalaryMin());
if (req.getExpectedSalaryMax() != null) pref.setExpectedSalaryMax(req.getExpectedSalaryMax());
if (req.getPreferredCities() != null) pref.setPreferredCities(toJson(req.getPreferredCities()));
if (req.getEmploymentTypes() != null) pref.setEmploymentTypes(toJson(req.getEmploymentTypes()));
if (req.getPreferredIndustries() != null) pref.setPreferredIndustries(toJson(req.getPreferredIndustries()));
if (req.getPreferredJobTypes() != null) pref.setPreferredJobTypes(toJson(req.getPreferredJobTypes()));
if (req.getRemoteAllowed() != null) pref.setRemoteAllowed(req.getRemoteAllowed());
StudentPreference saved = preferenceRepository.save(pref);
// R01-D4-1:偏好更新影响 COMPLIANCE 维度 → 登记当前周期刷新。
scoringRefreshRequestService.requestRefresh(studentUserId, LocalDateTime.now());
onlineResumeWriteGuard.afterLegacyWrite(onlineResume);
return toView(saved);
}
// ── 视图 ──────────────────────────────────────────────────
private Map<String, Object> toView(StudentPreference pref) {
Map<String, Object> view = new HashMap<>();
view.put("id", pref.getId());
view.put("studentUserId", pref.getStudentUserId());
view.put("expectedSalaryMin", pref.getExpectedSalaryMin());
view.put("expectedSalaryMax", pref.getExpectedSalaryMax());
view.put("preferredCities", fromJson(pref.getPreferredCities()));
view.put("employmentTypes", fromJson(pref.getEmploymentTypes()));
view.put("preferredIndustries", fromJson(pref.getPreferredIndustries()));
view.put("preferredJobTypes", fromJson(pref.getPreferredJobTypes()));
view.put("remoteAllowed", pref.getRemoteAllowed());
view.put("createdAt", pref.getCreatedAt());
view.put("updatedAt", pref.getUpdatedAt());
return view;
}
private Map<String, Object> emptyView() {
Map<String, Object> view = new HashMap<>();
view.put("expectedSalaryMin", null);
view.put("expectedSalaryMax", null);
view.put("preferredCities", List.of());
view.put("employmentTypes", List.of());
view.put("preferredIndustries", List.of());
view.put("preferredJobTypes", List.of());
view.put("remoteAllowed", false);
return view;
}
private String toJson(List<String> list) {
try {
return mapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize", 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();
}
}
}