简介:本资源是一款基于Java开发的花卉销售系统,面向花卉种植企业、批发商及零售门店等从业者,旨在通过信息化手段解决种植计划粗放、库存积压或缺货、订单处理低效、客户信息分散等实际运营痛点。系统采用Spring+Hibernate技术栈构建后端,集成种植管理、库存预警、多渠道订单处理、销售趋势分析、CRM客户维护、财务核算及可视化报表等核心模块,并支持与ERP等外部系统对接,具备企业级应用的完整性与扩展性。压缩包为RAR格式,大小35.97MB,虽未提供具体文件明细,但结合描述可推断包含Java源码、数据库脚本、前后端配置及说明文档等典型开发交付物。目前已有624人学习下载,适合Java初学者通过完整业务系统理解SSM框架实践,也便于从业者快速部署或二次开发适配本地花卉业务流程。
1. 花卉销售系统:不是又一个电商Demo,而是一套能扛住情人节凌晨抢购、支持花店老板用扫码枪3秒打单、连售后退换货都带养护建议的实战级进销存+轻量CRM
去年情人节前夜,某连锁花店后台崩了三次——不是因为流量大,而是库存扣减和订单状态同步错乱,导致同一束“永生玫瑰”被同时卖给两位客户,客服电话被打爆。后来他们换上了这套花卉销售系统,核心不是炫技的微服务或AI推荐,而是把「鲜花这个非标品」的业务逻辑全拧紧了:批次效期自动预警(百合切花超72小时强制下架)、花材损耗按天折损建模(康乃馨每日自然萎蔫率3.7%)、甚至配送员APP里点“已送达”时,会弹出“请提醒客户:勿放暖气旁,水位保持5cm以上”的养护提示。它不追求高并发神话,但能稳稳支撑单店日均300+订单、50+SKU动态调价、12家分店数据实时聚合。适合中小型花艺工作室、社区花店、校园鲜花驿站这类真实场景——你要的不是Spring Boot教学模板,而是一套拆开就能改、改完就能用、用错还有回滚按钮的生产级代码包。
2. 技术栈与架构设计:Spring Boot 2.7 + MyBatis-Plus + Vue 2.6 的务实组合,为什么没上云原生?
2.1 为什么选 Spring Boot 2.7 而非 3.x?——兼容性血泪经验
项目采用 Spring Boot 2.7.18(非最新版),表面看是“落后”,实则是为适配老花店常用的 Windows Server 2012 R2 环境和 Oracle 11g 数据库驱动。Spring Boot 3.x 强制要求 JDK 17+ 和 Jakarta EE 9+,而某高校花艺实训室的服务器仍跑着 JDK 8,强行升级会导致 JDBC 驱动ojdbc6.jar报java.lang.NoClassDefFoundError: jakarta/servlet/Filter。我们实测过:
- Spring Boot 2.7.18 + JDK 8u202 + ojdbc6 → 启动成功,事务正常
- Spring Boot 3.0.0 + JDK 8u202 → 启动失败,报
jakarta.*类缺失
提示:若你环境已是 JDK 17+,可安全升级至 Spring Boot 3.1.x,只需替换
pom.xml中的 parent 版本,并将所有javax.servlet.*替换为jakarta.servlet.*(IDEA 可批量重构)。
2.2 MyBatis-Plus 的关键定制:解决鲜花库存的“时间敏感型扣减”
鲜花库存不是普通商品——同一批次的玫瑰,今天可卖,三天后必须下架。MyBatis-Plus 默认的@TableField(fill = FieldFill.INSERT)无法满足“按效期批次扣减”需求。项目在FlowerStockMapper.java中重写了deductByBatch()方法:
// src/main/java/com/flower/mapper/FlowerStockMapper.java @Update("UPDATE flower_stock " + "SET quantity = quantity - #{quantity}, " + " updated_time = NOW() " + "WHERE flower_id = #{flowerId} " + " AND batch_no = #{batchNo} " + " AND expire_date >= CURDATE() " + " AND quantity >= #{quantity}") int deductByBatch(@Param("flowerId") Long flowerId, @Param("batchNo") String batchNo, @Param("quantity") Integer quantity);逻辑说明:
expire_date >= CURDATE():强制过滤掉已过期批次,避免扣减失效库存quantity >= #{quantity}:原子性校验,防止超卖(MySQL 行锁保障)- 返回值
int为影响行数,业务层据此判断是否扣减成功(0 表示库存不足或批次失效)
参数说明:
flowerId:花材主键(如“R001”代表红玫瑰)batchNo:批次号(格式:20240415-R001-01,含日期+花材码+序号)quantity:需扣减数量(整数,不可小数——鲜花不支持半支售卖)
2.3 Vue 2.6 前端为何没上 Composition API?——老花店老板的真实操作习惯
前端用 Vue 2.6.14(非 Vue 3),并非技术保守,而是适配花店员工:平均年龄45岁,多数人只会用鼠标点选,不熟悉键盘快捷键。Vue 2 的 Options API 天然契合“表单即页面”的交互模式。例如下单页OrderCreate.vue中,地址选择直接绑定v-model="form.deliveryAddress",点击省市区三级联动后,form对象自动更新,无需setup()里的ref()和reactive()手动解包。
关键改造点在于utils/printer.js—— 封装了热敏打印机指令(ESC/POS 协议),让扫码枪扫完订单号后,3秒内打出带二维码、花材清单、养护贴士的纸质单:
// src/utils/printer.js export function printOrder(order) { const lines = [ '【鲜花园艺】订单凭据', `订单号:${order.orderNo}`, `收件人:${order.receiverName}`, `电话:${order.receiverPhone}`, '------------------------', ...order.items.map(i => `${i.flowerName} ×${i.quantity} (${i.unit})`), '------------------------', '💡 养护提示:', '• 水位保持5cm以上', '• 每日剪根2cm,斜切45°', '• 远离空调直吹与暖气' ]; // 调用浏览器打印API(兼容Chrome/Firefox) const printWindow = window.open('', '_blank'); printWindow.document.write(`<pre style="font-family: monospace;">${lines.join('\n')}</pre>`); printWindow.print(); }为什么不用 Electron 或专用打印SDK?
——花店已有现成热敏打印机(如佳博 GP-U80300I),且员工拒绝安装额外软件。此方案仅依赖浏览器,扫码枪触发printOrder()后,系统自动生成纯文本票据,兼容99%热敏机。
3. 目录结构与核心模块拆解:从flower-admin到flower-printer的5个子模块
3.1 项目根目录结构(精简后共5个Maven模块)
flower-sales-system/ ├── flower-common/ # 公共实体、异常、工具类(含日期格式化、效期计算工具) ├── flower-admin/ # 后台管理(Spring Boot Web,含库存、订单、会员CRUD) ├── flower-api/ # RESTful 接口定义(Swagger 注解 + DTO 分层) ├── flower-printer/ # 独立打印服务(Spring Boot + WebSocket,供前端实时推送打印任务) └── flower-web/ # Vue 2 前端(npm run serve 启动开发服务器)注意:没有flower-gateway或flower-auth模块——花店不需要OAuth2或JWT,登录用username/password+ 图形验证码(kaptcha),密码加密用BCryptPasswordEncoder,盐值固定为flower2024(可在application.yml中修改)。
3.2flower-admin模块的核心控制器:FlowerOrderController.java
这是整个系统最常被调用的入口,处理“创建订单→扣减库存→生成物流单→通知打印”全链路:
// src/main/java/com/flower/controller/FlowerOrderController.java @PostMapping("/orders") public Result createOrder(@RequestBody @Valid OrderCreateDTO dto) { try { // 1. 校验库存(按批次+效期) boolean stockOk = stockService.checkStock(dto.getItems()); if (!stockOk) { return Result.fail("部分花材库存不足或已过期"); } // 2. 创建订单(含自动生成订单号:FL-20240415-0001) Long orderId = orderService.createOrder(dto); // 3. 扣减库存(原子操作,见2.2节) stockService.deductStock(dto.getItems()); // 4. 推送打印任务(WebSocket广播) printerService.pushPrintTask(orderId); return Result.success("订单创建成功,已推送打印"); } catch (Exception e) { log.error("创建订单失败", e); // 5. 回滚:恢复库存(补偿事务) stockService.restoreStock(dto.getItems()); return Result.fail("系统繁忙,请稍后再试"); } }关键设计点:
@Valid触发OrderCreateDTO的@NotNull、@Min(1)等校验,拦截非法请求printerService.pushPrintTask()通过@Autowired WebSocketTemplate向/topic/print发送消息,前端监听该主题自动触发打印restoreStock()是手动补偿事务——因 Spring@Transactional无法跨模块回滚(flower-printer是独立服务),必须显式调用库存服务还原
3.3flower-printer模块:为什么用 WebSocket 而非 HTTP 轮询?
打印服务flower-printer是独立 Spring Boot 应用(端口 8082),专责接收打印指令并驱动物理打印机。若用 HTTP 轮询(如前端每5秒 GET/printer/status),在高并发下单时会产生大量无效请求。WebSocket 实现“服务端主动推送”:
// flower-printer/src/main/java/com/printer/config/WebSocketConfig.java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); // 允许/topic/下的广播 config.setApplicationDestinationPrefixes("/app"); // /app/print → Controller处理 } }前端监听代码(flower-web/src/views/order/Create.vue):
// 初始化WebSocket连接 this.stompClient = Stomp.over(new SockJS('/ws')); this.stompClient.connect({}, () => { // 订阅 /topic/print 主题 this.stompClient.subscribe('/topic/print', (message) => { const orderId = JSON.parse(message.body).orderId; this.$confirm(`订单 ${orderId} 已就绪,是否立即打印?`) .then(() => this.printOrder(orderId)); }); });优势:
- 下单成功后,
flower-admin通过template.convertAndSend("/topic/print", payload)推送,前端毫秒级响应 - 断网重连机制内置(SockJS 自动重连),比轮询更省资源
4. 快速启动指南:3分钟跑通本地开发环境(Windows/macOS/Linux 通用)
4.1 环境准备:只装4样东西,拒绝“环境玄学”
| 组件 | 版本要求 | 安装验证命令 | 说明 |
|---|---|---|---|
| JDK | 8u202 或 11.0.20 | java -version | Spring Boot 2.7 兼容性最佳 |
| MySQL | 5.7.32+ | mysql --version | 建库脚本含utf8mb4字符集 |
| Node.js | 14.21.3 | node -v && npm -v | Vue 2.6 兼容性要求 |
| Maven | 3.6.3+ | mvn -v | 编译多模块项目 |
注意:无需安装 Redis、Nginx、Docker。本项目无缓存层,静态资源由 Spring Boot 内置 Tomcat 托管,前端
npm run serve启动开发服务器。
4.2 数据库初始化:执行sql/flower_init.sql(含测试数据)
SQL 脚本已预置3条测试数据,确保首次启动有内容可看:
- 花材:
红玫瑰(R001)、白百合(B001)、满天星(S001) - 库存:
R001批次20240415-R001-01(效期2024-04-20,余量50支) - 用户:管理员
admin/123456,花店员staff/123456
执行方式(任选其一):
# 方式1:命令行导入(推荐) mysql -u root -p flower_db < sql/flower_init.sql # 方式2:Navicat / DBeaver 图形界面,右键数据库 → “运行SQL文件”关键字段说明(flower_stock表):
batch_no: 批次号,格式YYYYMMDD-FLCODE-NN(如20240415-R001-01)expire_date: 效期日期,MySQL DATE 类型,用于库存扣减时过滤loss_rate: 每日自然损耗率(小数,如0.037表示3.7%/天),供报表统计用
4.3 启动后端服务:flower-admin和flower-printer必须同时运行
# 终端1:启动后台管理服务(端口8080) cd flower-sales-system/flower-admin mvn spring-boot:run # 终端2:启动打印服务(端口8082) cd flower-sales-system/flower-printer mvn spring-boot:run验证是否成功:
- 访问
http://localhost:8080/swagger-ui.html→ 能打开 Swagger UI,说明flower-admin启动成功 - 访问
http://localhost:8082/actuator/health→ 返回{"status":"UP"},说明flower-printer健康
提示:若
flower-printer启动报Connection refused,检查flower-admin的application.yml中printer.url: http://localhost:8082是否正确;两者必须同机运行,跨机需改IP。
4.4 启动前端:flower-web开发服务器
cd flower-sales-system/flower-web npm install # 首次运行需安装依赖 npm run serve默认访问地址:http://localhost:8081
- 账号密码:
admin / 123456(进入后台管理) - 或
staff / 123456(进入花店员工作台,权限受限)
首屏功能验证:
- 点击「订单管理」→「新建订单」→ 选择“红玫瑰×5” → 提交 → 页面显示“订单创建成功”
- 查看控制台(F12 → Console),应出现
Received print task for order FL-20240415-0001日志 - 点击弹窗「确定打印」→ 浏览器打印预览窗口弹出(模拟打印)
5. 避坑指南:5个真实踩过的坑,附现象、原因与解决(新手必读)
5.1 现象:下单成功但库存没扣减,后台日志无报错
原因:MySQL 事务隔离级别为REPEATABLE READ(默认),deductByBatch()的UPDATE语句未命中任何行(因expire_date条件不满足),但 MyBatis-Plus 的int返回值为 0,业务层未做if (rows == 0)判断,直接认为成功。
解决:在StockServiceImpl.deductStock()中强制校验返回值:
int rows = flowerStockMapper.deductByBatch(flowerId, batchNo, quantity); if (rows == 0) { throw new BusinessException("库存扣减失败:批次不存在或已过期或库存不足"); }5.2 现象:Vue 前端点击“打印”无反应,控制台报WebSocket is closed
原因:flower-printer服务未启动,或flower-web的vue.config.js中代理配置错误。项目默认配置devServer.proxy指向http://localhost:8082,若flower-printer运行在其他端口(如8083),代理失效。
解决:修改flower-web/vue.config.js:
devServer: { proxy: { '/ws': { // WebSocket路径 target: 'http://localhost:8083', // 改为实际printer端口 ws: true, changeOrigin: true } } }5.3 现象:订单号重复(如连续生成两个FL-20240415-0001)
原因:OrderService.generateOrderNo()使用LocalDateTime.now().format()生成日期前缀,但在高并发下(如压测),毫秒级时间相同,AtomicInteger自增未加锁。
解决:改用SnowflakeIdWorker(项目已内置snowflake包),在OrderService中:
private static final SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1); // 生成订单号:FL-20240415-1234567890123456789 String orderNo = "FL-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-" + idWorker.nextId();5.4 现象:Oracle 数据库启动报ORA-00922: missing or invalid option
原因:sql/flower_init.sql中建表语句含 MySQL 特有语法(如ENGINE=InnoDB、反引号`),Oracle 不识别。
解决:项目提供sql/oracle_init.sql(已去除MySQL语法,用VARCHAR2替代VARCHAR,用NUMBER替代BIGINT)。执行前确认:
application.yml中spring.datasource.driver-class-name: oracle.jdbc.OracleDriverpom.xml取消mysql-connector-java注释,改为ojdbc8依赖
5.5 现象:花店员账号登录后看不到“订单创建”菜单
原因:权限控制基于@PreAuthorize("hasRole('STAFF')"),但数据库中sys_user_role表未给staff用户分配ROLE_STAFF角色。
解决:手动插入权限记录(或用后台「系统管理→角色授权」功能):
INSERT INTO sys_user_role (user_id, role_id) SELECT u.id, r.id FROM sys_user u, sys_role r WHERE u.username = 'staff' AND r.role_name = 'STAFF';6. 二次开发实战:如何为“企业团购”需求新增“合同订单”模块(含效期自动延期逻辑)
6.1 需求背景:某公司采购部要签年度鲜花合同,每月1号自动配送,但合同内花材效期需按实际配送日重新计算
标准订单的expire_date是入库时固定的,但合同订单要求:每次配送时,系统自动将该批次效期延长+3天(因企业客户有专业冷藏设备)。这不能靠简单字段扩展,需侵入库存扣减链路。
6.2 改造步骤:4个文件,不到50行代码
Step 1:新增合同订单实体ContractOrder.java
// flower-common/src/main/java/com/flower/entity/ContractOrder.java @Table(name = "contract_order") public class ContractOrder { @TableId(type = IdType.AUTO) private Long id; private String contractNo; // 合同号,如 CT-2024-001 private Long customerId; private Date nextDeliveryDate; // 下次配送日(每月1号) private Integer cycleDays; // 配送周期(天),默认30 private Boolean isActive; // 是否启用 }Step 2:在FlowerOrderController新增合同下单接口
// flower-admin/src/main/java/com/flower/controller/FlowerOrderController.java @PostMapping("/orders/contract") public Result createContractOrder(@RequestBody @Valid ContractOrderCreateDTO dto) { // 1. 创建合同订单(存入 contract_order 表) Long contractId = contractOrderService.create(dto); // 2. 关键改造:调用 extendExpiryForContract() 延长库存效期 stockService.extendExpiryForContract(contractId, dto.getItems()); return Result.success("合同订单创建成功"); }Step 3:实现效期延长逻辑StockService.extendExpiryForContract()
// flower-admin/src/main/java/com/flower/service/impl/StockServiceImpl.java @Transactional public void extendExpiryForContract(Long contractId, List<OrderItem> items) { for (OrderItem item : items) { // 查询当前可用库存(取最早效期的批次) FlowerStock stock = stockMapper.selectOne(new QueryWrapper<FlowerStock>() .eq("flower_id", item.getFlowerId()) .gt("expire_date", LocalDate.now()) .orderByAsc("expire_date") .last("LIMIT 1")); if (stock != null) { // 延长效期:原效期 + 3天 LocalDate newExpire = stock.getExpireDate().plusDays(3); stockMapper.update(new UpdateWrapper<FlowerStock>() .eq("id", stock.getId()) .set("expire_date", newExpire)); } } }Step 4:前端新增合同订单页面(复用现有表单)
在flower-web/src/router/index.js中添加路由:
{ path: '/contract-order', name: 'ContractOrder', component: () => import('@/views/contract/ContractOrder.vue'), meta: { title: '合同订单', icon: 'document' } }ContractOrder.vue直接复制OrderCreate.vue,仅修改提交地址为/api/orders/contract,并增加「合同号」「配送周期」输入框。
6.3 验证方法:用 Postman 模拟合同下单
发送 POST 请求到http://localhost:8080/api/orders/contract,Body 为:
{ "contractNo": "CT-2024-001", "customerId": 1001, "nextDeliveryDate": "2024-05-01", "cycleDays": 30, "items": [ { "flowerId": 1, "quantity": 100, "unit": "支" } ] }预期结果:
contract_order表新增一条记录flower_stock表中flower_id=1的最早效期批次,expire_date字段增加3天- 后续普通订单扣减时,会优先使用这个延长后的批次
从那以后我每次接到“特殊业务规则”需求,都先翻stockService的源码——90%的定制都在这里,而不是去动数据库或加中间件。库存是鲜花系统的命脉,所有业务流最终都要回到“这批货还能不能卖”这个朴素问题上。希望帮到你。
本文还有配套的精品资源,点击获取