☰
Java商品采购管理系统核心设计与实践指南
2026/9/28 8:32:28 网站建设 项目流程

1. 项目到底在解决什么问题

聊到 Java 商品采购管理系统,我估计很多第一次接触的人会有个误区:以为它就是个“进货记账本”。真不是。我见过不少开源仓库,也帮人改过这类系统做毕业设计,可以说“采购管理”在真实业务里牵扯的东西比想象中复杂得多。它要回答的核心问题是:一批货从“决定要买”到“摆在仓库里”,中间涉及的供应商、订单、审批、入库、对账、价格波动、库存预警,这些环节怎么被一套系统高效地串联起来,而不是靠 Excel 和微信群来回传文件。

这也是为什么这类系统在 GitHub 上常年热门、在 Java 课程设计选题里屡屡出现。因为它的业务边界清楚、模块划分典型、技术栈覆盖广,非常适合拿来练手或者二次开发。但恰恰因为太常见,反而很多人直接 clone 一个仓库就跑,最后发现代码也看不懂、业务逻辑也对不上,改起来更是无从下手。

所以这篇内容我不会只丢给你一个“项目介绍”,而是把这类 Java 开源采购管理系统从里到外拆开讲。你拿到手之后不只是能跑起来,还能知道它每一层在干什么、关键代码为什么要那么写、数据库表为什么那么设计,以及真正上手时会踩哪些坑。

先给这句话定个调:一个合格的 Java 商品采购管理系统,核心不是“增删改查”,而是“状态机的流转”和“数据一致性的保障”。理解了这句话,后面所有代码和表结构你都能看懂。

2. 系统核心模块与功能边界

2.1 从采购需求到入库完成的全链路

一个完整的采购流程通常长这样:某个部门或者仓库发现库存低于安全线,提出采购需求;采购员根据需求选择供应商、询价、生成采购订单;负责人审批通过后,供应商发货;到货后库管员验收、入库;最后财务对账、记录付款。整个过程里,“商品”“供应商”“采购订单”“入库单”“库存台账”这几个实体是核心。

开源项目里的功能边界也基本围绕这条链路展开。我梳理一下最常见的模块划分:

  • 基础资料:商品信息、供应商档案、分类管理。商品要维护名称、规格、条码、单位、默认供应商、参考进价等。
  • 采购业务:采购申请(需求)、采购订单、到货登记、退货单。这是主干流程。
  • 库存管理:入库记录、当前库存、库存流水。每一次采购入库都要联动更新库存表和流水表。
  • 报表统计:采购汇总表、供应商供货统计、价格波动分析、库存预警列表。
  • 系统管理:用户、角色、菜单权限。一般基于 RBAC 模型做。

这些模块说起来不复杂,但有个容易被新手忽略的点:单据之间是有先后关系和状态约束的。比如你不能把一张已经“审核通过”的采购订单退回草稿状态,也不允许在“待审核”状态下直接入库。很多开源项目在这个地方会做状态枚举和流程校验,这就是我刚才说的状态机流转。

2.2 业务数据如何落到数据库表结构

聊完功能,得看数据层面。我看过太多项目 clone 下来,结果连数据库脚本都没跑通。所以这里我挑最核心的几张表,把字段设计逻辑讲明白。

商品表(product):核心字段包括 id、product_name、specification(规格)、unit(单位)、price(当前采购价)、stock_quantity(当前库存)、safety_stock(安全库存)、status 等。这里要注意,商品表和分类表是一对多关系,分类字段设计成 category_id 外键就够用了,不需要冗余分类名称。

供应商表(supplier):id、supplier_name、contact_person、phone、address、bank_account、status。很多系统还会在这里加一个 price_rating 或者 quality_rating 字段,用来做供应商评估。

采购订单表(purchase_order):这张表是重量级的,字段很多。常见的有 id、order_no(唯一编号)、supplier_id、order_date、expected_arrival_date、total_amount、status(0草稿/1待审核/2已审核/3已到货/4已入库/5已取消)、create_by、create_time、remark 等。

