资讯动态

基于Spring Boot的鲜花商城系统设计与实现核心要点解析

发布时间:2026/9/19 19:00:13 来源:尧图企业网站定制
简介一份完整的基于Java Web的鲜花商城系统设计与实现毕业设计文档主要面向计算机相关专业学生、毕业设计撰写者以及Java Web初学者。文档以鲜花商城为业务场景系统讲解B/S开发模式、Java语言编码、JSP动态界面设计、MySQL数据库管理、JDBC数据库交互及Tomcat部署等内容详细介绍商品浏览、购物车管理、在线支付、订单处理、用户管理和评论互动等核心模块并给出数据表结构设计思路涵盖需求分析、可行性分析、技术支持、功能设计、数据库设计、功能测试等完整流程配有主要功能模块界面与操作界面说明适合用于课程设计、毕业设计或项目实战参考。资源包为1个docx文档整体大小2.75MB排版完整便于查阅与二次编辑。目前已有72人学习下载适合希望快速掌握校园商城项目设计思路与文档撰写规范的学习者。1. 拿 Java 做鲜花商城系统从哪里入手最稳很多计算机专业的朋友拿到“基于 Java 的鲜花商城系统设计与实现”这个课题第一反应是到处找完整源码。但真正到答辩或者作品展示时被问得最多的往往是“库存怎么扣的”“购物车价格怎么算的”“订单状态怎么流转的”。与其抄一套看不懂的代码不如用 Spring Boot MyBatis-Plus MySQL 这套主流组合把用户、商品、购物车、订单、分类这五条线自己搭一遍两周时间足够做到能演示、能讲原理。这篇博文按“技术选型 → 数据库设计 → 后端核心链路 → 前端数据渲染 → 验证技巧”的顺序展开所有命令和代码都按可复现的标准写。适合正在做课设、想写进简历的 Java 初学者也适合想快速补一套商城系统代码结构的面试准备者。2. 鲜花商城系统的技术选型和工程结构先用 Spring Boot 固定骨架2.1 为什么是 Spring Boot 而不是 JSP Servlet 或 SSM以前教材里常见的是 JSP Servlet JDBC或者 SSMSpring Spring MVC MyBatis手写大量 XML 配置。现在做鲜花商城系统最省事也最容易讲清楚的是 Spring Boot。它内置 Tomcat不用额外部署 war 包自动配置把数据源、事务、JSON 序列化这些琐事处理掉让你把精力集中在业务逻辑上。我一般建议用 Spring Boot 2.7.x 搭配 JDK 8 或 JDK 11原因有两个。第一网上绝大多数资料和面试题都基于这套版本遇到报错容易搜到答案第二Spring Boot 3.x 要求 JDK 17部分学校机房的环境不一定支持。MyBatis-Plus 选 3.5.x它对单表 CRUD 的支持很省事分页插件也好配。数据库用 MySQL 5.7 或 8.0 都行字符集统一 utf8mb4。前端模板引擎用 Thymeleaf不用前后端分离因为课设系统通常要演示“页面跳转 表单提交 数据回显”Thymeleaf 一套就能完成不需要再搭 Node 环境。提示如果导师明确要求“必须体现 Servlet 或 JSP”那可以在 Spring Boot 里引入 JSP 支持但核心业务逻辑仍放在 Service 层这样两头都占住。2.2 包结构划分用 controller / service / mapper 三层把职责分开鲜花商城系统至少包含用户登录注册、商品列表、商品详情、购物车、订单结算这几个模块。代码包结构我会这样设计每个包只负责一件事答辩时也方便按包讲解com.example.flower ├── FlowerApplication.java // 启动类 ├── config │ └── MybatisPlusConfig.java // MyBatis-Plus 分页插件配置 ├── controller │ ├── UserController.java // 登录、注册、退出 │ ├── GoodsController.java // 商品列表、详情、分类筛选 │ ├── CartController.java // 购物车增删改查 │ └── OrderController.java // 下单、订单列表、取消 ├── service │ ├── GoodsService.java │ ├── CartService.java │ ├── OrderService.java │ └── impl │ ├── GoodsServiceImpl.java │ ├── CartServiceImpl.java │ └── OrderServiceImpl.java ├── mapper │ ├── GoodsMapper.java │ ├── CartMapper.java │ ├── OrderMapper.java │ └── UserMapper.java ├── entity │ ├── User.java │ ├── Goods.java │ ├── Cart.java │ ├── Order.java │ └── OrderItem.java ├── dto │ └── CartVO.java // 购物车展示对象含商品名、单价、小计 └── common └── Result.java // 统一返回结构controller 只接收参数和返回结果不写业务代码service 处理事务和业务校验mapper 只做数据库操作。这个分层是“Java 后端完整成长路线”里反复强调的规范对 5 年以上经验的开发者来说可能觉得基础但对课设和简历项目来说能讲清分层本身就是加分项。2.3 最小可运行配置pom.xml 依赖和 application.yml 参数先建一个 Spring Boot 项目核心依赖就四个web、thymeleaf、mybatis-plus-boot-starter、mysql-connector-java。lombok 建议加上减少实体类 getter/setter 代码。下面是 pom.xml 里最关键的片段dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency对应的 application.ymlserver: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/flower_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置里有两个容易踩的坑。第一url 必须带serverTimezoneAsia/Shanghai否则 MySQL 8.0 会报时区错误。第二thymeleaf.cachefalse一定要开开发时改完页面刷新就能看到效果不用重启。log-impl配置成 StdOutImpl 后控制台会打印每条 SQL方便调试参数传递。配好后运行mvn spring-boot:run或直接启动主类浏览器访问http://localhost:8080能看到默认错误页说明项目骨架没问题。3. 鲜花商城系统的核心表设计六张表把业务边界划清楚3.1 用户、商品、分类、购物车、订单、订单明细的建表 SQL鲜花商城系统不建议建太多表六张足够覆盖需求。重点在字段类型和索引设计。商品表的价格用decimal(10,2)数量用int状态用tinyint。订单表要单独存一份商品快照信息因为用户下单后商品可能改价或下架。下面是建表 SQLCREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT MD5或BCrypt, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods_category ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, sort int(11) DEFAULT 0 COMMENT 排序值越小越靠前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) NOT NULL, name varchar(100) NOT NULL COMMENT 商品名称, subtitle varchar(200) DEFAULT NULL COMMENT 卖点描述, main_image varchar(255) DEFAULT NULL COMMENT 图片相对路径, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cart ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, goods_id bigint(20) NOT NULL, quantity int(11) NOT NULL DEFAULT 1, checked tinyint(4) NOT NULL DEFAULT 1 COMMENT 是否勾选结算, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id bigint(20) NOT NULL, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name varchar(50) NOT NULL, receiver_phone varchar(20) NOT NULL, receiver_address varchar(200) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL, goods_id bigint(20) NOT NULL, goods_name varchar(100) NOT NULL COMMENT 商品名称快照, goods_image varchar(255) DEFAULT NULL, price decimal(10,2) NOT NULL COMMENT 成交单价快照, quantity int(11) NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品表上建了idx_category_status联合索引因为商城首页最常见的查询是“某个分类下查上架商品”这个索引能直接覆盖查询条件。购物车表用联合唯一键uk_user_goods防止同一用户把同一商品加两遍代码里要做INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1或先查后插。3.2 使用 MyBatis-Plus 分页查询商品列表并支持分类和关键字筛选MyBatis-Plus 的分页功能需要先配置分页插件不然Page对象查出来的记录数永远是 0。配置类如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(100L)是保护性参数防止有人传一个超大的 pageSize 把数据库打慢。查询服务层写法Override public PageGoods pageGoods(long pageNum, long pageSize, Long categoryId, String keyword) { PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 1) .eq(categoryId ! null, Goods::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Goods::getName, keyword) .orderByDesc(Goods::getSales) .orderByDesc(Goods::getCreateTime); return goodsMapper.selectPage(page, wrapper); }eq(condition, column, value)是 MyBatis-Plus 条件构造器的固定写法第一个参数为 true 时才会拼接这段 SQL。这种写法避免了手写动态 SQL 时那些if标签但要提醒一点条件构造器适合单表简单查询多表 join 还是要写在 XML 里。商品表和分类表 join 的场景不多用两次查询或直接在 SQL 里 LEFT JOIN 都可以我更推荐后者因为一次 IO 比两次快。3.3 下单时的数据一致性事务、库存扣减和订单状态商城系统里最容易出问题的就是“扣库存”和“生成订单”不是同时成功的。Spring 的Transactional能把这两个操作放在同一个数据库事务里。扣库存的 SQL 要写条件stock quantity这样并发时不会扣成负数这也是面试里常问的“乐观锁/悲观锁”之外最常见的方案Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { String orderNo generateOrderNo(); // 1. 锁定购物车中勾选的商品 ListCart cartList cartMapper.selectCheckedList(dto.getUserId()); // 2. 遍历商品逐个扣库存 for (Cart cart : cartList) { int rows goodsMapper.deductStock(cart.getGoodsId(), cart.getQuantity()); if (rows 0) { throw new BusinessException(商品库存不足 cart.getGoodsId()); } } // 3. 保存订单和明细 orderMapper.insert(order); orderItemMapper.batchInsert(orderItems); // 4. 清空购物车 cartMapper.deleteByUserAndChecked(dto.getUserId()); return order.getId(); }rollbackFor Exception.class说明任何异常都回滚事务。deductStock的 XML 写法是重点update iddeductStock UPDATE goods SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{goodsId} AND stock gt; #{quantity} /updateaffected rows 为 0 就代表库存不足抛出异常让整个事务回滚。这里不要把“先查库存再更新”写成两条 SQL因为查和改之间存在时间差并发下单时会超卖。4. 鲜花商城系统的后端业务链路从登录到购物车再到订单4.1 登录状态用 Session ThreadLocal 保存当前用户商城系统几乎所有操作都要知道“当前登录用户是谁”。常见做法是登录成功后把用户 ID 放到 Session 里再通过拦截器把 ID 取出来放进 ThreadLocal。好处是业务代码里不用每个方法都传 userId 参数。public class UserContext { private static final ThreadLocalLong HOLDER new ThreadLocal(); public static void set(Long userId) { HOLDER.set(userId); } public static Long get() { return HOLDER.get(); } public static void clear() { HOLDER.remove(); } } public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(/user/login); return false; } UserContext.set(user.getId()); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }拦截器要在配置类里注册并指定拦截路径。UserContext.clear()一定要放在afterCompletion里防止线程池复用导致下个请求读到上一个用户的 ID。4.2 购物车接口加购、改数量、删除和价格重算购物车表存的是user_id goods_id quantity展示时要把商品表的信息 join 出来。我一般定义一个CartVO来承接展示数据。加购代码public void addToCart(Long userId, Long goodsId, Integer quantity) { if (quantity null || quantity 0) { throw new BusinessException(数量不合法); } Goods goods goodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } Cart cart cartMapper.selectByUserIdAndGoodsId(userId, goodsId); if (cart null) { Cart newCart new Cart(); newCart.setUserId(userId); newCart.setGoodsId(goodsId); newCart.setQuantity(quantity); cartMapper.insert(newCart); } else { int newQuantity cart.getQuantity() quantity; cartMapper.updateQuantity(cart.getId(), newQuantity); } }改数量时要判断用户传上来的数量是否超过库存以及是否小于等于 0。价格不直接存购物车表每次展示时用商品当前价格乘以数量这样商品改价后购物车不出现脏数据代码也更简单。4.3 下单接口的幂等性防止重复提交订单用户点击“提交订单”后如果网络卡顿可能会连点两次。如果不做处理会生成两个一样的订单。常见方案是在页面上生成一个一次性 token提交时带到后端校验。也可以用数据库唯一约束来处理订单号order_no是唯一键同一用户同一时间生成的订单号只要保证唯一重复提交会导致第二次插入失败。我一般用UUID.replace(-, )生成订单号但这种方式在分布式下不保证有序。课设项目可以直接用时间戳加用户 IDprivate String generateOrderNo() { return System.currentTimeMillis() userId; }如果担心同一毫秒重复可以在后面加随机数。实现时还可以在order表加一个user_id create_time的组合约束进一步防止重复插入。5. 鲜花商城系统的页面渲染和数据传递Thymeleaf 直接连后端5.1 用 Thymeleaf 布局让每个页面共用导航栏和底部不用前后端分离时Thymeleaf 是 Spring Boot 官方推荐模板。页面文件放在src/main/resources/templates下静态资源如 CSS、JS、图片放在src/main/resources/static下。导航栏、底部版权信息这些公共部分可以抽成一个片段模板fragments/common.htmlhtml xmlns:thhttp://www.thymeleaf.org body nav th:fragmentnavbar classnavbar a th:href{/goods/list}首页/a a th:href{/cart/list}购物车/a a th:href{/order/list}我的订单/a span th:if${session.loginUser ! null} th:text${session.loginUser.nickname}/span /nav /body /html其他页面引入方式div th:replace~{fragments/common :: navbar}/divth:replace会用片段的标签替换当前 divth:insert则是把片段内容插入 div 内部。这两个的区别在页面出样式问题时经常需要排查。5.2 商品列表页的 Controller 数据传递方式商品列表页需要两个数据分页后的商品列表和当前选中的分类 ID。代码GetMapping(/goods/list) public String list(RequestParam(defaultValue 1) long pageNum, RequestParam(defaultValue 12) long pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword, Model model) { PageGoods page goodsService.pageGoods(pageNum, pageSize, categoryId, keyword); model.addAttribute(page, page); model.addAttribute(categoryId, categoryId); model.addAttribute(keyword, keyword); return goods/list; }defaultValue 1和12保证用户没传参数时页面不会报错。required false是关键categoryId 和 keyword 是可选的如果写成 trueURL 没带参数直接 400。页面里用th:each遍历商品列表div classgoods-grid div classgoods-card th:eachgoods : ${page.records} img th:src{${goods.mainImage}} th:alt${goods.name} h3 th:text${goods.name}鲜花/h3 p classprice th:text¥ ${#numbers.formatDecimal(goods.price, 1, 2)}¥99.00/p a classbtn th:href{/goods/detail/{id}(id${goods.id})}查看详情/a /div /div#numbers.formatDecimal(goods.price, 1, 2)是 Thymeleaf 内置的数字格式化工具保证价格显示保留两位小数。图片路径用th:src{${goods.mainImage}}如果数据库存的是/upload/rose.jpg会自动拼上上下文路径。5.3 上传图片的显示问题用静态资源映射解决 404商城系统后台要能上传商品图片。图片如果直接传到项目目录下重启后可能丢失传到服务器磁盘的某个目录页面又会 404因为 Spring Boot 默认只映射classpath:/static/。解决办法是加一个资源映射配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); } }这样访问http://localhost:8080/upload/rose.jpg时会映射到项目运行目录下的upload文件夹。user.dir是 JVM 启动时的工作目录用 IDE 启动就是项目根目录用java -jar启动就是 jar 所在目录。生产环境一般改用绝对路径或对象存储但课设这样够用。6. 验证系统可靠性的三个关键点实测记录、参数校验和慢 SQL6.1 用“分”为单位存储金额避免 BigDecimal 比较陷阱鲜花商品价格如果用double存储0.1 加 0.2 会得到 0.30000000000000004。数据库字段用decimal(10,2)能存对但代码里做订单总金额累加时仍建议把元转成分用long做加法。定义公共工具类public final class MoneyUtil { private MoneyUtil() { } public static long toFen(BigDecimal yuan) { return yuan.setScale(2, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)) .longValue(); } public static BigDecimal toYuan(long fen) { return BigDecimal.valueOf(fen, 2); } }累加金额时用分相加展示时再转回元这样不会出浮点误差。这个点写在答辩 PPT 里很加分。6.2 压测下单接口观察库存是否超卖写完下单接口后可以开两个浏览器窗口同时登录同一个账号对同一个商品同时下单。正常情况下一单成功一单提示库存不足而不是两单都成功。想更严谨可以用 JMeter 模拟 50 个并发请求观察goods.stock扣减总和是否等于初始值减订单总量。如果发现超卖优先检查deductStock的 XML 里是否带stock #{quantity}条件以及 Service 方法上有没有Transactional。开启 MyBatis-Plus 的 SQL 日志后看控制台输出的 UPDATE 语句是否包含 WHERE 条件。6.3 查询慢的接口用 EXPLAIN 验证索引商品搜索页如果数据量上万关键词模糊查询会很慢。先执行EXPLAIN SELECT * FROM goods WHERE name LIKE %玫瑰%看type列是不是ALLrows列是不是很大。如果是说明没走索引。LIKE %关键字%前置通配符会导致索引失效这是八股文里常考的知识点实际调优时可以把搜索改成全文索引MySQL FULLTEXT或加一个专门的搜索表。课上演示时先把idx_category_status索引建上再对比一下走分类筛选和全表扫描的查询耗时用数据说话。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价