基于Spring Boot与Vue的高校社团管理平台架构设计与实现
项目概述
随着高校信息化建设的深入,传统的人工社团管理模式已无法满足日益增长的数据处理需求。信息孤岛、数据冗余以及审批流程繁琐等问题凸显。为此,设计并实现一套现代化的社团管理平台至关重要。该平台旨在通过数字化手段重塑社团管理流程,实现学生、团长与管理员之间的高效协同,提升信息流转效率与数据安全性。
技术栈选型
本系统采用前后端分离的架构设计,以确保系统的高内聚、低耦合及良好的可扩展性。
- 后端框架:选用 Spring Boot。其"约定优于配置"的核心理念大幅简化了 Spring 应用的初始搭建与开发过程。通过自动配置和内嵌服务器,开发者可专注于业务逻辑的实现。同时,结合 MyBatis-Plus 作为持久层框架,提升数据库操作效率。
- 前端框架:采用 Vue.js 配合 Element UI 构建单页面应用(SPA)。Vue 的组件化开发模式和响应式数据绑定机制,使得复杂交互界面的开发更加高效,同时保证了用户体验的流畅性。
- 数据库:选用 MySQL 关系型数据库。其体积小、速度快、开源免费的特点,非常适合中小型Web应用的数据存储需求,且能完美支持复杂的关联查询与事务处理。
系统需求与业务流程
在系统开发前期,对核心业务流程进行了梳理与建模,确保系统逻辑严密且符合用户操作直觉。
核心操作流程
系统采用基于 Token 的身份认证机制。用户访问受保护资源时,需先通过登录接口获取凭证,后续请求携带该凭证进行鉴权。
登录鉴权流程
登录模块负责校验用户身份。系统接收客户端提交的凭证,与数据库中的哈希密码进行比对,校验通过后生成 JWT Token 并返回。
数据生命周期管理
系统对核心数据(如社团信息、活动申请)实行严格的增删改查及逻辑删除机制,确保数据的可追溯性与安全性。
数据库模型设计
数据库设计遵循第三范式,减少数据冗余。以下为核心实体的 E-R 模型及物理表结构设计。
核心实体关系
物理表结构
| 字段名 | 数据类型 | 约束 | 描述 |
|---|---|---|---|
| id | BIGINT | PK, Auto Increment | 主键ID |
| account | VARCHAR(50) | Unique, Not Null | 登录账号 |
| credential | VARCHAR(100) | Not Null | 加密密码 |
| role_code | VARCHAR(20) | Not Null | 角色标识 |
| created_at | DATETIME | Default CURRENT_TIMESTAMP | 创建时间 |
| 字段名 | 数据类型 | 约束 | 描述 |
|---|---|---|---|
| id | BIGINT | PK, Auto Increment | 主键ID |
| club_code | VARCHAR(32) | Unique | 社团编号 |
| club_name | VARCHAR(100) | Not Null | 社团名称 |
| category_id | INT | FK | 社团分类ID |
| leader_id | BIGINT | FK | 团长用户ID |
| is_deleted | TINYINT | Default 0 | 逻辑删除标记 |
核心模块代码实现
以下展示系统核心模块的后端接口与前端组件实现。代码经过重构,采用了更现代的命名规范与设计模式。
1. 身份认证控制器 (AuthController)
负责处理用户的登录、注册及会话管理,采用 RESTful 风格设计。
package com.clubmanage.controller;
import com.clubmanage.common.ApiResponse;
import com.clubmanage.dto.LoginRequest;
import com.clubmanage.entity.SysUser;
import com.clubmanage.service.AccountService;
import com.clubmanage.util.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AccountService accountService;
@Autowired
private JwtTokenProvider tokenProvider;
@PostMapping("/login")
public ApiResponse<?> authenticateUser(@RequestBody LoginRequest request) {
SysUser user = accountService.findByAccount(request.getAccount());
if (user == null || !accountService.verifyPassword(request.getCredential(), user.getCredential())) {
return ApiResponse.error(401, "账号或密码错误");
}
String token = tokenProvider.generateToken(user.getId(), user.getAccount(), user.getRoleCode());
return ApiResponse.success("登录成功")
.put("accessToken", token)
.put("role", user.getRoleCode());
}
@PostMapping("/register")
public ApiResponse<?> registerUser(@RequestBody SysUser newUser) {
if (accountService.existsByAccount(newUser.getAccount())) {
return ApiResponse.error(400, "该账号已被注册");
}
accountService.createUser(newUser);
return ApiResponse.success("注册成功");
}
@PostMapping("/logout")
public ApiResponse<?> invalidateSession(@RequestHeader("Authorization") String token) {
tokenProvider.blacklistToken(token);
return ApiResponse.success("已安全退出");
}
}
2. Web MVC 拦截器配置 (WebMvcConfig)
通过实现 WebMvcConfigurer 接口,优雅地注册自定义拦截器并配置静态资源映射,避免覆盖 Spring Boot 的默认自动配置。
package com.clubmanage.config;
import com.clubmanage.interceptor.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/v1/auth/**", "/static/**", "/error");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.addResourceLocations("classpath:/public/uploads/");
}
}
3. 公告管理业务控制器 (AnnouncementController)
使用 MyBatis-Plus 的 LambdaQueryWrapper 构建类型安全的查询条件,实现公告的分页查询、详情获取及逻辑删除。
package com.clubmanage.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.clubmanage.common.ApiResponse;
import com.clubmanage.entity.Announcement;
import com.clubmanage.service.AnnouncementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/announcements")
public class AnnouncementController {
@Autowired
private AnnouncementService announcementService;
@GetMapping("/page")
public ApiResponse<Page<Announcement>> getPageData(@RequestParam Map<String, Object> params) {
int current = Integer.parseInt(params.getOrDefault("page", "1").toString());
int size = Integer.parseInt(params.getOrDefault("limit", "10").toString());
LambdaQueryWrapper<Announcement> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Announcement::getIsDeleted, 0);
if (params.containsKey("title")) {
wrapper.like(Announcement::getTitle, params.get("title"));
}
wrapper.orderByDesc(Announcement::getCreatedAt);
Page<Announcement> pageResult = announcementService.page(new Page<>(current, size), wrapper);
return ApiResponse.success(pageResult);
}
@PostMapping
public ApiResponse<?> createAnnouncement(@RequestBody Announcement announcement) {
announcement.setCreatedAt(new Date());
announcement.setIsDeleted(0);
announcementService.save(announcement);
return ApiResponse.success("发布成功");
}
@DeleteMapping("/{id}")
public ApiResponse<?> removeAnnouncement(@PathVariable Long id) {
Announcement entity = new Announcement();
entity.setId(id);
entity.setIsDeleted(1); // 逻辑删除
announcementService.updateById(entity);
return ApiResponse.success("删除成功");
}
}
4. 前端递归菜单组件 (RecursiveMenu.vue)
采用 Vue 3 Composition API 重构侧边栏菜单组件,利用递归调用实现无限层级的动态路由菜单渲染。
<template>
<el-sub-menu v-if="node.children && node.children.length" :index="String(node.id)">
<template #title>
<el-icon v-if="node.icon"><component :is="node.icon" /></el-icon>
<span>{{ node.label }}</span>
</template>
<recursive-menu
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</el-sub-menu>
<el-menu-item v-else :index="String(node.id)" @click="handleRouteJump(node)">
<el-icon v-if="node.icon"><component :is="node.icon" /></el-icon>
<span>{{ node.label }}</span>
</el-menu-item>
</template>
<script setup>
import { useRouter } from 'vue-router';
const props = defineProps({
node: {
type: Object,
required: true
}
});
const router = useRouter();
const handleRouteJump = (menuNode) => {
if (menuNode.routePath) {
router.push({ path: menuNode.routePath });
}
};
</script>
<style scoped>
/* 菜单样式微调 */
</style>