☰
JSP+Servlet实现的彝族文化服饰交易平台毕业设计全解析
2026/10/13 7:31:04 网站建设 项目流程

彝族文化服饰交易平台这类题目,我这些年见过不少学生做类似的“行业+电商”组合。说实话,选这个题的人很多,但真正能讲清楚设计思路、把JSP这套老技术栈玩明白的没几个。这篇东西我就按毕业设计的标准流程,从选题定位、架构设计、数据库建模到核心代码实现,再配上调试过程里最常见的坑,一次说透。

1. 选题思路与项目定位

1.1 为什么选“文化服饰交易平台”这类题目

毕业设计选题有个潜规则:题目要看起来有“行业价值”,但技术上又必须是你当前水平能完成的。彝绣、苗绣、民族服饰这类文化主题,天然满足这个要求——它自带文化传播的噱头,答辩时你可以讲“用信息化手段助力民族文化产业”,但落到功能上,它本质上就是一个标准电商系统:用户注册登录、商品展示、购物车、订单管理、后台商品维护。换任何一类商品(数码、图书、生鲜)都行,只是商品表里多了“民族分类”这个字段而已。

但千万别把这个“本质”理解成坏事。恰恰是因为电商业务模型成熟,你才能把精力集中在把数据库关系理清、把Servlet控制逻辑写稳、把页面交互做顺畅这些基本功上。技术栈用JSP+Servlet+JavaBean+MySQL+Tomcat,虽然现在听起来老,但这套组合是Java Web最底层的骨架,你把它跑通了,后面理解Spring MVC那套东西会快很多——本质上都是请求进来、控制器分发、模型处理、视图渲染那一套流程。

1.2 项目核心需求拆解

我一般建议做这类题目之前,先画一张“角色-功能”矩阵,把系统的边界定死。这个项目里角色就两类:前台用户和后台管理员。

前台用户能做什么:注册登录、浏览服饰商品列表、按分类(比如彝族服饰下的“日常装”“节日盛装”“银饰配件”)筛选、查看商品详情、加入购物车、提交订单、查看个人订单列表和订单详情、修改个人信息。注意,我特意没加支付功能——毕设项目做到“模拟下单成功”就足够了,接支付宝/微信支付接口反而给自己找麻烦,涉及商户资质、回调逻辑,容易翻车。

后台管理员能做什么:商品管理(添加、上下架、编辑、删除)、商品分类管理、订单管理(查看所有订单、修改订单状态为“已发货”或“已完成”)、用户管理(查看用户列表、禁用账号)。这里也不用做太复杂的权限分级,管理员本身就是最高权限,一个角色到底,别搞RBAC,那是给自己加戏。

1.3 适用人群与难度评估

这个项目适合什么水平的人?我个人判断:Java基础扎实、学过Servlet和JSP基本语法、能写简单SQL的在校学生,正常开发周期大概两到三周(每天三四个小时),加上写论文和做PPT,总共一个月比较从容。

技术难度上,整个项目里最“容易卡壳”的两个点,一是商品图片的上传与回显,二是订单提交时购物车数据的跨页面传递。这两块我会在后面的实现章节单独拿出来细讲。跨域、并发、高可用这些词跟这个项目完全没关系,不用想。

2. 技术选型与整体架构

2.1 技术栈选型的真实理由

先回答一个很多学生纠结的问题:为什么不用SSM(Spring+SpringMVC+MyBatis)?为什么不用Spring Boot?

我的观点很简单:毕设的目的是“证明你掌握了Web开发的底层原理”,而不是“证明你会用现成的框架”。JSP+Servlet意味着所有请求处理、参数封装、数据库连接、事务管理都要你手写,这个过程能帮你把体系结构理解透。等你步入工作后发现,Spring Boot再方便,它底层处理的也是Servlet那一套生命周期、请求转发、session管理。你见过底层长什么样,用框架时才不会觉得那是魔法。

其次,这题“JSP”已经锁定了技术栈方向,别自己偷偷换成Spring Boot,除非你想跟题目对着干。答辩老师看到题目里明确写了JSP,结果你用了Thymeleaf,第一印象就很差。

