feat(analysis): 添加日报分析进度回写建议功能

- 新增日报分析建议服务,支持获取和应用进度更新建议
- 添加进度回写建议数据结构,支持任务和里程碑状态/进度更新
- 移除项目时间节点相关功能,简化项目数据结构
- 扩展AI分析提示词,要求生成可回写的进度建议
- 新增相关Mapper、Controller和服务实现
- 优化JsonbTypeHandler以支持日期序列化
This commit is contained in:
2026-04-01 11:18:31 +08:00
parent 7cf624c933
commit 3bca27254b
17 changed files with 725 additions and 105 deletions

View File

@@ -2,6 +2,7 @@ package cn.yinlihupo.common.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.postgresql.util.PGobject;
@@ -17,7 +18,9 @@ import java.sql.SQLException;
*/
public class JsonbTypeHandler extends BaseTypeHandler<Object> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectMapper objectMapper = new ObjectMapper()
.findAndRegisterModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
private static final String JSONB_TYPE = "jsonb";
@Override

View File

@@ -0,0 +1,97 @@
package cn.yinlihupo.controller.analysis;
import cn.yinlihupo.common.core.BaseResponse;
import cn.yinlihupo.common.enums.ErrorCode;
import cn.yinlihupo.common.exception.BusinessException;
import cn.yinlihupo.common.util.ResultUtils;
import cn.yinlihupo.common.util.SecurityUtils;
import cn.yinlihupo.domain.dto.ApplyDailyReportSuggestionsRequest;
import cn.yinlihupo.domain.entity.DailyReportUpdateSuggestion;
import cn.yinlihupo.domain.vo.DailyReportAnalysisSuggestionsVO;
import cn.yinlihupo.mapper.DailyReportUpdateSuggestionMapper;
import cn.yinlihupo.service.analysis.DailyReportSuggestionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 日报分析建议
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/daily-report/analysis")
@RequiredArgsConstructor
public class DailyReportAnalysisController {
private final DailyReportSuggestionService dailyReportSuggestionService;
private final DailyReportUpdateSuggestionMapper dailyReportUpdateSuggestionMapper;
/**
* 获取日报进度更新建议
*
* @param projectId 项目ID
* @param reportId 日报ID
* @param reportDate 日报日期
* @param submitterUsername 日报提交人用户名
*/
@GetMapping("/suggestions")
public BaseResponse<DailyReportAnalysisSuggestionsVO> getSuggestions(
@RequestParam Long projectId,
@RequestParam(required = false) Long reportId,
@RequestParam(required = false) LocalDate reportDate,
@RequestParam(required = false) String submitterUsername) {
DailyReportAnalysisSuggestionsVO vo;
if (reportId != null) {
vo = dailyReportSuggestionService.getLatestSuggestionsByReportId(projectId, reportId);
} else {
vo = dailyReportSuggestionService.getLatestSuggestionsByUniqueKey(projectId, reportDate, submitterUsername);
}
return ResultUtils.success("查询成功", vo);
}
/**
* 应用日报进度回写建议
*
* @param request 建议ID列表
* @return 应用结果
*/
@PostMapping("/suggestions/apply")
public BaseResponse<Integer> applySuggestions(@RequestBody @Valid ApplyDailyReportSuggestionsRequest request) {
Long userId = SecurityUtils.getCurrentUserId();
if (userId == null) {
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
}
List<DailyReportUpdateSuggestion> suggestions = dailyReportUpdateSuggestionMapper.selectBatchIds(request.getSuggestionIds());
if (suggestions == null || suggestions.isEmpty()) {
return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "建议不存在");
}
Set<String> types = new HashSet<>();
for (DailyReportUpdateSuggestion s : suggestions) {
if (s != null && StringUtils.hasText(s.getTargetType())) {
types.add(s.getTargetType().toLowerCase());
}
}
if (types.contains("task") && !SecurityUtils.hasPermission("project:task:update")) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限回写任务");
}
if (types.contains("milestone") && !SecurityUtils.hasPermission("project:milestone:update")) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限回写里程碑");
}
int applied = dailyReportSuggestionService.applySuggestions(request.getProjectId(), request.getSuggestionIds(), userId);
log.info("应用日报进度回写建议成功, projectId={}, appliedCount={}, userId={}", request.getProjectId(), applied, userId);
return ResultUtils.success("应用成功", applied);
}
}

