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 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 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 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; } }