1. 项目概述:现代Web技术栈构建的智能考勤解决方案
这个基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0的考勤系统,是我在高校信息化建设项目中实际落地的案例。相比传统考勤方式,它解决了三个核心痛点:纸质签到易造假、Excel统计效率低、多系统数据孤岛。系统采用前后端分离架构,后端用Java生态的SpringBoot框架提供RESTful API,前端用Vue3组合式API实现动态交互,数据层通过MyBatis-Plus简化CRUD操作,整体技术选型兼顾了开发效率和运行时性能。
在具体功能上,系统支持人脸识别/二维码双模式考勤,自动生成课时统计报表,并与教务系统实现数据互通。实测在300人并发签到场景下,平均响应时间保持在800ms以内,MySQL查询通过索引优化控制在200ms以下。接下来我会从技术实现角度,拆解这个项目中值得关注的架构设计和编码实践。
2. 技术栈深度解析与选型依据
2.1 SpringBoot2后端框架配置要点
选用SpringBoot 2.7.x版本(非最新的3.x)主要考虑高校服务器仍多运行JDK8环境。在application.yml中需要特别配置:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/attendance?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 加密后的密码建议用Jasypt处理
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER # 解决Swagger与新版SpringBoot的兼容问题
踩坑提示:MySQL8.0驱动需要显式指定时区,否则会出现时差问题。生产环境务必启用SSL连接。
2.2 Vue3前端工程化实践
通过Vite创建的项目比传统Webpack构建快3-5倍,vite.config.js关键配置如下:
javascript复制export default defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'comps': path.resolve(__dirname, './src/components')
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
采用Pinia替代Vuex进行状态管理,典型store模块化结构:
code复制stores/
├── user.js # 用户登录状态
├── course.js # 课程数据
└── attendance.js # 考勤记录
2.3 MyBatis-Plus高效数据操作
在实体类上使用Lombok简化代码,配合MP注解实现ORM映射:
java复制@Data
@TableName("t_student")
public class Student {
@TableId(type = IdType.AUTO)
private Long id;
private String studentNo;
private String name;
@TableField(exist = false)
private List<AttendanceRecord> records;
}
动态查询示例(避免XML编写):
java复制public Page<Student> queryStudents(QueryWrapper<Student> wrapper, int pageNum) {
return studentMapper.selectPage(new Page<>(pageNum, 10),
wrapper.like("name", "张").orderByDesc("create_time"));
}
3. 核心功能实现细节
3.1 双模式考勤识别方案
人脸识别流程:
- 前端调用浏览器API获取视频流
- 通过Canvas截取面部特征帧
- 使用TensorFlow.js进行本地特征提取
- 将特征数据POST到后端比对
关键代码(Vue3组合式API):
javascript复制const captureFace = async () => {
const video = document.getElementById('video');
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/jpeg', 0.9));
const formData = new FormData();
formData.append('file', blob);
formData.append('courseId', courseId.value);
const res = await axios.post('/api/attendance/face', formData);
handleResponse(res);
}
二维码生成校验逻辑:
java复制// 生成带时效的课程签到码
public String generateQRCode(Long courseId) {
String salt = RandomStringUtils.randomAlphanumeric(8);
String rawText = courseId + "|" + System.currentTimeMillis() + "|" + salt;
return DigestUtils.md5DigestAsHex(rawText.getBytes());
}
// 验证二维码有效性(5分钟内有效)
public boolean validateQRCode(String code, Long courseId) {
long current = System.currentTimeMillis();
for (int i = 0; i <= 5; i++) {
String testTime = String.valueOf(current - i * 60000);
String rawText = courseId + "|" + testTime + "|" + salt;
if (code.equals(DigestUtils.md5DigestAsHex(rawText.getBytes()))) {
return true;
}
}
return false;
}
3.2 实时考勤数据看板
使用ECharts实现动态可视化,WebSocket推送更新:
javascript复制// 建立WS连接
const socket = new WebSocket(`wss://${location.host}/api/ws/attendance`);
socket.onmessage = ({ data }) => {
const payload = JSON.parse(data);
updateChart(payload.realTimeData);
};
// Vue3响应式更新图表
const updateChart = (data) => {
chartInstance.setOption({
series: [{
data: data.map(item => ({
name: item.courseName,
value: item.attendanceRate
}))
}]
});
};
后端事件推送处理:
java复制@GetMapping("/ws/attendance")
public void handleWebSocket(ServletRequest request, HttpServletResponse response) {
try {
WebSocketHandler handler = new AttendanceWebSocketHandler();
new WebSocketServerFactory().upgrade(request, response, handler);
} catch (Exception e) {
log.error("WebSocket建立失败", e);
}
}
4. 性能优化关键策略
4.1 MySQL查询优化方案
索引设计:
sql复制-- 考勤记录表核心索引
ALTER TABLE t_attendance
ADD INDEX idx_course_student (course_id, student_id),
ADD INDEX idx_check_time (check_time);
-- 使用覆盖索引优化统计查询
EXPLAIN SELECT COUNT(*) FROM t_attendance
WHERE course_id = 101 AND status = 1;
慢查询监控(需在my.cnf配置):
code复制slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
4.2 Redis缓存实践
Spring Cache配置类示例:
java复制@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues())
.build();
}
}
缓存击穿防护方案:
java复制@Cacheable(value = "courses", key = "#courseId", sync = true)
public Course getCourseWithCache(Long courseId) {
return courseMapper.selectById(courseId);
}
5. 部署与监控方案
5.1 Docker Compose编排
docker-compose.yml关键配置:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: attendance
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
5.2 Prometheus监控配置
SpringBoot Actuator集成:
yaml复制management:
endpoints:
web:
exposure:
include: "*"
metrics:
tags:
application: ${spring.application.name}
Prometheus抓取配置:
yaml复制scrape_configs:
- job_name: 'attendance-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['host.docker.internal:8080']
6. 典型问题排查实录
6.1 跨域问题解决方案
当出现Access-Control-Allow-Origin错误时,需检查:
- 后端全局CORS配置(推荐):
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.maxAge(3600);
}
}
- Nginx反向代理配置:
nginx复制location /api {
proxy_pass http://backend:8080;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
}
6.2 事务失效常见场景
失效案例1: 非public方法调用
java复制// 错误示例
private void updateAttendance() {
// 事务不会生效
}
// 正确做法
public void publicMethod() {
updateAttendance();
}
失效案例2: 自调用问题
java复制// 错误示例
public void process() {
this.updateAttendance(); // 绕过代理导致事务失效
}
// 解决方案1:注入自身代理
@Autowired
private AttendanceService self;
// 解决方案2:通过AopContext获取代理
((AttendanceService) AopContext.currentProxy()).updateAttendance();
7. 项目文档规范建议
7.1 Swagger接口文档集成
Knife4j增强配置:
java复制@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.attendance.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("考勤系统API文档")
.version("1.0")
.contact(new Contact("开发者", "", "dev@example.com"))
.build();
}
7.2 数据库文档生成
使用SchemaCrawler生成HTML文档:
bash复制java -jar schemacrawler.jar \
--server=mysql --database=attendance \
--user=root --password=123456 \
--info-level=standard --command=schema \
--output-format=html --output-file=db-doc.html
在项目根目录的docs文件夹中,建议包含以下文档:
API接口文档.md:核心接口说明部署手册.md:环境准备与启动步骤数据结构.md:主要表设计说明常见问题.md:已知问题解决方案
这个项目在实施过程中,我们发现前端采用Vue3的Composition API比Options API更适合复杂交互场景,而后端的MyBatis-Plus动态SQL能力大幅减少了样板代码。特别要注意的是MySQL8.0的密码加密方式变更问题,建议开发团队统一使用caching_sha2_password插件并做好连接池配置。