feat(core): 完成AI项目管理平台基础模块开发

- 新增Spring Boot配置文件,支持多环境切换与数据库配置
- 集成PostgreSQL数据库和MyBatis Plus实现数据访问层
- 配置Spring AI和MinIO对象存储服务,支持文件上传下载功能
- 自定义错误码枚举,提供统一错误处理标准
- 实现MinIO客户端自动配置及服务端上传、下载和删除文件功能
- 开发OSS控制器及服务接口,实现文件管理API及文件存储操作
- 开发AI项目初始化模块,支持通过文本和文件生成结构化项目数据
- 设计项目初始化结果DTO,定义项目、里程碑、任务、成员、资源、风险等数据结构
- 实现项目初始化服务,调用AI聊天模型解析项目文档生成结构化输出
- 添加分页查询工具类,支持动态排序和分页参数构建
- 项目构建配置完善,集成必要依赖,支持Spring Boot 3和Java 17环境
- 代码结构规范,增加模块包说明及统一响应结果封装体系
This commit is contained in:
2026-03-26 17:18:05 +08:00
parent e82b5c2f0b
commit 4656090683
28 changed files with 141 additions and 313 deletions

View File

@@ -0,0 +1,71 @@
package cn.yinlihupo.service.oss;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
/**
* OSS文件服务接口
* 用于文件上传、下载和管理
*/
public interface OssService {
/**
* 上传文件到OSS
*
* @param file 文件
* @param fileName 文件名
* @return 文件访问URL
*/
String uploadFile(MultipartFile file, String fileName);
/**
* 上传文件到OSS指定存储桶
*
* @param file 文件
* @param fileName 文件名
* @param bucketName 存储桶名称
* @return 文件访问URL
*/
String uploadFile(MultipartFile file, String fileName, String bucketName);
/**
* 从URL读取文件内容为字符串
*
* @param fileUrl 文件URL
* @return 文件内容
*/
String readFileAsString(String fileUrl);
/**
* 从URL获取文件输入流
*
* @param fileUrl 文件URL
* @return 文件输入流
*/
InputStream getFileInputStream(String fileUrl);
/**
* 删除文件
*
* @param fileUrl 文件URL
*/
void deleteFile(String fileUrl);
/**
* 获取文件URL
*
* @param objectName 对象名称
* @return 文件URL
*/
String getFileUrl(String objectName);
/**
* 获取文件URL指定存储桶
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 文件URL
*/
String getFileUrl(String bucketName, String objectName);
}

View File