View File

@@ -0,0 +1,18 @@
package cn.yinlihupo.domain.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class ApplyDailyReportSuggestionsRequest {
@NotNull(message = "projectId不能为空")
private Long projectId;
@NotEmpty(message = "suggestionIds不能为空")
private List<Long> suggestionIds;
}

View File

@@ -12,6 +12,11 @@ import java.util.List;
@Data
public class DailyReportAnalysisResult {
/**
* 日报ID
*/
private Long reportId;
/**
* 项目 ID
*/
@@ -47,6 +52,11 @@ public class DailyReportAnalysisResult {
*/
private List<ProgressSuggestion> progressSuggestions;
/**
* 可直接回写到任务/里程碑的进度更新建议(需要用户确认后才执行)
*/
private List<ProgressUpdateRecommendation> progressUpdateRecommendations;
/**
* 识别的风险列表 (直接入库)
*/
@@ -191,6 +201,42 @@ public class DailyReportAnalysisResult {
private String expectedEffect;
}
/**
* 进度更新建议(可回写)
*/
@Data
public static class ProgressUpdateRecommendation {
/**
* 建议作用对象task / milestone
*/
private String targetType;
/**
* 任务ID或里程碑ID
*/
private Long targetId;
/**
* 建议状态pending / in_progress / completed / delayed 等(与现有状态体系保持一致)
*/
private String suggestedStatus;
/**
* 建议进度 0-100
*/
private Integer suggestedProgress;
/**
* 建议理由
*/
private String reason;
/**
* 置信度 0-1
*/
private BigDecimal confidence;
}
/**
* 识别的风险 (直接入库)
*/

View File

@@ -0,0 +1,39 @@
package cn.yinlihupo.domain.entity;
import cn.yinlihupo.common.handler.JsonbTypeHandler;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("daily_report_analysis")
public class DailyReportAnalysisRecord {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long reportId;
private Long projectId;
private LocalDate reportDate;
private String submitterUsername;
@TableField(typeHandler = JsonbTypeHandler.class)
private Object analysisResult;
private String status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,49 @@
package cn.yinlihupo.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("daily_report_update_suggestion")
public class DailyReportUpdateSuggestion {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long analysisId;
private Long reportId;
private Long projectId;
private String targetType;
private Long targetId;
private String suggestedStatus;
private Integer suggestedProgress;
private String reason;
private BigDecimal confidence;
private String status;
private Long appliedBy;
private LocalDateTime appliedTime;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,24 @@
package cn.yinlihupo.domain.vo;
import cn.yinlihupo.domain.dto.DailyReportAnalysisResult;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Data
public class DailyReportAnalysisSuggestionsVO {
private Long analysisId;
private Long reportId;
private Long projectId;
private LocalDate reportDate;
private DailyReportAnalysisResult.OverallProgressAssessment overallProgressAssessment;
private List<DailyReportUpdateSuggestionVO> suggestions;
}

View File

@@ -0,0 +1,32 @@
package cn.yinlihupo.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class DailyReportUpdateSuggestionVO {
private Long suggestionId;
private String targetType;
private Long targetId;
private String targetName;
private String currentStatus;
private Integer currentProgress;
private String suggestedStatus;
private Integer suggestedProgress;
private String reason;
private BigDecimal confidence;
private String status;
}

View File

@@ -200,11 +200,6 @@ public class ProjectDetailVO {
*/
private List<RiskInfo> risks;
/**
* 时间节点列表
*/
private List<TimelineInfo> timelineNodes;
// ==================== 内部类定义 ====================
/**

View File

@@ -50,12 +50,6 @@ public class ProjectInitResult {
@JsonProperty("risks")
private List<RiskInfo> risks;
/**
* 时间节点
*/
@JsonProperty("timeline_nodes")
private List<TimelineNodeInfo> timelineNodes;
// ==================== 内部类定义 ====================
@Data
@@ -204,22 +198,4 @@ public class ProjectInitResult {
@JsonProperty("mitigation_plan")
private String mitigationPlan;
}
@Data
public static class TimelineNodeInfo {
@JsonProperty("node_name")
private String nodeName;
@JsonProperty("node_type")
private String nodeType;
@JsonProperty("plan_date")
private LocalDate planDate;
@JsonProperty("description")
private String description;
@JsonProperty("kb_scope")
private List<String> kbScope;
}
}

View File

@@ -0,0 +1,10 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.DailyReportAnalysisRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DailyReportAnalysisRecordMapper extends BaseMapper<DailyReportAnalysisRecord> {
}

View File

@@ -0,0 +1,10 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.DailyReportUpdateSuggestion;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DailyReportUpdateSuggestionMapper extends BaseMapper<DailyReportUpdateSuggestion> {
}

View File

@@ -1,12 +0,0 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.ProjectTimeline;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 项目时间节点Mapper接口
*/
@Mapper
public interface ProjectTimelineMapper extends BaseMapper<ProjectTimeline> {
}

View File

@@ -0,0 +1,16 @@
package cn.yinlihupo.service.analysis;
import cn.yinlihupo.domain.vo.DailyReportAnalysisSuggestionsVO;
import java.time.LocalDate;
import java.util.List;
public interface DailyReportSuggestionService {
DailyReportAnalysisSuggestionsVO getLatestSuggestionsByReportId(Long projectId, Long reportId);
DailyReportAnalysisSuggestionsVO getLatestSuggestionsByUniqueKey(Long projectId, LocalDate reportDate, String submitterUsername);
int applySuggestions(Long projectId, List<Long> suggestionIds, Long appliedBy);
}

View File

@@ -37,6 +37,8 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
private final ResourceMapper resourceMapper;
private final RiskMapper riskMapper;
private final ProjectDailyReportMapper projectDailyReportMapper;
private final DailyReportAnalysisRecordMapper dailyReportAnalysisRecordMapper;
private final DailyReportUpdateSuggestionMapper dailyReportUpdateSuggestionMapper;
/**
* AI 分析系统提示词模板
@@ -55,6 +57,7 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
3. **资源需求分析**: 识别是否需要新增人力、物料、设备等资源
4. **进度建议**: 针对当前进度提出可操作的调整建议
5. **风险识别**: 识别潜在的项目风险并评估
6. **进度回写建议**: 给出可直接回写到任务/里程碑的【状态/进度】更新建议(必须带 targetType+targetId便于后续用户确认并回写
# 输出格式
@@ -99,6 +102,24 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
"expectedEffect": "预期效果"
}
],
"progressUpdateRecommendations": [
{
"targetType": "task",
"targetId": 456,
"suggestedStatus": "in_progress",
"suggestedProgress": 60,
"reason": "依据日报内容与当前任务状态判断已推进到 60%",
"confidence": 0.72
},
{
"targetType": "milestone",
"targetId": 123,
"suggestedStatus": "delayed",
"suggestedProgress": 40,
"reason": "里程碑风险高且预计延期 5 天",
"confidence": 0.68
}
],
"identifiedRisks": [
{
"riskName": "风险名称",
@@ -125,6 +146,8 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
4. 识别的风险应该具体且可操作,不要泛泛而谈
5. 建议要结合日报中的实际工作内容,有针对性
6. 如果日报中没有明显风险,可以返回空数组,不要强行编造
7. progressUpdateRecommendations 中的 targetId 必须来自【任务列表】或【里程碑信息】中给出的 ID
8. suggestedProgress 必须是 0-100 的整数;如无法判断,返回 null
""";
@Override
@@ -194,6 +217,7 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
// 4. 补充项目信息
if (result != null) {
result.setReportId(report.getId());
result.setProjectId(projectId);
result.setReportDate(report.getReportDate());
@@ -236,10 +260,53 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
if (result.getOverallProgressAssessment() != null) {
updateProjectProgress(projectId, result.getOverallProgressAssessment());
}
saveDailyReportAnalysis(projectId, result, now);
log.info("[日报 AI 分析] 结果保存完成projectId={}", projectId);
}
private void saveDailyReportAnalysis(Long projectId, DailyReportAnalysisResult result, LocalDateTime now) {
if (result.getReportId() == null) {
return;
}
ProjectDailyReport report = projectDailyReportMapper.selectById(result.getReportId());
DailyReportAnalysisRecord record = new DailyReportAnalysisRecord();
record.setProjectId(projectId);
record.setReportId(result.getReportId());
record.setReportDate(result.getReportDate());
record.setSubmitterUsername(report != null ? report.getSubmitterUsername() : null);
record.setAnalysisResult(result);
record.setStatus("completed");
dailyReportAnalysisRecordMapper.insert(record);
if (result.getProgressUpdateRecommendations() == null || result.getProgressUpdateRecommendations().isEmpty()) {
return;
}
for (DailyReportAnalysisResult.ProgressUpdateRecommendation rec : result.getProgressUpdateRecommendations()) {
if (rec == null || rec.getTargetId() == null || rec.getTargetType() == null) {
continue;
}
DailyReportUpdateSuggestion suggestion = new DailyReportUpdateSuggestion();
suggestion.setAnalysisId(record.getId());
suggestion.setReportId(result.getReportId());
suggestion.setProjectId(projectId);
suggestion.setTargetType(rec.getTargetType());
suggestion.setTargetId(rec.getTargetId());
suggestion.setSuggestedStatus(rec.getSuggestedStatus());
suggestion.setSuggestedProgress(rec.getSuggestedProgress());
suggestion.setReason(rec.getReason());
suggestion.setConfidence(rec.getConfidence());
suggestion.setStatus("pending");
suggestion.setAppliedBy(null);
suggestion.setAppliedTime(null);
dailyReportUpdateSuggestionMapper.insert(suggestion);
}
}
/**
* 构建项目上下文数据
*/
@@ -272,7 +339,8 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
if (!milestones.isEmpty()) {
sb.append("【里程碑信息】\n");
for (ProjectMilestone milestone : milestones) {
sb.append(String.format("- %s (计划:%s, 状态:%s, 进度:%d%%)\n",
sb.append(String.format("- [milestoneId=%d] %s (计划:%s, 状态:%s, 进度:%d%%)\n",
milestone.getId(),
milestone.getMilestoneName(),
milestone.getPlanDate(),
milestone.getStatus(),
@@ -293,10 +361,12 @@ public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisServic
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",
sb.append(String.format("- [taskId=%d] %s [%s] %s %s (计划:%s ~ %s, 状态:%s, 进度:%d%%)\n",
task.getId(),
task.getTaskCode() != null ? task.getTaskCode() : "",
task.getTaskType() != null ? task.getTaskType() : "task",
task.getTaskName(),
assigneeInfo,
task.getPlanStartDate(),
task.getPlanEndDate(),
task.getStatus(),

View File

@@ -0,0 +1,308 @@
package cn.yinlihupo.service.analysis.impl;
import cn.yinlihupo.common.enums.ErrorCode;
import cn.yinlihupo.common.exception.BusinessException;
import cn.yinlihupo.domain.dto.DailyReportAnalysisResult;
import cn.yinlihupo.domain.entity.DailyReportAnalysisRecord;
import cn.yinlihupo.domain.entity.DailyReportUpdateSuggestion;
import cn.yinlihupo.domain.entity.ProjectDailyReport;
import cn.yinlihupo.domain.entity.ProjectMilestone;
import cn.yinlihupo.domain.entity.Task;
import cn.yinlihupo.domain.vo.DailyReportAnalysisSuggestionsVO;
import cn.yinlihupo.domain.vo.DailyReportUpdateSuggestionVO;
import cn.yinlihupo.mapper.DailyReportAnalysisRecordMapper;
import cn.yinlihupo.mapper.DailyReportUpdateSuggestionMapper;
import cn.yinlihupo.mapper.ProjectDailyReportMapper;
import cn.yinlihupo.mapper.ProjectMilestoneMapper;
import cn.yinlihupo.mapper.TaskMapper;
import cn.yinlihupo.service.analysis.DailyReportSuggestionService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class DailyReportSuggestionServiceImpl implements DailyReportSuggestionService {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.findAndRegisterModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
private final ProjectDailyReportMapper projectDailyReportMapper;
private final DailyReportAnalysisRecordMapper dailyReportAnalysisRecordMapper;
private final DailyReportUpdateSuggestionMapper dailyReportUpdateSuggestionMapper;
private final TaskMapper taskMapper;
private final ProjectMilestoneMapper projectMilestoneMapper;
@Override
public DailyReportAnalysisSuggestionsVO getLatestSuggestionsByReportId(Long projectId, Long reportId) {
if (projectId == null || reportId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "projectId/reportId不能为空");
}
DailyReportAnalysisRecord record = dailyReportAnalysisRecordMapper.selectOne(
new LambdaQueryWrapper<DailyReportAnalysisRecord>()
.eq(DailyReportAnalysisRecord::getProjectId, projectId)
.eq(DailyReportAnalysisRecord::getReportId, reportId)
.eq(DailyReportAnalysisRecord::getDeleted, 0)
.orderByDesc(DailyReportAnalysisRecord::getCreateTime)
.last("LIMIT 1")
);
if (record == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到日报分析结果");
}
return buildSuggestionsVO(record, "pending");
}
@Override
public DailyReportAnalysisSuggestionsVO getLatestSuggestionsByUniqueKey(Long projectId, LocalDate reportDate, String submitterUsername) {
if (projectId == null || reportDate == null || !StringUtils.hasText(submitterUsername)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "projectId/reportDate/submitterUsername不能为空");
}
ProjectDailyReport report = projectDailyReportMapper.selectOne(
new LambdaQueryWrapper<ProjectDailyReport>()
.eq(ProjectDailyReport::getProjectId, projectId)
.eq(ProjectDailyReport::getReportDate, reportDate)
.eq(ProjectDailyReport::getSubmitterUsername, submitterUsername)
.eq(ProjectDailyReport::getDeleted, 0)
.last("LIMIT 1")
);
if (report == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到日报");
}
return getLatestSuggestionsByReportId(projectId, report.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int applySuggestions(Long projectId, List<Long> suggestionIds, Long appliedBy) {
if (projectId == null || suggestionIds == null || suggestionIds.isEmpty()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "projectId/suggestionIds不能为空");
}
if (appliedBy == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
}
List<DailyReportUpdateSuggestion> suggestions = dailyReportUpdateSuggestionMapper.selectBatchIds(suggestionIds);
if (suggestions == null || suggestions.isEmpty()) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "建议不存在");
}
Map<Long, DailyReportUpdateSuggestion> suggestionMap = suggestions.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(DailyReportUpdateSuggestion::getId, Function.identity(), (a, b) -> a));
for (Long id : suggestionIds) {
if (!suggestionMap.containsKey(id)) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "建议不存在: " + id);
}
}
for (DailyReportUpdateSuggestion suggestion : suggestions) {
if (suggestion == null) {
continue;
}
if (!Objects.equals(projectId, suggestion.getProjectId())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "建议不属于该项目: " + suggestion.getId());
}
if (suggestion.getDeleted() != null && suggestion.getDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "建议已被删除: " + suggestion.getId());
}
if (!"pending".equals(suggestion.getStatus())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "建议状态不可应用: " + suggestion.getId());
}
}
LocalDateTime now = LocalDateTime.now();
int appliedCount = 0;
for (DailyReportUpdateSuggestion suggestion : suggestions) {
if (suggestion == null) {
continue;
}
applySingleSuggestion(suggestion, now);
suggestion.setStatus("applied");
suggestion.setAppliedBy(appliedBy);
suggestion.setAppliedTime(now);
dailyReportUpdateSuggestionMapper.updateById(suggestion);
appliedCount++;
}
return appliedCount;
}
private void applySingleSuggestion(DailyReportUpdateSuggestion suggestion, LocalDateTime now) {
String targetType = suggestion.getTargetType();
if (!StringUtils.hasText(targetType) || suggestion.getTargetId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "建议目标不完整: " + suggestion.getId());
}
if ("task".equalsIgnoreCase(targetType)) {
Task task = taskMapper.selectById(suggestion.getTargetId());
if (task == null || task.getDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "任务不存在: " + suggestion.getTargetId());
}
if (!Objects.equals(suggestion.getProjectId(), task.getProjectId())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "任务不属于该项目: " + suggestion.getTargetId());
}
Integer progress = suggestion.getSuggestedProgress();
String status = suggestion.getSuggestedStatus();
if (progress != null) {
if (progress < 0 || progress > 100) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "任务进度建议必须在0-100之间: " + suggestion.getId());
}
task.setProgress(progress);
if (progress == 100) {
task.setStatus("completed");
} else if (progress > 0) {
task.setStatus("in_progress");
}
}
if (StringUtils.hasText(status)) {
task.setStatus(status);
if ("completed".equals(status)) {
task.setProgress(100);
}
}
taskMapper.updateById(task);
return;
}
if ("milestone".equalsIgnoreCase(targetType)) {
ProjectMilestone milestone = projectMilestoneMapper.selectById(suggestion.getTargetId());
if (milestone == null || milestone.getDeleted() == 1) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "里程碑不存在: " + suggestion.getTargetId());
}
if (!Objects.equals(suggestion.getProjectId(), milestone.getProjectId())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "里程碑不属于该项目: " + suggestion.getTargetId());
}
Integer progress = suggestion.getSuggestedProgress();
String status = suggestion.getSuggestedStatus();
if (progress != null) {
if (progress < 0 || progress > 100) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "里程碑进度建议必须在0-100之间: " + suggestion.getId());
}
milestone.setProgress(progress);
if (progress == 100) {
milestone.setStatus("completed");
milestone.setActualDate(now.toLocalDate());
} else if (progress > 0) {
milestone.setStatus("in_progress");
}
}
if (StringUtils.hasText(status)) {
milestone.setStatus(status);
if ("completed".equals(status)) {
milestone.setProgress(100);
milestone.setActualDate(now.toLocalDate());
}
}
projectMilestoneMapper.updateById(milestone);
return;
}
throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的targetType: " + targetType);
}
private DailyReportAnalysisSuggestionsVO buildSuggestionsVO(DailyReportAnalysisRecord record, String suggestionStatus) {
DailyReportAnalysisResult analysisResult = null;
if (record.getAnalysisResult() != null) {
analysisResult = OBJECT_MAPPER.convertValue(record.getAnalysisResult(), DailyReportAnalysisResult.class);
}
List<DailyReportUpdateSuggestion> suggestions = dailyReportUpdateSuggestionMapper.selectList(
new LambdaQueryWrapper<DailyReportUpdateSuggestion>()
.eq(DailyReportUpdateSuggestion::getAnalysisId, record.getId())
.eq(DailyReportUpdateSuggestion::getDeleted, 0)
.eq(StringUtils.hasText(suggestionStatus), DailyReportUpdateSuggestion::getStatus, suggestionStatus)
.orderByAsc(DailyReportUpdateSuggestion::getCreateTime)
);
List<Long> taskIds = new ArrayList<>();
List<Long> milestoneIds = new ArrayList<>();
for (DailyReportUpdateSuggestion suggestion : suggestions) {
if (suggestion == null || suggestion.getTargetId() == null) {
continue;
}
if ("task".equalsIgnoreCase(suggestion.getTargetType())) {
taskIds.add(suggestion.getTargetId());
} else if ("milestone".equalsIgnoreCase(suggestion.getTargetType())) {
milestoneIds.add(suggestion.getTargetId());
}
}
Map<Long, Task> taskMap = taskIds.isEmpty()
? Collections.emptyMap()
: taskMapper.selectBatchIds(taskIds).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(Task::getId, Function.identity(), (a, b) -> a));
Map<Long, ProjectMilestone> milestoneMap = milestoneIds.isEmpty()
? Collections.emptyMap()
: projectMilestoneMapper.selectBatchIds(milestoneIds).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(ProjectMilestone::getId, Function.identity(), (a, b) -> a));
List<DailyReportUpdateSuggestionVO> voList = new ArrayList<>();
for (DailyReportUpdateSuggestion suggestion : suggestions) {
if (suggestion == null) {
continue;
}
DailyReportUpdateSuggestionVO vo = new DailyReportUpdateSuggestionVO();
vo.setSuggestionId(suggestion.getId());
vo.setTargetType(suggestion.getTargetType());
vo.setTargetId(suggestion.getTargetId());
vo.setSuggestedStatus(suggestion.getSuggestedStatus());
vo.setSuggestedProgress(suggestion.getSuggestedProgress());
vo.setReason(suggestion.getReason());
vo.setConfidence(suggestion.getConfidence());
vo.setStatus(suggestion.getStatus());
if ("task".equalsIgnoreCase(suggestion.getTargetType())) {
Task task = taskMap.get(suggestion.getTargetId());
if (task != null) {
vo.setTargetName(task.getTaskName());
vo.setCurrentStatus(task.getStatus());
vo.setCurrentProgress(task.getProgress());
}
} else if ("milestone".equalsIgnoreCase(suggestion.getTargetType())) {
ProjectMilestone milestone = milestoneMap.get(suggestion.getTargetId());
if (milestone != null) {
vo.setTargetName(milestone.getMilestoneName());
vo.setCurrentStatus(milestone.getStatus());
vo.setCurrentProgress(milestone.getProgress());
}
}
voList.add(vo);
}
DailyReportAnalysisSuggestionsVO vo = new DailyReportAnalysisSuggestionsVO();
vo.setAnalysisId(record.getId());
vo.setReportId(record.getReportId());
vo.setProjectId(record.getProjectId());
vo.setReportDate(record.getReportDate());
vo.setOverallProgressAssessment(analysisResult != null ? analysisResult.getOverallProgressAssessment() : null);
vo.setSuggestions(voList);
return vo;
}
}

