ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM+JSP网上书店系统:从架构到部署的全链路实战解析

SSM+JSP网上书店系统:从架构到部署的全链路实战解析 简介这是一份基于SSM框架和JSP技术的网上书店商城系统毕业设计资源包主要面向Java Web学习者、高校在校生以及需要快速搭建商城项目的开发人员。资源包内共包含5个文件分别有项目源码压缩包、数据库表结构脚本、系统设计文档、答辩演示文稿以及项目介绍文本整体大小38.36MB能够支撑从环境搭建、代码调试到论文撰写的完整流程。目前已有2443人学习下载。系统功能划分清晰前台用户模块实现了注册登录、图书信息浏览、购物车操作、图书评价、最近浏览、个人中心以及订单查看与确认收货后台管理员模块则提供了用户信息管理、图书商品管理、订单管理等多种增删改查操作。随资源还附带数据库初始化SQL和说明文档方便使用者快速理解表结构关系、降低部署门槛适用于课程设计、毕业设计或SSM框架综合实践参考。1. 网上书店系统里JSP页面报 500 该先查哪里一个典型的 SSMJSP 项目出问题很多人第一反应是去看 Controller 或者 Service 的 Java 代码。但网上书店这类以 JSP 作为视图层的系统里页面一旦 500第一批要查的其实是 Tomcat 的work目录——JSP 首次被访问时会先翻译成 .java 再编译成 .class改完 JSP 看不到效果多半是旧的 class 还在。这套基于 SSMJSP 的网上书店商城系统把框架分层和 JSP 渲染这两个关键点都完整地放在了一个可运行的项目里前台有注册登录、图书浏览、购物车、评价、最近浏览、订单管理后台有管理员登录、用户 CRUD、图书商品 CRUD、订单管理。适合正在做课程设计的学生也适合刚学完 SSM 但不知道 Controller 和 JSP 怎么配合工作的人。2. SSMJSP 分层结构bookstore 源码骨架与配置解析拿到源码压缩包解压后先别急着运行。这个项目的目录结构是典型的 Maven Web 工程但部署到 Tomcat 时用的是webapp目录而不是src/main/webapp之外的路径。先看清分层后面改功能、查报错才有方向。bookstore ├── src/main/java │ ├── com.bookstore.controller │ ├── com.bookstore.service │ ├── com.bookstore.mapper │ ├── com.bookstore.pojo │ └── com.bookstore.util ├── src/main/resources │ ├── jdbc.properties │ ├── spring-dao.xml │ └── springmvc.xml ├── webapp │ ├── WEB-INF │ │ ├── web.xml │ │ └── jsp │ └── static ├── bookstore.sql └── 项目介绍.txt2.1springmvc.xml视图解析器与静态资源放行这个项目的 JSP 全部放在WEB-INF/jsp下目的是不通过 Controller 就无法直接访问页面文件资源和逻辑彻底隔离。在 IDEA 里创建 JSP 文件时右键webapp/WEB-INF/jsp目录选择 New 一个 JSP File 即可IDEA 会自动识别 Web 根目录。如果你的 FreeMarker 或者 Thymeleaf 用多了回到 JSP 项目最容易忽略的就是视图解析器前缀后缀配置!-- springmvc.xml 核心配置 -- mvc:annotation-driven / mvc:default-servlet-handler / bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp/ / property namesuffix value.jsp / /beanInternalResourceViewResolver的prefix和suffix决定了 Controller 里返回的字符串最终对应哪个文件。比如return user/login;会被解析为/WEB-INF/jsp/user/login.jsp。mvc:default-servlet-handler是为了放行 CSS、JS、图片等静态资源没有这一行static目录下的样式表全都会 404。2.2web.xml编码过滤器与 DispatcherServlet 映射JSP 项目最常见的乱码问题根源通常不在数据库而在web.xml里没有强制 UTF-8 编码。这个项目的 web.xml 有两处值得抄filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mappingforceEncoding设为true后不仅请求体按 UTF-8 解码响应也会统一使用 UTF-8。DispatcherServlet的url-pattern是/而不是*.do这意味着所有请求都进 Spring MVC如果静态资源配置不到位页面会连 CSS 都加载不出来。Tomcat 的server.xml里如果配置了URIEncodingUTF-8那是最好没配的话 GET 请求中文参数也可能乱forceEncoding救不了 GET只能靠 Tomcat 连接器属性。2.3 Mapper 与 Service事务边界放哪里网上书店这类课程设计项目事务一般放在 Service 层。spring-dao.xml里开启注解事务购物车生成订单这种多表操作才能保证一致性bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource / /bean tx:annotation-driven transaction-managertransactionManager /对应的 Service 实现类上标注Transactional。需要特别注意的是MyBatis 的 Mapper 接口扫描路径spring-dao.xml里配置的basePackage要和com.bookstore.mapper完全一致漏掉一层包名运行时就报Invalid bound statement (not found)。3. 前台购物车到订单链路CartController 到 OrdersServiceImpl 的实现思路前台模块里最容易出问题的两块购物车的数据结构选型以及订单创建时购物车如何销毁。网上书店项目里购物车并没有单独建表而是直接放在 HttpSession 中——这是 JSP 时代最常见也最务实的做法数据和业务绑定在会话内不需要额外表。3.1 购物车数据结构为什么不用 List先定义CartItem和Cartpublic class CartItem { private Book book; // 图书对象 private Integer count; // 购买数量 public Double getSubtotal() { return book.getPrice() * count; } } public class Cart { private MapInteger, CartItem items new HashMapInteger, CartItem(); private double total; public void addBook(Book book) { CartItem item items.get(book.getId()); if (item null) { item new CartItem(); item.setBook(book); item.setCount(1); items.put(book.getId(), item); } else { item.setCount(item.getCount() 1); } updateTotal(); } }用MapInteger, CartItem而不是ListCartItem的好处很直接bookId就是 Key同一本书加入购物车时 O(1) 找到已有条目并更新数量不需要遍历 List。total在每次增删后通过updateTotal()重新累加避免会话里旧数据残留。很多入门写法用 ArrayList会出现同一个 bookId 多条记录还要额外写去重逻辑没必要。3.2 加入购物车的 Controller 流程CartController的addToCart方法RequestMapping(/addToCart) ResponseBody public MapString, Object addToCart(Integer bookId, Integer count, HttpSession session) { Book book bookService.findById(bookId); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } cart.addBook(book, count); MapString, Object result new HashMapString, Object(); result.put(code, 0); result.put(totalCount, cart.getTotalCount()); return result; }注意这里没有直接返回 JSP 页面而是返回一个 Map 并由 Jackjson 转成 JSON。前端 JSP 页面里的$.post拿到返回结果后只更新页面右上角的购物车角标数字不整页刷新。这是 JSP 老项目里很常用的局部刷新方式不用引入 Vue 或 React。3.3 最近浏览用 Session 还是 Cookie项目里「最近浏览功能」的实现我拆的时候看到的是放在 Session 中的一个LinkedListBook。每次查看图书详情时先移除已存在的相同 bookId再插入到头部最后截断前 10 条SuppressWarnings(unchecked) public void addViewedBook(Book book, HttpSession session) { LinkedListBook viewed (LinkedListBook) session.getAttribute(viewedBooks); if (viewed null) { viewed new LinkedListBook(); } for (IteratorBook it viewed.iterator(); it.hasNext();) { if (it.next().getId().equals(book.getId())) { it.remove(); } } viewed.addFirst(book); while (viewed.size() 10) { viewed.removeLast(); } session.setAttribute(viewedBooks, viewed); }这里的取舍是Session 方案实现简单、跟着登录态走但服务器重启就丢失Cookie 方案能持久化但只能存 4KB 且容易被用户清掉。图书封面 URL 比较短存 Cookie 也够但一般课程设计没必要上 CookieSession 方案足够撑起演示。3.4 订单生成的事务边界订单创建是整个前台业务里最重的一个操作它要同时写主订单表、订单明细表、扣减库存、清空购物车。这一步必须加上事务Transactional public Orders createOrder(Integer userId, Integer cartId, HttpSession session) { Cart cart (Cart) session.getAttribute(cart); Orders order new Orders(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); // 时间戳 随机数生成订单号 order.setState(0); // 0待付款 1待发货 2待收货 3已完成 order.setTotal(cart.getTotal()); ordersMapper.insert(order); for (CartItem item : cart.getCartItems()) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setBookId(item.getBook().getId()); oi.setPrice(item.getBook().getPrice()); oi.setCount(item.getCount()); orderItemMapper.insert(oi); } session.removeAttribute(cart); // 订单入库后再清购物车 return order; }注意session.removeAttribute(cart)放在事务方法内部——如果先清 Session 再写库一旦订单明细插入失败事务回滚了购物车却没了这是很隐蔽的 bug。ordersMapper.insert(order)执行完后MyBatis 的useGeneratedKeys会把自增主键回填到order.getId()否则后续orderItemMapper.insert(oi)里的orderId全是 0。3.5 图书评价与 JSP 中的 JSONArray 处理图书评价这块典型场景是商品详情 JSP 页面加载完成后异步请求评价列表。老项目中常出现jsp jsonarray import这类需求因为 JSP 页面里如果需要解析后端返回的 JSON 数组要在页面顶部手动导入 java.util 相关类并用c:forEach循环渲染而不是像前后端分离项目那样直接操作 DOM。常见做法是后端接口返回一个 JSONArray 字符串JSP 中通过 EL 表达式拿到后配合JSONArray.fromObject()解析。如果你的 JSP 项目要渲染评价列表更省事的方式是后端直接用 ModelAndView 把评论 List 塞进去再借 JSTL 标签遍历避免在 JSP 里写大段的 scriptlet。4. 后台管理员模块用户、图书、订单三类 CRUD 的权限校验与状态流转后台管理模块从代码结构上就是三个垂直切片用户管理、图书信息管理、订单管理。这类 CRUD 功能写起来不难但管理员权限控制和订单状态流转的边界是这个项目里值得细看的部分。4.1 登录拦截器拦截的是路径而不是按钮后台页面的 URL 全是/admin/xxx开头。如果没有拦截器任何人敲/admin/book/list都能进入后台。项目的做法是自定义一个HandlerInterceptorpublic class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object admin request.getSession().getAttribute(adminUser); if (admin null) { response.sendRedirect(request.getContextPath() /admin/login); return false; } return true; } }在 springmvc.xml 里注册时mapping path配/admin/**exclude-mapping配/admin/login。这里的坑点是路径匹配规则用 Ant 风格时/admin/*只能拦一级路径/admin/book/edit/1这种带参数的 URL 必须用/admin/**才能拦到。4.2 动态 SQL 更新图书MyBatis 的set标签图书信息管理里编辑功能会用到部分字段更新。项目里 BookMapper.xml 的 update 语句用了set标签这样前端只传了书名和价格时不会把库存和销量误置为 nullupdate idupdateByPrimaryKeySelective parameterTypecom.bookstore.pojo.Book update tb_book set if testname ! nullname #{name},/if if testauthor ! nullauthor #{author},/if if testprice ! nullprice #{price},/if if teststock ! nullstock #{stock},/if if testcategoryId ! nullcategory_id #{categoryId},/if /set where id #{id} /updateset标签会自动去除末尾多余的逗号这是 MyBatis 里处理部分更新最典型的写法。与之相对的if判断条件里注意price ! null对 0 值也成立所以价格设置为 0 的商品不会被漏掉。图书封面上传时的文件名如果存入的是 UUID这里 update 时要记得附带覆盖。4.3 用户管理的逻辑删除而非物理删除用户管理模块的删除功能项目里没做物理DELETE而是给tb_user加了一个del_flag字段。删除时执行update tb_user set del_flag 1 where id #{id}。这样做的核心原因是订单表和用户表存在外键关联物理删除会直接把历史订单的关联数据也毁掉。所有前台展示用户的查询语句都带where del_flag 0条件漏掉这个条件就会看到一堆已注销账号。新人在接手这样的代码时很容易产生疑惑明明调用了 delete 接口数据库行却没消失。4.4 后台首页的数据看板管理员登录后的首页通常会展示几个数字今日订单数、图书库存告警、用户总量。代码实现也不算复杂RequestMapping(/dashboard) public String dashboard(Model model) { model.addAttribute(todayOrderCount, ordersMapper.countToday()); model.addAttribute(userCount, userMapper.countAll()); model.addAttribute(bookCount, bookMapper.countAll()); model.addAttribute(lowStockList, bookMapper.selectLowStock(20)); return admin/dashboard; }countToday()的 SQL 里用了where datediff(order_time, curdate()) 0这种写法能正确走索引吗要注意对order_time字段使用复杂表达式会导致索引失效大数据量下会出现全表扫描。更优的写法是order_time 2024-06-01 00:00:00 and order_time 2024-06-02 00:00:00但课程设计数据量通常不大datediff写法直接复制到生产环境就危险了。5. MySQL 数据库脚本与论文材料bookstore.sql 的表关系拆解网上书店系统压缩包里的bookstore.sql是整套项目的基础不导入这个脚本哪怕代码编译过了页面也全是数据库错误。项目压缩包根目录里还有论文文档和答辩 PPT这套 SQL 与论文中的 E-R 图和功能模块是一一对应的论文里每一个诚实的图表在代码里都能找到实现。5.1 六张核心表的设计先看这份 SQL 里最核心的几张表CREATE TABLE tb_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, phone varchar(20) DEFAULT NULL, address varchar(255) DEFAULT NULL, del_flag int(1) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4; CREATE TABLE tb_book ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, author varchar(50) DEFAULT NULL, price decimal(10,2) DEFAULT NULL, stock int(11) DEFAULT 0, category_id int(11) DEFAULT NULL, cover_url varchar(255) DEFAULT NULL, sales int(11) DEFAULT 0, description text, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4; CREATE TABLE tb_orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int(11) NOT NULL, total decimal(10,2) DEFAULT NULL, state int(1) DEFAULT 0, order_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, deliver_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;订单表的时间字段值得模仿order_time、pay_time、deliver_time、finish_time四个时间字段分别对应订单创建、支付、发货、完成四个节点。Java 代码里根据state不同展示不同时间无需额外建日志表就能还原一条订单的完整生命周期。订单号用UNIQUE KEY约束保证唯一。5.2 核心表的关联关系表之间的外键关系怎样设计的tb_orders表通过user_id关联tb_user但这里实际项目一般不会真正加上FOREIGN KEY约束而是靠应用层保证一致性。这样做的原因是很多老系统在数据量上来之后外键约束会对插入、删除操作造成额外性能开销。订单明细表设计如下它通过order_id关联主订单表通过book_id关联图书表CREATE TABLE tb_orderitem ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, book_id int(11) NOT NULL, book_name varchar(100) DEFAULT NULL, price decimal(10,2) DEFAULT NULL, count int(11) DEFAULT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;其中book_name字段的设计值得讨论为什么订单明细表要冗余一个商品名称因为如果不冗余图书改价或者下架后历史订单里明细就再也查不到当时的购买快照。经常看到有人把订单设计和表关联过度硬要在明细表里再查一次tb_book写多表 JOIN 不说商品改名后订单历史里显示的还是新名字这可就不太合适了。评价表tb_comment则设计了book_id和user_id双外键存储评价内容、评分和评价时间。最近浏览记录可以不落表直接在 Session 中维护这也是压缩包里bookstore.sql中没有浏览记录表的原因。5.3 从 SQL 导入到论文图表对照命令行导入这个 SQL 脚本mysql -uroot -p bookstore bookstore.sql如果 bookstore 数据库并不存在需要先创建SQL 脚本里可能没有CREATE DATABASE语句直接执行会报No database selected。mysql -uroot -p mysql create database bookstore default charset utf8mb4; mysql use bookstore; mysql source /你的路径/bookstore.sql;导入成功后会在 bookstore 库下看到上面这几张表加上一张管理员表tb_manager初始管理员账号密码在 SQL 的INSERT语句里格式是admin和admin的哈希值——如果你改了密码进不去后台直接去 MySQL 里 UPDATEtb_manager表重置就好不用再想方设法绕过登录。论文里的数据库设计章节一定有一张 E-R 图投标或者答辩时老师会着重看 E-R 图与实际建表是否一致。这里有一个实用的小技巧把论文中的表名字段名和bookstore.sql中逐个对照凡是论文里画了而 SQL 里没有的表——比如很多同学论文里画了「管理员操作日志表」但代码里根本不存在——就是答辩时最容易翻车的地方。6. 部署到 Tomcat 的验证清单与 JSP 编译踩坑最后一步是让系统在 IDEA 里跑起来并验证核心链路。这一步不是简单启动就行我把验证顺序和 JSP 编译的坑放在一起说。6.1 部署顺序与验证清单1. 创建 bookstore 数据库并导入 bookstore.sql 2. IDEA 中修改 jdbc.properties 连接账号密码 3. 配置 TomcatDeployment 选择 exploded 方式 4. 启动后访问 http://localhost:8080/bookstore/ 5. 注册一个测试账号加入购物车生成订单 6. 用管理员账号登录 /admin/login看到订单数与用户数这里有个容易被忽略的细节IDEA 配置 Tomcat 的Application context填成/bookstore后访问路径才是http://localhost:8080/bookstore/如果填的是/或者和项目名不一致所有 JSP 里的 contextPath 都会错位页面能打开但 CSS、JS 全丢登录跳转也绕回首页。6.2 JSP 编译后的 class 文件到底在哪JSP 不是直接被浏览器执行的Tomcat 会把.jsp翻译成 Java 文件再编译成 class。验证方式是启动项目并访问过一个 JSP 页面后去 Tomcat 的work目录下找apache-tomcat/work/Catalina/localhost/bookstore/org/apache/jsp/这个目录下面能看到index_jsp.java和index_jsp.class。idea 创建 jsp 文件后第一次访问就会生成对应的*_jsp.java如果你改了 JSP 内容却发现页面没变化去这个目录把org/apache/jsp整个文件夹删掉再重启 Tomcat 即可。Tomcat 通过 JSP 文件的时间戳判断是否需要重编译偶尔因为系统时间错乱或文件同步问题导致判断失败就会一直加载旧 Java 文件。生产环境部署时修改了 JSP 却不生效百分之九十是 work 缓存问题。6.3 常见报错速查报错现象排查方向解决建议HTTP 404 且页面显示原样 JSP 源码视图解析器 prefix/suffix 配置错误检查 springmvc.xmlreturn book/list实际找的是/WEB-INF/jsp/book/list.jsp500 JasperException: Unable to compile class for JSPJSP 里使用了未依赖的类库如 JSTL 标签检查pom.xml或 WEB-INF/lib 是否引入 jstl.jar数据库中文乱码编码链路不止一处依次确认jdbc.url加characterEncodingutf-8、web.xml过滤器、MySQL 表字符集三处一致IDEA 控制台报内存溢出JSP 编译需要额外 metaspaceTomcat 启动 VM options 添加-XX:MaxMetaspaceSize256mInvalid bound statementMapper 接口和 XML 路径不匹配检查mybatis.mapper-locations是否指向classpath:mapper/*.xml如果访问.jsp页面报 500Jasper 异常信息会直接打印在 IDEA 的 Console 里还会显示具体的行号。最常见的是某个 EL 表达式写了不存在的属性例如${book.coverUrl}而实体类字段其实是cover_url的驼峰转换问题。MyBatis 开启mapUnderscoreToCamelCase后数据库下划线字段才能真正映到实体类驼峰属性不然查询结果全是 null。还有一个经验给网上书店做过二次开发后webapp/WEB-INF/jsp下的页面文件如果新增了一个目录需要在 IDEA 的 Artifacts 配置里勾选Build on make否则新加的 JSP 不会自动拷贝到 target 目录运行时还是报 404。用 exploded 方式部署时尤其容易出现这个问题Rebuild Project 有时也解决不了脱掉部署并重新配置 Artifact 反而是最快的办法。本文还有配套的精品资源点击获取
返回列表