ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0供应商管理系统源码实战解析

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0供应商管理系统源码实战解析

接手过不少管理系统项目,其中“供应商管理系统”这类业务是我认为最适合新手进阶、也最适合公司内部快速交付的选题之一。身边同事拿到的源码头衔很直白:Java Web 供应商管理系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】,技术栈一点也不花哨,全是国内中小团队日常用得最顺手的组合。这篇文章我打算把它拆开讲透:从业务盘点、技术选型、数据库建模,到前后端落地、部署避坑,给出一套可以直接参照的完整路径。适合正在学 SpringBoot 和 Vue3 的开发者,也适合想在公司内部快速搭出一套供应商入库、采购、对账后台的同学。

1. 供应商管理系统练手前,先想清楚要解决的业务问题

很多源码拿到手,第一反应是赶紧启动、赶紧看页面。这其实是个误区。供应商管理系统是个典型的企业级 CRUD 项目,但它跟随意写个用户管理不一样,它的业务关系层层嵌套,如果一开始不理解数据模型和状态流转,后面改一个需求都可能牵一发动全身。

1.1 核心业务模块拆解

供应商管理系统最基本的业务闭环是这样的:先维护供应商档案,再基于档案做询价和采购,采购之后有入库,入库之后有对账结算。落到功能上,通常分为四块。

  • 供应商档案模块:记录供应商的基础信息、联系人、地址、银行账户、许可证、合作状态、资质文件等。这是全系统的地基,所有后续单据都要关联这里的 supplier_id。
  • 采购寻源模块:包括询价单、报价单、比价结果以及采购订单。主体流程是“发询价 → 供应商报价 → 确认中标 → 转采购订单”。
  • 收货质检模块:到货登记、质检单、不合格处理、入库单。库房收到货后需要跟采购订单做数量核对。
  • 对账结算模块:月度对账单、发票登记、付款申请、付款记录。供应商能不能按时收到钱,全看这个环节是否清晰。

我这几年看过不少所谓“完整源码”,最容易出问题的反而不是页面华丽不华丽,而是这几个模块之间的状态衔接。比如采购订单已经入库了一部分,再点“整单入库”系统必须拦下来;供应商被禁用之后,旧订单还得能查询但不能新增。这类规则才是供应商管理系统真正的价值所在,也是面试官最爱问的东西。

1.2 角色权限与界面终态

除了业务单据,供应商管理系统背后一定有角色区分。最典型的有三类角色:采购专员负责建档案、发询价、做采购单;库管员负责收货和质检;财务或审核员负责对账付款。不同角色看到的菜单、按钮权限不一样。

这个点在源码里通常体现为前端路由守卫 + 后端接口权限校验。很多人在练手项目里只做了登录和前端菜单过滤,后端接口全部放开,这是很危险的。真放在部门内部使用,至少要在后端 spring security 或自定义拦截器里校验角色编码。

我当时梳理需求时习惯先把每个角色的操作路径画成一张功能矩阵表,比如供应商档案:采购专员可新增、编辑、禁用,库管员只读,财务只读。确定矩阵后再定接口权限,后端代码写起来就非常清晰。这个习惯能直接提高需求评审效率,也能让源码的可读性上一个台阶。

2. 技术选型:为什么这套组合能撑起中小型管理系统

“SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0”这套组合,在今天的国内中小型项目里几乎属于默认选项。不是因为它最先进,而是因为它在开发效率、学习曲线、部署成本、招人难度四者之间取得了一个很舒服的平衡。

2.1 后端为何是 SpringBoot 2 而非 SpringBoot 3

SpringBoot 3 已经出了挺久,但大量企业内部项目和网上的成熟轮子仍然停留在 SpringBoot 2。原因并不复杂:SpringBoot 2 的稳定版本经过了大规模生产验证,第三方组件如各种工作流引擎、短信 SDK、支付 SDK 对它的适配最全;同时,JDK 8 在国内存量部署中占比依然非常高。

你可能会问,那新项目为什么不用 SpringBoot 3?这里要区分场景。如果你是学习目的,想把 Java 17 的 sealed class、record 用起来,上 SpringBoot 3 完全没问题。但如果你是参照源码做二次开发,或者想在公司快速交付,SpringBoot 2 + JDK 8 能省去大量环境兼容问题。我看过好几个团队因为非要用 SpringBoot 3 而上生产,结果底层数据库连接池版本冲突,排查了一个通宵。

