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

@@ -41,6 +41,13 @@
<version>${lombok.version}</version> <version>${lombok.version}</version>
</dependency> </dependency>
<!--hutool all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<!-- PostgreSQL 数据库驱动 --> <!-- PostgreSQL 数据库驱动 -->
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>

View File

@@ -1,35 +0,0 @@
package cn.yinlihupo.common.base;
import cn.yinlihupo.common.enums.ErrorCode;
import lombok.Data;
import java.io.Serializable;
/**
* 通用返回类
*
* @param <T>
*/
@Data
public class BaseResponse<T> implements Serializable {
private int code;
private T data;
private String message;
public BaseResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public BaseResponse(int code, T data) {
this(code, data, "");
}
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage());
}
}

View File

@@ -1,5 +0,0 @@
/**
* 基础模块
* 包含基础实体、基础接口等
*/
package cn.yinlihupo.ylhpaiprojectmanager.common.base;

View File

@@ -1,28 +0,0 @@
package cn.yinlihupo.common.exception;
import cn.yinlihupo.common.enums.ErrorCode;
/**
* 自定义异常类
*/
public class BusinessException extends RuntimeException {
/**
* 错误码
*/
private final int code;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}

View File

@@ -1,94 +0,0 @@
package cn.yinlihupo.common.exception;
import cn.yinlihupo.common.base.BaseResponse;
import cn.yinlihupo.common.enums.ErrorCode;
import cn.yinlihupo.common.util.ResultUtils;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
*/
@RestControllerAdvice
@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("BusinessException", e);
return ResultUtils.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(OrderException.class)
public BaseResponse<?> orderExceptionHandler(OrderException e) {
log.error("OrderException", e);
// 如果没有设置错误码,使用默认的业务错误码
Integer code = e.getCode();
if (code == null) {
return ResultUtils.error(ErrorCode.OPERATION_ERROR, e.getMessage());
}
return ResultUtils.error(code, e.getMessage());
}
@ExceptionHandler(ServiceException.class)
public BaseResponse<?> serviceExceptionHandler(ServiceException e) {
log.error("ServiceException", e);
// 如果没有设置错误码,使用默认的业务错误码
Integer code = e.getCode();
if (code == null) {
return ResultUtils.error(ErrorCode.OPERATION_ERROR, e.getMessage());
}
return ResultUtils.error(code, e.getMessage());
}
@ExceptionHandler(UtilException.class)
public BaseResponse<?> utilExceptionHandler(UtilException e) {
log.error("UtilException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public BaseResponse<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException", e);
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return ResultUtils.error(ErrorCode.PARAMS_ERROR, errorMsg);
}
@ExceptionHandler(BindException.class)
public BaseResponse<?> bindExceptionHandler(BindException e) {
log.error("BindException", e);
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return ResultUtils.error(ErrorCode.PARAMS_ERROR, errorMsg);
}
@ExceptionHandler(ConstraintViolationException.class)
public BaseResponse<?> constraintViolationExceptionHandler(ConstraintViolationException e) {
log.error("ConstraintViolationException", e);
String errorMsg = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
return ResultUtils.error(ErrorCode.PARAMS_ERROR, errorMsg);
}
@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
log.error("RuntimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
}

View File

@@ -1,54 +0,0 @@
package cn.yinlihupo.common.util;
import cn.yinlihupo.common.base.BaseResponse;
import cn.yinlihupo.common.enums.ErrorCode;
/**
* 返回工具类
*
*/
public class ResultUtils {
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(200, data, "ok");
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
/**
* 失败
*
* @param code
* @param message
* @return
*/
public static BaseResponse error(int code, String message) {
return new BaseResponse(code, null, message);
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String message) {
return new BaseResponse(errorCode.getCode(), null, message);
}
}

View File

