news 2026/10/7 18:43:39

基于Spring Boot与MyBatis-Plus的柑橘水果管理系统:批次追溯与库存扣减实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot与MyBatis-Plus的柑橘水果管理系统:批次追溯与库存扣减实战

简介:本资源为基于Java语言的柑橘类水果管理系统设计源码,面向计算机相关专业学生、Java初学者及需要课程设计或毕业设计参考的开发者,帮助解决水果生产、销售与库存跟踪等业务场景下的系统搭建问题。压缩包共554个文件,约39.79MB,其中179个XML配置文件负责数据库连接、服务器及运行参数设置,120个Java源文件承载水果信息录入、库存管理、销售记录等核心业务逻辑,另有2个SQL脚本用于建表与初始化数据,以及iml项目配置、properties、json、cookies、http等辅助文件,便于在IDEA中导入运行与调试。资源还包含readme.txt入门说明与doc文档,可帮助读者快速理解系统结构与运行机制。目前已有286人学习下载,适合作为课程设计、毕业设计或Java Web入门练手项目,通过阅读源码掌握分层设计、配置管理与数据库持久化的完整实现思路。

1. 柑橘类水果管理系统到底管什么:从果园台账到出库结算的一条链

柑橘类水果管理系统,说白了就是把果园、批次、库存、订单、结算这几件事塞进一套 Java 后端里跑起来。我最早接触这类需求是在一个柑橘合作社,他们当时用 Excel 记采摘批次,结果同一批果子在冷库和档口之间来回调拨,账面上多出三百斤,谁也说不清去哪了。这类系统的核心不是“管理水果”,而是管理批次与库存的流转关系——一棵树上的果子从采摘那刻起就有批次号,之后每一次分拣、入库、调拨、出库都要留痕。

这套系统适合谁?做 Java 课程设计的学生、接农业信息化小单的外包团队、以及想给自己果园或合作社搭一套内部台账的开发者。它不复杂,但涉及的知识点很全:Spring Boot 分层、MyBatis-Plus 的 CRUD 与条件构造、库存扣减的并发控制、以及最容易被忽略的批次追溯。热搜里常出现“java课程设计案例源码”“spring boot + mybatis 开源商城源码”,其实柑橘管理系统就是这类项目的农业垂直版本,把商品换成有保质期、有产地、有等级的水果,业务约束反而更清晰。下面按我实际搭过的一版结构,把选型、建表、核心接口和踩坑一次讲透。

2. 技术选型与工程骨架:为什么用 Spring Boot + MyBatis-Plus 而不是裸 Servlet

2.1 分层结构与依赖取舍

柑橘管理系统的业务量不大,但实体关系不简单:产地、品种、等级、批次、仓库、客户、订单,七张核心表互相外键关联。用裸 Servlet + JDBC 写,光是 ResultSet 到对象的映射就能写到你怀疑人生。我一般直接上 Spring Boot 2.7 + MyBatis-Plus,理由有三条:一是 MyBatis-Plus 的BaseMapper省掉 80% 的单表 CRUD;二是它的条件构造器LambdaQueryWrapper在写“查某产地某等级且库存大于零的批次”这种组合条件时,比手写 XML 清爽得多;三是 Spring Boot 的@Transactional能直接管住库存扣减和订单写入的一致性。

依赖清单我固定用这几个,版本按你本地 Maven 仓库能拉到的稳定版走:

<!-- pom.xml 关键依赖 --> <dependencies> <!-- Web 层,提供 REST 接口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus,注意用 boot-starter 而非裸 mybatis --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok,省掉 getter/setter --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这段依赖里最容易被新手忽略的是 MyBatis-Plus 的 starter 和裸mybatis的区别:用错 starter 会导致BaseMapper注入失败,启动直接报NoSuchBeanDefinitionException。另外 MySQL 驱动从 8.x 起包名是com.mysql.cj.jdbc.Driver,配置文件里别写成老的com.mysql.jdbc.Driver,否则连接池初始化就抛异常。

2.2 包结构与配置项

包结构我按controller / service / mapper / entity / dto / config六层分,别把 entity 和 dto 混用——柑橘系统里“批次”在入库时只需要产地和数量,出库时却要带客户和结算价,字段差异大,混用会让接口参数越来越臃肿。application.yml里几个必调参数:

spring: datasource: url: jdbc:mysql://localhost:3306/citrus_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库 batch_no 自动映射 batchNo log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期打印 SQL global-config: db-config: id-type: auto # 主键自增,别用默认的雪花 ID 除非分库 logic-delete-field: deleted # 逻辑删除字段

