ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

二手交易小程序全栈拆解:Spring Boot + 微信小程序毕设实战

二手交易小程序全栈拆解:Spring Boot + 微信小程序毕设实战 简介微信小程序二手物品交易系统源码与数据库面向高校毕业设计及Java课程设计场景为需要完成类似选题的学生提供一套可直接运行学习的完整项目。源码经过本地编译验证下载后配置JDK、MySQL及微信开发者工具环境即可运行功能模块覆盖用户登录、商品发布、浏览、搜索、收藏、下单等二手交易核心流程整体设计满足老师验收要求。资源包共1154个文件包体约17MB包含Java后端源码、微信小程序前端页面、SQL数据库脚本及配置文件其中png/jpg/gif为界面与商品图片js/wxml/wxss为小程序页面逻辑与样式java/class为后端业务代码xml/json为配置与数据交换。已有405人学习下载适合毕业设计参考、项目复现或二次开发借助完整目录结构可快速定位前后端代码与数据库脚本节省环境搭建和编码时间。1. 毕业设计里最常见的二手交易小程序其实拆开看就三件事二手物品交易在毕业设计和课程设计里属于出现频率最高的一类选题功能链路完整业务深度适中既不会简单到没东西写也不会复杂到一个人做不完。这套源码的价值不在于页面视觉效果而在于它把前后端分离、登录态管理、订单状态机这些生产环境常见的结构放进了一个能本地直接跑通的小工程里。项目包含微信小程序前端、Java后端和数据库脚本核心围绕用户登录、商品发布、订单流转、收藏管理四个模块展开。适合正在做毕设或课设的人下载后先跑通再二次开发也适合刚学完 Java 基础、想看看完整全栈项目怎么组织代码的开发者。拿到手先不要急着改功能把数据库表和请求链路理清楚后续所有改造都会顺很多。2. 数据库表设计与订单状态机先把业务模型拆清楚2.1 技术选型为什么是 Spring Boot MyBatis-Plus 微信原生小程序这个量级的项目用微信原生小程序加 Spring Boot 的组合是最稳妥的。微信原生开发工具打开即编译不需要像 uniapp 那样先配 HBuilderX 再做条件编译在毕设答辩现场出问题的概率更低。后端 Spring Boot 生态成熟MyBatis-Plus 提供分页查询和代码生成能省掉大量重复的 SQL 编写工作。数据库用 MySQL 5.7 或 8.0 都可以JDK 建议用 1.8 或 11这套源码按本机编译环境打包下载后先检查 JDK 和 Maven 版本是否一致再启动能避开大部分「编译报错」的问题。为什么不建议在这个项目里引入 Redis 做缓存单体应用加缓存会引入缓存与数据库一致性维护的成本毕设答辩时反而容易被追问「缓存穿透怎么办」。先用单库单表把业务跑通把 JWT 鉴权和订单状态机这两个核心点讲清楚已经足够支撑一篇合格的毕业设计论文。2.2 核心表结构用户、商品、订单、收藏、轮播图打开数据库脚本后重点关注前五张表用户表、商品表、订单表、收藏表、轮播图表。商品表是整个业务的核心字段设计直接影响买家端筛选和卖家端管理。核心建表语句如下CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 卖家ID关联user表, title VARCHAR(100) NOT NULL COMMENT 商品标题, description TEXT COMMENT 商品描述, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价用于显示折扣, cover_image VARCHAR(255) DEFAULT NULL COMMENT 封面图URL, images TEXT COMMENT 多图URLJSON数组格式存储, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在售 1已下架 2已售出, view_count INT NOT NULL DEFAULT 0 COMMENT 浏览数列表页直接展示, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status_create (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT二手商品表;价格字段必须用 DECIMAL 而不是 FLOAT。float 在比较运算时存在精度误差订单金额出问题在答辩时很难解释。images 字段用 JSON 数组字符串存储多张图片路径比如[/uploads/1.jpg,/uploads/2.jpg]查询出来后在 Java 里用 Fastjson 转成 List 即可不需要单独建一张商品图片表——毕设级别的图片数量用这种方式更简单。订单表字段需要同时冗余商品标题、商品封面和交易价格即使卖家后来删除了商品订单详情页依然能正常显示。如果通过关联查询去拿商品信息商品被删后订单页就空了。表名职责关键约束与索引user用户信息含 openid、昵称、头像openid 加唯一索引登录时按 openid 查goods商品信息含价格、图片、状态user_id 索引status 与 create_time 组合索引orders交易订单含买卖双方、商品快照order_no 唯一索引buyer_id 与 seller_id 分别建索引favorite收藏关系用户与商品的关联user_id goods_id 联合唯一索引banner首页轮播图status 字段控制是否展示收藏表加联合唯一索引是有意义的能防止用户对同一商品重复收藏。插入时用INSERT IGNORE或先查后插都可以前者性能更好且代码更少。用户表不在登录时写入手机号而是在用户主动填写个人资料时更新避免微信授权弹窗过多导致注册流失。2.3 订单状态流转从「待付款」到「已完成」订单状态是这个项目里最有含金量的设计。毕设场景不接入真实微信支付所以付款动作通常是模拟的但状态机本身要完整。定义如下状态public enum OrderStatus { PENDING_PAYMENT(0, 待付款), PENDING_DELIVERY(1, 待发货), PENDING_RECEIPT(2, 待收货), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; // 构造方法省略 }流转规则是买家下单创建订单初始状态为待付款买家点击「模拟支付」后变为待发货卖家发货后变为待收货买家确认收货后变为已完成。买家或卖家在待付款阶段可以取消订单状态变为已取消。待发货之后买家不能单方面取消必须走「申请退款」流程——这个边界条件在答辩时常被问到代码里要处理。防止商品被重复下单是另一个关键点。下单时不能只查商品状态要用条件更新原子地完成「检查并修改」UPDATE goods SET status 2 WHERE id #{goodsId} AND status 0这条 SQL 影响行数为 1 说明抢购成功为 0 说明商品已被买走或下架。用 UPDATE 的行数作为判定依据避免了并发场景下的竞态条件这是比先 SELECT 再 UPDATE 更规范的做法。3. 后端接口登录鉴权、商品发布与订单流程的 Java 实现3.1 微信登录 code2Session 换取 openid 并签发 JWT后端登录接口的逻辑是小程序端调用wx.login()拿到临时 code传给后端后端用 code 去微信服务器换 openid 和 session_key。不要在客户端自行解析用户信息作为身份凭证openid 才是用户在微信体系内的唯一标识客户端传过来的任何 ID 都不可信。核心代码RestController RequestMapping(/api/auth) public class AuthController { Resource private UserMapper userMapper; Resource private JwtUtil jwtUtil; PostMapping(/login) public Result login(RequestBody LoginRequest request) { // 1. 用 code 换取 openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code request.getCode() grant_typeauthorization_code; String response HttpUtil.get(url); JSONObject json JSON.parseObject(response); String openid json.getString(openid); // 2. 根据 openid 查找用户不存在则注册 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户 openid.substring(0, 6)); userMapper.insert(user); } // 3. 签发 JWT token String token jwtUtil.generateToken(user.getId()); return Result.success(token); } }逻辑说明第一步用HttpUtil.get调用微信接口实际项目中建议把 appId 和 appSecret 放到application.yml配置里不要硬编码在 Java 类中。第二步查不到 openid 就自动注册新用户这里不用先查再插是因为 openid 有唯一索引兜底重复插入会抛异常但正常流程下同一用户不会同时发起两次登录。第三步签发的 token 里只放 userId不放 openid 和头像昵称保持 token 体积小。前端拿到 token 后存入wx.setStorageSync后续所有请求在 header 里带Authorization: Bearer token。后端用拦截器统一校验 token 并解析出 userId写入 ThreadLocal 供 Controller 直接使用。这样每个接口的代码里不需要重复写解析逻辑拦截器代码如下public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { throw new BusinessException(401, 未登录); } String token auth.substring(7); Integer userId jwtUtil.parseToken(token); if (userId null) { throw new BusinessException(401, 登录已过期); } UserContext.set(userId); return true; } }拦截器要注册到 WebMvcConfigurer 中并配置excludePathPatterns放行/api/auth/login。注意放行路径要写在排除列表里否则登录接口也会被拦导致小程序首次登录请求直接 401。3.2 发布商品接口参数校验与图片路径处理发布商品是卖家端的核心操作。前端通过wx.uploadFile上传图片到后端后端返回图片 URL商品文字信息走 JSON 提交。图片上传和商品发布分开处理上传失败时不会产生「商品信息已保存但图片缺失」的半成品数据。发布接口实现PostMapping(/goods) public Result publish(RequestBody Goods goods) { // 1. 基础参数校验 if (goods.getPrice() null || goods.getPrice().compareTo(BigDecimal.ZERO) 0) { return Result.error(价格必须大于0); } if (StringUtils.isBlank(goods.getTitle()) || goods.getTitle().length() 50) { return Result.error(标题不能为空且不超过50字); } // 2. 补充卖家信息 Integer userId UserContext.get(); goods.setUserId(userId); goods.setStatus(0); // 3. 保存 goodsMapper.insert(goods); return Result.success(goods.getId()); }参数说明价格为 0 或负数直接拒绝标题长度限制 50 字这两个校验能挡住大部分异常请求。UserContext.get()从拦截器写入的 ThreadLocal 取当前登录用户不需要前端传 userId防止用户伪造他人身份发布商品。图片列表在Goods实体中是 String 类型前端提交前把图片路径数组JSON.stringify后再放进请求体。发布成功后在售列表的 SQL 查询要注意索引利用率。列表页筛选条件按 status 过滤在售商品按 create_time 倒序排列之前在 goods 表建的idx_status_create组合索引正好命中这两个字段。分页用 MyBatis-Plus 的Page对象PageGoods page new Page(current, size, true); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 0) .orderByDesc(Goods::getCreateTime); goodsMapper.selectPage(page, wrapper);current是页码size是每页条数小程序端列表触底时current 1继续请求。true表示自动查询总条数用于前端判断是否还有更多数据可以加载。3.3 下单与状态更新事务控制创建订单接口需要同时操作商品表和订单表必须加Transactional保证原子性。核心逻辑是先条件更新商品状态影响行数为 1 才创建订单否则返回「商品已被购买」。创建订单时把商品标题、封面、价格冗余到订单表生成唯一的订单号规则可以用时间戳加用户 IDTransactional(rollbackFor Exception.class) public Order createOrder(Integer goodsId) { Integer buyerId UserContext.get(); // 1. 原子更新商品状态防止并发重复下单 Goods goods goodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 0) { throw new BusinessException(商品不存在或已下架); } int rows goodsMapper.updateStatus(goodsId, 2, 0); if (rows 0) { throw new BusinessException(手慢了商品已被别人买走); } // 2. 创建订单冗余商品快照 Order order new Order(); order.setOrderNo(generateOrderNo(buyerId, goodsId)); order.setGoodsId(goodsId); order.setBuyerId(buyerId); order.setSellerId(goods.getUserId()); order.setTitle(goods.getTitle()); order.setCoverImage(goods.getCoverImage()); order.setPrice(goods.getPrice()); order.setStatus(0); // 待付款 orderMapper.insert(order); return order; }部分代码省略了Transactional注解的位置——注解要加在 public 方法上且不能同类内自调用否则事务不生效。updateStatus对应之前那条件更新 SQL第三个参数0是期望的当前状态。可以加一个兜底如果goods.getStatus() ! 0提前做一次业务校验避免无意义的 SQL 执行。事务的一个思维陷阱是很多人以为加Transactional就能保证一切但这个场景真正保证不超卖的是那条UPDATE ... WHERE status 0条件更新语句事务只是保证「扣库存」和「建订单」要么都成功要么都失败两者职责不同。4. 小程序端token 管理、商品列表渲染与订单双视角4.1 wx.request 封装与登录态生命周期小程序端最核心的封装是一个统一的请求函数。所有页面不再直接调用wx.request而是走这个封装统一处理 token 注入、业务错误提示和登录过期跳转。代码如下const request (url, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) wx.request({ url: http://localhost:8080${url}, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.statusCode 401) { // token 过期清空并回登录页 wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) reject(new Error(登录已过期)) return } if (res.data.code ! 200) { wx.showToast({ title: res.data.msg, icon: none }) reject(new Error(res.data.msg)) return } resolve(res.data) }, fail: (err) { wx.showToast({ title: 网络请求失败, icon: none }) reject(err) } }) }) }说明baseURL 如果写死localhost会导致真机调试无法访问因为真机上 localhost 指向手机自身。开发阶段用微信开发者工具的「不校验合法域名」选项可以绕过域名限制真机调试时把localhost换成电脑的局域网 IP例如http://192.168.1.100:8080前后端在同一 Wi-Fi 下即可联调。401 处理很关键。后端 token 过期返回 401 时前端不能只在控制台打印要清空本地 token 并跳转登录页。这里有个容易被忽略的细节多个请求同时返回 401 时会触发多次navigateTo跳转导致页面栈混乱。常见做法是用一个布尔变量控制跳转只执行一次或者在跳转前检查当前页面是否已经是登录页。4.2 首页商品列表与触底分页加载首页列表的逻辑是页面加载时请求第一页数据下拉刷新时重置页码重新加载触底时页码加一继续请求。代码Page({ data: { goodsList: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadGoods() }, onPullDownRefresh() { this.setData({ page: 1, hasMore: true }) this.loadGoods(() wx.stopPullDownRefresh()) }, onReachBottom() { if (this.data.hasMore !this.data.loading) { this.setData({ page: this.data.page 1 }) this.loadGoods() } }, loadGoods(callback) { if (this.data.loading) return this.setData({ loading: true }) request(/api/goods?current${this.data.page}size${this.data.pageSize}) .then((result) { const list result.data.records const newList this.data.page 1 ? list : this.data.goodsList.concat(list) this.setData({ goodsList: newList, hasMore: result.data.pages this.data.page }) }) .finally(() { this.setData({ loading: false }) if (callback) callback() }) } })逻辑说明请求参数current对应后端 MyBatis-Plus 的页码size对应每页条数。pages是后端返回的总页数当前页小于总页数时hasMore为 true页面的onReachBottom才会继续触发请求。loading标记防止触底事件在请求未返回时重复触发多次请求这个在高频滚动时很容易踩坑。图片懒加载方面image标签设置lazy-load属性用户滚动到可视区域才加载图片。商品卡片组件里价格用红色加粗展示原价用灰色删除线点击卡片跳转详情页时带上id参数const goDetail (e) { const id e.currentTarget.dataset.id wx.navigateTo({ url: /pages/goods/detail?id${id} }) }4.3 「我买到的」与「我发布的」订单双视角管理订单模块是这个项目里页面结构最有特点的地方。同一个订单列表页通过 URL 参数区分是买家视角还是卖家视角。买家看到的是「我买到的」卖家看到的是「我发布的含卖出记录」。后端接口设计为同一个接口根据是否传入 sellerId 走不同查询条件GetMapping(/orders) public Result listOrders(Integer buyerId, Integer sellerId, Integer current, Integer size) { LambdaQueryWrapperOrder wrapper new LambdaQueryWrapper(); if (buyerId ! null) { wrapper.eq(Order::getBuyerId, buyerId); } if (sellerId ! null) { wrapper.eq(Order::getSellerId, sellerId); } wrapper.orderByDesc(Order::getCreateTime); PageOrder page orderMapper.selectPage(new Page(current, size), wrapper); return Result.success(page); }前端页面通过wx.navigateTo跳转时传typebuy或typesell页面获取到类型后决定调用哪个参数。订单卡片组件复用同一个模板根据订单状态字段显示不同的操作按钮待付款买家看到「去支付」「取消订单」待发货买家看到「提醒发货」卖家看到「确认发货」待收货买家看到「确认收货」已完成买家看到「查看评价」卖家看到「查看评价」每个订单项都带一个倒计时或状态高亮待付款订单超过 30 分钟自动关闭。这个功能在毕设里可以用「前端布局时判断创建时间」实现不用专门写定时任务——真正定时任务涉及分布式锁工作量超出毕设范围答辩时能说清楚边界就行。5. 本地复现与联调验证把「能跑」变成「可验证」5.1 启动顺序与配置核对整套项目本地运行遵循严格的启动顺序先导入数据库再启动后端最后打开微信开发者工具。顺序反了会出现「前端请求失败」「数据库连接池启动报错」这类难以定位的问题。数据库导入方式mysql -u root -p -e CREATE DATABASE second_hand DEFAULT CHARACTER SET utf8mb4 mysql -u root -p second_hand second_hand.sql导入后打开本地数据库工具Navicat 或其他核对表数量和表名前缀是否与后端实体类一致。常见的坑是 SQL 文件里表名是goods实体类注解写TableName(goods)能对上但如果 SQL 导入时带上了CREATE DATABASE语句导致导到了错误的库后端一样能起来但列表全是空的。检查数据源配置spring: datasource: url: jdbc:mysql://localhost:3306/second_hand?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456characterEncodingutf8mb4参数必须有否则中文商品标题在数据库中会变成乱码前端展示出「???」时先查这里。后端起不来时看控制台报错优先排查端口被占用和数据库账号密码不匹配。端口占用在本地很常见换端口或杀掉占用进程即可Java 工程改server.port在 application.yml 里改。5.2 用开发者工具 Network 面板验证一条完整请求链路验活不只看页面有没有渲染出来要看请求链路上每一步是否真的发生了。微信开发者工具自带的 Network 面板能看小程序发出的所有请求比后端打印日志更直接。这里给一个可执行的下单链路验证步骤在开发者工具点击「登录」Network 面板出现POST /api/auth/login点开看请求体里有code响应里有token同时 Storage 面板里能看到本地存上了同一个 token。点击「发布」按钮填入商品信息提交Network 面板出现POST /api/goods请求头里Authorization字段前缀是Bearer响应返回新商品 ID。打开 Navicat 执行SELECT id, title, status FROM goods ORDER BY id DESC LIMIT 5能看到刚发布的商品记录status 为 0。用另一个账号或退出重新登录浏览该商品并下单执行SELECT * FROM orders WHERE goods_id 刚才的ID确认买家 ID、卖家 ID、价格三个字段都正确。这套验证能同时确认后端接口、数据库脚本、前端传参三块没有问题。如果某一步断掉错误会指向明确的位置请求没发出是前端问题请求发出但报 500 是后端代码或 SQL 问题请求 200 但数据库没数据是事务没提交或提交后回滚了。5.3 真机调试与抓包对比开发者工具正常但手机不行的排查思路开发者工具里接口全部正常但一上真机就请求失败这个现象在毕设阶段出现频率出奇地高。原因基本都是「localhost」指向了自己开发者工具里 localhost 是电脑真机上 localhost 是手机。把request封装的 baseURL 换成电脑局域网 IP比如http://192.168.31.45:8080然后手机和电脑连同一个 Wi-Fi。换完 IP 还不行就用 Charles 对手机流量做代理抓包对比开发者工具和真机发出的请求报文差异重点看 Host 字段和实际连接的服务地址。真机调试时可以额外开一个后端日志终端窗口观察请求是否真的到了服务端——如果日志没打印说明请求被手机系统或网络环境拦截问题在链路层面不在项目代码里。按「清微信缓存、重启微信、确认后端启动参数」三个动作再走一遍大多数环境类问题能直接定位到原因。微信开发者工具的「真机调试」功能配合 Network 面板是定位这类前后端联调问题最高效的方式。本文还有配套的精品资源点击获取
返回列表