作为一个前后端都摸过不少坑的开发者,去年年初接到一个挺有意思的需求:做一个基于微信小程序的汉服租赁平台。当时团队里前端资源紧张,又要兼顾后续可能上 App 的计划,我在技术选型上纠结了好一阵,最后定了Python + uniapp + 微信小程序这套组合。项目上线大半年,经历过压测、经历过支付回调半夜抽风、也经历过用户反馈"日期选不了"的尴尬问题,目前整套系统跑得还算稳。这篇文章我把整个项目的设计思路、核心代码、以及那些文档里不会写的坑全部捋一遍,给正在做租赁类小程序或者想入坑 uniapp 的同学一个真实参考。
这个项目最终交付的是一个完整可运营的汉服租赁小程序,覆盖用户端(浏览、下单、支付、押金、归还)和管理端(商品管理、订单管理、库存管理)。整个选题的价值在于:租赁业务的复杂程度比普通电商高不少,涉及库存时间轴校验、押金流转、订单状态机,能把这些捋清楚,后面做任何租借类项目都会顺很多。
1. 项目整体设计与技术选型思路
1.1 为什么咬定微信小程序不放手
汉服租赁这个场景,用户画像非常清晰:18到30岁、喜欢国风、参加活动前临时租衣服、决策周期短。这类用户几乎不可能专门去下载一个 App,但微信里随手搜一下小程序、或者朋友转发一个链接,转化链路就短得多。小程序"即用即走"的特性,和租赁这种低频但刚需的业务非常匹配。
另外,微信生态自带的登录能力(wx.login拿 code 换 openid)帮我们省掉了手机号注册这一大套流程。用户进来第一秒就能看到商品,不需要先填一堆资料,这对转化率的提升是实打实的。后面如果想要推送"归还提醒""活动上新",小程序订阅消息也能顶上去,比短信便宜且触达率高得多。
1.2 uniapp 是性价比最高的跨端方案
选 uniapp 的原因很简单:我只需要写一套 Vue 语法的前端代码,就可以同时编译输出微信小程序、H5、以及 iOS/Android App。虽然项目标题是"微信小程序",但保不齐哪天要套壳上架安卓应用市场,uniapp 一套代码全搞定,不用重写。
实际开发中 uniapp 的坑也确实多,比如自定义导航栏在不同端的表现不一致、uni.request的返回值包裹层级不同、某些组件在 H5 端和小程序端行为有差异。但这些属于"熟悉之后能规避"的问题,比起用原生微信小程序写一套再拿原生安卓写一套,成本省了至少一半。如果你们团队本来就是 Vue 技术栈,uniapp 的上手成本几乎为零。
1.3 Python 后端:开发效率优先
后端选 Python 而不是 Java 或 Go,核心原因是团队当时没人专门写 Java,而 Python 我们都能上手,且这套业务本身复杂度并没有高到必须上 Java 体系。我用了 Django + Django REST Framework(DRF),开发速度极快。Django 自带 Admin 后台,给运营人员做商品管理和订单管理相当省事,不需要我从零写一个管理页面。
Python 后端的性能很多人担心,实际上对于这种体量的小程序,QPS 压力远远到不了瓶颈。我们用 MySQL 存业务数据、Redis 做缓存和 Token 管理,加上 Django 的 ORM 做查询优化,单机部署跑个几千用户毫无压力。后面如果真要做大,Django 也可以横向扩展,不至于被架构卡死。
1.4 整体系统架构一览
整个系统分三层:
- 前端展示层:uniapp 编译生成的微信小程序,负责页面渲染、用户交互、网络请求。
- 后端服务层:Django + DRF 提供 RESTful API,处理登录鉴权、商品管理、订单流转、支付回调、定时任务。
- 数据与中间件层:MySQL 存储业务数据,Redis 缓存会话与热点数据,云存储存放汉服图片。
前端通过wx.request/uni.request与后端 API 通信,所有接口统一返回{ code, message, data }格式,前端根据code做统一拦截。支付走微信支付 Native + JSAPI,后端负责生成支付参数,小程序端调uni.requestPayment拉起收银台。
这套分层逻辑是常规但又非常稳的,每一层职责清晰,出了问题能快速定位。
2. 核心业务逻辑与数据库设计
2.1 租赁业务闭环与订单状态机
普通电商订单状态无非"待付款 -> 待发货 -> 待收货 -> 已完成",但租赁多了一条归还链路。我把整套状态机设计成这样:
待支付 -> 已支付(待发货/待自取)-> 租赁中 -> 待归还 -> 已完成 -> 已逾期(超时未还)-> 扣款处理 -> 已取消(支付前取消 / 超时自动取消)前端页面里需要根据当前订单状态展示不同的操作按钮,比如"待归还"状态显示"申请续租"和"我要归还","已完成"状态显示"再次租赁"和"评价"。状态机是整个系统的骨架,宁可前期多画几张图,也不要写代码时边写边改。
2.2 关键数据库表设计
| 表名 | 核心字段 | 关键说明 |
|---|---|---|
users | openid, nickname, avatar, phone | 以 openid 为唯一标识,手机号可在下单时补充 |
costume | title, category, dynasty, size, rent_price, deposit, cover_image, description | 分类按形制(齐胸、圆领、道袍等)与朝代划分 |
costume_sku | costume_id, size, stock, status | SKU 维度管理库存,库存不放在 costume 表里 |
order | order_no, user_id, sku_id, start_date, end_date, rent_fee, deposit, status | 订单同时存租金和押金金额,方便对账 |
order_timeline | order_id, status, remark, create_time | 订单状态变更流水,方便排查纠纷 |
payment | order_no, transaction_id, total_fee, pay_status | 记录微信支付单,退款押金也要在支付表里留痕 |
一个容易被忽略的细节:押金不能挂在订单表里当普通字段用,一定要单独存并且和租金拆开。退押金的时候要走微信支付的退款接口,这是一笔独立资金流,对账和审计都依赖这笔记录。
2.3 档期冲突校验:租赁业务的核心难点
租赁和电商最大的不同在于商品同一时间段内不能被两个人同时租。用户下单时要选择租赁开始时间和结束时间(精确到天),后端必须校验该 SKU 在目标时间段内是否已有冲突订单。
我最初的设计是在订单表里加start_date和end_date,然后通过 SQL 查"是否有重叠订单"。重叠条件其实很经典:
# Django ORM 判断时间段重叠 conflict_exists = Order.objects.filter( sku_id=sku_id, status__in=["paid", "renting", "pending_return"], ).filter( start_date__lt=end_date, end_date__gt=start_date, ).exists()这个判断逻辑要仔细理解一下:新订单的开始时间要早于已有订单的结束时间,新订单的结束时间要晚于已有订单的开始时间,两者同时满足说明存在交集。
后来订单量上来了,我加了一个stock_lock表,专门在用户提交订单时先锁定库存时间段,支付超时自动释放。这个锁表的作用类似并发场景下的"占位",避免两个用户同时提交同一件汉服的同一时段,结果都校验通过但也都不支付的情况。
2.4 押金流转方案
押金流程是整个项目里退款纠纷最多的一环,我采用"同时收取、分开退还"的策略:
- 下单时,用户一次性支付"租金 + 押金",调用微信支付一笔还是两笔?这里我选了一笔支付,原因很简单:微信支付笔数多了手续费和管理成本都高,而且用户看到一个总金额也比看到两笔扣款更舒服。
- 后台支付回调里,将
total_fee按照订单里租金和押金各占多少拆账记录。 - 用户归还汉服、商家验收无损后,后台触发押金退款。退款走微信支付
refund接口,原路退回。
![押金流转时序,没有图表,文字说明就够清楚了]其实时序大体是:下单支付 -> 平台同时收到两笔钱 -> 归还验收 -> 押金原路退回 -> 租金归商家。
3. 前端小程序端核心实现细节
3.1 微信登录与静默授权
微信小程序登录的完整链路比我最初预想的要绕一点。小程序端uni.login拿到的是临时code,这个code要到后端换openid,然后再换我们自己的登录态 Token。
// uniapp 端登录逻辑 uni.login({ provider: 'weixin', success: async (loginRes) => { const res = await request({ url: '/api/auth/login', method: 'POST', data: { code: loginRes.code } }); // 保存自己的 token uni.setStorageSync('token', res.data.token); uni.setStorageSync('userInfo', res.data.userInfo); } });后端拿着 code 去https://api.weixin.qq.com/sns/jscode2session换取openid和session_key,然后我用openid查库里有没有这个用户,没有就创建一条新用户记录,最后签发 Token。Token 有效期我设成了 7 天,小程序端请求拦截器里发现 401 就重新调uni.login刷新 Token。整套流程走下来,用户层面是"无感登录"的,体验很好。
3.2 请求封装:统一处理登录态和错误码
uniapp 的uni.request功能够用但比较原始,我在项目里封装了一个request.js,统一处理基础 URL拼接、Token 注入、401 拦截、错误弹窗。这层封装在整个开发周期里省了大量重复工作。
// 封装后的 request 方法(简化版) const request = (options) => { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 401) { // token 过期,重新登录 handleLoginExpired(); reject(res.data); } else if (res.data.code === 200) { resolve(res.data.data); } else { uni.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: (err) => { uni.showToast({ title: '网络异常,请重试', icon: 'none' }); reject(err); } }); }); };注意uni.request的返回结构:开发时如果你在微信开发者工具里看到data里多包了一层,别慌,那是没做解构。建议所有后端接口统一返回{ code, message, data },前端拦截器里直接解构,后面 Debug 会舒服很多。
3.3 顶部导航栏高度与安全区适配
项目里商品详情页和订单结算页我用了自定义导航栏,因为默认导航栏样式太受限,不好放自定义按钮。但自定义导航栏意味着必须自己计算状态栏高度和胶囊按钮位置,这也是微信小程序开发中最经典的适配问题。
// 适配方案:获取胶囊按钮位置 + 状态栏高度 const getNavBarInfo = () => { const systemInfo = uni.getSystemInfoSync(); const capsuleInfo = uni.getMenuButtonBoundingClientRect(); // 导航栏高度 = 胶囊按钮高度 + 上下留白 const navBarHeight = (capsuleInfo.top - systemInfo.statusBarHeight) * 2 + capsuleInfo.height; return { statusBarHeight: systemInfo.statusBarHeight, navBarHeight: navBarHeight, // 右侧占位宽度 capsuleWidth: capsuleInfo.width, capsuleRight: capsuleInfo.width + (systemInfo.windowWidth - capsuleInfo.left) }; };这套方案实测在 iPhone 和安卓机上表现基本一致,主要是把胶囊按钮的top和状态栏statusBarHeight之间的间距计算成导航栏的上下 padding,这样自定义标题文字可以完美垂直居中。
注意:
uni.getMenuButtonBoundingClientRect()只在微信小程序端存在,如果你后面要编译到 H5 或 App,需要做条件编译处理,否则会报错。
3.4 uview-plus 组件库的引入与配置
项目里我引入了uview-plus(uview 的 uni-app 升级版)来快速搭建界面,表单、弹窗、日历选择这些组件都能直接复用。通过 HBuilderX 插件市场导入非常方便,但要注意配置:
- 在
main.js里import uViewPlus from 'uview-plus',然后app.use(uViewPlus)。 - 在
uni.scss里引入主题变量。 pages.json里配置easycom规则,这样页面里可以直接用up-button这类组件标签,不需要手动 import。
"easycom": { "autoscan": true, "custom": { "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue" } }这块容易踩的坑是 easycom 规则写错导致组件不渲染。我当时折腾了大半天,最后发现是正则写错了,u-$1和up-$1对不上,页面里标签和组件文件根本没匹配上。
日历组件在租赁场景是刚需。uview-plus 的日历组件支持mode="range"范围选择和禁用日期,我把它封装成商品详情页的房态选择器,用户选起止日期后,前端直接把选中范围回显到页面上,体验比纯手填日期好太多。
3.5 订单支付的完整串通
支付这条链路前后端配合的点比较多。我把流程理成下面这样:
- 用户提交订单,后端创建订单记录,状态为"待支付"。
- 后端调用微信支付统一下单接口,传
openid、订单号、金额,拿到prepay_id。 - 后端用
prepay_id发起二次签名,生成timeStamp、nonceStr、package、signType、paySign参数返回给前端。 - 前端调用
uni.requestPayment拉起支付收银台。
// uniapp 端调起支付 const payOrder = (orderNo) => { const payParams = await request({ url: '/api/pay/wxpay', method: 'POST', data: { order_no: orderNo } }); uni.requestPayment({ provider: 'wxpay', timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign: payParams.paySign, success: () => { uni.showToast({ title: '支付成功', icon: 'success' }); }, fail: (err) => { // 支付失败或取消,不要直接提示失败 // 需要查后台订单状态确认 } }); };支付回调的后端部分我在第四章详说,这里只强调一点:前端支付回调的success并不绝对可靠。用户可能支付成功但前端回调失败(比如支付过程中小程序被杀掉),所以前端弹"支付成功"后,必须调后端确认订单状态的接口刷新页面数据。我见过太多同学只在success里写业务逻辑,结果出现"钱扣了但界面还停在待支付"的尴尬情况。
4. Python 后端核心接口实现与实操
4.1 Django 项目结构与配置
后端项目用 Django 4.x + DRF 搭建,目录结构大致如下:
backend/ ├── manage.py ├── config/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ └── ... ├── apps/ │ ├── users/ # 用户模块 │ ├── costume/ # 汉服商品模块 │ ├── order/ # 订单模块 │ └── payment/ # 支付模块 └── utils/ # 公共工具函数我把各业务按模块拆分到独立 app 里,模块间通过函数调用和模型关联交互,避免各写各的导致代码面条化。配置方面关键的几点:
MySQL连接配置好字符集为utf8mb4,不然 emoji 存不进去,用户在昵称里放个表情就报错。- Redis 用
django-redis做缓存和 Token 存储。 DRF的认证类设置为TokenAuthentication,全局鉴权配合自定义IsAuthenticated权限。
4.2 登录接口:代码换 Token
登录接口是客户端第一步必调的接口。核心逻辑就是拿前端传来的code去微信接口换openid,然后签发自己的 Token。这个接口写好了,用户体系就通了。
# apps/users/views.py import requests from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.authtoken.models import Token from .models import User class WxLoginView(APIView): authentication_classes = [] # 登录接口不需要鉴权 permission_classes = [] def post(self, request): code = request.data.get('code') appid = settings.WX_APPID secret = settings.WX_SECRET url = 'https://api.weixin.qq.com/sns/jscode2session' params = { 'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code' } resp = requests.get(url, params=params).json() if 'openid' not in resp: return Response({'code': 400, 'message': '登录失败'}) openid = resp['openid'] user, _ = User.objects.get_or_create(openid=openid) token, _ = Token.objects.get_or_create(user=user) return Response({ 'code': 200, 'message': 'ok', 'data': { 'token': token.key, 'user_info': { 'nickname': user.nickname, 'avatar': user.avatar, } } })微信的jscode2session接口要注意异常处理。微信那边偶尔会返回errcode,比如40029(code 无效)、45011(频率限制),这些错误信息不能直接抛给用户,要分门别类写到日志里去排查。我在线上观察到45011频发的时候基本是有人拿校验工具在刷接口,顺藤摸瓜能抓到恶意请求。
4.3 商品列表与详情接口:库存实时性如何保证
商品列表接口相对简单,但有几个细节值得优化。列表页我用了 DRF 的ListAPIView配合分页,每页返回 10 条。响应中不仅要有汉服的基本信息,还需要把首图压缩图地址单独返回,这样前端能直接渲染,不用在小程序端做图片二次处理。
详情页接口除了返回汉服的详细图文、尺码表之外,最重要的是返回"当前可租状态"和"已占用档期列表"。这里我用了 Redis 做缓存:
- 商品信息本身变动少,缓存 30 分钟。
- 已占用档期必须实时查库,不能缓存太久,否则用户看到"可选"的日期实际下单时被拒。
# apps/costume/views.py class CostumeDetailView(APIView): def get(self, request, costume_id): # 先查缓存 cache_key = f'costume_detail_{costume_id}' cached = cache.get(cache_key) if cached: return Response({'code': 200, 'data': cached}) costume = Costume.objects.prefetch_related('skus').get(id=costume_id) # 被占用档期:查询该商品所有未完成订单的时间段 occupied = list(Order.objects.filter( sku__costume_id=costume_id, status__in=['paid', 'renting'], ).values_list('start_date', 'end_date')) data = CostumeSerializer(costume).data data['occupied_dates'] = occupied cache.set(cache_key, data, timeout=1800) return Response({'code': 200, 'data': data})这个接口里occupied_dates对前端非常关键,它决定了日历组件要禁用哪些日期。如果这个数据不准,用户端的日历就会显示错误,误下单后要退货,体验崩盘。所以在缓存选择上,商品信息可以缓存久一点,但占用档期必须每次实时拉取。
4.4 下单与支付回调接口:处理幂等与并发
下单接口要干的事情很多:校验 SKU 是否存在、校验时间段是否冲突、计算订单金额(租金 = 单价 × 天数)、创建订单记录。核心的并发防御贯穿"生成订单号 -> 锁库存 -> 返回支付参数"整个链路。
为了防止重复下单和并发冲突,我在数据库层面加了约束,同时业务层做select_for_update锁行:
# 下单接口的逻辑片段 with transaction.atomic(): sku = CostumeSku.objects.select_for_update().get(id=sku_id) # 再次校验档期冲突 conflict_exists = Order.objects.filter( sku_id=sku.id, status__in=['paid', 'renting', 'pending_return'], start_date__lt=end_date, end_date__gt=start_date, ).exists() if conflict_exists: raise ValidationError('该时间段已被占用') # 创建订单 order = Order.objects.create( order_no=generate_order_no(), user=request.user, sku=sku, start_date=start_date, end_date=end_date, rent_fee=sku.rent_price * days, deposit=sku.deposit, status='pending_payment' )select_for_update在 MySQLInnoDB引擎下会对命中行加排他锁,这样即便两个请求同时进来,第二个请求也要等第一个释放行锁后才执行查询,时间段校验就不会出现"同时通过"的情况。不过这个锁只针对更新逻辑,如果订单量极大(一天几千单),可能需要考虑把库存时间片预先切好。对租赁这种体量,目前的方案已经稳稳够用。
支付回调接口是整套流程中最容易埋雷的地方。
- 微信回调可能因为网络问题多次请求,接口必须做幂等处理:同一笔订单重复回调只处理一次。
- 回调必须验签,微信签名算法是 SHA256 的 HMAC,密钥是自己的支付 API v3 Key,验签不过的直接返回"失败",绝不要做任何业务操作。
- 回调处理完要返回 XML 格式的
<return_code>SUCCESS</return_code>,如果返回其他内容微信会认为回调失败,不断重试,造成重复通知。
# apps/payment/views.py def wx_pay_callback(request): # 1. 验签(这里简写,实际用微信官方 sdk 的 callback 解析) result = wxpay.callback(request.body) if not result.verify(): return HttpResponse('签名验证失败', status=400) order_no = result.get('out_trade_no') # 2. 幂等处理:订单状态已经是"已支付"就跳过 order = Order.objects.filter(order_no=order_no).first() if order.status == 'paid': return HttpResponse('<xml><return_code>SUCCESS</return_code></xml>') # 3. 更新订单状态 order.status = 'paid' order.paid_time = timezone.now() order.save() return HttpResponse('<xml><return_code>SUCCESS</return_code></xml>')回调里永远先查一次订单当前状态,如果已经处理过就直接返回成功,这是幂等最基础也最有效的实现。
4.5 定时任务:自动取消与押金原路退回
订单超时未支付自动取消、租赁到期自动提醒、押金自动退回,这些操作不能靠用户手动触发,必须走定时任务。我用 Django 的django-crontab或 Celery 的 beat 定时调度,每天凌晨跑一次任务。
押金退回的定时任务分两类:其一,用户归还验收通过后,后台审核通过那一刻即刻触发退款;其二,用户到了归还日后 3 天仍未归还,自动推送逾期提醒和扣款说明,并在订单状态上标记"已逾期"。押金退款走微信支付的refund接口,退款金额不能超过原支付金额,这两点都要在代码里做校验。
5. 微信小程序端与 uniapp 高频问题排查实录
5.1 日志不输出:排查思路比结果更重要
有段时间小程序端console.log完全打不出来,控制台一片空白,排查了半天才发现问题不是出在代码里,而是出现在线上环境被运营开了"调试模式"关闭上。微信开发者工具的"真机调试"和"预览"模式日志显示机制不一样,如果你在预览模式下打开小程序,日志默认不输出到 IDE 控制台,需要打开 vConsole 看。
总结一下常见原因:
- 发布体验版/正式版时:代码里
console.log被压缩工具去掉了,或者在生产环境被手动屏蔽。 - 开发工具里选错环境:当前选的是"发布模式"而非"调试模式"。
- 真机调试未打开 vConsole:点开右上角胶囊按钮,找到 vConsole 开启。
另外,uniapp 项目在 HBuilderX 里还有个坑:uni.showToast和console.log在 H5 端正常,但小程序端如果manifest.json里配置了"minimize"选项(代码压缩混淆),日志和部分调试代码会被剔除。排查这个问题时先把这个配置关掉,确认代码没问题再重新开启压缩,能节省大量时间。
5.2 up加载组件不渲染:easycom 规则十有八九写错
uview-plus 这类组件库在小程序端不渲染,十有八九是pages.json里的easycom规则配错了。Easycom 是 uni-app 的自动按需引入方案,它要求自定义规则里的$1和组件目录结构严格对应。
"easycom": { "autoscan": true, "custom": { "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue" } }这里如果你用的是up-button标签,正则规则要把up-转换成u-开头的组件文件。所以^up-(.*)对应components/u-$1/u-$1.vue是把up-button映射到components/u-button/u-button.vue,而 uview-plus 的组件文件名刚好就是u-button.vue。
还有一个容易忽略的点:easycom配置里的autoscan如果没开,即使你把组件文件装好了也没法自动扫描到。在 HBuilderX 插件市场导入 uview-plus 后,记得确认autoscan是true,否则所有 up- 组件全部无效。
5.3 H5 唤起小程序链接无法访问
项目里有一个场景:公众号文章跳转到小程序。结果测试反馈点击链接打不开,报"无法访问"。排查发现问题不在地图或链接本身,而是微信要求 H5 跳小程序必须有有效的 AppID、页面路径、并且用户值必须是"线上版本"。
如果你的wx-open-launch-weapp标签配置正确但依然无法打开,大概率是这几个原因:
- 小程序未发布,体验版无法通过 H5 跳转访问。
- H5 页面域名没在小程序后台配置为
业务域名(不是 request 域名,是不同的配置项)。 - 微信开放平台未绑定小程序 AppID。
path参数写错,页面地址必须是pages/index/index这种格式,且必须带.html后缀(特定场景)。
这个功能比较绕,我建议如果非必要就暂时不要做 H5 跳小程序,直接引导用户通过搜索小程序进入,省掉这一堆配置和兼容性问题。
5.4 小程序如何发给别人试用与体验版管理
开发过程中需要把小程序发给用户体验,但微信对小程序包大小和发布有严格限制。我测试下来最常用的做法:
微信开发者工具右上角点"预览",生成一个预览二维码,用户扫码即可体验(仅限开发者本人微信和已绑定成员,普通用户不行)。- 把代码上传到微信后台,在"版本管理"中"开发版本"里设为"体验版",生成体验版二维码,任意用户扫码都可以体验。这是最接近线上环境的测试方式。
关键提醒:体验版默认只能由"体验成员"打开,需要在"成员管理"里添加体验成员微信号。另外体验版如果超过 7 天未更新会被微信自动清空版本列表,所以需要定期重新上传代码。
5.5 微信小程序内置导航栏高度与自定义导航栏的切换
不少组件库的navbar高度默认写的是44px,iPhone X 系列真实导航区高度是44px+ 状态栏高度。我在实际开发中发现,即使在iPhone 6/7/8那种状态栏较矮的设备上,44px 也偏高,底部按钮会顶到安全区。
我做自定义导航栏时统一用statusBarHeight + navbarHeight计算页面占位高度,并预留底部安全区(env(safe-area-inset-bottom))。这样在 iPhone 和安卓全面屏手机上都能正确展示,不会出现按钮被系统手势条遮挡的情况。
5.6 打包上线的关键准备
uniapp 项目开发完要上线微信小程序,有几个坑我提前踩过:
manifest.json里的mp-weixin配置必须填写正确的appid,否则开发者工具直接报错,编译出来的包无法预览。微信小程序要求所有接口请求域名必须是 HTTPS 且已备案,且要在小程序后台配置 request 合法域名。开发阶段可以勾选"不校验域名",上线前一定要关掉。
分包机制:如果代码包超过 2MB,需要把
pages按功能拆成subPackages,微信小程序的tabBar页面必须在主包,商品详情、订单详情这类非核心页面可以放进分包。我项目里把支付结果页、订单结算页、个人中心相关页面都做了分包,主包压缩后稳定在 1MB 左右。uni.request请求在小程序端如果header里设置Content-Type为application/json,部分后端框架接收 POST 数据时要额外处理,建议统一用application/json并在 Django 侧配置好JSONParser。
6. 常见问题速查表与避坑指南
6.1 高频问题汇总
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 自定义导航栏错位 | 小程序真机上标题不居中、偏上/偏下 | 用getMenuButtonBoundingClientRect精确计算胶囊高度,动态计算导航栏高度 |
| 用户点支付后订单状态没变 | 支付成功但界面停在"待支付" | 前端success回调里必须请求后端接口确认订单状态;后端做好回调幂等 |
| 档期冲突校验失效 | 同一件汉服同一天被两个用户下单成功 | 用select_for_update锁行 + 数据库唯一约束双重保障 |
| 商品图片加载不出来 | 详情页图片空白或裂开 | 检查图片域名是否在小程序后台 downloadFile 合法域名列表里;图片地址必须 HTTPS |
| 体验版二维码过期 | 用户扫码说"版本不存在" | 体验版 7 天未更新会失效,重新上传代码并设置体验版 |
| 支付回调不触发 | 用户支付成功但后端没收到回调,订单一直待支付 | 检查回调地址必须公网可访问、且为 HTTPS;微信会重试 3 次,超过后可在后端配置定时主动查单 |
| Token 突然失效 | 用户用着用着被踢下线 | 检查 Token 有效期设置;小程序端 401 拦截后自动重新登录 |
uni.requestPayment报错 | 调不起收银台、报invalid sign | 后端生成的支付参数必须用paySign,且package参数是prepay_id=xxx格式,前端不要改任何参数原样传入 |
6.2 抓包调试建议
小程序端接口出问题时,我最常用的调试手段是Charles抓包(Windows/macOS 都有对应版本),把手机代理指向电脑,手机上安装 Charles 证书后就能看到小程序的 HTTPS 请求内容。微信小程序的证书校验比较严格,但调试模式下可以通过开发者工具打开"不校验合法域名",Charles 只用来查看请求参数和返回内容,基本够用。
我实际调试中最常抓的就是支付回调的参数格式是否正确。微信支付回调返回的是 XML,很多后端同学用 JSON 解析直接报错,抓包就能一眼看出问题:微信回调体是 XML,必须用 XML 解析器处理。
6.3 运营层面的常见坑
这个项目上线后运营那边也反馈了几个问题,虽说不是技术问题,但值得开发者提前了解:
- 押金退款延迟:微信支付的退款不是实时的,通常在 1-5 个工作日内到账。用户如果急着要押金,容易产生客诉。我在小程序端做了"退款进度说明"页面,明确告知到账时间。
- 归还后的质检流程:如果商家人工质检有纠纷(比如衣服破损),必须有人工介入的仲裁流程。我在后台给管理员增加了"争议订单"标记,配合订单状态流水,方便双方核对。
7. 后续扩展建议与个人心得体会
这个项目做下来最大的感受是:租赁业务的技术难点不在功能多复杂,而在于状态管理和时间维度上的并发控制。如果你只做普通商城,下单支付发货完事,库存扣减是静态的。但租赁生意,同一件衣服在时间轴上滚动,每个 SKU 在每个时间段都是一个"虚拟库存",如何高效且正确地管理这种虚拟库存,决定了整个平台能不能跑得起来。
我后续如果继续迭代,会优先做两件事:
- 引入消息队列(RabbitMQ / 或直接用 Redis Stream),处理支付回调后的异步通知和订单状态变更。现在的同步处理逻辑在回调高峰期会有轻微延迟,切到 MQ 后能更平滑。
- 接入小程序的订阅消息,在用户租赁到期前一天发送"归还提醒",在商家验收通过后发送"押金已退回"。这比短信便宜且转化率高很多。
另外,我也在抽空研究uniapp的uni-app x方向(后面如果做原生 App 可以顺路迁移),这块技术迭代挺快,但建议先别急着追新,把当前这套微信小程序稳定跑起来才是硬道理。
最后分享一个小技巧:在我这个项目里,所有订单金额(租金、押金、退款)我都统一用整数分存储,不直接用浮点数。原因很简单,微信支付金额单位就是分,如果后端按元计算会出现0.1 + 0.2这类浮点精度问题,对账的时候差一毛钱都浑身难受。前端展示的时候再除以 100 转成元,这样从入库到出账全程没有浮点计算,省掉了大量对账烦恼。这个习惯,做任何涉及支付的系统都强烈建议保留。