map-underscore-to-camel-case必须开,否则batch_no映射不到batchNo,查出来全是 null,这个坑我见过至少五个人踩。log-impl只在开发期开,上线关掉,不然日志量能把磁盘写满。逻辑删除字段建议加上,柑橘批次被“删除”时实际是标记deleted=1,否则历史订单关联的批次会变成孤儿记录,追溯直接断链。

3. 核心表结构与批次追溯:七张表怎么建才不返工

3.1 建表 SQL 与字段含义

表设计是这类系统返工率最高的地方。我第一版把产地、品种、等级全塞进一张fruit表,结果同一品种不同产地没法区分价格,只能推倒重来。正确做法是拆成基础维度表和业务流转表。核心七张表如下:

表名作用关键字段
origin产地id, name, region
variety品种id, name, season
grade等级id, name, price_factor
batch采摘批次id, batch_no, origin_id, variety_id, grade_id, quantity, produce_date
warehouse仓库id, name, capacity
stock库存流水id, batch_id, warehouse_id, change_qty, type, create_time
order订单id, customer, batch_id, qty, amount, status

建表时batch_no加唯一索引,格式用产地码+品种码+日期+序号,比如GX-WG-20240315-001。stock表只记增量不记存量,存量由流水累加得出,这样任何一次库存对不上都能顺着流水查到具体哪一笔。下面是batch和stock的建表语句:

