校园二手交易小程序开发实战:从状态机设计到Node.js接口联调
2026/9/13 5:18:57 网站建设 项目流程

简介:这份基于JavaScript开发的校园二手交易微信小程序完整源码包,主要面向计算机相关专业在校生、教师及初级开发者,可用于课程设计、毕业设计或项目起步演示。项目实现了定位大学、当地天气、轮播图、推荐列表、购物车、商品发布、订单管理、身份验证等典型二手交易功能,覆盖小程序常用交互与数据流链路。整个压缩包共205个文件,以51个js逻辑脚本、40个wxml页面结构、42个wxss样式、42个json配置为主,并配有多张png/jpg界面图片及README文档说明,压缩后仅1.2MB,轻量清晰,便于直接运行和二次修改。当前已有100人学习/下载,源码已通过运行测试,若遇到环境配置问题还可与作者沟通,获取远程教学指导。无论用于课程作业、毕设演示,还是想快速理解微信小程序项目结构,这套源码都具有不错的参考和复用价值。

1. 校园二手交易微信小程序:先定交易闭环,再写页面

拿到“校园二手交易微信小程序”这个标题,很多人第一反应是去画首页、做轮播图,但我建议先把交易闭环画出来。校园场景里有发布闲置的学生,有搜索想买的同学,还有交易完成后需要确认收货的节点,这三者构成了一个完整的二手交易平台。相比普通商城,二手物品没有标准 SKU,价格可议、成色不一,所以订单状态、商品上下架、交易取消这些逻辑,远比页面美观更重要。这篇文章以 JavaScript 为技术主线,围绕微信小程序前端和 Node.js 后端,把“校园二手书 / 校园二手物品交易”从数据模型到接口联调,再到项目源码与文档说明的交付,完整走一遍。适合正在做课程设计或毕业设计的开发者,也适合接手别人二手交易源码时快速理解项目结构的人。

2. 需求分析与数据模型:二手交易的状态机和字段设计

2.1 从闲置发布到确认收货:拆解核心交易链路

校园二手交易和普通电商最大的区别在于“信任”和“线下交接”。学生发布一本书,可能当天就被同校的人买走,也可能挂一个月无人问津。作为开发者,第一件事不是写页面,而是把业务状态罗列出来:

  • 商品有“在售 / 已下架 / 已售出”三种状态;
  • 订单有“买家下单 / 卖家确认 / 买家收货 / 交易完成 / 已取消”五种状态;
  • 每个状态变更都必须有操作人(买家或卖家)和发生时间。

我一般会先画一张状态流转图,而不是直接建表。比如买家下单后,商品状态要立刻从“在售”变成“锁定”,否则两个人同时下单就会出现超卖;卖家确认后,订单进入“待收货”;买家确认收货后,订单变成“交易完成”,商品变成“已售出”。这些规则如果不提前定好,后面写接口时一定会到处打补丁。把状态转移画成大圈套小圈的图,比任何需求文档都直观;画完后保留这张图,后续写后端接口、写前端页面、写文档说明时都能对照它检查逻辑是否遗漏。

2.2 三张核心表:用户、商品、订单的字段清单

数据模型是整个项目源码里最值得反复推敲的部分。以最常见的校园二手平台为例,最少需要三张表:userproductorder。下面是我常用的字段设计:

字段类型说明
useropenidvarchar(64)微信登录唯一标识,设为主键
usernicknamevarchar(32)用户昵称
useravatar_urlvarchar(255)头像地址
userstudent_idvarchar(20)学号,校园场景用于实名
usercreate_timedatetime注册时间
productproduct_idint主键,自增
producttitlevarchar(100)商品标题
productdescriptiontext商品描述
productpricedecimal(10,2)价格,注意用 decimal 而不是 float
productoriginal_pricedecimal(10,2)原价或参考价
productimagesjson图片 URL 列表,用 JSON 存多图
productcategoryvarchar(20)分类:教材、数码、生活用品等
productstatustinyint0在售 1锁定 2已售 3下架
productseller_openidvarchar(64)发布者 openid
productcreate_timedatetime发布时间
orderorder_idbigint主键
orderorder_novarchar(32)订单号,业务上显示用
orderproduct_idint关联商品
orderbuyer_openidvarchar(64)买家
orderseller_openidvarchar(64)卖家
orderstatustinyint0待确认 1待收货 2已完成 3已取消
orderpricedecimal(10,2)成交价格,下单时从商品快照
ordercreate_timedatetime下单时间

