Vue3+SpringBoot二手商城实战:前后端分离工程落地指南 📅 发布时间:2026/9/18 8:34:09 👁 浏览次数: 简介本资源是一份面向计算机专业本科生的毕业设计论文文档聚焦大学生二手电子产品交易平台的系统化设计与实现适用于Java Web开发、前后端分离实践及毕业论文写作参考。全文基于VueSpringBoot技术栈展开涵盖平台需求分析、系统架构设计、前后端功能模块实现、数据挖掘在交易优化中的应用以及信息管理系统在交易监控与报表生成中的实际作用内容兼具理论深度与工程落地性。资源为单个3.35MB的Word文档.docx完整包含摘要、中英文关键词、目录、绪论、相关技术介绍、系统设计与实现、总结与展望等标准论文结构附有详细技术选型依据与对比分析。目前已有193人学习下载可直接用于毕业答辩材料准备、课程设计复盘或VueSpringBoot全栈开发学习参考。1. 这不是又一个“毕设Demo”VueSpringBoot二手电子商城的真实工程切口很多同学拿到“大学生二手电子产品商城”这个题目第一反应是套个若依、RuoYi-Vue或SpringBoot脚手架填几个CRUD接口、改几处页面样式交差了事。但真正拆过这份毕业设计文档的人会发现它在技术选型上埋了一个关键伏笔——前端用 Vue非Vue2旧模板后端用 SpringBoot非SSM老架构数据库用 MySQL非H2内存库且明确要求“B/S架构”“MVC三层分离”“管理员/用户双角色权限控制”。这不是在堆砌技术名词而是在模拟一个真实轻量级电商系统的最小可行闭环商品发布→浏览搜索→下单支付→订单管理→后台审核。尤其值得注意的是文档中反复强调“实用性”“易用性”“结构清晰”说明它刻意规避了微服务、分布式事务、高并发秒杀等超纲内容把焦点收束在单体应用内可落地的前后端分离实践上。对刚走出课堂的开发者而言它是一份极佳的“过渡型项目”既不会因过度简化失去工程感又不会因过度复杂陷入理论空转。你不需要部署K8s集群但必须搞懂RequestBody怎么接收Vue发来的JSON、vue-router如何与SpringBoot静态资源路径协同、MySQL的datetime字段如何被MyBatis-Plus自动映射为JavaLocalDateTime——这些才是校招面试官真正在意的“能跑通的细节”。2. Vue前端工程搭建从环境配置到路由守卫的实战闭环2.1 Vue 3 Vite 环境初始化与依赖安装毕业论文虽未指定Vue版本但结合“Vue-SpringBoot解决方案”的表述及当前主流实践应采用 Vue 3Composition API配合 Vite 构建工具。Vite 的冷启动速度和热更新效率远超传统 Webpack对开发体验提升显著。执行以下命令完成初始化# 创建项目使用npm npm create vuelatest # 按提示选择✔ Add TypeScript? ... Yes # ✔ Add JSX Support? ... No # ✔ Add Vue Router for Single Page Application development? ... Yes # ✔ Add Pinia for state management? ... Yes # ✔ Add Vitest for Unit testing? ... No毕设阶段可暂略 # ✔ Add Cypress for both Unit and End-to-End testing? ... No # ✔ Add ESLint for code quality? ... Yes # ✔ Add Prettier for code formatting? ... Yes提示create vuelatest会自动拉取 Vue 官方推荐的最新脚手架避免手动配置vue-cli的兼容性问题。若网络受限可先npm config set registry https://registry.npmmirror.com切换国内镜像源。安装完成后进入项目目录并安装核心业务依赖cd vue-springboot-campus-market npm install axios1.6.7 element-plus2.7.6 vueuse/core10.9.0axios1.6.7稳定版HTTP客户端支持拦截器统一处理Tokenelement-plus2.7.6成熟UI组件库提供el-table、el-form等电商后台必需组件vueuse/core提供useStorage本地缓存用户Token、useDebounceFn防抖搜索等实用组合式函数。2.2 基于角色的路由守卫与权限控制实现系统存在“管理员”与“普通用户”两类角色需在前端层面拦截非法访问。Vue Router 4 提供router.beforeEach全局前置守卫结合Pinia状态管理实现动态权限校验// src/router/index.ts import { createRouter, createWebHistory } from vue-router import { useUserStore } from /stores/user const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /login, name: Login, component: () import(/views/Login.vue), meta: { requiresAuth: false } // 显式标记无需登录 }, { path: /admin, name: AdminDashboard, component: () import(/views/admin/Dashboard.vue), meta: { requiresAuth: true, role: admin } }, { path: /user/profile, name: UserProfile, component: () import(/views/user/Profile.vue), meta: { requiresAuth: true, role: user } } ] }) // 全局路由守卫 router.beforeEach(async (to, from, next) { const userStore useUserStore() // 若目标路由需要认证 if (to.meta.requiresAuth) { // 尝试从localStorage恢复用户状态 if (!userStore.token) { try { await userStore.fetchUserInfo() // 调用API获取用户信息并设置token } catch (error) { next(/login) // 获取失败跳转登录页 return } } // 角色校验管理员只能访问admin路由用户只能访问user路由 if (to.meta.role userStore.role ! to.meta.role) { next(userStore.role admin ? /admin : /user/profile) return } } next() // 放行 }) export default router参数说明meta.requiresAuth控制是否需要登录meta.role指定该路由允许的角色类型。userStore.fetchUserInfo()内部调用/api/user/info接口返回{ id, username, role, token }并将token存入localStorage供后续请求携带。此设计避免了每次刷新页面都需重新登录符合“实用性”要求。2.3 商品列表页的响应式布局与搜索过滤逻辑二手商品列表页是核心交互场景需支持关键词搜索、分类筛选、价格区间过滤。Vue 3 的ref与computed可高效实现数据驱动!-- src/views/user/MarketList.vue -- template div classmarket-list !-- 搜索栏 -- el-input v-modelsearchKeyword placeholder输入商品名称、品牌搜索... inputdebouncedSearch clearable / !-- 分类筛选下拉框 -- el-select v-modelselectedCategory placeholder全部分类 changefilterByCategory el-option v-forcat in categories :keycat.id :labelcat.name :valuecat.id / /el-select !-- 价格区间滑块 -- el-slider v-modelpriceRange range :min0 :max5000 changefilterByPrice / !-- 商品卡片列表 -- div classgoods-grid el-card v-foritem in filteredGoods :keyitem.id classgoods-card img :srcitem.picture alt商品图片 classgoods-img / div classgoods-info h3{{ item.name }}/h3 p classprice¥{{ item.price }}/p p classbrand{{ item.brand }} | {{ item.condition }}成新/p el-button typeprimary sizesmall clickgoToDetail(item.id) 查看详情 /el-button /div /el-card /div /div /template script setup langts import { ref, computed, onMounted } from vue import { useDebounceFn } from vueuse/core import { getGoodsList } from /api/goods // 响应式数据 const searchKeyword ref() const selectedCategory refnumber | null(null) const priceRange ref[number, number]([0, 5000]) const allGoods refany[]([]) const categories ref{id: number, name: string}[]([]) // 计算属性过滤后的商品列表 const filteredGoods computed(() { return allGoods.value.filter(item { const keywordMatch item.name.includes(searchKeyword.value) || item.brand.includes(searchKeyword.value) const categoryMatch !selectedCategory.value || item.categoryId selectedCategory.value const priceMatch item.price priceRange.value[0] item.price priceRange.value[1] return keywordMatch categoryMatch priceMatch }) }) // 防抖搜索避免频繁请求 const debouncedSearch useDebounceFn(() { // 实际项目中此处应调用API毕设可先用本地数据模拟 }, 300) // 初始化加载商品数据 onMounted(async () { try { const res await getGoodsList() allGoods.value res.data // categories 数据可从 /api/category/list 接口获取 } catch (error) { console.error(加载商品失败:, error) } }) /script逻辑说明filteredGoods是一个计算属性实时响应searchKeyword、selectedCategory、priceRange的变化无需手动触发filter方法。useDebounceFn将搜索输入延迟300ms执行防止用户连续敲击时触发多次无效请求。getGoodsList()应封装为 Axios 请求URL 指向 SpringBoot 后端/api/goods/list接口返回 JSON 格式商品数组。3. SpringBoot后端开发从RESTful接口设计到MyBatis-Plus分页查询3.1 RESTful风格接口规范与Controller层实现毕业论文强调“B/S架构”与“MVC三层分离”后端需严格遵循 RESTful 设计原则资源路径用名词/api/goods、动作用HTTP方法GET/POST/PUT/DELETE、状态码语义化200成功、401未授权、404不存在。以商品管理为例Controller 层代码如下// src/main/java/com/example/market/controller/GoodsController.java RestController RequestMapping(/api/goods) RequiredArgsConstructor public class GoodsController { private final GoodsService goodsService; /** * GET /api/goods?page1size10keyword手机categoryId2 * 分页查询商品列表支持关键词、分类ID过滤 */ GetMapping public ResultPageGoods list( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { PageGoods result goodsService.listWithFilter(page, size, keyword, categoryId); return Result.success(result); } /** * POST /api/goods * 用户发布二手商品需JWT Token校验 */ PostMapping PreAuthorize(hasRole(USER)) public ResultString publish(RequestBody Valid Goods goods, Authentication authentication) { Long userId ((JwtAuthenticationToken) authentication).getTokenAttributes() .get(userId, Long.class); goods.setUserId(userId); goods.setStatus(GoodsStatus.PENDING); // 待审核状态 goodsService.save(goods); return Result.success(发布成功等待管理员审核); } /** * GET /api/goods/{id} * 查询单个商品详情含关联的评论 */ GetMapping(/{id}) public ResultGoodsDetailVO detail(PathVariable Long id) { GoodsDetailVO vo goodsService.getDetailById(id); if (vo null) { return Result.fail(商品不存在); } return Result.success(vo); } }参数说明RequestParam绑定查询参数PathVariable绑定路径变量RequestBody接收JSON请求体。PreAuthorize(hasRole(USER))是Spring Security注解确保只有角色为USER的用户才能调用发布接口。ResultT是自定义统一封装类包含code、msg、data字段避免前端重复解析状态。3.2 MyBatis-Plus分页插件配置与多条件动态查询MyBatis-Plus 的Page对象与QueryWrapper是实现分页与动态查询的核心。需在 SpringBoot 配置类中启用分页插件// src/main/java/com/example/market/config/MybatisPlusConfig.java Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }GoodsService的listWithFilter方法使用QueryWrapper构建动态SQL// src/main/java/com/example/market/service/impl/GoodsServiceImpl.java Service RequiredArgsConstructor public class GoodsServiceImpl extends ServiceImplGoodsMapper, Goods implements GoodsService { private final GoodsMapper goodsMapper; Override public PageGoods listWithFilter(Integer page, Integer size, String keyword, Long categoryId) { PageGoods pageObj new Page(page, size); QueryWrapperGoods wrapper new QueryWrapper(); // 动态添加WHERE条件 if (StringUtils.isNotBlank(keyword)) { wrapper.like(name, keyword).or().like(brand, keyword); } if (categoryId ! null categoryId 0) { wrapper.eq(category_id, categoryId); } // 状态为上架非删除、非下架 wrapper.eq(status, GoodsStatus.ONLINE.getCode()); return goodsMapper.selectPage(pageObj, wrapper); } }逻辑说明QueryWrapper会根据keyword和categoryId是否为空智能拼接WHERE子句。例如当keywordiPhone且categoryId2时生成的SQL为SELECT * FROM goods WHERE (name LIKE %iPhone% OR brand LIKE %iPhone%) AND category_id 2 AND status 1 LIMIT 0,10。selectPage方法自动注入LIMIT和COUNT(*)无需手动写分页SQL。3.3 JWT Token认证与用户权限拦截器毕业论文要求“管理员/用户双角色”需通过 JWT 实现无状态认证。Spring Security 配置如下// src/main/java/com/example/market/config/SecurityConfig.java Configuration EnableWebSecurity EnableMethodSecurity // 启用PreAuthorize注解 public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) // 毕设可关闭CSRF前后端分离场景 .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz - authz .requestMatchers(/api/login, /api/register, /api/public/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .requestMatchers(/api/user/**).hasAnyRole(USER, ADMIN) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } }JwtAuthenticationFilter从AuthorizationHeader 中提取Token解析出用户ID和角色并存入SecurityContext// src/main/java/com/example/market/filter/JwtAuthenticationFilter.java public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) { this.tokenProvider tokenProvider; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null tokenProvider.validateToken(token)) { Authentication auth tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }关键点JwtTokenProvider需实现generateToken()登录成功时生成、validateToken()校验签名与过期时间、getAuthentication()解析Token载荷创建UsernamePasswordAuthenticationToken。载荷中必须包含userId和role字段供PreAuthorize注解读取。4. 数据库设计与MyBatis-Plus实体映射从E-R图到Java对象4.1 核心数据表结构解析与字段设计依据毕业论文附录中的 E-R 图与数据表定义是数据库设计的直接依据。以goods二手商品表为例其字段设计需兼顾业务需求与查询效率字段名类型长度说明设计依据idBIGINT-主键自增所有表通用主键nameVARCHAR200商品名称用户搜索核心字段需索引priceDECIMAL(10,2)售价金融类数据用DECIMAL避免浮点误差category_idBIGINT-外键关联category表支持分类筛选需建立索引user_idBIGINT-发布者ID外键关联用户表记录归属关系brandVARCHAR100品牌搜索高频字段如“苹果”、“华为”conditionTINYINT-新旧程度1-5枚举值节省存储空间pictureLONGTEXT-图片URLJSON数组毕设阶段存URL而非二进制降低DB压力statusTINYINT-状态0-待审核1-上架2-下架3-已售支持后台审核流避免硬删除注意picture字段存储多个图片URL格式为[https://xxx/1.jpg,https://xxx/2.jpg]前端解析为数组渲染轮播图。status字段用TINYINT而非VARCHAR便于SQL条件判断WHERE status 1比WHERE status ONLINE效率更高。4.2 MyBatis-Plus实体类与TableField注解详解MyBatis-Plus 通过TableName和TableId注解将Java类与数据库表映射。针对goods表实体类定义如下// src/main/java/com/example/market/entity/Goods.java import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(goods) public class Goods { TableId(type IdType.ASSIGN_ID) // 使用雪花算法生成分布式ID private Long id; private String name; private BigDecimal price; TableField(category_id) private Long categoryId; TableField(user_id) private Long userId; private String brand; TableField(condition) private Integer condition; // 1-5成新 TableField(picture) private String picture; // JSON字符串 TableField(status) private Integer status; // 对应GoodsStatus枚举 TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; }参数说明TableId(type IdType.ASSIGN_ID)指定主键策略为雪花算法生成全局唯一Long型ID避免数据库自增ID在分库分表时的冲突风险TableField(category_id)明确指定数据库字段名解决Java驼峰命名与数据库下划线命名的映射问题fill FieldFill.INSERT表示createTime字段在插入时由MyBatis-Plus自动设置为当前时间无需在Controller中手动赋值。4.3 多表关联查询商品详情与评论的VO封装商品详情页需同时展示商品信息与用户评论需进行goods与goods_comment表的关联查询。MyBatis-Plus 不推荐在Select中写复杂JOIN而是采用TableField(exist false) 服务层组装的方式// src/main/java/com/example/market/vo/GoodsDetailVO.java import lombok.Data; import java.util.List; Data public class GoodsDetailVO { private Long id; private String name; private BigDecimal price; private String brand; private Integer condition; private String picture; private String description; // 商品描述 // 关联的评论列表 private ListCommentVO comments; } // src/main/java/com/example/market/vo/CommentVO.java Data public class CommentVO { private Long id; private String nickname; // 评论者昵称 private String avatarUrl; // 头像URL private String content; // 评论内容 private LocalDateTime createTime; }GoodsService.getDetailById()方法通过两次查询组装VOOverride public GoodsDetailVO getDetailById(Long id) { // 1. 查询商品基本信息 Goods goods this.getById(id); if (goods null) return null; GoodsDetailVO vo BeanUtil.copyProperties(goods, GoodsDetailVO.class); // 2. 查询关联评论按时间倒序 QueryWrapperGoodsComment commentWrapper new QueryWrapper(); commentWrapper.eq(goods_id, id).orderByDesc(create_time); ListGoodsComment comments goodsCommentService.list(commentWrapper); // 3. 转换为VO列表 vo.setComments(comments.stream() .map(c - { CommentVO cv new CommentVO(); cv.setId(c.getId()); cv.setNickname(c.getNickname()); cv.setAvatarUrl(c.getAvatarUrl()); cv.setContent(c.getContent()); cv.setCreateTime(c.getCreateTime()); return cv; }) .collect(Collectors.toList())); return vo; }优势相比单次JOIN查询此方式更易维护、调试和扩展。若未来需增加“点赞数”统计只需在CommentVO中添加likeCount字段并在Stream中调用commentLikeService.countByCommentId(c.getId())即可无需修改SQL。5. 前后端联调与常见问题排错从CORS到跨域Cookie的实战方案5.1 开发环境跨域问题的三种解决路径Vue 开发服务器http://localhost:5173与 SpringBoot 后端http://localhost:8080端口不同必然触发浏览器CORS跨域资源共享限制。毕业设计中需选择一种可靠方案方案一Vue代理开发阶段首选在vite.config.ts中配置代理将/api前缀请求转发至后端// vite.config.ts export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, // 修改请求头中的host为target rewrite: (path) path.replace(/^\/api/, ) // 去掉/api前缀 } } } })此时前端请求axios.get(/api/goods)会被代理到http://localhost:8080/goods浏览器认为是同源请求彻底规避CORS。方案二SpringBoot全局CORS配置测试/演示阶段若需独立运行前端可在SpringBoot中开启CORS// src/main/java/com/example/market/config/WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) // 允许的前端地址 .allowCredentials(true) // 允许携带Cookie/Token .maxAge(3600) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS); } }注意allowCredentials(true)必须配合allowedOrigins指定具体域名不能为*否则浏览器会拒绝响应。方案三Nginx反向代理生产部署标准做法将Vue打包产物dist目录与SpringBoot JAR包部署在同一台服务器用Nginx统一入口# nginx.conf server { listen 80; server_name campus-market.example.com; location / { root /var/www/vue-dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; # 转发到SpringBoot proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }此时所有请求均走http://campus-market.example.com彻底消除跨域。5.2 登录状态丢失的典型原因与修复步骤学生在联调时常遇到“登录成功但刷新页面后变回未登录状态”根本原因在于Token未正确持久化或请求未携带。排查步骤如下检查前端Token存储位置登录成功后确认localStorage.setItem(token, response.data.token)是否执行。打开浏览器开发者工具 → Application → Local Storage查看token键值是否存在。验证Axios请求拦截器是否注入Token在src/utils/request.ts中检查拦截器// 请求拦截器添加Authorization头 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} // 注意Bearer空格 } return config })后端JWT解析是否匹配Header格式JwtAuthenticationFilter.resolveToken()方法中request.getHeader(Authorization)返回值应为Bearer eyJhbGciOi...。若前端传的是Authorization: eyJhbGciOi...缺少Bearer前缀则解析失败。可通过Postman测试GET http://localhost:8080/api/user/infoHeaders中添加Authorization: Bearer your-token。检查Spring Security是否放行OPTIONS预检请求若控制台出现403 Forbidden且请求Method为OPTIONS说明CORS预检被拦截。在SecurityConfig的authorizeHttpRequests中确保/api/**路径被permitAll()或authenticated()正确覆盖且addFilterBefore的顺序无误。5.3 MySQL中文乱码与日期格式化问题速查表问题现象根本原因解决方案插入中文显示为???MySQL服务端字符集非utf8mb4修改my.cnf[client]default-character-set utf8mb4[mysqld]character-set-server utf8mb4collation-server utf8mb4_unicode_ciLocalDateTime返回JSON为{}空对象Jackson未配置JavaTimeModule在application.yml中添加spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT8MyBatis-Plus插入时间字段为0000-00-00 00:00:00MySQL SQL模式包含NO_ZERO_DATE执行SQLSET GLOBAL sql_mode(SELECT REPLACE(sql_mode,NO_ZERO_DATE,));关键操作修改MySQL配置后必须重启MySQL服务sudo systemctl restart mysqld并重新创建数据库CREATE DATABASE campus_market CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;旧数据库需执行ALTER DATABASE campus_market CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。本文还有配套的精品资源点击获取