diff --git a/src/main/java/cn/yinlihupo/common/handler/JsonbTypeHandler.java b/src/main/java/cn/yinlihupo/common/handler/JsonbTypeHandler.java new file mode 100644 index 0000000..e562c02 --- /dev/null +++ b/src/main/java/cn/yinlihupo/common/handler/JsonbTypeHandler.java @@ -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 { + + 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); + } + } +} diff --git a/src/main/java/cn/yinlihupo/controller/project/ProjectController.java b/src/main/java/cn/yinlihupo/controller/project/ProjectController.java index fde06e3..fdf3cb5 100644 --- a/src/main/java/cn/yinlihupo/controller/project/ProjectController.java +++ b/src/main/java/cn/yinlihupo/controller/project/ProjectController.java @@ -1,8 +1,7 @@ package cn.yinlihupo.controller.project; import cn.yinlihupo.common.result.Result; -import cn.yinlihupo.domain.dto.ProjectInitRequest; -import cn.yinlihupo.domain.dto.ProjectInitResult; +import cn.yinlihupo.domain.vo.ProjectInitResult; import cn.yinlihupo.service.oss.OssService; import cn.yinlihupo.service.project.ProjectService; import lombok.RequiredArgsConstructor; @@ -38,14 +37,8 @@ public class ProjectController { } try { - // 上传文件到OSS - String fileUrl = ossService.uploadFile(file, file.getOriginalFilename()); - log.info("文件上传成功,URL: {}", fileUrl); - - // 根据文件生成项目初始化数据 - String fileType = getFileExtension(file.getOriginalFilename()); - ProjectInitResult result = projectService.generateProjectFromFile(fileUrl, fileType); - + // 上传文件、生成项目初始化数据并保存到数据库 + ProjectInitResult result = projectService.generateAndSaveProject(file); return Result.success("项目初始化成功", result); } catch (Exception e) { log.error("项目初始化失败: {}", e.getMessage(), e); diff --git a/src/main/java/cn/yinlihupo/domain/entity/Project.java b/src/main/java/cn/yinlihupo/domain/entity/Project.java new file mode 100644 index 0000000..3c58e17 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/Project.java @@ -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 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; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/ProjectInitRecord.java b/src/main/java/cn/yinlihupo/domain/entity/ProjectInitRecord.java new file mode 100644 index 0000000..06ea50a --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/ProjectInitRecord.java @@ -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 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; + } +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/ProjectMember.java b/src/main/java/cn/yinlihupo/domain/entity/ProjectMember.java new file mode 100644 index 0000000..9fe2747 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/ProjectMember.java @@ -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; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/ProjectMilestone.java b/src/main/java/cn/yinlihupo/domain/entity/ProjectMilestone.java new file mode 100644 index 0000000..141bafc --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/ProjectMilestone.java @@ -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 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; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/ProjectTimeline.java b/src/main/java/cn/yinlihupo/domain/entity/ProjectTimeline.java new file mode 100644 index 0000000..871e71a --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/ProjectTimeline.java @@ -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 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; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/Resource.java b/src/main/java/cn/yinlihupo/domain/entity/Resource.java new file mode 100644 index 0000000..84993d4 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/Resource.java @@ -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 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; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/Risk.java b/src/main/java/cn/yinlihupo/domain/entity/Risk.java new file mode 100644 index 0000000..3166823 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/Risk.java @@ -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 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 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; +} diff --git a/src/main/java/cn/yinlihupo/domain/entity/Task.java b/src/main/java/cn/yinlihupo/domain/entity/Task.java new file mode 100644 index 0000000..e08834f --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/entity/Task.java @@ -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 tags; + + /** + * 附件列表 + */ + @TableField(typeHandler = JsonbTypeHandler.class) + private List 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; +} diff --git a/src/main/java/cn/yinlihupo/domain/dto/ProjectInitResult.java b/src/main/java/cn/yinlihupo/domain/vo/ProjectInitResult.java similarity index 99% rename from src/main/java/cn/yinlihupo/domain/dto/ProjectInitResult.java rename to src/main/java/cn/yinlihupo/domain/vo/ProjectInitResult.java index bdf4419..1f447fd 100644 --- a/src/main/java/cn/yinlihupo/domain/dto/ProjectInitResult.java +++ b/src/main/java/cn/yinlihupo/domain/vo/ProjectInitResult.java @@ -1,4 +1,4 @@ -package cn.yinlihupo.domain.dto; +package cn.yinlihupo.domain.vo; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectInitRecordMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectInitRecordMapper.java new file mode 100644 index 0000000..7dfe4e8 --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ProjectInitRecordMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java new file mode 100644 index 0000000..d316b26 --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ProjectMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectMemberMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectMemberMapper.java new file mode 100644 index 0000000..3b8cc91 --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ProjectMemberMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectMilestoneMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectMilestoneMapper.java new file mode 100644 index 0000000..0148368 --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ProjectMilestoneMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/ProjectTimelineMapper.java b/src/main/java/cn/yinlihupo/mapper/ProjectTimelineMapper.java new file mode 100644 index 0000000..ff298cc --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ProjectTimelineMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/ResourceMapper.java b/src/main/java/cn/yinlihupo/mapper/ResourceMapper.java new file mode 100644 index 0000000..c61941e --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/ResourceMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/RiskMapper.java b/src/main/java/cn/yinlihupo/mapper/RiskMapper.java new file mode 100644 index 0000000..eed53da --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/RiskMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/mapper/TaskMapper.java b/src/main/java/cn/yinlihupo/mapper/TaskMapper.java new file mode 100644 index 0000000..2c9043a --- /dev/null +++ b/src/main/java/cn/yinlihupo/mapper/TaskMapper.java @@ -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 { +} diff --git a/src/main/java/cn/yinlihupo/service/project/ProjectService.java b/src/main/java/cn/yinlihupo/service/project/ProjectService.java index ba785f0..4e33b98 100644 --- a/src/main/java/cn/yinlihupo/service/project/ProjectService.java +++ b/src/main/java/cn/yinlihupo/service/project/ProjectService.java @@ -1,6 +1,9 @@ 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项目初始化服务接口 @@ -24,4 +27,21 @@ public interface ProjectService { * @return 项目初始化结果 */ ProjectInitResult generateProjectFromFile(String fileUrl, String fileType); + + /** + * 上传文件并生成项目初始化数据,同时保存到数据库 + * + * @param file 项目资料文件 + * @return 项目初始化结果 + */ + ProjectInitResult generateAndSaveProject(MultipartFile file); + + /** + * 根据项目资料内容生成并保存项目初始化数据 + * + * @param content 项目资料文本内容 + * @param inputFiles 输入文件列表信息 + * @return 项目初始化结果 + */ + ProjectInitResult generateAndSaveProjectFromContent(String content, List inputFiles); } diff --git a/src/main/java/cn/yinlihupo/service/project/impl/ProjectServiceImpl.java b/src/main/java/cn/yinlihupo/service/project/impl/ProjectServiceImpl.java index e6b48aa..f019c9a 100644 --- a/src/main/java/cn/yinlihupo/service/project/impl/ProjectServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/project/impl/ProjectServiceImpl.java @@ -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 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 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 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 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; + } } diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index d53690f..9d7f755 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -6,7 +6,7 @@ spring: # PostgreSQL 数据库配置 datasource: - url: jdbc:postgresql://10.200.8.25:5432/ylhp_ai_project_manager + url: jdbc:postgresql://10.200.8.25:5432/aiprojectmanager username: postgres password: postgres driver-class-name: org.postgresql.Driver