毕业设计做到库存管理系统算是最经典的题目之一了。我在写这套基于SpringBoot3Vue3的超市库存管理系统时最深的感受是难的不是把增删改查页面堆出来而是把库存数据的变化讲清楚。库存系统表面上是一堆CRUD但真正把进货、出货、盘点、预警、对账串起来涉及数据库设计、并发控制、权限验证、前后端联调一整条链路。这套系统源码和数据库脚本我都整理好了下面把从选型思考到落地实现的全过程拆开讲给正在做类似毕设、或者想快速搭一个内部管理系统的同学一份能直接照做的参考。先说结论这套系统采用前后端完全分离的架构后端用SpringBoot3 MyBatis-Plus MySQL 8.0前端用Vue3 Vite Element Plus Pinia登录认证用JWT库存扣减做了并发安全处理核心业务覆盖商品管理、分类管理、供应商管理、入库出库、库存盘点、低库存预警、月度出入库趋势统计。无论你是要应付毕业答辩还是想拿来改一改做真实小超市的后台这套代码的基本盘都是稳的。1. 项目先想清楚再动手问题域与选型思路1.1 超市库存管理到底在管什么很多人一上来就写表结构结果写着写着发现业务逻辑理不清。我建议先回到真实场景把超市库存的日常业务拆成一条闭环采购进货、库存更新、销售出库、盘点调整、缺货预警。超市的一个典型流程是这样的——店长发现某品牌可乐快卖完了查看库存预警联系供应商下单到货后做进货入库库存量增加顾客在收银台买走可乐销售系统扣减库存每周做一次盘点发现账面和实物有差异需要做盘盈盘亏调整临期商品还需要提示避免过期损耗。所以库存管理系统本质是围绕“商品、供应商、仓库、库存数量、流水记录”这五个核心要素做管理。商品是静态档案供应商是货源库存是动态数量仓库决定商品存放位置流水记录每一次数量和金额变化。比起一个普通的学生管理系统这套业务闭环更完整也更能体现你对业务的把握能力。1.2 为什么选定 SpringBoot3 Vue3选技术栈这件事既要考虑项目的合理性也要考虑答辩时能不能讲出东西。SpringBoot3 是当前企业级Java开发的绝对主流它基于Spring6把JakartaEE命名空间、项级异常处理、优雅的配置绑定都带进来了用它做后端面试和答辩都会加分。Vue3 也早已过了过渡期组合式API、reactive响应式、Pinia状态管理、语法糖都是实际项目在用的东西还守着Vue2的写法反而显得过时。更实际的原因是SpringBoot3 Vue3 的组合前后端完全分离逻辑边界清晰。后端只负责提供REST接口前端负责交互渲染分工明确代码好维护正好适合展示一个完整软件项目的结构。JWT做无状态登录、MyBatis-Plus做数据访问、Element Plus做后台组件库这些工具组合在一起开发效率很高两三天把主体页面跑起来完全没问题。1.3 这套系统适合什么人参考我把目标受众想得很清楚第一类是正在准备毕业设计或课程设计的计算机相关专业学生需要一个能讲清楚架构、也有完整代码支撑的选题第二类是想接触前后端分离项目开发的初级程序员尤其是想熟悉Vue3组合式API和SpringBoot3接口开发的人第三类是创业或个体经营者想快速搭一个轻量超市/便利店管理后台用这套源码改改界面就能上岗。本系统最终实现的功能模块包括系统登录与JWT鉴权、员工用户管理、商品信息管理、商品分类、供应商管理、入库单管理、出库单管理、库存实时查询、库存预警、盘点管理、登录日志与操作日志、基于ECharts的出入库趋势图表。对毕设来说这个功能量已经足够撑起一篇不错的论文了。模块功能点关键设计登录认证账号密码登录、JWT签发与校验密码BCrypt加密无状态鉴权商品档案商品增删改查、条形码、规格、价格分类和供应商外键关联库存管理实时库存、低库存预警、库存台账扣库存用条件更新防超卖单据管理入库单、出库单、审核流程单据审核通过后写流水盘点管理盘点单创建、差异调整差异以盘点单形式入流水统计报表出入库趋势、商品成本毛利ECharts前端图表展示2. 数据库设计库存系统的心脏2.1 核心表结构与关系拆解库存系统做得好不好数据库设计至少占一半。我先列出系统里的核心表用户表、商品分类表、供应商表、商品表、库存表、库存流水表、入库单表、出库单表、盘点单表。其中最关键的是商品表、库存表和库存流水表这三张。商品表负责静态档案商品ID、名称、条形码、分类ID、供应商ID、规格单位、进货价、零售价、保质期天数、上下架状态。库存表则需要单独拆出来字段是库存ID、商品ID、仓库ID、当前数量、安全库存阈值、最近更新时间。我强烈不建议把当前库存直接塞进商品表的字段里因为商品的描述信息很少变动而库存数字每次出入库都在变两者放一起会导致商品表频繁更新锁竞争严重还会让历史快照变得难以追溯。这就像记账时分流动资产和固定资产账户你不可能把现金数额写在资产名称后面。库存流水表是这套系统的命脉字段包括流水ID、商品ID、单据编号、业务类型入库、出库、盘点调整、变动前数量、变动后数量、变动数量、经办人ID、备注、创建时间。每一条流水都不可修改、不可删除只允许新增。你只要保留完整的流水任何时间点的库存数量都能通过流水重建这就是审计能力。在设计时我给商品表和库存表都加了唯一索引商品表用条形码保证不重复库存表用商品ID加仓库ID的组合来保证同一商品在一个仓库只有一条库存记录这一步对防止脏数据至关重要。2.2 单据与流水设计的思路单据和流水的配合是很多人会搞反的一件事。一开始我想过直接调库存接口做加减后来发现这样的系统根本没法对账。正确做法是入库出库都先落单据再走审核审核通过后才真正影响库存、产生流水。为什么非要中间插一道单据因为真实超市不可能没有凭证供应商送货有送货单盘点有盘点表。单据是业务凭证流水是库存变动轨迹两者通过单号关联出现争议时能回溯。单据表的设计也比较直接入库单表包含单号、供应商ID、入库类型、总金额、审核状态、制单人、审核人、入库时间入库单明细表记录每一种商品、数量、进价、小计。出库单同理只是把供应商换成出库类型比如销售出库或报损出库。审核这个动作很关键我专门设计了一个状态字段未审核的单据不会影响库存这样操作员录错单还有机会改而不是直接污染库存数据。流水表的变动前数量和变动后数量我建议一定保留虽然有点冗余但排查对账问题时候能省至少一半的时间。2.3 数据库设计时容易踩的坑第一个坑是金额和数量选错字段类型。单价和金额要用decimal(10,2)绝对不能用float或double——二进制浮点数在十进制计算时有精度误差超市卖菜价格算错一分钱都能被找上门。数量字段我用的decimal(12,3)因为部分商品按千克计价保留三位小数更灵活。第二个坑是删除策略供应商被商品引用后不能物理删除我选择逻辑删除字段这样避免外键约束报错也保留历史关系。第三个坑是忽略操作人和时间字段这个问题直到我做对账的时候才意识到没有经办人和操作时间的流水根本没法查责任后来把所有业务表都补上了创建人、更新人、创建时间、更新时间四个通用字段。提示数据库设计阶段多花两个小时画清楚实体关系图后面写代码能少改两天。库存系统最忌讳的就是上线跑了一段时间才发现库存对不上。3. 后端实现SpringBoot3 的关键环节3.1 工程骨架与依赖配置后端项目我从Spring Initializr生成的空工程开始Java版本用17SpringBoot版本3.2.x。这里必须提醒一个坑SpringBoot3与旧版MyBatis-Plus的starter不兼容因为SpringBoot3使用的是JakartaEE规范包名从javax变成了jakarta直接用mybatis-plus-boot-starter会启动报错。正确做法是引入mybatis-plus-spring-boot3-starter早期我用2.x版本折磨了很久才发现是坐标问题。核心依赖大致是这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency顺便说下为什么加validation依赖参数的校验不能只靠前端接口层必须用NotNull、Min这类注解做服务端校验超市商品的进货价不能负数入库数量不能小于等于零这些都是刚需。除了这些我还配置了统一异常处理器用RestControllerAdvice对所有业务异常做兜底返回结构统一成code、message、data三件套前端联调效率能提高很多。3.2 登录认证与接口鉴权登录认证选型时我纠结过Sa-Token和JWT最后选了JWT。原因很简单前后端分离后接口是无状态的JWT把用户身份信息编码进token里服务端不用存会话水平扩容也方便。实际部署中前端把token存在localStorage每次请求在Authorization头里带上Bearer token后端用拦截器统一校验。生成token的核心逻辑如下public class JwtUtil { private static final long EXPIRE 1000L * 60 * 60 * 24; private static final SecretKey KEY Keys.hmacShaKeyFor( please-change-this-secret-key-32bytes-min.getBytes(StandardCharsets.UTF_8)); public static String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(KEY) .build() .parseClaimsJws(token) .getBody(); } }顺便强调一点安全实践用户表里的密码不能存明文更不能存简单的MD5我用Spring Security里的BCryptPasswordEncoder做哈希同样的密码每次生成的哈希串不同有内建盐值相对安全。登录接口通过校验后返回token同时把它写入Redis做失效控制这样管理员能强制踢人下线。后端拦截器只放行登录和获取验证码的接口其余接口一律检查token有效期token过期或者被篡改直接返回401前端收到401就跳回登录页。3.3 出入库事务与库存扣减的并发控制库存扣减是整个系统最容易出问题的地方也是答辩时最能体现水平的地方。最常见的错误写法是先查询库存判断是否够扣够的话再执行update减库存。这个写法在并发量大时必然出问题两个请求同时查到库存只剩1件各自判断“够扣”然后同时做扣减库存就变成负数了。这就叫超卖。解决超卖的办法有好几个等级。简单可靠的是把“判断库存够不够”和“扣减库存”合并成一条带条件的update语句靠数据库行级锁和受影响行数来保证原子性Transactional(rollbackFor Exception.class) public void deductStock(Long goodsId, BigDecimal quantity, Long operatorId) { int rows stockMapper.deductWithCondition(goodsId, quantity); if (rows 0) { throw new BusinessException(库存不足或商品不存在); } Stock stock stockMapper.selectByGoodsId(goodsId); StockLog log new StockLog(); log.setGoodsId(goodsId); log.setQuantity(quantity); log.setBeforeQuantity(stock.getQuantity().add(quantity)); log.setAfterQuantity(stock.getQuantity()); log.setType(OUT); log.setOperatorId(operatorId); stockLogMapper.insert(log); }对应的SQL是update iddeductWithCondition update stock set quantity quantity - #{quantity} where goods_id #{goodsId} and quantity #{quantity} /update这个方案的核心理念是让数据库通过update语句自带的行锁做并发控制只有真正抢到锁的行才能成功更新抢不到锁的请求affected rows是0直接抛异常提示库存不足。注意一点库存扣减和流水记录必须在同一个事务里一旦流水写入失败库存也要回滚。还有个小陷阱——Transactional默认只在RuntimeException和Error时回滚所以你抛自定义业务异常时一定要指定rollbackFor Exception.class否则事务不生效库存被扣了流水没记录账就乱了。3.4 供应商、盘点与预警的实现要点供应商模块看起来是普通增删改查但删除逻辑要谨慎。我处理成逻辑删除删除前先检查商品表里是否还有关联商品有的话提示“该供应商已有商品关联不能删除”这样避免数据库外键报错也防止历史数据断裂。盘点模块我是这么设计的创建盘点单时记录商品的账面数实盘数由操作员录入系统自动算出差额盘点单审核后生成盘盈盘亏流水。有人可能觉得直接改库存数量更快但这样做你永远不知道库存为什么变了也给不出让店长信服的解释。预警功能分两块低库存预警和临期预警。低库存预警靠库存表里的安全库存字段商品当前数量低于阈值就展示红色高亮临期预警则需要记录商品的入库批次时间用当前时间加上保质期天数去推算商品是否临期。这两个查询逻辑都不复杂但要注意性能商品多的时候定时任务扫全表会慢我建议用MySQL的定时事件或者让前端在小范围内轮询统计数据毕设量级的表加好索引就足够了。4. 前端实现Vue3 组合式 API 实战4.1 初始化与目录规划前端脚手架我用Vite创建命令一行搞定npm create vitelatest admin -- --template vue cd admin npm install vue-router4 pinia element-plus axios目录结构我一开始就规划好api目录放所有接口请求views目录按业务模块拆页面stores目录放Pinia状态components目录放复用组件router目录放路由配置utils目录放axios封装和工具函数。很多新手把所有代码堆在App.vue里短期看能跑后期需求和页面多了根本没法维护模块化不是给老师看的是给自己熬夜省时间的。这里得重点说说Composition API和Options API的选择。如果你只是简单展示数据两者差别不大但如果业务复杂组合式API的优越性很快会体现出来。拿商品列表页举例搜索条件、分页数据、表单弹窗、数据加载这些逻辑之间是有依赖关系的用Options API要把相关代码分散到data、methods、watch、computed各个区块里改一个功能要来回跳用组合式API可以把同一业务逻辑封装成一个一个的小函数思路清晰、复用性高这就是Vue3的核心价值。4.2 登录页与权限路由登录页逻辑不复杂但联调的时候容易出问题。流程用户输入账号密码前端先做必填校验通过后调用登录接口后端返回token和用户信息。前端把token写入localStorage同时把用户信息存入Pinia随后跳转到首页。我在axios里做了统一封装用请求拦截器给每个请求自动加上Authorization头用响应拦截器统一处理错误尤其是401状态码让它自动清理token并跳转登录页。这样业务代码里就不用每个接口都写一遍异常处理了。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.message || 请求失败) return Promise.reject(error) } )权限路由用的是路由守卫在导航守卫里读取用户角色再根据menu配置动态生成可访问路由不同角色看到的管理菜单不一样。我数据库里设计了三档角色管理员、店长、操作员管理员能看到员工管理和系统日志操作员只能处理商品入库出库这样在答辩时能讲出“基于角色的访问控制”这个点。4.3 库存看板与表单联动Vue3 响应式实战核心页面是库存看板它不是一个简单表格而是一个集搜索、分页、预警高亮、操作按钮于一体的综合界面。我用reactive创建了查询条件对象包括商品名称关键字、分类、库存状态、当前页码、每页条数再通过computed计算是否符合低库存预警的筛选结果。表格用Element Plus的el-table配置了多条件筛选、多选行、库存数低于安全库存的高亮样式。分页组件我用el-pagination每页条数变化或页码变化时重新加载列表接口返回的total作为分页总数体验很顺。表单联动是另一个容易踩坑的点。拿入库单举例操作员选择商品后表单应该自动带出该商品的供应商、规格、单位、当前可用库存然后输入的数量不能超过一个合理上限。这个需求在Vue3里用watch监听选中的商品ID变化后调接口拉详情并填充表单。有一点必须提醒在组合式API里watch被解构出来的属性时往往会丢失响应式正确写法是传入包含响应式对象的引用比如watch(() form.goodsId, handler)。我当时在这个问题上卡了两个小时页面一直不更新最后才发现是watch监听错了对象。4.4 数据可视化与 ECharts 集成毕业设计加一个统计图表页面效果和答辩印象分会明显不同。我用ECharts做了一个近30天出入库趋势图数据来自后端的统计接口返回每天的入库总量和出库总量。Vue3中使用ECharts有个关键注意事项图表实例必须在onMounted之后初始化因为此时DOM元素才真正渲染出来组件销毁时必须调用dispose释放实例否则页面反复切换会造成内存泄漏和卡顿。import * as echarts from echarts import { onMounted, onBeforeUnmount, ref } from vue const chartRef ref(null) let chart null onMounted(() { chart echarts.init(chartRef.value) chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: dateList.value }, yAxis: { type: value }, series: [ { name: 入库, type: line, data: inList.value }, { name: 出库, type: line, data: outList.value } ] }) }) onBeforeUnmount(() { chart?.dispose() })统计接口返回的数据我用reactive包裹接口响应后直接赋值给chart配置再调用setOption。这里有个我自己踩过的坑ECharts的setOption是增量更新如果你只是改了数据没改坐标轴直接setOption没问题但如果数据维度变化需要加一个true参数做完全替换否则图表会残留旧数据。5. 完整搭建流程从空目录到跑通系统5.1 环境准备与数据库初始化先列出我本机的完整环境JDK 17、Maven 3.9.1、MySQL 8.0.33、Node.js 18.18、Redis 7.0。顺序很重要先装基础环境再初始化数据库再启动后端最后启动前端。数据库初始化很简单创建数据库并导入我写好的初始化SQL脚本脚本里包含建库语句、建表语句和基础测试数据顺便预置一个管理员账号。后端配置文件注意两点数据源url要加上时区参数serverTimezoneAsia/Shanghai否则时间字段会差8小时驱动类在MySQL8版本是com.mysql.cj.jdbc.Driver以前那个com.mysql.jdbc.Driver已经废弃了。如果连接被拒绝检查MySQL服务有没有启动、端口是不是3306、用户名密码是否正确这三大件排查完80%的问题都能解决。5.2 后端启动与接口验证在项目根目录打开终端执行mvn spring-boot:run看到Spring Boot启动成功的日志并且监听8080端口后就说明后端起来了。我习惯先用curl验证核心接口curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123}如果返回包含token的JSON结构说明登录接口正常。然后再用一个带token的请求去访问受保护接口比如商品列表不带token访问应该返回401带正确token返回数据这就验证了JWT拦截器生效。5.3 前端启动与联调跨域问题的根治前端启动前有一件事必须先做否则联调全是跨域报错——在vite.config.js里配置代理。开发环境下前端页面跑在5173端口后端接口在8080端口直接请求必定跨域解决方案是把前端对/api的请求代理到后端export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配置之后原来请求http://localhost:8080/api/goods的接口前端统一写成/api/goods由Vite开发服务器转发浏览器看到的请求是同源的自然没有跨域。我遇到过很多人开发环境用axios直接填后端完整地址然后又到后端写一堆CORS配置其实开发环境根本不需要后端配CORS代理就够用了。后端配CORS在跨域请求复杂时会触发OPTIONS预检反而容易出问题。开发环境跑通后执行npm run dev浏览器打开http://localhost:5173用初始账号登录进系统能正常加载商品列表和库存数据说明前后端联调成功。5.4 生产构建与部署注意事项后端的打包部署比较直接mvn clean package -Dmaven.test.skiptrue java -jar target/supermarket-admin.jar --spring.profiles.activeprod前端先npm run build生成dist静态资源目录然后交给Nginx托管。这里要注意的是Vue Router如果用了HTML5的history模式用户直接访问某个子路由比如/goods时Nginx找不到对应的物理文件会返回404所以Nginx配置里必须加try_filesserver { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }两个location一个管前端页面一个把/api的请求反向代理给后端Java服务。不想折腾Nginx的同学也可以直接用hash模式路由在url里带个#号刷新不会有问题但美观程度差一点。生产环境我建议前端和后端部署在同一台2核4G的云服务器上就够了再大的超市这个配置也顶得住。6. 常见问题与排查实录6.1 登录后页面一直401甚至无限跳登录页这是前后端联调最常遇到的问题。排查思路分三步第一步打开浏览器开发者工具的Network面板看请求有没有发出、有没有带Authorization请求头第二步看后端日志有没有报JWT解析异常异常里会写明是token过期、签名错误还是token为空第三步看前端响应拦截器确认401时是否触发重复跳转。常见原因无非是token没存对地方或者请求拦截器里取token的key跟存储时不一致我甚至见过有人把token存pinia里但刷新页面pinia状态被清空导致又变成登录状态后来统一改成localStorage配合pinia双写才稳定。还要注意一个细节后端密码是BCrypt加密如果测试数据里预置的密码是明文登录永远会失败要确保数据库里的密码字段存的确实是BCrypt哈希字符串。6.2 库存出现负数或者扣减后流水对不上库存负数是最典型的并发问题。排查时先看扣减SQL是不是用了带quantity #{quantity}条件的那条update语句如果用的是先select再update那配置再好的机器在高并发下也会出问题。第二个要检查的是事务配置看扣库存方法和写流水方法是否在同一个Transactional里事务有没有配置rollbackFor Exception.class。第三个容易被忽略的问题是MyBatis的二级缓存万一你开启了对库存实体的缓存并发场景下读到旧数据的概率更高库存相关的查询我建议全部走一级缓存或者强制刷新。我复盘过整个系统最重要的实践经验是库存操作的所有入口必须收口不能在多处Service里各写一套扣库存逻辑否则有人加了单据、有人直接改库存流水就完全对不上。收口到一个InventoryService所有业务都调它出了事也好排查。6.3 Vue3 表格数据更新但页面不刷新这个问题的根源在于Vue3的响应式机制。最常见的情况是从接口拿回数组后直接整体赋值给ref变量但其实你用ref包的是数组时必须用.value赋值否则只是把数据存到变量的普通属性上页面不会感知变化。还有一个坑是把reactive对象解构赋给普通变量解构后得到的普通变量不再具备响应式改成任何地方都不触发更新。遇到这类问题我建议先打印一下赋值后变量的实际类型和变化前后引用地址通常能立刻看出是不是把响应式对象搞丢了。el-table本身还有一个小坑当表格数据更新但列配置没有变化时表格可能不重新渲染尤其你用了筛选后的数据。解决办法是给el-table加一个:key值可以用当前查询条件的hash或时间戳或者用nextTick强制刷新表格。这些不是框架bug是ECMAScript引用变化的特性决定的理解了原理就很好解决。6.4 其他典型问题速查表问题现象可能原因解决办法Maven依赖下载极慢默认中央仓库网络不稳定配置阿里云镜像仓库MyBatis-Plus与Boot3不兼容用错starter坐标换成mybatis-plus-spring-boot3-starter8080端口被占用其它java进程占用换端口或kill占用进程npm install报权限错误全局安装目录权限不足使用nvm管理Node避免用sudo装全局包MySQL连接超时时区参数缺失url追加serverTimezoneAsia/Shanghai前端打包后刷新404Vue history模式未配Nginx配置try_files $uri $uri/ /index.htmlECharts图表不显示容器没有高度chart容器必须显式设置width和heightVite启动端口被占用5173被占配置server.port或自动递增每个问题我都实际碰到过其中依赖坐标和时区这两个最隐蔽因为它们报错的信息不直接而是以奇怪的运行时异常出现排查周期很长。所以我把这些问题整理成速查表你们遇到类似现象时可以优先对照查看。7. 做完这套系统后的一些个人体会这套超市库存管理系统从数据库设计到最后部署上线我前后花了两周多时间。刚开始我也以为就是普通的管理系统真正动手才发现库存相关功能的细节多到超出想象并发扣减、流水对账、盘点差异、单据状态流转、预警逻辑……任何一个点都能延伸出值得展开讲的内容。我现在回头看最庆幸的是当初把库存流水设计成了不可修改的追加模式否则真到了数据对不上账那天查错成本会灾难性。如果你们也打算参考这套源码做毕设或者改造上线我建议优先扩展这几个方向多仓库支持、商品批次与保质期管理、移动端扫码枪或小程序盘点入口、以及更细粒度的权限控制。后面的小程序端和APP端都会用到现有接口后端基本不用大改只要扩充业务类型即可。这套系统里我最想强调的编程习惯是“先收口、再扩展”库存扣减只留一个方法入口流水写入和库存变更永远在同一事务里单据审核是唯一的业务闸口。把这些底层规矩定好上面的功能再怎么加都不会乱。毕业设计不只是做一个能演示的页面好的过程管理和工程设计习惯才是这套源码能带给你的最大价值。