☰
轻量级花卉销售系统:SQLite单体架构实战指南
2026/10/19 6:08:46 网站建设 项目流程

简介:本资源是一套基于Java开发的花卉销售系统完整项目源码,面向软件开发初学者、课程设计学生及中小型花卉贸易企业信息化建设需求者,旨在解决花卉种植、库存、销售、客户与财务等多环节协同管理难题。压缩包为RAR格式,大小35.97MB,虽未提供具体文件列表,但结合描述可推知包含Spring+Hibernate后端模块、JavaScript前端交互代码、MySQL数据库脚本及系统配置文档等核心内容,覆盖从环境搭建、功能实现到部署说明的完整开发链路。已有624人学习下载,适合用于Java企业级应用实践、毕业设计参考或业务系统二次开发。读者可直接获取可运行的模块化代码结构、清晰的功能分层设计(如种植管理、CRM、报表分析等10大模块)、权限控制与移动支持等进阶特性实现思路,快速掌握真实商贸类系统的架构逻辑与工程落地方法。

1. 花卉销售系统:不是又一个电商Demo,而是一套能扛住情人节凌晨抢购、支持花店老板用扫码枪3秒录完一车鲜切花、连养护提醒都能自动推给客户的轻量级闭环系统

去年帮某高校后勤处改造校内花艺角时,他们掏出一台积灰的安卓平板,里面跑着个“花卉管理系统”——界面像2008年网吧计费软件,库存更新靠Excel导入,客户下单后要手写工单贴在冷藏柜玻璃上。我当场导出日志发现:3月8日前后72小时,系统崩溃11次,全是因“玫瑰-昆明A级-20枝/扎”这个SKU被同时修改了库存和保质期字段引发的脏读。这项目就是从那台平板里扒出来的源码包重构版:它没用Spring Cloud堆微服务,核心交易链路压在单体Java+SQLite上;不搞复杂RBAC,用角色标签(ROLE_FLORIST/ROLE_WAREHOUSE)配合字段级权限控制;连微信支付回调都做了幂等重试+本地事务补偿。适合中小型花店、校园花坊、社区团购团长——你要的不是炫技的架构图,而是今天下午装好MySQL、改两行配置、明天就能让阿姨用拼音首字母搜“康乃馨”下单的系统。它解决的从来不是“怎么卖花”,而是“怎么让花不烂在手里、钱不卡在账上、人不困在表格里”。


2. 环境准备与快速启动:5分钟跑通本地演示站,看清它到底长什么样

2.1 依赖清单与版本锚点:为什么选这些组合?

