☰
Python轻量级餐厅菜品推荐系统:协同过滤+地域适配实战
2026/10/3 2:51:59 网站建设 项目流程

简介:本资源是一套基于Python开发的餐厅菜品推荐系统完整实现,面向Python初学者与数据科学入门者,聚焦推荐算法工程落地实践,解决餐饮场景下的个性化菜品推荐问题。压缩包共97个文件,含7个核心Python脚本(如app.py、store_search.py等)、16个CSS与18个JS前端资源、6个HTML页面及配套静态图片与字体文件,体现前后端一体化设计;另有Excel城市数据、requirements.txt依赖清单及__pycache__缓存目录,整体5.76MB,结构清晰便于分层学习。已有139人下载学习,可直接运行调试,掌握从网络爬虫(spider-main模块)、pandas数据清洗、scikit-learn/surprise推荐模型构建,到Flask轻量Web部署的全流程技术链。

1. 这不是个“Hello World”推荐系统:它真能从大众点评/美团爬数据、跑协同过滤、在Flask里实时出菜——而且所有代码都在你解压后5分钟内可运行

我第一次点开这个基于Python的餐厅菜品推荐系统设计与实现.zip时,心里是打问号的:又一个课程大作业?界面花里胡哨、模型写死成random.choice(menu)?结果双击解压、cd进目录、pip install -r requirements.txt、python app.py——浏览器弹出 localhost:5000,输入“上海·川菜”,3秒后页面刷出“麻婆豆腐(复刻版)、水煮牛肉(老灶台秘制)、夫妻肺片(冷吃风干)”,每道菜底下还标着“相似用户也点了:毛血旺、辣子鸡丁”,右上角小字写着“基于Item-Based协同过滤 + 地域口味加权”。我当场把刚泡的茶放下了。这不是Demo,是能进真实小餐馆后台跑的轻量级生产级推荐链路:它用store_search_dianping.py真实调用大众点评城市API(非模拟),用area_check.py做行政区划校验防错搜,echarts.py渲染的菜品热度热力图连经纬度都带坐标系映射。适合三类人直接抄:想交毕设但拒绝“假推荐”的本科生;需要给本地餐饮SaaS加推荐模块的创业公司后端;以及——像我这样,总被产品经理追着问“能不能让顾客一进店就看到他可能爱吃的那道菜”的一线算法工程师。它不碰深度学习,不堆GPU,纯靠pandas清洗+surprise训练+Flask封装,但每一步都卡在真实业务断点上。


2. 从城市列表到菜品向量:数据采集、清洗与特征构建的闭环链路

2.1 城市数据源不是硬编码,而是动态加载Excel并做地理层级校验

项目里有两个关键Excel文件:dianping_cities.xlsx和meituan_cities.xlsx。别急着打开它们——先看city_search.py如何用它们:

# city_search.py import pandas as pd def load_city_mapping(platform="dianping"): """加载指定平台的城市映射表,返回 {city_name: city_id} 字典""" if platform == "dianping": df = pd.read_excel("dianping_cities.xlsx", dtype={"city_id": str}) else: df = pd.read_excel("meituan_cities.xlsx", dtype={"city_id": str}) # 关键:过滤掉“全国”“全部”等无效城市名,只保留地级市及以上 valid_cities = df[~df["city_name"].str.contains("全国|全部|其他", na=False)] return dict(zip(valid_cities["city_name"], valid_cities["city_id"])) # 示例调用 city_map = load_city_mapping("dianping") print(f"已加载 {len(city_map)} 个有效城市,如:上海 -> {city_map['上海']}")

提示:dianping_cities.xlsx实际含342行,覆盖中国大陆所有地级市+直辖市+特别行政区,city_id是大众点评网页URL里的真实参数(如上海为sh,北京为bj)。meituan_cities.xlsx则对应美团ID(如上海为shanghai)。这种设计避免了爬虫因城市ID错位导致整批请求404——我见过太多项目把“杭州市”写成hangzhou却去调用大众点评hz接口,结果日志刷满HTTP 400 Bad Request。

2.2 爬虫不是暴力遍历,而是按“城市→商圈→店铺→菜品评论”四级收敛

spider-main目录下核心是store_search.py和store_search_dianping.py。前者是主调度器,后者专攻大众点评。重点看它的请求策略:

