feat(project): 实现AI项目初始化功能

- 新增项目初始化控制器,支持文件上传生成项目结构化数据
- 定义项目初始化结果DTO,包含项目、里程碑、任务、成员、资源、风险和时间节点等信息
- 实现项目初始化服务接口及其实现类,集成Spring AI结构化输出能力
- 支持根据内容或文件生成项目初始化数据,并保存到数据库
- 增加项目、里程碑、任务、成员、资源、风险及时间节点实体及对应Mapper
- 实现文件上传到OSS及项目初始化记录功能,记录解析状态及结果
- 添加PostgreSQL JSONB类型处理器,支持JSON对象字段存储
- 修改开发环境数据库配置,更新连接的数据库名称为aiprojectmanager
This commit is contained in:
2026-03-26 20:05:55 +08:00
parent 0bf41c5353
commit 729af44585
22 changed files with 1623 additions and 15 deletions

View File

@@ -0,0 +1,73 @@
package cn.yinlihupo.common.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.postgresql.util.PGobject;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* PostgreSQL JSONB 类型处理器
* 用于处理 Java 对象与 PostgreSQL jsonb 类型之间的转换
*/
public class JsonbTypeHandler extends BaseTypeHandler<Object> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String JSONB_TYPE = "jsonb";
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
PGobject jsonObject = new PGobject();
jsonObject.setType(JSONB_TYPE);
try {
jsonObject.setValue(objectMapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("Error converting object to JSONB", e);
}
ps.setObject(i, jsonObject);
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
if (json == null) {
return null;
}
try {
return objectMapper.readValue(json, Object.class);
} catch (JsonProcessingException e) {
throw new SQLException("Error parsing JSONB from column " + columnName, e);
}
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
if (json == null) {
return null;
}
try {
return objectMapper.readValue(json, Object.class);
} catch (JsonProcessingException e) {
throw new SQLException("Error parsing JSONB from column index " + columnIndex, e);
}
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
if (json == null) {
return null;
}
try {
return objectMapper.readValue(json, Object.class);
} catch (JsonProcessingException e) {
throw new SQLException("Error parsing JSONB from callable statement column index " + columnIndex, e);
}
}
}

View File

