package com.ruoyi.system.service; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.system.mapper.*; import com.ruoyi.system.model.*; import com.ruoyi.system.model.Record; import com.ruoyi.system.vo.ProjectScoreData; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.*; import java.util.stream.Collectors; import java.time.LocalDate; import java.time.ZoneId; /** * 项目类别计算服务 * * @author MiTao * @date 2026-01-21 */ @Service @RequiredArgsConstructor @Slf4j public class ProjectCategoryCalculator { private final EvalResultMapper evalResultMapper; private final ClientParamMapper clientParamMapper; private final CheckMapper checkMapper; private final TaskMapper taskMapper; private final RecordMapper recordMapper; /** * 计算在建工程项目类别 * * @param projects 项目列表 * @param clientId 委托方ID * @param startDate 开始日期 * @param endDate 结束日期 * @return 项目ID -> 类别(A/B/C)的映射 */ public Map calculateOngoingProjectCategories( List projects, Long clientId, Date startDate, Date endDate) { Map categoryMap = new HashMap<>(); if (projects == null || projects.isEmpty()) { return categoryMap; } try { // 1. 获取委托方配置 ClientParam clientParam = getClientParam(clientId); if (clientParam == null) { log.warn("委托方配置不存在,clientId: {}", clientId); return categoryMap; } // 2. 解析在建工程配置 JSONObject config = parseJsonConfig( clientParam.getUnderConstructionProject(), "在建工程配置"); if (config == null) { return categoryMap; } // 3. 查询项目关联的任务 List projectIds = projects.stream() .map(Project::getId) .collect(Collectors.toList()); // 将 Date 转换为 LocalDate LocalDate startLocalDate = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); LocalDate endLocalDate = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); LambdaQueryWrapper taskWrapper = new LambdaQueryWrapper<>(); taskWrapper.in(Task::getProjectId, projectIds) .ge(Task::getDate, startLocalDate) .le(Task::getDate, endLocalDate); List tasks = taskMapper.selectList(taskWrapper); if (tasks.isEmpty()) { log.info("项目没有任务记录,projectIds: {}", projectIds); return categoryMap; } List checkTaskIds = tasks.stream() .map(Task::getId) .collect(Collectors.toList()); // 4. 根据评价类型查询评估结果 Integer commentType = clientParam.getCommentType(); String evalType = (commentType != null && commentType == 1) ? "fuzzy" : "score"; LambdaQueryWrapper evalWrapper = new LambdaQueryWrapper<>(); evalWrapper.eq(EvalResult::getType, evalType) .in(EvalResult::getCheckTaskId, checkTaskIds) .eq(EvalResult::getDeleted, false); List evalResults = evalResultMapper.selectList(evalWrapper); if (evalResults.isEmpty()) { log.info("项目没有评估结果,projectIds: {}", projectIds); return categoryMap; } // 5. 计算项目平均分 Map projectScoreMap = calculateProjectScores( tasks, evalResults); // 6. 根据配置判断项目类别 for (Map.Entry entry : projectScoreMap.entrySet()) { Integer projectId = entry.getKey(); ProjectScoreData scoreData = entry.getValue(); try { String category = determineOngoingCategory(scoreData, config); if (category != null) { categoryMap.put(projectId, category); } } catch (Exception e) { log.error("判断项目类别失败,projectId: {}", projectId, e); } } } catch (Exception e) { log.error("计算在建工程项目类别失败,clientId: {}", clientId, e); } return categoryMap; } /** * 计算固定资产项目类别 * * @param projects 项目列表 * @param clientId 委托方ID * @param startDate 开始日期 * @param endDate 结束日期 * @return 项目ID -> 类别(A/B/C)的映射 */ public Map calculateFixedAssetCategories( List projects, Long clientId, Date startDate, Date endDate) { Map categoryMap = new HashMap<>(); if (projects == null || projects.isEmpty()) { return categoryMap; } try { // 1. 获取委托方配置 ClientParam clientParam = getClientParam(clientId); if (clientParam == null) { log.warn("委托方配置不存在,clientId: {}", clientId); return categoryMap; } // 2. 解析固定资产配置 JSONObject config = parseJsonConfig( clientParam.getAssetProject(), "固定资产配置"); if (config == null) { return categoryMap; } // 3. 查询项目关联的任务 List projectIds = projects.stream() .map(Project::getId) .collect(Collectors.toList()); // 将 Date 转换为 LocalDate LocalDate startLocalDate = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); LocalDate endLocalDate = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); LambdaQueryWrapper taskWrapper = new LambdaQueryWrapper<>(); taskWrapper.in(Task::getProjectId, projectIds) .ge(Task::getDate, startLocalDate) .le(Task::getDate, endLocalDate); List tasks = taskMapper.selectList(taskWrapper); if (tasks.isEmpty()) { log.info("项目没有任务记录,projectIds: {}", projectIds); return categoryMap; } List checkTaskIds = tasks.stream() .map(Task::getId) .collect(Collectors.toList()); // 4. 查询隐患记录(record_type='error') LambdaQueryWrapper recordWrapper = new LambdaQueryWrapper<>(); recordWrapper.eq(Record::getRecordType, "error") .in(Record::getCheckTaskId, checkTaskIds) .eq(Record::getDeleted, false); List hazardRecords = recordMapper.selectList(recordWrapper); if (hazardRecords.isEmpty()) { log.info("项目没有隐患记录,projectIds: {}", projectIds); return categoryMap; } // 5. 按项目分组统计隐患等级数量 Map taskToProjectMap = tasks.stream() .collect(Collectors.toMap(Task::getId, Task::getProjectId)); Map> projectHazardMap = new HashMap<>(); for (Record record : hazardRecords) { Integer projectId = taskToProjectMap.get(record.getCheckTaskId()); if (projectId == null) { continue; } Integer level = record.getLevel(); if (level == null) { continue; } projectHazardMap .computeIfAbsent(projectId, k -> new HashMap<>()) .merge(level, 1, Integer::sum); } // 6. 根据配置判断项目类别 for (Map.Entry> entry : projectHazardMap.entrySet()) { Integer projectId = entry.getKey(); Map hazardCounts = entry.getValue(); try { String category = determineFixedAssetCategory(hazardCounts, config); if (category != null) { categoryMap.put(projectId, category); } } catch (Exception e) { log.error("判断项目类别失败,projectId: {}", projectId, e); } } } catch (Exception e) { log.error("计算固定资产项目类别失败,clientId: {}", clientId, e); } return categoryMap; } /** * 获取委托方配置 * * @param clientId 委托方ID * @return 委托方配置 */ private ClientParam getClientParam(Long clientId) { if (clientId == null) { return null; } LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ClientParam::getClientId, clientId.intValue()); return clientParamMapper.selectOne(wrapper); } /** * 解析 JSON 配置 * * @param jsonStr JSON 字符串 * @param configName 配置名称 * @return JSON 对象 */ private JSONObject parseJsonConfig(String jsonStr, String configName) { if (!StringUtils.hasText(jsonStr)) { log.warn("{}为空", configName); return null; } try { return JSON.parseObject(jsonStr); } catch (Exception e) { log.error("解析{}失败: {}", configName, jsonStr, e); return null; } } /** * 计算项目评分数据 * * @param tasks 任务列表 * @param evalResults 评估结果列表 * @return 项目ID -> 评分数据的映射 */ private Map calculateProjectScores( List tasks, List evalResults) { Map projectScoreMap = new HashMap<>(); // 按 check_task_id 关联到 project_id Map checkToProjectMap = tasks.stream() .collect(Collectors.toMap(Task::getId, Task::getProjectId)); // 按项目分组评估结果 Map> projectEvalMap = evalResults.stream() .filter(e -> checkToProjectMap.containsKey(e.getCheckTaskId())) .collect(Collectors.groupingBy(e -> checkToProjectMap.get(e.getCheckTaskId()))); // 计算每个项目的平均分 for (Map.Entry> entry : projectEvalMap.entrySet()) { Integer projectId = entry.getKey(); List results = entry.getValue(); // 计算安全检查平均分 double avgSafetyScore = results.stream() .filter(e -> e.getScore() != null) .mapToDouble(EvalResult::getScore) .average() .orElse(0.0); // 计算设备检查平均分(需要解析 device_res_map) List deviceScores = results.stream() .filter(e -> StringUtils.hasText(e.getDeviceResMap())) .map(e -> calculateDeviceScore(e.getDeviceResMap())) .filter(score -> score != null && score > 0) .toList(); ProjectScoreData scoreData = new ProjectScoreData(); scoreData.setProjectId(projectId); scoreData.setSafetyScore((float) avgSafetyScore); if (!deviceScores.isEmpty()) { double avgDeviceScore = deviceScores.stream() .mapToDouble(Float::doubleValue) .average() .orElse(0.0); scoreData.setDeviceScore((float) avgDeviceScore); scoreData.setHasDeviceScore(true); } else { scoreData.setHasDeviceScore(false); } projectScoreMap.put(projectId, scoreData); } return projectScoreMap; } /** * 计算设备得分 *

* device_res_map 字段的 JSON 结构(来自 estate_expert 项目): *

     * {
     *   "设备ID1": {
     *     "content": {
     *       "data": {
     *         "score": 85.5,
     *         "weight": 100.0,
     *         "total": 100.0,
     *         "totalDeduct": 14.5,
     *         "totalErrorCounts": [1, 2, 0, 0]
     *       },
     *       "children": [...]
     *     }
     *   },
     *   "设备ID2": {
     *     "content": {
     *       "data": {
     *         "score": 90.0,
     *         ...
     *       },
     *       "children": [...]
     *     }
     *   }
     * }
     * 
*

* 解析路径: deviceMap[deviceId].content.data.score *

* 计算逻辑: 提取所有设备的 score 字段并计算平均值 * * @param deviceResMapJson device_res_map 字段的 JSON 字符串 * @return 设备平均分,如果无法计算则返回 null */ private static Float calculateDeviceScore(String deviceResMapJson) { try { // 1. 解析 JSON 字符串 JSONObject deviceMap = JSON.parseObject(deviceResMapJson); if (deviceMap == null || deviceMap.isEmpty()) { log.debug("设备评估结果映射为空"); return null; } // 2. 提取所有设备的分数 List scores = new ArrayList<>(); for (String deviceId : deviceMap.keySet()) { try { // 获取设备数据对象 JSONObject deviceData = deviceMap.getJSONObject(deviceId); if (deviceData == null) { log.debug("设备数据为空,deviceId: {}", deviceId); continue; } // 获取 content 对象 JSONObject content = deviceData.getJSONObject("content"); if (content == null) { log.debug("设备 content 为空,deviceId: {}", deviceId); continue; } // 获取 data 对象 JSONObject data = content.getJSONObject("data"); if (data == null) { log.debug("设备 data 为空,deviceId: {}", deviceId); continue; } // 获取 score 字段 if (!data.containsKey("score")) { log.debug("设备 data 中不包含 score 字段,deviceId: {}", deviceId); continue; } Float score = data.getFloat("score"); if (score != null && score > 0) { scores.add(score); log.debug("成功提取设备得分,deviceId: {}, score: {}", deviceId, score); } } catch (Exception e) { log.warn("提取设备得分失败,deviceId: {}", deviceId, e); } } // 3. 计算平均分 if (scores.isEmpty()) { log.debug("没有有效的设备得分数据"); return null; } float avgScore = (float) scores.stream() .mapToDouble(Float::doubleValue) .average() .orElse(0.0); log.debug("设备平均得分计算完成,设备数量: {}, 平均分: {}", scores.size(), avgScore); return avgScore; } catch (Exception e) { log.warn("解析设备得分失败,deviceResMapJson: {}", deviceResMapJson, e); return null; } } public static void main(String[] args) { String json = "{\"47\": {\"content\": {\"id\": 0, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"设备体系-标准-层级\", \"path\": \"/\", \"children\": {\"0\": {\"id\": 0, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"塔式起重机\", \"path\": \"/0\", \"children\": {\"1\": {\"id\": 1, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"塔式起重机实体(市站)\", \"path\": \"/0/1\", \"children\": {\"0\": {\"id\": 0, \"data\": {\"score\": 83.111336, \"weight\": 1.0, \"include\": true, \"riskLevel\": \"P3\", \"membershipList\": [0.025, 0.5, 0.3917, 0.0833, 0.0], \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"一般项目\", \"path\": \"/0/1/0\", \"children\": {\"0\": {\"id\": 0, \"data\": {\"score\": 0.0, \"weight\": 1.0, \"include\": true, \"riskLevel\": null, \"membershipList\": null, \"totalErrorCounts\": [0, 1, 0, 0]}, \"name\": \"基础与轨道\", \"path\": \"/0/1/0/0\", \"children\": {}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}, \"childIdCnt\": 0}}}"; Float v = calculateDeviceScore(json); System.out.println(v); } /** * 根据在建工程配置确定项目类别 * * @param scoreData 项目评分数据 * @param config 在建工程配置 * @return 项目类别(A/B/C) */ private String determineOngoingCategory(ProjectScoreData scoreData, JSONObject config) { if (config == null) { log.warn("在建工程配置为空,无法计算项目类别"); return null; } // 解析配置 Float thresholdA = config.getFloat("a"); Float thresholdB = config.getFloat("b"); Float thresholdC = config.getFloat("c"); Float safeProp = config.getFloat("safe_prop"); Float deviceProp = config.getFloat("device_prop"); if (thresholdA == null || thresholdB == null || thresholdC == null || safeProp == null || deviceProp == null) { log.warn("在建工程配置不完整: {}", config.toJSONString()); return null; } // 计算综合得分 float finalScore; if (scoreData.isHasDeviceScore()) { // 有设备得分:按比例计算 finalScore = scoreData.getSafetyScore() * (safeProp / 100.0f) + scoreData.getDeviceScore() * (deviceProp / 100.0f); } else { // 无设备得分:安全检查得分按 100% 计算 finalScore = scoreData.getSafetyScore(); } // 根据阈值判断类别(从高到低判断) if (finalScore >= thresholdC) { return "A"; } else if (finalScore >= thresholdB) { return "B"; } else if (finalScore >= thresholdA) { return "C"; } else { return "C"; // 不满足任何类别 } } /** * 根据固定资产配置确定项目类别 * * @param hazardCounts 隐患等级统计 Map * @param config 固定资产配置 JSON * @return 项目类别(A/B/C) */ private String determineFixedAssetCategory( Map hazardCounts, JSONObject config) { if (config == null) { log.warn("固定资产配置为空,无法计算项目类别"); return null; } // 按优先级顺序判断:A -> B -> C String[] categories = {"a", "b", "c"}; for (String category : categories) { JSONArray categoryRules = config.getJSONArray(category); if (categoryRules == null || categoryRules.isEmpty()) { continue; } // 检查是否满足该类别的任一规则(OR 条件) boolean matchesCategory = false; for (int i = 0; i < categoryRules.size(); i++) { JSONArray rule = categoryRules.getJSONArray(i); if (rule == null) { continue; } // 检查规则中的所有条件(AND 条件) boolean matchesRule = true; for (int j = 0; j < rule.size(); j++) { JSONObject condition = rule.getJSONObject(j); if (condition == null) { continue; } // 条件格式:{"0": 5} 表示 level=0 的隐患数量 >= 5 for (String levelStr : condition.keySet()) { Integer level = Integer.parseInt(levelStr); Integer requiredCount = condition.getInteger(levelStr); Integer actualCount = hazardCounts.getOrDefault(level, 0); if (actualCount < requiredCount) { matchesRule = false; break; } } if (!matchesRule) { break; } } if (matchesRule) { matchesCategory = true; break; } } if (matchesCategory) { return category.toUpperCase(); // 返回 "A", "B", "C" } } return null; // 不满足任何类别 } }