采购订单明细表(purchase_order_item):id、order_id(外键)、product_id、quantity、purchase_price、subtotal。这里要注意,为什么不把商品和数量直接塞进主表?因为一张单要买多个商品,明细表天然是主表的子表,一对多关系。

这几张表是最核心的。开源项目一般还会把“入库单”和“采购订单”分开建表,入库单又关联一个入库明细表。它们之间的关联逻辑是:采购订单审核通过后,可以生成入库单(或者直接点击“入库”按钮生成),入库数量不能超过订单数量。这个校验就是数据一致性保障的关键。

值得多提一句:唯一编号 order_no 的设计非常重要。很多项目直接用自增 ID 当业务编号,这在演示还行,真实场景很容易被运营吐槽。稍微成熟一点的开源项目会采用“日期 + 随机数”或者“日期 + 序列”的方式生成单号,例如PO20250415001。这个细节可以拿来在面试里讲。

2.3 状态流转:采购单的生命周期管理

采购单从创建到最终归档,一般有这几个状态:草稿、待审核、已审核(已下单)、部分到货、完全到货、已入库、已取消。

为什么要搞这么多状态?因为采购操作不是一次性的,供应商可能分批发货,仓库可能分批入库。如果你只搞一个“已完成”和“未完成”,那系统根本无法回答“这批货到底到了多少、还欠多少”这个问题。所以开源项目常见的做法是:

  • 在订单主表记录 order_status 大状态
  • 在订单明细表上记录每个商品的 arrived_quantity 已到数量和 inbound_quantity 已入库数量
  • 通过比较 quantity 和 arrived_quantity 的关系,推导出明细行级的状态

这里有个很实用的开发技巧:不要在明细表里再单独存一个“明细状态”字段,而是通过数量关系实时计算。这样可以避免数据不一致,比如状态写成“部分到货”但数量却等于订单数量,这种脏数据就是典型的过渡设计导致的。

我用代码片段展示一下这个推导逻辑,你拿去可以直接用:

if (item.getArrivedQuantity() == 0) { item.setItemStatus("未到货"); } else if (item.getArrivedQuantity() < item.getQuantity()) { item.setItemStatus("部分到货"); } else if (item.getArrivedQuantity() >= item.getQuantity() && item.getInboundQuantity() < item.getQuantity()) { item.setItemStatus("已到货待入库"); } else { item.setItemStatus("已完成"); }

主表的 order_status 则根据明细行的汇总结果向上推导:全部明细“未到货”则订单状态为“待审核/已审核”,任一明细“部分到货”则订单为“部分到货”,全部“已到货待入库”就显示“待入库”,全部“已完成”则订单归档。这种状态推导模式,比状态机用事件回调去逐个改写主表状态要清爽得多,也不容易出现状态跑飞的问题。

3. 技术栈选型解析:为什么是这些组件

3.1 后端:Spring Boot 依然是这类项目的主力框架

现在 GitHub 上你能搜到的 Java 商品采购管理系统开源项目,九成以上是基于 Spring Boot。原因很直接:Spring Boot 对中小型管理系统来说确实好用,内置 Tomcat、自动配置一大堆模板代码都可以省掉,起步快,资料多,遇到问题随便一搜就是一堆答案。

这里顺便回答一个高频疑问:SSM(Spring + Spring MVC + MyBatis)和 Spring Boot 到底选哪个?我的观点很明确:新项目、新学习,直接上 Spring Boot。并不是说 SSM 不能用于生产,而是 Spring Boot 将大量的配置收敛成了自动装配,让开发者把精力放在业务代码而不是 XML 配置上。但如果你深处传统公司、维护老项目,SSM 的底子也得懂。观察开源项目的技术选型你会发现,它们几乎都同步提供两个版本或至少一个 Boot 版本,这个趋势很清晰。

在具体模块划分上,一个标准的分层结构大致是这样的:

controller(接收请求、参数校验) service(业务逻辑、事务控制) mapper/dao(数据库访问) entity/model(实体对象) dto/vo(数据传输/视图对象) config(配置类)

