先想一个常被问的问题拿到“Python旅游景点信息可视化系统”这类题目很多人的第一反应是上网找一套源码改改名字、换换图片然后交差。这个做法短期应付可以但一旦被问到“表结构怎么设计的”“图表数据从哪来”“大模型接口怎么对接的”很容易露馅。更值得做的思路是把这套系统当成一条完整的数据链路来理解数据采集、清洗入库、Django 业务层、可视化展示、大模型智能问答。每一层都有明确的技术选型和踩坑点。这篇文章就按这条链路展开把环境搭建、数据清洗、模型设计、图表渲染、DeepSeek 接入和常见排查一次讲透帮你真正跑通一个能演示、能说清原理的景点信息可视化系统。需要说明的是本文涉及的爬虫采集只适用于公开、合法授权的数据来源不建议采集任何带有版权限制或用户隐私的内容涉及生产部署、数据库操作时务必在个人测试环境验证并做好备份。1. 这个系统到底在解决什么问题旅游景点信息有个典型特点数据分散且非结构化。携程有评分、马蜂窝有游记、高德有地理坐标、景区官网有开放时间但这些信息互相割裂。游客想规划一条线路往往要开五六个页面来回比对景区运营方想了解游客偏好也只能靠零星问卷。这个可视化系统要解决的就是把分散的景点数据统一到一个平台里再通过两个出口把数据价值释放出来第一是可视化出口。通过柱状图、饼图、地图散点、词云等方式把“哪些城市景点最多”“哪些景区评分最高”“游客人均消费集中在什么区间”这类问题直观呈现。第二是智能问答出口。用户用自然语言提问比如“北京哪些景点评分最高”系统先基于本地景点数据库做检索再由大模型生成回答这样模型不会凭空编造回答有数据支撑。从技术栈看Django 在这里的角色不只是网站框架而是整个系统的“数据中枢”。它负责数据建模、业务查询、JSON 接口输出同时也作为大模型调用和前端图表渲染之间的桥梁。这也是为什么推荐用 Django 而不是 FlaskDjango 自带 ORM、Admin 后台、迁移机制对数据密集型系统来说少写很多底层代码。2. 系统整体架构与核心概念2.1 分层架构一个标准的旅游景点信息可视化系统建议分成四层层级职责涉及技术数据层数据采集、清洗、存储requests、pandas、SQLite/MySQL业务层数据处理与接口输出Django ORM、Django REST Framework展示层可视化图表与页面交互ECharts、Bootstrap、jQuery智能层大模型问答与推荐DeepSeek API、Agent 编排层与层之间通过明确的数据结构连接。数据层产出标准化的景点数据表业务层通过 ORM 查询后序列化成 JSON展示层拿到 JSON 后渲染图表智能层在调用大模型之前先从数据库查询结果拼装上下文。2.2 Django 的 MVT 设计模式MVT 是 Django 的核心设计模式很多初学者容易把它和 MVC 搞混。理解它的关键在于数据流向Model定义数据结构对应数据库表。比如景点表、城市表、评论表。View接收请求、处理业务逻辑、返回响应。这里的 View 对应 MVC 中的 Controller。Template负责渲染 HTML 页面。对应 MVC 中的 View。用户请求一个 URLDjango 先通过 URLconf 找到对应的 View 函数View 从 Model 中查询数据再把数据传递给 Template 渲染成页面返回给用户。这个流程理解透了后面写接口和页面会非常顺手。2.3 数据挖掘与可视化在本项目中的边界项目标题出现“数据挖掘”但实际毕设场景中不需要做复杂的机器学习算法。真正有价值的数据挖掘工作集中在三块数据清洗处理缺失值、重复值、异常值。统计聚合按城市、按评分区间、按消费水平分组统计。关联分析分析景点类型与评分之间的关系。可视化则是这些分析结果的最直观表达方式。没有分析的数据是死数据没有可视化的分析不便于理解。两者配合才让系统有了“数据分析”的属性。3. 环境准备与依赖安装3.1 版本选型原则项目开发中版本并不是越新越好。Django 的版本迭代非常频繁某些第三方库可能还没有适配最新版本。更稳妥的做法是选择一个自己熟悉的稳定版本并锁定关键依赖版本号。本文演示以 Django 4.x 的通用思路为准你本地的实际版本以你创建项目时安装的为准。推荐使用虚拟环境避免全局安装导致依赖冲突。3.2 创建虚拟环境与安装依赖以下以 Windows 系统为例macOS/Linux 命令基本相同。# 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境macOS/Linux source venv/bin/activate # 安装核心依赖 pip install django pip install pandas pip install requests pip install mysqlclient如果使用 MySQL需要安装mysqlclient或者使用pymysql需要在__init__.py中手动pymysql.install_as_MySQLdb()。如果使用 SQLite则不需要额外安装数据库驱动直接使用 Django 内置配置即可。3.3 创建 Django 项目与应用# 创建项目项目名不受限 django-admin startproject travel_vis # 进入项目目录 cd travel_vis # 创建应用 python manage.py startapp spots创建完成后可以在settings.py的INSTALLED_APPS中加入spotsINSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, spots, ]4. 景点数据获取与数据清洗4.1 数据来源方式景区数据的合法来源主要有三类第一开源数据集网站。如 Kaggle、DataFountain 等平台上的旅游相关公开数据通常有明确授权说明。第二政府开放数据平台。部分省市会发布 A 级景区名录、旅游收入等数据这类数据权威且可自由使用。第三自有爬虫采集。只针对公开页面且必须遵守网站的 robots 协议控制请求频率不采集用户隐私数据。对于演示项目可以先用一份 CSV 格式的公开景点数据作为初始数据源字段大致包括景点名称、城市、省份、评分、门票价格、景点类型、简介、经度、纬度、热门程度。如果暂时拿不到足够真实的数据也可以自己构造一份标准化的演示数据重点在于跑通流程而不是追求数据量级。4.2 pandas 清洗数据示例拿到原始数据后第一步不是急着入库而是先用 pandas 做清洗。这里给出一个相对完整的清洗示例# 文件路径scripts/clean_data.py import pandas as pd # 读取原始数据 raw_df pd.read_csv(data/raw_spots.csv, encodingutf-8) # 去除完全重复的行 raw_df raw_df.drop_duplicates() # 去除关键字段缺失的行 raw_df raw_df.dropna(subset[name, city]) # 填充数值型字段缺失值评分使用平均值填充 raw_df[rating] raw_df[rating].fillna(raw_df[rating].mean()) # 统一评分为 0-5 分制超过 5 按 5 分处理 raw_df[rating] raw_df[rating].clip(0, 5) # 门票价格转数值类型 raw_df[ticket_price] pd.to_numeric(raw_df[ticket_price], errorscoerce).fillna(0) # 去掉简介中的换行和多余空格 raw_df[description] raw_df[description].str.replace(\n, ).str.strip() # 保留筛选后的字段 columns [name, city, province, rating, ticket_price, type, description, longitude, latitude] clean_df raw_df[columns] # 输出清洗后的数据 clean_df.to_csv(data/clean_spots.csv, indexFalse, encodingutf-8) print(清洗完成共保留 {} 条景点数据.format(len(clean_df)))这段代码里有几个细节值得注意drop_duplicates()默认去除所有列完全重复的数据。如果部分数据存在同城同名但信息不同的情况建议按[name, city]作为去重子集保留更完整的那条记录。clip(0, 5)用于处理因为不同数据源评分口径不一致导致的异常值比直接用 replace 更保险。errorscoerce会把无法转换的值变为 NaN再用fillna(0)处理逻辑上更清晰。5. Django 模型设计与数据导入5.1 模型字段设计数据清洗完成后需要设计 Django ORM 模型。一个基础但完整的景点信息表应该覆盖三类信息基础属性、地理信息和业务统计字段。# 文件路径spots/models.py from django.db import models class City(models.Model): name models.CharField(max_length50, uniqueTrue, verbose_name城市名称) province models.CharField(max_length50, verbose_name省份) class Meta: verbose_name 城市 verbose_name_plural 城市 def __str__(self): return self.name class Spot(models.Model): name models.CharField(max_length100, verbose_name景点名称) city models.ForeignKey(City, on_deletemodels.CASCADE, related_namespots, verbose_name所属城市) rating models.FloatField(default0, verbose_name评分) ticket_price models.FloatField(default0, verbose_name门票价格) spot_type models.CharField(max_length50, blankTrue, verbose_name景点类型) description models.TextField(blankTrue, verbose_name简介) longitude models.FloatField(nullTrue, blankTrue, verbose_name经度) latitude models.FloatField(nullTrue, blankTrue, verbose_name纬度) # 该字段可以由评论数量统计而得避免每次实时计算 popularity models.IntegerField(default0, verbose_name热度值) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 景点 verbose_name_plural 景点 ordering [-rating] def __str__(self): return self.name几个设计上的考虑说明一下第一城市单独建模而不是直接在景点表里存一个字符串。好处是查询“某城市所有景点”时走外键关联性能更好数据一致性也更高。第二on_deletemodels.CASCADE表示删除城市时级联删除其下的景点在测试环境很方便但生产环境如果要保留历史数据建议改成PROTECT或SET_NULL。第三热度值单独存储而不是实时计算属于典型的空间换时间思路。5.2 批量导入脚本有了模型和数据文件写一个 Django 管理命令脚本将清洗后的数据导入数据库。推荐使用自定义 management command 而不是在脚本中手动写 ORM 逻辑方便复用。# 文件路径spots/management/commands/import_spots.py import csv from django.core.management.base import BaseCommand from spots.models import City, Spot class Command(BaseCommand): help 从清洗后的 CSV 文件导入景点数据 def handle(self, *args, **options): # 清空旧数据方便重复导入测试 Spot.objects.all().delete() City.objects.all().delete() file_path data/clean_spots.csv city_cache {} with open(file_path, r, encodingutf-8) as f: reader csv.DictReader(f) count 0 for row in reader: city_name row[city].strip() if city_name not in city_cache: city_obj, _ City.objects.get_or_create( namecity_name, defaults{province: row.get(province, ).strip()} ) city_cache[city_name] city_obj else: city_obj city_cache[city_name] Spot.objects.create( namerow[name].strip(), citycity_obj, ratingfloat(row.get(rating) or 0), ticket_pricefloat(row.get(ticket_price) or 0), spot_typerow.get(type, ).strip(), descriptionrow.get(description, ).strip(), longitudefloat(row.get(longitude) or 0), latitudefloat(row.get(latitude) or 0), popularityint(float(row.get(popularity) or 0)), ) count 1 self.stdout.write(self.style.SUCCESS(成功导入 {} 条景点数据.format(count)))这里每次测试导入时先清空旧数据是为了保证可重复执行。如果是生产环境一定要去掉这行删除逻辑改用update_or_create做增量更新。# 执行迁移和导入 python manage.py makemigrations python manage.py migrate python manage.py import_spots在导入过程中如果报错优先排查 CSV 字段名是否与DictReader读取的 header 完全一致。比如系统里字段名是type但 CSV 里写的是景点类型就会导致读不到值。6. 数据分析与可视化实现6.1 分析维度的选取景点数据可视化不是把图表堆在页面上就行了每一个图表都要回答一个具体问题。推荐先从这四个维度入手城市景点数量 Top10回答“哪里旅游资源最丰富”。各景点评分分布回答“景区整体口碑如何”。门票价格区间分布回答“游客的消费门槛集中在哪个区间”。景点类型分布回答“当地热门景点以自然风光还是人文景观为主”。四个维度做下来页面既不会太空也不会因为图表过多导致开发压力过大。6.2 使用 ECharts 渲染图表ECharts 是目前最适合做数据可视化的前端库之一图表交互流畅文档完善引入方式也非常灵活。这里演示如何通过 Django 视图输出 JSON 数据然后在前端页面渲染一个柱状图。后端视图代码# 文件路径spots/views.py import json from django.db.models import Count from django.http import JsonResponse from django.shortcuts import render from spots.models import City, Spot def dashboard(request): return render(request, spots/dashboard.html) def city_spot_stats(request): 统计城市景点数量 Top10 cities City.objects.annotate(spot_countCount(spots)).order_by(-spot_count)[:10] data { categories: [city.name for city in cities], values: [city.spot_count for city in cities], } return JsonResponse(data)路由配置# 文件路径travel_vis/urls.py from django.urls import path from spots import views urlpatterns [ path(admin/, admin.site.urls), path(, views.dashboard, namedashboard), path(api/city-spot-stats/, views.city_spot_stats, namecity_spot_stats), ]前端模板!-- 文件路径templates/spots/dashboard.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title旅游景点数据看板/title !-- 通过静态文件加载 ECharts -- script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idcityChart stylewidth: 800px; height: 400px;/div script srchttps://cdn.jsdelivr.net/npm/jquery3.7.1/dist/jquery.min.js/script script $(function () { $.get(/api/city-spot-stats/, function (data) { var chart echarts.init(document.getElementById(cityChart)); chart.setOption({ title: { text: 城市景点数量 Top10 }, tooltip: {}, xAxis: { type: category, data: data.categories }, yAxis: { type: value }, series: [{ type: bar, data: data.values, itemStyle: { color: #3398DB } }] }); }); }); /script /body /html这个过程中最容易遇到的问题有两个第一个是静态文件加载失败。开发阶段把 ECharts 放到本地 static 目录也是可以的但需要先在 settings.py 中配置 STATIC_URL 和 STATICFILES_DIRS。使用 CDN 是一个简便的选择但部署到内网环境时需要提前下载到本地。第二个是 JSON 接口跨域问题。Django 项目模板默认对同源请求没有跨域限制但如果你前后端分离部署前端 8080 端口Django 8000 端口就需要使用django-cors-headers库配置允许跨域来源。6.3 编写其它统计查询数据可视化的核心是查询能力。多写几个统计查询页面内容就丰富起来了。# 文件路径spots/views.py追加 def rating_distribution(request): 统计评分分布区间 values [] ranges [(0, 2), (2, 3.5), (3.5, 4.2), (4.2, 4.6), (4.6, 5.0)] labels [低分区(0~2), 一般区(2~3.5), 良好区(3.5~4.2), 优秀区(4.2~4.6), 高分区(4.6~5.0)] data [] for r in ranges: count Spot.objects.filter(rating__gter[0], rating__ltr[1]).count() data.append(count) return JsonResponse({labels: labels, values: data}) def price_distribution(request): 统计门票价格区间分布 from django.db.models import Case, When, Value, IntegerField queryset Spot.objects.annotate( price_groupCase( When(ticket_price0, thenValue(0)), When(ticket_price__lt50, thenValue(1)), When(ticket_price__lt100, thenValue(2)), When(ticket_price__lt200, thenValue(3)), defaultValue(4), output_fieldIntegerField(), ) ).values(price_group).annotate(countCount(id)).order_by(price_group) labels [免费, 0~50元, 50~100元, 100~200元, 200元以上] data [0] * 5 for item in queryset: data[item[price_group]] item[count] return JsonResponse({labels: labels, values: data})利用Case/When做区间统计是 Django ORM 的进阶用法。它把 SQL 的 CASE WHEN 翻译成 Python 代码比在 Python 内存里 groupby 更高效数据量大了以后优势更明显。7. 大模型接入基于 DeepSeek 的景点智能问答7.1 为什么要在可视化系统里接入大模型很多人不理解可视化系统已经能展示数据了为什么还要接大模型从用户视角看图表是“被动”的用户需要自己看图、读数据、得出结论。大模型问答则是“主动”的用户直接提问系统返回自然语言答案。两者的使用门槛和体验完全不同。尤其在做课程设计或项目演示时一个“能回答问题的系统”比一组静态图表更能说明项目的完整性和技术深度。从技术视角看Django 后端调用大模型 API 并不复杂关键是设计好“检索增强”的结构让大模型基于本地数据库内容回答而不是凭空发挥。7.2 Agent 在这个场景里的落地方式Agent 并不是一个神秘的框架它的核心思想是把一个大任务拆解成几个步骤每一步让模型或程序执行再根据结果决定下一步。在景点问答场景里一个最简的 Agent 流程可以这样设计接收用户自然语言问题。在景点数据库中检索相关景点信息获得候选结果。将候选结果拼装进提示词模板。调用 DeepSeek 生成最终回答。返回给前端展示。这个流程本质上就是 RAG检索增强生成的简化版。它的好处是答案里的每个景点名称、评分、城市都有本地数据作为依据大模型只负责组织和润色语言极大减少了幻觉问题。7.3 后端 API 调用实现首先安装openaiPython SDK因为 DeepSeek 的接口兼容 OpenAI 格式pip install openai需要说明的是DeepSeek 官方 API 使用https://api.deepseek.com作为 base_url模型名称以deepseek-chat这类实际存在的模型名为准具体请以官方文档为准。以下是调用示例# 文件路径spots/services.py import os from openai import OpenAI from spots.models import City, Spot client OpenAI( api_keyos.environ.get(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com ) def search_related_spots(question): 根据问题关键词在数据库中检索相关景点 keywords [kw.strip() for kw in question.split() if kw.strip()] # 优先按城市匹配再按景点名称匹配 qs Spot.objects.all() if keywords: query None for kw in keywords: if City.objects.filter(name__containskw).exists(): qs qs.filter(city__name__containskw) else: qs qs.filter(name__containskw) return qs[:5] def build_prompt(question, spots): spot_lines [] for idx, spot in enumerate(spots, 1): spot_lines.append( f{idx}. {spot.name}所属城市{spot.city.name}评分{spot.rating} f门票{spot.ticket_price}元类型{spot.spot_type}简介{spot.description[:50]} ) context \n.join(spot_lines) prompt f你是一个旅游助手。请根据以下数据库检索到的景点信息回答用户的问题。 数据库结果 {context} 用户问题{question} 请用简洁、口语化的中文回答。如果检索结果无法回答问题直接说信息不足。 return prompt def ask_deepseek(question): spots search_related_spots(question) prompt build_prompt(question, spots) response client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是一个严谨、可靠的旅游助手。}, {role: user, content: prompt} ], temperature0.3, max_tokens500 ) return response.choices[0].message.content注意事项第一API Key 绝不能硬编码在项目里。建议使用os.environ.get(DEEPSEEK_API_KEY)读取环境变量或在本地使用.env文件管理确保.env不提交到版本库。一旦密钥泄露到公开仓库别人就可以用你的额度调用接口。第二temperature0.3是刻意设置的低参数。问答场景我们希望答案稳定、忠实于数据不希望模型太有“创意”。第三提示词里先给检索数据再给用户问题。这个顺序很重要模型会优先关注上下文里的数据减少幻觉概率。在 Django 视图中调用# 文件路径spots/views.py追加 from django.views.decorators.http import require_POST from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from spots.services import ask_deepseek csrf_exempt require_POST def ai_answer(request): import json body json.loads(request.body) question body.get(question, ) if not question: return JsonResponse({error: 问题不能为空}, status400) try: answer ask_deepseek(question) return JsonResponse({answer: answer}) except Exception as e: return JsonResponse({error: str(e)}, status500)csrf_exempt在测试阶段可以方便地用 Postman 调试但生产环境不建议全局关闭 CSRF更推荐在前端页面通过模板变量获取 CSRF token并放在请求头中。7.4 前端问答输入框!-- 在 dashboard.html 中追加 -- div input typetext idquestion placeholder例如北京有哪些高分景点 / button idaskBtn提问/button /div div idanswerBox stylemargin-top: 10px; padding: 10px; border: 1px solid #ddd; min-height: 60px;/div script $(#askBtn).click(function () { var question $(#question).val(); if (!question) return; $(#answerBox).text(思考中……); $.ajax({ url: /api/ai-answer/, type: POST, contentType: application/json, data: JSON.stringify({question: question}), success: function (data) { $(#answerBox).text(data.answer); }, error: function (xhr) { $(#answerBox).text(请求失败 xhr.responseJSON.error); } }); }); /script路由添加一行即可path(api/ai-answer/, views.ai_answer, nameai_answer),8. 运行流程与效果验证8.1 完整启动步骤第一次从零开始运行整个项目建议按下面的顺序操作每一步都确保能看到预期结果再进行下一步# 1. 激活虚拟环境 venv\Scripts\activate # Windows # source venv/bin/activate # macOS/Linux # 2. 执行数据库迁移 python manage.py makemigrations python manage.py migrate # 3. 导入清洗后的数据 python manage.py import_spots # 4. 启动开发服务器 python manage.py runserver启动成功后浏览器访问http://127.0.0.1:8000/能看到数据看板页面。访问http://127.0.0.1:8000/admin/使用创建超级管理员账号登录python manage.py createsuperuser登录后可以在管理后台看到景点表和城市表的数据也可以直接在后台编辑数据方便调试。8.2 如何确定系统运行正确按这个顺序验证数据导入成功。运行import_spots命令后终端显示导入条数在 Django Admin 后台能看到对应记录。首页图表加载。进入dashboard页面四个图表正常渲染鼠标悬停有 tooltip 提示。JSON 接口正常。直接访问http://127.0.0.1:8000/api/city-spot-stats/浏览器返回 JSON 数据。大模型问答正常。输入问题后先显示“思考中……”然后在 3-10 秒内返回 AI 生成的回答。异常处理正常。输入空问题提交返回{error: 问题不能为空}且状态码 400。如果某个环节失败优先从数据流的角度排查接口返回了什么JSON 结构是否和前端预期一致API 调用是否报错报错信息停在哪个层面8.3 不同数据量下的表现如果导入的数据量很小比如只有 20 条图表依然会渲染但各类型的分布会比较单一。建议演示时至少准备 200 条以上的数据页面效果和说服力会好很多。通过 pandas 造数时注意分布尽量均匀比如评分集中在 3.5~4.5 之间价格集中在免费到 100 元区间这样图表看起来更接近真实情况。9. 常见问题与排查方法9.1 依赖与数据导入阶段问题现象可能原因排查方式解决方案pip install mysqlclient报错缺少编译工具或对应依赖查看报错中的 gcc/cl.exe 提示改用 SQLite 开发或安装对应系统依赖后再试python manage.py migrate报错数据库已存在旧表或有不一致迁移记录查看完整 traceback定位到具体 app 的迁移文件开发环境可删库重建生产环境务必走备份和回滚流程导入 CSV 后中文乱码CSV 编码不是 UTF-8用记事本或 VSCode 查看文件编码统一保存为 UTF-8 编码读取时指定encodingutf-8导入后记录条数与 CSV 行数不一致清洗时drop_duplicates或dropna删除了记录在清洗脚本中打印每步删除了多少行确认清洗规则是否符合预期再重新导入9.2 Django 运行阶段问题现象可能原因排查方式解决方案页面能打开但图表空白JS 报错或接口返回结构不对打开浏览器 F12 控制台查看报错直接访问接口地址对比接口返回 JSON 与前端setOption所需数据是否一致静态文件 404STATIC_URL 或静态文件路径配置错误检查浏览器网络请求状态确认文件是否存在于 static 目录开发环境启用django.contrib.staticfiles生产环境使用 collectstatic大模型 API 调用超时网络不稳定或模型响应慢查看后端日志的超时时间先单独测试 API设置合理的超时参数调用时用异步任务或在前端提示响应时间大模型回答仍出现编造信息检索上下文不充分或提示词约束不够打印build_prompt生成的完整上下文增加检索结果条数增强“只能根据数据库结果回答”的提示Postman 调试接口提示 CSRF 校验失败请求没有附带 CSRF token查看请求头缺少X-CSRFToken字段测试阶段使用csrf_exempt正式前后端分离建议用 Token 认证9.3 性能与安全方面问题现象可能原因排查方式解决方案景点数据有几万条图表加载变慢统计分析接口每次实时全表聚合查看 Django Debug Toolbar 的 SQL 执行时间提前在数据导入阶段算好统计结果存 Redis 或建立索引大模型 API 密钥出现在前后端代码中密钥硬编码或提交到版本库用git log和扫描工具检查仓库历史重置密钥使用环境变量将.env加入.gitignore10. 最佳实践与工程建议10.1 数据处理与安全数据是这个系统最核心的资产处理起来要谨慎。采集阶段只选择明确开放授权的数据源不使用违反条款的采集方法不采集任何个人隐私数据。清洗阶段保留一份原始数据备份清洗逻辑记录在脚本中这样后续数据源更新时可以直接重跑脚本而不是手动改数据库。涉及数据库操作建议遵循最小权限原则。开发环境可以用 root 账号图省事生产环境务必创建专用账号只授予业务库的 SELECT、INSERT、UPDATE、DELETE 权限不要把 DDL 权限开放给应用账号。删除数据前先在备份库验证涉及批量清理时使用事务包住操作rollback是最后的兜底手段。10.2 配置管理项目中有三类配置一定要区分管理业务配置如每页展示条数、图表配色、默认城市。放在 Django settings.py 中即可。环境敏感配置如数据库密码、API 密钥、SECRET_KEY。必须放在环境变量或本地.env文件中绝不提交到版本库。部署配置如端口号、静态文件路径。放在部署脚本或 docker-compose.yml 中。10.3 日志与监控生产环境的 Django 项目日志配置决定排查问题的效率。建议在 settings.py 中加入简单的日志配置LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: INFO, class: logging.FileHandler, filename: logs/travel.log, }, }, root: { handlers: [file], level: INFO, }, }使用logging.getLogger(__name__)在视图中记录关键操作比如导入批次、大模型调用成功与失败次数、异常堆栈。日志文件建议使用 logrotate 按天切割避免单个文件过大。10.4 大模型接入的安全边界大模型接口是一个独立的外部系统接入时必须意识到它不在你的安全边界内。生产环境调用时注意这几点API 密钥由后端持有前端只负责提交问题和展示答案对用户输入做长度校验防止构造极长提示词消耗 token在大模型返回结果展示给用户前增加简单的敏感内容过滤避免生成内容存在合规风险。另外大模型 API 的调用成本和延迟通常远高于普通业务接口。如果有多人同时访问建议加一层本地缓存命中相同问题时直接返回缓存结果减少重复调用。10.5 团队协作与版本管理哪怕是一个人开发也推荐从项目初始化开始就用 Git 管理。提交信息按功能模块写清楚比如feat: 添加景点导入命令、fix: 修复评分统计区间边界。依赖使用requirements.txt锁定版本pip freeze requirements.txt.gitignore至少包含以下内容venv/ __pycache__/ *.pyc .env logs/ data/raw_spots.csv11. 总结与后续学习方向这篇内容用一个完整的旅游景点信息可视化项目把 Django 开发链路走了一遍从数据清洗入库到 ORM 模型设计到 ECharts 可视化展示再到 DeepSeek 大模型问答接入。里面没有特别高深的技术但每一步都有真实的取舍比如为什么城市单独建模、为什么要做 Case/When 区间统计、为什么提示词要先给数据再给问题。这些细节决定系统能否稳定跑起来也决定你在答辩或面试时能不能把逻辑讲清楚。如果后续想继续深入有三个方向值得投入第一个是推荐系统在现有数据基础上根据用户浏览历史和城市偏好给出景点推荐排序第二个是地理可视化用高德地图 API 或者 Leaflet 把景点坐标散点叠加到地图上具备更直观的空间信息表达第三个是用户行为分析采集用户的搜索与浏览记录结合大模型做更个性化的问答推荐。做任何扩展之前先把当前这版跑顺然后挑一个方向在测试环境做好备份后逐步迭代。数据系统的价值不在于技术栈多新而在于每一层数据是否可靠、链路是否清晰以及是否需要时能讲清楚背后逻辑。