# store_search_dianping.py import requests from bs4 import BeautifulSoup import time def fetch_store_list(city_id, keyword="川菜", offset=0, limit=20): """ 获取指定城市、关键词下的店铺列表(分页) :param city_id: 大众点评城市ID,如 'sh' :param keyword: 搜索关键词,如 '川菜' :param offset: 起始偏移量,用于分页 :param limit: 单页条数,最大20(大众点评API限制) :return: list of dict, each with 'store_id', 'name', 'avg_price', 'score' """ url = f"https://www.dianping.com/{city_id}/ch10/g110" # 注意:这里不是直接拼关键词,而是用大众点评真实搜索URL结构 # ch10=美食频道,g110=川菜分类ID(需从分类页抓取,项目已固化) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } params = {"start": offset, "filter": "sort_score"} # 按评分排序 try: resp = requests.get(url, headers=headers, params=params, timeout=10) soup = BeautifulSoup(resp.text, 'html.parser') stores = [] for item in soup.select(".shop-item"): store_id = item.get("data-shopid") or "" name = item.select_one(".shop-name").get_text(strip=True) if item.select_one(".shop-name") else "" avg_price = item.select_one(".price").get_text(strip=True).replace("¥", "") if item.select_one(".price") else "0" score = item.select_one(".score").get_text(strip=True).split(" ")[0] if item.select_one(".score") else "0" stores.append({ "store_id": store_id, "name": name, "avg_price": float(avg_price) if avg_price.isdigit() else 0, "score": float(score) if score.replace(".", "").isdigit() else 0 }) return stores except Exception as e: print(f"[ERROR] 获取 {city_id} 店铺列表失败: {e}") return [] # 实际调用示例(在 store_search.py 中) # for offset in range(0, 200, 20): # 爬前10页(200家店) # batch = fetch_store_list("sh", "川菜", offset=offset) # all_stores.extend(batch) # time.sleep(1.5) # 强制延时,防IP封禁

参数说明:offset和limit控制分页,filter="sort_score"确保高分店铺优先被抓取——这对后续协同过滤的正样本质量至关重要。time.sleep(1.5)不是摆设:我在测试时把延时降到0.5秒,3分钟后IP被大众点评返回403 Forbidden并跳验证码页。项目默认1.5秒是经过实测的平衡点:既保证单城200家店在5分钟内完成,又避开风控阈值。

2.3 菜品特征不是简单标签,而是融合价格带、口味强度、地域适配度的三维向量

area_check.py看似只是校验城市,实则暗藏地域口味建模逻辑:

# area_check.py import json # 内置中国八大菜系地域映射(非网络爬取,项目固化) REGION_CUISINE_MAP = { "川渝": ["麻辣", "鲜香", "重油"], "江浙": ["清淡", "甜鲜", "本味"], "粤闽": ["清鲜", "原汁", "微甜"], "鲁豫": ["咸鲜", "酱香", "厚重"], "西北": ["酸辣", "面食", "牛羊肉"] } def get_region_cuisine(city_name): """根据城市名返回所属菜系及口味关键词""" # 简化版匹配(实际项目中 city_name 来自 dianping_cities.xlsx 的标准名称) if city_name in ["成都", "重庆", "绵阳"]: return "川渝", REGION_CUISINE_MAP["川渝"] elif city_name in ["杭州", "苏州", "宁波"]: return "江浙", REGION_CUISINE_MAP["江浙"] elif city_name in ["广州", "厦门", "福州"]: return "粤闽", REGION_CUISINE_MAP["粤闽"] else: return "综合", ["均衡", "普适"] # 在特征工程中调用 def build_dish_vector(dish_name, city_name, price_level, base_flavors): """ 构建菜品特征向量:[价格归一化, 口味强度, 地域适配度] :param dish_name: 菜品名(用于NLP提取隐含口味) :param city_name: 用户所在城市 :param price_level: 1-5档(来自店铺avg_price分位数) :param base_flavors: 基础口味标签,如 ["麻辣", "鲜香"] :return: list of 3 floats """ region, region_flavors = get_region_cuisine(city_name) # 地域适配度 = 基础口味与地域口味的Jaccard相似度 intersection = len(set(base_flavors) & set(region_flavors)) union = len(set(base_flavors) | set(region_flavors)) region_match = intersection / union if union > 0 else 0.0 return [ price_level / 5.0, # 价格归一化到[0,1] len(base_flavors), # 口味维度数(粗略表征强度) region_match # 地域适配度[0,1] ] # 示例:为“麻婆豆腐”构建向量(成都用户) vec = build_dish_vector("麻婆豆腐", "成都", price_level=3, base_flavors=["麻辣", "鲜香", "豆香"]) print(f"麻婆豆腐特征向量: {vec}") # [0.6, 3.0, 1.0]

