ARTICLE DETAIL

资讯详情

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

Vue + UniApp + Python 小程序点餐订单系统开发实战

Vue + UniApp + Python 小程序点餐订单系统开发实战 做点餐订单系统这事最早是帮一个做餐饮的朋友忙。他店里想上小程序点餐省去服务员来回跑但问了一圈外包报价动辄几万块还说不清后续怎么维护。我当时正好在折腾 Vue 和 UniApp就接过来自己搞后端顺手用 Python 写了接口前后端加起来一个多月跑通最后真在店里用上了。这篇就是把整个 Vue UniApp Python 小程序点餐订单系统的完整落地过程捋一遍从技术选型、后端设计、前端页面到打包上架能帮到正在做类似项目或者想拿小程序练手的朋友。1. 项目全景为什么是 Vue UniApp Python 这套组合先说结论这套组合不是最酷的但一定是个人开发者或者小团队最稳的选择。我一个独立开发不可能同时维护 iOS App、安卓 App、微信小程序和网页端UniApp 让我写一套代码编译到小程序、H5 和 App后端接口用 Python 写开发效率是第一位的。1.1 技术选型的真实理由很多人纠结要不要上 UniApp担心性能和原生差距。我的看法是点餐系统这种业务页面不复杂交互也简单核心是下单链路通顺UniApp 完全够用。真实情况是UniApp 的底层已经封装得比较成熟小程序端最终编译出来还是走的微信原生组件只是开发语言从 WXML 换成了 Vue 语法。Vue 这边我选的是 Vue 3 组合式 API。因为 UniApp 从 3.x 版本开始默认支持 Vue 3语法更简洁逻辑复用也方便写购物车状态管理、订单提交这种共享逻辑比 Vue 2 的 Options API 舒服很多。Python 后端没有悬念用的 Flask。选 Flask 不是因为它比 Django 强而是因为这种小型点餐系统总共就菜品、订单、用户这几张表Django 的 Admin 后台、ORM、认证体系都偏重。Flask 轻路由自己定义搭配 SQLAlchemy 操作数据库够用了。前端UniApp Vue 3 Pinia后端Python 3.8 Flask SQLAlchemy SQLite数据库SQLite业务量大了再换 MySQL结构基本不用动1.2 系统的功能边界与业务流做系统前先画清楚边界不然很容易被需求带着跑。我定的核心链路就是三条第一用户进小程序看到菜品分类点菜加入购物车。第二购物车结算生成订单选择桌号或者自取提交订单。第三后厨或者服务员看到新订单更新状态用户在小程序端看到订单状态变化。功能边界上第一期不碰支付。微信支付需要商户号、资质审核个人开发者不好搞。我的方案是「下单后线下付款」或者「到店扫码结账」订单状态由商家后台手动改成已完成。这样系统能跑通又不会被支付卡脖子。等真有商户号了再对接支付其实也就是加一个下单后唤起支付的步骤。1.3 工程结构怎么摆这是我自己实际跑下来的目录结构。前端按 UniApp 的标准来后端单独一个目录千万别混合在一起后期部署维护才不头疼。order-system/ ├── frontend/ # UniApp 前端工程 │ ├── pages/ # 页面目录 │ │ ├── index/ # 点餐首页 │ │ ├── cart/ # 购物车页 │ │ ├── orders/ # 订单列表页 │ │ └── order-detail/ # 订单详情页 │ ├── store/ # Pinia 状态管理 │ ├── utils/ # 请求封装、工具函数 │ └── static/ # 静态资源 ├── backend/ # Python 后端工程 │ ├── app.py # Flask 入口 │ ├── models.py # 数据模型 │ ├── api/ # 蓝图路由 │ └── requirements.txt # 依赖列表一个重要的经验接口地址一定要用相对路径配置成全局变量。我一开始把接口地址写死在每个页面的请求里后来从本地联调切到线上服务器改了几十个地方血泪教训。2. 开发前的环境准备Vue、UniApp、Python 三套环境怎么装不折腾环境搭建看着简单actually 踩坑最多的地方往往就是这里。很多人卡在第一步就放弃了其实大部分问题都是版本不匹配。我把整个过程捋一下你照着来基本不会出问题。2.1 Vue 环境Node 版本是第一个坑Vue 3 的开发依赖 Node.js。Node 版本太低装 Vue CLI 和运行项目的时候会报各种奇怪的错。我当时用的 Node 14装依赖的时候总报ERESOLVE unable to resolve dependency tree折腾半天发现是版本太老。建议直接装 Node 18 LTS现在主流版本都兼容。装完验证一下node -v npm -vVue 官方推荐用create-vue或者 Vite 的方式创建项目。但因为咱们是 UniApp这里其实不需要单独创建 Vue 项目UniApp 自带完整的 Vue 环境。这也是很多新手绕弯的地方先单独建了一个 Vue 项目然后又去创建 UniApp 项目结果搞不清关系。记住UniApp 项目本身就是 Vue 项目不需要重复创建。2.2 UniApp 创建项目HBuilderX 还是 CLI 二选一UniApp 有两种开发方式我用的是 HBuilderX 可视化创建因为它对小程序打包封装得最好开发者工具、模拟器这些不用自己配。但如果你习惯命令行也可以用 CLI 方式npm create uni-applatest这个命令会创建一个标准 UniApp 工程。注意创建的时候它会问是否启用 TypeScript如果你之前没有 TS 经验第一期先别勾用 JavaScript 把业务跑通再说。我后来自己项目的确上了 TS但前期没有 TS 也能做得很顺。HBuilderX 方式更简单下载安装后新建项目里直接选uni-app Vue3 项目。打开就能跑内置了代码提示和编译。我两种都试过最终生产项目用的 CLI 方式居多因为代码管理、CI 那一套更跟手。2.3 Python 环境从解释器到虚拟环境Python 装起来看似简单但坑也不少。我用的 Python 3.8官网直接下载安装包安装的时候注意勾选Add Python to PATH。很多人装完发现python命令不是内部或外部命令就是这个勾没选上。装完验证python --version pip --version依赖管理我强烈建议用虚拟环境。为什么因为后期的 Flask 版本、SQLAlchemy 版本跟系统里其他项目冲突了非常难受。虚拟环境就是一个隔离的 Python 空间python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install flask flask-cors flask-sqlalchemy把依赖记录到 requirements.txtpip freeze requirements.txt后端我用到的核心依赖就这几个依赖包用途必装原因flaskWeb 框架路由、请求处理flask-cors跨域支持H5 调试时前端 8080 端口调后端 5000 端口必用flask-sqlalchemyORM操作数据库不用写原生 SQLrequests出库调用微信登录换手机号时要用这里有个常见误区很多人以为小程序调后端接口不需要处理跨域。生产环境大部分情况确实不用但你在浏览器 H5 里调试的时候没有 CORS 就等着看一堆红色报错吧所以 flask-cors 一定要装。3. 后端接口设计Python 实现菜品、订单与状态流转后端是整个系统的心脏前端页面再好看接口不通全是白搭。我把后端拆成四个核心模块每个模块说清楚数据库怎么设计、接口怎么定义、状态怎么流转。3.1 数据模型设计三张表搞定核心业务点餐系统看着复杂抽象到底层就三张核心表菜品表、订单表、订单明细表。用户信息在登录时直接存一份不单独做用户系统。菜品表字段设计class Dish(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse) category db.Column(db.String(20), nullableFalse) # 分类热菜、凉菜、汤类 price db.Column(db.Float, nullableFalse) image db.Column(db.String(200)) # 图片URL status db.Column(db.Integer, default1) # 1上架 0下架 sales_count db.Column(db.Integer, default0) # 销量 created_at db.Column(db.DateTime, defaultdatetime.now)订单表class Order(db.Model): id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue) # 订单号 table_no db.Column(db.String(10)) # 桌号 total_amount db.Column(db.Float, nullableFalse) status db.Column(db.Integer, default0) # 0待处理 1已接单 2制作中 3已完成 remark db.Column(db.String(200)) openid db.Column(db.String(64)) # 用户标识 created_at db.Column(db.DateTime, defaultdatetime.now)订单明细表class OrderItem(db.Model): id db.Column(db.Integer, primary_keyTrue) order_id db.Column(db.Integer, db.ForeignKey(order.id)) dish_id db.Column(db.Integer) dish_name db.Column(db.String(50)) # 冗余菜品名防止菜品被删后订单没记录 price db.Column(db.Float) # 当时的单价 quantity db.Column(db.Integer) # 数量我的实践心得订单明细一定要冗余菜品名称和价格。你想想商家今天改了菜品价格昨天下的单如果只是存了菜品 ID查历史订单的时候价格就乱套了。冗余字段就是保存下单那一刻的快照这是订单系统的常规做法但很多新手没意识到。3.2 菜品接口列表、分类和详情菜品接口是最简单的前端点餐首页需要拿到分类和分类下的菜品。我设计成返回一个二维结构app.route(/api/dishes, methods[GET]) def get_dishes(): dishes Dish.query.filter_by(status1).all() result {} for dish in dishes: if dish.category not in result: result[dish.category] [] result[dish.category].append({ id: dish.id, name: dish.name, price: dish.price, image: dish.image, sales_count: dish.sales_count }) return jsonify(result)前端拿到这个结构左边渲染分类导航右边渲染菜品列表天然就是category - dishes[]的对齐关系不需要再自己组装。3.3 下单与订单状态流转核心逻辑下单接口我用了事务处理。下单不只是插入一条订单记录还要更新菜品销量、扣减库存如果有库存概念的话。这些操作要么全成功要么全失败否则数据就乱了。app.route(/api/order/create, methods[POST]) def create_order(): data request.get_json() items data.get(items, []) if not items: return jsonify({code: 1, msg: 订单不能为空}) # 计算总额 total 0 for item in items: dish Dish.query.get(item[dishId]) if not dish or dish.status ! 1: return jsonify({code: 1, msg: f菜品 {item[dishId]} 不可用}) total dish.price * item[quantity] # 生成订单号 order_no datetime.now().strftime(%Y%m%d%H%M%S) str(random.randint(100, 999)) try: order Order( order_noorder_no, table_nodata.get(tableNo, ), total_amountround(total, 2), remarkdata.get(remark, ), openiddata.get(openid, ) ) db.session.add(order) db.session.flush() # 拿到 order.id for item in items: dish Dish.query.get(item[dishId]) db.session.add(OrderItem( order_idorder.id, dish_iddish.id, dish_namedish.name, pricedish.price, quantityitem[quantity] )) dish.sales_count item[quantity] db.session.commit() return jsonify({code: 0, data: {orderId: order.id, orderNo: order_no}}) except Exception as e: db.session.rollback() return jsonify({code: 1, msg: str(e)})订单状态流转我定义为 0 → 1 → 2 → 3 的线性流程。前端订单列表页每 10 秒轮询一次状态接口拿到最新状态渲染进度条。为什么不建议用 WebSocket点餐系统状态变更频率极低商家可能几分钟才操作一次轮询完全够用还省心。状态接口加了一个小优化只返回该用户最近 20 条订单避免订单多了之后响应越来越慢。app.route(/api/order/list, methods[GET]) def get_orders(): openid request.args.get(openid) orders Order.query.filter_by(openidopenid).order_by(Order.created_at.desc()).limit(20).all() result [] for order in orders: result.append({ id: order.id, orderNo: order.order_no, totalAmount: order.total_amount, status: order.status, createdAt: order.created_at.strftime(%Y-%m-%d %H:%M:%S) }) return jsonify({code: 0, data: result})4. 前端核心页面UniApp 里的点餐页、购物车与订单页后端接口就绪后接下来是前端页面。这一节是我觉得整个项目最有含金量的地方因为涉及到的状态管理、页面通信、交互细节都是实际开发中反复调出来的。4.1 点餐首页分类联动菜品列表点餐首页是最复杂的页面。左侧是分类导航右侧是可滚动的菜品列表。点击左侧分类右侧滚动到对应分类滚动右侧列表左侧高亮对应的分类。这个双向联动我第一次做的时候搞了很久。UniApp 里可以用 scroll-view 实现view classmenu-wrap !-- 左侧分类 -- scroll-view classcategory-scroll scroll-y view v-for(cat, index) in categories :keycat :class[category-item, { active: currentCategory index }] clickswitchCategory(index) {{ cat }} /view /scroll-view !-- 右侧菜品 -- scroll-view classdish-scroll scroll-y :scroll-into-viewscrollIntoId view v-for(cat, index) in categories :keycat :idcat-${index} view classcategory-title{{ cat }}/view view v-fordish in dishList[cat] :keydish.id classdish-item image :srcdish.image modeaspectFill / view classdish-info view classdish-name{{ dish.name }}/view view classdish-price¥{{ dish.price }}/view view classstepper button clickremoveFromCart(dish)-/button text{{ cartCount(dish.id) }}/text button clickaddToCart(dish)/button /view /view /view /view /scroll-view /view很多人会忽视scroll-into-view的值是动态 ID 这个细节。你必须给每个分类区块加上idcat-0、idcat-1这样的锚点scroll-into-view才有东西可定位。我一开始只给滚动区域设了scroll-y没设 ID结果怎么点都不动。右侧滚动联动左侧高亮用 scroll-view 的scroll事件实时计算当前位置落在哪个分类区间更新currentCategoryfunction onDishScroll(e) { const scrollTop e.detail.scrollTop // 用每个分类的高度区间判断当前所在分类 for (let i 0; i categoryOffsets.length; i) { if (scrollTop categoryOffsets[i].start scrollTop categoryOffsets[i].end) { currentCategory.value i break } } }4.2 购物车状态管理Pinia 是正确选择购物车是点餐系统的核心状态。用户加了哪些菜、每个菜数量多少、总价多少这些数据要被首页、购物车页、订单确认页共享。在 Vue 3 里Pinia 是比 Vuex 更现代、更简单的选择。创建一个store/cart.jsimport { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [], // [{ dishId, name, price, quantity }] tableNo: }), getters: { totalCount: (state) state.items.reduce((sum, item) sum item.quantity, 0), totalPrice: (state) state.items.reduce((sum, item) sum item.price * item.quantity, 0).toFixed(2) }, actions: { addToCart(dish) { const found this.items.find(item item.dishId dish.id) if (found) { found.quantity } else { this.items.push({ dishId: dish.id, name: dish.name, price: dish.price, quantity: 1 }) } }, removeFromCart(dishId) { const index this.items.findIndex(item item.dishId dishId) if (index -1) { if (this.items[index].quantity 1) { this.items[index].quantity-- } else { this.items.splice(index, 1) } } }, clearCart() { this.items [] } } })购物车用 Pinia 之后首页和购物车页数据自动同步不用手动传参不用全局事件通知这也是 Vue 3 组合式开发的明显优势。如果当时用 Vue 2购物车共享逻辑会分散到各个页面的 data 里改起来痛得要命。4.3 订单提交与状态轮询下单页面把购物车里的 items 组装成后端需要的格式调创建订单接口。这里的难点是用户可能没有登录或者 openid 还没拿到。我在 uni.request 封装里加了一个中间态如果 openid 为空就先走登录流程登录成功再继续下单。订单列表页的状态展示我用了一个简单的映射const statusMap { 0: 待确认, 1: 已接单, 2: 制作中, 3: 已完成 }轮询接口用setInterval页面onHide时清掉定时器。这是个比较常见的坑有人忘了清除定时器页面切走之后还在发请求严重的话会被微信判定为接口异常。let timer null onShow(() { fetchOrders() timer setInterval(fetchOrders, 10000) }) onHide(() { if (timer) clearInterval(timer) })5. 小程序登录与联调手机号登录、导航栏适配和调试技巧小程序和网页开发最大的区别在于平台限制。这一章说的这些细节每一项都是我在实际开发里被折腾过的写出来帮你少走弯路。5.1 微信登录与获取手机号button 配合 getPhoneNumber点餐场景下用户最好能一键使用手机号登录这样商家可以联系用户用户下次进店也能同步历史订单。小程序最新的获取手机号方式是用户点击一个带open-typegetPhoneNumber的按钮微信返回一个 code后端拿 code 换取手机号。button open-typegetPhoneNumber getphonenumberonGetPhoneNumber 微信一键登录 /buttonasync function onGetPhoneNumber(e) { if (e.detail.code) { // 把 code 发到后端 const res await request(/api/login/phone, { method: POST, data: { code: e.detail.code } }) // 后端返回 openid 和手机号前端存起来 uni.setStorageSync(token, res.data.token) uni.setStorageSync(userInfo, res.data.userInfo) } }这里有个和过去不一样的地方以前是先调uni.login拿 code再用 code 换 openid手机号的获取是单独的流程。现在微信把手机号获取也改成了 code 换取模式而且这个 code 只能在后端调接口换具体换取的接口文档里有不在这展开。我的建议是下单前不需要强制登录。用户进店扫码是想快点吃完你弹个登录框让他输手机号他大概率就退出去了。我做的方案是用户点击结算时才提示登录并且用微信一键登录的方式点击一下基本没有感知。5.2 顶部导航栏高度与机型适配这是小程序开发里绕不开的适配问题。不同机型的顶部状态栏高度不一样iPhone 刘海屏和普通安卓的标题栏位置差很多。UniApp 里可以通过uni.getSystemInfoSync()拿到const systemInfo uni.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight // 自定义导航栏时菜单按钮的位置 const menuButtonInfo uni.getMenuButtonBoundingClientRect()顶部胶囊按钮就是右上角那个圆圈的位置在不同机型上也有差异。我做的是自定义导航栏把标题栏高度设为menuButtonInfo.top menuButtonInfo.height 4这样不管什么机型标题都和胶囊按钮对齐。如果不做自定义导航栏直接使用微信默认导航栏那就简单多了navigationBarTitleText配置一下就行。但默认导航栏左上角会有一个返回按钮如果你是首页要配置navigationStyle: custom来隐藏它不然会显示一个不能点击的返回箭头很难看。5.3 联调调试接口不通、日志不打印怎么排查前端和后端联调是问题最多的时候最典型的是「接口 404」和「接口 500」。我有一个固定排查顺序第一步确认接口地址对不对。用 H5 模式在浏览器里跑F12 打开 Network 看请求发了没有、URL 长什么样。很多时候 404 是因为路径不对或者少了一个斜杠。第二步确认跨域有没有配置。浏览器里跑的时候接口地址是http://localhost:5000页面是http://localhost:8080没有 CORS 配置必挂。第三步确认参数名对不对。前端传dishId后端读dish_id这种字段名不匹配的问题排查起来很费时间。我在实践中统一用 camelCase 风格前后端保持一致。关于日志遇到的怪问题是uniapp 不打印日志信息。在微信开发者工具里console 里什么都看不到但代码逻辑在执行。查了半天发现是开发者工具的「调试器」没有打开或者打开了但 Console 面板没选中。还有一个可能你的代码里用了console.log但项目配置了minified: true压缩的时候把 console 干掉了。建议开发期间保证minified: false。真机调试有个技巧可以用uni.showToast来验证逻辑有没有执行到因为真机上 console 日志不方便看。小程序端的日志系统本来就是个玄学多准备几个调试手段总能救命。6. 打包发布微信小程序上架、安卓应用市场与常见报错处理开发完成后打包和发布是另一个世界。这一章帮大家把 UniApp 打出微信小程序包、安卓包的过程和坑位讲清楚。6.1 manifest 配置项目从 H5 到小程序的关键manifest.json 是 UniApp 项目的全局配置文件。这里面的配置项很多最核心的就是「小程序配置」里的appid。首次打包一定要填对 appid不然微信开发者工具直接报错。// manifest.json 中的 mp-weixin 节点 { mp-weixin: { appid: 你的小程序AppID, setting: { urlCheck: false, es6: true, minified: true }, usingComponents: true, permission: {} } }urlCheck这个配置特别重要。开发的时候如果设置为 true微信开发者工具会校验你请求的接口域名是否在小程序后台配置过。本地调http://localhost也会被拦所以开发阶段要设为 false。但是发布前记得改回来并且在小程序后台把正式服务器的域名配好而且必须是 HTTPS。注意微信小程序对 request 合法域名有严格要求不能带端口、必须是 HTTPS。这正是很多人开发完了卡在发布那一步的原因——你的后端没有 HTTPS 域名。解决办法是申请一个域名配好 SSL 证书然后在小程序管理后台配置 request 合法域名。这一步绕不过去。6.2 小程序打包从 HBuilderX 到微信开发者工具打包流程不复杂但顺序错了会浪费时间。我的操作流程是在 HBuilderX 里点击「发行」→「小程序-微信」输入小程序 appid回车HBuilderX 会在dist/dev/mp-weixin目录下生成编译产物打开微信开发者工具导入这个目录在开发者工具里点击「上传」填版本号和备注上传之后去微信公众平台后台提交审核。审核一般 1-3 天点餐类的小程序通过率较高但要注意内容里不能有诱导分享、不能有虚拟支付之类的东西。如果你要上架安卓应用市场UniApp 也支持云打包。在 HBuilderX 里点「发行」→「原生App-云打包」选择安卓然后等着就行。云打包需要一个 DCloud 账号而且如果要用到定位、后台运行等特殊权限还需要在 manifest.json 里的「App模块配置」里勾选对应的模块。6.3 常见报错与处理办法打包过程中有几个高频报错我列一个表方便你自查报错信息原因解决方案appid is not configuredmanifest 里没填小程序 appid填入正确 appidrequest:fail url not in domain list请求域名没有配置后台配置合法域名开发时设 urlCheck falsetsconfig not found项目启用了 TS 但配置缺失检查 tsconfig.json 是否在根目录Failed to load pluginHBuilderX 插件未安装打开 HBuilderX 插件市场安装对应插件Cannot read property xxx of null数据格式和预期不一致先 console.log 打印接口返回看看结构和字段名关于 uni-app 在安卓应用市场单独说一下上架的应用市场和微信小程序完全两回事。应用市场要求你有软件著作权证书、隐私政策、ICP 备案这些不是打包好就能发。업체如果是给商家做的还是要做好心理准备这一步耗时比开发长。如果要正规运营提前安排软著申请。6.4 自定义分享与版本管理项目上线后还有一个高频需求客户想让用户分享小程序给朋友。默认分享是转当前页面但点餐场景下最好分享的是点餐首页。在页面上配置onShareAppMessage() { return { title: 快来这家店点餐, path: /pages/index/index } }如果想让分享文案更吸引人可以把菜品图片加到分享卡片里。微信分享卡片现在支持自定义 imageUrl但注意安卓端路径要求和实际资源对应否则分享出去图片是空的。版本管理我建议不管项目多小都用 Git。第一次提交前先把node_modules和dist加入.gitignore不然仓库体积爆炸。我见过有人把node_modules提交上去几万个文件同事拉代码拉到崩溃。最后的个人体会回头看这个项目最有价值的不是代码本身而是把「门店需求 → 技术方案 → 可上线效果」这条路走通了。独立开发者做小程序最大的敌人不是技术难度是需求边界不清楚。我的体会是第一版一定要砍掉所有可选功能只做点餐、购物车、订单、桌号这四个核心闭环等你跑通了商家自然会告诉你还需要什么到时候再迭代也不迟。技术栈上Vue 3 生态已经非常成熟UniApp 的跨端能力也够用Python 后端短期扛个小店流量毫无压力。如果你现在正在纠结要不要用这套组合做点餐小程序我的建议是直接开工别等。最后再分享一个实用的小技巧做后台菜品管理的时候不用单独开发管理端。直接用 Flask 加一个简单的/admin页面用表格把菜品列出来支持增删改查。或者接一个现成的后台模板半个小时搞定。这样你不用维护两套前端菜品上新、价格调整都在同一个后台完成后续改需求也方便。省下来的时间多测试几遍下单流程比什么都强。
返回列表