这套基于SpringBoot2+Vue3的企业项目管理系统采用了典型的前后端分离架构,这种设计模式在当前企业级应用开发中已经成为主流选择。前后端分离的核心优势在于解耦了展示层与业务逻辑层,让前后端开发团队能够并行工作,大幅提升开发效率。
后端选择SpringBoot2框架主要基于以下几个实际考量:
前端选用Vue3则是因为:
数据库选用MySQL8.0主要看中其:
系统采用分层架构设计,各层职责明确:
code复制客户端层
├─ Web前端 (Vue3 + Element Plus)
├─ 移动端 (预留接口)
└─ 第三方系统 (API对接)
应用服务层
├─ 认证服务 (JWT)
├─ 业务逻辑服务
└─ 文件服务
数据持久层
├─ MyBatis-Plus
├─ Redis缓存
└─ MySQL8.0
基础设施层
├─ 阿里云OSS
├─ 消息队列
└─ 监控系统
这种分层设计使得系统各模块耦合度低,便于后期扩展和维护。特别是在需要添加新功能模块时,只需在相应层级进行扩展,不会影响其他部分。
用户权限管理是企业系统的核心模块,我们实现了基于RBAC(基于角色的访问控制)模型的权限系统。数据库设计中,user_role字段定义了基本的角色区分:
java复制// 角色枚举定义
public enum Role {
ADMIN(1, "管理员"),
USER(2, "普通用户");
private final int code;
private final String desc;
// 构造方法等...
}
在Spring Security的配置中,我们通过注解方式控制接口访问权限:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/project/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
}
}
权限控制的几个关键实现细节:
项目管理模块的数据表设计考虑了企业实际需求:
sql复制CREATE TABLE `project` (
`project_id` bigint NOT NULL AUTO_INCREMENT,
`project_name` varchar(100) NOT NULL,
`project_desc` text,
`project_status` int NOT NULL DEFAULT '1',
`start_time` datetime DEFAULT NULL,
`end_time` datetime DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`project_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在业务逻辑实现上,有几个值得注意的设计:
java复制public class ProjectStateMachine {
private static final Map<Integer, List<Integer>> TRANSITIONS = Map.of(
1, List.of(2), // 未开始 → 进行中
2, List.of(3,4), // 进行中 → 已完成/已暂停
3, List.of(), // 已完成 → 无
4, List.of(2) // 已暂停 → 进行中
);
public static boolean canTransition(int from, int to) {
return TRANSITIONS.getOrDefault(from, List.of()).contains(to);
}
}
java复制public void checkProjectTimeConflict(LocalDateTime start, LocalDateTime end) {
long overlappingProjects = projectMapper.selectCount(new LambdaQueryWrapper<Project>()
.le(Project::getStartTime, end)
.ge(Project::getEndTime, start)
.ne(Project::getProjectStatus, 3)); // 排除已完成项目
if (overlappingProjects > 0) {
throw new BusinessException("时间范围内存在其他进行中的项目");
}
}
任务管理模块实现了完整的CRUD操作,其中任务分配算法值得详细说明。我们采用基于负载均衡的任务分配策略:
java复制public Long assignTask(Task task) {
// 获取项目成员及其当前任务数
List<MemberTaskCount> members = userMapper.selectMembersWithTaskCount(task.getProjectId());
if (members.isEmpty()) {
throw new BusinessException("项目中没有可分配成员");
}
// 按任务数升序排序,选择任务最少的成员
members.sort(Comparator.comparingInt(MemberTaskCount::getTaskCount));
Long assigneeId = members.get(0).getUserId();
task.setAssigneeId(assigneeId);
taskMapper.insert(task);
return assigneeId;
}
任务进度更新时,会自动计算项目整体进度:
java复制@Transactional
public void updateTaskProgress(Long taskId, int progress) {
Task task = taskMapper.selectById(taskId);
task.setProgress(progress);
taskMapper.updateById(task);
// 重新计算项目进度
Project project = projectMapper.selectById(task.getProjectId());
List<Task> tasks = taskMapper.selectByProjectId(project.getProjectId());
double avgProgress = tasks.stream()
.mapToInt(Task::getProgress)
.average()
.orElse(0);
project.setProgress((int) Math.round(avgProgress));
projectMapper.updateById(project);
}
系统采用RESTful API设计规范,所有接口返回统一格式的JSON数据。我们定义了通用的响应封装类:
java复制public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String msg;
private T data;
public static <T> R<T> ok() {
return restResult(null, 200, "操作成功");
}
public static <T> R<T> ok(T data) {
return restResult(data, 200, "操作成功");
}
public static <T> R<T> error(String msg) {
return restResult(null, 500, msg);
}
// 其他工具方法...
}
前端使用axios进行API调用,并配置了统一的请求/响应拦截器:
javascript复制const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
config => {
const token = store.getters.token
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
Message.error(res.msg || 'Error')
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res
}
},
error => {
Message.error(error.message)
return Promise.reject(error)
}
)
系统实现了通用的文件上传服务,支持本地存储和阿里云OSS两种方式。通过策略模式实现存储方式的灵活切换:
java复制public interface FileStorageStrategy {
String upload(MultipartFile file, String path);
InputStream download(String filePath);
}
@Service
@RequiredArgsConstructor
public class FileService {
private final FileStorageStrategy fileStorageStrategy;
public String uploadFile(MultipartFile file, String path) {
String originalFilename = file.getOriginalFilename();
String fileExt = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID() + fileExt;
return fileStorageStrategy.upload(file, path + "/" + fileName);
}
// 其他方法...
}
// OSS实现示例
@Service
@Profile("prod")
public class OssStorageStrategy implements FileStorageStrategy {
private final OSS ossClient;
private final String bucketName;
@Override
public String upload(MultipartFile file, String path) {
try {
ossClient.putObject(bucketName, path, file.getInputStream());
return path;
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
}
报表导出功能使用Apache POI实现Excel导出:
java复制public void exportProjectReport(Long projectId, HttpServletResponse response) {
Project project = projectMapper.selectById(projectId);
List<Task> tasks = taskMapper.selectByProjectId(projectId);
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("项目报表");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("任务名称");
headerRow.createCell(1).setCellValue("负责人");
headerRow.createCell(2).setCellValue("进度");
// 填充数据
for (int i = 0; i < tasks.size(); i++) {
Row row = sheet.createRow(i + 1);
Task task = tasks.get(i);
row.createCell(0).setCellValue(task.getTaskName());
row.createCell(1).setCellValue(task.getAssigneeName());
row.createCell(2).setCellValue(task.getProgress() + "%");
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=project_report.xlsx");
workbook.write(response.getOutputStream());
} catch (IOException e) {
throw new RuntimeException("导出失败", e);
}
}
系统支持多种部署环境,通过Spring Profile实现配置隔离:
yaml复制# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/pm_dev?useSSL=false
username: devuser
password: devpass
redis:
host: localhost
port: 6379
# application-prod.yml
server:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-db:3306/pm_prod?useSSL=true
username: ${DB_USER}
password: ${DB_PASS}
redis:
host: redis-cluster
port: 6379
部署时通过激活不同的Profile来切换配置:
bash复制# 开发环境
java -jar project-management.jar --spring.profiles.active=dev
# 生产环境
java -jar project-management.jar --spring.profiles.active=prod
java复制@Data
@TableName("task")
public class Task {
@TableId(type = IdType.AUTO)
private Long taskId;
private String taskName;
@TableField(exist = false)
private String assigneeName;
}
// 查询示例
public List<Task> getTasksWithAssignee(Long projectId) {
return taskMapper.selectList(new QueryWrapper<Task>()
.eq("project_id", projectId)
.select("t.*, u.user_name as assignee_name")
.apply("left join user u on t.assignee_id = u.user_id"));
}
java复制@Cacheable(value = "user", key = "#userId", unless = "#result == null")
public User getUserWithCache(Long userId) {
return userMapper.selectById(userId);
}
@CacheEvict(value = "user", key = "#user.userId")
public void updateUser(User user) {
userMapper.updateById(user);
}
javascript复制const routes = [
{
path: '/projects',
component: () => import('../views/ProjectList.vue')
},
{
path: '/tasks',
component: () => import('../views/TaskBoard.vue')
}
]
系统集成了Spring Boot Actuator提供健康检查:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
日志系统采用Logback,按天归档并区分日志级别:
xml复制<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</configuration>
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
java复制@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
javascript复制// axios响应拦截器中添加token刷新逻辑
service.interceptors.response.use(
response => {
// ...
},
error => {
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
return store.dispatch('refreshToken').then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token
return service(originalRequest)
})
}
return Promise.reject(error)
}
)
yaml复制spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
javascript复制// 前端分片上传实现
async function uploadFile(file) {
const chunkSize = 5 * 1024 * 1024 // 5MB
const chunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkNumber', i + 1)
formData.append('totalChunks', chunks)
formData.append('originalFilename', file.name)
await axios.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
}
java复制public interface NotificationService {
void send(Notification notification);
}
@Service
@RequiredArgsConstructor
public class NotificationDispatcher {
private final List<NotificationService> services;
public void dispatch(Notification notification) {
services.forEach(service -> service.send(notification));
}
}
在实际开发过程中,有几个关键点值得特别注意:
java复制@Transactional
@OperationLog(module = "项目", type = "创建")
public Project createProject(Project project) {
// 业务逻辑
}
typescript复制// stores/project.ts
export const useProjectStore = defineStore('project', {
state: () => ({
projects: [] as Project[],
currentProject: null as Project | null
}),
actions: {
async fetchProjects() {
this.projects = await api.getProjects()
}
}
})
这套企业项目管理系统经过多个实际项目的验证,架构合理、扩展性强,能够满足大多数企业的项目管理需求。特别是在任务分配算法、进度自动计算等业务逻辑的实现上,经过了多次优化,具有较高的实用价值。