推荐的环境版本组合:

  • JDK 8(别用11或更高,Tomcat兼容和老教材匹配度都不如8)
  • Apache Tomcat 8.5(稳定,配置Eclipse或IDEA都很顺手)
  • MySQL 5.7(8.0也行,但要注意驱动包用8.x版本)
  • Eclipse IDE for Java EE Developers 或 IntelliJ IDEA(个人推荐IDEA,社区版就行)
  • mysql-connector-java 5.1.49(JDK8配这个驱动不用改URL参数)

2.2 三层架构与MVC划分

整个项目我建议用标准的JSP Model 2架构,也就是MVC模式在Java Web里的经典落法:

  • 视图层(View):JSP页面,负责展示数据和收集用户输入。页面放在webapp目录下,按业务建子目录,比如admin放后台页面,user放前台页面,common放公共组件(如导航栏、页脚)。这里所有人名、图片路径、表单提交地址全是JSP里的常规写法,不要用任何框架标签,避免自找麻烦。
  • 控制层(Controller):Servlet,负责接收请求参数、调用业务层、决定跳转哪个JSP页面。这是真正“干活”的入口,务必把每个Servlet的功能做得单一,可以通过一个统一的路径映射来实现零配置路由,但更建议用@WebServlet注解,省去web.xml里的繁琐配置。
  • 模型层(Model):JavaBean + DAO。实体类对应数据库表字段,DAO类负责封装JDBC操作,Service类负责业务逻辑(比如注册时检查用户名是否重复、下单时校验库存)。

这样的分层有一个直接好处:答辩时老师问“你的项目怎么体现MVC”,你可以直接按着代码目录指给他看。如果将来扩展成Spring Boot项目,每一层的代码几乎可以平移过去,只是把Servlet换成Controller注解而已。

2.3 项目目录结构设计

拿到题目第一步,先把目录建好。这是一个完整的参考结构:

src ├─ dao(数据访问层) │ ├─ impl(接口实现) │ ├─ UserDao.java │ ├─ ProductDao.java │ ├─ CategoryDao.java │ ├─ OrderDao.java │ └─ ... ├─ service(业务逻辑层) │ ├─ UserService.java │ ├─ ProductService.java │ ├─ OrderService.java │ └─ ... ├─ servlet(控制层) │ ├─ user(前台用户相关Servlet) │ ├─ admin(后台管理相关Servlet) │ └─ ... ├─ entity(实体类) │ ├─ User.java │ ├─ Product.java │ ├─ Category.java │ ├─ Order.java │ ├─ OrderItem.java │ └─ CartItem.java ├─ util(工具类) │ ├─ DBUtil.java(数据库连接工具) │ └─ ... webapp ├─ static(css、images、js) ├─ user(前台页面) ├─ admin(后台页面) ├─ common(公共页面) └─ WEB-INF ├─ web.xml └─ lib(放JDBC驱动jar包和JSTL jar包)

这个目录你建完,代码结构问题就解决了一半,后面写代码的过程中只要不刻意乱放,项目天然是清晰的。

3. 数据库设计与核心表结构

3.1 数据表规划原则

数据库设计是整个项目的地基。电商系统最核心的表,我从头到尾数一遍:用户表、商品分类表、商品表、购物车表(可选)、订单表、订单明细表、管理员表。一共就七张,一张不多一张不少。

设计表的时候,牢记三个原则:

第一,每个表必须有自增主键id,int类型,命名就叫id,统一和简洁。

第二,字段的命名风格统一用下划线驼峰。Product表里商品名称叫name,价格叫price,库存叫stock,这些都好办。注意分类和商品的关系是“一对多”,所以在Product表里放category_id作为外键就行,不用建关联表。

第三,金额字段用decimal(10,2),别用float或double。这个坑无数人踩过:浮点数算金额会产生0.30000000000000004这种误差,答辩时被问到“订单金额为什么多了0.01”别提多尴尬。decimal是定点数,能精确到分。

3.2 核心建表SQL详解

直接上能用的建表语句,注意这里有一些设计上的“小心机”:

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(64) NOT NULL COMMENT '密码(md5加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `gender` varchar(10) DEFAULT NULL COMMENT '性别', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `address` varchar(200) DEFAULT NULL COMMENT '收货地址', `avatar` varchar(200) DEFAULT '/static/img/default.png' COMMENT '头像路径', `status` tinyint(4) DEFAULT '1' COMMENT '账号状态 1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

