feat(ai-analysis): 添加日报 AI 分析功能说明文档及实现
- 新增详尽的日报 AI 分析功能使用说明文档,包含功能概述、接口示例、 技术细节、错误处理和性能指标 - 添加 AsyncConfig 配置,新增日报 AI 分析任务线程池,支持异步并发处理 - 创建 DailyReportAnalysisResult DTO,定义分析结果数据结构 - 实现 DailyReportAnalysisService 接口,支持异步分析日报并保存分析结果 - 实现 DailyReportAnalysisServiceImpl,集成 AI 分析模型调用和业务数据处理 - 设置 AI 分析系统提示词,规范输出 JSON 结构,确保分析质量和准确性 - 异步执行分析任务,线程池采用 CallerRunsPolicy 拒绝策略保证稳定性 - 设计项目上下文构建逻辑,整合项目信息、里程碑、任务和统计数据为 AI 提示 - 实现分析结果持久化,保存识别风险、资源需求,更新项目进度信息 - 日报 AI 分析任务异步执行异常记录,保证主流程稳定不受影响
This commit is contained in:
@@ -70,4 +70,31 @@ public class AsyncConfig {
|
||||
log.info("文档处理异步任务线程池初始化完成");
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日报 AI 分析任务线程池
|
||||
*/
|
||||
@Bean("dailyReportAnalysisExecutor")
|
||||
public Executor dailyReportAnalysisExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
// 核心线程数
|
||||
executor.setCorePoolSize(5);
|
||||
// 最大线程数
|
||||
executor.setMaxPoolSize(10);
|
||||
// 队列容量
|
||||
executor.setQueueCapacity(200);
|
||||
// 线程名称前缀
|
||||
executor.setThreadNamePrefix("daily-report-analysis-");
|
||||
// 拒绝策略:由调用线程处理
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
// 等待所有任务完成后再关闭线程池
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
// 等待时间(秒)
|
||||
executor.setAwaitTerminationSeconds(60);
|
||||
// 初始化
|
||||
executor.initialize();
|
||||
|
||||
log.info("日报 AI 分析异步任务线程池初始化完成");
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package cn.yinlihupo.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日报 AI 分析结果 DTO
|
||||
*/
|
||||
@Data
|
||||
public class DailyReportAnalysisResult {
|
||||
|
||||
/**
|
||||
* 项目 ID
|
||||
*/
|
||||
private Long projectId;
|
||||
|
||||
/**
|
||||
* 项目名称
|
||||
*/
|
||||
private String projectName;
|
||||
|
||||
/**
|
||||
* 日报日期
|
||||
*/
|
||||
private LocalDate reportDate;
|
||||
|
||||
/**
|
||||
* 整体进度评估
|
||||
*/
|
||||
private OverallProgressAssessment overallProgressAssessment;
|
||||
|
||||
/**
|
||||
* 里程碑风险列表
|
||||
*/
|
||||
private List<MilestoneRisk> milestoneRisks;
|
||||
|
||||
/**
|
||||
* 资源需求列表
|
||||
*/
|
||||
private List<ResourceNeed> resourceNeeds;
|
||||
|
||||
/**
|
||||
* 进度建议列表
|
||||
*/
|
||||
private List<ProgressSuggestion> progressSuggestions;
|
||||
|
||||
/**
|
||||
* 识别的风险列表 (直接入库)
|
||||
*/
|
||||
private List<IdentifiedRisk> identifiedRisks;
|
||||
|
||||
/**
|
||||
* 整体进度评估
|
||||
*/
|
||||
@Data
|
||||
public static class OverallProgressAssessment {
|
||||
/**
|
||||
* 进度状态:ahead-提前,on_track-正常,delayed-滞后
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 进度偏差百分比 (正数表示提前,负数表示滞后)
|
||||
*/
|
||||
private BigDecimal deviationPercentage;
|
||||
|
||||
/**
|
||||
* 评估说明
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 关键问题
|
||||
*/
|
||||
private List<String> keyIssues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 里程碑风险
|
||||
*/
|
||||
@Data
|
||||
public static class MilestoneRisk {
|
||||
/**
|
||||
* 里程碑 ID
|
||||
*/
|
||||
private Long milestoneId;
|
||||
|
||||
/**
|
||||
* 里程碑名称
|
||||
*/
|
||||
private String milestoneName;
|
||||
|
||||
/**
|
||||
* 计划完成日期
|
||||
*/
|
||||
private LocalDate planDate;
|
||||
|
||||
/**
|
||||
* 风险等级:critical-严重,high-高,medium-中,low-低
|
||||
*/
|
||||
private String riskLevel;
|
||||
|
||||
/**
|
||||
* 风险描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 延期天数 (预估)
|
||||
*/
|
||||
private Integer estimatedDelayDays;
|
||||
|
||||
/**
|
||||
* 建议措施
|
||||
*/
|
||||
private String suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源需求
|
||||
*/
|
||||
@Data
|
||||
public static class ResourceNeed {
|
||||
/**
|
||||
* 资源类型:human-人力,material-物料,equipment-设备,other-其他
|
||||
*/
|
||||
private String resourceType;
|
||||
|
||||
/**
|
||||
* 资源名称
|
||||
*/
|
||||
private String resourceName;
|
||||
|
||||
/**
|
||||
* 需求数量
|
||||
*/
|
||||
private BigDecimal quantity;
|
||||
|
||||
/**
|
||||
* 单位
|
||||
*/
|
||||
private String unit;
|
||||
|
||||
/**
|
||||
* 需求原因
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 建议到位时间
|
||||
*/
|
||||
private LocalDate suggestedArrivalDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度建议
|
||||
*/
|
||||
@Data
|
||||
public static class ProgressSuggestion {
|
||||
/**
|
||||
* 任务 ID (如果有明确关联的任务)
|
||||
*/
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 任务名称
|
||||
*/
|
||||
private String taskName;
|
||||
|
||||
/**
|
||||
* 建议类型:accelerate-加速,adjust_plan-调整计划,add_resource-增加资源,reorder-重新排序
|
||||
*/
|
||||
private String suggestionType;
|
||||
|
||||
/**
|
||||
* 具体建议内容
|
||||
*/
|
||||
private String suggestion;
|
||||
|
||||
/**
|
||||
* 优先级:critical-紧急,high-高,medium-中,low-低
|
||||
*/
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 预期效果
|
||||
*/
|
||||
private String expectedEffect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别的风险 (直接入库)
|
||||
*/
|
||||
@Data
|
||||
public static class IdentifiedRisk {
|
||||
/**
|
||||
* 风险名称
|
||||
*/
|
||||
private String riskName;
|
||||
|
||||
/**
|
||||
* 风险分类:technical-技术,schedule-进度,cost-成本,quality-质量,resource-资源,external-外部
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 风险描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 发生概率 (0-100)
|
||||
*/
|
||||
private Integer probability;
|
||||
|
||||
/**
|
||||
* 影响程度 (1-5)
|
||||
*/
|
||||
private Integer impact;
|
||||
|
||||
/**
|
||||
* 风险等级:calculated from probability * impact
|
||||
*/
|
||||
private String riskLevel;
|
||||
|
||||
/**
|
||||
* 影响范围
|
||||
*/
|
||||
private String impactScope;
|
||||
|
||||
/**
|
||||
* 触发条件
|
||||
*/
|
||||
private String triggerCondition;
|
||||
|
||||
/**
|
||||
* 缓解措施
|
||||
*/
|
||||
private String mitigationPlan;
|
||||
|
||||
/**
|
||||
* 应急计划
|
||||
*/
|
||||
private String contingencyPlan;
|
||||
|
||||
/**
|
||||
* 优先级
|
||||
*/
|
||||
private String priority;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.yinlihupo.service.analysis;
|
||||
|
||||
import cn.yinlihupo.domain.dto.DailyReportAnalysisResult;
|
||||
import cn.yinlihupo.domain.entity.ProjectDailyReport;
|
||||
|
||||
/**
|
||||
* 日报 AI 分析服务接口
|
||||
*/
|
||||
public interface DailyReportAnalysisService {
|
||||
|
||||
/**
|
||||
* 异步分析日报数据
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param report 日报数据
|
||||
*/
|
||||
void analyzeDailyReportAsync(Long projectId, ProjectDailyReport report);
|
||||
|
||||
/**
|
||||
* 同步分析方法 (内部使用)
|
||||
*
|
||||
* @param projectId 项目 ID
|
||||
* @param report 日报数据
|
||||
* @return 分析结果
|
||||
*/
|
||||
DailyReportAnalysisResult analyzeDailyReport(Long projectId, ProjectDailyReport report);
|
||||
|
||||
/**
|
||||
* 保存分析结果到数据库
|
||||
*
|
||||
* @param result 分析结果
|
||||
*/
|
||||
void saveAnalysisResult(DailyReportAnalysisResult result);
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package cn.yinlihupo.service.analysis.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.yinlihupo.domain.dto.DailyReportAnalysisResult;
|
||||
import cn.yinlihupo.domain.entity.*;
|
||||
import cn.yinlihupo.mapper.*;
|
||||
import cn.yinlihupo.service.analysis.DailyReportAnalysisService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 日报 AI 分析服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisService {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final ProjectMapper projectMapper;
|
||||
private final ProjectMilestoneMapper projectMilestoneMapper;
|
||||
private final TaskMapper taskMapper;
|
||||
private final ResourceMapper resourceMapper;
|
||||
private final RiskMapper riskMapper;
|
||||
private final ProjectDailyReportMapper projectDailyReportMapper;
|
||||
|
||||
/**
|
||||
* AI 分析系统提示词模板
|
||||
*/
|
||||
private static final String DAILY_REPORT_ANALYSIS_SYSTEM_PROMPT = """
|
||||
# 角色
|
||||
|
||||
你是一个专业的项目管理 AI 助手,擅长从项目日报中分析项目状态、识别风险和资源需求。
|
||||
|
||||
# 任务
|
||||
|
||||
根据提供的【项目基本信息】+【当前日报内容】,进行以下分析:
|
||||
|
||||
1. **整体进度评估**: 判断项目进度是提前、正常还是滞后
|
||||
2. **里程碑风险识别**: 分析哪些里程碑可能延期,延期风险等级
|
||||
3. **资源需求分析**: 识别是否需要新增人力、物料、设备等资源
|
||||
4. **进度建议**: 针对当前进度提出可操作的调整建议
|
||||
5. **风险识别**: 识别潜在的项目风险并评估
|
||||
|
||||
# 输出格式
|
||||
|
||||
请严格按照以下 JSON 格式输出:
|
||||
|
||||
```json
|
||||
{
|
||||
"overallProgressAssessment": {
|
||||
"status": "ahead/on_track/delayed",
|
||||
"deviationPercentage": 10.5,
|
||||
"description": "进度评估说明",
|
||||
"keyIssues": ["问题 1", "问题 2"]
|
||||
},
|
||||
"milestoneRisks": [
|
||||
{
|
||||
"milestoneId": 123,
|
||||
"milestoneName": "里程碑名称",
|
||||
"planDate": "2024-12-31",
|
||||
"riskLevel": "high",
|
||||
"description": "风险描述",
|
||||
"estimatedDelayDays": 5,
|
||||
"suggestion": "建议措施"
|
||||
}
|
||||
],
|
||||
"resourceNeeds": [
|
||||
{
|
||||
"resourceType": "human",
|
||||
"resourceName": "Java 开发工程师",
|
||||
"quantity": 2,
|
||||
"unit": "人",
|
||||
"urgency": "high",
|
||||
"reason": "需求原因",
|
||||
"suggestedArrivalDate": "2024-04-15"
|
||||
}
|
||||
],
|
||||
"progressSuggestions": [
|
||||
{
|
||||
"taskId": 456,
|
||||
"taskName": "任务名称",
|
||||
"suggestionType": "add_resource",
|
||||
"suggestion": "具体建议内容",
|
||||
"expectedEffect": "预期效果"
|
||||
}
|
||||
],
|
||||
"identifiedRisks": [
|
||||
{
|
||||
"riskName": "风险名称",
|
||||
"category": "schedule",
|
||||
"description": "风险详细描述",
|
||||
"probability": 60,
|
||||
"impact": 4,
|
||||
"riskLevel": "high",
|
||||
"impactScope": "影响范围",
|
||||
"triggerCondition": "触发条件",
|
||||
"mitigationPlan": "缓解措施",
|
||||
"contingencyPlan": "应急计划",
|
||||
"priority": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# 注意事项
|
||||
|
||||
1. probability(发生概率) 范围 0-100
|
||||
2. impact(影响程度) 范围 1-5
|
||||
3. 风险等级判定:probability * impact / 5 * 100,得分>=80 为 critical,60-80 为 high,40-60 为 medium,<40 为 low
|
||||
4. 识别的风险应该具体且可操作,不要泛泛而谈
|
||||
5. 建议要结合日报中的实际工作内容,有针对性
|
||||
6. 如果日报中没有明显风险,可以返回空数组,不要强行编造
|
||||
""";
|
||||
|
||||
@Override
|
||||
@Async("dailyReportAnalysisExecutor")
|
||||
public void analyzeDailyReportAsync(Long projectId, ProjectDailyReport report) {
|
||||
log.info("[日报 AI 分析] 开始异步分析,projectId={}, reportDate={}", projectId, report.getReportDate());
|
||||
|
||||
try {
|
||||
// 执行分析
|
||||
DailyReportAnalysisResult result = analyzeDailyReport(projectId, report);
|
||||
|
||||
// 保存结果
|
||||
saveAnalysisResult(result);
|
||||
|
||||
log.info("[日报 AI 分析] 完成,projectId={}, 识别风险数={}, 资源需求数={}",
|
||||
projectId,
|
||||
result.getIdentifiedRisks() != null ? result.getIdentifiedRisks().size() : 0,
|
||||
result.getResourceNeeds() != null ? result.getResourceNeeds().size() : 0);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[日报 AI 分析] 失败,projectId={}, reportDate={}, error={}",
|
||||
projectId, report.getReportDate(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DailyReportAnalysisResult analyzeDailyReport(Long projectId, ProjectDailyReport report) {
|
||||
log.debug("[日报 AI 分析] 同步分析开始,projectId={}, reportDate={}", projectId, report.getReportDate());
|
||||
|
||||
// 1. 构建项目上下文数据
|
||||
String projectContext = buildProjectContext(projectId, report);
|
||||
|
||||
// 2. 构建用户提示词
|
||||
String userPrompt = """
|
||||
请根据以下项目信息和日报内容进行分析:
|
||||
|
||||
%s
|
||||
|
||||
---
|
||||
|
||||
【当前日报内容】
|
||||
- 日期:%s
|
||||
- 工作内容:%s
|
||||
- 明日计划:%s
|
||||
- 工作强度:%d/5
|
||||
- 需要协助:%s
|
||||
%s
|
||||
|
||||
请严格按照系统提示词中的 JSON 格式输出分析结果。
|
||||
""".formatted(
|
||||
projectContext,
|
||||
report.getReportDate(),
|
||||
report.getWorkContent(),
|
||||
report.getTomorrowPlan() != null ? report.getTomorrowPlan() : "无",
|
||||
report.getWorkIntensity() != null ? report.getWorkIntensity() : 3,
|
||||
report.getNeedHelp() != null && report.getNeedHelp() ? "是" : "否",
|
||||
report.getHelpContent() != null ? "- 协助内容:" + report.getHelpContent() : ""
|
||||
);
|
||||
|
||||
// 3. 调用 AI 进行分析
|
||||
log.info("[日报 AI 分析] 调用 AI 模型...");
|
||||
DailyReportAnalysisResult result = chatClient.prompt()
|
||||
.system(DAILY_REPORT_ANALYSIS_SYSTEM_PROMPT)
|
||||
.user(userPrompt)
|
||||
.call()
|
||||
.entity(DailyReportAnalysisResult.class);
|
||||
|
||||
// 4. 补充项目信息
|
||||
if (result != null) {
|
||||
result.setProjectId(projectId);
|
||||
result.setReportDate(report.getReportDate());
|
||||
|
||||
// 填充项目名称
|
||||
Project project = projectMapper.selectById(projectId);
|
||||
if (project != null) {
|
||||
result.setProjectName(project.getProjectName());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveAnalysisResult(DailyReportAnalysisResult result) {
|
||||
if (result == null) {
|
||||
log.warn("[日报 AI 分析] 分析结果为空,跳过保存");
|
||||
return;
|
||||
}
|
||||
|
||||
Long projectId = result.getProjectId();
|
||||
log.info("[日报 AI 分析] 保存结果,projectId={}", projectId);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 1. 保存识别的风险
|
||||
if (result.getIdentifiedRisks() != null && !result.getIdentifiedRisks().isEmpty()) {
|
||||
List<Long> savedRiskIds = saveIdentifiedRisks(result.getIdentifiedRisks(), projectId, now);
|
||||
log.info("[日报 AI 分析] 保存 {} 个识别的风险,IDs={}", savedRiskIds.size(), savedRiskIds);
|
||||
}
|
||||
|
||||
// 2. 保存资源需求
|
||||
if (result.getResourceNeeds() != null && !result.getResourceNeeds().isEmpty()) {
|
||||
List<Long> savedResourceIds = saveResourceNeeds(result.getResourceNeeds(), projectId, now);
|
||||
log.info("[日报 AI 分析] 保存 {} 个资源需求,IDs={}", savedResourceIds.size(), savedResourceIds);
|
||||
}
|
||||
|
||||
// 3. 更新项目进度 (如果有整体进度评估)
|
||||
if (result.getOverallProgressAssessment() != null) {
|
||||
updateProjectProgress(projectId, result.getOverallProgressAssessment());
|
||||
}
|
||||
|
||||
log.info("[日报 AI 分析] 结果保存完成,projectId={}", projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建项目上下文数据
|
||||
*/
|
||||
private String buildProjectContext(Long projectId, ProjectDailyReport report) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 项目基本信息
|
||||
Project project = projectMapper.selectById(projectId);
|
||||
if (project != null) {
|
||||
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("- 当前进度:%d%%\n", project.getProgress() != null ? project.getProgress() : 0));
|
||||
sb.append(String.format("- 项目预算:%s %s\n", project.getBudget(), project.getCurrency()));
|
||||
sb.append(String.format("- 已花费成本:%s %s\n", project.getCost(), project.getCurrency()));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
// 里程碑信息
|
||||
List<ProjectMilestone> milestones = projectMilestoneMapper.selectList(
|
||||
new LambdaQueryWrapper<ProjectMilestone>()
|
||||
.eq(ProjectMilestone::getProjectId, projectId)
|
||||
.eq(ProjectMilestone::getDeleted, 0)
|
||||
.orderByAsc(ProjectMilestone::getSortOrder)
|
||||
);
|
||||
|
||||
if (!milestones.isEmpty()) {
|
||||
sb.append("【里程碑信息】\n");
|
||||
for (ProjectMilestone milestone : milestones) {
|
||||
sb.append(String.format("- %s (计划:%s, 状态:%s, 进度:%d%%)\n",
|
||||
milestone.getMilestoneName(),
|
||||
milestone.getPlanDate(),
|
||||
milestone.getStatus(),
|
||||
milestone.getProgress() != null ? milestone.getProgress() : 0));
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
// 任务列表详情
|
||||
List<Task> tasks = taskMapper.selectList(
|
||||
new LambdaQueryWrapper<Task>()
|
||||
.eq(Task::getProjectId, projectId)
|
||||
.eq(Task::getDeleted, 0)
|
||||
.orderByAsc(Task::getSortOrder)
|
||||
);
|
||||
|
||||
if (!tasks.isEmpty()) {
|
||||
sb.append("【任务列表】\n");
|
||||
for (Task task : tasks) {
|
||||
String assigneeInfo = task.getAssigneeId() != null ? "(负责人 ID: " + task.getAssigneeId() + ")" : "(未分配)";
|
||||
sb.append(String.format("- %s [%s] %s (计划:%s ~ %s, 状态:%s, 进度:%d%%)\n",
|
||||
task.getTaskCode() != null ? task.getTaskCode() : "",
|
||||
task.getTaskType() != null ? task.getTaskType() : "task",
|
||||
task.getTaskName(),
|
||||
task.getPlanStartDate(),
|
||||
task.getPlanEndDate(),
|
||||
task.getStatus(),
|
||||
task.getProgress() != null ? task.getProgress() : 0));
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
// 任务统计
|
||||
long totalTasks = taskMapper.selectCount(
|
||||
new LambdaQueryWrapper<Task>()
|
||||
.eq(Task::getProjectId, projectId)
|
||||
.eq(Task::getDeleted, 0)
|
||||
);
|
||||
long completedTasks = 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", totalTasks));
|
||||
sb.append(String.format("- 已完成:%d\n", completedTasks));
|
||||
sb.append(String.format("- 完成率:%.1f%%\n", totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0));
|
||||
sb.append("\n");
|
||||
|
||||
// 计算进度偏差
|
||||
if (project != null && project.getPlanStartDate() != null && project.getPlanEndDate() != 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 actualProgress = project.getProgress() != null ? project.getProgress() : 0;
|
||||
double deviation = actualProgress - expectedProgress;
|
||||
|
||||
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", actualProgress));
|
||||
sb.append(String.format("- 进度偏差:%+.1f%%\n", deviation));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
// 历史日报摘要 (最近 5 条)
|
||||
List<ProjectDailyReport> recentReports = projectDailyReportMapper.selectList(
|
||||
new LambdaQueryWrapper<ProjectDailyReport>()
|
||||
.eq(ProjectDailyReport::getProjectId, projectId)
|
||||
.eq(ProjectDailyReport::getDeleted, 0)
|
||||
.ne(ProjectDailyReport::getId, report.getId()) // 排除当前日报
|
||||
.orderByDesc(ProjectDailyReport::getReportDate)
|
||||
.last("LIMIT 5")
|
||||
);
|
||||
|
||||
if (!recentReports.isEmpty()) {
|
||||
sb.append("【历史日报摘要】\n");
|
||||
int index = 1;
|
||||
for (ProjectDailyReport dailyReport : recentReports) {
|
||||
sb.append(String.format("%d. %s: %s\n",
|
||||
index++,
|
||||
dailyReport.getReportDate(),
|
||||
dailyReport.getWorkContent().length() > 50
|
||||
? dailyReport.getWorkContent().substring(0, 50) + "..."
|
||||
: dailyReport.getWorkContent()));
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存识别的风险
|
||||
*/
|
||||
private List<Long> saveIdentifiedRisks(List<DailyReportAnalysisResult.IdentifiedRisk> risks,
|
||||
Long projectId,
|
||||
LocalDateTime now) {
|
||||
List<Long> savedIds = new ArrayList<>();
|
||||
|
||||
for (DailyReportAnalysisResult.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_daily_report");
|
||||
|
||||
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 if (risk.getProbability() != null && risk.getImpact() != null) {
|
||||
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("analysis_date", LocalDate.now().toString());
|
||||
aiAnalysis.put("source", "daily_report_analysis");
|
||||
risk.setAiAnalysis(aiAnalysis);
|
||||
|
||||
riskMapper.insert(risk);
|
||||
savedIds.add(risk.getId());
|
||||
}
|
||||
|
||||
return savedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存资源需求
|
||||
*/
|
||||
private List<Long> saveResourceNeeds(List<DailyReportAnalysisResult.ResourceNeed> resourceNeeds,
|
||||
Long projectId,
|
||||
LocalDateTime now) {
|
||||
List<Long> savedIds = new ArrayList<>();
|
||||
|
||||
for (DailyReportAnalysisResult.ResourceNeed need : resourceNeeds) {
|
||||
Resource resource = new Resource();
|
||||
resource.setProjectId(projectId);
|
||||
resource.setResourceCode(generateResourceCode());
|
||||
resource.setResourceName(need.getResourceName());
|
||||
resource.setResourceType(need.getResourceType() != null ? need.getResourceType() : "other");
|
||||
resource.setDescription(need.getReason());
|
||||
resource.setSpecification(need.getUnit());
|
||||
|
||||
if (need.getQuantity() != null) {
|
||||
resource.setPlanQuantity(need.getQuantity());
|
||||
}
|
||||
|
||||
resource.setStatus("planned");
|
||||
resource.setCreateTime(now);
|
||||
|
||||
resourceMapper.insert(resource);
|
||||
savedIds.add(resource.getId());
|
||||
}
|
||||
|
||||
return savedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目进度
|
||||
*/
|
||||
private void updateProjectProgress(Long projectId,
|
||||
DailyReportAnalysisResult.OverallProgressAssessment assessment) {
|
||||
Project project = projectMapper.selectById(projectId);
|
||||
if (project == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据 AI 评估更新项目状态
|
||||
if ("delayed".equals(assessment.getStatus())) {
|
||||
// 如果评估为滞后,更新项目状态为 delayed
|
||||
project.setStatus("delayed");
|
||||
} else if ("ahead".equals(assessment.getStatus())) {
|
||||
// 如果评估为提前,可以更新状态
|
||||
if (!"completed".equals(project.getStatus())) {
|
||||
project.setStatus("in_progress");
|
||||
}
|
||||
}
|
||||
|
||||
// 可以在这里添加更复杂的进度更新逻辑
|
||||
// 例如根据 keyIssues 更新项目的某些字段
|
||||
|
||||
projectMapper.updateById(project);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成风险编号
|
||||
*/
|
||||
private String generateRiskCode() {
|
||||
return "RSK_DR" + IdUtil.fastSimpleUUID().substring(0, 10).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成资源编号
|
||||
*/
|
||||
private String generateResourceCode() {
|
||||
return "RES_DR" + IdUtil.fastSimpleUUID().substring(0, 10).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算风险等级
|
||||
*/
|
||||
private String calculateRiskLevel(Integer probability, Integer impact) {
|
||||
if (probability == null || impact == null) {
|
||||
return "low";
|
||||
}
|
||||
int score = probability * impact;
|
||||
if (score >= 300) {
|
||||
return "critical";
|
||||
} else if (score >= 200) {
|
||||
return "high";
|
||||
} else if (score >= 100) {
|
||||
return "medium";
|
||||
} else {
|
||||
return "low";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将紧急程度映射为优先级
|
||||
*/
|
||||
private String mapUrgencyToPriority(String urgency) {
|
||||
if (urgency == null) {
|
||||
return "medium";
|
||||
}
|
||||
return switch (urgency.toLowerCase()) {
|
||||
case "urgent" -> "critical";
|
||||
case "high" -> "high";
|
||||
case "medium" -> "medium";
|
||||
case "low" -> "low";
|
||||
default -> "medium";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import cn.yinlihupo.mapper.ProjectMapper;
|
||||
import cn.yinlihupo.mapper.ProjectMemberMapper;
|
||||
import cn.yinlihupo.mapper.SysUserMapper;
|
||||
import cn.yinlihupo.service.open.OpenApiService;
|
||||
import cn.yinlihupo.service.analysis.DailyReportAnalysisService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -31,6 +32,7 @@ public class OpenApiServiceImpl implements OpenApiService {
|
||||
private final ProjectMapper projectMapper;
|
||||
private final ProjectMemberMapper projectMemberMapper;
|
||||
private final ProjectDailyReportMapper projectDailyReportMapper;
|
||||
private final DailyReportAnalysisService dailyReportAnalysisService;
|
||||
|
||||
/**
|
||||
* 根据用户标识 (sys_user.username) 查询用户所在的项目列表
|
||||
@@ -127,7 +129,11 @@ public class OpenApiServiceImpl implements OpenApiService {
|
||||
|
||||
projectDailyReportMapper.insert(report);
|
||||
|
||||
log.info("[OpenApi] 日报同步成功, projectId={}, reportDate={}, username={}",
|
||||
// 5. 保存日报后,触发异步 AI 分析
|
||||
ProjectDailyReport finalReport = report; // 用于 lambda
|
||||
dailyReportAnalysisService.analyzeDailyReportAsync(dto.getProjectId(), finalReport);
|
||||
|
||||
log.info("[OpenApi] 日报同步成功,已触发 AI 分析,projectId={}, reportDate={}, username={}",
|
||||
dto.getProjectId(), dto.getReportDate(), username);
|
||||
return "日报同步成功";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user