这里有几个容易忽略的点:价格字段千万不要用float,因为二进制浮点数在计算总价时会有精度误差,尤其微信小程序端 JavaScript 做加法也可能出现0.1 + 0.2 !== 0.3的问题;商品图片用json类型存数组,避免单独建一张图片表;订单里冗余一份seller_openidprice,是为了防止商品信息被修改后订单数据跟着变,这在交易系统里叫“快照”。如果你统计过同类校园二手平台的数据,会发现教材类商品占到发布量的四成以上,所以category字段建议预置教材、数码、生活用品等几个固定值,前端渲染单选框时也方便。

2.3 状态机与 JavaScript 校验函数

数据表定好后,把状态流转写进代码。这部分用纯 JavaScript 定义常量,比写在注释里更可靠。

// constants.js const PRODUCT_STATUS = { ON_SALE: 0, // 在售 LOCKED: 1, // 已锁定(有人下单未完成) SOLD: 2, // 已售出 OFF_SHELF: 3 // 已下架 }; const ORDER_STATUS = { PENDING: 0, // 买家已下单,等待卖家确认 CONFIRMED: 1, // 卖家已确认,等待买家收货 COMPLETED: 2, // 买家确认收货,交易完成 CANCELED: 3 // 已取消 }; // 定义允许的状态迁移:旧状态 -> 新状态 const ORDER_TRANSITIONS = { [ORDER_STATUS.PENDING]: [ORDER_STATUS.CONFIRMED, ORDER_STATUS.CANCELED], [ORDER_STATUS.CONFIRMED]: [ORDER_STATUS.COMPLETED, ORDER_STATUS.CANCELED] }; function canTransition(fromStatus, toStatus) { const allowed = ORDER_TRANSITIONS[fromStatus]; return !!allowed && allowed.includes(toStatus); } module.exports = { PRODUCT_STATUS, ORDER_STATUS, canTransition };

这段代码的逻辑说明:ORDER_TRANSITIONS是一个映射表,key是当前订单状态,value是允许跳转到的目标状态数组。canTransition函数返回布尔值,后端接口在更新状态前必须先调用它做校验。比如订单处于PENDING(等待卖家确认)时,买家可以取消,卖家可以确认,但它不能直接跳到COMPLETED,因为中间少了确认收货动作。把这种规则集中在文件里,比散落在各个接口中好维护得多。参数说明:fromStatus是旧状态,toStatus是目标状态,两者都必须是上面定义的常量,建议在接口层再包一层防御判断,防止数据库写入脏数据。后面写接口、做订单操作时,只认这组常量,不许在代码里出现裸数字01,否则排错时很容易把业务含义搞混。

3. 小程序前端实现:页面、组件与微信 API 的配合

3.1 用 HBuilderX 创建项目并配置 app.json

开发微信小程序,工具链上我推荐 HBuilderX,原因很简单:它内置了微信小程序的编译、预览和上传能力,同时支持htmlcssjavascript的语法高亮,如果你从零开始,也可以直接用官方微信开发者工具。这里遵循标题里 JavaScript 的技术背景,用原生小程序语法讲解,不引入额外框架。HBuilderX 的快捷键和插件体系偏向前端工程化,熟悉 Vue 的同学也能快速上手;不过要强调一点,HBuilderX 只是编辑器,真正编译运行微信小程序仍然依赖微信开发者工具的基础库,两者要配合使用。

新建一个“微信小程序”项目后,先看app.json,这是整个小程序的全局配置:

{ "pages": [ "pages/index/index", "pages/publish/publish", "pages/detail/detail", "pages/order/order", "pages/mine/mine" ], "window": { "navigationBarTitleText": "校园二手交易", "navigationBarBackgroundColor": "#07c160", "navigationBarTextStyle": "white", "backgroundColor": "#f5f5f5" }, "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/publish/publish", "text": "发布" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }

pages数组的第一个元素是小程序的首页,后续所有新页面都要在这里注册,否则跳转时会报“页面不存在”。tabBar配置底部导航,注意tabBar页面必须在pages中出现,且图标路径不能为空。这里我没有加图标文件,直接用了文字,如果你需要自定义图标,要在iconPathselectedIconPath里填本地图片路径。顶部导航栏高度在不同机型上不一致,如果你要自定义导航栏,需要关注状态栏高度,这块在 3.2 节会涉及。

3.2 首页列表:WXML 渲染、下拉刷新与加载状态

