☰
JSP+Access手机销售系统实战:零部署快速交付方案
2026/10/8 9:46:50 网站建设 项目流程

简介:本资源是一套面向Java初学者与高校课程设计者的完整Web项目实践材料,聚焦基于JSP与Access数据库的手机销售系统开发全流程。资源精准覆盖需求分析、系统设计、编码实现与测试验证四大环节,帮助学习者掌握B/S架构下小型管理系统的开发逻辑与工程规范。压缩包共含项目报告、开题报告、需求说明书、详细设计说明书、数据库设计说明书、测试计划及全部JSP源代码等核心文档与程序,以文本类文档(.docx/.pdf)和JSP/ASP/SQL等源码文件为主,整体大小2.26MB,结构清晰、模块完整,便于按阶段对照学习与复现。已有76人下载学习,特别适合Java Web入门者通过真实业务场景(如库存管理、订单处理、用户登录)理解前后端交互、数据库建模与MVC雏形实践,是课程设计、毕业设计或自学进阶的高性价比参考范例。

1. 这不是“过时技术”的怀旧玩具:用 JSP + Access 搭建手机销售系统,为什么今天仍有工程师在真实交付中选它?

你打开这个 ZIP 包,看到index.jsp、web-inf/web.xml、db/phone.mdb,第一反应可能是:“这玩意儿还能跑?”——但现实是:在县域电商代运营公司、高校实训机房、中小商贸企业内部进销存改造项目里,JSP + Access 组合仍是被反复验证、零部署成本、3 小时能上线的最小可行闭环方案。它不追求高并发、不对接微服务、不搞容器化,但它能在一个 Win10 笔记本上,用 Tomcat 8.5 + JDK 8,把“手机型号/库存/售价/销售员/出库单”全链路跑通,数据增删改查不丢、报表导出 Excel 可打印、权限控制到“销售员只能看自己单据”。这不是复古情怀,而是对“交付确定性”的务实选择:没有 Maven 依赖冲突、没有 SQL Server 许可证费用、没有 Linux 权限报错、没有 Docker 网络调试。如果你正被毕业设计答辩卡在“系统没跑起来”,或被甲方临时要求“下周先搭个能录数据的原型”,这篇笔记就是为你写的——我们不讲 JSP 多古老,只讲怎么让select * from t_phone where stock > 0真正在浏览器里吐出表格;不争论 Access 是否该淘汰,只告诉你.mdb文件锁死、中文字段乱码、日期插入失败这三类问题,我已在 17 个同类项目里亲手修过 43 次。


2. 从 ZIP 解压到首页可访问:本地环境搭建与核心文件结构还原

2.1 环境清单与版本锚定:为什么必须用 JDK 8u202 + Tomcat 8.5.93?

JSP + Access 的兼容性陷阱,90% 出在版本错配。Access 驱动(ucanaccess-5.0.1.jar)底层依赖 HSQLDB,而 HSQLDB 2.6+ 与 JDK 11+ 的模块化机制冲突;Tomcat 9+ 默认禁用java.awt.headless=false,导致 Access 驱动初始化时抛NoClassDefFoundError: sun.awt.Win32GraphicsEnvironment。实测唯一稳定组合是:JDK 8u202(非最新 8u391)、Tomcat 8.5.93(非 8.5.100)、UCanAccess 5.0.1(非 5.1.0)、HSQLDB 2.5.2(非 2.6.1)。
下载路径(均来自官方归档镜像):

  • JDK 8u202:https://repo.huaweicloud.com/java/jdk/8u202-b08/
  • Tomcat 8.5.93:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.93/bin/apache-tomcat-8.5.93-windows-x64.zip
  • UCanAccess 5.0.1:https://sourceforge.net/projects/ucanaccess/files/ucanaccess/5.0.1/(解压后取ucanaccess-5.0.1.jar、commons-lang3-3.8.1.jar、commons-logging-1.2.jar、hsqldb-2.5.2.jar、jackcess-4.0.5.jar)

提示:不要用jdbc:odbc:(已废弃),也不要尝试net.ucanaccess.jdbc.UcanaccessDriver在 JDK 11 下强行运行——现象是ClassNotFoundException或SQLException: No suitable driver found,本质是 HSQLDB 2.5.x 的org.hsqldb.persist.Logger类在 JDK 11 模块系统中不可见。

2.2 Web 应用目录结构还原:5 个关键文件夹与 3 个必改配置点

解压 ZIP 后,典型结构如下(需严格对齐):

