ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于SSM+Vue的校园二手交易网站设计与实现:从数据库建模到前后端联调

2026/9/23 1:25:48 拓冰建站 浏览量
基于SSM+Vue的校园二手交易网站设计与实现:从数据库建模到前后端联调 简介这是一份基于JavaSSMVue框架的校园二手交易网站完整项目面向计算机专业毕业设计或课程设计场景系统包含前台用户与系统管理员两类角色用户可浏览和发布二手商品、查看详情、收藏、下单支付也能发起物品求购并管理个人信息管理员则统一处理商品、用户、订单及论坛模块的日常维护整体业务链路较为完整。资源包共5个文件整体压缩包约25.16MB内含项目源码zip包、MySQL数据库脚本sql、毕业论文doc、SSM开发文档docx和答辩PPTpptx从代码部署到论文撰写、答辩展示的材料均已备齐。项目经过严格调试可导入运行并附有配套环境软件、部署视频教程与论文参考便于快速复现系统即使初学者也能按教程完成环境搭建并掌握SSMVue核心开发流程。目前已有125人学习下载适合作为可直接提交的课程实训课题或毕业设计参考。1. 校园二手交易网站的设计起点交易不难难在信任与检索把「校园二手交易网站」拆开看商品增删改查、登录注册、下单支付这些功能在 JavaWeb 课设里已经是被写烂了的模块。真正让这个题目区别于图书管理系统、学生选课系统的地方其实是两个容易被忽略的点商品搜索的准确率以及交易状态的可信流转。前者决定用户能不能快速找到想要的教材和数码产品后者决定买卖双方愿不愿意完成线下交易。SSM Vue 的组合恰好能覆盖这两条线后端用 SSM 管理数据与状态机前端用 Vue 做实时筛选和响应式交互。这套项目适合用来做毕业设计或课程设计也适合 Java 初学者把它当完整案例从头到尾跟一遍理解 JavaWeb 工程从数据库建模到前后端联调的全过程。2. SSM Vue 的架构选型这套组合到底在解决什么问题2.1 为什么是 SSM 而不是 Spring Boot以及 Vue 在其中扮演的角色很多人在做课设时会纠结现在企业里都是 Spring Boot为什么还要用 SSM这里要分清两个层面。如果是找实习、做商业项目直接上 Spring Boot 没有争议但作为计算机毕业设计很多学校的任务书里明确写了「基于 SSM 框架」或者导师希望看到你手动配置 Spring、Spring MVC、MyBatis 整合过程——这个过程本身就是 JavaWeb 教学的核心内容。Spring 管对象生命周期Spring MVC 管请求路由MyBatis 管 SQL 映射三者各司其职比 Spring Boot 的自动配置更接近 JavaWeb 底层原理。答辩时被问「Bean 的作用域」「SQL 注入怎么防」「Spring MVC 的请求流程」SSM 反而更好回答。Vue 在这套体系里的位置是彻底替代 JSP 作为视图层。传统 SSM 项目用 JSP JSTL 渲染页面前后端耦合在同一个 Tomcat 里而引入 Vue 后后端只负责返回 JSON前端通过 Axios 异步请求数据、用 Vue Router 管理页面跳转形成前后端分离的结构。这在毕业设计里是加分项因为论文里可以写「前后端分离架构」答辩时可以现场演示浏览器 Network 面板里的 XHR 请求。2.2 运行环境的版本搭配与工程目录划分常见做法是把项目拆成两个目录campus-secondhand-backendMaven 工程SSM和campus-secondhand-frontendVue CLI 工程。后端运行在 Tomcat 8.5JDK 1.8MySQL 5.7 或 8.0前端用 Vue 2 或 Vue 3 均可但如果教程资源更多、遇到报错更容易搜到答案建议先用 Vue 2 Element UI。不是说 Vue 3 不好而是毕业设计的时间窗口有限Vue 2 的生态资料覆盖了绝大多数报错场景。后端包结构建议按分层命名而不是按功能拆分com.campus.secondhand ├── controller # 接收请求、参数校验、返回 Result ├── service # 业务逻辑商品状态流转、订单创建 ├── dao # MyBatis Mapper 接口 ├── entity # 对应数据库表的实体类 ├── interceptor # 登录拦截器 ├── config # Spring MVC 配置、拦截器注册 └── util # MD5、JWT、日期工具这种分包方式的优势是每层职责单一写毕业论文的「系统设计」章节时可以直接对应着画分层架构图。包名不要用bean、pojo这种含糊的称谓答辩老师看到entity和service一眼就知道结构。2.3 核心依赖与配置文件的三个关键点Maven 的pom.xml里除了常用的 spring-webmvc、mybatis、mysql-connector-java还需要注意三个容易漏的依赖jackson-databind负责 Java 对象和 JSON 互相转换、commons-fileupload商品图片上传、jjwt生成和解析登录令牌。没有 jacksonController 里返回的对象无法自动序列化成 JSON前端就收不到数据没有 fileupload图片上传接口会直接报 500。SSM 整合最常出问题的配置是 Spring MVC 的注解驱动和静态资源放行。下面是一份经过验证的spring-mvc.xml关键配置mvc:annotation-driven / mvc:default-servlet-handler / bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views/ / property namesuffix value.jsp / /bean mvc:interceptors mvc:interceptor mvc:mapping path/api/**/ mvc:exclude-mapping path/api/auth/**/ mvc:exclude-mapping path/api/goods/list/ mvc:exclude-mapping path/api/goods/detail/**/ bean classcom.campus.secondhand.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这段配置解决两个核心问题第一annotation-driven让 Spring 能识别RestController、RequestMapping等注解否则所有接口都返回 404第二拦截器放行了登录注册接口、商品列表和详情接口未登录用户可以逛网站但不能下单和发布这是二手交易平台比较合理的权限边界。default-servlet-handler保证前端部署后能拿到静态资源不然会出现 HTML 能打开但 JS/CSS 全部 404 的情况。3. 数据库设计商品、订单、用户三张核心表的建模与状态流转3.1 用户表和商品表的设计细节校园二手交易的本质是 C2C 交易平台方不参与商品流转所以在数据建模时不需要设计库存表、SKU 表这些电商系统里复杂的结构核心就是用户、商品、订单、留言四类数据。用户表要额外关注的是联系方式字段——校园交易基本都在线下完成买家和卖家需要互留微信或电话所以qq、wechat、phone这三个字段不能省。如果做成纯站内信沟通就背离了校园二手交易的实际使用场景。商品表是最需要细致设计的一张表。发布商品时用户会上传多张图片常见做法是建一张goods_image子表或者用逗号分隔的字符串存多个路径。毕业设计建议用前者虽然查询时要多做一次关联但在论文里可以写「商品图片一对多关系设计」数据规范性上更站得住脚。CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT MD5加密后的密码, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, wechat varchar(50) DEFAULT NULL, qq varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL COMMENT 头像路径, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE goods ( id int NOT NULL AUTO_INCREMENT, seller_id int NOT NULL COMMENT 卖家ID, title varchar(100) NOT NULL, description text COMMENT 商品描述, category varchar(30) NOT NULL COMMENT 分类教材/数码/生活用品/运动/其他, price decimal(10,2) NOT NULL, original_price decimal(10,2) DEFAULT NULL COMMENT 原价用于展示折扣力度, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, status tinyint DEFAULT 0 COMMENT 0在售 1已预定 2已售出 3下架, view_count int DEFAULT 0 COMMENT 浏览次数, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category (category), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT二手商品表;商品编号选择自增 ID 而不是雪花 ID核心原因是课设场景数据量小自增主键在 InnoDB 里是聚集索引写入性能更好且分页查询ORDER BY id DESC时走主键索引非常快。utf8mb4必须要用因为用户可能在商品描述里输入 emoji 表情普通utf8字符集会报Incorrect string value错误。cover_image字段冗余了封面图路径这样商品列表页查询时不需要每行都去图片子表里取数据这个冗余是刻意的。3.2 订单表的状态机设计三态流转是答辩时的核心考点订单表是交易流程的载体。校园二手交易的线下属性决定了它没有物流信息所以订单状态不需要电商系统那套「待发货、已发货、运输中、已签收」的长链路三到四个状态足够。建议的状态机是买家下单后订单为「待确认」卖家确认后变为「待交易」线下见面完成交易后双方确认变为「已完成」任何一方在「待确认」阶段可以取消订单。CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号时间戳随机数生成, goods_id int NOT NULL, seller_id int NOT NULL, buyer_id int NOT NULL, price decimal(10,2) NOT NULL COMMENT 成交价, status tinyint NOT NULL DEFAULT 0 COMMENT 0待确认 1待交易 2已完成 3已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_goods (goods_id), KEY idx_buyer (buyer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;seller_id和buyer_id都冗余在订单表里这是个刻意的设计。很多课设会把订单表和用户表做关联查询来拿买卖双方信息但每查一次订单都要 JOIN 两张用户表代码复杂度高、SQL 执行慢。直接在订单表存两个 ID查订单详情时用SELECT * FROM orders WHERE id ?拿到 ID 后再分别查用户信息逻辑更清晰也方便在订单列表页同时展示买卖双方的昵称和联系方式。update_time的ON UPDATE CURRENT_TIMESTAMP是 MySQL 5.6.5 以上的特性状态每次变更会自动更新时间做「最近交易记录」排序时直接用这个字段不需要在 DAO 层手动 set。状态流转的核心逻辑不在数据库而在 Service 层。商品表有一个status字段订单表也有一个status字段这两个状态必须联动买家下单成功后商品状态从「0 在售」变为「1 已预定」这样其他用户搜索时看到的是「已被预定」而不是可以继续下单交易完成后商品变为「2 已售出」在列表页直接过滤掉。如果只更新订单状态而忘了更新商品状态就会出现同一件商品被两个人下单的并发问题。在高并发场景下要用悲观锁或乐观锁解决但毕业设计里只需保证在同一个Transactional事务里按顺序更新两条记录。3.3 商品搜索的 SQL 写法与分页参数设计搜索是二手交易网站区别于普通增删改查系统的关键功能也是答辩时最容易被追问的模块。常见误区是用SELECT * FROM goods WHERE title LIKE % #{keyword} %一把梭这在数据量小的时候没问题但有几个细节要处理一是分类筛选要和关键词搜索组合下拉框选了「数码」之后只搜数码类商品二是排序规则要支持发布时间倒序和价格升序三是搜索结果要分页。下面是一条组合查询的 Mapper SQLselect idsearchGoods resultTypecom.campus.secondhand.entity.Goods SELECT id, title, cover_image, price, original_price, category, status, create_time FROM goods where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if testcategory ! null and category ! AND category #{category} /if AND status IN (0, 1) /where choose when testsort price_asc ORDER BY price ASC /when otherwise ORDER BY create_time DESC /otherwise /choose LIMIT #{offset}, #{pageSize} /select这条 SQL 用了 MyBatis 的动态 SQL 标签where会自动处理条件拼接时的多余 ANDchoose实现排序切换。LIKE在这里用了CONCAT(%, #{keyword}, %)而不是直接在 SQL 里写%${keyword}%区别在于#{}是预编译参数占位符MyBatis 会将其转成?做 PreparedStatement 参数绑定从根本上防止 SQL 注入${}是字符串拼接用户传入 OR 11 --这类内容会直接拼进 SQL 造成严重问题。分页参数offset在代码里计算offset (pageNum - 1) * pageSizepageSize建议固定 12 或 20因为这是前端商品卡片列表一屏展示的合适数量也避免一次查出过大数据拖慢页面。4. 后端业务实现登录令牌、全局异常与交易状态的联动更新4.1 登录接口和 JWT 令牌的完整交互流程用户登录模块在课设里最常见的实现有两种Session 和 JWT。Session 的问题是前后端分离后跨域 Session 维护麻烦前端要手动带着 Cookie 才能维持登录态而且如果前端部署在 Nginx 而接口在 Tomcat不同域名下的 Cookie 跨域非常难处理。JWT 把用户信息加密后生成长字符串前端收到后存在 localStorage每次请求在 Header 里带上Authorization: Bearer token后端拦截器校验令牌的签名和过期时间即可。整条链路简单且不需要共享 Session 存储。RestController RequestMapping(/api/auth) public class AuthController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest req) { if (req.getUsername() null || req.getPassword() null) { return Result.error(用户名和密码不能为空); } User user userService.findByUsername(req.getUsername()); if (user null) { return Result.error(用户不存在); } String encryptedPwd MD5Util.md5(req.getPassword()); if (!encryptedPwd.equals(user.getPassword())) { return Result.error(密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername()); user.setPassword(null); return Result.ok().put(token, token).put(user, user); } }这段代码里有两个容易被忽略的细节。第一密码必须经过 MD5 加密后再和数据库里存储的密文比较数据库中不能存明文密码这是数据库设计的基本要求也是答辩时的加分点。第二返回给前端前要把user对象里的password字段置空否则用户信息会带着密码哈希暴露在浏览器 Network 面板里虽然 MD5 不可逆但是不应该返回到前端的数据就不要返。Result是统一的返回体结构通常是{ code: 200, message: success, data: {...} }前端 Axios 拦截器里判断code是否为 200 决定是否走成功逻辑。4.2 登录拦截器的两个坑放行白名单和 OPTIONS 预检请求写了 JWT 之后很多课设会遇到一个奇怪的问题前端明明带了 token请求还是报 401。排查时需要先分清两种情况。第一种是 token 过期需要在拦截器里返回一个明确的错误码让前端跳转到登录页第二种是前端还没来得及把 token 存好就发出了请求。更隐蔽的是第三种浏览器在发送跨域 POST 请求前会先发一个OPTIONS预检请求探测服务器允许的跨域策略这个预检请求不会带Authorization头如果拦截器没有放行 OPTIONS 方法所有跨域请求都会被拦死。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\登录状态已过期请重新登录\}); return false; } } }拦截器里request.setAttribute(userId, ...)这一步很关键后面发布商品、下单等接口直接从 request 里拿当前用户 ID不需要每个接口都去解析一次 token。同时还要在 Spring MVC 配置里添加跨域支持最简单的方式是实现WebMvcConfigurer接口并重写addCorsMappings方法允许前端开发服务器跨域调用。注意这样的跨域配置只适合开发环境生产环境里前后端通常通过 Nginx 反向代理到同一个域名下跨域问题会自动消失。4.3 发布商品和下单的事务一致性商品状态必须先锁定发布商品的核心逻辑比较直接接收前端上传的图片文件保存到服务器本地磁盘或配置的图片路径生成访问 URL再和表单数据一起插入 goods 表。图片不能存到数据库的 BLOB 字段这是性能灾难数据库会迅速膨胀且无法利用 CDN 加速。正确做法是文件存磁盘数据库只存路径。考虑到课设部署环境可能不是 Linux尽量不要使用绝对路径比如D:/upload建议通过配置项动态读取这样部署到不同机器时只需要改配置文件。下单流程是整个后端的核心事务。买家点击「立即购买」后Service 层要在一个Transactional方法里完成以下几个操作Transactional(rollbackFor Exception.class) public void createOrder(OrderCreateRequest req, Integer buyerId) { Goods goods goodsDao.findById(req.getGoodsId()); if (goods null || goods.getStatus() ! 0) { throw new BusinessException(商品不存在或已被预定); } if (goods.getSellerId().equals(buyerId)) { throw new BusinessException(不能购买自己发布的商品); } String orderNo OD System.currentTimeMillis() RandomUtil.randomNumbers(6); Order order new Order(); order.setOrderNo(orderNo); order.setGoodsId(goods.getId()); order.setSellerId(goods.getSellerId()); order.setBuyerId(buyerId); order.setPrice(goods.getPrice()); order.setStatus(0); orderDao.insert(order); goodsDao.updateStatus(goods.getId(), 1); }这个方法的顺序值得说明先查商品并校验状态是否为「在售」再校验不能买自己的商品——这两个校验是业务规则层面的防线必须写在事务里否则会出现并发请求同时读到状态为 0 的商品、同时创建两张订单的问题。Transactional保证一整个方法里的所有数据库操作要么全部成功要么全部回滚。如果没有这个注解商品状态更新成功但订单插入失败就会出现商品显示已预定但订单不存在的脏数据。订单号用时间戳加随机数拼接不依赖数据库自增方便后续在订单列表页直接按订单号搜索。5. Vue 前端联调从脚手架到接口对接的完整路径5.1 Vue CLI 开发环境的关键配置代理解决跨域前端部分用 Vue CLI 创建工程后第一件要做的事就是配置vue.config.js里的 devServer 代理。开发时前端运行在localhost:8080后端在localhost:8080Tomcat 默认端口两者端口不同浏览器会阻止跨域请求。解决方式不是在后端搞复杂的跨域配置而是在 Vue 的 devServer 里把/api开头的请求代理到后端地址这样浏览器里看到的请求是同源的完美避开 CORS。// vue.config.js const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true }, /upload: { target: http://localhost:8080, changeOrigin: true } } } })代理配置里有两个路径要代理/api是业务接口/upload是商品图片的访问路径。changeOrigin: true表示让后端收到的请求的 Host 头变成后端地址否则部分反代场景会出现 403。这样配置后前端代码里所有请求都写成相对路径/api/goods/list不需要写完整的http://localhost:8080部署到生产环境时把代理配置替换成 Nginx 的location /api { proxy_pass ... }即可前端代码一行都不用改。开发环境端口选择 3000 而不是 8080是因为很多机器的 8080 已经被后端 Tomcat 占用了避免端口冲突。5.2 Axios 封装与登录态注入前端所有请求都走一个统一的 Axios 实例而不是每个组件里单独import axios。这样做的目的是在拦截器统一处理 token 注入和错误码跳转。封装文件建议放在src/utils/request.jsimport axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default request这段封装的要点是请求拦截器里从 localStorage 取出 token 注入到 Header响应拦截器统一判断后端返回的code遇到 401 清空本地登录态并跳转登录页。业务组件里调用接口时request.get(/goods/list, { params })拿到的是已经解包后的res可以直接访问res.data不再需要每个页面写重复的错误处理。Element UI 的Message组件用来弹提示不需要每个页面重复引入——这是把基础设施下沉到封装的收益。5.3 商品列表页的关键交互下拉选择驱动数据刷新商品列表页是 Vue 语法展示最充分的页面涉及响应式数据、生命周期、事件处理三个方面。页面结构通常是顶部搜索栏关键词输入框、分类下拉框、排序下拉框下方是商品卡片网格。当下拉框或搜索条件变化时让页面重新请求接口并刷新列表。这里要注意 Vue 的响应式边界问题直接通过索引修改数组元素不会触发视图更新必须使用this.goodsList response.data整体赋值或者用this.$set()方法。搜索和筛选请求要做一层防抖处理否则用户每敲一个字符就发一次请求后端会收到大量无效请求。常见做法是在 watch 监听关键词变化时加 lodash 的debounce或者自己写一个 300ms 的延时器。商品列表的分页用 Element UI 的 Pagination 组件current-page和page-size绑定到 data页码变化时重新请求数据。这些交互串联起来就是毕业设计项目「前后端分离」这个概念最直观的体现前端只负责交互和展示所有数据逻辑都在后端接口里。6. 毕业论文与答辩 PPT 的技术一致性从演示路径到追问预案毕业设计项目的代码、论文、答辩 PPT 三者的技术表述必须完全对齐这是很多同学最终拿不到高分的原因——代码里用了 JWT论文里却写成 Session数据库设计画的是三张表代码里实际建了六张表。论文的「系统实现」章节每一张功能截图都应该能在代码里找到对应的 Controller 方法每一个接口调用的返回数据都要能在数据库里验证到。建议在论文里附一张接口清单表包含接口路径、请求方法、参数说明、返回数据答辩时老师说「你把发布商品的接口讲一下」你直接照表讲就行。演示环节要设计一条连贯的业务路径而不是登录后乱点一通。推荐顺序是用测试账号登录 → 搜索「高等数学」教材 → 进入商品详情 → 点击立即购买 → 切换为卖家账号查看订单 → 确认交易 → 回到列表页看到商品状态变为「已预定」。这条路径覆盖了搜索、详情、下单、状态流转四个核心功能每一个操作都能在前端界面和后端日志里看到完整链路。准备预案同样重要比如演示前把 MySQL 服务手动重启一次让「数据库连接失败」这种故障提前暴露而不是现场出问题图片上传路径改成临时目录避免在演示机器上没有写入权限。用账号体系演示「买家与卖家交互」时可以提前准备两个账号浏览器用一个开发者工具的无痕窗口再用另一个不需要退出重登。最后一个小技巧所有测试账号的密码统一比如都设置为123456演示时不需要回忆密码也不会因为记错密码而尴尬。针对技术追问要准备三层应答第一层是「怎么实现的」比如搜索用 LIKE 模糊匹配加分类过滤第二层是「为什么这么实现」比如多表冗余是因为课设数据量小避免过度 JOIN第三层是「还可以怎么改进」比如后期可以引入全文检索引擎替换 LIKE。这三个层次覆盖了答辩老师大多数追问方向数据能自圆其说分数自然不会差。本文还有配套的精品资源点击获取