中小企业人事管理系统是当前数字化转型浪潮中的刚需产品。传统Excel或纸质档案管理方式在员工规模超过50人后就会暴露出效率低下、数据孤岛、统计困难等问题。我们团队基于SpringBoot+Vue技术栈实现的这套系统,正是为了解决以下痛点:
这套系统采用前后端分离架构,前端用Vue实现响应式界面,后端基于SpringBoot提供RESTful API,数据库选用MySQL 8.0配合MyBatis-Plus实现高效数据操作。完整开源代码包含20+核心功能模块,从技术实现到业务逻辑都经过真实企业环境验证。
系统采用经典的三层架构模式,具体技术选型如下:
code复制前端层:Vue 3 + Element Plus + Axios
├─ 视图渲染:Vue 3 Composition API
├─ UI组件库:Element Plus 2.3.x
└─ HTTP客户端:Axios 1.3.x
后端层:SpringBoot 2.7 + MyBatis-Plus + Sa-Token
├─ Web框架:SpringBoot 2.7.12
├─ ORM框架:MyBatis-Plus 3.5.3
├─ 权限控制:Sa-Token 1.34.0
└─ 其他组件:Lombok/Hutool/POI-TL
数据层:MySQL 8.0 + Redis 7.0
├─ 主数据库:MySQL 8.0.33(InnoDB)
└─ 缓存数据库:Redis 7.0.11
技术选型理由:Vue3的Composition API更适合复杂业务逻辑组织,Element Plus提供丰富的企业级UI组件。SpringBoot+MyBatis-Plus组合在Java后端领域有最成熟的生态支持,Sa-Token相比Shiro更轻量且API友好。
核心表结构设计遵循第三范式的同时做了适当冗余优化:
sql复制-- 员工基础表
CREATE TABLE `sys_employee` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`emp_no` VARCHAR(20) NOT NULL COMMENT '员工编号',
`name` VARCHAR(50) NOT NULL COMMENT '姓名',
`dept_id` BIGINT NOT NULL COMMENT '部门ID',
`position_id` BIGINT NOT NULL COMMENT '职位ID',
`entry_date` DATE NOT NULL COMMENT '入职日期',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1在职 2离职)',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_emp_no` (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 考勤记录表(按月分表)
CREATE TABLE `attendance_record_202307` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`emp_id` BIGINT NOT NULL,
`clock_in` DATETIME COMMENT '打卡时间',
`clock_out` DATETIME COMMENT '签退时间',
`status` TINYINT COMMENT '状态(1正常 2迟到 3早退 4旷工)',
PRIMARY KEY (`id`),
INDEX `idx_emp_date` (`emp_id`, `clock_in`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计技巧:考勤表按月分表可控制单表数据量;在emp_no字段建立唯一索引避免重复;状态字段使用TINYINT比VARCHAR更节省空间。
前端采用Element Plus的表格+表单组合实现CRUD操作,关键实现点:
vue复制<template>
<el-table :data="employeeList" border>
<el-table-column prop="empNo" label="工号" width="120" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="deptName" label="部门" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" title="员工编辑">
<el-form :model="formData" label-width="80px">
<el-form-item label="姓名" required>
<el-input v-model="formData.name" />
</el-form-item>
<!-- 其他表单字段 -->
</el-form>
</el-dialog>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getEmployeeList, updateEmployee } from '@/api/employee'
const employeeList = ref([])
const dialogVisible = ref(false)
const formData = ref({})
const loadData = async () => {
const res = await getEmployeeList()
employeeList.value = res.data
}
const handleEdit = (row) => {
formData.value = { ...row }
dialogVisible.value = true
}
onMounted(() => {
loadData()
})
</script>
后端对应Controller实现RESTful风格接口:
java复制@RestController
@RequestMapping("/api/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping
public Result<List<EmployeeVO>> list(EmployeeQuery query) {
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<EmployeeVO> list = employeeService.listEmployees(query);
return Result.success(list);
}
@PostMapping
public Result<String> add(@Valid @RequestBody EmployeeDTO dto) {
return employeeService.addEmployee(dto) ?
Result.success("添加成功") :
Result.error("添加失败");
}
}
考勤计算的核心业务逻辑:
java复制public class AttendanceCalculator {
// 计算当月考勤状态
public AttendanceStatsVO calculateMonthlyStats(Long empId, YearMonth month) {
LocalDate startDate = month.atDay(1);
LocalDate endDate = month.atEndOfMonth();
// 获取该员工当月所有考勤记录
List<AttendanceRecord> records = recordMapper.selectByEmpAndDateRange(
empId, startDate, endDate);
// 获取节假日配置
Set<LocalDate> holidays = holidayService.getHolidays(startDate, endDate);
AttendanceStatsVO stats = new AttendanceStatsVO();
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
if (isWeekend(date) || holidays.contains(date)) continue;
Optional<AttendanceRecord> recordOpt = records.stream()
.filter(r -> r.getClockIn().toLocalDate().equals(date))
.findFirst();
if (recordOpt.isPresent()) {
processWorkdayRecord(recordOpt.get(), stats);
} else {
stats.addAbsentDays(); // 旷工
}
}
return stats;
}
private void processWorkdayRecord(AttendanceRecord record, AttendanceStatsVO stats) {
LocalTime clockIn = record.getClockIn().toLocalTime();
LocalTime clockOut = record.getClockOut().toLocalTime();
if (clockIn.isAfter(LocalTime.of(9, 30))) {
stats.addLateTimes(); // 迟到
}
if (clockOut.isBefore(LocalTime.of(18, 0))) {
stats.addLeaveEarlyTimes(); // 早退
}
stats.addNormalDays(); // 正常出勤
}
}
性能优化:使用Java 8的Stream API处理日期序列和记录匹配,节假日数据通过Redis缓存减少数据库查询。
权限系统采用RBAC模型,核心表包括sys_user、sys_role、sys_menu、sys_role_menu。认证流程实现:
java复制@Configuration
public class SecurityConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
SaRouter.match("/**")
.notMatch("/api/auth/login")
.check(r -> StpUtil.checkLogin());
})).addPathPatterns("/**");
}
}
// 登录控制器示例
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public Result<String> login(@RequestBody LoginDTO dto) {
SysUser user = userService.getByUsername(dto.getUsername());
if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
return Result.error("用户名或密码错误");
}
StpUtil.login(user.getId());
return Result.success(StpUtil.getTokenValue());
}
}
前端在axios拦截器中添加token:
javascript复制service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
}, error => {
return Promise.reject(error)
})
敏感数据加密:
SQL注入防护:
XSS防护:
推荐使用Docker Compose部署:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: hr_system
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7.0
ports:
- "6379:6379"
volumes:
- redis_data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/hr_system
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
redis_data:
缓存策略:
数据库优化:
前端优化:
问题1:前端编译时报Element Plus组件未注册
解决方案:
javascript复制// 正确导入方式
import { ElButton, ElTable } from 'element-plus'
createApp(App)
.use(ElButton)
.use(ElTable)
.mount('#app')
问题2:SpringBoot启动报数据库连接失败
检查要点:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/hr_system?useSSL=false&serverTimezone=Asia/Shanghai
问题3:MyBatis-Plus更新操作不生效
典型错误示例:
java复制// 错误做法 - 不会更新非null字段
employeeService.updateById(new Employee().setId(1L).setName("张三"));
// 正确做法
Employee emp = employeeService.getById(1L);
emp.setName("张三");
employeeService.updateById(emp);
问题4:Vue页面刷新后路由丢失
解决方案:
javascript复制const router = createRouter({
history: createWebHistory(),
routes
})
// 后端Nginx配置
location / {
try_files $uri $uri/ /index.html;
}
集成钉钉/企业微信考勤同步:
增加BI分析模块:
多租户改造:
这套系统经过三个月的开发迭代和两家中小企业的实际使用验证,核心功能稳定可靠。源码中包含完整的开发文档和SQL脚本,开发者可以快速部署体验,也可以基于现有代码进行二次开发满足个性化需求。对于Java全栈开发者而言,这个项目涵盖了企业级应用开发的典型技术组合,具有很好的学习参考价值。