Vue进销存ERP源码:SaaS多租户与二开商用实践
“最强”这种词放在软件源码前面我一般是不太信的尤其是进销存ERP这种被过度包装的市场。但2026年回头看Vue 进销存ERP SaaS多租户 可二开商用这套组合确实是我近几年落地最顺、交付最快的路线之一。我自己手里这套系统从拿到源码到跑通第一个商用项目前后不到两周做到第四个客户场景时已经从单纯进销存延展到采购审批、门店调拨、财务对账和老板手机报表全靠二开一点点加出来的。这篇文章就把这套Vue进销存ERP源码的结构、多租户设计的取舍、二开过程中的真实坑以及企业级商用部署时那些文档里不写的东西一次性聊透。适合谁看手里已经有源码准备改造的或者正在选型Vue进销存ERP、第一次做SaaS化二开的这篇应该能帮你省掉好几天的试错时间。1. 为什么我会把进销存ERP的技术栈押在Vue上早年间我做进销存用的还是服务端渲染加一堆jQuery片段改一个单据联动要翻三处文件后来接手过一个AngularJS的老项目历史包袱重到连招人都困难。真正让我确定选Vue是后来做多仓库调拨功能时同一份数据要在出库单、入库单、库存余额三个视图里联动更新Vue的响应式模型让这种状态同步变成了一件相对自然的事。1.1 进销存ERP这种系统到底吃前端哪些能力进销存表面看是CRUD实际上对前端框架的要求很高至少有三个点特别吃框架功力。第一是表单密集型。一张采购入库单可能带十几行明细每行有数量、单价、税率、仓库、批号改动任何一行都要实时重算整单金额、税额、合计客户还经常要求明细数量改一下表头汇总立刻变。这个场景如果用传统DOM操作写起来极度痛苦但Vue的响应式数据配合计算属性天然就是为这种联动准备的。第二是状态联动性。单据从草稿到审核、驳回、作账状态一变按钮可用性、行数据颜色、后续流程是否可见全要跟着变。Vue的Computed和Watch能很优雅地处理这种状态派生逻辑不会出现状态改了但按钮忘了更新的低级问题。第三是权限层级深。在SaaS多租户模式下一个用户同时受租户、角色、门店、数据范围四层约束菜单、按钮、字段甚至接口返回的数据都可能是裁剪过的。这套权限模型落到前端需要动态路由、指令权限、路由守卫、请求拦截器协同工作——Vue生态里这些方案的资料最全踩坑成本最低。1.2 Vue在这个场景下的真实手感我用了三年Vue全家桶做管理系统最明显的感觉是业务代码的可预测性很高。进销存这种项目70%的界面是表格 弹窗表单 状态标签的组合。用Vue写组件拆好后新页面基本是拼接工作。尤其是Composition API普及之后逻辑复用不用再纠结mixins命名冲突自定义hooks直接暴露出单据操作、库存刷新、缓存清理这些通用能力二开时新来的开发也能快速上手。再一个点是生态成熟度。说法可能有点俗但进销存ERP二开项目真正花时间的不是技术难点而是把通用能力串起来文件上传附件进销存里的合同扫描件、验收单、电子发票、图表报表老板要的销售趋势柱状图、库存占比饼图、打印模板出库单、对账单导出。这些场景在Vue生态里都有验证过的成熟方案而且踩坑记录全网都是随便一搜就能避开前人挖过的坑。1.3 和React、传统后端的横向对比这里不捧一踩一只说适配场景。React在中大型复杂交互和跨端一致性上很强但如果你的核心诉求是快速交付一个让客户用得住的管理系统并且后续能持续二开Vue的组件库效率优势很明显Element Plus提供的表格分页、表单校验、树形控件、穿梭框覆盖了进销存90%以上的界面需求不用自己造轮子。和传统服务端渲染比Vue在前端交互流畅度和团队分工上完全是另一个时代。当年我做一个报表筛选器服务端渲染要整个页面刷新现在Vue只需要局部更新数据区。那套vue打包放进springboot的部署方式也是很多企业客户接受度最高的方案——后端不折腾前端静态资源往后端工程里一放一个包部署完事。方案表单联动开发效率权限模型落地二开人力门槛典型适用场景Vue Element Plus高高低SaaS管理系统、进销存ERPReact Ant Design高中中复杂交互、跨端产品服务端渲染 jQuery低低高老系统维护、轻量页面2. SaaS多租户的第一步数据隔离方案得先拍板很多人把多租户当成一个功能其实它是一个贯穿数据库、后端、缓存、前端菜单的全局约束。我接手这套源码时第一件事不是细看业务逻辑而是先确认租户数据到底怎么隔离。这个问题不定后面一切二开都是白谈。2.1 三种主流隔离模式怎么选业界的方案无非三种独立数据库、共享库独立Schema、共享库共享表加租户ID。我整理成一张表方便对比。方案隔离性成本运维复杂度适合体量独立数据库最强最高高大客户、金融级合规场景共享库独立Schema中中中数据量大且租户数量少共享库共享表 tenant_id弱一些但够用低低中小企业多租户、进销存ERP我最后选了共享库共享表加租户ID。原因很直白第一这套Vue进销存ERP面向的大多是中小企业和门店单租户月单据量撑死几十万条共享一张表完全跑得动第二SaaS运营方要管几百上千个租户时独立数据库的备份、升级、统计都是噩梦共享表模式用一个简单的迁移脚本就能给所有租户一起加字段第三对二开最友好——新增一张表照样子加个tenant_id字段就行不用动数据库连接层和迁移工具。2.2 共享表模式下代码层面怎么强制隔离隔离不能靠开发自觉必须靠框架机制拦死。这套系统用的是MyBatis-Plus直接启用TenantLineInnerInterceptor它会在每次SQL执行时自动拼接租户条件。关键配置是这样的MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantInterceptor new TenantLineInnerInterceptor( new TenantLineHandler() { Override public Expression getTenantId() { return new LongValue(TenantContextHolder.getTenantId()); } Override public String getTenantIdColumn() { return tenant_id; } Override public boolean ignoreTable(String tableName) { // 全局字典表、系统配置表不需要租户隔离 return Arrays.asList(sys_dict_item, sys_config) .contains(tableName); } } ); interceptor.addInnerInterceptor(tenantInterceptor);这里有个特别容易踩的坑一定要把字典表、系统配置表、全局序列号表加进ignoreTable列表否则连登录接口都跑不通——因为用户表本身都带租户条件了但查询字典的时候根本不知道租户是谁。后端每个Mapper查询自动带上了AND tenant_id ?前端传参根本改不了这个条件这就在接口层面堵死了越权。我做二开时加过好几个自定义报表查询只要走Mapper租户隔离自动生效不用每个SQL都手写一遍租户条件。这套机制能省下大量重复代码也减少了二开人员在不了解架构时写漏条件的概率。2.3 租户上下文和缓存维度是多数源码容易忽略的地方数据层隔离搞定后还有两个隐藏点需要注意。一个是租户上下文的传递这套系统用的是请求线程的ThreadLocal由后端拦截器在每次请求进入时从Token里解析租户ID存进去请求结束释放。这里问题是异步线程和多线程池场景ThreadLocal不会自动传递处理Excel导出、邮件通知这类异步任务时要手动把租户ID带进子线程。另一个是缓存key必须带租户维度。常见错误是拿商品ID或客户ID直接当Redis key结果租户A改完商品价格租户B看到的价格也变了。我在这套系统里定了一个死规矩任何和业务数据相关的缓存key前缀必须带租户编码。比如商品基础信息的缓存key写成stock:tenant_{tenantId}:product:{productId}这样租户之间从根本上就不共享缓存数据。3. 前端权限设计动态路由、按钮控制、租户套餐三件套进销存ERP的权限往细了说分三块菜单权限能看到哪些页面、按钮权限页面里能用哪些按钮、数据权限能看哪些门店和单据。这套系统的前端权限设计把这三点拆得很清楚。3.1 动态路由菜单由后端下发前端只负责渲染现在Vue项目做权限第一反应都是动态路由但真正做扎实的不多。这套系统的流程是用户登录后后端返回菜单树和权限码列表前端根据菜单树用router.addRoute动态注册路由。好处是租户买了哪些模块、角色配了哪些菜单完全在后端调整前端不用发版。const modules import.meta.glob(/views/**/*.vue); function buildRoutes(menuTree) { return menuTree.map(menu ({ path: menu.path, name: menu.name, component: modules[/views/${menu.component}.vue], meta: { title: menu.title, icon: menu.icon }, children: menu.children ? buildRoutes(menu.children) : [] })); }这里有一个必须处理的坑刷新页面时路由是重新注册的但Vue Router此时可能还在用旧路由。我加了全局前置守卫每次路由切换前检查store里的路由是否已注册未注册就先addRoute再放行否则会出现刷新后白屏的老问题。这套源码里这个坑已经填掉了但二开时如果要改登录流程务必保留这个恢复机制。3.2 按钮级权限用指令做v-permission菜单权限只是第一层真正让业务方觉得专业的是按钮权限。采购和销售都可能打开同一张单据详情页但销售只能看、采购能改、财务能作废——这靠菜单权限做不到。按钮权限我用了一个自定义指令用法很贴近业务app.directive(permission, { mounted(el, binding) { const requiredPerm binding.value; // 比如 purchase:order:audit const userPerms usePermissionStore().getPermCodes; if (!userPerms.includes(requiredPerm)) { el.parentNode?.removeChild(el); } } });用法就是v-permissionpurchase:order:audit没有权限的按钮直接DOM移除比改成disabled更安全——因为没有权限的按钮不渲染不看接口请求根本不知道有这个功能。权限码的规范也值得学习统一用模块:功能:动作三层结构比如report:sales:export、stock:transfer:create这样后端做接口鉴权时可以直接复用同一套权限码前后端保持一致。3.3 租户套餐模式和菜单下发组合起来就是SaaS的核心玩法这一步是SaaS多租户商业上最值钱的部分。租户买了什么套餐决定了菜单树返回什么不同套餐的租户登录后看到的功能完全不一样。免费版只给基础进销存专业版才给采购审批和自定义报表企业版再加多仓和财务模块。菜单树的生成逻辑里后端会先查租户套餐编码再过滤功能模块最后叠加角色权限。前端完全不用关心这些只管渲染。在做二开时新增一个功能模块的成本变得非常低后端加菜单记录前端加对应页面老租户不购买就看不到入口完全不影响现有业务。4. 进销存核心链路实战从SKU的数据建模到全业务闭环权限、租户这些底层打好了接下来聊聊系统真正干活的部分。这也就是客户掏钱的核心逻辑进货、卖货、管库存、算成本、出报表。我拆开来说。4.1 商品、库存、单据的数据建模要能撑起多仓库逻辑这套系统的商品表不是简单的一张表而是分了三个层次基础资料SKU、条码、品牌、规格、库存余额每个仓库一行、单据流水每次出入库一行。库存余额表设计得像一张账本CREATE TABLE stock_balance ( id BIGINT PRIMARY KEY, tenant_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, quantity DECIMAL(18,3) DEFAULT 0, available_quantity DECIMAL(18,3) DEFAULT 0, cost_price DECIMAL(18,4) DEFAULT 0, last_inbound_at DATETIME, UNIQUE KEY uk_tenant_sku_wh (tenant_id, sku_id, warehouse_id) );注意几个细节数量字段用了DECIMAL而不是浮点进销存里浮点精度问题会把账算到想哭available_quantity 表示可用库存和物理库存quantity分开——销售出库时锁定预占库存审核后才扣减物理库存这是进销存的标准玩法。采购单审核后系统会生成入库流水并更新库存余额销售出库同理。这个单据驱动库存的模型相当干净库存余额永远等于期初加所有流水合计对账时直接查流水即可。4.2 从采购到销售再到盘点流程闭环是商用系统的生死线我自己最看重的是流程的闭环性。常见的开发错误是只做了我卖东西你付钱但进销存的商用场景远不止这些。采购退货要关联到原采购单销售退货要按原单价格回滚库存盘点做多出的走盘盈、亏空的走盘亏审核后自动调整库存余额不同仓库之间调拨要生成调拨出库单和调拨入库单两个仓库数量一减一加还有组装拆卸商品时父SKU库存减少、子SKU库存增加同时要重新计算子SKU的成本。这套源码在这块的完成度是相当高的每个单据类型都有独立的审核和冲销逻辑互不污染。以我二开和交付的经验看能把这些流程做成闭环才真正达到了企业级的标准。4.3 成本核算移动加权平均法的工程化落地进销存里最容易让客户拍桌子的问题就是成本和毛利计算。这套系统用了移动加权平均法逻辑是每次入库后重新计算成本价公式为原库存成本加本次入库成本除以原库存数量加本次入库数量。用代码表达就是def recalculate_cost(balance, inbound_qty, inbound_amount): old_amount balance[cost_price] * balance[quantity] new_qty balance[quantity] inbound_qty if new_qty 0: balance[cost_price] 0 else: balance[cost_price] (old_amount inbound_amount) / new_qty balance[quantity] new_qty return balance[cost_price]出库时按当前成本价结转销售成本毛利就是销售收入减出库成本。这个逻辑听着简单但工程化的难点在于并发和反审核两张入库单同时审核时要用数据库行锁防超卖冲账反审核一张单据时要顺势回滚后续所有相关单据的库存和成本。我自己二开时在这个位置上最开始用的是全表更新成本价数据一上万就慢得不行后来改成只重算受影响SKU的后续流水性能才过测试门槛。4.4 报表可视化老板要的不是数据是能一眼看懂的东西商用交付时老板们最喜欢的页面一定是首页驾驶舱看板。这套系统里用ECharts画了销售趋势图、毛利环比柱状图、库存占用金额排行、库龄预警列表。这里有一个经验可以分享同一个数据维度最好同时给柱状图加折线的组合图。比如销售趋势柱形表示销售额折线表示订单量一个图同时回答卖了多少和客户来了几趟。热词里就有人搜vue中用echarts画两个柱状统计图其实用两个div并列放两个echarts实例是最简单的方案都比在一个图里塞过多系列清晰。做报表时还要记得权限范围门店店长只能看自己门店的数据老板能看全部门店合并。这个数据范围过滤在后端SQL里自动带上的纯粹靠前端隐藏列是不安全的。5. 二开的正确姿势不是上来就改代码是先立规矩可二开商用是整个标题里最有含金量的一句。但说实话很多源码号称能二开实际一改就是灾难。这套系统之所以能撑住多个商用项目并行我觉得是因为架构上留了很多和气眼。5.1 拿到源码后我推荐按照这个顺序摸透系统第一次拿到这类源码最忌讳直接改某个页面。我一般按三步走。第一先把环境跑起来。前端用Vue CLI或者Vitenpm install的时候注意Node版本跟package.json里的engines是否匹配这里是最容易卡住新手的地方后端一般就是一个Spring Boot工程配置好数据库连接和Redis地址启动后把初始化SQL脚本跑一遍。这一步的目标不是理解代码而是确保基线版本能跑。第二走后端接口文档把核心链路走一遍。登录、建商品、建仓库、做采购单审核、做销售单审核、看报表每个环节看一下接口返回的数据结构和前端页面怎么对应的。这个动作能帮你快速建立数据从哪里来、到哪去的整体感。第三挑一个最简单的小需求完整做一遍。比如给商品表加一个是否临期的标记字段前端表单加开关列表加标签报表里加一个临期占比统计。这个需求麻雀虽小但五脏俱全能让你理清这系统从前端表单、后端实体、Mapper、SQL到报表一条龙的操作习惯。5.2 二开中最常见的四类定制需求代码层面怎么改从我交付的客户来看四类需求出现频率最高也最有规律。第一类打印模板定制。客户要的不仅是A4出库单有可能是58mm小票、铜版纸标签、带logo的送货单。这套系统前端把打印模板拆成了纯HTML模板加CSS样式二开只改模板内容和media print样式就行。改完以后用浏览器的打印预览验证页面边距是最快的。第二类对接外部接口。比如对接电子面单、对接财务软件、对接考勤门禁。统一的做法是加一个独立的adapter模块对外部API做适配层绝不直接在主流程里嵌第三方调用的代码。这样外部接口变动时不会波及核心链路这是二开商用非常重要的边界意识。第三类扩展单据字段。客户可能要求采购单加一个承运单号销售单加一个客户备注。如果只是加一列直接扩展业务表的reserve字段就行如果要做成可配置的扩展字段需要单独做一张字段配置表前端动态渲染表单项。这套系统默认支持后者把这套机制吃透后很多定制需求都不用写死代码。第四类Vue插槽改造。Element Plus组件本身的slot非常丰富比如表格操作列做自定义按钮组、下拉弹层显示自定义内容这些场景二开时优先用slot而不是改组件库源码。改组件库源码的后果是之后升级依赖时会一次次冲突实在难受。5.3 二开阶段最容易被忽略的代码规范问题这里给一个掏心窝的建议商用二开项目代码规范比功能实现重要。一个人开发时看不出来一旦多个人并行改一套代码命名和分层混乱会让你恨不得重写。我在项目里强制了三条后端Service里禁止直接写巨型方法一个方法超过80行就必须拆前端页面的API请求统一走api目录禁止在组件里直接fetch所有敏感操作作废单据、审核、删除必须有操作日志记录。这套系统本身就带了一套基于AOP的操作日志注解二开时随手加上后面排查线上问题能省一半的时间。6. 企业级部署与性能优化从开发机到客户服务器的最后一段路系统开发完、二开完接下来就是让客户验收、上线。这一步踩的坑一点不比写代码少我把最常见的几个问题都列出来。6.1 前端怎么部署兼顾Nginx和SpringBoot两种模式现在的部署模式基本是两种。客户自己有服务器和运维能力的前端用Nginx部署后端SpringBoot独立跑前后端通过配置的API地址通信。客户的运维能力比较弱的就用vue打包放进springboot的单体模式前端构建产物直接复制到SpringBoot的src/main/resources/static目录下后端一个JAR包搞定一切。这里的关键是前端路由要用createWebHashHistory而不是createWebHistory否则刷新页面时Nginx和SpringBoot都要做路径重写配置非常容易漏。我用hash模式跑了好几个客户项目刷新白屏的问题一次也没犯过。6.2 构建层面的性能优化进销存ERP页面多如果全量打包首屏加载会慢到被客户嫌弃。我的做法是Vite配置里手动拆包把element-plus、echarts、vue这些大依赖单独打成chunk同时开启路由懒加载。具体来说vite.config.js里这样配export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { vue-vendor: [vue, vue-router, pinia], element-plus: [element-plus], charts: [echarts] } } } } })配合Nginx的Gzip压缩首屏能控制在2秒以内。这个优化做完客户在演示环境打开系统的第一感受会好非常多。首页的图表数据本来就慢再配上大体积的JS包客户就等着拿这个做理由说你这系统不行。6.3 部署阶段关于安全性和稳定性的三个底线商用系统在部署上线前有三件事必须做。第一强制性改掉默认密码这看起来像废话但二开项目里真有人把admin/123456直接交到客户手里第二数据库要做定时全量备份进销存数据是客户的命根子一旦误删或服务宕机数据丢失造成的信任损失是不可逆的第三操作日志和登录日志必须开启归档SaaS多租户场景下一旦出现了租户数据访问纠纷审计日志是处理的依据。这套系统本身的部署文档里还提示了一个细节中间件层面尽量启用HTTPS接口层面做请求来源校验和频率限制防止被人抓包或爆破。商用系统不是你自己的Demo出了安全事故赔钱是小口碑崩了是大。6.4 上线之后自己先当一个月运维我给这套Vue进销存ERP源码做过交付后一个强烈的体会是上线第一个月自己一定要盯着线上日志看。客户的使用习惯和你测试时预设的完全不一样——有人会连续狂按保存有人会导进一份几万行的Excel有人会在审核过程中直接关掉浏览器。这种真实流量下的问题只有在上线初期才能暴露得最彻底。前一个月把线上当测试环境用后面才能睡得着觉。再一个建议是给客户上线后预留至少两轮反馈迭代。进销存系统在客户业务里是每天都要打开的工具第一周的反馈往往是最有价值的。把这几轮改完项目的成交才会真正变成口碑。我做这套Vue进销存ERP的SaaS改造和二开交付最大的感受就一句话技术选型上Vue生态对这类中后台系统的支撑非常成熟商业变现上一套架构干净、权限清晰、多租户隔离做得到位的源码真正能带来的收益远不止一套系统的开发成本。如果现在再让我从零做一次我依然会选Vue依然会在一个业务模块做完后先花两天时间把权限和数据隔离机制补牢然后再去铺其他功能这也是这套系统在商用项目里能走这么稳的根本原因。