简介面向计算机相关专业毕业设计和小程序开发实战人群这份校园二手交易平台项目基于微信小程序完整实现商品发布、分类浏览、搜索、订单管理等核心交易流程难度适中适合作为课程设计、毕业设计或项目练习的参考。压缩包共127个文件约2.31MB文件类型以Java后端、小程序前端WXML/WXSS/JS/JSON、XML配置、SQL数据库脚本为主并附带PNG、JPG图片资源与项目说明文档源码均经过本地编译调试可运行性有保障。项目目录按前后端模块划分便于对照学习数据表设计、接口调用与页面交互逻辑。目前已有415人学习下载适合需要完整毕业设计源码、数据库文件以及实战排错经验的同学使用。1. 校园二手交易平台小程序是什么一份能跑通的源码和数据库到底包含什么校园二手交易小程序是每年毕业设计清单里出镜率最高的题目之一。你手上这份“基于微信小程序校园二手交易平台小程序源码数据库”说白了就是三样东西一个能编译运行的微信小程序前端工程、一组提供增删改查能力的后端接口、一份带初始数据的 MySQL 数据库脚本。它解决的业务问题非常具体学生发布闲置、按分类浏览、私聊议价最后生成一个可追溯的订单。适合谁第一类是准备答辩的本科生需要一个能现场演示登录、发布、下单全流程的完整项目第二类是想学小程序全栈的开发者想从微信鉴权一直看到数据落库。下面我按平时过毕设代码的检查顺序把这套系统的每个零件拆开讲清楚包括框架选型、表结构、接口规范和最容易翻车的几个位置。2. 小程序端从登录到首页用户表、导航栏高度与原生框架选型微信小程序的前端工程是整个答辩演示的门面。拿到源码先别急着打开页面先搞清楚它是原生开发还是依赖了 uni-app 之类的东西因为这两种项目在微信开发者工具里的打开方式有本质区别。2.1 选原生小程序还是 uni-app毕设怎么选不翻车市面上的毕业设计交付源码原生微信小程序写法占绝大多数原因很简单原生工程的根目录直接放app.js、app.json、pages微信开发者工具点“导入”就能跑不需要安装 HBuilderX不需要把 uniapp 的中间产物重新编译更不需要关心 uniapp 微信小程序开发者工具插件是不是和你本地的工具版本匹配。有些同学看到 uni-app 能“一套代码多端发布”就想把项目改成 uni-app 体系。如果你还有两三个月时间确实可以折腾但如果离答辩只有一到两周我的建议是不要换框架。原生的生命周期onLoad、onShow、onReachBottom对应业务时更直观答辩老师问你“为什么这个页面数据在onShow里刷新”你回答“因为每次切回页面要重新拉库存状态”非常自然。大学课程设计往往只验收微信小程序端多端发布不是必选项。推荐的原生目录结构大概是这样的pages/index首页商品列表pages/release发布商品页pages/detail商品详情页pages/order我的订单列表页utils/request.js封装wx.requestcomponents/goods-card商品卡片组件先把目录结构在脑子里过一遍后面改代码时才知道去哪找。2.2 微信小程序登录获取手机号前先拿 openid 落用户表登录流程是毕设答辩第一个要演示的点不能出问题。常见做法不是“输入手机号 密码”而是直接用微信静默授权拿到openid再把这个openid写入用户表。小程序端拿到code后提交给后端后端用微信官方接口交换openid。// pages/login/login.js wx.login({ success: async (res) { const { code } res; if (!code) { wx.showToast({ title: 登录失败, icon: none }); return; } wx.request({ url: https://api.example.com/api/login, method: POST, data: { code }, header: { Content-Type: application/x-www-form-urlencoded }, success: (resp) { const { token, userInfo } resp.data.data; wx.setStorageSync(token, token); wx.setStorageSync(userInfo, userInfo); } }); } });这个代码里最关键的参数是data.code它是微信随机生成的临时凭证有效期只有五分钟所以后端收到后要立刻去换openid。token是你自己的后端生成的登录态后续所有接口请求都带这个 token用来识别当前用户。下面是对应后端的 PHP 处理逻辑// api/login.php $code $_POST[code]; $appid 你的appid; $secret 你的secret; $url https://api.weixin.qq.com/sns/jscode2session?appid{$appid}secret{$secret}js_code{$code}grant_typeauthorization_code; $res file_get_contents($url); $data json_decode($res, true); if (!isset($data[openid])) { fail(登录失败请稍后重试); } $openid $data[openid]; // 根据 openid 查用户表不存在就新增用户 $user findUserByOpenid($openid); if (!$user) { $user createUser($openid); } done([token createToken($user[id]), userInfo $user]);这里的file_get_contents是 PHP 最简单的 HTTPS 请求方式但在正式接口里我建议用curl后面避坑章节会提到为什么。openid是用户在这个小程序下的唯一标识同一个用户在不同公众号、小程序下的openid都不同所以一定不要用openid当全局主键去关联别的业务表最好建一张自增id作为主键。用户表结构如下CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL, nickname varchar(64) DEFAULT , avatar varchar(255) DEFAULT , phone varchar(20) DEFAULT , create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;openid加了唯一索引避免同一用户重复注册。phone字段先留着毕设阶段获取真实手机号有门槛后面避坑章节会展开说。2.3 微信小程序顶部导航栏高度自定义导航栏时别写死 88很多小程序页面会自定义顶部导航栏让标题栏和胶囊按钮对齐。如果你把高度写死在88px那到了 iPhone 4S 或者带灵动岛的机型上标题会上下错位。要动态获取顶部导航栏高度常见做法是拿到胶囊按钮的位置再做计算。// utils/navbar.js const systemInfo wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); const statusBarHeight systemInfo.statusBarHeight; const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height; module.exports { statusBarHeight, navBarHeight };这里的逻辑我解释一下menuButton.top是胶囊按钮上边缘到屏幕顶部的距离减去状态栏高度statusBarHeight得到的是自定义导航栏上边距通常上下边距相等所以乘 2 再加按钮高度整个导航栏高度就出来了。开发时你可以在app.json里配置navigationStyle: custom然后在每个页面的onLoad里把navBarHeight设置到data中用styleheight:{{navBarHeight}}px渲染。这个热搜关键词背后全是机型的差异直接写死必翻车。2.4 数据库连接统一封装用 PDO 而不是每次连接口层如果每个 PHP 文件都写一遍mysqli_connect后面改数据库密码时你会改到怀疑人生。我一般把数据库连接封装到一个函数里所有接口文件引用它。// config/db.php ?php function db(): PDO { $host 127.0.0.1; $port 3306; $dbname campus_secondhand; $user campus; $pass your_password; $dsn mysql:host$host;port$port;dbname$dbname;charsetutf8mb4; $pdo new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); return $pdo; }这里有两个点需要说明一是ATTR_ERRMODE设为ERRMODE_EXCEPTION这样 SQL 出错会直接抛异常而不是返回false让你到处找错二是FETCH_ASSOC默认返回关联数组前端取字段名时更直观。如果数据库连接失败先检查这里的账号、密码和端口别急着改代码。3. 商品发布与订单流转三张核心表的设计和增删改查怎么写商品、订单、收藏这三张表几乎可以承载一个二手交易平台的全部核心流程。把这三张表的字段设计清楚后面所有 CRUD 都能顺下来。3.1 商品表、订单表、收藏表怎么设计选字段就是选逻辑商品表要覆盖二手交易的特殊性原价和现价都有意义图片至少支持多张状态必须能区分“上架、下架、已被拍下、已售出”。CREATE TABLE goods ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, title varchar(100) NOT NULL, description text, price decimal(10,2) NOT NULL DEFAULT 0.00, original_price decimal(10,2) DEFAULT NULL, category tinyint(4) NOT NULL DEFAULT 0, images varchar(1000) DEFAULT , status tinyint(4) NOT NULL DEFAULT 0, view_count int(11) NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status建议用数字代替字符串0 表示在售1 表示已下架2 表示已被拍下3 表示已售出。用数字的好处是查询和排序都更快而且不会被字符串大小写问题干扰。price用decimal(10,2)不要用float否则 9.9 这种价格在比较时会出各种奇怪结果。images用逗号分隔存储比如/uploads/1.jpg,/uploads/2.jpg在详情页用split(,)拿到数组。订单表用于记录一次交易关系CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, goods_id int(11) NOT NULL, seller_id int(11) NOT NULL, buyer_id int(11) NOT NULL, price decimal(10,2) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_goods (goods_id), KEY idx_buyer (buyer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意订单里面冗余了一个price字段。为什么不在下单时去查商品表因为卖家可能在生成订单后又改了价格订单必须保留下单那一刻的价格防止纠纷。收藏表就比较简单加一个唯一索引防止重复收藏CREATE TABLE favorites ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, goods_id int(11) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这三张表建好之后数据库增删改查就很常规了查在售商品用WHERE status0找某人收藏用WHERE user_id?。毕设答辩时老师大概率会问“这几张表的关系”你要能说清楚orders表通过seller_id和buyer_id关联两行user记录这就是核心逻辑。3.2 发布商品页面表单校验与图片上传的完整请求发布页是整个前端工程里最容易出 bug 的页面因为图片上传是异步操作而且wx.uploadFile和wx.request不是同一个 API。很多学生在这里掉进回调地狱然后数据库里存了空字符串。先看一个递归上传多图的实现// pages/release/release.js function uploadImages(imageList, index, resultList, callback) { if (index imageList.length) { callback(resultList); return; } const filePath imageList[index]; wx.uploadFile({ url: https://api.example.com/api/upload, filePath, name: file, success: (res) { const data JSON.parse(res.data); if (data.code 0) { resultList.push(data.data.url); } uploadImages(imageList, index 1, resultList, callback); }, fail: () { wx.showToast({ title: 第${index 1}张上传失败, icon: none }); } }); }这里name: file必须和后端$_FILES[file]对应。很多人只改了前端字段名忘了后端接收字段导致服务端一直报找不到上传文件。完成后再调用wx.request把商品信息提交给后端图片字段用拼接好的 URL 字符串。表单里的分类选择我建议用radio-group而不是picker因为四五个分类选项用单选框展示更清晰答辩截图也好看。提交前至少校验标题非空、价格是正数、至少选了一张图这些校验能避免给后端塞一堆脏数据。3.3 商品列表的分页与筛选动态拼接 SQL 的坑首页商品列表只要数据超过二十条就必须做分页。小程序端触发方式很简单在onReachBottom里把page加一再拉一次。后端动态拼接查询条件时最怕 SQL 注入。public function listGoods($page, $category, $keyword) { $page max(intval($page), 1); $pageSize 10; $offset ($page - 1) * $pageSize; $sql SELECT * FROM goods WHERE status0; $params []; if ($category ! ) { $sql . AND category :category; $params[:category] intval($category); } if ($keyword ! ) { $sql . AND title LIKE :keyword; $params[:keyword] % . $keyword . %; } $sql . ORDER BY create_time DESC LIMIT $offset, $pageSize; $stmt db()-prepare($sql); $stmt-execute($params); return $stmt-fetchAll(); }这里的几个参数说明$page由前端传入必须是大于等于 1 的整数所以用intval强制转换LIMIT的两个数字我直接拼进 SQL因为已经做了intval不存在注入风险keyword模糊搜索用了绑定参数但%要写在占位符的值里不能写在 SQL 字符串里。如果你在 SQL 里写LIKE %:keyword%PDO 会把它当成字面量查询结果永远是空的。3.4 订单状态流转与“我要买”的最小实现当买家点击详情页的“我要买”时前端只需要提交goods_id后端要做的事就没那么简单了。必须确认商品存在且状态为在售再插入订单还要更新商品状态。$pdo-beginTransaction(); try { $stmt $pdo-prepare(SELECT * FROM goods WHERE id? AND status0 FOR UPDATE); $stmt-execute([$goodsId]); $goods $stmt-fetch(); if (!$goods) { throw new Exception(商品不存在或已下架); } $pdo-prepare(INSERT INTO orders (goods_id, seller_id, buyer_id, price, status) VALUES (?,?,?,?,0)) -execute([$goodsId, $goods[user_id], $buyerId, $goods[price]]); $pdo-prepare(UPDATE goods SET status2 WHERE id?) -execute([$goodsId]); $pdo-commit(); } catch (Exception $e) { $pdo-rollBack(); fail($e-getMessage()); }这里用到了事务和FOR UPDATE行锁作用是防止两个人同时点击“我要买”导致卖同一件商品。毕设答辩时老师问“高并发怎么办”你能说出这个方案就已经超出多数同学的水平了。status2表示已被拍下买家取消订单后再改回status0这套状态机就是订单模块的核心。4. 后端接口与文件上传PHP 封装接口时的参数规范和日志排查毕设小程序的后端不需要很重但接口规范必须要统一。前端最怕看到这次返回{code:0,data:[]}下次返回{success:true,message:}完全没法写逻辑。4.1 建立统一入口所有接口走路由或入口文件很多老式 PHP 源码是每个接口单独一个 PHP 文件比如login.php、goods_add.php文件多了以后很难维护。常见做法是做一个index.php入口文件通过参数区分路由。// public/index.php ?php header(Content-Type: application/json; charsetutf-8); require ../config/db.php; $route $_GET[route] ?? ; switch ($route) { case goods.list: echo json_encode([code 0, data listGoods()]); break; case order.create: echo json_encode(createOrder()); break; default: echo json_encode([code 404, msg 接口不存在]); }前端请求地址就可以统一为https://api.example.com/index.php?routegoods.listpage1。这里要注意小程序的wx.request的url必须是合法域名不能带端口和路径参数url里带?routegoods.list是允许的但合法域名部分只能写到https://api.example.com这是很多人在后台配置域名时容易忽略的点。4.2 统一返回格式与参数校验前后端少吵一架我为这类项目定的返回到格式很简单{code, msg, data}code0代表成功非 0 代表失败失败的原因放在msg。再封装两个小函数所有接口都不再裸写json_encode。function ok($data []) { echo json_encode([code 0, msg , data $data]); exit; } function fail($msg) { echo json_encode([code 1, msg $msg, data null]); exit; } function need($fields) { foreach ($fields as $field) { if (!isset($_POST[$field]) || $_POST[$field] ) { fail(缺少参数{$field}); } } }参数校验一定要先于业务逻辑执行。比如创建订单接口第一步need([goods_id])再查数据库。否则你写半天 SQL最后发现是参数没传日志里全是数据库报错找这种错误最浪费时间。4.3 文件上传保存路径、大小限制和重命名图片上传是后端最容易出安全问题的环节。如果直接把客户端传的原始文件名拿来存不仅会遇到中文乱码还可能被人传一个.php文件上来。我平时是这么处理的if ($_FILES[file][error] ! UPLOAD_ERR_OK) { fail(上传失败); } $size $_FILES[file][size]; if ($size 2 * 1024 * 1024) { fail(图片不能超过2M); } $ext strtolower(pathinfo($_FILES[file][name], PATHINFO_EXTENSION)); $allow [jpg, jpeg, png, gif, webp]; if (!in_array($ext, $allow)) { fail(不支持的图片格式); } $filename date(YmdHis) . _ . bin2hex(random_bytes(8)) . . . $ext; move_uploaded_file($_FILES[file][tmp_name], __DIR__ . /uploads/ . $filename);参数说明UPLOAD_ERR_OK是0但它比直接写 0 可读性好大小校验卡在 2M因为小程序端chooseMedia选原图很容易超过这个值重命名用时间戳加随机字节避免文件名被猜出来。图片访问时如果通过/uploads/xxx.jpg找不到先检查uploads目录是否有可读权限Linux 下执行chmod -R 755 uploads再试。4.4 错误日志先看日志再改代码很多同学调接口时只看微信开发者工具返回的 500然后开始瞎猜。正确做法是把 PHP 错误写到日志文件再对照日志定位问题。ini_set(display_errors, 0); ini_set(log_errors, 1); ini_set(error_log, __DIR__ . /logs/php_error.log);在联调阶段用 bash 实时看日志尾部变化tail -f /var/www/html/campus_secondhand/logs/php_error.logtail -f会持续输出新内容每点一次小程序页面的刷新按钮日志里就会多一行。看到类似SQLSTATE[HY000] [1045] Access denied那就是数据库连接问题看到syntax error, unexpected说明 PHP 语法写错了。有了日志你就不用靠玄学改代码了。5. 避坑排查微信小程序联调时最容易翻车的 5 个点5.1 坑一request 请求报“不在合法域名列表”现象是开发工具里接口请求直接失败红色文字提示“URL not in domain list”或者“不在合法域名列表”。原因很简单微信要求wx.request和wx.uploadFile的请求地址必须在小程序后台配置为合法域名开发环境默认也做校验。解决方法是本地开发时在微信开发者工具“详情-本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。这个选项能让你用http://127.0.0.1或局域网 IP 调试后端。但要注意真机预览时这个选项不生效你需要一个已经备案且配置好 HTTPS 证书的域名并且在微信公众平台“开发管理-服务器域名”里把request 合法域名和uploadFile 合法域名都加上。合法域名不能带http://前缀实际上配置时只填域名加https://不能带端口。这是我见过最多人忽略的点。5.2 坑二模拟器正常真机白屏现象在开发者工具里每页都能打开一扫码真机就白屏或页面空白。原因基本是两个第一代码里用了较新的 ES 语法比如可选链?.、空值合并??真机的 JavaScript 引擎版本不够解析不了第二项目里使用了本地临时文件路径作为图片地址真机冷启动后临时目录清掉了。解决方法是在开发者工具“详情-本地设置”里勾选“将 JS 编译成 ES5”同时在app.json里声明最低基础库版本比如libVersion: 2.10.0让低版本手机直接打不开而不是白屏。另外所有图片上传后要立刻转成服务器绝对地址不要用本地wxfile://路径存库。5.3 坑三MySQL 8 连接不上认证方式不对现象后端接口返回数据库连接失败PHP 报SQLSTATE[HY000] [1045] Access denied for user但在命令行里用同一个账号密码反而能登录。原因是因为 MySQL 8 默认认证插件是caching_sha2_password老版本 PHP 的mysqlnd驱动不兼容。解决方式不是改全局默认认证而是专门为项目创建一个使用旧版认证的账号CREATE USER campuslocalhost IDENTIFIED WITH mysql_native_password BY your_password; GRANT ALL PRIVILEGES ON campus_secondhand.* TO campuslocalhost; FLUSH PRIVILEGES;然后把你db.php里的连接账号改成campus。这样既不影响系统里其他数据库也能立刻解决 1045 报错。如果你用的 PHP 版本比较新也可以安装mysqlnd扩展后继续用caching_sha2_password但那需要在 php.ini 里确认扩展加载不如第一种方案直接。5.4 坑四图片上传成功但前端显示不出来现象wx.uploadFile返回 success数据库里也存了/uploads/xxx.jpg但小程序图片组件裂了。原因有两个方向一个是后端返回的是相对路径小程序直接拿相对路径去请求https://api.example.com/uploads/xxx.jpg没问题但如果返回了C:/www/...这种本地绝对路径小程序肯定打不开。另一个是后端返回http://而小程序正式环境只允许 HTTPS浏览器会拦截。解决方法是后端上传成功后直接拼接完整可访问地址$url https://api.example.com/uploads/ . $filename;同时在小程序后台确认downloadFile 合法域名也加上了这个域名否则image组件的图片一样加载不出来。有人在这里反复改前端其实问题出在小程序后台域名配置这类问题最容易耗掉半天时间。5.5 坑五微信小程序登录获取手机号失败卡在用户注册现象前端写了button open-typegetPhoneNumber点击后回调报错或者后端拿到code后去微信接口换手机号失败。原因是获取真实手机号接口这两年调整了政策个人主体小程序没有权限必须是企业、个体户等认证主体并且要开通相应能力。毕设项目如果只是模拟演示我强烈建议不要在这个功能上死磕常见做法是先用第 2 章的wx.login静默完成 openid 注册用户表里的phone字段留空在“个人中心”页面放一个普通输入框让用户填手机号只做11位数字格式校验。答辩时你可以说“出于隐私保护这里采用手动填写手机号”这是合规且实际的解释。如果老师一定要求真实手机号那就只能认证企业小程序再重新实现getPhoneNumber流程。6. 上线前多花一小时真机预览、域名校验与数据检查的验证方法6.1 真机预览和性能测试发布前至少跑一遍这些操作小程序不是写完就完微信开发者工具里能跑通真机不一定能跑通。我在交付毕设前一般让同学做一轮“真机调试”在手机微信里跑这几个核心路径首次登录、首页下拉刷新、发布商品并上传两张图、详情页点“我要买”、订单列表状态变化。把真机调试打开一边操作一边看 console 面板有没有红色报错。如果发现页面滚动吃力检查一下setData的频率尤其是列表页不要在onScroll里频繁调用setData那个是微信小程序性能测试里最常见的问题。另外在开发者工具“体验评分”里跑一遍分数低于 80 的话先清理无用的console.log和页面里未用的图片资源这个动作通常能显著提升流畅度。6.2 数据初始化验证别等答辩才发现没数据最后一步必须检查数据库初始化脚本。很多“源码数据库”的交付包虽然给了campus_secondhand.sql但里面只有建表语句没有INSERT测试数据。答辩现场从零开始演示时首页空空如也场面很尴尬。导入数据库后执行这两条 SQLSELECT COUNT(*) AS goods_count FROM goods; SELECT COUNT(*) AS user_count FROM user;如果数量是 0回到sql文件里找是否有INSERT INTO语句没有就自己补几条典型数据三本教材、一台旧手机、一个宿舍用的电水壶图片路径可以用占位地址。我自己的习惯是在答辩前一天把数据库重新导一遍所有代码按“从克隆到跑通”的路径重新走一次。之前有个同学告诉我“代码没问题”结果现场发现db.php里写的是测试库密码正式环境根本连不上。这种问题不难修但在台上连续报错几下整体印象就差很多。希望这个习惯能帮你在答辩前多留一点底气也少一点临时改代码的慌张。希望帮到你。本文还有配套的精品资源点击获取