很多开源项目会在此基础上加一个common或core包,放统一返回结果 Result、异常处理、工具类、常量定义。我第一次看这类项目时,最困惑的就是项目中一堆AjaxResult、ResultCode之类的东西,后来理解了:统一返回体是好习惯,它让前端拿到的数据结构永远是一致的长相,比如{code:200, msg:"操作成功", data:{...}},前端可以统一做拦截。

3.2 数据持久层:MyBatis-Plus vs Spring Data JPA

这是个非常经典的技术选型争议。两类框架在开源项目里都有大量使用,但我见到的商品采购管理系统项目里,MyBatis-Plus 出现的频率明显更高。

MyBatis-Plus 的好处有三点最直接:第一,单表 CRUD 不需要手写 SQL,内置了 BaseMapper 那样的通用接口,userMapper.selectById(1)就能查出实体;第二,条件是靠 LambdaQueryWrapper 构造器拼出来的,阅读代码时能顺着业务逻辑读下去,而不需要在 XML 和 Java 之间来回跳;第三,支持分页插件,一句PageHelper或者MybatisPlusInterceptor配置就能完成分页查询。

对应的,JPA 在关联查询和多表聚合上有优势,因为你可以用实体之间的映射关系直接导航到关联数据。但很多 Java 开发者对 JPA 的“懒加载”“N+1 查询”问题有心理阴影,尤其在多表关系复杂的库存类系统中,出现几次性能问题就开始劝退了。

我的习惯是:如果是快速原型、课设级别的项目,MyBatis-Plus 能大幅度提升效率;如果业务中复杂报表查询非常多,那 MyBatis/MyBatis-Plus 配合手写 SQL 反而更可控。采购管理系统恰好是这两者的混合体——日常 CRUD 多、报表查询也多,所以主流的开源方案选了 MyBatis-Plus 并不意外。

3.3 前端:从 JSP 到 Vue 的演变趋势

早期或者教学性质特别强的开源项目,会直接使用 JSP + Bootstrap,服务端渲染页面,Java 里写 ModelAndView 返回视图。我曾经也干过这事,说实话 JSP 项目部署起来挺省心的,一个 war 包丢到 Tomcat 就能跑,对新手友好。

不过现在稍微有点“现代感”的开源项目,基本都是前后端分离架构:Vue 2/3 + Element UI/Element Plus,后端只提供 JSON 接口。这样做的好处是前端部署可以是独立的 Nginx 服务,后端可以换网关、做服务化改造,二开空间更大。代价是你要在本地装 Node.js、执行 npm install、配置代理转发。这一步对很多不熟悉前端的 Java 开发者来说,反而成了跑通项目的第一大坎。

4. 从 Clone 到跑通:完整实操记录与踩坑复盘

4.1 本地环境准备与初始化

我先说下我实操时用的环境组合,这套组合在目前的开源项目里兼容性特别稳:

  • JDK:1.8 或 11(如果项目标注了 Spring Boot 2.x,就千万不要用 JDK 17,否则会出现一些诡异的反射异常)
  • Maven:3.6 以上
  • MySQL:5.7 或 8.0
  • IDE:IDEA Community/Ultimate 都可以
  • Node.js:14/16(仅当前端是 Vue 2 项目时,Vue 3 项目最好是 16 以上)

把仓库 clone 下来之后,第一步不是直接跑,而是先读文档。百分之八九十的项目 README 里会写环境要求、导入步骤和数据库脚本位置。如果 README 里什么也没有,那就按下面的顺序排查结构:

  1. 看pom.xml里的 Spring Boot 版本和依赖,锁定技术栈
  2. 找到sql目录或者根目录下的.sql文件
  3. 看application.yml或者application.properties,确认数据库连接配置方式

接来下我以典型的 Spring Boot + Vue 前后端分离项目为例,走一遍完整的流程。

4.2 数据库脚本导入的两种方式

我强烈建议你在命令行用 mysql 客户端导入,比用 IDEA 图形化工具导入要更可靠:

