☰
Spring Boot+Vue在线购物平台毕设资源包:从环境配置到全栈跑通
2026/10/9 18:56:51 网站建设 项目流程

简介:一份基于Springboot与Vue的在线购物平台毕业设计项目资料,面向计算机相关专业正在准备毕业设计的学生,也适合需要项目实战练习的Java学习者,可直接用于课程设计或期末大作业。项目采用Springboot+Mybatis后端与Vue前端,支持JDK1.8、MySQL5.7及Maven3.3环境,运行环境说明清晰。压缩包共607个文件,大小约20.79MB,涵盖124个Java源文件、94个Vue组件、63个JavaScript脚本、SQL数据库脚本、构建与启动批处理脚本,以及开发说明文档和部署讲解视频,目录结构清晰方便按需查阅。目前已有58人学习浏览。从文件构成看,既覆盖后端逻辑与前端页面,也包含数据库脚本和界面图标素材,整体完整性较高。资源内含完整项目源码、数据库脚本、开发说明文档、部署视频和代码讲解视频,项目经过调试可正常运行,可帮助读者快速掌握前后端分离开发流程、Mybatis数据库操作与在线购物业务逻辑,也可基于此改造为其他管理类毕设项目。

1. 在线购物平台这个资源包,值不值得拿来做 Spring Boot + Vue 的毕设/课设

Spring Boot + Vue 的在线购物平台,是很多 Java 毕业设计、课程设计和期末大作业里出现频率最高的选题之一。这份资源解压后就是一个完整的前后端分离商城:后端负责商品、购物车、订单、用户管理等接口,前端用 Vue 搭页面,两者通过 JSON 交互。很多同学拿到第一反应是直接启动,结果不是数据库连不上就是前端 404。我按自己的拆解习惯,先看目录结构和依赖,再初始化数据,最后把前后端一起跑通,整个过程大约半小时。适合正在做毕设、课设但缺一个可改造底子的从业者,也适合想快速理解 Spring Boot 全栈流程的初学者。这篇文章把每一步落的参数、命令和最容易翻车的位置都写清楚。

2. 先看懂项目骨架:Spring Boot + Vue 的全栈商城怎么分层

2.1 后端结构:Controller-Service-Mapper 三层的职责边界

第一次解压这种压缩包,别急着用 IDE 打开就跑。先解压到纯英文路径,比如D:\workspace\shop,然后打开pom.xml看 dependencies。这套在线购物平台后端用的是 Spring Boot 2.x,配套依赖一般是 MyBatis-Plus 做持久层、JWT 做登录令牌、MySQL 做存储。看清依赖以后,读代码的顺序应该是:启动类 → application.yml → Controller → Service → Mapper。启动类决定了扫描哪些包,配置文件决定了端口和数据库,Controller 则暴露了所有对外接口。

三层职责可以这样理解:Controller 只做参数接收和数据校验,不写业务逻辑;Service 负责事务、订单流程、库存等业务规则;Mapper 只管 SQL 和实体映射。拆开以后每个类的代码量都不大。比如商品管理里最典型的翻页接口,Controller 只有几行:

@RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; // 分页查询商品接口,支持关键字搜索 @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.success(productService.findPage(pageNum, pageSize, keyword)); } }

这里@RestController表示所有方法直接返回 JSON,不需要再写@ResponseBody。@RequestMapping("/api/product")把这一类商品接口统一挂在/api/product下,前端 axios 请求路径也是这个前缀。参数pageNum和pageSize默认值分别是 1 和 10,keyword是可选的模糊查询条件。返回值Result.success把数据和状态码包成一个统一结构,前端拦截器只需要判断code和message就能统一处理错误,不用每个页面重复写。

对应到 Service 层,常见做法是注入一个继承 MyBatis-Plus 的BaseMapper,用 LambdaQueryWrapper 做条件拼装:

public Page<Product> findPage(Integer pageNum, Integer pageSize, String keyword) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { // like 条件会拼到 WHERE 后面 wrapper.like(Product::getProductName, keyword) .or().like(Product::getProductDesc, keyword); } return productMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); }

这段代码在 MyBatis-Plus 里能返回 Page 对象,但前提是配置了PaginationInnerInterceptor。很多同学照抄别人的代码没配分页插件,查出来仍然全表数据,这是后话。这里的关键是:LambdaQueryWrapper 的 like 条件默认是拼接在 WHERE 后面的,多个 or 条件要注意括号优先级,否则关键字会串到别的表查询里。A同学 调一个“搜索商品却连带把分类也过滤了”的问题,最后发现就是 or 把条件扩大了。

读代码时,我会先在整体目录里数一数 Controller 数量,数完大概就知道这个商城有多少模块:通常有用户模块、商品模块、购物车模块、订单模块、后台管理模块。如果某个模块只有 Controller 没有 Service,说明它只做了简单查询,后续你改订单流程就得自己补 Service,这是看清资源包边界最好的方式。前端同理,打开src目录数views下面的页面文件,能反过来验证后端接口是否齐全。

2.2 前端结构:Vue 组件、Router 与 Axios 请求封装

Vue 端的入口不用看App.vue的细节,先看src/router/index.js。这个在线购物平台是典型的前后端分离,页面跳转全部由 router 接管。用户未登录时,前端会做一个路由守卫,判断 Vuex 或 sessionStorage 里有没有 token,没有就强制跳转登录页。配置大致这样:

const routes = [ { path: '/', component: HomeView, meta: { requiresAuth: true } }, { path: '/login', component: () => import('../views/Login.vue') }, { path: '/product/:id', component: () => import('../views/ProductDetail.vue') }, { path: '/cart', component: () => import('../views/Cart.vue'), meta: { requiresAuth: true } }, { path: '/order', component: () => import('../views/OrderList.vue'), meta: { requiresAuth: true } } ] router.beforeEach((to, from, next) => { const token = sessionStorage.getItem('token') // 需要登录的页面没有 token 就踢回登录页 if (to.meta.requiresAuth && !token) { next({ path: '/login' }) } else { next() } })

懒加载的写法用了箭头函数配合 import,首屏不会一次性把所有页面加载完。requiresAuth这个 meta 字段是自定义的,路由守卫靠它来判断哪些页面需要登录。登录成功以后新建的 axios 实例会把 token 塞进请求头:

const request = axios.create({ baseURL: '/api', timeout: 8000 }) request.interceptors.request.use(config => { const token = sessionStorage.getItem('token') if (token) { // 统一在请求头加 Bearer Token config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use(response => { const res = response.data if (res.code === 401) { sessionStorage.removeItem('token') window.location.href = '/login' } return res })

注意这里的baseURL是/api,而不是完整的localhost:8080。开发环境里vue.config.js会把/api代理到后端地址,这样既绕开了跨域,也不用在代码里写死后端 IP。业务代码里所有请求都统一走这个 request 对象,比如购物车加购:

request({ url: '/cart/add', method: 'post', data: { productId: this.productId, count: this.count } })

URL 不写/api/cart/add是因为 baseURL 已经带了/api,写重复路径会让代理匹配失败。这个细节很多初学者会踩,如果你发现请求发到/api/api开头的地址,基本都是这个原因。

到这里应该明白,前后端之间唯一的沟通规则就是 REST 路径和 JSON 结构。后端 Controller 的@RequestMapping决定了路径,前端 request 的 baseURL 决定了前缀;后端 Result 封装了 code/message/data,前端拦截器统一处理。搞懂这条链路后,剩下的页面组件都是在不同页面里重复同一套交互。所以拆项目第一步不是看某个页面多好看,而是先画一张“接口清单”,把哪些页面对应哪些接口、哪些接口需要登录标出来。这张表会成为后续改需求、补功能、答辩讲架构的共同语言。

3. 把项目跑起来:环境准备、数据库导入与本地联调

3.1 环境清单与版本匹配

解压资源包后最忌讳的是“缺什么装什么”,装到一半发现 JDK 版本太高导致 Spring Boot 2 起不来。我一般先列一张环境版本对照表,再决定用什么工具跑。这里给一份在这类在线购物平台上验证过的组合:

组件推荐版本说明
JDK1.8 或 11Spring Boot 2.x 在 JDK 8 上最稳,17 以上容易遇到反射和动态代理问题
Maven3.6.33.8+ 要检查 mirror 和仓库配置,部分源会拉不到旧依赖
Node.js14.x 或 16.xVue 2 + vue-cli 项目在 18 上经常出现 OpenSSL 错误
MySQL5.7 或 8.0注意驱动和时区配置差异
IDE任意后端建议用 IntelliJ IDEA,前端 VSCode 够用

版本不匹配的表现很直接:后端 Maven 编译报cannot find symbol,前端 npm install 报digital envelope routines::unsupported。这些错误经常被误认为是代码问题,实际就是换了环境。如果已经在 JDK 17 和 Node 18 上,最省事的做法是装一个 JDK 8 和 Node 16 双版本,用工具切换,不要赌运气。

检查当前环境的命令用一串就够了:

# 检查本机开发环境版本 java -version mvn -version node -v npm -v mysql --version

在这个在线购物平台资源包里,后端用的父级 pom 依赖集中在 Spring Boot 2.3 附近,数据库驱动默认匹配 MySQL 8。如果你本地是 MySQL 5.7 也有救,只要把依赖里的 mysql-connector-java 换成 5.1.49 版本,同时把驱动类从com.mysql.cj.jdbc.Driver换成com.mysql.jdbc.Driver,后面会单独说这个坑。

3.2 数据库初始化:SQL 脚本的导入顺序和角色数据

资源包里通常有一个shop.sql或sql目录。不要用 IDE 的图形化界面直接全选执行,那样一旦某段脚本有中文注释乱码,后面建表全废。我习惯用命令行先建库,再指定表编码导入。打开命令行进入 sql 文件目录,执行:

mysql -u root -p -e "CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p shop < shop.sql

utf8mb4是必须的,购物平台里有商品详情、用户昵称、订单备注,都可能出现 emoji 或特殊中文。字段如果建成了 utf8,这些字符会报1366 Incorrect string value错误。导入成功后先不要着急跑后端,用单条 SQL 验证数据是不是真的进去:

SELECT COUNT(*) FROM sys_user; SELECT COUNT(*) FROM mall_product; SELECT COUNT(*) FROM mall_order;

表名前缀不一定完全相同,有的资源包用t_user、tb_product,但这步验证能确认 SQL 脚本执行到哪一步崩了。如果导入时提示未知表,往往是因为脚本开头没有CREATE DATABASE,或者把use shop写在了注释里。这时候自己补一句就行。

接下来修改后端配置文件。常见路径是src/main/resources/application.yml,核心内容如下:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-please-change expire-hours: 72

url 里的参数几乎每个都要解释:useUnicode和characterEncoding保证中文不乱码;serverTimezone=Asia/Shanghai解决 MySQL 8 的时区差 8 小时问题;useSSL=false是为了开发环境免去 SSL 握手。driver-class-name如果没有写,Spring Boot 2 会根据 URL 自动推断,但写上更明确。jwt.secret是签名的密钥,资源包里默认值往往是一样的,上线前必须换,否则任何人都能伪造 token。mapper-locations这里用了classpath:mapper/**/*.xml,如果你的项目把 SQL 写在注解里,不配置这个也没关系,但常见的商城资源都会用 XML 存复杂 SQL,所以还是留着。

3.3 前后端启动与本地联调:端口、代理和第一个请求

后端启动最简单的方式是命令行先打包再运行:

mvn clean package -DskipTests java -jar target/shop-0.0.1-SNAPSHOT.jar

-DskipTests要放在package后面,写成-DskipTests=true也可以。如果依赖下载很慢,检查 Maven 是否用了国内镜像;如果打包时报找不到主类,看一下 pom.xml 里 spring-boot-maven-plugin 的 mainClass 是否指向了@SpringBootApplication所在的类。

前端启动前先改代理配置。这个项目里vue.config.js通常长这样:

module.exports = { devServer: { port: 3000, proxy: { // 所有 /api 开头的请求都转发到后端 '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

端口选择 3000 是为了避开 vue-cli 默认的 8080,防止后端也占 8080 时资源冲突。proxy 的意思是把所有以/api开头的请求转发到 localhost:8080。前端代码里请求/api/product/page,浏览器看的地址是localhost:3000/api/...,proxy 改写成localhost:8080/api/...。如果 proxy 没生效,最常见的错误是 axios baseURL 写成/api且请求路径也带/api,双重前缀导致代理匹配失败;或者是vue.config.js在工程根目录但没被加载,可以加一行输出测试判断。

启动:

npm install npm run serve

npm install 在这类老项目里可能报 ERESOLVE 依赖冲突,常见原因是 Node 版本太高。解决办法是加--legacy-peer-deps参数,而不是删 package-lock.json。如果已经跑起来,浏览器访问http://localhost:3000,用资源包里预置的管理员账号登录后台,再开一个普通用户账号走一遍前台。到这里项目才算真正跑通,后面看功能代码才有安全感。

登录验证时我建议直接看浏览器 F12 的 Network,先确认请求是否到达后端,再看 Response 返回的 JSON 是否正常。如果请求是红色 404,基本是路径问题;如果 500,是后端代码或数据库字段对不上;如果 200 但 code 是 400,多半是参数校验失败。先把这三个状态区分开,再去翻日志,能省一半排查时间。

4. 关键功能落地:商品管理、购物车与订单流程

4.1 商品分页与图片上传:一个接口搞定列表和搜索

在线购物平台的商品模块是整个项目的门面。前端首页的商品列表,后台的商品管理,本质上都复用同一个分页接口。商品表字段大概包括商品名称、分类、价格、库存、图片 URL、上下架状态。Controller 层已经看到了,Service 层核心是拼查询条件,Mapper 层处理分页和关联分类名称。

如果想改造成“按价格区间过滤”,可以在 Controller 增加两个可选参数:

@GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) BigDecimal minPrice, @RequestParam(required = false) BigDecimal maxPrice) { return Result.success(productService.findPage(pageNum, pageSize, keyword, minPrice, maxPrice)); }

Service 对应改动:

// 价格区间查询条件 if (minPrice != null) { wrapper.ge(Product::getPrice, minPrice); } if (maxPrice != null) { wrapper.le(Product::getPrice, maxPrice); }

ge是大于等于,le是小于等于。很多同学在参数为 null 时忘了做空判断,SQL 会变成WHERE price >= null,查出来永远空列表。这也是为什么建议在 Controller 入口做参数校验,而不是依赖 MyBatis-Plus 自动处理。

图片上传这块,资源包里通常有一个FileController,用 MultipartFile 接收文件,再把文件保存到本地目录,并在数据库里存访问路径。保存路径常写到 application.yml 里:

file: upload-dir: D:/workspace/shop/uploads

上传接口核心逻辑如下:

@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) throws IOException { String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); // UUID 重命名避免同名覆盖 String filename = UUID.randomUUID().toString().replace("-", "") + suffix; File targetFile = new File(uploadDir + File.separator + filename); file.transferTo(targetFile.getAbsoluteFile()); return Result.success("/files/" + filename); }

这里有个细节用 UUID 重命名,是为了避免用户上传同名文件互相覆盖。有些资源包直接拿原始文件名保存,一旦两个商品图片都叫 1.jpg,后面的会覆盖前面的。同时还要处理扩展名校验,不然别人传一个 .jsp 或 .html 到静态目录,会成为安全漏洞。这是拆这类项目时第一个要检查的点:上传路径是否在项目外,访问路径是否做了虚拟映射。如果上传目录写在项目 src 里面,重新打包后文件全没;如果没做虚拟映射,数据库存的/files/xxx会 404。

4.2 购物车:数据库实现和 Redis 取舍

购物车在这个资源包里没有用 Redis,而是直接存了一张shop_cart表,字段包括 id、user_id、product_id、count、checked。原因很直接:课程设计和毕业设计要求能演示数据库操作,Redis 会增加部署成本,数据也更容易在答辩现场丢失。如果想把购物车换成 Redis,不是不可以,但要注意商品价格变化、勾选状态同步、登录状态过期这些边界,往往比用数据库复杂得多。

数据库式购物车的核心操作是“存在则数量加一,不存在则新增”。Java 代码里常见做法是先查再决定更新或插入:

public Cart addCart(Long userId, Long productId, Integer count) { LambdaQueryWrapper<Cart> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Cart::getUserId, userId) .eq(Cart::getProductId, productId); Cart exist = cartMapper.selectOne(wrapper); if (exist != null) { // 已存在则累加数量 exist.setCount(exist.getCount() + count); cartMapper.updateById(exist); } else { Cart cart = new Cart(); cart.setUserId(userId); cart.setProductId(productId); cart.setCount(count); cart.setChecked(true); cartMapper.insert(cart); exist = cart; } return exist; }

注意这里getUserId要从 session 或 JWT 里取,而不是前端传 user_id。很多接口直接把 userId 放请求体,后端没做校验,导致用户可以操作别人购物车,这是毕业设计答辩时被问“安全性”的高频坑。正确做法是在拦截器里解析 token,把 userId 塞进 ThreadLocal 或 request attribute,Service 里再统一读。这个资源包如果没做,建议改之前先补上,成本不高但很显水平。

购物车列表页要展示商品图、名称、单价、数量、小计,所以数据库查询不能只查 cart 表,要联表查询 product 表。MyBatis-Plus 里一般写一个 CartVO,用 XML 自定义 SQL join:

select c.id, c.user_id, c.count, c.checked, p.product_name, p.price, p.image, p.stock from shop_cart c left join shop_product p on c.product_id = p.id where c.user_id = #{userId}

left join 而不是 inner join 的好处是,就算商品被删了,购物车记录还在,前端可以提示“商品已失效”。用 inner join 的话,一旦商品上下架或删除,购物车记录会静默丢失,用户以为没加过。字段命名用下划线,配合map-underscore-to-camel-case会自动映射成驼峰。

4.3 下单流程:库存扣减与订单状态机

订单模块是这类商城最复杂的地方,也是答辩中容易暴露问题的地方。一个完整流程是:购物车勾选商品 → 生成确认订单页 → 提交订单 → 扣库存 → 生成支付单 → 支付回调 → 更新订单状态。资源包里为了演示方便,通常会简化成两步:点提交订单直接创建订单,再模拟支付回调把状态从待支付改成已支付。

订单表字段常见有 order_no、user_id、total_price、status、receiver_name、receiver_phone、create_time。status 一般用整数表状态:0 待支付,1 已支付,2 已发货,3 已完成,4 已取消。很多资源包把这个状态直接写在代码里,建议至少要定义常量或枚举:

public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), FINISHED(3, "已完成"), CANCELLED(4, "已取消"); private final Integer code; private final String desc; }

创建订单的核心是事务方法,因为要同时插入订单主表、订单明细表、扣减库存、清空购物车。任何一步失败,前面插入的数据都应该回滚。典型实现:

@Transactional(rollbackFor = Exception.class) public Order createOrder(OrderCreateRequest req) { // 1. 校验库存 for (OrderItem item : req.getItems()) { Product product = productService.getById(item.getProductId()); if (product.getStock() < item.getCount()) { throw new ServiceException("库存不足:" + product.getProductName()); } } // 2. 创建订单主表 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setStatus(OrderStatus.UNPAID.getCode()); order.setTotalPrice(calculateTotal(req)); orderMapper.insert(order); // 3. 插入明细、扣库存、清购物车 orderItemService.saveBatch(req.getItems()); productService.reduceStock(item.getProductId(), item.getCount()); cartService.clearCart(req.getUserId()); return order; }

@Transactional默认只回滚 RuntimeException,rollbackFor = Exception.class是为了连受检异常也一起回滚。扣库存这里应该用乐观锁或更新语句加条件,而不是查出来再 update,原因是不能确定两个用户会不会同时买最后一个商品。安全写法是在 Mapper 里写一条 update SQL:

update shop_product set stock = stock - #{count} where id = #{productId} and stock >= #{count}

这条 SQL 返回更新行数,如果返回 0 说明库存不足。很多同学用 select 判断库存大于 0,然后再 update,中间隔了几毫秒就可能超卖。把这个点写在答辩稿里,老师会觉得你是认真想过并发问题的。

支付回调的模拟更简单,通常就是一个接口把订单状态从 0 改成 1,顺带记录支付时间。真实支付渠道需要签名验签和异步通知,这部分不是资源包的重点,知道边界就行。

5. 避坑指南:常见问题与排查记录

5.1 前端路由刷新后 404:历史模式与静态资源冲突

现象:本地npm run serve正常,但打包后丢到后端运行,或直接访问/product/1再刷新,浏览器报 404。

原因:Vue Router 开启了 history 模式,URL 里没有#。后端只识别接口路径,不识别前端路由,刷新时向后端请求/product/1,自然 404。

解决:开发环境用 Webpack devServer 的historyApiFallback,生产环境在 Nginx 用try_files;如果坚持把前端静态文件放进 Spring Boot,需要实现一个 forward 控制器,把非/api路径转发到index.html。先切回 hash 模式验证,改成 createWebHashHistory,刷新后问题消失,确认是 history 模式问题后再去守生产配置。

5.2 跨域请求被拦截:后端加 CORS 还是不优雅

现象:后端接口用 Postman 测试正常,浏览器访问前端页面后,控制台报CORS policy: No 'Access-Control-Allow-Origin' header。

原因:前后端分离后,前端 devServer 代理如果没配,浏览器直接跨域请求后端,后端响应头没允许跨域。开发环境用了代理但还是报错,往往是 axios baseURL 写了完整http://localhost:8080而不是/api,代理被绕过。

解决:开发环境统一走代理,代码里不写绝对地址;后端如需兼容多端调试,可以加一个 CORS 配置类,限定允许的 origin 不是*,并允许 Authorization 头。不要为了省事直接配allowedOriginPatterns("*"),答辩时老师很可能会问“为什么允许所有域名跨域”,到时候不好接住。

5.3 MySQL 连接失败:驱动、时区、SSL 三选一

现象:后端启动报Access denied for user或Communications link failure或The server time zone value。

原因:密码不对和 URL 参数缺 serverTimezone 是最常见的两种。密码那个问题有些人会怀疑人生,但其实是 SQL 脚本初始化的密码和 application.yml 不一致。时区问题只出现在 MySQL 8 和连接驱动版本不匹配时。

解决:第一步,用命令行连一次 MySQL,确认密码;第二步,把 URL 加上serverTimezone=Asia/Shanghai和useSSL=false;第三步,MySQL 8 用com.mysql.cj.jdbc.Driver,MySQL 5.7 用com.mysql.jdbc.Driver。日志里如果明确写 Access denied,基本不用看别的,先去核对账号密码和 host 授权。

5.4 后端启动报错:端口占用和 Lombok 编译失败

现象:执行java -jar后日志出现Port 8080 was already in use;或mvn compile时报cannot find symbol,但代码里明明写了@Data。

原因:端口占用是上一个 Java 进程没停。Lombok 报错很反直觉,本质是 Lombok 版本不兼容新 JDK,或者 IDE 的 Lombok 插件没启用。

解决:端口占用用命令查 PID 再杀掉:

netstat -ano | findstr :8080 taskkill /PID 进程号 /F

Lombok 问题先检查 pom 里 Lombok 版本和 IDE 版本,再看 IDE 注解处理是否开启。如果用的是 Maven 命令行打包,报错更直接,换一个小版本号后重新跑mvn clean compile。另外注意,Spring Boot DevTools 热重启也会让端口短暂占用,杀掉 java 进程时看清楚是不是多个任务。

5.5 图片上传成功但访问 404:虚拟路径映射没配置

现象:前台商品页图片全部裂开,F12 看图片地址是localhost:8080/files/abc.jpg,直接打开也 404,但文件明明已经被保存到了本地。

原因:文件保存在了项目外的D:/workspace/shop/uploads,但 Spring Boot 没有把/files/**映射到这个目录。静态资源默认扫描 classpath:/static,不扫描外部目录。

解决:在配置类里注册一个 addResourceHandlers:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // /files/** 映射到本地磁盘目录 registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadDir + "/"); }

addResourceHandler是前端访问的 URL 路径,addResourceLocations是真实磁盘路径。file:前缀不能省,否则会被当成 classpath。如果上传目录不在项目里,重新部署后图片不会丢,这也是推荐外部存储路径的原因。Windows 和 Linux 的路径分隔符要按系统处理,不要写死成D:/。

6. 让项目在答辩/验收中更稳:验证清单与两个实用的扩展

6.1 主流程核验清单

在展示之前,我会先跑一遍这张表,避免现场翻车:

验证项预期结果检查点
用户注册/登录登录后跳首页token 写入 sessionStorage,刷新后不失效
商品浏览分页正常,图片显示图片虚拟映射已配置
添加购物车数量累加同一商品重复添加
提交订单库存减少,订单生成库存扣减用条件更新
支付模拟状态变为已支付订单状态跳转
后台管理商品可上下架接口权限校验

这张表看起来基础,却是最容易松的一环。常见情况是购物车和订单不是同一个 userId,最后发现是登录用户从请求体读取导致的数据错位。从那以后我每次拿到新项目,都会强制把主流程完整走一遍,再写代码。

6.2 加一个订单导出功能

如果答辩时间宽裕,最值得加的小功能是订单导出到 Excel。用 EasyExcel 写一个导出接口,代码量不大,但能把“读写文件、流处理、接口设计”都体现出来:

@GetMapping("/export") public void exportOrders(HttpServletResponse response, @RequestParam Long userId) throws IOException { List<Order> orders = orderService.listByUserId(userId); List<OrderExcelVO> data = orders.stream().map(order -> { OrderExcelVO vo = new OrderExcelVO(); vo.setOrderNo(order.getOrderNo()); vo.setStatus(OrderStatus.getByCode(order.getStatus()).getDesc()); vo.setTotalPrice(order.getTotalPrice()); return vo; }).collect(Collectors.toList()); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-Disposition", "attachment;filename=orders.xlsx"); EasyExcel.write(response.getOutputStream(), OrderExcelVO.class).sheet("订单").doWrite(data); }

导出功能要提醒的是,不能用前端 setTimeout 直接下载,要写成 Promise 处理成功和失败;文件名不要用中文,避免响应头编码问题。答辩时老师问“这个项目有什么可以改进”,你就能顺势回答:订单导出、支付回调、库存乐观锁、Redis 缓存热点商品,这些都是看得见的点。这份在线购物平台的资源包按标题就能找到,下载后建议先核对第 3 章的版本清单再启动。资源包本身可以跑通全流程,但你要把它变成“自己的项目”,至少要在代码里留下三五个属于自己的改动,哪怕只是把魔法数字换成枚举、把上传文件名加 UUID,也足以拉开差距。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询