☰
基于Python+uniapp的微信小程序汉服租赁平台开发实践
2026/10/6 9:43:16 网站建设 项目流程

这两年穿汉服出门的人肉眼可见地多了,节假日公园里全是披着斗篷、提着裙摆的姑娘小伙。但租衣服这件事,线下汉服馆的体验依然停留在几年前:排期靠纸笔记,押金靠口头约定,档期撞车了只能来回打电话协调。我接这个“基于微信小程序的汉服服装租赁平台”项目时,第一反应就是——这玩意儿太适合小程序了。低频但有明确需求的使用场景,用户扫开就能下单,还完就离开,根本不需要为了租衣服专门装一个App。整个项目最终落地成三条线:后端用 Python 写接口,前端用 uniapp 写跨端页面,最终编译发布到微信小程序。这篇文章就把从技术选型、需求梳理、后端到前端、最后上线的完整过程讲一遍,适合正在做毕设的计算机专业学生、想给线下门店做线上化的店主,以及想用一套代码练全栈的开发者。

1. 为什么这个项目我坚持Python写后端、uniapp写前端

技术选型不是越高级越好,而是匹配项目体量。汉服租赁平台的核心是“管理服装、管理订单、处理押金”,本质是一套带状态流转的业务系统,没有高并发、没有海量数据,对性能的要求很低。这个前提直接决定了选型方向。

1.1 后端选Python:不是偷懒,是匹配业务迭代速度

后端框架我在 Flask 和 Django 之间犹豫过一阵,最终选了 Flask + SQLAlchemy + PyMySQL 这套组合。原因很简单:Flask 足够轻量,项目结构完全由自己掌控,路由、模型、服务层怎么组织一目了然。Django 自带 Admin 后台确实诱人,首次接触 ORM、迁移、中间件这些概念时容易绕进去,而且 Django 自带的东西很多我们用不到,反而是负担。

Python 版本用的 3.10,依赖隔离用 venv,requirements.txt 锁住 Django 爸爸的包就行。对比一下后端方案的取舍:

方案上手曲线后台管理异步支持对这个项目的适配度
Flask平缓需自己搭可加适合,代码透明
Django较陡自带Admin需改造也行,但偏重
FastAPI平缓需自己搭原生异步适合,生态较年轻

有人会问,为什么不用 Java 或者 Node?Java 在这个业务体量下,配置和部署的成本都偏重,Spring Boot 一套初始化项目就得加载一堆依赖。Node 写接口本身没问题,但团队里熟悉 Python 的成员更多,而且 Python 在微信支付、OSS 上传这些场景下都有非常成熟的 SDK,拿来就能用。技术选型是团队协作的产物,不是技术秀。

1.2 前端选uniapp:一份代码、五个端,真正省下的时间

前端用 uniapp 的理由只有一个核心:一份 Vue 语法的代码,能同时编译到微信小程序、H5、App。对租赁平台这种重业务、轻交互的项目,跨端能力直接省掉一半工作量。

很多人纠结要不要用微信原生小程序开发,我的看法是:原生小程序适合产品形态已经很稳定、团队有专职小程序开发的情况。但独立开发或者小组作战时,uniapp 的效率优势很明显。比如页面里的轮播图、宫格导航、时间选择器、表单校验,用 uview-plus 组件库直接拖进来就能用。uview-plus 在 HBuilderX 插件市场里一键导入,然后在 pages.json 里配置 easycom 规则,页面模板里就能直接写组件标签,不用手动 import。

uniapp 编译到微信小程序后,底层产物还是 WXML,微信原生 API 通过uni.xxx封装后照常调用,同时也支持条件编译处理各端差异。需要注意的一点:uniapp 并不能完全屏蔽端差异,导航栏高度、分享参数、支付回调这些地方仍然要做小程序端特判。但对租赁平台这种没有复杂原生功能的项目,适配成本很低。

1.3 微信小程序端起量快、转化顺,天然适合线下场景

