JavaWeb入门实战:基于Servlet+JSP+MySQL的柜员绩效管理系统

发布时间:2026/9/26 6:23:12
JavaWeb入门实战:基于Servlet+JSP+MySQL的柜员绩效管理系统 带过不少零基础转行和在校生之后我发现新手做JavaWeb项目最怕的不是没需求而是需求一上来就不知道从哪儿下手。今天聊的这个柜员业务绩效管理系统是我觉得特别适合初学者拿来完整过一遍JavaWeb技术链路的入门项目。它不碰Spring、不碰微服务只用最经典的Java Servlet JSP MySQL组合却能让你把面向对象设计、数据库建模、会话管理、权限控制、增删改查这些核心基本功全部串起来跑一遍。你跟着做完以后再去学SSM或SpringBoot会明显感觉到不是在背概念而是有个项目经验打底。这个系统要解决的场景其实很清晰银行网点里柜员每天办理大量业务如何统计每个人的业务量、差错情况、客户评价并据此算出绩效排名。它天然自带“多角色权限”“数据统计汇总”“业务流水记录”这些真实项目的关键元素复杂度恰到好处不会让初学者被业务绕晕也不会只是一个光秃秃的增删改查Demo。1. 柜员业务绩效管理系统到底在管什么1.1 业务背景网点柜员的绩效从哪来很多新手拿到这个项目第一反应是“绩效系统不就是记录谁干了多少活吗”但如果你真的去银行柜台坐一天就会发现事情没那么简单。柜员办的业务分很多类型存取款、开卡销户、挂失、转账汇款、对公业务每一项耗时不一样风险等级也不一样。办一笔挂失可能要比办十笔活期存款还费神所以绩效不能只看流水笔数。这就带出一个核心设计思想绩效记录必须带上“业务类型”这个维度每种业务类型对应不同的积分权重。系统里存的不是简单的“柜员张某某办了5笔业务”而是“张某某在某个时间段办理了哪几类业务、每类多少笔、乘上权重系数后得多少分”。我建议你动手之前先用Word或画图板把下面几个对象理清楚柜员登录系统的用户能查看自己某个月的业务量和绩效得分。主管能看所有柜员的数据能给绩效记录做复核、扣分看得出错率。管理员维护柜员信息、维护业务类型和权重系数。业务流水每一次柜台操作对应一条绩效明细包含柜员、业务类型、办理时间、是否差错、客户评价等级。把这几个对象理清楚表结构基本也就出来了。初学者最容易犯的错是上来直接建表边写代码边加字段最后代码里到处是硬编码表也乱成一片。先想清楚业务关系再动手建库这个习惯建立起之后受益很长时间。1.2 角色模型为什么权限控制是这个项目的重点权限控制是JavaWeb学习里很容易被忽略的一块。新手项目经常做出来一个所有页面都能直接访问的系统没有登录拦截没有角色区分看起来功能都有但根本不像真实项目。这个绩效系统天然有三个角色正好用来练权限柜员登录进入个人工作台看自己的绩效明细和月度汇总只有查看权限。主管登录进入管理面板查看所有柜员的绩效列表复核异常记录调整差错标记。管理员登录进入系统维护页面对柜员账号做停用/启用对业务类型做维护。我们不用搞太复杂的Spring Security或Shiro就用Servlet的HttpSession配合一个Filter过滤器就能把未登录用户和越权访问拦住。这一步做完你对“会话”“拦截器思想”会有非常直观的感觉比刷十道八股文都管用。2. 数据库设计最值得反复琢磨的四张核心表2.1 先从实体关系开始别急着建库我见过很多新手一打开Navicat就先咔咔建表字段想到一个加一个建完发现表之间对不上又删了重建。其实关系型数据库设计是有套路的先列出业务对象再标出对象之间是“一对一”“一对多”还是“多对多”。这个系统里最核心的关系是“一个柜员办理了多条绩效记录”也就是一对多。绩效记录需要关联业务类型因为得分是类型权重算出来的还需要关联操作日志方便做数据追溯。管理员和主管表要不要建我建议直接合并成一张用户表用“角色”字段区分就行柜员信息单独建表。原因很简单柜员表里要存工号、所属网点、入职时间等业务属性和登录用的用户名密码不是一回事拆开结构更清晰。2.2 建表SQL与字段说明我用MySQL 8.0实测下面这套建表脚本可以直接粘贴运行。字符集统一utf8mb4排序规则用utf8mb4_general_ci。有些教程还在推荐utf8但utf8在MySQL里其实不是完整的UTF-8存emoji和生僻字会报错新手没必要在这个老坑里挣扎。CREATE DATABASE IF NOT EXISTS teller_performance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE teller_performance; -- 用户表登录账号和角色 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30) NOT NULL, role VARCHAR(10) NOT NULL COMMENT TELLER/LEADER/ADMIN, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ); -- 柜员信息表业务属性 CREATE TABLE t_teller ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, teller_no VARCHAR(20) NOT NULL UNIQUE COMMENT 柜员工号, branch_name VARCHAR(50) COMMENT 所属网点, hire_date DATE, FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 业务类型表类型权重系数 CREATE TABLE t_biz_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_code VARCHAR(20) NOT NULL UNIQUE COMMENT 业务编码, type_name VARCHAR(50) NOT NULL COMMENT 业务名称, weight DECIMAL(5,2) DEFAULT 1.00 COMMENT 绩效权重, sort_order INT DEFAULT 0 ); -- 绩效记录表核心流水 CREATE TABLE t_performance ( id INT PRIMARY KEY AUTO_INCREMENT, teller_id INT NOT NULL, biz_type_id INT NOT NULL, biz_date DATE NOT NULL, trans_count INT DEFAULT 1 COMMENT 笔数, error_flag TINYINT DEFAULT 0 COMMENT 是否差错, customer_rating VARCHAR(10) DEFAULT C COMMENT A/B/C/D, remark VARCHAR(255), approve_status TINYINT DEFAULT 0 COMMENT 0未复核 1已复核, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (teller_id) REFERENCES t_teller(id), FOREIGN KEY (biz_type_id) REFERENCES t_biz_type(id) );这里重点解释几个容易被忽视的字段。approve_status是主管复核标记很多初学者做绩效系统时会漏掉“审核”这个动作导致柜员自己录入的分数直接进了汇总表。customer_rating用来记录客户评价等级差评和好评在绩效里权重不同。error_flag对应差错标记一票顶三笔这种事在绩点规则里很常见。字段类型上也要注意。笔数用INT权重系数必须用DECIMAL(5,2)不要用FLOAT或DOUBLE否则计算浮点误差会在月度汇总时莫名其妙多出0.01。日期只用DATE不需要带时分秒的业务日期。绩效汇总时按biz_date分组就好。3. IDEA里跑通一个JavaWeb项目的完整配置3.1 技术栈选型不追求时髦但够学到东西我推荐的组合是Java 8 Maven Tomcat 9 Servlet 4.0 JSP MySQL 8.0 JDBC原生。这套组合看起来“老”但每一层都是明明白白能看得见的Maven负责依赖管理你能看到jar是怎么被引进来的。Servlet是Web应用的最底层入口你能理解HTTP请求是怎么被接住的。JSP让页面能直接渲染Java数据虽然现在企业里不太直接用它做前端但懂了它你才看得懂后来各种模板引擎在解决什么问题。JDBC让你亲手写连接、写PreparedStatement、写ResultSet映射后面再用MyBatis/JPA时你会感谢自己“苦过这段”。为什么不建议一上来就用SpringBoot因为SpringBoot把Tomcat内嵌了、把连接池自动配置了、把MyBatis给你封装好了一个新手跑起来之后往往是“项目能启动但脑子是空的”换一个报错根本无从排查。跑通这个传统JavaWeb项目你会对启动、部署、请求链路有肌肉记忆。3.2 IDEA集成Tomcat与依赖配置这里讲一下我实测过的配置流程。用IntelliJ IDEA社区版和专业版都行社区版也足够跑完这个项目。第一步新建Maven项目打包方式选warJava版本用1.8。pom.xml里加这几行依赖dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies注意Servlet和JSP依赖的scope一定要写provided因为Tomcat自带这些类不写的话部署时会报java.lang.LinkageError之类的冲突或者Tomcat启动时显示jar包重复加载。MySQL驱动用8.0版本驱动类名是com.mysql.cj.jdbc.Driver这是新手第一个会卡住的地方。第二步配置Tomcat。IDEA里点击Run - Edit Configurations - - Tomcat Server - Local。如果下拉框没有Tomcat Server选项说明IDEA版本或插件不支持可以去File - Settings - Plugins确认Tomcat插件没被禁用。配置Application context时我建议路径就叫/perf不要带版本号这样访问地址稳定http://localhost:8080/perf/login.jsp。第三步项目必须打成war包才能被Tomcat识别。在右侧Maven面板执行package然后配置Deployment选择xxx:war exploded这个模式支持改完代码以后自动热重载新手改错一个JSP不需要反复重启Tomcat。不过依赖布局有改动时还是要重启一下。3.3 目录结构没必要照抄框架但要有包的概念不需要搞得很花哨但包名一定要分好。我常用的是这个结构src/main/java ├─ com.example.perf │ ├─ entity // 实体类 User、Teller、BizType、Performance │ ├─ dao // JDBC数据访问层 │ ├─ service // 业务层绩效计算逻辑 │ ├─ servlet // 控制器层 │ ├─ filter // 登录和权限过滤器 │ └─ util // DBUtil、DateUtil src/main/webapp ├─ page // JSP页面 │ ├─ teller │ ├─ leader │ └─ admin ├─ static // css、js └─ WEB-INF/web.xml很多新手把所有类一股脑塞进default包或者全扔在servlet包里短平快的作业这样可能没事一旦项目超过五张表就会乱。分实体层、Dao层、Service层、Servlet层这叫三层架构的雏形JavaWeb面试必问你现在就养成习惯后面学SpringMVC会非常轻松。4. 核心代码实现把登录、权限与绩效计算落到实处4.1 会话管理与LoginServlet登录是第一个要实现的入口但新手经常把登录做成了摆设——页面跳过去Session不存也没有过滤器。我之前写过一个版本登录后直接在地址栏输入后台页面就能进去这等于没有任何权限控制。正确的思路是登录成功后把用户信息放进Session同时写一个Filter拦截所有/page/*下的请求判断Session里有没有用户有用户再看角色能不能访问对应目录。// LoginServlet 登录处理的核心代码 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(MD5Util.md5(password))) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(60 * 30); // 半小时超时 response.sendRedirect(request.getContextPath() /page/index.jsp); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }这里有三个细节要强调。第一是Session超时时间要显式设置不设置的话IDEA自带的Tomcat默认超时和浏览器行为在不同环境下差异很大演示时很容易“明明登录了但马上被弹回登录页”。第二是密码字段不能明文存初学者不需要上BCrypt至少用MD5加个盐这点很多网上的简单项目没做。第三是非登录用户不能走sendRedirect跳到登录页这样更友好但注意不能把POST请求直接重定向出去否则页面显示会有问题。4.2 权限拦截一个Filter挡住所有未登录请求Filter是JavaWeb里非常经典的组件。你可以在web.xml里配置也可以用WebFilter注解。新手我建议用注解代码量少看得见摸得着。WebFilter(urlPatterns {/page/*}) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); User user session ! null ? (User) session.getAttribute(loginUser) : null; if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } // 角色目录级权限控制 String uri req.getRequestURI(); String role user.getRole(); if (uri.contains(/page/leader/) !LEADER.equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (uri.contains(/page/admin/) !ADMIN.equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }这套写法的好处是你要加新页面、新角色只需要在Filter里加一个URI前缀判断不需要在每个Servlet里重复写权限校验。等后面学了Spring Security你会明白它就是把这套东西框架化、抽象化了。4.3 绩效录入与月度统计的SQL逻辑绩效录入算是整个系统的“写”核心。主管或管理员录入一条绩效记录插入t_performance表。录入时要注意事务问题——既然是给柜员录绩效就尽量不要出现“插入明细成功但是修改汇总表失败”的半截数据。虽然我们这套表没有单独放汇总表都是按需统计但新人必须养成事务意识因为后面真实项目中会有这种场景。// 事务示例录入绩效明细同时更新柜员累计业务量伪代码 Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); PreparedStatement ps1 conn.prepareStatement( INSERT INTO t_performance(teller_id, biz_type_id, biz_date, trans_count, error_flag, customer_rating, remark) VALUES(?,?,?,?,?,?,?)); // 填充参数... ps1.executeUpdate(); PreparedStatement ps2 conn.prepareStatement( UPDATE t_teller SET total_count total_count ? WHERE id ?); // 填充参数... ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { DBUtil.close(conn, null, null); }注意事务只要出现就一定要有rollback而且close要放在finally里否则连接不释放开发时没感觉跑上一百个用户同时录入就会把数据库连接池打满。月度汇总的SQL是这套系统的核心统计逻辑。按柜员、按业务类型、按月份组合SELECT t.real_name AS teller_name, bt.type_name, SUM(p.trans_count) AS total_count, SUM(CASE WHEN p.error_flag 1 THEN 1 ELSE 0 END) AS error_count, SUM(p.trans_count * bt.weight) AS score FROM t_performance p JOIN t_teller tt ON p.teller_id tt.id JOIN t_user t ON tt.user_id t.id JOIN t_biz_type bt ON p.biz_type_id bt.id WHERE DATE_FORMAT(p.biz_date, %Y-%m) ? GROUP BY t.id, bt.id ORDER BY score DESC;刚开始学SQL的人看到CASE WHEN和GROUP BY组合可能会懵但这里其实就两步先把所有明细横向关联好再按柜员和业务类型分组计算。CASE WHEN的作用是把差错的记录数变成1或0SUM出来就是差错总数。这个写法在公司做报表的时候天天用属于必须掌握的SQL技巧。4.4 JSP页面怎么展示绩效排行展示部分我用一个performance_list.jsp来显示排行榜。因为项目没上Vue所以就用JSP内置JSTL标签循环。这里强烈建议别在% %脚本里写Java代码看起来方便后面维护极痛苦。用JSTL% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table thead tr th排名/th th柜员姓名/th th业务类型/th th业务笔数/th th差错数/th th加权得分/th /tr /thead tbody c:forEach varrow items${scoreList} varStatusst tr td${st.index 1}/td td${row.tellerName}/td td${row.typeName}/td td${row.totalCount}/td td${row.errorCount}/td td${row.score}/td /tr /c:forEach /tbody /table如果你想让页面更有“绩效看板”的感觉可以在scoreList里多查一个scoreRate字段代表该柜员得分占全网点总得分的比例页面里直接用一个width样式百分比去渲染简单的横向柱状条。纯CSS就能实现不需要引入任何图表库新手也能看懂。div classbar-wrapper span张柜员/span div classbar stylewidth: ${row.scoreRate * 100}%score 86.5/div /div这一版我不建议立刻上ECharts。先把列表和比例条做出来跑通之后你自然会发现要更复杂的图表那时再引ECharts也不迟。5. 一次完整的启动事故排错思路比答案重要5.1 我实测过程遇到的五个坑这个项目我完整跑了三遍每次都有人在群里面提问最集中的几个问题基本是下列情况症状根本原因解决办法启动Tomcat报Failed to load driver class没导入MySQL驱动jar或驱动类名写成com.mysql.jdbc.Driver检查pom依赖的scopeMySQL 8.0用com.mysql.cj.jdbc.Driver中文乱码页面显示问号JSP编码、请求编码、数据库编码三层不一致所有页面统一UTF-8response.setCharacterEncoding(UTF-8)连接串加characterEncodingutf8日期显示全是0000-00-00MySQL sql_mode设置了严格模式非法日期直接报错插入前格式化日期或用java.sql.Date.valueOf()Session没生效登录后跳转又回登录页Cookie被禁用或web.xml配置了短超时检查session.setMaxInactiveInterval设置确认没有配置错误的session-timeout内网演示时图片/样式加载不出来JSP里用了绝对路径/perf/static但上下文被改统一用${pageContext.request.contextPath}拼接静态资源路径重点说一下中文乱码。这是JavaWeb新手绕不过去的大山而且往往不是一处的问题。我建议项目一开始就做一个CharacterEncodingFilter实现一个把所有请求响应都设为UTF-8的过滤器而不是在每一个Servlet里重复写三行设置代码。这是全公司都在用的做法你以后公司的老项目里一定见过类似的东西WebFilter(/*) public class CharacterEncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }5.2 建议复现一次“请求完整生命周期”很多新手项目跑通了就算完根本没有在浏览器F12里看过请求到底是怎么走的。我强烈建议你做这么一个小练习打开浏览器开发者工具勾选Preserve log。输入/perf/page/leader/leader_home.jsp不要登录。观察请求状态是302还是403Location头指向哪里如果你能说清楚“为什么Filter拦截后是302因为重定向为什么直接访问JSP也是302因为Filter是/*匹配”那你对HTTP状态码、请求转发和重定向的区别就掌握了。这几个概念比多刷几道题有用得多后面学框架时遇到401和403也不会傻傻分不清。6. 项目收尾前的几个小重构别让代码停在“能跑”6.1 消除JSP里的Java脚本片段第一版做到“能跑”接下来就可以做一次局部重构。我检查的优先级是这样的JSP里不能再出现% ... %脚本数据渲染交给EL表达式和JSTL。Servlet里不要再写JDBC代码数据访问统一放进DAO。业务计算逻辑比如绩效得分公式单独放到Service里方便以后改绩效规则。拿绩效算法举例第一版你们可能写在Servlet里double score count * weight - errorCount * 5;但如果以后想改成“差评超过三次扣10分”呢你就得翻Servlet这个体验就像去冰箱里找书一样难受。把计算放到PerformanceService.calculateScore(...)里规则只改一个方法。这个“为什么分层”的问题等你改第二版需求时会被放大得很有教育意义。6.2 引入连接池一条配置就能救回服务原生JDBC每次请求都DriverManager.getConnection在小项目中看不出来问题但如果你开两百个并发去压这个系统连接建立开销会把数据库直接压垮。引入连接池是JavaWeb进阶必经的一步。Druid是最容易接入的dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency然后写一个DruidUtil初始化连接池替换DBUtil里的获取连接方式。注意连接池里拿出来的连接用完之后close()其实是还给池子不是真关所以你必须保证每个连接在finally里关闭否则池子很快会被耗尽。这个话题面试时高频出现能顺带讲清楚一次。我实测连接池配置大概这样DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/teller_performance?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); dataSource.setUsername(root); dataSource.setPassword(你的密码); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(5);serverTimezoneAsia/Shanghai这段很关键MySQL 8.0不配时区你在传日期参数时会报The server time zone value...这是老教程里最坑人的历史遗留问题。6.3 再往下走还能做哪些功能如果你想在答辩或简历上多点亮点这个系统有非常自然的扩展方向导出月度绩效Excel报表用POI生成xlsx也算入门主流工具。分页查询绩效明细不用每次把几百条记录全部压到一个页面。核心是把LIMIT offset, pageSize加上再算总页数。按网点、按日期范围做多条件筛选下拉框联动非常容易出效果。做一条“绩效规则配置表”把权重、扣分规则做成数据库可配置让主管不用改代码就能调整绩效规则。我的建议是挑两个做就行千万别什么都加进去。项目做完的功能能讲清楚原理、说明白为什么这样设计比堆十个插件页面有价值得多。我第一次带学生做这个项目时有个人把分页、导出、统计图全部做完了答辩时被问“导出失败后事务怎么处理”当场问住了。你要保证你写的每一行代码你能讲出它的来龙去脉。最后分享一个小技巧把整个项目做完后用git init把代码提交到本地版本库中途每完成一个功能就提交一次。等面试官问你“你这个项目遇到过什么复杂问题”你翻提交记录就能想起当时改了什么、为什么改、踩了什么坑。这一步不是形式主义它会让你的项目经验变得真正经得起追问。项目不在多而在于你是否带着思考完整跑通了一次从设计到部署的链路。这套柜员绩效管理系统做完对JavaWeb的核心知识覆盖就已经足够扎实后面的路会顺很多。