去年帮一家制造企业做采购数字化改造,老板上来就直说:能不能拿一套供应商在线询价报价采购招标管理系统源码,自己改一改就让供应商在线上报价、比价、走招标?我没急着拍胸脯,而是先问他:你现在的流程里,最痛的是不是询价靠微信群、报价靠Excel回传、比价靠人工排序,经常报错价没人知道,供应商资质过期了也没人提醒?他愣了一下,说你怎么知道。这套系统看着名字很长,拆开看其实每个词都对应着一个具体的业务痛点:供应商要管、询价要有流程、报价要能留痕、采购招标要合法合规。它的本质,不是给你一个代码仓库下载下来就能用,而是把一个企业采购部门从"打电话发邮件"变成"线上发起、线上响应、线上定标"的完整业务闭环。
这篇内容我不打算给你贴一堆无用的项目介绍,而是从一个开发者角度讲清楚这套源码该怎么理解、怎么拆解、怎么真正落地。适合正在评估自研采购系统的技术负责人,也适合接外包后需要对采购业务建模的程序员。我尽量把业务建模、表结构、核心接口、避坑经验这些能直接抄作业的东西都放进来。
1. 别急着写代码:先想清楚这套采购系统解决的是谁的什么痛点
1.1 供应商在线询价报价的核心业务场景
很多人一看到"在线询价报价系统"就觉得是个表单工具,供应商填一个价格,采购员汇总一下,完事。真正的企业采购远没有这么简单。我在实际项目里遇到的真实场景是这样的:采购员A负责非生产物料,比如包装箱、刀具、办公耗材,一个月要发出去几十份询价单,每份询价单里有好几个料号,每个料号还要指定技术要求、交期、付款方式。他用Excel做询价单,用邮件发给几十家供应商,然后天天盯邮箱收报价,漏一份都不行。
这套源码要解决的第一件事,就是把上面这套人工动作变成系统动作。供应商在线询价,指的是采购方在系统里创建一份询价单,系统自动通知符合条件的供应商;供应商在规定的截标时间之前登录系统填写报价,可以改、可以撤回;截标时间一到,系统自动关闭报价入口。整个过程不需要发一封邮件,不需要人工对账,所有报价记录都有时间戳和操作人。
如果只做到这里,那确实是个表单工具。真正有价值的是后面的比价和定标环节。系统要能按料号维度把各家供应商的报价拉平了对比,同样的规格、同样的数量、同样的含税条件,谁的裸价低,谁的综合成本低,系统给出比价表,采购员只需要在比价表上做决策。这套逻辑一旦跑通,采购部门最耗时的日常询价工作量能砍掉百分之七八十。
1.2 招标管理与询价报价的区别和联系
很多人会把"询价"和"招标"混在一起,觉得都是让供应商报价,谁便宜选谁。实际业务里两者界限非常清晰。询价通常用于采购金额不大、规格明确、市场上供应商充足的物料,核心诉求是快、省,流程可以轻。招标则用于金额大、技术要求复杂、需要多家供应商竞争的采购项目,核心诉求是合规、可追溯、经得起审计,流程要重。
采购招标管理系统源码里的招标流程,一般包含几个关键步骤:招标立项、编制招标文件、发布招标公告、供应商报名、资格预审、回标、开标、评标、定标、中标公示。每一步都有对应的角色和权限,比如只有评标专家能看评分维度,只有采购经理能调整评标权重,供应商回标之后任何人都不能改自己的标书内容。这套流程在源码层面要支持配置化,不能写死,因为每个公司的采购管理办法都不一样。
我见到很多初次开发的团队,容易把询价流程套在招标上面,结果做一个"简化版招标",核心的合规留痕全丢了,甲方验收时直接打回。反过来也有把询价做成全套招标的,流程重、响应慢,采购员用两天就嫌烦,又回到Excel。所以做这套系统前,先跟业务方确认一个关键问题:这个项目里询价和招标并存,还是只做其中一个?这直接影响你的表设计和管理端菜单结构。
1.3 源码类系统与SaaS采购平台的差异
网上有各种采购平台可以注册即用,为什么还有人坚持要源码?这个问题的答案其实决定了你的技术路线。我接触过的大多数企业客户,理由基本有三个:一是数据安全要求高,采购价格和供应商目录属于核心经营数据,不想放在第三方平台;二是想要深度定制,比如要和内部OA、ERP打通,要把采购数据接进BI做分析;三是一次性买断和按年订阅的成本模型不同,很多老板更接受买断源码、内部维护。
但说句实在话,买源码不是买个省心。你自己部署这套系统,意味着后续的服务器运维、安全补丁、功能迭代全都要有人负责。很多企业买完源码,过了两年二次开发的人离职了,系统就烂在那里。所以如果你正在考虑自己部署,一定要确保源码的技术栈不是太冷门。Java Spring Boot是目前主流的选择,社区活跃、招人容易、配套方案成熟,这也是我后面所有示例都用Java体系来写的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心业务建模:询价、报价、招标、定标的状态流转
2.1 询价单生命周期:从草稿到核价关闭
业务建模最忌讳一上来就建表,我习惯先把每个核心单据的状态机画清楚。一套完整的询价单,状态流转大概是这样的:
| 状态 | 触发动作 | 能看的角色 | 说明 |
|---|---|---|---|
| 草稿 | 采购员创建 | 采购员本人、采购经理 | 可以随意编辑,不通知供应商 |
| 已发布 | 点击发布,系统通知供应商 | 所有供应商、采购员 | 供应商开始收到消息,可报价 |
| 已截标 | 到达报价截止时间 | 采购员、采购经理 | 供应商端报价入口关闭 |
| 比价中 | 采购员进入比价页面 | 采购员、采购经理 | 允许多轮比价、与供应商议价 |
| 已授标 | 选择中标供应商并确认 | 采购员、采购经理 | 生成中标结果,通知供应商 |
| 已关闭 | 采购员手动关闭 | 采购员、采购经理 | 可能因为取消采购或重复下单 |
一个细节很容易被忽视:从"已发布"到"已截标"之间,供应商改过几次报价,每次改的价格、修改时间、操作IP都要留痕。采购方和供应商发生争议时,这些记录就是唯一的证据。我在源码里把报价历史单独存了一张表,每次修改不覆盖原记录,而是插入一条新记录,用版本号区分。这样不仅满足了审计需求,后面做报价分析时也有数据可用。
2.2 报价环节的关键约束:截标时间、报价规则、防篡改
报价环节是整个系统里最容易出bug的地方,因为很多约束是交叉的。截标时间是第一个约束,系统要做两件事:发布询价时记录截止时间,供应商每次打开报价页面前校验当前时间是否超时;同时服务端在提交报价接口里再校验一次。单纯前端校验必死,因为供应商可能截标前几秒打开页面、截标后才提交,网络有延迟。
报价规则是第二个约束。比如"同一询价单里的不同料号,必须全部报价才能提交",或者"所有报价必须含税到厂价,价格含运费"。这些规则必须做成可配置项,而不是写死在代码里。我用的是策略模式,每个规则一个实现类,配置项里以规则的编码列表控制启停。比如规则编码NO_PRICE_IF_BLANK表示不允许空白报价,规则编码MIN_ORDER_QTY_VALIDATE表示校验最小起订量。这样业务方想改规则时,不需要改代码,改配置就能生效。
防篡改是第三个约束。供应商提交报价后,如果允许他们在截标前修改,那么数据库里存的一定要包含每一次修改的完整快照。如果业务上允许截标后议价,那议价后的报价必须单独标记为"议价价",和原始的"报价价"区分开。这样才能在比价表上同时展示"初始报价"和"最终成交价",给采购决策提供完整信息。
2.3 招标流程拆解:发标、回标、开标、评标、定标
招标流程比询价多了一个很重要的概念:只有通过资格预审的供应商才有资格回标。这意味着状态管理要拆成两个维度:项目维度和供应商维度。
项目维度的状态是:立项、编制标书、发布公告、资格预审进行中、回标中、开标、评标中、定标公示、已结束。供应商维度的状态是:已报名、待资格预审、资格预审通过、资格预审不通过、已下载标书、已回标、已撤回回标。这两个维度在数据库里是分开的,项目表存项目状态,供应商报名表存供应商状态,两个状态联动但没有依赖关系。
开标本身上是个很敏感的动作。有的企业要求开标时所有投标供应商在场当众拆封,有的企业允许线上开标只要系统自动解密。源码层面要做的,是给投标文件加密存储,开标时间到之前,任何人不能查看标书内容,包括管理员。我之前的做法是:供应商上传标书后用AES加密存文件服务器,密钥由系统生成后再用项目负责人的公钥加密保存,只有开标动作触发后才把密钥释放出来。这个设计会复杂一些,但能实打实避免很多合规风险。
2.4 状态机的代码化表达与数据库字段联动
不要用一堆if else去判断状态能不能流转,状态多了迟早会乱。我会在代码里用一个枚举类定义所有状态,再写一个流转规则表,把"当前状态+动作=目标状态"这个映射关系维护在一个二维数组里。每次状态变更时先查规则表,不匹配直接抛异常。这样做的好处是,业务方提一个"已发布状态能不能撤回草稿"的需求时,改一行数组就能搞定。
数据库里对应的是每个单据上必然有个status字段,再加一个updated_by和updated_time。但光有status还不够,很多场景需要保留"上一步状态",方便回退。我一般是保留一张状态变更历史表,字段包括:业务单号、从状态、到状态、操作人、操作时间、变更原因。这样即使状态机里有漏洞,至少能看清楚数据是怎么变成今天这样的。
3. 技术选型与整体架构:一套能商用落地的源码应该怎么组织
3.1 选型逻辑:单体优先还是微服务
采购招标系统要不要上微服务?我的回答是,除非你的验证目标是写简历,否则初期一定要从单体架构开始。一个中型制造企业的采购并发量,远没到单体扛不住的程度。真正的复杂度在业务逻辑,不在并发。单体应用一样可以把供应商模块、询价模块、招标模块、系统管理模块拆成清晰的package或者module,保持边界,后面真需要拆服务时再拆。
我见过一个反面案例:客户坚持要微服务,花了两三个月搭注册中心、网关、配置中心、链路追踪,业务页面一个都没写,供应商一听要登录系统,第二天就开始催。最后我建议他们把服务先合并回单体,用模块化保留边界,两周就把核心询价报价流程跑通了。技术选型的问题,本质上不是"哪个更好",而是"哪个在现阶段更匹配你的团队和业务阶段"。
对于这种系统,我常用的后端技术栈是:Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis + Spring Security + JWT。前端管理端用Vue3 + Element Plus,供应商端如果要做小程序,就用uni-app一套代码同时出H5和小程序。文件存储用MinIO自建,不走云OSS,免得客户换环境又要改配置。部署用Docker Compose,一台4核8G的服务器就能把整套系统跑起来。
3.2 后端核心模块划分与权限模型
后端模块我是按照业务域来切的,不是按技术层切:
- supplier-service:供应商注册、资质管理、供应商分级、淘汰
- purchase-core:询价单、报价单、比价、询价授标
- tender-core:招标项目、标书、回标、开标、评标、定标
- system-base:用户、角色、菜单、组织架构、字典、操作日志
权限模型必须支持RBAC,而且要做到数据权限的维度。比如"采购员A只能看到自己创建的询价单""采购员B可以看全部询价单""供应商只能看到发给自己的询价单"。如果只用接口级别的权限控制,开发阶段没什么问题,一上线就会出现互相能看到对方询价的严重事故。我在项目里是用一个数据权限切面来解决的,通过注解标明当前接口的数据权限范围,SQL层面动态拼接条件。
3.3 前端与移动端形态:管理端、供应商端、审批端
这套系统的用户角色差异非常大,前端不能只做一个大而全的后台。我通常拆成三个端:
- 管理端:给采购员、采购经理、财务、管理层用,核心页面是询价单列表、比价表、招标项目、供应商档案、数据看板。
- 供应商端:给供应商工作人员用,核心页面是询价报价、项目报名、标书下载、回标上传、中标通知、订单协同。供应商端尽量简洁,注册流程要能在10分钟内完成。
- 审批端:钉钉/企业微信风格的待办审批页,或者直接在管理端集成待办中心。审批流我很少自己从零写,而是集成Flowable工作流引擎,用流程定义去管理询价审批、招标立项审批、定标审批。
移动端优先做供应商端。这是我在实际项目里发现的规律:采购员在电脑前坐一天,供应商老板一天到晚在外面跑。如果供应商端只能电脑操作,报价及时性会很差。用uni-app把供应商端的小程序和H5一起出了,报价提醒通过短信/微信模板消息推送,供应商体验才能上来。
3.4 目录结构与源码组织技巧
这里我给一个可以直接参考的Module结构:
text复制purchase-system/
├── pom.xml
├── purchase-admin/ # 管理端接口模块
│ └── src/main/java/com/example/admin/
│ ├── controller/
│ ├── service/
│ └── mapper/
├── purchase-supplier-api/ # 供应商端接口模块
│ └── src/main/java/com/example/supplier/
│ ├── controller/
│ └── service/
├── purchase-core/ # 核心业务逻辑模块
│ └── src/main/java/com/example/core/
│ ├── model/ # 实体与DTO
│ ├── enums/ # 状态机枚举
│ ├── engine/ # 比价引擎、规则引擎
│ └── utils/
├── purchase-system/ # 系统管理模块
├── purchase-framework/ # 框架配置,安全、Redis、OSS等
└── sql/ # 初始化脚本
源码组织上有一个容易被忽略的问题:不要把数据库脚本和源码仓库分开管理。采购系统上线后必然要升级,到时候跑在一台老掉了牙的服务器上的生产库,和你的开发库早就不一样了。把DDL和初始化数据按版本号放在sql目录下,每次发版执行对应版本的脚本,能省掉无穷无尽的"生产环境怎么多了一个字段"的灾难。
4. 数据模型设计:金额、供应商、报价单这三张表决定了系统下限
4.1 供应商档案表与资质有效期管理
供应商表是整个系统的主数据,设计得好不好直接影响后面所有模块。我建议的核心字段至少包含:
- 供应商编号:全局唯一,用业务规则生成,比如SUP+年月日+流水号
- 供应商名称、统一社会信用代码:唯一索引
- 联系人、手机号、邮箱
- 开户行、银行账号
- 供货分类:用树形字典,比如"包装类/纸箱""五金类/螺丝"
- 供应商等级:战略/核心/一般/待开发
- 状态:潜在/合作中/暂停/淘汰
大部分团队容易漏掉的是资质有效期管理。供应商的ISO证书、安全生产许可证、代理授权书都有有效期,过期了就必须暂停交易。光有证书图片是不够的,要在抬头信息表里加一张供应商资质子表,存资质类型、证书编号、发证日期、到期日期。用定时任务每天扫描,快到期时给供应商和采购员发提醒。就这一个功能,客户满意度能提升好几个档次。
供应商的登录账号也是特别容易出问题的地方。一个供应商公司可能有多个人登录操作,比如业务员负责报价、财务负责确认收款账户,所以账号设计一定是"供应商主数据+多用户账号",每个账号有独立的角色,比如报价员、管理员。不能做成一个公司一个账号所有人共用。
4.2 询价单、报价单、招标项目的主从表结构
采购单据和订单一样,天然是主从结构。询价单头存的是公共信息:询价编号、标题、采购部门、采购员、报价截止时间、交货地址、付款方式、备注、状态。询价单明细存的是本次询价的每个料件:料号、品名、规格型号、单位、数量、期望交期、技术要求附件、参考价、起订量。
供应商报价表也是主从结构,头是报价单编号、询价单编号、供应商ID、总报价金额、币种、税率、报价有效期、备注。从表是报价单明细:询价单明细ID、单价、总价、可交货日期、是否完全满足规格、附加说明。
招标项目表与询价表类似,但要额外存标书信息:招标编号、项目名称、采购方式、评标方法(最低价/综合评分)、最高限价、投标保证金金额、开标时间、评标专家组成。招标的报价表字段会更多,比如投标总价、分项报价、质保期、业绩说明等。
有一个细节是报价明细必须引用到询价单明细的ID,而不是只存料号。因为同一个料号可能在询价单里出现了两次,一次是不同交期,一次是不同技术规格。如果只存料号,对账时就会错乱。这个外键关系要写清楚,比价引擎才能按明细维度对得上。
4.3 金额精度与币种处理
采购系统里金额处理不当,审计时会被问到怀疑人生。我踩过的坑是直接用double存价格,看上去没毛病,数据库里出来是0.1+0.2=0.30000000000000004,比价差一分钱,财务同事当场崩溃。所以在数据库层面,金额字段一律用DECIMAL(18, 4),Java实体一律用BigDecimal。注意是DECIMAL(18, 4),不是DECIMAL(10, 2),因为价格经常会有小数点后三位甚至四位的情况,比如含税单价57.125,四舍五入到分要在展示层做,数据库要把原始精度保留住。
币种问题同样是关键。跨境采购有美元、欧元、人民币,不同币种的报价不能直接比大小。我一般在询价单头上指定本币币种,报价单头上记录供应商报价币种,同时在报价明细中冗余一个本币折算金额。汇率在发布询价时锁定,避免供应商报价后汇率变动导致比价结果失真。
字段清单里要明确区分"报价金额"和"含税总价"、"裸价"和"结算价"。很多采购系统只有单价没有税率,等到财务要入账时就抓瞎了。报价明细里必须同时存:不含税单价、税率、含税单价、含税总价,这四个字段缺一不可。
4.4 多轮报价与历史版本保留
实际采购里,一轮报价往往不够。比价后采购员可能觉得价格偏高,会和供应商议价,供应商可能愿意降价5%。如果系统只支持"报价一次,定终身",那采购员就只能在线下议价,再把最终价手工录入系统。这个流程漏洞很大:线下议价没有记录,价格没有走线上审批,审计根本查不到。
正确的做法是把报价本身设计成多轮的。询价单可以开启"允许二次报价",第一次报价截止后进入比价,采购员对各家的价格有初步印象,然后可以选择性地邀请部分供应商进入二次报价。第二轮报价同样有截标时间,同样有历史记录。每轮报价独立版本化,比价表上能清晰地看到"第一轮报价"和"最终报价"。
历史版本保留不仅是为了追溯,还有一个很实际的用途:供应商在截标前频繁改价,如果只展示最终版,采购员根本看不清他是不是在试探底价。展示版本轨迹之后,供应商的心理价位变化一目了然。这个数据对后续谈判非常有价值。
5. 核心流程的代码化实现思路:从发布询价到比价授标
5.1 发布询价单的后端校验与并发控制
发布询价这个动作看起来就是改个状态、发个通知,实际上有一串校验要做:明细不能为空,数量必须大于0,报价截止时间必须晚于当前时间,供应商分组至少选一个,存在有效的已审核供应商,相同物料和相同供应商在同一个询价单里不能重复出现。
发布动作要加事务和锁。我常用的方案是在询价单头上加一个version字段,用乐观锁控制并发更新。发布时执行UPDATE语句带上WHERE status = 'DRAFT' AND version = 1,如果影响行数为0,说明已经被别人发布了,直接提示"操作冲突,请刷新页面"。不要用select后再update,那个缝隙里就会跑出脏数据。
通知供应商的方式,不要在主事务里直接调短信接口。发布询价的事务提交后,把"待通知供应商"的消息写到一张通知任务表,由后台任务异步发送短信、微信模板消息、站内信。加这一步的原因很现实:短信接口不稳定,如果在通知环节失败,导致主事务回滚,那询价单发布不出去,供应商看到的就是假数据。消息任务表还能记录每条通知的发送状态,失败重试三次后标记失败,人工介入。
5.2 供应商报价的幂等设计与截标控制
报价接口是整个系统里最容易出并发问题的接口。供应商可能在截标前最后一秒同时提交报价,采购员可能在同一个瞬间打开比价页面。如果服务端不做拦截,就可能出现"截标前报价成功但显示未报价"或者"供应商重复提交产生两条报价"的情况。
我的方案是这样的:
java复制@Transactional
public QuoteResult submitQuote(QuoteSubmitRequest request, String supplierId) {
RfqOrder rfq = rfqOrderMapper.selectById(request.getRfqId());
// 截标时间校验,以数据库时间为准
if (rfq.getQuoteEndTime().isBefore(LocalDateTime.now())) {
throw new BizException("已超过报价截止时间");
}
// 幂等控制:同一个询价单+供应商+版本号只能提交一次
String idempotentKey = rfq.getId() + ":" + supplierId + ":" + request.getQuoteVersion();
boolean first = redisTemplate.opsForValue()
.setIfAbsent("quote:idem:" + idempotentKey, "1", Duration.ofMinutes(10));
if (!first) {
throw new BizException("请勿重复提交报价");
}
// 保存报价主表和明细,version+1
return doSaveQuote(rfq, request, supplierId);
}
这里用Redis的setIfAbsent做分布式锁,配合数据库唯一索引兜底。不要把截标时间判断放在代码里就完事,一定要再建一条数据库约束:在报价表上加CHECK或者用触发器在截标后禁止写入。有些团队用的是Oracle或者PG,用规则约束更稳。MySQL的话就在插入前用事务锁住询价单行记录,确保按时间顺序处理。
5.3 比价逻辑:最低价、综合评分、自动授标
比价是采购员最核心的决策页面,代码逻辑要分三层。
第一层是拉平数据。把报价单、报价明细、供应商档案、资质信息关联起来,按询价单明细ID分组。每个料号下方列出所有供应商的报价,含税价、税率、交期、付款条件。如果有的供应商少报了某个明细,页面上要显式标红"未报价",不能无声地忽略。
第二层是算分。最低价简单,每个料号价格最低的供应商得满分,其他按比例扣分。综合评分要支持分数模板:价格权重60分、交期权重20分、供应商资质10分、历史合作10分。每一项的打分逻辑都做成可配置规则,采购经理在管理后台可以调整权重。注意评分规则要存档,定标报告里必须能看到当时的评分明细,否则审计时讲不清楚为什么没选最低价。
第三层是决定是否自动授标。如果采购单配置了"最低价自动授标",系统在比价页一键生成授标结果;如果配置了"人工决策",则比价页提供"拟中标供应商"选择框,采购员确认后进入审批流。自动授标要做好阻断条件:如果最低价供应商资质过期、或者有黑名单记录、或者报价有附加条件,系统必须弹窗提醒,不能默默授标。
5.4 评标与定标:多人评分与防串通
评标通常用于招标项目。一个招标项目有多个评标专家,每个专家对每家供应商打分,最后汇总。评标阶段的代码要处理两个关键问题:一是每个专家的打分互相不可见,二是汇总时不能出现某一个专家操纵结果。
实现上,我建议把评分表按专家维度拆开:专家评分明细表里存expert_id、supplier_id、evaluation_item_id、score,只有开标汇总时才能看到所有人的评分。同一个专家不能重复提交同一家供应商的评分,提交后修改必须有审批或留痕。汇总算法要支持去掉最高分和最低分再取平均,这是很多企业评标办法里的硬性规定。
定标动作完成后,状态流转到"已定标",系统自动生成中标通知书供下载。同时把落标通知发给其他供应商,这是采购合规的要求,很多系统漏掉了。落标通知要个性化:同一项目里,不同供应商看到的中标价格不能都一样,防止泄露中标价导致供应商集体议价。数据权限在这里同样要做好,一个供应商只能看自己的中标结果。
6. 开发中绕不开的坑:权限、日志、金额、并发,我踩过的都在这
6.1 越权访问:横向权限漏洞怎么堵
采购系统的数据天然是敏感的:采购员A不想让采购员B能看到自己的谈判底价,供应商A不想让供应商B看到自己的报价。这类"只能操作自己的数据"的权限,叫横向数据权限,是最容易出漏洞的地方。很多开发只在接口上加了登录校验,没有校验数据归属,导致供应商只要改一下询价单ID,就能看到别人的报价。我在源码里专门做了一个数据权限切面:
java复制@Aspect
@Component
public class DataScopeAspect {
@Before("@annotation(dataScope)")
public void checkDataScope(JoinPoint point, DataScope dataScope) {
Object[] args = point.getArgs();
for (Object arg : args) {
if (arg instanceof RfqQueryRequest) {
RfqQueryRequest req = (RfqQueryRequest) arg;
// 强制追加当前用户的数据范围条件
req.setDataScope(SecurityUtils.buildDataScope());
}
}
}
}
核心原则是:数据范围条件必须在Service层由后端强制注入,不能信前端传参。前端传过来的供应商ID只用于查询条件,但最终的SQL里会强制加上"当前登录用户所属供应商ID=xxx"或者"当前采购员ID=xxx"。我会在开发自测清单里专门加一条越权测试用例,直接从供应商A的浏览器DevTools里改动询价单ID去请求供应商B的报价数据,以此验证权限是否真的隔离。
6.2 报价表金额精度问题:为什么BigDecimal还不够
谁都知道金额要用BigDecimal,但实际项目里还是不断有人踩坑。真正的问题往往出现在两个地方:一是把字符串变成BigDecimal时用了new BigDecimal(String)没有做科学计数法校验,用户输入1E+3之后数据库报错;二是JSON序列化时,前端把BigDecimal转成了前端数字类型,精度丢失了,比如999999999.99变成了1000000000.00。
我的避坑做法是:所有接口返回金额字段时,用@JsonSerialize(using = ToStringSerializer.class)强制把BigDecimal序列化成字符串。前端拿到字符串,展示时用Number函数转数字,提交时再用字符串提交。这一步虽然丑,但能彻底避免精度问题。数据库层面统一DECIMAL(18, 4),做汇总计算时在SQL里用ROUND函数而不是把结果拉到Java里再算,因为到Java里再算又会产生浮点误差。
如果系统里要处理多币种,金额相关的表结构里建议冗余一个币种代码字段。我见过某个项目把币种放在配置表里,结果同一张报价单里既有人民币又有美元,对账时一片混乱。要么单独设置汇率表,要么在每张单上写明币种,不能偷懒。
6.3 截标时间与并发报价的竞态条件
截标时间处理不当,会引发非常大的客诉。供应商声称自己在截标前点了提交,但系统提示超时。这种问题十有八九是前端时钟和服务端时钟不同步。供应商电脑慢了30秒,他看到的剩余时间比真实时间多了30秒,实际已经截止了他还在填单。
正确做法是:供应商端页面上的倒计时一定要从服务端下发,不能靠前端本地时间。获取服务端当前时间和截止时间,用setInterval每隔几秒调一次服务端时间接口,或者至少通过WebSocket推送。更重要的是提交报价时,校验时间以数据库服务器时间为准,而不是应用服务器时间。如果应用服务器和数据库服务器时间不一致,要在第一次启动时做时钟同步。
另外一个并发容易出问题的是:截标那一刻,恰好有多个供应商同时提交。用事务+行锁锁住询价单记录,同一时间只有一个事务能提交,其他事务在锁释放后发现截标时间已过,直接抛出超时异常。不要用Redis锁替代数据库行锁,因为Redis挂了锁就失效了,数据库行锁才是最后的兜底。
6.4 审计日志与操作留痕:别等扯皮了才想起来
采购系统的审计日志,不是记录"谁登录了系统"就完了。真正需要的是关键业务的"操作留痕":谁在什么时间把询价单从"草稿"改成了"已发布"?谁在比价的时候把拟中标供应商从A改成了B?原始报价是多少,修改成了多少?这些内容要写进操作日志表,并且操作前后对比要有,不是简单的一句话说明。
我用的方案是:在核心业务实现类里,对状态变更和金额变更统一调用OperationLogService,记录beforeJson和afterJson。afterJson用Jackson序列化当前实体,beforeJson用快照记录操作前实体。查看日志时,可以直接通过diff看到字段变化。
日志不能只写给程序员看。采购经理和审计员看日志时,要能看到"采购员张三在2025-04-10 14:23把询价单RFQ20250410001的报价截止时间从2025-04-11 18:00改成了2025-04-12 09:00,原因填写:供应商反馈准备时间不足"这样的完整记录。如果日志只是"修改数据"四个字,那没有任何追溯价值。
7. 从源码到交付:部署、安全加固和二次开发建议
7.1 环境准备与部署流程
这套源码到了你手里,第一件事不是改代码,而是把环境跑起来。我的推荐部署方式是Docker Compose,把MySQL、Redis、MinIO、后端、前端nginx都编排好。一个四核八G的服务器就够了,数据库单独跑也可以用云数据库,但一开始就上云数据库会有一个问题:本地开发和线上环境不一致,容易踩没必要的坑。
部署的关键点是配置分离。源码里建议不要写死数据库密码和Redis密码,用环境变量注入。我在项目里用一个application-prod.yml覆盖本地配置,生产环境启动时指定--spring.profiles.active=prod。接数据库时要注意连接池的initial-size和max-active,采购系统平时并发不高,但月底集中对账时数据库连接会被打满,适当调大max-active到50左右。
还有一个很容易忽略的是文件上传路径。MinIO的桶名、访问域名都要在配置里单独维护,生产环境建议把桶设置为私有,通过后端生成临时URL给供应商下载,防止标书被直接公开访问。
7.2 安全加固清单
这类系统涉及价格、供应商资质、招投标文件,上线前安全加固要过一遍。第一个是登录安全,密码不能明文存储,用BCrypt加密;登录失败5次锁定账号15分钟,防止暴力破解。第二个是接口安全,管理端接口必须校验JWT,同时做细粒度的RBAC权限点控制。供应商端和采购员端不能共用同一套接口,数据权限要隔离。第三个是文件安全,上传附件要做类型校验和大小限制,比如只允许jpg、png、pdf、zip,单个文件不超过20MB。不能依赖前端校验,后端必须再做一次,防止直接Post上传恶意文件。
如果系统的采购金额很大,建议加上电子签章和短信验证码双重验证。定标报告和合同文件上的签名要能追溯,谁签的、什么时候签的、签的是哪一版,都要有记录。这一块如果源码里没有集成,二次开发时可以优先考虑。
7.3 二次开发最常见的需求方向
把基于源码的系统交付给客户之后,我遇到的二次开发需求排行前几名基本是固定的:第一是对接内部ERP系统,把采购订单直接推送成ERP里的采购入库单;第二是供应商小程序,方便供应商在手机上报报价、收通知;第三是电子签章,网上签合同不用来回邮寄快递;第四是数据看板,老板要看每个月采购额、节约成本、供应商准时交付率这些指标。
小程序端建议用uni-app来做,开发一套代码可以同时发布微信小程序、支付宝小程序和H5。小程序端主要面向供应商,不要做太复杂,保证核心流程可用就行:登录、资质维护、询价列表、报价、回标、中标通知。采购方管理端的东西再多,也不要硬塞到供应商端,否则会让供应商用户直接弃用。
对接ERP的核心,是订单号映射和状态同步。源码里建议把采购订单落库后提供一个消息队列topic,或者至少留一个触发回调接口的位置,方便外部系统实时获取订单状态。不要等到客户提出对接时才去翻代码找哪里可以扩展,一开始做架构时就要把这一层解耦出来。
我在这个项目里最深的一个体会是:采购系统的源码好不好用,不在于代码写得多花哨,而在于有没有把业务方那些"没说出口的要求"想清楚。比如供应商报价后能不能改、比例价表允不允许线下议价、招标开标时标书能不能看、资质过期要不要暂停交易。这些规则看起来是小事,每一个都能让验收结果从"能用"变成"难用"。如果你正在自研或二次开发这套系统,我建议你把业务规则当一等公民来设计,用配置驱动而不是改代码去适配。最后再啰嗦一句:部署到生产之前,把数据的备份脚本写清楚,采购数据丢了是真的会出大事的。
