ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java鲜花商城系统开发实战:JSP+MySQL+B/S架构全解析

Java鲜花商城系统开发实战:JSP+MySQL+B/S架构全解析 简介一份基于Java Web的鲜花商城系统设计与实现文档面向正在做毕业设计或课程设计的计算机专业学生以及希望学习B/S架构商城开发流程的Java初学者。文档围绕传统鲜花商城人工管理模式效率低下的问题完整介绍了采用Java、JSP、MySQL和Tomcat构建系统的全过程。资源为1个docx文件大小2.75MB内容涵盖需求分析、可行性分析、系统功能设计、数据库表结构设计及功能测试等核心环节并展示了主要功能模块的设计界面和操作界面。其中商品浏览、购物车、在线支付、订单处理、用户管理、评论互动等模块均有说明可帮助读者理清从环境搭建到系统部署的思路。目前已有73人学习下载适合作为同类课题设计、论文写作或项目演示的参考模板。1. 为什么是这套 Java 鲜花商城系统它能解决的不只是毕设查重如果你搜“java 鲜花商城系统”大概率是在找一份能直接落地的课程设计或毕业设计蓝本。这套系统的价值不在“鲜花”这个业务本身而在于它把 Java Web 最常见的功能面都覆盖了一遍注册登录、商品管理、订单流转、收藏留言、后台数据维护配上 JSP 动态页面和 MySQL 数据表是典型的 B/S 结构教学样本。我拆这份文档时最直接的感受是它不搞花活就是老老实实按需求分析 → 可行性分析 → 数据库设计 → 模块实现 → 系统测试的流程走。对于正在做毕设、或者想快速搭一个 Java Web 项目练手的人来说这份资源能让你少走很多弯路——你不需要从零设计表结构不需要反复纠结 JSP 页面怎么写甚至不需要头疼测试用例从哪来。适合三类人一是准备毕业设计的本科生二是想系统过一遍 Java Web 全流程的初学者三是需要快速交付演示项目的从业者。2. 技术栈选型为什么是 B/S JSP MySQL而不是前后端分离2.1 B/S 结构省掉客户端浏览器即入口这套系统选了 B/S 架构而不是 C/S。C/S 模式下用户必须下载安装客户端程序对鲜花商城这种面向零散用户的场景来说安装成本会直接劝退一大批人。B/S 模式把业务逻辑和数据库交互放在服务端用户只需要打开浏览器输入地址就能访问。部署方也省事——只要维护一台 Tomcat 服务器更新版本时替换 Web 应用目录下的文件即可客户端零维护。B/S 还有一个隐性的好处开发调试的反馈链路更短。你在 IDEA 里改完 JSP 页面重新部署到 Tomcat刷新浏览器就能看到效果。对比 C/S 的客户端和服务端联调这种模式对单人开发、短周期交付的项目非常友好。2.2 Java JSP MySQL 的组合逻辑为什么这套系统选 Java 而不是 PHP 或 ASP核心原因是 Java 的跨平台能力和对象导向特性。系统在 Windows 上开发最终部署到 Linux 服务器代码不需要重写。Java 自带的垃圾回收机制解决了内存管理问题开发人员不用像写 C 语言那样手动释放内存这对学生项目和中小型商业项目都是加分项。JSP 的角色是动态页面渲染。它的本质是服务端模板JSP 文件在第一次被访问时由容器解析成 Servlet然后动态生成 HTML 响应给浏览器。所以你在 JSP 里可以写 HTML 标签也可以嵌入 Java 代码片段还可以通过 JSTL 或 EL 表达式简化页面的数据展示。对于这个鲜花商城JSP 承担了两种职责一是把数据库里的鲜花列表、订单状态渲染成用户能看的页面二是接收表单提交参数并转发给后端处理。MySQL 则是数据层的底座。选它不是因为性能有多极致而是因为三个现实原因开源免费、占用资源少、中小型网站的并发压力完全够用。这套系统的数据量级就是几千条商品记录加几千个用户MySQL 的单表查询和简单联表查询在这个量级下没有任何性能瓶颈。配合 Navicat 或 DataGrip 做可视化操作建表、导数据、看查询结果都非常直观。2.3 工程结构一份能直接导入 IDEA 的目录规划拿到这份文档后我建议你先重建目录结构再对照文档里的功能模块去填代码。典型的 JSP 项目结构如下flower-mall/ ├── src/ │ ├── com.flower.dao/ # 数据访问层 JDBC 操作封装 │ ├── com.flower.entity/ # 实体类 User、 Flower、 Orders 等 │ ├── com.flower.service/ # 业务逻辑层 订单、 库存、 用户校验 │ ├── com.flower.servlet/ # 控制层 接收请求、 调用业务、 转发页面 │ └── com.flower.util/ # 工具类 DBUtil、 分页工具、 字符编码过滤器 ├── web/ │ ├── admin/ # 后台管理页面 JSP 文件 │ ├── front/ # 前台用户页面 商品列表、 购物车、 订单 │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、 过滤器配置 │ │ └── lib/ # JDBC 驱动、 JSTL 标签库 │ ├── index.jsp # 系统入口 │ └── static/ # CSS、 JS、 图片 └── sql/ └── flower_mall.sql # 建库建表语句和初始数据这里要特别强调 web.xml 的作用。它是 Servlet 3.0 之前 JSP 项目的“总开关”LoginFilter 的路径映射、字符编码过滤器的注册、Servlet 的 URL 映射都在这文件里声明。如果你用 IDEA 新建 Web 项目时没注意 web.xml 是否存在部署后会出现“访问路径 404 但文件明明存在”的诡异问题——原因多半是 Servlet 映射没声明或者声明路径不一致。3. 系统分析与数据库设计从需求到表结构的完整推导3.1 角色划分与功能边界文档把系统用户分成两类管理员和普通用户。这个划分决定了整个系统的功能边界。管理员侧的核心职责是数据维护。具体包括用户管理查看注册用户列表、禁用异常账号、重置密码鲜花管理商品的增删改查、上下架、库存调整、价格修改订单管理查看所有订单、修改订单状态待发货 → 已发货 → 已完成基础数据管理鲜花分类的维护内容管理轮播图、公告、论坛帖子的发布与删除用户侧的功能则聚焦消费场景注册登录、个人信息维护浏览鲜花列表按分类、价格筛选查看鲜花详情加入收藏提交订单、查看订单状态对已购商品留言、评价在论坛发帖、回帖注意一个细节前台用户只有“提交订单”和“查看自己的订单”没有“修改订单”“删除订单”的权限。订单的流转控制权在管理员手里。这种设计避免了用户随意取消订单导致库存数据混乱的问题是在需求分析阶段就该定下来的规则。3.2 核心功能模块的拆解方式拆功能模块时不要按页面拆要按数据对象拆。这套系统最核心的数据对象是四个用户、鲜花、订单、评价。围绕用户对象注册、登录、个人中心修改资料、管理员禁用/启用账号。 围绕鲜花对象管理员增删改查、用户浏览筛选、收藏。 围绕订单对象用户创建订单、管理员更新状态、订单项关联鲜花和数量。 围绕评价对象用户对已收货商品评价、管理员可以删除违规评价。这种按数据对象拆的方法在后端代码组织上非常顺手一个实体类对应一张表一个 DAO 类封装这张表的增删改查一个 Servlet 对应一组页面操作。文档里展示的 11 个管理模块本质就是这四个核心对象在管理员视角下的细分。3.3 核心数据表设计字段、类型与约束数据库逻辑设计是这份文档最有参考价值的部分。我提炼了五张核心表的字段设计表名核心字段关键约束userid, username, password, nickname, phone, email, avatar, role, statususername 唯一password 存储密文role 区分管理员/用户flowerid, name, category_id, price, stock, image, description, sales, statusprice 用 DECIMAL(10,2)stock 默认为 0status 控制上下架categoryid, name用于鲜花分类ordersid, order_no, user_id, total_amount, status, address, create_timeorder_no 唯一status 用 TINYINT 表示状态值order_itemid, order_id, flower_id, quantity, price冗余下单时的单价快照commentid, user_id, flower_id, content, score, create_time关联用户和商品特别注意 price 字段的类型选择。很多初学者用 DOUBLE 存价格但 DOUBLE 是浮点数在比较和求和时可能产生精度误差。商品的单价、订单总金额这类数据必须用 DECIMAL(10,2)一共 10 位有效数字小数点后 2 位。订单金额的累加计算在 Java 侧也建议用 BigDecimal不要用 double 做加法。3.4 建表 SQL 与字段边界说明对应上面设计的建表 SQL 是这样的CREATE TABLE flower ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 鲜花名称, category_id INT DEFAULT NULL COMMENT 分类 ID, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, image VARCHAR(255) DEFAULT NULL COMMENT 商品图片路径, description TEXT COMMENT 商品描述, sales INT NOT NULL DEFAULT 0 COMMENT 销量, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 上架 0 下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id INT NOT NULL COMMENT 下单用户, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1待发货 2已发货 3已完成 4已取消, address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有三个边界要守住。第一字符集统一用 utf8mb4不要用 utf8——utf8mb4 才能完整支持生僻字和特殊符号避免鲜花商品名里带个不常见字导致插入失败。第二订单编号 ORDER_NO 必须唯一不能靠自增主键当订单号对外展示否则用户能通过订单 ID 猜测平台总订单量。第三订单金额 total_amount 是下单时从商品表读取单价计算后写入的后续商品调价不影响历史订单。这就是“下单时冗余单价快照”的思路。订单状态字段我用 TINYINT 而不是 VARCHAR 存字符串目的是避免脏数据。如果直接存“待付款”“已发货”后台上传数据时很容易拼错一个字用数字枚举配合 Java 侧的状态映射数据层的容错性会好很多。具体映射关系0 待付款、1 待发货、2 已发货、3 已完成、4 已取消。4. 后端实现登录会话、分页查询与订单事务4.1 登录会话控制Filter 拦截器实现未登录跳转一个商城系统最容易被攻击的漏洞就是未登录直接访问后台页面。JSP 页面本身不设防用户直接在浏览器地址栏输入 /admin/flower_list.jsp如果服务器没做拦截就能绕过登录看到后台数据。解决办法是用 Filter 统一拦截。public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String path request.getRequestURI(); // 放行登录页、注册页、静态资源其余请求全部校验会话 if (path.endsWith(login.jsp) || path.contains(/static/) || path.endsWith(register.jsp) || path.endsWith(index.jsp)) { chain.doFilter(request, response); return; } Object user session ! null ? session.getAttribute(loginUser) : null; Object admin session ! null ? session.getAttribute(loginAdmin) : null; if (user null admin null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这块代码里有三个关键点。第一用 getSession(false) 而不是 getSession(true)前者在会话不存在时返回 null不会强制创建新会话避免每次请求都往服务器写入无意义的 session。第二放行路径的判断必须包含 contextPath所以代码里用 request.getContextPath() 拼接重定向地址否则部署到带项目名的路径时会出现跳转 404。第三静态资源必须放行——CSS、JS、图片文件被拦截的话页面样式会全部丢失。4.2 鲜花列表分页Servlet JSP 的参数传递商品列表不分页的话数据超过 100 条页面就会变得很长且数据库全表扫描的压力也不小。这套系统的分页逻辑是标准的前后端参数配合。public class FlowerListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int pageNum 1; // 默认第 1 页 int pageSize 8; // 每页 8 条 String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { pageNum Integer.parseInt(pageParam); } FlowerDao dao new FlowerDao(); ListFlower flowerList dao.findByPage(pageNum, pageSize); int totalCount dao.countAll(); int totalPages (int) Math.ceil((double) totalCount / pageSize); request.setAttribute(flowerList, flowerList); request.setAttribute(totalPages, totalPages); request.setAttribute(currentPage, pageNum); request.getRequestDispatcher(/front/flower_list.jsp) .forward(request, response); } }这里有两个值得留意的边界情况。第一totalPages 的计算要用 Math.ceil 向上取整如果总记录数是 17 条、每页 8 条总页数应该是 3 而不是 2。直接用整数除法 17 / 8 会得到 2 页最后一页的 1 条数据永远查不出来。第二页面端要用 EL 表达式遍历 List 而不是用 Java 脚本片段。c:forEach items${flowerList} varflower div classflower-item img src${flower.image} alt${flower.name} / h3${flower.name}/h3 p价格${flower.price}/p a hrefflower_detail?id${flower.id}查看详情/a /div /c:forEach div classpagination a hrefflower_list?page${currentPage - 1}上一页/a span第 ${currentPage} / ${totalPages} 页/span a hrefflower_list?page${currentPage 1}下一页/a /div分页的排序和过滤条件也值得提前想清楚。我的习惯是默认按创建时间倒序让新上架的花卉排前面价格筛选通过额外的请求参数传进来。这里有个坑需要注意JSP 中引用 EL 表达式时如果对象属性不存在页面会显示空字符串而不是报错排错时看起来像“页面空白”实际是属性名拼错了。4.3 订单提交库存扣减与事务控制订单模块是整套系统最容易出数据问题的环节。用户提交订单时系统同时要执行三件事读取鲜花价格计算总金额、扣减库存、插入订单记录。这三步操作必须在一个数据库事务里完成否则会出现“订单创建成功但库存没扣”或者“库存扣了但订单没生成”的脏数据。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 检查库存并扣减 String updateStockSql UPDATE flower SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement ps1 conn.prepareStatement(updateStockSql); ps1.setInt(1, quantity); ps1.setInt(2, flowerId); ps1.setInt(3, quantity); int rows ps1.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足); } // 2. 插入订单主表 String insertOrderSql INSERT INTO orders(order_no, user_id, total_amount, status, address) VALUES(?, ?, ?, 0, ?); PreparedStatement ps2 conn.prepareStatement(insertOrderSql); ps2.setString(1, generateOrderNo()); ps2.setInt(2, userId); ps2.setBigDecimal(3, totalAmount); ps2.setString(4, address); ps2.executeUpdate(); // 3. 插入订单明细表 String insertItemSql INSERT INTO order_item(order_id, flower_id, quantity, price) VALUES(?, ?, ?, ?); PreparedStatement ps3 conn.prepareStatement(insertItemSql); ps3.setInt(1, orderId); ps3.setInt(2, flowerId); ps3.setInt(3, quantity); ps3.setBigDecimal(4, flowerPrice); ps3.executeUpdate(); conn.commit(); // 全部成功提交事务 } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步失败全部回滚 } throw new RuntimeException(下单失败 e.getMessage()); } finally { // 恢复自动提交关闭连接 }这个逻辑里最巧妙的是库存扣减 SQL 里的AND stock ?条件。普通写法是“先查库存 → 判断是否充足 → 执行 UPDATE”但这是两步操作中间存在并发窗口。用户 A 和用户 B 同时下单时两个线程都查到库存剩 1 件都判定“够”然后都执行扣减库存就变成了 -1。在 UPDATE 语句里带上库存充足条件让数据库去判断如果库存不足影响行数是 0程序抛异常回滚从根上规避了超卖问题。setAutoCommit(false)是事务开启的关键。JDBC 默认每条 SQL 自动提交一旦第一条 UPDATE 执行完数据库就写入磁盘了。所以要先在代码里关掉自动提交等三个操作全部成功后再手动 commit。任何一个环节抛异常就执行 rollback 把数据恢复到事务开始前的状态。finally 块里的连接关闭放在 rollback 或 commit 之后避免连接未释放导致连接池耗尽。5. 部署与排查五个必踩的坑和应对方法5.1 从 IDEA 部署到 Tomcat 的标准流程拿到文档后能不能跑起来很大程度取决于你的部署动作。我建议用 IDEA 的 Artifact 机制而不是手动把文件复制到 Tomcat 的 webapps 目录。先在 IDEA 里配置一个本地的 Tomcat 服务器。Project Structure → Artifacts → 选择 Web Application: Exploded把项目输出目录指向 target/flower-mall。然后在 Run Configuration 里配置 Tomcat ServerDeployment 选项中选择这个 ArtifactApplication Context 填写/flower-mall。启动后浏览器访问http://localhost:8080/flower-mall/index.jsp如果能显示登录页说明部署成功。403 错误通常是 index.jsp 不存在404 则多半是 Application Context 路径配置不一致或者 web.xml 里没配置欢迎页。5.2 常见问题排查记录问题一数据库连接报错提示com.mysql.jdbc.Driver找不到。现象启动项目后第一次访问页面抛出 ClassNotFoundException。原因一是 MySQL 驱动 jar 未放入 WEB-INF/lib 目录二是 MySQL 8.x 版本已经换了驱动类名。解决MySQL 8.x 使用com.mysql.cj.jdbc.Driver同时连接串要带时区参数serverTimezoneAsia/Shanghai。问题二页面中文乱码插入数据库的数据显示为问号。现象页面上显示正常但数据库里的中文都变成了???或者页面直接乱码。原因JSP 页面编码、请求编码和数据库字符集三方不一致。解决JSP 头部统一用pageEncodingUTF-8web.xml 里配置 CharacterEncodingFilter 强制请求编码数据库连接串追加characterEncodingutf8建库时指定DEFAULT CHARSETutf8mb4。四层全设为 UTF-8乱码基本绝迹。问题三管理员后台能登录但跳转后页面样式全丢。现象登录成功后跳转到列表页页面没有样式和图片F12 控制台显示 CSS 文件 404。原因页面里引用的 CSS 路径是相对路径登录前后 URL 层级变化导致路径失效。解决所有静态资源引用改为绝对路径用${pageContext.request.contextPath}/static/css/admin.css拼出完整路径。问题四Tomcat 部署时端口被占用。现象启动 Tomcat 时控制台报Port 8080 was already in use。原因之前启动的 Tomcat 实例没关闭或者其他程序占用了 8080 端口。解决IDEA 控制台里红色按钮停掉旧实例命令行执行netstat -ano | findstr 8080找到占用端口的 PIDtaskkill /PID xxx /F强制结束。问题五订单提交后库存没有减少的“玄学”问题。现象测试时连续提交两笔订单数据库里订单表有两条记录但商品表的库存数完全没变。原因订单事务里第一步扣减库存的 UPDATE 执行失败但异常被 catch 吞掉事务回滚了订单数据和库存数据页面却跳转到了成功页。解决下单方法里不要用空的 catch 块必须把异常信息打印到日志页面显示失败提示绝不能跳转成功页。这是数据一致性的底线问题。5.3 测试用例怎么设计文档给系统测试部分提供了一个很好的范本。我把它的思路复述成可复用的测试用例设计方法测试模块测试操作预期结果实际验证点管理员登录输入正确账号密码跳转后台首页Session 写入管理员标记管理员登录输入错误密码提示“用户名或密码错误”不能泄露账号是否存在用户注册用户名重复提示“用户名已存在”注册接口的完整性校验鲜花管理新增商品价格填 0拒绝提交价格必须大于 0订单管理修改订单状态状态流转正确非法流转已完成→待发货被拦截测试时容易疏漏的是“非法流转”场景。订单状态从待发货变成已完成是合理的但从已完成改回待发货就是异常操作。业务逻辑层要写一个状态机的校验方法禁止状态倒流。6. 进阶打磨把课程设计变成能给人演示的完整作品当你照着文档把系统跑通之后下一步不是急着交差而是做三个“只差一步”的升级。这三个技巧能让你在答辩或演示时多一张王牌。第一密码存储从明文改成 MD5 加盐。文档里的 user 表 password 字段建议存密文很多初学者直接存明文密码这是最容易被老师挑刺的安全漏洞。改造方案是注册时用MD5(密码 固定盐值)得到密文再入库登录时用同样算法加密后比对。加盐不是为了防 MD5 彩虹表而是为了防止两个用户密码相同时密文一样被一眼看出密码规律。public static String md5WithSalt(String rawPassword, String salt) { String str rawPassword salt; MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(str.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) sb.append(0); sb.append(hex); } return sb.toString(); }第二数据库连接用连接池代替 DriverManager。现在 Demo 代码里最常见的写法是每次请求都DriverManager.getConnection()用完就关。这种模式在演示环境没问题一旦有几十个人同时访问数据库连接的创建和销毁开销会拖慢响应。改造方式很简单引入阿里巴巴的 Druid 连接池在工具类里用静态代码块初始化每次从池里拿连接用完归还而不是关闭。第三做一个简易的数据校验页面。在后台加一个“订单与库存核对”功能遍历所有订单明细对比订单里的商品数量和商品表当前的库存把不一致的记录列出来。这个页面本质是给自己留的“后悔药”——做演示之前跑一遍确认核心数据是干净的不会被测试时的脏数据搞得当场翻车。从那以后我每次拿到一份类似的 Java Web 项目资源都会强制走一遍“部署 → 建库 → 跑通核心流程 → 补安全加固”的流程确认数据一致性和路径配置都正常了才去演示。这个流程能帮你过滤掉大部分隐藏问题也希望这次拆解能帮你把这套鲜花商城真正跑起来希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进