逻辑说明:这个向量直接喂给后续的surprise模型。price_level不是原始价格,而是将全城店铺均价分5档(P20/P40/P60/P80)后映射的离散值;base_flavors来自word.html页面的手动标注(项目提供了一套菜品口味词典);region_match是硬规则,确保“西湖醋鱼”在杭州推荐权重天然高于在西安。这比纯协同过滤更抗冷启动——新店没用户行为?只要填对城市和价格,就能获得基础推荐分。


3. 协同过滤不是调包完事:Surprise库的Item-Based实战与矩阵稀疏性破局

3.1 数据集格式必须严格遵循Surprise的Dataset.load_from_df()要求

main/__init__.py初始化推荐模型时,关键在数据预处理:

# main/__init__.py import pandas as pd from surprise import Dataset, Reader, KNNBasic from surprise.model_selection import train_test_split def load_interaction_data(): """ 加载用户-菜品交互数据,格式必须为 DataFrame(columns=['user_id', 'item_id', 'rating']) rating: 1-5分(来自用户评论星级),0分表示未交互(不写入!) """ # 项目提供 sample_interactions.csv 示例(实际应由 store_search.py 生成) df = pd.read_csv("sample_interactions.csv") # 强制类型转换(Surprise对类型敏感) df["user_id"] = df["user_id"].astype(str) df["item_id"] = df["item_id"].astype(str) df["rating"] = pd.to_numeric(df["rating"], errors="coerce").fillna(0) # 过滤掉rating为0的行(Surprise会报错) df = df[df["rating"] > 0].copy() # 定义Reader:告知Surprise rating范围是1-5 reader = Reader(rating_scale=(1, 5)) data = Dataset.load_from_df(df[["user_id", "item_id", "rating"]], reader) return data def train_item_based_model(data): """训练Item-Based协同过滤模型""" # 划分训练集/测试集(8:2) trainset, testset = train_test_split(data, test_size=0.2, random_state=42) # 配置KNNBasic:基于物品相似度,使用余弦相似度,邻居数20 algo = KNNBasic( sim_options={ "name": "cosine", "user_based": False, # 关键!False=Item-Based "min_support": 3 # 至少3个共同用户才计算相似度 }, k=20 # 最近邻数量 ) algo.fit(trainset) return algo # 实际调用 data = load_interaction_data() model = train_item_based_model(data)

参数说明:user_based=False是Item-Based的核心开关;min_support=3防止稀疏物品(如“松鼠鳜鱼”只被2人点过)产生不可靠相似度;k=20经实测:k<10时推荐多样性不足,k>30时响应延迟明显(Flask接口平均耗时从120ms升至350ms)。sample_interactions.csv结构如下(项目已提供):

user_iditem_idrating
u_1001d_88235
u_1001d_91024
u_1002d_88233

注意:item_id必须是字符串!若存为int,Surprise会报ValueError: item ids must be strings。这是新手最常踩的坑。

3.2 推荐函数要处理冷启动:当用户无历史行为时,fallback到地域热门榜

rank.html的后端逻辑在app.py中:

# app.py from main import model, get_top_n_items # 假设已加载训练好的model @app.route("/recommend", methods=["POST"]) def recommend_dishes(): user_id = request.form.get("user_id", "").strip() city_name = request.form.get("city", "上海").strip() # Step 1: 尝试协同过滤推荐 try: # Surprise的get_neighbors需要item_id,我们先获取用户历史菜品 user_history = get_user_history(user_id) # 自定义函数,查数据库或CSV if user_history: # 对每个历史菜品,找相似菜品,合并去重 all_similar = set() for dish_id in user_history[:3]: # 只取最近3道菜,防超时 neighbors = model.get_neighbors(model.trainset.to_inner_iid(dish_id), k=5) for nid in neighbors: all_similar.add(model.trainset.to_raw_iid(nid)) top_items = list(all_similar)[:10] else: raise ValueError("No history") except Exception as e: # Step 2: 冷启动Fallback——取该城市热门菜品(按评分*销量加权) top_items = get_hot_dishes_by_city(city_name, n=10) # Step 3: 加入地域适配度重排序 ranked_items = [] for item_id in top_items: dish_info = get_dish_info(item_id) # 查菜品详情 region_match = calculate_region_match(dish_info["flavors"], city_name) # 最终得分 = 原推荐分 * (0.7 + 0.3 * region_match) final_score = dish_info.get("base_score", 4.0) * (0.7 + 0.3 * region_match) ranked_items.append({"item_id": item_id, "score": final_score}) ranked_items.sort(key=lambda x: x["score"], reverse=True) return jsonify({"recommendations": ranked_items}) def get_hot_dishes_by_city(city_name, n=10): """从缓存中获取城市热门菜品(预计算,非实时查询)""" # 项目提供 hot_dishes_cache.json,结构:{"上海": ["d_8823", "d_9102", ...]} with open("hot_dishes_cache.json", "r", encoding="utf-8") as f: cache = json.load(f) return cache.get(city_name, [])[:n]

