简介:一份基于JSP+Access的手机销售系统毕业设计完整资料包,面向计算机相关专业学生,解决毕业设计或课程设计从选题、需求分析、系统设计、论文撰写到答辩材料准备全流程缺资料、不完整的问题。资源包约3.03MB,包含论文、系统源码、开题报告、封面扉页、需求分析、任务书等材料,构成一套完整可参考的B2C网上购物系统项目。系统围绕商品目录、用户注册、网上订货、购物车、订单管理、库存查询与后台数据库管理展开,核心功能包括24小时在线订购服务、商品信息维护与管理、科学灵活的商品分类存储、强大快捷的查询、订单号模块,并在订单流程中融入客户商业逻辑规则,针对不同客户属性提供不同付款方式。描述中还给出B2C、网上购物系统、JSP、数据库等关键词,可直接作为功能设计、论文框架与代码编写的参考。目前已有63人浏览学习,适合需要快速梳理JSP+Access项目结构、获取完整毕业设计模板与答辩文档的学生使用。
1. 从打包结构看 JSP+Access 手机销售系统的真实交付边界
一个毕业设计项目的打包标题里同时含有“论文、系统、开题、封面、扉页、需求分析、任务书”,这本身就说明 JSP+Access 手机销售系统不是只写代码的工程,而是一套需要你从头能讲、能改、能演示的完整交付物。JSP 负责把手机列表、购物车、订单渲染成 HTML,Access 以单文件数据库的形式保存商品与订单数据,Servlet 和 JavaBean 夹在中间处理请求与业务规则。拿到这类项目后,常见做法是先跑通 Tomcat 下的登录页面,再对照需求分析文档反推数据库表和页面跳转,最后把论文里的 ER 图、测试用例与代码逐一对应。整套技术栈的优势是部署轻、入门快,但驱动位数、文件独占锁、SQL 方言差异这些问题,往往要到真正演示时才暴露出来。
2. 用 UCanAccess 打通 JSP 到 Access 的连接配置与最小运行实例
2.1 老教材里的 JDBC-ODBC 桥为什么在 64 位环境下频繁翻车
早期电子书里的 access 数据库教程,驱动加载代码几乎都是Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"),再拼一个jdbc:odbc:PhoneSales。这条路在 JDK 8 正式移除 JDBC-ODBC Bridge 之后已经走不通,在 JDK 8+ 环境运行第一行就会抛ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver。如果强行退回 JDK 7 并在 64 位 Windows 上配 ODBC 数据源,Tomcat 进程大概率会直接崩溃退出,事件查看器里能看到0xc0000005对应的 access violation,也就是热词里process exited with code 3221225477这个数字——它正好是 0xC0000005 换算成十进制的值。这类崩溃的根因是 ODBC 桥调用本机 odbc32.dll 时发生非法内存访问,跟业务代码没有关系,也不是重启就能好的。
网上不少 access 数据库教程还停留在 JDBC-ODBC 桥时代,光下载一个 access 数据库64位系统驱动程序也救不回来,因为 JDK 8 已经把桥从标准库中整段移除,驱动位数和 ODBC 数据源位数不一致时,连 32 位数据源列表都看不到。所以现在我接到 .accdb/.mdb 文件,统一用 UCanAccess。它基于 Jackcess 用纯 Java 读写 Access,不依赖 ODBC 数据源,JDK 8 到 17 都能用。如果确实只能在 JDK 5/6/7 上跑,才考虑 32 位 JDK 配 32 位 ODBC 的老方案,而且系统 ODBC 管理工具也要切成 32 位。ODBC 数据源会读注册表,换一台机器就要重新配置;UCanAccess 只把数据库文件当作普通文件读取,Java 进程能读到文件就能连,环境迁移省事得多。
2.2 UCanAccess 驱动包、连接串与参数说明
如果是 Maven 工程,依赖写法如下:
<!-- ucanaccess 5.x 缺省打包了 jackcess/hsqldb 等匹配依赖 --> <dependency> <groupId>net.sf.ucanaccess</groupId> <artifactId>ucanaccess</artifactId> <version>5.0.1</version> </dependency>非 Maven 部署时,把主 Jar 连同 commons-lang3、commons-logging、jackcess-core、hsqldb 一并复制到WEB-INF/lib。UCanAccess 的 5.x 版已经内置这些依赖的匹配版本,不需要再从别处找兼容包。连接串按下面这种方式写:
String url = "jdbc:ucanaccess:///D:/PhoneSales/data/phones.accdb;memory=false;singleConnection=true";参数有三个关键点:memory=false是默认值,SQL 直接作用到文件,适合演示场景;memory=true会把表加载进内存,写操作后必须显式压缩,中途断电容易丢数据,答辩现场不适合。singleConnection=true让同一个 JVM 的连接复用同一个底层文件句柄,能减少 Access 文件被多连接打开时的锁冲突。还可以加shrink=true在连接释放后压缩数据库,但如果此时 Access 桌面版打开了同一个文件,压缩会抛异常,所以演示前记得关掉 Office。
2.3 最小可复现的 JSP 查询页与数据库文件放置路径
<%@ page import="java.sql.*" %> <%@ page contentType="text/html;charset=UTF-8" %> <% // 数据库放 WEB-INF 下,既能定位又不会被浏览器直接下载 String url = "jdbc:ucanaccess://" + application.getRealPath("/WEB-INF/phones.accdb") + ";singleConnection=true"; String sql = "SELECT brand, model, price FROM phone WHERE stock > 0"; try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { out.println("<p>" + rs.getString("brand") + " " + rs.getString("model") + " ¥" + rs.getBigDecimal("price") + "</p>"); } } catch (SQLException e) { out.println("<p>连接失败:" + e.getMessage() + "</p>"); } %>代码里application.getRealPath("/WEB-INF/phones.accdb")取到的是部署后的绝对路径,把数据库放进 WEB-INF 后,浏览器无法通过 URL 直接下载 .accdb 文件。Class.forName被省略是因为驱动 Jar 的META-INF/services会被DriverManager自动发现,写不写都行。getBigDecimal读取货币字段,避免用 getFloat 出现金额浮点误差。如果你查过jsp编译class文件保存在哪里,Tomcat 会在work/Catalina/localhost/工程名/org/apache/jsp目录下生成 JSP 对应的 Java 与 class 文件,页面报 500 时去这里看生成源码,能定位到具体是哪一行 Scriptlet 出错。
常见报错按下面这张表定位最快:
| 现场报错 | 根因 | 处理办法 |
|---|---|---|
ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver | JDK 8+ 已移除旧桥接驱动 | 换 UCanAccess,驱动类改为自动发现 |
process exited with code 3221225477 / 0xc0000005 | 64 位环境 ODBC 桥内存访问违规 | 删除 ODBC 数据源,全部改用 UCanAccess |
The process cannot access the file because... | Access 文件被 Office 或另一进程独占 | 关闭 Office 打开的文件,taskkill /F /IM javaw.exe清僵尸进程 |
提示:UCanAccess 支持readOnly=true参数缓解文件占用问题,但演示前最稳妥的仍然是只开一个 Tomcat 实例,并确保没有在桌面版 Access 里打开同一个数据库文件。
3. 手机销售系统的表结构、登录与购物车实现
3.1 用 5 张表撑起一个可答辩的手机销售系统
Access 里的表结构,我一般这样建:
| 表名 | 关键字段 | Access 类型 | 说明 |
|---|---|---|---|
| admin | admin_id, username, pwd | 自动编号, 短文本, 短文本 | 后台管理员 |
| phone | phone_id, brand, model, price, stock | 自动编号, 短文本, 短文本, 货币, 长整型 | 手机主表 |
| customer | customer_id, username, pwd, phone_no, address | 自动编号, 短文本, 短文本, 短文本, 长文本 | 注册客户 |
| orders | order_id, order_no, customer_id, total, create_time | 自动编号, 短文本, 长整型, 货币, 日期/时间 | 订单主表 |
| order_item | item_id, order_id, phone_id, qty, price | 自动编号, 长整型, 长整型, 整型, 货币 | 订单明细 |
字段命名有两个要避开的地方:order、user、name在 Access 里都有保留字风险,表名用orders,客户名字段用username或real_name。金额字段用货币,因为双精度保存价格会出现精度抖动,排序和汇总时尤其明显。自增字段用 Access 的自动编号,注意它不能像 MySQL 那样指定起始值,插入主记录后要用 JDBC 的getGeneratedKeys()取回订单号。
3.2 登录接口与个人信息展示页的 Session 校验
登录接口用 PreparedStatement 处理:
String sql = "SELECT admin_id, username FROM admin WHERE username=? AND pwd=?"; try (Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, request.getParameter("username")); ps.setString(2, request.getParameter("pwd")); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { request.getSession().setAttribute("loginAdmin", rs.getString("username")); response.sendRedirect("admin_phone_list.jsp"); } else { response.sendRedirect("login.jsp?error=1"); } } }参数占位符?配合setString,保证用户输入的引号、分号只作为字符串内容解析,不会拼接进 SQL 语义里,这是预防 SQL 注入的最小做法。如果写成字符串拼接,数据里出现单引号就会让整条语句失控,安全扫描一抓一个准。
对应到用户个人中心这类 jsp 个人信息展示页面,顶部统一做 Session 校验:
<% String loginUser = (String) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("login.jsp?timeout=1"); return; // 阻止后续页面继续输出 } %>return语句保证未登录时直接跳转、后面的页面代码不再执行。更规范的做法是抽一个 Filter 统一拦截/user/*和/admin/*路径,但课程设计里先在需要保护的页面各写一遍,再抽取成公共 JSP 片段也不丢分。
3.3 手机列表分页与按品牌筛选的 SQL 写法
UCanAccess 内置 HSQLDB 风格的 SQL parser,支持LIMIT/OFFSET,分页比老式 Access 的TOP + NOT IN简洁:
SELECT phone_id, brand, model, price, stock FROM phone ORDER BY phone_id LIMIT 5 OFFSET 0分页必须有稳定的ORDER BY,否则结果可能重复或跳行。第二页时把偏移改成 5,第三页改成 10。Java 侧计算:
int pageNo = 1; int pageSize = 5; try { pageNo = Integer.parseInt(request.getParameter("page")); } catch (NumberFormatException e) { pageNo = 1; } if (pageNo < 1) pageNo = 1; int offset = (pageNo - 1) * pageSize;再加品牌筛选时,SQL 用WHERE 1=1动态拼接条件:
String sql = "SELECT * FROM phone WHERE 1=1"; if (brand != null && !brand.isEmpty()) sql += " AND brand=?"; if (keyword != null && !keyword.isEmpty()) sql += " AND model LIKE ?"; // 通配符 % 放在参数值里,而不是拼进 SQL 文本 PreparedStatement ps = conn.prepareStatement(sql); int idx = 1; if (brand != null && !brand.isEmpty()) ps.setString(idx++, brand); if (keyword != null && !keyword.isEmpty()) ps.setString(idx++, "%" + keyword + "%");把%放进参数值而不是拼进 SQL,是为了让?占位符仍然覆盖整条动态片段,LIKE 的转义字符也按参数处理,避免输入%或_时改变查询语义。
3.4 从购物车到订单:事务边界和库存扣减
购物车用 Session 里的 Map 保存:
HttpSession session = request.getSession(); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<>(); } cart.merge(Integer.parseInt(request.getParameter("phoneId")), 1, Integer::sum); session.setAttribute("cart", cart);merge的三个参数分别是键、增量、合并函数,Integer::sum表示同一部手机第二次加入时在原有数量上加 1。用LinkedHashMap保持加购顺序,结算页展示时不会乱跳。订单生成放在一个事务里:
conn.setAutoCommit(false); try { PreparedStatement psOrder = conn.prepareStatement( "INSERT INTO orders (order_no, customer_id, total, create_time) VALUES (?,?,?,?)", Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, "NO" + System.currentTimeMillis()); psOrder.setInt(2, customerId); psOrder.setBigDecimal(3, total); psOrder.setTimestamp(4, new Timestamp(System.currentTimeMillis())); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); keys.next(); int orderId = keys.getInt(1); // 逐条插入 order_item,并 UPDATE phone SET stock = stock - ? WHERE phone_id = ? AND stock >= ? conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; }Statement.RETURN_GENERATED_KEYS与getGeneratedKeys()是 JDBC 标准方式,在 Access/UCanAccess 下同样适用。库存更新 SQL 多写一个AND stock >= ?,执行后若updateCount == 0,说明库存不足,抛出异常让整个事务回滚。这个场景最核心的是原子性:订单主表、明细表、库存表要么一起成功,要么一起失败,绝不允许只扣了库存却生成不了订单。
4. 论文、开题、需求分析与 JSP 系统代码的对应关系
4.1 需求分析文档里的功能边界怎么划分
需求分析里最容易出问题的是把所有角色揉成一个。正确的划分是先给三类角色:游客浏览手机列表、按品牌和关键词搜索、查看详情、注册;客户在游客基础上增加登录、购物车、提交订单、查看自己的订单记录;管理员维护手机信息、处理订单状态、查看客户列表。然后用一张表把它们钉死,后续论文的模块设计、数据库设计、页面清单都基于这张表展开:
| 需求分析中的功能点 | 对应 JSP 页面 | 涉及数据表 / 存储 |
|---|---|---|
| 游客浏览手机列表 | index.jsp, phone_list.jsp | phone |
| 按品牌、价格筛选 | phone_list.jsp?brand=Huawei | phone |
| 用户注册与登录 | register.jsp, login.jsp | customer |
| 个人信息展示 | center.jsp | customer, orders |
| 购物车管理 | cart.jsp | HTTP Session |
| 生成订单 | order_confirm.jsp | orders, order_item |
| 后台手机管理 | admin_phone_list.jsp, admin_phone_edit.jsp | phone, admin |
| 订单状态更新 | admin_order_list.jsp | orders |
购物车不落表,所以用 HTTP Session 标识存储位置。把这句话写进需求分析,能解释清楚为什么重启 Tomcat 后购物车会清空,让老师挑不出边界矛盾。
4.2 论文中的 ER 图、用例图、数据字典如何从现有表跑出来
数据库已经建好在 Access 里,先打开 Access 的关系视图,把 5 张表拉进去,在 phone、customer、orders 之间建立关系,Access 自己就能生成接近 ER 图的关系图,导出后用 draw.io 补上基数标记。用例图三个角色加八种动作,动作必须和 4.1 的功能点一一对应。
数据字典的常见问题是字段类型写错。Access 的货币在论文里应描述为DECIMAL(10,2),自动编号是INT 自增主键,日期/时间对应DATETIME/TIMESTAMP。order_no、create_time这类字段要标默认值,order_no默认值可以是"NO" & Now(),create_time默认值是Now()。对应的 Access DDL 是:
-- Access 的保留字避开 order,因此表名用 phone CREATE TABLE phone ( phone_id AUTOINCREMENT PRIMARY KEY, brand TEXT(50), model TEXT(100), price CURRENCY, stock INTEGER );AUTOINCREMENT对应 MySQL 的AUTO_INCREMENT,CURRENCY对应DECIMAL(10,2),UCanAccess 下这条语句可以直接执行,也是数据字典最可靠的来源。
4.3 任务书与开题报告的技术路线描述框架
任务书里需要明确技术选型:B/S 架构,JSP+Servlet+JavaBean 分层,数据库选 Access,驱动选 UCanAccess,运行环境是 JDK 8 / Tomcat 8.5。开题报告把每一层写具体:
表现层由 JSP 页面构成,负责渲染与简单表单校验;控制层由 Servlet 接收请求并调用业务类;业务层用 JavaBean 封装登录、购物车、订单生成、库存扣减方法;数据层用 DAO 封装 JDBC 操作,通过 UCanAccess 驱动连接 phones.accdb。
进度安排不要写“开发调试”这种模糊词,要用表格写清第几周完成哪个交付物:第 1 周需求分析、第 2 周数据库与页面原型、第 3-5 周编码、第 6-7 周测试与文档、第 8 周答辩演练。评阅老师拿到任务书先看进度表,再看技术路线,这两块写不满会被直接要求改。
4.4 论文测试章节与演示脚本的绑定
系统测试章节最稳的写法是先列功能测试用例,再给回归测试表格。功能测试表至少包含用例编号、模块、前置条件、操作步骤、预期结果、实际结果。举两个例子:
| 用例编号 | 模块 | 前置条件 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC-SALE-01 | 订单 | 手机A库存10,已登录客户 | 下单3台并提交 | 订单总金额正确,库存变为7 |
| TC-RETURN-01 | 订单 | 管理员登录后台 | 将订单状态改为已发货 | 订单列表状态更新 |
演示脚本固定为 15 分钟链路:游客访问首页→搜索“华为”→注册并登录→加入购物车→结算生成订单→管理员登录→修改手机价格→在订单列表标为已发货。每演示完一步打勾,被打断也能快速找回断点。脚本里要准备两类恢复手段:一是预置一个库存为 100 的测试账号,避免演示时库存耗尽;二是准备一段重置订单状态的 SQL,出问题时 30 秒内恢复现场。
5. 手机销售系统答辩前十分钟验证清单与 5 个高频报错处置
5.1 演示前验证顺序
答辩前按下面这张表过一遍,能覆盖 90% 的现场问题:
| 检查项 | 操作或命令 | 通过标准 |
|---|---|---|
| JDK 版本 | java -version | 与 Servlet 编译版本匹配 |
| 端口占用 | netstat -ano | findstr :8080 | Tomcat 进程占用,无残留 javaw |
| 驱动存在 | 看WEB-INF/lib下 ucanaccess*.jar | 文件存在且非 0 字节 |
| 数据库解锁 | 项目目录无.laccdb锁文件 | 文件可复制、可改名 |
| 核心链路 | 登录→加购→结算→后台改价 | 每一步跳转符合预期 |
特别提醒:Maven 打包前先执行 package,再检查target/xxx/WEB-INF/lib里有没有 UCanAccess,不然换机器部署时第一分钟就开始报驱动找不到。
5.2 现场不能慌的 5 个报错与处置
第一个是ClassNotFoundException: net.ucanaccess.jdbc.UcanaccessDriver,驱动 Jar 没进 WEB-INF/lib,把 Jar 复制进去后重启 Tomcat。第二个是process exited with code 3221225477 / 0xc0000005这类 JVM 崩溃,多出现在旧代码残留 ODBC 配置,检查代码里没有jdbc:odbc:连接串,并保证 Tomcat 与 JDK 都是 64 位。第三个是error 1045 access denied for user 'root'@'localhost',通常是搜索答案时误把 MySQL 连接串里的账号密码填进 Access 工程,处理办法是把 UCanAccess 的 URL 换成无账号密码的文件路径。第四个是 JSP 页面改动不生效,因为 JSP 编译后的 class 缓存在 Tomcat work 目录,删除work/Catalina/localhost/工程名后重启即可。第五个是库存出现负数,说明下单时先查库存再更新,缺少UPDATE ... WHERE stock >= ?这个并发保护条件,也就是 3.4 节事务代码解决的问题。
最后补一个偏交互的加分技巧:在手机编辑页加上beforeunload未保存提示,也就是很多人搜索的“屏蔽jsp离开页面提示”场景。JSP 本身不参与浏览器弹窗逻辑,只负责渲染下面这段脚本:
let dirty = false; document.querySelectorAll('input, select').forEach(el => { el.addEventListener('change', () => { dirty = true; }); }); window.addEventListener('beforeunload', e => { if (dirty) { e.preventDefault(); e.returnValue = ''; // 触发浏览器的离开确认框 } });dirty标志在表单发生编辑后置为 true,beforeunload事件返回空字符串就会弹出离开确认框。演示“修改价格后直接点导航离开”时页面会拦下来,这个细节能让评委认为你考虑了数据丢失风险。把这段脚本抽成 common_guard.jsp 公共片段,需要保护的编辑页 include 进去就能复用。
本文还有配套的精品资源,点击获取