ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+MyBatis+MySQL物资管理系统实战详解

SpringBoot+Vue+MyBatis+MySQL物资管理系统实战详解 做物资管理系统说实话这需求我已经接过好几次了。每次版本要求都不一样但核心都绕不开那一套入库、出库、库存台账、分类管理、供应商信息。这次用SpringBootVueMyBatisMySQL这套经典组合重新搭一遍顺便把实现过程中一些容易踩坑的细节记录下来。这套系统不是那种只有增删改查的玩具项目而是把库存流水、事务控制、动态条件查询这些实际业务问题都考虑进去的完整方案。1. 为什么选SpringBootVueMyBatisMySQL这套组合来搭物资管理系统1.1 一个典型物资管理系统的真实需求清单很多人在做这类系统时容易犯一个毛病——拿到需求就开始写代码结果数据库表设计得一塌糊涂联调阶段才发现逻辑对不上。我一般在动手之前会先把自己代入到实际使用场景里把用户真正要做的事情列清楚。物资管理系统的核心使用场景其实就几类物资管理员需要把新采购的物资录入系统录入时要知道这是哪个分类、放哪个仓库、入库数量是多少、供应商是哪家、采购单价是多少。领用人需要申请领用物资管理员审批后库存减少系统要能查得到这笔领用记录。库存不够的时候要能快速知道哪些物资低于库存预警下限方便及时补货。财务或领导需要看某个时间段内物资的入库、出库统计知道每个月大概采买了多少钱的物资。把这些场景翻译成系统功能基本就是物资分类管理、供应商管理、物资信息管理、入库管理、出库管理、库存台账、预警管理、统计报表、系统用户管理这几大模块。这套需求和仓库管理系统WMS不一样不需要管库位、批次、序列号那些复杂的仓储概念重点在账目清晰、流程可追溯、库存准确这三件事上。所以技术选型不需要太重SpringBootVueMyBatisMySQL这套组合刚好能稳稳接住。1.2 这套技术栈的分工逻辑谁负责什么为什么这么分技术选型不是看哪个框架热门就无脑上而是看这套组合是否让开发效率、维护成本和业务匹配度达到平衡。后端SpringBoot负责的是业务规则和接口生命周期。它自带Tomcat内嵌容器、自动配置、健康检查、参数校验一堆现成能力写好一个启动类就能跑起来对于中小型管理系统的开发效率提升非常明显。尤其是SpringBoot对事务管理、依赖注入、AOP切面的支持非常成熟后面处理入库必须同时更新库存和写流水这种原子性操作时一个Transactional注解就能搞定不用自己写一堆事务模板代码。MyBatis负责的是SQL的精细控制。物资管理系统的统计查询会涉及多表关联、条件动态拼接、聚合分组MyBatis的XML动态SQL在这种场景下非常好用。比如根据分类、名称、仓库、供应商等多个条件联合筛选物资列表用if标签动态拼接条件比JPA那种先查出来再内存过滤的方式性能上高了一个量级而且SQL完全自己掌控出现性能问题可以直接优化SQL本身。Vue负责的是页面交互和数据驱动的渲染。物资管理系统的表单、表格、弹窗、统计图表这类页面Vue的双向绑定和组件化开发模式天然契合。数据一变页面跟着变那种库存数量变化后表格自动刷新的体验用Vue很自然就实现了。MySQL负责的是稳定的数据持久化。绝大多数管理系统是读多写少MySQL在这种场景下配合InnoDB引擎、合理的索引设计完全够用。而且MySQL的安装和维护成本很低部署环境随处可用。这套组合的配合逻辑简单说就是用户操作Vue页面Vue通过Axios调用SpringBoot接口SpringBoot通过MyBatis操作MySQL数据再一步步返回给前端渲染。链路清晰出了问题也容易定位。2. 数据库设计这个系统的命根子全在表和表关系上2.1 从业务故事反推核心数据表结构数据库设计我习惯先从一笔完整业务要在系统里留下哪些痕迹出发而不是照着网上的表结构模板抄。拿物资入库来说这一笔业务至少要在系统里留下这些痕迹单据信息单号、入库时间、入库人、供应商、明细信息入了哪些物资、每种物资的数量单价、库存变化对应物资的库存数量增加、流水记录什么时间、谁、因为什么单据、让哪个物资库存变化了多少。缺了其中任何一环后续查账都会对不上。基于这个思路我的核心表设计如下表名用途关键字段说明category物资分类表分类名称、父级ID支持两级分类、排序号supplier供应商表供应商名称、联系人、联系电话、地址、状态material物资信息表物资编码唯一、物资名称、规格型号、计量单位、分类ID、库存上限、库存下限、状态stock库存台账表物资ID唯一、当前库存数量、最近入库时间、最近出库时间in_stock入库单主表入库单号、供应商ID、入库总金额、入库时间、入库人、备注in_stock_item入库单明细表入库单ID、物资ID、入库数量、入库单价、小计金额out_stock出库单主表出库单号、领用人、出库时间、出库总金额、备注out_stock_item出库单明细表出库单ID、物资ID、出库数量、小计金额stock_record库存流水表物资ID、流水类型入库/出库、关联单号、变化数量、变化后库存、操作时间、操作人sys_user系统用户表用户名、密码BCrypt加密、姓名、角色、状态role/menu角色权限相关表基础RBAC模型用户-角色-菜单关联这里有个设计细节material物资信息和stock现有库存必须拆成两张表。我第一次做的时候以为一张表能搞定物资信息当前库存放一起多省事。但实际运作起来问题就来了物资信息是基本档案变更频率很低库存是高频变化的动态数据每次出入库都要update。如果放一起并发高的时候update会锁行影响其他读物资信息的操作。而且从查询角度看查所有物资信息和查库存低于预警线的物资是完全不同的两个维度拆开以后SQL写起来更清晰。2.2 为什么必须单独建一张库存流水表库存流水表stock_record是我做这个系统始终坚持的表哪怕早期版本业务简单我也没省过。很多初学做管理系统的人会觉得有入库单和出库单就够了要看记录直接查单据就好流水表是多余的。实际用过就知道为什么必须建这张表。假如某个物资今天入库50个明天出库30个后来又被领用20个你查入库单只能看到入过50个查领用单只能看到每次领了多少。但如果要把这个物资的生命周期陈列在一条时间线上显示什么时间、因为哪张单子、库存从多少变成了多少没有流水表就只能靠多张单据表join之后按时间排序还要处理单据之间的时间交叉极其麻烦。而有了流水表这个库存变化时间线的查询就是一次简单的SELECT * FROM stock_record WHERE material_id ? ORDER BY create_time。而且流水表还有一个作用——对账。系统维护中经常出现页面库存数和实际库存对不上的问题排查思路就是从上次盘点确认的库存数开始把流水一条条加或减看差异出在哪一步。没有流水表这种排查是没法做的。我遇到过好几次有流水表的情况下基本几分钟就能定位到是哪个操作员的哪笔单据录错了数。这就是流水表的真正价值——它不是业务必须的但它是运维和信任度必须的。2.3 索引设计就这几个字段索引设计对了一切都顺MySQL在数据量小的时候感觉不到索引存在的必要但物资管理系统跑个两三年流水表几十万条记录非常正常。这个时候索引没建好查询性能会肉眼可见地慢。我的索引设计原则简单粗暴高频查询条件的字段建索引联合查询的依据建复合索引。实践下来这几个索引收益最大stock_record表上建(material_id, create_time)复合索引。查某个物资的历史流水是最高频操作之一这个索引能让查询直接走覆盖索引提前过滤。in_stock_item和out_stock_item表的material_id单列索引。根据物资反查哪些单据出现过这个物资这个是统计报表的常用查询路径。in_stock、out_stock表上的create_time索引。按时间段统计出入库数量是报表标配没有这个索引时间范围查询就是全表扫描。顺便说一句索引不是越多越好。每个索引在写入的时候都要维护B树索引建太多会让insert和update变慢。我的原则是查询需求明确到能背出来的时候才建对应的索引模棱两可的字段宁可不建。3. 后端接口实现SpringBootMyBatis落地过程中的核心细节3.1 项目基础结构与统一返回体设计后端工程我习惯用标准的三层结构Controller层负责接收参数和返回结果Service层写业务逻辑Mapper层通过MyBatis操作数据库。实体类放在entity包DTO数据传输对象放dto包VO视图对象放vo包。项目初始搭建没啥新鲜的Spring Initializr创建工程勾选Web、MyBatis、MySQL Driver、Lombok这些依赖。需要注意的一点是SpringBoot版本和MyBatis-Spring-Boot-Starter版本要匹配SpringBoot 2.x对应mybatis-spring-boot-starter2.x版本SpringBoot 3.x就要用3.x版本了不然启动时会报各种奇怪的ClassNotFound。统一返回体这块虽然看着是个小细节但我强烈建议在做任何接口之前先定下来。所有接口返回ResultT结构包含code、msg、data三个字段Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMsg(msg); return result; } }没有统一返回体的时候每个Controller都自己拼Map返回前端对接时心态真的会崩。这个设计能保证前端Axios拦截器里面一次处理成功和失败的判断逻辑全局生效。3.2 MyBatis动态SQL库存列表的多条件查询是动态SQL的经典场景物资列表页通常有搜索栏按物资名称模糊搜索、按分类下拉选择、按库存状态正常/缺货/超储筛选、按供应商下拉筛选。这里的用户填了几个条件就按几个条件查一个都没填就查全部必须用MyBatis的动态SQL来实现不能用Java代码拼SQL字符串。我在编写多条件查询时Mapper XML中的实现大致是这样select idselectMaterialPage resultTypecom.example.vo.MaterialStockVO SELECT m.id, m.code, m.name, m.spec, m.unit, c.name AS category_name, s.name AS supplier_name, st.current_stock, m.stock_lower_limit, m.stock_upper_limit FROM material m LEFT JOIN category c ON m.category_id c.id LEFT JOIN supplier s ON m.supplier_id s.id LEFT JOIN stock st ON m.id st.material_id where if testname ! null and name ! AND m.name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND m.category_id #{categoryId} /if if teststockStatus ! null and stockStatus 1 AND st.current_stock lt; m.stock_lower_limit /if if teststockStatus ! null and stockStatus 2 AND st.current_stock gt; m.stock_upper_limit /if /where ORDER BY m.create_time DESC /select这里有个细节很容易坑到人和在XML里必须转义成lt;和gt;不转义XML解析直接报错。我遇到过很多次有人在XML里直接写然后报元素内容必须由格式正确的字符数据或标记组成就是这个问题。多条件查询为什么不用MyBatis-Plus的QueryWrapper这里我要说明一下自己的倾向简单的单表查询用MyBatis-Plus确实快但这种多表LEFT JOIN查询最终还是得写XML。既然统计报表、物资列表这些复杂查询都绕不开XML那干脆全局统一都用XML写SQL避免一个项目里两种风格满天飞后维护的人看着也统一。3.3 入库和出库的事务处理为什么先更新库存再写流水是正确的顺序物资入库的后端逻辑看起来不复杂插入入库单主表、插入入库单明细表、更新库存表数量、插入库存流水表。但这四个操作必须全部成功或者全部失败绝不能出现单据保存了库存没涨这种数据不一致。SpringBoot通过Transactional注解搞定这件事这个就不多说了。我想重点说的是操作顺序的问题。入库时更新库存和写流水的顺序有讲究。我的做法是先更新库存表UPDATE stock SET current_stock current_stock #{count}再插入流水表。为什么是这个顺序假设并发场景下两个操作同时操作同一个物资的库存一个是入库加50一个是出库减30。如果先写流水再更新库存流水表里两条记录的时间先后可能和库存实际变化不一致——后写流水那条记录在时间线上可能在先但库存实际是先加了再减的。出现这种问题会导致流水时间线错乱对账时根本看不清楚。而先更新库存再写流水当前时间点的库存值是确定的流水记录的是这次操作后库存变成了多少天然地对齐了时间线。代码示例Transactional(rollbackFor Exception.class) public void inStock(InStockDTO dto) { // 1. 插入入库单主表 InStock inStock new InStock(); inStock.setStockNo(generateStockNo(IN)); // ...省略其他字段设置 inStockMapper.insert(inStock); // 2. 批量插入入库明细 for (InStockItemDTO item : dto.getItems()) { InStockItem record new InStockItem(); record.setStockId(inStock.getId()); record.setMaterialId(item.getMaterialId()); record.setQuantity(item.getQuantity()); record.setPrice(item.getPrice()); record.setTotalAmount(item.getQuantity().multiply(item.getPrice())); inStockItemMapper.insert(record); // 3. 更新库存 stockMapper.increaseStock(item.getMaterialId(), item.getQuantity()); // 4. 记录流水 StockRecord recordLog new StockRecord(); recordLog.setMaterialId(item.getMaterialId()); recordLog.setType(IN); recordLog.setRelatedNo(inStock.getStockNo()); recordLog.setChangeCount(item.getQuantity()); recordLog.setAfterStock(stockMapper.getStockByMaterialId(item.getMaterialId())); recordLog.setOperator(loginUser.getUsername()); stockRecordMapper.insert(recordLog); } }上面代码中increaseStock的SQL是UPDATE stock SET current_stock current_stock #{count}这种原子更新方式不是先SELECT查出来再UPDATE写成新的值。两者有什么本质区别current_stock #{count}这个操作由MySQL在行锁保护下原子完成多个并发请求即使同时到达也会被行锁串行化各自加自己的数量。而先查后改存在经典的并发覆盖问题两个线程同时读到100一个加50后写150另一个减30后写70最后一次写操作会把前一次的结果覆盖掉——最终库存居然是70直接数据错乱。这个坑是我在真实项目里踩过的第一次做库存扣减时用的先查后改压测一打就发现问题后来全部改成原子更新才解决。3.4 MyBatis缓存和生成编号的实用技巧MyBatis的缓存机制经常被面试问到实际开发中的价值需要区分情况。一级缓存SqlSession级别的缓存默认开启同一个SqlSession内重复查询相同SQL会直接返回缓存结果。但在SpringBoot集成环境下每次Mapper操作通常都是独立的SqlSession所以一级缓存的作用范围被大幅削弱了——说句实话基本感知不到它的存在。二级缓存Mapper级别的缓存可以跨SqlSession共享查询结果听起来很美好但我强烈建议不要在涉及事务和频繁更新的模块开启二级缓存。原因很简单物资管理系统的库存、流水这些数据都是实时性要求极高的二级缓存一旦生效你入库了一笔库存另一个请求查询库存信息时可能拿到的还是旧缓存数据。刷新缓存的时机会滞后这种业务根本承受不起脏读。所以MyBatis二级缓存我在这套系统里是关闭的只在那些几乎不更新、查询量又极大的数据比如物资分类列表上手工做Redis缓存这样做效果反而好。还有一个容易被忽略的点——业务单据编号的生成问题。入库单号如果用数据库自增ID拼接一旦删除过数据ID就会跳号单据号有断层财务查账时会追问原因。还要防止并发重复比如同一秒内两个用户同时入库如果单号规则是IN yyyyMMddHHmmss 随机数极端情况可能撞号。我的做法是用日期 当天自增序列序列存在一张独立的sequence表里生成编号时使用UPDATE ... SET value value 1然后SELECT value原子地拿到当天第几个单号拼接出类似IN2025011500001这样的编号。既保证了顺序又能从单号看出日期查账也方便。4. 前端Vue部分从页面组件到数据交互的落地过程4.1 从零搭建Vue项目哪些配置该改、哪些坑必须先踩前端这块我用的是Vue 2 Vue Router Vuex Axios Element UI的组合。这套组合虽然不算最新但胜在生态成熟、资料多Element UI的表格、表单、弹窗、分页组件拿来即用能省下不少写样式的时间。如果从零开始创建项目我会用Vue CLI来初始化vue create material-web cd material-web npm install element-ui axios创建完项目我做的第一件事不是写代码而是改目录结构和几个基础配置。目录上我会增加src/api放接口请求方法、src/utils放Axios实例和工具函数、src/router放路由配置、src/store放Vuex状态管理页面文件统一放在src/views下每个模块一个文件夹。需要特别注意的配置有这几个开发环境代理。Vue开发服务器端口默认8080我后端接口在8081直接请求会跨域。在vue.config.js里配置代理是最省事的方案module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端请求/api/material/list开发服务器会自动转发到http://localhost:8081/material/list跨域问题在开发阶段直接消解掉了。路由模式。默认的history模式刷新页面时如果后端没有做相应的回退处理会出现404。开发环境配了代理没问题生产环境我一般直接用hash模式简单可靠省去后端配置的麻烦。Element UI按需引入。全量引入虽然省事但打包出来体积很大。我实际做的时候用了babel-plugin-component做按需加载首屏加载速度肉眼可见地提升了不少。4.2 Axios请求封装与前端拦截器的实际作用前端所有接口请求统一走一个封装好的Axios实例这样拦截器、错误处理、Token携带这些逻辑写一遍全站生效。我的封装习惯是这样的import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 请求失败) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.msg)) } return res }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } )这里有几个细节要说一下。baseURL用环境变量控制开发环境指向/api走代理生产环境可以直接指向实际接口域名换环境不用改代码。请求拦截器统一把本地存的Token塞进请求头省得每个接口单独传。响应拦截器统一处理后端返回的ResultT结构如果业务码是401就说明登录过期直接清掉Token并踢回登录页。这样一个封装写下来页面里调接口就只需要关心成功之后的数据错误处理和登录失效逻辑全局统一了。4.3 物资列表、出入库操作和库存预警的页面交互设计物资列表页是这类系统里最核心的页面交互上要注意的点比较细。列表页面用的是el-table列字段包括物资编码、名称、分类、规格、单位、供应商、当前库存、库存上下限、状态标签。库存状态那一列我用el-tag动态显示当前库存低于下限显示红色缺货标签高于上限显示橙色超储正常显示绿色正常。数据来源是后端返回的库存状态字段前端根据字段值映射成对应的Tag类型这种做法的好处是后端只管算好状态前端只负责展示逻辑分层干净。分页用的是el-pagination组件配置需要注意current-page和page-size都要用.sync修饰符绑定或者用事件更新否则翻页的时候页码会跳回第一页。总条数从后端返回的total字段拿我这里统一约定分页接口返回的数据结构是{ records: [], total: 10 }前端拿到后设置给分页组件。出入库操作的弹窗表单核心交互是动态明细行。入库时可以一次录入多个物资所以明细表单要支持动态增删行。我用el-dialog里嵌一个动态表格每个物资行都有物资下拉选择、数量、单价的输入框当选择物资后自动带出规格、单位、库存上下限信息方便录入员核对。明细行删除时如果只剩一行就要拦截防止误删。这里有个细节很值得一说入库单里选了某个物资前端把整套DTO直接提交给后端后端在事务内做插入单据更新库存写流水。前端表单只要有必填校验就行所有的一致性校验必须交给后端处理。比如入库数量必须大于0这种校验前端校验只是用户体验优化后端必须再校验一遍。为什么因为系统用户完全可以用Postman直接调接口绕过前端如果后端不校验一个负数入库请求就会把库存改乱。这套系统的安全性依赖后端兜底不是前端那些漂亮的校验提示。库存预警页面我用的是卡片列表混合布局顶部放几个统计卡片显示库存短缺N种超储N种下方表格直接展示这些异常库存的物资列表点击去补货按钮会带着物资信息跳转到入库页面并自动选中该物资。这个联动操作看着小实际使用中非常提升效率——从发现缺货到补货完成原本要经过查库存→记物资名→去入库页→重新搜物资→填数量五步缩短为看到提示→点补货→填数量→提交三步。5. 联调与部署阶段我踩过的坑和最终的解决方案5.1 后端接口还没写完前端怎么并行开发实际开发中前后端并行是常态但最怕的就是互相等。后端接口没定义清楚前端没法写请求前端页面没起来后端也没法验证联调效果。我一般用两种方式解决第一种是提前定义好接口文档比如Swagger注解或者独立的YApi/Apifox文档前后端各写各的联调阶段再统一对齐。第二种是在前端用Mock数据进行页面开发和调试等到后端接口就绪后再替换成真实请求。实际操作中我用Mock的节奏比较合适对于依赖用户权限、依赖后端计算状态的逻辑比如库存状态标签我先Mock一份明确的返回数据把页面画起来后端好了之后Mock数据直接删掉改调真实接口。联调阶段可能遇到最多的问题我整理一下常见问题原因解决方案跨域请求被拦截前端域名和接口域名不一致开发环境用Vue代理生产环境Nginx反向代理或后端配置CORS时间字段显示成2025-01-12T08:30:00.00000:00后端返回了ISO标准时间格式后端在application.yml配置spring.jackson.date-format和时区前端用dayjs格式化表格数据多出很多不需要的字段后端直接返回了实体类定义VO只返回前端需要的字段避免把数据库字段全暴露分页后复选框选中状态丢失分页切换后组件重新渲染用row-key属性配合预留选中数组跨页保存选中状态5.2 部署时最容易忽略的SpringBoot配置项系统开发完毕部署到服务器时有一堆配置细节容易被忽略。我把自己部署时都会检查一遍的配置项列一下数据库时区。MySQL连接的JDBC URL上要明确指定时区比如jdbc:mysql://localhost:3306/material?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。不指定时区Ubuntu上安装的MySQL默认时区可能是UTC导致查询出来的时间和北京时间差8个小时。这个问题排查起来极其隐蔽因为开发环境Windows默认本地时区不指定也没问题一到Linux服务器上就暴露。数据库连接池参数。SpringBoot默认使用HikariCP我一般会显式配置几个参数spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 max-lifetime: 1800000max-lifetime如果不配置默认是1800000毫秒30分钟这本身没问题。但有一种情况需要注意如果MySQL的wait_timeout设置得比连接池的最大存活时间短空闲连接会被MySQL服务端断开连接池里的Connection却以为还活着下次使用时就会报Connection is not available, request timed out after Xms。所以我部署时会同时检查MySQL的wait_timeout和Hikari的max-lifetime保证Hikari的存活时间小于MySQL的空闲超时时间。JVM内存参数。管理系统的后端服务一般几百MB就够但如果服务器内存比较紧张启动时可以用java -jar -Xms256m -Xmx512m material.jar限制一下内存上下限防止系统把整台服务器的内存吃满。-Xms和-Xmx设置为相同值可以避免运行时动态扩容造成的性能抖动。5.3 Nginx部署前端项目历史路由刷新404的处理方案前端项目打包后生成dist目录我部署时通过Nginx把它作为一个静态站点发布同时用反向代理把/api请求转发给后端Java服务。一个经典的Nginx配置片段长这样server { listen 80; server_name material.example.com; # 前端静态资源 location / { root /var/www/material/dist; index index.html; try_files $uri $uri/ /index.html; # history模式关键配置 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }最关键的是try_files $uri $uri/ /index.html这一行。如果用了Vue的history模式前端路由是/material/list这种路径浏览器刷新时Nginx拿到这个路径会去服务器找对应的文件找不到就404。try_files的作用就是告诉Nginx找不到对应文件就回退到index.html然后由Vue Router接管路由渲染对应的页面。如果用了hash模式就不需要这行配置因为路由变化不发请求到服务器。我在生产环境用hash模式多一些部署省心、不用额外配置代价就是URL里多个#符号。对内部管理系统来说这个代价完全可以接受。6. 从这套基础版延伸权限控制、统计报表和后续扩展思路6.1 RBAC权限模型在这套系统里的实现方式管理系统基本都逃不开用户权限控制我在这套系统里用的是经典的RBAC基于角色的访问控制模型。三层关系用户表关联角色表角色表关联菜单表权限点表。用户登录后查询出角色和对应的菜单权限动态生成前端路由和按钮级别的控制。后端接口层面实现权限控制我选择的是Spring Security JWT这种组合。登录成功后生成JWT Token前端存到localStorage每次请求通过拦截器放在请求头里。后端的Security拦截器负责解析Token、校验有效期、判断用户角色是否允许访问某个接口。有一个具体的场景可以说明这种设计的作用管理员可以访问用户管理模块普通仓管员只能访问物资管理和出入库模块。后端通过PreAuthorize(hasRole(ADMIN))注解控制接口访问权限前端根据用户角色动态渲染菜单项没有权限的菜单直接不显示。如果某个普通用户通过手工拼URL访问了无权限的接口后端会返回403前端拦截器统一提示无权限访问。6.2 统计报表里最有用的两张表月度入库出库统计和物资周转情况管理层看系统主要不是为了看那些表单而是看统计数据。我做统计报表时的两个核心查询思路可以分享一下月度出入库统计。按月度汇总出库数量和金额典型SQL如下SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(CASE WHEN type IN THEN change_count ELSE 0 END) AS total_in, SUM(CASE WHEN type OUT THEN change_count ELSE 0 END) AS total_out FROM stock_record WHERE create_time #{startDate} AND create_time #{endDate} GROUP BY DATE_FORMAT(create_time, %Y-%m)前端用ECharts折线图展示管理人员一眼能看出哪个月出入库量大、是否存在异常波动。这里查询用到的create_time索引在数据库设计阶段就已经加上了所以这个GROUP BY查询性能不会有问题。库存积压分析。查询当前库存数量大、最近出库时间距今很久的物资这些可能就是积压库存占用了资金和仓储空间SELECT m.name, m.spec, st.current_stock, st.last_out_time, st.current_stock * m.average_price AS stock_amount FROM stock st LEFT JOIN material m ON st.material_id m.id WHERE st.current_stock 0 AND (st.last_out_time IS NULL OR st.last_out_time DATE_SUB(NOW(), INTERVAL 90 DAY)) ORDER BY stock_amount DESC这种90天没有出库记录且库存不为零的查询能帮管理人员识别出哪些物资长时间不用考虑调拨或清理。我第一次做类似功能时只做了月度统计后来做复盘会议时发现管理层其实更关心钱压在哪些货上这个积压分析报表的点击率比出入库统计还高。6.3 如果业务量涨了这套系统的扩展方向是什么基础版的物资管理系统可以满足几百人规模的公司使用。如果业务量涨上去我会按这个顺序逐步演进引入Redis做热点数据缓存。物资分类、供应商下拉列表这类读取频繁但更新很少的数据从数据库查询改为缓存读减轻数据库压力。库存数据自身的读写压力不大不建议缓存保持实时读库更安全。引入消息队列做异步操作。比如出入库成功后需要给相关人员发通知邮件发短信提醒库存预警这些操作不走核心业务链路可以丢到消息队列异步消费。系统不会因为邮件服务变慢而拖慢核心入库操作。引入定时任务做自动化盘点报告。每天凌晨生成一份库存日报昨日出入库数量、当前库存总量、缺货清单、超储清单推送给管理员邮箱。这个用SpringBoot的Scheduled就能实现成本很低收益却很直接——管理员每天打开邮箱就能掌握全局。这套系统的整体设计思路核心就两句话表结构要能支撑账实相符的核对逻辑后端事务和并发控制要守住库存数据一致性这条底线。把这两件事做到了后面加再多功能模块都只是增量开发。我在做类似管理系统的过程中最深的体会是这类系统难的不是单个功能而是模块之间的数据联动——一个入库操作牵动单据、库存、流水三张表一处漏了整个账就对不上。写代码之前先把这些数据流转路径想透开发过程中能少踩一大半的坑。
返回列表