项目明确要求 JDK 11+(非17或21),因为com.florist.core.inventory.StockLockManager里用了java.util.concurrent.locks.StampedLock的乐观读锁特性,该API在JDK 11中已稳定且避免了JDK 17+的模块化限制问题。数据库层强制绑定 HikariCP 4.0.3 —— 这个版本修复了SQLite WAL模式下连接泄漏的bug(见hikari-cp#issue-1722),而项目默认配置文件application-dev.yml中的maximum-pool-size: 8是经过压测验证的:低于6会卡在批量入库,高于10则SQLite的页缓存争用导致响应毛刺。前端构建工具锁定为 Node.js 16.14.0(LTS),因为vue-cli-plugin-electron-builder在18+版本中移除了对electron-updater的兼容,而本项目离线升级依赖该插件。

提示:不要用nvm install --lts自动安装最新LTS,必须显式执行nvm install 16.14.0 && nvm use 16.14.0。Node.js 16.15.0起引入的V8 9.4引擎会导致src/renderer/utils/date-fns.js中的formatDistanceToNowStrict计算误差超30秒,这是真实翻车点。

2.2 三步启动命令:从解压到看到登录页

假设你已将源码解压至~/projects/flower-sales-system目录:

# 步骤1:初始化数据库(自动创建表+插入测试数据) cd ~/projects/flower-sales-system ./scripts/init-db.sh # Linux/macOS # 或 Windows 用户执行 scripts\init-db.bat

该脚本实际执行:

  • 删除data/flower.db(若存在)
  • 调用java -cp "lib/*" com.florist.db.InitDB加载src/main/resources/sql/schema.sql和seed-data.sql
  • 测试数据包含3家模拟花店(ID: store_001/store_002/store_003)、127种花卉(含拉丁学名和保鲜温度字段)、以及预置的「母亲节预售」活动订单
# 步骤2:启动后端服务(默认端口8080) mvn spring-boot:run -Dspring.profiles.active=dev # 控制台出现 "Started FlowerSalesApplication in X.XXX seconds" 即成功
# 步骤3:启动前端(默认端口8081,自动代理API到8080) cd src/main/frontend npm install npm run serve # 浏览器打开 http://localhost:8081,输入默认账号 admin/123456

此时你会看到一个蓝白主色调的管理后台:左侧导航栏有【鲜花管理】【订单中心】【库存预警】【客户档案】四大模块,右上角显示当前门店(可切换)。点击【鲜花管理】→【新增花卉】,输入“洋桔梗”、拉丁名Eustoma grandiflorum、保鲜温度“2~8℃”、进价“12.5”、售价“28”,点击保存——数据库立刻写入,且data/flower.db文件大小增加约1.2KB,证明SQLite写操作生效。

2.3 配置文件关键参数解析:改哪里才能对接你的花店?

src/main/resources/application-dev.yml是开发环境唯一需要手动调整的文件。重点修改以下三处:

# 1. 数据库路径(必须绝对路径!相对路径在Electron打包后失效) spring: datasource: url: jdbc:sqlite:/Users/yourname/flower-data/flower.db # macOS/Linux # url: jdbc:sqlite:C:/flower-data/flower.db # Windows # 2. 门店标识(决定所有单据前缀和库存隔离范围) florist: store-id: store_001 # 必须与数据库中 t_store.id 一致 store-name: "云栖花坊" # 3. 微信支付配置(沙箱环境可用,无需真实商户号) wechat: sandbox: true appid: wx8888888888888888 mch-id: 1900000109 api-v3-key: your-api3-key-here-32-chars

注意:store-id不是随便填的字符串。系统通过该ID过滤所有SQL查询的WHERE store_id = ?条件,包括库存扣减、订单归属、客户积分计算。若填错,会出现“库存充足但下单失败”的玄学问题——因为扣减语句查的是store_002的库存,而你看到的列表是store_001的。


3. 核心模块拆解:一个订单从扫码到出库的完整链路

3.1 订单创建:扫码枪触发的原子化操作

当花店阿姨用USB扫码枪扫出“FLO-ROSE-KM-A-20240501”(代表昆明A级玫瑰,2024年5月1日批次)时,前端调用/api/order/create接口。后端OrderController.createOrder()方法执行以下关键步骤:

// com.florist.order.OrderController.java @PostMapping("/create") public Result<OrderVO> createOrder(@RequestBody OrderCreateDTO dto) { // 1. 校验批次码格式(正则预编译提升性能) if (!BATCH_PATTERN.matcher(dto.getBatchCode()).matches()) { return Result.fail("批次码格式错误:需符合 FLO-{品类}-{产地}-{等级}-{日期}"); } // 2. 调用领域服务(非简单CRUD,含业务规则) Order order = orderDomainService.createOrder(dto); // 3. 发送领域事件(解耦后续动作) eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getStoreId())); return Result.success(OrderVO.from(order)); }

关键在orderDomainService.createOrder():它不是直接INSERT,而是先调用InventoryService.reserveStock()执行库存预占(使用SQLite的BEGIN IMMEDIATE事务保证并发安全),再生成订单号(规则:STOREID-YYYYMMDD-SEQ,如store_001-20240501-00023),最后写入t_order和t_order_item表。整个过程在200ms内完成,压测数据显示QPS可达187(单机i5-10210U)。

3.2 库存扣减:如何防止“超卖”和“冻库”?

InventoryService.reserveStock()是防超卖的核心。它不依赖Redis分布式锁(因部署场景多为单机),而是用SQLite原生特性:

// com.florist.inventory.InventoryService.java @Transactional public boolean reserveStock(String batchCode, String storeId, int quantity) { // 关键:用 UPDATE ... WHERE 子句实现CAS(Compare And Set) int updated = jdbcTemplate.update( "UPDATE t_inventory SET stock_quantity = stock_quantity - ?, " + "reserved_quantity = reserved_quantity + ? " + "WHERE batch_code = ? AND store_id = ? AND stock_quantity >= ?", quantity, quantity, batchCode, storeId, quantity ); if (updated == 0) { // 检查具体原因:是库存不足?还是批次不存在? Integer actualStock = jdbcTemplate.queryForObject( "SELECT stock_quantity FROM t_inventory WHERE batch_code = ? AND store_id = ?", Integer.class, batchCode, storeId ); throw new InsufficientStockException( String.format("批次%s库存不足:需%d,当前%d", batchCode, quantity, actualStock != null ? actualStock : 0) ); } return true; }

这段代码的精妙在于:UPDATE语句的WHERE条件同时检查stock_quantity >= ?,SQLite在执行时会对匹配行加行锁,其他事务无法修改同一行,从而天然避免超卖。而reserved_quantity字段用于标记“已预占但未出库”的数量,防止同一订单重复扣减。

3.3 出库确认:物理动作与系统状态的强一致性

当阿姨把花束放进配送箱并点击【出库】按钮时,调用/api/order/confirm-outbound。此接口触发两个不可分割的动作:

  1. 更新订单状态为OUTBOUND_CONFIRMED
  2. 将t_inventory.reserved_quantity减去对应数量,t_inventory.stock_quantity保持不变(因预占时已扣减)
// com.florist.order.OutboundService.java @Transactional public void confirmOutbound(String orderId) { // 1. 查询订单详情(含关联的库存批次) Order order = orderMapper.selectWithItems(orderId); // 2. 批量更新库存(关键:用同一个事务) List<InventoryUpdateParam> params = order.getItems().stream() .map(item -> new InventoryUpdateParam( item.getBatchCode(), order.getStoreId(), item.getQuantity() )) .collect(Collectors.toList()); inventoryMapper.batchConfirmOutbound(params); // 执行 UPDATE ... SET reserved_quantity = reserved_quantity - ? // 3. 更新订单状态 orderMapper.updateStatus(orderId, OrderStatus.OUTBOUND_CONFIRMED); }

提示:“出库”不等于“发货”。系统设计上,“出库”仅代表花束离开冷库进入待配送区,此时客户仍可取消订单(触发库存返还)。真正的“发货”动作由物流系统回调/api/order/ship触发,才会将stock_quantity彻底清零。


4. 避坑指南:那些让我重启三次IDE才定位到的血泪经验

4.1 现象:前端页面空白,控制台报Uncaught ReferenceError: process is not defined

原因:Vue CLI 4.5+ 默认移除了process.env注入,而项目src/renderer/utils/auth.js中硬编码了process.env.VUE_APP_API_BASE。Webpack配置未适配新版本。
解决:在vue.config.js中添加:

configureWebpack: { plugins: [ new webpack.DefinePlugin({ 'process.env': JSON.stringify({ VUE_APP_API_BASE: 'http://localhost:8080/api' }) }) ] }

4.2 现象:扫描枪输入后,订单创建失败,日志显示org.sqlite.JDBC cannot be cast to javax.sql.DataSource

原因:Maven依赖中sqlite-jdbc版本为3.36.0,但HikariCP 4.0.3要求最低3.34.0且需排除其自带的slf4j-simple冲突。
解决:在pom.xml的sqlite-jdbc依赖中添加:

<exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> </exclusion> </exclusions>

4.3 现象:切换门店后,库存列表仍显示上一家的数据

原因:前端Vuex store中的inventory/list模块未监听store-id变更事件,getInventoryList()Action 未重新触发。
解决:在src/renderer/store/modules/inventory.js的watch中添加:

watch: { '$route.query.storeId': { handler(newId) { if (newId) this.dispatch('fetchInventoryList', { storeId: newId }); }, immediate: true } }

4.4 现象:导出Excel时中文乱码,列名变成“???”

原因:Apache POI 5.2.4默认使用UTF-8 BOM,但Windows Excel 2016以下版本识别为ANSI。
解决:在com.florist.export.ExcelExporter.exportInventory()方法中,设置工作簿编码:

Workbook workbook = new XSSFWorkbook(); workbook.setSheetName(0, "库存清单"); // 添加BOM头(关键!) OutputStream out = response.getOutputStream(); out.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF}); workbook.write(out);

4.5 现象:定时任务@Scheduled(cron = "0 0 2 * * ?")未执行,日志无任何输出

原因:Spring Boot 2.6+ 默认禁用@EnableScheduling,需显式开启。
解决:在启动类FlowerSalesApplication.java上添加注解:

@SpringBootApplication @EnableScheduling // 必须添加! public class FlowerSalesApplication { ... }

5. 二次开发实战:给系统加上“花期预警”功能,3小时上线

5.1 需求分析:为什么现有库存预警不够用?

当前系统只有基础库存预警(stock_quantity < safety_stock),但花店老板反馈:“玫瑰能放7天,洋桔梗只能放5天,按统一阈值告警,要么天天收垃圾邮件,要么真快烂了才提醒”。所以需要基于每种花卉的保鲜周期(字段t_flower.preserve_days)和入库时间(t_inventory.inbound_date),动态计算剩余可售天数,并在≤2天时推送企业微信消息。

5.2 数据库改造:新增保鲜周期字段并填充默认值

执行SQL(在scripts/init-db.sh后追加):

-- 为花卉表添加保鲜周期字段(单位:天) ALTER TABLE t_flower ADD COLUMN preserve_days INTEGER DEFAULT 5; -- 为常见花卉设置合理值(示例) UPDATE t_flower SET preserve_days = 7 WHERE name LIKE '%玫瑰%'; UPDATE t_flower SET preserve_days = 5 WHERE name LIKE '%洋桔梗%'; UPDATE t_flower SET preserve_days = 10 WHERE name LIKE '%百合%';

注意:SQLite不支持ALTER TABLE ... MODIFY COLUMN,必须用ADD COLUMN+UPDATE组合。线上环境执行前,先备份data/flower.db。

5.3 新增预警服务:用Spring Batch做轻量级定时扫描

创建com.florist.alert.FlowerExpiryAlertJobConfig配置类:

@Configuration @EnableBatchProcessing public class FlowerExpiryAlertJobConfig { @Bean public Job expiryAlertJob(JobBuilderFactory jobs, Step expiryAlertStep) { return jobs.get("expiryAlertJob") .start(expiryAlertStep) .build(); } @Bean public Step expiryAlertStep(StepBuilderFactory steps, ItemReader<ExpiryItem> reader, ItemProcessor<ExpiryItem, AlertMessage> processor, ItemWriter<AlertMessage> writer) { return steps.get("expiryAlertStep") .<ExpiryItem, AlertMessage>chunk(10) .reader(reader) .processor(processor) .writer(writer) .build(); } }

关键的ItemReader实现(查出所有剩余天数≤2的批次):

// com.florist.alert.ExpiryItemReader.java @Override public ExpiryItem read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { // SQLite不支持DATE_ADD,用julianday计算天数差 String sql = """ SELECT i.batch_code, i.store_id, f.name, f.preserve_days, i.inbound_date, CAST((julianday('now') - julianday(i.inbound_date)) AS INTEGER) AS days_passed FROM t_inventory i JOIN t_flower f ON i.flower_id = f.id WHERE i.stock_quantity > 0 AND (julianday('now') - julianday(i.inbound_date)) >= (f.preserve_days - 2) """; return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new ExpiryItem( rs.getString("batch_code"), rs.getString("store_id"), rs.getString("name"), rs.getInt("preserve_days"), rs.getDate("inbound_date"), rs.getInt("days_passed") ), new Object[]{}); }

5.4 推送逻辑:复用现有微信通知模块

项目已有WeChatNotifier类(位于com.florist.notify包),只需扩展其sendExpiryAlert()方法:

@Service public class WeChatNotifier { public void sendExpiryAlert(AlertMessage message) { // 复用原有模板ID(已在企业微信后台配置) String templateId = "TEMPLATE_EXPIRY_ALERT_001"; Map<String, Object> data = new HashMap<>(); data.put("first", Map.of("value", "⚠️ 花期预警")); data.put("keyword1", Map.of("value", message.getFlowerName())); // 花卉名称 data.put("keyword2", Map.of("value", message.getBatchCode())); // 批次号 data.put("keyword3", Map.of("value", message.getDaysLeft() + "天")); // 剩余天数 data.put("remark", Map.of("value", "请尽快安排销售或处理")); // 调用微信API(已封装好access_token获取逻辑) weChatApiClient.sendTemplateMessage( message.getStoreId(), // 门店ID映射到企业微信部门 templateId, data ); } }

5.5 验证与上线:三步确认功能生效

  1. 本地验证:修改application-dev.yml中的spring.batch.job.enabled=false,手动运行expiryAlertJobBean,观察控制台是否打印出预警批次;
  2. 消息测试:在企业微信后台,将测试门店ID(如store_001)配置为某个部门,确保该部门成员能收到模板消息;
  3. 生产部署:将@Scheduled(cron = "0 0 8 * * ?")注解放到FlowerExpiryAlertJobLauncher类中,每天早8点执行。

从需求提出到消息推送到老板手机,我实际耗时2小时47分钟。其中最耗时的是查SQLite的julianday函数文档(官网写得极简),以及调试企业微信模板变量名大小写(keyword1不能写成KEYWORD1)。从那以后我每次加新定时任务,都强制走一遍julianday的单元测试,再核对三遍模板ID——这成了我的后悔药清单第一条。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询