第16章:Flask Web开发基础
"Web开发就像经营一家咖啡厅,Flask是你的得力助手,帮你打造温馨舒适的数字空间。"
🎯 学习目标
通过本章学习,你将能够:
- 🏗️ 掌握Flask 框架基础:理解Web应用的工作原理,学会使用Flask构建Web应用
- 🛣️ 设计路由系统:创建灵活的URL路由,处理不同类型的HTTP请求
- 📝 处理表单数据:实现用户输入验证,管理会话状态
- 🗄️ 集成数据库:使用SQLAlchemy进行数据建模和操作
- 🔌 开发API接口:构建RESTful API,实现前后端分离
- 🚀 部署Web应用:将应用部署到生产环境
🏪 "Web咖啡厅"比喻体系
在学习Flask之前,让我们建立一个有趣的比喻:
🏪 Flask应用 = 经营一家咖啡厅
├── 🍽️ 路由系统 = 菜单设计(客人看什么,点什么)
├── 👨🍳 视图函数 = 厨师团队(谁来做,怎么做)
├── 🍽️ 模板引擎 = 餐具摆盘(怎么呈现给客人)
├── 🛎️ 请求处理 = 服务员接单(理解客人需求)
├── 📦 数据库 = 厨房仓库(存储所有原料)
├── 🚚 API接口 = 外卖服务(远程提供服务)
└── 🏢 部署上线 = 连锁经营(规模化运营)
这个比喻将贯穿整个章节,让抽象的技术概念变得具体可感!
16.1 Flask框架入门 - "咖啡厅开张" ☕
万事开头难,但开一家咖啡厅其实很简单——你需要的只是一个地方、一份菜单、和一位厨师。
🎯 本节目标
- 理解Flask的基本概念和工作原理
- 学会创建第一个Flask应用
- 掌握路由系统和视图函数
- 了解模板引擎的使用方法
📚 理论基础:Flask是什么?
Flask 是一个轻量级的Python Web框架,它提供了构建Web应用所需的核心功能,同时保持简洁和灵活。
Flask的核心特点
# Flask的设计哲学"""🎯 微框架 (Micro Framework)- 核心简单,功能通过扩展添加- 给开发者最大的自由度🔧 WSGI兼容- 遵循Python Web服务器网关接口标准- 可以部署到各种Web服务器📦 内置功能- 路由系统:URL到函数的映射- 模板引擎:Jinja2模板系统- 请求处理:HTTP请求和响应处理- 会话管理:用户状态保持🧩 扩展生态- Flask-SQLAlchemy:数据库ORM- Flask-WTF:表单处理- Flask-Login:用户认证- Flask-Mail:邮件发送"""
🛠️ 环境准备
1. 安装Flask和相关依赖
# 创建虚拟环境
python -m venv flask_env
flask_env\Scripts\activate # Windows
# source flask_env/bin/activate # Linux/Mac
# 安装核心包
pip install Flask==2.3.3
pip install Flask-SQLAlchemy==3.0.5
pip install Flask-WTF==1.1.1
pip install Flask-Login==0.6.2
2. 项目结构设计
my_coffee_shop/ # 我们的咖啡厅项目
├── app.py # 主应用文件(咖啡厅管理中心)
├── config.py # 配置文件(经营方针)
├── requirements.txt # 依赖清单(供应商列表)
├── templates/ # 模板目录(餐具仓库)
│ ├── base.html # 基础模板(标准餐具)
│ ├── index.html # 首页模板(欢迎台布置)
│ └── about.html # 关于页面(店铺介绍)
├── static/ # 静态文件(装饰用品)
│ ├── css/ # 样式文件(装修风格 )
│ ├── js/ # JavaScript(智能设备)
│ └── images/ # 图片(装饰画)
└── models/ # 数据模型(仓库管理)
🚀 第一个Flask应用:Hello Coffee!
让我们从最简单的咖啡厅开始:
# app.py - 咖啡厅的第一天营业from flask import Flask# 创建咖啡厅实例(开店!)app = Flask(__name__)# 设置咖啡厅的基本信息app.config['SECRET_KEY'] = 'your-secret-key-here' # 店铺密钥@app.route('/') # 菜单项:首页(欢迎光临)def home():"""首页视图函数 - 相当于门口的欢迎员"""return """<h1>🏪 欢迎来到我的咖啡厅!</h1><p>☕ 这里有最香浓的咖啡和最温暖的服务</p><a href="/menu">查看菜单</a> |<a href="/about">关于我们</a>"""@app.route('/menu') # 菜单项:菜单页面def menu():"""菜单视图函数 - 相当于菜单展示员"""menu_items = ["☕ 美式咖啡 - ¥25","🥛 拿铁咖啡 - ¥30","🍰 提拉米苏 - ¥35","🥪 三明治 - ¥20"]menu_html = "<h1>📋 咖啡厅菜单</h1><ul>"for item in menu_items:menu_html += f"<li>{item}</li>"menu_html += "</ul><a href='/'>返回首页</a>"return menu_html@app.route('/about') # 菜单项:关于页面def about():"""关于页面视图函数 - 相当于店铺介绍员"""return """<h1>🏪 关于我们</h1><p>我们是一家温馨的咖啡厅,致力于为每位客人提供最好的咖啡体验。</p><p>📍 地址:Python街Flask大道16号</p><p>⏰ 营业时间:9:00-21:00</p><a href="/">返回首页</a>"""# 启动咖啡厅(开始营业!)if __name__ == '__main__':print("🏪 咖啡厅正在开张...")print("🌐 请访问:http://127.0.0.1:5000")app.run(debug=True) # debug=True 相当于"试营业模式"
运行你的第一个咖啡厅
# 启动应用
python app.py
# 你会看到:
"""
🏪 咖啡厅正在开张...
🌐 请访问:http://127.0.0.1:5000
* Running on http://127.0.0.1:5000
* Debug mode: on
"""
打开浏览器访问 http://127.0.0.1:5000,你的第一家数字咖啡厅就开张了!
🛣️ 深入理解:路由系统
路由系统就像咖啡厅的菜单设计,决定了客人能点什么,以及每个订单由谁来处理。
1. 基本路由语法
@app.route('/路径') # 装饰器:定义菜单项def 视图函数名(): # 函数:指定处理的厨师return '响应内容' # 返回:端给客人的东西```#### 2. 动态路由:个性化服务
# 个性化欢迎 - 像咖啡师记住常 客的名字@app.route('/welcome/<name>')def welcome_customer(name):"""个性化欢迎函数"""return f"""<h1>🎉 欢迎 {name} 来到我们的咖啡厅!</h1><p>很高兴再次见到您!</p><a href="/">返回首页</a>"""# 订单详情 - 根据订单号查看详情@app.route('/order/<int:order_id>')def order_detail(order_id):"""订单详情查看"""# 模拟订单数据orders = {1: {"item": "美式咖啡", "price": 25, "status": "制作中"},2: {"item": "拿铁咖啡", "price": 30, "status": "已完成"},3: {"item": "提拉米苏", "price": 35, "status": "已送达"}}order = orders.get(order_id)if order:return f"""<h1>📋 订单 #{order_id} 详情</h1><p>商品:{order['item']}</p><p>价格:¥{order['price']}</p><p>状态:{order['status']}</p><a href="/menu">继续点餐</a>"""else:return f"<h1>❌ 订单 #{order_id} 不存在</h1><a href='/'>返回首页</a>"# 分类菜单 - 按类别展示商品@app.route('/category/<category>')def show_category(category):"""按分类显示商品"""categories = {'coffee': ['美式咖啡', '拿铁咖啡', '卡布奇诺'],'dessert': ['提拉米苏', '芝士蛋糕', '马卡龙'],'snack': ['三明治', '沙拉', '饼干']}items = categories.get(category, [])if items:items_html = "<ul>"for item in items:items_html += f"<li>{item}</li>"items_html += "</ul>"return f"""<h1>📂 {category.title()} 分类</h1>{items_html}<a href="/menu">查看完整菜单</a>"""else:return f"<h1>❌ 分类 '{category}' 不存在</h1><a href='/menu'>查看菜单</a>"
3. HTTP方法:不同的服务方式
from flask import request# 支持多种HTTP方法的路由@app.route('/contact', methods=['GET', 'POST'])def contact():"""联系我们页面 - 支持查看和提交"""if request.method == 'GET':# GET请求:显示联系表单(客人想要联系表)return """<h1>📞 联系我们</h1><form method="POST"><p>姓名:<input type="text" name="name" required></p><p>邮箱:<input type="email" name="email" required></p><p>消息:<textarea name="message" required></textarea></p><p><button type="submit">发送消息</button></p></form><a href="/">返回首页</a>"""else:# POST请求:处理表单提交(客人提交了联系信息)name = request.form.get('name')email = request.form.get('email')message = request.form.get('message')# 这里可以保存到数据库或发送邮件return f"""<h1>✅ 消息已收到!</h1><p>感谢 {name} 的反馈!</p><p>我们会尽快回复到 {email}</p><a href="/">返回首页</a>"""# 仅支持POST的路由(比如订单提交)@app.route('/submit_order', methods=['POST'])def submit_order():"""提交订单 - 只接受POST请求"""item = request.form.get('item')quantity = request.form.get('quantity', 1)return f"""<h1>✅ 订单已提交!</h1><p>商品:{item}</p><p>数量:{quantity}</p><p>我们正在为您准备...</p><a href="/menu">继续点餐</a>"""
🍽️ 模板引擎:餐具与摆盘艺术
如果说路由是菜单,视图函数是厨师,那么模板就是餐具和摆盘——决定了美食如何优雅地呈现给客人。
1. 为什么需要模板?
# 不使用模板的问题(直接在视图函数中写HTML)@app.route('/ugly_page')def ugly_page():return """<html><head><title>丑陋的页面</title></head><body><h1>这样写HTML很痛苦</h1><p>代码混乱,难以维护</p><p>没 有复用性</p></body></html>"""# 问题:HTML和Python代码混在一起,就像在厨房里摆盘一样混乱!
2. Jinja2模板引擎基础
首先创建模板目录和基础模板:
<!-- templates/base.html - 基础餐具模板 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的咖啡厅{% endblock %}</title>
<style>
/* 咖啡厅基础装修风格 */
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5dc; /* 米色背景 */
color: #4a4a4a;
}
.header {
background-color: #8b4513; /* 咖啡色 */
color: white;
padding: 1rem;
text-align: center;
}
.nav {
background-color: #deb887; /* 浅咖啡色 */
padding: 0.5rem;
text-align: center;
}
.nav a {
color: #4a4a4a;
text-decoration: none;
margin: 0 1rem;
font-weight: bold;
}
.nav a:hover {
color: #8b4513;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.footer {
background-color: #8b4513;
color: white;
text-align: center;
padding: 1rem;
margin-top: 2rem;
}
</style>
</head>
<body>
<!-- 咖啡厅顶部标识 -->
<header class="header">
<h1>☕ 我的温馨咖啡厅</h1>
<p>品味生活,从一杯好咖啡开始</p>
</header>
<!-- 导航菜单 -->
<nav class="nav">
<a href="/">🏠 首页</a>
<a href="/menu">📋 菜单</a>
<a href="/about">ℹ️ 关于我们</a>
<a href="/contact">📞 联系我们</a>
</nav>
<!-- 主要内容区域 -->
<main class="container">
{% block content %}
<!-- 具体页面内容将在这里显示 -->
{% endblock %}
</main>
<!-- 页脚信息 -->
<footer class="footer">
<p>© 2025 我的咖啡厅 | 用Flask制作,用❤️调味</p>
</footer>
</body>
</html>
<!-- templates/index.html - 首页模板 -->
{% extends "base.html" %}
{% block title %}首页 - 我的咖啡厅{% endblock %}
{% block content %}
<div style="text-align: center;">
<h1>🎉 欢迎来到我们的咖啡厅!</h1>
<div style="margin: 2rem 0;">
<img src="https://via.placeholder.com/400x200/8b4513/ffffff?text=☕+Welcome"
alt="咖啡厅欢迎图" style="max-width: 100%; border-radius: 10px;">
</div>
<p style="font-size: 1.2rem; margin: 1.5rem 0;">
在这里,每一杯咖啡都承载着我们的用心,每一个微笑都传递着温暖。
</p>
<div style="display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap;">
<a href="/menu" style="background-color: #8b4513; color: white; padding: 0.8rem 1.5rem;
text-decoration: none; border-radius: 5px; font-weight: bold;">
📋 查看菜单
</a>
<a href="/about" style="background-color: #deb887; color: #4a4a4a; padding: 0.8rem 1.5rem;
text-decoration: none; border-radius: 5px; font-weight: bold;">
ℹ️ 了解更多
</a>
</div>
<!-- 今日推荐 -->
<div style="margin-top: 3rem; padding: 1.5rem; background-color: white; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<h2>🌟 今日推荐</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem;">
<div style="text-align: center; padding: 1rem;">
<h3>☕ 招牌拿铁</h3>
<p>香浓咖啡与丝滑牛奶的完美融合</p>
<span style="color: #8b4513; font-weight: bold;">¥30</span>
</div>
<div style="text-align: center; padding: 1rem;">
<h3>🍰 提拉米苏</h3>
<p>意式经典甜品,入口即化</p>
<span style="color: #8b4513; font-weight: bold;">¥35</span>
</div>
<div style="text-align: center; padding: 1rem;">
<h3>🥪 俱乐部三明治</h3>
<p>新鲜食材,营养丰富</p>
<span style="color: #8b4513; font-weight: bold;">¥25</span>
</div>
</div>
</div>
</div>
{% endblock %}
3. 在视图函数中使用模板
from flask import render_template# 更新后的视图函数 - 使用模板@app.route('/')def home():"""首页视图 - 使用模板渲染"""return render_template('index.html')@app.route('/menu')def menu():"""菜单页面 - 传递数据到模板"""# 咖啡厅菜单数据(实际项目中来自数据库)menu_data = {'coffee': [{'name': '美式咖啡', 'price': 25, 'description': '经典黑咖啡,苦中带香'},{'name': '拿铁咖啡', 'price': 30, 'description': '咖啡与牛奶的完美融合'},{'name': '卡布奇诺', 'price': 28, 'description': '浓郁咖啡配奶泡'},{'name': '摩卡咖啡', 'price': 32, 'description': '咖啡巧克力双重享受'}],'dessert': [{'name': '提拉米苏', 'price': 35, 'description': '意式经典甜品'},{'name': '芝士蛋糕', 'price': 30, 'description': '浓郁芝士香味'},{'name': '马卡龙', 'price': 8, 'description': '法式精致小点'}],'snack': [{'name': '俱乐部三明治', 'price': 25, 'description': '多层丰富口感'},{'name': '凯撒沙拉', 'price': 22, 'description': '新鲜蔬菜健康选择'},{'name': '牛角包', 'price': 15, 'description': '法式酥脆面包'}]}return render_template('menu.html', menu=menu_data)@app.route('/about')def about():"""关于页面 - 传递店铺信息"""shop_info = {'name': '我的温馨咖啡厅','address': 'Python街Flask大道16号','phone': '400-FLASK-16','hours': '周一至周日 9:00-21:00','story': '我们的咖啡厅成立于2025年,致力于为每位客人提供最优质的咖啡体验。我们精选世界各地的优质咖啡豆,用心烘焙,用爱调制。','features': ['☕ 精选咖啡豆,现场烘焙','🍰 手工制作甜品','📚 舒适阅读环境','💻 免费WiFi','🎵 轻松背景音乐']}return render_template('about.html', shop=shop_info)
对应的模板文件:
<!-- templates/menu.html - 菜单页面模板 -->
{% extends "base.html" %}
{% block title %}菜单 - 我的咖啡厅{% endblock %}
{% block content %}
<h1 style="text-align: center; color: #8b4513;">📋 咖啡厅菜单</h1>
{% for category, items in menu.items() %}
<div style="margin: 2rem 0; padding: 1.5rem; background-color: white; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<h2 style="color: #8b4513; border-bottom: 2px solid #deb887; padding-bottom: 0.5rem;">
{% if category == 'coffee' %}☕ 咖啡类
{% elif category == 'dessert' %}🍰 甜品类
{% else %}🥪 轻食类{% endif %}
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; margin-top: 1rem;">
{% for item in items %}
<div style="border: 1px solid #deb887; border-radius: 8px; padding: 1rem;">
<h3 style="margin: 0 0 0.5rem 0; color: #8b4513;">{{ item.name }}</h3>
<p style="margin: 0.5rem 0; color: #666; font-size: 0.9rem;">{{ item.description }}</p>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem;">
<span style="font-size: 1.2rem; font-weight: bold; color: #8b4513;">¥{{ item.price }}</span>
<button style="background-color: #8b4513; color: white; border: none; padding: 0.5rem 1rem; border-radius: 5px; cursor: pointer;">
加入订单
</button>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<div style="text-align: center; margin-top: 2rem;">
<p style="color: #666;">所有价格均为人民币,如有变动恕不另行通知</p>
<a href="/" style="color: #8b4513; text-decoration: none;">← 返回首页</a>
</div>
{% endblock %}
🚀 实战项目:个人博客系统
现在让我们把学到的知识整合起来,创建一个完整的个人博客系统:
# blog_app.py - 完整的博客应用from flask import Flask, render_template, request, redirect, url_forfrom datetime import datetimeapp = Flask(__name__)app.config['SECRET_KEY'] = 'your-blog-secret-key'# 模拟博客数据(实际项目中使用数据库)blog_posts = [{'id': 1,'title': '我的第一篇博客','content': '欢迎来到我的博客!这里我会分享我的学习心得和生活感悟。','author': '博主','date': datetime(2025, 1, 1),'tags': ['生活', '学习']},{'id': 2,'title': 'Flask学习笔记','content': 'Flask是一个很棒的Python Web框架,轻量级但功能强大。','author': '博主','date': datetime(2025, 1, 15),'tags': ['技术', 'Python', 'Flask']}]@app.route('/')def blog_home():"""博客首页 - 显示所有文章"""return render_template('blog/index.html', posts=blog_posts)@app.route('/post/<int:post_id>')def blog_post(post_id):"""文章详情页"""post = next((p for p in blog_posts if p['id'] == post_id), None)if post:return render_template('blog/post.html', post=post)else:return "文章不存在", 404@app.route('/new_post', methods=['GET', 'POST'])def new_post():"""创建新文章"""if request.method == 'GET':return render_template('blog/new_post.html')else:# 处理表单提交title = request.form.get('title')content = request.form.get('content')tags = request.form.get('tags', '').split(',')# 创建新文章new_post = {'id': len(blog_posts) + 1,'title': title,'content': content,'author': '博主','date': datetime.now(),'tags': [tag.strip() for tag in tags if tag.strip()]}blog_posts.append(new_post)return redirect(url_for('blog_home'))@app.route('/about')def blog_about():"""关于页面"""return render_template('blog/about.html')if __name__ == '__main__':app.run(debug=True)
对应的模板文件:
<!-- templates/blog/base.html - 博客基础模板 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的个人博客{% endblock %}</title>
<style>
body {
font-family: 'Georgia', serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
text-align: center;
}
.nav {
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1rem 0;
}
.nav-container {
max-width: 800px;
margin: 0 auto;
display: flex;
justify-content: center;
gap: 2rem;
}
.nav a {
color: #333;
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
}
.nav a:hover {
color: #667eea;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.footer {
background-color: #333;
color: white;
text-align: center;
padding: 2rem 0;
margin-top: 4rem;
}
</style>
</head>
<body>
<header class="header">
<h1>📝 我的个人博客</h1>
<p>记录生活,分享思考</p>
</header>
<nav class="nav">
<div class="nav-container">
<a href="/">🏠 首页</a>
<a href="/new_post">✍️ 写文章</a>
<a href="/about">👤 关于我</a>
</div>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<p>© 2025 我的个人博客 | 用Flask构建,用心维护</p>
</footer>
</body>
</html>
📝 本节小结
通过"咖啡厅开张"这一节,我们学会了:
- Flask基础概念:理解了微框架的设计理念
- 路由系统:学会了静态路由、动态路由和HTTP方法
- 模板引擎:掌握了Jinja2的基本语法和模板继承
- 项目实战:完成了从简单页面到博客系统的完整开发
核心要点回顾:
- 🏪 Flask应用就像咖啡厅,需要菜单(路由)、厨师(视图函数)、餐具(模板)
- 🛣️ 路由系统负责URL到函数的映射,支持动态参数和多种HTTP方法
- 🍽️ 模板引擎分离了展示逻辑和业务逻辑,提高代码可维护性
- 🚀 实际项目中要合理组织代码结构,使用模板继承提高复用性
下一节预告:在16.2节"顾客点餐服务"中,我们将学习如何处理用户输入、验证表单数据、管理用户会话,让我们的咖啡厅能够真正为客人提供个性化服务!
16.2 请求处理与表单 - "顾客点餐服务" 🛎️
一家咖啡厅的成功不仅在于好的咖啡,更在于优质的服务。服务员要能听懂客人的需求,准确记录订单,还要记住常客的喜好。
🎯 本节目标
- 掌握HTTP请求的处理方法
- 学会创建和验证HTML表单
- 理解会话管理和用户状态保持
- 实现用户注册和登录系统
📚 理论基础:HTTP请求处理
在Web开发中,客户端(浏览器)和服务器之间通过HTTP协议进行通信,就像客人和服务员之间的对话。
HTTP请求的组成部分
"""HTTP请求 = 客人的完整订单├── 请求方法 (GET/POST/PUT/DELETE) = 服务类型(查看菜单/下订单/修改订单/取消订单)├── 请求头 (Headers) = 客人的基本信息(语言偏好、设备类型等)├── 请求参数 (Query Parameters) = 具体要求(不加糖、少冰等)└── 请求体 (Request Body) = 详细订单内容(表单数据、JSON数据等)"""
🛎️ Flask中的请求处理
1. 获取请求数据
from flask import Flask, request, render_template, redirect, url_for, flashapp = Flask(__name__)app.config['SECRET_KEY'] = 'your-secret-key-for-sessions'@app.route('/order_info')def show_request_info():"""展示请求信息 - 像服务员了解客人需求"""info = {'method': request.method, # 请求方法'url': request.url, # 完整URL'path': request.path, # 路径部分'args': dict(request.args), # URL参数'form': dict(request.form), # 表单数据'headers': dict(request.headers), # 请求头'remote_addr': request.remote_addr, # 客户端IP'user_agent': request.user_agent.string # 浏览器信息}return f"""<h1>📋 请求信息详情</h1><h2>基本信息</h2><p><strong>请求方法:</strong> {info['method']}</p><p><strong>请求URL:</strong> {info['url']}</p><p><strong>客户端IP:</strong> {info['remote_addr']}</p><h2>URL参数 (Query Parameters)</h2><pre>{info['args']}</pre><h2>表单数据</h2><pre>{info['form']}</pre><a href="/">返回首页</a>"""# 测试URL: /order_info?drink=coffee&size=large&sugar=no
2. 处理不同类型的请求参数
@app.route('/search')def search_menu():"""菜单搜索功能 - 根据客人需求筛选"""# 获取URL参数 (Query Parameters)keyword = request.args.get('q', '') # 搜索关键词category = request.args.get('category', 'all') # 分类筛选min_price = request.args.get('min_price', 0, type=int) # 最低价格max_price = request.args.get('max_price', 100, type=int) # 最高价格# 模拟菜单数据menu_items = [{'name': '美式咖啡', 'category': 'coffee', 'price': 25},{'name': '拿铁咖啡', 'category': 'coffee', 'price': 30},{'name': '提拉米苏', 'category': 'dessert', 'price': 35},{'name': '三明治', 'category': 'snack', 'price': 20}]# 根据条件筛选filtered_items = []for item in menu_items:# 关键词匹配if keyword and keyword.lower() not in item['name'].lower():continue# 分类筛选if category != 'all' and item['category'] != category:continue# 价格范围if not (min_price <= item['price'] <= max_price):continuefiltered_items.append(item)# 构建结果HTMLresult_html = f"<h1>🔍 搜索结果</h1>"result_html += f"<p>关键词: '{keyword}' | 分类: {category} | 价格: ¥{min_price}-{max_price}</p>"if filtered_items:result_html += "<ul>"for item in filtered_items:result_html += f"<li>{item['name']} - {item['category']} - ¥{item['price']}</li>"result_html += "</ul>"else:result_html += "<p>❌ 没有找到符合条件的商品</p>"result_html += """<form method="GET" action="/search"><p>关键词: <input type="text" name="q" value="">分类: <select name="category"><option value="all">全部</option><option value="coffee">咖啡</option><option value="dessert">甜品</option><option value="snack">轻食</option></select></p><p>价格范围: ¥<input type="number" name="min_price" value="0" min="0"> -¥<input type="number" name="max_price" value="100" min="0"></p><p><button type="submit">🔍 搜索</button></p></form><a href="/">返回首页</a>"""return result_html
📝 HTML表单处理
表单是客人向咖啡厅下订单的主要方式,我们需要仔细处理每一个细节。
1. 基础表单创建和处理
@app.route('/order', methods=['GET', 'POST'])def place_order():"""下订单功能 - 完整的点餐流程"""if request.method == 'GET':# 显示订单表单return render_template('order_form.html')else:# 处理订单提交# 获取表单数据customer_name = request.form.get('customer_name')drink = request.form.get('drink')size = request.form.get('size')sugar = request.form.get('sugar')milk = request.form.get('milk')special_requests = request.form.get('special_requests', '')# 基础验证errors = []if not customer_name:errors.append("请填写客人姓名")if not drink:errors.append("请选择饮品")if not size:errors.append("请选择杯型")if errors:# 有错误,重新显示表单return render_template('order_form.html',errors=errors,form_data=request.form)# 计算价格prices = {'americano': {'small': 20, 'medium': 25, 'large': 30},'latte': {'small': 25, 'medium': 30, 'large': 35},'cappuccino': {'small': 23, 'medium': 28, 'large': 33}}total_price = prices.get(drink, {}).get(size, 0)# 生成订单号import timeorder_id = f"ORD{int(time.time())}"# 保存订单(实际项目中保存到数据库)order = {'id': order_id,'customer_name': customer_name,'drink': drink,'size': size,'sugar': sugar,'milk': milk,'special_requests': special_requests,'total_price': total_price,'status': '制作中','order_time': time.strftime('%Y-%m-%d %H:%M:%S')}# 使用flash消息显示成功信息flash(f'订单 {order_id} 已成功提交!', 'success')return render_template('order_success.html', order=order)# 对应的模板文件
<!-- templates/order_form.html - 订单表单模板 -->
{% extends "base.html" %}
{% block title %}下订单 - 我的咖啡厅{% endblock %}
{% block content %}
<div style="max-width: 600px; margin: 0 auto;">
<h1 style="text-align: center; color: #8b4513;">📝 客人订单</h1>
<!-- 显示错误信息 -->
{% if errors %}
<div style="background-color: #ffe6e6; border: 1px solid #ff9999; padding: 1rem; border-radius: 5px; margin-bottom: 1rem;">
<h3 style="color: #cc0000; margin: 0 0 0.5rem 0;">❌ 请修正以下错误:</h3>
<ul style="margin: 0; color: #cc0000;">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- 显示flash消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div style="background-color: {% if category == 'success' %}#e6ffe6{% else %}#ffe6e6{% endif %};
border: 1px solid {% if category == 'success' %}#99ff99{% else %}#ff9999{% endif %};
padding: 1rem; border-radius: 5px; margin-bottom: 1rem;">
<p style="margin: 0; color: {% if category == 'success' %}#006600{% else %}#cc0000{% endif %};">
{{ message }}
</p>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" style="background-color: white; padding: 2rem; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<!-- 客人信息 -->
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem;">👤 客人姓名:</label>
<input type="text" name="customer_name"
value="{{ form_data.customer_name if form_data else '' }}"
style="width: 100%; padding: 0.8rem; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem;"
placeholder="请输入您的姓名" required>
</div>
<!-- 饮品选择 -->
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem;">☕ 选择饮品:</label>
<select name="drink" style="width: 100%; padding: 0.8rem; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem;" required>
<option value="">请选择饮品</option>
<option value="americano" {% if form_data and form_data.drink == 'americano' %}selected{% endif %}>美式咖啡</option>
<option value="latte" {% if form_data and form_data.drink == 'latte' %}selected{% endif %}>拿铁咖啡</option>
<option value="cappuccino" {% if form_data and form_data.drink == 'cappuccino' %}selected{% endif %}>卡布奇诺</option>
</select>
</div>
<!-- 杯型选择 -->
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem;">📏 选择杯型:</label>
<div style="display: flex; gap: 1rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="size" value="small"
{% if form_data and form_data.size == 'small' %}checked{% endif %}
style="margin-right: 0.5rem;">
小杯 (+¥0)
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="size" value="medium"
{% if form_data and form_data.size == 'medium' %}checked{% endif %}
style="margin-right: 0.5rem;">
中杯 (+¥5)
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="size" value="large"
{% if form_data and form_data.size == 'large' %}checked{% endif %}
style="margin-right: 0.5rem;">
大杯 (+¥10)
</label>
</div>
</div>
<!-- 糖分选择 -->
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem;">🍯 糖分要求:</label>
<select name="sugar" style="width: 100%; padding: 0.8rem; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem;">
<option value="normal" {% if form_data and form_data.sugar == 'normal' %}selected{% endif %}>正常糖</option>
<option value="less" {% if form_data and form_data.sugar == 'less' %}selected{% endif %}>少糖</option>
<option value="none" {% if form_data and form_data.sugar == 'none' %}selected{% endif %}>无糖</option>
<option value="extra" {% if form_data and form_data.sugar == 'extra' %}selected{% endif %}>多糖</option>
</select>
</div>
<!-- 奶制品选择 -->
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem;">🥛 奶制品选择:</label>
<div style="display: flex; flex-wrap: wrap; gap: 1rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" name="milk" value="whole_milk"
{% if form_data and 'whole_milk' in form_data.getlist('milk') %}checked{% endif %}
style="margin-right: 0.5rem;">
全脂牛奶
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" name="milk" value="skim_milk"
{% if form_data and 'skim_milk' in form_data.getlist('milk') %}checked{% endif %}
style="margin-right: 0.5rem;">
脱脂牛奶
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" name="milk" value="soy_milk"
{% if form_data and 'soy_milk' in form_data.getlist('milk') %}checked{% endif %}
style="margin-right: 0.5rem;">
豆奶
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" name="milk" value="oat_milk"
{% if form_data and 'oat_milk' in form_data.getlist('milk') %}checked{% endif %}
style="margin-right: 0.5rem;">
燕麦奶
</label>
</div>
</div>
<!-- 特殊要求 -->
<div style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem;">📝 特殊要求:</label>
<textarea name="special_requests"
style="width: 100%; padding: 0.8rem; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem; min-height: 80px;"
placeholder="如有特殊要求请在此说明...">{{ form_data.special_requests if form_data else '' }}</textarea>
</div>
<!-- 提交按钮 -->
<div style="text-align: center;">
<button type="submit" style="background-color: #8b4513; color: white; border: none; padding: 1rem 2rem; border-radius: 5px; font-size: 1.1rem; cursor: pointer; font-weight: bold;">
🛎️ 提交订单
</button>
</div>
</form>
<div style="text-align: center; margin-top: 1rem;">
<a href="/" style="color: #8b4513; text-decoration: none;">← 返回首页</a>
</div>
</div>
{% endblock %}
🎫 会话管理:记住常客喜好
会话(Session)就像咖啡厅的会员卡系统,让我们能够记住每位客人的偏好和历史订单。
1. Flask Session基础
from flask import session@app.route('/set_preference', methods=['POST'])def set_preference():"""设置客人偏好 - 像办理会员卡"""# 从表单获取偏好设置favorite_drink = request.form.get('favorite_drink')favorite_size = request.form.get('favorite_size')sugar_level = request.form.get('sugar_level')# 保存到会话中session['favorite_drink'] = favorite_drinksession['favorite_size'] = favorite_sizesession['sugar_level'] = sugar_levelsession['customer_name'] = request.form.get('customer_name')flash('偏好设置已保存!下次点餐会为您自动填充', 'success')return redirect(url_for('order_with_preference'))@app.route('/order_with_preference')def order_with_preference():"""智能订单表单 - 根据会员偏好预填充"""# 从会话中获取偏好preferences = {'customer_name': session.get('customer_name', ''),'favorite_drink': session.get('favorite_drink', ''),'favorite_size': session.get('favorite_size', ''),'sugar_level': session.get('sugar_level', 'normal')}return render_template('smart_order_form.html', preferences=preferences)@app.route('/clear_session')def clear_session():"""清除会话 - 像注销会员卡"""session.clear()flash('会话已清除,偏好设置已重置', 'info')return redirect(url_for('home'))@app.route('/view_session')def view_session():"""查看当前会话内容 - 调试用"""return f"""<h1>🎫 当前会话信息</h1><pre>{dict(session)}</pre><a href="/clear_session">清除会话</a> |<a href="/">返回首页</a>"""