SpringBoot2+Vue3供应商管理系统实战:从表结构设计到前后端部署全解析 每个做企业应用的老哥应该都遇到过类似的场景老板哪天突然甩过来一句搞一套供应商管理系统把咱们的供货商资料和采购买卖捋清楚然后你就要在一个月内从建库到上线拿出东西。这套项目就是干这个事儿的技术栈是SpringBoot2 Vue3 MyBatis-Plus MySQL8.0前后端分离的经典配方源码和文档都齐了。适合有JavaSE基础、想练手完整业务系统的人也适合毕业设计或者中小企业内部系统快速搭建的参考。我这篇文章就把这套系统从头到尾拆开讲从需求分析到表结构设计从后端接口到前端页面再附带我实操中踩过的坑和复盘笔记。内容偏实战你可以直接照着做也可以在这个基础上升级改造。1. 项目定位与功能蓝图拆解1.1 供应商管理系统到底要管什么很多初学朋友一听到供应商管理系统第一反应是这不就是个增删改查吗。业务上确实以增删改查为核心但关键在于查哪张表、改什么状态背后对应的业务规则。这套系统设计上覆盖了供应商全生命周期管理核心角色为采购员、仓库管理员和系统管理员三类。模块功能业务价值供应商档案录入、编辑、冻结、导出供应商基本信息建立统一的供应商资源池资质证书管理关联上传资质文件跟踪到期提醒防范合规风险采购订单下单、审核、收货、退货跟踪采购执行进度到货验收仓库按订单收货登记实收数量库存与财务对账的源头供应商评价从质量、交期、服务三个维度打分为采购决策提供数据参考它的核心目标是解决供应商信息散落在Excel和聊天记录里这个痛点。我见过不少小公司用Excel管供应商一个人一个格式数据从没准过。这套系统把供应商主数据、订单流程、考核评价整合到一条链路上所有数据可追溯这才是它真正值钱的地方。1.2 角色权限与业务流程设计业务流从供应商注册或录入开始到确认合作、采购下单、仓库收货最后回到对供应商的评价打分形成闭环。管理员维护系统用户、角色、菜单权限查看统计报表采购员维护供应商档案、发起采购订单、导出数据仓库管理员按订单收货入库、填写验收结果供应商可选可扩展账号查看自己的订单和评价权限这块我建议不要过度设计用简单的角色-菜单-按钮三级控制就行。每个请求拦截下来校验token对应的角色再判断是否放行。这个系统用的就是JWT 拦截器实现不用引入Spring Security这个重家伙入门理解起来容易也不耽误二次开发。这里有个值得留意的设计细节订单状态和供应商状态要分开控制。供应商状态解决的是能不能下单订单状态解决的是这笔单走到哪一步。如果混在一起会出现供应商冻结之后历史订单状态也跟着乱套的麻烦。所以系统里供应商状态字段supplier_status独立管理订单状态order_status只归订单管两套状态互不干扰。1.3 非功能需求与开发范围约定这种系统看起来简单但真要落地还是要约束一下范围。支持数据导入导出供应商Excel模板批量导入、供应商列表导出敏感操作留痕拉黑、冻结供应商要有操作日志数据库采用逻辑删除不从物理上删数据权限数据在Redis做缓存可选登录token有效期为24小时前端采用动态菜单渲染根据角色返回菜单列表我遇到的不少同学做这类项目时一上来就搞一堆花哨功能什么消息推送、数据大屏全部往上加结果核心的订单闭环反而没打通。建议你先按上面这套范围跑通主流程把文档和测试用例补上再考虑锦上添花。2. 技术选型与架构设计为什么是这套配方2.1 SpringBoot2比SpringBoot3更稳妥SpringBoot2.7.x是目前存量企业项目里存量极大的版本线网上资料、开源组件兼容性、招聘岗位需求都是围绕这个版本展开的。选它不是因为新而是因为稳。如果选SpringBoot3最低要求JDK17很多老一点的办公电脑和企业服务器JDK8还没换跑起来就是一堆环境兼容问题。SpringBoot2搭配JDK8依然是当前后端开发的主流尤其适合这种业务系统。需要提醒的是SpringBoot2.7与SpringBoot2.5、2.6之间也有细微差异比如循环依赖默认禁止策略在2.6以后调整了配置属性的绑定规则也有变化。建议锁定SpringBoot 2.7.18这个版本这是SpringBoot2线的最后一个版本bug修复相对完善也是我实测下来最稳的。2.2 为什么MyBatis-Plus取代原生MyBatisMyBatis-Plus本质上是MyBatis的增强工具它的BaseMapper帮你把单表CRUD全封装好了。开发这套系统时单据类SQL如订单明细按orderNo关联查询、统计报表查询用自定义XML简单CRUD全靠ServiceImpl继承IService不需要写一行Mapper XML。以供应商档案管理为例自动生成的insert、updateById、selectPage这些方法直接可用省下大量样板代码。事务处理上采购订单创建要同时插入订单主表和明细表我直接在Service方法上加Transactional(rollbackFor Exception.class)一条SQL失败整单回滚不产生半截订单。这个特性在原生MyBatis下你得自己写事务管理代码MyBatis-Plus完全省了。2.3 MySQL8.0到底强在哪如果还在用MySQL5.7这套系统跑也能跑但8.0有几个优势是实打实的。默认字符集utf8mb4emoji和特殊符号存储不报错窗口函数支持统计类SQL更简洁不可见索引、直方图等优化特性让慢SQL排查更方便支持公用表表达式CTE写递归查询更直观这套系统中统计供应商订单总金额时我用了一段窗口函数直接按供应商维度算出累计采购额比传统group by 子查询的性能表现好得多。数据库连接串记得加useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai不然连上8.0会报时区错误。这个坑后面第6节我会详细讲。2.4 前端Vue3 Element Plus的组合逻辑Vue3的组合式APIComposition API确实比Vue2的Options API适合做中后台系统因为逻辑复用很舒服。同样的供应商查询条件、分页逻辑用组合式函数composables抽出来以后订单页、评价页都能复用同一套逻辑。Element Plus在表单、表格、弹窗、反馈组件上非常成熟尤其是el-table的列配置和el-form的校验机制。这套系统的前端不是硬凑的核心页面都用了Element Plus的Pro风格布局菜单、面包屑、标签页一应俱全。前端工程化用的是Vite而不是Webpack本地启动开发服务器基本秒开热更新速度也远快于Webpack。Vite的proxy配合后端联调非常方便一个/api前缀直接代理到后端8080端口。3. 数据库设计这套系统的地基3.1 核心表结构与字段设计思路数据库表设计是整个系统的地基我直接贴出核心表的字段设计结合业务场景来解释每个关键字段。供应商主表supplier字段名类型说明supplier_idbigint主键雪花IDsupplier_codevarchar(32)供应商编码唯一supplier_namevarchar(128)供应商名称contact_personvarchar(32)默认联系人contact_phonevarchar(20)联系电话credit_codevarchar(64)统一社会信用代码business_typetinyint业务类型1物资2服务3工程supplier_statustinyint状态0冻结1启用is_deletedtinyint逻辑删除标记为什么主键不用自增ID而用雪花ID业务系统的供应商数据可能要从老系统迁移、要和其他系统同步自增ID容易冲突。雪花ID全局唯一且带有时间趋势MyBatis-Plus内置了ID_WORKER雪花算法策略指定TableId(type IdType.ASSIGN_ID)即可。供应商联系人单独拆一张表supplier_contact和企业客户的一客多联系人同理。一个供应商可能有采购对接人、财务对接人、售后对接人主表只冗余一个默认联系人用于列表展示完整的联系人均保存在子表。3.2 订单与明细如何挂接采购订单主表purchase_order的核心字段包括order_no、supplier_id、order_status、total_amount、payment_status、delivery_date。采购订单明细表purchase_order_item的字段为order_id、product_name、specification、unit_price、quantity、amount。主表和明细表通过order_id关联一对多关系。做采购订单时前端表格里可以动态添加多行商品明细提交时后端用一个DTO统一接收主表数据和明细列表数据一起insert。明细表冗余了product_name商品名称和specification规格型号这里刻意不做商品维度的大表设计因为这类系统往往还没有完整的物料主数据先按文本记录用后面接ERP再升级物料编码。订单金额的计算我采用了单价x数量累加的方式前端负责展示后端重新计算一遍防止伪造提交。前端算的金额不可信后端必须二次计算这也是这类系统的底线约束。3.3 资质证书与评价表的关键设计资质证书表supplier_certification有cert_type证书类型、cert_code证书编号、issue_date、expire_date、file_url附件地址、audit_status审核状态。证书到期提醒的SQL就是在查询时过滤expire_date 当前日期30天的记录把即将到期的证书推给管理员处理。供应商评价表supplier_evaluation的设计要留意评分字段的拆分质量评分quality_score、交期评分delivery_score、服务评分service_score总分为三项加权合计。总分用decimal(5,2)保存避免因浮点运算产生精度问题。每次到货验收完成后仓库管理员可以发起一次评价数据积累起来就能用SQL统计出每家供应商的平均分页面展示排名。这套系统的评价维度只有三项如果业务复杂还可以扩展价格评分、响应速度评分字段设计预留了扩展空间。表结构千万不要一开始就埋太多冗余字段宁可后期加字段也别让表长成杂货铺。3.4 常用查询SQL与索引设计订单查询场景最多的是某个供应商在某个时间段内的订单列表。我在purchase_order表上建了联合索引(idx_supplier_create)supplier_id create_time这样按供应商查询时间范围的数据索引命中率非常高。供应商列表搜索条件通常是供应商编码、名称、状态我在supplier表的supplier_code上建了唯一索引在supplier_name上建了普通索引。这里要注意模糊查询如果写成LIKE %xx%即使有索引也不走索引。可以用前缀匹配优化或者用MySQL8.0的全文索引不过量小的话直接like就行性能不是瓶颈。常见的统计SQL是查每个供应商的订单总数和采购总额SELECT supplier_id, COUNT(order_id) AS order_count, SUM(total_amount) AS total_amount FROM purchase_order WHERE order_status ! -1 AND create_time #{startTime} GROUP BY supplier_id ORDER BY total_amount DESC数据量大时考虑加创建时间的过滤条件避免全表扫描。只要索引用对这套系统的SQL写到什么阶段都不会有性能问题。4. 后端核心业务从接口设计到权限控制4.1 统一结果集与异常处理后端接口设计上我先统一返回结构Result。这个类包含code、message、data三个基本字段code为200表示成功401表示未认证500表示服务器异常业务异常也可以自定义错误码。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }配合RestControllerAdvice做全局异常捕获业务异常抛出ServiceException时统一转成Result结构返回。这样前端axios拦截器只需要判断code字段不用每个接口单独处理错误分支。4.2 供应商档案的分页/查询/导入导出供应商档案是系统的第一个完整CRUD模块。分页查询使用MyBatis-Plus的Page对象Override public PageSupplier querySupplierPage(SupplierQuery query) { PageSupplier page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperSupplier wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getSupplierName()), Supplier::getSupplierName, query.getSupplierName()) .eq(query.getSupplierStatus() ! null, Supplier::getSupplierStatus, query.getSupplierStatus()) .orderByDesc(Supplier::getCreateTime); return this.page(page, wrapper); }这段代码看起来不起眼但有一个关键点分页插件必须配置MybatisPlusInterceptor不配置的话分页不会生效这是新手最容易踩的坑。后面第6节我会写清楚。Excel导入导出用的是EasyExcel阿里开源的相比POI它封装度高、内存占用小。导入模板第一行是供应商名称、联系人、电话等字段解析时一行行校验校验失败的行单独收集起来给出错误提示不因为一行数据有问题就把整个导入任务中断。导出则直接查数据然后写Excel返回给前端下载。4.3 采购订单的状态机设计订单状态管理是这套系统业务复杂度最高的地方。我先定义了状态机0 待审核 - 1 审核通过 - 2 部分收货 - 3 已完成 - -1 驳回 0 待审核 - -2 作废状态流转用OrderStatusEnum枚举管理public enum OrderStatusEnum { PENDING(0, 待审核), APPROVED(1, 审核通过), PART_RECEIVED(2, 部分收货), COMPLETED(3, 已完成), REJECTED(-1, 已驳回), CANCELLED(-2, 已作废); }这里有一个值得反复强调的思想状态流转要写在Service层不能在前端随便改。每次变更状态前做前置校验比如只有审核通过的订单才能生成收货单待审核的订单不能直接跳到已完成。这才能保证数据不出现逻辑上不可能的脏状态。收货时如果实收数量小于订单数量状态置为部分收货如果全部收齐状态置为已完成。这个判断在收货Service里完成用事务包起来收货单插入成功的同时更新订单状态。4.4 JWT登录认证与权限拦截登录流程用户输入账号密码后端验证成功后生成JWT token返回前端。前端把token存在localStorage每次请求通过axios拦截器在header加上Authorization字段。后端拦截器校验tokenpublic class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token) JwtUtil.verify(token)) { return true; } response.setStatus(401); response.getWriter().write(未登录或token已过期); return false; } }配置拦截器时需要对登录接口、静态资源、Swagger文档放行。权限控制在角色层面用自定义注解RequireRole(ADMIN)标注在需要特定角色的接口上拦截器里读取角色信息和注解比对不匹配直接返回403。JWT这里有个实用细节token里不要放太多业务信息userId和role就够用了。用户被冻结之后要立即失去权限光靠token是解决不了的。所以我加了一个简单的Redis token黑名单机制冻结用户时把token加入黑名单拦截器里查一下黑名单实现实时踢人的效果。5. 前端Vue3页面实现与前后端交互5.1 工程初始化与项目结构前端工程用Vite创建npm create vitelatest supplier-web选择Vue3 JavaScript。项目目录结构如下src ├── api │ ├── supplier.js │ ├── order.js │ └── user.js ├── components │ ├── Pager.vue │ └── SupplierSelect.vue ├── router │ └── index.js ├── stores │ ├── user.js │ └── app.js ├── views │ ├── dashboard │ ├── supplier │ │ ├── SupplierList.vue │ │ └── SupplierDetail.vue │ ├── order │ │ ├── OrderList.vue │ │ └── OrderCreate.vue │ └── login │ └── Login.vue └── utils └── request.jsapi目录统一放接口定义views目录按业务模块拆页面每个页面对应一个路由。中后台系统这么分层维护成本是最低的。5.2 Axios二次封装与请求拦截request.js对axios做统一封装核心逻辑是请求拦截器加token响应拦截器处理业务错误码统一提示。import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) service.interceptors.response.use(response { const res response.data if (res.code 200) { return res } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) }) export default service这套封装抽出来以后每个业务模块的接口文件就非常干净// api/supplier.js import request from /utils/request export function getSupplierPage(params) { return request.post(/supplier/page, params) } export function updateSupplier(data) { return request.put(/supplier, data) }5.3 供应商列表页动态表格与分页供应商列表页是最典型的表格页面。页面加载时调用分页接口把查询条件和分页参数传给后端。组合式API在这里优势很明显把查询、加载、重置逻辑封装成可复用的函数export function useSupplierList() { const loading ref(false) const list ref([]) const total ref(0) const queryParams reactive({ pageNum: 1, pageSize: 10, supplierName: , supplierStatus: null }) async function fetchList() { loading.value true const res await getSupplierPage({ ...queryParams }) list.value res.data.records total.value res.data.total loading.value false } return { loading, list, total, queryParams, fetchList } }分页组件直接配合el-paginationcurrent-change和size-change事件触发fetchList重新查询。页面侧边栏菜单根据登录角色动态渲染Vue Router的路由守卫在跳转前判断权限不匹配页面会重定向到403页面。5.4 采购订单创建页动态添加明细订单创建页是前端交互最复杂的部分。用户选择供应商后下方明细表格可以逐行添加商品、填写单价数量合计金额实时计算。实现用的是el-table el-input组合明细数据保存在订单表单的items数组里。每次添加一行就push一个新对象删除行就splice掉。因为明细项总金额单价×数量用computed对items做reduce求和页面实时显示总金额。提交时把form和items打包成一个payload发送给后端。这里前端有一个隐藏逻辑提交前必须校验明细行至少有一行商品名称不能为空单价不能小于0数量必须是大于0的整数。Element Plus表单校验规则配合自定义validator可以完成大部分校验但行内校验我习惯在提交前整体循环检查一次保证万无一失。6. 前后端联调、部署与常见坑点实录6.1 开发环境跨域代理前端用Vite本地开发时接口路径/api的请求默认会被浏览器拦截跨域通过在vite.config.js中配置proxy解决export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })生产环境交给Nginx反向代理配置下面这段就够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; }6.2 MySQL8.0时区报错与驱动兼容MySQL8.0连接串少了serverTimezone参数启动项目时会直接抛InvalidConnectionAttributeException异常。这个问题我开发时连着两次遇到印象很深。正确的URL格式jdbc:mysql://localhost:3306/supplier_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue是MySQL8.0的另一个小坑不设的话部分版本会报Public Key Retrieval is not allowed错误。驱动建议用mysql-connector-j 8.0.x版本JDBC驱动类和5.x不同从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。6.3 MyBatis-Plus分页失效与正确配置很多人写好了Page查询却发现返回的total一直是0records也没有分页效果。原因就是没有注入分页插件。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置类的完整版还需要配置LogicDelete逻辑删除插件和OptimisticLocker乐观锁插件。逻辑删除我用的是TableLogic注解配合全局配置logic-delete-field删除操作自动转成update set is_deleted1查询自动过滤is_deleted0的数据这比物理删除安全得多。6.4 LocalDateTime序列化格式SpringBoot返回的LocalDateTime默认序列化成2025-01-15T10:30:00这种带字母T的格式前端显示很不友好。我在application.yml里做了全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样所有接口返回的日期格式统一前端表格渲染不再需要单独处理时间字符串。6.5 订单明细批量插入的事务控制采购订单创建接口涉及订单主表明细表操作日志三张表的写操作。任何一张表插入失败数据都不能落库。在Service方法上添加事务注解并指定rollbackForTransactional(rollbackFor Exception.class) public Long createPurchaseOrder(PurchaseOrderCreateDTO dto) { // 1. 校验供应商状态 // 2. 计算订单总金额 // 3. 插入订单主表 // 4. 批量插入订单明细 // 5. 插入操作日志 return orderId; }rollbackFor Exception.class这个细节不能省不加的话RuntimeException才会回滚某些受检异常可能导致事务失效。交易类场景的代码里我就是被这个问题坑过才长记性的。6.6 几张实用排查SQL上线后发现某个供应商的订单金额对不上我会先用这几条SQL定位问题-- 查看订单主表金额 SELECT order_no, total_amount, create_time FROM purchase_order WHERE supplier_id 1001 AND create_time BETWEEN 2025-01-01 00:00:00 AND 2025-12-31 23:59:59; -- 按明细累计金额核对主表金额是否一致 SELECT order_id, SUM(quantity * unit_price) AS detail_total FROM purchase_order_item GROUP BY order_id HAVING detail_total 0;这类SQL在联调阶段能帮你快速定位是前端传参问题还是后端计算问题建议实际项目中提前准备。7. 实操心得开发这套系统的经验复盘项目开发过程中我踩过不少坑也积累了些值得分享的经验。列几条最实用的字段类型和校验要前置设计。供应商的联系电话在数据库里用varchar(20)但前端表单校验规则里一定要写手机号正则。数据从Excel导入时电话一列能解析成科学计数法比如1.38E10必须在导入逻辑里做格式转换。这个问题在导出Excel回来看数据时极其常见。逻辑删除不要和唯一索引冲突。供应商表的supplier_code是唯一索引逻辑删除的供应商还占着这行数据如果再次导入同一个code会违反唯一索引冲突。我采用的方案是唯一索引改为supplier_code is_deleted联合索引这样逻辑删除的数据不阻塞重新录入同编码供应商。不要过度封装。这种规模系统的Service层直接暴露业务动作是最直观的。我见过有人搞了三层泛型基类套娃改一个字段要跳五层代码维护成本反而暴增。中小型项目优先保证代码直白可读比花里胡哨的架构设计更重要。接口参数校验别省。前端做了表单校验后端同样要走参数校验我习惯在DTO字段上标注NotBlank、NotNull、Min等校验注解配合Validated统一处理。不要信任任何前端传过来的数据这笔功夫不能省。库存和金额用Decimal不要用Double。Java的Double在计算金额时会有精度丢失风险数据库存储金额用decimal(10,2)实体类用BigDecimal。BigDecimal除法还要指定精度和舍入模式这些细节看着小但在财务对账时就是大事儿。这套系统做完我最大的感受是一个项目值钱的部分不在代码量而在数据结构和状态流转是否设计得干净。供应商管理系统的技术点不复杂但设计表结构时有没有想到一客多联系人做订单时有没有考虑部分收货的状态做评价时有没有把评分维度拆开这些才是真正体现业务能力的地方。如果在现有基础上继续扩展可以往三个方向走引入消息队列做采购订单异步通知、加PowerJob或Quartz做证书到期定时提醒、用ECharts做采购金额趋势大屏。不管怎么扩展核心主链路这版代码已经打好了底子框架和思路都在这儿你想怎么玩都有空间。