先说一个很多新手容易踩的坑:拿到一份“校园商店商城购物小程序”的源码,第一反应是双击 .zip 里的文件、或者拖到浏览器里看效果——“上一轮交付的是微信小程序源码工程,它不能像网页那样直接打开”,这句话我已经听过无数次了。小程序不是网页,它必须跑在微信开发者工具里,而且现实中的小程序商城项目,也从来不是“一个前端文件夹”就能跑起来的。它背后一定有一个提供商品、订单、登录数据的后端服务,前端大多数情况下是用 uniapp 开发的跨端商城,可以同时编译成微信小程序、抖音小程序、支付宝小程序和 App。
这篇就围绕“Python + uniapp 校园商店商城购物小程序”这个项目展开,把技术选型、功能拆解、前后端联调、多端打包上架、真机调试和常见坑一次性讲透。内容偏实战记录,适合正在做毕设、课设,或者想上手“小程序电商”这套体系的开发者参考。不管是后端用 Python(Django REST Framework / FastAPI 均可),还是前端用 uniapp 的 Vue3 语法,你都能从里面找到能直接抄作业的部分。
1. 项目整体设计与技术选型思路
1.1 为什么是 Python 后端 + uniapp 前端
先把技术栈拆开看。后端选 Python,核心原因是生态成熟、上手快,尤其在做校园类中小型项目时特别划算。Django REST Framework 自带 Admin 后台、ORM、认证体系和序列化器,你不需要自己写一堆重复的增删改查接口;FastAPI 则更轻,性能好,自带 Swagger 文档,适合喜欢前后端分离、接口调试流畅的开发者。校园商店这种项目,QPS 不会高,瓶颈根本不在 Python 本身,而在于你的业务逻辑是否清晰、接口设计是否合理。
前端选 uniapp,最大的价值是“一套代码多端发布”。你写一次商城页面,就可以编到微信小程序、H5、App,甚至支付宝小程序。对于校园项目来说,这意味着你不需要分别学微信原生语法和安卓开发,只需要会 Vue 的组件化思路,就能覆盖“微信小程序 + 安卓/iOS App”两个主流终端。开发效率翻倍,而且维护成本很低。
需要注意一点:uniapp 不是“一套代码完全不改就能跑所有端”的银弹。端差异集中在导航栏高度、登录方式、支付方式、定位权限和地图组件上。比如微信端登录用 wx.login,App 端就需要 uni.login 配合第三方登录,支付更是直接分成了微信支付和支付宝支付两套流程。所以设计时就要预留多端适配层,把“端差异”隔离在统一封装里。
1.2 校园商店的业务边界与角色设计
这个项目叫“校园商店商城购物小程序”,要先把“校园”两个字落实。它和普通电商最大的区别在于:用户群体集中在校园内,商品偏生活化(零食、饮品、二手教材、学习用品、手办、打印服务、代取快递),配送范围基本是“宿舍区到教学区”。所以商品模块不太需要复杂的全国物流,但需要“自提点”或“校内配送时间段”这种设计。
角色划分上,我建议至少做四种:
- 学生/普通用户:浏览商品、加购物车、下单、支付、查看订单、评价、申请售后。
- 商家/店主:管理自己的商品库存、上下架商品、接单、发货或标记自提完成。
- 平台管理员:审核商家、审核商品、处理投诉、查看全站订单数据。
- 系统游客:只能浏览首页和商品详情,不能加购和下单。
如果你做的版本是“单商户”的,简化成一个用户模型加一个管理员模型也可以,但明确这些边界能让你后续扩展省很多事。
再往下拆,是核心业务表。我最常用的一套设计是:用户表、收货地址表、商品分类表、商品表、商品规格表(SKU)、购物车表、订单表、订单商品快照表、支付流水表、售后申请表、商家表。其中订单表和订单商品快照表必须拆开,因为订单里的商品信息是“下单那一刻的封存数据”,不能直接关联商品表,否则商品改名或删掉后订单显示就会错乱。
1.3 前端目录结构怎么组织才不乱
很多 uniapp 项目的坏味道,是把所有逻辑堆在pages里。等页面一多,改一个接口要把整个项目翻一遍。推荐按“功能模块 + 分层调用”的方式组织:
src/ ├─ pages/ // 页面:仅负责渲染和交互 │ ├─ index/ // 首页 │ ├─ goods/ // 商品列表与详情 │ ├─ cart/ // 购物车 │ ├─ order/ // 订单确认/列表/详情 │ └─ user/ // 个人中心、地址管理、售后 ├─ components/ // 通用组件:商品卡片、空状态、价格标签 ├─ api/ // 接口层:按模块封装 request │ ├─ request.js // uni.request 封装,统一处理 token │ ├─ goods.js │ ├─ cart.js │ └─ order.js ├─ store/ // Pinia,管理登录态、购物车角标、用户信息 ├─ utils/ // 工具函数:金额格式化、时间格式化、防抖 └─ static/ // 静态资源所有请求都必须走api/request.js,不能在页面里直接写uni.request。这样统一加 token、统一处理 401 跳转登录、统一弹错误信息、统一埋点,后面会轻松非常多。
2. 核心业务模块设计与接口实现
2.1 用户登录与手机号授权,别再傻傻用 wx.getUserProfile
微信小程序登录这套流程,网上很多旧教程已经过时了。现在正确且稳定的流程是:前端调uni.login拿临时code,后端拿code + appid + secret请求微信接口换openid,再用openid作为用户唯一标识生成你自己的登录态(比如 JWT token),返回给前端存起来。
手机号授权这块,小程序已经不允许通过弹窗拿到手机号了。必须让用户点击<button open-type="getPhoneNumber">按钮,得到code,然后把这个code传给后端,后端调用微信的接口去换取手机号。这个按钮不能模拟点击,必须用户主动触发。
App 端有所不同:uni.login拿到的 code 不能直接用于微信小程序换 openid,App 端的逻辑一般走“手机号验证码登录”或者“微信授权登录(openSDK)”。所以项目里建议再加一个“手机号 + 验证码”的登录通道,多端通用,也便于测试。前端只做一个“登录页”,内部根据平台自动切换逻辑:
// api/login.js export function wxLogin() { return new Promise((resolve, reject) => { uni.login({ provider: 'weixin', success: (res) => { // res.code 传给后端 loginByWxCode({ code: res.code }).then(resolve).catch(reject) }, fail: reject }) }) }后端伪代码逻辑是这样的:
# Django 示例 def wx_login(request): code = request.data.get('code') url = "https://api.weixin.qq.com/sns/jscode2session" params = { "appid": settings.WX_APPID, "secret": settings.WX_SECRET, "js_code": code, "grant_type": "authorization_code" } resp = requests.get(url, params=params).json() openid = resp.get("openid") # 根据 openid 找到或创建用户,签发 JWT token = create_token(user) return JsonResponse({"token": token, "user": user_info})我这里强烈建议:不要在前端保存 openid,更不要用 openid 直接当用户 id 返给前端。暴露 openid 有风险,统一用你自己生成的 user_id 和外层 token 隔离。
2.2 商品、购物车、订单的状态机设计
购物车是典型的“临时数据”,设计时不需要太复杂。核心字段是:用户 id、商品 SKU id、数量、选中状态、加购时间。加购同一个 SKU 时数量累加,不新增记录;修改规格时如果目标 SKU 已存在,也要合并数量。
购物车的接口建议一次性返回全部关联信息,包括商品名、封面图、规格名、单价、库存,前端不用再循环请求详情接口。后端用 ORM 的select_related或prefetch_related做联表查询,一次性把数据捞齐。
订单状态设计我觉得是这类项目里最该认真画的一张图:
- 待付款:用户下单成功但未支付,有支付倒计时(一般 30 分钟)。
- 已支付(待发货):支付回调成功后进入此状态,小商户可以手动点击发货。
- 已发货/待收货:填入物流单号或标记“自提点已备货”。
- 已完成:用户确认收货,订单生命周期完结。
- 已取消:用户主动取消或超时未支付。
- 售后中:部分商品申请退款,订单进入特殊状态。
- 已退款:退款完成。
后端做状态流转时必须校验“合法迁移路径”,不允许从“待付款”直接跳到“已完成”。我用 Django 的话会在模型里加一个status_choices,在 Service 层写单个transition函数统一处理,前端只需要传递action,由后端决定新的状态,而不是把状态直接交给前端去改。
下单接口有一个我吃过亏的点:一定要做“下单时库存预扣”或“支付后扣库存”,二选一。校园商店商品数量不大,推荐“下单预扣 + 取消释放”,防止多人同时下单造成超卖。预扣时要用“乐观锁”或者数据库行锁:
# UPDATE 行级原子扣减,避免超卖 updated = GoodsSKU.objects.filter( id=sku_id, stock__gte=quantity ).update(stock=F('stock') - quantity)如果updated == 0,说明库存不足,直接抛异常。
2.3 后端 API 的分层与权限控制
很多小伙伴拿到一份现成 Python 源码,第一眼看到几十个接口函数会懵,等自己写了一次之后才明白:接口层的代码不能一股脑全堆在views.py里。哪怕是小项目,也要把“路由—视图—Service—模型”拆开。给一个我从实际项目里简化过的文件布局:
backend/ ├─ apps/ │ ├─ goods/ │ │ ├─ models.py # 商品、分类、SKU │ │ ├─ serializers.py # DRF 序列化器 │ │ ├─ views.py # 只做请求参数解析、调用 Service │ │ └─ service.py # 业务逻辑:上架、库存修改、商品搜索 │ ├─ order/ │ │ ├─ models.py │ │ ├─ service.py # 下单、支付回调处理、订单状态流转 │ └─ user/ │ ├─ models.py │ └─ service.py # 登录、注册、地址管理为什么把业务逻辑放 Service 层而不是直接写在 view 里?因为同一个逻辑可能在多个入口复用。比如“创建订单”不仅要被订单接口调用,还要被“立即购买”和“购物车结算”两个入口调用,写在 Service 里就只需要一份代码。权限上用 DRF 的IsAuthenticated做全局默认,再把首页、商品列表、商品详情这几个“游客可访问”的接口用permission_classes单独放开。
支付回调是后端最容易写错的地方,因为微信会连续通知多次。回调处理必须是“幂等”的:通过订单号和支付流水号判断,如果流水已经处理过,直接返回成功,不重复改订单状态、不重复加积分。我举一个很常见的例子:用户支付成功,回调里你给商户账户加了一次钱,但因为网络原因微信重试了一次,如果你没做幂等,商户账户就被加了两次。
3. uniapp 多端适配、打包与上架
3.1 微信小程序打包与“分包”解决 2MB 恐惧症
刚接触 uniapp 的人第一次编译微信小程序,经常看到这句报错:
上传失败:source size 2612kb exceed max limit 2mb
微信小程序主包体积限制是 2MB(现在部分情况可以通过压缩扩展到 3MB 或更多,但规则随时收紧),而 uniapp 项目里光是 uni-ui、uview-plus、自定义字体、大图资源就很容易超。解决思路是“大资源往外放、小包只留骨架”。
第一板斧是分包。把“首页、分类、购物车、我的、商品详情”这些核心页面留在主包,把“订单详情、售后、评价、优惠券、商家后台、客服聊天”等低频页面放进subPackages。配置很简单:
// pages.json { "pages": [ "pages/index/index", "pages/goods/list", "pages/goods/detail", "pages/cart/cart", "pages/user/user" ], "subPackages": [ { "root": "pagesA", "pages": [ "order/list", "order/detail", "order/refund", "coupon/coupon" ] }, { "root": "pagesB", "pages": [ "merchant/goods_manage", "merchant/order_manage" ] } ] }微信小程序运行时,只有访问到分包里的页面才会去下载对应资源。但要注意:分包之间不能互相跳转文件,公共组件和公共样式还是留在主包里。另外页面跳转如果用到uni.navigateTo,目标路径要写成“/pagesA/order/detail?id=xxx”这种完整分包路径,第一次进入会有一个小白屏加载过程,这是正常的。
第二板斧是资源外置。商品图片、Banner 图不要打成包再上传,直接放 OSS/CDN,页面里用https://外链地址。字体图标能精简就精简,能用系统图标就不用自定义字体。uview-plus 这类第三方组件库,尽量按需引入,不要全量注册到easycom。
第三板斧是压缩。开发模式下 uniapp 编译出来会带比较详细的 sourcemap,发布模式勾选“压缩”选项。还有pages.json里全局navigationBarTitleText别设得太长,页面 JSON 配置里能删的注释都删掉——这些虽然看着不起眼,但积少成多。
3.2 manifest 配置与安卓/iOS 上架那些事
uniapp 打包微信小程序端相对简单,填好小程序 appid 就行。麻烦的是打包 App。打开项目的manifest.json,里面有几个必填项容易漏:
- 基础配置里的
uni-app 应用标识(appid)是 DCloud 平台生成的,不是微信那个 appid。 - App 模块配置:地图要用
Maps,支付要用Payment,定位要用Geolocation。 - 权限配置:常用
Android权限要勾定位权限、相机权限、存储权限,很多上架整改就是因为权限声明远多于实际功能。 - 隐私政策弹窗:安卓应用市场强制要求,必须在 App 启动前弹窗展示隐私政策。
云打包在 HBuilderX 里点“发行—原生App-云打包”,选安卓包就行。但要注意包名和证书,同一个包名如果之前已经在应用市场占用了,不换新包名是发不上去的。上架安卓应用市场时,各市场的审核侧重点不太一样,但核心是三点:需要软著(部分市场)、必须能正常拉起登录和支付、隐私政策必须真实有效可点击。
iOS 上架是另一个世界:需要 Apple 开发者账号($99/年),需要用 Mac 上的 Xcode 做最后的归档签名。校园项目如果只是演示,建议直接用苹果 TestFlight,省去审核周期。如果你的项目准备上架 App Store,注意音频后台播报这类功能需要在 Xcode 里配置UIBackgroundModes,否则息屏播放会直接被系统杀掉。
3.3 热更新、后台运行与定位监测的配置
uni-app 的 App 端热更新是一个很实用的能力,它可以在用户打开 App 时检查服务端发布的新版本,自动下载差量包并重启加载。打包时分为“资源更新”和“整包更新”:
- 资源更新:只更新前端页面和 JS 逻辑,打包成
.wgt包,App 内静默安装。 - 整包更新:改动了原生插件、SDK、权限配置时,必须重新提交商店审核。
实际项目中维护一套热更新接口,App 启动时请求checkVersion,返回是否有新版本,再下载对应.wgt包。需要注意:老版本 App 的“基座版本”如果和新的.wgt不一致,热更新会失败。所以版本校验时不能只看业务版本号,还要对比plus.runtime.version。
后台定位监测是商城 App 里常见又容易出问题的地方。校园场景可能是“骑手配送轨迹追踪”,需要用uni.startLocation开启 GPS 定位,再配合plus.geolocation.watchPosition持续监听位置变化。但这里有两个关键坑:
- 小程序端不支持长时间后台定位,后台一定被回收;App 端也需要在 manifest 里勾选
后台运行定位,并在手机系统设置里打开“始终允许定位”。 - iOS 端要求写明“后台定位用途”,否则审核被拒;Android 11+ 对后台定位权限进一步收紧。实现上推荐用
plus.geolocation原生能力,而不是单纯的uni.getLocation单次定位。
封装参考:
function startTracking() { plus.geolocation.watchPosition( (res) => { const { longitude, latitude } = res.coords uploadLocation({ longitude, latitude }) }, (err) => console.error(err), { enableHighAccuracy: true, maximumAge: 0, timeout: 10000 } ) }还要提醒一下:所有涉及用户位置的上报,都必须在前端有明确的功能说明和隐私弹窗提示,这是合规底线。
4. 开发调试中的常见问题与排查
4.1 用抓包工具调试自己的接口,别碰别人的数据
项目联调阶段,前后端对接最头疼的就是“前端看不见后端到底返回了什么”“后端说没问题但前端就是报错”。这时候就该上抓包工具,最常用的就是 Charles。它的价值在于:把手机上小程序发出的 HTTPS 请求解密出来,你可以清清楚楚看到 URL 参数、请求头、响应体。真正的开发姿势是拿它调自己的项目,排查参数错误、调试支付回调、看带没带 token。
配置流程大致是:PC 端开启 SSL Proxying,手机设置 HTTP 代理指向电脑 IP 和端口 8888,然后手机浏览器访问chls.pro/ssl下载并信任证书。Android 7.0 以上默认不信任用户证书,需要在 manifest 的networkSecurityConfig里加上调试证书信任,开发模式可以用android:debuggable="true"配合测试包绕过。
这里要守住一条线:只对自己开发的后端接口抓包,不要拿抓包工具去分析其他小程序、去伪造请求干不该干的事。最近热词里有人问“利用应用宝获取通用小程序 code”,这类灰色操作千万别碰。调试好自己的支付回调、登录授权比什么都强。
4.2 日志不打印和真机调试的问题
“uniapp 不打印日志信息”是很玄学的一件事,明明前端写了console.log,真机调试就是看不到。我自己踩过的坑有三个:
- HBuilderX 的 Console 面板过滤级别默认可能只显示 Error,要把级别切到“Verbose”或“Info”;
- 微信开发者工具里,真机调试和模拟器的日志是分开的,如果你连接的是真机,要在微信开发者工具“真机调试”面板里看;
- App 端的真机运行,部分安卓手机系统会杀掉日志进程,打开“开发者选项—关闭日志缓冲”再试。
排除这些之后如果还是看不到,在代码里临时打一个uni.showToast({ title: JSON.stringify(data), icon: 'none' }),粗暴但有效。
真机调试还容易遇到“网络请求失败”。手机和电脑必须处于同一局域网;微信开发者工具开的“不校验合法域名”只在工具里生效,真机上必须把后端域名加到微信公众平台后台的“服务器域名”白名单里,且要求是 HTTPS。我惯用的做法是本地开发用 Python 起一个临时 HTTPS 转发(用mkcert签证书),真机调试走内网 IP,联调结束后切回线上域名。
4.3 高频问题速查表
我把这个项目里最容易踩、被问最多的几个点整理成一份速查表,适合直接贴在工位上:
| 现象 | 根本原因 | 处理办法 |
|---|---|---|
| 编译报 source size 2612kb exceed max limit 2mb | 主包体积超 2MB | 拆分包、图片转 CDN、压缩代码 |
| 用户真实手机号拿不到 | 必须用 button open-type="getPhoneNumber" 获取 code | 前端按钮触发,不能直接 uni.login 拿手机号 |
| App 上架后被要求整改 | 权限声明过多或隐私政策缺失 | 清理 manifest 里没用的权限,补隐私弹窗 |
| 动态标题不生效 | 调用时机太早或页面未加载完 | onLoad里用uni.setNavigationBarTitle设置 |
| 列表加载更多重复请求 | 没有加分页锁 | onReachBottom判断isLoading和hasMore |
| 视频流播放不了 | 小程序 live-player 需开通对应类目 | 确认后台申请直播权限,且只播对应类目内容 |
| 热更新后界面没变 | .wgt 包版本号没变 | 检查版本号递增,且基座版本匹配 |
| 支付成功后订单状态没变 | 回调没做幂等或回调地址不可达 | 检查回调日志,补幂等判断 |
| 单选框样式难看 | 原生 radio 样式不可控 | 自定义图标组件,或使用 uview radio 组件 |
比如“列表加载更多”,很多人的代码一进页面就是请求第一页,上拉底部又重复发起加载。我习惯用一个pageNo/pageSize/hasMore/isLoading四件套:
const loadMore = async () => { if (state.isLoading || !state.hasMore) return state.isLoading = true const res = await getGoodsList({ page: state.pageNo, pageSize: 10 }) state.goods = state.goods.concat(res.list) state.hasMore = res.list.length === 10 state.pageNo += 1 state.isLoading = false }还有一个细节经常被忽略:onReachBottom在部分安卓手机上触发很灵敏,页面一出现就连续触发几次,所以一定要加上“锁”。这里的锁就是isLoading判断,每次请求结束前不允许触发第二次加载。
5. 支付与订单联动,后端必须守住的三道关
5.1 微信支付前置条件与统一下单
校园商城最终避不开在线支付。如果只是毕业设计,可以考虑用“模拟支付”,前端调后端支付接口,后端直接把订单改成已支付,简单省事。但如果是真实上线,微信支付的下单流程必须走对。
首先小程序端要满足:认证主体是企业或个体工商户,申请了微信支付商户号,并且在小程序后台关联了“微信支付”能力。个人主体的小程序不支持微信支付,这一点要提前查清楚。
后端统一下单逻辑很简单:前端把订单 id 传给后端,后端拿订单号去微信支付 API 创建预支付单,拿到paySign、nonceStr、timeStamp等参数后返回给前端,前端再调uni.requestPayment拉起收银台。关键在后端生成paySign时用的签名算法,这个必须在后端完成,密钥不能出现在前端。很多从网上抄的项目把商户号 API 密钥写死在pages.json里,这是极其危险的。
支付成功后微信会往配置的回调 URL 发通知,后端代码要做三件事:
- 验签:根据微信的签名算法重新 MD5 对比;
- 查单:拿订单号到微信后台确认支付金额一致,防止伪造回调;
- 幂等:同一笔 order_no 处理过一次就直接返回成功。
5.2 订单超时与库存释放
校园项目的商品大多是有实际库存的,超时未支付如果一直占着库存,会导致“库存不少,但一件都买不了”。我建议用 Django 的定时任务或者 Celery Beat 定期扫描待付款订单,超过 30 分钟未支付就自动取消并回补库存。
如果不想引入消息队列,最简单的做法是在下单时记录pay_deadline,每次访问商品详情时顺手关掉超时订单,再在接单列表里做同样的清理。这种“懒清理”对小项目够用,但如果你追求严谨,还是按定时任务来。释放库存时要注意并发:一个订单取消的同时,另一个用户可能刚下单,此时更新库存要用上面的原子操作去扣减,不能先查再改。
热门词里有一条“免费python源码大全”,其实这些源码很多都能在开源平台直接找到,但用到生产环境里时,一定要把支付密钥、数据库密码、Redis 连接串全部用环境变量管理,不要提交到仓库里。只要是涉及真实支付的项目,密钥泄露就是灾难性的,轻则被刷单,重则商户号被冻结。
6. 从“跑起来”到“能演示”,最后这几步必须做完
很多人的项目卡在“后端接口一堆,小程序页面一堆,但连起来就崩溃”。我的经验是:先别急着调样式,先通接口链路。按照“登录 → 首页 → 商品详情 → 加入购物车 → 生成订单 → 模拟支付 → 订单列表 → 确认收货”这个主流程,逐个接口调试。每一段都打通了,再去优化 UI 和交互。
联调时建议后端把所有接口的响应格式统一。比如统一为{ code, message, data }结构,前端封装的request.js里统一处理code。这样比让后端随时改变返回结构要省心得多。测试阶段一定要给后端加日志,打印每次请求的耗时、参数、返回状态,否则排查问题全靠猜。
还有一点很重要:项目演示前先清理掉测试数据。如果购物车里残留了一堆奇怪的体验商品、订单记录里全是乱码备注,评委或用户一眼就能看出这是个“半成品”。准备一个一键初始化的脚本,演示前跑一遍,把订单状态全部重置为合理状态。
我自己在做这类校园商城项目时最大的体会是:你说的“商城”二字,最核心的不是页面漂亮,而是下单和支付这条主链路的稳定。页面可以在后期反复美化,但订单状态机、库存、回调幂等这些底层设计,一旦前期没想清楚,后期改起来就是牵一发动全身。
最后再分享一个小技巧:开发时前端不要依赖后端的真实登录态,我在request.js里加了一个“游客模式开关”,开关开启时自动注入一个测试 token,后端提供一个testLogin接口返回假用户数据。这样你一个人同时写后端和前端时,就不用来回扫码登录了,联调效率能提升不少。等前后端都稳定了,再关掉开关,走真实微信登录链路。这个小工具让我省下的时间,至少够我多写一套完整的后台管理页面。