From 375821398917e1f38ae18a243e365e6a00a4f105 Mon Sep 17 00:00:00 2001 From: JiaoTianBo Date: Mon, 30 Mar 2026 14:27:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(risk):=20=E5=AE=9E=E7=8E=B0AI=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=A3=8E=E9=99=A9=E8=AF=84=E4=BC=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增RiskAssessmentResult DTO,定义AI风险评估结果结构 - 在RiskService接口和实现中添加assessProjectRisk方法 - RiskServiceImpl中集成ChatClient调用AI进行全面风险评估 - 构建项目数据上下文包含基本信息、任务统计、团队成员、风险数量、进度和预算等 - 解析AI返回结果,存储识别风险至数据库并更新项目风险等级 - RiskController新增接口 /assess/{projectId} 提供REST风评估入口 - 完善日志记录,捕获异常并返回友好错误信息 --- .../controller/risk/RiskController.java | 24 ++ .../domain/vo/RiskAssessmentResult.java | 249 ++++++++++++++ .../yinlihupo/service/risk/RiskService.java | 12 + .../service/risk/impl/RiskServiceImpl.java | 313 +++++++++++++++++- 4 files changed, 592 insertions(+), 6 deletions(-) create mode 100644 src/main/java/cn/yinlihupo/domain/vo/RiskAssessmentResult.java diff --git a/src/main/java/cn/yinlihupo/controller/risk/RiskController.java b/src/main/java/cn/yinlihupo/controller/risk/RiskController.java index 4239602..d9063eb 100644 --- a/src/main/java/cn/yinlihupo/controller/risk/RiskController.java +++ b/src/main/java/cn/yinlihupo/controller/risk/RiskController.java @@ -6,6 +6,7 @@ import cn.yinlihupo.common.util.ResultUtils; import cn.yinlihupo.common.util.SecurityUtils; import cn.yinlihupo.domain.dto.CreateRiskRequest; import cn.yinlihupo.domain.dto.CreateWorkOrderRequest; +import cn.yinlihupo.domain.vo.RiskAssessmentResult; import cn.yinlihupo.domain.vo.RiskStatisticsVO; import cn.yinlihupo.domain.vo.RiskVO; import cn.yinlihupo.service.risk.RiskService; @@ -196,4 +197,27 @@ public class RiskController { return ResultUtils.error("更新失败: " + e.getMessage()); } } + + // ==================== AI 风险评估接口 ==================== + + /** + * AI风险评估 + * 使用AI能力对项目整体的进度、人员、资金等会影响项目开展的所有因素进行风险评估 + * 评估完成后自动将识别的风险入库 + * + * @param projectId 项目ID + * @return 风险评估结果 + */ + @PostMapping("/assess/{projectId}") + public BaseResponse assessProjectRisk(@PathVariable Long projectId) { + log.info("AI风险评估, projectId: {}", projectId); + + try { + RiskAssessmentResult result = riskService.assessProjectRisk(projectId); + return ResultUtils.success("风险评估完成", result); + } catch (Exception e) { + log.error("AI风险评估失败: {}", e.getMessage(), e); + return ResultUtils.error("风险评估失败: " + e.getMessage()); + } + } } diff --git a/src/main/java/cn/yinlihupo/domain/vo/RiskAssessmentResult.java b/src/main/java/cn/yinlihupo/domain/vo/RiskAssessmentResult.java new file mode 100644 index 0000000..1269191 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/vo/RiskAssessmentResult.java @@ -0,0 +1,249 @@ +package cn.yinlihupo.domain.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * AI项目风险评估结果DTO + * 使用AI对项目整体进行风险评估,分析进度、人员、资金等影响项目开展的因素 + */ +@Data +public class RiskAssessmentResult { + + /** + * 项目ID + */ + private Long projectId; + + /** + * 项目名称 + */ + private String projectName; + + /** + * 整体风险评估等级: critical-严重, high-高, medium-中, low-低 + */ + @JsonProperty("overall_risk_level") + private String overallRiskLevel; + + /** + * 整体风险得分(0-100) + */ + @JsonProperty("overall_risk_score") + private BigDecimal overallRiskScore; + + /** + * 风险评估摘要 + */ + @JsonProperty("assessment_summary") + private String assessmentSummary; + + /** + * 关键风险领域 + */ + @JsonProperty("risk_areas") + private List riskAreas; + + /** + * 识别的风险列表 + */ + @JsonProperty("identified_risks") + private List identifiedRisks; + + /** + * 风险应对建议 + */ + private List recommendations; + + /** + * 评估时间 + */ + private LocalDate assessmentDate; + + // ==================== 内部类定义 ==================== + + /** + * 风险领域评估 + */ + @Data + public static class RiskArea { + /** + * 领域名称: schedule-进度, resource-资源, budget-预算, quality-质量, personnel-人员, external-外部 + */ + @JsonProperty("area_name") + private String areaName; + + /** + * 风险等级 + */ + @JsonProperty("risk_level") + private String riskLevel; + + /** + * 风险得分(0-100) + */ + @JsonProperty("risk_score") + private Integer riskScore; + + /** + * 风险描述 + */ + private String description; + + /** + * 关键指标 + */ + @JsonProperty("key_indicators") + private List keyIndicators; + } + + /** + * 关键指标 + */ + @Data + public static class KeyIndicator { + /** + * 指标名称 + */ + @JsonProperty("indicator_name") + private String indicatorName; + + /** + * 当前值 + */ + @JsonProperty("current_value") + private String currentValue; + + /** + * 目标值/预期值 + */ + @JsonProperty("target_value") + private String targetValue; + + /** + * 状态: normal-正常, warning-警告, critical-严重 + */ + private String status; + + /** + * 说明 + */ + private String description; + } + + /** + * 识别的风险 + */ + @Data + public static class IdentifiedRisk { + /** + * 风险名称 + */ + @JsonProperty("risk_name") + private String riskName; + + /** + * 风险分类: technical-技术风险, schedule-进度风险, cost-成本风险, quality-质量风险, resource-资源风险, external-外部风险, personnel-人员风险 + */ + private String category; + + /** + * 风险描述 + */ + private String description; + + /** + * 发生概率(0-100%) + */ + private Integer probability; + + /** + * 影响程度(1-5) + */ + private Integer impact; + + /** + * 风险等级 + */ + @JsonProperty("risk_level") + private String riskLevel; + + /** + * 影响范围 + */ + @JsonProperty("impact_scope") + private String impactScope; + + /** + * 触发条件 + */ + @JsonProperty("trigger_condition") + private String triggerCondition; + + /** + * 缓解措施 + */ + @JsonProperty("mitigation_plan") + private String mitigationPlan; + + /** + * 应急计划 + */ + @JsonProperty("contingency_plan") + private String contingencyPlan; + + /** + * 建议优先级: critical-紧急, high-高, medium-中, low-低 + */ + private String priority; + } + + /** + * 风险应对建议 + */ + @Data + public static class RiskRecommendation { + /** + * 建议标题 + */ + private String title; + + /** + * 建议类型: preventive-预防性, mitigative-缓解性, contingent-应急性 + */ + @JsonProperty("recommendation_type") + private String recommendationType; + + /** + * 建议内容 + */ + private String content; + + /** + * 优先级 + */ + private String priority; + + /** + * 预期效果 + */ + @JsonProperty("expected_effect") + private String expectedEffect; + + /** + * 实施时间建议 + */ + @JsonProperty("suggested_timeline") + private String suggestedTimeline; + + /** + * 责任方建议 + */ + @JsonProperty("responsible_party") + private String responsibleParty; + } +} diff --git a/src/main/java/cn/yinlihupo/service/risk/RiskService.java b/src/main/java/cn/yinlihupo/service/risk/RiskService.java index d2f3a16..2a5f2c4 100644 --- a/src/main/java/cn/yinlihupo/service/risk/RiskService.java +++ b/src/main/java/cn/yinlihupo/service/risk/RiskService.java @@ -3,6 +3,7 @@ package cn.yinlihupo.service.risk; import cn.yinlihupo.common.page.TableDataInfo; import cn.yinlihupo.domain.dto.CreateRiskRequest; import cn.yinlihupo.domain.dto.CreateWorkOrderRequest; +import cn.yinlihupo.domain.vo.RiskAssessmentResult; import cn.yinlihupo.domain.vo.RiskStatisticsVO; import cn.yinlihupo.domain.vo.RiskVO; @@ -84,4 +85,15 @@ public interface RiskService { * @return 是否成功 */ Boolean batchUpdateStatus(java.util.List riskIds, String status); + + // ==================== AI 风险评估相关接口 ==================== + + /** + * 对项目进行AI风险评估并入库 + * 使用AI能力对项目整体的进度、人员、资金等会影响项目开展的所有因素进行风险评估 + * + * @param projectId 项目ID + * @return 风险评估结果(已入库的风险ID列表) + */ + RiskAssessmentResult assessProjectRisk(Long projectId); } diff --git a/src/main/java/cn/yinlihupo/service/risk/impl/RiskServiceImpl.java b/src/main/java/cn/yinlihupo/service/risk/impl/RiskServiceImpl.java index a11e253..66524fb 100644 --- a/src/main/java/cn/yinlihupo/service/risk/impl/RiskServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/risk/impl/RiskServiceImpl.java @@ -6,14 +6,19 @@ import cn.yinlihupo.common.util.SecurityUtils; import cn.yinlihupo.domain.dto.CreateRiskRequest; import cn.yinlihupo.domain.dto.CreateWorkOrderRequest; import cn.yinlihupo.domain.entity.Project; +import cn.yinlihupo.domain.entity.ProjectMember; import cn.yinlihupo.domain.entity.Risk; import cn.yinlihupo.domain.entity.SysUser; +import cn.yinlihupo.domain.entity.Task; import cn.yinlihupo.domain.entity.WorkOrder; +import cn.yinlihupo.domain.vo.RiskAssessmentResult; import cn.yinlihupo.domain.vo.RiskStatisticsVO; import cn.yinlihupo.domain.vo.RiskVO; import cn.yinlihupo.mapper.ProjectMapper; +import cn.yinlihupo.mapper.ProjectMemberMapper; import cn.yinlihupo.mapper.RiskMapper; import cn.yinlihupo.mapper.SysUserMapper; +import cn.yinlihupo.mapper.TaskMapper; import cn.yinlihupo.mapper.WorkOrderMapper; import cn.yinlihupo.service.risk.RiskService; import cn.yinlihupo.service.risk.WorkOrderService; @@ -21,6 +26,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,9 +47,95 @@ public class RiskServiceImpl implements RiskService { private final RiskMapper riskMapper; private final ProjectMapper projectMapper; + private final ProjectMemberMapper projectMemberMapper; + private final TaskMapper taskMapper; private final SysUserMapper sysUserMapper; private final WorkOrderMapper workOrderMapper; private final WorkOrderService workOrderService; + private final ChatClient chatClient; + + /** + * AI风险评估系统提示词模板 + */ + private static final String RISK_ASSESSMENT_SYSTEM_PROMPT = """ + # 角色 + + 你是一个专业的项目风险管理专家,擅长对项目进行全面的风险评估和分析。 + + # 任务 + + 根据提供的项目数据,对项目整体进行风险评估,分析以下方面的风险因素: + 1. 进度风险 - 项目时间节点、里程碑、任务完成情况 + 2. 资源风险 - 人力、物料、设备等资源分配情况 + 3. 预算风险 - 成本控制、预算执行情况 + 4. 质量风险 - 交付物质量、验收标准 + 5. 人员风险 - 团队配置、人员能力、人员流动 + 6. 外部风险 - 政策变化、市场环境、供应商等 + + # 输出格式 + + 请严格按照以下JSON格式输出: + + ```json + { + "overall_risk_level": "critical/high/medium/low", + "overall_risk_score": 75, + "assessment_summary": "整体风险评估摘要,说明主要风险点和建议", + "risk_areas": [ + { + "area_name": "schedule/resource/budget/quality/personnel/external", + "risk_level": "critical/high/medium/low", + "risk_score": 80, + "description": "该领域的风险描述", + "key_indicators": [ + { + "indicator_name": "指标名称", + "current_value": "当前值", + "target_value": "目标值", + "status": "normal/warning/critical", + "description": "指标说明" + } + ] + } + ], + "identified_risks": [ + { + "risk_name": "风险名称", + "category": "technical/schedule/cost/quality/resource/external/personnel", + "description": "风险详细描述", + "probability": 60, + "impact": 4, + "risk_level": "high", + "impact_scope": "影响范围描述", + "trigger_condition": "触发条件", + "mitigation_plan": "缓解措施", + "contingency_plan": "应急计划", + "priority": "critical/high/medium/low" + } + ], + "recommendations": [ + { + "title": "建议标题", + "recommendation_type": "preventive/mitigative/contingent", + "content": "具体建议内容", + "priority": "high", + "expected_effect": "预期效果", + "suggested_timeline": "建议实施时间", + "responsible_party": "责任方" + } + ] + } + ``` + + # 注意事项 + + 1. probability(发生概率) 范围 0-100 + 2. impact(影响程度) 范围 1-5 + 3. 风险得分计算:probability * impact / 5 * 100,得出0-100的分数 + 4. 风险等级判定:score>=80为critical,60<=score<80为high,40<=score<60为medium,score<40为low + 5. 识别的风险应该具体且可操作,不要泛泛而谈 + 6. 建议要结合项目实际情况,有针对性 + """; @Override @Transactional(rollbackFor = Exception.class) @@ -199,12 +291,8 @@ public class RiskServiceImpl implements RiskService { .like(Risk::getDescription, keyword)); } - // 按风险等级和得分排序 - wrapper.orderByAsc( - r -> "critical".equals(r.getRiskLevel()) ? 1 : - "high".equals(r.getRiskLevel()) ? 2 : - "medium".equals(r.getRiskLevel()) ? 3 : 4 - ).orderByDesc(Risk::getRiskScore); + // 按风险等级和得分排序(使用原生SQL避免Lambda表达式问题) + wrapper.last("ORDER BY CASE risk_level WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END ASC, risk_score DESC"); Page riskPage = riskMapper.selectPage(page, wrapper); @@ -413,4 +501,217 @@ public class RiskServiceImpl implements RiskService { return vo; } + + // ==================== AI 风险评估相关实现 ==================== + + @Override + @Transactional(rollbackFor = Exception.class) + public RiskAssessmentResult assessProjectRisk(Long projectId) { + log.info("开始AI风险评估, projectId: {}", projectId); + + // 1. 验证项目存在 + Project project = projectMapper.selectById(projectId); + if (project == null || project.getDeleted() == 1) { + throw new RuntimeException("项目不存在"); + } + + // 2. 构建项目数据上下文 + String projectDataContext = buildProjectDataContext(projectId); + + // 3. 构建用户提示词 + String userPrompt = """ + 请根据以下项目数据,进行全面的风险评估分析: + + %s + + 请严格按照系统提示词中的JSON格式输出评估结果,确保识别的风险具体且可操作。 + """.formatted(projectDataContext); + + // 4. 调用AI进行风险评估 + log.info("调用AI进行风险评估..."); + RiskAssessmentResult result = chatClient.prompt() + .system(RISK_ASSESSMENT_SYSTEM_PROMPT) + .user(userPrompt) + .call() + .entity(RiskAssessmentResult.class); + + // 5. 设置项目信息 + result.setProjectId(projectId); + result.setProjectName(project.getProjectName()); + result.setAssessmentDate(LocalDate.now()); + + // 6. 将识别的风险入库 + if (result.getIdentifiedRisks() != null && !result.getIdentifiedRisks().isEmpty()) { + List savedRiskIds = saveIdentifiedRisks(result.getIdentifiedRisks(), projectId); + log.info("已保存 {} 个识别的风险", savedRiskIds.size()); + } + + // 7. 更新项目的风险等级 + if (result.getOverallRiskLevel() != null) { + project.setRiskLevel(result.getOverallRiskLevel()); + projectMapper.updateById(project); + } + + log.info("AI风险评估完成, overallRiskLevel: {}", result.getOverallRiskLevel()); + return result; + } + + /** + * 构建项目数据上下文 + */ + private String buildProjectDataContext(Long projectId) { + StringBuilder sb = new StringBuilder(); + + // 项目基本信息 + Project project = projectMapper.selectById(projectId); + sb.append("## 项目基本信息\n"); + sb.append(String.format("- 项目名称: %s\n", project.getProjectName())); + sb.append(String.format("- 项目类型: %s\n", project.getProjectType())); + sb.append(String.format("- 项目状态: %s\n", project.getStatus())); + sb.append(String.format("- 计划开始日期: %s\n", project.getPlanStartDate())); + sb.append(String.format("- 计划结束日期: %s\n", project.getPlanEndDate())); + sb.append(String.format("- 实际开始日期: %s\n", project.getActualStartDate())); + sb.append(String.format("- 当前进度: %s%%\n", project.getProgress())); + sb.append(String.format("- 项目预算: %s %s\n", project.getBudget(), project.getCurrency())); + sb.append(String.format("- 已花费成本: %s %s\n", project.getCost(), project.getCurrency())); + sb.append(String.format("- 优先级: %s\n", project.getPriority())); + sb.append("\n"); + + // 使用 MyBatis 查询关联数据 + // 查询任务统计 + long taskCount = taskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Task::getProjectId, projectId) + .eq(Task::getDeleted, 0) + ); + long completedTaskCount = taskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Task::getProjectId, projectId) + .eq(Task::getDeleted, 0) + .eq(Task::getStatus, "completed") + ); + sb.append("## 任务统计\n"); + sb.append(String.format("- 任务总数: %d\n", taskCount)); + sb.append(String.format("- 已完成任务: %d\n", completedTaskCount)); + sb.append(String.format("- 任务完成率: %.1f%%\n", taskCount > 0 ? (completedTaskCount * 100.0 / taskCount) : 0)); + sb.append("\n"); + + // 查询成员数量 + long memberCount = projectMemberMapper.selectCount( + new LambdaQueryWrapper() + .eq(ProjectMember::getProjectId, projectId) + .eq(ProjectMember::getDeleted, 0) + ); + sb.append("## 团队成员\n"); + sb.append(String.format("- 团队人数: %d\n", memberCount)); + sb.append("\n"); + + // 查询已识别风险数量 + long existingRiskCount = riskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Risk::getProjectId, projectId) + .eq(Risk::getDeleted, 0) + ); + long highRiskCount = riskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Risk::getProjectId, projectId) + .eq(Risk::getDeleted, 0) + .in(Risk::getRiskLevel, "critical", "high") + ); + sb.append("## 已识别风险\n"); + sb.append(String.format("- 已识别风险数: %d\n", existingRiskCount)); + sb.append(String.format("- 高风险数量: %d\n", highRiskCount)); + sb.append("\n"); + + // 计算项目进度偏差 + if (project.getPlanEndDate() != null && project.getPlanStartDate() != null) { + LocalDate now = LocalDate.now(); + long totalDays = java.time.temporal.ChronoUnit.DAYS.between(project.getPlanStartDate(), project.getPlanEndDate()); + long elapsedDays = java.time.temporal.ChronoUnit.DAYS.between(project.getPlanStartDate(), now); + elapsedDays = Math.max(0, Math.min(elapsedDays, totalDays)); + double expectedProgress = totalDays > 0 ? (elapsedDays * 100.0 / totalDays) : 0; + int progressDeviation = project.getProgress() != null ? (int)(project.getProgress() - expectedProgress) : 0; + + sb.append("## 进度分析\n"); + sb.append(String.format("- 计划工期: %d 天\n", totalDays)); + sb.append(String.format("- 已过时间: %d 天\n", elapsedDays)); + sb.append(String.format("- 预期进度: %.1f%%\n", expectedProgress)); + sb.append(String.format("- 实际进度: %d%%\n", project.getProgress() != null ? project.getProgress() : 0)); + sb.append(String.format("- 进度偏差: %+.1f%%\n", (double) progressDeviation)); + sb.append("\n"); + } + + // 预算使用情况 + if (project.getBudget() != null && project.getBudget().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal cost = project.getCost() != null ? project.getCost() : BigDecimal.ZERO; + double budgetUsage = cost.multiply(BigDecimal.valueOf(100)) + .divide(project.getBudget(), 2, RoundingMode.HALF_UP).doubleValue(); + sb.append("## 预算使用\n"); + sb.append(String.format("- 预算使用率: %.1f%%\n", budgetUsage)); + sb.append(String.format("- 剩余预算: %s %s\n", + project.getBudget().subtract(cost), project.getCurrency())); + sb.append("\n"); + } + + return sb.toString(); + } + + /** + * 保存识别的风险到数据库 + */ + private List saveIdentifiedRisks(List risks, Long projectId) { + List savedIds = new ArrayList<>(); + LocalDateTime now = LocalDateTime.now(); + + for (RiskAssessmentResult.IdentifiedRisk riskInfo : risks) { + Risk risk = new Risk(); + risk.setProjectId(projectId); + risk.setRiskCode(generateRiskCode()); + risk.setRiskName(riskInfo.getRiskName()); + risk.setCategory(riskInfo.getCategory() != null ? riskInfo.getCategory() : "other"); + risk.setDescription(riskInfo.getDescription()); + risk.setRiskSource("ai_detection"); + + if (riskInfo.getProbability() != null) { + risk.setProbability(BigDecimal.valueOf(riskInfo.getProbability())); + } + if (riskInfo.getImpact() != null) { + risk.setImpact(BigDecimal.valueOf(riskInfo.getImpact())); + } + + // 计算风险得分 + if (risk.getProbability() != null && risk.getImpact() != null) { + BigDecimal score = risk.getProbability() + .multiply(risk.getImpact()) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + risk.setRiskScore(score); + } + + // 设置风险等级 + if (riskInfo.getRiskLevel() != null) { + risk.setRiskLevel(riskInfo.getRiskLevel()); + } else { + risk.setRiskLevel(calculateRiskLevel(riskInfo.getProbability(), riskInfo.getImpact())); + } + + risk.setMitigationPlan(riskInfo.getMitigationPlan()); + risk.setContingencyPlan(riskInfo.getContingencyPlan()); + risk.setTriggerCondition(riskInfo.getTriggerCondition()); + risk.setStatus("identified"); + risk.setDiscoverTime(now); + + // 存储 AI 分析结果 + Map aiAnalysis = new HashMap<>(); + aiAnalysis.put("impact_scope", riskInfo.getImpactScope()); + aiAnalysis.put("priority", riskInfo.getPriority()); + aiAnalysis.put("assessment_date", LocalDate.now().toString()); + risk.setAiAnalysis(aiAnalysis); + + riskMapper.insert(risk); + savedIds.add(risk.getId()); + log.debug("保存风险: {}, ID: {}", risk.getRiskName(), risk.getId()); + } + + return savedIds; + } }