最近在整理之前完成的一个基于 Spring Boot 的汽配销售管理系统,顺手把整套思路和踩坑记录沉淀下来。这是个很适合 Java 初学者和毕设党的完整项目,技术栈以 Spring Boot + JavaWeb 为核心,数据库用 MySQL,前端用服务端渲染模板,配合源码、文档、运行视频和讲解视频,能够帮你把课堂上学到的那些零散知识点串成一条完整的业务线。
我为什么推荐拿这种“管理系统”类型的项目来练手?原因很简单:它麻雀虽小,但五脏俱全,用户登录、权限控制、增删改查、复杂条件查询、库存联动、统计报表这些真实企业项目里高频出现的需求,它全部覆盖了。你把它吃透,再去面 Spring Boot 相关岗位时,很多面试题都能结合这个项目说出干货来,而不是只背八股文。这篇文章就围绕这个项目从头梳理一遍,从需求分析、表结构设计、后端接口落地、前端页面渲染,到最后的部署运行和常见问题排查,把每个关键环节的为什么和怎么做都讲明白。
1. 项目定位与整体设计思路
1.1 汽配行业的业务特征与系统需求拆解
先搞明白你要解决的业务问题。汽车配件销售听起来只是“卖东西”,但它和普通商品销售有一个非常大的区别:配件有极强的车型适配性。同样叫“刹车片”,它可能适配丰田凯美瑞 2018-2020 款,却装不上同品牌的其他车型;同一个零件在不同渠道还有原厂件、副厂件、品牌件之分,价格差距能翻好几倍。这就导致汽配销售的核心难点不在收银,而在“找得准、查得快、库存对得上”。
我在梳理需求时,把整个系统拆成了六个核心模块:商品管理(配件信息维护)、库存管理(出入库与库存盘点)、销售管理(开单下单与订单跟踪)、采购管理(供应商供货与补货入库)、客户管理(销售客户档案与历史记录)、系统管理(用户登录与角色权限)。其中商品模块必须支持按配件名称、OE 编号(原厂编号)、适用车型、品牌这几个维度检索,这是汽配业务里最刚需的功能,没有之一。
在技术选型上,我没有用很重的分布式微服务架构,而是坚持用了 Spring Boot 单体应用。对于这种中小体量的进销存系统,单体架构完全够用,部署简单、调试方便,逻辑链路清晰,特别适合学习者去逐行读懂每一段代码。你自己做项目或者中小公司内部系统,优先考虑单体 + 模块化分层,别一上来就引入 Nacos、Feign 那一套,增加无谓复杂度。
1.2 为什么选 Spring Boot + JavaWeb 这套技术组合
Spring Boot 和 JavaWeb 不是二选一的关系,而是“框架”与“基础”的递进关系。传统的 JavaWeb 指 Servlet、JSP、Filter、Listener 这一套底层规范,理解它你能搞明白 HTTP 请求从浏览器发出后,是如何被容器接收、分发、处理后返回响应的。而 Spring Boot 在这些规范之上做了大量封装,内置 Tomcat,自动装配各种组件,让你几行配置就能启动一个 Web 服务。
这套组合带来的直接好处有三个。第一,开发和调试效率高,Spring Boot 的自动配置省掉了以前 Spring 和 SpringMVC 里繁琐的 XML 配置,一个 @SpringBootApplication 注解就能启动应用,对初学者非常友好。第二,生态成熟,无论你接 MyBatis、JPA 还是 Redis,都有大量官方和社区文档,遇到问题搜一下基本都能解决。第三,学习曲线平滑,你先通过这个项目掌握 Spring Boot 的使用方式,后续再去理解它背后的自动装配原理、条件注解、Bean 生命周期这些底层机制,就有了实际的抓手。
提示:如果你现在对照着做,不要跳过 JavaWeb 基础直接学 Spring Boot。我见过不少新手,上来就写 Spring Boot 接口,结果遇到 404、请求参数拿不到、响应乱码这些问题时完全一头雾水,其实就是不理解 Servlet 和 HTTP 协议导致的。
1.3 项目整体架构与分层设计
这个项目采用的是经典的三层架构:Controller 层(接口接收与调度)、Service 层(业务逻辑处理)、Mapper 层(数据库持久化操作),再加上统一的实体类(entity/dto/vo)和公共工具类(util/config)。核心思想是“层层隔离、单向依赖”,Controller 不直接写 SQL,Service 不直接处理 HTTP 请求,这样每一层都能独立维护和测试。
分层设计在后端业务里的具体体现就是:一个典型的“新增配件”请求,会先由 Controller 接收前端传来的表单数据并做基础校验,然后调用 Service 层的 addPart 方法;Service 里会先检查配件编号是否重复、车型参数是否合法,再调用 Mapper 层执行 insert,最后把结果封装成统一返回对象回传给前端。数据流向清晰,出了问题也容易定位——是参数校验的问题,还是库存扣减的 SQL 写错了,顺着调用链很快就能找到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:汽配业务的表结构到底怎么建
2.1 配件信息表:汽配业务最核心的商品模型
配件表是整个系统的地基,设计得好不好直接决定后续所有功能的开发难度。我在建表时重点考虑了汽配行业区别于普通商品的两个关键需求:适配车型的关联和 OE 编号的标准化。
我设计的 part_info 表核心字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| part_code | varchar(50) | 配件内部编码,唯一 |
| oe_code | varchar(100) | 原厂 OE 编号,如 04465-0D030 |
| part_name | varchar(100) | 配件名称,如刹车片、机油滤清器 |
| brand | varchar(50) | 品牌,如博世、曼牌、原厂件 |
| vehicle_model | varchar(200) | 适用车型,如丰田凯美瑞 2018-2020 |
| category_id | bigint | 分类 ID,外键关联分类表 |
| purchase_price | decimal | 进货价 |
| sale_price | decimal | 销售价 |
| stock_quantity | int | 当前库存数量 |
| stock_warning | int | 库存预警阈值 |
| status | tinyint | 状态:1上架、0下架 |
| create_time | datetime | 创建时间 |
其中 vehicle_model 字段我建议存储结构化数据而不是自由文本,比如把“丰田 凯美瑞 2018-2020 2.5L”拆成品牌、车系、年份区间、排量几个字段,查询时用 LIKE 或者全文索引匹配。实际开发中有人为了省事直接存一个长字符串,结果用户搜“凯美瑞”时只能 LIKE '%凯美瑞%',数据量一大查询效率惨不忍睹。我自己做的时候是拆成了 vehicle_brand、vehicle_series 两个字段,再配合索引,既能精确搜又能模糊搜。
2.2 库存流水表:不直接改库存数字的底层逻辑
很多初学者做库存扣减时,喜欢直接在 part_info 表里执行 update stock_quantity = stock_quantity - number。这个写法看着简单,但会丢掉最关键的信息:库存变化的轨迹。当你想查“这批货是什么时候进来的、成本是多少、是哪个供应商送的”时,数据库里一片空白。
所以我在设计时加了一张 stock_flow 库存流水表,记录每一次库存变化的明细。字段包括:流水号、关联配件 ID、变化类型(1 入库、2 出库、3 盘点调整、4 退货)、变化数量(正负号表示增减)、操作前库存、操作后库存、关联订单号(销售单号或采购单号)、操作人、创建时间。
扣库存的正确姿势应该是:在同一事务里,先写入 stock_flow 流水记录,再更新 part_info 表的现有库存,并且更新库存时带上 where stock_quantity >= number 条件。这样既保留了完整审计轨迹,又能在多线程高并发下防止超卖。Redis 缓存在这里不是必须的,MySQL 的行锁配合事务已经能保证一致性——对这种业务场景,过度设计只会增加系统复杂度。
2.3 订单表与销售单据的关联设计
订单模块要回答的问题是:一张销售单里有哪些配件、数量多少、单价多少、总价多少、优惠了多少、由谁经手、卖给哪个客户。我用了主从表结构,主表 sale_order 存订单整体信息(订单号、客户、销售员、订单金额、实付金额、创建时间),从表 sale_order_item 存每一行配件明细(关联配件 ID、数量、单价、小计金额)。
主从表是管理系统的经典设计模式,主表存“头部信息”,从表存“行明细”。在保存订单时,Service 层需要同时处理多张表的数据,Controller 接收前端传来的订单主数据和配件明细列表参数,Service 层开事务先插入主表拿到订单 ID,再循环插入明细,最后统一扣减对应配件的库存。这个过程必须在事务里执行,任何一步失败都要整体回滚,否则会出现“订单建了但库存没扣”或“扣了库存但订单没建”的数据不一致问题。
多个配件存入一个订单时,要特别注意一个细节:扣减库存的顺序。如果项目后续有并发场景,建议按配件 ID 升序依次扣减库存,这样可以降低多个事务同时操作多行数据的死锁概率。这套项目的业务流量虽然不大,但养成这个习惯,写出来的代码更接近企业级水准。
还有一个实用的小经验:订单号和流水号不要用数据库自增 ID 直接展示,自增 ID 会暴露业务数据量,也容易被遍历猜测。我在系统里用的方案是“日期 + 随机数 + 业务类型前缀”,比如 XS2025010112340001,生成逻辑写在公共工具类里,业务层直接调用即可。
3. 后端核心功能落地:从零到一搭建 Spring Boot 后端
3.1 项目初始化与环境准备
搭建环境的分歧点主要在 Spring Boot 版本和 JDK 版本的选择上。我在做这个项目时选的方案是:JDK 1.8 + Spring Boot 2.7.x + MySQL 5.7/8.0 + Maven 3.6+。这套组合非常成熟,资料也多,踩坑成本低。不建议一上来就追最新的 Spring Boot 3.x,因为 3.x 强制要求 JDK 17,很多老版本教程和开源代码跑不起来,对新手来说排查成本太高。
如果使用 IDEA 创建项目,具体步骤是:File -> New -> Project -> Spring Initializr,然后填好 Group 和 Artifact,依赖勾选 Spring Web、MyBatis Framework、MySQL Driver。这里有个常见的坑,国内网络环境下 Spring Initializr 经常超时或卡住,解决方案有两个:把 Server URL 改成阿里云的镜像地址,比如 start.aliyun.com;或者先手动创建一个普通的 Maven 项目,然后在 pom.xml 里手动引入 Spring Boot 的 parent 和 starter 依赖。我推荐第二种方式,虽然多写几步,但你能看到每个依赖是干什么的,比 IDE 自动生成更理解项目的启动原理。
pom.xml 里的核心坐标大致是下面这样的结构,注意 MyBatis 需要额外引入 mybatis-spring-boot-starter,而数据库连接池我用的是 HikariCP,Spring Boot 2.x 默认自带,无需额外配置:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 核心配置文件:application.yml 的最佳实践
application.yml 是整个项目运行的中枢,很多新手配置完发现启动报错,绝大多数是这里的连接参数填错了。我把核心配置拆开来看:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/auto_part_sales?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.autoparts.entity
configuration:
map-underscore-to-camel-case: true
这里有几个关键细节要说明。serverTimezone=Asia/Shanghai 一定要加上,否则连 MySQL 8.x 时会报时区错误。useSSL=false 是避免本地开发时的 SSL 警告和连接失败。map-underscore-to-camel-case: true 打开后,数据库里的字段 create_time 就能自动映射到 Java 实体类的 createTime,省掉大量 XML 映射文件里的人工映射。
注意:如果你的 MySQL 是 5.7 版本,驱动类名可以保持
com.mysql.jdbc.Driver,但用 8.x 驱动连 5.7 也没问题,直接统一用com.mysql.cj.jdbc.Driver最省心。密码千万不要写成明文硬编码在代码里,可以通过环境变量注入,但在课程设计阶段为了便于复现,写在 yml 里是完全可以接受的。
3.3 登录认证与权限控制:基于拦截器的实现方案
权限控制是管理系统里绕不开的模块。这个项目里我用的是经典方案:登录时把用户信息存入 Session,然后定义一个拦截器统一校验请求是否已登录,同时校验访问路径与用户角色的匹配关系。这个方案比引入 Spring Security 或 Shiro 更轻量,也更容易让学习者理解权限控制的原始逻辑——先搞清楚原理,再去做框架集成会事半功倍。
核心实现分三步走。第一步,写一个 LoginInterceptor 类实现 HandlerInterceptor 接口,重写 preHandle 方法,从 HttpSession 中取出登录用户的标识,取不到就重定向到登录页面。第二步,写一个配置类实现 WebMvcConfigurer,注册拦截器并配置要拦截的路径,比如 addPathPatterns("/**") 排除 /login、/css/**、/js/** 等静态资源路径。第三步,在需要更细粒度控制的 Controller 方法上,用自定义注解或者简单判断当前用户角色,决定是否允许执行。
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
String user = (String) session.getAttribute("loginUser");
if (user == null) {
response.sendRedirect("/login");
return false;
}
return true;
}
}
配置类里的拦截规则,注意要放行登录接口和静态资源:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/css/**", "/js/**", "/images/**");
}
}
在实际调试中,我遇到过配置了拦截器但静态资源被拦截,页面样式全部丢失的情况,原因就是上面的排除路径没有写全。排查方式很简单,浏览器按 F12 看 Network 面板,哪些静态文件返回 302 就知道漏了哪些路径。
3.4 核心业务功能实现:配件查询、下单开单、库存联动
配件的多条件查询是整个系统使用频率最高的接口,用户在页面上输入配件名称、OE 编号、品牌、车型,系统要能灵活组合条件返回结果。我使用的是 MyBatis 的动态 SQL 来实现,在 PartMapper.xml 的 selectPartList 方法里,通过 <where> 标签结合 <if> 逐条件拼接,只有用户填了对应字段才追加过滤条件。这样做的好处是只用写一个查询方法,就能应对所有组合场景,无需为每种查询单独写 SQL。
xml复制<select id="selectPartList" resultType="com.example.autoparts.entity.PartInfo">
select * from part_info
<where>
<if test="partName != null and partName != ''">
and part_name like concat('%', #{partName}, '%')
</if>
<if test="oeCode != null and oeCode != ''">
and oe_code = #{oeCode}
</if>
<if test="brand != null and brand != ''">
and brand like concat('%', #{brand}, '%')
</if>
<if test="vehicleSeries != null and vehicleSeries != ''">
and vehicle_series like concat('%', #{vehicleSeries}, '%')
</if>
</where>
order by id desc
</select>
查询接口一个容易忽略的优化点是,数据量变大后 LIKE '%keyword%' 无法命中索引,会走全表扫描。解决方案有两种:数据量大时引入搜索引擎或全文索引,这里不展开;还有一种是在汽配业务里常用的手段——优先支持 OE 编号的精确匹配,因为 OE 编号是国际标准,原厂配件查 OE 编号是行业内最常规的做法,把精确匹配字段建好唯一索引,速度会非常快。
销售开单接口是核心中的核心。它的业务逻辑用一段伪码展开就是这样:
text复制1. 前端提交:客户ID、销售员ID、配件ID列表、数量列表
2. 校验请求参数,数量不能为负数或超库存
3. 计算订单总金额,生成订单号
4. 在高并发的场景下,该处必须开启数据库事务,保证操作的原子性
在第 4 步开启事务后,具体操作顺序是:插入 sale_order 主单,拿到自增主键;循环插入 sale_order_item 明细行,计算每个配件的合计金额;更新 part_info 表库存,这里必须加条件 where stock_quantity >= #{buyNum},如果影响行数为 0 说明库存不足,直接抛出异常触发回滚;写入 stock_flow 库存流水,状态为出库。用 @Transactional 注解打在 Service 方法上,事务边界由 Spring 统一管理,抛出 RuntimeException 时自动回滚。
3.5 库存预警与统计报表的简单高效实现
库存预警在汽配行业里非常重要。刹车片、机油滤芯这类快消品的损耗速度快,一旦库存低于安全线而采购又没跟上,会直接影响门店的销售。我的实现思路非常简单但非常有效:part_info 表里加了 stock_warning 阈值字段,查询配件列表时,如果 stock_quantity <= stock_warning,就在返回的列表数据上加一个低库存标记,前端用红色高亮展示;同时首页可以加一个“预警列表”,默认按库存量升序取 Top N 条记录。核心就是一条 SQL:
sql复制select id, part_name, stock_quantity, stock_warning, brand, vehicle_series
from part_info
where stock_quantity <= stock_warning
order by stock_quantity asc;
统计报表模块则直接基于 MySQL 的聚合函数和日期函数实现。比如统计近 7 天的销售额趋势,可以用 DATE(create_time) 分组再 SUM(amount);统计热销配件 Top 10,则对 sale_order_item 表按配件 ID 分组求和。这些数据量不大,直接在 MySQL 里算好,后端接口照常返回 List 集合,前端用图表库绘制。相比引入复杂的 OLAP 数仓方案,这种轻量统计在小型进销存系统里完全够用,而且你把这个实现思路写进项目文档,面试时反而能体现出你能权衡技术选型,而不是只会跟风堆组件。
4. 前端页面与交互:JavaWeb 项目的前端方案选择
4.1 服务端渲染:Thymeleaf + Bootstrap 的搭配
这个项目的前端页面,我用的是 Thymeleaf 模板引擎 + Bootstrap 3/4 + jQuery 的组合。选择 Thymeleaf 而不是 JSP,是因为 Spring Boot 官方生态对 Thymeleaf 的支持更自然,不需要额外配置 JSP 的编译器插件;同时 Thymeleaf 的语法对 HTML 编写者非常友好,用 th:each 遍历列表、th:text 输出变量,初学者只要有一点 HTML 基础就能快速上手。
前端页面主要包含:登录页、系统主框架页(左侧导航 + 顶部菜单 + 右侧内容区)、配件列表页(含查询表单和表格)、配件新增/编辑页、销售开单页、订单列表页、库存预警页、统计报表页。每个页面的风格保持一致,使用 Bootstrap 栅格系统布局表单和表格,通过 cdn 方式引 Bootstrap 的 CSS 和 JS 资源。
用服务端渲染方案有一个很舒服的地方:后端 Controller 返回数据时,直接在 Model 里塞数据,然后返回模板名称,页面上的表格循环语句能直接拿到数据填充,不用单独写接口调用和 AJAX 渲染逻辑,对于页面交互不复杂的管理系统来说开发效率非常高。
4.2 列表查询页和表单页的交互细节
列表页和后端接口之间的配合是整个前端开发里工作量最大的部分。页面加载时,向后端 /part/list 发送请求,后端返回配件列表数据;用户点击“查询”按钮时,前端把表单里的查询参数序列化后作为请求参数,重新向后端发请求,后端返回过滤后的结果。为了保持查询条件在翻页后不丢失,我建议使用 GET 请求并让分页参数始终携带查询表单的所有字段值。
一个非常好用的表格轮轴技巧是给前端定义统一的 JSON 返回结构。我在系统里定义了一个通用返回类 Result,包含 code、msg、data 三个字段,code 为 200 表示成功,401 表示未登录,500 表示异常。Controller 里所有接口统一返回这个类型,前端在 AJAX 的 success 回调里先判断 code,再决定是渲染数据还是弹出错误提示。这样你在后续独立做新功能时,整个协作模式都是统一的,不必一个接口一个接口地单独处理。
表单校验我分为两层。后端在 Controller 和 Service 层做了参数校验,比如页面上传的配件价格为负数时必须拒绝,销售数量大于库存时必须提示。前端则用 jQuery 做即时校验,输入框失焦时该判断就判断、该提示就提示。后端校验是不可省略的,前端校验只是体验优化。如果你想验证一下这句话,可以试着绕过登录页直接访问后台接口路径,或者用浏览器开发者工具把表单校验删除再提交,你会发现很多看起来安全的系统瞬间就被打穿了。
4.3 销售开单页:动态添加商品行的实现思路
销售开单页是整个系统前端交互最复杂的页面。用户需要在一个单据上添加多种配件,每添加一种配件,页面下方的订单明细表格就会插入一行,包含配件名称、单价、数量、小计;用户修改数量时,小计和总价自动重新计算;提交时,页面要把所有明细行数据封装成后端需要的参数结构。
我的实现思路是:页面用一个 JavaScript 数组维护当前订单的全部明细,定义一个全局变量 orderItems = [],每添加一行就 push 一个对象进去 {partId, partName, salePrice, quantity};表格里的每一行都通过 data-index 属性关联到数组的索引位置,数量输入框的 onchange 事件触发时,先同步更新数组里对应对象的值,再调用函数重算小计和总价并更新 DOM。
提交时,通过 AJAX 把主表字段和明细数组一起发送给后端,直接使用 jQuery 的 AJAX 序列化方式转成 JSON 字符串,Controller 用自定义的 DTO 对象接收。这个页面两个最常见的坑是:第一,动态添加的行在重新渲染后事件丢失,解决方法是事件绑定必须使用事件委托,比如 $(document).on('change', '.quantity-input', function(){...});第二,传参时明细数据格式和后端 DTO 的 List 参数对不上,要保证 JSON 里是数组,而不是对象嵌套字符串。
5. 部署运行与常见问题排查:让项目真正跑起来
5.1 本地环境准备与数据库初始化
如果你拿到了这套项目的源码,第一次运行建议按照下面的步骤操作,顺序错了容易连锁踩坑。首先要确认本地环境具备 JDK 8、Maven 3.6+、MySQL 5.7/8.0、IDEA 这四个基础工具。接着用 SQL 用户脚本建库和导入表结构,执行后确认核心表都创建成功,可以执行一条 show tables 检查。
导入后建议在 part_info 表里插入几条测试数据,方便后面验证查询和下单流程。项目导入 IDEA 时,选择以 Maven 项目的方式打开,等待依赖下载完成,重点看右下角进度条是否报红。下载依赖过程中经常出现的两个问题是:连不上 Maven 中央仓库导致报错、版本冲突导致编译失败。解决方式是修改 Maven 的 settings.xml 文件,配置阿里云镜像仓库。
最后在 IDEA 里修改 application.yml 中的数据库账号密码,启动 Application 主类,看到控制台输出 Tomcat started on port(s): 8080 表示启动成功。然后浏览器访问 http://localhost:8080/login 进入登录页,用文档里提供的管理员账号登录,这套项目就算跑起来了。
5.2 高频故障速查表:我踩过的坑和解决方案
整理了一份我在开发过程中以及给同学调试项目时遇到的最高频问题清单,你对照着排查,能节省大量时间:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 启动时提示 Access denied for user root | 数据库账号或密码错误 | 检查 yml 中 username/password 是否和 MySQL 一致 |
| 启动报时区错误 The server time zone | MySQL 连接串缺少 serverTimezone | URL 加上 serverTimezone=Asia/Shanghai |
| 页面 404 或跳回登录页 | 拦截器未放行该路径 | 检查拦截器 excludePathPatterns 是否包含该路径 |
| 前端表格中文乱码 | 请求响应编码不一致 | 确认 yml 中 characterEncoding=utf8,且页面 meta 设置 utf-8 |
| insert 语句报 SQLException: Field xxx doesn't have a default value | 表中字段 NOT NULL 但未传值 | 检查前端表单提交的字段和后端实体是否一一对应 |
| 库存扣成负数 | 更新库存时未拼接库存条件 | 使用 update part_info set stock_quantity = stock_quantity - #{num} where id=#{id} and stock_quantity >= #{num} |
| 明明登录了但访问接口返回未登录 | Session 丢失 | 检查浏览器是否开启了隐私模式、服务端 session 超时时间设置是否过短 |
| 静态资源样式全部丢失 | 静态资源路径被拦截 | 在拦截器配置中放行 /static/** 或对应目录 |
| mybatis XML 文件报错找不到 statement | mapper 扫描路径配置错误 | 检查启动类 @MapperScan 注解路径和 yml 中 mapper-locations |
这里面我想单独展开讲一下数据库时区问题。MySQL 8.x 默认时区设置和国内本地环境不一致时,连接报错非常典型。有一次我帮人调试,程序启动时一直报 java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,这就是典型的时区配置缺失或者系统时区识别异常导致的。解决方式除了连接串加参数,也可以在 MySQL 客户端执行 set global time_zone = '+08:00',不过最稳妥的还是前者。
另一个高频问题是 MyBatis 的 XML 文件没有被编译到 target 目录。IDEA 默认情况下不会把 src/main/java 目录下的非 Java 文件打包,如果你把 mapper XML 放在了 Java 包目录下,就会在运行时报 Invalid bound statement (not found)。解决方案是把 XML 放到 resources/mapper/ 目录,或者在 pom.xml 的 build 节点里配置 resource 包含 XML 文件。我统一推荐前者,目录结构更清晰。
5.3 文档与视频配套学习:源码之外的正确打开方式
很多同学拿到源码后的第一反应是“直接跑起来”,然后点一点页面,感觉“项目学会了”,这种做法其实浪费了整套资料的价值。我更建议的学习路径是:第一步,先运行项目,把文档里写的“项目介绍”和“功能模块说明”对着页面逐一确认“什么东西在哪个页面、点了以后发生了什么”;第二步,打开数据库,对着表结构和页面显示内容,把“一条销售数据在哪些表里产生了记录”理清楚;第三步,从 Controller 入手,逐个阅读接口的实现代码——看它接收什么参数、调用了哪些 Service 方法、返回了什么数据;最后,再根据文档里的“开发环境搭建”部分,自己从一个空的 Spring Boot 项目开始,把核心模块重写一遍。
配套的视频资源也一样,不要全程只看不动手。我强烈建议看视频前先自己尝试搭建环境和运行项目,遇到问题时暂停视频自己排查;实在解决不了再去看视频里的调试过程。重写代码的过程中,当你发现自己能不看文档就把登录拦截器写出来、把销售开单的事务逻辑写对,那这个项目才真正变成了你自己的能力。
6. 实际开发中的一些感触
这个项目真正有意思的地方,不是某段炫酷的代码,而是那些看起来平平无奇的决策。比如最初在“要不要把库存数量和流水记到同一张表”上,我花了一个晚上查资料、动手验证,最后才想明白“主库存只存结果,流水表保存过程”这个道理。这类经验,教科书上写不满一页,但只有自己踩过坑、对比过不同方案,才真正内化成能力。
如果在阅读或者运行项目的过程中遇到问题,请优先按照上面整理的排查表逐项核对,绝大多数问题都出在环境配置或拦截器路径上。项目本身是死的,但你把它调试通、读明白,甚至动手改造出几个自己构思的功能页,它对你的价值才真正开始显现。
