
1. 项目概述与需求拆解1.1 劳务派遣人事系统到底解决什么问题劳务派遣这个行业和普通企业的人力资源管理差异很大。普通企业员工稳定合同一年一签甚至长期合同社保关系简单而劳务派遣公司面对的是典型的“人员流动大、用工单位多、社保基数杂”的三角关系。一个派遣员工从入职到离职中间涉及派遣公司、实际用工单位、社保经办机构三方任何一环断了轻则工资算错重则劳动纠纷。劳务派遣人事系统本质上就是把这套多方协作的流程搬到线上。它的核心价值不是“录入员工信息”这种表层的电子化而是把合同到期提醒、社保增减员联动、薪酬计算这些容易在Excel表格里用“人脑记忆”去兜底的事情变成系统自动触发、自动联动、自动留痕。我接触过不少劳务中介管理几百号派遣工靠的就是一张大表身份证复印件一沓合同到期没到期全靠翻日历这是非常危险的。所以我在拆解这个项目需求时第一原则是功能宁可做深不要做宽。做太宽但每个模块都浅最后就是个花架子业务方真正用起来还是回到Excel。系统必须让HR在系统里完成“入职建档→合同签署→社保增员→考勤维护→月度核薪→离职减员”这一整条链路的闭环操作。那这套系统到底适合谁学习或使用我觉得适合三类人一是正在做Java方向课程设计或毕业设计的学生需要一套结构完整、可跑通的工程二是中小劳务派遣公司或人力资源服务公司需要一个轻量级管理后台三是想把“人员管理”软件化思路迁移到自己业务里的独立开发者和产品经理。1.2 核心功能模块的拆解思路拿到这个需求我不会立刻去建表而是先绘制业务路线图。派遣员工的“生命周期管理”可以分成四个阶段进场、在职、发薪、离场。围绕这四个阶段我拆出下面这几个核心模块模块核心功能业务价值系统管理用户、角色、权限、登录日志区分管理员与HR防止越权员工档案基本信息、证件、教育经历、紧急联系人人员信息统一沉淀避免重复录入用工单位管理客户信息、派遣配置、结算标准明确每一个员工的甲方归属合同管理合同签署、续签、到期提醒降低劳务纠纷风险项目命脉社保管理参保城市、险种、基数、增员减员记录保证社保台账与社保局一致薪酬管理薪资方案、月度核算、发放记录从考勤与社保反推工资减少错账入离职管理入职登记、离职审批、停保联动联动变更状态不产生“僵尸档案”统计报表人员台账、到期清单、社保明细导出给管理层提供数据依据我刻意把“合同管理”“社保管理”“入离职管理”这三个模块放在了优先级最高的位置。原因很简单派遣业务的利润薄管理风险大合同忘签、社保漏停一期造成的损失都是实打实的。技术难度不在这三个模块多高级而在于“联动”比如员工离职审批通过后系统要自动生成社保减员记录而不是让HR再去手工操作一遍。1.3 技术选型背后的思考技术选型上这个项目采用了典型的Spring Boot单体应用方案。具体的组合是这样的JDK 1.8或11 Maven 3.8构建工具不用Gradle因为Maven生态在Java后端课程设计和中小企业项目里仍然是绝对主流排查依赖问题也简单。Spring Boot 2.7.x这是一个非常稳妥的选择。Spring Boot 3.0以后强制要求JDK17且jakarta命名空间变化对很多还在用JDK8的生产环境和学习环境并不友好所以2.7仍是落地最稳的版本。MyBatis-Plus 3.5.x用来做数据持久层。这个框架对单表CRUD支持极好自带逻辑删除、分页插件、字段自动填充可以明显减少重复代码量。MySQL 8.0 InnoDB引擎字符集用utf8mb4。因为员工姓名、地址、备注里面可能出现生僻字和表情符号utf8mb4不会出现写入失败。Spring Security JWT做认证授权。劳务派遣系统里权限其实很有讲究管理员能看所有公司的数据分公司HR只能看自己客户的员工操作员可能只能录入不能删除。用Spring Security做角色和接口级权限控制比自己手写拦截器规范不少。Redis可以选配用于缓存登录token和验证码。如果部署环境紧张纯MySQLXJWT也能跑但引入Redis后扩展性更好。有人可能会问为什么不直接上Spring Cloud微服务、或者用JPA这个问题我在实际项目里经常被问到。我的观点是一个几百人的劳务派遣公司内部系统单机能扛住微服务纯粹是给自己找运维麻烦而JPA对于动态查询和复杂报表SQL反而没有MyBatis好写。项目选型不是追新是看“维护成本交付速度团队熟悉度”的综合得分。2. 数据库设计与核心表结构2.1 从业务模型抽象出表关系数据库设计是整个项目最值得花时间的部分。刚开始接触这个需求时我差点把“员工”和“用工单位”设计成一张关联表后来发现派遣业务比较特殊一个员工在同一时期只属于一个派遣合同但一个派遣公司可以同时服务多家用工单位一名员工离开A单位后过半年可能又被派到B单位去。如果只用一张“员工表”关联“单位ID”历史关系就彻底丢了。我最终采用的是这样的核心实体设计sys_user系统登录用户内部账号和员工表没有强绑定方便管理。employee_info派遣员工档案包括身份证号、姓名、性别、联系电话、户籍地址、紧急联系人等。company_info用工单位甲方信息包括单位全称、统一社会信用代码、联系人和派遣合作模式。contract_info派遣合同表核心字段是派遣员工ID、用工单位ID、合同开始日期、结束日期、岗位、报酬标准、合同状态。social_security_record社保记录表记录该员工在某月份在哪个参保城市、按什么基数缴纳了哪些险种。salary_record薪酬发放记录表记录每月应发、实发、扣款明细。attendance_record考勤记录用于月度薪酬计算的输入数据。员工表与合同表是一对多合同表与社保记录表是一对多员工和社保其实是通过合同关联到具体用工单位的。这样设计的好处是将来统计“某家甲方单位聘用了多少人”“哪些人合同下月到期”“哪些员工本月的社保需要减员”全部只需要沿着合同这张关系表去SQL查询就行。2.2 员工表与合同表的建表实操先看最核心的员工档案表CREATE TABLE employee_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, emp_no varchar(32) NOT NULL COMMENT 员工编号系统自动生成, name varchar(64) NOT NULL COMMENT 姓名, id_card varchar(18) NOT NULL COMMENT 身份证号, gender tinyint(1) DEFAULT NULL COMMENT 性别 1男 2女, phone varchar(20) DEFAULT NULL COMMENT 联系电话, address varchar(255) DEFAULT NULL COMMENT 户籍地址, education varchar(32) DEFAULT NULL COMMENT 学历, emergency_contact varchar(64) DEFAULT NULL COMMENT 紧急联系人, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 员工状态 0离职 1在职, deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, create_time datetime DEFAULT NULL COMMENT 创建时间, update_time datetime DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT派遣员工档案表;这里有个细节值得注意id_card不是主键但加了唯一索引。因为身份证是最自然的业务标识但实际中员工可能改名、换发证件业务主键用自增ID最稳定唯一索引保证不会重复建档。status字段用tinyint而不是字符串是为了查询效率也方便在MyBatis-Plus中用枚举做类型转换。再看合同表CREATE TABLE contract_info ( id bigint(20) NOT NULL AUTO_INCREMENT, employee_id bigint(20) NOT NULL COMMENT 员工ID, company_id bigint(20) NOT NULL COMMENT 用工单位ID, contract_no varchar(64) NOT NULL COMMENT 合同编号, position varchar(128) DEFAULT NULL COMMENT 派遣岗位, start_date date NOT NULL COMMENT 开始日期, end_date date DEFAULT NULL COMMENT 结束日期, salary_standard decimal(10,2) DEFAULT NULL COMMENT 报酬标准, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 合同状态 0待生效 1生效中 2到期 3解除, remark varchar(500) DEFAULT NULL COMMENT 备注, deleted tinyint(1) NOT NULL DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_employee_id (employee_id), KEY idx_company_id (company_id), KEY idx_end_date (end_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT派遣合同表;合同状态我设计了四个枚举值这里没有用时间判断去隐含“合同是否到期”而是维护了一个显式的状态字段。为什么因为合同可能提前解除也可能续签时把旧合同作废单纯用“结束日期是否小于今天”去推断无法表达“解除”这个业务动作。2.3 字段设计的避坑经验列一个简单清单吧这些都是我在实际开发中反复踩过的坑金额字段一律用decimal(10,2)禁止用float/double这是核算对不上账的根源。所有核心表加上逻辑删除标记deleted劳务派遣场景中历史档案很重要不能物理删除。身份证号要校验格式存18位定长不要用varchar(20)不设校验就完事。日期用date时间用datetime两者语义不同合同起止用date创建更新用datetime。创建时间和更新时间统一维护不要靠应用层手动set而是用MyBatis-Plus的自动填充功能。关联外键建议加索引但这个项目里不建物理外键约束因为派遣业务里的关联关系经常需要“软处理”物理外键在数据回溯时会成为障碍。3. 核心后端功能实现与关键代码3.1 项目目录结构参考Spring Boot工程我喜欢按“模块分包”而不是“按层分包”组织因为功能模块多以后按层分包会让controller和service堆成一坨。这个项目最终的目录结构大致如下src/main/java/com/example/dispatch/ ├── DispatchApplication.java ├── config/ │ ├── SecurityConfig.java │ ├── MybatisPlusConfig.java │ └── WebMvcConfig.java ├── controller/ │ ├── EmployeeController.java │ ├── ContractController.java │ ├── SocialSecurityController.java │ ├── SalaryController.java │ └── AuthController.java ├── service/ │ ├── EmployeeService.java │ ├── ContractService.java │ ├── SocialSecurityService.java │ └── SalaryService.java ├── mapper/ │ ├── EmployeeMapper.java │ ├── ContractMapper.java │ └── ... ├── entity/ │ ├── EmployeeInfo.java │ ├── ContractInfo.java │ └── ... └── common/ ├── Result.java ├── BusinessException.java └── GlobalExceptionHandler.javaresult/entity/mapper/service/controller五层结构对课程设计和中小企业项目都非常常见。controller只做参数接收和结果包装业务判断全在service层mapper只做数据库操作这样后续加新功能时不会多个类互相纠缠。3.2 登录鉴权与权限控制的落地方法Spring Security JWT的配置在这个项目里是绕不开的。具体流程不复杂用户提交用户名密码到/auth/login接口。校验通过后系统用JWT生成tokentoken里带上用户ID和角色列表。后续请求在Header中携带Authorization: Bearer token后端通过过滤器解析并校验。校验通过后把用户信息放入SecurityContextHolder供接口层获取当前登录人。在SecurityConfig里核心是放行登录接口与静态资源其他接口一律认证。对于角色权限我使用PreAuthorize(hasRole(ADMIN))这样的注解加到controller方法上比如删除员工档案的接口只允许ADMIN角色而普通HR只能查看和修改。这里有个细节我给新手提个醒JWT oncePerRequestFilter 一定要设置“不拦截”的路径否则登录接口本身也会被安全过滤器拦下来导致死循环。3.3 员工入职与离职的业务联动实现入职离职是派遣系统里最具联动价值的场景。我举入职为例核心逻辑在EmployeeService.createEmployee()中顺序大概是校验身份证是否已经存在防止重复建档。生成员工编号规则是“派遣年份机构编码流水号”比如P2025-00123。插入employee_info表状态为在职。根据前端提交的合同信息插入一条contract_info记录状态为“待生效”或“生效中”。如果员工参保信息也提交了同步插入社保记录表。离职逻辑则相反先判断该员工有没有未完结的合同如果没有则更新员工状态为离职合同状态改为“解除”同时生成一条社保减员记录。这里最怕的是“离职了但合同没终止或者社保没停”所以service层需要保证这几张表的数据更新要么全部成功要么全部失败。我直接用Transactional事务注解包住整个方法。3.4 合同到期提醒的定时任务实现合同到期提醒是这个系统的“良心功能”。实现上我用Spring自带的Scheduled注解Component public class ContractRemindTask { Resource private ContractService contractService; Scheduled(cron 0 0 8 * * ?) public void checkExpiringContracts() { LocalDate today LocalDate.now(); LocalDate remindDate today.plusDays(30); LambdaQueryWrapperContractInfo wrapper new LambdaQueryWrapper(); wrapper.eq(ContractInfo::getStatus, 1) .between(ContractInfo::getEndDate, today, remindDate); ListContractInfo list contractService.list(wrapper); // 将到期合同写入提醒表或推送给管理人员 } }这段代码每天早8点执行一次查询所有状态为“生效中”且结束日期在未来30天内的合同。为什么用between(today, remindDate)而不是只查endDate remindDate因为已经过期的合同状态早就应该被处理了如果查出大量昨天到期的合同说明业务流程本身就有问题定时任务应该起到的是“提前预警”作用而不是“事后补救”。提醒结果我一般是写入一张消息表管理员登录后在首页看到待办事项而不是直接发短信因为企业内部系统对这种强提醒依赖并不大一个醒目的工作站内消息就足够了。4. 开发环境搭建与调试部署4.1 开发环境准备工作这个项目要本地跑起来环境真的不需要很复杂。我列一个标准的版本组合工具推荐版本说明JDK1.8 或 11推荐1.8兼容性最好Maven3.6.3 或 3.8.x3.9也能用但部分老仓库可能有问题MySQL8.05.7也可以但8.0对JSON和窗口函数支持更好IDEA2022社区版免费即可Redis可选7.x如果不启用缓存和验证码可跳过导入工程后第一件事是检查pom.xml中依赖是否完全下载。国内网络环境下Maven依赖下载慢建议把中央仓库镜像换成阿里云镜像地址否则在下载Spring Security相关依赖时可能卡很久。IDEA里设置user settings的mirrors这是标准操作不展开。4.2 核心配置文件示例这个项目的application.yml是我要重点提醒的部分因为绝大多数启动失败都和它有关。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dispatch_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true jwt: secret: your-very-long-secret-key expire-hours: 24关于数据源URL我几乎每次开新项目都要强调三个参数useSSLfalse是因为本地开发和内网部署完全不需要SSL加密握手开着反而容易报证书错误characterEncodingutf8是防止中文入库变成问号serverTimezoneAsia/Shanghai是必须的MySQL 8.0对时区非常敏感不加这一项会直接报Server time zone异常。map-underscore-to-camel-case: true这个配置也很重要可以让数据库的create_time字段自动映射到Java实体的createTime属性少写一堆TableField注解。4.3 数据库初始化与调试流程拿到源码后数据库初始化一般有两种方式一种是执行项目里附带的全量SQL脚本另一种是系统启动时自动执行SQL。我建议第一种把初始化脚本拆成schema.sql建表和data.sql基础数据通过Navicat或命令行一次性导入这样看得见摸得着不会出现“程序启动成功但查不到任何数据”的困惑。启动Spring Boot项目后推荐先用Swagger或Knife4j测试接口。访问http://localhost:8080/doc.html先调用登录接口拿到token然后调试员工列表、合同列表等核心接口。调试时重点看三件事数据是否正常返回有没有空指针。分页结果是否符合预期总条数是否和数据库实际数据一致。登录token过期时间是否正常别调一半就401了。4.4 打包与部署上线的标准流程部署环节我见得太多人在这里翻车。本质上就是一个Maven打包加上Java启动命令的事# 在项目根目录执行 mvn clean package -DskipTests # 启动前台运行适合验证 java -jar target/dispatch-system-1.0.0.jar # 生产环境后台运行 nohup java -jar target/dispatch-system-1.0.0.jar app.log 21 打包前记得改生产环境的数据库密码不要把application.yml里的本地密码带到服务器上。我习惯的做法是外挂application-prod.yml通过--spring.profiles.activeprod来切换这样本地配置和线上配置互不干扰。如果服务器上端口被占用报Address already in use用netstat -tlnp | grep java查一下进程再kill旧进程即可。5. 常见问题排查与避坑实录5.1 数据库连接不上时区、驱动、权限三座大山本地启动最经典的报错有几种。第一种是Unknown initial character set index utf8mb4 received from server这是因为数据库驱动和MySQL版本不匹配驱动没升级到com.mysql.cj.jdbc.Driver。第二种是The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这个就是典型的时区问题我前面已经说了在URL后面加serverTimezoneAsia/Shanghai即可。第三种是Access denied for user rootlocalhost这时候不要怀疑代码先检查数据库账号密码大概率是本地MySQL密码和配置不一致。5.2 MyBatis-Plus分页失效很多人写完分页查询发现无论传什么current和size返回的数据都是全量不是分页结果。这通常不是SQL写错了而是分页插件没有初始化。MyBatis-Plus 3.x之后需要显式配置分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不配置这个BeanPage对象虽然能创建但分页不会生效只是把全部数据塞进列表里。5.3 前后端联调跨域问题这个项目的后端如果提供了REST API并且前端同学在本地用Vue的8080端口访问后端8080就一定会碰到跨域。解决办法是在后端配置一个全局CORS过滤器或者直接实现WebMvcConfigurerConfiguration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); } }allowedOriginPatterns(*)和allowCredentials(true)必须组合使用如果写成allowedOrigins(*)会让allowCredentials(true)报错。5.4 静态资源404问题如果系统采用前后端分离但前端构建后要放进后端一起部署记得把前端打包生成的dist目录内容放到src/main/resources/static下。但是Maven在打包时有可能会过滤掉非标准资源导致resources目录下的静态文件没有被打进jar。这时需要在pom.xml显式指定资源目录build resources resource directorysrc/main/resources/directory filteringfalse/filtering /resource /resources /build另外还要注意Spring Boot在路由层面的一个坑如果项目里配置了RestController的/路径映射可能会吞掉静态资源的访问导致访问/index.html返回JSON而不是页面。最简单的解决办法是不要在全项目里使用RequestMapping(/)这种映射让Spring Boot默认的静态资源处理器接管。5.5 时间与编码问题速查最后整理一个我实际开发中最常遇到的小问题清单现象根因解决数据库存的中文变成??URL缺少characterEncodingutf8修改数据源URLJSON返回时间是时间戳没配置Jackson日期格式在yaml中配置date-format和time-zone查看日志的SQL太多打开了StdOutImpl生产环境把log-impl改为Slf4jImpl或关闭合同到期提醒重复推送定时任务部署了多实例引入分布式锁单机部署可忽略数据库连接池报Communication link failureMySQL默认空闲8小时断连配置spring.datasource.hikari.max-lifetime小于MySQL的wait_timeout个人在实际操作中最大的体会是劳务派遣系统这类“企业内部管理系统”技术难度永远不是瓶颈真正的瓶颈是对业务流程的理解和对细节的把控。比如社保基数调整、合同续签提醒、离职停保联动这些看起来不起眼的小功能才是用户每天都在用的东西。我的建议是拿到项目后先自己站在HR的角色上把流程走一遍再去动手写代码你会发现数据库设计里缺的字段、接口里缺的参数在流程演练中就会暴露出来比后期返工要省太多时间。如果你也是第一次接触这类项目按照“员工档案→合同管理→社保记录→薪酬核发”的顺序去推进每完成一个模块就运行测试一下整套系统很快就能跑起来。