简介:基于Spring Boot与uniapp的校园二手交易微信小程序源码包,属于前后端分离的毕设级完整工程,适合计算机相关专业学生、小程序开发者以及需要快速搭建校园交易场景的技术人员。项目覆盖用户浏览与购买、商家发布与商品管理、后台管理员审核与数据维护等完整业务链路,可作为项目开发或二次改造的直接基础。压缩包共1444个文件,大小约25.24MB,前端以vue、js、json、scss等文件为主,后端以java、xml、yml等文件为主,另含sql数据库脚本及png、jpg界面截图,便于对照页面理解数据表结构与交互逻辑。目前已有111人浏览学习,包内提供完整前后端工程、数据库初始化脚本、配置文件与环境说明、打包运行批处理文件,目录按后台、商家端、用户端分层组织,可帮助读者快速定位商品管理、买卖交流、系统管理等模块的实现细节。整体上手门槛适中,适合具备Spring Boot与Vue基础、希望从零整合全栈项目的学习者。
1. 校园二手小程序:为什么值得用 Spring Boot + uniapp 这对组合
每年毕业季前后,校园里的二手交易需求会突然爆发:考研资料、电动车、宿舍小家电、教材,这些东西在闲鱼上挂出去,要么没人看,要么需要跨校区跑腿。更麻烦的是,闲鱼没有“同一校园”这个天然信任半径,买家卖家互相猜忌,交易成本反而高。于是很多学校的学生团队或毕设项目,都会考虑做一个只服务本校的二手交易微信小程序。
技术选型上,如果只有小程序端,用微信云开发最快;但一旦需要自定义审核流程、复杂订单状态机、对接校内支付或后续扩展 App,还是 Spring Boot + uniapp 这套组合更稳。Spring Boot 负责提供 RESTful 接口、权限校验和业务状态流转,uniapp 负责一套代码同时输出微信小程序、H5 和 Android/iOS App。源码项目交付的通常也是这两个部分:一个 Java 后端工程,一个 uniapp 前端工程,外加打包小程序的配置。这篇文章面向的是有 Java 基础、想跑通整个交易链路并准备二次开发的人,我会按后端、前端、业务参数、避坑这条线把话说透。
2. Spring Boot 后端骨架:从登录鉴权到二手商品接口的落地方式
2.1 工程结构与必备依赖
拿到源码后,第一步不是急着改代码,而是先确认工程分层。一个标准的校园二手交易后端,至少要有 controller、service、mapper、entity、config、common 这几层。很多毕设项目把业务逻辑全堆在 controller 里,短期能跑,但你要加一个“举报”功能时就痛苦了。
依赖方面,pom.xml 里必须有 spring-boot-starter-web、mybatis-plus、mysql-connector-java、lombok 和一套鉴权方案。登录这块我用的是 Sa-Token 或 JWT,二者选一即可。Sa-Token 的好处是封装了登录态和权限,适合快速开发;JWT 则完全无状态,适合接口分散的场景。源码里如果看到的是一堆自定义拦截器,也不用慌,只要拦截器里做的是 token 校验就行。mybatis-plus 的 BaseMapper 能省掉大量单表 CRUD,尤其适合商品、订单这类基础表。
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.37.0</version> </dependency>上面两个依赖是核心。mybatis-plus 用来做数据访问,sa-token 用来做登录鉴权。如果你拿到的源码用的是 JWT + 拦截器,也是常规做法,不要强求统一。这里要说一个容易踩的坑:mybatis-plus 3.5.x 对 JDK 版本有要求,如果你的 JDK 是 8,注意选 3.5.3 之前的小版本,否则启动直接报 illegalArgumentException。
工程结构上,我建议按功能分包而不是按技术层分包。也就是说,商品相关的 controller、service、mapper 放一个 package 下,订单相关的放另一个 package 下。这样当你想把“商品”模块整个抽出去做微服务时,复制成本极低。校园项目虽然用不上微服务,但这种组织方式让新人接手时更容易定位到具体代码。
2.2 微信登录换 openid 的最小闭环
微信小程序登录的流程是固定的:前端调用uni.login拿到临时 code,后端拿这个 code 去微信的 code2Session 接口交换 openid 和 session_key。拿到 openid 后,你要做的不是把它直接返给前端,而是根据 openid 查用户表,不存在就自动注册,存在就生成一个自定义登录态 token。这个 token 才是后续所有请求头里带的凭证。
这一步很多人做错的地方在于:直接把 openid 当 token 用,或者把 openid 返给前端。openid 相当于用户的身份证号,一旦泄露,别人就能伪装成该用户调用接口。正确的做法是 openid 只留在后端,前端拿到的是一串随机 token,token 在 Redis 里关联 openid。
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private WxService wxService; @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { // 1. code 是 uni.login 返回的临时凭证,5 分钟内有效且只能用一次 String openid = wxService.code2Session(request.getCode()); // 2. 查用户表,不存在则根据 openid 创建新用户 User user = userService.findOrCreate(openid); // 3. 生成随机 token,存入 Redis,有效期 2 天 String token = UUID.randomUUID().toString().replaceAll("-", ""); redisTemplate.opsForValue().set("login:user:" + token, user.getId(), 2, TimeUnit.DAYS); return Result.success(token); } }这段代码是登录链路的骨架。code2Session 方法内部调用https://api.weixin.qq.com/sns/jscode2session,需要填 appid、secret、js_code 和 grant_type。这里有个细节:小程序端和后端要配置同一个 appid,否则登录一定失败,报错信息却很隐晦。token 用 UUID 而不是自增 ID,是为了避免被人遍历。Redis 的过期时间设 2 天是折中值:太短用户老要重新登录,太长存在安全风险。
2.3 二手商品发布与图片上传的接口设计
商品发布是二手交易的核心入口,它涉及两个接口:一个是上传图片,一个是提交商品信息。图片上传要独立出来,因为 uni.uploadFile 是单独的多媒体请求,和表单提交分开更干净。后端接收图片后,需要做文件类型校验和大小限制,常见的限制是只允许 jpg/png/webp,单张不超过 5MB。存储方式上,本地磁盘存储适合开发环境,上线后建议切到 OSS。
但本地存储在开发阶段有个好处:你能直接看到文件生成在哪个目录,方便排查“图片上传成功但访问 404”这类问题。当然后端需要配置一个资源映射,把 /upload/ 路径映射到磁盘目录。商品提交接口里,接收方要校验商品名称、描述、价格和图片 URL 列表。价格这里我建议用分而不是元来存储,避免浮点数精度问题。
@PostMapping("/publish") public Result publish(@RequestBody PublishRequest req) { // 1. 从请求头里拿 token,换出当前用户 ID Long userId = loginService.getUserIdByToken(req.getToken()); // 2. 参数校验:价格不低于 0.01 元,描述不短于 10 个字 if (req.getPrice() < 1 || req.getDescription().length() < 10) { return Result.error("价格或描述不符合要求"); } // 3. 组装商品实体,初始状态为 ON_SALE Goods goods = new Goods(); goods.setUserId(userId); goods.setTitle(req.getTitle()); goods.setPriceFen(req.getPrice()); goods.setImages(String.join(",", req.getImages())); goods.setStatus(0); return Result.success(goodsService.save(goods)); }参数校验放在 Controller 里是我故意的,因为这个小项目没有复杂到需要单独 Validation 类。价格用价格 * 100转成整数存储,在 MyBatis Plus 的实体类里对应一个 Integer 字段。图片 URL 列表用逗号拼接成一个字符串,省去建子表的麻烦,但你要知道这个设计在查询详情时需要 split 处理。另外一个容易被忽略的地方是:发布接口要做频率限制,同一个用户 5 分钟内最多发布 10 条,防刷屏。这个限流用 Redis Incr 就能实现,比引入 Sentinel 轻量得多。
中间这一套接口跑通后,前端小程序展示商品列表、发布商品、查看商品详情的主链路就已经通了。订单模块则可以做成简单的“我想要”按钮,点击后生成订单记录,双方在微信里沟通交易时间地点。
3. uniapp 端到微信小程序的映射:页面结构、请求封装与打包链路
3.1 创建 uniapp 项目时的目录约定和 manifest 配置
unisapp 项目创建后,你拿到的源码通常包含 pages、static、utils、api、store 这类目录。pages 里每个页面就是一个文件夹,内有 vue 文件和一个同名的 json 配置。pages.json 是整个小程序的页面路由表,它决定了哪些页面能通过uni.navigateTo跳转。第一次打开源码时,我建议先看 pages.json,这比看代码更快了解项目有哪些功能。
manifest.json 是 uniapp 的全局配置,微信小程序打包相关的配置全部在这里。你需要确认的是mp-weixin节点里的 appid 是否正确,还有minified是否设置为 true,这个值控制打包时是否压缩代码,会影响最终产物大小。在 uniapp 里,每个页面 vue 文件同时也是一个组件,但和组件的区别是页面有生命周期——onLoad、onShow、onReachBottom 这些是页面独有的。
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "校园二手" } }, { "path": "pages/publish/publish", "style": { "navigationBarTitleText": "发布闲置" } }, { "path": "pages/order/order", "style": { "navigationBarTitleText": "我的订单" } } ], "globalStyle": { "navigationBarTextStyle": "white", "navigationBarBackgroundColor": "#07C160" } }上面的 pages.json 配置了三件事:第一个页面是首页,发布页面,订单页面。navigationBarTextStyle设为 white,配合navigationBarBackgroundColor的绿色,是跟微信官方视觉风格对齐的做法。这里有一个新手容易踩的坑:页面 json 里的 navigationBarTitleText 里如果写了超过 7 个汉字,真机上会被截断,开发工具里却能正常显示,不注意的话会以为代码出问题了。
3.2 首页列表和发布页的请求封装写法
首页是小程序的门面,它要做的是调用后端的商品列表接口,以卡片流形式展示商品封面、标题和价格。异常处理是刚性的:接口超时、token 过期、后端 500,三种错误必须区分提示,否则用户会以为小程序卡死了。我会封装一个 uni.request 的 Promise 版本,然后在拦截器里统一处理 token 续期。
export function request(config) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + config.url, method: config.method || 'GET', data: config.data || {}, header: { 'Content-Type': 'application/json', 'token': uni.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else if (res.data.code === 401) { uni.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { uni.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络开小差了', icon: 'none' }) reject(err) } }) }) }BASE_URL 在开发环境和生产环境是不同的:开发时是http://localhost:8080,真机预览时必须改成电脑的局域网 IP,因为真机和电脑不在同一个 localhost 上。这里的 token 从 storage 里取,是模拟登录态的常规做法。showToast 的 title 不能超过 7 个汉字,超出的部分会被截断,所以错误消息要尽量精简。
发布页最麻烦的组件是图片选择器。uni.chooseImage 拿到的是临时路径,需要调用 uni.uploadFile 上传到后端,然后拿到后端返回的 URL。上传接口的返回值和普通接口不一样,它会直接返回一个 uploadFile 的成功对象,里面嵌套着后端数据。这里我吃过亏:uploadFile 的 success 回调里 res.data 是字符串,必须先用 JSON.parse 解析一遍,否则直接取 res.data.url 会得到 undefined。
uni.chooseImage({ count: 6, success: (res) => { const tasks = [] res.tempFilePaths.forEach((path, index) => { tasks.push(new Promise((resolve) => { uni.uploadFile({ url: BASE_URL + '/api/upload/image', filePath: path, name: 'file', success: (uploadRes) => { const data = JSON.parse(uploadRes.data) resolve(data.url) } }) })) }) Promise.all(tasks).then((urls) => { this.form.images = urls }) } })这段代码用了 Promise.all 来并发上传多张图片,上传完成后统一赋值给表单的 images 字段。count 设为 6 是参考了闲鱼等主流交易 App 的上限,你需要根据后端接口的容量来调整。注意这里有个性能问题:6 张图同时上传,如果有一张超时,Promise.all 会全部失败。更稳的做法是全部成功就提交,部分失败则把失败的图片剔除后重新上传。
3.3 HBuilderX 打包微信小程序与 sourceSize 超限的解法
uniapp 转微信小程序,常规流程是在 HBuilderX 里点击菜单栏的“发行”,选择“小程序-微信”,输入小程序 appid,生成 dist/build/mp-weixin 目录。然后用微信开发者工具打开这个目录,就能看到完整小程序。这套流程本身不复杂,但很多人卡在“发行失败”或“打包后体积超限”上。
微信小程序对单包体积有上限,主包不能超过 2MB,否则上传代码时会直接报错。前面热搜里的 “source size 2612kb exceed max limit 2mb” 就是这个错误。解决路径有两条:第一是压缩静态资源,把图片尽量从本地挪到 CDN 或后端服务器,本地只保留 TabBar 图标;第二是开启分包加载,把发布页、订单详情页这类非首屏页面放到 subpackages 里。
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } }, { "path": "pages/mine/mine", "style": { "navigationBarTitleText": "我的" } } ], "subpackages": [ { "root": "pages_publish", "pages": [ { "path": "publish/publish", "style": { "navigationBarTitleText": "发布闲置" } } ] } ] }subpackages 里的页面会被打入分包,用户访问到这些页面时才下载对应代码。root字段是分包目录的名字,不能和 pages 里的目录重名。我这里用pages_publish作为分包根目录,其下的路径相对于这个根目录写。分包不是万能的,tabBar 页面不能放分包里,所以首页和“我的”页面必须留在主包。静态图片也要注意:微信开发者工具里的“本地资源”越少,主包体积越小,上传到后端或图床的图片是不计入包体积的。
4. 校园交易业务的边界参数:价格、定位、订单状态怎么设才对
4.1 商品和订单的状态机设计
二手交易不是简单的下单支付,它有“挂出、被人想要、线下交易、完成、下架”这几个阶段。设计状态机时最忌讳的是用数字硬编码,比如 0 表示上架、1 表示下架,然后在 Java 代码里到处写if (status == 0)。时间一长,你会忘记 0 和 1 的具体含义,容易把下架的商品显示成可购买。
我建议用枚举类来定义状态,并且状态流转必须经过统一的 service 方法,严禁 controller 里直接改 status 字段。下面这个枚举对应了商品的常见状态:
| 状态码 | 含义 | 描述 |
|---|---|---|
| 0 | ON_SALE | 商品上架,可见可下单 |
| 1 | TAKEN | 已被下单,但未成交,其他用户不可下单 |
| 2 | SOLD | 已卖出,交易完成 |
| 3 | OFF_SHELF | 已下架,卖家手动下架或管理员下架 |
| 4 | BANNED | 违规被封禁,仅管理员可操作 |
字段用 int 保存枚举的 code,Java 层面用枚举类做转换。这样前端传status = 2时,后端必须校验商品当前状态为 TAKEN 才能流转到 SOLD,等于给状态转换上了锁。订单状态机的流转逻辑要复杂一些,因为“线下交易”这个环节需要买卖双方确认。我一般会加一个超时机制:商品被下单后 24 小时内未确认交易,自动恢复为 ON_SALE。这个定时任务用 Spring Schedule 就能做,不需要引入额外框架。
4.2 地理围栏:把交易半径锁在校园里的参数
校园二手和普通电商最大的差异在于“地理属性”。卖一辆电动车,卖家不可能邮寄,只能线下见面交易。所以在发布商品时,如果直接记录经纬度,买家搜索时就能按距离排序。但经纬度是个隐私数据,在校园场景里用户会抵触填写。我用的方案是:让用户在发布时选择“交易地点”,后端从地点映射表里查出对应的坐标,而不是用 GPS 定位。
交易半径这个参数需要根据学校大小来调。小型校区 600 米就能覆盖整个校区,大型大学可能需要 1.5 公里。放在 Spring Boot 的 application.yml 里做可配置项更合理:
market: trade-radius: 1000 # 匹配买家搜索时,最大允许的交易距离,单位米 max-price-fen: 999900 # 商品价格上限,9999 元 publish-interval-seconds: 60 # 两次发布之间的最小间隔trade-radius这个值不要设太大,超过 2 公里就到校外了,反而丧失“校园信任”的本质。前端搜索列表里我用的是射线法来计算两个坐标间的距离,有现成的 Hutool 工具类DistanceUtil可以直接调用,不必重复造轮子。如果源码里没有距离计算工具,建议在 service 层自己写个 20 行的小方法,用 Haversine 公式即可。
这里有一个边界:如果商品只在某栋宿舍楼下交易,距离计算按直线距离会误差很大。所以校内的交易半径建议只作为排序依据,而不是硬性过滤条件;真正的距离确认还是交给买卖双方聊天沟通。每当学生用户搜索不到想要的商品时,往往会认为是小程序坏了,实际是半径设小了。有反馈说“刚才还能看到,现在又没了”,多半就是这个参数被调整过。
4.3 防刷与举报的参数阈值
发布接口的限流、同一 IP 的注册限制、价格敏感词的拦截,这些不直接出现在页面上,但决定了一个交易平台能不能用。免费源码里通常不包含这些,因为写起来繁琐还看不出效果。但上线后一旦遇到刷单,你会发现没有防刷的系统一夜之间就会被垃圾商品塞满。
我建议在源码基础上至少补上四个参数:单个用户每日最大发布数(默认 20)、单条商品描述最短长度(默认 10 字)、同个商品每天被举报达到 3 次自动下架、新注册用户 24 小时内不能发布商品。这四个值都在后台可配置,不写死在代码里。
举报处理是管理员模块的事。后台管理页面里,管理员能看到举报列表,点击详情后跳转到商品页,可以选择“下架商品”或“忽略举报”。这个权限控制在 Sa-Token 里就是加一个@SaCheckPermission("goods:ban")注解的事。前端管理端用 uniapp 的 H5 版编译出来,部署到服务器后用一个浏览器访问即可,不用单独写一个 PC 管理后台。
5. 微信小程序避坑指南:真机调试、抓包、日志与编译异常排查
5.1 真机预览时无法拿到登录态:后端接口能通,但小程序里报 10002 错误
小程序在开发者工具里一切正常,预览到手机上就提示“系统错误”,后台日志里出现 10002 错误。这个 10002 通常是微信内部系统繁忙或参数错误,但更常见的原因是首次登录时 code 被重复使用。uni.login返回的 code 只能用一次,如果你在 onLoad 里调用 login,又在某个按钮点击事件里又调用一次 login,第二次用同样的 code 请求后端,微信就会返回 10002。
解决方法是保证一个登录生命周期内只调用一次uni.login。登录态存到 storage 后,后续请求都带 token,不重复登录。你可以在 App.vue 的 onLaunch 里先检查 storage 里有没有 token,有就跳过登录流程。另外一个相关问题是:后端把 token 返给前端后,前端没有存进 storage,页面一刷新登录态就丢。因为 uni.request 每次都要从 storage 取 token,没有存自然就失效了。
5.2 charles 抓包看不到微信小程序的请求
热词里出现过的 charles 抓包,是排查线上接口问题时的高频手段。微信小程序在 Android 7.0 之后默认不信任用户安装的证书,所以 charles 装了证书也抓不到 HTTPS 包,这是安全策略,不是你的配置有错。能抓到的办法是使用微信开发者工具里的 mp 调试模式,或者在 Android 设备上打开“允许抓取网络流量”的相关开关。
后端排错如果依赖抓包,更好的方式是在后端日志里加请求入口日志,把每个请求的 URL、参数、耗时都打出来。Spring Boot 的拦截器里顺手加一个 log.info 即可。
5.3 uniapp 不打印日志信息:console.log 在真机调试里没有输出
这是一个典型的“你以为代码没走,实际上走了但没回显”的问题。微信小程序真机调试时,console.log 默认输出到 vConsole 面板,而不是电脑的终端。很多同学点击预览后盯着 HBuilderX 的控制台看,等半天没有日志,误判成接口没被调用。正确姿势是打开手机上的 vConsole 悬浮窗按钮,在里面查看。
而且 uniapp 编译到小程序后,某些 console.log 会被压缩掉,特别是在发行版里。如果你发布的是体验版而非开发版,日志不输出是正常的。解决方法是使用console.error来代替console.log,error 级别的日志在微信开发者工具和 vConsole 里都一定会显示。这个细节能省下大量排查时间。
5.4 sourceSize 超限之外:主包超 2MB 的持久化对策
前面提到的分包是一个手段,但真正让主包失控的往往是页面里直接 import 了大型组件库。比如引入了一个包含完整图表库的 ui 组件,或者把几个页面公用的工具库放在了主包。持久化对策除了分包,还有按需引入和图片外链。
构建产物里有个详细体积分析:微信开发者工具“详情”面板里能看到主包和分包各占多少。当主包超出 2MB,第一步是看 common/vendor.js 是不是太大了,这个文件里是全局引用的 vue、uni 相关依赖。如果 vendor.js 超过 1MB,你需要检查是否在 main.js 里把不必要的内容注册成了全局组件。
5.5 商品图片在真机不显示,开发者工具却正常
很多小程序图片路径写的是/static/images/goods/1.jpg,开发者工具能显示,因为资源在项目里。但真机跑的是编译后的包,一旦资源体积让主包超限,图片资源会被自动压缩甚至剔除,表现为图片裂开。此时需要换思路:商品图片必须上传到后端,页面通过接口返回的 URL 来渲染,不依赖本地资源。
这里还有一个隐藏坑:后端返回的图片 URL 是http://localhost:8080/upload/xxx.jpg,在真机上 localhost 指向手机本身,自然无法访问。后端要配置一个可对外访问的域名或局域网 IP,返回前把 host 部分替换成实际地址。很多源码在这里偷懒,直接拼接常量,上线后图片全挂。
6. 从源码到上线:部署流程、验收方法和二次开发的三条习惯
源码能跑起来只算完成了 30%,剩下 70% 是部署和验收。本地起步最快的部署方式是 docker-compose 把 MySQL 和 Spring Boot 打包起来。如果你不熟悉 docker,直接在服务器上安装 MySQL,然后执行 Spring Boot 的mvn spring-boot:run或java -jar启动 jar 包也完全可以。不用为了追求部署方式而引入额外的复杂度。
验收时要走完五条主链路才能交付:微信登录、发布商品、浏览商品、下单交易、后台审核。每条链路都要在真机而不是开发者工具里跑一遍。开发者工具不能模拟弱网,也不能模拟手机存储空间不足的情况。我在交付一个校园项目时,就遇到过真机上发布商品点击“提交”后没有反应的问题,最后定位到是发布页图片 Base64 太长,超过了微信的请求体限制。这个在开发者工具里是复现不出来的。
二次开发时保持三个习惯能让你少踩坑:第一,修改数据库表结构时,先在实体类和 mapper 里同步加字段,不要只在数据库里加,否则 MyBatis Plus 的查询结果映射会报错;第二,状态流转和价格计算必须在后端完成,前端只负责展示,不要相信前端传过来的 finalPrice 字段;第三,枚举值永远用 Java 枚举类来操作,不要写魔法值。价格单位统一用分,小数转换只发生在前后端交互的边界。
最后分享一个我自己的教训:第一次做完这个项目时,我把商品状态在数据库里设计成 tinyint,代码里到处用魔法数字。结果运营同学在后台把某个违规商品下了架,但商品详情页还是能被用户访问,因为下架的校验逻辑只写在列表接口里,忘记写在详情接口了。后来我把详情接口也补上了状态校验,并把所有魔法值换成了枚举。售后维护时,这个改动省下来的时间远超当初重构的投入。希望这一篇能帮你少走这些弯路,让校园二手交易小程序真正能跑起来、发得出去、扛得住用。
本文还有配套的精品资源,点击获取