简介:这是一套基于JSP与Access数据库的手机销售系统完整设计与实现资源,主要面向Java初学者以及需要完成课程设计或毕业设计的计算机专业学生。系统覆盖需求分析、系统设计、编码实现到测试验收的完整流程,其中JSP负责处理用户请求、生成动态页面,Access用于存储手机库存、销售记录和客户信息,并支持数据的增删改查与简单报表生成。需求说明书明确了手机型号展示、库存管理、订单处理、用户注册登录等核心功能,数据库设计说明书展示了实体关系与表结构关联,测试计划则列出了系统上线前的验证方案。压缩包大小约2.26MB,内部整合了源代码、项目报告、需求说明书、详细设计说明书、数据库设计说明书、开题报告和测试计划等,各部分按项目阶段组织,便于按图索骥。目前已有76人学习下载,适合希望通过完整案例掌握JSP开发Web应用的流程、数据库设计方法以及规范项目文档编写的学习者。
1. 一个看似过时的 JSP + Access 项目,为什么还是 Java 入门的标本
JSP + Access,一个在今天招聘市场几乎消失的技术组合,却是理解 Java Web 最便宜的入场券。这套手机销售系统没有 Spring、没有 MyBatis、没有 Redis,但注册登录、手机型号展示、库存管理、订单处理全都有,而且配套齐全——开题报告、需求说明书、详细设计说明书、数据库设计说明书、测试计划、项目报告一份不少。对想搞懂 Java Web 请求链路、写毕业设计、或者接手老系统做二次开发的人,它比那些框架 demo 更值得下载。
别被 Access 数据库劝退。正因为它是文件型数据库,你不需要装 MySQL、不需要配服务,文件拷到哪儿都能跑;也正因为 JSP 直接拼 HTML,你能一眼看清一次请求从浏览器到数据库再回来的全过程。这篇就把这份资源拆开,讲清楚它是什么、怎么跑、核心代码长什么样、坑在哪、以及怎么改。
2. 技术选型与整体架构:JSP、Access 和六份文档分别怎么读
拿到压缩包先别急着跑。先把里面的东西分两类:代码归代码,文档归文档。这套项目最值钱的地方不在于 JSP 页面写了多少行,而在于“需求 → 设计 → 编码 → 测试”这条完整链路被六份文档固定下来了,这在今天的速成项目里反而是稀缺品。
2.1 JSP 与 Access 的分工:没有框架的年代,一次请求怎么走完
JSP 的经典形态是 HTML 里嵌 Java 代码。浏览器发请求到 Tomcat,Tomcat 把 JSP 编译成 Servlet,执行 Java 逻辑、通过 JDBC 操作 Access 数据库,再把结果拼成 HTML 返回。Access 在这里就是一个文件型关系数据库,负责存用户、手机、订单三类数据。
这个组合放到今天看,缺了分层、缺了 ORM、缺了连接池,但也正因为缺,请求参数从哪来、数据库连接怎么开、结果集怎么遍历、连接要不要手动关——这些在 Spring Boot 里被框架掩盖的细节全部暴露出来了。对想做毕业设计、或者面试前想补底层的人来说,这种“什么都自己写”的项目反而是最好的复习材料。
2.2 拿到资源先看什么:目录结构和部署步骤
压缩包里通常是这样的结构:
phone_sale/ ├─ src/ # Java 源码(工具类、Servlet) ├─ WebRoot/ │ ├─ login.jsp # 登录页 │ ├─ register.jsp # 注册页 │ ├─ index.jsp # 首页/手机列表 │ ├─ order.jsp # 下单页 │ ├─ WEB-INF/ │ │ ├─ web.xml # Web 部署描述文件 │ │ └─ lib/ # 第三方 jar 放这里 │ └─ database/ │ └─ phone.mdb # Access 数据库文件 └─ 文档/ ├─ 开题报告.pdf ├─ 需求说明书.pdf ├─ 详细设计说明书.pdf ├─ 数据库设计说明书.pdf ├─ 测试计划.pdf └─ 项目报告.pdf先把 JSP 文件扫一遍,再看数据库设计说明书,顺序别反。JSP 页面上的表单向哪个页面提交、session 里存了什么 key,看代码最快;而 t_user、t_phone、t_order 三张表的字段类型和关联关系,看数据库设计说明书最准。这两者对应上了,项目就等于看懂了一半。
部署步骤我一般这样走:第一步,用 Eclipse 或 MyEclipse 建立 Dynamic Web Project,把 WebRoot 内容拷进去;第二步,把 Access 驱动 jar 放进 WEB-INF/lib,如果用 UCanAccess,需要 ucanaccess 主包加 commons-lang、commons-logging、hsqldb 几个依赖;第三步,确认 phone.mdb 的实际路径并修改连接代码里的 URL;第四步,部署到 Tomcat 启动,浏览器访问http://localhost:8080/项目名/login.jsp。每一步失败的原因和解法,第 4 章专门讲。
2.3 文档清单:哪份解决什么问题
六份文档对应项目生命周期的六个节点,用法不一样:
| 文档 | 内容 | 什么时候用 |
|---|---|---|
| 开题报告 | 项目背景、技术路线、预期成果 | 开题答辩、立项前 |
| 需求说明书 | 功能需求:注册登录、手机展示、库存、订单 | 动手写代码前核对功能 |
| 数据库设计说明书 | 表结构、字段定义、实体关系 | 建库前、排查数据问题时 |
| 详细设计说明书 | 模块设计、界面设计、编码指导 | 编码时对照着写 |
| 测试计划 | 测试目标、方法、策略、验收标准 | 上线前、答辩演示前 |
| 项目报告 | 总体介绍、设计思路、实现与效果评估 | 写结题材料时 |
如果你的目的是复现和二次开发,需求说明书 + 数据库设计说明书 + 源代码三件套就够了;如果是为了课程答辩,其余三份正好补齐过程文档。同类资源里,文档能配到这么齐的其实不多。
2.4 数据库设计:用户、手机、订单三张实体表的关系
Access 数据库里一般就三张核心表。用户表存账号密码和联系方式,手机表存品牌、型号、价格、库存,订单表通过外键关联用户和手机,并记录购买数量和下单时间。实体关系是用户 1 对多订单,订单多对 1 手机——一个用户可以下多单,一个手机可以出现在多张订单里。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| t_user | id, username, password, phone, address | username 唯一,密码是明文 |
| t_phone | id, brand, model, price, stock | stock 不能为负 |
| t_order | id, user_id, phone_id, count, total_price, order_time | user_id 关联 t_user.id,phone_id 关联 t_phone.id |
Access 里主键可以直接在表设计视图里指定,外键约束比 MySQL 弱不少,所以订单里的 user_id、phone_id 一致性主要靠代码保证。下单时读 t_phone 的 price 计算 total_price,而不是让用户传价格——这一点在需求说明书里写得很清楚,老代码里通常也是这么实现的。
3. 核心模块拆解:从注册登录到下单扣库存,一条请求的完整链路
挑三个最有代表性的模块拆开看:注册登录、手机列表、下单扣库存。这三段看完,基本就等于读完了整套源码的主干。每个模块我都先贴代码,再讲逻辑和参数怎么改。
3.1 登录模块:request 取参 → 查库 → session 记账
登录页表单提交到 login.jsp 自身,处理逻辑写在 JSP 的 scriptlet 里,这是老项目最常见的写法:
<% String username = request.getParameter("username"); String password = request.getParameter("password"); if (username != null && password != null) { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection conn = DriverManager.getConnection("jdbc:odbc:phone", "", ""); PreparedStatement ps = conn.prepareStatement( "SELECT * FROM t_user WHERE username=? AND password=?"); ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery(); if (rs.next()) { session.setAttribute("loginUser", username); response.sendRedirect("index.jsp"); } else { out.println("<script>alert('用户名或密码错误');history.back();</script>"); } rs.close(); ps.close(); conn.close(); } %>逻辑说明:先request.getParameter拿表单提交的用户名和密码,用 PreparedStatement 预编译查询,避免字符串拼接 SQL。查到了就把用户名放进 session,然后重定向到首页;查不到就弹窗提示。最后关掉 rs、ps、conn 三个资源——这一步在 Access 项目里是保命操作,漏关连接会把 mdb 文件锁住,第二次请求直接报“文件正在使用”。
参数说明:这里用的是老式 JDBC-ODBC 桥连接,jdbc:odbc:phone里的phone是控制面板里 ODBC 数据源管理器中配置的数据源名,跟代码无关。改数据库文件路径时,改的是 ODBC 配置而不是代码里的 URL。如果你在 64 位系统上跑,这行驱动加载会直接报 ClassNotFoundException,解决方式在第 4 章,常见做法是换成 UCanAccess 驱动。
注册模块逻辑类似,只是把查询换成插入:
<% String username = request.getParameter("username"); String password = request.getParameter("password"); Connection conn = DriverManager.getConnection("jdbc:odbc:phone", "", ""); PreparedStatement ps = conn.prepareStatement( "INSERT INTO t_user(username, password) VALUES(?, ?)"); ps.setString(1, username); ps.setString(2, password); int result = ps.executeUpdate(); if (result > 0) { out.println("<script>alert('注册成功');location.href='login.jsp';</script>"); } else { out.println("<script>alert('注册失败');history.back();</script>"); } ps.close(); conn.close(); %>逻辑说明:executeUpdate返回的是影响行数,返回 1 说明插入成功,页面跳回登录页。这段代码的问题在于没先查用户名是否已存在,如果 t_user 表的 username 字段没设唯一索引,重复注册会插出多条脏数据,二开时这是第一个要补的校验。
3.2 手机列表与库存查询:结果集遍历拼表格
首页的手机列表是 JSP 里最典型的 ResultSet 循环:
<% Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection conn = DriverManager.getConnection("jdbc:odbc:phone", "", ""); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM t_phone"); while (rs.next()) { %> <tr> <td><%=rs.getString("brand")%></td> <td><%=rs.getString("model")%></td> <td><%=rs.getDouble("price")%> 元</td> <td><%=rs.getInt("stock")%> 台</td> <td><% if (rs.getInt("stock") > 0) { %>有货<% } else { %>无货<% } %></td> </tr> <% } rs.close(); stmt.close(); conn.close(); %>逻辑说明:while (rs.next())每循环一次输出一行表格,这是 JSP 没有 JSTL 标签库时的标准遍历方式。库存用getInt("stock")取值,大于 0 显示有货。这段用 Statement 没问题,因为查询条件是写死的;但如果你要按品牌、价格区间搜索,必须改成 PreparedStatement,否则就是给下一个开发者留 SQL 注入漏洞。
参数说明:rs.getString("brand")里的brand是数据库列名,大小写不敏感但拼写必须一致,拼错了运行时报Column not found。要加条件查询,把 SQL 改成SELECT * FROM t_phone WHERE brand=?,再用ps.setString(1, brand)传参。这个改动同时解决注入问题和参数绑定问题,是二改里优先级最高的。
3.3 下单与库存扣减:两条 SQL 必须放进同一个事务
下单是整个项目里最值得抄的一段代码,因为它把事务和库存扣减的边界讲清楚了:
<% Connection conn = DriverManager.getConnection("jdbc:odbc:phone", "", ""); conn.setAutoCommit(false); try { PreparedStatement ps1 = conn.prepareStatement( "INSERT INTO t_order(user_id, phone_id, count, total_price, order_time) VALUES(?,?,?,?,?)"); ps1.setInt(1, userId); ps1.setInt(2, phoneId); ps1.setInt(3, count); ps1.setDouble(4, price * count); ps1.setTimestamp(5, new Timestamp(System.currentTimeMillis())); ps1.executeUpdate(); PreparedStatement ps2 = conn.prepareStatement( "UPDATE t_phone SET stock = stock - ? WHERE id=? AND stock >= ?"); ps2.setInt(1, count); ps2.setInt(2, phoneId); ps2.setInt(3, count); int update = ps2.executeUpdate(); if (update == 0) throw new RuntimeException("库存不足"); conn.commit(); } catch (Exception e) { conn.rollback(); out.println("下单失败:" + e.getMessage()); } finally { conn.setAutoCommit(true); conn.close(); } %>逻辑说明:setAutoCommit(false)关闭自动提交,两条 SQL 要么一起成功要么一起回滚。核心设计在第二条 UPDATE 的 WHERE 条件里带了stock >= ?,意思是“库存够才扣,不够就不扣”,如果影响行数为 0 说明库存不足,直接抛异常回滚——订单不会插入,库存不会被扣成负数。这比先查库存再下单的写法少一次数据库往返,也避免了并发情况下超卖。
参数说明:price * count是下单时就算好总价,不依赖数据库再查一遍;order_time用setTimestamp写入当前时间,不依赖数据库方言,以后换 MySQL 也不用改 SQL。userId 和 phoneId 在真实项目里应该从session.getAttribute("userId")取,而不是从页面参数拿,否则用户传一个别人的 id 就能替别人下单——这也是老项目常见的越权漏洞。
三块代码看完,你会发现这套项目没有 Servlet、没有过滤器、没有 MVC 分层,所有逻辑都堆在 JSP 页面里。能跑,但很脆。第 5 章的改造思路就是从这里切入的。
4. 避坑与排查:老项目跑不起来,十有八九栽在这几处
翻车集中区。这类项目在“跑起来”这一步劝退了很多人,问题往往不在代码,而在环境。我把最常见的四类问题按“现象 → 原因 → 解决”写出来,你照着排除基本能搞定。
4.1 64 位系统连不上 Access:JDBC-ODBC 桥失效
现象:启动 Tomcat 后访问登录页,点登录按钮报java.sql.SQLException: [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序;或者加载驱动时直接ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver。
原因:JDBC-ODBC 桥只存在于 32 位 JDK 和旧版本系统上,64 位 JDK 里已经移除了这个驱动类。而且 64 位系统下 ODBC 数据源管理器分 32 位和 64 位两套,你配置的数据源可能跟 JVM 位数对不上,配了也找不到。
解决:换 UCanAccess 驱动,这是跨平台且不依赖系统 ODBC 配置的做法。把连接代码改成:
Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); Connection conn = DriverManager.getConnection( "jdbc:ucanaccess://D:/phone.mdb", "", "");需要把 ucanaccess 主 jar 和 commons-lang、commons-logging、hsqldb 等依赖一起放进 WEB-INF/lib。第一次连接会比较慢,因为 UCanAccess 要把 mdb 文件映射成内存库,属正常现象,别以为是卡死了。
提示:如果项目里多处写了
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"),建议抽一个 DBUtil 工具类统一管理驱动加载和连接获取,避免每页改一遍。
4.2 中文乱码:页面编码和请求编码各说各话
现象:注册的用户名写进 Access 里变成问号,或者页面显示的一直是乱码。
原因:老 JSP 项目的 pageEncoding 常常写的是 GBK,或者干脆没写,而 Tomcat 默认请求编码是 UTF-8,POST 过来的参数被按 ISO-8859-1 解码,存进 Access 就花了。乱码的本质是“写入时的编码”和“读取时的编码”不一致。
解决:三步走。第一,所有 JSP 第一行统一写<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>;第二,在所有request.getParameter调用之前加request.setCharacterEncoding("UTF-8");第三,如果改了还乱,去 Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8"。已经写进 Access 的乱码数据救不回来,只能清掉重插。
4.3 Access 数据库文件被锁:连接没关干净
现象:项目第一次跑没问题,第二次操作数据库就报“文件正在使用”或“无法更新数据库,因为另一个用户已锁定”,数据库目录下多出.laccdb或.ldb锁文件。
原因:代码里 rs、stmt、conn 没关,或者页面抛异常时没有走 finally,连接一直占在进程里。Access 是文件型数据库,没有 MySQL 那种连接池回收机制,连接不释放就锁文件,而且只有重启 Tomcat 才能解锁。
解决:养成“开了三个对象,finally 里关三个”的习惯。最省事的做法是抽一个关闭工具方法,接收 Connection、Statement、ResultSet,统一判空关闭。改造时优先把这段补上,否则你做一次功能测试就要重启一次 Tomcat,非常痛苦。
4.4 Tomcat 起不来、首页白屏:端口和 welcome-file
现象:启动 Tomcat 报端口被占用;或者项目能打开但直接白屏,没有任何提示。
原因:8080 端口被别的进程占用是第一个常见原因。白屏则多半是 web.xml 里没配 welcome-file,或者配的首页路径不存在;也有一种情况是 index.jsp 里直接查数据库,连接失败时异常被吞掉,页面就空白了。
解决:端口占用改 server.xml 里的 Connector port,改成 8081 之类不冲突的端口。白屏先看 web.xml 的 welcome-file-list 配置:
<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>如果 welcome-file 指向 index.jsp,但 index.jsp 里没有登录判断,那任何人都能跳过登录直接访问首页——这是二开时优先级最高的安全问题,应该在 index.jsp 开头加一段 session 判断,没登录就response.sendRedirect("login.jsp")。
5. 二次开发改造:把 JSP 大杂烩改成能继续生长的工程
如果你不只是想跑通,而是想把它当毕业设计或课程项目交上去,按下面的顺序改,改动量小且收益高。
5.1 第一步:把 Java 逻辑从 JSP 里抽成 DAO
第 3 章那些 scriptlet 代码,第一件事就是挪出 JSP。常见做法是建dao包,放 DBUtil、UserDao、PhoneDao、OrderDao 四个类,JSP 里只保留显示逻辑和调用:
public class UserDao { public boolean checkLogin(String username, String password) { String sql = "SELECT * FROM t_user WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery(); return rs.next(); } catch (Exception e) { e.printStackTrace(); return false; } } }逻辑说明:try-with-resources帮你自动关连接,正好解决第 4 章 4.3 的连接泄漏问题。JSP 页面里只需要<% if (new UserDao().checkLogin(username, password)) { ... } %>,可读性和可维护性立刻上了一个台阶。这一步不换技术栈,风险最低,建议最先做。
5.2 第二步:数据库从 Access 换成 MySQL
如果你的机器上装了 MySQL,换库是让项目更像“正经工程”的关键一步。改动集中在三处:驱动类、连接 URL、SQL 方言。Access 和 MySQL 的对应关系:
| 项目 | Access 写法 | MySQL 写法 |
|---|---|---|
| 驱动类 | net.ucanaccess.jdbc.UcanaccessDriver | com.mysql.jdbc.Driver |
| 连接 URL | jdbc:ucanaccess://D:/phone.mdb | jdbc:mysql://localhost:3306/phone |
| 自增主键 | 表设计视图设自动编号 | INT AUTO_INCREMENT |
| 时间函数 | Now() | NOW() |
逻辑说明:MySQL 8 的驱动类要换成com.mysql.cj.jdbc.Driver,URL 里还要加useSSL=false&serverTimezone=Asia/Shanghai,这是 8.x 的强制要求。改完 SQL 后,把 mdb 里的数据用 Access 导出成 CSV,再导入 MySQL,数据迁移就完成了。如果不想装 MySQL,也可以用 H2 这种内嵌数据库,代码改动更小。
5.3 第三步:补分页和权限,拿需求说明书当验收清单
改到这里,你可以顺手把两个高频功能补上:列表分页和管理员权限。分页在 MySQL 下用LIMIT ? OFFSET ?最简单,每页 10 条、当前第 2 页就是LIMIT 10 OFFSET 10。权限则在 index.jsp 开头加 session 判断,没登录一律踢回 login.jsp。
这时候再把需求说明书翻出来,逐条对照:手机型号展示、库存管理、订单处理、用户注册登录——每一条对应哪个页面、哪段代码,写在文档边上。答辩时老师问“你这个需求怎么实现的”,你直接报页面名和代码位置,比现场翻代码从容得多。
那次拆这套手机销售系统,我整整一个下午耗在环境上,最后发现不过是 ODBC 数据源位数配错了,代码一行没动。从那以后我每次拿到这种老项目,都强制自己先走一遍“JDK 位数 → 驱动 jar → 数据库路径 → 端口”四项检查,再去读代码;每改一处 SQL,也强制在 Access 里先执行一遍,确认不是语法层的问题再动 Java 代码。希望帮到你。
本文还有配套的精品资源,点击获取