ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue+Spring Boot二手商城实战:前后端分离与权限控制

Vue+Spring Boot二手商城实战:前后端分离与权限控制 简介本资源是一份面向计算机专业本科生的毕业设计论文文档聚焦大学生二手电子产品交易平台的系统化设计与实现适用于Java Web开发、前后端分离项目实践及毕业论文参考场景。全文基于VueSpringBoot技术栈展开涵盖平台需求分析、系统架构设计、前后端功能模块实现、数据挖掘应用、信息管理系统集成及安全性优化等核心内容并附有摘要、关键词、目录、绪论、技术选型、系统实现与展望等完整论文结构。资源为单个DOCX文件共1个大小3.35MB格式规范、排版清晰便于直接用于论文撰写或技术方案学习。目前已有193人下载学习读者可获取完整的毕业设计逻辑脉络、可复用的技术实现思路、数据处理与平台优化方法以及针对校园二手交易场景的落地化解决方案。1. 用 Vue Spring Boot 搭建大学生二手电子产品商城不是堆功能而是控流程、保数据、防踩坑一个计算机专业本科生在做毕业设计时常被“商城系统”四个字吓住——以为要重造淘宝。其实真正落地的大学生二手电子商城核心不在高并发或秒杀而在闭环交易流学生发布闲置手机/笔记本带实拍图基础参数同学浏览筛选按品牌、成色、价格区间、发起询价或一口价下单卖家确认后生成简易订单双方线下交付并标记完成。这个过程里Vue 负责让页面响应快、表单校验严、图片上传稳Spring Boot 则要扛住多用户同时发帖、查库存、改状态还要把用户身份、商品状态、订单生命周期管清楚。它适合 Java 基础扎实、已学过 Spring MVC 和 Vue 基础语法的学生——不需要你写分布式事务但必须能看懂Transactional为什么加在 service 层也得会用v-model.lazy防止输入框频繁触发请求。本文不讲“如何从零安装 Node.js”而是聚焦怎么让 Vue 页面和 Spring Boot 后端真正对话起来、怎么避免跨域调试时接口全 404、怎么用最少代码实现“学生只能删自己发布的商品”这类权限逻辑。2. 前后端分离架构下Vue 与 Spring Boot 的通信边界与数据契约必须明确定义2.1 为什么必须前后端分离——避开毕业答辩时最常被问的架构合理性问题很多学生用 Thymeleaf 或 JSP 直接渲染页面看似简单但答辩时老师会追问“如果后期要加微信小程序端你的商品列表接口还能复用吗” 此时若答“要重写 Controller”就暴露了架构短板。而 Vue Spring Boot 的分离模式天然定义了清晰的数据契约前端只关心 JSON 字段名和状态码后端只暴露 RESTful 接口。比如/api/products?statuson_saleminPrice500maxPrice2000返回标准 JSON{ code: 200, message: success, data: { list: [ { id: 1024, title: iPhone 12 128G 黑色 成色95%, price: 2800.00, brand: Apple, condition: 95%, images: [https://oss.example.com/img/1024_1.jpg], publisherId: 2021001, createdAt: 2024-03-15T10:22:33 } ], total: 1 } }提示这个 JSON 结构不是随便定的。code和message是统一响应体ResponseResult 类所有 Controller 方法返回ResponseResultListProductVO而非裸ListProductVO。这样前端 Axios 拦截器才能统一处理登录过期code401或参数错误code400避免每个.then()里重复写if (res.code ! 200) alert(res.message)。2.2 Vue 端用 Axios 封装请求用 Composition API 管理商品列表状态在src/api/product.js中封装商品相关请求强制携带 token从 localStorage 读取// src/api/product.js import axios from axios const apiClient axios.create({ baseURL: http://localhost:8080/api, // Spring Boot 启动端口 timeout: 10000, headers: { Content-Type: application/json } }) // 请求拦截器自动添加 Authorization 头 apiClient.interceptors.request.use(config { const token localStorage.getItem(user_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一错误处理 apiClient.interceptors.response.use( response response.data, // 只返回 data 字段剥离 code/message 包装 error { if (error.response?.status 401) { localStorage.removeItem(user_token) window.location.href /login } return Promise.reject(error) } ) export const productApi { // 获取商品列表支持分页和筛选 list: (params) apiClient.get(/products, { params }), // 发布新商品 create: (data) apiClient.post(/products, data), // 根据 ID 获取商品详情 detail: (id) apiClient.get(/products/${id}) }在src/views/ProductList.vue中使用 Composition API 管理状态!-- src/views/ProductList.vue -- template div classproduct-list h2二手电子产品/h2 div classfilter-bar input v-modelfilters.brand placeholder品牌如华为、小米 / select v-modelfilters.condition option value全部成色/option option value90%90%以上/option option value80%80%-89%/option /select button clickloadProducts搜索/button /div div classproduct-grid ProductCard v-foritem in products :keyitem.id :productitem / /div Pagination :currentpageInfo.current :totalpageInfo.total changehandlePageChange / /div /template script setup import { ref, onMounted } from vue import { productApi } from /api/product import ProductCard from /components/ProductCard.vue import Pagination from /components/Pagination.vue const products ref([]) const pageInfo ref({ current: 1, total: 0 }) const filters ref({ brand: , condition: }) // 加载商品列表 const loadProducts async () { try { const res await productApi.list({ ...filters.value, page: pageInfo.value.current, size: 12 }) products.value res.data.list || [] pageInfo.value.total res.data.total || 0 } catch (err) { console.error(加载商品失败:, err) } } const handlePageChange (page) { pageInfo.value.current page loadProducts() } onMounted(() { loadProducts() }) /script2.2.1 关键参数说明与避坑点参数说明为什么重要常见误用baseURLVue 调用接口的根路径必须与 Spring Boot 的server.port和spring.mvc.servlet.path一致若设为http://localhost:8080而后端实际跑在8081所有请求 404写死8080却没改application.yml中的server.portAuthorization头Bearer Token 认证Spring Security 依赖此头识别用户没加此头后端PreAuthorize(hasRole(STUDENT))直接拒掉所有请求在main.js全局设置axios.defaults.headers.common[Authorization]但登录后 token 变化未同步params对象传参GET 请求参数自动拼到 URL如{ brand: Apple } → ?brandApple避免手动拼 URL 字符串防止特殊字符空格、编码错误用JSON.stringify()把对象转字符串再传导致后端接收为{brand:Apple}字符串而非对象2.3 Spring Boot 端用 RESTController Lombok MyBatis-Plus 快速构建可验证接口在pom.xml中确保关键依赖版本兼容以 Spring Boot 2.7.18 为例避免springboot版本太高导致 MyBatis-Plus 不兼容dependencies !-- Web 核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus比原生 MyBatis 少写 70% XML -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version !-- 注意3.5.x 适配 Spring Boot 2.x -- /dependency !-- Lombok省去 getter/setter -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies定义商品实体类Lombok 自动注入 getter/setter/toString// src/main/java/com/example/ecommerce/entity/Product.java import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(t_product) public class Product { TableId(type IdType.AUTO) private Long id; private String title; // 商品标题 private BigDecimal price; // 价格 private String brand; // 品牌索引字段 private String condition; // 成色如 95% private String images; // JSON 字符串如 [url1,url2] private Long publisherId; // 发布者学号关联 user 表 private Integer status; // 0-草稿 1-上架 2-已售出 private LocalDateTime createdAt; }编写 Controller严格遵循 RESTful 规范并用Valid校验入参// src/main/java/com/example/ecommerce/controller/ProductController.java import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.ecommerce.entity.Product; import com.example.ecommerce.entity.ResponseResult; import com.example.ecommerce.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; RestController RequestMapping(/api/products) public class ProductController { Autowired private ProductService productService; // GET /api/products?page1size12brandApplecondition95% GetMapping public ResponseResultPageProduct list( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 12) Integer size, String brand, String condition) { PageProduct productPage new Page(page, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(brand ! null !brand.trim().isEmpty(), Product::getBrand, brand) .eq(condition ! null !condition.trim().isEmpty(), Product::getCondition, condition) .eq(Product::getStatus, 1); // 只查上架中商品 PageProduct result productService.page(productPage, wrapper); return ResponseResult.success(result); } // POST /api/products PostMapping public ResponseResultProduct create(Valid RequestBody Product product) { // 设置默认值 product.setStatus(1); // 新发布即上架 product.setCreatedAt(LocalDateTime.now()); boolean saved productService.save(product); return saved ? ResponseResult.success(product) : ResponseResult.fail(保存失败); } // GET /api/products/{id} GetMapping(/{id}) public ResponseResultProduct detail(PathVariable Long id) { Product product productService.getById(id); return product ! null ? ResponseResult.success(product) : ResponseResult.fail(商品不存在); } }2.3.1 为什么用 MyBatis-Plus 而非 JPA学习成本低productService.save(product)直接插入不用写EntityTableColumn一堆注解SQL 可控复杂查询仍可用LambdaQueryWrapper构建比 JPA 的Query注解更直观毕业答辩友好老师问“你怎么查某个品牌的商品”你可以指着wrapper.eq(Product::getBrand, brand)说“这就是动态拼 WHERE 条件”。3. 学生身份与商品归属强绑定用 JWT Spring Security 实现细粒度权限控制3.1 登录流程Vue 提交账号密码 → Spring Boot 验证 → 返回 JWT Token → Vue 存入 localStorage前端登录逻辑src/views/Login.vue// 提交登录表单 const login async () { try { const res await apiClient.post(/auth/login, { username: form.username, // 学号如 2021001 password: form.password }) // 后端返回 { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... } localStorage.setItem(user_token, res.data.token) localStorage.setItem(user_id, res.data.userId) router.push(/dashboard) } catch (err) { message.error(登录失败 (err.response?.data?.message || 未知错误)) } }Spring Boot 端配置 JWT 过滤器关键代码节选// src/main/java/com/example/ecommerce/config/JwtAuthenticationFilter.java public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token getTokenFromRequest(request); if (token ! null jwtUtil.validateToken(token)) { String userId jwtUtil.getUserIdFromToken(token); // 从数据库查用户角色STUDENT / ADMIN User user userService.getById(Long.valueOf(userId)); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }配置 Spring SecuritySecurityConfig.javaConfiguration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz - authz .requestMatchers(/auth/login, /register, /swagger-ui/**).permitAll() .requestMatchers(HttpMethod.POST, /api/products).authenticated() // 发布商品需登录 .requestMatchers(HttpMethod.DELETE, /api/products/**).access(securityService.canDeleteProduct(authentication, request)) // 自定义权限 .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }3.2 关键权限逻辑学生只能删自己发布的商品不能只靠前端隐藏“删除按钮”必须后端校验。创建SecurityService实现PreAuthorize表达式Service public class SecurityService { Autowired private ProductService productService; // PreAuthorize(securityService.canDeleteProduct(authentication, #id)) public boolean canDeleteProduct(Authentication auth, HttpServletRequest request) { String path request.getRequestURI(); // /api/products/1024 String idStr path.substring(path.lastIndexOf(/) 1); Long productId; try { productId Long.parseLong(idStr); } catch (NumberFormatException e) { return false; } // 获取当前登录用户 ID Long currentUserId getCurrentUserId(auth); // 查询该商品的 publisherId 是否等于当前用户 Product product productService.getById(productId); return product ! null product.getPublisherId().equals(currentUserId); } private Long getCurrentUserId(Authentication auth) { Object principal auth.getPrincipal(); if (principal instanceof User) { return ((User) principal).getId(); } return null; } }然后在ProductController的删除方法上加注解DeleteMapping(/{id}) PreAuthorize(securityService.canDeleteProduct(authentication, #id)) public ResponseResultString delete(PathVariable Long id) { boolean removed productService.removeById(id); return removed ? ResponseResult.success(删除成功) : ResponseResult.fail(删除失败); }3.2.1 为什么这个方案比PreAuthorize(principal.id #productId.publisherId)更可靠后者需要在 SpEL 中访问#productId的publisherId但#id是路径变量Long 类型不是 Product 对象SpEL 无法穿透查询自定义SecurityService方法可主动查库逻辑清晰、可单元测试、报错信息明确如“您无权删除他人商品”毕业答辩时老师问“如果学生 A 伪造请求删学生 B 的商品你怎么拦”你可以直接展示这段代码和对应的单元测试用例。4. 图片上传与存储用本地磁盘 Nginx 静态服务避开云存储配置复杂度4.1 Vue 端用 el-upload 组件上传限制格式与大小!-- ProductForm.vue 中的图片上传区 -- el-upload classavatar-uploader action/api/upload :http-requesthandleUpload :show-file-listfalse :limit3 :on-exceedhandleExceed img v-ifimageUrl :srcimageUrl classavatar / i v-else classel-icon-plus avatar-uploader-icon/i /el-upload自定义上传方法避免直接用action导致跨域// handleUpload 方法 handleUpload({ file }) { const formData new FormData() formData.append(file, file) // 使用 apiClient已配好 token apiClient.post(/upload, formData, { headers: { Content-Type: multipart/form-data } }).then(res { this.imageUrls.push(res.data.url) // 后端返回形如 /uploads/20240410/abc.jpg }).catch(err { this.$message.error(上传失败 err.response?.data?.message) }) }4.2 Spring Boot 端接收 MultipartFile存入uploads/目录并返回相对路径// ProductController.java 新增方法 PostMapping(/upload) public ResponseResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return ResponseResult.fail(文件不能为空); } if (file.getSize() 5 * 1024 * 1024) { // 5MB 限制 return ResponseResult.fail(文件不能超过 5MB); } String contentType file.getContentType(); if (!image/jpeg.equals(contentType) !image/png.equals(contentType)) { return ResponseResult.fail(仅支持 JPG/PNG 格式); } try { // 生成唯一文件名时间戳 随机数 原后缀 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename System.currentTimeMillis() _ RandomStringUtils.randomAlphanumeric(6) ext; // 存入 uploads 目录相对于 jar 包同级 String uploadDir uploads; Path uploadPath Paths.get(uploadDir); if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } Path targetPath uploadPath.resolve(newFilename); file.transferTo(targetPath); // 返回可被 Nginx 映射的路径注意不带盘符是相对路径 String url /uploads/ newFilename; return ResponseResult.success(url); } catch (Exception e) { return ResponseResult.fail(上传失败 e.getMessage()); } }4.3 Nginx 配置将/uploads/请求映射到本地磁盘目录在nginx.conf的http块内添加# 将 /uploads/ 开头的请求指向项目根目录下的 uploads 文件夹 location /uploads/ { alias /path/to/your/project/uploads/; # 替换为你的绝对路径如 /home/user/ecommerce/uploads/ expires 7d; add_header Cache-Control public, immutable; }注意Spring Boot 默认不处理静态资源路径/uploads/所以必须用 Nginx 拦截并转发。若跳过 Nginx 直接用ResourceHandlerRegistry则需额外配置addResourceLocations(file:uploads/)但 Windows 路径写法易出错Nginx 方案更稳定、更贴近生产环境。5. 毕业论文可呈现的关键技术点与答辩话术从代码到逻辑的闭环表达5.1 如何在论文“系统实现”章节中把技术选择讲出深度不要写“本系统采用 Vue 框架因为它是渐进式框架”。要写“选择 Vue 3 Composition API 而非 Options API是因为其函数式组织方式更利于模块复用。例如商品列表页ProductList.vue与搜索页Search.vue共用loadProducts()逻辑只需将该函数抽离为useProductList()自定义 Hook两页导入调用即可避免复制粘贴导致后续修改不同步。这符合本科毕设‘小而精’的要求——不追求炫技但体现工程化思维。”对应代码示例src/composables/useProductList.jsimport { ref, onMounted } from vue import { productApi } from /api/product export function useProductList(initialFilters {}) { const products ref([]) const pageInfo ref({ current: 1, total: 0 }) const filters ref({ ...initialFilters }) const load async (page 1) { pageInfo.value.current page const res await productApi.list({ ...filters.value, page, size: 12 }) products.value res.data.list || [] pageInfo.value.total res.data.total || 0 } onMounted(() load()) return { products, pageInfo, filters, load } }5.2 答辩高频问题预判与应答要点问题应答核心30 秒内说完论文可写位置“为什么用 MyBatis-Plus 而不用 JPA”“JPA 抽象层较厚对本科毕设而言MyBatis-Plus 提供了 XML 零配置的 CRUD同时保留 SQL 可视化能力。比如商品搜索的多条件组合用LambdaQueryWrapper动态构建 WHERE 子句比 JPA 的 Criteria API 更直观也便于我在论文中截图展示查询逻辑。”系统设计章节 → 持久层技术选型“JWT Token 怎么保证不被窃取”“Token 存于 localStorage 有 XSS 风险因此我在登录成功后立即清除登录表单中的密码字段并在所有 API 请求头中使用Authorization: Bearer xxx后端通过PreAuthorize注解强制校验。此外Token 设置了 2 小时过期过期后前端自动跳转登录页避免长期有效 Token 泄露。”安全性设计章节 → 认证机制“图片上传为什么不用阿里云 OSS”“OSS 需申请 AccessKey 并配置跨域策略对毕设环境增加部署复杂度。我采用本地磁盘Nginx 静态服务既满足功能需求又能在答辩时现场演示从上传、存储、到页面显示的完整链路所有代码和配置均在 Git 仓库中可查。”系统实现章节 → 文件存储方案5.3 一个能让老师眼前一亮的细节优化商品列表页的防抖搜索学生常把搜索框input绑定loadProducts()导致每敲一个字就发请求。改成防抖// ProductList.vue 中 import { ref, onUnmounted } from vue const searchTimer ref(null) const loadProducts () { // 清除上一次定时器 if (searchTimer.value) { clearTimeout(searchTimer.value) } // 设置新定时器延迟 300ms 执行 searchTimer.value setTimeout(() { // 执行实际请求 fetchProducts() }, 300) } onUnmounted(() { if (searchTimer.value) { clearTimeout(searchTimer.value) } })这个改动不到 10 行代码却体现了对用户体验和服务器负载的双重考虑——答辩时老师看到搜索框不再疯狂刷新会自然点头。本文还有配套的精品资源点击获取
返回列表