上玩具銷售系統(tǒng):從環(huán)境搭建到下單事務(wù)的完整實(shí)現(xiàn))
簡介這是一份面向高校計(jì)算機(jī)相關(guān)專業(yè)畢業(yè)設(shè)計(jì)的完整項(xiàng)目源碼包主題為基于JSP的網(wǎng)上玩具銷售系統(tǒng)適合正在準(zhǔn)備畢設(shè)或需要Java Web實(shí)戰(zhàn)案例的學(xué)生與開發(fā)者參考。項(xiàng)目以JavaServer Pages為核心結(jié)合Servlet、JavaBeans實(shí)現(xiàn)服務(wù)端邏輯涵蓋商品瀏覽、分類導(dǎo)航、購物車、用戶中心與訂單管理等典型電商模塊并涉及數(shù)據(jù)庫設(shè)計(jì)與會(huì)話安全等知識(shí)點(diǎn)。壓縮包共1059個(gè)文件約23.2MB包含76個(gè)jsp頁面、98個(gè)html、29個(gè)css及大量js與圖片素材另有7個(gè)java源文件、7個(gè)class、5個(gè)jar與1個(gè)sql腳本以及doc項(xiàng)目文檔便于理解整體結(jié)構(gòu)與部署流程。目前已有390人學(xué)習(xí)下載。通過該資源可獲取一套可運(yùn)行的畢設(shè)參考方案幫助梳理JSP開發(fā)流程、數(shù)據(jù)庫建表思路與前后端交互方式適合作為課程設(shè)計(jì)或自學(xué)練手素材。1. 基于 JSP 的網(wǎng)上玩具銷售系統(tǒng)從選題到能跑通的完整落地路徑很多同學(xué)拿到「基于 JSP 的網(wǎng)上玩具銷售系統(tǒng)」這個(gè)畢業(yè)設(shè)計(jì)題目時(shí)第一反應(yīng)是去 GitHub 找一個(gè)現(xiàn)成項(xiàng)目改改交差。但真正動(dòng)手才發(fā)現(xiàn)JSP 項(xiàng)目不像 Spring Boot 那樣開箱即用Tomcat 版本、JDK 版本、數(shù)據(jù)庫驅(qū)動(dòng)、JSTL 標(biāo)簽庫任何一個(gè)環(huán)節(jié)對不上就是 404 或 500。我?guī)н^幾屆學(xué)生的畢設(shè)最常見的翻車場景不是代碼寫不出來而是環(huán)境跑不起來、頁面能打開但數(shù)據(jù)出不來、答辯時(shí)被問「你這個(gè)登錄是怎么驗(yàn)證的」答不上來。這篇筆記面向的是選了 JSP 方向、需要從零搭出一個(gè)能演示、能答辯、能講清楚原理的網(wǎng)上玩具銷售系統(tǒng)的同學(xué)。我會(huì)按「技術(shù)選型 → 數(shù)據(jù)庫設(shè)計(jì) → 核心功能實(shí)現(xiàn) → 避坑 → 進(jìn)階」的順序把每個(gè)環(huán)節(jié)的關(guān)鍵代碼和參數(shù)配置寫清楚。讀完你應(yīng)該能獨(dú)立跑通一個(gè)包含商品展示、購物車、下單、后臺(tái)管理的完整系統(tǒng)并且知道每一層為什么這么寫。2. 技術(shù)選型與環(huán)境搭建為什么 JSP 項(xiàng)目最容易死在第一步2.1 JSP Servlet JDBC 三層結(jié)構(gòu)到底怎么分JSP 本身是服務(wù)端頁面技術(shù)最終會(huì)被容器編譯成 Servlet。很多教程把業(yè)務(wù)邏輯直接寫在 JSP 的% %腳本片段里這樣寫小 demo 沒問題但畢設(shè)要展示架構(gòu)能力答辯老師一定會(huì)問「你的 MVC 體現(xiàn)在哪里」。所以推薦的結(jié)構(gòu)是JSP 層只負(fù)責(zé)展示和表單提交不寫 SQL不寫業(yè)務(wù)判斷Servlet 層接收請求、調(diào)用 Service、控制跳轉(zhuǎn)Service 層業(yè)務(wù)邏輯比如「下單時(shí)扣庫存 生成訂單 清空購物車」DAO 層只做數(shù)據(jù)庫增刪改查一個(gè)方法對應(yīng)一條 SQL實(shí)體類Model和數(shù)據(jù)庫表一一對應(yīng)的 JavaBean這個(gè)分層不是為了好看而是為了答辯時(shí)有話講。比如老師問「如果我要把 MySQL 換成 Oracle 怎么辦」你可以說「只需要改 DAO 層的實(shí)現(xiàn)Service 和 Servlet 不動(dòng)」。2.2 環(huán)境版本組合一套經(jīng)過驗(yàn)證的配置JSP 項(xiàng)目對版本極其敏感下面這套組合是我在多個(gè)機(jī)器上驗(yàn)證過能跑通的組件版本說明JDK8 或 11JDK 17 以上和舊版 Tomcat 有兼容問題Tomcat8.5 或 9.0不要用 Tomcat 10包名從 javax 變成 jakartaMySQL5.7 或 8.08.0 需要改驅(qū)動(dòng)類和連接串mysql-connector-java5.1.49 或 8.0.28與 MySQL 版本對應(yīng)JSTL1.2必須放對位置否則標(biāo)簽不解析IDEIDEA 或 EclipseIDEA 需要配置 Artifact提示Tomcat 10 把javax.servlet全部改成了jakarta.servlet網(wǎng)上絕大多數(shù) JSP 教程代碼在 Tomcat 10 下直接編譯報(bào)錯(cuò)。畢設(shè)求穩(wěn)用 Tomcat 9。2.3 在 IDEA 里跑通第一個(gè) JSP 頁面的最小步驟新建項(xiàng)目后目錄結(jié)構(gòu)應(yīng)該是這樣toy-shop/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/toy/ │ │ ├── dao/ │ │ ├── service/ │ │ ├── servlet/ │ │ └── entity/ │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ │ ├── css/ │ ├── js/ │ └── index.jspweb.xml最小配置?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-appindex.jsp先寫一個(gè)能顯示當(dāng)前時(shí)間的頁面驗(yàn)證環(huán)境% page contentTypetext/html;charsetUTF-8 languagejava % % page importjava.util.Date % html headtitle玩具商城/title/head body h1網(wǎng)上玩具銷售系統(tǒng)/h1 p服務(wù)器時(shí)間% new Date() %/p /body /html配置 Tomcat Run Configuration 時(shí)注意 Deployment 標(biāo)簽頁里 Application context 設(shè)為/toy這樣訪問地址就是http://localhost:8080/toy/index.jsp。如果這一步 404先檢查 Artifact 有沒有正確添加 Web Application: Exploded。3. 數(shù)據(jù)庫設(shè)計(jì)與實(shí)體映射玩具銷售系統(tǒng)的表怎么建才不返工3.1 六張核心表與字段設(shè)計(jì)玩具銷售系統(tǒng)的數(shù)據(jù)模型比通用商城簡單但有幾個(gè)玩具品類特有的點(diǎn)適齡段、安全認(rèn)證、品牌。下面是我一般會(huì)用的表結(jié)構(gòu)CREATE DATABASE toy_shop DEFAULT CHARSET utf8mb4; USE toy_shop; -- 用戶表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20), address VARCHAR(200), role TINYINT DEFAULT 0 COMMENT 0普通用戶 1管理員, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 商品表 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, category_id INT, age_range VARCHAR(20) COMMENT 適齡段如3-6歲, brand VARCHAR(50), image VARCHAR(200), description TEXT, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ); -- 分類表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); -- 購物車表 CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 訂單表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已發(fā)貨 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 訂單明細(xì)表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT, price DECIMAL(10,2) );注意訂單表我用了orders而不是order因?yàn)閛rder是 SQL 關(guān)鍵字用它會(huì)報(bào)語法錯(cuò)誤這是血淚經(jīng)驗(yàn)。3.2 實(shí)體類與 DAO 的對應(yīng)寫法以 Product 為例實(shí)體類字段名和表字段一一對應(yīng)package com.toy.entity; public class Product { private Integer id; private String name; private Double price; private Integer stock; private Integer categoryId; private String ageRange; private String brand; private String image; private String description; private Integer status; // getter/setter 省略 }DAO 層用 JDBC 工具類統(tǒng)一管理連接避免每個(gè)方法都寫一遍 DriverManagerpackage com.toy.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/toy_shop?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密碼; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) {} try { if (ps ! null) ps.close(); } catch (SQLException e) {} try { if (conn ! null) conn.close(); } catch (SQLException e) {} } }連接串里的serverTimezoneAsia/Shanghai必須加否則 MySQL 8.0 會(huì)報(bào)時(shí)區(qū)錯(cuò)誤。characterEncodingutf8不加的話中文商品名會(huì)變成問號(hào)。3.3 商品查詢的 DAO 實(shí)現(xiàn)與分頁參數(shù)商品列表頁需要分頁核心 SQL 用LIMITpublic ListProduct findByPage(int page, int pageSize, String keyword) { ListProduct list new ArrayList(); String sql SELECT * FROM product WHERE status1 AND name LIKE ? LIMIT ?,?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % (keyword null ? : keyword) %); ps.setInt(2, (page - 1) * pageSize); ps.setInt(3, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setPrice(rs.getDouble(price)); p.setImage(rs.getString(image)); p.setAgeRange(rs.getString(age_range)); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }page從 1 開始(page-1)*pageSize是偏移量。如果前端傳的 page 是 0 或者負(fù)數(shù)這里會(huì)拋異常所以 Servlet 層要做一次校驗(yàn)int page req.getParameter(page) null ? 1 : Integer.parseInt(...)并且判斷小于 1 就設(shè)為 1。4. 核心功能實(shí)現(xiàn)從商品展示到下單的完整鏈路4.1 商品列表頁的 JSTL 渲染與圖片路徑處理JSP 里不要用腳本片段遍歷列表用 JSTL 的c:forEach% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % ... div classproduct-list c:forEach items${productList} varp div classproduct-item img src${pageContext.request.contextPath}/upload/${p.image} alt${p.name} width200 height200 h3${p.name}/h3 p適齡${p.ageRange}/p p classpricefmt:formatNumber value${p.price} pattern0.00//p a href${pageContext.request.contextPath}/product/detail?id${p.id}查看詳情/a /div /c:forEach /div${pageContext.request.contextPath}會(huì)輸出/toy這樣圖片路徑就是/toy/upload/xxx.jpg。如果直接寫/upload/xxx.jpg在非根 context 下會(huì) 404。圖片上傳目錄建議放在 webapp 下的upload文件夾Tomcat 部署后可以直接訪問。4.2 購物車Session 還是數(shù)據(jù)庫購物車有兩種實(shí)現(xiàn)方式畢設(shè)里我推薦用數(shù)據(jù)庫存因?yàn)榇疝q時(shí)老師更認(rèn)可「數(shù)據(jù)持久化」的說法。但 Session 實(shí)現(xiàn)更簡單適合時(shí)間緊的情況。這里給數(shù)據(jù)庫版本的 ServletWebServlet(/cart/add) public class CartAddServlet extends HttpServlet { private CartService cartService new CartServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user (User) req.getSession().getAttribute(user); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } int productId Integer.parseInt(req.getParameter(productId)); int quantity Integer.parseInt(req.getParameter(quantity)); cartService.addToCart(user.getId(), productId, quantity); resp.sendRedirect(req.getContextPath() /cart/list); } }Service 層的addToCart要先查購物車?yán)锸欠褚延性撋唐酚芯透聰?shù)量沒有就插入public void addToCart(int userId, int productId, int quantity) { Cart existing cartDao.findByUserAndProduct(userId, productId); if (existing ! null) { cartDao.updateQuantity(existing.getId(), existing.getQuantity() quantity); } else { Cart cart new Cart(); cart.setUserId(userId); cart.setProductId(productId); cart.setQuantity(quantity); cartDao.insert(cart); } }4.3 下單流程事務(wù)處理與庫存扣減下單是最容易出 bug 的地方。必須保證「扣庫存、生成訂單、生成訂單明細(xì)、清空購物車」四步在同一個(gè)事務(wù)里。JDBC 手動(dòng)事務(wù)寫法public String createOrder(int userId, ListCart cartList) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 計(jì)算總金額并校驗(yàn)庫存 double total 0; for (Cart cart : cartList) { Product p productDao.findById(conn, cart.getProductId()); if (p.getStock() cart.getQuantity()) { conn.rollback(); return 庫存不足 p.getName(); } total p.getPrice() * cart.getQuantity(); } // 2. 生成訂單 String orderNo System.currentTimeMillis() userId; int orderId orderDao.insert(conn, orderNo, userId, total); // 3. 扣庫存 寫明細(xì) for (Cart cart : cartList) { productDao.reduceStock(conn, cart.getProductId(), cart.getQuantity()); orderItemDao.insert(conn, orderId, cart.getProductId(), cart.getQuantity(), cart.getPrice()); } // 4. 清空購物車 cartDao.deleteByUser(conn, userId); conn.commit(); return orderNo; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return null; } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (SQLException e) {} DBUtil.close(conn, null, null); } }關(guān)鍵點(diǎn)所有 DAO 方法都要接收同一個(gè)Connection參數(shù)不能各自去DBUtil.getConnection()否則事務(wù)不生效。這是 JSP 畢設(shè)里最隱蔽的坑之一。4.4 后臺(tái)管理的權(quán)限攔截后臺(tái)頁面不能只靠「不顯示鏈接」來保護(hù)必須在 Servlet 里判斷角色WebServlet(/admin/*) public class AdminFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; User user (User) request.getSession().getAttribute(user); if (user null || user.getRole() ! 1) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }用 Filter 攔截/admin/*路徑比在每個(gè) Servlet 里寫判斷更可靠。web.xml 里配置 filter-mapping 或者用WebFilter注解都可以。5. 避坑與排查JSP 玩具商城最常見的 5 個(gè)翻車現(xiàn)場5.1 現(xiàn)象頁面顯示 404控制臺(tái)無報(bào)錯(cuò)原因URL 路徑寫錯(cuò)或者 Servlet 的WebServlet注解值和訪問路徑不一致。常見的是 JSP 里表單 action 寫了/cart/add但項(xiàng)目 context 是/toy實(shí)際應(yīng)該訪問/toy/cart/add。解決所有鏈接和表單 action 統(tǒng)一用${pageContext.request.contextPath}/xxx不要手寫絕對路徑。在瀏覽器 F12 的 Network 里看實(shí)際請求 URL 和響應(yīng)狀態(tài)。5.2 現(xiàn)象JSTL 標(biāo)簽不解析頁面直接顯示c:forEach原因jstl.jar和standard.jar沒有放到WEB-INF/lib下或者只放到了 IDEA 的 Libraries 里但沒有打包進(jìn) Artifact。解決確認(rèn)WEB-INF/lib下有jstl-1.2.jar然后在 IDEA 的 Project Structure → Artifacts → 輸出布局里確認(rèn)這個(gè) jar 被包含。Tomcat 9 只需要jstl-1.2.jar一個(gè)文件。5.3 現(xiàn)象中文商品名存入數(shù)據(jù)庫變成???原因數(shù)據(jù)庫連接串沒有加characterEncodingutf8或者數(shù)據(jù)庫建庫時(shí)沒有指定utf8mb4。解決連接串加characterEncodingutf8建庫用DEFAULT CHARSET utf8mb4JSP 頁面頭部加% page contentTypetext/html;charsetUTF-8 %。三處都對了才不會(huì)亂碼。5.4 現(xiàn)象下單后庫存沒變但訂單生成了原因DAO 方法各自獲取了獨(dú)立的 Connection導(dǎo)致事務(wù)沒有覆蓋所有操作。解決把 Connection 作為參數(shù)在 DAO 方法間傳遞Service 層統(tǒng)一控制setAutoCommit(false)和commit/rollback。檢查每個(gè) DAO 方法是不是都用了傳入的 conn。5.5 現(xiàn)象Tomcat 啟動(dòng)報(bào)ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 驅(qū)動(dòng) jar 沒有放到WEB-INF/lib或者 MySQL 5.x 用了com.mysql.cj.jdbc.Driver5.x 應(yīng)該用com.mysql.jdbc.Driver。解決MySQL 8.0 用com.mysql.cj.jdbc.Drivermysql-connector-java-8.0.28.jarMySQL 5.7 用com.mysql.jdbc.Drivermysql-connector-java-5.1.49.jar。驅(qū)動(dòng) jar 必須放WEB-INF/lib不要只加在 IDEA 的模塊依賴?yán)铩?. 進(jìn)階技巧讓畢設(shè)從「能跑」變成「能拿高分」6.1 用過濾器統(tǒng)一解決 POST 中文亂碼每個(gè) Servlet 都寫req.setCharacterEncoding(UTF-8)太啰嗦用一個(gè) Filter 統(tǒng)一處理WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }這個(gè) Filter 要放在所有其他 Filter 之前執(zhí)行WebFilter的 order 不好控制的話就在 web.xml 里按順序配置。6.2 商品圖片上傳的坐標(biāo)定位裁剪思路熱搜里有人問「jsp圖片如何對坐標(biāo)定位」實(shí)際場景是商品主圖需要統(tǒng)一尺寸。常見做法是前端用 cropper.js 讓用戶框選區(qū)域拿到 x、y、width、height 四個(gè)參數(shù)后端用 Java 的BufferedImage裁剪public static void cropImage(String src, String dest, int x, int y, int width, int height) throws IOException { BufferedImage original ImageIO.read(new File(src)); BufferedImage cropped original.getSubimage(x, y, width, height); ImageIO.write(cropped, jpg, new File(dest)); }getSubimage的四個(gè)參數(shù)就是前端傳過來的坐標(biāo)和尺寸。注意 xwidth 不能超過原圖寬度否則拋RasterFormatException所以后端要加邊界校驗(yàn)。6.3 答辯時(shí)怎么講清楚「JSP 和 Servlet 的關(guān)系」這是高頻問題。你可以這樣回答JSP 本質(zhì)是一個(gè)特殊的 Servlet第一次被訪問時(shí)會(huì)被 Tomcat 的 Jasper 編譯器翻譯成 Java 文件再編譯成 class 文件。JSP 里寫的 HTML 會(huì)被out.write()輸出腳本片段會(huì)變成_jspService方法里的代碼。所以 JSP 適合做展示Servlet 適合做控制兩者配合就是 MVC。能講清楚這個(gè)老師基本不會(huì)再追問。6.4 一個(gè)我踩過的坑不要在 JSP 里直接 new DAO早期我圖省事在 JSP 里寫% ProductDao dao new ProductDao(); %然后遍歷數(shù)據(jù)。這樣寫頁面能出來但一旦數(shù)據(jù)庫連接出問題整個(gè)頁面直接 500而且異常堆棧指向 JSP 編譯后的文件根本看不懂。后來全部改成 Servlet 先查好數(shù)據(jù)req.setAttribute傳到 JSPJSP 只負(fù)責(zé)渲染。這個(gè)習(xí)慣讓我后面調(diào) bug 的時(shí)間少了一半。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取