feat(risk): 实现AI项目风险评估功能

- 新增RiskAssessmentResult DTO,定义AI风险评估结果结构
- 在RiskService接口和实现中添加assessProjectRisk方法
- RiskServiceImpl中集成ChatClient调用AI进行全面风险评估
- 构建项目数据上下文包含基本信息、任务统计、团队成员、风险数量、进度和预算等
- 解析AI返回结果,存储识别风险至数据库并更新项目风险等级
- RiskController新增接口 /assess/{projectId} 提供REST风评估入口
- 完善日志记录,捕获异常并返回友好错误信息
This commit is contained in:
2026-03-30 14:27:40 +08:00
parent 4e1415a033
commit 3758213989
4 changed files with 592 additions and 6 deletions

View File

@@ -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<Long> riskIds, String status);
// ==================== AI 风险评估相关接口 ====================
/**
* 对项目进行AI风险评估并入库
* 使用AI能力对项目整体的进度、人员、资金等会影响项目开展的所有因素进行风险评估
*
* @param projectId 项目ID
* @return 风险评估结果已入库的风险ID列表
*/
RiskAssessmentResult assessProjectRisk(Long projectId);
}

View File

@@ -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为critical60<=score<80为high40<=score<60为mediumscore<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<Risk> 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<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;
}
}