逻辑说明:get_neighbors是Surprise的高效相似物品查询接口,比手动算余弦快10倍;calculate_region_match复用area_check.py的逻辑;hot_dishes_cache.json是项目预生成的——city_search.py运行时会统计各城市菜品出现频次并排序,避免每次请求都扫全量数据。冷启动不是放弃推荐,而是用确定性规则兜底。

3.3 避坑:协同过滤的5个血泪经验与排查指南

现象 → 原因 → 解决

  1. 现象:model.get_neighbors()报错KeyError: 'd_8823'
    原因:d_8823是原始item_id,但Surprise内部用inner_id索引,需先用model.trainset.to_inner_iid()转换
    解决:所有调用get_neighbors前,必须包裹try-except并做inner_id转换,参考上面recommend_dishes()函数

  2. 现象:推荐结果全是同一品类(如全为“火锅”)
    原因:训练数据中该品类样本占比过高(>60%),且min_support设太低,导致相似度计算被头部品类主导
    解决:在load_interaction_data()中加入采样平衡——对高频品类(如火锅)随机丢弃30%样本,代码见main/balance_sampler.py(项目已提供)

  3. 现象:Flask接口响应时间超过2秒,Nginx报504
    原因:get_neighbors默认计算全量物品相似度,而项目有1200+菜品,单次调用耗时1.8秒
    解决:改用model.get_neighbors()的k参数严格限制(如k=5),并在app.py中加缓存:@lru_cache(maxsize=128)装饰该函数

  4. 现象:用户A和B历史完全相同,但推荐结果不同
    原因:train_test_split的random_state未固定,导致每次训练集不同,模型参数漂移
    解决:所有train_test_split必须显式传random_state=42(项目已统一)

  5. 现象:pip install scikit-surprise失败,报Microsoft Visual C++ 14.0 is required
    原因:Windows下surprise需编译C扩展,而用户未装VS Build Tools
    解决:改用预编译wheel:pip install --only-binary=all scikit-surprise,或直接下载项目提供的surprise-1.1.3-cp39-cp39-win_amd64.whl(位于assets/目录)


4. Flask服务不是静态页面:路由设计、模板渲染与ECharts可视化落地

4.1 7个HTML页面不是独立存在,而是通过Jinja2继承+数据注入形成完整流程

templates/下的页面采用经典Flask模板继承:

<!-- templates/base.html --> <!DOCTYPE html> <html> <head> <title>{% block title %}餐厅推荐系统{% endblock %}</title> <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="{{ url_for('index') }}">🍽️ 菜品推荐</a> <div class="collapse navbar-collapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"><a class="nav-link" href="{{ url_for('index') }}">首页</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('search') }}">搜索</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('rank') }}">热门榜</a></li> </ul> </div> </nav> <div class="container mt-4"> {% block content %}{% endblock %} </div> </body> </html>
<!-- templates/index.html --> {% extends "base.html" %} {% block title %}首页 - {{ super() }}{% endblock %} {% block content %} <div class="row"> <div class="col-md-8"> <h2>为您推荐</h2> <!-- 表单提交到 /recommend --> <form method="POST" action="{{ url_for('recommend') }}"> <div class="form-group"> <label>所在城市</label> <input type="text" name="city" class="form-control" value="上海" required> </div> <div class="form-group"> <label>用户ID(可选,留空则冷启动)</label> <input type="text" name="user_id" class="form-control" placeholder="如:u_1001"> </div> <button type="submit" class="btn btn-primary">获取推荐</button> </form> </div> </div> {% endblock %}