首页是二手物品交易平台的流量入口。用微信小程序的wx.request从后端拉数据,然后在WXML里用wx:for渲染列表。先看数据层代码:

// pages/index/index.js Page({ data: { productList: [], loading: false, finished: false, page: 1, pageSize: 10 }, onLoad() { this.loadProducts(true); }, onPullDownRefresh() { this.loadProducts(true).finally(() => { wx.stopPullDownRefresh(); }); }, onReachBottom() { if (this.data.finished) return; this.loadProducts(false); }, loadProducts(reset) { if (this.data.loading) return Promise.resolve(); this.setData({ loading: true }); const page = reset ? 1 : this.data.page + 1; return wx.request({ url: 'http://localhost:3000/api/products', data: { page, pageSize: this.data.pageSize }, success: (res) => { const list = res.data.data.list; this.setData({ productList: reset ? list : this.data.productList.concat(list), page: page, finished: list.length < this.data.pageSize }); }, fail: () => { wx.showToast({ title: '加载失败', icon: 'none' }); }, complete: () => { this.setData({ loading: false }); } }); } });

代码说明:loadProducts(reset)同时承担首次加载、下拉刷新和触底翻页三种操作。resettrue时从第一页拉数据并替换列表;为false时页码加 1,并通过concat把新数据追加到已有列表末尾。finished用来判断是否还有更多数据,避免无意义的请求。wx.request是小程序发起网络请求的核心 API,success回调里拿到的是后端返回的完整响应体,我这里约定的res.data.data.list是“统一响应格式”中的 data 字段,具体封装在第四章。逻辑说明:onPullDownRefresh需要页面的json配置中设置"enablePullDownRefresh": true,否则下拉不会触发;onReachBottom是页面滚动到底部的钩子,不需要额外配置。

WXML 侧的结构比较简单:

<view class="product-list"> <block wx:for="{{productList}}" wx:key="product_id"> <view class="product-card" bindtap="gotoDetail">// pages/publish/publish.js Page({ data: { title: '', category: '教材', price: '', description: '', images: [], categories: ['教材', '数码', '生活用品', '运动户外', '其他'] }, onTitleInput(e) { this.setData({ title: e.detail.value }); }, onCategoryChange(e) { // e.detail.value 是选中的单选框 value this.setData({ category: e.detail.value }); }, onPriceInput(e) { let value = e.detail.value.replace(/[^\d.]/g, ''); this.setData({ price: value }); }, chooseImage() { wx.chooseMedia({ count: 3 - this.data.images.length, mediaType: ['image'], success: (res) => { const tempFiles = res.tempFiles; const images = this.data.images.concat(tempFiles.map(f => f.tempFilePath)); this.setData({ images }); } }); }, submit() { const { title, price, description, category, images } = this.data; if (!title.trim()) { wx.showToast({ title: '请填写标题', icon: 'none' }); return; } if (!price || parseFloat(price) <= 0) { wx.showToast({ title: '请填写正确价格', icon: 'none' }); return; } if (images.length === 0) { wx.showToast({ title: '请至少上传一张图片', icon: 'none' }); return; } // 先上传图片,拿到 URL 后再提交商品信息 wx.showLoading({ title: '发布中' }); this.uploadImages(images).then((urls) => { wx.request({ url: 'http://localhost:3000/api/products', method: 'POST', data: { title, price: parseFloat(price), description, category, images: urls }, success: (res) => { wx.hideLoading(); if (res.data.code === 0) { wx.showToast({ title: '发布成功', icon: 'success' }); setTimeout(() => wx.navigateBack(), 1500); } } }); }); }, uploadImages(images) { const tasks = images.map((path) => { return new Promise((resolve, reject) => { wx.uploadFile({ url: 'http://localhost:3000/api/upload', filePath: path, name: 'file', success: (res) => resolve(JSON.parse(res.data).data.url), fail: reject }); }); }); return Promise.all(tasks); } });

这段代码的核心是submit中的发布流程:先做本地校验,再上传图片,全部成功后用wx.request提交商品数据。chooseImage中的count: 3 - this.data.images.length用来控制最多上传三张图。onPriceInput里的正则只允许数字和小数点,这是防止用户在价格框里输入字母的最简单手段。上传题图时wx.uploadFilename参数必须与后端接收文件字段一致,否则后端收不到文件。注意,wx.chooseMedia是较新的 API,基础库 2.10.0 以后支持;如果你要兼容老版本,可以用wx.chooseImage。单选框在这里通过categories数组渲染,WXML 中用radio-groupradio实现,bindchange事件回调里e.detail.value就是选中的值。这种写法比写死五个单选按钮更可维护。发布成功延迟navigateBack是给用户一个看到“发布成功”提示的时间窗口,时间太长反而让人觉得卡顿,1.5 秒刚好。

