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<Integer, String> calculateOngoingProjectCategories(
|
List<Project> projects, Long clientId, Date startDate, Date endDate) {
|
|
Map<Integer, String> 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<Integer> 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<Task> taskWrapper = new LambdaQueryWrapper<>();
|
taskWrapper.in(Task::getProjectId, projectIds)
|
.ge(Task::getDate, startLocalDate)
|
.le(Task::getDate, endLocalDate);
|
List<Task> tasks = taskMapper.selectList(taskWrapper);
|
|
if (tasks.isEmpty()) {
|
log.info("项目没有任务记录,projectIds: {}", projectIds);
|
return categoryMap;
|
}
|
|
List<Integer> checkTaskIds = tasks.stream()
|
.map(Task::getId)
|
.collect(Collectors.toList());
|
|
// 4. 根据评价类型查询评估结果
|
Integer commentType = clientParam.getCommentType();
|
String evalType = (commentType != null && commentType == 1) ? "fuzzy" : "score";
|
|
LambdaQueryWrapper<EvalResult> evalWrapper = new LambdaQueryWrapper<>();
|
evalWrapper.eq(EvalResult::getType, evalType)
|
.in(EvalResult::getCheckTaskId, checkTaskIds)
|
.eq(EvalResult::getDeleted, false);
|
List<EvalResult> evalResults = evalResultMapper.selectList(evalWrapper);
|
|
if (evalResults.isEmpty()) {
|
log.info("项目没有评估结果,projectIds: {}", projectIds);
|
return categoryMap;
|
}
|
|
// 5. 计算项目平均分
|
Map<Integer, ProjectScoreData> projectScoreMap = calculateProjectScores(
|
tasks, evalResults);
|
|
// 6. 根据配置判断项目类别
|
for (Map.Entry<Integer, ProjectScoreData> 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<Integer, String> calculateFixedAssetCategories(
|
List<Project> projects, Long clientId, Date startDate, Date endDate) {
|
|
Map<Integer, String> 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<Integer> 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<Task> taskWrapper = new LambdaQueryWrapper<>();
|
taskWrapper.in(Task::getProjectId, projectIds)
|
.ge(Task::getDate, startLocalDate)
|
.le(Task::getDate, endLocalDate);
|
List<Task> tasks = taskMapper.selectList(taskWrapper);
|
|
if (tasks.isEmpty()) {
|
log.info("项目没有任务记录,projectIds: {}", projectIds);
|
return categoryMap;
|
}
|
|
List<Integer> checkTaskIds = tasks.stream()
|
.map(Task::getId)
|
.collect(Collectors.toList());
|
|
// 4. 查询隐患记录(record_type='error')
|
LambdaQueryWrapper<Record> recordWrapper = new LambdaQueryWrapper<>();
|
recordWrapper.eq(Record::getRecordType, "error")
|
.in(Record::getCheckTaskId, checkTaskIds)
|
.eq(Record::getDeleted, false);
|
List<Record> hazardRecords = recordMapper.selectList(recordWrapper);
|
|
if (hazardRecords.isEmpty()) {
|
log.info("项目没有隐患记录,projectIds: {}", projectIds);
|
return categoryMap;
|
}
|
|
// 5. 按项目分组统计隐患等级数量
|
Map<Integer, Integer> taskToProjectMap = tasks.stream()
|
.collect(Collectors.toMap(Task::getId, Task::getProjectId));
|
|
Map<Integer, Map<Integer, Integer>> 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<Integer, Map<Integer, Integer>> entry : projectHazardMap.entrySet()) {
|
Integer projectId = entry.getKey();
|
Map<Integer, Integer> 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<ClientParam> 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<Integer, ProjectScoreData> calculateProjectScores(
|
List<Task> tasks, List<EvalResult> evalResults) {
|
|
Map<Integer, ProjectScoreData> projectScoreMap = new HashMap<>();
|
|
// 按 check_task_id 关联到 project_id
|
Map<Integer, Integer> checkToProjectMap = tasks.stream()
|
.collect(Collectors.toMap(Task::getId, Task::getProjectId));
|
|
// 按项目分组评估结果
|
Map<Integer, List<EvalResult>> projectEvalMap = evalResults.stream()
|
.filter(e -> checkToProjectMap.containsKey(e.getCheckTaskId()))
|
.collect(Collectors.groupingBy(e -> checkToProjectMap.get(e.getCheckTaskId())));
|
|
// 计算每个项目的平均分
|
for (Map.Entry<Integer, List<EvalResult>> entry : projectEvalMap.entrySet()) {
|
Integer projectId = entry.getKey();
|
List<EvalResult> results = entry.getValue();
|
|
// 计算安全检查平均分
|
double avgSafetyScore = results.stream()
|
.filter(e -> e.getScore() != null)
|
.mapToDouble(EvalResult::getScore)
|
.average()
|
.orElse(0.0);
|
|
System.out.println("results"+results);
|
|
// 计算设备检查平均分(需要解析 device_res_map)
|
List<Float> 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;
|
}
|
|
/**
|
* 计算设备得分
|
* <p>
|
* device_res_map 字段的 JSON 结构(来自 estate_expert 项目):
|
* <pre>
|
* {
|
* "设备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": [...]
|
* }
|
* }
|
* }
|
* </pre>
|
* <p>
|
* 解析路径: deviceMap[deviceId].content.data.score
|
* <p>
|
* 计算逻辑: 提取所有设备的 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<Float> 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 >= thresholdA) {
|
return "A";
|
} else if (finalScore >= thresholdB) {
|
return "B";
|
} else if (finalScore >= thresholdC) {
|
return "C";
|
} else {
|
return "C"; // 不满足任何类别
|
}
|
}
|
|
/**
|
* 根据固定资产配置确定项目类别
|
*
|
* @param hazardCounts 隐患等级统计 Map<level, count>
|
* @param config 固定资产配置 JSON
|
* @return 项目类别(A/B/C)
|
*/
|
private String determineFixedAssetCategory(
|
Map<Integer, Integer> 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; // 不满足任何类别
|
}
|
}
|