ARTICLE DETAIL

建站实战干货

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

SSM+微信小程序校园二手交易跳蚤市场:毕设源码与实战解析

2026/9/26 15:13:03 拓冰建站 浏览量
SSM+微信小程序校园二手交易跳蚤市场:毕设源码与实战解析 简介这是一套基于SSM框架SpringSpringMVCMyBatis与微信小程序开发的校园二手交易跳蚤市场毕业设计项目面向计算机相关专业学生、教师及初步接触全栈开发的爱好者。项目以校园闲置物品交易为业务场景覆盖用户登录、商品发布、信息浏览、在线沟通与订单管理等典型功能模块前端采用微信小程序后端由SSM提供接口与数据处理适合用于毕业设计、课程设计或项目初期演示。压缩包大小约22.98MB内含可运行源码、数据库脚本及使用文档源码已经本地编译验证评审分达95分以上功能完整且难度适中。目前已有63人学习/下载适合需要完整参考实现或在此基础上做二次开发的学习者。通过本套代码可以掌握SSM整合开发、微信小程序接口对接、数据库表设计及前后端联调等关键技能同时借助使用文档快速完成环境搭建与部署验证。1. 毕业设计选它没错SSM 微信小程序的校园二手交易跳蚤市场源码和文档都在上周帮人看一个 java 毕业设计的启动报错项目就是这套基于 SSM 微信小程序的校园二手交易跳蚤市场。打开源码包翻了五分钟我就知道这是个能直接拿来答辩的项目后端用的 Spring SpringMVC MyBatis 这一套经典组合前端是微信小程序原生页面数据库脚本、使用文档、源码包一个不少。校园二手交易这个题材本身就是毕设热门业务链路完整——登录、发闲置、逛商品、收藏、下单每一步都能对应上 SSM 的一个层次。适合三类人正在选毕设题目的 java 方向学生想拿完整前后端项目练手加深理解的开发者以及需要快速复现并改造成自己课题的从业者。这项目最值钱的地方不是界面而是整条业务链路的闭环。2. SSM 微信小程序工程结构拆解与技术选型的取舍2.1 为什么毕设爱用 SSM配置全在明面上答辩才问不倒SSM 说的是 Spring、SpringMVC、MyBatis 三件套。Spring 管对象创建和依赖注入SpringMVC 管请求进来怎么找到对应的方法MyBatis 管 SQL 怎么写和怎么执行。这套组合里每个环节的配置都是显式写在 XML 或注解里的比如数据源配置在 jdbc.properties请求映射写在 Controller 的 RequestMapping 上SQL 写在 mapper 目录的 XML 文件里。为什么毕设项目普遍用它而不是 Spring Boot因为 SSM 的配置足够透明。答辩时老师问请求是怎么进来的SQL 是在哪里执行的你可以指着 web.xml 里的 DispatcherServlet 配置再翻到 GoodsMapper.xml 里的 select 语句一步步讲清楚调用链路。Spring Boot 虽然开发快但自动配置把大量细节封装成了黑匣子老师追问自动配置原理时很容易卡壳。对比点SSMSpring Boot对毕设的实际影响配置可见性显式 XML逐层可控自动配置细节封装答辩讲原理更顺手依赖管理手动在 pom.xml 管理版本统一版本管理集成第三方库时可能有版本坑上手速度配置多慢开箱即用快资源包带使用文档能弥补应用场景大量存量项目仍在使用新项目主流java 面试两边都会被问如果你正在准备 java 基础面试或暑期实习亲手把 SSM 项目跑起来比单纯背八股文更能回答MVC 执行流程这一类问题。这套组合的请求链路、事务配置、SQL 映射都是面试常客。拆这个压缩包时我注意到它解压后是标准 Maven 工程 小程序原生前端。这里说的小程序原生指的是用开发者工具直接打开就能跑不是 uniapp 转的那套。原生项目在调试时更稳路由和生命周期都直接走微信的规则。2.2 工程目录逐个拆后端三层与小程序的 pages 怎么对应后端是典型的 SSM 分层前端是原生小程序整个目录结构如下ssm_secondhand ├── pom.xml # Maven 依赖Spring、MyBatis、MySQL 驱动、PageHelper ├── src/main/java/com/shop │ ├── controller # 接口层接收小程序请求并返回 JSON │ ├── service # 业务层登录、商品、订单等核心逻辑 │ ├── dao # MyBatis 数据访问层定义接口 │ └── entity # 实体类User、Goods、Order、Category ├── src/main/resources │ ├── mapper # GoodsMapper.xml 等 SQL 实现文件 │ ├── jdbc.properties # 数据源连接配置 │ ├── spring-mvc.xml # 控制器扫描、静态资源映射 │ └── mybatis-config.xml # MyBatis 全局配置 └── src/main/webapp/WEB-INF # web.xmlDispatcherServlet 入口 miniprogram ├── app.js # 小程序全局逻辑onLaunch 里做登录 ├── app.json # 页面路由和窗口配置 ├── utils/request.js # 封装 wx.request统一带 token └── pages ├── index # 首页商品列表 ├── publish # 发布闲置 ├── detail # 商品详情 ├── order # 我的订单 └── user # 个人中心展示我的发布和收藏逻辑说明后端长这样说明它是标准 Maven 工程不是单文件脚本。controller 层只负责接收参数和返回结果service 层写业务规则dao 层只做数据访问。小程序的 pages 目录和后端接口是一一对应的index 对应商品列表接口publish 对应发布接口detail 对应商品详情接口。从一次首页加载看整个链路pages/index 的 onLoad 里调用 wx.request 访问后端 /goods/list 接口请求先到 web.xml 配置的 DispatcherServletSpringMVC 根据请求路径找到 GoodsController 的 list 方法Service 层组装查询条件再调用 GoodsMapper 接口mapper 目录里的 XML 文件执行真正的 SQL查询结果一层层返回最终以 JSON 格式回到小程序端。全程就是一个标准的 MVC 流程也是这个项目的脊柱。参数说明后端默认端口是 8080小程序入口 utils/request.js 里的 baseUrl 写的是 http://localhost:8080/ssm_secondhand。在开发者工具里打开不校验合法域名就能访问本机。等你要在真机上预览时这个 localhost 是必须改掉的第一个坑后面避坑章节会展开讲。3. 登录到发布再到下单核心链路的代码实现3.1 微信登录wx.login 拿到 code后端换 openid再返回 token二手交易小程序不需要用户注册账号常见做法是微信授权一键登录。流程分三段小程序端调 wx.login 获取临时 code后端拿 code 加 appid 和 secret 去微信接口换 openid最后查库找到或创建用户返回登录态。这里有个细节openid 是用户在你这套小程序里的唯一身份标识但你不能直接把 openid 当 token 返回给前端。因为每次小程序冷启动wx.login 生成的 code 都不同而且 openid 一旦在网络传输中被截获等于对方拿到了你的身份。常见做法是后端根据 openid 查到 userId再返回一个自定义 token。下面的封装直接落地了这套逻辑// utils/request.js 里的登录封装 const login () { return new Promise((resolve, reject) { wx.login({ success: (res) { if (!res.code) { reject(new Error(登录失败拿不到 code)) return } // 把 code 发给后端由后端去换 openid wx.request({ url: ${baseUrl}/user/login, method: POST, data: { code: res.code }, success: (resp) { // 后端返回 token存到 storage后续请求统一带 wx.setStorageSync(token, resp.data.data.token) wx.setStorageSync(userId, resp.data.data.userId) resolve(resp.data) }, fail: reject }) }, fail: reject }) }) }逻辑说明wx.login 返回的 code 有效期只有 5 分钟而且是一次性的后端拿到后必须立刻去微信的 jscode2session 接口换 session_key 和 openid。整个封装用 Promise是为了在 app.js 的 onLaunch 里能先 await 登录完成再进首页避免页面渲染时还没有 userId。为什么要让后端去换 openid 而不是前端直接拿因为换 openid 的请求需要携带小程序的 appid 和 secretsecret 一旦写进小程序前端代码抓包就能被看到等于把用户数据接口完全暴露。所以这个请求必须放在后端 Java 代码里用 HttpClient 或 Hutool 的工具类请求微信接口。// UserController 登录接口 Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(value /login, method RequestMethod.POST) ResponseBody public Result login(RequestBody MapString, String params) { String code params.get(code); // 用 code 换 openid内部走 HTTP 请求微信接口 String openid userService.getOpenidByCode(code); if (openid null) { return Result.error(微信登录失败); } // 查不到就自动注册新用户不用手动填资料 User user userService.findOrCreateByOpenid(openid); // 生成 token 返回给前端openid 只在后端内部使用 String token userService.createToken(user.getId()); return Result.ok(token, user); } }逻辑说明getOpenidByCode 里做的事情是拼一个 HTTPS 请求到 https://api.weixin.qq.com/sns/jscode2session参数是 appid、secret、code、grant_type。返回的 JSON 里带 openid 和 session_key把它提取出来返回。findOrCreateByOpenid 的意思是第一次进小程序的人自动建一条用户记录之后再进来直接返回既有用户。这样用户不需要手动注册体验最顺。参数说明appid 和 secret 在小程序后台的「开发管理 → 开发设置」里获取Java 侧写到配置文件里。token 的生成不用做太复杂常见做法是 UUID 或 userId 加时间戳拼一个字符串存到 Redis 或直接放数据库的 token 字段里。毕设项目通常不引入 Redis直接存在用户表字段里就够用。3.2 商品发布与分页列表图片上传、状态字段、LIMIT 参数发布闲置是整个系统的核心操作流程是小程序端选图片先把图片文件传给后端的上传接口后端保存到本地磁盘并返回一个可访问的 URL再把标题、描述、价格、分类和图片地址一起提交到商品接口。商品表里必须有一个状态字段 status。这是跳蚤市场业务里最容易被忽略的设计点——一个商品从上架到交易完成状态不是只有有和没有而是至少有三种0 在售、1 已售、2 下架。首页列表只查 status0 的数据用户自己发布的商品可以看全部状态。// pages/publish/publish.js 的提交方法 const submit () { const title this.data.title.trim() if (!title) { wx.showToast({ title: 标题不能为空, icon: none }) return } wx.request({ url: ${baseUrl}/goods/add, method: POST, header: { token: wx.getStorageSync(token) }, data: { title: title, description: this.data.description, price: this.data.price, img: this.data.imgUrl, // 图片上传接口返回的地址 categoryId: this.data.categoryId }, success: (res) { if (res.data.code 200) { wx.showToast({ title: 发布成功 }) setTimeout(() wx.switchTab({ url: /pages/index/index }), 1500) } } }) }逻辑说明header 里带 token 是后端识别当前是哪个用户的凭证。如果后端接口没有做登录拦截这个发布接口就是个裸奔接口任何人都能往里塞数据。小程序端 switchTab 跳转回首页并延时 1.5 秒是为了让用户看清发布成功的提示这个细节在答辩演示时很加分。价格字段的校验容易被忽略我一般会在后端做一层价格必须大于 0description 不能超过 500 字标题不能为空。这些校验规则要和数据库表结构对齐否则报了错用户也看不懂。再来看后端的商品列表接口。它要同时支持首页展示、分类筛选、上拉加载更多所以必须做分页// GoodsController 分页查询在售商品 RequestMapping(/list) ResponseBody public Result list(int pageNum, int pageSize, Integer categoryId) { PageHelper.startPage(pageNum, pageSize); ListGoods goods goodsService.listOnSale(categoryId); PageInfoGoods pageInfo new PageInfo(goods); return Result.ok(pageInfo); }逻辑说明PageHelper 是 SSM 项目里最常用的分页插件原理是在 MyBatis 执行 SQL 前自动拼接 LIMIT 语句。注意 startPage 方法后面必须紧跟第一条数据库查询中间不能插入其他 SQL否则分页会作用到错误的查询语句上。这个细节是 PageHelper 使用时的经典大坑。返回的 PageInfo 对象里自带 total、pageNum、pages、list 等字段前端拿到 list 直接渲染列表判断 pageNum 小于 pages 时继续加载下一页。整个跳蚤市场首页的加载逻辑就是这套。参数说明pageNum 从 1 开始传pageSize 默认给 10。小程序端上拉触底时让 pageNum 自增把返回的数据用 concat 拼到当前数组后面。categoryId 为空时查询全部分类不为空时走分类筛选。后端 Service 层的 listOnSale 方法里拼的是WHERE status 0这样下架和已售的商品不会出现在公共列表里。4. 数据库设计核心表结构与订单状态机4.1 核心表结构用户、商品、分类、收藏、订单跳蚤市场的数据库设计比电商系统简单但该有的表一张都不能少。这个项目的数据库叫 secondhand.sql一共五张核心表user 用户表、goods 商品表、category 分类表、favorite 收藏表、orders 订单表。每张表解决一个业务侧面的问题表名核心字段作用userid, openid, nickname, avatar, phone用户身份与个人资料goodsid, user_id, title, description, price, img, status, category_id, create_time闲置商品信息categoryid, name商品分类如教材、数码、生活用品favoriteid, user_id, goods_id收藏关系记录谁收藏了哪个商品ordersid, order_no, goods_id, buyer_id, seller_id, price, status, create_time交易记录字段设计的几个关键点orders 表必须同时存 buyer_id 和 seller_id因为跳蚤市场有两个视角——我发布过的商品看 seller我下单的商品看 buyer。我的订单页面要同时展示我买到的和我卖出的一张表存两个用户 id 才能支撑这个查询。商品表的建表 SQL 值得细看CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 发布者ID, title VARCHAR(50) NOT NULL COMMENT 商品标题, description VARCHAR(500) DEFAULT NULL COMMENT 详细描述, price DECIMAL(10,2) NOT NULL COMMENT 价格, img VARCHAR(255) DEFAULT NULL COMMENT 商品主图URL, status TINYINT DEFAULT 0 COMMENT 0在售 1已售 2下架, category_id INT DEFAULT NULL COMMENT 分类ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明status 字段单独建索引因为首页列表的所有查询都是WHERE status 0这个字段是查询频率最高的条件。user_id 建索引是为了支撑我的发布这类用户维度的查询。price 用 DECIMAL(10,2) 而不是 FLOAT因为浮点数在 MySQL 里做比较和运算会有精度问题涉及钱的数据必须用定点数这是答辩时老师最喜欢问的数据库知识点。订单状态机是这套业务里真正有含金量的部分。跳蚤市场本质是线下见面交易所以状态机比电商简单得多0 待确认状态买家下单后生成1 已完成双方线下交易成功后卖家确认2 已取消买家或卖家主动取消。整个状态流转是单向的不涉及退款、售后、物流这些复杂分支。在代码里控制状态流转时只允许 0 到 1、0 到 2 这两个方向防止用户通过构造请求把订单改到任意状态。4.2 MyBatis 的 SQL 与事务减库存和生成订单为什么必须放同一个事务在跳蚤市场里减库存对应的是商品状态从 0 改成 1。买家下单这个动作在代码里要做两件事插入一条订单记录同时把商品状态改为已售。这两步操作要么一起成功要么一起失败否则就会出现订单创建了但商品还在售的数据不一致问题。// OrderServiceImpl 创建订单核心方法 Transactional(rollbackFor Exception.class) public boolean createOrder(Order order) { // 1. 校验商品状态必须是 0 在售 Goods goods goodsMapper.selectById(order.getGoodsId()); if (goods null || goods.getStatus() ! 0) { throw new BusinessException(商品不存在或已下架); } // 2. 生成订单号并插入订单表 order.setOrderNo(OrderNoUtil.create()); order.setStatus(0); orderMapper.insert(order); // 3. 把商品状态改成 1已售防止重复下单 goodsMapper.updateStatus(goods.getId(), 1); return true; }逻辑说明第 1 步先查商品状态这是业务校验第 2 步插入订单第 3 步更新商品状态。Transactional 注解保证这三步在同一个数据库事务里执行任何一步抛出异常前面的操作全部回滚。rollbackFor Exception.class 的意思是所有异常都触发回滚这个参数必须显式写明因为 Spring 默认只在 RuntimeException 时才回滚如果你抛的是自定义 BusinessException 且它继承自 Exception默认情况下事务不会回滚。事务要生效还有一个隐藏条件这个方法必须通过 Spring 的代理对象调用。如果你在同一个类里用 this.createOrder() 从另一个方法调用它事务会静默失效。Controller 调 Service 正是走代理对象的路径所以通常没问题但如果你在 Service 内部写了 self-invocation数据就会出诡异的半截状态。对应商品状态更新的 Mapper XML 里有一个用乐观锁防止并发重复下单的写法update idupdateStatus UPDATE goods SET status #{status} WHERE id #{id} AND status 0 /update逻辑说明WHERE status 0是乐观锁最简单粗暴的实现。两个买家同时下单同一件商品数据库层面第一个 update 会把 status 从 0 改成 1影响行数为 1第二个 update 因为 status 已经不是 0影响行数为 0。在 Java 代码里判断影响行数为 0 就抛手慢了商品已被别人下单。这个方案比在 Service 层加 synchronized 锁可靠得多因为 synchronized 只对单个 Tomcat 实例有效而数据库的行锁天然支持多实例并发。5. 避坑五个高频翻车点的现象、原因与解决5.1 小程序请求后端一直失败开发者工具能通真机打不开现象在微信开发者工具里打开不校验合法域名模拟器上登录、列表、发布全正常。换成真机预览后所有请求全部报request:fail。原因两个层面的问题。第一小程序开发工具默认可以跳过域名校验但真机上微信对请求域名有硬性校验必须是已经在小程序后台配置过的 HTTPS 合法域名。第二很多毕设项目后端跑在 localhost:8080真机上的 localhost 指的是手机自己根本不是你的电脑。解决调试阶段有三步——把 utils/request.js 里的 localhost 改成你电脑的局域网 IP保证手机和电脑在同一个 WiFi在开发者工具里打开不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书选项这个选项在详情 → 本地设置里真机调试时把开发版的小程序打开调试模式。如果只是答辩演示局域网 IP 加调试模式就够了。要正式发布才需要备案域名加 HTTPS 证书。5.2 图片上传成功真机上一片 404现象模拟器上商品图片显示正常真机上所有图片都裂了控制台报 404。用缩略图方式排查发现图片 URL 指向的是localhost:8080或本地磁盘路径。原因后端上传接口把图片存到了本地磁盘返回给前端的 URL 是http://localhost:8080/ssm_secondhand/upload/xxx.jpg或者带盘符的绝对路径。模拟器上 localhost 指向你的电脑所以能访问真机上指向手机自然 404。解决上传接口不能返回带 localhost 的绝对路径正确做法是返回相对路径/upload/xxx.jpg前端展示时用 baseUrl 拼接。同时后端要在 spring-mvc.xml 里配置静态资源映射让/upload/**这个路径指向磁盘上的物理目录mvc:resources mapping/upload/** location/upload/ /逻辑说明这行配置的意思是凡是路径以 /upload/ 开头的请求SpringMVC 直接去项目根目录的 upload 文件夹下找文件。location 指向的是后端服务器上的物理目录不一定是项目内相对路径正式部署时经常配成/usr/local/upload/这种绝对路径。这样一来前端不用拼乱七八糟的地址后端换服务器也不用改前端代码。5.3 下单时间比本地时间晚了 8 小时现象数据库里订单的 create_time 显示比实际时间晚 8 小时。例如下午 3 点下单库里记录是早上 7 点。原因MySQL 默认时区不是东八区。在没有显式指定时区的情况下数据库连接会使用系统或连接的默认时区很多服务器默认 UTC而 UTC 比北京时间慢 8 小时。解决改 jdbc.properties 里的数据库连接 URL加上 serverTimezone 参数jdbc.urljdbc:mysql://localhost:3306/secondhand?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8参数说明serverTimezoneAsia/Shanghai 告诉 MySQL 驱动按东八区处理时间字段characterEncodingutf8 保证中文写入不乱码这两个参数是所有 SSM 项目都要配的。另外要注意如果用的是 MySQL 8.0 以上的驱动驱动类名是 com.mysql.cj.jdbc.Driver不是老写的 com.mysql.jdbc.Driver这个差异会在项目启动时直接报 ClassNotFoundException。5.4 分页数据错位或重复上拉加载越看越乱现象首页第一页和第二页数据有重叠或者翻页后数据突然少了一半。有时第一页 10 条第二页只有 3 条。原因大部分是前端传参和后端分页插件的约定不一致。有人前端从 0 开始传 pageNum后端 PageHelper 从 1 开始导致第一页和第二页查的是同一批数据。还有一种是 PageHelper.startPage 后面不是紧跟查询语句中间插了其他 MyBatis 操作分页条件加到了错误的 SQL 上。解决统一约定 pageNum 从 1 开始pageSize 固定传 10。后端分页接口第一行必须是 PageHelper.startPage(pageNum, pageSize)紧接着调用 list 查询。前端上拉加载时判断返回值里的 pageNum 是否小于 pages是则继续加载否则提示没有更多了。还有一个细节分页接口的返回结构里必须有 total 总条数方便前端计算是否还有下一页如果后端返回的 JSON 里没有这个字段前端拉到底会出现无限请求。5.5 事务像没生效订单插进去了商品还是在售现象下单过程中人为制造一个异常比如商品状态改成不可售结果异常确实抛了但订单记录还是写进了数据库商品状态也没有改变。原因Transactional 注解没生效常见有两个原因。第一是这个方法所在的 Service 类没有被 Spring 扫描到或者事务管理器和注解驱动没有在 XML 里配置第二是同类内部调用例如 OrderService 里一个方法直接 this.createOrder()绕过了 Spring 的代理对象事务拦截器根本没执行。解决先确认 spring-mvc.xml或 spring-context.xml里配了以下两行bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/逻辑说明第一行声明事务管理器第二行开启 Transactional 注解的解析。有了这两行配置Spring 才会为标注了 Transactional 的方法创建代理对象。检查完配置后再确认调用入口是 Controller 注入的 Service 接口而不是同类内部的 this 调用。判断事务是否生效有个笨办法在 createOrder 方法里故意抛一个 RuntimeException看订单表是否还有数据没有就说明事务正常工作。6. 上线前最后一道检查从本地到真机的验证清单毕设项目做到能演示只是第一步真正稳妥的交付是换个环境也能跑起来。我每次拿到这类 SSM 小程序的源码包都会按固定顺序过三道检查。第一道是本地全链路。先启动 MySQL把 secondhand.sql 导进去确认五张表都建出来了然后改 jdbc.properties 里的账号密码、时区和编码接着用 Tomcat 启动后端浏览器访问接口地址确认 JSON 能吐出来最后打开微信开发者工具导入 miniprogram 目录关闭域名校验看首页商品列表是否加载出来。任何一个环节没有数据出来就先看控制台报错而不是瞎猜。第二道是真机预览。把 request.js 的 baseUrl 从 localhost 改成电脑的局域网 IP手机和电脑连同一个 WiFi点真机预览。重点测三件事登录是否正常、发布图片是否显示、下单后商品是否变成已售。这三条链路分别对应登录态、文件上传、事务回滚三个最容易出问题的模块能跑通说明核心业务没有硬伤。第三道是项目文档的核对。打开使用文档看里面的部署步骤和实际工程是否对得上。有些项目 SQL 版本和代码对不上有些依赖版本不一致这些都要在交付前排查掉。我的经验是拿到任何源码包第一件事不是看业务代码而是先按文档把项目跑起来跑不起来就优先解决环境问题而不是改业务逻辑。有一回我图省事没走清缓存、关校验、真机同网这套流程直接拿模拟器数据去答辩演示结果现场网络策略加上代理首页白屏了五分钟场面非常狼狈。从那以后我每次拿到这类 SSM 小程序的毕设项目都强制先走一遍SQL 先导、后端先起、小程序先连、真机先看四步跑通了再谈别的。希望帮到你。本文还有配套的精品资源点击获取