为什么最终落地端落在微信小程序,而不是 H5 或者独立 App?因为汉服租赁是典型“低频刚需”业务,用户一年可能就租三五次,让他下载一个 App 几乎不可能。小程序扫开即用,还完就走,使用成本极低。

更关键的是微信生态给的完整闭环:微信登录直接拿到用户身份,微信支付完成租金和押金结算,订阅消息发送归还提醒。线下汉服馆的典型场景是,店门口立一个二维码牌子,用户扫一下就能查看店内可租的服装和档期,不需要关注公众号、不需要注册账号密码。这个转化路径比任何投放渠道都直接。

2. 汉服租赁业务的流程图背后:需求拆解比写代码更重要

很多第一次做项目的同学上来就建表、写接口,做到一半发现订单状态对不上、押金退不了、库存超卖了,回头来改数据结构,整个推倒重来。我的经验是:业务需求拆解先于代码,尤其是订单状态机,必须在一开始就定义清楚。

2.1 核心是一个订单状态机,不是一堆页面

汉服租赁平台有两个角色:用户和管理员(门店)。页面看起来不少,但所有逻辑都围绕订单状态机展开。我最终定义的订单状态如下:

状态含义可流转到触发动作
待付款下单成功但未支付已取消、待发货支付、超时自动取消
已取消用户取消或超时未付无释放库存
待发货/待取货支付完成,等待商家发货或到店取衣租赁中商家确认、用户取衣
租赁中租期开始待归还用户提交归还
待归还验证用户已归还,等待商家验收已完成、租赁中商家确认验收、验收不合格退回
已完成验收通过、押金已退无自动或手动退押金

为什么“租赁中”不能直接跳到“已完成”?因为汉服需要检查污渍、破损、超时,没有验收环节的话,后续纠纷完全无法追溯。哪怕线下门店当面交接,也要拍照留档,线上化之后这个步骤更不能省。

2.2 汉服租赁特有的几个坑:押金、租期、尺码、库存

这些是普通电商平台不会遇到的规则,也是这个项目的难点所在。

第一是押金怎么收。很多第一次做的人想用微信支付的“冻结金额”功能,实际上普通商户很难申请到预授权接口。实操中最常见的方案是:下单时把“租金+押金”合为一笔订单支付,归还验收通过后,再用微信支付退款接口把押金原路退回。订单表里必须同时记录rent_fee和deposit两个字段,退款时才知道退多少、扣多少。

第二是租期怎么算。汉服租赁不是按“租几天”笼统算,而是要精确到小时。订单记录start_time和expect_return_time,比如周六上午10点取衣,周一上午10点前必须归还。超时费规则也要提前定好,我采用的是:超过约定时间6小时以内不计费,超过6小时按一天加收日租金,之后每满24小时再加一天。

第三是尺码问题。汉服版型偏差比日常服装大得多,同一个尺码在不同商家的版型上能差出一个号。解决方法是详情页做身高体重尺码对照表,下单备注栏允许用户填身高体重,店主根据实际情况在后台备注建议尺码。评价模块也鼓励用户上传上身图,帮后来人做选择参考。

第四是库存锁定。同一款衣服可能只有一两件,下单必须锁库存,支付成功才真正占用库存,超时未支付要自动释放。我用stock和locked_stock两个字段配合解决,下文会写具体实现。

2.3 我最后定的模块清单与页面地图

对照原始需求,我最终砍掉了购物车。原因很直接:汉服租赁不是多选凑单的电商场景,用户一次基本只租一套,在详情页选定租期后直接下单更顺畅。购物车只是增加一次点击,却要多维护一张表、一个页面,没有实际价值。

