ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue运动服装销售系统:前后端分离电商项目实战解析

SpringBoot+Vue运动服装销售系统:前后端分离电商项目实战解析

做电商类的毕设或者练手项目,SpringBoot加Vue这套组合基本是绕不开的。今天要聊的这款"运动服装销售系统",正好是这个技术栈里比较典型的一个:后端SpringBoot负责业务逻辑和数据处理,前端Vue负责页面交互和界面渲染,数据走MySQL,整体是一套完整的前后端分离电商类项目。系统围绕运动服装这个品类,把商品展示、分类检索、购物车、下单流程、后台商品管理、订单管理这些核心模块都做了,既可以直接当毕业设计选题来用,也可以拿来当作自己学习前后端分离开发流程的参考范本。如果你正在找一套结构清晰、资料齐全的JavaWeb项目来跟代码、理解思路,或者需要一套能改改就用、能讲清楚设计亮点的选题,这篇文章值得看完。

1. 项目整体设计与技术选型思路

1.1 为什么是SpringBoot+Vue这对组合

先说后端。SpringBoot这几年在Java后端项目里的统治力没什么好争议的,它把Spring那一套繁琐的XML配置全部干掉,用自动配置和内嵌容器把项目启动成本压到极低。对毕设或者个人练手来说,最直接的好处是:你不用花大量时间在处理Tomcat部署、Spring配置这些环境问题上,打开IDE、写个启动类,项目就能跑起来。而且SpringBoot生态里的starter机制非常成熟,接MyBatis、接Redis、接文件上传,都是引入一个依赖、加几行配置的事,学习成本友好,答辩的时候也容易讲清楚原理。

前端选Vue的原因更简单:它在国内中小型项目里的普及率太高了,组件化开发、双向绑定、虚拟DOM,上手曲线比React平滑很多,配合Element UI或者Element Plus这类组件库,两天时间就能把管理后台的界面搭得像个样子。对于销售系统这种页面类型固定、交互偏CRUD的项目,Vue的模板语法和组件复用机制简直是为它量身定做的。

再说回业务本身。运动服装销售系统本质上是一个垂直电商系统,业务链路是"用户浏览商品→加入购物车→生成订单→后台发货管理",再加上后台的商品维护、分类管理、会员管理。这类业务用前后端分离架构非常合适:前端专注页面展示和交互体验,后端专注接口和数据处理,两边通过JSON格式的RESTful接口通信。实际开发中也可以把前端页面拆成用户端商城和管理后台两个部分,复用同一套后端接口,这也是这类项目展示设计能力的一个加分项。

1.2 功能模块与业务逻辑拆解

这类销售系统的功能模块看起来多,但归纳起来就是两条线:用户购物线和后台管理线。

用户购物线包括:

  • 注册与登录:账号密码注册登录,登录后获取Token,后续请求携带Token访问受限接口
  • 商品浏览:首页推荐、分类筛选、商品详情查看
  • 购物车管理:加入购物车、修改数量、删除、勾选结算
  • 订单流程:确认订单、生成订单、在线支付(模拟)、查看订单状态
  • 个人中心:我的订单、收货地址、个人信息修改

后台管理线包括:

  • 管理员登录与权限校验
  • 商品管理:商品的增删改查、上下架、库存调整、图片上传
  • 分类管理:商品分类的维护,支持一级分类或两级分类
  • 订单管理:订单列表筛选、发货操作、查看订单明细
  • 用户管理:用户列表、禁用/启用账号
  • 数据统计:简单的销售数据统计,比如订单量、销售额曲线

这个功能划分是标准的毕设级电商系统粒度。做的时候我的建议是:不要一开始就想着把功能堆满,先把主链路打通——从商品列表到下单成功,这是一条完整闭环;链路通了之后,再往后台加管理功能,最后有余力再补统计、补营销工具。很多同学喜欢先做登录、先做后台,结果核心购物流程反倒赶工,答辩时被问几个业务细节就露馅了。

1.3 数据库设计要点

数据库设计是这类项目里最值得花时间的地方,因为面试和答辩都喜欢问表结构、表关系。

核心表我列一下:

表名核心字段说明
userid, username, password, nickname, phone, avatar, role, status用户表,role区分普通用户和admin
categoryid, name, parent_id, sort分类表,parent_id为0表示一级分类
productid, category_id, name, subtitle, main_image, detail_images, price, stock, sales, status商品表,status控制上下架
cartid, user_id, product_id, quantity, checked购物车表
ordersid, order_no, user_id, total_price, status, receiver_name, receiver_phone, receiver_address, create_time, pay_time, ship_time订单主表
order_itemid, order_id, product_id, product_name, product_image, price, quantity订单明细表,冗余商品快照

几个关键点:

一是订单表要拆成主表和明细表。一个订单包含多个商品,如果不拆,相同订单号要重复存多次收货信息,数据库冗余严重。明细表里为什么要冗余商品名称和图片?因为商品信息后续可能改名字、换图片,但历史订单必须保持下单时的快照,这是电商系统的常识。

二是商品表的库存字段和销量字段。库存字段是必须的,下单时要扣减库存;销量字段可以没有,但是加上之后做"热卖排行"非常方便,只是需要在订单生成时做同步更新,事务里一起处理。

三是status字段的语义要统一。比如商品status:1上架、0下架;订单status:0待付款、1待发货、2已发货、3已完成、4已取消。用数字还是用字符串,我建议用数字,因为前端做筛选和状态标签映射比较方便,代码里用一个常量类或者枚举统一管理,避免魔法数字满天飞。

2. 核心功能模块的细节与实现关键

2.1 登录鉴权方案:JWT还是Session

这是一个很值得展开的技术点,也是答辩时老师大概率会问的问题。

早期单体项目用Session比较普遍:用户登录后,后端把登录态存在Session里,返回一个JSESSIONID给前端,前端请求时带上这个Cookie。如果前端是服务端渲染的页面,这套方案完全没有问题。但到了前后端分离场景,问题就来了:前端和后端可能不在同一个域名下,跨域请求对Cookie的限制比较麻烦;而且后端如果做成集群,Session复制又是一个额外的工作量。

所以现在前后端分离的毕设项目,绝大多数用JWT(JSON Web Token)。JWT的思路是:用户登录成功后,后端生成一个包含用户ID、用户名、过期时间等信息的加密Token,返回给前端;前端把它存在localStorage或者vuex里,每次请求在请求头里加上"Authorization: Bearer xxx";后端通过拦截器解析Token,得到当前登录用户,校验通过就放行。

这套方案的好处是后端不需要存储Token,天然支持多实例部署,跨域也方便。代价是Token无法主动失效,所以通常会设置一个过期时间,比如2个小时,前端在请求拦截器里发现Token过期就跳转登录页。

实现上很简单:

  • 后端引入jjwt依赖,写一个JwtUtil工具类,提供生成Token和解析Token的方法
  • 写一个拦截器,拦截需要登录的接口路径,从请求头取Token,解析失败就返回401
  • 前端在axios请求拦截器里统一设置Token请求头,在响应拦截器里统一处理401,跳转登录页

有几个细节需要提醒:

Token里不要放敏感信息。有的同学图方便把密码也放进Token里,这是很不好的习惯,Token虽然带签名,但也只是Base64编码,是可解开的。

前端Token的存储位置,localStorage刷新不丢,但XSS攻击能读到;用HttpOnly Cookie更安全,但实现稍复杂。毕设场景用localStorage完全够,但答辩时如果能主动说出这个取舍,会显得你对安全问题有思考。

2.2 商品与分类的数据设计

商品和分类是销售系统最容易做"糙"的地方,很多方案把分类做死,商品字段也随意,导致后期扩展困难。

分类我这里推荐的两级结构:一级分类比如"上衣"、"裤装"、"运动鞋"、"配件",二级分类比如"上衣"下面再分"T恤"、"卫衣"、"外套"。表结构上就是category表里加一个parent_id字段,parent_id为0的就是顶级分类。查询某个分类下的商品时,先查该分类及子分类的ID集合,再做商品查询。这个设计既简单,又能体现树形结构的思路,比单级分类扩展性好,比无限极分类实现成本低,很适合这个项目。

商品表里interaction比较大的是图片字段。一张主图、多张详情图,怎么存储?

方案一是在product表里存一个主图URL字段,再加一个product_image子表存详情图,这是最规范的。方案二是偷懒一点,主图字段存一个URL,详情图片用逗号拼接存到一个字段里,查询时用split拆开。毕设项目用方案二完全没问题,因为详情图数量少、很少单独维护,但需要在代码里写一个图片列表的转换方法,别在页面里去处理逗号字符串。

