简介:这套基于Java Web的绩效考评系统项目包,面向Java Web初学者、高校在校生以及需要人事数字化的企业管理者,采用Servlet、JSP、JavaBean等经典技术栈完成员工绩效考核全流程。项目共包含509个文件,压缩包大小约982KB,除Java源码、JSP页面、class编译文件外,还提供SQL数据库脚本、js/css/images前端素材、SVN元数据及项目配置文件,导入Eclipse或IDEA后即可查看目录结构并进行调试运行。系统功能覆盖员工档案管理、绩效指标自定义、考核周期设置、上级评价录入、综合得分自动计算、可视化报表生成以及结果反馈模块,同时包含权限管理设计思路。数据库脚本可快速初始化员工表、部门表、绩效标准表和评价记录表,省去手动建表步骤。已有174人学习/下载,方便开发者直观理解Java Web分层架构、JDBC数据库交互与考核流程的业务封装,适合作为课程设计、毕业设计或中小企业绩效系统二次开发的参考模板。
1. Java Web绩效考评系统:Servlet + DAO 的完整闭环
在企业做绩效这件事,最麻烦的从来不是打分这个动作本身,而是每个考评周期里员工数据、指标权重、评分记录和部门横向对比怎么汇总到一起。这个 Java Web 绩效考评系统压缩包,走的正是经典 Servlet + DAO 分层结构:压缩包里能看到 LoginServlet、ScoreingServlet、ProjectAddServlet 这些控制器类,以及 UserDAOImpl、ScoreDAOImpl、ProjectDAOImpl 这几个数据访问实现,属于非常典型的基于 Java 的 web 绩效项目。它能解决的问题很直接:把员工管理、绩效指标设置、周期设定、评价录入、自动计算和简单的报表输出串成一个可以运行的闭环。适合两类人——一类是 Java Web 课程设计或者毕设阶段的学生,需要一套能演示、能讲清楚技术点的完整项目;另一类是中小企业里想用内网 Web 后台替代 Excel 做绩效打分的管理人员。下文我就从架构拆解、部署、核心业务逻辑、权限控制和踩坑点五个层面把它完整剥开。
2. 压缩包结构与部署准备:先让项目本地跑起来
拿到绩效考评系统压缩包,第一步不是急着打开 IDE 写代码,而是先把文件分类看清楚。这个包里放着 WdatePicker.js.bak、若干 .class 文件、Servlet 源码和数据库配置文件,它们的组织方式直接决定了你要不要补依赖、改路径、换版本。
2.1 文件分类:从 .class 反推包结构与技术栈
把压缩包解压后,观察文件列表能发现几类信息。首先是 WdatePicker.js.bak,这是 My97 日期选择器的一个备份文件,说明前端用了日期控件来做考评周期的起止时间选择。其次是 LoginServlet.class、ScoreingServlet.class、ProjectAddServlet.class、UserUpdateMyinfoServlet.class、TZUpdateServlet.class、RPUpdateServlet.class 这一批编译后的字节码文件,它们对应的是控制器层。
提示:压缩包里同时存在 .class 和 .java 时,优先以 .java 源码为准。.class 文件只能用于反编译对照,不能作为修改基础。
从类名命名可以反推包结构。LoginServlet 负责登录入口,ScoreingServlet 负责评分录入,ProjectAddServlet 负责绩效项目或指标的新增,UserUpdateMyinfoServlet 负责用户修改个人信息,TZUpdateServlet 和 RPUpdateServlet 分别对应调整分更新和评语(Report)更新。再加上 UserDAOImpl、ScoreDAOImpl、ProjectDAOImpl,这套代码的层次很清楚:Servlet 接收请求,调用 DAO 接口,DAO 实现类操作数据库。没有 Spring、没有 MyBatis,是原生 Servlet + JDBC 的经典教学式结构。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>绩效考评系统</title> <script language="javascript" src="js/WdatePicker.js"></script> </head> <body> <form action="scoreing" method="post"> 考评周期:<input type="text" name="period" onclick="WdatePicker({dateFmt:'yyyy-MM'})" readonly> <input type="submit" value="进入评分"> </form> </body> </html>这段 JSP 的核心是引入 WdatePicker.js 日期控件并绑定到考评周期输入框。dateFmt 设为 yyyy-MM,代表以月为最小粒度的考评周期。这里我把 action 指向 scoreing 这个 Servlet 映射,实际项目中如果你看到的是 scoreingServlet,需要到 web.xml 里核对 url-pattern,这是我部署时经常遇到的前后端路径不一致问题。
2.2 数据库初始化:员工表、部门表、绩效标准表与评价记录表
建表脚本是这套系统能不能跑起来的前提。绩效考评系统的典型关系型数据库设计包含四类表。
CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), department_id INT, role TINYINT DEFAULT 1 COMMENT '1=员工, 2=主管, 3=管理员' ); CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(100) NOT NULL ); CREATE TABLE project ( id INT PRIMARY KEY AUTO_INCREMENT, project_name VARCHAR(100) NOT NULL, weight DECIMAL(5,2) COMMENT '指标权重,如0.30表示30%', target_value DECIMAL(10,2) COMMENT '目标值', actual_value DECIMAL(10,2) COMMENT '实际完成值', dept_id INT, period VARCHAR(20) COMMENT '考评周期,如2025-Q1' ); CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, project_id INT, score_value DECIMAL(5,2) NOT NULL, adjust_value DECIMAL(5,2) DEFAULT 0 COMMENT '调整分', comment TEXT, score_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );user 表的核心是 role 字段,它决定了登录后能看到哪些菜单。project 表里的 weight 是权重字段,系统自动计算综合得分时就是拿 score_value 乘以 weight。score 表里我额外解释一下 adjust_value,TZUpdateServlet 对应的就是调整分回写,比如员工对某个指标得分有异议,主管可以给一个修正分差。
性能方面,如果记录量大,建议给 score 表的 user_id 和 project_id 加联合索引,否则每次汇总报表都会触发全表扫描,这个后面在验证阶段会讲到。
2.3 本地部署流程:Tomcat 版本选择与数据源配置
这个项目既然是原生 Servlet,部署目标就是一个标准的 Tomcat Web 容器。我的做法是优先选 Tomcat 8.5 或 9.0,注意 Servlet 版本与 web.xml 头部的对应关系。如果 web.xml 里声明的是 Servlet 3.0 规范,Tomcat 7 也能跑,但建议避免使用过低的版本。
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.performance.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <servlet> <servlet-name>ScoreingServlet</servlet-name> <servlet-class>com.performance.servlet.ScoreingServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ScoreingServlet</servlet-name> <url-pattern>/scoreing</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> </web-app>这段 web.xml 配置里最需要注意的是 servlet-class 的完整限定名,必须和实际包路径一致。很多新手部署项目时报 404 或 500,八成是这里的包名写错,或者 url-pattern 里的路径和 JSP 表单提交的 action 不一致。我在部署时通常会全局搜索一下 form 标签的 action 属性,再和 web.xml 的 url-pattern 逐一比对,这样能省下大量查日志的时间。
数据源方面,常见做法是在 Tomcat 的 context.xml 里配置 JNDI 数据源,也可以直接用 JDBC 工具类加载驱动。
<Context> <Resource name="jdbc/performanceDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="5" maxWaitMillis="10000" username="root" password="123456" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/performance?useUnicode=true&characterEncoding=utf8"/> </Context>这里关键参数有三个。第一个是 maxTotal,控制连接池最大连接数,绩效考评系统这种内部系统一般 20 就够了。第二个是 maxWaitMillis,如果设为 10000,意味着连接池耗尽时最多等待 10 秒,超过就抛异常。第三个是 url 里的 characterEncoding=utf8,这个参数决定中文会不会乱码,很多跨平台部署翻车都是漏了它。
3. 核心业务逻辑:绩效得分计算与 DAO 层实现
绩效考评系统一旦跑起来,核心就看两件事:得分怎么自动算,评分记录怎么落库。这一章我会从得分模型开始,再深入到 ScoreDAOImpl 和调整分更新的具体实现。
3.1 定量与定性指标的权重计算逻辑
考评系统最常见的得分算法是加权求和。先给每个考评项目设定 weight 字段,再录入 actual_value 与 target_value 对比,得到完成率,最后乘以权重得到加权得分。
计算公式可以概括为:综合得分 = Σ(完成率 × 权重)+ 调整分。
如果一个员工的三个指标分别是:销售额完成率 1.1(权重 0.5)、客户满意度完成率 0.95(权重 0.3)、考勤完成率 1.0(权重 0.2),那么不加调整分的情况是 1.1×0.5 + 0.95×0.3 + 1.0×0.2 = 1.035,再换算成百分制就是 103.5 分。
为了把这一逻辑落到数据库查询里,常见做法是在 ScoreDAOImpl 里做一次联表聚合。
3.2 ScoreDAOImpl 的聚合查询与结果映射
ScoreDAOImpl 承担了最重的数据访问工作。该类里除了基本的增删改查方法,还有一个核心的 getTotalScore 方法,它把 user、project、score 三张表关联起来。
public class ScoreDAOImpl implements ScoreDAO { private JdbcUtils utils = new JdbcUtils(); public List<ScoreVO> getTotalScore(String period, String deptId) { List<ScoreVO> list = new ArrayList<>(); String sql = "SELECT u.real_name, d.dept_name, " + "SUM(p.weight * s.score_value) AS total_score, " + "SUM(s.adjust_value) AS adjust_total " + "FROM score s " + "LEFT JOIN user u ON s.user_id = u.id " + "LEFT JOIN department d ON u.department_id = d.id " + "LEFT JOIN project p ON s.project_id = p.id " + "WHERE s.period = ? " + (deptId != null && !deptId.isEmpty() ? "AND u.department_id = ? " : "") + "GROUP BY u.id, d.dept_name"; try (Connection conn = utils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, period); if (deptId != null && !deptId.isEmpty()) { ps.setString(2, deptId); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ScoreVO vo = new ScoreVO(); vo.setRealName(rs.getString("real_name")); vo.setDeptName(rs.getString("dept_name")); vo.setTotalScore(rs.getBigDecimal("total_score")); vo.setAdjustTotal(rs.getBigDecimal("adjust_total")); list.add(vo); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }这段代码的逻辑分三层。第一层是 SQL 拼接,period 必填,deptId 可空,所以用条件判断来决定是否追加过滤条件。第二层是 PreparedStatement 参数填充,注意参数索引从 1 开始,前面 condition 里如果加了 deptId,第二个占位符才有效。第三层是结果集到 ScoreVO 的映射,这种手工映射虽然写起来繁琐,但对学习阶段的人来说反而更直观,能看明白每一列去哪了。
使用 LEFT JOIN 的原因是要保留没有评分记录的员工。如果用 INNER JOIN,一个员工没有任何 score 记录时,汇总结果里就会少掉这个人,绩效报表就不完整了。这一点在写报表类功能时非常容易忽略。
3.3 TZUpdateServlet 与 RPUpdateServlet:调整分与评语的数据回写
TZUpdateServlet 和 RPUpdateServlet 这两个类名在压缩包里很显眼。TZ 大概率是“调整”的拼音缩写,RP 大概率是“评语”或“Report”的缩写。它们的作用场景不一样。
@WebServlet("/tzupdate") public class TZUpdateServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String scoreId = req.getParameter("scoreId"); String adjustValue = req.getParameter("adjustValue"); if (scoreId == null || adjustValue == null) { resp.sendError(400, "参数缺失"); return; } ScoreDAO dao = new ScoreDAOImpl(); boolean flag = dao.updateAdjustValue( Integer.parseInt(scoreId), new BigDecimal(adjustValue) ); if (flag) { resp.sendRedirect("report.jsp"); } else { req.setAttribute("msg", "调整分更新失败"); req.getRequestDispatcher("error.jsp").forward(req, resp); } } }这里要特别注意的是 BigDecimal 的构造问题。从 request 里拿到的 adjustValue 是字符串,直接用 new BigDecimal(String) 是安全的,但如果你写成 new BigDecimal(Double.parseDouble(adjustValue)),就可能会引入浮点精度误差。调整分是直接改员工最终绩效结果的数据,精度问题绝对不能马虎。
RPUpdateServlet 的逻辑与之类似,差别在于它更新的字段是 comment,也就是评语区。评语一般较长,从前端传过来时需要注意 getParameter 对长度没有内在限制,但数据库字段如果是 TEXT 类型,超过 65535 字节就会插入失败。这个问题在数据量大的时候会出现,我一般会在 DAO 层做一次长度校验。
4. 权限管理:UserDAOImpl 的角色控制与防越权设计
Web 系统里的权限控制,说到底是两件事:第一,没登录的人能不能访问;第二,登录了但角色不够的人能不能越权操作。这个项目里的 UserDAOImpl 就是整个权限体系的底层支撑。
4.1 基于 Session 的登录态与角色区分
由于这是一个原生 Servlet 项目,没有 Spring Security,所以权限控制主要在 LoginServlet 和各个业务 Servlet 里手动完成。登录成功之后,把当前用户对象塞进 session,后续请求都先查 session 再放行。
public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); req.setCharacterEncoding("UTF-8"); UserDAO dao = new UserDAOImpl(); User user = dao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("currentUser", user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect("index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }setMaxInactiveInterval(30 * 60) 的含义是 session 最大空闲 30 分钟,超过就作废。企业绩效系统一般不会太长,因为涉及敏感数据。
角色区分通常通过 User 对象里的 role 字段实现,页面端用 JSTL 或 scriptlet 来判断。
<c:if test="${sessionScope.currentUser.role == 2}"> <a href="project_add.jsp">新建绩效指标</a> </c:if> <c:if test="${sessionScope.currentUser.role == 3}"> <a href="user_manage.jsp">员工管理</a> </c:if>这种写法的问题在于,只做了页面隐藏,没有做后端拦截。会翻车的地方是:如果用户直接访问 project_add.jsp 或者对应的 Servlet,系统不会识别他到底有没有权限。常见做法是写一个 Filter,统一拦截需要鉴权的路径。
4.2 密码存储与登录校验:了解散列的必要性
UserDAOImpl 里如果直接拿明文密码比对,那就只能用于本地学习,不能上生产。我一般会在代码里加入 MD5 或 BCrypt 哈希后再存储。
public User findByUsernameAndPassword(String username, String password) { String hashed = MD5Util.md5(password); String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; // 执行查询并返回结果 }如果数据库里存的是明文,一旦数据泄露,所有员工账号全裸奔。MD5 虽然不够强,但至少能阻挡大部分意外泄露。
对于课设项目,请假条和答辩讲解用的登录模块写成这样是可以接受的,但如果是真实企业部署,建议至少换成 BCrypt。
一个我常见的越权漏洞是:员工可以通过改 URL 里的参数,比如请求 /user/update/3,把 id 改成别人的编号,从而修改他人信息。
解决办法是在 Servlet 里校验当前登录用户的 id 与目标 id 是否一致,或者判断角色是否为管理员,否则直接返回 403。这个防越权检查,是绩效考评系统里最容易被忽视但又最关键的一道防线。
5. 部署避坑:Tomcat 版本、MySQL 编码与日期组件的常见排查
这部分是实操记录。我先后在不同机器上部署过这套类似结构的 Java Web 系统,踩过不少坑,有些问题几乎每个初学者都会遇到。下面按照现象到原因再到解决的格式列出最常见的四条。
5.1 404 错误
现象:访问 login.jsp 能打开,但提交表单后提示 404 或页面找不到。
原因:web.xml 中 Servlet 映射的 url-pattern 与 form 表单 action 里的路径不一致,比如 action 写了 scoreing 而配置是 /scoreing。
解决:以斜杠开头统一配置。在 web.xml 的 url-pattern 里写成 /scoreing,JSP 里也写 scoreing,或者统一用绝对路径。调试时用浏览器的 F12 查看 Network 面板,能快速看出实际请求路径。
5.2 中文乱码
现象:评分页面显示中文正常,但保存到数据库后,报表里出现乱码。
原因:MySQL 连接 url 缺少 characterEncoding,数据库表默认排序规则不是 utf8,或者 JSP 页面缺失 contentType 声明。
解决:连接 url 加 useUnicode=true&characterEncoding=utf8;建表时统一用 DEFAULT CHARSET=utf8mb4;JSP 顶部保留 contentType="text/html;charset=UTF-8"。这三处都对了,中文乱码基本可以避免。
5.3 WdatePicker.js 点击没反应
现象:部署后点击日期输入框没有弹出日历控件,浏览器控制台报找不到 WdatePicker。
原因:WdatePicker.js 文件没有拷贝到正确目录,或者引用的路径大小写不对。压缩包中文件名是 WdatePicker.js.bak,需要先重命名为 WdatePicker.js 再放进 js 目录。
解决:解压后立刻把所有 .bak 后缀改回正常文件后缀,统一放到 WebContent/js 下。引用时注意项目路径,推荐用 ${pageContext.request.contextPath}/js/WdatePicker.js,防止路径在多级目录部署时出问题。
5.4 连接池配置
现象:系统运行几个小时后,页面偶尔报连接超时。
原因:连接池里的连接被 MySQL 主动断开,但池中连接没有自动剔除。
解决:如果是 JNDI 数据源配置,设置 testWhileIdle=true 和 validationQuery="SELECT 1"。如果是直接用 JDBC 工具类,确保连接用完后在 finally 块里关闭。
成功部署后,登录系统、创建绩效指标、录入评分、生成汇总报表,整个流程在一个小时内是可以走通的。这一步走通了,后面再去研究更细的业务优化才有意义。
6. 进阶验证:绩效周期自动结转与报表导出技巧
项目跑通之后,还有一个经常被忽视的需求——绩效周期自动结转。每个季度末,管理员需要把上一个周期的数据归档,新的周期从零开始;同时,把汇总报表导出为 Excel 或 CSV 发给各部门确认。
这里给你一个比较实用的做法:在 ScoreDAOImpl 里加一个 archivePeriod 方法,用一个事务完成两步操作——先把 score 表里当前周期的记录复制到 score_archive 表,然后删除 score 表中的旧记录。
public boolean archivePeriod(String oldPeriod, String newPeriod) { String copySql = "INSERT INTO score_archive " + "(user_id, project_id, score_value, adjust_value, comment, period) " + "SELECT user_id, project_id, score_value, adjust_value, comment, ? " + "FROM score WHERE period = ?"; String deleteSql = "DELETE FROM score WHERE period = ?"; Connection conn = null; try { conn = utils.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(copySql)) { ps.setString(1, newPeriod); ps.setString(2, oldPeriod); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(deleteSql)) { ps.setString(1, oldPeriod); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里有几个参数值得细看。setAutoCommit(false) 的含义是关闭自动提交,让 copy 和 delete 变成一个原子操作。如果先删除旧数据再复制,一旦复制失败,这季度数据就全没了,所以必须先复制后删除,并且用 try-catch 把回滚兜住。
报表导出也可以用 CSV 方式实现,Tomcat 内置的 response 设置 CSV 下载格式即可。
resp.setContentType("text/csv; charset=UTF-8"); resp.setHeader("Content-Disposition", "attachment; filename=performance_" + System.currentTimeMillis() + ".csv");这种导出方式对中文处理的可靠性较高,因为 UTF-8 编码在 Excel 中只要不是太老的版本都能默认识别。如果想做 Excel 样式,再引入 POI,但 CSV 方式在绩效数据导出场景里完全够用。
从那以后,我每次部署这类 Java Web 系统,都会强制走一遍“路径核对 → 编码验证 → 权限检查 → 周期归档”这四个步骤。特别是 WdatePicker 后缀和 web.xml 路径这两个细节,真的是一步一个坑。希望这篇文章能帮你把这些坑提前绕过去,顺利把项目跑起来,也希望这份绩效考评系统的源码能成为你学习 Java Web 的一个扎实起点。希望帮到你。
本文还有配套的精品资源,点击获取