所以标题里写明了 SpringBoot2,我觉得反而是一种务实信号:目标是把业务跑通、稳定交付,而不是追新。

2.2 Vue3 为什么是前端的主选

Vue3 的 Composition API 让逻辑复用从“mixin 混入”变成“组合式函数”,这对管理系统来说太重要了。管理系统页面高度相似:列表页、搜索表单、弹窗表单、详情页。以前用 Vue2 Options API 写逻辑,每个页面都是 data、methods、watch 散落一地,复制粘贴多了之后维护非常痛苦。

用 Composition API 可以很干净地把一段业务逻辑抽成 useSupplierForm()、useSupplierTable() 这样的组合函数,在不同页面里复用。此外,Vue3 配合 Vite 开发服务器启动速度非常快,哪怕是大型后台项目,热更新基本都在毫秒级,不像老项目改个页面要等好几秒。

2.3 MyBatis-Plus 与 MySQL 8.0 的配合逻辑

MyBatis-Plus 在社区里口碑很两极,有人觉得它不够“原生”,有人觉得它极大提升了效率。我的观点是:对于供应商管理系统这种以单表 CRUD 为主、少量复杂关联查询的系统,MyBatis-Plus 的 BaseMapper 能干掉至少四成手写 SQL,LambdaQueryWrapper 又能避免拼接字符串带来的低级错误。

MySQL 8.0 的价值则体现在两个容易被忽略的地方:一是窗口函数,做供应商对账排行、订单明细汇总特别方便;二是 JSON 字段类型,供应商资质千奇百怪,如果硬要每类资质建一张表,建模会很痛苦,用 JSON 字段存储扩展属性则灵活得多。数据库版本千万别再用 5.7,8.0 在默认字符集 utf8mb4、索引优化、窗口函数支持上都明显更强。

3. 数据库建模:供应商模块最核心的几张表怎么设计

供应商管理系统说到底是围着几张表转的。表设计得好,后面接口和前端都会很顺;表设计得乱,后期每个查询都会不停补锅。下面我以最常复用的核心表为例拆一下。

3.1 供应商主表与联系人子表

供应商主表我习惯叫 supplier,不做复数也不加 sys_ 前缀。核心字段如下:

字段名类型说明
idbigint主键,雪花算法生成
supplier_codevarchar(64)供应商编码,系统自动生成
supplier_namevarchar(128)供应商名称
categoryvarchar(32)供应商类别,如原材料/外包/服务
credit_codevarchar(32)统一社会信用代码
statustinyint状态:0 禁用 1 正常
deletedtinyint逻辑删除标记
create_timedatetime创建时间
update_timedatetime更新时间

联系人子表 contact 则保存 main_contact 等字段,多个联系人之间通过 supplier_id 关联。为什么不直接把联系人塞到主表里?因为供应商往往有销售、售后、财务多个联系人,一张表塞不下,而且联系人权限不同,塞在一起很容易覆盖覆盖。

建表我建议直接用 MySQL 8.0 的 utf8mb4 字符集,默认排序规则用 utf8mb4_0900_ai_ci。主键避免使用自增 int,因为涉及订单等业务表后,数据量上来时自增主键容易在大批导入场景产生间隙和锁竞争。用 MyBatis-Plus 配置 ASSIGN_ID 雪花算法就是为此准备的。

