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 implements EvalScoreService { private static final Logger log = LoggerFactory.getLogger(EvalScoreServiceImpl.class); @Resource private EvalScoreResultRepository repository; @Override public PageResp list(Integer clientId, List planIds, List 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 listMini(List 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 records, List 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 errorRecords, List 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 devResMap, List devices) { if(sysRes != null && sysRes.root() != null) checkTask.setSystemScore(sysRes.root().getData().getScore()); else checkTask.setSystemScore(null); var deviceScoreMap = new TreeMap(); 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 problemMap, ScoreResult scoreResult, List records, List 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.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.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 problemMap, ScoreResult scoreResult, List records, List 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); } }