
做课程设计或毕业设计时,最怕的就是选了一个自己都说不清原理的题目。你这个基于SpringBootVueuniapp微信小程序仓储进销存管理系统,光看标题就是一套典型的前后端分离 移动端三端架构。我前前后后带过不少学生做类似题目,自己也用这套组合重构过几个实际项目,今天这篇就把整个系统的拆解思路、核心设计、实现细节和踩坑点一次性讲透,供你参考和复现。这套系统能做什么?简单说:后台用SpringBoot提供接口,Vue写一个给管理员/仓管员用的PC端管理后台,uniapp编译成微信小程序,给仓库现场人员和老板在手机上做扫码入库、库存查询、单据审批。业务上覆盖了商品管理、供应商与客户管理、采购入库、销售出库、退换货、库存流水、库存预警、数据看板这些仓储进销存的标准功能。适合用来做课程设计、毕业设计,也适合作为你学习前后端分离项目的第一份完整工程模板。1. 为什么是这个技术栈:三端分离先解决分工问题先别急着写代码,你要先想明白:这套系统为什么要拆成三个端,而不是一个单体应用全搞定?这个逻辑想通了,写出来的代码结构和答辩时说的话,才站得住脚。1.1 三端架构的职责边界仓储进销存系统跟普通博客、商城不一样,它的使用场景是办公区 仓库现场并存的。仓库管理员不可能随时坐在电脑前操作,收货、发货、盘点都在货架旁边发生。所以要有一个移动端,让现场人员掏出手机就能操作。这是uniapp/微信小程序存在的理由。PC端管理后台是给老板、采购、财务看的,他们要处理复杂的单据、查看报表、维护基础资料。SpringBoot后端则统一负责数据的校验、库存计算、权限控制。三端分离之后,每一端的职责都很纯粹:SpringBoot:只写接口,不关心页面长什么样Vue后台:只做管理操作,不处理移动端适配uniapp小程序:只做移动场景下的高频操作,不需要承载复杂报表这个分工不只是技术选型,也是进销存业务天然的岗位分工。答辩的时候如果老师问为什么用三端,把这个场景讲清楚,比背一百句技术名词都有说服力。1.2 技术选型为什么是它们SpringBoot不用多说,Java生态做课设最稳的选择,社区资料多、配置简单、出问题容易搜到答案。搭配MyBatis-Plus能省掉大量重复的CRUD代码,单表操作几乎不用写SQL,这对于课设时间紧的情况非常友好。Vue这边建议直接上Vue 3 Element Plus Vite。Vite启动快,Element Plus的表格、表单、对话框组件改一改就能做出一个像样的管理后台。如果你对Vue 3还不太熟,用Vue 2 Element UI也一样能实现,只是新项目我建议直接用Vue 3,插件生态已经很成熟了。uniapp前端用Vue语法写一遍,编译到微信小程序。要注意一点:uniapp不是所有Vue语法都能100%平移,比如自定义指令、部分DOM操作在微信小程序里是受限的。好在仓储管理系统的页面都比较简单,列表、表单、扫码、详情,这些uniapp支持得很好。关于数据库,MySQL是标准答案。如果真的想让成绩好看一点,可以在Redis里做缓存和分布式锁,但这个属于加分项,不是必须的。1.3 项目目录结构先规划好三端分离不意味着三个git仓库,课程设计项目放在一个仓库里管理更省事。我建议这样的顶层结构:warehouse-system/ ├── server/ # SpringBoot后端 │ ├── src/main/java/com/xxx/warehouse/ │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务逻辑 │ │ ├── mapper/ # MyBatis-Plus的Mapper │ │ ├── entity/ # 实体类 │ │ ├── dto/ # 请求参数封装 │ │ └── vo/ # 响应数据封装 │ └── src/main/resources/ ├── admin-web/ # Vue管理后台 └── app/ # uniapp小程序端这种结构的优势在于:后端代码按照controller/service/mapper分层,答辩时能被老师一眼看出你理解分层架构;前端两个端互不干扰,依赖版本不会冲突。我自己做项目也习惯这么拆,后期维护的时候想改小程序不影响后台,想动后台也不怕碰坏小程序。2. 进销销存业务建模:先把单据流和库存流理清楚很多同学动手就建表,做完才发现库存对不上、数据一团乱。根子在于没把进销存的业务本质想明白。仓库管理表面上管的是货,实际上管的是单据和流水。2.1 两类核心数据:基础资料和动态单据基础资料包括商品、供应商、客户、仓库、用户这些不怎么变的数据。动态单据就是采购入库单、销售出库单、退货单、盘点单。单据又分主表和明细表,比如一张采购入库单,主表记录单号、供应商、入库仓库、经办人、入库时间、总金额,明细表记录这个单里包含哪几个商品、每个商品进多少数量、单价是多少。这个主表明细结构是进销存系统的命根子,一定不能省。有的同学为了偷懒,只建一张表把商品id和数量用逗号拼起来存,当时觉得方便,联调的时候就知道难受了——改一个明细查不到、导出报表对不上、统计的时候还得拆分字符串,全是坑。2.2 核心表结构参考下面这套表结构是我在多个项目里用过的精简版,覆盖了进销存的核心链路,又不至于让你写上百张表无法收场。表名用途关键字段goods商品表id, name, category_id, spec, unit, stock, safety_stock, purchase_price, sale_price, statuscategory商品分类id, name, parent_idsupplier供应商表id, name, contact, phone, address, statuscustomer客户表id, name, contact, phone, address, statususer系统用户表id, username, password, nickname, role, statuspurchase_order采购单主表id, order_no, supplier_id, warehouse_id, total_amount, status, operator_id, remarkpurchase_order_item采购单明细id, order_id, goods_id, quantity, price, amountsales_order销售单主表id, order_no, customer_id, warehouse_id, total_amount, status, operator_id, remarksales_order_item销售单明细id, order_id, goods_id, quantity, price, amountstock_log库存流水表id, goods_id, type, quantity, before_stock, after_stock, relation_no, create_time这套表看着不多,但已经能跑通采购入库 → 库存增加 → 销售出库 → 库存减少 → 库存流水可追溯的完整闭环。2.3 库存字段存哪里:冗余字段和流水表的关系goods表里的stock字段是一个冗余的当前库存数,目的是让查询列表时不用实时汇总流水表那么大计算量。但冗余字段必须通过stock_log流水来维护。也就是说,每次库存变动,先写一条stock_log,再更新goods.stock。两条操作必须在同一个事务里。这个设计理念是你答辩时最值得展开讲的点之一。可以这样表达:goods.stock是结果,stock_log是过程,过程决定结果。一旦库存对不上,查流水就能知道哪个环节出了问题。3. 后端核心实现:库存变动必须锁死在一个事务入口里后端的开发顺序建议先做基础CRUD,再做单据流转和库存变动,最后做统计报表。整个过程里最考验工程能力的就是库存变动这块,值得单独拎出来详细讲。3.1 统一扣库存接口的设计销售出库、采购退货、盘点亏损,这三类业务都会导致库存减少。如果每个业务都自己写一段扣库存的代码,后期一旦要加一个库存不足不允许出库的规则,你得改三个地方。正确做法是抽一个库存服务,所有入库、出库都走这一个入口。核心方法大致长这样:Transactional(rollbackFor Exception.class) public void changeStock(StockChangeDTO dto) { // 1. 查询商品并加锁,防止并发修改 Goods goods goodsMapper.selectByIdForUpdate(dto.getGoodsId()); if (goods null) { throw new BizException(商品不存在); } // 2. 计算变动后的库存 int beforeStock goods.getStock(); int afterStock beforeStock dto.getQuantity(); // 3. 出库场景不允许负库存 if (dto.getQuantity() 0 afterStock 0) { throw new BizException(库存不足,当前库存: beforeStock); } // 4. 更新冗余库存字段 goods.setStock(afterStock); goodsMapper.updateById(goods); // 5. 写库存流水 StockLog log new StockLog(); log.setGoodsId(dto.getGoodsId()); log.setType(dto.getType()); log.setQuantity(dto.getQuantity()); log.setBeforeStock(beforeStock); log.setAfterStock(afterStock); log.setRelationNo(dto.getRelationNo()); stockLogMapper.insert(log); }这段代码里有几个细节值得留意:selectByIdForUpdate是行级锁,在这个事务没提交之前,其他事务对同一条商品记录的更新会阻塞。对于课设场景,行锁已经足够,没必要用Redis分布式锁增加复杂度。事务注解必须加,而且要把异常抛出到事务边界,不然事务不会回滚。每个单据的提交、取消、删除,都要重新计算库存,不能直接给一个固定的增减值。3.2 采购入库和销售出库的完整流程以采购入库为例,普通的CRUD思路是建单 → 手动加库存,这个流程在演示时容易被老师追着问:如果单据还没审核,就先把库存加了,一旦单据作废怎么办?所以入库单的流程建议设计成:创建采购单(状态:待审核)审核通过(状态:已审核,此时才触发库存变动)如果要作废已审核的单据,需要先做反审核,同时扣减或返还库存对应的流程状态机是:待审核 → 已审核 → 已作废。每张单据的主表增加一个status字段,审核时不仅改状态,还要遍历明细表逐条调用changeStock方法。出库单反着来:创建时校验每个商品的库存是否足够,审核通过时扣减库存。销售退货单又反着来,审核通过时加库存。全部复用同一个changeStock方法,参数里的quantity正负不同而已。3.3 金额计算和数据精度金额统一用BigDecimal,不要用double。double在电商支付场景下会出现0.1 0.2不等于0.3的问题,做账更是大忌。数据库里金额字段用DECIMAL(10,2),Java代码用BigDecimal,JSON传输时需要注意序列化格式,前端展示时保留两位小数。明细表的金额计算,建议在后端算,不要把价格和数量传给前端让前端算完再传回来。前端算得再快,也没有后端算得可信。4. Vue管理后台:页面编排、权限与数据看板管理后台是展示工作量最多的地方,也是课设里视觉上最出效果的部分。Vue端不用做太花哨,把核心页面做扎实、字段做完整就行。4.1 后台的页面结构我的建议是侧边栏由路由配置生成,不要每个菜单都手写一遍,后续加页面只改路由。核心页面如下:数据看板:统计今日入库量、今日出库量、库存总量、预警商品数,用图表展示近7天的出入库趋势商品管理:商品列表(分页、模糊搜索、库存预警颜色标记)、新增/编辑商品弹窗分类管理:树形分类,支持两级就行供应商管理:列表 新增/编辑/停用客户管理:同上采购入库单:单据列表 新建单据(明细行动态增删)销售出库单:同上库存流水:按商品、时间、类型筛选,支持导出Excel用户管理:系统用户列表、重置密码、分配角色4.2 动态明细行的实现单据页面是最能体现进销存系统专业感的地方。新建采购入库单时,表格每一行包含商品选择器、数量、单价、金额。商品选择器建议用远程搜索的下拉框,搜关键字加载商品列表,选中后自动带出单价和库存。明细行里有个交互细节:数量或单价变化时,当前行金额自动重算;头部显示合计金额;提交时校验每一行商品不能为空、数量必须大于0。Element Plus的el-table操作列加删除按钮,控制最小保留一行。4.3 权限控制别做得太复杂很多同学一上来就建五张权限表,然后被权限逻辑困了一个星期。课程设计的核心是把业务跑通,权限做到角色级别就够了。user表加一个role字段,后台登录后根据角色控制侧边栏菜单显示。比如:admin:所有菜单operator:商品管理、单据管理、库存查询,不显示用户管理和报表路由层面用vue-router的meta字段标记需要的角色,配合router.beforeEach做拦截。按钮级别的权限,比如只有admin才能作废单据,可以做,但如果时间不够,不做一个字段级别的操作按钮也说得过去。4.4 数据看板让项目立刻值钱看板页面是答辩时的第一视觉印象。千万不要让老师打开系统看到一张空荡荡的首页。建议在首页放四个统计卡片,下面放一个折线趋势图。用ECharts的折线图展示最近7天入库和出库的数量走势,数据从后端统计接口取。另外,系统初始化时必须准备一批真实的演示数据。商品至少二三十条,库存流水二三百条,这样看板才有曲线、列表才有翻页效果。很多同学测试时只加三五条数据,演示时页面显得冷清,印象分一下就掉了。5. uniapp小程序端:登录、扫码与移动审批的落地细节小程序端是整个项目的亮点,也是最容易在联调阶段翻车的部分。uniapp的核心价值是一套代码,多端运行,但目标端是微信小程序时,还是有一些专属细节要注意。5.1 微信登录的完整流程小程序端不能走传统的账号密码登录,要用微信的code2session流程:用户点授权按钮 → wx.login拿到code前端把code传给后端 → 后端调用微信接口换取openid后端根据openid找到或创建本地用户 → 签发自定义token返回前端前端把token存到uni.setStorageSync,之后所有请求都带token这里有个容易踩的坑:如果你用uni.login代替wx.login,在H5端调试时也能拿到code,但换不到openid,因为H5没有微信小程序的appid环境。所以调试要在微信开发者工具里运行,不要用浏览器预览来测登录。登录之后建议做一个简单的授权页面,让用户选择仓库管理员或审批员身份,或者由admin在后台给小程序用户绑定角色。更简单粗暴的做法是首次登录自动创建一个仓管员账号,需要更高权限的操作在后台管理。对课设来说,能说明清楚整个登录链路是加分项。5.2 扫码录入的实现小程序端的核心操作是扫码入库和扫码查询。uniapp里调用uni.scanCode可以调起微信扫一扫。扫到的结果可能是商品条码( EAN-13等),也可能是你自己生成的二维码。商品表里建议加一个bar_code字段,专门存条码。扫码后拿着这个值去后端查商品,查到就把这条商品加到当前入库单里,查不到就提示未找到该商品,请先到后台维护。需要注意:微信开发者工具里的扫一扫是模拟的,扫码接口不支持真机测试。代码写完之后,要么用工具里的模拟扫码功能选一张带条码的图片,要么直接真机预览测试。我见过好几个学生代码写得没问题,但又没装微信开发者工具的扫码插件,以为程序坏了,实际是环境问题。5.3 移动端页面别做太多小程序的页面建议精简,只做仓库现场真正高频的操作:首页:库存概况 待办审批数量 快捷入口入库扫码:扫码后展示商品信息,输入数量直接提交入库单库存查询:搜索商品名称/条码,展示库存量、预警状态单据审批:待审核的采购/销售单列表,点进去看明细,通过或驳回程序端不要去做复杂报表,手机上展示趋势图意义不大,反而浪费开发时间。把移动端做加减法这个观点在文档和答辩中明确写出来,体现产品思维,这是加分点。5.4 uniapp请求封装的细节uniapp的request和小程序原生的wx.request一样,都需要配置code 401的处理。建议封装一个request.js:const BASE_URL http://你的后端地址:8080 export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: uni.getStorageSync(token) || , Content-Type: application/json }, success: (res) { if (res.data.code 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject(res.data) } else { resolve(res.data) } }, fail: (err) reject(err) }) }) }这段代码里的BASE_URL是个关键变量,后面联调踩坑还会遇到。6. 联调阶段最容易翻车的几个点与完整排查链路前后端写完,分开跑都能通,一联调就各种连不上、报错、数据对不上。这是三端项目最常见的阶段。我把几个高频问题和排查方法整理成一份清单,你对照着查就行。6.1 小程序连不上后端接口现象:微信开发者工具里请求一直在转圈,或直接报ERR_CONNECTION_REFUSED。这个坑90%的原因是开发者工具默认开启了不校验合法域名选项(开发环境需要勾选),但即使勾了,localhost依然是没用的——小程序的代码运行在微信的渲染层,本机回环地址在小程序运行时往往被拦截。正确做法:后端启动时监听0.0.0.0,不要只监听127.0.0.1。小程序请求地址写成你电脑的局域网IP,比如http://192.168.1.101:8080,不是localhost。微信开发者工具:详情 → 本地设置 → 勾选不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书。如果改了还连不上,打开电脑的防火墙,把8080端口放行一次。很多学生的后端启动没问题,就是被Windows防火墙拦了。排查的时候可以先在电脑的浏览器里访问http://局域网IP:8080接口,本地浏览器能通、小程序不通,再考虑是不是小程序那边的问题。6.2 后端接口能通,但uniapp端登录失败现象:后端日志里能看到请求进来了,但返回的code永远报错,或者前端拿不到data。先看后端返回的JSON结构。SpringBoot默认返回的字段名是驼峰命名,比如userName,小程序端如果写成了user_name,取到的就是undefined。建议在SpringBoot里配置全局的命名策略,让JSON字段统一为下划线风格,或者干脆后端实体类加JsonProperty显式指定。前后端约定一个统一响应体:public class ResultT { private Integer code; private String message; private T data; }所有接口返回Result对象。code200代表成功,401代表未授权,其他都是业务异常。前端统一处理,就不会每个页面都写一遍错误判断了。6.3 并发扣库存数据不对现象:两个小程序用户同时提交出库单,查出来的库存是对的,扣完后库存变成负数。原因是读取库存和更新库存之间没有加锁。解决办法有两个层次:第一层,在SQL层面做条件更新,让数据库来判断库存是否够:UPDATE goods SET stock stock - #{quantity} WHERE id #{goodsId} AND stock #{quantity}执行后判断影响行数,如果为0说明库存不足,抛异常并回滚事务。第二层,使用前面提到的selectByIdForUpdate行锁,在事务内部先锁住商品记录再操作。对于课程设计,两种方法都可以,推荐把两种都写进文档,然后说明因为性能考虑选择了第一种。这个问题的讨论能体现出你对并发问题的理解深度。6.4 数据库大小写、时间格式的坑后端配置里注意一个常见问题:MySQL表名和字段名,在Linux环境下是区分大小写的。本地Windows开发时没事,部署到Linux的服务器上突然报Table doesnt exist。建议建表时统一用小写加下划线,SpringBoot配置里加上:spring.jpa.hibernate.naming.physical-strategyorg.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl如果你是mybatis-plus,保持表名与实体类名映射时注意大小写策略。时间格式也要全局统一。后端返回LocalDateTime,默认序列化格式是yyyy-MM-ddTHH:mm:ss,前端展示很丑。配置一个Jackson的全局格式:Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - { builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; }存到数据库里的时间格式、接口返回给前端的时间格式保持一致,这是进销存这种重单据系统特别加印象分的地方,因为单据的时间常用于对账和排序。6.5 排查链路方法论联调问题,最怕的是东改一改西改一改,最后都不知道是哪一步解决的。我的习惯是严格按下面这个链路来:先确认后端接口本身有没有通:用Postman或直接在浏览器访问接口路径,带参数测试。接口不通,问题一定在后端,别去折腾前端。确认前端请求有没有真的发出去:在浏览器开发者工具的Network面板或微信开发者工具的Network面板,看请求的URL、请求头、请求体。URL不对就改BASE_URL,请求体格式不对就检查JSON序列化。看后端日志:Controller有没有收到请求、Service有没有抛异常、SQL有没有执行成功。SpringBoot的控制台日志是排查问题最快的路径。检查数据库:数据有没有写进去?字段有没有截断?数据状态对不对?每一步都不跳,90%的问题能在前三步定位。见到一个报错不要急着百度,先看它发生在哪个环节,这个习惯比多做十个功能都值钱。7. 课程设计与毕业设计里容易漏掉的加分细节功能都做完了,系统能跑了,接下来要做的就是让它像一份能打85分的作业。下面是几个容易被忽略但非常关键的点。7.1 数据库设计文档要画ER图和流程图很多同学交了源码和运行视频就完事了,文档里只有几张截图。老师看一个进销存系统,最想看的其实是ER图和业务流程图。ER图能用工具画,同样,采购入库、销售出库的泳道图或者状态图也要画出来,哪怕手画拍照都比纯文字强。画这些图不只是为了应付文档,画的过程就是梳理业务的过程。把你的表结构、状态机画完,代码里的逻辑会清晰很多。7.2 演示数据必须精心准备打开系统时是空空如也还是数据丰富,观感差别极大。我建议做一套完整的初始化SQL,里面包含:20个以上的商品,覆盖多个分类10个供应商、10个客户近30天的出入库流水,让看板图表有曲线几个处于待审核状态的单据,方便演示审批流程演示时特意点开一个待审核的采购单,点一遍审核,然后去库存列表里看库存数量发生变化,再去流水表看多了一条记录。这一连串操作,能把系统的核心闭环清楚地展示给老师看,比讲十页PPT都有说服力。7.3 源码里注释和命名规范代码是给老师看的第二张脸。类名、方法名、变量名不要用汉语拼音缩写,如cks、sprk这种,尽量用英文单词。关键业务逻辑写注释,比如审核采购单并增加库存这种注释,老师扫一眼就能知道你的设计思路。方法内部的代码,如果能保持Service层在100行以内,可读性就会大幅提升。过于庞大的Service方法也是坏味道,要把相同逻辑抽出来复用,特别是重复的Redis操作、重复的校验,都要抽出公共方法。7.4 答辩演示的顺序演示的时候,严格按基础资料 → 采购入库 → 库存查询 → 销售出库 → 流水追踪 → 统计报表的顺序去操作。这一个顺序是完整的业务流程,老师顺着你的节奏看下去,很容易理解;如果你一会儿点报表、一会儿点用户管理,演示就会显得杂乱。小程序端演示建议放在后台演示之后,形成后台下单、小程序端审核/查看的呼应。演示前先准备好数据,避免现场才录商品,手忙脚乱。7.5 部署时的注意事项如果只是交课设,本地运行演示就够了。要部署到服务器,记得把后端的数据库连接配置从yml里抽出来放到环境变量里,不要硬编码。服务器上的MySQL版本、时区、字符集都要提前确认,最经典的坑是时区不对,存进去的时间差了8个小时,做报表的时候全对不上。小程序要真机预览或发布体验版,后端接口必须有HTTPS域名,本地联调用IP没问题,但正式体验版必须配置合法域名,这个需要提前准备。只做课设的话,开发者工具里勾选不校验域名就够了。最后再分享几个实操过程中的体会这套系统我陆陆续续重构过三版,最大的感悟是:进销存系统最核心的不是页面好不好看,而是库存数据能不能做到每一笔变动都有据可查。你写代码的时候,心里要时刻有一根弦——从采购单创建,到审核通过,再到库存增加,最后在流水表里能看到记录,这整条链路是闭环的。任何一环断了,系统在真实场景里都是没法用的。给你一个建议:开发的时候先把采购入库 → 看库存变化 → 看流水这个小闭环跑通,再往外面扩展销售、退货、盘点。只要这个闭环通了,系统的骨架就是稳的,剩下的都是往骨架上面添肉。另外,做课程设计最容易犯的毛病是追求功能多,结果每个功能都是半吊子。仓储管理系统,比起做了十个不完善的页面,不如把商品管理、采购入库、销售出库、库存流水、数据看板这五个页面做到无懈可击。功能少一点没关系,核心流程全通、代码结构清晰,分数反而更高。如果最后你想把系统进一步打磨,可以考虑加一个库存预警的定时任务,每天扫描低于安全库存的商品,通过小程序模板消息推送给管理员。这个功能单独拿出来,技术含量不高,但主动预警带来的产品价值非常明显,在答辩时是一个很加分的差异化亮点。