Java进销存ERP源码实战:从启动失败到生产就绪
简介:这是一套基于SpringBoot的Java进销存ERP管理系统完整源码,面向Java初学者与中小型企业管理软件开发者,提供可直接运行、二次开发的全功能业务系统参考实现。系统覆盖零售、采购、销售、仓库、财务及报表查询等核心模块,支持预付款、组装拆卸、调拨、订单全流程管理,并实现细粒度权限控制(精确到按钮级)与多维度库存统计报表。资源包共2000个文件,含251个Java业务逻辑类、260个JS交互脚本、250个HTML页面、244个CSS样式文件、512个PNG图标资源及2个SQL建库脚本,技术栈清晰完整(MyBatis+EasyUI+AdminLTE+Log4j),压缩包大小52.83MB。目前已有746人学习下载,开箱即用,附带完整目录结构与典型配置(如start.bat、数据库脚本、JDK1.8/Mysql5.7适配说明),适合用于课程设计、毕业项目或企业轻量级ERP快速原型开发。
1. Java进销存ERP管理系统源码:不是拿来就能跑的“开箱即用”,而是要亲手拧紧每一颗螺丝的生产级工程
你搜到这个标题,大概率正站在三个岔路口:刚接下小工厂的定制单,老板说“要个能管进货、卖货、查库存的系统,越快上线越好”;或是校招面试被问“做过什么完整项目”,想拿套真实可演示的Java后台系统补简历;又或者在技术群看到别人分享“SpringBoot+Vue进销存源码”,点开压缩包发现连数据库脚本都缺两份、登录页404、pom.xml里还写着<version>2.3.0.RELEASE</version>——而你本地装的是JDK 17。这不是代码质量问题,而是所有真实业务系统源码的共性:它不承诺运行,只提供骨架;不替代理解,只放大认知差。这套“Java进销存ERP管理系统源码”,本质是一套面向中小制造/贸易企业的轻量级ERP核心模块集合:覆盖采购订单→入库质检→销售出库→库存调拨→财务应付应收→基础BOM与简单MRP推演。它不追求SAP那种千人千面的配置化,但要求你必须亲手配通MySQL字符集、调平MyBatis动态SQL的嵌套层级、把Shiro权限树和前端菜单JSON对齐、甚至手动补全日志中报错的java.lang.NoClassDefFoundError: org/apache/commons/collections4/MapUtils——因为打包者删了lib目录。适合能写Controller、会看Druid监控面板、敢改application-prod.yml里数据库密码的人。新手照着做能打通全流程,老手能借它反向吃透ERP领域建模逻辑。别信“一键部署”,信“三小时排错”。
2. 从解压到登录:五步走通本地启动链,每一步都卡在真实环境的毛细血管里
2.1 解压后第一眼:识别源码包的真实技术栈与隐含约束
拿到.zip或.rar文件,别急着mvn clean install。先用文本编辑器打开根目录下的pom.xml,重点抓三处:
<!-- 示例:真实项目中常见的“陷阱式”依赖声明 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.14</version> <!-- 注意:此版本要求JDK 8-16,JDK 17会报错 --> </parent> <properties> <java.version>1.8</java.version> <!-- 强制锁定JDK 8,即使你装了17也得切版本 --> <mybatis-plus.version>3.4.3.4</mybatis-plus.version> <!-- 非官方MyBatis-Plus,注意Mapper扫描路径 --> </properties>提示:很多源码包的
pom.xml里藏着“版本绑架”。比如<java.version>1.8</java.version>意味着你必须用JDK 8(推荐Adoptium Temurin 8u362),否则编译阶段就卡在Unsupported class file major version 61。别试图强行升级——MyBatis-Plus 3.4.x与Spring Boot 2.5.x深度耦合,升Spring Boot版本会导致@TableField注解失效、分页插件不生效。
再检查src/main/resources目录结构:
- 必有
application.yml(开发配置)和application-prod.yml(生产配置模板) - 若存在
mapper/目录且是空的,说明SQL映射文件被外置——要去src/main/resources/mapper/找XML文件,而非@MapperScan注解指向的位置 - 若有
static/和templates/,说明是前后端未分离架构(Thymeleaf渲染),此时Vue前端代码大概率在另一个压缩包里,当前Java包只负责API
2.2 数据库初始化:字符集、时区、外键三道生死线
进销存系统对数据一致性极度敏感。直接执行sql/目录下的init.sql大概率失败。必须按顺序操作:
- 创建数据库并指定字符集(MySQL 5.7+):
# 登录MySQL后执行 CREATE DATABASE erp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; SET NAMES utf8mb4;为什么必须utf8mb4?进销存单据号常含emoji(如客户备注“✅已付款”)、生僻字(如“䶮”、“堃”),utf8仅支持3字节,会截断报错
Incorrect string value。
- 修正SQL脚本中的时区问题: 打开
init.sql,查找所有DEFAULT CURRENT_TIMESTAMP,替换为:
-- 原始(危险!) `create_time` datetime DEFAULT CURRENT_TIMESTAMP, -- 替换为(显式指定时区,避免服务器时区漂移) `create_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,血泪经验:某次客户服务器时区设为UTC+0,而业务要求东八区时间,导致所有单据创建时间比实际晚8小时,财务对账直接崩盘。
- 外键约束必须显式开启(尤其MySQL 8.0默认禁用):
-- 执行前确认 SHOW VARIABLES LIKE 'foreign_key_checks'; -- 若为OFF,则执行 SET FOREIGN_KEY_CHECKS = 1;否则purchase_order表插入时因supplier_id外键未关联supplier表而报错Cannot add or update a child row。
2.3 后端服务启动:绕过Shiro认证拦截的调试模式
Spring Boot项目启动类通常叫ErpApplication.java。但直接java -jar target/erp.jar会卡在登录页——因为Shiro默认拦截所有/api/**请求。调试阶段需临时关闭认证:
- 在
application-dev.yml中添加:
shiro: filter: anon: /swagger-ui.html,/webjars/**,/v2/api-docs,/druid/**,/actuator/** authc: /** # 此行暂时注释掉- 启动时强制使用dev配置:
java -jar target/erp.jar --spring.profiles.active=dev- 验证启动成功:
- 访问
http://localhost:8080/druid(Druid监控台),输入默认账号admin/admin - 查看SQL监控中是否有
SELECT * FROM sys_user WHERE username = ?——证明MyBatis已连通数据库 - 若出现
Whitelabel Error Page,检查pom.xml是否漏加spring-boot-starter-thymeleaf
2.4 前端联调:当Vue页面报404,问题90%在后端跨域与静态资源路径
若源码包含vue-admin目录,需单独npm run serve。但常见错误是Vue请求/api/login返回404。此时不是前端错了,而是后端没配好:
- 检查
WebMvcConfigurer实现类(常位于config/WebConfig.java):
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 必须精确匹配前端请求路径 .allowedOrigins("http://localhost:8080") // Vue默认端口 .allowCredentials(true) .maxAge(3600); }注意:
allowedOrigins不能写*,否则allowCredentials=true会失效,浏览器报The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'
- 静态资源路径映射(针对Thymeleaf混合架构):
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将/static/映射到classpath:/static/ registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); // 关键:Vue构建后的dist目录需映射到根路径 registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/dist/"); // 确保dist目录存在 } }3. 核心业务闭环:采购入库→销售出库→库存扣减,三张表如何用事务锁死数据一致性
3.1 采购入库:从PO到Stock,两次更新必须原子化
进销存最脆弱环节是“采购入库”——用户提交入库单,系统需同时更新:
①purchase_order表的status字段(从“待入库”→“已完成”)
②stock表对应商品的quantity字段(增加入库数量)
若用两个独立SQL,网络中断会导致订单完成但库存未增,造成“幽灵库存”。正确做法是Service层加@Transactional,并用SELECT FOR UPDATE锁住库存行:
@Service public class PurchaseService { @Transactional(rollbackFor = Exception.class) public void confirmReceipt(Long poId) { // 1. 锁定采购订单(防止重复提交) PurchaseOrder po = purchaseOrderMapper.selectById(poId); if (!"WAIT_RECEIPT".equals(po.getStatus())) { throw new BusinessException("订单状态非待入库,无法确认"); } // 2. 锁定库存记录(关键!) Stock stock = stockMapper.selectOne( new QueryWrapper<Stock>() .eq("goods_id", po.getGoodsId()) .last("FOR UPDATE") // MySQL行锁,阻塞其他事务修改同一商品库存 ); // 3. 更新订单状态 po.setStatus("RECEIVED"); purchaseOrderMapper.updateById(po); // 4. 更新库存(在锁住的stock对象上操作) stock.setQuantity(stock.getQuantity() + po.getQuantity()); stockMapper.updateById(stock); } }参数说明:
last("FOR UPDATE")是MyBatis-Plus的SQL拼接方法,生成SELECT * FROM stock WHERE goods_id = ? FOR UPDATE。此锁在事务提交后才释放,确保库存变更绝对串行。
3.2 销售出库:乐观锁防超卖,版本号比分布式锁更轻量
销售出库面临“超卖”风险:A、B用户同时下单100件,库存只剩90件,若不加控制,两人均可能扣减成功。悲观锁(SELECT FOR UPDATE)会严重降低并发。进销存系统首选乐观锁:
- 在
stock表增加version字段(BIGINT,默认值1) - Mapper XML中写带版本校验的UPDATE:
<update id="decreaseStock"> UPDATE stock SET quantity = quantity - #{quantity}, version = version + 1 WHERE goods_id = #{goodsId} AND quantity >= #{quantity} AND version = #{version} <!-- 关键:版本号必须匹配 --> </update>- Service层捕获更新失败:
int rows = stockMapper.decreaseStock(goodsId, quantity, currentVersion); if (rows == 0) { // 版本号不匹配,说明库存已被其他事务修改 throw new BusinessException("库存不足,请刷新后重试"); }为什么不用Redis分布式锁?进销存单据量通常<1000TPS,MySQL行锁+乐观锁组合足够。引入Redis反而增加运维复杂度,且Redis故障会导致整个出库功能不可用。
3.3 库存流水:用数据库触发器还是应用层记录?选后者
stock_log表记录每次库存变动(谁、何时、何原因、增减多少)。有人用MySQL触发器自动生成,但强烈建议在应用层记录:
// 入库成功后,显式插入日志 StockLog log = new StockLog(); log.setGoodsId(po.getGoodsId()); log.setChangeType("IN"); // IN/OUT/ADJUST log.setChangeQuantity(po.getQuantity()); log.setOperator("system"); log.setRemark("采购订单" + po.getPoNo() + "入库"); stockLogMapper.insert(log);原因:触发器无法获取操作人(
sys_user.username)、无法写入业务单据号(po_no)、异常时难以回滚。而应用层日志可随主事务一起提交/回滚,且便于后续对接ES做库存溯源分析。
4. 避坑指南:五个让开发者凌晨三点还在查日志的真实问题
4.1 现象:登录成功后跳转首页,但所有API返回401
原因:Shiro的SecurityManager未正确注入RememberMeManager,导致Session未持久化。源码中常遗漏配置:
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); // realm配置正确 // ❌ 缺少这行! securityManager.setRememberMeManager(new CookieRememberMeManager()); return securityManager; }解决:在ShiroConfig.java中补全RememberMeManager配置,并确保Cookie的maxAge设为合理值(如60 * 60 * 24秒)。
4.2 现象:Druid监控台显示SQL执行慢,但EXPLAIN显示索引命中
原因:MySQL的innodb_buffer_pool_size过小(默认128M),导致频繁磁盘IO。进销存系统需至少2G:
-- 查看当前设置 SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- 临时调整(重启失效) SET GLOBAL innodb_buffer_pool_size = 2147483648; -- 永久生效:修改my.cnf [mysqld] innodb_buffer_pool_size = 2G4.3 现象:导出Excel报表时中文乱码,字段名显示为??
原因:Apache POI 4.x+默认用UTF-8,但Tomcat 8.5+的URIEncoding未设为UTF-8,导致GET请求参数乱码。
解决:在application.yml中强制设置响应编码:
spring: http: encoding: charset: UTF-8 force: true servlet: context-path: /并在pom.xml中确认POI版本≥4.1.2(修复了Excel导出UTF-8 BOM问题)。
4.4 现象:定时任务(如日结库存)在Linux服务器不执行
原因:源码中用了@Scheduled(cron = "0 0 23 * * ?"),但未启用定时任务支持。
解决:在启动类上加@EnableScheduling,并检查Linux时区:
# 查看服务器时区 timedatectl status # 若为UTC,需改为Asia/Shanghai sudo timedatectl set-timezone Asia/Shanghai4.5 现象:修改商品价格后,历史销售单据的金额未联动更新
原因:这是ERP设计常识性错误!销售单据必须保存当时的商品快照(sale_price、tax_rate等),而非实时关联商品表。源码中sale_order_item表若只有goods_id外键,没有sale_price字段,即存在致命缺陷。
解决:立即在sale_order_item表中添加sale_price DECIMAL(10,2)字段,并在创建销售单时将当前商品价格写入该字段。永远不要在财务单据中用JOIN实时查价格。
5. 生产就绪 checklist:从源码到交付,这12项必须人工核验
把源码变成客户能签字验收的系统,远不止“能登录”那么简单。以下是我经手23个进销存项目后沉淀的硬性核验清单,每项都对应过真实翻车现场:
| 序号 | 核验项 | 检查方法 | 不通过后果 | 我的实操技巧 |
|---|---|---|---|---|
| 1 | 数据库备份脚本是否可用 | 手动执行mysqldump -u root -p erp_db > backup.sql,再mysql -u root -p erp_db < backup.sql验证恢复 | 客户服务器宕机后无法还原,合同违约 | 备份脚本中必须含--single-transaction --routines --triggers,否则BOM表存储过程丢失 |
| 2 | 所有日期字段是否带时区处理 | 查application.yml中spring.jackson.time-zone=GMT+8,并检查LocalDateTime字段的@JsonFormat注解 | 跨时区访问时单据时间错乱8小时 | 统一用ZonedDateTime替代LocalDateTime,序列化时自动带+08:00 |
| 3 | 日志文件是否按天滚动且保留30天 | 查logback-spring.xml中<rollingPolicy>配置,手动touch测试文件生成 | 磁盘爆满导致服务假死 | fileNamePattern必须含%d{yyyy-MM-dd},maxHistory设为30 |
| 4 | 敏感信息是否脱敏 | 检查SysUser实体类中password字段是否有@JsonIgnore,phone字段是否有@JsonSerialize(using = PhoneDesensitizeSerializer.class) | 客户投诉隐私泄露,法律风险 | 自定义序列化器:return value.substring(0,3) + "****" + value.substring(7) |
| 5 | 权限控制粒度是否到按钮级 | 登录不同角色账号,尝试点击“删除供应商”按钮,观察是否403 | 客户说“你们系统不安全”,丢单 | Shiro中@RequiresPermissions("supplier:delete")必须与前端v-if="hasPermission('supplier:delete')"严格一致 |
| 6 | 打印模板是否适配A4纸 | 用Chrome打印预览/print/purchase?poId=123,检查边距、表格线是否完整 | 客户财务拒收纸质单据 | 模板CSS中强制@page { size: A4; margin: 0.5cm; },表格用table-layout: fixed |
| 7 | 外部接口(如短信)是否可热切换 | 修改application-prod.yml中sms.api.url,不重启服务,发测试短信验证 | 对接第三方失败时无法快速降级 | 用@RefreshScope注解配置类,配合Nacos配置中心 |
| 8 | 批量导入Excel是否做行级错误反馈 | 上传含100条数据的Excel,故意让第5行商品编码不存在,检查是否返回“第5行:商品编码XXX不存在” | 用户无法定位错误数据,反复试错 | 使用EasyExcel的AnalysisEventListener,在invoke()中捕获异常并记录行号 |
| 9 | 单据编号是否全局唯一且可读 | 查purchase_order表po_no字段,规则应为PO202405200001(日期+6位序号) | 财务对账时发现重复单号,整月账目作废 | 用Redis原子计数器生成序号:INCR po_seq_20240520 |
| 10 | 文件上传是否限制类型与大小 | 尝试上传.exe文件,检查是否拦截;上传200MB文件,观察是否超时 | 服务器被植入木马,或磁盘占满 | application.yml中设spring.servlet.multipart.max-file-size=10MB,Controller用@RequestParam MultipartFile file接收 |
| 11 | SQL注入防护是否生效 | 在搜索框输入' OR '1'='1,检查是否报错或返回全部数据 | 黑客拖库,客户起诉 | MyBatis必须用#{}而非${},动态表名用<bind>标签预处理 |
| 12 | JVM参数是否优化 | 查启动脚本start.sh中java -Xms2g -Xmx2g -XX:+UseG1GC ... | 高并发时Full GC频繁,响应超时 | -Xms与-Xmx必须相等,G1GC的-XX:MaxGCPauseMillis=200 |
最后说个我踩过的坑:曾有个客户要求“所有单据PDF存档”,我直接用iText生成,结果生成100页PDF耗时12秒。后来换成wkhtmltopdf命令行工具(HTML模板+CSS),同样100页只要1.8秒。技术选型没有银弹,只有场景最优解。这套Java进销存源码的价值,不在于它多完美,而在于它逼你直面每一个生产细节——从MySQL的innodb_buffer_pool_size到PDF生成的毫秒级优化。当你亲手拧紧这12颗螺丝,你就不再是个“会跑源码”的人,而是能扛起一个ERP交付的技术负责人。希望帮到你。
本文还有配套的精品资源,点击获取