☰
JSP+MySQL农产品商城源码实战:从环境搭建到下单事务的完整拆解
2026/10/7 11:57:30 网站建设 项目流程

简介:本资源为基于Java的农产品网上销售系统设计与实现文档,面向计算机相关专业学生、课程设计或毕业设计开发者,以及希望了解电商系统开发流程的初学者。文档围绕农产品线上销售场景,从需求分析、系统设计到功能实现进行了完整阐述,涵盖用户浏览购物、商品管理、订单处理等核心模块,并采用JSP开发语言与MySQL数据库保证系统稳定性,采用模块化设计便于后期扩展维护。资源包共1个docx文件,大小约1.27MB,内容包含摘要、英文摘要、目录及绪论等章节,结构清晰,便于按章节查阅与参考。目前已有72人学习下载,适合需要撰写开题报告、系统设计文档或参考电商项目实现思路的读者,可帮助快速理解农产品销售系统的整体架构与开发要点,具有较高的参考与借鉴价值。

1. 从一份 JSP 农产品商城源码说起:它到底能跑出什么

打开这份《基于java的农产品网上销售系统设计与实现.docx》,第一眼看到的是完整的毕业论文结构:绪论、技术简介、需求分析、系统设计、系统实现、测试,一路到参考文献。但真正让一线开发者愿意花时间拆它的原因,不是论文写得多规范,而是它背后对应着一套可落地的 JSP+MySQL 农产品商城实现方案。农产品网上销售系统这个选题在高校毕设里出现频率极高,但大多数文档只讲“做了什么”,不讲“怎么跑起来、哪里会翻车”。这份材料难得地把用户模块和管理员模块的功能边界、数据库实体、活动图都列了出来,等于给了你一张施工图。

它适合三类人:正在做 JavaWeb 课程设计、需要一套能改能交的系统;想用 JSP 技术栈练手完整电商流程的初学者;以及需要一份“需求分析到数据库设计”完整参照的开发者。系统本身不复杂,用户端覆盖注册登录、商品浏览、购物车、订单、退货、留言,管理端覆盖会员、类别、商品、订单、退货审核。麻雀虽小,但电商该有的闭环它都有。接下来我不复述论文,而是把它当成一个真实项目来拆:环境怎么搭、数据库怎么建、核心模块怎么写、哪些地方最容易卡住。

2. 环境与数据库落地:JSP+MySQL 跑通的第一步

2.1 技术栈选型与运行环境确认

这套系统用的是 JSP 作为页面技术、MySQL 存数据、B/S 三层结构,开发工具是 MyEclipse,服务器是 Tomcat。这套组合在今天看来偏传统,但它的好处是资料多、门槛低、学校机房环境普遍能跑。选型上没必要纠结“为什么不用 Spring Boot”,因为这份资源的价值就在于它是一套纯 JSP+Servlet 的经典实现,你能清楚看到请求怎么从浏览器到 Servlet 再到 DAO 最后落库,中间没有框架帮你藏黑匣子。

运行环境按文档给的最低配置来就行:Windows XP 或 Win7 均可,CPU 酷睿 i3 以上、内存 2G 以上、硬盘 320GB。实际我建议直接上 Win10/Win11,Tomcat 用 8.5 或 9.0 版本,JDK 用 1.8,MySQL 用 5.7。这里有个血泪经验:JDK 版本别乱升,JSP 老项目在 JDK 11 以上容易遇到javax.servlet包找不到的问题,因为高版本 JDK 移除了部分 Java EE 模块。MySQL 5.7 和 8.0 的驱动类名不同,5.7 用com.mysql.jdbc.Driver,8.0 用com.mysql.cj.jdbc.Driver,连接串还要加时区和 SSL 参数,这个后面避坑章节会细说。

环境变量配置是新手第一道坎。JAVA_HOME指向 JDK 安装目录,CATALINA_HOME指向 Tomcat 目录,Path里加上%JAVA_HOME%\bin和%CATALINA_HOME%\bin。配完在命令行敲java -version和startup.bat,能起来就说明基础环境没问题。

2.2 数据库表设计与建表脚本

