首页 / 资讯中心 / 文章详情

基于Vue+uniapp+Python的点餐系统设计与实现全流程解析

基于Vue+uniapp+Python的点餐系统设计与实现全流程解析 ★ FEATURED ARTICLE
做点餐系统这个需求我前前后后接触过不少从最开始商家只求“能扫码看菜单”到后来要求小程序端、商家后台、打印小票、营销活动一条龙项目边界不断在变。今天写这个“vueuniapp小程序Python美食菜品点餐订单系统”是把一套适合中小餐饮门店快速落地的点餐方案完整拆给大家看小程序端负责点餐、购物车、下单支付后端用Python提供接口服务中间用订单状态机把整个业务流程串起来。内容不多不少恰好覆盖从需求分析、数据库设计、接口开发到联调上线的全流程适合正在做毕业设计、个人作品集或者给餐饮客户做数字化升级的朋友参考。我尽量不说废话直接讲这套系统里真正有价值的东西表结构怎么设计才不容易返工购物车到底该放本地还是后端订单提交事务和扣库存为什么容易出事uniapp写小程序时那些官方文档不会明说的坑。这些事看起来零碎但都是实际项目里躲不开的坎。1. 整体设计思路与技术选型1.1 需求拆解一个最小可用点餐系统要包含什么先别急着写代码把需求捋清楚。一套完整的点餐订单系统拆到最小可用闭环至少要包含下面几条链路用户端浏览菜品分类和列表、加入购物车、确认订单、提交支付、查看订单状态和历史订单。商家端维护菜品分类与菜品信息、管理订单状态待支付、待接单、制作中、已完成等。底层支撑用户身份、菜品数据、购物车数据、订单数据、订单明细数据以及前后端分离的接口服务。大多数做这套系统的同学容易上来就怼着页面写写到一半发现购物车数据不知道放哪订单状态不知道谁来改。所以我做项目的第一件事永远是先画业务闭环用户从“进店-点菜-下单-支付-等餐”商家从“接单-制作-出餐”这一步走通了再往里面加会员、优惠券、打印机这些外围功能。用我之前给一家小吃店做的调研来说商家最痛的不是点菜而是“接单后不知道谁点了啥、在哪桌、要不要辣”。所以订单明细里必须保存菜品快照名称、价格、图片不能只存菜品ID否则以后菜品改价或删除历史订单就全花了。这些看起来是小细节但设计阶段没想清楚后期全是返工。1.2 选型理由为什么是vueuniapp为什么是Python前端选uniapp而不是原生小程序核心就一个原因多端复用。一套Vue语法的代码同时编译到微信小程序、H5、甚至App对没有原生开发团队的餐饮商家来说太省事了。你在微信里跑通了点餐后端接口不变改改配置就能出支付宝小程序或H5商城边际成本几乎为零。Vue3的组合式API写起来也顺手生态成熟社区里例子多遇到问题搜一圈基本都能找到答案。后端选Python而不是Java不是Java不行而是项目体量决定的。中小餐饮门店的并发量撑死几百人同时点餐这个压力下Python的开发效率优势比性能劣势重要得多。框架我推荐Flask而不是Django原因是Flask轻、灵活、没有强制约束做个接口服务非常合适Django自带Admin和ORM固然香但对这种规模的项目来说有点重而且魔法太多新手反而容易踩坑。数据库生产环境我建议MySQL开发和本地调试用SQLite也行反正对接了SQLAlchemy ORM之后切换成本很低。整套系统的数据量不大单表几千几万条记录而已关键是表关系设计清楚不要动不动就搞出四五张关联表把简单问题复杂化。1.3 总体架构与目录结构架构上就是标准的前后端分离uniapp小程序作为前端通过HTTPS调用Flask提供的RESTful API数据落在MySQL里。前端项目里按pages管理页面后端按业务模块划分蓝图Blueprint不把接口全堆在一个文件里。frontend/ # uniapp 项目根目录 ├── pages/ │ ├── index/index.vue # 首页分类菜品列表 │ ├── cart/cart.vue # 购物车 │ ├── order/confirm.vue # 确认订单 │ ├── order/list.vue # 订单列表 │ └── user/user.vue # 我的 ├── utils/request.js # 请求封装 └── stores/cart.js # 购物车状态管理 backend/ # Flask 项目根目录 ├── app.py # 应用入口 ├── models.py # ORM模型 ├── auth.py # JWT鉴权 ├── blueprints/ │ ├── user.py # 登录、用户信息 │ ├── dish.py # 菜品和分类 │ ├── cart.py # 购物车 │ └── order.py # 订单 └── requirements.txt这套结构的好处是边界清晰改首页不影响购物车逻辑改订单接口不碰菜品代码。多人协作时每个人负责一个蓝图就行自己单干时也好维护半年后回来看代码不用从头猜。2. 数据库设计与接口契约设计2.1 核心表结构先定表再写代码数据库是整个系统的地基字段少一个后面全要补。我这套项目沉淀下来的核心表有六张用户表、菜品分类表、菜品表、购物车表、订单表、订单明细表。下面直接把建表SQL放出来你可以直接抄作业。-- 用户表 CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid唯一标识, nickname varchar(64) DEFAULT COMMENT 昵称, avatar varchar(255) DEFAULT COMMENT 头像, phone varchar(20) DEFAULT COMMENT 手机号, role tinyint NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜品分类表 CREATE TABLE category ( id int NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL COMMENT 分类名如热菜、凉菜, sort int NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜品表 CREATE TABLE dish ( id int NOT NULL AUTO_INCREMENT, category_id int NOT NULL COMMENT 所属分类, name varchar(64) NOT NULL COMMENT 菜品名, description varchar(255) DEFAULT COMMENT 描述/口味说明, image varchar(255) DEFAULT COMMENT 图片URL, price decimal(10,2) NOT NULL COMMENT 单价单位元, stock int NOT NULL DEFAULT 0 COMMENT 库存0表示售罄, sales int NOT NULL DEFAULT 0 COMMENT 销量用于排序展示, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表 CREATE TABLE cart ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, dish_id int NOT NULL, quantity int NOT NULL DEFAULT 1 COMMENT 数量, selected tinyint NOT NULL DEFAULT 1 COMMENT 是否勾选结算, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_dish (user_id, dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号业务唯一, user_id int NOT NULL, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付/待接单 2制作中 3待取餐 4已完成 5已取消, remark varchar(255) DEFAULT COMMENT 用户备注, address varchar(255) DEFAULT COMMENT 取餐方式或配送地址, pay_time datetime DEFAULT NULL COMMENT 支付时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL, dish_id int NOT NULL, dish_name varchar(64) NOT NULL COMMENT 菜品快照名称, dish_image varchar(255) DEFAULT COMMENT 菜品快照图片, price decimal(10,2) NOT NULL COMMENT 下单时的单价, quantity int NOT NULL COMMENT 数量, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计细节值得说。订单表和订单明细表我故意拆成两张因为一张订单有多个菜品明细必须单独存否则字段没法设计。订文明细里存dish_name和dish_image而不是关联查询菜品表这叫“历史快照”因为菜品以后可能改价、改名甚至删除但用户的订单记录必须保持原样。购物车表用(user_id, dish_id)做唯一键同一个用户同一道菜只存一条记录数量累加避免购物车出现两行一样的菜。2.2 订单状态机的设计别让状态满天飞订单状态是整个系统里最容易出bug的地方。我见过不少项目没定义清楚状态前端传个数字、后端再改一个数字两边对不上订单就卡死了。我的方案是把状态模型定义成五档0待支付下单后创建给用户15分钟支付时间。1已支付/待接单支付成功等商家确认。2制作中商家接单开始做。3待取餐/待配送餐做好了。4已完成用户取餐或确认收货。5已取消用户主动取消或超时取消。状态流转只允许相邻推进禁止跳状态比如“待支付”不能直接变成“制作中”必须先支付成功。这个约束在后端接口里做判断不要指望前端自觉。前端每次刷新订单列表时拉最新状态商家端操作后同步状态用户端才能看到实时进展。2.3 接口响应格式与鉴权方案前后端联调最怕什么最怕接口返回格式不统一。有的接口返回数组有的返回对象有的报错只给个英文文本前端解析代码写八遍。我统一约定一个响应格式所有接口都必须遵守{ code: 0, message: ok, data: {} }code为0表示成功非0表示业务错误message给出用户可读的提示data放真正的数据。前端请求封装里统一拦截code非0就直接toast前端代码不用每个接口都写错误处理。接口清单给一个汇总表开发时照着表逐项实现就行模块方法路径说明用户POST/api/user/logincode换openid返回token分类GET/api/category/list获取分类列表菜品GET/api/dish/list支持分类筛选、分页购物车GET/api/cart/list查询我的购物车购物车POST/api/cart/add加入购物车购物车POST/api/cart/update修改数量/勾选状态购物车POST/api/cart/delete删除购物车项订单POST/api/order/submit提交订单订单GET/api/order/detail订单详情订单GET/api/order/list订单列表按状态筛选订单POST/api/order/pay模拟支付鉴权我用JWT登录成功后后端返回一个token小程序端存到storage里之后每个请求在header里带Authorization: Bearer token。后端用一个装饰器校验身份用户相关接口必须登录菜品分类这类公开接口则放行。后面我专门开一节讲登录和鉴权这里是先把契约定清楚前端后端照着同一份文档开发效率高很多。3. 小程序端核心功能落地3.1 初始化manifest配置与请求封装uniapp项目的第一步不是写页面是确认manifest.json的配置。微信小程序的appid、权限配置、基础库版本都要在这里面搞定。很多新手没配appid就急着跑调试结果真机预览黑屏类型的报错跑到怀疑人生。请求封装是前端的基建统一处理baseURL、token注入、错误处理和401跳转。baseURL我习惯按环境区分开发环境用http://127.0.0.1:5000/api生产环境换成HTTPS域名。uniapp里可以通过process.env.NODE_ENV判断环境也可以用uni.getSystemInfoSync()的platform区分平台总之别把URL写死在每个页面里。// utils/request.js const BASE_URL https://your-domain.com/api; export function request(options) { return new Promise((resolve, reject) { const token uni.getStorageSync(token); uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.statusCode 401) { uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(new Error(未登录)); return; } if (res.data.code ! 0) { uni.showToast({ title: res.data.message, icon: none }); reject(new Error(res.data.message)); return; } resolve(res.data.data); }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }这里有几个经验点。401跳转登录后最好加一个开关防止并发请求时连续跳转多次后面踩坑实录里我会细说。第二个是uni.request的fail回调很多同学不处理网络错误用户没网时界面就卡住了必须统一给个toast提示。3.2 首页菜品展示与分类筛选首页是点餐系统的门面做得好的标准是用户2秒内能找到想吃的菜。我采用“左侧分类tab 右侧菜品列表”的布局左边显示分类名右边滚动显示该分类下的菜品。这个布局在原生小程序里很常见uniapp里用scroll-view实现也很方便。菜品列表接口我设计成按分类分页加载每页10条。这样首屏只请求第一页触底再加载下一页避免一次拉几百条数据把用户流量烧光。页面逻辑用Vue3组合式API组织状态管理清晰代码可读性高!-- pages/index/index.vue -- script setup import { ref, onMounted } from vue; import { request } from /utils/request; const categories ref([]); const currentCategory ref(null); const dishList ref([]); const page ref(1); const loading ref(false); const finished ref(false); async function loadCategories() { const data await request({ url: /category/list }); categories.value data; if (data.length 0) { currentCategory.value data[0].id; loadDishes(true); } } async function loadDishes(reset false) { if (loading.value) return; if (reset) { page.value 1; finished.value false; dishList.value []; } loading.value true; const data await request({ url: /dish/list, data: { category_id: currentCategory.value, page: page.value, size: 10 } }); dishList.value.push(...data.list); page.value 1; if (data.list.length 10) { finished.value true; } loading.value false; } function onSwitchCategory(id) { currentCategory.value id; loadDishes(true); } onMounted(loadCategories); /script菜品卡片上展示图片、名称、月售量和价格。价格必须用分或者保留两位小数的字符串处理避免浮点计算精度问题比如0.10.2不等于0.3这种经典事故。加购按钮放在卡片右下角点击后触发全局购物车状态更新。3.3 购物车状态管理本地还是后端这是个问题购物车有两种实现思路。一种是把购物车数据存在小程序本地storage里好处是响应快、不占后端资源坏处是换设备就丢另一种是存后端数据库好处是跨端同步坏处是每次加购都要请求接口。我的建议是做作品集或练习项目用后端存因为订单流程需要服务端参与购物车也放后端整个数据链路最一致做演示Demo可以用本地存上线简单。本项目我按后端存储来设计因为后面订单提交时要校验菜品状态和库存前端购物车数据并不一定可信统一走后端更安全。购物车页面用状态管理工具uniapp内置vuex或pinia存放数据结构同时和后端同步。核心操作包括加购、减少数量、勾选结算、删除。每次操作调用后端接口成功后更新本地状态。加购时如果购物车里已有这道菜数量加1否则新增一行。3.4 订单确认与支付从购物车到订单的最后一公里订单确认页要做的事情很明确展示用户勾选的菜品清单、计算总价、让用户填备注和取餐方式、提交订单。提交时前端把购物车里勾选项的信息传给后端后端在事务里创建订单主表和订单明细。这里必须强调一遍金额不能信任前端传值后端要根据菜品表的实际价格重新计算总价否则懂点接口知识的人就能改价格下单了。支付环节我使用了模拟支付和真实微信支付两套方案通过配置开关切换。模拟支付就是点击“确认支付”后前端调一次/api/order/pay接口后端直接把订单状态从0改成1并记录支付时间适合开发调试和演示。真实支付则要对接微信支付的下单和支付回调需申请商户号个人开发阶段一般用不上。// 提交换单核心代码 async function submitOrder() { const cartItems cartStore.selectedItems; if (cartItems.length 0) { uni.showToast({ title: 请选择菜品, icon: none }); return; } const res await request({ url: /order/submit, method: POST, data: { items: cartItems.map(item ({ dish_id: item.dish_id, quantity: item.quantity })), remark: remark.value, address: address.value } }); // 下单成功跳转支付或订单详情 uni.redirectTo({ url: /pages/order/detail?id${res.order_id} }); }提交订单和支付是两个步骤而不是一步搞定。先创建待支付订单再调支付这样用户中途退出也能回来继续付不会出现“钱没付但订单没建”的尴尬。这个设计在银行、电商系统里叫“分离式订单”点餐系统也必须这样。4. Python后端API开发与联调4.1 Flask项目结构与启动后端我用Flask SQLAlchemy PyJWT的组合requirements.txt如下flask2.3.3 flask-sqlalchemy3.1.1 flask-cors4.0.0 pyjwt2.8.0 requests2.31.0入口文件app.py负责初始化应用、注册蓝图、开启跨域from flask import Flask from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() def create_app(): app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost/order_db app.config[JWT_SECRET_KEY] your-secret-key CORS(app) db.init_app(app) from blueprints.user import bp as user_bp from blueprints.dish import bp as dish_bp from blueprints.cart import bp as cart_bp from blueprints.order import bp as order_bp app.register_blueprint(user_bp, url_prefix/api/user) app.register_blueprint(dish_bp, url_prefix/api) app.register_blueprint(cart_bp, url_prefix/api/cart) app.register_blueprint(order_bp, url_prefix/api/order) with app.app_context(): db.create_all() return app if __name__ __main__: app create_app() app.run(host0.0.0.0, port5000, debugTrue)数据库连接串我这里写的MySQL端口和密码你自己改。db.create_all()在开发和练习阶段够用生产环境建议加个迁移工具管理表结构变更否则后期加字段会非常痛苦。4.2 用户登录与JWT鉴权微信小程序登录的标准流程分两步小程序端调uni.login()拿到临时code把code传给后端后端用code调用微信接口换取openid。换取openid要求你有小程序的appid和secret这两个配置千万别写在前端代码里必须放后端环境变量。# blueprints/user.py import requests import jwt import time from flask import Blueprint, request, jsonify from models import User, db bp Blueprint(user, __name__) bp.route(/login, methods[POST]) def login(): data request.get_json() code data.get(code) if not code: return jsonify({code: 1, message: 缺少code}) appid your-appid secret your-secret resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{appid: appid, secret: secret, js_code: code, grant_type: authorization_code} ) wx_data resp.json() openid wx_data.get(openid) if not openid: return jsonify({code: 1, message: 登录失败}) user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid, nickname微信用户) db.session.add(user) db.session.commit() token jwt.encode( {user_id: user.id, exp: int(time.time()) 7 * 24 * 3600}, app.config[JWT_SECRET_KEY], algorithmHS256 ) return jsonify({code: 0, message: ok, data: {token: token, user_info: {nickname: user.nickname}}})为了开发调试方便我额外加了一个测试登录接口传个userId直接返回token这样前端跑通业务流程时不用每次都依赖微信登录联调效率翻倍。这个接口上线前一定要关掉否则等于后端裸奔。鉴权装饰器是所有需要登录接口的守卫# auth.py from functools import wraps import jwt from flask import request, jsonify, current_app from models import User def login_required(f): wraps(f) def wrapper(*args, **kwargs): auth_header request.headers.get(Authorization, ) if not auth_header.startswith(Bearer ): return jsonify({code: 401, message: 未登录}) token auth_header[7:] try: payload jwt.decode(token, current_app.config[JWT_SECRET_KEY], algorithms[HS256]) user User.query.get(payload[user_id]) if not user: return jsonify({code: 401, message: 用户不存在}) request.current_user user except jwt.ExpiredSignatureError: return jsonify({code: 401, message: 登录已过期}) except jwt.InvalidTokenError: return jsonify({code: 401, message: 无效token}) return f(*args, **kwargs) return wrapper把用户对象挂到request.current_user上后面每个接口只要在视图函数里直接用它就能拿到当前用户省去重复解析token。4.3 菜品、购物车、订单核心接口实现菜品列表接口要支持分类筛选和分页一个常规的list查询# blueprints/dish.py bp.route(/dish/list, methods[GET]) def dish_list(): category_id request.args.get(category_id, typeint) page request.args.get(page, default1, typeint) size request.args.get(size, default10, typeint) query Dish.query.filter_by(status1) if category_id: query query.filter_by(category_idcategory_id) total query.count() dishes query.order_by(Dish.sales.desc()).offset((page - 1) * size).limit(size).all() data { total: total, list: [{ id: d.id, name: d.name, price: str(d.price), # 转字符串避免浮点问题 image: d.image, sales: d.sales, stock: d.stock, description: d.description } for d in dishes] } return jsonify({code: 0, message: ok, data: data})购物车接口相对直接增删改查都要带上login_required装饰器先拿到当前用户再操作。订单提交接口是整个后端的核心必须用事务处理。一个订单包含创建主表、创建明细、扣减库存、清空购物车四个操作任何一个失败都要全部回滚不能出现“订单建了但库存没扣”的脏数据。库存扣减我用了带条件的UPDATE防止超卖# blueprints/order.py from sqlalchemy import update from datetime import datetime import random, string bp.route(/submit, methods[POST]) login_required def submit_order(): user request.current_user data request.get_json() items data.get(items, []) if not items: return jsonify({code: 1, message: 菜品不能为空}) try: db.session.begin() # 1. 计算总价并校验库存 total_amount 0 order_items [] for item in items: dish Dish.query.filter_by(iditem[dish_id], status1).with_for_update().first() if not dish or dish.stock item[quantity]: db.session.rollback() return jsonify({code: 1, message: f菜品{dish.name if dish else }库存不足}) # 2. 扣减库存 dish.stock - item[quantity] dish.sales item[quantity] subtotal dish.price * item[quantity] total_amount subtotal order_items.append({ dish_id: dish.id, dish_name: dish.name, dish_image: dish.image, price: dish.price, quantity: item[quantity] }) # 3. 生成订单号 order_no datetime.now().strftime(%Y%m%d%H%M%S) .join(random.choices(string.ascii_uppercase string.digits, k6)) # 4. 创建订单 order Order( order_noorder_no, user_iduser.id, total_amounttotal_amount, remarkdata.get(remark, ), addressdata.get(address, ), status0 ) db.session.add(order) db.session.flush() # 获取 order.id # 5. 创建订单明细 for oi in order_items: db.session.add(OrderItem(order_idorder.id, **oi)) # 6. 清空购物车 Cart.query.filter_by(user_iduser.id).delete() db.session.commit() return jsonify({code: 0, message: ok, data: {order_id: order.id, order_no: order.order_no}}) except Exception as e: db.session.rollback() return jsonify({code: 1, message: 下单失败})这里的with_for_update()是行级锁同一道菜同时被多人抢购时串行化防止库存扣成负数。事务必须用db.session.begin()显式开启让提交和回滚的边界清晰。订单号用时间戳加6位随机字符串在单机环境基本不会重复还能保证长度统一。4.4 商家端接口的思路完整点餐系统肯定需要商家端。这里不展开全部代码讲两个思路。商家端接口和用户端接口分离但共用同一套数据库。用role字段区分身份用户表里role1是管理员只有管理员的token能访问商家接口。菜品管理、订单状态流转这些接口都加一层admin_required装饰器在login_required基础上再判断user.role ! 1就拒绝。订单状态流转接口的设计也很简单比如商家接单bp.route(/status, methods[POST]) login_required admin_required def update_status(): data request.get_json() order_id data.get(order_id) new_status data.get(status) order Order.query.get(order_id) if not order: return jsonify({code: 1, message: 订单不存在}) # 状态流转校验只允许从pengding到making这类合法迁移 if not is_valid_transition(order.status, new_status): return jsonify({code: 1, message: 非法的状态流转}) order.status new_status if new_status 1: order.pay_time datetime.now() db.session.commit() return jsonify({code: 0, message: ok})is_valid_transition的实现就是把2.2节定义的状态机写成硬编码校验这是系统稳定性的真正保障。5. 常见问题与避坑实录5.1 uniapp多端兼容一套代码不是万能药uniapp打的口号是“一套代码多端运行”但实际跑起来你会发现不同平台还是有细节差异。第一个坑是条件编译有些API只存在于微信小程序比如uni.getUserProfile在H5端用不了需要页面启动时判断平台或者用条件编译注释掉不兼容的代码。第二个坑是单位uniapp里推荐用rpx做响应式单位但在小程序端和H5端的渲染效果偶尔会有1px的偏差需要真机调试微调。第三个坑是图片小程序端的图片必须用HTTPS地址本地调试也要把图片放到服务器或用base64否则无法显示。我最近帮A同学调他的点餐项目首页菜品图在开发者工具里显示正常真机上一片空白排查了一圈发现是图片域名没加入小程序后台的“downloadFile合法域名”。这种事文档里写得明白但很多新手不看踩一次坑就记住了。5.2 token过期与401竞态处理前面提过401并发跳转的问题这里展开细讲。用户A进入首页首页同时发起三个请求三个请求都返回401前端拦截器就执行三次navigateTo。结果不是跳三次登录页而是页面栈里压了三个登录页用户返回时直接卡死。我的解法是设置一个isRedirecting标志位let isRedirecting false; // 在401处理逻辑中 if (!isRedirecting) { isRedirecting true; uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); setTimeout(() { isRedirecting false; }, 3000); }用时间窗口比如3秒把重复跳转拦掉等第一个登录页跳转完后面的401请求自然不会再触发重复跳转。真正生产环境还要上refresh token机制但小系统用这个标志位方案已经够稳。5.3 订单与库存的并发超卖是怎么发生的超卖是库存系统最经典的事故。我见过一个项目扣库存的代码是“先select剩余库存判断大于0再update库存减1”这代码在低并发下没问题但在秒杀场景下两个请求同时读到库存1都判断可以卖都执行update库存就变成-1了。这就是典型的“竞态条件”。解决超卖有三个层次。最基础的是在扣库存的SQL里加上条件UPDATE dish SET stock stock - 1 WHERE id ? AND stock 0让数据库自己保证原子性进阶一点像我上面写的用with_for_update()对菜品行加锁再高级用Redis分布式锁或乐观锁版本号但点餐系统并发没到这个量级。我推荐的方案是“条件更新事务”简单可靠也不用引入额外组件。5.4 前端性能与体验优化点餐系统的前端性能优化核心是首屏加载速度。图片是这个系统的性能大头一张没压缩的菜品图可能2MB首页拉10个菜就是20MB流量用户早跑了。我一般用小程序端的image组件自带懒加载模式再加上后端做图片压缩一张菜品图压到100KB以内。列表滚动性能方面长列表用scroll-view配合分页加载不要一上来就把全部分类的菜全拉下来。菜品分类和列表的加载交互也要设计好切换分类时清掉旧列表、展示骨架屏或loading动画避免用户以为卡了。这些优化一次做完后面用户体验报告好看很多。最后再分享一个实际小技巧这套系统做到后面我最大的体会是先把最核心的“点菜-下单-支付-查单”闭环跑通再考虑加花活。很多开发者一上来就想着做优惠券、会员积分、营销活动结果核心功能还没稳定需求越滚越大项目永远交付不了。我自己做项目是先列表页点餐——购物车——提交订单——模拟支付整条链路通了再逐步加上商家端、打印小票、数据分析这些模块。如果你打算在这个基础上扩展我有两个很实用的方向。一个是给商家端加个简单的Web管理页面用Vue3全家桶搭一个菜品管理后台前端代码和小程序端复用很大一部分接口成本很低另一个是接入实际的微信支付把模拟支付的开关换成真实下单对接下支付回调接口这算是商业化落地前的临门一脚。把这两步做完这套系统就能从演示Demo变成真正能上线的商用产品。
阅读完成 · 觉得有帮助?
咨询建站