简介这份资源是面向高校计算机相关专业学生与Java初学者的一套校园二手交易平台毕业设计完整方案基于微信小程序前端与Java后端组合开发可用于毕业设计、课程设计或项目实战练习。压缩包共收录1411个文件整体约20.63MB其中Java源码136个、js脚本199个、vue组件148个配合wxml与wxss构建小程序页面另有png、svg、jpg等图片素材及sql数据库脚本、json配置、xml与properties文件覆盖前后端与数据库各层。资源内附说明文档与数据库文件目录结构清晰便于按模块阅读与二次开发。目前已有484人学习下载适合需要完整赛题方案、参考接口设计与页面实现、梳理项目搭建与排错思路的读者也可作为课程答辩与功能扩展的基础模板。1. 校园二手交易平台毕设从选题到跑通的完整拆解每年毕业季计算机相关专业的选题里校园二手交易平台几乎是出现频率最高的那一类。原因很直接业务场景每个学生都熟悉功能边界清晰答辩时老师一听就懂不会追问“你这个东西到底解决什么问题”。但真正动手做的时候翻车的人不在少数——有人卡在微信开发者工具的环境配置上有人数据库表建了十几张结果外键全乱还有人前端页面画完了发现后端接口对不上。这个标题里的关键词拆开看微信小程序是前端载体Java 是后端语言校园二手交易平台是业务场景毕业设计是交付目标源码加数据库加说明是最终要交出去的东西。它适合正在准备毕设的本科生也适合想拿一个完整项目练手小程序加 Java 全栈的初学者。接下来我会按实际做项目的顺序把技术选型、数据库设计、接口开发、联调排错这几个环节讲清楚让你看完能直接动手而不是停留在“知道有这么回事”。2. 技术选型与项目骨架为什么是这套组合2.1 微信小程序加 Java 后端的分工逻辑先把这个架构的分工说清楚。微信小程序负责所有跟用户直接交互的部分页面渲染、表单提交、图片上传、下拉刷新、消息提示。它运行在微信客户端里用的是 WXML 加 WXSS 加 JavaScript 这套技术栈跟标准 Web 前端有相似之处但不完全一样。Java 后端负责业务逻辑和数据持久化用户注册登录、商品发布审核、订单状态流转、消息通知。两者之间通过 HTTP 接口通信数据格式用 JSON。为什么这个组合在毕设里这么常见因为微信小程序有官方开发者工具不需要自己搭前端工程化环境下载安装就能写页面。Java 后端用 Spring Boot 的话一个 main 方法就能启动内嵌 Tomcat不需要额外配服务器。两边都有大量现成的文档和社区内容遇到问题搜一下基本能找到答案。对于时间有限、经验有限的毕设场景来说这套组合的容错率最高。具体到版本选择我一般会这样定微信开发者工具用当前稳定版即可基础库选 2.30 以上因为要用的部分组件和 API 在这个版本之后才稳定。Java 用 JDK 8 或 JDK 11Spring Boot 用 2.7.x 版本这个版本对 JDK 8 兼容好依赖冲突少。数据库用 MySQL 5.7 或 8.08.0 的话注意连接驱动要换成com.mysql.cj.jdbc.Driver。构建工具用 Maven前端包管理用 npm 或 yarn 都行小程序原生开发其实不太依赖 npm但如果你要用 Vant Weapp 这类组件库就需要。注意不要一上来就追求最新版本。毕设的核心是跑通和答辩不是技术尝鲜。JDK 17 加 Spring Boot 3.x 虽然新但很多教程和依赖还没跟上遇到问题搜到的答案可能对不上。2.2 项目目录结构与初始化命令后端项目我习惯用 Maven 多模块或者单模块分层。毕设规模不大单模块就够了包结构按 controller、service、mapper、entity、config、util 来分。前端小程序项目在微信开发者工具里新建目录结构是 pages、components、utils、images、api 这几个。先看后端初始化。用 Spring Initializr 生成骨架或者直接手写 pom.xml。关键依赖包括spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok、hutool-all。MyBatis-Plus 比原生 MyBatis 省事单表 CRUD 不用写 XML。!-- pom.xml 关键依赖片段 -- dependencies !-- Web 支持提供 REST 接口能力 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus简化数据库操作单表查询不用手写 SQL -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Lombok减少 getter/setter 样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Hutool 工具类处理日期、字符串、加密等 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.20/version /dependency /dependencies这段配置里MyBatis-Plus 的版本选 3.5.x 是因为它对 Spring Boot 2.7 兼容稳定。MySQL 驱动 8.0.33 是 8.0 系列里比较新的小版本修复了一些连接问题。Hutool 不是必须的但里面封装好的工具方法能省不少事比如SecureUtil.md5()做密码加密、DateUtil.format()做时间格式化。小程序端初始化更简单。打开微信开发者工具新建项目填 AppID没有的话选测试号模板选“不使用云服务”语言选 JavaScript。建完之后在app.json里配置页面路由和 tabBar在app.js里写全局逻辑比如登录态检查。// app.js 全局逻辑管理登录态和全局数据 App({ globalData: { userInfo: null, // 当前登录用户信息 baseUrl: http://localhost:8080/api, // 后端接口基础地址 token: // 登录令牌后续请求携带 }, onLaunch() { // 启动时检查本地缓存里有没有 token const token wx.getStorageSync(token); if (token) { this.globalData.token token; } } })baseUrl这里写 localhost 是因为开发阶段后端跑在本机。但要注意微信开发者工具默认会校验合法域名localhost 不在白名单里。解决办法是在开发者工具右上角“详情”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。这个选项只在开发阶段用上线前必须换成备案过的 HTTPS 域名。2.3 数据库表设计六张核心表撑起整个业务校园二手交易平台的业务不复杂核心表六张就够用户表、商品表、商品分类表、订单表、收藏表、消息表。每张表的字段要结合小程序端的实际展示需求来定不要照搬电商系统的复杂设计。用户表t_user存微信 openid、昵称、头像、手机号、信用分、创建时间。openid 是微信登录时后端调用code2Session接口拿到的每个用户在小程序里的唯一标识。商品表t_product存发布者 ID、分类 ID、标题、描述、价格、原价、成色、图片路径、交易地点、状态在售/已售/下架、发布时间。订单表t_order存买家 ID、卖家 ID、商品 ID、订单金额、订单状态待付款/待发货/待收货/已完成/已取消、创建时间。-- 商品表建表语句字段类型和长度按实际业务量估算 CREATE TABLE t_product ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, user_id BIGINT NOT NULL COMMENT 发布者ID, category_id INT NOT NULL COMMENT 分类ID, title VARCHAR(100) NOT NULL COMMENT 商品标题, description TEXT COMMENT 商品描述, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价, condition_level TINYINT DEFAULT 9 COMMENT 成色1-10默认9成新, images VARCHAR(500) DEFAULT NULL COMMENT 图片路径多张用逗号分隔, trade_location VARCHAR(100) DEFAULT NULL COMMENT 交易地点, status TINYINT DEFAULT 1 COMMENT 状态1在售 2已售 3下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_category_id (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;price用 DECIMAL 而不是 FLOAT因为金额计算不能有精度丢失。images字段用逗号分隔存多张图片路径这是毕设里常见的简化做法正规项目应该单独建一张图片表。索引建在 user_id、category_id、status 上因为列表查询经常按分类筛选、按状态过滤、按用户查自己的发布。提示建表时字符集统一用 utf8mb4不要用 utf8。utf8 在 MySQL 里是残缺的存不了 emoji而小程序用户昵称里带 emoji 的情况很常见。3. 后端接口开发从登录到商品发布的完整链路3.1 微信登录接口与 token 机制小程序的登录流程跟传统 Web 不一样。前端调用wx.login()拿到一个临时 code这个 code 传给后端后端拿 code 加上 AppID 和 AppSecret 去请求微信的code2Session接口换回 openid 和 session_key。openid 是用户唯一标识session_key 用于解密敏感数据。后端收到 code 之后用 Hutool 的HttpUtil发请求就行。拿到 openid 后查数据库如果用户不存在就自动注册一条记录然后生成一个自定义 token 返回给前端。token 可以用 UUID 或者 JWT毕设里用 UUID 存 Redis 或者直接存数据库都行简单点就存数据库的 user 表加一个 token 字段。// 微信登录接口接收前端传来的 code PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO) { // 1. 用 code 换 openid String url https://api.weixin.qq.com/sns/jscode2session; MapString, Object params new HashMap(); params.put(appid, appId); params.put(secret, appSecret); params.put(js_code, loginDTO.getCode()); params.put(grant_type, authorization_code); String response HttpUtil.get(url, params); JSONObject json JSONUtil.parseObj(response); String openid json.getStr(openid); if (StrUtil.isBlank(openid)) { return Result.fail(微信登录失败); } // 2. 查用户不存在则注册 User user userService.getByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(用户 RandomUtil.randomNumbers(6)); user.setCreateTime(new Date()); userService.save(user); } // 3. 生成 token 并返回 String token UUID.randomUUID().toString().replace(-, ); user.setToken(token); userService.updateById(user); return Result.ok(user); }appId和appSecret不要硬编码在代码里放到application.yml里用Value注入。code2Session接口有频率限制同一个 code 只能用一次所以前端每次登录都要重新调wx.login()拿新 code。token 生成后用 UUID 去掉横杠长度 32 位够用且不容易碰撞。3.2 商品发布与图片上传的处理商品发布涉及两个接口图片上传和商品信息提交。图片上传用MultipartFile接收存到本地磁盘或者对象存储。毕设里存本地磁盘最简单配一个静态资源映射就能通过 URL 访问。// 图片上传接口 PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { // 校验文件类型 String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); if (!Arrays.asList(.jpg, .jpeg, .png, .gif).contains(suffix.toLowerCase())) { return Result.fail(只支持图片格式); } // 生成唯一文件名按日期分目录 String dateDir DateUtil.format(new Date(), yyyyMMdd); String fileName UUID.randomUUID() suffix; File dest new File(uploadPath dateDir / fileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } try { file.transferTo(dest); } catch (IOException e) { return Result.fail(上传失败); } // 返回可访问的 URL String url /upload/ dateDir / fileName; return Result.ok(url); }uploadPath在配置文件里指定比如D:/upload/或者/data/upload/。静态资源映射在配置类里加addResourceHandlers把/upload/**映射到实际磁盘路径。前端拿到 URL 后拼上后端地址就能显示图片。商品发布接口接收 JSON字段包括 title、description、price、categoryId、images、tradeLocation。后端做参数校验比如价格不能为负、标题不能为空、图片数量不超过 9 张。校验通过后插入数据库返回商品 ID。// 商品发布接口 PostMapping(/product/publish) public Result publish(RequestBody ProductDTO dto, RequestHeader(token) String token) { // 1. 校验 token 获取用户 User user userService.getByToken(token); if (user null) { return Result.fail(请先登录); } // 2. 参数校验 if (StrUtil.isBlank(dto.getTitle()) || dto.getPrice() null) { return Result.fail(标题和价格不能为空); } if (dto.getPrice().compareTo(BigDecimal.ZERO) 0) { return Result.fail(价格不能为负); } // 3. 组装实体并保存 Product product new Product(); product.setUserId(user.getId()); product.setTitle(dto.getTitle()); product.setDescription(dto.getDescription()); product.setPrice(dto.getPrice()); product.setCategoryId(dto.getCategoryId()); product.setImages(String.join(,, dto.getImages())); product.setTradeLocation(dto.getTradeLocation()); product.setStatus(1); product.setCreateTime(new Date()); productService.save(product); return Result.ok(product.getId()); }RequestHeader(token)从请求头里取 token这是前后端约定好的。前端在wx.request的 header 里带上 token。商品状态默认设为 1在售后续用户可以在“我的发布”里手动下架。3.3 商品列表查询与分页列表查询是使用频率最高的接口。首页展示在售商品支持按分类筛选、按关键词搜索、按发布时间排序。用 MyBatis-Plus 的Page对象做分页前端传 page 和 size 两个参数。// 商品列表查询支持分类和关键词筛选 GetMapping(/product/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Integer categoryId, RequestParam(required false) String keyword) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1); // 只查在售 if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } if (StrUtil.isNotBlank(keyword)) { wrapper.like(Product::getTitle, keyword); } wrapper.orderByDesc(Product::getCreateTime); PageProduct result productService.page(new Page(page, size), wrapper); return Result.ok(result); }LambdaQueryWrapper是 MyBatis-Plus 提供的条件构造器用方法引用的方式写字段名避免手写字符串出错。orderByDesc按发布时间倒序保证最新发布的商品排在前面。分页返回的Page对象里包含records当前页数据、total总条数、current当前页码、size每页条数前端根据 total 和 size 计算总页数。注意分页查询时如果数据量小直接SELECT *没问题。但如果商品表数据超过几万条like模糊查询会变慢。毕设场景数据量不大不用过度优化但答辩时老师可能会问你可以提一句“后续可以加全文索引或者接入 Elasticsearch”。4. 小程序端页面开发从首页到订单的交互实现4.1 首页商品流与下拉刷新首页是用户打开小程序看到的第一个页面核心是一个商品列表。用scroll-view或者直接页面滚动都行配合onReachBottom实现上拉加载更多onPullDownRefresh实现下拉刷新。// pages/index/index.js Page({ data: { products: [], // 商品列表 page: 1, // 当前页码 size: 10, // 每页条数 hasMore: true, // 是否还有更多数据 categoryId: null // 当前分类 }, onLoad() { this.loadProducts(); }, // 加载商品列表 loadProducts() { const { page, size, categoryId } this.data; wx.request({ url: getApp().globalData.baseUrl /product/list, data: { page, size, categoryId }, success: (res) { if (res.data.code 200) { const records res.data.data.records; this.setData({ products: page 1 ? records : this.data.products.concat(records), hasMore: records.length size }); } } }); }, // 下拉刷新 onPullDownRefresh() { this.setData({ page: 1, hasMore: true }); this.loadProducts(); wx.stopPullDownRefresh(); }, // 上拉加载更多 onReachBottom() { if (!this.data.hasMore) return; this.setData({ page: this.data.page 1 }); this.loadProducts(); } })page 1时直接替换列表否则用concat追加。hasMore判断当前返回的条数是否等于 size如果小于 size 说明没有更多了。wx.stopPullDownRefresh()必须调用否则下拉刷新动画不会消失。4.2 商品详情页与收藏功能详情页接收商品 ID调后端接口拿商品详情同时判断当前用户是否已收藏。收藏功能用一个单独的接口传入商品 ID 和 token后端在收藏表里插入或删除记录。// pages/detail/detail.js Page({ data: { product: null, isFavorite: false }, onLoad(options) { const id options.id; this.loadDetail(id); this.checkFavorite(id); }, loadDetail(id) { wx.request({ url: getApp().globalData.baseUrl /product/detail, data: { id }, success: (res) { if (res.data.code 200) { this.setData({ product: res.data.data }); } } }); }, checkFavorite(id) { const token getApp().globalData.token; if (!token) return; wx.request({ url: getApp().globalData.baseUrl /favorite/check, data: { productId: id }, header: { token }, success: (res) { this.setData({ isFavorite: res.data.data }); } }); }, toggleFavorite() { const token getApp().globalData.token; if (!token) { wx.showToast({ title: 请先登录, icon: none }); return; } wx.request({ url: getApp().globalData.baseUrl /favorite/toggle, method: POST, data: { productId: this.data.product.id }, header: { token }, success: (res) { if (res.data.code 200) { this.setData({ isFavorite: !this.data.isFavorite }); wx.showToast({ title: this.data.isFavorite ? 已收藏 : 已取消, icon: none }); } } }); } })checkFavorite在页面加载时调用判断当前用户是否收藏过这个商品。toggleFavorite做切换后端根据当前状态决定是插入还是删除。注意wx.showToast的icon设为none时只显示文字不显示图标适合这种轻提示。4.3 订单创建与状态流转订单创建发生在买家点击“立即购买”之后。前端传商品 ID 和买家留言后端校验商品是否还在售、买家不能买自己的商品、然后生成订单记录并把商品状态改为已售。// 创建订单接口 PostMapping(/order/create) public Result createOrder(RequestBody OrderDTO dto, RequestHeader(token) String token) { User buyer userService.getByToken(token); if (buyer null) { return Result.fail(请先登录); } Product product productService.getById(dto.getProductId()); if (product null || product.getStatus() ! 1) { return Result.fail(商品已售出或不存在); } if (product.getUserId().equals(buyer.getId())) { return Result.fail(不能购买自己发布的商品); } // 创建订单 Order order new Order(); order.setBuyerId(buyer.getId()); order.setSellerId(product.getUserId()); order.setProductId(product.getId()); order.setAmount(product.getPrice()); order.setStatus(1); // 1待付款 order.setCreateTime(new Date()); orderService.save(order); // 更新商品状态为已售 product.setStatus(2); productService.updateById(product); return Result.ok(order.getId()); }订单状态流转1 待付款、2 待发货、3 待收货、4 已完成、5 已取消。买家创建订单后是待付款付款后变待发货卖家发货后变待收货买家确认收货后变已完成。毕设里可以不接真实支付模拟一个“确认付款”按钮直接改状态就行。提示订单状态变更接口要做权限校验只有买家能取消订单只有卖家能发货只有买家能确认收货。不校验的话任何人拿到订单 ID 都能改状态答辩时被问到就是硬伤。5. 避坑与排查那些让我熬夜的翻车现场5.1 微信开发者工具请求本地接口失败现象前端调wx.request请求http://localhost:8080/api/login控制台报错“不在以下 request 合法域名列表中”。原因微信开发者工具默认校验请求域名localhost 和 IP 地址都不在白名单里。这是安全策略不是代码问题。解决在开发者工具右上角“详情”面板里找到“本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。勾上之后重新编译请求就能发出去。上线前记得换成备案过的 HTTPS 域名并在微信公众平台配置服务器域名。5.2 MySQL 连接报时区错误现象Spring Boot 启动时报错The server time zone value йʱ is unrecognized或者连接成功但插入的时间比实际时间少 8 小时。原因MySQL 8.0 的驱动默认使用 UTC 时区而国内服务器通常是东八区。驱动和数据库时区不一致导致时间偏移。解决在application.yml的 JDBC URL 里加上serverTimezoneAsia/Shanghai。完整 URL 类似jdbc:mysql://localhost:3306/campus_trade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse。useSSLfalse在开发阶段关掉 SSL 可以避免证书警告。5.3 图片上传后前端显示 404现象图片上传成功后端返回了 URL但小程序里image标签加载不出来控制台显示 404。原因后端把文件存到了磁盘某个目录但没有配置静态资源映射Spring Boot 不知道这个目录下的文件可以通过 URL 访问。解决加一个配置类继承WebMvcConfigurer重写addResourceHandlers方法把/upload/**映射到实际磁盘路径。同时确认uploadPath配置的路径以斜杠结尾拼接文件名时不会少斜杠。Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到磁盘上的 uploadPath 目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }file:前缀不能少表示本地文件系统路径。如果uploadPath是D:/upload/映射后访问http://localhost:8080/upload/20250101/xxx.jpg就能拿到文件。5.4 token 过期或丢失导致接口 401现象用户登录后过一段时间再操作接口返回“请先登录”但用户觉得自己没退出。原因token 存在数据库或 Redis 里设置了过期时间或者小程序端globalData.token在页面刷新后丢失。解决小程序端在app.js的onLaunch里从wx.getStorageSync(token)恢复 token登录成功后用wx.setStorageSync(token, token)持久化。后端 token 过期时间设长一点毕设里设 7 天足够。如果 token 失效前端拦截 401 响应跳转到登录页重新登录。5.5 商品列表分页数据重复现象上拉加载更多时第二页的数据和第一页有重复。原因排序字段只用create_time如果多条商品发布时间相同MySQL 返回的顺序不稳定分页时可能把同一条数据分到两页。解决排序加一个唯一字段作为第二排序条件比如orderByDesc(Product::getCreateTime).orderByDesc(Product::getId)。这样即使发布时间相同ID 也能保证顺序唯一。或者用create_time精确到毫秒但更简单的做法就是加 ID 排序。6. 答辩前必做的三件事与一个压箱底技巧走到这一步项目基本能跑了。但能跑和能答辩是两回事。我见过太多人功能都实现了答辩时被老师问几句就卡住。下面这三件事建议在答辩前一周内做完。第一件把数据库导出成 SQL 文件连同后端源码、小程序源码一起打包。导出时用mysqldump -u root -p campus_trade campus_trade.sql注意加上--default-character-setutf8mb4避免中文乱码。打包目录结构要清晰backend/放 Java 项目miniprogram/放小程序项目database/放 SQL 文件docs/放说明文档。说明文档不用写太长把环境要求、启动步骤、默认账号写清楚就行。第二件准备一份接口清单。把每个接口的 URL、请求方式、参数、返回值列成表格。答辩时老师如果问“你这个登录怎么实现的”你可以直接翻到接口清单指给他看。表格用 Markdown 写就行不用搞得多正式。接口方法参数返回/api/loginPOSTcodetoken、用户信息/api/product/listGETpage、size、categoryId商品分页列表/api/product/publishPOSTtitle、price、images 等商品 ID/api/order/createPOSTproductId订单 ID第三件录一个三到五分钟的演示视频。从登录开始到发布商品、浏览列表、查看详情、下单、查看订单完整走一遍。视频不用剪辑得多精美手机录屏就行。答辩时如果现场网络出问题或者环境跑不起来直接放视频比干讲强。最后说一个压箱底的技巧在application.yml里把日志级别调到debug把 MyBatis-Plus 的 SQL 日志打开。这样每次请求后端控制台都会打印实际执行的 SQL联调时前端说“数据不对”你一眼就能看出是 SQL 条件写错了还是参数没传对。配置就两行mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这个习惯我从做第一个项目保持到现在。每次遇到“明明代码没问题但结果就是不对”的情况打开 SQL 日志十有八九能定位到原因。希望帮到你。本文还有配套的精品资源点击获取