在烹饪小程序的开发过程中,后端服务是整个系统的核心枢纽,它承担着数据存储、图片管理以及AI智能解析等关键功能。今天我们将深入探讨如何使用Flask框架搭建一个完整的后端服务,配合MySQL数据库存储菜谱信息,并集成DeepSeek大模型API来实现菜谱的智能结构化解析。整个技术栈包括Flask作为Web框架、SQLAlchemy作为ORM工具、PyMySQL作为数据库驱动,以及微信小程序前端通过HTTP请求与后端进行数据交互。本文将按照模块化的思路,逐一拆解数据库表设计、图片上传接口、菜谱列表与详情接口、以及AI解析接口的实现细节,帮助你构建起完整的技术认知。
一、MySQL数据库表设计:Food表字段的语义与结构化存储的意义
数据库表设计是整个系统的基础,合理的字段规划直接影响到后续功能的扩展性和查询效率。在本项目中,我们设计了一张名为Food的核心表,用于存储每一道菜谱的完整信息。该表包含六个字段:自增主键id、菜谱名称name、封面图片路径image_url、菜谱描述文本desc、AI生成的结构化数据structured_data以及创建时间create_time。其中,name和desc属于用户直接输入的基础信息,image_url存储的是图片在后端服务器上的相对路径,而create_time则通过Python的datetime模块自动生成,用于记录每条数据的入库时间,方便后续按时间排序展示。
python
class Food(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
image_url = db.Column(db.String(500))
desc = db.Column(db.Text)
structured_data = db.Column(db.Text)
create_time = db.Column(db.DateTime, default=datetime.now)
这段代码使用SQLAlchemy的ORM模型定义了Food表的结构。id字段作为主键,其值由数据库自动递增生成,确保每条记录具有唯一标识。name字段被设置为不可为空,这是业务逻辑的基本要求,因为一个没有名称的菜谱是没有意义的。image_url和desc允许为空,考虑到用户可能在创建菜谱时尚未上传图片或填写描述。structured_data字段是整个设计中最值得关注的部分,它使用Text类型存储,实际上保存的是一个完整的JSON字符串。之所以要将AI解析后的结构化数据单独存成一个字段,而不是将其拆分成多张关联表,是因为AI返回的JSON结构包含了菜谱的步骤、食材、工具、动画类型等高度嵌套的信息,如果强行拆成关系型表结构,不仅会大幅增加表数量和查询复杂度,还会在写入和读取时产生大量的拆解与组装开销。将整个JSON作为一个文本字段存储,既保持了数据的完整性,又极大地简化了读写逻辑。当小程序端需要展示菜谱详情时,后端直接将这个JSON字符串原样返回,前端拿到后解析渲染即可。
二、Flask应用初始化与跨域配置:为前后端分离架构铺路
在正式编写业务接口之前,需要完成Flask应用实例的创建以及相关中间件的配置。由于小程序前端和后端服务运行在不同的端口上,浏览器的同源策略会阻止前端直接请求后端接口,因此必须启用CORS跨域资源共享。Flask-CORS扩展提供了极其简洁的解决方案,只需在创建app实例后调用CORS(app)即可全局放行所有跨域请求。与此同时,数据库连接字符串的配置也至关重要,它告诉SQLAlchemy如何连接到本地的MySQL数据库。连接字符串采用标准的URI格式,依次指定数据库驱动类型、用户名、密码、主机地址和数据库名称。
python
app = Flask(__name__)
CORS(app)
app.config['SQLALCHEMY_DATABASE_URI'] = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
在这段配置代码中,CORS(app)一行完成了跨域支持,使得来自微信小程序开发工具的请求不会被浏览器拦截。SQLALCHEMY_DATABASE_URI配置项的值被拼接为完整的MySQL连接字符串,其中pymysql指明使用PyMySQL作为数据库驱动,花括号中的变量分别对应数据库用户名、密码、主机地址和数据库名。SQLALCHEMY_TRACK_MODIFICATIONS被显式设置为False,这个配置项用于关闭Flask-SQLAlchemy的事件追踪系统,避免不必要的内存开销。最后,SQLAlchemy(app)将app实例与ORM框架绑定,后续所有数据库操作都通过db对象来完成。此外,代码中还定义了一个UPLOAD_FOLDER常量,并调用os.makedirs确保上传目录在服务启动前就已经存在,为图片上传功能做好准备。
三、图片上传接口:处理小程序端的文件传输
图片上传是烹饪小程序的基础功能之一,用户在发布菜谱时需要选择一张封面图片。微信小程序端通过wx.chooseImage获取图片的本地临时路径,再调用wx.uploadFile将文件以multipart/form-data格式发送到后端的/api/upload接口。后端接收到文件后,需要将其保存到服务器磁盘的指定目录中,并返回一个可供后续访问的文件路径。为了确保文件名不重复,我们采用了时间戳加原始文件名的组合命名策略。
python
@app.route('/api/upload', methods=['POST'])
def upload_image():
if 'image' not in request.files:
return jsonify({"code": 400, "msg": "请选择图片"})
file = request.files['image']
if file.filename == '':
return jsonify({"code": 400, "msg": "图片不能为空"})
filename = datetime.now().strftime("%Y%m%d%H%M%S_") + file.filename
save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(save_path)
return jsonify({"code": 200, "url": save_path, "msg": "上传成功"})
这段接口代码做了三层防御性检查。首先通过'image' not in request.files判断请求中是否确实包含名为image的文件字段,如果没有,直接返回错误提示。其次,即使文件字段存在,用户也可能没有选择任何文件就提交了表单,此时file.filename会是空字符串,第二个判断负责拦截这种情况。通过这两层校验之后,代码使用datetime.now()生成一个精确到秒的时间戳字符串,拼接到原始文件名之前,这样就有效避免了多用户同时上传同名文件导致的覆盖问题。os.path.join负责将上传目录路径与文件名拼接成完整的保存路径,最后调用file.save将文件写入磁盘。返回给前端的url是文件在服务器上的相对路径,前端可以基于此路径构建出完整的访问URL。
四、文件访问路由:让上传的图片能够被前端展示
图片上传到服务器后,还需要一个能够响应HTTP请求的路由来提供文件访问服务。Flask内置的send_from_directory函数专门用于安全地从指定目录中发送文件给客户端。我们定义了一个动态路由,接受文件名作为URL参数,然后调用send_from_directory将对应文件返回给请求方。这个路由的存在使得小程序端可以通过拼接URL的方式直接访问服务器上的图片资源。
python
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
这段代码虽然简短,但背后涉及一个重要的安全设计原则。send_from_directory函数在发送文件时会进行路径安全检查,防止恶意用户通过构造包含.../的filename参数来访问服务器上的其他目录。使用这个Flask内置函数比自己手动拼接路径并打开文件要安全得多。路由中的是一个动态参数,Flask会将URL中该位置的实际字符串捕获并传入视图函数。当前端请求http://127.0.0.1:5000/uploads/20240521120000_photo.jpg时,Flask会提取出20240521120000_photo.jpg,然后在UPLOAD_FOLDER目录中查找该文件并返回。结合图片上传接口返回的路径,小程序端就可以构建出完整的图片URL来展示封面图。
五、菜谱列表接口:按时间倒序返回所有数据
菜谱列表接口是小程序首页的数据来源,它需要查询数据库中所有的菜谱记录,并按照创建时间倒序排列,确保最新发布的菜谱排在最前面。接口的返回数据需要经过一定的加工处理,将数据库中的相对路径转换为完整的图片访问URL,这样前端拿到数据后可以直接使用,无需再自行拼接。使用SQLAlchemy的query方法配合order_by和all可以轻松完成数据库查询操作。
python
@app.route('/api/food/list', methods=['GET'])
def food_list():
foods = Food.query.order_by(Food.id.desc()).all()
data = []
for f in foods:
img_url = f"http://127.0.0.1:5000/uploads/{os.path.basename(f.image_url)}" if f.image_url else ""
item = {
"id": f.id,
"name": f.name,
"image": img_url,
"desc": f.desc
}
data.append(item)
return jsonify({"code": 200, "data": data})
这里的关键在于数据库查询与数据格式转换的配合。Food.query.order_by(Food.id.desc())构建了一个按id降序排列的查询对象,调用all()后得到的是一个Food模型实例的列表。由于数据库中的image_url字段可能存储的是上传时返回的完整相对路径,其中可能包含目录前缀,为了统一生成标准格式的URL,代码使用os.path.basename提取出纯文件名,然后与固定的服务器地址和uploads路径前缀拼接。这种处理方式保证了即使将来修改了上传目录的命名,列表接口返回的图片链接依然格式统一。遍历完成后,每条菜谱被转换成一个只包含id、name、image和desc四个字段的字典,前端列表页只需要这些摘要信息,不需要加载完整的结构化数据,从而减少了数据传输量。
六、菜谱详情接口:读取并返回结构化JSON数据
当用户点击列表中的某一道菜谱时,小程序需要请求详情接口获取该菜谱的完整信息,包括AI生成的结构化数据。这个接口接收菜谱的id作为URL参数,通过主键查询数据库获取对应的Food记录。如果记录不存在,返回404错误;如果存在,则将数据库中的structured_data字段原样返回给前端。这个字段存储的是一个JSON字符串,前端拿到后使用JSON.parse即可还原为JavaScript对象,用于渲染步骤动画、食材清单等内容。
python
@app.route('/api/food/<int:food_id>', methods=['GET'])
def get_food_detail(food_id):
food = Food.query.get(food_id)
if not food:
return jsonify({"code": 404, "msg": "不存在"})
img_url = f"http://127.0.0.1:5000/uploads/{os.path.basename(food.image_url)}" if food.image_url else ""
res = {
"id": food.id,
"name": food.name,
"image": img_url,
"desc": food.desc,
"structured_data": food.structured_data
}
return jsonify(res)
这段代码展示了RESTful风格接口的标准写法。路由中的int:food_id是一个类型转换器,Flask会自动将URL中对应位置的字符串转换为整数类型传入视图函数。Food.query.get(food_id)是SQLAlchemy提供的主键快速查询方法,它直接通过主键索引定位记录,性能远高于条件查询。返回的响应对象中,structured_data字段的值是数据库中存储的原始JSON文本,后端不需要对它做任何解析或修改,直接将这个字符串原封不动地返回给前端。这种"存储即返回"的策略大幅简化了后端逻辑,让AI生成的结构化数据能够完整透传到展示层。如果structured_data字段为空,返回空字符串即可,前端可以根据此字段是否有值来判断是否需要展示结构化的步骤内容。
七、AI解析接口:调用DeepSeek大模型实现菜谱智能化处理
AI解析接口是整个小程序中最具技术亮点的部分。用户在发布菜谱时,只需要输入一段自然语言描述的菜谱做法,后端会将这段文本发送给DeepSeek大模型,由AI自动提取出结构化的烹饪步骤、食材清单、工具列表、动画类型等信息,并以标准JSON格式返回。为了实现这个功能,我们精心设计了一个Prompt提示词,明确规定了AI输出的JSON结构以及各个字段的枚举值约束,确保返回结果格式统一、字段规范。
python
@app.route("/api/parse_recipe", methods=["POST"])
def parse_recipe():
data = request.get_json()
content = data.get("content", "")
prompt = """
你是专业菜谱结构化AI,只输出标准JSON,无任何文字。
规则如下:
【animation 只能7选】cut, pour, stir, fry, boil, steam, stew
【tools 只能12选】菜刀,菜板,炒锅,汤锅,砂锅,蒸锅,铲子,勺子,筷子,碗,盘子,滤网
【ingredients.type 只能3选】main, aux, seasoning
【输出JSON必须严格如下】
{
"id": "recipe_001",
"title": "西红柿炒蛋",
"cover": "",
"totalTime": 10,
"difficulty": "easy",
"author": "用户上传",
"ingredients": [{"name":"","amount":"","type":""}],
"steps": [{"stepIndex":1,"action":"","animation":"","voiceText":"","tools":[],"time":3}],
"tips": "",
"fromUser": true,
"isStructured": true
}
解析内容:
""" + content
resp = requests.post(
"https://api.deepseek.com/chat/completions",
headers={
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": "deepseek-chat",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"response_format": {"type": "json_object"}
}
)
res_data = resp.json()
ai_content = res_data["choices"][0]["message"]["content"]
recipe_json = json.loads(ai_content)
return jsonify({"success": True, "data": recipe_json})
这段代码的Prompt设计是整个AI解析功能的核心。Prompt中明确列出了animation字段的七种可选值,包括切、倒、搅拌、翻炒、煮、蒸、炖,这七种动画类型对应了烹饪过程中最常见的操作动作。tools字段限定了十二种可选工具,涵盖了中式厨房的基本装备。ingredients的type字段则分为主料、辅料和调料三类。通过在Prompt中预设这些枚举约束,我们有效地控制了AI输出的规范性,避免它生成不在预定义范围内的无效值。在调用DeepSeek API时,temperature参数被设置为0.1,这是一个较低的值,意味着AI的输出会更加确定和一致,减少随机性和创造性,适合需要精确结构化结果的场景。response_format参数指定为json_object,确保API返回的内容是合法可解析的JSON格式。拿到AI返回的字符串后,使用json.loads将其解析为Python字典,再包装成统一的响应格式返回给小程序端。
八、菜谱添加接口:接收前端数据并写入数据库
菜谱添加接口是整个数据流的终点,它接收小程序端提交的完整菜谱信息,包括菜名、描述文本、图片路径以及AI解析后的结构化JSON数据,然后将这些数据持久化存储到MySQL数据库中。这个接口的设计关键在于如何处理structured_data字段。前端在调用AI解析接口拿到JSON对象后,需要先将其序列化为字符串,再随其他字段一起提交。后端接收到这个字符串后,直接赋值给Food模型的structured_data字段即可,SQLAlchemy会自动将其存入数据库的Text类型字段中。
python
@app.route('/api/food/add', methods=['POST'])
def add_food():
data = request.get_json()
if not data.get('name'):
return jsonify({"code": 400, "msg": "菜名不能为空"})
food = Food(
name=data.get('name'),
desc=data.get('desc'),
image_url=data.get('image_url'),
structured_data=data.get('structured_data')
)
db.session.add(food)
db.session.commit()
return jsonify({"code": 200, "msg": "菜谱发布成功"})
这段接口代码遵循了典型的CRUD操作流程。首先通过request.get_json()获取前端以JSON格式提交的请求体,然后对必填字段name进行校验,如果菜名为空则直接返回错误。接着创建一个Food模型实例,将各个字段的值从请求数据中取出并赋值。db.session.add(food)将新创建的实例注册到当前数据库会话中,此时数据尚未真正写入磁盘。db.session.commit()执行事务提交,SQLAlchemy会在底层生成对应的INSERT SQL语句并发送给MySQL执行。提交成功后,food对象的id属性会被自动填充为数据库生成的自增主键值。这种先构建对象再统一提交的方式保证了数据的一致性,如果在提交过程中发生任何异常,整个事务会自动回滚,不会产生脏数据。
想要解锁更多小程序组件化封装、JSON 结构化菜谱解析、Lottie/GIF 动画适配、全栈项目落地实战干货、零基础入门避坑教程吗?
持续关注,后续将更新云端部署、跨端适配、样式统一美化、历史菜谱收藏功能等硬核内容,手把手带你吃透小程序全栈开发流程!