
簡介基于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和支付寶公鑰是配對的——很多同學在沙箱環境下復制錯公鑰驗簽總是失敗。本文還有配套的精品資源點擊獲取