1. 项目背景与核心需求
在高校教育体系中,导师选择是研究生培养的关键环节。传统的人工分配方式存在信息不对称、流程不透明、沟通效率低下等问题。我曾参与过某高校研究生院的调研,发现超过60%的学生在导师双选过程中遇到过匹配不精准的问题,而导师们也普遍反映难以全面了解学生的科研意向。
这个基于SpringBoot+Vue的导师选择管理系统,正是为了解决以下核心痛点:
- 双向选择流程数字化:将纸质申请转为线上流程,支持学生提交/修改/撤销申请,导师实时查看和审批
- 师生交流场景全覆盖:除正式申请流程外,提供非正式的交流空间(类似群聊功能)
- 状态追踪可视化:所有申请状态的实时更新与通知机制
- 权限隔离与数据安全:不同角色(学生/导师/管理员)的权限控制体系
2. 技术架构设计解析
2.1 整体技术栈选型
后端技术栈:
- Spring Boot 2.7 + MyBatis-Plus 3.5 + Lombok
- 认证方案:JWT + 自定义Token验证
- 数据库:MySQL 8.0(需注意字符集设置为utf8mb4以支持emoji)
前端技术栈:
- Vue 3 + Element Plus + Axios
- WebSocket协议实现实时消息推送
- 采用Pinia进行状态管理
技术选型考量:Spring Boot的自动配置特性大幅减少了XML配置,与Vue的前后端分离架构契合现代Web开发趋势。MyBatis-Plus在传统ORM基础上增强了代码生成和条件构造器功能,特别适合快速迭代的管理系统开发。
2.2 系统分层架构
code复制└── com.example.mentor
├── config # 配置类(跨域/拦截器等)
├── controller # 请求入口
├── entity # 数据库实体
├── interceptor # 自定义拦截器
├── mapper # MyBatis接口
├── service # 业务逻辑
├── util # 工具类
└── vo # 视图对象
关键设计原则:
- 前后端分离:通过RESTful API交互,接口文档使用Swagger生成
- 模块化开发:按功能划分package,如
mentor、application、message等 - 统一响应格式:所有接口返回
R对象(含code/message/data)
3. 核心功能实现细节
3.1 师生双选流程实现
数据库设计关键表:
sql复制CREATE TABLE `t_application` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`student_id` BIGINT NOT NULL,
`mentor_id` BIGINT NOT NULL,
`status` TINYINT DEFAULT 0 COMMENT '0-待处理 1-已接受 2-已拒绝',
`application_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`response_time` DATETIME,
FOREIGN KEY (`student_id`) REFERENCES `t_student`(`id`),
FOREIGN KEY (`mentor_id`) REFERENCES `t_mentor`(`id`)
);
状态机设计:
java复制public enum ApplicationStatus {
PENDING(0, "待处理"),
ACCEPTED(1, "已接受"),
REJECTED(2, "已拒绝");
// 省略getter/setter
}
核心业务逻辑:
java复制@Transactional
public R submitApplication(ApplicationVO vo) {
// 校验是否已存在申请
LambdaQueryWrapper<Application> wrapper = Wrappers.lambdaQuery();
wrapper.eq(Application::getStudentId, vo.getStudentId())
.eq(Application::getMentorId, vo.getMentorId())
.in(Application::getStatus,
ApplicationStatus.PENDING.getCode(),
ApplicationStatus.ACCEPTED.getCode());
if (applicationService.count(wrapper) > 0) {
return R.error("已存在待处理或已接受的申请");
}
// 创建新申请
Application entity = new Application();
BeanUtils.copyProperties(vo, entity);
entity.setStatus(ApplicationStatus.PENDING.getCode());
applicationService.save(entity);
// 发送系统通知
messageService.sendSystemMsg(
vo.getMentorId(),
"您有新的指导申请待处理",
MessageType.APPLICATION_NOTICE);
return R.ok();
}
3.2 实时交流模块实现
WebSocket配置:
java复制@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler(), "/ws/chat")
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
@Bean
public WebSocketHandler chatHandler() {
return new MentorChatHandler();
}
}
消息存储设计:
sql复制CREATE TABLE `t_message` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`sender_id` BIGINT NOT NULL,
`receiver_id` BIGINT,
`group_id` BIGINT COMMENT '群组ID',
`content` TEXT NOT NULL,
`msg_type` TINYINT DEFAULT 1 COMMENT '1-文本 2-图片 3-文件',
`send_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`is_read` BOOLEAN DEFAULT false
);
前端消息处理:
javascript复制// 建立WebSocket连接
const socket = new WebSocket(`ws://${location.host}/ws/chat`);
// 接收消息
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'CHAT') {
chatStore.addMessage(msg);
} else if (msg.type === 'SYSTEM') {
notificationStore.addNotice(msg);
}
};
// 发送消息
function sendMessage(content) {
const message = {
senderId: userStore.id,
receiverId: currentChat.value.id,
content: content,
type: 'CHAT'
};
socket.send(JSON.stringify(message));
}
4. 关键问题解决方案
4.1 并发申请冲突处理
问题场景:当多个学生同时申请热门导师时,可能出现超配额情况
解决方案:
- 数据库添加version字段实现乐观锁
- 使用Redis分布式锁控制关键操作
- 定时任务检查导师配额
java复制public R acceptApplication(Long applicationId) {
// 获取分布式锁
String lockKey = "mentor:accept:" + applicationId;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
return R.error("操作过于频繁,请稍后重试");
}
try {
// 查询申请详情
Application app = applicationService.getById(applicationId);
// 检查导师剩余名额
Long acceptedCount = applicationService.count(
Wrappers.<Application>lambdaQuery()
.eq(Application::getMentorId, app.getMentorId())
.eq(Application::getStatus, ApplicationStatus.ACCEPTED.getCode())
);
Mentor mentor = mentorService.getById(app.getMentorId());
if (acceptedCount >= mentor.getQuota()) {
return R.error("导师接收名额已满");
}
// 更新申请状态
app.setStatus(ApplicationStatus.ACCEPTED.getCode());
app.setResponseTime(new Date());
applicationService.updateById(app);
// 发送通知
messageService.sendSystemMsg(
app.getStudentId(),
"您的导师申请已被接受",
MessageType.APPLICATION_RESULT);
return R.ok();
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
4.2 消息实时性与一致性保障
技术方案:
- 客户端维护消息ID和本地存储
- 服务端采用消息队列削峰填谷
- 离线消息通过轮询补发
java复制// 消息发送服务
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendMessage(Message message) {
// 持久化到数据库
messageMapper.insert(message);
// 投递到消息队列
rabbitTemplate.convertAndSend(
"mentor.message.exchange",
"message.route",
message
);
}
@RabbitListener(queues = "mentor.message.queue")
public void handleMessage(Message message) {
// 查询接收者在线状态
boolean isOnline = webSocketSessionManager
.isUserOnline(message.getReceiverId());
if (isOnline) {
// 实时推送
webSocketSessionManager.sendMessage(
message.getReceiverId(),
JSON.toJSONString(message)
);
} else {
// 存入待推送队列
redisTemplate.opsForList().rightPush(
"offline:msg:" + message.getReceiverId(),
message.getId()
);
}
}
}
5. 系统安全设计
5.1 认证与授权体系
JWT增强方案:
- 自定义Token实体存储用户角色信息
- 双Token机制(accessToken + refreshToken)
- 接口级权限控制
java复制// 增强的Token拦截器
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 跨域预检请求直接放行
if ("OPTIONS".equals(request.getMethod())) {
return true;
}
// 检查注解权限
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
// 公开接口放行
if (hm.hasMethodAnnotation(PublicApi.class)) {
return true;
}
// 角色校验
RequiredRoles roles = hm.getMethodAnnotation(RequiredRoles.class);
if (roles != null) {
String userRole = (String) request.getAttribute("role");
if (!Arrays.asList(roles.value()).contains(userRole)) {
throw new BusinessException(403, "权限不足");
}
}
}
// Token验证
String token = request.getHeader("Authorization");
TokenEntity tokenEntity = tokenService.verifyToken(token);
request.setAttribute("currentUser", tokenEntity);
return true;
}
}
5.2 数据安全措施
- 敏感数据加密:
- 密码使用BCrypt加密存储
- 敏感字段数据库加密(如手机号)
java复制// 数据加密处理器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据加密
interceptor.addInnerInterceptor(new DataEncryptInterceptor(
Arrays.asList("phone", "email"),
new AesEncryptor(env.getProperty("db.encrypt.key"))
));
return interceptor;
}
- 审计日志:
sql复制CREATE TABLE `t_operation_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`operation` VARCHAR(50) NOT NULL,
`method` VARCHAR(100),
`params` TEXT,
`ip` VARCHAR(50),
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
6. 性能优化实践
6.1 数据库优化
索引设计:
sql复制-- 申请状态查询索引
ALTER TABLE `t_application`
ADD INDEX `idx_mentor_status` (`mentor_id`, `status`);
-- 消息查询索引
ALTER TABLE `t_message`
ADD INDEX `idx_receiver_read` (`receiver_id`, `is_read`);
查询优化示例:
java复制// 优化前的N+1查询
List<Application> apps = applicationService.list(
Wrappers.<Application>query()
.eq("mentor_id", mentorId)
);
apps.forEach(app -> {
Student s = studentService.getById(app.getStudentId());
app.setStudentName(s.getName());
});
// 优化后的联表查询
List<ApplicationVO> apps = applicationMapper.selectApplicationsWithStudents(
Wrappers.<Application>query()
.eq("a.mentor_id", mentorId)
);
6.2 缓存策略
多级缓存设计:
- 本地Caffeine缓存高频访问数据(如用户基本信息)
- Redis缓存业务数据(如导师列表)
- 数据库作为最终存储
java复制// 缓存配置示例
@Cacheable(value = "mentor", key = "#id")
public Mentor getMentorWithCache(Long id) {
return mentorMapper.selectById(id);
}
@CacheEvict(value = "mentor", key = "#mentor.id")
public void updateMentor(Mentor mentor) {
mentorMapper.updateById(mentor);
}
7. 测试与部署
7.1 测试策略
测试类型:
- 单元测试(JUnit + Mockito)
- API测试(Postman + Newman)
- 压力测试(JMeter)
性能测试指标:
| 场景 | 并发用户 | 平均响应时间 | 错误率 | TPS |
|---|---|---|---|---|
| 登录 | 100 | 230ms | 0% | 420 |
| 提交申请 | 50 | 180ms | 0% | 280 |
| 消息推送 | 200 | 150ms | 0.2% | 350 |
7.2 部署方案
容器化部署:
dockerfile复制# Spring Boot服务
FROM openjdk:17-jdk
COPY target/mentor-system.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
# Nginx配置
upstream backend {
server mentor-app:8080;
}
server {
listen 80;
location /api {
proxy_pass http://backend;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
CI/CD流程:
- Git提交触发Jenkins流水线
- 自动运行单元测试和构建
- Docker镜像构建并推送到Harbor
- Kubernetes滚动更新
8. 项目演进方向
- 智能推荐算法:基于学生科研兴趣和导师研究方向的匹配算法
- 多维度评价体系:师生互评+科研成果追踪
- 移动端适配:开发微信小程序版本
- 数据分析看板:导师选择趋势、学科热度等统计
java复制// 简单的推荐算法示例
public List<Mentor> recommendMentors(Long studentId) {
// 获取学生兴趣标签
Student student = studentService.getById(studentId);
Set<String> interests = tagService.parseTags(student.getInterestTags());
// 查询匹配导师
return mentorMapper.selectList(
Wrappers.<Mentor>lambdaQuery()
.apply("JSON_CONTAINS(research_tags, {0})",
JSON.toJSONString(interests))
.last("ORDER BY (quota - accepted_count) DESC LIMIT 10")
);
}
在项目开发过程中,最大的收获是理解了如何平衡系统功能的完备性和用户体验的简洁性。例如在申请流程设计中,最初版本包含了过多状态和校验规则,导致用户操作负担加重。经过三次迭代简化为现在的核心状态机模型,既保证了业务完整性,又提升了操作流畅度。