另外一个常见的坑是商品价格用浮点类型。Java后端用float或者double算金额,比较时容易出现精度误差,比如0.1+0.2不等于0.3。规范的方案是用BigDecimal,尤其是涉及订单总价计算时。MySQL端对应字段用decimal(10,2)。这个细节如果能在设计说明里写一句"金额使用BigDecimal避免精度丢失",那专业度一下子就上来了。

2.3 购物车与订单流程

购物车模块看起来简单,其实有几处容易掉坑的逻辑。

一是购物车表要不要冗余商品信息。我的答案是不要存商品名称、图片、价格,购物车表只存user_id、product_id、quantity、checked就够了。页面展示购物车列表时,通过商品ID去查商品表,拿商品的最新信息。因为购物车是临时的,商品改价后用户看到的应该是当前价格,所以冗余反而会造成数据不一致。

二是商品加入购物车时的数量校验。很多方案没做库存校验,用户添加100件也直接入库,等到下单才报错,体验很差。我的建议是加入购物车时就校验一次库存,下单时再校验一次,双重保障。

订单流程建议做成一个严格的状态机:

0待付款 → 1待发货 → 2已发货 → 3已完成 → 4已取消

用户提交订单时,如果状态是待付款,可以取消;管理员发货后状态变为已发货,不能再取消;用户确认收货后变为已完成;已完成的订单不能退款(毕设可以不写退款逻辑,但状态要占位)。

下单这个动作是整个系统里最需要事务的环节。一次下单涉及的数据库操作有:插入订单主表、插入订单明细表、扣减商品库存、增加商品销量。任何一个操作失败,都必须整体回滚,不然就会出现"订单建了但库存没减"这种严重问题。后端实现时在Service方法上加@Transactional注解,默认遇到RuntimeException就会回滚,这个必须会用。

还有一个稍微进阶的操作:防止重复提交订单。用户快速点两次"提交订单",可能生成两笔相同订单。简单的处理方式是前端按钮加loading禁用,但严谨一点后端可以在订单表加一个用户ID和时间的幂等判断,或者在下单前查询一下是否有相同用户、相同金额、相同商品且时间相近的未支付订单。这个算是加分项,量力而行。

2.4 文件上传与图片处理

商品图片上传是管理后台必须的功能。SpringBoot里用MultipartFile接收前端上传的文件,默认大小限制是1MB,一般商品图片肯定超,所以要在配置文件里调大:

spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=50MB

存储路径有两个方向。一个是本地存储,把上传的文件写到项目运行目录下的upload文件夹,同时配置一个静态资源映射路径,让图片可以通过URL直接访问。另一个是对象存储,比如国内的七牛云、阿里云OSS,把文件传到云上,拿到一个CDN链接。毕设项目推荐本地存储,简单可控,不涉及第三方SDK和付费问题;但如果你想让项目方案更完整,可以在设计文档里对比一下两种方案,说明"企业级生产环境下应该使用对象存储,本地存储仅用于开发环境",这就是一个很好的答辩话术。

本地存储的静态映射配置示例:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceResolver(new PathResourceResolver()); } }

写路径时要注意:Windows下用D:/xxx/upload/,Linux下用/usr/local/upload/,开发时最好把路径配到配置文件里,不要把绝对路径写死在代码中,不然换一台电脑项目就崩了。

另外,上传的文件名一定要处理一下。用户上传的原始文件名可能是中文,可能包含特殊字符,直接拼接路径很容易出问题。常用的办法是用UUID或者时间戳生成新文件名,保留原始后缀名,比如:

String filename = UUID.randomUUID().toString().replace("-", "") + originalFilename.substring(originalFilename.lastIndexOf("."));

这样处理后,文件命名唯一,还能保留图片格式。

3. 从搭建到部署的完整实操记录

3.1 环境准备清单

先把环境捋一遍。我建议的版本组合是:

工具版本建议说明
JDK1.8 或 11如果你是SpringBoot 2.x,1.8完全够;如果是SpringBoot 3.x,必须JDK 17+
Maven3.6.3+后端依赖管理,IDEA内置也可以
MySQL5.7 或 8.0数据库,推荐8.0
Node.js14.x 以上前端运行环境,Vue CLI 5要求Node 12+,推荐14或16
前端脚手架Vue CLI 4/5也可以直接用Vite创建Vue 3项目

