JSP+Access手机销售系统开发详解:从建库到部署全流程
2026/9/11 11:57:06 网站建设 项目流程

简介:这是一套围绕手机销售场景、采用JSP与Access数据库开发的完整项目资料包,面向需要完成课程设计或毕业设计的Java学习者。资料内容覆盖从需求分析、系统设计、编码实现到测试验收的完整软件生命周期,能帮助使用者解决项目文档不完整、代码结构不清晰等典型问题。压缩包体积仅2.26MB,内部包含项目报告、开题报告、需求说明书、详细设计说明书、数据库设计说明书、测试计划以及全部源代码,各类文档与代码相互配套,便于按阶段查阅。目前已有75人学习。借助这些资料,可以深入领会JSP动态网页技术与关系型数据库的结合方式,掌握手机型号展示、库存管理、订单处理、用户注册登录等核心功能的实现细节,同时参考规范化的项目文档写法,为独立完成同类管理系统积累实战经验。

1. JSP+Access手机销售系统:课程设计里最需要先想清楚的那几件事

很多人拿到"基于jsp+access的手机销售系统"这个题目,第一反应是找现成源码,但我更建议你先想清楚一件事:Access这种文件型数据库配上JSP这种动态页面技术,到底解决的是"业务逻辑验证"还是"生产级并发"。答案是前者。JSP把手机型号、价格、库存动态渲染成HTML,Access把数据落在本地一个.accdb文件里,整个系统不需要独立数据库服务器,杀毒软件不拦,拷给老师演示也方便。这个组合特别适合时间紧、又需要同时交项目报告、源代码和开题文档的课程设计。下面按从建库到部署再到验收演示的顺序,把一个可复现的路径讲清楚。

2. JSP+Access手机销售系统的Access库:从需求分析到表结构

2.1 需求分析落地成表:用户、手机、订单的主表设计

需求分析文档里会写"管理员维护手机信息""用户浏览下单",落到Access里就是四张表。以下是常见字段方案:

表名关键字段类型用途
UsersuserId, userName, password, isAdmin自动编号/文本/数字登录注册,区分普通用户和管理员
PhonephoneId, brand, model, price, stock自动编号/文本/货币/整数商品展示与库存扣减
OrdersorderId, userId, orderDate, totalPrice自动编号/数字/日期/货币订单主表
OrderDetaildetailId, orderId, phoneId, quantity, price自动编号/数字/数字/货币订单子表,记录每部手机购买数量

设计时让userId关联Users表,phoneId关联Phone表,这样按品牌统计销量时不需要解析长文本字段。Access里打开“数据库工具→关系”,把Orders.userId连到Users.userId,OrderDetail.phoneId连到Phone.phoneId,勾选“实施参照完整性”和“级联删除相关记录”。数据库层面先挡住脏数据,JSP层再配合DAO校验,两条线兜底。

2.1.1 用SQL建表而不是鼠标点建表

虽然Access支持可视化建表,但项目报告里通常会要求贴SQL语句。在Access的查询设计视图里切到SQL视图执行:

CREATE TABLE Phone ( phoneId AUTOINCREMENT PRIMARY KEY, brand TEXT(50), model TEXT(50), price CURRENCY, stock INTEGER, description MEMO );

AUTOINCREMENT对应JSP里的自增主键;CURRENCY是Access的货币类型,避免double浮点误差;MEMO是长文本,放手机参数和卖点介绍。在JDBC读ResultSet时,CURRENCY映射为BigDecimal,正好可以直接set到页面上,不会出现0.999999这类金额误差。

2.2 JSP访问Access:用UCanAccess替代JDBC-ODBC桥

早期教材让配ODBC数据源,类名是sun.jdbc.odbc.JdbcOdbcDriver。JDK 8开始移除了JDBC-ODBC桥,新项目直接报ClassNotFoundException。现在常见做法是使用UCanAccess,它通过Jackcess直接读写.mdb/.accdb文件,不需要在控制面板建DSN,也不依赖系统位数。

