简介:面向计算机相关专业学生与初、中级开发者的采购管理系统完整项目包,覆盖采购合同、供应商、采购单、发货单、返厂单等核心业务模块,可满足毕业设计、课程设计或Java Web开发实战练习场景。资源共113个文件,其中Java源码、XML配置与映射文件、FreeMarker模板(ftl)为主,辅以JS、SQL初始化脚本、properties配置等,整体压缩包仅115KB,目录结构清晰,便于导入IDE后快速理解工程布局。压缩包内含项目说明与数据库脚本,业务代码已测试通过,部署即可运行;通过采购订单到发货、返厂等流程串联,能帮助学习者掌握数据表设计、后端业务逻辑与前端模板渲染的完整协作方式,也可作为中小型管理系统二次开发的基础模板。已有341人学习下载,适合需要快速搭建采购业务演示或梳理管理信息系统实现路径的读者。
1. 采购管理系统源码包:这个压缩包里到底是什么
拿到一个写着“采购管理系统源码+项目说明+数据库”的压缩包,绝大多数人的第一反应是解压、找启动类、点运行。我建议你先忍住,先花十分钟把里面的.sql脚本和项目说明翻一遍。这个标题背后是一套典型的内部管理后台:供应商、采购合同、采购单、发货单、返厂单,再加报表和权限,差不多就是制造业或贸易公司采购部门最常用到的五类单据。源码解决“怎么维护这些数据”,数据库脚本解决“这些数据按什么结构存”,项目说明解决“单据之间怎么流转”。
这类系统最适合两类人:一是刚做完 Java 课程设计、想找一个贴近真实业务的项目来练手的学生;二是公司内部没有预算上大型 ERP,需要快速落地一套采购台账的小团队。它的价值不在代码多花哨,而在单据闭环——从合同一路追到返厂,每一笔数量对得上。下面就按我平时接手这类源码的顺序,把模块拆解、数据库设计、部署启动和踩坑记录一次说清。
2. 从采购合同到返厂单:核心单据模型与状态流转
2.1 供应商与采购合同:主数据的边界划分
采购管理系统里最容易模糊的地带,是“供应商”和“采购合同”到底谁属于谁。常见的做法是:供应商是主数据,独立建表;采购合同是业务单据,挂在供应商下面,一份供应商可以有多份合同。如果你把合同字段直接塞进供应商表,后面做采购单选择合同来源时就彻底卡住了——一个订单不知道该对应哪份采购条款。
我在代码里见过一种典型错误:supplier表里放了contract_no和contract_amount两个字段,结果同一个供应商签了两份合同后,旧合同的信息就被覆盖了。正确做法是合同独立成表,supplier_id作为外键;供应商表只保留supplier_code、supplier_name、status这种长期不变的信息。合同的有效期、金额、付款方式这些变化频繁的字段,全部下沉到合同表。
这两张表加上字典表(用于维护合同类型、币种、付款条件这些枚举值),构成了整个系统的数据基座。后续所有采购单、发货单、返厂单,都可以通过contract_id反查供应商信息,而不需要每张单据都冗余一份供应商地址和开户行。
2.2 采购单到发货单:一单一货还是分批到货
采购单(PO)是核心单据。字段上除了order_no、contract_id、supplier_id、order_type,还要有status来驱动流程。常见的状态有:草稿、已确认、部分到货、全部到货、已关闭。需要注意一个容易被忽视的点:采购单和发货单的关系是“一对多”还是“多对一”,取决于业务上允不允许分批到货。
多数制造业场景是允许分批的,也就是一张采购单对应多张发货单。这时数据库就不能只设计purchase_order和delivery_note两张主表,必须引入明细层:purchase_order_detail记录本次采购的物料和数量,delivery_note_detail记录每张发货单实际发了哪些物料。关键在数量字段——采购明细表上要有received_qty(累计到货数量),每次新增发货单时校验“本次发货数量 + 已到货数量 <= 采购数量”。
我见过不少源码为了省事,只做单头不做明细,发货时整单确认。这种设计短期能用,一旦遇到一张单分三次发货,就只能手工改采购单数量,最后对账时一塌糊涂。所谓“采购管理系统源码”值不值钱,先看它有没有把明细层做好。
2.3 返厂单的独立性与冲销逻辑
返厂单(也有叫退货单、不合格品处理单)是整个系统里最容易被设计错的地方。很多新手会把返厂单做成采购单的子表,直接在采购单下挂一个return_qty字段。可实际业务里,返厂可能发生在发货单之后、也可能发生在部分入库之后,甚至可能跨采购单退货——这批物料是上一张单送的,质量问题到下张单才被检验出来。
因此返厂单必须是一张独立主表,包含return_no、delivery_note_id、purchase_order_id、supplier_id、reason_type、status,再挂一个明细表记录具体退哪些物料、退多少。它的状态流转一般是:申请、供应商确认、已退货、已冲销。其中“已冲销”很关键——返厂不是把数据删掉,而是生成一张负数单据冲减库存,同时更新发货单的可退数量。
我在接手类似项目时,会额外做一张return_reason字典表,把质量不合格、数量短缺、型号发错这些常驻原因收进去。这样月末统计返厂率时,可以直接按reason_type聚合,而不是靠人工看备注文本。
3. 数据库设计:七张核心表的字段与约束
3.1 主数据表:supplier 与 purchase_contract
拿到.sql脚本后,先看表和字段,不要急着跑起来。我一般会直接打开数据库里的supplier表结构,确认几个关键字段是不是符合业务预期:
CREATE TABLE `supplier` ( `id` bigint NOT NULL AUTO_INCREMENT, `supplier_code` varchar(32) NOT NULL COMMENT '供应商编码', `supplier_name` varchar(128) NOT NULL COMMENT '供应商名称', `contact_person` varchar(64) DEFAULT NULL COMMENT '联系人', `contact_phone` varchar(32) DEFAULT NULL COMMENT '联系电话', `bank_account` varchar(64) DEFAULT NULL COMMENT '银行账号', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1启用,0停用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_supplier_code` (`supplier_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商表';这段建表语句里有三个点值得注意。supplier_code加了唯一约束,这是硬性校验,防止同一编码重复录入——没有这个约束,程序里再怎么判断都会有漏网之鱼。status用tinyint而不是直接删记录,为后面的逻辑删除做铺垫。create_time和update_time都给了默认值,这样插入数据时业务代码即使漏填,数据库层也能兜底。
合同表的核心是金额和有效期:
CREATE TABLE `purchase_contract` ( `id` bigint NOT NULL AUTO_INCREMENT, `contract_no` varchar(32) NOT NULL COMMENT '合同编号', `supplier_id` bigint NOT NULL COMMENT '供应商ID', `contract_type` tinyint NOT NULL COMMENT '1年度框架合同,2单次采购合同', `total_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总金额', `effective_date` date NOT NULL COMMENT '生效日期', `expire_date` date NOT NULL COMMENT '失效日期', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1生效,2已作废', PRIMARY KEY (`id`), UNIQUE KEY `uk_contract_no` (`contract_no`), KEY `idx_supplier_id` (`supplier_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购合同表';total_amount用decimal(14,2)不用double,这是财务字段的基本底线。effective_date和expire_date用date类型而不是datetime——合同的生效失效按天计算,带时分秒反而会在边界判断上出问题。索引上,除了唯一键还额外加了一个idx_supplier_id,因为日常查询几乎都是“查某供应商名下有哪些合同”,这个索引能明显加速。
3.2 单据表:purchase_order 与 purchase_order_detail
采购单主表保存单据头和整体状态,明细表保存每一行物料。把这两张表放一起说,因为它们的约束是配合工作的:
CREATE TABLE `purchase_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '采购单号', `contract_id` bigint NOT NULL COMMENT '关联合同ID', `supplier_id` bigint NOT NULL COMMENT '供应商ID', `order_type` tinyint NOT NULL DEFAULT '1' COMMENT '1普通采购,2紧急采购', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0草稿,1已确认,2部分到货,3全部到货,4已关闭', `total_amount` decimal(14,2) NOT NULL DEFAULT '0.00', `create_by` bigint NOT NULL COMMENT '创建人ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_contract_id` (`contract_id`), KEY `idx_supplier_id` (`supplier_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购单主表'; CREATE TABLE `purchase_order_detail` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '采购单ID', `material_code` varchar(32) NOT NULL COMMENT '物料编码', `material_name` varchar(128) NOT NULL COMMENT '物料名称', `unit` varchar(16) NOT NULL COMMENT '单位', `price` decimal(14,2) NOT NULL COMMENT '含税单价', `order_qty` decimal(14,2) NOT NULL COMMENT '采购数量', `received_qty` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '累计已到货数量', `returned_qty` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '累计已返厂数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购单明细表';关键在明细表上的四个数字:采购数量、累计到货、累计返厂,再加上一条隐含的“未到货数量 = order_qty - received_qty”。order_qty用decimal(14,2)而不用int,是因为很多物料按公斤、按米采购,数量有小数。这里received_qty和returned_qty是冗余字段,虽然违背了三范式的洁癖,但在实际查询“某张单还剩多少没到”时极其好用,不用每次汇总发货明细表。
但冗余字段必须由业务代码或存储过程保证一致。你可以通过一个物化视图或者触发器去同步,也可以把这层逻辑放在服务层事务里——新增发货单时,同时UPDATE purchase_order_detail SET received_qty = received_qty + 本次数量。少了这一步,报表必错。
3.3 物流与质量单据:delivery_note 与 return_note
发货单与返厂单的结构完全对称,都是为了承载“一对多”的分批业务。发货单的核心字段如下:
CREATE TABLE `delivery_note_detail` ( `id` bigint NOT NULL AUTO_INCREMENT, `dn_id` bigint NOT NULL COMMENT '发货单ID', `order_detail_id` bigint NOT NULL COMMENT '采购单明细ID', `qty` decimal(14,2) NOT NULL COMMENT '本次发货数量', PRIMARY KEY (`id`), KEY `idx_dn_id` (`dn_id`), KEY `idx_order_detail_id` (`order_detail_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发货单明细表';order_detail_id把发货明细精确到采购单的某一行,这个关联很关键——分批到货时,每批发的是同一种物料,但不一定是同样的单价,所以不能只关联到采购单主表。返厂单的表结构与此类似,但需要多一个delivery_note_id字段,因为返厂通常是针对某一次到货来退的,少了一个发货单维度,月底对账时根本说不清这退货是从哪批货里退的。
在源码包里,最应该确认的就是这三张明细表的关联关系。如果发货单直接挂purchase_order_id而没有order_detail_id,说明这套系统不支持“一张采购单分多物料、分批发货”的业务,拿到后要自己加字段。
返厂单还有一个额外的数量校验需求:同一发货单的同一行物料,累计返厂数量不能超过发货数量。这个校验可以放在代码里做,也可以通过唯一约束 + 触发器保证。推荐后者的一个简化版做法:在return_note_detail表上建uk_dn_order_detail(dn_id, order_detail_id)的唯一索引,同一发货明细只允许一条返厂记录,并把qty设计成允许负数,用负数表示冲减。这样数据永远只有一条,不存在多行累加的并发问题。
4. 让系统跑起来:环境准备、初始化SQL与启动配置
4.1 常规技术栈与最小环境要求
绝大多数这类源码包沿用的是 Spring Boot + MyBatis + MySQL 的结构,前端可能是 Thymeleaf 服务端渲染,也可能是 Vue 分离部署,这取决于压缩包里项目说明怎么写。我的经验是:先看pom.xml的依赖,确定了 Spring Boot 版本再决定用哪个 JDK——Spring Boot 2.x 配 JDK 8 最稳,Spring Boot 3.x 则必须 JDK 17,搞反了启动直接报版本错误。
环境准备我一般按下面这个清单核对:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8 或 17 | 取决于Spring Boot版本,切忌混用 |
| Maven | 3.6+ | 国内建议配置阿里云镜像加速依赖下载 |
| MySQL | 5.7 或 8.0 | 8.0注意驱动与连接串差异 |
| Navicat / DBeaver | 任意 | 用于执行SQL脚本和验收数据 |
如果源码包里自带doc/sql目录,这就是数据库初始化的入口。先看目录里有没有schema.sql和data.sql之分。有则是结构数据分离,没有说明作者把建表和初始化数据混在一个脚本里,执行时需要注意字符集和覆盖顺序。不要一上来就双击.sql,先打开脚本文件拉到最底下,确认有没有DROP DATABASE或DROP TABLE语句——有的话,执行前必须确认这台机器上没有你自己的重要数据。
4.2 初始化数据库的固定步骤
初始化数据库的正确姿势是先在 MySQL 里建一个空的库,再导入脚本,而不是让脚本自动建库。除非源码说明里明确写了脚本自带CREATE DATABASE,否则手动建库更安全,也方便后续多个项目共用一个 MySQL 实例:
mysql -uroot -p -e "CREATE DATABASE purchase_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p purchase_db < /path/to/schema.sql mysql -uroot -p purchase_db < /path/to/data.sql上面三条命令分两步:第一步建库,指定utf8mb4字符集;第二步导入表结构;第三步导入初始化数据。分开执行的好处是,如果data.sql里某条数据语法错误,你能立刻定位是结构问题还是数据问题。
执行完后不要急着关终端,跑一句验证数据是否完整:
USE purchase_db; SELECT COUNT(*) FROM supplier; SHOW TABLES;如果supplier表能查出初始化数据,说明脚本导入生效。常见问题是在 Windows 上执行.sql文件时路径带中文或空格,MySQL 客户端解析不了,解决办法是先把脚本复制到纯英文路径下再执行,不要直接拖拽带中文的文件夹路径进命令行。
如果脚本导入时报Unknown collation之类的错,大概率是脚本是用 MySQL 8.0 的默认字符集导出的,而你的数据库是 5.7。解决方法是把脚本里的utf8mb4_0900_ai_ci全部替换成utf8mb4_general_ci,再重新导入。
4.3 application.yml 必改的三个参数
Spring Boot 项目启动前,打开src/main/resources/application.yml,核心配置项里最容易被忽略的是下面这三个:
spring: datasource: url: jdbc:mysql://localhost:3306/purchase_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueserverTimezone=Asia/Shanghai这个参数必须显式声明。MySQL 8.0 默认时区是 UTC,如果你不指定中国时区,所有datetime字段插入后会比本地时间晚 8 小时——这是采购系统里对账时间错位的第一大来源,查出来的发货时间全在下午,但实际上应该是凌晨。map-underscore-to-camel-case: true是把数据库的supplier_code自动映射成 Java 的supplierCode,没有这行配置,MyBatis 查出来的对象字段全是 null,又不容易察觉到。
密码不要明文写在application.yml里提交到 Git。接手这套源码时,先看一下项目里有没有.gitignore文件;如果源码包本身就是个裸露的目录,至少把配置里的密码改成你自己的数据库密码再启动,不要沿用作者随手写的123456。
4.4 打包启动与首次登录验证
Maven 项目直接命令行打包启动,比在 IDE 里按绿箭头更接近生产流程:
cd /path/to/project mvn clean package -DskipTests java -jar target/purchase-system-1.0.0.jar-DskipTests在导入别人源码时几乎是必加的参数——原作者的测试用例很可能是连着他本地数据库写的,你跑测试大概率失败,跳过测试先让服务起来更重要。Java 命令带参数启动也是常用操作:
java -jar target/purchase-system-1.0.0.jar --server.port=8081 --spring.profiles.active=dev服务起来后,日志里出现Started字样只是第一步。第二步是打开浏览器访问登录页,用项目说明里给出的初始账号登录。如果项目说明里没写初始账号,去数据库里查sys_user表,找status=1且create_time最早的那个用户,密码通常是admin123或123456,再不行就用 MD5 加密串反查——21232f297a57a5a743894a0e4a801fc3就是admin的 MD5,拿去比对数据库里的 password 字段就知道默认密码是什么了。
5. 实战踩坑:单据状态、数量冲销与导入导出乱码的修复记录
5.1 删了供应商,历史订单全断链:逻辑删除与停用状态
这不是看代码能发现的坑,得在项目说明里找线索。很多源码在实现“删除供应商”功能时,直接执行DELETE FROM supplier WHERE id = ?。这在开发环境没问题,一旦有历史采购单、合同关联着这行记录,删除动作就把外键关联的根给拔了——采购单列表还在,但点击单号查询供应商详情时,后台报空指针。
最后我的解决方案是彻底放弃物理删除:删除按钮改为调用UPDATE supplier SET status = 0 WHERE id = ?,所有下拉列表默认只查status = 1的供应商。代码里需要改动的点也简单,把原来控制器里的delete方法换成updateStatus,SQL语句把DELETE换成UPDATE,返回值从“影响行数”改成“更新行数”。历史数据安然无恙,新单据也选不到已停用的供应商。
如果源码里已经用了物理删除,还有一个补救办法:给supplier表添加deleted字段后,把所有老数据的deleted置为 0,再把所有业务表的关联查询改成LEFT JOIN supplier s ON s.id = xxx AND s.deleted = 0。这样历史记录还在,只是在界面上不展示已删除的供应商而已。
5.2 返厂超退导致负数库存:唯一约束与可退数量
返厂单最容易翻车的地方,是对同一张发货单重复退货。业务场景是:一批货到库,质检抽检发现 100 件里有 3 件不合格,录入返厂 3 件;一周后又发现 2 件型号发错,再次录入返厂 2 件。如果代码里没有累计校验,第二次录入时系统不知道已经退过 3 件,照样放行 2 件。两种情况叠加,超过发货数量时数据库里的返厂总数就比发货总数还大——库存变成负数,月底对账谁都得疯。
经典的修复方案是在返厂单新增接口里加三重校验:先查delivery_note_detail的本次发货数量;再SUM该发货明细关联的所有return_note_detail.qty;最后判断本次返厂数量加累计返厂数量是否超出。但并发场景下,两个用户同时提交返厂单会同时通过校验,数据照样超退。要治本,就在数据库层加约束:
ALTER TABLE `delivery_note_detail` ADD COLUMN `returned_qty` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '累计返厂数量'; ALTER TABLE `return_note_detail` ADD UNIQUE KEY `uk_dn_detail` (`delivery_note_detail_id`);在return_note_detail上加唯一约束,同一发货明细只允许一张返厂单,后面的返厂走“负数冲销”而不是新增行。每次返厂时UPDATE delivery_note_detail SET returned_qty = returned_qty + ?,这条 SQL 在事务里执行,数据库会自动锁行,从根上杜绝并发超退。代价是返厂表只能记录一次性的批量退货,多次退货需要拆成多张返厂单,这也是可接受的。
5.3 金额字段用 double 导致对账差几分:DECIMAL 精度
采购总金额、合同金额、入库金额,这些字段如果数据库里用double存,程序里用Float或Double计算,月末财务对账必然差几分钱。原理不复杂:二进制浮点数无法精确表示 10 进制小数,0.1 + 0.2在 Java 里算出来是0.30000000000000004,累加若干行后误差就显性化了。
这类坑最气人的是:大部分时候账是对的,偶尔某几张单价是三位小数的单据,汇总后多一分钱或少一分钱,非常具有迷惑性。排查时先查数据库字段类型,再查 Java 实体里的类型声明。字段类型是decimal的话,Java 实体对应字段必须声明为BigDecimal,不能偷懒用Double。
如果已经被污染,先把所有金额字段转成decimal(14,2),再用一条 SQL 计算出当前账面的总误差:
SELECT SUM(amount) - SUM(CAST(amount AS DECIMAL(14,2))) AS diff FROM payment_record WHERE diff != 0;误差行找出来后,按单据号追溯到明细,手工调整差异那几行。调整完以后,在 MyBatis 的 resultMap 里确认所有金额字段的 typeHandler 用默认的 BigDecimal 处理,不要再让应用层做字符串到浮点的隐式转换。
5.4 导出Excel打开乱码:UTF-8 BOM 与字符集问题
采购系统里导出供应商列表、导出对账单是高频操作。很多源码用最简单的 CSV 导出,代码里写response.setContentType("text/csv"),不指定字符集就让浏览器自己猜。Chrome 通常能正常解码,但 Excel 在 Windows 上打开时默认按 GBK 解析,UTF-8 无 BOM 的中文内容全部变成乱码。
不乱码的写法是输出带 BOM 头的 UTF-8,让 Excel 自动识别编码:
response.setContentType("text/csv;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=supplier.csv"); OutputStream out = response.getOutputStream(); out.write(0xEF); out.write(0xBB); out.write(0xBF);先写入EF BB BF三个字节的 BOM,再写 CSV 内容,Windows 下 Excel 打开就不乱码。另一个角度是数据库连接串没加characterEncoding=utf8,导出时读取出来就已经是乱码,写回去自然还是乱码——所以排查时先画一条链路:数据库读出是否正确、Java 字符串是否正确、输出响应是否正确,逐段确认。
5.5 分页统计跳单:先查主键再查明细
这个坑最容易出现在“带明细的分页查询”上。典型场景是:采购单列表分页,每页显示 10 条主表记录,每条主表下面展开 N 条明细。新手写法是一条 SQL 把主表和明细JOIN出来,然后用 LIMIT 分页。结果一张采购单有 5 行明细,分页时这一单占了 5 个位置,10 条的分页实际只显示了 2 张完整的单,后台翻页时还会出现重复数据。
正确的做法是两条 SQL 分开查,也叫“先查主键再查明细”:
-- 第一步:只查主表,分页取出这一页的采购单ID SELECT id FROM purchase_order WHERE status = 1 ORDER BY create_time DESC LIMIT 10 OFFSET 0; -- 第二步:查出这一页所有采购单ID,批量带出明细 SELECT * FROM purchase_order_detail WHERE order_id IN (?, ?, ?, ...);代码层把第二步的明细按order_id分组,Map 组装后塞进主表对象里。这样分页永远按主表条数计算,明细不会撑爆分页。这个改造在大部分源码包里都能做,改动点集中在 mapper XML 里,把一个嵌套查询拆成两条独立查询,性能反而更好——尤其采购明细数量大时,JOIN 后的临时表会占用大量内存。
6. 进阶验证:用一张视图把采购执行率算给老板看
系统跑通、单据能录之后,真正体现这套源码价值的时刻是月末对账。老板会问:这个月从哪些供应商买了什么、到了多少、退了多少、执行率怎么样?不要临时写一堆 Java 代码在内存里算,直接在数据库层建视图,把现货率转换成一条查询:
CREATE OR REPLACE VIEW v_supplier_analysis AS SELECT s.supplier_code, s.supplier_name, COUNT(DISTINCT po.id) AS order_cnt, SUM(pod.order_qty) AS total_order_qty, SUM(pod.received_qty) AS total_received_qty, SUM(pod.returned_qty) AS total_returned_qty, CASE WHEN SUM(pod.order_qty) = 0 THEN 0 ELSE ROUND(SUM(pod.received_qty) / SUM(pod.order_qty) * 100, 2) END AS arrival_rate, CASE WHEN SUM(pod.received_qty) = 0 THEN 0 ELSE ROUND(SUM(pod.returned_qty) / SUM(pod.received_qty) * 100, 2) END AS return_rate FROM supplier s LEFT JOIN purchase_order po ON po.supplier_id = s.id AND po.status IN (1, 2, 3) LEFT JOIN purchase_order_detail pod ON pod.order_id = po.id GROUP BY s.supplier_code, s.supplier_name;视图的统计口径值得推敲。采购单只统计状态下是已确认、部分到货、全部到货的单,草稿和已关闭的不算,否则会把废单计入分母。返厂率的分子是返厂数量,分母是实际到货数而不是订货数,这样反映的是供应商交付质量的真实水平。到货率则是到货数除以订货数,衡量采购执行的履约度。
有了这个视图,后台的报表可以这样组织成一张“采购执行验收表”:
| 检查项 | 验证SQL | 预期结果 |
|---|---|---|
| 数据闭环 | 对比purchase_order_detail.received_qty与发货单明细汇总 | 两者一致 |
| 返厂冲销 | 对比delivery_note_detail.returned_qty与返厂单明细汇总 | 返厂不超发货 |
| 合同到期预警 | SELECT * FROM purchase_contract WHERE expire_date BETWEEN 现在 AND 30天后 | 能查出即将到期合同 |
| 订单库存充足性 | 检查是否实现了“发货时扣减、返厂时回补” | 不出现负数 |
验收表里第一条对账 SQL,是压垮不少源码的最后一根稻草。如果发现对不上,优先怀疑采购明表的received_qty不是数据库维护的,而是应用层代码通过发货单汇总后回填的字段——说明系统在数据一致性上存在风险点,日后再加“库存扣减”或“应付生成”会连环出错。
我自己的习惯是:每次拿到一套采购源码,先写上面这条视图,再跑一遍三条对账 SQL,通不过就直接看代码里有没有事务注解,没有的话说明这是一套演示级源码,别直接拿去给真实公司用。如果通过了,把视图赋给报表账号,月底直接导出,再配合项目说明里的数据库设计文档,就能作为二开基座。希望这些经验和踩坑记录能帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取