简介:这是一套采用Java Servlet、JSP与MySQL数据库实现的Web新闻发布系统完整源码,面向JavaWeb初学者、在校学生以及需要完成课程设计或毕业设计的开发者。系统基于MVC分层思想,包含用户注册登录、新闻发布、列表展示、详情查看与管理后台等核心功能,通过JDBC预编译SQL实现数据交互,兼顾基础安全与查询性能。资源包共269个文件,大小6.13MB,其中包含112个class编译文件、56个java源文件、50个JSP页面,以及SQL数据库脚本、XML配置、jar依赖包、图片与CSS样式等辅助文件,目录结构清晰,便于导入IDE后直接运行和二次开发。同时提供了数据库初始化脚本,可快速建表并接入系统。目前已有3433人学习下载。借助完整源码、SQL脚本和配置文件,读者可以快速理解Servlet控制层、JSP视图层与MySQL数据层之间的协作方式,掌握新闻系统的典型实现流程,并在此基础上扩展评论、分类、权限控制等更多功能。
1. Web新闻发布系统:一个把Servlet、JSP、JDBC、MySQL串成闭环的实战案例
Java+Servlet+JSP+MySQL实现的Web新闻发布系统,是Java后端面试和课程设计里出现频率极高的经典组合。它不像Spring Boot那样把底层封装好,反而因为什么都要自己写,把Servlet接收请求、JSP渲染页面、JDBC操作MySQL这条链路完整摊开。这套资源的核心类包括Index、NewsSerlvet、CommonDAO、PhotoUpload、OperatorServlet,覆盖新闻展示、后台发布、图片上传和数据库读写。
适合两类人:一是准备java后端面试、想拿真实web项目串知识点的应届生,二是课程设计要交“能跑、有库、有后台”系统的在校生。前置条件很低,一个Tomcat加一个MySQL就能跑,不碰Spring那套依赖。
下文按拆包顺序推进:先看类结构与MVC分层,再跟一条新闻从发布到展示的请求链,中间放一份踩坑清单,最后落到部署验证和两个进阶改法。目标是看完能复现,复现完能讲清楚。
2. 拆解核心Class:五个Java文件如何撑起MVC三层
从压缩包里拿到的不是源码,而是一批编译好的.class文件:Index、NewsSerlvet、CommonDAO、PhotoUpload、OperatorServlet,其中 Index 和 NewsSerlvet 还各出现了两份同名产物。新手第一眼容易懵,我一般把这个当成免费的架构图来读——类名怎么起、方法怎么分,作者的 MVC 思路全写在命名里。
先说一个肉眼可见的细节:NewsSerlvet这个拼写是错的,按规范应该是NewsServlet。老项目里这种命名错误很常见,类名内部一致就能编译运行,但你如果打算在它基础上加功能,建议动手前先把名字改回来,不然天天被拼写折磨。
2.1 五个核心类分别负责什么
| Class | 定位 | 职责 |
|---|---|---|
| Index | 首页入口 | 查询新闻列表,转发到 index.jsp 渲染 |
| NewsSerlvet | 新闻控制器 | 接收/news请求,按 action 分发增删改查 |
| CommonDAO | 数据访问层 | 统一管理 JDBC 连接,封装通用的增删改查与登录校验 |
| PhotoUpload | 上传工具 | 接收 multipart 文件,生成唯一文件名,写入 uploads 目录 |
| OperatorServlet | 后台控制器 | 管理员的登录校验、新闻发布/编辑/删除入口 |
这套结构一眼就能对上 MVC:JSP 是 View,两个 Servlet 是 Controller,CommonDAO 是 Model。Index 和 OperatorServlet 的区别在于权限边界——Index 面向访客,只读;OperatorServlet 面向管理员,写操作和登录态校验都集中在它这里。你拿到任何一个老项目,先按“哪个类是入口、哪个类碰数据库”圈一圈,五分钟就能把架构盘明白。
至于重复的.class文件,多半是 IDE 编译输出目录和手工 javac 输出混在一起了。不用管它,Tomcat 部署时找到正确的那份就行,这不是运行问题。
2.2 谁在碰请求、谁在碰数据库:一次完整请求的往返
把一次“打开首页看新闻列表”的请求拆开,整个过程是:浏览器发 GET 请求 → 容器根据 web.xml 或注解里的 url-pattern 找到 Index/NewsSerlvet → Servlet 里取参数、调 DAO → CommonDAO 用 JDBC 执行 SQL → MySQL 返回结果集 → Servlet 把 List 塞进 request 域 → forward 到 JSP → JSP 输出 HTML。
关键点在于 Servlet 只做“接请求、转方向”,不写 SQL;DAO 只做“取数、还数”,不碰页面。这个边界守住了,后面换数据库、改页面都不会互相踩脚。面试问到“MVC 怎么分层”,你把这条链路背熟,比背概念有用得多。
路由配置有两种做法:Servlet 3.0 之后直接在类上写@WebServlet("/news"),老项目则是在web.xml里配<servlet-mapping>。两套写法效果完全等价,但这套资源里如果同时出现注解和 web.xml 配置,容易造成映射冲突,后面避坑章节会专门说。
2.3 CommonDAO的连接管理:PreparedStatement是第一道安全闸
所有数据库操作的入口都在 CommonDAO 里,连接参数一般长这样:
public class CommonDAO { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/newsdb" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws Exception { Class.forName(DRIVER); // 注册JDBC驱动,老版本必须显式加载 return DriverManager.getConnection(URL, USER, PASSWORD); } public User login(String username, String password) { String sql = "SELECT * FROM user WHERE username=? AND password=?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return new User(rs.getString("username"), rs.getString("role")); } } } catch (Exception e) { e.printStackTrace(); } return null; } }这里三个参数值得记牢。DRIVER:MySQL 5.x 用com.mysql.jdbc.Driver,MySQL 8 必须换成com.mysql.cj.jdbc.Driver,换错直接抛 ClassNotFoundException。URL里characterEncoding=utf8管中文入库不乱码,serverTimezone=Asia/Shanghai是 MySQL 8 的硬性要求,不加会报时区错误。try-with-resources会自动关连接,老代码里手写conn.close()的写法容易漏,漏一次连接池就耗尽一次。
登录查询用PreparedStatement而不是Statement,这是防 SQL 注入的关键。拿用户名admin' or '1'='1举例:如果用字符串拼接 SQL,拼出来的是WHERE username='admin' or '1'='1',恒真条件直接绕过密码校验;用ps.setString(1, username)绑定参数,单引号会被转义,输入再脏也只是一串普通字符。判断一个老项目能不能要,先看它 DAO 里有没有 PreparedStatement,没有的直接判死刑。
提示:这套资源里的连接是每次请求都新建的,简单项目够用。并发上来以后,建议换成 Druid 或 HikariCP 连接池,改动点集中在 getConnection 这一处。
3. 新闻CRUD请求链路:doGet/doPost分发与JDBC参数绑定
前半程看完架构,这一章跟着一条新闻从后台录入到前台展示,把完整的请求链路走一遍。Servlet 的活其实很机械:收参数、调 DAO、转发页面,难点在参数的命名约定和状态维护。
3.1 统一入口与action分发:一个Servlet处理五种操作
多数老项目不会给每个功能建一个 Servlet,而是用一个入口加一个action参数来分发。这套资源里的 NewsSerlvet 就是典型写法:
@WebServlet("/news") // Servlet 3.0 注解路由,等价于 web.xml 里的 servlet-mapping public class NewsSerlvet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); // GET 统一交给 doPost,避免两套逻辑 } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 必须在取参数之前设置 String action = req.getParameter("action"); if ("list".equals(action)) { list(req, resp); } else if ("detail".equals(action)) { detail(req, resp); } else if ("add".equals(action)) { add(req, resp); } else if ("update".equals(action)) { update(req, resp); } else if ("delete".equals(action)) { delete(req, resp); } else { resp.sendRedirect(req.getContextPath() + "/index"); } } }对应的 JSP 页面里,表单和链接都往这个入口指:
<%-- newsList.jsp 列表页局部 --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> ... <% for (News n : (List<News>) request.getAttribute("newsList")) { %> <div class="news-item"> <h3><a href="news?action=detail&id=<%= n.getId() %>"><%= n.getTitle() %></a></h3> <span><%= n.getPublishTime() %></span> </div> <% } %>req.setCharacterEncoding("UTF-8")必须放在取参数的代码之前,放后面就失效了,这是中文乱码最常见的源头。action 分发的好处是路由集中、好排查;坏处是方法多了以后 if-else 会很长,自己重构时可以用 switch 或 Map 映射优化。JSP 里的<% %>叫 scriptlet,老项目标配,读起来不优雅但逻辑直白。
提示:列表页点“查看详情”是 GET 请求,表单提交新闻是 POST 请求,两种都走到 doPost 的写法在面试时会被追问“GET 和 POST 区别”,答“doGet 里调 doPost 为了少写代码”不算错,但最好补一句:查询用 GET、写操作用 POST 是 HTTP 语义规范。
3.2 新增新闻的SQL与时间处理:用数据库时钟还是服务器时钟
后台发布新闻的核心逻辑在 OperatorServlet 的 add 方法里,最终落到 CommonDAO 的插入语句:
public int insertNews(News news) throws Exception { String sql = "INSERT INTO news (title, content, author, image_path, publish_time, clicks) " + "VALUES (?, ?, ?, ?, NOW(), 0)"; try (Connection conn = CommonDAO.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, news.getTitle()); ps.setString(2, news.getContent()); ps.setString(3, news.getAuthor()); ps.setString(4, news.getImagePath()); return ps.executeUpdate(); // 返回受影响行数,1 表示插入成功 } }publish_time直接用 SQL 里的NOW()取数据库服务器时间,而不是在 Java 代码里 new Date() 再传进去。原因是:应用服务器和数据库服务器时钟存在偏差时,以数据库时间为准能保证所有数据的时间轴一致;而且省掉了 SimpleDateFormat 的时区转换,少一处踩坑。代价是入库后取出来是 Timestamp,页面上要展示成“2024-06-01 10:30”得再格式化,常见做法是查询时用DATE_FORMAT(publish_time, '%Y-%m-%d %H:%i')直接格式化好。
clicks字段默认 0,是给后面热门新闻排序留的口子,这属于作者预留扩展点的好习惯。你给别人的系统加字段时,也尽量带上这种“先占位、后使用”的默认值。
3.3 图片上传:PhotoUpload的保存路径与页面回显
新闻带图是发布系统的刚需,PhotoUpload 组件解决的是“图片存哪、数据库记什么、页面怎么读”。核心逻辑如下:
@WebServlet(value = "/upload", multipartConfig = @MultipartConfig( maxFileSize = 5 * 1024 * 1024, // 单文件上限 5MB maxRequestSize = 10 * 1024 * 1024)) // 一次请求总上限 10MB public class PhotoUpload extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part part = req.getPart("imageFile"); // 表单里 file 控件的 name String fileName = part.getSubmittedFileName(); // 原始文件名,如 a.jpg String ext = fileName.substring(fileName.lastIndexOf(".")); // 用时间戳+随机串拼新文件名,避免同名覆盖和中文文件名乱码 String newName = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8) + ext; String uploadDir = getServletContext().getRealPath("/uploads"); File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } part.write(uploadDir + File.separator + newName); // 数据库里只存相对路径,页面直接用 /uploads/xxx.jpg 访问 String webPath = "uploads/" + newName; // 把 webPath 存进 news 表的 image_path 字段 req.setAttribute("imagePath", webPath); req.getRequestDispatcher("publish.jsp").forward(req, resp); } }这里最关键的设计是“数据库只存相对路径”。如果把C:\tomcat\webapps\news\uploads\xxx.jpg这类绝对路径存进去,项目换个目录部署就全部 404;只存uploads/xxx.jpg,JSP 里写<img src="${imagePath}">就能沿着上下文路径访问。
getRealPath("/uploads")取到的是 Tomcat 部署目录下的物理路径,这在开发环境没问题,但有个隐藏坑:重新部署 war 包时 uploads 目录会被覆盖,用户传的图全丢。课程设计无所谓,真要上线就得把上传目录挪到 webapp 外面,用 Tomcat 的虚拟目录映射指过来。这个后面避坑章节会细说。
4. 避坑指南:跑通这套老项目最常见的五个翻车现场
接下来这部分是血泪经验。老项目本身没有问题,问题都出在“代码是 2015 年的、运行环境是 2024 年的”这种错位上。以下五个坑是我照着这类项目一步步跑的时候真踩过的,按“现象—原因—解决”写清楚。
4.1 编译通过但访问Servlet报404或ClassNotFound
现象:Tomcat 能启动,首页能开,但访问/news直接 404,catalina.out 里报java.lang.ClassNotFoundException: NewsSerlvet。
原因:类名拼写(Serlvet)和注解路径不一致,或者.class文件没编译进WEB-INF/classes。IDEA/Eclipse 的编译输出目录默认是out或build,没配好时 Tomcat 加载的是空的 classes 目录。另外重复的.class文件也可能让容器加载到旧的坏版本。
解决:先核对类上的@WebServlet值和访问地址,注意大小写;再确认WEB-INF/classes里真的有 NewsSerlvet.class;最后把 Tomcat 的work/Catalina目录清空重启,让容器重新编译 JSP、重载类。
4.2 MySQL 8连接报 Communications link failure 或 Public Key Retrieval is not allowed
现象:项目启动后第一次查数据库就抛Communications link failure,或者报Public Key Retrieval is not allowed,但 MySQL 服务明明在跑。
原因:MySQL 8 默认认证插件是caching_sha2_password,老驱动 5.1.x 不认识这个插件;而且加密连接需要客户端主动允许服务器下发公钥。这两个报错本质是驱动版本和数据库版本不匹配。
解决:把mysql-connector-java换到 8.0.x,Driver 类改成com.mysql.cj.jdbc.Driver,URL 上加allowPublicKeyRetrieval=true&useSSL=false。如果不想换驱动,可以把 MySQL 用户改回老认证:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';两个方案二选一,推荐前者。
4.3 页面中文全部变成问号
现象:数据库表里手工插入的中文没问题,但通过页面发布的中文全是??,或者页面显示乱码。
原因:四个环节的编码不一致。JSP 页面声明的 pageEncoding、Servlet 里 request 的编码、JDBC URL 里的 characterEncoding、MySQL 表的字符集,任何一个掉链子,中文就保不住。
解决:按顺序查——JSP 头部写<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 入口req.setCharacterEncoding("UTF-8")且必须在取参之前;JDBC URL 带useUnicode=true&characterEncoding=utf8;建表语句用DEFAULT CHARACTER SET utf8mb4。四个全对齐,中文基本不会再出问题。
提示:utf8mb4 比 utf8 多支持四字节字符(emoji、生僻字),建库时直接用 utf8mb4,省得以后为表情符号折腾。
4.4 上传的图片不显示,重启项目后图片消失
现象:上传时显示成功,数据库 image_path 也有值,但<img>标签 404;更诡异的是第二天重启 Tomcat,之前的图片全没了。
原因:getRealPath("/uploads")指向的是webapps/news/uploads,这个目录在重新部署 war 包时会被整个覆盖。开发环境里手动重启还好,一旦用 IDE 重新部署,用户上传的文件就被清了。
解决:课程设计阶段,把 uploads 目录固定在 webapp 下,并且部署时选择“不重新解压”模式;正经一点的做法是把上传目录配置到 Tomcat 的conf/server.xml里做 Context 映射,让磁盘路径和 webapp 分离:
<Context path="/news/uploads" docBase="/data/news/uploads" reloadable="false" />这样图片存在/data/news/uploads,访问路径是/news/uploads/xxx.jpg,重新部署不再丢文件。代码里getRealPath也要换成读配置项的方式。
4.5 登录接口被SQL注入直接绕过
现象:在用户名输入框填admin' or '1'='1,密码随便填,居然登录成功了,直接进后台。
原因:DAO 里用了Statement字符串拼接 SQL,或者虽然用了 PreparedStatement 但写在某个分支里没走全。这种漏洞在注入工具面前就是裸奔。
解决:全局搜一遍createStatement和+ " WHERE ..."这类字符串拼 SQL 的代码,统一改成PreparedStatement参数绑定。另外给后台逻辑加一道保险:登录成功后session.setAttribute("user", user),OperatorServlet 处理任何写操作前先校验session.getAttribute("user")是否为空,为空就重定向回登录页。两道闸一起上,才算把后台保住。
5. 部署与自测:Tomcat启动、建库SQL与接口验证三步走
代码没问题,下一步就是落地跑起来。很多启动失败看起来像玄学,其实九成是版本不匹配和环境变量没配好。这一章给出一套我每次部署老项目都会走的固定流程。
5.1 版本选型与java环境配置
先给一份我常用的版本搭配表,按这套组合几乎不会出兼容性问题:
| 组件 | 推荐版本 | 理由 |
|---|---|---|
| JDK | 1.8 | Servlet/JSP 老代码最稳定的底座,lambda 也能用 |
| Tomcat | 8.5 或 9.0 | 使用 javax.servlet 命名空间,老代码无需改动 |
| MySQL | 5.7 或 8.0(如 5.7.44) | 5.7 配老驱动,8.0 配新驱动,别混搭 |
| mysql-connector | 5.1.49(配5.7)/ 8.0.x(配8.0) | 版本必须和 MySQL 认证插件对上 |
| IDE | IDEA / Eclipse | 课程设计用 Eclipse 更省事,IDEA 记得关自动部署 |
特别强调一条:不要用 Tomcat 10。Tomcat 10 把javax.servlet.*全部换成了jakarta.servlet.*,这套资源里所有import javax.servlet.http.HttpServlet的代码直接编译不过,除非批量替换包名,否则别自找麻烦。
java -version、mysql --version先确认命令可用;JAVA_HOME 环境变量指向 JDK 安装目录,%JAVA_HOME%\bin(或$JAVA_HOME/bin)加进 PATH。每次 Java 环境配置翻车,九成是 JAVA_HOME 指到了 jre 而不是 jdk。
5.2 建库建表SQL与初始化数据
新闻系统只需要两张表就够跑,一张新闻表、一张用户表:
CREATE DATABASE IF NOT EXISTS newsdb DEFAULT CHARACTER SET utf8mb4; USE newsdb; CREATE TABLE news ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, content TEXT NOT NULL, author VARCHAR(50) DEFAULT 'admin', image_path VARCHAR(255) DEFAULT '', publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, clicks INT DEFAULT 0 ) ENGINE=InnoDB; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role VARCHAR(20) DEFAULT 'user' ) ENGINE=InnoDB; -- 初始化一个管理员账号和一条测试新闻 INSERT INTO user (username, password, role) VALUES ('admin', '123456', 'admin'); INSERT INTO news (title, content, author, image_path, publish_time) VALUES ('第一条测试新闻', '这里是新闻正文内容,用来验证详情页是否正常渲染。', 'admin', '', NOW());ENGINE=InnoDB保证事务和外键能力,MyISAM 在断电恢复时容易丢数据,别用。publish_time DATETIME DEFAULT CURRENT_TIMESTAMP是 MySQL 5.6 以后支持的写法,插入不传时间会自动取当前时间。密码字段这里存的是明文,课程设计能交差,真要给别人用必须改成 MD5/BCrypt 哈希,这是老项目最常见的硬伤。
5.3 导入Tomcat、启动与curl自测
项目打成 war 包丢进 webapps,或者直接把整个项目文件夹拷进去,启动验证按下面顺序走:
# 1. 部署:把 news.war 拷到 Tomcat 的 webapps 目录 cp news.war /opt/tomcat/webapps/ # 2. 启动 Tomcat 并持续看日志 /opt/tomcat/bin/startup.sh tail -f /opt/tomcat/logs/catalina.out # 3. 验证首页可访问,-I 只取响应头 curl -I http://localhost:8080/news/ # 4. 验证详情页返回了测试新闻标题(假设数据库里 id=1) curl -s "http://localhost:8080/news?action=detail&id=1" | grep -o "第一条测试新闻" # 5. 验证后台登录接口,携带 cookie 保持会话 curl -s -c cookies.txt -d "username=admin&password=123456" \ "http://localhost:8080/news?action=login" # 6. 关停 Tomcat(不用时记得释放端口) /opt/tomcat/bin/shutdown.shwar 包文件名决定访问路径,news.war部署后上下文就是/news,如果你部署成ROOT.war则直接用http://localhost:8080/访问。第 4 步如果 grep 不到内容,先看 catalina.out 里有没有 SQL 异常,多半是连接参数或表名对不上。第 5 步返回的 cookie 文件留着,后面模拟管理员操作时curl -b cookies.txt带上会话,能验证登录态是否生效。
注意 8080 端口:本机起了其他服务占用端口时,Tomcat 会启动失败但日志报错不显眼,直接在server.xml里把<Connector port="8080"/>改成 8081 最省事。
6. 进阶改法:给新闻列表加分页和点击量统计
系统跑通,只是及格线。数据一多,列表页一次性拉全表就会变慢,这时候分页和点击量统计就是最值得加的两个功能,改动量小、见效快,面试还能当亮点讲。
6.1 分页查询:LIMIT两个参数与页码边界处理
分页的核心是“查总数”和“查当前页”两条 SQL:
int page = 1, pageSize = 10; String p = request.getParameter("page"); if (p != null && p.matches("\\d+")) { // 非数字参数直接忽略,防注入式参数 page = Integer.parseInt(p); } // 第一条 SQL:查总条数,用于算总页数 String countSql = "SELECT COUNT(*) FROM news"; // 第二条 SQL:LIMIT 第一个参数是偏移量,第二个是每页条数 String listSql = "SELECT id, title, author, publish_time, clicks " + "FROM news ORDER BY publish_time DESC LIMIT ?, ?";偏移量(page - 1) * pageSize是分页最容易写错的地方,第一页偏移是 0,第二页才是 10。总页数算出来后,还要把请求里的页码夹回合法区间:page > totalPages时强制等于 totalPages,page < 1时等于 1,否则用户手动改 URL 参数能把你列表页打出空白。JSP 底部渲染上一页/下一页时,页码用${page - 1}和${page + 1}拼链接,查不到数据时隐藏“下一页”。
6.2 点击量自增:详情页更新一行的两种写法
点击量最朴素的实现,是在新闻详情页被访问时执行一条自增 SQL:
UPDATE news SET clicks = clicks + 1 WHERE id = ?;用clicks = clicks + 1而不是先 SELECT 再 UPDATE,是为了避免“读到的旧值覆盖新值”的并发问题,两条 UPDATE 同时执行也能正确累加。想在前台显示“热门新闻”,列表查询改成ORDER BY clicks DESC LIMIT 10即可。
但有个明显缺陷:刷新一次页面就算一次点击,数字涨得虚高。进阶做法是把已点击的新闻 id 记到 session 里,同一个会话只统计一次:
List<Integer> visited = (List<Integer>) session.getAttribute("visited"); if (visited == null) { visited = new ArrayList<>(); session.setAttribute("visited", visited); } if (!visited.contains(id)) { visited.add(id); dao.increaseClicks(id); // 执行 UPDATE news SET clicks = clicks + 1 }session 方案挡得住普通用户刷新,挡不住清 cookie 换会话,但对于新闻系统的热度排序来说,量级上已经够真实了。真正的线上做法是把点击事件写消息队列异步落库,课程项目没必要上这个复杂度。
从那以后我每次拿到这类老项目,都强制先过四个地方:类名拼写、驱动版本、三处编码、上传目录位置。这四个点全部核对完,整套系统基本半小时内能跑起来。希望帮到你。
本文还有配套的精品资源,点击获取