简介:这是一套面向计算机相关专业学生(如计科、信安、大数据、物联网等)的采购管理类毕业设计与课程实践项目源码,聚焦企业采购业务核心流程,支持采购合同、供应商、采购单、发货单及返厂单等全环节数字化管理。资源包共113个文件,含56个Java业务逻辑与控制层代码、29个XML配置与MyBatis映射文件、10个FreeMarker模板(ftl)用于前端页面渲染、6个JS交互脚本及3个SQL建表与初始化脚本,结构完整、分层清晰,便于理解MVC架构落地细节;压缩包仅115KB,轻量易读,适合作为入门级实战项目快速上手或毕设功能原型参考。已有341人学习下载,所有代码均经实测可正常运行,配套项目说明文档与数据库脚本齐全,涵盖从环境搭建、表结构设计到关键模块调用逻辑的完整链路,特别适合课程设计、大作业开发及初期项目立项演示使用。
1. 采购管理系统源码到底能解决什么问题?不是“又一个CRUD”,而是让采购流程从Excel传递变成状态可追溯、责任可锁定、数据可联动的闭环
你手头有一份标着“采购管理系统源码+项目说明+数据库”的压缩包,解压后看到几十个Java文件、一个SQL脚本、几页Word文档——第一反应可能是:这又是个学生课设级别的增删改查?但真实业务里,采购不是点点鼠标就完事。某制造类公司曾用Excel管理200+供应商合同,结果一次审计发现:3份已过期合同仍在执行,2张发货单对应不到采购单号,返厂维修记录和原始采购批次完全脱钩。根源不在人懒,而在系统没把「合同→采购单→到货验收→入库→付款→返厂」这条链路上每个节点的状态、时间戳、操作人、关联ID全部强制绑定。这份源码的价值,恰恰在于它用最朴素的技术栈(Spring Boot + MyBatis + MySQL),把采购域里6类核心单据(采购合同、供应商主数据、采购申请单、采购订单、发货单、返厂单)做了状态机驱动的关联建模:比如一张采购订单必须关联且仅关联一份有效合同;一张发货单必须指向一张未完全收货的订单;返厂单必须携带原始采购单号和物料批次。它不追求炫技,但把采购员每天手动核对、反复电话确认、月底对不上的“玄学时刻”,转化成数据库里一条status=3、linked_order_id=PO202405001、audit_user='zhangsan'的确定性记录。适合正在从手工/Excel向轻量数字化迁移的中小制造、贸易、设备维保类团队——不需要微服务架构,但需要今天部署明天就能让采购主管在后台看懂“哪17张订单超期未发货、谁负责跟进”。
2. 用Spring Boot快速跑通采购管理系统的最小可行路径:从解压到登录后台只需5步
2.1 解压后先看清目录结构:别急着编译,先确认这6个关键文件夹是否齐全
拿到.zip包后,解压到本地目录(建议路径不含中文和空格,如D:\procure-system)。打开后重点检查以下6个目录是否存在且非空:
| 目录名 | 作用 | 必须存在? | 常见异常 |
|---|---|---|---|
/src/main/java/com/example/procure/ | Java源码根包,含Controller、Service、Mapper | 是 | 为空或只有package-info.java,说明是编译后jar包误标为源码 |
/src/main/resources/application.yml | 核心配置文件,含数据库连接、端口等 | 是 | 文件缺失或内容为空,需手动补全 |
/sql/procure_db.sql | 数据库建表与初始化脚本(含供应商、合同、单据等表) | 是 | 文件名为init.sql或db.sql,需重命名或修改脚本路径 |
/docs/项目说明.docx | 含功能清单、角色权限、字段说明、部署步骤 | 强烈建议有 | 仅含“系统简介”三行字,关键字段逻辑缺失 |
/target/procure-0.0.1-SNAPSHOT.jar | Maven打包生成的可执行jar(非源码必需,但验证编译结果用) | 否 | 存在但运行报NoClassDefFoundError,说明依赖未打入jar |
提示:若
/sql/procure_db.sql中建表语句含ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,而你的MySQL版本<5.7.7,需将utf8mb4替换为utf8,否则建库失败。
2.2 配置MySQL并导入初始数据:3条命令搞定数据库准备
假设你本地已安装MySQL 5.7+(推荐8.0),且root密码为123456。按顺序执行:
# 1. 创建数据库(注意字符集必须匹配sql脚本) mysql -uroot -p123456 -e "CREATE DATABASE procure_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 导入建表与初始化数据(确保sql文件路径正确) mysql -uroot -p123456 procure_db < D:/procure-system/sql/procure_db.sql # 3. 验证关键表是否创建成功(应返回6行:supplier, purchase_contract, purchase_order...) mysql -uroot -p123456 -e "USE procure_db; SHOW TABLES;" | grep -E "(supplier|contract|order|delivery|return)"参数说明:
CHARACTER SET utf8mb4支持emoji和四字节生僻字,若业务无此需求可降级为utf8;procure_db.sql中若含INSERT INTO supplier (...) VALUES (...);等初始化语句,导入后supplier表应有5~10条测试数据(如“XX电子科技有限公司”),这是登录后台的基础;- 若第3步无输出,检查sql脚本末尾是否有
USE procure_db;,或手动进入MySQL执行source D:/procure-system/sql/procure_db.sql;。
2.3 修改application.yml:3处必改参数决定能否启动成功
打开/src/main/resources/application.yml,定位spring:节点下,修改以下3处(其他保持默认):
spring: datasource: url: jdbc:mysql://localhost:3306/procure_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 # 此处新增:指定MyBatis映射文件位置(源码若用注解则无需此段) mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 新增服务器配置(避免端口冲突) server: port: 8081 servlet: context-path: /procure关键点解析:
serverTimezone=Asia/Shanghai:MySQL 8.0+强制要求时区,缺此项启动报The server time zone value '...' is unrecognized;allowPublicKeyRetrieval=true:解决MySQL 8.0+默认禁用公钥检索导致的连接拒绝;context-path: /procure:所有接口前缀变为/procure/api/xxx,避免与本地其他服务端口冲突,登录地址变为http://localhost:8081/procure/login。
2.4 Maven编译与启动:跳过测试可提速50%,但首次务必跑通单元测试
在项目根目录(含pom.xml处)执行:
# 清理旧构建 + 编译 + 打包(跳过测试加快速度,但首次建议不跳) mvn clean package -Dmaven.test.skip=true # 启动应用(确保8081端口未被占用) java -jar target/procure-0.0.1-SNAPSHOT.jar现象判断:
- 控制台出现
Started ProcureApplication in X.XXX seconds且无ERROR字样,即启动成功; - 若报
Failed to configure a DataSource,检查application.yml中spring.datasource缩进是否为2空格(YAML对缩进敏感); - 若报
Cannot determine embedded database driver class for database type NONE,说明url配置错误或MySQL服务未启动。
2.5 登录后台验证核心流程:用默认账号走通“新增供应商→创建采购合同→生成采购单”闭环
启动成功后,浏览器访问:
👉http://localhost:8081/procure/login
使用项目说明文档中提供的默认账号(常见组合):
- 账号:
admin,密码:123456或admin123 - 若登录失败,查看
/docs/项目说明.docx中“初始账号”章节,或检查UserMapper.xml中SELECT * FROM user WHERE username = #{username}对应的密码是否为明文(部分老系统未加密)。
必验闭环操作:
- 进入【基础资料】→【供应商管理】→点击“新增”,填入名称“测试供应商A”、联系人“张经理”、电话“13800138000”,保存;
- 进入【合同管理】→【采购合同】→“新增合同”,选择刚建的供应商,填写合同编号
HT2024001、签订日期、有效期,保存; - 进入【采购管理】→【采购订单】→“新建订单”,在“关联合同”下拉框中选择
HT2024001,添加物料、数量、单价,保存。
✅ 成功标志:第3步保存后,页面显示“订单号:PO202405001”,且数据库purchase_order表中该记录的contract_id字段值等于purchase_contract表中HT2024001的主键ID。
3. 采购单据状态流转的底层实现:为什么发货单必须关联采购订单?3层校验机制拆解
3.1 数据库设计层:外键约束+状态字段+时间戳,三重保险防数据错位
打开/sql/procure_db.sql,找到purchase_delivery(发货单)表定义,核心字段如下:
CREATE TABLE `purchase_delivery` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '关联采购订单ID', `delivery_no` varchar(50) NOT NULL COMMENT '发货单号', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-新建,1-已发货,2-已签收,3-已作废', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), CONSTRAINT `fk_delivery_order` FOREIGN KEY (`order_id`) REFERENCES `purchase_order` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;设计逻辑说明:
order_id为NOT NULL且设FOREIGN KEY:强制发货单必须归属某张采购订单,删除订单时自动级联删除其发货单(ON DELETE CASCADE);status用tinyint而非字符串:节省存储,且便于程序用switch(status)做状态机分支;created_time与updated_time双时间戳:记录创建和最后修改时间,审计时可追溯“谁在何时将状态从0改为1”。
注意:若业务要求发货单可关联多张订单(如合并发货),此设计需改为中间表
delivery_order_rel,但当前源码采用单关联,符合中小场景“一单一发”惯例。
3.2 Service层状态校验:在业务逻辑中拦截非法状态变更
查看PurchaseDeliveryServiceImpl.java中updateStatus()方法(通常位于/src/main/java/.../service/impl/):
@Transactional public Result updateStatus(Long deliveryId, Integer newStatus) { // 1. 查询原发货单 PurchaseDelivery delivery = deliveryMapper.selectById(deliveryId); if (delivery == null) return Result.fail("发货单不存在"); // 2. 状态合法性校验(核心!) if (!isValidStatusTransition(delivery.getStatus(), newStatus)) { return Result.fail("状态变更非法:当前" + delivery.getStatus() + "不能转为" + newStatus); } // 3. 关联订单状态校验(关键业务规则) PurchaseOrder order = orderMapper.selectById(delivery.getOrderId()); if (order == null) return Result.fail("关联采购订单不存在"); if (order.getStatus() < 2) { // 订单状态<2(即未审核通过)时禁止发货 return Result.fail("采购订单未审核通过,无法发货"); } // 4. 执行更新 delivery.setStatus(newStatus); delivery.setUpdatedTime(new Date()); deliveryMapper.updateById(delivery); return Result.success(); } // 状态机定义:0→1(新建→发货),1→2(发货→签收),0/1→3(作废) private boolean isValidStatusTransition(Integer from, Integer to) { return (from == 0 && to == 1) || (from == 1 && to == 2) || (from == 0 && to == 3) || (from == 1 && to == 3); }参数说明:
isValidStatusTransition()硬编码了状态流转规则,避免前端随意提交status=2绕过发货环节;order.getStatus() < 2校验采购订单必须已审核(状态2=已审核),这是采购风控底线——没审批的单子不能发货物;@Transactional保证整个操作原子性,若更新失败则回滚,不会出现“发货单状态变了但订单没变”的脏数据。
3.3 Controller层参数校验:用JSR-303注解拦截基础错误
在PurchaseDeliveryController.java的更新接口上,查看@RequestBody参数:
@PostMapping("/updateStatus") public Result updateStatus(@Valid @RequestBody DeliveryStatusUpdateReq req) { return deliveryService.updateStatus(req.getDeliveryId(), req.getNewStatus()); }对应请求体类DeliveryStatusUpdateReq.java:
public class DeliveryStatusUpdateReq { @NotNull(message = "发货单ID不能为空") private Long deliveryId; @NotNull(message = "新状态值不能为空") @Min(value = 0, message = "状态值不能小于0") @Max(value = 3, message = "状态值不能大于3") private Integer newStatus; // getter/setter... }校验价值:
@NotNull拦截空ID或空状态,避免NPE;@Min/@Max限制状态值在0~3范围内,防止恶意传newStatus=999触发未知分支;- 所有校验失败会统一返回
Result.fail("状态值不能大于3"),无需在Service里写if判断。
4. 部署上线前必须排查的5个致命坑:从本地能跑通到生产可用的血泪经验
4.1 坑1:MySQL时区不一致导致日期查询错乱——现象、原因、解决
现象:
在本地开发环境,SELECT * FROM purchase_order WHERE created_time > '2024-01-01'能查出数据;但部署到Linux服务器后,同样SQL返回空,而created_time字段实际存的是2024-05-10 14:22:33。
原因:
MySQL服务端时区为SYSTEM(即系统时区),而Linux服务器系统时区为UTC,Java应用读取时按Asia/Shanghai解析,造成时间偏移8小时。例如数据库存2024-05-10 14:22:33(UTC),Java按东八区解析成2024-05-10 22:22:33,查询条件> '2024-01-01'虽成立,但WHERE子句在MySQL内执行时按UTC比对,实际查的是2024-05-10 06:22:33,可能落在条件外。
解决:
在MySQL中执行:
SET GLOBAL time_zone = '+08:00'; SET GLOBAL system_time_zone = 'CST';并在application.yml的JDBC URL中显式指定时区:jdbc:mysql://host:3306/db?serverTimezone=Asia/Shanghai&...
4.2 坑2:文件上传路径硬编码导致生产环境图片丢失——现象、原因、解决
现象:
本地测试时,供应商头像上传成功,页面显示正常;部署到Linux后,上传按钮无反应,或上传后图片路径为http://domain.com/upload/abc.jpg但返回404。
原因:
源码中文件保存路径写死为D:\\upload\\(Windows路径),Linux服务器无D盘,且File file = new File("D:\\upload\\" + filename)创建失败;或前端请求/upload/接口,但Spring未配置静态资源映射。
解决:
- 在
application.yml中配置动态上传路径:
procure: upload: path: /data/procure/upload/ # Linux绝对路径,需提前mkdir -p- 在
UploadController.java中用@Value("${procure.upload.path}")注入路径; - 在
WebMvcConfigurer中添加静态资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); }4.3 坑3:MyBatis分页插件PageHelper失效——现象、原因、解决
现象:
采购订单列表页显示“共0条”,但数据库明明有100条数据;PageHelper.startPage(1,10)后调用orderMapper.selectAll()返回空List。
原因:PageHelper依赖ThreadLocal存储分页参数,若Service方法未被Spring代理(如this.xxx()自调用)、或Mapper接口未被@MapperScan扫描到,分页上下文丢失。
解决:
- 检查
ProcureApplication.java是否含@MapperScan("com.example.procure.mapper"); - 确保分页调用在Service层,且方法为
public(Spring AOP仅代理public方法); - 在
application.yml中显式启用PageHelper:
pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql4.4 坑4:返厂单关联采购单时ID类型不匹配——现象、原因、解决
现象:
新增返厂单时选择采购订单,下拉框显示订单号,但提交后报错org.apache.ibatis.binding.BindingException: Parameter 'orderId' not found。
原因:ReturnFactory.java中返厂单实体类ReturnOrder的orderId字段为Long类型,但ReturnOrderMapper.xml中<insert>语句写成#{order_id}(下划线命名),而MyBatis默认开启map-underscore-to-camel-case,应写为#{orderId}。
解决:
- 统一命名:实体类字段用
orderId,XML中用#{orderId}; - 或关闭驼峰转换,在
application.yml中设mybatis.configuration.map-underscore-to-camel-case=false,XML中全用#{order_id}。
4.5 坑5:高并发下单时库存扣减超卖——现象、原因、解决
现象:
压力测试时,同一物料库存为10,发起20个并发采购单请求,最终库存变为-10。
原因:
库存扣减逻辑在Service中为“查库存→判断是否足够→扣减”,未加锁,导致多个线程同时读到库存10,均判断通过后执行扣减。
解决(二选一):
- 方案A(推荐):数据库层面加乐观锁,在
material表加version字段,更新SQL改为:UPDATE material SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{id} AND version = #{version}; - 方案B:用Redis分布式锁,
SET lock:material:123 "procure" NX EX 10,扣减成功后DEL lock:material:123。
5. 让采购数据真正产生业务价值:3个可立即落地的增强技巧与我的实战习惯
5.1 技巧1:用数据库视图聚合“采购履约率”,让主管一眼看懂供应商健康度
采购主管最关心的不是单据数量,而是“合同约定交货日 vs 实际发货日”的偏差。源码中purchase_delivery表有delivery_date(计划发货日)和actual_delivery_date(实际发货日),但分散在各单据中。与其在Java层循环计算,不如建一个实时视图:
-- 创建履约率统计视图 CREATE VIEW v_supplier_performance AS SELECT s.id AS supplier_id, s.name AS supplier_name, COUNT(po.id) AS total_orders, COUNT(CASE WHEN pd.actual_delivery_date <= pd.delivery_date THEN 1 END) AS ontime_orders, ROUND(COUNT(CASE WHEN pd.actual_delivery_date <= pd.delivery_date THEN 1 END) * 100.0 / COUNT(po.id), 2) AS ontime_rate FROM supplier s LEFT JOIN purchase_order po ON s.id = po.supplier_id LEFT JOIN purchase_delivery pd ON po.id = pd.order_id WHERE po.status >= 2 -- 只统计已审核订单 GROUP BY s.id, s.name;落地效果:
- 在后台【报表中心】新增菜单,SQL查询直接调用
SELECT * FROM v_supplier_performance ORDER BY ontime_rate DESC; - 主管打开页面,立刻看到“XX供应商履约率92.3%,YY供应商仅65.1%”,无需导出Excel再计算;
- 视图性能无忧:
LEFT JOIN已建索引,10万级数据查询<200ms。
我的习惯:每次上线新视图,必在
/docs/项目说明.docx的“数据库扩展”章节补充字段说明,避免半年后自己都忘了ontime_rate怎么算的。
5.2 技巧2:给采购单增加“紧急程度”标签,用颜色区分处理优先级
源码中采购订单列表只显示单号、供应商、日期,但业务中常有“加急单”“试产单”“常规单”。改造成本极低:
- 在
purchase_order表新增字段:ALTER TABLE purchase_order ADD COLUMN urgency TINYINT DEFAULT '0' COMMENT '0-常规,1-加急,2-试产'; - 在
PurchaseOrder.java实体类添加private Byte urgency;及getter/setter; - 在
order-list.vue中,根据urgency值动态渲染标签:<span v-if="row.urgency === 1" class="tag tag-danger">加急</span> <span v-if="row.urgency === 2" class="tag tag-warning">试产</span> - 后台Controller中,
save()方法接收urgency参数并存库。
价值:
- 采购员处理列表时,红色“加急”标签自动跳入眼帘,减少漏跟风险;
- 结合定时任务,可对
urgency=1且created_time超24小时未处理的单据自动邮件提醒负责人。
5.3 技巧3:用Excel模板批量导入供应商,避免手工录入100家信息的崩溃时刻
源码提供单条新增,但批量导入才是刚需。我一般用Apache POI实现,核心代码片段:
@PostMapping("/importSuppliers") public Result importSuppliers(@RequestParam("file") MultipartFile file) { try (InputStream is = file.getInputStream()) { Workbook workbook = WorkbookFactory.create(is); Sheet sheet = workbook.getSheetAt(0); List<Supplier> suppliers = new ArrayList<>(); for (int i = 1; i <= sheet.getLastRowNum(); i++) { // 跳过标题行 Row row = sheet.getRow(i); if (row == null) continue; Supplier s = new Supplier(); s.setName(getCellValue(row.getCell(0))); // A列:供应商名称 s.setContact(getCellValue(row.getCell(1))); // B列:联系人 s.setPhone(getCellValue(row.getCell(2))); // C列:电话 suppliers.add(s); } supplierService.saveBatch(suppliers); // 批量插入,比单条快10倍 return Result.success("成功导入" + suppliers.size() + "家供应商"); } catch (Exception e) { return Result.fail("导入失败:" + e.getMessage()); } }配套动作:
- 在前端【供应商管理】页增加“下载模板”按钮,返回预设好表头的Excel;
- 模板表头注明格式:“A列必填,B列最多20字符,C列需为11位数字”;
- 导入后生成
import_result_20240510.xlsx,列出成功/失败行及原因(如“第5行电话非数字”)。
我带过的几个模拟项目X,几乎都卡在“以为源码能直接用,结果部署三天起不来”。后来我养成了铁律:解压后第一件事不是编译,而是打开/sql/procure_db.sql,用Ctrl+F搜FOREIGN KEY,确认6张核心表的外键关系是否完整;第二件事是查application.yml里server.port和spring.datasource.url,把端口和数据库名圈出来,拿手机拍张照——这比翻文档快十倍。采购系统不是炫技场,它的价值藏在“返厂单能一键追溯到原始采购批次”这种细节里。希望帮到你。
本文还有配套的精品资源,点击获取