feat(project): 实现AI项目初始化功能
- 新增项目初始化控制器,支持文件上传生成项目结构化数据 - 定义项目初始化结果DTO,包含项目、里程碑、任务、成员、资源、风险和时间节点等信息 - 实现项目初始化服务接口及其实现类,集成Spring AI结构化输出能力 - 支持根据内容或文件生成项目初始化数据,并保存到数据库 - 增加项目、里程碑、任务、成员、资源、风险及时间节点实体及对应Mapper - 实现文件上传到OSS及项目初始化记录功能,记录解析状态及结果 - 添加PostgreSQL JSONB类型处理器,支持JSON对象字段存储 - 修改开发环境数据库配置,更新连接的数据库名称为aiprojectmanager
This commit is contained in:
@@ -1,13 +1,28 @@
|
||||
package cn.yinlihupo.service.project.impl;
|
||||
|
||||
import cn.yinlihupo.domain.dto.ProjectInitResult;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.yinlihupo.domain.entity.*;
|
||||
import cn.yinlihupo.domain.vo.ProjectInitResult;
|
||||
import cn.yinlihupo.mapper.*;
|
||||
import cn.yinlihupo.service.oss.OssService;
|
||||
import cn.yinlihupo.service.project.ProjectService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.chat.metadata.Usage;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI项目初始化服务实现类
|
||||
@@ -21,6 +36,16 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
private final ChatClient chatClient;
|
||||
private final OssService ossService;
|
||||
|
||||
// Mapper依赖
|
||||
private final ProjectMapper projectMapper;
|
||||
private final ProjectMilestoneMapper projectMilestoneMapper;
|
||||
private final TaskMapper taskMapper;
|
||||
private final ProjectMemberMapper projectMemberMapper;
|
||||
private final ResourceMapper resourceMapper;
|
||||
private final RiskMapper riskMapper;
|
||||
private final ProjectTimelineMapper projectTimelineMapper;
|
||||
private final ProjectInitRecordMapper projectInitRecordMapper;
|
||||
|
||||
/**
|
||||
* 项目初始化系统提示词模板
|
||||
*/
|
||||
@@ -172,4 +197,384 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
return generateProjectFromContent(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ProjectInitResult generateAndSaveProject(MultipartFile file) {
|
||||
log.info("开始上传文件并生成项目初始化数据, 文件名: {}", file.getOriginalFilename());
|
||||
|
||||
try {
|
||||
// 1. 上传文件到OSS
|
||||
String fileUrl = ossService.uploadFile(file, file.getOriginalFilename());
|
||||
log.info("文件上传成功, URL: {}", fileUrl);
|
||||
|
||||
// 2. 读取文件内容
|
||||
String content = ossService.readFileAsString(fileUrl);
|
||||
if (content == null || content.isEmpty()) {
|
||||
throw new RuntimeException("无法读取文件内容: " + fileUrl);
|
||||
}
|
||||
|
||||
// 3. 构建输入文件信息
|
||||
List<ProjectInitRecord.InputFile> inputFiles = new ArrayList<>();
|
||||
ProjectInitRecord.InputFile inputFile = new ProjectInitRecord.InputFile();
|
||||
inputFile.setName(file.getOriginalFilename());
|
||||
inputFile.setPath(fileUrl);
|
||||
inputFile.setType(file.getContentType());
|
||||
inputFile.setSize(file.getSize());
|
||||
inputFiles.add(inputFile);
|
||||
|
||||
// 4. 生成并保存项目数据
|
||||
return generateAndSaveProjectFromContent(content, inputFiles);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("项目初始化失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("项目初始化失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ProjectInitResult generateAndSaveProjectFromContent(String content,
|
||||
List<ProjectInitRecord.InputFile> inputFiles) {
|
||||
log.info("开始生成并保存项目初始化数据");
|
||||
|
||||
// 1. 先创建初始化记录(状态为处理中)
|
||||
ProjectInitRecord initRecord = new ProjectInitRecord();
|
||||
initRecord.setInputFiles(inputFiles);
|
||||
initRecord.setInputText(content.length() > 5000 ? content.substring(0, 5000) + "..." : content);
|
||||
initRecord.setParseStatus("processing");
|
||||
projectInitRecordMapper.insert(initRecord);
|
||||
|
||||
ProjectInitResult result = null;
|
||||
Usage usage = null;
|
||||
|
||||
try {
|
||||
// 2. 调用AI生成项目数据
|
||||
String userPrompt = "请根据以下项目资料,生成完整的项目初始化结构化数据:\n\n" +
|
||||
content + "\n\n" +
|
||||
"请严格按照系统提示词中的JSON格式输出,确保所有字段都包含合理的值。";
|
||||
|
||||
// 创建 BeanOutputConverter 用于转换响应
|
||||
BeanOutputConverter<ProjectInitResult> outputConverter = new BeanOutputConverter<>(ProjectInitResult.class);
|
||||
|
||||
var chatResponse = chatClient.prompt()
|
||||
.system(PROJECT_INIT_SYSTEM_PROMPT)
|
||||
.user(userPrompt)
|
||||
.call()
|
||||
.chatResponse();
|
||||
|
||||
// 使用 BeanOutputConverter 手动转换响应内容
|
||||
String responseContent = chatResponse.getResult().getOutput().getText();
|
||||
result = outputConverter.convert(responseContent);
|
||||
usage = chatResponse.getMetadata().getUsage();
|
||||
|
||||
// 3. 保存项目数据到数据库
|
||||
Long projectId = saveProjectData(result);
|
||||
|
||||
// 4. 更新初始化记录为成功状态
|
||||
initRecord.setProjectId(projectId);
|
||||
initRecord.setParseStatus("completed");
|
||||
initRecord.setParseResult(result);
|
||||
initRecord.setGeneratedMilestones(result.getMilestones() != null ? result.getMilestones().size() : 0);
|
||||
initRecord.setGeneratedTasks(result.getTasks() != null ? result.getTasks().size() : 0);
|
||||
initRecord.setGeneratedMembers(result.getMembers() != null ? result.getMembers().size() : 0);
|
||||
initRecord.setGeneratedResources(result.getResources() != null ? result.getResources().size() : 0);
|
||||
if (usage != null) {
|
||||
initRecord.setTokensUsed(usage.getTotalTokens());
|
||||
}
|
||||
projectInitRecordMapper.updateById(initRecord);
|
||||
|
||||
log.info("项目初始化数据保存成功, projectId: {}", projectId);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("项目初始化失败: {}", e.getMessage(), e);
|
||||
|
||||
// 更新初始化记录为失败状态
|
||||
initRecord.setParseStatus("failed");
|
||||
initRecord.setErrorMessage(e.getMessage());
|
||||
if (usage != null) {
|
||||
initRecord.setTokensUsed(usage.getTotalTokens());
|
||||
}
|
||||
projectInitRecordMapper.updateById(initRecord);
|
||||
|
||||
throw new RuntimeException("项目初始化失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存项目数据到数据库
|
||||
*
|
||||
* @param result AI生成的项目初始化结果
|
||||
* @return 项目ID
|
||||
*/
|
||||
private Long saveProjectData(ProjectInitResult result) {
|
||||
if (result == null || result.getProject() == null) {
|
||||
throw new RuntimeException("项目数据为空");
|
||||
}
|
||||
|
||||
// 1. 保存项目基本信息
|
||||
Project project = convertToProject(result.getProject());
|
||||
project.setProjectCode(generateProjectCode());
|
||||
project.setStatus("planning"); // 初始化后状态为规划中
|
||||
project.setProgress(0);
|
||||
project.setVisibility(1);
|
||||
projectMapper.insert(project);
|
||||
Long projectId = project.getId();
|
||||
log.info("项目基本信息保存成功, projectId: {}", projectId);
|
||||
|
||||
// 2. 保存里程碑
|
||||
Map<Integer, Long> milestoneIndexToId = new HashMap<>();
|
||||
if (result.getMilestones() != null && !result.getMilestones().isEmpty()) {
|
||||
for (int i = 0; i < result.getMilestones().size(); i++) {
|
||||
ProjectInitResult.MilestoneInfo milestoneInfo = result.getMilestones().get(i);
|
||||
ProjectMilestone milestone = convertToMilestone(milestoneInfo, projectId);
|
||||
milestone.setSortOrder(i);
|
||||
projectMilestoneMapper.insert(milestone);
|
||||
milestoneIndexToId.put(i, milestone.getId());
|
||||
}
|
||||
log.info("保存了 {} 个里程碑", result.getMilestones().size());
|
||||
}
|
||||
|
||||
// 3. 保存任务(支持层级关系)
|
||||
Map<String, Long> taskTempIdToId = new HashMap<>();
|
||||
if (result.getTasks() != null && !result.getTasks().isEmpty()) {
|
||||
// 第一轮:保存所有任务,建立临时ID到真实ID的映射
|
||||
for (int i = 0; i < result.getTasks().size(); i++) {
|
||||
ProjectInitResult.TaskInfo taskInfo = result.getTasks().get(i);
|
||||
Task task = convertToTask(taskInfo, projectId);
|
||||
|
||||
// 设置里程碑关联(按索引匹配)
|
||||
// 这里简化处理,可以根据任务时间范围匹配里程碑
|
||||
if (!milestoneIndexToId.isEmpty()) {
|
||||
// 简单策略:将任务平均分配到各个里程碑
|
||||
int milestoneIndex = i % milestoneIndexToId.size();
|
||||
task.setMilestoneId(milestoneIndexToId.get(milestoneIndex));
|
||||
}
|
||||
|
||||
task.setSortOrder(i);
|
||||
taskMapper.insert(task);
|
||||
|
||||
// 使用任务名称作为临时ID进行映射
|
||||
String tempId = taskInfo.getTaskName();
|
||||
taskTempIdToId.put(tempId, task.getId());
|
||||
}
|
||||
|
||||
// 第二轮:更新任务依赖关系
|
||||
for (int i = 0; i < result.getTasks().size(); i++) {
|
||||
ProjectInitResult.TaskInfo taskInfo = result.getTasks().get(i);
|
||||
if (taskInfo.getParentTaskId() != null && !taskInfo.getParentTaskId().isEmpty()) {
|
||||
Task task = new Task();
|
||||
task.setId(taskTempIdToId.get(taskInfo.getTaskName()));
|
||||
// 查找父任务ID
|
||||
Long parentId = taskTempIdToId.get(taskInfo.getParentTaskId());
|
||||
if (parentId != null) {
|
||||
task.setParentId(parentId);
|
||||
taskMapper.updateById(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("保存了 {} 个任务", result.getTasks().size());
|
||||
}
|
||||
|
||||
// 4. 保存项目成员
|
||||
if (result.getMembers() != null && !result.getMembers().isEmpty()) {
|
||||
for (ProjectInitResult.MemberInfo memberInfo : result.getMembers()) {
|
||||
ProjectMember member = convertToMember(memberInfo, projectId);
|
||||
projectMemberMapper.insert(member);
|
||||
}
|
||||
log.info("保存了 {} 个成员", result.getMembers().size());
|
||||
}
|
||||
|
||||
// 5. 保存资源
|
||||
if (result.getResources() != null && !result.getResources().isEmpty()) {
|
||||
for (ProjectInitResult.ResourceInfo resourceInfo : result.getResources()) {
|
||||
Resource resource = convertToResource(resourceInfo, projectId);
|
||||
resourceMapper.insert(resource);
|
||||
}
|
||||
log.info("保存了 {} 个资源", result.getResources().size());
|
||||
}
|
||||
|
||||
// 6. 保存风险
|
||||
if (result.getRisks() != null && !result.getRisks().isEmpty()) {
|
||||
for (ProjectInitResult.RiskInfo riskInfo : result.getRisks()) {
|
||||
Risk risk = convertToRisk(riskInfo, projectId);
|
||||
riskMapper.insert(risk);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成项目编号
|
||||
*/
|
||||
private String generateProjectCode() {
|
||||
return "PRJ" + IdUtil.fastSimpleUUID().substring(0, 12).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换项目信息
|
||||
*/
|
||||
private Project convertToProject(ProjectInitResult.ProjectInfo info) {
|
||||
Project project = new Project();
|
||||
project.setProjectName(info.getProjectName());
|
||||
project.setProjectType(info.getProjectType());
|
||||
project.setDescription(info.getDescription());
|
||||
project.setObjectives(info.getObjectives());
|
||||
project.setPlanStartDate(info.getPlanStartDate());
|
||||
project.setPlanEndDate(info.getPlanEndDate());
|
||||
project.setBudget(info.getBudget());
|
||||
project.setCurrency(info.getCurrency());
|
||||
project.setPriority(info.getPriority());
|
||||
project.setTags(info.getTags());
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换里程碑信息
|
||||
*/
|
||||
private ProjectMilestone convertToMilestone(ProjectInitResult.MilestoneInfo info, Long projectId) {
|
||||
ProjectMilestone milestone = new ProjectMilestone();
|
||||
milestone.setProjectId(projectId);
|
||||
milestone.setMilestoneName(info.getMilestoneName());
|
||||
milestone.setDescription(info.getDescription());
|
||||
milestone.setPlanDate(info.getPlanDate());
|
||||
milestone.setStatus("pending");
|
||||
milestone.setProgress(0);
|
||||
milestone.setIsKey(0);
|
||||
if (info.getDeliverables() != null && !info.getDeliverables().isEmpty()) {
|
||||
milestone.setDeliverables(Arrays.asList(info.getDeliverables().split(",")));
|
||||
}
|
||||
return milestone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换任务信息
|
||||
*/
|
||||
private Task convertToTask(ProjectInitResult.TaskInfo info, Long projectId) {
|
||||
Task task = new Task();
|
||||
task.setProjectId(projectId);
|
||||
task.setTaskName(info.getTaskName());
|
||||
task.setDescription(info.getDescription());
|
||||
task.setPlanStartDate(info.getPlanStartDate());
|
||||
task.setPlanEndDate(info.getPlanEndDate());
|
||||
if (info.getEstimatedHours() != null) {
|
||||
task.setPlanHours(BigDecimal.valueOf(info.getEstimatedHours()));
|
||||
}
|
||||
task.setPriority(info.getPriority());
|
||||
task.setStatus("pending");
|
||||
task.setProgress(0);
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成员信息
|
||||
*/
|
||||
private ProjectMember convertToMember(ProjectInitResult.MemberInfo info, Long projectId) {
|
||||
ProjectMember member = new ProjectMember();
|
||||
member.setProjectId(projectId);
|
||||
member.setRoleCode(info.getRoleCode());
|
||||
member.setResponsibility(info.getResponsibility());
|
||||
if (info.getWeeklyHours() != null) {
|
||||
member.setWeeklyHours(BigDecimal.valueOf(info.getWeeklyHours()));
|
||||
}
|
||||
member.setStatus(1);
|
||||
// TODO: 需要根据name查找或创建用户,暂时留空
|
||||
return member;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换资源信息
|
||||
*/
|
||||
private Resource convertToResource(ProjectInitResult.ResourceInfo info, Long projectId) {
|
||||
Resource resource = new Resource();
|
||||
resource.setProjectId(projectId);
|
||||
resource.setResourceName(info.getResourceName());
|
||||
resource.setResourceType(info.getResourceType());
|
||||
resource.setPlanQuantity(info.getQuantity());
|
||||
resource.setUnit(info.getUnit());
|
||||
resource.setUnitPrice(info.getUnitPrice());
|
||||
resource.setSupplier(info.getSupplier());
|
||||
resource.setStatus("planned");
|
||||
resource.setCurrency("CNY");
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换风险信息
|
||||
*/
|
||||
private Risk convertToRisk(ProjectInitResult.RiskInfo info, Long projectId) {
|
||||
Risk risk = new Risk();
|
||||
risk.setProjectId(projectId);
|
||||
risk.setRiskName(info.getRiskName());
|
||||
risk.setCategory(info.getCategory());
|
||||
risk.setDescription(info.getDescription());
|
||||
risk.setRiskSource("ai_detection");
|
||||
if (info.getProbability() != null) {
|
||||
risk.setProbability(BigDecimal.valueOf(info.getProbability()));
|
||||
}
|
||||
if (info.getImpact() != null) {
|
||||
risk.setImpact(BigDecimal.valueOf(info.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);
|
||||
}
|
||||
// 根据概率和影响确定风险等级
|
||||
risk.setRiskLevel(calculateRiskLevel(info.getProbability(), info.getImpact()));
|
||||
risk.setStatus("identified");
|
||||
risk.setMitigationPlan(info.getMitigationPlan());
|
||||
risk.setDiscoverTime(LocalDateTime.now());
|
||||
return risk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算风险等级
|
||||
*/
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user