
搞过物资管理系统的朋友应该都知道这类项目最磨人的不是CRUD本身而是库存逻辑、权限模型和数据一致性之间的纠缠。这次分享一套基于SpringBootVue的物资综合管理系统源码技术栈是SpringBoot Vue MyBatis MySQL前后端分离覆盖了物资入库、出库、调拨、盘点、报废、统计报表等核心业务闭环适合拿来当毕业设计、企业内训项目或者二次开发的底座。如果你正在准备SpringBoot和Vue相关的面试这套系统里的登录鉴权、动态路由、MyBatis缓存、事务处理都是高频考点。如果你是想快速搭一套能用的管理系统那这部分源码基本可以“开箱即改”。我按实际开发中踩过的坑、做过的取舍、调过的配置来复盘一遍争取把设计思路和落地细节都讲透。1. 系统整体设计与技术选型拆解1.1 为什么选前后端分离SpringBoot Vue的组合逻辑先聊聊技术选型背后的真实考量。物资综合管理系统的特点是表格多、表单多、权限维度复杂、报表导出频繁。如果用传统的服务端渲染模板比如Thymeleaf、JSP页面一旦复杂起来前后端职责会搅在一起改一个查询条件都要动页面模板联调效率很低。换成SpringBoot Vue之后前后端通过JSON交互Vue负责页面交互和组件复用SpringBoot只负责提供RESTful接口。这样做的好处非常直接前端组件可以沉淀比如物资表格、审批弹窗、统计图表都能抽成公共组件后端接口可以被微信小程序、移动端H5复用不用为每个端重写一套逻辑。SpringBoot在这套系统里承担的是“聚合根”的角色。它整合了MyBatis作为数据访问层整合了Spring Security或拦截器做接口鉴权整合了MinIO做附件存储再通过Maven统一管理依赖版本。这里我说一个实际踩过的坑SpringBoot版本不要盲目追新2025年这个时间点2.7.x依然是很稳的选择3.x虽然性能更好但部分老牌MyBatis插件和连接池的兼容性需要额外适配对于业务系统来说稳定优先。1.2 项目目录结构与模块职责划分这套源码的目录结构是标准的Maven多模块单工程结构没有强行拆成微服务这对物资管理系统来说是合理的。微服务是有代价的如果业务量没到那个规模拆了反而增加部署和运维成本。后端包结构大致是这样com.taizhou.material ├── common # 公共模块统一返回结果、异常处理、工具类 ├── config # 配置类MyBatis、CORS、拦截器、文件上传 ├── controller # 接口层接收请求、参数校验、返回数据 ├── service # 业务层核心业务逻辑、事务控制 ├── mapper # 数据访问层MyBatis的Mapper接口 ├── entity # 实体类对应数据库表结构 ├── dto # 数据传输对象接口入参出参 └── utils # 工具类JWT工具、Excel导出、日期处理前端Vue部分的结构采用了标准的Vue CLI工程src ├── api # 按模块封装的请求接口 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置含动态路由 ├── store # Vuex状态管理 ├── views # 页面视图 └── utils # 请求封装、权限指令这个结构我建议直接沿用不要在初期过度设计。真正的业务复杂度集中在service层和mapper层的SQL上过早的抽象只会让新人接手时一头雾水。2. 数据库设计库存类系统的核心命门2.1 八张核心业务表的设计思路物资管理系统的数据库设计是整个项目的灵魂。很多同学喜欢把字段堆得很全但真正设计良好的表结构是“够用且扩展性好”。这套源码中核心表包括用户表、角色表、菜单权限表、物资分类表、物资信息表、入库单表、出库单表、库存表。用户表和权限表是标准RBAC模型用户-角色-菜单三级关联前端配合动态路由实现按角色渲染菜单。物资分类表采用父子层级结构用parent_id字段实现树形分类支持无限层级分类。物资信息表要特别注意唯一性约束物资编码要设置为唯一索引避免同一编码重复入库。库存表是重中之重它的设计直接关系到并发安全核心字段包括物资ID、仓库ID、当前数量、预警阈值、状态字段。这里我要强调一个设计细节库存表不要和物资信息表合并成一张表。虽然直观上“物资就应该有库存数量”但一旦物资属性修改或者库存流水增加合并表的锁竞争会非常严重。拆开后物资表负责静态属性库存表负责动态数量各司其职。2.2 关键字段类型与索引设计经验我在实际建表时有一些具体经验和大家分享。数量字段建议用DECIMAL(12, 2)而不是FLOAT或者DOUBLE。为什么因为浮点数在MySQL中会存在精度丢失的问题物资数量一旦涉及金额核算或者报损计算差一分钱在报表上都对不上账。金额字段同样使用DECIMAL(12, 2)。所有业务表建议统一带上这几个字段create_by创建人、create_time创建时间、update_by更新人、update_time更新时间、deleted逻辑删除标识。逻辑删除非常重要物资系统的单据不能物理删除否则报表历史数据会断链。数据无价删了就真的没了。索引方面有几个是必须加的物资编码唯一索引、入库单号和出库单号唯一索引、库存表的(material_id, warehouse_id)联合唯一索引。联合唯一索引的意义在于数据库层面兜底防止同一物资在同一仓库出现两条库存记录。查询频繁的字段比如单据日期、物资分类ID也应该加上普通索引避免全表扫描。2.3 初始化SQL脚本里的那些细节这套源码自带的init.sql脚本挺完整的包含建库、建表、初始数据。我建议在使用前先过一遍特别是注意字符集和排序规则CREATE DATABASE IF NOT EXISTS material_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;千万不要再用utf8了utf8在MySQL中最多存3个字节遇到生僻字或者Emoji就会报错。utf8mb4才是真正的完整UTF-8编码支持。排序规则推荐utf8mb4_general_ci它不区分大小写做条件查询时行为更符合直觉。初始化数据里会默认创建一个管理员账号密码通常是经过BCrypt加密的。如果你不想用自带的加密密码可以用Spring Boot的测试类跑一段代码生成BCrypt密文替换掉这个后面会提到。3. 后端核心模块实现要点3.1 JWT登录鉴权与SpringBoot拦截器配置登录模块是这套系统的入口源码采用JWTJSON Web Token实现无状态鉴权。流程是用户提交用户名密码后端校验通过后生成Token返回给前端前端后续请求在Header里带上Authorization: Bearer token后端通过拦截器校验Token有效性。JWT相比Session方案的好处是天然支持前后端分离和横向扩展不需要在服务端保存会话状态。但在实际项目里有个安全点必须处理Token过期时间不能太长。我习惯设置成2小时有效期并配合前端路由守卫实现过期自动跳转登录页。拦截器配置在SpringBoot中是这样的思路Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /captcha, /error); } }注意放行路径一定要包含登录接口和验证码接口否则用户根本进不来系统。我在刚接手项目时遇到过一个问题由于放行了/doc.html和/webjars/**导致接口文档页面绕过鉴权直接访问内部接口信息后来把这些路径全部收紧了。千万记住放行的路径越少越好。另外建议在配置类中显式开启CORS跨域支持否则部署到不同端口时前端请求会被浏览器拦截。需要重点关注的是allowCredentials(true)要和allowedOriginPatterns(*)配合使用在较新的Spring版本中allowedOrigins(*)配allowCredentials(true)会直接报错。3.2 入库与出库的业务逻辑闭环入库单和出库单是物资系统的两条核心流水。很多同学写这部分逻辑时会漏掉一个关键点业务操作和库存更新必须放在同一个事务里。入库逻辑相对简单一些前端提交入库单数据物资明细、数量、入库仓库、经办人后端生成入库单主记录和明细记录遍历明细查询库存表是否存在该物资在该仓库的库存记录存在则数量累加不存在则新建库存记录记录库存流水方便后续追溯出库逻辑就更复杂因为涉及库存扣减的校验。核心代码如下Transactional(rollbackFor Exception.class) public void outbound(OutboundDTO dto) { for (OutboundItem item : dto.getItems()) { Stock stock stockMapper.selectByMaterialAndWarehouse( item.getMaterialId(), dto.getWarehouseId()); if (stock null || stock.getQuantity().compareTo(item.getQuantity()) 0) { throw new BusinessException(库存不足物资编码 item.getMaterialCode()); } // 扣减库存 stock.setQuantity(stock.getQuantity().subtract(item.getQuantity())); stockMapper.updateById(stock); // 记录流水 stockLogMapper.insert(buildLog(stock, item, OUT)); } }这里用了Transactional(rollbackFor Exception.class)这个注解的细节值得说清楚。默认情况下Spring的事务管理只有在遇到RuntimeException时才会回滚如果你在业务代码中抛出了Exception类型的自定义异常不加rollbackFor就可能导致库存扣了但单据没生成成功出现严重的数据不一致问题。这算是我踩过的最深刻的坑之一。3.3 MyBatis的使用细节与动态SQL实战这套源码的持久层用的是MyBatisMapper接口结合XML文件。我推荐在MyBatis项目里坚持使用XML编写复杂SQL而不是全部堆注解因为动态SQL的判断条件一旦变多注解会变得难以维护。以物资条件分页查询为例动态SQL是这样的select idselectMaterialPage resultTypecom.taizhou.material.entity.Material SELECT * FROM material_info where if testkeyword ! null and keyword ! AND (material_code LIKE CONCAT(%, #{keyword}, %) OR material_name LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理第一个条件前面的AND避免“WHERE AND”这种SQL语法错误这是MyBatis最实用的特性之一。另外注意LIKE查询用CONCAT(%, #{keyword}, %)不要直接用%${keyword}%后者存在SQL注入风险因为${}是字符串拼接#{}才是预编译参数两者有本质区别。如果你对MyBatis的缓存机制有疑问二阶段缓存这里简单提一下。MyBatis一级缓存是SqlSession级别的默认开启二级缓存是Mapper级别的需要手动开启。在物资管理系统这种并发读写频繁的场景下二级缓存我建议直接关掉。原因很简单库存数据实时性要求极高缓存一旦没刷新前端显示的可售库存和实际库存不一致轻则用户体验受损重则超卖。缓存不是越多越好用对地方才是关键。3.4 事务控制与库存并发扣减的解决方案库存的超卖问题是物资系统最容易出的生产事故尤其在多个用户同时对同一个物资出库时。传统的做法是先查库存再更新但查和更新之间存在时间差并发场景下两个请求都查到库存充足然后都执行扣减最终库存就变成了负数。源码里提供了乐观锁的解决方案。数据库层面在库存表增加version字段更新时带上版本号判断int rows stockMapper.reduceStock(materialId, warehouseId, quantity, version); if (rows 0) { throw new BusinessException(操作太频繁请重试); }对应的SQL是update idreduceStock UPDATE stock SET quantity quantity - #{quantity}, version version 1 WHERE material_id #{materialId} AND warehouse_id #{warehouseId} AND quantity #{quantity} AND version #{version} /update这里的关键是通过quantity #{quantity}条件让数据库在原子操作中完成库存校验配合version条件确保并发安全。这种方案在实际生产环境中非常稳定比分布式锁成本低也没有死锁风险。如果你遇到库存更新返回0行的情况不要直接报“系统异常”而是提示用户“库存已变更请刷新后重试”体验会好很多。4. 前端Vue实现从路由到组件封装的完整方案4.1 前端工程结构与环境准备要点前端基于Vue CLI构建建议使用Node.js 16.x或18.x LTS版本。我在实际开发中发现Node.js版本过高或过低会导致依赖安装失败或者构建时内存溢出。具体的安装和环境配置这里不展开但有一个很常见的坑值得提醒npm install时如果网络状况不理想建议配置淘宝镜像源之后再安装npm install -g cnpm --registryhttps://registry.npmmirror.com cnpm install国内网络环境下这个操作能节省大量等待时间。项目跑起来之前还需要确认vue.config.js中配置了开发服务器的代理将/api开头的请求转发到后端服务端口避免开发环境出现跨域问题devServer: { port: 8080, proxy: { /api: { target: http://localhost:8088, changeOrigin: true } } }这里的target值要改成你本机后端启动的实际端口我见过不少新手因为端口没对上前端请求全部404还以为是代码写错了。4.2 动态路由与前端权限控制动态路由是这套系统里比较出彩的功能。本质逻辑是用户登录成功后后端返回该用户的角色和对应菜单权限列表前端根据菜单数据动态生成路由表再通过router.addRoutes()注册到Vue Router中。实现思路分为四步登录成功后将后端返回的角色信息存到Vuex和localStorage在前端路由配置中定义一份“基础路由”和一份“动态路由映射表”在路由守卫中判断用户是否已加载过动态路由没有则根据角色请求菜单权限调用router.addRoutes()挂载动态路由使用next({ ...to, replace: true })重新进入目标路由这里最关键的一个细节是路由守卫里要防止死循环。如果你在动态路由加载完成后不判断router.hasRoute()而是每次跳转都重新请求权限接口就会出现“跳转A - 加载路由 - 跳转A - 加载路由”的死循环。正确做法是在Vuex中维护一个isDynamicRoutesLoaded状态位只有为false时才加载动态路由。菜单权限之外按钮级别的权限控制也很有用。源码中封装了一个v-permission自定义指令根据当前用户权限点列表控制按钮的显隐。比如“删除物资”按钮只在用户拥有material:delete权限时才渲染这个设计比单纯控制路由的粒度更细实际业务中非常实用。4.3 Axios请求封装与统一异常处理前端请求层我习惯做统一封装源码里在utils/request.js中基于Axios做了一层封装核心是请求拦截器和响应拦截器。请求拦截器主要做两件事从localStorage取Token并加到请求头判断URL是否需要携带Token。响应拦截器主要做三件事统一处理HTTP错误码处理后端返回的业务错误码遇到401时清空用户信息并跳转登录页。service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 系统异常) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { store.dispatch(user/resetToken) router.push(/login?redirect${router.currentRoute.fullPath}) } Message.error(error.message || 请求失败) return Promise.reject(error) } )指一个非常容易漏掉的细节如果后端返回的是二进制文件流比如Excel导出response拦截器就不能走res.code判断逻辑因为返回的是Blob对象。需要用responseType: blob单独处理这算是我在写导出功能时实打实踩过的坑。4.4 表格与表单组件的高效复用物资管理系统的前端页面高度相似都是左侧搜索条件、中间表格、右侧操作按钮的结构。源码里封装了ProTable组件通过配置列定义、请求接口、操作按钮数组就能快速生成一个完整的数据展示页面。组件内部处理了分页、加载状态、空数据占位、操作列插槽等常规逻辑。这种抽象的好处非常明显新增一个物资分类管理页面前端只需要写几行配置代码基本上不需要重复编写模板。我把这种开发方式叫作“配置式开发”它特别适合表单和表格密集的管理系统。但组件封装也不要过度我曾经见过有人把表格组件封装到“连列定义都需要用JSON Schema配置”的程度结果维护成本比不用组件还高。好的封装是让90%的场景可以配置化10%的特殊场景提供插槽和props扩展而不是把所有灵活性都吞掉。5. 附件存储与扩展功能落地5.1 MinIO集成从本地存储到对象存储物资系统经常需要上传物资图片、采购合同扫描件、验收单据等附件。前期开发阶段可以先把文件存到本地磁盘但生产环境不建议这样做一是本地磁盘空间有限二是应用服务重启或扩容时文件会丢失三是大文件吞吐会影响应用性能。把MinIO集成到SpringBoot中是目前很流行的方案。MinIO是一个开源的对象存储服务兼容Amazon S3接口部署简单特别适合中小型项目的私有化部署。集成过程的思路是本地或服务器部署MinIO服务默认端口9000控制台端口9001SpringBoot配置文件中配置MinIO的Endpoint、AccessKey、SecretKey和Bucket名称编写MinioService封装上传、下载、删除等方法Controller提供文件上传接口返回文件的访问URL物资表单中通过URL回显图片或附件核心的配置类代码如下Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.accessKey}) private String accessKey; Value(${minio.secretKey}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }文件上传接口中要注意生成唯一的文件名防止重复覆盖。我习惯用UUID拼接原文件扩展名String objectName UUID.randomUUID().toString().replace(-, ) . file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(.) 1);5.2 播放视频附件m3u8与前端视频播放方案物资管理系统里的安全培训视频、设备操作录像等这类视频附件可以直接以mp4格式存储后简单播放。但如果你想做更专业的视频支持m3u8这种HLS流媒体格式是值得了解的。m3u8本质是一个索引文件里面记录了一串TS视频分片的URL。播放器按顺序加载这些分片就能实现流畅播放而且天然支持拖动进度和倍速播放。前端播放m3u8不需要安装任何插件基于hls.js就能实现我实测下来很稳。实现思路是在Vue组件中引入hls.js判断浏览器是否支持Hls.isSupported()支持则动态创建播放器实例import Hls from hls.js mounted() { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(this.videoUrl) hls.attachMedia(this.$refs.video) } else if (this.$refs.video.canPlayType(application/vnd.apple.mpegurl)) { // 原生支持HLS的浏览器比如Safari this.$refs.video.src this.videoUrl } }这里有个实际应用中的坑如果m3u8来自MinIO或其它带鉴权的存储服务URL中可能携带临时签名参数hls.js在解析分片时会请求相对路径的TS文件此时签名参数会丢失导致分片加载失败。解决方案是在后端写一个转接口对流地址做代理转发或者在存储服务上为HLS文件单独设置公开读权限和CORS规则。5.3 中文分词与全文检索的轻量集成物资系统里经常会按物资名称的关键词搜索物资。MySQL自带的LIKE %关键词%在数据量小的时候够用但一旦物资数据超过十万级LIKE查询的代价就变得很高而且无法实现“搜电源线也能匹配到电缆”这种语义搜索。手写分词器又过于复杂好在有HanLP这样的轻量方案可以嵌入SpringBoot。HanLP是一个开源的自然语言处理工具包支持中文分词、词性标注、命名实体识别等。集成到SpringBoot中的常规做法是引入依赖后直接调用分词API将分词结果存储到数据库的搜索字段中查询时先分词再按词匹配。这样做的好处是搜索体验大幅提升坏处是多了一张分词索引表需要维护更新。如果项目刚起步我建议先用MySQL的LIKE查询顶着等确实出现性能瓶颈后再考虑引入中文分词和全文检索。6. 部署上线与运维排障实战6.1 多环境配置管理与Jar包打包这套系统分为开发、测试、生产三个环境SpringBoot中通过application-{profile}.yml文件区分。配置文件的拆分思路很简单公共配置放在application.yml环境相关配置放在application-dev.yml、application-test.yml、application-prod.yml启动时指定激活哪个环境。生产环境启动命令我一般是这样用的nohup java -Xms512m -Xmx1024m -jar material-admin.jar \ --spring.profiles.activeprod \ --server.port8088 \ server.log 21 nohup和配合使用可以让进程在后台运行日志输出到server.log文件。JVM内存参数根据服务器配置调整不要盲目设置大值Heap设置得过大反而会导致GC停顿时间变长。对于这套系统512M初始堆和1G最大堆在4核8G的服务器上是比较健康的配置。Maven打包时有些同学会遇到“程序包不存在”的错误这通常是因为子模块之间的依赖没有先install到本地仓库。解决办法是在根目录执行mvn clean install -DskipTests注意-DskipTests只是跳过测试运行但会编译测试代码。如果你想连测试代码编译都跳过用-Dmaven.test.skiptrue更彻底。6.2 MySQL 8连接配置与常见坑这套源码默认使用MySQL 5.7或8.0。MySQL 8和5.7在连接配置上有几个明显的差异我特意拿出来说明一下。驱动类名不同。MySQL 5.7用com.mysql.jdbc.DriverMySQL 8必须用com.mysql.cj.jdbc.Driver。虽然新版驱动兼容旧类名但会在启动时输出警告日志建议直接用新类名。URL需要带上时区参数。MySQL 8的默认时区是UTC如果不显式指定serverTimezone查询时间字段时会和本地时间相差8小时。配置如下spring: datasource: url: jdbc:mysql://localhost:3306/material_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这里useSSLfalse在本地开发环境可以关掉避免SSL握手报错。allowPublicKeyRetrievaltrue是MySQL 8的一个坑使用非SSL连接时如果用户密码是caching_sha2_password加密方式客户端需要先获取RSA公钥才能验证密码不加上这个参数就会抛出Public Key Retrieval is not allowed错误。6.3 Vue项目打包后如何整合进SpringBoot很多同学会问Vue打包后的静态文件能不能直接塞进SpringBoot的resources/static目录实现一个端口跑前后端答案是能但生产环境我强烈不建议。如果确实要单端口部署方法是先执行npm run build生成dist目录然后把dist下的文件复制到SpringBoot的src/main/resources/static目录重启应用即可。SpringBoot默认会把这部分静态资源映射为根路径访问。要注意的是前端路由的history模式会有一个严重问题用户直接访问/material/list这类深层路径时后端不知道这是前端路由会返回404。解决方式是配置一个错误页面跳转到首页Controller public class IndexController { RequestMapping(value /{path:[^\\.]*}) public String redirect() { return forward:/index.html; } }路由回退配置是单端口部署的必备操作千万不能漏。生产环境用Nginx做前端静态托管、反向代理到后端才是主流方案。Nginx配置的要点是location /指向Vue的dist目录location /api反向代理到SpringBoot服务。同时还要配置gzip压缩静态资源性能提升非常明显。6.4 MySQL连接报错与数据库访问问题排查表不管是在本地开发还是服务器部署连接MySQL这一步总是会出各种问题。我把常见的报错和排查方向整理成一张表对照着处理会快很多。报错信息可能原因处理方案Access denied for user rootlocalhost账号密码错误确认密码MySQL 8初次安装后默认密码在安装日志中Public Key Retrieval is not allowed驱动版本和认证插件不兼容连接URL增加allowPublicKeyRetrievaltrueThe server time zone value is unrecognizedMySQL时区未设置连接URL增加serverTimezoneAsia/Shanghaicom.mysql.cj.exceptions.InvalidConnectionAttributeException驱动类名错误使用com.mysql.cj.jdbc.DriverTable xxx doesnt exist表名或库名错误检查DP库、表名大小写敏感性Linux下MySQL默认区分大小写Unknown column xxx in where clauseSQL字段不存在对照实体类字段和数据库字段检查驼峰映射还有一个容易忽略的点MySQL 8的默认认证插件是caching_sha2_password而一些比较老的数据库连接池比如Druid的某些版本和图形化工具比如旧版Navicat对这个插件的兼容性不好。如果遇到“Authentication plugin caching_sha2_password cannot be loaded”错误可以单独把用户改成mysql_native_password认证ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这样改虽然降低了认证强度但在内网开发环境是完全够用的。如果对外提供服务还是建议保留caching_sha2_password并处理驱动兼容。7. 使用这套源码的注意事项与避坑指南7.1 拿到源码后的初始化步骤新拿到这套源码不要急着启动按顺序做好这几步能省掉很多排查时间第一步检查JDK版本。SpringBoot 2.7.x建议用JDK 8或11不要用JDK 17以上否则会出现一些反射相关的兼容问题。执行java -version确认版本。第二步导入数据库脚本。打开Navicat或命令行工具先创建数据库再执行SQL脚本mysql -uroot -p init.sql第三步修改后端application-dev.yml中的数据库账号密码和端口。不要沿用脚本里的默认密码改成自己本地的实际配置。第四步启动后端。看到SpringBoot启动日志输出“Started Application”后用浏览器访问http://localhost:8088/doc.html能打开接口文档就说明后端没问题。第五步启动前端。在Vue工程目录执行npm install安装依赖然后npm run serve启动开发服务器浏览器访问http://localhost:8080用管理员账号登录。整个启动流程走通之后再逐步去改代码和业务逻辑不要一上来就想着改造。7.2 Jackson序列化和日期格式的统一约定前后端联调时最让人头疼的问题之一就是日期格式不一致。Java端默认序列化的LocalDateTime格式是2025-01-15T10:30:00而前端组件期望的格式是2025-01-15 10:30:00导致表格时间列显示异常。统一的解决方案是配置Jackson的全局日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8如果实体字段用的是LocalDateTime还需要加一个配置类统一注册JavaTimeModule的序列化器。如果你使用的是SpringBoot 2.7直接在application.yml中配置spring.jackson即可生效如果你的项目是SpringBoot 3.x部分配置可能需要通过Jackson2ObjectMapperBuilderCustomizer来定制。7.3 事务失效的3个隐蔽场景事务失效是个经典面试题但在真实项目中依然经常踩雷。我总结3个我在开发中真实遇到过的场景对照检查你的代码。第一自调用导致事务失效。在同一个类中方法A调用方法BB上有Transactional而A没有。这个调用是经过this引用的不会走Spring代理对象B上的事务注解不会生效。把B的调用拆到另一个Service类中或者通过注入自己的代理对象来调才能触发事务。第二方法不是public导致事务失效。Transactional只能作用于public方法private和protected方法上的注解会被直接忽略。这在IDE里完全没有任何提示非常隐蔽。第三异常被吞掉导致不回滚。业务代码里捕获了异常后只打印日志没有向上抛出Spring就认为方法正常完成事务自然就不会回滚。正确的做法是捕获异常后转换为自定义业务异常抛出或者不捕获直接交给全局异常处理器。7.4 报表导出的内存溢出预防物资系统肯定有月度出入库报表导出功能。最常见的实现是先用SELECT * FROM tb_x WHERE ...查出全部数据然后循环生成Excel行最后一次性输出到响应流。数据量一旦过万就可能遭遇内存溢出或者浏览器下载超时。我之前在处理一个约50万行数据的导出需求时就是按照下面优化方案处理的使用MyBatis的流式查询逐批读取数据而不是一次性加载到内存Excel写入采用逐行追加每写5000行就刷新一次导出过程中限制查询条件防止用户一次性导出一年以上的数据。如果业务确实需要导出海量数据更科学的方案是后端异步生成任务生成完成后通知前端下载避免HTTP请求长连接占用资源。8. 写在最后这套系统的拓展方向和我的操作体会整套系统跑通一遍之后我个人的感受是物资综合管理系统虽然看起来只是“增删改查”但想做得扎实背后涉及RBAC权限模型、动态路由、事务边界、并发扣减、报表导出这些通用能力都是企业业务系统的高频基础设施。把这套源码吃透对理解SpringBoot全家桶的协作方式、Vue前端工程化的组织方式都很有帮助。最后分享一个我实际操作中的小技巧不管你在哪台机器上部署这套系统先把application-prod.yml里的数据库密码改成环境变量读取的方式比如${DB_PASSWORD}。这样即使代码仓库被共享生产数据库的密码也不会泄露到Git历史里。成本很低但安全性提升明显。后面如果你想继续扩展建议优先考虑加入消息队列做库存流水异步写入或者接入Redis做热点物资的缓存查询这些都是性价比很高的演进方向。