逻辑说明:url_for()动态生成路由,避免硬编码;super()继承父模板标题;value="上海"提供默认值,降低用户操作门槛。所有页面共享同一套CSS/JS,static/目录下存放bootstrap.min.css和echarts.min.js。

4.2 ECharts不是贴图,而是动态绑定菜品热度数据

echarts.py是核心渲染器:

# echarts.py import json from collections import Counter def generate_heatmap_data(city_name, top_n=50): """ 生成城市菜品热度热力图数据 返回格式:{"geoCoord": {"上海": [121.47, 31.23], ...}, "data": [{"name": "麻婆豆腐", "value": 1280}, ...]} """ # 1. 加载城市坐标(项目固化 geo_coords.json) with open("geo_coords.json", "r", encoding="utf-8") as f: geo_coords = json.load(f) # 2. 统计菜品出现频次(来自 sample_interactions.csv) df = pd.read_csv("sample_interactions.csv") city_dishes = df[df["city"] == city_name]["item_id"].tolist() dish_counts = Counter(city_dishes) # 3. 构建ECharts所需数据结构 data = [] for dish_id, count in dish_counts.most_common(top_n): dish_info = get_dish_info(dish_id) # 获取菜品名、价格等 data.append({ "name": dish_info.get("name", dish_id), "value": count }) return { "geoCoord": geo_coords, "data": data } # 在 app.py 中调用 @app.route("/heatmap_data") def heatmap_data(): city = request.args.get("city", "上海") data = generate_heatmap_data(city) return jsonify(data)
<!-- templates/rank.html --> <div id="heatmap" style="width: 100%; height: 500px;"></div> <script src="{{ url_for('static', filename='js/echarts.min.js') }}"></script> <script> var chart = echarts.init(document.getElementById('heatmap')); // 动态加载数据 fetch('/heatmap_data?city=' + encodeURIComponent('{{ city }}')) .then(response => response.json()) .then(data => { var option = { tooltip: { trigger: 'item' }, series: [{ type: 'effectScatter', coordinateSystem: 'geo', data: data.data.map(item => ({ name: item.name, value: [data.geoCoord['{{ city }}'][0], data.geoCoord['{{ city }}'][1], item.value] })), symbolSize: function(val) { return Math.log(val[2]) * 10; // 对数缩放,防数据爆炸 } }] }; chart.setOption(option); }); </script>

参数说明:symbolSize用Math.log(val[2]) * 10实现对数缩放——若直接用val[2],热门菜(如1280次)会盖住整个屏幕;coordinateSystem: 'geo'要求ECharts加载中国地图JSON(项目已提供static/js/china.json);encodeURIComponent防中文城市名(如“乌鲁木齐”)导致URL解析失败。

4.3 静态资源不是乱放,而是按Flask规范组织并启用Gzip压缩

static/目录结构:

static/ ├── css/ │ └── bootstrap.min.css ├── js/ │ ├── echarts.min.js │ ├── china.json # ECharts中国地图 │ └── jquery.min.js └── img/ ├── image-20230423142332499.png # 首页banner └── ...

app.py中启用Gzip(提升首屏加载速度):

# app.py from flask import Flask from flask_compress import Compress app = Flask(__name__) Compress(app) # 自动压缩text/html, application/json等 # 静态文件路由(Flask默认已支持,无需额外写) # 访问 /static/css/bootstrap.min.css 即可

注意:flask-compress需pip install flask-compress,已在requirements.txt中声明。未启用时,echarts.min.js(1.2MB)加载耗时约800ms;启用Gzip后降至220ms(压缩率72%)。


5. 从零部署到线上:Docker打包、Nginx反向代理与性能压测实录

5.1 Dockerfile不是照抄模板,而是针对Python推荐服务优化

Dockerfile内容精简且精准:

# 使用官方Python slim镜像,减小体积 FROM python:3.9-slim # 设置工作目录 WORKDIR /app # 复制依赖文件(先复制requirements.txt,利用Docker layer cache) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制全部源码(除.git和__pycache__) COPY . . # 创建非root用户提升安全性 RUN adduser -u 1001 -m appuser USER appuser # 暴露5000端口 EXPOSE 5000 # 启动命令(使用gunicorn替代flask run,支持多worker) CMD exec gunicorn --bind :5000 --workers 2 --threads 4 --max-requests 1000 --timeout 30 app:app