最终模块清单:用户与登录、服装分类、服装管理、订单与租期、归还验收、押金退款、评价、公告。小程序端页面包括首页、分类列表、服装详情、下单确认、订单列表、订单详情、个人中心,底部 TabBar 用四个:首页、分类、订单、我的。管理端我没有单独做 App,而是用 Flask 的 Jinja2 模板搭了一个简易后台,功能只有三个:服装上下架、订单发货/验收、押金退款处理。管理员大概率是门店老板,在电脑上操作比在手机上快得多。

3. 后端API和数据表:把“租期、押金、库存”变成可运行的逻辑

业务规则理清之后,后端反而是最顺手的部分。核心就是几张表、一组接口和一个状态机。

3.1 数据库表设计:核心五张表

数据库我用 MySQL 8.0,字符集统一utf8mb4,别问为什么用这个,等你存用户昵称里出现表情符号乱码的时候就知道疼了。

用户表user:openid唯一索引,存微信登录凭证;session_key只在需要解密手机号时用,不建议明文长期存储,可以放缓存;nickname、avatar从微信头像昵称填写能力拿。

分类表category:name、sort,排序字段控制首页展示顺序,这个表很小但别省。

服装表dress:category_id、name、cover、images、size、color、daily_price、deposit、stock、locked_stock、status、description。cover和images分开存,列表页只需要一张封面,详情页要相册;图片 URL 存数据库,图片文件放 OSS,不要打进小程序包里。

订单表order:order_sn唯一业务单号、user_id、dress_id、dress_name、cover、start_time、expect_return_time、actual_return_time、rent_fee、deposit、penalty、status、pay_time、refund_time、remark。字段里冗余dress_name和cover是个容易被忽略的点:如果服装之后下架、改名,历史订单仍然能正确显示,不会变成一堆残缺记录。

评价表evaluation:order_id唯一约束保证一处订单只能评价一次,user_id、content、images、star。没完成的订单不允许评价,规则在后端判断。

3.2 核心接口与关键实现

接口设计不是越多越好,而是每个接口对应一个业务动作。核心接口清单如下:

接口方法说明
/api/auth/loginPOSTwx.login 换 code 后换取 openid,返回自签 token
/api/dressesGET分页、分类筛选、关键字搜索
/api/dresses/{id}GET详情、库存、评价列表
/api/ordersPOST创建订单,锁库存
/api/orders/{id}/payPOST微信支付统一下单
/api/orders/{id}/cancelPOST取消订单,释放库存
/api/orders/{id}/returnPOST提交归还
/api/admin/orders/{id}/verifyPOST管理员验收并退押金
/api/evaluationsPOST/GET提交、查看评价

创建订单时,库存扣减是整个并发安全的关键。不能先查出库存再在代码里判断够不够,那样两个用户同时下单会把同一件衣服卖出去。正确做法是直接执行原子 SQL:

# Flask + SQLAlchemy 创建订单并锁库存 @order_bp.route('/api/orders', methods=['POST']) def create_order(): data = request.get_json() dress_id = data.get('dress_id') user_id = g.user_id start_time = datetime.fromisoformat(data.get('start_time')) expect_return_time = datetime.fromisoformat(data.get('expect_return_time')) # 原子扣减可用库存 result = db.session.execute( text("UPDATE dress SET locked_stock = locked_stock + 1 " "WHERE id = :id AND stock - locked_stock > 0"), {"id": dress_id} ) db.session.commit() if result.rowcount == 0: return jsonify(code=400, msg="库存不足") days = calc_days(start_time, expect_return_time) rent_fee = Decimal(dress.daily_price) * days deposit = Decimal(dress.deposit) order = Order( order_sn=generate_order_sn(), user_id=user_id, dress_id=dress_id, dress_name=dress.name, cover=dress.cover, start_time=start_time, expect_return_time=expect_return_time, rent_fee=rent_fee, deposit=deposit, status='pending_payment' ) db.session.add(order) db.session.commit() return jsonify(code=0, data=order.to_dict())

这个WHERE stock - locked_stock > 0是行级条件判断,数据库本身会保证并发下的原子性,比SELECT FOR UPDATE的写法更轻量,也不会死锁。超时未支付释放库存时,反向执行UPDATE dress SET locked_stock = locked_stock - 1 WHERE id = :id AND locked_stock > 0即可。

