ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SSM整合教科书:网上书城系统从源码到部署全解析

SSM整合教科书:网上书城系统从源码到部署全解析 简介基于SSM架构的网上书城系统完整源码包面向Java Web开发学习者与课程设计人群演示了Spring、SpringMVC、MyBatis三大框架的整合应用。项目功能覆盖用户注册登录、个人中心、图书展示与搜索、购物车管理、订单状态跟踪及在线支付等典型电商流程代码结构清晰注释到位适合作为毕业设计或SSM框架实战练习的参考蓝本。压缩包共1253个文件大小14.6MB核心代码以120个java文件、100个jsp页面为主辅以364个js脚本、146个css样式表及png/gif等图片素材同时包含sql数据库脚本和xml配置文件便于直接导入数据库并快速启动项目。目前已有43人学习适合需要完整项目源码进行二次开发或框架学习的读者。1. 网上书城系统SSM整合的教科书式样本这个基于SSM架构的网上书城系统不是那种只跑通单表增删改查的演示品。它把用户注册登录、图书检索、购物车、订单流转、在线支付回调整个闭环都装进了一个工程里而且用的是最传统的JSP Servlet容器方案。拿到源码你会发现它没有Spring Boot自动配置的魔法所有Bean、映射器、视图解析器都是手写XML声明出来的这反而让它的学习价值更高——你能看清楚Spring容器到底在你启动Tomcat时干了什么。对于刚学完Spring、想用实际项目把框架串起来的人或者需要一套能快速二次开发的交易类系统原型的工程师这份代码的参考意义都很大。它同时回答了三个问题SSM整合的规范格式是什么用户购书这条链路涉及哪些状态切换老式Eclipse Web项目的目录结构长什么样。2. SSM整合与项目骨架拆解2.1 为什么选SSM而不是直接上Spring Boot这个源码是典型的Eclipse Dynamic Web Project结构没有Maven的pom.xml依赖全部放在WEB-INF/lib下。放到今天很多人会直接选Spring Boot但SSM自有它的场景一是老系统的维护和迁移需求仍然大量存在二是SSM的显式配置能让人把依赖注入、事务传播、AOP切面这些概念看得明明白白。比如你在applicationContext.xml里看到tx:advice就会直观地理解“事务是把Dao方法包进代理类”这件事。从这个项目里你能学到一种分层惯例Controller只做参数接收和视图转发Service层用Transactional控制数据库事务Dao层写接口和XML映射。书城的核心业务——库存扣减、订单生成——正好需要这种严格的事务边界所以SSM放在这里是合适的不是框架在硬套业务。2.2 项目目录与配置文件的映射关系拿到源码后先看.classpath和org.eclipse.wst.common.component这两个文件它们告诉你这个Web项目的上下文路径和源码目录。正式代码集中在src目录下按com.xxx.bookstore这样的包名分成controller、service、dao、entity四层JSP页面和静态资源在WebContent目录。配置文件的职责分割是这样的配置文件管什么对应技术点applicationContext.xml数据源、事务、Service和Dao的BeanSpring核心容器spring-mvc.xml控制器扫描、视图解析器、静态资源映射SpringMVC家族mybatis-config.xml别名、驼峰转换、映射文件注册MyBatis全局参数db.properties数据库连接串、用户名密码外部化配置这些配置文件的加载顺序决定了项目能不能启动。web.xml里会配置ContextLoaderListener加载applicationContext.xml同时配置DispatcherServlet加载spring-mvc.xml。注意两个容器是父子关系子容器SpringMVC不能扫描Service层否则会出现事务不生效的坑。2.2.1 数据访问层配置的关键代码看一下spring-dao.xml或applicationContext.xml里的对应片段的常见写法!-- 数据库连接池 -- bean iddataSource classorg.apache.commons.dbcp.BasicDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean !-- SqlSessionFactory让MyBatis知道去哪里找XML映射文件 -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:com/xxx/bookstore/dao/*.xml/ /bean !-- 扫描Dao接口为每个接口生成代理对象 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.xxx.bookstore.dao/ /bean这里值得展开的是mapperLocations的路径写法。常见做法是把Mapper XML和Dao接口放在同一个包下这样扫描路径就是classpath:com/xxx/bookstore/dao/*.xml。如果XML放在resources目录路径要改成classpath*:mapper/*.xml。这两个弄混是最常见的启动报错来源。MapperScannerConfigurer不需要sqlSessionFactory属性它会自动从容器里拿。但要注意它不能用Bean方式定义在Configuration类里否则Spring容器初始化顺序不对时会扫描不到接口这个坑我后面还会说。2.3 MyBatis映射文件与实体别名mybatis-config.xml里通常开启驼峰映射configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings typeAliases package namecom.xxx.bookstore.entity/ /typeAliases /configuration开启驼峰映射后数据库字段book_name自动对应实体属性bookName省略一大串resultMap。但这里有个前提你的SQL返回列名必须是下划线风格如果写了select book_name as bookname驼峰映射就失效了。看书城源码里BookDao.xml会发现它的大部分查询都直接用了resultTypeBook靠的正是这个全局配置。3. 用户模块与图书检索的落地写法3.1 用户注册中的MD5加密与校验书城项目的用户表一般有username、password、email、phone这几个字段。注册逻辑里最不能省略的就是密码处理。源码里虽然可能直接用了MD5工具类但这里要提醒你MD5本身并不可逆但彩虹表攻击很容易破解弱密码。合格的做法是加盐public String encryptPassword(String plainPassword, String salt) { String salted plainPassword salt; String md5Hex DigestUtils.md5DigestAsHex(salted.getBytes(StandardCharsets.UTF_8)); // 实际项目里再叠加一次SHA-1或使用BCrypt更好 return md5Hex; } // 注册时生成随机盐 String salt UUID.randomUUID().toString().substring(0, 6); user.setSalt(salt); user.setPassword(encryptPassword(rawPassword, salt));参数说明salt是每个用户独立的随机字符串它让相同明文密码产生不同的密文。验证登录时先根据用户名查出用户再用用户自己的盐去加密输入密码比对结果。这个项目里如果只做了裸MD5你可以顺手改掉不影响其他模块。3.2 登录状态管理Session与拦截器SSM项目最常见的登录校验是使用HandlerInterceptor而不是Servlet的Filter原因是拦截器能拿到Handler对象更细粒度地控制哪些URL需要登录。代码骨架如下public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { // 这里要区分是否为Ajax请求 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setStatus(401); } else { response.sendRedirect(request.getContextPath() /login.jsp); } return false; } return true; } }然后在spring-mvc.xml里注册mvc:interceptors mvc:interceptor mvc:mapping path/cart/**/ mvc:mapping path/order/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ bean classcom.xxx.bookstore.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这里的关键是exclude-mapping。如果你把/books、/search也排除了游客就能浏览图书这符合网上书城的业务逻辑。注意区分/cart/**和/cart前者匹配/cart/add、/cart/list但匹配不到/cart本身需要单独写一条mvc:mapping path/cart/。3.3 图书搜索SQL动态拼装与防注入图书搜索一般按书名、作者、分类三个条件组合查询。源码里的BookDao.xml常见写法是select idsearchBooks resultTypeBook SELECT * FROM book where if testkeyword ! null and keyword ! AND (book_name LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY sale_count DESC LIMIT #{offset}, #{pageSize} /select动态SQL的where标签会自动去掉第一个条件前的AND比手动加where 11干净得多。#{keyword}使用PreparedStatement参数占位不会产生SQL注入。注意这里不要写成${keyword}后者是字符串拼接在搜索框输入 or 11会炸穿全库。分页参数offset和pageSize要从Controller接收并转换成整数再做运算因为LIMIT不支持预编译参数直接运算你可以在Service层先计算出offset (pageNum - 1) * pageSize。这个项目如果是用PageHelper插件则不需要手动算但看源码时注意它到底用的哪种方式会影响你修改时的思路。4. 购物车、订单与支付状态机4.1 购物车是存内存还是存库书城系统网上购物车有两种实现路径匿名用户存Session登录用户存数据库。这个源码采用的是登录后才允许加购物车所以购物车数据直接落库表结构一般是字段类型说明cart_idint PK主键user_idint用户IDbook_idint图书IDquantityint数量checkedtinyint是否选中结算add_timedatetime加入时间落库的好处是换设备购物车不丢坏处是每次进入购物车页面都要查库。如果后续要做性能优化可以用Redis缓存用户购物车以cart:userId为Key使用Hash结构存储bookId - quantity。但要注意库存扣减的时机不能只改Redis最终下单时还是要校验数据库中的真实库存。4.2 订单状态流转与数据表设计订单表是这类系统的核心。状态字段建议用tinyint类型而不是字符串因为数字更好比较、占空间更小。字段status的取值定义需要在代码里以常量类维护public class OrderStatus { public static final int WAIT_PAY 0; // 待付款 public static final int PAID 1; // 已付款 public static final int SHIPPED 2; // 已发货 public static final int COMPLETED 3; // 已完成 public static final int CANCELED 4; // 已取消 }生成订单的时序应该是从购物车读取选中项 → 计算总价 → 创建订单主表记录含订单号 → 批量插入订单明细表 → 扣减库存 → 清空购物车。整个过程必须放在一个事务里否则会出现订单创建成功但库存没扣的问题。Service层方法的典型写法Transactional(rollbackFor Exception.class) public Order createOrder(Integer userId, Integer[] cartIds) { // 1. 查询购物车列表 ListCartItem items cartDao.selectByIds(cartIds); // 2. 校验库存 for (CartItem item : items) { Book book bookDao.selectById(item.getBookId()); if (book.getStock() item.getQuantity()) { throw new BookStockNotEnoughException(库存不足: book.getBookName()); } } // 3. 生成订单主记录 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalPrice(calculateTotal(items)); order.setStatus(OrderStatus.WAIT_PAY); orderDao.insert(order); // 4. 插入明细 for (CartItem item : items) { OrderDetail detail new OrderDetail(); detail.setOrderId(order.getId()); detail.setBookId(item.getBookId()); detail.setQuantity(item.getQuantity()); detail.setPrice(bookDao.selectById(item.getBookId()).getPrice()); orderDetailDao.insert(detail); // 5. 扣库存: update book set stock stock - #{num} where id #{id} and stock #{num} int rows bookDao.deductStock(item.getBookId(), item.getQuantity()); if (rows 0) { throw new BookStockNotEnoughException(扣减失败); } } // 6. 清空购物车 cartDao.deleteByIds(cartIds); return order; }注意方法上的rollbackFor Exception.class默认Spring事务只回滚RuntimeException如果业务异常是受检异常就要用这种方式指定。扣库存的SQL用where stock #{num}通过更新影响行数来判断是否超卖比先select再判断更稳妥。4.3 在线支付的模拟回调与定时关单源码里的在线支付大概率是接入的第三方支付demo但真实支付回调必须处理验签和幂等。如果只是本地模拟一般提供一个paySuccess接口供前端点击模拟支付后调用RequestMapping(/pay/success) ResponseBody public String paySuccess(String orderNo) { Order order orderService.getByOrderNo(orderNo); if (order ! null order.getStatus() OrderStatus.WAIT_PAY) { order.setStatus(OrderStatus.PAID); order.setPayTime(new Date()); orderDao.updateStatus(order); } return success; }关键点在于只更新状态为“待付款”的订单防止重复回调导致订单状态被反向覆盖。真正对接微信或支付宝回调时还需要验证签名参数你拿到的是用RSA签名的内容必须用平台公钥验签不能直接信任请求参数里的orderNo。更完整的状态机体验还会有一个定时任务去扫描超过30分钟未付款的订单并自动取消、恢复库存。SSM项目里可以用Spring Task在applicationContext.xml里启用task:annotation-driven/然后在方法上标注Scheduled(cron 0 */5 * * * *)来轮询。5. 从Eclipse到Tomcat的部署与常见坑5.1 那些.bak文件是做什么的下载解压后你会看到styles.css.bak、index.jsp.bak、setMenu.js.bak、topNav.jsp.bak这类文件。它们是上一任开发者修改文件前的备份意思是“我改动了这些文件但怕改坏了留个底”。对你来说它们没有运行时作用Tomcat不会解析.bak文件的扩展名我们直接把这种后缀的文件删除或者忽略即可。不过它们也有点情报价值从topNav.jsp.bak能看出前端导航栏可能被重写过index.jsp.bak说明首页经历过定制化修改。5.2 部署时最容易炸的三个点我前两次部署这类老工程时踩过几个坑列出来给你排查症状原因处理方式java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServletlib里缺少Spring Web的jar包把spring-webmvc和spring-web的jar放进WEB-INF/libFailed to configure a DataSourcedb.properties路径不对或中文乱码确认*.properties文件编码为UTF-8检查classpath:前缀Invalid bound statement (not found): com.xxx.dao.UserDao.findByNameMapper XML的namespace写错或没被扫描打开XML里的namespace必须与Dao接口全限定名一致最容易忽略的是JDK编译级别。Eclipse导出的项目默认可能是1.5而Tomcat 9要求Java 8导入IDE后第一件事是把项目的Java Compiler改为1.8同时把Tomcat运行时依赖加入Build Path。5.3 快速定位和验证部署不需要完整的流程你只需要先在本地验证两件事第一启动Tomcat后能打开首页第二能注册一个新用户并登录。把这两条链路走通了购物车和订单模块基本不会有大问题。注册接口返回500时看Tomcat catalina日志里的异常栈。如果是org.mybatis.spring.MyBatisSystemException优先检查SQL语句里的表名和字段名老项目的数据库字段经常用desc这种关键词需要加反引号。登录不进去时先看控制台是否打印了SQL没打印说明Mapper接口没注入成功回spring-mvc.xml检查context:component-scan的base-package是否包含Controller目录且没有覆盖到Service实现。支付回调测不通时使用Postman直接往/pay/success发GET请求看订单状态是否更新。能更新就说明代码逻辑没问题问题在网络环境或回调配置上。这个项目里PaymentUtil如果用的是支付宝沙箱请确认app_id和支付宝公钥是配对的——很多同学在沙箱环境下复制错公钥验签总是失败。本文还有配套的精品资源点击获取
返回列表