这里有个版本兼容性提醒:很多同学项目跑不起来,不是代码问题,是版本错配。比如用了SpringBoot 2.7又配上MyBatis-Plus 3.5的旧版本,或者用Node 18跑Vue CLI 4的依赖树,容易出现奇奇怪怪的报错。我的建议是:后端如果参照网上的教程用SpringBoot 2.x,那就别去动MySQL 8以下的驱动版本;前端如果用Vue 2配Element UI,就不要混着装Element Plus的组件,组件库版本错乱是前端报错的重灾区。

3.2 后端项目的搭建流程

后端环境准备好之后,用IDEA的Spring Initializr创建项目,或者直接去官网start点一下生成zip,下载导入。

核心依赖我列一份:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

MyBatis-Plus算是国内毕设项目的事实标准了,好处是你不用写一大堆Mapper XML,BaseMapper提供单表CRUD方法,分页用分页插件,Demo里需求基本上靠条件构造器就够了。如果你对MyBatis原生写法更熟悉,也可以不用MP,手写XML,代码量会多一些,但对于学习价值来说更高。

后端代码的分层结构要规范,Controller-Service-Mapper三件套是底线,再往上是entity、dto、vo、config、common、utils这几个包。我见过不少同学的代码,所有类都堆在一个包下,几百行一个Service类,虽然功能能跑,但答辩时老师问"你分层分在哪了"会很尴尬。

一个标准的商品列表接口大概是这样的:

Controller层:

@RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "8") Integer pageSize, @RequestParam(required = false) Long categoryId) { return Result.success(productService.getProductPage(pageNum, pageSize, categoryId)); } }

Service层:

public Page<Product> getProductPage(Integer pageNum, Integer pageSize, Long categoryId) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getStatus, 1); if (categoryId != null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.orderByDesc(Product::getSales); Page<Product> page = new Page<>(pageNum, pageSize); return productMapper.selectPage(page, wrapper); }

这里有几个容易被忽略的点:

Result统一返回体。很多同学每个接口都返回不同的结构,前端写得很痛苦。建议写一个泛型Result类,所有接口都返回Result.success(data)或者Result.error(msg),里面有code、msg、data三个字段。前端只用判断code是不是200,代码会清爽很多。

分页参数和返回结构的规范。前端传pageNum和pageSize,后端返回total、pages、records等内容,这也是约定俗成的分页规范,不要自己发明格式。

商品列表查询时优惠的是"只查上架商品"。如果后台下架了一件商品,用户端列表里就不应该再出现它,所以查询条件里加status=1,这是一个非常容易漏掉的逻辑。

3.3 前端项目的搭建流程

前端用Vue CLI创建一个基础项目,然后按需引入Element UI(Vue2)或者Element Plus(Vue3)。

项目结构建议这样:

src/ ├── api/ # 接口封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # vuex状态管理 ├── views/ # 页面 │ ├── admin/ # 后台管理页面 │ └── user/ # 用户端页面 ├── utils/ # 工具函数 └── App.vue

axios封装是前端工程质量的分水岭。很多同学在每个页面里直接写axios.get,代码重复不说,改一个baseURL要全局搜索替换。我的做法是先在utils/request.js里建一个实例:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:添加token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default request

然后每个接口模块一个文件,比如src/api/product.js:

import request from '@/utils/request' export function getProductList(params) { return request.get('/product/list', { params }) }

页面里调用时就很清爽了:getProductList({pageNum:1, pageSize:8}).then(...)。

商品列表页面的核心逻辑无非是:页面加载时调接口拿数据渲染列表,点击分类时带分类ID重新请求,分页组件切换时更新查询参数。这里面有个小技巧:搜索关键词和分页参数要用响应式数据统一管理,每次变化时调用同一个加载方法,而不是在三个地方各写一遍请求代码。

3.4 联调、打包与部署

本地联调阶段,最大的问题就是跨域。

开发时最简单的处理方式:前端在vue.config.js里配置代理,把/api开头请求转发到后端地址:

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

这样前端请求/api/product/list,实际转发到后端的/api/product/list,开发环境下不存在跨域问题,因为浏览器看到的是同源请求。