注意几个细节:

  • 表名user是MySQL的保留字,但不加反引号也能用。不过建议,凡是表名接近保留字的,建表语句里都加上反引号最安全。
  • username加了唯一约束,这一步是数据库层面的防重,比业务代码里先查一遍再插入可靠得多。
  • status字段一定加,后台管理要禁用用户时靠它,没有这个字段后台“用户管理”功能会少一半。

再看商品表:

CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) NOT NULL COMMENT '所属分类ID', `name` varchar(100) NOT NULL COMMENT '商品名称', `description` text COMMENT '商品描述', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量', `image` varchar(200) DEFAULT NULL COMMENT '商品图片路径', `status` tinyint(4) DEFAULT '1' COMMENT '状态 1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

商品表里image字段存的是相对路径,比如“/static/img/products/yiqun01.jpg”。这里强烈建议不要把图片存进数据库的BLOB字段,你上传、读取、分页都会变得非常痛苦,毕设阶段把图片保存到服务器目录、数据库存路径,是最实用且业界通用的做法。

订单表的设计需要多讲两句,它是整个系统里表之间关系最复杂的一张:

CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` int(11) NOT NULL COMMENT '下单用户ID', `total_price` decimal(10,2) NOT NULL COMMENT '订单总金额', `receiver_name` varchar(50) NOT NULL COMMENT '收货人姓名', `receiver_phone` varchar(20) NOT NULL COMMENT '收货人电话', `receiver_address` varchar(200) NOT NULL COMMENT '收货地址', `status` tinyint(4) DEFAULT '1' COMMENT '订单状态 1待付款 2已付款 3已发货 4已完成 5已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

这里有个很重要的设计点:收货人信息是“冗余”地存在订单表里的。这是故意为之。订单是历史快照,如果用户后来改了地址,你不能让以前的订单也跟着变。电商系统里这叫“数据不可追溯性”,答辩时如果你主动说出这个设计考量,老师会高看你一眼。

订单明细表则记录每个订单里买了哪些商品、当时什么价格:

CREATE TABLE `order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL COMMENT '订单ID', `product_id` int(11) NOT NULL COMMENT '商品ID', `product_name` varchar(100) NOT NULL COMMENT '商品名称快照', `product_image` varchar(200) DEFAULT NULL COMMENT '商品图片快照', `price` decimal(10,2) NOT NULL COMMENT '成交单价', `quantity` int(11) NOT NULL COMMENT '购买数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

product_name和product_image字段同样是“快照设计”的思路——将来后台把商品改名字了,历史订单明细不能跟着变。这两个字段的精妙之处,很多工作三五年的人都未必能说清楚。

3.3 购物车数据放Session还是放数据库

这是毕设项目里一个经典分歧点。我的建议是:购物车用Session存,不建数据库表。

理由很现实:数据库购物车表需要额外处理“游客购物车合并”“登录前加入购物车,登录后怎么同步”这些复杂逻辑,毕设阶段做这些纯属浪费时间。而Session购物车天然是绑定会话的,用户登录后从中读购物车列表,下单后清空,简单可靠。

对应的CartItem实体不需要建表,它就是内存里的一个Java对象,属性有productId、name、price、image、quantity、totalPrice。

答辩如果被问到“购物车为什么不用数据库”,你可以这样答:购物车是临时性数据,生命周期不超过一次登录会话,用Session可以减少无意义的数据库读写压力。这个回答也符合实际工程里的经验做法——大厂的高并发购物车系统另说,小规模系统Session完全够用。

4. 核心功能模块实现与关键代码

4.1 数据库连接工具类DBUtil

所有DAO操作共用一个连接工具类。这里建议用最简单的JDBC方式,不要引入Druid、C3P0这些连接池——不是说不好,是毕设项目里连接池配置容易出幺蛾子,还说不清楚原理。用一个静态块加载驱动、每次通过DriverManager获取连接,代码量二十行左右,胜在稳定通透:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/yi_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

URL里这几个参数一个都不能少:useUnicode=true&characterEncoding=utf8解决中文乱码,useSSL=false避免MySQL 8的SSL警告刷屏,serverTimezone=Asia/Shanghai解决时区错误。你遇到“The server time zone value”报错时,就知道这几个参数的价值了。

4.2 用户注册与登录实现细节

用户模块是系统的入口,实现上有一个原则:密码不能明文存储。哪怕毕设也建议用MD5加盐处理。这里贴出注册Servlet核心逻辑:

@WebServlet("/user/register") public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String nickname = request.getParameter("nickname"); // 校验用户名是否已存在 UserService userService = new UserService(); if (userService.isUsernameExists(username)) { request.setAttribute("msg", "用户名已存在"); request.getRequestDispatcher("/user/register.jsp").forward(request, response); return; } // 密码MD5加密后入库 String md5Password = MD5Util.md5(password); User user = new User(); user.setUsername(username); user.setPassword(md5Password); user.setNickname(nickname); boolean success = userService.register(user); if (success) { response.sendRedirect(request.getContextPath() + "/user/login.jsp"); } else { request.setAttribute("msg", "注册失败,请重试"); request.getRequestDispatcher("/user/register.jsp").forward(request, response); } } }

两个容易踩坑的地方:

一是密码加密的方法务必用字符串拼接加盐,比如MD5Util.md5(password + “salt_abc”),否则同密码同MD5,彩虹表一查就破。这个细节讲了“懂安全”。

二是注册成功用sendRedirect重定向到登录页,失败用forward转发回注册页并携带msg提示。二者的区别是:重定向是浏览器重新发起请求(地址栏会变),刷新页面不会重复提交表单;转发则是一次请求内的跳转,刷新会重复提交。正确区分这两个方法,答辩必问。

登录逻辑同理,从数据库查出用户后比对MD5值。登录成功的标记用session.setAttribute("loginUser", user),页面通过EL表达式${sessionScope.loginUser.nickname}显示欢迎语。

4.3 商品列表与分类展示

商品展示是用户最能直观感受到的功能。我用一个ProductListServlet来处理:接收页码参数page、每页数量pageSize(默认8)、分类ID参数categoryId,然后调用ProductService进行分页查询。

分页查询的SQL是基本功,必须自己写:

SELECT * FROM product WHERE status = 1 <if test="categoryId != null"> AND category_id = #{categoryId} </if> ORDER BY create_time DESC LIMIT ?, ?

这里“如果分类ID为空则查询全部”是动态SQL的思路,JSP阶段你只能在Servlet里拼接SQL字符串,但逻辑是一样的。

配套还需要一条统计总记录数的SQL:

SELECT COUNT(*) FROM product WHERE status = 1

拿到总记录数后计算总页数:

int totalPage = (int) Math.ceil(totalCount * 1.0 / pageSize);

分页条显示上一页、页码列表、下一页,当前页高亮。我强烈建议自己写这个分页组件,不要引入PageHelper。自己写过一遍,你才知道LIMIT的两个参数分别代表什么,才知道为什么要totalCount先乘1.0再除pageSize(否则整数除法会丢掉小数)。

4.4 购物车的实现策略

购物车我建议直接在Session里存一个List 。

加入购物车的Servlet核心逻辑:

@WebServlet("/cart/add") public class CartAddServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int productId = Integer.parseInt(request.getParameter("id")); // 从Session获取购物车,没有则创建 HttpSession session = request.getSession(); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } // 查询商品信息 ProductService productService = new ProductService(); Product product = productService.getById(productId); // 判断购物车里是否已有该商品:有则数量+1,没有则新建item boolean exists = false; for (CartItem item : cart) { if (item.getProductId() == productId) { item.setQuantity(item.getQuantity() + 1); exists = true; break; } } if (!exists) { CartItem cartItem = new CartItem(); cartItem.setProductId(product.getId()); cartItem.setProductName(product.getName()); cartItem.setPrice(product.getPrice()); cartItem.setImage(product.getImage()); cartItem.setQuantity(1); cart.add(cartItem); } // 回到商品列表页 response.sendRedirect(request.getContextPath() + "/cart/cart.jsp"); } }

这个实现里有几个工程细节值得说:

一是判断“购物车已有该商品”要在添加之前遍历,用exists标志位区分“新增”还是“累加”,写错的话会出现同一商品多个条目。

二是CartItem保存了productName和price的“运行期快照”,这样购物车页面不需要再查数据库,减少DB压力。这个思想和小节3.2里订单表的快照设计是一脉相承的。

三是加入购物车后直接重定向回购物车页面,避免用户按F5时重复添加。

购物车页面的总价计算,用一个循环累加getTotalPrice(),显示在页面底部。下单时把购物车列表转成订单和订单明细,然后清空session中的cart。

4.5 商品图片上传与回显