3.4 个人中心与我的发布:管理交易状态

“我的”页面通常要展示:我的头像、昵称、我发布的商品、我买到的、我卖出的。对于校园二手物品交易来说,“我卖出的”比“我买到的”更重要,因为卖家需要处理订单确认和发货环节。

// pages/mine/mine.js Page({ data: { userInfo: null, myProducts: [] }, onShow() { this.checkLogin(); }, checkLogin() { const token = wx.getStorageSync('token'); if (!token) { wx.navigateTo({ url: '/pages/login/login' }); return; } this.fetchUserInfo(token); this.fetchMyProducts(token); }, fetchMyProducts(token) { wx.request({ url: 'http://localhost:3000/api/my/products', header: { Authorization: token }, success: (res) => { this.setData({ myProducts: res.data.data.list }); } }); }, onProductTap(e) { const productId = e.currentTarget.dataset.id; wx.navigateTo({ url: `/pages/product-status/product-status?productId=${productId}` }); } });

这里的checkLogin在每次页面显示时执行,因为用户可能在其他页面修改了登录状态。wx.getStorageSync('token')是同步读取本地缓存,适合这种轻量判断。fetchMyProducts通过请求头的Authorization字段传递 token,后端再根据 token 解析出 openid,从而查询该用户发布的商品列表。点击某件商品后跳转到状态管理页,那里可以看到该商品当前的订单、以及切换上下架的按钮。这种“我的发布→商品状态→订单处理”的层级关系,比把所有功能堆在“我的”页里更清晰。如果你想让管理页更直观,可以在每个订单项上直接放“确认”和“取消”按钮,按钮的>// db.js const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: '123456', database: 'campus_secondhand', waitForConnections: true, connectionLimit: 10, namedPlaceholders: true }); module.exports = pool;

// productController.js const pool = require('../db'); const { PRODUCT_STATUS, ORDER_STATUS, canTransition } = require('../constants'); const { v4: uuidv4 } = require('uuid'); // 商品分页列表 async function getProducts(req, res) { const page = parseInt(req.query.page) || 1; const pageSize = parseInt(req.query.pageSize) || 10; const offset = (page - 1) * pageSize; const [rows] = await pool.query( `SELECT product_id, title, price, images, status FROM product WHERE status = ? ORDER BY create_time DESC LIMIT ? OFFSET ?`, [PRODUCT_STATUS.ON_SALE, pageSize, offset] ); res.json({ code: 0, data: { list: rows } }); } // 发布商品 async function createProduct(req, res) { const { title, description, price, category, images } = req.body; const openid = req.user.openid; // 中间件解析 token 后挂载 const [result] = await pool.query( `INSERT INTO product (title, description, price, original_price, images, category, status, seller_openid) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [title, description, price, price, JSON.stringify(images || []), category, PRODUCT_STATUS.ON_SALE, openid] ); res.json({ code: 0, data: { productId: result.insertId } }); } // 创建订单 async function createOrder(req, res) { const { productId } = req.body; const buyerOpenid = req.user.openid; const [products] = await pool.query( `SELECT * FROM product WHERE product_id = ? FOR UPDATE`, [productId] ); if (products.length === 0) { return res.status(404).json({ code: 1002, message: '商品不存在' }); } const product = products[0]; // 校验商品状态:只有在售才能下单 if (product.status !== PRODUCT_STATUS.ON_SALE) { return res.json({ code: 1003, message: '商品已被锁定或售出' }); } // 更新商品状态为锁定,同时插入订单 const orderNo = 'SO' + Date.now() + uuidv4().slice(0, 6); await pool.query( `UPDATE product SET status = ? WHERE product_id = ? AND status = ?`, [PRODUCT_STATUS.LOCKED, productId, PRODUCT_STATUS.ON_SALE] ); await pool.query( `INSERT INTO \`order\` (order_no, product_id, buyer_openid, seller_openid, status, price) VALUES (?, ?, ?, ?, ?, ?)`, [orderNo, productId, buyerOpenid, product.seller_openid, ORDER_STATUS.PENDING, product.price] ); res.json({ code: 0, data: { orderNo } }); } // 订单状态流转 async function updateOrderStatus(req, res) { const orderId = req.params.id; const { action } = req.body; // confirm / receive / cancel const openid = req.user.openid; const [orders] = await pool.query( `SELECT * FROM \`order\` WHERE order_id = ?`, [orderId] ); if (orders.length === 0) { return res.status(404).json({ code: 1004, message: '订单不存在' }); } const order = orders[0]; // 权限校验:卖家只能确认/取消,买家只能收货/取消 let targetStatus; if (action === 'confirm') { if (order.seller_openid !== openid) return res.status(403).json({ code: 403, message: '无权限' }); targetStatus = ORDER_STATUS.CONFIRMED; } else if (action === 'receive') { if (order.buyer_openid !== openid) return res.status(403).json({ code: 403, message: '无权限' }); targetStatus = ORDER_STATUS.COMPLETED; } else if (action === 'cancel') { targetStatus = ORDER_STATUS.CANCELED; } else { return res.status(400).json({ code: 400, message: '无效操作' }); } // 状态机校验 if (!canTransition(order.status, targetStatus)) { return res.json({ code: 1005, message: '当前状态不允许该操作' }); } await pool.query( `UPDATE \`order\` SET status = ? WHERE order_id = ? AND status = ?`, [targetStatus, orderId, order.status] ); // 如果订单完成或取消,商品回到在售或标记售出 if (targetStatus === ORDER_STATUS.COMPLETED) { await pool.query(`UPDATE product SET status = ? WHERE product_id = ?`, [PRODUCT_STATUS.SOLD, order.product_id]); } else if (targetStatus === ORDER_STATUS.CANCELED) { await pool.query(`UPDATE product SET status = ? WHERE product_id = ?`, [PRODUCT_STATUS.ON_SALE, order.product_id]); } res.json({ code: 0, data: { status: targetStatus } }); } module.exports = { getProducts, createProduct, createOrder, updateOrderStatus };

