首页 / 资讯中心 / 文章详情

Java Web期末项目实战:城市印象系统从选型到答辩全攻略

Java Web期末项目实战:城市印象系统从选型到答辩全攻略 ★ FEATURED ARTICLE
简介这份资源是面向具备Java与Web开发基础的学生、开发者及项目管理者的期末项目设计文档围绕「城市印象系统」展开帮助读者完成从需求分析到系统实现的完整开发流程。文档以MVC模式规划了城市概览、地图导航、景点推荐、文化体验、旅游指南、用户评论评分、实时天气、社交分享、旅行日志、线下活动与特色产品购买等模块并给出数据库设计与设计报告格式规范适合练习架构设计与团队协作。压缩包内共1个docx文件约132KB集中承载功能选择、框架结构、数据库保存与界面美化等要求便于直接参考撰写设计报告。目前已有181人学习读者可据此掌握Java Web开发方法、MVC落地思路与项目分工管理并对照格式要求完成规范文档。1. 城市印象系统一个Java Web期末项目到底要落地哪些东西每年到了期末总有一批同学被“基于Java的Web程序设计”这门课的大作业卡住。题目听起来很文艺——城市印象系统但真打开IDE那一刻脑子里全是问号这玩意儿到底要做成什么样是做个城市介绍页面还是要带后台管理数据库几张表前端要不要框架答辩的时候老师会问什么我带的几届学生里做这个题目的翻车率其实不低。不是因为技术有多难而是因为大多数人一开始就没想清楚“城市印象”这四个字在技术层面到底对应什么。说白了它就是一个以城市为主题的内容展示与管理系统前台让用户浏览城市信息、看图文介绍、可能还要打分或留言后台让管理员增删改查城市数据、管理用户评论。技术栈通常就是Servlet/JSP或者Spring Boot加MyBatis再加MySQL前端用HTML/CSS/JS或者套个Bootstrap。适合谁适合正在学Java Web、需要在一个学期内交出可运行、可演示、代码量可控的期末项目的同学。这篇文章就按我实际带项目的路径把选型、建表、编码、排错、答辩要点全部拆开讲让你少走弯路。2. 技术选型与数据库设计别一上来就堆框架2.1 Servlet/JSP还是Spring Boot期末项目怎么选这是第一个分叉口。很多同学听说Spring Boot“高级”上来就搞结果光是配依赖和解决版本冲突就耗掉一周。我的建议很直接看你课程教的是什么。如果课程主要讲Servlet、JSP、JDBC那就老老实实用ServletJSPJDBC老师看的是你对请求响应流程的理解不是你会不会用注解。如果课程后半段讲了Spring MVC或Spring Boot那用Spring BootMyBatis更省事配置少、启动快。具体判断标准可以看这张表对比项ServletJSPJDBCSpring BootMyBatis学习成本低课程通常已覆盖中需要理解IoC和Mapper代码量较大每个功能要写Servlet较小ControllerServiceMapper调试难度低流程直观中出错时堆栈较深答辩加分体现基础扎实体现工程化意识适合场景课程只讲到Servlet课程涉及Spring我一般会让学生先花半天时间把两种方案的最小可运行Demo各跑一个哪个跑通了用哪个。不要在这个环节纠结超过一天期末项目的时间要留给业务逻辑。2.2 城市印象系统的数据库表怎么设计数据库是整个系统的地基。城市印象系统最少需要四张表城市表、城市分类表、用户表、评论表。如果还要做图片轮播再加一张城市图片表。下面是我常用的建表SQL直接可以在MySQL 5.7或8.0里执行-- 城市分类表比如“历史名城”“海滨城市”“美食之都” CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort_order INT DEFAULT 0 COMMENT 排序权重 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 城市表核心数据 CREATE TABLE city ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 城市名称, category_id INT COMMENT 所属分类, description TEXT COMMENT 城市简介, cover_img VARCHAR(255) COMMENT 封面图路径, view_count INT DEFAULT 0 COMMENT 浏览量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表简单起见只存用户名密码 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL COMMENT 存MD5值别存明文, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评论表用户对城市的留言 CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, city_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (city_id) REFERENCES city(id), FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数要说明。字符集用utf8mb4而不是utf8因为城市简介里可能出现生僻字或emoji。外键约束在期末项目里建议加上答辩时老师看到外键会认为你理解关系型数据库的完整性。view_count字段用来做简单的热度排序不需要额外建表。password字段长度给100是为了存MD5加密后的32位字符串留余量。2.3 项目目录结构怎么组织才不乱不管用哪种技术栈目录结构要清晰。Servlet方案下我一般这样分src/ com.cityimpression.dao/ -- 数据库操作类 com.cityimpression.entity/ -- 实体类和表对应 com.cityimpression.service/ -- 业务逻辑 com.cityimpression.servlet/ -- 控制器 com.cityimpression.util/ -- 工具类如DBUtil、MD5Util WebContent/ css/ js/ images/ WEB-INF/web.xml index.jsp cityList.jsp cityDetail.jsp login.jsp admin/Spring Boot方案下更简单按controller、service、mapper、entity分包即可。关键原则是一个功能模块的代码放在一起不要按技术层散落到各处。比如“城市管理”相关的Servlet、Service、DAO都放在city子包下找起来快答辩演示时也方便讲。3. 核心功能编码从城市列表到评论提交的完整链路3.1 用JDBC工具类打通数据库连接不管后面用什么框架理解JDBC连接池是基础。期末项目里不需要上Druid或HikariCP手写一个简单的DBUtil就够。下面这个工具类负责加载驱动、获取连接、关闭资源package com.cityimpression.util; import java.sql.*; public class DBUtil { // 数据库配置改成你自己的库名和密码 private static final String URL jdbc:mysql://localhost:3306/city_impression?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD your_password; static { try { // MySQL 8.0驱动类名是com.mysql.cj.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(数据库驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭资源顺序不能错ResultSet - Statement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明静态代码块保证驱动只加载一次。URL里的serverTimezone参数在MySQL 8.0下必须加否则会报时区错误这是血泪经验。characterEncoding设为utf8mb4防止中文乱码。close方法的关闭顺序不能颠倒先关结果集再关语句再关连接否则可能抛异常。参数怎么改URL中的city_impression换成你的数据库名USER和PASSWORD换成你的MySQL账号。如果MySQL是5.7版本驱动类名可以保留com.mysql.jdbc.Driver但建议统一用8.0驱动。3.2 城市列表分页查询的Servlet实现城市列表是前台最核心的页面。数据量不大时可以直接全查但为了体现分页思想我建议加上分页。下面是一个CityListServlet的核心代码package com.cityimpression.servlet; import com.cityimpression.entity.City; import com.cityimpression.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.List; WebServlet(/cityList) public class CityListServlet extends HttpServlet { private static final int PAGE_SIZE 6; // 每页显示6条 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 获取当前页码默认第1页 int page 1; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { try { page Integer.parseInt(pageStr); } catch (NumberFormatException ignored) {} } if (page 1) page 1; ListCity cityList new ArrayList(); int totalCount 0; String countSql SELECT COUNT(*) FROM city; String listSql SELECT c.*, cat.name AS categoryName FROM city c LEFT JOIN category cat ON c.category_id cat.id ORDER BY c.view_count DESC LIMIT ? OFFSET ?; try (Connection conn DBUtil.getConnection()) { // 先查总数 try (PreparedStatement ps conn.prepareStatement(countSql); ResultSet rs ps.executeQuery()) { if (rs.next()) totalCount rs.getInt(1); } // 再查当前页数据 try (PreparedStatement ps conn.prepareStatement(listSql)) { ps.setInt(1, PAGE_SIZE); ps.setInt(2, (page - 1) * PAGE_SIZE); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { City city new City(); city.setId(rs.getInt(id)); city.setName(rs.getString(name)); city.setDescription(rs.getString(description)); city.setCoverImg(rs.getString(cover_img)); city.setViewCount(rs.getInt(view_count)); city.setCategoryName(rs.getString(categoryName)); cityList.add(city); } } } } catch (SQLException e) { e.printStackTrace(); req.setAttribute(error, 数据查询失败请稍后重试); } int totalPages (int) Math.ceil((double) totalCount / PAGE_SIZE); req.setAttribute(cityList, cityList); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/cityList.jsp).forward(req, resp); } }逻辑说明先查总数再查当前页这是分页的标准做法。LIMIT和OFFSET的参数用PreparedStatement设置防止SQL注入。LEFT JOIN分类表是为了在列表页显示分类名称避免在JSP里再查一次。try-with-resources语法自动关闭资源比手动close更安全。参数说明PAGE_SIZE设为6是考虑到页面布局每行3个卡片、共2行。page参数从URL获取比如cityList?page2。如果页码超出范围可以在JSP里做判断也可以在这里加边界检查。3.3 评论提交与防重复刷屏评论功能看似简单但有两个坑一是没登录也能提交二是同一用户短时间重复提交。下面这段代码在插入前做了登录校验和时间间隔检查WebServlet(/addComment) public class AddCommentServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 1. 登录校验 Object userObj req.getSession().getAttribute(user); if (userObj null) { resp.sendRedirect(login.jsp?msg请先登录); return; } int userId ((User) userObj).getId(); int cityId Integer.parseInt(req.getParameter(cityId)); String content req.getParameter(content); // 2. 内容非空和长度校验 if (content null || content.trim().isEmpty() || content.length() 500) { resp.sendRedirect(cityDetail?id cityId msg评论内容不合法); return; } // 3. 防重复检查该用户对该城市最近一条评论的时间 String checkSql SELECT create_time FROM comment WHERE user_id? AND city_id? ORDER BY create_time DESC LIMIT 1; String insertSql INSERT INTO comment(city_id, user_id, content) VALUES(?,?,?); try (Connection conn DBUtil.getConnection()) { try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, userId); ps.setInt(2, cityId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { long lastTime rs.getTimestamp(create_time).getTime(); // 30秒内不允许重复评论 if (System.currentTimeMillis() - lastTime 30_000) { resp.sendRedirect(cityDetail?id cityId msg评论太频繁); return; } } } } try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, cityId); ps.setInt(2, userId); ps.setString(3, content.trim()); ps.executeUpdate(); } } catch (SQLException e) { e.printStackTrace(); } resp.sendRedirect(cityDetail?id cityId); } }逻辑说明登录校验放在最前面未登录直接跳转登录页。内容校验包括非空和长度上限。防重复用“查最近一条评论时间”的方式30秒内不允许再评。这个阈值可以根据演示需要调整比如改成10秒。参数说明30_000是毫秒数代表30秒。content长度限制500和数据库字段一致。cityId从表单隐藏域传入。4. 避坑与排查那些让项目跑不起来的常见问题4.1 中文乱码从请求到响应到数据库的三层排查现象页面显示的城市名是“æå¤´”这样的乱码或者提交的评论存进数据库变成问号。原因乱码可能出现在三个环节——请求编码、响应编码、数据库连接编码。很多人只改了JSP的pageEncoding忽略了POST请求体和数据库连接串。解决第一JSP文件头部确保是% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。第二在Servlet的doPost第一行加req.setCharacterEncoding(UTF-8)doGet在Tomcat 8以后默认UTF-8但保险起见也加。第三数据库连接URL里加characterEncodingutf8mb4。第四建表时指定DEFAULT CHARSETutf8mb4。四层都对齐乱码基本消失。4.2 数据库连接报“Public Key Retrieval is not allowed”现象MySQL 8.0下启动项目报错java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。原因MySQL 8.0默认使用caching_sha2_password认证插件JDBC连接时如果不允许公钥检索就会失败。解决在连接URL后面加allowPublicKeyRetrievaltrue。完整URL类似jdbc:mysql://localhost:3306/city_impression?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrue。这个参数只影响本地开发答辩演示时没问题。4.3 图片上传后访问404现象后台添加城市时上传了封面图数据库里存了路径但前台img标签加载不出来。原因图片存到了项目外的目录或者存到了WebContent下但重启后丢失或者路径写的是绝对路径而Tomcat部署后路径变了。解决期末项目最简单的做法是把图片存到WebContent/images/city/目录下数据库存相对路径如images/city/beijing.jpgJSP里用${pageContext.request.contextPath}/images/city/beijing.jpg拼接。如果存到项目外需要在Tomcat里配置虚拟路径映射但期末项目不建议这么复杂。另外注意上传目录要有写权限。4.4 分页查询页码越界导致空列表现象在地址栏手动输入cityList?page999页面显示空白但没有报错。原因OFFSET计算后超出了总记录数SQL返回空结果集JSP遍历时没有数据。解决在Servlet里计算totalPages后如果page大于totalPages把page重置为totalPages。或者在JSP里判断currentPage totalPages时显示“没有更多数据”。更优雅的做法是在SQL层用LIMIT ? OFFSET ?之前先校验page范围。4.5 答辩时被问“你的系统有什么技术难点”怎么答现象演示很流畅但老师一问“你觉得难点在哪”就卡壳。原因只关注了功能实现没有提炼技术问题。解决提前准备三个点。第一分页查询的SQL优化说明为什么用LIMIT而不是全查再截取。第二防SQL注入说明PreparedStatement的原理。第三会话管理说明Session和Cookie的区别以及登录状态怎么保持。每个点准备一句话解释加一句代码位置答辩时直接翻到对应代码。5. 进阶技巧让城市印象系统在答辩时多拿几分5.1 用过滤器统一处理编码和登录拦截如果每个Servlet都写一遍req.setCharacterEncoding和登录校验代码重复且容易漏。用Filter可以一次性解决WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 放行登录页、登录接口、静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(req, resp); return; } // 后台页面需要管理员权限 if (uri.contains(/admin/)) { Object user request.getSession().getAttribute(user); if (user null || ((User) user).getRole() ! 1) { response.sendRedirect(request.getContextPath() /login.jsp); return; } } chain.doFilter(req, resp); } }逻辑说明过滤器拦截所有请求统一设置编码。通过URI判断放行静态资源和登录相关路径。后台路径检查Session中的用户角色。这样Servlet里就不用重复写编码和权限代码了。参数说明WebFilter(/*)表示拦截所有路径。放行列表根据你的项目结构调整。角色判断里role1代表管理员。5.2 用AJAX做无刷新评论提升演示效果答辩演示时提交评论后整页刷新会打断节奏。用AJAX提交评论区局部更新观感更好。前端代码// 提交评论 function submitComment(cityId) { var content document.getElementById(commentContent).value.trim(); if (!content) { alert(请输入评论内容); return; } fetch(addComment, { method: POST, headers: {Content-Type: application/x-www-form-urlencoded}, body: cityId cityId content encodeURIComponent(content) }).then(function(resp) { if (resp.redirected) { // 被重定向说明未登录或参数不合法 window.location.href resp.url; } else { // 刷新评论列表 loadComments(cityId); document.getElementById(commentContent).value ; } }); }逻辑说明用fetch发送POST请求Content-Type设为表单格式。encodeURIComponent防止内容里的特殊字符截断参数。如果后端返回重定向说明校验未通过直接跳转。否则重新加载评论列表并清空输入框。参数说明cityId从页面隐藏域或JSP变量获取。loadComments是另一个AJAX函数负责查询并渲染评论列表。5.3 答辩前必做的五项自检第一换一台电脑部署确认数据库脚本能完整执行、项目能启动。第二把所有硬编码的数据库密码改成配置文件读取至少说明你知道配置分离。第三准备一份数据备份SQL演示前导入避免现场数据被改乱。第四把浏览器控制台和Tomcat日志打开确认没有红色报错。第五录一段3分钟的演示视频作为备份万一现场环境出问题可以播放。我自己的习惯是答辩前一晚把整个流程走三遍启动、登录、浏览、评论、后台增删改查、退出。每遍记录耗时和卡顿点。第三遍如果能在5分钟内走完且无报错基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站