简介:基于Django框架与多模态知识图谱技术实现的智能旅游推荐系统完整源码包,提供个性化旅游目的地推荐服务,适合计算机科学与技术、数据科学、人工智能、物联网工程等专业师生用于课程实践、毕业设计或科研项目开发。资源包共201个文件,整体约201.94MB,涵盖14个py源码、11个pyc编译文件,8个html、8个css与8个js构成的前端界面,47个jpg和41个png图片素材,5个xls数据表格、3个mp4演示视频,以及zbak备份、字体和json配置等辅助内容,结构完整可直接部署运行。已有63人学习浏览。系统采用模块化设计,代码注释规范,预留充分的功能扩展接口,既支持直接部署运行现有版本,也便于个性化二次开发。通过本项目实践,可深入理解Django框架在企业级应用开发中的实施方法,以及知识图谱融合用户偏好、景点特征、时空关系等多源信息实现智能推荐的完整路径。
1. 智能旅游推荐系统:为什么这次我选了 Django + 知识图谱这条路
做毕业设计最怕的不是功能太少,而是功能做出来之后讲不清楚“它为什么能工作”。我在拆这套基于 Django 与多模态知识图谱的智能旅游推荐系统源码时,最直观的感受是:它把推荐这件事从“黑匣子”变成了可回溯的图路径。景点、用户、评论、标签、城市、类别都作为节点存进图数据库,推荐就是在这张图上走路径。这对打算用 Python 做毕业设计、又想把答辩讲透的人来说,是很好的参考模板。它解决两类问题:冷启动——新用户没有评分也能靠标签路径拿到候选集;可解释性——每一条推荐都能给出完整的关联链路。适合需要完整 Django 后端、数据库建模和图查询经验的开发者和学生。
2. 架构与数据模型:把多模态信息拆成关系表和图节点
2.1 项目目录:四个 App 划分出清晰的模块边界
拿到源码先看目录结构,这是最快摸清项目脾气的方式。这套工程的 manage.py 位于项目根目录下,应用拆成了四个:users 管用户与登录注册,scenic 管景点、图片和评论,rec 管推荐接口与算法编排,graph 管 Neo4j 的模型映射与查询封装。边界划分的依据是数据源:业务数据走 MySQL,图谱数据走 Neo4j,graph 是唯一允许写 Cypher 的应用,views 层不直接碰图数据库。
# 初始化项目与四个应用的命令序列 django-admin startproject travel_recommend . python manage.py startapp users python manage.py startapp scenic python manage.py startapp rec python manage.py startapp graph命令里的.表示在当前目录生成 settings.py 和 urls.py,不额外嵌套一层同名目录,后续部署时路径更好记。startproject 之后要做的第一件事是把四个应用注册进 INSTALLED_APPS,顺序是 users、scenic、rec、graph——这直接影响 makemigrations 的扫描顺序,顺序不对可能导致部分应用的表不被识别。四个应用都控制在三百行代码以内,说明模块划分粒度是合适的,单文件过大就该警惕了。
| 应用名 | 职责边界 | 是否直接访问图数据库 | |----------|------------------------------|---------------------| | users | 用户注册、登录、偏好标签选择 | 否 | | scenic | 景点、图片、评论的数据维护 | 否 | | rec | 推荐接口与排序算法编排 | 否 | | graph | Neo4j 连接、Cypher 查询封装 | 是 |graph 是唯一允许写 Cypher 的应用,其他应用要查图谱都走 graph.services 对外暴露的函数。这套源码的配置里还做了一件对毕业设计和课程设计都很实用的事:用环境变量拆分配置。settings.py 里数据库密码和 Django SECRET_KEY 不写死,而是从 os.environ.get() 读取,本地开发用 .env 文件兜底。这样源码公开或传给别人时,不用改代码,只需要复制一份 .env.example 改成 .env。安全上避免了密钥泄露,部署到服务器时也不用动业务代码。
2.2 业务表设计:五张表把景点、用户、评论和图像串起来
关系型数据库这部分,核心是五张表:用户表、景点表、景点图表、评论表和用户偏好表。多模态在这里的体现,是把图片和评论作为独立实体建模,而不是在景点表里堆一堆 TextField。我见过不少项目把图片地址用逗号拼成一个字段,查询时再 split,这套源码没这么干,它用 ScenicImage 单独成表,一个景点对多张图片,feature_vector 字段直接存图像特征向量。
# scenic/models.py 核心模型,字段按线上运行版本精简 from django.db import models class Scenic(models.Model): name = models.CharField(max_length=128, verbose_name="景点名称") city = models.CharField(max_length=64, db_index=True, verbose_name="城市") category = models.CharField(max_length=64, verbose_name="景点类别") latitude = models.DecimalField(max_digits=9, decimal_places=6, verbose_name="纬度") longitude = models.DecimalField(max_digits=9, decimal_places=6, verbose_name="经度") rating = models.FloatField(default=0.0, verbose_name="平均评分") intro = models.TextField(verbose_name="景点简介") class ScenicImage(models.Model): scenic = models.ForeignKey(Scenic, on_delete=models.CASCADE, related_name="images") image_url = models.URLField(verbose_name="图片地址") feature_vector = models.JSONField(default=list, verbose_name="图像特征向量") class Comment(models.Model): user = models.ForeignKey("users.UserProfile", on_delete=models.CASCADE) scenic = models.ForeignKey(Scenic, on_delete=models.CASCADE, related_name="comments") content = models.TextField(verbose_name="评论内容") sentiment_score = models.FloatField(default=0.0, verbose_name="情感得分")这段代码里有两个值得留意的参数。Scenic.rating 是冗余字段,推荐排序时需要频繁按平均评分过滤,如果每次查询都走聚合计算,接口压力会成倍增长,所以写入时同步维护一个均值。ScenicImage.feature_vector 用 JSONField 存储图像模型输出的向量,推荐阶段直接取出计算余弦相似度,不用再调一次图像接口,这是多模态融合落到工程上的关键点。这个设计同时也是血泪经验换来的——早期版本把图片地址拼成逗号字符串,序列化时每次都要 split,后来单独成表才把这块逻辑理顺。
围绕这五张表,源码里还配了一套序列化和视图。scenic 相关的接口用的是 Django REST Framework 的 ModelSerializer,Comment 序列化时嵌套返回用户名和景点名,因为前端详情页需要同时展示内容和归属地,不嵌套就得在前端二次请求,体验差一截。用 ViewSet 加 router 注册的方式组织路由,URL 结构是 /api/scenic/ 和 /api/scenic/{id}/,既满足 RESTful 风格,也方便 Postman 直接测。
2.3 图谱模型:六类节点五类关系,把路径变成推荐依据
MySQL 存的是实体属性,Neo4j 存的是语义关系。图模型设计是本系统的重头戏:User、Scenic、Tag、City、Category、Comment 六类节点,PREFER、LOCATED_IN、HAS_TAG、BELONGS_TO、WRITE 五类关系。设计原则很朴素——推荐时需要走的路径,都要预先变成显式关系,查询时才能一条 Cypher 走到底。
// graph/init_schema.cypher 初始化约束和三类核心关系 CREATE CONSTRAINT user_id IF NOT EXISTS ON (u:User) ASSERT u.uid IS UNIQUE; CREATE CONSTRAINT scenic_id IF NOT EXISTS ON (s:Scenic) ASSERT s.sid IS UNIQUE; MATCH (u:User {uid: $uid}), (s:Scenic {sid: $sid}) CREATE (u)-[:PREFER {weight: $weight}]->(s); MATCH (c:City {name: $city}), (s:Scenic {sid: $sid}) CREATE (s)-[:LOCATED_IN]->(c); MATCH (t:Tag {name: $tag}), (s:Scenic {sid: $sid}) CREATE (s)-[:HAS_TAG]->(t);这里约束的命名用了_id后缀,Neo4j 的约束和索引是两回事:约束保证 uid 和 sid 唯一,同时自动带索引,所以查询时能走 index lookup 而不是全表扫描。PREFER 关系带 weight 属性,表示用户对景点的偏好强度,来源可以是显式评分,也可以由行为日志折算。City 和 Category 是作为中间节点存在的,它们存在的意义是让推荐可以沿着“用户喜欢某类景点 → 这类景点分布在哪些城市 → 城市里还有哪些同类景点”这条路径去扩展候选集。
写 Django 代码时,graph 应用里封装了一个 Neo4jClient 类,负责连接管理和 Cypher 执行。所有图查询都不在 views 里出现,统一收敛到 graph/services.py,views 拿到的是处理好的推荐候选集。这个分层习惯很关键——图数据库的 Cypher 调优成本和关系型 SQL 完全不同,集中管理才能控制住复杂度。连接参数按 bolt://localhost:7687 配置,用户名密码通过模块级常量读取,换库时只改一处。
3. 数据入库与图谱构建:从 CSV 到 Cypher 的完整通路
3.1 数据预处理:多模态字段的清洗与对齐
这套源码自带了一条可复现的数据管道:原始 CSV 放 data/raw/,包含景点表、评论表、图片表三份文件,清洗脚本输出 data/clean/。清洗的重点不是简单去重,而是多模态字段的对齐——景点的 id 要和评论里的 scenic_id 一致,图片表的 scenic_id 要能从景点表里找到对应记录。对不上的行在入库阶段会产生外键错误,这一步能拦下一大半。
# data/preprocess.py 清洗与对齐逻辑 import pandas as pd def clean_scenic(path): df = pd.read_csv(path) df = df.drop_duplicates(subset=["name"]) df = df[df["rating"].between(0, 5)] df = df[df["latitude"].between(20, 50) & df["longitude"].between(80, 130)] df = df.dropna(subset=["name", "city", "category"]) return df这段脚本在项目里负责把脏数据挡在入库之前。rating 过滤掉越界值,经纬度过滤掉明显错误坐标,dropna 确保必填字段不为空。参数 between(20, 50) 和 between(80, 130) 是按国内景点坐标范围定的,如果你换数据集,要按自己的地域范围调。drop_duplicates 用 name 做去重键,这套数据里景点名唯一,实际项目如果存在同名不同地点,就要改成 name 加 city 的组合键。
评论数据清洗时要注意编码问题。pandas 读 CSV 默认猜编码,Windows 导出的文件常常是 GBK,Linux 下直接读就乱码。这套源码统一用 utf-8-sig 读入再转存,utf-8-sig 会吃掉开头的 BOM,比 utf-8 更稳。评论里还要做情感分数的预计算,这一步可以用简单的词典打分,也可以调现成的情感分析库,源码把结果直接落进 sentiment_score 字段,入库后就不再跑模型。
3.2 Django 管理命令:把清洗结果批量写进 MySQL
批量入库没有写在 views 里,而是做成了 Django 管理命令,这是这套源码值得抄的地方。python manage.py import_scenic、import_comment、import_image 三条命令各干各的,执行完有明确输出。管理命令的好处是能复用 Django 的 ORM 和配置环境,不需要额外写连接代码。
# scenic/management/commands/import_scenic.py import csv from django.core.management.base import BaseCommand from django.db import transaction from scenic.models import Scenic class Command(BaseCommand): help = "从清洗后的 CSV 导入景点数据" def add_arguments(self, parser): parser.add_argument("--csv", type=str, default="data/clean/scenic.csv") @transaction.atomic def handle(self, *args, **options): path = options["csv"] with open(path, encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: Scenic.objects.update_or_create( name=row["name"], defaults={ "city": row["city"], "category": row["category"], "latitude": row["latitude"], "longitude": row["longitude"], "rating": row["rating"], "intro": row["intro"], }, ) self.stdout.write(self.style.SUCCESS(f"景点导入完成: {path}"))update_or_create 是这里的关键写法。按 name 查找,存在就更新属性,不存在就创建,重复执行脚本不会产生脏数据,这是导入脚本的后悔药。transaction.atomic 把整批操作包在一个事务里,中途出错全部回滚,不会出现导了一半、下次再导直接冲突的情况。参数 --csv 有默认值,但允许手动指定路径,数据放在不同目录时不用改代码。
这个脚本跑完后,景点数据就进了 MySQL。评论和图片的导入脚本结构类似,区别在于评论要按 user 和 scenic 的关联去 set,图片要附带 feature_vector 字段,它是一串 JSON 数组,如果来源是图像模型输出的 numpy 数组,记得先调 .tolist() 再入库,否则会序列化失败。三个脚本都执行完毕后,MySQL 部分的数据基础就到位了,景点的增删改查也就全走 Django ORM,不需要再手写 SQL。
3.3 Cypher 导入:把关系写进 Neo4j
图数据库的导入和关系型不一样。MySQL 是表驱动,Neo4j 是节点和关系驱动,导入时要分两步:先建节点,再建关系。节点不存在时直接建关系会报错,所以源码里用了 MERGE 而不是 CREATE,MERGE 的语义是按属性匹配,存在就不重复创建。
// graph/load_data.cypher 参数化导入的核心语句 UNWIND $scenics AS s MERGE (scenic:Scenic {sid: s.sid}) SET scenic.name = s.name, scenic.city = s.city UNWIND $relations AS rel MATCH (u:User {uid: rel.uid}), (s:Scenic {sid: rel.sid}) MERGE (u)-[p:PREFER]->(s) SET p.weight = rel.weight这里用 UNWIND 把参数列表展开成行,一次请求处理一批数据,而不是一条条发 Cypher,性能差距在几千条数据时就能体现出来。参数 $scenics 和 $relations 由 Python 驱动传入,类型是列表套字典。MERGE 之后的 SET 用于补属性,因为 MERGE 只保障节点存在,不保障属性完整。实践中我一般把批大小控制在 2000 条以内,单批太大容易触发事务超时。
Python 侧导入时用的是 neo4j 官方驱动,GraphDatabase.driver 建立连接,session.execute_write 提交写事务。连接串和认证信息统一从配置读取。导入完成后可以跑一条验证语句,统计节点数和关系数,和 CSV 行数做对比,能快速判断有没有丢数据。这一步是图谱构建的收尾,跑完这张图才算真正建起来,后面推荐算法才有东西可查。
4. 推荐算法实现:图谱路径评分与多模态特征融合
4.1 选型理由:为什么不是纯协同过滤
毕业设计里最常见的推荐实现是协同过滤,计算用户相似度或者物品相似度,但这套源码选了图谱路径推荐作为主路径。原因很实际:旅游场景的数据稀疏得厉害,一个用户可能只去过三五个景点,物品相似度矩阵大部分是零,协同过滤的相似度算出来没有区分度。而知识图谱的优势在于,即使交互记录很少,用户只要关联了偏好标签,就能顺着标签节点找到一批同类型景点。
这个选型带来的副产品是可解释性。协同过滤给用户推荐“因为相似用户也喜欢”,这句话在答辩时很难展开;图谱路径推荐能直接说“因为你喜欢古镇类景点,而西塘和乌镇共享古镇标签,所以推荐西塘”。每一跳路径都是可视化的,这正好是知识图谱的价值点。系统也没有完全抛弃协同过滤,它把标签相似和图像相似作为打分特征融进排序阶段,而不是单独跑一套召回。
4.2 候选集生成:一条 Cypher 挖出用户可能喜欢的景点
候选集生成对应的是召回环节,目标是从全量景点里筛出几十个可能相关的条目。代码里用的是两跳路径:用户偏好过的景点,它的标签和类别,反查同标签或同类别的其他景点。
# graph/services.py 中候选集生成的 Cypher MATCH (u:User {uid: $uid})-[:PREFER]->(liked:Scenic) MATCH (liked)-[:HAS_TAG]->(tag:Tag)<-[:HAS_TAG]-(candidate:Scenic) WHERE candidate.sid <> liked.sid RETURN candidate.sid AS sid, count(DISTINCT tag) AS tag_hit ORDER BY tag_hit DESC LIMIT $limit这条查询的核心是 WHERE candidate.sid <> liked.sid,把用户已经去过的景点排除掉,避免推荐已经消费过的内容。count(DISTINCT tag) 统计候选景点和偏好景点共享的标签数,共享标签越多,路径上的语义相似度越高。LIMIT 参数控制在 50 左右,候选集不需要太大,太多会拖慢排序阶段的速度。
这里的图谱路径没有限定死跳数,如果你想扩展召回面,可以把路径延长到三跳,比如通过 City 节点做城市内扩散。但路径越长噪声越大,两跳在旅游场景下是召回率和精确率的折中。源码里这步结果只保留 sid 和 tag_hit 两个字段,排序前不急着取景点详情,避免大批量 ORM 查询拖慢接口。
Cypher 执行完成后,Python 侧拿到候选 sid 列表,再去 MySQL 查出对应的景点对象。这里有个顺序讲究:先在图数据库里缩小范围,再用关系型数据库取详情,反过来做会先把全表加载一遍,性能直接翻车。这个“图谱召回、MySQL 补全”的配合模式,是这套源码里最值得沿用的工程手法。
4.3 多模态特征打分:把图片、评论、标签融合成排序分数
候选集有了,接下来是排序。这套实现把分数拆成四个维度:标签命中数、图像特征相似度、评论情感得分、景点热度。四个维度的权重在源码里是标签 0.4、图像 0.3、情感 0.2、热度 0.1,权重放在配置项里,方便调参。
# rec/services.py 多模态融合打分核心逻辑 import numpy as np from django.db.models import Avg, Count from scenic.models import Scenic, ScenicImage, Comment def score_candidate(sid, tag_hit_normalized, liked_vectors): scenic = Scenic.objects.get(pk=sid) comment_count = min(scenic.comments.aggregate(c=Count("id"))["c"], 500) heat = scenic.rating * 0.6 + comment_count / 500 * 0.4 image_feat = ScenicImage.objects.filter(scenic=sid).first() img_sim = 0.0 if image_feat and liked_vectors: vec = np.array(image_feat.feature_vector) target = np.mean(np.array(liked_vectors), axis=0) img_sim = float(np.dot(vec, target) / (np.linalg.norm(vec) * np.linalg.norm(target) + 1e-8)) sentiment = Comment.objects.filter(scenic=sid).aggregate(a=Avg("sentiment_score"))["a"] or 0.0 sentiment_norm = (sentiment + 1) / 2 return 0.4 * tag_hit_normalized + 0.3 * img_sim + 0.2 * sentiment_norm + 0.1 * heat打分时先把各维度归一化到 0 到 1 再加权求和。tag_hit_normalized 来自图谱召回阶段的计数,除以候选集里的最大值完成归一化,score_candidate 直接使用已归一化结果。图像相似度用余弦相似度,liked_vectors 是用户偏好过景点的图像特征向量列表,取均值作为用户偏好的视觉中心。这里加了 1e-8 防止零向量除零,是数值计算里常见的小技巧。
评论情感分数做了 (sentiment + 1) / 2 的归一化,因为情感得分范围是 -1 到 1,要映射到 0 到 1 才能和其他维度相加。热度部分把评论数压到 500 封顶,防止个别超级热门景点靠评论数霸榜。这套打分逻辑不是唯一解,但它把多模态信息都用上了:图像特征覆盖视觉偏好,评论情感覆盖口碑,标签覆盖语义,评分覆盖热度,四种信息缺一种都不会崩,只是排序效果会变化。实际调参时把权重放到 settings 里,答辩时可以现场演示不同权重对结果的影响,这是加分项。另外这种把向量直接存业务表、排序时再取出来算相似度的做法,适合向量规模不大的场景;如果景点图像特征上万条,就该考虑单独的向量数据库了。
5. 实战避坑:从建表到查询的五个高频问题
5.1 迁移文件不生效,数据库一直缺字段
现象:执行 python manage.py makemigrations 提示 No changes detected,但 models.py 里明明新增了字段,迁移到数据库后表结构还是旧的。这是刚上手 Django 的人和数据库打交道最常见的翻车现场。
原因:Django 迁移机制靠的是应用目录下的 migrations 文件夹,它会把模型状态和上一次迁移文件做对比。问题通常出在两个地方:一是新增的应用没有注册进 INSTALLED_APPS,Django 根本没扫描到这个应用的模型;二是新建的 app 里没有 migrations 目录,makemigrations 找不到参照状态就直接跳过。
解决:先检查 settings.py 里 INSTALLED_APPS 有没有这个应用,没有就补上;确认后再跑 python manage.py makemigrations users --name add_scenic_images,指定应用名和迁移名,强制生成迁移文件。最后 python manage.py migrate --plan 查看将要执行的迁移,确认包含目标表再执行。这个 --plan 参数是排查迁移问题的利器,能看到 Django 眼里数据库的预期状态。
5.2 Neo4j 驱动连接超时,图数据写入一半就断
现象:批量导入 Cypher 时,跑到几千条突然报 ServiceUnavailable 或 Session expired,重跑一次又在前面的位置断掉,数据写入多少完全看运气。
原因:这个系统的 Neo4j 连接用的是官方 Python 驱动,默认的事务超时时间比较短。大批量写操作耗时超过服务器配置的 db.transaction.timeout,事务被强制中断。另一个隐蔽原因是连接池里的会话没有正确关闭,驱动达到最大连接数后,新请求排队等待,排队时间一长就直接超时。
解决:写入时控制批大小,每批 1000 到 2000 条,一批提交完再发下一批,不要一个会话里塞几万条数据。同时显式设置 neo4j 驱动的连接池参数:max_connection_pool_size 和 connection_acquisition_timeout。写完后记得 session.close(),不要依赖垃圾回收。这套源码后来加了批处理和连接复用,导入速度反而比原来一次性发送更快。
5.3 ORM 查询 N+1,推荐接口慢到用户投诉
现象:推荐接口返回 50 个景点,响应时间两秒多,日志里 SQL 查询数量上千条。表面看是数据库慢,实际是查询次数太多。
原因:这是经典的 ORM N+1 问题。获取候选景点列表后,对每个景点又单独查图片、查评论、查城市,50 个景点就是 50 次图片查询加 50 次评论查询。Django ORM 默认惰性加载,外键关联不会主动带上,只有模板或序列化器访问到关联字段时才发查询。
解决:用 select_related 和 prefetch_related 提前加载。select_related 适用于外键一对一的场景,一次 JOIN 把关联对象查出来;prefetch_related 适用于反向外键和多对多,它先查主表再批量查关联表,最后在内存里做匹配。推荐接口一般在查询景点时加 .select_related("city").prefetch_related("images", "comments")。这个优化在数据量小的时候看不出差别,景点上千之后就是天壤之别,属于必须先做的优化项。
5.4 CSV 中文乱码,pandas 读出来全是乱字符
现象:Windows 上导出的 CSV,在 Linux 服务器上用 pandas 读,景点名称变成“鏄寒”之类无法辨认的字符,导入 MySQL 后前端显示乱码。
原因:Excel 默认按 GBK 编码保存 CSV,而 pandas 在 Linux 环境下默认按 utf-8 猜测,两者对不上就产生乱码。这不是数据问题,是编码声明问题。更隐蔽的是带 BOM 的 utf-8 文件,utf-8 读取时 BOM 会被当成不可见字符拼进第一列字段名,导致 DictReader 取不到对应的 key。
解决:统一在读文件时指定 encoding="utf-8-sig"。utf-8-sig 能兼容带 BOM 和不带 BOM 的 utf-8 文件,对 GBK 文件则要先转码。处理步骤是先用 chardet 检测文件编码,再用检测结果读取并转存为 utf-8。数据库连接串里也要加上 charset=utf8mb4,否则即使 CSV 正确,MySQL 端也可能因为连接字符集不对把中文改坏。这三个地方都做对,中文才不会在管线上某个环节突然变成问号。
5.5 新用户没有交互记录,图谱推荐直接空结果
现象:注册一个新账号,推荐接口返回空数组,前端页面一片空白。用户还没产生任何行为,图谱里没有 PREFER 关系,候选集查询自然查不到东西。
原因:纯粹的图谱推荐依赖偏好关系,新用户没有偏好,这就是冷启动问题。这套系统在数据层面没有为新用户预置任何引导,所以直接查路径是查不到候选的。很多项目在这里的处理是退化成热门推荐,但这套源码做了更细的区分。
解决:注册流程里加入偏好标签选择,至少选三个标签,系统为每个标签建一条 User 到 Tag 的 INTERESTED_IN 关系。候选集查询里增加一条路径:User 通过 INTERESTED_IN 到 Tag,Tag 再关联到景区。这样新用户第一次进推荐页就有结果,而且结果与所选标签强相关。没有标签数据的用户,兜底逻辑是按评分和评论数取热门 Top-N。冷启动不解决,推荐系统在演示阶段就会出洋相,这一步在初始化数据时就要埋好。
6. 部署验证与线上小技巧:从测试到眼见为实
6.1 用少量数据做回归验证
推荐系统这类项目,最怕的是算法逻辑正常但数据没灌对,结果推荐结果看起来像“玄学”。这套源码带了一组很小的测试数据:三个用户、六个景点、若干标签和评论。用这套数据跑推荐接口,预期结果是能手工推算的,比拿完整数据验证高效得多。验证逻辑就三步:调用接口、核对返回景点是否与图谱路径关联、检查排序是否符合权重预期。
python manage.py test rec.tests.RecommendApiTests除了自动化测试,我习惯再做一次手动可视化验证。把用户偏好过的景点和推荐结果景点分别导出,在 Neo4j Browser 里执行路径查询,肉眼确认存在关联链路。这一步虽然土,但能发现 Cypher 路径方向写反、候选集带回已消费景点这类隐性 bug。真实项目里我遇到过接口分数都对,但 Cypher 里把关系方向写反,导致推荐的全是同类但用户明确不喜欢的东西,这种问题靠单元测试很难暴露,可视化路径一眼就能看出来。
6.2 部署前要过的三道闸
上服务器前,配置层面有三件事必须做。第一,DEBUG 必须改成 False,否则报错页面会泄露源码路径和数据库配置,答辩演示现场如果弹出 Debug 页,场面会很难看。第二,ALLOWED_HOSTS 要填上服务器的域名或 IP,不然 Django 直接拒绝请求返回 400。第三,Neo4j 和 MySQL 的连接配置从环境变量读取,数据库密码不写进仓库。我把这三项做成一个 check 清单,每部署一台机器就照着过一遍。
这套源码和数据库实现我已经打包好,下载后按顺序执行迁移、导入、启动三步就能跑起来,推荐接口、图谱可视化、管理命令这三块我都实际跑通过。从那以后我每次交付 Django 项目,都强制走一遍这套验证和部署流程,它帮我拦住过好几次严重翻车。希望帮到你。
本文还有配套的精品资源,点击获取