这段代码有几个关键点。SELECT ... FOR UPDATE是行级锁,在 MySQL 事务或自动提交下,锁住商品行,防止并发下单导致超卖。UPDATE product SET status = ? WHERE product_id = ? AND status = ?是乐观锁的玩法:更新时再次校验状态,如果状态已经被别人改了,影响行数为 0,可以据此判断冲突。订单操作里的权限校验是必要的,尤其是“取消”操作,买卖双方都可能有取消诉求,但在不同状态下的取消含义不同,所以我在代码里做了细分:买家在PENDING状态可以取消,卖家在PENDING状态可以直接取消,但在CONFIRMED状态卖家不能再取消,只能等买家确认或申请平台介入。注意order是 MySQL 的保留字,表名要加反引号。参数说明:action字段定义在接口文档里,前端只传字符串,后端做映射;如果你用 TypeScript,可以用联合类型约束更严格,但 JavaScript 项目里靠文档和这层校验就足够了。

4.3 小程序端 wx.request 封装与请求拦截

原生wx.request用起来比较繁琐,我习惯封装成一个request函数:

// utils/request.js const BASE_URL = 'http://localhost:3000/api'; function request(path, options = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', Authorization: wx.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res); return; } if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: reject }); }); } module.exports = { request };

封装后的使用方式:

const { request } = require('../../utils/request'); request('/products', { data: { page: 1, pageSize: 10 } }).then((data) => { console.log(data.list); });

BASE_URL可以放在单独配置文件里,根据开发/生产环境切换。headerAuthorization可以从缓存读取,这样每个接口自动带 token。res.data.code === 0是业务成功判断,如果后端返回 401 或业务码非 0,统一在这里处理,业务代码里就不需要到处写错误提示了。注意,wx.request默认超时时间是 60 秒,如果后端接口慢,可以在wx.request中加timeout参数,比如实际请求量大的时候把超时改成 10000 毫秒。

4.4 真机调试、抓包定位与常见报错

开发微信小程序,模拟器能跑通不代表真机没问题。真机调试有两种方式:一种是在微信开发者工具中点击“预览”生成二维码,用手机微信扫码打开;另一种是使用真机调试 2.0,可以像浏览器 DevTools 一样看 Console 和 Network。如果小程序运行报错,最常见的坑有三个:

  • request 合法域名校验失败:在开发者工具中勾选“不校验合法域名”,或者在微信公众平台后台配置 request 合法域名。本地开发建议用前者,上线前必须配置 HTTPS 域名。
  • 无法连接服务器:手机和电脑不在同一个局域网,或者后端服务监听的是127.0.0.1。本地联调时把后端监听地址改成0.0.0.0,并让手机访问电脑的局域网 IP。
  • 图片上传失败wx.uploadFileurl不能与wx.requestbaseUrl混用,需要单独定义UPLOAD_URL