把ucanaccess、jackcess以及依赖的commons-lang、commons-logging放入WEB-INF/lib,然后写一个连接工具类:

package util; import java.sql.*; public class DBUtil { // 固定外部路径,避免相对路径找错目录 private static final String URL = "jdbc:ucanaccess:///E:/phonesale/db/phone.accdb"; static { try { Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL); } }

注意URL里三个斜杠:jdbc:ucanaccess:///后面跟绝对路径。如果只写两个斜杠,会被当成host:port格式去解析,必然连不上。若把.accdb放进Tomcat的webapps,目录锁会和文件锁冲突,建议放外部目录。UCanAccess驱动在JDK 8及以上运行更稳,低于这个版本建议直接升级JDK。

2.3 把SQL收进DAO,订单事务别散在JSP里

有些参考代码直接在JSP脚本片段里写JDBC,页面一多就难以维护。按照项目报告里“系统设计”一节的要求,三层结构更完整。PhoneDAO负责商品查询,UserDAO负责登录,OrderDAO负责下单事务。下面是一个带事务的下单方法:

public boolean createOrder(int userId, List<CartItem> cart) { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 关闭自动提交,订单与明细同生共死 String insertOrder = "INSERT INTO Orders(userId, orderDate, totalPrice) VALUES(?,?,?)"; PreparedStatement ps1 = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); ps1.setInt(1, userId); ps1.setDate(2, new java.sql.Date(System.currentTimeMillis())); double total = cart.stream().mapToDouble( i -> i.getPrice().doubleValue() * i.getQuantity()).sum(); ps1.setDouble(3, total); ps1.executeUpdate(); ResultSet keys = ps1.getGeneratedKeys(); int orderId = keys.next() ? keys.getInt(1) : 0; String insertDetail = "INSERT INTO OrderDetail(orderId, phoneId, quantity, price) VALUES(?,?,?,?)"; String updateStock = "UPDATE Phone SET stock = stock - ? WHERE phoneId = ? AND stock >= ?"; for (CartItem item : cart) { try (PreparedStatement ps2 = conn.prepareStatement(insertDetail)) { ps2.setInt(1, orderId); ps2.setInt(2, item.getPhoneId()); ps2.setInt(3, item.getQuantity()); ps2.setBigDecimal(4, item.getPrice()); ps2.executeUpdate(); } // 乐观更新:库存不够时影响行数为0,直接回滚 try (PreparedStatement ps3 = conn.prepareStatement(updateStock)) { ps3.setInt(1, item.getQuantity()); ps3.setInt(2, item.getPhoneId()); ps3.setInt(3, item.getQuantity()); if (ps3.executeUpdate() == 0) { throw new SQLException("库存不足或手机不存在"); } } } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { e.printStackTrace(); } } return false; } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

关键点是setAutoCommit(false)之后,所有SQL必须在同一个事务里执行,中途任一一步失败就rollback。WHERE phoneId = ? AND stock >= ?这一段是防止超卖的关键,JDBC setInt时参数顺序不能错。Access本身并发能力弱,这种乐观更新方式在课程设计场景里足够。

3. 手机销售系统页面流程:列表、购物车、管理员的增删改查

3.1 手机列表页:JSTL循环渲染ResultSet

手机销售系统的门面是首页的手机列表。Servlet从PhoneDAO取List 后,请求转发到list.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table border="1"> <tr> <th>品牌</th><th>型号</th><th>价格</th><th>库存</th><th>操作</th> </tr> <c:forEach items="${phoneList}" var="p"> <tr> <td>${p.brand}</td> <td>${p.model}</td> <td>${p.price}</td> <td>${p.stock}</td> <td><a href="addCart?phoneId=${p.phoneId}">加入购物车</a></td> </tr> </c:forEach> </table>

这里最核心的是contentType里的UTF-8,否则从Access里读出的中文会变成问号。EL表达式用${p.brand}访问getter,Phone类里必须有getBrand这个方法,而不是把字段直接暴露。如果想让列表更灵活,也可以引入jsp jsonarray import,把phoneList序列化成JSON,交给前端Bootstrap Table渲染,分页排序会方便很多。

3.2 用户登录与注册:Session存登录状态

登录页提交到LoginServlet,先查Users表,成功后再把用户对象放进Session:

String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDAO.findByUsernameAndPassword(username, password); if (user != null) { // 登录成功才写session,失败保留原页面 HttpSession session = request.getSession(); session.setAttribute("user", user); if (user.getIsAdmin() == 1) { response.sendRedirect("admin/index.jsp"); } else { response.sendRedirect("index.jsp"); } } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }

findByUsernameAndPassword内部必须用PreparedStatement参数化查询,不能把username和password拼接进SQL字符串,否则登录接口就是注入重灾区。登出时调用session.invalidate()清掉整个会话,只remove一个属性容易造成权限残留。

3.3 加入购物车与下单:Session临时存储与事务写入

购物车放Session,用户加购不写库,结账时才走2.3的事务方法:

HttpSession session = request.getSession(); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } int phoneId = Integer.parseInt(request.getParameter("phoneId")); for (CartItem item : cart) { if (item.getPhoneId() == phoneId) { item.setQuantity(item.getQuantity() + 1); return; } } // 从Phone带出名称和价格,购物车页面不用反复查库 CartItem item = new CartItem(); item.setPhoneId(phoneId); item.setQuantity(1); Phone phone = phoneDAO.findById(phoneId); item.setName(phone.getModel()); item.setPrice(phone.getPrice()); cart.add(item);

CartItem里同时存phoneId、name、price,是因为JSP渲染购物车时只需要这三项,没必要每次都回查Phone表。真正下单时再从Phone表读一次价格做复核,避免Session里价格被人为改掉。

3.4 后台管理:手机信息维护的update/delete

管理员按品牌或型号搜索后,编辑手机价格和库存:

public void updatePhone(Phone p) { String sql = "UPDATE Phone SET brand=?, model=?, price=?, stock=?, description=? WHERE phoneId=?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, p.getBrand()); ps.setString(2, p.getModel()); ps.setBigDecimal(3, p.getPrice()); ps.setInt(4, p.getStock()); ps.setString(5, p.getDescription()); ps.setInt(6, p.getPhoneId()); int rows = ps.executeUpdate(); // 更新行数为0说明记录不存在,主动抛异常便于前端提示 if (rows == 0) { throw new RuntimeException("手机不存在,可能被删除"); } } catch (SQLException e) { throw new RuntimeException("更新手机信息失败", e); } }

delete操作建议加二次确认,前端用onclick="return confirm('确定删除?')",避免演示时误删数据。如果手机已被OrderDetail引用,Access会因参照完整性拒绝删除,这时要先清掉订单明细,这正好可以在项目报告里作为数据完整性保护的实际案例。

4. 部署Tomcat的必调参数与JSP+Access常见报错

4.1 内存访问冲突:process exited with code 3221225477 / 0xc0000005

Tomcat或JSP首次编译时如果出现process exited with code 3221225477 / 0xc0000005,说明JVM进程崩溃到了非法内存地址。在JSP+Access项目里,最常见原因是驱动和JDK位数不匹配,或者UCanAccess依赖的原生文件映射和Tomcat冲突。

排查步骤按顺序做:第一,确认java -version和Tomcat的JAVA_HOME指向同一个JDK,32位JDK配64位Tomcat最容易触发。第二,停掉Tomcat,删掉work目录下整个Catalina缓存,避免旧class文件和新驱动冲突。第三,如果崩溃总是发生在读取.accdb时,把Access数据库另存为2002-2003格式(.mdb),UCanAccess对旧格式的兼容更保守。处理完记得重启Tomcat,并观察catalina.out最后一段日志。

4.2 Access文件锁与access denied

Access出于并发控制会在数据文件旁边生成.ldb锁文件。当JSP里连接没关闭时,第二次请求会报Could not lock file。解决办法是把所有Connection、PreparedStatement、ResultSet放到try-with-resources里,保证方法结束就释放。如果仍频繁报错,检查E:/phonesale/db目录的写入权限,进程对.accdb旁边生成的.ldb文件也要有读写权。

这里说的access denied和MySQL的ERROR 1045 access denied for user 'root'@'localhost'完全不同,后者是MySQL账号密码错,两个报错不要混在一起查。Access项目里出现access denied,先看文件目录权限和是否被另一个进程独占。

4.3 JSP编译class文件路径与页面不刷新问题

JSP首次访问后,Tomcat会在work/Catalina/localhost/<项目名>/org/apache/jsp/目录下生成对应的.java和.class文件。如果Maven或IDEA热部署失效,改完JSP后页面还是旧的,按下面顺序处理:

  1. 浏览器按Ctrl+F5强制刷新,排除浏览器缓存。
  2. 查看Tomcat的conf/web.xml里是否有development=false配置,开发阶段要删掉,否则JSP会缓存编译结果。
  3. 直接删除work目录下项目名对应的整个目录,重启Tomcat重新编译。这一步能解决90%的“JSP改了不生效”问题,因为.class文件就是JSP页面的最终产物。

4.4 web.xml统一定义字符编码过滤器

Access读取出来中文乱码,最常发生在请求和响应的编码不一致时。在web.xml最前面加一个编码过滤:

<filter> <filter-name>encoding</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

这是Tomcat自带的过滤器,不需要额外jar包。它会把request和response的编码统一成UTF-8。注意Access默认排序规则可能是简体中文GBK,存进去时如果用GBK,读出后需要在DAO层转码,建议建表时把Access数据库的排序规则设为Unicode,能省去后面大量乱码修复时间。

5. 验收加分项:登录过滤器与销售统计Excel导出

5.1 用Filter拦截admin目录,权限校验只写一次

管理后台每个页面都检查session会重复写代码,直接写一个Filter拦截/admin/*:

public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { ((HttpServletResponse) resp).sendRedirect("../login.jsp"); return; } User u = (User) session.getAttribute("user"); if (u.getIsAdmin() != 1) { resp.getWriter().write("无权限访问"); return; } chain.doFilter(request, resp); } }

映射URL是/admin/*,重定向时要写相对路径../login.jsp,因为Filter里request的当前路径是项目根下的admin目录,相对路径要上跳一级。权限校验只写这一处,后续新增后台页面自动被保护,项目报告里在“安全设计”小节可以明确写出这一条。

5.2 按品牌统计销量并导出Excel

汇总SQL用Access语法:

SELECT p.brand, SUM(od.quantity) AS totalQty FROM OrderDetail od INNER JOIN Phone p ON od.phoneId = p.phoneId GROUP BY p.brand ORDER BY totalQty DESC;

导出时把响应设为Excel MIME类型,数据用制表符分隔,Excel能直接打开:

response.setContentType("application/vnd.ms-excel;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=sales.xls"); PrintWriter out = response.getWriter(); out.println("品牌\t销量"); // 遍历ResultSet输出 while (rs.next()) { out.println(rs.getString("brand") + "\t" + rs.getInt("totalQty")); }

演示时在管理员首页放一个“导出销售统计”按钮,点击后浏览器直接下载xls。把导出的文件用Excel打开,配合代码里的UCanAccess连接串,就能向老师展示“数据从Access来,经过JSP输出,最终落地成Excel报表”这一整条链路。再加一个按日期的订单明细查询,用同样方式导出,整个系统的数据流就完整了。

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

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

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

立即咨询