
简介这套基于 Java 的花店管理系统源码与数据库包面向 JavaWeb 初学者及课程设计人群可用于快速搭建包含前台展示与后台管理的花店业务原型。压缩包共 98 个文件以 27 个 Java 源文件、16 个 JSP 页面及 CSS、JS 等前端资源为主并配有 21 张 jpg 示例图片辅助页面效果预览、数据库相关 SQL 及 XML 配置文件整体约 2.56MB结构较完整。项目涵盖登录鉴权、用户管理、订单管理、花材数据维护等典型模块前台页面与后台管理分离Java 类负责业务逻辑JSP/CSS/JS 负责页面展示与交互源码分层清晰便于理解 Servlet/JSP 开发流程同时附带数据库文件与项目配置文件下载后即可导入 IDE 运行调试适合毕业设计、课程实训或入门实战参考。目前已有 636 人学习与下载是快速上手店铺管理类项目的典型实用案例。1. 别急着解压运行这份 Java 花店管理系统源码到底在讲什么拿到「基于java的花店管理系统源码数据库.zip」这类压缩包第一反应通常是解压、导入 IDEA、配好数据库、点运行。其实这份源码真正的价值是把一套典型 Java Web 全栈流程压缩在「卖花」这个场景里前台下单、后台管库、订单流转、客户档案。花店看着简单订单、库存和花材批次之间的联动却恰好覆盖了增删改查之外的事务、外键和统计问题这也是数据库课程设计常拿它当练手对象的原因。对刚过完 java 基础语法的人它补上了从配置到部署的完整链路对做数据库课程设计的学生现成的 ER 图和 SQL 脚本是很好的起步材料对老手真正值得看的是库存扣减和订单状态如何落表。后面按「先定表结构和环境、再写业务、最后加固」的顺序展开这也是此类管理系统最常见的开发顺序解压后按这个顺序走能少踩一半的坑。2. 选型与表设计花店管理系统的数据库先行在一套 Java 花店管理系统里技术栈的选型决定了 zip 解压后你看到的是 War 包工程还是 Spring Boot 工程。早年课程设计常见 JSP Servlet JDBC Tomcat 的搭配优点是每行代码都能看见请求怎么进来、连接怎么打开近五年则基本转向 Spring Boot MyBatis MySQL原因很实际配置收敛到 yml、SQL 与 Java 分离、单体应用够用且配套资料最多。如果你拿到的是老式 JSP 工程不要急着换框架先把数据库脚本导入确认表结构合理再决定是否用 MyBatis 重写数据访问层。2.1 花店核心实体拆分从花材到订单的六张表凡是涉及「销售 库存」的系统表设计的第一原则是「订单与库存分离金额与数量分离」。花店管理系统最少需要六张业务表花材表flower、花材分类表category、库存表stock、客户表customer、订单主表orders、订单明细表order_item。再加一张供应商表supplier和采购表purchase就能支持进货场景。企业里常见的设计还会拆出花束组合表因为「一束花 多枝花 包装耗材」这里先不展开避免把表结构撑得太散。其中最容易出问题的是库存表的设计。很多学生会把库存数量直接放在花材表里下单就UPDATE flower SET stock stock - 1。这种做法在只有一台机器、没有并发的时候没问题一旦两个订单同时买同一款花后提交的更新会覆盖先提交的数量库存就变成负数。标准做法是单独建stock表按flower_id一对一定义quantity和safety_stock并在更新时用带条件的 UPDATE 来做减法控制。订单主表和订单明细表的拆分是「一主多从」的经典结构。主表存订单号、客户 ID、订单状态、总金额、创建时间明细表存每一行花材的 ID、单价、数量、小计金额。注意明细表必须冗余「单价快照」不能下单后去关联花材表的当前售价——花店促销频繁两个月前的订单明细如果跟着花材表变价对账就没法做了。2.1.1 六张表的 MySQL 建表脚本以下脚本使用 MySQL 8.0存储引擎 InnoDB字符集 utf8mb4。utf8mb4 比 utf8 多覆盖了 emoji 字符花店备注字段里客户经常发 这类符号用 utf8 会直接报错或乱码这是数据库课程设计里最高频的坑之一。-- 花材分类表 CREATE TABLE category ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL COMMENT 分类名称, parent_id BIGINT DEFAULT NULL COMMENT 父分类ID0表示顶级, sort_order INT DEFAULT 0 COMMENT 排序值越小越靠前, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_parent (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT花材分类表; -- 花材表 CREATE TABLE flower ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 花材名称, category_id BIGINT NOT NULL COMMENT 所属分类, unit VARCHAR(10) DEFAULT 枝 COMMENT 销售单位, price DECIMAL(10,2) NOT NULL COMMENT 当前售价, cost_price DECIMAL(10,2) NOT NULL COMMENT 成本价, image_url VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), CONSTRAINT fk_flower_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT花材表;逻辑说明花材表通过category_id外键关联分类表price和cost_price都用DECIMAL而不是FLOAT。FLOAT是近似存储做总价累计时会出现 0.1 加 0.2 不等于 0.3 的情况财务相关字段一律用DECIMAL(10,2)这是 Java 后端拿到BigDecimal而不是Double的根本原因。-- 库存表 CREATE TABLE stock ( flower_id BIGINT NOT NULL COMMENT 花材ID, quantity INT NOT NULL DEFAULT 0 COMMENT 当前可用库存, safety_stock INT NOT NULL DEFAULT 0 COMMENT 安全库存, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (flower_id), CONSTRAINT fk_stock_flower FOREIGN KEY (flower_id) REFERENCES flower(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存表; -- 客户表 CREATE TABLE customer ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL COMMENT 手机号登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt密文, address VARCHAR(200) DEFAULT NULL, level TINYINT DEFAULT 1 COMMENT 会员等级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表; -- 订单主表 CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号唯一, customer_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2配送中 3已完成 4已取消, remark VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_customer (customer_id), CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customer(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; -- 订单明细表 CREATE TABLE order_item ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, flower_id BIGINT NOT NULL, item_name VARCHAR(100) NOT NULL COMMENT 商品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;参数说明order_no用唯一索引而不是主键因为订单号是业务号可能包含门店编码和日期例如SH20250521001主键自增 ID 只对系统内部可见。order_item.item_name和price是冗余快照字段它们的值来自下单那一刻的花材信息之后花材改名、调价都不影响历史订单。这六张表的关系就是一张标准 ER 图category与flower是 1 对 Nflower与stock是 1 对 1customer与orders是 1 对 Norders与order_item是 1 对 N。表名关键字段是否冗余快照说明categoryname, parent_id否支持二级分类flowerprice, cost_price, status否售价随促销变动stockquantity, safety_stock否与 flower 一对一customerphone, password, level否手机号唯一orderstotal_amount, status主表金额可重算状态用数字order_itemitem_name, price是下单时快照不受调价影响2.2 为什么订单状态不用枚举字段直接存字符串订单状态这里用了 TINYINT 0-4。成长期的项目会倾向用 VARCHAR 存PAID、SHIPPING这类语义化字符串便于日志里直接看懂。但花店管理系统这种规模用数字状态位的好处是status字段的排序、分组和索引都很轻配合 Java 端的枚举类就能兼顾可读性。public enum OrderStatus { UNPAID(0, 待付款), PAID(1, 已付款), DELIVERING(2, 配送中), FINISHED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } public static OrderStatus of(int code) { for (OrderStatus s : values()) { if (s.code code) return s; } throw new IllegalArgumentException(未知订单状态: code); } }逻辑说明数据库存0Java 层通过OrderStatus.of(0)取出枚举再拿desc渲染页面既不影响 SQL 的WHERE status 1查询也不会在代码里散落魔法数字。状态流转的合法性建议单独写校验不允许从「待付款」直接跳到「已完成」这类状态机校验是 java 面试题里经常追问的点源码里如果没实现可以自己补上。2.3 供应商与采购表别把花材表当成万能表在订单明细冗余了单价快照之后另一个常见问题是把供应商和采购信息全部塞进花材表。一个花店可能有三个供应商都在供玫瑰各自价格不同、到货批次不同。如果在flower表加supplier_id和purchase_price两个字段每次进货都要 UPDATE 花材行不但覆盖了历史采购价还让「这个月从谁家进了多少货」完全查不出来。正确做法是再加供应商表supplier(id, name, contact, phone)和采购表purchase(id, supplier_id, flower_id, quantity, price, purchase_time)。采购表每进一次货插一行price存这一批的实际进价花材表里的cost_price只是用于毛利估算的参考值。这套设计在花店管理系统里可能用不到但它是「业务对象」和「业务行为」分离的典型例子花材是静态档案采购是动态行为静态档案和动态行为混在一张表里半年后对账必出问题。到这里 zip 里的数据库脚本应该能看懂了。下一步先把这套结构在本地真正运行起来再去读业务代码顺序反了容易陷入「看半天没有任何反馈」的境地。3. 本地跑通从 zip 到能打开的花店管理系统Java 环境与数据库初始化拿到源码包后先别双击运行。花店管理系统这类 Java Web 项目绝大多数跑不起来不是因为代码错而是环境三件套不对齐JDK 主版本、Maven 依赖仓库、MySQL 的字符集和时区。先确认 Java 版本和构建工具版本再导数据库最后改配置启动每一步都能得到即时反馈出了问题也知道是哪一层。3.1 JDK、Maven 与 MySQL 版本对齐检查打开解压目录先找三个文件pom.xmlMaven 工程或build.gradlesrc/main/resources/application.yml或application.properties以及sql/或db/目录下的.sql脚本。在没有pom.xml的时候要认命这可能是 JSP Servlet 老工程运行方式会变成部署 War 包到 Tomcat这里先按 Spring Boot 工程讲。在pom.xml里看java.version和spring-boot-starter-parent的版本。常见组合是 Java 8 配 Spring Boot 2.x、Java 17 配 Spring Boot 3.x。Spring Boot 3 把javax.*包换成jakarta.*如果你的 JDK 是 17 却硬跑 Boot 2 的源码编译期会报程序包 javax.servlet 不存在。java -version mvn -v mysql --version逻辑说明这三条命令分别确认 JDK、Maven、MySQL 是否在 PATH 里。mvn -v输出里会带上它使用的 Java 版本如果 Maven 用的是 JDK 8 而工程要求 17可以在 IDEA 的 Settings 里把 Project SDK 和 Maven Runner 的 JDK 统一。MySQL 版本直接决定建立连接时用不用useSSLfalse参数8.0 默认开 SSL 认证老驱动连 5.7 没这个问题。3.1.1 初始化数据库的两种方式方式一命令行导入。先创建数据库再指定字符集导入脚本。mysql -uroot -p -e CREATE DATABASE flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p flower_shop sql/flower_shop.sql逻辑说明第一行建库时把utf8mb4和排序规则写死避免 MySQL 默认的latin1导致中文乱码第二行把 SQL 脚本导入。如果脚本里有CREATE DATABASE语句导入前要确认库名是否和你后续配置里的连接 URL 一致不一致可以直接改 URL不必重导。导入完成后执行SHOW TABLES;应该能看到六张以上业务表。方式二用 Navicat 或 IDEA 自带的 Database 面板图形化导入。Navicat 里右键「flower_shop」选「运行 SQL 文件」把脚本拖进去执行。对新手来说图形化更容易定位到具体哪一行 SQL 报错但注意 Navicat 默认会把DELIMITER相关的存储过程脚本处理得和你预期不完全一致纯表结构脚本没这个问题。图形化工具只负责执行真正决定编码的还是建库语句里的 CHARSET。3.2 改 application.yml 的三个关键参数连接串、账号密码、时区Spring Boot 工程启动前最必要的改动在application.yml或application.properties里。花店系统的源码只要改对数据源这四个参数大概率能直接启起来。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/flower_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mvc: hiddenmethod: filter: enabled: true参数说明serverTimezoneAsia/Shanghai解决 MySQL 8.0 与服务端时区不一致导致的 8 小时偏移问题allowPublicKeyRetrievaltrue配合useSSLfalse使用否则新版 MySQL 驱动用caching_sha2_password认证时连接会报Public Key Retrieval is not allowed。characterEncodingutf8这里虽然写的是utf8MySQL 驱动会按utf8mb4处理真正决定字符集的是数据库本身的 CHARSET。hiddenmethod.filter.enabled是为了让表单能提交 PUT/DELETE 请求很多旧系统的增删改查页面依赖这个过滤器少了它编辑花材时总是 405。改完后执行mvn spring-boot:run启动日志里看到Tomcat started on port(s): 8080就算成功。如果端口被占用在application.yml里加server.port: 8081换端口。启动失败的常见顺序是先报数据库连不上排除 URL 和密码再报Table flower_shop.xxx doesnt exist说明脚本没导入成功最后报Failed to configure a DataSource说明pom.xml里少了mybatis-spring-boot-starter或 JDBC 驱动的依赖。3.3 MyBatis 的 mapper 路径与驼峰映射Spring Boot MyBatis 最常见的启动报错是Invalid bound statement (not found)。原因是接口和 XML 文件没有配对。花店系统里通常有com.example.mapper.FlowerMapper接口和resources/mapper/FlowerMapper.xml。确保 yml 里配置了mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true参数说明mapper-locations告诉 MyBatis 去resources/mapper目录下找 XML 文件map-underscore-to-camel-case把数据库的create_time自动映射成 Java 属性createTime不配置的话查询结果里createTime全是 null这是老手也经常栽的隐性 bug。type-aliases-package让 XML 里可以直接写resultTypeFlower而不必写全限定类名。这三行配置能解决花店管理系统八成以上的查询异常。3.4 启动期报错排查清单报错内容直接原因处理方式Public Key Retrieval is not allowedMySQL 8 使用 caching_sha2_passwordURL 加allowPublicKeyRetrievaltrue和useSSLfalseInvalid bound statement (not found)Mapper 接口和 XML 未配对检查 mapper-locations 路径和 XML 里 namespaceTable flower_shop.xxx doesnt exist脚本未导入或连错库确认库名和 yml URL用 SHOW TABLES 验证Access denied for user rootlocalhost密码或 host 不对检查 yml 账号密码确认 MySQL 用户表的 hostPort 8080 was already in use端口冲突换server.port或杀掉占用进程程序包 javax.servlet 不存在Spring Boot 3 使用 jakarta 包名换 JDK 17 与 Boot 3 匹配或降级 Boot 2这张表基本覆盖了课程设计答辩前最容易预见的启动问题。如果你读的是 MyBatis 源码会知道上面的mapper-locations本身就是在构造XMLMapperBuilder时解析出来的资源路径数组路径写错时异常信息往往隐藏在「nested exception」里别只看最上面一行。4. 核心业务实现库存扣减、下单与统计Java 代码实战跑通之后真正决定一个花店管理系统能不能用的是三个人人都会问的业务点下单时库存怎么扣、总金额怎么算、月度报表怎么出。这一章同步给后端代码和 SQL你可以直接对照源码里对得上的那部分。4.1 下单主流程单方法内完成校验、扣库存、写订单下单的核心不是「INSERT 订单」而是「校验库存 扣减 生成主单和明细」必须在一个事务里完成。任何一步失败之前写的订单都要回滚。Spring 的做法是在 Service 方法上加Transactional。Service public class OrderService { private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; private final StockMapper stockMapper; public OrderService(OrderMapper orderMapper, OrderItemMapper orderItemMapper, StockMapper stockMapper) { this.orderMapper orderMapper; this.orderItemMapper orderItemMapper; this.stockMapper stockMapper; } Transactional(rollbackFor Exception.class) public String createOrder(Long customerId, ListCartItem items, String remark) { for (CartItem item : items) { int updated stockMapper.deductStock(item.getFlowerId(), item.getQuantity()); if (updated 0) { throw new BizException(库存不足或商品已下架: item.getFlowerId()); } } Order order new Order(); order.setOrderNo(OrderNoGenerator.next(SH)); order.setCustomerId(customerId); order.setStatus(OrderStatus.UNPAID.getCode()); order.setRemark(remark); order.setTotalAmount(calculateTotal(items)); orderMapper.insert(order); for (CartItem item : items) { OrderItem oi buildOrderItem(order.getId(), item); orderItemMapper.insert(oi); } return order.getOrderNo(); } }逻辑说明业务先扣库存、再写订单主表、最后写明细顺序不能反过来否则明细写成功但库存扣减失败时整个事务虽然回滚了日志里却会多出一次无意义的扣减尝试。stockMapper.deductStock返回受影响行数返回 0 就意味着库存不够这是一个原子操作下面看它的 SQL。注意事务里不要写远程调用和耗时的外部 IO锁会一直持有到方法结束下单接口超时往往会拖垮整个库。4.1.1 防超卖的 UPDATE 语句写法update iddeductStock UPDATE stock SET quantity quantity - #{quantity}, update_time NOW() WHERE flower_id #{flowerId} AND quantity #{quantity} /update逻辑说明关键在WHERE quantity #{quantity}这个条件。UPDATE 在 InnoDB 里默认对命中的行加排他锁两个并发请求同时改同一行时后到的会等前一个提交或回滚后再执行此时quantity已被减过条件不满足自然返回 0。这种「条件更新」比「先 SELECT 后 UPDATE」在并发下安全得多也避免了对 SELECT 加悲观锁带来的死锁风险。若要更精细还可以在flower表加version字段做乐观锁但对花店这种高并发并不存在的系统条件更新已经足够。4.2 花店销售统计按天、按月聚合的 SQL 写法统计模块是花店管理系统里最能看出 SQL 水平的部分。最朴素的写法是把所有订单查出来在 Java 里循环累加数据量到几千条就开始卡。正确做法是让 MySQL 完成聚合只回传结果集。-- 按月统计销售额和订单数不含已取消订单 SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS order_count, SUM(total_amount) AS sales_amount FROM orders WHERE status ! 4 AND create_time 2025-01-01 00:00:00 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month;参数说明DATE_FORMAT(create_time, %Y-%m)对创建时间做按月截断让 1 号到 31 号的订单归到同一组GROUP BY的字段要和 SELECT 里的表达式完全一致MySQL 的 ONLY_FULL_GROUP_BY 模式下写GROUP BY month这种别名会报错status ! 4排除取消单。如果还想看每一天的销量排行可以再写一条按flower_id分组、按销量倒序的明细聚合。聚合维度关键函数典型场景按天DATE_FORMAT(create_time, %Y-%m-%d)每日销售日报按月DATE_FORMAT(create_time, %Y-%m)月度对账按花材flower_id 分组 SUM(quantity)畅销排行与进货参考按客户customer_id 分组 COUNT(*)会员复购分析4.3 分页查询PageHelper 的参数边界花材列表和管理端订单列表都要分页。常见做法是用 MyBatis 的分页插件 PageHelper但它有一个人尽皆知的坑PageHelper.startPage()后面必须紧跟第一条查询语句中间不能有别的 SQL 或逻辑判断。public PageInfoFlower pageFlower(String keyword, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListFlower list flowerMapper.queryByKeyword(keyword); return new PageInfo(list); }逻辑说明startPage会修改 ThreadLocal 里的分页参数MyBatis 执行下一条查询时自动追加LIMIT查询完成后清理。如果你在startPage和queryByKeyword之间做了日志查询或其他 Mapper 调用分页参数可能作用到错误的查询上。pageSize也要做上限保护超过 100 强制截断避免有人恶意传pageSize999999直接拖全表。提示分页参数 ThreadLocal 的清理依赖 MyBatis 插件拦截器如果配置了多个拦截器注意PageInterceptor的执行顺序要排在自定义拦截器之后否则分页 SQL 可能生成错误。还有一个老生常谈的细节分页查询的 count 语句在数据量大时会单独执行一次SELECT COUNT(*)花店系统里如果订单表有几十万行这个 count 会成为慢查询。可以在queryByKeyword的 XML 里用resultTypelong的独立 count 查询或者接受 PageHelper 自动生成的 count前提是 where 条件上有合适索引。4.4 订单取消与库存回补别在 Service 里散落 UPDATE订单取消是花店管理系统里非常高频的操作客户下单后悔、花材当天售罄都要走取消流程。如果每个 Controller 里都写一次stockMapper.rollbackStock时间一长各种状态遗漏就会冒出来。正确做法是把「取消订单」收敛成 Service 里的一个方法先校验当前状态再回补库存再更新状态同样在一个事务里。Transactional(rollbackFor Exception.class) public void cancelOrder(Long orderId) { Order order orderMapper.selectById(orderId); if (order null || order.getStatus() ! OrderStatus.UNPAID.getCode()) { throw new BizException(当前状态不可取消); } ListOrderItem items orderItemMapper.selectByOrderId(orderId); for (OrderItem item : items) { stockMapper.increaseStock(item.getFlowerId(), item.getQuantity()); } orderMapper.updateStatus(orderId, OrderStatus.CANCELLED.getCode()); }逻辑说明先查状态再回补库存回补用的是quantity quantity #{quantity}天然幂等配合 UPDATE 的影响行数可以排查重复取消。取消已发货订单时状态要从「配送中」流转到「已取消」可能需要财务确认这类复杂状态机在花店场景里通常不实现但代码结构上给它留一个独立方法后面要加审核流程时改动最小。5. 进阶验证与加固让花店系统从「能跑」到「能交付」到这里系统能跑、能下单、能出报表。但如果这份源码要作为课程设计答辩或入职作品展示还有三个层面的工作值得做验证、安全、可维护性。5.1 五分钟冒烟测试下单、扣库存、回滚各验证一次冒烟测试三个用例正常下单、库存不足、订单取消后库存回补。# 查看某花材初始库存 mysql -uroot -p flower_shop -e SELECT flower_id, quantity FROM stock WHERE flower_id 1; # 通过接口下单 curl -X POST http://localhost:8080/api/order \ -H Content-Type: application/json \ -d {customerId:1,items:[{flowerId:1,quantity:2}]}验证逻辑下单后再次查库存quantity应减少 2同时orders表和order_item表各新增一条记录。接着把quantity传到超过当前库存的数接口应返回业务异常而不是 500 错误并且数据库里没有残留的订单半成品。库存不足时的回滚验证最能体现Transactional是否生效如果发现主表插入了订单但明细表是空的说明事务没生效或 MyBatis 插入用了 autocommit。5.2 三个安全加固点参数化查询、密码存储、越权校验第一所有 Mapper XML 里的${}都要改成#{}。${}直接拼接字符串#{userName}预编译成占位符可以防 SQL 注入。花店管理系统的登录查询如果写成WHERE phone ${phone}输入 OR 11就能绕过密码校验这是最容易被答辩老师点名的漏洞。第二客户密码不能明文存。数据库脚本里password字段长度 100就是给 BCrypt 密文准备的。登录校验用BCryptPasswordEncoder.matches(rawPassword, encodedPassword)不要自己写 MD5。String rawPassword flower123; String encoded new BCryptPasswordEncoder().encode(rawPassword); boolean ok new BCryptPasswordEncoder().matches(rawPassword, encoded);第三后台管理接口要做登录状态校验不能只靠前端按钮隐藏。实际项目里一般用拦截器或 Spring Security 拦截/api/admin/**路径没带 token 或 session 的直接返回 401。花店这种场景不需要上 OAuth 2.0一个基于 session 的拦截器就够。5.3 库表大小与索引给订单表加两个最常用的复合索引订单列表页最常用的查询条件是「客户 时间」和「状态 时间」。orders表上除了主键和uk_order_no建议再补两个复合索引ALTER TABLE orders ADD INDEX idx_customer_time (customer_id, create_time); ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);逻辑说明idx_customer_time支持「查某个客户的历史订单」并按时间倒序idx_status_time支持「按状态筛选待处理订单」。为什么不能只建单列索引因为 MySQL 8.0 里WHERE customer_id ? ORDER BY create_time DESC用复合索引可以直接走索引排序避免 filesort。索引不是越多越好订单表的索引控制在 5 个以内写多读少的业务更要注意更新索引的成本。最后给一个通用技巧理解一份花店管理系统源码关键是先画 ER 图再读代码。把六张表的关系画出来昨天看不懂的 Service 代码今天大概率能看懂——因为所有逻辑都在围着这些表转。本文还有配套的精品资源点击获取