如果需要抓包分析请求细节,可以借助 Charles。电脑端启动 Charles 后,手机设置 HTTP 代理指向电脑的 IP 和端口 8888,即可看到小程序发出的 HTTPS 请求。注意微信小程序的部分接口走的是 HTTPS,Charles 默认会拦截,但你可以只关注自己的接口。证书安装这一步比较繁琐,但为了定位问题值得做。实在搞不定,也可以用微信开发者工具自带的 Network 面板,虽然比 Charles 功能少,但能看到请求头、响应体和耗时,大多数问题足够定位。抓包时重点看三处:请求 URL 是否用了可访问的地址、请求 Header 是否带了正确的 token、响应 JSON 的 code 字段是不是 0。只要这三处正常,联调问题通常已经走完八成了。

5. 文档说明与源码交付:让项目源码具备可持续性

5.1 README 里必须写清楚的五件事

源码包交付不是把代码压缩发过去就完事了。接手的人要能快速跑起来,至少要看到以下内容:

  • 项目简介:这个二手交易平台解决什么问题,核心功能列表;
  • 技术栈:前端用 JavaScript 原生小程序,后端用 Node.js + Express + MySQL;
  • 环境要求:Node.js 版本(建议 14+)、MySQL 版本、微信开发者工具版本;
  • 启动步骤:创建数据库并导入 SQL 脚本,修改db.js的数据库配置,npm installnpm start,最后用微信开发者工具导入小程序目录;
  • 目录结构:说明client(小程序代码)和server(后端代码)分别在哪。

README 开头不要写一堆介绍语,直接写“如何运行”,因为看文档的人第一需求就是跑起来。如果你把 README 放在 GitHub 上,还可以在顶部加一个徽章或者一张效果截图,但那不影响核心功能描述。

5.2 接口文档与 SQL 初始化脚本的组织方式

接口文档我推荐用 Markdown 形式,命名API.md,放在docs目录下。每个接口写清楚:用途、URL、请求方法、请求参数、响应示例。上面第二章的字段表可以直接进文档。SQL 脚本命名为init.sql,放在server/sql目录下,包含建库、建表和初始数据(如分类字典)。脚本要可重复执行,建议在开头加DROP TABLE IF EXISTSCREATE DATABASE IF NOT EXISTS

# 后端启动 cd server npm install # 修改 db.js 中的数据库密码 node app.js # 小程序端 # 用微信开发者工具导入 client 目录,并将 BASE_URL 改为 http://localhost:3000/api

这两个步骤必须能从零执行,不要省略任何细节。比如我第一次接手别人的二手项目时,对方没给 SQL 脚本,我花了半小时建表,还猜了好几个字段类型,这种体验很容易让人放弃。所以交付文档里,SQL 脚本、接口文档、README 三件套缺一不可。如果你愿意多花一点时间,可以再写一个DEPLOY.md说明如何部署到云服务器和配置 HTTPS 域名,但那是上线阶段的事,课程设计不强制。

5.3 验收清单:从克隆源码到跑通全流程的检查表

最后给一份自测清单,适合开发者在交付前逐项检查,也适合接手者验证项目是否完整。核心流程是:注册登录 → 发布商品 → 在首页看到 → 另一个账号下单 → 卖家确认 → 买家收货 → 商品变为已售。

检查项预期结果状态
导入 SQL 脚本后执行node app.js后端监听 3000 端口,无报错
微信开发者工具导入client后编译首页显示商品列表(或空状态提示)
用手机号登录(或模拟登录)获取到 openid,token 写入 Storage
发布一件“九成新教材”首页出现该商品,价格和图片正确
使用第二个账号下单商品状态变为锁定,第一个账号的订单列表出现待确认订单
卖家确认订单买家订单状态变为待收货
买家确认收货订单状态变为已完成,商品状态变为已售
买家取消订单(未确认前)订单状态变为已取消,商品回到在售

这份清单可以直接复制进你的交付文档,作为源码包的最后一个文件。把验收清单命名为CHECKLIST.md放进项目根目录,接手的同学从拉取代码到跑通全流程,只需要对照它逐项勾选;勾到最后一项时,这个校园二手交易微信小程序项目就可以放心写上“已交付”了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询