mysql -u root -p < /path/to/schema.sql

如果项目提供了data.sql(也就是种子数据),通常顺序是先执行 schema(建表),再执行 data(初始化数据)。很多项目偷懒,只给一个完整的init.sql,那直接执行它就行。

导入完成后,建议顺手检查一下:

SHOW TABLES; SELECT COUNT(*) FROM product;

如果 product 表有数据,说明种子数据成功导入。如果某些表是空的,也不用紧张,很多项目为了演示把商品数据做成了后台管理录入,而不是预置数据。

4.3 后端启动前的三个必改配置

数据库配置百发百中必定要改,否则项目跑不起来。典型的改动位置是application.yml:

spring: datasource: url: jdbc:mysql://localhost:3306/purchase_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver

三个必改点分别是:数据库名(如果脚本建库名不叫 purchase_system 要对应改)、用户名、密码。很多同学在这里卡住,是因为密码里有特殊字符比如@、#没有处理——在 yml 里面密码建议用引号包起来,比如password: "abc@123",否则解析会出问题。

第三个需要留意的是时区参数serverTimezone。MySQL 8.0 的驱动对时区敏感,不加serverTimezone=Asia/Shanghai就会报一个 “The server time zone value” 的异常。这属于典型的“不是代码问题、是配置问题”的坑。

启动类一般长这样:

mvn spring-boot:run

如果依赖已经正确下载,控制台会刷出 Spring Boot 的启动 Logo,看到 “Started Application in xx seconds” 就算启动成功。如果没有安装 Maven 的独立命令行工具,也可以在 IDEA 里直接执行启动类的 main 方法。

4.4 前端工程的启动与代理配置

前端的 node_modules 安装是个耐心活。如果你所在网络环境比较差,建议提前切换 npm 镜像源:

npm config set registry https://registry.npmmirror.com

切换完之后再执行:

npm install

装完依赖后启动开发服务器:

npm run dev

Vue 项目默认启动在 9528 端口(常见配置),然后需要在vue.config.js里确认 devServer 的 proxy 配置。如果没有配置代理而前端代码里请求地址是/api/xxx,那这些请求会打到前端服务器 9528 端口,后端接口 8080 端口就没反应。正确配置长这样:

