简介:这是一份基于Java Web的公司访客登记系统课程设计完整资源包,主要面向计算机专业学生及Java Web入门开发者,用于完成访客信息管理类毕业设计或课程设计作业。系统实现了访客信息的添加、修改、删除、检索查询与分类统计五大核心功能,可结合JSP、Servlet、JDBC等主流技术栈理解MVC三层架构,并覆盖从页面交互到数据库操作的完整业务链路。压缩包共64个文件,其中包括14个Java源文件、12个JSP页面、14个class文件以及SQL数据库脚本、系统报告文档、说明文件和工程配置文件,总大小966KB,资源结构清晰,便于按源码、页面、数据库、报告分层查阅。目前已有127人学习下载,适合需要参考完整项目代码、数据库设计或实训报告写法的读者。通过研读源码与SQL脚本,可以快速掌握企业访客登记场景下的数据表设计、条件查询、分类统计及前后端交互流程,为独立完成同类课程设计提供有效支撑。
1. 课程设计题目拆解:访客登记系统考的就是这三件事
前台放一本纸质访客登记本,来访人自行填姓名电话,签离时再划一笔,这套流程在很多公司运行了十几年。课程设计把它搬上 Java web,要交付的无非三件套:能跑的源码、能导入的数据库脚本、能讲清楚的设计报告。别小看这个题目,它把 Java web 入门阶段最重要的东西全部覆盖了:页面到 Servlet 的请求流转、JDBC 对 MySQL 的增删改查、以及把登记数据变成统计报表的聚合 SQL。
适合正在选题的学生,也适合刚学完 Java 基础想拿一个完整 web 项目练手的开发者。常见做法是把系统拆成前台登记端和管理员查询端:登记端负责来访登记、签离、预约,管理员端负责查询记录、统计报表、导出数据。这个拆分不是功能拍脑袋,而是对应数据库里"多个入口访问同一张表,用状态字段区分业务阶段"的设计思路,评卷老师通常盯的就是这张表设计。
2. 技术选型与数据库建模:访客系统先定 ER 图再写业务代码
好多同学拿到题目第一件事是新建 Maven 项目,这顺序反了。课程设计的评分逻辑和公司项目不一样,老师会先翻报告里的数据库设计,再跑系统验证功能。所以先把表定下来,再回头写代码,反而能少返工。
2.1 选型:JSP+Servlet+JDBC 为什么比 Spring Boot 更稳
这个题目的技术栈有三个常见选择:纯 JSP+Servlet+JDBC、JSP+Servlet+MyBatis、Spring Boot + Thymeleaf。我一般推荐第一种,原因是课程设计报告要求把请求处理流程写清楚,Servlet 里从取参、校验、调 DAO 到转发页面都是显式的,每个方法都能对应到报告的一小节。Spring Boot 当然也可以,但若课程大纲本身没讲 Spring,答辩时被问到"为什么用框架"容易答偏。
技术选型表:
| 组件 | 版本建议 | 负责的事 |
|---|---|---|
| JDK | 8 或 11 | 编译运行环境 |
| Tomcat | 9.x(配 JDK8) | Servlet 容器,承载 web 应用 |
| MySQL | 5.7 或 8.0 | 持久化访客数据 |
| 连接驱动 | mysql-connector-java 5.1.49 或 8.0.x | JDBC 桥接 |
| 前端 | JSP + jQuery + ECharts | 页面渲染与统计图 |
这里有一个常见翻车点:Tomcat 10 把javax.servlet包改成了jakarta.servlet,如果课设代码是从旧项目抄的import javax.servlet.http.HttpServlet,部署到 Tomcat 10 会直接编译报错。保守起见,要么配 Tomcat 9 + JDK 8,要么把依赖换成jakarta.servlet-api。数据库驱动版本同理,MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,而 5.x 用com.mysql.jdbc.Driver,两者不通用。
2.2 访客与用户表设计:一个状态字段串起登记、在访、签离、预约
表结构是这份课设报告的灵魂。常见的合理设计是两张表:t_user存前台和管理员账号,t_visitor存访客来访记录。很多同学会把预约单独建一张预约表,再和来访记录关联,对于课程设计这个体量没必要,额外引入联表查询反而增加出错面。用单一status字段表达业务阶段,既是主流做法,统计报表时也少写一次 JOIN。
CREATE DATABASE visitor_sys DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE visitor_sys; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5 后的密码', real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '1 前台,2 管理员', dept VARCHAR(50) COMMENT '所属部门' ) ENGINE=InnoDB; CREATE TABLE t_visitor ( id INT PRIMARY KEY AUTO_INCREMENT, visitor_name VARCHAR(50) NOT NULL COMMENT '访客姓名', phone VARCHAR(20) NOT NULL, company VARCHAR(100) COMMENT '来访单位', visit_target VARCHAR(50) NOT NULL COMMENT '被访人', visit_reason VARCHAR(200) COMMENT '来访事由', status TINYINT NOT NULL DEFAULT 0 COMMENT '0 在访,1 签离,2 预约,3 取消', arrive_time DATETIME COMMENT '签到时间', leave_time DATETIME COMMENT '签离时间', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE INDEX idx_visitor_status ON t_visitor(status); CREATE INDEX idx_visitor_arrive ON t_visitor(arrive_time);说明几个设计要点。status用 TINYINT 而不是字符串,是为了让后续 SQL 的WHERE status = 0走索引更高效,也便于报表分组;arrive_time和create_time分开,是因为预约场景下记录创建时间和实际到访时间不是一回事。两个索引是给报表查询准备的,GROUP BY DATE(arrive_time)和频繁的WHERE status = ?都用得到。ER 图画这张图就够了:t_user与t_visitor在业务上通过"前台操作记录"关联,图上画成一对多即可。
2.3 初始化数据与 JDBC 连接参数:五分钟跑通数据库
数据库建好后要初始化一个管理员账号,密码建议先 MD5 存进去,避免明文写在 SQL 里:
-- 初始化账号,密码为 admin123 的 MD5 值 INSERT INTO t_user(username, password, real_name, role) VALUES('admin', '0192023a7bbd73250516f069df18b500', '系统管理员', 2);JDBC 连接参数是另一个高频报错点。写一个DBUtil工具类统一管理连接,URL 里显式指定时区,能省掉大量"时间差 8 小时"的排查时间:
public class DBUtil { // MySQL 8 必须带 serverTimezone,否则连接报时区错误 private static final String URL = "jdbc:mysql://localhost:3306/visitor_sys?useUnicode=true" + "&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new SQLException("驱动类未找到,请检查 mysql-connector-java 版本", e); } return DriverManager.getConnection(URL, USER, PASSWORD); } }URL 里的三个参数分别对应三件常踩的事:useUnicode=true配合characterEncoding=utf8保证中文不乱码;serverTimezone指定数据库所在时区,不写的话 MySQL 8 驱动会把它当作与本地时间相同而给出错误偏移,签离时间可能比签到时间还早半天;数据库端和连接端的COLLATE保持一致,避免联表排序时出现字符集不匹配。用户名密码不要写死在代码里交作业,报告里注明这是演示配置即可。
3. 核心业务实现:登记、签离、预约的 Java 代码怎么写才不被问倒
数据库定好后,代码其实只剩下三件事:登记时插入一条"在访"记录,签离时把同一行改成"已签离",预约时先生成"预约"状态再等人确认。把这三段写清楚,这个项目的核心就完成了。
3.1 登记接口:插入之前先做非空和手机号校验
登记页放在register.jsp,提交到VisitorRegisterServlet。Servlet 里先做参数校验再写库,是答辩时最值得讲的一段——很多同学的代码是前端校验完就直接插数据库,实际上前端校验可以被绕过,后端必须重复一遍。
@WebServlet("/visitor/register") public class VisitorRegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setContentType("application/json;charset=UTF-8"); String name = trim(req.getParameter("visitorName")); String phone = trim(req.getParameter("phone")); String company = trim(req.getParameter("company")); if (name.isEmpty()) { resp.getWriter().write("{\"code\":400,\"msg\":\"访客姓名不能为空\"}"); return; } // 只做手机号长度和首位的格式判断,不校验真伪 if (phone.length() != 11 || !phone.startsWith("1")) { resp.getWriter().write("{\"code\":400,\"msg\":\"手机号格式不正确\"}"); return; } try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO t_visitor" + "(visitor_name, phone, company, visit_target, visit_reason," + " status, arrive_time) VALUES(?,?,?,?,?,0,NOW())")) { ps.setString(1, name); ps.setString(2, phone); ps.setString(3, company); ps.setString(4, trim(req.getParameter("visitTarget"))); ps.setString(5, trim(req.getParameter("visitReason"))); int rows = ps.executeUpdate(); if (rows > 0) { resp.getWriter().write("{\"code\":200,\"msg\":\"登记成功\"}"); } } catch (SQLException e) { e.printStackTrace(); resp.getWriter().write("{\"code\":500,\"msg\":\"数据库异常\"}"); } } }这段代码有四个可以主动讲的点:为什么不拼接 SQL 而用PreparedStatement,因为参数化查询能挡住 SQL 注入,这是课程设计报告里必须写的一条;status直接在 INSERT 里写死 0,而不是先查一次再插入,避免多一次查询;NOW()由数据库生成时间,保证和报表里的DATE(arrive_time)使用同一时钟源;返回 JSON 而不是转发 JSP,让登记页用 jQuery 的$.post提交后根据 code 决定提示还是跳转,页面不用整体刷新。phone 的校验只做了长度和首位,接口文档里要注明"不做运营商真实性校验",这属于演示系统的合理边界。
3.2 签离接口:更新同一行而不是插入新记录
签离的错误实现是把一条新记录插入表里并把旧记录置为"已签离",这会产生两条源访客数据,报表统计会翻倍。正确的做法是对同一个id做行内更新,把status从 0 改成 1,同时写入leave_time:
@WebServlet("/visitor/signOut") public class VisitorSignOutServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id = Integer.parseInt(req.getParameter("id")); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement( "UPDATE t_visitor SET status = 1, leave_time = NOW() " + "WHERE id = ? AND status = 0")) { ps.setInt(1, id); int rows = ps.executeUpdate(); if (rows == 1) { resp.getWriter().write("{\"code\":200}"); } else { // rows 为 0 说明记录不存在或已经签离过 resp.getWriter().write( "{\"code\":400,\"msg\":\"记录不存在或已签离\"}"); } } catch (SQLException e) { resp.getWriter().write("{\"code\":500,\"msg\":\"数据库异常\"}"); } } }关键在WHERE id = ? AND status = 0。这个条件让更新天然具备幂等性:前端连点两次签离按钮,第一次把 status 改成 1 并写入 leave_time,第二次WHERE匹配不到任何行,rows是 0 而不是抛异常。这样既避免了重复签离,又不需要额外加一张防重表。答辩时老师常问"两个入口同时签离同一个人会怎样",回答这个条件就是答案。
3.3 预约与确认:用一张表里的 status 走完预约流程
预约场景下,访客先填预约表单,数据插入时status = 2、arrive_time存预计到访时间;前台在预约列表里点击"确认"时,把status从 2 改成 0,并把arrive_time更新为当前实际时间;如果爽约,则改成 3。整个过程不新增表,只在状态机上迁移:
String sql = "UPDATE t_visitor SET status = ?, arrive_time = ? WHERE id = ?"; // 确认: 参数为 (0, 当前时间, id) // 取消: 参数为 (3, 原值, id) // 这个接口不接受 status = 1,签离只能由签离接口完成这里需要说明为什么确认时改arrive_time:预约时存的是"预计到达时间",确认到访时应该把实际签到时间写进去,否则日报统计会把预约生成时间当成到访时间,统计口径就乱了。状态机的四个取值在前端下拉框里展示成中文文案时,用一个 Map 或 switch 转换即可,不要在前端写死数字。业务规则汇总如下:
| 前端动作 | status 变化 | arrive_time | leave_time |
|---|---|---|---|
| 现场登记 | 无 -> 0 | 当前时间 | 空 |
| 提交预约 | 无 -> 2 | 预计时间 | 空 |
| 确认预约到访 | 2 -> 0 | 覆盖为实际时间 | 空 |
| 取消预约 | 2 -> 3 | 不变 | 空 |
| 签离 | 0 -> 1 | 不变 | 当前时间 |
把这五条规则做成一张表写进报告,比写十页代码更能说明你理解了这个业务。实现层面再补一个约束:确认和取消按钮在 JSP 列表里要带status == 2的判断,避免状态错乱。
4. 分页查询与统计报表:把访客数据变成能进报告的结果
登记和签离是数据入口,报表才是决定这门课设上限的部分。老师跑完系统后会重点看查询页能不能筛条件、报表数字对不对,这两块正好对应数据库课里学的 WHERE 组合和 GROUP BY。
4.1 多条件查询 DAO:参数拼接与分页的配合
访客列表页要支持按被访人、来访日期范围、状态三个条件组合筛选。DAO 层用动态拼接 WHERE 的方式实现,注意每加一个条件都要对应一个?,参数按顺序 set:
public List<Visitor> query(String target, String startDate, String endDate, Integer status, int page, int pageSize) { StringBuilder sql = new StringBuilder( "SELECT id, visitor_name, phone, company, visit_target, status," + " arrive_time, leave_time FROM t_visitor WHERE 1=1"); List<Object> params = new ArrayList<>(); if (target != null && !target.isEmpty()) { sql.append(" AND visit_target LIKE ?"); params.add("%" + target + "%"); } if (startDate != null && !startDate.isEmpty()) { sql.append(" AND DATE(arrive_time) >= ?"); params.add(startDate); } if (endDate != null && !endDate.isEmpty()) { sql.append(" AND DATE(arrive_time) <= ?"); params.add(endDate); } if (status != null) { sql.append(" AND status = ?"); params.add(status); } sql.append(" ORDER BY arrive_time DESC LIMIT ?, ?"); params.add((page - 1) * pageSize); params.add(pageSize); // 执行 PreparedStatement,按 params 顺序 setObject 即可 }WHERE 1=1不是多余的写法,它让后续每个AND分支都不用判断"是不是第一个条件",代码可读性更好,执行计划也不受影响。分页用LIMIT ?, ?,第一个参数是偏移量(page - 1) * pageSize,第二个是每页条数,记得 set 的时候两个都得转成 int,因为 MySQL 的 LIMIT 不接受字符串参数。日期比较用DATE(arrive_time) >= ?而不是arrive_time >= ?,这样传2024-05-01也能匹配当天的任意时刻记录,不会被隐式类型转换坑到。
4.2 日报统计 SQL:按天分组后注入 ECharts 数据
报表页最常见的图表是"最近 7 天每日来访人次",对应 SQL 是:
SELECT DATE(arrive_time) AS visit_date, COUNT(*) AS total FROM t_visitor WHERE arrive_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND status IN (0, 1) GROUP BY DATE(arrive_time) ORDER BY visit_date;status IN (0, 1)把取消的预约排除在统计口径外,COUNT(*)统计的是来访人次而不是人数,同一个访客来两次算两笔,报告里要写明这个口径。Servlet 里把结果集包装成List<Map<String, Object>>,用 Jackson 或手拼字符串转成 JSON 返回,前端 ECharts 拿date和total两个数组直接画柱状图。日期缺失的某一天返回值为空,前端要补 0,而不是让图出现空洞。
4.3 CSV 导出:报告附录里的数据从哪来
课程设计报告要求放运行截图和数据说明,与其截图一堆列表页,不如做一个导出 CSV 的接口,把筛选条件下的数据原样导出,用 Excel 打开后可以直接贴进报告附录:
resp.setContentType("text/csv;charset=UTF-8"); resp.setHeader("Content-Disposition", "attachment;filename=visitor_" + System.currentTimeMillis() + ".csv"); resp.getWriter().write("\uFEFF"); // BOM,否则 Excel 打开中文会乱码 resp.getWriter().write("姓名,手机号,来访单位,被访人,状态,签到时间,签离时间\n"); while (rs.next()) { resp.getWriter().write(rs.getString("visitor_name") + "," + rs.getString("phone") + "," + rs.getString("company") + "," + rs.getString("visit_target") + "," + statusText(rs.getInt("status")) + "," + rs.getString("arrive_time") + "," + rs.getString("leave_time") + "\n"); }\uFEFF是 UTF-8 BOM,不加它 Excel 默认按 ANSI 编码读取,中文全部乱码,这是导出功能最常见的坑。字段里如果允许逗号或换行符,导出前要做转义;课程设计里可以在登记校验阶段就禁止特殊字符,把问题挡在源头。导出接口不需要单独的表,直接把 4.1 那个动态查询条件复用到导出 SQL 上即可。
5. 答辩演示检查清单:从跑通到能讲清楚的最小验收
系统能跑只是及格线,答辩环节老师更想听到你怎么解释设计决策。演示顺序要有剧本,先给结论:这个题目最值得讲的就是状态机设计和报表口径,把这两个讲透,20 分钟答辩时间足够。
5.1 演示路径与话术
按"登记 -> 列表 -> 签离 -> 报表 -> 导出"的顺序操作,每步配一句解释。登记时强调后端二次校验和 PreparedStatement 防注入;签离时强调WHERE status = 0的幂等性;报表时强调统计口径排除了取消状态。前台账号进系统只能看到登记页和签离列表,管理员账号才能看统计报表和导出按钮,这个权限差别用 Filter 实现,比在 JSP 里 if 判断更值得讲。
@WebFilter("/*") public class LoginFilter implements Filter { private static final Set<String> WHITE_LIST = Set.of("/login.jsp", "/loginServlet"); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); if (WHITE_LIST.contains(request.getRequestURI()) || (session != null && session.getAttribute("user") != null)) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } } }演示前把浏览器缓存清空,开启控制台 Network 面板,这样每次请求的 200/302 状态码都能被老师看到,比口头说"跳转了"更有说服力。时间字段全部用数据库的 NOW() 生成,演示时的签到时间一定和系统当前时间一致,不会出现"签离时间早于签到时间"这种损害信任度的低级错误。
5.2 三个最容易暴露的坑,演示前必须自查
| 检查点 | 自查方法 | 修复方式 |
|---|---|---|
| 中文乱码 | 登记一个含中文的单位名 | JSP 顶部声明 pageEncoding,Servlet 里 req.setCharacterEncoding |
| 重复点击 | 签离按钮快速点两下 | WHERE 条件带 status=0,观察第二次返回值 |
| 时间偏移 | 看签到和签离时间差值 | JDBC URL 加 serverTimezone=Asia/Shanghai |
最后一个技巧:把数据库初始化 SQL 和账号密码写进报告附录,评卷老师导入数据库时能一次成功,这份印象分值得花五分钟。源码目录按src、web、sql、docs四层整理,报告里的 ER 图、用例图、表结构说明和源码注释保持一致的命名,这套交付节奏不仅适用于访客登记系统,也是后续所有课程设计的通用模板。
本文还有配套的精品资源,点击获取