3.3 押金退回与超时费:最容易出错的环节

押金退款走微信支付 v3 的退款接口,路径是/v3/refund/domestic/refunds,需要商户号、证书序列号、私钥。用 requests 直接调:

import requests def refund_deposit(order, refund_amount): url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds" payload = { "out_trade_no": order.order_sn, "out_refund_no": f"REFUND_{order.order_sn}", "amount": { "refund": int(refund_amount * 100), # 单位是分 "total": int((order.rent_fee + order.deposit) * 100), "currency": "CNY" } } # 构造签名头并请求,代码省略 resp = requests.post(url, json=payload, headers=auth_headers) resp_data = resp.json() if resp_data.get("status") == "PROCESSING": order.refund_status = "pending" db.session.commit() return resp_data

这里有个非常经典的坑:金额单位是分,不是元。租一天 128 元,传给微信支付必须写成 12800。我见过有人传了128导致用户只退了 1.28 元的线下事故。

超时费的计算逻辑也要放在后端统一实现:

def calc_penalty(expect_return_time, actual_return_time, daily_price): diff = actual_return_time - expect_return_time if diff <= timedelta(hours=6): return Decimal(0) days = (diff.days + 1) if (diff.seconds > 0 or diff.days >= 0) else 0 return Decimal(daily_price) * days

验收通过后,实际退款金额refund_amount = order.deposit - penalty,如果押金不够扣超时费,就记录欠款,线下追收。订单表里要把penalty字段存下来,退款之后用户问为什么少退了,直接拿数据说话。

3.4 定时任务:后台的隐形员工

订单状态机上有些自动流转是靠定时任务驱动的。我在 Flask 里集成了 APScheduler,起了三个任务:

  1. 每 10 分钟扫描超过 15 分钟未支付的订单,状态改为已取消,恢复锁定库存。
  2. 每天早上 9 点扫一遍租期即将到期或已经超时的订单,通过微信订阅消息提醒用户归还。
  3. 对退款状态为pending的订单查微信支付结果,未成功的隔一段时间重试。

定时任务逻辑不复杂,但没它系统会漏掉大量状态更新。尤其“超时未支付自动取消”这个行为,用户不一定主动取消,没有定时扫描的话库存会被长时间占死。

4. 前端页面与微信小程序适配:从H5原型到真机预览的差异

后端稳定之后,前端就是典型的 uniapp 开发。这里单独讲一下从 H5 页面调试到小程序真机的差异,因为很多问题只在真机上才会暴露。

4.1 项目创建与目录规划

我用 HBuilderX 创建 uniapp 项目,选择 Vue 3 版本。目录结构按惯例走:pages放页面、components放公共组件、static放本地静态资源、utils/request.js放请求封装、uni_modules放组件库。pages.json里的tabBar配四个入口,navigationBarTitleText设置每页标题。

组件库选了 uview-plus,HBuilderX 插件市场直接导入,然后在pages.json配置 easycom:

{ "easycom": { "autoscan": true, "custom": { "^u-(.*)": "uview-plus/components/u-$1/u-$1.vue" } } }

配置完之后页面里直接写<u-button>、<u-swiper>,不需要手动 import。这一套组合在开发效率上非常舒服,但要注意版本一致,我见过有人升级组件库之后表单组件 API 变了,页面大面积报错。

4.2 request封装:token、baseURL 和环境切换

小程序里请求后端必须走uni.request,封装统一的request.js能省大量重复代码:

// utils/request.js const BASE_URL = 'https://api.example.com' export function request(path, { method = 'GET', data = {}, needAuth = true } = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', ...(needAuth ? { Authorization: uni.getStorageSync('token') } : {}) }, success: res => { if (res.data.code === 401) { uni.navigateTo({ url: '/pages/login/login' }) return } resolve(res.data) }, fail: reject }) }) }