module.exports = { devServer: { port: 9528, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

这一步能解决 90% 的“前端页面打开但数据加载不出来”的问题。

4.5 后端数据库时区问题与中文乱码的处理

前面提到serverTimezone参数,我再展开讲讲这个问题的常见表现形式。如果你使用的是 MySQL 8.0 的驱动包,启动后偶尔会在凌晨零点后(也就是北京时间 0 点但 UTC 时间还是前一天 16 点多)报错,或者接口查询时间比实际晚 8 小时。这个不是 Java 代码的 bug,而是连接参数和 JVM 时区不一致导致的。

规范一点的写法是,在 JVM 启动参数或配置中明确指定时区:

-Duser.timezone=Asia/Shanghai

至于中文乱码,分两种情况:一种是数据库表字段的字符集不是 utf8mb4,导致存进去是乱码;另一种是前端页面显示时编码不一致。推荐在建库时显式指定字符集:

CREATE DATABASE purchase_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

utf8mb4 比 utf8 多出来的是对表情符号和特殊字符的支持,虽然不是采购系统刚需,但存商品备注里万一有个“”之类的特殊符号,utf8 就当场报错。

5. 核心业务逻辑与代码改造实战

5.1 采购下单时如何校验库存和价格

接下来我带你过一遍采购订单从创建到入库的核心代码逻辑,这里是最能体现系统业务严谨性的地方。

创建采购订单的 service 方法通常会长这样:

public Result createPurchaseOrder(PurchaseOrderDTO dto) { // 1. 校验供应商是否存在且状态正常 Supplier supplier = supplierMapper.selectById(dto.getSupplierId()); if (supplier == null || supplier.getStatus() != 1) { return Result.error("供应商不存在或已停用"); } // 2. 循环检查每个明细商品的合法性 BigDecimal totalAmount = BigDecimal.ZERO; List<PurchaseOrderItem> itemList = new ArrayList<>(); for (PurchaseOrderItemDTO itemDTO : dto.getItemList()) { Product product = productMapper.selectById(itemDTO.getProductId()); if (product == null) { return Result.error("商品ID=" + itemDTO.getProductId() + " 不存在"); } // 校验采购数量必须大于 0 if (itemDTO.getQuantity() <= 0) { return Result.error("商品" + product.getProductName() + "采购数量必须大于0"); } // 计算明细金额 BigDecimal subtotal = product.getPurchasePrice() .multiply(BigDecimal.valueOf(itemDTO.getQuantity())); totalAmount = totalAmount.add(subtotal); } // 3. 保存订单主表和明细表 ... }

这段代码要学习的核心思维习惯是:任何对数据库的写操作之前,先把业务规则校验完。比如商品数量负数这种数据,如果不在这一层拦截掉,后面入库、盘点、报表全部都会被污染。

还有一点是关于 BigDecimal 的。这里特别强调:金额计算一律用 BigDecimal,禁止用 double 或 float。原因学过计算机组成原理的都懂,二进制无法精确表示 0.1 这种十进制小数,用浮点算钱差一分钱是常态,这在采购系统里是致命的。JDK 里 MySQL DECIMAL 映射到 Java 类型就是 BigDecimal,用就对了。

5.2 入库操作中的事务管理与一致性保障

入库是整个系统里最需要严谨对待的操作,因为涉及多张表联动更新:采购订单状态要变、明细表入库数量要累加、库存表要增加、库存流水表要插入新记录。任何一步失败,都会导致库存数据和单据对不上。

这时就得用事务。Spring 里最直观的方式就是加@Transactional注解:

@Transactional(rollbackFor = Exception.class) public Result inbound(PurchaseInboundDTO dto) { // 1. 记录入库单主表 // 2. 更新采购订单明细的已入库数量 // 3. 更新商品库存余额 // 4. 写入库存流水 }

这里有个重要细节:为什么rollbackFor = Exception.class必须写?因为 Spring 默认只在运行时异常(RuntimeException)时回滚,如果业务代码里 catch 住了自定义异常又没往外抛,事务就失效了。很多刚接触的人在这里踩坑,误以为事务没生效,其实是异常被吞了。

再进一步,能不能并发入库?想象这个场景:仓库管理员同时提交两张针对同一订单的入库单。如果不做额外控制,数据库里的“已入库数量”可能会被覆盖更新成错误值。解决办法有两种:

  • 悲观锁:查询时加SELECT ... FOR UPDATE锁住采购订单明细行,直到事务结束才释放
  • 乐观锁:在明细表加 version 字段,更新时比较 version 和当前版本,不同则说明数据被修改,返回失败

开源项目里为了演示方便很少做并发控制,但真实业务中这是必备的。讲面试时如果能主动谈到这两条方案,会是一个很大的加分点。

5.3 库存流水表的设计逻辑:为什么不能只改库存

我第一次设计库存模块时犯过一个低级错误:只更新 product 表的 stock_quantity 字段,不记录变化过程。结果库存数量对不上时,根本没法排查是从哪一笔订单开始错的。后来看成熟项目的设计,才意识到库存流水表(stock_flow)是必须的。

库存流水表的核心字段一般包括:id、product_id、flow_type(1入库/2出库/3盘盈/4盘亏)、quantity、before_quantity、after_quantity、source_order_no(来源单号)、create_time、create_by。

每次变更库存时,先查出当前库存作为 before_quantity,计算出变更后的 after_quantity,同时往流水表插一条记录。这样任何时候只要把商品表的库存和流水表里的汇总值对账,数据有没有脏、哪里脏,一目了然。

5.4 报表统计中的 MySQL 聚合查询与时间范围处理

报表是采购管理系统的招牌功能。常见的报表需求有:

  • 每月采购总额趋势
  • 每个供应商的采购占比
  • 热销商品排行
  • 库存预警清单

这些报表几乎都用 MySQL 的聚合函数加时间范围筛选。举一个按月汇总采购金额的 SQL 写法:

SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(total_amount) AS month_total FROM purchase_order WHERE order_status NOT IN (0, 5) GROUP BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY month DESC;

注意这里有个细节:查询时把 “草稿状态” 和 “已取消状态” 排除掉,否则统计数据会虚高。报表数据的准确性,不只是靠 SQL 写对,更取决于业务源头有没有做好状态约束。这一点也是系统设计的自洽性体现。

6. 二开与扩展:从跑通到“成为自己的项目”

6.1 权限系统的引入:从单用户到多角色

很多开源采购管理系统默认是单用户或者简化登录,所有操作都只有一个 admin。真实场景显然没有这么简单:采购员能创建订单但不能审核,库管员只做入库操作,财务只看报表。所以用户拿到项目后,第一件事通常是想加权限。

我推荐直接集成 Spring Security + JWT 的方式做无状态认证。Spring Security 在 Spring Boot 里集成不算复杂,核心三步:自定义 UserDetailsService 加载用户、配置 SecurityFilterChain 的放行路径、加 JWT 过滤器做 token 校验。配合数据库里的 user、role、menu、user_role、role_menu 这几张表,就能实现 RBAC。

这里有个省事的思路:不一定要把权限做得非常精细到按钮级别。很多系统做到“菜单权限”级别就够用,即不同角色看到不同菜单、点击不同功能模块。真正到按钮级权限,用 Vue 的 v-permission 指令加上后端接口权限校验,工作量至少要翻一倍。

6.2 仿造 RestAPI 版本分离:为什么要关注接口设计

如果你打算把这套系统作为面试项目来讲,那接口设计的规范化是必须提的。我建议看看开源项目的 controller 代码,绝大多数接口路径设计是符合 REST 风格的:

功能接口路径HTTP方法
分页查询采购订单/api/purchaseOrdersGET
创建采购订单/api/purchaseOrdersPOST
更新采购订单/api/purchaseOrders/{id}PUT
审核订单/api/purchaseOrders/{id}/approvePUT
删除订单/api/purchaseOrders/{id}DELETE
查询订单详情/api/purchaseOrders/{id}GET

这套风格的好处是,路径即语义,前端对接时几乎不需要额外文档。如果你二开时发现项目原有接口都是/getPurchaseOrderById那种动词命名的,建议顺手重构一下,对你理解 Mapper、Service、Controller 之间的流转也帮助很大。

6.3 增加采购审批流程的思路

前面讲了状态机,但很多开源项目里的“审核”只是把 status 从 1 改成 2,没有真正的审批流。如果企业里要求“采购金额超过 1 万需要部门经理审批,超过 5 万需要总经理审批”,这时候就得加审批流模块。

最简单的做法是:在采购订单主表加一个 required_approval_level 字段,金额不同对应不同审批层级,再建一张 approval_record 表记录每级审批的操作人、结果、意见、时间。每次审核请求进来,先判断当前操作人的角色是否满足 level 要求,满足才允许通过。这样做比引入 Flowable/Activiti 那种重量级工作流引擎要轻得多,而且没有学习成本。

6.4 进销存一体化的扩展方向

采购到位后,自然会产生销售业务。很多人在做完整采购系统以后,会考虑把它扩展成“进销存(采购、销售、库存)”一体化系统。这个扩展方向我认为非常自然,因为库存表的 model 已经支持入库出库两种流水类型,销售模块只需要反向增加“销售订单 + 出库”流程即可。

但扩展时要小心一个问题:商品成本核算。采购入库的价格可能每批次不同,那么销售出库时,成本是按“先进先出(FIFO)”算,还是“移动加权平均”算?绝大多数小系统采用移动加权平均,实现比较简单:每次入库后重新计算商品的平均成本,销售出库按这个平均成本结转。

这个细节可以作为另一个加分点:说明你不仅懂技术,还理解基本的企业财务原理。

7. 常见问题与排查技巧实录

7.1 后端启动报错合集

错误关键字常见原因解决方案
ClassNotFoundException: com.mysql.jdbc.Driver驱动类路径不对将com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver
Access denied for user数据库用户名/密码错误检查 yml 配置,注意特殊字符加引号
Unknown database数据库未创建或库名不对执行建库脚本或改 url 中的库名
Fieldiddoesn't have a default value主键没有自增策略检查表结构,主键字段改为 AUTO_INCREMENT
The server time zone valueMySQL 时区问题url 参数加serverTimezone=Asia/Shanghai
Invalid bound statementMyBatis XML 映射没扫到检查 mapper 接口和 XML 的 namespace 是否一致

7.2 前端跑通后数据不展示的排查路径

前端页面能打开、但表格里没有数据,这是前后端分离项目最普遍的故障,排查顺序非常重要:

  1. 打开浏览器 F12,看 Network 里请求的状态码。如果是 404,多半是代理没配好或后端接口路径不对。
  2. 如果是 200,但 data 是空数组,可能是数据库里确实没数据,或者是查询条件带着默认值筛空了。可以先在数据库里执行一遍同样的 SQL。
  3. 如果是 401/403,说明登录鉴权拦截了接口,检查前端 token 是否带上,或者后端的白名单路径没配完整。
  4. 如果是 500,去后端控制台看日志,大概率是 SQL 执行错误或者空指针。

7.3 一个经典案例:明明加了商品却查询不到

我去年帮人排查过一个问题:商品管理页面能新增,新增完提示成功,但列表刷新后看不到数据。第一反应是新数据没写进去,结果查数据库,数据都在。再看列表查询接口,发现分页查询里加了一个条件status = 1,而新增时未给 status 赋值,MySQL 的默认值是 0,所以列表永远查不出状态为 0 的数据。

这种问题不是 Bug,是“业务约束不一致”。所以后来我在项目里就养成了一个习惯:实体类里所有状态字段都要显式初始化,而不是依赖数据库默认值。比如:private Integer status = 1;这样写,新数据默认就是可用状态,避免默认值和查询条件的错位。

7.4 性能优化:分页查询与大表索引

当采购订单积累到几十万条时,列表查询会明显变慢。这时候要检查的首先是索引设计。采购订单列表查询最常见的过滤条件是:供应商 id、状态、下单时间区间。对应的联合索引建议:

ALTER TABLE purchase_order ADD INDEX idx_supplier_status_time (supplier_id, order_status, order_date);

MySQL 的联合索引最左前缀原则,决定了查询条件必须从最左列开始用才能走到索引。如果项目已经设计好了索引但 SQL 里supplier_id没传,那索引就退化了。经常有开发者抱怨索引没生效,排查时用EXPLAIN SELECT ...看 type 和 key 字段,秒懂。

8. 聊聊我对这类开源项目的总体看法

做 Java 后端这行,几乎每个人都绕不开这样一套管理系统项目。它没有高并发、没有分布式、没有炫目的架构,但它是检验基本功最实在的练功房。你在这个项目里能不能把事务用对、把状态设计清楚、把数据库关系理明白,基本就代表了你能不能胜任真实企业里的中等复杂度业务开发。

我会推荐两类人重点研究这种项目:一类是正在准备毕业设计或者找实习的在校生,把它吃透、冲着二开去改,远比背一百道面试题管用;另一类是刚转行 Java 开发、想系统梳理业务建模能力的开发者,用这样的项目把“从需求到表、从表到代码、从代码到部署”的整条链路走一遍,后面再去看大项目就不会晕了。

最后再分享一个我个人验证过多次的小技巧:拿到任何开源项目,先别着急跑起来,花半小时用文本编辑器把项目的 README、目录结构、核心表结构都过一遍。在你脑子里把系统跑一遍,再按下启动键。顺序对了,你踩坑的概率至少少一半。如果这套采购系统能顺利跑通并且你能独立说清楚它的每条状态流转逻辑,那它的价值就远远不止一个 GitHub 星标数字了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询