资讯详情 基于Java的网上花店系统实战:从数据库设计到订单库存扣减的完整链路
📅 2026/10/8 13:49:46
简介这是一套面向Java初学者与毕业设计学生的网上花店系统完整实战资料围绕Java Web开发流程帮助读者理解并掌握从数据库设计到前后端交互的关键技术。资源包共5个文件包含2个zip源码与数据库脚本压缩包、2个mp4部署演示视频以及1个sql数据库文件整体约358.69MB目录按视频、代码、参考资料分层组织便于按模块检索学习。系统采用Servlet与JSP处理请求结合MySQL等关系型数据库完成商品、用户、订单等数据表的增删改查并可能运用MVC模式与Bootstrap等前端技术优化界面与交互。部署视频详细演示Tomcat环境配置、数据库脚本导入与项目运行流程源码则可深入分析Servlet生命周期、JDBC操作及MVC实现细节。目前已有273人学习适合准备Java毕业设计或希望提升Web开发能力的开发者参考。1. 网上花店系统从一份 Java 课设到能跑通订单的完整链路很多人第一次接触「基于 Java 的网上花店系统」是在课程设计或毕业设计选题表上。它看起来简单——不就是商品列表加购物车吗但真正动手才会发现花店业务和普通商城差别不小鲜花有保鲜期、有花语、有节日溢价订单还要区分预订和即时配送。这个标题背后其实是一套完整的 Java Web 工程实践Spring Boot 做后端、MySQL 存数据、Thymeleaf 或 Vue 做前端再加上订单状态机、库存扣减和支付模拟。它适合三类人正在找毕设题目的学生、想练手一个完整 CRUD 项目的 Java 新手、以及需要快速搭一套小型电商原型的开发者。源代码、部署视频、数据库脚本这三件套恰好对应了「能看懂、能跑起来、能改」三个层次的需求。下面我按实际落地顺序把选型、建库、编码、部署和排错讲清楚。2. 技术选型与数据库设计为什么这套组合最稳2.1 后端框架选 Spring Boot 而不是原生 Servlet网上花店系统的核心是「商品—订单—用户」三张主表加若干关联表业务逻辑集中在订单状态流转和库存扣减上。用原生 Servlet JSP 当然能写但配置量大、依赖管理靠手动导 jar调试成本高。Spring Boot 的自动配置和起步依赖能把 Web 层、数据访问层、模板引擎一次性拉齐省下的时间可以花在业务逻辑上。常见做法是 Spring Boot 2.7.x 或 3.x 搭配 MyBatis-Plus。MyBatis-Plus 相比原生 MyBatis 多了通用 CRUD 和条件构造器花店系统里大量「按分类查花、按价格区间筛选、按销量排序」的查询用 LambdaQueryWrapper 几行就能写完。如果你更熟悉 JPA用 Spring Data JPA 也可以但要注意花店系统里多表联查订单明细关联商品用 JPA 需要写 JPQL 或原生 SQL灵活度不如 MyBatis-Plus。前端方面课设级别用 Thymeleaf 服务端渲染最省事不用单独起前端工程如果想让简历好看一点用 Vue3 Element Plus 前后端分离后端只提供 REST 接口。两种都行但部署视频里通常演示的是 Thymeleaf 版本因为一个 jar 包就能跑不用配 Nginx 反向代理。提示JDK 版本要和 Spring Boot 版本匹配。Spring Boot 3.x 要求 JDK 17 及以上如果你本地是 JDK 8就选 Spring Boot 2.7.x否则启动直接报 UnsupportedClassVersionError。2.2 数据库表结构六张核心表怎么定花店系统的数据库设计有几个容易翻车的地方商品表要不要存库存、订单表要不要冗余商品快照、用户表要不要分角色。我一般会建下面六张表字段和类型如下。表名作用关键字段注意点user用户信息id, username, password, phone, rolerole 区分普通用户和管理员category花卉分类id, name, sort_order支持前台按分类筛选flower花卉商品id, name, category_id, price, stock, image, description, statusstatus 控制上架下架cart购物车id, user_id, flower_id, quantity同一用户同一商品只存一条orders订单主表id, order_no, user_id, total_amount, status, address, create_timeorder_no 用时间戳加随机数order_item订单明细id, order_id, flower_id, flower_name, price, quantity冗余商品名和单价做快照订单明细里冗余 flower_name 和 price 是血泪经验如果只存 flower_id商品改价或下架后历史订单显示的价格就变了对账时对不上。库存扣减放在订单创建时用UPDATE flower SET stock stock - ? WHERE id ? AND stock ?这种带条件的更新靠数据库行锁保证不超卖比先查再改安全。建库语句用 utf8mb4 字符集否则花名里的特殊符号或 emoji 会乱码。下面这段 SQL 可以直接在 MySQL 8.0 里执行。CREATE DATABASE flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE flower_shop; CREATE TABLE flower ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 花名, category_id BIGINT NOT NULL COMMENT 分类ID, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, image VARCHAR(255) COMMENT 图片路径, description TEXT COMMENT 花语描述, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成 4已取消, address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB;字段类型上金额一律用 DECIMAL 不用 FLOAT避免 0.10.2 这类精度问题。status 用 TINYINT 而不是 VARCHAR查询和索引效率更高。order_no 加唯一索引防止并发下生成重复订单号。2.3 项目分层与包结构拿到源代码后先别急着跑花十分钟看包结构能省很多事。标准分层是 controller、service、mapper、entity、config、util。controller 只做参数校验和调用 service业务逻辑全在 service 里mapper 只负责数据库操作。这样改需求时定位快比如「下单送积分」只需要动 service 层。配置文件 application.yml 里重点看三处数据库连接、MyBatis-Plus 的 mapper 扫描路径、文件上传目录。数据库密码如果是加密的部署视频里一般会告诉你原始密码或解密方式。文件上传目录要改成你本地的绝对路径否则图片存到临时目录重启就没了。3. 从零跑通环境搭建、建库与启动的完整命令3.1 本地环境准备与依赖安装先确认三样东西JDK、Maven、MySQL。命令行里分别执行java -version、mvn -v、mysql --version版本对得上再往下走。JDK 用 8 或 17 看项目 pom.xml 里的java.versionMaven 用 3.6 以上MySQL 用 5.7 或 8.0 都行但 8.0 的驱动类名是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver改错启动就报驱动找不到。如果本地没装 MySQL用 Docker 起一个最省事一条命令搞定端口映射到 3306密码自己定。docker run -d --name flower-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEflower_shop \ mysql:8.0 --character-set-serverutf8mb4这条命令做了三件事后台启动 MySQL 8.0 容器、把容器 3306 映射到宿主机、创建名为 flower_shop 的数据库并设置 root 密码。--character-set-serverutf8mb4保证服务端字符集正确避免中文乱码。容器名 flower-mysql 方便后续用docker logs flower-mysql看日志。3.2 导入数据库脚本并核对数据源代码包里通常有一个 .sql 文件用命令行导入比用图形工具更可靠因为图形工具偶尔会截断大文件。mysql -h 127.0.0.1 -u root -p flower_shop flower_shop.sql导入后进去核对三件事表数量对不对、flower 表有没有测试数据、orders 表是不是空的。执行SHOW TABLES;和SELECT COUNT(*) FROM flower;。如果 flower 表是空的前台首页会一片空白这不是代码问题是数据没导进去。测试数据里一般有十几条花卉记录价格和库存都有值。注意导入前先确认 .sql 文件里的数据库名和 application.yml 里配的一致。有些脚本写的是USE flower;而配置里是flower_shop这种不一致会导致表建到另一个库里启动时报 Table doesnt exist。3.3 修改配置并启动项目打开src/main/resources/application.yml改数据库连接四要素url、username、password、driver-class-name。url 里要带时区和编码参数否则插入中文或时间会出问题。spring: datasource: url: jdbc:mysql://127.0.0.1:3306/flower_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 10MBserverTimezoneAsia/Shanghai必须加MySQL 8.0 不加会报时区错误。useSSLfalse关掉 SSL 警告本地开发不需要。文件上传大小限制按需调花卉图片一般 2MB 以内够用设 10MB 留余量。改完在项目根目录执行启动命令。用 Maven 插件启动适合开发调试打包成 jar 适合部署。# 开发模式启动 mvn spring-boot:run # 打包后启动 mvn clean package -DskipTests java -jar target/flower-shop-1.0.0.jar-DskipTests跳过测试加快打包但第一次跑建议不加让测试跑一遍能提前发现配置问题。启动成功的标志是控制台出现Started FlowerShopApplication in x.xxx seconds然后浏览器访问http://localhost:8080。如果端口被占用在 application.yml 里加server.port: 8081换端口。3.4 验证核心链路注册、加购、下单跑起来只是第一步要确认业务链路通。按这个顺序点一遍注册一个新用户 → 登录 → 首页选一束花加购物车 → 进购物车改数量 → 提交订单 → 在订单列表看到这条记录。每一步都对应后端一个接口哪一步报错就看对应 controller 的日志。下单成功后去数据库查SELECT * FROM orders ORDER BY id DESC LIMIT 1;确认 total_amount 和 order_item 里的明细对得上。再查SELECT stock FROM flower WHERE id ?;看库存有没有减。如果订单生成了但库存没减说明扣减逻辑没放在同一个事务里这是常见 bug后面避坑章节会细说。4. 核心业务代码订单状态机与库存扣减怎么写4.1 订单创建与库存扣减的事务处理订单创建是整个系统最容易出并发问题的地方。两个人同时买最后一束花如果先查库存再更新中间有时间窗口会超卖。正确做法是用带条件的 UPDATE把判断和扣减合成一条原子操作。Service public class OrderServiceImpl implements OrderService { Autowired private FlowerMapper flowerMapper; Autowired private OrdersMapper ordersMapper; Autowired private OrderItemMapper orderItemMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(Long userId, ListCartItem items, String address) { // 1. 生成订单号时间戳 用户ID后四位 随机数 String orderNo System.currentTimeMillis() String.format(%04d, userId % 10000) (int) (Math.random() * 9000 1000); BigDecimal total BigDecimal.ZERO; // 2. 逐项扣库存扣失败直接抛异常回滚 for (CartItem item : items) { int affected flowerMapper.deductStock(item.getFlowerId(), item.getQuantity()); if (affected 0) { throw new RuntimeException(库存不足 item.getFlowerName()); } total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 写订单主表 Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); order.setAddress(address); ordersMapper.insert(order); // 4. 写订单明细冗余商品名和单价 for (CartItem item : items) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setFlowerId(item.getFlowerId()); oi.setFlowerName(item.getFlowerName()); oi.setPrice(item.getPrice()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); } return orderNo; } }Transactional(rollbackFor Exception.class)保证任何一步失败都整体回滚不会出现订单写了但库存没扣的情况。deductStock对应的 SQL 是UPDATE flower SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}返回受影响行数0 表示库存不够。订单号用时间戳加随机数并发下重复概率极低加上数据库唯一索引兜底。4.2 订单状态流转与超时取消订单状态从 0 到 4 的流转要有约束不能随便改。常见做法是在 service 层写一个状态校验方法只允许合法流转0→1付款、1→2发货、2→3确认收货、0→4取消、1→4退款取消。非法流转直接抛异常。超时取消用 Spring 的Scheduled定时任务每分钟扫一次超过 30 分钟未付款的订单批量改成已取消并回滚库存。Scheduled(cron 0 * * * * ?) public void cancelTimeoutOrders() { // 查出 30 分钟前创建且状态为待付款的订单 ListOrders timeoutOrders ordersMapper.selectList( new LambdaQueryWrapperOrders() .eq(Orders::getStatus, 0) .lt(Orders::getCreateTime, LocalDateTime.now().minusMinutes(30)) ); for (Orders order : timeoutOrders) { order.setStatus(4); ordersMapper.updateById(order); // 回滚库存 ListOrderItem items orderItemMapper.selectByOrderId(order.getId()); for (OrderItem item : items) { flowerMapper.rollbackStock(item.getFlowerId(), item.getQuantity()); } } }cron 表达式0 * * * * ?表示每分钟的第 0 秒执行。回滚库存用UPDATE flower SET stock stock #{qty} WHERE id #{id}。定时任务要在启动类上加EnableScheduling才生效忘了加这个注解任务不会跑这是新手常踩的坑。4.3 文件上传与图片回显花卉图片上传是花店系统的刚需。后端用 MultipartFile 接收存到本地磁盘或对象存储数据库只存相对路径。存本地时要注意路径分隔符Windows 和 Linux 不一样用File.separator或直接拼/。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.error(文件为空); } // 用 UUID 重命名避免中文名和重名覆盖 String original file.getOriginalFilename(); String suffix original.substring(original.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; // 上传目录从配置读取默认 ./upload/ String uploadDir System.getProperty(user.dir) /upload/; File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadDir fileName)); // 返回可访问的相对路径 return Result.success(/upload/ fileName); }用 UUID 重命名是为了避免用户上传同名文件互相覆盖也避免中文文件名在某些服务器上乱码。返回的路径要能被前端直接访问所以还需要一个静态资源映射配置把/upload/**映射到本地目录否则图片存进去了但浏览器 404。5. 部署与避坑那些让项目跑不起来的细节5.1 打包部署到服务器的完整流程本地跑通后部署到服务器是另一道坎。流程是服务器装 JDK → 上传 jar 包 → 后台启动 → 配防火墙。JDK 版本要和打包时一致本地用 17 打包服务器装 8 就会报版本错误。# 服务器后台启动日志输出到 nohup.out nohup java -jar flower-shop-1.0.0.jar --spring.profiles.activeprod nohup.out 21 # 查看启动日志 tail -f nohup.out # 确认端口监听 netstat -tlnp | grep 8080--spring.profiles.activeprod指定生产环境配置application-prod.yml 里改数据库地址和上传路径。nohup ... 让进程在后台跑关掉终端也不停。21把错误输出也重定向到日志文件否则报错看不到。启动后如果浏览器访问不了先netstat看端口有没有监听再检查服务器安全组或防火墙有没有放行 8080。5.2 数据库连接与字符集排查启动报Communications link failure是连不上数据库检查三处MySQL 服务有没有起、url 里的 IP 和端口对不对、服务器防火墙有没有放行 3306。报Access denied for user是密码错或该用户没有远程访问权限MySQL 8.0 默认 root 只能本地登录要执行CREATE USER root% IDENTIFIED BY 密码;并授权。中文乱码分两种存进去就乱还是取出来才乱。存进去乱是连接字符集问题url 里加characterEncodingutf8取出来乱是数据库或表字符集问题建库时用 utf8mb4。还有一种情况是前端页面没声明meta charsetUTF-8浏览器按 GBK 解析这种改 HTML 头就行。5.3 常见问题与排查清单现象启动报 Table flower_shop.flower doesnt exist。原因数据库脚本没导入或导入到了别的库。 解决执行SHOW TABLES;确认表在不在当前库不在就重新导入注意脚本里的USE语句指向的库名。现象下单成功但库存没减。原因扣库存和写订单不在同一个事务里或Transactional没生效比如同类内部方法调用。 解决确认扣库存和写订单在同一个 service 方法内且该方法被外部调用。同类内部this.xxx()调用不走代理事务不生效。现象图片上传成功但页面显示裂图。原因静态资源映射没配或上传路径和访问路径不一致。 解决加 WebMvcConfigurer 把/upload/**映射到实际目录确认返回的路径和映射前缀一致。现象定时任务不执行。原因启动类没加EnableScheduling或 cron 表达式写错。 解决启动类加注解cron 用在线工具验证注意 Spring 的 cron 是 6 位不是 Linux 的 5 位。现象打包后运行报 no main manifest attribute。原因pom.xml 里没配 spring-boot-maven-plugin。 解决在 build 节点加插件配置重新mvn clean package。6. 进阶技巧用接口测试和日志把问题钉死项目跑通之后真正拉开差距的是排查效率。我一般会做两件事用 Postman 或 curl 把核心接口单独测一遍以及在关键路径打日志。接口测试能快速区分是前端问题还是后端问题日志能定位到具体哪一行出错。先看接口测试。下单接口依赖登录态所以要先调登录拿 token再带着 token 调下单。用 curl 演示这条链路# 1. 登录拿 token curl -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:test,password:123456} # 2. 带 token 下单 curl -X POST http://localhost:8080/api/order/create \ -H Content-Type: application/json \ -H token: 上一步返回的token \ -d {address:北京市朝阳区xx路1号,items:[{flowerId:1,quantity:2}]}第一步返回的 JSON 里会有 token 字段复制出来填到第二步的 header 里。如果第一步就失败说明登录逻辑或用户数据有问题如果第一步成功第二步失败问题在下单逻辑。这种二分法比在页面上点来点去快得多。日志方面在 service 方法入口和关键分支加log.info用占位符而不是字符串拼接。比如log.info(创建订单 userId{}, items{}, userId, items.size());。日志级别在 application.yml 里配开发时用 debug生产用 info。下单失败时看日志里有没有「库存不足」或异常堆栈比猜快。还有一个实用技巧把订单状态流转做成枚举而不是散落的数字。枚举里定义状态码和描述前端展示和状态校验都从枚举取改需求时只改一处。状态码枚举名含义允许的下一步0UNPAID待付款1 或 41PAID已付款2 或 42SHIPPED已发货33FINISHED已完成无4CANCELLED已取消无用枚举做状态校验非法流转直接抛异常比一堆 if-else 清晰。这套东西做完网上花店系统就不只是一个课设而是一个能讲清楚设计取舍的小型工程。我自己踩过最深的坑是事务不生效导致库存对不上查了两个小时才发现是同类内部调用。所以每次写完带事务的方法我都会问自己一句这个方法是从外部调进来的吗希望帮到你。本文还有配套的精品资源点击获取