ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Layui+SSM+SpringBoot仓库信息管理系统:库存流水与防超卖实战

Layui+SSM+SpringBoot仓库信息管理系统:库存流水与防超卖实战 仓库信息管理系统这个题目我在外包和毕设辅导里见过太多版本了绝大多数停留在能跑就行的阶段增删改查四件套、库存数量随便改、两个人同时出库就数据打架。真到一个小工厂或者电商仓库里用三天就出问题。这篇就把我自己做过的一套仓库信息管理系统layui SSM SpringBoot从头到尾拆开讲包括为什么这么选技术、库存怎么算才不会错、Layui 的 tabs 刷新到底怎么处理、SpringBoot 版本选高了会怎么死。不管你是要交毕设、还是给朋友的小仓库做个内部工具或者单纯想把 SSM 那套东西真正搞明白这篇都能直接抄作业。1. 需求拆解与整体设计思路1.1 一线仓库的真实痛点在哪先说清楚这个系统要解决什么。很多人做仓库管理第一反应是商品表 库存字段一个商品一条记录入库加数量出库减数量完事。这套东西在单仓库、单货位、不管批次的场景下确实能跑但只要业务稍微长一点就崩。我接触过的一个真实情况是做五金配件的同一个型号的螺丝钉进了三批采购价不一样客户退货时要按原批次退结果系统里只有一个总数根本对不上。还有库位问题仓库有 8 个货架每个货架 4 层同一个物料可能同时放在 A 区第二层和 C 区第一层拣货员拿着单子到处找。另外就是账实不符——系统显示 500 个实际盘点 483 个差在哪一天、哪一单完全追溯不了。所以这套系统真正要解决的是三件事库存要能拆到仓库 库位 批次这一层不能只有一个总数每一次数量变动都要留流水谁在什么时候、因为哪张单据、把多少数量从多少改到了多少单据要有状态机不能点一下按钮库存就变了得是制单 → 审核 → 确认过账这样一步步走。这三点想清楚了表结构和代码结构基本就定了。反过来说如果一开始只想着能加能减后面想补流水和批次基本等于重做。1.2 表结构怎么设计才不返工仓库系统最容易返工的地方就是表设计。我的经验是主数据表和业务表要彻底分开物料、仓库、库位、供应商、客户这些是主数据改一次影响全局入库单、出库单、盘点单这些是业务单据只增不改。核心表大概这几张表名作用关键点wms_material物料主数据编码唯一安全库存阈值wms_warehouse仓库支持多仓wms_location库位挂在仓库下编码唯一wms_stock实时库存仓库库位物料批次唯一wms_stock_log库存流水只增不改记录前后值wms_in_order / wms_in_item入库单主表/明细一单多明细wms_out_order / wms_out_item出库单主表/明细同上这里有一个我必须强调的点wms_stock 的唯一键一定要是仓库、库位、物料、批次四个字段的组合不是只有物料。很多人图省事只给 material_id 加唯一键做到后面想做库位管理整张表要改主键数据迁移能让你哭。另外 wms_stock_log 这张流水表一定要存 before_qty 和 after_qty不要只存 change_qty。原因很简单只存变动量的话你想查某天这个物料到底有多少得把历史上所有流水加起来几十万条数据一算就是十几秒。存了前后值任何一条流水拿出来就能看清单价。这张表还会长得很快索引给 (material_id, create_time) 就够日常查了。1.3 功能模块和权限边界模块划分我建议按角色走不按技术分层走。仓库系统里典型三种角色管理员、仓管员、查询用户。管理员管主数据物料、仓库、库位、用户、权限仓管员管单据制单、审核、过账、盘点查询用户只能看报表和流水。这个区分不是形式主义是安全底线——如果仓管员能改物料主数据的单位把个改成箱那所有历史库存数据的含义全变了。菜单层面大概是这样基础数据、入库管理、出库管理、库存查询、库存流水、盘点管理、报表统计、系统管理。前七个是业务最后一个是权限。做得再简单权限也得有哪怕只是在拦截器里判断一下角色也比完全没有强。2. 技术选型layui SSM SpringBoot 这三样怎么摆2.1 先纠正一个常见误解SSMSpringBoot到底指什么这个组合名字其实有点历史遗留味道很多人第一眼会懵SSM 是 Spring SpringMVC MyBatisSpringBoot 是另一个东西怎么能同时用真相是SpringBoot 本身不替代 SSM它是把 SSM 的配置自动化了。SpringBoot 内嵌了 Spring 和 SpringMVC你只要引spring-boot-starter-web就等于同时拿到了 Spring 容器和 SpringMVC 的 DispatcherServlet不用再写 web.xml、不用配mvc:annotation-driven、不用管 Tomcat 部署。再引mybatis-spring-boot-starterMyBatis 的 SqlSessionFactory 也自动装配好了。所以layui SSM SpringBoot翻译成人话就是用 SpringBoot 作为运行容器里面跑的是 SpringMVC 做 Web 层、MyBatis 做持久层前端用 Layui 做后台管理界面。你要把它理解成用 SpringBoot 的方式写 SSM 项目就完全通了。这个认知很关键因为网上大量的 SSM 教程是XML 配置流——applicationContext.xml、spring-mvc.xml、mybatis-config.xml 三件套。而 SpringBoot 版本里这些 XML 基本消失改成 application.yml 注解。两者思路一样但写法差很远抄代码的时候千万别混。2.2 版本组合和 JDK 选择这是最大的坑这块我用一个表把常见版本的对应关系钉死你照着抄就不会出错组件推荐版本说明JDK8 或 118 最稳11 也行SpringBoot2.7.182.x 最后一个小版本支持 JDK8mybatis-spring-boot-starter2.3.1配 SpringBoot 2.xMySQL5.7 / 8.08.0 记得改时区参数mysql-connector8.0.33驱动类 com.mysql.cj.jdbc.DriverDruid1.2.20连接池PageHelper1.4.7分页插件Layui2.9.x社区延续版本线为什么把 SpringBoot 钉在 2.7.18 而不是最新版这是新手最容易踩的坑。SpringBoot 3.x 强制要求 JDK 17 起步而且把javax.*全家桶换成了jakarta.*。你从网上抄的任何一段 SSM 老代码只要带javax.servlet.http.HttpServletRequest或者javax.annotation.Resource在 3.x 下直接编译不过。这不是改一行两行的事是满项目找替换。更隐蔽的是 MyBatis 的版本也跟 SpringBoot 大版本绑死SpringBoot 3.x 要配 mybatis-spring-boot-starter 3.x你要是拿 2.3.1 去配 SpringBoot 3启动时报NoSuchMethodError或者 Bean 装配失败报错信息还特别绕新手能查一天。还有一个很多人在 IntelliJ IDEA 里遇到的怪事新版 IDEA 的 Spring Initializr 界面里Java 版本下拉框没有 8 这个选项了最低 17。这不是你环境坏了是官方脚手架默认不再支持老 JDK。解决办法有两个一是把手动下载的 SpringBoot 2.7.18 的 parent 写进 pom.xml 里自己搭工程二是用国内镜像站的 Initializr比如 start.aliyun.com那边还保留 Java 8 选项。提示如果项目是学校毕设或者小企业内部工具不要追新。停在 SpringBoot 2.7.18 JDK 8 这一套教程多、报错少、资料全能省下大量折腾时间。2.3 2024 年了为什么还在用 LayuiLayui 现在确实不是最时髦的选择Vue3 Element Plus 才是主流。但仓库管理这类项目有它特殊的地方使用者是仓管员和财务不是年轻人界面稳定、字体清楚、按钮大比动画炫酷重要得多。而且 Layui 是引入即用不需要 node、不需要打包、不需要 npm install一个 HTML 加两行 script 就能出表格对只懂 Java 不懂前端的人来说这个门槛差异是致命的。Layui 的原作者停止维护后社区在原有版本线上继续维护2.8、2.9 这条线是能正常用的。它自带 table、form、layer、element、date、upload 这些后台管理必备组件做增删改查的表格页面特别快。唯一的短板是组件之间的通信设计比较弱尤其是 iframe 版后台模板里的 tabs 刷新下面第 4 章会专门讲这个。3. 后端核心实现从建表到出入库事务3.1 工程骨架和依赖配置先给一份能直接跑的 pom 依赖片段去掉注释就是完整配置parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意 mysql 驱动这里我用的是com.mysql:mysql-connector-j这是新的坐标。老的mysql:mysql-connector-java在 8.0.31 之后停止更新了虽然还能用但新项目建议直接用新的。两个坐标的驱动类都是com.mysql.cj.jdbc.Driver能通用。工程包结构建议这样分controller接口层、service service.impl业务层、mapper持久层接口、entity实体、dto/vo传输对象、common统一返回、异常、工具。作为一个新手层数不要贪多DTO 和 VO 在中小型项目里合成一个也没关系关键是别让 Controller 里出现 SELECT 语句。3.2 关键配置application.yml配置部分我挑几个重点说完整的贴在下面server: port: 8080 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/wms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1 test-while-idle: true jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.wms.entity configuration: map-underscore-to-camel-case: true pagehelper: helper-dialect: mysql reasonable: true几个不写就会出问题的细节serverTimezoneAsia/Shanghai必须加不加的话 MySQL 8 存时间会差 8 小时useSSLfalseallowPublicKeyRetrievaltrue是为了让 MySQL 8 的默认加密方式能被老驱动连上不加会报Public Key Retrieval is not allowedmap-underscore-to-camel-case: true让下划线字段名自动映射成驼峰属性省掉一大堆 ResultMap。Druid 的 max-active 我一般给 20不要给太大。一个内部仓库系统并发不会高给到 100 反而容易把 MySQL 的连接数打满。test-while-idle: true加上validation-query: SELECT 1能避免 MySQL 8 小时空闲断连之后第一次请求报错的问题这个坑很经典。3.3 入库流程和幂等设计入库的业务逻辑我拆成制单和过账两步。制单只是往 wms_in_order 和 wms_in_item 写记录状态是 0草稿过账才真正动库存状态变 1已过账。为什么要拆开因为现实中入库经常要改数量写错了、供应商临时少送两箱。如果制单就直接加库存改单的时候你得反向扣回去一出一进两条流水不说还得处理已经出库了一部分这种复杂情况。拆成两步草稿单随便改只有确认过账才落库存逻辑干净得多。过账代码大致长这样Service public class StockInServiceImpl implements StockInService { Transactional(rollbackFor Exception.class) Override public void confirmIn(Long orderId, String operator) { // 1. 状态机校验只有草稿单能过账重复点击直接拦掉 StockInOrder order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 0) { throw new BizException(单据不存在或已过账); } int locked orderMapper.updateStatusToPosting(orderId, 0, 2); if (locked 0) { throw new BizException(单据正在处理中请勿重复提交); } // 2. 逐条明细加库存 ListStockInItem items itemMapper.listByOrderId(orderId); for (StockInItem item : items) { StockKey key new StockKey(item.getWarehouseId(), item.getLocationId(), item.getMaterialId(), item.getBatchNo()); int updated stockMapper.increase(key, item.getQty()); if (updated 0) { stockMapper.insertIgnore(key, item.getQty()); } Stock stock stockMapper.selectByKey(key); // 3. 写流水记录变动前和变动后的值 stockLogMapper.insert(buildLog(item, stock, operator)); } orderMapper.updateStatus(orderId, 2, operator); } }这里有个幂等的技巧值得单独说updateStatusToPosting用了一条带状态条件的 UPDATEWHERE id ? AND status 0返回影响行数为 0 就说明单据已经被处理过了直接抛异常。这样就避免了仓管员手抖点两下按钮库存被加两次。这个做法比在 Java 里synchronized靠谱得多因为多实例部署的时候 synchronized 根本挡不住。注意状态值 2 表示处理中是一个中间态。如果处理过程中抛异常事务回滚状态也会回到 0。这个中间态的作用是防止并发重复提交过账完成后改成最终状态。3.4 出库扣减与防超卖出库比入库复杂因为要处理库存不够和从哪个批次出。先讲防超卖。最简单的写法是先查出来判断再更新// 错误示范先查后改并发下必超卖 Stock s stockMapper.selectById(id); if (s.getQty().compareTo(need) 0) throw new BizException(库存不足); stockMapper.updateQty(id, s.getQty().subtract(need));两个人同时出库同一个物料各出 60实际库存 100。两个线程同时查到 100都判断通过都执行扣减最后库存变成 -20。这个就是经典的超卖。正确做法是把判断和扣减合成一条原子 SQLUPDATE wms_stock SET qty qty - #{qty}, version version 1, update_time NOW() WHERE id #{id} AND qty #{qty}AND qty #{qty}这个条件是数据库层面执行的行锁保证同一时刻只有一个更新能生效。返回影响行数 0 就说明库存不够直接抛库存不足。整个判断不需要在 Java 里做也不需要分布式锁。再讲批次分配。出库单上通常只写了物料和数量不指定批次系统要按先进先出自动分配。这时候要按入库时间排序SELECT id, location_id, batch_no, qty FROM wms_stock WHERE warehouse_id #{warehouseId} AND material_id #{materialId} AND qty 0 ORDER BY inbound_date ASC, id ASC查出来的库存清单在 Java 里循环扣减扣到数量够了为止。一单可能跨好几个库位、好几个批次所以出库明细下还要有一层批次分配明细否则你后面查这批货发给客户的是哪个批次就查不到。这一层在很多简化版系统里被省掉了省的时候很爽出问题的时候很难受。3.5 分页和统一返回格式Layui 的 table 组件对返回格式有固定要求{code: 0, msg: , count: 总数, data: [列表]}。code 不为 0 会直接弹错误。所以后端要写一个统一返回类Data public class TableResultT { private Integer code 0; private String msg ; private Long count; private ListT data; public static T TableResultT ok(Long count, ListT data) { TableResultT r new TableResult(); r.setCount(count); r.setData(data); return r; } }分页用 PageHelper 是最省事的在 Service 里PageHelper.startPage(page, limit)之后紧跟的那一次查询就会被自动分页然后new PageInfo(list).getTotal()拿总数。要注意startPage只对紧跟其后的第一条查询生效中间不要插别的查询插了分页就会跑到别的语句上去这个坑我见过好几次。4. Layui 前端落地实操4.1 表格渲染和列模板Layui 的表格渲染核心就是一个table.render。库存查询页的典型写法table.render({ elem: #stockTable, url: /api/stock/page, method: get, page: true, limit: 20, limits: [20, 50, 100], cols: [[ {type: checkbox}, {field: materialCode, title: 物料编码, width: 140}, {field: materialName, title: 物料名称, minWidth: 180}, {field: warehouseName, title: 仓库, width: 120}, {field: locationName, title: 库位, width: 110}, {field: batchNo, title: 批次, width: 130}, {field: qty, title: 库存数量, width: 110, align: right, templet: function(d){ return d.qty null ? 0.00 : Number(d.qty).toFixed(2); }}, {field: safetyStock, title: 安全库存, width: 110, align: right}, {fixed: right, title: 操作, width: 150, toolbar: #barTpl} ]] });有两个细节得注意。第一是数字列一定要align: right加上templet格式化因为后端返回的 BigDecimal 序列化成 JSON 可能是 500 也可能是 500.0000不格式化看起来很乱。第二是fixed: right的操作列如果放按钮建议用toolbar加模板的方式而不是在templet里拼 HTML 字符串前者更好维护。安全库存这一列我建议加个颜色提示低于安全库存的显示红色。做法是在 templet 里判断返回带 class 的 span配合自定义 CSS 就能实现。仓库管理员最需要的其实就是哪些货快没了这个视觉提示比任何报表都直接。4.2 表单提交和父子页面刷新这块是 Layui 后台模板里最让人头疼的部分因为它涉及 iframe 通信。先说结构Layui 的 iframe 版后台左边是菜单右边是 tabs每个 tab 里塞的是一个独立的 HTML 页面iframe。列表页和新增表单页很可能是两个不同的 iframe。你在新增页提交完要做的动作是刷新列表页的那个 iframe 里的表格这两个 document 不是同一个。所以下面这种写法是无效的// 无效这是在表单页自己的 document 里找 table layui.table.reload(stockTable);正确的写法是找到父页面再找到列表页的 windowform.on(submit(saveMaterial), function(data){ $.post(/api/material/save, data.field, function(res){ if (res.code 0) { parent.layer.msg(保存成功, {icon: 1}); // 方式一列表和数据在同一个页面非 iframe 模式 // parent.layui.table.reload(materialTable, {page: {curr: 1}}); // 方式二列表在另一个 iframe 里通过 contentWindow 调用它的刷新函数 var listFrame parent.$(iframe[namematerialList])[0]; if (listFrame listFrame.contentWindow.reloadTable) { listFrame.contentWindow.reloadTable({page: {curr: 1}}); } // 关闭当前 tab var idx parent.layer.getFrameIndex(window.name); parent.layer.close(idx); } else { layer.msg(res.msg, {icon: 2}); } }); return false; });在列表页里你需要主动暴露一个刷新函数出来window.reloadTable function(opt){ table.reload(materialTable, opt || {}); };这套做法的好处是不依赖具体模板实现只要 iframe 的 name 对得上就能刷新。另外parent.layer.close(idx)关闭当前 tab 时一定要用parent.layer.getFrameIndex(window.name)拿索引不要自己算算不准就会关错 tab。还有一个新手常踩的坑Layui 的 tabs 每点一次菜单就开一个 tab来回点几次同一菜单会开出一排重复 tab。解决办法是在打开 tab 之前先查一下有没有同名的有就切过去而不是新建。这个逻辑通常要改后台模板的index.js思路是遍历已有的 tab 标题命中就element.tabChange。4.3 导出、打印和批量操作仓库系统里导出 Excel 是刚需财务每月要对账。最简单的方式是后端用 POI 或 EasyExcel 生成前端直接window.location.href /api/stock/export?...触发下载。用 location 跳转而不是 ajax是因为 ajax 拿不到浏览器的下载行为还得额外处理 blob费劲。导出时有个细节要注意导出一定要带上当前的查询条件。用户筛了某仓库某物料点了导出结果导出来是全库数据这是使用体验上最容易被投诉的问题。做法是把 table 的查询条件同步到导出 URL 上Layui 的table.reload之前可以用table.cache拿到当前条件。批量操作则是用表格第一列的 checkbox 配合工具栏按钮选中后拿到table.checkStatus(stockTable).data把 id 数组传给后端。这个逻辑没什么难度关键是后端接口要设计成接收 id 列表 一个操作类型而不是每个小功能开一个接口。4.4 菜单权限怎么跟前端配合权限这块后端我一般做三层登录态用 Session 或 JWT接口层用拦截器校验登录方法层用注解校验角色。前端做的是根据后端返回的菜单列表动态渲染左侧菜单。这里要提醒一点前端隐藏菜单不等于安全。菜都藏起来了用户直接敲 URL 还是能访问。所以每个接口都必须独立校验权限前端的菜单渲染只是体验优化。我在审别人代码时见过太多次前端把按钮藏了就当权限做好了这是很典型的安全漏洞。菜单渲染的做法是登录后调/api/menu/list拿到树形菜单 JSON然后递归拼 HTML 塞进 Layui 的element导航里。菜单表设计成带 parent_id 和 sort 的自关联表就够用了不用搞太复杂。5. 部署上线与真实验证5.1 打包和启动方式SpringBoot 的部署有两种主流方式打成 jar 直接跑或者打成 war 丢进外置 Tomcat。仓库系统我推荐第一种mvn clean package之后java -jar wms.jar --spring.profiles.activeprod干净利落不依赖外部容器。生产环境的配置要单独放一个application-prod.yml数据库地址、密码、日志级别都跟开发环境隔开。密码不要明文写在配置文件里能用环境变量就用环境变量。启动脚本里加一句nohup java -jar ... logs/app.log 21 配合定时清理日志基本就够一个内部系统用了。Layui 是纯静态资源直接放在src/main/resources/static下面就行SpringBoot 会自动映射。不需要 Nginx 也不需要单独的前端服务这也是这套技术组合对个人开发者特别友好的地方——一个 jar 包就是一个完整系统。5.2 数据量和性能的真实情况我拿自己那套跑了组数据做个参考环境是 2 核 4G 的云主机MySQL 8 同机场景数据量耗时库存分页查询带库位和物料联表库存表 5 万行约 60ms库存流水查询按物料时间流水表 80 万行约 120ms单张入库单过账含 30 条明细-约 250ms出库 FIFO 分配跨 8 个批次-约 180ms能看出来这个量级下性能完全不是问题。真正会拖慢的是两件事一是联表查询没建索引物料名称模糊搜索用 LIKE %xx%几十万行一扫就是几秒二是流水表没按月归档跑了两年之后单表上千万行任何查询都变慢。所以我的做法是模糊搜索的字段建前缀索引能用 xx% 就不写 %xx%流水表按年分表或者定期归档一年以上的数据挪到历史表。这两招下去一个中小仓库用三五年不会有明显性能问题。5.3 后续可以往哪些方向扩如果这套系统已经稳定跑了往下扩展有几个方向值得考虑。一是扫码进货出货用扫码枪直接对接物料编码比手输快十倍Layui 这边只要监听输入框的回车事件就行。二是对接电子秤或者标签打印机这个要看具体硬件。三是加一个简单的预警通知安全库存低于阈值时在首页飘红或者定时发个消息。至于要不要换成前后端分离我的看法是如果只是内部几个人用Layui 这套足够了别折腾。微服务、Redis 缓存、消息队列这些东西在仓库日单量几百的场景下纯属增加复杂度。等技术栈真的成为瓶颈了再换那时候你也积累够了改的经验。6. 常见问题速查与排查思路6.1 环境和版本类问题这一类问题占了新手求助的八成我整理成表现象根因解决启动报 javax 包找不到SpringBoot 3.x 用了 jakarta降级到 2.7.18 或全局替换包名NoSuchMethodError 在 MyBatis 装配时starter 版本和大版本不匹配SpringBoot 2.x 配 mybatis starter 2.3.1MySQL 连接报 Public Key Retrieval8.0 默认加密方式URL 加 allowPublicKeyRetrievaltrue时间差 8 小时时区没指定URL 加 serverTimezoneAsia/ShanghaiIDEA 建工程没有 Java 8 选项新版脚手架不支持手写 pom 或用镜像站 Initializr第一次请求报连接已关闭空闲连接被 MySQL 断开开启 test-while-idle还有一个特别隐蔽的问题Lombok 版本和 JDK 不匹配。JDK 8 配新版 Lombok 有时候编译报一堆莫名其妙的错遇到这种情况先把 Lombok 降到 1.18.24 左右往往就好了。6.2 数据和事务类问题库存对不上但找不到原因这是反馈最多的问题。排查顺序是先看流水表最后一条的 after_qty 和库存表的 qty 是否一致不一致说明有代码绕过了流水直接改库存去搜 SQL 有没有漏写日志的地方。再看有没有同一张单据被过账两次看流水里同一个 biz_no 出现了几次。出库偶尔报死锁一般是扣减多个批次的时候加锁顺序不一致导致的。两个事务各锁了两个库位顺序相反就互相等。解决办法是在扣减之前按 stock_id 排序保证所有事务的加锁顺序一致。这个在并发量上来之后才会暴露小规模测试根本测不出来。事务不生效八成是这几个原因方法不是 public、同类内部调用this.xxx() 走的是代理对象之外的路径、异常被 catch 吞掉没往外抛、用的是默认的 RuntimeException 回滚规则但抛的是受检异常。我一般直接写Transactional(rollbackFor Exception.class)把受检异常也覆盖掉。6.3 前端交互类问题Layui 表格数据不显示接口明明返回了数据。检查三处返回 JSON 里 code 是不是 0count 字段名是不是count不是 totaldata 是不是数组。有一处不对表格就是空的而且不报错。表单重复提交。Layui 的 submit 回调里如果没return false表单会因为默认行为刷新页面看起来像提交了两次。另外提交按钮要加 loading 状态防止连点。tabs 关闭后页面还留着。Layui 的 iframe 关 tab 时要同时layer.close和从 tab 列表里移除只做一样会留下空白或残留。这个逻辑跟具体使用的后台模板有关得看清楚模板里element.tabDelete的调用方式。日期控件选完时间格式不对。后端返回的是2024-01-01 10:00:00但 Layui 的 laydate 默认可能按时间戳处理。统一在laydate.render里指定type: datetime和format: yyyy-MM-dd HH:mm:ss前后端格式对齐就不会出问题。这些坑我自己大部分都踩过一遍说穿了都不复杂但没人告诉你的时候一个小问题能耗掉一整天。做技术项目最怕的不是难题是这种看起来莫名其妙的低级报错把人的耐心一点点磨掉。真正做起来你会发现这套 layui SSM SpringBoot 的组合最大的价值不在于技术有多先进而在于它足够稳、资料足够多、遇到问题总能搜到答案对于要把事情做完、做稳的场景这一点比任何时髦的技术都重要。
返回列表