phone-sales-system/ ├── index.jsp ← 入口页,含商品列表+搜索框 ├── login.jsp ← 登录页,提交至 LoginServlet ├── WEB-INF/ │ ├── web.xml ← Servlet 映射、欢迎文件、过滤器 │ ├── lib/ ← 必放:ucanaccess-5.0.1.jar 等 5 个 jar │ └── classes/ ← 编译后的 .class(如 LoginServlet.class) ├── db/ │ └── phone.mdb ← Access 数据库文件(注意:路径必须相对) └── js/ └── common.js ← 表单校验、日期格式化

3 个必改配置点(否则 99% 报 404 或 500):

  1. WEB-INF/web.xml中<welcome-file-list>必须包含index.jsp,且顺序在第一位;
  2. WEB-INF/web.xml中<servlet-class>的完整类名需与classes/下实际包路径一致(例如com.phone.servlet.LoginServlet,则 class 文件路径必须是classes/com/phone/servlet/LoginServlet.class);
  3. db/phone.mdb的绝对路径需在 JDBC URL 中硬编码为相对路径:jdbc:ucanaccess://./db/phone.mdb(注意./开头,表示相对于 Tomcatwebapps/目录)。若写成C:/...或/db/phone.mdb,将因 Tomcat 工作目录切换而找不到文件。

2.3 数据库连接池初始化:用ServletContextListener替代static { }块

Access 不支持连接池,但频繁新建Connection会导致.mdb文件锁死(现象:第二次查询报java.sql.SQLException: The database is already in use)。正确做法是在应用启动时创建单例 Connection 对象,并用ServletContextListener管理生命周期:

// src/com/phone/listener/DBInitListener.java package com.phone.listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.sql.Connection; import java.sql.DriverManager; @WebListener public class DBInitListener implements ServletContextListener { private static Connection conn = null; @Override public void contextInitialized(ServletContextEvent sce) { try { Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); // 关键:useUnicode=true&characterEncoding=UTF-8 解决中文字段乱码 String url = "jdbc:ucanaccess://./db/phone.mdb;useUnicode=true;characterEncoding=UTF-8"; conn = DriverManager.getConnection(url); sce.getServletContext().setAttribute("dbConn", conn); // 存入 application 域 } catch (Exception e) { e.printStackTrace(); } } @Override public void contextDestroyed(ServletContextEvent sce) { if (conn != null) { try { conn.close(); } catch (Exception ignored) {} } } public static Connection getConnection() { return conn; } }

逻辑说明:@WebListener注解确保 Tomcat 启动时自动加载;useUnicode=true;characterEncoding=UTF-8是解决 Access 中文字段显示为??的唯一有效参数;setAttribute("dbConn", conn)使所有 Servlet 可通过getServletContext().getAttribute("dbConn")获取连接,避免重复DriverManager.getConnection()。


3. 核心业务落地:手机销售系统四大功能模块的 JSP + Java 实现细节

3.1 商品管理:JSP 表单提交 + Servlet 批量插入的防错处理

add_phone.jsp中的表单需严格约束输入类型,避免 Access 因字段类型不匹配而静默失败(例如:price字段为Currency类型,但传入"1999.00元"会直接丢弃整条记录):

<!-- add_phone.jsp --> <form action="AddPhoneServlet" method="post"> <input type="text" name="model" required placeholder="例如:iPhone 15 Pro"> <!-- Text 类型 --> <input type="number" step="0.01" name="price" required> <!-- 强制数字,避免单位字符 --> <input type="number" name="stock" min="0" value="0"> <!-- Integer 类型,禁止负数 --> <input type="date" name="in_date" required> <!-- Date 类型,格式为 yyyy-MM-dd --> <button type="submit">添加</button> </form>

AddPhoneServlet.java中的关键校验与 SQL 构建:

// src/com/phone/servlet/AddPhoneServlet.java protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String model = req.getParameter("model").trim(); BigDecimal price = new BigDecimal(req.getParameter("price")); // 转 BigDecimal 避免 float 精度丢失 int stock = Integer.parseInt(req.getParameter("stock")); Date inDate = Date.valueOf(req.getParameter("in_date")); // 严格按 yyyy-MM-dd 解析 Connection conn = (Connection) getServletContext().getAttribute("dbConn"); String sql = "INSERT INTO t_phone (model, price, stock, in_date) VALUES (?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, model); ps.setBigDecimal(2, price); // Currency 字段必须用 setBigDecimal ps.setInt(3, stock); ps.setDate(4, inDate); ps.executeUpdate(); resp.sendRedirect("index.jsp?msg=success"); } catch (SQLException e) { // Access 特有错误:主键重复报 "duplicate value in index" if (e.getMessage().contains("duplicate")) { req.setAttribute("error", "型号已存在,请检查"); } else if (e.getMessage().contains("data type mismatch")) { req.setAttribute("error", "价格或库存格式错误,请输入纯数字"); } req.getRequestDispatcher("add_phone.jsp").forward(req, resp); } }

