package cn.iocoder.yudao.module.research.service.review;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.research.controller.admin.review.vo.ResearchTopicReviewSaveReqVO;
|
import cn.iocoder.yudao.module.research.dal.dataobject.application.ResearchApplicationDO;
|
import cn.iocoder.yudao.module.research.dal.dataobject.review.ResearchTopicReviewDO;
|
import cn.iocoder.yudao.module.research.dal.mysql.application.ResearchApplicationMapper;
|
import cn.iocoder.yudao.module.research.dal.mysql.review.ResearchTopicReviewMapper;
|
import cn.iocoder.yudao.module.research.enums.ResearchApplicationStatusEnum;
|
import cn.iocoder.yudao.module.research.enums.ResearchReviewConclusionEnum;
|
import cn.iocoder.yudao.module.research.service.application.ResearchApplicationService;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDateTime;
|
import java.util.Comparator;
|
import java.util.List;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.research.enums.ErrorCodeConstants.*;
|
|
/**
|
* 专家评审明细 Service 实现类
|
*
|
* 评分权重与「均分 + 多数结论」通过规则参考 SRM 评标
|
* (srm_bid_evaluation:price*0.4 + tech*0.3 + delivery*0.2 + service*0.1 的加权汇总 + calculateRanking)
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class ResearchTopicReviewServiceImpl implements ResearchTopicReviewService {
|
|
// ===== 评分权重(占位规则,需学会最终确认,改动只需改这里)=====
|
private static final BigDecimal TOPIC_WEIGHT = new BigDecimal("0.3");
|
private static final BigDecimal PLAN_WEIGHT = new BigDecimal("0.3");
|
private static final BigDecimal TEAM_WEIGHT = new BigDecimal("0.2");
|
private static final BigDecimal RESULT_WEIGHT = new BigDecimal("0.2");
|
|
/** 推荐立项的均分阈值 */
|
private static final BigDecimal PASS_SCORE = new BigDecimal("60");
|
private static final BigDecimal SCORE_MAX = new BigDecimal("100");
|
|
@Resource
|
private ResearchTopicReviewMapper reviewMapper;
|
|
@Resource
|
private ResearchApplicationMapper applicationMapper;
|
|
@Resource
|
private ResearchApplicationService applicationService;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createReview(ResearchTopicReviewSaveReqVO createReqVO) {
|
validateApplicationReviewable(createReqVO.getApplicationId(), REVIEW_CREATE_FAIL_STATUS);
|
validateScores(createReqVO);
|
|
ResearchTopicReviewDO review = BeanUtils.toBean(createReqVO, ResearchTopicReviewDO.class, in -> in
|
.setId(null)
|
.setReviewTime(LocalDateTime.now()));
|
reviewMapper.insert(review);
|
|
recalculate(createReqVO.getApplicationId());
|
return review.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateReview(ResearchTopicReviewSaveReqVO updateReqVO) {
|
ResearchTopicReviewDO exist = reviewMapper.selectById(updateReqVO.getId());
|
if (exist == null) {
|
throw exception(REVIEW_NOT_EXISTS);
|
}
|
validateApplicationReviewable(exist.getApplicationId(), REVIEW_UPDATE_FAIL_STATUS);
|
validateScores(updateReqVO);
|
|
ResearchTopicReviewDO updateObj = BeanUtils.toBean(updateReqVO, ResearchTopicReviewDO.class, in ->
|
in.setReviewTime(LocalDateTime.now()));
|
reviewMapper.updateById(updateObj);
|
|
recalculate(exist.getApplicationId());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteReview(Long id) {
|
ResearchTopicReviewDO exist = reviewMapper.selectById(id);
|
if (exist == null) {
|
throw exception(REVIEW_NOT_EXISTS);
|
}
|
validateApplicationReviewable(exist.getApplicationId(), REVIEW_DELETE_FAIL_STATUS);
|
|
reviewMapper.deleteById(id);
|
recalculate(exist.getApplicationId());
|
}
|
|
@Override
|
public List<ResearchTopicReviewDO> getReviewListByApplicationId(Long applicationId) {
|
return reviewMapper.selectListByApplicationId(applicationId);
|
}
|
|
// =================== 内部方法 ===================
|
|
/**
|
* 重算某申报书的全部评审明细:加权合计 → 排名 → 主单汇总均分与结论
|
*/
|
private void recalculate(Long applicationId) {
|
List<ResearchTopicReviewDO> list = reviewMapper.selectListByApplicationId(applicationId);
|
|
// 1. 逐行重算加权合计
|
for (ResearchTopicReviewDO review : list) {
|
review.setCompositeScore(weightedScore(review));
|
}
|
// 2. 按加权合计降序排名,名次回写
|
List<ResearchTopicReviewDO> ranked = list.stream()
|
.sorted(Comparator.comparing(ResearchTopicReviewDO::getCompositeScore,
|
Comparator.reverseOrder()))
|
.toList();
|
for (int i = 0; i < ranked.size(); i++) {
|
ranked.get(i).setRanking(i + 1);
|
}
|
for (ResearchTopicReviewDO review : list) {
|
reviewMapper.updateById(new ResearchTopicReviewDO()
|
.setId(review.getId())
|
.setCompositeScore(review.getCompositeScore())
|
.setRanking(review.getRanking()));
|
}
|
|
// 3. 回写主单:均分 + 是否推荐立项(无明细时清空)
|
if (CollUtil.isEmpty(list)) {
|
applicationService.updateReviewSummary(applicationId, null, null);
|
return;
|
}
|
BigDecimal sum = list.stream().map(ResearchTopicReviewDO::getCompositeScore)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
BigDecimal avg = sum.divide(BigDecimal.valueOf(list.size()), 2, RoundingMode.HALF_UP);
|
long recommendCount = list.stream()
|
.filter(r -> ResearchReviewConclusionEnum.RECOMMEND.getConclusion().equals(r.getConclusion()))
|
.count();
|
boolean pass = avg.compareTo(PASS_SCORE) >= 0 && recommendCount * 2 > list.size();
|
applicationService.updateReviewSummary(applicationId, avg, pass ? 1 : 0);
|
}
|
|
private BigDecimal weightedScore(ResearchTopicReviewDO review) {
|
return nvl(review.getTopicScore()).multiply(TOPIC_WEIGHT)
|
.add(nvl(review.getPlanScore()).multiply(PLAN_WEIGHT))
|
.add(nvl(review.getTeamScore()).multiply(TEAM_WEIGHT))
|
.add(nvl(review.getResultScore()).multiply(RESULT_WEIGHT))
|
.setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private void validateApplicationReviewable(Long applicationId, cn.iocoder.yudao.framework.common.exception.ErrorCode errorCode) {
|
ResearchApplicationDO application = applicationMapper.selectById(applicationId);
|
if (application == null) {
|
throw exception(APPLICATION_NOT_EXISTS);
|
}
|
if (!ResearchApplicationStatusEnum.APPROVING.getStatus().equals(application.getStatus())) {
|
throw exception(errorCode, application.getNo(),
|
ResearchApplicationStatusEnum.nameOf(application.getStatus()));
|
}
|
}
|
|
/** 逐项校验分值范围,提示里指出具体是哪个维度、实际值多少 */
|
private void validateScores(ResearchTopicReviewSaveReqVO reqVO) {
|
checkScore("选题价值与意义", reqVO.getTopicScore());
|
checkScore("研究方案可行性", reqVO.getPlanScore());
|
checkScore("团队与研究基础", reqVO.getTeamScore());
|
checkScore("预期成果与效益", reqVO.getResultScore());
|
}
|
|
private void checkScore(String label, BigDecimal score) {
|
if (score == null) {
|
return;
|
}
|
if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(SCORE_MAX) > 0) {
|
throw exception(REVIEW_SCORE_OUT_OF_RANGE, label, score);
|
}
|
}
|
|
private static BigDecimal nvl(BigDecimal value) {
|
return value != null ? value : BigDecimal.ZERO;
|
}
|
|
}
|