简介基于 JavaJSP 的洛阳旅游管理系统是一套面向毕业设计场景的完整源码与数据库资源适合计算机相关专业学生用于课程设计、毕设参考或项目二次开发。系统采用 JSPjQueryServletJDBC 的经典技术组合涵盖前台门户展示与后台管理两端包含景点、资讯、留言、管理员、系统设置等模块并支持景点图片上传、名称模糊搜索、轮播图展示及天气信息展示等细节功能。资源包共 349 个文件以 JSP 页面、Java 类、JS 脚本、CSS 样式为主辅以 SQL 数据库脚本、图片与字体文件及少量配置文件整体约 30.18MB结构清晰导入 IDEA 并配置 MySQL 5.7、Tomcat 9、JDK 1.8 环境即可运行。目前已有 33 人浏览学习适合作为传统 Java Web 开发从入门到综合实践的学习样板可帮助读者快速理解 ServletJDBC 的请求处理流程、前后台功能组织方式以及旅游类网站常见业务的实现思路。1. 基于 javajsp 的洛阳旅游管理系统课程设计的“最后一公里”也是中型 Servlet 项目的缩影如果你在毕业设计或 Java 课程作业里拿到“基于 javajsp 的洛阳旅游管理系统源码数据库”第一反应大概是又是一套老掉牙的增删改查。但真把这份源码打开你会发现它不像教材里那种只能跑通一个页面的 demo——它有景点、线路、订单、登录、角色权限数据库脚本里有上百条表结构和初始数据。这类项目恰恰是检验 Java Web 基本功的试金石Servlet 生命周期、JSP 标签库、JDBC 连接管理、Session 会话全都要串起来才能跑得动。适合两类人一是课程设计想交一份“能演示、能答辩”的完整系统的学生二是准备 Java 面试前想找一个真实项目练手、能讲清楚每一层在干什么的求职者。这篇文章就按我实际做这类系统的顺序——先看数据库、再理后端分层、然后调权限和会话、最后聊部署排查——把整个落地路径拆给你。2. 把 145 条表结构读薄洛阳旅游的实体关系与数据库落库拿到源码包第一步不是急着往 IDE 里导而是先把数据库脚本从头到尾读一遍。这个项目叫“洛阳旅游管理系统”核心数据逃不出三类用户、景点、订单。你看到的“145”通常指数据库脚本的版本编号或者表/初始化数据行数不管哪种它都说明这套库不是玩具里面起码有角色数据、旅游线路、景区分类、评论留言这类真实业务表。先读懂表关系再动手改代码后面所有坑都会少一半。2.1 登录与角色user 表为什么要有 role 字段而不是三张表很多教材项目会把“管理员”和“普通用户”拆成两张表甚至三张表。但这套系统更常见的做法是在同一张 user 表里放一个 role 字段用 int 或者 varchar 区分角色。为什么这样设计因为洛阳旅游管理系统的权限层级并不复杂——无非是管理员维护景点和订单普通用户浏览景点、下单、查看自己的订单。两种角色共享几乎相同的字段用户名、密码、手机号、注册时间。拆成多张表只会让登录查询变成 union反而增加麻烦。看表结构时重点盯三个地方。第一密码字段是不是明文。如果脚本里初始用户的密码是 123456 这种直接可读的字符串说明项目年份较早或课程设计取向复制到自己项目里一定要改成 MD5 或 BCrypt 加密存储。第二role 字段的取值范围是 0/1 还是 1/2这决定了后端的权限判断怎么写。第三唯一索引在哪个字段上一般应该是 username否则注册接口容易被重复数据搞崩。这三处看清了登录模块的改造方向也就定了。2.2 景点、分类与订单一对多关系怎么建外键旅游系统的业务核心是“景点”和“订单”。景点表一般包含景点名称、所在区域洛阳下面有老城、涧西、洛龙等、简介、图片路径、门票价格、开放时间这些字段。图片路径这一项要特别留意JSP 项目里图片有两种存法一种是上传到服务器磁盘数据库只存相对路径另一种是直接把图片以 Base64 或二进制塞进数据库。前者是主流后者会让数据库体积暴涨而且页面加载慢。如果脚本里用的是 longblob 类型字段存图片建议你改成存路径这是这类项目最常见的改造点。订单表必然通过外键关联用户和景点order 表里有 user_id 和 scenic_id 两个外键字段。查询的时候用 JOIN 把用户名和景点名拼出来。看懂这张表的时候顺带留意一下订单状态字段——是 int 还是 varchar取值有哪几种前端下拉框和后端逻辑是否一致。很多项目翻车就翻在状态字段取值不统一数据库里 0 表示未支付代码里却用 1 判断。2.3 建库建表 SQLutf8mb4、时间戳与索引拿到 sql 文件后先用文本编辑器打开看建库语句。字符集是不是 utf8mb4 很关键如果是老旧的 utf8插入“洛阳”“龙门石窟”没问题但一旦遇到 emoji 或者生僻字就会报 Incorrect string value。更坑的是如果表结构是 utf8 而连接串里写了 characterEncodingutf-8数据库会直接拒绝带特殊字符的写入。改法是把整个 sql 文件里的 CHARSETutf8 全局替换成 CHARSETutf8mb4再把连接串加上 useUnicodetruecharacterEncodingutf-8。时间字段建议统一用 datetime 而不是 timestamp。timestamp 的范围只能到 2038 年而且受时区影响。如果用 datetimeJava 侧的 java.util.Date 映射不会有任何意外。索引方面除了主键至少给外键字段 user_id、scenic_id 和订单表的 create_time 加上普通索引。课程设计的数据量不大不加也能跑但这属于面试时能讲的点——你主动加了索引并且能说出“覆盖高频查询的 where 条件”比背十遍索引原理更有说服力。-- 以常见的三张核心表为例展示洛阳旅游系统的库表设计骨架 CREATE DATABASE IF NOT EXISTS luoyang_tour DEFAULT CHARACTER SET utf8mb4; USE luoyang_tour; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, role INT NOT NULL DEFAULT 0, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_scenic ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, district VARCHAR(50), price DECIMAL(10,2), image_path VARCHAR(255), intro TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, scenic_id INT NOT NULL, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, status INT DEFAULT 0, KEY idx_user (user_id), KEY idx_scenic (scenic_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_order_scenic FOREIGN KEY (scenic_id) REFERENCES t_scenic(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里 KEY idx_user 和 KEY idx_scenic 就是给外键查询建索引JOIN 的时候不会全表扫。外键约束在课程设计里可以保留它能让数据库层面保证不产生孤儿订单但如果你打算把这个项目改造成 Spring Boot MyBatis 再上线外键通常会被去掉改成应用层校验原因是分布式和高并发下外键会影响插入性能。另外注意 t_user 的 role 字段默认值是 0意思是一般用户管理员由脚本里的 INSERT 语句单独插入 role1 的记录。导入数据库我用命令行而不是 Navicat 的导入向导因为命令行能看到完整的报错信息。命令是 mysql -uroot -p luoyang_tour.sql如果报错就去看报错行号对应的 SQL。很多时候是 sql 文件里带了 DROP DATABASE 语句在别人的机器上执行会直接删库导入前把这类危险语句注释掉——这是血泪经验。3. 在 Servlet JSP 里实现增删改查三层结构、请求流转与最小可运行代码数据库落好以后开始碰 Java 代码。这套系统的后端骨架是经典三层Servlet 接收请求、Service 处理业务、DAO 操作数据库。JSP 只做展示。很多新手把 JDBC 代码直接写在 Servlet 里一页代码几百行跑通是能跑通但答辩或者面试的时候一问“订单状态为什么在两个地方各写了一份判断”就答不上来。分层不是做样子而是让每一层能独立修改。3.1 实体类、DAO、Service 的分层边界打开源码包先看包结构。常见的命名是 com.luoyang.entity、com.luoyang.dao、com.luoyang.service、com.luoyang.servlet对应实体、数据访问、业务逻辑、控制器四层。实体类就是数据库表的 Java 映射字段名和表列名一一对应用 int 对应 INT用 BigDecimal 对应 DECIMAL用 java.util.Date 对应 DATETIME。不要在实体类里写业务方法它就是单纯的数据容器。DAO 层只做一件事写 SQL。查询景点列表、按 id 查订单、插入新用户这些方法里只有 JDBC 模板代码——获取连接、预编译、执行、解析 ResultSet、关闭资源。Service 层调 DAO 的接口处理业务规则比如下单时要判断用户是否登录、景点是否存在、库存门票余量够不够。Servlet 层只负责两件事——从 request 里取参数把结果 setAttribute 到 request 或者 session然后 forward 或者 redirect 到 JSP。这个边界一旦清晰你改任何一处都不会牵连到另外两层。以“景点管理”为例后端至少要这一组方法list() 查全部、getById() 查单个、insert() 新增、update() 修改、delete() 按 id 删除。有的项目还会把分页也塞进 DAO用 LIMIT offset, size 实现。课程设计里不分页也能交差但面试官如果问“数据量大了你怎么优化”你能说出分页和索引就已经超过了大部分应届生。3.2 景点管理增删改查一个 Servlet 处理一类资源的写法最省事的 Servlet 设计是一个 ScenicServlet用不同的参数或者请求路径区分动作。常见做法是给 Servlet 配置多个 URL 映射或者在一个 doGet/doPost 里用 action 参数分派。对于课程设计这个体量action 参数分派最简单也最好讲清楚。WebServlet(/scenic) public class ScenicServlet extends HttpServlet { private ScenicService scenicService new ScenicService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (list.equals(action)) { ListScenic list scenicService.list(); req.setAttribute(scenicList, list); req.getRequestDispatcher(/scenic/list.jsp).forward(req, resp); } else if (delete.equals(action)) { int id Integer.parseInt(req.getParameter(id)); scenicService.deleteById(id); resp.sendRedirect(req.getContextPath() /scenic?actionlist); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (add.equals(action)) { Scenic scenic new Scenic(); scenic.setName(req.getParameter(name)); scenic.setDistrict(req.getParameter(district)); scenic.setPrice(new BigDecimal(req.getParameter(price))); scenic.setIntro(req.getParameter(intro)); scenicService.insert(scenic); resp.sendRedirect(req.getContextPath() /scenic?actionlist); } } }代码有两点要说清楚。第一req.setCharacterEncoding(UTF-8) 必须放在读取任何参数之前否则 POST 提交的中文直接乱码。第二新增或删除成功后跳转用的是 sendRedirect 而不是 forward这是为了避免表单重复提交——用户按 F5 刷新时如果上次请求是 forward 转发浏览器会重新提交一次表单订单或者景点记录就会多出一条。改成重定向之后地址栏变成列表页的 URLF5 刷新只是重新查列表不会重复写库。这个 Servlet 里没有写任何 JDBC 代码全部委托给 ScenicService。这就是分层的意义——如果你要把数据库连接池从 DriverManager 换成 Druid只需改 DAO 层Servlet 和 JSP 一行都不用动。3.3 JSP 页面怎么拿数据JSTL 与 EL 的配合JSP 里最忌讳写大段 Java 代码也就是 Scriptlet。项目里如果看到 % ... % 这种块尽量换成 JSTL 标签。原因很实际JSP 页面里混 Java 代码页面设计师没法改样式而且 Java 代码在 JSP 里出了异常报错信息非常难看。EL 表达式负责取值JSTL 的 c:forEach 负责循环两者配合就能搞定 90% 的列表页面。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table border1 tr th景点名称/thth所在区域/thth门票价格/thth操作/th /tr c:forEach items${scenicList} varscenic tr td${scenic.name}/td td${scenic.district}/td tdfmt:formatNumber value${scenic.price} typecurrency//td td a href${pageContext.request.contextPath}/scenic?actioneditid${scenic.id}编辑/a a href${pageContext.request.contextPath}/scenic?actiondeleteid${scenic.id} onclickreturn confirm(确定删除吗)删除/a /td /tr /c:forEach /table这段 JSP 有两点值得学习。第一href 里拼的是 ${pageContext.request.contextPath} 而不是写死的项目名这样把项目改个名字或者部署到别的容器链接不会断。第二删除按钮加了 onclick confirm 确认弹窗这是一层前端拦截防止手滑误删。但你要明白这只是用户体验层面的保护不是安全层面的——攻击者可以直接构造一个 /scenic?actiondeleteid3 的地址来删除数据所以后端 servlet 里必须做管理员角色校验不能只看前端有没有按钮。格式化价格用了 fmt:formatNumber这是 JSTL 的格式化标签避免出现 12.0 这种不好看的显示。如果项目里没引入 JSTL 的 jar 包页面会报 500 错误报错信息是“Unable to find tag library”。把 jstl-1.2.jar 和 standard.jar 放进 WEB-INF/lib 就能解决。这也是老 JSP 项目最容易在换个环境后原地炸裂的点。4. 把登录和会话管起来过滤器、Session 与角色权限洛阳旅游管理系统既然有用户和管理员两种角色登录和权限就是必须能讲清楚的部分。面试问“你怎么实现登录拦截”时答“每个 Servlet 里都写一遍判断登录”当然不行会被追问“那新加一个 Servlet 你忘了写怎么办”。正确方案是过滤器Web 容器在请求到达 Servlet 之前先过一遍 Filter登录校验在那里统一做掉。4.1 登录校验的过滤链LoginFilter 怎么写过滤器在 web.xml 里配置或者在 Servlet 3.0 用 WebFilter 注解。它的执行时机是请求进入容器后先经过 Filter再进入 Servlet。所以只要在 Filter 里发现 session 里没有登录用户就直接重定向到登录页不给 Servlet 任何执行机会。WebFilter(/*) public class LoginFilter implements Filter { 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); String uri request.getRequestURI(); boolean isLoginPage uri.endsWith(login.jsp) || uri.endsWith(LoginServlet) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/); if (isLoginPage || (session ! null session.getAttribute(currentUser) ! null)) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }注意 getSession(false) 的 false 参数意思是“如果当前没有 session 就返回 null而不是新建一个”。为什么不直接 getSession()因为如果一个匿名用户访问登录页你给他新建一个 session 纯属浪费空间而且会让“session 里有用户”这个判断失效。另外静态资源 css/js/images 必须放行否则页面上所有样式和图片全挂——这是新手写过滤器最容易翻车的地方症状是登录页能打开但白花花一片没有样式。登录成功后的动作是把用户对象塞进 sessionsession.setAttribute(currentUser, user)。后续 JSP 里用 ${sessionScope.currentUser.username} 就能显示登录用户名。退出登录则是 session.invalidate() 加上重定向回登录页。invalidate 会把 session 整个销毁这样攻击者拿到的 jsessionid 就彻底失效了。4.2 权限控制普通用户和管理员的入口差别登录拦截只能解决“是否登录”解决不了“谁能干什么”。普通用户和管理员看到的操作按钮应该不同普通用户能下单、查看自己的订单管理员能删景点、改价格、查所有订单。这一步的实现分两层。前端层面在 JSP 里判断当前登录用户的 role 属性决定要不要渲染删除按钮这样普通用户界面上看不到危险操作。后端层面在 Servlet 里再校验一次 role防止有人绕过界面直接构造请求。// 在后端删除景点的 Servlet 方法里必须检查角色 HttpSession session request.getSession(false); User currentUser (User) session.getAttribute(currentUser); if (currentUser null || currentUser.getRole() ! 1) { response.sendRedirect(request.getContextPath() /login.jsp); return; }这 5 行代码就是权限控制的底线。前端隐藏按钮是体验问题后端检查才是安全问题。课程设计可能没人攻击你但面试聊到“越权漏洞”时你能主动说出“前端隐藏不等于安全后端每个写操作都要校验角色”这个回答价值很高。更讲究一点的做法是把这类权限判断抽到一个工具类或者再写一个 AdminCheckFilter避免每个 Servlet 里重复粘贴这段代码。4.3 会话控制细节Session 超时与并发登录Tomcat 里 session 默认超时时间是 30 分钟web.xml 里可以改。课程设计一般不用动但你要知道在哪里配置browser session 超时用 下的 单位是分钟。还有一个坑如果项目里存在两个 Tomcat 实例共用同一个应用但没配会话保持用户登录后下一次请求打到另一个实例上session 就丢了表现为“刚登录就跳回登录页”。课程设计用单机部署不会遇到但这是分布式会话的引子——面试时能接住这个话题就行。另一个常见问题是点击浏览器后退按钮回到登录前的页面。这不一定是漏洞因为 JSP 是服务器渲染的后退显示的可能是浏览器缓存的静态页面。要严格控制的话在敏感页面加 no-cache 响应头。课程设计里不做也行答辩时如果被问到说明你已经想过这个问题答“通过过滤器对未登录请求做重定向避免访问受保护资源”就足够。5. 踩坑记录与排查手册部署、乱码、Tomcat 路径和数据库连接这一章把我在 JSP 项目上见过最多的四类问题写成排查手册。每条按“现象 → 原因 → 解决”的顺序你照着对照就行。这些问题每一个我都踩过写出来免得你再走一遍。5.1 部署到 Tomcat 后访问 404Context Path 对不上现象代码在 IDE 里按 CtrlF11 能跑浏览器也能打开登录页但把 war 包丢到 Tomcat 的 webapps 下输入 http://localhost:8080/项目名 却 404。原因IDE 运行时的访问路径是 IDE 配置的上下文路径可能叫 /luoyang_tour而 war 包解压出来的目录名是另一个名字比如 luoyang_tour_145。浏览器地址栏里访问的路径和后端 redirect 的路径对不上自然找不到资源。还有一种是 JSP 页面里写死了 /luoyang_tour/login.jsp换到别的部署名就全断。解决一是部署前统一项目名war 包名字改成你要的上下文路径。二是把 JSP 里的所有写死路径替换成 ${pageContext.request.contextPath} 拼接。三是在 Tomcat 的 conf/server.xml 里给 Host 配 Context 的 path 属性——但不建议这样做因为把路径写死在 server.xml 里迁移到别的服务器又得改。5.2 中文乱码JSP 页面、请求参数和数据库三层查现象登录页输入“张三”提交后页面上显示“å¼ ä¸‰”。数据库里查出来也是乱码或者插入直接报错。原因三层有一个地方字符集不一致就会乱。第一层 JSP 页面本身没指定编码或者编码是 ISO-8859-1第二层 Servlet 读取请求参数时没设置 UTF-8第三层数据库表是 utf8 或 latin1。三者只要有一个掉链子中文就废了。解决JSP 第一行写 % page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %。Servlet 里在读取任何参数之前调用 request.setCharacterEncoding(UTF-8)POST 请求这个设置才有效GET 请求的参数在 URL 上Tomcat 8 以上版本默认 UTF-8 还好老版本需要在 server.xml 的 Connector 上加 URIEncodingUTF-8。数据库层面建表时指定 utf8mb4JDBC 连接串加 useUnicodetruecharacterEncodingutf-8。你不想每次都在代码里手动 setCharacterEncoding可以写一个 EncodingFilter 统一设置效果一样。5.3 启动时报数据库连接失败驱动、连接串和密码三件套现象Tomcat 启动时控制台刷出一堆 SQLExceptionAccess denied for user rootlocalhost或者 ClassNotFoundException: com.mysql.jdbc.Driver。原因前者是用户名密码不对。很多课程设计项目为了保证能跑通会在代码里写死 root/123456但你的 MySQL 密码不是这个。后者是缺驱动 jar 包——JSP 项目要手动把 mysql-connector-java 的 jar 放进 WEB-INF/libMaven 项目则要在 pom.xml 里加依赖如果 jar 没打包进 war部署到别的机器上就报这个错。解决先确认 MySQL 密码然后全局搜 jdbc:mysql:// 找连接串改密码。驱动 jar 版本要跟 MySQL 版本匹配MySQL 5.x 用 5.1.49MySQL 8.x 用 8.0.33连接串写法也略有差异——8.x 需要加 serverTimezoneAsia/Shanghai否则会报 CST 时区错误。这个时区错误很多人第一次遇到会懵其实是 MySQL 8 默认时区要求更严格了加上参数就行。5.4 自带的管理员账号为什么登不进去现象数据库脚本里明明白白插了一条 usernameadmin、password123456 的记录但登录页面一直提示“用户名或密码错误”。原因源码里的登录逻辑对密码做了 MD5 加密后再比对而那条初始数据的密码是明文数据库里存的是 123456代码里计算出的 MD5 值是一长串十六进制永远对不上。解决这一步有几个方向。如果你只是演示把登录逻辑里的加密去掉改成明文比对是最快的做法但答辩时容易被问“密码怎么能明文存”。正规做法是写一个小工具类把初始密码改成加密后的值再 UPDATE 进数据库或者在注册功能里写一段逻辑用相同的加密方式存储密码。顺带说一句我见过最离谱的情况是程序里用了 MD5 加盐但脚本里的数据是用另一种算法生成的对不上非常正常。遇到这类问题先在代码里找密码工具类再反推初始数据是怎么生成的不要上来就改加密算法。5.5 页面样式全丢项目路径里多了个“工程名”现象登录页能打开但所有 CSS、JS、图片都加载不出来。浏览器 F12 打开开发者工具Network 标签里一堆 404请求地址全是 http://localhost:8080/css/style.css而项目实际访问地址是 http://localhost:8080/luoyang_tour/login.jsp。原因JSP 页面里引用静态资源的路径写成了 /css/style.css少了项目上下文路径。浏览器解析的时候以为 css 文件在根路径下但应用实际挂在 /luoyang_tour 下所以 404。解决页面里所有静态资源引用改成 ${pageContext.request.contextPath}/css/style.css。如果项目用了 Bootstrap 这类第三方组件检查引入的路径也是同样的改法。还有一种解法是在 JSP 页面顶部用 c:set varctx value${pageContext.request.contextPath} /然后资源路径都写成 ${ctx}/...能少写很多字符。6. 用这套 145 的库接着做加一个“评论”需求要动哪几个文件如果你已经跑通原项目下一步最值得做的改造是加一个“景点评论”功能。为什么推荐这个因为评论必然涉及“新增”和“列表查询”同时关联了用户表、景点表两张表还牵扯到登录权限——一次改造就把前面所有知识点练了一遍。具体要动五个文件。第一数据库加一张 t_comment 表字段是 id、scenic_id、user_id、content、create_time外键关联前两表。第二新建 Comment 实体类和 CommentDaoDAO 里写一个按 scenic_id 查评论列表的方法、一个插入评论的方法。第三新建 CommentServletdoPost 接收评论内容doGet 查询评论列表。第四在景点详情 JSP 页面加评论区上面是评论列表用 c:forEach 遍历下面是输入框和提交按钮。第五在 LoginFilter 里确认评论提交的 URL 需要登录才能访问——普通浏览可以看评论但要发表评论必须登录。整个改造半天足够但覆盖了增删改查、多表 JOIN、过滤器权限三个核心技能点。改的时候留意一件事评论列表查询要 JOIN 查出用户名不能只显示 user_id。SQL 大概是 SELECT c.*, u.username FROM t_comment c JOIN t_user u ON c.user_id u.id WHERE c.scenic_id ? ORDER BY c.create_time DESC。这种 SQL 写一遍你对外键和 JOIN 的理解就扎实了面试手写 SQL 也更有底气。如果你还想往上走可以把这个项目的登录模块改造成一个简易的 Token 方案登录成功后生成一个 UUID 存入数据库并返回前端后续请求带上这个 token后端校验。这样就从 Session 会话模式自然过渡到前后端分离的认证思路跟现在主流 Spring Boot JWT 的面试题也能衔接上。改完这一轮这套课程设计源码就不再是应付交差的“作业”而是能写进简历的完整项目了。我从第一次做这种项目到现在一直保留一个习惯每改完一个功能先把数据库导出一份备份 sql 存到项目外的目录再启动 Tomcat 验证。这套系统改动频繁数据库一旦改坏有备份就有后悔药。希望帮到你。本文还有配套的精品资源点击获取