
二手交易系统这类项目在校园里几乎是“毕业设计常青树”在企业里也能对应到真实的C2C业务流程。只要做过一次从数据库设计到前后端联调的全流程后面再遇到类似的进销存、拍卖、租赁系统套路都是通用的。这篇文章我直接把这套基于SpringBootVue的二手交易管理系统的完整实现思路、核心代码逻辑、环境配置和踩坑记录梳理出来全程按可复现的标准来写想拿它当毕设参考或者想自己动手写一个类似项目的都可以直接对照着做。这个项目本身选型很标准后端用SpringBoot2.x整合MyBatis前端用Vue2 ElementUI数据库用MySQL权限控制走JWT或Session拦截器核心模块覆盖用户管理、商品发布、商品分类、商品检索、订单管理、留言评价、个人中心这些典型功能。整套做完大概是一个标准的管理后台加C端展示页面的结构。先说清它能解决什么问题一个是买家和卖家之间交易信息撮合的问题另一个是管理员对用户、商品、订单做后台管控的问题。适合谁参考JavaWeb基础刚过关、想完整走一遍前后端分离项目的人或者毕业设计选了二手交易方向但不知道从哪下手的人。1. 项目整体设计与思路拆解在动手敲代码之前先要把系统的角色边界和数据流向理清楚。这个系统我建议按三类角色来设计普通用户买家和卖家身份重叠、管理员、系统本身的后台任务比如商品自动下架、订单超时处理这些如果用得上可以做定时任务。1.1 核心功能模块划分二手交易系统的核心矛盾和电商系统一样关键是建立“信任”。所以功能模块不能只做增删改查还要考虑交易链路怎么闭环。从C端用户视角看功能链路是注册登录 → 浏览商品/搜索商品 → 查看商品详情 → 下单购买或联系卖家 → 支付或线下交易 → 确认收货 → 互相评价。从这个流程里能拆出来的模块包括用户模块注册、登录、个人信息维护、密码修改、头像上传商品模块商品发布、商品编辑/下架、商品详情、分类筛选、关键字搜索、图片上传订单模块创建订单、订单状态流转、卖家发货/买家确认、订单取消消息模块买家与卖家的在线留言或站内信管理后台用户管理、商品审核/下架、订单管理、分类管理、数据统计这里有个容易被忽略的设计点二手商品和全新商品不一样它不是标准化SKU每个商品都有独立成单的逻辑。所以“购物车”这个功能我建议不要做或者说做了反而别扭。二手商品的交易更多是“看上就直接联系卖家”的模式下单本质上是买家对某个独立商品发起购买请求这跟电商购物车是两种心智。我见过很多二手交易系统强行套电商购物车结果订单逻辑复杂不说用户还根本不买账。1.2 技术选型背后的考量后端用SpringBoot其实没什么好纠结的。SpringBoot的最大价值不是功能多而是用自动配置帮你省掉了大量SpringMVC和MyBatis整合时的XML配置。在学校里写SSM整合可能要折腾大半天的配置文件在SpringBoot里一个注解就解决了。对毕业设计或者中小型管理系统来说SpringBoot的健壮性和开发效率都是最稳的选择。MyBatis相比JPA可控性更强。二手交易系统的查询条件组合非常灵活比如按价格区间、按成色、按分类、按关键字、按发布时间排序这些在MyBatis里可以写动态SQL去处理逻辑清楚也方便后期调优。而且MyBatis对复杂多表联查的掌控力比JPA的自动映射要直观得多你写了一条SQL基本就能预判它执行出来的结果这种确定感在实际开发里特别重要。前端选Vue的理由更直接生态成熟、上手门槛低、中文资料多。ElementUI提供的那套表格、表单、弹窗组件做后台管理系统几乎是开箱即用。Vue的双向绑定特性让表单交互和数据回显变得非常自然配合axios做异步请求前后端分离的开发体验确实很舒服。MySQL就不多说了这个体量的项目用它完全够用。唯一要提醒的是MySQL 8.x和5.7在驱动包、时区配置、密码加密方式上有一些差异具体怎么处理后面在环境配置部分细说。2. 数据库设计表的关联关系与字段规划我接手过太多项目上来先写代码写到连表查询的时候发现数据模型不对又要回头改表结构改来改去浪费大量时间。数据库设计是二手交易系统的地基这一层设计好了后面开发效率至少提升一半。2.1 核心表结构分析这个系统的核心表我按业务域拆成四组来设计。第一组是用户域。user表的核心字段包括id、username、password、nickname、avatar、phone、email、status启用/禁用、create_time。密码字段我建议存MD5或BCrypt加密后的密文后面登录校验时再做匹配。这里要额外加一个role字段用0和1区分普通用户和管理员这样后台权限校验时直接从token里取角色判断不用再查一次数据库。第二组是商品域。goods表是整个系统的核心业务表字段有id、user_id卖家ID、category_id分类ID、title、description、price、original_price原价参考、degree成色、images图片JSON数组、status0在售、1已售、2下架、view_count、create_time。这里有个设计要点images字段我推荐用JSON字符串存储前端展示时直接JSON.parse后遍历省掉单独建一张商品图片表的繁琐。这个方案在数据量不大的场景下完全可行查询性能也不用担心。第三组是交易域。orders表记录每一次交易行为字段包括id、order_no订单编号、goods_id、seller_id、buyer_id、price、status0待付款、1待发货、2待收货、3已完成、4已取消、create_time、pay_time、finish_time。order_no要按规则生成比如时间戳加随机数目的是保证并发下单时订单号不重复。第四组是互动域。包括category表分类、comment表评价、message表站内留言。comment表关联orders表或goods表用来做交易完成后的互相评价。2.2 表关系如何梳理这些表之间的核心关系用一句话讲一个用户拥有多个商品一个商品属于一个分类一个用户可以下单购买多个商品每个订单对应一个商品一个商品下可以有多条评价和留言。主外键关系在建表时建议写在DDL里保持数据完整性。如果说你这套系统是给毕业设计用的建议准备一份数据库初始化SQL脚本把所有建表语句、初始数据、测试账号都写进去。这样答辩的时候老师要跑你的项目直接导入SQL就能看到数据效果比临时造数据强太多了。初始数据里至少要包含5个以上的商品数据、2个用户账号一个普通用户、一个管理员、3~5个分类数据。关于数据库设计的注意事项我整理了几条实际踩过的坑不要在user表里直接存图片Base64编码数据库会爆炸的前端传上来的图片应该保存到服务器本地目录或OSS数据库里只存URL路径。也别把商品描述字段设置成varchar(255)真实描述肯定不够用直接用text类型。3. 后端核心模块SpringBoot集成MyBatis的落地实现后端开发是这套系统的重头戏我按项目分层来讲Controller层负责接口暴露Service层负责业务逻辑Mapper层通过MyBatis操作数据库。这种分层的好处是职责清晰出了问题能快速定位也方便后期扩展。3.1 项目结构规划一个标准的SpringBoot项目结构长这样com.example.secondhand ├── controller // 接收请求、返回响应 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象比如分页查询参数 ├── vo // 视图对象比如商品详情展示的数据 ├── config // 配置类拦截器、跨域、文件上传等 ├── common // 统一返回结果、异常处理、常量 ├── utils // JWT工具类等 └── SecondhandApplication.java // 启动类为什么要区分entity、dto、vo这是很多初学者最容易忽略的。直接把数据库实体类返回给前端会面临两个问题一是多余字段暴露比如用户密码直接返回给前端二是多表联查时实体类装不下。比如商品列表页面要显示卖家的昵称和头像但goods表里只有user_id这时候必须用一个vo对象去承接联查结果。所以我的建议是凡是接口返回给前端的数据结构一律用vo类承接entity只负责和数据库表字段一一对应。3.2 统一返回结果与异常处理的必要性写后端接口时一定要设计一个统一的响应体不然后端返回数据的格式五花八门前端axios封装的时候没法做统一拦截处理。我的习惯是封装一个Result类public class ResultT { private Integer code; // 1为成功0为失败 private String msg; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(1); result.setMsg(success); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(0); result.setMsg(msg); return result; } }这个Result类的价值在于前端axios拦截器里可以统一判断code是否为1如果为0就直接弹出错误提示不用每个接口都单独处理错误分支。你很难想象没有统一响应体是什么体验。我和别人合作过一个项目有人返回的接口成功状态是“success”有人用true有人用200前端恨不得为每个接口单独写一套判断逻辑维护成本成倍增加。全局异常处理的思路是一样的。写一个RestControllerAdvice注解的全局异常处理器捕获业务异常、运行时异常统一封装成Result返回。这样后端即使代码报错了前端拿到的也不会是一堆看不懂的默认错误页面而是友好提示。3.3 商品发布接口的核心逻辑商品发布是系统最重要的一个接口。前端通过表单提交商品信息包括标题、描述、价格、成色、分类、图片列表。这里涉及一个关键设计前端先调用文件上传接口把图片上传到服务器拿到图片URL数组之后再把URL作为表单字段一并提交给商品发布接口。不要试图在发布接口里同时接收图片文件和表单字段那样做会让接口职责混乱还容易超时。发布接口的核心代码逻辑如下PostMapping(/goods) public Result? publish(RequestBody Goods goods, RequestAttribute(userId) Integer userId) { goods.setUserId(userId); // 当前登录用户就是卖家 goods.setStatus(0); // 默认上架状态 goods.setViewCount(0); goods.setCreateTime(new Date()); goodsMapper.insert(goods); return Result.success(null); }这里用RequestAttribute(userId)来拿当前登录用户ID这个userId是从哪里来的呢是在拦截器里解析JWT Token后塞到request对象里的。这样设计的好处是Service层不需要关心用户身份从哪来只要保证请求先经过拦截器校验就行。3.4 MyBatis动态SQL实现多条件搜索二手交易系统里最常用的功能就是搜索和筛选。用户可能在首页点分类可能搜索关键字可能按价格排序也可能三者同时操作。这就要用MyBatis的动态SQL来实现多条件组合查询。看一下商品分页查询的Mapper写法select idselectGoodsPage resultTypecom.example.secondhand.vo.GoodsVO SELECT g.*, u.nickname AS sellerName, u.avatar AS sellerAvatar FROM goods g LEFT JOIN user u ON g.user_id u.id where if testcategoryId ! null AND g.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (g.title LIKE CONCAT(%, #{keyword}, %) OR g.description LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND g.price gt; #{minPrice} /if if testmaxPrice ! null AND g.price lt; #{maxPrice} /if if teststatus ! null AND g.status #{status} /if /where ORDER BY g.create_time DESC /selectMyBatis里 标签的智能之处在于它会自动处理第一个 条件前面的AND关键字不会因为某个条件为空拼接出语法错误的SQL。这是MyBatis使用再频繁不过的功能如果你用JPA反而要写一堆Specification可读性差得多。分页这里建议用PageHelper插件。PageHelper的用法是在查询前调用PageHelper.startPage(pageNum, pageSize)然后紧接着执行查询它会自动拦截SQL并生成LIMIT语句同时返回PageInfo对象给你里面包含了总条数、总页数这些分页参数。PageHelper.startPage(pageNum, pageSize); ListGoodsVO goodsList goodsMapper.selectGoodsPage(params); PageInfoGoodsVO pageInfo new PageInfo(goodsList);PageInfo里默认带了很多属性比如total、pages、pageNum、pageSize、list前端分页组件直接返回这个数据结构就好。3.5 文件上传与静态资源映射图片上传用SpringBoot原生实现就够了不需要引入第三方OSS。在application.yml里配置上传路径file: upload-dir: ./upload/然后写一个文件上传接口接收MultipartFile把文件保存到本地目录返回访问URLPostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { // 1. 生成唯一文件名避免重名覆盖 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID().toString().replace(-, ) suffix; // 2. 保存文件到本地目录 File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadDir newFileName)); // 3. 返回图片访问URL return Result.success(/upload/ newFileName); }这里有个关键操作配置WebMvcConfigurer的addResourceHandlers方法把/upload/**路径映射到本地磁盘目录。如果不做这一步图片URL在上传成功后的第一分钟可能能访问但重启后或者换个环境部署就404了这个映射是必须的。Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadDir); } }启动前记得先创建好upload目录或者在代码里做判断不要等到上传时报错才去建目录。4. 前端Vue项目实现从搭建到接口对接前端我用的是Vue2 ElementUI Vue Router Axios的组合。说句老实话Vue3和Element Plus现在也能用了但很多学校的毕业设计和教学环境还在用Vue2而且这套组合的成熟案例最多遇到问题搜一下基本都能解决。如果你是在公司做新项目可以考虑Vue3但如果是做管理类系统和学习参考Vue2完全够用。4.1 前端项目目录结构src ├── api // 所有接口请求方法 │ ├── goods.js │ ├── user.js │ └── order.js ├── assets // 静态资源 ├── components // 通用组件商品卡片、分页等 ├── router // 路由配置 ├── store // Vuex状态管理 ├── utils // axios封装等工具 ├── views // 页面视图 │ ├── home // 首页商品展示 │ ├── goods // 商品详情、发布商品 │ ├── order // 订单管理 │ ├── user // 个人中心 │ └── admin // 管理员后台 └── App.vue前端项目的最怕问题是“接口散落各处”。你如果只在某个页面里直接调用axios不统一封装一层api后面维护时找接口定义会找得你怀疑人生。我的做法是每个模块建一个api文件统一导出接口函数页面里只引入函数名不直接写URL。好处是后端如果改了接口路径只需要改api文件不用在几十个页面里逐个找。4.2 axios封装与请求拦截器axios封装里有几个核心设计。第一是baseURL配置开发环境用http://localhost:8080后面部署到服务器上只需要改一个变量。第二是请求拦截器每次请求前从localStorage里取token并放到header里。第三是响应拦截器后端返回的数据先解一层壳如果code为0说明业务失败直接Message.error提示。// utils/request.js import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: process.env.VUE_APP_BASE_API || 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 1) { return res } if (res.code 401) { // token过期清除登录状态跳转登录页 localStorage.removeItem(token) router.push(/login) } Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }) export default request401这个状态码设计很关键。实际开发中有很多用户操作到一半token过期的情况如果不在拦截器里统一处理401前端每个页面都得写一套“去登录”的逻辑代码冗余且容易漏。统一处理逻辑一旦响应码触发401清除本地登录状态然后跳转登录页体验上等于“强制重新登录”。4.3 商品列表页和详情页的关键实现商品列表页是前端最核心的展示页面。这里有两个技术细节一个是商品卡片组件渲染多张图片另一个是分页和筛选条件的状态管理。商品卡片组件里图片轮播用el-carousel如果一张图就只显示一张。展示价格的时候要注意保留两位小数这个可以在计算属性里处理。商品状态如果是已售出卡片上要盖上“已售”的水印实现方式是用CSS定位加半透明背景色并不复杂。商品详情页面的核心是数据加载逻辑。进入页面时先读取路由参数里的商品ID然后调用接口获取商品详情。这里的坑在于当用户从“相似推荐”点进去另一个商品时路由参数变了但页面组件实例是复用的created钩子不会重新触发。解决办法是用watch监听$route的变化重新拉取数据。这个问题我在Vue开发里遇到太多次了凡是“列表页跳详情页再点推荐”的场景都会踩这个坑。watch: { $route.params.id: { handler(newId) { if (newId) { this.loadGoodsDetail(newId) } }, immediate: true } }4.4 管理员后台表格弹窗表单的组合套路管理后台的三个核心页面用户管理、商品管理、订单管理套路几乎一样页面加载时请求分页列表数据渲染在el-table里点击操作按钮时弹出el-dialog里面放el-form提交时调用接口成功后刷新列表。这个套路学会后后台类页面的开发速度会飞快。商品管理页面里需要有一个特色的操作审核和上下架。管理员可以把违规商品强制下架也可以把用户误操作下架的商品重新上架。对应后端接口就是修改商品status字段。页面里的操作列可以这样实现el-table-column label操作 width180 template slot-scopescope el-button v-ifscope.row.status 0 typedanger sizemini clickoffShelf(scope.row)下架/el-button el-button v-else typesuccess sizemini clickonShelf(scope.row)上架/el-button el-button typeinfo sizemini clickshowDetail(scope.row)详情/el-button /template /el-table-column管理后台还有一块用户管理的“禁用账号”功能。被禁用的用户在前端登录时要被拦截这个操作是在后端登录校验里做的检查用户status字段如果是禁用状态就直接返回“账号已被禁用”的错误信息。5. 环境搭建与配置指南从零跑起来很多人在项目能写代码之前就先被环境搞崩溃了。这一节我把开发环境的版本选择、安装步骤和项目初始化过程完整写出来按这个流程操作基本不会出问题。5.1 开发工具与版本选择我的推荐版本组合是这样的JDK 1.8不要用太高版本1.8最稳定SpringBoot2.x对1.8支持最好Maven 3.6.3MySQL 5.7或8.0Node.js 14.0以上Vue2项目建议用Node14或16太高版本有兼容问题IDEA 2021装Lombok插件用社区版也行前端脚手架Vue CLI 4.5或5.0这里要注意JDK版本和SpringBoot版本的对应关系。SpringBoot 2.x系列用JDK1.8完全没问题SpringBoot 3.x必须JDK17以上同时javax包名变成了jakarta很多第三方库的兼容性还没跟上。对老手来说这都不是事但博主在这里劝你保守一点直接SpringBoot2.7.x JDK1.8这套组合最省心。5.2 SpringBoot项目的初始化步骤在IDEA里新建SpringBoot项目有两种方式。一种是去Spring Initializr网站上生成项目压缩包下载后导入IDEA另一种是IDEA自带的Spring Initializr工具直接创建。创建过程中就选好Java版本和依赖勾选Web、MyBatis、MySQL Driver、Lombok这几个依赖。创建完成后在pom.xml里再补充两个配置。第一个是分页插件PageHelper的坐标dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency第二个是JWT依赖用来做登录鉴权dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency如果版本不对或者依赖拉不下来多半是Maven仓库源的问题。可以在Maven配置文件settings.xml里配置阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/central/url /mirror5.3 application.yml配置详解SpringBoot的配置文件是项目的门面。数据库连接、端口号、MyBatis配置、文件上传路径都在这边定义。我的做法是把多个环境的配置拆开写开发环境用application-dev.yml生产环境用application-prod.yml主配置文件里用spring.profiles.active指定当前生效的环境。毕业设计不需要搞得太复杂但养成这个习惯对以后工作有好处。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/secondhand?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 100MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.secondhand.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl注意几个细节。数据库URL里的serverTimezoneAsia/Shanghai是MySQL 8必须带的不配的话会报时区错误。useSSLfalse是为了避免本地连接时的SSL警告。allowPublicKeyRetrievaltrue是MySQL 8在某种情况下需要配置的否则会报Public Key Retrieval is not allowed错误。mapping下划线转驼峰这个配置强烈建议开启。数据库字段是create_time实体类属性是createTime开启map-underscore-to-camel-case后MyBatis能自动完成映射不用在每一条查询里写resultMap。字段少的表可以不写resultMap但字段多的表还是建议写明确指定映射关系最稳妥。5.4 Vue项目创建与依赖安装前端项目用Vue CLI创建。先全局安装Vue CLInpm install -g vue/cli然后创建项目vue create secondhand-web创建过程中会有几个交互选项选择Manually select features勾选Router、Vuex、CSS Pre-processors其他可以不需要。进入项目后安装ElementUI和Axioscd secondhand-web npm install element-ui npm install axios如果npm安装速度太慢或卡住改用淘宝镜像源npm config set registry https://registry.npm.taobao.org这里有个实际开发中经常遇到的情况npm install半天装不上最后发现是网络或者node版本不兼容的问题。Node版本太高容易导致node-sass编译失败建议用Node 14或16配合sass-loader版本8.x。如果你用的Vue CLI是5.x内置的sass版本可能和你安装的不匹配可以考虑不勾选CSS预处理器直接写纯CSS不折腾sass。我的建议是能少装一个依赖就少装一个毕业设计不是生产项目没必要追求最新的技术组合稳定跑通最重要。6. 核心功能完整实现从零到联调这一节把系统里几个核心功能的前后端完整实现流程串起来从用户登录到商品发布再到下单购买每一步的接口设计、数据库操作、前端交互都给出来。6.1 用户注册与登录JWT鉴权用户模块的第一件事是注册。注册接口接收用户名、密码、确认密码。后端要做校验用户名是否为空、密码长度是否足够、用户名是否已被注册。如果校验通过密码用MD5加盐或BCypt加密后存入数据库。登录接口的逻辑是接收用户名和密码查询数据库比对密码比对成功则生成JWT Token返回前端。JWT里可以放userId、username、role这些关键信息。Service public class UserServiceImpl implements UserService { Override public String login(String username, String password) { User user userMapper.selectByUsername(username); if (user null) { throw new RuntimeException(用户名不存在); } // 校验密码 String md5Password MD5Utils.md5(password salt); if (!md5Password.equals(user.getPassword())) { throw new RuntimeException(密码错误); } // 校验账号状态 if (user.getStatus() 1) { throw new RuntimeException(账号已被禁用); } // 生成JWT Token String token JwtUtils.createToken(user.getId(), user.getRole()); return token; } }前端拿到token后存到localStorage里。后续在路由守卫里判断没有token的用户不能访问需要登录的页面。路由守卫的写法router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })JWT的过期时间建议设置为24小时。这里有一个安全注意点JWT不是加密的是Base64编码的任何人拿到token都可以解码看到里面存了什么。所以敏感信息不要放JWT里只在token里存userId和role就够了其他信息需要时再查数据库。6.2 登录权限拦截器实现后端需要在请求进入Controller之前做统一鉴权。SpringBoot里用HandlerInterceptor实现Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册、首页这些不需要鉴权的接口 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(/register) || uri.contains(/goods) || uri.contains(/category)) { return true; } // 从header里拿出token String token request.getHeader(Authorization); if (StringUtils.isEmpty(token)) { throw new RuntimeException(未登录); } // 解析token把userId塞到request里 Claims claims JwtUtils.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(userRole, claims.get(role)); return true; } }然后在WebConfig里注册这个拦截器Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /register); }这里有个关键的体验细节登录拦截器不应该对所有路径生效。首页的商品列表、商品详情、分类查询这些公开接口如果也拦截用户不登录就什么都看不了这个体验太差了。合理的策略是浏览商品的接口公开但涉及发布商品、下订单、评价这些“写”操作的接口需要登录。6.3 商品发布功能完整流程商品发布的前端表单包括标题、分类、成色、价格、原价、描述、图片上传。图片上传模块用ElementUI的el-upload组件手动控制上传流程el-upload action/api/upload :headersuploadHeaders :on-successhandleUploadSuccess list-typepicture-card i classel-icon-plus/i /el-uploadel-upload组件自带的上传逻辑只要配置action为后端接口地址组件就会自动发请求。要注意的是因为要带token所以headers里必须塞上Authorization。上传成功后把返回的图片URL存到一个数组里最后提交表单时数组用JSON.stringify转成字符串传给后端。商品发布接口提交的数据结构{ title: 九成新罗技鼠标办公用, categoryId: 3, price: 49.9, originalPrice: 129, degree: 九成新, description: 购于一年前使用频率低功能正常无维修记录, images: [\/upload/xxx1.jpg\,\/upload/xxx2.jpg\] }6.4 下单购买的业务逻辑用户点击商品详情页的“立即购买”按钮后前端执行三步操作先检查用户登录状态再调用订单创建接口最后跳转到订单详情或支付页面。订单创建的接口逻辑需要处理一个并发问题如果两个用户同时下单同一个商品怎么保证只有一个成功解决方案是数据库层面加一个状态判断的更新操作利用MySQL的行锁保证原子性UPDATE goods SET status 1 WHERE id #{goodsId} AND status 0这条SQL执行后返回影响的行数。如果影响行数为1说明商品成功从在售状态改成已售状态这次下单有效如果影响行数为0说明商品已经被别人买走直接返回“商品已售出”。这个方案比先查再更新要安全得多。先查再更新的方式存在经典的“查询时商品在售更新时别人已经下单”的竞态条件。在Java单机环境下可以用synchronized锁分布式下用Redis分布式锁但都没有这条SQL简单可靠。订单创建完成后订单状态是“待付款”。对二手交易来说线上支付不是必选项很多场景是用户拍下后线下交易。所以这里我给一个简化方案订单状态由卖家手动推进买家下单后卖家在“我的订单”里看到新订单联系买家线下交易后双方确认完成订单状态变成“已完成”。减少一个在线支付环节对毕业设计来说既能展示核心流程又不用接入微信支付宝这些复杂API。6.5 我的发布与我的订单用户中心里的“我的发布”页面展示当前用户发布的所有商品支持下架、编辑、删除操作。这个页面用一张表格展示商品缩略图、标题、价格、状态点击下架就调接口改status。“我的订单”页面区分两个Tab我买到的、我卖出的。两个列表的数据来源不同我买到的查orders表里buyer_id等于当前用户的数据我卖出的查seller_id等于当前用户的数据。订单列表里有一个状态流转操作卖家视角的订单有“发货/确认交易”按钮买家视角有“确认收货”按钮。每次操作后订单状态更新同时记录操作时间。这一套状态机逻辑虽然简单但前端的按钮显示要根据当前用户角色和订单状态动态计算不能在页面里写死。7. 常见问题与排查技巧实录开发这种规模的项目一定会遇到各种问题很多问题搜索都不好搜到直接答案。我把实际操作中遇到的典型问题整理成一份问题速查表方便你对照排查。7.1 后端常见问题排查问题一项目启动报数据库连接失败报错信息通常是Communications link failure或Access denied for user。排查思路第一步检查MySQL服务是否启动Windows下到服务里看MySQL服务状态第二步检查用户名密码是否正确第三步检查数据库URL里的serverTimezone参数是否配置。还有一种情况是MySQL 8的驱动类变了从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver如果pom.xml里引入的是mysql-connector-java 8.x版本配置时要写新的驱动类。问题二MyBatis的Mapper接口无法注入报错Field goodsMapper in xxxService required a bean of type xxxMapper that could not be found。这个一看就是缺少MapperScan注解。在启动类上加上MapperScan(com.example.secondhand.mapper)或者在每个Mapper接口上标注Mapper注解。两个方法选一个就行推荐用MapperScan一次扫描所有Mapper接口不会漏。问题三MyBatis一级缓存引发查询结果不一致MyBatis的一级缓存是SqlSession级别的默认开启。在同一个SqlSession里执行两次相同的查询第二次会直接命中缓存返回同一个对象引用。如果在两次查询之间通过Java代码修改了这个对象第二次查询拿到的就是被修改后的数据。这个问题在Spring集成MyBatis后不太容易出现因为Spring管理的Mapper每次查询都会创建新的SqlSession但如果你在测试代码里手动创建SqlSession操作就要注意这个问题。解决方法是避免手动修改查询返回的实体类对象如果非要修改用DTO或VO来承接。问题四Maven依赖冲突或下载慢spring-boot-starter-parent的版本统一管理了大部分依赖的版本所以引入新依赖时除非特殊需求不要自己指定版本号。如果某些包下载不下来查看Maven仓库镜像配置。如果依赖版本发生冲突用mvn dependency:tree查看依赖树定位冲突的包用exclusion排除掉多余的依赖版本。问题五文件上传返回的URL图片加载不出来首先检查路径映射是否配置正确访问http://localhost:8080/upload/xxx.jpg看是否能直接打开。如果404说明静态资源映射有问题如果能打开但前端页面显示不出来那就是前端URL拼接的问题检查前端访问图片时是否把完整路径拼对了。另一个常被忽略的问题是上传目录的写入权限在Linux服务器上部署时如果上传目录没有写权限文件保存会失败。7.2 前端常见问题排查问题一npm install报错ERESOLVE unable to resolve dependency tree这个错误通常出现在Node版本过高和依赖包的semver版本兼容性上。解决方式使用npm install --legacy-peer-deps命令跳过peer依赖检查或者把Node版本降低到16.x。如果项目不需要复杂的sass功能建议移除sass相关依赖。问题二跨域请求报错CORS前后端分离项目最常见的报错。前端跑在localhost:8081后端跑在8080浏览器默认不允许跨域请求。解决办法有两种一是在后端加一个跨域配置类二是用Vue CLI的devServer代理配置把/api下的请求全部转发到后端地址。// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这种方式在前端代码里不用写完整的后端地址只用写/api/goods这种相对路径。实际开发中我觉得代理方案更干净因为后端不用处理跨域逻辑前端代码在上线前只需把baseURL改成真实域名就行。问题三Vue打包部署后页面白屏或静态资源404打包命令npm run build后dist目录下的index.html引用的静态资源路径默认是根路径/。如果部署在子目录下资源路径就会404。解决方法是修改vue.config.js里的publicPath为./让资源使用相对路径加载。另外Vue Router如果是history模式部署后需要配置服务器的重写规则把找不到页面的请求都指向index.html。毕业设计没必要折腾这个直接用hash模式最省事部署后刷新页面也不会404。问题四ElementUI的图标不显示ElementUI 2.x版本使用iconfont字体文件如果打包后字体文件加载失败图标就不显示。检查public目录下是否有fonts文件夹或者考虑引入element-ui的完整CSS。新版Element Plus改成SVG图标了不存在这个问题但老版本确实会遇到。8. 项目部署与答辩准备建议系统开发完成后还需要考虑到部署和展示。这个项目的部署方式很简单后端打jar包前端打包静态文件然后用Nginx做反向代理把两个服务串起来。整个过程我梳理出几个关键步骤。8.1 后端打包部署在项目根目录执行 Maven 打包命令mvn clean package -DskipTests打包成功后target目录下会生成一个secondhand-0.0.1-SNAPSHOT.jar文件。在服务器上运行java -jar secondhand-0.0.1-SNAPSHOT.jar如果服务器内存不大可以用nohup让程序后台运行nohup java -jar secondhand-0.0.1-SNAPSHOT.jar app.log 21 用nohup启动后日志输出到app.log文件排查问题就看这个文件。程序关闭用kill命令找到对应的PID后杀掉不要直接关服务器。8.2 前端部署与Nginx配置前端打包npm run build把dist目录下的所有文件上传到服务器Nginx的静态目录比如/usr/share/nginx/html/secondhand。Nginx配置示例server { listen 80; server_name your-domain.com; # 前端页面 location / { root /usr/share/nginx/html/secondhand; index index.html; try_files $uri $uri/ /index.html; } # 后端接口和图片资源转发 location /api/ { proxy_pass http://localhost:8080/; } location /upload/ { proxy_pass http://localhost:8080/upload/; } }这里把/api/开头的请求转发到后端的8080端口/upload/图片路径也直接转发给后端。这样用户访问你的系统只需要一个域名或IP前后端的端口都不用暴露出来架构看起来也专业。8.3 答辩或展示时的加分细节如果你是用于毕业设计我真心建议你在答辩或演示时准备这么几样东西系统里预先准备3~5个商品覆盖不同分类和价格区间。演示的时候可以快速展示搜索和筛选的效果不用现发布商品等半天。再准备一个管理员账号现场演示后台的“用户禁用”和“商品下架”功能这个操作视觉冲击力强老师更容易理解你系统的权限设计。还可以在PPT里加上数据表结构图标注清楚每张表的作用以及表和表之间的关系这是体现你工程设计能力最直观的材料。8.4 项目扩展方向参考如果完成基础功能后还想给系统增加一些亮点可以考虑这几个方向。推荐系统根据用户的浏览历史和购买记录在首页做“猜你喜欢”推荐。用标签匹配相似商品不一定上机器学习简单的标签交集计算也能做出效果。站内通知当买家下单、卖家确认交易、系统下架了你的商品时通过站内信或邮件通知用户。这个功能用一个notification表加定时扫描逻辑就能实现。数据可视化管理员后台增加销售数据统计图用ECharts展示每日交易量、商品数量变化、热门分类排行等。线条和柱状图非常直观而且代码不难写。收藏和关注用户对感兴趣的商品可以点收藏收藏后可以快速查看商品的状态变化。引入一个favorite表关联userId和goodsId即可。每个方向的改动量都不大但展示的时候能明显提升系统的完整度和技术含量也能体现你主动思考问题的意识。9. 写在最后的实操心得这套系统我从零开始写完到跑通完整流程前后花了大概一周多的时间。有个很深刻的体会项目能不能顺利推进关键不在某个技术点会不会写而在你对整体流程有没有清晰的概念。商品、订单、用户这三条主线的数据流转一旦想通了剩下的都是体力活。还有一点想多说几句。很多人在写MyBatis的Mapper时习惯在Java代码里拼接SQL这是很不好的写法。XML里的动态SQL语法虽然要花点时间学但绝对值得。一个是SQL和Java代码分离修改SQL不用重新编译Java代码另一个是动态SQL在复杂条件查询上的表达力比Java字符串拼接强太多。刚开始写MyBatis的时候可能不习惯写多了之后你会爱上这种方式的。另外一个值得养成的习惯是写接口文档。你可以用Swagger集成也可以简单写一个README文档把每个接口的路径、参数、返回值都记录下来。这个习惯在实际开发中太重要了。这个项目里我自己当时没写文档后面前端对接时一边翻后端的Controller代码一边对接口效率极低。最后补充一点经验如果你在开发过程中遇到某个奇怪的问题优先去查环境版本和依赖冲突不要上来就怀疑代码逻辑。我遇到的大部分离奇Bug最后追根溯源都是依赖版本不匹配或者环境配置不对。代码反而是最不容易骗你的东西有一说一你写的逻辑对不对DEBUG跑一遍就知道了。这套二手交易系统的开发到这里就完整结束了。按这个流程走下来你收获的不仅是一个能跑的项目更是一套“从需求分析到结构设计再到前后端编码与部署上线”的完整方法论。这套方法论才是比源码本身值钱得多的东西。