每年到毕业设计季,都会有人拿同一个问题来找我:“到底选什么题目最稳?”我给的固定答案里,进销存管理系统始终排在前三名。原因很简单:它看着不花哨,但把后端增删改查、数据库建模、权限控制、前端交互、统计报表全串起来了,工作量卡在一个人能独立完成、又有足够“干货”可以写进论文的范围。更重要的是,用 SpringBoot3 + Vue3 这套组合做出来,答辩时讲技术栈也很有底气。
这篇我打算按“为什么选它 -> 数据库怎么设计 -> SpringBoot3 后端怎么写 -> Vue3 前端怎么做 -> 文档和答辩怎么准备 -> 实际交付会遇到哪些坑”的顺序,把一套完整进销存管理系统的思路拆开讲。标题里提到的教程、源码、数据库脚本、文档、答疑,本质上是一个可复用的项目包,适合零基础的同学边看边改,而不是直接拿现成代码交差。你就把这篇当成一位做过同类项目的人,在和你做一次完整的设计复盘。
1. 为什么进销存系统适合做毕业设计,以及技术选型的思考
1.1 进销存系统的业务主线看上去简单,但内部闭环完整
进销存,其实就是“进货、销售、存货”三个字的缩写。一个典型的小型商贸公司,日常业务大概是这样的:先从供应商采购商品,采购单审核后入库,仓库库存增加;然后客户下单购买商品,系统生成销售单,销售出库后库存减少;最后老板要看哪些商品卖得好、库存还有多少、哪个供应商欠款多,于是引出统计报表和往来账模块。
这条业务主线最大的好处是:它不是一个孤立的功能堆砌,而是一环扣一环的数据流转。你做“商品管理”的时候,可以只做增删改查;但一旦做到“采购入库”和“销售出库”,就必须考虑库存联动和事务一致性;做到“统计报表”时,又要写聚合查询和图表展示。这种递进关系特别适合写进毕业设计论文里的“需求分析”和“系统设计”章节,评审老师一看就知道你不是只做了个花架子。
另外一个很实际的理由是:这个题目的业务规则足够明确,每个人都能理解“库存不能为负数”“销售单需要扣减库存”这类约束,不需要你去虚构一个没人懂的领域模型。相比“智能推荐系统”“人脸识别考勤”之类容易把范围越做越大的题目,进销存的边界清晰,更容易控制进度。
1.2 为什么技术栈要选 SpringBoot3 + Vue3,而不是老版本组合
现在网上能找到大量 SpringBoot2 + Vue2 的旧项目,很多人图省事直接拿老项目改。我不建议这么做,原因有三点。第一,SpringBoot3 是基于 Spring Framework6 的新一代框架,它要求 JDK17 起步,整个生态已经把旧的 javax 命名空间替换成了 jakarta,如果你现在学 SpringBoot2,等毕业入职后发现团队已经升到 SpringBoot3,会出现明显的知识断层。第二,Vue3 配合 Vite 构建工具,热更新速度和开发体验比 Vue2 + Webpack 好太多,Element Plus 这个组件库也是照着 Vue3 的 Composition API 设计的,KPI 指标就是老师在答辩时听你说“用 Composition API + <script setup> 组织代码”,会觉得你踩在了技术趋势上。
第三,前后端分离本身就是简历上值得写的一笔。后端 SpringBoot3 只负责提供 RESTful API,前端 Vue3 独立工程通过 Axios 调用接口,部署时前端可以放 Nginx,后端打 Jar 包跑在服务器上。这种架构就算项目规模不大,也能让你完整走一遍“前端工程化 -> 后端接口设计 -> 跨域联调 -> 打包部署”的全过程,毕业设计和面试都能用上。
1.3 零基础起步的正确路径:先拆解,再复现,最后改造
标题里写着“教程+源码+数据库+文档+答疑”,很多同学会觉得“太好了,有源码等于直接过关”。这里必须先泼一盆冷水:直接抄源码,答辩必翻车,因为老师随便问一个“为什么要建这张表”“这个事务注解删掉会怎样”,你答不上来就等于承认不是自己做的。更合理的路径是三步走。
第一步,把源码当成“答案”而不是“作业”。对照数据库脚本,先搞懂每一张表存什么、表和表之间怎么关联;再对着后端代码看每个接口做了哪些步骤;最后打开前端页面,把页面上的按钮和接口调用的 URL 对应上。第二步,自己动手复现一个最小闭环:先建数据库,再搭后端工程,把“商品管理”和“销售出库”两个模块独立写出来。写不出来就回头看源码,这比抄一遍源码有效十倍。第三步,做改造。把商品字段加一两个、把报表换个统计维度、把菜单权限从写死改成从数据库读取,这些改动写到论文里就是“创新点”,也是你在答辩时能自信回答问题的底气。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 需求分析与数据库设计:先把表和字段设计明白
2.1 角色划分与核心业务场景
进销存系统最常见的角色划分是四种:系统管理员、采购员、销售员、仓库管理员。管理员负责创建用户和分配权限;采购员录入采购订单并提交入库申请;仓库管理员执行审核和入库操作,同时负责库存盘点和库存调整;销售员创建销售订单,确认收款后触发销售出库。当然,小项目里也可以让一个人兼任多个角色,但设计时角色边界一定要清楚,因为“谁做了什么操作”这种审计需求,在论文“系统测试”里会反复用到。
围绕这四类角色,核心业务场景可以整理成一个闭环清单:用户登录鉴权 -> 商品档案维护 -> 供应商/客户资料维护 -> 采购下单 -> 采购审核入库 -> 库存同步增加 -> 销售下单 -> 销售审核出库 -> 库存同步减少 -> 库存查询与盘点 -> 销售/采购报表统计。每个场景在数据库里都要有相应的表来承接,这就是“需求驱动表设计”。
2.2 核心数据表:一套可以直接落地的建表方案
下面这份表设计不是我凭空编的,而是把上述业务场景逐条映射出来的结果。实际项目里字段会更多,但下面是骨架,理解了骨架,加字段就是顺手的事。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 系统用户 | id, username, password, real_name, role, status |
| product | 商品档案 | id, category_id, name, spec, unit, sale_price, status |
| category | 商品分类 | id, parent_id, name |
| supplier | 供应商 | id, name, contact, phone, address |
| customer | 客户 | id, name, contact, phone, address |
| purchase_order | 采购单主表 | id, order_no, supplier_id, total_amount, status, create_time |
| purchase_order_item | 采购单明细 | id, order_id, product_id, quantity, price, amount |
| sales_order | 销售单主表 | id, order_no, customer_id, total_amount, status, create_time |
| sales_order_item | 销售单明细 | id, order_id, product_id, quantity, price, amount |
| stock | 库存表 | id, product_id, quantity |
| stock_record | 库存流水表 | id, product_id, change_type, change_quantity, before_quantity, after_quantity, biz_no |
主表和明细表为什么要分开?这是数据库设计的常见考点。一张采购单可能包含多个商品,如果把商品直接塞在主表里,就会违反范式,产生大量冗余字段。拆开后,主表存整单的公共信息(供应商、总金额、状态),明细表存每个商品的逐行数据,两张表通过 order_id 关联。你写“为什么拆表”这段,正好可以展示自己对数据库范式是有理解的。
sys_user 表建议预置三个初始化用户:管理员、采购员、销售员,方便演示时切换角色。同时要注意,密码字段不要存明文,至少用 BCrypt 加密。这个点虽然小,但答辩时很加分,因为能说明你考虑了数据安全。
2.3 核心设计难点:库存表、流水表、事务如何配合
很多刚学的人会问:库存数量不是可以直接从“采购入库数量减销售出库数量”算出来吗,为什么要单独建 stock 表?这里必须讲清楚:实时统计可以算,但性能扛不住。商品一旦多起来,每查一次库存就要聚合所有历史流水,页面响应会越来越慢。所以要有 stock 表专门保存“当前结存数量”,每次入库和出库时直接更新它。与此同时,stock_record 表负责记录每一次变化,用于追溯和对账。用一句话总结:stock 管“现在”,stock_record 管“曾经”。
采购入库的场景,后端要做两件事:往 purchase_order 和 purchase_order_item 插入数据,然后更新 stock 表并把变化写入 stock_record。这两件事必须在一个数据库事务里完成,否则可能出现“单录入了但库存没加”的问题。事务的核心目的,就是保证这组操作要么全部成功,要么全部回滚。在 SpringBoot 中,只需要在 Service 方法上加 @Transactional 就能实现。
销售出库比采购入库多一个约束:必须在扣减库存时校验库存足够。如果只靠应用层先查询再判断,在高并发场景下会出现超卖。更稳的做法是在 SQL 层做条件更新,这一点我在第 3 部分会给出具体代码。
3. 后端落地:SpringBoot3 项目结构、鉴权与核心接口实现
3.1 初始化项目时的依赖清单和目录规划
我建议用 Spring Initializr 生成基础工程,Java 版本选 17,构建方式选 Maven,依赖先勾选 Spring Web。生成之后,再手工往 pom.xml 里补三组依赖:MyBatis-Plus 或 Spring Data JPA(二选一,MyBatis-Plus 更贴近国内教学习惯)、MySQL 驱动、spring-security 和 jjwt。如果不想一开始就引入 Spring Security,也可以只用拦截器 + JWT,但对毕业设计来说,写清楚“Spring Security 过滤链”会让技术深度上一个档次。
一个推荐的包结构是:
text复制com.example.inventory
├── controller // 接收前端请求,返回 R 结构
├── service // 业务逻辑,事务都放在这一层
├── mapper // MyBatis-Plus 的 Mapper 接口
├── entity // 数据库实体类
├── dto // 前端传来的请求体,例如登录DTO、销售单DTO
├── config // SecurityConfig、跨域配置、MyBatis-Plus 配置
├── security // JWT 工具类、过滤器、UserDetailsService
├── common // 统一返回结果 R、全局异常类
└── InventoryApplication.java
controller 只做参数接收和结果返回,service 放业务逻辑,mapper 负责 SQL 操作。层级职责分明,论文里的“系统分层设计图”就按这个画。
3.2 统一返回结果与全局异常处理:前后端协作的基石
前后端分离项目,最忌讳后端错误信息直接抛给前端。我习惯封装一个 R 类,格式固定为 code、msg、data。code 为 200 表示成功,401 表示未登录,500 表示业务错误。这样前端 axios 拦截器只需要判断 code 就能统一提示错误。
java复制public class R<T> {
private Integer code;
private String msg;
private T data;
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.code = 200;
r.msg = "success";
r.data = data;
return r;
}
public static <T> R<T> error(String msg) {
R<T> r = new R<>();
r.code = 500;
r.msg = msg;
return r;
}
}
业务代码里如果遇到“库存不足”“用户不存在”这类可预期问题,不要直接返回 null,更不要抛空指针,而是抛一个自定义 BusinessException,再由 @RestControllerAdvice 捕获并转换成 R 结构返回。这种方式的好处是,前端拿到的永远是标准 JSON,不会因为异常堆栈把接口打挂。
3.3 基于 JWT 的登录鉴权:放行、校验、获取用户信息
登录接口的流程是:前端提交用户名密码 -> 后端查 sys_user 表 -> 用 BCrypt 比对密码 -> 验证通过后生成 JWT 返回给前端。JWT 里可以塞 userId、username、role,签名密钥放在 application.yml 中。
Spring Security 配置要做这几件事:关闭 CSRF、设置 Session 为无状态、放行 /api/auth/login、其他请求都要经过 JwtAuthenticationFilter。这个过滤器会取出请求头里的 Authorization,解析 token,把用户信息放进 SecurityContext 供后续接口使用。JWT 的过期时间建议设为 2 小时,前端在 axios 响应拦截器里遇到 401 就跳回登录页。
java复制http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
这段配置几乎是每个前后端分离项目的标准写法。答辩时如果被问到“为什么用 JWT”,你可以说:后端不存 Session,天然适合横向扩展,多台服务器也能共用同一种 token 校验逻辑。
3.4 销售出库的事务代码:先上锁,再扣减,再记流水
销售出库是最能体现业务深度的一段代码。我用一个 createSaleOrder 方法演示完整流程。先说明两个方法论:第一,主表头和明细表头要在同一次事务里插入,用订单号关联;第二,库存扣减不要用“先 select 检查再 update”,而要用带条件的 update 语句,把“库存是否足够”的判断交给数据库。
java复制@Transactional
public Long createSaleOrder(SaleOrderCreateDTO dto) {
// 1. 计算订单总金额,同时校验商品信息
BigDecimal totalAmount = BigDecimal.ZERO;
for (SaleOrderItemDTO item : dto.getItems()) {
Product product = productMapper.selectById(item.getProductId());
if (product == null) {
throw new BusinessException("商品不存在,id=" + item.getProductId());
}
totalAmount = totalAmount.add(product.getSalePrice().multiply(item.getQuantity()));
}
// 2. 创建销售单主表和明细表
SalesOrder order = new SalesOrder();
order.setOrderNo(generateOrderNo());
order.setCustomerId(dto.getCustomerId());
order.setTotalAmount(totalAmount);
order.setStatus("待出库");
salesOrderMapper.insert(order);
for (SaleOrderItemDTO item : dto.getItems()) {
SalesOrderItem orderItem = new SalesOrderItem();
orderItem.setOrderId(order.getId());
orderItem.setProductId(item.getProductId());
orderItem.setQuantity(item.getQuantity());
orderItem.setPrice(productMapper.selectById(item.getProductId()).getSalePrice());
salesOrderItemMapper.insert(orderItem);
}
// 3. 扣减库存:只有当当前库存 >= 扣减数量时才会更新成功
for (SaleOrderItemDTO item : dto.getItems()) {
int rows = stockMapper.reduceStock(item.getProductId(), item.getQuantity());
if (rows == 0) {
throw new BusinessException("库存不足,商品id=" + item.getProductId());
}
stockRecordMapper.insert(buildStockRecord(item, "SALE_OUT", order.getOrderNo()));
}
return order.getId();
}
reduceStock 对应的 SQL 是核心:
sql复制UPDATE stock
SET quantity = quantity - #{quantity}
WHERE product_id = #{productId} AND quantity >= #{quantity}
这里的妙处在于,数据库在一条 update 语句内完成了“判断”和“扣减”,不会出现两个请求同时读到库存 10,然后再互相覆盖结果的问题。@Transactional 保证了订单插入和库存扣减要么一起成功,要么一起回滚。你把这个逻辑写在论文“核心功能实现”里,比贴十张页面截图都有说服力。
4. 前端落地:Vue3 + Element Plus 从登录页到业务页面
4.1 Vite 初始化与前端目录划分
前端工程用 Vite 创建即可。打开命令行执行:
bash复制npm create vite@latest invent-ui -- --template vue
cd invent-ui
npm install
然后按功能划分目录:
text复制src
├── api // 接口请求函数,按模块拆分:auth.js, product.js, stock.js
├── router // vue-router 路由配置,配合登录守卫
├── stores // Pinia 状态管理,保存用户信息和菜单
├── views // 页面级组件:登录、商品管理、采购、销售、报表
├── components // 可复用组件:分页表格、表单弹窗、图片上传
├── layout // 整体布局:侧边栏 + 顶栏 + 主内容区
└── utils // axios 实例、格式化工具
很多初学者喜欢把接口请求直接写在页面组件里,短期能跑,但一旦页面多了,改后端接口地址就得全局搜索。把 api 独立成目录,以后每个模块的维护成本会低很多,这一点在你写论文“系统实现”章节时也能体现出工程化思维。
4.2 Axios 封装:请求拦截器、响应拦截器和 401 跳转
axios 必须封装,否则每个页面都要处理 loading、错误提示、token 注入。下面是我常用的最小封装:
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config
})
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === 200) {
return res
}
if (res.code === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(res.msg || '请求失败')
return Promise.reject(new Error(res.msg || '请求失败'))
},
error => {
ElMessage.error('网络异常')
return Promise.reject(error)
}
)
export default service
这里有个细节:前端路由守卫也要同步判断 token,否则用户手动清掉 localStorage 后还能看到页面框架。route 前置守卫里检查一下 localStorage.getItem('token'),没有就跳 /login。两者配合才能保证“未登录不可访问业务页面”。
4.3 商品管理页:表格展示、弹窗表单、分页查询三件套
商品管理页是所有列表页的基础模板。逻辑很简单:页面加载时调用 /api/product/page 获取第一页数据;点击“新增”弹出 el-dialog,提交时调用 /api/product/save;点击“编辑”先回显数据再提交修改;点击“删除”弹确认框。Element Plus 的 el-table、el-dialog、el-pagination 三件套足以搞定。
vue复制<el-table :data="tableData" border stripe>
<el-table-column prop="name" label="商品名称" min-width="140" />
<el-table-column prop="categoryName" label="分类" min-width="100" />
<el-table-column prop="salePrice" label="销售价" width="120" />
<el-table-column prop="stockQuantity" label="当前库存" width="120">
<template #default="{ row }">
<el-tag :type="row.stockQuantity === 0 ? 'danger' : 'success'">
{{ row.stockQuantity }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
分页参数 name、pageNum、pageSize 可以统一封装在查询对象里。注意这里的“当前库存”字段需要后端联表查询,因为 product 表里没有库存数量,只有 stock 表里有。前端接口返回给表格用的 DTO,应该包含“商品表字段 + 库存字段 + 分类名称”,而不是让前端自己去拼多次请求。这也是前后端设计的一个原则:接口返回的数据尽量是前端直接可展示的完整视图模型。
4.4 统计报表与 ECharts 图表的接入思路
报表模块是进销存系统的高光页面,通常放两类图表:一是按商品分类统计库存数量的柱状图,二是最近 30 天销售趋势的折线图。ECharts 的主流用法是安装 echarts 后,在组件里 import * as echarts from 'echarts',然后在 onMounted 里初始化图表实例。
vue复制<script setup>
import * as echarts from 'echarts'
import { onMounted, ref } from 'vue'
import { getStockCategoryStat } from '@/api/stock'
const chartRef = ref()
onMounted(async () => {
const res = await getStockCategoryStat()
const chart = echarts.init(chartRef.value)
chart.setOption({
xAxis: { type: 'category', data: res.data.map(i => i.categoryName) },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: res.data.map(i => i.quantity) }]
})
})
</script>
这属于后端“聚合查询”能力的体现。对应 SQL 大概是 SELECT c.name, SUM(s.quantity) FROM stock s JOIN product p JOIN category c GROUP BY c.name。你在论文里把这条 SQL 和这段前端图表代码放在一起,就能完整讲述一个“数据从数据库到页面可视化的链路”。
5. 论文文档与答辩准备:让项目说明书写得有条理
5.1 设计文档的标准章节结构与每章写作重点
标题里提到的“文档”,通常指毕业设计说明书或毕业论文。我见过太多同学把文档写成“代码功能说明书”,通篇贴代码截图,这是不对的。毕业设计文档更看重分析设计过程,建议按以下章节组织:
- 第一章 绪论:写选题背景、国内外现状、课题意义。不要夸大,两三页足够。
- 第二章 相关技术介绍:写 SpringBoot3、Vue3、MySQL、MyBatis-Plus 的核心特性和选型理由。
- 第三章 需求分析:写角色分析、功能需求列表、用例图和非功能需求。
- 第四章 系统设计:写系统总体架构图、功能模块图、数据库 ER 图和表结构、接口设计规范。
- 第五章 系统实现:按模块贴核心代码和页面截图,注意每段代码都要配文字解释“为什么这么做”。
- 第六章 系统测试:写测试环境、功能测试用例表、测试结果。最好强调库存扣减、并发下不超卖这类测试点。
- 第七章 总结与展望:总结项目完成情况,再写一两个后续优化方向。
其中第四章和第五章是全文重点,加起来至少占 60% 篇幅。数据库设计部分可以直接放表结构清单和 ER 图,系统实现部分突出库存事务、JWT 鉴权、报表统计三块亮点。
5.2 用例图、ER 图、流程图从哪里来
文档里最常被老师挑刺的就是图不规范。三张图是底线:系统用例图、数据库 ER 图、核心业务流程图。用例图画清楚四种角色和各自的功能,比如“销售员”能“创建销售单”“查询销售记录”。ER 图画出 product、stock、sales_order、sales_order_item 之间的关系,标注一一对应还是一对多。核心流程图建议画“销售出库业务流程图”,从销售员下单开始,到库存扣减、记录流水、更新库存结束。
这类图不需要多高级的软件,用 draw.io、ProcessOn 这类在线工具就能完成。画完后统一用黑白或同色系风格,不要五颜六色堆满,打印在纸质论文里也清晰。
5.3 演示脚本和答辩高频问题的回答思路
答辩演示往往限定五到十分钟,演示脚本要提前写好。我建议按这条链路演示:登录界面输入管理员账号,切到商品管理新增一个商品,切到采购管理录一张采购单并完成入库,再到商品列表确认库存增加了,然后建一张销售单完成出库,再确认库存减少,最后打开报表页展示销售趋势图。这条链路能把所有核心模块串起来,比零散点击每个菜单更有说服力。
答辩时被问得最多的几个问题,按下面的思路准备就不会慌。问“为什么用 JWT”——答 Session 不适合前后端分离和横向扩展。问“库存扣减怎么防止超卖”——答 SQL 条件更新和数据库事务。问“如果用户同时点击两次按钮,会不会重复入库”——答前端按钮 loading 加后端幂等校验。问“密码怎么存的”——答 BCrypt 不可逆加密。这些问题答案其实都在这篇博客前面讲过的内容里,只要你真动手写过一遍,回答起来非常自然。
6. 从立项到交付:常见踩坑、源码包组织和答疑经历
6.1 开发环境安装阶段最容易踩的五个坑
零基础同学最容易在环境搭建环节卡住,而且往往卡住的不是代码,是版本匹配问题。这里列举我答疑时反复出现的几种情况。
第一个坑是 JDK 版本不对。SpringBoot3 必须 JDK17 以上,很多同学电脑里装的是 JDK8,启动就报 UnsupportedClassVersionError。解决办法是统一安装 JDK17,并在 IDEA 的 Project Structure 里把 SDK 指过去。第二个坑是 Maven 依赖下载慢。如果仓库在国外,spring-boot-starter 相关包可能拉到一半就中断,建议在 settings.xml 里配置国内镜像源。第三个坑是 MySQL 连接报时区错误,连接 URL 必须加上 serverTimezone=Asia/Shanghai 和 useSSL=false。第四个坑是 Node.js 版本太老,Vite 对 Node 有最低版本要求,建议用 Node 18 以上。第五个坑是 MySQL8 的密码加密规则兼容问题,驱动要换 com.mysql.cj.jdbc.Driver,旧版 com.mysql.jdbc.Driver 已经废弃。
这些坑看起来都是小事,但每一个都能让人卡半天。我建议第一次搭建环境时,把安装步骤截图存档,作为论文“开发环境”章节的素材,一举两得。
6.2 交付源码包如何做到“打开就能跑”
标题里提到“源码+数据库+文档”,这三样东西如果只是塞一个压缩包,接收者大概率跑不起来。一个合格的项目交付包至少包含四个部分:源码目录、数据库脚本目录、文档目录、README 说明文件。
README 一定要写清楚五件事:JDK 和 Node 的版本要求;数据库创建语句和初始化脚本执行步骤;后端 application.yml 里需要修改的配置项,比如数据库密码;前端启动命令和后端启动命令;默认账号密码。源码目录里要删掉 target、node_modules 这类生成目录,否则压缩包巨大且无意义。数据库脚本建议拆成 schema.sql 和 data.sql,一个建表一个写初始化数据,这样别人既能一次跑通,也能单独看表结构。
6.3 答疑中出现频率最高的五类问题和排查思路
最后一个实用板块,整理我实际答疑时反复讲过的排查方法。很多问题其实不是代码 bug,而是环境或配置问题。
| 现场现象 | 可能原因 | 排查方向 |
|---|---|---|
| 后端启动报 Failed to configure a DataSource | 数据库没创建,或 application.yml 配置错误 | 检查数据库名、用户名、密码、URL |
| 后端启动成功,但前端登录时请求 404 | 前端代理没配置,或接口路径不对 | 检查 vite.config.js 的 proxy,对比后端 controller 的 @RequestMapping |
| 前端请求接口报跨域错 | 后端未配置 CORS,或 Nginx 未放行 | 在 Spring Security 配置里放行 OPTIONS 请求,并配置 CorsConfigurationSource |
| 登录后请求业务接口返回 401 | token 没过期,但过滤器没放行该接口 | 检查 SecurityConfig 的放行规则和 JWT 过滤器顺序 |
| 打包部署后页面打不开 | 前端静态文件路径或后端 context-path 设置不一致 | 统一 baseURL 和前端路由 hash/history 模式,必要时改用 hash 模式 |
最后这点也算是我带了这么多项目后越来越确定的体会:进销存这类系统,代码本身并不难,难的是把数据从一个表“流动”到另一个表、从一个页面“流动”到另一个页面时,所有环节保持一致。你如果能亲手把所有模块串联起来跑通一遍,顺便把第 6 部分这些坑提前踩掉,那么不管是写文档还是答辩,心里都会特别有底。做完这个项目,你获得的不只是一份能交差的毕业设计,更是一段完整的前后端协作、数据库设计、问题排查的真实经历——这些东西在面试场上,比任何证书都管用。