CREATE TABLE supplier ( id BIGINT NOT NULL COMMENT '主键ID', supplier_code VARCHAR(64) NOT NULL COMMENT '供应商编码', supplier_name VARCHAR(128) NOT NULL COMMENT '供应商名称', category VARCHAR(32) DEFAULT NULL COMMENT '供应商类别', credit_code VARCHAR(32) DEFAULT NULL COMMENT '统一社会信用代码', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0禁用 1正常', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0未删 1已删', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_supplier_code (supplier_code) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '供应商主表';

3.2 采购订单与订单明细的设计要点

采购订单和订单明细是一对多的父子结构。主表 procurement_order 记录单号、供应商 ID、总金额、单据状态、制单人;明细表 procurement_order_item 记录商品或服务名称、规格、数量、单价、金额、已入库数量。

一个高频设计错误是把“金额合计”直接依赖数据库 SUM,而不是在明细维护后由后端重新计算写回主表。这样做虽然冗余,但能让你在列表页直接展示订单总金额,不用每行都去子表聚合查询。这在大数据量下性能差别非常大,属于典型的以空间换时间。

状态字段我建议用 tinyint 而不是 varchar 字符串,比如 0 草稿、1 已确认、2 部分收货、3 已完成、4 已取消。为什么不用字符串?因为字符串状态一旦被前端拼错大小写,接口查出来的数据就是空集;用数字加枚举类,后端代码里还能直接写成常量,IDE 还能自动提示。

3.3 对账与付款状态如何衔接

对账模块我会增加一张 settlement_record 表,关联供应商 ID、周期开始时间、周期结束时间、应付总金额、已付金额、对账状态。每次付款生成一条 payment_record,并回写 settlement_record 的已付金额。

这里要特别注意并发问题:如果两个管理员同时点付款,可能把已付金额覆盖错。稳妥的做法是在后端支付记录表插入时用乐观锁,或者在更新 settlement_record 已付金额时用 SQL 的原子更新,比如SET paid_amount = paid_amount + #{amount},而不是先查出来再 set 回去。很多人忽略这点,等财务发现账对不上再排查,非常麻烦。

4. 后端落地:SpringBoot 2 + MyBatis-Plus 的后端工程组织方式

看完表结构,后端需要做的就是把这些表变成能被人调用的 API。工程组织得好不好,直接影响后面加功能的速度。

4.1 包结构与统一响应模型

推荐按模块分包,而不是按技术分层分包。也就是说,不要搞 controller、service、mapper 这种顶层包,而是把订单相关的 Controller、Service、Mapper 放在 procurement 包下,把供应商相关的放在 supplier 包下。这样当一个模块需要改动时,你只进同一个包里改,不用在十几个平行包之间来回跳。

每个接口的返回体建议统一封装成 Result ,至少包含 code、message、data 三个字段。这能避免前端每调一个接口都要适配不同的返回格式。我在项目里见过最难受的写法是有的接口返回原对象,有的接口返回 Map,有的直接返回 null,前端要写三四层判断,极其折磨。

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "success"; r.data = data; return r; } public static <T> Result<T> fail(String message) { Result<T> r = new Result<>(); r.code = 500; r.message = message; return r; } }

4.2 MyBatis-Plus 配置与常用功能的坑

MyBatis-Plus 不是引入依赖就万事大吉,至少要做三件事:配置分页插件、配置自动填充、配置逻辑删除。分页插件没配好,selectPage 方法会失效,查出来的数据还是全量,这是新手最常见的坑。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

自动填充通常处理 create_time 和 update_time,实现 MetaObjectHandler 后,在插入和更新时自动 set 字段,业务代码里就不用到处写了。

逻辑删除则在实体字段上标注@TableLogic,配置之后删除接口默认变成 update。这里有一个需要警惕的细节:逻辑删除字段必须参与唯一索引的设计,否则一个供应商被删除后再建同名的,会因唯一索引冲突而失败。常见解法是把 deleted 字段变成删除时间戳,或者组合索引里带 deleted。

4.3 认证与权限校验怎么做

对于管理系统,认证方案最常见的组合是 Spring Security + JWT,也有项目用更轻量的 Sa-Token。如果只是内部管理系统,Sa-Token 确实更简洁,拦截器加@SaCheckRole注解就能完成角色校验,学习成本低于 Spring Security。

不管用哪个框架,后端接口权限不可省略。以供应商禁用接口为例,如果采购专员都能调用,业务规则就废了。一般我会在 Controller 方法上加角色或权限标识,对应的初始数据表里存上角色和菜单权限点。

JWT 存储建议只用令牌本身,不在里面塞太多业务数据。令牌过期时间设置为 2 到 8 小时都可以,根据团队使用习惯来。刷新令牌这层如果短期内部使用可以先不做,但日志里务必记录用户 ID,方便问题回溯。

4.4 核心接口流程示例:新增供应商并关联联系人

新增供应商这个接口,表面看只是两张表的 insert,但实际要处理的事情不少。首先要对传入的供应商编码做去重校验,然后保存主表拿到生成的 ID,再循环保存联系人列表。事务必须加在方法上,保证主表和子表同时成功。