后端也可以开启CORS配置,两种方式都行。我的建议是只在开发环境开代理,后端不要全局开启CORS,因为生产环境下前端和后端通常是同域部署或者经过Nginx转发,后端的CORS配置反而会有隐患。

打包部署是比较容易出问题的一步,这里给出一个亲测可用的方案:

后端打包:mvn clean package,得到target目录下的jar包,用java -jar xxx.jar启动。注意内置Tomcat默认端口是8080,如果被占用了可以加--server.port=9090指定。

前端打包:npm run build,产出dist目录,里面是编译后的静态文件。

生产环境的部署方式有两种:

方式一:单体部署,把Vue的dist目录放到后端项目的src/main/resources/static目录下,重新打包,这样一个jar里同时包含后端接口和前端页面,直接java -jar启动后,访问本机IP加端口就是完整系统。这种方式部署最简单,适合交作业演示。

方式二:前后端分离部署。后端jar单独跑在8080,前端dist目录交给Nginx托管,Nginx监听80端口,请求/api开头的路径转发到后端。这种方式更贴近生产,配置也不复杂:

location /api/ { proxy_pass http://localhost:8080/; } location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

最后一行try_files很关键,它解决的是前端路由刷新404的问题,后面会展开讲。

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

4.1 跨域问题

跨域报错大概是你做前后端分离项目第一个遇到的拦路虎。浏览器控制台出现"No 'Access-Control-Allow-Origin' header is present",不用慌张,这就是前端页面在3000端口,后端在8080端口,两个端口不同就产生了跨域。

解决方案前面提到了:开发环境用前端代理,生产环境用Nginx反代。但还有一个临时应急的办法,就是后端加一个CORS配置类:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

这段配置开发时很好使,但生产环境我仍然建议关掉它,原因前面已经说了。

有一个隐蔽的坑:如果后端用了拦截器,跨域预检请求OPTIONS会被拦截器拦截掉,导致CORS配置失效。遇到这种情况,在拦截器里放行OPTIONS请求即可。

4.2 前端路由刷新404

这是Vue路由的一个经典坑。前端用了Vue Router的History模式,URL看起来是"http://localhost:8080/#/home"这种,刷新是按"http://localhost:8080/product/1"这种模式访问的。

开发环境一般不会出问题,因为webpack-dev-server默认支持history fallback。但部署到Nginx后就露馅了:用户在首页点击跳转没问题,因为这是前端路由,所有页面切换都是前端控制的,浏览器地址栏有反应但不发送真实请求;可是用户按F5刷新,浏览器就会向服务器发起一个对"/product/1"的请求,Nginx发现没有这个文件,直接返回404。

解决办法就是在Nginx配置里加try_files指令,把所有的路由请求都指向index.html,由前端路由去解析:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

如果是SpringBoot单体的部署方式,则要在后端加一个ForwardController,把所有非接口的请求转发到index.html:

@Controller public class ForwardController { @RequestMapping(value = "{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }

4.3 数据库连接与中文乱码

数据库连接串里,最经典的两个参数是useSSL和serverTimezone。

如果用MySQL 8.0,连接串建议这样写:

jdbc:mysql://localhost:3306/sport_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai

serverTimezone这个参数不写,通常报"server time zone value"错误,因为MySQL 8.0默认时区设置和中国不一样。用useSSL=false是因为本地开发用SSL加密连接没有意义,反而会产生告警。

中文乱码问题分三层排查:

第一层是数据库表的编码。建表时指定utf8mb4,这是兜底。

第二层是后端接口返回的中文乱码。SpringBoot默认就会把响应用UTF-8编码,一般不会出问题。重点检查的是数据库驱动连接串有没有带characterEncoding=utf8。

第三层是前端页面显示乱码。检查HTML文件的meta标签,确认charset是utf-8。

从MySQL看,拷贝一张表到项目里时最容易出现乱码,因为原库可能用了latin1或gbk。解决方法是建库时显式指定字符集:

CREATE DATABASE sport_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

4.4 图片上传成功了但页面看不到图片

这个问题高发。图片传上去了,数据库里也存了路径,但浏览器访问图片路径是404。

原因通常是后端没做静态资源映射。SpringBoot默认只映射classpath:/static/作为静态资源目录,如果你把图片写到D:/upload/这种外部目录,不配置映射是访问不到的。

解决方案前面已经写过WebMvcConfig,注册/addResourceHandler("/upload/")映射到本地磁盘路径。这里提醒一个细节:映射路径不要写成"/upload/"而存储目录是"upload/"相对路径,这样在打包成jar后运行会上很多坑,因为jar内部结构不是普通文件系统。我建议统一使用本地磁盘的绝对路径,比如D:/project/upload/,映射到/upload/**,这样开发、部署后都稳定。

第二个常见原因是权限/跨域问题,特别是前后端分离部署的时候,后端接口返回的是类似"http://localhost:8080/upload/xxx.jpg"的地址,前端页面在80端口,图片请求8080端口又产生了跨域,需要后端对图片接口也能处理跨域或者通过Nginx转发。

4.5 登录状态丢失

Token登录模式下,最常见的"登录状态丢失"是这么发生的:用户登录成功,跳转页面,刷新一下或者重新打开浏览器,发现又回到登录页了。

排查方向有一个:确认Token存哪里。如果用了localStorage,刷新不会丢;如果用了vuex,刷新就清空。很多同学把Token从接口返回后存到vuex里,然后忘记同时写一份到localStorage,结果页面一刷新vuex重新初始化,Token就没了。

另一个方向是时间问题。后端设置的Token过期时间太短,比如30分钟模拟支付,用户隔一会儿再操作就提示登录过期。毕设项目建议Token有效期设置成2小时或者更长,省得演示到一半跳登录页这种尴尬情况。

如果要做到关闭浏览器后仍保持登录,可以把Token存到localStorage,并在axios拦截器里每次请求都带上它。如果要更严谨一些,后端做token续期,前端拦截每个请求的响应,如果token快过期就自动刷新,不过这是加分项,不是必修项。

4.6 端口占用与Maven依赖下载慢

启动项目报"Port 8080 was already in use"是最容易解决的:

Windows下用netstat -ano | findstr 8080查占用进程,然后taskkill /PID 进程号 /F,或者直接换端口启动。

Maven依赖下载慢的根源是默认中央仓库在国外。解决方法是修改Maven的settings.xml,加一个国内镜像源,这一点对国内开发者来说几乎是必做操作。换完镜像之后,新项目导入依赖的速度肉眼可见地提升。

还有一个小坑:MyBatis-Plus 3.5以上版本,分页插件需要单独配置一个分页拦截器bean,不配置的话分页查询不会真正生效,返回的数据是全量而非当前页。这是很多人写完后发现"明明分了页怎么数据不对"的原因:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

5. 写在最后的几点实操心得

这个运动服装销售系统,看着是一个典型的JavaWeb毕设项目,但把每个细节做好、讲清楚,其实能体现出不少工程素养。

我自己的体会是,做这类项目最忌讳的是"重代码轻设计",拿到一个Demo就开始抄,界面抄完了,数据库表和接口为什么这么设计完全说不上来。答辩时老师不太会去抠你某个页面布局,更多会问你:表为什么要拆成两张、下单时库存怎么扣、Token过期了怎么办、跨域怎么处理、分页怎么实现的。这些问题你一旦自己动手写过、踩过坑、修过Bug,回答起来跟背出来的感觉是完全不一样的。

另外一个很实际的建议:整套项目除了源码,配套的论文、部署文档和讲解视频,最好自己通读一遍,改动代码前先在文档里标出对应的模块,因为答辩现场可能不允许完全照搬别人写的东西,有时候问你一个"这里为什么这么写",如果你连文档里是怎么描述的都答不上来,就非常难圆场。

如果你打算拿这个项目做二次开发,我建议优先把这几块做升级:商品搜索模块(目前很多基础版本只有分类筛选,加上关键词模糊搜索就能覆盖不少场景)、订单的物流跟踪(用一个物流单号字段加状态即可)、数据看板(把统计报表做成可视化图表)。这三个方向投入产出比高,并且都能在答辩时讲出"我做了什么、解决了什么问题"。

最后再分享一个小技巧:开发时把数据库的表结构文档、接口文档、部署步骤三份文档整理好放一起,每完成一个模块就把对应的SQL字段、接口调用数据、验证结果记录下来。等最后写论文和做演示的时候,你会感谢自己当时的记录习惯。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进