简介:这是一套完整的校园二手交易平台微信小程序毕业设计源码,面向计算机专业本科生及Java全栈初学者,解决高校学生闲置物品线上流转难、交易信息不对称等实际问题。系统采用SSM框架开发后端,前端基于微信原生小程序技术栈,包含管理员、卖家、用户三类角色,覆盖商品发布、求购管理、订单处理、咨询互动等核心业务闭环。资源包共1412个文件,含136个Java后端逻辑文件、148个Vue组件、199个JS交互脚本、319个PNG图标资源及配套WXML/WXSS样式文件,结构清晰、模块解耦,便于理解前后端分离架构与小程序生命周期管理;压缩包大小为25.89MB。已有162人学习下载,提供完整可运行工程(含3个bat启动脚本)、带.bak备份的Vue页面源码、数据库SQL文件及基础配置说明,开箱即用,适合课程设计复现、毕设二次开发与小程序+Java技术栈综合实践。
1. 这不是个“能跑就行”的毕业设计——它是一套可落地的校园二手交易闭环系统
你拿到的校园二手交易平台小程序.zip,表面看是几个.vue.bak文件和三支.bat脚本,但实际拆开后会发现:它用 SSM(Spring + SpringMVC + MyBatis)构建了完整后台服务层,前端基于 Vue.js 封装成微信小程序兼容结构(非 uni-app,但保留了跨端适配基础),并已预置管理员、卖家、用户三角色权限隔离逻辑。它解决的不是“做个页面交差”,而是真实校园场景下高频出现的「闲置物品流转效率低、求购信息不对称、交易信任链缺失」三大痛点。适合计算机/软件工程专业本科生做毕业设计——因为代码结构清晰、模块边界明确、数据库字段命名规范,且所有核心业务(商品发布、求购发布、订单状态机、咨询消息队列)都已实现,你只需替换微信 AppID、配置支付回调地址、补全图片 CDN 域名,就能在真机上完成从浏览→加购→下单→通知卖家的全流程。尤其对没接触过前后端联调的同学,.bat脚本封装了环境初始化关键步骤,比手动敲 Maven 命令更容错。
2. 为什么选 SSM 而非 SpringBoot?后台架构与三角色权限控制实现细节
2.1 SSM 选型的真实动因:可控性优先于开发速度
当前很多毕业设计盲目堆砌 SpringBoot + MyBatis-Plus,但本项目坚持用原生 SSM,核心原因有三点:
第一,教学友好性——SpringMVC 的@RequestMapping显式声明路径,MyBatis 的 XML SQL 易于理解执行逻辑,学生调试时能直接看到 Controller 层参数绑定、Service 层事务边界、Mapper 层 SQL 拼接过程;
第二,部署轻量性——SSM 打包为 WAR 包后可直接部署到 Tomcat 8.5+,无需内嵌容器,对学校服务器资源要求更低;
第三,权限扩展性——本项目采用自研 RBAC(基于角色的访问控制)模型,而非 Shiro 或 Spring Security 的黑盒拦截器。这意味着你能清晰看到SysRolePermission表如何关联sys_role和sys_permission,以及PermissionInterceptor如何在preHandle()中校验request.getRequestURI()是否在当前角色权限白名单内。
提示:不要被
IndexMain.vue.bak这类文件名误导——.bak是开发过程中的备份标记,并非不可用文件。实际IndexMain.vue是首页路由组件,其mounted()钩子中调用this.$http.get('/api/index/recommend')获取轮播图和推荐商品,该接口由IndexController.java的@ResponseBody @GetMapping("/recommend")方法提供。
2.2 三角色权限表结构与动态菜单生成逻辑
系统通过四张核心表实现权限控制:
sys_user(用户主表,含role_type字段:0=管理员,1=卖家,2=普通用户)sys_role(角色定义,仅 3 条记录)sys_permission(权限资源,如/admin/goods/list,/seller/order/updateStatus)sys_role_permission(中间表,记录角色与权限的多对多关系)
当用户登录后,LoginController.java的login()方法会查询其role_type,再通过RolePermissionService查询该角色拥有的全部permissionCode,最终存入HttpSession的permissions属性中。前端IndexAsideStatic.vue.bak组件在created()阶段调用this.$store.dispatch('loadMenu'),触发 Vuex 的menu.js模块读取sessionStorage.getItem('userPermissions'),再遍历menuConfig.js中预定义的菜单树(含path、name、icon、hidden字段),仅渲染permissionCode匹配的节点。
2.2.1 关键代码:动态菜单过滤逻辑(src/store/modules/menu.js)
// src/store/modules/menu.js const state = { menuList: [] } const mutations = { SET_MENU_LIST(state, list) { state.menuList = list } } const actions = { loadMenu({ commit }) { const permissions = JSON.parse(sessionStorage.getItem('userPermissions') || '[]') const allMenus = [ { path: '/admin', name: 'AdminHome', icon: 'el-icon-s-home', hidden: false, permission: 'admin:home' }, { path: '/admin/user', name: 'UserManage', icon: 'el-icon-user', hidden: false, permission: 'admin:user:list' }, { path: '/seller/goods', name: 'SellerGoods', icon: 'el-icon-goods', hidden: true, permission: 'seller:goods:list' }, { path: '/user/cart', name: 'UserCart', icon: 'el-icon-shopping-cart-full', hidden: true, permission: 'user:cart:view' } ] // 过滤:仅保留当前用户权限包含的菜单项 const filtered = allMenus.filter(item => permissions.includes(item.permission)) commit('SET_MENU_LIST', filtered) } }参数说明:
hidden: true表示该菜单默认不显示在侧边栏,但可通过路由跳转访问(如用户端的购物车页);permission字段必须与sys_permission.code完全一致,大小写敏感。若修改权限码,需同步更新数据库和此处字符串。
2.3 后台管理接口的 RESTful 设计规范
所有后台接口遵循统一前缀/api/{module}/{action},例如:
GET /api/admin/user/list?page=1&limit=10→ 管理员用户列表分页POST /api/seller/goods/publish→ 卖家发布闲置商品(Body 含title,price,categoryId,images数组)PUT /api/user/order/{id}/pay→ 用户确认支付(触发状态机:unpaid→paid)
关键点在于SellerOrderController.java中的订单状态变更方法:
// SellerOrderController.java @PostMapping("/updateStatus") @ResponseBody public Result updateOrderStatus(@RequestBody Map<String, Object> params) { String orderId = (String) params.get("orderId"); String status = (String) params.get("status"); // 可选值:shipped, received, cancelled Order order = orderService.getById(orderId); // 状态机校验:禁止越级变更(如 unpaid → received) if (!OrderStatus.isValidTransition(order.getStatus(), status)) { return Result.fail("非法状态变更:" + order.getStatus() + " → " + status); } order.setStatus(status); order.setUpdateTime(new Date()); orderService.updateById(order); return Result.success(); }注意:
OrderStatus.java中定义了严格的状态流转规则:unpaid → paid → shipped → received,cancelled可从任意状态进入。若测试时发现状态无法更新,先检查order.getStatus()当前值是否符合isValidTransition()的判定逻辑。
3. 前端小程序化改造与三支.bat脚本的底层作用解析
3.1 从 Vue Web 项目到微信小程序的适配策略
本项目前端并非原生微信小程序(WXML/WXSS),而是基于 Vue CLI 构建的 H5 项目,通过wx-miniprogram-adapter库实现小程序 API 兼容。核心适配点有三处:
- 路由系统:
router/index.js中mode: 'hash'改为mode: 'history',并启用base: '/miniapp/',使https://domain.com/miniapp/#/user/cart可被微信 WebView 正确加载; - API 请求封装:
src/utils/request.js中axios.defaults.baseURL = '/api',但实际请求时由wx.request()代理,需在main.js初始化时调用wxMiniprogramAdapter.init()注入适配器; - 组件样式隔离:
main.css.bak中所有选择器均添加[data-wechat]属性前缀(如[data-wechat] .header { ... }),避免与微信原生组件样式冲突。
提示:
BreadCrumbs.vue.bak是面包屑组件,其props接收crumbs: Array<{name: string, path: string}>,在IndexHeader.vue.bak中通过<bread-crumbs :crumbs="currentCrumbs"/>调用。若修改导航层级,需同步更新currentCrumbs的计算属性逻辑。
3.2 三支.bat脚本的真实作用与执行顺序
| 脚本名 | 执行时机 | 核心命令 | 关键作用 |
|---|---|---|---|
1-install.bat | 首次部署前 | mvn clean install -Dmaven.test.skip=true | 编译 Java 代码,打包 WAR,跳过单元测试(毕业设计通常无测试用例) |
2-run.bat | 开发调试时 | mvn spring-boot:run(注:虽为 SSM,但项目引入了spring-boot-maven-plugin插件) | 启动内嵌 Tomcat,监听8080端口,自动热部署src/main/resources下的配置文件 |
3-build.bat | 发布上线前 | call 1-install.bat && call 2-run.bat && pause | 串联编译与启动流程,末尾pause防止窗口闪退,便于查看控制台日志 |
3.2.12-run.bat的隐藏配置项
2-run.bat内部实际调用的是mvn spring-boot:run -Dserver.port=8080 -Dspring.profiles.active=dev,其中:
-Dserver.port=8080指定服务端口,若端口被占用,需修改为8081并同步更新src/main/resources/application-dev.yml中的server.port;-Dspring.profiles.active=dev激活开发配置,此时application-dev.yml中的spring.datasource.url指向本地 MySQL(如jdbc:mysql://localhost:3306/campus_secondhand?useSSL=false&serverTimezone=Asia/Shanghai)。
注意:
application-prod.yml中的数据库密码为明文password: root,上线前必须修改为强密码并使用mysql_config_editor加密存储,或改用 JNDI 数据源。
3.3 微信小程序端的登录态穿透机制
用户在小程序中点击「微信登录」后,前端调用wx.login()获取code,再 POST 到/api/user/wxLogin接口。该接口在UserController.java中处理:
@PostMapping("/wxLogin") @ResponseBody public Result wxLogin(@RequestBody Map<String, String> params) { String code = params.get("code"); // 调用微信接口换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + wechatAppId + "&secret=" + wechatSecret + "&js_code=" + code + "&grant_type=authorization_code"; String response = HttpUtil.get(url); // 使用 Hutool 工具类 JSONObject json = JSON.parseObject(response); String openid = json.getString("openid"); // 根据 openid 查询或创建用户 User user = userService.getByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setCreateTime(new Date()); userService.save(user); } // 生成 JWT Token(有效期 7 天) String token = JwtUtil.sign(user.getId().toString(), user.getOpenid()); return Result.success().put("token", token).put("userId", user.getId()); }关键参数:
wechatAppId和wechatSecret必须在application-dev.yml中配置,否则json.getString("openid")将为null,导致后续所有接口返回401 Unauthorized。若微信接口返回{"errcode":40029,"errmsg":"invalid code"},说明code已过期(5分钟有效)或已被使用过一次。
4. 商品发布与求购匹配的核心算法及数据库优化实践
4.1 闲置商品与求购需求的双向匹配逻辑
系统未采用 Elasticsearch 实现全文检索,而是通过 MySQL 的LIKE与FULLTEXT混合策略提升搜索效率。关键表结构如下:
-- goods 表(闲置商品) CREATE TABLE `goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '商品标题', `description` text COMMENT '商品描述', `price` decimal(10,2) NOT NULL, `category_id` int(11) NOT NULL, `user_id` bigint(20) NOT NULL, `status` tinyint(4) DEFAULT '1' COMMENT '1=上架,0=下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), FULLTEXT KEY `ft_title_desc` (`title`,`description`) ) ENGINE=InnoDB; -- demand 表(商品求购) CREATE TABLE `demand` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '求购标题', `min_price` decimal(10,2) DEFAULT NULL, `max_price` decimal(10,2) DEFAULT NULL, `category_id` int(11) NOT NULL, `user_id` bigint(20) NOT NULL, `status` tinyint(4) DEFAULT '1' COMMENT '1=有效,0=失效', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB;当用户在首页搜索框输入关键词(如「MacBook」)时,前端发送GET /api/goods/search?keyword=MacBook,后端GoodsController.java执行:
@GetMapping("/search") @ResponseBody public Result searchGoods(@RequestParam String keyword) { // 方案1:全文检索(高相关度,但需 MySQL 5.6+ 且表引擎为 MyISAM/InnoDB) List<Goods> goodsList = goodsService.lambdaQuery() .apply("MATCH(title, description) AGAINST(? IN NATURAL LANGUAGE MODE)", keyword) .list(); // 方案2:模糊匹配(兼容性更好,但性能略低) if (goodsList.isEmpty()) { goodsList = goodsService.lambdaQuery() .like("title", keyword).or().like("description", keyword) .eq("status", 1) .list(); } return Result.success(goodsList); }注意:
MATCH...AGAINST在中文分词效果较差,建议对title字段单独建立FULLTEXT索引,并在插入数据时对标题做简单分词(如按空格、顿号、逗号切分),将关键词存入goods_keyword辅助表,实现更精准匹配。
4.2 订单状态机与库存扣减的并发安全控制
商品购买涉及「库存扣减」与「订单创建」两个操作,必须保证原子性。本项目采用数据库乐观锁方案,在goods表中增加version字段:
ALTER TABLE `goods` ADD COLUMN `version` int(11) DEFAULT '0';OrderService.java中的下单方法:
@Transactional(rollbackFor = Exception.class) public boolean createOrder(Long userId, Long goodsId, Integer quantity) { // 1. 查询商品(带 version) Goods goods = goodsMapper.selectById(goodsId); if (goods.getStock() < quantity) { throw new RuntimeException("库存不足"); } // 2. 扣减库存(where version = ?) int updated = goodsMapper.update(null, new UpdateWrapper<Goods>() .setSql("stock = stock - " + quantity) .set("version", goods.getVersion() + 1) .eq("id", goodsId) .eq("version", goods.getVersion())); // 乐观锁条件 if (updated == 0) { throw new RuntimeException("库存扣减失败,请重试"); } // 3. 创建订单 Order order = new Order(); order.setUserId(userId); order.setGoodsId(goodsId); order.setQuantity(quantity); order.setStatus("unpaid"); order.setCreateTime(new Date()); orderMapper.insert(order); return true; }参数说明:
version字段每次更新自增 1,UPDATE ... WHERE id = ? AND version = ?语句若返回影响行数为 0,说明其他线程已抢先更新,当前事务需回滚重试。测试时可用 JMeter 模拟 100 并发请求同一商品,验证是否出现超卖。
4.3 MySQL 查询性能瓶颈与索引优化清单
针对高频查询场景,必须添加以下复合索引:
| 查询场景 | SQL 示例 | 推荐索引 |
|---|---|---|
| 管理员查看某分类下所有商品 | SELECT * FROM goods WHERE category_id = 5 AND status = 1 ORDER BY create_time DESC | ALTER TABLE goods ADD INDEX idx_cat_status_time (category_id, status, create_time DESC) |
| 卖家查看自己所有订单 | SELECT * FROM orders WHERE seller_id = 1001 AND status IN ('shipped','received') | ALTER TABLE orders ADD INDEX idx_seller_status (seller_id, status) |
| 用户查看我的收藏 | SELECT g.* FROM goods g JOIN user_fav f ON g.id = f.goods_id WHERE f.user_id = 2001 | ALTER TABLE user_fav ADD INDEX idx_user_goods (user_id, goods_id) |
提示:执行
EXPLAIN SELECT ...查看type是否为ref或range,key是否命中索引。若Extra列出现Using filesort或Using temporary,说明排序或分组未走索引,需调整索引字段顺序。
5. 修改刚进入的加载页面与动态设置小程序顶部标题的实操技巧
5.1 替换默认 loading 页面:从index.html到splash.vue
微信小程序首次加载时显示白屏,本项目通过src/views/splash.vue实现自定义启动页。其核心逻辑是:
- 在
main.js中,new Vue({ router, store }).$mount('#app')前插入Splash.show(); Splash.vue组件内嵌<img src="@/assets/logo.png" class="logo">和<div class="loading-text">校园二手平台加载中...</div>;- 加载完成后调用
Splash.hide(),再挂载主应用。
5.1.1splash.vue关键代码(支持渐隐动画)
<!-- src/views/splash.vue --> <template> <div v-show="show" class="splash" @click="hide"> <img :src="logo" alt="logo" class="logo" /> <div class="loading-text">{{ text }}</div> </div> </template> <script> export default { name: 'Splash', data() { return { show: false, logo: require('@/assets/logo.png'), text: '校园二手平台加载中...' } }, methods: { show() { this.show = true // 模拟资源加载耗时(实际应监听 axios.all 或图片 onload) setTimeout(() => { this.hide() }, 1500) }, hide() { this.show = false // 触发主应用挂载 this.$emit('ready') } } } </script> <style scoped> .splash { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 9999; display: flex; flex-direction: column; justify-content: center; align-items: center; transition: opacity 0.3s ease; } .logo { width: 120px; height: 120px; margin-bottom: 20px; } .loading-text { font-size: 16px; color: #666; } </style>注意:
require('@/assets/logo.png')中的路径必须为相对路径,若替换新 logo,需将图片放入src/assets/目录并修改require()参数。setTimeout时间可根据实际网络环境调整,生产环境建议改为监听document.fonts.load()或window.addEventListener('load')。
5.2 动态设置微信小程序顶部标题栏
微信小程序的navigationBarTitleText默认在app.json中静态配置,但本项目通过wx.setNavigationBarTitle()实现页面级动态设置。以商品详情页为例:
<!-- src/views/goods/detail.vue --> <script> export default { name: 'GoodsDetail', data() { return { goods: {} } }, mounted() { this.fetchGoods() }, methods: { fetchGoods() { this.$http.get(`/api/goods/${this.$route.params.id}`).then(res => { this.goods = res.data // 动态设置标题(注意:需在 DOM 渲染后调用) this.$nextTick(() => { wx.setNavigationBarTitle({ title: this.goods.title.substring(0, 12) + '...' // 限制长度防溢出 }) }) }) } } } </script>关键参数:
wx.setNavigationBarTitle()的title字符串长度建议 ≤12 个汉字(微信限制 32 字节),超出部分用...截断。若页面切换时标题未更新,检查是否遗漏this.$nextTick()—— 因为wx.setNavigationBarTitle()需等待 DOM 渲染完成才能生效。
5.3 微信小程序顶部导航栏高度适配方案
不同机型导航栏高度不同(iPhone X+ 为 88px,Android 普通机型为 48px),本项目通过wx.getSystemInfoSync()获取statusBarHeight和navigationBarHeight,动态计算内容区padding-top:
// src/utils/nav-height.js export function getNavHeight() { const systemInfo = wx.getSystemInfoSync() const isIOS = systemInfo.system.indexOf('iOS') !== -1 const statusBarHeight = systemInfo.statusBarHeight || 20 const navigationBarHeight = isIOS ? 44 : 48 return statusBarHeight + navigationBarHeight } // 在 main.css.bak 中全局设置 :root { --nav-height: 64px; /* 默认值 */ } [data-wechat] .content { padding-top: var(--nav-height); }然后在main.js中注入:
// main.js import { getNavHeight } from '@/utils/nav-height' document.documentElement.style.setProperty('--nav-height', getNavHeight() + 'px')提示:
getNavHeight()返回值需在App.onLaunch()中调用,确保首次加载即生效。若systemInfo.statusBarHeight为undefined,说明微信客户端版本过低(< 7.0.0),应降级为固定值64px。
本文还有配套的精品资源,点击获取