基于Spring Boot与MyBatis-Plus的鲜花商城系统实战:从建表到下单 简介这份资源是一篇基于Java Web的鲜花商城系统设计与实现文档面向计算机相关专业学生、Java初学者及需要完成课程设计或毕业设计的人群帮助解决传统鲜花商城人工管理效率低、信息更新滞后的问题。压缩包内共1个docx文件约2.75MB内容涵盖需求分析、可行性分析、技术支持、功能设计、数据库设计与功能测试等完整章节。文档以B/S架构为主线采用Java语言编码、MySQL存储数据、JSP开发动态界面并借助IDEA与Tomcat完成部署系统与数据库通过JDBC交互。功能模块涉及商品浏览、购物车管理、在线支付、订单处理、用户管理及评论互动数据库部分还给出商品表、用户表、订单表等结构设计思路。目前已有73人学习适合作为Java Web入门实践与论文写作的参考范本也可为后续系统更新维护提供思路。1. 鲜花商城系统从课程设计到能跑起来的技术选型很多同学拿到「基于 Java 的鲜花商城系统设计与实现」这个题目时第一反应是去搜一套现成源码改改包名、换换 Logo 就交差。但真正做过的人都知道这类系统翻车往往不在业务逻辑而在环境配置、依赖冲突和数据库设计这三道坎上。我见过太多人本地跑得好好的换台机器就起不来最后连答辩演示都撑不过去。这个系统的本质是一套典型的 B2C 电商单体应用用户浏览鲜花、加购物车、下单支付、后台管理商品和订单。技术栈上Spring Boot MyBatis-Plus MySQL 是当前最稳的组合前端用 Vue 或 Thymeleaf 都能落地。它适合计算机专业做毕设的学生也适合想练手完整业务闭环的 Java 新手。接下来我会把选型理由、建表脚本、核心接口和踩坑记录一层层拆开让你照着就能复现一套能演示、能讲清楚、能扛住提问的鲜花商城系统。2. 技术选型与工程骨架为什么是 Spring Boot MyBatis-Plus2.1 单体架构在毕设场景下的合理性电商系统天然适合微服务但毕设场景下我强烈建议走单体。原因很直接你只有一个人、几周时间微服务带来的注册中心、网关、分布式事务全是负担答辩老师也不会因为你用了 Nacos 就多给分。单体架构把 Controller、Service、Mapper 分层做好业务闭环跑通反而更容易讲清楚设计思路。Spring Boot 的价值在于自动配置和起步依赖。你不需要手动配 Tomcat、不需要写一堆 XML一个spring-boot-starter-web就能把 Web 层撑起来。MyBatis-Plus 则在 MyBatis 基础上封装了通用 CRUD单表操作几乎不用写 SQL分页插件也是开箱即用。对于鲜花商城这种以单表查询为主的系统MyBatis-Plus 能省掉至少一半的 Mapper 代码量。版本选择上JDK 用 8 或 11 都行Spring Boot 2.7.x 是当前最稳的毕设版本别追 3.xJDK 17 的适配问题会浪费你大量时间。MySQL 用 5.7 或 8.0 均可注意 8.0 的驱动类名和时区配置与 5.7 不同。2.2 用 MyBatis-Plus 根据实体类生成建表 SQL热搜里有人问「mybatisplus 根据 java 实体类生成创建表的 sql 语句」这其实是个很实用的需求。MyBatis-Plus 本身不直接提供建表功能但它的注解体系可以配合手写脚本或第三方工具实现。我一般会先写好实体类再用注解标注字段类型和约束然后手动整理成建表 SQL。下面是一个鲜花商品实体类的写法import com.baomidou.mybatisplus.annotation.*; import java.math.BigDecimal; import java.time.LocalDateTime; TableName(flower_product) public class FlowerProduct { TableId(type IdType.AUTO) private Long id; TableField(product_name) private String productName; TableField(category_id) private Long categoryId; TableField(price) private BigDecimal price; TableField(stock) private Integer stock; TableField(cover_img) private String coverImg; TableField(value create_time, fill FieldFill.INSERT) private LocalDateTime createTime; TableField(value update_time, fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; // getter 和 setter 省略 }这段代码里几个关键点TableId(type IdType.AUTO)对应数据库自增主键TableField显式绑定字段名避免驼峰与下划线映射出错fill FieldFill.INSERT配合 MetaObjectHandler 实现创建时间自动填充TableLogic标记逻辑删除字段。根据这个实体类对应的建表 SQL 如下CREATE TABLE flower_product ( id bigint(20) NOT NULL AUTO_INCREMENT, product_name varchar(128) NOT NULL COMMENT 鲜花名称, category_id bigint(20) NOT NULL COMMENT 分类ID, price decimal(10,2) NOT NULL COMMENT 售价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, cover_img varchar(255) DEFAULT NULL COMMENT 封面图路径, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT鲜花商品表;参数说明decimal(10,2)保证金额精度别用 floatutf8mb4支持 emoji鲜花名称里可能有特殊符号idx_category加速按分类筛选。逻辑删除字段用tinyint(1)而不是 booleanMySQL 里更直观。2.3 工程目录结构与依赖配置一个清晰的目录结构能让你的代码在答辩时加分。我一般这样组织flower-mall ├── src/main/java/com/example/flower │ ├── controller │ ├── service │ │ └── impl │ ├── mapper │ ├── entity │ ├── config │ └── common ├── src/main/resources │ ├── application.yml │ ├── mapper │ └── static └── pom.xmlpom.xml里核心依赖就四个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok。别引入一堆用不上的 starter启动慢还容易冲突。application.yml里重点配好数据源和 MyBatis-Plus 的日志spring: datasource: url: jdbc:mysql://localhost:3306/flower_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver 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: 0serverTimezoneAsia/Shanghai是必须的否则 MySQL 8.0 会报时区错误。log-impl打开后控制台会打印执行的 SQL调试时非常有用上线前记得关掉。3. 核心业务模块落地从商品浏览到下单支付3.1 商品列表与分类查询接口鲜花商城的首页需要展示商品列表支持按分类筛选和关键词搜索。用 MyBatis-Plus 的分页插件几行代码就能搞定RestController RequestMapping(/api/product) public class ProductController { Autowired private FlowerProductService productService; GetMapping(/page) public ResultIPageFlowerProduct page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { LambdaQueryWrapperFlowerProduct wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, FlowerProduct::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), FlowerProduct::getProductName, keyword) .orderByDesc(FlowerProduct::getCreateTime); return Result.success(productService.page(new Page(pageNum, pageSize), wrapper)); } }逻辑说明LambdaQueryWrapper用方法引用代替字符串字段名重构时不会漏改。eq和like的第一个参数是条件布尔值只有条件成立才拼接 SQL这样避免了写多个 if 分支。分页插件需要在配置类里注册MybatisPlusInterceptor否则page方法不会真正分页。参数说明pageNum从 1 开始pageSize建议限制最大值防止恶意请求。categoryId和keyword都是可选参数前端传空字符串时StringUtils.hasText会返回 false条件不生效。3.2 购物车与订单创建的数据库设计购物车和订单是电商系统最容易出问题的地方。我的血泪经验是购物车表不要存商品快照只存商品 ID 和数量订单表必须存快照因为商品价格和名称可能随时变。下面是两张核心表CREATE TABLE cart_item ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, product_id bigint(20) NOT NULL, quantity int(11) NOT NULL DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表; CREATE TABLE flower_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, status tinyint(2) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name varchar(64) NOT NULL, receiver_phone varchar(20) NOT NULL, receiver_address varchar(255) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL, product_id bigint(20) NOT NULL, product_name varchar(128) NOT NULL COMMENT 下单时名称快照, product_price decimal(10,2) NOT NULL COMMENT 下单时价格快照, quantity int(11) NOT NULL, cover_img varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;cart_item表加了uk_user_product唯一索引同一用户同一商品只留一条记录加购时用INSERT ... ON DUPLICATE KEY UPDATE更新数量。order_item表冗余了商品名称、价格和封面图这就是快照订单生成后商品改价不影响历史订单。3.3 下单接口的事务与库存扣减下单是最容易出并发问题的地方。用户点「提交订单」时需要同时做三件事扣库存、创建订单、清空购物车。这三步必须在一个事务里否则会出现库存扣了订单没生成或者订单生成了购物车没清空的情况。Service public class OrderServiceImpl implements OrderService { Autowired private FlowerProductMapper productMapper; Autowired private FlowerOrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Autowired private CartItemMapper cartItemMapper; Override Transactional(rollbackFor Exception.class) public String createOrder(Long userId, ListLong cartItemIds) { ListCartItem cartItems cartItemMapper.selectBatchIds(cartItemIds); if (CollectionUtils.isEmpty(cartItems)) { throw new BusinessException(购物车为空); } BigDecimal total BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItem cart : cartItems) { // 悲观锁扣库存防止超卖 FlowerProduct product productMapper.selectForUpdate(cart.getProductId()); if (product.getStock() cart.getQuantity()) { throw new BusinessException(product.getProductName() 库存不足); } product.setStock(product.getStock() - cart.getQuantity()); productMapper.updateById(product); total total.add(product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); OrderItem item new OrderItem(); item.setProductId(product.getId()); item.setProductName(product.getProductName()); item.setProductPrice(product.getPrice()); item.setQuantity(cart.getQuantity()); item.setCoverImg(product.getCoverImg()); orderItems.add(item); } FlowerOrder order new FlowerOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); for (OrderItem item : orderItems) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } cartItemMapper.deleteBatchIds(cartItemIds); return order.getOrderNo(); } }逻辑说明Transactional(rollbackFor Exception.class)保证任何异常都回滚默认只回滚 RuntimeException加上rollbackFor更保险。selectForUpdate是自定义 Mapper 方法对应SELECT ... FOR UPDATE用行锁防止并发超卖。订单号用时间戳加随机数生成别用自增 ID 直接暴露。参数说明cartItemIds是前端传来的购物车记录 ID 列表不是商品 ID。库存扣减用悲观锁在毕设场景下足够真实高并发场景会改用 Redis 预扣减或乐观锁版本号。4. 避坑与排查鲜花商城系统最常见的 5 个翻车点4.1 启动报时区错误或驱动类找不到现象启动时抛The server time zone value ???ú±ê×??± is unrecognized或Loading class com.mysql.jdbc.Driver警告。原因MySQL 8.0 默认时区是 UTC且驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。解决JDBC URL 加上serverTimezoneAsia/Shanghai驱动类名用com.mysql.cj.jdbc.Driver。如果还报错检查mysql-connector-java版本是否与 MySQL 服务端匹配8.0 服务端用 8.0.x 驱动。4.2 MyBatis-Plus 分页不生效现象调分页接口返回全部数据total和pages都是 0。原因没有注册分页插件MyBatis-Plus 默认不分页。解决在配置类里加一个 BeanConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意DbType.MYSQL要和你实际用的数据库一致用错数据库类型分页 SQL 会拼错。4.3 逻辑删除后唯一索引冲突现象删除一个商品后想重新添加同名商品报唯一索引冲突。原因逻辑删除只是把deleted置为 1记录还在表里唯一索引仍然生效。解决要么唯一索引加上deleted字段组成联合唯一索引要么删除时把唯一字段改名比如加时间戳后缀。我一般用联合唯一索引建表时写成UNIQUE KEY uk_name_deleted (product_name, deleted)但这样只能支持一次删除多次删除还是会冲突。更彻底的做法是物理删除加归档表毕设场景下用联合索引就够了。4.4 前端跨域请求被拦截现象前端调后端接口报Access to XMLHttpRequest has been blocked by CORS policy。原因前后端分离部署时浏览器同源策略拦截了跨域请求。解决后端加全局跨域配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别前者支持通配符且允许携带凭证后者在allowCredentials(true)时不能用*。4.5 订单金额用 double 导致精度丢失现象订单总金额出现0.30000000000000004这种数字。原因double 和 float 是浮点数无法精确表示十进制小数。解决金额字段一律用BigDecimal数据库用decimal(10,2)。运算时用BigDecimal.add()、multiply()不要用和*。比较金额用compareTo()而不是equals()因为equals()会比较精度。5. 进阶技巧用 AI 辅助生成实体类和建表脚本5.1 让 AI 根据需求描述生成实体类现在用 AI 辅助开发已经很普遍了。我的习惯是先把表结构用自然语言描述清楚让 AI 生成实体类再人工核对注解和字段类型。比如这样提问「生成一个 Java 实体类对应 MySQL 表 flower_category字段有 id 自增主键、category_name 分类名称、sort_order 排序、create_time 创建时间用 MyBatis-Plus 注解。」AI 生成的代码基本可用但要注意检查TableField的字段名映射和IdType是否正确。5.2 实体类到建表 SQL 的自动化转换思路前面提到 MyBatis-Plus 不直接支持根据实体类生成建表 SQL但可以自己写一个简单的工具类通过反射读取注解拼接 CREATE TABLE 语句。核心思路是遍历实体类的所有字段读取TableField的 value 作为列名根据 Java 类型映射到 MySQL 类型再读取TableId判断主键。这个工具类不需要太完善能覆盖你项目里的实体就行。public class SqlGenerator { public static String generate(Class? clazz) { TableName tableName clazz.getAnnotation(TableName.class); StringBuilder sql new StringBuilder(CREATE TABLE ) .append(tableName.value()).append( (\n); for (Field field : clazz.getDeclaredFields()) { TableField tf field.getAnnotation(TableField.class); TableId tid field.getAnnotation(TableId.class); if (tf null tid null) continue; String column tf ! null ? tf.value() : field.getName(); String type mapType(field.getType()); sql.append( ).append(column).append( ).append(type); if (tid ! null) { sql.append( NOT NULL AUTO_INCREMENT); } sql.append(,\n); } sql.append( PRIMARY KEY (id)\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sql.toString(); } private static String mapType(Class? type) { if (type Long.class) return bigint(20); if (type Integer.class) return int(11); if (type String.class) return varchar(255); if (type BigDecimal.class) return decimal(10,2); if (type LocalDateTime.class) return datetime; return varchar(255); } }这个工具类的价值在于当你改了实体类字段重新跑一遍就能生成新的建表 SQL不用手动同步。注意它只处理简单类型枚举、集合、自定义对象需要额外扩展。生成后一定要人工检查特别是字段长度和索引AI 和工具都替代不了你对业务的理解。5.3 验证系统完整性的三个检查点系统写完后别急着答辩先过这三关。第一关冷启动测试。把数据库删掉重新执行建表 SQL启动项目看能否正常注册、登录、浏览商品。第二关边界测试。库存设为 1两个用户同时下单看是否只有一个成功。第三关异常测试。下单过程中手动断开数据库连接看事务是否回滚购物车是否还在。我自己的习惯是每次改完代码先跑一遍下单流程再跑一遍后台商品管理最后检查日志里有没有慢 SQL。这套系统不难难的是细节。把每个字段的类型、每个接口的参数、每个异常的分支都想清楚答辩时老师问什么你都能接住。希望帮到你。本文还有配套的精品资源点击获取