资讯动态

SSM+微信小程序校园二手交易平台毕设实战:从表设计到订单状态机

发布时间:2026/9/13 2:01:45 来源:尧图企业网站定制
简介面向计算机相关专业毕业生与在校生这是一套基于SSM框架与微信小程序开发的校园二手交易平台毕业设计项目。项目涵盖微信小程序前端、SSM后台管理、数据库脚本与部署说明已在Windows10/11环境调试通过答辩评分97分可直接用于毕业设计展示也可作为课程设计或学习SSM小程序开发的实战范本。资源包共811个文件体积20.33MB主要包含java源码、vue页面、wxml/wxss小程序文件、sql数据库脚本、mp4演示视频与md使用文档等。文件组织清晰便于按前后端模块、数据库、文档分类查阅适合二次开发与功能扩展。目前已有129人学习浏览尤其适合需要快速搭建校园二手交易场景、理解SSM与微信小程序前后端交互逻辑的开发者。获取后即可得到完整可运行源码、初始化数据库、使用文档、演示视频及部署脚本配合教程能显著缩短环境配置时间是完成毕业设计或积累项目经验的高质量素材。1. 为什么校园二手交易平台总被选为SSM高分毕设毕业设计答辩时评委最常问的不是“你用了什么框架”而是“商品图片存在哪里”“订单状态怎么流转”“token过期了怎么办”。这三个问题恰好被这个基于SSM和微信小程序的校园二手交易平台完整覆盖Spring管理业务事务SpringMVC暴露REST接口MyBatis操作MySQL小程序端通过wx.request消费接口。源码包里带了数据库脚本、使用文档和演示视频在Windows 10/11下配合三个批处理脚本就能把服务拉起来。它适合两类人一类是需要完整Java毕设参考的学生另一类是正打算做前后端分离课程设计的开发者。高分不是因为功能多而是把登录、发布、下单这些流程组织成了能讲清楚、能复现的工程结构。2. SSMMySQL后端表结构设计和项目分层是拿分基础拿到源码先不要急着双击启动脚本先把后端目录过一遍。SSM项目最常见的结构是com.school.shop.controller、service、mapper、entityresources下放 MyBatis 映射文件和 Spring 配置。在这次的二手交易平台源码中后端承担两类职责给小程序提供 JSON 接口给管理后台提供页面和数据。所以 controller 里会看到两类写法一类用RestController直接返回对象另一类返回ModelAndView指向 webapp 下的静态页面。理解这个区别答辩时就不会被问懵。2.1 从源码目录看懂SSM三个模块的分工Spring 管 bean 生命周期和事务SpringMVC 管请求路由MyBatis 管 SQL 与对象映射。三者各干各的不要混着讲。实际拆包后目录通常长这样src/main/java/com/school/shop/ ├── controller/ # 接口入口Controller与RestController ├── service/ # 业务逻辑注解事务 ├── mapper/ # MyBatis的Mapper接口 ├── entity/ # 实体类对应数据表 └── utils/ # JWT、MD5、文件上传等工具 src/main/resources/ ├── mapper/ # 各实体对应的XML文件 ├── spring/ # spring与mybatis配置 └── jdbc.properties # 数据库连接 src/main/webapp/ # 管理后台静态资源与页面controller 层不写 SQLservice 层才是事务控制的地方mapper.xml 里只放 SQL。如果答辩被问“为什么不用 JPA”可以从“笔试题更容易讲清 SQL 优化”和“动态 SQL 方便分页”两个角度回答。另外注意webapp里出现的bootstrap.css、main.css、global-restaurant.css都是后台页面的样式它们直接决定管理端能不能正常展示。2.2 用户、商品、订单三张核心表的字段设计面试和答辩都喜欢问表设计高分项目不会只有 user 一张表。核心表是 user、goods、orders下面是最常被问到的字段。表关键字段说明useropenid, student_no, nickname, avatar, phone, statusopenid标识微信用户student_no用于校园身份goodsuser_id, category_id, title, description, price, original_price, images, status, create_time用status区分在售/下架/已卖出ordersorder_no, goods_id, buyer_id, seller_id, price, status, create_time, pay_timestatus驱动买方和卖方的订单状态机favoriteuser_id, goods_id收藏关系表用于“我喜欢的”列表orders 表要在 order_no 和 goods_id 上建索引goods 表要在 category_id 和 status 上建索引因为首页按分类和状态检索订单详情按 order_no 查。这两个索引建好分页查询的 explain 结果会好看很多。2.2.1 用户表openid与学号同时保留微信小程序登录后拿到 openid校园交易又需要实名身份所以用户表不能只存 openid。读数据时要注意 student_no 脱敏答辩时可以提“学号脱敏”这个点。CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, student_no varchar(20) DEFAULT NULL COMMENT 学号, nickname varchar(50) DEFAULT NULL, avatar varchar(200) DEFAULT NULL, phone varchar(11) DEFAULT NULL, status tinyint DEFAULT 0 COMMENT 0正常 1禁用, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;openid 是唯一键重复登录才不会创建多个用户。学号允许为空因为用户可以后期补全。讲到这里评委会觉得你考虑过真实场景而不是只照着课程设计模板抄表结构。2.2.2 商品表状态字段决定上下架商品如果直接删除记录订单历史会断掉所以用 status 逻辑删除。常用枚举0 下架1 在售2 卖出3 审核中。前端首页默认只查 status1 的记录。要实现“我卖出的”和“我买到的”只需在 SQL 里分别过滤 seller_id 和 buyer_id同时附带商品状态。这是整个项目最基础的查询组合之一也是答辩时百分百会被问到的点。2.2.3 订单表用status字段实现状态机订单状态是最容易讲出层次感的地方别为了省事用一个布尔字段。状态推进要遵循“创建订单→买家付款→卖家发货→买家确认完成”这个主链路中间允许取消。CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, goods_id int NOT NULL, buyer_id int NOT NULL, seller_id int NOT NULL, price decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待付款 1待发货 2待收货 3已完成 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_goods_id (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意使用 order_no 作业务唯一键而不是用自增 id。自增 id 一旦做数据迁移就会暴露order_no 可以内置时间戳和随机数。状态只有 0 到 4 五个值却对应买卖双方两套界面逻辑后面第 4 章会讲具体的状态流转代码。2.3 初始化脚本和jdbc.properties配置数据库脚本在源码包的 sql 目录下常见做法是 navicat 里新建 second_hand 库再运行 init.sql。但在这套源码里更省事的是使用 1-install.bat 自动导入后面第 5 章会讲。这里先说连接配置。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/second_hand?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordrootdriver 用 cj.jdbc.Driver 是因为项目适配了 MySQL 8.0。serverTimezone 必须显式设置否则 Windows 下会报时间区错误。useSSLfalse 是本地开发的标准设置线上要改为 true 或者去掉。password 这里写的是 root实际部署时要用自己的数据库密码否则数据库导入成功但后端连不上这是启动失败最常见的原因之一。MyBatis 的 XML 文件里最常出现的问题是数据库字段名和实体属性名对不上字段多的表建议开启驼峰映射configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings /configuration开启后user 表的 openid 可以自动映射到实体的 openId 字段不用在 XML 里写一堆 resultMap。这个小配置答辩时提一嘴比背一堆概念有用。3. 微信小程序端从wx.login到token鉴权的完整链路小程序端和服务端的交互全在 wx.request 上。如果打算白手起家做一个完整项目最难的不是页面而是登录态。源码包里的小程序端目录是典型的原生微信小程序没有引 uni-app 框架胜在部署和讲解都直观。3.1 小程序目录结构与app.json配置miniprogram/ ├── pages/ │ ├── index/ # 首页商品列表 │ ├── detail/ # 商品详情 │ ├── publish/ # 发布商品 │ ├── order/ # 我的订单 │ └── mine/ # 个人中心 ├── utils/ │ └── request.js # 封装的请求 ├── app.js └── app.json页面路径和 tabBar 需要在 app.json 里注册tabBar 的图标路径必须是本地图片网络图片在真机上不会正常显示。各页面与后端接口的关系如下页面路径主要作用对应后端接口pages/index/index首页商品列表GET /api/goodspages/publish/publish发布商品POST /api/goodspages/order/order我的订单GET /api/orderspages/mine/mine个人中心GET /api/user/info{ pages: [ pages/index/index, pages/publish/publish, pages/order/order, pages/mine/mine ], window: { navigationBarTitleText: 校园二手, enablePullDownRefresh: true }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/publish/publish, text: 发布 }, { pagePath: pages/order/order, text: 订单 }, { pagePath: pages/mine/mine, text: 我的 } ] } }pages 里的第一项就是启动页面调整启动页面只需要改变这个数组顺序不用改业务代码。很多人问“修改刚进入的加载页面”怎么处理答案就在这里。3.2 wx.login 与后端 code2Session 对接小程序登录的标准流程是 wx.login 拿到临时 code后端拿 code 去微信接口换 openid。毕设通常不会真的调用微信支付但登录流程可以按真实工程实现。先看小程序侧// app.js onLaunch() { wx.login({ success: (res) { wx.request({ url: http://localhost:8080/api/auth/login, method: POST, data: { code: res.code }, success: (resp) { const { token } resp.data.data; wx.setStorageSync(token, token); } }); } }); }这里的 code 是临时凭证5 分钟有效且只能用一次。后端拿到 code 后需要调用微信的 jscode2session 接口换取 openid 和 session_key不能自己拼接 openid。再看后端处理RestController RequestMapping(/api/auth) public class AuthController { PostMapping(/login) public Result login(RequestBody LoginDTO dto) { String openid wxService.code2Session(dto.getCode()); // 调用微信接口 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } String token JwtUtil.createToken(user.getId()); return Result.ok(token); } }code2Session 返回的 openid 是用户在小程序里的唯一 ID同一个微信用户在不同小程序里 openid 不同。token 用 JWT 生成过期时间设为 7 天小程序端每次请求在 Authorization 头里带上。3.3 封装wx.request统一注入Authorization如果每个页面都直接写 wx.request后期改 baseURL 会改到怀疑人生。源码包里自带 utils/request.js常见做法是包一个 Promiseconst BASE_URL http://localhost:8080; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 401) { wx.navigateTo({ url: /pages/mine/mine }); reject(res); return; } resolve(res.data); }, fail: reject }); }); } module.exports { request };这个方法把 token 读取、401 处理、Promise 化收敛到一个文件里。后端过滤器校验 token 时如果解析失败会返回 401前端统一跳到个人中心重新登录。header 里的键名是 Authorization不是 token因为后端 JWT 过滤器通常按标准头读取。3.4 首页列表渲染和分页参数首页列表需要分页不然商品多了接口会越来越慢。小程序端的分页常见做法是 onReachBottom 触底加载下一页。// pages/index/index.js const { request } require(../../utils/request.js); Page({ data: { list: [], page: 1, size: 10, hasMore: true }, onLoad() { this.loadData(); }, loadData() { if (!this.data.hasMore) return; request(/api/goods?page${this.data.page}size${this.data.size}) .then((res) { const list this.data.list.concat(res.data.list); this.setData({ list: list, hasMore: res.data.list.length this.data.size, page: this.data.page 1 }); }); }, onReachBottom() { this.loadData(); } });page 从 1 开始size 是每页条数后端 MyBatis 分页时用 PageHelper 或者 limit 偏移量。返回记录数小于 size 说明没有更多数据所以 hasMore 的判定写成length size而不是“返回 0 条就停止”。因为最后一页可能恰好有 10 条下一页返回 0 条时才会停会多一次无效请求。这个细节在答辩时经常被追问。4. 商品发布、图片上传与订单状态流转的实现商品发布是整个项目中业务最完整的功能接收表单参数、处理图片、插入商品表、关联用户。订单状态流转则是逻辑最容易出 BUG 的地方下面分开讲。4.1 后端接收MultipartFile保存到本地并生成访问URL小程序端用 wx.uploadFile 上传图片后端用 MultipartFile 接收。常见做法是保存到服务器的 upload 目录再把相对路径存到数据库字段里PostMapping(/api/goods/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; String dir System.getProperty(user.dir) File.separator upload; File dest new File(dir, fileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); return Result.ok(/upload/ fileName); }文件后缀必须过滤不能直接信任 originalFilename否则可能出现路径穿越。UUID 做文件名是为了避免中文名乱码和同名覆盖。保存路径用 user.dir也就是 Java 进程的工作目录Windows 下通常在项目根目录。项目跑起来图片打不开多半是上传请求成功但返回的 URL 没有映射到本地目录。4.2 商品发布与图片表的事务性写入如果一张商品有多张图片设计上一般拆两张表goods 和 goods_image。发布商品时要同时插入主表和图片表任何一步失败都需要回滚。Transactional(rollbackFor Exception.class) public void publish(GoodsDTO dto) { Goods goods new Goods(); goods.setUserId(dto.getUserId()); goods.setCategoryId(dto.getCategoryId()); goods.setTitle(dto.getTitle()); goods.setPrice(dto.getPrice()); goods.setStatus(1); // 上架 goodsMapper.insert(goods); for (String url : dto.getImageUrls()) { GoodsImage image new GoodsImage(); image.setGoodsId(goods.getId()); image.setUrl(url); goodsImageMapper.insert(image); } }Transactional 是 Spring 声明式事务的关键rollbackForException.class 保证编译期异常也能回滚。默认情况下 RuntimeException 才会触发回滚如果你在 service 里捕获了异常却没抛出事务会失效。答辩时问“为什么加了事务没回滚”答案往往在这里。4.3 订单状态流转从待付款到已完成的状态机用 int 字段表示订单状态不同状态对应不同业务操作状态值含义触发动作下一步动作0待付款买家创建订单买家付款后变11待发货买家已付款卖家发货后变22待收货卖家已发货买家确认后变33已完成交易结束无4已取消买家取消/超时关闭无状态推进最好收敛在独立方法里调用方传订单号和当前状态public boolean changeStatus(String orderNo, int from, int to) { int rows orderMapper.updateStatus(orderNo, from, to); return rows 1; }updateStatus 的 SQL 要带上期望的旧状态UPDATE orders SET status #{to}, finish_time NOW() WHERE order_no #{orderNo} AND status #{from}用受影响行数判断是否更新成功如果 rows 等于 0说明当前状态已经不是 from客户端拿到失败提示后重新拉取订单详情。这种乐观锁写法能防止卖家发货和买家取消同时发生时状态错乱。最忌讳的做法是先 select 状态再在 Java 里 if 判断因为并发场景下两个请求可能都读到旧状态。4.4 管理后台静态资源与前端构建物源码包里看到的global-restaurant.css、index.2d26d90a.css、bootstrap.css这些文件是管理后台的静态资源。index.2d26d90a.css带了哈希指纹说明这个页面是打包工具生成的产品文件不是源文件。如果你想改管理后台样式不能直接改这个哈希文件应该找到对应 scss 或 less 源码改完再执行 build.bat 生成新的 css。而main.css、icon.css这类不带哈希的才是可以手动修改的公共样式。SpringMVC 对 webapp 目录下的静态资源默认会映射但如果请求路径和 Controller 的 RequestMapping 冲突会导致静态资源 404。排查方法是在 spring-mvc.xml 里配置默认 servlet 处理mvc:default-servlet-handler/只有写这行之后bootstrap.css 和 index.2d26d90a.css 才会被容器静态资源 servlet 接管而不是被 DispatcherServlet 拦截。这是管理后台页面打不开最常见的坑比改代码优先级更高。5. Windows环境从zip包到跑通install、run、build脚本与最后一个验证点5.1 先看批处理脚本再人工操作解压后根目录的1-install.bat、2-run.bat、3-build.bat不只是摆设拿 Notepad 打开就能看到它们做的事情echo off echo 正在导入数据库... mysql -uroot -proot sql/init.sql echo 数据库导入完成 pause这段命令依赖 mysql 命令已经在 PATH 里并且数据库密码是 root。如果你的 mysql 密码不是 root打开这个 bat 改成自己的密码再执行否则会导入失败。另外根目录的main.css.bak是样式表备份手动改 main.css 前建议先复制一份避免改乱了找不到原始版本。5.2 数据库导入和账号统一导入完成后后端 jdbc.properties 里的用户名密码和数据库名要和 bat 里保持一致。很多人跑不起来就是两处密码不一致批处理用了 root/rootjdbc.properties 写的是 123456。最好把所有数据库配置收敛到一个文件便于答辩演示时快速迁移。5.3 图片目录在Windows下的路径踩坑商品图片上传到项目根目录 upload但 SpringMVC 默认不暴露项目外路径。要在 spring-mvc.xml 或者配置类里注册虚拟路径映射mvc:resources mapping/upload/** locationfile:${user.dir}/upload/ /Windows 下 user.dir 后面要带斜杠写成file:D:/project/upload/也可以。如果图片 404先看磁盘目录里文件有没有生成再根据这个映射规则检查 URL 前缀是不是 /upload/ 开头。5.4 答辩前用一条curl命令验证登录接口演示前最稳妥的自测是绕过小程序界面直接验证接口是否正常。用 curl 带参数调用登录接口观察 HTTP 状态码和返回体curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {\code\:\test-code\}如果返回 200 且带 token 字段说明后端链路通。如果再配合执行curl http://localhost:8080/upload/test.jpg能看到图片内容说明静态资源映射也没问题。打开小程序前先确认这两个 curl 的结果都符合预期。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价