feat(risk): 实现AI项目风险评估功能
- 新增RiskAssessmentResult DTO,定义AI风险评估结果结构
- 在RiskService接口和实现中添加assessProjectRisk方法
- RiskServiceImpl中集成ChatClient调用AI进行全面风险评估
- 构建项目数据上下文包含基本信息、任务统计、团队成员、风险数量、进度和预算等
- 解析AI返回结果,存储识别风险至数据库并更新项目风险等级
- RiskController新增接口 /assess/{projectId} 提供REST风评估入口
- 完善日志记录,捕获异常并返回友好错误信息
This commit is contained in:
@@ -6,6 +6,7 @@ import cn.yinlihupo.common.util.ResultUtils;
|
|||||||
import cn.yinlihupo.common.util.SecurityUtils;
|
import cn.yinlihupo.common.util.SecurityUtils;
|
||||||
import cn.yinlihupo.domain.dto.CreateRiskRequest;
|
import cn.yinlihupo.domain.dto.CreateRiskRequest;
|
||||||
import cn.yinlihupo.domain.dto.CreateWorkOrderRequest;
|
import cn.yinlihupo.domain.dto.CreateWorkOrderRequest;
|
||||||
|
import cn.yinlihupo.domain.vo.RiskAssessmentResult;
|
||||||
import cn.yinlihupo.domain.vo.RiskStatisticsVO;
|
import cn.yinlihupo.domain.vo.RiskStatisticsVO;
|
||||||
import cn.yinlihupo.domain.vo.RiskVO;
|
import cn.yinlihupo.domain.vo.RiskVO;
|
||||||
import cn.yinlihupo.service.risk.RiskService;
|
import cn.yinlihupo.service.risk.RiskService;
|
||||||
@@ -196,4 +197,27 @@ public class RiskController {
|
|||||||
return ResultUtils.error("更新失败: " + e.getMessage());
|
return ResultUtils.error("更新失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== AI 风险评估接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI风险评估
|
||||||
|
* 使用AI能力对项目整体的进度、人员、资金等会影响项目开展的所有因素进行风险评估
|
||||||
|
* 评估完成后自动将识别的风险入库
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @return 风险评估结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/assess/{projectId}")
|
||||||
|
public BaseResponse<RiskAssessmentResult> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
249
src/main/java/cn/yinlihupo/domain/vo/RiskAssessmentResult.java
Normal file
249
src/main/java/cn/yinlihupo/domain/vo/RiskAssessmentResult.java
Normal file
@@ -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<RiskArea> riskAreas;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 识别的风险列表
|
||||||
|
*/
|
||||||
|
@JsonProperty("identified_risks")
|
||||||
|
private List<IdentifiedRisk> identifiedRisks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 风险应对建议
|
||||||
|
*/
|
||||||
|
private List<RiskRecommendation> 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<KeyIndicator> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package cn.yinlihupo.service.risk;
|
|||||||
import cn.yinlihupo.common.page.TableDataInfo;
|
import cn.yinlihupo.common.page.TableDataInfo;
|
||||||
import cn.yinlihupo.domain.dto.CreateRiskRequest;
|
import cn.yinlihupo.domain.dto.CreateRiskRequest;
|
||||||
import cn.yinlihupo.domain.dto.CreateWorkOrderRequest;
|
import cn.yinlihupo.domain.dto.CreateWorkOrderRequest;
|
||||||
|
import cn.yinlihupo.domain.vo.RiskAssessmentResult;
|
||||||
import cn.yinlihupo.domain.vo.RiskStatisticsVO;
|
import cn.yinlihupo.domain.vo.RiskStatisticsVO;
|
||||||
import cn.yinlihupo.domain.vo.RiskVO;
|
import cn.yinlihupo.domain.vo.RiskVO;
|
||||||
|
|
||||||
@@ -84,4 +85,15 @@ public interface RiskService {
|
|||||||
* @return 是否成功
|
* @return 是否成功
|
||||||
*/
|
*/
|
||||||
Boolean batchUpdateStatus(java.util.List<Long> riskIds, String status);
|
Boolean batchUpdateStatus(java.util.List<Long> riskIds, String status);
|
||||||
|
|
||||||
|
// ==================== AI 风险评估相关接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对项目进行AI风险评估并入库
|
||||||
|
* 使用AI能力对项目整体的进度、人员、资金等会影响项目开展的所有因素进行风险评估
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @return 风险评估结果(已入库的风险ID列表)
|
||||||
|
*/
|
||||||
|
RiskAssessmentResult assessProjectRisk(Long projectId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,19 @@ import cn.yinlihupo.common.util.SecurityUtils;
|
|||||||
import cn.yinlihupo.domain.dto.CreateRiskRequest;
|
import cn.yinlihupo.domain.dto.CreateRiskRequest;
|
||||||
import cn.yinlihupo.domain.dto.CreateWorkOrderRequest;
|
import cn.yinlihupo.domain.dto.CreateWorkOrderRequest;
|
||||||
import cn.yinlihupo.domain.entity.Project;
|
import cn.yinlihupo.domain.entity.Project;
|
||||||
|
import cn.yinlihupo.domain.entity.ProjectMember;
|
||||||
import cn.yinlihupo.domain.entity.Risk;
|
import cn.yinlihupo.domain.entity.Risk;
|
||||||
import cn.yinlihupo.domain.entity.SysUser;
|
import cn.yinlihupo.domain.entity.SysUser;
|
||||||
|
import cn.yinlihupo.domain.entity.Task;
|
||||||
import cn.yinlihupo.domain.entity.WorkOrder;
|
import cn.yinlihupo.domain.entity.WorkOrder;
|
||||||
|
import cn.yinlihupo.domain.vo.RiskAssessmentResult;
|
||||||
import cn.yinlihupo.domain.vo.RiskStatisticsVO;
|
import cn.yinlihupo.domain.vo.RiskStatisticsVO;
|
||||||
import cn.yinlihupo.domain.vo.RiskVO;
|
import cn.yinlihupo.domain.vo.RiskVO;
|
||||||
import cn.yinlihupo.mapper.ProjectMapper;
|
import cn.yinlihupo.mapper.ProjectMapper;
|
||||||
|
import cn.yinlihupo.mapper.ProjectMemberMapper;
|
||||||
import cn.yinlihupo.mapper.RiskMapper;
|
import cn.yinlihupo.mapper.RiskMapper;
|
||||||
import cn.yinlihupo.mapper.SysUserMapper;
|
import cn.yinlihupo.mapper.SysUserMapper;
|
||||||
|
import cn.yinlihupo.mapper.TaskMapper;
|
||||||
import cn.yinlihupo.mapper.WorkOrderMapper;
|
import cn.yinlihupo.mapper.WorkOrderMapper;
|
||||||
import cn.yinlihupo.service.risk.RiskService;
|
import cn.yinlihupo.service.risk.RiskService;
|
||||||
import cn.yinlihupo.service.risk.WorkOrderService;
|
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 com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -41,9 +47,95 @@ public class RiskServiceImpl implements RiskService {
|
|||||||
|
|
||||||
private final RiskMapper riskMapper;
|
private final RiskMapper riskMapper;
|
||||||
private final ProjectMapper projectMapper;
|
private final ProjectMapper projectMapper;
|
||||||
|
private final ProjectMemberMapper projectMemberMapper;
|
||||||
|
private final TaskMapper taskMapper;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
private final WorkOrderMapper workOrderMapper;
|
private final WorkOrderMapper workOrderMapper;
|
||||||
private final WorkOrderService workOrderService;
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@@ -199,12 +291,8 @@ public class RiskServiceImpl implements RiskService {
|
|||||||
.like(Risk::getDescription, keyword));
|
.like(Risk::getDescription, keyword));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按风险等级和得分排序
|
// 按风险等级和得分排序(使用原生SQL避免Lambda表达式问题)
|
||||||
wrapper.orderByAsc(
|
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");
|
||||||
r -> "critical".equals(r.getRiskLevel()) ? 1 :
|
|
||||||
"high".equals(r.getRiskLevel()) ? 2 :
|
|
||||||
"medium".equals(r.getRiskLevel()) ? 3 : 4
|
|
||||||
).orderByDesc(Risk::getRiskScore);
|
|
||||||
|
|
||||||
Page<Risk> riskPage = riskMapper.selectPage(page, wrapper);
|
Page<Risk> riskPage = riskMapper.selectPage(page, wrapper);
|
||||||
|
|
||||||
@@ -413,4 +501,217 @@ public class RiskServiceImpl implements RiskService {
|
|||||||
|
|
||||||
return vo;
|
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<Long> 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<Task>()
|
||||||
|
.eq(Task::getProjectId, projectId)
|
||||||
|
.eq(Task::getDeleted, 0)
|
||||||
|
);
|
||||||
|
long completedTaskCount = taskMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Task>()
|
||||||
|
.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<ProjectMember>()
|
||||||
|
.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<Risk>()
|
||||||
|
.eq(Risk::getProjectId, projectId)
|
||||||
|
.eq(Risk::getDeleted, 0)
|
||||||
|
);
|
||||||
|
long highRiskCount = riskMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Risk>()
|
||||||
|
.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<Long> saveIdentifiedRisks(List<RiskAssessmentResult.IdentifiedRisk> risks, Long projectId) {
|
||||||
|
List<Long> 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<String, Object> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user