简介:这份资源是面向Java Web初学者与课程设计学习者的完整手机销售系统项目包,基于JSP与Access数据库实现,覆盖从需求分析、系统设计、编码实现到测试验收的全流程,适合作为毕业设计、课程大作业或自学练手案例。压缩包为zip格式,整体约2.26MB,内含项目报告、详细设计说明书、需求说明书、数据库设计说明书、开题报告、测试计划及全部JSP源代码,文档与代码相互对应,便于理解系统架构与实现细节。系统功能涵盖手机型号展示、库存管理、订单处理、用户注册登录等模块,可帮助读者掌握JSP处理请求、与Access交互及动态页面生成的基本方法。目前已有76人学习下载,对于想积累Java Web实战经验、了解小型管理系统开发流程的读者,是一份结构完整、可直接参考的实践资料。
1. 手机销售系统选 JSP+Access:一个被低估的毕设技术栈
如果你正在翻毕业设计选题,看到「基于 JSP+Access 的手机销售系统」这个题目,第一反应可能是:这技术栈是不是太老了?我一开始也这么想。但带过几届学生、自己也用这套组合交付过小型进销存之后,我的判断变了——它恰恰是「能在两周内跑通、答辩时讲得清、代码量可控」的少数派选择。JSP 负责页面渲染和请求分发,Access 用文件数据库免去装 MySQL 的折腾,手机销售系统这个业务场景又天然包含商品管理、订单、库存、用户四个模块,需求边界清晰。这篇文章不讲空话,我会把从环境搭建、表结构设计、核心页面实现到 Access 那些让人翻车的坑,一条条拆开讲清楚。适合谁看:正在做这个题目的同学,以及想用最低成本验证一个电商类小系统的开发者。
2. 环境与数据库:把 JSP+Access 的骨架先立起来
2.1 为什么这套组合在毕设场景下反而合理
先说服自己,再动手。JSP 是 Java Server Pages 的缩写,本质是嵌了 Java 代码的 HTML,由 Tomcat 这类 Servlet 容器编译执行。Access 是微软的桌面级关系数据库,单个.mdb或.accdb文件就是一个完整数据库。很多人一听 Access 就皱眉,觉得它「不是正经数据库」,但在手机销售系统这种数据量在几千条以内、并发只有答辩老师一个人点的情况,Access 的零配置优势被严重低估了。
对比一下常见替代方案:MySQL 要装服务、配端口、建用户、导驱动,任何一步出错都能卡住半天;SQL Server 体积大,Express 版虽然免费但安装包也不小。Access 只需要一个文件,用 JDBC-ODBC 桥或者 UCanAccess 驱动就能连。代价是并发能力弱、不支持复杂存储过程,但手机销售系统的业务逻辑本来就简单,这些短板碰不到。
选型上我一般这样定:如果题目明确写了 Access,就别自作主张换 MySQL,答辩时老师问「为什么用 Access」你能答上「轻量、免部署、适合中小规模数据」就够了。如果题目没锁死,而你时间充裕,换 MySQL 也不是不行,但本文按 Access 讲,因为这是标题给定的技术栈。
2.2 用 UCanAccess 打通 Java 到 Access 的连接
Access 的连接方式有两种:老式的 JDBC-ODBC 桥(JDK 8 之后已被移除)和纯 Java 的 UCanAccess 驱动。现在必须用后者,否则你会遇到经典的ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver。UCanAccess 需要四个 jar 包:ucanaccess、jackcess、commons-lang、commons-logging,缺一不可。
# 项目 lib 目录下放置以下 jar(版本以你下载到的为准,不要混用大版本) # ucanaccess-x.x.x.jar # jackcess-x.x.x.jar # commons-lang-x.x.jar # commons-logging-x.x.jar # 同时确保 Tomcat 的 lib 或项目 WEB-INF/lib 下能加载到它们连接代码封装成一个工具类,避免每个 Servlet 都写一遍:
// DBUtil.java —— 统一获取 Access 连接 import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 注意:路径用绝对路径或相对于 Tomcat 启动目录的路径,相对路径极易翻车 private static final String DB_URL = "jdbc:ucanaccess://D:/phone_sale/data/phone_sale.accdb;memory=false"; private static final String USER = ""; private static final String PWD = ""; static { try { // UCanAccess 驱动类名,写错会直接 ClassNotFound Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConn() throws SQLException { // memory=false 表示不把整个库加载进内存,数据量大时更稳 return DriverManager.getConnection(DB_URL, USER, PWD); } }逻辑说明:DB_URL里的memory=false是关键参数。UCanAccess 默认会把整个 Access 文件读进内存操作,小库没问题,但如果你在代码里频繁开关连接,内存模式反而容易造成文件锁冲突。参数说明:USER和PWD留空是因为 Access 文件没设密码;如果你的.accdb加了密码,格式是jdbc:ucanaccess://路径;password=你的密码。路径千万别用./data/xxx.accdb这种相对写法,Tomcat 的工作目录和你想象的不一样,这是血泪经验。
2.3 手机销售系统的四张核心表怎么设计
Access 建表可以用图形界面,也可以用 SQL 语句。我建议用 SQL 建,方便你写进项目报告里。四张表:用户表、手机商品表、订单表、订单明细表。
-- 用户表:区分普通用户和管理员 CREATE TABLE t_user ( id AUTOINCREMENT PRIMARY KEY, username TEXT(50) NOT NULL, password TEXT(50) NOT NULL, role TEXT(10) DEFAULT 'user', -- user 或 admin phone TEXT(20), address TEXT(200) ); -- 手机商品表:手机销售系统的核心 CREATE TABLE t_phone ( id AUTOINCREMENT PRIMARY KEY, brand TEXT(50), -- 品牌,如 华为、小米 model TEXT(100), -- 型号 price CURRENCY, -- 价格用货币类型,别用 TEXT stock INT, -- 库存 img_url TEXT(200), -- 图片路径 description MEMO -- 描述用备注类型,可存长文本 ); -- 订单表 CREATE TABLE t_order ( id AUTOINCREMENT PRIMARY KEY, user_id INT, order_no TEXT(50), total CURRENCY, create_time DATETIME, status TEXT(20) -- 待付款/已付款/已发货 ); -- 订单明细表 CREATE TABLE t_order_item ( id AUTOINCREMENT PRIMARY KEY, order_id INT, phone_id INT, quantity INT, subtotal CURRENCY );逻辑说明:Access 的AUTOINCREMENT对应 MySQL 的AUTO_INCREMENT,CURRENCY是货币类型,MEMO是长文本。参数说明:price和total用CURRENCY而不是TEXT,否则排序和求和会按字符串处理,99会排在100后面,这个坑很隐蔽。stock用INT,扣库存时记得加事务或至少判断stock >= quantity,否则并发下会超卖——虽然毕设场景并发低,但代码里体现这个意识,答辩是加分项。
3. 核心功能落地:从商品列表到下单的完整链路
3.1 商品列表页:JSP 里怎么优雅地遍历结果集
商品列表是手机销售系统最常被访问的页面,也是展示你 JSP 功底的地方。原则是:JSP 只负责显示,数据库查询放在 Servlet 或 JavaBean 里。但毕设为了代码直观,很多人直接在 JSP 里写 JDBC,这能跑通,但答辩时老师可能问「MVC 体现在哪」。折中方案:JSP 顶部用<% %>调用一个 DAO 方法拿到List,下面用 JSTL 遍历。
<%@ page contentType="text/html;charset=UTF-8" %> <%@ page import="java.util.List" %> <%@ page import="com.phone.dao.PhoneDAO" %> <%@ page import="com.phone.entity.Phone" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <% // 页面加载时查询全部手机,实际项目应加分页 List<Phone> phoneList = new PhoneDAO().findAll(); request.setAttribute("phoneList", phoneList); %> <html> <head><title>手机列表</title></head> <body> <table border="1"> <tr><th>品牌</th><th>型号</th><th>价格</th><th>库存</th><th>操作</th></tr> <c:forEach var="p" items="${phoneList}"> <tr> <td>${p.brand}</td> <td>${p.model}</td> <td>${p.price}</td> <td>${p.stock}</td> <td><a href="addCart.jsp?id=${p.id}">加入购物车</a></td> </tr> </c:forEach> </table> </body> </html>逻辑说明:PhoneDAO.findAll()内部用DBUtil.getConn()拿连接,执行SELECT * FROM t_phone,把ResultSet逐行映射成Phone对象塞进List。参数说明:<c:forEach>的items属性接收 EL 表达式${phoneList},这要求你request.setAttribute的 key 和它一致。注意 JSTL 需要jstl.jar和standard.jar,Tomcat 10 之后包名从javax变成jakarta,如果你用 Tomcat 10+,taglib的 uri 要相应调整,否则标签不解析,页面直接原样输出<c:forEach>,这是新手最常见的翻车点之一。
3.2 下单逻辑:库存扣减和订单号生成的两个细节
下单是整个系统里唯一需要「事务感」的地方。Access 对事务支持有限,但 JDBC 的setAutoCommit(false)仍然可用。流程是:插入订单主表 → 拿到自增 ID → 插入订单明细 → 扣减库存。任何一步失败就回滚。
// OrderService.java —— 下单核心逻辑 public boolean createOrder(int userId, int phoneId, int quantity) throws SQLException { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 查库存并校验 PreparedStatement ps1 = conn.prepareStatement( "SELECT price, stock FROM t_phone WHERE id=?"); ps1.setInt(1, phoneId); ResultSet rs = ps1.executeQuery(); if (!rs.next() || rs.getInt("stock") < quantity) { conn.rollback(); return false; // 库存不足 } double price = rs.getDouble("price"); // 2. 生成订单号:时间戳 + 用户ID,简单但够用 String orderNo = System.currentTimeMillis() + "_" + userId; // 3. 插入订单主表 PreparedStatement ps2 = conn.prepareStatement( "INSERT INTO t_order(user_id,order_no,total,create_time,status) VALUES(?,?,?,?,?)"); ps2.setInt(1, userId); ps2.setString(2, orderNo); ps2.setDouble(3, price * quantity); ps2.setTimestamp(4, new java.sql.Timestamp(System.currentTimeMillis())); ps2.setString(5, "待付款"); ps2.executeUpdate(); // 4. 扣库存 PreparedStatement ps3 = conn.prepareStatement( "UPDATE t_phone SET stock = stock - ? WHERE id=?"); ps3.setInt(1, quantity); ps3.setInt(2, phoneId); ps3.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn != null) conn.rollback(); e.printStackTrace(); return false; } finally { if (conn != null) conn.close(); } }逻辑说明:setAutoCommit(false)之后所有操作在同一个事务里,commit()才真正写入,异常时rollback()撤销。参数说明:订单号用System.currentTimeMillis()加用户 ID,保证同一毫秒内不同用户不冲突,同一用户同一毫秒重复下单的概率极低,毕设够用。注意 Access 的AUTOINCREMENT主键在回滚后不会回退,也就是说你插入了订单又回滚,下次插入的 ID 会跳号,这不是 bug,是 Access 的特性,报告里可以提一句。
3.3 用 PreparedStatement 挡住 SQL 注入
热搜里「access 注入」这个词不是空穴来风,Access 同样怕 SQL 注入。如果你用字符串拼接写查询,比如"SELECT * FROM t_user WHERE username='" + name + "'",别人输入' OR '1'='1就能绕过登录。解决办法只有一个:全部用PreparedStatement。
// 登录校验:正确写法 public User login(String username, String password) throws SQLException { String sql = "SELECT * FROM t_user WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRole(rs.getString("role")); return u; } } } return null; }逻辑说明:?是占位符,setString会把用户输入当作纯数据,即使里面含单引号也不会被解析成 SQL 语法。参数说明:try-with-resources语法自动关闭Connection、PreparedStatement、ResultSet,比手动finally里 close 更安全,避免连接泄漏导致 Access 文件被锁死。这一点在 Access 上尤其重要,因为 Access 是文件锁,连接没关干净,下次打开就报「文件已被占用」。
4. 避坑与排查:Access+JSP 最容易翻车的五个地方
4.1 报错「文件已被占用」或「无法锁定文件」
现象:程序运行一段时间后,所有数据库操作都抛异常,提示文件被占用或锁定。原因:Access 是文件级锁,只要有没关闭的Connection或ResultSet,文件就处于锁定状态,累积多了直接锁死。解决:所有数据库操作必须用try-with-resources或finally里显式close(),并且DBUtil里不要用静态单例连接长期持有,每次操作开新连接、用完即关。另外开发时别同时用 Access 软件打开着这个.accdb文件,那也会锁。
4.2 Tomcat 10+ 下 JSTL 标签不生效
现象:页面把<c:forEach>原样输出,或者报The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved。原因:Tomcat 10 开始,Servlet API 的包名从javax.*迁移到jakarta.*,老的 JSTL 1.2 不兼容。解决:换用 Jakarta 版的 JSTL 实现,taglib的 uri 改为jakarta.tags.core,同时确认WEB-INF/lib下放的是新包。如果你不想折腾,直接用 Tomcat 9 最省事,毕设环境没必要追新。
4.3 中文乱码:从表单到数据库全链路
现象:商品名、用户名存进去变成问号或乱码。原因:JSP 页面编码、请求编码、数据库编码三处不一致。解决:JSP 顶部<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 里request.setCharacterEncoding("UTF-8")必须在读取任何参数之前调用;Access 本身对 Unicode 支持没问题,但 UCanAccess 连接串可以加;charSet=UTF-8保险。GET 请求的中文还要在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。
4.4 价格字段用 TEXT 导致排序错乱
现象:商品按价格排序,99排在100后面,1000排在200前面。原因:建表时price用了TEXT类型,Access 按字符串逐位比较。解决:把price改成CURRENCY或DOUBLE类型。已经建好表的,用ALTER TABLE t_phone ALTER COLUMN price CURRENCY修改,但注意改类型可能丢数据,先备份。这个坑我在两个项目里都见过,属于典型的「建表一时爽,排序火葬场」。
4.5 图片上传后路径对但显示 404
现象:上传成功,数据库里img_url也有值,但页面<img src="${p.img_url}">显示不出来。原因:图片存到了项目源码目录,Tomcat 部署的是编译后的webapps目录,两者不是同一个地方;或者路径用了绝对磁盘路径,浏览器无法访问。解决:图片统一存到webapps/你的项目/uploads/下,数据库里存相对路径如uploads/1.jpg,页面引用时确保上下文路径正确。更稳妥的做法是用<%=request.getContextPath()%>/uploads/1.jpg拼出完整路径。
5. 让系统更耐看:分页查询与答辩演示的加分技巧
5.1 手写一个 Access 兼容的分页查询
商品多了以后列表页会很长,分页是必备功能。MySQL 用LIMIT offset, size,但 Access 不支持LIMIT,得用TOP配合子查询,或者干脆在 Java 层用List.subList做内存分页。毕设数据量小,我推荐内存分页,代码简单不易错。
// 内存分页:先查全部,再截取当前页 public List<Phone> findByPage(int pageNo, int pageSize) throws SQLException { List<Phone> all = findAll(); // 复用之前的查询 int from = (pageNo - 1) * pageSize; if (from >= all.size()) return new ArrayList<>(); int to = Math.min(from + pageSize, all.size()); return all.subList(from, to); // 注意 subList 返回的是视图,别直接改 }逻辑说明:from是当前页起始索引,to是结束索引,Math.min防止越界。参数说明:pageNo从 1 开始,pageSize一般设 8 或 10,正好铺满一屏。注意subList返回的是原列表的视图,如果你后续要序列化或修改,最好new ArrayList<>(all.subList(from, to))包一层。如果非要用 SQL 分页,Access 的写法是SELECT TOP pageSize * FROM t_phone WHERE id NOT IN (SELECT TOP offset id FROM t_phone ORDER BY id) ORDER BY id,又长又容易写错,不推荐。
5.2 答辩演示前必须做的三项检查
第一,把数据库文件路径改成相对项目部署目录的写法,或者干脆在DBUtil里用getServletContext().getRealPath()动态取,避免换台电脑就找不到文件。第二,准备一份初始化数据脚本,包含至少 10 条手机商品和 2 个测试账号(一个 admin 一个普通用户),演示时直接导入,不要现场一条条加。第三,把 Tomcat 的日志级别调到INFO以上,关掉DEBUG输出,否则控制台刷屏,老师看着乱。
5.3 我踩过的最大一个坑
最后说个我自己的教训。第一次做这套系统时,我把 Access 文件放在项目源码的src目录下,用相对路径./src/data.accdb连接。本地 Eclipse 里跑得好好的,一部署到 Tomcat 就报找不到文件。查了半天才明白,Tomcat 启动时的工作目录是bin目录,不是项目根目录,相对路径全错位。后来我养成习惯:数据库文件路径要么写死绝对路径(演示机固定),要么用ServletContext.getRealPath("/WEB-INF/data/phone_sale.accdb")动态获取,把文件放在WEB-INF下还能防止被浏览器直接下载。这个习惯帮我省了后面无数次折腾。希望帮到你。
本文还有配套的精品资源,点击获取