资讯动态

校园二手交易微信小程序开发:登录鉴权、数据库设计与订单状态管理

发布时间:2026/9/10 4:52:52 来源:尧图企业网站定制
简介本资源是一套基于微信平台的校园二手交易平台小程序完整项目面向计算机相关专业学生、毕业设计开发者及小程序入门学习者。项目采用SSM框架、Java技术、MySQL数据库与微信小程序端配合B/S架构实现覆盖登录注册、商品发布与交易、管理员后台及卖家功能模块等核心流程并配有详细说明文档与系统测试内容适合用于课程设计、毕设选题及实战练手。压缩包共1412个文件约24.8MB包含Java源码、Vue后台页面、微信小程序wxml/wxss/js文件、数据库SQL脚本及说明文档等整体目录结构清晰便于按前后端模块定位学习。已有1500余人浏览学习。借助这份资源读者可获得可直接运行的源码框架、数据库设计思路以及从需求分析到系统实现的完整项目文档有助于快速理解校园二手交易类小程序的开发全流程。1. 校园二手交易选择微信小程序而不是 App 的原因与边界校园二手交易平台真正的技术考题不在“二手”两个字而在“发现”和“信任”买家怎么知道你有什么双方怎么不见面就把东西卖出去。微信小程序在这里占了两点便宜——学生离不开微信扫码就能进不需要应用商店审核商品卡片可以直接甩进宿舍群和朋友圈获客半径天然比 H5 大。这篇文章按我自己搭这类项目时会走的路线来讲先定数据库边界再写小程序端最核心的登录、发布、下单三条链路接着补齐后端鉴权和图片存储最后落到开发者工具里的验证和审核避坑。适合正在做毕设、校园创业 MVP 或想熟悉微信生态开发的读者。提前说一个判断支付相关的能力需求弱后文会专门解释为什么校园二手别急着接微信支付 V3也别指望文档里把证书问题写清楚就能省掉资质门槛。2. 数据库与功能模块把闲鱼那套交易模型压缩进微信小程序2.1 功能边界先定下来不做什么比做什么更重要实际做校园二手交易平台最容易犯的错是把闲鱼的所有功能都搬过来。闲鱼有闲鱼币、直播、验货宝那是阿里生态的资源位不是一个校园团队该承担的复杂度。我一般会把功能边界收敛成一张表模块必须做必须不做微信平台对应能力用户微信一键登录、学号展示、个人主页积分系统、关注关系wx.login、getUserProfile商品发布、多图上传、编辑、下架、搜索筛选拍卖、竞价、直播wx.chooseMedia、wx.uploadFile订单买家发起意向、卖家确认、线下成交标记在线支付、退款订阅消息、客服会话消息订阅消息推送、订单留言实时 IM、已读回执requestSubscribeMessage表里的“必须不做”不是偷懒而是对应微信平台的合规成本。比如在线支付在小程序里要走微信支付 V3光有商户资质还不够申请后要在商户平台 API 安全里配置平台证书我见过很多项目卡在“无可用的平台证书”这一步实际上要先去商户平台下载证书并配置 API v3 密钥才能继续调起支付。校园二手每单金额通常只有几十元支付链路带来的研发和审核成本远比成交价值高所以项目标准做法是“站内约定、线下支付、线上确认”。边界定完数据库结构就清楚了核心只有用户表、商品表、订单表外加一张可选的收藏表。2.2 用户表用 openid 做唯一键学号只做展示字段微信小程序没有密码登录这个概念。用户进入小程序后前端调用 wx.login 拿到 code后端拿 code 换 openid。openid 是用户在某个小程序下的稳定身份标识同一用户在不同小程序里 openid 不同但在同一小程序里不会变。因此用户表主键直接用自增 ID再加一个 openid 唯一索引而不是反过来用学号做主键。CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, openid CHAR(28) NOT NULL COMMENT 微信 openid小程序内唯一, nickname VARCHAR(64) NOT NULL DEFAULT COMMENT 昵称前端展示用, avatar_url VARCHAR(255) NOT NULL DEFAULT COMMENT 头像地址, student_no VARCHAR(20) NOT NULL DEFAULT COMMENT 学号仅展示用不做登录凭证, campus TINYINT NOT NULL DEFAULT 0 COMMENT 校区0 未填1 本部2 东区3 西区, credit_score SMALLINT NOT NULL DEFAULT 100 COMMENT 信用分初始 100, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 正常0 封禁, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这里有两个参数值得留意。第一个是uk_openid唯一索引必须加否则 code2Session 重复回调时可能插入两条同一用户的记录。第二个是student_no字段只做展示、不做登录判断道理很直接学号可以被旁人知道拿学号当凭证等于裸奔。需要真实身份认证时正规做法是接学校统一身份认证或邮件验证码而不是前端传一个学号字符串就当成认证结果。2.3 商品表和订单表状态字段不要散着写商品表的常见误区是用“在售/已售”两个状态搞定一切结果买家点了“我想要”商品却还挂在列表里产生超卖。我习惯用四个状态1 在售、2 已被锁定、3 已卖出、4 已下架。CREATE TABLE goods ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, seller_id INT UNSIGNED NOT NULL COMMENT 对应 user.id, title VARCHAR(100) NOT NULL COMMENT 商品标题搜索关键词来源, description TEXT NOT NULL, price_cents INT UNSIGNED NOT NULL COMMENT 价格单位分避免浮点误差, images JSON NOT NULL COMMENT 图片 URL 数组最多 9 张, category TINYINT NOT NULL DEFAULT 0 COMMENT 分类0 教材1 数码2 生活用品3 其他, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 在售2 已被锁定3 已卖出4 已下架, view_count INT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;注意price_cents用整数存分而不是 DECIMAL 或 FLOAT这能避免后续计算时 0.1 加 0.2 这类浮点问题。images用 JSON 字段而不是单独建图片表因为查询商品详情时不需要 JOIN 一次图片表微信平台限制最多 9 张图JSON 数组足够用。订单表比商品表多一个买家维度同时要在业务上保证“一件商品只有一个进行中的订单”CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, goods_id INT UNSIGNED NOT NULL, buyer_id INT UNSIGNED NOT NULL, seller_id INT UNSIGNED NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 买家发起2 卖家确认3 已成交4 已取消, meet_point VARCHAR(255) NOT NULL DEFAULT COMMENT 线下交易地点如 图书馆北门, remark VARCHAR(255) NOT NULL DEFAULT , created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_goods_active (goods_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;uk_goods_active这个唯一索引在不同状态语义下有不同含义这里真正要保证的是同一商品同时只有一个状态为 1 的有效订单。数据库层面能兜住业务并发比在代码里 SELECT 再 INSERT 两个步骤更可靠。这也是说明文档里我会单独画状态流转图的地方因为前后端联调时几乎所有争议都发生在“商品被锁定后还能不能改价”“订单取消后商品要不要回到在售”这类状态迁移问题上。3. 小程序端核心链路登录态、发布商品、下单操作的可运行代码3.1 登录态wx.login 到 code2Session 再到自定义 token微信小程序的官方登录流程是前端 wx.login() 拿临时 code传给后端后端用 code 调 code2Session 接口换 openid 和 session_key再返回给前端一个业务 token。实际代码层面最常见的错误是前端每次进页面都重新 wx.login()正确做法是登录一次拿 token后续请求都带 token。// utils/login.js const login () { return new Promise((resolve, reject) { wx.login({ success: async (res) { if (!res.code) { reject(new Error(wx.login 拿不到 code)); return; } try { const resp await wx.request({ url: https://your.domain.com/api/auth/login, method: POST, data: { code: res.code } }); const { token } resp.data; wx.setStorageSync(token, token); resolve(token); } catch (e) { reject(e); } }, fail: reject }); }); }; module.exports { login };后端拿到 code 后调微信接口要用 appid 和 secret。这里有个安全点secret 绝不能出现在小程序前端代码里必须放在后端环境变量中。# app.pyFlask 示例 import requests from flask import Flask, request, jsonify app Flask(__name__) app.post(/api/auth/login) def auth_login(): code request.json.get(code) appid app.config[WX_APPID] secret app.config[WX_SECRET] resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: appid, secret: secret, js_code: code, grant_type: authorization_code } ).json() # resp 形如 {openid: ..., session_key: ...} # 出错了是 {errcode: 40029, errmsg: invalid code} if openid not in resp: return jsonify({error: code 已失效或 appid/secret 不匹配}), 400 token create_token(resp[openid]) # 业务自签 token内部映射 openid return jsonify({token: token})参数说明js_code 是一次性的有效期约 5 分钟不能重复换取在开发者工具里连续点登录按钮容易报40029错误这不是代码问题重新触发一次 wx.login 即可。session_key 不要下发到前端它用于解密手机号和后续敏感操作前端拿到它没有意义。如果后端查日志发现 code2Session 返回40163说明 code 被用过检查是不是前端重复提交了登录请求。3.2 发布商品先传图片拿 URL再提交商品表单发布商品看似是选图、填表、提交三步实际有前后依赖。正确顺序是先上传图片拿到图片 URL 数组后再连同表单数据提交给后端。如果反过来先提交商品再传图片商品与图片之间会形成两阶段写入失败时要清理脏数据。// pages/publish/index.js Page({ async onChooseImage() { const res await wx.chooseMedia({ count: 9, // 小程序限制一次最多 9 张 mediaType: [image], sourceType: [album, camera], sizeType: [compressed] // 拿压缩图原图可能 5MB 以上 }); const tasks res.tempFiles.map((file, i) this.uploadOne(file.tempFilePath, i) ); const urls await Promise.all(tasks); this.setData({ imageUrls: urls }); }, uploadOne(filePath, index) { return new Promise((resolve, reject) { wx.uploadFile({ url: https://your.domain.com/api/upload, filePath, name: file, formData: { index: String(index) }, success: (res) { const data JSON.parse(res.data); resolve(data.url); }, fail: reject }); }); }, async onSubmit(e) { const form e.detail.value; // 通过 form 组件拿数据 const payload { title: form.title, description: form.desc, price_cents: Math.round(parseFloat(form.price) * 100), images: this.data.imageUrls }; const resp await wx.request({ url: https://your.domain.com/api/goods, method: POST, data: payload, header: { Authorization: Bearer ${wx.getStorageSync(token)} } }); if (resp.data.id) { wx.showToast({ title: 发布成功, icon: success }); } } });价格用Math.round(parseFloat(form.price) * 100)转成整数分避免“9.99 元”在小数运算中变成 9.989999。上传并发没有特殊设置wx.uploadFile 默认并发数是 10校园 Wi-Fi 下够用如果发现图片顺序乱了在 formData 里带上 index再用 Promise.all 保持顺序拼接 URL 数组。图片尺寸上chooseMedia 已经返回压缩图但如果选原图再压缩可以在本地用 canvas 缩放一次把单张限制在 500KB 以内能显著减少后端存储压力详情页加载也不会白屏。3.3 下单流程一个“我想要”按钮同时改商品和订单状态买家在商品详情页点“我想要”前端把商品 ID 传给后端后端在一个数据库事务里做两件事把 goods.status 从 1 改成 2插入一条订单记录。两个操作必须放在同一事务否则会出现“订单插进去了但商品还能被别人买”的竞态。// pages/goods-detail/index.js async onTapWant() { const goodsId this.data.goods.id; try { const resp await wx.request({ url: https://your.domain.com/api/orders, method: POST, data: { goods_id: goodsId, meet_point: 图书馆北门 }, header: { Authorization: Bearer ${wx.getStorageSync(token)} } }); if (resp.data.order_id) { wx.showToast({ title: 已通知卖家, icon: none }); this.setData({ goodsStatus: 2 }); } } catch (e) { wx.showToast({ title: e.errMsg || 下单失败, icon: none }); } }卖家端是订单列表页每个订单有“确认交易”按钮。确认后订单状态变为 2此时调订阅消息接口通知买家并展示约定见面地点和时间。不要在这个页面堆叠太多跳转微信小程序页面栈最多 10 层如果从商品页跳到详情页再跳到订单确认页用户反复进出很容易触发navigateTo栈溢出报错页面直接卡死这类问题在真机上复现概率比开发者工具高得多。3.4 请求层封装不要在每个页面重复写 wx.request整个小程序至少有登录、发商品、下订单、拉列表、改状态五个地方要发请求我习惯封装一个 request 函数统一处理 token 注入、超时、401 重新登录// utils/request.js const request (options) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ ...options, url: https://your.domain.com options.url, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, timeout: 8000, success: (res) { if (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/index }); reject(new Error(登录已过期)); return; } resolve(res.data); }, fail: reject }); }); }; module.exports { request };timeout 8000 这个值校园网高峰期延迟可能到两三秒太短会让请求被误判失败太长用户体验差。上传图片要用 wx.uploadFile它用的是 uploadTask 独立通道超时控制另算不要和 request 混在一起。还有一点容易被忽略小程序端拿到的 res.data 在开发阶段可能是字符串因为后端返回的 Content-Type 没设对前端要能兼容typeof res.data string时先 JSON.parse 一下否则真机上报错很难排查。4. 后端接口与图片存储用 Flask 几十行代码搭起最小可用方案4.1 接口路由设计RESTful 风格但不过度抽象后端这里直接用 Flask 单文件就能跑通。接口一共五条覆盖最核心流程方法路径作用POST/api/auth/login登录返回 tokenPOST/api/upload上传图片返回 URLPOST/api/goods发布商品PUT/api/goods/{id}修改或下架商品POST/api/orders发起订单意向app.post(/api/goods) def create_goods(): uid get_current_user_id() # 从 token 解析 data request.json goods Goods( seller_iduid, titledata[title], descriptiondata[description], price_centsdata[price_cents], imagesjson.dumps(data[images]), status1 ) db.session.add(goods) db.session.commit() return jsonify({id: goods.id}), 201参数说明get_current_user_id() 是从请求头 Authorization 里解析 token具体实现在下一节。写接口时注意不要把 seller_id 放在请求体里让前端传否则任何用户都能指定别人的 seller_id 冒充发布这个值必须从 token 里解出来。price_cents 在后端也可以再校验一次必须大于 0前端做校验只是体验后端做校验才是安全。4.2 鉴权中间件统一校验 token 和资源归属微信生态里接口鉴权最常见的错误是每个视图函数各写一套 token 解析有的还漏掉资源归属校验。统一中间件可以这样写from functools import wraps from flask import request, jsonify, g def login_required(f): wraps(f) def wrapper(*args, **kwargs): auth request.headers.get(Authorization, ) token auth.replace(Bearer , ) user_id verify_token(token) if user_id is None: return jsonify({error: unauthorized}), 401 g.user_id user_id return f(*args, **kwargs) return wrapper app.get(/api/orders/mine) login_required def my_orders(): uid g.user_id return jsonify(db_get_orders_by_buyer(uid))很多跨用户越权漏洞出在资源归属校验上。修改商品接口要校验当前登录用户是不是商品卖家app.put(/api/goods/int:goods_id) login_required def update_goods(goods_id): goods db.session.get(Goods, goods_id) if not goods: return jsonify({error: not found}), 404 if goods.seller_id ! g.user_id: return jsonify({error: forbidden}), 403 # 只允许在售和已下架之间切换已锁定或已卖出不允许修改 if goods.status not in (1, 4): return jsonify({error: current status not editable}), 400 return jsonify({ok: True})这部分代码量不大但它是整个源码里安全价值最高的十几行。开发时容易犯的错是只校验“是否登录”不校验“是否是资源所有者”这样只要拿到商品 ID 就能任意操作。还有一点订单接口要校验买家不能对自己发布的商品下单即 goods.seller_id 不能等于 g.user_id业务逻辑虽小漏了就会产生自买自卖的刷数据问题。4.3 图片存储本地磁盘写一版再决定要不要迁对象存储毕设和校园项目初期没有云存储账号首先用 Flask 把图片保存到服务器本地磁盘import os from flask import request from werkzeug.utils import secure_filename UPLOAD_DIR /data/campus_secondhand/images ALLOWED_EXT {jpg, jpeg, png, webp} app.post(/api/upload) login_required def upload_image(): file request.files.get(file) if not file: return jsonify({error: no file}), 400 ext file.filename.rsplit(., 1)[-1].lower() if ext not in ALLOWED_EXT: return jsonify({error: bad extension}), 400 folder str(g.user_id) os.makedirs(os.path.join(UPLOAD_DIR, folder), exist_okTrue) fname secure_filename(f{uuid4().hex}.{ext}) file.save(os.path.join(UPLOAD_DIR, folder, fname)) return jsonify({url: f/images/{folder}/{fname}})这个方案有几个要注意的地方。图片名不要直接用用户传来的原名会产生撞名和解码问题扩展名必须做白名单遇到 php、py 这类直接拒绝。本地磁盘方案缺点明显单机磁盘扩容要动服务器、没有备份适合日 UV 低于 500 的阶段性项目。流量起来后建议迁对象存储迁移成本集中在图片 URL 域名变化代码逻辑几乎不用动。对比项本地磁盘对象存储部署成本零额外依赖需要 Bucket 和密钥扩容手动挂盘按量自动外链访问需要 Nginx 映射静态目录自带 CDN 不耗服务器带宽安全需自己加防盗链提供签名 URL4.4 订阅消息额度有限只用在订单关键节点订阅消息是小程序触达用户的唯一官方推送通道规则是“用户点击一次授权只能推送一次”。所以不要在所有行为上都弹订阅授权把额度留给最关键的“卖家确认交易后通知买家”这一个节点。// 买家发起订单时预先请求订阅授权 await wx.requestSubscribeMessage({ tmplIds: [YOUR_TEMPLATE_ID] });后端在卖家确认订单时推送def send_order_notify(openid, order_id, goods_title): body { touser: openid, template_id: YOUR_TEMPLATE_ID, page: f/pages/order/detail?id{order_id}, data: { thing1: {value: goods_title}, thing2: {value: 卖家已确认请联系交易} } } token get_access_token() resp requests.post( https://api.weixin.qq.com/cgi-bin/message/subscribe/send, params{access_token: token}, jsonbody ) return resp.json()推送时绑定的 openid 是买家的不是卖家的。模板内容要和申请模板时字段保持一致模板里有的字段才能填没有的字段填了会被微信拒绝。订阅消息一次授权的额度只有一次买家取消订单再重新下单时会发现推送次数用完需要考虑前端在什么时机请求授权通常在用户首次点击“我想要”时请求比较自然。5. 上线前验证开发者工具调试、审核被拒原因与交易安全边界5.1 开发者工具上必做的三个调试场景第一是登录态失效把开发者工具“清除缓存 → 清除数据缓存”后立刻进入商品详情页能正常弹回登录页才算过关。第二是弱网模拟开发者工具的 Network 面板可以限速到 2G/3G这时候发布商品如果超时要看前端有没有展示重试按钮而不是白屏。第三是图片顺序上传时故意乱序选择图片发布后在详情页检查图片顺序是否和用户点选顺序一致。这三个场景覆盖了登录、上传、状态同步三条最容易出问题的链路。5.2 审核常见被拒原因微信小程序审核和普通网站不同它先看资质和页面是否符合类目再检查功能。校园二手一般选“二手闲置交易”或“电商平台”类目需要提交营业执照或学校社团的资质证明。另一个高频拒审原因是隐私协议缺失小程序里如果收集了学号、手机号甚至仅收集 openid都需要在小程序后台配置《用户隐私保护指引》并把弹窗说明写清楚。订阅消息的授权文案必须写“用于接收交易订单进度通知”不能写“为了更好的服务体验”这种含糊描述。源码包里附带的说明文档最好专门写一节审核材料清单包括需要准备什么证照、每个权限对应的用途描述节省反复提交的时间。5.3 为什么校园二手不建议接微信支付 V3最后说回开头那个判断。微信支付 V3 需要企业主体申请商户号个人开发者拿不到即使有营业执照还要申请平台证书、配置 API v3 密钥中间任何环节不一致就报“无可用的平台证书”错误。校园二手每笔几十块钱手续费、退款、对账的成本远大于利润空间而且一旦接入支付就涉及售后纠纷和资金池问题学生团队没有客服团队去处理这些。行业成熟做法是“线下当面交易线上确认状态”平台只负责撮合和规则不碰资金流。这个边界想清楚整个项目周期可以从一个月压缩到两周文档里需要写清楚的核心逻辑也会从支付对账变成状态机、鉴权和图片上传这三件事。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价