package com.estate.expert.admin.service.impl;
|
|
import com.estate.expert.admin.domain.business.device.Device;
|
import com.estate.expert.admin.domain.business.device.DeviceType;
|
import com.estate.expert.admin.domain.common.request.PageReq;
|
import com.estate.expert.admin.domain.common.response.PageResp;
|
import com.estate.expert.admin.domain.exam.eval.score.*;
|
import com.estate.expert.admin.domain.exam.record.error.ErrorRecord;
|
import com.estate.expert.admin.domain.exam.task.check.CheckTask;
|
import com.estate.expert.admin.domain.plan.PlanType;
|
import com.estate.expert.admin.domain.standard.expert.Expert;
|
import com.estate.expert.admin.domain.standard.hierachy.HierarchyData;
|
import com.estate.expert.admin.service.eval.EvalScoreService;
|
import com.estate.expert.admin.service.eval.EvalServiceImpl;
|
import com.estate.expert.admin.service.expert.DeviceExpertService;
|
import com.estate.expert.admin.service.expert.SysExpertService;
|
import com.estate.expert.admin.utils.tree.helper.TreeHelper;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.stereotype.Service;
|
|
import javax.annotation.Resource;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.TreeMap;
|
import java.util.stream.Stream;
|
|
/**
|
* 评分算法服务实现
|
*/
|
@Service
|
public class EvalScoreServiceImpl extends EvalServiceImpl<EvalScoreResult, EvalScoreResultDto, EvalScoreResultInfo, ScoreResult> implements EvalScoreService {
|
|
private static final Logger log = LoggerFactory.getLogger(EvalScoreServiceImpl.class);
|
@Resource
|
private EvalScoreResultRepository repository;
|
|
@Override
|
public PageResp<EvalScoreResultInfo> list(Integer clientId, List<Integer> planIds, List<Integer> taskIds,
|
LocalDateTime startDate, LocalDateTime endDate, PageReq pageReq) {
|
var page = repository.list(clientId, planIds, taskIds, startDate, endDate,pageReq.toPageRequest());
|
return PageResp.of(page, page.getContent());
|
}
|
|
@Override
|
public List<EvalScoreResultMiniInfo> listMini(List<Integer> projectIds, PlanType planType) {
|
return repository.list(projectIds, planType);
|
}
|
|
@Override
|
public EvalScoreResult getByCheckTaskId(Integer checkTaskId) {
|
return repository.getFirstByTaskIdAndDeleted(checkTaskId, false);
|
}
|
|
@Override
|
public boolean enableByPass() {
|
return true;
|
}
|
|
/**
|
* 打分算法
|
*
|
* 扣除分数:所选记录,对应的,问题的问题数量集合中,
|
* 应得分数:体系层级中
|
* 权重:在专家管理,打分算法,权重值,需要归一化;如果找不到,就从层级中取
|
* (应得分数 - 扣除分数) * 权重,每层累加上去
|
* 上一级的得分= 下级得分之和/下级应得分之和 * 上一级的总分
|
* 叶子节点小问题只能出现一次
|
*
|
* 叶子节点的得分计算逻辑
|
*
|
* 假定一个节点为10分,每发现其下属的一个问题,就扣除相应的分数
|
* 最终得到该节点的分数
|
*/
|
@Override
|
public ScoreResult evaluate(Expert scoreExpert, SysExpertService expertService, List<ErrorRecord> records, List<String> byPassPaths){
|
// 初始化问题映射表
|
var problemMap = initProblemMap(scoreExpert, expertService);
|
|
// 根据专家的模型,构建评分树
|
var scoreResult = initSysResult(scoreExpert, expertService);
|
return commonEvaluate(problemMap, scoreResult, records, byPassPaths, 6);
|
}
|
|
@Override
|
protected ScoreResult evaluate(Expert expert, DeviceType deviceType,
|
DeviceExpertService expertService, List<ErrorRecord> errorRecords, List<String> byPassPaths) {
|
// 初始化问题映射表
|
var deviceScoreResult = initDeviceResult(deviceType, expert, expertService);
|
var problemMap = initDeviceProblemMap(deviceType, expert, expertService);
|
return commonEvaluate(problemMap, deviceScoreResult, errorRecords, byPassPaths, 4);
|
}
|
|
@Override
|
protected EvalScoreResult saveResult(CheckTask checkTask, ScoreResult sysRes, Map<Integer, ScoreResult> devResMap, List<Device> devices) {
|
if(sysRes != null && sysRes.root() != null)
|
checkTask.setSystemScore(sysRes.root().getData().getScore());
|
else
|
checkTask.setSystemScore(null);
|
|
var deviceScoreMap = new TreeMap<Integer, Float>();
|
devResMap.forEach((k, v) -> {
|
if(v != null && v.root() != null)
|
deviceScoreMap.put(k, v.root().getData().getScore());
|
else
|
deviceScoreMap.put(k, null);
|
});
|
checkTask.setDeviceScoreMap(deviceScoreMap);
|
return add(null , new EvalScoreResult(checkTask, sysRes, devResMap, devices));
|
}
|
|
private ScoreResult commonEvaluate(TreeMap<Integer, Boolean> problemMap, ScoreResult scoreResult,
|
List<ErrorRecord> records, List<String> byPassPaths, int leafDepth){
|
// 计数错误记录,将记录的扣分,累加到对应的节点上
|
count(problemMap, scoreResult, records, byPassPaths);
|
|
// 标记要参与评分的节点
|
mark(scoreResult);
|
|
// 优化掉没有参与评分的节点
|
scoreResult = prune(scoreResult, leafDepth);
|
|
// 按照树结构,逐层计算得分
|
compute(scoreResult, leafDepth);
|
|
prettyDisplay(scoreResult);
|
return scoreResult;
|
}
|
|
private ScoreResult initSysResult(Expert expert, SysExpertService expertService) {
|
var tree = expertService.getHierarchyTree(expert.getId());
|
var root = TreeHelper.<HierarchyData, ScoreValue>convertTree(tree.root(), (node, newNode) ->
|
ScoreValue.builder()
|
.total(node.getData().getScore())
|
.weight(node.getData().getWeight())
|
.build());
|
return new ScoreResult(root);
|
}
|
|
private ScoreResult initDeviceResult(DeviceType deviceType, Expert expert, DeviceExpertService expertService){
|
var tree = expertService.getHierarchyTree(expert.getId());
|
var root = TreeHelper.<HierarchyData, ScoreValue>convertTree(tree.root(), (node, newNode) ->
|
ScoreValue.builder()
|
.total(node.getData().getScore())
|
.weight(node.getData().getWeight())
|
.build(), (n) -> {
|
//删除掉非相关掉设备类型的节点
|
return n.getDepth() != 1 || deviceType.hierarchyPath().equals(n.getPath());
|
});
|
|
return new ScoreResult(root);
|
}
|
|
private void count(TreeMap<Integer, Boolean> problemMap, ScoreResult scoreResult, List<ErrorRecord> records, List<String> byPassPaths) {
|
// 打分算法
|
// 将每个问题记录的扣分,累加到对应的节点上
|
if(records != null) {
|
records.forEach(errorRecord -> {
|
var problem = errorRecord.getProblem();
|
var ps = problemMap.get(problem.getId());
|
|
// 如果问题不在模型中,不计算
|
// 如果问题已经计算过,也不计算(同一问题的记录只计算一次)
|
if (ps == null || ps)
|
return;
|
|
problemMap.put(problem.getId(), true);
|
|
// 数据出错,不计算
|
if (problem.getNumberFactors().isEmpty() || errorRecord.getNumberFactorIndex() >= problem.getNumberFactors().size())
|
return;
|
|
// 计算应扣除的分数
|
var node = scoreResult.get(problem.getHierarchyPath());
|
if(node == null)
|
return;
|
var numberFactor = problem.getNumberFactors().get(errorRecord.getNumberFactorIndex());
|
var scoreValue = node.getData();
|
scoreValue.setDeduct(scoreValue.getDeduct() + numberFactor.getDeductPoint());
|
scoreValue.setInclude(true);
|
scoreValue.addError(errorRecord.getLevel());
|
node.setData(scoreValue);
|
});
|
}
|
|
// 轮空项,如果设置了,则需要参与计算
|
if(byPassPaths != null){
|
byPassPaths.forEach(path -> {
|
var node = scoreResult.get(path);
|
if(node == null)
|
return;
|
var scoreValue = node.getData();
|
scoreValue.setInclude(true);
|
node.setData(scoreValue);
|
});
|
}
|
|
// 累加到上一级
|
TreeHelper.travel(scoreResult.root(), (node) -> {
|
var nodeData = node.getData();
|
for(var child : node.getChildren().values()) {
|
var childData = child.getData();
|
for(var i = 0; i < 4; i++) {
|
nodeData.getChildErrorCounts()[i] += childData.getErrorCounts()[i] + childData.getChildErrorCounts()[i];
|
}
|
}
|
});
|
}
|
|
/**
|
* 标记节点,用于裁剪不需要参与计算的节点
|
*/
|
private void mark(ScoreResult scoreResult)
|
{
|
TreeHelper.travel(scoreResult.root(), (node) -> {
|
// 只去更新非叶子节点的include属性
|
if(node.getChildren().isEmpty())
|
return;
|
|
// 如果有子节点参与评分,当前节点也参与评分
|
boolean include = false;
|
for (var child : node.getChildren().values()) {
|
if(child.getData().isInclude())
|
include = true;
|
}
|
node.getData().setInclude(include);
|
});
|
}
|
|
// 对个别需要显示的数值进行更新,便于前端展示
|
private void prettyDisplay(ScoreResult result) {
|
TreeHelper.travel(result.root(), (node) -> {
|
var fuzzyValue = node.getData();
|
// totalErrorCounts = errorCounts + childErrorCounts
|
fuzzyValue.setTotalErrorCounts(Stream.of(0,1,2,3)
|
.map(i -> fuzzyValue.getErrorCounts()[i] + fuzzyValue.getChildErrorCounts()[i])
|
.mapToInt(Integer::intValue)
|
.toArray());
|
});
|
}
|
|
/**
|
* 根据层级和权重,计算得分
|
*/
|
private void compute(ScoreResult scoreResult, int leafDepth) {
|
TreeHelper.travel(scoreResult.root(), (node) -> {
|
log.debug("计算节点: {}", node);
|
float totalWeight = 0;
|
float totalDeduct = 0;
|
|
if(node.getDepth() >= (leafDepth - 2))
|
{
|
// 最下的两层的计算逻辑不一样,只计算得分,不计算权重
|
float total = 0;
|
for(var child : node.getChildren().values()) {
|
if (!child.getData().isInclude())
|
continue;
|
|
// 计算总扣分
|
var sv = child.getData();
|
totalDeduct += Math.min(sv.getTotal(), sv.getDeduct() + sv.getChildrenDeduct());
|
total += sv.getTotal();
|
}
|
|
var value = node.getData();
|
if(node.getDepth() != leafDepth)
|
value.setTotal(total);
|
|
value.setChildrenDeduct(totalDeduct);
|
value.setTotalDeduct(value.getDeduct() + value.getChildrenDeduct());
|
value.setScore(Math.max(value.getTotal() - value.getTotalDeduct(), 0));
|
value.setNormalizedWeight(1.0f);
|
|
log.info("计算节点: {},得分: {}", node, value.getScore());
|
}else {
|
var total = 0.0f;
|
|
// 计算总权重
|
for (var child : node.getChildren().values()) {
|
if (!child.getData().isInclude())
|
continue;
|
|
totalWeight += Math.max(child.getData().getWeight(), 1e-3f); // 防止权重为0
|
}
|
|
// 归一化权重,计算总扣分
|
for (var child : node.getChildren().values()) {
|
if (!child.getData().isInclude())
|
continue;
|
|
var sv = child.getData();
|
sv.setNormalizedWeight(Math.max(sv.getWeight(), 1e-3f) / totalWeight);
|
|
// 最大扣分不能超过总分, 节点的扣分包含两部分
|
totalDeduct += Math.min(sv.getTotal(), sv.getDeduct() + sv.getChildrenDeduct()) / sv.getTotal() * sv.getNormalizedWeight();
|
total += sv.getTotal();
|
}
|
|
var value = node.getData();
|
value.setChildrenDeduct(totalDeduct * value.getTotal());
|
value.setTotalDeduct(value.getDeduct() + value.getChildrenDeduct());
|
// 当前节点的得分 = 下级分数之和(换算成当前节点的) - 当前节点的扣分
|
value.setScore(Math.max(value.getTotal() - value.getTotalDeduct(), 0));
|
value.setNormalizedWeight(1.0f);
|
|
log.info("计算节点: {},得分: {}", node, value.getScore());
|
}
|
});
|
}
|
|
/**
|
* 优化掉没有参与评分的节点
|
*/
|
private ScoreResult prune(ScoreResult scoreResult, int depth){
|
var root = TreeHelper.pruneTree(scoreResult.root(), (n) -> {
|
return n.getData().isInclude() || n.getDepth() == depth;
|
});
|
|
return new ScoreResult(root);
|
}
|
|
}
|