1. 项目概述:SpringBoot+Vue+MySQL全栈考勤管理系统
这套考勤管理系统采用前后端分离架构,后端基于SpringBoot 2.7.x开发,前端使用Vue 3组合式API,数据库选用MySQL 8.0。系统实现了员工打卡、排班管理、请假审批、统计报表等核心功能模块,特别适合50-500人规模的中小型企业使用。源码包经过实测可直接运行,内置了Docker部署脚本和Swagger接口文档。
我在实际部署中发现,这套系统对硬件要求极低——2核4G的云服务器就能流畅运行,但MySQL配置需要特别注意字符集设置为utf8mb4才能完整支持emoji表情签到(这个细节很多同类系统都忽略了)。系统默认采用JWT+RBAC权限控制,权限粒度可以精确到按钮级别。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构深度解析
2.1 后端技术栈设计
SpringBoot框架选用2.7.18稳定版而非最新的3.x系列,主要考虑企业环境对JDK版本的兼容性(仍有许多公司使用JDK8)。持久层采用MyBatis-Plus 3.5.3,其Lambda表达式查询构建器让动态SQL编写效率提升40%以上。特别值得注意的是分页插件配置:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 防止全表更新与删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
}
考勤业务的核心在于时间计算,系统使用Hutool的DateUtil处理复杂的跨天打卡场景。例如计算迟到分钟数的关键算法:
java复制public int calculateLateMinutes(LocalDateTime signTime, LocalDateTime shouldSignTime) {
long diff = DateUtil.between(signTime, shouldSignTime, DateUnit.MINUTE);
return diff > 0 ? 0 : (int) Math.abs(diff);
}
2.2 前端工程化实践
Vue 3项目采用Vite 4构建,相比Webpack冷启动时间缩短80%。Element Plus按需引入配置需要特别注意:
javascript复制// vite.config.js
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
Components({
resolvers: ElementPlusResolver({
importStyle: 'sass' // 使用sass主题变量
})
})
],
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "~/styles/element/index.scss" as *;`
}
}
}
})
路由设计采用动态加载方案,配合后端权限数据实现菜单动态渲染。关键的路由守卫逻辑:
javascript复制router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (!userStore.token) {
if (to.meta.requiresAuth) {
next('/login')
} else {
next()
}
} else {
if (!userStore.hasRoutes) {
await userStore.getUserInfo()
next({ ...to, replace: true })
} else {
next()
}
}
})
3. 数据库设计与优化
3.1 核心表结构
考勤系统的灵魂在于考勤记录表的设计,本系统采用纵向分表策略——基础信息与详细记录分离:
sql复制CREATE TABLE `attendance_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '员工ID',
`date` date NOT NULL COMMENT '考勤日期',
`status` tinyint NOT NULL COMMENT '1正常 2迟到 3早退...',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_date` (`user_id`,`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `attendance_detail` (
`record_id` bigint NOT NULL,
`sign_in_time` datetime DEFAULT NULL COMMENT '签到时间',
`sign_out_time` datetime DEFAULT NULL COMMENT '签退时间',
`location` varchar(100) DEFAULT NULL COMMENT 'GPS坐标',
`device_info` varchar(50) DEFAULT NULL COMMENT '设备标识',
FOREIGN KEY (`record_id`) REFERENCES `attendance_record` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 性能优化实践
针对百万级考勤记录的统计报表查询,我们采用以下优化方案:
- 建立按月分区的归档表:
sql复制CREATE TABLE `attendance_archive` (
`id` bigint NOT NULL,
`user_id` bigint NOT NULL,
`date` date NOT NULL,
-- 其他字段
PRIMARY KEY (`id`,`date`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(date)) (
PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),
-- 后续分区...
);
- 为部门统计报表创建物化视图:
sql复制CREATE TABLE `attendance_department_stats` (
`department_id` int NOT NULL,
`stat_date` date NOT NULL,
`normal_count` int DEFAULT 0,
`late_count` int DEFAULT 0,
PRIMARY KEY (`department_id`,`stat_date`)
) ENGINE=InnoDB;
4. 典型业务场景实现
4.1 弹性考勤规则引擎
系统采用策略模式实现可配置的考勤规则,核心接口设计:
java复制public interface AttendancePolicy {
AttendanceResult check(AttendanceRecord record);
}
@Component
@Slf4j
public class FlexiblePolicy implements AttendancePolicy {
@Value("${attendance.flexible.grace-period}")
private int gracePeriod; // 允许迟到分钟数
@Override
public AttendanceResult check(AttendanceRecord record) {
LocalDateTime actualSignIn = record.getSignInTime();
LocalDateTime standardSignIn = calculateStandardTime(record.getUserId(), record.getDate());
if (actualSignIn.isAfter(standardSignIn.plusMinutes(gracePeriod))) {
return AttendanceResult.late(standardSignIn, actualSignIn);
}
return AttendanceResult.normal();
}
}
4.2 移动端定位签到防作弊
结合百度地图API实现多重校验:
- GPS坐标与WiFi指纹双验证
- 设备唯一标识绑定
- 实时照片比对(使用OpenCV进行人脸特征提取)
关键校验逻辑:
java复制public boolean verifySignLocation(SignRequest request) {
// 1. 检查设备白名单
if (!deviceService.isTrustedDevice(request.getDeviceId())) {
throw new BusinessException("未授权的设备");
}
// 2. 计算与公司坐标距离
double distance = LocationUtils.getDistance(
request.getLongitude(),
request.getLatitude(),
companyConfig.getLongitude(),
companyConfig.getLatitude()
);
// 3. 动态围栏半径(不同类型员工不同范围)
double allowedDistance = employeeService.getAllowedDistance(request.getUserId());
return distance <= allowedDistance;
}
5. 部署与运维实战
5.1 Docker Compose全栈部署
项目提供的docker-compose.yml包含三个服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0.32
container_name: att-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
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"]
backend:
build: ./backend
container_name: att-backend
depends_on:
mysql:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/attendance
ports:
- "8080:8080"
frontend:
build: ./frontend
container_name: att-frontend
ports:
- "80:80"
5.2 性能调优参数
后端JVM参数建议配置(基于2核4G环境):
bash复制java -jar -Xms1024m -Xmx1024m \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=256m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:ParallelGCThreads=2 \
-XX:ConcGCThreads=1 \
attendance-system.jar
MySQL关键参数优化(my.cnf):
ini复制[mysqld]
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
sync_binlog = 100
max_connections = 200
thread_cache_size = 10
table_open_cache = 2000
6. 二次开发指南
6.1 如何扩展考勤规则
- 实现新的Policy接口:
java复制public class ShiftPolicy implements AttendancePolicy {
@Override
public AttendanceResult check(AttendanceRecord record) {
Shift shift = shiftService.getUserShift(record.getUserId(), record.getDate());
// 根据班次规则校验...
}
}
- 注册到Spring容器:
java复制@Configuration
public class PolicyConfig {
@Bean
@ConditionalOnProperty(name = "attendance.policy.mode", havingValue = "shift")
public AttendancePolicy shiftPolicy() {
return new ShiftPolicy();
}
}
6.2 前端自定义表单开发
使用Vue的动态组件实现可配置表单:
vue复制<template>
<component
v-for="field in formSchema"
:is="field.component"
:key="field.name"
v-model="formData[field.name]"
v-bind="field.props"
/>
</template>
<script setup>
import { ref } from 'vue'
const formSchema = ref([
{
name: 'leaveType',
component: 'el-select',
props: {
options: [
{ label: '年假', value: 1 },
{ label: '病假', value: 2 }
]
}
},
// 更多字段...
])
</script>
7. 常见问题排查
7.1 打卡记录不同步
典型症状:前端显示提交成功但后台无记录
排查步骤:
- 检查浏览器控制台Network选项卡,确认API请求是否返回200
- 查看后端日志:
bash复制docker logs att-backend | grep 'AttendanceController'
- 检查MySQL二进制日志是否开启:
sql复制SHOW VARIABLES LIKE 'log_bin';
7.2 报表生成缓慢
优化方案:
- 为统计字段添加计算列:
sql复制ALTER TABLE attendance_record
ADD COLUMN week_of_year TINYINT AS (WEEK(date, 3)) STORED,
ADD INDEX idx_week (week_of_year);
- 使用定时任务预生成报表:
java复制@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void generateDailyReport() {
// 使用CTE优化查询
String sql = """
WITH dept_stats AS (
SELECT department_id,
COUNT(CASE WHEN status=1 THEN 1 END) as normal_count,
COUNT(CASE WHEN status=2 THEN 1 END) as late_count
FROM attendance_record ar
JOIN users u ON ar.user_id = u.id
WHERE ar.date = CURDATE() - INTERVAL 1 DAY
GROUP BY department_id
)
INSERT INTO attendance_department_stats
SELECT *, CURDATE() - INTERVAL 1 DAY FROM dept_stats
ON DUPLICATE KEY UPDATE
normal_count = VALUES(normal_count),
late_count = VALUES(late_count)
""";
jdbcTemplate.update(sql);
}
8. 安全加固方案
8.1 防止XSS攻击
前端使用DOMPurify过滤富文本:
javascript复制import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title']
})
后端配置Jackson防XSS:
java复制@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
return new Jackson2ObjectMapperBuilder()
.deserializerByType(String.class, new HtmlEscapeStringDeserializer());
}
public class HtmlEscapeStringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) {
return StringEscapeUtils.escapeHtml4(p.getValueAsString());
}
}
8.2 接口防刷策略
基于Guava RateLimiter实现:
java复制@Aspect
@Component
public class RateLimitAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
String key = getRequestKey(); // 根据IP+接口生成key
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.value()));
if (!limiter.tryAcquire()) {
throw new BusinessException("操作过于频繁");
}
return pjp.proceed();
}
}
这套系统我在三家客户企业部署实施过程中,发现最容易被忽视的是考勤规则的边界条件测试。特别是跨时区分支机构的情况,一定要在数据库统一使用UTC时间存储,前端按用户时区显示。曾经有个客户因为没配置时区参数,导致美国团队全部显示旷工——后来我们增加了时区自动检测功能,现在系统会根据员工IP自动匹配所在时区规则。