@Transactional(rollbackFor = Exception.class) public Long createSupplier(SupplierCreateVO vo) { Supplier entity = new Supplier(); BeanUtils.copyProperties(vo, entity); entity.setSupplierCode(generateSupplierCode()); entity.setStatus(1); supplierMapper.insert(entity); if (CollectionUtil.isNotEmpty(vo.getContacts())) { for (ContactVO contactVO : vo.getContacts()) { Contact contact = new Contact(); contact.setSupplierId(entity.getId()); contact.setContactName(contactVO.getContactName()); contact.setPhone(contactVO.getPhone()); contactMapper.insert(contact); } } return entity.getId(); }

这段逻辑里有个关键点:后端不要接受前端传过来的 supplier_code,应该在后端自己生成。编码规则可以设计为SUP + 日期 + 四位流水号。让前端生成很容易因为多端并发而撞号。

5. 前端落地:Vue3 组合式 API 下页面与接口如何协同

前端这部分,很多人拿到源码后想着要先装依赖、先启动。其实打开源码第一件事应该是看 router 和 api 两个目录,把页面路由和接口目录对上号。这样你对整个系统的功能分布就有了整体印象。

5.1 前端工程结构与 Axios 封装

以 Vite + Vue3 + Pinia + Vue Router + Element Plus 为标准配置。api 目录下按业务模块建 js 文件,比如 supplier.js、purchase.js,每个文件导出对应的请求方法。这样做的好处是,当你要改供应商接口的 URL 时,只需要在一个文件里修改。

Axios 封装要处理两件事:请求头自动携带 token,响应拦截里统一解包后端返回的 Result。如果后端返回 code 不为 200,直接在前端弹出错误消息,业务代码里就不用每个接口都写 try-catch。

import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use(res => { const data = res.data if (data.code !== 200) { ElMessage.error(data.message) return Promise.reject(new Error(data.message)) } return data }, error => { ElMessage.error(error.message) return Promise.reject(error) })

5.2 供应商列表页的搜索区、表格区、操作区设计

供应商列表页几乎是所有管理页面的母版。页面从上到下划分为三块:搜索区放关键词输入框和查询重置按钮;表格区展示供应商编码、名称、类别、状态、创建时间;操作区放编辑、禁用、详情按钮。

搜索区和表格区联动,我一般会写一个 useSupplierQuery 的组合函数,把查询条件、分页参数、list 数据、loading 状态都放在一起,表格和搜索表单通过调用同一个 loadData 函数完成同步。这样即便以后新增一个“供应商选择弹窗”,也能复用这套逻辑。

表格列字段不要一股脑全展示,筛选出业务真正关心的四五个核心字段就够。比如联系人电话可以放在详情弹窗里看,不要占表格宽度。

5.3 新增和编辑用同一个表单组件

供应商新增和编辑共用一个表单组件,是管理系统里很常规的优化手段。做法是在组件里接收一个 modelValue 或者 id prop,如果为空就认为是新增,否则先调用详情接口回填表单。

表单的校验是另一个不能省的部分。供应商名称、统一社会信用代码建议做必填校验;统一社会信用代码还可以在前端用正则校验格式。提交时如果校验不通过,Element Plus 会自动把错误提示标在对应表单项下。

有个容易被忽略的细节:编辑回填时,联系人列表要能动态增删行,并且每行也有自己的校验。Element Plus 的动态表单校验写起来稍绕,但这是供应商管理场景中的刚需,因为一个公司往往不止一个联系人。

<template> <el-dialog v-model="visible" :title="form.id ? '编辑供应商' : '新增供应商'" width="720px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="110px"> <el-form-item label="供应商名称" prop="supplierName"> <el-input v-model="form.supplierName" placeholder="请输入供应商名称" /> </el-form-item> <el-form-item label="类别" prop="category"> <el-select v-model="form.category" placeholder="请选择类别"> <el-option label="原材料" value="RAW" /> <el-option label="外包服务" value="SERVICE" /> </el-select> </el-form-item> </el-form> </el-dialog> </template>

6. 部署运行与常见坑:从本地跑通到交付演示的完整路径

源码拿到手,前五分钟最重要的事情是把项目跑起来。跑不起来,一切分析和学习都无从谈起。这一节我把本地跑通和交付演示时要踩的坑集中说一遍。

6.1 本地环境配置和数据库初始化顺序

建议的环境版本:JDK 8、Maven 3.6+、Node 16+、MySQL 8.0。数据库初始化时,先执行项目里 sql 目录下的建库脚本,再执行数据初始化脚本。注意顺序不能反,否则外键关联会报错。

