做这套系统的时候,我刚从“能跑就行”的阶段过渡到“别给后人挖坑”的阶段。小型企业的人事信息管理系统,听起来不像电商平台或者数据中台那么有排面,但它覆盖的场景非常典型:人员花名册、部门组织、考勤记录、请假审批、工资台账、登录权限。能用 Java Web 技术把这套东西从零做到能投产、能维护,比单纯跑通几个 Demo 有价值得多。这篇文章就围绕这套系统的设计与实现来拆,适合正在做毕业设计、刚入行想练手、或者真想给小公司做内部工具的开发者参考。
1. 需求拆解:先搞明白小企业要的到底是什么
1.1 小企业人事管理的三个核心痛点
小企业的人事管理跟大厂完全不是一个量级。大厂有专门的人事系统、薪酬系统、考勤系统,数据在各系统之间通过接口流转;小企业往往是一个行政兼人事的同事,用 Excel 管理上百号人的档案和考勤。真实痛点不外乎三个:
第一,数据分散且口径不一致。今天这个人调了部门,明天那个人改了岗位,Excel 表格改了这版忘了那版,跟工资条对不上的情况经常发生。
第二,权限不清晰。一个小公司,老板要看全局,部门主管看本部门,普通员工只能看自己。Excel 很难做到这种粒度,共享文件要么全可见,要么干脆都不可见。
第三,流程回溯困难。请假审批、入离职登记、工资调整,这些操作如果没有系统记录,出了纠纷根本说不清当时是谁批的、批了多少。
这套 Java Web 人事系统的需求,就围绕这三个痛点展开。模块上划分成员工档案管理、部门岗位管理、考勤管理、薪资管理、系统用户与权限管理五块,外加一个统计首页。注意,我没有加绩效考核、社保公积金自动计算这些重功能。小企业人事系统最忌讳一口气吃成胖子,真正的需求是把基础数据管清楚,把日常流程跑通,再谈扩展。
1.2 核心业务闭环:从入职到发薪的一个完整链路
设计系统的时候,我习惯先画一条业务闭环。企业招人后,第一步是在员工档案里新建一条记录,录入姓名、身份证号、岗位、所属部门、入职日期;第二步是开通系统账号,分配员工或主管角色;第三步是日常考勤,每天上下班打卡或者由管理员登记;第四步是请假、加班审批;第五步是每个月根据出勤天数和基本工资计算应发工资,再由财务确认发放。
这条链路跑通,系统就有了被日常使用的机会。如果只做增删改查,那跟 Excel 没有任何区别,用户不会愿意用。所以我在设计数据库时不是简单堆表,而是把员工表、考勤表、请假表、薪资表之间的关联看得非常重。比如考勤记录必然挂在某个员工 ID 上,薪资发放记录也必然关联员工和月份。有了这个闭环,统计首页才能展示实时的在岗人数、本月请假人数、待发薪人数,管理层才真正感受到系统带来的便利。
1.3 角色权限模型:老板、主管、员工看的是同一个系统里的不同世界
权限设计这块,纯 JavaScript 隐藏按钮是没用的,必须在后端做真实校验。系统采用基于角色的权限控制,用户表、角色表、菜单/权限表、用户角色关联表四件套。我定义了四个角色:
- 系统管理员:拥有全部权限,包括用户管理、系统参数配置。
- 人事专员:可以操作员工档案、考勤、薪资录入审核。
- 部门主管:可以查看本部门员工信息,审批本部门的请假申请。
- 普通员工:只能查看自己的档案、考勤、工资条。
登录成功后,后端把当前用户的角色标识放进 session,每次请求到达 Servlet 时先经过一个过滤器,根据请求路径判断是否具备访问权限。比如 /salary/list 这种路径,普通员工角色直接过滤掉,连数据都不能查到。这个模型实现成本不高,但比单纯靠前端菜单隐藏要靠谱得多。
2. 技术选型与架构设计:Java Web 项目怎么搭才不容易返工
2.1 技术栈选择的几个考量点
现在的 Java Web 项目有很多流派:纯 JSP+S Servlet、SSH 老项目、Spring MVC + MyBatis、Spring Boot 全家桶。考虑到这套系统的定位是小型企业、内部使用、代码清晰易维护,我最终选择了经典的 Servlet + JSP + JDBC 作为底层,也就是朴素的三层架构。为什么不用 Spring Boot?不是说 Spring Boot 不好,而是当系统只有这几十张表和几个业务模块时,框架本身的复杂度会超过业务复杂度。而且,用 Servlet 写一遍控制层转发、请求参数封装、数据库连接管理,能让人把 HTTP 请求的生命周期、Session 机制、事务边界这些底层概念彻底吃透,再回头用框架时思路会清晰很多。
当然,如果团队已经习惯了 Spring Boot,那我建议用 Spring Boot + MyBatis 替换,业务层接口不用大改,只是控制层的写法从 servlet 换成 @Controller。不过,本文面向的需求场景是“学习和练兵”,所以主要讲 Servlet + JSP 的实现思路。
技术栈清单如下:
- 开发语言:Java 8
- 前端:JSP + JSTL + Bootstrap + JavaScript(图表用 ECharts)
- 后端:Servlet 3.0 + JDBC + Druid 连接池(或用标准连接池组件)
- 数据库:关系型数据库,SQL 为标准语法
- 构建工具:Maven
- 部署环境:开源 Servlet 容器
2.2 分层设计:Controller、Service、Dao 各管各的事
系统的包结构是第一道防线。我从一开始就坚持按功能分包,而不是把所有类都丢在一个包里。基础结构如下:
com.company.hrms.controller:Servlet 控制层,负责接收请求、解析参数、调用 Service、跳转视图。 com.company.hrms.service:业务接口层,同时提供事务边界。 com.company.hrms.service.impl:业务实现层,一个类对应一个接口。 com.company.hrms.dao:数据访问接口层。 com.company.hrms.dao.impl:JDBC 实现层。 com.company.hrms.entity:实体类,对应数据库表。 com.company.hrms.common:工具类、常量、过滤器等。 com.company.hrms.filter:登录、权限、编码过滤器。
这套分层的理由很简单:Servlet 里不写 SQL,Dao 里不写业务判断,Service 层只处理业务规则。举个典型场景,删除一个部门之前,要先判断这个部门下是否有员工,如果有员工就不能直接删。这个逻辑放在 Service 层做,Controller 只负责告诉 Service “我要删这个部门”,Dao 只负责执行 delete 语句。一旦出现问题,顺着调用链就能定位,不会为了查一个 SQL 拼写错误翻遍整个项目。
事务管理是分层之后必须顺势解决的一环。JDBC 写事务很简单:con.setAutoCommit(false),执行完业务操作再 con.commit(),异常时 con.rollback()。我把连接绑定到每个线程的同一个连接上,确保一次请求中所有 Dao 操作共用一个数据库连接。具体做法是用 ThreadLocal 保存 Connection,在 Service 层的事务过滤器里统一开启和关闭事务。
3. 核心功能模块怎么落地:每一个模块都能写出花
3.1 员工档案模块:不只是增删改查
人事系统最重要的数据底座就是员工档案。这个模块看似简单,实际踩坑的地方不少。
员工表的核心字段我设计为:员工编号、姓名、性别、身份证号、手机号、邮箱、最高学历、入职日期、转正日期、部门ID、岗位ID、员工状态(试用/正式/离职)、备注。需要注意身份证号不能当主键,原因很简单:员工可能离职后再入职,身份信息不变但员工业务身份变了,系统应该允许同一个身份证号对应多条历史记录,所以主键必须用自增ID。出生日期和性别也不需要用户手动录入,可以由身份证号反推。代码里封装一个 IdCardUtils 工具类,对输入的身份证进行校验和解析,避免脏数据。
录入员工信息时,头像上传是可选项。文件上传我用 Servlet 的 @MultipartConfig 注解配合 Part 对象处理,图片保存到服务器磁盘的一个指定目录,数据库只存相对路径。这里有个很重要的小细节:绝对路径一定不要存进数据库,因为项目部署路径会变,把项目从开发机换到服务器,绝对路径就全废了。存相对路径,然后用常量拼出访问 URL,才是最稳妥的做法。
员工列表页默认展示在职员工,搜索条件按姓名、部门、岗位、状态进行模糊查询。分页用 PageBean 包装,每页默认十条。导出 Excel 的功能我是用 CSV 格式实现,生成一个带 BOM 头的 CSV 文件,浏览器下载后可以用 Excel 直接打开,避免引入重量级 excel 依赖库。
3.2 部门与岗位管理:树形结构与数据联动
部门管理不能做成平铺列表,因为小公司也可能有三四级组织架构,比如总公司下面有生产部和销售部,生产部下面又有一车间和二车间。数据库设计采用 parent_id 自关联结构,字段为部门ID、部门名称、上级部门ID、排序号。前端 JSP 页面用递归算法把部门列表渲染成树形下拉框,选择员工所属部门时就能按层级选择。
岗位表相对简单:岗位ID、岗位名称、所属部门ID。岗位跟部门绑定,避免出现“研发部里选了个财务专员”这种不伦不类的情况。
这里要重点讲一个容易出错的设计点:删除部门时不能只检查部门表有没有子部门,还要检查员工表里有没有员工引用这个部门。我的做法是在 Service 层写一个 deleteDepartment 方法,先查子部门数量,再查该部门及所有子部门下的员工数量,都为零才允许删除。事务范围覆盖这两个查询和最终删除,避免并发情况下出现数据不一致。
3.3 考勤与请假审批:状态机思想的具体运用
考勤模块是人事系统里最容易写乱的部分。我把它拆成两个子功能:每日考勤登记和请假管理。
每日考勤记录字段为考勤ID、员工ID、考勤日期、上班时间、下班时间、考勤类型(正常/迟到/早退/请假/缺勤)、备注。小企业一般不做实时打卡,而是由人事专员每天在后台批量登记,因此页面提供“按部门批量初始化今日考勤”的功能,默认生成所有在职员工的正常出勤记录,人事只需要修改异常项。这个设计非常贴近现实使用习惯。
请假管理我用了一个简单的状态机:提交申请 -> 主管审批 -> 人事确认。状态字段维护三个值:0 待审批、1 已通过、2 已驳回。员工提交请假单后,主管审批通过,状态变为 1,系统自动把该员工在对应日期范围内的考勤记录更新为请假类型。这个过程涉及两张表的业务联动,必须放在同一个事务里。Servlet 控制层只接收审批动作,Service 层根据当前状态判断哪些流转是被允许的。比如已驳回的单子不允许再次审批,已经撤销的请假单也不能被主管重复操作。
3.4 薪资管理:工资项可配置才不会被抱怨死
薪资模块的坑主要在于工资项不是固定的。基本工资、岗位工资、绩效奖金、餐补、全勤奖,每个人所在部门不同,薪资构成也不同。如果给员工表加上一排固定工资字段,后期调整很痛苦。
我的做法是设计两张表:工资项配置表和员工工资明细表。工资项配置表定义所有可能的工资项名称、类型(增项还是减项)、默认值;员工工资明细表记录某个员工在某个月份每个工资项的具体金额。这样既能灵活适配不同人的薪资结构,又能追溯历史调整记录。
每月计算工资时,系统根据员工基本工资和考勤统计生成带薪天数,再根据请假天数扣除对应金额。全勤奖的判断依据是当月迟到、早退、请假天数是否为零。计算完成后生成“待确认”状态的月度工资记录,由人事专员审核后确认发放。确认之后,该月工资记录不允许修改,必须走调薪单流程,这是为了防止误操作后账对不上。
3.5 登录认证与系统管理:简单的 RBAC 也有细节要打磨
最后是用户跟权限。用户表字段要有 id、用户名、密码、盐值、员工ID、角色ID、状态。密码绝对不能明文存储。我用的方案是 MD5 加盐:随机生成一个长度为 8 的盐值,把盐值和密码拼接后做 MD5 摘要,数据库里同时保存盐值和摘要值。登录校验时再取盐值重新计算后比较。虽然现在大家谈论加密都更推荐 BCrypt 一类算法,但在 Servlet + JDBC 的老技术栈里,加盐 MD5 作为练习题是合格的,如果拿到生产环境,建议升级为 BCrypt。
登录检查通过过滤器实现。用户第一次访问任何需要登录的页面时,过滤器先检查 session 里是否有 loginUser,没有就重定向到登录页。已登录用户访问权限之外的路径,返回 403 或跳转到权限不足页面。前端菜单按照角色动态渲染,后端接口按角色做白名单校验,两层配合才敢说权限是真的。
3.6 统计首页:用数据让管理层愿意天天打开系统
首页统计是一个很容易被忽略但很能展示系统价值的地方。我在首页放了几张卡片:在职人数、本月新入职、本月离职、本月请假人次;下面用折线图展示近六个月入职趋势,用柱状图展示各部门人数分布。查询 SQL 主要是 count、group by 加上日期范围筛选。图表用 ECharts 的简单折线图和柱状图,后端返回 JSON,前端异步加载渲染。
我这样做的原因很直接:如果系统的首页就是一排菜单,老板不会觉得有什么;但如果一打开就能看到人员和部门概况,他会自然对这个系统产生信任感,而这个信任感对争取项目资源、推动系统在企业里落地非常关键。
4. 数据库表设计:建表之前先想三个问题
4.1 一张图看懂核心表的关联关系
表结构是整个系统的地基,我建议把主要表设计成下面这样,再做调整:
- 系统用户表 sys_user:id、username、password、salt、emp_id、role_id、status
- 角色表 sys_role:id、role_name、role_key
- 员工表 employee:id、emp_no、name、id_card、gender、birth_date、phone、email、education、hire_date、regular_date、dept_id、position_id、status
- 部门表 department:id、dept_name、parent_id、sort_order
- 岗位表 position:id、pos_name、dept_id
- 考勤表 attendance:id、emp_id、att_date、clock_in、clock_out、att_type、remark
- 请假表 leave_request:id、emp_id、begin_date、end_date、leave_type、reason、status、approver_id、approve_time
- 工资项配置表 salary_item:id、item_name、item_type、default_value
- 员工薪资明细表 employee_salary_detail:id、emp_id、salary_month、item_id、amount
- 月度工资确认表 monthly_salary:id、emp_id、salary_month、base_pay、total_pay、status、confirm_time
4.2 外键和冗余字段怎么权衡
新学数据库时老师要求外键约束必须加,但实际开发中我更多用逻辑关联而不用数据库外键约束。原因有两方面:一是避免删除部门时被 Employee 表外键卡住,业务逻辑已经拦截了非法删除,不需要数据库再兜底第二层;二是后续要拆表、分库的时候,物理外键会成为障碍。
冗余字段同样要有取舍。部门名称在员工列表页经常要显示,如果完全靠 join 查,每次列表页都要关联部门表,SQL 更复杂。我的折中方案是 employee 表里冗余一个 dept_name 字段,员工调部门时先更新员工部门 ID,再同步更新冗余的部门名称。虽然理论上存在数据一致性风险,但在这个量级下收益远大于风险。如果你喜欢更规范的做法,不在员工表冗余部门名称,只在查询 SQL 里 join,也完全可行,只是性能在数据量增大时会受影响。
4.3 日期类型和字符集是最容易踩的两个坑
日期处理是 Java Web 老区最容易反复踩坑的地方。MySQL 的 date 类型不带时分秒,datetime 带时分秒。员工入职日期用 date 就行,考勤打卡时间用 datetime。Java 端接收字符串“2025-04-10”,用 SimpleDateFormat 解析后传给 PreparedStatement 的 setDate 方法。查询日期范围时,SQL 写法为 attendance_date >= ? AND attendance_date <= ?,字符串可以用 yyyy-MM-dd 的当天最大值解决边界问题,也可以直接用 < 次日 00:00:00。我建议用“att_date >= ? and att_date < date_add(?, interval 1 day)”,这样就不会漏掉当天 23:59 的记录。
字符集方面,数据库连接 URL 必须加 useUnicode=true&characterEncoding=UTF-8,Servlet 层面加 CharacterEncodingFilter 强制设置请求和响应编码。JSP 页面顶部也要声明 pageEncoding="UTF-8"。这三处缺一不可,否则就可能出现插入中文正常、查询中文却是乱码的怪象。
5. 关键代码实现:直接贴出可以复用的部分
5.1 用 BaseServlet 统一处理请求分发
如果每个功能都写一个 Servlet,那 doGet 和 doPost 里必然充满重复代码。我写了一个 BaseServlet 基类,通过反射根据 method 参数调用对应方法,类似于 Spring MVC 里 @Controller + @RequestMapping 的效果。
public class BaseServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { handleRequest(req, resp); } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); handleRequest(req, resp); } private void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getParameter("method"); if (method == null || method.isEmpty()) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少 method 参数"); return; } try { java.lang.reflect.Method action = this.getClass().getMethod(method, HttpServletRequest.class, HttpServletResponse.class); action.setAccessible(true); action.invoke(this, req, resp); } catch (NoSuchMethodException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "找不到对应方法"); } catch (Exception e) { throw new ServletException(e); } } }比如员工模块的 Servlet 写为:
public class EmployeeServlet extends BaseServlet { private EmployeeService employeeService = new EmployeeServiceImpl(); public void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pageNum = req.getParameter("pageNum"); String searchName = req.getParameter("name"); PageBean<Employee> page = employeeService.findPage(Integer.parseInt(pageNum), 10, searchName); req.setAttribute("page", page); req.getRequestDispatcher("/WEB-INF/jsp/employee/list.jsp").forward(req, resp); } public void save(HttpServletRequest req, HttpServletResponse resp) throws Exception { Employee emp = parseEmployeeFromRequest(req); boolean ok = employeeService.saveOrUpdate(emp); resp.sendRedirect(req.getContextPath() + "/employee?method=list"); } }这样做的好处是,新增一个功能只需要继承 BaseServlet 并添加一个方法,URL 统一维护成 /employee?method=xxx,无需在 web.xml 里配置一长串 Servlet 映射。
5.2 数据库连接池与事务管理
连接不直接 new、不用 DriverManager.getConnection,而是从连接池获取,用完归还,避免频繁创建销毁连接。工具类大致如下:
public class DataSourceUtil { private static DruidDataSource dataSource = new DruidDataSource(); static { dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/hrms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("your_password"); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }事务管理的实现思路是每个请求开始时从连接池拿一个连接,放到 ThreadLocal 里,Service 层操作时统一获取,请求结束在 Filter 的 finally 里提交或回滚。这个模式不复杂,但请务必注意一个问题:Filter 里的事务拦截器必须在权限过滤器之后执行,而且 try/catch 要包住整个 filterChain.doFilter 调用,否则事务提交发生在视图渲染之前就不可控了。
5.3 密码加盐与登录拦截
密码加密工具类里面保存加盐逻辑:
public class PasswordUtil { public static String generateSalt() { String chars = "abcdefghijklmnopqrstuvwxyz0123456789"; StringBuilder sb = new StringBuilder(); Random random = new SecureRandom(); for (int i = 0; i < 8; i++) { sb.append(chars.charAt(random.nextInt(chars.length()))); } return sb.toString(); } public static String encrypt(String password, String salt) { String src = password + salt; return DigestUtils.md5DigestAsHex(src.getBytes(StandardCharsets.UTF_8)); } public static boolean verify(String rawPassword, String salt, String encrypted) { return encrypt(rawPassword, salt).equals(encrypted); } }登录过滤器要排除登录页、静态资源,其余请求全部检查 session:
public class LoginFilter implements Filter { private List<String> excludedFields = Arrays.asList("/login", "/static"); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getRequestURI().substring(request.getContextPath().length()); for (String prefix : excludedFields) { if (path.startsWith(prefix)) { chain.doFilter(req, resp); return; } } Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }5.4 避免 SQL 注入:所有用户输入都要过 PreparedStatement
有朋友写业务代码时喜欢拼 SQL,比如 String sql = "select * from employee where name like '%" + name + "%'",这属于底线问题。所有带参数的查询必须用 PreparedStatement 的 ? 占位符,以及 like 的写法为 "name like ?",传入参数 "%" + name + "%"。我在代码评审时看到拼 SQL 都是直接打回。另外,数据字典下拉项也要从后端常量或数据库表读取,不要硬编码到 JSP 页面里,不然加一个学历类型就要改 JSP 重新编译。
6. 部署上线与常见问题排查实录
6.1 小企业服务器部署怎么操作
内部系统部署到一台普通办公电脑或者小云主机即可。步骤可以不复杂:Maven 执行 clean package 打出 war 包,把 war 包复制到 Servlet 容器的 webapps 目录下,启动容器,等待自动解压部署。如果有独立数据库服务器,就在生产库上执行建表脚本,修改项目里数据库连接配置,重新打包。如果只是共用一个库,那要注意建表前先备份原有测试数据,避免覆盖开发库。
Linux 服务器上需要注意防火墙和端口问题。数据库端口不要对公网开放,应用端口用一台 Nginx 反代并绑定域名。虽然小企业内部工具看起来不重要,但最基本的网络安全底线还是要有。
6.2 高频报错与排查方式
- 驱动类找不到:确认 pom 里数据库驱动依赖是否引入,打包后有没有塞进 lib 目录。
- 中文乱码:按顺序检查数据库连接地址、数据库建库字符集、JSP 页面编码、Servlet 编码过滤器,四个地方统一为 UTF-8。
- 连接池获取连接超时:检查数据库是否启动、账号密码、防火墙。
- 页面 404:检查 Servlet 映射路径和 JSP 在 WEB-INF 下的路径,WEB-INF 下视图必须走 RequestDispatcher 转发,不能直接浏览器访问。
- 页面 500:绝大多数是空指针或 SQL 错误。先看控制台完整堆栈,别只看第一行“NullPointerException”,要找到项目自己代码的那一层。
- 文件上传为空:MultipartConfig 注解是否加到 Servlet 上,表单是否写了 enctype="multipart/form-data"。
- 登录后立刻失效:session 作用域被误配置为 request,或者浏览器开启了禁用 Cookie,而容器默认使用 cookie 保存 session id。
6.3 常见问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 插入中文变问号 | 数据库连接字符集错误 | 连接URL加 characterEncoding=UTF-8 |
| JSP 页面中文乱码 | 页面编码不对 | 统一 pageEncoding=UTF-8 |
| 列表页数据库连接超时 | 连接池配置过小 | 调大 initialSize/maxActive |
| filter 中转发页面死循环 | 登录过滤排除列表不全 | 排除掉 login 和静态资源路径 |
| 删除部门报错 | 有子部门或员工引用 | Service 层先查询,阻止删除 |
| 工资计算结果对不上 | 考勤日期边界问题 | 用左闭右开区间查询 |
| 导出文件中文乱码 | CSV 未写 BOM | 文件头加 UTF-8 BOM 字节 |
从项目到经验:我还想提的三件事
第一件事,代码永远比想象中需要更多时间,尤其是联调和修改历史数据。人事系统上线最忙的不是开发,而是把员工档案、考勤历史录入新系统的阶段。我在开发时顺手写了一个 SQL 初始化脚本,把测试数据换成完全脱敏的模拟数据,同时预留了 Excel 批量导入员工档案的入口,这样上线时就能快速迁移。
第二件事,权限设计看起来简单,但真正考验你的是“改需求时不会推倒重来”。我的角色表里留了 role_key 字段,后续如果想增加“部门助理”这种角色,只需要插入一行角色数据,再在菜单权限表里配置,不需要改 Java 代码。同理,员工状态、请假类型、工资项类型都尽量用数据字典维护,而不是硬编码常量。
第三件事,也是我踩过的最深一次坑:本地测试一切正常,部署到服务器后时间查询差八小时。原因出在时区参数,连接字符串里加 serverTimezone=Asia/Shanghai 就解决了。这类环境差异问题,在交付部署文档时一定要提前写明白。
这套系统做下来,最大的收获不是学会了某种框架,而是理解了怎么把一个模糊的需求翻译成表结构、接口和页面。如果你想练 Java Web 的完整流程,人事系统绝对比随便写两个接口有价值得多。先把数据关系理清楚,再把事务边界切好,最后把页面做顺手,等这些基本功扎实了,再引入 Spring Boot、MyBatis 或者更复杂的微服务方案,会轻松很多。