@@ -0,0 +1,89 @@
package cn.yinlihupo.controller.oss;
import cn.yinlihupo.common.result.Result;
import cn.yinlihupo.service.oss.OssService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* OSS文件控制器
* 提供文件上传、下载和管理功能
*/
@Slf4j
@RestController
@RequestMapping("/oss")
@RequiredArgsConstructor
public class OssController {
private final OssService ossService;
/**
* 上传文件到OSS返回文件URL
*
* @param file 文件
* @return 文件URL
*/
@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file) {
log.info("收到文件上传请求, 文件名: {}", file.getOriginalFilename());
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
try {
String fileUrl = ossService.uploadFile(file, file.getOriginalFilename());
return Result.success("文件上传成功", fileUrl);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
return Result.error("文件上传失败: " + e.getMessage());
}
}
/**
* 上传文件到指定存储桶
*
* @param file 文件
* @param bucketName 存储桶名称
* @return 文件URL
*/
@PostMapping("/upload/{bucketName}")
public Result<String> uploadFileToBucket(
@RequestParam("file") MultipartFile file,
@PathVariable String bucketName) {
log.info("收到文件上传请求, 文件名: {}, 存储桶: {}", file.getOriginalFilename(), bucketName);
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
try {
String fileUrl = ossService.uploadFile(file, file.getOriginalFilename(), bucketName);
return Result.success("文件上传成功", fileUrl);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
return Result.error("文件上传失败: " + e.getMessage());
}
}
/**
* 删除文件
*
* @param fileUrl 文件URL
* @return 操作结果
*/
@DeleteMapping("/delete")
public Result<Void> deleteFile(@RequestParam("fileUrl") String fileUrl) {
log.info("收到文件删除请求, fileUrl: {}", fileUrl);
try {
ossService.deleteFile(fileUrl);
return Result.success("文件删除成功", null);
} catch (Exception e) {
log.error("文件删除失败: {}", e.getMessage(), e);
return Result.error("文件删除失败: " + e.getMessage());
}
}
}

View File

