JSP商品评价系统实战:从SmartUpload到事务处理的完整解析
简介一份基于JSPServlet的商品管理与评价系统完整项目面向Java Web初学者与课程设计人群适合用来理解商品增删改查、用户评分、登录验证等典型电商功能。压缩包共275个文件约1.39MB核心代码由58个Java源文件与对应class文件构成另有38个JSP页面负责动态展示76个GIF、31个JPG及6个BMP作为界面资源并包含SmartUpload等JAR依赖组件整体结构完整便于直接导入开发环境运行调试。项目覆盖商品管理模块与评价系统模块管理员可添加、编辑或删除商品信息浏览者登录后可对商品评分并发表评论实现平均分更新与评价列表展示后端通过DAO与JavaBean封装数据库操作会话控制与权限校验保障数据安全并整体采用MVC思想分层设计适合全面练习JSP核心语法、JDBC交互及综合项目架构。目前已有131人学习下载对于需要完成课程设计、毕业设计或想从零搭建JSP电商功能的开发者是一份结构清晰、可直接二次开发的实用参考。1. 一个 JSP 商品评价系统拆开看远比你想象的耐玩拿到这份star.zip资源里面既有SmartUpload.class这种老牌文件上传组件也有ComDAO.class、crDAO.class这类数据访问层的编译产物配合guanyu.bmp、smyi.bmp等商品图片基本能拼出一个完整的 JSP 商品管理与评价系统。这类项目放在十年前是电商实训课的标准作业放在今天依然有拆解价值它的登录会话管理、文件上传、评论聚合逻辑正好对应着现代 Java Web 项目中最容易被忽略的三个瓶颈点——并发请求下的session同步、上传文件与业务表的事务边界、评分计算时的读写一致性。本文不打算复述 JSP 基础语法而是直接以这套资源为蓝本讲清楚管理员商品管理、浏览者评分、文件上传三条链路各自该怎么实现以及当你需要把它改造成 Servlet MyBatis 或 Spring Boot 结构时哪些代码可以直接迁移哪些逻辑需要推倒重写。适合正在做课程设计的学生、维护老系统的运维工程师以及想从 JSP 过渡到主流框架的初中级开发者。2. 商品管理模块从 SmartUpload 到 DAO 的数据链路2.1 老项目中为什么离不开 SmartUploadSmartUpload.class在资源里出现说明商品管理模块大概率支持图片上传。SmartUpload 是 JSP 时代最常见的文件上传组件它和后来的 Commons FileUpload 最大的区别在于SmartUpload 可以直接把请求中的文件字段解析为内存中的com.jspsmart.upload.File对象并自动处理表单中其他普通字段。在那个 Servlet 3.0 还没普及的年代这几乎是 JSP 里唯一不需要写大段request.getInputStream()解析逻辑的方案。但注意SmartUpload 有几个天然缺陷它对上传大小没有默认限制需要手动调用setMaxFileSize它在解析时会把整个请求体读入内存大文件场景下容易 OOM。所以现在改造老项目我一般会建议替换为 Commons FileUpload 或直接走 Servlet 3.0 的MultipartConfig但如果你只是维护现有代码理解它的调用方式依然必要。2.2 商品新增与图片上传的 JSP 表单设计商品管理页面的核心表单结构一般长这样form actionadmin/ProductAdd.jsp methodpost enctypemultipart/form-data input typetext namepname placeholder商品名称 / input typetext nameprice placeholder价格 / input typetext namestock placeholder库存 / textarea namedescription rows3 placeholder商品描述/textarea input typefile namepic acceptimage/bmp,image/jpeg / button typesubmit提交/button /form这里的关键在于enctypemultipart/form-data。如果不设置这个属性request.getParameter(pname)还能拿到值但file字段只会传文件名文件内容全部丢失。而在 JSP 里处理这个请求传统做法是% page importcom.jspsmart.upload.* % % SmartUpload su new SmartUpload(); su.initialize(pageContext); su.setMaxFileSize(2 * 1024 * 1024); // 限制图片最大 2MB su.upload(); // 解析请求 File uploadFile su.getFiles().getFile(0); String fileName System.currentTimeMillis() _ uploadFile.getFileName(); uploadFile.saveAs(/upload/ fileName, SmartUpload.SAVE_VIRTUAL); String pname su.getRequest().getParameter(pname); double price Double.parseDouble(su.getRequest().getParameter(price)); int stock Integer.parseInt(su.getRequest().getParameter(stock)); String description su.getRequest().getParameter(description); %这段代码里有几个容易踩坑的参数su.initialize(pageContext)必须写在任何response.getWriter()输出之前否则会报IllegalStateExceptionsaveAs的第二个参数SmartUpload.SAVE_VIRTUAL表示相对于 Web 应用根目录的路径如果写成SAVE_PHYSICAL则必须传绝对路径。文件名用时间戳加原名拼接可以避免多用户同时上传同名文件相互覆盖。2.3 ComDAO 的增删改查与参数绑定ComDAO.class在资源包里出现通常封装了商品表的通用操作。在 JSP 页面中直接调用 DAO 的做法在课程设计里很常见代码一般如下public class ComDAO { private Connection conn; public boolean addProduct(Product p) { String sql INSERT INTO product(name, description, price, stock, pic) VALUES(?,?,?,?,?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getName()); ps.setString(2, p.getDescription()); ps.setDouble(3, p.getPrice()); ps.setInt(4, p.getStock()); ps.setString(5, p.getPic()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }注意这里的PreparedStatement参数占位符是?不是 JSP 页面里的${}。很多新手会把 JSP 表达式语言和 SQL 参数混用导致 SQL 注入或类型转换异常。另外conn连接资源一定要在finally或 try-with-resources 中关闭否则在高并发下 MySQL 会抛出Too many connections。2.4 商品列表的分页与条件筛选商品列表页不能一次性查出所有记录否则数据库压力太大。常见做法是用LIMIT offset, size配合 JSP 页面传参SELECT * FROM product WHERE name LIKE CONCAT(%, ?, %) ORDER BY add_time DESC LIMIT ?, ?offset从(currentPage - 1) * pageSize计算得到pageSize固定为 10 或 20。在 JSP 里写分页标签时注意PreparedStatement的setInt方法不能直接传入字符串页码必须先用Integer.parseInt转换并做非空判断否则空指针异常会直接打断整个页面渲染。3. 评价系统评分聚合、评论列表与事务保障3.1 评分字段设计不要只存平均分浏览者评分是这个系统里最容易设计出错的地方。很多初学 JSP 的人会在商品表里加一个avg_score字段每次用户打分就对它做一次加权平均。这个方案在低并发下看似可行但一旦两个用户同时打分后写的数据会覆盖先写的结果最终平均分完全失真。正确的做法是设计独立的评价表CREATE TABLE product_rating ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, user_id INT NOT NULL, score TINYINT NOT NULL CHECK (score BETWEEN 1 AND 5), comment VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_product_user (product_id, user_id) );UNIQUE KEY uk_product_user的作用是约束同一个用户对同一商品只能评价一次。如果要允许重复评价就去掉这个唯一键但那样的话“平均分”的计算逻辑就必须包含所有历史记录不能只存最后一次。3.2 评分提交与平均分更新的 JSP 处理评价提交页面的核心逻辑分三步接收表单数据、写入评价记录、重新计算该商品的平均分。JSP 页面里直接写 Java 代码实现如下% int productId Integer.parseInt(request.getParameter(productId)); int userId Integer.parseInt(session.getAttribute(userId).toString()); int score Integer.parseInt(request.getParameter(score)); String comment request.getParameter(comment); Connection conn (Connection) application.getAttribute(conn); conn.setAutoCommit(false); try { PreparedStatement psInsert conn.prepareStatement( INSERT INTO product_rating(product_id, user_id, score, comment) VALUES(?,?,?,?)); psInsert.setInt(1, productId); psInsert.setInt(2, userId); psInsert.setInt(3, score); psInsert.setString(4, comment); psInsert.executeUpdate(); PreparedStatement psUpdate conn.prepareStatement( UPDATE product SET avg_score (SELECT ROUND(AVG(score),1) FROM product_rating WHERE product_id ?) WHERE id ?); psUpdate.setInt(1, productId); psUpdate.setInt(2, productId); psUpdate.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); response.sendRedirect(error.jsp); } finally { conn.setAutoCommit(true); } %这段代码中setAutoCommit(false)和commit()/rollback()构成了一个完整的事务边界。之所以必须用事务是因为插入评价和更新平均分是两步操作如果第一步成功了第二步失败就会造成评价存在但平均分没更新的脏数据。另外注意AVG(score)的子查询里product_id出现了两次前一个是子查询的过滤条件后一个是主表更新的定位条件不要混淆。3.3 评论列表的排序与展示优化评论列表页通常按时间倒序展示并且要关联用户表显示评论者昵称。一个常见的优化是通过JOIN一次查出所需字段而不是在 JSP 里循环嵌套查询SELECT r.score, r.comment, r.create_time, u.nickname FROM product_rating r LEFT JOIN user u ON r.user_id u.id WHERE r.product_id ? ORDER BY r.create_time DESC LIMIT 0, 20如果评价数量很大可以进一步做分页。这里还有一个细节score在前端展示为星星时建议后端直接返回整型数值不要在 DAO 层做格式化让 JSP 的c:forEach循环生成对应个数的星星图标这样比返回拼接好的 HTML 字符串更清晰也更容易实现半星效果。4. 用户登录验证与权限控制的 JSP 实现4.1 session 与 cookie 的身份保持机制系统里有管理员和普通浏览者两类角色权限控制的关键在 JSP 的session对象。登录成功后一般会在后台写入session.setAttribute(userRole, admin); session.setAttribute(userId, 1); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作失效setMaxInactiveInterval是容易忽略的参数。如果不设置Tomcat 默认 session 超时时间是 30 分钟但如果你在web.xml里覆盖了全局超时配置这里就必须显式设置否则用户体验会很差——好不容易填完商品信息提交时发现 session 过期了。管理员页面的入口处需要做权限校验常见做法是在每个管理 JSP 页面顶部加一段% Object role session.getAttribute(userRole); if (role null || !admin.equals(role)) { response.sendRedirect(login.jsp); return; } %这里注意两点第一return不能少否则页面会继续渲染第二字符串比较要用admin.equals(role)而不是role.equals(admin)因为role可能为 null反向调用会抛空指针。4.2 防止恶意刷分的会话校验逻辑评价系统的浏览者评分必须校验用户身份否则任何人都可以伪造请求直接 POST 到rating.jsp。在提交评分的处理逻辑里除了检查session中是否存在userId还要校验该用户是否真的购买过该商品。虽然课程设计里往往没有订单表但至少要做一层简单的 token 校验% String expectedToken (String) session.getAttribute(ratingToken_ productId); String actualToken request.getParameter(token); if (expectedToken null || !expectedToken.equals(actualToken)) { response.sendRedirect(error.jsp?msginvalid_token); return; } session.removeAttribute(ratingToken_ productId); %生成 token 的地方是在商品详情页渲染时% String token UUID.randomUUID().toString(); session.setAttribute(ratingToken_ productId, token); % input typehidden nametoken value% token % /这个做法的逻辑是每次打开商品详情页生成一次性 token提交评分时必须携带该 token提交后立刻从 session 中删除。这样能挡住一部分简单的重复请求刷分尝试至少比完全不设防要正规得多。当然真正的防刷要配合验证码和服务端 IP 限制这里只是课程设计层面的合规做法。5. 把 JSP 项目移植到 Servlet JSTL 时怎么改5.1 页面中的 Java 脚本片段迁移方向这套资源里的 JSP 页面普遍存在% %脚本片段这在维护时非常痛苦——前端工程师和 Java 代码耦合在一起改个页面样式都可能产生NullPointerException。移植到标准 MVC 结构时我一般会把页面里的数据处理逻辑拆到独立的 Servlet 控制器中JSP 只保留展示逻辑。以商品列表为例原本 JSP 里直接写 JDBC 查询的代码迁移后变成WebServlet(/product/list) public class ProductListServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page req.getParameter(page) null ? 1 : Integer.parseInt(req.getParameter(page)); int pageSize 10; ListProduct list productDao.findPage(page, pageSize); req.setAttribute(productList, list); req.setAttribute(currentPage, page); req.getRequestDispatcher(/WEB-INF/jsp/product_list.jsp).forward(req, resp); } }JSP 端则改成 JSTL 标签遍历c:forEach varp items${productList} tr td${p.name}/td td${p.price}/td td${p.stock}/td td c:forEach begin1 end${p.avgScore}★/c:forEach /td /tr /c:forEach注意avgScore是 Double 类型c:forEach的end属性只能接收整型所以需要在 getter 里做一次intValue()或者用${p.avgScore.intValue()}显式转换否则会抛NumberFormatException。5.2 文件上传组件替换为 Commons FileUploadSmartUpload 在迁移时建议直接淘汰改用 Commons FileUpload 的核心原因有两个一是它不需要依赖pageContext可以在 Servlet 环境里自由使用二是它支持流式解析适合大文件场景。同样的商品图片上传在 Servlet 中写法为DiskFileItemFactory factory new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(2 * 1024 * 1024); ListFileItem items upload.parseRequest(req); for (FileItem item : items) { if (!item.isFormField()) { String fileName new File(item.getName()).getName(); String savePath getServletContext().getRealPath(/upload) File.separator System.currentTimeMillis() _ fileName; item.write(new File(savePath)); } else { String fieldName item.getFieldName(); String fieldValue item.getString(UTF-8); } }这里setFileSizeMax限制的是单个文件大小setSizeThreshold是内存缓冲阈值超过该阈值的文件会写入临时目录避免内存溢出。item.getString(UTF-8)指定了表单字段的编码格式否则中文商品名会出现乱码。与 SmartUpload 相比这个方案更可控而且可以在循环中分别处理普通字段和文件字段不需要再做额外的请求解析。5.3 评价事务迁移到 Spring 的事务管理原项目通过conn.setAutoCommit(false)手动控制事务迁移到 Spring 后可以直接声明式事务管理。原来那段插入评价并更新平均分的代码可以封装为一个 service 方法Transactional(rollbackFor Exception.class) public void addRating(Rating rating) { ratingDao.insert(rating); productDao.updateAverageScore(rating.getProductId()); }Transactional注解的默认回滚条件是RuntimeException这里显式指定rollbackFor Exception.class是为了让SQLException等受检异常也能触发回滚。原本 JSP 里手动管理的conn不再需要担心关闭和并发问题Spring 会在方法执行结束后自动提交或回滚。这个改动也是整套资源里价值最高的一次升级——它把耦合在页面里的事务逻辑彻底剥离后续无论是换数据库还是加 Redis 缓存改动面都会小很多。本文还有配套的精品资源点击获取