资讯动态

SpringBoot超市货品管理系统:从数据库设计到库存并发控制实践

发布时间:2026/9/7 18:13:47 来源:尧图企业网站定制
毕业设计选了个超市货品信息管理系统听起来好像就是个普通的增删改查项目但真的动手做起来你会发现它其实是一个特别典型的“小而全”全栈练习。一方面它要覆盖商品从进货、入库、上架到销售出库的全过程另一方面还得处理库存变化、供应商对账、预警提醒这些业务细节。用springboot来做这个题目可以说既稳妥又有足够的发挥空间。这篇文章我就把自己做这个系统时的一些设计方案、代码思路和踩坑记录整理出来给准备做类似选题的同学一个参考。全文会围绕系统设计、数据库建模、核心模块实现、以及那些最容易在答辩时被追问的细节展开希望能帮你少走点弯路。1. 整体设计与技术选型背后的考量1.1 为什么是springboot而不是其他框架很多同学纠结过这个问题到底是springboot还是ssm或者直接用若依这种快速开发平台。我的建议是如果你的毕业设计题目里明确写的是springboot那就稳稳当当用springboot做不要在技术选型上搞花活。原因很简单springboot是目前企业级Java开发的事实标准它简化了spring那一大堆繁琐的配置内置tomcat打成一个jar包就能跑这在演示和部署的时候会省掉非常多麻烦。从答辩角度讲springboot的知识点也更清晰自动配置原理、starter机制、约定优于配置这些都是能讲出东西的点。相比之下如果用了若依之类的脚手架虽然开发速度快了但很多代码不是你写的老师一问细节就容易露馅。1.2 系统功能模块怎么划分才合理超市货品信息管理系统的核心业务说白了就是围绕“货品”这个主角管理它的整个生命周期。我在设计时把功能拆成了这样几个模块货品信息管理商品的基础信息维护包括条码、名称、分类、规格、单位、进价、售价、库存上下限等。供应商管理维护供货商的基本资料方便进货时选择。这个模块很容易被忽略但实际上超市货品不可能脱离供应商独立存在。进货入库管理记录每一次采购入库自动更新库存。销售出库管理模拟前台收银或者批发销售出库库存自动扣减。库存管理库存查询、库存预警、库存盘点。系统管理用户登录、角色权限、操作日志。这套模块划分的逻辑是货品信息是基础数据供应商是辅助数据进货和销售是导致库存变化的两个入口库存管理是最终的业务落脚点。整个数据流是闭环的讲起来也顺。1.3 为什么没用工作流引擎有人可能会想到springboot整合flowable或者activiti来做审批流比如采购单需要经理审批。我的看法是超市货品管理系统本质上是一个进销存系统它的核心是状态管理和数量计算不是流程审批。工作流引擎在这个场景里有点杀鸡用牛刀而且会引入大量的表结构和配置反而把简单问题复杂化。除非你的题目里明确写了需要流程审批否则不建议自己给自己加戏。真要有审批需求用状态字段加一个审核人字段就能解决简单直接还容易讲清楚。做毕业设计最忌讳的就是堆砌技术老师看到你的系统里躺着一大堆没用上的框架反而会质疑你的设计能力。2. 数据库设计整个系统的地基2.1 核心表结构与字段设计数据库设计是这类管理系统项目的重中之重。我见过不少同学上来就建表结果做到一半发现字段不够用或者表之间关联关系乱了回头改表结构改到崩溃。这里我把自己最终确定下来的核心表结构分享一下。商品信息表是基础中的基础我给它取名叫product。关键字段包括id、product_code商品编码通常是条码、product_name、category_id分类id、specification规格比如500ml/瓶、unit单位瓶/包/箱、purchase_price进价、sale_price售价、stock_upper_limit库存上限、stock_lower_limit库存下限、status状态上架/下架、create_time和update_time。这里有个设计细节要注意库存最好不要直接存在商品表里而是单独建一张库存表或者至少要把库存逻辑和商品基础信息分开。我采用的是单独一张stock表字段是product_id、quantity当前库存数量、last_update_time。这样做的原因后面讲并发更新的时候会详细说这里先记住结论。进货单表purchase_order记录每一次进货字段包括id、supplier_id、total_amount总金额、status待入库/已入库、create_time。进货单明细表purchase_order_item记录这一次进了哪些货、每样进了多少、进价是多少字段包括id、purchase_order_id、product_id、quantity、price。销售出库表sale_order和进货单表结构对称就是少了供应商改成客户或者直接就是零售。销售明细表sale_order_item记录卖出去的商品和数量。供应商表supplier比较简单id、supplier_name、contact_person、contact_phone、address、remark。2.2 为什么要用逻辑删除这是个很容易被忽略但很实用的设计。商品信息在被引用比如已经在进货单里出现过了之后是不能物理删除的否则历史单据关联的商品就查不出来了。我的做法是给所有基础表都加上一个deleted字段默认0删除时置为1查询条件里统一加上deleted 0。逻辑删除的好处是数据不会真正消失出问题还能追溯。坏处是每次查询都要记得带条件而且唯一性约束可能会出冲突。比如商品编码做了唯一索引删除了一条记录后再插入相同编码的商品就会报错。解决办法是把唯一索引改成复合索引(product_code, deleted)这样同一条码在未删除的数据里只会存在一条。2.3 库存变化的记录流水表是关键很多同学做进销存系统只关注库存当前的数字没有记录变化的过程。我强烈建议加一张stock_flow流水表字段包括id、product_id、change_type1入库 2出库 3盘点调整、change_quantity正数增加、负数减少、before_quantity、after_quantity、operator_id操作人、create_time。这张表的作用非常大。第一它能完整还原每一个商品的库存变化历史第二它能在库存对不上的时候用来排查问题第三老师答辩时问你“库存怎么保证准确性”你就可以把流水表这个设计拿出来讲这是一个明显的加分项。3. 核心功能实现代码层面的关键点3.1 增删改查不是无脑写虽然系统主要就是增删改查但每个操作的业务逻辑还是有讲究的。拿商品新增来说保存的时候后端至少要做这几件事Service public class ProductServiceImpl implements ProductService { Autowired private ProductMapper productMapper; Autowired private StockMapper stockMapper; Override Transactional(rollbackFor Exception.class) public void addProduct(ProductDTO dto) { // 1. 校验商品编码是否唯一 if (productMapper.countByCode(dto.getProductCode()) 0) { throw new BusinessException(商品编码已存在); } // 2. 保存商品基本信息 Product product new Product(); // 手动属性拷贝不推荐BeanUtils字段多了容易出错 product.setProductCode(dto.getProductCode()); product.setProductName(dto.getProductName()); // ...省略其他字段 productMapper.insert(product); // 3. 初始化库存记录数量为0 Stock stock new Stock(); stock.setProductId(product.getId()); stock.setQuantity(0); stockMapper.insert(stock); } }注意这里的事务注解Transactional一定要加因为新增商品涉及两张表的写入操作商品表和库存表。万一商品保存成功了库存初始化失败没有事务的话就会出现商品存在但库存不存在的数据不一致问题。3.2 入库操作的完整流程进货入库是整个系统里最核心的一个操作它涉及的数据变动比较多。实现入库时我的处理逻辑是这样的首先校验进货单状态只有状态为“待入库”的单子才能入库已入库的不允许重复操作。然后遍历进货单明细逐条更新库存数量。最后把进货单状态改成“已入库”。Override Transactional(rollbackFor Exception.class) public void confirmPurchase(Integer purchaseOrderId) { PurchaseOrder order purchaseOrderMapper.selectById(purchaseOrderId); if (order null) { throw new BusinessException(进货单不存在); } if (!待入库.equals(order.getStatus())) { throw new BusinessException(该进货单已入库请勿重复操作); } ListPurchaseOrderItem items purchaseOrderItemMapper.selectByOrderId(purchaseOrderId); for (PurchaseOrderItem item : items) { // 更新库存 Stock stock stockMapper.selectByProductId(item.getProductId()); if (stock null) { throw new BusinessException(商品ID item.getProductId() 无库存记录); } int before stock.getQuantity(); int after before item.getQuantity(); // 更新库存使用乐观锁防止并发 int rows stockMapper.updateQuantityWithVersion(stock.getId(), after, stock.getVersion()); if (rows 0) { throw new BusinessException(库存更新失败请重试); } // 写流水 StockFlow flow new StockFlow(); flow.setProductId(item.getProductId()); flow.setChangeType(1); flow.setChangeQuantity(item.getQuantity()); flow.setBeforeQuantity(before); flow.setAfterQuantity(after); flow.setOperatorId(LoginUser.getId()); stockFlowMapper.insert(flow); } // 更新进货单状态 order.setStatus(已入库); purchaseOrderMapper.updateById(order); }3.3 库存扣减的并发问题与乐观锁库存扣减是这类系统里最容易出问题的地方。大家自己写代码测试的时候单用户操作怎么跑都是对的但一旦考虑多用户同时购买同一件商品就可能会出问题。举个例子商品A库存还剩10件用户甲和用户乙同时下单各买5件。如果代码是先查库存、判断够不够、再扣减那么两个用户可能同时读到库存10件都判断够然后分别扣减成5件。最终结果库存变成了5件但实际应该卖出去10件、库存变成0件。解决并发扣减的常见方案有悲观锁和乐观锁。悲观锁就是select ... for update查库存的时候把这条记录锁住别人就不能查了直到事务结束才释放。这个方案简单可靠但并发高的时候性能会差一些。乐观锁就是给库存表加一个version字段更新的时候带上版本号int rows stockMapper.updateQuantityWithVersion(stock.getId(), after, stock.getVersion()); if (rows 0) { throw new BusinessException(库存更新失败请重试); }对应的SQL是UPDATE stock SET quantity #{afterQuantity}, version version 1 WHERE id #{id} AND version #{version}如果更新影响的行数为0说明版本号已经变了有人比你抢先一步改了库存这时候就得让用户重新查询库存再试。对于毕业设计来说明确说出“库存扣减存在并发问题我使用乐观锁来解决”并且在代码里真正做了实现这是一个非常加分的答辩点。3.4 库存预警怎么写库存预警的逻辑很简单查询stock表和product表找出现有库存低于库存下限或者高于库存上限的商品。我把它做成一个定时任务每天定时检查一次也可以做成每次登录系统后自动检查一次。Component public class StockWarningTask { Autowired private StockMapper stockMapper; Scheduled(cron 0 0 8 * * ?) // 每天早上8点执行 public void checkStock() { ListStockWarningVO warnings stockMapper.selectLowStockList(); if (warnings ! null !warnings.isEmpty()) { // 记录预警信息可以发邮件、写日志、存预警表 for (StockWarningVO warning : warnings) { System.out.println(商品 warning.getProductName() 库存不足当前库存 warning.getQuantity()); } } } }在springboot的启动类或者配置类上记得加上EnableScheduling注解否则定时任务不会生效。4. 权限管理一个容易被低估的模块4.1 登录认证与密码安全超市货品信息管理系统肯定要有登录功能不然谁都能进来改数据。登录的常规做法是用session保存登录状态配合拦截器实现访问控制。密码存储不能是明文用BCrypt加密是主流做法。springboot整合spring security倒是能做但说实话这个项目用security有点重了配置过程也容易让新手懵。我的建议是写一个简单的拦截器就够了核心逻辑是登录成功后把用户信息放到session里写一个AuthInterceptor拦截所有请求判断session里有没有用户没有就跳转到登录页。密码加密用spring security里的BCryptPasswordEncoder单独使用不需要引入整个security框架Autowired private BCryptPasswordEncoder passwordEncoder; // 注册时 String encodedPassword passwordEncoder.encode(rawPassword); // 登录时 boolean matches passwordEncoder.matches(rawPassword, encodedPassword);这样既能保证密码安全又没有引入过于复杂的框架配置答辩时讲起来也从容。4.2 角色权限控制系统里一般会有两种角色管理员和普通员工。管理员可以维护基础数据商品、供应商、查看所有报表普通员工可能只能做销售出库和库存查询。实现方式很简单用户表加一个role字段0是管理员1是员工。拦截器里判断请求的路径前缀比如/admin/**的请求要求role必须为0否则返回403。有人可能觉得这不就是一个if判断吗不够高大上。但做项目讲究的是合适不是炫技。一个内部管理系统用基于角色的简单访问控制完全够用还能保证代码的可读性。如果你想在答辩时讲得更深一点可以说“我使用的是基于角色的访问控制模型RBAC它的核心思想是把权限赋予角色再把角色赋予用户”一句话就能把理论高度拉起来。5. 前端页面的实现思路5.1 为什么选择简单方案很多毕业设计的前端会纠结用Vue还是React用Element UI还是Ant Design。我的建议是如果团队成员对前端不熟就直接用模板引擎加Bootstrap或者用Thymeleaf做服务端渲染。如果对Vue比较熟就学AdminLTE或者vue-element-admin这样的开源后台模板。这个系统本质上是一个后台管理系统UI的美观程度远没有功能完整性和逻辑正确性重要。老师评审的时候看得更多的是数据库设计合理性、业务逻辑是否闭环、代码结构是否清晰。当然页面也不能太丑用一套成熟的开源后台模板是最快最稳妥的方式。我实际使用的是vue-element-admin这套模板改造的它的表格、表单、弹窗组件都比较齐全可以快速搭建出商品列表、进货单录入、库存查询这些页面。如果你不想引入太重的前端工程也可以用Bootstrap自己拼页面效果也不会差太多。5.2 前后端接口设计注意点接口设计遵循RESTful风格但不必死板。核心接口大概是这样GET /api/product/list分页查询商品列表POST /api/product新增商品PUT /api/product/{id}修改商品信息DELETE /api/product/{id}删除商品逻辑删除POST /api/purchase/order创建进货单POST /api/purchase/order/confirm确认入库GET /api/stock/warning库存预警查询返回值统一封装成ResultT包含code、message和data三个字段。这样前端处理起来统一后端返回错误信息也方便。这个封装很简单但写项目时如果不从一开始就统一好后面到处都是各种格式的返回还容易出错。6. 常见问题与排查技巧实录6.1 数据库连接不上这个是最常见也是最基础的问题。启动项目时报Communications link failure或者Access denied大概率是application.yml里的数据库配置有问题。检查三件事url里的ip和端口对不对、用户名密码对不对、数据库有没有创建成功。一个值得注意的小细节是serverTimezone参数数据库连接字符串最好加上spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai不加时区参数在高版本的mysql驱动下可能报时区错误。这个错误信息比较隐晦不一定一眼能看出来。6.2 修改数据库表后程序报字段不存在这是开发过程中特别容易遇到的。改了表结构但实体类没同步更新或者MyBatis的Mapper.xml里的结果映射和实体类对不上。排查思路是先看报错信息里提示的字段名然后对着实体类和表结构查一遍。有些同学习惯用select *这种方式在表结构变化时其实更容易掩盖问题我建议在Mapper.xml里把字段列名明确写出来。6.3 接口返回的数据变成了null这个通常有三个原因实体类字段和表字段对不上、JSON序列化时没有getter方法、或者逻辑删除后数据确实查不出来了。用Lombok的话注意看有没有写Data注解没写的话MyBatis映射是能查到值但转JSON的时候全变成null。6.4 库存变成负数库存扣减的时候没有判断库存是否充足或者判断了但存在并发问题。比如前端页面校验了库存大于等于购买数量但后端没有校验通过直接调接口就能把库存扣成负数。解决办法是后端在扣减前必须重新查询库存确认足够后执行扣减。如果用了乐观锁版本冲突时提示用户重试这样能把负数问题从根本上解决。// 出库前检查库存 Stock stock stockMapper.selectByProductId(item.getProductId()); if (stock.getQuantity() item.getQuantity()) { throw new BusinessException(商品 item.getProductName() 库存不足当前库存 stock.getQuantity()); }6.5 遇到这个问题先别慌做项目遇到报错是正常现象最忌讳的是看到错误就懵了或者直接复制错误信息到网上瞎搜。我的做法是先看完整的异常堆栈定位到具体是哪个类哪一行出的问题再根据错误信息的关键词去排查。95%以上的报错堆栈信息里已经明确告诉你问题出在哪了只是很多人没有耐心看。7. 写在最后对我做这个项目的几点体会这个系统的开发周期如果一个人全职做大概需要两到三周其中数据库设计加核心功能实现占了一周半剩下的时间基本都在写页面和修bug。如果你现在刚开始动手我建议先花一整天把数据库表全部建好把表关系理清楚后面所有代码都会顺畅很多。表结构没想清楚就急着写代码后面返工的时间够你重新写一遍前端页面。还有一个建议是尽早部署起来本地开发环境和你最后演示的环境要保持一致。我见过太多同学代码在自己电脑上跑得好好的一到答辩现场就这报错那报错其实很多问题都是jdk版本不一致或者mysql版本不一致引起的。有条件的话项目做完后打包成jar包在一台干净的环境上完整跑一遍这一步能帮你避开演示翻车的尴尬。最后说句掏心窝的话如果你的目标是做一款真正能用的系统那就不要只停留在“能跑就行”。试着把某个点做深比如库存流水的完整性校验、比如并发扣减的测试、比如敏感操作的操作日志记录这些细节才是真正拉开差距的地方。答辩时候老师最常问的问题就是“你这个系统还有什么可以改进的地方”如果你能列举出几个自己思考过的真实优化方向并且说出具体的实现思路那这段对话就会非常顺利。如果你在开发过程中也遇到了什么坑或者对某个功能的实现有更好的方案欢迎一起交流。做这类系统不难但把它做扎实、做得有条理其实是需要一点细心和耐心的。祝顺利。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价