鲜花销售系统:Spring Boot 2.7实战进销存+轻量CRM

发布时间:2026/10/19 8:40:59
鲜花销售系统:Spring Boot 2.7实战进销存+轻量CRM
简介本资源是一款基于Java开发的花卉销售系统面向花卉种植企业、批发商及零售门店等从业者旨在通过信息化手段解决种植计划粗放、库存积压或缺货、订单处理低效、客户信息分散等实际运营痛点。系统采用SpringHibernate技术栈构建后端集成种植管理、库存预警、多渠道订单处理、销售趋势分析、CRM客户维护、财务核算及可视化报表等核心模块并支持与ERP等外部系统对接具备企业级应用的完整性与扩展性。压缩包为RAR格式大小35.97MB虽未提供具体文件明细但结合描述可推断包含Java源码、数据库脚本、前后端配置及说明文档等典型开发交付物。目前已有624人学习下载适合Java初学者通过完整业务系统理解SSM框架实践也便于从业者快速部署或二次开发适配本地花卉业务流程。1. 花卉销售系统不是又一个电商Demo而是一套能扛住情人节凌晨抢购、支持花店老板用扫码枪3秒打单、连售后退换货都带养护建议的实战级进销存轻量CRM去年情人节前夜某连锁花店后台崩了三次——不是因为流量大而是库存扣减和订单状态同步错乱导致同一束“永生玫瑰”被同时卖给两位客户客服电话被打爆。后来他们换上了这套花卉销售系统核心不是炫技的微服务或AI推荐而是把「鲜花这个非标品」的业务逻辑全拧紧了批次效期自动预警百合切花超72小时强制下架、花材损耗按天折损建模康乃馨每日自然萎蔫率3.7%、甚至配送员APP里点“已送达”时会弹出“请提醒客户勿放暖气旁水位保持5cm以上”的养护提示。它不追求高并发神话但能稳稳支撑单店日均300订单、50SKU动态调价、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-modelform.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 stylefont-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()是手动补偿事务——因 SpringTransactional无法跨模块回滚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样东西拒绝“环境玄学”组件版本要求安装验证命令说明JDK8u202 或 11.0.20java -versionSpring Boot 2.7 兼容性最佳MySQL5.7.32mysql --version建库脚本含utf8mb4字符集Node.js14.21.3node -v npm -vVue 2.6 兼容性要求Maven3.6.3mvn -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 # 方式2Navicat / DBeaver 图形界面右键数据库 → “运行SQL文件”关键字段说明flower_stock表batch_no: 批次号格式YYYYMMDD-FLCODE-NN如20240415-R001-01expire_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.jsdevServer: { 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 特有语法如ENGINEInnoDB、反引号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, ListOrderItem items) { for (OrderItem item : items) { // 查询当前可用库存取最早效期的批次 FlowerStock stock stockMapper.selectOne(new QueryWrapperFlowerStock() .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 UpdateWrapperFlowerStock() .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/contractBody 为{ contractNo: CT-2024-001, customerId: 1001, nextDeliveryDate: 2024-05-01, cycleDays: 30, items: [ { flowerId: 1, quantity: 100, unit: 支 } ] }预期结果contract_order表新增一条记录flower_stock表中flower_id1的最早效期批次expire_date字段增加3天后续普通订单扣减时会优先使用这个延长后的批次从那以后我每次接到“特殊业务规则”需求都先翻stockService的源码——90%的定制都在这里而不是去动数据库或加中间件。库存是鲜花系统的命脉所有业务流最终都要回到“这批货还能不能卖”这个朴素问题上。希望帮到你。本文还有配套的精品资源点击获取