ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+MySQL+Web企业仓库存储管理系统实战:从Excel台账到实时库存

Java+MySQL+Web企业仓库存储管理系统实战:从Excel台账到实时库存 简介这是一套面向高校计算机相关专业学生与Java Web初学者、课程设计开发者的企业仓库存储管理系统完整源码采用Spring BootJavaMavenMySQLMyBatis技术栈基于B/S结构使用IntelliJ IDEA开发可作为课程设计、毕业设计或Java Web综合练习的参考方案。系统功能覆盖客户、仓库、产品三类基本信息管理以及用户权限管理、入库与出库记录管理、库存管理和系统日志查看业务模块较为完整。资源包共221个文件包含42个Java源文件、28个HTML页面、24个JavaScript脚本、9个XML配置、8个CSS样式及若干图片与字体资源另有1个SQL建库脚本和1个properties配置文件压缩包约6.95MB目录结构清晰便于按模块阅读与二次开发。目前已有568人学习下载适合需要理解Spring Boot整合MyBatis、掌握增删改查与权限控制实现思路的读者参考借鉴。1. 从一张 Excel 台账说起企业仓库存储管理系统到底要解决什么我见过太多中小企业的仓库管理现状入库靠手写单据出库靠微信群喊一声月底盘点时三个人对着 Excel 台账核到凌晨两点最后还是差了 17 件货对不上。老板问「这批物料到底还剩多少」仓管翻半天表格说「大概还有两百多」。这不是段子是我去现场做需求调研时真实遇到的场景。企业仓库存储管理系统要解决的就是把这个「大概」变成「精确到个位、精确到库位、精确到批次」的实时数字。这个标题里的关键词拆开看Java 是后端语言选型MySQL 是数据落地的关系型数据库Web 是交付形态——仓管用浏览器打开就能操作不需要装客户端。合在一起就是一套典型的 JavaWeb 企业级应用。它适合谁适合有两到三个仓库、SKU 在几千到几万级别、需要多角色协作仓管、采购、财务、管理员的中小企业也适合正在找 JavaWeb 项目实战练手的开发者——因为这个业务场景的复杂度刚好卡在「不简单到没东西写也不复杂到做不完」的区间。我一般会把这类系统的核心诉求归成四件事库存实时准确、出入库有据可查、多角色权限隔离、报表能导出给老板看。后面所有章节的选型和代码都是围绕这四件事展开的。2. 技术选型与数据库设计为什么是 Spring Boot MyBatis-Plus MySQL2.1 后端框架选型别再用 Servlet JSP 硬写了现在做 JavaWeb 项目如果还在用原生 Servlet JSP开发效率会低到让你怀疑人生。常见做法是 Spring Boot 打底配合 MyBatis-Plus 做持久层。理由很直接Spring Boot 内嵌 Tomcat打成 jar 包java -jar就能跑部署时不用单独装 Web 服务器。MyBatis-Plus 在 MyBatis 基础上封装了通用 CRUD单表增删改查不用写 XMLBaseMapper直接给你selectById、insert、updateById。配合 Lombok 省掉 getter/setter实体类清爽很多。有热词提到「mybatisplus根据java实体类生成创建表的sql语句」这个能力在项目初期建表时确实省事但我要泼一盆冷水自动生成的 DDL 只能当草稿不能直接上生产。它不会帮你加合理的索引、不会考虑字段长度、不会设置外键约束。我一般用它生成初版然后手工改索引和约束。依赖的核心坐标Spring Boot 3.x 版本!-- pom.xml 关键依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency注意 MyBatis-Plus 在 Spring Boot 3 下要用mybatis-plus-spring-boot3-starter用错了会启动报NoClassDefFoundError这个坑我踩过。2.2 数据库表设计六张核心表撑起整个系统仓库系统的表不用多但每张都要设计到位。核心是这六张表名作用关键字段sys_user用户与角色id, username, password, role, statuswarehouse仓库信息id, name, address, manager_idgoods商品/物料档案id, code, name, spec, unit, category_idinventory实时库存id, goods_id, warehouse_id, quantity, warn_linestock_in入库单id, order_no, goods_id, quantity, operator, create_timestock_out出库单id, order_no, goods_id, quantity, operator, create_timeinventory表是灵魂它记录每个商品在每个仓库的实时数量。这里有个设计决策库存数量是实时算还是存字段我的选择是存字段出入库时同步更新。原因是仓库系统查询频率远高于写入频率每次查库存都去SUM出入库记录数据量一大就慢得没法看。代价是要保证出入库和库存更新在同一个事务里。CREATE TABLE inventory ( id BIGINT NOT NULL AUTO_INCREMENT, goods_id BIGINT NOT NULL COMMENT 商品ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, warn_line INT DEFAULT 10 COMMENT 预警线, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_goods_warehouse (goods_id, warehouse_id), KEY idx_warehouse (warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT实时库存表;uk_goods_warehouse这个唯一索引是关键——它保证同一个商品在同一个仓库只有一条库存记录从数据库层面杜绝了重复插入导致的库存翻倍。idx_warehouse是为了按仓库维度查库存列表时走索引。2.3 出入库的事务处理一个不能省 Transactional 的地方入库操作要同时做两件事写一条stock_in记录更新inventory的数量。这两步必须原子。我见过有人分开写结果入库单写成功了、库存更新失败了账实不符查了半天。Service public class StockInService { Autowired private StockInMapper stockInMapper; Autowired private InventoryMapper inventoryMapper; /** * 入库操作写单据 更新库存同一事务 * param goodsId 商品ID * param warehouseId 仓库ID * param quantity 入库数量必须为正 */ Transactional(rollbackFor Exception.class) public void doStockIn(Long goodsId, Long warehouseId, Integer quantity) { if (quantity null || quantity 0) { throw new BizException(入库数量必须大于0); } // 1. 写入库单 StockIn record new StockIn(); record.setOrderNo(generateOrderNo(IN)); record.setGoodsId(goodsId); record.setWarehouseId(warehouseId); record.setQuantity(quantity); record.setCreateTime(new Date()); stockInMapper.insert(record); // 2. 更新库存存在则累加不存在则插入 Inventory inv inventoryMapper.selectOne( new LambdaQueryWrapperInventory() .eq(Inventory::getGoodsId, goodsId) .eq(Inventory::getWarehouseId, warehouseId)); if (inv null) { inv new Inventory(); inv.setGoodsId(goodsId); inv.setWarehouseId(warehouseId); inv.setQuantity(quantity); inventoryMapper.insert(inv); } else { inv.setQuantity(inv.getQuantity() quantity); inventoryMapper.updateById(inv); } } }rollbackFor Exception.class不能省。默认 Spring 只对RuntimeException回滚如果抛的是受检异常事务不会回滚库存就脏了。generateOrderNo用「前缀 时间戳 随机数」生成别用自增 ID 当单号否则单号会泄露业务量。3. 核心功能落地从登录鉴权到库存预警的完整链路3.1 登录鉴权Session 还是 JWTWeb 项目做权限两条路Session 和 JWT。我的建议是后台管理系统用 Session前后端分离用 JWT。这个项目如果是传统 Web 页面Thymeleaf 或 JSP 渲染Session 更简单Spring Security 或拦截器都能搞定。如果是 Vue/React 前端用 JWTtoken 放请求头。用拦截器做 Session 鉴权的核心代码Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(/static/)) { return true; } HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } return true; } }getSession(false)表示不存在就不创建避免每次请求都生成新 Session 浪费内存。返回 401 而不是重定向到登录页是因为前端拿到 401 后自己跳转更灵活。3.2 库存预警定时任务还是查询时判断库存低于预警线要提醒。两种做法定时任务扫全表或者查询库存列表时实时判断。我选后者因为定时任务扫全表在库存表几万行时开销不小而且预警本身是「看的时候才需要知道」的信息。public ListInventoryVO listInventory(Long warehouseId) { ListInventoryVO list inventoryMapper.selectInventoryList(warehouseId); for (InventoryVO vo : list) { // 库存低于预警线标记为预警状态 vo.setWarning(vo.getQuantity() vo.getWarnLine()); } return list; }selectInventoryList在 XML 里做goods和warehouse的关联查询一次查出商品名、仓库名、库存数避免 N1 查询。前端拿到warningtrue的行标红显示。3.3 分页查询MyBatis-Plus 分页插件配置列表页必须分页否则几千条数据一次查出来页面直接卡死。MyBatis-Plus 的分页需要手动配置插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 分页插件指定数据库类型为 MySQL interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不配这个插件Page对象传进去也不会真正分页SQL 里不会拼LIMIT这是新手最容易翻车的地方之一。分页参数一般pageNum从 1 开始pageSize默认 10前端传参时注意别传 0否则LIMIT -1会报语法错误。4. 避坑与排查那些让我加班到深夜的坑4.1 库存更新并发问题两个仓管同时出库同一商品现象两个仓管同时给同一个商品出库各出 5 件库存本来 8 件结果变成 -2 件。原因先select查库存再update减数量两个线程都查到 8都减 5最后写回 3 或者 -2取决于执行顺序。这是典型的读-改-写竞态。解决用数据库行锁。在select时加FOR UPDATE或者直接用原子 SQLUPDATE inventory SET quantity quantity - #{num} WHERE goods_id ? AND quantity #{num}靠quantity num条件保证不会超卖返回影响行数为 0 就说明库存不足。// 原子扣减避免并发超卖 Update(UPDATE inventory SET quantity quantity - #{num} WHERE goods_id #{goodsId} AND warehouse_id #{warehouseId} AND quantity #{num}) int deductStock(Param(goodsId) Long goodsId, Param(warehouseId) Long warehouseId, Param(num) Integer num);4.2 MySQL 8 时区问题时间差 8 小时现象入库单的create_time存进去是下午 3 点查出来变成早上 7 点。原因MySQL 8 的 JDBC 驱动默认用 UTC 时区而服务器在东八区写入和读取时区不一致。解决连接串加serverTimezoneAsia/Shanghai或者用useSSLfalseserverTimezoneGMT%2B8。完整连接串spring.datasource.urljdbc:mysql://localhost:3306/warehouse?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue在 MySQL 8 用caching_sha2_password认证插件时是必须的不加会报Public Key Retrieval is not allowed。4.3 中文乱码从数据库到页面全链路排查现象商品名「螺丝刀」存进去变成「??」。原因三个环节任一没设对都会乱码——数据库字符集、连接串字符集、页面编码。解决建库时CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci连接串加characterEncodingutf8页面meta charsetUTF-8。用utf8mb4而不是utf8因为 MySQL 的utf8是阉割版存不了 emoji 和部分生僻字。4.4 事务失效方法内部调用不生效现象doStockIn加了Transactional但入库单写进去了、库存没更新事务没回滚。原因在同一个类里A 方法直接调 B 方法this.b()B 方法的Transactional不生效因为没走代理对象。解决把事务方法抽到单独的 Service 类或者注入自己Autowired private StockInService self;用self.doStockIn()调用。最干净的做法是拆类。4.5 分页总数不对关联查询的 count 陷阱现象库存列表分页每页 10 条但总数显示 1000实际只有 200 条。原因MyBatis-Plus 自动生成的 count SQL 在关联查询时可能把 join 后的行数当总数如果 join 产生笛卡尔积就会偏大。解决自定义 count 查询在 XML 里手写SELECT COUNT(DISTINCT i.id) FROM inventory i LEFT JOIN goods g ...用DISTINCT去重。5. 进阶技巧把报表导出和权限控制做扎实5.1 报表导出EasyExcel 比 POI 省内存库存报表要导出 Excel 给老板。用 Apache POI 写几万行会 OOM因为它是全量加载到内存。用阿里开源的 EasyExcel逐行写内存占用极低。public void exportInventory(HttpServletResponse response, Long warehouseId) throws IOException { ListInventoryExportVO data inventoryMapper.selectExportData(warehouseId); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); // 文件名用 URL 编码避免中文乱码 String fileName URLEncoder.encode(库存报表, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment;filename*utf-8 fileName .xlsx); EasyExcel.write(response.getOutputStream(), InventoryExportVO.class) .sheet(库存) .doWrite(data); }InventoryExportVO上用ExcelProperty(商品名称)注解指定列名。文件名用filename*utf-8格式比直接filename对中文兼容性好。注意导出接口不要走拦截器的 JSON 响应逻辑否则会把 Excel 流覆盖掉。5.2 权限控制用注解做细粒度鉴权不同角色能看的菜单不一样。仓管只能出入库财务只能看报表管理员全权限。用自定义注解 拦截器实现Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); // 允许的角色 }拦截器里读方法上的注解比对当前用户角色。这样每个接口需要什么权限一目了然比在 XML 里配 URL 拦截直观得多。注意注解要加在 Controller 方法上加在 Service 上拦截器读不到。5.3 一个验证系统是否可靠的小习惯我做完这类系统一定会做三件事验证第一用两个浏览器同时登录不同账号对同一商品并发出库看库存会不会变负第二把数据库连接断掉看前端报错是不是友好的提示而不是一堆堆栈第三导出一次全量报表看数据条数和数据库COUNT(*)是否一致。这三件事花不了半小时但能挡住 80% 上线后才会暴露的问题。做仓库系统最忌讳的就是「看起来能跑就行」。库存数字差一件可能就是一整批订单发错。我现在的习惯是任何涉及库存增减的代码写完先自己用并发工具压一遍确认没有超卖才提交。这个习惯帮我省掉了无数次半夜被叫起来查账的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表