1. 项目背景与核心需求
去年夏天,我在某互联网公司技术部实习时,亲眼目睹了HR部门用Excel表格管理30多名实习生的混乱场景。每周五下午,HR小姐姐都要逐个核对实习生的考勤、项目分配和导师评价,经常因为版本混乱导致数据冲突。这促使我萌生了开发一套轻量级实习生管理系统的想法,最终作为本科毕业设计课题落地。
这个系统需要解决三个核心痛点:
- 多角色协同管理(HR、部门主管、导师、实习生)
- 实习全生命周期管理(从入职分配到最终评价)
- 数据可视化与报表生成
技术选型上,我采用SpringBoot+Vue的主流前后端分离架构。SpringBoot 2.7.4提供RESTful API,Vue 3作为前端框架,Element Plus组件库加速界面开发。数据库选用MySQL 8.0,配合Redis缓存高频访问的实习生基础信息。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 技术栈全景图
code复制前端层:Vue3 + Vue Router + Pinia + Axios + Element Plus
网关层:Nginx反向代理 + JWT鉴权
服务层:SpringBoot 2.7.4 + MyBatis-Plus + Lombok
数据层:MySQL 8.0 + Redis 6.2
辅助工具:Hutool工具库 + EasyExcel
2.2 关键架构决策
-
前后端分离:前端通过8080端口访问,后端服务运行在9090端口,Nginx解决跨域问题。这种架构让前端同学可以独立开发,不受后端进度影响。
-
状态管理方案:放弃Vuex选择Pinia,因为其更简单的API设计和TypeScript支持。在实习生信息管理模块中,使用Pinia的store管理分页状态和筛选条件。
-
数据库设计技巧:
- 使用逻辑删除而非物理删除(is_deleted字段)
- 为导师评价表添加复合索引(evaluator_id + intern_id + create_time)
- 考勤记录表采用按月分表策略
3. 核心功能实现细节
3.1 实习生入职流程自动化
开发中遇到最棘手的问题是入职材料收集。传统做法是邮件发送PDF附件,但容易出现版本混乱。我的解决方案:
- 使用SpringBoot集成MinIO搭建文件服务器
- Vue前端集成vue-pdf-viewer预览文档
- 通过WebSocket实时通知材料审核状态
关键代码片段(后端签名验证):
java复制@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam MultipartFile file,
@RequestHeader("X-User-Id") Long userId) {
// 校验文件类型
String fileType = FileTypeUtil.getType(file.getInputStream());
if(!ALLOW_TYPES.contains(fileType)){
throw new BusinessException("不支持的文件类型");
}
// 存储到MinIO
String objectName = "intern/" + userId + "/" + UUID.randomUUID() + "." + fileType;
minioClient.putObject(
PutObjectArgs.builder()
.bucket("hr-bucket")
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return Result.success(objectName);
}
3.2 考勤签到防作弊机制
实习生的移动端签到功能需要防止位置伪造:
- 前端调用高德地图API获取实际坐标
- 后端校验坐标与公司地址的距离(500米内)
- 结合WiFi指纹二次验证(采集设备连接的BSSID)
vue复制<template>
<el-button
@click="handleCheckIn"
:disabled="!isInRange">
签到
</el-button>
</template>
<script setup>
import { ref } from 'vue';
import { AMapLoader } from '@amap/amap-jsapi-loader';
const isInRange = ref(false);
AMapLoader.load({
key: 'your-amap-key',
version: '2.0'
}).then((AMap) => {
const geolocation = new AMap.Geolocation();
geolocation.getCurrentPosition((status, result) => {
if(status === 'complete'){
const distance = calculateDistance(
result.position.lng,
result.position.lat,
companyLng,
companyLat
);
isInRange.value = distance <= 500;
}
});
});
</script>
4. 典型问题排查实录
4.1 跨域会话保持问题
现象:前端登录后,后续请求依然返回401未授权。
排查过程:
- 检查浏览器Application面板发现Cookie确实携带了JSESSIONID
- 后端日志显示每次请求的sessionId都不同
- 发现Nginx配置缺少
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=Lax"; - 添加后问题依旧,最终发现是Chrome的SameSite策略导致
解决方案:
nginx复制server {
location /api {
proxy_pass http://backend:9090;
proxy_cookie_flags ~ secure samesite=lax;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
4.2 MyBatis-Plus分页失效
现象:前端传了分页参数,但查询总是返回全部数据。
根本原因:未配置分页拦截器,添加配置类后解决:
java复制@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
5. 性能优化实践
5.1 实习生列表缓存策略
采用多级缓存方案:
- 第一层:前端本地缓存分页数据(Pinia persist)
- 第二层:Redis缓存热点数据(过期时间2小时)
- 第三层:MySQL查询优化(覆盖索引)
缓存击穿防护代码示例:
java复制public List<Intern> getInternListByDept(Long deptId) {
String cacheKey = "intern:dept:" + deptId;
// 1. 尝试从Redis获取
String json = redisTemplate.opsForValue().get(cacheKey);
if(StringUtils.isNotBlank(json)){
return JSON.parseArray(json, Intern.class);
}
// 2. 获取分布式锁
RLock lock = redissonClient.getLock("lock:" + cacheKey);
try {
if(lock.tryLock(3, 10, TimeUnit.SECONDS)){
// 3. 二次检查缓存
json = redisTemplate.opsForValue().get(cacheKey);
if(StringUtils.isNotBlank(json)){
return JSON.parseArray(json, Intern.class);
}
// 4. 查询数据库
List<Intern> list = internMapper.selectList(
new LambdaQueryWrapper<Intern>()
.eq(Intern::getDeptId, deptId)
);
// 5. 写入缓存
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(list),
2, TimeUnit.HOURS
);
return list;
}
} finally {
lock.unlock();
}
return Collections.emptyList();
}
5.2 批量导入性能提升
HR需要批量导入实习生信息,最初采用单条INSERT语句导致性能低下。优化方案:
- 使用MyBatis-Plus的
saveBatch()方法(默认分批提交) - 开启rewriteBatchedStatements=true优化JDBC批量操作
- 前端采用Web Worker解析Excel避免界面卡顿
实测数据:导入500条记录从28秒降至3秒
6. 安全防护措施
6.1 防止XSS攻击
针对实习生提交的日报内容:
- 前端使用DOMPurify过滤HTML标签
- 后端采用Jackson的
@JsonSerialize注解转义特殊字符 - 响应头设置Content-Security-Policy
java复制@JsonSerialize(using = XssJsonSerializer.class)
public class InternDailyReport {
private String content;
// getters & setters
}
public class XssJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString(HtmlUtils.htmlEscape(value));
}
}
6.2 权限控制方案
基于RBAC模型实现四级权限:
- 实习生:仅查看自己的信息
- 导师:管理所带实习生
- 部门主管:查看本部门数据
- HR:全局管理权限
使用Spring Security的PreAuthorize注解:
java复制@PreAuthorize("@ss.hasPermi('intern:manage')")
@PostMapping("/assign")
public Result assignMentor(@RequestBody AssignDTO dto) {
// 分配导师逻辑
}
7. 项目总结与反思
这个毕设项目从技术角度实现了既定目标,但在实际推广中发现了几个值得改进的点:
-
移动端适配不足:虽然使用了响应式布局,但导师反馈在手机上填写评价表时体验不佳。下次可以考虑开发专门的微信小程序版本。
-
文档自动化生成:目前实习鉴定表需要手动导出PDF,计划集成Apache POI实现模板化自动生成。
-
工作流引擎缺失:请假审批等流程硬编码在代码中,后续可以集成Flowable引擎提升灵活性。
一个意外的收获是:在实现WebSocket消息推送时,发现Chrome浏览器对同一域名下的并发连接数有限制(通常是6个)。这导致当多个Tab页打开系统时,部分连接会被阻塞。最终的解决方案是采用STOMP over WebSocket协议,通过单一连接复用多路消息。
