ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaWeb员工管理系统实战:数据库设计、增删改查与权限过滤

JavaWeb员工管理系统实战:数据库设计、增删改查与权限过滤 简介这是一套面向高校计算机专业学生与JavaWeb初学者、毕业设计选题者的企业员工信息管理系统完整开发资料围绕员工信息管理、请假审批、工资与出勤等业务场景提供从需求分析到系统测试的全流程参考方案。资源包共8个文件包含2个doc与1个docx文档、1个ppt答辩演示、1个sql数据库脚本、1个zip源代码压缩包以及2个mp4讲解视频整体约29.64MB覆盖论文、任务书、中期检查表、数据库建表与项目源码等关键材料。系统采用B/S结构分为管理员与普通员工两大模块管理员可进行部门管理、员工信息管理、出勤管理、工资管理及请假审核普通员工仅可查看工资与提交请假兼顾功能完整性与信息安全。视频部分演示了数据库创建、项目部署启动以及修改密码、我的工资、我的请假、部门管理、员工管理、出勤管理、工资管理、请假审核等模块的运行效果便于对照源码理解实现思路。目前已有178人学习适合需要快速搭建课程设计或毕业设计项目、参考论文写作与答辩准备的读者。1. 从一张员工花名册说起JavaWeb 员工管理系统到底解决什么问题很多团队管员工信息起点都是一张 Excel 花名册入职加一行离职标个灰调岗改个部门列。三五个人时没问题一旦过百人问题就来了——同一个人在不同表里部门不一致HR 改完 IT 那边还是旧数据想按「入职满一年且绩效为 A」筛个人得手动翻半天。企业员工信息管理系统要解决的就是把这张散落的表变成一个有约束、可查询、能分权的数据底座。这个标题里的 JavaWeb 是技术栈员工信息管理是业务场景数据库是落地核心。它适合两类人一是课程设计或毕设阶段需要一套完整可跑案例的学生二是中小团队里想用 JSP/Servlet 或 SSM 快速搭一套内部人事台账的开发者。整套东西的价值不在界面多花哨而在增删改查稳、字段约束严、权限分得清。下面我按「先立数据模型、再搭工程、最后填业务」的顺序把能复现的路径讲透。2. 先把数据库设计对员工表、部门表与权限表的字段取舍2.1 三张核心表怎么拆为什么不做成一张大宽表新手最容易犯的错是把员工编号、姓名、部门名、岗位、薪资、登录账号全塞进一张employee表。这样做的直接后果是部门改名要批量更新几百行而且部门本身的信息负责人、编制人数无处安放。常见做法是拆成三张表department存部门employee存员工主数据并用dept_id外键关联sys_user存登录凭证和角色。三张表职责单一改部门名只动一行。字段设计上有几个必须定死的点。员工编号用业务编号如EMP2024001还是自增主键我一般两者都留id做自增主键给程序用emp_no加唯一索引给业务用避免主键暴露业务含义。手机号、身份证号这类字段要加唯一约束否则重复录入时数据库不会拦你。状态字段用tinyint1 在职 / 0 离职而不是字符串查询和索引都更省。CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, manager VARCHAR(30), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, gender TINYINT DEFAULT 1 COMMENT 1男 0女, phone VARCHAR(20) UNIQUE, dept_id INT, position VARCHAR(50), hire_date DATE, status TINYINT DEFAULT 1 COMMENT 1在职 0离职, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明department的dept_name加唯一约束防止建出两个「技术部」。employee的dept_id建外键指向部门表删除部门时如果有在职员工会被数据库拦住这是好事逼你先处理人员归属。参数上字符集统一用utf8mb4否则姓名里的生僻字或 emoji 会存成问号存储引擎用 InnoDB因为要事务和外键。2.2 索引加在哪几列加错了比不加还慢索引不是越多越好。员工表最常用的查询是「按姓名模糊搜」「按部门筛」「按状态筛」所以name、dept_id、status值得建索引。但status只有 0 和 1 两个值选择性太低单独建索引意义不大通常和dept_id组成联合索引idx_dept_status(dept_id, status)更实用。phone和emp_no因为已经有唯一约束唯一索引自动就有了不用重复建。一个血泪经验别在name上建前缀索引还指望LIKE %张%走索引。前置通配符会让索引失效这是 MySQL 的硬规则。如果确实要做姓名模糊搜数据量小就全表扫数据量大就上全文索引或搜索引擎别在 B 树上硬扛。CREATE INDEX idx_dept_status ON employee(dept_id, status); CREATE INDEX idx_hire_date ON employee(hire_date);idx_dept_status让「查技术部所有在职员工」这类高频查询走索引idx_hire_date服务于「按入职时间范围统计」。加完索引用EXPLAIN看一眼type列出现ref或range算正常出现ALL就是全表扫得回头查 SQL 写法。3. 用 IDEA 把 JavaWeb 工程跑起来目录结构与数据库连接配置3.1 标准 Maven Web 工程长什么样在 IDEA 里新建项目选 Maven 加webapp骨架或者建普通 Maven 项目后手动补src/main/webapp/WEB-INF目录。标准结构是src/main/java放 Servlet 和 Servicesrc/main/resources放db.properties和 MyBatis 映射文件src/main/webapp放 JSP 和静态资源WEB-INF/web.xml做 Servlet 注册。很多人卡在「IDEA 运行 javaweb 项目配置」这一步核心是两件事Artifact 要配成war explodedTomcat 里部署这个 Artifact。pom.xml里依赖别贪多一套最小组合是javax.servlet-apiprovided 范围因为 Tomcat 自带、mysql-connector-java、druid连接池、mybatis或dbutils。用 provided 范围很关键打成 war 时不会把 servlet-api 塞进去否则和容器冲突报ClassNotFoundException。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency参数说明mysql-connector-java8.x 对应 MySQL 8驱动类名是com.mysql.cj.jdbc.DriverURL 要带时区和字符集参数否则中文乱码或时间差 8 小时。Druid 做连接池比每次新建连接快得多也方便监控慢 SQL。3.2 数据库连接配置与连接池参数db.properties里把连接信息抽出来别硬编码在 Java 里。URL 里useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai三个参数一个都不能少。连接池的初始连接数、最大连接数按并发量调内部管理系统一般initialSize5、maxActive20够用设太大反而占数据库连接数。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/emp_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password druid.initialSize5 druid.maxActive20 druid.maxWait3000maxWait3000表示拿不到连接最多等 3 秒就抛异常避免请求线程无限挂起。这个值设太大数据库压力高时页面会一直转圈设太小高峰期容易误报连接超时。3000 毫秒是内部系统的经验值。public class DBUtil { private static DataSource ds; static { Properties p new Properties(); try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { p.load(in); DruidDataSource d new DruidDataSource(); d.setDriverClassName(p.getProperty(jdbc.driver)); d.setUrl(p.getProperty(jdbc.url)); d.setUsername(p.getProperty(jdbc.username)); d.setPassword(p.getProperty(jdbc.password)); d.setInitialSize(Integer.parseInt(p.getProperty(druid.initialSize))); d.setMaxActive(Integer.parseInt(p.getProperty(druid.maxActive))); ds d; } catch (Exception e) { throw new RuntimeException(数据源初始化失败, e); } } public static Connection getConn() throws SQLException { return ds.getConnection(); } }逻辑说明静态代码块在类加载时执行一次把连接池建好后续getConn()直接从池里取。用 try-with-resources 读配置文件流会自动关闭。异常包成RuntimeException抛出让启动阶段就暴露配置错误而不是等到第一次查询才报错。4. 增删改查落地Servlet 分层与分页查询的写法4.1 分层别偷懒Servlet 只做参数接收把 JDBC 代码直接写在 Servlet 里能跑但改一个字段要翻遍所有 Servlet。常见做法是分三层Servlet 收参数、调 Service、转发 JSPService 做业务校验和事务Dao 只管 SQL。员工新增时Service 要先查emp_no是否已存在再插入这两步放一个事务里避免并发下插出两条相同编号。WebServlet(/emp/add) public class EmpAddServlet extends HttpServlet { private EmpService empService new EmpServiceImpl(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Employee e new Employee(); e.setEmpNo(req.getParameter(empNo)); e.setName(req.getParameter(name)); e.setPhone(req.getParameter(phone)); e.setDeptId(Integer.parseInt(req.getParameter(deptId))); e.setHireDate(java.sql.Date.valueOf(req.getParameter(hireDate))); try { empService.addEmployee(e); resp.sendRedirect(req.getContextPath() /emp/list); } catch (BizException ex) { req.setAttribute(msg, ex.getMessage()); req.getRequestDispatcher(/emp_add.jsp).forward(req, resp); } } }逻辑说明req.setCharacterEncoding(UTF-8)必须在取参数之前调用否则 POST 中文乱码。java.sql.Date.valueOf把yyyy-MM-dd字符串转成日期格式不对会抛IllegalArgumentException前端最好用日期控件限制输入。业务异常单独捕获后转发回表单页并带上提示而不是抛 500 让用户看堆栈。4.2 分页查询别用 LIMIT 大偏移也别在内存里分员工列表一定要分页否则几千条一次查出来页面直接卡死。分页 SQL 用LIMIT offset, size但offset很大时比如第 1000 页MySQL 会先扫过前面所有行再丢弃越翻越慢。优化手段是记住上一页最后一条的id用WHERE id lastId LIMIT size做游标分页。内部系统页码一般不深普通LIMIT够用但要知道这个边界。SELECT e.id, e.emp_no, e.name, d.dept_name, e.position, e.hire_date FROM employee e LEFT JOIN department d ON e.dept_id d.id WHERE e.status 1 AND (#{name} IS NULL OR e.name LIKE CONCAT(%, #{name}, %)) AND (#{deptId} IS NULL OR e.dept_id #{deptId}) ORDER BY e.id DESC LIMIT #{offset}, #{size};逻辑说明用LEFT JOIN而不是INNER JOIN保证部门被误删时员工记录还能查出来虽然外键一般会拦住。#{name} IS NULL OR ...这种写法让同一个 SQL 支持「不传条件」和「传条件」两种情况省得拼字符串。注意LIKE CONCAT(%, #{name}, %)用的是参数绑定不是字符串拼接能防 SQL 注入。public PageResultEmployee query(String name, Integer deptId, int page, int size) { int offset (page - 1) * size; long total empDao.count(name, deptId); ListEmployee list empDao.query(name, deptId, offset, size); return new PageResult(list, total, page, size); }参数说明page从 1 开始offset换算成(page-1)*size。total单独查一次COUNT(*)别用list.size()当总数那只是当前页条数。PageResult里带上总页数(total size - 1) / size前端分页条直接用。5. 避坑与排查数据库和工程配置里最容易翻车的五件事5.1 中文乱码从数据库到 JSP 要全链路统一现象新增员工后姓名显示成???或å¼ ä¸‰。原因通常有三处没统一数据库表字符集不是utf8mb4、JDBC URL 没带characterEncoding、JSP 页面没声明pageEncoding。解决顺序是先SHOW CREATE TABLE employee看表字符集再查 URL最后在 JSP 顶部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。三处都对了才不会乱。5.2 连接池报「too many connections」现象压测或多人同时用时抛CommunicationsException数据库端SHOW PROCESSLIST看到连接数爆满。原因是maxActive设太大或者代码里getConn()后忘了close()连接不归还池子。解决把maxActive调到数据库max_connections的合理比例并确保每个 Connection 都在 finally 或 try-with-resources 里关闭。用 Druid 的话开removeAbandonedtrue能兜底回收泄漏连接但那是后悔药根子还是代码要关。5.3 外键约束导致删部门失败现象删除部门时抛Cannot delete or update a parent row。原因是还有员工的dept_id指向这个部门。这不是 bug是外键在保护数据。解决要么先把该部门员工调到别的部门要么在业务层做「部门下有在职员工不允许删除」的校验并给出友好提示。别图省事直接SET FOREIGN_KEY_CHECKS0那是在给自己埋雷。5.4 IDEA 部署后 404控制台却没报错现象Tomcat 启动成功访问/emp/list返回 404。原因多半是 Artifact 没配或web.xml里 Servlet 映射写错。排查先看 IDEA 的 Deployment 标签页里 Artifact 是不是war exploded再看Application context路径是不是/。如果用的是WebServlet注解确认web.xml的metadata-complete不是true否则注解会被忽略。5.5 时间字段差 8 小时现象插入的hire_date或create_time在数据库里比实际早或晚 8 小时。原因是 JDBC URL 没指定serverTimezone驱动用了 UTC。解决URL 里加serverTimezoneAsia/Shanghai同时确认 MySQL 服务器时区。如果用的是DATETIME类型它本身不带时区存进去是什么就是什么问题一般出在驱动转换环节。6. 让这套系统真正能用权限过滤与数据校验的两个进阶技巧6.1 用 Filter 做登录拦截别在每个 Servlet 里复制判断系统上线前一定要加登录拦截否则任何人敲 URL 就能看员工薪资。最省事的做法是写一个LoginFilter拦截/emp/*和/dept/*检查 session 里有没有用户对象没有就跳登录页。注意放行登录页和静态资源否则会死循环。WebFilter(urlPatterns {/emp/*, /dept/*}) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }逻辑说明urlPatterns只拦业务路径登录页和 CSS/JS 不在范围内避免拦截登录请求本身。session 里存的是登录成功后放进去的用户对象判断非空即可。更细的权限比如普通员工不能删人可以在 Filter 里再读角色字段做二次判断。6.2 服务端校验不能省前端校验只是体验前端用required和正则做校验能提升体验但绕过太容易——禁用 JS 或直接发请求就跳过了。服务端必须再校验一遍手机号格式、入职日期不能晚于今天、员工编号不能重复。校验逻辑放 Service 层Dao 只管存取。我一般写一个Validator工具类把规则集中管理改规则只改一处。public void addEmployee(Employee e) { if (e.getEmpNo() null || !e.getEmpNo().matches(^EMP\\d{6}$)) { throw new BizException(员工编号格式应为 EMP6位数字); } if (empDao.findByEmpNo(e.getEmpNo()) ! null) { throw new BizException(员工编号已存在); } if (e.getHireDate().after(new java.util.Date())) { throw new BizException(入职日期不能晚于今天); } empDao.insert(e); }参数说明正则^EMP\d{6}$约束编号格式和数据库唯一索引形成双保险。findByEmpNo查重和insert之间理论上存在并发窗口高并发下要靠数据库唯一索引兜底捕获DuplicateKeyException再转成友好提示。这就是为什么唯一约束不能省——它是最后一道防线。这套系统真正值不值得做取决于你有没有把它当成「数据有约束、操作有权限、查询有分页」的完整闭环来练。我踩过最深的坑是早期图快把校验全放前端结果测试时用 Postman 直接插了一条入职日期在 2099 年的记录排查了半天才发现服务端根本没拦。后来养成习惯凡是数据库约束能表达的一定在表结构里写死凡是业务规则一定在 Service 层再校验一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表