@@ -1,8 +1,7 @@
package cn.yinlihupo.controller.project; package cn.yinlihupo.controller.project;
import cn.yinlihupo.common.result.Result; import cn.yinlihupo.common.result.Result;
import cn.yinlihupo.domain.dto.ProjectInitRequest; import cn.yinlihupo.domain.vo.ProjectInitResult;
import cn.yinlihupo.domain.dto.ProjectInitResult;
import cn.yinlihupo.service.oss.OssService; import cn.yinlihupo.service.oss.OssService;
import cn.yinlihupo.service.project.ProjectService; import cn.yinlihupo.service.project.ProjectService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -38,14 +37,8 @@ public class ProjectController {
} }
try { try {
// 上传文件到OSS // 上传文件、生成项目初始化数据并保存到数据库
String fileUrl = ossService.uploadFile(file, file.getOriginalFilename()); ProjectInitResult result = projectService.generateAndSaveProject(file);
log.info("文件上传成功URL: {}", fileUrl);
// 根据文件生成项目初始化数据
String fileType = getFileExtension(file.getOriginalFilename());
ProjectInitResult result = projectService.generateProjectFromFile(fileUrl, fileType);
return Result.success("项目初始化成功", result); return Result.success("项目初始化成功", result);
} catch (Exception e) { } catch (Exception e) {
log.error("项目初始化失败: {}", e.getMessage(), e); log.error("项目初始化失败: {}", e.getMessage(), e);

View File

@@ -0,0 +1,157 @@
package cn.yinlihupo.domain.entity;
import cn.yinlihupo.common.handler.JsonbTypeHandler;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 项目实体类
* 对应数据库表: project
*/
@Data
@TableName("project")
public class Project {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 项目编号
*/
private String projectCode;
/**
* 项目名称
*/
private String projectName;
/**
* 项目类型
*/
private String projectType;
/**
* 项目描述
*/
private String description;
/**
* 项目目标
*/
private String objectives;
/**
* 项目经理ID
*/
private Long managerId;
/**
* 项目发起人ID
*/
private Long sponsorId;
/**
* 计划开始日期
*/
private LocalDate planStartDate;
/**
* 计划结束日期
*/
private LocalDate planEndDate;
/**
* 实际开始日期
*/
private LocalDate actualStartDate;
/**
* 实际结束日期
*/
private LocalDate actualEndDate;
/**
* 项目预算
*/
private BigDecimal budget;
/**
* 已花费金额
*/
private BigDecimal cost;
/**
* 币种
*/
private String currency;
/**
* 进度百分比
*/
private Integer progress;
/**
* 状态: draft-草稿, planning-规划中, ongoing-进行中, paused-暂停, completed-已完成, cancelled-已取消
*/
private String status;
/**
* 优先级: critical-关键, high-高, medium-中, low-低
*/
private String priority;
/**
* 风险等级: high-高, medium-中, low-低
*/
private String riskLevel;
/**
* 可见性: 1-公开, 2-部门内, 3-项目成员
*/
private Integer visibility;
/**
* 标签列表
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<String> tags;
/**
* 扩展数据
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object extraData;
/**
* 创建人
*/
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,105 @@
package cn.yinlihupo.domain.entity;
import cn.yinlihupo.common.handler.JsonbTypeHandler;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 项目初始化记录实体类
* 对应数据库表: project_init_record
*/
@Data
@TableName("project_init_record")
public class ProjectInitRecord {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 项目ID
*/
private Long projectId;
/**
* 上传的文件列表[{name, path, type, size}]
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<InputFile> inputFiles;
/**
* 用户输入的项目描述
*/
private String inputText;
/**
* 状态: pending-待解析, processing-解析中, completed-已完成, failed-失败
*/
private String parseStatus;
/**
* 解析结果(结构化数据)
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object parseResult;
/**
* 生成的里程碑数量
*/
private Integer generatedMilestones;
/**
* 生成的任务数量
*/
private Integer generatedTasks;
/**
* 生成的成员数量
*/
private Integer generatedMembers;
/**
* 生成的资源数量
*/
private Integer generatedResources;
/**
* 使用的AI模型
*/
private String model;
/**
* 消耗的Token数
*/
private Integer tokensUsed;
/**
* 错误信息
*/
private String errorMessage;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 输入文件信息内部类
*/
@Data
public static class InputFile {
private String name;
private String path;
private String type;
private Long size;
}
}

View File

@@ -0,0 +1,83 @@
package cn.yinlihupo.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 项目成员实体类
* 对应数据库表: project_member
*/
@Data
@TableName("project_member")
public class ProjectMember {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 项目ID
*/
private Long projectId;
/**
* 用户ID
*/
private Long userId;
/**
* 项目角色: manager-项目经理, leader-负责人, member-成员, observer-观察者
*/
private String roleCode;
/**
* 加入日期
*/
private LocalDate joinDate;
/**
* 离开日期
*/
private LocalDate leaveDate;
/**
* 职责描述
*/
private String responsibility;
/**
* 每周投入小时数
*/
private BigDecimal weeklyHours;
/**
* 状态: 1-正常, 0-已移除
*/
private Integer status;
/**
* 创建人
*/
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,106 @@
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;
import java.util.List;
/**
* 项目里程碑实体类
* 对应数据库表: project_milestone
*/
@Data
@TableName("project_milestone")
public class ProjectMilestone {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 项目ID
*/
private Long projectId;
/**
* 里程碑名称
*/
private String milestoneName;
/**
* 描述
*/
private String description;
/**
* 计划日期
*/
private LocalDate planDate;
/**
* 实际日期
*/
private LocalDate actualDate;
/**
* 状态: pending-待开始, in_progress-进行中, completed-已完成, delayed-延期
*/
private String status;
/**
* 完成进度
*/
private Integer progress;
/**
* 排序
*/
private Integer sortOrder;
/**
* 是否关键里程碑: 1-是, 0-否
*/
private Integer isKey;
/**
* 交付物列表
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<String> deliverables;
/**
* 扩展数据
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object extraData;
/**
* 创建人
*/
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,96 @@
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;
import java.util.List;
/**
* 项目时间节点实体类
* 对应数据库表: project_timeline
*/
@Data
@TableName("project_timeline")
public class ProjectTimeline {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 项目ID
*/
private Long projectId;
/**
* 节点名称
*/
private String nodeName;
/**
* 节点类型: phase-阶段, milestone-里程碑, event-事件, checkpoint-检查点
*/
private String nodeType;
/**
* 父节点ID
*/
private Long parentId;
/**
* 计划日期
*/
private LocalDate planDate;
/**
* 实际日期
*/
private LocalDate actualDate;
/**
* 描述
*/
private String description;
/**
* 状态: pending-待开始, in_progress-进行中, completed-已完成, delayed-延期
*/
private String status;
/**
* 排序
*/
private Integer sortOrder;
/**
* 知识库范围配置["report","file","risk","ticket"]
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<String> kbScope;
/**
* 扩展数据
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object extraData;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,147 @@
package cn.yinlihupo.domain.entity;
import cn.yinlihupo.common.handler.JsonbTypeHandler;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 资源实体类
* 对应数据库表: resource
*/
@Data
@TableName("resource")
public class Resource {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 资源编号
*/
private String resourceCode;
/**
* 项目ID
*/
private Long projectId;
/**
* 资源类型: human-人力, material-物料, equipment-设备, software-软件, finance-资金, other-其他
*/
private String resourceType;
/**
* 资源名称
*/
private String resourceName;
/**
* 资源描述
*/
private String description;
/**
* 规格型号
*/
private String specification;
/**
* 单位
*/
private String unit;
/**
* 计划数量
*/
private BigDecimal planQuantity;
/**
* 实际数量
*/
private BigDecimal actualQuantity;
/**
* 单价
*/
private BigDecimal unitPrice;
/**
* 币种
*/
private String currency;
/**
* 供应商/来源
*/
private String supplier;
/**
* 状态: planned-计划中, requested-已申请, approved-已批准, procuring-采购中, arrived-已到货, in_use-使用中, completed-已完成
*/
private String status;
/**
* 计划到位日期
*/
private LocalDate planArriveDate;
/**
* 实际到位日期
*/
private LocalDate actualArriveDate;
/**
* 负责人ID
*/
private Long responsibleId;
/**
* 存放位置
*/
private String location;
/**
* 标签
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<String> tags;
/**
* 扩展数据
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object extraData;
/**
* 创建人
*/
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,169 @@
package cn.yinlihupo.domain.entity;
import cn.yinlihupo.common.handler.JsonbTypeHandler;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 风险实体类
* 对应数据库表: risk
*/
@Data
@TableName("risk")
public class Risk {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 风险编号
*/
private String riskCode;
/**
* 项目ID
*/
private Long projectId;
/**
* 风险分类: technical-技术风险, schedule-进度风险, cost-成本风险, quality-质量风险, resource-资源风险, external-外部风险, other-其他
*/
private String category;
/**
* 风险名称
*/
private String riskName;
/**
* 风险描述
*/
private String description;
/**
* 风险来源: internal-内部, external-外部, ai_detection-AI检测
*/
private String riskSource;
/**
* 风险类型
*/
private String riskType;
/**
* 发生概率(0-100%)
*/
private BigDecimal probability;
/**
* 影响程度(1-5)
*/
private BigDecimal impact;
/**
* 风险得分(概率*影响)
*/
private BigDecimal riskScore;
/**
* 风险等级: critical-严重, high-高, medium-中, low-低
*/
private String riskLevel;
/**
* 状态: identified-已识别, assigned-已分派工单, mitigating-缓解中, resolved-已解决, closed-已关闭
*/
private String status;
/**
* 负责人ID
*/
private Long ownerId;
/**
* 关联的工单ID数组
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<Long> workOrderIds;
/**
* 缓解措施
*/
private String mitigationPlan;
/**
* 应急计划
*/
private String contingencyPlan;
/**
* 触发条件
*/
private String triggerCondition;
/**
* 发现时间
*/
private LocalDateTime discoverTime;
/**
* 预期解决日期
*/
private LocalDate dueDate;
/**
* 解决时间
*/
private LocalDateTime resolvedTime;
/**
* AI分析结果
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object aiAnalysis;
/**
* 标签
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<String> tags;
/**
* 扩展数据
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object extraData;
/**
* 创建人
*/
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,158 @@
package cn.yinlihupo.domain.entity;
import cn.yinlihupo.common.handler.JsonbTypeHandler;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 任务实体类
* 对应数据库表: task
*/
@Data
@TableName("task")
public class Task {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 任务编号
*/
private String taskCode;
/**
* 项目ID
*/
private Long projectId;
/**
* 所属里程碑ID
*/
private Long milestoneId;
/**
* 父任务ID
*/
private Long parentId;
/**
* 任务名称
*/
private String taskName;
/**
* 任务描述
*/
private String description;
/**
* 任务类型
*/
private String taskType;
/**
* 执行人ID
*/
private Long assigneeId;
/**
* 计划开始日期
*/
private LocalDate planStartDate;
/**
* 计划结束日期
*/
private LocalDate planEndDate;
/**
* 实际开始日期
*/
private LocalDate actualStartDate;
/**
* 实际结束日期
*/
private LocalDate actualEndDate;
/**
* 计划工时(小时)
*/
private BigDecimal planHours;
/**
* 实际工时(小时)
*/
private BigDecimal actualHours;
/**
* 进度百分比
*/
private Integer progress;
/**
* 优先级: critical-关键, high-高, medium-中, low-低
*/
private String priority;
/**
* 状态: pending-待开始, in_progress-进行中, completed-已完成, cancelled-已取消
*/
private String status;
/**
* 排序
*/
private Integer sortOrder;
/**
* 标签
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<String> tags;
/**
* 附件列表
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private List<Object> attachments;
/**
* 扩展数据
*/
@TableField(typeHandler = JsonbTypeHandler.class)
private Object extraData;
/**
* 创建人
*/
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -1,4 +1,4 @@
package cn.yinlihupo.domain.dto; package cn.yinlihupo.domain.vo;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;

View File

@@ -0,0 +1,12 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.ProjectInitRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 项目初始化记录Mapper接口
*/
@Mapper
public interface ProjectInitRecordMapper extends BaseMapper<ProjectInitRecord> {
}

View File

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

View File

@@ -0,0 +1,12 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.ProjectMember;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 项目成员Mapper接口
*/
@Mapper
public interface ProjectMemberMapper extends BaseMapper<ProjectMember> {
}

View File

@@ -0,0 +1,12 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.ProjectMilestone;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 项目里程碑Mapper接口
*/
@Mapper
public interface ProjectMilestoneMapper extends BaseMapper<ProjectMilestone> {
}

View File

@@ -0,0 +1,12 @@
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,12 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.Resource;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 资源Mapper接口
*/
@Mapper
public interface ResourceMapper extends BaseMapper<Resource> {
}

View File

@@ -0,0 +1,12 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.Risk;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 风险Mapper接口
*/
@Mapper
public interface RiskMapper extends BaseMapper<Risk> {
}

View File

@@ -0,0 +1,12 @@
package cn.yinlihupo.mapper;
import cn.yinlihupo.domain.entity.Task;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 任务Mapper接口
*/
@Mapper
public interface TaskMapper extends BaseMapper<Task> {
}

View File

@@ -1,6 +1,9 @@
package cn.yinlihupo.service.project; package cn.yinlihupo.service.project;
import cn.yinlihupo.domain.dto.ProjectInitResult; import cn.yinlihupo.domain.vo.ProjectInitResult;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/** /**
* AI项目初始化服务接口 * AI项目初始化服务接口
@@ -24,4 +27,21 @@ public interface ProjectService {
* @return 项目初始化结果 * @return 项目初始化结果
*/ */
ProjectInitResult generateProjectFromFile(String fileUrl, String fileType); ProjectInitResult generateProjectFromFile(String fileUrl, String fileType);
/**
* 上传文件并生成项目初始化数据,同时保存到数据库
*
* @param file 项目资料文件
* @return 项目初始化结果
*/
ProjectInitResult generateAndSaveProject(MultipartFile file);
/**
* 根据项目资料内容生成并保存项目初始化数据
*
* @param content 项目资料文本内容
* @param inputFiles 输入文件列表信息
* @return 项目初始化结果
*/
ProjectInitResult generateAndSaveProjectFromContent(String content, List<cn.yinlihupo.domain.entity.ProjectInitRecord.InputFile> inputFiles);
} }

View File

@@ -1,13 +1,28 @@
package cn.yinlihupo.service.project.impl; 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.oss.OssService;
import cn.yinlihupo.service.project.ProjectService; import cn.yinlihupo.service.project.ProjectService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient; 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.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项目初始化服务实现类 * AI项目初始化服务实现类
@@ -21,6 +36,16 @@ public class ProjectServiceImpl implements ProjectService {
private final ChatClient chatClient; private final ChatClient chatClient;
private final OssService ossService; 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); 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;
}
} }

View File

@@ -6,7 +6,7 @@ spring:
# PostgreSQL 数据库配置 # PostgreSQL 数据库配置
datasource: datasource:
url: jdbc:postgresql://10.200.8.25:5432/ylhp_ai_project_manager url: jdbc:postgresql://10.200.8.25:5432/aiprojectmanager
username: postgres username: postgres
password: postgres password: postgres
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver