
简介这是基于微信小程序的校园二手交易平台完整项目源码定位为高分毕业设计面向计算机专业毕业生、课程设计与期末大作业学生。资源包含前端小程序与后端服务代码前端由wxml、wxss、js、json等构成涵盖页面结构与交互逻辑后端以java文件为主配合xml配置实现用户、商品、订单等核心业务接口另有sql数据库文件可直接导入以及png/jpg等界面素材与说明文档。包内共258个文件压缩包仅3.67MB结构清晰便于快速部署与二次开发。已有456人学习下载适合用来参考项目架构、接口设计及微信小程序实战开发助你快速完成高评价毕设。1. 校园二手交易小程序别把毕设做成“能跑就行”的页面堆叠「基于微信小程序的校园二手交易平台」这类标题在毕业设计里非常典型前端是微信小程序原生页面后端提供接口MySQL 存数据里面通常已经建好了商品表、订单表、用户表甚至带了一部分测试数据。它要解决的核心问题不是界面多好看而是把「发布商品 → 浏览联系 → 下单交易 → 订单状态变更」这条完整链路跑通并且被老师追问时讲得清楚。适合两类人一是需要完成毕设、想从源码里真正学到东西的学生二是想在小程序里做交易类练手项目、希望少踩重复坑的开发者。这个标题的含金量不在“页面有多少个”而在数据库设计、登录态、订单状态机和图片处理这些看不见的地方。2. 拿到源码先拆业务四张表、状态机和连接池是关键很多同学拿到这类源码包第一步就打开微信开发者工具导入前端目录结果全是报错因为小程序端没有后端接口根本起不来。正确的顺序是先拆业务再聊启动。微信小程序有一个硬性限制前端不能直连 MySQL。数据库必须通过后端接口转发所以源码包里一定有一个接口层。你拿到包以后先把目录结构看明白通常是一个小程序前端目录 一个后端目录 一个 SQL 文件。后端语言不固定常见是 Node.js、Java Spring Boot 或 PHP但核心思路一致都是「启动接口服务 → 连接 MySQL → 小程序用 wx.request 请求接口」。真正决定你答辩能不能过关的不是后端用的什么框架而是数据库怎么设计、状态怎么流转。2.1 商品表、订单表、用户表怎么写字段、类型和必须加的索引先看数据库设计。二手交易平台的核心是三张业务表加一张日志表用户表、商品表、订单表、订单状态流转日志表。为什么要有状态日志因为交易纠纷时你需要知道订单从哪一步变到哪一步尤其是答辩时老师问「你这个订单为什么是从已付款直接跳到已取消的」没有日志就是一笔糊涂账。用户表openid 是微信小程序用户的唯一标识必须加唯一索引。用自增 id 做主键没问题业务上的编号不要依赖自增主键订单号单独用带时间戳的业务号。CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 用户ID, openid VARCHAR(64) NOT NULL COMMENT 微信openid唯一, nickname VARCHAR(32) DEFAULT COMMENT 昵称, avatar_url VARCHAR(255) DEFAULT COMMENT 头像地址, phone VARCHAR(20) DEFAULT COMMENT 手机号可为空, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;user在 MySQL 里是保留字建表时可以写成user加反引号但更稳妥的做法是改成user_info。openid 必须唯一这是微信侧身份和本地用户的映射如果出现重复登录逻辑就会混乱。商品表关键在 status 字段。二手交易不是电商后台商品状态只有三个在售、已下架、已卖出。很多毕设会把「已卖出」和「订单状态」混在一起这是概念错误。商品状态应该由商品自身维护订单状态由订单维护两个状态通过商品 id 关联。CREATE TABLE goods ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL COMMENT 发布者ID, title VARCHAR(64) NOT NULL COMMENT 标题, description TEXT COMMENT 描述, price DECIMAL(10,2) NOT NULL COMMENT 价格单位元, images TEXT COMMENT 图片URL多个用逗号分隔, category TINYINT NOT NULL DEFAULT 0 COMMENT 分类0图书 1数码 2生活用品 3其他, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在售 1已下架 2已卖出, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status (status), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;images字段用逗号分隔存多张图虽然不符合第一范式但小程序商城项目里这种设计非常常见读取时用字符串拆分成数组即可不用为图片单独建表。索引方面user_id用来查「我发布的」status用来查「在售列表」category做分类筛选。注意索引不是越多越好每个索引都会拖慢写入这张表四个索引足够。订单表交易类项目的核心。order_status的取值要在代码里集中定义不能散落在各个页面里写死数字。CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号例如 2025010123456789, goods_id INT UNSIGNED NOT NULL COMMENT 商品ID, seller_id INT UNSIGNED NOT NULL COMMENT 卖家ID, buyer_id INT UNSIGNED NOT NULL COMMENT 买家ID, amount DECIMAL(10,2) NOT NULL COMMENT 成交金额, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1待发货 2待收货 3已完成 4已取消, pay_time DATETIME DEFAULT NULL COMMENT 付款时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer_id (buyer_id), KEY idx_seller_id (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单号和自增 id 分离是必须的自增 id 能推断出订单量接口直接暴露出去不安全业务号要在后端生成不要前端传。日志表记录订单每一次状态跳转。CREATE TABLE order_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT UNSIGNED NOT NULL COMMENT 订单ID, from_status TINYINT DEFAULT NULL COMMENT 变更前状态, to_status TINYINT NOT NULL COMMENT 变更后状态, operator_id INT UNSIGNED NOT NULL COMMENT 操作人ID, remark VARCHAR(255) DEFAULT COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单状态流转日志;2.2 状态字段用数字还是字符串我建议数字常量加状态机商品和订单的状态字段网上很多教程喜欢用字符串比如pending、paid、shipped可读性强但数据库存储空间大索引效率低而且最容易出错的地方是前端和后端各写一套字符串大小写不统一就出 bug。我一般用 TINYINT 数字存储在前后端各维护一份状态常量表。小程序端放在utils/constants.js里// utils/constants.js const GOODS_STATUS { ON_SALE: 0, // 在售 OFF_SHELF: 1, // 已下架 SOLD: 2 // 已卖出 }; const ORDER_STATUS { PENDING_PAYMENT: 0, // 待付款 PENDING_SHIPMENT: 1, // 待发货 PENDING_RECEIPT: 2, // 待收货 COMPLETED: 3, // 已完成 CANCELLED: 4 // 已取消 }; // 状态机每种状态允许流向哪些状态 const ORDER_TRANSITIONS { [ORDER_STATUS.PENDING_PAYMENT]: [ORDER_STATUS.PENDING_SHIPMENT, ORDER_STATUS.CANCELLED], [ORDER_STATUS.PENDING_SHIPMENT]: [ORDER_STATUS.PENDING_RECEIPT], [ORDER_STATUS.PENDING_RECEIPT]: [ORDER_STATUS.COMPLETED] }; module.exports { GOODS_STATUS, ORDER_STATUS, ORDER_TRANSITIONS };后端代码里用同样的定义。状态机和 if 嵌套的区别在于状态机把「谁可以走到哪一步」集中在一个配置里改流程时只改一张表不用全项目搜索数字。毕设项目规模不大但答辩老师很吃这一套比到处写if (status 3)要专业得多。2.3 为什么 MySQL 连不上不是密码错连接池和 8 小时超时后端连接数据库时如果每收到一个请求就创建一个连接高并发下数据库会直接崩溃。正确做法是使用连接池。以 Node.js 的mysql2/promise为例// db.js const mysql require(mysql2/promise); const pool mysql.createPool({ host: 127.0.0.1, user: root, password: your_password, database: campus_market, waitForConnections: true, // 连接池满时排队等待 connectionLimit: 10, // 最大连接数 queueLimit: 0, // 排队上限0 表示不限制 enableKeepAlive: true, // 保活避免空闲连接被 MySQL 断开 keepAliveInitialDelay: 0 }); module.exports pool;教科书不会讲的两个坑第一connectionLimit不是越大越好每个连接都要占内存本地毕设设 10 足够第二MySQL 默认有一个wait_timeout空闲连接 8 小时会被服务端杀掉第二天打开电脑再跑后端会报「连接已关闭」。加enableKeepAlive: true只是单向保活最稳的做法是写一个查询前先SELECT 1探活或者干脆在连接池配置里把wait_timeout调大。血泪经验答辩演示前先把后端和数据库重启一遍别拿跑了十几个小时的旧进程上台。3. 本地跑通的最小路径SQL 导入、配置修改与第一屏把源码跑起来本质上是在做三件事把 SQL 文件导入数据库、改后端的数据库连接配置、把小程序端导入微信开发者工具并指向正确的接口地址。链路上任何一环没对上看到的就是白屏、404 或者请求失败。这一章按最小路径走不看多余的东西。3.1 先认目录拿到源码包后第一件事不是导入而是盘点源码包的目录结构直接决定了运行步骤。常见结构分三块miniprogram/或client/微信小程序前端用微信开发者工具打开server/或back-end/后端接口服务一般还有package.json或pom.xml一个.sql文件数据库建表和测试数据脚本通常在根目录或db/目录下有个很容易忽略的点如果包里只有一个 MySQL 的.sql文件而没有后端代码这个项目是跑不起来的。微信小程序端不支持直连 MySQL 数据库所有数据库操作必须走后端接口。有些源码包作者把后端整个目录漏掉了只有前端页面和数据库文件这种包价值就大打折扣了。文件清单确认后检查开发环境微信开发者工具稳定版即可导入小程序前端目录MySQL 5.7 或 8.0用于导入数据库文件后端运行环境按源码包技术栈选Node.js 需要npm installJava 需要 Maven 和 JDKPHP 需要 PHP 环境加 Nginx/Apache3.2 建库与改配置SQL 导入与数据库连接参数首先在 MySQL 里创建一个空数据库然后把 SQL 文件导入进去。命令行方式最通用# 登录 MySQL mysql -u root -p # 创建数据库字符集必须和项目一致 CREATE DATABASE campus_market DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出后导入 SQL 文件 mysql -u root -p campus_market /path/to/campus_market.sql注意utf8mb4这个字符集如果你在建库时用了默认的utf8遇到 emoji 表情或者特殊符号会直接报错或乱码。SQL 文件在导入前建议先打开看一眼文件头部有没有DROP TABLE IF EXISTS语句。有些毕设 SQL 写得比较随意反复导入会积累脏数据有这行才能在每次导入前自动清理重建。导入成功后改后端数据库连接配置。几乎所有后端框架的连接配置都长这样// 以 Node.js 的配置为例Java 的 application.yml、PHP 的 .env 结构类似 const dbConfig { host: 127.0.0.1, user: root, password: 123456, // 改成你自己的 MySQL 密码 database: campus_market, port: 3306 };常见的翻车点有三个host写成了localhost在部分操作系统上被解析成 IPv6 的::1连不上时就换成127.0.0.1密码里如果含有或#记得看框架是否需要 URL 编码端口要确认 MySQL 没有改成 3307 之类的自定义端口。改完配置不要急着启动先用一段最小代码验证连接# Node.js 项目 npm install node -e require(./db).query(SELECT 1).then(() console.log(ok)).catch(err console.error(err))输出ok说明数据库层通了再继续下一步。3.3 启动后端、导入小程序端从命令行到开发者工具数据库通了之后启动后端接口服务。Node.js 项目最常见步骤也是标准的cd server npm install # 安装依赖 npm run dev # 启动开发环境一般默认端口 3000 或 3001后端启动成功的标志是控制台输出监听端口信息。接着打开微信开发者工具导入小程序前端目录。导入之后第一件事不是点编译而是打开项目里的请求配置文件——通常是app.js或utils/request.js——把接口地址改成本地后端地址// app.js App({ globalData: { // 本地开发后端跑在哪这里就指向哪 // 手机真机预览时改成电脑的局域网 IP不能用 localhost baseUrl: http://127.0.0.1:3000/api } });然后点击编译。如果一切正常小程序首页应该能拉到数据库里的测试商品数据了。但是这里有一道关卡还没过默认情况下微信开发者工具会拦截非https接口请求。开发阶段需要手动关闭校验在下图位置操作微信开发者工具右上角「详情」→「本地设置」→ 勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。不勾选http://127.0.0.1:3000会被直接拦截报错是request:fail url not in domain list。3.4 页面白屏和接口 404先查这三处配置启动过程中最常见的三种异常按大概率排序第一白屏且控制台报request:fail。先看「不校验合法域名」有没有勾选再看baseUrl是否拼对。很多人改了app.js忘记重启编译微信开发者工具对globalData的修改有时不会热更新手动点一次编译就好。第二接口返回 404。说明请求已经到了后端但路径不对。打开后端日志看实际收到的 URL再把baseUrl和wx.request里的 url 拼起来对比。常见错误是baseUrl以/结尾而接口路径又以/开头拼出来变成双斜杠//。第三接口返回 500 或数据库查询报错。多半是 SQL 导入不完整或者表结构对不上后端代码的查询字段。直接看后端控制台的报错信息哪张表的哪个字段不存在回数据库对照一下用DESC 表名;查看实际结构。4. 核心链路代码落地登录、发布、列表到订单改状态跑通第一屏只是开始。毕设答辩和实际开发的差别在于页面能看不算会写链路能走通才算。这一章按交易链路拆出四段代码登录怎么换 token、发布商品时图片和表单怎么协同、列表页的请求封装与适配、订单状态怎么安全流转。4.1 登录链路wx.login 换 openidtoken 管理要收口小程序端的登录和传统 Web 登录完全不同。没有账号密码输入框而是用wx.login拿到临时 code发给后端后端拿 code 去微信接口换 openid 和 session_key然后返回一个自定义 token 给小程序端。小程序端保存 token之后所有请求在 header 里带上它。// pages/login/login.js const { request } require(../../utils/request); Page({ onLoad() { // 页面加载就发起静默登录 wx.login({ success: (res) { if (res.code) { request(/auth/login, POST, { code: res.code }) .then((data) { // data.token 是后端签发的自定义登录态 wx.setStorageSync(token, data.token); wx.setStorageSync(userInfo, data.userInfo); wx.switchTab({ url: /pages/index/index }); }) .catch((err) { console.error(登录失败, err); wx.showToast({ title: 登录失败, icon: none }); }); } } }); } });注意两个细节第一wx.login返回的 code 一次性有效而且有效期只有五分钟不能缓存复用第二token 的存储用wx.setStorageSync没问题但读取时要集中处理不要每个页面都写一遍wx.getStorageSync(token)应该在utils/request.js里统一加 header。后端收到 code 后用code2Session接口换 openid。以 Node.js 为例这段逻辑通常是// 后端 auth.js 简化示例 const { code2Session } require(../utils/wx); const jwt require(jsonwebtoken); router.post(/login, async (req, res) { const { code } req.body; const session await code2Session(code); // 向微信接口换取 openid let user await findUserByOpenid(session.openid); if (!user) { // 首次登录自动注册 user await createUser(session.openid); } const token jwt.sign({ userId: user.id }, your_secret_key, { expiresIn: 7d }); res.json({ code: 0, data: { token, userInfo: user } }); });毕设项目用 JWT 做 token 足够了不需要引入 Redis 会话存储。cookie 方案在小程序端不推荐小程序对 cookie 的支持不完整手动维护还容易出错。token 过期时间建议 7 天短期频繁要求重新登录体验很差长期则有安全风险。4.2 发布商品图片上传和表单提交别写在同一个回调里发布商品页是整个小程序里最能体现工程能力的页面。有交互有网络请求图片还要先上传再随表单提交。新手最容易写错的地方是在wx.chooseMedia的 success 回调里同步把商品信息一起提交导致图片还没传完就调用了创建接口。我一般把图片上传和表单提交彻底解耦图片选择后立即开始上传全部上传完成拿到 URL 列表再允许点「发布」按钮。// pages/publish/publish.js const { request } require(../../utils/request); const app getApp(); Page({ data: { images: [], // 上传成功后的 URL 数组 title: , price: , category: 0, uploading: false }, chooseImage() { wx.chooseMedia({ count: 6, mediaType: [image], success: async (res) { this.setData({ uploading: true }); try { // 并发上传所有图片全部成功才继续 const tasks res.tempFiles.map((item) this.uploadImage(item.tempFilePath)); const urls await Promise.all(tasks); this.setData({ images: this.data.images.concat(urls), uploading: false }); } catch (err) { this.setData({ uploading: false }); wx.showToast({ title: 图片上传失败, icon: none }); } } }); }, uploadImage(filePath) { return new Promise((resolve, reject) { wx.uploadFile({ url: app.globalData.baseUrl /goods/upload, filePath: filePath, name: file, success(res) { const data JSON.parse(res.data); if (data.code 0) { resolve(data.data.url); } else { reject(new Error(data.message)); } }, fail: reject }); }); }, submit() { if (!this.data.title.trim() || !this.data.price) { wx.showToast({ title: 请填写完整信息, icon: none }); return; } if (this.data.images.length 0) { wx.showToast({ title: 请至少上传一张图片, icon: none }); return; } request(/goods/create, POST, { title: this.data.title, price: this.data.price, category: this.data.category, images: this.data.images.join(,) }).then(() { wx.showToast({ title: 发布成功 }); setTimeout(() wx.switchTab({ url: /pages/index/index }), 1000); }); } });这里最有价值的代码是Promise.all并发上传六张图同时传比串行快了近六倍。如果上传时对顺序有要求比如封面必须排第一张就改成reduce串行上传否则并发够了。另外一个细节上传接口返回的是 JSON 字符串必须先JSON.parse再取字段很多人忘了这一步拿到整段字符串直接往数组里塞渲染出来全是[object Object]。4.3 商品列表请求封装、下拉刷新和顶部导航栏高度适配列表页是高频页面请求逻辑一定要封装。一个能用的request方法至少要处理loading 状态、token 注入、状态码判断、错误统一提示。// utils/request.js const app getApp(); function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: app.globalData.baseUrl path, method: method, data: data, header: { Authorization: wx.getStorageSync(token) || }, timeout: 10000, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { // token 失效清掉登录态跳登录页 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { reject(res.data); } }, fail(err) { reject(err); } }); }); } module.exports { request };封装的核心收益是统一后端返回结构统一是{ code, data, message }接口成功与否只看code是否为 0页面逻辑不用关心 HTTP 状态码的含义。列表页除了数据加载还有个细节很容易被忽略顶部导航栏高度。吸顶筛选栏、下拉刷新动画都要基于导航栏高度计算布局。微信小程序的胶囊按钮高度在不同机型上不一样不能用写死的 44 像素。// app.js const systemInfo wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); App({ globalData: { statusBarHeight: systemInfo.statusBarHeight, navBarHeight: systemInfo.statusBarHeight 44, // 状态栏 导航栏 baseUrl: http://127.0.0.1:3000/api } });新版基础库推荐用wx.getWindowInfo替代已废弃的wx.getSystemInfoSync。navBarHeight在自定义导航栏组件里用来撑开顶部占位不计算的话内容会顶到刘海屏的状态栏下面。商品列表的下拉刷新是硬需求页面配置里要开启再监听// pages/index/index.js Page({ onPullDownRefresh() { this.loadGoods(true); }, loadGoods(refresh false) { request(/goods/list, GET, { page: 1, pageSize: 20 }) .then((data) { this.setData({ list: data.list }); if (refresh) wx.stopPullDownRefresh(); }) .catch(() { if (refresh) wx.stopPullDownRefresh(); }); } });stopPullDownRefresh必须在请求完成或失败后手动调用不然刷新动画一直转这是新手最容易漏的。4.4 订单状态机用表驱动代替 if 嵌套后端校验别省订单模块是交易类项目和普通展示类项目的分水岭。最容易写出 bug 的点不是创建订单而是对已完成订单再次发货、对已取消订单再次支付。状态机的价值在这里体现。前端根据配置判断按钮可点性后端则必须对每次状态变更做校验。生成订单后的关键操作是扣减商品库存——对二手交易来说就是抢占商品防止一人下单另一人同时购买。// 后端创建订单接口Node.js mysql2/promise 简化示例 const conn await pool.getConnection(); try { await conn.beginTransaction(); // 关键一步条件更新只有商品状态仍在售时才能抢到 const [result] await conn.execute( UPDATE goods SET status 2 WHERE id ? AND status 0, [goodsId] ); if (result.affectedRows 0) { throw new Error(手慢了商品已被买走); } // 插入订单 const [orderResult] await conn.execute( INSERT INTO orders (order_no, goods_id, seller_id, buyer_id, amount, order_status) VALUES (?, ?, ?, ?, ?, 1), [orderNo, goodsId, sellerId, userId, price] ); // 写状态流转日志 await conn.execute( INSERT INTO order_log (order_id, from_status, to_status, operator_id, remark) VALUES (?, NULL, 1, ?, 创建订单), [orderResult.insertId, userId] ); await conn.commit(); } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); }这段代码最核心的思路UPDATE goods SET status 2 WHERE id ? AND status 0。status 0是条件affectedRows为 0 说明商品状态已经不是「在售」可能是被下架了也可能已经被别人抢单。这个写法和先SELECT再UPDATE的区别在于SELECT在并发场景下存在时间差两个人同时查都看到在售然后都执行更新条件更新则把校验和更新合一步完成数据库层面的原子操作不需要锁表。中间件层层加库最后在数据库层面兜底这是交易链路代码的安全底线。5. 避坑记录答辩前最容易翻车的 5 个问题这类毕设项目的问题高度相似我按「现象 → 原因 → 解决」的排查方式把自己踩过的坑整理出来每一条都比背一套理论值钱。5.1 图片上传成功但商品图片显示不出来现象发布商品时提示上传成功回到首页却看到小图裂开点大图加载不出来。控制台输出the server responded with a status of 404。原因前端把wx.chooseMedia返回的临时路径http://tmp/...直接存进了goods.images字段。临时路径在小程序本次会话内有效重启后立刻失效。更隐蔽的是有的接口返回了相对路径但后端静态资源没配置好请求 404。解决图片上传接口的返回值必须是一个可访问的绝对 URL。排查分两步先在浏览器直接打开这个 URL 看能不能显示能显示说明后端没问题问题在存储字段不能显示查后端静态资源目录配置。另外前端拼接 URL 时容易犯的错误是漏掉协议头缩略图组件要用完整https://地址。5.2 真机预览白屏开发工具里却很正常现象开发者工具里列表、详情、下单全部正常点「预览」生成二维码手机扫码打开后只有白屏控制台全是request:fail。原因手机访问了http://localhost:3000但localhost指向的是手机自己不是你的电脑。另外后端默认监听127.0.0.1手机通过局域网访问电脑时如果电脑防火墙拦住了 3000 端口请求也会超时。解决后端监听地址改成0.0.0.0前端baseUrl换成电脑的局域网 IP例如http://192.168.1.10:3000/api。手机和电脑连同一个 WiFi在真机上打开调试模式确认请求是否到达后端。最省事的方案是开发期间始终用真机调试模式。这一步显著影响答辩演示建议提前在平板或备用手机上测一遍完整流程。5.3 MySQL 8.0 连不上密码对了也报 Access denied现象后端启动报ER_ACCESS_DENIED_ERROR仔细确认 MySQL 密码没输错mysql -u root -p命令行能登录后端就是连不上。原因MySQL 8.0 默认用caching_sha2_password认证插件较老版本的 Node.js mysql 驱动和 JDBC 驱动不认识这个插件。解决把数据库用户的认证方式改回mysql_native_password或者升级驱动到支持新插件的最新版本。改动用户认证方式一句 SQL 就行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;改动后重启后端服务。这条只针对 MySQL 8.0 早期版本和旧驱动如果是新驱动一般不需要。判断标准很简单后端控制台报错里带caching_sha2_password就是这个问题换驱动优先尽量不降低 MySQL 侧的安全策略。5.4 一个商品被两个人同时买走现象两个人同时对一个在售商品下单两个订单都创建成功商品被卖了两次。原因后端代码是最原始的「先查商品状态状态是在售然后插入订单最后修改商品状态」。两个请求同时执行时都通过了状态检查然后各自插入订单。解决高二学生的项目里这是正常现象但你要知道怎么改。方案就是第 4 章写的条件更新UPDATE goods SET status 2 WHERE id ? AND status 0判断affectedRows。这一步必须放在事务里商品状态更新成功后再创建订单否则订单创建失败时商品已经被锁死。有条件的话给订单表加goods_id唯一索引数据库层面保证同一商品只有一个有效订单双保险。5.5 数据库中文乱码或 emoji 变成问号现象商品标题里的「苹果手机」显示成「????」带表情的标题直接变成空。原因建库时用了utf8而不是utf8mb4。MySQL 的utf8只支持基本多语言平面emoji 在utf8mb4里才能存。另外导入 SQL 时终端编码不是 UTF-8数据在导入环节就已经损坏。解决建库语句用DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci已存在的库用ALTER DATABASE database_name CHARACTER SET utf8mb4;模块级同步改表字段字符集。导入 SQL 文件时加参数避免 shell 客户端造成转码问题mysql --default-character-setutf8mb4 -u root -p campus_market campus_market.sql答辩前建议专门用一条带 emoji 的测试数据走一遍发布流程这个细节常被抽查能体现工程的细致程度。6. 往加分项上改缓存一致性、图片瘦身与上线自检项目能跑只是及格往这几个方向上做一点改造答辩时就能主动讲出「工程化」的味道。6.1 首页数据缓存 5 分钟交易类小程序首页商品列表是高频接口每次进入都重新请求会拖慢首屏。微信小程序的wx.setStorage本身就是个本地缓存方案不用引入额外依赖。缓存策略要同时记录数据和时间戳// pages/index/index.js const CACHE_KEY goods_list_cache; const CACHE_TIME_KEY goods_list_cache_time; const CACHE_MAX_AGE 5 * 60 * 1000; // 5 分钟 Page({ onShow() { const cache wx.getStorageSync(CACHE_KEY); const cacheTime wx.getStorageSync(CACHE_TIME_KEY); if (cache Date.now() - cacheTime CACHE_MAX_AGE) { // 命中缓存直接用不发请求 this.setData({ list: cache }); return; } loadGoodsFromServer(); } });这里容易犯的错是只存数据不存时间无法判断缓存是否过期。注意一致性发布新商品后主动清掉缓存而不是等 5 分钟自然过期否则刚发布的商品自己看不到。6.2 图片瘦身本地压缩加后端限制校园二手交易场景里用户拍的照片动辄几 MB原图直接传到服务器既浪费存储又拖慢列表加载。小程序端有现成的压缩 APIwx.compressImage只支持压缩本地临时文件所以要在上传前处理// 上传前压缩 wx.compressImage({ src: tempFilePath, quality: 80, success(res) { // res.tempFilePath 是压缩后的路径 uploadImage(res.tempFilePath); } });毕设项目不要求像商业系统那样上对象存储但后端一定要限制上传文件大小防止有人传 100 MB 的文件把磁盘打满。常见做法是后端限制单文件不超过 5 MB超了直接返回 413。6.3 上线前的自检清单如果要把项目提交到微信官方审核有三个硬性门槛毕设答辩时讲出来也是加分项。第一request合法域名必须是备案过的 HTTPS 域名本地调试时勾选的「不校验合法域名」在上线后无效第二小程序后台要配置用户隐私保护指引说明你收集了用户手机号、位置信息否则涉及隐私接口会被打回第三注册时需要个人或企业主体个人主体不能开通微信支付所以「下单付款」这一步大部分毕设是用「模拟支付」按钮跳过的答辩时主动说明这一点比被老师问到卡壳要体面得多。我自己的习惯是每次答辩或演示前把项目完整跑一遍再走一遍发布商品、下单、确认收到货的主流程同时打开后端日志看有没有红字报错。足够细心比背八股文更能打动老师。希望这套拆解思路能帮到你把这份源码真正变成自己的东西。本文还有配套的精品资源点击获取