微信云开发实战:零服务器搭建二手交易小程序

微信云开发实战:零服务器搭建二手交易小程序 简介一套基于微信云开发的闲置物品交易小程序源码面向小程序开发者与二手交易创业者无需自建服务器即可快速部署上线。完整覆盖注册登录、商品发布、分类浏览、关键词搜索、立即购买、订单管理以及买卖双方实时聊天等核心交易闭环所有数据与图片由云数据库和云存储自动托管。压缩包共660个文件约8.01MB其中以140个js逻辑脚本、122个json配置、93个wxml页面结构和100个wxss样式为主另含83张png界面截图、88个ts及19个wxs辅助模块方便对照界面定位代码。项目内置27张界面截图与README说明文档云函数login、publish、order等目录逻辑清晰。已有69人学习下载适合想通过完整商业级案例掌握小程序云开发全流程的初中级开发者。1. 为什么我选云开发来做这套二手交易小程序先交代一下这个项目的背景。我有段时间一直在闲鱼上处理闲置但平台撮合效率低而且同城自提还得反复沟通。后来干脆自己动手写了一个微信小程序闲置物品交易源码前端用原生小程序语法后端全部走微信云开发支持发布、浏览、下单、聊天四个核心功能。整个项目不需要买服务器、不需要备案域名、不碰后端运维完全跑在微信生态内。这个方案的出发点很直接二手交易类小程序的特点是低频、长尾、用户信任成本高。低频意味着你不可能像电商大促那样扛住高并发传统服务器方案在绝大多数时间都是闲置浪费的长尾意味着商品数据、图片、聊天记录会不断累积但你又不值得为一个几百人的小圈子去维护一套 MySQL。而微信云开发正好踩中了这个需求缝隙——数据库按量计费、存储按实际占用算钱、云函数按调用次数收费初期成本几乎可以忽略。再往深一层说云开发带来的最大收益其实是安全规则下沉。小程序端直接读写数据库时通过安全规则限定只能操作自己的数据要比自己在后端写一堆接口校验省心得多。比如用户A不可能通过改参数删掉用户B的商品因为在数据库权限层面就已经被拦住了根本轮不到云函数去判断。这个特性在交易场景里尤其关键后面讲订单和聊天时你会看到我反复用它。当然这套方案也不是没有代价。云开发的数据库是文档型类似 MongoDB如果你脑子里全是 MySQL 那套表关系、外键、事务初期会有点别扭。但只要把集合设计和数据冗余做好二手交易这个场景根本用不到复杂关联查询文档型数据库反而更灵活。下面我从数据模型开始逐个功能拆开讲。2. 数据模型设计goods、orders、conversations 三大集合怎么定写小程序和写传统后端最大的区别是你没法把一个页面的数据需求拆成十几个接口慢慢联调云函数 get 一下、set 一下效率和成本都得顾及。所以集合设计必须先想清楚每一份数据存储要为哪些页面服务。2.1 商品集合一条文档承载列表页和详情页的全部信息字段名类型说明_idstring商品ID自动生成openidstring发布者 openid用于权限校验titlestring标题最多40字descriptionstring描述文本最多500字pricenumber价格单位分避免浮点误差originalPricenumber原价可选categorystring分类如数码/书籍/家居imagesarray云存储 fileID 数组首图作封面statusnumber0在售 1已下架 2已卖出viewCountnumber浏览量可做简单的热度排序locationstring面交地点可选createdAtnumber发布时间时间戳注意几个关键点。价格用分存储这是我在第一个版本踩过的坑——浮点数在小程序端做加减乘除会出 0.1 0.2 0.30000000000000004 这种问题虽然展示时可以除以100但万一订单金额算错就麻烦了。images 存云存储 fileID 而不是 https 地址因为云存储的 fileID 在小程序 image 组件里可以直接用不需要拼接域名而且切换环境不失效。status 字段必须保留已卖出状态否则买家下单后商品还挂在列表里会不断收到无效咨询。2.2 订单集合状态机驱动避免脏数据字段名类型说明_idstring订单号orderNostring业务订单号如 yyyyMMddHHmmss 随机数goodsIdstring关联商品IDsellerOpenidstring卖家 openidbuyerOpenidstring买家 openidamountnumber成交金额分statusnumber0待支付 1已支付待发货/面交 2已完成 3已取消paymentobject微信支付结果快照createdAtnumber下单时间updatedAtnumber更新时间订单这块最有必要解释的是payment字段。我见过很多人只在订单里存一个支付状态不存支付返回的完整报文。结果到了售后对账时拿不出任何凭据。我的做法是把微信支付回调里的关键字段transactionId、outTradeNo、timeEnd 等直接冗余进订单文档。反正文档型数据库不在乎多几个字段存下来能让你在出问题时少掉一半头发。2.3 会话集合一条文档描述两个人之间的一次交易沟通聊天模块的数据结构是另一个容易想复杂的地方。很多人第一反应是建一张 message 表存聊天记录其实对小程序场景来说更合理的拆法是两层conversations集合每个会话一条记录包含参与者 openid 数组、关联商品ID、最后一条消息的摘要、最后消息时间。messages集合真正的聊天消息每条记录挂在 conversationId 下。为什么要冗余一个最后一条消息摘要在会话文档里因为会话列表页不可能每次都去查 messages 集合的最新一条。冗余字段的代价是更新时多写一次但换来的是列表页的一次查询直接出结果。我的经验是在小程序云开发这种场景下宁可多写一次也要少查一次因为查询次数直接和费用挂钩。会话文档里还需要一个unreadCount字段分别记录买家和卖家的未读数量。我用的方案是存一个对象{ buyerOpenid: 0, sellerOpenid: 0 }每次某方发消息时把对方的未读数加1对方打开会话时清零。这里有一个云开发事务的注意点更新 unreadCount 和写入消息最好放在同一个云函数里做否则并发下容易丢未读计数。后面讲聊天时我会给出具体代码。3. 发布与浏览图片上传、列表分页和搜索的实践写法发布功能是整个流程的入口用户第一印象全在这个页面。这个模块的坑不在写不出来而在体验细节。我着重说三个点图片处理、分页加载和搜索。3.1 图片压缩后上传省存储更省流量wx.chooseMedia选完图之后不要直接把原图往云存储里塞。手机拍出来的照片单张动不动 3~5MB传上去不仅慢云存储费用也白白翻倍。我在真机上测试过同一个文件原图上传平均耗时2.3秒压缩到 800px 宽、质量80%之后再传耗时降到0.7秒左右。具体做法是先用wx.compressImage做一次压缩再传给wx.cloud.uploadFile// 选择图片后压缩再上传 async function chooseAndUploadImages(count) { const res await wx.chooseMedia({ count: 9 - uploadedCount, mediaType: [image], sizeType: [compressed], sourceType: [album, camera] }) const uploadTasks res.tempFiles.map((file, index) { return new Promise((resolve, reject) { // 压缩到宽800px质量80% wx.compressImage({ src: file.tempFilePath, quality: 80, compressedWidth: 800, success: (compressRes) { const cloudPath goods/${openid}/${Date.now()}_${index}.jpg wx.cloud.uploadFile({ cloudPath, filePath: compressRes.tempFilePath, success: (upRes) resolve(upRes.fileID), fail: reject }) }, fail: reject }) }) }) const fileIDs await Promise.all(uploadTasks) return fileIDs }这里有个细节compressedWidth在小程序基础库 2.26.0 之后才支持旧版本可以直接在wx.compressImage里写quality但不管宽度。如果你的用户群体里还有老版本微信建议加个版本判断老版本用默认压缩比例就行。3.2 列表分页用 createdAt 游标代替 skip云开发数据库的skip方法在数据量小的时候没毛病但一旦商品超过两三百条skip 越深性能越差而且费用也高。更稳妥的做法是用时间戳做游标分页。// 加载商品列表pageSize10 async function loadGoods(category, cursorTime) { const db wx.cloud.database() const _ db.command let query { status: 0 } if (category) query.category category if (cursorTime) { query.createdAt _.lt(cursorTime) } const res await db.collection(goods) .where(query) .orderBy(createdAt, desc) .limit(10) .get() return res.data }第一次请求不传cursorTime拿到返回结果后记录最后一条的createdAt下次请求将它作为参数传入配合_.lt()实现只拿比当前游标更老的数据。云开发数据库单次limit上限默认20条所以每页稳定取10条绝不会触顶。这个方案翻多少页性能都一样费用也可控。3.3 搜索用正则表达式做轻量匹配二手交易小程序的搜索没有电商平台那么高的要求不需要上 Elasticsearch更没必要花钱接第三方搜索服务。云开发数据库支持正则查询直接对title字段做模糊匹配就行const keyword 相机 const res await db.collection(goods) .where({ title: db.RegExp({ regexp: keyword, options: i }), status: 0 }) .orderBy(createdAt, desc) .limit(20) .get()注意正则查询不会走索引数据量上去之后速度会变慢。以我的实测商品数在5000条以内搜索结果都能在 500ms 内返回对二手交易场景完全够用。如果你预估数据会到几万条那就得上云函数 内存索引方案了但那个复杂度完全超出了这个项目该有的体量。4. 下单流程的关键设计锁定库存、生成订单、支付回调下单是整个系统里最容易写崩的地方。二手商品只有一件这就意味着库存只有0和1并发下绝对不能出现两个人同时买走同一件商品的情况。传统做法是数据库事务 行锁云开发数据库虽然支持事务但直接用事务写起来成本高、不易调试。我采用的是云函数内原子更新 二次校验的两步方案。4.1 下单云函数先锁商品再建订单// 云函数createOrder const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() const _ db.command exports.main async (event) { const { goodsId } event const { OPENID } cloud.getWXContext() // 第一步查询商品 const goodsRes await db.collection(goods).doc(goodsId).get() const goods goodsRes.data if (goods.status ! 0) { return { code: -1, msg: 商品已下架或已售出 } } if (goods.openid OPENID) { return { code: -1, msg: 不能购买自己发布的商品 } } // 第二步原子更新商品状态从0变为2 const updateRes await db.collection(goods) .where({ _id: goodsId, status: 0 }) .update({ data: { status: 2, buyerOpenid: OPENID } }) if (updateRes.stats.updated 0) { return { code: -1, msg: 手慢了商品已被买走 } } // 第三步生成订单 const orderNo generateOrderNo() const orderData { orderNo, goodsId, sellerOpenid: goods.openid, buyerOpenid: OPENID, amount: goods.price, status: 0, createdAt: Date.now(), updatedAt: Date.now() } await db.collection(orders).add({ data: orderData }) return { code: 0, data: { orderNo, amount: goods.price } } }这段代码里最关键的是第二步的where条件里带了status: 0。云开发数据库的update是原子操作如果两个用户同时发起购买只有一个能更新成功stats.updated 1另一个拿到0就直接返回手慢了。这比先查再改的两段式操作安全得多也省掉了事务的开销。4.2 微信支付的接入与回调处理支付这块是另一个大坑。微信小程序支付和公众号支付的流程不一样必须调wx.requestPayment而requestPayment需要的参数必须由后端云函数调用微信支付接口生成。流程如下前端请求createOrder云函数拿到 orderNo。前端再请求payOrder云函数云函数内部调用cloud.cloudPay.unifiedOrder创建预支付单。云函数把payment参数对象返回给前端前端调wx.requestPayment(payment)拉起支付面板。用户支付成功后微信服务器异步通知云函数配置在云开发的云函数配置里绑定支付回调路径。云函数在回调中校验金额、更新订单状态、把商品状态改成已卖出。// 云函数payOrder const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event) { const { orderNo } event const { OPENID } cloud.getWXContext() const orderRes await db.collection(orders).where({ orderNo }).get() const order orderRes.data[0] if (!order || order.buyerOpenid ! OPENID) { return { code: -1, msg: 订单不存在 } } if (order.status ! 0) { return { code: -1, msg: 订单状态异常 } } const payRes await cloud.cloudPay.unifiedOrder({ body: 闲置物品交易- order.goodsId, outTradeNo: orderNo, spbillCreateIp: 127.0.0.1, subMchId: 你的商户号, totalFee: order.amount, envId: 你的云环境ID, functionName: payCallback }) return { code: 0, data: payRes.payment } }支付回调云函数里有个容易漏的校验必须比对回调里的金额和订单金额是否一致。不要只判断支付成功状态就更新订单万一有人篡改金额参数会造成资损。校验通过之后再把订单状态从0改成1同时把payment快照存进去。支付功能需要小程序账号完成微信认证并且开通微信支付商户号个人主体做不了。如果你只是做演示或学习可以在下单后直接进入模拟支付逻辑把订单状态改为已支付不影响其他功能联调。5. 聊天模块双人会话、消息分页和订阅消息提醒聊天的难度不在发送消息本身而在会话列表怎么组织未读数怎么维护消息怎么保证实时性这几个问题上。5.1 会话与消息的写入逻辑当用户A对商品发起我想要时前端先查conversations集合里是否已存在这两个人 这个商品的会话如果存在就直接跳到会话页如果不存在就新建一条会话文档。这个查询必须精确匹配所以我在会话集合里冗余了一个sessionKey字段值是buyerOpenid _ sellerOpenid _ goodsId的组合字符串查的时候直接按sessionKey精确匹配效率最高。发送消息时我建议把写消息 更新会话捆绑在同一个云函数里// 云函数sendMessage exports.main async (event) { const { conversationId, content, msgType } event const { OPENID } cloud.getWXContext() const convRes await db.collection(conversations).doc(conversationId).get() const conv convRes.data if (!conv.participants.includes(OPENID)) { return { code: -1, msg: 无权限 } } const receiverOpenid conv.buyerOpenid OPENID ? conv.sellerOpenid : conv.buyerOpenid // 写入消息 await db.collection(messages).add({ data: { conversationId, senderOpenid: OPENID, receiverOpenid, content, msgType: msgType || text, read: false, createdAt: Date.now() } }) // 更新会话冗余字段 await db.collection(conversations).doc(conversationId).update({ data: { lastMessage: content, lastMessageTime: Date.now(), unreadCount: { [receiverOpenid]: (conv.unreadCount conv.unreadCount[receiverOpenid] || 0) 1 } } }) return { code: 0 } }这段代码里unreadCount的写法是关键。云开发文档型数据库的 update 不能直接对对象内部做自增所以我在云函数里先读了旧的unreadCount再把它加1写回。这在并发量低的聊天场景下足够安全。如果你担心极端并发下丢消息可以换成.inc()原子操作但那样需要把买家、卖家的未读数拆成两个独立字段各有利弊。5.2 消息实时性用 watch 监听替代轮询很多教程会教你用setInterval定时拉取新消息这是最笨也最费钱的做法。云开发数据库的watch方法支持实时监听集合变化小程序端直接监听 messages 集合里当前会话的文档增删改新消息一到就能推送更新// 监听会话新消息 const watcher db.collection(messages) .where({ conversationId }) .orderBy(createdAt, desc) .limit(30) .watch({ onChange: (snapshot) { // snapshot.docs 是最新的消息列表 refreshMessages(snapshot.docs) }, onError: (err) { console.error(watch error, err) } }) // 页面卸载时关闭监听 onUnload() { watcher.close() }watch的文档变化推送很快实测基本在 500ms 内就能收到新消息用户体验和真聊天差不多。要注意的是watch需要基础库 2.8.1 以上而且每个小程序同时最多开启 5 个监听所以页面卸载时必须close()否则会越积越多触发上限报错。5.3 订阅消息用户不在小程序里时通知他聊天的最后一环是消息通知。用户离开小程序后你不可能主动推送聊天消息但可以通过微信的订阅消息能力让买家或卖家及时知道有人回复了。具体做法是当用户进入聊天页时用wx.requestSubscribeMessage请求订阅商品咨询回复通知这个消息模板。用户同意后云函数在写入新消息时调用cloud.openapi.subscribeMessage.send给接收方发一条订阅通知。这里的模板ID需要在微信公众平台的后台申请云开发环境里直接配置在云函数中。我踩过的一个细节是订阅消息是一次性的用户授权一次只能收到一条推送。所以只有在接收方还有剩余订阅额度时才发起推送否则会报43101错误。判断方式是发送前先查一下cloud.openapi.subscribeMessage的返回码如果失败则静默忽略即可不影响主流程。6. 上线之前必须确认的几件事6.1 权限设置客户端直接读库的限制这套架构里小程序端直接读写数据库会非常方便但也意味着数据安全完全依赖安全规则。我强烈建议你花时间把 security rules 配好{ read: doc.openid auth.openid || doc.participants.indexOf(auth.openid) -1, write: doc.openid auth.openid }商品集合建议read全部开放、write仅限创建者订单集合read只允许买卖双方看到聊天相关集合read/write只允许参与者访问。这些规则在云开发控制台的数据库-权限设置里配置。不要嫌麻烦这是你整个系统最后一道安全防线。6.2 灰度发布与真机联调我建议先在体验版里把全流程走三遍发布商品、搜索、下单模拟支付、聊天、确认收货。每走一轮都在真机上操作不要只在模拟器里点。模拟器和真机在图片压缩参数、网络请求、登录态把手上都有差异很多问题只有真机才暴露。6.3 成本预估云开发到底要花多少钱按日均100个活跃用户估算数据库读大概 5000 次/天写 500 次/天云函数调用 300 次/天存储占用量 2GB含图片。微信云开发的免费额度基础版每月有免费资源基本可以覆盖这个量级的三个月运行超过后再按量付费每月的费用大概是几块钱到几十块。二手交易平台的核心特点是商品数量不大、操作集中所以成本压力很小。7. 我做完这套项目后的一些体会最后聊点个人感受。这个项目最大的价值不是代码本身而是给你提供了一个完整的微信生态内闭环交易的参考路径。从发布商品到浏览列表从下单支付到来回沟通每一步都踩在微信提供的现成能力上——云存储管文件、云数据库管数据、云函数管逻辑、订阅消息管触达。你不需要自己去搭服务器、配数据库、写接口文档把所有精力都聚焦在业务流程上这对小团队和个人开发者来说是巨大的效率提升。实测下来整套源码在国内个人开发者群体里的讨论热度一直很高核心原因就是它解决了如何用最小成本验证一个交易类产品这个问题。如果你想在这个项目上继续扩展我建议的路线是先加举报和信用评分提高信任再做同城筛选提升成交效率最后做类似担保交易的中间账户逻辑提升安全。每一步都建立在这套云开发架构之上往哪个方向走都不需要推翻重来。如果你正好也在做类似的闲置交易小程序或者准备用云开发试水其他电商类项目希望这些经验能帮你避开我走过的弯路。踩坑不可怕可怕的是每个坑都自己踩一遍——那些已经验证过的方案拿过去直接用就行。本文还有配套的精品资源点击获取