简介:这份资源是面向高校计算机相关专业学生与Java初学者的大学生闲置物品交易小程序毕业设计全套资料,基于微信小程序、SSM框架与MySQL数据库开发,可用于课程设计、毕业设计选题参考或小程序全栈入门练习。压缩包共1180个文件,约51.16MB,包含127个Java后端源码、137个Vue前端组件、160个JavaScript脚本、74个wxss与72个wxml小程序页面文件,以及2个SQL数据库脚本、4个mp4演示视频和毕业论文等文档,覆盖后端接口、管理后台、小程序端与数据库设计。系统区分管理员、卖家、用户三种角色,管理员负责学生、卖家、商品类型与商品信息管理,学生可购买商品,卖家可发布商品,功能链路完整。目前已有162人学习下载,适合需要完整赛题方案、可运行源码、数据库脚本与演示录屏的读者,帮助快速理解SSM与微信小程序的整合开发流程。
1. 从一份毕业设计需求说起:大学生闲置交易小程序到底要交付什么
每年到了三四月,计算机相关专业的群里就会密集出现同一类需求:想做一个校园二手交易平台,技术栈锁定微信小程序加 SSM 加 MySQL,最后要能交出一套完整的东西——能跑起来的小程序、能启动的后端、能导入的数据库脚本,外加一篇查重能过的论文和一段演示视频。这个标题背后其实是一份典型的本科毕业设计交付清单,而不是一个要上线的商业产品。它解决的核心问题是:让一个还没进过公司的学生,在有限时间里独立走完「需求分析、数据库设计、接口开发、前端联调、文档撰写」这条完整链路。
适合看这篇的人有三类:正在做同类选题、卡在某个环节的应届生;需要快速判断这套技术栈能不能撑起答辩的指导老师视角的读者;以及想拿一个真实小项目练手 SSM 和微信小程序的初级开发者。需要先摆正预期:这套组合的技术含量不在算法,而在工程完整度和细节自洽。答辩老师真正会追问的,往往是「你的订单状态怎么流转」「并发下单会不会超卖」「数据库为什么这么建表」,而不是你用了多新的框架。所以整篇的落点会放在能复现的工程细节上,而不是堆概念。
2. 技术选型与整体架构:为什么是微信小程序 + SSM + MySQL
2.1 三端各自的职责边界
先把这套架构拆成三层来看,理解每一层为什么存在,后面写代码才不会乱。
微信小程序负责的是「用户能看见和点到的部分」:商品列表、详情、发布、下单、个人中心。它跑在微信客户端里,用 WXML 描述结构、WXSS 描述样式、JS 处理逻辑,通过wx.request调用后端接口。选它的现实理由是:不用装 App、有现成的登录态体系、开发工具开箱即用,对毕业设计这种周期短、演示要求高的场景非常合适。
SSM 是 Spring + SpringMVC + MyBatis 的缩写,负责后端。Spring 管对象和事务,SpringMVC 管 HTTP 请求路由,MyBatis 管 SQL 映射。它比 SpringBoot 啰嗦,但正因为配置都摊在明面上,反而适合用来讲清楚「一个请求从进来到落库」的完整过程,这也是很多学校指定 SSM 的原因。
MySQL 负责持久化,存用户、商品、订单、分类这些数据。选它的理由很朴素:免费、资料多、和 MyBatis 配合成熟,mysql安装配置教程这类内容网上一抓一大把,出问题好查。
2.2 一次「下单」请求的完整链路
把三层串起来看一次真实调用,比看十张架构图有用。用户在商品详情页点「立即购买」,小程序发一个 POST 请求到/order/create,带上商品 id 和买家 id。请求到达 SpringMVC 的 Controller,Controller 调用 Service,Service 里开启事务,先查商品是否还在售、库存是否足够,再插入订单记录、更新商品状态,最后返回结果。MyBatis 负责把这几步翻译成 SQL 打到 MySQL。
这条链路里最容易出问题的是事务边界和状态判断。如果 Service 没加@Transactional,或者先更新商品再判断库存,就会出现「商品卖掉了但订单没生成」或者「同一件商品被两个人同时买走」的经典翻车。所以下面这张表把核心数据表先定清楚,后面所有代码都围绕它展开。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, openid, nickname, avatar, phone | 用户表,openid 是小程序登录的唯一标识 |
| goods | id, seller_id, title, price, category_id, status, create_time | 商品表,status 区分在售/已售/下架 |
| orders | id, goods_id, buyer_id, seller_id, amount, status, create_time | 订单表,status 走待付款/已完成/已取消 |
| category | id, name, sort | 分类表,给首页做筛选 |
2.3 环境准备与依赖版本
动手前先把环境对齐,版本不一致是新手最常见的第一个坑。JDK 用 1.8(SSM 老项目对高版本 JDK 兼容性差),Maven 用 3.6 以上,Tomcat 用 8.5,MySQL 用 5.7 或 8.0 都行但要注意驱动包版本对应。MySQL 8 要用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver,写错了启动就报错。
<!-- pom.xml 核心依赖,版本按自己环境微调 --> <dependencies> <!-- Spring 核心,管 IOC 和事务 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.20</version> </dependency> <!-- SpringMVC,处理 web 请求 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.20</version> </dependency> <!-- MyBatis 与 Spring 整合包 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- MySQL 驱动,8.0 用 cj 版本 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> </dependencies>这段依赖里,spring-context和spring-webmvc是 SSM 的骨架,mybatis-spring是把 MyBatis 的 SqlSession 交给 Spring 管理的关键,少了它事务不生效。MySQL 驱动版本要和本地数据库大版本匹配,这是参数说明里最容易被忽略的一条。装 MySQL 时如果卡在mysql安装配置教程里的初始化步骤,记住 root 密码一定要记下来,后面applicationContext.xml里要填。
3. 数据库设计与后端接口实现:从建表到跑通第一个接口
3.1 建表脚本与几个必须想清楚的设计点
数据库是整个项目的根,表建错了后面全是返工。下面给出核心建表脚本,重点看字段类型和索引的选择。
-- 用户表,openid 加唯一索引防止重复注册 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信登录唯一标识', `nickname` VARCHAR(50) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表,status 用 tinyint 表示状态,price 用 decimal 不用 float CREATE TABLE `goods` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `seller_id` INT NOT NULL, `title` VARCHAR(100) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `category_id` INT DEFAULT NULL, `status` TINYINT DEFAULT 1 COMMENT '1在售 2已售 3下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_seller` (`seller_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个设计点值得展开。第一,价格字段用DECIMAL(10,2)而不是FLOAT,因为浮点数做金额运算会出现0.1+0.2=0.30000000000000004这种精度问题,答辩时被问到能答上来是加分项。第二,status字段加了索引,因为首页列表几乎每次查询都要按状态过滤,数据量上来后没索引会明显变慢。字符集统一用utf8mb4,否则用户昵称里的 emoji 存进去会变问号。
3.2 MyBatis 映射与商品列表接口
建完表开始写后端。先写 Mapper 接口和对应的 XML,这是 MyBatis 的核心。
// GoodsMapper.java 接口,只声明方法,SQL 写在 XML 里 public interface GoodsMapper { // 按状态分页查询商品,offset 是起始行,limit 是每页条数 List<Goods> selectByStatus(@Param("status") Integer status, @Param("offset") Integer offset, @Param("limit") Integer limit); // 根据 id 查询单个商品 Goods selectById(@Param("id") Integer id); // 更新商品状态,用于下单后把商品置为已售 int updateStatus(@Param("id") Integer id, @Param("status") Integer status); }<!-- GoodsMapper.xml,namespace 必须指向接口全限定名 --> <mapper namespace="com.demo.mapper.GoodsMapper"> <select id="selectByStatus" resultType="com.demo.entity.Goods"> SELECT * FROM goods WHERE status = #{status} ORDER BY create_time DESC LIMIT #{offset}, #{limit} </select> <select id="selectById" resultType="com.demo.entity.Goods"> SELECT * FROM goods WHERE id = #{id} </select> <update id="updateStatus"> UPDATE goods SET status = #{status} WHERE id = #{id} </update> </mapper>namespace写错是新手高频错误,一旦和接口路径对不上,启动就报Invalid bound statement。#{}是预编译占位符,能防 SQL 注入,${}是字符串拼接,除非做动态排序否则别用。分页这里用的是LIMIT offset, limit的物理分页,简单直接,数据量不大时够用。
3.3 Service 层事务与下单逻辑
下单是整套系统里唯一必须保证原子性的操作,Service 层要写清楚。
@Service public class OrderServiceImpl implements OrderService { @Autowired private GoodsMapper goodsMapper; @Autowired private OrderMapper orderMapper; // @Transactional 保证查库存、插订单、改状态三步要么全成要么全回滚 @Override @Transactional(rollbackFor = Exception.class) public String createOrder(Integer goodsId, Integer buyerId) { Goods goods = goodsMapper.selectById(goodsId); // 先判断商品是否存在且在售,这是防超卖的第一道闸 if (goods == null || goods.getStatus() != 1) { return "商品已售出或不存在"; } // 卖家不能买自己的东西 if (goods.getSellerId().equals(buyerId)) { return "不能购买自己发布的商品"; } // 插入订单 Orders order = new Orders(); order.setGoodsId(goodsId); order.setBuyerId(buyerId); order.setSellerId(goods.getSellerId()); order.setAmount(goods.getPrice()); order.setStatus(1); orderMapper.insert(order); // 把商品状态改为已售,这一步和上面插入在同一事务里 goodsMapper.updateStatus(goodsId, 2); return "success"; } }@Transactional的rollbackFor = Exception.class很关键,默认只对运行时异常回滚,如果中途抛了受检异常事务不会回滚,订单插了但商品状态没改,数据就脏了。判断顺序也有讲究:先查状态再操作,把校验放在写操作之前。这套逻辑在单机低并发下没问题,但如果答辩老师追问「两个人同时点购买怎么办」,可以答:加数据库行锁SELECT ... FOR UPDATE,或者用乐观锁在 update 时带上 status 条件判断影响行数。
3.4 小程序端请求封装与列表渲染
后端接口通了,前端要能调。小程序里建议把wx.request封装一层,避免每个页面重复写。
// utils/request.js 统一封装请求,方便加 baseUrl 和错误处理 const BASE_URL = 'http://localhost:8080/api'; function request(url, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'content-type': 'application/json' }, success: (res) => { // 后端约定 code 为 200 表示成功 if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data.msg); } }, fail: (err) => reject(err) }); }); } module.exports = { request };封装的好处是 baseUrl 只改一处,联调时从本地切到服务器地址不用满项目找。注意小程序开发阶段要在开发者工具里勾选「不校验合法域名」,否则localhost请求会被拦。上线才需要配置真实域名和 HTTPS,毕业设计演示阶段用本地即可。
4. 小程序端页面开发与前后端联调:把功能真正跑起来
4.1 首页列表与分页加载
首页是演示时第一眼看到的东西,列表要能滚动加载。用小程序原生的onReachBottom做触底加载更多。
// pages/index/index.js 首页商品列表 const { request } = require('../../utils/request.js'); Page({ data: { goodsList: [], page: 0, pageSize: 10, hasMore: true }, onLoad() { this.loadGoods(); }, loadGoods() { if (!this.data.hasMore) return; // 计算偏移量,第几页就跳过多少条 const offset = this.data.page * this.data.pageSize; request('/goods/list?status=1&offset=' + offset + '&limit=' + this.data.pageSize, 'GET') .then(list => { this.setData({ goodsList: this.data.goodsList.concat(list), page: this.data.page + 1, // 返回条数小于 pageSize 说明没有更多了 hasMore: list.length === this.data.pageSize }); }); }, onReachBottom() { this.loadGoods(); } });hasMore这个标志位是分页的关键,没有它会出现「一直往下拉一直请求空数据」的情况。concat而不是直接赋值,是为了保留已加载的数据。这套分页逻辑和前面 MyBatis 的LIMIT offset, limit是配套的,前后端参数名要对齐,否则查出来永远是第一页。
4.2 发布商品与图片上传
发布页涉及表单和图片上传,是小程序里相对复杂的部分。图片上传用wx.uploadFile,后端用 MultipartFile 接收。
// pages/publish/publish.js 发布商品 const { request } = require('../../utils/request.js'); Page({ data: { title: '', price: '', description: '', imageUrl: '' }, // 选择图片并上传 chooseImage() { wx.chooseImage({ count: 1, success: (res) => { const filePath = res.tempFilePaths[0]; wx.uploadFile({ url: 'http://localhost:8080/api/upload', filePath: filePath, name: 'file', success: (up) => { // 后端返回图片访问路径 const data = JSON.parse(up.data); this.setData({ imageUrl: data.data }); } }); } }); }, submit() { // 简单校验,价格必须填且为数字 if (!this.data.title || !this.data.price) { wx.showToast({ title: '请填写完整', icon: 'none' }); return; } request('/goods/publish', 'POST', { title: this.data.title, price: this.data.price, description: this.data.description, imageUrl: this.data.imageUrl }).then(() => { wx.showToast({ title: '发布成功' }); wx.navigateBack(); }); } });上传接口后端要配置MultipartResolver,否则MultipartFile收不到文件。图片存本地磁盘时路径要能通过 URL 访问到,常见做法是存到 Tomcat 的 webapp 目录下,或者配一个静态资源映射。这里有个坑:小程序uploadFile的name必须和后端@RequestParam("file")的名字一致,不一致就报 400。
4.3 登录态与 openid 获取
小程序的登录不是账号密码,而是wx.login拿 code,后端换 openid。这是整套系统里唯一需要调用微信官方接口的地方。
// 登录流程:前端拿 code,后端换 openid 并返回自定义 token wx.login({ success: (res) => { // res.code 是一次性凭证,五分钟内有效 request('/user/login', 'POST', { code: res.code }) .then(userInfo => { // 把后端返回的用户信息存本地缓存 wx.setStorageSync('user', userInfo); }); } });后端拿到 code 后,用 appid 和 secret 去请求微信接口换 openid,再查库判断是否新用户,是就插入。这里要注意code只能用一次,重复使用会失败。appid 和 secret 在小程序后台获取,写在后端配置文件里,不要硬编码在代码中。演示阶段如果不想真调微信接口,可以在后端写个 mock,用固定 openid 返回,但论文里要说明这是测试方案。
4.4 联调时的接口约定与跨域
前后端联调最容易卡在接口格式不统一。建议一开始就约定好统一响应结构:{ code: 200, msg: "success", data: {...} },前端只认code判断成败。跨域问题在小程序里其实不存在,因为小程序不是浏览器环境,没有同源策略,但如果你用浏览器直接测后端接口,就需要在 SpringMVC 里配 CORS 或者用 Postman 测。联调顺序建议是:先用 Postman 把每个接口单独测通,再接到小程序里,这样出问题能快速定位是前端还是后端。
5. 避坑与排查:那些让答辩翻车的细节
5.1 中文乱码,数据库存进去变问号
现象:发布商品时标题是中文,存到数据库里变成???,或者接口返回的 JSON 里中文乱码。原因通常是三处字符集没统一:数据库连接 URL 没加characterEncoding=utf8、建表时没指定utf8mb4、Tomcat 的server.xml没配 URIEncoding。解决方法是逐层检查,JDBC URL 写成jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,建表统一utf8mb4,Tomcat 连接器加URIEncoding="UTF-8"。三处都对了才不会乱码。
5.2 事务不生效,订单插了商品状态没改
现象:下单后订单表有记录,但商品还是「在售」,能被人重复购买。原因多半是@Transactional加在了 Controller 上而不是 Service 上,或者类没有被 Spring 扫描到,或者方法内部自己try-catch把异常吞了导致不回滚。解决方法是确认注解加在 Service 实现类的方法上,确认包扫描路径包含该类,异常要往外抛不要自己吃掉。这是答辩高频追问点,务必自己测一遍。
5.3 小程序请求报「不在以下 request 合法域名列表中」
现象:开发者工具里请求后端接口,控制台报域名不合法。原因是小程序默认只允许请求已备案的 HTTPS 域名。解决方法是开发阶段在开发者工具「详情 - 本地设置」里勾选「不校验合法域名、web-view、TLS 版本以及 HTTPS 证书」,演示时用本地地址即可。上线才需要真实域名,毕业设计不用折腾这个。
5.4 MySQL 8 连接报时区错误
现象:启动后端时报The server time zone value 'xxx' is unrecognized。原因是 MySQL 8 的时区配置和驱动默认值不匹配。解决方法是在 JDBC URL 后面加serverTimezone=Asia/Shanghai,或者去 MySQL 里执行SET GLOBAL time_zone = '+8:00'。这个错误在mysql安装后第一次连库时特别常见,记住加参数就行。
5.5 图片上传后访问 404
现象:图片上传成功,数据库里也存了路径,但小程序里显示不出来。原因是上传的物理路径和 URL 访问路径没对应上,或者 Tomcat 没配静态资源映射。解决方法是把图片存到 Tomcat webapps 下的固定目录,或者用 SpringMVC 的<mvc:resources>把访问路径映射到磁盘目录。检查时先在浏览器直接访问图片 URL,能打开说明是前端问题,打不开就是映射没配好。
6. 论文与演示视频的收尾技巧:让交付物经得起追问
到了最后阶段,代码能跑只是及格线,论文和演示视频才是决定分数的部分。论文里最容易被挑刺的是「需求分析」和「数据库设计」两章,前者别写成功能罗列,要按角色分:买家能做什么、卖家能做什么、管理员能做什么,每个用例配一张用例图。数据库设计章要把 E-R 图放上去,说明实体之间的关系是一对多还是多对多,比如用户和商品是一对多,商品和订单是一对一。表结构别只贴建表语句,加一段文字解释每个字段为什么这么设计,前面提到的价格用 DECIMAL、状态加索引,都是可以写进去的设计理由。
演示视频建议按「登录 - 浏览 - 发布 - 下单 - 查看订单」这条主线录,控制在三到五分钟。录之前把数据库清一遍,用干净的测试数据,避免录到一半列表里全是乱码标题。录屏时把开发者工具的 Network 面板也开着,展示一次完整的请求响应,比单纯点页面更有说服力。如果答辩老师问「你这个和市面上的二手平台有什么区别」,老实回答这是教学项目、聚焦核心交易流程即可,不要硬吹。
我自己的习惯是:代码提交前一定把applicationContext.xml里的数据库密码改成占位符,把target目录和node_modules这类不该进仓库的东西写进.gitignore,论文里的截图统一分辨率。这些细节不写进论文,但答辩老师翻你代码仓库时一眼就能看出来。这套技术栈不新,但把每个环节都做扎实,本身就是一种能力。希望帮到你。
本文还有配套的精品资源,点击获取