feat(analysis): 添加日报分析进度回写建议功能
- 新增日报分析建议服务,支持获取和应用进度更新建议 - 添加进度回写建议数据结构,支持任务和里程碑状态/进度更新 - 移除项目时间节点相关功能,简化项目数据结构 - 扩展AI分析提示词,要求生成可回写的进度建议 - 新增相关Mapper、Controller和服务实现 - 优化JsonbTypeHandler以支持日期序列化
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别的风险 (直接入库)
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -200,11 +200,6 @@ public class ProjectDetailVO {
|
||||
*/
|
||||
private List<RiskInfo> risks;
|
||||
|
||||
/**
|
||||
* 时间节点列表
|
||||
*/
|
||||
private List<TimelineInfo> timelineNodes;
|
||||
|
||||
// ==================== 内部类定义 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user