@@ -0,0 +1,213 @@
package cn.yinlihupo.service.oss.impl;
import cn.yinlihupo.common.config.MinioConfig;
import cn.yinlihupo.service.oss.OssService;
import io.minio.*;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* OSS文件服务实现类基于MinIO
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OssServiceImpl implements OssService {
private final MinioClient minioClient;
private final MinioConfig minioConfig;
@Override
public String uploadFile(MultipartFile file, String fileName) {
return uploadFile(file, fileName, minioConfig.getBucketName());
}
@Override
public String uploadFile(MultipartFile file, String fileName, String bucketName) {
try {
// 确保存储桶存在
ensureBucketExists(bucketName);
// 生成唯一的对象名称
String objectName = generateObjectName(fileName);
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
log.info("文件上传成功: {}/{}", bucketName, objectName);
// 返回文件URL
return getFileUrl(bucketName, objectName);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
}
}
@Override
public String readFileAsString(String fileUrl) {
try (InputStream inputStream = getFileInputStream(fileUrl);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toString(StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("读取文件内容失败: {}", e.getMessage(), e);
throw new RuntimeException("读取文件内容失败: " + e.getMessage(), e);
}
}
@Override
public InputStream getFileInputStream(String fileUrl) {
try {
// 解析URL获取bucket和object名称
URL url = new URL(fileUrl);
String path = url.getPath();
// 去掉开头的/
if (path.startsWith("/")) {
path = path.substring(1);
}
// 解析bucket和object
int slashIndex = path.indexOf('/');
String bucketName;
String objectName;
if (slashIndex > 0) {
bucketName = path.substring(0, slashIndex);
objectName = path.substring(slashIndex + 1);
} else {
bucketName = minioConfig.getBucketName();
objectName = path;
}
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
} catch (Exception e) {
log.error("获取文件输入流失败: {}", e.getMessage(), e);
throw new RuntimeException("获取文件输入流失败: " + e.getMessage(), e);
}
}
@Override
public void deleteFile(String fileUrl) {
try {
// 解析URL获取bucket和object名称
URL url = new URL(fileUrl);
String path = url.getPath();
if (path.startsWith("/")) {
path = path.substring(1);
}
int slashIndex = path.indexOf('/');
String bucketName;
String objectName;
if (slashIndex > 0) {
bucketName = path.substring(0, slashIndex);
objectName = path.substring(slashIndex + 1);
} else {
bucketName = minioConfig.getBucketName();
objectName = path;
}
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
log.info("文件删除成功: {}/{}", bucketName, objectName);
} catch (Exception e) {
log.error("删除文件失败: {}", e.getMessage(), e);
throw new RuntimeException("删除文件失败: " + e.getMessage(), e);
}
}
@Override
public String getFileUrl(String objectName) {
return getFileUrl(minioConfig.getBucketName(), objectName);
}
@Override
public String getFileUrl(String bucketName, String objectName) {
try {
// 构建文件URL
String endpoint = minioConfig.getEndpoint();
if (endpoint.endsWith("/")) {
endpoint = endpoint.substring(0, endpoint.length() - 1);
}
return endpoint + "/" + bucketName + "/" + objectName;
} catch (Exception e) {
log.error("获取文件URL失败: {}", e.getMessage(), e);
throw new RuntimeException("获取文件URL失败: " + e.getMessage(), e);
}
}
/**
* 确保存储桶存在
*
* @param bucketName 存储桶名称
*/
private void ensureBucketExists(String bucketName) throws Exception {
boolean exists = minioClient.bucketExists(
BucketExistsArgs.builder()
.bucket(bucketName)
.build()
);
if (!exists) {
minioClient.makeBucket(
MakeBucketArgs.builder()
.bucket(bucketName)
.build()
);
log.info("创建存储桶成功: {}", bucketName);
}
}
/**
* 生成对象名称
*
* @param originalFileName 原始文件名
* @return 对象名称
*/
private String generateObjectName(String originalFileName) {
String uuid = UUID.randomUUID().toString().replace("-", "");
String timestamp = String.valueOf(System.currentTimeMillis());
// 提取文件扩展名
String extension = "";
int dotIndex = originalFileName.lastIndexOf('.');
if (dotIndex > 0) {
extension = originalFileName.substring(dotIndex);
}
return timestamp + "_" + uuid + extension;
}
}

View File

@@ -0,0 +1,27 @@
package cn.yinlihupo.service.project;
import cn.yinlihupo.domain.dto.ProjectInitResult;
/**
* AI项目初始化服务接口
* 使用Spring AI结构化输出能力从项目文档中提取结构化信息
*/
public interface ProjectService {
/**
* 根据项目资料内容生成项目初始化结构化数据
*
* @param content 项目资料文本内容
* @return 项目初始化结果,包含项目信息、里程碑、任务、成员、资源、风险等
*/
ProjectInitResult generateProjectFromContent(String content);
/**
* 根据OSS文件URL生成项目初始化结构化数据
*
* @param fileUrl OSS文件URL
* @param fileType 文件类型
* @return 项目初始化结果
*/
ProjectInitResult generateProjectFromFile(String fileUrl, String fileType);
}

View File

@@ -0,0 +1,173 @@
package cn.yinlihupo.service.project.impl;
import cn.yinlihupo.domain.dto.ProjectInitResult;
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.stereotype.Service;
/**
* AI项目初始化服务实现类
* 使用Spring AI结构化输出能力解析项目文档
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProjectServiceImpl implements ProjectService {
private final ChatClient chatClient;
private final OssService ossService;
/**
* 项目初始化系统提示词模板
*/
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("开始根据内容生成项目初始化数据");
PromptTemplate promptTemplate = new PromptTemplate(USER_PROMPT_TEMPLATE);
String userPrompt = promptTemplate.createMessage(java.util.Map.of("content", content)).toString();
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);
}
}