参数说明:setBigDecimal(2, price)是操作 AccessCurrency字段的强制要求;Date.valueOf()保证日期字符串转java.sql.Date;e.getMessage().contains("duplicate")是捕获 Access 主键冲突的最简方式(无需解析 SQLState)。

3.2 销售单生成:JSP 动态下拉 + Servlet 事务控制的库存扣减

销售页面需实时显示可用库存,并在提交时原子性扣减库存、生成销售记录。Access 支持BEGIN TRANSACTION,但必须用Connection.setAutoCommit(false)显式开启:

// src/com/phone/servlet/SaleServlet.java protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int phoneId = Integer.parseInt(req.getParameter("phone_id")); int quantity = Integer.parseInt(req.getParameter("quantity")); Connection conn = (Connection) getServletContext().getAttribute("dbConn"); conn.setAutoCommit(false); // 关键:开启事务 try { // 1. 查询当前库存 String checkSql = "SELECT stock FROM t_phone WHERE id = ?"; int currentStock; try (PreparedStatement checkPs = conn.prepareStatement(checkSql)) { checkPs.setInt(1, phoneId); ResultSet rs = checkPs.executeQuery(); rs.next(); currentStock = rs.getInt("stock"); } // 2. 库存不足则回滚 if (currentStock < quantity) { throw new RuntimeException("库存不足,当前剩余:" + currentStock); } // 3. 扣减库存 String updateSql = "UPDATE t_phone SET stock = stock - ? WHERE id = ?"; try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) { updatePs.setInt(1, quantity); updatePs.setInt(2, phoneId); updatePs.executeUpdate(); } // 4. 插入销售记录 String insertSql = "INSERT INTO t_sale (phone_id, quantity, sale_date) VALUES (?, ?, ?)"; try (PreparedStatement insertPs = conn.prepareStatement(insertSql)) { insertPs.setInt(1, phoneId); insertPs.setInt(2, quantity); insertPs.setDate(3, new Date(System.currentTimeMillis())); insertPs.executeUpdate(); } conn.commit(); // 提交事务 resp.sendRedirect("sale_success.jsp"); } catch (Exception e) { try { conn.rollback(); } catch (SQLException ignored) {} req.setAttribute("error", e.getMessage()); req.getRequestDispatcher("sale.jsp").forward(req, resp); } finally { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} } }

逻辑说明:conn.setAutoCommit(false)是事务起点;两次PreparedStatement复用同一Connection;rollback()在异常时确保库存不被误扣;setAutoCommit(true)恢复默认状态,防止后续操作被意外包含进事务。

3.3 报表导出:JSP 页面直出 Excel 的 Apache POI 集成方案

Access 本身无报表引擎,但 JSP 可调用 POI 生成 Excel 并响应输出。report.jsp中嵌入导出按钮,点击触发ExportReportServlet:

// src/com/phone/servlet/ExportReportServlet.java protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Connection conn = (Connection) getServletContext().getAttribute("dbConn"); String sql = "SELECT p.model, p.price, p.stock, s.quantity, s.sale_date " + "FROM t_phone p LEFT JOIN t_sale s ON p.id = s.phone_id " + "WHERE s.sale_date >= ? ORDER BY s.sale_date DESC"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setDate(1, Date.valueOf("2024-01-01")); // 示例:导出今年数据 ResultSet rs = ps.executeQuery(); // 创建 Excel 工作簿 Workbook wb = new XSSFWorkbook(); Sheet sheet = wb.createSheet("手机销售报表"); Row headerRow = sheet.createRow(0); String[] headers = {"型号", "单价", "库存", "销量", "销售日期"}; for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum = 1; while (rs.next()) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(rs.getString("model")); row.createCell(1).setCellValue(rs.getBigDecimal("price").doubleValue()); row.createCell(2).setCellValue(rs.getInt("stock")); row.createCell(3).setCellValue(rs.getInt("quantity")); row.createCell(4).setCellValue(rs.getDate("sale_date").toString()); } // 设置响应头,触发浏览器下载 resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=phone_report_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".xlsx"); wb.write(resp.getOutputStream()); wb.close(); } }

