ARTICLE DETAIL

资讯详情

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

基于SpringBoot的食品公司采购管理系统设计与实践

基于SpringBoot的食品公司采购管理系统设计与实践 最近好几个读者在问采购管理系统怎么做刚好我手头刚完成一个基于SpringBoot的食品公司采购管理系统的设计与实现趁着热乎把整个思路和踩坑记录整理出来。这个项目是给东方红食品公司做的属于典型的中小企业进销存类系统技术栈就是SpringBoot MyBatis Plus MySQL Vue经典的前后端分离结构。如果你正准备做类似的毕设、课设或者公司内部要上一套轻量采购系统这篇内容应该能帮你少走不少弯路。先说下这套系统到底解决什么问题。食品公司的采购跟一般贸易公司还不一样原料品类多、批次杂还有保质期和检验报告这些特殊字段。东方红食品公司原来的采购流程基本靠Excel加微信沟通采购申请、审批、下单、入库全凭人工传递时间一长就出现数据对不上、供应商价格没法横向对比、库存积压和缺货并存这些老毛病。系统要做的就是把采购申请、供应商管理、订单跟踪、入库验收、应付账款这几条线全部数字化让每一笔采购从申请到付款都有据可查。1. 项目整体设计与技术选型思路1.1 需求分析食品行业采购的特有痛点在做需求调研的时候我列了一个问题清单挨个业务口去问采购部、仓储部、财务部、分管副总各聊了一遍。最后梳理出来的核心需求其实就六条采购申请走线上审批、供应商信息统一管理、采购订单全程跟踪、到货验收与入库联动、采购数据多维度统计、系统权限分级控制。食品行业还多了两个特殊要求一是原料必须有生产日期和保质期字段临近保质期的库存要能预警二是每批原料要关联质检报告没有质检报告的物料不能入库。这些需求乍一看不复杂但真正落地的时候会发现采购订单状态流转比想象中麻烦得多。从“待审批”到“已审批”再到“部分到货”“全部到货”最后“已入库”“已结算”中间的边界条件特别多。比如一张订单采购1000公斤面粉供应商先送了600公斤这时候订单既不能算完成也不能简单标记为到货必须支持分批收货并自动计算剩余数量。这些业务细节如果前期不梳理清楚后期开发就是返工重来的命。1.2 技术栈选型为什么是SpringBoot而不是SSH或SpringMVC技术选型这块我基本没犹豫就定了SpringBoot。理由很实际第一SpringBoot的自动配置机制能把过去SSH时代那一大堆XML配置彻底干掉项目启动快开发效率高对中小企业项目来说维护成本也低。第二SpringBoot生态太成熟了MyBatis Plus、Redis、JWT、EasyExcel这些常用的轮子都有现成的Starter引入即用。第三招人好招现在搞Java的谁不会SpringBoot以后公司自己维护系统也不愁没人接手。对比一下传统SpringMVC工程最大的区别就是SpringBoot把“约定大于配置”做到了极致。原来要写web.xml、spring-mvc.xml、applicationContext.xml三个配置文件还得在Tomcat里手动部署war包。SpringBoot直接内嵌Tomcat一个jar包扔上去就跑了部署运维的精力能省掉一大半。当然SpringBoot也不是没有缺点自动配置的魔法太多了出了问题排查起来确实比传统XML配置要费劲这个我在后面问题排查部分会详细讲。1.3 项目目录结构与分层架构设计项目结构我采用的是标准的分层架构com.dongfang.purchase作为根包下面按功能模块拆分子包。这种结构的好处是边界清晰控制层只负责参数接收和结果返回业务层处理核心逻辑数据访问层跟数据库打交道每一层都能独立测试、独立替换。com.dongfang.purchase ├── controller # REST接口层 ├── service # 业务逻辑层 │ └── impl ├── mapper # MyBatis Plus数据访问层 ├── entity # 数据库实体类 ├── dto # 数据传输对象 ├── vo # 视图返回对象 ├── config # 配置类拦截器、跨域、MyBatis Plus等 ├── common # 通用类统一返回结果、异常处理、常量 └── utils # 工具类JWT工具、日期工具等每个业务模块内部再按功能分包比如supplier包下面有SupplierController、SupplierService、SupplierMapper、Supplier实体。订单模块单独抽出OrderController、OrderItemController因为订单主表和订单明细表是一对多的关系拆开写更清晰。统一返回结果我用的是Result类包含code、message、data三个字段前端拿到code等于200就知道请求成功非200的统一走全局异常处理器弹出错误提示。2. 数据库设计与核心表结构2.1 采购管理系统的E-R模型与核心表梳理数据库设计是整个项目的地基这块我花的时间最多。食品公司采购管理一共涉及到十来张表核心的几张是供应商表、采购申请单表、采购订单表、订单明细表、入库单表、库存表、质检报告表、用户表、角色表、权限表。每张表之间都有明确的关联关系用外键逻辑关联而不是物理外键这样既保证数据完整性又不会因为物理外键拖累查询性能。供应商表要注意的字段有供应商编码、名称、联系人、电话、地址、开户行、银行账号、合作状态启用/停用、创建时间。采购订单表的核心字段是订单编号、供应商ID、采购员ID、订单状态、订单金额、预计交货日期、实际交货日期、备注。订单明细表要记录物料ID、采购数量、单价、金额、已收货数量、生产日期、保质期。入库单表要关联订单ID、仓库ID、入库类型采购入库/退货入库/盘点入库、入库人、入库时间。2.2 订单状态与审批流程的状态机设计采购订单的状态流转是整个系统最容易写乱的地方我画了一张状态图代码里用常量类或者枚举来统一管理。我的订单状态设计如下0待审批、1已审批待发货、2部分到货、3全部到货、4已入库、5已取消。注意这里把“到货”和“入库”分成了两个状态就是因为实际业务中到货之后不一定马上就入库质检可能还要走一两天。状态流转的边界条件要提前跟业务方确认清楚。比如订单已审批但还没到货的时候采购员能不能修改订单数量我的设计是不允许改数量因为供应商那边已经开始备货了数量一变容易出纠纷。但允许修改预计交货日期因为物流延迟是常事。还有一种情况是订单已经全部到货但质检不合格需要退货这时候不能直接改状态而是要生成一张退货单反向冲减入库数据。这些业务规则如果不在代码层面做控制光靠前端按钮的显示隐藏是挡不住的。2.3 库存与质检数据的一致性方案食品公司的库存管理跟普通商品一个很大的区别就是要按批次管理。同一个面粉生产日期不同不能混放在一个库存记录里。所以库存表的设计不是简单的“物料ID 数量”而是“物料ID 生产日期 批次号 数量”。入库的时候自动生成批次号出库的时候遵循先进先出原则优先出生产日期早的那批。质检报告的关联我设计在入库环节。供应商送货过来之后仓库管理员先录入到货信息这时候订单状态更新为部分到货或全部到货。但入库按钮是灰的必须等质检员上传质检报告并审核通过后才能执行入库操作。入库的同时系统自动把质检报告的编号关联到库存批次上以后哪批货出了问题直接通过批次号就能反向查到供应商、采购订单、入库时间和质检报告实现全链路追溯。3. SpringBoot核心功能实现与编码实操3.1 用IDEA快速创建一个SpringBoot项目创建项目的姿势我推荐两种一种是用IDEA自带的Spring Initializr一种是直接去start.spring.io网站下载压缩包。IDEA里操作很简单File - New - Project左侧选Spring InitializrSDK选JDK 1.8或者11然后填Group和Artifact。依赖这块我选的是Spring Web、MyBatis Framework、MySQL Driver、Lombok。注意IDEA默认选的SpringBoot版本可能比较新如果本地JDK版本跟不上的话很尴尬这里我建议一开始就选一个跟JDK匹配的稳定版本比如JDK1.8配SpringBoot 2.7.xJDK17可以配SpringBoot 3.x。项目创建好之后第一件事就是检查pom.xml。我之前有次帮别人排查问题发现项目跑不起来最后一看是SpringBoot 2.7和MyBatis Plus 3.5.3的兼容性没问题但跟Druid数据源连接池的某个版本冲突了启动直接报NoSuchMethodError。这种问题排查起来特别浪费时间所以版本管理从一开始就要谨慎。我的习惯是在pom.xml的properties节点里把所有版本号都列出来统一管理核心依赖的版本尽量不追新用稳定版就好。3.2 核心依赖与关键注解的实践解析SpringBoot的核心优势很大程度体现在注解上RestController标记一个类为REST接口控制器RequestMapping或者它的快捷变体GetMapping、PostMapping用来映射请求路径。Autowired做依赖注入Service标记业务层组件MapperScan在启动类上扫Mapper接口。这里重点说一下SpringBootApplication这个注解它其实是个组合注解包含SpringBootConfiguration、EnableAutoConfiguration、ComponentScan三合一。EnableAutoConfiguration是SpringBoot自动配置的灵魂它通过读取META-INF/spring.factories文件里的自动配置类按条件装配Bean。比如你引入了spring-boot-starter-data-redis它就会自动帮你配好RedisTemplate引入了druid-spring-boot-starter就会自动帮你创建DruidDataSource。理解了这一点后面排查“为什么我什么都没配就能用”或者“为什么我配了不生效”这类问题就顺手多了。MyBatis Plus的注解也值得说一下。实体类上TableName指定表名主键用TableId(type IdType.AUTO)表示数据库自增非主键字段用TableField指定列名。有些字段比如createdTime在数据库里叫created_timeJava实体里叫createdTime需要在TableField里做映射不然查询直接报字段不存在。3.3 采购订单模块的后端编码实现采购订单模块是整个系统的核心我拆成两部分订单主表和订单明细表。前端提交订单的时候一次性把主表信息和明细列表一起传过来后端用一个DTO接收然后在一个事务里同时插入主表和明细表保证数据一致性。这里贴一个核心的Service实现片段思路就是在事务方法里先算订单总金额再循环插入明细最后插入主表。Service Slf4j public class PurchaseOrderServiceImpl implements PurchaseOrderService { Autowired private PurchaseOrderMapper orderMapper; Autowired private PurchaseOrderItemMapper orderItemMapper; Override Transactional(rollbackFor Exception.class) public Result saveOrder(PurchaseOrderSaveDTO dto) { // 校验供应商是否存在且启用 Supplier supplier supplierMapper.selectById(dto.getSupplierId()); if (supplier null || supplier.getStatus() ! 1) { return Result.error(供应商不存在或已停用); } // 校验订单明细不能为空 if (dto.getItems() null || dto.getItems().isEmpty()) { return Result.error(订单明细不能为空); } // 生成订单号格式PO 日期 随机数 String orderNo PO LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) RandomUtil.randomNumbers(4); PurchaseOrder order new PurchaseOrder(); order.setOrderNo(orderNo); order.setSupplierId(dto.getSupplierId()); order.setOrderStatus(0); // 待审批 // 计算总金额并插入明细 BigDecimal totalAmount BigDecimal.ZERO; for (PurchaseOrderItemDTO item : dto.getItems()) { PurchaseOrderItem orderItem new PurchaseOrderItem(); orderItem.setMaterialId(item.getMaterialId()); orderItem.setPurchaseQuantity(item.getPurchaseQuantity()); orderItem.setUnitPrice(item.getUnitPrice()); BigDecimal amount item.getUnitPrice().multiply(BigDecimal.valueOf(item.getPurchaseQuantity())); orderItem.setAmount(amount); totalAmount totalAmount.add(amount); orderItemMapper.insert(orderItem); } order.setTotalAmount(totalAmount); orderMapper.insert(order); return Result.success(order.getId()); } }这段代码有几点要注意。第一Transactional注解一定加上rollbackFor Exception.class因为Spring的事务默认只在遇到RuntimeException时才回滚如果业务代码抛的是自定义的CheckedException事务是不会回滚的。第二金额计算用BigDecimal不要用doubledouble的精度问题在涉及钱的场景是致命的。第三订单号我采用了“前缀时间戳随机数”的生成方式在并发量不高的场景够用如果以后并发上来了要换成分布式ID方案。审批接口的逻辑也不复杂角色为“采购经理”或“分管副总”的用户调用审批接口传订单ID和审批结果通过/驳回以及审批意见。审批通过后把订单状态改成1驳回则改成5并记录驳回原因。这个操作同样要加事务并且要记录审批日志方便后续追溯。3.4 供应商管理与采购入库的联动逻辑供应商管理模块相对基础就是增删改查加一个启停用状态。但有一个关键校验如果某个供应商已经存在未完成的采购订单是不能直接删除的只能停用。我在删除接口里加了一个订单关联查询只要查到该供应商名下有待审批或已审批状态的订单就提示“该供应商存在未完成订单无法删除请先完成订单或停用供应商”。采购入库是联动的重点。订单全部或部分到货后仓库管理员在入库页面选择采购订单系统自动带出订单明细管理员填写每个物料的实收数量、生产日期和保质期然后提交入库。后端逻辑分三步更新订单明细的已收货数量判断是否全部到货并更新订单状态插入入库单并把物料按批次加入库存表。这三步在一个事务里完成任何一步失败都要整体回滚。同时如果订单关联了质检报告且质检异常入库接口要直接拦截提示必须先处理质检异常。3.5 数据权限与用户认证的实现方案系统一共有四种角色系统管理员、采购员、采购经理、仓库管理员。管理员拥有全部权限采购员能维护供应商、创建采购订单、查看订单明细采购经理能审批订单、查看采购报表仓库管理员只能访问入库相关功能。认证方案我用的JWT SpringBoot拦截器。用户登录成功后后端生成一个token返回给前端前端在后续每个请求的Header里带上Authorization: Bearer token。拦截器里校验token的合法性然后把用户ID和角色信息放进ThreadLocal方便后续在Service里获取当前登录用户。这里有一个坑要提醒JWT是无状态的服务端一旦签发无法主动让token失效所以退出登录功能的实现要靠前端删掉本地token或者引入Redis做黑名单。权限控制我用的是自定义注解RequirePermission配合拦截器实现。在需要权限控制的接口上加注解比如RequirePermission({purchase:order:approve})拦截器拿到当前用户的权限列表做匹配匹配不上直接返回403。这种方式比在代码里写一堆if-else判断角色要优雅得多新增接口的时候只需要在方法上声明需要的权限码就行。4. 全链路项目开发实践与经验总结4.1 SpringBoot配置中心的经验从application.yml到多环境切换SpringBoot的配置文件这块我确实踩过不少坑。默认情况下是application.yml可以设置服务器端口、数据库连接、日志级别等等。但实际项目肯定要区分开发环境、测试环境、生产环境我通常用application-dev.yml、application-test.yml、application-prod.yml三个文件然后在主配置文件里用spring.profiles.active来指定当前启用哪个环境。启动的时候也可以动态指定比如命令行启动jar包时用--spring.profiles.activeprod这样打完包以后不需要改任何代码就能切换环境。数据库连接配置这块有个细节很多新手容易忽略。因为现在基本都是微服务拆分的趋势很多系统的数据库都不止一个但单体的采购系统一般只要一个主库就够了。如果你真遇到了多数据源的场景比如订单库和用户库分开那就要自己配置DataSource路由了。我的建议是掌握MyBatis Plus的动态数据源starter通过DS注解在Mapper或者Service方法上切换数据源比手动管理多个SqlSessionFactory简单太多。4.2 前端Vue页面与后端接口交互的几个关键细节我负责后端但跟前端联调时也发现了一些值得注意的问题。前后端分离模式下最容易出乱子的就是时间格式。后端返回的LocalDateTime默认序列化成数组或者带T的格式前端拿到根本没法直接显示。解决方案是在application.yml里全局配置Jackson的日期格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8还有一个是跨域问题。如果是前后端分离部署前端在8080端口后端在9090端口那前端直接调后端接口肯定会有CORS报错。我在config包里写了一个CorsConfig实现WebMvcConfigurer接口重写addCorsMappings方法允许所有来源和所有请求头跨域。如果是生产环境更好的做法是用Nginx做反向代理把/api路径转发到后端服务让前端看起来是同源的这样连跨域配置都省了。4.3 版本兼容性排查和一些典型的坑SpringBoot版本太高这个热搜词确实值得单独拿出来说。我遇到过的情况是本地JDK是1.8但SpringBoot Initializr默认创建的SpringBoot版本已经是3.2了而SpringBoot 3.x要求JDK17起步直接启动就报UnsupportedClassVersionError。这种情况要么升级JDK要么手动把版本降到2.7.x并重新导入依赖。还有一个常见坑是SpringBoot 3.x把javax包名改成了jakarta引入一些老依赖的时候会直接编译失败。MyBatis Plus的Wrapper查询也有坑。用LambdaQueryWrapper的时候如果实体类某个字段在数据库里不存在但实体里又写了这个属性查询的时候MyBatis Plus会把所有属性拼进SQL的WHERE条件导致SQL执行报错。解决方案是在实体类对应字段上加TableField(exist false)注解告诉MyBatis Plus这个字段不是表字段。4.4 部署上线与性能优化的实用心得项目的部署方式我推荐用jar包加托管的模式直接java -jar命令启动加上内存参数调优。中小企业服务器配置一般不高所以我习惯这么启动java -Xms512m -Xmx1024m -XX:UseG1GC -jar purchase-system.jar --spring.profiles.activeprod数据库连接池配的是Druid最大连接数20初始化5最小空闲5再开一个慢SQL监控。食品采购系统并发量不会特别大但报表查询偶尔会卡我给订单表、明细表加了几个常用组合索引比如supplier_id, order_status、order_id, material_id查询性能提升非常明显。再就是查询条件里如果有日期范围索引设计要考虑最左匹配原则不能盲目建一堆索引不然写性能反而下降。Redis在系统里的角色主要是缓存字典数据和最近访问的热点数据比如供应商下拉列表、物料列表这些基本不变的数据从数据库查一次然后缓存到Redis接口响应时间从几十毫秒降到几毫秒。另外我还在Redis里保存了登录用户的token、权限列表方便拦截器快速校验。网上有很多关于Redis在SpringBoot中的使用的文章但每个人的场景不一样我这边只用了最基础的String和Hash两种结构没有引入太复杂的设计。4.5 食品采购系统面试常见的SpringBoot考点整理由于好几个读者是在准备毕业设计答辩和实习面试我把SpringBoot要准备的知识点也梳理了一下。自动装配原理属于必问题要能说清楚SpringBootApplication的底层结构、spring.factories文件的作用、Conditional的条件装配机制。常用注解也是高频问题Controller层的RestController、PathVariable、RequestParam业务层的Service、Transactional配置相关的ConfigurationProperties、Value都要能结合项目实例讲清楚。SpringBoot启动流程也经常被问到基本套路是从main方法启动SpringApplication先推断Web应用类型设置初始化器和监听器然后启动Spring容器刷新容器完成自动配置最后执行runner回调方法。回答的时候如果能结合自己项目的实际操作比如在ApplicationRunner里放了一段初始化缓存数据的逻辑会显得更有说服力。这些知识点要提前准备好答辩的时候才能做到心里有数而不是临时抱佛脚。5. 系统测试与问题排查实录5.1 采购审批流程联调中的典型bug这个审批流程的bug特别有代表性。前端点审批通过按钮之后接口发现采购经理这个角色明明有权限但后端返回了403。我查了半天最后发现是拦截器里从token解析角色的时候解析出来的是角色编码比如MANAGER但权限配置表里存的是角色ID比如3两者对不上权限匹配逻辑直接挂了。这就是典型的“数据约定不一致”问题前端、后端、数据库三方对同一概念的表示方式没对齐。之后我在代码规范里加了一条所有枚举状态和角色编码前后端统一使用字符串编码不再用数字ID做业务判断。另一个bug是订单批量审批的场景。审批人勾选多张订单然后统一驳回结果发现只驳回了一部分另外几张状态还是“待审批”。排查下来发现问题在事务边界批量审批方法是一个循环循环里每次调审批方法但审批方法的Transactional是private方法之间的自调用事务不生效。解决方法是把事务注解加在批量审批的public方法上或者在循环里也保持整体事务一次commit或一次rollback。5.2 性能瓶颈与慢SQL的排查记录系统上线跑了一周之后发现供应商列表接口慢得离谱从原来的几百毫秒涨到了三四秒。查监控日志发现数据库有个SQL执行了4.5秒执行计划显示走了全表扫描。原因很典型供应商表的数据量虽然只有几百条但这个SQL用到了like %关键词%的模糊查询条件而且关联了另一张几千条数据的联系人表没有走索引。解决方案是给联系人姓名和供应商名称加上前缀模糊查询的优化或者直接用全文检索的方案。但考虑到数据量不大我采用了更简单的方案前端改成按名称前缀搜索SQL里like 关键词%就能命中索引。同时给关联查询的字段加了复合索引性能一下就回落到300毫秒以内。这里提醒一下每天坚持看一次慢SQL日志很多性能隐患都是这么发现的。5.3 一套拿来即用的调试排错技巧最后分享几个实际开发中特别管用的调试技巧。第一SpringBoot项目启动日志一定要看到“Started Application in x.xxx seconds”才算真正启动成功如果卡在Started Application之前多半是某个Bean初始化失败去日志里找Caused by就能定位问题。第二接口调不通的时候先看Nginx、网关的转发日志再看后端服务的访问日志最后看数据库和Redis的日志层层排查不要一上来就去搜代码。第三多利用Postman或者Apifox做接口调试尤其是事务相关的操作可以把请求过程抓包下来跟后端日志一步步比对。说一个我常用的排查手段在application.yml里临时把MyBatis Plus的日志级别调到debug这样所有执行的SQL语句都会打在控制台里一眼就能看出SQL和参数是不是预期的那样。排查完再调回info级别避免生产环境日志量太大。做这个项目最大的体会是技术本身永远不是项目成败的关键关键在于是不是把业务逻辑吃透了。一个状态字段设计得好不好一个审批流程走不走得通一个事务边界画得对不对这些细节直接决定了系统好不好用。SpringBoot只是提供了一套高效开发的框架真正值钱的是你对业务的理解和把这些业务变成代码的能力。如果你正在做类似的项目建议先把业务流程画清楚把每个状态的流转路径和边界条件想明白再动手写代码你会发现后面的开发顺很多。
返回列表