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

@@ -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<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());
}
}
}

View 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;
}
}

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;
}
}