View File

@@ -44,7 +44,6 @@ public class ProjectServiceImpl implements ProjectService {
private final ProjectMemberMapper projectMemberMapper;
private final ResourceMapper resourceMapper;
private final RiskMapper riskMapper;
private final ProjectTimelineMapper projectTimelineMapper;
private final SysUserMapper sysUserMapper;
/**
@@ -136,15 +135,6 @@ public class ProjectServiceImpl implements ProjectService {
"impact": 4,
"mitigation_plan": "缓解措施"
}
],
"timeline_nodes": [
{
"node_name": "时间节点名称",
"node_type": "milestone/phase/event",
"plan_date": "2024-06-01",
"description": "描述",
"kb_scope": ["report", "file", "risk", "ticket"]
}
]
}
```
@@ -379,17 +369,6 @@ public class ProjectServiceImpl implements ProjectService {
log.info("保存了 {} 个风险", result.getRisks().size());
}
// 7. 保存时间节点
if (result.getTimelineNodes() != null && !result.getTimelineNodes().isEmpty()) {
for (int i = 0; i < result.getTimelineNodes().size(); i++) {
ProjectInitResult.TimelineNodeInfo nodeInfo = result.getTimelineNodes().get(i);
ProjectTimeline timeline = convertToTimeline(nodeInfo, projectId);
timeline.setSortOrder(i);
projectTimelineMapper.insert(timeline);
}
log.info("保存了 {} 个时间节点", result.getTimelineNodes().size());
}
return projectId;
}
@@ -549,21 +528,6 @@ public class ProjectServiceImpl implements ProjectService {
}
}
/**
* 转换时间节点信息
*/
private ProjectTimeline convertToTimeline(ProjectInitResult.TimelineNodeInfo info, Long projectId) {
ProjectTimeline timeline = new ProjectTimeline();
timeline.setProjectId(projectId);
timeline.setNodeName(info.getNodeName());
timeline.setNodeType(info.getNodeType());
timeline.setPlanDate(info.getPlanDate());
timeline.setDescription(info.getDescription());
timeline.setStatus("pending");
timeline.setKbScope(info.getKbScope());
return timeline;
}
// ==================== 项目查询相关实现 ====================
@Override
@@ -1105,31 +1069,6 @@ public class ProjectServiceImpl implements ProjectService {
.count();
detailVO.setHighRiskCount(highRiskCount);
// 8. 查询时间节点
List<ProjectTimeline> timelines = projectTimelineMapper.selectList(
new LambdaQueryWrapper<ProjectTimeline>()
.eq(ProjectTimeline::getProjectId, projectId)
.eq(ProjectTimeline::getDeleted, 0)
.orderByAsc(ProjectTimeline::getSortOrder)
);
List<ProjectDetailVO.TimelineInfo> timelineVOList = timelines.stream()
.map(timeline -> {
ProjectDetailVO.TimelineInfo timelineInfo = new ProjectDetailVO.TimelineInfo();
timelineInfo.setId(timeline.getId());
timelineInfo.setNodeName(timeline.getNodeName());
timelineInfo.setNodeType(timeline.getNodeType());
timelineInfo.setPlanDate(timeline.getPlanDate());
timelineInfo.setActualDate(timeline.getActualDate());
timelineInfo.setDescription(timeline.getDescription());
timelineInfo.setStatus(timeline.getStatus());
timelineInfo.setSortOrder(timeline.getSortOrder());
timelineInfo.setKbScope(timeline.getKbScope());
return timelineInfo;
})
.collect(Collectors.toList());
detailVO.setTimelineNodes(timelineVOList);
return detailVO;
}
}