做轴承贸易这块的进销存,最麻烦的不是写代码,而是理清楚业务到底怎么走。我接手某公司这个进销存系统的时候,库管、销售、财务三套Excel各记各的账,轴承规格又多又杂,一个型号有国产有进口,按套卖还是按公斤卖都不一样,月底对账全靠吼。这次我把整套系统从零搭起来,用SpringBoot2做后端、Vue3做前端、MyBatis-Plus操作MySQL8.0,顺便把需求梳理、表结构设计、权限控制和那些踩过的坑全部记录下来,给准备做类似管理系统的朋友一个参考。
1. 项目定位与整体技术选型
1.1 进销存系统到底要解决什么问题
很多刚接触这类项目的朋友容易把进销存想简单了,以为无非就是入库、出库、查库存三个页面。真去现场待两天就知道,业务远比这复杂。以轴承行业为例,同一个型号的货,可能同时存在几个供应商渠道,进价不一样,批次不一样,质保周期也不一样;客户下单的时候,可能还要求指定品牌、指定产地,甚至指定要某个批次段的产品。仓库发货的时候要能追溯到具体是哪个厂家、哪天进的货、当时的采购单价是多少。这些需求会直接影响数据库设计——不是说记个流水账就完事了。
这套系统上线时确定的边界很清晰:采购管理、销售管理、库存管理、往来账务、基础资料、系统权限六大模块,外加一个灵活的数据看板。采购管供应商和进货,销售管客商和出货,库存管批次和调拨,往来账管应收应付,基础资料管客户、供应商、商品、仓库,权限系统管不同角色能看什么页面、能操作哪些按钮。每一条数据都要求留痕,谁经手的、什么时候录入的、改了什么都查得到。
1.2 为什么选SpringBoot2+Vue3这套组合
选型的时候考虑过三个方案:单体JSP老项目、前后端分离的SpringBoot2+Vue3、以及微服务架构。实话说,这种体量的企业内部系统,微服务完全属于过度设计,一个团队两三个人维护,搞一堆服务拆分、注册中心、配置中心,光运维成本就压死人。单体JSP面临的问题是前后端代码耦合太严重,改个页面样式都可能碰后端逻辑,后面想扩展接口也很被动。
最终敲定SpringBoot2.7 + Vue3 + MyBatis-Plus + MySQL8.0这套组合,原因很直接:SpringBoot2是当前生产环境最稳的版本,3.x虽然新但部分第三方库兼容性还要踩坑,没必要冒险;MyBatis-Plus的单表CRUD和分页插件能省掉大量重复的MapperSQL,复杂报表再手写SQL兜底;Vue3的Composition API比Options API更适合中后台这种大量表单和列表页面的场景,代码复用抽hook很方便。MySQL8.0选得没什么悬念,窗口函数做统计报表比5.7的临时表写法简洁太多。
这套项目适合准备做毕业设计或者正在接企业内部管理系统的开发者参考,特别是后端用SpringBoot、前端用Vue3、需要一套完整代码结构的场景,可以直接把骨架搬走改造。
2. 数据结构设计与核心表关系
2.1 商品信息与规格维度的拆分
轴承商品的信息有一个容易被新手坑到的点:一个轴承型号,往往对应多个品牌、多个产地、多种包装规格,价格还不一样。比如同一个深沟球轴承,SKF的和国产普通品牌的进价差好几倍,客户需求完全是两个方向。如果把所有信息塞进一张商品表,字段冗余不说,后续做批次核算会非常痛苦。
我当时的设计思路是把商品拆成三个层次:商品主表、商品规格SKU表、批次库存表。商品主表存的是商品族维度的信息,比如名称、型号分类、计量单位(套/公斤/个);SKU表在商品下再细分品牌、产地、包装,相当于具体到"哪个牌子、哪个产地、什么包装方式";批次库存表则记录每次进货的批次号、采购单价、入库数量、剩余数量。这样销售开单选SKU,仓库发货扫批次,财务对账按批次核算采购成本,三层各管各的,逻辑清晰不出乱子。
所谓计量单位的问题也需要提前处理。轴承行业内按套计算最多,但也有按公斤报价的谈判方式,尤其大宗原材料采购时客户喜欢按重量估算总价。我的方案是SKU表中设主计量单位和辅助计量单位,同时存换算比例,比如1套等于0.85公斤(实际重量按型号查),开单时用户输套数自动带出公斤数,两个数都写进单据明细,后面统计哪个维度都有数据支撑。
2.2 关键单据表的血缘关系设计
进销存的单据流转是典型的主子表结构,而且每一类单据都要保留来源去向的关联关系。采购入库单、销售出库单、库存盘点单、调拨单,这四类是核心。以采购为例,我建了采购订单和采购入库单两张表:采购订单是跟供应商谈好的计划,采购入库单是实际到货验收入库的记录。订单可以分批到货,所以一张订单对应多张入库单,每张入库单关联订单号,同时生成唯一的入库批次号。
销售端类似,销售订单转销售出库单,出库单明细按批次库存表减扣。这里必须做"批次溯源":出库单明细上记录批次号,一旦客户反馈质量问题,可以立即查到这个批次是哪个供应商、哪一天进的货、同批次还剩下多少、分别卖给了哪些客户。这种追溯能力在轴承这类工业品业务中属于刚需,不是锦上添花。
2.3 库存账与财务账的协同思路
很多进销存系统做到最后沦为流水账,就是因为库存和财务是割裂的。我在设计时要求每一笔业务单据自动生成对应的库存变动流水和往来账记录。采购入库生成库存增加流水,同时生成应付账款;销售出库生成库存减少流水,同时生成应收账款;收款、付款在往来账里单独处理并关联到对应单据,月底对账直接对收付款流水就行。
具体表结构上,库存流水表记录批次号、变动数量、变动前余额、变动后余额、关联单据编号,保证随时可以重算某个时间点的库存快照。往来账表记录客户供应商的应收应付余额,每笔收款冲抵对应销售单时会记录关联关系,避免"收了钱不知道冲哪张单"的糊涂账。这套设计对财务人员来说非常友好,月底三个数——库存总额、应收总额、应付总额——一键就能对平。
3. 后端核心模块与实现细节
3.1 通用基础架构搭建
后端我采用的是经典的分层架构:Controller层只做参数接收和结果封装,Service层写业务逻辑,Mapper层通过MyBatis-Plus的BaseMapper提供基础CRUD,复杂查询自写XML。统一的返回结果类设计成"code + message + data"的结构,code为0表示成功,非0表示业务异常;业务异常统一用全局异常处理器捕获并转化为友好提示,不会把堆栈信息直接甩给前端。
数据库连接池用的HikariCP,是SpringBoot默认的,就不要换了,稳定性足够好。Redis用来存登录Token和验证码,这个项目没做复杂的权限框架,而是用拦截器+自定义注解实现接口级权限控制:后端定义权限码,前端按钮根据权限码控制是否显示,访问接口时拦截器校验用户是否拥有对应权限码。对比Shiro或者Spring Security,这种方式对中后台系统来说更轻量,好理解也好维护。
MyBatis-Plus的几个特性用好了能省很多事。分页插件只要配置一个拦截器就能用,配合Page对象实现物理分页,数据量大也顶得住。字段自动填充可以把创建时间、创建人等公共字段设置成自动填充,不用每张表都手动set一遍。逻辑删除建议全局开启,虽然是软删,但对进销存这种数据需要审计的场景非常必要,误删可以恢复,查历史也好追溯。
3.2 采购入库与库存批次生成
采购入库的后端流程是:前端提交采购入库单——包含单头信息(供应商、仓库、入库日期、经办人)和明细列表(SKU、数量、采购单价、生产批次号)——后端事务内完成三件事:写入采购入库单主表和明细表、根据入库明细生成批次库存记录、写库存变动流水。
这里有个容易忽略的细节:入库操作发生在某个仓库,入库的可用数量要到对应仓库的库存中。因此批次库存表的主键我是设计成"仓库ID+SKU ID+批次号"联合唯一,不好直接用自增ID做主键,否则同一个批次在不同仓库会产生歧义。查询某个SKU的可售库存时,要按期望仓库对剩余数量做汇总,同时过滤掉锁定数量,才能得到真实可售数。
事务边界也要把握好。上面三步必须在一个事务中完成,任何一个失败都全部回滚。如果不这么做,可能出现单子保存成功、库存没加上的脏数据。还一个容易踩的问题:并发重复提交。我用的是前端按钮防重加后端幂等校验双重保障——后端在保存前先按业务单号查一遍,已存在的直接返回"请勿重复提交",防止网络抖动导致用户多点了一次按钮。
3.3 销售出库与库存扣减
销售出库比入库多一个关键环节:库存校验。下单的时候必须校验库存是否足够,而且是按仓库、按SKU汇总扣减。当然,实际业务中存在先开单后发货的情况,所以还得区分预占库存和实发库存。
我的处理方式是在库存表增加一个锁定数量字段。销售订单审核通过时,根据订单明细锁定库存(锁定数量+,可用数量-);仓库实际发货出库时,扣除锁定数量,同时生成出库流水。如果订单取消,锁定数量回滚,可用数量恢复。这样做的好处是订单占用的库存不会被其他单子抢走,超卖问题从源头被切断。
锁定逻辑有一个细节值得单独说:锁定到哪个批次?因为客户可能指定品牌指定批次,所以锁定不能只锁SKU总量,要锁到具体的批号上。实现时按SKU查询正数批次列表,按先进先出原则逐个批次锁定,不够了再锁下一个批次。同时把"锁定批次、锁定数量"写到销售订单明细的冗余字段里,避免后期重复计算。
3.4 库存盘点与差异处理
盘点功能看起来简单——现场数一遍数量,跟系统对一对,把差额填平。实际上坑很多,最主要的一个是:盘点单录完差异直接改库存,那中间的差异去哪了?查不到账。
我的做法是让盘点变成一张独立单据:盘点单保存的是盘点时的账面数、实盘数、差异数。审核盘点单后,生成一张库存调整单,调整单按差异方向正数或负数调整库存,并注明"盘点调整"这个业务类型。这样库存流水里每一笔变动都能解释来路,月底看库存变动报表也说得清楚哪部分是盘盈、哪部分是盘亏。
盘点也碰到过并发问题:仓库在盘点期间同时发生出入库,账面对不上。业界通用的做法是分仓库盘点,盘点某个仓库时,先锁定这个仓库的出库入库审核操作,或者记录盘点期间业务流水再事后冲减。我们这个系统因为量级不大,采用了简单方案:盘点单创建时生成该仓库的账面快照,盘点期间发生的业务变动单独记录,审核时用快照加期间变动数跟实盘数做对比,差异才是真实盈亏。这个逻辑跟客户解释了好几遍才被接受,但用下来确实靠谱。
4. 前端页面落地与交互方案
4.1 Vue3项目结构与状态管理
前端我用的是Vue3 + Vite + TypeScript + Pinia的组合,UI框架选的Element Plus。TypeScript在这个项目里带来的好处非常明显:大量单据明细的数据结构复杂,比如采购入库明细要同时携带商品SKU信息、数量、单价、批次号,有类型定义后写起来不容易出错,接口返回的数据也有了约束。如果你的团队不熟悉TS,建议还是要用,进销存这种数据密集型系统,很值得付出学习成本。
项目的页面组织按业务模块分目录,common放公共组件,views下面每个业务子目录一套页面。状态管理用Pinia,存三样东西:用户信息与权限码、系统字典数据、应用配置(比如折叠侧边栏状态)。服务器返回的字典项在登录时一次性拉取缓存到本地,像计量单位列表、单据状态、仓库列表这些,页面用到时直接取用,不用每次调接口。
路由权限这块,我采用的方案是后端返回用户可访问的路由列表,前端动态添加路由。这样用户没权限的模块连地址都访问不了,而不是进了页面再被接口拦截。两种方案都验证过,动态路由在体验上更好一点,毕竟菜单栏上压根不显示无权限的菜单。
4.2 单据页面的交互设计
进销存页面交互上边,最复杂的是开单页。以采购入库单为例,页面顶部是单头信息区(供应商下拉、仓库下拉、入库日期选择器、经办人、备注),中部是明细表格区——可以通过搜索弹窗把要入库的商品勾选进来,也可以是条形码扫描后自动追加一行。每行要录入数量、采购单价,自动计算行金额和整单金额。底部还有合计信息和一个大按钮区:保存草稿、提交审核、打印。
这里我特意做了草稿机制。仓库录入员在录入途中可能被叫走去处理其他事,如果数据没保存就全丢了,体验非常差。所以单据设计成多状态:草稿、待审核、已审核、已驳回。草稿可以继续编辑,提交审核后编辑按钮消失,审核通过后生成正式的业务流水。审核这个动作放在列表页操作列里,也可以做成批量审核,仓库量大的时候特别实用。
明细表格的编辑体验我下了不少功夫。Element Plus的表格组件默认没有单元格编辑功能,我封装了个子组件,在表格内动态渲染输入框,配合校验规则在失焦时校验。键盘上下键切换单元格是另一个加分项,熟练的录入员完全不用碰鼠标,效率提升非常明显。这个细节虽然开发时费了点功夫,但客户满意度很高。
4.3 看板与报表的可视化方案
管理看板我用的是ECharts,图表没有直接用封装好的大屏组件,而是自己拼的。布局是一行三列:左边放采购金额趋势折线图,中间放销售金额柱状图,右边放库存周转率仪表盘;下方再一张表格展示当月销售Top10商品。数据来源是一个聚合接口,一次查询返回多个统计结果,避免前端多次请求造成数据库压力。
报表页面这边,我封装了一个通用查询组件:左侧筛选区(日期范围、仓库、业务类型、关键字),右侧明细表格,支持自定义列显示、排序、导出Excel。导出功能刚开始是用前端表格直接导出,销售量一大内存直接爆掉。后来改成后端异步导出:前端提交查询条件和导出请求,后端用EasyExcel查一批写一批生成文件,生成完传文件地址给前端下载。数据量几十万行也能稳定跑完。
日期筛选这里有个实用技巧:数据库存储的日期时间字段,前端传的是字符串范围,后端直接用create_time >= start && create_time < end(end补上23:59:59)处理就不会丢掉当天数据。这个坑出过一次,某天晚上23点后的入库单日期范围查询查不出来,排查了半天才发现是边界条件写死成<= end,Mind the off-by-one.
5. 踩坑实录与排查技巧
5.1 MySQL8.0时间字段与JSON字段的坑
MySQL8.0相比5.7有几个使用上的变化,第一个就是datetime默认值不能直接写CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP作为两个字段的组合。在5.7里可以一张表多个timestamp字段带自动更新,8.0的版本对默认值表达式限制更严格,更推荐的做法是时间字段交给MyBatis-Plus自动填充,插入和更新时Java代码统一赋值。
JSON字段是8.0的亮点功能,存储业务扩展属性确实方便。但要注意:不要在JSON字段上做关联查询和频繁的条件过滤,更不要在上面建索引,性能会很难看。我的使用范围控制在"存一些不参与核心逻辑的配置、扩展属性",比如单据的打印模板配置、商品图片地址列表。需要参与统计计算的数据宁可拆出来建独立字段。
另外,8.0的默认字符集是utf8mb4,比5.7的utf8多了emoji支持,但要注意建表时统一指定COLLATE为utf8mb4_general_ci还是utf8mb4_0900_ai_ci。这个选择会影响排序和比较行为,尤其是后期如果要按拼音排序查询客户名称,两边行为不一致会查出不同结果。我统一用了utf8mb4_general_ci,稳定保守。
5.2 常见问题排查记录
开发过程中积累了不少问题,挑几个典型的记录在这里,供遇到类似情况的朋友参考。
第一个是MyBatis-Plus分页查不出数据。第一次用分页插件时,明明配置了PaginationInnerInterceptor,但分页查询返回total一直是0。后来查文档才知道,MyBatis-Plus 3.4版本后分页插件需要new一个MybatisPlusInterceptor,再addInnerInterceptor,很多老教程的方法是过时的,配置方式变了。这个坑很隐蔽,因为不报错,只是数据不对。
第二个是金额计算的精度问题。钱这个东西,用double就是找死。数据库字段用DECIMAL(10,2),Java实体对应BigDecimal,所有涉及金额加减乘除的地方必须用BigDecimal的add、subtract、multiply、divide方法,禁止用原生运算符。尤其注意除法要指定精度和舍入模式,不指定会报ArithmeticException。
第三个是并发扣库存导致超卖。我们用"先更新后查询"的方式解决——更新语句的where条件带"剩余数量 >= 要扣减的数量",受影响行数为0说明库存不够,整个事务回滚。这种方式比先查询再判断要安全,不需要额外加锁,高并发下也扛得住。
第四个是前端日期传参格式问题。默认的日期组件返回的是标准日期格式带T的情况,后端LocalDateTime解析时报错。统一在前端把日期格式化为yyyy-MM-dd HH:mm:ss字符串再提交,后端用@DateTimeFormat注解或者配置全局Jackson序列化格式。注意不要两种处理方式混用,会打出非常奇怪的错误。
5.3 性能优化经验
进销存系统最怕的是月底统计报表跑太久,甚至挂死。优化从三个维度同时做:SQL层面、索引层面、缓存层面。
SQL层面最核心的一点:不要在Java内存里做关联和过滤,尽量下推到SQL。比如统计某个客户某个月的采购总额,一条group by SQL搞定,别先把明细全查出来再在内存里累加。MyBatis-Plus的Wrapper查询子句,能用的条件全部用上,减少回表。
索引层面,我在使用频率高的查询字段都建了组合索引。这里有个经验:索引不是越多越好,每一张索引都会拖慢插入和更新速度。对于进销存这种以单据为中心的系统,索引主要加在这几个方向上:单据表按状态+业务日期建索引,明细表按单据ID建索引,库存表按仓库+SKU ID建索引,流水表按业务类型+业务日期建索引。已经够用了。
缓存层面,变化不频繁的字典数据用Redis缓存,比如商品分类列表、仓库列表。登录用户的信息缓存也用Redis,Token过期时间设为2小时,用户无操作自动失效。业务数据不要缓存——单据数据时效性太强,硬缓存反而容易出问题。
6. 文档沉淀与部署注意事项
6.1 从开发到交付的完整闭环
代码写完后,项目交付不只是编译打包给客户就完了。我的习惯是先做一轮完整的自测,把核心流程全部走一遍:建商品、建客户、建供应商、发起采购入库、做一张销售出库、盘点一次、看报表数据是否正确。自测没问题再让客户的关键用户试用,收集反馈后迭代。
在交付文档这块,我整理了一个交付文档包,包含:需求说明文档(系统有哪些功能模块)、数据库设计文档(核心字段、E-R关系、字典表说明)、部署运维手册(环境要求、数据库初始化SQL、配置文件修改指引、启动步骤)、以及用户操作手册(带截图的每个模块怎么用)。用户操作手册特别重要,直接决定客户能不能自己上手——不要觉得写文档麻烦,每一次客户咨询都节省双方时间。
部署采用的是单机部署方案:一台服务器,MySQL8.0数据库、后端jar包、前端打包后的dist目录用Nginx托管。前后端分离部署的好处是后端接口不直接暴露公网,Nginx配置反向代理到后端端口,静态资源和动态请求分开处理。前端打包时要注意Vite的base配置,如果部署在子路径下不加base会白屏,这个坑我踩过一次。
6.2 多环境配置与数据安全备份
SpringBoot的application.yml,我习惯拆成三个配置文件:application-dev.yml(本地开发连本地库)、application-test.yml(测试环境)、application-prod.yml(生产环境)。启动时用--spring.profiles.active=prod指定环境,避免本地测试把生产库干翻的惨案。
数据库备份是非常容易被忽略的一环。我配置了crontab定时任务,每天凌晨全量备份数据库。备份用mysqldump命令,保留最近7天的备份文件,超过的自动清理。不只是防删库跑路,日常误操作、业务数据错乱,都可以从备份恢复。配合binlog日志还能做到时间点恢复,可以说是进销存数据安全的最后生命线。
这里分享一个恢复演练的心得:备份脚本写完之后,一定找一台空机器恢复一次,验证备份文件真的能用。有些备份脚本平时跑得好好的,真到恢复时发现备份文件损坏或者缺少关键参数,那就等于没有备份。这个教训会让人非常刻骨铭心。
6.3 系统上线后的演进方向
系统上线交付之后,后续的迭代方向可以往这几个地方走。第一是移动端适配,业务员在外面跑客户,需要手机上看库存、代下单,我给前端页面做了一套简单的响应式适配,关键页面能用手机操作,不需要专门开发App。第二是多仓库支持,业务量上来之后,一个总仓会拆成多个分仓,需要在现有数据模型上强化仓库维度的独立性。第三是更灵活的报表自定义,业务人员希望自己拖拽字段生成报表,这块可以集成一些开源报表引擎。
我在实际落地中的体会是:一套进销存系统,技术难点从来不在代码本身,而是对业务规律的理解和把握。轴承行业有它的特殊性,消费品行业的进销存逻辑套过来未必合适。能把进销存做扎实,需要考虑的因素比单纯写CRUD多得多。
7. 核心源码结构清单
后端源码目录结构大致如下,方便参考搭项目时规划模块边界:
- common:通用工具类、常量定义、异常定义、全局响应处理
- config:框架配置类(MyBatisPlus分页配置、跨域配置、拦截器配置、Redis配置)
- controller:控制器层,按模块分包
- service:业务逻辑层
- mapper:数据访问层(MyBatis-Plus的Mapper接口)
- entity:数据库实体类
- dto:数据传输对象(入参出参封装)
- vo:视图对象(接口响应数据)
- utils:工具类(JWT工具、Excel导入导出工具等)
- quartz:定时任务包(库存预警检查、数据备份调度等)
前端目录结构:
- src/api:接口请求封装
- src/router:路由配置
- src/stores:Pinia状态管理
- src/views:页面组件
- src/components:公共组件
- src/utils:工具函数(请求拦截器、日期格式化等)
数据库主要表清单:系统用户表、角色表、权限表、菜单表、客户表、供应商表、仓库表、商品分类表、商品表、商品SKU表、采购订单表、采购入库单表及明细表、销售订单表、销售出库单表及明细表、批次库存表、库存流水表、盘点单表、库存调整单表、应收应付流水表、系统字典表。
这二十多张表构成了整个进销存体系的骨架,表与表之间通过单据编号、业务类型、关联ID串起来,数据闭环完整可靠。
最后再分享一个小技巧:单据编号的生成规则,建议用"前缀+日期+流水号"的组合,比如采购单格式CG20250518001,销售单XS20250518001。前缀便于直观区分单据类型,日期方便按天定位,流水号每天从1开始重新计数,不会太长。这个需求当时客户提出来还有一点点争议,用了两个月之后,财务那边反馈查单确实方便了很多,一个编号就能判断大概是什么时间的什么业务,电话沟通少了很多来回解释。