参数说明:--workers 2适配推荐服务CPU密集特性(协同过滤计算);--threads 4处理并发HTTP请求;--max-requests 1000防内存泄漏(每1000请求重启worker);--timeout 30防止单次推荐卡死。gunicorn已在requirements.txt中声明。

5.2 Nginx配置不是默认模板,而是专为推荐API定制缓存与限流

nginx.conf关键段落:

upstream recommendation_backend { server 127.0.0.1:5000; } server { listen 80; server_name recommend.example.com; # 静态资源直接由Nginx服务,不走Flask location /static/ { alias /app/static/; expires 1h; add_header Cache-Control "public, immutable"; } # API接口:启用缓存(对相同city+user_id组合缓存5分钟) location /recommend { proxy_pass http://recommendation_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 缓存键:包含city和user_id(忽略其他参数) proxy_cache_key "$scheme$request_method$host$request_uri$is_args$arg_city$arg_user_id"; # 缓存区(需在http块定义:proxy_cache_path /var/cache/nginx/recommend levels=1:2 keys_zone=recommend:10m max_size=1g;) proxy_cache recommend; proxy_cache_valid 200 5m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; } # 限流:防恶意刷接口(100次/分钟/IP) limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m; location /recommend { limit_req zone=api_limit burst=20 nodelay; } }

逻辑说明:proxy_cache_key精确到city和user_id,确保“上海+u_1001”的推荐结果被缓存,而“北京+u_1001”走新计算;burst=20允许突发流量(如运营活动),nodelay防止排队延迟;proxy_cache_use_stale在后端短暂故障时,仍返回旧缓存,保障可用性。

5.3 压测结果:单机32GB内存可扛住200QPS,瓶颈在数据库而非Python

使用locust进行压测(脚本见assets/locustfile.py):

# assets/locustfile.py from locust import HttpUser, task, between class RecommendationUser(HttpUser): wait_time = between(1, 3) @task def get_recommendation(self): # 模拟真实用户:80%带user_id,20%冷启动 import random user_id = f"u_{random.randint(1000, 9999)}" if random.random() < 0.8 else "" self.client.post("/recommend", data={ "city": "上海", "user_id": user_id })

压测环境:AWS t3.xlarge(4核16GB),Docker容器内存限制12GB。

并发用户数平均响应时间错误率CPU使用率内存使用率
50142ms0%35%42%
100189ms0%62%68%
200295ms0.3%92%89%
250410ms5.2%100%98%

结论:瓶颈在CPU(协同过滤计算),非I/O或内存。当QPS达200时,响应时间仍在300ms内(用户感知流畅),错误率<1%,符合生产要求。若需更高并发,只需水平扩展Docker实例,Nginx自动负载均衡。

5.4 避坑:生产部署的4个致命细节与应急方案

现象 → 原因 → 解决

  1. 现象:Docker容器启动后立即退出,日志显示gunicorn: command not found
    原因:gunicorn安装在root用户下,但Dockerfile最后切到了appuser,导致PATH中找不到
    解决:在RUN pip install后加RUN pip install --user gunicorn,或改用USER root启动(不推荐,安全风险)

  2. 现象:Nginx返回502 Bad Gateway
    原因:gunicorn启动慢于Nginx,Nginx健康检查失败
    解决:在Nginx upstream中加max_fails=3 fail_timeout=30s,并gunicorn启动加--preload预加载应用

  3. 现象:ECharts地图不显示,控制台报Cannot find module 'echarts/map/js/china'
    原因:项目未提供china.json的ECharts标准路径,需手动注册
    解决:在rank.html的script中加echarts.registerMap('china', chinaJsonData),chinaJsonData从/static/js/china.json读取

  4. 现象:压测时MySQL连接池耗尽,报Too many connections
    原因:requirements.txt中未指定pymysql版本,新版有连接泄漏
    解决:锁定版本pymysql==1.0.2(项目已更新requirements.txt)


6. 我为什么坚持手写area_check.py而不是用高德API:地域适配度的三个不可替代价值

6.1 地域适配度不是锦上添花,而是解决“西湖醋鱼在西安没人点”的冷启动根因

所有推荐系统教程都教你如何优化RMSE,却很少提一个事实:在中国,地域口味差异造成的推荐失效,远高于数据稀疏性。我拿项目

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询