做采购供应链这块的同行应该都清楚,市面上成套的SRM(供应商关系管理)系统要么是SaaS年费高得离谱,要么是定制化项目报价直接劝退中小企业。我前两年帮一家制造业客户落地供应商管理和采购管理系统时,也是被各种商业方案的价格和服务方式折腾得够呛,最后干脆基于Java Spring Boot加Vue这套技术栈从零搭了一套,效果反倒比预期好很多。这篇文章我就把整个系统的代码结构、核心模块、业务流程以及踩过的坑一次性整理出来,给正在考虑自研或二次开发的团队做个参考。
1. SRM系统整体设计与模块拆解
1.1 先搞清楚SRM到底解决什么问题
很多刚接触供应链系统的开发者容易把SRM和ERP里的采购模块混为一谈,实际上两者的侧重点差别很大。ERP的采购模块更多是解决“买了什么、花了多少钱、入库没有”这些结果记录,而SRM重点管的是“供应商怎么进来、怎么评估、怎么协作、怎么淘汰”的过程管理。换句话说,ERP管的是订单链路,SRM管的是供应商生命周期。
我在设计这套系统时,核心围绕四条线展开:
- 供应商全生命周期管理:从注册、准入审核、资质文件上传、分级分类,到日常绩效评估、黑名单管理。
- 采购业务协同:把传统通过邮件、电话、微信群进行的寻源、询价、报价、下单、送货、对账流程,全部线上化。
- 数据沉淀与决策支持:记录供应商的报价历史、交货及时率、来料合格率,给采购员和决策层提供量化依据。
- 权限隔离与审计追溯:不同角色(供应商、采购员、审批人、财务)只能看到自己权限范围内的数据,所有操作留痕。
这里有一个容易被忽略的设计前提:SRM系统不只是企业内部系统,它往往还承担供应商门户的角色。也就是说,系统要暴露一部分功能给外部用户使用,比如供应商的注册、报价、确认订单、发货登记、对账确认。这跟传统企业内部管理系统有一个本质区别,就是用户群体不可控,你要在数据安全和易用性之间做平衡。
1.2 为什么选Spring Boot加Vue这套组合
技术选型方面,我直接锁定了Spring Boot加Vue,原因很简单:这套组合的人才储备最充足,招人容易,后续维护成本相对可控。后端的Spring Boot 2.x(现在也可以用3.x)生态成熟,做权限、审批流、定时任务这些典型业务都有非常成熟的方案;前端的Vue配合Element UI或者Element Plus,表单密集型的后台管理页面开发效率极高,这一点比React加Ant Design在某些场景下反而更顺手。
另外一个重要的考量是部署成本。Spring Boot打成一个可执行Jar包,前端Vue构建后由Nginx托管静态资源,再反代一下后端接口,一台2核4G的云服务器就能跑起来。对预算有限的团队来说,这点非常友好。
关于版本选择我多说一句,如果你是新项目,建议直接上Spring Boot 2.7以上或者3.x,搭配JDK17。前端如果不需要兼容老旧浏览器,Vue3加Element Plus是首选,组合API写起来比Vue2的Options API更清晰,特别是处理表格、表单这种复用逻辑多的场景。如果是接手老项目,Vue2加Element UI也完全可以继续维护,没必要为了追新而重构。
2. 后端Spring Boot核心实现
2.1 项目结构与数据库设计
后端我按模块分包,没有用微服务,一个单体应用承载所有模块。项目结构大致如下:
com.company.srm ├── common // 公共类:统一返回、异常处理、工具类 ├── config // 配置:安全配置、定时任务、跨域 ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层(MyBatis-Plus) ├── entity // 实体类 ├── dto // 请求响应对象 └── enums // 枚举:状态、类型定义没有一开始就拆多模块,是因为单体在业务量可控的情况下开发效率最高,后续如果需要拆分,按现在的业务边界切分也不难。这算是我给中小型项目的一个实践建议:架构设计要留扩展余地,但别过度设计。
数据库方面,核心表我列一下,基本覆盖了SRM的主要业务场景:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| srm_supplier | 供应商主数据 | 供应商编码、名称、状态、分类、评级 |
| srm_supplier_qualification | 供应商资质文件 | 文件类型、有效期、关联供应商 |
| srm_supplier_evaluation | 供应商绩效评估 | 评分维度、得分、评估周期 |
| srm_source_inquiry | 询价单主表 | 询价单号、状态、截止时间 |
| srm_source_inquiry_item | 询价单明细 | 物料编码、数量、期望单价 |
| srm_source_quote | 报价单 | 关联询价单、供应商、报价金额 |
| srm_purchase_order | 采购订单主表 | 订单号、供应商、订单状态 |
| srm_purchase_order_item | 采购订单明细 | 物料、数量、单价、交期 |
| srm_arrival_notice | 到货通知 | 关联订单、收货数量、收货日期 |
| srm_settlement_bill | 对账结算单 | 对账周期、应付金额、状态 |
值得专门说的是供应商编码规则。很多团队在开发初期忽略了这个细节,随便用自增主键当供应商编号,后面对接ERP、财务系统时就会非常痛苦。我当时的做法是编码前缀加业务类型加日期再加流水号,比如,SUP-CN-20240512-001。这样从编码上就能直观看出供应商的注册时间,也方便后续系统间集成。
Rbac权限模型是另一块核心。用户表、角色表、菜单权限表、用户角色关联表、角色菜单关联表,这套经典模型在SRM里完全够用。不过要注意,SRM里的权限控制不只是控制菜单和按钮,还要控制数据行权限。举个例子,普通采购员只能看到自己负责品类的供应商和订单,采购经理能看到整个部门的,这就需要单独设计数据权限规则,不能只依赖前后端路由判断。
2.2 供应商管理的核心接口与状态流转
供应商模块是整个SRM的根基。我把供应商状态设计成:注册、待审核、已准入、合作中、暂停、黑名单这么几个状态。注册后由供应商管理员初审,通过后进入合作中;如果合作过程中出现重大质量问题或者违规行为,可以暂停交易或者拉黑。这套状态流转看起来简单,但落地的时候有几个细节容易出问题。
第一个是资质文件的有效期管理。很多供应商的营业执照、生产许可证、质量体系认证是有有效期的,系统里要在资质表上记录到期时间,通过定时任务每天扫描,即将到期前给供应商和采购员发提醒。这个功能我在第一版漏掉了,结果出现了供应商资质过期还在继续交易的情况,后来补上才算完善。
第二个是供应商的准入审批流。不同分类的供应商可能需要不同的审批节点,比如普通办公用品供应商采购经理审批就行,大宗原材料的供应商可能要采购总监加财务会签。我建议这里的审批流设计采用可配置的策略,后台可以按供应商分类维护审批节点和审批人。用现成的工作流引擎比如Flowable当然也可以,但如果你只是做审批流的简单串签和会签,自己写一个基于任务的审批表会更轻量。
核心接口设计示例,供应商注册接口大概是这样的:
@PostMapping("/supplier/register") @ApiOperation("供应商注册") public Result<Long> register(@RequestBody @Valid SupplierRegisterDTO dto) { // 校验统一社会信用代码是否已存在 Supplier existing = supplierMapper.selectOne( new LambdaQueryWrapper<Supplier>() .eq(Supplier::getCreditCode, dto.getCreditCode())); if (existing != null) { return Result.error("该信用代码已注册"); } Supplier supplier = new Supplier(); BeanUtils.copyProperties(dto, supplier); supplier.setSupplierCode(generateSupplierCode()); supplier.setStatus(SupplierStatus.REGISTERED.getCode()); supplierMapper.insert(supplier); // 发送通知给审核员 notifyService.sendSupplierAuditNotice(supplier.getId()); return Result.success(supplier.getId()); }这里有一个业务校验点容易被忽略:统一社会信用代码的唯一性。供应商可能会换联系人、换注册邮箱,但统一信用代码是终身不变的,必须作为唯一索引,防止同一家供应商在系统里注册出多个账号。
审核接口则要支持两种结果:通过和不通过。不通过时要填写驳回原因,并通知供应商补充材料后重新提交。审核通过后,系统自动给供应商分配门户账号,初始密码走短信或者邮件发给供应商联系人,强制要求首次登录修改密码。
2.3 采购订单与状态机设计
采购订单模块是SRM里业务逻辑最重的部分,因为它涉及的状态多、参与角色多。我整理出来的状态流转是这样的:
- 草稿:采购员创建订单但未提交。
- 待供应商确认:订单已发送给供应商,等待对方确认。
- 已确认:供应商确认接受订单。
- 已拒绝:供应商拒绝接单,需要采购员重新处理。
- 送货中:供应商发货,关联到货通知。
- 已收货:仓库入库确认。
- 已完结:对账完成,订单闭环。
这里核心的实现难点是状态机控制。我在开发时用了一个简单的状态机枚举类,定义每个状态允许的流转事件,防止代码里到处写业务判断导致状态错乱。
public enum OrderStatus { DRAFT(0, "草稿"), PENDING_CONFIRM(1, "待供应商确认"), CONFIRMED(2, "已确认"), REJECTED(3, "已拒绝"), SHIPPING(4, "送货中"), RECEIVED(5, "已收货"), FINISHED(6, "已完结"); private final Integer code; private final String desc; private static final Map<Integer, List<OrderStatus>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(DRAFT.code, Arrays.asList(PENDING_CONFIRM, DRAFT)); TRANSITIONS.put(PENDING_CONFIRM.code, Arrays.asList(CONFIRMED, REJECTED)); TRANSITIONS.put(CONFIRMED.code, Arrays.asList(SHIPPING, PENDING_CONFIRM)); TRANSITIONS.put(SHIPPING.code, Arrays.asList(RECEIVED)); TRANSITIONS.put(RECEIVED.code, Arrays.asList(FINISHED)); } public static boolean canTransit(OrderStatus current, OrderStatus target) { return TRANSITIONS.getOrDefault(current.code, Collections.emptyList()).contains(target); } }状态机不只是控制订单流转,还有一个重要作用是可以让异常数据在重启恢复时被识别。比如一个订单卡在“待供应商确认”超过7天,系统自动通过定时任务置为“已过期”状态,通知采购员重新处理。这些逻辑都依赖清晰的状态定义。
关于金额精度问题也提一下。采购订单的金额计算,数据库里一定要用Decimal类型,Java代码里用BigDecimal,禁止直接用double或者float。遇到过供应商报价小数点后第三位截断导致对账差几分钱扯皮的情况。前端展示时保留两位小数就行,但后端计算必须用高精度类型。
3. 前端Vue实现与页面交互
3.1 前端工程结构与权限路由
前端我用Vue3加Element Plus加Vite,工程结构这样组织:
src ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 公共组件 ├── layout // 布局组件 ├── router // 路由配置 ├── store // 状态管理(Pinia) ├── utils // 工具函数 └── views // 页面视图 ├── dashboard // 工作台 ├── supplier // 供应商管理 ├── sourcing // 寻源管理 ├── order // 采购订单 └── settlement // 对账结算权限路由是整个前端的关键。我的做法是登录后从后端拿到当前用户的角色和权限标识列表,前端在路由守卫里动态判断当前路由是否有权限。
router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('srm_token'); if (!token && to.path !== '/login') { next('/login'); return; } if (token && !userStore.userInfo) { await userStore.fetchUserInfo(); } const permission = userStore.permissions; if (to.meta.permission && !permission.includes(to.meta.permission)) { next('/403'); return; } next(); });这里有个实际开发中的考量:按钮级别的权限要不要做?我的答案是如果你的系统没有开放给外部第三方开发者,按钮级权限做了只会增加维护成本。SRM系统里角色是相对固定的,做路由级加菜单级权限控制就够了。真正的数据安全靠后端接口的鉴权,前端隐藏按钮只是改善体验,不是安全措施。
供应商门户页面和企业内部管理页面,我分了两个Layout。供应商登录后看到的是待办提醒(待报价、待确认订单、待发货)、历史报价、账户信息;内部采购员看到的是供应商档案、询价管理、订单管理、报表中心。这两套界面如果混在一个布局里,权限判断会变得很复杂,分开处理更干净。
3.2 供应商门户与采购订单页面
供应商门户里最常用的页面是报价页面。这里我踩过一个交互设计的坑:第一版报价页面做成了单元格内直接编辑,供应商操作起来容易误触,而且报价金额校验不直观。后来改成弹窗编辑加校验,提交前二次确认,误操作率明显下降。
报价页面的核心交互逻辑是这样的:供应商从待办列表进入一张询价单,看到物料明细,逐项填写含税单价、税率、交货周期、起订量,最后汇总报价总金额。前端在提交前要做一轮格式校验,单价必须大于零,交货周期不能为空。同时要支持保存草稿,因为供应商可能要查内部成本后才能填完一份报价,一次性填完并不现实。
采购订单页面按状态分Tab展示:待确认、已确认、送货中、已收货、已完结、已拒绝。每个Tab对应一个列表,列表支持按订单号、供应商名称、创建时间筛选。订单详情页展示物料明细、金额明细、附件信息。这里有个体验细节,所有金额字段在前端统一用千分位格式化,避免采购员看一长串数字眼晕。
表格组件的封装建议直接做成公共组件。Element Plus的ElTable如果每个页面都重复写分页、加载状态、空数据提示,代码会非常冗余。我封装了一个ProTable组件,把分页、列配置、搜索条件、导出按钮都内置进去,各页面只需要传表格列配置项和查询接口,效率能提升不少。
3.3 前后端联调时那些容易扯皮的细节
接口联调是前后端协作最容易出问题的环节,这里面有几个问题我几乎每次做项目都会遇到,专门说一下。
第一个是时间格式的传递。后端返回的时间格式默认可能是2024-05-12T10:30:00,但前端期望的是2024-05-12 10:30。我建议后端在配置层统一设定时间格式化格式,避免每个接口单独处理。Spring Boot里可以在配置文件中全局配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8但注意,如果前端需要传时间参数给后端做查询,我建议统一用字符串类型接收,后端再转成LocalDateTime。直接在接口参数里用Date接收,格式解析很容易出问题。
第二个是空值的处理。MyBatis-Plus查询结果里Decimal字段为空时,JSON序列化会输出null,前端表格展示会显示空白,容易让用户以为是数据缺失。实际上可能是该数据确实没有值。我在后端统一把BigDecimal类型的空值序列化为0,用Jackson的全局配置或者字段注解都行。
第三个是接口响应结构统一。无论是正常返回还是业务异常,接口响应结构必须是{ code, message, data }这种统一格式。前端Axios拦截器统一处理code非200的情况,弹出Message错误提示。这个看似基础的习惯,在实际项目中经常因为赶进度被破坏,后面再做接口联调时痛苦得不行。
4. 核心业务流程与状态流转
4.1 询比价流程实现
询比价是SRM系统里使用频率最高的采购寻源功能。业务流程是:采购员创建询价单,明确采购物料、数量、期望交期、报价截止时间,系统向符合条件的供应商群发询价通知;供应商在截止时间前提交报价;截止后采购员综合比价,选择中标供应商,系统生成采购订单。
从技术实现角度看,有以下几个重点:
一是供应商自动匹配逻辑。创建询价单时,系统要根据物料品类自动带出该类目下有合作资格且状态正常的供应商列表。这个匹配逻辑在数据库层面实施,是基于物料分类和供应商经营范围分类的对应关系。如果前期没有建设物料分类体系和供应商经营分类,这个功能很难做。所以我在项目初期就建议客户梳理物料分类,这直接决定了后续寻源功能的可用性。
二是报价截止时间的处理。前端通过倒计时提醒供应商剩余时间,后端在截止时间后定时任务自动关闭报价通道。这里有个隐藏问题:供应商可能在截止时间前最后一秒提交报价,可能导致数据库写入延迟。我当时的做法是后端接口在接收到报价时先判断当前时间是否超过截止时间,超过直接拒绝,毫秒级的并发问题通过数据库唯一索引兜底。
三是比价逻辑。系统把所有有效报价按总价升序展示,同时展示每家供应商的历史合作评分、交货及时率等参考指标。这里的排序只是辅助,最终选标由采购员人工决策,系统不做自动定标。这是多数制造企业的真实需求,因为价格不是唯一决定因素,还要考虑供应商关系、质量、交期稳定性等软性指标。
4.2 采购订单审批流
采购订单审批流的实现,我用了动态审批节点的设计。企业里不同金额的订单审批层级不同:小于一万元的采购订单采购经理审批即可;一万元到五万元需要采购总监审批;超过五万元需要总经理审批。这个金额阈值是后台可以配置的,不能写死在代码里。
实现方式是建一张审批配置表,存储规则类型、条件值、审批人角色。提交订单时,根据订单金额匹配审批链,生成审批任务列表。审批人逐级审批,所有节点通过后订单状态才流转为待发送给供应商。
另外要处理审批驳回到某一步的场景。如果二级审批人驳回了,订单应该回到起草人手里修改,而不是重新从一级开始审批。这个流程在代码里要区分“驳回”和“退回”。
有个容易被忽略的点是审批意见必须留痕。每个审批节点要记录审批人、处理时间、意见以及附件材料。这在后续审计或者内外审时非常重要。很多团队在开发审批流时只关注状态流转,忘记记录审批明细,后面补数据时就很麻烦。
4.3 对账结算与发票管理
对账结算是采购链路的最后一环,也是财务最关心的环节。业务逻辑是:每笔订单收货后产生应付数据,供应商定期汇总一定时期内的送货明细、收货记录,向企业发起对账申请。企业内部人员核对无误后确认应付金额,供应商开票,财务安排付款。
技术实现上有几个难点:
第一,对账单的数据来源要准确。对账单明细必须来源于已经收货确认的订单明细,不能手工录入,否则对账数据就失去了可信度。我在设计时把对账单与收货记录强关联,未收货的订单明细无法出现在对账单中。
第二,金额计算规则要统一。含税与不含税金额的转换、税率的叠加、运费与杂费的拆分,这些规则必须在系统里有统一的定义。税务政策发生变化时,要能通过配置修改,而不是改动代码。比如某类物料的税率从13%调整到11%,改一下配置项就可以了。
第三,对账状态与付款状态要同步。对账单确认后生成付款申请,付款完成后反写对账单的付款状态。如果企业用的有独立财务系统,这里一般通过接口对接或者定期同步脚本实现。
5. 常见问题与排查实录
5.1 并发下的数据一致性问题
SRM系统里有两个典型的并发场景。第一个是多个供应商同时提交报价,第二个是同一采购订单被多个操作人员同时处理。
先看报价并发。如果同一个询价单下有多个供应商同时对同一个物料报价,正常情况下不同供应商是独立的报价记录,不存在数据冲突。但如果是同一家供应商的两个账号同时操作(实际上不太可能,因为账号是按供应商隔离的),或者同一个供应商在一次询价单里重复提交报价,就可能产生重复记录。
我的解决办法是在数据库表上加联合唯一索引,比如(inquiry_id, supplier_id, item_id)加物料明细维度。当重复提交时直接报唯一键冲突,程序捕获后转换成友好提示。这种兜底手段比在业务代码里逐层判断要可靠得多。
再看订单并发。采购员修改订单状态的同时,供应商可能在确认订单,理论上有状态竞争。因为订单状态机已经限制了非法流转,比如草稿状态不能直接跳到已收货,所以在接口层面再通过乐观锁控制版本号即可。实体里加一个version字段,更新时带上版本号条件:
UPDATE srm_purchase_order SET status = #{newStatus}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion}影响行数为0时表示数据已被其他人修改,需要刷新重试。这个方法简单有效,多花五分钟能避免很多线上问题。
5.2 数据权限设计不到位带来的坑
SRM系统的数据权限比普通管理系统的角色权限更复杂。原因是数据天然存在归属关系:一个采购员负责某些供应商,一个供应商只能看到自己的数据,一个财务可以看所有订单的金额但不能看供应商的联系方式。
我第一版只做了角色菜单权限,结果采购员登录后能看到全公司所有供应商的报价和订单,客户直接不满意。后来重新设计了数据权限模型,给角色增加数据范围概念:
- 全部数据:财务、管理员。
- 本部门数据:部门经理。
- 本人数据:普通采购员。
- 按供应商范围:供应商相关角色只能看到自己的数据。
实现方式是在查询SQL里动态拼接数据权限条件。我用MyBatis-Plus的Interceptor做数据权限拦截,根据当前登录用户的数据权限类型自动追加查询条件。比如普通采购员查订单列表时,自动在SQL后面拼接AND create_by = 当前用户ID。
这里要注意一个安全细节:前端的筛选条件参数是用户可以篡改的,比如用户通过接口传?supplierId=xxx尝试查看别人的供应商数据,后端必须在查询时再做一层数据权限校验。所有涉及按供应商查询的接口,都要把当前登录用户能访问的供应商ID列表作为隐含过滤条件,不能完全信任前端传参。
5.3 报表查询与性能优化
SRM系统的报表模块很吃数据库性能。采购汇总报表、供应商绩效报表、价格波动分析,这些报表动辄要关联五六张表做聚合查询,数据量上来之后,接口响应经常超过五秒。
我经历过的真实场景是,供应商绩效报表跑了十五秒都没返回,数据库CPU直接飙到80%以上。当时查了一下SQL执行计划,发现有几个索引没生效,明细表是全表扫描。优化思路大概分了几步:
第一步,给高频查询字段建立联合索引。比如订单表按(supplier_id, create_time, status)建联合索引,查询供应商在某时间段内的订单状态时,走索引效率高非常多。
第二步,不直接查业务表做报表。引入一张汇总表,通过定时任务每隔一小时把订单明细按供应商、按日、按物料分类统计一次,报表接口只查汇总表。这算是空间换时间的典型思路。实时性要求不高的报表完全够用。
第三步,前端处理大数据量列表时启用虚拟滚动。Element Plus的表格一万行以上就会有明显卡顿,我给报表表格组件加了虚拟滚动,渲染性能提升明显。
除了这三步,还有几个可以立竿见影的小优化:查询列表接口避免使用select *,明确查询字段,避免不必要的字段传输;分页查询的count语句如果很慢,可以考虑改用二级缓存记录计数。
5.4 权限与数据隔离的五个高频问题速查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 供应商登录后看到其他供应商的报价信息 | 数据权限拦截器未生效,查询未追加数据范围条件 | 全局检查供应商端所有查询接口,统一走数据权限拦截器 |
| 部门经理看不到下属采购员的数据 | 数据权限规则配置有误,部门关系维护不完整 | 确认组织架构部门关联字段在用户表正确维护 |
| 同一个订单被两个人同时编辑覆盖 | 缺少乐观锁版本控制 | 更新SQL增加version条件,失败时提示重新编辑 |
| 供应商报价成功后页面还一直转圈 | 后端接口超时,可能是报价接口里发了耗时较长的通知短信 | 将通知类操作改为异步执行或消息队列 |
| 报表数据显示的是几小时前的数据 | 定时汇总任务未执行或执行失败 | 检查定时任务日志,补充手动触发接口做补偿 |
6. 这套系统的扩展方向
我自己做完这个项目后,明显的体会是SRM系统的核心价值不在代码量多少,而在业务规则梳理得是否清晰。
如果后面要继续扩展,我觉得有几个方向值得关注。一是采购寻源环节可以增加供应商自动推荐评分,把历史绩效数据纳入评分模型。二是对账环节可以接入电子发票接口,实现发票自动验真和核销。三是供应商门户可以增加移动端适配,很多供应商的业务人员习惯用手机处理订单,做个H5版本的轻量门户对提升协同效率帮助很大。四是和ERP系统的对接,通过接口把采购订单、收货数据同步到财务系统,减少人工重复录入。
写代码的人常有一个误区,觉得系统上线就万事大吉,其实真正的项目交付是从上线后才开始的。供应商在使用门户时提出的反馈,采购员在操作过程中觉得别扭的交互,才是下一次迭代最宝贵的信息来源。做SRM这类业务系统,贴近业务方、听懂他们的痛点,比写漂亮的代码重要得多。