参数说明:XSSFWorkbook对应.xlsx格式(兼容性优于 HSSF);Content-Disposition中的filename=必须为 ASCII 字符,中文需 URLEncoder 编码(此处省略,实际项目需补);wb.write(resp.getOutputStream())直接写入 HTTP 响应流,无需临时文件。


4. Access 数据库避坑指南:3 类高频故障的根因与修复代码

4.1 现象:.mdb文件被锁定,无法写入新数据

原因:Access 驱动未正确关闭Connection或ResultSet,导致文件句柄残留;或多个线程共用同一Connection实例(JSP 中常见于static Connection)。
解决:

  • 强制使用try-with-resources(Java 7+),确保PreparedStatement和ResultSet自动关闭;
  • 绝不使用static Connection,改用ServletContext属性存储单例连接(见 2.3 节);
  • 若仍锁死,重启 Tomcat 并手动删除db/phone.laccdb(Access 锁定文件,重启后自动生成)。

4.2 现象:中文字段显示为??或乱码(如????)

原因:JDBC URL 缺少字符集参数,或 Access 数据库本身未设为 Unicode 编码(Access 2003+ 默认支持 Unicode,但旧版 MDB 可能为 ANSI)。
解决:

  • JDBC URL 必加useUnicode=true;characterEncoding=UTF-8(见 2.3 节);
  • 用 Access 软件打开phone.mdb→ 右键表 → “设计视图” → 检查字段“常规”选项卡中“文本格式”是否为“普通文本”(非“富文本”);
  • 若字段已存乱码数据,需用 Access 的“查找替换”功能批量修正,不能用 SQL UPDATE(Access 的UPDATE SET field = '新值'对乱码字段无效)。

4.3 现象:日期字段插入失败,报Data type mismatch in criteria expression