application.yml 里最常改的是数据库账号密码、端口、以及 mapper XML 扫描路径。如果你看到启动报“Invalid bound statement”,通常就是 mapper xml 的路径跟 MapperScan 配置不一致。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/supplier_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

前端启动前先 npm install,然后看 vite.config.js 里的代理配置。本地联调时,一般把/api代理到http://localhost:8080。如果不配置代理,前端请求会 404。

6.2 部署到服务器时的常见坑

本地跑通后,部署到服务器还要踩一轮坑。后端打成 jar 包,用java -jar启动时,记得把日志输出到文件而不是控制台,方便排查问题。用 nohup 命令时加上-XX:+HeapDumpOnOutOfMemoryError参数,内存溢出时能留下现场。

前端 build 后是纯静态文件,交给 Nginx 托管即可。Nginx 配置里需要做三件事:静态资源缓存、history 路由的 try_files 配置、反向代理 API 地址。

location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /opt/supplier-web/dist; index index.html; try_files $uri $uri/ /index.html; }

6.3 交付演示前一定要检查的几个点

给领导或客户演示项目时,有几个点不提前检查就会翻车。第一,演示数据要先准备好,别在演示现场才录供应商,真录到一半网络卡住会非常尴尬。第二,角色权限要严格按演示脚本操作,如果演示采购专员角色却操作了只有财务才能点的按钮,会被误以为系统权限有问题。第三,时间显示要统一,如果数据库是 UTC、前端又是北京时间,列表页会莫名其妙少 8 小时。

还有一个我实际踩过的坑是打包时把前端 dist 放进了后端 resources 目录,结果 SpringBoot 的静态资源映射把登录页和接口路径全部接管,导致 Nginx 反向代理失效。后来我用了独立部署方案,前端和后端完全分开,开发和交付都清爽了很多。

7. 二次开发时可以继续深入的几个方向

跑通源码只是开始。供应商管理系统这类项目,做完了基础 CRUD 之后,还有很多可以向实用性延伸的地方,这也是我建议团队在源码基础上继续做二次开发的原因。

7.1 文件上传与资质管理

供应商的营业执照、ISO 证书、合作协议通常都是文档或图片。把 file 模块加进来,用本地存储或对象存储保存文件,数据库表里记录附件路径和上传人。注意前端做上传时要用 FormData 格式,Axios 的 header 里不能手动设置 content-type,否则文件流会变成字符串,后端接收后就乱码了。

7.2 采购流程的审批与消息通知

如果团队在使用中觉得流程太随意,可以在采购订单上增加审批链:采购员提交、部门主管审批、分管领导审批。用一张审批记录表记录每个节点的处理人、动作、意见和时间。这不是必须,但做了之后系统的真实可用度会明显提升。

7.3 数据看板与报表导出

管理系统最容易被领导看中的其实是数据报表。供应商数量、月度采购金额、到货及时率、付款周期,这些统计用 MySQL 8.0 的窗口函数可以写得很简洁。前端用 ECharts 展示趋势图,再用后端 EasyExcel 或 POI 导出 Excel,整个项目的“完成度”立刻就能提升一个量级。

我自己第二次做类似项目时,把报表模块提前到了核心迭代里,虽然交付周期长了一些,但领导验收时通过得非常顺畅,后面追加需求也少了,因为决策者对系统的第一印象已经被报表建立起来了。

8. 最后分享一点实操体会

供应商管理系统这个题目做下来,我最大的体会是:源码的价值不在代码本身,而在它能不能帮你建立“从业务到表结构、从表结构到接口、从接口到页面”的完整映射感。很多人学完 SpringBoot 和 Vue3 之后还是不会做项目,就是因为缺少一条把技术串起来的线,供应商管理系统恰好是那条线最合适的载体之一。

如果照着这套源码学习,我建议的学习顺序是先跑起来,再删掉一个模块,然后不看参考代码自己重新实现一遍。在这个过程里你会被迫思考很多平时忽略的边界问题:数据校验怎么做、状态流转怎么设计、事务边界放哪、权限怎么拦。把这些想清楚,比多刷十道面试题都更有价值。

最后再给一个小技巧:本地开发时,把 MySQL 的慢查询日志打开,long_query_time设为 2 秒。跑测试用例时一旦某个接口响应超过阈值,日志里就会记录 SQL。管理系统数据量不大时性能问题不明显,但养成看慢 SQL 的习惯,以后处理真正高并发事故时你会感谢这个动作。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进