@@ -1,10 +1,10 @@
package cn.yinlihupo.controller; 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.dto.ProjectInitRequest;
import cn.yinlihupo.domain.dto.ProjectInitResult; import cn.yinlihupo.domain.dto.ProjectInitResult;
import cn.yinlihupo.service.ai.MinioFileService; import cn.yinlihupo.service.oss.OssService;
import cn.yinlihupo.service.ai.ProjectInitService; 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.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -16,12 +16,12 @@ import org.springframework.web.multipart.MultipartFile;
*/ */
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/api/v1/project-init") @RequestMapping("/project-init")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ProjectInitController { public class ProjectController {
private final ProjectInitService projectInitService; private final ProjectService projectService;
private final MinioFileService minioFileService; private final OssService ossService;
/** /**
* 根据文本内容生成项目初始化数据 * 根据文本内容生成项目初始化数据
@@ -38,7 +38,7 @@ public class ProjectInitController {
} }
try { try {
ProjectInitResult result = projectInitService.generateProjectFromContent(request.getContent()); ProjectInitResult result = projectService.generateProjectFromContent(request.getContent());
return Result.success("项目初始化成功", result); return Result.success("项目初始化成功", result);
} catch (Exception e) { } catch (Exception e) {
log.error("项目初始化失败: {}", e.getMessage(), e); log.error("项目初始化失败: {}", e.getMessage(), e);
@@ -61,13 +61,13 @@ public class ProjectInitController {
} }
try { try {
// 上传文件到MinIO // 上传文件到OSS
String fileUrl = minioFileService.uploadFile(file, file.getOriginalFilename()); String fileUrl = ossService.uploadFile(file, file.getOriginalFilename());
log.info("文件上传成功URL: {}", fileUrl); log.info("文件上传成功URL: {}", fileUrl);
// 根据文件生成项目初始化数据 // 根据文件生成项目初始化数据
String fileType = getFileExtension(file.getOriginalFilename()); String fileType = getFileExtension(file.getOriginalFilename());
ProjectInitResult result = projectInitService.generateProjectFromFile(fileUrl, fileType); ProjectInitResult result = projectService.generateProjectFromFile(fileUrl, fileType);
return Result.success("项目初始化成功", result); return Result.success("项目初始化成功", result);
} catch (Exception e) { } catch (Exception e) {
@@ -76,55 +76,6 @@ public class ProjectInitController {
} }
} }
/**
* 根据已上传的文件URL生成项目初始化数据
*
* @param request 包含文件URL的请求
* @return 项目初始化结构化数据
*/
@PostMapping("/from-url")
public Result<ProjectInitResult> generateFromUrl(@RequestBody ProjectInitRequest request) {
log.info("收到项目初始化请求文件URL");
if (request.getFileUrl() == null || request.getFileUrl().trim().isEmpty()) {
return Result.error("文件URL不能为空");
}
try {
ProjectInitResult result = projectInitService.generateProjectFromFile(
request.getFileUrl(),
request.getFileType()
);
return Result.success("项目初始化成功", result);
} catch (Exception e) {
log.error("项目初始化失败: {}", e.getMessage(), e);
return Result.error("项目初始化失败: " + e.getMessage());
}
}
/**
* 仅上传文件到MinIO返回文件URL
*
* @param file 文件
* @return 文件URL
*/
@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file) {
log.info("收到文件上传请求, 文件名: {}", file.getOriginalFilename());
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
try {
String fileUrl = minioFileService.uploadFile(file, file.getOriginalFilename());
return Result.success("文件上传成功", fileUrl);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
return Result.error("文件上传失败: " + e.getMessage());
}
}
/** /**
* 获取文件扩展名 * 获取文件扩展名
* *

View File

@@ -1,17 +1,17 @@
package cn.yinlihupo.service.ai; package cn.yinlihupo.service.oss;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream; import java.io.InputStream;
/** /**
* MinIO文件服务接口 * OSS文件服务接口
* 用于文件上传下载和管理 * 用于文件上传下载和管理
*/ */
public interface MinioFileService { public interface OssService {
/** /**
* 上传文件到MinIO * 上传文件到OSS
* *
* @param file 文件 * @param file 文件
* @param fileName 文件名 * @param fileName 文件名
@@ -20,7 +20,7 @@ public interface MinioFileService {
String uploadFile(MultipartFile file, String fileName); String uploadFile(MultipartFile file, String fileName);
/** /**
* 上传文件到MinIO指定存储桶 * 上传文件到OSS指定存储桶
* *
* @param file 文件 * @param file 文件
* @param fileName 文件名 * @param fileName 文件名

View File

@@ -1,10 +1,9 @@
package cn.yinlihupo.service.ai.impl; package cn.yinlihupo.service.oss.impl;
import cn.yinlihupo.common.config.MinioConfig; import cn.yinlihupo.common.config.MinioConfig;
import cn.yinlihupo.service.ai.MinioFileService; import cn.yinlihupo.service.oss.OssService;
import io.minio.*; import io.minio.*;
import io.minio.errors.*; import io.minio.errors.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -13,18 +12,15 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.*; import java.io.*;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** /**
* MinIO文件服务实现类 * OSS文件服务实现类基于MinIO
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class MinioFileServiceImpl implements MinioFileService { public class OssServiceImpl implements OssService {
private final MinioClient minioClient; private final MinioClient minioClient;
private final MinioConfig minioConfig; private final MinioConfig minioConfig;

View File

@@ -1,4 +1,4 @@
package cn.yinlihupo.service.ai; package cn.yinlihupo.service.project;
import cn.yinlihupo.domain.dto.ProjectInitResult; import cn.yinlihupo.domain.dto.ProjectInitResult;
@@ -6,7 +6,7 @@ import cn.yinlihupo.domain.dto.ProjectInitResult;
* AI项目初始化服务接口 * AI项目初始化服务接口
* 使用Spring AI结构化输出能力从项目文档中提取结构化信息 * 使用Spring AI结构化输出能力从项目文档中提取结构化信息
*/ */
public interface ProjectInitService { public interface ProjectService {
/** /**
* 根据项目资料内容生成项目初始化结构化数据 * 根据项目资料内容生成项目初始化结构化数据
@@ -17,9 +17,9 @@ public interface ProjectInitService {
ProjectInitResult generateProjectFromContent(String content); ProjectInitResult generateProjectFromContent(String content);
/** /**
* 根据MinIO文件URL生成项目初始化结构化数据 * 根据OSS文件URL生成项目初始化结构化数据
* *
* @param fileUrl MinIO文件URL * @param fileUrl OSS文件URL
* @param fileType 文件类型 * @param fileType 文件类型
* @return 项目初始化结果 * @return 项目初始化结果
*/ */

View File

@@ -1,13 +1,12 @@
package cn.yinlihupo.service.ai.impl; package cn.yinlihupo.service.project.impl;
import cn.yinlihupo.domain.dto.ProjectInitResult; import cn.yinlihupo.domain.dto.ProjectInitResult;
import cn.yinlihupo.service.ai.ProjectInitService; import cn.yinlihupo.service.oss.OssService;
import cn.yinlihupo.service.ai.MinioFileService; 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.prompt.PromptTemplate;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@@ -17,10 +16,10 @@ import org.springframework.stereotype.Service;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ProjectInitServiceImpl implements ProjectInitService { public class ProjectServiceImpl implements ProjectService {
private final ChatClient chatClient; private final ChatClient chatClient;
private final MinioFileService minioFileService; private final OssService ossService;
/** /**
* 项目初始化系统提示词模板 * 项目初始化系统提示词模板
@@ -149,7 +148,7 @@ public class ProjectInitServiceImpl implements ProjectInitService {
log.info("开始根据内容生成项目初始化数据"); log.info("开始根据内容生成项目初始化数据");
PromptTemplate promptTemplate = new PromptTemplate(USER_PROMPT_TEMPLATE); PromptTemplate promptTemplate = new PromptTemplate(USER_PROMPT_TEMPLATE);
String userPrompt = promptTemplate.createMessage(java.util.Map.of("content", content)).getContent(); String userPrompt = promptTemplate.createMessage(java.util.Map.of("content", content)).toString();
return chatClient.prompt() return chatClient.prompt()
.system(PROJECT_INIT_SYSTEM_PROMPT) .system(PROJECT_INIT_SYSTEM_PROMPT)
@@ -162,8 +161,8 @@ public class ProjectInitServiceImpl implements ProjectInitService {
public ProjectInitResult generateProjectFromFile(String fileUrl, String fileType) { public ProjectInitResult generateProjectFromFile(String fileUrl, String fileType) {
log.info("开始根据文件生成项目初始化数据, fileUrl: {}, fileType: {}", fileUrl, fileType); log.info("开始根据文件生成项目初始化数据, fileUrl: {}, fileType: {}", fileUrl, fileType);
// MinIO下载文件内容 // OSS下载文件内容
String content = minioFileService.readFileAsString(fileUrl); String content = ossService.readFileAsString(fileUrl);
if (content == null || content.isEmpty()) { if (content == null || content.isEmpty()) {
throw new RuntimeException("无法读取文件内容: " + fileUrl); throw new RuntimeException("无法读取文件内容: " + fileUrl);

View File

@@ -6,9 +6,9 @@ spring:
# PostgreSQL 数据库配置 # PostgreSQL 数据库配置
datasource: datasource:
url: jdbc:postgresql://localhost:5432/ylhp_ai_project_manager url: jdbc:postgresql://10.200.8.25:5432/ylhp_ai_project_manager
username: ${DB_USERNAME:postgres} username: postgres
password: ${DB_PASSWORD:postgres} password: postgres
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
hikari: hikari:
maximum-pool-size: 10 maximum-pool-size: 10
@@ -33,19 +33,19 @@ spring:
# Spring AI 配置 # Spring AI 配置
ai: ai:
openai: openai:
api-key: ${OPENAI_API_KEY:your-api-key} api-key: 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
base-url: ${OPENAI_BASE_URL:https://api.openai.com} base-url: https://sg1.proxy.yinlihupo.cc/proxy/https://openrouter.ai/api/v1
chat: chat:
options: options:
model: ${OPENAI_MODEL:gpt-4o} model: gpt-4o
temperature: 0.3 temperature: 0.3
# MinIO 对象存储配置 # MinIO 对象存储配置
minio: minio:
endpoint: ${MINIO_ENDPOINT:http://localhost:9000} endpoint: 10.200.8.25:9000
access-key: ${MINIO_ACCESS_KEY:minioadmin} access-key: minioadmin
secret-key: ${MINIO_SECRET_KEY:minioadmin} secret-key: minioadmin
bucket-name: ${MINIO_BUCKET_NAME:ylhp-files} bucket-name: ylhp-files
# 日志配置 # 日志配置
logging: logging:

View File

@@ -6,4 +6,6 @@ spring:
# 公共配置 # 公共配置
server: server:
port: 8080 port: 8080
servlet:
context-path: /api/v1