CREATE TABLE batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_no VARCHAR(64) NOT NULL UNIQUE COMMENT '批次号,全局唯一', origin_id BIGINT NOT NULL, variety_id BIGINT NOT NULL, grade_id BIGINT NOT NULL, quantity DECIMAL(10,2) NOT NULL COMMENT '采摘数量,单位斤', produce_date DATE NOT NULL, deleted TINYINT DEFAULT 0, INDEX idx_produce (produce_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, change_qty DECIMAL(10,2) NOT NULL COMMENT '正数入库,负数出库', type VARCHAR(16) NOT NULL COMMENT 'IN/OUT/TRANSFER', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_batch (batch_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

quantity用DECIMAL不用INT,因为柑橘按斤称,半斤八两是常态,用整数会丢精度。stock表的change_qty允许负数,出库记负、入库记正,累加即当前库存,这个设计比单独维护一张存量表更不容易出错——存量表一旦更新失败就和流水对不上,而流水是只增不改的。

3.2 用 MyBatis-Plus 生成实体与 Mapper

实体类用 Lombok 的@Data,字段名和表字段驼峰对应。Batch实体:

@Data @TableName("batch") public class Batch { @TableId(type = IdType.AUTO) private Long id; private String batchNo; private Long originId; private Long varietyId; private Long gradeId; private BigDecimal quantity; private LocalDate produceDate; @TableLogic private Integer deleted; }

@TableLogic注解配合配置里的logic-delete-field生效,调用removeById时自动改成UPDATE batch SET deleted=1,查询自动过滤deleted=0。Mapper 接口只需继承BaseMapper<Batch>,不用写一行 XML 就有增删改查。这里有个细节:@TableId(type = IdType.AUTO)必须和数据库自增一致,如果数据库是自增而实体写成ASSIGN_ID,插入时 MyBatis-Plus 会自己生成雪花 ID 覆盖,导致自增列失效,这个坑排查起来很费时间,因为 SQL 日志看起来完全正常。

4. 库存扣减与订单接口:并发下怎么保证不超卖

4.1 库存扣减的两种写法与选择

柑橘出库时最怕超卖——仓库里只有 100 斤,两个订单同时扣,结果都扣成功,账面变成 -50 斤。常见做法有两种:一是UPDATE stock SET ... WHERE quantity >= ?的乐观锁写法,二是SELECT ... FOR UPDATE的行锁写法。我一般用第一种,因为柑橘系统的并发量不高,乐观锁足够,且不用开事务锁等待。

具体到代码,扣减前先查当前库存,再插入一条负数流水,同时校验结果:

@Service public class StockService { @Autowired private StockMapper stockMapper; /** * 出库扣减,返回是否成功 * @param batchId 批次ID * @param qty 出库数量,正数 */ @Transactional(rollbackFor = Exception.class) public boolean outbound(Long batchId, BigDecimal qty) { // 累加流水得到当前库存 BigDecimal current = stockMapper.sumByBatchId(batchId); if (current == null || current.compareTo(qty) < 0) { return false; // 库存不足,直接拒绝 } Stock stock = new Stock(); stock.setBatchId(batchId); stock.setChangeQty(qty.negate()); // 出库记负数 stock.setType("OUT"); stockMapper.insert(stock); return true; } }

sumByBatchId是一条自定义 SQL:SELECT COALESCE(SUM(change_qty),0) FROM stock WHERE batch_id = ?。用COALESCE是因为没有流水时SUM返回 null,直接比较会抛 NPE。@Transactional保证查库存和插流水在一个事务里,但注意:这个写法在高并发下仍有窗口——两个线程同时查到 100,都判断通过。要彻底解决,得在sumByBatchId上加FOR UPDATE,或者用数据库层面的唯一约束兜底。柑橘场景并发低,我通常加一个应用层锁(按 batchId 加ReentrantLock)就够了,别过度设计。

4.2 订单接口的参数校验与状态机

订单接口接收batchId、customer、qty,返回订单号和结算金额。金额 = 数量 × 等级系数 × 基础单价。参数校验用@Valid+ JSR303 注解,别在 service 里手写一堆 if:

@PostMapping("/order/create") public Result<OrderVO> create(@RequestBody @Valid OrderDTO dto) { // dto 里 qty 用 @DecimalMin("0.01") 校验 return Result.ok(orderService.create(dto)); }

OrderDTO的qty字段加@DecimalMin(value = "0.01", message = "出库数量必须大于零"),batchId加@NotNull。订单状态用枚举PENDING / PAID / SHIPPED / DONE,状态流转只能单向,别允许从DONE改回PENDING,否则对账时会出现已结算订单又被修改的情况。状态机我一般用一张order_status_log表记录每次变更,出问题时能查到是谁在什么时候改的。

5. 避坑与排查:柑橘系统上线后最常翻车的五个点

5.1 批次号重复导致插入失败

现象:采摘旺季批量导入批次,日志报Duplicate entry 'GX-WG-20240315-001' for key 'batch_no'。原因是批次号生成用了“日期+序号”,但序号是查当天最大序号再加一,两个请求同时查到相同最大值。解决:把序号生成放到数据库唯一索引兜底,捕获DuplicateKeyException后重试一次,或者直接用batch_no加时间戳毫秒。我后来改成日期+仓库码+自增序列,序列由数据库AUTO_INCREMENT提供,彻底避开并发。

5.2 逻辑删除后关联查询查不到数据

现象:删掉一个批次后,历史订单详情页的批次信息变成空白。原因是@TableLogic让批次查询自动过滤deleted=1,但订单关联查询走的是 join,join 条件里没带deleted=0,或者带了却把已删批次过滤掉了。解决:历史订单展示时用单独的查询,明确include deleted,或者干脆不物理删批次,只标记状态为“停用”。我的习惯是业务主表不做逻辑删除,用状态字段控制,逻辑删除只用在草稿类数据上。

5.3 库存流水累加出现小数精度误差

现象:100 斤果子分三次出库,33.33 + 33.33 + 33.33,账面剩 0.01 斤,对不上。原因是BigDecimal除法没指定精度,或者用了double做中间计算。解决:所有数量字段用BigDecimal,除法必须带setScale(2, RoundingMode.HALF_UP),比较用compareTo不用equals。equals会比较 scale,33.30和33.3用equals返回 false,这个坑我在对账时踩过,查了一下午。

5.4 事务失效导致库存扣了订单没生成

现象:出库成功但订单表没记录,或者反过来。原因是@Transactional加在 private 方法上,或者同类内部方法调用绕过了代理。解决:事务方法必须是 public,且从外部类调用。如果确实要在同类内调用,注入自身代理或用AopContext.currentProxy()。另外注意rollbackFor = Exception.class要显式写,默认只回滚RuntimeException,受检异常不回滚。

5.5 时间字段时区错乱

现象:采摘日期显示比实际早一天。原因是 MySQL 连接串没配serverTimezone,或者实体用java.util.Date而数据库是DATE。解决:连接串加serverTimezone=Asia/Shanghai,实体日期字段统一用LocalDate/LocalDateTime,别用Date。LocalDate和 MySQL 的DATE类型映射干净,没有时区偏移问题。

6. 进阶技巧:用 MyBatis-Plus 条件构造器做多维度库存看板

系统跑起来后,合作社最想要的是一个看板:按产地、品种、等级、仓库四个维度看当前库存。手写四条 SQL 再在 Java 里拼装太笨,MyBatis-Plus 的LambdaQueryWrapper配合groupBy能一次搞定。下面这段是我实际用的库存汇总查询:

public List<StockView> dashboard(String origin, String variety, String grade) { LambdaQueryWrapper<Stock> wrapper = new LambdaQueryWrapper<>(); // 动态条件:传了才拼,不传查全部 wrapper.eq(StringUtils.hasText(origin), Stock::getOriginName, origin) .eq(StringUtils.hasText(variety), Stock::getVarietyName, variety) .eq(StringUtils.hasText(grade), Stock::getGradeName, grade) .groupBy(Stock::getOriginName, Stock::getVarietyName, Stock::getGradeName) .select(Stock::getOriginName, Stock::getVarietyName, Stock::getGradeName, "SUM(change_qty) as totalQty"); return stockMapper.selectList(wrapper); }

eq的第一个参数是布尔条件,为 false 时整个条件不拼进 SQL,这就是动态查询的关键,比在 XML 里写一堆<if>标签清爽。groupBy后select里除了分组字段,其余必须用聚合函数,否则 MySQL 的ONLY_FULL_GROUP_BY模式会直接报错。SUM(change_qty)得到的是净库存,正负流水自动抵消。

看板接口建议加缓存,因为库存汇总查询会扫全表流水,数据量上万后响应变慢。我一般用 Spring Cache + Redis,缓存 key 按查询维度拼,过期时间设 5 分钟。注意缓存和事务的配合:出库成功后要主动清缓存,别等过期,否则看板数据滞后,合作社的人会以为系统坏了。

最后说个验证方法:拿一批真实数据,手动在数据库里插几条流水,然后调看板接口,核对SUM结果和手算是否一致。我习惯在测试环境留一个batch_id = -1的测试批次,专门用来跑边界用例,比如零库存出库、负数入库、超大数量。这个习惯帮我提前发现过三次精度问题。做这类系统,账目对得上比功能多更重要,批次追溯链一旦断了,后面全是血泪经验。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 18:42:50

中文细粒度情感分析实战:BERT-wwm+BiLSTM-CRF双行业落地

简介&#xff1a;本资源是一套面向计算机专业本科生的毕业设计级中文情感分析实战项目&#xff0c;聚焦酒店与书店两类典型场景的评论数据&#xff0c;实现端到端的情感分类与智能客服基础功能&#xff0c;适用于毕设选题、课程设计及深度学习项目实训。压缩包共195个文件&…

作者头像 李华
网站建设 2026/10/7 18:41:31

VR实时交互延迟优化:从460ms到120ms的全链路调优指南

1. 这不是游戏卡顿&#xff0c;是实时交互系统在“窒息”——VRChat延迟飙到460ms意味着什么你刚戴上头显&#xff0c;挥手打招呼&#xff0c;对方却三秒后才点头回应&#xff1b;你转身想避开NPC&#xff0c;视角却像被胶水粘住一样滞后半拍&#xff1b;语音刚出口&#xff0c…

作者头像 李华
网站建设 2026/10/7 18:41:22

Claude跨会话记忆增强:claude-mem原理与部署调优

1. 项目概述&#xff1a;为什么我需要给Claude装上记忆 先说说这个东西解决了什么问题。用过Claude的朋友应该都有同感&#xff1a;单次对话里它确实聪明&#xff0c;但一旦关掉窗口或者切换会话&#xff0c;之前的上下文就全丢了。每次开新对话都得重新自我介绍、重新交代背景…

作者头像 李华
网站建设 2026/10/7 18:41:14

AI写嵌入式驱动为何频刷砖?五大核心细节与安全开发流程

1. 为什么“AI写驱动”这件事在嵌入式圈子里争议这么大1.1 一个真实场景&#xff1a;从“效率神器”到“刷砖惨案”前阵子有个做工业网关的朋友找我救急&#xff0c;说他用AI生成了一段W25Q32JVSSIQ的SPI Flash驱动&#xff0c;本地编译通过、逻辑看着也没毛病&#xff0c;烧进…

作者头像 李华
网站建设 2026/10/7 18:40:35

USB2.0设备改Type-C接口全攻略:两颗5.1kΩ电阻搞定

先交代一下动手的由来。手上有一个小键盘&#xff0c;一直走Micro-USB供电和通讯&#xff0c;这两年Micro线越来越难找&#xff0c;而且这个口本身不支持正反插&#xff0c;插错方向稍微用点力&#xff0c;端子就歪了。手边Type-C线倒是抓一把都是&#xff0c;索性给它换个Type…

作者头像 李华
网站建设 2026/10/7 18:40:32

Codex本地部署实战:从Docker到VS Code的AI编程助手搭建

1. 项目概述&#xff1a;为什么一个“AI编程助手”的本地部署值得花三天时间折腾&#xff1f; Codex 这个名字&#xff0c;对写代码的人而言&#xff0c;就像当年第一次看到 GitHub 的 clone 按钮——它不单是个工具&#xff0c;而是一种工作流的重新定义。但很多人点开官网、注…

作者头像 李华