
这套 SpringBootVue 网上服装商城平台核心就是一个前后端分离的 Java Web 项目完整度拉到毕设答辩完全够用。包含项目源码、SQL 脚本和接口文档这三件套意味着你拿到手不是一堆零散页面而是一条从数据库建模、后端接口、前端交互到联调部署都能跑通的完整链路。我用实际开发这个项目踩过的坑和填过的细节把整个拆解过程写出来正在做 Java Web 毕设、或者想拿真实商城项目做二次开发的同学可以直接照着这套思路走。这个项目解决的典型痛点是很多毕设选题不是太简单就是太空洞网上商城这种“麻雀虽小五脏俱全”的业务模型刚好卡在合适的复杂度上——用户、商品、购物车、订单、后台管理都有但又没有复杂到半年搞不完。技术上用了 SpringBoot 做后端接口Vue 做前端页面两端的交互通过 JSON 完成这也正是目前主流企业开发中最常见的分工方式。1. 项目整体拆解与技术选型为什么这套 JavaWeb 组合值得做先说技术选型的原因。SpringBoot 的核心价值不在于它有多新而在于它极大降低了 Spring 家族的使用门槛。传统 SSM 项目要手工配一大堆 XML光数据源、事务管理器、MyBatis 扫包就得折腾半天SpringBoot 用自动配置帮你把大部分基础工作做了。你只需要保证依赖引入正确、配置文件写清楚启动类一跑整个容器就起来了。对毕设来说重点能放在业务代码上而不是一次次被环境配置折磨。Vue 这边同理。我用的是 Vue 2 配合 Vuex 和 Vue Router 的组合这套在国内教学和毕设项目里最普及资料多到查不完。Vue 的组件化开发方式天然适合商城这种页面模块高度复用的场景商品卡片是一个组件购物车条目是一个组件后台表格也可以抽成公共组件。组件写好之后页面与页面之间只需要通过路由切换数据传递靠状态管理或者父传子的 props思维负担比直接操作 DOM 的 jQuery 时代小得多。1.1 前后端分离这个毕设背后的核心架构思路前后端分离是这个项目最核心的架构决定。简单说前端只负责“看起来怎么样”和“用户点了之后干什么”后端只负责“数据是什么”和“数据该怎么处理”。前端通过 axios 发 HTTP 请求后端返回 JSON 字符串两边唯一的约定就是接口地址和数据结构。这个选择有几个直接好处并行开发效率高。前端不用等后端写好才能动手先 Mock 一套假数据就能把页面做完等后端接口出来再把请求地址一换就行。职责边界清楚。后端不会因为页面改版而被迫改代码前端也不会因为业务规则变动而去翻 Java 代码。部署灵活。前端打包成静态文件丢到 Nginx 或者直接放在后端静态资源目录后端打包成 Jar 独立运行任何一个崩了都能单独排查。对毕设来说前后端分离还附带一个隐性优势论文中能多写一个“前后端交互设计”环节把 HTTP 请求流程、数据格式约定、接口文档设计讲清楚这正好是答辩评审喜欢看到的内容。1.2 服装商城的模块边界从用户浏览到后台发货做商城第一步不是写代码是画功能边界。我的做法是把整个系统拆成两个端口、四个业务域。用户端解决的是“逛、选、买、查”后台管理端解决的是“管商品、管订单、管用户”。整个系统的具体功能模块拆下来大概是这样的模块用户端功能管理端功能用户模块注册、登录、个人信息修改用户列表查看、禁用/启用商品模块分类浏览、关键字搜索、商品详情商品新增/编辑/上下架、分类管理购物车模块加购、修改数量、删除、清空不涉及数据存储层面共享订单模块提交订单选收货地址、支付模拟、订单列表、订单详情、取消订单订单列表、订单状态审核/发货其他首页轮播图、公告展示轮播图维护这个边界划分想清楚之后后面的数据库设计和接口规划就有了参照物。要特别注意一个容易被忽略的点购物车的状态到底是登录后存后端表还是只能前端存 localStorage我最终选择存后端表虽然前端存储能让体验更快但毕设论文里“数据持久化到 MySQL”这句话更站得住脚答辩被问“刷新后购物车为什么还在”时后端方案就是最好的回答。真正做成两个端口之后你会发现前端页面再多后端接口其实是按资源来命名的一个商品模块就对应一个/product系列的接口不会乱。这套模块拆分还有一个隐藏设计原则不追求大而全但必须闭环。用户从注册登录到浏览商品再到下单完成是一条完整可验收的业务链路管理员从商品录入到订单处理也是一条完整可验收的业务链路。两条链路互不交叉但共享数据这就是商城系统最有价值的参考点。2. 数据库设计与 SQL 脚本落地先把数据的根扎稳商城系统的核心是数据而数据的第一站就是数据库表。很多同学一上来就写代码结果写一半发现表结构不对回头改表、改实体类、改 SQL三件套跟着一起返工非常痛苦。我拿到项目后是先花一个晚上把表结构定清楚的之后再写任何一行代码心里都有底。2.1 核心表结构7 张表讲清楚商城的数据脉络这个网上服装商城我总共设计了 7 张核心业务表外加一张管理员表。下面这 7 张表构成了整个系统的数据基础表名含义关键字段说明user用户表id, username, password, phone, avatar, create_timecategory商品分类表id, name, parent_id支持多级分类product商品表id, category_id, name, description, price, stock, cover_image, images, statuscart_item购物车表id, user_id, product_id, quantity, checkedorders订单主表id, order_no, user_id, total_amount, receiver_name, receiver_phone, address, status, create_timeorder_item订单明细表id, order_id, product_id, product_name, product_image, price, quantityaddress收货地址表id, user_id, receiver_name, receiver_phone, province, city, detailproduct 表是整张表结构的重中之重。服装类商品和普通商品不一样它有颜色、尺码、材质这些属性。如果每增加一个属性就加一列表结构会变得很死板。我采用的是简化做法把颜色、尺码直接作为普通字段存进 product 表如color、size、material这样对毕设来说足够直观也方便前端做筛选。如果要做真正的电商系统应该拆出 SKU 表但那对毕设来说会显得过于复杂答辩解释起来也费劲。orders 和 order_item 是典型的主从表结构。一张订单可能包含多个商品如果全塞进一张表里会出现大量重复的收货人信息。所以拆成两张表orders 只存订单的主信息谁买的、买了多少钱、送到哪order_item 存明细行每个商品单价多少、数量几件、当时快照的名称和图片。这个拆法不只在毕设里正确在真实电商系统里也是一样的思路。订单明细里为什么要把 product_name 和 product_image 冗余存一份这是很多人不理解的地方。因为商品表的数据是可以被修改甚至删除的而订单是交易凭证下单那一刻的商品信息必须被固定下来。哪怕管理员后来把商品改名、删图用户的订单详情里依然能看到当时买的东西长什么样、叫什么名字、多少钱买的。这个细节在答辩时亮出来是非常加分的设计考量。2.2 从建库到初始化数据SQL 脚本的关键细节拿到项目源码后第一个要跑起来的就是 SQL 脚本。脚本看起来就是一段建表语句但有几个细节直接决定你能不能一把跑通。首先是字符集问题。建库时我明确指定了DEFAULT CHARSETutf8mb4而不是用数据库默认的 latin1。如果忘记指定插入中文数据时大概率出现乱码前端页面上就是一堆问号。utf8mb4 比 utf8 多一层好处是完整支持 emoji 字符虽然商城可能用不上表情包但商品名称里出现特殊符号时更稳。CREATE DATABASE IF NOT EXISTS clothing_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE clothing_mall; CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, name varchar(128) NOT NULL COMMENT 商品名称, description text COMMENT 商品描述, price decimal(10,2) NOT NULL COMMENT 单价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, images text COMMENT 多图URL逗号分隔, color varchar(32) DEFAULT NULL COMMENT 颜色, size varchar(32) DEFAULT NULL COMMENT 尺码, material varchar(64) DEFAULT NULL COMMENT 材质, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT商品表;InnoDB 引擎的选择也要多解释一句。早期 MySQL 默认可能是 MyISAM但它不支持事务和外键。商城里的下单操作一定是事务性的订单插入成功、明细插入成功、库存扣减成功三个操作要么全成功要么全失败否则会出现“扣了库存但没有订单”这种数据不一致的严重 bug。InnoDB 支持行级锁和事务是商城系统唯一正确的选择。初始化数据方面脚本里预先插入了一批测试数据三到四个分类T恤、卫衣、裤装、外套每个分类下面十几个商品一个测试账号用户名test密码加密后的值一个管理员账号一条默认收货地址。这样前端页面一旦连接成功后立刻能看到有模有样的商品列表不会面对一片空白发慌。SQL 脚本里还有一个容易被忽略的点外键约束要不要加我最后选择了不加物理外键而是在代码层面维护关系。原因很简单物理外键会在删除分类时产生约束冲突比如分类下面还有商品就不让删这在实际操作中很烦而且如果你后来想做分库分表或者换数据库物理外键会变成负担。毕设里用逻辑外键就是普通索引字段通过 service 层代码控制引用完整性完全足够答辩时也能自圆其说。3. SpringBoot 后端实现接口怎么写得又快又稳后端是整套系统的心脏。我用 SpringBoot 2.x 搭建MyBatis 做数据持久化MySQL 存储数据。这里一个关键选择是没用 MyBatis-Plus虽然它的BaseMapper封装的 CRUD 很方便但毕设项目里手写 SQL 更能展示基本功。你想想答辩导师问“讲讲你的数据库操作怎么实现的”你说“框架自动生成的”和你说“我手写 SQL 关联了两张表做了分页查询和条件拼接”效果完全不一样。3.1 项目分层结构与统一响应体封装后端代码结构必须做到见名知意。我的包结构是标准的 Controller-Service-Mapper 三层com.example.mall ├── controller // 接收前端请求 ├── service // 业务逻辑层接口实现 ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体类 ├── dto // 请求/响应数据传输对象 ├── config // 配置类CORS、拦截器、WebMvc ├── common // 统一响应、异常处理、工具类 └── MallApplication.java前后端对接最容易出问题的地方就是返回格式不统一。早期我很随意有的接口返回true、有的返回字符串、有的返回List前端接数据时就得写一堆分支判断非常痛苦。后来我设计了统一响应体ResultT所有接口不管成功失败返回的都是同一个 JSON 结构public class ResultT { private Integer code; // 200成功 400参数错误 401未登录 500服务器异常 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(操作成功); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }这个统一响应体带来的直接收益就是前端 axios 响应拦截器只需要判断code 200就进入正常业务处理其他情况统一弹错误提示。不需要每个请求单独去写 try-catch 或者判断status值代码量直接少掉三分之一。分页查询的返回结构也值得设计一下。我用了一个PageResultT里面包含total总条数、pages总页数、list当前页数据这个结构是我手动算出来的先查总条数再算偏移量最后执行 LIMIT 查询。前端的 el-pagination 组件吃这个结构刚刚好。3.2 注册登录与 JWT 鉴权不是所有接口都能随便进商城有两类接口一类是公开接口比如商品列表、商品详情另一类是必须登录才能访问的比如购物车、订单。实现上我用 JWTJSON Web Token做无状态鉴权。登录流程是这样的用户提交用户名和密码后端先查数据库确认用户存在然后校验密码。密码我用了 BCrypt 做加密而不是最初的 MD5。MD5 的问题是同样的密码加密结果一样彩虹表一查就破解BCrypt 每次加密都会混入随机盐同一个密码两次加密结果完全不同安全性高一个数量级。Service public class UserServiceImpl implements UserService { Override public String login(String username, String password) { User user userMapper.findByUsername(username); if (user null) { throw new BizException(用户不存在); } // BCrypt校验密码 if (!BCrypt.checkpw(password, user.getPassword())) { throw new BizException(密码错误); } // 生成JWT有效期24小时 String token JwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return token; } }JWT 生成之后返回给前端前端存在 localStorage 里每次请求带在请求头的Authorization字段。后端用一个拦截器统一处理public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行登录接口和商品公开查询接口 if (handler instanceof HandlerMethod) { String uri request.getRequestURI(); if (uri.contains(/user/login) || uri.contains(/user/register) || uri.contains(/product/list) || uri.contains(/product/detail)) { return true; } } // 其他请求校验token String token request.getHeader(Authorization); if (token ! null JwtUtil.verify(token)) { return true; } response.setStatus(401); return false; } }这个拦截器有个细节要注意放行规则不能硬编码在拦截器里越写越多。更好的方案是自定义一个PassToken注解在不需要登录的接口方法上打注解拦截器通过反射判断方法上有没有这个注解来决定放行还是拦截。这样以后新增公开接口只需要在 controller 方法上加一行注解不用改拦截器逻辑。3.3 商品查询与购物车接口的实战写法商品查询是商城最核心的接口之一。用户在首页或列表页选择分类、输入搜索词、设置价格区间然后按页码翻页。这个请求参数比较多我用一个ProductQueryDTO来接收public class ProductQueryDTO { private Long categoryId; private String keyword; private Double minPrice; private Double maxPrice; private Integer pageNum 1; private Integer pageSize 10; private String sortRule; // default 综合 new 新品 price_asc 价格升序 price_desc 价格降序 }对应的 SQL 写法要注意两点一是多条件查询用动态 SQL 拼接MyBatis 的if标签在这里特别好用二是排序规则不要拼死在 SQL 里否则每一个排序方式都要写一遍 SQL。我是传到 SQL 后用${sortRule}动态替换排序字段的但这里有一个 SQL 注入隐患所以我在后端做了白名单校验只允许传id, price, create_time这三个字段参与排序。购物车接口是典型的“增删改查”四件套但有一个业务细节我在这里提醒一下。加购接口需要做的是“如果商品已经在购物车里就数量加一如果不在就新增一条记录”而不是无脑 insert。我在 mapper 里写了这么一句 SQLINSERT INTO cart_item (user_id, product_id, quantity, checked) VALUES (#{userId}, #{productId}, 1, 1) ON DUPLICATE KEY UPDATE quantity quantity 1;配合user_id和product_id的唯一索引这个语法能同时处理“插入”和“更新”两种情况一条 SQL 解决数据库层的幂等问题。这也是平时写代码很难想到的一个优化点属于“只有在实际项目里踩过坑才会知道”的知识。3.4 下单流程的事务与库存扣减下单是整个商城最复杂、最容易出问题的环节。一次下单操作需要做的事情有校验用户、查询商品价格、校验库存、生成订单主表记录、批量生成订单明细、扣减库存、清空购物车对应商品。这里面任何一步失败都不能留下半成品数据所以必须用事务包起来。我在 Service 方法上加Transactional注解这个注解会让 Spring 在方法进入时开启数据库事务方法正常结束才提交中途任何一个异常都整体回滚保证数据一致性。这是 Java Web 事务最基础的写法。Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, Long addressId, ListCartItemDTO items) { // 1. 校验地址存在 // 2. 计算订单总金额 // 3. 插入 orders 主表 // 4. 循环插入 order_item 明细 // 5. 更新商品库存 // 6. 删除购物车中已下单的商品 // 返回订单号 }库存扣减是这里最容易出 bug 的地方。初学者最容易写出的问题是先SELECT stock FROM product拿到库存在 Java 里判断够不够够了再UPDATE product SET stock stock - 1。这个逻辑在单用户测试下没问题但并发场景下会超卖——两个用户同时查到库存剩 1都判断“够”都去更新结果库存变成 -1 或者最后一个下单的人没货可发。毕设项目的并发量一般不高但代码里仍然应该体现正确的意识。我用的是最稳妥的方案把判断和扣减合并为一条 SQLUPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这条 SQL 返回的受影响行数如果大于 0说明扣减成功等于 0 说明库存不足直接抛出“库存不足”异常触发事务回滚。原子性由数据库自己保证不用加锁也不会有超卖问题这个写法哪怕放到生产环境也是站得住的。订单号生成也是一个可以讲的点。订单主表里我用order_no做业务编号规则是“时间戳 随机数”防止重复比如20250605135120 4位随机数。不要直接用自增 ID 当订单号暴露给用户一方面会暴露平台的订单量另一方面也容易被人遍历接口恶意查询。简单规则同样能说明你在设计层面思考过细节。3.5 接口文档与 IDEA 插件实践接口文档是这个项目交付三件套里最容易被忽略、但其实最体现工程素养的一部分。我采用的方案是 Swagger 注解 自动生成在 controller 方法上标注ApiOperation和ApiImplicitParams启动项目后访问http://localhost:8080/doc.html就能看到所有接口的在线文档。ApiOperation(分页查询商品列表) ApiImplicitParams({ ApiImplicitParam(name categoryId, value 分类ID, dataType Long, paramType query), ApiImplicitParam(name keyword, value 搜索关键词, dataType String, paramType query), ApiImplicitParam(name pageNum, value 页码, defaultValue 1, dataType int, paramType query) }) GetMapping(/product/list) public ResultPageResultProductVO list(ProductQueryDTO query) { return Result.success(productService.pageQuery(query)); }接口文档最大的价值是让前端同学或者答辩时的评审在看不到代码的情况下依然能清楚地知道每个接口的请求地址、请求参数、返回结构。写接口文档这件事跟写代码一样重要因为它决定了你的系统能不能被别人快速接手。很多商业项目里接口文档混乱导致的返工和沟通成本远比写文档本身多得多。4. Vue 前端开发与接口对接页面做得好看还不乱前端我用了 Vue 2 Element UI 组合。Element UI 的表格、表单、分页、弹窗组件开箱即用对毕设项目来说是提升开发效率的最大功臣。虽然现在 Vue 3 已经是主流但 Vue 2 Element UI 的资料量和稳定性对新手更友好而且网上的毕设项目模板大多也是这套遇到问题提问也更容易被回答。4.1 前端目录结构与路由划分前端和后端一样需要清晰的组织结构。我的 src 目录长这样src ├── api // 接口请求的统一封装 ├── assets // 静态资源图片、样式 ├── components // 公共组件商品卡片、分页组件等 ├── router // 路由配置 ├── store // Vuex状态管理 ├── views // 页面级组件 ├── utils // axios请求封装、工具函数 └── main.js // 入口文件路由配置上我用了嵌套路由和动态路由两个知识点。商城前台的页面结构是Home 首页、Category 分类页、ProductDetail 商品详情页、Cart 购物车页、Order 订单页、Mine 个人中心。后台管理的页面结构是Dashboard 控制台、ProductManage 商品管理、OrderManage 订单管理、UserManage 用户管理。前后台其实可以共用一个路由配置文件但我选择分成两套独立的路由树然后用一个layout组件做区分。前台/下面挂商城页面后台/admin下面挂管理页面两边的侧边栏、顶栏、导航方式完全不同用不同的 layout 组件承载更干净。const routes [ { path: /, component: Layout, children: [ { path: , component: Home }, { path: category/:id, component: Category }, { path: product/:id, component: ProductDetail }, { path: cart, component: Cart }, { path: order, component: OrderList }, { path: mine, component: Mine } ] }, { path: /admin, component: AdminLayout, redirect: /admin/dashboard, children: [ { path: dashboard, component: Dashboard }, { path: product, component: ProductManage }, { path: order, component: OrderManage }, { path: user, component: UserManage } ] } ]路由守卫是这里必须做的一个功能。用户中心的订单页、购物车必须登录才能访问管理员后台更是如此。我在router.beforeEach里检查用户 token 是否存在如果没有就跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path.startsWith(/admin) !token) { next(/login) } else if (to.path /cart !token) { next(/login) } else { next() } })这个守卫逻辑很简单但很多毕设项目直接忽略导致用户点击“购物车”按钮时后端返回 401前端页面就崩了一个空白。路由层面把门堵住体验会好很多。如果你需要更细的权限控制可以像后台管理一样再配合菜单权限做但毕设阶段守好“登录才能进”这一道关就够了。4.2 axios 封装与 token 注入实战axios 的封装是整个前后端联调的枢纽。我统一做了三件事baseURL指向后端的服务地址、请求拦截器里自动加上Authorization请求头、响应拦截器里统一处理后端返回的code和 HTTP 状态码。// utils/request.js import axios from axios const request axios.create({ baseURL: http://localhost:8080, timeout: 10000 }) // 请求拦截器注入token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理业务码 request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { localStorage.removeItem(token) window.location.href /login return Promise.reject(new Error(登录已过期)) } else { return Promise.reject(new Error(res.message)) } }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } )封装完成后每个页面请求后端数据就非常优雅了。以商品列表页为例在api/product.js里定义一个函数页面里调用并渲染根本不需要在每个组件里重复处理错误提示、token 注入和 loading 控制// api/product.js import request from ../utils/request export function getProductList(params) { return request.get(/product/list, { params }) } export function getProductDetail(id) { return request.get(/product/detail/ id) }我强烈建议所有请求函数统一放到 src/api 目录下集中管理。早期我把请求直接写在页面组件里后来接口一多页面文件又长又乱想找一个接口只能满屏 CtrlF。集中管理之后接口路径一目了然以后改后端地址或者新增接口只需要动这一个目录。4.3 购物车和订单页前端交互最难啃的两块骨头购物车是前端交互逻辑最复杂的页面。它需要处理选中状态、合计金额、数量加减、删除商品、全选全不选、提交订单等多个状态。我用了 Vuex 管理购物车数据页面里的每个操作都通过dispatch提交 mutation 或者 action保证数据的单向流动。这里踩过最大的坑是购物车选中状态的数据源不一致。我的做法是购物车列表每一条数据包含一个checked布尔字段后端返回时默认全true。页面上勾选时先更新 Vuex 中的数据再调用一个计算函数重新计算“已选商品总价”。这个总价必须是实时计算的不能手动维护一个变量否则很容易出现“勾选了一件总价没变”的诡异 bug。订单提交页面相对简单一些主要是确认收货地址、显示商品清单、展示总金额然后点击提交调用/order/create接口。这个接口传的数据结构是addressId加上一个[{ productId, quantity }]的数组。前端在提交前要把购物车中选中的商品筛选出来组成这个数组。一个实用小技巧是提交订单成功后直接调用清空购物车接口而不是跳转后前端手动去删本地数据——把数据变更放到后端统一处理前端只做同步可以避免两边状态不一致。4.4 前端环境配置与打包发布的细节Vue 项目开发时必须设置跨域代理。我在vue.config.js里配置了 devServer 的 proxy所有/api开头的请求都转发到http://localhost:8080module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这个配置不是可有可无的。如果不做代理前后端端口不同浏览器会直接拦截请求跨域页面会白屏报错。做了代理后开发环境前端的请求相对路径是/api/product/list看起来像请求自己的同源服务其实底层由 webpack-dev-server 转发到后端完美规避跨域。如果你是直接把 vue.config.js 从别人的项目里拷贝过来一定要检查 proxy 里的 target 是否指向了你本机的后端端口以及 baseURL 是否匹配。打包的时候我执行npm run build产物是在dist目录下。Vue 项目默认打包出来的资源路径是绝对路径/js/app.js如果部署在服务器的根路径没问题但如果放在子目录里就全 404 了。解决办法是在vue.config.js里设置publicPath: ./让资源路径变成相对路径。这个细节等你真上服务器部署的时候就会发现能救你一命。5. 毕设开发避坑实录踩过的坑和排查方法这部分是我觉得最有价值的部分。代码能跑通只是底线真正让你在答辩时说得头头是道的是你对“为什么这么写”和“遇到问题怎么解决”的理解。我把自己在这个项目中实际踩过的坑整理成清单全部都是真实经历。5.1 前后端联调的第一个坑跨域问题跨域是前后端分离项目绕不开的第一道坎。现象是前端启动后浏览器控制台报错Access to XMLHttpRequest at http://localhost:8080/... from origin http://localhost:3000 has been blocked by CORS policy。这个问题的本质是浏览器同源策略只要协议、域名、端口三者有一个不同浏览器就会阻止页面脚本读取返回数据。前端的localhost:3000和后端的localhost:8080端口不同就算都跑在本地也一样跨域。解决跨域有两个方案我两个都用过。第一个方案是在后端加 CORS 配置这是最治本的做法因为接口是被前端消费的后端允许谁来访问是它的事。我写了一个全局配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }第二个方案是前端的 webpack 代理就是上面 4.4 节提到的 proxy 配置。面试或答辩都会被问“跨域怎么解决”这两个方案都要能说清楚并且能区分后端 CORS 是真实的环境配置前端代理只是开发期的便利手段生产环境的反向代理是 Nginx 负责的。5.2 图片上传与显示的连环坑服装商城没了图片展示效果减半所以图片上传功能我前后折腾了很久。第一个坑就是上传后图片路径错乱前端访问http://localhost:8080/upload/xxx.jpg报 404。原因在于 SpringBoot 默认的静态资源目录是classpath:/static/而我的图片是上传到本地磁盘的D:/uploads/目录不在 SpringBoot 的静态资源扫描范围内。解决方案是自定义一个静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将 /upload/** 请求映射到本地磁盘目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/uploads/); } }图片显示的第二个坑是浏览器缓存。修改图片后前端页面还是显示旧图这是因为浏览器对同名图片有缓存机制。解决办法很土但有效给图片 URL 加一个时间戳后缀src/upload/xxx.jpg?t Date.now()强制浏览器重新请求。第三个坑是图片大小和类型没限制。如果直接让人传任意文件可能上传了巨大的图片把磁盘塞满或者传个 .exe 文件上去引发安全问题。我在 controller 层对文件扩展名只允许 .jpg、.png、.gif和文件大小限制不超过 5MB做了校验超出就直接返回错误提示。这不仅是安全考虑也是商城系统的规范性要求。5.3 中文乱码、JWT 过期与前端白屏中文乱码这个问题通常是字符集不统一导致的。判断路径要从前端到数据库走一遍前端页面meta charsetutf-8和 axios 请求体编码、后端 SpringBoot 的响应编码、数据库表结构的 utf8mb4 字符集、JDBC 连接串里的characterEncodingutf8任何一个环节不对都会出问题。我排查这个问题的顺序是从数据库往反方向走先看数据库存进去是不是好的如果好的说明问题出在传输层如果库里就是乱的那问题在连接配置或后端编码。JWT 过期的问题在项目里表现很隐蔽。用户登录后逛了几个小时回来再点购物车页面直接跳回登录页但用户以为是 bug。我在响应拦截器里对code 401或 HTTP 状态码 401 做了统一处理提示“登录已过期请重新登录”然后才跳转。这样体验好很多。同时我把 token 的有效期设置成了 24 小时不至于在正常使用中频繁过期也让答辩时“登录态安全策略”这个点有得讲。前端白屏的排查思路是所有新手都必须掌握的。我遇到过的白屏原因有三类你们可以照单排查一是npm run dev启动后访问的 URL 端口和配置里的port不一致二是某个组件里引用了不存在的依赖或者路由路径写错命令行会报红色错误看控制台三是 Vue 组件里方法名和模板引用不一致运行时报is not defined页面渲染到某一步卡住变成空白。5.4 常见问题速查表问题现象可能原因排查思路与解决办法数据库连接失败MySQL 未启动或账号密码错误检查application.yml的url/username/password用 Navicat 先手动连一次中文乱码字符集不统一数据库指定 utf8mb4JDBC 连接串加characterEncodingutf8HTML/axios 都走 UTF-8前端跨域报错端口不同未做代理或 CORS开发期配 webpack proxy生产期后端加 CORS 或 Nginx 反向代理图片 404上传目录不在静态资源映射范围内在 WebMvcConfig 里将/upload/**映射到本地磁盘目录首次登录失效324 小时过期看配置检查 JWT 有效期设置合理过期时间前端 401 统一跳转打包后资源 404publicPath 是绝对路径在 vue.config.js 设置publicPath: ./购物车刷新丢失数据只存了 localStorage购物车数据存后端表前端登录后拉取下单后库存没减库存更新未包含在事务里或 SQL 逻辑错误用UPDATE product SET stockstock-#{qty} WHERE id#{id} AND stock#{qty}6. 开发完成后的自我反思与扩展建议这个项目做完我自己有一个特别深的体会毕设项目不是一个“做完就完”的任务而是一块可以反复打磨的基石。很多同学做完一个项目就丢在 GitHub 上再也不看了但如果你把这个项目里每个模块的设计原因、每个接口的数据流向、每个坑的解决办法都吃透它完全可以变成你技术面试时的核心谈资。面试官问“你做过哪些项目”你能把购物车幂等、订单事务、库存原子扣减、JWT 拦截器这些细节讲明白比背十道八股文管用得多。最后再分享一个小建议。做完这个网上服装商城后可以自己尝试给它加一个基于 Redis 的商品缓存热点商品数据从数据库查一次后放进 Redis后面请求直接走缓存商品更新时主动删除缓存让下一次查询重新加载。这个改造做下来你对缓存的穿透、击穿、雪崩这些概念的认知就不再停留在背定义而是真正知道它们在什么场景下会发生、代码里怎么防。这个项目后续的扩展空间非常大值得慢慢往深挖。