JavaEE网上书店实战:Servlet+JSP+JDBC从架构设计到部署上线 简介基于JavaEE的网上书店项目完整代码与SQL脚本专为JavaWeb初学者设计可用于课程设计、毕业设计或自学练手。项目覆盖用户注册登录、图书展示与搜索、购物车管理、订单处理及后台管理等模块涉及Servlet、JSP、JPA与EJB技术结合MySQL数据库可深入理解JavaEE分层架构、持久化映射、会话跟踪、表单校验、权限控制等关键知识点。压缩包共88个文件zip格式约519KB包含40个JSP页面、11个Java源文件及对应class文件、数据库建表SQL脚本以及CSS、JS、XML配置与图片素材结构紧凑便于快速下载和导入开发环境。项目内含bookdb.sql建表脚本与初始数据src与WebRoot目录分离可对照源码看清一个网上书店从数据库设计、后端逻辑到前端展示的完整链路。目前已有3134人学习下载整套代码可直接运行适合在课程设计答辩、毕业设计演示或JavaWeb二次开发中作为完整参考。1. 从课程设计到能上线的网站这个网上书店到底做了什么如果你正在为JavaEE课程设计或毕业设计头疼多半需要的是一个“功能完整、能跑通、敢写进论文”的项目而不是一个只有注册登录的Demo。这套基于JavaEE的网上书店恰好卡在课程设计和真实项目之间的分界线上——它把Servlet、JSP、MySQL这些JavaWeb基础技术串成了一个完整闭环从用户注册登录、分类浏览图书、购物车加购到订单生成、后台图书管理每一环都有真实代码支撑而不是靠截图撑门面。项目的价值在于它是个“能落地的骨架”不依赖任何重量级框架纯Servlet JSP JDBC MySQL实现对刚学完JavaWeb的人来说每行代码都能看懂、能调试适合拿来做课程设计答辩演示也适合作为学习SSM、Spring Boot之前的过渡项目。对想二次开发的人它的分层结构足够清晰替换数据库、增加支付接口、改前端页面都不至于伤筋动骨。接下来我会拆解它的技术选型、核心代码链路、部署过程以及我实际踩过的坑。2. 核心技术拆解Servlet JSP JDBC的分层架构设计2.1 为什么选JavaEE原生技术栈而不是Spring Boot这个项目选型有明确的背景网上书店是JavaEE课程设计和毕业设计里出现频率最高的题目之一而多数高校的JavaEE课程还是以Servlet JSP JDBC为主线。Spring Boot虽然工程化程度高但课程考核往往要求体现“JavaEE体系”——Servlet生命周期、Session管理、过滤器、JDBC事务这些核心概念在Servlet项目中是实打实写在代码里的而Spring Boot会把它们隐藏起来。从技术栈构成看这个项目由四部分拼成视图层用JSP JSTL EL表达式渲染页面控制层用Servlet接收请求和分发响应数据访问层用JDBC MySQL数据库辅助层用Filter处理编码和登录校验、用JavaBean封装数据模型。这套组合的好处是“没有黑匣子”请求从进入Tomcat到返回页面每一跳都是自己写的代码出了问题能用断点一路跟到底。2.2 项目目录结构与分层职责拿到源码后先看目录这套结构遵循了JavaWeb项目最常见的分层方式src/ ├── com.bookstore.entity -- 实体类 │ ├── User.java -- 用户实体 │ ├── Book.java -- 图书实体 │ ├── Category.java -- 图书分类 │ └── Order.java -- 订单实体 ├── com.bookstore.dao -- 数据访问层JDBC操作 │ ├── UserDao.java │ ├── BookDao.java │ ├── CategoryDao.java │ └── OrderDao.java ├── com.bookstore.service -- 业务逻辑层事务控制在这里 │ ├── UserService.java │ ├── BookService.java │ └── OrderService.java ├── com.bookstore.controller -- Servlet控制层全部继承HttpServlet │ ├── UserServlet.java │ ├── BookServlet.java │ ├── CartServlet.java │ ├── OrderServlet.java │ └── AdminServlet.java ├── com.bookstore.filter -- 过滤器 │ ├── EncodingFilter.java -- 统一编码处理 │ └── LoginFilter.java -- 登录状态拦截 └── com.bookstore.util -- 工具类 └── DBUtil.java -- 数据库连接工具 WebContent/ ├── index.jsp -- 首页图书列表分类导航 ├── user/ -- 用户端页面登录、注册、购物车、订单 ├── admin/ -- 后台管理页面 ├── css/ js/ images/ -- 静态资源 └── WEB-INF/ ├── web.xml -- 部署描述文件 └── lib/ -- 依赖Jar包JSTL、MySQL驱动这个分层决定了项目的可维护性Servlet不直接写SQLDAO层不出现页面跳转逻辑Service层夹在中间专管业务规则。举个例子下单时先在Service里查库存、算总价、再写订单表和订单明细表期间一旦任何一步出错整个事务回滚——这种事务边界不在Servlet里写是因为Servlet只负责“调用”不该知道“怎么保证数据一致性”。2.3 数据库设计五张表覆盖完整业务流程SQL文件中包含五张核心表这是整个系统的数据地基CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(50) NOT NULL, email VARCHAR(100), phone VARCHAR(20), reg_time DATETIME ); CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description VARCHAR(200) ); CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT, name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), stock INT, sales INT DEFAULT 0, img VARCHAR(200), description TEXT, FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) UNIQUE, total_price DECIMAL(10,2), status INT DEFAULT 0, create_time DATETIME, FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, book_name VARCHAR(100), price DECIMAL(10,2), quantity INT, FOREIGN KEY (order_id) REFERENCES t_order(id) );注意几个设计的用意订单表单独建order_no订单号字段而不是直接用自增主键当订单号这是在为后续对接支付、查询物流留余地库存放在book表而非订单明细表是为了下单扣库存时能直接更新订单明细冗余了book_name和price字段是因为图书信息可能被修改或删除而历史订单里的商品快照不该跟着变。表结构对应了“用户—订单—订单明细—图书”这条业务主线分类表则负责图书的树状组织。3. 核心模块实战前端请求到后端返回的完整链路3.1 数据库连接与编码过滤器所有请求的第一道关卡先看最底层的DBUtil所有DAO都依赖这个类获取连接。项目里用的是传统JDBC方式通过DriverManager获取Connectionpublic class DBUtil { private static String url jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static String username root; private static String password 123456; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) {} } if (ps ! null) { try { ps.close(); } catch (SQLException e) {} } if (conn ! null) { try { conn.close(); } catch (SQLException e) {} } } }参数说明这里要留意url里的useSSLfalse是因为MySQL 8.x默认开启SSL但本地开发通常不需要serverTimezoneAsia/Shanghai解决JDBC 8.x连接时区报错的老问题Class.forName那行在MySQL 8.x下驱动类可以写com.mysql.cj.jdbc.Driver但为了兼容5.x版本项目保留了com.mysql.jdbc.Driver。如果你用的MySQL 8.0以上版本建议显式改成新的驱动类名否则可能抛找不到驱动的异常。编码问题是JavaWeb最容易翻车的点项目里用Filter一次性解决了WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }这个过滤器映射到所有路径保证POST表单提交的中文参数不是乱码JSP页面输出的中文也不会变成问号。从实践来看乱码往往不是JSP页面没写pageEncoding而是GET请求的URI编码在Tomcat的server.xml里没配置所以如果在Windows下遇到GET乱码记得去Tomcat的conf/server.xml给Connector加上URIEncodingUTF-8。3.2 用户注册登录Session与密码处理用户模块是系统的入口登录既是业务也是权限控制。看登录的Servlet实现WebServlet(/login) public class UserServlet extends HttpServlet { private UserDao userDao new UserDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); String code request.getParameter(code); // 从Session取验证码生成时由CaptchaServlet写入 String sessionCode (String) request.getSession().getAttribute(code); String msg ; if (code null || !code.equalsIgnoreCase(sessionCode)) { msg 验证码错误; request.setAttribute(msg, msg); request.getRequestDispatcher(/user/login.jsp).forward(request, response); return; } User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { request.getSession().setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /index); } else { msg 用户名或密码错误; request.setAttribute(msg, msg); request.getRequestDispatcher(/user/login.jsp).forward(request, response); } } }这里有两个容易被忽略的细节一是验证码校验放在用户查询之前避免数据库在验证码错误时做无用查询二是登录成功后用sendRedirect重定向而不是forward转发这是为了避免表单重复提交——直接转发的话用户刷新页面就会弹“确认重新提交表单”这在答辩演示时非常尴尬。Session里存放的是完整的User对象后续下单、查看订单都直接从Session取当前用户不再查库。密码存储方面这个项目作为课程设计级别默认是明文比对但如果要二次开发或正式上线必须改成MD5加盐或BCrypt加密。我一般会在UserDao查询前对密码做一次哈希处理把加密逻辑放在Service层这样既不影响前端交互又能在论文里多写一章“安全性设计”。3.3 购物车从加购到结算的数据流转购物车是电商系统最容易写乱的部分。常见做法有三种Session临时存储适合匿名购物、数据库Cart表持久化适合登录后同步、Redis缓存性能最优。这个项目采用第一种方案Session中用一个Map存购物车WebServlet(/cart) public class CartServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); String bookId request.getParameter(bookId); HttpSession session request.getSession(); if (add.equals(action)) { // 从Session取购物车没有则创建 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMapInteger, Integer(); session.setAttribute(cart, cart); } // 图书ID作为key数量作为value已存在则数量加1 int id Integer.parseInt(bookId); Integer count cart.get(id); cart.put(id, count null ? 1 : count 1); response.sendRedirect(request.getContextPath() /cart?actionlist); } else if (delete.equals(action)) { MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart ! null) { cart.remove(Integer.parseInt(bookId)); } response.sendRedirect(request.getContextPath() /cart?actionlist); } else if (clear.equals(action)) { session.removeAttribute(cart); response.sendRedirect(request.getContextPath() /cart?actionlist); } else if (list.equals(action)) { // 展示购物车时查出图书详情 总价 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); ListCartItem items new ArrayListCartItem(); int totalPrice 0; if (cart ! null) { for (Map.EntryInteger, Integer entry : cart.entrySet()) { Book book new BookDao().findById(entry.getKey()); CartItem item new CartItem(); item.setBook(book); item.setQuantity(entry.getValue()); items.add(item); totalPrice book.getPrice() * entry.getValue(); } } request.setAttribute(cartItems, items); request.setAttribute(totalPrice, totalPrice); request.getRequestDispatcher(/user/cart.jsp).forward(request, response); } } }逻辑说明很简单Map的key是图书IDvalue是购买数量。加购时判断Map里有没有这本书有就1没有就置1。清空购物车直接移除Session属性最粗暴也最有效。列表展示时重新查了一次数据库这是为了保证页面显示的图书价格是数据库最新值而不是Session里可能过期的缓存——这个设计在并发修改价格时能避免用户看到旧价格。参数注意点所有数量处理都用Integer.parseInt包装如果前端传入非法参数比如空字符串、abc会抛NumberFormatException。如果时间充裕建议在解析前加try-catch或正则校验否则别人用Postman构造一个畸形请求就能让你的Servlet直接500。3.4 下单与库存扣减事务控制的完整示范下单是整个项目中技术含量最高的一段代码因为它涉及多表写入和一致性保证。看OrderService的关键实现public class OrderService { private OrderDao orderDao new OrderDao(); private BookDao bookDao new BookDao(); public boolean createOrder(int userId, MapInteger, Integer cart) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启手动事务 // 1. 生成订单号时间戳 随机数 String orderNo System.currentTimeMillis() (int)(Math.random() * 1000); double totalPrice 0; // 2. 先计算订单总价同时检查并扣减库存 for (Map.EntryInteger, Integer entry : cart.entrySet()) { int bookId entry.getKey(); int quantity entry.getValue(); Book book bookDao.findById(bookId); if (book.getStock() quantity) { conn.rollback(); return false; // 库存不足回滚事务 } totalPrice book.getPrice() * quantity; } // 3. 写入订单主表 Order order new Order(); order.setUserId(userId); order.setOrderNo(orderNo); order.setTotalPrice(totalPrice); order.setStatus(0); // 0-待发货 order.setCreateTime(new Date()); int orderId orderDao.insert(conn, order); // 4. 写入订单明细表 扣减库存 for (Map.EntryInteger, Integer entry : cart.entrySet()) { int bookId entry.getKey(); int quantity entry.getValue(); OrderItem item new OrderItem(); item.setOrderId(orderId); item.setBookId(bookId); item.setQuantity(quantity); orderDao.insertItem(conn, item); bookDao.decreaseStock(conn, bookId, quantity); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException e1) {} e.printStackTrace(); return false; } finally { DBUtil.close(conn, null, null); } } }这段代码在答辩时可以当核心亮点讲先用setAutoCommit(false)关闭自动提交让后续的订单主表写入、订单明细写入、库存扣减成为一个原子操作——任何一个环节失败rollback()都会让前面所有操作全部撤销不会出现“订单生成了但没扣库存”或“款扣了但订单没生成”的数据不一致。这里面的设计教训是库存检查和扣减分布在不同循环里并发时会有超卖风险课程设计阶段可以接受但若要上线必须改成“UPDATE t_book SET stock stock - ? WHERE id ? AND stock ?”这种原子操作让数据库来保证检查与扣减的一体性。我在二次开发的时候会把库存扣减的SQL直接改成带条件的方式然后用update返回值判断是否扣减成功比程序里先查后减更靠谱。4. 部署与运行从SQL导入到Tomcat启动全流程4.1 环境准备与数据库初始化跑通这个项目需要准备的基础环境如下JDK 1.8以上版本8和11都试过没问题、Tomcat 8.5或9.x、MySQL 5.7或8.x、Eclipse IDE for Java EE或者IDEA装好Tomcat插件。如果你的本机已经装了PHPStudy或者XAMPP直接用自带的MySQL和Tomcat也行注意端口别冲突。第一步是初始化数据库。打开MySQL命令行或者Navicat新建数据库bookstore字符集选utf8mb4不要选utf8排序规则用utf8mb4_general_ci然后把SQL文件导进去mysql -u root -p -e CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p bookstore bookstore.sql如果SQL文件里没有建库语句只有建表语句上面两步分开执行没毛病如果SQL文件里已经包含了CREATE DATABASE直接执行第二行就行。导入完成后先跑几条查询验证表结构是否正确USE bookstore; SHOW TABLES; SELECT COUNT(*) FROM t_book;正常情况能看到五张表而且t_book表里应该有预置的样例图书数据。如果你发现t_book是空的说明SQL文件里没插测试数据那就要自己在后台管理员页面录入或者手动INSERT几条——这直接影响首页能不能显示图书是个很容易漏掉的坑。4.2 修改数据库连接配置与部署到Tomcat数据库建好后打开src/com/bookstore/util/DBUtil.java把url、username、password改成你自己MySQL的实际配置。绝大多数翻车现场都是这一步没改我用的是root账户密码123456你本地密码是abc123结果就是控制台报Access denied for user rootlocalhost。改完密码必须重启Tomcat才能生效因为JDBC驱动和连接池加载过一次后不会自动重新读取。在IDEA里的部署步骤打开Run Configuration新建Tomcat Server本地配置Deployment选项卡里选择Artifact把这个项目的war包或exploded版本添加到部署列表Application context建议填/bookstore。如果项目没有自动打包成Artifact先要Project Structure → Artifacts → 加Web Application: Exploded依赖选Module然后重新Build。如果用Eclipse更简单右键项目 → Run As → Run on Server → 选择已配置的TomcatEclipse会自动把项目部署到Tomcat的webapps目录下并启动浏览器访问。启动日志看到INFO: Server startup in [4-6] seconds就算成功访问http://localhost:8080/bookstore/就能看到首页。4.3 启动验证功能自查清单项目启动后别急着截图按下面清单过一遍所有功能确认每个模块都没问题再收工管理员账户是否可用后台登录入口通常在首页底部或/admin/login.jsp注册新用户、退出后重新登录往购物车加几本书刷新页面看数量是否保留结算下单后数据库t_order和t_order_item是否真的多了记录后台能不能添加图书、改库存改完首页是否刷新。这里尤其要测的是“刷新页面重复下单”的问题下单页面提交后如果没做重定向处理用户按F5就会重复扣库存。这个项目里下单成功后是sendRedirect跳转到订单列表所以天然规避了这个问题。如果你在测试时发现F5会重复下单检查代码里下单成功后是不是用了forward而不是redirect。5. 避坑指南从零跑通这个项目的十三个血泪经验5.1 数据库连接失败时区与驱动类的双重陷阱现象是启动项目后访问首页页面报500错误Tomcat日志里看到Cannot create PoolableConnectionFactory或The server time zone value ???ú±ê׼ʱ¼ä is unrecognized。原因有两个层面MySQL 8.x的驱动要求连接URL必须显式指定serverTimezone驱动类名在8.x里应该用com.mysql.cj.jdbc.Driver如果你用的MySQL 5.x而驱动包是8.x的反而会导致兼容性问题。还有一个隐藏点是你MySQL服务本身没启动Windows下MariaDB和MySQL 3306端口可能冲突。解决办法是在url后面追加serverTimezoneAsia/ShanghaiuseSSLfalse然后把DBUtil里的Class.forName改成com.mysql.cj.jdbc.Driver。如果还报错用CMD执行netstat -ano | find 3306看3306端口究竟被谁占着有时是本地装了多个MySQL实例导致连接错库。5.2 JSP页面中文乱码三个位置必须保持一致现象是登录注册后页面上的中文标题、提示信息全变成了问号。原因是没有统一字符编码JSP页面、Servlet输出、数据库连接三处各用各的编码。常见误用是只在JSP第一行写了pageEncodingUTF-8但Servlet输出到页面的字段没设ContentType或者数据库连接URL没指定characterEncoding。解决办法是同时检查三处JSP页面头部的contentType改为text/html; charsetUTF-8EncodingFilter里设request.setCharacterEncoding(UTF-8)DBUtil的url带上useUnicodetruecharacterEncodingutf8。特别注意MySQL的utf8不是真正的四字节编码建库要选utf8mb4否则存生僻字或emoji会报Incorrect string value错误。5.3 404或页面样式丢失项目部署路径与资源引用不一致现象是首页能打开但CSS、JS、图片全部加载失败控制台一堆404。原因是JSP页面里引用了绝对路径比如href/css/style.css如果你的Context Path不是根路径浏览器就会在错误的路径下找资源。比如访问地址是http://localhost:8080/bookstore/index.jsp但style.css的引用写的是/css/style.css那浏览器实际请求的是http://localhost:8080/css/style.css自然是404。解决办法有两个最简单的把所有静态资源引用改成带上下文路径的写法${pageContext.request.contextPath}/css/style.css或者在JSP页面用base href%basePath%统一基准路径。这个项目里我最初入手时好几个页面用的是相对路径一旦换Context Path就翻车后来统一改成EL表达式取contextPath再没出过问题。5.4 部署后找不到JSTL标签库lib下的jar包没同步现象是页面顶部报javax.servlet.ServletException: java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core。原因是项目用了JSTL标签库但WEB-INF/lib下缺少jstl.jar和standard.jar或者IDEA打包时没把lib目录同步进去。这个问题在Eclipse里出现的概率更大因为Eclipse默认可能不把项目lib目录下的jar包复制到部署目录。解决办法是确认WebContent/WEB-INF/lib下有没有这两个jar包没有就去网上下载1.2版本的jstl.jar和standard.jar放进目录然后在IDEA的Project Structure → Artifacts里确认lib目录被勾选为打包进WEB-INF/lib。改完配置后要Rebuild Artifact再重启Tomcat。5.5 后台登录拦截失效Filter路径映射的常见误用现象是没登录也能直接访问/admin/addBook.jsp页面或者反过来登录了却一直被拦截跳回登录页。原因是LoginFilter的mapping配置不对。如果WebFilter注解写的是WebFilter(/admin/*)那只有admin目录下的请求会被拦截而Servlet路径/adminServlet如果不在admin目录下就逃过一劫。反之如果mapping范围过宽比如/*那登录Servlet的请求自己也被拦了导致永远登不进去。解决办法是在LoginFilter里加一个排除逻辑获取请求路径如果是/login、/register、/adminLogin这样的入口直接放行其余路径才检查Session里有没有loginUser。我在二次开发时会把所有不需要登录的路径集中配在一个列表里用数组存起来循环判断比在web.xml里堆一堆url-pattern更清爽。6. 二次开发升级方向从课程设计到可用商城拿到这个项目如果不想止步于交作业可以从下面几个方向做升级。难度最低的是前端美化——把JSP页面里的Table布局改成Bootstrap或Layui首页加轮播图和分类楼层做完整个项目的气质从“课程设计”变成“商业站点”。但更推荐做有技术含量的改造下面三个方向对论文和面试都有实实在在的加分。第一个方向是购物车持久化改造。当前购物车存在Session里用户关掉浏览器就丢了这不满足真实购物场景。改造思路是把t_cart表和t_cart_item表加到数据库登录状态下点击加购先写库未登录才用Session临时存放登录成功后再把Session里的临时数据并入库。这一步涉及到DAO层新增接口和Servlet层判断逻辑能把你的数据库设计能力体现出来。第二个方向是引入订单状态机。现在t_order的status只有0和1两个值升级后可以定义枚举状态0待付款、1已付款待发货、2已发货、3已完成、4已取消然后给管理员后台增加发货操作按钮订单列表按状态筛选。这个改造的核心在OrderService里加状态流转校验比如已取消的订单不允许发货。改动量不大但能让系统看起来有完整的业务闭环。第三个方向是数据访问层框架替换。把JDBC手写代码改成DBUtils或者MyBatis这是最经典的重构理由。以MyBatis为例只需要在原来的DAO方法体里替换SQL执行逻辑把Connection管理交给mybatis-config.xml统一配置Service层的接口签名完全不动——这种“换掉底层上层无感”的重构经验是面试时能聊半个小时的话题。说一个实际改造中我踩过的坑给这个项目加订单状态时我直接在t_order表加了ALTER TABLE语句却忘了已有的订单数据status都是默认0结果旧数据全部变成“待付款”——如果线上系统这么搞等于把所有历史订单状态全部弄错。从那以后我每次改表结构都会强制走一遍“先备份、再改表、后刷数据”的三步流程顺手把SQL语句和备份文件放进项目的sql目录。希望这篇拆解能帮你有条不紊地把它跑起来改下去。最后想说项目跑起来只是第一步真正把这些代码逐行读懂、改造顺手的功夫远比拿到一个能交差的Demo值钱。希望帮到你去跑通它。本文还有配套的精品资源点击获取