文档第 4 章给了数据库概念设计和实体,但没给完整建表语句。按农产品商城的业务闭环,核心表至少要有:用户表、商品表、商品类别表、购物车表、订单表、订单明细表、退货表、留言表。我一般会先画实体关系再落 SQL,下面这套建表脚本是按文档描述的实体补全的,字段类型和约束都按实际业务来定。

-- 创建数据库,字符集用 utf8mb4 兼容中文和特殊符号 CREATE DATABASE agri_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE agri_shop; -- 用户表:区分普通用户和管理员,role 字段控制权限 CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(64) NOT NULL COMMENT '建议存 MD5 或 BCrypt 哈希', real_name VARCHAR(50), phone VARCHAR(20), address VARCHAR(200), role TINYINT DEFAULT 0 COMMENT '0普通用户 1管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品类别表 CREATE TABLE t_category ( cate_id INT PRIMARY KEY AUTO_INCREMENT, cate_name VARCHAR(50) NOT NULL, cate_desc VARCHAR(200) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表:price 用 DECIMAL 避免浮点误差,stock 控制库存 CREATE TABLE t_product ( pro_id INT PRIMARY KEY AUTO_INCREMENT, pro_name VARCHAR(100) NOT NULL, cate_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, pro_img VARCHAR(200) COMMENT '图片相对路径', pro_desc TEXT, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', FOREIGN KEY (cate_id) REFERENCES t_category(cate_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 购物车表:同一用户同一商品只存一条,数量累加 CREATE TABLE t_cart ( cart_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, pro_id INT NOT NULL, quantity INT DEFAULT 1, UNIQUE KEY uk_user_pro (user_id, pro_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表:order_no 用时间戳+随机数生成,避免自增 ID 暴露业务量 CREATE TABLE t_order ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, order_status TINYINT DEFAULT 0 COMMENT '0待付款 1已付款 2已发货 3已完成 4已取消', receiver VARCHAR(50), receiver_phone VARCHAR(20), receiver_addr VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表:下单时把商品快照写进来,防止商品改价影响历史订单 CREATE TABLE t_order_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, pro_id INT NOT NULL, pro_name VARCHAR(100) COMMENT '下单时的商品名快照', price DECIMAL(10,2) COMMENT '下单时的单价快照', quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES t_order(order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 退货表 CREATE TABLE t_return ( return_id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, user_id INT NOT NULL, reason VARCHAR(500), audit_status TINYINT DEFAULT 0 COMMENT '0待审核 1通过 2驳回', apply_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 留言表 CREATE TABLE t_message ( msg_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, content VARCHAR(1000), reply VARCHAR(1000), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段脚本有几个参数值得说明。DECIMAL(10,2)用于金额,比FLOAT可靠,电商系统里金额算错是致命的。t_order_item里冗余存了pro_name和price,这是订单快照的常见做法,因为商品可能改价或下架,历史订单必须保留成交时的信息。t_cart上建了user_id+pro_id的唯一索引,配合INSERT ... ON DUPLICATE KEY UPDATE quantity=quantity+1就能实现“重复加购只累加数量”。字符集统一用utf8mb4,农产品名称里偶尔会有生僻字,utf8存不下。

建完表后,在项目里配置数据库连接。JSP 项目通常用db.properties或直接在工具类里写死,我建议抽成配置文件:

# db.properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/agri_shop?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你的密码

useUnicode=true&characterEncoding=utf8解决中文乱码,useSSL=false在本地开发时关掉 SSL 警告。如果是 MySQL 8.0,驱动类改成com.mysql.cj.jdbc.Driver,URL 末尾加&serverTimezone=Asia/Shanghai,否则会报时区错误。

3. 核心模块实现:从登录到下单的代码拆解

3.1 登录鉴权与权限拦截

登录模块是整个系统的入口,文档里的活动图写得很清楚:判断账号是否存在、验证码是否正确、识别用户角色。实现上分三层:JSP 页面收集表单、Servlet 处理请求、DAO 查库比对。下面是一个典型的登录 Servlet 写法。

// LoginServlet.java @WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String code = req.getParameter("code"); // 1. 校验验证码,session 里存的是生成时的值 String sessionCode = (String) req.getSession().getAttribute("checkCode"); if (code == null || !code.equalsIgnoreCase(sessionCode)) { req.setAttribute("msg", "验证码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 2. 密码做 MD5 后再比对,数据库不存明文 String md5Pwd = MD5Util.encode(password); UserDao dao = new UserDao(); User user = dao.findByUsernameAndPwd(username, md5Pwd); if (user == null) { req.setAttribute("msg", "账号或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 3. 登录成功写 session,按角色跳不同首页 req.getSession().setAttribute("loginUser", user); if (user.getRole() == 1) { resp.sendRedirect(req.getContextPath() + "/admin/index.jsp"); } else { resp.sendRedirect(req.getContextPath() + "/index.jsp"); } } }

逻辑说明:验证码校验放在最前面,防止暴力破解;密码用 MD5 哈希后比对,数据库里存的是哈希值不是明文;登录成功后把User对象放进 session,后续页面通过session.getAttribute("loginUser")判断是否登录。参数上,req.setCharacterEncoding("UTF-8")必须在取参数之前调用,否则中文用户名会乱码。

权限拦截不能只靠页面隐藏菜单。常见做法是写一个LoginFilter,拦截/admin/*路径,检查 session 里有没有管理员身份,没有就踢回登录页。

// LoginFilter.java @WebFilter("/admin/*") public class LoginFilter implements Filter { 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("loginUser"); // 未登录或不是管理员,一律拦截 if (user == null || user.getRole() != 1) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

这个过滤器是权限控制的关键,很多毕设系统只在前端用if判断菜单显示,后端接口裸奔,直接改 URL 就能进管理页,这是典型的安全漏洞。

3.2 购物车与订单事务处理

购物车和下单是电商系统里最容易出 bug 的地方,核心难点在库存扣减和事务一致性。加购逻辑相对简单,用唯一索引配合ON DUPLICATE KEY UPDATE就能搞定。

// CartDao.java 添加购物车 public void addToCart(int userId, int proId, int quantity) { String sql = "INSERT INTO t_cart(user_id, pro_id, quantity) VALUES(?,?,?) " + "ON DUPLICATE KEY UPDATE quantity = quantity + ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, proId); ps.setInt(3, quantity); ps.setInt(4, quantity); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }

下单才是重头戏。用户点“提交订单”时,要做三件事:生成订单主记录、写入订单明细、扣减商品库存。这三步必须在一个事务里,任何一步失败都要回滚,否则会出现“订单生成了但库存没扣”或者“库存扣了但订单没生成”的脏数据。

// OrderService.java 下单核心逻辑 public String createOrder(int userId, List<CartItem> items, String receiver, String phone, String addr) throws SQLException { Connection conn = null; PreparedStatement psOrder = null; PreparedStatement psItem = null; PreparedStatement psStock = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 计算总金额并生成订单号 BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { total = total.add(item.getPrice().multiply(new BigDecimal(item.getQuantity()))); } String orderNo = System.currentTimeMillis() + "" + (int)(Math.random()*9000+1000); // 2. 插入订单主表 String sqlOrder = "INSERT INTO t_order(order_no,user_id,total_amount,receiver,receiver_phone,receiver_addr) VALUES(?,?,?,?,?,?)"; psOrder = conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, orderNo); psOrder.setInt(2, userId); psOrder.setBigDecimal(3, total); psOrder.setString(4, receiver); psOrder.setString(5, phone); psOrder.setString(6, addr); psOrder.executeUpdate(); ResultSet rs = psOrder.getGeneratedKeys(); int orderId = 0; if (rs.next()) orderId = rs.getInt(1); // 3. 循环插入明细并扣库存,库存不足直接抛异常回滚 String sqlItem = "INSERT INTO t_order_item(order_id,pro_id,pro_name,price,quantity) VALUES(?,?,?,?,?)"; String sqlStock = "UPDATE t_product SET stock = stock - ? WHERE pro_id = ? AND stock >= ?"; psItem = conn.prepareStatement(sqlItem); psStock = conn.prepareStatement(sqlStock); for (CartItem item : items) { psItem.setInt(1, orderId); psItem.setInt(2, item.getProId()); psItem.setString(3, item.getProName()); psItem.setBigDecimal(4, item.getPrice()); psItem.setInt(5, item.getQuantity()); psItem.addBatch(); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getProId()); psStock.setInt(3, item.getQuantity()); if (psStock.executeUpdate() == 0) { throw new SQLException("商品[" + item.getProName() + "]库存不足"); } } psItem.executeBatch(); conn.commit(); // 全部成功才提交 return orderNo; } catch (SQLException e) { if (conn != null) conn.rollback(); // 任何异常回滚 throw e; } finally { if (conn != null) conn.close(); } }

这段代码的关键点:conn.setAutoCommit(false)关闭自动提交开启事务;扣库存的 SQL 带了AND stock >= ?条件,利用数据库行锁保证并发下不会超卖,如果返回影响行数为 0 说明库存不够,直接抛异常触发回滚;Statement.RETURN_GENERATED_KEYS用来拿刚插入订单的自增主键,供明细表关联。参数上,订单号用时间戳加随机数,保证唯一的同时不暴露业务量。

3.3 商品管理与分页查询

管理端的商品管理是增删改查的典型场景,用户端的商品列表则必须做分页,否则商品一多页面直接卡死。分页的核心是LIMIT offset, size,配合一条COUNT(*)查总数。

// ProductDao.java 分页查询 public PageBean<Product> findByPage(int pageNum, int pageSize, Integer cateId) { PageBean<Product> page = new PageBean<>(); StringBuilder where = new StringBuilder(" WHERE status = 1 "); List<Object> params = new ArrayList<>(); if (cateId != null) { where.append(" AND cate_id = ? "); params.add(cateId); } // 先查总数 String countSql = "SELECT COUNT(*) FROM t_product" + where; // 再查当前页数据,按上架时间倒序 String dataSql = "SELECT * FROM t_product" + where + " ORDER BY pro_id DESC LIMIT ?,?"; try (Connection conn = DBUtil.getConnection()) { PreparedStatement psCount = conn.prepareStatement(countSql); for (int i = 0; i < params.size(); i++) psCount.setObject(i+1, params.get(i)); ResultSet rs = psCount.executeQuery(); int total = rs.next() ? rs.getInt(1) : 0; PreparedStatement psData = conn.prepareStatement(dataSql); int idx = 1; for (Object p : params) psData.setObject(idx++, p); psData.setInt(idx++, (pageNum - 1) * pageSize); // offset psData.setInt(idx, pageSize); ResultSet rsData = psData.executeQuery(); List<Product> list = new ArrayList<>(); while (rsData.next()) { Product p = new Product(); p.setProId(rsData.getInt("pro_id")); p.setProName(rsData.getString("pro_name")); p.setPrice(rsData.getBigDecimal("price")); p.setStock(rsData.getInt("stock")); p.setProImg(rsData.getString("pro_img")); list.add(p); } page.setList(list); page.setTotalCount(total); page.setPageNum(pageNum); page.setPageSize(pageSize); } catch (SQLException e) { e.printStackTrace(); } return page; }

参数说明:offset = (pageNum - 1) * pageSize,这是分页公式,pageNum 从 1 开始。ORDER BY pro_id DESC让新上架的商品排在前面。where条件用 StringBuilder 动态拼接,配合参数列表防止 SQL 注入,比直接字符串拼接安全得多。商品图片上传用commons-fileupload组件处理,存到项目外的独立目录,数据库只存相对路径,这样重新部署 war 包时图片不会丢。

4. 避坑与排查:这套 JSP 商城最容易翻车的地方

4.1 中文乱码三处源头

现象:商品名、用户名、留言内容在页面上显示成问号或乱码。原因通常有三个源头:JSP 页面编码、请求编码、数据库编码。解决要三处一起改。JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;Servlet 里取参数前调req.setCharacterEncoding("UTF-8");数据库连接串加characterEncoding=utf8,建库建表用utf8mb4。如果是 GET 请求传中文,还要改 Tomcat 的server.xml,在 Connector 标签加URIEncoding="UTF-8"。三处缺一处都可能乱码,这是新手最常卡的地方。

4.2 数据库连接报错与驱动版本

现象:启动后访问页面报ClassNotFoundException: com.mysql.jdbc.Driver或No suitable driver found。原因是驱动 jar 没放进WEB-INF/lib目录,或者 MySQL 版本和驱动类名不匹配。MySQL 5.7 用com.mysql.jdbc.Driver,8.0 用com.mysql.cj.jdbc.Driver。另外 MySQL 8.0 必须加serverTimezone=Asia/Shanghai,否则报时区错误。驱动 jar 一定要放WEB-INF/lib下,放项目根目录或者 Tomcat 的 lib 下都可能因为类加载顺序出问题。

4.3 库存超卖与事务失效

现象:并发下单时库存扣成负数,或者订单生成了但库存没减。原因是没开事务,或者扣库存的 SQL 没加库存判断条件。解决:下单逻辑必须用conn.setAutoCommit(false)开启事务,扣库存 SQL 写成UPDATE t_product SET stock = stock - ? WHERE pro_id = ? AND stock >= ?,靠数据库行锁和条件判断双重保险。还要注意,如果 DAO 里每次操作都从连接池拿新连接,事务会失效,必须保证下单过程中的所有操作共用同一个 Connection 对象。

4.4 图片上传路径与部署丢失

现象:本地测试图片能显示,重新部署 war 包后图片全没了。原因是图片存到了项目编译输出目录里,重新部署被覆盖。解决:图片存到项目外的独立目录,比如D:/agri_upload/,数据库存相对路径,页面上用<img src="${pageContext.request.contextPath}/upload/${product.proImg}">访问,再配一个 Servlet 或 Tomcat 虚拟路径映射到真实目录。这样重新部署不影响已上传的图片。

4.5 session 失效与登录状态丢失

现象:用户登录后操作几步就跳回登录页。原因是 session 默认 30 分钟超时,或者 Tomcat 重启导致 session 丢失。解决:在web.xml里配置<session-config><session-timeout>60</session-timeout></session-config>延长超时时间。如果是 Tomcat 重启丢 session,开发阶段可以接受,生产环境要考虑 session 持久化或改用 token 方案。另外注意,浏览器禁用 cookie 时 session 也会失效,需要 URL 重写兜底。

5. 二次开发与验证:让这套系统真正能用起来

拿到这套源码后,别急着改业务,先做一轮完整验证。我的习惯是:建库、导数据、配连接、启动 Tomcat、走一遍“注册→登录→浏览商品→加购→下单→后台发货→确认收货→申请退货→后台审核”的全流程。这条链路能跑通,说明系统骨架是完整的。跑不通的地方,就是你要重点排查的点。

验证通过后,二次开发可以从三个方向入手。第一是安全加固,把密码哈希从 MD5 升级到 BCrypt,加登录失败次数限制,给所有表单加 CSRF token。第二是功能扩展,比如接入支付宝沙箱做真实支付回调,或者加一个农产品溯源信息字段,扫码能看到产地和检测报告,这个在农产品场景里很实用。第三是性能优化,商品列表加 Redis 缓存,热门商品不每次都查库;分页查询在cate_id和status上建联合索引。

这里给一个 BCrypt 替换 MD5 的示例,依赖jbcrypt库:

// 注册时加密 String hashed = BCrypt.hashpw(password, BCrypt.gensalt(12)); // 登录时校验 boolean match = BCrypt.checkpw(inputPassword, storedHash);

gensalt(12)里的 12 是计算强度,值越大越安全但越慢,一般 10 到 12 够用。BCrypt 每次生成的盐不同,所以同一个密码每次哈希结果都不一样,比 MD5 安全得多。

还有一个容易被忽略的点:这套系统的订单状态流转是硬编码的数字,0 到 4 分别代表不同状态。二次开发时建议抽成枚举类,避免代码里到处是魔法数字,改起来容易漏。

public enum OrderStatus { UNPAID(0, "待付款"), PAID(1, "已付款"), SHIPPED(2, "已发货"), FINISHED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } }

从那以后我每次接手这类 JSP 老项目,都强制先跑一遍全链路,再动一行代码。因为这类系统的坑往往不在业务逻辑本身,而在环境、编码、事务这些基础设施上,先把地基验一遍,后面改起来才踏实。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询