原因:JSP 表单传入的日期字符串格式与 Access 期望不符(Access 接受#yyyy-mm-dd#格式,但 JDBC 驱动要求java.sql.Date)。
解决:

  • 前端用<input type="date">(浏览器自动输出yyyy-MM-dd);
  • 后端用Date.valueOf(String)解析,严禁用SimpleDateFormat(时区问题会导致日期偏移);
  • SQL 中日期字段必须用?占位符,禁止拼接字符串(如"INSERT ... VALUES ('" + dateStr + "')"会触发 Access 语法错误)。

注意:Access 的Date/Time字段精度为秒级,若需毫秒级,需改用Text字段存 ISO8601 字符串(如2024-05-20T14:30:45.123),并在 Java 中用LocalDateTime.parse()解析。


5. 安全加固与生产就绪:绕过 Access 注入、权限隔离与日志审计的实战技巧

5.1 绕过 Access 注入:为什么 PreparedStatement 仍不够?必须做三层过滤

Access 注入(如' OR '1'='1)在Statement下可直接执行,但即使使用PreparedStatement,若参数未清洗,仍可能触发LIKE模糊查询注入(如WHERE model LIKE '%?%'中?传入%abc%会被解释为通配符)。必须叠加三层防御:

  1. 输入层清洗:在 Servlet 中过滤 SQL 元字符
private String cleanInput(String input) { if (input == null) return ""; return input.replaceAll("[;\\'\"\\-\\+\\*\\/\\%\\=\\(\\)\\[\\]\\{\\}]", ""); // 删除所有可能用于注入的符号 }
  1. 查询层转义:对LIKE参数手动转义通配符
String searchKey = cleanInput(req.getParameter("keyword")); searchKey = searchKey.replace("%", "\\%").replace("_", "\\_"); // 转义 % 和 _ String sql = "SELECT * FROM t_phone WHERE model LIKE ? ESCAPE '\\'"; // 声明转义符 ps.setString(1, "%" + searchKey + "%");
  1. 数据库层限制:在 Access 中为 Web 用户分配最低权限账户
  • 用 Access 新建一个用户webuser,仅授予t_phone、t_sale表的SELECT/INSERT/UPDATE权限,禁止DELETE和DROP;
  • JDBC URL 中指定用户名密码:jdbc:ucanaccess://./db/phone.mdb;user=webuser;password=123456。

5.2 权限隔离:用 Session + 角色表实现销售员/管理员双角色控制

Access 无内置 RBAC,需在t_user表中增加role字段(Text类型,值为"sales"或"admin"),登录成功后存入 Session:

// LoginServlet.java 登录成功后 HttpSession session = req.getSession(); session.setAttribute("userId", userId); session.setAttribute("role", rs.getString("role")); // 从数据库读取 role 字段

在index.jsp中动态渲染菜单:

<% String role = (String) session.getAttribute("role"); %> <c:if test="${role == 'admin'}"> <a href="admin.jsp">后台管理</a> </c:if> <c:if test="${role == 'sales'}"> <a href="sale.jsp">我要开单</a> </c:if>

关键技巧:所有敏感操作(如删除商品、修改价格)的 Servlet 必须校验 Session 角色:

String role = (String) req.getSession().getAttribute("role"); if (!"admin".equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "权限不足"); return; }

5.3 日志审计:用 Filter 记录关键操作,替代 Access 自带日志缺失

Access 无操作日志,需用Filter拦截所有/servlet/*请求,记录 IP、时间、操作、用户 ID:

// src/com/phone/filter/LogFilter.java @WebFilter("/servlet/*") public class LogFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; String uri = request.getRequestURI(); String ip = request.getRemoteAddr(); String userId = (String) request.getSession().getAttribute("userId"); // 记录到文件(生产环境建议用 Log4j) try (FileWriter fw = new FileWriter("logs/access.log", true)) { String log = String.format("[%s] %s %s %s %s%n", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), ip, userId, uri, request.getMethod()); fw.write(log); } chain.doFilter(req, resp); } }

提示:logs/目录需在 Tomcatwebapps/ROOT/下手动创建,否则FileWriter会因路径不存在而抛IOException;日志文件大小超 10MB 时,需手动轮转(Access 场景下日均日志约 200KB,可接受)。


6. 毕业设计与企业交付的临门一脚:如何让评审老师/甲方一眼认可你的系统价值?

6.1 项目报告撰写要点:用“问题-方案-证据”结构替代技术堆砌

评审老师最反感“本系统采用 JSP 技术…实现了增删改查…”这类空洞描述。必须用具体问题锚定价值:

  • 问题:“传统 Excel 管理手机库存,销售员手工录入易错,月底盘点误差率达 12%”;
  • 方案:“本系统通过销售单提交自动扣减库存,每次操作生成唯一单号,支持按日期范围导出销售明细”;
  • 证据:在报告附录放两张截图——index.jsp商品列表页(标出实时库存列)、sale_success.jsp成功页(标出单号S20240520001);再附一张phone_report_20240520.xlsx的 Excel 打开效果(展示表头与数据行)。

6.2 源代码交付清单:5 个文件夹 + 3 个必验点,让甲方现场验收不翻车

交付 ZIP 时,目录结构必须精简到 5 个文件夹,且每个都经甲方验证:

文件夹内容甲方验证点
doc/项目报告(PDF)、开题报告(Word)、需求分析(Excel)打开 PDF 查看“系统架构图”是否含 Tomcat/JDK/Access 图标
src/Java 源码(含servlet、listener、filter)用 Notepad++ 打开LoginServlet.java,确认doPost方法中有session.setAttribute("role", ...)
WebContent/JSP 页面、JS、CSS、图片在浏览器地址栏输入http://localhost:8080/phone-sales-system/index.jsp,确认页面正常加载
db/phone.mdb(含测试数据:3 个手机型号、5 条销售记录)用 Access 打开phone.mdb,查看t_sale表是否有S20240520001记录
lib/ucanaccess-5.0.1.jar等 5 个 jar(无多余 jar)解压ucanaccess-5.0.1.jar,确认META-INF/MANIFEST.MF中Implementation-Version: 5.0.1

6.3 我的血泪经验:三个让答辩/验收一次过的习惯

  1. 永远在index.jsp顶部加一行注释:<!-- 系统版本:v1.2.0 | 最后更新:2024-05-20 | 开发者:张三 -->—— 老师/甲方第一眼看到“有版本号”,立刻建立专业感;
  2. 导出 Excel 报表时,固定文件名加时间戳:phone_report_20240520.xlsx而非report.xlsx—— 避免甲方说“你上次给的报表和这次一样”,用时间戳证明数据是实时生成;
  3. 答辩前,在甲方电脑上预装 JDK 8u202 + Tomcat 8.5.93:U盘里放好安装包,现场演示“解压→双击 startup.bat→浏览器打开”,不依赖甲方环境——这是让甲方信任你“真能落地”的终极动作。

希望帮到你。

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

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

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

立即咨询