开发阶段的 baseURL 是个大坑。小程序开发者工具里可以勾选“不校验合法域名”,用局域网 IP 访问 Flask 服务;一旦切到真机预览,localhost就失效了,必须换成电脑的局域网 IP。发布上线前再切到正式 HTTPS 域名,并把开发者工具的“不校验合法域名”关掉,防止线上出现请求直接失败的情况。

4.3 列表分页与核心页面交互

首页结构是轮播图 + 分类宫格 + 推荐服装列表,数据从/api/dresses接口拉取。列表页的分页加载是每个小程序都会遇到的功能,核心在于状态控制:

let page = 1 let hasMore = true let loading = false async function loadList(reset = false) { if (loading || (!reset && !hasMore)) return loading = true if (reset) { page = 1 hasMore = true } const res = await request(`/api/dresses?page=${page}`) const list = res.data.list if (reset) { this.dressList = list } else { this.dressList.push(...list) } hasMore = list.length === 20 page += 1 loading = false }

在页面里用onReachBottom触底时调用loadList(false),用onPullDownRefresh下拉时调用loadList(true)。有个细节:如果没有loading标志位,用户快速上下滑动会发出大量重复请求,接口和页面双双卡死。

服装详情页要展示封面大图、轮播图、尺码表、租金押金、租期选择。图片预览用uni.previewImage,租期选择用picker的multiSelector模式选起止日期。下单页把rent_fee + deposit明细展示出来,同时提示超时规则,把丑话说在前面,能减少很多售后纠纷。订单列表按状态分 Tab,每个订单卡片显示状态标签,点击进入订单详情。

4.4 微信登录、手机号与订阅消息

登录流程已经不需要弹窗授权了,直接在页面uni.login拿到 code,传给后端,后端调用微信code2Session接口换 openid,返回自签 token,前端存进uni.setStorageSync。手机号字段通过button open-type="getPhoneNumber"获取,需要在小程序后台申请对应权限。

订阅消息值得重点做,因为汉服租赁天然适合“归还提醒”场景。用户下单后弹一次uni.requestSubscribeMessage,同意后后端就能在归还日前一天给他发模板消息。注意一件事:小程序的一次性订阅,用户点一次只能发一条,所以不能只在首页弹一次就完事,最好在归还成功、评价完成这些关键时刻再次引导订阅。

4.5 H5和小程序端的适配差异

最明显的差异是顶部导航栏高度。H5 页面用系统导航栏很统一,到小程序就乱了:刘海屏、胶囊按钮高度不固定。如果自定义导航栏,需要用uni.getSystemInfoSync()拿到状态栏高度,再根据胶囊按钮的边界把导航栏高度算出来:

const systemInfo = uni.getSystemInfoSync() const menu = uni.getMenuButtonBoundingClientRect() const navHeight = menu.bottom + (menu.top - systemInfo.statusBarHeight)

分享功能也必须自定义,在onShareAppMessage里配置title和path,path 要带参数,比如/pages/detail/detail?id=12,别写成不带 query 的路径,否则用户从分享点进来永远看到第一件衣服。涉及微信特有 API 的地方用条件编译// #ifdef MP-WEIXIN包住,保持 H5 端也能正常编译。

5. 小程序打包上线避坑:2MB限制、导航栏与审核边界

写完之后最折磨人的不是写代码,而是上传发布。第一次打包上传就撞上了经典的source size 2612kb exceed max limit 2mb,这一节把排错过程完整讲一遍。

5.1 2612KB 超过 2MB 怎么办

报错原因很好猜:uview-plus 全量组件进了主包,加上 static 目录里本地图片占了空间,几个页面也全塞在主包里,直接把主包撑爆。解决思路是“主包精简 + 分包加载 + 资源外置”。

第一步,把不常访问的页面放进分包。pages.json里配置subPackages:

{ "pages": [ { "path": "pages/index/index" }, { "path": "pages/category/category" }, { "path": "pages/order/order" }, { "path": "pages/my/my" } ], "subPackages": [ { "root": "pagesDress", "pages": [ { "path": "detail/detail" }, { "path": "checkout/checkout" }, { "path": "evaluate/evaluate" } ] }, { "root": "pagesOrder", "pages": [ { "path": "list/list" }, { "path": "detail/detail" } ] } ] }

主包只保留 TabBar 页面和公共组件,分包页面按功能域拆分。第二步,把 static 里超过几十 KB 的图片全部传到 OSS,数据库存 URL。第三步,uview-plus 也支持按需引入,只引入用到的组件模块,能进一步压缩体积。改完之后我的心跳终于正常了,体积从 2.6MB 降到了 1.2MB 左右。

5.2 域名、HTTPS 与微信支付商户号

小程序 request 合法域名必须是已备案的 HTTPS 域名,自签名证书不行。开发阶段可以在开发者工具里勾选“不校验合法域名”来调试,但发布前必须在微信公众平台后台配置好正式域名。我当时因为忘配域名,真机扫码打开全是请求超时,排查了大半天才发现是域名没加白名单。

微信支付需要在服务商申请商户号。这里提醒一下资质问题:个人主体的小程序基本做不了微信支付,至少需要个体工商户资质,平台类目还要额外提供资质证明。商户号申请下来后,后端统一下单接口返回prepay_id,前端调uni.requestPayment把支付参数传进去。金额单位是分,签名算法要用 v3 的 SHA256-RSA,别用旧版 v2 的 MD5 签名,微信已经逐步淘汰了。

5.3 审核被拒的常见理由与应对

小程序审核被拒基本是这几个原因,提前准备能省好几轮提审:

  1. 类目不符合。服装租赁可以选“生活服务 > 租赁”或“电商平台”,不同类目要求不同资质。个人主体很多类目直接不给过,建议从一开始就用企业或个体工商户主体注册。
  2. 审核人员无法体验完整流程。提供测试账号,或者在备注里写明完整的操作路径,如果有微信支付环节,准备一个可用的测试金额说明。
  3. 隐私协议缺失。小程序后台必须配置《用户隐私保护指引》,页面首次启动弹隐私协议弹窗,用户同意之后才能调uni.login、getPhoneNumber这类接口。这个现在审核必查,别等驳回再补。
  4. 素材版权。服装图片、详情页 banner 不使用网络搬运图,用商家自己拍的实物图或购买版权的图。汉服圈的图片版权纠纷特别多,一旦被举报下架更麻烦。
  5. 明显 bug 和空状态。审核人员会真实点一遍每个页面,接口报错、白屏、无数据状态不处理,直接打回。每个列表页都做好空态展示,至少不会因为难看被拒。

5.4 上线后的运维清单

发布不等于结束。Flask 后端用logging模块把关键动作全打日志:支付回调、退款请求、订单状态变化,每一条都要有时间戳和订单号。MySQL 的慢查询日志开着,一个月看一次,给order.user_id、order.status、dress.category_id这些高频查询字段加索引。小程序后台自带的错误监控打开,uni.request的 fail 回调里把错误堆栈上报。

版本更新也不要骚操作,微信小程序没有热更新通道(有违规风险),老老实实走体验版→正式版提审流程。体验版先发给店主实测一遍,没问题再提审正式版。

最后再分享一个我个人的体会:做完这个项目后回头想,技术上的难点并不在 Python、uniapp、微信小程序这些工具本身,而在于订单状态机、押金退款策略、库存一致性这些业务规则的严密程度。把业务规则写清楚,框架和语言都只是顺手工具。如果后面想继续扩展,可以考虑接入芝麻信用做免押、支持多门店入驻、把摄影师约拍作为增值服务加入订单,但前提一定是先把手上的退款闭环跑得足够稳,再谈增长。

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

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

立即咨询