ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flask+微信小程序:同城二手交易系统开发实战攻略

Flask+微信小程序:同城二手交易系统开发实战攻略 做Python-flask同城二手交易小程序这套东西起因挺简单。之前朋友在一个本地二手群里当群主群里每天都有人发转让旧手机、旧家电、婴儿车的信息消息一多就刷屏成交基本靠运气。后来我们干脆自己动手用Flask搭了后端配上微信小程序当前端把“同城二手交易”从登录、发布、找附近商品到沟通和下单的完整闭环做了一遍。这个项目很适合想同时练后端和小程序的人做毕设、课设或者接外包之前拿来找手感都很合适。同城二手交易跟普通电商最大的区别就是“同城”两个字。二手商品有天然的地域性一件沙发运到外地的物流成本可能比沙发本身还贵所以买卖双方需要按距离匹配。这意味着后端要能接收经纬度、能算距离、能按范围过滤小程序端要学会定位、地图选点、权限处理这些都不是写几个增删改查接口就能糊弄过去的。这篇文章不是教科书式讲代码而是把从零搭建这个系统时踩过的坑、做过的技术选型、还有那些文档里查不到的细节全部整理出来。我会按后端主流程、小程序端实现、部署上线、问题排查几个部分展开代码都是能直接搬去改的实战版本。1. 项目需求拆解与技术选型1.1 同城二手交易到底要解决什么问题做项目之前先别急着建表和写接口得想清楚业务到底要什么。二手交易不是普通商城它的核心诉求有三个词就近、信任、议价。就近是说商品最好在同城或几公里范围内买家能看到实物卖家不用打包发货。这个靠LBS经纬度解决而不是靠用户填写的城市名称因为城市字段只能精确到地级市没法回答“离我3公里内有没有在卖冰箱”这个问题。信任是二手交易最大的门槛。平台能做的有限但可以设计几个机制微信实名登录、评价记录、订单完成后的互评还有引导线下见面验货。这些机制会直接影响数据库表怎么设计比如需要给用户加信任分给订单加状态流转日志。议价是跟普通商城差异最大的一点。商城商品价格是固定的二手交易很多时候是先聊后买双方在IM里讨价还价然后才生成订单。所以聊天会话模块不能等到订单产生后再做要在商品详情页就开始支持“我想聊一下”这个入口。1.2 为什么是Flask而不是Django或Node技术选型这块我纠结过一段时间。当时对比了三种方案Flask、Django和Node.js的Express。方案优势劣势适合场景Flask轻量、灵活、上手快路由和蓝图结构清晰很多功能要自己组装中小项目、学习型项目、快速迭代Django自带Admin后台、ORM功能强、组件齐全重量级明明只用十分之一的功能却要接受全套约束后台管理复杂、团队规范严格的场景Express异步性能好前后端都用JS生态虽大但质量参差不适合初学者直接上手团队本身就是JS技术栈最终选了Flask主要是看中它的可控性。像用户认证、数据库连接、文件上传这些功能Flask不会替你决定用什么方案你可以按自己的需求选。二手交易系统规模不大体量可能就是几千个用户用Flask配合MySQL完全够用开发效率不比Django慢而且代码写在明面上出问题好排查。另外还有一个现实因素Flask写的接口很好对接小程序的wx.request。小程序对后端没有特殊要求只要是HTTPS的HTTP接口就行Flask天然支持JSON返回结构简单小程序端解析也方便。1.3 功能模块先拆开再合并先把整个系统按用户视角拆成四个主模块用户模块微信登录、个人信息、我的发布、我的订单、评价记录。商品模块发布商品、商品详情、分类筛选、按距离排序、下架和编辑。交易模块会话沟通、创建订单、确认交易完成、评价。管理模块后台数据统计、商品审核有问题下架、用户封禁。这四个模块不是各自独立的它们通过用户ID和商品ID串联。比如用户A发布了商品商品表里记录user_id用户B对商品发起会话会话表里记录from_user和to_user交易完成后订单表里同时记录买家ID和卖家ID评价表再关联订单ID。数据模型的关联关系一开始就要理顺不然后面写接口时越写越乱。数据库我建议先设计成六张核心表users、products、orders、product_images、conversations、messages。不要把图片字段直接塞在商品表里因为一个商品有多张图单独建一张product_images表用product_id关联查询时一次性取出扩展性会好很多。2. Flask后端用户、商品、订单三条主线2.1 微信登录与JWT认证微信小程序的登录不靠手机号密码而是走wx.login获取临时code后端拿code去微信接口换openid和session_key。openid是用户在小程序生态内的唯一标识这个值必须存到数据库作为用户表的唯一索引。后端实现大致是这样的(/api/login, methods[POST]) def login(): code request.json.get(code) url https://api.weixin.qq.com/sns/jscode2session params { appid: app.config[WX_APP_ID], secret: app.config[WX_APP_SECRET], js_code: code, grant_type: authorization_code } resp requests.get(url, paramsparams).json() openid resp.get(openid) if not openid: return jsonify({code: 1, msg: 登录失败}) user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid, nickname微信用户, avatar) db.session.add(user) db.session.commit() token jwt.encode({uid: user.id, exp: int(time.time()) 604800}, app.config[SECRET_KEY], algorithmHS256) return jsonify({code: 0, data: {token: token, user_id: user.id}})这里有个经验点session_key是解密手机号和敏感数据用的一般场景不需要存到数据库除非你要做微信运动这类需要解密的功能。如果存了一定要妥善保密因为它是会话敏感信息。JWT的有效期我设成7天小程序端每次请求把token放在Header的Authorization里。token过期后前端统一拦截401跳转登录页重新调用wx.login整个过程对用户无感。2.2 商品发布、图片上传与状态管理商品发布是用户操作频率最高的功能也是最容易出问题的模块。这里踩过最大的坑是图片上传。小程序端通过wx.uploadFile把图片传给Flask后端接收后必须做两件事压缩和校验。压缩是因为现在手机随便拍张照片都好几MB不压缩的话用户传3张图服务器带宽和存储都撑不住。我的做法是在后端用Pillow把图片转成宽度不超过1280像素的JPEG质量设为80一张图基本能压到200KB以内。校验则是检查文件后缀名和大小防止有人传WebShell上来。商品状态必须用状态机管理不要只用一个简单的is_active布尔值。实际会出现“在售”、“已预订”、“已售出”、“已下架”四种状态为什么不是只有在售和已售因为买家可能会说“我先留着明天看货”这时候商品不能继续被别人下单但也不算卖出所以需要一个预订态。class Product(db.Model): id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) title db.Column(db.String(100)) description db.Column(db.Text) price db.Column(db.Float) category db.Column(db.String(50)) status db.Column(db.String(20), defaulton_sale) latitude db.Column(db.Float) longitude db.Column(db.Float) create_time db.Column(db.DateTime)状态更新建议只走后端封装的函数每次变更记录日志。否则多人操作时容易出现重复下单这种并发问题。2.3 同城商品的LBS排序与范围筛选这是整个后端最有技术含量的一部分。同城交易的核心就是“按距离找商品”实现方案不是用数据库的模糊搜索而是算出每个商品与用户之间的距离再排序。地球是个球面算距离要用球面距离公式常见的是Haversine公式。精度够用计算量也不大。from math import radians, sin, cos, asin, sqrt def haversine(lat1, lng1, lat2, lng2): R 6371 d_lat radians(lat2 - lat1) d_lng radians(lng2 - lng1) a sin(d_lat/2)**2 cos(radians(lat1)) * cos(radians(lat2)) * sin(d_lng/2)**2 return 2 * R * asin(sqrt(a))注意接口不能每次都对全表几万条商品算一遍距离那样CPU扛不住。我会先对经纬度做一步粗筛圈定一个经纬度范围例如在用户坐标的±0.3度范围内查找商品再用Haversine对这批数据精算排序。0.3度大约对应30多公里对同城交易足够了。直接写一条SQL把经纬度范围作为WHERE条件配合MySQL的复合索引查询速度非常快。2.4 交易流程与订单状态机二手交易流程跟电商订单不一样的地方在于先沟通、后下单。用户看到商品先点“我想要”进入聊天界面问几句确认没问题后才生成订单。甚至有些卖家要求当面交易平台直接生成订单只是作为信用凭证付款环节可以跳过。我的订单状态设计是pending待确认、confirmed已确认、completed已完成、cancelled已取消。为什么要有“待确认”因为买家可以发起订单但卖家未必同意卖要留一个双方确认缓冲期。对应的接口要设计成买家下单、卖家确认或拒绝、买家确认完成三组操作。确认完成后双方进入评价环节。评价表记录订单ID、评价人ID、被评价人ID、评分和内容。评分会汇总到用户表的一个字段作为信用展示。这里建议关注一个细节评价一旦提交不提供修改接口。买家和卖家都只有一次评价机会多几个来回很容易被人刷信用。3. 小程序端从登录到列表加载的实战细节3.1 请求封装的正确姿势小程序端没有axios所有网络请求都走wx.request。如果每个页面直接写wx.requesttoken注入、错误提示、状态码处理这些逻辑会重复百遍所以必须封装一个统一的请求函数。const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method, data, header: { Authorization: Bearer wx.getStorageSync(token) }, success: (res) { if (res.statusCode 200) { resolve(res.data) } else if (res.statusCode 401) { handleLogin() } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res) } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }我见过很多初学者在每个页面里复制粘贴wx.request后来项目越改越乱。统一封装的好处不只是代码量少更关键的是集中处理登录态过期和错误提醒后端返回什么格式、前端该怎么兜底都在一个地方管。这算是做小程序必改的第一步。3.2 首页列表与“加载更多”分页实现二手商品列表是典型的分页场景需要同时处理上拉加载更多和下拉刷新。用微信小程序原生的onReachBottom和pageScrollTo就能实现不需要引入第三方库。这里的核心是页码管理很多人的翻车点就在这里。正确的做法是维护两个变量page当前页码和hasMore是否还有更多。data: { list: [], page: 1, hasMore: true, loading: false }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return this.setData({ loading: true }) this.loadProducts() }, async loadProducts() { const res await request(/api/products?page${this.data.page}limit10) const newList this.data.page 1 ? res.data.list : this.data.list.concat(res.data.list) this.setData({ list: newList, page: this.data.page 1, hasMore: res.data.list.length 0, loading: false }) }注意一个细节每次下拉刷新要把page重置为1同时清空list否则数据会叠加翻倍。还有setData的list如果是长列表建议用setData更新整个list而不是用this.data.list.push再赋值前者会触发整个列表重新渲染后者可能出现UI不同步的问题。3.3 地图选点、定位与授权处理同城交易要填位置小程序端不能只让用户选城市必须支持地图选点。wx.chooseLocation可以打开地图选择具体地址返回经纬度。注意装base库时要提前声明权限用途。发布商品页正常流程是选好位置后回填经纬度和文字地址提交时跟随表单传到后端。这里有两个容易踩的坑第一用户没有授权定位权限时wx.chooseLocation可能直接报错。要在页面onLoad里先调一次wx.getSetting判断是否已经拒绝过授权。如果用户明确拒绝过不能一次次弹授权框而是提供一个“去设置”的按钮跳转打开小程序的设置页。第二经纬度精度问题。wx.chooseLocation返回的经纬度是火星坐标如果你的后端地图用的是Google坐标系会有一点偏移。国内项目建议统一用腾讯或高德的坐标系后端不要做坐标系转换保持一致性就好。3.4 页面动态标题和分类筛选小程序的默认标题是每个页面在json里写死的navigationBarTitleText。我做了两个动态标题的场景进入商品详情页时标题设置为“商品详情”进入用户主页时改成用户昵称加“的主页”这样页面更有辨识度。wx.setNavigationBarTitle({ title: userInfo.nickname 的主页 })分类筛选我用的是横向滚动菜单点了就切换列表。实现上就是给products接口加一个category参数后端查询加上条件。还有一个隐藏小细节切换分类时要重置page和hasMore清空列表跟下拉刷新逻辑保持一致否则用户从“手机”切到“家电”列表还会残留上一类别的商品。4. 从本地跑通到线上部署4.1 本地开发环境的搭建与真机联调本地开发第一步是创建虚拟环境不要让项目依赖污染全局Python环境这是Python项目最基本的纪律。python3 -m venv venv source venv/bin/activate pip install flask flask-sqlalchemy flask-jwt-extended pymysql pillow requests数据库我建议本地先用SQLite方便快速调试部署到服务器时再切换MySQL。Flask的配置里写两个数据库URL靠环境变量切换。这样做的好处是本地和线上逻辑完全一致不会出现“本地没问题线上连不上数据库”这种诡异情况。真机调试时注意一个问题手机跟电脑要在同一个局域网内电脑防火墙要放行Flask默认的5000端口。Flask默认只监听127.0.0.1这时候要用host0.0.0.0启动不然手机根本访问不到。提示小程序开发工具里要勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”否则本地请求直接报错。但上线前必须取消这个勾选配好合法域名。4.2 服务器部署方案Nginx Gunicorn本地Flask自带的开发服务器不能用于生产环境。部署方案我用了最成熟的一套Nginx做反向代理Gunicorn做WSGI服务Systemd管理进程。为什么用Gunicorn因为Flask自带的Werkzeug在并发情况下扛不住而且不支持进程管理。Gunicorn可以配置多worker进程对于小项目2到4个worker就足够了。gunicorn -w 4 -b 127.0.0.1:8000 app:appNginx配置里最主要的是把443端口转发到8000端口同时配置SSL证书。小程序要求所有请求必须HTTPS所以这类项目必须在服务器上申请SSL证书现在用免费的Lets Encrypt或者云厂商的免费证书就行。部署完之后建议做一个最基本的自检浏览器访问https://你的域名/api/health能返回200然后用开发者工具把域名填到request合法域名里再试。域名不要带路径系统会自动把你填的域名作为前缀注意要和你代码里的baseUrl完全一致。4.3 小程序体验版、提审与上线小程序开发完先上传到微信平台生成体验版用体验版二维码发给朋友收集试用反馈。这一步很重要因为体验版不经过审核可以直接通过适合做内部测试。体量小的话让十个人试用一周收集来的bug往往比自己写一天测试用例更有效。提审时最担心的是被拒。二手交易类小程序只要做好两点基本都能过一是不能涉及虚拟支付商品不能是话费、会员卡这类虚拟物品二是商品分类不能涉黄赌毒和违禁品而且要有“举报”入口。这些在提审前就要加好审核老师看项目结构和页面文案很仔细千万别等到被拒才补。上线之后别忘了配置微信小程序的订阅消息。下单、交易完成这些关键节点给用户发一条模板通知能有效提升成交率和回访率。订阅消息是一次性订阅需要用户主动点击授权要设计成下单或确认交易的按钮上顺带触发不要单独做弹窗打扰用户。5. 常见问题与排查实录5.1 列表加载更多时数据重复或错乱这个问题常见于下拉刷新和上拉加载共用一个pagination逻辑的情况。场景是这样的用户拉到第3页然后下拉刷新你没把page重置成1结果第3页的数据前面又拼上了第1页的数据列表瞬间出现几十条重复。排查思路很简单打印每次请求的page参数。看请求时page是不是预期的值。真正的问题是并发下拉刷新和上拉加载如果同时触发page会跳两个数所以刷新时一定要加互斥锁用loading标志位防止并发请求。5.2 iOS上的时间解析崩溃后端返回的时间字段是字符串格式是2024-12-01 10:30:00。小程序端为了显示“今天”、“昨天”做了new Date(time)解析结果在iOS上直接NaN时间全部显示Invalid Date。原因是iOS的WebView不认识“-”分隔符只认“/”。修复方案是在前端做一步兼容time.replace(/-/g, /)或者更稳妥的办法是后端直接返回时间戳前端new Date(parseInt(timestamp))处理这样完全避开格式兼容问题。做小程序日期处理时不要想当然认为跟Web浏览器一样JS引擎版本差异真的会咬人。5.3 真机请求失败模拟器却正常开发工具模拟器里一切正常一上真机接口全挂大概率是两类问题。第一没有关闭“校验合法域名”微信的合法域名校验只针对真机模拟器里不校验也能通第二HTTPS证书有问题比如证书过期、不包含完整证书链、用了自签名证书。排查时先用手机浏览器直接访问接口地址能正常打开就说明网络没问题问题出在小程序配置。另外记得开发版和体验版可以使用不校验合法域名但正式版不行正式版一旦发布就锁定域名了。5.4 位置筛选不准显示浙江的商品跑到了广东这个坑我印象特别深。后端筛选用的是经纬度计算前端发布商品时存的是定位坐标本来是没问题的。结果测试的时候发现有些商品的距离计算结果完全对不上。查了后发现是发布时用户移动太快定位返回了一个精度极低的初始坐标比如城市级别的0.05度精度而不是街区级别的0.001度精度。所以采集经纬度时一定要检查返回的accuracy或precision字段。wx.getLocation返回的accuracy代表精确度如果值太大比如超过等于1000米直接提示用户“正在获取精确位置请稍后再试”不要让低精度坐标落到数据库里。6. 一点实操经验收尾最后分享一个当时上线后优化体验的小技巧。距离排序这个功能原始版本的SQL是先按距离近的排但商品太少时用户会发现“附近没什么可看”。后来我在排序策略里加了一个混合因子距离权重占70%发布时间权重占30%。这样既能让用户优先看到近处的商品又不会因为几公里内没货而显得内容空洞。这个调整是数据驱动做的看后台点击量统计发现加入发布时间因子后列表页到详情页的跳出率下降了大概百分之二十。另外再提一句关于代码维护的体会这个项目做完后我最大的感受是“前后端联调比写代码更花时间”。尤其是字段名不统一的问题后端叫productId小程序端叫id导致接口联调来回改了三轮。后来所有接口的返回字段都先列一个JSON结构给大家对一遍再动手这种前置对齐看起来多花十分钟实际上省了一天调试时间。如果你也在做一个类似的项目建议先把MVP做出来不要一开始就追求功能大而全。一个能登录、能发商品、能按距离刷列表、能下单的闭环比十个华丽但用不上的模块有价值得多。等核心闭环稳定了再慢慢加聊天提醒、信用体系、后台管理这些锦上添花的功能什么时候都不晚。
返回列表