尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flask微信小程序订餐系统:后端架构与API设计实战

Flask微信小程序订餐系统:后端架构与API设计实战 简介一套面向餐饮场景的微信小程序订餐系统完整源码后端基于Python Flask框架前端为微信小程序原生实现适合Python学习者、中小型餐饮创业者或需要课程设计与毕业设计范式的开发者参考。资源包共含692个文件涵盖Python后端逻辑、Flask模板与路由、小程序页面脚本、样式表、配置文件及大量界面预览图压缩包仅7.82MB便于快速下载与部署。当前已有3253人学习或下载结合源码目录可直观理解从用户点餐、订单管理到后台数据交互的完整闭环。通过该源码可掌握Flask API设计与小程序wx.request通信方式参考其中JSON配置和WXML/WXSS结构快速搭建自己的订餐应用也可在现有基础上扩展支付、菜品分类或商家管理模块节省从零搭建的重复工作。1. 微信订餐系统不是只有小程序Flask 这边的活更重看到pythonflask微信小程序订餐系统源码.zip这个名字多数人会以为核心是app.js和wxml真正打开源码才发现静态资源里躺着bootstrap.min.css、ueditor.css、font-awesome.css、video-js.css说明这个项目至少有两端给顾客用的小程序端和给店家用的 Web 管理后台。Flask 要同时承担小程序 API、后台渲染、文件上传、富文本编辑几件事很多从零开始写点餐系统的人第一版往往只做了接口层等到要接后台就发现代码没法复用。这套源码正好展示了把两套前端压在同一个 Flask 应用下的组织方式适合正在犹豫「小程序端和后端怎么拆」的 Python 开发也适合想拿现成后台快速改一版订餐业务的从业者。下面我按自己拆这套代码的顺序从数据模型到接口验证把可复现的部分梳一遍。2. Flask 端的目录结构与数据库建模方案2.1 项目拆分小程序 API 和管理后台的边界先看 Flask 端目录这个结构决定了后面扩展订单、优惠券时会不会乱ordering/ ├── app/ │ ├── __init__.py # create_app 工厂 │ ├── models/ │ │ ├── __init__.py │ │ ├── user.py │ │ ├── dish.py │ │ ├── order.py │ ├── api/ │ │ ├── __init__.py # Blueprint: /api │ │ ├── auth.py │ │ ├── menu.py │ │ └── order.py │ ├── admin/ │ │ ├── __init__.py # Blueprint: /admin │ │ ├── views.py │ │ └── forms.py │ ├── static/ │ │ ├── bootstrap.min.css │ │ ├── ueditor.min.js │ │ └── ... │ ├── templates/ │ │ ├── admin/ │ │ └── ... ├── manage.py └── requirements.txtapp/api下所有路由都挂在/api前缀返回 JSONapp/admin走服务端渲染使用 Jinja2 模板。小程序端不需要关心管理后台的页面逻辑只会请求/api/menu、/api/order这类接口。如果以后要拆成微服务只需要把api里的视图函数搬到独立服务models 层几乎可以原样带走。这里有一个容易犯错的设计不要把小程序 API 和管理后台写到同一个 views.py 里。这套源码把俩 Blueprint 分开登录鉴权逻辑也按来源区分。后台用session CSRF小程序用token两套安全模型混在一起会互相污染后面接支付回调时尤其麻烦。2.2 订单核心表用户、菜单、订单、订单项的设计点餐系统的数据表比普通商城简单但订单和订单项必须分表因为一个订单对应多个菜而且菜品价格可能调整。这套源码的模型定义可以直接抄from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) openid db.Column(db.String(64), uniqueTrue, nullableFalse, indexTrue) nickname db.Column(db.String(64)) mobile db.Column(db.String(20)) created_at db.Column(db.DateTime, defaultdatetime.now) class Dish(db.Model): __tablename__ dish id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse) price db.Column(db.Numeric(10, 2), nullableFalse) image_url db.Column(db.String(256)) category_id db.Column(db.Integer, db.ForeignKey(category.id)) status db.Column(db.SmallInteger, default1) # 0下架 1上架 class Order(db.Model): __tablename__ order id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse) user_id db.Column(db.Integer, db.ForeignKey(user.id)) total_price db.Column(db.Numeric(10, 2), nullableFalse) status db.Column(db.SmallInteger, default0) # 0待支付 1已支付 2已完成 remark db.Column(db.String(128)) created_at db.Column(db.DateTime, defaultdatetime.now) class OrderItem(db.Model): __tablename__ order_item id db.Column(db.Integer, primary_keyTrue) order_id db.Column(db.Integer, db.ForeignKey(order.id)) dish_id db.Column(db.Integer, db.ForeignKey(dish.id)) dish_name db.Column(db.String(64)) price db.Column(db.Numeric(10, 2)) quantity db.Column(db.Integer, default1)字段不复杂但要注意两个点OrderItem里冗余了dish_name和price这是故意做的。菜品改名或调价时历史订单里的快照不能跟着变否则对账时金额对不上。User表的openid直接建唯一索引微信登录返回的openid是用户在小程序维度的唯一标识后续查询和去重都靠它。如果要做多门店还得加shop_id字段这套源码没有属于单店版本。建表命令不多说flask db init那套容易绕晕我建议直接flask shell from app import db db.create_all()开发环境这样跑没问题生产环境再用 Alembic 迁移。表建完后用sqlite3 ordering.db .tables看一眼实际建出来的表名确认没有因为__tablename__写错导致映射到奇怪的名字。2.3 购物车不该建表append 到缓存才是常态看到很多半路出家的点餐系统会把购物车也做成一张表这个设计我不推荐。购物车是典型的临时态用户可能加加减减未登录时根本不知道是谁的购物车。这套源码的思路是把购物车放到 Redis 或者写到小程序本地存储里后端只在下单时接收一个完整的菜品列表。如果后端需要临时保存购物车可以参考这种缓存结构import redis import json r redis.Redis(host127.0.0.1, port6379, db2) def save_cart(openid, cart_items): # cart_items: [{dish_id:1, quantity:2}, {dish_id:3, quantity:1}] key fcart:{openid} r.setex(key, 86400, json.dumps(cart_items)) def get_cart(openid): key fcart:{openid} data r.get(key) return json.loads(data) if data else []setex设置了 24 小时过期避免用户丢在餐桌上的一次性购物车占用 Redis 内存。小程序端每次改动购物车都通过接口同步到后端下单时以后端这份为准不能信小程序传过来的total_price。价格必须由后端根据dish_id重新计算一遍。3. 小程序登录鉴权与点餐主流程接口3.1 code2session 换 openid 的坑小程序端点「微信授权登录」实际拿到的是wx.login产生的code后端拿这个code去微信接口换session_key和openid。这套源码里auth.py的核心就这一段但坑不少import requests import json from flask import Blueprint, request, jsonify auth_bp Blueprint(auth, __name__) auth_bp.route(/login, methods[POST]) def login(): code request.json.get(code) appid your_appid # 小程序 AppID secret your_appsecret # 小程序 AppSecret resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: appid, secret: secret, js_code: code, grant_type: authorization_code }, timeout5 ) data resp.json() if openid not in data: return jsonify({code: 400, msg: 登录失败}), 400 user User.query.filter_by(openiddata[openid]).first() if not user: user User(openiddata[openid]) db.session.add(user) db.session.commit() token generate_token(user.id) # 项目里常见做法是用 itsdangerous return jsonify({code: 0, data: {token: token, user_id: user.id}})这里最容易出问题的是requests.get没有设置timeout微信接口如果抖动请求会卡住几十秒前端表现就是永远转圈。另一个坑是appid和secret写在代码里这套源码如果直接拿去改记得先从环境变量读取import os appid os.environ.get(WX_APPID, your_appid) secret os.environ.get(WX_APPSECRET, your_appsecret)code是一次性的第二次用同一个code请求会报invalid code。排查时可以打印微信返回的errcode最常见的40163就是重复使用code。3.2 菜单列表与购物车下单的 Flask 路由小程序首页要展示菜品分类和列表后端接口一般这样写from flask import jsonify, request menu_bp.route(/menu, methods[GET]) def get_menu(): category_id request.args.get(category_id, typeint) query Dish.query.filter_by(status1) if category_id: query query.filter_by(category_idcategory_id) dishes query.order_by(Dish.id.desc()).all() data [ { id: d.id, name: d.name, price: str(d.price), image_url: d.image_url, category_id: d.category_id } for d in dishes ] return jsonify({code: 0, data: data})注意price转成了str因为Numeric类型在 JSON 序列化时可能变成 Decimal 对象直接jsonify会报错。request.args.get(category_id, typeint)是 Flask 自带类型转换传了非数字会返回 400比自己在视图里 try/except 干净。下单接口是这个系统的核心order_bp.route(/create, methods[POST]) def create_order(): token request.headers.get(Authorization) user_id verify_token(token) # 解析 token 得到 user_id if not user_id: return jsonify({code: 401, msg: 未登录}), 401 items request.json.get(items) # [{dish_id:1, quantity:2}] if not items or not isinstance(items, list): return jsonify({code: 400, msg: items 参数错误}), 400 total_price 0 order_items [] for item in items: dish Dish.query.filter_by(iditem[dish_id], status1).first() if not dish: return jsonify({code: 400, msg: f菜品 {item[dish_id]} 不存在}), 400 subtotal dish.price * int(item[quantity]) total_price subtotal order_items.append(OrderItem( dish_iddish.id, dish_namedish.name, pricedish.price, quantityitem[quantity] )) order Order( order_nogenerate_order_no(), user_iduser_id, total_pricetotal_price, status0 ) db.session.add(order) db.session.flush() # 拿到 order.id for oi in order_items: oi.order_id order.id db.session.add(oi) db.session.commit() return jsonify({code: 0, data: {order_id: order.id, order_no: order.order_no}})db.session.flush()是个关键动作它把order的主键立即写回 Python 对象不需要先 commit 再查询。整套逻辑放在一个事务里中间任何一个菜品不存在直接 return 400之前累积的db.session变更会自动回滚不会留下残缺订单。3.3 用 CHARLES 抓包核对小程序请求参数小程序点餐看起来简单联调时问题基本出在请求参数对不上。调试方法不是在小程序里打日志而是用 Charles 抓包看实际发出的 HTTP 请求。电脑端 Charles 设置好 SSL Proxying 后手机上信任证书微信开发者工具里把「不校验合法域名」关掉就能看到https://api.example.com/api/create发出的完整请求体。这套源码里最容易抓出来的问题有两个content-type是application/json还是application/x-www-form-urlencodedFlask 的request.json只认前者。小程序wx.request的header如果不显式设置默认是application/json但如果你用了uni-app的uni.request有些版本要手动写header: {content-type: application/json}。后端接口返回的price是字符串18.00小程序端直接用parseFloat计算可能得到18和total_price比较时要用而不是否则永远不相等。替换成uni-app开发微信小程序也很常见uni.request的url只写路径域名前缀放到manifest.json的h5配置或小程序配置里。抓包时看到请求http://127.0.0.1:5000没问题但真机预览时127.0.0.1指向手机自己必须改成电脑局域网的 IP。4. 管理后台Bootstrap UEditor Video.js 的组合拳4.1 后台模板复用要改的静态资源路径这套源码的静态目录里出现了bootstrap.min.css、ueditor.css、video-js.css、font-awesome.css说明管理后台是一个带富文本编辑、图片轮播、视频展示的完整界面。直接把静态文件复制到 Flask 的static目录后十个有八个会遇到样式不加载的问题。Flask 默认静态文件路径是/static模板里的引用应该写成link relstylesheet href{{ url_for(static, filenamebootstrap.min.css) }} script src{{ url_for(static, filenameueditor.min.js) }}/script不要直接写href/static/bootstrap.min.css虽然开发时能跑但以后如果加了蓝图的子域名或 CDN 前缀url_for会自动带上SEND_FILE_MAX_AGE_DEFAULT等配置改起来少动一个文件。UEditor 的配置它会自己加载一堆image.css、video-js.css如果你发现编辑器按钮能出但排版乱检查ueditor.config.js里的window.UEDITOR_HOME_URL是否指向了正确的 Flask 静态路径window.UEDITOR_HOME_URL /static/ueditor/;4.2 UEditor 富文本与图片上传的安全处理UEditor 上传图片接口可以直接用 Flask 接收文件但网上能找到不少「上传 getshell」的案例问题全出在没限制文件类型和路径。安全做法是上传目录不能放在应用根目录下而且文件名要重写import os import uuid from flask import request, jsonify from werkzeug.utils import secure_filename admin_bp.route(/upload, methods[POST]) def upload_image(): f request.files.get(upfile) # UEditor 默认文件字段名 if not f: return jsonify({state: FAIL, msg: 未获取到文件}) ext os.path.splitext(f.filename)[1].lower() allowed {.jpg, .jpeg, .png, .gif, .webp} if ext not in allowed: return jsonify({state: FAIL, msg: 文件类型不允许}) filename f{uuid.uuid4().hex}{ext} save_path os.path.join(app/static/uploads, filename) f.save(save_path) return jsonify({state: SUCCESS, url: f/static/uploads/{filename}})uuid.uuid4().hex生成 32 位随机文件名避免中文名转码问题和路径穿越。secure_filename只保留 ASCII 字符对中文文件名最好自己用uuid重写否则secure_filename可能把中文全删掉导致文件扩展名丢失。4.3 使用 Flask-WTF 做表单校验和 CSRF后台管理页每次新增菜或改价格都涉及表单提交。直接用request.form.get()不是不行但字段多的时候校验逻辑会挤占视图代码。Flask-WTF 的FlaskForm可以做一个干净的写法from flask_wtf import FlaskForm from wtforms import StringField, DecimalField, IntegerField from wtforms.validators import DataRequired, NumberRange class DishForm(FlaskForm): name StringField(菜名, validators[DataRequired()]) price DecimalField(价格, validators[DataRequired(), NumberRange(min0.01)]) category_id IntegerField(分类, validators[DataRequired()])视图里调用form.validate_on_submit()如果返回 False模板里会把错误渲染出来。这个库另一个价值是默认开启 CSRF 防护render_template里的表单需要加form.csrf_token。小程序端不需要 CSRF因为它的token已经放在请求头里但后台表单如果忘加这行以后被脚本刷接口连周界都没有。5. 部署前要过一遍的验收命令与细节优化订餐系统要真上微信小程序后端必须跑 HTTPS而且域名要在小程序后台配置为 request 合法域名。小程序端拿用户openid的接口微信要求appid和secret必须来自同一个小程序账号测试的时候用测试号拿到的openid和生产号不通用这点最容易在换账号后突然无法登录。生产环境别用flask run跑那是开发服务器单进程扛不住多个人同时点单。常见做法是gunicorn geventpip install gunicorn gevent gunicorn -w 4 -k gevent -b 0.0.0.0:8000 manage:app --timeout 30-k gevent让网络请求变成协程并发点餐这种短请求接口很合适。--timeout 30防止某个微信接口响应慢时 worker 被强制杀掉。启动后用curl -s -X POST https://你的域名/api/login -H Content-Type: application/json -d {code:test}验证接口是否能按预期返回400或无openid的错误。不要只在浏览器里打开curl看不到渲染层能直接暴露路由和 JSON 问题。小程序顶部导航栏高度不一样这是真机适配最常见的坑。拿到用户胶囊按钮位置wx.getMenuButtonBoundingClientRect()后把返回的top和height存在全局变量里计算占位视图高度不要把写死的64px传下去。抓包只是第一次联调用后续接口改动频繁时直接在 Python 代码里临时加一行print(request.json)再tail -f日志定位问题比看半个小时的 Charles 原始请求更直接。最后确认requirements.txt里锁住的版本Flask2.2.5、Flask-SQLAlchemy3.0.5这类避免下次部署时依赖升级把Numeric的序列化行为改掉。本文还有配套的精品资源点击获取
返回列表