Files
ylhp-ai-project-manager/src/main/java/cn/yinlihupo/service/project/impl/ProjectServiceImpl.java
JiaoTianBo 294ef21d50 feat(project): 实现AI项目初始化及相关实体管理
- 新增通用返回类BaseResponse用于统一接口响应格式
- 新增业务异常BusinessException及全局异常处理GlobalExceptionHandler
- 新增OSS文件上传控制器支持文件上传与删除接口
- 添加项目核心实体类Project、ProjectMember、ProjectMilestone、ProjectTimeline和Resource
- 实现ProjectService接口及其实现类,使用AI能力从项目文档生成结构化项目数据
- 在ProjectServiceImpl中实现项目数据解析、保存及业务逻辑,包括项目、里程碑、任务、成员、资源、风险等
- 项目初始化控制器ProjectController提供文件上传触发项目初始化功能
- 设计了详细的系统提示词和用户提示词,用于AI模型指导生成严格格式的结构化项目数据
- 设计项目数据持久化流程,确保生成的数据正确保存至数据库,支持事务回滚
- 增强日志记录,便于追踪项目初始化全过程及错误调试
2026-03-27 10:25:13 +08:00

539 lines
20 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cn.yinlihupo.service.project.impl;
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.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项目初始化服务实现类
* 使用Spring AI结构化输出能力解析项目文档
*/
@Slf4j
@Service
@RequiredArgsConstructor
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 static final String PROJECT_INIT_SYSTEM_PROMPT = """
# 角色
你是一个专业的项目管理助手,擅长从项目文档中提取结构化信息,自动生成项目计划。
# 任务
根据用户提供的项目资料,解析并生成以下结构化数据:
1. 项目基本信息(名称、周期、预算、目标)
2. 项目里程碑(关键节点)
3. 任务清单WBS工作分解结构
4. 项目成员及角色
5. 资源需求
6. 风险识别
# 输出格式
请严格按照以下JSON格式输出
```json
{
"project": {
"project_name": "项目名称",
"project_type": "工程项目/研发项目/运营项目",
"description": "项目描述",
"objectives": "项目目标",
"plan_start_date": "2024-01-01",
"plan_end_date": "2024-12-31",
"budget": 1000000,
"currency": "CNY",
"priority": "high/medium/low",
"tags": ["标签1", "标签2"]
},
"milestones": [
{
"milestone_name": "里程碑名称",
"description": "描述",
"plan_date": "2024-03-01",
"deliverables": "交付物",
"owner_role": "负责人角色"
}
],
"tasks": [
{
"task_name": "任务名称",
"parent_task_id": null,
"description": "任务描述",
"plan_start_date": "2024-01-15",
"plan_end_date": "2024-02-15",
"estimated_hours": 80,
"priority": "high",
"assignee_role": "执行者角色",
"dependencies": ["前置任务ID"],
"deliverables": "交付物"
}
],
"members": [
{
"name": "姓名",
"role_code": "manager/leader/member",
"responsibility": "职责描述",
"department": "部门",
"weekly_hours": 40
}
],
"resources": [
{
"resource_name": "资源名称",
"resource_type": "human/material/equipment/software/finance",
"quantity": 1,
"unit": "单位",
"unit_price": 1000,
"supplier": "供应商"
}
],
"risks": [
{
"risk_name": "风险名称",
"category": "technical/schedule/cost/quality/resource/external",
"description": "风险描述",
"probability": 60,
"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"]
}
]
}
```
# 注意事项
1. 日期格式统一使用 YYYY-MM-DD
2. 任务之间要建立合理的依赖关系
3. 里程碑应该是关键节点,不宜过多
4. 资源要考虑人力、物料、设备等
5. 风险识别要基于项目特点
6. 如果文档信息不完整,根据项目类型合理推断
""";
/**
* 用户提示词模板
*/
private static final String USER_PROMPT_TEMPLATE = """
请根据以下项目资料,生成完整的项目初始化结构化数据:
{content}
请严格按照系统提示词中的JSON格式输出确保所有字段都包含合理的值。
""";
@Override
public ProjectInitResult generateProjectFromContent(String content) {
log.info("开始根据内容生成项目初始化数据");
// 构建用户提示词,直接将内容嵌入
String userPrompt = "请根据以下项目资料,生成完整的项目初始化结构化数据:\n\n" +
content + "\n\n" +
"请严格按照系统提示词中的JSON格式输出确保所有字段都包含合理的值。";
return chatClient.prompt()
.system(PROJECT_INIT_SYSTEM_PROMPT)
.user(userPrompt)
.call()
.entity(ProjectInitResult.class);
}
@Override
public ProjectInitResult generateProjectFromFile(String fileUrl, String fileType) {
log.info("开始根据文件生成项目初始化数据, fileUrl: {}, fileType: {}", fileUrl, fileType);
// 从OSS下载文件内容
String content = ossService.readFileAsString(fileUrl);
if (content == null || content.isEmpty()) {
throw new RuntimeException("无法读取文件内容: " + fileUrl);
}
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. 生成并保存项目数据
return generateAndSaveProjectFromContent(content);
} catch (Exception e) {
log.error("项目初始化失败: {}", e.getMessage(), e);
throw new RuntimeException("项目初始化失败: " + e.getMessage(), e);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ProjectInitResult generateAndSaveProjectFromContent(String content) {
log.info("开始生成并保存项目初始化数据");
ProjectInitResult result;
try {
// 1. 调用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);
// 2. 保存项目数据到数据库
Long projectId = saveProjectData(result);
log.info("项目初始化数据保存成功, projectId: {}", projectId);
return result;
} catch (Exception e) {
log.error("项目初始化失败: {}", e.getMessage(), e);
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;
}
}