JSP公路车销售系统实战:数据库建模、事务处理与上传避坑全解析

发布时间:2026/10/15 4:25:24
JSP公路车销售系统实战:数据库建模、事务处理与上传避坑全解析 一提到“jsp网上公路车销售系统”很多人第一反应是“这年头还有人在搞JSP”——其实真不少。我前段时间就完整做了一套踩了不少坑也把很多细节重新捋清楚了。这篇直接把整个系统的设计思路、数据库建模、核心功能链路、后台管理、文件上传以及几个高频坑全拆开讲适合正在做JSP课程设计、毕业设计或者想用老技术栈快速搭一套“能跑、能演示、能答辩”的完整项目的人。我要先把话说在前面JSP确实老了但用它做一套B2C电商原型反而能让你把Servlet、Filter、Session、JDBC事务、JSTL、MVC分层这些底子打得非常扎实。这套公路车销售系统不是放着好看的演示品是真能下单、真能支付模拟、真能发货的那种完整闭环。下面从选型逻辑开始讲。1. 为什么选题是“公路车”为什么技术栈选了JSPServlet1.1 公路车这个品类的特殊性决定了系统要做哪些事公路车不是普通的标品。它有两个非常突出的特点价格高、属性多。一辆入门级公路车也要两三千碳架整车两三万也不稀奇所以购买决策周期比买普通运动用品长得多用户会反复看配置、看几何、看尺码。作为一个销售系统你得把“车架材质、变速套件等级、轮组类型、车架尺码、刹车方式、整车重量”这些信息清晰展示出来否则用户没法做决策。这就决定了系统不能只做一个简单的商品列表。商品详情页要有参数表、图片集合、价格展示、库存状态还要能按品牌、按价格区间、按变速套件等级筛选。后台则要能维护这些参数能上下架商品、改库存、处理订单。把这些功能做完其实已经覆盖了一个标准电商后台80%的要点。1.2 为什么我依然选择JSPServlet而不是Spring Boot我做这个选型前其实认真对比过Spring Boot Thymeleaf和前后端分离的做法。最后选了最“古董”的组合理由有三个第一这套系统最核心的价值是教学过程和技术底子。JSP直接暴露了HTTP请求从Servlet到JSP渲染的完整路径Filter怎么拦截、Session怎么存、JDBC连接怎么管理每一步都看得见摸得着。Spring Boot把这些都封装掉了你反而不知道底层发生了什么。第二部署环境简单。不需要Maven全家桶不需要复杂的打包配置一个Tomcat扔进去就能跑。课程设计和毕业设计场景下这简直是最大的省心点。第三很多老系统的维护需求依然存在。学会JSP意味着你能看懂那些跑了好多年的老项目这个技能在现实工作中并不冷门。技术方案上手门槛对Servlet/JDBC底层理解的帮助部署复杂度适合场景JSPServletJDBC较低非常直接低扔Tomcat就能跑课程设计、老项目维护Spring BootThymeleaf中等较低框架封装多中需Maven打包新项目快速开发前后端分离(VueAPI)较高较低需懂跨域/鉴权高需前端部署企业级团队开发我最终的建议是如果你是为了学东西JSP这套方案值得完整做一遍如果你后续要找工作再基于这个项目去理解Spring Boot的封装方式也不迟两者不冲突。2. 数据库设计是第一道分水岭公路车的SKU属性必须拆开存很多JSP课设的数据库就一张商品表加一张订单表字段一贴就完事。这种设计放到公路车品类上一定会出问题。公路车一个车系往往有几款配置版本比如同一款车架可能有“禧玛诺105变速版本”和“禧玛诺UT变速版本”价格、重量、库存都不一样。属性不拆开后台维护就是灾难。2.1 商品主表和属性表宁可设计冗余不要字段堆砌我设计的核心表是bike公路车车型表字段包括id主键bike_name车型名称brand_id关联品牌表model_year年款frame_material车架材质铝架/碳架/钢架groupset变速套件等级wheel_size轮径700c/650bbrake_type刹车方式圈刹/碟刹price价格stock库存status上下架状态0下架1上架create_time创建时间这里的关键点在于我没有把“变速器品牌”“飞轮齿比”“车架重量”“建议身高”这些全部做成字段而是留了一个bike_attr钩子表做属性扩展。原因很简单公路车的参数变化太频繁每年新款都可能加新参数改表结构成本高用键值对结构灵活得多。CREATE TABLE bike ( id INT PRIMARY KEY AUTO_INCREMENT, bike_name VARCHAR(120) NOT NULL, brand_id INT NOT NULL, model_year INT DEFAULT 2024, frame_material VARCHAR(30), groupset VARCHAR(50), wheel_size VARCHAR(20), brake_type VARCHAR(20), price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE bike_attr ( id INT PRIMARY KEY AUTO_INCREMENT, bike_id INT NOT NULL, attr_name VARCHAR(50) NOT NULL, attr_value VARCHAR(200) NOT NULL );有的同学问weight、height_range这些字段直接加在bike表里不行吗也行但你要知道参数的增减是不可控的。用到最后bike表会有二三十个字段一半都是空的页面渲染时还要写死一套if判断很蠢。属性表看起来多了一张表实际维护成本低很多。展示的时候用一次循环把相同bike_id的属性全部拉出来比你写几十个字段取别名灵活多了。2.2 订单相关表从订单主表到订单明细的强一致关系订单设计我给了两张表orders和order_item。orders存放一笔订单的总体信息order_item存放每一件商品的快照信息。这里特别要注意“快照”两个字。我在设计时故意把商品名称、价格都冗余到了order_item里而不去关联bike表。为什么因为公路车的价格和配置随时会变用户在购买时的价格必须以当时的记录为准否则过几个月查订单商品改价了历史订单就对不上账了。CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待付款,1已付款,2已发货,3已完成,4已取消, receiver_name VARCHAR(30) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(200) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, bike_id INT NOT NULL, bike_name VARCHAR(120) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL );订单状态我用了五个数字表示这是整个系统里很重要的一个设计。页面和后台都只认这个数字不认中文需要用JSTL或后台枚举来转换展示名。状态流转逻辑是固定的待付款可以取消待付款点击支付变成已付款已付款后台发货变成已发货已发货确认收货变成已完成。这个状态机保证了前后台操作的边界后台不能把已取消的订单直接改成已发货。2.3 品牌、用户、购物车这几个表的取舍品牌表很简单idnamelogo_url就不贴结构了。用户表除了id、username、password、phone我加了create_time这是方便后台做用户列表排序。购物车我建议用Session实现不建表。有些同学会把购物车建到数据库里理由是刷新不丢但登录用户才能用未登录的浏览用户反而没有购物车体验很差。课程设计阶段Session级购物车完全够用你要做的只是把购物车的数据结构设计好。3. 商品展示到提交订单核心链路的完整拆解3.1 首页与列表页分类导航、筛选、排序和MySQL分页首页我用的是一个index.jsp上半部分是顶部导航和轮播图下半部分是“精品推荐”和“全部车型”两个区域。精品推荐我直接从bike表里查库存大于0且状态为1的前6条数据没有做复杂的推荐算法。列表页按品牌、变速套件等级、价格区间筛选SQL是在dao层动态拼接的。分页这里我强烈建议自己手写不要一上来就引入PageHelper之类的插件。手写分页你才能真正理解LIMIT offset, size的用法。我的做法是前端传page当前页码默认1和pageSize每页条数默认8DAO层查出总记录数算出总页数查询当前页数据LIMIT (page-1)*pageSize, pageSize把数据List、当前页、总页数封装到PageBean对象里放进request域c:forEach items${pageBean.list} varbike div classbike-card img src${bike.picUrl} alt${bike.bikeName}/ h3${bike.bikeName}/h3 p${bike.groupset}/p span classprice${bike.price}/span /div /c:forEach 页码区域 a hrefbikeList?page${pageBean.page-1} c:if test${pageBean.page1}classdisabled/c:if上一页/a span${pageBean.page}/${pageBean.totalPage}/span a hrefbikeList?page${pageBean.page1} c:if test${pageBean.pagepageBean.totalPage}classdisabled/c:if下一页/a这里有个细节很多人会忽略总页数计算。总记录数是15每页8条总页数应该是2。用(totalCount pageSize - 1) / pageSize来算而不是直接整除。直接整除去掉余数最后一页数据就永远显示不出来。3.2 Session级购物车的实现与取舍购物车在JSP项目里最经典的实现是Session里放一个Map。键是商品id值是一个CartItem对象里面封装bikeId、bikeName、price、quantity这几个字段。这样做的好处是用户不登录也能加购物车结账时才要求登录转化路径更顺畅坏处是Session过期购物车就没了但这本来就是一个演示项目的合理取舍不需要太重。前面加购物车的ServletHttpSession session request.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMapInteger, CartItem(); } int bikeId Integer.parseInt(request.getParameter(bikeId)); BikeDao bikeDao new BikeDao(); Bike bike bikeDao.findById(bikeId); if (cart.containsKey(bikeId)) { CartItem item cart.get(bikeId); item.setQuantity(item.getQuantity() 1); } else { CartItem item new CartItem(bike.getId(), bike.getBikeName(), bike.getPrice(), 1); cart.put(bikeId, item); } session.setAttribute(cart, cart);CartItem用Integer做键存在一个经典陷阱Integer缓存范围是-128到127超出范围时Integer.valueOf()会创建新对象。如果你不小心用了两个新的Integer对象做键却不重写equals和hashCode就会拿不到购物车里的数据。实际开发中我建议大家直接用int做参数让HashMap自动装箱并且CartItem里的bikeId字段保持简单类型避免踩这个坑。3.3 提交订单一次事务要保护哪几张表提交订单是整套系统里最需要认真写的环节因为它涉及多表一致性。一个完整的提交订单操作至少要执行三步往orders表插入一条订单主记录循环往order_item表插入订单明细把bike表的库存减掉对应数量清空Session里的购物车如果中间任何一步失败前面插入的订单和明细就都白写了会出现“订单主表有了明细没有”的脏数据。解决办法是JDBC事务关闭自动提交所有操作成功再commit任何异常直接rollback。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 插入订单主表 OrderDao orderDao new OrderDao(); int orderId orderDao.insert(conn, order); // 循环插入订单明细 for (CartItem item : cart.values()) { orderDao.insertItem(conn, orderId, item); } // 扣减库存 bikeDao.decreaseStock(conn, item.getBikeId(), item.getQuantity()); conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } e.printStackTrace(); throw new RuntimeException(提交订单失败); } finally { DBUtil.closeAll(conn, null, null); }注意一个很小的细节很多JSP项目里Dao层每个方法都自己获取连接、释放连接这个写法在单表操作时没问题但遇到这种跨表事务就只能全部重写。所以我在设计这个功能时让insert、insertItem、decreaseStock这些方法都接收外部传入的conn由Service层统一管理连接和事务。这个思想算是我建议你的“事务边界放在Service层”的完整产物。4. 后台管理与文件上传比想象中麻烦的部分4.1 后台模块划分商品、订单、用户三个大头后台管理页面我用了独立的admin文件夹用一个admin_login.jsp做登录入口登录成功后把管理员账号放进Session。后台的拦截逻辑不要写在每个管理员Servlet里那会重到怀疑人生应该用一个Filter统一拦一下filter filter-nameAdminAuthFilter/filter-name filter-classcom.shop.filter.AdminAuthFilter/filter-class /filter filter-mapping filter-nameAdminAuthFilter/filter-name url-pattern/admin/*/url-pattern /filter-mappingpublic void doFilter(...) { HttpSession session req.getSession(); Object admin session.getAttribute(admin); if (admin null) { resp.sendRedirect(admin_login.jsp); return; } chain.doFilter(req, resp); }后台的商品管理无非增删改查但有两个点必须写清楚图片上传和库存修改。库存修改时我做了个简单的乐观锁处理每次更新库存的时候带上原来查到的版本号如果更新影响行数为0就说明被并发修改了提示用户刷新重试。这个功能在课程设计里属于加分项但实际写起来也就几行SQL的事。4.2 图片上传的路径问题和避免重启丢文件的两种方案JSP上传图片如果用Servlet 3.0的MultipartConfig代码本身不复杂真正容易出问题的是图片保存路径。很多教程让你把图片存到项目根目录的upload文件夹下这在启动时只是临时解压的目录Tomcat重启后可能被清空导致已上传的图片全变成404。我试过几种方案最后稳定可行的是这样图片存到服务器本地固定目录比如/webdata/upload然后给Tomcat配置虚拟路径映射让浏览器通过http://localhost:8080/upload/xxx.jpg访问。在Spring Boot里配置虚拟映射很简单但在原生的JSP里Java代码也可以做到类似效果String uploadPath C:/webdata/upload/; // 按实际环境修改 String fileName UUID.randomUUID().toString().replace(-, ) suffix; bike.getPart(pic).write(uploadPath fileName);这里最关键的避坑点是文件名一定不能用用户上传的原始文件名否则两个用户上传“车.jpg”就会相互覆盖。用UUID重命名是最省心的做法。展示图片时页面里的img标签src直接写成/upload/重命名后的文件名再配合Tomcat的虚拟路径映射就完全没问题了。5. 用JSP做公路车销售系统绕不开的五个坑5.1 中文乱码Filter设置字符集的顺序问题JSPServlet做中文项目乱码是必然遇到的。乱码有三个入口请求参数乱码、响应输出乱码、数据库存取乱码。推荐用一个Filter统一处理我习惯在web.xml里配置CharacterEncodingFilter设置encoding为UTF-8并且forceEncoding为true这样请求和响应都会被处理。但有个容易忽略的小坑如果你的web.xml里Filter映射顺序不对或者JSP页面里没有指定pageEncodingUTF-8Filter再怎么写也白搭。页面顶部的% page contentTypetext/html;charsetUTF-8 languagejava %这几个字一定要行行落实到位。5.2 金额精度double是魔鬼公路车整车价格动辄几千上万算总价时如果数据库字段是double总金额在页面上显示成“19999.999999”就很尴尬。我全套代码用的是BigDecimal和DECIMAL(10,2)。购物车结算时用BigDecimal的add方法累加禁止用double直接相乘。这个问题在查看订单报表时尤其明显精度不处理好对账就对不上。5.3 JSTL 标签库版本与新容器的兼容JSTL在JSP项目里几乎是必需品但很多人会遇到“明明写了% taglib urihttp://java.sun.com/jsp/jstl/core prefixc %页面却报错找不到标签库”的情况。这多半是引的JSTL jar包太老。Tomcat 8及以上建议用JSTL 1.2它会自动识别旧版URI。把这些jar放进WEB-INF/lib后最好clean一下再用Maven的同学建议直接引入jstl依赖版本1.2就能解。5.4 SQL注入与PreparedStatementJSP项目因为直接手写SQL注入风险最大。商品筛选这种动态条件SQL只要用户在前端地址栏塞一个or 11很可能就把整个表数据全捞出来。我的原则是所有SQL一律用PreparedStatement参数用?占位禁止用字符串拼接。哪怕筛选条件为空也只拼条件片段不拼用户输入值。这一条必须写进你的代码规范里。5.5 上传图片404与Tomcat重启丢文件这个问题就是我在第4节提到的图片路径问题。很多同学做完上传发现图片能存但页面访问404通常有两个原因一是当前IDE里部署的是解压项目webapps实际目录和你看到的不一致二是没有配虚拟路径映射。我的建议是在本地用一个固定的外部目录存储图片并把存储路径写到配置文件里不要说改就改。还有一个附加问题商品已删除但图片文件还留着长期运行会积累垃圾文件这个虽然不影响功能但你的项目里最好加一个“删除商品时一并删除图片文件”的逻辑这在答辩时是个很不错的细节亮点。我做完这套系统后最大的体会是JSP项目的难点不在JSP标签本身而在于如何在一个没有框架辅助的环境里把分层、事务、路径、编码这些基础问题都处理干净。这些经验一旦沉淀下来后面学什么框架都顺。如果你动手做的时候碰到和我一样的问题或者有自己更顺手的解法评论区聊一聊互相抄抄作业也是好事。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询