Spring Boot进销存项目实战:从运行到二次开发与避坑
简介面向Java毕业设计场景的Spring Boot进销存管理系统完整源码包覆盖企业物资流跟踪管理中的采购、入库、出库、销售及退货等核心业务流程。项目基于Spring Boot框架包含前端页面、后端接口与数据库SQL文件适用于计算机相关专业毕业设计或中小型企业进销存系统快速搭建参考。资源包共672个文件其中含103个Java源码、103个编译后class文件、98个HTML页面、84个JS脚本及29个JSON配置等整体压缩包大小约4.07MB目录结构清晰便于按模块学习或二次开发。目前已有2536人学习下载尤其适合需要完整可运行项目源码作为课程设计或毕业设计底稿的同学。借助完整源码可掌握Spring Boot与MyBatis等框架整合方式理解采购订单、销售发货、仓库库存修改等业务功能的持久化实现与前后端交互逻辑。1. 毕业设计里的进销存不是又一个增删改查而是能跑通的业务闭环打开这个压缩包之前先想清楚你说的“进销存”到底意味着什么。很多毕设项目做完就是一张表一页CRUD但进销存不一样它要覆盖进货、销售、库存、供应商、客户、订单状态还要管“入库之后库存涨了销售之后库存降了”这种最容易被忽略的联动。Spring Boot 在这类项目里几乎是毕业设计的事实标准因为它起步简单、不用写一堆 XML 配置内嵌Tomcat让部署演示也快。这篇文章我会带你做一个完整的事先把这个 Spring Boot 进销存项目从压缩包跑起来再把核心业务代码拆开看然后告诉你改造成自己业务的顺序和最常见的坑。适合三类人正在选毕设题目的学生、拿到源码不知道怎么运行的初学者、想快速二次开发却没读过代码的应届生。注意我按“Spring Boot MyBatis-Plus MySQL Thymeleaf 单体项目”这个最常见的毕设结构来讲。你手上如果带 Vue 或者 JSP原理一样改到的位置我在文中会指出来。2. 把源码包跑起来导库、改配置、启动三步走很多人拿到压缩包第一步就卡住不敢双击这个 zip因为不知道里面是什么。别慌先解压到不含中文和空格的目录比如D:\jxc然后按顺序做三件小事看目录、导库、启动。这三步做完你就能看到登录页。2.1 解压后的目录先找出后端入口和数据库脚本一个标准的 Spring Boot 毕设压缩包解压后至少会有这些部分后端工程目录通常是一个 Maven 项目里面有pom.xml、src/main/java、src/main/resources数据库脚本通常叫jxc.sql或db/jxc_db.sql也可能是独立的.sql文件数据库可能是一个.sql文件也可能是 mysql 的 data 目录但毕设一般给sql说明文档README 或 毕设报告里面大多会写数据库名和用户名第一步不是急着打开 IDEA而是先看pom.xml里的框架版本。用记事本打开重点看这三项parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent这段是 Spring Boot 的父工程它决定后续所有依赖的版本。看到 2.7.x 就用 JDK 8 或 11看到 3.x 就必须用 JDK 17。很多同学上来用 JDK 8 运行 Spring Boot 3 项目启动直接报UnsupportedClassVersionError这不是你代码问题是 JDK 版本不匹配。逻辑说明spring-boot-starter-parent管理了spring-boot-starter-web、mybatis-plus等所有依赖的版本你不该自己乱加版本号。参数说明如果发现没有这个 parent说明项目不是标准 Spring Boot 结构那就要看pom.xml里有没有spring-boot-maven-plugin没有的话这代码可能没法直接mvn spring-boot:run启动需要补一下。接着打开src/main/resources/application.yml或application.properties先只读不急着改。数据库配置长这样spring: datasource: url: jdbc:mysql://127.0.0.1:3306/jxc_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root这里第一件事是把jxc_db这个数据库名记下来后面建库要用第二件事记一下serverTimezoneAsia/Shanghai如果你的 MySQL 报时区错误就是这行没写对。2.2 导入数据库先建库再导入SQL别反过来很多人的习惯是双击 sql 文件结果 MySQL 弹了一堆乱码或者告诉你“数据库不存在”。正确顺序是先建一个空库再导内容。打开命令行用 root 登录 MySQL 执行mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS jxc_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p jxc_db D:/jxc/jxc_db.sql第一行是建库字符集选 utf8mb4 是防止商品名称里面有个别生僻字或表情符号存不进去。第二行把 SQL 文件导入指定的库。这里要确认两件事你 mysql 命令能不能用以及 SQL 文件路径里没有中文。如果执行第二行时报Unknown database说明第一行没生效先检查你是否连上了 MySQL 服务端而不是连错了 socket。参数说明-uroot是用户名-p表示密码会交互式输入是输入重定向把 SQL 文件内容喂给 mysql 客户端。导入后用一行命令确认表建对了mysql -uroot -p -e USE jxc_db; SHOW TABLES;看到类似sys_user、product、stock_record、sales_order这些表名就说明数据库已经就位。如果没有多半是 SQL 文件里有建库语句而你没建库或者导错了库。逻辑说明很多毕设 SQL 文件自带CREATE DATABASE和USE这时候你直接导入也能建库但字符集可能乱我习惯先建库再导入可控性更高。2.3 用Maven启动项目先看到登录页再说数据库就位后回到项目根目录确认你的 IDE 已经配好 JDK 和 Maven。在项目根目录打开命令行先执行mvn clean package -DskipTests如果提示mvn不是内部命令说明环境变量没配 Maven或者你用的是 IDEA 自带 Maven那就直接在 IDEA 的Maven面板里点package。打包成功后target 目录下会生成一个 jar 包然后运行java -jar target/jxc-system-0.0.1-SNAPSHOT.jar或者更简单开发阶段直接用mvn spring-boot:run看到日志里出现了Started JxcApplication in 12.34 seconds项目就起来了。接着浏览器访问http://localhost:8080/如果能看到登录页说明整个链路通了一半。为什么说一半因为登录页会向后端发请求你还没验证接口。验证接口是否正常用浏览器开发者工具或 curlcurl http://localhost:8080/user/login -X POST -H Content-Type: application/json -d {username:admin,password:123456}如果返回 JSON 里带token或success那么恭喜数据库连接、Spring Boot 启动、接口映射、登录逻辑全都通了。这里要特别说明一个细节默认端口是8080如果你的电脑上 8080 被占用了启动会直接失败。改端口用server: port: 8088放在application.yml里或在启动命令后面加--server.port8088。参数说明-DskipTests是跳过测试必要的因为毕设项目很多测试类没适配数据库跑测试会报错阻断打包。3. 理清进销存核心逻辑库存流水、权限和订单状态跑起来只是第一步你能演示的深度取决于对代码的理解。进销存和普通 CRUD 最大的区别在于库存不是只读一个字段而是由“流水”驱动。这一段我会把代码里最关键的三个点拆开讲。3.1 从数据表关系看进销存的数据流进销存的核心表不会太多但关系要清楚。典型的结构有这几张表名作用关键字段sys_user登录用户id, username, password, roleproduct商品主数据id, name, price, stocksupplier供应商id, name, contactstock_record库存流水id, product_id, type, quantity, before_stock, after_stocksales_order销售订单id, order_no, user_id, total_amount, status为什么要有stock_record而不是直接改product.stock因为审计需求你要能回答“这批货什么时候进的”“是谁操作的”“之前库存多少”。所以你会看到 Service 层代码里只要涉及库存变动必然先写流水再更新Product的库存字段。实体类里product是这样映射的public class Product { private Long id; private String name; private BigDecimal price; private Integer stock; private Integer status; }字段和表一一对应没什么魔法。但注意一个坑表字段如果是create_time而实体是createTimeMyBatis-Plus 默认会开启驼峰命名映射没问题如果你手动改了数据库字段名比如把库存改成stock_count那application.yml里要确认mybatis-plus: configuration: map-underscore-to-camel-case: true这个开关控制数据库下划线字段转实体驼峰属性。很多同学改了表字段没开这个开关查询结果一堆 null排查半天发现是映射问题。3.2 核心接口入库、出库、库存查询的参数与业务逻辑进销存项目最常被老师问的就是“入库和出库怎么保证库存正确”。下面这段代码是典型的入库逻辑我简化后但保留了核心Service public class StockService { Autowired private ProductMapper productMapper; Autowired private StockRecordMapper stockRecordMapper; Transactional(rollbackFor Exception.class) public boolean inbound(Long productId, Integer quantity) { // 1. 查询当前库存 Product product productMapper.selectById(productId); if (product null) { throw new RuntimeException(商品不存在); } // 2. 更新库存 int oldStock product.getStock(); int newStock oldStock quantity; product.setStock(newStock); productMapper.updateById(product); // 3. 记录一条流水 StockRecord record new StockRecord(); record.setProductId(productId); record.setType(IN); record.setQuantity(quantity); record.setBeforeStock(oldStock); record.setAfterStock(newStock); stockRecordMapper.insert(record); return true; } }代码逻辑是三层查商品、改库存、写流水。这里有几个参数需要注意。Transactional(rollbackFor Exception.class)是事务注解意思是只要方法里抛了异常前两步已经执行的 update 和 insert 都会回滚不会出现库存变了但流水没记的情况。rollbackFor必须要写因为 Spring 默认只回滚RuntimeException如果你抛的是自定义的受检异常不加这个注解就会导致库存更新成功、异常却回滚不掉。入库的核心是“加”那出库就是“减”但减之前要校验库存是否充足if (oldStock quantity) { throw new RuntimeException(库存不足); }这个判断必须放在事务里否则两个请求同时进来都读到oldStock10都扣 8最后库存变成 10-8-8-6这就是负库存。正确做法是加数据库锁常见写法SELECT * FROM product WHERE id #{productId} FOR UPDATE在 Mapper 里写一个加锁查询事务内部查询时锁定这一行其他事务必须等它提交后才能继续查。这个写法很实用也是面试的高频考点。参数说明FOR UPDATE是行级锁必须配合事务才生效且要放在查询语句末尾。3.3 登录与角色权限在Spring Boot里怎么控制进销存系统一般有管理员、采购员、销售员三种角色。毕设里最常见的做法不是引入 Spring Security而是用一个拦截器加 HandlerInterceptorpublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } // 解析 JWT校验过期和用户信息 String userId JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, userId); return true; } }然后在WebMvcConfigurer里注册拦截器Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/user/login, /static/**); } }这里要理解的是addPathPatterns(/**)和excludePathPatterns的配合。如果你把登录接口也拦截了那前端登录请求永远到不了 Controller如果你没加/static/**那 Thymeleaf 模板引用的 css/js 也会 401页面看起来是光板。参数说明Bearer是 JWT 的固定前缀很多项目用Token或直接放 header 的token字段前后端约定一致即可但改了前缀就要同步改拦截器这是最容易翻车的地方。权限控制再往细一点就进到方法级普通用户不能访问管理端的供应商接口可以在 Controller 方法上做注解拦截但多数毕设只在菜单上控制后端没有强校验。你如果想让老师觉得你有深度可以在拦截器里增加一个角色判断解析 JWT 时带上 role 字段然后判断当前请求路径是否以/admin/开头且 role 不是 admin就返回 403。4. 二次开发怎么把这个项目改成自己的业务拿到源码不一定是终点毕设答辩时老师最常问的是“这个地方如果需求变了你怎么改”。这一章我带你把改动流程跑一遍理解一次后面换任何表你都会改。4.1 新增一张采购表实体、Mapper、Service、Controller改动顺序假设你要加一张“采购单”表先别急着写代码先在数据库里把表建好CREATE TABLE purchase_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, supplier_id BIGINT, total_amount DECIMAL(10,2), status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );然后按“实体 - Mapper - Service - Controller”的顺序改。实体类TableName(purchase_order) public class PurchaseOrder { TableId(type IdType.AUTO) private Long id; private String orderNo; private Long supplierId; private BigDecimal totalAmount; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }TableName指定表名如果你的类名和表名不一样不加这个注解就会报“无效的表名”。TableId(type IdType.AUTO)意思是主键自增如果数据库用的是雪花ID或者手动插入这里要对应改。Mapper 类极简Mapper public interface PurchaseOrderMapper extends BaseMapperPurchaseOrder { }注意BaseMapper是 MyBatis-Plus 提供的它让你不写 SQL 就能完成单表 CRUD。如果没有用 MyBatis-Plus而是原生 MyBatis那这里要写 XML 和一堆方法代码会差很多。逻辑说明扩展了BaseMapper后自动拥有selectById、insert、updateById等方法但多表查询还是要自己写。Service 层一般先有接口再有实现但毕设可以直接写类Service public class PurchaseOrderService { Autowired private PurchaseOrderMapper purchaseOrderMapper; public ListPurchaseOrder list() { return purchaseOrderMapper.selectList(null); } }Controller 暴露接口RestController RequestMapping(/purchase) public class PurchaseOrderController { Autowired private PurchaseOrderService purchaseOrderService; GetMapping(/list) public Result list() { return Result.success(purchaseOrderService.list()); } }这套顺序是固定的先确认表结构再写实体映射字段然后 Mapper 管数据库操作Service 写具体业务比如生成单号、计算金额Controller 只做参数接收和返回。很多应届生习惯把业务全写在 Controller 里老师问事务在哪就答不上来。所以哪怕麻烦也要用 Service 夹在中间。4.2 分页查询和商品模糊搜索MyBatis-Plus 的分页参数进销存商品一多全量列表肯定卡。MyBatis-Plus 的分页插件配置先放在一个配置类里Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个 BeanPage对象不会自动生成 count 查询分页不生效。然后 Service 里public PageProduct searchProducts(int page, int size, String keyword) { PageProduct pageObj new Page(page, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword) .orderByDesc(Product::getId); return productMapper.selectPage(pageObj, wrapper); }这里两个参数很关键page从 1 开始不是 0size是每页条数。Page对象在 SQL 执行后会拿到total、pages、records三个属性前端要的是这些所以返回Page而不是List。LambdaQueryWrapper里的like(condition, column, value)第一个参数是条件这里用StringUtils.hasText(keyword)表示 keyword 不为空时才拼接LIKE这样搜索词为空时查全列表不会报 SQL 空指针。分页的坑是如果你用了selectPage但返回的records是空先检查MybatisPlusInterceptor是否注册了分页插件。很多人只引入了依赖没加这个配置然后发现分页方法直接查出了全表。4.3 给管理端加一个导出Excel接口答辩加分项进销存数据导出做不做是区分“会 Spring Boot”和“会用 Spring Boot 做事”的区别。用 Apache POI 写一个库存导出接口后端代码控制在几十行。先加依赖dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependencyController 里加方法GetMapping(/export) public void export(HttpServletResponse response) throws IOException { ListProduct products productService.list(); Workbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(商品库存); Row header sheet.createRow(0); header.createCell(0).setCellValue(商品名); header.createCell(1).setCellValue(单价); header.createCell(2).setCellValue(库存); for (int i 0; i products.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(products.get(i).getName()); row.createCell(1).setCellValue(products.get(i).getPrice().doubleValue()); row.createCell(2).setCellValue(products.get(i).getStock()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameinventory.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }注意几个参数XSSFWorkbook对应 xlsx 格式HSSFWorkbook对应 xls后者单表单上限 65536 行别选错。response.setContentType写错会导致文件下载下来但 Excel 打不开。最后workbook.write用输出流直接返回不需要返回 ModelAndView。答辩时老师喜欢问这个你回答“POI 生成 xlsx 然后通过 HttpServletResponse 的字节流输出”就够。提示导出接口如果要加权限拦截记得在拦截器排除路径里加/export或者在前端给用户一个下载按钮。别让导出的接口暴露在无鉴权状态这个问题很多毕设里有。5. 避坑跑Spring Boot进销存项目常见问题排查代码跑不起来的原因其实就那么几个我把最常见的五条按“现象 - 原因 - 解决”写出来。你可以直接对照。5.1 数据库连接失败时区、驱动和版本错位现象启动时报java.sql.SQLException: The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或Communications link failure。原因MySQL 8.0 以上对时区有严格校验连接串里的serverTimezone缺失或用了简单GMT前缀会导致客户端和服务端对不上。还有驱动com.mysql.jdbc.Driver是老驱动只兼容 MySQL 5.x要是你的 pom 里写的是 MySQL 8必须用com.mysql.cj.jdbc.Driver。解决连接串改成jdbc:mysql://127.0.0.1:3306/jxc_db?serverTimezoneAsia/ShanghaiuseSSLfalse驱动类改成com.mysql.cj.jdbc.Driver。pom 里 MySQL 驱动的版本可以放宽但不要自作主张写太高和数据库服务端匹配即可。如果你用的是 MariaDB 数据库那要和 MySQL 区别对待别强上 MySQL 驱动。5.2 启动时报数据库连接池初始化失败但不一定是配置写错了现象启动日志里出现HikariPool-1 - Exception during pool initialization你检查了用户名密码都对但就是连不上。原因第一可能是 MySQL 服务没启动Windows 下按了电源键进入睡眠后服务自动停止第二可能是localhost解析到 IPv6 导致连错地址第三是数据库权限问题root 用户可能被限制只能从本机连接但你连的是远程地址。解决先确认 MySQL 服务状态Windows 下用services.msc看 MySQL 是否在运行。连接串里的127.0.0.1比localhost更可靠因为强制走 IPv4。如果密码里带#之类的字符连接串里要 URL 编码比如密码是admin123写成password: admin123没问题但在 YAML 里有时会被截断建议密码固定在简单的字母数字组合毕设项目不用为了演示设置复杂密码。5.3 页面请求404或403端口、跨域和静态资源路径现象项目启动了登录页能看到但点击登录提示 404或者后端接口能通但前端页面加载不出样式。原因404 多半是前端请求的路径和后端 RequestMapping 不一致。比如前端调/user/login后端映射是/user/login/多了个斜杠就会变 404。403 则是拦截器把登录请求拦了或者跨域请求被后端拒绝。解决打开浏览器 F12看 Network 里面失败的请求 URL 和后端实际路径差别。拦截器排除路径里确认是否有/user/login。如果是前后端分离前端跑在 8081后端在 8080需要在 Spring Boot 加跨域配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE); } }这个配置写在WebConfig里也行但注意如果同时有拦截器跨域请求会先过拦截器所以拦截器里要放行OPTIONS请求否则预检直接 401。5.4 库存扣成负数事务和锁没加对现象两个人同时操作相同商品最后库存变成负数或者一条流水记录缺失。原因业务逻辑是先查再改两个线程读到同一个库存值都以为自己扣了之后还有货最终库存被扣穿。没有事务时会有一部分 SQL 成功一部分失败。解决给 Outbound 方法加Transactional(rollbackFor Exception.class)然后查询库存时用SELECT ... FOR UPDATE锁行。还要在代码里检查更新成功数int updated productMapper.deductStock(productId, quantity); if (updated 0) { throw new RuntimeException(库存扣减失败); }这里是乐观锁思路deductStock的 SQL 写成UPDATE product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}影响行数为 0 就说明库存不足。这种写法比“先查再改”更省事也更稳。5.5 修改代码不生效热部署、target目录和缓存现象你改了 Java 文件重启后发现接口还是旧逻辑或者改了数据库字段查询一直返回 null。原因IDEA 默认不会自动编译mvn spring-boot:run启动后改了代码但不重启不会有变化还有可能是浏览器或 MyBatis 的缓存。数据库返回 null 多半是实体字段没对上。解决开发阶段建议加spring-boot-devtools它会监听 classpath 变化自动重启但注意加了这个依赖后有些项目重启时间反而长个人建议不需要就别用。改代码后用mvn clean清掉 target 再重新打包比猜“为什么没生效”更高效。数据库字段返回 null 时先看 SQL 日志确认查询的列名和实体属性是否匹配再看map-underscore-to-camel-case是不是被改了。前端页面改了没生效CtrlF5 强制刷新或者换无痕窗口验证避免把缓存问题当成后端问题。6. 让它能上线演示连接池参数、接口验证和打包部署毕业设计答辩现场只用一次但你应该让它不只是“能跑”而是“看起来像工程”。最后给你三个具体技巧。第一生产环境连接池不要用默认值。HikariCP 是 Spring Boot 默认的连接池你把它调小一点演示时不会把数据库拖垮spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000maximum-pool-size控制最大连接数毕设项目 5 到 10 够了现在调成 50 没有意义。connection-timeout是请求连接的最大等待毫秒数如果数据库负载高别的请求会阻塞最多 30 秒才报错至少不会一直卡死。第二答辩前用 curl 验证核心接口。不要只靠浏览器页面页面可能掩盖接口问题。写一个简单的验证清单接口方法参数预期/user/loginPOSTusername/password返回 token/product/listGETpage/size返回分页 JSON/stock/inboundPOSTproductId/quantity返回成功库存增加/stock/outboundPOSTproductId/quantity库存减少且不能扣成负数我习惯把这几条命令写到一个test.sh里答辩前跑一遍哪个接口挂了立刻能看到不用临时看页面。第三打包部署。通常用mvn clean package -DskipTests生成 jar然后让老师在一台有 JDK 和 MySQL 的机器上运行。很多同学到这一步才发现application.yml里的 ip 写死了比如写成了localhost换到另一台机器就连不上。运输到演示环境前把数据库配置改成环境变量可覆盖spring: datasource: url: ${MYSQL_URL:jdbc:mysql://127.0.0.1:3306/jxc_db?serverTimezoneAsia/Shanghai} username: ${MYSQL_USERNAME:root} password: ${MYSQL_PASSWORD:root}这样部署时传环境变量就行不用改代码重新编译。启动命令mvn clean package -DskipTests java -jar target/jxc-system-0.0.1-SNAPSHOT.jar --MYSQL_URLjdbc:mysql://192.168.1.10:3306/jxc_db?serverTimezoneAsia/Shanghai不过要注意Spring Boot 默认不会直接读取你自定义的${MYSQL_URL}作为命令行的同名参数它会把它当作配置键。更稳妥的方式是在你常用的启动脚本里用export先设置环境变量再启动 jar。这招回答“你这个项目部署到别的电脑还能跑吗”时非常加分。我自己的习惯是拿到任何一份源码都先做一次“从头跑通”记录写下启动页、数据库版本、JDK 版本、改过的配置再开始改业务。这个习惯帮你省去很多答辩前的临时修 bug 时间也让你对项目的理解更扎实。下次再有人给你一个 Spring Boot 进销存源码包你走到这一步心里应该有底了。希望帮到你。本文还有配套的精品资源点击获取