图片上传是JSP项目里最容易卡壳的环节,因为原生Servlet 3.0之前处理multipart/form-data格式很麻烦。好在Servlet 3.0开始自带Part接口,配置一下就能用。

先给web.xml配置上传相关参数:

<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping> <multipart-config> <location>/tmp</location> <max-file-size>5242880</max-file-size> <max-request-size>20971520</max-request-size> <file-size-threshold>0</file-size-threshold> </multipart-config>

注意,静态资源放行是必须的,tomcat默认会把多数静态资源交给default servlet,如果你的图片放在/static/images下访问不到,多半是web.xml把default映射给覆盖了。

后台添加商品的Servlet中接收文件:

Part part = request.getPart("image"); String fileName = part.getSubmittedFileName(); // 重命名文件,避免中文名和重名问题 String newFileName = UUID.randomUUID().toString().replace("-", "") + fileName.substring(fileName.lastIndexOf(".")); String saveDir = getServletContext().getRealPath("/static/images/products"); File file = new File(saveDir, newFileName); part.write(file.getAbsolutePath()); product.setImage("/static/images/products/" + newFileName);

这里UUID重命名有两个用意:一是避免用户上传的同名文件互相覆盖,二是避免中文文件名导致Tomcat返回URL中文路径乱码。如果你不重命名,就会发现第一次上传“彝族上衣.jpg”成功,第二次再上传同名的直接把第一个覆盖了。

回显时,商品列表页直接写:

<img src="${product.image}" alt="${product.name}">

因为image字段存的是从项目根路径开始的完整URL,浏览器会自动拼接域名上下文。如果图片展示不出来,八成是保存路径和数据库路径不一致,检查getRealPath返回的物理路径是否真的存在这个文件。

4.6 订单提交的事务处理

订单提交是整个项目里唯一涉及多表写入的操作:往orders表插一条主记录,再往order_item表循环插入多条明细,同时要减少各商品的库存。这三个操作要么全部成功,要么全部失败,必须用事务。这是数据库事务的经典应用场景。

public boolean createOrder(Order order, List<CartItem> cartLiist) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主表,生成订单ID OrderDao orderDao = new OrderDao(); int orderId = orderDao.insert(conn, order); // 2. 循环插入订单明细 OrderItemDao orderItemDao = new OrderItemDao(); for (CartItem item : cartList) { orderItemDao.insert(conn, orderId, item); } // 3. 扣减库存 ProductDao productDao = new ProductDao(); for (CartItem item : cartList) { productDao.decreaseStock(conn, item.getProductId(), item.getQuantity()); } conn.commit(); return true; } catch (Exception e) { if (conn != null) { conn.rollback(); // 失败回滚 } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } DBUtil.close(conn, null, null); } } }

注意这几个点:

一是conn.setAutoCommit(false)必须在拿到连接后立刻设置,这是开启事务的标志。

二是DAO方法里不能自己关连接,连接必须由service层统一管理。所以这里的DAO方法都显式接收conn参数。很多学生第一次写事务失败,就是因为DAO内部用了DBUtil.getConnection()各自拿各自的连接,导致事务根本不在同一个连接上——那Commit和Rollback都管不到别的连接的操作。

三是finally块里恢复autoCommit为true再关闭,否则连接归还连接池时残留事务状态,下一个请求会被影响。我们在调试时就用到了这个点,你的代码里必须有。

订单号生成我建议用时间戳+随机数:

String orderNo = System.currentTimeMillis() + "" + (int)((Math.random()*9+1)*100000);

这个方案足够保证单机下的唯一性。加了唯一索引uk_order_no后,万一重复会抛异常,到时候重试一次即可。

4.7 登录验证过滤器

页面访问控制是后台管理的基本要求。如果用户没登录就能直接访问/admin/index.jsp,那后台等于裸奔。用过滤器统一拦截:

@WebFilter("/admin/*") public class AdminAuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(); Object admin = session.getAttribute("admin"); if (admin == null) { // 未登录,重定向到后台登录页 response.sendRedirect(request.getContextPath() + "/admin/login.jsp"); return; } chain.doFilter(req, resp); } }

前台用户访问个人中心也应该有类似的用户过滤器,拦截/user/*下的页面,只是判断的是session里的loginUser。记得放行登录、注册、商品列表这些公开页面,不要一刀切全拦——那会导致没登录时连商品页都打不开,答辩演示时就是大型翻车现场。

4.8 后台管理模块的实现要点

后台管理功能比前台简单,基本都是CRUD。

商品管理页面建议做成分页表格,每一行有“上架/下架”“编辑”“删除”按钮。其中删除要加上确认弹窗,可以用JS最简单的confirm():

<a href="#" onclick="return confirm('确定删除该商品吗?') ? location.href='${ctx}/admin/product/delete?id=${p.id}' : false;">删除</a>

上架/下架这个功能为什么单独说?因为它是商品表中status字段的更新操作。很多学生删除商品时把记录物理删掉,导致历史订单里关联的商品ID找不到对应记录,订单明细里的商品名称快照还能显示,但如果回显图片就会裂图。所以我的建议是:商品“删除”其实执行的是status=0(下架),而不是DELETE FROM。这是软删除的思想,在答辩时如果主动讲出来,又是一个加分点。

订单管理页面核心操作是修改状态。用一个下拉框选择状态,提交后更新orders表的status字段。这里注意订单状态流转最好有约束:已取消的订单不能改成已付款,已完成的订单不能倒退。最简单的做法是在Service层做状态校验:

public boolean updateStatus(int orderId, int newStatus) { Order order = getById(orderId); if (order == null) return false; // 简单校验:只能按 待付款->已付款->已发货->已完成 单向流转 if (newStatus != order.getStatus() + 1 && newStatus != 5) { return false; } return orderDao.updateStatus(orderId, newStatus); }

这里的逻辑是“状态只能前进一位”,允许5(取消)作为特例。答辩时可以讲讲“状态机”这个词,会让老师觉得你考虑问题有体系。

5. 前端页面设计与用户体验细节

5.1 整体风格设计

民族文化服饰交易平台,页面风格一定不能是那种赤裸裸的“后台管理系统蓝白风格”。我建议在前台页面上下点功夫:整体配色采用彝族的经典色——红、黑、黄,这是彝族服饰中最常见的主色调。首页顶部导航栏用深黑色底,配上红色和黄色的装饰线条,商品卡片的边框用暖黄色,按钮用正红色。

当然你不可能自己在CSS里从零画民族纹样,我的做法是去一些免费素材网站找“彝族纹样”相关的背景矢量图,作为首页横幅和页脚装饰。或者用CSS渐变+线性纹理模拟出粗犷的图腾感。

细节上,商品卡片上可以加一个“民族元素”的小标签标识,比如“彝绣”“银饰”“传统纹样”等。这个在数据模型里不一定要另建字段,后台商品编辑页面加一个“文化标签”的文本框存逗号分隔的字符串即可。产品列表页解析后用CSS渲染成彩色小徽章,视觉上文化感立刻拉满。

5.2 页面组件复用

JSP最实用的复用手段是include指令。导航栏、页脚、头部信息这些每个页面都有的内容,提取成公共JSP片段:

<%@ include file="/common/header.jsp" %>

这里用的是静态include(<%@ include %>),它把文件内容原样嵌入当前页面,属于编译时合并。还有一种方式是<jsp:include page="">,运行时动态包含。对公共导航这种几乎不变的内容,静态include效率更高。要注意的是,被包含的公共页面里不能有重复的HTML结构标签(比如

、),否则页面结构会乱。

菜单高亮怎么处理?在公共导航里用EL判断当前请求URI:

<a class="${pageContext.request.requestURI == '/user/product_list.jsp' ? 'active' : ''}" href="${ctx}/product/list">服饰商城</a>

注意ctx是你在pageContext里设置的上下文路径变量:<c:set var="ctx" value="${pageContext.request.contextPath}"/>。所有URL地址前都要加${ctx},否则部署到非根路径的项目下,所有链接都是404。这个细节也是新手重灾区。

5.3 表单验证与提示交互

表单验证分前端JS验证和后端验证两层。前端验证做即时反馈,比如用户名长度、手机号格式、两次密码是否一致。用HTML5自带的required、pattern属性加少量JS就能实现,不必引入jQuery Validate这种大块头。

后端验证才是真正的安全屏障。注意,任何前端验证都可以被绕过,所以Servlet处理注册、登录、提交订单时都要再验证一次参数合法性。比如用户名不能为空、密码长度不低于6位、商品数量不能为负数,这些是硬校验。

操作提示我用request域放msg的方式,在JSP页面用EL回显:

<c:if test="${not empty msg}"> <div class="alert alert-danger">${msg}</div> </c:if>

做编辑页面时,表单里的回显值要注意用value="${product.name}",而不是硬编码死值。编辑完提交后,如果校验失败转发回编辑页,也要带上之前用户填写的值(在Servlet里重新封装进request),否则每次报错后表单被清空,体验极差。

5.4 商品详情的文化展示

商品详情页是能多出彩的地方。除了常规的商品图、价格、库存、数量选择、立即购买/加入购物车按钮,我建议增加一个“文化背景”栏。这不需要单独的数据库表,在商品描述(description)字段里,让后台管理员录入时用简单的分段格式,包含“产品故事”“工艺特点”“穿着场景”几段文本。页面用一个Tab切换或手风琴组件来展示。

数量选择器用

  • 下拉,选项从1到当前库存数量(如果库存超过10则只显示到10)。库存为0时按钮置灰并显示“已售罄”,这里需要在Servlet里判断:
  • <c:choose> <c:when test="${product.stock > 0}"> <button>加入购物车</button> </c:when> <c:otherwise> <button disabled>暂时缺货</button> </c:otherwise> </c:choose>

    “立即购买”这个按钮要不要做?我的建议是不做。因为它要绕过购物车直接生成订单,逻辑上中断了购物车的统一流程,会增加代码的复杂度和出bug概率。如果你非要做,它的实现本质就是“把商品作为唯一一项加入购物车,然后跳转到结算页”,仅此而已。

    6. 典型问题排查与避坑实录

    6.1 JSP页面顶部一大片报错信息

    这个几乎是所有JSP新手都会遇到:页面能显示,但顶部有一堆英文异常堆栈。原因通常是页面里用到了JSTL标签,但webapp/WEB-INF/lib下没放jstl.jar和standard.jar。解决办法很简单,下载对应的两个jar包放进去,或者在Maven里加:

    <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>

    另外注意JSP页面的taglib声明要写在第一行:

    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

    6.2 中文乱码问题的三种情况

    乱码问题分三个层面,你要分清楚是哪一种:

    • JSP页面显示乱码:在JSP第一行设置<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>。这里pageEncoding和contentType的charset都要设,缺一个都可能乱。
    • 表单提交到Servlet后乱码:在doPost方法第一行加request.setCharacterEncoding("UTF-8");如果是GET请求带参数,还得在Tomcat的server.xml的Connector标签里加URIEncoding="UTF-8"。不过用POST提交表单是标准做法,我建议所有写操作都用POST。
    • 数据库存储乱码:建库时指定utf8mb4:
    CREATE DATABASE yi_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

    同时JDBC URL里带上characterEncoding=utf8。三者全做对,项目里基本见不到乱码。

    6.3 明明登录了,页面还是跳回登录页

    这个问题十有八九是session域和request域搞混了。登录时你把用户对象放进了request.setAttribute("loginUser"),但过滤器检查的是session.getAttribute("loginUser"),当然为null。记住:跨请求传递必须放session,request是一个请求内共享的。另外注意,如果你用了重定向response.sendRedirect,request里的属性全部丢失,这也是经典原因之一。

    6.4 商品图片上传后页面显示裂图

    排查步骤按这个顺序走:

    1. 确认文件是否真的保存成功——去Tomcat的webapps/项目名/static/images/products目录下看有没有那个文件。
    2. 确认数据库image字段值是否正确——根路径应该以/开头,如/static/images/products/xxx.jpg。
    3. 在浏览器直接访问图片URL看是否404。404的话检查图片存放目录和URL路径是否一致,特别是getRealPath返回的目录结构,lenovo的Eclipse部署路径和实际项目路径不同是常事。
    4. 如果访问出来是乱码或打不开,检查文件名是否含中文或空格,用UUID重命名后一般都能解决。

    6.5 Tomcat启动报端口被占用

    Eclipse或IDEA里重复启动导致Tomcat没完全关闭,端口被占用。Windows命令行执行:

    netstat -ano | findstr 8080

    查到占用端口的PID后:

    taskkill /pid PID /F

    如果是自己开发环境,建议直接把Tomcat的端口改成8081、8082这种少见端口,一劳永逸。改端口在server.xml里:

    <Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/>

    6.6 分页的页数点击后还是第一页

    分页链接里没有正确传递page参数。检查你的分页条代码:

    <a href="${ctx}/product/list?page=${p.pageNum-1}&categoryId=${param.categoryId}">上一页</a>

    注意两个坑:第一页时上一页要禁用,否则page=0会查空;翻页时如果之前选过分类,分类参数必须拼接上,否则翻页就丢失筛选条件。我在项目里就是把这两个参数组合成字符串,分页条循环生成时一次拼接到位。

    6.7 购物车数量减到负数

    用户在购物车页面手动修改数量输入框,输了个-3。你的updateServlet得做校验:

    int quantity = Integer.parseInt(request.getParameter("quantity")); if (quantity <= 0) { quantity = 1; }

    更进一步,数量上限不能超过库存,否则下单时库存扣减会变成负数。最简单粗暴的做法是给product表的stock字段加无符号约束:

    ALTER TABLE product MODIFY stock int(11) UNSIGNED NOT NULL DEFAULT 0;

    这样即便代码漏了校验,数据库也会报错而不是产生负数库存——数据库层兜底,是安全设计里很靠谱的思路。

    7. 项目答辩要点与扩展方向

    7.1 答辩时主动讲解的五个加分点

    根据我做项目总结的经验,答辩就是把你项目里“比别人多想了半步”的地方讲出来。这个项目我认为有五个值得主动展开的点:

    一是订单表和订单明细表的“快照设计”。老师问“为什么不直接关联商品表”,你回答“商品可能被修改或删除,但历史订单必须保持当时购买的商品名称和价格,这是电商系统的溯源需求”。这一句话直接证明你有工程意识。

    二是商品“删除”用软删除(status=0)而不是物理删除。可以顺带回答“保证历史订单的完整性”。

    三是事务处理时连接由Service层统一管理。这是底层原理问题,你能解释为什么DAO要传连接参数,说明你真正写过多表操作。

    四是购物车放Session的取舍。重点讲清临时数据与持久数据的分界。

    五是密码MD5加盐存储。哪怕只是简单加固定盐,也说明你有安全意识。

    7.2 项目后续扩展方向

    如果你精力充足,想把项目作为一个亮点写进简历,三个方向可以延伸:

    第一个方向是前后端分离改造。保留业务逻辑不变,将JSP替换成HTML静态页+AJAX调用JSON接口。Servlet通过写JSON字符串响应,前端用原生fetch获取数据渲染。做完这个改造,你就等于接触了当前业界主流的开发模式,和工作的距离拉近了一大截。

    第二个方向是引入Maven管理依赖。把Eclipse里手拖jar包的方式换成pom.xml声明依赖,并顺手把Druid连接池集成进来。这能精确命中“懂构建工具、懂连接池”两个加分点。

    第三个方向是增加简单的数据可视化后台。比如用ECharts在管理员首页展示“近七日订单数量柱状图”“商品分类占比饼图”。ECharts引入极简,写起来也不复杂,但展示效果一下子就把项目档次拉高了。

    7.3 关于这个项目的一些实在话

    最后说点不那么“技术”但很实在的体会。彝族文化服饰交易平台这个题目,本质上是一个“行业壳+电商核”的组合。它的意义不在于技术难度有多高,而在于你通过它完整走了一遍Web项目的生命周期:选题、建库、编码、调试、答辩。

    我见过不少同学纠结“JSP是不是已经过时了”,纠结半天最后把项目改得四不像。这里我的建议很明确:毕设的首要目标是完整交付。你用JSP把系统做扎实了,把每一个请求从浏览器到Servlet到DAO到数据库再回显到页面的完整链路讲清楚,这个项目的价值就已经实现了。

    我自己的体验是,这种“老技术栈”项目反而能逼着你去触碰真正底层的细节:JDBC连接怎么管理、事务边界怎么划、状态字段怎么设计、Session生命周期怎么跟踪。这些东西在未来任何技术栈里都是通用的。等到你工作了再去理解“原来Spring的事务管理器就是在帮我自动做commit和rollback”,你会感谢当年写JSP时踩过的那些坑。

    另外一个建议:项目不要等到最后一个月才动手。数据库表设计完先跑通注册登录,再做商品展示,再逐步加上购物车和订单,最后回头美化页面。每一步都能独立运行,积累起来就是一个五脏俱全的系统。中途遇到问题优先自己打断点调试、看日志,实在不行带着排查过程的截图去问人——带着思考去提问,别人才愿意帮你。

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

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

    立即咨询